Parcourir la source

feat(purchase): 健康启航计划后端核心

Sisyphus il y a 4 semaines
Parent
commit
ef56ebc76e

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java

@@ -0,0 +1,73 @@
+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 = params.get("amount") != null ? ((Number) params.get("amount")).intValue() : null;
+        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);
+    }
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java

@@ -0,0 +1,29 @@
+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;
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java

@@ -0,0 +1,29 @@
+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;
+}

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

@@ -0,0 +1,9 @@
+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> {
+}

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

@@ -0,0 +1,9 @@
+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> {
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.service;
+
+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);
+}

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java

@@ -0,0 +1,20 @@
+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);
+}

+ 137 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java

@@ -0,0 +1,137 @@
+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;
+
+    @Override
+    public void initChallengeTasks(Long purchaseId, Long memberId) {
+        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)
+        );
+        
+        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)
+        );
+        
+        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);
+        }
+        
+        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);
+        
+        FamilyMember member = memberMapper.selectById(progress.getMemberId());
+        if (member != null) {
+            int currentPoints = member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+            member.setSystemPoints(currentPoints + progress.getRewardCf());
+            memberMapper.updateById(member);
+        }
+    }
+}

+ 206 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java

@@ -0,0 +1,206 @@
+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.util.*;
+
+@Service
+public class PurchaseServiceImpl implements PurchaseService {
+
+    @Autowired private PurchaseOrderMapper orderMapper;
+    @Autowired private FamilyMembershipMapper membershipMapper;
+    @Autowired private FamilyMemberMapper memberMapper;
+
+    @Override
+    public Map<String, Object> checkPurchase(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, userId)
+           .eq(PurchaseOrder::getStatus, "paid")
+           .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.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(
+                java.time.LocalDate.now(), order.getEndDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate());
+            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);
+        order.setStatus("pending");
+        order.setStartDate(new Date());
+        
+        java.time.LocalDateTime endDate = java.time.LocalDateTime.now().plusDays(7);
+        order.setEndDate(java.sql.Timestamp.valueOf(endDate));
+        
+        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);
+        
+        int cfReward = 300;
+        order.setCfRewardGiven(cfReward);
+        
+        LambdaQueryWrapper<FamilyMember> mqw = new LambdaQueryWrapper<>();
+        mqw.eq(FamilyMember::getFamilyId, order.getFamilyId())
+           .eq(FamilyMember::getUserId, order.getUserId())
+           .last("LIMIT 1");
+        FamilyMember member = memberMapper.selectOne(mqw);
+        
+        if (member != null) {
+            int currentPoints = member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+            member.setSystemPoints(currentPoints + 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<>();
+        
+        LambdaQueryWrapper<FamilyMember> qw = new LambdaQueryWrapper<>();
+        qw.select(FamilyMember::getSystemPoints, FamilyMember::getFamilyId)
+           .eq(FamilyMember::getUserId, userId)
+           .last("LIMIT 1");
+        FamilyMember member = memberMapper.selectOne(qw);
+        
+        int cfBalance = member != null && member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+        int discount = Math.min(cfBalance, 19900);
+        
+        result.put("success", true);
+        result.put("cfBalance", cfBalance);
+        result.put("discount", discount);
+        result.put("payAmount", 199900 - discount);
+        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());
+        java.time.LocalDateTime endDate = java.time.LocalDateTime.now().plusYears(1);
+        renewalOrder.setEndDate(java.sql.Timestamp.valueOf(endDate));
+        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(java.sql.Timestamp.valueOf(endDate));
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo(orderNo);
+        membershipMapper.insert(membership);
+        
+        result.put("success", true);
+        result.put("orderNo", orderNo);
+        result.put("membershipValid", true);
+        return result;
+    }
+}

+ 41 - 0
cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql

@@ -0,0 +1,41 @@
+-- 健康启航计划购买订单表
+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='闯关任务进度';

+ 39 - 7
cfc-backend/src/main/resources/schema.sql

@@ -5116,12 +5116,44 @@ CREATE TABLE IF NOT EXISTS profile_history (
     INDEX idx_date (snapshot_date)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户画像历史快照';
 
--- 每日能量上限配置(迁移255)
-CREATE TABLE IF NOT EXISTS energy_daily_limit (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    dimension_code VARCHAR(20) NOT NULL COMMENT '维度代码: body/mind/wisdom/action/wealth',
-    daily_limit INT NOT NULL DEFAULT 50 COMMENT '每日能量上限',
+-- 健康启航计划购买订单表
+CREATE TABLE IF NOT EXISTS purchase_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    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;
+
+-- 闯关任务进度表
+CREATE TABLE IF NOT EXISTS challenge_progress (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    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,
-    UNIQUE KEY uk_dimension_code (dimension_code)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日能量上限配置';
+    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;