fix(p16/p13a): Task 3.3 三项清偿 —— 互选超时接线、随访审计、随访风险检测(负向验证已证守卫有牙)
P16-T13 互选超时接线: - 缺陷本体:expires_at 全仓无人写入(配置 48h 在、confirm 的过期过滤在,唯独创建不赋值) → (expires_at IS NULL OR expires_at > NOW()) 恒真,「截止后48小时」窗口形同虚设 - 修复:createMutualConsent 新建时写 expires_at = 活动结束时间 + ActivityConfig.mutualDeadlineHours (已确认口径「截止后48小时」),活动缺失/无结束时间回退创建时刻 - 守卫:ActivityMutualConsentServiceTest 新增 2 用例;负向验证(摘掉写入)恰 2 红、其余 7 绿 - 原测试 expiredConsentRejectsConfirm 手动补 expires_at 恰好掩盖了创建侧缺陷,如实记录 P13A-T31 随访审计: - assigned Counselor 读他人咨询正文/随访此前零审计 → getDetailForCounselor 接线 recordSensitiveAccess(counseling_followup);本人读自己与 manager 摘要视图不审计 P13A-T33 随访风险检测: - detectCounselingRisk 此前只扫申请摘要 → addFollowup 正文接入同一检测(source=counseling), INV-013 加固:不记录正文只记级别(同 apply 模式) - 守卫:CounselingFollowupAuditAndRiskGuardTest 4 用例(审计落库回查 ×2 + 风险事件落库回查 ×2),全绿
This commit is contained in:
+15
@@ -1,15 +1,19 @@
|
||||
package com.danyang.match.platform.activity_mutual.service;
|
||||
|
||||
import com.danyang.match.platform.activity.entity.Activity;
|
||||
import com.danyang.match.platform.activity.mapper.ActivityMapper;
|
||||
import com.danyang.match.platform.activity_mutual.dto.MutualInterestResultDTO;
|
||||
import com.danyang.match.platform.activity_mutual.entity.ActivityMutualConsent;
|
||||
import com.danyang.match.platform.activity_mutual.mapper.ActivityMutualConsentMapper;
|
||||
import com.danyang.match.platform.relationship.service.RelationshipService;
|
||||
import com.danyang.match.platform.shared.config.ActivityConfig;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
@@ -20,6 +24,8 @@ public class ActivityMutualConsentService {
|
||||
|
||||
private final ActivityMutualConsentMapper consentMapper;
|
||||
private final RelationshipService relationshipService;
|
||||
private final ActivityMapper activityMapper;
|
||||
private final ActivityConfig activityConfig;
|
||||
|
||||
public List<MutualInterestResultDTO> detectMutualResults(String activityId) {
|
||||
List<ActivityMutualConsent> bothConfirmed = consentMapper.findBothConfirmedByActivity(activityId);
|
||||
@@ -44,6 +50,15 @@ public class ActivityMutualConsentService {
|
||||
}
|
||||
consent.setUserAStatus(Constants.MUTUAL_CONSENT_STATUS_PENDING);
|
||||
consent.setUserBStatus(Constants.MUTUAL_CONSENT_STATUS_PENDING);
|
||||
// P16-T13 接线:写入 expires_at,否则 confirmSideIfPending 的
|
||||
// `(expires_at IS NULL OR expires_at > NOW())` 过滤因恒 NULL 而永不生效,
|
||||
// 「截止后 48 小时」窗口(PENDING_DECISIONS 已确认口径)形同虚设。
|
||||
// 基准点 = 活动结束时间(ActivityConfig.mutualDeadlineHours 注释的既定语义),
|
||||
// 活动不存在或无结束时间时回退为创建时刻。
|
||||
Instant deadlineBase = Optional.ofNullable(activityMapper.selectById(activityId))
|
||||
.map(Activity::getEndAt)
|
||||
.orElseGet(Instant::now);
|
||||
consent.setExpiresAt(deadlineBase.plus(Duration.ofHours(activityConfig.getMutualDeadlineHours())));
|
||||
consentMapper.insert(consent);
|
||||
}
|
||||
return consent;
|
||||
|
||||
+25
@@ -1,5 +1,6 @@
|
||||
package com.danyang.match.platform.counseling.service;
|
||||
|
||||
import com.danyang.match.platform.audit.service.AuditLogService;
|
||||
import com.danyang.match.platform.counseling.entity.CounselingFollowup;
|
||||
import com.danyang.match.platform.counseling.entity.CounselingServiceRecord;
|
||||
import com.danyang.match.platform.counseling.mapper.CounselingFollowupMapper;
|
||||
@@ -78,6 +79,7 @@ public class CounselingService {
|
||||
private final NotificationService notificationService;
|
||||
private final RiskReferralService riskReferralService;
|
||||
private final DataScopeGuard dataScopeGuard;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
// ==================== 用户端:申请 ====================
|
||||
|
||||
@@ -264,6 +266,23 @@ public class CounselingService {
|
||||
fu.setResultCode(resultCode);
|
||||
fu.setNextFollowupAt(nextFollowupAt);
|
||||
followupMapper.insert(fu);
|
||||
|
||||
// P13A-T33: 随访正文与申请摘要共用同一风险检测(来源同为 source=counseling)。
|
||||
// 随访由 counselor 撰写、更贴近用户近况,是风险关键词的另一个真实入口;
|
||||
// 此前只有申请摘要(apply:133)被扫描,随访正文完全绕过风控。
|
||||
// INV-013 加固同 apply:绝不记录正文,只记级别与记录 ID。
|
||||
CounselingServiceRecord owner = mapper.findFull(serviceId);
|
||||
if (owner != null) {
|
||||
try {
|
||||
String riskLevel = riskReferralService.detectCounselingRisk(owner.getUserId(), content);
|
||||
if (riskLevel != null) {
|
||||
log.info("随访内容触发风险检测: serviceId={}, followupId={}, riskLevel={}",
|
||||
serviceId, fu.getId(), riskLevel);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("随访风险检测异常: serviceId={}, err={}", serviceId, e.getMessage());
|
||||
}
|
||||
}
|
||||
return fu;
|
||||
}
|
||||
|
||||
@@ -333,6 +352,12 @@ public class CounselingService {
|
||||
"INV-012: 只能查看分配给自己的咨询服务");
|
||||
return null;
|
||||
}
|
||||
// P13A-T31: assigned Counselor 读取的是**他人**的咨询正文与随访记录 ——
|
||||
// 敏感访问必须留痕(同 Phase 17 report/risk_profile 的 recordSensitiveAccess 模式)。
|
||||
// 本人读自己的不算敏感访问;manager/matchmaker 走脱敏摘要视图,不触正文,无需审计。
|
||||
if (isAssignedCounselor) {
|
||||
auditLogService.recordSensitiveAccess(requesterId, "counseling_followup", serviceId);
|
||||
}
|
||||
return svc;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user