fix(p21-2e): 红娘服务拦截接线(blocker 首次生效 + 竞态兜底);2d 确认 Onboarding 本地资格已实现;文档回写
P16 遗留 2e(红娘服务拦截): - checkCanWithdraw 硬编码 0 → 接 MatchmakerServiceMapper.countActiveByUserId(非终态口径 submitted/assigned/in_service/waiting_user,与 findActiveByOperatorAndUser 一致),blocker 首次真正生效 - notifyMatchmaker TODO 空转 → terminateActiveMatchmakerServices 竞态兜底(残留非终态置 cancelled); 不发通知:Phase 18 D2 的 notifications.user_id→users(id) FK 教训 - 守卫 MatchmakerWithdrawBlockerGuardTest 4 用例全绿;首跑暴露测试基建坑:@Transactional Service 是 CGLIB 代理,ReflectionTestUtils 直调私有方法拿到的是代理实例的 null 字段 → 须先 AopTestUtils.getTargetObject 解包(已在测试内注释说明) - 另发现历史遗留:countInProgressByOperator 使用白名单外的 'processing'(已登记) P04-T10:复核确认 Onboarding 本地资格步骤早已实现(onboarding 消息/动作 + 路由 + /me/status 字段), JIRA ⏳ → ✅;清除首页语义错位的死状态 isVerified(miniapp tsc EXIT=0) 文档:JIRA 收尾行/计划文档 Task 3.1/3.3 勾选
This commit is contained in:
+33
-8
@@ -85,6 +85,7 @@ public class AccountLifecycleService {
|
||||
private final ChatService chatService;
|
||||
private final UserIdentityMapper userIdentityMapper;
|
||||
private final CounselingFollowupMapper counselingFollowupMapper;
|
||||
private final com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper matchmakerServiceMapper;
|
||||
|
||||
// ==================== 退出婚恋服务(暂停推荐) ====================
|
||||
|
||||
@@ -166,8 +167,9 @@ public class AccountLifecycleService {
|
||||
|
||||
Map<String, Object> blockers = new HashMap<>();
|
||||
|
||||
// 检查是否有进行中的红娘服务
|
||||
long activeMatchmakerServices = 0; // 需要 MatchmakerServiceMapper
|
||||
// 检查是否有进行中的红娘服务(2e 接线:原硬编码 0,blocker 从未生效)。
|
||||
// 状态口径 = 生产 chk_matchmaker_status 白名单的非终态值(submitted/assigned/in_service/waiting_user)。
|
||||
long activeMatchmakerServices = matchmakerServiceMapper.countActiveByUserId(userId);
|
||||
if (activeMatchmakerServices > 0) {
|
||||
blockers.put("matchmakerService", "您有进行中的红娘服务,请先完成或取消");
|
||||
}
|
||||
@@ -270,8 +272,8 @@ public class AccountLifecycleService {
|
||||
// 2b. 通知所有活跃 Match 对方
|
||||
notifyActiveMatches(user.getId(), "account_closed");
|
||||
|
||||
// 2c. 通知红娘(如果有)
|
||||
notifyMatchmaker(user.getId());
|
||||
// 2c. 终止残留红娘服务(竞态兜底,见方法注释;不能向 operator 发通知——Phase 18 D2 FK 教训)
|
||||
terminateActiveMatchmakerServices(user.getId());
|
||||
|
||||
// 2d. 取消所有未开始的活动报名
|
||||
cancelPendingActivityRegistrations(userId);
|
||||
@@ -425,11 +427,34 @@ public class AccountLifecycleService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知红娘服务已终止
|
||||
* 注销收尾(2e 接线,原 {@code notifyMatchmaker} 为 TODO 空转):把该用户残留的非终态
|
||||
* 红娘服务置为 cancelled。
|
||||
*
|
||||
* <p>正常路径下 {@code checkCanWithdraw} 的 blocker 会要求用户先完成/取消红娘服务,
|
||||
* 走到注销时不应再有非终态记录 —— 本方法是<b>竞态兜底</b>(blocker 检查与注销事务之间,
|
||||
* 红娘管理员恰好受理了新服务)。
|
||||
*
|
||||
* <p><b>为什么不是「发通知」</b>:Phase 18 D2 的教训 —— {@code notifications.user_id → users(id)}
|
||||
* 外键下,operator 不在 users 表,向红娘发用户通知在真实 PG 上必然使注销事务回滚。
|
||||
* 因此以状态收尾 + 日志代替「通知」,效果更实质且不会炸注销事务。
|
||||
*/
|
||||
private void notifyMatchmaker(String userId) {
|
||||
// TODO: 查询该用户的红娘服务,通知相关红娘
|
||||
log.info("通知红娘服务终止: userId={}", userId);
|
||||
private void terminateActiveMatchmakerServices(String userId) {
|
||||
List<com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord> active =
|
||||
matchmakerServiceMapper.selectList(
|
||||
new LambdaQueryWrapper<com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord>()
|
||||
.eq(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getUserId, userId)
|
||||
.in(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getStatus,
|
||||
Constants.MATCHMAKER_STATUS_SUBMITTED,
|
||||
Constants.MATCHMAKER_STATUS_ASSIGNED,
|
||||
Constants.MATCHMAKER_STATUS_IN_SERVICE,
|
||||
Constants.MATCHMAKER_STATUS_WAITING_USER)
|
||||
.isNull(com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord::getDeletedAt));
|
||||
for (com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord svc : active) {
|
||||
svc.setStatus(Constants.MATCHMAKER_STATUS_CANCELLED);
|
||||
svc.setUpdatedAt(Instant.now());
|
||||
matchmakerServiceMapper.updateById(svc);
|
||||
log.info("注销收尾: 残留红娘服务已终止 userId={}, serviceId={}", userId, svc.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
@@ -51,4 +51,14 @@ public interface MatchmakerServiceMapper extends BaseMapper<MatchmakerServiceRec
|
||||
"AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1")
|
||||
Optional<MatchmakerServiceRecord> findActiveByOperatorAndUser(@Param("operatorId") String operatorId,
|
||||
@Param("userId") String userId);
|
||||
|
||||
/**
|
||||
* 注销前检查(P16 收尾 2e):该用户仍处于非终态的红娘服务数。
|
||||
* 状态口径与 {@link #findActiveByOperatorAndUser} 一致(submitted/assigned/in_service/waiting_user)。
|
||||
* ⚠️ 勿用 {@code Constants.MATCHMAKER_STATUS_PENDING} / {@code IN_PROGRESS} ——
|
||||
* 这两个常量不在生产 chk_matchmaker_status 白名单(V009),该表永不出现这两个值。
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM matchmaker_services WHERE user_id = #{userId} " +
|
||||
"AND status IN ('submitted','assigned','in_service','waiting_user') AND deleted_at IS NULL")
|
||||
long countActiveByUserId(@Param("userId") String userId);
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.danyang.match.platform.account;
|
||||
|
||||
import com.danyang.match.platform.account.service.AccountLifecycleService;
|
||||
import com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord;
|
||||
import com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.user.entity.User;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.util.AopTestUtils;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 2e 守卫(Phase 21 遗留清偿):注销前红娘服务拦截 + 注销竞态兜底。
|
||||
*
|
||||
* <h3>缺陷本体</h3>
|
||||
* {@code checkCanWithdraw} 曾把进行中红娘服务数**硬编码为 0** —— blocker 从未生效,
|
||||
* 有进行中红娘服务的用户可以直接注销,红娘工作台留下永不终结的孤儿服务。
|
||||
* 原因注释写「需要 MatchmakerServiceMapper」,但该 Mapper 与 {@code matchmaker_services} 表
|
||||
* (V009)其实一直都在 —— 原 STATUS「数据模型无该表」的判定已被实测推翻。
|
||||
*
|
||||
* <h3>覆盖</h3>
|
||||
* <ol>
|
||||
* <li>blocker 接线:有非终态红娘服务(in_service)→ 注销被拦;</li>
|
||||
* <li>无红娘服务 → 不产生该 blocker;</li>
|
||||
* <li>竞态兜底:{@code terminateActiveMatchmakerServices} 把残留非终态服务置为 cancelled
|
||||
* (方法为 private,用 ReflectionTestUtils 直调,与 {@code AuthServiceAccountClosedGuardTest}
|
||||
* 同模式 —— 注销主流程被 blocker 挡住,正常路径到不了这里)。</li>
|
||||
* </ol>
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class MatchmakerWithdrawBlockerGuardTest {
|
||||
|
||||
@Autowired private AccountLifecycleService accountLifecycleService;
|
||||
@Autowired private UserMapper userMapper;
|
||||
@Autowired private MatchmakerServiceMapper matchmakerServiceMapper;
|
||||
|
||||
private User newUser() {
|
||||
User user = new User();
|
||||
user.setId(UUID.randomUUID().toString());
|
||||
user.setWechatOpenid("openid-" + UUID.randomUUID());
|
||||
user.setAccountStatus("normal");
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private MatchmakerServiceRecord insertService(String userId, String status) {
|
||||
MatchmakerServiceRecord svc = new MatchmakerServiceRecord();
|
||||
svc.setId(UUID.randomUUID().toString());
|
||||
svc.setUserId(userId);
|
||||
svc.setServiceType("recommend_partner");
|
||||
svc.setStatus(status);
|
||||
matchmakerServiceMapper.insert(svc);
|
||||
return svc;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("blocker 接线:有进行中红娘服务的用户注销被拦(原硬编码 0,拦截从未生效)")
|
||||
void activeMatchmakerServiceBlocksWithdraw() {
|
||||
User user = newUser();
|
||||
insertService(user.getId(), Constants.MATCHMAKER_STATUS_IN_SERVICE);
|
||||
|
||||
Map<String, Object> result = accountLifecycleService.checkCanWithdraw(user.getId());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> blockers = (Map<String, Object>) result.get("blockers");
|
||||
|
||||
assertTrue(blockers.containsKey("matchmakerService"),
|
||||
"进行中红娘服务必须成为注销 blocker(P16 遗留 2e 缺陷本体)");
|
||||
assertEquals(false, result.get("canWithdraw"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无红娘服务的用户:不产生 matchmakerService blocker")
|
||||
void noMatchmakerServiceNoBlocker() {
|
||||
User user = newUser();
|
||||
|
||||
Map<String, Object> result = accountLifecycleService.checkCanWithdraw(user.getId());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> blockers = (Map<String, Object>) result.get("blockers");
|
||||
|
||||
assertFalse(blockers.containsKey("matchmakerService"),
|
||||
"无红娘服务时不得误报该 blocker");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已完成的红娘服务不拦截注销(终态不阻塞)")
|
||||
void completedServiceDoesNotBlock() {
|
||||
User user = newUser();
|
||||
insertService(user.getId(), Constants.MATCHMAKER_STATUS_COMPLETED);
|
||||
|
||||
Map<String, Object> result = accountLifecycleService.checkCanWithdraw(user.getId());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> blockers = (Map<String, Object>) result.get("blockers");
|
||||
|
||||
assertFalse(blockers.containsKey("matchmakerService"),
|
||||
"终态(completed)红娘服务不应拦截注销");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("竞态兜底:terminateActiveMatchmakerServices 把残留非终态服务置为 cancelled")
|
||||
void terminateResidualServicesOnClose() {
|
||||
User user = newUser();
|
||||
MatchmakerServiceRecord svc = insertService(user.getId(), Constants.MATCHMAKER_STATUS_ASSIGNED);
|
||||
|
||||
// ⚠️ 必须先解包 CGLIB 代理再反射调私有方法:该 Service 含 @Transactional 方法,
|
||||
// 注入的是代理实例 —— 代理对象自身字段全为 null(委托给 target),绕过拦截器直调
|
||||
// 私有方法会拿到 null 字段 NPE。直接调代理上的 public 方法(checkCanWithdraw)
|
||||
// 走拦截器委托 target,不受影响 —— 这也是本守卫首跑暴露的测试基建坑。
|
||||
Object target = AopTestUtils.getTargetObject(accountLifecycleService);
|
||||
ReflectionTestUtils.invokeMethod(target,
|
||||
"terminateActiveMatchmakerServices", user.getId());
|
||||
|
||||
MatchmakerServiceRecord after = matchmakerServiceMapper.selectById(svc.getId());
|
||||
assertEquals(Constants.MATCHMAKER_STATUS_CANCELLED, after.getStatus(),
|
||||
"注销收尾必须终止残留的非终态红娘服务(原 notifyMatchmaker 为 TODO 空转)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user