Răsfoiți Sursa

chore: auto bump version and changelog [skip ci]

iwt 1 săptămână în urmă
părinte
comite
46d8909653

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

@@ -10813,6 +10813,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         addProductMinDiscountLevel();
         restoreBasicMembershipLevel();
         backfillMembershipLevelOrder();
+        fixMembershipLevelOrder();
         removeReportUploadQuota();
 
         createProductDimensionMappingTable();
@@ -11015,15 +11016,15 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         }
 
         // 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
-        // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
+        // - membership_levels.level_order: 级别数字(数字越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)
         // - products.min_visible_level: 最低可见会员级别编码(NULL=所有用户含未登录可见)
         // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=所有用户含未登录可见)
         ensureColumn("membership_levels", "level_order",
-                "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5'");
+                "INT DEFAULT 1 COMMENT '级别数字(越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)'");
         try {
             jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
-                    "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
-                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
+                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 " +
+                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 WHEN 'PROVIDER' THEN 1 ELSE level_order END");
             log.info("迁移321: 已回填 membership_levels.level_order");
         } catch (Exception e) {
             log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
@@ -11637,21 +11638,38 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
     }
 
     // 迁移351: membership_levels.level_order 顺延,为 BASIC 让出独立档位
-    // 会员级别包含关系(数字越小级别越低、可见范围越大): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6
+    // 会员级别包含关系(数字越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5
+    // PROVIDER 为服务商独立角色(不走会员权益),与 FREE 同级=1;最终值由迁移381 fixMembershipLevelOrder 兜底
     private void backfillMembershipLevelOrder() {
         try {
             int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
-                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 4 " +
-                    "WHEN 'PREMIUM' THEN 5 WHEN 'LIFETIME' THEN 6 ELSE level_order END");
-            log.info("迁移351: 已回填 membership_levels.level_order({} 行):FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6", updated);
+                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 1 " +
+                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
+            log.info("迁移351: 已回填 membership_levels.level_order({} 行):FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)", updated);
         } catch (Exception e) {
             log.warn("迁移351: 回填 membership_levels.level_order 失败: {}", e.getMessage());
         }
     }
 
+    // 迁移381: 修正 membership_levels.level_order(迁移351/360 执行顺序导致的 FAMILY 被重置为 2 的问题;
+    // 同时 PROVIDER 为服务商独立角色,不走会员权益,降至与 FREE 同级,避免其参与 getMemberLevel / resolveEffectiveLevel 的等级比较)
+    // level_order 语义:数字越大级别越高; FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)
+    private void fixMembershipLevelOrder() {
+        try {
+            int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
+                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 " +
+                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 WHEN 'PROVIDER' THEN 1 ELSE level_order END " +
+                    "WHERE level_code IN ('FREE','BASIC','FAMILY','PREMIUM','LIFETIME','PROVIDER')");
+            log.info("迁移381: 已修正 membership_levels.level_order({} 行):" +
+                    "FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5 PROVIDER=1(非会员)", updated);
+        } catch (Exception e) {
+            log.warn("迁移381: 修正 membership_levels.level_order 失败: {}", e.getMessage());
+        }
+    }
+
     /**
      * 迁移360: 恢复 9.9 元/月 BASIC 体验会员等级与权益(幂等)。
-     * 反向还原提交 f86dd164b 删除的内容;level_order 由 backfillMembershipLevelOrder 统一回填。
+     * 反向还原提交 f86dd164b 删除的内容;level_order 由 fixMembershipLevelOrder(迁移381)统一回填。
      */
     private void restoreBasicMembershipLevel() {
         try {

+ 18 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -332,7 +332,7 @@ public class MembershipController {
     }
 
     /**
-     * 获取任务模板价格列表
+     * 获取会员方案价格列表(购买档位;已过滤不可购买的 LIFETIME 与服务商角色 PROVIDER)
      */
     @Operation(summary = "获取会员方案价格列表")
     @PostMapping("/plans")
@@ -340,8 +340,8 @@ public class MembershipController {
         List<MembershipLevelDTO> levels = membershipService.getAllLevels();
         List<Map<String, Object>> plans = new ArrayList<>();
         for (MembershipLevelDTO level : levels) {
-            // LIFETIME 不可购买,过滤不返回
-            if ("LIFETIME".equals(level.getLevelCode())) {
+            // LIFETIME 不可购买、PROVIDER 为服务商独立角色(非会员),均过滤不返回
+            if ("LIFETIME".equals(level.getLevelCode()) || "PROVIDER".equals(level.getLevelCode())) {
                 continue;
             }
             Map<String, Object> plan = new HashMap<>();
@@ -355,11 +355,26 @@ public class MembershipController {
             plan.put("originalYearly", level.getOriginalPriceYearly());
             plan.put("originalMonthly", level.getOriginalPriceMonthly());
             plan.put("originalQuarterly", level.getOriginalPriceQuarterly());
+            // 附加实际权限字段,供购买页对比渲染
+            plan.put("maxChildren", level.getMaxChildren());
+            plan.put("maxTasksPerDay", level.getMaxTasksPerDay());
             plans.add(plan);
         }
         return Result.success(plans);
     }
 
+    /**
+     * 获取会员权益对比表数据(购买页 / 会员中心使用)
+     * 列:FREE / BASIC / FAMILY / PREMIUM(可购档 + 免费;LIFETIME 不可购、PROVIDER 为服务商角色均不展示)
+     * 行来自事实源:membership_levels.max_children/max_tasks_per_day、sys_config 折扣、BasicQuotaService 配额、
+     *               membership_benefits 权益描述;值均为文本,保证与实际权限一致。
+     */
+    @Operation(summary = "获取会员权益对比表数据")
+    @PostMapping("/compare")
+    public Result<Map<String, Object>> getMembershipCompare() {
+        return Result.success(membershipService.getMembershipCompareData());
+    }
+
     /**
      * 处理支付回调
      */

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -112,12 +112,13 @@ public class ActivityDTO {
 
     /**
      * Resolve final price based on membership level.
-     * Members (FAMILY/PREMIUM/PROVIDER) get memberPrice if available, otherwise base price.
+     * Members (FAMILY/PREMIUM) get memberPrice if available, otherwise base price.
+     * 服务商(PROVIDER)为独立角色,不走会员权益,不享受会员价。
      */
     public static Integer resolveMemberPrice(Integer basePrice, Integer memberPrice, String memberLevel) {
         if (basePrice == null) return null;
         // If user has member level and a memberPrice is set, use memberPrice
-        if (memberLevel != null && ("FAMILY".equals(memberLevel) || "PREMIUM".equals(memberLevel) || "PROVIDER".equals(memberLevel))) {
+        if (memberLevel != null && ("FAMILY".equals(memberLevel) || "PREMIUM".equals(memberLevel))) {
             if (memberPrice != null) {
                 return memberPrice;
             }

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

@@ -45,7 +45,7 @@ public class MembershipLevel implements Serializable {
 
     private Integer teacherConsultation;  // 老师咨询
 
-    private Integer levelOrder;  // 级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
+    private Integer levelOrder;  // 级别数字(越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员,服务商独立角色)
 
     private Date createdAt;
 

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

@@ -62,7 +62,7 @@ public class ActivityOrderService {
 
         String memberLevel = null;
         try { memberLevel = membershipService.getMemberLevel(userId); } catch (Exception e) { /* ignore */ }
-        boolean isMember = "FAMILY".equals(memberLevel) || "PROVIDER".equals(memberLevel);
+        boolean isMember = "FAMILY".equals(memberLevel) || "PREMIUM".equals(memberLevel);
 
         int totalAmount = 0;
         List<Map<String, Object>> snapshot = new ArrayList<>();

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

@@ -28,7 +28,7 @@ public class BasicQuotaService {
     private MembershipService membershipService;
 
     // BASIC 会员月度配额上限
-    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    public static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
     static {
         QUOTA_LIMITS.put("AI_CHAT", 3);
         QUOTA_LIMITS.put("REPORT_READING", 1);
@@ -36,6 +36,13 @@ public class BasicQuotaService {
         QUOTA_LIMITS.put("FOOD_RECOGNIZE", 3);
     }
 
+    /**
+     * 按配额类型返回月度上限(0 表示无配额限制)
+     */
+    public static int getQuotaLimit(String quotaType) {
+        return QUOTA_LIMITS.getOrDefault(quotaType, 0);
+    }
+
     /**
      * 查询指定用户指定月份某配额的剩余次数
      */

+ 169 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -16,12 +16,13 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Collections;
 import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
@@ -283,18 +284,17 @@ public class MembershipService implements MembershipServiceInterface {
                     || "mascot_ai".equals(feature);
         }
 
-        // PROVIDER也有全部功能
-        return true;
+        // 服务商(PROVIDER)为独立角色,不走会员权益体系;未知等级按无权限处理
+        return false;
     }
 
     /**
      * 检查用户是否有指定功能的权限
      * <p>
      * 根据设计文档 Section 5.2:
-     * 1. 获取用户 memberLevel
+     * 1. 获取用户 memberLevel(getMemberLevel 已归一化:PROVIDER 服务商身份不构成会员等级,按 FREE 处理)
      * 2. 查 membership_levels 表对应等级的 features JSON
      * 3. 检查 featureCode 是否在 features 数组中
-     * 4. 特殊:PROVIDER 类型还需校验 vendorStatus == 'approved'
      *
      * @param userId     用户ID
      * @param featureCode 功能代码(如 "free_activities", "discount_purchase")
@@ -312,10 +312,6 @@ public class MembershipService implements MembershipServiceInterface {
         );
         if (levelConfig == null || levelConfig.getFeatures() == null) return false;
 
-        if ("PROVIDER".equals(level) && !"approved".equals(user.getVendorStatus())) {
-            return false;
-        }
-
         String featuresJson = levelConfig.getFeatures();
         try {
             // features 格式: ["free_activities","free_courses",...] 或逗号分隔
@@ -1261,6 +1257,10 @@ public class MembershipService implements MembershipServiceInterface {
 
         // 个人会员身份(含有效期校验)
         String personal = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
+        // 服务商(PROVIDER)为独立角色,不构成会员等级身份,不参与会员权益判定
+        if ("PROVIDER".equals(personal)) {
+            personal = "FREE";
+        }
         if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null) {
             if (user.getMemberExpireTime().before(new Date())) {
                 personal = "FREE";
@@ -1275,7 +1275,7 @@ public class MembershipService implements MembershipServiceInterface {
     }
 
     /**
-     * 获取等级编码对应的级别数字(数字越小级别越低),未知等级回退 1(最低)
+     * 获取等级编码对应的级别数字(数字越大级别越高;PROVIDER=1 与 FREE 同级,视为非会员),未知等级回退 1(最低)
      */
     public int getLevelOrder(String levelCode) {
         if (levelCode == null || levelCode.isEmpty()) {
@@ -1681,4 +1681,163 @@ public class MembershipService implements MembershipServiceInterface {
         );
         return benefits;
     }
+
+    // ==================== 权益对比表(购买页/会员中心) ====================
+
+    /**
+     * 生成会员权益对比表数据。
+     * 列:FREE / BASIC / FAMILY / PREMIUM(可购档 + 免费;LIFETIME 不可购、PROVIDER 为服务商角色均不展示)。
+     * 行全部来自事实源:
+     * - 商品折扣:sys_config member_discount_family/premium(bps)
+     * - 成员/任务上限:membership_levels.max_children / max_tasks_per_day
+     * - 月度次数:BasicQuotaService.QUOTA_LIMITS
+     * - 其余权益:membership_benefits 描述 + canUseFeature 功能开关
+     * 值均为展示文本,保证与实际权限一致。
+     */
+    public Map<String, Object> getMembershipCompareData() {
+        List<String> columns = Arrays.asList("FREE", "BASIC", "FAMILY", "PREMIUM");
+
+        // 各等级配置(上限字段)
+        Map<String, MembershipLevel> levelByCode = new HashMap<>();
+        for (String code : columns) {
+            MembershipLevel lv = levelMapper.selectOne(
+                    new LambdaQueryWrapper<MembershipLevel>().eq(MembershipLevel::getLevelCode, code).last("LIMIT 1"));
+            if (lv != null) {
+                levelByCode.put(code, lv);
+            }
+        }
+        // 各等级权益描述(备用,后续可按 benefit_code 维度扩展对比行)
+        List<Map<String, Object>> rows = new ArrayList<>();
+
+        // 1. 商品折扣:FREE=普通价格;BASIC=无;FAMILY=9折;PREMIUM=8折(bps 配置)
+        rows.add(compareRow("商品折扣", columns, code -> {
+            if ("FREE".equals(code)) return "普通价格";
+            if ("BASIC".equals(code)) return "—";
+            String cfgKey = "FAMILY".equals(code) ? "member_discount_family" : "member_discount_premium";
+            return formatDiscountBps(sysConfigService.getValue(cfgKey));
+        }));
+
+        // 2. 家庭成员上限
+        rows.add(compareRow("家庭成员上限", columns, code -> {
+            MembershipLevel lv = levelByCode.get(code);
+            return (lv != null && lv.getMaxChildren() != null && lv.getMaxChildren() > 0)
+                    ? lv.getMaxChildren() + "人" : "—";
+        }));
+
+        // 3. 每日任务上限
+        rows.add(compareRow("每日任务上限", columns, code -> {
+            MembershipLevel lv = levelByCode.get(code);
+            return (lv != null && lv.getMaxTasksPerDay() != null && lv.getMaxTasksPerDay() > 0)
+                    ? lv.getMaxTasksPerDay() + "个/日" : "—";
+        }));
+
+        // 4. 优惠券:FREE=—;BASIC=平台券;FAMILY/PREMIUM=平台+会员券
+        rows.add(compareRow("优惠券权益", columns, code -> {
+            if ("FREE".equals(code)) return "—";
+            if ("BASIC".equals(code)) return "平台优惠券";
+            return "平台券+会员专享券";
+        }));
+
+        // 5. AI健康顾问:BASIC 限次,FAMILY+ 不限
+        rows.add(compareRow("AI健康顾问", columns, code -> {
+            if ("FREE".equals(code)) return "—";
+            if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("AI_CHAT") + "次";
+            return "不限次数";
+        }));
+
+        // 6. 报告解读:BASIC 限次,FAMILY 不限,PREMIUM 深度
+        rows.add(compareRow("报告解读", columns, code -> {
+            if ("FREE".equals(code)) return "—";
+            if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("REPORT_READING") + "次";
+            if ("PREMIUM".equals(code)) return "不限·深度解读";
+            return "不限次数";
+        }));
+
+        // 7. 测评预约:FREE 仅查看;BASIC 限次;FAMILY 不限;PREMIUM 优先
+        rows.add(compareRow("测评预约", columns, code -> {
+            if ("FREE".equals(code)) return "仅查看";
+            if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("ASSESSMENT_QUOTA") + "次";
+            if ("PREMIUM".equals(code)) return "不限·优先";
+            return "不限次数";
+        }));
+
+        // 8. 食材图片解析:BASIC 限次,FAMILY+ 不限
+        rows.add(compareRow("食材图片解析", columns, code -> {
+            if ("FREE".equals(code)) return "—";
+            if ("BASIC".equals(code)) return "每月" + BasicQuotaService.getQuotaLimit("FOOD_RECOGNIZE") + "次";
+            return "不限次数";
+        }));
+
+        // 9. 专属活动:FREE/BASIC 部分;FAMILY 全部;PREMIUM 优先
+        rows.add(compareRow("会员专属活动", columns, code -> {
+            if ("FREE".equals(code) || "BASIC".equals(code)) return "部分活动";
+            if ("PREMIUM".equals(code)) return "优先参与";
+            return "全部活动";
+        }));
+
+        // 10. 邀请家人佣金:BASIC+ 可参与
+        rows.add(compareRow("邀请佣金", columns, code -> "FREE".equals(code) ? "—" : "可参与"));
+
+        // 11. 消费返佣:FAMILY+ 可参与
+        rows.add(compareRow("消费返佣", columns, code -> {
+            if ("FREE".equals(code) || "BASIC".equals(code)) return "—";
+            return "可参与";
+        }));
+
+        // 12. 管家服务:FAMILY 月1次;PREMIUM 周1次
+        rows.add(compareRow("专属管家", columns, code -> {
+            if ("FAMILY".equals(code)) return "每月1次";
+            if ("PREMIUM".equals(code)) return "每周1次";
+            return "—";
+        }));
+
+        // 13. 成长规划师咨询:PREMIUM 专属
+        rows.add(compareRow("规划师咨询", columns, code -> {
+            if ("PREMIUM".equals(code)) return "每月1次";
+            return "—";
+        }));
+
+        // 14. 免费课程/活动(全员可用基准项)
+        rows.add(compareRow("免费课程与活动", columns, code -> "可参与"));
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("columns", columns);
+        result.put("rows", rows);
+        return result;
+    }
+
+    /**
+     * 构造一行对比数据:row = {name, <levelCode小写>: 值}。
+     */
+    private Map<String, Object> compareRow(String name, List<String> columns,
+                                           java.util.function.Function<String, String> valueFn) {
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("name", name);
+        for (String code : columns) {
+            String v = valueFn.apply(code);
+            row.put(code.toLowerCase(), v == null ? "—" : v);
+        }
+        return row;
+    }
+
+    /**
+     * 折扣 bps 转展示文本:9000 → "9折",8500 → "8.5折";非法值返回 "—"。
+     */
+    private String formatDiscountBps(String cfgValue) {
+        if (cfgValue == null || cfgValue.isEmpty()) {
+            return "—";
+        }
+        try {
+            int bps = Integer.parseInt(cfgValue);
+            if (bps <= 0 || bps >= 10000) {
+                return "—";
+            }
+            if (bps % 1000 == 0) {
+                return (bps / 1000) + "折";
+            }
+            return String.format(java.util.Locale.ROOT, "%.1f折", bps / 1000.0);
+        } catch (Exception e) {
+            return "—";
+        }
+    }
 }

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

@@ -3986,7 +3986,7 @@ CREATE TABLE IF NOT EXISTS membership_levels (
     ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核功能',
     priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服',
     teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询',
-    level_order INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5',
+    level_order INT DEFAULT 1 COMMENT '级别数字(越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员,服务商独立角色)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     UNIQUE KEY uk_level_code (level_code)

+ 44 - 153
cfc-frontend/pages/membership/benefits.vue

@@ -52,7 +52,6 @@
         <view class="benefit-detail" v-if="item.detail">
           <text class="detail-text">{{ item.detail }}</text>
         </view>
-        <button class="btn-use" v-if="item.canUse" @click="useBenefit(item)">立即使用</button>
       </view>
       
       <view class="empty-state" v-if="availableBenefits.length === 0">
@@ -110,7 +109,7 @@
 </template>
 
 <script>
-import { getMySubscription, checkBenefit } from '../../utils/api.js'
+import { getMyMembership, getMembershipBenefits } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '../../mixins/font-scale.js'
 
@@ -120,99 +119,19 @@ export default {
     return {
       subscription: null,
       activeTab: 'available',
-      benefits: [
-        { 
-          code: 'AI_CHAT_UNLIMITED', 
-          name: '健康顾问', 
-          desc: '7×24 小时不限次数问答',
-          icon: '🤖', 
-          bg: '#DBEAFE',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '支持文字/语音提问,报告解读'
-        },
-        { 
-          code: 'FAMILY_MEMBERS', 
-          name: '家庭档案管理', 
-          desc: '管理最多 10 位家庭成员',
-          icon: '👨‍👩‍👧‍👦', 
-          bg: '#D1FAE5',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '查看全家健康数据,统一规划'
-        },
-        { 
-          code: 'HEALTH_PLAN', 
-          name: '月度健康计划', 
-          desc: '个性化健康改善方案',
-          icon: '📋', 
-          bg: '#FEF3C7',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '基于家庭健康画像自动生成'
-        },
-        { 
-          code: 'REPORT_READING', 
-          name: '报告解读', 
-          desc: '体检/菌群/测评报告 解读',
-          icon: '📊', 
-          bg: '#FCE7F3',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '通俗易懂的解读 + 行动建议'
-        },
-        {
-          code: 'MANAGER_SERVICE',
-          name: '专属健康教练服务',
-          desc: '认证健康教练月度随访',
-          icon: '👨‍🏫',
-          bg: '#FFEDD5',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '15-20 分钟电话随访 + 在线答疑'
-        },
-        {
-          code: 'GROWTH_COACH',
-          name: '7X24小时成长教练',
-          desc: '智能教练随时解答家庭教育问题',
-          icon: '🎯',
-          bg: '#FEF3C7',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '浠宝/福宝助手,随时回答育儿、学习、情绪等问题'
-        },
-        { 
-          code: 'PRODUCT_DISCOUNT', 
-          name: '商品会员折扣', 
-          desc: '全场商品 8-9 折优惠',
-          icon: '🛒', 
-          bg: '#E0E7FF',
-          canUse: true,
-          used: false,
-          usedAt: null,
-          detail: '结算时自动享受会员价'
-        }
-      ]
+      benefits: []
     }
   },
   computed: {
     levelIcon() {
       if (!this.subscription || !this.subscription.level) return '○'
-      if (this.subscription.level === 'L2') return '💎'
-      if (this.subscription.level === 'L1') return '❤️'
-      return '○'
+      var map = { BASIC: '🧡', FAMILY: '👑', PREMIUM: '💎', LIFETIME: '🌟' }
+      return map[this.subscription.level] || '○'
     },
     levelName() {
-      if (!this.subscription || !this.subscription.level) return '未订阅'
-      if (this.subscription.level === 'L2') return '久久一生'
-      if (this.subscription.level === 'L1') return '一生一世'
-      return '注册会员'
+      if (!this.subscription || !this.subscription.level) return '未开通'
+      var map = { FREE: '免费用户', BASIC: '体验会员', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
+      return map[this.subscription.level] || this.subscription.level
     },
     availableBenefits() {
       return this.benefits.filter(function(b) { return !b.used })
@@ -234,80 +153,52 @@ export default {
     this.loadData()
   },
   methods: {
-    async loadData() {
-      try {
-        const res = await getMySubscription()
-        if (res.data) {
-          this.subscription = res.data
-        }
-      } catch (e) {
-        console.error('获取订阅信息失败', e)
-      }
-      this.loadBenefits()
-    },
-    loadBenefits() {
+    // 读取当前用户实际有效会员等级(getMyMembership 返回 memberLevel=个人与家庭身份取高者),
+    // 免费用户展示体验会员权益(与会员中心 index.vue 一致)。
+    loadData() {
       var self = this
-      var token = uni.getStorageSync('token')
-      var baseUrl = ''
-      try {
-        var config = require('../../config.js')
-        baseUrl = config.default.baseUrl || config.baseUrl || ''
-      } catch (e) {
-        baseUrl = ''
-      }
-      uni.request({
-        url: baseUrl + '/api/membership/benefits',
-        method: 'POST',
-        data: { levelCode: 'FAMILY' },
-        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
-          if (res.data && res.data.code === 200 && res.data.data) {
-            var benefitsData = res.data.data
-            if (benefitsData.length > 0) {
-              // 用后端返回的权益替换硬编码的
-              self.benefits = benefitsData.map(function(b) {
-                return {
-                  code: b.benefitCode,
-                  name: b.benefitName,
-                  desc: b.benefitDesc || '',
-                  icon: b.icon || '🎁',
-                  bg: b.bgColor || '#E0E7FF',
-                  canUse: b.benefitCode !== 'NO_BENEFIT' && b.benefitCode !== 'UPGRADE_HINT',
-                  used: false,
-                  usedAt: null,
-                  detail: b.benefitDesc || ''
-                }
-              }).filter(function(b) { return b.code !== 'NO_BENEFIT' && b.code !== 'UPGRADE_HINT' })
-            }
-          }
-        }
-      })
-    },
-    useBenefit(item) {
-      var self = this
-      uni.showModal({
-        title: '使用权益',
-        content: '确认使用「' + item.name + '」?',
-        success: function(res) {
-          if (res.confirm) {
-            self.doUseBenefit(item)
+      getMyMembership().then(function(res) {
+        var m = res.data || {}
+        var levelCode = m.memberLevel || m.levelCode || 'FREE'
+        if (levelCode !== 'FREE') {
+          self.subscription = {
+            level: levelCode,
+            expireTime: m.memberExpireTime || (m.membership && m.membership.endDate) || null
           }
+          self.loadBenefits(levelCode)
+        } else {
+          self.subscription = { level: 'FREE', expireTime: null }
+          self.loadBenefits('BASIC')
         }
+      }).catch(function() {
+        self.subscription = { level: 'FREE', expireTime: null }
+        self.loadBenefits('BASIC')
       })
     },
-    doUseBenefit(item) {
+    loadBenefits: function(levelCode) {
       var self = this
-      var data = { benefitCode: item.code }
-      checkBenefit(data).then(function(res) {
-        if (res.data && res.data.success) {
-          item.used = true
-          item.usedAt = new Date()
-          uni.showToast({ title: '使用成功', icon: 'success' })
+      getMembershipBenefits(levelCode).then(function(res) {
+        var benefitsData = (res && res.data) || []
+        if (benefitsData.length > 0) {
+          self.benefits = benefitsData.map(function(b) {
+            return {
+              code: b.benefitCode,
+              name: b.benefitName,
+              desc: b.benefitDesc || '',
+              icon: b.icon || '🎁',
+              bg: b.bgColor || '#E0E7FF',
+              canUse: false, // membership_benefits 为展示型权益,无单独使用入口
+              used: false,
+              usedAt: null,
+              detail: b.benefitDesc || ''
+            }
+          }).filter(function(b) { return b.code !== 'NO_BENEFIT' && b.code !== 'UPGRADE_HINT' })
         } else {
-          uni.showToast({ title: res.data.message || '使用失败', icon: 'none' })
+          self.benefits = []
         }
       }).catch(function(e) {
-        uni.showToast({ title: e.message || '使用失败', icon: 'none' })
+        console.error('加载权益失败', e)
+        self.benefits = []
       })
     },
     formatDate(date) {

+ 14 - 12
cfc-frontend/pages/membership/index.vue

@@ -80,7 +80,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMyButler, reportAppEnter, getMembershipBenefits
+import { getMyMembership, getMyButler, reportAppEnter, getMembershipBenefits, getMembershipCompare
 } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '../../mixins/font-scale.js'
@@ -93,16 +93,7 @@ export default {
       isFree: true,
       currentLevelName: '免费用户',
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
-      compareData: [
-        { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', basic: '3位成员', family: '10位成员', premium: '20位成员' },
-        { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限' },
-        { name: '报告解读', free: '—', basic: '1次/月', family: '不限', premium: '深度解读' },
-        { name: '测评预约', free: '仅查看', basic: '1次/月', family: '不限', premium: '优先' },
-        { name: '管家服务', free: '—', basic: '—', family: '月1次', premium: '周1次' },
-        { name: '推荐佣金', free: '不可参与', basic: '可参与', family: '可参与', premium: '可参与' },
-        { name: '专属活动', free: '部分参与', basic: '部分参与', family: '全部参与', premium: '优先参与' }
-      ],
+      compareData: [],
       myButlerInfo: null,
       isL2Active: false
     }
@@ -117,8 +108,9 @@ export default {
   },
   onShow() {
     reportAppEnter('membership');
-    
+
     this.loadData()
+    this.loadCompare()
     this.loadButlerEntry()
   },
   methods: {
@@ -145,6 +137,16 @@ export default {
         }).catch(function() {})
       }).catch(function() {})
     },
+    loadCompare() {
+      var self = this
+      getMembershipCompare().then(function(res) {
+        if (res.code === 200 && res.data && res.data.rows) {
+          self.compareData = res.data.rows
+        }
+      }).catch(function(e) {
+        console.error('加载权益对比表失败', e)
+      })
+    },
     getLevelName(code) {
       var map = { FREE: '免费用户', BASIC: '体验会员', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
       return map[code] || code || ''

+ 16 - 14
cfc-frontend/pages/membership/upgrade.vue

@@ -101,7 +101,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMembershipPlans, getUpgradeRecords, getJoinCouponRules } from '../../utils/api.js'
+import { getMyMembership, getMembershipPlans, getUpgradeRecords, getJoinCouponRules, getMembershipCompare } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -112,18 +112,7 @@ export default {
       records: [],
       isFree: true,
       currentLevelName: '免费用户',
-      compareData: [
-        { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣', lifetime: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', basic: '3位成员', family: '10位成员', premium: '20位成员', lifetime: '全部功能+专属管家' },
-        { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限', lifetime: '不限' },
-        { name: '报告解读', free: '—', basic: '1次/月', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
-        { name: '测评预约', free: '仅查看', basic: '1次/月', family: '不限', premium: '优先', lifetime: '优先' },
-        { name: '管家服务', free: '—', basic: '—', family: '月1次', premium: '每周一次', lifetime: '专属管家7×24h' },
-        { name: '推荐佣金', free: '不可参与', basic: '可参与', family: '可参与', premium: '可参与', lifetime: '可参与' },
-        { name: '专属活动', free: '部分参与', basic: '部分参与', family: '全部参与', premium: '优先参与', lifetime: 'VIP专属+定制活动' },
-        { name: '成长报告', free: '月度', basic: '基础模板', family: '每周+深度', premium: '每日+深度+专家解读', lifetime: '实时+专家深度解读' },
-        { name: '任务配额', free: '每日5个', basic: '每日3个', family: '不限量', premium: '不限量+优先审核', lifetime: '不限量+专属通道' }
-      ],
+      compareData: [],
       periods: [
         { key: 'monthly', label: '月度', unit: '/月' },
         { key: 'quarterly', label: '季度', unit: '/季' },
@@ -238,6 +227,17 @@ export default {
       } catch (e) {
         console.error('加载升级记录失败', e)
       }
+      this.loadCompare()
+    },
+    loadCompare() {
+      var self = this
+      getMembershipCompare().then(function(res) {
+        if (res.code === 200 && res.data && res.data.rows) {
+          self.compareData = res.data.rows
+        }
+      }).catch(function(e) {
+        console.error('加载权益对比表失败', e)
+      })
     },
     selectLevel: function(code) {
       this.selectedLevel = code
@@ -259,7 +259,9 @@ export default {
     },
     applyDefaultLevel: function() {
       var current = this.membership && this.membership.memberLevel ? this.membership.memberLevel : 'FREE'
-      var rank = { 'FREE': 0, 'BASIC': 1, 'FAMILY': 2, 'PREMIUM': 3 }
+      // level_order 与 DB membership_levels.level_order 一致:FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5;
+      // PROVIDER 为服务商独立角色(不参与会员权益),level_order=1 与 FREE 同级
+      var rank = { 'FREE': 1, 'BASIC': 2, 'FAMILY': 3, 'PROVIDER': 1, 'PREMIUM': 4, 'LIFETIME': 5 }
       var currentRank = rank[current] !== undefined ? rank[current] : 0
       var target = null
       for (var i = 0; i < this.buyableLevels.length; i++) {

+ 3 - 0
cfc-frontend/utils/api.js

@@ -599,6 +599,9 @@ export const getMyMembership = () => {
 }
 export const getMembershipBenefits = (levelCode) => request('/api/membership/benefits', 'POST', { levelCode })
 
+// 会员权益对比表(购买页/会员中心动态渲染,后端聚合各档实际权限)
+export const getMembershipCompare = () => request('/api/membership/compare', 'POST')
+
 export const getGiftMembershipProgress = () => {
 	return request('/api/membership/gift-membership-progress', 'POST')
 }

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-534591149def1c682f0f051d8f455f18bc574843
+08c4c0662af91826cb6ad9ddce96b0789f27b350

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1650",
+  "version": "1.0.1651",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1650",
+      "version": "1.0.1651",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1651",
+  "version": "1.0.1652",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 23 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,29 @@
 
 ---
 
+## v1.0.1652 (2026-10-17)
+
+### 新功能
+- 管理端发货后自动同步物流到微信小程序交易管理服务
+- 管理端定义活动海报模板 + 小程序端动态模板渲染
+
+### 其他
+-   - order_key 用 mchid+out_trade_no(order_number_type=1),delivery_mode=1、logistics_type=1
+-   - 快递中文名映射微信编码(顺丰→SF/中通→ZTO/圆通→YTO/韵达→YD/申通→STO/京东→JD/EMS)
+-   - 响应解析优先检查 shipping_list 条目级失败(微信部分失败时顶层 errcode=0)
+-   - fail-open:失败只记日志不阻塞本地发货;wechat.test-mode=true 自动跳过
+- - ProductOrderService.ship()/updateExpress() 发货成功后调 syncShippingInfoToWechat()
+-   - 查买家 openid,组装「商品名 x数量」描述(≤120字),部分更新时回退订单已存快递值
+- - application.yml 新增 wechat.fulfillment-enabled 开关(默认 true,WECHAT_FULFILLMENT_ENABLED 可覆盖)
+- - 文档:API_REFERENCE 4.8 补充发货同步微信说明;PROJECT-OVERVIEW 追加变更记录
+- 
+- - 后端: /api/admin/poster-template/{list,save,delete}(admin/operator) + /api/poster-template/list(小程序)
+- - 管理端: PosterTemplateManage.vue 模板 CRUD(三色/动态色板/排序/启停) + 路由菜单权限
+- - 小程序: ContentSharePoster 新增 templates prop 动态模板+色板+qrSize,activity-detail 生成海报时拉取
+- - 文档: spec + PROJECT-OVERVIEW + API_REFERENCE(4.49)
+- 
+
+
 ## v1.0.1651 (2026-10-17)
 
 ### 文档

+ 24 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1651
+> 当前版本: v1.0.1652
 
 ## 历史版本
 
@@ -8,6 +8,29 @@
 
 ---
 
+## v1.0.1652 (2026-10-17)
+
+### 新功能
+- 管理端发货后自动同步物流到微信小程序交易管理服务
+- 管理端定义活动海报模板 + 小程序端动态模板渲染
+
+### 其他
+-   - order_key 用 mchid+out_trade_no(order_number_type=1),delivery_mode=1、logistics_type=1
+-   - 快递中文名映射微信编码(顺丰→SF/中通→ZTO/圆通→YTO/韵达→YD/申通→STO/京东→JD/EMS)
+-   - 响应解析优先检查 shipping_list 条目级失败(微信部分失败时顶层 errcode=0)
+-   - fail-open:失败只记日志不阻塞本地发货;wechat.test-mode=true 自动跳过
+- - ProductOrderService.ship()/updateExpress() 发货成功后调 syncShippingInfoToWechat()
+-   - 查买家 openid,组装「商品名 x数量」描述(≤120字),部分更新时回退订单已存快递值
+- - application.yml 新增 wechat.fulfillment-enabled 开关(默认 true,WECHAT_FULFILLMENT_ENABLED 可覆盖)
+- - 文档:API_REFERENCE 4.8 补充发货同步微信说明;PROJECT-OVERVIEW 追加变更记录
+- 
+- - 后端: /api/admin/poster-template/{list,save,delete}(admin/operator) + /api/poster-template/list(小程序)
+- - 管理端: PosterTemplateManage.vue 模板 CRUD(三色/动态色板/排序/启停) + 路由菜单权限
+- - 小程序: ContentSharePoster 新增 templates prop 动态模板+色板+qrSize,activity-detail 生成海报时拉取
+- - 文档: spec + PROJECT-OVERVIEW + API_REFERENCE(4.49)
+- 
+
+
 ## v1.0.1651 (2026-10-17)
 
 ### 文档

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

@@ -211,7 +211,7 @@
 ### 4.3 家庭会员体系 ⭐ P2
 
 **阶段:** Phase 2
-**状态:** ✅ 全栈完成(MemberSubscriptionService + MembershipController + membership/ 3 前端页面,含 FREE/FAMILY/PREMIUM/LIFETIME 四等级价格体系,价格通过 membership_levels 表内配置,商城升级提示通用化,**LIFETIME 不可购买仅管理员审核授予**)
+**状态:** ✅ 全栈完成(MemberSubscriptionService + MembershipController + membership/ 3 前端页面,含 FREE/BASIC/FAMILY/PREMIUM/LIFETIME 五等级价格体系,价格通过 membership_levels 表内配置,商城升级提示通用化,**LIFETIME 不可购买仅管理员审核授予**)+ **权益对比表动态化 + PROVIDER 退出会员权益体系(2026-10-18 已实施)**
 
 | 功能 | 关联文档 |
 |------|---------|
@@ -220,6 +220,7 @@
 | AI健康管家+推荐引擎(P1) | `plans/2026-07-22-comprehensive-implementation.md`(Phase 2 Wave 3) |
 | **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 |
 
 ---
 
@@ -378,7 +379,11 @@
 | `2026-08-31-self-check-ai-integration-design.md` | 🟡 设计稿(P0-1 纯AI替换静态建议 + P0-2 用户点击生成健康计划 + P1-1 历史趋势AI解读 + P1-2 Chat上下文注入,P2延后) | 五维家庭自检 AI 结合设计 |
 | `2026-09-09-team-hierarchy-design.md` | 🟡 设计稿(团队层级树从下往上生长 + 团队消费额分级统计 + 直推成员消费/CF明细 + 我的推荐人展示 + 去掉富页面推荐码) | 团队层级与推荐关系可视化 |
 | `2026-09-10-report-quick-analyze-design.md` | 🟡 设计稿(报告详情页快速分析:免费版每日 1 次 Redis 计数 + 付费版多轮;复用 chat_graph + AiGateway,LangGraph 零改动) | 报告快速分析设计 |
+<<<<<<< Updated upstream
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程;2026-09-06 补充家庭成员切换/回收箱接口:`/leave`、`/kick` 回收箱分流、`/recycle`;2026-09-10 补充关系三图接口:`/api/octopus`、`/api/pearl`、`/api/ability`;2026-09-12 补充供应商审核接口:`/api/product/order/review/update`;2026-10-04 补充统一服务角色申请 6 接口 `/api/service-role/*`(4.44,含 status 语义、角色落地规则及 admin/operator 权限口径)、规划师解绑家庭 `/api/guide/families/unbind`(4.45)、规划师方案审核 `/api/health/plan/pending-review/*`(4.46,含未分配池 `teacher_id IS NULL` 规则);2026-10-05 补充商品批量上下架 `POST /api/admin/product/batch-shelve`(body: `{productIds, shelve}`,批量校验仅审核通过商品可操作);2026-10-09 补充指标统一层 9 接口(4.48:视图 4 个 `/api/indicator/*` + admin 5 个 `/api/admin/indicator/*`,含家庭归属校验、关联校准口径、映射导入/review);2026-10-18 补充发货同步微信说明(4.8:`/api/product/order/ship` 发货成功后自动调微信 `upload_shipping_info` 同步物流,`wechat.fulfillment-enabled` 开关,fail-open 不阻塞)) | 后台接口参考文档 |
+=======
+| `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程;2026-09-06 补充家庭成员切换/回收箱接口:`/leave`、`/kick` 回收箱分流、`/recycle`;2026-09-10 补充关系三图接口:`/api/octopus`、`/api/pearl`、`/api/ability`;2026-09-12 补充供应商审核接口:`/api/product/order/review/update`;2026-10-04 补充统一服务角色申请 6 接口 `/api/service-role/*`(4.44,含 status 语义、角色落地规则及 admin/operator 权限口径)、规划师解绑家庭 `/api/guide/families/unbind`(4.45)、规划师方案审核 `/api/health/plan/pending-review/*`(4.46,含未分配池 `teacher_id IS NULL` 规则);2026-10-05 补充商品批量上下架 `POST /api/admin/product/batch-shelve`(body: `{productIds, shelve}`,批量校验仅审核通过商品可操作);2026-10-09 补充指标统一层 9 接口(4.48:视图 4 个 `/api/indicator/*` + admin 5 个 `/api/admin/indicator/*`,含家庭归属校验、关联校准口径、映射导入/review);2026-10-18 补充会员权益对比表 `POST /api/membership/compare`(4.50:列 FREE/BASIC/FAMILY/PREMIUM,行值由 sys_config 折扣 bps / membership_levels 上限 / BasicQuotaService 配额 / features 开关聚合,购买页与会员中心对比表改动态渲染;`/api/membership/plans` 同次过滤 PROVIDER 并加 maxChildren/maxTasksPerDay)) | 后台接口参考文档 |
+>>>>>>> Stashed changes
 | `2026-09-10-key-person-expansion-octopus-design.md` | 🟡 设计规格已确认(成效对象=会员客户 + 类型bitmask + 关键人画像 + 四步法 + 高频词≤8方向;3表/10接口/三层画布;替换旧章鱼图) | 关键人拓展章鱼图替换设计(关系三图重构) |
 | `2026-09-19-article-reading-quiz-design.md` | 🟢 已实施(补全现有阅读答题:article_quiz_records 表 + LangGraph article_quiz_graph 出题 + AiGateway 接入 + 提交答题落库 + CF值/五维能量双奖励 + profile_snapshot.quiz_summary 认知雷达) | 文章阅读后答题奖励 + 认知雷达记录设计 |
 | `2026-09-22-guide-popup-config-design.md` | ✅ 已实施(4表/guide_popup+step+trigger+log,9接口,ConfigurableGuide.vue组件+管理端2页;旧 sys_config 方案保留并行) | 弹窗引导页配置化管理设计 |

+ 44 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -1704,6 +1704,50 @@ Web 管理端定义活动海报模板(迁移381 `activity_poster_templates` 
 
 ---
 
+### 4.50 会员权益对比表(`/api/membership/compare`)
+
+购买页(`pages/membership/upgrade.vue`)与会员中心(`pages/membership/index.vue`)的权益对比表数据源。**全部权益值由后端聚合实际逻辑后返回展示文本**,前端不再硬编码权益文案,避免出现「文案说不限量 / 实际 20 个」这类偏差。
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/membership/compare` | 返回 `{columns, rows}`,无需请求参数 |
+
+**返回值结构**
+
+```json
+{
+  "code": 200,
+  "data": {
+    "columns": ["FREE", "BASIC", "FAMILY", "PREMIUM"],
+    "rows": [
+      { "name": "商品折扣", "free": "普通价格", "basic": "—", "family": "9折", "premium": "8折" },
+      { "name": "家庭成员上限", "free": "1人", "basic": "3人", "family": "6人", "premium": "10人" },
+      { "name": "每日任务上限", "free": "5个/日", "basic": "3个/日", "family": "20个/日", "premium": "50个/日" }
+    ]
+  }
+}
+```
+
+**行值口径(事实源)**
+
+| 权益行 | 数据来源 |
+|--------|----------|
+| 商品折扣 | `sys_config.member_discount_family` / `member_discount_premium`(bps:9000→9折、8000→8折),BASIC 无折扣配置显示 `—` |
+| 家庭成员上限 | `membership_levels.max_children`(0/空视为无该权益 → `—`) |
+| 每日任务上限 | `membership_levels.max_tasks_per_day` |
+| AI健康顾问 / 报告解读 / 测评预约 / 食材图片解析 | `BasicQuotaService.QUOTA_LIMITS` 月度配额常量(BASIC 限次)+ `canUseFeature` 判定(FAMILY/PREMIUM 不限) |
+| 优惠券权益 | 普通券面向所有登录用户(`discount_purchase` feature,BASIC 起可用);会员专享券仅 FAMILY/PREMIUM(`CouponService.canUseMemberCoupon`) |
+| 会员专属活动 | `member_activities` feature,BASIC 及以下为部分活动 |
+| 邀请佣金 / 消费返佣 | `referral_commission` / `purchase_commission` feature |
+| 专属管家、规划师咨询 | `membership_levels.priority_support` / `teacher_consultation` 对应的产品权益设定 |
+
+> **列口径**:仅 `FREE/BASIC/FAMILY/PREMIUM` 四列。`LIFETIME` 为赠送/特殊发放、不可购买,不作为列展示;`PROVIDER` 是**服务商角色**,不属于会员权益体系(`level_order=1` 与 FREE 同级,`features` 为 NULL),不参与本表。
+> **`/api/membership/plans` 同步变更**:除 `LIFETIME` 外再过滤 `PROVIDER`,并附加 `maxChildren` / `maxTasksPerDay` 两个字段,前端档位卡片可直接引用。
+
+*文档更新:2026-10-18(会员权益对比表动态化 + PROVIDER 退出会员权益体系)*
+
+---
+
 *文档最后更新:2026-10-04*
 
 ---