deep-dive-plan-activation.md 7.3 KB

专项深挖 · 方案激活与拆任务幂等(Plan Activation / Task Splitting)

目的:基于源码对「方案激活 → 拆任务」做代码级深挖,重点验证「拆任务是否幂等」这一用户关注点,并映射到缺陷与用例。 阅读对象:QA / 研发 / 测试架构。本文件为只读分析,不修改源码。 证据来源:

  • cfc-backend/src/main/java/com/etotem/cfc/service/PlanActivationService.java
  • cfc-backend/src/main/java/com/etotem/cfc/service/TaskPlanService.java(generateTasksFromTemplate 第 195–275 行、applyAndActivate 第 284–303 行)
  • cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java(第 115 行调用 activate)
  • cfc-backend/src/test/java/com/etotem/cfc/unit/TaskPlanServiceTest.java(幂等单测)

1. 激活流程与状态机

PlanActivationService.activate(planId, operatorId)(@Transactional):

① 取方案 plan = selectById(planId)
② 校验 status == "reviewed"  ← 守卫:否则抛「方案状态异常,需要先审核通过」
③ 校验 childId 在 users 或 family_members 存在(任一即可)
④ taskPlanService.generateTasksFromTemplate(plan, operatorId)  ← 拆任务(建引导任务+模板任务)
⑤ plan.setStatus("active"),写 activatedAt / totalDays(默认14) / completedDays=0

入口:PlanReviewController 规划师审核通过后调 planActivationService.activate(planId, userId)(第 115 行)。另有一条「家长自助」路径 applyAndActivate(跳过审核,直接 active)。

「拆任务」即 generateTasksFromTemplate:按 packageId 从 task_template_items(按 sort_order)批量生成每日/模板任务,并额外生成 1 条「方案启动」引导任务。

2. 幂等性分析(核心)

2.1 单线程幂等:成立 ✅

generateTasksFromTemplate 开头:

Long existing = taskMapper.selectCount(
    new LambdaQueryWrapper<Task>()
        .eq(Task::getSourceType, "plan")
        .eq(Task::getSourceId, planId)
        .ne(Task::getStatus, "cancelled"));
if (existing != null && existing > 0) return 0;   // 已生成过任务则跳过
  • 已存在非 cancelled 任务 → 直接返回 0(不重复插入)。
  • PlanActivationService.activate 还有 status=="reviewed" 守卫:激活后 status=active,再次调 activate 会因状态非 reviewed 而抛异常。
  • 单测 generateTasksFromTemplate_已生成过则跳过 覆盖:selectCount 返回 5 → count=0 且 insert 从未被调用。

结论 G1(设计良好,P0 通过项):单线程下「拆任务」幂等,重复激活被双重守卫(任务存在性 + 状态机)拦截。

2.2 并发幂等:存在缺口 ⚠️(关键发现)

selectCount → insert 是 非原子 的「检查再插入」(TOCTOU),且 无唯一约束 / 乐观锁 兜底:

  • 同一 reviewed 方案被并发激活时,两个事务都先读到 status=reviewed(守卫放行),都进入 generateTasksFromTemplate,都通过 existing==0 检查,随后都执行 insert → 重复生成任务(双份引导任务 + 双份模板任务)。
  • PlanActivationService.activate 的 status=="reviewed" 守卫挡不住并发:两个事务在 setStatus("active") 提交前都看到了 reviewed,全部提交成功。

结论 G2(并发缺陷,中/高):高并发下同一方案可能产出重复任务,破坏「1 方案 → 1 套任务」的不变量。与测评预约模块的 F4(创建无幂等)、F7(额度扣减非原子)属同类「检查-执行非原子」问题。

2.3 取消后重新激活:有意设计 ✅(需文档化)

generateTasksFromTemplate 的计数条件 .ne(Task::getStatus, "cancelled") —— 已 cancelled 的任务不计入。因此:方案激活 → cancelPlan(任务置 cancelled)→ 重新激活时 existing==0 → 重新生成任务。这是有意设计(避免取消后漏生成),但需在文档中明确,否则易被误读为幂等漏洞。

3. 其他发现

编号 严重度 位置 现象 期望/建议
G1 设计良好 generateTasksFromTemplate 存在性守卫 + activate 状态守卫 单线程幂等、重复激活被拒 维持;补并发单测
G2 中/高 generateTasksFromTemplate 检查-插入非原子 并发激活同方案 → 重复任务 加唯一索引 (source_type, source_id) + 冲突忽略;或对 plan 行加 SELECT ... FOR UPDATE 行锁
G3 低 generateTasksFromTemplate 第 239 行 if (plan.getPackageId() != null) packageId==null 时只生成引导任务、无模板任务 符合预期但需文档化;若套餐被清空,激活后无日常任务且无告警
G4 低(待确认) buildTodayDeadline() 用于所有任务 deadline 引导任务与模板任务 deadline 均设为「当天 23:59:59」;模板任务多为 frequency=daily,首条即当天过期 确认是否设计意图;若是周期任务,deadline 应为 N 天后,否则「每日任务」首日均显示逾期

4. 深挖专项测试用例

优先级:P0 主链路/并发 · P1 重要 · P2 一般。

TC 编号 场景 模块 角色 优先级 类型 前置 步骤 预期 关联发现
TC-PA-01 方案审核通过→激活→拆任务 激活/拆任务 规划师 P0 功能 方案 status=reviewed,套餐含 N 个模板项 调 activate status→active;生成 1 引导 + N 模板任务;能量/流水一致 G1
TC-PA-02 重复激活应被拒(单线程) 激活/幂等 规划师 P0 异常 已激活方案 再次 activate 抛「方案状态异常,需要先审核通过」 G1
TC-PA-03 并发激活同方案不重复拆任务 激活/并发幂等 规划师×2 P1 并发 同一 reviewed 方案 两线程同时 activate 任务总数 = 1+ N(不翻倍);若翻倍→【缺陷 G2】 G2
TC-PA-04 取消后重新激活重新拆任务 激活/取消 规划师 P1 功能 已激活→cancelPlan 重新 activate 旧任务 cancelled 不计入,重新生成 1+N 任务 G3
TC-PA-05 套餐为空/packageId 空 拆任务/边界 规划师 P2 边界 方案 packageId=null activate 仅生成引导任务,无模板任务(文档化) G3
TC-PA-06 周期任务 deadline 合理性 拆任务/数据 规划师 P2 数据 daily 模板项 activate 后查任务 deadline 非当天过期(待确认设计) G4

5. 给研发的修复建议(五要素摘要)

  1. G2 并发幂等(优先):对 task 表加唯一索引 (source_type, source_id)(或 (source_type, source_id, task_type)),插入冲突时 ON DUPLICATE KEY 忽略 / 捕获 DuplicateKeyException 返回 0;或在 activate 中对方案行加 SELECT ... FOR UPDATE 行锁,使并发激活串行化。目的:保证「1 方案 → 1 套任务」。闭环:TC-PA-03 复测任务不翻倍。
  2. G3 文档化:明确 packageId==null 时仅生成引导任务的设计意图,避免误判。
  3. G4 确认 deadline:与产品确认周期任务首条 deadline 是否应为当天;若否,改为「今天 + cycleDays 偏移」。闭环:TC-PA-06。

以上为只读分析结论,修复由研发团队实施;本文件仅供测试与评审使用。