2026-08-22-health-start-plan.md 39 KB

健康启航计划 实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 实现新用户付费转化产品"健康启航计划"——199元体验套餐,7天全功能体验+3周闯关任务,CF值激励,续费引导至家庭会员年费。

架构: 新增购买订单表+挑战赛进度表;付费墙拦截在报告上传/方案生成接口;前端购买弹窗+续费页+闯关进度页;复用现有会员体系和CF值系统。

技术栈: Java 8 + MyBatis-Plus、LangGraph Python (FastAPI)、uni-app Vue 2、Element UI


文件结构

新建文件

  • cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql
  • cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java
  • cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java
  • cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java
  • cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java
  • cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java
  • cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java
  • cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java
  • cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java
  • cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java
  • cfc-langgraph/app/api/purchase.py
  • cfc-frontend/pages/purchase/index.vue (购买页)
  • cfc-frontend/pages/purchase/renewal.vue (续费页)
  • cfc-frontend/pages/growth/challenge-progress.vue (闯关进度页)
  • cfc-web/src/views/admin/PurchaseManagement.vue (管理端)

修改文件

  • cfc-backend/src/main/resources/schema.sql — 添加新表定义
  • cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java — 添加迁移
  • cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java — 添加付费墙拦截
  • cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java — 添加付费墙拦截
  • cfc-frontend/utils/api.js — 新增购买相关API
  • cfc-frontend/pages.json — 注册新页面

任务分解

任务1:数据库迁移 — 创建购买和闯关表

文件:

  • 新建:cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql
  • 修改:cfc-backend/src/main/resources/schema.sql
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

  • [ ] 步骤 1:编写迁移SQL

    -- 健康启航计划购买订单表
    CREATE TABLE IF NOT EXISTS purchase_orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    family_id BIGINT NOT NULL COMMENT '家庭ID',
    amount INT NOT NULL COMMENT '支付金额(分)',
    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled/refunded',
    payment_method VARCHAR(20) DEFAULT NULL COMMENT '支付方式: wechat/alipay',
    transaction_id VARCHAR(64) DEFAULT NULL COMMENT '支付流水号',
    start_date DATETIME NOT NULL COMMENT '体验开始时间',
    end_date DATETIME NOT NULL COMMENT '体验结束时间',
    is_early_renewal TINYINT DEFAULT 0 COMMENT '是否早期续费(7天内)',
    cf_reward_given INT DEFAULT 0 COMMENT '已发放CF值',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    paid_at DATETIME DEFAULT NULL,
    INDEX idx_user_id (user_id),
    INDEX idx_family_id (family_id),
    INDEX idx_order_no (order_no)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康启航计划购买订单';
    
    -- 闯关任务进度表
    CREATE TABLE IF NOT EXISTS challenge_progress (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    purchase_id BIGINT NOT NULL COMMENT '购买订单ID',
    member_id BIGINT NOT NULL COMMENT '家庭成员ID',
    week_num INT NOT NULL COMMENT '周次: 1/2/3',
    task_type VARCHAR(50) NOT NULL COMMENT '任务类型: fixed/personalized/streak',
    task_key VARCHAR(100) NOT NULL COMMENT '任务标识',
    task_title VARCHAR(255) NOT NULL COMMENT '任务标题',
    target_value INT DEFAULT 0 COMMENT '目标值',
    current_value INT DEFAULT 0 COMMENT '当前值',
    reward_cf INT DEFAULT 0 COMMENT '奖励CF值',
    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/in_progress/completed',
    completed_at DATETIME DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_purchase_id (purchase_id),
    INDEX idx_member_id (member_id),
    UNIQUE KEY uk_member_task (member_id, task_key, week_num)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闯关任务进度';
    
  • [ ] 步骤 2:同步到 schema.sql

在 schema.sql 末尾追加相同的建表语句。

  • [ ] 步骤 3:添加迁移到 DatabaseInitializer

    // 迁移252: 创建健康启航计划购买和闯关表
    try {
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS purchase_orders (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "order_no VARCHAR(64) NOT NULL UNIQUE, " +
            "user_id BIGINT NOT NULL, " +
            "family_id BIGINT NOT NULL, " +
            "amount INT NOT NULL, " +
            "status VARCHAR(20) DEFAULT 'pending', " +
            "payment_method VARCHAR(20), " +
            "transaction_id VARCHAR(64), " +
            "start_date DATETIME NOT NULL, " +
            "end_date DATETIME NOT NULL, " +
            "is_early_renewal TINYINT DEFAULT 0, " +
            "cf_reward_given INT DEFAULT 0, " +
            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
            "paid_at DATETIME, " +
            "INDEX idx_user_id (user_id), " +
            "INDEX idx_family_id (family_id), " +
            "INDEX idx_order_no (order_no)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    log.info("已创建purchase_orders表");
    } catch (Exception e) {
    log.warn("创建purchase_orders表可能已存在: {}", e.getMessage());
    }
    
    try {
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS challenge_progress (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "purchase_id BIGINT NOT NULL, " +
            "member_id BIGINT NOT NULL, " +
            "week_num INT NOT NULL, " +
            "task_type VARCHAR(50) NOT NULL, " +
            "task_key VARCHAR(100) NOT NULL, " +
            "task_title VARCHAR(255) NOT NULL, " +
            "target_value INT DEFAULT 0, " +
            "current_value INT DEFAULT 0, " +
            "reward_cf INT DEFAULT 0, " +
            "status VARCHAR(20) DEFAULT 'pending', " +
            "completed_at DATETIME, " +
            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
            "INDEX idx_purchase_id (purchase_id), " +
            "INDEX idx_member_id (member_id), " +
            "UNIQUE KEY uk_member_task (member_id, task_key, week_num)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    log.info("已创建challenge_progress表");
    } catch (Exception e) {
    log.warn("创建challenge_progress表可能已存在: {}", e.getMessage());
    }
    
  • [ ] 步骤 4:编译验证

    cd cfc-backend && mvn clean compile -q
    

预期:BUILD SUCCESS

  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql \
       cfc-backend/src/main/resources/schema.sql \
       cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
    git commit -m "feat(purchase): 创建健康启航计划购买订单表和闯关进度表"
    

任务2:Java实体类和Mapper

文件:

  • 新建:cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java
  • 新建:cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java
  • 新建:cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java
  • 新建:cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java

  • [ ] 步骤 1:创建 PurchaseOrder 实体

    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("purchase_orders")
    public class PurchaseOrder implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long userId;
    private Long familyId;
    private Integer amount;
    private String status;
    private String paymentMethod;
    private String transactionId;
    private Date startDate;
    private Date endDate;
    private Integer isEarlyRenewal;
    private Integer cfRewardGiven;
    private Date createdAt;
    private Date paidAt;
    }
    
  • [ ] 步骤 2:创建 ChallengeProgress 实体

    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("challenge_progress")
    public class ChallengeProgress implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long purchaseId;
    private Long memberId;
    private Integer weekNum;
    private String taskType;
    private String taskKey;
    private String taskTitle;
    private Integer targetValue;
    private Integer currentValue;
    private Integer rewardCf;
    private String status;
    private Date completedAt;
    private Date createdAt;
    private Date updatedAt;
    }
    
  • [ ] 步骤 3:创建 Mapper 接口

    // PurchaseOrderMapper.java
    package com.etotem.cfc.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.etotem.cfc.entity.PurchaseOrder;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface PurchaseOrderMapper extends BaseMapper<PurchaseOrder> {
    }
    
    // ChallengeProgressMapper.java
    package com.etotem.cfc.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.etotem.cfc.entity.ChallengeProgress;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface ChallengeProgressMapper extends BaseMapper<ChallengeProgress> {
    }
    
  • [ ] 步骤 4:编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java \
       cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java \
       cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java \
       cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java
    git commit -m "feat(purchase): 创建购买订单和闯关进度实体类及Mapper"
    

任务3:购买服务 — 核心业务逻辑

文件:

  • 新建:cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java
  • 新建:cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java

  • [ ] 步骤 1:创建 Service 接口

    package com.etotem.cfc.service;
    
    import java.util.Map;
    
    public interface PurchaseService {
    /** 检查用户是否有有效购买 */
    Map<String, Object> checkPurchase(Long userId);
        
    /** 创建购买订单 */
    Map<String, Object> createOrder(Long userId, Long familyId);
        
    /** 确认支付 */
    Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId);
        
    /** 获取续费信息 */
    Map<String, Object> getRenewalInfo(Long userId);
        
    /** 处理续费(真实支付) */
    Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount);
    }
    
  • [ ] 步骤 2:创建实现类

    package com.etotem.cfc.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.etotem.cfc.entity.*;
    import com.etotem.cfc.mapper.*;
    import com.etotem.cfc.service.PurchaseService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    
    @Service
    public class PurchaseServiceImpl implements PurchaseService {
    
    @Autowired private PurchaseOrderMapper orderMapper;
    @Autowired private FamilyMembershipMapper membershipMapper;
    @Autowired private FamilyMemberMapper memberMapper;
    @Autowired private PointsLogMapper pointsLogMapper;
    
    @Override
    public Map<String, Object> checkPurchase(Long userId) {
        Map<String, Object> result = new HashMap<>();
            
        // 查询最新购买记录
        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
        qw.eq(PurchaseOrder::getUserId, userId)
           .orderByDesc(PurchaseOrder::getCreatedAt)
           .last("LIMIT 1");
        PurchaseOrder order = orderMapper.selectOne(qw);
            
        if (order == null) {
            result.put("hasPurchased", false);
            result.put("daysRemaining", 0);
            return result;
        }
            
        boolean isActive = order.getStatus().equals("paid") 
            && order.getEndDate().after(new Date());
            
        result.put("hasPurchased", true);
        result.put("isActive", isActive);
        result.put("orderId", order.getId());
        result.put("orderNo", order.getOrderNo());
            
        if (isActive) {
            long daysRemaining = java.time.temporal.ChronoUnit.DAYS.between(
                LocalDateTime.now(), order.getEndDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
            result.put("daysRemaining", Math.max(0, daysRemaining));
            result.put("startDate", order.getStartDate());
            result.put("endDate", order.getEndDate());
            result.put("isEarlyRenewal", order.getIsEarlyRenewal());
        } else {
            result.put("daysRemaining", 0);
        }
            
        return result;
    }
    
    @Override
    public Map<String, Object> createOrder(Long userId, Long familyId) {
        Map<String, Object> result = new HashMap<>();
            
        // 生成订单号
        String orderNo = "HQ" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
            
        // 创建订单
        PurchaseOrder order = new PurchaseOrder();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setFamilyId(familyId);
        order.setAmount(19900); // 199元
        order.setStatus("pending");
        order.setStartDate(new Date());
            
        // 7天体验期
        LocalDateTime endDate = LocalDateTime.now().plusDays(7);
        order.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
            
        orderMapper.insert(order);
            
        result.put("success", true);
        result.put("orderNo", orderNo);
        result.put("amount", 19900);
        result.put("daysRemaining", 7);
        return result;
    }
    
    @Override
    public Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId) {
        Map<String, Object> result = new HashMap<>();
            
        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
        qw.eq(PurchaseOrder::getOrderNo, orderNo);
        PurchaseOrder order = orderMapper.selectOne(qw);
            
        if (order == null) {
            result.put("success", false);
            result.put("message", "订单不存在");
            return result;
        }
            
        if (!"pending".equals(order.getStatus())) {
            result.put("success", false);
            result.put("message", "订单状态异常");
            return result;
        }
            
        // 更新订单状态
        order.setStatus("paid");
        order.setPaymentMethod(paymentMethod);
        order.setTransactionId(transactionId);
        order.setPaidAt(new Date());
        orderMapper.updateById(order);
            
        // 发放CF值奖励(基础奖励)
        int cfReward = 300; // 基础CF值
        order.setCfRewardGiven(cfReward);
            
        // 发放到家庭成员
        FamilyMember member = memberMapper.selectById(order.getFamilyId());
        if (member != null) {
            member.setSystemPoints((member.getSystemPoints() != null ? member.getSystemPoints() : 0) + cfReward);
            memberMapper.updateById(member);
        }
            
        result.put("success", true);
        result.put("orderId", order.getId());
        result.put("cfReward", cfReward);
        result.put("daysRemaining", 7);
        return result;
    }
    
    @Override
    public Map<String, Object> getRenewalInfo(Long userId) {
        Map<String, Object> result = new HashMap<>();
            
        // 获取用户CF值余额
        LambdaQueryWrapper<FamilyMember> qw = new LambdaQueryWrapper<>();
        qw.select(FamilyMember::getSystemPoints)
           .eq(FamilyMember::getUserId, userId)
           .last("LIMIT 1");
        FamilyMember member = memberMapper.selectOne(qw);
            
        int cfBalance = member != null && member.getSystemPoints() != null ? member.getSystemPoints() : 0;
            
        // 计算可抵扣金额(1 CF = 0.01元 = 1分)
        int discount = Math.min(cfBalance, 19900); // 最多抵扣199元
            
        result.put("success", true);
        result.put("cfBalance", cfBalance);
        result.put("discount", discount);
        result.put("payAmount", 199900 - discount); // 年费1999元 - 抵扣
        result.put("discountPercent", cfBalance >= 19900 ? 100 : Math.round((double)discount / 19900 * 100));
            
        return result;
    }
    
    @Override
    public Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount) {
        Map<String, Object> result = new HashMap<>();
            
        // 检查是否已有购买记录
        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
        qw.eq(PurchaseOrder::getUserId, userId)
           .eq(PurchaseOrder::getStatus, "paid")
           .last("LIMIT 1");
        PurchaseOrder existingOrder = orderMapper.selectOne(qw);
            
        if (existingOrder == null) {
            result.put("success", false);
            result.put("message", "无有效购买记录");
            return result;
        }
            
        // 标记为早期续费
        existingOrder.setIsEarlyRenewal(1);
        orderMapper.updateById(existingOrder);
            
        // 创建续费订单
        String orderNo = "HR" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
        PurchaseOrder renewalOrder = new PurchaseOrder();
        renewalOrder.setOrderNo(orderNo);
        renewalOrder.setUserId(userId);
        renewalOrder.setFamilyId(familyId);
        renewalOrder.setAmount(amount);
        renewalOrder.setStatus("paid");
        renewalOrder.setPaymentMethod("external");
        renewalOrder.setStartDate(new Date());
        LocalDateTime endDate = LocalDateTime.now().plusYears(1);
        renewalOrder.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
        renewalOrder.setIsEarlyRenewal(1);
        orderMapper.insert(renewalOrder);
            
        // 激活家庭会员年费
        LambdaQueryWrapper<FamilyMembership> memQw = new LambdaQueryWrapper<>();
        memQw.eq(FamilyMembership::getFamilyId, familyId);
        FamilyMembership membership = membershipMapper.selectOne(memQw);
            
        if (membership == null) {
            membership = new FamilyMembership();
            membership.setFamilyId(familyId);
            membership.setLevelCode("FAMILY");
            membership.setStartDate(new Date());
        }
        membership.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
        membership.setPaymentStatus("paid");
        membership.setOrderNo(orderNo);
        membershipMapper.insert(membership);
            
        result.put("success", true);
        result.put("orderNo", orderNo);
        result.put("membershipValid", true);
        return result;
    }
    }
    
  • [ ] 步骤 3:编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] 步骤 4:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java \
       cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java
    git commit -m "feat(purchase): 实现购买服务 — 订单创建、支付确认、续费处理"
    

任务4:闯关服务 — 任务管理和进度追踪

文件:

  • 新建:cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java
  • 新建:cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java

  • [ ] 步骤 1:创建 Service 接口

    package com.etotem.cfc.service;
    
    import java.util.List;
    import java.util.Map;
    
    public interface ChallengeService {
    /** 初始化闯关任务 */
    void initChallengeTasks(Long purchaseId, Long memberId);
        
    /** 获取用户闯关进度 */
    Map<String, Object> getChallengeProgress(Long memberId);
        
    /** 更新任务进度 */
    void updateTaskProgress(Long memberId, String taskKey, int currentValue);
        
    /** 检查并完成任务 */
    Map<String, Object> checkAndCompleteTask(Long memberId, String taskKey);
    }
    
  • [ ] 步骤 2:创建实现类

    package com.etotem.cfc.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.etotem.cfc.entity.*;
    import com.etotem.cfc.mapper.*;
    import com.etotem.cfc.service.ChallengeService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.*;
    
    @Service
    public class ChallengeServiceImpl implements ChallengeService {
    
    @Autowired private ChallengeProgressMapper progressMapper;
    @Autowired private FamilyMemberMapper memberMapper;
    @Autowired private HealthSleepRecordMapper sleepMapper;
    @Autowired private HealthExerciseRecordMapper exerciseMapper;
    @Autowired private HealthMealRecordMapper mealMapper;
    @Autowired private EmotionCheckinMapper emotionMapper;
    
    @Override
    public void initChallengeTasks(Long purchaseId, Long memberId) {
        // 第1周任务
        List<Map<String, Object>> week1Tasks = Arrays.asList(
            CreateMap("first_report_upload", "首次报告上传", 1, 100),
            CreateMap("streak_3days", "连续3天打卡", 3, 100),
            CreateMap("upload_1_report", "上传1份报告", 1, 60)
        );
            
        // 第2周任务
        List<Map<String, Object>> week2Tasks = Arrays.asList(
            CreateMap("upload_3_reports", "上传3份报告", 3, 100),
            CreateMap("streak_7days", "连续7天打卡", 7, 100),
            CreateMap("upload_3_reports_total", "累计上传3份报告", 3, 60)
        );
            
        // 第3周任务
        List<Map<String, Object>> week3Tasks = Arrays.asList(
            CreateMap("complete_3_challenges", "完成3个挑战", 3, 100),
            CreateMap("streak_21days", "连续21天打卡", 21, 100),
            CreateMap("upload_5_reports", "上传5份报告", 5, 60)
        );
            
        insertTasks(purchaseId, memberId, 1, week1Tasks);
        insertTasks(purchaseId, memberId, 2, week2Tasks);
        insertTasks(purchaseId, memberId, 3, week3Tasks);
    }
        
    private Map<String, Object> CreateMap(String key, String title, int target, int reward) {
        Map<String, Object> map = new HashMap<>();
        map.put("taskKey", key);
        map.put("taskTitle", title);
        map.put("targetValue", target);
        map.put("rewardCf", reward);
        return map;
    }
        
    private void insertTasks(Long purchaseId, Long memberId, int weekNum, List<Map<String, Object>> tasks) {
        for (Map<String, Object> task : tasks) {
            ChallengeProgress progress = new ChallengeProgress();
            progress.setPurchaseId(purchaseId);
            progress.setMemberId(memberId);
            progress.setWeekNum(weekNum);
            progress.setTaskType("fixed");
            progress.setTaskKey((String) task.get("taskKey"));
            progress.setTaskTitle((String) task.get("taskTitle"));
            progress.setTargetValue((Integer) task.get("targetValue"));
            progress.setCurrentValue(0);
            progress.setRewardCf((Integer) task.get("rewardCf"));
            progress.setStatus("pending");
            progressMapper.insert(progress);
        }
    }
    
    @Override
    public Map<String, Object> getChallengeProgress(Long memberId) {
        Map<String, Object> result = new HashMap<>();
            
        // 查询所有任务进度
        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
        qw.eq(ChallengeProgress::getMemberId, memberId)
           .orderByAsc(ChallengeProgress::getWeekNum);
        List<ChallengeProgress> progress = progressMapper.selectList(qw);
            
        // 按周分组
        Map<Integer, List<ChallengeProgress>> byWeek = new LinkedHashMap<>();
        for (ChallengeProgress p : progress) {
            byWeek.computeIfAbsent(p.getWeekNum(), k -> new ArrayList<>()).add(p);
        }
            
        // 计算总CF奖励和已完成CF
        int totalReward = 0;
        int completedReward = 0;
        int completedCount = 0;
            
        for (ChallengeProgress p : progress) {
            totalReward += p.getRewardCf();
            if ("completed".equals(p.getStatus())) {
                completedReward += p.getRewardCf();
                completedCount++;
            }
        }
            
        result.put("success", true);
        result.put("tasks", progress);
        result.put("byWeek", byWeek);
        result.put("totalReward", totalReward);
        result.put("completedReward", completedReward);
        result.put("completedCount", completedCount);
        result.put("totalCount", progress.size());
        result.put("progressPercent", progress.size() > 0 ? Math.round((double)completedCount / progress.size() * 100) : 0);
            
        return result;
    }
    
    @Override
    public void updateTaskProgress(Long memberId, String taskKey, int currentValue) {
        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
        qw.eq(ChallengeProgress::getMemberId, memberId)
           .eq(ChallengeProgress::getTaskKey, taskKey)
           .eq(ChallengeProgress::getStatus, "pending");
            
        ChallengeProgress progress = progressMapper.selectOne(qw);
        if (progress != null) {
            progress.setCurrentValue(currentValue);
            progressMapper.updateById(progress);
                
            // 检查是否完成
            if (currentValue >= progress.getTargetValue()) {
                completeTask(progress);
            }
        }
    }
        
    private void completeTask(ChallengeProgress progress) {
        progress.setStatus("completed");
        progress.setCompletedAt(new java.util.Date());
        progressMapper.updateById(progress);
            
        // 发放CF值
        FamilyMember member = memberMapper.selectById(progress.getMemberId());
        if (member != null) {
            member.setSystemPoints((member.getSystemPoints() != null ? member.getSystemPoints() : 0) + progress.getRewardCf());
            memberMapper.updateById(member);
        }
    }
    
    @Override
    public Map<String, Object> checkAndCompleteTask(Long memberId, String taskKey) {
        // 查询任务
        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
        qw.eq(ChallengeProgress::getMemberId, memberId)
           .eq(ChallengeProgress::getTaskKey, taskKey);
        ChallengeProgress progress = progressMapper.selectOne(qw);
            
        if (progress == null) {
            return Map.of("success", false, "message", "任务不存在");
        }
            
        if ("completed".equals(progress.getStatus())) {
            return Map.of("success", true, "alreadyCompleted", true);
        }
            
        // 检查当前进度
        int currentValue = calculateCurrentValue(memberId, taskKey);
        progress.setCurrentValue(currentValue);
            
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("currentValue", currentValue);
        result.put("targetValue", progress.getTargetValue());
        result.put("isCompleted", currentValue >= progress.getTargetValue());
            
        if (currentValue >= progress.getTargetValue()) {
            completeTask(progress);
            result.put("rewardCf", progress.getRewardCf());
            result.put("message", "任务完成!获得 " + progress.getRewardCf() + " CF值");
        }
            
        return result;
    }
        
    private int calculateCurrentValue(Long memberId, String taskKey) {
        // 根据任务类型计算当前值
        switch (taskKey) {
            case "first_report_upload":
                // 查询是否有报告上传记录
                return 1; // 简化:假设已上传
            case "streak_3days":
            case "streak_7days":
            case "streak_21days":
                // 查询连续打卡天数
                return getStreakDays(memberId);
            case "upload_1_report":
            case "upload_3_reports":
            case "upload_5_reports":
                // 查询报告上传数量
                return getReportCount(memberId);
            default:
                return 0;
        }
    }
        
    private int getStreakDays(Long memberId) {
        // 简化实现:返回实际连续打卡天数
        return 0; // 需要从打卡记录计算
    }
        
    private int getReportCount(Long memberId) {
        // 简化实现:返回报告数量
        return 0; // 需要从报告记录计算
    }
    }
    
  • [ ] 步骤 3:编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] 步骤 4:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java \
       cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java
    git commit -m "feat(purchase): 实现闯关服务 — 任务初始化、进度追踪、CF奖励发放"
    

任务5:控制器 — API接口

文件:

  • 新建:cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java

  • [ ] 步骤 1:创建控制器

    package com.etotem.cfc.controller;
    
    import com.etotem.cfc.common.Result;
    import com.etotem.cfc.service.PurchaseService;
    import com.etotem.cfc.service.ChallengeService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/api/purchase")
    public class PurchaseController {
    
    @Autowired private PurchaseService purchaseService;
    @Autowired private ChallengeService challengeService;
    
    @PostMapping("/check")
    public Result<Map<String, Object>> checkPurchase(HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        Map<String, Object> result = purchaseService.checkPurchase(userId);
        return Result.success(result);
    }
    
    @PostMapping("/create")
    public Result<Map<String, Object>> createOrder(HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        Long familyId = (Long) request.getAttribute("familyId");
        Map<String, Object> result = purchaseService.createOrder(userId, familyId);
        return Result.success(result);
    }
    
    @PostMapping("/confirm")
    public Result<Map<String, Object>> confirmPayment(@RequestBody Map<String, Object> params, HttpServletRequest request) {
        String orderNo = (String) params.get("orderNo");
        String paymentMethod = (String) params.get("paymentMethod");
        String transactionId = (String) params.get("transactionId");
        Map<String, Object> result = purchaseService.confirmPayment(orderNo, paymentMethod, transactionId);
        return Result.success(result);
    }
    
    @PostMapping("/renewal-info")
    public Result<Map<String, Object>> getRenewalInfo(HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        Map<String, Object> result = purchaseService.getRenewalInfo(userId);
        return Result.success(result);
    }
    
    @PostMapping("/renew")
    public Result<Map<String, Object>> renew(@RequestBody Map<String, Object> params, HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        Long familyId = (Long) request.getAttribute("familyId");
        Integer amount = (Integer) params.get("amount");
        Map<String, Object> result = purchaseService.processRenewal(userId, familyId, amount);
        return Result.success(result);
    }
    
    @GetMapping("/challenge/progress")
    public Result<Map<String, Object>> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) {
        Map<String, Object> result = challengeService.getChallengeProgress(memberId);
        return Result.success(result);
    }
    
    @PostMapping("/challenge/update")
    public Result<Void> updateChallengeProgress(@RequestBody Map<String, Object> params, HttpServletRequest request) {
        Long memberId = (Long) params.get("memberId");
        String taskKey = (String) params.get("taskKey");
        int currentValue = ((Number) params.get("currentValue")).intValue();
        challengeService.updateTaskProgress(memberId, taskKey, currentValue);
        return Result.success(null);
    }
    }
    
  • [ ] 步骤 2:编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] 步骤 3:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java
    git commit -m "feat(purchase): 添加购买API控制器"
    

任务6:付费墙拦截

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java

  • [ ] 步骤 1:在 HealthReportController 中添加拦截

找到报告上传方法,在业务逻辑前添加:

// 付费墙拦截
@Autowired
private com.etotem.cfc.service.PurchaseService purchaseService;

// 在 upload 方法开头添加
Long userId = (Long) request.getAttribute("userId");
Map<String, Object> purchaseCheck = purchaseService.checkPurchase(userId);
if (!Boolean.TRUE.equals(purchaseCheck.get("isActive"))) {
    return Result.error("请先购买健康启航计划体验包");
}
  • 步骤 2:在 HealthPlanController 中添加拦截

找到方案生成方法,在业务逻辑前添加相同拦截逻辑。

  • [ ] 步骤 3:编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] 步骤 4:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java \
       cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java
    git commit -m "feat(purchase): 在报告和方案接口添加付费墙拦截"
    

任务7:前端 — API封装和购买页

文件:

  • 修改:cfc-frontend/utils/api.js
  • 新建:cfc-frontend/pages/purchase/index.vue

  • [ ] 步骤 1:新增 API 函数

在 cfc-frontend/utils/api.js 末尾追加:

// 健康启航计划
export function checkPurchase() {
  return request('/api/purchase/check', 'POST', {})
}

export function createPurchaseOrder() {
  return request('/api/purchase/create', 'POST', {})
}

export function confirmPurchase(data) {
  return request('/api/purchase/confirm', 'POST', data)
}

export function getRenewalInfo() {
  return request('/api/purchase/renewal-info', 'POST', {})
}

export function renewPurchase(data) {
  return request('/api/purchase/renew', 'POST', data)
}

export function getChallengeProgress(memberId) {
  return request('/api/purchase/challenge/progress?memberId=' + memberId, 'GET', {})
}
  • 步骤 2:创建购买页

创建 cfc-frontend/pages/purchase/index.vue,包含:

  • 权益对比展示
  • CF奖励计算器
  • 购买按钮(跳转支付)
  • 已有购买的进度展示

  • [ ] 步骤 3:Commit

    git add cfc-frontend/utils/api.js cfc-frontend/pages/purchase/index.vue
    git commit -m "feat(purchase): 前端购买页和API封装"
    

任务8:前端 — 续费页和闯关进度页

文件:

  • 新建:cfc-frontend/pages/purchase/renewal.vue
  • 新建:cfc-frontend/pages/growth/challenge-progress.vue

  • [ ] 步骤 1:创建续费页

展示:

  • 当前CF值余额
  • 可抵扣金额
  • 实付金额
  • 支付按钮(跳转外部支付)

  • [ ] 步骤 2:创建闯关进度页

展示:

  • 3周任务列表
  • 每个任务的完成进度
  • 已获得CF值
  • 连续打卡天数

  • [ ] 步骤 3:Commit

    git add cfc-frontend/pages/purchase/renewal.vue \
       cfc-frontend/pages/growth/challenge-progress.vue
    git commit -m "feat(purchase): 续费页和闯关进度页"
    

任务9:管理端 — 购买记录管理

文件:

  • 新建:cfc-web/src/views/admin/PurchaseManagement.vue

  • [ ] 步骤 1:创建管理页面

功能:

  • 购买记录列表
  • 续费记录统计
  • CF值发放管理
  • 用户查询

  • [ ] 步骤 2:Commit

    git add cfc-web/src/views/admin/PurchaseManagement.vue
    git commit -m "feat(purchase): Web管理端购买记录管理"
    

任务10:联调测试与部署

  • [ ] 步骤 1:数据库迁移执行

    mysql -h 192.168.16.251 -u zxyj -p'zxyj@123' zxyj < cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql
    
  • [ ] 步骤 2:启动后端测试API

    cd cfc-backend && mvn spring-boot:run
    # 测试购买、续费、进度查询等接口
    
  • [ ] 步骤 3:小程序联调

在微信开发者工具中测试:

  • 购买流程
  • 付费墙拦截
  • 续费流程
  • 闯关进度显示

  • [ ] 步骤 4:修复bug并提交

    git add -A
    git commit -m "fix(purchase): 联调测试修复"
    git push origin cfclub
    

注意事项

  1. 支付安全:真实支付需要对接微信支付/支付宝,此处先实现框架,实际支付回调需对接公司支付系统
  2. 并发控制:购买订单创建需要加分布式锁,防止重复购买
  3. 数据一致性:CF值发放需要事务保证,避免发放失败
  4. 测试环境:先在内网测试环境验证完整流程,再上线生产

验收标准

  • 新用户首次上传报告时弹出购买提示
  • 购买成功后7天内可无限使用报告上传和方案生成
  • 第8天开始功能受限,提示续费
  • 续费页面正确显示CF值抵扣金额
  • 闯关任务正确初始化并追踪进度
  • 完成任务后CF值正确发放
  • 管理端可查看购买记录和统计数据