浏览代码

feat(cf): F-1 数据层 + W0.1 配置归口(cf_balance/cf_earn_rule/service_config + 迁移407-412)

数据层(F-1):
- 新建 8 表: cf_balance / cf_balance_log / cf_earn_rule(+24行F02-F24种子) / cf_earn_rule_history / cf_direct_sales_tier / cf_indirect_sales_tier / cf_earn_daily_limit(+N11 20行) / service_config
- coupon / user_coupon 补 family_id(优惠券家庭化 N3)
- DatabaseInitializer 迁移 407-412,全部 try-catch + 存在性判断,可重复执行
- schema.sql 同步建表定义
- 实体 + Mapper(@TableName/@TableId AUTO,MapperScan 覆盖)

配置治理(W0.1):
- service_config 10 项 CF 通用运营参数 + cf.version
- sys_config/products 散落参数迁入 service_config,旧位置标记(不删值,过渡兼容)
- ConfigService.getConfig(本地缓存 TTL 60s → DB → 兜底常量)+ CfConfigCache.invalidate
- 新增 /api/admin/service-config 与 /api/admin/cf-earn-rule 配置接口,save/toggle 挂缓存失效 + 规则变更留痕
- L1 常量类 OrderConstants / AdminConstants(仅 CF 无关技术参数)

不含任何发放/扣减业务逻辑(F-2/F-3 批)
zxyj-web 2 周之前
父节点
当前提交
f8b145d3a
共有 34 个文件被更改,包括 2560 次插入 和 14 次删除
  1. 80 0
      cfc-backend/src/main/java/com/etotem/cfc/config/CfConfigCache.java
  2. 220 0
      cfc-backend/src/main/java/com/etotem/cfc/config/ConfigService.java
  3. 619 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  4. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/constant/AdminConstants.java
  5. 73 0
      cfc-backend/src/main/java/com/etotem/cfc/constant/CfConfigKeys.java
  6. 28 0
      cfc-backend/src/main/java/com/etotem/cfc/constant/OrderConstants.java
  7. 113 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfEarnRuleController.java
  8. 62 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminServiceConfigController.java
  9. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/config/PublicConfigController.java
  10. 8 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java
  11. 50 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfBalance.java
  12. 47 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfBalanceLog.java
  13. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfDirectSalesTier.java
  14. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnDailyLimit.java
  15. 75 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnRule.java
  16. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnRuleHistory.java
  17. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CfIndirectSalesTier.java
  18. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java
  19. 45 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceConfigItem.java
  20. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java
  21. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfBalanceLogMapper.java
  22. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfBalanceMapper.java
  23. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfDirectSalesTierMapper.java
  24. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnDailyLimitMapper.java
  25. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnRuleHistoryMapper.java
  26. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnRuleMapper.java
  27. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CfIndirectSalesTierMapper.java
  28. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceConfigItemMapper.java
  29. 166 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CfEarnRuleService.java
  30. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/service/DimensionEnergySyncService.java
  31. 141 4
      cfc-backend/src/main/resources/schema.sql
  32. 1 0
      docs/superpowers/PROJECT-OVERVIEW.md
  33. 33 1
      docs/superpowers/api/API_REFERENCE.md
  34. 559 0
      docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md

+ 80 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/CfConfigCache.java

@@ -0,0 +1,80 @@
+package com.etotem.cfc.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * CF 配置本地缓存(L2 读取加速)。
+ *
+ * 规则(见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 (3)):
+ * - TTL 60 秒,过期回源 DB;
+ * - 管理端任何 CF 参数/规则改动后必须调用 {@link #invalidate()} 主动失效
+ *   (已挂在 AdminServiceConfigController.save、AdminCfEarnRuleController.save/toggle、
+ *   SysConfigController.update 上);
+ * - 缓存只存 service_config 的字符串原值,类型转换在 ConfigService 内完成。
+ */
+public final class CfConfigCache {
+
+    private static final Logger log = LoggerFactory.getLogger(CfConfigCache.class);
+
+    /** 缓存 TTL:60 秒 */
+    public static final long TTL_MS = 60_000L;
+
+    private static final Map<String, Entry> CACHE = new ConcurrentHashMap<>();
+
+    private CfConfigCache() {
+    }
+
+    /** 取缓存值,未命中/过期返回 null */
+    public static String get(String key) {
+        if (key == null) {
+            return null;
+        }
+        Entry e = CACHE.get(key);
+        if (e == null) {
+            return null;
+        }
+        if (e.expireAt < System.currentTimeMillis()) {
+            CACHE.remove(key, e);
+            return null;
+        }
+        return e.value;
+    }
+
+    /** 写入缓存(TTL 60s) */
+    public static void put(String key, String value) {
+        if (key == null) {
+            return;
+        }
+        CACHE.put(key, new Entry(value, System.currentTimeMillis() + TTL_MS));
+    }
+
+    /** 主动失效单个 key */
+    public static void invalidate(String key) {
+        if (key == null) {
+            return;
+        }
+        CACHE.remove(key);
+        log.info("CfConfigCache 失效 key={}", key);
+    }
+
+    /** 主动失效全部 CF 配置缓存(管理端保存后调用) */
+    public static void invalidate() {
+        int size = CACHE.size();
+        CACHE.clear();
+        log.info("CfConfigCache 全量失效,清理 {} 项", size);
+    }
+
+    private static final class Entry {
+        private final String value;
+        private final long expireAt;
+
+        private Entry(String value, long expireAt) {
+            this.value = value;
+            this.expireAt = expireAt;
+        }
+    }
+}

+ 220 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/ConfigService.java

@@ -0,0 +1,220 @@
+package com.etotem.cfc.config;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.constant.CfConfigKeys;
+import com.etotem.cfc.entity.ServiceConfigItem;
+import com.etotem.cfc.mapper.ServiceConfigItemMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * L2 配置读取封装(CF 通用运营参数唯一读路径)。
+ *
+ * 读取链路:本地缓存({@link CfConfigCache},TTL 60s)→ service_config 表 → Java 兜底默认值
+ * ({@link CfConfigKeys#fallbackOf(String)},仅配置行缺失时生效)。
+ *
+ * 写入链路:管理端 → {@link #setConfig(String, String, Long)} → DB upsert + {@link CfConfigCache#invalidate()}。
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 / §3.7 W0.1
+ */
+@Service
+public class ConfigService {
+
+    private static final Logger log = LoggerFactory.getLogger(ConfigService.class);
+
+    @Resource
+    private ServiceConfigItemMapper serviceConfigItemMapper;
+
+    /**
+     * 读取配置原值:缓存 → DB → 兜底常量。
+     *
+     * @return 配置值;未登记且无兜底的 key 返回 null
+     */
+    public String getConfig(String key) {
+        if (key == null || key.trim().isEmpty()) {
+            return null;
+        }
+        String cached = CfConfigCache.get(key);
+        if (cached != null) {
+            return cached;
+        }
+        String value = null;
+        try {
+            ServiceConfigItem item = serviceConfigItemMapper.selectOne(
+                    new LambdaQueryWrapper<ServiceConfigItem>()
+                            .eq(ServiceConfigItem::getConfigKey, key)
+                            .last("LIMIT 1"));
+            if (item != null) {
+                value = item.getConfigValue();
+            }
+        } catch (Exception e) {
+            log.warn("读取 service_config 失败 key={},回退兜底值:{}", key, e.getMessage());
+        }
+        if (value == null) {
+            value = CfConfigKeys.fallbackOf(key);
+        }
+        if (value != null) {
+            CfConfigCache.put(key, value);
+        }
+        return value;
+    }
+
+    /** 读取配置原值,缺失时返回指定默认值 */
+    public String getConfig(String key, String defaultValue) {
+        String v = getConfig(key);
+        return v != null ? v : defaultValue;
+    }
+
+    /** 读取 int 配置 */
+    public int getIntConfig(String key, int defaultValue) {
+        String v = getConfig(key);
+        if (v == null) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(v.trim());
+        } catch (NumberFormatException e) {
+            log.warn("配置 {} 非整数:{},使用默认值 {}", key, v, defaultValue);
+            return defaultValue;
+        }
+    }
+
+    /** 读取 double 配置 */
+    public double getDoubleConfig(String key, double defaultValue) {
+        String v = getConfig(key);
+        if (v == null) {
+            return defaultValue;
+        }
+        try {
+            return Double.parseDouble(v.trim());
+        } catch (NumberFormatException e) {
+            log.warn("配置 {} 非小数:{},使用默认值 {}", key, v, defaultValue);
+            return defaultValue;
+        }
+    }
+
+    /** 读取 boolean 配置(true/1/on/yes 为真) */
+    public boolean getBooleanConfig(String key, boolean defaultValue) {
+        String v = getConfig(key);
+        if (v == null) {
+            return defaultValue;
+        }
+        String s = v.trim().toLowerCase();
+        if ("true".equals(s) || "1".equals(s) || "on".equals(s) || "yes".equals(s)) {
+            return true;
+        }
+        if ("false".equals(s) || "0".equals(s) || "off".equals(s) || "no".equals(s)) {
+            return false;
+        }
+        return defaultValue;
+    }
+
+    /** 列出配置(groupKey 为空时返回全部) */
+    public List<ServiceConfigItem> listConfigs(String groupKey) {
+        LambdaQueryWrapper<ServiceConfigItem> wrapper = new LambdaQueryWrapper<>();
+        if (groupKey != null && !groupKey.trim().isEmpty()) {
+            wrapper.eq(ServiceConfigItem::getGroupKey, groupKey);
+        }
+        wrapper.orderByAsc(ServiceConfigItem::getGroupKey).orderByAsc(ServiceConfigItem::getConfigKey);
+        return serviceConfigItemMapper.selectList(wrapper);
+    }
+
+    /**
+     * 保存配置(upsert):写库后立即失效本地缓存,并递增 cf.version 供前端感知。
+     *
+     * @param key       配置键(建议 cf.* 前缀)
+     * @param value     配置值(字符串原值)
+     * @param updatedBy 操作人 userId,可为空
+     */
+    public void setConfig(String key, String value, Long updatedBy) {
+        if (key == null || key.trim().isEmpty()) {
+            throw new IllegalArgumentException("config key 不能为空");
+        }
+        ServiceConfigItem item = serviceConfigItemMapper.selectOne(
+                new LambdaQueryWrapper<ServiceConfigItem>()
+                        .eq(ServiceConfigItem::getConfigKey, key)
+                        .last("LIMIT 1"));
+        Date now = new Date();
+        if (item == null) {
+            item = new ServiceConfigItem();
+            item.setConfigKey(key);
+            item.setGroupKey(groupKeyOf(key));
+            item.setConfigValue(value);
+            item.setValueType(guessValueType(value));
+            item.setEditable(1);
+            item.setUpdatedBy(updatedBy);
+            item.setCreatedAt(now);
+            item.setUpdatedAt(now);
+            serviceConfigItemMapper.insert(item);
+        } else {
+            item.setConfigValue(value);
+            if (item.getValueType() == null) {
+                item.setValueType(guessValueType(value));
+            }
+            item.setUpdatedBy(updatedBy);
+            item.setUpdatedAt(now);
+            serviceConfigItemMapper.updateById(item);
+        }
+        bumpVersion(updatedBy);
+        CfConfigCache.invalidate();
+    }
+
+    /** 配置版本号 +1(前端据此局部刷新) */
+    private void bumpVersion(Long updatedBy) {
+        try {
+            ServiceConfigItem version = serviceConfigItemMapper.selectOne(
+                    new LambdaQueryWrapper<ServiceConfigItem>()
+                            .eq(ServiceConfigItem::getConfigKey, CfConfigKeys.CF_VERSION)
+                            .last("LIMIT 1"));
+            if (version == null) {
+                return;
+            }
+            long next = 1L;
+            try {
+                next = Long.parseLong(version.getConfigValue().trim()) + 1;
+            } catch (Exception ignored) {
+                next = 1L;
+            }
+            version.setConfigValue(String.valueOf(next));
+            version.setUpdatedBy(updatedBy);
+            version.setUpdatedAt(new Date());
+            serviceConfigItemMapper.updateById(version);
+        } catch (Exception e) {
+            log.warn("cf.version 递增失败:{}", e.getMessage());
+        }
+    }
+
+    /** cf.rate.points_multiplier → cf.rate */
+    private String groupKeyOf(String key) {
+        int idx = key.lastIndexOf('.');
+        return idx > 0 ? key.substring(0, idx) : "general";
+    }
+
+    private String guessValueType(String value) {
+        if (value == null) {
+            return "string";
+        }
+        String v = value.trim();
+        if ("true".equalsIgnoreCase(v) || "false".equalsIgnoreCase(v)) {
+            return "bool";
+        }
+        try {
+            Integer.parseInt(v);
+            return "int";
+        } catch (NumberFormatException ignored) {
+            // 继续尝试小数
+        }
+        try {
+            Double.parseDouble(v);
+            return "double";
+        } catch (NumberFormatException ignored) {
+            // 非数值
+        }
+        return "string";
+    }
+}

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

@@ -3257,6 +3257,29 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移406: activity_poster_templates 增加 layout_config 列(海报模板元素位置/样式可配置,需求:活动详情海报预览重设计)
         addActivityPosterTemplateLayoutConfigColumn();
+
+        // 迁移407: 创建 cf_balance / cf_balance_log(个人 CF 值账本,成员维度;F-1)
+        createCfBalanceTables();
+
+        // 迁移408: 创建 cf_earn_rule / cf_earn_rule_history + 24 行规则种子(F02–F24;F-1)
+        createCfEarnRuleTables();
+        seedCfEarnRules();
+
+        // 迁移409: 创建 cf_direct_sales_tier / cf_indirect_sales_tier(两级销售奖档位;F-1)
+        createCfSalesTierTables();
+
+        // 迁移410: 创建 cf_earn_daily_limit + 会员等级日限种子(N11;F-1)
+        createCfEarnDailyLimitTable();
+        seedCfEarnDailyLimits();
+
+        // 迁移411: coupon / user_coupon 补 family_id(优惠券家庭化 N3;F-1)
+        addCouponFamilyIdColumns();
+
+        // 迁移412: 创建 service_config + 散落参数迁入 + CF 通用运营参数种子(W0.1)
+        // 顺序:先迁旧位置实际值,再补种子默认值(避免默认值覆盖 sys_config 里的真实配置)
+        createServiceConfigTable();
+        migrateScatteredCfConfigs();
+        seedCfServiceConfig();
     }
 
     private void migrateHealthDimensionsTable() {
@@ -13106,4 +13129,600 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.warn("迁移406: 添加 activity_poster_templates.layout_config 列失败: {}", e.getMessage());
         }
     }
+
+    // ==================================================================================
+    // F-1 数据层(CF 值体系统一):cf_balance / cf_balance_log / cf_earn_rule /
+    // cf_earn_rule_history / cf_direct_sales_tier / cf_indirect_sales_tier / cf_earn_daily_limit
+    // 口径:docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §五 + §3.6
+    // 仅建表 + 种子,不含任何发放/扣减业务逻辑(F-2 批)
+    // ==================================================================================
+
+    // 迁移407: 创建 cf_balance / cf_balance_log(个人 CF 值账本,成员维度)
+    private void createCfBalanceTables() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_balance` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`member_id` BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)', "
+                    + "`user_id` BIGINT NULL COMMENT '关联登录用户ID(可为空,不能登录成员无账号)', "
+                    + "`family_id` BIGINT NULL COMMENT '所属家庭ID(冗余,便于聚合)', "
+                    + "`total_earned` INT NOT NULL DEFAULT 0 COMMENT '累计获得', "
+                    + "`available` INT NOT NULL DEFAULT 0 COMMENT '可用', "
+                    + "`frozen` INT NOT NULL DEFAULT 0 COMMENT '提现冻结', "
+                    + "`withdrawn` INT NOT NULL DEFAULT 0 COMMENT '已提现', "
+                    + "`exchanged` INT NOT NULL DEFAULT 0 COMMENT '已兑换消费', "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "UNIQUE KEY `uk_member` (`member_id`), "
+                    + "KEY `idx_family` (`family_id`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值余额(家庭成员维度)'");
+            log.info("迁移407: 已创建 cf_balance 表");
+        } catch (Exception e) {
+            log.warn("迁移407: 创建 cf_balance 表失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_balance_log` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`member_id` BIGINT NOT NULL, "
+                    + "`user_id` BIGINT NULL, "
+                    + "`family_id` BIGINT NULL, "
+                    + "`type` VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust', "
+                    + "`ref_type` VARCHAR(50) NOT NULL COMMENT '规则编码或迁移标识(F02-F24 / migration_*)', "
+                    + "`ref_id` BIGINT NULL, "
+                    + "`amount` INT NOT NULL COMMENT '变动数量(正负)', "
+                    + "`balance_after` INT NOT NULL COMMENT '变动后可用余额', "
+                    + "`remark` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "KEY `idx_member` (`member_id`), "
+                    + "KEY `idx_family` (`family_id`), "
+                    + "UNIQUE KEY `uk_ref` (`member_id`, `ref_type`, `ref_id`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值流水'");
+            log.info("迁移407: 已创建 cf_balance_log 表");
+        } catch (Exception e) {
+            log.warn("迁移407: 创建 cf_balance_log 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    // 迁移408: 创建 cf_earn_rule / cf_earn_rule_history(CF 值获取规则 + 变更审计)
+    private void createCfEarnRuleTables() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_rule` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`code` VARCHAR(20) NOT NULL COMMENT '规则编码 F02~F24(F03 拆 F03a/F03b)', "
+                    + "`name` VARCHAR(100) NOT NULL COMMENT '途径名称', "
+                    + "`category` VARCHAR(20) NOT NULL COMMENT 'consume消费返佣/behavior成长行为/admin管理员', "
+                    + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用(随时开关)', "
+                    + "`rule_type` VARCHAR(20) NOT NULL COMMENT 'fixed固定/formula公式/per_item逐人发放', "
+                    + "`amount` INT NULL COMMENT '固定数量(rule_type=fixed)', "
+                    + "`formula` VARCHAR(500) NULL COMMENT '计算公式(rule_type=formula)', "
+                    + "`params_json` TEXT NULL COMMENT '公式参数(bps/倍数/档位表等,JSON)', "
+                    + "`daily_limit_per_member` INT NULL COMMENT '单成员每日上限,空=不限', "
+                    + "`total_limit_per_member` INT NULL COMMENT '单成员累计上限,空=不限', "
+                    + "`cooldown_seconds` INT NULL COMMENT '同一成员冷却秒数,空=不限', "
+                    + "`dedup_ref_type` VARCHAR(50) NOT NULL COMMENT '幂等refType,写入cf_balance_log', "
+                    + "`effective_from` DATETIME NULL COMMENT '生效起,空=立即', "
+                    + "`effective_to` DATETIME NULL COMMENT '生效止,空=长期', "
+                    + "`sort_order` INT NOT NULL DEFAULT 0, "
+                    + "`remark` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "`updated_by` BIGINT NULL COMMENT '最后修改人', "
+                    + "UNIQUE KEY `uk_code` (`code`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值获取规则(运营可配置)'");
+            log.info("迁移408: 已创建 cf_earn_rule 表");
+        } catch (Exception e) {
+            log.warn("迁移408: 创建 cf_earn_rule 表失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_rule_history` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`rule_code` VARCHAR(20) NOT NULL, "
+                    + "`old_json` TEXT NULL COMMENT '变更前整行快照', "
+                    + "`new_json` TEXT NULL COMMENT '变更后整行快照', "
+                    + "`changed_by` BIGINT NOT NULL, "
+                    + "`change_reason` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "KEY `idx_rule` (`rule_code`, `created_at`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则变更历史'");
+            log.info("迁移408: 已创建 cf_earn_rule_history 表");
+        } catch (Exception e) {
+            log.warn("迁移408: 创建 cf_earn_rule_history 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 24 行规则种子(F02–F24,F03 拆 F03a/F03b),列序:
+     * code, name, category, ruleType, amount, formula, paramsJson, dailyLimit, totalLimit,
+     * cooldownSeconds, dedupRefType, sortOrder, remark
+     *
+     * 数值口径:§三 各条「参数来源」+ §九 修正后数值(F06=1990、F18 与 F02 同口径)。
+     * 注意:§三 公式写作 floor(实付(分)/100)×100,与 §九「19.9 元=1990 CF」不自洽;
+     * 按 §九 修正口径执行 —— 1 元=100 CF 即 实付(分)→CF 1:1,公式记作 floor(amt)。
+     */
+    private static final String[][] CF_EARN_RULE_SEEDS = {
+            {"F02", "消费返CF(所有订单)", "consume", "formula", "",
+                    "floor(amt)",
+                    "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"note\":\"1元=100CF,实付分→CF 1:1\"}",
+                    "", "", "", "cf_consume_order", "10",
+                    "所有消费订单(商品/套餐/测评/会员/订阅/调研)均返"},
+            {"F03a", "直接销售奖", "consume", "formula", "",
+                    "floor(base_ppoints * rate_percent / 100)",
+                    "{\"base\":\"team_annual_ppoints\",\"tier_table\":\"cf_direct_sales_tier\",\"window\":\"1y_rolling\",\"team_depth\":8,\"max_rate_percent\":70,\"downgrade\":true}",
+                    "", "", "", "cf_direct_sales", "20",
+                    "基数=直接推荐人消费P点;档位按团队8层内年度累计P点(cf_direct_sales_tier)"},
+            {"F03b", "间接销售奖", "consume", "formula", "",
+                    "floor(base_ppoints * rate_percent / 100)",
+                    "{\"base\":\"self_annual_ppoints\",\"tier_table\":\"cf_indirect_sales_tier\",\"window\":\"1y_rolling\",\"max_rate_percent\":20,\"downgrade\":true}",
+                    "", "", "", "cf_indirect_sales", "30",
+                    "基数=间接推荐人消费P点;档位按自身年度累计消费P点(cf_indirect_sales_tier)"},
+            {"F04", "商品赠品CF", "consume", "per_item", "",
+                    "",
+                    "{\"source\":\"product_order_gift.cf_value\"}",
+                    "", "", "", "product_order_gift", "40",
+                    "赠品规则管理端配置"},
+            {"F05", "活动签到CF", "consume", "per_item", "",
+                    "",
+                    "{\"source\":\"activities.platform_points\"}",
+                    "", "", "", "activity_checkin", "50",
+                    "活动配置数量"},
+            {"F06", "菌群健康调研打卡返还", "consume", "formula", "",
+                    "floor(amt)",
+                    "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"fallback\":\"gut_health_project.cf_refund_amount\",\"example\":\"19.9元=1990CF\"}",
+                    "7", "1", "", "gut_health_refund", "60",
+                    "cf_refund_amount 为空时按项目价×100(19.9元=1990CF,§九修正值);一周项目只发一次"},
+            {"F07", "文章答题奖励", "behavior", "formula", "",
+                    "score + (all_correct ? 3 : 0)",
+                    "{\"bonus_all_correct\":3,\"dedup\":\"article\"}",
+                    "5", "", "", "article_answer", "70",
+                    "答对题数+全对3;日限5次(N9)"},
+            {"F08", "AI阅读答题奖励", "behavior", "fixed", "5",
+                    "",
+                    "{\"require_all_correct\":true}",
+                    "3", "", "", "ai_reading_answer", "80",
+                    "仅全部答对才发(原硬编码已外置)"},
+            {"F09", "完成测评建档", "behavior", "formula", "",
+                    "first_report ? 50 : 10",
+                    "{\"first_amount\":50,\"repeat_amount\":10,\"dedup_key\":\"report_hash\"}",
+                    "3", "", "", "assessment_report", "90",
+                    "第1份50/后续每份10,重复报告不计(N10)"},
+            {"F10", "家庭挑战完成", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"family_challenge.reward_points\",\"pending\":\"Q1 家庭合计 or 按人各发\"}",
+                    "1", "1", "", "family_challenge", "100",
+                    "按挑战配置数量(按人各发 or 家庭合计待 Q1 确认)"},
+            {"F11", "邀请里程碑", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"invite_milestone.reward_points\"}",
+                    "", "", "", "invite_milestone", "110",
+                    "按里程碑配置"},
+            {"F12", "成长任务", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"growth_task.reward_points\"}",
+                    "", "", "", "growth_task", "120",
+                    "按任务模板配置"},
+            {"F13", "分享阅读奖励", "behavior", "fixed", "5",
+                    "",
+                    "{\"source\":\"cf.rate.share_read_reward_points\"}",
+                    "5", "", "", "share_read", "130",
+                    "默认5CF;日限5次"},
+            {"F14", "喝水打卡", "behavior", "fixed", "5",
+                    "",
+                    "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
+                    "", "", "", "health_water", "140",
+                    "5CF/次;日限按会员等级(N11)"},
+            {"F15", "饮食打卡", "behavior", "fixed", "5",
+                    "",
+                    "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
+                    "", "", "", "health_meal", "150",
+                    "5CF/次;日限按会员等级(N11)"},
+            {"F16", "每日健康打卡", "behavior", "fixed", "10",
+                    "",
+                    "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
+                    "", "", "", "daily_health_checkin", "160",
+                    "10CF/次;日限按会员等级(N11)"},
+            {"F17", "挑战赛子任务完成", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"challenge_task.reward_cf\"}",
+                    "", "", "", "challenge_sub_task", "170",
+                    "按挑战赛任务配置"},
+            {"F18", "健康启航计划付费", "consume", "formula", "",
+                    "floor(amt)",
+                    "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"note\":\"与F02同口径\"}",
+                    "1", "1", "", "health_start_plan", "180",
+                    "实付分→CF 1:1(§九修正口径,原硬编码已外置)"},
+            {"F19", "管理员手动调整", "admin", "per_item", "",
+                    "",
+                    "{\"require_parent_password\":true,\"allow_negative\":true}",
+                    "", "", "", "admin_adjust", "190",
+                    "正负任意,需家长密码校验,必须留痕(Q9)"},
+            {"F20", "问题域全部任务完成", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"problem_domain.reward_cf_value\"}",
+                    "", "", "", "problem_domain", "200",
+                    "按管理端配置"},
+            {"F21", "关卡通关奖励", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"unlock_gate.reward_points\"}",
+                    "", "", "", "unlock_gate", "210",
+                    "按管理端配置"},
+            {"F22", "任务完成CF", "behavior", "per_item", "",
+                    "",
+                    "{\"source\":\"task.cf_amount\",\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
+                    "", "", "", "task_complete", "220",
+                    "保留个人发放,取消家庭池那条;日限按会员等级(N11)"},
+            {"F23", "成员间转让(收款方)", "behavior", "per_item", "",
+                    "",
+                    "{\"direction\":\"transfer_in\",\"pair_type\":\"transfer_out\"}",
+                    "", "", "", "cf_transfer", "230",
+                    "转入+X / 转出−X,同一事务"},
+            {"F24", "分享奖励(推荐注册)", "behavior", "fixed", "20",
+                    "",
+                    "{\"trigger\":\"referral_register\"}",
+                    "10", "", "", "referral_register", "240",
+                    "N8 新增:推荐他人注册即得,建议20CF/人,日限10次"}
+    };
+
+    /** 幂等写入 24 行规则种子(已存在则跳过,不覆盖运营改过的配置) */
+    private void seedCfEarnRules() {
+        int inserted = 0;
+        for (String[] r : CF_EARN_RULE_SEEDS) {
+            try {
+                Integer cnt = jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM cf_earn_rule WHERE code = ?", Integer.class, r[0]);
+                if (cnt != null && cnt > 0) {
+                    continue;
+                }
+                jdbcTemplate.update("INSERT INTO cf_earn_rule (code, name, category, enabled, rule_type, amount, "
+                                + "formula, params_json, daily_limit_per_member, total_limit_per_member, cooldown_seconds, "
+                                + "dedup_ref_type, sort_order, remark, created_at, updated_at) "
+                                + "VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())",
+                        r[0], r[1], r[2], r[3], cfIntOrNull(r[4]), cfTextOrNull(r[5]), cfTextOrNull(r[6]),
+                        cfIntOrNull(r[7]), cfIntOrNull(r[8]), cfIntOrNull(r[9]), r[10],
+                        cfIntOrNull(r[11]) != null ? cfIntOrNull(r[11]) : 0, cfTextOrNull(r[12]));
+                inserted++;
+            } catch (Exception e) {
+                log.warn("迁移408: 写入规则种子 {} 失败: {}", r[0], e.getMessage());
+            }
+        }
+        if (inserted > 0) {
+            log.info("迁移408: cf_earn_rule 规则种子新增 {} 行(共 {} 条定义)", inserted, CF_EARN_RULE_SEEDS.length);
+        }
+    }
+
+    // 迁移409: 创建 cf_direct_sales_tier / cf_indirect_sales_tier(两级销售奖档位,数值由运营配置)
+    private void createCfSalesTierTables() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_direct_sales_tier` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`tier_name` VARCHAR(50) NOT NULL COMMENT '档位名称', "
+                    + "`min_annual_ppoints` INT NOT NULL COMMENT '团队8层内年度累计P点下限', "
+                    + "`rate_percent` INT NOT NULL COMMENT '分润比例(%)', "
+                    + "`sort_order` INT NOT NULL DEFAULT 0, "
+                    + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', "
+                    + "`remark` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "UNIQUE KEY `uk_min` (`min_annual_ppoints`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直接销售奖档位(按团队8层内年度P点累计)'");
+            log.info("迁移409: 已创建 cf_direct_sales_tier 表");
+        } catch (Exception e) {
+            log.warn("迁移409: 创建 cf_direct_sales_tier 表失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_indirect_sales_tier` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`tier_name` VARCHAR(50) NOT NULL, "
+                    + "`min_self_ppoints` INT NOT NULL COMMENT '自身年度累计消费P点下限', "
+                    + "`rate_percent` INT NOT NULL COMMENT '分润比例(%)', "
+                    + "`sort_order` INT NOT NULL DEFAULT 0, "
+                    + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', "
+                    + "`remark` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "UNIQUE KEY `uk_min` (`min_self_ppoints`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='间接销售奖档位(按自身年度消费P点累计)'");
+            log.info("迁移409: 已创建 cf_indirect_sales_tier 表");
+        } catch (Exception e) {
+            log.warn("迁移409: 创建 cf_indirect_sales_tier 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    // 迁移410: 创建 cf_earn_daily_limit(规则 × 会员等级 每日次数上限,N11)
+    private void createCfEarnDailyLimitTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_daily_limit` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`rule_code` VARCHAR(20) NOT NULL COMMENT '规则编码(cf_earn_rule.code)', "
+                    + "`level_code` VARCHAR(32) NOT NULL COMMENT '会员等级编码 FREE/BASIC/FAMILY/LIFETIME/PREMIUM', "
+                    + "`daily_limit` INT NOT NULL COMMENT '每日次数上限', "
+                    + "`remark` VARCHAR(255) NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "UNIQUE KEY `uk_rule_level` (`rule_code`, `level_code`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则×会员等级每日次数上限(N11)'");
+            log.info("迁移410: 已创建 cf_earn_daily_limit 表");
+        } catch (Exception e) {
+            log.warn("迁移410: 创建 cf_earn_daily_limit 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    /** N11 会员等级日限:FREE=1(建议) / BASIC=3 / FAMILY=10 / LIFETIME=10 / PREMIUM=20,PROVIDER 待定不落行 */
+    private static final String[][] CF_DAILY_LIMIT_LEVELS = {
+            {"FREE", "1", "免费会员(建议值,Q11)"},
+            {"BASIC", "3", "9.9体验会员"},
+            {"FAMILY", "10", "家庭会员"},
+            {"LIFETIME", "10", "终身会员"},
+            {"PREMIUM", "20", "高级会员"}
+    };
+
+    /** 按会员等级限次的规则(任务/打卡类):F14 喝水 / F15 饮食 / F16 每日健康 / F22 任务完成 */
+    private static final String[] CF_DAILY_LIMIT_RULES = {"F14", "F15", "F16", "F22"};
+
+    private void seedCfEarnDailyLimits() {
+        int inserted = 0;
+        for (String ruleCode : CF_DAILY_LIMIT_RULES) {
+            for (String[] lv : CF_DAILY_LIMIT_LEVELS) {
+                try {
+                    Integer cnt = jdbcTemplate.queryForObject(
+                            "SELECT COUNT(*) FROM cf_earn_daily_limit WHERE rule_code = ? AND level_code = ?",
+                            Integer.class, ruleCode, lv[0]);
+                    if (cnt != null && cnt > 0) {
+                        continue;
+                    }
+                    jdbcTemplate.update("INSERT INTO cf_earn_daily_limit (rule_code, level_code, daily_limit, remark, created_at, updated_at) "
+                            + "VALUES (?, ?, ?, ?, NOW(), NOW())", ruleCode, lv[0], Integer.valueOf(lv[1]), lv[2]);
+                    inserted++;
+                } catch (Exception e) {
+                    log.warn("迁移410: 写入日限种子 {}/{} 失败: {}", ruleCode, lv[0], e.getMessage());
+                }
+            }
+        }
+        if (inserted > 0) {
+            log.info("迁移410: cf_earn_daily_limit 日限种子新增 {} 行", inserted);
+        }
+    }
+
+    // 迁移411: coupon / user_coupon 补 family_id(优惠券家庭化 N3)
+    private void addCouponFamilyIdColumns() {
+        if (!tableExists("coupon") || !tableExists("user_coupon")) {
+            log.warn("迁移411: coupon / user_coupon 表不存在,跳过 family_id 迁移");
+            return;
+        }
+        ensureColumn("coupon", "family_id", "BIGINT DEFAULT NULL COMMENT '模板块绑定家庭ID(NULL=全平台模板)'");
+        ensureColumn("user_coupon", "family_id", "BIGINT DEFAULT NULL COMMENT '券实例归属家庭ID(家庭内任意成员可用)'");
+        try {
+            if (!indexExists("user_coupon", "idx_family")) {
+                jdbcTemplate.execute("ALTER TABLE user_coupon ADD INDEX idx_family (family_id)");
+                log.info("迁移411: 已添加 user_coupon.idx_family 索引");
+            }
+        } catch (Exception e) {
+            log.warn("迁移411: 添加 user_coupon.idx_family 失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            if (!indexExists("coupon", "idx_family")) {
+                jdbcTemplate.execute("ALTER TABLE coupon ADD INDEX idx_family (family_id)");
+                log.info("迁移411: 已添加 coupon.idx_family 索引");
+            }
+        } catch (Exception e) {
+            log.warn("迁移411: 添加 coupon.idx_family 失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    // 迁移412: 创建 service_config(L2 配置中心;W0.1)
+    private void createServiceConfigTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `service_config` ("
+                    + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "`group_key` VARCHAR(64) NOT NULL DEFAULT 'general' COMMENT '配置分组(如 cf.rate / cf.commission)', "
+                    + "`config_key` VARCHAR(128) NOT NULL COMMENT '配置键(如 cf.rate.points_multiplier)', "
+                    + "`config_value` TEXT NULL COMMENT '配置值(统一字符串存储,按 value_type 解析)', "
+                    + "`value_type` VARCHAR(16) NOT NULL DEFAULT 'string' COMMENT 'string/int/double/bool/json', "
+                    + "`description` VARCHAR(255) NULL, "
+                    + "`editable` TINYINT NOT NULL DEFAULT 1 COMMENT '1允许后台编辑 0系统托管', "
+                    + "`updated_by` BIGINT NULL, "
+                    + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+                    + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
+                    + "UNIQUE KEY `uk_config_key` (`config_key`), "
+                    + "KEY `idx_group_key` (`group_key`) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务配置中心(L2,CF通用运营参数归口)'");
+            log.info("迁移412: 已创建 service_config 表");
+        } catch (Exception e) {
+            log.warn("迁移412: 创建 service_config 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
+    /** W0.1 §3.6.6(2) 10 项 CF 通用运营参数 + cf.version 版本号 */
+    private void seedCfServiceConfig() {
+        ensureServiceConfig("cf.rate.points_multiplier", "1.0", "double",
+                "积分倍数(消费→CF 换算);原散落 products.points_multiplier / sys_config", 1);
+        ensureServiceConfig("cf.rate.repeat_rate_default", "1.0", "double",
+                "复购率默认系数(未匹配规则时不加权)", 1);
+        ensureServiceConfig("cf.commission.settle_days", "7", "int",
+                "佣金结算周期(天),财务口径常用", 1);
+        ensureServiceConfig("cf.invite.valid_days", "7", "int",
+                "邀请码有效期(天)", 1);
+        ensureServiceConfig("cf.price.default_factor", "1.0", "double",
+                "默认价格系数(未定价时原价)", 1);
+        ensureServiceConfig("cf.stock.deduction_mode", "immediate", "string",
+                "库存扣减策略 immediate/on_pay(Phase 4 定稿)", 1);
+        ensureServiceConfig("cf.rate.share_read_reward_points", "5", "int",
+                "分享阅读奖励 CF;原散落 sys_config.share_read_reward_points", 1);
+        ensureServiceConfig("cf.commission.service_rate", "1000", "int",
+                "佣金服务费率(bps);原散落 sys_config.commission_service_rate", 1);
+        ensureServiceConfig("cf.product.gut_health_price", "1990", "int",
+                "肠道健康项目定价(分);原散落 GutHealthProjectService.DEFAULT_PRICE / sys_config", 1);
+        ensureServiceConfig("cf.product.platform_points_share", "1000", "int",
+                "产品平台积分分成比例(bps);原散落 sys_config.product_platform_points_share", 1);
+        ensureServiceConfig("cf.version", "1", "int",
+                "CF 配置版本号(单调递增,前端据此局部刷新提示)", 0);
+    }
+
+    /**
+     * 迁移散落在 sys_config / products 的 CF 参数到 service_config:
+     * 优先取旧位置的实际值(存在则沿用),旧位置仅做标记清理,不删值(F-2/F-3 收口前保持兼容)。
+     * 幂等:新 key 已存在则跳过;旧位置标记带唯一前缀,重复执行不重复追加。
+     */
+    private void migrateScatteredCfConfigs() {
+        migrateSysConfigToServiceConfig("commission_service_rate", "cf.commission.service_rate", "1000");
+        migrateSysConfigToServiceConfig("product_platform_points_share", "cf.product.platform_points_share", "1000");
+        migrateSysConfigToServiceConfig("share_read_reward_points", "cf.rate.share_read_reward_points", "5");
+        migrateSysConfigToServiceConfig("gut_health_project_price", "cf.product.gut_health_price", "1990");
+
+        // products.points_multiplier(列)→ cf.rate.points_multiplier,并标记列注释
+        try {
+            if (!serviceConfigExists("cf.rate.points_multiplier")) {
+                String val = null;
+                try {
+                    // 仅当所有商品倍数一致时才沿用该值,否则回落到 1.0(避免把某个商品的特殊倍数当成全局口径)
+                    Integer distinct = jdbcTemplate.queryForObject(
+                            "SELECT COUNT(DISTINCT points_multiplier) FROM products WHERE points_multiplier IS NOT NULL",
+                            Integer.class);
+                    if (distinct != null && distinct == 1) {
+                        val = jdbcTemplate.queryForObject(
+                                "SELECT CAST(points_multiplier AS CHAR) FROM products WHERE points_multiplier IS NOT NULL LIMIT 1",
+                                String.class);
+                    }
+                } catch (Exception ignored) {
+                    // 列不存在或表为空
+                }
+                ensureServiceConfig("cf.rate.points_multiplier", val != null ? val : "1.0", "double",
+                        "积分倍数(消费→CF 换算);原散落 products.points_multiplier", 1);
+            }
+            markProductsPointsMultiplierMigrated();
+        } catch (Exception e) {
+            log.warn("迁移412: products.points_multiplier 迁移失败: {}", e.getMessage());
+        }
+    }
+
+    private void migrateSysConfigToServiceConfig(String oldKey, String newKey, String defaultVal) {
+        try {
+            if (serviceConfigExists(newKey)) {
+                markSysConfigMigrated(oldKey, newKey);
+                return;
+            }
+            String val = null;
+            try {
+                val = jdbcTemplate.queryForObject(
+                        "SELECT config_value FROM sys_config WHERE config_key = ? LIMIT 1", String.class, oldKey);
+            } catch (Exception ignored) {
+                // 旧配置不存在,使用默认值
+            }
+            ensureServiceConfig(newKey, val != null && !val.trim().isEmpty() ? val : defaultVal,
+                    cfValueTypeOf(defaultVal), "由 sys_config." + oldKey + " 迁入(W0.1)", 1);
+            markSysConfigMigrated(oldKey, newKey);
+        } catch (Exception e) {
+            log.warn("迁移412: {} → {} 迁移失败: {}", oldKey, newKey, e.getMessage());
+        }
+    }
+
+    /** 旧 sys_config 行标记为已迁移(保留值做过渡兼容,不物理删除) */
+    private void markSysConfigMigrated(String oldKey, String newKey) {
+        try {
+            int updated = jdbcTemplate.update(
+                    "UPDATE sys_config SET description = CONCAT(COALESCE(description, ''), ?) "
+                            + "WHERE config_key = ? AND (description IS NULL OR description NOT LIKE '%【已迁移→service_config%')",
+                    " 【已迁移→service_config " + newKey + ",本行仅过渡期兼容】", oldKey);
+            if (updated > 0) {
+                log.info("迁移412: 已标记 sys_config.{} 迁往 service_config.{}", oldKey, newKey);
+            }
+        } catch (Exception e) {
+            log.warn("迁移412: 标记 sys_config.{} 失败: {}", oldKey, e.getMessage());
+        }
+    }
+
+    /** products.points_multiplier 列注释标记(列保留兼容,不再作为权威来源) */
+    private void markProductsPointsMultiplierMigrated() {
+        try {
+            String comment = jdbcTemplate.queryForObject(
+                    "SELECT COLUMN_COMMENT FROM information_schema.COLUMNS "
+                            + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'points_multiplier'",
+                    String.class);
+            if (comment != null && comment.contains("已迁移")) {
+                return;
+            }
+            jdbcTemplate.execute("ALTER TABLE products MODIFY COLUMN points_multiplier DECIMAL(10,2) NOT NULL DEFAULT 1.00 "
+                    + "COMMENT '积分倍数【已迁移→service_config cf.rate.points_multiplier,本列仅过渡期兼容】'");
+            log.info("迁移412: 已标记 products.points_multiplier 迁往 service_config.cf.rate.points_multiplier");
+        } catch (Exception e) {
+            log.warn("迁移412: 标记 products.points_multiplier 失败: {}", e.getMessage());
+        }
+    }
+
+    private boolean serviceConfigExists(String key) {
+        try {
+            Integer cnt = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM service_config WHERE config_key = ?", Integer.class, key);
+            return cnt != null && cnt > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /** 幂等写入 service_config(已存在则跳过,不覆盖运营改过的值) */
+    private void ensureServiceConfig(String key, String value, String valueType, String description, int editable) {
+        try {
+            if (serviceConfigExists(key)) {
+                return;
+            }
+            jdbcTemplate.update("INSERT INTO service_config (group_key, config_key, config_value, value_type, description, editable, created_at, updated_at) "
+                            + "VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())",
+                    groupKeyOfConfig(key), key, value, valueType, description, editable);
+        } catch (Exception e) {
+            log.warn("迁移412: 写入 service_config.{} 失败: {}", key, e.getMessage());
+        }
+    }
+
+    /** cf.rate.points_multiplier → cf.rate */
+    private String groupKeyOfConfig(String key) {
+        if (key == null) {
+            return "general";
+        }
+        int idx = key.lastIndexOf('.');
+        return idx > 0 ? key.substring(0, idx) : "general";
+    }
+
+    private String cfValueTypeOf(String value) {
+        if (value == null) {
+            return "string";
+        }
+        try {
+            Integer.parseInt(value.trim());
+            return "int";
+        } catch (NumberFormatException ignored) {
+            // 非整数
+        }
+        try {
+            Double.parseDouble(value.trim());
+            return "double";
+        } catch (NumberFormatException ignored) {
+            // 非数值
+        }
+        return "string";
+    }
+
+    private Integer cfIntOrNull(String v) {
+        if (v == null || v.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(v.trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private String cfTextOrNull(String v) {
+        return (v == null || v.trim().isEmpty()) ? null : v;
+    }
+
+    private boolean indexExists(String table, String indexName) {
+        try {
+            Integer cnt = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() "
+                            + "AND TABLE_NAME = ? AND INDEX_NAME = ?",
+                    Integer.class, table, indexName);
+            return cnt != null && cnt > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
 }

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/constant/AdminConstants.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.constant;
+
+/**
+ * L1 硬编码常量:后台请求超时等技术参数(与 CF 值算法无关)。
+ *
+ * 依据:docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 (4)。
+ */
+public final class AdminConstants {
+
+    private AdminConstants() {
+    }
+
+    /** 后台 API 请求超时(毫秒) */
+    public static final int ADMIN_API_TIMEOUT_MS = 5000;
+
+    /** 前端请求超时(毫秒,供管理端/小程序对齐) */
+    public static final int FRONTEND_REQUEST_TIMEOUT_MS = 15000;
+}

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/constant/CfConfigKeys.java

@@ -0,0 +1,73 @@
+package com.etotem.cfc.constant;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * CF 值相关 L2 配置键(service_config.config_key)。
+ *
+ * 说明:本类只放 **键名 + 引导期兜底默认值**,不放业务数值的"权威值"。
+ * 权威值一律在 `service_config` 表,由管理端 `/api/admin/service-config/save` 维护,
+ * 通过 {@code ConfigService.getConfig(key)} 读取(本地缓存 TTL 60s)。
+ * 兜底默认值仅在配置行缺失(如新库尚未 seed、读取异常)时生效,禁止在业务代码中另写常量。
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 / W0.1
+ */
+public final class CfConfigKeys {
+
+    private CfConfigKeys() {
+    }
+
+    /** 积分倍数(消费 → CF 换算) */
+    public static final String RATE_POINTS_MULTIPLIER = "cf.rate.points_multiplier";
+    /** 复购率默认系数(未匹配规则时不加权) */
+    public static final String RATE_REPEAT_RATE_DEFAULT = "cf.rate.repeat_rate_default";
+    /** 佣金结算周期(天) */
+    public static final String COMMISSION_SETTLE_DAYS = "cf.commission.settle_days";
+    /** 邀请码有效期(天) */
+    public static final String INVITE_VALID_DAYS = "cf.invite.valid_days";
+    /** 默认价格系数(未定价时原价) */
+    public static final String PRICE_DEFAULT_FACTOR = "cf.price.default_factor";
+    /** 库存扣减策略 immediate / on_pay */
+    public static final String STOCK_DEDUCTION_MODE = "cf.stock.deduction_mode";
+    /** 分享阅读奖励 CF */
+    public static final String RATE_SHARE_READ_REWARD_POINTS = "cf.rate.share_read_reward_points";
+    /** 佣金服务费率(bps) */
+    public static final String COMMISSION_SERVICE_RATE = "cf.commission.service_rate";
+    /** 肠道健康项目定价(分) */
+    public static final String PRODUCT_GUT_HEALTH_PRICE = "cf.product.gut_health_price";
+    /** 产品平台积分分成比例(bps) */
+    public static final String PRODUCT_PLATFORM_POINTS_SHARE = "cf.product.platform_points_share";
+    /** 配置版本号(单调递增,前端据此做局部刷新提示) */
+    public static final String CF_VERSION = "cf.version";
+
+    /** 引导期兜底默认值(配置行缺失时使用,非权威值) */
+    private static final Map<String, String> FALLBACKS;
+
+    static {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put(RATE_POINTS_MULTIPLIER, "1.0");
+        m.put(RATE_REPEAT_RATE_DEFAULT, "1.0");
+        m.put(COMMISSION_SETTLE_DAYS, "7");
+        m.put(INVITE_VALID_DAYS, "7");
+        m.put(PRICE_DEFAULT_FACTOR, "1.0");
+        m.put(STOCK_DEDUCTION_MODE, "immediate");
+        m.put(RATE_SHARE_READ_REWARD_POINTS, "5");
+        m.put(COMMISSION_SERVICE_RATE, "1000");
+        m.put(PRODUCT_GUT_HEALTH_PRICE, "1990");
+        m.put(PRODUCT_PLATFORM_POINTS_SHARE, "1000");
+        m.put(CF_VERSION, "1");
+        FALLBACKS = Collections.unmodifiableMap(m);
+    }
+
+    /** 引导期兜底默认值;未知 key 返回 null */
+    public static String fallbackOf(String key) {
+        return key == null ? null : FALLBACKS.get(key);
+    }
+
+    /** 是否为本方案登记的 CF 配置键 */
+    public static boolean isCfKey(String key) {
+        return key != null && key.startsWith("cf.");
+    }
+}

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/constant/OrderConstants.java

@@ -0,0 +1,28 @@
+package com.etotem.cfc.constant;
+
+/**
+ * L1 硬编码常量:订单/锁/超时等 **与 CF 值算法无关** 的纯技术参数。
+ *
+ * 依据:docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 (4)。
+ * CF 业务/算法参数(比例、档位、天数、系数、价格)禁止放这里,一律 L2(service_config / cf_earn_rule)。
+ */
+public final class OrderConstants {
+
+    private OrderConstants() {
+    }
+
+    /** 订单超时自动取消(分钟) */
+    public static final int ORDER_TIMEOUT_MIN = 30;
+
+    /** 库存锁时间窗口(毫秒,30 分钟) */
+    public static final long STOCK_LOCK_WINDOW_MS = 30L * 60 * 1000;
+
+    /** 库存锁 TTL(秒,20 分钟) */
+    public static final int STOCK_LOCK_TTL_SEC = 20 * 60;
+
+    /** 库存锁持有超时告警阈值(分钟) */
+    public static final int STOCK_LOCK_WARN_AFTER_MIN = 20;
+
+    /** 订单状态刷新 TTL(秒) */
+    public static final int ORDER_STATUS_REFRESH_SEC = 60;
+}

+ 113 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfEarnRuleController.java

@@ -0,0 +1,113 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.CfEarnRule;
+import com.etotem.cfc.entity.CfEarnRuleHistory;
+import com.etotem.cfc.service.CfEarnRuleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CF 值获取规则管理端接口(L2 规则配置)。
+ *
+ * 仅配置读写与留痕,不涉及任何发放计算。
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.4
+ */
+@Tag(name = "管理端-CF值获取规则")
+@RestController
+@RequestMapping("/api/admin/cf-earn-rule")
+public class AdminCfEarnRuleController {
+
+    @Resource
+    private CfEarnRuleService cfEarnRuleService;
+
+    @Operation(summary = "规则列表")
+    @PostMapping("/list")
+    public Result<List<CfEarnRule>> list(@RequestBody(required = false) Map<String, Object> params) {
+        String category = params == null ? null : (String) params.get("category");
+        Integer enabled = params == null || params.get("enabled") == null ? null
+                : Integer.valueOf(String.valueOf(params.get("enabled")));
+        return Result.success(cfEarnRuleService.list(category, enabled));
+    }
+
+    @Operation(summary = "新建/编辑规则(写库后失效配置缓存并留痕)")
+    @PostMapping("/save")
+    public Result<CfEarnRule> save(@RequestBody Map<String, Object> params) {
+        if (params == null) {
+            return Result.error("参数不能为空");
+        }
+        Long changedBy = params.get("changedBy") == null ? null
+                : Long.valueOf(String.valueOf(params.get("changedBy")));
+        String reason = params.get("changeReason") == null ? null : String.valueOf(params.get("changeReason"));
+        CfEarnRule rule = new CfEarnRule();
+        rule.setCode(str(params.get("code")));
+        rule.setName(str(params.get("name")));
+        rule.setCategory(str(params.get("category")));
+        rule.setEnabled(intVal(params.get("enabled")));
+        rule.setRuleType(str(params.get("ruleType")));
+        rule.setAmount(intVal(params.get("amount")));
+        rule.setFormula(str(params.get("formula")));
+        rule.setParamsJson(str(params.get("paramsJson")));
+        rule.setDailyLimitPerMember(intVal(params.get("dailyLimitPerMember")));
+        rule.setTotalLimitPerMember(intVal(params.get("totalLimitPerMember")));
+        rule.setCooldownSeconds(intVal(params.get("cooldownSeconds")));
+        rule.setDedupRefType(str(params.get("dedupRefType")));
+        rule.setSortOrder(intVal(params.get("sortOrder")));
+        rule.setRemark(str(params.get("remark")));
+        try {
+            return Result.success(cfEarnRuleService.save(rule, changedBy, reason));
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "启停规则")
+    @PostMapping("/toggle")
+    public Result<CfEarnRule> toggle(@RequestBody Map<String, Object> params) {
+        if (params == null || params.get("code") == null) {
+            return Result.error("code 不能为空");
+        }
+        String code = String.valueOf(params.get("code"));
+        Integer enabled = intVal(params.get("enabled"));
+        Long changedBy = params.get("changedBy") == null ? null
+                : Long.valueOf(String.valueOf(params.get("changedBy")));
+        String reason = params.get("changeReason") == null ? null : String.valueOf(params.get("changeReason"));
+        try {
+            return Result.success(cfEarnRuleService.toggle(code, enabled, changedBy, reason));
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "规则变更历史")
+    @PostMapping("/history")
+    public Result<List<CfEarnRuleHistory>> history(@RequestBody(required = false) Map<String, Object> params) {
+        String code = params == null ? null : str(params.get("code"));
+        Integer limitVal = params == null ? null : intVal(params.get("limit"));
+        int limit = limitVal != null ? limitVal : 100;
+        return Result.success(cfEarnRuleService.history(code, limit));
+    }
+
+    private String str(Object v) {
+        return v == null ? null : String.valueOf(v);
+    }
+
+    private Integer intVal(Object v) {
+        if (v == null) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(String.valueOf(v).trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+}

+ 62 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminServiceConfigController.java

@@ -0,0 +1,62 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.config.CfConfigCache;
+import com.etotem.cfc.config.ConfigService;
+import com.etotem.cfc.entity.ServiceConfigItem;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * L2 配置中心(service_config)管理端接口。
+ *
+ * CF 通用运营参数(key 前缀 cf.)的唯一维护入口,保存后立即失效本地缓存。
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 / W0.1
+ */
+@Tag(name = "管理端-服务配置中心(CF 参数)")
+@RestController
+@RequestMapping("/api/admin/service-config")
+public class AdminServiceConfigController {
+
+    @Resource
+    private ConfigService configService;
+
+    @Operation(summary = "配置列表(可按 groupKey 过滤,默认返回全部)")
+    @PostMapping("/list")
+    public Result<List<ServiceConfigItem>> list(@RequestBody(required = false) Map<String, Object> params) {
+        String groupKey = params == null ? null : (String) params.get("groupKey");
+        return Result.success(configService.listConfigs(groupKey));
+    }
+
+    @Operation(summary = "保存配置(upsert,保存后主动失效 CF 配置缓存)")
+    @PostMapping("/save")
+    public Result<String> save(@RequestBody Map<String, Object> params) {
+        String key = params == null ? null : (String) params.get("configKey");
+        Object rawValue = params == null ? null : params.get("configValue");
+        if (key == null || key.trim().isEmpty()) {
+            return Result.error("configKey 不能为空");
+        }
+        if (rawValue == null) {
+            return Result.error("configValue 不能为空");
+        }
+        Long updatedBy = params.get("updatedBy") == null ? null
+                : Long.valueOf(String.valueOf(params.get("updatedBy")));
+        configService.setConfig(key, String.valueOf(rawValue), updatedBy);
+        return Result.success("保存成功");
+    }
+
+    @Operation(summary = "手动失效 CF 配置缓存(排障用)")
+    @PostMapping("/invalidate")
+    public Result<String> invalidate() {
+        CfConfigCache.invalidate();
+        return Result.success("缓存已失效");
+    }
+}

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/config/PublicConfigController.java

@@ -15,7 +15,7 @@ import java.util.Map;
 public class PublicConfigController {
 
     @Resource
-    private SysConfigService configService;
+    private SysConfigService sysConfigService;
 
     @PostMapping("/value")
     public Result<String> getValue(@RequestBody Map<String, String> params) {
@@ -23,7 +23,7 @@ public class PublicConfigController {
         if (key == null || key.isEmpty()) {
             return Result.error("key is required");
         }
-        String value = configService.getValue(key);
+        String value = sysConfigService.getValue(key);
         return Result.success(value);
     }
 }

+ 8 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.controller.config;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.config.CfConfigCache;
 import com.etotem.cfc.dto.SysConfigDTO;
 import com.etotem.cfc.service.SysConfigService;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -17,11 +18,11 @@ import java.util.Map;
 public class SysConfigController {
 
     @Resource
-    private SysConfigService configService;
+    private SysConfigService sysConfigService;
 
     @PostMapping("/list")
     public Result<List<SysConfigDTO>> list() {
-        return configService.listAll();
+        return sysConfigService.listAll();
     }
 
     @PostMapping("/update")
@@ -29,11 +30,14 @@ public class SysConfigController {
         String key = (String) params.get("configKey");
         String value = (String) params.get("configValue");
         String description = (String) params.get("description");
-        return configService.update(key, value, description);
+        Result<String> result = sysConfigService.update(key, value, description);
+        // 过渡期:sys_config 仍存有少量 CF 参数(已标记迁往 service_config),改动后同步失效 CF 配置缓存
+        CfConfigCache.invalidate();
+        return result;
     }
 
     @PostMapping("/all")
     public Result<Map<String, String>> getAllAsMap() {
-        return configService.getAllAsMap();
+        return sysConfigService.getAllAsMap();
     }
 }

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfBalance.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 个人 CF 值余额(家庭成员维度,唯一键 member_id)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §5.1
+ */
+@Data
+@TableName("cf_balance")
+public class CfBalance implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 家庭成员ID(family_members.id) */
+    private Long memberId;
+
+    /** 关联登录用户ID(可为空,不能登录的成员无账号) */
+    private Long userId;
+
+    /** 所属家庭ID(冗余,便于聚合) */
+    private Long familyId;
+
+    /** 累计获得 */
+    private Integer totalEarned;
+
+    /** 可用 */
+    private Integer available;
+
+    /** 提现冻结 */
+    private Integer frozen;
+
+    /** 已提现 */
+    private Integer withdrawn;
+
+    /** 已兑换消费 */
+    private Integer exchanged;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 47 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfBalanceLog.java

@@ -0,0 +1,47 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 个人 CF 值流水(幂等键 member_id + ref_type + ref_id)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §5.2
+ */
+@Data
+@TableName("cf_balance_log")
+public class CfBalanceLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 家庭成员ID(family_members.id) */
+    private Long memberId;
+
+    private Long userId;
+
+    private Long familyId;
+
+    /** earn/spend/freeze/unfreeze/withdraw/adjust */
+    private String type;
+
+    /** 规则编码或迁移标识(F02~F24 / migration_*) */
+    private String refType;
+
+    private Long refId;
+
+    /** 变动数量(正负) */
+    private Integer amount;
+
+    /** 变动后可用余额 */
+    private Integer balanceAfter;
+
+    private String remark;
+
+    private Date createdAt;
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfDirectSalesTier.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 直接销售奖档位(F03a,按团队 8 层内年度累计 P 点分档)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §4.2
+ */
+@Data
+@TableName("cf_direct_sales_tier")
+public class CfDirectSalesTier implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String tierName;
+
+    /** 团队 8 层内年度累计 P 点下限 */
+    private Integer minAnnualPpoints;
+
+    /** 分润比例(%) */
+    private Integer ratePercent;
+
+    private Integer sortOrder;
+
+    /** 1启用 0停用 */
+    private Integer enabled;
+
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnDailyLimit.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * CF 值规则 × 会员等级 每日次数上限映射(N11)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.4
+ */
+@Data
+@TableName("cf_earn_daily_limit")
+public class CfEarnDailyLimit implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 规则编码(cf_earn_rule.code) */
+    private String ruleCode;
+
+    /** 会员等级编码 FREE/BASIC/FAMILY/LIFETIME/PREMIUM */
+    private String levelCode;
+
+    /** 每日次数上限 */
+    private Integer dailyLimit;
+
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 75 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnRule.java

@@ -0,0 +1,75 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * CF 值获取规则(运营可配置,24 行种子 F02–F24)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.1
+ */
+@Data
+@TableName("cf_earn_rule")
+public class CfEarnRule implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 规则编码 F02~F24(F03 拆为 F03a/F03b) */
+    private String code;
+
+    /** 途径名称 */
+    private String name;
+
+    /** consume消费返佣 / behavior成长行为 / admin管理员 */
+    private String category;
+
+    /** 1启用 0停用 */
+    private Integer enabled;
+
+    /** fixed固定 / formula公式 / per_item逐人发放 */
+    private String ruleType;
+
+    /** 固定数量(rule_type=fixed) */
+    private Integer amount;
+
+    /** 计算公式(rule_type=formula) */
+    private String formula;
+
+    /** 公式参数(bps/倍数/档位表等,JSON) */
+    private String paramsJson;
+
+    /** 单成员每日上限,空=不限 */
+    private Integer dailyLimitPerMember;
+
+    /** 单成员累计上限,空=不限 */
+    private Integer totalLimitPerMember;
+
+    /** 同一成员冷却秒数,空=不限 */
+    private Integer cooldownSeconds;
+
+    /** 幂等 refType,写入 cf_balance_log */
+    private String dedupRefType;
+
+    /** 生效起,空=立即 */
+    private Date effectiveFrom;
+
+    /** 生效止,空=长期 */
+    private Date effectiveTo;
+
+    private Integer sortOrder;
+
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+
+    /** 最后修改人 */
+    private Long updatedBy;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfEarnRuleHistory.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * CF 值规则变更历史(审计留痕)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.2
+ */
+@Data
+@TableName("cf_earn_rule_history")
+public class CfEarnRuleHistory implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String ruleCode;
+
+    /** 变更前整行快照(JSON) */
+    private String oldJson;
+
+    /** 变更后整行快照(JSON) */
+    private String newJson;
+
+    private Long changedBy;
+
+    private String changeReason;
+
+    private Date createdAt;
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CfIndirectSalesTier.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 间接销售奖档位(F03b,按自身年度累计消费 P 点分档)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §4.2
+ */
+@Data
+@TableName("cf_indirect_sales_tier")
+public class CfIndirectSalesTier implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String tierName;
+
+    /** 自身年度累计消费 P 点下限 */
+    private Integer minSelfPpoints;
+
+    /** 分润比例(%) */
+    private Integer ratePercent;
+
+    private Integer sortOrder;
+
+    /** 1启用 0停用 */
+    private Integer enabled;
+
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java

@@ -31,6 +31,9 @@ public class Coupon implements Serializable {
     /** 绑定商品ID,NULL=全场通用 */
     private Long productId;
 
+    /** 模板块绑定家庭ID(NULL=全平台模板;优惠券家庭化 N3) */
+    private Long familyId;
+
     /** 积分兑换价,0=不可积分兑换 */
     private Integer pointsPrice;
 

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceConfigItem.java

@@ -0,0 +1,45 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 服务配置项(L2 配置中心,CF 通用运营参数归口)
+ *
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.6 / W0.1
+ */
+@Data
+@TableName("service_config")
+public class ServiceConfigItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 配置分组(如 cf.rate / cf.commission) */
+    private String groupKey;
+
+    /** 配置键(如 cf.rate.points_multiplier),全局唯一 */
+    private String configKey;
+
+    /** 配置值(统一字符串存储,按 value_type 解析) */
+    private String configValue;
+
+    /** string/int/double/bool/json */
+    private String valueType;
+
+    private String description;
+
+    /** 1允许后台编辑 0系统托管 */
+    private Integer editable;
+
+    private Long updatedBy;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java

@@ -27,4 +27,7 @@ public class UserCoupon implements Serializable {
     private Date usedAt;
 
     private Long orderId;
+
+    /** 券实例归属家庭ID(优惠券家庭化 N3,家庭内任意成员可用) */
+    private Long familyId;
 }

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfBalanceLogMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfBalanceLog;
+
+public interface CfBalanceLogMapper extends BaseMapper<CfBalanceLog> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfBalanceMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfBalance;
+
+public interface CfBalanceMapper extends BaseMapper<CfBalance> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfDirectSalesTierMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfDirectSalesTier;
+
+public interface CfDirectSalesTierMapper extends BaseMapper<CfDirectSalesTier> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnDailyLimitMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfEarnDailyLimit;
+
+public interface CfEarnDailyLimitMapper extends BaseMapper<CfEarnDailyLimit> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnRuleHistoryMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfEarnRuleHistory;
+
+public interface CfEarnRuleHistoryMapper extends BaseMapper<CfEarnRuleHistory> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfEarnRuleMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfEarnRule;
+
+public interface CfEarnRuleMapper extends BaseMapper<CfEarnRule> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CfIndirectSalesTierMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfIndirectSalesTier;
+
+public interface CfIndirectSalesTierMapper extends BaseMapper<CfIndirectSalesTier> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceConfigItemMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ServiceConfigItem;
+
+public interface ServiceConfigItemMapper extends BaseMapper<ServiceConfigItem> {
+}

+ 166 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CfEarnRuleService.java

@@ -0,0 +1,166 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.config.CfConfigCache;
+import com.etotem.cfc.entity.CfEarnRule;
+import com.etotem.cfc.entity.CfEarnRuleHistory;
+import com.etotem.cfc.mapper.CfEarnRuleHistoryMapper;
+import com.etotem.cfc.mapper.CfEarnRuleMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * CF 值获取规则配置服务(L2 规则层,管理端读写 + 变更留痕)。
+ *
+ * 只做规则配置的读写与审计,不做任何发放计算(发放逻辑在 F-2 批 CfBalanceService)。
+ * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.6.1 / §3.6.4
+ */
+@Service
+public class CfEarnRuleService {
+
+    private static final Logger log = LoggerFactory.getLogger(CfEarnRuleService.class);
+
+    @Resource
+    private CfEarnRuleMapper cfEarnRuleMapper;
+
+    @Resource
+    private CfEarnRuleHistoryMapper cfEarnRuleHistoryMapper;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
+    /** 规则列表(可按 category 过滤,enabled 为空表示不过滤) */
+    public List<CfEarnRule> list(String category, Integer enabled) {
+        LambdaQueryWrapper<CfEarnRule> wrapper = new LambdaQueryWrapper<>();
+        if (category != null && !category.trim().isEmpty()) {
+            wrapper.eq(CfEarnRule::getCategory, category);
+        }
+        if (enabled != null) {
+            wrapper.eq(CfEarnRule::getEnabled, enabled);
+        }
+        wrapper.orderByAsc(CfEarnRule::getSortOrder).orderByAsc(CfEarnRule::getCode);
+        return cfEarnRuleMapper.selectList(wrapper);
+    }
+
+    /** 按规则编码取单条(空=不存在) */
+    public CfEarnRule getByCode(String code) {
+        if (code == null || code.trim().isEmpty()) {
+            return null;
+        }
+        return cfEarnRuleMapper.selectOne(
+                new LambdaQueryWrapper<CfEarnRule>().eq(CfEarnRule::getCode, code).last("LIMIT 1"));
+    }
+
+    /**
+     * 新建/编辑规则(按 code 判重),写库后留痕并失效本地缓存。
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public CfEarnRule save(CfEarnRule rule, Long changedBy, String changeReason) {
+        if (rule == null || rule.getCode() == null || rule.getCode().trim().isEmpty()) {
+            throw new IllegalArgumentException("规则编码 code 不能为空");
+        }
+        CfEarnRule existing = getByCode(rule.getCode());
+        Date now = new Date();
+        String oldJson = existing == null ? null : toJson(existing);
+        CfEarnRule saved;
+        if (existing == null) {
+            if (rule.getEnabled() == null) {
+                rule.setEnabled(1);
+            }
+            if (rule.getSortOrder() == null) {
+                rule.setSortOrder(0);
+            }
+            rule.setCreatedAt(now);
+            rule.setUpdatedAt(now);
+            rule.setUpdatedBy(changedBy);
+            cfEarnRuleMapper.insert(rule);
+            saved = rule;
+        } else {
+            // 只覆盖可编辑字段,避免 id/createdAt 被外部传入值破坏
+            existing.setName(rule.getName());
+            existing.setCategory(rule.getCategory());
+            existing.setEnabled(rule.getEnabled());
+            existing.setRuleType(rule.getRuleType());
+            existing.setAmount(rule.getAmount());
+            existing.setFormula(rule.getFormula());
+            existing.setParamsJson(rule.getParamsJson());
+            existing.setDailyLimitPerMember(rule.getDailyLimitPerMember());
+            existing.setTotalLimitPerMember(rule.getTotalLimitPerMember());
+            existing.setCooldownSeconds(rule.getCooldownSeconds());
+            existing.setDedupRefType(rule.getDedupRefType());
+            existing.setEffectiveFrom(rule.getEffectiveFrom());
+            existing.setEffectiveTo(rule.getEffectiveTo());
+            existing.setSortOrder(rule.getSortOrder());
+            existing.setRemark(rule.getRemark());
+            existing.setUpdatedAt(now);
+            existing.setUpdatedBy(changedBy);
+            cfEarnRuleMapper.updateById(existing);
+            saved = existing;
+        }
+        writeHistory(saved.getCode(), oldJson, toJson(saved), changedBy, changeReason);
+        CfConfigCache.invalidate();
+        log.info("CF 规则已保存 code={} by={}", saved.getCode(), changedBy);
+        return saved;
+    }
+
+    /** 启停规则(写库 + 留痕 + 失效缓存) */
+    @Transactional(rollbackFor = Exception.class)
+    public CfEarnRule toggle(String code, Integer enabled, Long changedBy, String changeReason) {
+        CfEarnRule rule = getByCode(code);
+        if (rule == null) {
+            throw new IllegalArgumentException("规则不存在:" + code);
+        }
+        String oldJson = toJson(rule);
+        rule.setEnabled(enabled == null || enabled == 0 ? 0 : 1);
+        rule.setUpdatedAt(new Date());
+        rule.setUpdatedBy(changedBy);
+        cfEarnRuleMapper.updateById(rule);
+        writeHistory(rule.getCode(), oldJson, toJson(rule), changedBy,
+                changeReason != null ? changeReason : "切换开关 enabled=" + rule.getEnabled());
+        CfConfigCache.invalidate();
+        log.info("CF 规则开关已变更 code={} enabled={} by={}", code, rule.getEnabled(), changedBy);
+        return rule;
+    }
+
+    /** 变更历史(按时间倒序) */
+    public List<CfEarnRuleHistory> history(String code, int limit) {
+        LambdaQueryWrapper<CfEarnRuleHistory> wrapper = new LambdaQueryWrapper<>();
+        if (code != null && !code.trim().isEmpty()) {
+            wrapper.eq(CfEarnRuleHistory::getRuleCode, code);
+        }
+        wrapper.orderByDesc(CfEarnRuleHistory::getCreatedAt).orderByDesc(CfEarnRuleHistory::getId)
+                .last("LIMIT " + Math.max(1, Math.min(limit, 500)));
+        return cfEarnRuleHistoryMapper.selectList(wrapper);
+    }
+
+    private void writeHistory(String ruleCode, String oldJson, String newJson, Long changedBy, String reason) {
+        try {
+            CfEarnRuleHistory history = new CfEarnRuleHistory();
+            history.setRuleCode(ruleCode);
+            history.setOldJson(oldJson);
+            history.setNewJson(newJson);
+            history.setChangedBy(changedBy != null ? changedBy : 0L);
+            history.setChangeReason(reason);
+            history.setCreatedAt(new Date());
+            cfEarnRuleHistoryMapper.insert(history);
+        } catch (Exception e) {
+            log.warn("写 cf_earn_rule_history 失败 code={}:{}", ruleCode, e.getMessage());
+        }
+    }
+
+    private String toJson(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (Exception e) {
+            log.warn("规则快照序列化失败:{}", e.getMessage());
+            return null;
+        }
+    }
+}

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionEnergySyncService.java

@@ -39,7 +39,7 @@ public class DimensionEnergySyncService {
     private EnergyService energyService;
 
     @Resource
-    private SysConfigService configService;
+    private SysConfigService sysConfigService;
 
     private final ObjectMapper objectMapper = new ObjectMapper();
 
@@ -93,7 +93,7 @@ public class DimensionEnergySyncService {
      */
     private Map<String, Double> loadWeights() {
         try {
-            String json = configService.getValue(CONFIG_WEIGHTS);
+            String json = sysConfigService.getValue(CONFIG_WEIGHTS);
             if (json != null && !json.isEmpty()) {
                 Map<String, Double> parsed = objectMapper.readValue(json,
                         new TypeReference<Map<String, Double>>() {});
@@ -112,7 +112,7 @@ public class DimensionEnergySyncService {
      */
     private double loadFactor() {
         try {
-            String val = configService.getValue(CONFIG_FACTOR);
+            String val = sysConfigService.getValue(CONFIG_FACTOR);
             if (val != null && !val.isEmpty()) {
                 return Double.parseDouble(val);
             }

+ 141 - 4
cfc-backend/src/main/resources/schema.sql

@@ -1178,7 +1178,7 @@ CREATE TABLE IF NOT EXISTS products (
     dimension_weights JSON DEFAULT NULL COMMENT '五维权重百分比: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0}',
     profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)',
     sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)',
-    points_multiplier DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数',
+    points_multiplier DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT '积分倍数【已迁移→service_config cf.rate.points_multiplier,本列仅过渡期兼容】',
     delivery_method TINYINT DEFAULT 1 COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人 6=同城配送',
     purchase_count_threshold INT DEFAULT 0 COMMENT '最小购买数量限制',
     category_id BIGINT COMMENT '商品类目ID',
@@ -2339,7 +2339,7 @@ CREATE TABLE IF NOT EXISTS coupon (
     used_count INT DEFAULT 0,
     discount_rate INT COMMENT '折扣券折扣率(千分比,9000=9折)',
     product_id BIGINT COMMENT '绑定商品ID,NULL=全场通用',
-    family_id BIGINT DEFAULT NULL COMMENT '绑定家庭ID(NULL=不绑定)',
+    family_id BIGINT DEFAULT NULL COMMENT '模板块绑定家庭ID(NULL=全平台模板)',
     points_price INT DEFAULT 0 COMMENT '积分兑换价,0=不可积分兑换',
     grant_type VARCHAR(16) COMMENT '发放规则:JOIN/PERIODIC/POPULATION,NULL=不自动发放',
     grant_level_code VARCHAR(32) COMMENT '发放规则对应会员等级',
@@ -2347,7 +2347,8 @@ CREATE TABLE IF NOT EXISTS coupon (
     grant_quantity INT DEFAULT 1 COMMENT '每次发放数量',
     status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE=启用/DISABLED=停用',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_valid_period (valid_from, valid_until)
+    INDEX idx_valid_period (valid_from, valid_until),
+    INDEX idx_family (family_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券配置表';
 
 CREATE TABLE IF NOT EXISTS user_coupon (
@@ -2358,9 +2359,11 @@ CREATE TABLE IF NOT EXISTS user_coupon (
     received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     used_at DATETIME,
     order_id BIGINT,
+    family_id BIGINT DEFAULT NULL COMMENT '券实例归属家庭ID(家庭内任意成员可用)',
     INDEX idx_user_coupon (user_id, coupon_id),
     INDEX idx_status (status),
-    INDEX idx_order_id (order_id)
+    INDEX idx_order_id (order_id),
+    INDEX idx_family (family_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠券表';
 
 -- =============================================
@@ -6658,3 +6661,137 @@ CREATE TABLE IF NOT EXISTS planner_exam_attempts (
     PRIMARY KEY (id),
     KEY idx_user (user_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核作答记录';
+
+-- =============================================
+-- CF 值体系统一(个人 CF 值 + 优惠券家庭化)
+-- 口径: docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md
+-- F-1 数据层 + W0.1 配置归口(仅建表/种子,不含发放逻辑)
+-- =============================================
+
+-- 表 个人CF值余额(家庭成员维度)
+CREATE TABLE IF NOT EXISTS cf_balance (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    user_id BIGINT COMMENT '关联登录用户ID(可为空,不能登录成员无账号)',
+    family_id BIGINT COMMENT '所属家庭ID(冗余,便于聚合)',
+    total_earned INT NOT NULL DEFAULT 0 COMMENT '累计获得',
+    available INT NOT NULL DEFAULT 0 COMMENT '可用',
+    frozen INT NOT NULL DEFAULT 0 COMMENT '提现冻结',
+    withdrawn INT NOT NULL DEFAULT 0 COMMENT '已提现',
+    exchanged INT NOT NULL DEFAULT 0 COMMENT '已兑换消费',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_member (member_id),
+    KEY idx_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值余额(家庭成员维度)';
+
+-- 表 个人CF值流水(幂等键 member_id+ref_type+ref_id)
+CREATE TABLE IF NOT EXISTS cf_balance_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL,
+    user_id BIGINT,
+    family_id BIGINT,
+    type VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust',
+    ref_type VARCHAR(50) NOT NULL COMMENT '规则编码或迁移标识(F02-F24 / migration_*)',
+    ref_id BIGINT,
+    amount INT NOT NULL COMMENT '变动数量(正负)',
+    balance_after INT NOT NULL COMMENT '变动后可用余额',
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    KEY idx_member (member_id),
+    KEY idx_family (family_id),
+    UNIQUE KEY uk_ref (member_id, ref_type, ref_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值流水';
+
+-- 表 CF值获取规则(运营可配置,24 行种子 F02–F24)
+CREATE TABLE IF NOT EXISTS cf_earn_rule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    code VARCHAR(20) NOT NULL COMMENT '规则编码 F02~F24(F03 拆 F03a/F03b)',
+    name VARCHAR(100) NOT NULL COMMENT '途径名称',
+    category VARCHAR(20) NOT NULL COMMENT 'consume消费返佣/behavior成长行为/admin管理员',
+    enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用(随时开关)',
+    rule_type VARCHAR(20) NOT NULL COMMENT 'fixed固定/formula公式/per_item逐人发放',
+    amount INT COMMENT '固定数量(rule_type=fixed)',
+    formula VARCHAR(500) COMMENT '计算公式(rule_type=formula)',
+    params_json TEXT COMMENT '公式参数(bps/倍数/档位表等,JSON)',
+    daily_limit_per_member INT COMMENT '单成员每日上限,空=不限',
+    total_limit_per_member INT COMMENT '单成员累计上限,空=不限',
+    cooldown_seconds INT COMMENT '同一成员冷却秒数,空=不限',
+    dedup_ref_type VARCHAR(50) NOT NULL COMMENT '幂等refType,写入cf_balance_log',
+    effective_from DATETIME COMMENT '生效起,空=立即',
+    effective_to DATETIME COMMENT '生效止,空=长期',
+    sort_order INT NOT NULL DEFAULT 0,
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    updated_by BIGINT COMMENT '最后修改人',
+    UNIQUE KEY uk_code (code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值获取规则(运营可配置)';
+
+-- 表 CF值规则变更历史(审计)
+CREATE TABLE IF NOT EXISTS cf_earn_rule_history (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    rule_code VARCHAR(20) NOT NULL,
+    old_json TEXT COMMENT '变更前整行快照',
+    new_json TEXT COMMENT '变更后整行快照',
+    changed_by BIGINT NOT NULL,
+    change_reason VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    KEY idx_rule (rule_code, created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则变更历史';
+
+-- 表 直接销售奖档位(按团队8层内年度P点累计)
+CREATE TABLE IF NOT EXISTS cf_direct_sales_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL COMMENT '档位名称',
+    min_annual_ppoints INT NOT NULL COMMENT '团队8层内年度累计P点下限',
+    rate_percent INT NOT NULL COMMENT '分润比例(%)',
+    sort_order INT NOT NULL DEFAULT 0,
+    enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_min (min_annual_ppoints)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直接销售奖档位(按团队8层内年度P点累计)';
+
+-- 表 间接销售奖档位(按自身年度消费P点累计)
+CREATE TABLE IF NOT EXISTS cf_indirect_sales_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL,
+    min_self_ppoints INT NOT NULL COMMENT '自身年度累计消费P点下限',
+    rate_percent INT NOT NULL COMMENT '分润比例(%)',
+    sort_order INT NOT NULL DEFAULT 0,
+    enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_min (min_self_ppoints)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='间接销售奖档位(按自身年度消费P点累计)';
+
+-- 表 CF值规则×会员等级每日次数上限(N11)
+CREATE TABLE IF NOT EXISTS cf_earn_daily_limit (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    rule_code VARCHAR(20) NOT NULL COMMENT '规则编码(cf_earn_rule.code)',
+    level_code VARCHAR(32) NOT NULL COMMENT '会员等级编码 FREE/BASIC/FAMILY/LIFETIME/PREMIUM',
+    daily_limit INT NOT NULL COMMENT '每日次数上限',
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_rule_level (rule_code, level_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则×会员等级每日次数上限(N11)';
+
+-- 表 服务配置中心(L2,CF通用运营参数归口)
+CREATE TABLE IF NOT EXISTS service_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    group_key VARCHAR(64) NOT NULL DEFAULT 'general' COMMENT '配置分组(如 cf.rate / cf.commission)',
+    config_key VARCHAR(128) NOT NULL COMMENT '配置键(如 cf.rate.points_multiplier)',
+    config_value TEXT COMMENT '配置值(统一字符串存储,按 value_type 解析)',
+    value_type VARCHAR(16) NOT NULL DEFAULT 'string' COMMENT 'string/int/double/bool/json',
+    description VARCHAR(255),
+    editable TINYINT NOT NULL DEFAULT 1 COMMENT '1允许后台编辑 0系统托管',
+    updated_by BIGINT,
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_config_key (config_key),
+    KEY idx_group_key (group_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务配置中心(L2,CF通用运营参数归口)';

+ 1 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -225,6 +225,7 @@
 | **LIFETIME 终身会员(方案A:等级扩展+实时聚合+管理员审核)** | `specs/2026-08-07-xaf-partner-ppt-redesign-design.md` + `plans/2026-08-09-lifetime-membership.md` |
 | **AI健康教练人格分化与管家自助选择**(L2 自助绑管/换绑 + 浠宝/福宝人格话术 + 管理端分配管理与停接开关) ✅ 已完成 | `specs/2026-08-23-coach-butler-design.md` + `plans/2026-08-23-coach-butler.md` |
 | **BASIC 体验会员 + 权益对比表动态化 + PROVIDER 退出会员权益**(2026-10-18):迁移 381/351/321 统一 level_order 语义(越大越高,PROVIDER=1 与 FREE 同级)+ `getMemberLevel` 归一 PROVIDER→FREE + `canUseFeature` 移除全权限兜底 + 活动价判定去 PROVIDER + 新接口 `POST /api/membership/compare` 聚合各档实际权限(折扣 bps / max_children / max_tasks_per_day / BasicQuotaService 配额 / features 开关),upgrade.vue 与 index.vue 对比表改动态渲染 | `api/API_REFERENCE.md` 4.50 |
+| **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` 六 |
 
 ---
 

+ 33 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -2050,4 +2050,36 @@ Web 管理端定义活动海报模板(迁移381 `activity_poster_templates` 
 
 - 待建设入口(当前代码不存在,预留):营养方案/饮食建议(`nutritionist`)。
 
-*文档更新:2026-10-22(规划师培训认证模块落地:9 表 + 迁移394-401 + 规划师端/管理端/webhook 接口 + 接单闸门)*
+---
+
+## 六、CF 值体系统一 · 配置治理接口(F-1 / W0.1)
+
+> 口径:`docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md` §3.6.6 / §3.7 W0.1
+> 本批只提供 **数据层 + 配置层** 接口,发放/扣减接口在 F-2/F-3 批新增。
+
+### 6.1 服务配置中心(`/api/admin/service-config`)
+
+CF 通用运营参数唯一维护入口(`service_config` 表,key 前缀 `cf.`)。保存后自动 `CfConfigCache.invalidate()`(本地缓存 TTL 60s)+ `cf.version` 自增。
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/service-config/list` | 配置列表,body 可选 `{"groupKey":"cf.rate"}`,不传返回全部 |
+| `POST /api/admin/service-config/save` | 保存配置(upsert),body `{"configKey":"cf.rate.points_multiplier","configValue":"1.0","updatedBy":1}` |
+| `POST /api/admin/service-config/invalidate` | 手动失效 CF 配置缓存(排障用) |
+
+已登记配置项:`cf.rate.points_multiplier`、`cf.rate.repeat_rate_default`、`cf.commission.settle_days`、`cf.invite.valid_days`、`cf.price.default_factor`、`cf.stock.deduction_mode`、`cf.rate.share_read_reward_points`、`cf.commission.service_rate`、`cf.product.gut_health_price`、`cf.product.platform_points_share`、`cf.version`。
+
+### 6.2 CF 值获取规则(`/api/admin/cf-earn-rule`)
+
+24 条规则(F02–F24,F03 拆 F03a/F03b)配置入口;每次保存/开关写 `cf_earn_rule_history` 并失效配置缓存。
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/cf-earn-rule/list` | 规则列表,body 可选 `{"category":"behavior","enabled":1}` |
+| `POST /api/admin/cf-earn-rule/save` | 新建/编辑(按 `code` 判重),body 含 `code/name/category/ruleType/amount/formula/paramsJson/dailyLimitPerMember/totalLimitPerMember/cooldownSeconds/dedupRefType/remark/changedBy/changeReason` |
+| `POST /api/admin/cf-earn-rule/toggle` | 启停,body `{"code":"F07","enabled":0,"changedBy":1,"changeReason":"..."}` |
+| `POST /api/admin/cf-earn-rule/history` | 变更历史,body 可选 `{"code":"F07","limit":100}` |
+
+> 读取封装:`ConfigService.getConfig(key)`(缓存 60s → DB → 兜底常量);规则读取:`CfEarnRuleService.getByCode(code)`。
+
+*文档更新:2026-10-24(CF 值体系统一 F-1 数据层 + W0.1 配置归口:8 张新表/迁移407-412 + service_config 与 cf_earn_rule 管理端接口)*

+ 559 - 0
docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md

@@ -0,0 +1,559 @@
+# CF 值体系统一方案 · 个人维度 + 优惠券家庭化
+
+> 日期:2026-10-24
+> 状态:待派发(批 F)
+> 前置文档:`2026-08-12-platform-points-family-pool-design.md`、`2026-09-01-cf-commission-redesign.md`
+> 本方案取代上述两份文档中与"CF 值归属维度"相关的全部结论
+
+---
+
+## 一、需求口径变更(2026-10-24 用户拍板,单一事实源)
+
+| # | 新口径 | 取代的旧口径 |
+|---|---|---|
+| N1 | **取消家庭 CF 值**,家庭池不再承接任何 CF 值发放/余额 | 2026-08-12:CF 值=家庭共享池 |
+| N2 | **CF 值是个人的,记账维度=家庭成员(family_member.id)** | 206-08-12 家庭池 / 2026-09-01 个人钱包(user 维度) |
+| N3 | **优惠券是家庭的**:个人用 CF 值兑换的优惠券一律归属家庭共享 | 2026-08-12 需求第 3 条"CF 值不参与券兑换" |
+| N4 | 必须落实**所有**个人 CF 值获取途径的**规则与数量**(本文档 §三) | — |
+| N5 | **推荐分润合并为一条**:团队规模阶梯分佣 + 旧 L1/L2 分佣合并 | 2026-09-01 双体系并存 |
+| N6 | **消费返 CF**:按实际消费,**1 元 = 100 CF**,**所有消费都返**,商品收货规则(F01)去掉 | 2026-08-12 F01 商品收货 1元=1积分 + F02 消费返 CF(双轨) |
+| N7 | **推荐分润两级**:<br>① **直接销售奖**:基数=直接推荐人的消费 P 点,按团队(8 层以内)年度累计 P 点分档,最高 70%<br>② **间接销售奖**:基数=间接推荐人的消费 P 点,按自身年度累计消费 P 点分档,最高 20%<br>累计按**一年滚动**计算,累计单位是 **P 点**(非金额),用户级别可**降级** | 2026-09-01 单一团队规模阶梯 |
+| N8 | **分享奖励**:推荐他人注册平台即获得 CF 值(新增 F24) | — |
+| N9 | **F07 文章答题** 日限 5 次 | 原无日限 |
+| N10 | **F09 测评建档**:第 1 份报告 50 CF,后续每份 10 CF,**重复报告不计** | 原仅首次 50 |
+| N11 | **任务、打卡获取 CF 值**按会员等级不同次数上限:<br>• 9.9 体验会员(BASIC):3 次/天<br>• 家庭会员(FAMILY)& 终身会员(LIFETIME):10 次/天<br>• 高级会员(PREMIUM):20 次/天<br>• 免费会员(FREE)与标准会员(PROVIDER)待定 | 原固定每日 1 次 |
+
+### 1.1 配套决策(已确认)
+
+| # | 决策 | 结论 |
+|---|---|---|
+| D1 | 记账维度 | **家庭成员维度**(`family_members.id`),非登录用户维度 |
+| D2 | 存量家庭池余额处理 | **迁移到家庭创建者账号**,一次性脚本,写迁移流水,幂等 |
+
+**为何选成员维度而非用户维度**:答题、打卡、家庭挑战、成长任务、测评建档这 13 条获取途径当前全部挂在 `family_member` 维度;若改挂 `user`,则无独立账号的孩子(`memberType=local`)无法持有 CF 值,打卡/答题/挑战将静默失效。成员维度实现零改动、零损失,且与家庭关系树天然对齐。
+
+---
+
+## 二、现状:三套账本
+
+| 账本 | 表/字段 | 维度 | 获取途径数 | 唯一键 |
+|---|---|---|---|---|
+| A · 家庭 CF 值 | `family_platform_balance` | 家庭 | 4 | `family_id` |
+| B · 个人 CF 钱包 | `user_platform_balance` | 用户 | 9 | `user_id` |
+| C · 孩子 CF 值 | `family_members.system_points` | 家庭成员 | 13 | `family_member.id` |
+
+问题:`system_points` 注释写作"CF值余额"(`PointsService.java:106`),但用户端 `wealth-cf` 页展示的是家庭 CF 值 —— 获得来源与可见余额不在同一账本。
+
+---
+
+## 三、个人 CF 值获取规则与数量总表(N4 核心交付)
+
+合并后共 **24 条**有效获取途径(原 23 条:F01 删除,F02/F03 重写,新增 F24,F06 修正,F07/F09/F14-F16/F22 按会员等级分级)。
+
+### 3.1 消费返佣类(5 条)
+
+| # | 途径 | 数量规则 | 参数来源 | 落账 |
+|---|---|---|---|---|
+| ~~F01~~ | ~~商品确认收货返积分~~ | **删除(N6)** | — | — |
+| F02 | **消费返 CF(所有订单)** | **`floor(实付金额(分)/100) × 100`**<br>即 **1 元 = 100 CF**;所有消费订单(商品/套餐/测评/会员/订阅/调研)均返 | 固定系数 **100**,可外置 `sys_config.cf_per_yuan`(默认 100) | member |
+| F03a | **直接销售奖** | **基数 = 直接推荐人消费的 P 点**<br>**比例档 = 按团队(8 层以内)年度累计 P 点分档**,最高 70%<br>累计:滚动一年(以消费时间为准),累计单位 P 点<br>可降级 | `cf_direct_sales_tier` 表(档位可配,见 §四)<br>`cf_referral_service.getTeamAnnualPpoints(referrerId, 8)`(团队 8 层内一年 P 点累计) | member |
+| F03b | **间接销售奖** | **基数 = 间接推荐人消费的 P 点**<br>**比例档 = 按自身年度累计消费 P 点分档**,最高 20%<br>累计:滚动一年,累计单位 P 点<br>可降级 | `cf_indirect_sales_tier` 表(档位可配,见 §四)<br>`cf_referral_service.getSelfAnnualPpoints(referrerId)` | member |
+| F04 | 商品赠品 CF | `赠品配置的 CF 数量` | `product_order_gift.cf_value`(赠品规则管理端配置) | member |
+| F05 | 活动签到 CF | 活动配置的 CF 数量 | `activities.platform_points` | member |
+| F06 | 菌群健康调研打卡返还 | `cf_refund_amount`,为空按 `floor(项目价/100) × 100`(1 元=100 CF)<br>示例:19.9 元 → **1,990 CF**(原兜底 1990 恰自洽) | `GutHealthProject.cf_refund_amount` / `sys_config.gut_health_project_price`(兜底常量去除) | member |
+
+### 3.2 成长行为类(14 条)
+
+| # | 途径 | 数量规则 | 参数来源 | 代码出处 |
+|---|---|---|---|---|
+| F07 | 文章答题奖励 | `答对题数 + (全对 ? 3 : 0)`<br>**日限 5 次(N9)** | `cf_earn_rule.amount`(固定公式),`daily_limit_per_member`=5 | `ArticleCommentController.java:213` |
+| F08 | AI 阅读答题奖励 | `5`(仅全部答对才发) | `cf_earn_rule.amount`=5(固定) | `ArticleService.java:1071` |
+| F09 | 完成测评建档 | **第 1 份报告 50 CF,后续每份 10 CF,重复报告不计(N10)**<br>需按报告文件哈希/编号去重 | `cf_earn_rule.formula`=`first_report?50:10`,去重键 `report_hash` | `GuideRecordController.java:58` |
+| F10 | 家庭挑战完成 | `challenge.reward_points`,按每个完成成员各发一次(待确认是否改家庭合计) | `family_challenge.reward_points`(管理端配置) | `FamilyChallengeService.java:465-475` |
+| F11 | 邀请里程碑 | `milestone.reward_points`,按里程碑配置 | 里程碑配置表 | `InviteMilestoneService.java:120` |
+| F12 | 成长任务 | `task.reward_points`,按任务模板配置 | 成长任务模板配置 | `GrowthTaskService.java:108` |
+| F13 | 分享阅读奖励 | 默认 `5` | `sys_config.share_read_reward_points` / `cf_earn_rule.amount`=5 | `ShareService.java:103` |
+| F14 | 喝水打卡 | 固定 `5` CF/次<br>**日限按会员等级(N11):**<br>FREE=1/天(建议),BASIC=3/天,FAMILY=10/天,LIFETIME=10/天,PREMIUM=20/天,PROVIDER=待定 | `cf_earn_rule.amount`=5,`daily_limit_by_tier` 映射表(见 §3.4) | `HealthWaterController.java:103` |
+| F15 | 饮食打卡 | 固定 `5` CF/次<br>**日限同上(N11)** | 同上 | `HealthMealController.java:294` |
+| F16 | 每日健康打卡 | 固定 `10` CF/次<br>**日限同上(N11)** | 同上 | `DailyCheckinService.java:78` |
+| F17 | 挑战赛子任务完成 | `progress.reward_cf`,按任务配置 | 挑战赛任务配置 | `ChallengeServiceImpl.java:142` |
+| F18 | 健康启航计划付费 | `floor(实付金额/100) × 100`(1 元=100 CF) | `cf_earn_rule.formula`=同 F02 | `PurchaseServiceImpl.java:109` |
+| F19 | 管理员手动调整 | 正负任意,需家长密码校验 | 管理端输入 | `PointsService.java:70` |
+| **F24** | **分享奖励(推荐注册)** | **推荐他人注册平台即获得 CF 值(N8)**<br>数量可配(建议 20 CF/人) | `cf_earn_rule.amount`(建议 20),`daily_limit_per_member`=10 | — |
+
+### 3.3 问题域与关卡类(2 条)
+
+| # | 途径 | 数量规则 | 参数来源 | 代码出处 |
+|---|---|---|---|---|
+| F20 | 问题域全部任务完成奖励 | `domain.reward_cf_value` | `problem_domain.reward_cf_value`(管理端配置) | `ProblemCompletionService.java:127` |
+| F21 | 关卡通关奖励 | `gate.reward_points` | `unlock_gate.reward_points`(管理端配置) | `UnlockGateService.java:300` |
+
+### 3.4 任务与成员间流转(2 条)
+
+| # | 途径 | 数量规则 | 参数来源 | 代码出处 |
+|---|---|---|---|---|
+| F22 | 任务完成 CF | `task.cf_amount`(现**双发**个人+家庭池,取消家庭池那条) | 任务配置 | `TaskService.java:998`(保留)/ `:1001`(**删除**) |
+| F23 | 成员间转让(收款方) | 转入方 +金额,转出方 -金额,同一事务 | 转账金额 | `CfTransferController.java:63` |
+
+### 3.4 会员等级 → 每日次数上限映射表(N11)
+
+| 会员等级 | level_code | 任务/打卡每日上限次数 | 备注 |
+|---|---|---|---|
+| 免费会员 | FREE | 1(建议) | 未明确口径,建议 1 |
+| 9.9 体验会员 | BASIC | 3 | N11 明确 |
+| 家庭会员 | FAMILY | 10 | N11 明确 |
+| 终身会员 | LIFETIME | 10 | N11 明确 |
+| 高级会员 | PREMIUM | 20 | N11 明确 |
+| 标准会员 | PROVIDER | 待定 | 规划师/专家向,非消费端 |
+
+> 实现:`CfBalanceService.earnByRule` 在执行前查 `member` 关联的 `user` → `family_memberships` → `membership_levels.level_code`,按上表取 `daily_limit`。`cf_earn_rule` 增加 `daily_limit_by_tier` JSON 字段或单独表 `cf_earn_daily_limit(rule_code, level_code, daily_limit)`。
+
+### 3.5 待外置的硬编码清单(合规改造项)
+
+F08、F18 两处 CF 数量为硬编码,需迁到 `cf_earn_rule`(F07/F14-F16 已在规则表配置,F09 已公式化,F13 已 sys_config)。F10 家庭挑战按人各发或家庭合计待确认。
+
+---
+
+### 3.6 规则与数量数据库化设计(N4 落地核心)
+
+**总原则:发放收口到单一入口**。24 条途径全部改调 `CfBalanceService.earnByRule(memberId, ruleCode, refId, ctx)`,代码中**不再保留任何 CF 数量常量**——否则"随时调整"落不了地。
+
+#### 3.6.1 规则表 `cf_earn_rule`(24 行种子,F02–F24)
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_earn_rule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    code VARCHAR(20) NOT NULL COMMENT '规则编码 F02~F24',
+    name VARCHAR(100) NOT NULL COMMENT '途径名称',
+    category VARCHAR(20) NOT NULL COMMENT 'consume消费返佣/behavior成长行为/admin管理员',
+    enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用(随时开关)',
+    rule_type VARCHAR(20) NOT NULL COMMENT 'fixed固定/formula公式/per_item逐人发放',
+    amount INT COMMENT '固定数量(rule_type=fixed)',
+    formula VARCHAR(500) COMMENT '计算公式(rule_type=formula)',
+    params_json TEXT COMMENT '公式参数(bps/倍数等)',
+    daily_limit_per_member INT COMMENT '单成员每日上限,空=不限',
+    total_limit_per_member INT COMMENT '单成员累计上限,空=不限',
+    cooldown_seconds INT COMMENT '同一成员冷却秒数,空=不限',
+    dedup_ref_type VARCHAR(50) NOT NULL COMMENT '幂等refType,写入cf_balance_log',
+    effective_from DATETIME COMMENT '生效起,空=立即',
+    effective_to DATETIME COMMENT '生效止,空=长期',
+    sort_order INT NOT NULL DEFAULT 0,
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    updated_by BIGINT COMMENT '最后修改人',
+    UNIQUE KEY uk_code (code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值获取规则(运营可配置)';
+```
+
+#### 3.6.2 变更历史 `cf_earn_rule_history`(审计)
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_earn_rule_history (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    rule_code VARCHAR(20) NOT NULL,
+    old_json TEXT COMMENT '变更前整行快照',
+    new_json TEXT COMMENT '变更后整行快照',
+    changed_by BIGINT NOT NULL,
+    change_reason VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    KEY idx_rule (rule_code, created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则变更历史';
+```
+
+#### 3.6.3 统一发放入口 `CfBalanceService.earnByRule`
+
+执行顺序(任一步失败即不发):
+1. 读规则(按 `code` + 当前时间落在 `effective_from/to` 内)→ `enabled=0` 直接返回
+2. **上限校验**:`daily_limit_per_member`(按日聚合当日流水)、`total_limit_per_member`(按 `total_earned` 或累计)、`cooldown_seconds`(查最近一笔流水时间)
+3. **计算数量**:`fixed` 取 `amount`;`formula` 用 `params_json` 求值(公式白名单函数:`floor`/`amt`/`qty`/`cfg`);`per_item` 由 `ctx` 提供逐人数量
+4. 幂等写入(`(member_id, dedup_ref_type, ref_id)` 唯一键)+ 写 `cf_balance_log`
+5. 发送通知
+
+> 硬约束:`points_multiplier`、`product_platform_points_share`、`commission_service_rate`、`share_read_reward_points`、`gut_health_project_price` 等散落在 `sys_config`/`products` 的 CF 参数,一律迁入 `cf_earn_rule.params_json` 或作为 `formula` 参数,避免规则散落两处。`cf_rate_tier` **废弃停用**(保留只读),替代者 `cf_direct_sales_tier` / `cf_indirect_sales_tier`(§4.2)。
+
+#### 3.6.4 管理端
+
+- `AdminCfEarnRuleController`:`/list`(分页)/`/save`(新建+编辑)/`/toggle`(开关)/`/history`(变更历史)
+- `cfc-web` 新增 `views/admin/CfEarnRuleManage.vue`:规则列表 + 开关 + 编辑抽屉(名称/类型/数量或公式/参数/三道闸/生效期)+ 变更历史抽屉
+- 权限:仅 admin 角色;修改即时生效(无需重启),历史可追溯
+
+#### 3.6.5 24 条规则种子初始值
+
+见 §三 各条「参数来源」列,初始值按现状录入,**F06、F18 两处按修正后数值录入**(见 §九)。
+
+**归口说明**:CF 规则本身的参数(比例、档位、基数、窗口、三道闸)→ 挂 `cf_earn_rule.params_json`(§3.6.3);CF 通用运营参数(如 `commission_service_rate`、`points_multiplier`、`share_read_reward_points` 等散落项)→ 迁到 `service_config` 表(§3.6.6 L2)。**两类均走 L2 后台 UI,禁止散落常量或裸改库**。
+
+#### 3.6.6 配置治理 SOP(Phase 1-2 全程遵守)⚠️
+
+**核心原则(本次调整):CF 值相关的一切业务/算法参数一律归入 L2(`service_config` + `cf_earn_rule`),L1 常量类只保留与 CF 算法无关的纯订单/技术参数,L3 环境变量不变。CF 参数不再硬编码,一律通过 `ConfigService.getConfig(key)` 读取(含缓存)。**
+
+| 层 | 位置 | 谁能改 | 谁读 | 适用范围 |
+|---|---|---|---|---|
+| L1 硬编码常量 | Java 常量类 | 开发者(改代码) | 全局 | **仅**订单/锁/超时等纯技术参数,与 CF 值算法无关 |
+| **L2 数据库配置** | **`service_config` 表 + `cf_earn_rule` 表** | **超级管理员(`/admin/configs` 后台 UI)** | **`ConfigService.getConfig(key)` + `CfEarnRuleService`(含缓存)** | **CF 值全部业务/算法参数**(含 24 条规则种子) |
+| L3 环境变量 | `.env` / 系统属性 | 运维(重启生效) | Spring `@Value` | DB 连接、第三方 API key |
+
+**新增配置的标准动作**:
+
+1. **CF 业务/算法参数 → 一律 L2**:包括任何涉及积分、分润、佣金、比例、档位、有效期、扣减策略、价格系数的配置,无论是否"当前基本不变",都放 `service_config` 或 `cf_earn_rule`
+2. **CF 无关的纯技术参数 → 放 L1 常量**:订单锁 TTL、请求超时、缓存 TTL 等,与 CF 值算法无耦合
+3. **基础设施 → 放 L3 环境变量**:DB/密钥
+4. **禁止直接改数据库**:所有 L2 改动走 `/admin/configs` UI 或 seed SQL(Flyway 初始化),修改必须走审计(W0.3)
+
+### W0.1 CF 值配置清单(全部入 L2)
+
+**(1)`cf_earn_rule.params_json` 承载(挂到对应规则行)**
+
+见 §3.6.3 与 §三 各条「参数来源」列,24 条规则(F01–F24)的**比例档位、基数口径、时间窗口、三道闸、生效期**全部落在 `cf_earn_rule.params_json`,不再散落到 `sys_config`/`products`。
+
+**(2)`service_config` 承载(CF 通用运营参数,key 前缀 `cf.`)**
+
+| 配置项 | config key | 默认值 | 说明 |
+|---|---|---|---|
+| 积分倍数(消费 → CF 换算) | `cf.rate.points_multiplier` | 1.0 | 原散落 sys_config |
+| 复购率默认系数 | `cf.rate.repeat_rate_default` | 1.0 | 未匹配规则时不加权 |
+| 佣金结算周期 | `cf.commission.settle_days` | 7 | 财务口径常用 |
+| 邀请码有效期 | `cf.invite.valid_days` | 7 | 兼顾转化与时限 |
+| 默认价格系数 | `cf.price.default_factor` | 1.0 | 未定价时原价 |
+| 库存扣减策略 | `cf.stock.deduction_mode` | `immediate` | immediate/on_pay 由 Phase 4 决定 |
+| 分享阅读奖励积分 | `cf.rate.share_read_reward_points` | 现有值 | 原散落 sys_config |
+| 佣金服务费率 | `cf.commission.service_rate` | 现有值 | 原散落 sys_config |
+| 肠道健康项目定价 | `cf.product.gut_health_price` | 现有值 | 原散落 products |
+| 产品平台积分分成比例 | `cf.product.platform_points_share` | 现有值 | 原散落 products |
+
+**(3)缓存与变更**
+
+- 每次 CF 参数改动后,通过 `CfConfigCache.invalidate()` 清缓存(可挂在 `CfEarnRuleController.save/toggle` 与 `AdminConfigController.save` 的 AOP 上)
+- 版本号:`service_config.cf_version` 单调递增,前端可根据版本号做局部刷新提示
+- 读路径:`ConfigService.getConfig("cf.rate.points_multiplier")` → 命中本地缓存(TTL 60s)→ 失效回源 DB → 兜底 Java 常量
+
+**(4)L1 常量类仅保留的技术参数(不进 L2)**
+
+这些项**与 CF 值算法无耦合**,属订单/请求/缓存基础设施,仍留在 Java 常量类:
+
+| 配置项 | 常量类 | 默认值 |
+|---|---|---|
+| 订单超时取消 | `OrderConstants.ORDER_TIMEOUT_MIN` | 30 分钟 |
+| 库存锁时间窗口 | `OrderConstants.STOCK_LOCK_WINDOW_MS` | 30 分钟 |
+| 库存锁 TTL | `OrderConstants.STOCK_LOCK_TTL_SEC` | 20 分钟 |
+| 锁超时告警 | `OrderConstants.STOCK_LOCK_WARN_AFTER_MIN` | 20 分钟 |
+| 订单状态刷新 TTL | `OrderConstants.ORDER_STATUS_REFRESH_SEC` | 60 秒 |
+| 后台 API 超时 | `AdminConstants.ADMIN_API_TIMEOUT_MS` | 5000 |
+| 前端请求超时 | 前端常量 | 15000 |
+
+### W0.2 双配置中心合并(`service_config` ← `product_config`)
+
+CF 参数归 L2 后,product 侧遗留的 `product_config` 也应并入,形成**唯一的 `service_config` 入口**。
+
+| 步骤 | 动作 | 负责人 |
+|---|---|---|
+| 1 | 备份 `product_config` 全表数据(含审计日志) | 运维 |
+| 2 | 写 Flyway 脚本:`ALTER TABLE service_config ADD COLUMN product_key VARCHAR(64)` | 开发者 |
+| 3 | 批量 `INSERT INTO service_config` 从 `product_config` 迁数据,`product_key` 填对应产品标识 | 开发者 |
+| 4 | 改 `ProductConfigServiceImpl` 从查 `product_config` 改为查 `service_config WHERE product_key = ?` | 开发者 |
+| 5 | 保留 `product_config` 表 30 天只读(回滚用),第 31 天删除表 | 运维 |
+| 6 | 前端配置管理页只保留一个"配置中心"入口 | 前端 |
+
+### SOP 检查清单(Phase 1-2 每个新配置项落地前问一遍)
+
+- [ ] 这是 CF 值相关参数吗?是 → 一律 L2(`service_config` 或 `cf_earn_rule.params_json`)
+- [ ] 如果是 L2 配置,key 是否符合 `cf.{module}.{key}` 命名规则?
+- [ ] 管理员 UI 有没有入口(`/admin/configs` 或 `/admin/cf-earn-rules`)?
+- [ ] 改完后缓存是否失效(`CfConfigCache.invalidate` 覆盖到了吗)?
+- [ ] 有没有走审计(W0.3 的 AOP 切面拦截到了吗)?
+- [ ] 是否属于 CF 无关的纯技术参数(订单/锁/超时)?是 → 才允许放 L1 常量
+- [ ] 是否属于基础设施(DB/API key)?是 → 放 L3 环境变量
+
+**两套配置现状(合并背景)**:
+- `ProductConfigItem`(product 侧):与 product 服务绑定的价格/参数/开关
+- `ServiceConfigItem`(service 侧):与 cf 服务绑定的价格/参数/开关
+- 二者 schema 高度相似(groupKey + configKey + configValue + valueType),**Phase 1-2 期间统一到 `service_config` 一张表**,product 侧只留 `product_key` 关联键。
+
+### 3.7 待办(W 项)
+
+#### W0.1 CF 值配置全部归口 L2
+- **交付物**:
+  - Flyway seed SQL:把 §3.6.6 的 10 项 CF 通用运营参数写入 `service_config`(key 前缀 `cf.`)
+  - `ConfigService.getConfig(key)` 读取封装(含本地缓存 + TTL 60s + `CfConfigCache.invalidate()` 主动失效钩子)
+  - 迁移脚本:把散落在 `sys_config`/`products` 的 CF 参数(`points_multiplier`、`product_platform_points_share`、`commission_service_rate`、`share_read_reward_points`、`gut_health_project_price`)迁到 `service_config`
+  - L1 常量类:`OrderConstants.java` / `AdminConstants.java`(**仅**订单/锁/超时等 CF 无关的技术参数)
+- **位置**:
+  - Flyway:`cfc-backend/src/main/resources/db/migration/`
+  - Java:`backend/src/main/java/com/cfc/backend/config/ConfigService.java` + `backend/src/main/java/com/cfc/backend/constant/`
+- **验收**:
+  - Phase 1 结束:**任何 CF 业务/算法参数**只能通过 `ConfigService.getConfig("cf.*")` 或 `CfEarnRuleService` 读取,禁止硬编码业务数值(比例、档位、天数、系数、开关、价格)
+  - `sys_config`/`products` 中 CF 相关参数已清空并归档
+  - 每次 L2 修改走审计(W0.3)+ 缓存失效钩子被触发
+
+#### W0.2 `service_config` 与 `product_config` 合并
+- **交付物**:Flyway 迁移脚本 + `ProductConfigServiceImpl` 改造 + 前端配置管理页合并
+- **迁移路径**:见 §3.6.6 六步流程
+- **验收**:Phase 2 结束时 `product_config` 表已下线,配置中心只有一个入口(`/admin/configs`)
+
+#### W0.3 审计日志统一
+- **交付物**:`audit_log` 表 + AOP 切面(所有 @Admin 修改操作自动落审计,含 `CfEarnRuleController.save/toggle` 与 `AdminConfigController.save`)
+- **验收**:管理员每次配置/规则改动在后台可追溯(谁、何时、改前、改后),且审计触发时同步调用 `CfConfigCache.invalidate()`
+
+---
+
+## 四、推荐分润设计(N7 核心改造)
+
+### 4.1 两级销售奖(取代原团队规模阶梯)
+
+#### 4.1.1 直接销售奖(F03a)
+
+| 维度 | 规则 |
+|---|---|
+| 基数 | 直接推荐人的消费 P 点(即被 A 直接推荐的人 B,B 消费产生的 P 点) |
+| 比例档位 | 按 **A 的团队(8 层以内)年度累计 P 点** 分档 |
+| 最高比例 | **70%** |
+| 累计窗口 | **滚动一年**(按消费时间),累计单位 **P 点**(非金额) |
+| 降级 | **支持**:滚动窗口内 P 点累计下降则降档 |
+| 落账 | A 的 `cf_balance` |
+
+#### 4.1.2 间接销售奖(F03b)
+
+| 维度 | 规则 |
+|---|---|
+| 基数 | 间接推荐人的消费 P 点(即 B 的消费 P 点,由 A 的间接推荐链路上溯) |
+| 比例档位 | 按 **A 自身年度累计消费 P 点** 分档 |
+| 最高比例 | **20%** |
+| 累计窗口 | 滚动一年,累计单位 P 点 |
+| 降级 | 支持 |
+| 落账 | A 的 `cf_balance` |
+
+### 4.2 档位配置表设计
+
+#### `cf_direct_sales_tier`(直接销售奖档位)
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_direct_sales_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL COMMENT '档位名称',
+    min_annual_ppoints INT NOT NULL COMMENT '团队8层内年度累计P点下限',
+    rate_percent INT NOT NULL COMMENT '分润比例(%)',
+    sort_order INT NOT NULL DEFAULT 0,
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_min (min_annual_ppoints)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直接销售奖档位(按团队8层内年度P点累计)';
+```
+
+#### `cf_indirect_sales_tier`(间接销售奖档位)
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_indirect_sales_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL,
+    min_self_ppoints INT NOT NULL COMMENT '自身年度累计消费P点下限',
+    rate_percent INT NOT NULL COMMENT '分润比例(%)',
+    sort_order INT NOT NULL DEFAULT 0,
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_min (min_self_ppoints)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='间接销售奖档位(按自身年度消费P点累计)';
+```
+
+> 档位数值由运营在管理端配置,不在此方案硬编码。两表均需 `enabled` 字段与 `cf_earn_rule_history` 留痕。
+
+### 4.3 与旧体系的迁移
+
+| 旧体系 | 处理 |
+|---|---|
+| `cf_rate_tier`(团队规模阶梯,5 档 5/10/15/20/25%) | **废弃**:表保留只读,停止引用 |
+| `commission_dist_log`(旧 L1/L2 分佣日志) | 保留只读,停止写入 |
+| 直接销售奖与间接销售奖 | 按新档位表计算,写入 `cf_balance` + `cf_balance_log` |
+
+### 4.4 团队 8 层查询
+
+`cf_referral_service.getTeamAnnualPpoints(referrerId, 8)`:按 `referral_tree` 取 referrerId 向下 8 层所有成员,统计其**最近一年消费产生的 P 点累计**。P 点来源:`PpointConfigService.getEffectivePpoint()` × 实付金额。需新增 P 点消费日志表 `ppoint_consumption_log`(如不存在)或聚合 `orders.ppoint_amount`。
+
+### 4.1 现状双轨
+
+| | 新体系 | 旧体系 |
+|---|---|---|
+| 服务 | `CfCommissionService.distributeToReferrers` | `CommissionDistService.settleTwoLevel`(`@Deprecated` 仍被 5 个订单服务调用) |
+| 比例来源 | `cf_rate_tier` 团队规模阶梯(5%/10%/15%/20%/25%) | 推荐人**财富成长等级** `PromotionTierService.getCurrentTier().wealthPercent` |
+| 基数 | 商品=`有效P点`;非商品=`金额×serviceRateBps` | `sourcePpoint`(商品 P 点) |
+| 层级 | 只返第一个非同家庭引荐人 | L1 + L2 各发一次 |
+| 落库 | `platform_points_service.earn` + `cf_transfer_record` | `platform_points_service.earn` + `commission_dist_log` |
+
+### 4.2 合并结论:**统一走团队规模阶梯,废弃旧体系**
+
+理由:`cf_rate_tier` 是按团队规模可配置的比例表,与"财富成长等级"(财富积累折算)是两套互斥的比例模型;2026-09-01 设计 D1 已明确"纯团队规模阶梯"为确认决策,旧体系标记废弃却未收口属实现遗漏。
+
+**改造动作**:
+1. `CommissionDistService.settleTwoLevel` 全库调用点(套餐/测评/会员/订阅 5 处)改调 `CfCommissionService.settle` / `settleReferrerOnly`。
+2. `CommissionDistService` 保留读接口、写接口标记 `410 Gone`,`commission_dist_log` 转只读历史表。
+3. `PromotionTierService` 的 `wealthPercent` 从分佣链路移除,仅保留在等级展示/权益场景。
+4. 幂等:`cf_rate_tier` 阶梯比例变更后按新比例计算,不影响历史流水。
+
+### 4.3 合并后被取消的途径(不计入 24 条)
+
+| 原途径 | 处理 |
+|---|---|
+| 旧 L1 推荐人分润(`CommissionDistService.java:152`) | 并入 F03 |
+| 旧 L2 推荐人分润(`CommissionDistService.java:156`) | 并入 F03 |
+| 家庭池商品返积分(`ProductOrderService.java:1565`) | 改投购买人 member,合并到 F01 |
+
+---
+
+## 五、目标数据模型
+
+### 5.1 新建 `cf_balance`(成员维度个人 CF 值)
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_balance (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    user_id BIGINT COMMENT '关联登录用户ID(可为空,孩子无账号)',
+    family_id BIGINT COMMENT '所属家庭ID(冗余,便于聚合)',
+    total_earned INT NOT NULL DEFAULT 0 COMMENT '累计获得',
+    available INT NOT NULL DEFAULT 0 COMMENT '可用',
+    frozen INT NOT NULL DEFAULT 0 COMMENT '提现冻结',
+    withdrawn INT NOT NULL DEFAULT 0 COMMENT '已提现',
+    exchanged INT NOT NULL DEFAULT 0 COMMENT '已兑换消费',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_member (member_id),
+    KEY idx_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值余额(家庭成员维度)';
+```
+
+### 5.2 新建 `cf_balance_log`
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_balance_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL,
+    user_id BIGINT,
+    family_id BIGINT,
+    type VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust',
+    ref_type VARCHAR(50) NOT NULL COMMENT '见 F02-F24 编码',
+    ref_id BIGINT,
+    amount INT NOT NULL,
+    balance_after INT NOT NULL,
+    remark VARCHAR(255),
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    KEY idx_member (member_id),
+    KEY idx_family (family_id),
+    UNIQUE KEY uk_ref (member_id, ref_type, ref_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值流水';
+```
+
+幂等:`(member_id, ref_type, ref_id)` 唯一键,重复发放直接拦截。
+
+### 5.3 优惠券家庭化(N3)
+
+- `coupon` / `user_coupon` 增加 `family_id`(`2026-08-31-coupon-family-based-design.md` 已设计,本次落地)。
+- 兑换:个人扣 CF(`cf_balance.available` → `exchanged`)→ 发券挂 `family_id` → 家庭内任意成员可用。
+- 退券:CF 返还**原兑换成员**(非家庭池)。
+
+### 5.4 消费侧改造
+
+| 消费场景 | 旧 | 新 |
+|---|---|---|
+| 会员续费抵扣 | 家庭池 `FamilyPlatformPointsService` | `cf_balance`(支付人 member) |
+| 商品购买抵扣 | 家庭池 | `cf_balance`(购买人 member) |
+| 优惠券兑换 | 家庭池(`exchangeCouponByCf`) | `cf_balance` 扣款 + 券挂家庭 |
+| 提现 | 家庭池 `WithdrawalService` | `cf_balance`(个人申请、个人提现) |
+| 家庭内部许愿兑换 | `system_points`(`deductCfPoints`) | `cf_balance` |
+| 管理员手动调整 | 家庭池 `adjust` | `cf_balance` `adjust` |
+
+---
+
+## 六、存量迁移(D2)
+
+一次性脚本,`DatabaseInitializer.runMigrations()` 幂等执行(新增迁移编号,接续现有最大编号):
+
+1. `family_platform_balance` → 家庭 `creator` 的 `family_member` → `cf_balance`(`available + frozen` 合入 `available`,`withdrawn`/`exchanged` 原样带入),写 `cf_balance_log`(`ref_type='migration_family_pool'`,`ref_id=0`,幂等)。
+2. `family_members.system_points > 0` → 各成员 `cf_balance`,写流水(`ref_type='migration_system_points'`)。
+3. `user_platform_balance.available + frozen` → 用户自己的 `family_member` 对应 `cf_balance`,写流水(`ref_type='migration_user_wallet'`)。
+4. 三类流水 `ref_type` 独立,天然幂等,可重复执行不重复入账。
+5. 迁移后 `family_platform_balance` / `family_members.system_points` / `user_platform_balance` 三表**保留为只读历史**,写接口标记 `410 Gone`。
+
+---
+
+## 七、派发批次(批 F,待 A 批 3 会话完成后派发)
+
+| 子批 | 内容 | 依赖 | 验收 |
+|---|---|---|---|
+| **F-1 数据层** | 建 `cf_balance` + `cf_balance_log`、`cf_earn_rule` + `cf_earn_rule_history`、`cf_direct_sales_tier`、`cf_indirect_sales_tier`、`cf_earn_daily_limit`(会员等级日限映射);`coupon`/`user_coupon` 补 `family_id`;同步 `schema.sql`;`DatabaseInitializer` 新增迁移 | 无 | `mvn clean compile` 通过;迁移幂等 |
+| **F-2 服务层** | 新建 `CfBalanceService`(`earn/spend/freeze/unfreeze/withdraw/adjust/getBalance/getLogs`,`SELECT ... FOR UPDATE` 锁行,`(member_id,ref_type,ref_id)` 幂等)+ **`earnByRule(memberId, ruleCode, refId, ctx)` 统一发放入口**(读规则 → 会员等级日限/冷却校验 → 计算数量 → 幂等写入) | F-1 | 单测覆盖 earn/spend/幂等拦截/等级日限 |
+| **F-3 发放接入** | 24 条途径全部改投 `earnByRule`:F02 消费返 CF(1元=100CF)、F03a/F03b 两级销售奖、F04-F06、F07-F24(见 §三);F08/F18 硬编码外置到 `cf_earn_rule`;F09 报告去重 | F-2 | 每条途径有流水、有 refType、可追溯;F09 第 1 份 50/后续 10 |
+| **F-4 分佣收口** | §四改造:`cf_rate_tier` 停用;新建直接/间接销售奖档位表并实现 `getTeamAnnualPpoints(referrerId, 8)` / `getSelfAnnualPpoints(referrerId)`;`CommissionDistService` 写接口标 410 | F-2 | 两级分润按新档位计算;滚动一年可降级 |
+| **F-5 消费侧** | 会员续费/商品抵扣/券兑换/提现/许愿兑换/管理员调整 6 处改投 `cf_balance`;券挂家庭 | F-2 | 6 场景端到端可走通 |
+| **F-6 存量迁移** | §六脚本,三类迁移流水幂等 | F-1 | 迁移前后总 CF 值守恒校验 |
+| **F-7 前端** | `wealth-cf` 页改个人 CF 值视图(我的 CF 值 + 家庭共享优惠券);`wealth` 页五维「富」维度中"财务积累"改读 `cf_balance.total_earned`(现读家庭池 `totalEarned`,`EnergyService.java:457`);管理端新增规则/档位配置页面 | F-3/F-5 | 页面余额与流水一致 |
+| **F-8 清理** | `PointsService`/`PlatformPointsService`/`FamilyPlatformPointsService` 写接口标 410 并注释;清理 `system_points` "CF值"误导性注释;补《CF 值获取/消耗单一事实源》文档 | F-1~F-7 | 全库无"CF值=家庭池"残留表述 |
+
+---
+
+## 八、待确认点(不阻塞 F-1/F-2)
+
+| # | 问题 | 建议 |
+|---|---|---|
+| Q1 | F10 家庭挑战奖励"每个完成成员各发一次"还是"家庭合计一次"?现实现为**每个成员各发**,数量可能放大 | 建议改为家庭合计一次、发给挑战发起人 |
+| ~~Q2~~ | ~~F01 商品返积分倍数是否保留~~ | **已定:F01 删除,统一走 F02 消费返 CF(N6)** |
+| Q3 | `wealth` 页五维「富」维度中"财务积累"(现 5000CF=100 分)改读个人 CF 后,家长维度如何聚合?家庭成员 CF 合计还是仅本人? | 建议:家长本人 CF + 家庭其他成员 CF 合计(体现"家庭共同富裕") |
+| Q4 | 提现是否仍支持(N2 改为个人后,个人 CF 直接提现涉及合规审查) | 建议本次保留功能但默认关闭,走管理端开关 |
+| Q5 | 存量迁移后历史流水是否需要在新流水页可查 | 建议:只在新流水页展示迁移后的流水,历史三表保留独立查询入口 |
+| ~~Q6~~ | ~~F01 是否关停(与 F02 重叠)~~ | **已定:F01 删除(N6)** |
+| ~~Q7~~ | ~~F03 非商品基数是否统一~~ | **已定:F03 改为两级销售奖,基数=消费 P 点(N7)** |
+| ~~Q8~~ | ~~F06 的 1990 CF 是否已知晓~~ | **已定:1 元=100 CF 口径下 1990 恰自洽(N6),兜底常量去除改为公式** |
+| ~~Q9~~ | ~~F19 管理员调整是否必须审批+留痕~~ | **已定:必须留痕(N11 体系下 admin 调整写入 `cf_balance_log` + `cf_earn_rule_history`)** |
+| Q10 | 直接销售奖/间接销售奖的**档位数值**(如铜/银/金/钻对应多少 P 点、各档比例) | 建议运营在管理端配置,档位表已在 §4.2 设计 |
+| Q11 | **FREE 免费会员**任务/打卡每日次数上限(N11 未提及) | 建议 1 次/天(最低体验) |
+| Q12 | **PROVIDER 标准会员**是否纳入 N11 次数体系(该等级为规划师/专家向) | 建议不纳入(非消费端会员),或按 20 次/天对齐 PREMIUM |
+| Q13 | **分享奖励(F24)数量**:推荐注册得多少 CF(N8 未给数值) | 建议 20 CF/人,日限 10 次 |
+
+---
+
+## 九、逐条数值核算(新口径,供修正评估)
+
+按 **1 元 = 100 CF**(N6)新口径核算:
+
+| # | 途径 | 示例场景 | 新数值 | 问题/备注 |
+|---|---|---|---|---|
+| F02 | 消费返 CF | ¥199 实付 | 199 × 100 = **19,900 CF** | 所有消费都返(N6) |
+| F03a | 直接销售奖 | 团队年度 P 点 1000,直接推荐人消费 P 点 500 | 按档位表计算(比例待定) | 档位待运营配置(Q10) |
+| F03b | 间接销售奖 | 自身年度消费 P 点 800,间接推荐人消费 P 点 300 | 按档位表计算(比例待定) | 同上 |
+| F04 | 商品赠品 | 配置 | 按赠品规则 | 管理端配置 |
+| F05 | 活动签到 | 配置 | 按活动配置 | 管理端配置 |
+| F06 | 菌群打卡返还 | 项目价 19.9 元 | 19.9 × 100 = **1,990 CF** | 与旧兜底值 1990 自洽(N6 修正) |
+| F07 | 文章答题 | 3 题全对 | 3+3 = **6 CF**;**日限 5 次** | N9 已定日限 |
+| F08 | AI 阅读答题 | 全对 | **5 CF** | 建议日限 3 次 |
+| F09 | 测评建档 | 第 1 份 | **50 CF**;第 2 份起 **10 CF**;重复不计 | N10 已定 |
+| F10 | 家庭挑战 | 3 成员各完成 | `reward_points × 3`(待确认改家庭合计) | Q1 待确认 |
+| F11 | 邀请里程碑 | 配置 | 按里程碑配置 | — |
+| F12 | 成长任务 | 配置 | 按任务模板配置 | — |
+| F13 | 分享阅读 | 默认 | **5 CF** | 建议日限 5 次 |
+| F14 | 喝水打卡 | 每日 | **5 CF/次**;日限:BASIC=3/FAMILY=10/LIFETIME=10/PREMIUM=20 | N11 已定按会员等级 |
+| F15 | 饮食打卡 | 每日 | **5 CF/次**;同上 | 同上 |
+| F16 | 每日健康打卡 | 每日 | **10 CF/次**;同上 | 叠加 F14+F15=20 CF/天(BASIC 档 3 次即 60 CF/天,PREMIUM 档 20 次即 400 CF/天) |
+| F17 | 挑战赛子任务 | 配置 | `reward_cf` | 需补流水(原 P1) |
+| F18 | 健康启航付费 | 按实付 | `floor(实付/100) × 100` | 改为与 F02 同口径 |
+| F19 | 管理员调整 | 任意 | 正负任意,需密码 | 需审批+留痕(Q9 已定) |
+| F20 | 问题域完成 | 配置 | `domain.reward_cf_value` | 管理端配置 |
+| F21 | 关卡通关 | 配置 | `gate.reward_points` | 管理端配置 |
+| F22 | 任务完成 CF | `task.cf_amount` | 保留个人;**日限按会员等级** | N11 已定 |
+| F23 | 成员间转让 | 转入+X/转出−X | 同事务 | — |
+| **F24** | **分享奖励(推荐注册)** | 推荐 1 人注册 | 建议 **20 CF**;日限 10 次 | N8 新增,数量待定(Q13) |
+
+### 建议初始配置值(待你修正)
+
+| 规则 | 建议数量 | 建议 daily_limit | 建议 total_limit | 说明 |
+|---|---|---|---|---|
+| F02 | 1 元 = 100 CF | — | — | 全消费返 |
+| F06 | 项目价 × 100 | 7 | 1 | 一周项目只发一次 |
+| F07 | `score + (全对?3:0)` | **5 次(N9)** | — | 按篇去重由 refId 保证 |
+| F08 | 5 | 3 | — | 限制每日刷题 |
+| F09 | 第 1 份 50 / 后续 10 | 3 | — | 重复报告不计(N10) |
+| F10 | 保留配置值 | 1 | 1 | 建议改家庭合计一次 |
+| F13 | 5 | 5 | — | 每日分享上限 5 篇 |
+| F14/F15/F16 | 5 / 5 / 10 | **按会员等级(N11)** | — | BASIC=3,FAMILY/LIFETIME=10,PREMIUM=20 |
+| F18 | `floor(实付/100) × 100` | 1 | 1 | 与 F02 同口径 |
+| F24 | 20(建议) | 10 | — | 分享注册奖励(N8) |