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

feat(cf): F-8 清理收尾-三套旧账本写接口410Gone+补CF值单一事实源文档

zxyj-web 2 недель назад
Родитель
Сommit
f4cefe9808
22 измененных файлов с 643 добавлено и 494 удалено
  1. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
  3. 19 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyPlatformBalanceController.java
  4. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
  5. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/CfPathwayEarnService.java
  6. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java
  7. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  8. 79 143
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java
  9. 90 187
      cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java
  10. 40 59
      cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java
  11. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  12. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/service/api/PointsServiceInterface.java
  13. 3 3
      cfc-backend/src/main/resources/schema.sql
  14. 94 70
      cfc-backend/src/test/java/com/etotem/cfc/service/PlatformPointsServiceTest.java
  15. 1 1
      cfc-frontend/pages.json
  16. 1 1
      cfc-frontend/pages/wealth/index.vue
  17. 4 0
      docs/cfc-redesign-progressive-unlock.md
  18. 4 0
      docs/function-analysis-by-membership.md
  19. 5 2
      docs/superpowers/PROJECT-OVERVIEW.md
  20. 6 1
      docs/superpowers/specs/2026-08-12-platform-points-family-pool-design.md
  21. 6 1
      docs/superpowers/specs/2026-09-01-cf-commission-redesign.md
  22. 253 0
      docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2180,7 +2180,7 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         String[][] childColumns = {
         String[][] childColumns = {
                 {"penalty_enabled", "TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)'"},
                 {"penalty_enabled", "TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)'"},
                 {"theme", "VARCHAR(32) DEFAULT 'default' COMMENT '主题'"},
                 {"theme", "VARCHAR(32) DEFAULT 'default' COMMENT '主题'"},
-                {"system_points", "INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励)'"},
+                {"system_points", "INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励,仅只读历史;CF值见cf_balance)'"},
                 {"streak_days", "INT DEFAULT 0 COMMENT '连续打卡天数'"},
                 {"streak_days", "INT DEFAULT 0 COMMENT '连续打卡天数'"},
                 {"last_task_date", "DATE COMMENT '上次任务日期'"},
                 {"last_task_date", "DATE COMMENT '上次任务日期'"},
                 {"focus_max_daily", "TINYINT DEFAULT 3 COMMENT '每日专注上限'"},
                 {"focus_max_daily", "TINYINT DEFAULT 3 COMMENT '每日专注上限'"},
@@ -9146,7 +9146,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         }
         }
         ensureColumn("products", "points_multiplier", "DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数'");
         ensureColumn("products", "points_multiplier", "DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数'");
         log.info("已添加products.points_multiplier列");
         log.info("已添加products.points_multiplier列");
-        // 迁移227: 创建 family_platform_exchange_record 表(家庭CF值兑换记录)
+        // 迁移227: 创建 family_platform_exchange_record 表(优惠券CF兑换记录;券挂家庭,CF扣兑换人个人账本)
         try {
         try {
             jdbcTemplate.execute(
             jdbcTemplate.execute(
                     "CREATE TABLE IF NOT EXISTS family_platform_exchange_record (" +
                     "CREATE TABLE IF NOT EXISTS family_platform_exchange_record (" +
@@ -9163,7 +9163,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
                             "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                             "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
                             "  INDEX idx_family_time (family_id, created_at), " +
                             "  INDEX idx_family_time (family_id, created_at), " +
                             "  INDEX idx_user (user_id)" +
                             "  INDEX idx_user (user_id)" +
-                            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭CF值兑换记录'"
+                            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券CF兑换记录(券挂家庭,CF扣兑换人个人账本cf_balance)'"
             );
             );
             log.info("已创建family_platform_exchange_record表");
             log.info("已创建family_platform_exchange_record表");
         } catch (Exception e) {
         } catch (Exception e) {
@@ -13330,9 +13330,9 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
                     "第1份50/后续每份10,重复报告不计(N10)"},
                     "第1份50/后续每份10,重复报告不计(N10)"},
             {"F10", "家庭挑战完成", "behavior", "per_item", "",
             {"F10", "家庭挑战完成", "behavior", "per_item", "",
                     "",
                     "",
-                    "{\"source\":\"family_challenge.reward_points\",\"pending\":\"Q1 家庭合计 or 按人各发\"}",
+                    "{\"source\":\"family_challenge.reward_points\",\"distribution\":\"per_completed_member\"}",
                     "1", "1", "", "family_challenge", "100",
                     "1", "1", "", "family_challenge", "100",
-                    "按挑战配置数量(按人各发 or 家庭合计待 Q1 确认)"},
+                    "按挑战配置数量(Q1 已定:每个完成成员各发一次)"},
             {"F11", "邀请里程碑", "behavior", "per_item", "",
             {"F11", "邀请里程碑", "behavior", "per_item", "",
                     "",
                     "",
                     "{\"source\":\"invite_milestone.reward_points\"}",
                     "{\"source\":\"invite_milestone.reward_points\"}",

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java

@@ -17,6 +17,17 @@ import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
+/**
+ * CF 分佣/钱包查询接口(⚠️ F-8 / 2026-10-25:/summary 与 /list 为只读历史)
+ *
+ * <p>{@link PlatformPointsService} 的写接口已全部 410 Gone,
+ * {@code user_platform_balance} 与 {@code platform_balance_log} 仅作只读历史账本保留。
+ * 本控制器的 {@code /summary}、{@code /list} 因此只会返回 F-6 迁移后的零值/历史流水。
+ *
+ * <p>实时 CF 余额与流水请走 {@code /api/cf/balance}、{@code /api/cf/logs}
+ * (见 {@code CfBalanceController} → {@code cf_balance} / {@code cf_balance_log},成员维度)。
+ * {@code /rate} 的分佣档位查询(F-4 两级销售奖)不受影响。
+ */
 @RestController
 @RestController
 @RequestMapping("/api/commission/cf")
 @RequestMapping("/api/commission/cf")
 public class CfCommissionController {
 public class CfCommissionController {

+ 19 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyPlatformBalanceController.java

@@ -15,8 +15,18 @@ import java.util.Map;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
 
 
 /**
 /**
- * 家庭平台积分(CF值)接口
- * 提供给小程序端查询余额、流水,以及用CF值兑换优惠券
+ * 家庭维度 CF 接口(⚠️ F-8 / 2026-10-25:家庭池只读历史 + 券兑换)
+ *
+ * <p>CF 值不存在家庭池:统一账本是 {@code cf_balance}(<b>成员</b>维度),
+ * C 端实时查询走 {@code /api/cf/balance}、{@code /api/cf/logs}(见 {@code CfBalanceController})。
+ * 本控制器保留两类能力:
+ * <ul>
+ *   <li><b>只读历史</b>:家庭池 {@code family_platform_balance} 的余额/流水查询
+ *       (F-6 迁移后该表已停止写入,仅保留历史,供旧端与对账使用)</li>
+ *   <li><b>券兑换</b>:优惠券兑换/家庭券查询 —— 券<b>挂家庭</b>共享,
+ *       但 CF 扣的是<b>兑换人自己</b>的 {@code cf_balance}(N3)</li>
+ * </ul>
+ * 管理员调整同样落到家庭创建者成员的 {@code cf_balance},不写家庭池。
  */
  */
 @RestController
 @RestController
 @RequestMapping("/api/family-platform")
 @RequestMapping("/api/family-platform")
@@ -32,7 +42,7 @@ public class FamilyPlatformBalanceController {
     @Resource
     @Resource
     private UserMapper userMapper;
     private UserMapper userMapper;
 
 
-    /** 获取当前用户归属家庭的CF值余额 */
+    /** 获取当前用户归属家庭的CF值余额(只读历史:family_platform_balance,F-6 后已停止写入) */
     @PostMapping("/balance")
     @PostMapping("/balance")
     public Result<Map<String, Object>> getBalance(@RequestAttribute("userId") Long userId) {
     public Result<Map<String, Object>> getBalance(@RequestAttribute("userId") Long userId) {
         if (userId == null) return Result.error("未登录");
         if (userId == null) return Result.error("未登录");
@@ -43,7 +53,7 @@ public class FamilyPlatformBalanceController {
         return Result.success(familyPlatformPointsService.getBalance(user.getFamilyId()));
         return Result.success(familyPlatformPointsService.getBalance(user.getFamilyId()));
     }
     }
 
 
-    /** 获取当前用户归属家庭的CF值流水 */
+    /** 获取当前用户归属家庭的CF值流水(只读历史:family_platform_balance_log) */
     @PostMapping("/logs")
     @PostMapping("/logs")
     public Result<Page<FamilyPlatformBalanceLog>> getLogs(
     public Result<Page<FamilyPlatformBalanceLog>> getLogs(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
@@ -58,7 +68,7 @@ public class FamilyPlatformBalanceController {
         return Result.success(familyPlatformPointsService.getLogs(user.getFamilyId(), page, size));
         return Result.success(familyPlatformPointsService.getLogs(user.getFamilyId(), page, size));
     }
     }
 
 
-    /** 用家庭CF值兑换优惠券 */
+    /** 用个人CF值兑换优惠券(券挂家庭共享,扣兑换人自己的 cf_balance,N3) */
     @PostMapping("/exchange-coupon")
     @PostMapping("/exchange-coupon")
     public Result<Long> exchangeCoupon(
     public Result<Long> exchangeCoupon(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
@@ -78,7 +88,7 @@ public class FamilyPlatformBalanceController {
         }
         }
     }
     }
 
 
-    /** 管理员:查询指定家庭的CF值余额 */
+    /** 管理员:查询指定家庭的CF值余额(只读历史) */
     @PostMapping("/admin/balance")
     @PostMapping("/admin/balance")
     public Result<Map<String, Object>> adminBalance(@RequestBody Map<String, Object> params,
     public Result<Map<String, Object>> adminBalance(@RequestBody Map<String, Object> params,
                                                      @RequestAttribute("role") String role) {
                                                      @RequestAttribute("role") String role) {
@@ -88,7 +98,7 @@ public class FamilyPlatformBalanceController {
         return Result.success(familyPlatformPointsService.getBalance(familyId));
         return Result.success(familyPlatformPointsService.getBalance(familyId));
     }
     }
 
 
-    /** 管理员:查询指定家庭的CF值流水 */
+    /** 管理员:查询指定家庭的CF值流水(只读历史) */
     @PostMapping("/admin/logs")
     @PostMapping("/admin/logs")
     public Result<Page<FamilyPlatformBalanceLog>> adminLogs(@RequestBody Map<String, Object> params,
     public Result<Page<FamilyPlatformBalanceLog>> adminLogs(@RequestBody Map<String, Object> params,
                                                              @RequestAttribute("role") String role) {
                                                              @RequestAttribute("role") String role) {
@@ -100,7 +110,7 @@ public class FamilyPlatformBalanceController {
         return Result.success(familyPlatformPointsService.getLogs(familyId, page, size));
         return Result.success(familyPlatformPointsService.getLogs(familyId, page, size));
     }
     }
 
 
-    /** 管理员:手动调整家庭CF值 */
+    /** 管理员:手动调整CF值(落到家庭创建者成员的 cf_balance,不写家庭池) */
     @PostMapping("/admin/adjust")
     @PostMapping("/admin/adjust")
     public Result<String> adminAdjust(@RequestBody Map<String, Object> params,
     public Result<String> adminAdjust(@RequestBody Map<String, Object> params,
                                        @RequestAttribute("role") String role,
                                        @RequestAttribute("role") String role,
@@ -112,7 +122,7 @@ public class FamilyPlatformBalanceController {
         String remark = (String) params.get("remark");
         String remark = (String) params.get("remark");
         if (familyId == null || amount == 0) return Result.error("参数错误");
         if (familyId == null || amount == 0) return Result.error("参数错误");
         try {
         try {
-            // F-5:不再写家庭池,改投统一账本 cf_balance(家庭创建者成员的账本,ref_type=admin_adjust)
+            // F-5/F-8:不写家庭池(family_platform_balance 只读历史),改投统一账本 cf_balance(家庭创建者成员的账本,ref_type=admin_adjust)
             cfSpendDispatchService.adjustForFamily(familyId, amount,
             cfSpendDispatchService.adjustForFamily(familyId, amount,
                     remark != null ? remark : "管理员" + adminId + "手动调整", adminId);
                     remark != null ? remark : "管理员" + adminId + "手动调整", adminId);
             return Result.success("调整成功");
             return Result.success("调整成功");

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java

@@ -90,7 +90,7 @@ public class FamilyMember implements Serializable {
     /** 主题 */
     /** 主题 */
     private String theme;
     private String theme;
 
 
-    /** CF值(平台发放,用于提现/优惠券兑换) */
+    /** 系统积分(干预/平台奖励)——F-6 起仅保留只读历史;CF 值统一走 cf_balance */
     private Integer systemPoints;
     private Integer systemPoints;
 
 
     /** 连续打卡天数 */
     /** 连续打卡天数 */

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/CfPathwayEarnService.java

@@ -57,7 +57,7 @@ public class CfPathwayEarnService {
     public static final String RULE_F08 = "F08";
     public static final String RULE_F08 = "F08";
     /** F09 完成测评建档(第 1 份 50 CF / 后续每份 10 CF,重复报告不计 N10) */
     /** F09 完成测评建档(第 1 份 50 CF / 后续每份 10 CF,重复报告不计 N10) */
     public static final String RULE_F09 = "F09";
     public static final String RULE_F09 = "F09";
-    /** F10 家庭挑战完成(数量 = challenge.reward_points;按人各发 or 家庭合计待 Q1 确认) */
+    /** F10 家庭挑战完成(数量 = challenge.reward_points;Q1 已定:每个完成成员各发一次) */
     public static final String RULE_F10 = "F10";
     public static final String RULE_F10 = "F10";
     /** F11 邀请里程碑(数量 = 里程碑配置 reward_points) */
     /** F11 邀请里程碑(数量 = 里程碑配置 reward_points) */
     public static final String RULE_F11 = "F11";
     public static final String RULE_F11 = "F11";
@@ -219,9 +219,9 @@ public class CfPathwayEarnService {
     /**
     /**
      * F10 家庭挑战完成:数量 = challenge.reward_points。
      * F10 家庭挑战完成:数量 = challenge.reward_points。
      *
      *
-     * <p>口径待确认(Q1):当前保持"每个完成成员各发一次"的现状,ref_id = challenge.id,
-     * 幂等键含 member_id → 同一成员同一挑战只发一次。若后续改为"家庭合计一次发给发起人",
-     * 只需改本方法(ref_id 与发放对象),规则侧无需改动。</p>
+     * <p>口径已定(Q1,2026-10-25 拍板):<b>每个完成成员各发一次</b>——ref_id = challenge.id,
+     * 幂等键含 member_id → 同一成员同一挑战只发一次;数量随完成成员数放大属预期行为,
+     * 发放以 {@code memberId} 为记账维度(统一账本 cf_balance),规则侧无需改动。</p>
      *
      *
      * @param challengeId  家庭挑战ID(family_challenges.id)
      * @param challengeId  家庭挑战ID(family_challenges.id)
      * @param rewardPoints 挑战配置奖励数量
      * @param rewardPoints 挑战配置奖励数量

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java

@@ -471,7 +471,7 @@ public class FamilyChallengeService {
                         }
                         }
                         if (points > 0) {
                         if (points > 0) {
                             // 发放 CF 值(F10 统一入口:数量 = challenge.reward_points,规则 per_item 取 ctx.amount;
                             // 发放 CF 值(F10 统一入口:数量 = challenge.reward_points,规则 per_item 取 ctx.amount;
-                            // 口径待确认(Q1):当前保持"每个完成成员各发一次"现状,ref_id = challenge.id,
+                            // 口径已定(Q1,2026-10-25):每个完成成员各发一次,ref_id = challenge.id,
                             // 幂等键含 member_id → 同一成员同一挑战只发一次;硬错误随事务回滚,挑战保持 active 待重试)
                             // 幂等键含 member_id → 同一成员同一挑战只发一次;硬错误随事务回滚,挑战保持 active 待重试)
                             cfPathwayEarnService.earnFamilyChallengeCf(p.getChildId(), challenge.getId(),
                             cfPathwayEarnService.earnFamilyChallengeCf(p.getChildId(), challenge.getId(),
                                     points, challenge.getTitle());
                                     points, challenge.getTitle());

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -475,7 +475,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
      * 踢出家庭成员(管理员操作)
      * 踢出家庭成员(管理员操作)
      * 规则:
      * 规则:
      * - 仅管理员可踢出
      * - 仅管理员可踢出
-     * - 被踢成员 CF 值(system_points)随成员带走
+     * - 被踢成员 CF 值(统一账本 cf_balance,成员维度)随成员带走;family_members.system_points 为只读历史字段
      * - 收回该家庭发放给该成员的人口券(POPULATION)
      * - 收回该家庭发放给该成员的人口券(POPULATION)
      * - 微信登录用户:迁移资产到其初始家庭(无则创建新家庭)
      * - 微信登录用户:迁移资产到其初始家庭(无则创建新家庭)
      * - 非微信登录用户:进入回收箱(status=recycled),资产保留,可后续回收
      * - 非微信登录用户:进入回收箱(status=recycled),资产保留,可后续回收
@@ -583,7 +583,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
      * 主动退出家庭(微信登录用户)
      * 主动退出家庭(微信登录用户)
      * 规则:
      * 规则:
      * - 家庭创建者禁止退出,需先转让管理员
      * - 家庭创建者禁止退出,需先转让管理员
-     * - CF 值(system_points)清零丢弃(留在原家庭)
+     * - CF 值(统一账本 cf_balance,成员维度)随成员带离原家庭;family_members.system_points 仅清零只读历史残留值
      * - 迁移资产到初始家庭(无则创建新家庭)
      * - 迁移资产到初始家庭(无则创建新家庭)
      */
      */
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
@@ -616,7 +616,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
             throw new RuntimeException("未找到该用户的家庭成员记录");
             throw new RuntimeException("未找到该用户的家庭成员记录");
         }
         }
 
 
-        // CF 值清零丢弃(留在原家庭)
+        // family_members.system_points 为只读历史字段:清零残留值(CF 值在统一账本 cf_balance,成员维度,随成员走)
         member.setSystemPoints(0);
         member.setSystemPoints(0);
         member.setUpdatedAt(new Date());
         member.setUpdatedAt(new Date());
 
 
@@ -698,7 +698,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
      * 规则:
      * 规则:
      * - 非微信用户禁止自主切换
      * - 非微信用户禁止自主切换
      * - 家庭创建者禁止切换(需先转让管理员)
      * - 家庭创建者禁止切换(需先转让管理员)
-     * - CF 值(system_points)清零丢弃(留在原家庭)
+     * - CF 值(统一账本 cf_balance,成员维度)随成员带离原家庭;family_members.system_points 仅清零只读历史残留值
      * - 迁移资产到目标家庭
      * - 迁移资产到目标家庭
      */
      */
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
@@ -771,7 +771,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
                     userId, oldFamilyId, targetFamilyId, member.getId());
                     userId, oldFamilyId, targetFamilyId, member.getId());
         }
         }
 
 
-        // CF 值清零丢弃(留在原家庭)
+        // family_members.system_points 为只读历史字段:清零残留值(CF 值在统一账本 cf_balance,成员维度,随成员走)
         member.setSystemPoints(0);
         member.setSystemPoints(0);
         member.setUpdatedAt(new Date());
         member.setUpdatedAt(new Date());
 
 

+ 79 - 143
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -29,21 +29,31 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
 /**
 /**
- * 家庭平台积分池服务(CF值家庭维度)
- * 仿 PlatformPointsService,键为 familyId
+ * 家庭平台积分池服务 —— ⚠️ F-8(2026-10-25)写能力已下线,仅保留只读与「券兑换」能力
+ *
+ * <p>本服务原本承担「CF 值家庭池」的发放/消费/冻结/提现/调整全流程。
+ * 该语义已被 F-1~F-7 系列(2026-10-17 起)否定:<b>CF 值不存在家庭池</b>,
+ * 唯一事实源是统一账本 {@code cf_balance}(<b>成员</b>维度)。
+ * 迁移 F-6 已将 {@code family_platform_balance} 可用余额清零、冻结金额释放为可用,
+ * 该表仅作为只读历史账本保留,不再承接任何写入。
+ *
+ * <p><b>410 Gone</b>:{@link #getOrCreate}、{@link #earn}、{@link #spend}、
+ * {@link #freeze}、{@link #unfreeze}、{@link #confirmWithdraw}、{@link #adjust}
+ * 统一抛出 {@link IllegalStateException}(消息含 "410 Gone")。
+ *
+ * <p><b>保留能力</b>:
+ * <ul>
+ *   <li>只读历史:{@link #getBalance}、{@link #getWithdrawable}、{@link #getLogs}、
+ *       {@link #listPage}、{@link #getExchangeRecords}</li>
+ *   <li>券兑换:{@link #exchangeCouponByCf}、{@link #refundCouponCf}
+ *       —— 二者经 {@link CfSpendDispatchService} 写统一账本 {@code cf_balance},仍在服役</li>
+ * </ul>
  */
  */
 @Service
 @Service
 public class FamilyPlatformPointsService {
 public class FamilyPlatformPointsService {
 
 
     private static final Logger logger = LoggerFactory.getLogger(FamilyPlatformPointsService.class);
     private static final Logger logger = LoggerFactory.getLogger(FamilyPlatformPointsService.class);
 
 
-    private static final String TYPE_EARN = "earn";
-    private static final String TYPE_SPEND = "spend";
-    private static final String TYPE_FREEZE = "freeze";
-    private static final String TYPE_UNFREEZE = "unfreeze";
-    private static final String TYPE_WITHDRAW = "withdraw";
-    private static final String TYPE_ADJUST = "adjust";
-
     @Resource
     @Resource
     private FamilyPlatformBalanceMapper balanceMapper;
     private FamilyPlatformBalanceMapper balanceMapper;
 
 
@@ -70,174 +80,96 @@ public class FamilyPlatformPointsService {
     @Resource
     @Resource
     private FamilyPlatformExchangeRecordMapper exchangeRecordMapper;
     private FamilyPlatformExchangeRecordMapper exchangeRecordMapper;
 
 
-    /** 获取或创建家庭的积分池记录(悲观锁) */
+    /**
+     * 获取或创建家庭的积分池记录。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:迁移前本方法会向
+     * {@code family_platform_balance} 懒插入空行。迁移 F-6 之后该表仅作只读历史账本,
+     * 禁止再新增行;实时 CF 余额读写一律走 {@code cf_balance}(成员维度)。
+     */
+    @Deprecated
     @Transactional
     @Transactional
     public FamilyPlatformBalance getOrCreate(Long familyId) {
     public FamilyPlatformBalance getOrCreate(Long familyId) {
-        FamilyPlatformBalance balance = balanceMapper.selectOne(
-                new LambdaQueryWrapper<FamilyPlatformBalance>()
-                        .eq(FamilyPlatformBalance::getFamilyId, familyId));
-        if (balance == null) {
-            balance = new FamilyPlatformBalance();
-            balance.setFamilyId(familyId);
-            balance.setTotalEarned(0);
-            balance.setAvailable(0);
-            balance.setFrozen(0);
-            balance.setWithdrawn(0);
-            balance.setExchanged(0);
-            balance.setCreatedAt(new Date());
-            balance.setUpdatedAt(new Date());
-            balanceMapper.insert(balance);
-        }
-        return balance;
+        logger.warn("410 Gone:FamilyPlatformPointsService.getOrCreate 已下线 familyId={}", familyId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.getOrCreate 已下线(family_platform_balance 仅作只读历史账本),请使用 CfBalanceService");
     }
     }
 
 
     private int safeInt(Integer v) {
     private int safeInt(Integer v) {
         return v == null ? 0 : v;
         return v == null ? 0 : v;
     }
     }
 
 
-    private void writeLog(Long familyId, String type, int amount,
-                          int balanceAfter, String refType, Long refId, String remark) {
-        FamilyPlatformBalanceLog log = new FamilyPlatformBalanceLog();
-        log.setFamilyId(familyId);
-        log.setType(type);
-        log.setAmount(amount);
-        log.setBalanceAfter(balanceAfter);
-        log.setRefType(refType);
-        log.setRefId(refId);
-        log.setRemark(remark);
-        log.setCreatedAt(new Date());
-        logMapper.insert(log);
-    }
-
-    /** 家庭获得 CF 值(幂等:按 ref_type+ref_id 去重) */
+    /**
+     * 家庭获得 CF 值。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:CF 值不存在家庭池。获取统一走
+     * {@link CfBalanceService#earnByRule}(24 条获取规则种子见 cf_earn_rule,迁移 408)。
+     */
+    @Deprecated
     @Transactional
     @Transactional
     public void earn(Long familyId, int amount, String refType, Long refId, String remark) {
     public void earn(Long familyId, int amount, String refType, Long refId, String remark) {
-        if (amount <= 0) return;
-        // 幂等检查
-        Long existing = logMapper.selectCount(
-                new LambdaQueryWrapper<FamilyPlatformBalanceLog>()
-                        .eq(FamilyPlatformBalanceLog::getFamilyId, familyId)
-                        .eq(FamilyPlatformBalanceLog::getRefType, refType)
-                        .eq(FamilyPlatformBalanceLog::getRefId, refId));
-        if (existing != null && existing > 0) return;
-
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        b.setTotalEarned(safeInt(b.getTotalEarned()) + amount);
-        b.setAvailable(safeInt(b.getAvailable()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_EARN, amount, safeInt(b.getAvailable()),
-                refType, refId, remark);
+        logger.warn("410 Gone:FamilyPlatformPointsService.earn 已下线 familyId={}, amount={}, refType={}, refId={}",
+                familyId, amount, refType, refId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.earn 已下线,请使用 CfBalanceService.earnByRule");
     }
     }
 
 
     /**
     /**
-     * @deprecated F-5 起消费侧(券兑换 / 提现冻结 / 商品抵扣)已全部改投统一账本 {@link CfSpendDispatchService},
-     * 本方法不再作为消费入口使用;家庭池仅保留发放侧(F-8 清理时标注下线)。
+     * @deprecated <b>410 Gone(F-8 / 2026-10-25)</b>:CF 值不存在家庭池,消耗统一走
+     * {@link CfSpendDispatchService} → {@link CfBalanceService#spend},refType 见消费侧清单。
      */
      */
     @Deprecated
     @Deprecated
     @Transactional
     @Transactional
     public void spend(Long familyId, int amount, String refType, Long refId, String remark) {
     public void spend(Long familyId, int amount, String refType, Long refId, String remark) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("积分抵扣数量必须为正数");
-        }
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        int available = safeInt(b.getAvailable());
-        if (available < amount) {
-            throw new RuntimeException("家庭积分不足,当前可用 " + available + " 积分");
-        }
-        b.setAvailable(available - amount);
-        b.setExchanged(safeInt(b.getExchanged()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_SPEND, amount, safeInt(b.getAvailable()),
-                refType, refId, remark);
+        logger.warn("410 Gone:FamilyPlatformPointsService.spend 已下线 familyId={}, amount={}, refType={}, refId={}",
+                familyId, amount, refType, refId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.spend 已下线,请使用 CfSpendDispatchService / CfBalanceService.spend");
     }
     }
 
 
     /**
     /**
-     * @deprecated F-5 起提现冻结改走统一账本 {@link CfSpendDispatchService#freezeForMember},
-     * 家庭池不再承担消费冻结;F-8 清理时下线。
+     * @deprecated <b>410 Gone(F-8 / 2026-10-25)</b>:提现冻结走
+     * {@link CfSpendDispatchService#freezeForMember} → {@link CfBalanceService#freeze}。
      */
      */
     @Deprecated
     @Deprecated
     @Transactional
     @Transactional
     public void freeze(Long familyId, int amount, Long refId) {
     public void freeze(Long familyId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("提现金额必须为正数");
-        }
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        int available = safeInt(b.getAvailable());
-        if (available < amount) {
-            throw new RuntimeException("家庭积分不足");
-        }
-        b.setAvailable(available - amount);
-        b.setFrozen(safeInt(b.getFrozen()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_FREEZE, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现冻结");
+        logger.warn("410 Gone:FamilyPlatformPointsService.freeze 已下线 familyId={}, amount={}, refId={}",
+                familyId, amount, refId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.freeze 已下线,请使用 CfBalanceService.freeze");
     }
     }
 
 
     /**
     /**
-     * @deprecated F-5 起提现驳回解冻改走统一账本 {@link CfSpendDispatchService#unfreezeForMember},
-     * 家庭池不再承担消费解冻;F-8 清理时下线。
+     * @deprecated <b>410 Gone(F-8 / 2026-10-25)</b>:提现驳回解冻走
+     * {@link CfSpendDispatchService#unfreezeForMember} → {@link CfBalanceService#unfreeze}。
      */
      */
     @Deprecated
     @Deprecated
     @Transactional
     @Transactional
     public void unfreeze(Long familyId, int amount, Long refId) {
     public void unfreeze(Long familyId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("提现金额必须为正数");
-        }
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        int frozen = safeInt(b.getFrozen());
-        if (frozen < amount) {
-            throw new RuntimeException("冻结积分不足");
-        }
-        b.setFrozen(frozen - amount);
-        b.setAvailable(safeInt(b.getAvailable()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_UNFREEZE, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现审核拒绝解冻");
+        logger.warn("410 Gone:FamilyPlatformPointsService.unfreeze 已下线 familyId={}, amount={}, refId={}",
+                familyId, amount, refId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.unfreeze 已下线,请使用 CfBalanceService.unfreeze");
     }
     }
 
 
     /**
     /**
-     * @deprecated F-5 起提现扣款改走统一账本 {@link CfSpendDispatchService#withdrawForMember},
-     * 家庭池不再承担消费提现;F-8 清理时下线。
+     * @deprecated <b>410 Gone(F-8 / 2026-10-25)</b>:提现扣款走
+     * {@link CfSpendDispatchService#withdrawForMember} → {@link CfBalanceService#withdraw}。
      */
      */
     @Deprecated
     @Deprecated
     @Transactional
     @Transactional
     public void confirmWithdraw(Long familyId, int amount, Long refId) {
     public void confirmWithdraw(Long familyId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("提现金额必须为正数");
-        }
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        int frozen = safeInt(b.getFrozen());
-        if (frozen < amount) {
-            throw new RuntimeException("冻结积分不足");
-        }
-        b.setFrozen(frozen - amount);
-        b.setWithdrawn(safeInt(b.getWithdrawn()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_WITHDRAW, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现成功");
+        logger.warn("410 Gone:FamilyPlatformPointsService.confirmWithdraw 已下线 familyId={}, amount={}, refId={}",
+                familyId, amount, refId);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.confirmWithdraw 已下线,请使用 CfBalanceService.withdraw");
     }
     }
 
 
     /**
     /**
-     * @deprecated F-5 起管理端调整改走统一账本 {@link CfSpendDispatchService#adjustForFamily}
-     * (落到家庭创建者成员账本),家庭池不再承担调整;F-8 清理时下线。
+     * @deprecated <b>410 Gone(F-8 / 2026-10-25)</b>:管理端调整走
+     * {@link CfSpendDispatchService#adjustForFamily} → 统一账本 {@code cf_balance}。
      */
      */
     @Deprecated
     @Deprecated
     @Transactional
     @Transactional
     public void adjust(Long familyId, int amount, String remark) {
     public void adjust(Long familyId, int amount, String remark) {
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        b.setAvailable(Math.max(0, safeInt(b.getAvailable()) + amount));
-        if (amount > 0) {
-            b.setTotalEarned(safeInt(b.getTotalEarned()) + amount);
-        }
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(familyId, TYPE_ADJUST, Math.abs(amount), safeInt(b.getAvailable()),
-                "adjust", null, remark);
+        logger.warn("410 Gone:FamilyPlatformPointsService.adjust 已下线 familyId={}, amount={}, remark={}",
+                familyId, amount, remark);
+        throw new IllegalStateException("410 Gone:FamilyPlatformPointsService.adjust 已下线,请使用 CfSpendDispatchService.adjustForFamily");
     }
     }
 
 
     /**
     /**
@@ -384,23 +316,27 @@ public class FamilyPlatformPointsService {
                         .orderByDesc(FamilyPlatformExchangeRecord::getCreatedAt));
                         .orderByDesc(FamilyPlatformExchangeRecord::getCreatedAt));
     }
     }
 
 
-    /** 查询家庭 CF 值余额 */
+    /** 查询家庭 CF 值余额(只读历史;实时值用 CfBalanceService,F-6 后此表多清零) */
     public Map<String, Object> getBalance(Long familyId) {
     public Map<String, Object> getBalance(Long familyId) {
-        FamilyPlatformBalance b = getOrCreate(familyId);
+        FamilyPlatformBalance b = balanceMapper.selectOne(
+                new LambdaQueryWrapper<FamilyPlatformBalance>()
+                        .eq(FamilyPlatformBalance::getFamilyId, familyId));
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
         result.put("familyId", familyId);
         result.put("familyId", familyId);
-        result.put("available", safeInt(b.getAvailable()));
-        result.put("frozen", safeInt(b.getFrozen()));
-        result.put("withdrawn", safeInt(b.getWithdrawn()));
-        result.put("exchanged", safeInt(b.getExchanged()));
-        result.put("totalEarned", safeInt(b.getTotalEarned()));
+        result.put("available", b != null ? safeInt(b.getAvailable()) : 0);
+        result.put("frozen", b != null ? safeInt(b.getFrozen()) : 0);
+        result.put("withdrawn", b != null ? safeInt(b.getWithdrawn()) : 0);
+        result.put("exchanged", b != null ? safeInt(b.getExchanged()) : 0);
+        result.put("totalEarned", b != null ? safeInt(b.getTotalEarned()) : 0);
         return result;
         return result;
     }
     }
 
 
-    /** 查询可提现余额 */
+    /** 查询可提现余额(只读历史,F-6 迁移后恒为 0;实时值用 CfBalanceService) */
     public Integer getWithdrawable(Long familyId) {
     public Integer getWithdrawable(Long familyId) {
-        FamilyPlatformBalance b = getOrCreate(familyId);
-        return safeInt(b.getAvailable());
+        FamilyPlatformBalance b = balanceMapper.selectOne(
+                new LambdaQueryWrapper<FamilyPlatformBalance>()
+                        .eq(FamilyPlatformBalance::getFamilyId, familyId));
+        return b != null ? safeInt(b.getAvailable()) : 0;
     }
     }
 
 
     /** 积分流水分页 */
     /** 积分流水分页 */
@@ -412,7 +348,7 @@ public class FamilyPlatformPointsService {
                         .orderByDesc(FamilyPlatformBalanceLog::getCreatedAt));
                         .orderByDesc(FamilyPlatformBalanceLog::getCreatedAt));
     }
     }
 
 
-    /** 查询管理员端家庭池列表(分页) */
+    /** 查询管理员端家庭池历史余额列表(分页,只读;family_platform_balance 自 F-6 起已停止写入) */
     public Page<FamilyPlatformBalance> listPage(Integer page, Integer size, Long familyId) {
     public Page<FamilyPlatformBalance> listPage(Integer page, Integer size, Long familyId) {
         Page<FamilyPlatformBalance> pageParam = new Page<>(page, size);
         Page<FamilyPlatformBalance> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<FamilyPlatformBalance> wrapper = new LambdaQueryWrapper<>();
         LambdaQueryWrapper<FamilyPlatformBalance> wrapper = new LambdaQueryWrapper<>();

+ 90 - 187
cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java

@@ -1,219 +1,146 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.PlatformBalanceLog;
 import com.etotem.cfc.entity.PlatformBalanceLog;
-import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.UserPlatformBalance;
 import com.etotem.cfc.entity.UserPlatformBalance;
 import com.etotem.cfc.mapper.PlatformBalanceLogMapper;
 import com.etotem.cfc.mapper.PlatformBalanceLogMapper;
-import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserPlatformBalanceMapper;
 import com.etotem.cfc.mapper.UserPlatformBalanceMapper;
-import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
-import com.etotem.cfc.service.NotificationDispatchService;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
+/**
+ * 个人平台积分池服务 —— ⚠️ F-8(2026-10-25)写能力已下线,仅保留只读能力
+ *
+ * <p>本服务原本负责个人「CF 值」(平台积分) 的赚取/消费/冻结/提现全流程。
+ * 该「个人 CF 池」语义已被 F-1~F-7 系列(2026-10-17 起)否定:CF 值不存在「个人池」,
+ * 唯一事实源是统一账本 {@code cf_balance}(成员维度)。迁移 F-6 已将
+ * {@code user_platform_balance} 的可用余额清零、冻结金额释放为可用后,
+ * 该表仅作为只读历史账本保留,不再承接任何写入。
+ *
+ * <p><b>410 Gone</b>:以下写方法统一抛出 {@link IllegalStateException}
+ * (消息含 "410 Gone"),调用方必须迁移到
+ * {@link CfBalanceService}(获取:{@code earnByRule};消耗:{@code spend})。
+ * 参照同类下线的 {@link CommissionDistService}。
+ *
+ * <p><b>保留的只读方法</b>(不写 {@code user_platform_balance},仅用于历史查询/管理报表):
+ * {@link #getBalance}、{@link #getWithdrawable}、{@link #getLogs}。
+ */
 @Slf4j
 @Slf4j
 @Service
 @Service
 public class PlatformPointsService {
 public class PlatformPointsService {
 
 
-    private static final String TYPE_EARN = "earn";
-    private static final String TYPE_SPEND = "spend";
-    private static final String TYPE_FREEZE = "freeze";
-    private static final String TYPE_UNFREEZE = "unfreeze";
-    private static final String TYPE_WITHDRAW = "withdraw";
-
     @Resource
     @Resource
     private UserPlatformBalanceMapper balanceMapper;
     private UserPlatformBalanceMapper balanceMapper;
 
 
     @Resource
     @Resource
     private PlatformBalanceLogMapper logMapper;
     private PlatformBalanceLogMapper logMapper;
 
 
-    @Resource
-    private UserMapper userMapper;
-
-    @Resource
-    private NotificationDispatchService notificationDispatchService;
-
-    /** 获取或创建用户的 CF 值余额记录 */
+    /**
+     * 获取用户 CF 值余额记录(不创建)。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:迁移前本方法会向 {@code user_platform_balance}
+     * 懒插入空行。迁移 F-6 之后该表仅作只读历史账本保留,禁止再新增行;
+     * 新成员的 CF 余额由 {@link CfBalanceService} 写入 {@code cf_balance}。
+     */
+    @Deprecated
     private UserPlatformBalance getOrCreate(Long userId) {
     private UserPlatformBalance getOrCreate(Long userId) {
-        UserPlatformBalance balance = balanceMapper.selectOne(
-                new LambdaQueryWrapper<UserPlatformBalance>()
-                        .eq(UserPlatformBalance::getUserId, userId));
-        if (balance == null) {
-            balance = new UserPlatformBalance();
-            balance.setUserId(userId);
-            balance.setTotalEarned(0);
-            balance.setAvailable(0);
-            balance.setFrozen(0);
-            balance.setWithdrawn(0);
-            balance.setExchanged(0);
-            balance.setCreatedAt(new Date());
-            balance.setUpdatedAt(new Date());
-            balanceMapper.insert(balance);
-        }
-        // 回填家庭 ID(用户进家庭后首次操作时补齐,供流水按家庭维度统计)
-        if (balance.getFamilyId() == null) {
-            User u = userMapper.selectById(userId);
-            if (u != null && u.getFamilyId() != null) {
-                balance.setFamilyId(u.getFamilyId());
-                balance.setUpdatedAt(new Date());
-                balanceMapper.updateById(balance);
-            }
-        }
-        return balance;
+        log.warn("410 Gone:PlatformPointsService.getOrCreate 已下线,user_platform_balance 不再建行,请使用 CfBalanceService");
+        throw new IllegalStateException("410 Gone:PlatformPointsService.getOrCreate 已下线(user_platform_balance 仅作只读历史账本),请使用 CfBalanceService");
     }
     }
 
 
     private int safeInt(Integer v) {
     private int safeInt(Integer v) {
         return v == null ? 0 : v;
         return v == null ? 0 : v;
     }
     }
 
 
-    private void writeLog(Long userId, Long familyId, String type, int amount,
-                          int balanceAfter, String refType, Long refId, String remark) {
-        PlatformBalanceLog log = new PlatformBalanceLog();
-        log.setUserId(userId);
-        log.setFamilyId(familyId);
-        log.setType(type);
-        log.setAmount(amount);
-        log.setBalanceAfter(balanceAfter);
-        log.setRefType(refType);
-        log.setRefId(refId);
-        log.setRemark(remark);
-        log.setCreatedAt(new Date());
-        logMapper.insert(log);
-    }
-
-    /** 用户获得 CF 值 */
-    @Transactional
+    /**
+     * 用户获得 CF 值。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:CF 值不存在「个人池」。获取统一走
+     * {@link CfBalanceService#earnByRule}(24 条获取规则种子见 cf_earn_rule,迁移 408)。
+     */
+    @Deprecated
     public void earn(Long userId, int amount, String refType, Long refId, String remark) {
     public void earn(Long userId, int amount, String refType, Long refId, String remark) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-        // 幂等:按 (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;
-            }
-        }
-        UserPlatformBalance b = getOrCreate(userId);
-        b.setTotalEarned(safeInt(b.getTotalEarned()) + amount);
-        b.setAvailable(safeInt(b.getAvailable()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        try {
-            writeLog(userId, b.getFamilyId(), TYPE_EARN, amount, safeInt(b.getAvailable()),
-                    refType, refId, remark);
-            sendCfNotification(userId, TYPE_EARN, amount, remark);
-        } catch (DuplicateKeyException e) {
-            throw new RuntimeException("重复发放拦截: userId=" + userId + ", refType=" + refType + ", refId=" + refId, e);
-        }
+        log.warn("410 Gone:PlatformPointsService.earn 已下线 userId={}, amount={}, refType={}, refId={}",
+                userId, amount, refType, refId);
+        throw new IllegalStateException("410 Gone:PlatformPointsService.earn 已下线,请使用 CfBalanceService.earnByRule");
     }
     }
 
 
-    /** 扣减可用 CF 值(转让给家庭成员等),余额不足抛异常。原子条件扣减防并发超扣,不计入 exchanged(转让非兑换)。 */
-    @Transactional
+    /**
+     * 扣减可用 CF 值(转让给家庭成员等)。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:消耗统一走 {@link CfSpendDispatchService}
+     * → {@link CfBalanceService#spend},refType 见消费侧清单。
+     */
+    @Deprecated
     public void spend(Long userId, int amount, String refType, Long refId, String remark) {
     public void spend(Long userId, int amount, String refType, Long refId, String remark) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-        int updated = balanceMapper.update(null, new LambdaUpdateWrapper<UserPlatformBalance>()
-                .eq(UserPlatformBalance::getUserId, userId)
-                .ge(UserPlatformBalance::getAvailable, amount)
-                .setSql("available = available - " + amount)
-                .setSql("updated_at = NOW()"));
-        if (updated == 0) {
-            throw new RuntimeException("CF值余额不足");
-        }
-        UserPlatformBalance b = getOrCreate(userId);
-        writeLog(userId, b.getFamilyId(), TYPE_SPEND, amount, safeInt(b.getAvailable()),
-                refType, refId, remark);
-        sendCfNotification(userId, TYPE_SPEND, amount, remark);
+        log.warn("410 Gone:PlatformPointsService.spend 已下线 userId={}, amount={}, refType={}, refId={}",
+                userId, amount, refType, refId);
+        throw new IllegalStateException("410 Gone:PlatformPointsService.spend 已下线,请使用 CfSpendDispatchService / CfBalanceService.spend");
     }
     }
 
 
-    /** 提现申请:可用 → 冻结 */
-    @Transactional
+    /**
+     * 提现申请:可用 → 冻结。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:走 {@link CfBalanceService#freeze}(refType=withdrawal)。
+     */
+    @Deprecated
     public void freeze(Long userId, int amount, Long refId) {
     public void freeze(Long userId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-        UserPlatformBalance b = getOrCreate(userId);
-        int available = safeInt(b.getAvailable());
-        if (available < amount) {
-            throw new RuntimeException("CF值余额不足");
-        }
-        b.setAvailable(available - amount);
-        b.setFrozen(safeInt(b.getFrozen()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(userId, b.getFamilyId(), TYPE_FREEZE, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现冻结");
+        log.warn("410 Gone:PlatformPointsService.freeze 已下线 userId={}, amount={}, refId={}", userId, amount, refId);
+        throw new IllegalStateException("410 Gone:PlatformPointsService.freeze 已下线,请使用 CfBalanceService.freeze");
     }
     }
 
 
-    /** 提现审核拒绝:冻结 → 可用 */
-    @Transactional
+    /**
+     * 提现审核拒绝:冻结 → 可用。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:走 {@link CfBalanceService#unfreeze}(refType=withdrawal)。
+     */
+    @Deprecated
     public void unfreeze(Long userId, int amount, Long refId) {
     public void unfreeze(Long userId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-        UserPlatformBalance b = getOrCreate(userId);
-        int frozen = safeInt(b.getFrozen());
-        if (frozen < amount) {
-            throw new RuntimeException("冻结CF值不足");
-        }
-        b.setFrozen(frozen - amount);
-        b.setAvailable(safeInt(b.getAvailable()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(userId, b.getFamilyId(), TYPE_UNFREEZE, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现审核拒绝解冻");
+        log.warn("410 Gone:PlatformPointsService.unfreeze 已下线 userId={}, amount={}, refId={}", userId, amount, refId);
+        throw new IllegalStateException("410 Gone:PlatformPointsService.unfreeze 已下线,请使用 CfBalanceService.unfreeze");
     }
     }
 
 
-    /** 提现审核通过:冻结 → 已提现 */
-    @Transactional
+    /**
+     * 提现审核通过:冻结 → 已提现。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:走 {@link CfBalanceService#withdraw}(refType=withdrawal)。
+     */
+    @Deprecated
     public void confirmWithdraw(Long userId, int amount, Long refId) {
     public void confirmWithdraw(Long userId, int amount, Long refId) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-        UserPlatformBalance b = getOrCreate(userId);
-        int frozen = safeInt(b.getFrozen());
-        if (frozen < amount) {
-            throw new RuntimeException("冻结CF值不足");
-        }
-        b.setFrozen(frozen - amount);
-        b.setWithdrawn(safeInt(b.getWithdrawn()) + amount);
-        b.setUpdatedAt(new Date());
-        balanceMapper.updateById(b);
-        writeLog(userId, b.getFamilyId(), TYPE_WITHDRAW, amount, safeInt(b.getAvailable()),
-                "withdrawal", refId, "提现成功");
+        log.warn("410 Gone:PlatformPointsService.confirmWithdraw 已下线 userId={}, amount={}, refId={}", userId, amount, refId);
+        throw new IllegalStateException("410 Gone:PlatformPointsService.confirmWithdraw 已下线,请使用 CfBalanceService.withdraw");
     }
     }
 
 
-    /** 查询用户 CF 值余额 */
+    /**
+     * 查询用户 CF 值余额(只读历史)。
+     *
+     * <p>注:F-6 迁移已把 {@code user_platform_balance} 可用余额清零,
+     * 这里查到的多为 0 值历史快照;实时 CF 余额请用 {@link CfBalanceService#getBalance}。
+     */
     public Map<String, Object> getBalance(Long userId) {
     public Map<String, Object> getBalance(Long userId) {
-        UserPlatformBalance b = getOrCreate(userId);
+        UserPlatformBalance b = balanceMapper.selectOne(
+                new LambdaQueryWrapper<UserPlatformBalance>()
+                        .eq(UserPlatformBalance::getUserId, userId));
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
-        result.put("available", safeInt(b.getAvailable()));
-        result.put("frozen", safeInt(b.getFrozen()));
-        result.put("withdrawn", safeInt(b.getWithdrawn()));
-        result.put("exchanged", safeInt(b.getExchanged()));
-        result.put("totalEarned", safeInt(b.getTotalEarned()));
+        result.put("available", b != null ? safeInt(b.getAvailable()) : 0);
+        result.put("frozen", b != null ? safeInt(b.getFrozen()) : 0);
+        result.put("withdrawn", b != null ? safeInt(b.getWithdrawn()) : 0);
+        result.put("exchanged", b != null ? safeInt(b.getExchanged()) : 0);
+        result.put("totalEarned", b != null ? safeInt(b.getTotalEarned()) : 0);
         return result;
         return result;
     }
     }
 
 
-    /** 查询可提现余额 */
+    /** 查询可提现余额(只读历史,F-6 迁移后恒为 0;实时值用 CfBalanceService) */
     public Integer getWithdrawable(Long userId) {
     public Integer getWithdrawable(Long userId) {
-        UserPlatformBalance b = getOrCreate(userId);
-        return safeInt(b.getAvailable());
+        UserPlatformBalance b = balanceMapper.selectOne(
+                new LambdaQueryWrapper<UserPlatformBalance>()
+                        .eq(UserPlatformBalance::getUserId, userId));
+        return b != null ? safeInt(b.getAvailable()) : 0;
     }
     }
 
 
     /** CF 值流水分页 */
     /** CF 值流水分页 */
@@ -224,28 +151,4 @@ public class PlatformPointsService {
                         .eq(PlatformBalanceLog::getUserId, userId)
                         .eq(PlatformBalanceLog::getUserId, userId)
                         .orderByDesc(PlatformBalanceLog::getCreatedAt));
                         .orderByDesc(PlatformBalanceLog::getCreatedAt));
     }
     }
-
-    /** 发送 CF 值变更站内通知 */
-    private void sendCfNotification(Long userId, String type, int amount, String remark) {
-        try {
-            String title, content;
-            if (TYPE_EARN.equals(type)) {
-                title = "获得CF值";
-                content = String.format("%s获得%d CF值", remark != null ? remark + " " : "", amount);
-            } else if (TYPE_SPEND.equals(type)) {
-                title = "CF值支出";
-                content = String.format("%s支出%d CF值", remark != null ? remark : "消费", amount);
-            } else {
-                title = "CF值变动";
-                content = String.format("CF值变动%d", amount);
-            }
-            Map<String, Object> vars = new HashMap<>();
-            vars.put("amount", amount);
-            vars.put("type", type);
-            vars.put("remark", remark);
-            notificationDispatchService.sendByTemplate("cf_change", userId, vars, "platformBalanceLog", null, title, content);
-        } catch (Exception e) {
-            log.warn("cf notification failed userId={}, error={}", userId, e.getMessage());
-        }
-    }
 }
 }

+ 40 - 59
cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java

@@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.dto.CfBalanceResult;
 import com.etotem.cfc.dto.CfEarnResult;
 import com.etotem.cfc.dto.CfEarnResult;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
@@ -123,78 +122,62 @@ public class PointsService implements PointsServiceInterface {
     }
     }
 
 
     /**
     /**
-     * 发放 CF 值(F-5:改投统一账本 cf_balance,不再写 family_members.system_points)。
+     * 发放 CF 值。
      *
      *
-     * <p>本方法为旧接口兼容入口,无稳定业务单据 ID,故不参与幂等键(refId 传空)。</p>
-     *
-     * @return 发放后可用 CF 值余额
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:旧接口兼容入口下线。
+     * F-3 起 24 条获取途径已全部改投统一账本 {@code cf_balance}(cf_earn_rule 种子,迁移 408),
+     * 统一发放入口为 {@link CfBalanceService#earnByRule}(按规则校验等级日限/冷却/数量,幂等)。
+     * 本方法无稳定业务单据 ID(refId 传空),既不走规则也不参与幂等,已收口下线。
      */
      */
+    @Deprecated
     @Transactional
     @Transactional
     public int awardCfPoints(Long memberId, int amount, String reason) {
     public int awardCfPoints(Long memberId, int amount, String reason) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("CF值数量必须为正数");
-        }
-
-        FamilyMember child = familyMemberMapper.selectById(memberId);
-        if (child == null) {
-            throw new RuntimeException("孩子不存在");
-        }
-
-        CfBalanceResult result = cfSpendDispatchService.earnForMember(memberId, amount, "points_cf_award", null, reason);
-        int newCfPoints = result != null && result.getAvailable() != null ? result.getAvailable() : amount;
-        try { sendPointsNotification(memberId, "cf_earn", amount, reason); } catch (Exception e) { log.warn("cf notification failed", e); }
-
-        return newCfPoints;
+        log.warn("410 Gone:PointsService.awardCfPoints 已下线 memberId={}, amount={}, reason={}",
+                memberId, amount, reason);
+        throw new IllegalStateException("410 Gone:PointsService.awardCfPoints 已下线,请使用 CfBalanceService.earnByRule");
     }
     }
 
 
     /**
     /**
-     * 打卡返积分(平台发放,计入 CF值),仅 category 不同)
+     * 打卡返积分。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:F06 菌群打卡返还 / F14-F16 打卡类已改投
+     * {@link CfBalanceService#earnByRule}(日限按会员等级 N11),旧打卡入口下线。
      */
      */
+    @Deprecated
     @Transactional
     @Transactional
     public int awardCheckinPoints(Long memberId, int amount, String reason) {
     public int awardCheckinPoints(Long memberId, int amount, String reason) {
-        return awardCfPoints(memberId, amount, reason);
+        log.warn("410 Gone:PointsService.awardCheckinPoints 已下线 memberId={}, amount={}, reason={}",
+                memberId, amount, reason);
+        throw new IllegalStateException("410 Gone:PointsService.awardCheckinPoints 已下线,请使用 CfBalanceService.earnByRule");
     }
     }
 
 
     /**
     /**
-     * 心愿兑换扣除CF值(家长审批通过后调用)
-     * @param memberId 孩子ID
-     * @param amount 扣除数量
-     * @return true=扣除成功, false=余额不足
+     * 心愿兑换扣除CF值(家长审批通过后调用)。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:F-5 消费侧已改走
+     * {@link CfSpendDispatchService}(refType=wish_exchange)写统一账本 {@code cf_balance},旧入口下线。
      */
      */
+    @Deprecated
     @Transactional
     @Transactional
     public boolean deductForWishCf(Long memberId, Integer amount) {
     public boolean deductForWishCf(Long memberId, Integer amount) {
-        if (amount == null || amount <= 0) {
-            return false;
-        }
-        return deductCfPoints(memberId, amount, "心愿兑换扣除") >= 0;
+        log.warn("410 Gone:PointsService.deductForWishCf 已下线 memberId={}, amount={}", memberId, amount);
+        throw new IllegalStateException("410 Gone:PointsService.deductForWishCf 已下线,请使用 CfSpendDispatchService(refType=wish_exchange)");
     }
     }
 
 
     /**
     /**
-     * 扣除 CF 值(F-5:改投统一账本 cf_balance,不再写 family_members.system_points)。
-     *
-     * <p>旧接口兼容入口,无稳定业务单据 ID,不做幂等(refId 传空);余额不足返回 -1。</p>
+     * 扣除 CF 值。
      *
      *
-     * @return 扣除后可用 CF 值余额,余额不足返回 -1
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:旧接口兼容入口下线。
+     * 消耗统一走 {@link CfSpendDispatchService} → {@link CfBalanceService#spend},
+     * refType 见消费侧清单(membership_renewal_cf / product_order_points / coupon_exchange /
+     * coupon_refund / withdrawal / wish_exchange 等)。
      */
      */
+    @Deprecated
     @Transactional
     @Transactional
     public int deductCfPoints(Long memberId, int amount, String reason) {
     public int deductCfPoints(Long memberId, int amount, String reason) {
-        if (amount <= 0) {
-            throw new IllegalArgumentException("扣除数量必须为正数");
-        }
-
-        if (memberId == null) {
-            throw new RuntimeException("孩子不存在");
-        }
-        int currentCf = cfSpendDispatchService.getAvailableForMember(memberId);
-        if (currentCf < amount) {
-            return -1;
-        }
-
-        CfBalanceResult result = cfSpendDispatchService.spendForMember(memberId, amount, "points_cf_deduct", null, reason);
-        int newCfPoints = result != null && result.getAvailable() != null ? result.getAvailable() : (currentCf - amount);
-        try { sendPointsNotification(memberId, "cf_spend", -amount, reason); } catch (Exception e) { log.warn("cf deduction notification failed", e); }
-
-        return newCfPoints;
+        log.warn("410 Gone:PointsService.deductCfPoints 已下线 memberId={}, amount={}, reason={}",
+                memberId, amount, reason);
+        throw new IllegalStateException("410 Gone:PointsService.deductCfPoints 已下线,请使用 CfSpendDispatchService / CfBalanceService.spend");
     }
     }
 
 
     /** 查询 CF 值余额(F-5:改读统一账本 cf_balance,不再读 family_members.system_points) */
     /** 查询 CF 值余额(F-5:改读统一账本 cf_balance,不再读 family_members.system_points) */
@@ -215,18 +198,16 @@ public class PointsService implements PointsServiceInterface {
     }
     }
 
 
     /**
     /**
-     * 检查并发放CF值奖励
-     * @param memberId 孩子ID
-     * @param amount 奖励CF值
-     * @return 发放后CF值余额,余额不足返回-1
+     * 检查并发放CF值奖励。
+     *
+     * <p><b>410 Gone(F-8 / 2026-10-25)</b>:旧发放入口下线,统一走
+     * {@link CfBalanceService#earnByRule}(refType=wish_exchange_reward / 对应规则)。
      */
      */
+    @Deprecated
     @Transactional
     @Transactional
     public int checkAndGrantCfReward(Long memberId, int amount) {
     public int checkAndGrantCfReward(Long memberId, int amount) {
-        int currentBalance = getCfPointsBalance(memberId);
-        if (currentBalance < amount) {
-            return -1;
-        }
-        return awardCfPoints(memberId, amount, "心愿兑换奖励");
+        log.warn("410 Gone:PointsService.checkAndGrantCfReward 已下线 memberId={}, amount={}", memberId, amount);
+        throw new IllegalStateException("410 Gone:PointsService.checkAndGrantCfReward 已下线,请使用 CfBalanceService.earnByRule");
     }
     }
 
 
     @Transactional
     @Transactional

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -1257,7 +1257,7 @@ public class ProductOrderService {
      * 规则:
      * 规则:
      * <ul>
      * <ul>
      *   <li>券实例在 family_platform_exchange_record 有 completed 兑换记录 → 用户用 CF 值兑换而来,
      *   <li>券实例在 family_platform_exchange_record 有 completed 兑换记录 → 用户用 CF 值兑换而来,
-     *       按原始消耗 cfCost 退还到家庭池(退还幂等,同一券实例只退一次);</li>
+     *       按原始消耗 cfCost 原路退还到兑换人个人统一账本 cf_balance(ref_type=coupon_refund,退还幂等,同一券实例只退一次);</li>
      *   <li>无兑换记录 → 平台赠送券(JOIN 开通会员 / POPULATION 新增成员 / MANUAL 手动发放 /
      *   <li>无兑换记录 → 平台赠送券(JOIN 开通会员 / POPULATION 新增成员 / MANUAL 手动发放 /
      *       供应商临时券等),只归还券,不退还 CF 值。</li>
      *       供应商临时券等),只归还券,不退还 CF 值。</li>
      * </ul>
      * </ul>

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/api/PointsServiceInterface.java

@@ -9,7 +9,17 @@ public interface PointsServiceInterface {
     Map<String, Object> getBalance(Long memberId);
     Map<String, Object> getBalance(Long memberId);
     Page<PointsLog> getPointsLogs(Long memberId, Integer page, Integer size);
     Page<PointsLog> getPointsLogs(Long memberId, Integer page, Integer size);
     Map<String, Object> adjustPoints(Long memberId, Integer amount, String reason, String password, Long userId);
     Map<String, Object> adjustPoints(Long memberId, Integer amount, String reason, String password, Long userId);
+
+    /**
+     * @deprecated 410 Gone(F-8 / 2026-10-25):发放统一走 {@code CfBalanceService.earnByRule}
+     */
+    @Deprecated
     int awardCfPoints(Long memberId, int amount, String reason);
     int awardCfPoints(Long memberId, int amount, String reason);
+
+    /**
+     * @deprecated 410 Gone(F-8 / 2026-10-25):消耗统一走 {@code CfSpendDispatchService / CfBalanceService.spend}
+     */
+    @Deprecated
     int deductCfPoints(Long memberId, int amount, String reason);
     int deductCfPoints(Long memberId, int amount, String reason);
     int getCfPointsBalance(Long memberId);
     int getCfPointsBalance(Long memberId);
     Page<PointsLog> getPointsLogsByCategory(Long memberId, String category, Integer page, Integer size);
     Page<PointsLog> getPointsLogsByCategory(Long memberId, String category, Integer page, Integer size);

+ 3 - 3
cfc-backend/src/main/resources/schema.sql

@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS users (
 --     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否开启扣分(0否1是)',
 --     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否开启扣分(0否1是)',
 --     theme VARCHAR(32) DEFAULT 'default',
 --     theme VARCHAR(32) DEFAULT 'default',
 --     total_points INT DEFAULT 0,
 --     total_points INT DEFAULT 0,
---     system_points INT DEFAULT 0 COMMENT 'CF值(测评建档/平台发放)',
+--     system_points INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励,仅只读历史;CF值见cf_balance)',
 --     streak_days INT DEFAULT 0,
 --     streak_days INT DEFAULT 0,
 --     last_task_date DATE,
 --     last_task_date DATE,
 --     focus_max_daily TINYINT DEFAULT 3,
 --     focus_max_daily TINYINT DEFAULT 3,
@@ -2209,7 +2209,7 @@ CREATE TABLE IF NOT EXISTS family_members (
     id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号(孩子)',
     id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号(孩子)',
     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)',
     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)',
     theme VARCHAR(32) DEFAULT 'default' COMMENT '主题',
     theme VARCHAR(32) DEFAULT 'default' COMMENT '主题',
-    system_points INT DEFAULT 0 COMMENT 'CF值(干预/平台奖励)',
+    system_points INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励,仅只读历史;CF值见cf_balance)',
     streak_days INT DEFAULT 0 COMMENT '连续打卡天数',
     streak_days INT DEFAULT 0 COMMENT '连续打卡天数',
     last_task_date DATE COMMENT '上次任务日期',
     last_task_date DATE COMMENT '上次任务日期',
     focus_max_daily TINYINT DEFAULT 3 COMMENT '每日专注上限',
     focus_max_daily TINYINT DEFAULT 3 COMMENT '每日专注上限',
@@ -5035,7 +5035,7 @@ CREATE TABLE IF NOT EXISTS family_platform_exchange_record (
     updated_at    DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     updated_at    DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_family_time (family_id, created_at),
     INDEX idx_family_time (family_id, created_at),
     INDEX idx_user (user_id)
     INDEX idx_user (user_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭CF值兑换记录';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券CF兑换记录(券挂家庭,CF扣兑换人个人账本cf_balance)';
 
 
 -- ===== 健康方案汇总 =====
 -- ===== 健康方案汇总 =====
 
 

+ 94 - 70
cfc-backend/src/test/java/com/etotem/cfc/service/PlatformPointsServiceTest.java

@@ -1,33 +1,40 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.PlatformBalanceLog;
 import com.etotem.cfc.entity.PlatformBalanceLog;
-import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.UserPlatformBalance;
 import com.etotem.cfc.entity.UserPlatformBalance;
 import com.etotem.cfc.mapper.PlatformBalanceLogMapper;
 import com.etotem.cfc.mapper.PlatformBalanceLogMapper;
-import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserPlatformBalanceMapper;
 import com.etotem.cfc.mapper.UserPlatformBalanceMapper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
+import org.junit.jupiter.api.function.Executable;
 import org.mockito.InjectMocks;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoAnnotations;
-import org.springframework.dao.DuplicateKeyException;
+
+import java.util.Map;
 
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.when;
 
 
 /**
 /**
- * 个人 CF 钱包(PlatformPointsService)核心资金行为测试:
- *  - earn 幂等((userId, ref_type, ref_id) 命中即跳过)
- *  - earn 并发重复由唯一索引兜底回滚
- *  - spend 原子条件扣减(余额不足抛异常)
- *  - 家庭 ID 回填
+ * F-8(2026-10-25)个人 CF 钱包 {@link PlatformPointsService} 下线行为测试。
+ *
+ * <p>CF 值的唯一事实源是成员维度统一账本 {@code cf_balance};本服务承载的「个人 CF 池」
+ * 自 F-6 迁移后仅作为只读历史账本保留,写接口统一 410 Gone。
+ *
+ * <ul>
+ *   <li>{@code earn} / {@code spend} / {@code freeze} / {@code unfreeze} / {@code confirmWithdraw}
+ *       → 抛 {@link IllegalStateException},消息含 "410 Gone",且不产生任何写入</li>
+ *   <li>{@code getOrCreate} → 410 Gone,不再向 {@code user_platform_balance} 懒插入空行</li>
+ *   <li>只读方法 {@code getBalance} / {@code getWithdrawable} / {@code getLogs} 仍可用,
+ *       但<b>不再触发任何写入</b>(含建行)</li>
+ * </ul>
  */
  */
 class PlatformPointsServiceTest {
 class PlatformPointsServiceTest {
 
 
@@ -35,8 +42,6 @@ class PlatformPointsServiceTest {
     private UserPlatformBalanceMapper balanceMapper;
     private UserPlatformBalanceMapper balanceMapper;
     @Mock
     @Mock
     private PlatformBalanceLogMapper logMapper;
     private PlatformBalanceLogMapper logMapper;
-    @Mock
-    private UserMapper userMapper;
 
 
     @InjectMocks
     @InjectMocks
     private PlatformPointsService platformPointsService;
     private PlatformPointsService platformPointsService;
@@ -46,92 +51,111 @@ class PlatformPointsServiceTest {
         MockitoAnnotations.openMocks(this);
         MockitoAnnotations.openMocks(this);
     }
     }
 
 
-    private UserPlatformBalance balance(Long userId, int available, int totalEarned, Long familyId) {
+    private UserPlatformBalance balance(Long userId, int available, int totalEarned) {
         UserPlatformBalance b = new UserPlatformBalance();
         UserPlatformBalance b = new UserPlatformBalance();
         b.setUserId(userId);
         b.setUserId(userId);
         b.setAvailable(available);
         b.setAvailable(available);
         b.setTotalEarned(totalEarned);
         b.setTotalEarned(totalEarned);
-        b.setFamilyId(familyId);
+        b.setFrozen(0);
+        b.setWithdrawn(0);
+        b.setExchanged(0);
         return b;
         return b;
     }
     }
 
 
-    private User user(Long familyId) {
-        User u = new User();
-        u.setId(1L);
-        u.setFamilyId(familyId);
-        return u;
+    private void assertGone(Executable executable, String method) {
+        IllegalStateException ex = assertThrows(IllegalStateException.class, executable);
+        assertTrue(ex.getMessage().contains("410 Gone"),
+                method + " 应抛 410 Gone 异常,实际消息:" + ex.getMessage());
     }
     }
 
 
     @Test
     @Test
-    void earn_幂等键命中_跳过发放() {
-        when(logMapper.selectCount(any())).thenReturn(1L);
+    void earn_410Gone_doesNotWriteDb() {
+        assertGone(() -> platformPointsService.earn(1L, 10, "order_consume", 99L, "消费返CF"), "earn");
 
 
-        platformPointsService.earn(1L, 10, "order_consume", 99L, "x");
+        verify(balanceMapper, never()).updateById(any());
+        verify(balanceMapper, never()).insert(any());
+        verify(logMapper, never()).insert(any());
+    }
+
+    @Test
+    void spend_410Gone_doesNotWriteDb() {
+        assertGone(() -> platformPointsService.spend(1L, 10, "cf_transfer_out", 9L, "转给成员"), "spend");
 
 
         verify(balanceMapper, never()).updateById(any());
         verify(balanceMapper, never()).updateById(any());
+        verify(balanceMapper, never()).insert(any());
         verify(logMapper, never()).insert(any());
         verify(logMapper, never()).insert(any());
     }
     }
 
 
     @Test
     @Test
-    void earn_正常发放_回填家庭ID并写流水() {
-        when(logMapper.selectCount(any())).thenReturn(0L);
-        when(balanceMapper.selectOne(any())).thenReturn(balance(1L, 0, 0, null));
-        when(userMapper.selectById(1L)).thenReturn(user(200L));
-        when(balanceMapper.updateById(any())).thenReturn(1);
-        when(logMapper.insert(any())).thenReturn(1);
-
-        platformPointsService.earn(1L, 10, "order_consume", 99L, "消费返CF");
-
-        ArgumentCaptor<UserPlatformBalance> balanceCap = ArgumentCaptor.forClass(UserPlatformBalance.class);
-        verify(balanceMapper, org.mockito.Mockito.atLeastOnce()).updateById(balanceCap.capture());
-        UserPlatformBalance last = balanceCap.getValue();
-        assertEquals(10, last.getTotalEarned());
-        assertEquals(10, last.getAvailable());
-        assertEquals(200L, last.getFamilyId());
-
-        ArgumentCaptor<PlatformBalanceLog> logCap = ArgumentCaptor.forClass(PlatformBalanceLog.class);
-        verify(logMapper).insert(logCap.capture());
-        assertEquals("earn", logCap.getValue().getType());
-        assertEquals(10, logCap.getValue().getBalanceAfter());
-        assertEquals(99L, logCap.getValue().getRefId());
-        assertEquals(200L, logCap.getValue().getFamilyId());
+    void freeze_410Gone_doesNotWriteDb() {
+        assertGone(() -> platformPointsService.freeze(1L, 10, 99L), "freeze");
+
+        verify(balanceMapper, never()).updateById(any());
+        verify(balanceMapper, never()).insert(any());
+        verify(logMapper, never()).insert(any());
     }
     }
 
 
     @Test
     @Test
-    void earn_并发重复_唯一索引兜底回滚() {
-        when(logMapper.selectCount(any())).thenReturn(0L);
-        when(balanceMapper.selectOne(any())).thenReturn(balance(1L, 0, 0, 200L));
-        when(balanceMapper.updateById(any())).thenReturn(1);
-        when(logMapper.insert(any())).thenThrow(new DuplicateKeyException("Duplicate entry '1-order_consume-99'"));
-
-        assertThrows(RuntimeException.class,
-                () -> platformPointsService.earn(1L, 10, "order_consume", 99L, "x"));
+    void unfreeze_410Gone_doesNotWriteDb() {
+        assertGone(() -> platformPointsService.unfreeze(1L, 10, 99L), "unfreeze");
+
+        verify(balanceMapper, never()).updateById(any());
+        verify(balanceMapper, never()).insert(any());
+        verify(logMapper, never()).insert(any());
     }
     }
 
 
     @Test
     @Test
-    void spend_余额不足_抛异常且不写流水() {
-        when(balanceMapper.update(any(), any())).thenReturn(0);
+    void confirmWithdraw_410Gone_doesNotWriteDb() {
+        assertGone(() -> platformPointsService.confirmWithdraw(1L, 10, 99L), "confirmWithdraw");
 
 
-        assertThrows(RuntimeException.class,
-                () -> platformPointsService.spend(1L, 100, "cf_transfer_out", 9L, "x"));
+        verify(balanceMapper, never()).updateById(any());
+        verify(balanceMapper, never()).insert(any());
+        verify(logMapper, never()).insert(any());
+    }
+
+    @Test
+    void getBalance_readOnly_returnsHistorySnapshotWithoutInsert() {
+        when(balanceMapper.selectOne(any())).thenReturn(balance(1L, 30, 100));
 
 
+        Map<String, Object> result = platformPointsService.getBalance(1L);
+
+        assertEquals(30, result.get("available"));
+        assertEquals(100, result.get("totalEarned"));
+        // 关键:只读路径不得再向 user_platform_balance 建行
+        verify(balanceMapper, never()).insert(any());
+        verify(balanceMapper, never()).updateById(any());
         verify(logMapper, never()).insert(any());
         verify(logMapper, never()).insert(any());
     }
     }
 
 
     @Test
     @Test
-    void spend_成功扣减_写流水且不触碰exchanged() {
-        when(balanceMapper.update(any(), any())).thenReturn(1);
-        when(balanceMapper.selectOne(any())).thenReturn(balance(1L, 90, 100, 200L));
-        when(logMapper.insert(any())).thenReturn(1);
-
-        platformPointsService.spend(1L, 10, "cf_transfer_out", 9L, "转给成员");
-
-        // 原子条件更新:WHERE user_id=? AND available >= amount
-        verify(balanceMapper).update(isNull(), any());
-        ArgumentCaptor<PlatformBalanceLog> logCap = ArgumentCaptor.forClass(PlatformBalanceLog.class);
-        verify(logMapper).insert(logCap.capture());
-        assertEquals("spend", logCap.getValue().getType());
-        assertEquals(10, logCap.getValue().getAmount());
-        assertEquals(90, logCap.getValue().getBalanceAfter());
+    void getBalance_noRecord_returnsZeroWithoutInsert() {
+        when(balanceMapper.selectOne(any())).thenReturn(null);
+
+        Map<String, Object> result = platformPointsService.getBalance(1L);
+
+        assertEquals(0, result.get("available"));
+        assertEquals(0, result.get("totalEarned"));
+        verify(balanceMapper, never()).insert(any());
+    }
+
+    @Test
+    void getWithdrawable_readOnly_noInsertOrUpdate() {
+        when(balanceMapper.selectOne(any())).thenReturn(balance(1L, 30, 100));
+
+        assertEquals(30, platformPointsService.getWithdrawable(1L));
+
+        verify(balanceMapper, never()).insert(any());
+        verify(balanceMapper, never()).updateById(any());
+    }
+
+    @Test
+    void getLogs_readOnly_noWrite() {
+        when(logMapper.selectPage(any(), any())).thenReturn(new Page<>());
+
+        platformPointsService.getLogs(1L, 1, 20);
+
+        verify(logMapper, never()).insert(any());
+        verify(balanceMapper, never()).insert(any());
+        verify(balanceMapper, never()).updateById(any());
     }
     }
 }
 }

+ 1 - 1
cfc-frontend/pages.json

@@ -1937,7 +1937,7 @@
         {
         {
           "path": "index",
           "path": "index",
           "style": {
           "style": {
-            "navigationBarTitleText": "家庭CF值"
+            "navigationBarTitleText": "CF值与家庭优惠券"
           }
           }
         }
         }
       ]
       ]

+ 1 - 1
cfc-frontend/pages/wealth/index.vue

@@ -350,7 +350,7 @@ export default {
         { icon: '🔄', label: '售后服务', action: 'goToAfterSales' },
         { icon: '🔄', label: '售后服务', action: 'goToAfterSales' },
         { icon: '📊', label: '收益中心', action: 'goToIncome' },
         { icon: '📊', label: '收益中心', action: 'goToIncome' },
         { icon: '🎫', label: '我的优惠券', action: 'goToMyCoupons' },
         { icon: '🎫', label: '我的优惠券', action: 'goToMyCoupons' },
-        { icon: '💎', label: '家庭CF值', action: 'goToFamilyCf' }
+        { icon: '💎', label: 'CF值与家庭券', action: 'goToFamilyCf' }
       ],
       ],
       children: [],
       children: [],
       familyMembersVisible: [],
       familyMembersVisible: [],

+ 4 - 0
docs/cfc-redesign-progressive-unlock.md

@@ -1,5 +1,9 @@
 # 浠艾福小程序改版:需求驱动 · 逐步解锁
 # 浠艾福小程序改版:需求驱动 · 逐步解锁
 
 
+> ⚠️ **已被部分取代(2026-10-25,F-8 核销)**:「问题域完成奖励 CF 值 → 进家庭池」已改为
+> 个人成员账本 `cf_balance`(F20,见 `docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md`)。
+> 本文其余改版框架仍有效。
+
 > 阶段:实施计划 v1.0 | 分支:cfclub | 产品决策已确认(2026-08-13)
 > 阶段:实施计划 v1.0 | 分支:cfclub | 产品决策已确认(2026-08-13)
 
 
 ## 一、背景与目标
 ## 一、背景与目标

+ 4 - 0
docs/function-analysis-by-membership.md

@@ -1,5 +1,9 @@
 # 浠艾福功能分析定义表(按会员等级)
 # 浠艾福功能分析定义表(按会员等级)
 
 
+> ⚠️ **已被取代(2026-10-25,F-8 核销)**:「任务全完成奖励家庭池 / CF 值」相关表述已过时,
+> 按 `docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md` 执行
+> (F22 保留个人发放,唯一账本 `cf_balance`)。本文仅作历史参考。
+
 > 版本:v1.0 | 日期:2026-08-16 | 分支:cfclub
 > 版本:v1.0 | 日期:2026-08-16 | 分支:cfclub
 > 用途:对系统提供的功能进行分析定义,描述每个功能针对不同用户等级的前置关卡
 > 用途:对系统提供的功能进行分析定义,描述每个功能针对不同用户等级的前置关卡
 
 

+ 5 - 2
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,7 +1,7 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
 
-**文档版本:** v2.21
-**日期:** 2026-10-21
+**文档版本:** v2.22
+**日期:** 2026-10-25
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间⭐扇形菜单,设计已对齐实现)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(✅ 11 Tasks 全栈完成)** + **优惠券全链路改为家庭维度(待实施)** + **关系三图重构(关键人拓展章鱼图/珍珠图/能力图,设计规格已完成)** + **注册零填写+信息按需补充(✅ 全栈完成:登录直接进首页 + 家庭邀请独立页真实姓名/家庭身份必填 + 功能→信息字段映射表 + 添加成员手机号冲突邀请加入)** + **健康报告归属重分配(✅ 后端接口 + 管理端页面 + 小程序入口已实现,见 `specs/2026-10-05-health-report-reassign-design.md`)**
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间⭐扇形菜单,设计已对齐实现)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(✅ 11 Tasks 全栈完成)** + **优惠券全链路改为家庭维度(待实施)** + **关系三图重构(关键人拓展章鱼图/珍珠图/能力图,设计规格已完成)** + **注册零填写+信息按需补充(✅ 全栈完成:登录直接进首页 + 家庭邀请独立页真实姓名/家庭身份必填 + 功能→信息字段映射表 + 添加成员手机号冲突邀请加入)** + **健康报告归属重分配(✅ 后端接口 + 管理端页面 + 小程序入口已实现,见 `specs/2026-10-05-health-report-reassign-design.md`)**
 **指标变化曲线·指标速查·指标关联(✅ 全栈完成:后端11任务+小程序M1-M8+管理端W1-W6;HBuilderX人工验收待用户执行,见 `specs/2026-10-09-indicator-change-curve-design.md`)**
 **指标变化曲线·指标速查·指标关联(✅ 全栈完成:后端11任务+小程序M1-M8+管理端W1-W6;HBuilderX人工验收待用户执行,见 `specs/2026-10-09-indicator-change-curve-design.md`)**
 **饮食/睡眠/运动打卡接入任务完成链路(✅ 全栈完成:实体taskId字段 + 三个Controller isDailyProgress分流回写 + RepeatTaskGenerator每日生成打卡任务 + tasks.vue跳转 + 三打卡页taskId透传 + API文档;见 `specs/2026-10-13-health-checkin-task-linkage-design.md` 与 `plans/2026-10-13-health-checkin-task-linkage.md`)**
 **饮食/睡眠/运动打卡接入任务完成链路(✅ 全栈完成:实体taskId字段 + 三个Controller isDailyProgress分流回写 + RepeatTaskGenerator每日生成打卡任务 + tasks.vue跳转 + 三打卡页taskId透传 + API文档;见 `specs/2026-10-13-health-checkin-task-linkage-design.md` 与 `plans/2026-10-13-health-checkin-task-linkage.md`)**
@@ -9,6 +9,7 @@
 **菌群知识并入 dan_knowledge_base(🔄 实施中:809 篇文章以 source_type='article' 并入统一知识表 + 后端 sourceType/excludeSourceType 过滤 + langgraph 三源重建 + 管理端统一展示,见 `specs/2026-10-20-knowledge-unify-design.md` 与 `plans/2026-10-20-knowledge-unify.md`)**
 **菌群知识并入 dan_knowledge_base(🔄 实施中:809 篇文章以 source_type='article' 并入统一知识表 + 后端 sourceType/excludeSourceType 过滤 + langgraph 三源重建 + 管理端统一展示,见 `specs/2026-10-20-knowledge-unify-design.md` 与 `plans/2026-10-20-knowledge-unify.md`)**
 **限时特价(扫码闪购)(✅ 全栈完成:迁移389/390 → SpecialPriceService 7 方法 + 双控制器(公开 info 白名单 + admin 6 接口)→ 特价下单 createSpecialDealOrder + cancel/approveRefund/OrderTimeoutCancelTask 三处 releaseQuota 回滚 + 供应商改价拒绝 + 小程序商品页 sp 解析/倒计时/特价区块/checkout 透传 + cfc-web SpecialPriceManage.vue;5 个测试类 31 case 全过;见 `specs/2026-10-20-limited-time-special-price-design.md` 与 `plans/2026-10-20-limited-time-special-price.md`)**
 **限时特价(扫码闪购)(✅ 全栈完成:迁移389/390 → SpecialPriceService 7 方法 + 双控制器(公开 info 白名单 + admin 6 接口)→ 特价下单 createSpecialDealOrder + cancel/approveRefund/OrderTimeoutCancelTask 三处 releaseQuota 回滚 + 供应商改价拒绝 + 小程序商品页 sp 解析/倒计时/特价区块/checkout 透传 + cfc-web SpecialPriceManage.vue;5 个测试类 31 case 全过;见 `specs/2026-10-20-limited-time-special-price-design.md` 与 `plans/2026-10-20-limited-time-special-price.md`)**
 **后台多渠道发货(🟡 设计稿待审查:管理端发货对齐微信小程序后台四种方式——快递 logistics_type=1 / 同城配送 2(deliveryMethod 新增编码 6)/ 用户自提 4(不再跳过微信同步)/ 虚拟商品 3(支付成功自动发货 autoShipVirtualOrder,幂等 fail-open);另修正 upload_shipping_info URL 缺 /wxa 前缀;cfc-web 发货对话框按类型切换;见 `specs/2026-10-22-fulfillment-multi-mode-design.md`)**
 **后台多渠道发货(🟡 设计稿待审查:管理端发货对齐微信小程序后台四种方式——快递 logistics_type=1 / 同城配送 2(deliveryMethod 新增编码 6)/ 用户自提 4(不再跳过微信同步)/ 虚拟商品 3(支付成功自动发货 autoShipVirtualOrder,幂等 fail-open);另修正 upload_shipping_info URL 缺 /wxa 前缀;cfc-web 发货对话框按类型切换;见 `specs/2026-10-22-fulfillment-multi-mode-design.md`)**
+**CF 值体系统一收尾(F-8 清理批次,✅ 2026-10-25):三套旧 CF 账本(`family_platform_balance` / `family_members.system_points` / `user_platform_balance`)写接口全部 410 Gone(`PointsService` 5 个 + `PlatformPointsService` 6 个 + `FamilyPlatformPointsService` 7 个),只读历史保留;`system_points` 误导性「CF值」注释与 F-3b1 遗留「待 Q1 确认」注释清理完毕(F10 已定:每个完成成员各发一次);新增《CF 值获取/消耗单一事实源》规范文档;核销 W0.2「product_config 下线」条目(基线/测试库均无该表,`/api/admin/product-config/*` 已 410 Gone,无需删表);见 `specs/2026-10-25-cf-value-single-source-of-truth.md`**
 **维护:** 所有需求变更需更新本文档
 **维护:** 所有需求变更需更新本文档
 
 
 ---
 ---
@@ -228,6 +229,7 @@
 | **CF 值体系统一 · 数据层与配置归口(F-1 + W0.1)**(2026-10-24):8 张新表(cf_balance / cf_balance_log / cf_earn_rule + 24 行种子 / cf_earn_rule_history / cf_direct_sales_tier / cf_indirect_sales_tier / cf_earn_daily_limit + service_config)+ coupon/user_coupon 补 family_id + 迁移 407-412 + `ConfigService.getConfig`(缓存 TTL 60s + `CfConfigCache.invalidate`) | `plans/2026-10-24-cf-value-unification-plan.md` + `api/API_REFERENCE.md` 六 |
 | **CF 值体系统一 · 数据层与配置归口(F-1 + W0.1)**(2026-10-24):8 张新表(cf_balance / cf_balance_log / cf_earn_rule + 24 行种子 / cf_earn_rule_history / cf_direct_sales_tier / cf_indirect_sales_tier / cf_earn_daily_limit + service_config)+ coupon/user_coupon 补 family_id + 迁移 407-412 + `ConfigService.getConfig`(缓存 TTL 60s + `CfConfigCache.invalidate`) | `plans/2026-10-24-cf-value-unification-plan.md` + `api/API_REFERENCE.md` 六 |
 | **CF 值体系统一 · F-6 存量迁移**(2026-10-24):迁移417 将 `family_platform_balance`、`family_members.system_points`、`user_platform_balance` 一次性幂等迁入成员维度 `cf_balance`;三类 `migration_*` 流水守卫重复执行,旧表保留只读历史 | `plans/2026-10-24-cf-value-unification-plan.md` §六 + `api/API_REFERENCE.md` 6.4 |
 | **CF 值体系统一 · F-6 存量迁移**(2026-10-24):迁移417 将 `family_platform_balance`、`family_members.system_points`、`user_platform_balance` 一次性幂等迁入成员维度 `cf_balance`;三类 `migration_*` 流水守卫重复执行,旧表保留只读历史 | `plans/2026-10-24-cf-value-unification-plan.md` §六 + `api/API_REFERENCE.md` 6.4 |
 | **CF 值体系统一 · F-4 分佣收口 + F03a/F03b 发放接入**(2026-10-25):推荐人返佣收口为两级销售奖(F03a 按团队 8 层内滚动一年累计 P 点档位上限 70% / F03b 按自身滚动一年消费 P 点档位上限 20%),共同基数 = 本单消费 P 点;迁移418 补两级档位种子 + `dedup_ref_type` 对齐为 F03a/F03b,迁移420 新建 `ppoint_consumption_log`(滚动一年聚合数据源,唯一键 `order_type+order_id`;419 已被 `membership_levels.max_daily_analyze` 占用);发放统一走 F-2 `earnByRule` 只写 `cf_balance` + `cf_balance_log`;与 F-3a 共存(当前消费人 F02 + 推荐人 F03a/F03b 都走统一账本,`cf_transfer_record` 不再由分佣服务写入);`CommissionDistService.distribute` 写接口 410 Gone、`commission_dist_log` 转只读;`/api/commission/cf/rate` 扩展两级档位字段 | `plans/2026-10-24-cf-value-unification-plan.md` §四/§3.1/§4.2/§4.4 + `api/API_REFERENCE.md` 4.33 + 6.5 |
 | **CF 值体系统一 · F-4 分佣收口 + F03a/F03b 发放接入**(2026-10-25):推荐人返佣收口为两级销售奖(F03a 按团队 8 层内滚动一年累计 P 点档位上限 70% / F03b 按自身滚动一年消费 P 点档位上限 20%),共同基数 = 本单消费 P 点;迁移418 补两级档位种子 + `dedup_ref_type` 对齐为 F03a/F03b,迁移420 新建 `ppoint_consumption_log`(滚动一年聚合数据源,唯一键 `order_type+order_id`;419 已被 `membership_levels.max_daily_analyze` 占用);发放统一走 F-2 `earnByRule` 只写 `cf_balance` + `cf_balance_log`;与 F-3a 共存(当前消费人 F02 + 推荐人 F03a/F03b 都走统一账本,`cf_transfer_record` 不再由分佣服务写入);`CommissionDistService.distribute` 写接口 410 Gone、`commission_dist_log` 转只读;`/api/commission/cf/rate` 扩展两级档位字段 | `plans/2026-10-24-cf-value-unification-plan.md` §四/§3.1/§4.2/§4.4 + `api/API_REFERENCE.md` 4.33 + 6.5 |
+| **CF 值体系统一 · F-8 清理收尾**(2026-10-25):三套旧账本写接口 410 Gone(`PointsService` awardCfPoints/awardCheckinPoints/deductForWishCf/deductCfPoints/checkAndGrantCfReward;`PlatformPointsService` getOrCreate/earn/spend/freeze/unfreeze/confirmWithdraw;`FamilyPlatformPointsService` getOrCreate/earn/spend/freeze/unfreeze/confirmWithdraw/adjust;`adjustPoints` 保留为活跃 F19 适配层,`exchangeCouponByCf`/`refundCouponCf` 保留);清理 `system_points`「CF值」误导性注释与 F-3b1「待 Q1 确认」注释(F10 已定:每个完成成员各发一次);新增《CF 值获取/消耗单一事实源》规范文档;核销 W0.2「product_config 下线」;未新增 DB 迁移 | `specs/2026-10-25-cf-value-single-source-of-truth.md` + `plans/2026-10-24-cf-value-unification-plan.md` §七 F-8 |
 
 
 ---
 ---
 
 
@@ -323,6 +325,7 @@
 
 
 | 文件 | 状态 | 说明 |
 | 文件 | 状态 | 说明 |
 |------|:----:|------|
 |------|:----:|------|
+| `2026-10-25-cf-value-single-source-of-truth.md` | ✅ 当前(CF 值唯一事实源规范) | CF 值获取/消耗单一事实源:唯一账本 `cf_balance`(成员维度)+ 24 条获取规则与 refType + 消费侧 refType 清单 + 幂等/并发口径 + 三套旧账本 410 Gone 清单 + W0.2 核销记录 |
 | `2026-07-22-comprehensive-implementation.md` | ✅ Phase 2-4 全部完成 | 全面实施计划(Phase 2-4) |
 | `2026-07-22-comprehensive-implementation.md` | ✅ Phase 2-4 全部完成 | 全面实施计划(Phase 2-4) |
 | `2026-07-20-langgraph-migration-design.md` | ✅ 当前 | LangGraph 架构设计 |
 | `2026-07-20-langgraph-migration-design.md` | ✅ 当前 | LangGraph 架构设计 |
 | `2026-07-20-langgraph-requirements-summary.md` | ⚠️ 文件缺失 | 需求与决策汇总 |
 | `2026-07-20-langgraph-requirements-summary.md` | ⚠️ 文件缺失 | 需求与决策汇总 |

+ 6 - 1
docs/superpowers/specs/2026-08-12-platform-points-family-pool-design.md

@@ -1,8 +1,13 @@
 # 家庭平台积分(CF值)体系设计
 # 家庭平台积分(CF值)体系设计
 
 
+> ⚠️ **已被取代(2026-10-25,F-8 核销)**:本设计文档的「CF 值 = 家庭共享池」口径已被
+> `docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md` 否定(N1/N2:CF 值不存在家庭池,
+> 唯一事实源是成员维度 `cf_balance`),按 `specs/2026-10-25-cf-value-single-source-of-truth.md` 执行。
+> 本文仅作历史归档参考,**请勿据此实现**。
+
 > 日期:2026-08-12
 > 日期:2026-08-12
 > 分支:cfclub
 > 分支:cfclub
-> 状态:待评审
+> 状态:待评审(已废弃)
 
 
 ## 1. 背景与目标
 ## 1. 背景与目标
 
 

+ 6 - 1
docs/superpowers/specs/2026-09-01-cf-commission-redesign.md

@@ -1,7 +1,12 @@
 # CF 值分佣体系重构设计文档
 # CF 值分佣体系重构设计文档
 
 
+> ⚠️ **已被部分取代(2026-10-25,F-8 核销)**:本文「返个人钱包 / 不进家庭池」与
+> `confirmReceive` 返家庭池「保留不动」等口径已被 `plans/2026-10-24-cf-value-unification-plan.md` 覆盖
+> (F02 消费返 CF 覆盖所有订单;唯一事实源见 `specs/2026-10-25-cf-value-single-source-of-truth.md`)。
+> 分佣档位(F03a/F03b)部分仍有效。请勿据此实现 CF 归属。
+
 > 日期:2026-09-01
 > 日期:2026-09-01
-> 状态:待评审
+> 状态:待评审(部分已废弃)
 > 分支:cfc 主分支
 > 分支:cfc 主分支
 
 
 ## 1. 背景与目标
 ## 1. 背景与目标

+ 253 - 0
docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md

@@ -0,0 +1,253 @@
+# CF 值获取/消耗单一事实源(Single Source of Truth)
+
+> 版本:v1.0 | 日期:2026-10-25 | 分支:cfclub
+> 上游计划:`docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md`(F-8 清理批次交付物之一)
+> 本文件是 CF 值的**唯一事实源**。新增任何 CF 相关功能前必须先读本文件;与旧文档冲突时**以本文件为准**。
+
+---
+
+## 0. 一句话结论
+
+**CF 值只有一套账本:`cf_balance`(记账维度 = `family_members.id`,成员维度)+ `cf_balance_log`(流水)。**
+
+- **CF 值不存在「家庭池」**(N1:`family_platform_balance` 不再承接任何发放/余额)
+- **CF 值不存在「个人钱包 / user 维度」**(N2:不再走 `user_platform_balance`)
+- **CF 值不在 `family_members.system_points`**(该列仅保留只读历史值)
+- 优惠券「挂家庭」共享(N3),但 CF 扣的是**兑换人自己**的 `cf_balance`
+
+三套旧账本自 F-6 迁移(2026-10-24,迁移 417)起**停止写入、仅保留只读历史**,写接口已全部标 `410 Gone`(见 §5)。
+
+---
+
+## 1. 唯一账本
+
+| 表 | 主键/维度 | 用途 |
+|---|---|---|
+| `cf_balance` | `member_id` = `family_members.id` | 成员 CF 值余额(available / frozen / withdrawn / total_earned 等) |
+| `cf_balance_log` | `(member_id, type, ref_type, ref_id)` 唯一键 | 每一笔 CF 变动的流水(含 `operator_user_id` 留痕) |
+| `cf_earn_rule` | `code`(F02–F24,24 行) | 获取侧规则配置(数量/日限/冷却/档位表引用) |
+| `cf_earn_rule_history` | 变更审计 | 规则被修改的历史(N11 留痕) |
+| `cf_earn_daily_limit` | `level_code` | 会员等级日限映射(N11) |
+| `cf_direct_sales_tier` / `cf_indirect_sales_tier` | 档位表 | F03a/F03b 两级销售奖档位 |
+
+> 迁移 407–412、416–418、420(见计划 §七)。F-8 清理批次**未新增任何 DB 迁移**。
+
+---
+
+## 2. 获取侧(Earn)
+
+### 2.1 统一入口
+
+```java
+cfBalanceService.earnByRule(memberId, ruleCode, refId, ctx)
+```
+
+- **业务代码不要直接 `cfBalanceService.earn(...)` 发规则奖励**;一律经 `earnByRule`,
+  由它负责读规则 → 会员等级日限/冷却校验 → 计算数量 → 幂等写入。
+- 业务侧统一封装在 **`CfPathwayEarnService`**(按途径一个方法,规则编码常量 `RULE_F02`…`RULE_F24`)。
+- 规则数量/日限/冷却/档位表引用**禁止写死在业务代码**,一律读 `cf_earn_rule`(管理端可改,改完写 `cf_earn_rule_history`)。
+- 数量单位换算(1 元 = 100 CF)走规则的 `params_json`(`cf_per_yuan` + `amt_unit=cent`),不写死。
+
+### 2.2 24 条获取途径与幂等 refType
+
+| 规则 | 名称 | rule_type | 幂等 `ref_type` | 数量口径 |
+|---|---|---|---|---|
+| F02 | 消费返 CF(所有订单) | formula | `cf_consume_order` | `floor(amt)`,1 元=100 CF |
+| F03a | 直接销售奖 | formula | `F03a` | `floor(base_ppoints * rate_percent / 100)`,上限 70% |
+| F03b | 间接销售奖 | formula | `F03b` | 同上,上限 20% |
+| F04 | 商品赠品 CF | per_item | `product_order_gift` | `product_order_gift.cf_value` |
+| F05 | 活动签到 CF | per_item | `activity_checkin` | `activities.platform_points` |
+| F06 | 菌群健康调研打卡返还 | formula | `gut_health_refund` | `floor(amt)`,兜底 `gut_health_project.cf_refund_amount` |
+| F07 | 文章答题奖励 | formula | `article_answer` | `score + (all_correct ? 3 : 0)`,日限 5 |
+| F08 | AI 阅读答题奖励 | fixed | `ai_reading_answer` | 固定 5,须全部答对 |
+| F09 | 完成测评建档 | formula | `assessment_report` | 第 1 份 50 / 后续每份 10,重复报告不计 |
+| F10 | 家庭挑战完成 | per_item | `family_challenge` | `family_challenge.reward_points`,**每个完成成员各发一次** |
+| F11 | 邀请里程碑 | per_item | `invite_milestone` | `invite_milestone.reward_points` |
+| F12 | 成长任务 | per_item | `growth_task` | `growth_task.reward_points` |
+| F13 | 分享阅读奖励 | fixed | `share_read` | 默认 5,日限 5 |
+| F14 | 喝水打卡 | fixed | `health_water` | 5 CF/次,日限按会员等级 |
+| F15 | 饮食打卡 | fixed | `health_meal` | 5 CF/次,日限按会员等级 |
+| F16 | 每日健康打卡 | fixed | `daily_health_checkin` | 10 CF/次,日限按会员等级 |
+| F17 | 挑战赛子任务完成 | per_item | `challenge_sub_task` | `challenge_task.reward_cf` |
+| F18 | 健康启航计划付费 | formula | `health_start_plan` | `floor(amt)`,与 F02 同口径 |
+| F19 | 管理员手动调整 | per_item | `admin_adjust` | 正负任意,需家长密码,必须留痕 |
+| F20 | 问题域全部任务完成 | per_item | `problem_domain` | `problem_domain.reward_cf_value` |
+| F21 | 关卡通关奖励 | per_item | `unlock_gate` | `unlock_gate.reward_points` |
+| F22 | 任务完成 CF | per_item | `task_complete` | `task.cf_amount`,日限按会员等级 |
+| F23 | 成员间转让(收款方) | per_item | `cf_transfer` | 转入 +X / 转出 −X,同一事务 |
+| F24 | 分享奖励(推荐注册) | fixed | `referral_register` | 20 CF/人,日限 10 |
+
+> 种子数据:`DatabaseInitializer.CF_EARN_RULE_SEEDS`(迁移 408)。种子只「已存在则跳过」,不覆盖运营改过的配置。
+
+### 2.3 发放口径要点
+
+- **F10(Q1 已定,2026-10-25 拍板)**:每个完成成员各发一次——`ref_id = challenge.id`,
+  幂等键含 `member_id` → 同一成员同一挑战只发一次;数量随完成成员数放大属预期行为。
+- **F19 管理员调整**:走 `CfPathwayEarnService.adjustByAdmin`(F19 规则),
+  正数 `earnByRule`、负数 `cfBalanceService.adjust`,`ref_type` 同为 `admin_adjust`,
+  流水留操作人 `operator_user_id`。**禁止绕过它直接调 `cfBalanceService.adjust`。**
+- **消费返 CF(F02)** 与 **推荐人销售奖(F03a/F03b)** 在同一订单下同时发放,
+  两者 `ref_type` 不同,互不干扰。
+
+---
+
+## 3. 消耗侧(Spend)
+
+### 3.1 统一入口
+
+```java
+cfSpendDispatchService.<verb>(userId | memberId, amount, refType, refId, remark)
+```
+
+- `CfSpendDispatchService` 负责**用户维度 → 成员维度换算**(`family_members.user_id → id`),
+  再委托 `CfBalanceService` 的原子方法。业务代码优先用 `*ForUser` 变体。
+- **禁止绕过 `CfSpendDispatchService` 直接对接第三方 LLM 或直接写账本表**(同 `AiGateway` 约定)。
+- `refType` 必须用下方清单中的值;新增场景先在清单里登记,再实现。
+
+### 3.2 消费/退还 refType 清单
+
+| 场景 | 方法 | `ref_type` | `ref_id` |
+|---|---|---|---|
+| 会员续费抵扣 | `spendForUser` | `membership_renewal_cf` | 续费订单 ID |
+| 商品购买抵扣 | `spendForUser` | `product_order_points` | 商品订单 ID |
+| 商品订单退还 | `refundForUser` | `product_order_points_refund` | 商品订单 ID |
+| 优惠券兑换 | `spendForUser` | `coupon_exchange` | 券实例 ID(`family_coupon.id`) |
+| 优惠券退还 | `refundForUser` | `coupon_refund` | 券实例 ID(`family_coupon.id`) |
+| 积分兑换商品 | `spendForUser` | `exchange_product` | 兑换记录 ID |
+| 积分兑换优惠券 | `spendForUser` | `exchange_coupon` | 兑换记录 ID |
+| 提现(冻结) | `freezeForMember` | `withdrawal` | 提现申请 ID |
+| 提现(驳回解冻) | `unfreezeForMember` | `withdrawal` | 提现申请 ID |
+| 提现(通过) | `withdrawForMember` | `withdrawal` | 提现申请 ID |
+| 心愿兑换 | `spendForMember` | `wish_exchange` | 心愿 ID |
+| 管理员调整(家庭) | `adjustForFamily` | `admin_adjust` | —(落到家庭创建者成员) |
+| 成员间转让 | `CfTransferService` | `cf_transfer` | 转让记录 ID |
+
+### 3.3 退还(原路返还)规则
+
+- 退还一律按**原扣减成员、原数量、原单据 ID** 以 `earn` 方式回补。
+- 退还的 `ref_type` 与原扣减不同(`coupon_refund` vs `coupon_exchange`),
+  因此同一笔消费可安全「扣一次、退一次」;重复退命中账本唯一键被拦截。
+- 平台赠送券(JOIN 开通会员 / POPULATION 新增成员 / MANUAL 手动发放 / 供应商临时券)
+  **不退还 CF**(无 completed 兑换记录 → 只归还券)。
+
+### 3.4 开关与配置
+
+- 提现总开关:`service_config.cf.withdraw_enabled`(默认关,Q4:涉及合规审查)。
+- 数值类参数一律走 `ConfigService.getConfig`(L2 `service_config`,缓存 TTL 60s + `CfConfigCache.invalidate`),**禁止写死业务常量**。
+
+---
+
+## 4. 并发与幂等
+
+| 机制 | 实现 |
+|---|---|
+| 行锁 | `CfBalanceService` 写前 `SELECT ... FOR UPDATE`(按 `member_id`) |
+| 幂等键 | `cf_balance_log (member_id, type, ref_type, ref_id)` 唯一键(迁移 416 升级) |
+| 三道闸 | 会员等级日限(`cf_earn_daily_limit`)→ 累计上限(`total_limit_per_member`)→ 冷却期(`cooldown_seconds`);命中写 skip 流水 |
+| 规则变更留痕 | `cf_earn_rule_history`(迁移 408) |
+| 管理端写操作审计 | `audit_log`(迁移 414) |
+
+> `refId` 必须传**稳定业务单据 ID**(订单号、券实例 ID、挑战 ID、规则记录 ID 等)。
+> 传空 `refId` 是历史兼容入口的产物,已随 F-8 一并下线(见 §5.3)。
+
+---
+
+## 5. 三套旧账本:只读历史 + 410 Gone
+
+### 5.1 旧表处置
+
+| 旧表 | 处置 | 只读查询入口 |
+|---|---|---|
+| `family_platform_balance`(+ `_log`) | 停写,保留历史(F-6 后 available 清零、frozen 释放为 available) | `POST /api/family-platform/balance`、`/logs`、`/admin/balance`、`/admin/logs` |
+| `user_platform_balance`(+ `platform_balance_log`) | 停写,保留历史(available 清零) | `POST /api/commission/cf/summary`、`/list`;`POST /api/admin/cf-value/*` |
+| `family_members.system_points` | **列保留只读历史值**,不再写入(成员退出家庭时仅清零残留值) | `POST /api/points/system-balance`(已改读 `cf_balance`) |
+
+> 只读接口的返回可能是 0 值历史快照。**任何需要「实时 CF」的地方都必须读 `cf_balance`。**
+
+### 5.2 写接口 410 Gone 清单(F-8,2026-10-25)
+
+以下方法统一 `@Deprecated` + `log.warn("410 Gone...")` + `throw new IllegalStateException("410 Gone:...")`:
+
+**`PlatformPointsService`**(原个人 CF 池)
+`getOrCreate`、`earn`、`spend`、`freeze`、`unfreeze`、`confirmWithdraw`
+保留只读:`getBalance`、`getWithdrawable`、`getLogs`
+
+**`FamilyPlatformPointsService`**(原家庭 CF 池)
+`getOrCreate`、`earn`、`spend`、`freeze`、`unfreeze`、`confirmWithdraw`、`adjust`
+保留:`exchangeCouponByCf`、`refundCouponCf`(**二者经 `CfSpendDispatchService` 写统一账本,仍在服役**)、
+`getBalance`、`getWithdrawable`、`getLogs`、`listPage`、`getExchangeRecords`
+
+**`PointsService`**(原 `system_points` 写入口)
+`awardCfPoints`、`awardCheckinPoints`、`deductForWishCf`、`deductCfPoints`、`checkAndGrantCfReward`
+保留:`adjustPoints`(F19 管理员调整活跃适配层,已收口到 `CfPathwayEarnService.adjustByAdmin` 写统一账本)、
+`getBalance`、`getCfPointsBalance`(改读 `cf_balance`)、`getPointsLogs*`、
+`deductUserPoints` / `refundUserPoints`(写 `users.total_points` **普通成长积分**,非 CF,不在本批次范围)
+
+**`CommissionDistService.distribute`**(F-4 已下线,`commission_dist_log` 转只读)
+
+### 5.3 新增/修改 CF 相关代码的检查单
+
+- [ ] 发放走 `CfPathwayEarnService` → `cfBalanceService.earnByRule`?
+- [ ] 消耗走 `CfSpendDispatchService`?
+- [ ] `refType` 已在 §2.2 / §3.2 清单内?新增则先登记?
+- [ ] `refId` 传稳定业务单据 ID?
+- [ ] 数量/日限/冷却读 `cf_earn_rule`,数值读 `service_config`?
+- [ ] **没有**调用 §5.2 中标 410 的方法?
+- [ ] **没有**新增任何对 `family_platform_balance` / `user_platform_balance` / `family_members.system_points` 的写入?
+- [ ] 前端读实时 CF 用 `/api/cf/balance`、`/api/cf/logs`(不是 `/api/commission/cf/summary`)?
+
+---
+
+## 6. C 端与管理端接口对照
+
+| 需求 | 用这个 | 不要用这个(旧) |
+|---|---|---|
+| 我的 CF 余额 | `POST /api/cf/balance` | `POST /api/commission/cf/summary`、`POST /api/family-platform/balance` |
+| 我的 CF 流水 | `POST /api/cf/logs` | `POST /api/commission/cf/list` |
+| 家庭共享优惠券 | `POST /api/cf/family-coupons` | — |
+| 用 CF 兑换优惠券 | `POST /api/family-platform/exchange-coupon` | —(仍在服役) |
+| CF 规则/档位配置 | `POST /api/admin/cf-earn-rule/*`、`POST /api/admin/cf-rate-tier/*` | `POST /api/admin/product-config/*`(410 Gone) |
+| 管理员查历史账本 | `POST /api/admin/cf-value/*`、`POST /api/family-platform/admin/*` | —(只读对账用) |
+
+> C 端接口由 `CfBalanceController`(F-7a)提供;小程序入口 `pages/wealth-cf/index.vue`(标题「CF值与家庭优惠券」)。
+
+---
+
+## 7. F-6 存量迁移映射(迁移 417)
+
+| 来源 | 去向 | 流水 `ref_type` |
+|---|---|---|
+| `family_platform_balance`(available+frozen → available) | 家庭创建者对应 `family_member` 的 `cf_balance` | `migration_family_pool` |
+| `family_members.system_points > 0` | 各成员 `cf_balance` | `migration_system_points` |
+| `user_platform_balance`(available+frozen) | 该用户自己的 `family_member` 的 `cf_balance` | `migration_user_wallet` |
+
+三类流水 `ref_id = 0`,独立幂等,可重复执行不重复入账。
+
+---
+
+## 8. 核销记录:W0.2「product_config 下线」
+
+> 计划 §W0.2 要求「F-8 清理批次需按此口径核销 product_config 下线条目」。**核销结论(2026-10-25):已核销,无遗留动作。**
+
+| 项 | 状态 |
+|---|---|
+| `product_config` 表并入 `service_config` | ✅ 迁移 413「表存在才并入、缺失则幂等空跑」 |
+| 基线库 / 测试库 `192.168.16.251:3306/zxyj` 是否存在该表 | ❌ **均不存在**(亦无 `ProductConfigItem` 实体与 `ProductConfigServiceImpl`) |
+| 旧管理端写接口 | ✅ `AdminProductConfigController`(`/api/admin/product-config/*`)已 `@Deprecated` + `@ResponseStatus(HttpStatus.GONE)` + `Result.error(410, ...)` |
+| 替代入口 | ✅ `AdminServiceConfigController`(`/api/admin/service-config/list`、`/save`,保存时传 `productKey`) |
+| 验收「配置中心只有一个入口」 | ✅ 达成(`service_config` 唯一) |
+| 第 31 天(2026-11-23 后)删表 | ⚪ 表本就不存在,**无需删表**;仅需在 2026-11-23 后删除 `AdminProductConfigController`(可选项,保留 410 亦无害) |
+
+---
+
+## 9. 已被取代的历史文档(勿据此实现)
+
+| 文档 | 被谁取代 |
+|---|---|
+| `specs/2026-08-12-platform-points-family-pool-design.md`(「CF 值 = 家庭共享池」) | 本文件 + 计划 §一(N1/N2) |
+| `specs/2026-09-01-cf-commission-redesign.md`(「返个人钱包 / 不进家庭池」、`confirmReceive` 返家庭池「保留不动」) | 本文件 §2.2(F02)+ §5.2 |
+| `docs/cfc-redesign-progressive-unlock.md`(CF 奖励进家庭池) | 本文件 §2.2(F20 改为个人成员账本) |
+| `docs/function-analysis-by-membership.md`(「任务全完成奖励家庭池」) | 本文件 §2.2(F22 保留个人发放) |
+| `plans/2026-09-01-cf-commission-redesign.md`(`:827 confirmReceive` 家庭池返 CF 不改) | 本文件 §2.2(F02 覆盖所有订单) |
+
+> `cfc-web/public/CHANGELOG*.md` 中的历史条目为**追加式发布记录**,不修改;以时间戳区分新旧口径。