# CF 值分佣体系重构 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 将全部订单的返利/分佣统一迁移到 CF 值体系(个人钱包),废弃旧佣金体系写入;返佣比例由全层级团队规模阶梯决定;支持成员间转让 CF 与家庭共享优惠券。 **架构:** 新建 `CfCommissionService` 作为唯一分佣入口,按订单类型区分「双返(当前人+推荐人)」与「只返推荐人(会员/订阅)」。推荐关系以 `referral_tree`(物化路径)为全层级唯一事实源,绑定即物化并回填存量;比例由 `cf_rate_tier` 阶梯配置按 `total_team_size` 匹配。个人 CF 钱包复用 `user_platform_balance`。 **技术栈:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8;验证命令 `mvn clean compile`。 **前置事实(实现须知):** - `referral_tree` 表当前**无任何写入代码**(仅 `CommissionDistService`/`EnergyService` 读取),推荐关系实际存于 `users.referrer_id`(单层)。本计划需:① `bindReferral` 时物化整条祖先链到 `referral_tree`;② 提供存量回填。 - `PlatformPointsService.earn` **当前无幂等检查**,需补 `(ref_type, ref_id)` 去重。 - `promotion_tier_config` 表列(`min_team_size_1st/2nd + commission_rate_l1/l2`)与实体(`minTeamSize/profitSharePercent/enabled`)**不一致**,本计划整体废弃该表与实体,改用 `cf_rate_tier`。 - `CouponService.grant(Long userId, Long couponId, String grantType, String period, String source)` 已存在(line 195)。 - 后端唯一验证方式:`cd cfc-backend && mvn clean compile`(无运行时验证)。 **规格文档:** `docs/superpowers/specs/2026-09-01-cf-commission-redesign.md` --- ## 文件结构 **新建后端:** - `cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java` — 阶梯配置实体 - `cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java` — 阶梯配置 Mapper - `cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java` — CF 流转记录实体 - `cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java` — 流转记录 Mapper - `cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java` — 统一分佣核心 - `cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java` — 推荐树物化 + 团队规模统计 + 转让 - `cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java` — 用户查询接口 - `cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java` — CF 转让接口 - `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java` — 管理端阶梯配置 **修改后端:** - `service/CommissionService.java` — `bindReferral` 物化推荐树;`settle/settleTwoLevel` 标 `@Deprecated`(调用点摘除后) - `service/PlatformPointsService.java` — `earn` 补幂等 - `service/PromotionTierService.java` — 删除 3 层字段逻辑,加 `refreshRate` - `entity/PromotionTier.java` / `entity/PromotionTierConfig.java` — 精简/废弃 - `service/PromotionTierEvalService.java` — 改读 `cf_rate_tier` - `task/PromotionTierCheckScheduledTask.java` — 改调 `refreshRate` - `service/AssessmentOrderService.java` / `PackagePaymentService.java` / `PaymentService.java` / `MembershipService.java` / `MemberSubscriptionService.java` / `ProductOrderService.java` — 迁移结算调用点 - `service/CouponService.java` / `service/FamilyPlatformPointsService.java` — 券挂 `family_id` - `config/DatabaseInitializer.java` — 迁移脚本 - `resources/schema.sql` — 同步 DDL **新建前端(小程序 `cfc-frontend/`):** - `pages/promotion/team.vue`(修改)— 展示全层级团队规模 + 返佣比例 - `pages/promotion/commission.vue`(修改)— CF 分润流水 - `pages/promotion/index.vue`(修改)— 个人 CF 钱包汇总 - `pages/profile-extra/*`(修改)— 家庭券库 + CF 转让入口 - `utils/api.js`(修改)— 新增接口封装 --- ### 任务 1:数据库迁移(DatabaseInitializer + schema.sql) **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` - 修改:`cfc-backend/src/main/resources/schema.sql` - [ ] **步骤 1:确认最新迁移编号** 运行:`grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5` 预期:记录最大编号 `迁移N`(当前应 ≥113)。后续迁移编号从 N+1 递增。 - [ ] **步骤 2:在 `runMigrations()` 末尾追加 cf_rate_tier 建表+种子** ```java // 迁移N+1: 创建 cf_rate_tier 表(CF值返佣阶梯配置,替代 promotion_tier_config) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_rate_tier (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "tier_name VARCHAR(50) NOT NULL COMMENT '档位名称', " + "min_team_size INT NOT NULL DEFAULT 0 COMMENT '团队规模下限(含)', " + "rate_percent INT NOT NULL DEFAULT 0 COMMENT '返佣比例(%)', " + "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越高', " + "enabled TINYINT DEFAULT 1 COMMENT '1启用/0停用', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + "INDEX idx_min_size (min_team_size)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值返佣阶梯配置'"); log.info("已创建cf_rate_tier表"); Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM cf_rate_tier", Integer.class); if (cnt == null || cnt == 0) { jdbcTemplate.execute("INSERT INTO cf_rate_tier (tier_name, min_team_size, rate_percent, sort_order) VALUES " + "('铜牌', 0, 5, 1), ('银牌', 3, 10, 2), ('金牌', 10, 15, 3), ('铂金', 30, 20, 4), ('钻石', 100, 25, 5)"); log.info("cf_rate_tier 种子数据已初始化"); } } catch (Exception e) { log.warn("创建cf_rate_tier表失败: {}", e.getMessage()); } ``` - [ ] **步骤 3:追加 cf_transfer_record 建表** ```java // 迁移N+2: 创建 cf_transfer_record 表(CF值流转记录) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_transfer_record (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "from_user_id BIGINT COMMENT '转出用户ID(null=平台)', " + "to_user_id BIGINT COMMENT '转入用户ID', " + "family_id BIGINT COMMENT '所属家庭ID', " + "amount INT NOT NULL COMMENT 'CF值数量', " + "type VARCHAR(16) NOT NULL COMMENT 'transfer/allocate/refund', " + "ref_type VARCHAR(50) COMMENT '关联业务类型', " + "ref_id BIGINT COMMENT '关联业务ID', " + "remark VARCHAR(255), " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_from (from_user_id), " + "INDEX idx_to (to_user_id), " + "INDEX idx_family (family_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流转记录'"); log.info("已创建cf_transfer_record表"); } catch (Exception e) { log.warn("创建cf_transfer_record表失败: {}", e.getMessage()); } ``` - [ ] **步骤 4:promotion_tier 表精简 + coupon 加 family_id** ```java // 迁移N+3: promotion_tier 精简列(删三层,加 rate_percent) try { jdbcTemplate.execute("ALTER TABLE promotion_tier DROP COLUMN team_size_1st, DROP COLUMN team_size_2nd, DROP COLUMN team_size_3rd"); log.info("promotion_tier 已删除三层团队列"); } catch (Exception e) { log.warn("promotion_tier 三层列删除失败(可能不存在): {}", e.getMessage()); } try { jdbcTemplate.execute("ALTER TABLE promotion_tier ADD COLUMN rate_percent INT DEFAULT 0 COMMENT '当前返佣比例(%)'"); log.info("promotion_tier 已添加 rate_percent"); } catch (Exception e) { log.warn("promotion_tier.rate_percent 已存在: {}", e.getMessage()); } // 迁移N+4: coupon 表添加 family_id try { jdbcTemplate.execute("ALTER TABLE coupon ADD COLUMN family_id BIGINT DEFAULT NULL COMMENT '绑定家庭ID(NULL=不绑定)'"); log.info("coupon 已添加 family_id"); } catch (Exception e) { log.warn("coupon.family_id 已存在: {}", e.getMessage()); } ``` - [ ] **步骤 5:同步 schema.sql** 在 `schema.sql` 末尾追加两张新表(`cf_rate_tier`、`cf_transfer_record`)的 `CREATE TABLE IF NOT EXISTS`(与任务 1 步骤 2/3 SQL 一致);修改 `promotion_tier` 的 CREATE TABLE 去掉 `team_size_1st/2nd/3rd` 并加 `rate_percent`;`coupon` 的 CREATE TABLE 加 `family_id` 列。保持 schema.sql 为完整快照。 - [ ] **步骤 6:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 7:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql git commit -m "feat(cf): 数据库迁移:cf_rate_tier/cf_transfer_record 建表 + promotion_tier 精简 + coupon 加 family_id" ``` --- ### 任务 2:`CfRateTier` / `CfTransferRecord` 实体与 Mapper **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java` - [ ] **步骤 1:创建 `CfRateTier` 实体** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("cf_rate_tier") public class CfRateTier implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String tierName; private Integer minTeamSize; private Integer ratePercent; private Integer sortOrder; private Integer enabled; private Date createdAt; private Date updatedAt; } ``` - [ ] **步骤 2:创建 `CfRateTierMapper`** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.CfRateTier; public interface CfRateTierMapper extends BaseMapper { } ``` - [ ] **步骤 3:创建 `CfTransferRecord` 实体** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("cf_transfer_record") public class CfTransferRecord implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long fromUserId; private Long toUserId; private Long familyId; private Integer amount; private String type; // transfer/allocate/refund private String refType; private Long refId; private String remark; private Date createdAt; } ``` - [ ] **步骤 4:创建 `CfTransferRecordMapper`** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.CfTransferRecord; public interface CfTransferRecordMapper extends BaseMapper { } ``` - [ ] **步骤 5:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 6:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java git commit -m "feat(cf): 新增 cf_rate_tier 与 cf_transfer_record 实体/Mapper" ``` --- ### 任务 3:`PlatformPointsService.earn` 补幂等 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java` - [ ] **步骤 1:为 `earn` 增加幂等检查** 在 `PlatformPointsService.earn`(当前 line 73-84)开头,`amount <= 0` 校验之后、余额变更之前插入: ```java // 幂等:按 (userId, ref_type, ref_id) 去重 if (refId != null) { Long existing = logMapper.selectCount( new LambdaQueryWrapper() .eq(PlatformBalanceLog::getUserId, userId) .eq(PlatformBalanceLog::getRefType, refType) .eq(PlatformBalanceLog::getRefId, refId)); if (existing != null && existing > 0) { return; } } ``` 确认文件已 import `PlatformBalanceLog` 与 `LambdaQueryWrapper`(`PlatformBalanceLog` 当前未 import,需补 `import com.etotem.cfc.entity.PlatformBalanceLog;`;`LambdaQueryWrapper` 已在 line 3 存在)。 - [ ] **步骤 2:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java git commit -m "feat(cf): PlatformPointsService.earn 增加 (ref_type, ref_id) 幂等去重" ``` --- ### 任务 4:`CfReferralService` — 推荐树物化 + 团队规模统计 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java` **背景:** `referral_tree` 表当前无写入。本任务实现:绑定推荐时物化整条祖先链;全层级团队规模统计;按团队规模匹配阶梯比例。 - [ ] **步骤 1:创建 `CfReferralService`** ```java package com.etotem.cfc.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.etotem.cfc.entity.CfRateTier; import com.etotem.cfc.entity.ReferralTree; import com.etotem.cfc.entity.User; import com.etotem.cfc.mapper.CfRateTierMapper; import com.etotem.cfc.mapper.ReferralTreeMapper; import com.etotem.cfc.mapper.UserMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 推荐关系物化与团队规模统计(全层级) */ @Service public class CfReferralService { @Resource private ReferralTreeMapper referralTreeMapper; @Resource private UserMapper userMapper; @Resource private CfRateTierMapper cfRateTierMapper; /** * 绑定推荐关系时物化整条祖先链(含本人 L1)到 referral_tree。 * 调用方:CommissionService.bindReferral() 设置 user.referrerId 之后。 */ @Transactional public void materializeReferralTree(Long childId) { // 防止重复物化 Long cnt = referralTreeMapper.selectCount( new LambdaQueryWrapper().eq(ReferralTree::getChildId, childId)); if (cnt != null && cnt > 0) { return; } // 沿 referrerId 链收集祖先(child 本人算 level 0) List chain = new ArrayList<>(); User cur = userMapper.selectById(childId); while (cur != null && cur.getReferrerId() != null && !chain.contains(cur.getReferrerId())) { chain.add(cur.getReferrerId()); cur = userMapper.selectById(cur.getReferrerId()); } // 写 referral_tree:level = 1..n,path = 从根到该祖先 // path 格式: /ancestorId/.../directReferrerId/ StringBuilder path = new StringBuilder("/"); for (int i = chain.size() - 1; i >= 0; i--) { Long parentId = chain.get(i); path.append(parentId).append("/"); ReferralTree node = new ReferralTree(); node.setParentId(parentId); node.setChildId(childId); node.setLevel(chain.size() - i); node.setPath(path.toString()); node.setCreatedAt(new Date()); referralTreeMapper.insert(node); } } /** * 全层级团队总人数(所有下线,含间接)。 * 统计 referral_tree 中 parent_id = userId 的所有记录数。 */ public int getTotalTeamSize(Long userId) { Long cnt = referralTreeMapper.selectCount( new LambdaQueryWrapper().eq(ReferralTree::getParentId, userId)); return cnt == null ? 0 : cnt.intValue(); } /** * 按团队规模匹配阶梯比例:cf_rate_tier WHERE enabled=1 AND min_team_size <= size ORDER BY min_team_size DESC LIMIT 1 * 无匹配返回 0。 */ public CfRateTier matchRateTier(int teamSize) { List tiers = cfRateTierMapper.selectList( new LambdaQueryWrapper() .eq(CfRateTier::getEnabled, 1) .le(CfRateTier::getMinTeamSize, teamSize) .orderByDesc(CfRateTier::getMinTeamSize) .last("LIMIT 1")); return tiers.isEmpty() ? null : tiers.get(0); } } ``` - [ ] **步骤 2:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java git commit -m "feat(cf): CfReferralService 推荐树物化 + 全层级团队规模 + 阶梯比例匹配" ``` --- ### 任务 5:`CfCommissionService` — 统一分佣核心 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java` - [ ] **步骤 1:创建 `CfCommissionService`** ```java package com.etotem.cfc.service; import com.etotem.cfc.entity.CfRateTier; import com.etotem.cfc.entity.CfTransferRecord; import com.etotem.cfc.entity.ReferralTree; import com.etotem.cfc.entity.User; import com.etotem.cfc.mapper.CfTransferRecordMapper; import com.etotem.cfc.mapper.ReferralTreeMapper; import com.etotem.cfc.mapper.UserMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 统一 CF 值分佣服务。 * 规则: * - 普通订单(商品/套餐/测评):当前消费人返 CF + 推荐人按团队规模阶梯比例分润 * - 会员/订阅订单:只返推荐人,不返当前人 * - 同家庭互推:跳过本人,上溯到第一个非同家庭引荐人 */ @Service public class CfCommissionService { private static final Logger log = LoggerFactory.getLogger(CfCommissionService.class); @Resource private ReferralTreeMapper referralTreeMapper; @Resource private UserMapper userMapper; @Resource private CfReferralService cfReferralService; @Resource private PlatformPointsService platformPointsService; @Resource private CfTransferRecordMapper cfTransferRecordMapper; @Resource private PpointConfigService ppointConfigService; @Resource private SysConfigService sysConfigService; @Resource private ProductMapper productMapper; /** * 通用分佣(双返):当前人 + 推荐人 */ @Transactional public void settle(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId, Integer orderAmountCent, Long productId) { // 当前消费人返 CF(个人钱包) int buyerReturn = calcBuyerReturn(orderType, orderAmountCent, productId, buyerUserId); if (buyerReturn > 0) { try { platformPointsService.earn(buyerUserId, buyerReturn, "order_consume", orderId, "消费返CF:" + orderType); record(buyerUserId, null, null, buyerReturn, "allocate", "order_consume", orderId, "消费返CF"); } catch (Exception e) { log.error("当前人返CF失败: buyer={}, orderId={}, err={}", buyerUserId, orderId, e.getMessage()); } } // 推荐人分润 distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, productId); } /** * 会员/订阅专用:只返推荐人 */ @Transactional public void settleReferrerOnly(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId, Integer orderAmountCent) { distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, null); } /** * 计算当前消费人返 CF。 * 商品:floor(orderAmount/100) × P点 × shareBps/10000 * 非商品:floor(orderAmount/100) × serviceRateBps/10000 */ private int calcBuyerReturn(String orderType, Integer orderAmountCent, Long productId, Long buyerUserId) { if (orderAmountCent == null || orderAmountCent <= 0) return 0; int amountYuan = orderAmountCent / 100; if ("product".equals(orderType) && productId != null) { try { 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; } catch (Exception e) { return 0; } } int serviceRateBps = getSysBps("commission_service_rate", 1000); return amountYuan * serviceRateBps / 10000; } /** * 推荐人分润:查全链路 → 同家庭跳过上溯 → 按阶梯比例 */ private void distributeToReferrers(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId, Integer orderAmountCent, Long productId) { if (orderAmountCent == null || orderAmountCent <= 0) return; int amountYuan = orderAmountCent / 100; // 取买家全链路推荐人(referral_tree,含全部层级) List referrals = referralTreeMapper.selectList( new LambdaQueryWrapper() .eq(ReferralTree::getChildId, buyerUserId) .orderByAsc(ReferralTree::getLevel)); if (referrals == null || referrals.isEmpty()) return; List processed = new ArrayList<>(); for (ReferralTree ref : referrals) { Long referrerId = ref.getParentId(); if (processed.contains(referrerId)) continue; processed.add(referrerId); User referrer = userMapper.selectById(referrerId); if (referrer == null) continue; // 同家庭跳过(D5):不返,继续上溯(循环继续) if (buyerFamilyId != null && buyerFamilyId.equals(referrer.getFamilyId())) { continue; } // 按团队规模匹配阶梯比例 int teamSize = cfReferralService.getTotalTeamSize(referrerId); CfRateTier tier = cfReferralService.matchRateTier(teamSize); int ratePercent = tier == null ? 0 : tier.getRatePercent(); if (ratePercent <= 0) continue; // 分润基数:商品按 P点,非商品按服务费率 int base = calcReferrerBase(orderType, amountYuan, productId); if (base <= 0) continue; int share = base * ratePercent / 100; if (share <= 0) continue; try { platformPointsService.earn(referrerId, share, "referral_dist", orderId, "推荐分润:" + orderType); record(referrerId, null, referrer.getFamilyId(), share, "allocate", "referral_dist", orderId, "推荐分润"); } catch (Exception e) { log.error("推荐人分润失败: referrer={}, orderId={}, err={}", referrerId, orderId, e.getMessage()); } } } private int calcReferrerBase(String orderType, int amountYuan, Long productId) { if ("product".equals(orderType) && productId != null) { try { Product p = productMapper.selectById(productId); if (p == null) return 0; return ppointConfigService.getEffectivePpoint(productId, p.getCategoryId()); } catch (Exception e) { return 0; } } int serviceRateBps = getSysBps("commission_service_rate", 1000); return amountYuan * serviceRateBps / 10000; } private int getSysBps(String key, int def) { try { String v = sysConfigService.getValue(key); if (v != null && !v.isEmpty()) return Integer.parseInt(v); } catch (Exception ignored) {} return def; } private void record(Long fromUserId, Long toUserId, Long familyId, int amount, String type, String refType, Long refId, String remark) { CfTransferRecord r = new CfTransferRecord(); r.setFromUserId(fromUserId); r.setToUserId(toUserId); r.setFamilyId(familyId); r.setAmount(amount); r.setType(type); r.setRefType(refType); r.setRefId(refId); r.setRemark(remark); r.setCreatedAt(new Date()); try { cfTransferRecordMapper.insert(r); } catch (Exception e) { log.warn("写流转记录失败: {}", e.getMessage()); } } } ``` > **签名已确认:** `PpointConfigService.getEffectivePpoint(Long productId, Long categoryId)`(line 75)。商品订单需先 `productMapper.selectById(productId)` 取 `getCategoryId()` 再调用。上述 `calcBuyerReturn`/`calcReferrerBase` 已按此修正。`Product` 实体有 `getCategoryId()`(line 62)。 - [ ] **步骤 2:确认 `PpointConfigService.getEffectivePpoint` 签名(已核对,无需重复 grep)** 签名 `getEffectivePpoint(Long productId, Long categoryId)` 已确认;`ProductMapper`、`Product` 已在步骤 1 注入/引用。 - [ ] **步骤 3:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java git commit -m "feat(cf): CfCommissionService 统一分佣核心(双返/只返推荐人/同家庭上溯/阶梯比例)" ``` --- ### 任务 6:`CommissionService.bindReferral` 物化推荐树 + 团队规模更新 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(line 108-141 `bindReferral`) - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTierConfig.java` - [ ] **步骤 1:`bindReferral` 末尾调用物化** 在 `CommissionService.bindReferral()`(当前 `userMapper.updateById(user)` 与 `onboardingService.completeTask` 之间)插入: ```java // 物化推荐树(全层级) try { cfReferralService.materializeReferralTree(userId); } catch (Exception e) { log.warn("推荐树物化失败 userId={}", userId, e); } ``` 在类中新增注入: ```java @Resource private CfReferralService cfReferralService; ``` - [ ] **步骤 2:`PromotionTier` 实体精简** 删除 `teamSize1st/teamSize2nd/teamSize3rd` 三个字段(含 `@TableField` 注解),新增: ```java private Integer ratePercent; ``` - [ ] **步骤 3:`PromotionTierService` 重写 `updateTeamSize` + 新增 `refreshRate`** 将 `updateTeamSize(Long userId, int level, int delta)` 替换为: ```java /** * 团队规模 +delta(新推荐绑定后对所有祖先增量)。level 参数已废弃(改为全层级)。 */ public void updateTeamSize(Long userId, int delta) { PromotionTier tier = getCurrentTier(userId); if (tier == null) { tier = new PromotionTier(); tier.setUserId(userId); tier.setTier("R0"); tier.setTotalTeamSize(0); tier.setTotalReferralEarnings(0); tier.setTotalShareEarnings(0); tier.setCreatedAt(new Date()); tier.setUpdatedAt(new Date()); tierMapper.insert(tier); } tier.setTotalTeamSize((tier.getTotalTeamSize() == null ? 0 : tier.getTotalTeamSize()) + delta); tier.setLastChangeAt(new Date()); tier.setUpdatedAt(new Date()); tierMapper.updateById(tier); refreshRate(userId); } /** * 按全层级团队规模刷新等级与返佣比例(读 cf_rate_tier) */ public void refreshRate(Long userId) { try { int teamSize = cfReferralService.getTotalTeamSize(userId); CfRateTier tier = cfReferralService.matchRateTier(teamSize); PromotionTier pt = getCurrentTier(userId); if (pt == null) return; if (tier != null) { pt.setTier(tier.getTierName()); pt.setRatePercent(tier.getRatePercent()); } else { pt.setRatePercent(0); } pt.setUpdatedAt(new Date()); tierMapper.updateById(pt); } catch (Exception e) { log.warn("refreshRate失败 userId={}", userId, e); } } ``` 需要注入 `CfReferralService cfReferralService` 与 `CfRateTier` import。同时删除 `addReferralEarnings/addShareEarnings` 中对 `teamSize1st/2nd/3rd` 的依赖(现有实现未用它们,可直接保留,但确认无编译错误)。 - [ ] **步骤 4:`PromotionTierEvalService` 改读 cf_rate_tier** 将 `evaluateTier` 方法体替换为: ```java public String evaluateTier(Long userId) { try { int teamSize = cfReferralService.getTotalTeamSize(userId); CfRateTier tier = cfReferralService.matchRateTier(teamSize); return tier == null ? "R0" : tier.getTierName(); } catch (Exception e) { return "R0"; } } ``` 删除对 `promotion_tier_config` / `PromotionTierConfig` / `configMapper` 的依赖(或保留但不再查询)。`PromotionTierConfig` 实体标记 `@Deprecated` 并在类上加注释"已废弃,改用 cf_rate_tier"。 - [ ] **步骤 5:`PromotionTierCheckScheduledTask` 改调 `refreshRate`** 将定时任务改为批量 `refreshRate`(替代 `evaluateAllUsers`): ```java @Scheduled(cron = "0 0 1 * * ?") public void checkAndUpdateAllTiers() { log.info("开始执行推广等级定时刷新任务"); try { List allTiers = promotionTierMapper.selectList(null); for (PromotionTier pt : allTiers) { try { promotionTierService.refreshRate(pt.getUserId()); } catch (Exception e) { /* 单用户失败不影响 */ } } log.info("推广等级定时刷新完成,共{}个用户", allTiers.size()); } catch (Exception e) { log.error("推广等级定时刷新失败", e); } } ``` 需要注入 `PromotionTierMapper`。 - [ ] **步骤 6:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS(若 `CommissionDistService` 或 `EnergyService` 引用了已删除字段需一并修正) - [ ] **步骤 7:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTierConfig.java cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierEvalService.java cfc-backend/src/main/java/com/etotem/cfc/task/PromotionTierCheckScheduledTask.java git commit -m "feat(cf): 推荐绑定物化推荐树 + 团队规模全层级统计 + 阶梯比例刷新(替代 promotion_tier_config)" ``` --- ### 任务 7:订单结算迁移点切换(7 处) **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java:76` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java:220` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java:240` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java:784` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java:174,255` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java:633-648` - [ ] **步骤 1:测评订单迁移(AssessmentOrderService:76)** 将 `commissionService.settle(order.getId(), "assessment", order.getUserId(), ..., null)` 替换为: ```java cfCommissionService.settle(order.getId(), "assessment", order.getUserId(), userMapper.selectById(order.getUserId()).getFamilyId(), order.getActualPrice() != null ? order.getActualPrice().intValue() : 0, null); ``` 注入 `CfCommissionService cfCommissionService` 与 `UserMapper userMapper`(如已存在则复用)。注意 `orderType` 传 `"assessment"`。 - [ ] **步骤 2:套餐订单迁移(PackagePaymentService:220 + PaymentService:240)** 将 `commissionService.settle(..., "package", ...)` 替换为: ```java cfCommissionService.settle(order.getId(), "package", order.getUserId(), order.getFamilyId(), order.getPrice(), null); ``` 若 `order` 无 `getFamilyId()`,改用 `userMapper.selectById(order.getUserId()).getFamilyId()`。`orderType` 传 `"package"`。 > **注意:** `PackagePaymentService:220` 与 `PaymentService:240` 都处理 package 结算。因 `CfCommissionService` 依赖 `platformPointsService.earn` 的 `(ref_type, ref_id)` 幂等(任务 3 已实现),重复调用会自动跳过,无需额外去重逻辑。 - [ ] **步骤 3:会员订单迁移(MembershipService:784)** 将 `commissionService.settleTwoLevel(..., "membership", adminUserId, ...)` 替换为: ```java cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId, userMapper.selectById(adminUserId).getFamilyId(), order.getAmount()); ``` 注入 `CfCommissionService`。`orderType` 传 `"membership"`。 - [ ] **步骤 4:订阅订单迁移(MemberSubscriptionService:174,255)** 两处 `commissionService.settleTwoLevel(..., "subscription", family.getCreatorId(), amount, null)` 替换为: ```java cfCommissionService.settleReferrerOnly(order.getId(), "subscription", family.getCreatorId(), family.getId(), amount); ``` 注入 `CfCommissionService`。`orderType` 传 `"subscription"`。 - [ ] **步骤 5:商品订单统一入口(ProductOrderService:633-648)** 将 `:633` 个人 CF 返现块与 `:645` 推荐人分润块**整体替换**为单次 `cfCommissionService.settle(...)` 调用: ```java // CF值:统一分佣(当前人 + 推荐人) try { cfCommissionService.settle(order.getId(), "product", order.getBuyerId(), userMapper.selectById(order.getBuyerId()).getFamilyId(), order.getTotalAmount(), order.getProductId()); } catch (Exception e) { log.error("CF值分佣失败: orderId={}, error={}", order.getId(), e.getMessage()); } ``` 删除原 `:622-641` 的 `platformPointsService.earn(...)` 块与 `:643-648` 的 `commissionDistService.distribute(...)` 块。保留 `:827 confirmReceive` 家庭池返 CF(任务外,不改)。 - [ ] **步骤 6:活动订单确认** 确认 `ActivityOrderService` 无结算调用(现有即无),无需改动。 - [ ] **步骤 7:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS(需处理旧 `commissionService` 字段可能变为未使用——若不再被引用可保留注入或移除) - [ ] **步骤 8:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java git commit -m "feat(cf): 全部订单结算迁移到 CfCommissionService(统一 CF 值分佣),废弃旧佣金调用" ``` --- ### 任务 8:旧佣金体系废弃标记 + 清理 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java` - [ ] **步骤 1:确认无残留调用点** 运行:`grep -rn "commissionService.settle\|commissionService.settleTwoLevel" cfc-backend/src/main/java/com/etotem/cfc` 预期:仅 `CommissionService.java` 内部定义(`settle` → `settleTwoLevel` 委托),无其他业务调用。 - [ ] **步骤 2:`settle`/`settleTwoLevel` 加废弃注释** 确认 `settle`(line 180)与 `settleTwoLevel`(line 194)已有 `@Deprecated` 注解。在方法 javadoc 补充: ```java /** * 已废弃(CF值分佣替代)。保留历史数据读取与兼容旧调用方,不再被业务订单调用。 */ ``` - [ ] **步骤 3:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java git commit -m "chore(cf): 旧佣金 settle/settleTwoLevel 确认废弃(无业务调用方)" ``` --- ### 任务 9:用户查询接口(团队规模 + 返佣比例 + CF 流水) **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java` - [ ] **步骤 1:创建 `CfCommissionController`** ```java package com.etotem.cfc.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.CfRateTier; import com.etotem.cfc.entity.PlatformBalanceLog; import com.etotem.cfc.service.CfReferralService; import com.etotem.cfc.service.PlatformPointsService; import com.etotem.cfc.service.PromotionTierService; 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.HashMap; import java.util.Map; @RestController @RequestMapping("/api/commission/cf") public class CfCommissionController { @Resource private CfReferralService cfReferralService; @Resource private PlatformPointsService platformPointsService; @Resource private PromotionTierService promotionTierService; /** 我的团队规模 + 当前返佣比例 + 档位名 */ @PostMapping("/rate") public Result> rate(@RequestAttribute("userId") Long userId) { int teamSize = cfReferralService.getTotalTeamSize(userId); CfRateTier tier = cfReferralService.matchRateTier(teamSize); Map data = new HashMap<>(); data.put("totalTeamSize", teamSize); data.put("ratePercent", tier == null ? 0 : tier.getRatePercent()); data.put("tierName", tier == null ? "未入档" : tier.getTierName()); return Result.success(data); } /** 我的 CF 钱包汇总 */ @PostMapping("/summary") public Result> summary(@RequestAttribute("userId") Long userId) { return Result.success(platformPointsService.getBalance(userId)); } /** CF 流水(分页) */ @PostMapping("/list") public Result> list(@RequestAttribute("userId") Long userId, @RequestBody Map params) { int page = params.getOrDefault("page", 1); int size = params.getOrDefault("size", 20); return Result.success(platformPointsService.getLogs(userId, page, size)); } } ``` - [ ] **步骤 2:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java git commit -m "feat(cf): 用户查询接口(团队规模/返佣比例/CF汇总/流水)" ``` --- ### 任务 10:CF 转让接口 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java` - [ ] **步骤 1:创建 `CfTransferController`** ```java 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.CfTransferRecord; import com.etotem.cfc.entity.User; import com.etotem.cfc.mapper.CfTransferRecordMapper; import com.etotem.cfc.mapper.UserMapper; import com.etotem.cfc.service.PlatformPointsService; import org.springframework.transaction.annotation.Transactional; 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.Date; import java.util.Map; @RestController @RequestMapping("/api/cf/transfer") public class CfTransferController { @Resource private PlatformPointsService platformPointsService; @Resource private CfTransferRecordMapper cfTransferRecordMapper; @Resource private UserMapper userMapper; /** 成员间转让 CF(需同家庭) */ @PostMapping("/send") @Transactional public Result send(@RequestAttribute("userId") Long fromUserId, @RequestBody Map params) { Long toUserId = ((Number) params.get("toUserId")).longValue(); int amount = ((Number) params.get("amount")).intValue(); if (amount <= 0) { return Result.error("CF值必须为正数"); } User from = userMapper.selectById(fromUserId); User to = userMapper.selectById(toUserId); if (from == null || to == null) { return Result.error("用户不存在"); } 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", transferRefId, "转给成员: " + (to.getNickname() == null ? toUserId : to.getNickname())); platformPointsService.earn(toUserId, amount, "cf_transfer_in", transferRefId, "收到成员转让: " + (from.getNickname() == null ? fromUserId : from.getNickname())); CfTransferRecord record = new CfTransferRecord(); record.setFromUserId(fromUserId); record.setToUserId(toUserId); record.setFamilyId(from.getFamilyId()); record.setAmount(amount); record.setType("transfer"); record.setRemark("成员间转让"); record.setCreatedAt(new Date()); cfTransferRecordMapper.insert(record); return Result.success("转让成功"); } /** 转让记录(分页,按当前用户家庭) */ @PostMapping("/list") public Result> list(@RequestAttribute("userId") Long userId, @RequestBody Map 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 wrapper = new LambdaQueryWrapper<>(); if (familyId != null) { wrapper.eq(CfTransferRecord::getFamilyId, familyId); } wrapper.orderByDesc(CfTransferRecord::getCreatedAt); return Result.success(cfTransferRecordMapper.selectPage(new Page<>(page, size), wrapper)); } } ``` > **幂等说明:** 每次转让生成唯一 `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:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java git commit -m "feat(cf): CF 成员间转让接口(同家庭校验 + 双账本 + 流水)" ``` --- ### 任务 11:优惠券绑定家庭(家庭券库) **文件:** - 修改:`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`(新增家庭券库接口) - [ ] **步骤 1:`UserCoupon` 实体加 `familyId`** `cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java` 新增字段: ```java private Long familyId; ``` - [ ] **步骤 2:`grant` 方法支持传 familyId** `CouponService.grant`(line 195)增加重载: ```java public boolean grant(Long userId, Long couponId, String grantType, String period, String source, Long familyId) { UserCoupon uc = new UserCoupon(); uc.setUserId(userId); uc.setCouponId(couponId); uc.setStatus("AVAILABLE"); uc.setReceivedAt(new Date()); uc.setFamilyId(familyId); userCouponMapper.insert(uc); return true; } ``` 原 5 参 `grant` 保留并委托新方法(传 null)。核对原 `grant` 现有实现(line 195-231),保持 `CouponGrantLog` 写入等原有逻辑不丢失。 - [ ] **步骤 3:`exchangeCouponByCf` 兑换后券挂家庭** `FamilyPlatformPointsService.exchangeCouponByCf`(line 271)调用改为: ```java couponService.grant(refUserId, couponId, "CF_EXCHANGE", null, "family:" + familyId, familyId); ``` - [ ] **步骤 4:家庭券库查询接口(新建独立 Controller)** 新建 `cfc-backend/src/main/java/com/etotem/cfc/controller/CfCouponController.java`(避免在 `CfCommissionController` 注入券相关 Mapper 导致职责混杂): ```java 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> familyList(@RequestAttribute("userId") Long userId, @RequestBody Map 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 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)); } } ``` > 说明:本接口与规格 6.3 中的 `/api/coupon/family/list` 一致;原计划将接口塞进 `CfCommissionController` 的写法废弃,改为独立 `CfCouponController`,避免职责混杂。 - [ ] **步骤 5:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 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/CfCouponController.java git commit -m "feat(cf): 优惠券绑定家庭 + 家庭券库查询(CF兑换券挂family_id)" ``` --- ### 任务 12:管理端阶梯配置接口 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java` - [ ] **步骤 1:创建 `AdminCfRateTierController`** ```java package com.etotem.cfc.controller.admin; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.CfRateTier; import com.etotem.cfc.mapper.CfRateTierMapper; import org.springframework.web.bind.annotation.PostMapping; 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.Date; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/admin/cf-rate-tier") public class AdminCfRateTierController { @Resource private CfRateTierMapper cfRateTierMapper; @PostMapping("/list") public Result> list() { return Result.success(cfRateTierMapper.selectList( new LambdaQueryWrapper().orderByAsc(CfRateTier::getSortOrder))); } @PostMapping("/save") public Result save(@RequestBody CfRateTier tier) { if (tier.getId() != null) { tier.setUpdatedAt(new Date()); cfRateTierMapper.updateById(tier); } else { tier.setCreatedAt(new Date()); tier.setUpdatedAt(new Date()); if (tier.getEnabled() == null) tier.setEnabled(1); cfRateTierMapper.insert(tier); } return Result.success("已保存"); } @PostMapping("/delete") public Result delete(@RequestBody Map params) { Long id = ((Number) params.get("id")).longValue(); cfRateTierMapper.deleteById(id); return Result.success("已删除"); } } ``` - [ ] **步骤 2:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java git commit -m "feat(cf): 管理端 CF 返佣阶梯配置接口(list/save/delete)" ``` --- ### 任务 13:前端改造(小程序) **文件:** - 修改:`cfc-frontend/utils/api.js` - 修改:`cfc-frontend/pages/promotion/team.vue` - 修改:`cfc-frontend/pages/promotion/commission.vue` - 修改:`cfc-frontend/pages/promotion/index.vue` - [ ] **步骤 1:`utils/api.js` 新增接口封装** ```js // CF 分佣 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/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 }) ``` - [ ] **步骤 2:`pages/promotion/team.vue` 改造** 将 stats-card 改为展示 `getCfRate()` 返回的 `totalTeamSize` / `ratePercent` / `tierName`(替换原 L1/L2/团队佣金),模板与 methods 同步替换。删除 `getCommissionTeam` 依赖(或保留兼容)。 - [ ] **步骤 3:`pages/promotion/commission.vue` 改造** 数据源从旧 `/api/commission/list` 切换为 `getCfList()`,展示 CF 流水(金额/类型/时间),注意时间用 `parseDate()` 格式化。 - [ ] **步骤 4:`pages/promotion/index.vue` 改造** 头部汇总改用 `getCfSummary()`(available/frozen/totalEarned),保留推广二维码/邀请等原有功能。 - [ ] **步骤 5:前端语法校验** 运行:`node -e "require('@babel/parser')"` 不可用则用 `node --check` 提取 script 块逐文件校验。小程序由 HBuilderX 打包(Agent 不执行 build)。 - [ ] **步骤 6:Commit** ```bash git add cfc-frontend/utils/api.js cfc-frontend/pages/promotion/team.vue cfc-frontend/pages/promotion/commission.vue cfc-frontend/pages/promotion/index.vue git commit -m "feat(cf): 小程序推广中心切换到 CF 分佣接口(团队规模/比例/流水/钱包)" ``` --- ### 任务 14:测试与收尾 **文件:** - 创建:`cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java`(可选,若测试基建可用) - 修改:`docs/superpowers/api/API_REFERENCE.md` - [ ] **步骤 1:单元测试(阶梯匹配边界)** 若后端测试基建可用,创建测试: ```java // 验证 matchRateTier 边界:0/2/3/9/10/29/30/99/100 // 0 → 铜牌5;3 → 银牌10;10 → 金牌15;30 → 铂金20;100 → 钻石25 ``` - [ ] **步骤 2:全量编译 + 测试** 运行:`cd cfc-backend && mvn clean compile && mvn test` 预期:BUILD SUCCESS;测试通过(或记录预存失败) - [ ] **步骤 3:更新 API_REFERENCE.md** 在 `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** ```bash git add cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java docs/superpowers/api/API_REFERENCE.md git commit -m "docs(cf): API 文档补充 CF 分佣/转让/阶梯配置接口 + 单元测试" ``` --- ## 自检记录 **1. 规格覆盖度:** - 数据模型(cf_rate_tier / cf_transfer_record / 复用 user_platform_balance / coupon+family_id)→ 任务 1、2、11 ✓ - 统一分佣服务 + 双返/只返推荐人 + 同家庭上溯 + 阶梯比例 → 任务 5 ✓ - 推荐树物化 + 团队规模全层级 → 任务 4、6 ✓ - 等级评估链路修复(updateTeamSize 调用 + refreshRate + 定时任务)→ 任务 6 ✓ - 订单迁移点 7 处 → 任务 7 ✓ - 旧佣金废弃 → 任务 8 ✓ - 用户查询(团队规模/比例/流水)→ 任务 9 ✓ - CF 转让 → 任务 10 ✓ - 家庭券库 → 任务 11 ✓ - 管理端阶梯配置 → 任务 12 ✓ - 前端 → 任务 13 ✓ - 测试/文档 → 任务 14 ✓ **2. 占位符扫描:** 无"待定/TODO";任务 5 的 `getEffectivePpoint` 已用真实签名 `(productId, categoryId)` 修正;任务 10 转让幂等键已改为唯一 `transferRefId`;任务 11 家庭券库已改为独立 `CfCouponController` 完整实现。 **3. 类型一致性:** `CfRateTier`/`CfTransferRecord`/`CfReferralService`/`CfCommissionService` 名称在后续任务中一致;`refreshRate` 在任务 6 定义、任务 6 定时任务引用一致;`updateTeamSize(userId, delta)` 新签名在任务 6 统一。 --- ## 执行交接 计划已完成并保存到 `docs/superpowers/plans/2026-09-01-cf-commission-redesign.md`。两种执行方式: **1. 子代理驱动(推荐)** — 每个任务调度一个新的子代理,任务间进行审查,快速迭代 **2. 内联执行** — 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点