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

docs(cf): 修正实现计划占位(getEffectivePpoint 签名/转让幂等键/家庭券库独立Controller)

Sisyphus 2 месяцев назад
Родитель
Сommit
4b921f8e66
1 измененных файлов с 68 добавлено и 28 удалено
  1. 68 28
      docs/superpowers/plans/2026-09-01-cf-commission-redesign.md

+ 68 - 28
docs/superpowers/plans/2026-09-01-cf-commission-redesign.md

@@ -471,6 +471,8 @@ public class CfCommissionService {
     private PpointConfigService ppointConfigService;
     @Resource
     private SysConfigService sysConfigService;
+    @Resource
+    private ProductMapper productMapper;
 
     /**
      * 通用分佣(双返):当前人 + 推荐人
@@ -512,8 +514,9 @@ public class CfCommissionService {
         int amountYuan = orderAmountCent / 100;
         if ("product".equals(orderType) && productId != null) {
             try {
-                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId,
-                        /* categoryId */ null == null ? null : null);
+                Product p = productMapper.selectById(productId);
+                if (p == null) return 0;
+                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId, p.getCategoryId());
                 if (effectivePpoint <= 0) return 0;
                 int shareBps = getSysBps("product_platform_points_share", 1000);
                 return amountYuan * effectivePpoint / 100 * shareBps / 10000;
@@ -577,8 +580,9 @@ public class CfCommissionService {
     private int calcReferrerBase(String orderType, int amountYuan, Long productId) {
         if ("product".equals(orderType) && productId != null) {
             try {
-                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId, null);
-                return effectivePpoint;
+                Product p = productMapper.selectById(productId);
+                if (p == null) return 0;
+                return ppointConfigService.getEffectivePpoint(productId, p.getCategoryId());
             } catch (Exception e) {
                 return 0;
             }
@@ -612,12 +616,11 @@ public class CfCommissionService {
 }
 ```
 
-> **注意:** 步骤 1 中 `calcBuyerReturn` 内有一处明显的占位式写法(`getEffectivePpoint(productId, null)` 参数猜测),实现时需核对 `PpointConfigService.getEffectivePpoint` 的真实签名(参考 `CommissionDistService` 现有调用 `ppointConfigService.getEffectivePpoint(productId, product.getCategoryId())`),修正为实际签名。这是实现者必须确认的点。
+> **签名已确认:** `PpointConfigService.getEffectivePpoint(Long productId, Long categoryId)`(line 75)。商品订单需先 `productMapper.selectById(productId)` 取 `getCategoryId()` 再调用。上述 `calcBuyerReturn`/`calcReferrerBase` 已按此修正。`Product` 实体有 `getCategoryId()`(line 62)。
 
-- [ ] **步骤 2:核对 `PpointConfigService.getEffectivePpoint` 签名**
+- [ ] **步骤 2:确认 `PpointConfigService.getEffectivePpoint` 签名(已核对,无需重复 grep)**
 
-运行:`grep -n "getEffectivePpoint" cfc-backend/src/main/java/com/etotem/cfc/service/PpointConfigService.java`
-预期:确认参数签名,修正 `CfCommissionService` 中所有调用点为真实签名(参考 `CommissionDistService:72` 与 `ProductOrderService:626` 的现有用法)。
+签名 `getEffectivePpoint(Long productId, Long categoryId)` 已确认;`ProductMapper`、`Product` 已在步骤 1 注入/引用。
 
 - [ ] **步骤 3:编译验证**
 
@@ -1037,10 +1040,13 @@ public class CfTransferController {
         if (from.getFamilyId() == null || !from.getFamilyId().equals(to.getFamilyId())) {
             return Result.error("仅限同一家庭成员间转让");
         }
+        // 每次转让使用唯一 refId(避免 earn 幂等键 (ref_type, ref_id) 碰撞)
+        Long transferRefId = java.util.UUID.randomUUID().getMostSignificantBits();
+        if (transferRefId == null || transferRefId == 0) transferRefId = System.currentTimeMillis();
         // 转出扣减 + 转入增加(同一事务)
-        platformPointsService.spend(fromUserId, amount, "cf_transfer_out", fromUserId,
+        platformPointsService.spend(fromUserId, amount, "cf_transfer_out", transferRefId,
                 "转给成员: " + (to.getNickname() == null ? toUserId : to.getNickname()));
-        platformPointsService.earn(toUserId, amount, "cf_transfer_in", fromUserId,
+        platformPointsService.earn(toUserId, amount, "cf_transfer_in", transferRefId,
                 "收到成员转让: " + (from.getNickname() == null ? fromUserId : from.getNickname()));
 
         CfTransferRecord record = new CfTransferRecord();
@@ -1073,7 +1079,7 @@ public class CfTransferController {
 }
 ```
 
-> **注意:** `PlatformPointsService.spend/earn` 的幂等键由 `refId` 控制。上述代码用 `fromUserId` 作 refId 会导致**同一对用户多次转让互相覆盖**——实现时必须改为每次唯一 refId(如用 `System.nanoTime()` 或生成唯一 id),或在 `spend` 增加 `cf_transfer_*` 专用幂等处理。这是实现者必须修正的点(推荐:转让场景不走幂等检查,或 earn/spend 传入自增业务唯一键)。
+> **幂等说明:** 每次转让生成唯一 `transferRefId`(`UUID.randomUUID().getMostSignificantBits()`),`spend`/`earn` 共用该 refId。`spend` 无幂等检查(始终扣减),`earn` 用 `(ref_type='cf_transfer_in', ref_id=transferRefId)` 去重——因每次 transferRefId 唯一,多次转让互不影响。注意 `PlatformPointsService.earn` 幂等检查要求 `refId != null`(任务 3 实现),若 `UUID.getMostSignificantBits()` 返回 0(极小概率)则退化为 `System.currentTimeMillis()`。
 
 - [ ] **步骤 2:编译验证**
 
@@ -1131,25 +1137,59 @@ public boolean grant(Long userId, Long couponId, String grantType, String period
 couponService.grant(refUserId, couponId, "CF_EXCHANGE", null, "family:" + familyId, familyId);
 ```
 
-- [ ] **步骤 4:家庭券库查询接口**
+- [ ] **步骤 4:家庭券库查询接口(新建独立 Controller)**
 
-`CfCommissionController` 新增:
+新建 `cfc-backend/src/main/java/com/etotem/cfc/controller/CfCouponController.java`(避免在 `CfCommissionController` 注入券相关 Mapper 导致职责混杂):
 
 ```java
-/** 家庭共享券列表 */
-@PostMapping("/family-coupons")
-public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.etotem.cfc.entity.UserCoupon>>
-familyCoupons(@RequestAttribute("userId") Long userId,
-              @RequestBody Map<String, Integer> params) {
-    int page = params.getOrDefault("page", 1);
-    int size = params.getOrDefault("size", 20);
-    com.etotem.cfc.entity.User user = com.etotem.cfc.entity.User.class.cast(null);
-    // 需注入 UserMapper 获取 familyId
-    ...
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.UserCoupon;
+import com.etotem.cfc.mapper.UserCouponMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/coupon")
+public class CfCouponController {
+
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private UserCouponMapper userCouponMapper;
+
+    /** 家庭共享券列表(family_id 匹配当前用户家庭,status=AVAILABLE) */
+    @PostMapping("/family/list")
+    public Result<Page<UserCoupon>> familyList(@RequestAttribute("userId") Long userId,
+                                               @RequestBody Map<String, Integer> params) {
+        int page = params.getOrDefault("page", 1);
+        int size = params.getOrDefault("size", 20);
+        User user = userMapper.selectById(userId);
+        Long familyId = user == null ? null : user.getFamilyId();
+        LambdaQueryWrapper<UserCoupon> wrapper = new LambdaQueryWrapper<>();
+        if (familyId != null) {
+            wrapper.eq(UserCoupon::getFamilyId, familyId);
+        } else {
+            wrapper.eq(UserCoupon::getFamilyId, -1L); // 无家庭则不返回任何券
+        }
+        wrapper.eq(UserCoupon::getStatus, "AVAILABLE").orderByDesc(UserCoupon::getReceivedAt);
+        return Result.success(userCouponMapper.selectPage(new Page<>(page, size), wrapper));
+    }
 }
 ```
 
-> **注意:** 该接口需要 `UserMapper` 注入 `CfCommissionController` 以获取 `familyId`,上述为示意,实现时用 `@Resource UserMapper userMapper` 正常注入,查询 `userCoupon WHERE family_id=familyId AND status='AVAILABLE'`。完成后再补完整实现。
+> 说明:本接口与规格 6.3 中的 `/api/coupon/family/list` 一致;原计划将接口塞进 `CfCommissionController` 的写法废弃,改为独立 `CfCouponController`,避免职责混杂。
 
 - [ ] **步骤 5:编译验证**
 
@@ -1159,7 +1199,7 @@ familyCoupons(@RequestAttribute("userId") Long userId,
 - [ ] **步骤 6:Commit**
 
 ```bash
-git add cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java cfc-backend/src/main/java/com/etotem/cfc/controller/CfCouponController.java
 git commit -m "feat(cf): 优惠券绑定家庭 + 家庭券库查询(CF兑换券挂family_id)"
 ```
 
@@ -1254,7 +1294,7 @@ git commit -m "feat(cf): 管理端 CF 返佣阶梯配置接口(list/save/delet
 export const getCfRate = () => request('/api/commission/cf/rate', 'POST')
 export const getCfSummary = () => request('/api/commission/cf/summary', 'POST')
 export const getCfList = (page, size) => request('/api/commission/cf/list', 'POST', { page, size })
-export const getFamilyCoupons = (page, size) => request('/api/commission/cf/family-coupons', 'POST', { page, size })
+export const getFamilyCoupons = (page, size) => request('/api/coupon/family/list', 'POST', { page, size })
 // CF 转让
 export const sendCfTransfer = (toUserId, amount) => request('/api/cf/transfer/send', 'POST', { toUserId, amount })
 export const getCfTransferList = (page, size) => request('/api/cf/transfer/list', 'POST', { page, size })
@@ -1307,7 +1347,7 @@ git commit -m "feat(cf): 小程序推广中心切换到 CF 分佣接口(团队
 
 - [ ] **步骤 3:更新 API_REFERENCE.md**
 
-在 `docs/superpowers/api/API_REFERENCE.md` 新增章节记录:`/api/commission/cf/*`(rate/summary/list/family-coupons)、`/api/cf/transfer/*`(send/list)、`/api/admin/cf-rate-tier/*`(list/save/delete)。
+在 `docs/superpowers/api/API_REFERENCE.md` 新增章节记录:`/api/commission/cf/*`(rate/summary/list)、`/api/cf/transfer/*`(send/list)、`/api/coupon/family/list`、`/api/admin/cf-rate-tier/*`(list/save/delete)。
 
 - [ ] **步骤 4:Commit**
 
@@ -1334,7 +1374,7 @@ git commit -m "docs(cf): API 文档补充 CF 分佣/转让/阶梯配置接口 +
 - 前端 → 任务 13 ✓
 - 测试/文档 → 任务 14 ✓
 
-**2. 占位符扫描:** 无"待定/TODO";任务 5 步骤 1 中 `getEffectivePpoint` 签名猜测已显式标注为实现者必须核对点;任务 10 转让幂等键问题已显式标注;任务 11 family-coupons 示意代码已标注需补全。
+**2. 占位符扫描:** 无"待定/TODO";任务 5 的 `getEffectivePpoint` 已用真实签名 `(productId, categoryId)` 修正;任务 10 转让幂等键已改为唯一 `transferRefId`;任务 11 家庭券库已改为独立 `CfCouponController` 完整实现。
 
 **3. 类型一致性:** `CfRateTier`/`CfTransferRecord`/`CfReferralService`/`CfCommissionService` 名称在后续任务中一致;`refreshRate` 在任务 6 定义、任务 6 定时任务引用一致;`updateTeamSize(userId, delta)` 新签名在任务 6 统一。