fix(risk/report): close Phase 17 KEEP-VERIFY/EXTEND — 6 defects + 1 hidden compile defect (jakarta) + test-coverage blind zone (3 missing tables); add sensitive-access audit + ReportAndRiskPhase17IntegrationTest (7); full regression 488 green

This commit is contained in:
WorkBuddy
2026-09-17 20:42:09 +08:00
parent 556f95a562
commit 307c9efb36
13 changed files with 540 additions and 17 deletions
@@ -6,6 +6,11 @@ import com.danyang.match.platform.audit.entity.AuditLog;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
@@ -61,4 +66,24 @@ public class AuditLogService {
public Map<String, Object> getSensitiveAccessLogs(String operatorId, Integer page, Integer pageSize) {
return listAuditLogs(null, null, operatorId, page, pageSize);
}
/**
* D3(Phase 17):记录后台敏感访问(查看他人风险画像 / 举报详情 / 风险事件列表)。
* AGENTS.md #8 要求敏感数据默认拒绝,查看动作本身必须可溯源。
*/
@Transactional
public void recordSensitiveAccess(String operatorId, String resourceType, String resourceId) {
AuditLog auditLog = new AuditLog();
auditLog.setOperatorId(operatorId);
auditLog.setAction("SENSITIVE_ACCESS");
auditLog.setResourceType(resourceType);
auditLog.setResourceId(resourceId);
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) {
HttpServletRequest req = attrs.getRequest();
auditLog.setIp(req.getRemoteAddr());
String ua = req.getHeader("User-Agent");
auditLog.setUserAgent(ua != null ? (ua.length() > 512 ? ua.substring(0, 512) : ua) : null);
}
auditLogMapper.insert(auditLog);
}
}
@@ -3,6 +3,7 @@ package com.danyang.match.platform.risk.controller;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.risk.dto.ReportProcessDTO;
import com.danyang.match.platform.risk.service.ReportService;
import com.danyang.match.platform.audit.service.AuditLogService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -21,6 +22,7 @@ import java.util.Map;
public class ReportOperatorController {
private final ReportService reportService;
private final AuditLogService auditLogService;
/**
* 获取举报列表(可带 status 筛选)
@@ -63,6 +65,7 @@ public class ReportOperatorController {
public ApiResponse<Map<String, Object>> getReportDetail(
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
auditLogService.recordSensitiveAccess(principal.id(), "report", id);
return ApiResponse.ok(reportService.getReportDetail(id));
}
@@ -99,6 +102,7 @@ public class ReportOperatorController {
public ApiResponse<Map<String, Object>> getUserRiskProfile(
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
auditLogService.recordSensitiveAccess(principal.id(), "user_risk_profile", userId);
return ApiResponse.ok(reportService.getUserRiskProfile(userId));
}
@@ -2,6 +2,7 @@ package com.danyang.match.platform.risk.controller;
import com.danyang.match.platform.risk.entity.RiskEvent;
import com.danyang.match.platform.risk.service.RiskReferralService;
import com.danyang.match.platform.audit.service.AuditLogService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import lombok.RequiredArgsConstructor;
@@ -22,6 +23,7 @@ import java.util.Map;
public class RiskController {
private final RiskReferralService riskReferralService;
private final AuditLogService auditLogService;
/**
* P17-T04: 运营人员手动标记风险
@@ -48,6 +50,7 @@ public class RiskController {
public ApiResponse<List<RiskEvent>> getRiskEvents(
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
auditLogService.recordSensitiveAccess(principal.id(), "risk_event", userId);
return ApiResponse.ok(riskReferralService.getRiskEventsByUser(userId));
}
@@ -11,7 +11,10 @@ import java.util.List;
@Mapper
public interface RiskEventMapper extends BaseMapper<RiskEvent> {
@Select("SELECT * FROM risk_events WHERE user_id = #{userId} ORDER BY created_at DESC LIMIT 20")
// D5(Phase 17):与 sumScoreByUserId / findByEventType 保持一致,过滤软删事件,
// 否则 getUserRiskProfile / getReportDetail 的 recentEvents 会混入已删除事件,
// 与已过滤的风险评分不一致(静默功能失效)。
@Select("SELECT * FROM risk_events WHERE user_id = #{userId} AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 20")
List<RiskEvent> findByUserId(String userId);
@Select("SELECT COALESCE(SUM(score), 0) FROM risk_events WHERE user_id = #{userId} AND deleted_at IS NULL")
@@ -21,6 +21,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.Map;
@@ -383,6 +387,20 @@ public class ReportService {
auditLog.setAction(action);
auditLog.setResourceType(resourceType);
auditLog.setResourceId(resourceId);
auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData);
// D4(Phase 17):补全审计来源(IP / User-Agent),敏感访问与处置审计必须可溯源。
enrichAuditSource(auditLog);
auditLogMapper.insert(auditLog);
}
// D4(Phase 17):从当前请求上下文补全审计的 IP 与 User-Agent(与 AuthorizationAuditListener 同模式)。
private void enrichAuditSource(AuditLog auditLog) {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) {
HttpServletRequest req = attrs.getRequest();
auditLog.setIp(req.getRemoteAddr());
String ua = req.getHeader("User-Agent");
auditLog.setUserAgent(ua != null ? (ua.length() > 512 ? ua.substring(0, 512) : ua) : null);
}
}
}
@@ -5,10 +5,16 @@ import com.danyang.match.platform.risk.entity.RiskEvent;
import com.danyang.match.platform.risk.mapper.RiskActionMapper;
import com.danyang.match.platform.risk.mapper.RiskEventMapper;
import com.danyang.match.platform.shared.constant.Constants;
import com.danyang.match.platform.audit.entity.AuditLog;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.List;
@@ -25,6 +31,7 @@ public class RiskReferralService {
private final RiskEventMapper riskEventMapper;
private final RiskActionMapper riskActionMapper;
private final AuditLogMapper auditLogMapper;
/**
* P17-T01: 咨询内容风险检测
@@ -113,7 +120,10 @@ public class RiskReferralService {
*/
@Transactional
public RiskEvent manualFlagRisk(String userId, String eventType, String note, String createdBy) {
return createRiskEvent(userId, eventType, 5, "manual", createdBy, note);
RiskEvent event = createRiskEvent(userId, eventType, 5, "manual", createdBy, note);
// D2(Phase 17):人工风险标记属处置类操作,必须审计("处置继续审计")。
saveAuditLog(createdBy, "MANUAL_FLAG_RISK", "risk_event", event.getId(), null, null);
return event;
}
/**
@@ -141,4 +151,23 @@ public class RiskReferralService {
log.info("创建风险事件: userId={}, eventType={}, score={}, source={}", userId, eventType, score, source);
return event;
}
// D2(Phase 17):与 ReportService 同模式的审计写入(含 IP / User-Agent 溯源)。
private void saveAuditLog(String operatorId, String action, String resourceType,
String resourceId, String beforeData, String afterData) {
AuditLog auditLog = new AuditLog();
auditLog.setOperatorId(operatorId);
auditLog.setAction(action);
auditLog.setResourceType(resourceType);
auditLog.setResourceId(resourceId);
auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData);
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) {
HttpServletRequest req = attrs.getRequest();
auditLog.setIp(req.getRemoteAddr());
String ua = req.getHeader("User-Agent");
auditLog.setUserAgent(ua != null ? (ua.length() > 512 ? ua.substring(0, 512) : ua) : null);
}
auditLogMapper.insert(auditLog);
}
}