# 健康启航计划 实现计划 > **面向 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** ```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** ```java // 迁移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:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` 预期:BUILD SUCCESS - [ ] **步骤 5:Commit** ```bash 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 实体** ```java 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 实体** ```java 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 接口** ```java // 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 { } // 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 { } ``` - [ ] **步骤 4:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **步骤 5:Commit** ```bash 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 接口** ```java package com.etotem.cfc.service; import java.util.Map; public interface PurchaseService { /** 检查用户是否有有效购买 */ Map checkPurchase(Long userId); /** 创建购买订单 */ Map createOrder(Long userId, Long familyId); /** 确认支付 */ Map confirmPayment(String orderNo, String paymentMethod, String transactionId); /** 获取续费信息 */ Map getRenewalInfo(Long userId); /** 处理续费(真实支付) */ Map processRenewal(Long userId, Long familyId, Integer amount); } ``` - [ ] **步骤 2:创建实现类** ```java 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 checkPurchase(Long userId) { Map result = new HashMap<>(); // 查询最新购买记录 LambdaQueryWrapper 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 createOrder(Long userId, Long familyId) { Map 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 confirmPayment(String orderNo, String paymentMethod, String transactionId) { Map result = new HashMap<>(); LambdaQueryWrapper 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 getRenewalInfo(Long userId) { Map result = new HashMap<>(); // 获取用户CF值余额 LambdaQueryWrapper 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 processRenewal(Long userId, Long familyId, Integer amount) { Map result = new HashMap<>(); // 检查是否已有购买记录 LambdaQueryWrapper 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 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:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **步骤 4:Commit** ```bash 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 接口** ```java package com.etotem.cfc.service; import java.util.List; import java.util.Map; public interface ChallengeService { /** 初始化闯关任务 */ void initChallengeTasks(Long purchaseId, Long memberId); /** 获取用户闯关进度 */ Map getChallengeProgress(Long memberId); /** 更新任务进度 */ void updateTaskProgress(Long memberId, String taskKey, int currentValue); /** 检查并完成任务 */ Map checkAndCompleteTask(Long memberId, String taskKey); } ``` - [ ] **步骤 2:创建实现类** ```java 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> week1Tasks = Arrays.asList( CreateMap("first_report_upload", "首次报告上传", 1, 100), CreateMap("streak_3days", "连续3天打卡", 3, 100), CreateMap("upload_1_report", "上传1份报告", 1, 60) ); // 第2周任务 List> 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> 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 CreateMap(String key, String title, int target, int reward) { Map 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> tasks) { for (Map 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 getChallengeProgress(Long memberId) { Map result = new HashMap<>(); // 查询所有任务进度 LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(ChallengeProgress::getMemberId, memberId) .orderByAsc(ChallengeProgress::getWeekNum); List progress = progressMapper.selectList(qw); // 按周分组 Map> 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 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 checkAndCompleteTask(Long memberId, String taskKey) { // 查询任务 LambdaQueryWrapper 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 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:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **步骤 4:Commit** ```bash 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:创建控制器** ```java 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> checkPurchase(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); Map result = purchaseService.checkPurchase(userId); return Result.success(result); } @PostMapping("/create") public Result> createOrder(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); Long familyId = (Long) request.getAttribute("familyId"); Map result = purchaseService.createOrder(userId, familyId); return Result.success(result); } @PostMapping("/confirm") public Result> confirmPayment(@RequestBody Map params, HttpServletRequest request) { String orderNo = (String) params.get("orderNo"); String paymentMethod = (String) params.get("paymentMethod"); String transactionId = (String) params.get("transactionId"); Map result = purchaseService.confirmPayment(orderNo, paymentMethod, transactionId); return Result.success(result); } @PostMapping("/renewal-info") public Result> getRenewalInfo(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); Map result = purchaseService.getRenewalInfo(userId); return Result.success(result); } @PostMapping("/renew") public Result> renew(@RequestBody Map params, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); Long familyId = (Long) request.getAttribute("familyId"); Integer amount = (Integer) params.get("amount"); Map result = purchaseService.processRenewal(userId, familyId, amount); return Result.success(result); } @GetMapping("/challenge/progress") public Result> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) { Map result = challengeService.getChallengeProgress(memberId); return Result.success(result); } @PostMapping("/challenge/update") public Result updateChallengeProgress(@RequestBody Map 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:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **步骤 3:Commit** ```bash 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 中添加拦截** 找到报告上传方法,在业务逻辑前添加: ```java // 付费墙拦截 @Autowired private com.etotem.cfc.service.PurchaseService purchaseService; // 在 upload 方法开头添加 Long userId = (Long) request.getAttribute("userId"); Map purchaseCheck = purchaseService.checkPurchase(userId); if (!Boolean.TRUE.equals(purchaseCheck.get("isActive"))) { return Result.error("请先购买健康启航计划体验包"); } ``` - [ ] **步骤 2:在 HealthPlanController 中添加拦截** 找到方案生成方法,在业务逻辑前添加相同拦截逻辑。 - [ ] **步骤 3:编译验证** ```bash cd cfc-backend && mvn clean compile -q ``` - [ ] **步骤 4:Commit** ```bash 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` 末尾追加: ```javascript // 健康启航计划 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** ```bash 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** ```bash 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** ```bash git add cfc-web/src/views/admin/PurchaseManagement.vue git commit -m "feat(purchase): Web管理端购买记录管理" ``` --- ### 任务10:联调测试与部署 - [ ] **步骤 1:数据库迁移执行** ```bash 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** ```bash cd cfc-backend && mvn spring-boot:run # 测试购买、续费、进度查询等接口 ``` - [ ] **步骤 3:小程序联调** 在微信开发者工具中测试: - 购买流程 - 付费墙拦截 - 续费流程 - 闯关进度显示 - [ ] **步骤 4:修复bug并提交** ```bash git add -A git commit -m "fix(purchase): 联调测试修复" git push origin cfclub ``` --- ## 注意事项 1. **支付安全**:真实支付需要对接微信支付/支付宝,此处先实现框架,实际支付回调需对接公司支付系统 2. **并发控制**:购买订单创建需要加分布式锁,防止重复购买 3. **数据一致性**:CF值发放需要事务保证,避免发放失败 4. **测试环境**:先在内网测试环境验证完整流程,再上线生产 ## 验收标准 - [ ] 新用户首次上传报告时弹出购买提示 - [ ] 购买成功后7天内可无限使用报告上传和方案生成 - [ ] 第8天开始功能受限,提示续费 - [ ] 续费页面正确显示CF值抵扣金额 - [ ] 闯关任务正确初始化并追踪进度 - [ ] 完成任务后CF值正确发放 - [ ] 管理端可查看购买记录和统计数据