ソースを参照

feat: remove BASIC trial membership & add gut health questionnaire project

- Remove BASIC (9.9元/月体验版) member level from membership_levels
- Migrate storage users member_level from BASIC → FREE
- Remove BASIC benefits from membership_benefits table
- Mark basic_quota_usage table deprecated (preserve data, stop using)
- Add new gut_health_projects table for single-pay questionnaire project
- Add GutHealthProject entity, mapper, service, controller
- Add 6 new API endpoints for questionnaire flow:
  - /project/status - query project status
  - /project/pay - create order
  - /project/pay-notify - payment callback
  - /project/submit - submit questionnaire
  - /project/tasks - get action tasks
  - /project/checkin-status - get checkin status
  - /project/activate-after-purchase - activate family membership after purchase
  - /project/discount-voucher - get member discount voucher
- Update schemas, level_order constants, remove BASIC references throughout codebase
Sisyphus 2 日 前
親
コミット
f86dd164b3

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

@@ -6044,7 +6044,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_levels (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/BASIC/PROFESSIONAL/ENTERPRISE', " +
+                    "level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/PROFESSIONAL/ENTERPRISE', " +
                     "level_name VARCHAR(100) NOT NULL COMMENT '等级名称', " +
                     "level_desc TEXT COMMENT '等级描述', " +
                     "price_monthly INT COMMENT '月费(分)', " +
@@ -8126,7 +8126,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_benefits (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/BASIC/PROFESSIONAL/ENTERPRISE', " +
+                    "level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/PROFESSIONAL/ENTERPRISE', " +
                     "benefit_code VARCHAR(50) NOT NULL COMMENT '权益编码: AI_CHAT,FAMILY_MEMBERS等', " +
                     "benefit_name VARCHAR(100) NOT NULL COMMENT '权益名称', " +
                     "benefit_desc VARCHAR(500) COMMENT '权益描述', " +
@@ -10954,15 +10954,6 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         createDietFamilySelectedIngredientsTable();
         createSsoTicketTable();
 
-        // 迁移341: BASIC 体验会员 features JSON 补 referral_commission(迁移315 未包含,前端对比表已显示可参与)
-        try {
-            jdbcTemplate.update("UPDATE membership_levels SET features = ? WHERE level_code = 'BASIC' AND features NOT LIKE '%referral_commission%'",
-                    "[\"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\",\"referral_commission\"]");
-            log.info("迁移341: 已为 BASIC 体验会员补充 referral_commission 功能");
-        } catch (Exception e) {
-            log.warn("迁移341: 更新 BASIC features 失败: {}", e.getMessage());
-        }
-
         // 迁移327: 创建 report_feedback 表(报告反馈+AI解答)
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_feedback (" +
@@ -11028,55 +11019,6 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.warn("迁移314: meal_configs 添加索引 idx_config_date 失败(可能已存在): {}", 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)");
-            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) {
-            // 表已存在,忽略
-        }
 
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_stage (" +
@@ -11202,15 +11144,15 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         }
 
         // 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
-        // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6
+        // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
         // - products.min_visible_level: 最低可见会员级别编码(NULL=所有用户含未登录可见)
         // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=所有用户含未登录可见)
         ensureColumn("membership_levels", "level_order",
-                "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6'");
+                "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5'");
         try {
             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");
+                    "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
+                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
             log.info("迁移321: 已回填 membership_levels.level_order");
         } catch (Exception e) {
             log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
@@ -11801,7 +11743,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
     private void addProductMinDiscountLevel() {
         try {
             jdbcTemplate.execute("ALTER TABLE products ADD COLUMN min_discount_level VARCHAR(50) DEFAULT NULL " +
-                    "COMMENT '享受会员价所需的最低会员级别编码(FREE/BASIC/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)' AFTER min_visible_level");
+                    "COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)' AFTER min_visible_level");
             log.info("迁移350: 已添加 products.min_discount_level 列");
         } catch (Exception e) {
             // 列已存在,忽略
@@ -11814,20 +11756,20 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
     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");
+                    "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
+                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
             log.info("迁移351: 已顺延 membership_levels.level_order({} 行),BASIC=2", updated);
         } catch (Exception e) {
             log.warn("迁移351: 顺延 membership_levels.level_order 失败: {}", e.getMessage());
         }
     }
 
-    // 迁移352: 健康报告上传改为对所有登录用户免费无限,移除 REPORT_UPLOAD 配额与 BASIC 权益文案
+    // 迁移352: 健康报告上传改为对所有登录用户免费无限
     private void removeReportUploadQuota() {
         try {
             int deletedBenefits = jdbcTemplate.update(
                     "DELETE FROM membership_benefits WHERE level_code = 'BASIC' AND benefit_code = 'REPORT_UPLOAD_LITE'");
-            log.info("迁移352: 已移除 BASIC 权益 REPORT_UPLOAD_LITE({} 行)", deletedBenefits);
+            log.info("迁移352: 已清理遗留权益记录({} 行)", deletedBenefits);
         } catch (Exception e) {
             log.warn("迁移352: 移除 REPORT_UPLOAD_LITE 权益失败: {}", e.getMessage());
         }

+ 203 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/GutHealthProjectController.java

@@ -0,0 +1,203 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.GutHealthProjectService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 菌群健康调研项目(单次付费问卷)
+ *
+ * 业务流程:
+ * 1. 用户付费 19.9 元创建项目
+ * 2. 填写健康问卷(睡眠/饮食/体重/消化等菌群相关指标)
+ * 3. 生成健康建议 + 风险评分 + 一周行动任务
+ * 4. 用户获得「会员价购买菌群检测」+「一年家庭会员」资格(需手动激活)
+ * 5. 用户在小程序打卡完成一周任务后,返还同等金额的 CF 值
+ */
+@Tag(name = "菌群健康调研项目", description = "单次付费问卷 + 行动打卡 + 奖励体系")
+@RestController
+@RequestMapping("/api/gut-health")
+public class GutHealthProjectController {
+
+    @Resource
+    private GutHealthProjectService gutHealthProjectService;
+
+    /**
+     * 查询当前用户的项目状态
+     */
+    @Operation(summary = "查询项目状态")
+    @PostMapping("/project/status")
+    public Result<Map<String, Object>> getProjectStatus(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        return Result.success(gutHealthProjectService.getProjectStatus(userId));
+    }
+
+    /**
+     * 创建付费订单
+     */
+    @Operation(summary = "创建项目付费订单")
+    @PostMapping("/project/pay")
+    public Result<Map<String, Object>> createOrder(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        try {
+            return Result.success(gutHealthProjectService.createProjectOrder(userId));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 支付回调(微信/虚拟支付回调后调用)
+     */
+    @Operation(summary = "支付回调")
+    @PostMapping("/project/pay-notify")
+    public Result<Boolean> payNotify(@RequestBody Map<String, String> params) {
+        String orderNo = params.get("orderNo");
+        String transactionId = params.get("transactionId");
+        String payMethod = params.get("payMethod");
+        if (orderNo == null || transactionId == null || payMethod == null) {
+            return Result.error(400, "缺少必要参数");
+        }
+        return Result.success(gutHealthProjectService.processPaymentCallback(orderNo, transactionId, payMethod));
+    }
+
+    /**
+     * 提交问卷答案,生成健康建议 + 行动任务
+     */
+    @Operation(summary = "提交问卷")
+    @PostMapping("/project/submit")
+    public Result<Map<String, Object>> submitQuestionnaire(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> answers = (List<Map<String, Object>>) params.get("answers");
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        if (answers == null || answers.isEmpty()) {
+            return Result.error(400, "请完整填写问卷答案");
+        }
+        try {
+            return Result.success(gutHealthProjectService.submitQuestionnaire(userId, projectId, answers));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取行动任务列表
+     */
+    @Operation(summary = "获取行动任务")
+    @PostMapping("/project/tasks")
+    public Result<List<Map<String, Object>>> getActionTasks(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        return Result.success(gutHealthProjectService.getActionTasks(userId, projectId));
+    }
+
+    /**
+     * 打卡某个任务
+     */
+    @Operation(summary = "任务打卡")
+    @PostMapping("/project/task/checkin")
+    public Result<Map<String, Object>> checkinTask(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        int taskIndex = params.get("taskIndex") != null ? ((Number) params.get("taskIndex")).intValue() : -1;
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        if (taskIndex < 0) {
+            return Result.error(400, "缺少 taskIndex");
+        }
+        try {
+            return Result.success(gutHealthProjectService.checkinTask(userId, projectId, taskIndex));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取打卡进度
+     */
+    @Operation(summary = "获取打卡进度")
+    @PostMapping("/project/checkin-status")
+    public Result<Map<String, Object>> getCheckinStatus(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        return Result.success(gutHealthProjectService.getCheckinStatus(userId, projectId));
+    }
+
+    /**
+     * 购买菌群检测后激活一年家庭会员
+     */
+    @Operation(summary = "购买后激活家庭会员")
+    @PostMapping("/project/activate-after-purchase")
+    public Result<Map<String, Object>> activateAfterPurchase(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        try {
+            return Result.success(gutHealthProjectService.activateAfterPurchase(userId, projectId));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取菌群检测会员价购买凭证
+     */
+    @Operation(summary = "获取会员价购买凭证")
+    @PostMapping("/project/discount-voucher")
+    public Result<Map<String, Object>> getDiscountVoucher(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long projectId = params.get("projectId") != null ? ((Number) params.get("projectId")).longValue() : null;
+        if (projectId == null) {
+            return Result.error(400, "缺少 projectId");
+        }
+        try {
+            return Result.success(gutHealthProjectService.getMicrobeDiscountVoucher(userId, projectId));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+}

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

@@ -17,7 +17,7 @@ public class FamilyMembership implements Serializable {
 
     private Long familyId;
 
-    private String levelCode;  // FREE/BASIC/PROFESSIONAL/ENTERPRISE
+    private String levelCode;  // FREE/PROFESSIONAL/ENTERPRISE
 
     private Date startDate;
 

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GutHealthProject.java

@@ -0,0 +1,55 @@
+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("gut_health_projects")
+public class GutHealthProject implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long familyId;
+
+    private Long memberId;
+
+    private String status;
+
+    private String questions;
+
+    private String healthRecommendations;
+
+    private String riskScores;
+
+    private Integer microbeTestRecommended;
+
+    private Date paidAt;
+
+    private Date completedAt;
+
+    private Integer memberDiscountQualified;
+
+    private Integer familyMembershipQualified;
+
+    private Integer familyMembershipActivated;
+
+    private Integer cfRefundAmount;
+
+    private Integer cfRefunded;
+
+    private Integer completedTasksCount;
+
+    private String actionTasks;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -55,7 +55,7 @@ public class Product implements Serializable {
     /** 最低可见会员级别编码(NULL=全部可见): FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME */
     private String minVisibleLevel;
 
-    /** 享受会员价所需的最低会员级别编码(NULL=所有会员均可;BASIC/FAMILY/PREMIUM等) */
+    /** 享受会员价所需的最低会员级别编码(NULL=所有会员均可;FAMILY/PREMIUM等) */
     private String minDiscountLevel;
 
     // 佣金系统 - 平台利润率(千分比)

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

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

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

@@ -6,6 +6,7 @@ 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.etotem.cfc.mapper.UserMapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -73,12 +74,6 @@ public class BasicQuotaService {
     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);
@@ -126,85 +121,10 @@ public class BasicQuotaService {
     }
 
     /**
-     * 批量刷新所有 BASIC 用户当月配额(每月1号调用)
+     * 批量刷新所有用户当月配额(每月1号调用,BASIC体验版已取消,保留接口兼容)
      */
     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;
+        log.info("BASIC配额刷新已取消(体验版会员级别已删除)");
     }
 
     private Date getMonthStart(Date date) {

+ 865 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/GutHealthProjectService.java

@@ -0,0 +1,865 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.OrderNoGenerator;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 菌群健康调研项目(单次付费问卷)
+ * <p>
+ * 业务流程:
+ * 1. 用户付费 19.9 元创建项目
+ * 2. 填写健康问卷(睡眠/饮食/体重/消化等菌群相关指标)
+ * 3. 生成健康建议 + 风险评分 + 一周行动任务
+ * 4. 用户获得「会员价购买菌群检测」资格凭证(不自动开通会员)
+ * 5. 用户在小程序/商城使用会员价购买菌群检测商品
+ * 6. 购买成功后 → 自动开通一年家庭会员
+ * 7. 用户在小程序打卡完成一周任务后,返还同等金额的 CF 值
+ */
+@Slf4j
+@Service
+public class GutHealthProjectService {
+
+    /** 默认项目价格(分) */
+    private static final int DEFAULT_PRICE = 1990;
+    /** 任务列表默认总数(一周 7 天) */
+    private static final int DEFAULT_TASK_TOTAL = 7;
+
+    @Resource
+    private GutHealthProjectMapper projectMapper;
+
+    @Resource
+    private PaymentOrderMapper paymentOrderMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMembershipMapper familyMembershipMapper;
+
+    @Resource
+    private PlatformPointsService platformPointsService;
+
+    @Resource
+    private SysConfigService sysConfigService;
+
+    @Resource
+    private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
+
+    @Resource
+    private MembershipService membershipService;
+
+    // ==================== 问卷题库 ====================
+
+    /** 问卷题目定义(菌群健康相关指标) */
+    public static List<Map<String, Object>> getQuestionnaireQuestions() {
+        List<Map<String, Object>> questions = new ArrayList<>();
+
+        // 1. 睡眠质量
+        Map<String, Object> q1 = new LinkedHashMap<>();
+        q1.put("dimension", "sleep");
+        q1.put("label", "睡眠质量");
+        q1.put("question", "您最近两周的睡眠质量如何?");
+        q1.put("options", Arrays.asList(
+                mapOf("text", "很好,入睡快且无夜间醒来", "score", 0),
+                mapOf("text", "尚可,偶尔入睡困难或早醒", "score", 1),
+                mapOf("text", "较差,经常入睡困难或夜间多次醒来", "score", 2),
+                mapOf("text", "很差,几乎每晚失眠或严重早醒", "score", 3)));
+        questions.add(q1);
+
+        // 2. 饮食结构
+        Map<String, Object> q2 = new LinkedHashMap<>();
+        q2.put("dimension", "diet");
+        q2.put("label", "饮食结构");
+        q2.put("question", "您的日常饮食结构如何?(荤素搭配、蔬菜水果摄入)");
+        q2.put("options", Arrays.asList(
+                mapOf("text", "均衡饮食,蔬菜水果充足", "score", 0),
+                mapOf("text", "基本均衡,但蔬菜水果偏少", "score", 1),
+                mapOf("text", "偏荤或偏素,蔬菜水果摄入不足", "score", 2),
+                mapOf("text", "高油高糖高盐为主,极少蔬果", "score", 3)));
+        questions.add(q2);
+
+        // 3. 体重状况
+        Map<String, Object> q3 = new LinkedHashMap<>();
+        q3.put("dimension", "weight");
+        q3.put("label", "体重状况");
+        q3.put("question", "您的体重状况?(或身高体重比)");
+        q3.put("options", Arrays.asList(
+                mapOf("text", "体重正常(BMI 18.5-23.9)", "score", 0),
+                mapOf("text", "偏瘦或轻微超重(BMI 24-26)", "score", 1),
+                mapOf("text", "超重(BMI 26-28)", "score", 2),
+                mapOf("text", "肥胖(BMI ≥28)或严重偏瘦", "score", 3)));
+        questions.add(q3);
+
+        // 4. 消化状况
+        Map<String, Object> q4 = new LinkedHashMap<>();
+        q4.put("dimension", "digestion");
+        q4.put("label", "消化状况");
+        q4.put("question", "您近期的消化功能如何?");
+        q4.put("options", Arrays.asList(
+                mapOf("text", "消化良好,无腹胀腹泻便秘", "score", 0),
+                mapOf("text", "偶尔腹胀、排气或排便不规律", "score", 1),
+                mapOf("text", "经常腹胀、腹泻或便秘交替", "score", 2),
+                mapOf("text", "长期肠胃不适,严重影响生活", "score", 3)));
+        questions.add(q4);
+
+        // 5. 运动习惯
+        Map<String, Object> q5 = new LinkedHashMap<>();
+        q5.put("dimension", "exercise");
+        q5.put("label", "运动习惯");
+        q5.put("question", "您每周的运动频率如何?");
+        q5.put("options", Arrays.asList(
+                mapOf("text", "每周3次以上,每次30分钟以上", "score", 0),
+                mapOf("text", "每周1-2次,强度适中", "score", 1),
+                mapOf("text", "偶尔运动,不规律", "score", 2),
+                mapOf("text", "几乎不运动", "score", 3)));
+        questions.add(q5);
+
+        // 6. 压力水平
+        Map<String, Object> q6 = new LinkedHashMap<>();
+        q6.put("dimension", "stress");
+        q6.put("label", "压力水平");
+        q6.put("question", "您近期的精神压力状态如何?");
+        q6.put("options", Arrays.asList(
+                mapOf("text", "轻松愉快,压力很小", "score", 0),
+                mapOf("text", "偶尔有压力,能自我调节", "score", 1),
+                mapOf("text", "经常感到压力,难以放松", "score", 2),
+                mapOf("text", "长期高压焦虑,影响睡眠和食欲", "score", 3)));
+        questions.add(q6);
+
+        // 7. 膳食纤维
+        Map<String, Object> q7 = new LinkedHashMap<>();
+        q7.put("dimension", "fiber");
+        q7.put("label", "膳食纤维");
+        q7.put("question", "您每天的主食和膳食纤维摄入情况?");
+        q7.put("options", Arrays.asList(
+                mapOf("text", "全谷物/粗粮占主食一半以上", "score", 0),
+                mapOf("text", "粗细搭配,有一定全谷物", "score", 1),
+                mapOf("text", "以精米白面为主,很少粗粮", "score", 2),
+                mapOf("text", "完全精制主食,不吃粗粮豆类", "score", 3)));
+        questions.add(q7);
+
+        // 8. 抗生素与药物
+        Map<String, Object> q8 = new LinkedHashMap<>();
+        q8.put("dimension", "antibiotic");
+        q8.put("label", "抗生素使用");
+        q8.put("question", "近半年您是否使用过抗生素或长期服药?");
+        q8.put("options", Arrays.asList(
+                mapOf("text", "未使用过", "score", 0),
+                mapOf("text", "偶尔使用(1次以内)", "score", 1),
+                mapOf("text", "使用过2-3次", "score", 2),
+                mapOf("text", "多次使用或长期服药", "score", 3)));
+        questions.add(q8);
+
+        return questions;
+    }
+
+    private static Map<String, Object> mapOf(Object... kv) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        for (int i = 0; i + 1 < kv.length; i += 2) {
+            m.put(String.valueOf(kv[i]), kv[i + 1]);
+        }
+        return m;
+    }
+
+    // ==================== 业务方法 ====================
+
+    /**
+     * 获取当前用户的项目状态
+     * @return Map: {hasProject, project, questions, price}
+     */
+    public Map<String, Object> getProjectStatus(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        GutHealthProject project = getLatestProject(userId);
+        result.put("hasProject", project != null);
+        result.put("project", project != null ? toProjectVO(project) : null);
+        result.put("questions", getQuestionnaireQuestions());
+        result.put("price", getProjectPrice());
+        return result;
+    }
+
+    /**
+     * 创建项目付费订单
+     */
+    public Map<String, Object> createProjectOrder(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new IllegalArgumentException("用户不存在");
+        }
+
+        // 已存在未完成项目,禁止重复购买
+        GutHealthProject existing = getLatestProject(userId);
+        if (existing != null && "pending".equals(existing.getStatus())) {
+            throw new RuntimeException("您已有进行中的菌群健康调研项目,请先完成问卷");
+        }
+
+        int price = getProjectPrice();
+
+        PaymentOrder order = new PaymentOrder();
+        order.setOrderNo(OrderNoGenerator.generate("GH"));
+        order.setFamilyId(user.getFamilyId());
+        order.setLevelCode("GUT_HEALTH");
+        order.setPaymentType("one_time");
+        order.setPeriod("once");
+        order.setAmount(price);
+        order.setStatus("pending");
+        order.setPeriodNo(1);
+        order.setCreatedAt(new Date());
+        order.setUpdatedAt(new Date());
+        paymentOrderMapper.insert(order);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("orderNo", order.getOrderNo());
+        result.put("amount", order.getAmount());
+        return result;
+    }
+
+    /**
+     * 处理支付成功回调:创建项目记录
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public boolean processPaymentCallback(String orderNo, String transactionId, String payMethod) {
+        PaymentOrder order = paymentOrderMapper.selectOne(
+                new LambdaQueryWrapper<PaymentOrder>()
+                        .eq(PaymentOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return false;
+        }
+        if ("paid".equals(order.getStatus())) {
+            return true; // 幂等
+        }
+
+        order.setStatus("paid");
+        order.setTransactionId(transactionId);
+        order.setPayMethod(payMethod);
+        order.setPaidAt(new Date());
+        order.setUpdatedAt(new Date());
+        paymentOrderMapper.updateById(order);
+
+        // 创建项目记录
+        GutHealthProject project = new GutHealthProject();
+        Long userId = resolveUserIdByOrder(order);
+        project.setUserId(userId);
+        project.setFamilyId(order.getFamilyId());
+        project.setStatus("pending");
+        project.setMicrobeTestRecommended(0);
+        project.setMemberDiscountQualified(0);
+        project.setFamilyMembershipQualified(0);
+        project.setFamilyMembershipActivated(0);
+        project.setCfRefundAmount(order.getAmount());
+        project.setCfRefunded(0);
+        project.setCompletedTasksCount(0);
+        project.setPaidAt(new Date());
+        project.setCreatedAt(new Date());
+        project.setUpdatedAt(new Date());
+        projectMapper.insert(project);
+
+        log.info("菌群健康项目创建成功: orderNo={}, projectId={}, userId={}, amount={}",
+                orderNo, project.getId(), project.getUserId(), order.getAmount());
+        return true;
+    }
+
+    private Long resolveUserIdByOrder(PaymentOrder order) {
+        // 从 family_memberships 或用户表回查
+        if (order.getFamilyId() == null) return null;
+        List<User> users = userMapper.selectList(
+                new LambdaQueryWrapper<User>().eq(User::getFamilyId, order.getFamilyId()));
+        if (users != null && !users.isEmpty()) {
+            return users.get(0).getId();
+        }
+        return null;
+    }
+
+    /**
+     * 提交问卷答案,生成健康建议 + 行动任务 + 奖励资格
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> submitQuestionnaire(Long userId, Long projectId, List<Map<String, Object>> answers) {
+        GutHealthProject project = projectMapper.selectById(projectId);
+        if (project == null || !userId.equals(project.getUserId())) {
+            throw new RuntimeException("项目不存在或无权操作");
+        }
+        if ("completed".equals(project.getStatus())) {
+            throw new RuntimeException("该项目已完成,无需重复提交");
+        }
+
+        // 保存答案
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            project.setQuestions(mapper.writeValueAsString(answers));
+        } catch (Exception e) {
+            project.setQuestions(answers.toString());
+        }
+
+        // 计算风险评分
+        Map<String, Integer> riskScores = computeRiskScores(answers);
+        project.setRiskScores(safeJson(riskScores));
+
+        // 生成健康建议
+        List<Map<String, Object>> recommendations = generateRecommendations(riskScores, answers);
+        project.setHealthRecommendations(safeJson(recommendations));
+
+        // 判断是否推荐菌群检测(任一维度风险 >= 2)
+        boolean recommendTest = riskScores.values().stream().anyMatch(s -> s >= 2);
+        project.setMicrobeTestRecommended(recommendTest ? 1 : 0);
+
+        // 生成一周行动任务
+        List<Map<String, Object>> tasks = generateActionTasks(riskScores);
+        project.setActionTasks(safeJson(tasks));
+        project.setCompletedTasksCount(0);
+
+        // 授予会员价购买菌群检测资格(不自动开通会员)
+        project.setMemberDiscountQualified(1);
+        project.setStatus("completed");
+        project.setFamilyMembershipQualified(0);
+        project.setFamilyMembershipActivated(0);
+        project.setUpdatedAt(new Date());
+        projectMapper.updateById(project);
+        project.setFamilyMembershipActivated(0);
+        project.setCompletedAt(new Date());
+        project.setUpdatedAt(new Date());
+        projectMapper.updateById(project);
+
+        // 返回结果
+        Map<String, Object> result = new HashMap<>();
+        result.put("project", toProjectVO(project));
+        result.put("riskScores", riskScores);
+        result.put("recommendations", recommendations);
+        result.put("microbeTestRecommended", recommendTest);
+        result.put("tasks", tasks);
+        return result;
+    }
+
+    // ==================== 任务打卡 ====================
+
+    /**
+     * 获取行动任务列表
+     */
+    public List<Map<String, Object>> getActionTasks(Long userId, Long projectId) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null || project.getActionTasks() == null) {
+            return Collections.emptyList();
+        }
+        return parseJsonList(project.getActionTasks());
+    }
+
+    /**
+     * 打卡某个任务
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> checkinTask(Long userId, Long projectId, int taskIndex) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null) {
+            throw new RuntimeException("项目不存在或无权操作");
+        }
+        if (!"completed".equals(project.getStatus())) {
+            throw new RuntimeException("请先完成问卷后再打卡");
+        }
+        if (project.getCfRefunded() != null && project.getCfRefunded() == 1) {
+            throw new RuntimeException("任务已全部完成,CF值已返还");
+        }
+
+        List<Map<String, Object>> tasks = parseJsonList(project.getActionTasks());
+        if (taskIndex < 0 || taskIndex >= tasks.size()) {
+            throw new RuntimeException("任务索引越界");
+        }
+
+        Map<String, Object> task = tasks.get(taskIndex);
+        if (Boolean.TRUE.equals(task.get("done"))) {
+            throw new RuntimeException("该任务已打卡");
+        }
+
+        task.put("done", true);
+        task.put("doneAt", new Date().getTime());
+        project.setActionTasks(safeJson(tasks));
+
+        int doneCount = (int) tasks.stream().filter(t -> Boolean.TRUE.equals(t.get("done"))).count();
+        project.setCompletedTasksCount(doneCount);
+        project.setUpdatedAt(new Date());
+        projectMapper.updateById(project);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("doneCount", doneCount);
+        result.put("totalTasks", tasks.size());
+        result.put("allDone", doneCount >= tasks.size());
+
+        // 全部完成 → 返还 CF 值
+        if (doneCount >= tasks.size()) {
+            result.put("cfRefunded", refundCf(project));
+        } else {
+            result.put("cfRefunded", false);
+        }
+        return result;
+    }
+
+    /**
+     * 获取打卡进度
+     */
+    public Map<String, Object> getCheckinStatus(Long userId, Long projectId) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null) {
+            return Collections.emptyMap();
+        }
+        List<Map<String, Object>> tasks = parseJsonList(project.getActionTasks());
+        int doneCount = (int) tasks.stream().filter(t -> Boolean.TRUE.equals(t.get("done"))).count();
+        int total = tasks.size();
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("projectId", project.getId());
+        result.put("tasks", tasks);
+        result.put("doneCount", doneCount);
+        result.put("totalTasks", total);
+        result.put("allDone", total > 0 && doneCount >= total);
+        result.put("cfRefunded", project.getCfRefunded() != null && project.getCfRefunded() == 1);
+        result.put("cfRefundAmount", project.getCfRefundAmount() != null ? project.getCfRefundAmount() : 0);
+        result.put("memberDiscountQualified", project.getMemberDiscountQualified() != null && project.getMemberDiscountQualified() == 1);
+        result.put("familyMembershipQualified", project.getFamilyMembershipQualified() != null && project.getFamilyMembershipQualified() == 1);
+        result.put("familyMembershipActivated", project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1);
+        return result;
+    }
+
+    /**
+     * 全部任务完成 → 返还同等金额 CF 值
+     */
+    private boolean refundCf(GutHealthProject project) {
+        if (project.getCfRefunded() != null && project.getCfRefunded() == 1) {
+            return true; // 幂等
+        }
+        int amount = project.getCfRefundAmount() != null ? project.getCfRefundAmount() : 0;
+        if (amount <= 0) {
+            amount = getProjectPrice();
+        }
+        try {
+            platformPointsService.earn(project.getUserId(), amount, "GUT_HEALTH", project.getId(),
+                    "菌群健康调研一周打卡完成,返还CF值");
+            project.setCfRefunded(1);
+            project.setUpdatedAt(new Date());
+            projectMapper.updateById(project);
+            log.info("菌群健康项目CF值返还: userId={}, projectId={}, amount={}", project.getUserId(), project.getId(), amount);
+            return true;
+        } catch (Exception e) {
+            log.error("CF值返还失败: userId={}, projectId={}", project.getUserId(), project.getId(), e);
+            return false;
+        }
+    }
+
+    // ==================== 奖励激活 ====================
+
+    /**
+     * 激活一年家庭会员(从资格中激活)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> activateFamilyMembership(Long userId, Long projectId) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null) {
+            throw new RuntimeException("项目不存在或无权操作");
+        }
+        if (project.getFamilyMembershipQualified() == null || project.getFamilyMembershipQualified() != 1) {
+            throw new RuntimeException("您尚未获得一年家庭会员资格,请先完成健康问卷");
+        }
+        if (project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1) {
+            throw new RuntimeException("一年家庭会员已激活,请勿重复操作");
+        }
+
+        // 通过会员体系开通一年家庭会员
+        Long familyId = project.getFamilyId() != null ? project.getFamilyId() : getUserFamilyId(userId);
+        if (familyId == null) {
+            throw new RuntimeException("请先创建或加入家庭");
+        }
+        Date expireTime = addYears(new Date(), 1);
+
+        FamilyMembership membership = new FamilyMembership();
+        membership.setFamilyId(familyId);
+        membership.setLevelCode("FAMILY");
+        membership.setStartDate(new Date());
+        membership.setEndDate(expireTime);
+        membership.setPaymentMethod("gut_health_reward");
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo("GH-" + project.getId());
+        membership.setAmount(0);
+        membership.setCreatedAt(new Date());
+        membership.setUpdatedAt(new Date());
+        familyMembershipMapper.insert(membership);
+
+        // 同步全家会员身份
+        try {
+            java.lang.reflect.Method syncMethod = MembershipService.class.getDeclaredMethod(
+                    "syncMembershipToFamilyMembers", Long.class, String.class, Date.class, String.class, Long.class);
+            syncMethod.setAccessible(true);
+            syncMethod.invoke(membershipService, familyId, "FAMILY", expireTime, "gut_health_reward", null);
+        } catch (Exception e) {
+            log.warn("同步家庭会员失败,将单独更新用户: {}", e.getMessage());
+            // 兜底:直接更新用户
+            User u = userMapper.selectById(userId);
+            if (u != null) {
+                u.setMemberLevel("FAMILY");
+                u.setMemberExpireTime(expireTime);
+                u.setUpdatedAt(new Date());
+                userMapper.updateById(u);
+            }
+        }
+
+        project.setFamilyMembershipActivated(1);
+        project.setFamilyMembershipQualified(1);
+        project.setUpdatedAt(new Date());
+        projectMapper.updateById(project);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("activated", true);
+        result.put("levelCode", "FAMILY");
+        result.put("expireTime", expireTime);
+        return result;
+    }
+
+    /**
+     * 购买菌群检测后激活一年家庭会员
+     * <p>
+     * 流程:用户使用会员价购买菌群检测商品 → 调用本方法 → 自动开通一年家庭会员
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> activateAfterPurchase(Long userId, Long projectId) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null) {
+            throw new RuntimeException("项目不存在或无权操作");
+        }
+        if (project.getMemberDiscountQualified() == null || project.getMemberDiscountQualified() != 1) {
+            throw new RuntimeException("您尚未获得会员价购买菌群检测资格,请先完成健康问卷");
+        }
+        if (project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1) {
+            throw new RuntimeException("一年家庭会员已激活,请勿重复操作");
+        }
+
+        // 通过会员体系开通一年家庭会员
+        Long familyId = project.getFamilyId() != null ? project.getFamilyId() : getUserFamilyId(userId);
+        if (familyId == null) {
+            throw new RuntimeException("请先创建或加入家庭");
+        }
+        Date expireTime = addYears(new Date(), 1);
+
+        FamilyMembership membership = new FamilyMembership();
+        membership.setFamilyId(familyId);
+        membership.setLevelCode("FAMILY");
+        membership.setStartDate(new Date());
+        membership.setEndDate(expireTime);
+        membership.setPaymentMethod("gut_health_reward");
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo("GH-" + project.getId());
+        membership.setAmount(0);
+        membership.setCreatedAt(new Date());
+        membership.setUpdatedAt(new Date());
+        familyMembershipMapper.insert(membership);
+
+        // 方案B:全家同步会员身份(通过会员体系公共入口)
+        try {
+            membershipService.setFamilyMembershipLevel(familyId, "FAMILY", expireTime, "gut_health_purchase", "菌群检测购买赠送");
+        } catch (Exception e) {
+            log.warn("同步家庭会员失败,将单独更新用户: {}", e.getMessage());
+            // 兜底:直接更新用户
+            User u = userMapper.selectById(userId);
+            if (u != null) {
+                u.setMemberLevel("FAMILY");
+                u.setMemberExpireTime(expireTime);
+                u.setUpdatedAt(new Date());
+                userMapper.updateById(u);
+            }
+        }
+
+        project.setFamilyMembershipActivated(1);
+        project.setUpdatedAt(new Date());
+        projectMapper.updateById(project);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("activated", true);
+        result.put("levelCode", "FAMILY");
+        result.put("expireTime", expireTime);
+        return result;
+    }
+
+    /**
+     * 获取会员价购买菌群检测凭证
+     */
+    public Map<String, Object> getMicrobeDiscountVoucher(Long userId, Long projectId) {
+        GutHealthProject project = getOwnedProject(userId, projectId);
+        if (project == null) {
+            throw new RuntimeException("项目不存在或无权操作");
+        }
+        if (project.getMemberDiscountQualified() == null || project.getMemberDiscountQualified() != 1) {
+            throw new RuntimeException("您尚未获得会员价购买资格,请先完成健康问卷");
+        }
+
+        Map<String, Object> voucher = new HashMap<>();
+        voucher.put("qualified", true);
+        voucher.put("voucherCode", "GH-DISCOUNT-" + project.getId());
+        voucher.put("validUntil", addYears(new Date(), 1));
+        voucher.put("projectId", project.getId());
+        return voucher;
+    }
+
+    // ==================== 风险评分与建议生成 ====================
+
+    /**
+     * 计算各维度风险评分(0-3分)
+     */
+    private Map<String, Integer> computeRiskScores(List<Map<String, Object>> answers) {
+        Map<String, Integer> scores = new LinkedHashMap<>();
+        List<Map<String, Object>> questions = getQuestionnaireQuestions();
+
+        for (int i = 0; i < questions.size(); i++) {
+            Map<String, Object> question = questions.get(i);
+            String dimension = (String) question.get("dimension");
+            int score = 0;
+            if (i < answers.size() && answers.get(i) != null) {
+                Object answer = answers.get(i).get("score") != null
+                        ? answers.get(i).get("score")
+                        : answers.get(i).get("answer");
+                if (answer instanceof Number) {
+                    score = ((Number) answer).intValue();
+                } else if (answer != null) {
+                    // 尝试按选项文本匹配
+                    List<Map<String, Object>> options = (List<Map<String, Object>>) question.get("options");
+                    for (Map<String, Object> opt : options) {
+                        if (answer.toString().equals(opt.get("text"))) {
+                            score = ((Number) opt.get("score")).intValue();
+                            break;
+                        }
+                    }
+                }
+            }
+            scores.put(dimension, score);
+        }
+        return scores;
+    }
+
+    /**
+     * 根据风险评分生成健康建议
+     */
+    private List<Map<String, Object>> generateRecommendations(Map<String, Integer> riskScores, List<Map<String, Object>> answers) {
+        List<Map<String, Object>> recommendations = new ArrayList<>();
+
+        // 睡眠
+        int sleep = riskScores.getOrDefault("sleep", 0);
+        if (sleep >= 2) {
+            recommendations.add(rec("睡眠", "高风险", "睡眠质量差与肠道菌群失衡密切相关,建议规律作息(23点前入睡),睡前1小时远离电子屏幕,可尝试睡前温水泡脚或冥想。若长期失眠,建议进一步评估肠道健康。"));
+        } else if (sleep == 1) {
+            recommendations.add(rec("睡眠", "中风险", "睡眠质量略有波动,建议保持固定作息,增加日间户外活动,晚餐避免过饱,观察改善情况。"));
+        }
+
+        // 饮食
+        int diet = riskScores.getOrDefault("diet", 0);
+        if (diet >= 2) {
+            recommendations.add(rec("饮食", "高风险", "饮食结构偏向高油高糖或缺乏蔬果,会抑制有益菌生长。建议每日蔬菜摄入500g以上,水果200g左右,减少油炸和加工食品,增加发酵食品(酸奶、纳豆、泡菜)。"));
+        } else if (diet == 1) {
+            recommendations.add(rec("饮食", "中风险", "饮食结构基本均衡但蔬果摄入偏少,建议每天增加一份蔬菜或水果,粗细粮搭配,促进膳食纤维多样化。"));
+        }
+
+        // 体重
+        int weight = riskScores.getOrDefault("weight", 0);
+        if (weight >= 2) {
+            recommendations.add(rec("体重", "高风险", "体重超标与肠道菌群多样性下降相关。建议控制精制碳水摄入,增加优质蛋白和膳食纤维,每周进行150分钟以上中等强度运动,可考虑菌群检测了解自身菌群结构。"));
+        } else if (weight == 1) {
+            recommendations.add(rec("体重", "中风险", "体重处于临界范围,建议调整饮食结构并保持规律运动,将体重控制在健康范围。"));
+        }
+
+        // 消化
+        int digestion = riskScores.getOrDefault("digestion", 0);
+        if (digestion >= 2) {
+            recommendations.add(rec("消化", "高风险", "频繁腹胀、腹泻或便秘提示肠道菌群可能失衡。建议每日补充膳食纤维,喝足量水(1500-2000ml),规律三餐,避免暴饮暴食。如症状持续,强烈建议进行菌群检测归因分析。"));
+        } else if (digestion == 1) {
+            recommendations.add(rec("消化", "中风险", "消化功能偶有波动,建议记录饮食日记,找出易引起不适的食物,逐步建立规律饮食习惯。"));
+        }
+
+        // 运动
+        int exercise = riskScores.getOrDefault("exercise", 0);
+        if (exercise >= 2) {
+            recommendations.add(rec("运动", "高风险", "缺乏规律运动会减少肠道菌群多样性。建议每周至少3次30分钟以上中等强度运动(快走、慢跑、游泳),可从每天15分钟开始逐步增加。"));
+        } else if (exercise == 1) {
+            recommendations.add(rec("运动", "中风险", "运动频率偏低,建议每周增加1-2次规律运动,运动后补充水分和适量蛋白质。"));
+        }
+
+        // 压力
+        int stress = riskScores.getOrDefault("stress", 0);
+        if (stress >= 2) {
+            recommendations.add(rec("压力", "高风险", "长期高压通过脑-肠轴影响肠道功能。建议每天留出10-15分钟正念呼吸或冥想,培养一项放松爱好,必要时寻求专业心理支持。"));
+        } else if (stress == 1) {
+            recommendations.add(rec("压力", "中风险", "压力水平可控,建议保持现有放松习惯,增加运动释放压力。"));
+        }
+
+        // 膳食纤维
+        int fiber = riskScores.getOrDefault("fiber", 0);
+        if (fiber >= 2) {
+            recommendations.add(rec("膳食纤维", "高风险", "膳食纤维摄入不足是肠道菌群失衡常见原因。建议主食增加燕麦、糙米、藜麦等全谷物,每天摄入豆类、坚果,保证纤维来源多样化。"));
+        } else if (fiber == 1) {
+            recommendations.add(rec("膳食纤维", "中风险", "全谷物比例偏低,建议逐步用粗粮替代部分精制主食,每周3-4次。"));
+        }
+
+        // 抗生素
+        int antibiotic = riskScores.getOrDefault("antibiotic", 0);
+        if (antibiotic >= 2) {
+            recommendations.add(rec("抗生素使用", "高风险", "近期抗生素使用可能已影响肠道菌群平衡。建议增加益生菌和益生元摄入(酸奶、发酵食品、菊粉),均衡饮食帮助菌群恢复。"));
+        } else if (antibiotic == 1) {
+            recommendations.add(rec("抗生素使用", "中风险", "曾有抗生素使用史,建议日常增加发酵食品摄入,帮助肠道菌群维持平衡。"));
+        }
+
+        if (recommendations.isEmpty()) {
+            recommendations.add(rec("整体", "低风险", "您的各项指标总体良好,建议保持现有健康生活方式,定期关注肠道健康。"));
+        }
+        return recommendations;
+    }
+
+    private Map<String, Object> rec(String dimension, String level, String advice) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("dimension", dimension);
+        m.put("level", level);
+        m.put("advice", advice);
+        return m;
+    }
+
+    /**
+     * 根据风险评分生成一周行动任务
+     */
+    private List<Map<String, Object>> generateActionTasks(Map<String, Integer> riskScores) {
+        List<Map<String, Object>> tasks = new ArrayList<>();
+
+        // 任务 1: 睡眠调整
+        if (riskScores.getOrDefault("sleep", 0) >= 1) {
+            tasks.add(task("每晚23:00前入睡,记录实际入睡时间", "sleep", "改善睡眠-肠道节律"));
+        } else {
+            tasks.add(task("保持现有作息,记录一周睡眠时长", "sleep", "巩固良好睡眠习惯"));
+        }
+
+        // 任务 2: 饮食调整
+        if (riskScores.getOrDefault("diet", 0) >= 1) {
+            tasks.add(task("每天增加一份蔬菜(至少500g),减少油炸食品", "diet", "优化饮食结构"));
+        } else {
+            tasks.add(task("保持均衡饮食,每天记录三餐构成", "diet", "维持健康饮食"));
+        }
+
+        // 任务 3: 运动
+        if (riskScores.getOrDefault("exercise", 0) >= 1) {
+            tasks.add(task("每天快走或慢跑20-30分钟", "exercise", "提升菌群多样性"));
+        } else {
+            tasks.add(task("每周3次30分钟运动(跑步/游泳/健身操)", "exercise", "维持菌群多样性"));
+        }
+
+        // 任务 4: 膳食纤维
+        if (riskScores.getOrDefault("fiber", 0) >= 1) {
+            tasks.add(task("每天摄入一种全谷物或粗粮(燕麦/糙米/藜麦)", "fiber", "补充膳食纤维"));
+        } else {
+            tasks.add(task("保持全谷物摄入,记录每日粗粮种类", "fiber", "维持纤维摄入"));
+        }
+
+        // 任务 5: 饮水
+        tasks.add(task("每天饮水1500-2000ml,记录饮水量", "hydration", "促进肠道蠕动"));
+
+        // 任务 6: 压力管理
+        if (riskScores.getOrDefault("stress", 0) >= 1) {
+            tasks.add(task("每天10分钟正念呼吸或冥想", "stress", "调节脑-肠轴"));
+        } else {
+            tasks.add(task("每天10分钟放松活动(听音乐/散步/阅读)", "stress", "保持轻松状态"));
+        }
+
+        // 任务 7: 消化观察
+        if (riskScores.getOrDefault("digestion", 0) >= 1) {
+            tasks.add(task("记录每日排便情况和腹胀感受", "digestion", "观察消化功能变化"));
+        } else {
+            tasks.add(task("记录一周消化状况,留意异常信号", "digestion", "持续关注消化健康"));
+        }
+
+        return tasks;
+    }
+
+    private Map<String, Object> task(String content, String category, String goal) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("content", content);
+        m.put("category", category);
+        m.put("goal", goal);
+        m.put("done", false);
+        return m;
+    }
+
+    // ==================== 辅助方法 ====================
+
+    private GutHealthProject getLatestProject(Long userId) {
+        return projectMapper.selectOne(
+                new LambdaQueryWrapper<GutHealthProject>()
+                        .eq(GutHealthProject::getUserId, userId)
+                        .orderByDesc(GutHealthProject::getId)
+                        .last("LIMIT 1"));
+    }
+
+    private GutHealthProject getOwnedProject(Long userId, Long projectId) {
+        if (projectId == null) return null;
+        GutHealthProject project = projectMapper.selectById(projectId);
+        if (project == null || !userId.equals(project.getUserId())) return null;
+        return project;
+    }
+
+    private int getProjectPrice() {
+        String priceStr = sysConfigService.getValue("gut_health_project_price");
+        if (priceStr != null) {
+            try {
+                return Integer.parseInt(priceStr);
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return DEFAULT_PRICE;
+    }
+
+    private Long getUserFamilyId(Long userId) {
+        User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
+    private Date addYears(Date date, int years) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.add(Calendar.YEAR, years);
+        return cal.getTime();
+    }
+
+    private Map<String, Object> toProjectVO(GutHealthProject project) {
+        Map<String, Object> vo = new LinkedHashMap<>();
+        vo.put("id", project.getId());
+        vo.put("userId", project.getUserId());
+        vo.put("status", project.getStatus());
+        vo.put("paidAt", project.getPaidAt());
+        vo.put("completedAt", project.getCompletedAt());
+        vo.put("microbeTestRecommended", project.getMicrobeTestRecommended());
+        vo.put("memberDiscountQualified", project.getMemberDiscountQualified());
+        vo.put("familyMembershipQualified", project.getFamilyMembershipQualified());
+        vo.put("familyMembershipActivated", project.getFamilyMembershipActivated());
+        vo.put("cfRefundAmount", project.getCfRefundAmount());
+        vo.put("cfRefunded", project.getCfRefunded());
+        vo.put("completedTasksCount", project.getCompletedTasksCount());
+        return vo;
+    }
+
+    private String safeJson(Object obj) {
+        try {
+            return new ObjectMapper().writeValueAsString(obj);
+        } catch (Exception e) {
+            return obj.toString();
+        }
+    }
+
+    private List<Map<String, Object>> parseJsonList(String json) {
+        if (json == null || json.isEmpty()) {
+            return new ArrayList<>();
+        }
+        try {
+            return new ObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            return new ArrayList<>();
+        }
+    }
+}

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

@@ -243,21 +243,6 @@ 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)
-                    || "referral_commission".equals(feature);
-        }
-
         // FAMILY用户可用功能
         if ("FAMILY".equals(level)) {
             return "free_activities".equals(feature)
@@ -835,16 +820,6 @@ 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;
@@ -1305,7 +1280,7 @@ public class MembershipService implements MembershipServiceInterface {
 
         // 个人会员身份(含有效期校验)
         String personal = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
-        if (("FAMILY".equals(personal) || "BASIC".equals(personal)) && user.getMemberExpireTime() != null) {
+        if ("FAMILY".equals(personal) && user.getMemberExpireTime() != null) {
             if (user.getMemberExpireTime().before(new Date())) {
                 personal = "FREE";
             }
@@ -1422,19 +1397,10 @@ public class MembershipService implements MembershipServiceInterface {
     }
 
     /**
-     * 查询当前有效的 BASIC 会员用户ID列表(到期提醒用)
+     * 查询当前有效的会员用户ID列表(已废弃:BASIC体验版会员级别已删除)
      */
     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();
+        return Collections.emptyList();
     }
 
     /**

+ 32 - 25
cfc-backend/src/main/resources/schema.sql

@@ -1189,7 +1189,7 @@ CREATE TABLE IF NOT EXISTS products (
     family_scope TINYINT DEFAULT 1 COMMENT '是否面向家庭: 1=家庭成员消费过即排除, 0=仅按个人消费排除',
     visible_roles JSON DEFAULT NULL COMMENT '可见角色: ["parent","child","teacher","admin"],NULL=全部可见',
     min_visible_level VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME,NULL=全部可见)',
-    min_discount_level VARCHAR(50) DEFAULT NULL COMMENT '享受会员价所需的最低会员级别编码(FREE/BASIC/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)',
+    min_discount_level VARCHAR(50) DEFAULT NULL COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
@@ -3931,7 +3931,7 @@ CREATE TABLE IF NOT EXISTS emotion_alert (
 -- ============================================================
 CREATE TABLE IF NOT EXISTS membership_levels (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/BASIC/PROFESSIONAL/ENTERPRISE',
+    level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/PROFESSIONAL/ENTERPRISE',
     level_name VARCHAR(100) NOT NULL COMMENT '等级名称',
     level_desc TEXT COMMENT '等级描述',
     price_monthly INT COMMENT '月费(分)',
@@ -3946,7 +3946,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 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6',
+    level_order INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     UNIQUE KEY uk_level_code (level_code)
@@ -3957,7 +3957,7 @@ CREATE TABLE IF NOT EXISTS membership_levels (
 -- ============================================================
 CREATE TABLE IF NOT EXISTS membership_benefits (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/BASIC/PROFESSIONAL/ENTERPRISE',
+    level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/PROFESSIONAL/ENTERPRISE',
     benefit_code VARCHAR(50) NOT NULL COMMENT '权益编码: AI_CHAT,FAMILY_MEMBERS等',
     benefit_name VARCHAR(100) NOT NULL COMMENT '权益名称',
     benefit_desc VARCHAR(500) COMMENT '权益描述',
@@ -5707,26 +5707,6 @@ CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (
 ) 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","referral_commission"]',
-    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);
 
 CREATE TABLE IF NOT EXISTS basic_quota_usage (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
@@ -5740,7 +5720,34 @@ CREATE TABLE IF NOT EXISTS basic_quota_usage (
     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会员月度配额使用记录';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月度配额使用记录';
+
+-- 菌群健康调研项目(单次付费问卷)
+CREATE TABLE IF NOT EXISTS gut_health_projects (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '购买用户ID',
+    family_id BIGINT COMMENT '关联家庭ID',
+    member_id BIGINT COMMENT '测评对象(家庭成员ID)',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/paused/completed',
+    questions JSON COMMENT '本次问卷题目+答案JSON',
+    health_recommendations JSON COMMENT 'AI生成的健康建议',
+    risk_scores JSON COMMENT '各维度风险评分(0-100)',
+    microbe_test_recommended TINYINT DEFAULT 0 COMMENT '是否推荐菌群检测',
+    paid_at DATETIME COMMENT '付费时间',
+    completed_at DATETIME COMMENT '完成时间',
+    -- 新增字段:问卷完成后的奖励资格
+    member_discount_qualified TINYINT DEFAULT 0 COMMENT '是否获得会员价购买菌群检测资格',
+    family_membership_qualified TINYINT DEFAULT 0 COMMENT '是否获得一年家庭会员资格',
+    family_membership_activated TINYINT DEFAULT 0 COMMENT '是否已激活家庭会员',
+    -- 新增字段:打卡任务系统
+    cf_refund_amount INT DEFAULT 0 COMMENT 'CF值返还金额(分),19.9元=1990分',
+    cf_refunded TINYINT DEFAULT 0 COMMENT 'CF值是否已返还',
+    completed_tasks_count INT DEFAULT 0 COMMENT '已完成任务数/总任务数',
+    action_tasks JSON COMMENT 'AI生成的一周行动任务列表',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user_created (user_id, created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菌群健康调研项目';
 
 -- DOA 个人目标-周计划-周复盘(迁移318,家庭打卡个人目标模块)
 -- ============================================================