Просмотр исходного кода

fix(cf): 废弃旧套餐支付链路,修复组合商品 bundle 部分扣减 bug

- PackagePaymentController:全部接口返回 410 标记废弃(前端无调用)
- PackagePurchaseController.createOrder/getOrder:返回 410 标记废弃
  保留 getPackagePrice/getAvailableGuides(测评链路 VirtualPayService 复用)
- 删除 PackagePaymentService + PackagePaymentServiceInterface(已无调用方)
- ProductOrderService.handlePaymentSuccess 加 @Transactional,
  修复 bundle 多子商品库存扣减部分失败残留(原子性保障)
- PackagePaymentController /wechat params 空格路径 bug 修复
- 清理前端废弃 API createPackageOrder 及对应测试用例
iwt 2 недель назад
Родитель
Сommit
5cc1430a87

+ 34 - 77
cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java

@@ -1,129 +1,86 @@
 package com.etotem.cfc.controller;
-import javax.annotation.Resource;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.TaskPlanInstance;
-import com.etotem.cfc.service.MembershipService;
-import com.etotem.cfc.service.PackagePaymentService;
-import com.etotem.cfc.service.TaskPlanService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Map;
-import com.etotem.cfc.util.ParamUtils;
 
-@Tag(name = "任务模板支付", description = "任务模板购买支付接口")
+/**
+ * 任务模板支付控制器(已废弃)
+ *
+ * 旧链路:任务模板通过 package_orders 独立下单支付。
+ * 现链路:所有套餐统一合并到「组合商品(bundle)」走 product_orders + ProductOrderService 购买支付。
+ * 本控制器前端已无调用,保留类壳返回 410,待确认无调用后清理。
+ */
+@Deprecated
+@Tag(name = "任务模板支付(已废弃)", description = "旧链路,已废弃,请走组合商品购买")
 @RestController
 @RequestMapping("/api/payment")
 public class PackagePaymentController {
 
-    @Resource
-    private PackagePaymentService packagePaymentService;
-
-    @Resource
-    private TaskPlanService planService;
-
-    @Resource
-    private MembershipService membershipService;
-
     /**
-     * 创建支付订单
+     * 创建支付订单(已废弃)
      */
-    @Operation(summary = "创建支付订单")
+    @Deprecated
+    @Operation(summary = "创建支付订单(已废弃)")
     @PostMapping("/create")
     public Result<PackageOrder> createOrder(
             @RequestAttribute("userId") Long userId,
             @RequestBody Map<String, Object> params) {
-        Long familyId = membershipService.getUserFamilyId(userId);
-        if (familyId == null) {
-            return Result.noFamily("请先创建或加入家庭");
-        }
-        Long packageId = ParamUtils.getLong(params.get("packageId"));
-        String payMethod = params.get("payMethod") != null ? params.get("payMethod").toString() : "wechat";
-        Long couponId = ParamUtils.getLong(params.get("couponId"));
-        
-        PackageOrder order = packagePaymentService.createOrder(userId, familyId, packageId, payMethod, couponId);
-        return Result.success(order);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 
     /**
-     * 获取支付参数 (微信JSAPI)
+     * 获取支付参数 (微信JSAPI)(已废弃)
      */
-    @Operation(summary = "获取微信支付参数")
-    @PostMapping("/wechat params")
+    @Deprecated
+    @Operation(summary = "获取微信支付参数(已废弃)")
+    @PostMapping("/wechat/params")
     public Result<Map<String, Object>> getWechatPayParams(@RequestBody Map<String, Object> params) {
-        Long orderId = ParamUtils.getLong(params.get("orderId"));
-        String openid = params.get("openid").toString();
-        
-        Map<String, Object> payParams = packagePaymentService.createWechatPayOrder(orderId, openid);
-        return Result.success(payParams);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 
     /**
-     * 微信支付回调
+     * 微信支付回调(已废弃)
      */
-    @Operation(summary = "微信支付回调")
+    @Deprecated
+    @Operation(summary = "微信支付回调(已废弃)")
     @PostMapping("/notify")
     public String handlePayNotify(@RequestBody String xmlData) {
-        boolean success = packagePaymentService.handlePayNotify(xmlData);
-        
-        if (success) {
-            return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
-        } else {
-            return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>";
-        }
+        return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[接口已废弃]]></return_msg></xml>";
     }
 
     /**
-     * 查询订单状态
+     * 查询订单状态(已废弃)
      */
-    @Operation(summary = "查询订单状态")
+    @Deprecated
+    @Operation(summary = "查询订单状态(已废弃)")
     @PostMapping("/orders/{orderId}")
     public Result<PackageOrder> getOrderStatus(@PathVariable Long orderId) {
-        PackageOrder order = packagePaymentService.getOrderById(orderId);
-        return Result.success(order);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 
     /**
-     * 支付成功后应用任务模板
+     * 支付成功后应用任务模板(已废弃)
      */
-    @Operation(summary = "支付成功后应用任务模板")
+    @Deprecated
+    @Operation(summary = "支付成功后应用任务模板(已废弃)")
     @PostMapping("/apply")
     public Result<TaskPlanInstance> applyAfterPayment(@RequestBody Map<String, Object> params) {
-        Long orderId = ParamUtils.getLong(params.get("orderId"));
-        Long memberId = ParamUtils.getLong(params.get("memberId"));
-        java.util.List<Long> selectedItemIds = null;
-        if (params.get("selectedItemIds") instanceof java.util.List) {
-            selectedItemIds = new java.util.ArrayList<>();
-            for (Object itemId : (java.util.List<?>) params.get("selectedItemIds")) {
-                selectedItemIds.add(Long.valueOf(itemId.toString()));
-            }
-        }
-        
-        PackageOrder order = packagePaymentService.getOrderById(orderId);
-        if (order == null || !"paid".equals(order.getStatus())) {
-            return Result.error("订单未支付");
-        }
-
-        // 应用任务模板
-        com.etotem.cfc.dto.ApplyPackageDTO dto = new com.etotem.cfc.dto.ApplyPackageDTO();
-        dto.setPackageId(order.getPackageId());
-        dto.setMemberId(memberId);
-        dto.setSelectedItemIds(selectedItemIds);
-        
-        TaskPlanInstance instance = planService.applyPackage(dto, order.getFamilyId());
-        return Result.success(instance);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 
     /**
-     * 取消订单
+     * 取消订单(已废弃)
      */
-    @Operation(summary = "取消订单")
+    @Deprecated
+    @Operation(summary = "取消订单(已废弃)")
     @PostMapping("/orders/{orderId}/cancel")
     public Result<Boolean> cancelOrder(@PathVariable Long orderId) {
-        boolean success = packagePaymentService.cancelOrder(orderId);
-        return Result.success(success);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 }

+ 8 - 18
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PackagePurchaseController.java

@@ -51,32 +51,22 @@ public class PackagePurchaseController {
     }
 
     /**
-     * 创建任务模板订单
+     * 创建任务模板订单(已废弃)
      */
-    @Operation(summary = "创建任务模板订单")
+    @Deprecated
+    @Operation(summary = "创建任务模板订单(已废弃)")
     @PostMapping("/order/create")
     public Result<PackageOrder> createOrder(@RequestBody PackagePurchaseDTO dto) {
-        PackageOrder order = packagePurchaseService.createOrder(dto);
-        
-        if (order == null) {
-            return Result.error("创建订单失败");
-        }
-        
-        return Result.success(order);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 
     /**
-     * 获取订单详情
+     * 获取订单详情(已废弃)
      */
-    @Operation(summary = "获取订单详情")
+    @Deprecated
+    @Operation(summary = "获取订单详情(已废弃)")
     @PostMapping("/order/{orderNo}")
     public Result<PackageOrder> getOrder(@PathVariable String orderNo) {
-        PackageOrder order = packagePurchaseService.getOrder(orderNo);
-        
-        if (order == null) {
-            return Result.error("订单不存在");
-        }
-        
-        return Result.success(order);
+        return Result.error(410, "该接口已废弃,套餐购买请走组合商品下单");
     }
 }

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

@@ -1,344 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.etotem.cfc.service.api.PackagePaymentServiceInterface;
-import javax.annotation.Resource;
-
-import com.alibaba.fastjson.JSON;
-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;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.Date;
-import java.util.Map;
-import java.util.UUID;
-
-@Service
-public class PackagePaymentService implements PackagePaymentServiceInterface {
-
-    private static final Logger log = LoggerFactory.getLogger(PackagePaymentService.class);
-
-    @Resource
-    private PackageOrderMapper orderMapper;
-
-    @Resource
-    private TaskTemplatePackageService packageService;
-
-    @Resource
-    private CommissionService commissionService;
-
-    @Resource
-    private CfCommissionService cfCommissionService;
-
-    @Resource
-    private CouponService couponService;
-
-    @Resource
-    private UserIntentMapper userIntentMapper;
-
-    @Value("${wechat.appid}")
-    private String appid;
-
-    @Value("${wechat.mch-id}")
-    private String mchId;
-
-    @Value("${wechat.mch-key}")
-    private String mchKey;
-
-    @Value("${wechat.notify-url}")
-    private String notifyUrl;
-
-    private final RestTemplate restTemplate = new RestTemplate();
-
-    /**
-     * 创建支付订单
-     */
-    public PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod, Long userCouponId) {
-        // 获取任务模板信息
-        TaskTemplatePackage pkg = packageService.getPackageById(packageId);
-        if (pkg == null) {
-            throw new RuntimeException("任务模板不存在");
-        }
-
-        // 如果任务模板免费,直接创建并返回
-        if (pkg.getPrice() == null || pkg.getPrice() <= 0) {
-            PackageOrder order = new PackageOrder();
-            order.setOrderNo(generateOrderNo());
-            order.setUserId(userId);
-            order.setFamilyId(familyId);
-            order.setPackageId(packageId);
-            order.setPackageName(pkg.getName());
-            order.setPrice(0);
-            order.setPlatformFee(0);
-            order.setGuideId(pkg.getCreatorId());
-            order.setStatus("paid");
-            order.setPayMethod("free");
-            order.setPaidAt(new Date());
-            order.setCreatedAt(new Date());
-            order.setUpdatedAt(new Date());
-            orderMapper.insert(order);
-            // 购买完成 → 推进旅程 stage=3(幂等)
-            advanceStage(userId, 3);
-            return order;
-        }
-
-        int originalPrice = pkg.getPrice();
-        int finalPrice = originalPrice;
-        Long appliedCouponId = null;
-        if (userCouponId != null) {
-            Integer discount = couponService.apply(userId, userCouponId, "PRODUCT", originalPrice);
-            if (discount != null && discount > 0) {
-                finalPrice = Math.max(0, originalPrice - discount);
-                appliedCouponId = userCouponId;
-            }
-        }
-
-        // 创建付费订单
-        PackageOrder order = new PackageOrder();
-        order.setOrderNo(generateOrderNo());
-        order.setUserId(userId);
-        order.setFamilyId(familyId);
-        order.setPackageId(packageId);
-        order.setPackageName(pkg.getName());
-        order.setPrice(finalPrice);
-        order.setPlatformFee(pkg.getPlatformFee());
-        order.setUserCouponId(appliedCouponId);
-        order.setGuideId(pkg.getCreatorId());
-        order.setStatus("pending");
-        order.setPayMethod(payMethod);
-        order.setCreatedAt(new Date());
-        order.setUpdatedAt(new Date());
-        
-        orderMapper.insert(order);
-        
-        return order;
-    }
-
-    /**
-     * 发起微信支付 (JSAPI)
-     */
-    public Map<String, Object> createWechatPayOrder(Long orderId, String openid) {
-        PackageOrder order = orderMapper.selectById(orderId);
-        if (order == null) {
-            throw new RuntimeException("订单不存在");
-        }
-
-        // 调用微信统一下单接口
-        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
-        
-        JSONObject params = new JSONObject();
-        params.put("appid", appid);
-        params.put("mch_id", mchId);
-        params.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-        params.put("body", "浠艾福-" + order.getPackageName());
-        params.put("out_trade_no", order.getOrderNo());
-        params.put("total_fee", order.getPrice() * 100);
-        params.put("spbill_create_ip", "127.0.0.1");
-        params.put("notify_url", notifyUrl);
-        params.put("trade_type", "JSAPI");
-        params.put("openid", openid);
-
-        // 生成签名
-        String sign = generateSign(params, mchKey);
-        params.put("sign", sign);
-
-        try {
-            ResponseEntity<String> response = restTemplate.postForEntity(url, params.toJSONString(), String.class);
-            JSONObject result = JSON.parseObject(response.getBody());
-
-            if ("SUCCESS".equals(result.getString("return_code"))) {
-                // 返回预支付订单信息给前端
-                JSONObject payParams = new JSONObject();
-                payParams.put("orderId", orderId);
-                payParams.put("prepay_id", result.getString("prepay_id"));
-                payParams.put("nonce_str", params.getString("nonce_str"));
-                
-                // 生成前端签名
-                String paySign = generatePaySign(payParams, mchKey);
-                payParams.put("sign", paySign);
-
-                return payParams;
-            } else {
-                log.error("微信下单失败: {}", result.toJSONString());
-                throw new RuntimeException("微信下单失败");
-            }
-        } catch (Exception e) {
-            log.error("微信支付异常", e);
-            throw new RuntimeException("支付异常: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 处理微信支付回调
-     */
-    public boolean handlePayNotify(String xmlData) {
-        try {
-            // 解析XML
-            JSONObject params = parseXmlToJson(xmlData);
-            
-            if (!"SUCCESS".equals(params.getString("return_code"))) {
-                log.error("支付回调失败: {}", params.getString("return_msg"));
-                return false;
-            }
-
-            // 验签
-            String sign = params.getString("sign");
-            params.remove("sign");
-            String calculatedSign = generateSign(params, mchKey);
-            
-            if (!sign.equals(calculatedSign)) {
-                log.error("签名验证失败");
-                return false;
-            }
-
-            // 更新订单状态
-            String orderNo = params.getString("out_trade_no");
-            PackageOrder order = orderMapper.selectOne(
-                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<PackageOrder>()
-                    .eq("order_no", orderNo)
-            );
-            
-            if (order != null) {
-                order.setStatus("paid");
-                order.setTransactionId(params.getString("transaction_id"));
-                order.setPaidAt(new Date());
-                order.setNotifyData(xmlData);
-                order.setUpdatedAt(new Date());
-                orderMapper.updateById(order);
-
-                if (order.getUserCouponId() != null) {
-                    couponService.markUsed(order.getUserCouponId(), order.getId());
-                }
-
-                cfCommissionService.settle(order.getId(), "package", order.getUserId(),
-                        order.getFamilyId(), order.getPrice(), null);
-
-                // 购买完成 → 推进旅程 stage=3(幂等)
-                advanceStage(order.getUserId(), 3);
-
-                log.info("订单支付成功: {}", orderNo);
-                return true;
-            }
-            
-            return false;
-        } catch (Exception e) {
-            log.error("处理支付回调异常", e);
-            return false;
-        }
-    }
-
-    /**
-     * 查询订单状态
-     */
-    public PackageOrder getOrderById(Long orderId) {
-        return orderMapper.selectById(orderId);
-    }
-
-    /**
-     * 取消订单
-     */
-    public boolean cancelOrder(Long orderId) {
-        PackageOrder order = new PackageOrder();
-        order.setId(orderId);
-        order.setStatus("cancelled");
-        order.setUpdatedAt(new Date());
-        return orderMapper.updateById(order) > 0;
-    }
-
-    /**
-     * 生成订单号
-     */
-    private String generateOrderNo() {
-        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());
-        }
-    }
-
-    /**
-     * 生成签名
-     */
-    private String generateSign(JSONObject params, String mchKey) {
-        StringBuilder sb = new StringBuilder();
-        params.keySet().stream().sorted().forEach(key -> {
-            if (params.getString(key) != null && !params.getString(key).isEmpty()) {
-                sb.append(key).append("=").append(params.getString(key)).append("&");
-            }
-        });
-        sb.append("key=").append(mchKey);
-        
-        // MD5加密并转大写
-        try {
-            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
-            byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
-            StringBuilder hex = new StringBuilder();
-            for (byte b : digest) {
-                hex.append(String.format("%02x", b));
-            }
-            return hex.toString().toUpperCase();
-        } catch (Exception e) {
-            throw new RuntimeException("签名失败", e);
-        }
-    }
-
-    /**
-     * 生成前端支付签名
-     */
-    private String generatePaySign(JSONObject params, String mchKey) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("appId=").append(appid).append("&");
-        sb.append("nonceStr=").append(params.getString("nonce_str")).append("&");
-        sb.append("package=prepay_id=").append(params.getString("prepay_id")).append("&");
-        sb.append("signType=MD5&");
-        sb.append("timeStamp=").append(System.currentTimeMillis() / 1000).append("&");
-        sb.append("key=").append(mchKey);
-        
-        try {
-            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
-            byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
-            StringBuilder hex = new StringBuilder();
-            for (byte b : digest) {
-                hex.append(String.format("%02x", b));
-            }
-            return hex.toString().toUpperCase();
-        } catch (Exception e) {
-            throw new RuntimeException("签名失败", e);
-        }
-    }
-
-    /**
-     * 解析XML为JSON
-     */
-    private JSONObject parseXmlToJson(String xml) {
-        JSONObject json = new JSONObject();
-        // 简单XML解析
-        xml = xml.replace("<", "{").replace(">", ":").replace("</", "}").replace("><", ",");
-        // 这里简化处理,实际应该使用XML解析器
-        return json;
-    }
-}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -36,6 +36,7 @@ import com.etotem.cfc.service.CommissionService;
 import com.etotem.cfc.service.AssessmentProductService;
 import com.etotem.cfc.service.AssessmentQuotaService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.text.SimpleDateFormat;
@@ -556,6 +557,12 @@ public class ProductOrderService {
         return Result.success(result);
     }
 
+    /**
+     * 支付成功处理:扣减库存(physical 单商品 / bundle 穿透子商品)+ 更新订单状态
+     * 事务保证 bundle 多子商品扣减的原子性——任一子商品库存不足时整体回滚,
+     * 避免部分出库(已扣子商品库存 + 已写 outbound 流水残留)
+     */
+    @Transactional(rollbackFor = Exception.class)
     public Result<String> handlePaymentSuccess(String orderNo, String transactionId) {
         ProductOrder order = orderMapper.selectOne(
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));

+ 0 - 13
cfc-backend/src/main/java/com/etotem/cfc/service/api/PackagePaymentServiceInterface.java

@@ -1,13 +0,0 @@
-package com.etotem.cfc.service.api;
-
-import com.etotem.cfc.entity.PackageOrder;
-
-import java.util.Map;
-
-public interface PackagePaymentServiceInterface {
-    PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod, Long userCouponId);
-    Map<String, Object> createWechatPayOrder(Long orderId, String openid);
-    boolean handlePayNotify(String xmlData);
-    PackageOrder getOrderById(Long orderId);
-    boolean cancelOrder(Long orderId);
-}

+ 0 - 6
cfc-frontend/__tests__/api/api.test.js

@@ -628,12 +628,6 @@ describe('guide & packages API', () => {
 // --------------- payment module ---------------
 
 describe('payment API', () => {
-  test('createPackageOrder sends POST', async () => {
-    mockSuccess({ code: 200 })
-    await api.createPackageOrder({ packageId: 1, userId: 5 })
-    expect(lastRequest().url).toMatch(/\/api\/package\/order\/create$/)
-  })
-
   test('createWechatPayOrder sends POST', async () => {
     mockSuccess({ code: 200 })
     await api.createWechatPayOrder('NO001', '测试订单', 100)

+ 0 - 4
cfc-frontend/utils/api.js

@@ -924,10 +924,6 @@ export const getAvailableGuides = (packageId) => {
   return request('/api/package/available-guides', 'POST', { packageId })
 }
 
-// 创建套餐订单
-export const createPackageOrder = (data) => {
-  return request('/api/package/order/create', 'POST', data)
-}
 
 // 获取订单状态
 export const getOrderStatus = (orderNo) => {