Kaynağa Gözat

feat(api): 旅程 stage 3/4 推进 + 购买→方案关联校验 (P1)

- PackagePaymentService:支付回调成功/免费订单 → 推进旅程 stage=3(幂等向前)
- PackageLogisticsController:物流状态非 none → 推进旅程 stage=4(试剂盒已发/样本已收)
- ProblemPackageController.confirm:确认方案前校验套餐已购买(paid/pending 订单),打通购买→方案关联

Ultraworked with [Sisyphus](https://github.com/OhMyOpenCode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 ay önce
ebeveyn
işleme
c9ed05da20

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/PackageLogisticsController.java

@@ -3,7 +3,9 @@ package com.etotem.cfc.controller;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.entity.UserIntent;
 import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.mapper.UserIntentMapper;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import javax.annotation.Resource;
@@ -31,6 +33,9 @@ public class PackageLogisticsController {
     @Resource
     private PackageOrderMapper packageOrderMapper;
 
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
     @Operation(summary = "管理端发货/更新物流(orderId + logisticsStatus + trackingNo)")
     @PostMapping("/update")
     public Result<Boolean> update(@RequestAttribute("userId") Long userId,
@@ -58,6 +63,24 @@ public class PackageLogisticsController {
         }
         order.setUpdatedAt(new Date());
         packageOrderMapper.updateById(order);
+
+        // 物流已推进(非 none)→ 推进旅程 stage=4(试剂盒已发/样本已收,幂等)
+        if (order.getUserId() != null && !"none".equals(logisticsStatus)) {
+            try {
+                UserIntent intent = userIntentMapper.selectOne(
+                        new LambdaQueryWrapper<UserIntent>()
+                                .eq(UserIntent::getUserId, order.getUserId())
+                                .last("LIMIT 1"));
+                if (intent != null && (intent.getJourneyStage() == null || intent.getJourneyStage() < 4)) {
+                    intent.setJourneyStage(4);
+                    intent.setStageUpdatedAt(new Date());
+                    intent.setUpdatedAt(new Date());
+                    userIntentMapper.updateById(intent);
+                }
+            } catch (Exception e) {
+                // 旅程推进失败不影响物流更新
+            }
+        }
         return Result.success(true);
     }
 

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ProblemPackageController.java

@@ -8,6 +8,8 @@ import com.etotem.cfc.entity.TaskPlanInstance;
 import com.etotem.cfc.entity.TaskTemplateItem;
 import com.etotem.cfc.entity.TaskTemplatePackage;
 import com.etotem.cfc.entity.UserIntent;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.mapper.PackageOrderMapper;
 import com.etotem.cfc.mapper.ProblemDomainMapper;
 import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
 import com.etotem.cfc.mapper.UserIntentMapper;
@@ -55,6 +57,9 @@ public class ProblemPackageController {
     @Resource
     private MembershipService membershipService;
 
+    @Resource
+    private PackageOrderMapper packageOrderMapper;
+
     @Operation(summary = "套餐详情(名称/介绍/周期/包含任务模板/奖励说明)")
     @PostMapping("/detail")
     public Result<Map<String, Object>> detail(@RequestBody Map<String, Object> params) {
@@ -117,6 +122,16 @@ public class ProblemPackageController {
             packageId = Long.parseLong(domain.getTaskTemplatePackageIds().trim().split(",")[0]);
         }
 
+        // 购买→方案关联校验:确认方案前该套餐须已购买(存在订单记录)
+        Long orderCount = packageOrderMapper.selectCount(
+                new LambdaQueryWrapper<PackageOrder>()
+                        .eq(PackageOrder::getFamilyId, familyId)
+                        .eq(PackageOrder::getPackageId, packageId)
+                        .in(PackageOrder::getStatus, "paid", "pending"));
+        if (orderCount == null || orderCount == 0) {
+            return Result.error("请先购买套餐后再确认方案");
+        }
+
         // 幂等:同一家庭同一套餐已有激活中的方案实例则复用
         TaskPlanInstance plan = planInstanceMapper.selectOne(
                 new LambdaQueryWrapper<TaskPlanInstance>()

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java

@@ -8,7 +8,9 @@ import com.alibaba.fastjson.JSONObject;
 import com.etotem.cfc.common.OrderNoGenerator;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.entity.UserIntent;
 import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.mapper.UserIntentMapper;
 import com.etotem.cfc.service.CommissionService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -38,6 +40,9 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
     @Resource
     private CouponService couponService;
 
+    @Resource
+    private UserIntentMapper userIntentMapper;
+
     @Value("${wechat.appid}")
     private String appid;
 
@@ -79,6 +84,8 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
             order.setCreatedAt(new Date());
             order.setUpdatedAt(new Date());
             orderMapper.insert(order);
+            // 购买完成 → 推进旅程 stage=3(幂等)
+            advanceStage(userId, 3);
             return order;
         }
 
@@ -213,6 +220,9 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
                 commissionService.settle(order.getId(), "package", order.getUserId(),
                         order.getPrice(), null);
 
+                // 购买完成 → 推进旅程 stage=3(幂等)
+                advanceStage(order.getUserId(), 3);
+
                 log.info("订单支付成功: {}", orderNo);
                 return true;
             }
@@ -249,6 +259,24 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
         return OrderNoGenerator.generate("PKG");
     }
 
+    /** 推进用户旅程至指定阶段(幂等:只向前推进) */
+    private void advanceStage(Long userId, int stage) {
+        try {
+            UserIntent intent = userIntentMapper.selectOne(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<UserIntent>()
+                            .eq(UserIntent::getUserId, userId)
+                            .last("LIMIT 1"));
+            if (intent != null && (intent.getJourneyStage() == null || intent.getJourneyStage() < stage)) {
+                intent.setJourneyStage(stage);
+                intent.setStageUpdatedAt(new Date());
+                intent.setUpdatedAt(new Date());
+                userIntentMapper.updateById(intent);
+            }
+        } catch (Exception e) {
+            log.warn("推进旅程阶段失败 userId={}, stage={}: {}", userId, stage, e.getMessage());
+        }
+    }
+
     /**
      * 生成签名
      */