面向 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 — 阶梯配置 Mappercfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java — CF 流转记录实体cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java — 流转记录 Mappercfc-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 层字段逻辑,加 refreshRateentity/PromotionTier.java / entity/PromotionTierConfig.java — 精简/废弃service/PromotionTierEvalService.java — 改读 cf_rate_tiertask/PromotionTierCheckScheduledTask.java — 改调 refreshRateservice/AssessmentOrderService.java / PackagePaymentService.java / PaymentService.java / MembershipService.java / MemberSubscriptionService.java / ProductOrderService.java — 迁移结算调用点service/CouponService.java / service/FamilyPlatformPointsService.java — 券挂 family_idconfig/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(修改)— 新增接口封装文件:
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 建表+种子
// 迁移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 建表
// 迁移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
// 迁移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 为完整快照。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 7:Commit
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"
CfRateTier / CfTransferRecord 实体与 Mapper文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.javacfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.javacfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java创建:cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java
[ ] 步骤 1:创建 CfRateTier 实体
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
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.CfRateTier;
public interface CfRateTierMapper extends BaseMapper<CfRateTier> {
}
[ ] 步骤 3:创建 CfTransferRecord 实体
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
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.CfTransferRecord;
public interface CfTransferRecordMapper extends BaseMapper<CfTransferRecord> {
}
[ ] 步骤 5:编译验证
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
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"
PlatformPointsService.earn 补幂等文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java
[ ] 步骤 1:为 earn 增加幂等检查
在 PlatformPointsService.earn(当前 line 73-84)开头,amount <= 0 校验之后、余额变更之前插入:
// 幂等:按 (userId, ref_type, ref_id) 去重
if (refId != null) {
Long existing = logMapper.selectCount(
new LambdaQueryWrapper<PlatformBalanceLog>()
.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 存在)。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java
git commit -m "feat(cf): PlatformPointsService.earn 增加 (ref_type, ref_id) 幂等去重"
CfReferralService — 推荐树物化 + 团队规模统计文件:
cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java背景: referral_tree 表当前无写入。本任务实现:绑定推荐时物化整条祖先链;全层级团队规模统计;按团队规模匹配阶梯比例。
[ ] 步骤 1:创建 CfReferralService
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<ReferralTree>().eq(ReferralTree::getChildId, childId));
if (cnt != null && cnt > 0) {
return;
}
// 沿 referrerId 链收集祖先(child 本人算 level 0)
List<Long> 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<ReferralTree>().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<CfRateTier> tiers = cfRateTierMapper.selectList(
new LambdaQueryWrapper<CfRateTier>()
.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
git add cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java
git commit -m "feat(cf): CfReferralService 推荐树物化 + 全层级团队规模 + 阶梯比例匹配"
CfCommissionService — 统一分佣核心文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java
[ ] 步骤 1:创建 CfCommissionService
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<ReferralTree> referrals = referralTreeMapper.selectList(
new LambdaQueryWrapper<ReferralTree>()
.eq(ReferralTree::getChildId, buyerUserId)
.orderByAsc(ReferralTree::getLevel));
if (referrals == null || referrals.isEmpty()) return;
List<Long> 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)。
PpointConfigService.getEffectivePpoint 签名(已核对,无需重复 grep)签名 getEffectivePpoint(Long productId, Long categoryId) 已确认;ProductMapper、Product 已在步骤 1 注入/引用。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java
git commit -m "feat(cf): CfCommissionService 统一分佣核心(双返/只返推荐人/同家庭上溯/阶梯比例)"
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.javacfc-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 之间)插入:
// 物化推荐树(全层级)
try { cfReferralService.materializeReferralTree(userId); } catch (Exception e) { log.warn("推荐树物化失败 userId={}", userId, e); }
在类中新增注入:
@Resource
private CfReferralService cfReferralService;
PromotionTier 实体精简删除 teamSize1st/teamSize2nd/teamSize3rd 三个字段(含 @TableField 注解),新增:
private Integer ratePercent;
PromotionTierService 重写 updateTeamSize + 新增 refreshRate将 updateTeamSize(Long userId, int level, int delta) 替换为:
/**
* 团队规模 +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 的依赖(现有实现未用它们,可直接保留,但确认无编译错误)。
PromotionTierEvalService 改读 cf_rate_tier将 evaluateTier 方法体替换为:
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"。
PromotionTierCheckScheduledTask 改调 refreshRate将定时任务改为批量 refreshRate(替代 evaluateAllUsers):
@Scheduled(cron = "0 0 1 * * ?")
public void checkAndUpdateAllTiers() {
log.info("开始执行推广等级定时刷新任务");
try {
List<PromotionTier> 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。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS(若 CommissionDistService 或 EnergyService 引用了已删除字段需一并修正)
[ ] 步骤 7:Commit
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)"
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java:76cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java:220cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java:240cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java:784cfc-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) 替换为:
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"。
将 commissionService.settle(..., "package", ...) 替换为:
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 已实现),重复调用会自动跳过,无需额外去重逻辑。
将 commissionService.settleTwoLevel(..., "membership", adminUserId, ...) 替换为:
cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId,
userMapper.selectById(adminUserId).getFamilyId(), order.getAmount());
注入 CfCommissionService。orderType 传 "membership"。
两处 commissionService.settleTwoLevel(..., "subscription", family.getCreatorId(), amount, null) 替换为:
cfCommissionService.settleReferrerOnly(order.getId(), "subscription", family.getCreatorId(),
family.getId(), amount);
注入 CfCommissionService。orderType 传 "subscription"。
将 :633 个人 CF 返现块与 :645 推荐人分润块整体替换为单次 cfCommissionService.settle(...) 调用:
// 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(任务外,不改)。
确认 ActivityOrderService 无结算调用(现有即无),无需改动。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS(需处理旧 commissionService 字段可能变为未使用——若不再被引用可保留注入或移除)
[ ] 步骤 8:Commit
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 值分佣),废弃旧佣金调用"
文件:
修改: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 委托),无其他业务调用。
settle/settleTwoLevel 加废弃注释确认 settle(line 180)与 settleTwoLevel(line 194)已有 @Deprecated 注解。在方法 javadoc 补充:
/**
* 已废弃(CF值分佣替代)。保留历史数据读取与兼容旧调用方,不再被业务订单调用。
*/
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "chore(cf): 旧佣金 settle/settleTwoLevel 确认废弃(无业务调用方)"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
[ ] 步骤 1:创建 CfCommissionController
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<Map<String, Object>> rate(@RequestAttribute("userId") Long userId) {
int teamSize = cfReferralService.getTotalTeamSize(userId);
CfRateTier tier = cfReferralService.matchRateTier(teamSize);
Map<String, Object> 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<Map<String, Object>> summary(@RequestAttribute("userId") Long userId) {
return Result.success(platformPointsService.getBalance(userId));
}
/** CF 流水(分页) */
@PostMapping("/list")
public Result<Page<PlatformBalanceLog>> list(@RequestAttribute("userId") Long userId,
@RequestBody Map<String, Integer> 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
git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
git commit -m "feat(cf): 用户查询接口(团队规模/返佣比例/CF汇总/流水)"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java
[ ] 步骤 1:创建 CfTransferController
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<String> send(@RequestAttribute("userId") Long fromUserId,
@RequestBody Map<String, Object> 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<Page<CfTransferRecord>> list(@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<CfTransferRecord> 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()。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java
git commit -m "feat(cf): CF 成员间转让接口(同家庭校验 + 双账本 + 流水)"
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.javacfc-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 新增字段:
private Long familyId;
grant 方法支持传 familyIdCouponService.grant(line 195)增加重载:
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 写入等原有逻辑不丢失。
exchangeCouponByCf 兑换后券挂家庭FamilyPlatformPointsService.exchangeCouponByCf(line 271)调用改为:
couponService.grant(refUserId, couponId, "CF_EXCHANGE", null, "family:" + familyId, familyId);
新建 cfc-backend/src/main/java/com/etotem/cfc/controller/CfCouponController.java(避免在 CfCommissionController 注入券相关 Mapper 导致职责混杂):
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));
}
}
说明:本接口与规格 6.3 中的
/api/coupon/family/list一致;原计划将接口塞进CfCommissionController的写法废弃,改为独立CfCouponController,避免职责混杂。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
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)"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java
[ ] 步骤 1:创建 AdminCfRateTierController
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<CfRateTier>> list() {
return Result.success(cfRateTierMapper.selectList(
new LambdaQueryWrapper<CfRateTier>().orderByAsc(CfRateTier::getSortOrder)));
}
@PostMapping("/save")
public Result<String> 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<String> delete(@RequestBody Map<String, Object> 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
git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java
git commit -m "feat(cf): 管理端 CF 返佣阶梯配置接口(list/save/delete)"
文件:
cfc-frontend/utils/api.jscfc-frontend/pages/promotion/team.vuecfc-frontend/pages/promotion/commission.vue修改:cfc-frontend/pages/promotion/index.vue
[ ] 步骤 1:utils/api.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 依赖(或保留兼容)。
pages/promotion/commission.vue 改造数据源从旧 /api/commission/list 切换为 getCfList(),展示 CF 流水(金额/类型/时间),注意时间用 parseDate() 格式化。
pages/promotion/index.vue 改造头部汇总改用 getCfSummary()(available/frozen/totalEarned),保留推广二维码/邀请等原有功能。
运行:node -e "require('@babel/parser')" 不可用则用 node --check 提取 script 块逐文件校验。小程序由 HBuilderX 打包(Agent 不执行 build)。
[ ] 步骤 6:Commit
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 分佣接口(团队规模/比例/流水/钱包)"
文件:
cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java(可选,若测试基建可用)修改:docs/superpowers/api/API_REFERENCE.md
[ ] 步骤 1:单元测试(阶梯匹配边界)
若后端测试基建可用,创建测试:
// 验证 matchRateTier 边界:0/2/3/9/10/29/30/99/100
// 0 → 铜牌5;3 → 银牌10;10 → 金牌15;30 → 铂金20;100 → 钻石25
运行:cd cfc-backend && mvn clean compile && mvn test
预期:BUILD SUCCESS;测试通过(或记录预存失败)
在 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
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. 规格覆盖度:
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 执行任务,批量执行并设有检查点