2026-08-12-platform-points-family-pool-design.md 9.1 KB

家庭平台积分(CF值)体系设计

日期:2026-08-12 分支:cfclub 状态:待评审

1. 背景与目标

现有系统有三套并行积分体系,职责不清、兑换能力空壳:

体系 维度 发放 消费
孩子积分 PointsService 孩子(family_member) 任务/打卡/签到/文章 心愿兑换、优惠券兑换、兑换商品
家长积分 User.totalPoints 家长(user) 无自动发放 商品订单积分抵扣(1积分=¥0.01)
CF值 PlatformPointsService 用户(user_id 唯一键) 商品订单支付时、推荐分润、活动签到 仅提现;spend() 零调用

用户需求(已确认):

  1. 平台积分(CF值)是给整个家庭的共享池;家庭内部积分(家长积分+孩子积分)在每个人身上,两者是不同维度。
  2. 商品按实际购买金额送积分:1元 = 1积分,可针对商品设置积分倍数,按实付金额计算。
  3. 积分可兑换优惠券(保持现状:孩子积分兑换,家长积分/平台CF值不参与)。
  4. 每个产品设置优惠券的可用积分(现状已有Coupon.pointsPrice)。
  5. 平台CF值消费场景本次一并做:会员续费抵扣 + 商品购买抵扣
  6. user_platform_balance 余额一并迁入家庭池(按 user→family 归属;无家庭自动创建)。
  7. 家长积分彻底退出商品购买抵扣(现有结算页家长积分抵扣替换为平台CF值抵扣),家长积分只用于家庭内部许愿兑换。

2. 目标架构

平台CF值(家庭维度,family_platform_balance)
├─ 发放:商品确认收货(实付×倍数)、推荐分润、活动签到
├─ 消费:会员续费抵扣、商品购买抵扣、提现
└─ 原语:earn / spend / freeze / unfreeze / getBalance / getLogs / adjust

家庭内部积分(个人维度)
├─ 孩子(family_member):任务/打卡 → 心愿、兑换商品、兑换优惠券(现状保持)
└─ 家长(user.total_points):仅家庭内部许愿兑换(商品抵扣入口移除)

3. 数据库设计

3.1 新表 family_platform_balance

CREATE TABLE family_platform_balance (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  family_id BIGINT NOT NULL,
  total_earned INT NOT NULL DEFAULT 0,   -- 累计获得
  available INT NOT NULL DEFAULT 0,       -- 可用
  frozen INT NOT NULL DEFAULT 0,          -- 提现冻结
  withdrawn INT NOT NULL DEFAULT 0,       -- 已提现
  exchanged INT NOT NULL DEFAULT 0,       -- 已消费(抵扣/兑换)
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_family (family_id)
);

3.2 新表 family_platform_balance_log

CREATE TABLE family_platform_balance_log (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  family_id BIGINT NOT NULL,
  type VARCHAR(20) NOT NULL,              -- earn/spend/freeze/unfreeze/withdraw/adjust
  amount INT NOT NULL,
  balance_after INT NOT NULL,
  ref_type VARCHAR(50) NOT NULL,          -- product_order/referral_dist/activity/withdrawal/adjust/refund
  ref_id BIGINT,
  remark VARCHAR(255),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_family (family_id),
  KEY idx_ref (ref_type, ref_id)
);

3.3 修改 products

ALTER TABLE products ADD COLUMN points_multiplier DECIMAL(4,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数';

3.4 迁移

  • 一次性迁移:user_platform_balance 按 user.family_id 归属累加进 family_platform_balance;user 无 familyId 的自动创建家庭(无则 new)。
  • 迁移记录落 family_platform_balance_log(refType=migration,remark=来源 userId 列表),避免重复。
  • 迁移后 user_platform_balance保留(只读历史),新发放入家庭池。

4. 后端实现

4.1 FamilyPlatformPointsService(新)

仿 PlatformPointsService 实现,键为 familyId:

  • earn(familyId, amount, refType, refId, remark):total_earned+available,写流水,refId 幂等
  • spend(familyId, amount, refType, refId, remark):available→exchanged,余额不足抛错
  • freeze(familyId, amount, refId) / unfreeze(familyId, amount, refId) / confirmWithdraw(familyId, amount, refId):提现
  • adjust(familyId, amount, remark):管理端手动调整(正负),写 adjust 流水
  • getBalance(familyId) / getLogs(familyId, page, size) / getWithdrawable(familyId)
  • 并发安全:SELECT ... FOR UPDATE 锁行

4.2 商品送积分(核心)

触发点:ProductOrderService.confirmReceive()(L787-813)用户确认收货,status: pending_receipt → completed 成功分支之后。

计算:积分 = floor(moneyAmount / 100) × points_multiplier(向下取整)

  • moneyAmount:订单实付金额(分),即现金支付部分(totalAmount - pointsCost),积分抵扣的金额不产生积分,确认收货时已定
  • points_multiplier:从 products 表按 productId
  • 发放:familyPlatformPointsService.earn(familyId, points, "product_order", orderId, "购买商品: {productName}")
  • 幂等:earn 按 (ref_type, ref_id) 查流水,存在则跳过
  • 订单需能拿到 familyId:ProductOrder.buyerId → user.familyId

4.3 商品购买抵扣(替换家长积分抵扣)

  • 结算页 checkout.vue:积分抵扣输入从"家长积分"改为"家庭平台积分"
  • 下单入参 CreateProductOrderDTO.pointsUsed 语义不变(数量)
  • ProductOrderService:支付成功分支的 deductUserPoints 改为 familyPlatformPointsService.spend(familyId, pointsUsed, "product_order", orderId, "商品购买积分抵扣")
  • 支付失败/退款:refundUserPoints 改为 earn 回补(refType=refund
  • User.totalPoints 商品抵扣路径删除(保留字段,仅许愿兑换使用)

4.4 会员续费抵扣

MemberSubscriptionService.renew():增加可选平台积分抵扣参数

  • 抵扣比例可配置:sys_config membership_points_deduct_ratio(默认如 1000 = 10%)
  • 抵扣后实付 = max(0, 原价 - 抵扣额)
  • 成功即 spend(familyId, points, "membership", subId, "会员续费抵扣")

4.5 提现

WithdrawalService 改走家庭池(freeze/confirmWithdraw/unfreeze 换服务),新增提现记录含 familyId。

4.6 管理端

  • AdminFamilyBalanceController/api/admin/family-platform-balance/*
    • list(分页:family_id/余额/流水摘要)
    • logs(family_id + type/refType 筛选)
    • adjust(手动调整 + remark)
  • cfc-web 新增 FamilyPlatformBalanceManage.vue 页面(余额列表 + 流水 + 调整弹窗)

4.7 API(小程序)

  • GET /api/family-platform/balance:家庭余额(含累计/冻结/可提现)
  • GET /api/family-platform/logs:流水分页
  • 商品详情接口 ProductDTO 增加 pointsMultiplier 字段(透出)

5. 前端实现

5.1 小程序(cfc-frontend)

  • utils/api.js:新增 getFamilyPlatformBalance / getFamilyPlatformLogsProductDTO.pointsMultiplier 透出
  • 商品详情页 product-detail.vue:价格区显示"购此商品可得 X 积分"(倍数>1 高亮"×N 倍积分")
  • 结算页 checkout.vue:积分抵扣改为家庭平台积分(余额取自新接口;1积分=¥0.01 规则沿用);显示"预计可得 X 积分"
  • 支付页 payment.vue:积分抵扣文案更新
  • 家庭积分页(并入 pages/wealth/index.vue 或新建):余额 + 流水 + 提现入口

5.2 管理端(cfc-web)

  • 商品编辑表单:新增"积分倍数"输入(ProductForm)
  • 新增 FamilyPlatformBalanceManage.vue:余额列表、流水、手动调整

6. 错误处理与边界

场景 处理
积分余额不足 抵扣/提现抛业务异常,前端提示"平台积分不足"
并发抵扣 行锁 + 事务,二次校验余额
支付失败退款 回补积分(refType=refund),幂等
确认收货重复请求 earn 按 refId 幂等跳过
孤儿 user(无 family) 迁移时自动创建家庭
向下取整 floor,不四舍五入(如 599元×1.5=898 积分)

7. 测试

  • 单元:积分计算(floor + 倍数)、earn/spend/freeze 余额变化、幂等防重
  • 集成:确认收货→积分到账;结算抵扣→支付→spend;支付失败→回补
  • 迁移:旧余额按 family 汇总正确、孤儿自动建家庭、不重复
  • E2E:商品详情积分展示、家庭积分页

8. 实施顺序

  1. 数据库:两表 + products 字段 + 迁移(DatabaseInitializer + schema.sql 同步)
  2. FamilyPlatformPointsService 原语
  3. 商品送积分(confirmReceive 挂点)
  4. 商品购买抵扣替换 + 会员续费抵扣
  5. 提现改造
  6. 管理端接口 + 页面
  7. 小程序前端
  8. 测试 + 部署

9. 明确不做(YAGNI)

  • 不新建"家庭内部积分"表:家长/孩子积分保持现状
  • 优惠券兑换不改:孩子积分兑换,平台CF值/家长积分不参与
  • 不做积分商城(兑换实物商品):现状 PointsExchangeProduct 已有,不动
  • 不迁移 points_log(孩子积分流水),仅迁移 user_platform_balance