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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -62,11 +62,8 @@ class EntityMigrationParityTest {
|
||||
"operators",
|
||||
"organization_relationships",
|
||||
"permissions",
|
||||
"reports",
|
||||
"review_items",
|
||||
"review_reasons",
|
||||
"risk_actions",
|
||||
"risk_events",
|
||||
"role_permissions",
|
||||
"roles",
|
||||
"sensitive_access_logs",
|
||||
@@ -116,7 +113,13 @@ class EntityMigrationParityTest {
|
||||
"activity_staff_assignments",
|
||||
// Phase 16:活动互选二阶段同意表此前在可选白名单(= 无人测),
|
||||
// 导致双确认→建Match+会话的核心路径零覆盖。已补入 schema-h2.sql。
|
||||
"activity_mutual_connection_consents"
|
||||
"activity_mutual_connection_consents",
|
||||
// Phase 17:举报与风控三表此前全在可选白名单(= 无人测),
|
||||
// 导致 submitReport / detect* / processReport / manualFlagRisk 整条真实 DB 路径零覆盖,
|
||||
// 且 ReportServiceTest 只能写 assertTrue(true) 假绿。已补入 schema-h2.sql。
|
||||
"reports",
|
||||
"risk_events",
|
||||
"risk_actions"
|
||||
);
|
||||
/** BaseEntity 声明的公共列(对照 shared/base/BaseEntity.java)。 */
|
||||
private static final Set<String> BASE_ENTITY_COLUMNS =
|
||||
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
package com.danyang.match.platform.risk;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.danyang.match.platform.audit.entity.AuditLog;
|
||||
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
||||
import com.danyang.match.platform.risk.controller.ReportOperatorController;
|
||||
import com.danyang.match.platform.risk.controller.RiskController;
|
||||
import com.danyang.match.platform.risk.dto.ReportProcessDTO;
|
||||
import com.danyang.match.platform.risk.dto.ReportSubmitDTO;
|
||||
import com.danyang.match.platform.risk.entity.RiskAction;
|
||||
import com.danyang.match.platform.risk.entity.RiskEvent;
|
||||
import com.danyang.match.platform.risk.entity.Report;
|
||||
import com.danyang.match.platform.risk.mapper.ReportMapper;
|
||||
import com.danyang.match.platform.risk.mapper.RiskActionMapper;
|
||||
import com.danyang.match.platform.risk.mapper.RiskEventMapper;
|
||||
import com.danyang.match.platform.risk.service.ReportService;
|
||||
import com.danyang.match.platform.risk.service.RiskReferralService;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 17(举报与风控)真实 DB 集成测试。
|
||||
*
|
||||
* 背景:此前 risk_events / reports / risk_actions 三张表在 H2 测试库完全缺失(只在
|
||||
* TEST_SCHEMA_OPTIONAL_TABLES 白名单里 = 无人测),导致 ReportService / RiskReferralService
|
||||
* 整条真实 DB 路径零覆盖,ReportServiceTest 只能写 assertTrue(true) 假绿。
|
||||
*
|
||||
* 本测试在补齐三表后,真实验证:
|
||||
* - D1:submitReport 写入 reports + risk_events(abuse_report);三来源 detect* 写入对应风险事件;
|
||||
* 处置封禁写入 risk_actions(ban)。
|
||||
* - D2:manualFlagRisk(人工标记)写入 MANUAL_FLAG_RISK 审计。
|
||||
* - D3:后台敏感访问(查看风险画像 / 举报详情 / 风险事件列表)写入 SENSITIVE_ACCESS 审计。
|
||||
* - D4:submitReport / processReport 写入审计(PROCESS_REPORT 等),且在无请求上下文时 ip/userAgent 为 null 不崩。
|
||||
* - D5:RiskEventMapper.findByUserId 过滤软删事件,与 sumScoreByUserId 一致。
|
||||
* - 最小可见:getUserRiskProfile 不把咨询正文(userRequestSummary)等无关字段带出。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class ReportAndRiskPhase17IntegrationTest {
|
||||
|
||||
@MockBean
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private ReportService reportService;
|
||||
@Autowired
|
||||
private RiskReferralService riskReferralService;
|
||||
@Autowired
|
||||
private ReportMapper reportMapper;
|
||||
@Autowired
|
||||
private RiskEventMapper riskEventMapper;
|
||||
@Autowired
|
||||
private RiskActionMapper riskActionMapper;
|
||||
@Autowired
|
||||
private AuditLogMapper auditLogMapper;
|
||||
@Autowired
|
||||
private ReportOperatorController reportOperatorController;
|
||||
@Autowired
|
||||
private RiskController riskController;
|
||||
|
||||
private String auditCount(String action) {
|
||||
return String.valueOf(auditLogMapper.selectCount(
|
||||
new QueryWrapper<AuditLog>().eq("action", action)));
|
||||
}
|
||||
|
||||
// ==================== D1:举报提交写入 reports + risk_events ====================
|
||||
|
||||
@Test
|
||||
void submitReportCreatesReportAndAbuseRiskEvent() {
|
||||
ReportSubmitDTO dto = new ReportSubmitDTO();
|
||||
dto.setReportedUserId("reported-phase17-1");
|
||||
dto.setReasonType(Constants.REPORT_REASON_FRAUD);
|
||||
dto.setDescription("诈骗测试描述");
|
||||
|
||||
Map<String, Object> result = reportService.submitReport("reporter-phase17-1", dto);
|
||||
String reportId = (String) result.get("reportId");
|
||||
assertNotNull(reportId, "举报应返回 reportId");
|
||||
|
||||
Report report = reportMapper.selectById(reportId);
|
||||
assertNotNull(report, "reports 表应有真实写入");
|
||||
assertEquals("reported-phase17-1", report.getReportedUserId());
|
||||
assertEquals(Constants.REPORT_STATUS_PROCESSING, report.getStatus(),
|
||||
"诈骗类举报应自动置为 processing");
|
||||
|
||||
List<RiskEvent> events = riskEventMapper.findByUserId("reported-phase17-1");
|
||||
assertEquals(1, events.size(), "被举报人应有一条 abuse_report 风险事件");
|
||||
RiskEvent ev = events.get(0);
|
||||
assertEquals(Constants.RISK_EVENT_ABUSE_REPORT, ev.getEventType());
|
||||
assertEquals("report", ev.getSource());
|
||||
assertEquals(reportId, ev.getSourceId(), "风险事件应溯源到举报 ID");
|
||||
}
|
||||
|
||||
// ==================== D1:三来源接入统一 RiskEvent ====================
|
||||
|
||||
@Test
|
||||
void threeSourcesCreateUnifiedRiskEvents() {
|
||||
assertEquals("high", riskReferralService.detectCounselingRisk("u-counsel-17", "我想要自杀"));
|
||||
assertEquals("high", riskReferralService.detectAppointmentRisk("u-appt-17", "带刀赴约报复"));
|
||||
assertEquals("high", riskReferralService.detectActivityRisk("u-act-17", "现场暴力冲突", null));
|
||||
|
||||
List<RiskEvent> c = riskEventMapper.findByUserId("u-counsel-17");
|
||||
List<RiskEvent> a = riskEventMapper.findByUserId("u-appt-17");
|
||||
List<RiskEvent> ac = riskEventMapper.findByUserId("u-act-17");
|
||||
|
||||
assertEquals(1, c.size());
|
||||
assertEquals(Constants.RISK_EVENT_COUNSELING_RISK, c.get(0).getEventType());
|
||||
assertEquals(Constants.RISK_SOURCE_COUNSELING, c.get(0).getSource());
|
||||
|
||||
assertEquals(1, a.size());
|
||||
assertEquals(Constants.RISK_EVENT_APPOINTMENT_RISK, a.get(0).getEventType());
|
||||
assertEquals(Constants.RISK_SOURCE_SERVICE_STATION, a.get(0).getSource());
|
||||
|
||||
assertEquals(1, ac.size());
|
||||
assertEquals(Constants.RISK_EVENT_ACTIVITY_RISK, ac.get(0).getEventType());
|
||||
assertEquals(Constants.RISK_SOURCE_OFFLINE_ACTIVITY, ac.get(0).getSource());
|
||||
}
|
||||
|
||||
// ==================== D1 + D4:处置封禁写入 risk_actions(ban) + 审计 ====================
|
||||
|
||||
@Test
|
||||
void processReportBanCreatesRiskActionAndAudit() {
|
||||
ReportSubmitDTO dto = new ReportSubmitDTO();
|
||||
dto.setReportedUserId("reported-phase17-ban");
|
||||
dto.setReasonType(Constants.REPORT_REASON_FRAUD);
|
||||
dto.setDescription("封禁测试");
|
||||
String reportId = (String) reportService.submitReport("reporter-phase17-ban", dto).get("reportId");
|
||||
|
||||
long auditBefore = auditLogMapper.selectCount(
|
||||
new QueryWrapper<AuditLog>().eq("action", "PROCESS_REPORT"));
|
||||
|
||||
ReportProcessDTO pdto = new ReportProcessDTO();
|
||||
pdto.setProcessResult(Constants.PROCESS_RESULT_BAN);
|
||||
pdto.setProcessNote("确认诈骗,予以封禁");
|
||||
reportService.processReport(reportId, pdto, "op-phase17-ban");
|
||||
|
||||
List<RiskAction> actions = riskActionMapper.findActiveByUserId("reported-phase17-ban");
|
||||
assertEquals(1, actions.size(), "应有 1 条 active ban 处置");
|
||||
assertEquals(Constants.RISK_ACTION_BAN, actions.get(0).getActionType());
|
||||
|
||||
long auditAfter = auditLogMapper.selectCount(
|
||||
new QueryWrapper<AuditLog>().eq("action", "PROCESS_REPORT"));
|
||||
assertEquals(auditBefore + 1, auditAfter, "PROCESS_REPORT 审计应新增一条");
|
||||
}
|
||||
|
||||
// ==================== D2:人工风险标记必须审计 ====================
|
||||
|
||||
@Test
|
||||
void manualFlagRiskWritesAudit() {
|
||||
long before = Long.parseLong(auditCount("MANUAL_FLAG_RISK"));
|
||||
riskReferralService.manualFlagRisk("u-manual-17", "manual_flag", "运营人工标记", "op-manual-17");
|
||||
long after = Long.parseLong(auditCount("MANUAL_FLAG_RISK"));
|
||||
assertEquals(before + 1, after, "人工风险标记应写入 MANUAL_FLAG_RISK 审计");
|
||||
|
||||
AuditLog log = auditLogMapper.selectOne(
|
||||
new QueryWrapper<AuditLog>().eq("action", "MANUAL_FLAG_RISK")
|
||||
.eq("operator_id", "op-manual-17"));
|
||||
assertNotNull(log, "审计记录应可溯源到操作人");
|
||||
}
|
||||
|
||||
// ==================== D3:后台敏感访问必须审计 ====================
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void operatorRiskEndpointsRecordSensitiveAccessAudit() {
|
||||
OperatorPrincipal principal =
|
||||
new OperatorPrincipal("op-sensitive-17", Set.of("risk.report.view"));
|
||||
// 直接调用代理 bean 方法会经过 @PreAuthorize 方法安全拦截器,
|
||||
// 必须在 SecurityContext 中装配含对应权限的 Authentication。
|
||||
List<GrantedAuthority> authorities = principal.permissions().stream()
|
||||
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(principal, null, authorities));
|
||||
|
||||
// 准备一条真实存在的举报,供 getReportDetail 业务层读取(不存在会抛异常)。
|
||||
ReportSubmitDTO rdto = new ReportSubmitDTO();
|
||||
rdto.setReportedUserId("reported-sensitive-17");
|
||||
rdto.setReasonType(Constants.REPORT_REASON_FRAUD);
|
||||
rdto.setDescription("敏感访问审计测试");
|
||||
String reportId = (String) reportService.submitReport("reporter-sensitive-17", rdto)
|
||||
.get("reportId");
|
||||
|
||||
long before = Long.parseLong(auditCount("SENSITIVE_ACCESS"));
|
||||
|
||||
riskController.getRiskEvents(principal, "u-sensitive-x");
|
||||
reportOperatorController.getUserRiskProfile(principal, "u-sensitive-y");
|
||||
reportOperatorController.getReportDetail(principal, reportId);
|
||||
|
||||
long after = Long.parseLong(auditCount("SENSITIVE_ACCESS"));
|
||||
assertEquals(before + 3, after, "三个敏感访问端点都应写入 SENSITIVE_ACCESS 审计");
|
||||
}
|
||||
|
||||
// ==================== D5:findByUserId 过滤软删事件 ====================
|
||||
|
||||
@Test
|
||||
void findByUserIdFiltersSoftDeletedEvents() {
|
||||
RiskEvent live = new RiskEvent();
|
||||
live.setId(UUID.randomUUID().toString());
|
||||
live.setUserId("u-filter-17");
|
||||
live.setEventType("counseling_risk");
|
||||
live.setScore(5);
|
||||
live.setSource("counseling");
|
||||
live.setCreatedAt(Instant.now());
|
||||
riskEventMapper.insert(live);
|
||||
|
||||
RiskEvent deleted = new RiskEvent();
|
||||
deleted.setId(UUID.randomUUID().toString());
|
||||
deleted.setUserId("u-filter-17");
|
||||
deleted.setEventType("activity_risk");
|
||||
deleted.setScore(5);
|
||||
deleted.setSource("offline_activity");
|
||||
deleted.setCreatedAt(Instant.now());
|
||||
deleted.setDeletedAt(Instant.now());
|
||||
riskEventMapper.insert(deleted);
|
||||
|
||||
List<RiskEvent> events = riskEventMapper.findByUserId("u-filter-17");
|
||||
assertEquals(1, events.size(), "软删事件不应出现在查询中");
|
||||
assertEquals("counseling_risk", events.get(0).getEventType());
|
||||
}
|
||||
|
||||
// ==================== 最小可见:风险画像不泄露无关咨询正文 ====================
|
||||
|
||||
@Test
|
||||
void userRiskProfileDoesNotLeakCounselingDetail() {
|
||||
// 该接口只返回风险统计/事件/处置,不返回 Counseling 正文(userRequestSummary)。
|
||||
Map<String, Object> profile = reportService.getUserRiskProfile("u-minvis-17");
|
||||
assertNotNull(profile);
|
||||
assertFalse(profile.containsKey("userRequestSummary"),
|
||||
"风险画像不得携带咨询正文原文");
|
||||
assertFalse(profile.containsKey("counselingDetail"),
|
||||
"风险画像不得携带咨询正文原文");
|
||||
}
|
||||
}
|
||||
@@ -358,3 +358,56 @@ CREATE TABLE matchmaker_followups (
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Phase 17:举报与风控三表(与 V012__add_risk_and_report_tables.sql 生产 DDL 对齐)。
|
||||
-- 此前完全缺失 → ReportService / RiskReferralService 整条真实 DB 路径零覆盖,
|
||||
-- 且 ReportServiceTest 只能写 assertTrue(true) 假绿。已补入并移出 TEST_SCHEMA_OPTIONAL_TABLES 白名单。
|
||||
-- 注意:H2 无 operators 表,故去掉生产 DDL 中的 REFERENCES operators(id) 外键;
|
||||
-- 保留 status / action_type 的 CHECK 约束,防止非法取值在测试期被静默放过(同 Phase 14/15/16 原则)。
|
||||
CREATE TABLE reports (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
reporter_user_id VARCHAR(36) NOT NULL,
|
||||
reported_user_id VARCHAR(36),
|
||||
reason_type VARCHAR(64) NOT NULL,
|
||||
description CLOB,
|
||||
evidence_file_ids CLOB,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
process_result VARCHAR(64),
|
||||
processed_by VARCHAR(36),
|
||||
processed_at TIMESTAMP WITH TIME ZONE,
|
||||
process_note CLOB,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT chk_report_status CHECK (status IN ('pending', 'processing', 'resolved', 'rejected', 'dismissed'))
|
||||
);
|
||||
|
||||
CREATE TABLE risk_events (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
source VARCHAR(32) NOT NULL DEFAULT 'auto',
|
||||
source_id VARCHAR(128),
|
||||
note CLOB,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE risk_actions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
action_type VARCHAR(64) NOT NULL,
|
||||
reason VARCHAR(256) NOT NULL,
|
||||
reason_detail CLOB,
|
||||
created_by VARCHAR(36) NOT NULL,
|
||||
effective_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT chk_risk_action_type CHECK (action_type IN ('warning', 'feature_limit', 'temporary_suspend', 'ban')),
|
||||
CONSTRAINT chk_risk_action_status CHECK (status IN ('active', 'resolved', 'expired'))
|
||||
);
|
||||
|
||||
|
||||
+14
-3
@@ -16,7 +16,10 @@
|
||||
> `RelationshipService.confirmActivityInterest`,修复 **7 处缺陷 + 1 处隐藏编译缺陷 + 1 处测试保真度缺陷**,新建
|
||||
> `ActivityMutualConsentServiceTest`(7 用例),并经负向验证证明守卫有牙。新增 V041 `expires_at`+`match_id`。
|
||||
> 证据:`docs/Phase-16-V11-Recovery-Completion-Report-2026-09-17.md`。
|
||||
> **Phase 17 需用户确认后启动。**
|
||||
>
|
||||
> **2026-09-17 Phase 17 复验关闭:** EPIC-P17-V11 已完成。复验发现风控/举报整条真实 DB 路径因测试库缺 `reports`/`risk_events`/`risk_actions` 三表而**零覆盖**,`ReportServiceTest` 全是 `assertTrue(true)` 假绿;且敏感访问(查看他人风险画像/举报)此前**无审计**(违反 AGENTS.md #8 默认拒绝 + 查看需审计);另修复审计缺 `ip`/`userAgent`、`findByUserId` 未过滤软删、人工标记无审计,以及 `javax`→`jakarta` 隐藏编译缺陷与 `risk_events` 漏 `updated_at` 列。共修复 **6 处真实缺陷**,补齐三表并新增 `ReportAndRiskPhase17IntegrationTest`(7 例),负向验证证明守卫有牙。全量回归 **488 tests 全绿**。
|
||||
> 证据:`docs/Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`。
|
||||
> **Phase 18 需用户确认后启动。**
|
||||
|
||||
> **2026-09-16 状态校正:** Phase 07 UI 已按 2026-09-13 规格重新实现,**首批 11 页于 2026-09-16 通过用户验收(结论:基本达标)**,本文件此前"视觉否决/REOPENED"口径随之关闭。
|
||||
>
|
||||
@@ -137,6 +140,7 @@
|
||||
| Phase 14 多类型公益活动复验 | 已完成(2026-09-17) | 见 `Phase-14-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **11 处后端缺陷**(**D1 `ActivityMapper.countRemainingCapacity` 无 SQL 空壳致详情/报名/审核必崩**、**D2 报名与审核先读后写并发超卖** → 改原子 SQL、D3 类型/角色未校验生产 CHECK 白名单、**D4 DTO 无 `activityType` 致小程序分类 Tabs 恒空**、D5 男女人数 `approvedCount/2` 编造、D6 年龄门槛空壳、D7 按类型资格策略为空、**D8 运营端建活动丢弃类型且把主办组织错设为操作者 userId**、D9 三个运营端点错挂用户端双实现、D10 软删活动/缺字段 NPE、D11 DTO 缺 4 字段)+ **3 处前端契约缺陷**(分类 code `classroom` 不在白名单、卡片读 7 个不存在字段、admin-web 无活动管理页)+ **2 处测试保真度缺陷**(2 条占位假绿、测试库缺 V023 列与三张表);新增 `ActivityPhase14Test`(23)+ `ActivityConstraintGuardTest`(9,解析真实 DDL **与前端源码**),两轮负向验证证明守卫会红;admin-web `vue-tsc` 0 error + `vite build` 通过;后端 **450 tests 全绿**;**Phase 15 尚未获准** |
|
||||
| Phase 15 活动报名审核签到复验 | 已完成(2026-09-17) | 见 `Phase-15-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **16 处缺陷**(**D1 签到码一生成即作废**:`generateCheckinToken` 把 token 写 `"used"` 而扫码端见 `"used"` 即拒 → 扫码 100% 失败;**D9 取消/拒绝后永远无法再报名**:撞生产 `UNIQUE(activity_id,user_id)`,测试库缺该约束故零覆盖 → 改复用记录;D2 重复签到竞态、D3 签到无数据范围、D4 手工签到无校验、D5 统计 NPE+语义错、D6 非法 token 500、D7 签名非恒定时间比较、D10 后台审核列表无 pending、D11 报名窗口不生效、D12 取消无审计、D13 审核/取消非原子、D14 容量分组不参与扣减)+ **2 处前端契约缺陷**(admin-web 无签到工作台、小程序无取消报名入口);新增 V040 `activity_staff_assignments` 落实 INV-020 活动数据范围;**3 处测试保真度缺陷**(测试库缺 UNIQUE+2 CHECK、3 条 `assertTrue(true)` 假绿、表未入必选集合);新增 `ActivityPhase15Test`(21),`CheckinServiceTest` 假绿改真实守卫(5),约束守卫 +2,迁移基线 39→40;一轮负向验证 3 处注入 → 8 条用例精准变红并还原;admin-web `vue-tsc` 0 error + `vite build` 通过;后端 **474 tests 全绿**;**Phase 16 尚未获准** |
|
||||
| Phase 16 活动互选二阶段同意复验 | 已完成(2026-09-17) | 见 `Phase-16-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **7 处缺陷 + 1 处隐藏编译缺陷 + 1 处测试保真度缺陷**(**D1 双确认后根本不建 Match/会话(Gate 缺陷·INV-014/015)**、**D3 Phase 15 提交态引用未定义 `reopenEpisode` 致整个模块不编译("474 绿"为假象)**、D4 INV-006 单向意向泄露、D5 复用/新建 Match 后缺会话);统一双实现到 `RelationshipService.confirmActivityInterest`;`MatchMapper` 补 `reopenEpisode`;测试库补 `activity_mutual_connection_consents` 表并移入必选集合,`MigrationValidationTest` 基线 40→41;新建 `ActivityMutualConsentServiceTest`(7 用例);`ChatPhase09IntegrationTest:344` 断言对齐 AGENTS.md #20 复用语义;新增 V041 `expires_at`+`match_id`;负向验证 2 用例精准变红并还原;后端 **481 tests 全绿**;**Phase 17 尚未获准** |
|
||||
| Phase 17 举报与风控复验 | 已完成(2026-09-17) | 见 `Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **6 处真实缺陷**(**D1(Critical,必崩级盲区)测试库缺 `reports`/`risk_events`/`risk_actions` 三表 → 风控/举报真实 DB 路径零覆盖,`ReportServiceTest` 全 `assertTrue(true)` 假绿**;**D3(High)敏感访问(查看他人风险画像/举报)无审计(违反 AGENTS.md #8)**;D4 审计缺 `ip`/`userAgent`;D5 `findByUserId` 未过滤软删;D2 人工标记无审计;**D6 `javax`→`jakarta` 隐藏编译缺陷 + `risk_events` 漏 `updated_at` 列**);补齐三表并移出白名单入 `REQUIRED_IN_TEST_SCHEMA`;新增 `AuditLogService.recordSensitiveAccess` 并接线三端点;新增 `ReportAndRiskPhase17IntegrationTest`(7 用例);负向验证(注释 getReportDetail 审计 → D3 守卫精准变红并还原);后端 **488 tests 全绿**;**Phase 18 尚未获准** |
|
||||
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
||||
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
||||
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
||||
@@ -174,8 +178,8 @@
|
||||
| EPIC-P13A | P13A | 公益婚恋咨询与婚前辅导 | P1 | ✅
|
||||
| EPIC-P14-V11 | P14 | 多类型公益活动 | P1 |
|
||||
| EPIC-P15-V11 | P15 | 活动签到回归测试 | P1 |
|
||||
| EPIC-P16-V11 | P16 | 活动互选二阶段同意重构 | P0 |
|
||||
| EPIC-P17-V11 | P17 | 公益服务风险来源扩展 | P0 |
|
||||
| EPIC-P16-V11 | P16 | 活动互选二阶段同意重构 | P0 | ✅
|
||||
| EPIC-P17-V11 | P17 | 公益服务风险来源扩展 | P0 | ✅
|
||||
| EPIC-P18-V11 | P18 | 申诉保持 | P1 |
|
||||
| EPIC-P19-V11 | P19 | 善缘丹阳综合运营后台 | P1 |
|
||||
| EPIC-P20-V11 | P20 | 公益项目数据统计扩展 | P1 |
|
||||
@@ -494,6 +498,13 @@
|
||||
- P17-V11-T02 risk source service_station ✅ (RiskReferralService.detectAppointmentRisk)
|
||||
- P17-V11-T03 risk source offline_activity ✅ (RiskReferralService.detectActivityRisk)
|
||||
- P17-V11-T04 risk referral UI ⏳ Admin Web 后续补充(Phase 19)
|
||||
|
||||
### 2026-09-17 KEEP-VERIFY/EXTEND 复验结论
|
||||
- **D1(Critical,必崩级盲区)**:`reports`/`risk_events`/`risk_actions` 三表在 H2 测试库缺失 → 风控/举报真实 DB 路径零覆盖;`ReportServiceTest` 全 `assertTrue(true)` 假绿。已补三表(对齐 V012,去外键留 CHECK)并移出 `TEST_SCHEMA_OPTIONAL_TABLES` 入 `REQUIRED_IN_TEST_SCHEMA`。
|
||||
- **D3(High)**:敏感访问(查看他人风险画像/举报详情/风险事件列表)此前无审计(违反 AGENTS.md #8)。新增 `AuditLogService.recordSensitiveAccess` 并接线 `getReportDetail`/`getUserRiskProfile`/`RiskController.getRiskEvents`。
|
||||
- **D4**:`ReportService.saveAuditLog` 补 `ip`/`userAgent`;**D5**:`findByUserId` 补 `deleted_at IS NULL`;**D2**:`manualFlagRisk` 补 `MANUAL_FLAG_RISK` 审计;**D6**:`javax`→`jakarta` 隐藏编译缺陷 + `risk_events` 漏 `updated_at` 列。
|
||||
- 新增 `ReportAndRiskPhase17IntegrationTest`(7 例);负向验证(注释 getReportDetail 审计 → D3 守卫精准变红并还原);后端 **488 tests 全绿**。
|
||||
- 证据:`docs/Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`
|
||||
---
|
||||
|
||||
## EPIC-P18-V11: 申诉保持 ✅ 已验证(V1.0基础保持不变)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Phase 17 — 举报与风控 KEEP-VERIFY/EXTEND 复验完成报告
|
||||
|
||||
**日期**:2026-09-17
|
||||
**范围**:举报与风控(risk / report / referral 模块 + 后台风险页)
|
||||
**纪律**:KEEP-VERIFY/EXTEND。历史 ✅ 不可信;测试覆盖 ≠ 运行时路径覆盖;逐条核对运行时路径与生产约束;发现真实缺陷即修;补测试保真度;守卫必须解析真实 DDL;离线容器真实编译+执行;每阶段出报告后停止等确认。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论
|
||||
|
||||
**两项 Gate 全部达成,复验关闭。**
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 风险来源可追踪 | ✅ counseling / service_station / offline_activity 三来源均接入统一 `RiskEvent`,且已确认运行时真实调用(非定义未调用) |
|
||||
| 正文最小可见 | ✅ 后台风险画像只带必要来源上下文,不暴露无关咨询正文(INV-006 同族约束延续) |
|
||||
| 低权限人员不能扩大读取范围 | ✅ 禁止公开黑名单;敏感访问全部审计(AGENTS.md #8 敏感数据默认拒绝 + 查看需审计) |
|
||||
|
||||
**本轮修复真实缺陷 6 处(含 1 处必崩级测试盲区、1 处隐藏编译缺陷)**,新增集成测试 7 例。
|
||||
|
||||
- 全量回归:**488 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(约 4 分)
|
||||
- 基线 481 → 488,**净 +7**(Phase 17 集成测试 7 方法)
|
||||
- 负向验证:临时注释 `getReportDetail` 敏感访问审计 → `operatorRiskEndpointsRecordSensitiveAccessAudit` 精准变红,已还原
|
||||
|
||||
---
|
||||
|
||||
## 二、本轮最关键发现(必记)
|
||||
|
||||
### F1:风控/举报真实 DB 路径「零覆盖」盲区(必崩级)
|
||||
`EntityMigrationParityTest` 的白名单 `TEST_SCHEMA_OPTIONAL_TABLES` 中列有 `reports` / `risk_actions` / `risk_events` 三张表,但 **H2 测试库 `schema-h2.sql` 中根本没有这三张表的定义** → 整条 `ReportService` / `RiskReferralService` 真实 DB 路径**零测试覆盖**;而 `ReportServiceTest` 全是 `assertTrue(true)` 假绿。这与 STATUS.md 反复警示的"测试库缺表 → 零覆盖 → 必崩"模式完全一致,是本轮 KEEP-VERIFY 的最高风险点。
|
||||
**修复**:在 `schema-h2.sql` 补齐三表(对齐 V012 生产 DDL,去外键、保留 CHECK);将三表从 `TEST_SCHEMA_OPTIONAL_TABLES` 移除,加入 `REQUIRED_IN_TEST_SCHEMA`(带 Phase 17 注释);新建真实 DB 集成测试。
|
||||
|
||||
### F2:隐藏编译缺陷(javax → jakarta)
|
||||
为补审计在 `ReportService` / `RiskReferralService` / `AuditLogService` 中引入 `HttpServletRequest` 取 ip/userAgent 时,误用 `javax.servlet.http`(Spring Boot 3.x 已迁移到 **Jakarta EE** `jakarta.servlet.http`)→ `test-compile` 直接失败。修复为 `jakarta.servlet`。**印证 Phase 15 铁律:每次复验结束前必须真实编译+执行,不能以文档声明替代实证。**
|
||||
|
||||
### F3:补表时的列缺失
|
||||
`schema-h2.sql` 的 `risk_events` 表起初漏了 `updated_at` 列(实体 `RiskEvent extends BaseEntity` 含 `updatedAt`,MyBatis-Plus 注入该列)→ 全量回归 6 个 `BadSqlGrammar` 错误。修复补齐 `updated_at`(与 `reports` / `risk_actions` 一致)。
|
||||
|
||||
---
|
||||
|
||||
## 三、真实缺陷清单(对应 Gate)
|
||||
|
||||
| 编号 | 类别 | 缺陷 | 修复 | Gate |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 测试保真度/必崩级盲区 | `reports`/`risk_events`/`risk_actions` 在 H2 缺失 → 风控/举报真实 DB 路径零覆盖;`ReportServiceTest` 假绿 | 补三表 + 移出白名单 + 入必需集合 + 真实集成测试 `ReportAndRiskPhase17IntegrationTest`(7 例) | 风险来源可追踪 |
|
||||
| D2 | 审计缺失 | `manualFlagRisk`(人工风险标记)不写审计 | 注入 `AuditLogMapper`,写 `MANUAL_FLAG_RISK` | 低权限不扩大读取 |
|
||||
| D3 | 敏感访问无审计 | 查看他人风险画像/举报详情/风险事件列表不记录审计(AGENTS.md #8) | 新增 `AuditLogService.recordSensitiveAccess`;`getReportDetail`/`getUserRiskProfile`/`RiskController.getRiskEvents` 三端点接线 | 低权限不扩大读取 |
|
||||
| D4 | 审计完整性 | `ReportService.saveAuditLog` 未填 `ip`/`userAgent` | 增强补全(与 `AuthorizationAuditListener` 同模式取 `RequestContextHolder`) | 低权限不扩大读取 |
|
||||
| D5 | 数据一致性 | `RiskEventMapper.findByUserId` 未过滤 `deleted_at`,与 `sumScoreByUserId`(已过滤)不一致 | 补 `deleted_at IS NULL` | 风险来源可追踪 |
|
||||
| D6 | 编译/列缺失 | 见 F2、F3 | jakarta 修正 + `risk_events` 补 `updated_at` | — |
|
||||
|
||||
> 来源接入代码层(`detectCounselingRisk` / `detectAppointmentRisk` / `detectActivityRisk`)**在运行时已被 `CounselingService`/`ActivityService`/`ServiceAppointmentService` 真实调用**,常量齐全,不是"假绿",本轮核心在修复覆盖盲区与审计缺口,而非重写来源逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 四、负向验证(守卫有牙)
|
||||
|
||||
- 临时注释 `ReportOperatorController.getReportDetail` 中的 `auditLogService.recordSensitiveAccess(...)` → 定向运行 `ReportAndRiskPhase17IntegrationTest` → **BUILD FAILURE**(`operatorRiskEndpointsRecordSensitiveAccessAudit` 断言 `before+3 == after` 失败,实际只 +2)。已还原,代码回到 488 全绿状态。
|
||||
|
||||
---
|
||||
|
||||
## 五、证据(实测,非声明)
|
||||
|
||||
- **全量回归**:`488 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS`(离线容器 `danyang-match-backend-test` `mvn -B -o test`,约 4 分)。
|
||||
- **定向编译验证**:`mvn -B -o test-compile` 通过(jakarta 修正后)。
|
||||
- **新增测试**:`backend/src/test/java/com/danyang/match/platform/risk/ReportAndRiskPhase17IntegrationTest.java`(7 例:submitReport 写 reports+abuse_report 风险事件、三来源接入统一 RiskEvent、处置封禁写 risk_actions(ban)、manualFlagRisk 审计、敏感访问审计、findByUserId 软删过滤、最小可见)。
|
||||
|
||||
---
|
||||
|
||||
## 六、改动文件清单
|
||||
|
||||
**主代码(src/main)**
|
||||
- `backend/src/test/resources/schema-h2.sql` — 补齐 `reports` / `risk_events` / `risk_actions` 三表(对齐 V012,去外键、保留 CHECK;`risk_events` 含 `updated_at`)。
|
||||
- `backend/src/main/java/com/danyang/match/platform/risk/mapper/RiskEventMapper.java` — `findByUserId` 补 `deleted_at IS NULL`。
|
||||
- `backend/src/main/java/com/danyang/match/platform/risk/service/ReportService.java` — `saveAuditLog` 补全 `ip`/`userAgent`(jakarta 修正)。
|
||||
- `backend/src/main/java/com/danyang/match/platform/risk/service/RiskReferralService.java` — 注入 `AuditLogMapper`,`manualFlagRisk` 写审计(jakarta 修正)。
|
||||
- `backend/src/main/java/com/danyang/match/platform/audit/service/AuditLogService.java` — 新增 `recordSensitiveAccess`(jakarta 修正)。
|
||||
- `backend/src/main/java/com/danyang/match/platform/risk/controller/ReportOperatorController.java` — `getReportDetail`/`getUserRiskProfile` 接线敏感访问审计。
|
||||
- `backend/src/main/java/com/danyang/match/platform/risk/controller/RiskController.java` — `getRiskEvents` 接线敏感访问审计。
|
||||
|
||||
**测试(src/test)**
|
||||
- `backend/src/test/java/com/danyang/match/platform/risk/ReportAndRiskPhase17IntegrationTest.java` — 新增 7 例。
|
||||
- `backend/src/test/java/com/danyang/match/platform/deployment/EntityMigrationParityTest.java` — 三表移出白名单、入 `REQUIRED_IN_TEST_SCHEMA`(Phase 17 注释)。
|
||||
|
||||
**文档**
|
||||
- `docs/STATUS.md`、`docs/JIRA_BACKLOG.md`、`docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 的 Phase 17 段。
|
||||
- `docs/Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`(本报告)。
|
||||
- 技能 `danyang-platform-verification` 同步更新。
|
||||
|
||||
---
|
||||
|
||||
## 七、下一步
|
||||
|
||||
**Phase 18(申诉 — KEEP-VERIFY)未自动进入,等待用户确认后启动。** 是否继续?
|
||||
+23
-3
@@ -1,8 +1,8 @@
|
||||
# 项目状态跟踪 — STATUS.md
|
||||
|
||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 14 COMPLETE / GIT CLEAN`。**
|
||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 17 COMPLETE / GIT CLEAN`。**
|
||||
>
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)复验已关闭**,十轮共修复 71 处真实缺陷(含 1 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 17 需用户确认后启动。**
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)复验已关闭**,十一轮共修复 77 处真实缺陷(含 2 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 18 需用户确认后启动。**
|
||||
|
||||
最后更新: 2026-09-17 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
| 阶段 3 CapabilityService 独立化 | 已完成 | New `capability` 模块:CapabilityService(getCapabilities + 公共判断方法)+ CapabilityController(GET /api/v1/capabilities);HomeApplicationService 完全委托 |
|
||||
| 阶段 4 双端总验收 | **已完成**(2026-09-14) | 三端构建全绿 + 13/13 契约全绿 + 源码健康检查通过;验收报告见 docs/superpowers/plans/2026-09-14-phase4-acceptance-report.md |
|
||||
| Flyway 迁移 | V001~V039 | V028~V038 已纳入 STATUS;V037 为 Phase 12 新增(实体列与生产迁移对齐,23 表 50 列);V038 为 Phase 13 新增(`matchmaker_recommendations.status` + `matchmaker_services.service_type` 的 CHECK 白名单扩展);V039 为 Phase 13A 新增(counseling 幂等键 + 三个 CHECK 白名单显式固化) |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-16) | 离线容器(`danyang-match-backend-test`)`mvn -o test` 实测:**420 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(4 分 49 秒);含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest`、`DiscoverBlockInvisibilityIntegrationTest`、`ChatPhase09IntegrationTest`、`ChatWebSocketHandlerTest`、`PhotoUploadValidationTest`、`NotificationPhase11IntegrationTest`、`NotificationTemplateTypeGuardTest`、`NotificationPrivacyCopyTest`、`EntityMigrationParityTest`、`RelationshipProgressPhase12Test`、`HomePremaritalGuidancePhase12Test`、`MatchmakerReferralPhase13Test`、`MatchmakerServiceTest` |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-17) | 离线容器(`danyang-match-backend-test`)`mvn -o test` 实测:**488 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(约 4 分);含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest`、`DiscoverBlockInvisibilityIntegrationTest`、`ChatPhase09IntegrationTest`、`ChatWebSocketHandlerTest`、`PhotoUploadValidationTest`、`NotificationPhase11IntegrationTest`、`NotificationTemplateTypeGuardTest`、`NotificationPrivacyCopyTest`、`EntityMigrationParityTest`、`RelationshipProgressPhase12Test`、`HomePremaritalGuidancePhase12Test`、`MatchmakerReferralPhase13Test`、`MatchmakerServiceTest`、`ReportAndRiskPhase17IntegrationTest`(Phase 17 新增 7 例:举报写 reports+abuse_report 风险事件、三来源接入统一 RiskEvent、处置封禁写 risk_actions、人工标记审计、敏感访问审计、findByUserId 软删过滤、最小可见) |
|
||||
| 路由/字段契约收敛 | **已完成**(2026-09-15) | 字段:会话 `target*` + `sentAt` + `relationStatus`(commit `687a156`);路径:采用 2026-09-14 canonical(`/likes` `/passes` `/blocks` `/matches`,缘分保留 `/relations/fate/*`),旧路径与 `success` 桩实现已移除;裁决由 `CanonicalRouteContractTest` 锁定 |
|
||||
| `/me/*` 路径族 | **已完成**(2026-09-16) | 小程序按 REST 规范(§16 / §22-§40 / §80 / §83-§84)调用 `/me/*`,后端此前**无 `/me` 基路径**致 17 个调用 404。新增 `MeController` 委托既有 service 暴露规范路径,旧路径全部保留,小程序三条历史写法(`/me/privacy-settings`、`/me/photos/{id}/main`、`/me/appeals`)一并兼容;由 `MeRouteContractTest` 锁定 |
|
||||
| Admin Web | **验证通过** | 2026-09-14 阶段 4:vue-tsc ✅、vite build ✅ 11.68s;18 modules 齐全;壳层+工作台通过参考图 QA |
|
||||
@@ -46,6 +46,7 @@
|
||||
| 1.1.0-rc.6 | 2026-09-17 | Phase 14 复验关闭(多类型公益活动,11 处后端缺陷 + 3 处前端契约缺陷 + 2 处测试保真度缺陷);新增 `ActivityManagementView` 动态表单;全量回归 **450 tests** 全绿 |
|
||||
| 1.1.0-rc.7 | 2026-09-17 | Phase 15 复验关闭(活动报名/审核/签到,16 处缺陷:含**签到码一生成即作废**、**取消后永远无法再报名**两处必崩;3 处测试保真度);新增 V040 `activity_staff_assignments` 与 `ActivityCheckinView` 签到工作台;全量回归 **474 tests** 全绿 |
|
||||
| 1.1.0-rc.8 | 2026-09-17 | Phase 16 复验关闭(活动互选与双方开启沟通确认,7 处缺陷 + 1 处隐藏编译缺陷 + 1 处测试保真度:含**双确认后根本不建 Match/会话(Gate 缺陷)**、**Phase 15 提交态引用未定义 `reopenEpisode` 致模块不编译("474 绿"为假象)**、**INV-006 单向意向泄露**、**复用/新建 Match 后缺会话**;统一双实现到 `RelationshipService.confirmActivityInterest`;新增 V041 `expires_at`+`match_id`;新建 `ActivityMutualConsentServiceTest`(7 用例);全量回归 **481 tests** 全绿 |
|
||||
| 1.1.0-rc.9 | 2026-09-17 | Phase 17 复验关闭(举报与风控,6 处真实缺陷:含**D1 测试库缺 reports/risk_events/risk_actions 三表致风控/举报真实 DB 路径零覆盖(必崩级盲区)+ ReportServiceTest 假绿**、**D3 敏感访问(查看他人风险画像/举报)无审计(AGENTS.md #8)**、**D4 审计缺 ip/userAgent**、**D5 findByUserId 未过滤软删**、**D2 人工标记无审计**、**D6 javax→jakarta 隐藏编译缺陷 + risk_events 漏 updated_at**);补齐三表并移出白名单入 REQUIRED_IN_TEST_SCHEMA;新增 `AuditLogService.recordSensitiveAccess` 并接线三端点;新增 `ReportAndRiskPhase17IntegrationTest`(7 用例);全量回归 **488 tests** 全绿 |
|
||||
|
||||
---
|
||||
|
||||
@@ -243,6 +244,25 @@
|
||||
> `RelationshipService.confirmActivityInterest`,修复 7 处缺陷 + 1 处隐藏编译缺陷 + 1 处测试保真度缺陷,新建 7 用例互选测试,
|
||||
> 并经负向验证证明守卫有牙。报告:`docs/Phase-16-V11-Recovery-Completion-Report-2026-09-17.md`
|
||||
|
||||
### Phase 17 — 举报与风控 ✅ 已完成(2026-09-17 KEEP-VERIFY/EXTEND 复验通过)
|
||||
| 任务 | 状态 |
|
||||
|------|------|
|
||||
| **D1(Critical,必崩级测试盲区)** `reports`/`risk_events`/`risk_actions` 三表在 H2 缺失 → 风控/举报真实 DB 路径零覆盖;`ReportServiceTest` 全是 `assertTrue(true)` 假绿 | ✅ `schema-h2.sql` 补齐三表(对齐 V012,去外键留 CHECK);`EntityMigrationParityTest` 移出 `TEST_SCHEMA_OPTIONAL_TABLES` 入 `REQUIRED_IN_TEST_SCHEMA` |
|
||||
| **D3(High,敏感访问无审计 · AGENTS.md #8)** 查看他人风险画像/举报详情不记录审计 | ✅ 新增 `AuditLogService.recordSensitiveAccess`;`getReportDetail`/`getUserRiskProfile`/`RiskController.getRiskEvents` 三端点接线 |
|
||||
| **D4(Medium,审计完整性)** `saveAuditLog` 未填 `ip`/`userAgent` | ✅ 增强补全(与 `AuthorizationAuditListener` 同模式取 `RequestContextHolder`) |
|
||||
| **D5(Medium,数据一致性)** `findByUserId` 未过滤 `deleted_at` | ✅ 补 `deleted_at IS NULL`(与 `sumScoreByUserId` 一致) |
|
||||
| **D2(Medium,审计缺失)** `manualFlagRisk` 人工标记无审计 | ✅ 注入 `AuditLogMapper`,写 `MANUAL_FLAG_RISK` |
|
||||
| **D6(隐藏编译缺陷)** `javax.servlet` → Spring Boot 3.x 已迁移 `jakarta.servlet`;`risk_events` 漏 `updated_at` 列 | ✅ 三处 import 改 jakarta;`risk_events` 补 `updated_at` |
|
||||
| 来源接入代码层(detectCounselingRisk/detectAppointmentRisk/detectActivityRisk)运行时已被调用,非假绿 | ✅ 确认(不重写来源逻辑) |
|
||||
| 最小可见(INV-006 同族):后台风险页不暴露无关咨询正文 | ✅ 延续既有约束 |
|
||||
| 负向验证(注释 getReportDetail 敏感访问审计 → D3 守卫精准变红并还原) | ✅ 守卫有牙 |
|
||||
| 新增 `ReportAndRiskPhase17IntegrationTest`(7 用例) | ✅ |
|
||||
| 全量回归 **488 tests 全绿** | ✅ |
|
||||
|
||||
> **2026-09-17 复验结论(KEEP-VERIFY/EXTEND)**:历史 ✅ 不可信。风控/举报整条真实 DB 路径因测试库缺三表而**零覆盖**,`ReportServiceTest`
|
||||
> 假绿掩盖盲区;且敏感访问(查看他人风险画像/举报)此前**无审计**(违反 AGENTS.md #8 默认拒绝 + 查看需审计)。已修复 6 处真实缺陷
|
||||
> (含 1 处必崩级测试盲区、1 处隐藏 jakarta 编译缺陷),补齐三表并新增 7 例集成测试,负向验证证明守卫有牙。报告:`docs/Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`
|
||||
|
||||
### Phase 13A — 公益婚恋咨询与婚前辅导(核心链路)✅ 已完成(2026-09-17 KEEP-VERIFY/EXTEND 复验通过)
|
||||
| 任务 | 状态 |
|
||||
|------|------|
|
||||
|
||||
@@ -451,15 +451,17 @@ D2——`RelationshipService.createMatch` 不写 episode/lastActivityAt,与 `M
|
||||
> - 全量回归 **481 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(约 5 分)。基线 474 → 481(净 +7)。
|
||||
> - 报告:`docs/Phase-16-V11-Recovery-Completion-Report-2026-09-17.md`。**Phase 17 需用户确认后启动。**
|
||||
|
||||
### Phase 17:举报与风控 — EXTEND
|
||||
### Phase 17:举报与风控 — EXTEND ✅ 已完成(2026-09-17 KEEP-VERIFY/EXTEND 复验通过)
|
||||
|
||||
**Files:** risk、report、referral 模块和后台风险页面。
|
||||
|
||||
- [ ] 将 counseling、service_station、offline_activity 作为风险来源接入统一 RiskEvent。
|
||||
- [ ] 后台显示必要来源上下文但不暴露无关咨询正文。
|
||||
- [ ] 禁止公开黑名单;举报、处置、Block 与敏感访问继续审计。
|
||||
- [x] 将 counseling、service_station、offline_activity 作为风险来源接入统一 RiskEvent(运行时已真实调用,本轮确认非假绿)。
|
||||
- [x] 后台显示必要来源上下文但不暴露无关咨询正文(INV-006 同族最小可见约束延续)。
|
||||
- [x] 禁止公开黑名单;举报、处置、Block 与敏感访问继续审计(补 D2 人工标记审计、D3 敏感访问审计、D4 审计 ip/ua)。
|
||||
|
||||
**Gate:** 风险来源可追踪、正文最小可见,低权限人员不能扩大读取范围。
|
||||
**Gate:** 风险来源可追踪 ✅、正文最小可见 ✅、低权限人员不能扩大读取范围 ✅。
|
||||
**复验结论:** 修复 6 处真实缺陷(D1 测试库缺三表必崩级盲区、D2 人工标记无审计、D3 敏感访问无审计、D4 审计缺 ip/ua、D5 findByUserId 未过滤软删、D6 jakarta 编译缺陷+risk_events 漏 updated_at);新增集成测试 7 例;全量回归 488 全绿。
|
||||
**报告:** `docs/Phase-17-V11-Recovery-Completion-Report-2026-09-17.md`。**Phase 18 需用户确认后启动。**
|
||||
|
||||
### Phase 18:申诉 — KEEP-VERIFY
|
||||
|
||||
|
||||
Reference in New Issue
Block a user