Kaynağa Gözat

PhaseA3-CR020: 补vote/finish两个L1券触发点(路演投票+完课证书),champion留待排期

liaoxg 1 hafta önce
ebeveyn
işleme
d5ab795

+ 7 - 5
docs/合规核查报告.md

@@ -14,7 +14,7 @@
 |---|---|---|---|
 | CR-003 | 学员页面禁露销售·引流·钩子·L1凭券信息 | ⚠️ **6 处违规** | 需修复(A4) |
 | CR-019 | 案例 IP 三段流水线完整性 | ✅ **已完整** | 无需动作 |
-| CR-020 | 测评兴趣调研与转化发放规则 | ✅ **已核查(3/5 触发落地)** | vote/finish/champion 留待排期 |
+| CR-020 | 测评兴趣调研与转化发放规则 | ✅ **已核查(4/5 触发落地)** | champion 留待排期 |
 | CR-021 | 四线合规红线核查 | ✅ **文案基本合规** | 需法务确认 |
 | CR-022 | L0 半天营学员端禁露销售信息 | ⚠️ **与 CR-003 联动** | 随 CR-003 修复 |
 
@@ -132,12 +132,14 @@ pending(待授权) → authorized(已授权) / rejected(已拒绝) → published
 
 ### 核查结论(2026-09-09 A3 完成)
 1. **发放入口集中在 `PlanController.grantCoupon(uid, type, trigger, remark)`**,幂等:同 `uid+type+trigger` 不重复发放(`selectCount` 判重)。
-2. **实际落地的触发来源(2 个)**:
+2. **实际落地的触发来源(4 个)**:
    - `research` → 测评提交勾选"健康/成长"发放 `etotem` 测评兑换卡(`PlanController.survey`);
-   - `invite` → 转介绍报名成功后给邀请人发放 `private` 私教券(`PayService.rewardInviterIfInvited`,幂等)。
-3. **未落地的触发来源(3 个)**:`vote`(路演投票)、`finish`(完课)、`champion`(冠军)——`grantL1IfEligible(uid, trigger)` 已预留方法但**无调用点**,L1 券当前实际不会通过这些触发发放。
+   - `invite` → 转介绍报名成功后给邀请人发放 `private` 私教券(`PayService.rewardInviterIfInvited`,幂等);
+   - `vote` → 路演投票提交成功后发放 L1 课程券(`VoteController.cast` → `grantL1IfEligible(userId,"vote")`,2026-09-09 补实现);
+   - `finish` → 完课 6 项验收达标领取证书时发放 L1 课程券(`PlanController.cert` → `grantL1IfEligible(userId,"finish")`,2026-09-09 补实现)。
+3. **未落地的触发来源(1 个)**:`champion`(冠军)——依赖比赛结果判定与课程编排,进入待排期清单;L1 券不由此发放。
 4. **cfc 侧对齐**:`E:\cfc\...\task\CouponGrantTask.java` 为会员**周期性券补发**(PERIODIC,按 grant_period),与 train 的事件触发式券无关;train 的 `TrainCoupon` 是独立表,不依赖 cfc 券体系。
-5. **处置**:`vote/finish/champion` 三个触发点留待 L1 课程排期后按课程编排实现(进入待排期清单),当前无合规风险(不发券不会违规)。
+5. **处置**:`champion` 触发点留待 L1 课程排期/比赛判定机制落地后实现(进入待排期清单),当前无合规风险(不发券不会违规)。
 
 ---
 

+ 4 - 3
docs/需求变更清单.md

@@ -275,10 +275,11 @@
 |---|---|
 | **原始依据** | 11 旧SRS §FR-09:测评兴趣调研与转化发放(券自动发放 + 转介) |
 | **当前实现** | `survey/index` 已实现兴趣调研;券自动发放触发来源(vote/finish/research/champion/teaching)需确认完整 |
-| **变更内容** | **已核查(2026-09-09)**:发放入口集中在 `PlanController.grantCoupon`(幂等判重);实际落地 2 个触发——`research`(测评→etotem 兑换卡)、`invite`(转介绍→private 私教券);`vote/finish/champion` 三个触发点预留 `grantL1IfEligible` 但无调用,L1 券当前不由此发放 |
-| **影响范围** | PlanController、`pages/survey/index.vue`、PayService(转介绍激励) |
-| **优先级** | P1(已完成核查,`vote/finish/champion` 待排期实现) |
+| **变更内容** | **已核查(2026-09-09)**:发放入口集中在 `PlanController.grantCoupon`(幂等判重);实际落地 4 个触发——`research`(测评→etotem 兑换卡)、`invite`(转介绍→private 私教券)、`vote`(路演投票→L1 券)、`finish`(完课证书→L1 券);`champion`(冠军)触发依赖比赛判定机制,待排期实现 |
+| **影响范围** | PlanController、VoteController、`pages/survey/index.vue`、PayService(转介绍激励) |
+| **优先级** | P1(已完成核查,`champion` 待排期实现) |
 | **依据** | `docs/合规核查报告.md` §4(V1.1) |
+| **commit** | `c99e69d`(核查落档);vote/finish 触发实现见 VoteController/PlanController(2026-09-09) |
 
 ---
 

+ 6 - 1
train-backend/src/main/java/com/train/controller/PlanController.java

@@ -92,7 +92,12 @@ public class PlanController {
     @Operation(summary = "生成/领取完课证书")
     @PostMapping("/cert")
     public Result<Map<String, Object>> cert(@org.springframework.web.bind.annotation.RequestAttribute("userId") Long userId) {
-        return Result.success(certService.issue(userId));
+        Map<String, Object> data = certService.issue(userId);
+        // 完课(6 项验收达标)→ L1 课程券(幂等:同 uid+type+trigger 不重复发)
+        if (Boolean.TRUE.equals(data.get("eligible"))) {
+            grantL1IfEligible(userId, "finish");
+        }
+        return Result.success(data);
     }
 
     @Operation(summary = "卡券核销")

+ 4 - 0
train-backend/src/main/java/com/train/controller/VoteController.java

@@ -34,6 +34,8 @@ public class VoteController {
     private TrainScoreMapper trainScoreMapper;
     @Resource
     private ScoreService scoreService;
+    @Resource
+    private PlanController planController;
 
     // ============ 路演材料 ============
     @Operation(summary = "上传路演材料")
@@ -114,6 +116,8 @@ public class VoteController {
         if (myGroupId != null && (groupScoreExists == null || groupScoreExists == 0)) {
             scoreService.addGroupScoreIfAbsent(myGroupId, "vote", 50, null, "路演两轮投票");
         }
+        // 完成投票 → L1 课程券(幂等:同 uid+type+trigger 不重复发)
+        planController.grantL1IfEligible(userId, "vote");
         return Result.success(true);
     }