|
|
@@ -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 统一。
|
|
|
|