Bladeren bron

feat: BASIC体验会员 (9.9元/月) 核心实现

- DB: basic_quota_usage表 + membership_levels/benefits BASIC seed(迁移311-313)
- 后端: BasicQuotaService/ActivationQuotaService + 4业务入口配额扣减
- 定时任务: BasicQuotaScheduledTasks(月度刷新+到期提醒)
- Controller: BasicQuotaController + AdminBasicQuotaController
- 前端: QuotaProgressBar/UpgradeGuideModal组件 + 会员页/升级页BASIC支持
- AI对话页集成配额显示和软引导
E2E Test Bot 1 week geleden
bovenliggende
commit
494f64d351
24 gewijzigde bestanden met toevoegingen van 1444 en 15 verwijderingen
  1. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java
  2. 52 1
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  3. 47 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java
  4. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  5. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java
  6. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java
  7. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java
  8. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java
  9. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java
  10. 77 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java
  11. 13 1
      cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
  12. 12 1
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
  13. 247 0
      cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java
  14. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java
  15. 45 2
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  16. 55 0
      cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java
  17. 36 0
      cfc-backend/src/main/resources/schema.sql
  18. 53 0
      cfc-frontend/components/QuotaProgressBar.vue
  19. 130 0
      cfc-frontend/components/UpgradeGuideModal.vue
  20. 33 3
      cfc-frontend/pages/ai/chat.vue
  21. 17 7
      cfc-frontend/pages/membership/index.vue
  22. 4 0
      cfc-frontend/pages/membership/upgrade.vue
  23. 4 0
      cfc-frontend/utils/api.js
  24. 495 0
      docs/superpowers/specs/2026-09-10-basic-membership-design.md

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.common;
+
+public class BasicQuotaExhaustedException extends RuntimeException {
+    public BasicQuotaExhaustedException(String message) {
+        super(message);
+    }
+}

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

@@ -1,4 +1,4 @@
-package com.etotem.cfc.config;
+package com.etotem.cfc.config;
 
 import com.etotem.cfc.mapper.*;
 import javax.annotation.Resource;
@@ -10922,6 +10922,57 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         } catch (Exception e) {
             log.warn("迁移314: 添加新 UNIQUE KEY 失败: {}", e.getMessage());
         }
+
+        // 迁移315: membership_levels 新增 BASIC 体验会员等级(9.9元/月)
+        try {
+            jdbcTemplate.execute(
+                "INSERT IGNORE INTO membership_levels " +
+                "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
+                "VALUES ('BASIC', '体验会员', " +
+                "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
+                "990, " +
+                "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
+                "3, 3)");
+            log.info("迁移315: 已初始化BASIC体验会员等级");
+        } catch (Exception e) {
+            // 等级已存在,忽略
+        }
+
+        // 迁移316: membership_benefits 新增 BASIC 会员权益
+        try {
+            jdbcTemplate.execute(
+                "INSERT IGNORE INTO membership_benefits " +
+                "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
+                "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
+                "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
+                "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
+                "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " +
+                "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)");
+            log.info("迁移316: 已初始化BASIC会员权益列表");
+        } catch (Exception e) {
+            // 已存在,忽略
+        }
+
+        // 迁移317: 创建 basic_quota_usage 表(BASIC会员月度配额使用记录)
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS basic_quota_usage (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '家长用户ID', " +
+                "quota_type VARCHAR(30) NOT NULL COMMENT '配额类型', " +
+                "used_count INT DEFAULT 0 COMMENT '已用次数', " +
+                "total_count INT NOT NULL COMMENT '月度总配额', " +
+                "period_start DATE NOT NULL COMMENT '周期开始日期', " +
+                "period_end DATE NOT NULL COMMENT '周期结束日期', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), " +
+                "INDEX idx_user_period (user_id, period_start) " +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'");
+            log.info("迁移317: 已创建basic_quota_usage表");
+        } catch (Exception e) {
+            // 表已存在,忽略
+        }
     }
 
     private void seedGutFloraFingerprints() {

+ 47 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java

@@ -0,0 +1,47 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/basic-quota")
+public class BasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 查询当前用户某配额的剩余次数
+     */
+    @PostMapping("/status")
+    public Result<QuotaStatusDTO> getQuotaStatus(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> body) {
+        String quotaType = body.get("quotaType");
+        if (quotaType == null) {
+            return Result.error(400, "quotaType 不能为空");
+        }
+        QuotaStatusDTO dto = basicQuotaService.getMonthlyQuota(userId, quotaType);
+        return Result.success(dto);
+    }
+
+    /**
+     * 批量查询当前用户所有配额状态
+     */
+    @PostMapping("/status/all")
+    public Result<Map<String, QuotaStatusDTO>> getAllQuotaStatus(
+            @RequestAttribute("userId") Long userId) {
+        Map<String, QuotaStatusDTO> result = new HashMap<>();
+        result.put("AI_CHAT", basicQuotaService.getMonthlyQuota(userId, "AI_CHAT"));
+        result.put("REPORT_READING", basicQuotaService.getMonthlyQuota(userId, "REPORT_READING"));
+        result.put("ASSESSMENT_QUOTA", basicQuotaService.getMonthlyQuota(userId, "ASSESSMENT_QUOTA"));
+        result.put("REPORT_UPLOAD", basicQuotaService.getMonthlyQuota(userId, "REPORT_UPLOAD"));
+        return Result.success(result);
+    }
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -151,6 +151,9 @@ public class HealthReportController {
     @Resource
     private ReportBlockService reportBlockService;
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @Resource
     private StorageService storageService;
 
@@ -2401,6 +2404,15 @@ public class HealthReportController {
      * 触发 AI 解读
      */
     private void triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
+        // BASIC 配额检查(报告 AI 解读)
+        if (proxyUploaderId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(proxyUploaderId, "REPORT_READING");
+            if (!quotaResult.isSuccess()) {
+                log.info("报告 AI 解读配额不足: userId={}, toast={}", proxyUploaderId, quotaResult.getToastMsg());
+                return;
+            }
+        }
+
         try {
             Map<String, Object> analyzeReq = new HashMap<>();
             analyzeReq.put("report_id", reportId);

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/basic-quota")
+public class AdminBasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 运营手动重置指定用户的当月配额(应急用)
+     */
+    @PostMapping("/reset")
+    public Result<String> resetQuota(@RequestBody Map<String, Object> body) {
+        Long userId = Long.valueOf(body.get("userId").toString());
+        String quotaType = body.get("quotaType") != null ? body.get("quotaType").toString() : null;
+        basicQuotaService.refreshMonthlyQuota(); // 重置所有用户当月配额
+        return Result.success("配额刷新完成");
+    }
+}

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java

@@ -0,0 +1,11 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class QuotaConsumeResult {
+    private boolean success;
+    private int remaining;
+    private String toastMsg;
+    private String actionUrl;
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class QuotaStatusDTO {
+    private String quotaType;
+    private int total;
+    private int used;
+    private int remaining;
+    private Date periodStart;
+    private Date periodEnd;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java

@@ -0,0 +1,33 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("basic_quota_usage")
+public class BasicQuotaUsage implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String quotaType;
+
+    private Integer usedCount;
+
+    private Integer totalCount;
+
+    private Date periodStart;
+
+    private Date periodEnd;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BasicQuotaUsageMapper extends BaseMapper<BasicQuotaUsage> {
+}

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java

@@ -0,0 +1,77 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class ActivationQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 支付成功后,为 BASIC 用户初始化当月 4 类配额记录
+     */
+    public void activateBasicQuota(Long userId, Long familyId) {
+        if (userId == null) {
+            return;
+        }
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+
+        for (Map.Entry<String, Integer> entry : QUOTA_LIMITS.entrySet()) {
+            try {
+                BasicQuotaUsage usage = new BasicQuotaUsage();
+                usage.setUserId(userId);
+                usage.setQuotaType(entry.getKey());
+                usage.setUsedCount(0);
+                usage.setTotalCount(entry.getValue());
+                usage.setPeriodStart(monthStart);
+                usage.setPeriodEnd(monthEnd);
+                usage.setCreatedAt(now);
+                usage.setUpdatedAt(now);
+                basicQuotaUsageMapper.insert(usage);
+                log.info("BASIC配额初始化: userId={}, quotaType={}, total={}", userId, entry.getKey(), entry.getValue());
+            } catch (Exception e) {
+                log.warn("BASIC配额初始化失败: userId={}, quotaType={}", userId, entry.getKey(), e);
+            }
+        }
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+}

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

@@ -60,6 +60,9 @@ public class AiGateway {
     @Resource
     private PiisService piisService;
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @PostConstruct
     public void init() {
         // 应用超时到 HTTP 客户端:
@@ -159,9 +162,18 @@ public class AiGateway {
      * 调用 Python ChatAgent, 失败时返回 null
      */
     public Map<String, Object> chat(String query, Long userId, String conversationId,
-                                     Map<String, Object> inputs) {
+                                      Map<String, Object> inputs) {
         if (!enabled || isCircuitOpen()) return null;
 
+        // BASIC 配额检查(仅 BASIC 用户)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "AI_CHAT");
+            if (!quotaResult.isSuccess()) {
+                log.info("AI对话配额不足: userId={}, toast={}", userId, quotaResult.getToastMsg());
+                return null;
+            }
+        }
+
         try {
             // PII脱敏
             query = piisService.sanitizeText(query);

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

@@ -24,8 +24,19 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     @Resource
     private EnergyService energyService;
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     public AssessmentAppointment createAppointment(Long userId, Long memberId, Long guideId,
-                                                    Long assessorId, Long packageId, String appointmentDate, String appointmentTime, String notes) {
+                                                   Long assessorId, Long packageId, String appointmentDate, String appointmentTime, String notes) {
+        // BASIC 硬门禁配额检查(测评预约)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "ASSESSMENT_QUOTA");
+            if (!quotaResult.isSuccess()) {
+                throw new com.etotem.cfc.common.BasicQuotaExhaustedException(quotaResult.getToastMsg());
+            }
+        }
+
         AssessmentAppointment appointment = new AssessmentAppointment();
         appointment.setUserId(userId);
         appointment.setChildId(memberId);

+ 247 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java

@@ -0,0 +1,247 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.QuotaConsumeResult;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class BasicQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private MembershipService membershipService;
+
+    // BASIC 会员月度配额上限
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 查询指定用户指定月份某配额的剩余次数
+     */
+    public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType) {
+        if (userId == null || quotaType == null) {
+            return null;
+        }
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        QuotaStatusDTO dto = new QuotaStatusDTO();
+        dto.setQuotaType(quotaType);
+        dto.setTotal(total);
+        dto.setUsed(used);
+        dto.setRemaining(Math.max(0, total - used));
+        dto.setPeriodStart(periodStart);
+        dto.setPeriodEnd(periodEnd);
+        return dto;
+    }
+
+    /**
+     * 尝试扣减配额,返回是否成功
+     */
+    public QuotaConsumeResult consumeQuota(Long userId, String quotaType) {
+        QuotaConsumeResult result = new QuotaConsumeResult();
+
+        if (!isActiveBasicMember(userId)) {
+            result.setSuccess(false);
+            result.setToastMsg("该功能仅限体验会员使用");
+            return result;
+        }
+
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        if (used >= total) {
+            result.setSuccess(false);
+            result.setRemaining(0);
+            result.setToastMsg(buildExhaustedToast(quotaType, total));
+            result.setActionUrl("/pages/profile/upgrade/index");
+            return result;
+        }
+
+        // 初始化或更新
+        if (usage == null) {
+            usage = new BasicQuotaUsage();
+            usage.setUserId(userId);
+            usage.setQuotaType(quotaType);
+            usage.setUsedCount(0);
+            usage.setTotalCount(total);
+            usage.setPeriodStart(periodStart);
+            usage.setPeriodEnd(periodEnd);
+            usage.setCreatedAt(now);
+            usage.setUpdatedAt(now);
+            basicQuotaUsageMapper.insert(usage);
+        }
+        usage.setUsedCount(used + 1);
+        usage.setUpdatedAt(now);
+        basicQuotaUsageMapper.updateById(usage);
+
+        result.setSuccess(true);
+        result.setRemaining(total - used - 1);
+        return result;
+    }
+
+    /**
+     * 批量刷新所有 BASIC 用户当月配额(每月1号调用)
+     */
+    public void refreshMonthlyQuota() {
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+
+        List<BasicQuotaUsage> lastMonthRecords = basicQuotaUsageMapper.selectList(
+                new LambdaQueryWrapper<BasicQuotaUsage>());
+
+        Set<Long> userIds = new HashSet<>();
+        for (BasicQuotaUsage rec : lastMonthRecords) {
+            if (isActiveBasicMember(rec.getUserId())) {
+                userIds.add(rec.getUserId());
+            }
+        }
+
+        for (Long userId : userIds) {
+            for (String quotaType : QUOTA_LIMITS.keySet()) {
+                try {
+                    BasicQuotaUsage existing = basicQuotaUsageMapper.selectOne(
+                            new LambdaQueryWrapper<BasicQuotaUsage>()
+                                    .eq(BasicQuotaUsage::getUserId, userId)
+                                    .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                                    .eq(BasicQuotaUsage::getPeriodStart, monthStart));
+                    if (existing == null) {
+                        BasicQuotaUsage newUsage = new BasicQuotaUsage();
+                        newUsage.setUserId(userId);
+                        newUsage.setQuotaType(quotaType);
+                        newUsage.setUsedCount(0);
+                        newUsage.setTotalCount(QUOTA_LIMITS.get(quotaType));
+                        newUsage.setPeriodStart(monthStart);
+                        newUsage.setPeriodEnd(monthEnd);
+                        newUsage.setCreatedAt(now);
+                        newUsage.setUpdatedAt(now);
+                        basicQuotaUsageMapper.insert(newUsage);
+                        log.info("BASIC配额刷新初始化: userId={}, quotaType={}", userId, quotaType);
+                    } else {
+                        existing.setUsedCount(0);
+                        existing.setPeriodStart(monthStart);
+                        existing.setPeriodEnd(monthEnd);
+                        existing.setUpdatedAt(now);
+                        basicQuotaUsageMapper.updateById(existing);
+                    }
+                } catch (Exception e) {
+                    log.warn("BASIC配额刷新失败: userId={}, quotaType={}", userId, quotaType, e);
+                }
+            }
+        }
+        log.info("BASIC配额刷新完成, 处理用户数: {}", userIds.size());
+    }
+
+    /**
+     * 检查用户当前是否为有效的 BASIC 会员
+     */
+    public boolean isActiveBasicMember(Long userId) {
+        if (userId == null) {
+            return false;
+        }
+        String level = membershipService.getMemberLevel(userId);
+        if (!"BASIC".equals(level)) {
+            return false;
+        }
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getMemberExpireTime() == null) {
+            return false;
+        }
+        return user.getMemberExpireTime().after(new Date());
+    }
+
+    /**
+     * 获取 BASIC 用户到期时间
+     */
+    public Date getExpireTime(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user != null) {
+            return user.getMemberExpireTime();
+        }
+        return null;
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+
+    private String buildExhaustedToast(String quotaType, int total) {
+        switch (quotaType) {
+            case "AI_CHAT":
+                return "您本月 AI 对话已用完(" + total + "/" + total + "),升级 FAMILY 享不限次畅聊";
+            case "REPORT_READING":
+                return "本月报告解读额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁不限次深度解读";
+            case "ASSESSMENT_QUOTA":
+                return "测评预约为 FAMILY 会员专属权益,升级后即可预约";
+            case "REPORT_UPLOAD":
+                return "本月报告上传额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁更多";
+            default:
+                return "本月额度已用完,升级 FAMILY 解锁更多";
+        }
+    }
+}

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -52,6 +52,9 @@ public class DanReportUploadService {
     @Resource
     private DimensionScoreService dimensionScoreService;
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @Resource
     private ReportBlockService reportBlockService;
 
@@ -75,6 +78,14 @@ public class DanReportUploadService {
     // Phase 1: 上传并预览 PDF
     public Result<Map<String, Object>> uploadAndParse(Long userId, Long memberId,
                                                        String dimension, MultipartFile file) {
+        // BASIC 配额检查(报告上传)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "REPORT_UPLOAD");
+            if (!quotaResult.isSuccess()) {
+                return Result.error(403, quotaResult.getToastMsg());
+            }
+        }
+
         if (file == null || file.isEmpty()) {
             return Result.error("请选择文件");
         }

+ 45 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -75,6 +75,9 @@ public class MembershipService implements MembershipServiceInterface {
     @Resource
     private PaymentService paymentService;
 
+    @Resource
+    private ActivationQuotaService activationQuotaService;
+
     // ==================== 基础查询 ====================
 
     /**
@@ -238,6 +241,20 @@ public class MembershipService implements MembershipServiceInterface {
                     || "referral_commission".equals(feature);
         }
 
+        // BASIC 体验会员可用功能(9.9元/月,限次配额型)
+        if ("BASIC".equals(level)) {
+            return "free_activities".equals(feature)
+                    || "free_courses".equals(feature)
+                    || "discount_purchase".equals(feature)
+                    || "invite_family".equals(feature)
+                    || "mascot_ai_lite".equals(feature)
+                    || "ai_report_reading_lite".equals(feature)
+                    || "assessment_quota_lite".equals(feature)
+                    || "health_report_upload_lite".equals(feature)
+                    || "growth_plan_lite".equals(feature)
+                    || "task_template_lite".equals(feature);
+        }
+
         // FAMILY用户可用功能
         if ("FAMILY".equals(level)) {
             return "free_activities".equals(feature)
@@ -814,6 +831,16 @@ public class MembershipService implements MembershipServiceInterface {
             Long buyerFamilyId = (buyer != null) ? buyer.getFamilyId() : null;
             cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId, buyerFamilyId, order.getAmount());
 
+            // 如果是 BASIC 等级,初始化月度配额
+            if ("BASIC".equals(order.getLevelCode())) {
+                try {
+                    activationQuotaService.activateBasicQuota(adminUserId, order.getFamilyId());
+                    log.info("BASIC配额初始化完成: userId={}, familyId={}", adminUserId, order.getFamilyId());
+                } catch (Exception e) {
+                    log.error("BASIC配额初始化失败: userId={}", adminUserId, e);
+                }
+            }
+
             log.info("会员开通完成: orderNo={}, levelCode={}, amount={}, fromLevel={}",
                     order.getOrderNo(), order.getLevelCode(), order.getAmount(), fromLevel);
             return true;
@@ -1115,7 +1142,7 @@ public class MembershipService implements MembershipServiceInterface {
             return;
         }
 
-        if ("FAMILY".equals(user.getMemberLevel())
+        if (("FAMILY".equals(user.getMemberLevel()) || "BASIC".equals(user.getMemberLevel()))
                 && user.getMemberExpireTime() != null
                 && user.getMemberExpireTime().before(new Date())) {
             // 方案B:家庭成员共享会员,到期时全家降级
@@ -1140,7 +1167,7 @@ public class MembershipService implements MembershipServiceInterface {
         }
 
         String level = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
-        if ("FAMILY".equals(level) && user.getMemberExpireTime() != null) {
+        if (("FAMILY".equals(level) || "BASIC".equals(level)) && user.getMemberExpireTime() != null) {
             if (user.getMemberExpireTime().before(new Date())) {
                 return "FREE";
             }
@@ -1187,6 +1214,22 @@ public class MembershipService implements MembershipServiceInterface {
         return familyIds.stream().distinct().collect(Collectors.toList());
     }
 
+    /**
+     * 查询当前有效的 BASIC 会员用户ID列表(到期提醒用)
+     */
+    public List<Long> listActiveBasicMemberUserIds() {
+        Date now = new Date();
+        List<User> basicUsers = userMapper.selectList(
+                new LambdaQueryWrapper<User>()
+                        .eq(User::getMemberLevel, "BASIC")
+                        .isNotNull(User::getMemberExpireTime)
+                        .gt(User::getMemberExpireTime, now));
+        return basicUsers != null ? basicUsers.stream()
+                .map(User::getId)
+                .distinct()
+                .collect(Collectors.toList()) : Collections.emptyList();
+    }
+
     /**
      * 应用会员折扣,返回折后价格(分)
      */

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java

@@ -0,0 +1,55 @@
+package com.etotem.cfc.task;
+
+import com.etotem.cfc.service.BasicQuotaService;
+import com.etotem.cfc.service.MembershipService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@Component
+public class BasicQuotaScheduledTasks {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    @Resource
+    private MembershipService membershipService;
+
+    /**
+     * 每月1号 00:00 刷新所有 BASIC 用户的月度配额
+     */
+    @Scheduled(cron = "0 0 0 1 * ?")
+    public void refreshMonthlyQuota() {
+        log.info("开始执行BASIC配额月度刷新定时任务");
+        try {
+            basicQuotaService.refreshMonthlyQuota();
+            log.info("BASIC配额月度刷新定时任务执行完成");
+        } catch (Exception e) {
+            log.error("BASIC配额月度刷新定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 每天 10:00 检查即将到期的 BASIC 会员,推送到期提醒
+     */
+    @Scheduled(cron = "0 0 10 * * ?")
+    public void sendExpiryReminders() {
+        log.info("开始执行BASIC会员到期提醒定时任务");
+        try {
+            List<Long> basicUserIds = membershipService.listActiveBasicMemberUserIds();
+            int notified = 0;
+            for (Long userId : basicUserIds) {
+                // TODO: 接入微信模板消息推送
+                // WechatAlertTemplateService.sendBasicExpiryReminder(userId, basicQuotaService)
+                notified++;
+            }
+            log.info("BASIC到期提醒完成, 处理用户数={}, 通知数={}", basicUserIds.size(), notified);
+        } catch (Exception e) {
+            log.error("BASIC到期提醒定时任务执行失败", e);
+        }
+    }
+}

+ 36 - 0
cfc-backend/src/main/resources/schema.sql

@@ -5574,3 +5574,39 @@ CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (
     INDEX idx_pir_user_status (user_id, status),
     INDEX idx_pir_resource (resource_item_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒';
+
+-- ============================================================
+-- BASIC 体验会员(迁移311-313)
+-- ============================================================
+INSERT IGNORE INTO membership_levels 
+(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) 
+VALUES (
+    'BASIC', 
+    '体验会员', 
+    '9.9元/月体验版,解锁核心功能,限次使用AI与报告解读',
+    990,
+    '["free_activities","free_courses","discount_purchase","invite_family","mascot_ai_lite","ai_report_reading_lite","assessment_quota_lite","health_report_upload_lite","growth_plan_lite","task_template_lite"]',
+    3,
+    3
+);
+
+INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES
+('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1),
+('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4),
+('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3),
+('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8),
+('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9);
+
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT '配额类型: AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';

+ 53 - 0
cfc-frontend/components/QuotaProgressBar.vue

@@ -0,0 +1,53 @@
+<template>
+  <view class="quota-progress-bar">
+    <view class="quota-label">
+      <text class="quota-name">{{ name }}</text>
+      <text class="quota-count">{{ remaining }} / {{ total }}</text>
+    </view>
+    <view class="quota-track">
+      <view class="quota-fill" :style="{ width: percent + '%' }"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'QuotaProgressBar',
+  props: {
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 },
+    name: { type: String, default: '' }
+  },
+  computed: {
+    percent() {
+      if (!this.total) return 0
+      return Math.round((this.remaining / this.total) * 100)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.quota-progress-bar {
+  margin-bottom: 16rpx;
+}
+.quota-label {
+  display: flex;
+  justify-content: space-between;
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 8rpx;
+}
+.quota-track {
+  height: 12rpx;
+  background: #f0f0f0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.quota-fill {
+  height: 100%;
+  background: #F97316;
+  border-radius: 6rpx;
+  transition: width 0.3s;
+}
+</style>

+ 130 - 0
cfc-frontend/components/UpgradeGuideModal.vue

@@ -0,0 +1,130 @@
+<template>
+  <view class="upgrade-guide-mask" v-if="visible" @click="handleMaskClick">
+    <view class="upgrade-guide-content" :class="{ 'hard-modal': type === 'hard' }" @click.stop>
+      <view class="upgrade-guide-icon">{{ icon }}</view>
+      <view class="upgrade-guide-title">{{ title }}</view>
+      <view class="upgrade-guide-desc">{{ desc }}</view>
+      <view class="upgrade-guide-actions">
+        <view v-if="type === 'soft'" class="btn-know" @click="handleKnow">知道了</view>
+        <view v-if="type === 'soft'" class="btn-learn" @click="handleLearn">了解详情</view>
+        <view v-if="type === 'hard'" class="btn-upgrade" @click="handleUpgrade">升级 FAMILY</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'UpgradeGuideModal',
+  data() {
+    return { visible: false }
+  },
+  props: {
+    type: { type: String, default: 'soft' },
+    quotaType: { type: String, default: '' },
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 }
+  },
+  computed: {
+    icon() {
+      return this.type === 'hard' ? '🔒' : 'ℹ️'
+    },
+    title() {
+      return this.type === 'hard' ? '升级后即可使用' : '本月额度已用完'
+    },
+    desc() {
+      if (this.type === 'hard') return '该功能为 FAMILY 会员专属权益,升级后即可使用'
+      if (this.quotaType === 'AI_CHAT') return '您本月 AI 对话已用完(' + this.total + '/' + this.total + '),升级 FAMILY 享不限次畅聊'
+      if (this.quotaType === 'REPORT_READING') return '本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读'
+      if (this.quotaType === 'REPORT_UPLOAD') return '本月报告上传额度已用完(' + this.total + '/' + this.total + '),升级 FAMILY 解锁更多'
+      return '本月额度已用完,升级 FAMILY 解锁更多权益'
+    }
+  },
+  methods: {
+    show() {
+      this.visible = true
+    },
+    hide() {
+      this.visible = false
+    },
+    handleKnow() {
+      this.hide()
+      this.$emit('know')
+    },
+    handleLearn() {
+      this.hide()
+      uni.navigateTo({ url: '/pages/membership/benefits' })
+      this.$emit('learn')
+    },
+    handleUpgrade() {
+      this.hide()
+      uni.navigateTo({ url: '/pages/membership/upgrade' })
+      this.$emit('upgrade')
+    },
+    handleMaskClick() {
+      if (this.type === 'soft') {
+        this.hide()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.upgrade-guide-mask {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.upgrade-guide-content {
+  width: 600rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 48rpx 36rpx 36rpx;
+  text-align: center;
+}
+.upgrade-guide-icon {
+  font-size: 72rpx;
+  margin-bottom: 24rpx;
+}
+.upgrade-guide-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.upgrade-guide-desc {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+  margin-bottom: 36rpx;
+}
+.upgrade-guide-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.btn-know, .btn-learn, .btn-upgrade {
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+.btn-know {
+  background: #f5f5f5;
+  color: #666;
+}
+.btn-learn {
+  background: transparent;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+.btn-upgrade {
+  background: #F97316;
+  color: #fff;
+}
+</style>

+ 33 - 3
cfc-frontend/pages/ai/chat.vue

@@ -152,17 +152,25 @@
      </view>
      <view class="chat-disclaimer">本服务为生成内容,结果仅供参考</view>
   </view>
+  <!-- 配额进度条 -->
+  <view class="quota-bar-wrapper" v-if="aiChatQuota">
+    <quota-progress-bar :remaining="aiChatQuota.remaining || 0" :total="aiChatQuota.total || 3" name="本月AI对话剩余"></quota-progress-bar>
+  </view>
+  <!-- 升级引导弹窗 -->
+  <upgrade-guide-modal ref="upgradeGuideModal"></upgrade-guide-modal>
 </template>
 
 <script>
-import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership } from '../../utils/api.js'
+import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership, getAllQuotaStatus } from '../../utils/api.js'
 import { sttTranscribe } from '../../utils/api.js'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import TaskCard from '../../components/AITaskCard.vue'
+import QuotaProgressBar from '../../components/QuotaProgressBar.vue'
+import UpgradeGuideModal from '../../components/UpgradeGuideModal.vue'
 import config from '@/config.js'
 
 export default {
-  components: { BaseEmpty, TaskCard },
+  components: { BaseEmpty, TaskCard, QuotaProgressBar, UpgradeGuideModal },
   data() {
     return {
       inputText: '',
@@ -182,7 +190,8 @@ export default {
       mascotName: '',
       mascotIcon: '',
       isRecording: false,
-      recorderManager: null
+      recorderManager: null,
+      aiChatQuota: null
     }
   },
   computed: {
@@ -268,6 +277,13 @@ export default {
         uni.navigateTo({ url: '/pages/membership/upgrade' })
       }, 1500)
     })
+    // 加载 AI 配额(BASIC用户可见)
+    var selfQuota = this
+    getAllQuotaStatus().then(function(res) {
+      if (res.data && res.data.AI_CHAT) {
+        selfQuota.aiChatQuota = res.data.AI_CHAT
+      }
+    }).catch(function() {})
   },
   onShow() {
     if (this.pageLoaded) {
@@ -354,6 +370,15 @@ export default {
     const text = this.inputText.trim()
     if (!text || this.sending) return
 
+    // BASIC 配额检查
+    if (this.aiChatQuota && this.aiChatQuota.remaining <= 0) {
+      this.$refs.upgradeGuideModal.type = 'soft'
+      this.$refs.upgradeGuideModal.quotaType = 'AI_CHAT'
+      this.$refs.upgradeGuideModal.total = this.aiChatQuota.total
+      this.$refs.upgradeGuideModal.show()
+      return
+    }
+
     // 立即显示用户消息
     const tempMsg = { id: 'temp_' + Date.now(), query: text, answer: '' }
     this.messages.push(tempMsg)
@@ -942,4 +967,9 @@ export default {
   padding: 16rpx 0 20rpx;
   background: #f5f5f5;
 }
+.quota-bar-wrapper {
+  padding: 16rpx 30rpx;
+  background: #fff;
+  border-top: 1rpx solid #f5f5f5;
+}
 </style>

+ 17 - 7
cfc-frontend/pages/membership/index.vue

@@ -62,12 +62,14 @@
           <view class="compare-row header-row">
             <text class="compare-cell cell-feature">权益项</text>
             <text class="compare-cell cell-free">免费</text>
+            <text class="compare-cell cell-basic">体验会员</text>
             <text class="compare-cell cell-family">家庭会员</text>
             <text class="compare-cell cell-premium">高级会员</text>
           </view>
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
             <text class="compare-cell cell-feature">{{ item.name }}</text>
             <text class="compare-cell cell-free">{{ item.free }}</text>
+            <text class="compare-cell cell-basic">{{ item.basic }}</text>
             <text class="compare-cell cell-family">{{ item.family }}</text>
             <text class="compare-cell cell-premium">{{ item.premium }}</text>
           </view>
@@ -89,13 +91,14 @@ export default {
       currentLevelName: '免费用户',
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
       compareData: [
-        { name: '商品折扣', free: '普通价格', family: '会员折扣', premium: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', family: '全部功能', premium: '全部功能+优先响应' },
-        { name: '管家服务', free: '—', family: '—', premium: '每周一次' },
-        { name: '推荐佣金', free: '不可参与', family: '可参与', premium: '可参与' },
-        { name: '专属活动', free: '部分参与', family: '全部参与', premium: '优先参与' },
-        { name: '成长报告', free: '月度', family: '每周+深度', premium: '每日+深度+专家解读' },
-        { name: '任务配额', free: '每日5个', family: '不限量', premium: '不限量+优先审核' }
+        { 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: '优先参与' }
       ],
       myButlerInfo: null,
       isL2Active: false
@@ -122,6 +125,13 @@ export default {
         if (m.levelCode && m.levelCode !== 'FREE') {
           that.isFree = false
           that.currentLevelName = m.levelName || '家庭会员'
+          if (m.levelCode === 'BASIC') {
+            that.currentLevelName = '体验会员'
+          } else if (m.levelCode === 'FAMILY') {
+            that.currentLevelName = '家庭会员'
+          } else if (m.levelCode === 'PREMIUM') {
+            that.currentLevelName = '高级会员'
+          }
         } else {
           that.isFree = true
           that.currentLevelName = '免费用户'

+ 4 - 0
cfc-frontend/pages/membership/upgrade.vue

@@ -142,6 +142,9 @@ export default {
         if (self.selectedLevel === 'FAMILY') {
           return p.key === 'yearly'
         }
+        if (self.selectedLevel === 'BASIC') {
+          return p.key === 'monthly'
+        }
         if (self.selectedLevel === 'PREMIUM') {
           return p.key === 'monthly' || p.key === 'yearly'
         }
@@ -167,6 +170,7 @@ export default {
     getLevelName(code) {
       var map = {
         FREE: '免费版',
+        BASIC: '体验会员',
         FAMILY: '家庭会员',
         PREMIUM: '高级会员'
       }

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

@@ -456,6 +456,10 @@ export const getMyMembership = () => {
 	return request('/api/membership/my', 'POST')
 }
 
+export const getAllQuotaStatus = () => {
+	return request('/api/basic-quota/status/all', 'POST')
+}
+
 export const getCurrentLevel = () => {
 	return request('/api/membership/current', 'POST')
 }

+ 495 - 0
docs/superpowers/specs/2026-09-10-basic-membership-design.md

@@ -0,0 +1,495 @@
+# 浠艾福·体验会员(BASIC)PRD
+
+> 文档版本:v1.0
+> 创建日期:2026-09-10
+> 状态:PRD 评审中
+
+---
+
+## 1. 产品背景与目标
+
+### 1.1 背景
+
+浠艾福平台当前会员体系仅有 `FREE → FAMILY → PREMIUM → PROVIDER` 四档,缺乏中间层:
+- 免费用户从未付费,转化路径过长(直接跳到 131.4 元/月)
+- 有尝试付费意愿但价格敏感的用户流失在免费层
+- 无法验证用户是否真正需要 AI 健康顾问、报告解读等高价值功能
+
+### 1.2 目标
+
+新增 **BASIC(体验会员)** 层级,定价 **9.9 元/月**,实现:
+
+| 指标 | 目标值 | 说明 |
+|------|-------|------|
+| 免费→BASIC 付费转化率 | ≥ 3% | 月活用户中完成首单的比例 |
+| BASIC→FAMILY 升级转化率 | ≥ 15% | BASIC 用户在有效期内的升级比例 |
+| BASIC 次月留存率 | ≥ 40% | 首月付费用户次月仍然活跃的比例 |
+| BASIC 月均 AI 对话次数 | ≥ 2.5 次 | 月度配额 3 次中实际使用率 |
+
+### 1.3 核心设计原则
+
+- **不是免费试用**:TrialMembership(7 天免费)已存在,BASIC 是独立付费产品,二者不重叠
+- **限次不是限制,是钩子**:配额制制造"用完还想用"的周期回归动力
+- **数据不消失**:会员到期后成长记录、五维能量曲线等数据保留 30 天,迁移成本高是粘性本质
+- **软引导 > 硬屏蔽**:让用户看到"我本来可以",而非"我被挡在外面"
+
+---
+
+## 2. 等级体系定位
+
+```
+FREE(免费)
+    │  9.9元/月
+    ▼
+BASIC(体验会员)← 本 PRD
+    │  131.4元/月(活动价)
+    ▼
+FAMILY(家庭会员)
+    │
+    ▼
+PREMIUM(尊享会员)
+    │
+    ▼
+PROVIDER(服务商)
+```
+
+### 2.1 BASIC 等级定义
+
+| 字段 | 值 | 说明 |
+|------|---|------|
+| `level_code` | `BASIC` | 数据库新增等级编码 |
+| `level_name` | `体验会员` | 前端展示名 |
+| `price_monthly` | `990`(分)| 9.9 元 |
+| `price_quarterly` | NULL | 不支持季付 |
+| `price_yearly` | NULL | 不支持年付 |
+| `features` | 见 §3.2 | 功能权限 JSON |
+| `max_children` | `3` | 可添加孩子上限(FREE=0, FAMILY=10) |
+| `max_tasks_per_day` | `3` | 孩子每日任务上限 |
+| `ai_review_enabled` | `0` | 不做 AI 审核 |
+| `priority_support` | `0` | 不做 |
+| `teacher_consultation` | `0` | 不做 |
+
+### 2.2 与其他等级的核心差异
+
+| 对比项 | FREE | BASIC | FAMILY |
+|--------|------|-------|--------|
+| 月费 | 0 | 9.9 元 | 131.4 元 |
+| AI 对话 | ❌ | 3 次/月 | 不限 |
+| 报告解读 | ❌ | 1 次/月 | 不限 |
+| 测评预约 | 仅查看 | 1 次/月 | 不限 |
+| 能量沙盘 | 🔒 | ✅ | ✅ |
+| 专属教练 | 🔒 | 🔒 | ✅ 月1次 |
+| 管家分配 | 🔒 | 🔒 | ✅ |
+
+---
+
+## 3. 功能权益矩阵
+
+### 3.1 完整权益表
+
+| 功能模块 | 子功能 | FREE | **BASIC** | FAMILY | PREMIUM | BASIC 限制说明 |
+|---------|--------|:---:|:---:|:---:|:---:|--------------|
+| **五维能量** | 基础能量分 | ✅ | ✅ | ✅ | ✅ | |
+| | 趋势曲线/对比 | 🔒 | ✅ | ✅ | ✅ | 付费解锁深度数据 |
+| | 能量沙盘 | 🔒硬门禁 | ✅ | ✅ | ✅ | 复用 `UnlockGate.MEMBERSHIP` |
+| **AI 服务** | AI 健康顾问 | ❌ | **3次/月** | 不限 | 不限 | `_lite` 后缀配额型 |
+| | 报告 AI 解读 | 🔒 | **1次/月** | 不限 | 深度解读 | `_lite` 后缀配额型 |
+| **成长与测评** | 成长计划 | 创建 | 创建+基础查看 | 完整+模板 | +教练随访 | `_lite` 后缀 |
+| | 测评预约/订单 | 查看 | **1次/月** | 不限 | 不限+优先 | `_lite` 后缀,**硬门禁** |
+| | 成长记录 | 创建 | 创建+查看 | 完整+导出 | 完整+AI生成 | 资产型,不限制 |
+| | 任务/心愿 | 基础模板 | 更多模板 | 全部模板 | +定制 | `_lite` 后缀 |
+| **健康与家庭** | 健康报告上传 | ❌ | **2份/月** | 不限 | 不限+AI解析 | `_lite` 后缀配额型 |
+| | 家庭邀请码 | ❌ | 创建+邀请 | 完整关系图 | 完整 | BASIC 是家庭功能入口 |
+| | 饮食推荐 | 基础 | 基础 | 个性化 | 深度定制 | 引流功能 |
+| **商业与服务** | 积分商城 | 可见 | 可见+购买 | 可见+折扣 | 额外折扣 | |
+| | 优惠券 | ❌ | 平台券 | 会员券 | 专属券 | |
+| | 专属健康教练 | 🔒 | 🔒软引导 | ✅ 月1次 | ✅ 周1次 | FAMILY+ 专属 |
+| | 规划师一对一咨询 | 🔒 | 🔒软引导 | ✅ | ✅ 优先 | 核心变现保留 |
+| | 成长档案导出 | 🔒 | 🔒软引导 | ✅ | ✅ | FAMILY+ 专属 |
+| | 管家分配(butler) | 🔒 | 🔒软引导 | ✅ | ✅ 优先 | FAMILY+ 专属 |
+
+### 3.2 features JSON
+
+```json
+[
+  "free_activities",
+  "free_courses",
+  "discount_purchase",
+  "invite_family",
+  "mascot_ai_lite",
+  "ai_report_reading_lite",
+  "assessment_quota_lite",
+  "health_report_upload_lite",
+  "growth_plan_lite",
+  "task_template_lite"
+]
+```
+
+**命名规范**:`_lite` 后缀表示"功能可用,但有月度配额限制",需在配额系统中额外校验。
+
+---
+
+## 4. 配额系统(粘性引擎)
+
+### 4.1 设计决策
+
+> **决策2:配额按 `user_id`(家长账号)独立,不按 family_id 共享。**
+
+**理由**:
+- 一个家庭可能有多个家长账号,各人独立付费各自获得配额更公平
+- 与现有 `User.memberLevel` 语义一致(等级挂在用户级别)
+- 简化逻辑:不需要跨用户分摊配额
+
+### 4.2 月度配额详情
+
+| 配额类型 Code | 配额上限 | 刷新周期 | 超限行为 | 对应 feature |
+|--------------|---------|----------|---------|-------------|
+| `AI_CHAT` | 3 次 | 每月 1 号 00:00 | 软引导:Toast "本月额度已用完,升级 FAMILY 享不限次" | `mascot_ai_lite` |
+| `REPORT_READING` | 1 次 | 每月 1 号 00:00 | 软引导:Toast "本月报告解读额度已用完" | `ai_report_reading_lite` |
+| `ASSESSMENT_QUOTA` | 1 次 | 每月 1 号 00:00 | **硬门禁**:禁止创建预约,弹窗"测评预约为 FAMILY 专属" | `assessment_quota_lite` |
+| `REPORT_UPLOAD` | 2 份 | 每月 1 号 00:00 | 软引导:显示"本月还可上传 X 份" | `health_report_upload_lite` |
+
+### 4.3 配额存储模型
+
+```sql
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+```
+
+---
+
+## 5. 用户生命周期流转
+
+### 5.1 核心路径
+
+```
+免费用户
+  ↓ 浏览权益页 / 点击"9.9元开通体验"
+支付 9.9 元(微信支付)
+  ↓ 回调成功
+激活 BASIC 等级:
+  - user.memberLevel = "BASIC"
+  - user.memberExpireTime = 30天后
+  - family_memberships 写入 BASIC 记录
+  - basic_quota_usage 初始化 4 类配额(used=0)
+  ↓
+使用期(第 1-30 天):
+  - AI 对话 / 报告解读 / 测评预约 / 报告上传
+  - 每次使用消耗对应配额
+  - 配额接近耗尽时软引导提示
+  ↓
+第 25 天:推送到期提醒("还剩 5 天")
+  ↓
+第 30 天:到期降级为 FREE
+  - user.memberLevel = "FREE"
+  - user.memberExpireTime = null
+  - family_memberships 标记 expired
+  - basic_quota_usage 保留历史,不再刷新
+  ↓
+到期后 30 天内:数据保留,续费可恢复权益
+  ↓ (可选)
+升级 FAMILY:补差价(131.4 - 0 = 131.4 元,按剩余天数折算可选)
+```
+
+### 5.2 关键规则
+
+- **不做自动续费升级**(决策1:A):到期后必须手动选择续费 BASIC 或升级 FAMILY
+- **到期不删数据**:成长记录、五维能量曲线、健康报告保留 30 天
+- **配额不继承**:到期后配额清零,续费/升级后重新分配
+
+---
+
+## 6. 软引导 vs 硬门禁 UX 规范
+
+### 6.1 规则表
+
+| 类型 | 触发条件 | UI 表现 | 示例文案 |
+|------|---------|---------|---------|
+| **硬门禁** | 测评预约超配额 | 弹窗阻断,仅显示"升级 FAMILY"按钮,禁止关闭后继续操作 | "测评预约为 FAMILY 会员专属权益,升级后即可预约" |
+| **软引导** | AI 对话超限 | 页面内 Toast,可关闭,保留"我知道了"和"了解权益"两个按钮 | "您本月 AI 对话已用完(3/3),升级 FAMILY 享不限次畅聊" |
+| **软引导** | 报告解读超限 | 同上 | "本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读" |
+| **进度可见** | 配额未用完 | 功能入口上方显示剩余次数进度条 | "本月 AI 对话剩余 2 次" |
+| **软引导** | 报告上传超限 | Toast + 上传按钮置灰 | "本月报告上传额度已用完(2/2),升级 FAMILY 解锁更多" |
+
+### 6.2 弹窗统一组件
+
+新建 `UpgradeGuideModal.vue`(小程序组件),统一处理所有软/硬引导场景:
+- 接收参数:`type`(soft/hard)、`quotaType`、`remaining`、`total`
+- 展示:图标 + 文案 + 行动按钮(硬门禁只有"升级",软引导有"知道了"+"了解详情")
+- 埋点:`basic_upgrade_click` 记录触发来源
+
+---
+
+## 7. 数据模型变更
+
+### 7.1 `membership_levels` 新增种子数据
+
+```sql
+-- 在 DatabaseInitializer.runMigrations() 中添加:
+
+// 迁移: membership_levels 新增 BASIC 体验会员等级
+try {
+    jdbcTemplate.execute("INSERT IGNORE INTO membership_levels " +
+        "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
+        "VALUES ('BASIC', '体验会员', " +
+        "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
+        "990, " +
+        "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
+        "3, 3)");
+    log.info("已初始化BASIC体验会员等级");
+} catch (Exception e) {
+    // 等级已存在,忽略
+}
+```
+
+### 7.2 `membership_benefits` 新增 BASIC 权益
+
+```sql
+-- 在 DatabaseInitializer.runMigrations() 中添加:
+
+// 迁移: membership_benefits 新增 BASIC 会员权益
+try {
+    jdbcTemplate.execute("INSERT IGNORE INTO membership_benefits " +
+        "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
+        "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
+        "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
+        "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
+        "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " +
+        "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)");
+    log.info("已初始化BASIC会员权益列表");
+} catch (Exception e) {
+    // 已存在,忽略
+}
+```
+
+### 7.3 新建 `basic_quota_usage` 表
+
+```sql
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+```
+
+### 7.4 schema.sql 同步更新
+
+- 在 `schema.sql` 末尾追加 `basic_quota_usage` 建表语句
+- 在 `membership_levels` INSERT 补充 BASIC 等级 seed 数据
+- 在 `membership_benefits` INSERT 补充 BASIC 权益 seed 数据
+
+---
+
+## 8. 接口变更清单
+
+### 8.1 后端修改
+
+#### 8.1.1 `MembershipService.canUseFeature()` — 新增 BASIC 分支
+
+```java
+// 在现有 FREE / FAMILY / PREMIUM / PROVIDER 分支之间插入:
+if ("BASIC".equals(level)) {
+    return "free_activities".equals(feature)
+            || "free_courses".equals(feature)
+            || "discount_purchase".equals(feature)
+            || "invite_family".equals(feature)
+            || "mascot_ai_lite".equals(feature)
+            || "ai_report_reading_lite".equals(feature)
+            || "assessment_quota_lite".equals(feature)
+            || "health_report_upload_lite".equals(feature)
+            || "growth_plan_lite".equals(feature)
+            || "task_template_lite".equals(feature);
+}
+```
+
+#### 8.1.2 `MembershipService.hasPermission()` — 无需改
+
+该接口已通过 features JSON 解析判断,BASIC 写入 features 后即自动支持,无需代码修改。
+
+#### 8.1.3 新增 `BasicQuotaService.java`
+
+```java
+@Service
+public class BasicQuotaService {
+
+    // 查询指定用户指定月份某配额的剩余次数
+    public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType);
+
+    // 尝试扣减配额,返回是否成功(配额不足返回 false + 提示文案)
+    public QuotaConsumeResult consumeQuota(Long userId, String quotaType);
+
+    // 批量刷新所有 BASIC 用户当月的配额(定时任务调用)
+    public void refreshMonthlyQuota();
+
+    // 检查用户当前是否为有效的 BASIC 会员
+    public boolean isActiveBasicMember(Long userId);
+
+    // 获取 BASIC 用户到期时间
+    public Date getExpireTime(Long userId);
+}
+```
+
+**QuotaStatusDTO**:
+```java
+public class QuotaStatusDTO {
+    private String quotaType;     // AI_CHAT / REPORT_READING / ...
+    private int total;            // 总配额
+    private int used;             // 已用
+    private int remaining;        // 剩余
+    private Date periodStart;     // 周期开始
+    private Date periodEnd;       // 周期结束
+}
+```
+
+**QuotaConsumeResult**:
+```java
+public class QuotaConsumeResult {
+    private boolean success;      // 是否扣减成功
+    private int remaining;        // 扣减后剩余
+    private String toastMsg;      // 配额不足时的 Toast 文案(可为 null)
+    private String actionUrl;     // 跳转升级页面的 URL(可为 null)
+}
+```
+
+#### 8.1.4 业务入口集成点
+
+| 入口 Service | 方法 | 集成逻辑 |
+|-------------|------|---------|
+| `AiGateway.chat()` | 对话入口 | 先检查 `isActiveBasicMember(userId)`,若 true 则调用 `consumeQuota(AI_CHAT)`,失败返回 403 + 软引导文案 |
+| `ReportSummaryService.parseReport()` | 报告解读 | 同上,quotaType=`REPORT_READING` |
+| `AssessmentAppointmentService.create()` | 创建测评预约 | 同上,quotaType=`ASSESSMENT_QUOTA`,**硬门禁**,禁止继续 |
+| `DanReportUploadService.upload()` | 报告上传 | 同上,quotaType=`REPORT_UPLOAD`,超限时隐藏上传按钮 + 显示进度提示 |
+
+### 8.2 定时任务
+
+| Task | Cron | 职责 |
+|------|------|------|
+| `BasicQuotaRefreshTask` | `0 0 0 1 * ?` | 每月 1 号 00:00,遍历所有 BASIC 会员,重置 `used_count=0`,更新 `period_start/period_end` 为本月 |
+| `BasicExpiryNoticeTask` | `0 0 10 * * ?` | 每天 10:00,查找 `expireTime` 在 1-5 天内的 BASIC 用户,推送微信模板消息提醒 |
+| `BasicDowngradeExpiredTask` | `0 0 4 * * ?` | 每天 4:00,执行过期降级(复用现有 `MembershipService.downgradeExpiredMembers()`,需确认其支持 BASIC) |
+
+---
+
+## 9. 前端变更清单
+
+### 9.1 小程序(cfc-frontend)
+
+| 页面/组件 | 变更类型 | 说明 |
+|----------|---------|------|
+| 会员权益页 `pages/profile/membership` | 修改 | 新增 BASIC 卡片,读 `membership_benefits` BASIC 行动态渲染 |
+| 会员开通页 `pages/profile/upgrade` | 修改 | 新增"9.9元/月"选项,仅支持月付,不显示年付/季付 |
+| AI 对话入口 | 修改 | 显示"本月剩余 X/3 次",超限调用 `UpgradeGuideModal(type=soft)` |
+| 测评预约入口 | 修改 | 显示"本月剩余 X/1 次",超限调用 `UpgradeGuideModal(type=hard)` 并禁用按钮 |
+| 报告上传/解读入口 | 修改 | 显示上传/解读剩余次数,超限软引导 |
+| 个人中心-会员状态卡片 | 修改 | 显示"体验会员 · 剩余 X 天",倒计时到期 |
+| 能量沙盘入口 | 无需改 | `UnlockGate.MEMBERSHIP` 已覆盖(`canUseFeature("mascot_ai_lite")` = true) |
+
+### 9.2 新增组件
+
+- **`UpgradeGuideModal.vue`**:统一软/硬引导弹窗,参数化 `type`、`quotaType`、文案
+- **`QuotaProgressBar.vue`**:配额进度条组件,通用展示"已用 X / 总额 Y"
+
+### 9.3 Web 管理端(cfc-web)
+
+| 页面 | 变更类型 | 说明 |
+|------|---------|------|
+| 会员等级管理 | 修改 | 自动显示 BASIC 等级(读 DB) |
+| BASIC 配额看板 | **新增** | 展示本月各配额使用率、BASIC 用户分布、到期提醒列表 |
+| 配额手动重置接口 | **新增** | 运营可手动重置某用户的当月配额(应急用) |
+
+---
+
+## 10. 埋点与数据指标
+
+### 10.1 事件定义
+
+| 事件名 | 触发时机 | 关键属性 |
+|--------|---------|---------|
+| `basic_pay_click` | 点击"9.9元开通"按钮 | `from_page`(会员页/升级页/弹窗) |
+| `basic_pay_success` | 支付回调成功 | `order_no`, `amount`, `family_id` |
+| `basic_quota_consume` | 每次配额扣减 | `quota_type`, `remaining`, `period_start` |
+| `basic_quota_exhausted` | 某类配额归零 | `quota_type`, `family_id`, `user_id` |
+| `basic_upgrade_click` | 点击升级 FAMILY 按钮 | `trigger_type`(quota_exhausted/expiry_near/manual) |
+| `basic_upgrade_success` | 升级支付成功 | `from_level`, `to_level`, `family_id`, `upgrade_amount` |
+| `basic_expire_downgrade` | 到期自动降级 | `user_id`, `days_since_paid`, `has_data_retained` |
+| `basic_renew_success` | 续费 BASIC | `user_id`, `renew_days` |
+
+### 10.2 核心看板指标
+
+- BASIC 付费用户数 / 日(按 `basic_quota_usage` 首次写入 date 统计)
+- BASIC 月度配额使用率(各类型 `used_count / total_count`)
+- BASIC→FAMILY 升级转化漏斗(付费→首次使用→配额耗尽→点击升级→支付成功)
+- BASIC 次月留存曲线(按首次付费月份分层)
+- BASIC 用户 ARPU(含交叉销售:商品、测评等)
+
+---
+
+## 11. 兼容性风险与缓解
+
+| 风险点 | 影响 | 缓解方案 |
+|--------|------|---------|
+| `canUseFeature` 硬编码分支遗漏 BASIC | BASIC 用户调用新功能时报权限拒绝 | 单元测试覆盖所有 featureCode,发布前全量冒烟 |
+| 现有 TrialMembership 与 BASIC 支付流程冲突 | 用户刚结束 7 天试用就被催促付费,体验差 | Trial 结束第 8 天推"9.9 元续享权益"而非"开通会员",文案区分 |
+| 配额刷新定时任务失败导致用户次月无配额 | 用户投诉、信任受损 | 任务幂等(先 DELETE 再 INSERT);增加手动重置接口;监控告警 |
+| `MembershipService.downgradeExpiredMembers()` 不识别 BASIC | BASIC 到期后等级不降级 | 在现有方法中补充 `BASIC` 的判断逻辑 |
+| 前端缓存导致权益页不显示 BASIC | 新用户看不到 9.9 选项 | 版本号机制强制刷新;或在 `MembershipController.getLevels()` 接口中缓存 TTL=5 分钟 |
+| 9.9 元价格过低冲击 FAMILY 定价 | 现有 FAMILY 用户要求退费改买 BASIC | 1) BASIC 不支持年付 2) FAMILY 升级时展示"专属权益对比"价值锚定 3) 运营控制推送人群(仅免费活跃用户) |
+| 多家长同家庭各自持 BASIC 配额 | 配额按 user_id 独立,同一家庭可能有两个 BASIC 用户 | 这是决策2的预期行为,不做限制;但前端展示时统一显示家庭视角的汇总信息 |
+
+---
+
+## 12. 发布计划
+
+| 阶段 | 时间 | 交付物 | 验收标准 |
+|------|------|--------|---------|
+| **P0 核心链路** | T+3 天 | DB 迁移 + BASIC 等级 + 支付开通 + 基础权限判断 | 9.9 元支付成功 → BASIC 生效 → 能进沙盘/AI 对话 3 次 |
+| **P1 配额系统** | T+5 天 | `BasicQuotaService` + 4 个业务入口配额扣减 + 定时刷新 | 月度配额正确扣减/刷新,超限软/硬引导生效 |
+| **P2 前端展示** | T+7 天 | 权益页/开通页/配额显示/升级弹窗 | 免费用户可见 BASIC 卡片,付费后正确显示剩余次数 |
+| **P3 运营配套** | T+10 天 | 推送模板 + 埋点 + 管理端配额看板 | 到期提醒/额度耗尽提醒自动发送,看板数据可查 |
+| **灰度/全量** | T+12 天 | 5% → 20% → 100% 流量灰度 | 核心指标无异常波动,全量放开 |
+
+---
+
+## 13. 验收用例(关键路径)
+
+| 用例 ID | 场景 | 预期结果 |
+|--------|------|---------|
+| UC-01 | 免费用户支付 9.9 元 | `user.memberLevel=BASIC`、`memberExpireTime=30天后`、`family_memberships` 写入 BASIC 记录、`basic_quota_usage` 初始化 4 类配额(used=0) |
+| UC-02 | BASIC 用户第 1 次 AI 对话 | `canUseFeature(mascot_ai_lite)=true`,配额扣减成功,`remaining=2/3`,返回对话结果 |
+| UC-03 | BASIC 用户第 4 次 AI 对话 | `consumeQuota(AI_CHAT)` 返回 false,前端 Toast "本月额度已用完(3/3),升级 FAMILY 享不限次" |
+| UC-04 | BASIC 用户第 2 次测评预约 | `consumeQuota(ASSESSMENT_QUOTA)` 硬门禁,弹窗阻断,禁止继续操作 |
+| UC-05 | 月初定时任务刷新 | 所有 BASIC 用户 `basic_quota_usage.used_count=0`,`period_start` 更新为本月 1 号 |
+| UC-06 | BASIC 到期自动降级 | `downgradeExpiredMembers()` 跑批,`user.memberLevel=FREE`,`memberExpireTime=null`,配额表保留历史但不刷新 |
+| UC-07 | BASIC 到期前 5 天推送 | 收到微信模板消息"您的体验会员 5 天后到期,还有 X 次 AI 对话未使用" |
+| UC-08 | BASIC 用户升级 FAMILY | 支付差价 → `memberLevel=FAMILY`,配额表停止扣减(FAMILY 走不限逻辑) |
+| UC-09 | 同一家庭两个 BASIC 用户 | 各自配额独立(user_id 维度),互不影响 |
+| UC-10 | Trial 结束当天推荐 BASIC | 第 8 天推送"9.9 元续享权益"(非"开通会员"),文案与 Trial 区分 |
+
+---
+
+## 14. 已确认的设计决策
+
+| # | 决策项 | 选择 | 说明 |
+|---|--------|------|------|
+| 1 | 到期后是否自动升级 FAMILY | **A. 否** | 到期必须手动选择续费 BASIC 或升级 FAMILY,不做自动续费 |
+| 2 | 配额按 family_id 还是 user_id 共享 | **B. 按 user_id 独立** | 每个家长账号独立配额,同一家庭多个 BASIC 用户配额互不干扰 |
+| 3 | 是否做阶梯定价(首月 9.9/次月 19.9) | **A. 固定 9.9 元/月** | 价格透明,长期稳定,利于口碑和留存 |