面向 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.sqlcfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.javacfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.javacfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.javacfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.javacfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.javacfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.javacfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.javacfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.javacfc-langgraph/app/api/purchase.pycfc-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 — 新增购买相关APIcfc-frontend/pages.json — 注册新页面文件:
cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sqlcfc-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): 创建健康启航计划购买订单表和闯关进度表"
文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.javacfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.javacfc-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"
文件:
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): 实现购买服务 — 订单创建、支付确认、续费处理"
文件:
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奖励发放"
文件:
新建: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控制器"
文件:
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("请先购买健康启航计划体验包");
}
找到方案生成方法,在业务逻辑前添加相同拦截逻辑。
[ ] 步骤 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): 在报告和方案接口添加付费墙拦截"
文件:
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', {})
}
创建 cfc-frontend/pages/purchase/index.vue,包含:
已有购买的进度展示
[ ] 步骤 3:Commit
git add cfc-frontend/utils/api.js cfc-frontend/pages/purchase/index.vue
git commit -m "feat(purchase): 前端购买页和API封装"
文件:
cfc-frontend/pages/purchase/renewal.vue新建:cfc-frontend/pages/growth/challenge-progress.vue
[ ] 步骤 1:创建续费页
展示:
支付按钮(跳转外部支付)
[ ] 步骤 2:创建闯关进度页
展示:
连续打卡天数
[ ] 步骤 3:Commit
git add cfc-frontend/pages/purchase/renewal.vue \
cfc-frontend/pages/growth/challenge-progress.vue
git commit -m "feat(purchase): 续费页和闯关进度页"
文件:
新建:cfc-web/src/views/admin/PurchaseManagement.vue
[ ] 步骤 1:创建管理页面
功能:
用户查询
[ ] 步骤 2:Commit
git add cfc-web/src/views/admin/PurchaseManagement.vue
git commit -m "feat(purchase): Web管理端购买记录管理"
[ ] 步骤 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