Kaynağa Gözat

feat: 会员体系 + 推广裂变 + 优惠券 + 成长任务模块

Backend:
- 会员体系: MembershipService, PackagePaymentService, 会员升级/续费接口
- 推广裂变: ReferralLeaderboard(排行榜), InviteMilestone(里程碑), Coupon(优惠券)
- 成长任务: GrowthTask + GrowthTaskLog 实体/Service/Controller
- 分佣扩展: CommissionService 新增分佣逻辑
- 定时任务: LeaderboardWeeklyResetTask 每周排行榜重置
- 数据库: schema.sql 新增相关表结构

Frontend(小程序):
- 会员/推广: upgrade.vue, promotion/index.vue, invite.vue, leaderboard.vue
- 优惠券/成长任务: coupons.vue, daily-tasks.vue, onboarding.vue
- 导航/API: ProfileMenu 新增入口, api.js 新增接口

Admin(管理端):
- 优惠券管理 CouponManagement.vue
- 成长任务管理 GrowthTaskManagement.vue
- 推广管理 PromotionManagement.vue
- Layout/路由: 新增管理菜单项

测试:
- e2e: test-promotion-flow.sh 推广流程测试脚本
Xiaogang Liao 2 ay önce
ebeveyn
işleme
0f9b073240
58 değiştirilmiş dosya ile 4910 ekleme ve 15 silme
  1. 40 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 47 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/CouponController.java
  3. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java
  4. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/InviteMilestoneController.java
  5. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  6. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java
  7. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReferralLeaderboardController.java
  8. 84 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java
  9. 75 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminLeaderboardController.java
  10. 88 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminMilestoneController.java
  11. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
  12. 40 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java
  13. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTask.java
  14. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTaskLog.java
  15. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/InviteMilestone.java
  16. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PackageOrder.java
  17. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PaymentOrder.java
  18. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ReferralLeaderboard.java
  19. 30 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java
  20. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/CouponMapper.java
  21. 19 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/GrowthTaskLogMapper.java
  22. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/GrowthTaskMapper.java
  23. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/InviteMilestoneMapper.java
  24. 19 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ReferralLeaderboardMapper.java
  25. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/UserCouponMapper.java
  26. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
  27. 141 0
      cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java
  28. 255 0
      cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java
  29. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthCheckinService.java
  30. 148 0
      cfc-backend/src/main/java/com/etotem/cfc/service/InviteMilestoneService.java
  31. 21 2
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  32. 21 2
      cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java
  33. 133 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ReferralLeaderboardService.java
  34. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java
  35. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/api/MembershipServiceInterface.java
  36. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/api/PackagePaymentServiceInterface.java
  37. 45 0
      cfc-backend/src/main/java/com/etotem/cfc/task/LeaderboardWeeklyResetTask.java
  38. 106 0
      cfc-backend/src/main/resources/schema.sql
  39. 20 0
      cfc-frontend/pages.json
  40. 258 2
      cfc-frontend/pages/membership/upgrade.vue
  41. 21 0
      cfc-frontend/pages/profile/components/ProfileMenu.vue
  42. 386 0
      cfc-frontend/pages/profile/coupons.vue
  43. 280 0
      cfc-frontend/pages/profile/onboarding.vue
  44. 10 0
      cfc-frontend/pages/promotion/index.vue
  45. 149 2
      cfc-frontend/pages/promotion/invite.vue
  46. 429 0
      cfc-frontend/pages/promotion/leaderboard.vue
  47. 293 2
      cfc-frontend/pages/shop/checkout/checkout.vue
  48. 351 0
      cfc-frontend/pages/tasks/daily-tasks.vue
  49. 43 0
      cfc-frontend/utils/api.js
  50. 21 0
      cfc-web/src/api/coupon.js
  51. 21 0
      cfc-web/src/api/growthTask.js
  52. 17 0
      cfc-web/src/api/promotion.js
  53. 18 0
      cfc-web/src/router/index.js
  54. 3 0
      cfc-web/src/views/Layout.vue
  55. 282 0
      cfc-web/src/views/admin/CouponManagement.vue
  56. 288 0
      cfc-web/src/views/admin/GrowthTaskManagement.vue
  57. 233 0
      cfc-web/src/views/admin/PromotionManagement.vue
  58. 160 0
      tests/e2e/test-promotion-flow.sh

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2547,6 +2547,10 @@ log.info("已添加template_id列到tasks表");
             log.warn("numsoul_config 种子数据初始化失败: {}", e.getMessage());
         }
 
+<<<<<<< Updated upstream
+=======
+        // Migration: user_address table (收货地址)
+>>>>>>> Stashed changes
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_address (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -3015,6 +3019,25 @@ try {
             log.warn("初始化血型配置失败: {}", e.getMessage());
         }
 
+        // 初始化优惠券种子数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM coupon", Integer.class);
+            if (count == null || count == 0) {
+                jdbcTemplate.update(
+                    "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count) " +
+                    "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), ?, 0)",
+                    "新用户专享10元券", "FIXED", 1000, 0, "ALL", 100);
+                jdbcTemplate.update(
+                    "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count) " +
+                    "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), ?, 0)",
+                    "会员升级50元券", "FIXED", 5000, 9900, "MEMBERSHIP", 50);
+                log.info("优惠券种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化优惠券数据失败: {}", e.getMessage());
+        }
+
         // 初始化成长规划师模块相关表
         initGuideModuleTables(jdbcTemplate);
     }
@@ -3587,6 +3610,23 @@ try {
         ensureColumn("foods", "energy_kj", "DECIMAL(8,2) COMMENT '能量(kJ/100g)'");
         ensureColumn("foods", "starch", "DECIMAL(8,2) COMMENT '淀粉(g/100g)'");
         ensureColumn("foods", "cholesterol", "DECIMAL(8,2) COMMENT '胆固醇(mg/100g)'");
+
+        // 初始化成长任务种子数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM growth_task", Integer.class);
+
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO growth_task (type, title, description, reward_points, reward_energy, target_value, task_key, enabled, sort_order, created_at) VALUES " +
+                        "('DAILY', '每日签到', '完成健康打卡签到', 20, 3, 1, 'DAILY_CHECKIN', 1, 1, NOW()), " +
+                        "('DAILY', '完成任务', '完成一个任务', 30, 5, 1, 'DAILY_TASK', 1, 2, NOW()), " +
+                        "('DAILY', '每日分享', '分享小程序给好友', 10, 2, 1, 'DAILY_SHARE', 1, 3, NOW()), " +
+                        "('DAILY', 'AI对话', '与AI助手对话一次', 15, 3, 1, 'DAILY_AI', 1, 4, NOW())");
+                log.info("成长任务种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化成长任务种子数据失败: {}", e.getMessage());
+        }
     }
 
     private void insertSysConfigSeed(String key, String value, String desc) {

+ 47 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/CouponController.java

@@ -0,0 +1,47 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.service.CouponService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/coupon")
+public class CouponController {
+
+    @Resource
+    private CouponService couponService;
+
+    @PostMapping("/list")
+    public Result<List<Coupon>> list(@RequestAttribute("userId") Long userId) {
+        return Result.success(couponService.listAvailable(userId));
+    }
+
+    @PostMapping("/claim")
+    public Result<String> claim(@RequestAttribute("userId") Long userId,
+                                @RequestBody Map<String, Object> params) {
+        Long couponId = Long.valueOf(params.get("couponId").toString());
+        String result = couponService.claim(userId, couponId);
+        if ("领取成功".equals(result)) {
+            return Result.success(result);
+        }
+        return Result.error(result);
+    }
+
+    @PostMapping("/apply")
+    public Result<Map<String, Object>> apply(@RequestAttribute("userId") Long userId,
+                                             @RequestBody Map<String, Object> params) {
+        Long userCouponId = Long.valueOf(params.get("userCouponId").toString());
+        String orderType = (String) params.get("orderType");
+        Integer orderAmount = Integer.valueOf(params.get("orderAmount").toString());
+        Integer discount = couponService.apply(userId, userCouponId, orderType, orderAmount);
+        if (discount == null) {
+            return Result.error("优惠券不可用");
+        }
+        return Result.success(java.util.Collections.singletonMap("discount", discount));
+    }
+}

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.GrowthTaskService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/growth-task")
+public class GrowthTaskController {
+
+    @Resource
+    private GrowthTaskService growthTaskService;
+
+    @PostMapping("/list")
+    public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long userId,
+                                                   @RequestBody Map<String, String> params) {
+        String type = params.getOrDefault("type", "DAILY");
+        return Result.success(growthTaskService.getTaskList(userId, type));
+    }
+
+    @PostMapping("/claim")
+    public Result<String> claim(@RequestAttribute("userId") Long userId,
+                                @RequestBody Map<String, Long> params) {
+        Long taskLogId = params.get("taskLogId");
+        if (taskLogId == null) {
+            return Result.error("taskLogId不能为空");
+        }
+        String msg = growthTaskService.claimReward(userId, taskLogId);
+        if ("领取成功".equals(msg)) {
+            return Result.success(msg);
+        }
+        return Result.error(msg);
+    }
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/InviteMilestoneController.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.InviteMilestone;
+import com.etotem.cfc.service.InviteMilestoneService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/invite/milestone")
+public class InviteMilestoneController {
+
+    @Resource
+    private InviteMilestoneService inviteMilestoneService;
+
+    @PostMapping("/list")
+    public Result<List<InviteMilestone>> list(@RequestAttribute("userId") Long userId) {
+        return Result.success(inviteMilestoneService.getMilestones(userId));
+    }
+
+    @PostMapping("/claim")
+    public Result<String> claim(@RequestAttribute("userId") Long userId,
+                                @RequestBody Map<String, String> params) {
+        String milestone = params.get("milestone");
+        if (milestone == null || milestone.trim().isEmpty()) {
+            return Result.error("milestone参数不能为空");
+        }
+        String result = inviteMilestoneService.claimReward(userId, milestone);
+        if ("奖励已发放".equals(result)) {
+            return Result.success(result);
+        }
+        return Result.error(result);
+    }
+}

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -167,8 +167,9 @@ public class MembershipController {
         Long familyId = membershipService.getUserFamilyId(userId);
         String levelCode = params.getOrDefault("levelCode", "FAMILY").toString();
         String paymentType = params.getOrDefault("paymentType", "yearly").toString();
+        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
 
-        PaymentOrderDTO order = membershipService.createOrder(familyId, levelCode, paymentType);
+        PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, couponId);
         return Result.success(order);
     }
 
@@ -187,8 +188,9 @@ public class MembershipController {
         Long familyId = membershipService.getUserFamilyId(userId);
         String levelCode = params.get("levelCode").toString();
         String paymentType = params.get("paymentType").toString();
+        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
 
-        PaymentOrderDTO order = membershipService.createOrder(familyId, levelCode, paymentType);
+        PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, couponId);
         return Result.success(order);
     }
 

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java

@@ -34,8 +34,9 @@ public class PackagePaymentController {
             @RequestBody Map<String, Object> params) {
         Long packageId = Long.valueOf(params.get("packageId").toString());
         String payMethod = params.get("payMethod") != null ? params.get("payMethod").toString() : "wechat";
+        Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;
         
-        PackageOrder order = packagePaymentService.createOrder(userId, familyId, packageId, payMethod);
+        PackageOrder order = packagePaymentService.createOrder(userId, familyId, packageId, payMethod, couponId);
         return Result.success(order);
     }
 

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ReferralLeaderboardController.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ReferralLeaderboardService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/leaderboard")
+public class ReferralLeaderboardController {
+
+    @Resource
+    private ReferralLeaderboardService referralLeaderboardService;
+
+    /**
+     * 获取排行榜前20名
+     */
+    @PostMapping("/top")
+    public Result<List<Map<String, Object>>> top(@RequestBody(required = false) Map<String, String> params) {
+        String weekStart = params != null ? params.get("weekStart") : null;
+        List<Map<String, Object>> topUsers = referralLeaderboardService.getTopUsers(weekStart, 20);
+        return Result.success(topUsers);
+    }
+
+    /**
+     * 获取当前用户排名
+     */
+    @PostMapping("/my-rank")
+    public Result<Map<String, Object>> myRank(@RequestAttribute("userId") Long userId,
+                                               @RequestBody(required = false) Map<String, String> params) {
+        String weekStart = params != null ? params.get("weekStart") : null;
+        Map<String, Object> myRank = referralLeaderboardService.getMyRank(userId, weekStart);
+        return Result.success(myRank);
+    }
+}

+ 84 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java

@@ -0,0 +1,84 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.mapper.CouponMapper;
+import com.etotem.cfc.service.CouponService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/coupon")
+public class AdminCouponController {
+
+    @Resource
+    private CouponService couponService;
+
+    @Resource
+    private CouponMapper couponMapper;
+
+    @PostMapping("/list")
+    public Result<List<Coupon>> list(@RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        return Result.success(couponService.listAll());
+    }
+
+    @PostMapping("/create")
+    public Result<String> create(@RequestBody Coupon coupon,
+                                 @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        if (coupon.getType() == null) coupon.setType("FIXED");
+        if (coupon.getMinSpend() == null) coupon.setMinSpend(0);
+        if (coupon.getApplicableTo() == null) coupon.setApplicableTo("ALL");
+        if (coupon.getUsedCount() == null) coupon.setUsedCount(0);
+        if (coupon.getCreatedAt() == null) coupon.setCreatedAt(new Date());
+        couponMapper.insert(coupon);
+        return Result.success("创建成功");
+    }
+
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody Coupon coupon,
+                                 @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        couponMapper.updateById(coupon);
+        return Result.success("更新成功");
+    }
+
+    @PostMapping("/issue")
+    public Result<String> issue(@RequestBody Map<String, Object> params,
+                                @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long couponId = Long.valueOf(params.get("couponId").toString());
+        Object userIdsObj = params.get("userIds");
+        if (userIdsObj instanceof List) {
+            List<?> userIds = (List<?>) userIdsObj;
+            for (Object uid : userIds) {
+                couponService.issueToUser(couponId, Long.valueOf(uid.toString()));
+            }
+        }
+        return Result.success("发放成功");
+    }
+
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long id = Long.valueOf(params.get("id").toString());
+        couponMapper.deleteById(id);
+        return Result.success("删除成功");
+    }
+}

+ 75 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminLeaderboardController.java

@@ -0,0 +1,75 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.ReferralLeaderboardService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@RestController
+@RequestMapping("/api/admin/leaderboard")
+public class AdminLeaderboardController {
+
+    @Resource
+    private ReferralLeaderboardService referralLeaderboardService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @PostMapping("/top")
+    public Result<List<Map<String, Object>>> top(@RequestBody(required = false) Map<String, Object> params,
+                                                  @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        String weekStart = params != null && params.get("weekStart") != null
+                ? (String) params.get("weekStart") : null;
+        List<Map<String, Object>> topUsers = referralLeaderboardService.getTopUsers(weekStart, 20);
+        return Result.success(topUsers);
+    }
+
+    @PostMapping("/search")
+    public Result<List<Map<String, Object>>> search(@RequestBody Map<String, Object> params,
+                                                     @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Object userIdObj = params.get("userId");
+        String nickname = (String) params.get("nickname");
+
+        if (userIdObj != null) {
+            Long userId = ((Number) userIdObj).longValue();
+            Map<String, Object> myRank = referralLeaderboardService.getMyRank(userId, null);
+            User user = userMapper.selectById(userId);
+            if (user != null) {
+                myRank.put("nickname", user.getNickname() != null ? user.getNickname() : "微信用户");
+                myRank.put("avatarUrl", user.getAvatar() != null ? user.getAvatar() : "");
+            } else {
+                myRank.put("nickname", "未知用户");
+                myRank.put("avatarUrl", "");
+            }
+            return Result.success(Collections.singletonList(myRank));
+        }
+
+        if (nickname != null && !nickname.trim().isEmpty()) {
+            List<User> users = userMapper.selectList(
+                    new LambdaQueryWrapper<User>()
+                            .like(User::getNickname, nickname.trim())
+            );
+            List<Map<String, Object>> results = new ArrayList<>();
+            for (User user : users) {
+                Map<String, Object> rankInfo = referralLeaderboardService.getMyRank(user.getId(), null);
+                rankInfo.put("nickname", user.getNickname() != null ? user.getNickname() : "微信用户");
+                rankInfo.put("avatarUrl", user.getAvatar() != null ? user.getAvatar() : "");
+                results.add(rankInfo);
+            }
+            return Result.success(results);
+        }
+
+        return Result.error("请提供userId或nickname参数");
+    }
+}

+ 88 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminMilestoneController.java

@@ -0,0 +1,88 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.InviteMilestone;
+import com.etotem.cfc.mapper.InviteMilestoneMapper;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@RestController
+@RequestMapping("/api/admin/milestone")
+public class AdminMilestoneController {
+
+    @Resource
+    private InviteMilestoneMapper inviteMilestoneMapper;
+
+    private static final List<Map<String, Object>> MILESTONE_CONFIGS;
+
+    static {
+        List<Map<String, Object>> configs = new ArrayList<>();
+        Map<String, Object> m1 = new LinkedHashMap<>();
+        m1.put("milestone", "INVITE_3");
+        m1.put("rewardPoints", 200);
+        m1.put("rewardEnergy", 20);
+        m1.put("targetCount", 3);
+        configs.add(m1);
+
+        Map<String, Object> m2 = new LinkedHashMap<>();
+        m2.put("milestone", "INVITE_5");
+        m2.put("rewardPoints", 500);
+        m2.put("rewardEnergy", 50);
+        m2.put("targetCount", 5);
+        configs.add(m2);
+
+        Map<String, Object> m3 = new LinkedHashMap<>();
+        m3.put("milestone", "INVITE_10");
+        m3.put("rewardPoints", 1000);
+        m3.put("rewardEnergy", 100);
+        m3.put("targetCount", 10);
+        configs.add(m3);
+
+        MILESTONE_CONFIGS = Collections.unmodifiableList(configs);
+    }
+
+    private static final Set<String> VALID_MILESTONES = new HashSet<>(Arrays.asList("INVITE_3", "INVITE_5", "INVITE_10"));
+
+    @PostMapping("/config")
+    public Result<List<Map<String, Object>>> config(@RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        return Result.success(MILESTONE_CONFIGS);
+    }
+
+    @PostMapping("/config/update")
+    public Result<String> updateConfig(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        String milestone = (String) params.get("milestone");
+        if (milestone == null || !VALID_MILESTONES.contains(milestone)) {
+            return Result.error("无效的里程碑, 请使用 INVITE_3/INVITE_5/INVITE_10");
+        }
+        Object rewardPointsObj = params.get("rewardPoints");
+        Object rewardEnergyObj = params.get("rewardEnergy");
+        if (rewardPointsObj == null || rewardEnergyObj == null) {
+            return Result.error("rewardPoints和rewardEnergy不能为空");
+        }
+        int rewardPoints = ((Number) rewardPointsObj).intValue();
+        int rewardEnergy = ((Number) rewardEnergyObj).intValue();
+        if (rewardPoints < 0 || rewardEnergy < 0) {
+            return Result.error("奖励值不能为负数");
+        }
+
+        InviteMilestone updateRecord = new InviteMilestone();
+        updateRecord.setRewardPoints(rewardPoints);
+        updateRecord.setRewardEnergy(rewardEnergy);
+        inviteMilestoneMapper.update(updateRecord,
+                new LambdaQueryWrapper<InviteMilestone>()
+                        .eq(InviteMilestone::getMilestone, milestone)
+                        .eq(InviteMilestone::getStatus, "PENDING"));
+
+        return Result.success("更新成功");
+    }
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -40,6 +40,9 @@ public class AIChatController {
     @Resource
     private RecommendationService recommendationService;
 
+    @Resource
+    private com.etotem.cfc.service.GrowthTaskService growthTaskService;
+
     @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @PostMapping("/chat/send")
     public Result<Map<String, Object>> sendMessage(
@@ -80,6 +83,9 @@ public class AIChatController {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("answer", difyResp.getOrDefault("answer", ""));
         result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
+
+        try { growthTaskService.updateProgress(userId, "DAILY_AI", 1); } catch (Exception e) { log.warn("成长任务AI对话进度更新失败: userId={}, error={}", userId, e.getMessage()); }
+
         return Result.success(result);
     }
 

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java

@@ -0,0 +1,40 @@
+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("coupon")
+public class Coupon implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private String type;
+
+    /** 金额,单位:分 (1000 = 10元) */
+    private Integer value;
+
+    /** 最低消费,单位:分 */
+    private Integer minSpend;
+
+    /** 适用范围: ALL / MEMBERSHIP 等 */
+    private String applicableTo;
+
+    private Date validFrom;
+
+    private Date validUntil;
+
+    private Integer totalCount;
+
+    private Integer usedCount;
+
+    private Date createdAt;
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTask.java

@@ -0,0 +1,37 @@
+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("growth_task")
+public class GrowthTask implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String type;
+
+    private String title;
+
+    private String description;
+
+    private Integer rewardPoints;
+
+    private Integer rewardEnergy;
+
+    private Integer targetValue;
+
+    private String taskKey;
+
+    private Integer enabled;
+
+    private Integer sortOrder;
+
+    private Date createdAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthTaskLog.java

@@ -0,0 +1,31 @@
+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("growth_task_log")
+public class GrowthTaskLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long taskId;
+
+    private Integer progress;
+
+    private Integer completed;
+
+    private Integer claimed;
+
+    private String date;
+
+    private Date createdAt;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/InviteMilestone.java

@@ -0,0 +1,33 @@
+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("invite_milestone")
+public class InviteMilestone implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String milestone;
+
+    private Integer rewardPoints;
+
+    private Integer rewardEnergy;
+
+    private String status;
+
+    private Date achievedAt;
+
+    private Date claimedAt;
+
+    private Date createdAt;
+}

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PackageOrder.java

@@ -29,6 +29,8 @@ public class PackageOrder implements Serializable {
 
     private Integer platformFee; // 平台服务费(分)
 
+    private Long userCouponId; // 使用的用户优惠券ID
+
     private Long guideId;         // 成长规划师ID
 
     private String status;         // pending/paid/cancelled/refunded

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PaymentOrder.java

@@ -25,6 +25,8 @@ public class PaymentOrder implements Serializable {
 
     private Integer amount;
 
+    private Long userCouponId; // 使用的用户优惠券ID
+
     private String status;
 
     private String payMethod;

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ReferralLeaderboard.java

@@ -0,0 +1,27 @@
+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("referral_leaderboard")
+public class ReferralLeaderboard implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String weekStart;
+
+    private Integer referralCount;
+
+    private Integer rank;
+
+    private Date createdAt;
+}

+ 30 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java

@@ -0,0 +1,30 @@
+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("user_coupon")
+public class UserCoupon implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long couponId;
+
+    /** 状态: AVAILABLE / USED */
+    private String status;
+
+    private Date receivedAt;
+
+    private Date usedAt;
+
+    private Long orderId;
+}

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

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

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/GrowthTaskLogMapper.java

@@ -0,0 +1,19 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.GrowthTaskLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface GrowthTaskLogMapper extends BaseMapper<GrowthTaskLog> {
+
+    @Select("SELECT * FROM growth_task_log WHERE user_id = #{userId} AND date = #{date}")
+    List<GrowthTaskLog> findByUserAndDate(@Param("userId") Long userId, @Param("date") String date);
+
+    @Select("SELECT * FROM growth_task_log WHERE user_id = #{userId} AND task_id = #{taskId} AND date = #{date} LIMIT 1")
+    GrowthTaskLog findByUserTaskAndDate(@Param("userId") Long userId, @Param("taskId") Long taskId, @Param("date") String date);
+}

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

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

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

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

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ReferralLeaderboardMapper.java

@@ -0,0 +1,19 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ReferralLeaderboard;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface ReferralLeaderboardMapper extends BaseMapper<ReferralLeaderboard> {
+
+    @Select("SELECT * FROM referral_leaderboard WHERE week_start = #{weekStart} ORDER BY referral_count DESC LIMIT #{limit}")
+    List<ReferralLeaderboard> findTopByWeek(@Param("weekStart") String weekStart, @Param("limit") int limit);
+
+    @Select("SELECT * FROM referral_leaderboard WHERE user_id = #{userId} AND week_start = #{weekStart}")
+    ReferralLeaderboard findByUserAndWeek(@Param("userId") Long userId, @Param("weekStart") String weekStart);
+}

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

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

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java

@@ -44,12 +44,18 @@ public class CommissionService {
     @Resource
     private WithdrawalRequestMapper withdrawalRequestMapper;
 
+    @Resource
+    private InviteMilestoneService inviteMilestoneService;
+
     @Resource
     private SysConfigService sysConfigService;
 
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private ReferralLeaderboardService referralLeaderboardService;
+
     // ==================== 工具方法 ====================
 
     /**
@@ -112,6 +118,10 @@ public class CommissionService {
         userMapper.updateById(referrer);
 
         try { onboardingService.completeTask(userId, "BIND_REFERRAL"); } catch (Exception e) { /* onboarding trigger failure should not block referral bind */ }
+
+        try { inviteMilestoneService.checkMilestones(referrer.getId()); } catch (Exception e) { log.warn("Milestone check failed for referrer={}", referrer.getId(), e); }
+
+        try { referralLeaderboardService.incrementCount(referrer.getId()); } catch (Exception e) { log.warn("Leaderboard increment failed for referrer={}", referrer.getId(), e); }
     }
 
     public Page<User> getMyReferrals(Long userId, int page, int size) {

+ 141 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java

@@ -0,0 +1,141 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.entity.UserCoupon;
+import com.etotem.cfc.mapper.CouponMapper;
+import com.etotem.cfc.mapper.UserCouponMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class CouponService {
+
+    @Resource
+    private CouponMapper couponMapper;
+
+    @Resource
+    private UserCouponMapper userCouponMapper;
+
+    public List<Coupon> listAvailable(Long userId) {
+        List<UserCoupon> userCoupons = userCouponMapper.selectList(
+                new LambdaQueryWrapper<UserCoupon>()
+                        .eq(UserCoupon::getUserId, userId)
+                        .eq(UserCoupon::getStatus, "AVAILABLE"));
+        if (userCoupons.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<Long> couponIds = new ArrayList<>();
+        for (UserCoupon uc : userCoupons) {
+            couponIds.add(uc.getCouponId());
+        }
+        Date now = new Date();
+        return couponMapper.selectList(
+                new LambdaQueryWrapper<Coupon>()
+                        .in(Coupon::getId, couponIds)
+                        .le(Coupon::getValidFrom, now)
+                        .ge(Coupon::getValidUntil, now)
+                        .orderByAsc(Coupon::getValue));
+    }
+
+    @Transactional
+    public String claim(Long userId, Long couponId) {
+        Coupon coupon = couponMapper.selectById(couponId);
+        if (coupon == null) {
+            return "优惠券不存在";
+        }
+        if (coupon.getTotalCount() != null && coupon.getUsedCount() != null
+                && coupon.getUsedCount() >= coupon.getTotalCount()) {
+            return "优惠券已领完";
+        }
+        Date now = new Date();
+        if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
+            return "优惠券尚未开始";
+        }
+        if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            return "优惠券已过期";
+        }
+        Long count = userCouponMapper.selectCount(
+                new LambdaQueryWrapper<UserCoupon>()
+                        .eq(UserCoupon::getUserId, userId)
+                        .eq(UserCoupon::getCouponId, couponId));
+        if (count != null && count > 0) {
+            return "已领取过该优惠券";
+        }
+        UserCoupon uc = new UserCoupon();
+        uc.setUserId(userId);
+        uc.setCouponId(couponId);
+        uc.setStatus("AVAILABLE");
+        uc.setReceivedAt(new Date());
+        userCouponMapper.insert(uc);
+        coupon.setUsedCount(coupon.getUsedCount() == null ? 1 : coupon.getUsedCount() + 1);
+        couponMapper.updateById(coupon);
+        return "领取成功";
+    }
+
+    public Integer apply(Long userId, Long userCouponId, String orderType, Integer orderAmount) {
+        UserCoupon uc = userCouponMapper.selectById(userCouponId);
+        if (uc == null || !uc.getUserId().equals(userId) || !"AVAILABLE".equals(uc.getStatus())) {
+            return null;
+        }
+        Coupon coupon = couponMapper.selectById(uc.getCouponId());
+        if (coupon == null) {
+            return null;
+        }
+        Date now = new Date();
+        if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
+            return null;
+        }
+        if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            return null;
+        }
+        if (!"ALL".equals(coupon.getApplicableTo()) && !coupon.getApplicableTo().equals(orderType)) {
+            return null;
+        }
+        if (coupon.getMinSpend() != null && orderAmount < coupon.getMinSpend()) {
+            return null;
+        }
+        return Math.min(coupon.getValue(), orderAmount);
+    }
+
+    @Transactional
+    public void markUsed(Long userCouponId, Long orderId) {
+        UserCoupon uc = userCouponMapper.selectById(userCouponId);
+        if (uc != null) {
+            uc.setStatus("USED");
+            uc.setUsedAt(new Date());
+            uc.setOrderId(orderId);
+            userCouponMapper.updateById(uc);
+        }
+    }
+
+    @Transactional
+    public void revert(Long userCouponId) {
+        UserCoupon uc = userCouponMapper.selectById(userCouponId);
+        if (uc != null) {
+            uc.setStatus("AVAILABLE");
+            uc.setUsedAt(null);
+            uc.setOrderId(null);
+            userCouponMapper.updateById(uc);
+        }
+    }
+
+    public List<Coupon> listAll() {
+        return couponMapper.selectList(null);
+    }
+
+    @Transactional
+    public void issueToUser(Long couponId, Long userId) {
+        UserCoupon uc = new UserCoupon();
+        uc.setCouponId(couponId);
+        uc.setUserId(userId);
+        uc.setStatus("AVAILABLE");
+        uc.setReceivedAt(new Date());
+        userCouponMapper.insert(uc);
+    }
+}

+ 255 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java

@@ -0,0 +1,255 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.GrowthTask;
+import com.etotem.cfc.entity.GrowthTaskLog;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.GrowthTaskLogMapper;
+import com.etotem.cfc.mapper.GrowthTaskMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Slf4j
+@Service
+public class GrowthTaskService {
+
+    @Resource
+    private GrowthTaskMapper growthTaskMapper;
+
+    @Resource
+    private GrowthTaskLogMapper growthTaskLogMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private PointsService pointsService;
+
+    @Resource
+    private EnergyService energyService;
+
+    /**
+     * Get task list with user's progress for today (daily) or all-time (newbie).
+     */
+    public List<Map<String, Object>> getTaskList(Long userId, String type) {
+        if (type == null || type.isEmpty()) {
+            type = "DAILY";
+        }
+
+        List<GrowthTask> tasks = growthTaskMapper.selectList(
+                new LambdaQueryWrapper<GrowthTask>()
+                        .eq(GrowthTask::getType, type)
+                        .eq(GrowthTask::getEnabled, 1)
+                        .orderByAsc(GrowthTask::getSortOrder)
+        );
+
+        String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (GrowthTask task : tasks) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("id", task.getId());
+            item.put("type", task.getType());
+            item.put("title", task.getTitle());
+            item.put("description", task.getDescription());
+            item.put("rewardPoints", task.getRewardPoints());
+            item.put("rewardEnergy", task.getRewardEnergy());
+            item.put("targetValue", task.getTargetValue());
+            item.put("taskKey", task.getTaskKey());
+
+            // Find user's log for this task
+            LambdaQueryWrapper<GrowthTaskLog> logWrapper = new LambdaQueryWrapper<GrowthTaskLog>()
+                    .eq(GrowthTaskLog::getUserId, userId)
+                    .eq(GrowthTaskLog::getTaskId, task.getId());
+
+            if ("DAILY".equals(type)) {
+                logWrapper.eq(GrowthTaskLog::getDate, today);
+            }
+            // NEWBIE: no date filter, all-time
+
+            GrowthTaskLog taskLog = growthTaskLogMapper.selectOne(logWrapper.last("LIMIT 1"));
+
+            if (taskLog != null) {
+                item.put("taskLogId", taskLog.getId());
+                item.put("progress", taskLog.getProgress());
+                item.put("completed", taskLog.getCompleted());
+                item.put("claimed", taskLog.getClaimed());
+            } else {
+                item.put("taskLogId", null);
+                item.put("progress", 0);
+                item.put("completed", 0);
+                item.put("claimed", 0);
+            }
+
+            result.add(item);
+        }
+
+        return result;
+    }
+
+    /**
+     * Update progress for a task. Auto-marks completed when progress >= targetValue.
+     */
+    @Transactional
+    public void updateProgress(Long userId, String taskKey, int increment) {
+        GrowthTask task = growthTaskMapper.selectOne(
+                new LambdaQueryWrapper<GrowthTask>()
+                        .eq(GrowthTask::getTaskKey, taskKey)
+                        .eq(GrowthTask::getEnabled, 1)
+                        .last("LIMIT 1")
+        );
+        if (task == null) {
+            log.warn("成长任务不存在或未启用: taskKey={}", taskKey);
+            return;
+        }
+
+        String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+
+        // Find or create log
+        LambdaQueryWrapper<GrowthTaskLog> logWrapper = new LambdaQueryWrapper<GrowthTaskLog>()
+                .eq(GrowthTaskLog::getUserId, userId)
+                .eq(GrowthTaskLog::getTaskId, task.getId());
+
+        if ("DAILY".equals(task.getType())) {
+            logWrapper.eq(GrowthTaskLog::getDate, today);
+        }
+
+        GrowthTaskLog taskLog = growthTaskLogMapper.selectOne(logWrapper.last("LIMIT 1"));
+
+        if (taskLog == null) {
+            taskLog = new GrowthTaskLog();
+            taskLog.setUserId(userId);
+            taskLog.setTaskId(task.getId());
+            taskLog.setProgress(0);
+            taskLog.setCompleted(0);
+            taskLog.setClaimed(0);
+            if ("DAILY".equals(task.getType())) {
+                taskLog.setDate(today);
+            }
+            taskLog.setCreatedAt(new Date());
+            growthTaskLogMapper.insert(taskLog);
+        }
+
+        // Already completed, skip
+        if (taskLog.getCompleted() != null && taskLog.getCompleted() == 1) {
+            return;
+        }
+
+        // Update progress
+        int newProgress = (taskLog.getProgress() != null ? taskLog.getProgress() : 0) + increment;
+        taskLog.setProgress(newProgress);
+
+        // Auto-complete when target reached
+        int targetValue = task.getTargetValue() != null ? task.getTargetValue() : 1;
+        if (newProgress >= targetValue) {
+            taskLog.setCompleted(1);
+        }
+
+        growthTaskLogMapper.updateById(taskLog);
+    }
+
+    /**
+     * Claim reward for a completed task. Awards points + energy to the child.
+     */
+    @Transactional
+    public String claimReward(Long userId, Long taskLogId) {
+        GrowthTaskLog taskLog = growthTaskLogMapper.selectById(taskLogId);
+        if (taskLog == null) {
+            return "任务记录不存在";
+        }
+        if (!taskLog.getUserId().equals(userId)) {
+            return "无权操作此任务";
+        }
+        if (taskLog.getCompleted() == null || taskLog.getCompleted() != 1) {
+            return "任务未完成,无法领取";
+        }
+        if (taskLog.getClaimed() != null && taskLog.getClaimed() == 1) {
+            return "奖励已领取";
+        }
+
+        GrowthTask task = growthTaskMapper.selectById(taskLog.getTaskId());
+        if (task == null) {
+            return "任务不存在";
+        }
+
+        // Mark as claimed
+        taskLog.setClaimed(1);
+        growthTaskLogMapper.updateById(taskLog);
+
+        // Find child by userId
+        Child child = childMapper.selectOne(
+                new LambdaQueryWrapper<Child>()
+                        .eq(Child::getUserId, userId)
+                        .last("LIMIT 1")
+        );
+
+        if (child != null) {
+            // Award points
+            int rewardPoints = task.getRewardPoints() != null ? task.getRewardPoints() : 0;
+            if (rewardPoints > 0) {
+                try {
+                    pointsService.awardSystemPoints(child.getId(), rewardPoints, "growth:" + task.getTaskKey());
+                } catch (Exception e) {
+                    log.warn("成长任务积分发放失败: childId={}, taskKey={}, error={}", child.getId(), task.getTaskKey(), e.getMessage());
+                }
+            }
+
+            // Award energy
+            int rewardEnergy = task.getRewardEnergy() != null ? task.getRewardEnergy() : 0;
+            if (rewardEnergy > 0) {
+                try {
+                    energyService.awardEnergy(child.getId(), "growth_day", 0L, rewardEnergy,
+                            "每日任务奖励:" + task.getTaskKey(), 1);
+                } catch (Exception e) {
+                    log.warn("成长任务能量发放失败: childId={}, taskKey={}, error={}", child.getId(), task.getTaskKey(), e.getMessage());
+                }
+            }
+        } else {
+            log.warn("成长任务奖励: 用户{}无孩子记录,跳过积分/能量发放", userId);
+        }
+
+        return "领取成功";
+    }
+
+    /**
+     * Initialize newbie task logs for a new user.
+     * Called from OnboardingService.initForUser to create growth_task_log entries
+     * for newbie-type tasks (NEWBIE_PROFILE, NEWBIE_CHILD, NEWBIE_REFERRAL).
+     */
+    public void initNewbieTasks(Long userId) {
+        List<GrowthTask> newbieTasks = growthTaskMapper.selectList(
+                new LambdaQueryWrapper<GrowthTask>()
+                        .eq(GrowthTask::getType, "NEWBIE")
+                        .eq(GrowthTask::getEnabled, 1)
+        );
+
+        for (GrowthTask task : newbieTasks) {
+            // Check if log already exists (idempotent)
+            GrowthTaskLog existing = growthTaskLogMapper.selectOne(
+                    new LambdaQueryWrapper<GrowthTaskLog>()
+                            .eq(GrowthTaskLog::getUserId, userId)
+                            .eq(GrowthTaskLog::getTaskId, task.getId())
+                            .last("LIMIT 1")
+            );
+            if (existing != null) {
+                continue;
+            }
+
+            GrowthTaskLog taskLog = new GrowthTaskLog();
+            taskLog.setUserId(userId);
+            taskLog.setTaskId(task.getId());
+            taskLog.setProgress(0);
+            taskLog.setCompleted(0);
+            taskLog.setClaimed(0);
+            taskLog.setDate(null); // NEWBIE tasks are all-time, no date
+            taskLog.setCreatedAt(new Date());
+            growthTaskLogMapper.insert(taskLog);
+        }
+    }
+}

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthCheckinService.java

@@ -47,6 +47,9 @@ public class HealthCheckinService {
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private GrowthTaskService growthTaskService;
+
     public List<HealthCheckin> getCheckins(Long userId, Long childId, String yearMonth) {
         LambdaQueryWrapper<HealthCheckin> wrapper = new LambdaQueryWrapper<HealthCheckin>()
                 .eq(HealthCheckin::getUserId, userId);
@@ -170,6 +173,8 @@ public class HealthCheckinService {
 
         try { onboardingService.completeTask(checkin.getUserId(), "FIRST_CHECKIN"); } catch (Exception e) { /* onboarding trigger failure should not block checkin */ }
 
+        try { growthTaskService.updateProgress(checkin.getUserId(), "DAILY_CHECKIN", 1); } catch (Exception e) { log.warn("成长任务签到进度更新失败: userId={}, error={}", checkin.getUserId(), e.getMessage()); }
+
         return checkin;
     }
 

+ 148 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InviteMilestoneService.java

@@ -0,0 +1,148 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.CommissionRecord;
+import com.etotem.cfc.entity.InviteMilestone;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.CommissionRecordMapper;
+import com.etotem.cfc.mapper.InviteMilestoneMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class InviteMilestoneService {
+
+    private static final Logger log = LoggerFactory.getLogger(InviteMilestoneService.class);
+
+    @Resource
+    private InviteMilestoneMapper inviteMilestoneMapper;
+
+    @Resource
+    private CommissionRecordMapper commissionRecordMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private PointsService pointsService;
+
+    @Resource
+    private EnergyService energyService;
+
+    private static final LinkedHashMap<String, int[]> MILESTONE_TIERS = new LinkedHashMap<>();
+    static {
+        MILESTONE_TIERS.put("INVITE_3", new int[]{3, 200, 20});
+        MILESTONE_TIERS.put("INVITE_5", new int[]{5, 500, 50});
+        MILESTONE_TIERS.put("INVITE_10", new int[]{10, 1000, 100});
+    }
+
+    /**
+     * Check and create milestone records after a successful referral bind.
+     * Uses DB-level COUNT for accuracy and prevents duplicate milestones.
+     */
+    public void checkMilestones(Long userId) {
+        // DB-level count of direct referrals (level=1)
+        Long count = commissionRecordMapper.selectCount(
+                new LambdaQueryWrapper<CommissionRecord>()
+                        .eq(CommissionRecord::getReferrerId, userId)
+                        .eq(CommissionRecord::getLevel, 1)
+        );
+        int inviteCount = count != null ? count.intValue() : 0;
+
+        for (Map.Entry<String, int[]> entry : MILESTONE_TIERS.entrySet()) {
+            String milestoneKey = entry.getKey();
+            int threshold = entry.getValue()[0];
+            int rewardPoints = entry.getValue()[1];
+            int rewardEnergy = entry.getValue()[2];
+
+            if (inviteCount >= threshold) {
+                // Check if milestone already exists for this user+key
+                Long existing = inviteMilestoneMapper.selectCount(
+                        new LambdaQueryWrapper<InviteMilestone>()
+                                .eq(InviteMilestone::getUserId, userId)
+                                .eq(InviteMilestone::getMilestone, milestoneKey)
+                );
+                if (existing == null || existing == 0) {
+                    InviteMilestone milestone = new InviteMilestone();
+                    milestone.setUserId(userId);
+                    milestone.setMilestone(milestoneKey);
+                    milestone.setRewardPoints(rewardPoints);
+                    milestone.setRewardEnergy(rewardEnergy);
+                    milestone.setStatus("PENDING");
+                    milestone.setAchievedAt(new Date());
+                    milestone.setCreatedAt(new Date());
+                    inviteMilestoneMapper.insert(milestone);
+                    log.info("Created milestone {} for userId={}", milestoneKey, userId);
+                }
+            }
+        }
+    }
+
+    /**
+     * Claim a milestone reward. Sets status to CLAIMED and awards points + energy.
+     */
+    @Transactional
+    public String claimReward(Long userId, String milestoneKey) {
+        InviteMilestone milestone = inviteMilestoneMapper.selectOne(
+                new LambdaQueryWrapper<InviteMilestone>()
+                        .eq(InviteMilestone::getUserId, userId)
+                        .eq(InviteMilestone::getMilestone, milestoneKey)
+                        .last("LIMIT 1")
+        );
+        if (milestone == null) {
+            return "里程碑未达成";
+        }
+        if ("CLAIMED".equals(milestone.getStatus())) {
+            return "奖励已领取";
+        }
+        if (!"PENDING".equals(milestone.getStatus())) {
+            return "里程碑状态异常";
+        }
+
+        milestone.setStatus("CLAIMED");
+        milestone.setClaimedAt(new Date());
+        inviteMilestoneMapper.updateById(milestone);
+
+        // Resolve childId from userId for points/energy award
+        Child child = childMapper.selectOne(
+                new LambdaQueryWrapper<Child>()
+                        .eq(Child::getUserId, userId)
+                        .last("LIMIT 1")
+        );
+        if (child != null) {
+            try {
+                pointsService.awardSystemPoints(child.getId(),
+                        milestone.getRewardPoints(),
+                        "邀请里程碑奖励: " + milestoneKey);
+            } catch (Exception e) {
+                log.warn("Milestone points award failed: userId={}, milestone={}", userId, milestoneKey, e);
+            }
+            try {
+                energyService.awardEnergy(child.getId(), "invite_milestone", 0L,
+                        milestone.getRewardEnergy(),
+                        "邀请里程碑奖励: " + milestoneKey, 30);
+            } catch (Exception e) {
+                log.warn("Milestone energy award failed: userId={}, milestone={}", userId, milestoneKey, e);
+            }
+        }
+
+        return "奖励已发放";
+    }
+
+    /**
+     * Get all milestone records for a user.
+     */
+    public List<InviteMilestone> getMilestones(Long userId) {
+        return inviteMilestoneMapper.selectList(
+                new LambdaQueryWrapper<InviteMilestone>()
+                        .eq(InviteMilestone::getUserId, userId)
+                        .orderByAsc(InviteMilestone::getMilestone)
+        );
+    }
+}

+ 21 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -40,6 +40,9 @@ public class MembershipService implements MembershipServiceInterface {
     @Resource
     private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
 
+    @Resource
+    private CouponService couponService;
+
     // ==================== 基础查询 ====================
 
     /**
@@ -158,7 +161,7 @@ public class MembershipService implements MembershipServiceInterface {
     /**
      * 创建订单
      */
-    public PaymentOrderDTO createOrder(Long familyId, String levelCode, String paymentType) {
+    public PaymentOrderDTO createOrder(Long userId, Long familyId, String levelCode, String paymentType, Long userCouponId) {
         MembershipLevel level = levelMapper.selectOne(
                 new LambdaQueryWrapper<MembershipLevel>()
                         .eq(MembershipLevel::getLevelCode, levelCode)
@@ -182,12 +185,24 @@ public class MembershipService implements MembershipServiceInterface {
             amount = "yearly".equals(paymentType) ? level.getPriceYearly() : level.getPriceMonthly();
         }
 
+        int originalAmount = amount;
+        int finalAmount = originalAmount;
+        Long appliedCouponId = null;
+        if (userCouponId != null) {
+            Integer discount = couponService.apply(userId, userCouponId, "MEMBERSHIP", originalAmount);
+            if (discount != null && discount > 0) {
+                finalAmount = Math.max(0, originalAmount - discount);
+                appliedCouponId = userCouponId;
+            }
+        }
+
         PaymentOrder order = new PaymentOrder();
         order.setOrderNo(generateOrderNo());
         order.setFamilyId(familyId);
         order.setLevelCode(levelCode);
         order.setPaymentType(paymentType);
-        order.setAmount(amount);
+        order.setAmount(finalAmount);
+        order.setUserCouponId(appliedCouponId);
         order.setStatus("pending");
         order.setCreatedAt(new Date());
         order.setUpdatedAt(new Date());
@@ -232,6 +247,10 @@ public class MembershipService implements MembershipServiceInterface {
             order.setUpdatedAt(new Date());
             paymentOrderMapper.updateById(order);
 
+            if (order.getUserCouponId() != null) {
+                couponService.markUsed(order.getUserCouponId(), order.getId());
+            }
+
             // 查找该家庭的创建者(家庭管理员)
             Family family = familyMapper.selectById(order.getFamilyId());
             if (family == null || family.getCreatorId() == null) {

+ 21 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java

@@ -34,6 +34,9 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
     @Resource
     private CommissionService commissionService;
 
+    @Resource
+    private CouponService couponService;
+
     @Value("${wechat.appid}")
     private String appid;
 
@@ -51,7 +54,7 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
     /**
      * 创建支付订单
      */
-    public PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod) {
+    public PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod, Long userCouponId) {
         // 获取套餐信息
         TaskTemplatePackage pkg = packageService.getPackageById(packageId);
         if (pkg == null) {
@@ -78,6 +81,17 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
             return order;
         }
 
+        int originalPrice = pkg.getPrice();
+        int finalPrice = originalPrice;
+        Long appliedCouponId = null;
+        if (userCouponId != null) {
+            Integer discount = couponService.apply(userId, userCouponId, "PRODUCT", originalPrice);
+            if (discount != null && discount > 0) {
+                finalPrice = Math.max(0, originalPrice - discount);
+                appliedCouponId = userCouponId;
+            }
+        }
+
         // 创建付费订单
         PackageOrder order = new PackageOrder();
         order.setOrderNo(generateOrderNo());
@@ -85,8 +99,9 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
         order.setFamilyId(familyId);
         order.setPackageId(packageId);
         order.setPackageName(pkg.getName());
-        order.setPrice(pkg.getPrice());
+        order.setPrice(finalPrice);
         order.setPlatformFee(pkg.getPlatformFee());
+        order.setUserCouponId(appliedCouponId);
         order.setGuideId(pkg.getCreatorId());
         order.setStatus("pending");
         order.setPayMethod(payMethod);
@@ -190,6 +205,10 @@ public class PackagePaymentService implements PackagePaymentServiceInterface {
                 order.setUpdatedAt(new Date());
                 orderMapper.updateById(order);
 
+                if (order.getUserCouponId() != null) {
+                    couponService.markUsed(order.getUserCouponId(), order.getId());
+                }
+
                 commissionService.settle(order.getId(), "package", order.getUserId(),
                         order.getPrice(), null);
 

+ 133 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ReferralLeaderboardService.java

@@ -0,0 +1,133 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.ReferralLeaderboard;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ReferralLeaderboardMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ReferralLeaderboardService {
+
+    private static final Logger log = LoggerFactory.getLogger(ReferralLeaderboardService.class);
+
+    @Resource
+    private ReferralLeaderboardMapper referralLeaderboardMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    /**
+     * 获取当前周的周一日期(Asia/Shanghai时区)
+     */
+    public String getCurrentWeekStart() {
+        LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
+        LocalDate monday = today.with(java.time.DayOfWeek.MONDAY);
+        return monday.format(DateTimeFormatter.ISO_LOCAL_DATE);
+    }
+
+    /**
+     * 推荐成功后递增当前周的推荐计数
+     * 使用 INSERT ... ON DUPLICATE KEY UPDATE 保证原子性
+     */
+    public void incrementCount(Long userId) {
+        String weekStart = getCurrentWeekStart();
+        String sql = "INSERT INTO referral_leaderboard (user_id, week_start, referral_count, created_at) " +
+                "VALUES (?, ?, 1, NOW()) " +
+                "ON DUPLICATE KEY UPDATE referral_count = referral_count + 1";
+        try {
+            jdbcTemplate.update(sql, userId, weekStart);
+            log.info("排行榜计数递增成功: userId={}, weekStart={}", userId, weekStart);
+        } catch (Exception e) {
+            log.error("排行榜计数递增失败: userId={}, weekStart={}", userId, weekStart, e);
+        }
+    }
+
+    /**
+     * 获取指定周的排行榜前N名(含用户昵称和头像)
+     */
+    public List<Map<String, Object>> getTopUsers(String weekStart, int limit) {
+        if (weekStart == null || weekStart.isEmpty()) {
+            weekStart = getCurrentWeekStart();
+        }
+        List<ReferralLeaderboard> topList = referralLeaderboardMapper.findTopByWeek(weekStart, limit);
+        List<Map<String, Object>> result = new ArrayList<>();
+        int rank = 1;
+        for (ReferralLeaderboard entry : topList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("userId", entry.getUserId());
+            item.put("referralCount", entry.getReferralCount());
+            item.put("rank", rank);
+
+            User user = userMapper.selectById(entry.getUserId());
+            if (user != null) {
+                item.put("nickname", user.getNickname() != null ? user.getNickname() : "微信用户");
+                item.put("avatarUrl", user.getAvatar() != null ? user.getAvatar() : "");
+            } else {
+                item.put("nickname", "未知用户");
+                item.put("avatarUrl", "");
+            }
+            result.add(item);
+            rank++;
+        }
+        return result;
+    }
+
+    /**
+     * 获取当前用户在指定周的排名
+     */
+    public Map<String, Object> getMyRank(Long userId, String weekStart) {
+        if (weekStart == null || weekStart.isEmpty()) {
+            weekStart = getCurrentWeekStart();
+        }
+        ReferralLeaderboard entry = referralLeaderboardMapper.findByUserAndWeek(userId, weekStart);
+        Map<String, Object> result = new HashMap<>();
+        result.put("userId", userId);
+        if (entry != null) {
+            // 计算实际排名:统计比当前用户推荐数多的人数
+            String countSql = "SELECT COUNT(*) + 1 FROM referral_leaderboard " +
+                    "WHERE week_start = ? AND referral_count > ?";
+            Integer actualRank = jdbcTemplate.queryForObject(countSql, Integer.class, weekStart, entry.getReferralCount());
+            result.put("referralCount", entry.getReferralCount());
+            result.put("rank", actualRank != null ? actualRank : null);
+        } else {
+            result.put("referralCount", 0);
+            result.put("rank", null);
+        }
+        return result;
+    }
+
+    /**
+     * 计算指定周所有用户的排名并更新rank字段
+     */
+    public void calculateWeeklyRanks(String weekStart) {
+        String sql = "UPDATE referral_leaderboard r " +
+                "INNER JOIN (" +
+                "  SELECT id, @rn := @rn + 1 AS pos " +
+                "  FROM referral_leaderboard, (SELECT @rn := 0) vars " +
+                "  WHERE week_start = ? ORDER BY referral_count DESC" +
+                ") ranked ON r.id = ranked.id " +
+                "SET r.rank = ranked.pos";
+        try {
+            jdbcTemplate.update(sql, weekStart);
+            log.info("周排行榜排名计算完成: weekStart={}", weekStart);
+        } catch (Exception e) {
+            log.error("周排行榜排名计算失败: weekStart={}", weekStart, e);
+        }
+    }
+}

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -48,6 +48,9 @@ public class TaskService implements TaskServiceInterface {
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private GrowthTaskService growthTaskService;
+
     private static final int EARLY_BONUS = 1; // 提前完成奖励
     private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
     private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
@@ -407,6 +410,8 @@ pointsLog.setExpireAt(expireCal.getTime());
                     childId, taskId, e.getMessage());
         }
 
+        try { growthTaskService.updateProgress(child.getUserId(), "DAILY_TASK", 1); } catch (Exception e) { log.warn("成长任务进度更新失败: childId={}, error={}", childId, e.getMessage()); }
+
         Map<String, Object> result = new HashMap<>();
         result.put("pointsEarned", pointsEarned);
 result.put("newBalance", child.getTotalPoints());

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/api/MembershipServiceInterface.java

@@ -11,7 +11,7 @@ public interface MembershipServiceInterface {
     FamilyMembershipDTO getFamilyMembership(Long familyId);
     MembershipLevelDTO getCurrentLevel(Long familyId);
     boolean hasFeature(Long familyId, String feature);
-    PaymentOrderDTO createOrder(Long familyId, String levelCode, String paymentType);
+    PaymentOrderDTO createOrder(Long userId, Long familyId, String levelCode, String paymentType, Long userCouponId);
     boolean processPaymentCallback(String orderNo, String transactionId, String payMethod);
     boolean canUseFeature(Long familyId, String feature);
 

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/api/PackagePaymentServiceInterface.java

@@ -5,7 +5,7 @@ import com.etotem.cfc.entity.PackageOrder;
 import java.util.Map;
 
 public interface PackagePaymentServiceInterface {
-    PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod);
+    PackageOrder createOrder(Long userId, Long familyId, Long packageId, String payMethod, Long userCouponId);
     Map<String, Object> createWechatPayOrder(Long orderId, String openid);
     boolean handlePayNotify(String xmlData);
     PackageOrder getOrderById(Long orderId);

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/LeaderboardWeeklyResetTask.java

@@ -0,0 +1,45 @@
+package com.etotem.cfc.task;
+
+import com.etotem.cfc.service.ReferralLeaderboardService;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAdjusters;
+
+/**
+ * 排行榜周排名计算定时任务
+ * 每周一凌晨00:05执行,计算上周所有用户的最终排名
+ */
+@Slf4j
+@Component
+public class LeaderboardWeeklyResetTask {
+
+    @Resource
+    private ReferralLeaderboardService referralLeaderboardService;
+
+    /**
+     * 每周一凌晨00:05执行(Asia/Shanghai时区)
+     * 计算上周排行榜的最终排名
+     */
+    @Scheduled(cron = "0 5 0 * * MON")
+    public void calculateWeeklyRanks() {
+        log.info("开始执行排行榜周排名计算定时任务");
+        try {
+            // 获取上周的周一日期
+            LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
+            LocalDate lastMonday = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
+            String lastWeekStart = lastMonday.format(DateTimeFormatter.ISO_LOCAL_DATE);
+
+            referralLeaderboardService.calculateWeeklyRanks(lastWeekStart);
+            log.info("排行榜周排名计算定时任务执行完成: lastWeekStart={}", lastWeekStart);
+        } catch (Exception e) {
+            log.error("排行榜周排名计算定时任务执行失败", e);
+        }
+    }
+}

+ 106 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1604,3 +1604,109 @@ CREATE TABLE IF NOT EXISTS onboarding_task (
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     INDEX idx_user_id (user_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- =============================================
+-- 成长任务配置表
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS growth_task (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    type VARCHAR(16) NOT NULL COMMENT 'DAILY/NEWBIE',
+    title VARCHAR(100) NOT NULL,
+    description VARCHAR(255),
+    reward_points INT DEFAULT 0,
+    reward_energy INT DEFAULT 0,
+    target_value INT DEFAULT 1,
+    task_key VARCHAR(32) UNIQUE NOT NULL,
+    enabled TINYINT DEFAULT 1,
+    sort_order INT DEFAULT 0,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_type (type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长任务配置表';
+
+CREATE TABLE IF NOT EXISTS growth_task_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL,
+    task_id BIGINT NOT NULL,
+    progress INT DEFAULT 0,
+    completed TINYINT DEFAULT 0,
+    claimed TINYINT DEFAULT 0,
+    date VARCHAR(10) COMMENT 'YYYY-MM-DD for daily, empty for newbie',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_user_date (user_id, date),
+    INDEX idx_task_id (task_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长任务用户进度表';
+
+-- =============================================
+-- 优惠券配置表
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS coupon (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(64) NOT NULL,
+    type VARCHAR(16) DEFAULT 'FIXED',
+    value INT DEFAULT 0 COMMENT '金额 单位:分',
+    min_spend INT DEFAULT 0 COMMENT '最低消费 单位:分',
+    applicable_to VARCHAR(32) DEFAULT 'ALL',
+    valid_from DATETIME,
+    valid_until DATETIME,
+    total_count INT DEFAULT 0,
+    used_count INT DEFAULT 0,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_valid_period (valid_from, valid_until)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券配置表';
+
+CREATE TABLE IF NOT EXISTS user_coupon (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL,
+    coupon_id BIGINT NOT NULL,
+    status VARCHAR(16) DEFAULT 'AVAILABLE',
+    received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    used_at DATETIME,
+    order_id BIGINT,
+    INDEX idx_user_coupon (user_id, coupon_id),
+    INDEX idx_status (status),
+    INDEX idx_order_id (order_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠券表';
+
+-- =============================================
+-- 邀请里程碑表
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS invite_milestone (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL,
+    milestone VARCHAR(32) NOT NULL,
+    reward_points INT DEFAULT 0,
+    reward_energy INT DEFAULT 0,
+    status VARCHAR(16) DEFAULT 'PENDING',
+    achieved_at DATETIME,
+    claimed_at DATETIME,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_user_milestone (user_id, milestone)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请里程碑表';
+
+-- =============================================
+-- 推广排行榜周记录表
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS referral_leaderboard (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL,
+    week_start VARCHAR(10) NOT NULL COMMENT '周一日期 YYYY-MM-DD',
+    referral_count INT DEFAULT 0,
+    `rank` INT,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE INDEX idx_user_week (user_id, week_start),
+    INDEX idx_week_rank (week_start, referral_count DESC)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广排行榜周记录';
+
+-- =============================================
+-- 成长任务种子数据
+-- =============================================
+
+INSERT IGNORE INTO growth_task (type, title, description, reward_points, reward_energy, target_value, task_key, sort_order) VALUES
+('DAILY', '每日签到', '完成每日签到', 20, 3, 1, 'DAILY_CHECKIN', 1),
+('DAILY', '完成任务', '完成一个任务', 30, 5, 1, 'DAILY_TASK', 2),
+('DAILY', '分享文章', '分享一篇好文给好友', 10, 2, 1, 'DAILY_SHARE', 3),
+('DAILY', 'AI对话', '与AI助手进行一次对话', 15, 3, 1, 'DAILY_AI', 4);

+ 20 - 0
cfc-frontend/pages.json

@@ -234,6 +234,18 @@
         "navigationBarTitleText": "家庭成员"
       }
     },
+    {
+      "path": "pages/profile/onboarding",
+      "style": {
+        "navigationBarTitleText": "新手任务"
+      }
+    },
+    {
+      "path": "pages/profile/coupons",
+      "style": {
+        "navigationBarTitleText": "我的优惠券"
+      }
+    },
     {
       "path": "pages/family/add-member",
       "style": {
@@ -405,6 +417,10 @@
         {
           "path": "review",
           "style": { "navigationBarTitleText": "审核任务" }
+        },
+        {
+          "path": "daily-tasks",
+          "style": { "navigationBarTitleText": "每日任务" }
         }
       ]
     },
@@ -623,6 +639,10 @@
         {
           "path": "team",
           "style": { "navigationBarTitleText": "推广团队" }
+        },
+        {
+          "path": "leaderboard",
+          "style": { "navigationBarTitleText": "推广排行榜", "enablePullDownRefresh": true }
         }
       ]
     },

+ 258 - 2
cfc-frontend/pages/membership/upgrade.vue

@@ -41,6 +41,13 @@
           <text class="feature-text">{{ feature }}</text>
         </view>
       </view>
+      <!-- Coupon section -->
+      <view class="coupon-row" @click="openCouponPicker">
+        <text class="coupon-row-label">优惠券</text>
+        <text v-if="selectedCoupon" class="coupon-row-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }} {{ selectedCoupon.name }}</text>
+        <text v-else class="coupon-row-placeholder">选择优惠券</text>
+        <text class="coupon-row-arrow">›</text>
+      </view>
       <button class="btn-upgrade" @click="handleUpgrade">立即开通</button>
     </view>
 
@@ -55,6 +62,13 @@
         <text class="price-amount">365</text>
         <text class="price-unit">/年</text>
       </view>
+      <!-- Coupon section -->
+      <view class="coupon-row" @click="openCouponPicker">
+        <text class="coupon-row-label">优惠券</text>
+        <text v-if="selectedCoupon" class="coupon-row-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }} {{ selectedCoupon.name }}</text>
+        <text v-else class="coupon-row-placeholder">选择优惠券</text>
+        <text class="coupon-row-arrow">›</text>
+      </view>
       <button class="btn-upgrade btn-renew" @click="handleRenew">立即续费</button>
     </view>
 
@@ -87,10 +101,46 @@
       </view>
     </view>
   </scroll-view>
+
+  <!-- Coupon Picker Bottom Sheet -->
+  <view class="modal-mask" v-if="showCouponPicker" @click="showCouponPicker = false">
+    <view class="coupon-picker" @click.stop>
+      <view class="picker-header">
+        <text class="picker-title">选择优惠券</text>
+        <text class="picker-close" @click="showCouponPicker = false">关闭</text>
+      </view>
+      <scroll-view scroll-y class="picker-list">
+        <view
+          v-for="coupon in availableCoupons"
+          :key="coupon.id"
+          class="picker-item"
+          :class="selectedCoupon && selectedCoupon.id === coupon.id ? 'picker-item-active' : ''"
+          @click="selectCoupon(coupon)"
+        >
+          <view class="picker-item-left">
+            <text class="picker-item-value">{{ formatPriceWithSymbol(coupon.value) }}</text>
+          </view>
+          <view class="picker-item-right">
+            <text class="picker-item-name">{{ coupon.name }}</text>
+            <text class="picker-item-condition">{{ getCouponCondition(coupon.minSpend) }}</text>
+          </view>
+          <view class="picker-item-check" v-if="selectedCoupon && selectedCoupon.id === coupon.id">
+            <text>✓</text>
+          </view>
+        </view>
+        <view class="picker-empty" v-if="availableCoupons.length === 0">
+          <text>暂无可用优惠券</text>
+        </view>
+      </scroll-view>
+      <view class="picker-footer" v-if="selectedCoupon">
+        <button class="picker-btn-remove" @click="removeCoupon">不使用优惠券</button>
+      </view>
+    </view>
+  </view>
 </template>
 
 <script>
-import { getMyMembership, getMembershipLevels, getUpgradeRecords, upgradeMember, createOrder } from '../../utils/api.js'
+import { getMyMembership, getMembershipLevels, getUpgradeRecords, upgradeMember, createOrder, getCouponList } from '../../utils/api.js'
 
 export default {
   data() {
@@ -108,7 +158,20 @@ export default {
         { name: '专属活动', free: '部分参与', family: '全部参与' },
         { name: '成长报告', free: '月度', family: '每周+深度' },
         { name: '任务配额', free: '每日5个', family: '不限量' }
-      ]
+      ],
+      coupons: [],
+      selectedCoupon: null,
+      showCouponPicker: false
+    }
+  },
+  computed: {
+    availableCoupons() {
+      var self = this
+      return this.coupons.filter(function(c) {
+        if (c.status && c.status !== 'AVAILABLE') return false
+        if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'MEMBERSHIP') return false
+        return true
+      })
     }
   },
   onLoad() {
@@ -143,6 +206,31 @@ export default {
       } catch (e) {
         console.error('加载升级记录失败', e)
       }
+      try {
+        const couponsRes = await getCouponList()
+        this.coupons = couponsRes.data || []
+      } catch (e) {
+        console.error('加载优惠券失败', e)
+      }
+    },
+    openCouponPicker() {
+      if (this.availableCoupons.length === 0 && !this.selectedCoupon) {
+        uni.showToast({ title: '暂无可用优惠券', icon: 'none' })
+        return
+      }
+      this.showCouponPicker = true
+    },
+    selectCoupon(coupon) {
+      this.selectedCoupon = coupon
+      this.showCouponPicker = false
+    },
+    removeCoupon() {
+      this.selectedCoupon = null
+      this.showCouponPicker = false
+    },
+    getCouponCondition(minSpend) {
+      if (!minSpend || minSpend <= 0) return '无门槛'
+      return '满' + (minSpend / 100).toFixed(2) + '元可用'
     },
     handleUpgrade() {
       uni.showModal({
@@ -458,4 +546,172 @@ export default {
   font-weight: bold;
   color: #F97316;
 }
+
+/* ===== Coupon Row ===== */
+.coupon-row {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0;
+  border-top: 1rpx solid #f5f5f5;
+  margin-top: 16rpx;
+}
+
+.coupon-row:active {
+  opacity: 0.7;
+}
+
+.coupon-row-label {
+  font-size: 26rpx;
+  color: #666;
+  margin-right: 16rpx;
+}
+
+.coupon-row-discount {
+  flex: 1;
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: 600;
+  text-align: right;
+}
+
+.coupon-row-placeholder {
+  flex: 1;
+  font-size: 26rpx;
+  color: #ccc;
+  text-align: right;
+}
+
+.coupon-row-arrow {
+  font-size: 32rpx;
+  color: #ccc;
+  margin-left: 8rpx;
+}
+
+/* ===== Coupon Picker Modal ===== */
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.5);
+  z-index: 999;
+  display: flex;
+  align-items: flex-end;
+}
+
+.coupon-picker {
+  background: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  width: 100%;
+  max-height: 70vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.picker-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.picker-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.picker-close {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.picker-list {
+  max-height: 50vh;
+  padding: 0 20rpx;
+}
+
+.picker-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 16rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+
+.picker-item:active {
+  background: #fafafa;
+}
+
+.picker-item-active {
+  background: #FFF7ED;
+}
+
+.picker-item-left {
+  width: 120rpx;
+  text-align: center;
+  flex-shrink: 0;
+}
+
+.picker-item-value {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.picker-item-right {
+  flex: 1;
+  margin: 0 16rpx;
+}
+
+.picker-item-name {
+  font-size: 26rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.picker-item-condition {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.picker-item-check {
+  width: 40rpx;
+  height: 40rpx;
+  background: #F97316;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: bold;
+}
+
+.picker-empty {
+  text-align: center;
+  padding: 60rpx 0;
+  font-size: 26rpx;
+  color: #999;
+}
+
+.picker-footer {
+  padding: 20rpx;
+  border-top: 1rpx solid #eee;
+}
+
+.picker-btn-remove {
+  background: #f5f5f5;
+  color: #666;
+  font-size: 26rpx;
+  border-radius: 12rpx;
+  border: none;
+}
+
+.picker-btn-remove::after {
+  border: none;
+}
 </style>

+ 21 - 0
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -16,6 +16,14 @@
           <text>👨‍👩‍👧‍👦 家庭成员</text>
           <text class="arrow">›</text>
         </view>
+        <view class="menu-item" @click="goToDailyTasks">
+          <text>📋 每日任务</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" v-if="role === 'parent'" @click="goToOnboarding">
+          <text>🎯 新手任务</text>
+          <text class="arrow">›</text>
+        </view>
       </view>
 
       <!-- ===== 财富 ===== -->
@@ -29,6 +37,10 @@
           <text>📊 积分记录</text>
           <text class="arrow">›</text>
         </view>
+        <view class="menu-item" @click="goToCoupons">
+          <text>🎫 我的优惠券</text>
+          <text class="arrow">›</text>
+        </view>
       </view>
 
       <!-- ===== 服务 ===== -->
@@ -141,6 +153,12 @@ export default {
     goToFamilyMembers() {
       uni.navigateTo({ url: '/pages/profile/family-members' })
     },
+    goToOnboarding() {
+      uni.navigateTo({ url: '/pages/profile/onboarding' })
+    },
+    goToDailyTasks() {
+      uni.navigateTo({ url: '/pages/tasks/daily-tasks' })
+    },
     goToPointsLogs() {
       uni.navigateTo({ url: '/pages/points/points' })
     },
@@ -167,6 +185,9 @@ export default {
     goToPromotion() {
       uni.navigateTo({ url: '/pages/promotion/index' })
     },
+    goToCoupons() {
+      uni.navigateTo({ url: '/pages/profile/coupons' })
+    },
     showInviteActionSheet() {
       const familyId = uni.getStorageSync('familyId')
       const userInfo = uni.getStorageSync('userInfo')

+ 386 - 0
cfc-frontend/pages/profile/coupons.vue

@@ -0,0 +1,386 @@
+<template>
+  <view class="page">
+    <view class="tab-bar">
+      <view
+        v-for="(tab, idx) in tabs"
+        :key="idx"
+        class="tab-item"
+        :class="tabIndex === idx ? 'active' : ''"
+        @click="switchTab(idx)"
+      >
+        <text>{{ tab.name }}</text>
+      </view>
+    </view>
+
+    <scroll-view
+      scroll-y
+      class="coupon-scroll"
+      @scrolltolower="onScrollToLower"
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
+    >
+      <view v-if="loading" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="filteredList.length === 0" class="empty-state">
+        <text class="empty-icon">🎫</text>
+        <text class="empty-text">{{ tabIndex === 0 ? '暂无可用优惠券' : tabIndex === 1 ? '暂无已使用优惠券' : '暂无已过期优惠券' }}</text>
+        <text class="empty-hint" v-if="tabIndex === 0">去领券中心看看有哪些优惠吧</text>
+      </view>
+      <view v-else class="coupon-list">
+        <view
+          v-for="coupon in filteredList"
+          :key="coupon.id"
+          class="coupon-card"
+          :class="getStatusClass(coupon)"
+        >
+          <view class="card-left">
+            <text class="card-value">{{ formatPriceWithSymbol(coupon.value) }}</text>
+            <text class="card-type-label">{{ getTypeLabel(coupon.type) }}</text>
+          </view>
+          <view class="card-right">
+            <view class="card-header">
+              <text class="card-name">{{ coupon.name }}</text>
+              <text v-if="getStatusLabel(coupon)" class="card-status-badge" :class="getStatusClass(coupon)">{{ getStatusLabel(coupon) }}</text>
+            </view>
+            <text class="card-condition">{{ getConditionText(coupon.minSpend) }}</text>
+            <text class="card-scope">{{ getScopeText(coupon.applicableTo) }}</text>
+            <text class="card-expiry">有效期至 {{ formatDate(coupon.validUntil) }}</text>
+            <button
+              v-if="canClaim(coupon)"
+              class="claim-btn"
+              @click="handleClaim(coupon)"
+            >立即领取</button>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getCouponList, claimCoupon } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      tabs: [
+        { name: '可使用', status: 'AVAILABLE' },
+        { name: '已使用', status: 'USED' },
+        { name: '已过期', status: 'EXPIRED' }
+      ],
+      tabIndex: 0,
+      coupons: [],
+      loading: false,
+      refreshing: false
+    }
+  },
+  computed: {
+    filteredList() {
+      var self = this
+      var currentStatus = this.tabs[this.tabIndex].status
+      if (currentStatus === 'AVAILABLE') {
+        return this.coupons.filter(function(c) {
+          return c.status === 'AVAILABLE' || !c.status
+        })
+      }
+      return this.coupons.filter(function(c) {
+        return c.status === currentStatus
+      })
+    }
+  },
+  onShow() {
+    this.loadCoupons()
+  },
+  onPullDownRefresh() {
+    this.refreshing = true
+    this.loadCoupons()
+  },
+  methods: {
+    async loadCoupons() {
+      this.loading = true
+      try {
+        var res = await getCouponList()
+        this.coupons = res.data || []
+      } catch (e) {
+        console.error('加载优惠券失败', e)
+      } finally {
+        this.loading = false
+        this.refreshing = false
+        uni.stopPullDownRefresh()
+      }
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.loadCoupons()
+    },
+    onScrollToLower() {},
+    switchTab(idx) {
+      this.tabIndex = idx
+    },
+    canClaim(coupon) {
+      return this.tabIndex === 0 && (!coupon.status || coupon.status === 'AVAILABLE')
+    },
+    async handleClaim(coupon) {
+      try {
+        var res = await claimCoupon(coupon.id)
+        uni.showToast({ title: res.message || '领取成功', icon: 'success' })
+        this.loadCoupons()
+      } catch (e) {
+        uni.showToast({ title: e.message || '领取失败', icon: 'none' })
+      }
+    },
+    getConditionText(minSpend) {
+      if (!minSpend || minSpend <= 0) return '无门槛'
+      return '满' + (minSpend / 100).toFixed(2) + '元可用'
+    },
+    getScopeText(applicableTo) {
+      if (applicableTo === 'ALL') return '全品类适用'
+      if (applicableTo === 'MEMBERSHIP') return '仅限会员'
+      return applicableTo || '全品类适用'
+    },
+    getTypeLabel(type) {
+      if (type === 'FIXED') return '固定金额'
+      if (type === 'PERCENT') return '折扣券'
+      return type || ''
+    },
+    getStatusLabel(coupon) {
+      if (coupon.status === 'USED') return '已使用'
+      if (coupon.status === 'EXPIRED') return '已过期'
+      return ''
+    },
+    getStatusClass(coupon) {
+      if (coupon.status === 'USED') return 'status-used'
+      if (coupon.status === 'EXPIRED') return 'status-expired'
+      return ''
+    },
+    formatDate(date) {
+      if (!date) return ''
+      var d = new Date(date)
+      var y = d.getFullYear()
+      var m = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      return y + '.' + m + '.' + day
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+.tab-bar {
+  display: flex;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+
+.tab-item {
+  flex: 1;
+  text-align: center;
+  padding: 24rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  position: relative;
+}
+
+.tab-item.active {
+  color: #F97316;
+  font-weight: 600;
+}
+
+.tab-item.active::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 40rpx;
+  height: 4rpx;
+  background: #F97316;
+  border-radius: 2rpx;
+}
+
+.coupon-scroll {
+  height: calc(100vh - 88rpx);
+}
+
+.loading-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.empty-hint {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 12rpx;
+}
+
+.coupon-list {
+  padding: 20rpx;
+}
+
+.coupon-card {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+
+.coupon-card.status-used {
+  opacity: 0.65;
+}
+
+.coupon-card.status-expired {
+  opacity: 0.5;
+}
+
+.card-left {
+  width: 200rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  padding: 30rpx 12rpx;
+  flex-shrink: 0;
+}
+
+.status-used .card-left {
+  background: #b0b0b0;
+}
+
+.status-expired .card-left {
+  background: #d0d0d0;
+}
+
+.card-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.card-type-label {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.8);
+  margin-top: 8rpx;
+}
+
+.card-right {
+  flex: 1;
+  padding: 24rpx 24rpx 20rpx;
+  position: relative;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10rpx;
+}
+
+.card-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-status-badge {
+  font-size: 20rpx;
+  padding: 4rpx 14rpx;
+  border-radius: 20rpx;
+  flex-shrink: 0;
+  margin-left: 12rpx;
+}
+
+.card-status-badge.status-used {
+  background: #f0f0f0;
+  color: #999;
+}
+
+.card-status-badge.status-expired {
+  background: #fff1f0;
+  color: #ff4d4f;
+}
+
+.card-condition {
+  font-size: 24rpx;
+  color: #F97316;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.card-scope {
+  font-size: 22rpx;
+  color: #5B9BD5;
+  background: #f0f7ff;
+  display: inline-block;
+  padding: 2rpx 12rpx;
+  border-radius: 8rpx;
+  margin-bottom: 6rpx;
+}
+
+.card-expiry {
+  font-size: 22rpx;
+  color: #bbb;
+  display: block;
+}
+
+.claim-btn {
+  position: absolute;
+  right: 24rpx;
+  bottom: 20rpx;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 24rpx;
+  padding: 10rpx 28rpx;
+  border-radius: 28rpx;
+  border: none;
+  line-height: 1.4;
+  height: auto;
+}
+
+.claim-btn::after {
+  border: none;
+}
+
+.claim-btn:active {
+  opacity: 0.85;
+}
+</style>

+ 280 - 0
cfc-frontend/pages/profile/onboarding.vue

@@ -0,0 +1,280 @@
+<template>
+  <view class="container">
+    <!-- 进度统计 -->
+    <view class="progress-card">
+      <text class="progress-title">新手任务</text>
+      <view class="progress-bar-wrap">
+        <view class="progress-bar">
+          <view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
+        </view>
+        <text class="progress-text">{{ completedCount }}/6 已完成</text>
+      </view>
+      <text class="progress-hint">完成所有任务,领取丰厚奖励!</text>
+    </view>
+
+    <!-- 任务列表 -->
+    <view class="task-list">
+      <view class="task-item" v-for="item in taskList" :key="item.taskType">
+        <view class="task-left">
+          <view class="task-icon">{{ item.icon }}</view>
+          <view class="task-info">
+            <text class="task-name">{{ item.name }}</text>
+            <text class="task-reward">获得 {{ item.rewardPoints }} 积分 + {{ item.rewardEnergy }} 能量</text>
+          </view>
+        </view>
+        <view class="task-right">
+          <text class="task-status" v-if="item.status === 'CLAIMED'" :class="'status-claimed'">已领取</text>
+          <text class="task-status" v-else-if="item.status === 'COMPLETED'" :class="'status-completed'">已完成</text>
+          <text class="task-status" v-else :class="'status-pending'">待完成</text>
+          <button
+            v-if="item.status === 'COMPLETED'"
+            class="claim-btn"
+            @click="claimReward(item)"
+            :disabled="item.claiming"
+          >领取</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getOnboardingProgress, claimOnboardingReward } from '../../utils/api.js'
+
+var TASK_META = {
+  REGISTER: { icon: '📝', name: '完成注册' },
+  COMPLETE_PROFILE: { icon: '👤', name: '完善个人资料' },
+  ADD_CHILD: { icon: '👨\u200D👩\u200D👧\u200D👦', name: '添加家庭成员' },
+  FIRST_CHECKIN: { icon: '✅', name: '完成首次健康打卡' },
+  FIRST_TASK: { icon: '🎯', name: '完成首次任务' },
+  BIND_REFERRAL: { icon: '🤝', name: '绑定推荐人' }
+}
+
+export default {
+  data() {
+    return {
+      taskList: [],
+      loading: false
+    }
+  },
+  computed: {
+    completedCount() {
+      var count = 0
+      for (var i = 0; i < this.taskList.length; i++) {
+        if (this.taskList[i].status === 'COMPLETED' || this.taskList[i].status === 'CLAIMED') {
+          count++
+        }
+      }
+      return count
+    },
+    progressPercent() {
+      return Math.round((this.completedCount / 6) * 100)
+    }
+  },
+  onLoad() {
+    this.loadProgress()
+  },
+  onShow() {
+    this.loadProgress()
+  },
+  methods: {
+    async loadProgress() {
+      this.loading = true
+      try {
+        var res = await getOnboardingProgress()
+        var records = (res.data && res.data.records) || res.data || []
+        if (!Array.isArray(records)) {
+          records = []
+        }
+        this.taskList = records.map(function(item) {
+          var meta = TASK_META[item.taskType] || { icon: '📋', name: item.taskType }
+          return {
+            taskType: item.taskType,
+            icon: meta.icon,
+            name: meta.name,
+            rewardPoints: item.rewardPoints || 0,
+            rewardEnergy: item.rewardEnergy || 0,
+            status: item.status || 'PENDING',
+            claiming: false
+          }
+        })
+      } catch (e) {
+        console.error('获取新手任务进度失败', e)
+      }
+      this.loading = false
+    },
+    async claimReward(item) {
+      if (item.claiming) return
+      item.claiming = true
+      try {
+        await claimOnboardingReward(item.taskType)
+        item.status = 'CLAIMED'
+        uni.showToast({
+          title: '获得' + item.rewardPoints + '积分+' + item.rewardEnergy + '能量',
+          icon: 'none',
+          duration: 2000
+        })
+      } catch (e) {
+        uni.showToast({ title: '领取失败', icon: 'none' })
+      }
+      item.claiming = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+/* ===== 进度卡片 ===== */
+.progress-card {
+  background: linear-gradient(135deg, #F97316, #EA580C);
+  border-radius: 20rpx;
+  padding: 40rpx;
+  color: #fff;
+  margin-bottom: 24rpx;
+}
+.progress-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 24rpx;
+}
+.progress-bar-wrap {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.progress-bar {
+  flex: 1;
+  height: 16rpx;
+  background: rgba(255,255,255,0.3);
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: #fff;
+  border-radius: 8rpx;
+  transition: width 0.3s ease;
+}
+.progress-text {
+  font-size: 24rpx;
+  margin-left: 16rpx;
+  white-space: nowrap;
+}
+.progress-hint {
+  font-size: 22rpx;
+  opacity: 0.85;
+  display: block;
+}
+
+/* ===== 任务列表 ===== */
+.task-list {
+  background: #fff;
+  border-radius: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.task-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28rpx 30rpx;
+  border-bottom: 1rpx solid #FED7AA;
+}
+.task-item:last-child {
+  border-bottom: none;
+}
+.task-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  margin-right: 20rpx;
+}
+.task-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 16rpx;
+  background: #FEF3C7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.task-info {
+  flex: 1;
+}
+.task-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.task-reward {
+  font-size: 22rpx;
+  color: #F97316;
+  display: block;
+}
+.task-right {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+.task-status {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  margin-right: 12rpx;
+}
+.status-pending {
+  background: #F1F5F9;
+  color: #94A3B8;
+}
+.status-completed {
+  background: #DCFCE7;
+  color: #16A34A;
+}
+.status-claimed {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.claim-btn {
+  background: #F97316;
+  color: #fff;
+  font-size: 24rpx;
+  border-radius: 30rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  padding: 0 28rpx;
+  font-weight: 500;
+}
+.claim-btn::after {
+  border: none;
+}
+.claim-btn:active {
+  opacity: 0.8;
+}
+.claim-btn[disabled] {
+  opacity: 0.5;
+}
+
+.loading-state {
+  text-align: center;
+  padding: 60rpx;
+  color: #94A3B8;
+  font-size: 28rpx;
+}
+</style>

+ 10 - 0
cfc-frontend/pages/promotion/index.vue

@@ -65,6 +65,12 @@
         </view>
         <text class="menu-text">推广团队</text>
       </view>
+      <view class="menu-item" @click="goLeaderboard">
+        <view class="menu-icon-wrap icon-leaderboard">
+          <text class="menu-icon-text">🏆</text>
+        </view>
+        <text class="menu-text">推广排行榜</text>
+      </view>
     </view>
 
     <SharePoster :show="showPoster" :referralCode="referralCode" :qrCodeBase64="qrCodeBase64" :nickname="userNickname" :userAvatar="userAvatar" @close="showPoster = false" />
@@ -174,6 +180,9 @@ export default {
     },
     goTeam() {
       uni.navigateTo({ url: '/pages/promotion/team' })
+    },
+    goLeaderboard() {
+      uni.navigateTo({ url: '/pages/promotion/leaderboard' })
     }
   }
 }
@@ -306,6 +315,7 @@ export default {
 .icon-withdraw { background: #DBEAFE; }
 .icon-invite { background: #D1FAE5; }
 .icon-team { background: #FCE7F3; }
+.icon-leaderboard { background: #FFF3CD; }
 .menu-icon-text {
   font-size: 36rpx;
 }

+ 149 - 2
cfc-frontend/pages/promotion/invite.vue

@@ -21,6 +21,29 @@
       </view>
     </view>
 
+    <!-- 邀请里程碑 -->
+    <view class="milestone-card" v-if="milestones.length > 0">
+      <text class="milestone-title">邀请里程碑</text>
+      <view class="milestone-item" v-for="item in milestones" :key="item.id || item.milestone">
+        <view class="milestone-header">
+          <text class="milestone-name">{{ milestoneName(item.milestone) }}</text>
+          <text class="milestone-reward">+{{ item.rewardPoints || 0 }}积分</text>
+        </view>
+        <view class="milestone-bar-wrap">
+          <view class="milestone-bar-bg">
+            <view class="milestone-bar-fill" :style="'width:' + milestonePercent(item) + '%'"></view>
+          </view>
+          <text class="milestone-count">{{ item.currentCount || 0 }}/{{ item.targetCount || 0 }}</text>
+        </view>
+        <view class="milestone-action" v-if="item.status === 'PENDING'">
+          <button class="claim-btn" @click="claimReward(item)">领取</button>
+        </view>
+        <view class="milestone-action" v-if="item.status === 'CLAIMED'">
+          <text class="claimed-badge">✓ 已领取</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 邀请码展示 -->
     <view class="code-card">
       <text class="code-title">我的邀请码</text>
@@ -66,7 +89,7 @@
 </template>
 
 <script>
-import { getReferralCode, bindReferral, getReferralList, getReferralSummary, getInviteQrCode } from '../../utils/api.js'
+import { getReferralCode, bindReferral, getReferralList, getReferralSummary, getInviteQrCode, getInviteMilestones, claimInviteMilestone } from '../../utils/api.js'
 import SharePoster from '@/components/SharePoster.vue'
 
 export default {
@@ -84,13 +107,16 @@ export default {
       showPoster: false,
       qrCodeBase64: '',
       userAvatar: '',
-      userNickname: ''
+      userNickname: '',
+      milestones: [],
+      milestonesLoading: false
     }
   },
   onLoad() {
     this.loadCode()
     this.loadSummary()
     this.loadList()
+    this.loadMilestones()
   },
   onReachBottom() {
     if (this.hasMore && !this.loading) {
@@ -193,6 +219,43 @@ export default {
         uni.showToast({ title: e.message || '绑定失败', icon: 'none' })
       }
     },
+    async loadMilestones() {
+      this.milestonesLoading = true
+      try {
+        var res = await getInviteMilestones()
+        if (res.data) {
+          this.milestones = Array.isArray(res.data) ? res.data : []
+        }
+      } catch (e) {
+        console.error('获取里程碑失败', e)
+      }
+      this.milestonesLoading = false
+    },
+    async claimReward(item) {
+      try {
+        uni.showLoading({ title: '领取中...' })
+        await claimInviteMilestone(item.milestone)
+        uni.showToast({ title: '领取成功', icon: 'success' })
+        this.loadMilestones()
+      } catch (e) {
+        uni.showToast({ title: e.message || '领取失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    },
+    milestoneName(key) {
+      var names = {
+        INVITE_3: '邀请3人',
+        INVITE_5: '邀请5人',
+        INVITE_10: '邀请10人'
+      }
+      return names[key] || key
+    },
+    milestonePercent(item) {
+      if (!item.targetCount) return 0
+      var pct = Math.round((item.currentCount || 0) / item.targetCount * 100)
+      return Math.min(pct, 100)
+    },
     formatTime(time) {
       if (!time) return ''
       var t = new Date(time)
@@ -241,6 +304,90 @@ export default {
   background: #FED7AA;
 }
 
+/* ===== 邀请里程碑 ===== */
+.milestone-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.milestone-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 24rpx;
+}
+.milestone-item {
+  margin-bottom: 24rpx;
+}
+.milestone-item:last-child {
+  margin-bottom: 0;
+}
+.milestone-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.milestone-name {
+  font-size: 26rpx;
+  color: #1E293B;
+  font-weight: 500;
+}
+.milestone-reward {
+  font-size: 22rpx;
+  color: #F97316;
+}
+.milestone-bar-wrap {
+  display: flex;
+  align-items: center;
+}
+.milestone-bar-bg {
+  flex: 1;
+  height: 20rpx;
+  background: #FED7AA;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+.milestone-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F97316, #EA580C);
+  border-radius: 10rpx;
+}
+.milestone-count {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-left: 12rpx;
+  min-width: 50rpx;
+  text-align: right;
+}
+.milestone-action {
+  margin-top: 12rpx;
+  text-align: right;
+}
+.claim-btn {
+  background: #F97316;
+  color: #fff;
+  font-size: 24rpx;
+  border-radius: 30rpx;
+  height: 52rpx;
+  line-height: 52rpx;
+  padding: 0 28rpx;
+  display: inline-block;
+}
+.claim-btn::after {
+  border: none;
+}
+.claim-btn:active {
+  opacity: 0.9;
+}
+.claimed-badge {
+  font-size: 24rpx;
+  color: #10B981;
+}
+
 .code-card {
   background: linear-gradient(135deg, #F97316, #EA580C);
   border-radius: 20rpx;

+ 429 - 0
cfc-frontend/pages/promotion/leaderboard.vue

@@ -0,0 +1,429 @@
+<template>
+  <view class="container">
+    <!-- 标题区域 -->
+    <view class="header">
+      <text class="header-title">推广排行榜</text>
+      <text class="header-sub">本周</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && topList.length === 0">
+      <text>加载中...</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!loading && topList.length === 0">
+      <text class="empty-icon">🏆</text>
+      <text class="empty-text">本周还没有推广达人</text>
+    </view>
+
+    <!-- Top 3 领奖台 -->
+    <view class="podium-section" v-if="topList.length > 0">
+      <!-- #2 左侧 -->
+      <view class="podium-item podium-silver" v-if="topList.length >= 2">
+        <view class="podium-rank-badge silver-badge">2</view>
+        <view class="podium-avatar-wrap silver-border">
+          <image v-if="topList[1] && topList[1].avatarUrl" class="podium-avatar" :src="topList[1].avatarUrl" mode="aspectFill"></image>
+          <text v-else class="podium-avatar-icon">👤</text>
+        </view>
+        <text class="podium-name">{{ topList[1] && topList[1].nickname ? topList[1].nickname : '微信用户' }}</text>
+        <text class="podium-count">{{ topList[1] && topList[1].referralCount ? topList[1].referralCount : 0 }}人</text>
+      </view>
+
+      <!-- #1 中间(最大) -->
+      <view class="podium-item podium-gold">
+        <view class="crown-wrap">
+          <text class="crown-icon">👑</text>
+        </view>
+        <view class="podium-rank-badge gold-badge">1</view>
+        <view class="podium-avatar-wrap gold-border gold-size">
+          <image v-if="topList[0] && topList[0].avatarUrl" class="podium-avatar" :src="topList[0].avatarUrl" mode="aspectFill"></image>
+          <text v-else class="podium-avatar-icon">👤</text>
+        </view>
+        <text class="podium-name gold-name">{{ topList[0] && topList[0].nickname ? topList[0].nickname : '微信用户' }}</text>
+        <text class="podium-count gold-count">{{ topList[0] && topList[0].referralCount ? topList[0].referralCount : 0 }}人</text>
+      </view>
+
+      <!-- #3 右侧 -->
+      <view class="podium-item podium-bronze" v-if="topList.length >= 3">
+        <view class="podium-rank-badge bronze-badge">3</view>
+        <view class="podium-avatar-wrap bronze-border">
+          <image v-if="topList[2] && topList[2].avatarUrl" class="podium-avatar" :src="topList[2].avatarUrl" mode="aspectFill"></image>
+          <text v-else class="podium-avatar-icon">👤</text>
+        </view>
+        <text class="podium-name">{{ topList[2] && topList[2].nickname ? topList[2].nickname : '微信用户' }}</text>
+        <text class="podium-count">{{ topList[2] && topList[2].referralCount ? topList[2].referralCount : 0 }}人</text>
+      </view>
+    </view>
+
+    <!-- 排名列表 #4-#20 -->
+    <view class="rank-list" v-if="topList.length > 3">
+      <view class="rank-item" v-for="item in restList" :key="item.userId || item.rank">
+        <view class="rank-num-wrap">
+          <text class="rank-num">{{ item.rank }}</text>
+        </view>
+        <view class="rank-avatar-wrap">
+          <image v-if="item.avatarUrl" class="rank-avatar" :src="item.avatarUrl" mode="aspectFill"></image>
+          <text v-else class="rank-avatar-icon">👤</text>
+        </view>
+        <text class="rank-name">{{ item.nickname || '微信用户' }}</text>
+        <text class="rank-count">{{ item.referralCount || 0 }}人</text>
+      </view>
+    </view>
+
+    <!-- 我的排名(底部吸底) -->
+    <view class="my-rank-card" v-if="!loading">
+      <view class="my-rank-inner" v-if="myRank && myRank.rank">
+        <view class="my-rank-left">
+          <text class="my-rank-label">我的排名</text>
+          <text class="my-rank-pos">#{{ myRank.rank }}</text>
+        </view>
+        <view class="my-rank-right">
+          <text class="my-rank-count">{{ myRank.referralCount || 0 }}人</text>
+        </view>
+      </view>
+      <view class="my-rank-inner" v-else>
+        <view class="my-rank-left">
+          <text class="my-rank-label">我的排名</text>
+          <text class="my-rank-pos unranked">未上榜</text>
+        </view>
+        <view class="my-rank-right">
+          <text class="my-rank-hint">继续加油推广吧</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getLeaderboardTop, getLeaderboardMyRank } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      topList: [],
+      myRank: null,
+      loading: false
+    }
+  },
+  computed: {
+    restList() {
+      if (this.topList.length <= 3) return []
+      return this.topList.slice(3)
+    }
+  },
+  onLoad() {
+    this.loadTop()
+    this.loadMyRank()
+  },
+  onPullDownRefresh() {
+    this.loadTop()
+    this.loadMyRank()
+    uni.stopPullDownRefresh()
+  },
+  methods: {
+    async loadTop() {
+      this.loading = true
+      try {
+        var res = await getLeaderboardTop()
+        if (res.data) {
+          this.topList = res.data || []
+        }
+      } catch (e) {
+        console.error('获取排行榜失败', e)
+        uni.showToast({ title: '获取排行榜失败', icon: 'none' })
+      }
+      this.loading = false
+    },
+    async loadMyRank() {
+      try {
+        var res = await getLeaderboardMyRank()
+        if (res.data) {
+          this.myRank = res.data
+        }
+      } catch (e) {
+        console.error('获取我的排名失败', e)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding-bottom: 160rpx;
+}
+
+/* ===== 标题区域 ===== */
+.header {
+  text-align: center;
+  padding: 20rpx 0 30rpx;
+}
+.header-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #1E293B;
+  display: block;
+}
+.header-sub {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-top: 8rpx;
+  display: block;
+}
+
+/* ===== 领奖台 Top 3 ===== */
+.podium-section {
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  padding: 30rpx 0 40rpx;
+}
+.podium-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 200rpx;
+  position: relative;
+}
+.podium-gold {
+  width: 240rpx;
+}
+.crown-wrap {
+  margin-bottom: 4rpx;
+}
+.crown-icon {
+  font-size: 48rpx;
+}
+.podium-rank-badge {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 12rpx;
+}
+.gold-badge {
+  background: #FFD700;
+}
+.silver-badge {
+  background: #C0C0C0;
+}
+.bronze-badge {
+  background: #CD7F32;
+}
+.podium-avatar-wrap {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  margin-bottom: 12rpx;
+  background: #FEF3C7;
+}
+.gold-size {
+  width: 120rpx;
+  height: 120rpx;
+}
+.gold-border {
+  border: 4rpx solid #FFD700;
+  box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.4);
+}
+.silver-border {
+  border: 4rpx solid #C0C0C0;
+  box-shadow: 0 0 16rpx rgba(192, 192, 192, 0.3);
+}
+.bronze-border {
+  border: 4rpx solid #CD7F32;
+  box-shadow: 0 0 16rpx rgba(205, 127, 50, 0.3);
+}
+.podium-avatar {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+}
+.podium-avatar-icon {
+  font-size: 48rpx;
+}
+.podium-name {
+  font-size: 24rpx;
+  color: #1E293B;
+  font-weight: 500;
+  max-width: 180rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+  display: block;
+}
+.gold-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  max-width: 220rpx;
+}
+.podium-count {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+  display: block;
+}
+.gold-count {
+  color: #F97316;
+  font-weight: bold;
+  font-size: 26rpx;
+}
+
+/* ===== 排名列表 #4-#20 ===== */
+.rank-list {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249, 115, 22, 0.08);
+}
+.rank-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #FED7AA;
+}
+.rank-item:last-child {
+  border-bottom: none;
+}
+.rank-num-wrap {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 50%;
+  background: #FFF7ED;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.rank-num {
+  font-size: 24rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.rank-avatar-wrap {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: #FEF3C7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.rank-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+}
+.rank-avatar-icon {
+  font-size: 32rpx;
+}
+.rank-name {
+  flex: 1;
+  font-size: 28rpx;
+  color: #1E293B;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 0;
+}
+.rank-count {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+}
+
+/* ===== 我的排名(吸底) ===== */
+.my-rank-card {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: rgba(255, 247, 237, 0.95);
+  backdrop-filter: blur(10px);
+  z-index: 100;
+}
+.my-rank-inner {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 32rpx;
+  box-shadow: 0 4rpx 20rpx rgba(249, 115, 22, 0.15);
+  border: 2rpx solid #FED7AA;
+}
+.my-rank-left {
+  display: flex;
+  align-items: center;
+}
+.my-rank-label {
+  font-size: 28rpx;
+  color: #64748B;
+  margin-right: 16rpx;
+}
+.my-rank-pos {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.my-rank-pos.unranked {
+  color: #94A3B8;
+  font-size: 28rpx;
+  font-weight: 500;
+}
+.my-rank-right {
+  text-align: right;
+}
+.my-rank-count {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.my-rank-hint {
+  font-size: 24rpx;
+  color: #94A3B8;
+}
+
+/* ===== 空状态 / 加载状态 ===== */
+.empty-state {
+  text-align: center;
+  padding: 120rpx 0;
+}
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #94A3B8;
+  display: block;
+}
+.loading-state {
+  text-align: center;
+  padding: 120rpx 0;
+  color: #94A3B8;
+  font-size: 28rpx;
+}
+</style>

+ 293 - 2
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -53,6 +53,21 @@
         </view>
       </view>
 
+      <!-- Coupon Section -->
+      <view class="section coupon-section" @click="openCouponPicker">
+        <view class="info-row">
+          <text class="info-label">优惠券</text>
+          <view class="coupon-selected" v-if="selectedCoupon">
+            <text class="coupon-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }}</text>
+            <text class="coupon-name-tag">{{ selectedCoupon.name }}</text>
+          </view>
+          <view class="coupon-placeholder" v-else>
+            <text class="placeholder-text">选择优惠券</text>
+          </view>
+          <text class="address-arrow">›</text>
+        </view>
+      </view>
+
       <!-- Remark Section -->
       <view class="section">
         <view class="info-row">
@@ -67,12 +82,49 @@
       </view>
 
       <view class="bottom-spacer"></view>
+
+      <!-- Coupon Picker Bottom Sheet -->
+      <view class="modal-mask" v-if="showCouponPicker" @click="closeCouponPicker">
+        <view class="coupon-picker" @click.stop>
+          <view class="picker-header">
+            <text class="picker-title">选择优惠券</text>
+            <text class="picker-close" @click="closeCouponPicker">关闭</text>
+          </view>
+          <scroll-view scroll-y class="picker-list">
+            <view
+              v-for="coupon in availableCoupons"
+              :key="coupon.id"
+              class="picker-item"
+              :class="selectedCoupon && selectedCoupon.id === coupon.id ? 'picker-item-active' : ''"
+              @click="selectCoupon(coupon)"
+            >
+              <view class="picker-item-left">
+                <text class="picker-item-value">{{ formatPriceWithSymbol(coupon.value) }}</text>
+              </view>
+              <view class="picker-item-right">
+                <text class="picker-item-name">{{ coupon.name }}</text>
+                <text class="picker-item-condition">{{ getConditionText(coupon.minSpend) }}</text>
+              </view>
+              <view class="picker-item-check" v-if="selectedCoupon && selectedCoupon.id === coupon.id">
+                <text class="check-icon">✓</text>
+              </view>
+            </view>
+            <view class="picker-empty" v-if="availableCoupons.length === 0">
+              <text>暂无可用优惠券</text>
+            </view>
+          </scroll-view>
+          <view class="picker-footer" v-if="selectedCoupon">
+            <button class="picker-btn-remove" @click="removeCoupon">不使用优惠券</button>
+          </view>
+        </view>
+      </view>
     </scroll-view>
 
     <view class="bottom-bar">
       <view class="bottom-left">
         <text class="total-label">合计: </text>
-        <text class="total-amount">{{ formatPriceWithSymbol(actualAmount + freight) }}</text>
+        <text class="total-amount">{{ formatPriceWithSymbol(finalAmount) }}</text>
+        <text v-if="selectedCoupon" class="total-original">{{ formatPriceWithSymbol(actualAmount + freight) }}</text>
       </view>
       <button
         :class="['submit-btn', submitting ? 'disabled' : '']"
@@ -85,6 +137,7 @@
 
 <script>
 import config from '@/config.js'
+import { getCouponList } from '../../../utils/api.js'
 
 export default {
   data() {
@@ -95,7 +148,29 @@ export default {
       remark: '',
       loading: false,
       submitting: false,
-      actualAmount: 0
+      actualAmount: 0,
+      coupons: [],
+      selectedCoupon: null,
+      showCouponPicker: false
+    }
+  },
+  computed: {
+    finalAmount() {
+      var total = (this.actualAmount || 0) + (this.freight || 0)
+      if (this.selectedCoupon) {
+        total = total - this.selectedCoupon.value
+        if (total < 0) total = 0
+      }
+      return total
+    },
+    availableCoupons() {
+      var self = this
+      return this.coupons.filter(function(c) {
+        if (c.status && c.status !== 'AVAILABLE') return false
+        if (c.applicableTo && c.applicableTo !== 'ALL') return false
+        if (c.minSpend && c.minSpend > self.actualAmount) return false
+        return true
+      })
     }
   },
   onLoad() {
@@ -105,6 +180,7 @@ export default {
       this.calcAmount()
     }
     this.loadDefaultAddress()
+    this.loadCoupons()
   },
   methods: {
     calcAmount() {
@@ -114,6 +190,36 @@ export default {
       }
       this.actualAmount = total
     },
+    async loadCoupons() {
+      try {
+        var res = await getCouponList()
+        this.coupons = res.data || []
+      } catch (e) {
+        this.coupons = []
+      }
+    },
+    openCouponPicker() {
+      if (this.availableCoupons.length === 0 && !this.selectedCoupon) {
+        uni.showToast({ title: '暂无可用优惠券', icon: 'none' })
+        return
+      }
+      this.showCouponPicker = true
+    },
+    closeCouponPicker() {
+      this.showCouponPicker = false
+    },
+    selectCoupon(coupon) {
+      this.selectedCoupon = coupon
+      this.showCouponPicker = false
+    },
+    removeCoupon() {
+      this.selectedCoupon = null
+      this.showCouponPicker = false
+    },
+    getConditionText(minSpend) {
+      if (!minSpend || minSpend <= 0) return '无门槛'
+      return '满' + (minSpend / 100).toFixed(2) + '元可用'
+    },
     loadDefaultAddress() {
       this.loading = true
       var that = this
@@ -385,6 +491,191 @@ export default {
   font-size: 26rpx;
   color: #333;
 }
+
+/* ===== Coupon Section ===== */
+.coupon-section {
+  position: relative;
+}
+
+.coupon-section:active {
+  background: #fafafa;
+}
+
+.coupon-selected {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  justify-content: flex-end;
+  margin-right: 30rpx;
+}
+
+.coupon-discount {
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: 600;
+  margin-right: 12rpx;
+}
+
+.coupon-name-tag {
+  font-size: 22rpx;
+  color: #fff;
+  background: #F97316;
+  padding: 2rpx 12rpx;
+  border-radius: 6rpx;
+  max-width: 200rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.coupon-placeholder {
+  flex: 1;
+  text-align: right;
+  margin-right: 30rpx;
+}
+
+.placeholder-text {
+  font-size: 26rpx;
+  color: #ccc;
+}
+
+.total-original {
+  font-size: 22rpx;
+  color: #bbb;
+  text-decoration: line-through;
+  margin-left: 8rpx;
+}
+
+/* ===== Coupon Picker Modal ===== */
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.5);
+  z-index: 999;
+  display: flex;
+  align-items: flex-end;
+}
+
+.coupon-picker {
+  background: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  width: 100%;
+  max-height: 70vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.picker-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.picker-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.picker-close {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.picker-list {
+  max-height: 50vh;
+  padding: 0 20rpx;
+}
+
+.picker-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 16rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+
+.picker-item:active {
+  background: #fafafa;
+}
+
+.picker-item-active {
+  background: #FFF7ED;
+}
+
+.picker-item-left {
+  width: 120rpx;
+  text-align: center;
+  flex-shrink: 0;
+}
+
+.picker-item-value {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.picker-item-right {
+  flex: 1;
+  margin: 0 16rpx;
+}
+
+.picker-item-name {
+  font-size: 26rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.picker-item-condition {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.picker-item-check {
+  width: 40rpx;
+  height: 40rpx;
+  background: #F97316;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.check-icon {
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: bold;
+}
+
+.picker-empty {
+  text-align: center;
+  padding: 60rpx 0;
+  font-size: 26rpx;
+  color: #999;
+}
+
+.picker-footer {
+  padding: 20rpx;
+  border-top: 1rpx solid #eee;
+}
+
+.picker-btn-remove {
+  background: #f5f5f5;
+  color: #666;
+  font-size: 26rpx;
+  border-radius: 12rpx;
+  border: none;
+}
+
+.picker-btn-remove::after {
+  border: none;
+}
 .bottom-spacer {
   height: 140rpx;
 }

+ 351 - 0
cfc-frontend/pages/tasks/daily-tasks.vue

@@ -0,0 +1,351 @@
+<template>
+  <view class="container">
+    <!-- 分段控制 -->
+    <view class="segmented-control">
+      <view
+        class="segment-item"
+        :class="{ active: tabIndex === 0 }"
+        @click="switchTab(0)"
+      >
+        <text class="segment-text">每日任务</text>
+        <view class="segment-underline" v-if="tabIndex === 0"></view>
+      </view>
+      <view
+        class="segment-item"
+        :class="{ active: tabIndex === 1 }"
+        @click="switchTab(1)"
+      >
+        <text class="segment-text">新手任务</text>
+        <view class="segment-underline" v-if="tabIndex === 1"></view>
+      </view>
+    </view>
+
+    <!-- 进度统计 -->
+    <view class="progress-card">
+      <view class="progress-bar-wrap">
+        <view class="progress-bar">
+          <view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
+        </view>
+        <text class="progress-text">{{ completedCount }}/{{ taskList.length }} 已完成</text>
+      </view>
+    </view>
+
+    <!-- 任务列表 -->
+    <view class="task-list" v-if="taskList.length > 0">
+      <view class="task-item" v-for="item in taskList" :key="item.taskLogId">
+        <view class="task-left">
+          <view class="task-icon">{{ item.icon }}</view>
+          <view class="task-info">
+            <text class="task-name">{{ item.name }}</text>
+            <text class="task-progress">{{ item.currentProgress }}/{{ item.targetProgress }}</text>
+            <text class="task-reward">获得 {{ item.rewardPoints }} 积分 + {{ item.rewardEnergy }} 能量</text>
+          </view>
+        </view>
+        <view class="task-right">
+          <text class="task-status" v-if="item.status === 'CLAIMED'" :class="'status-claimed'">已领取</text>
+          <text class="task-status" v-else-if="item.status === 'COMPLETED'" :class="'status-completed'">已完成</text>
+          <text class="task-status" v-else :class="'status-pending'">待完成</text>
+          <button
+            v-if="item.status === 'COMPLETED'"
+            class="claim-btn"
+            @click="claimReward(item)"
+            :disabled="claimingMap[item.taskLogId]"
+          >领取</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!loading && taskList.length === 0">
+      <text class="empty-text">暂无任务</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getGrowthTaskList, claimGrowthTaskReward } from '../../utils/api.js'
+
+var TAB_TYPES = ['DAILY', 'NEWBIE']
+
+export default {
+  data() {
+    return {
+      tabIndex: 0,
+      taskList: [],
+      loading: false,
+      claimingMap: {}
+    }
+  },
+  computed: {
+    completedCount() {
+      var count = 0
+      for (var i = 0; i < this.taskList.length; i++) {
+        if (this.taskList[i].status === 'COMPLETED' || this.taskList[i].status === 'CLAIMED') {
+          count++
+        }
+      }
+      return count
+    },
+    progressPercent() {
+      if (this.taskList.length === 0) return 0
+      return Math.round((this.completedCount / this.taskList.length) * 100)
+    }
+  },
+  onLoad() {
+    this.loadTasks()
+  },
+  onShow() {
+    this.loadTasks()
+  },
+  methods: {
+    switchTab(index) {
+      if (this.tabIndex === index) return
+      this.tabIndex = index
+      this.loadTasks()
+    },
+    async loadTasks() {
+      this.loading = true
+      try {
+        var type = TAB_TYPES[this.tabIndex]
+        var res = await getGrowthTaskList(type)
+        var records = (res.data && res.data.records) || res.data || []
+        if (!Array.isArray(records)) {
+          records = []
+        }
+        this.taskList = records.map(function(item) {
+          var status = 'PENDING'
+          if (item.claimed === 1) {
+            status = 'CLAIMED'
+          } else if (item.completed === 1) {
+            status = 'COMPLETED'
+          }
+          return {
+            taskLogId: item.taskLogId || 0,
+            icon: '📋',
+            name: item.title || '',
+            currentProgress: item.progress || 0,
+            targetProgress: item.targetValue || 1,
+            rewardPoints: item.rewardPoints || 0,
+            rewardEnergy: item.rewardEnergy || 0,
+            status: status
+          }
+        })
+      } catch (e) {
+        console.error('获取任务列表失败', e)
+        uni.showToast({ title: '获取任务失败', icon: 'none' })
+      }
+      this.loading = false
+    },
+    async claimReward(item) {
+      if (this.claimingMap[item.taskLogId]) return
+      this.$set(this.claimingMap, item.taskLogId, true)
+      try {
+        await claimGrowthTaskReward(item.taskLogId)
+        item.status = 'CLAIMED'
+        uni.showToast({
+          title: '获得' + item.rewardPoints + '积分+' + item.rewardEnergy + '能量',
+          icon: 'none',
+          duration: 2000
+        })
+      } catch (e) {
+        uni.showToast({ title: '领取失败', icon: 'none' })
+      }
+      this.$set(this.claimingMap, item.taskLogId, false)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+/* ===== 分段控制 ===== */
+.segmented-control {
+  display: flex;
+  background: #fff;
+  border-radius: 20rpx;
+  margin-bottom: 24rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.segment-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx 0 16rpx;
+  position: relative;
+}
+.segment-item.active .segment-text {
+  color: #F97316;
+  font-weight: 600;
+}
+.segment-text {
+  font-size: 30rpx;
+  color: #94A3B8;
+}
+.segment-underline {
+  width: 48rpx;
+  height: 6rpx;
+  background: #F97316;
+  border-radius: 3rpx;
+  margin-top: 12rpx;
+}
+
+/* ===== 进度卡片 ===== */
+.progress-card {
+  background: linear-gradient(135deg, #F97316, #EA580C);
+  border-radius: 20rpx;
+  padding: 30rpx 40rpx;
+  color: #fff;
+  margin-bottom: 24rpx;
+}
+.progress-bar-wrap {
+  display: flex;
+  align-items: center;
+}
+.progress-bar {
+  flex: 1;
+  height: 16rpx;
+  background: rgba(255,255,255,0.3);
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: #fff;
+  border-radius: 8rpx;
+  transition: width 0.3s ease;
+}
+.progress-text {
+  font-size: 24rpx;
+  margin-left: 16rpx;
+  white-space: nowrap;
+}
+
+/* ===== 任务列表 ===== */
+.task-list {
+  background: #fff;
+  border-radius: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.task-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28rpx 30rpx;
+  border-bottom: 1rpx solid #FED7AA;
+}
+.task-item:last-child {
+  border-bottom: none;
+}
+.task-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  margin-right: 20rpx;
+}
+.task-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 16rpx;
+  background: #FEF3C7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.task-info {
+  flex: 1;
+}
+.task-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.task-progress {
+  font-size: 22rpx;
+  color: #64748B;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.task-reward {
+  font-size: 22rpx;
+  color: #F97316;
+  display: block;
+}
+.task-right {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+.task-status {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  margin-right: 12rpx;
+}
+.status-pending {
+  background: #F1F5F9;
+  color: #94A3B8;
+}
+.status-completed {
+  background: #DCFCE7;
+  color: #16A34A;
+}
+.status-claimed {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.claim-btn {
+  background: #F97316;
+  color: #fff;
+  font-size: 24rpx;
+  border-radius: 30rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  padding: 0 28rpx;
+  font-weight: 500;
+}
+.claim-btn::after {
+  border: none;
+}
+.claim-btn:active {
+  opacity: 0.8;
+}
+.claim-btn[disabled] {
+  opacity: 0.5;
+}
+
+/* ===== 空状态 ===== */
+.empty-state {
+  text-align: center;
+  padding: 120rpx 0;
+}
+.empty-text {
+  color: #94A3B8;
+  font-size: 28rpx;
+}
+
+/* ===== 加载状态 ===== */
+.loading-state {
+  text-align: center;
+  padding: 60rpx;
+  color: #94A3B8;
+  font-size: 28rpx;
+}
+</style>

+ 43 - 0
cfc-frontend/utils/api.js

@@ -1416,3 +1416,46 @@ export const getNutritionMappings = (data) => request('/api/health/nutrition/map
 // ===== 健康维度 =====
 export const getDimensionOverview = (data) => request('/api/dimension/overview', 'POST', data)
 export const uploadDimensionScore = (data) => request('/api/dimension/upload', 'POST', data)
+
+// ===== 邀请里程碑 =====
+export const getInviteMilestones = () => {
+  return request('/api/invite/milestone/list', 'POST')
+}
+export const claimInviteMilestone = (milestone) => {
+  return request('/api/invite/milestone/claim', 'POST', { milestone })
+}
+
+// ===================== 优惠券模块 =====================
+export const getCouponList = () => {
+  return request('/api/coupon/list', 'POST')
+}
+
+export const claimCoupon = (couponId) => {
+  return request('/api/coupon/claim', 'POST', { couponId })
+}
+
+// ===== 新手任务(Onboarding Rewards) =====
+export const getOnboardingProgress = () => {
+  return request('/api/onboarding/progress', 'POST')
+}
+export const claimOnboardingReward = (taskType) => {
+  return request('/api/onboarding/claim', 'POST', { taskType })
+}
+
+// ===== 成长任务(每日/新手任务系统) =====
+export const getGrowthTaskList = (type) => {
+  return request('/api/growth-task/list', 'POST', { type: type || 'DAILY' })
+}
+
+export const claimGrowthTaskReward = (taskLogId) => {
+  return request('/api/growth-task/claim', 'POST', { taskLogId })
+}
+
+// ===== 推广排行榜 =====
+export const getLeaderboardTop = () => {
+  return request('/api/leaderboard/top', 'POST')
+}
+
+export const getLeaderboardMyRank = () => {
+  return request('/api/leaderboard/my-rank', 'POST')
+}

+ 21 - 0
cfc-web/src/api/coupon.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function getCouponList(data) {
+  return request({ url: '/api/admin/coupon/list', method: 'post', data })
+}
+
+export function createCoupon(data) {
+  return request({ url: '/api/admin/coupon/create', method: 'post', data })
+}
+
+export function updateCoupon(data) {
+  return request({ url: '/api/admin/coupon/update', method: 'post', data })
+}
+
+export function deleteCoupon(id) {
+  return request({ url: '/api/admin/coupon/delete', method: 'post', data: { id } })
+}
+
+export function batchIssueCoupons(data) {
+  return request({ url: '/api/admin/coupon/issue', method: 'post', data })
+}

+ 21 - 0
cfc-web/src/api/growthTask.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function getGrowthTaskList(data) {
+  return request({ url: '/api/admin/growth-task/list', method: 'post', data })
+}
+
+export function updateGrowthTask(id, data) {
+  return request({ url: `/api/admin/growth-task/${id}`, method: 'post', data })
+}
+
+export function createGrowthTask(data) {
+  return request({ url: '/api/admin/growth-task/create', method: 'post', data })
+}
+
+export function getOnboardingConfigList() {
+  return request({ url: '/api/admin/onboarding/config', method: 'post' })
+}
+
+export function updateOnboardingConfig(data) {
+  return request({ url: '/api/admin/onboarding/config/update', method: 'post', data })
+}

+ 17 - 0
cfc-web/src/api/promotion.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function getMilestoneConfig() {
+  return request({ url: '/api/admin/milestone/config', method: 'post' })
+}
+
+export function updateMilestoneConfig(data) {
+  return request({ url: '/api/admin/milestone/config/update', method: 'post', data })
+}
+
+export function getLeaderboardTop(data) {
+  return request({ url: '/api/admin/leaderboard/top', method: 'post', data })
+}
+
+export function getLeaderboardByUser(data) {
+  return request({ url: '/api/admin/leaderboard/search', method: 'post', data })
+}

+ 18 - 0
cfc-web/src/router/index.js

@@ -61,6 +61,12 @@ const routes = [
         component: () => import('@/views/TaskTemplates.vue'),
         meta: { title: '任务模板', perm: 'task:templates' }
       },
+      {
+        path: 'growth-task',
+        name: 'GrowthTaskManagement',
+        component: () => import('@/views/admin/GrowthTaskManagement'),
+        meta: { title: '成长任务管理', perm: 'task:list' }
+      },
       // 奖励管理
       {
         path: 'rewards',
@@ -377,6 +383,18 @@ const routes = [
         meta: { title: '活动审核', perm: 'activity:review' }
       },
       // ========== 维度配置 + 知识库管理 ==========
+      {
+        path: 'coupon',
+        name: 'CouponManagement',
+        component: () => import('@/views/admin/CouponManagement'),
+        meta: { title: '优惠券管理', perm: 'system:coupon' }
+      },
+      {
+        path: 'promotion',
+        name: 'PromotionManagement',
+        component: () => import('@/views/admin/PromotionManagement'),
+        meta: { title: '推广管理', perm: 'system:promotion' }
+      },
       {
         path: 'dimension-config',
         name: 'DimensionConfig',

+ 3 - 0
cfc-web/src/views/Layout.vue

@@ -148,6 +148,7 @@ export default {
           children: [
             { path: '/tasks', label: '任务列表', icon: 'el-icon-s-order', perm: 'task:list' },
             { path: '/task-templates', label: '任务模板', icon: 'el-icon-document', perm: 'task:templates' },
+            { path: '/growth-task', label: '成长任务管理', icon: 'el-icon-s-management', perm: 'task:list' },
           ]},
         { title: '奖励管理', icon: 'el-icon-s-goods', perm: 'reward:*',
           children: [
@@ -231,6 +232,8 @@ export default {
             { path: '/zodiac-configs', label: '星座配置', icon: 'el-icon-s-management', perm: 'system:config' },
             { path: '/bazi-configs', label: '八字配置', icon: 'el-icon-s-management', perm: 'system:config' },
             { path: '/blood-type-configs', label: '血型配置', icon: 'el-icon-s-management', perm: 'system:config' },
+            { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'system:promotion' },
+            { path: '/coupon', label: '优惠券管理', icon: 'el-icon-s-management', perm: 'system:coupon' },
             { path: '/relationship-types', label: '关系类型', icon: 'el-icon-s-management', perm: 'system:config' },
           ]},
       ],

+ 282 - 0
cfc-web/src/views/admin/CouponManagement.vue

@@ -0,0 +1,282 @@
+<template>
+  <div class="coupon-management">
+    <el-card>
+      <div slot="header">
+        <span>优惠券管理</span>
+        <div style="float: right">
+          <el-button type="primary" size="small" @click="handleCreate">新建优惠券</el-button>
+          <el-button type="success" size="small" @click="handleBatchIssue">批量发放</el-button>
+        </div>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
+        <el-table-column label="类型" width="90">
+          <template slot-scope="{ row }">
+            <el-tag size="mini">{{ row.type }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="面值(元)" width="90">
+          <template slot-scope="{ row }">{{ (row.value / 100).toFixed(2) }}</template>
+        </el-table-column>
+        <el-table-column label="使用门槛(元)" width="110">
+          <template slot-scope="{ row }">{{ row.minSpend ? (row.minSpend / 100).toFixed(2) : '无' }}</template>
+        </el-table-column>
+        <el-table-column prop="applicableTo" label="适用对象" width="110">
+          <template slot-scope="{ row }">{{ applicableLabel(row.applicableTo) }}</template>
+        </el-table-column>
+        <el-table-column label="有效期" width="180">
+          <template slot-scope="{ row }">{{ row.validFrom }} ~ {{ row.validUntil }}</template>
+        </el-table-column>
+        <el-table-column prop="totalCount" label="总量" width="70" />
+        <el-table-column prop="usedCount" label="已用" width="70" />
+        <el-table-column label="状态" width="80">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.status === 'active' ? 'success' : 'info'" size="mini">
+              {{ row.status === 'active' ? '启用' : '停用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="140" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" @click="handleEdit(row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        @current-change="onPageChange"
+        @size-change="onPageChange"
+        :current-page="page"
+        :page-size="size"
+        :total="total"
+        layout="total, prev, pager, next"
+        style="margin-top: 20px; text-align: right"
+      />
+    </el-card>
+
+    <el-dialog :visible.sync="formDialogVisible" :title="isEdit ? '编辑优惠券' : '新建优惠券'" width="550px">
+      <el-form :model="form" label-width="120px" ref="couponForm">
+        <el-form-item label="优惠券名称" required>
+          <el-input v-model="form.name" placeholder="请输入优惠券名称" />
+        </el-form-item>
+        <el-form-item label="类型" required>
+          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
+            <el-option label="FIXED" value="FIXED" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="面值(元)" required>
+          <el-input-number v-model="form.valueYuan" :min="0.01" :step="0.5" :precision="2" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="使用门槛(元)">
+          <el-input-number v-model="form.minSpendYuan" :min="0" :step="10" :precision="2" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="适用对象">
+          <el-select v-model="form.applicableTo" placeholder="请选择适用对象" style="width: 100%">
+            <el-option label="全部商品" value="ALL" />
+            <el-option label="会员服务" value="MEMBERSHIP" />
+            <el-option label="特定商品" value="PRODUCT" />
+            <el-option label="测评服务" value="ASSESSMENT" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="有效期起" required>
+          <el-date-picker v-model="form.validFrom" type="date" placeholder="选择开始日期" value-format="yyyy-MM-dd" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="有效期止" required>
+          <el-date-picker v-model="form.validUntil" type="date" placeholder="选择结束日期" value-format="yyyy-MM-dd" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="发放总量" required>
+          <el-input-number v-model="form.totalCount" :min="1" :step="100" style="width: 200px" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="formDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="批量发放优惠券" :visible.sync="batchDialogVisible" width="500px">
+      <el-form label-width="120px">
+        <el-form-item label="选择优惠券" required>
+          <el-select v-model="batchCouponId" placeholder="请选择优惠券" style="width: 100%">
+            <el-option v-for="c in couponOptions" :key="c.id" :label="c.name" :value="c.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="用户ID列表" required>
+          <el-input v-model="batchUserIds" type="textarea" :rows="6" placeholder="每行一个用户ID&#10;例如:&#10;1001&#10;1002&#10;1003" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="batchDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmBatchIssue" :loading="batchSubmitting">确认发放</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons } from '@/api/coupon'
+
+export default {
+  name: 'CouponManagement',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      formDialogVisible: false,
+      isEdit: false,
+      submitting: false,
+      form: this.getEmptyForm(),
+      batchDialogVisible: false,
+      batchCouponId: '',
+      batchUserIds: '',
+      batchSubmitting: false,
+      couponOptions: []
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    getEmptyForm() {
+      return {
+        id: null,
+        name: '',
+        type: 'FIXED',
+        valueYuan: null,
+        minSpendYuan: null,
+        applicableTo: 'ALL',
+        validFrom: '',
+        validUntil: '',
+        totalCount: null
+      }
+    },
+    applicableLabel(val) {
+      const map = { ALL: '全部商品', MEMBERSHIP: '会员服务', PRODUCT: '特定商品', ASSESSMENT: '测评服务' }
+      return map[val] || val
+    },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getCouponList({ page: this.page, size: this.size })
+        if (res.data) {
+          this.list = res.data.records || res.data.list || res.data
+          this.total = res.data.total || this.list.length
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(p) {
+      if (p) this.page = p
+      this.loadData()
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = this.getEmptyForm()
+      this.formDialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = {
+        id: row.id,
+        name: row.name,
+        type: row.type,
+        valueYuan: row.value / 100,
+        minSpendYuan: row.minSpend ? row.minSpend / 100 : null,
+        applicableTo: row.applicableTo,
+        validFrom: row.validFrom,
+        validUntil: row.validUntil,
+        totalCount: row.totalCount
+      }
+      this.formDialogVisible = true
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定删除该优惠券吗?', '提示', { type: 'warning' })
+        await deleteCoupon(row.id)
+        this.$message.success('删除成功')
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    },
+    async handleSubmit() {
+      if (!this.form.name || !this.form.valueYuan || !this.form.totalCount || !this.form.validFrom || !this.form.validUntil) {
+        this.$message.warning('请填写完整信息')
+        return
+      }
+      this.submitting = true
+      try {
+        const payload = {
+          name: this.form.name,
+          type: this.form.type,
+          value: Math.round(this.form.valueYuan * 100),
+          minSpend: this.form.minSpendYuan ? Math.round(this.form.minSpendYuan * 100) : 0,
+          applicableTo: this.form.applicableTo,
+          validFrom: this.form.validFrom,
+          validUntil: this.form.validUntil,
+          totalCount: this.form.totalCount
+        }
+        if (this.isEdit) payload.id = this.form.id
+        if (this.isEdit) {
+          await updateCoupon(payload)
+        } else {
+          await createCoupon(payload)
+        }
+        this.$message.success('保存成功')
+        this.formDialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('保存失败')
+      } finally {
+        this.submitting = false
+      }
+    },
+    async handleBatchIssue() {
+      this.batchCouponId = ''
+      this.batchUserIds = ''
+      this.batchDialogVisible = true
+      try {
+        const res = await getCouponList({ page: 1, size: 999 })
+        this.couponOptions = res.data.records || res.data.list || res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async confirmBatchIssue() {
+      if (!this.batchCouponId) {
+        this.$message.warning('请选择优惠券')
+        return
+      }
+      const ids = this.batchUserIds.split('\n').map(s => s.trim()).filter(Boolean)
+      if (ids.length === 0) {
+        this.$message.warning('请输入至少一个用户ID')
+        return
+      }
+      this.batchSubmitting = true
+      try {
+        await batchIssueCoupons({ couponId: this.batchCouponId, userIds: ids })
+        this.$message.success(`成功向 ${ids.length} 个用户发放优惠券`)
+        this.batchDialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('发放失败')
+      } finally {
+        this.batchSubmitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.coupon-management { padding: 20px; }
+</style>

+ 288 - 0
cfc-web/src/views/admin/GrowthTaskManagement.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="growth-task-management">
+    <el-card>
+      <el-tabs v-model="activeTab" @tab-click="handleTabChange">
+        <el-tab-pane label="成长任务" name="growth">
+          <div style="margin-bottom: 16px">
+            <el-button type="primary" size="small" @click="handleCreateTask">新建任务</el-button>
+          </div>
+          <el-table :data="growthTaskList" v-loading="growthLoading" border stripe>
+            <el-table-column prop="id" label="ID" width="70" />
+            <el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="type" label="类型" width="90">
+              <template slot-scope="{ row }">
+                <el-tag size="mini">{{ row.type }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="rewardPoints" label="奖励积分" width="90" />
+            <el-table-column prop="rewardEnergy" label="奖励能量" width="90" />
+            <el-table-column prop="targetValue" label="目标值" width="80" />
+            <el-table-column prop="taskKey" label="任务Key" width="140" show-overflow-tooltip />
+            <el-table-column label="启用" width="70" align="center">
+              <template slot-scope="{ row }">
+                <el-switch v-model="row.enabled" :active-value="1" :inactive-value="0" @change="(val) => handleToggleEnabled(row, val)" />
+              </template>
+            </el-table-column>
+            <el-table-column prop="sortOrder" label="排序" width="70" />
+            <el-table-column label="操作" width="120" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button size="mini" @click="handleEditTask(row)">编辑</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane label="新手任务" name="onboarding">
+          <el-table :data="onboardingList" v-loading="onboardingLoading" border stripe>
+            <el-table-column prop="taskType" label="任务类型" min-width="180">
+              <template slot-scope="{ row }">{{ taskTypeLabel(row.taskType) }}</template>
+            </el-table-column>
+            <el-table-column prop="rewardPoints" label="奖励积分" width="100" />
+            <el-table-column prop="rewardEnergy" label="奖励能量" width="100" />
+            <el-table-column label="操作" width="120" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button size="mini" @click="handleEditOnboarding(row)">编辑</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <el-dialog :visible.sync="taskDialogVisible" :title="isEditTask ? '编辑成长任务' : '新建成长任务'" width="550px">
+      <el-form :model="taskForm" label-width="120px">
+        <el-form-item label="标题" required>
+          <el-input v-model="taskForm.title" placeholder="请输入任务标题" />
+        </el-form-item>
+        <el-form-item label="类型" required>
+          <el-select v-model="taskForm.type" placeholder="请选择类型" style="width: 100%">
+            <el-option label="DAILY" value="DAILY" />
+            <el-option label="NEWBIE" value="NEWBIE" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="奖励积分">
+          <el-input-number v-model="taskForm.rewardPoints" :min="0" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="奖励能量">
+          <el-input-number v-model="taskForm.rewardEnergy" :min="0" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="目标值">
+          <el-input-number v-model="taskForm.targetValue" :min="1" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="taskForm.sortOrder" :min="0" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="启用">
+          <el-switch v-model="taskForm.enabled" :active-value="1" :inactive-value="0" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="taskDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleTaskSubmit" :loading="taskSubmitting">保存</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="编辑新手任务配置" :visible.sync="onboardingDialogVisible" width="450px">
+      <el-form :model="onboardingForm" label-width="120px">
+        <el-form-item label="任务类型" required>
+          <el-input v-model="onboardingForm.taskType" disabled />
+        </el-form-item>
+        <el-form-item label="奖励积分">
+          <el-input-number v-model="onboardingForm.rewardPoints" :min="0" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="奖励能量">
+          <el-input-number v-model="onboardingForm.rewardEnergy" :min="0" style="width: 200px" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="onboardingDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleOnboardingSubmit" :loading="onboardingSubmitting">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getGrowthTaskList, updateGrowthTask, createGrowthTask, getOnboardingConfigList, updateOnboardingConfig } from '@/api/growthTask'
+
+const TASK_TYPE_LABELS = {
+  REGISTER: '注册账号',
+  COMPLETE_PROFILE: '完善个人资料',
+  ADD_CHILD: '添加孩子',
+  FIRST_CHECKIN: '首次签到',
+  FIRST_TASK: '完成首个任务',
+  BIND_REFERRAL: '绑定推荐码'
+}
+
+export default {
+  name: 'GrowthTaskManagement',
+  data() {
+    return {
+      activeTab: 'growth',
+      growthTaskList: [],
+      growthLoading: false,
+      onboardingList: [],
+      onboardingLoading: false,
+      taskDialogVisible: false,
+      isEditTask: false,
+      taskSubmitting: false,
+      taskForm: this.getEmptyTaskForm(),
+      onboardingDialogVisible: false,
+      onboardingSubmitting: false,
+      onboardingForm: {
+        taskType: '',
+        rewardPoints: 0,
+        rewardEnergy: 0
+      }
+    }
+  },
+  created() {
+    this.loadGrowthTasks()
+  },
+  methods: {
+    taskTypeLabel(type) {
+      return TASK_TYPE_LABELS[type] || type
+    },
+    getEmptyTaskForm() {
+      return {
+        id: null,
+        title: '',
+        type: 'DAILY',
+        rewardPoints: 0,
+        rewardEnergy: 0,
+        targetValue: 1,
+        sortOrder: 0,
+        enabled: 1
+      }
+    },
+    handleTabChange() {
+      if (this.activeTab === 'onboarding') {
+        this.loadOnboardingConfigs()
+      } else {
+        this.loadGrowthTasks()
+      }
+    },
+    async loadGrowthTasks() {
+      this.growthLoading = true
+      try {
+        const res = await getGrowthTaskList({})
+        if (res.data) {
+          this.growthTaskList = res.data.records || res.data.list || res.data
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.growthLoading = false
+      }
+    },
+    async loadOnboardingConfigs() {
+      this.onboardingLoading = true
+      try {
+        const res = await getOnboardingConfigList()
+        if (res.data) {
+          this.onboardingList = res.data.records || res.data.list || res.data
+        }
+      } catch (e) {
+        console.error(e)
+      } finally {
+        this.onboardingLoading = false
+      }
+    },
+    handleCreateTask() {
+      this.isEditTask = false
+      this.taskForm = this.getEmptyTaskForm()
+      this.taskDialogVisible = true
+    },
+    handleEditTask(row) {
+      this.isEditTask = true
+      this.taskForm = {
+        id: row.id,
+        title: row.title,
+        type: row.type,
+        rewardPoints: row.rewardPoints,
+        rewardEnergy: row.rewardEnergy,
+        targetValue: row.targetValue,
+        sortOrder: row.sortOrder,
+        enabled: row.enabled
+      }
+      this.taskDialogVisible = true
+    },
+    async handleToggleEnabled(row, val) {
+      try {
+        await updateGrowthTask(row.id, { enabled: val })
+        this.$message.success(val ? '已启用' : '已禁用')
+      } catch (e) {
+        this.$message.error('操作失败')
+        this.loadGrowthTasks()
+      }
+    },
+    async handleTaskSubmit() {
+      if (!this.taskForm.title) {
+        this.$message.warning('请输入任务标题')
+        return
+      }
+      this.taskSubmitting = true
+      try {
+        if (this.isEditTask) {
+          await updateGrowthTask(this.taskForm.id, {
+            title: this.taskForm.title,
+            type: this.taskForm.type,
+            rewardPoints: this.taskForm.rewardPoints,
+            rewardEnergy: this.taskForm.rewardEnergy,
+            targetValue: this.taskForm.targetValue,
+            sortOrder: this.taskForm.sortOrder,
+            enabled: this.taskForm.enabled
+          })
+        } else {
+          await createGrowthTask({
+            title: this.taskForm.title,
+            type: this.taskForm.type,
+            rewardPoints: this.taskForm.rewardPoints,
+            rewardEnergy: this.taskForm.rewardEnergy,
+            targetValue: this.taskForm.targetValue,
+            sortOrder: this.taskForm.sortOrder,
+            enabled: this.taskForm.enabled
+          })
+        }
+        this.$message.success('保存成功')
+        this.taskDialogVisible = false
+        this.loadGrowthTasks()
+      } catch (e) {
+        this.$message.error('保存失败')
+      } finally {
+        this.taskSubmitting = false
+      }
+    },
+    handleEditOnboarding(row) {
+      this.onboardingForm = {
+        taskType: row.taskType,
+        rewardPoints: row.rewardPoints,
+        rewardEnergy: row.rewardEnergy
+      }
+      this.onboardingDialogVisible = true
+    },
+    async handleOnboardingSubmit() {
+      this.onboardingSubmitting = true
+      try {
+        await updateOnboardingConfig({
+          taskType: this.onboardingForm.taskType,
+          rewardPoints: this.onboardingForm.rewardPoints,
+          rewardEnergy: this.onboardingForm.rewardEnergy
+        })
+        this.$message.success('保存成功')
+        this.onboardingDialogVisible = false
+        this.loadOnboardingConfigs()
+      } catch (e) {
+        this.$message.error('保存失败')
+      } finally {
+        this.onboardingSubmitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.growth-task-management {
+  padding: 20px;
+}
+</style>

+ 233 - 0
cfc-web/src/views/admin/PromotionManagement.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="promotion-management">
+    <el-card>
+      <el-tabs v-model="activeTab" @tab-click="handleTabChange">
+        <el-tab-pane label="邀请里程碑" name="milestone">
+          <el-table :data="milestoneList" v-loading="milestoneLoading" border stripe>
+            <el-table-column prop="milestone" label="里程碑" min-width="180" show-overflow-tooltip />
+            <el-table-column prop="rewardPoints" label="奖励积分" width="120" />
+            <el-table-column prop="rewardEnergy" label="奖励能量" width="120" />
+            <el-table-column label="操作" width="140" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button size="mini" @click="handleEditMilestone(row)" :disabled="row.claimed">
+                  {{ row.claimed ? '已达成' : '编辑' }}
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane label="排行榜" name="leaderboard">
+          <div style="margin-bottom: 16px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap">
+            <el-date-picker
+              v-model="weekStart"
+              type="date"
+              placeholder="选择周起始日"
+              value-format="yyyy-MM-dd"
+              @change="handleWeekChange"
+              style="width: 180px"
+            />
+            <el-input
+              v-model="searchKeyword"
+              placeholder="搜索用户昵称"
+              style="width: 200px"
+              clearable
+              @keyup.enter.native="handleSearch"
+            />
+            <el-button type="primary" @click="handleSearch">搜索</el-button>
+            <el-button @click="resetLeaderboard">重置</el-button>
+          </div>
+          <el-table :data="leaderboardList" v-loading="leaderboardLoading" border stripe>
+            <el-table-column label="排名" width="80" type="index" :index="leaderboardIndex" />
+            <el-table-column label="头像" width="80">
+              <template slot-scope="{ row }">
+                <el-avatar :src="row.avatar" size="small" />
+              </template>
+            </el-table-column>
+            <el-table-column prop="nickname" label="昵称" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="referralCount" label="邀请数" width="100" />
+          </el-table>
+          <el-pagination
+            v-if="leaderboardTotal > leaderboardSize"
+            @current-change="onLeaderboardPageChange"
+            :current-page="leaderboardPage"
+            :page-size="leaderboardSize"
+            :total="leaderboardTotal"
+            layout="total, prev, pager, next"
+            style="margin-top: 20px; text-align: right"
+          />
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <el-dialog title="编辑里程碑" :visible.sync="milestoneDialogVisible" width="450px">
+      <el-form :model="milestoneForm" label-width="120px">
+        <el-form-item label="里程碑" required>
+          <el-input v-model="milestoneForm.milestone" disabled />
+        </el-form-item>
+        <el-form-item label="奖励积分" required>
+          <el-input-number v-model="milestoneForm.rewardPoints" :min="0" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="奖励能量" required>
+          <el-input-number v-model="milestoneForm.rewardEnergy" :min="0" style="width: 200px" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="milestoneDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleMilestoneSubmit" :loading="milestoneSubmitting">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getMilestoneConfig, updateMilestoneConfig, getLeaderboardTop, getLeaderboardByUser } from '@/api/promotion'
+
+export default {
+  name: 'PromotionManagement',
+  data() {
+    return {
+      activeTab: 'milestone',
+      milestoneList: [],
+      milestoneLoading: false,
+      milestoneDialogVisible: false,
+      milestoneSubmitting: false,
+      milestoneForm: {
+        milestone: '',
+        rewardPoints: 0,
+        rewardEnergy: 0
+      },
+      leaderboardList: [],
+      leaderboardLoading: false,
+      weekStart: this.getCurrentWeekStart(),
+      searchKeyword: '',
+      leaderboardPage: 1,
+      leaderboardSize: 20,
+      leaderboardTotal: 0,
+      searchMode: false
+    }
+  },
+  created() {
+    this.loadMilestones()
+  },
+  methods: {
+    getCurrentWeekStart() {
+      const now = new Date()
+      const day = now.getDay()
+      const diff = now.getDate() - day + (day === 0 ? -6 : 1)
+      const monday = new Date(now.setDate(diff))
+      return monday.toISOString().split('T')[0]
+    },
+    leaderboardIndex(index) {
+      return (this.leaderboardPage - 1) * this.leaderboardSize + index + 1
+    },
+    handleTabChange() {
+      if (this.activeTab === 'leaderboard') {
+        this.loadLeaderboard()
+      } else {
+        this.loadMilestones()
+      }
+    },
+    async loadMilestones() {
+      this.milestoneLoading = true
+      try {
+        const res = await getMilestoneConfig()
+        if (res.data) {
+          this.milestoneList = res.data.records || res.data.list || res.data
+        }
+      } catch (e) {
+        console.error(e)
+        this.$message.error('加载里程碑配置失败')
+      } finally {
+        this.milestoneLoading = false
+      }
+    },
+    handleEditMilestone(row) {
+      if (row.claimed) return
+      this.milestoneForm = {
+        milestone: row.milestone,
+        rewardPoints: row.rewardPoints,
+        rewardEnergy: row.rewardEnergy
+      }
+      this.milestoneDialogVisible = true
+    },
+    async handleMilestoneSubmit() {
+      this.milestoneSubmitting = true
+      try {
+        await updateMilestoneConfig({
+          milestone: this.milestoneForm.milestone,
+          rewardPoints: this.milestoneForm.rewardPoints,
+          rewardEnergy: this.milestoneForm.rewardEnergy
+        })
+        this.$message.success('保存成功')
+        this.milestoneDialogVisible = false
+        this.loadMilestones()
+      } catch (e) {
+        this.$message.error('保存失败')
+      } finally {
+        this.milestoneSubmitting = false
+      }
+    },
+    async loadLeaderboard() {
+      this.leaderboardLoading = true
+      this.searchMode = false
+      try {
+        const params = { page: this.leaderboardPage, size: this.leaderboardSize }
+        if (this.weekStart) {
+          params.weekStart = this.weekStart
+        }
+        const res = await getLeaderboardTop(params)
+        if (res.data) {
+          this.leaderboardList = res.data.records || res.data.list || res.data
+          this.leaderboardTotal = res.data.total || this.leaderboardList.length
+        }
+      } catch (e) {
+        console.error(e)
+        this.$message.error('加载排行榜失败')
+      } finally {
+        this.leaderboardLoading = false
+      }
+    },
+    handleWeekChange() {
+      if (this.activeTab === 'leaderboard') {
+        this.leaderboardPage = 1
+        this.loadLeaderboard()
+      }
+    },
+    async handleSearch() {
+      if (!this.searchKeyword.trim()) {
+        this.resetLeaderboard()
+        return
+      }
+      this.leaderboardLoading = true
+      this.searchMode = true
+      try {
+        const res = await getLeaderboardByUser({ keyword: this.searchKeyword.trim() })
+        if (res.data) {
+          this.leaderboardList = res.data.records || res.data.list || res.data
+          this.leaderboardTotal = this.leaderboardList.length
+        }
+      } catch (e) {
+        console.error(e)
+        this.$message.error('搜索失败')
+      } finally {
+        this.leaderboardLoading = false
+      }
+    },
+    resetLeaderboard() {
+      this.searchKeyword = ''
+      this.leaderboardPage = 1
+      this.loadLeaderboard()
+    },
+    onLeaderboardPageChange(p) {
+      this.leaderboardPage = p
+      this.loadLeaderboard()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.promotion-management {
+  padding: 20px;
+}
+</style>

+ 160 - 0
tests/e2e/test-promotion-flow.sh

@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+#
+# test-promotion-flow.sh — End-to-end integration test for promotion flows
+#
+# Tests: register → onboarding → growth-tasks → share → invite →
+#        milestone → leaderboard → coupon
+#
+# Usage:
+#   BASE_URL=http://localhost:9082 ./test-promotion-flow.sh
+#
+set -euo pipefail
+
+BASE_URL="${BASE_URL:-http://localhost:9082}"
+PASS=0
+FAIL=0
+
+# ── helpers ──────────────────────────────────────────────────────────
+
+header()   { echo ""; echo "===== $1 ====="; }
+
+api() {
+  local ep="$1" tok="$2" body="${3:-{}}"
+  curl -s -X POST "${BASE_URL}${ep}" \
+    -H "Content-Type: application/json" \
+    $( [ -n "$tok" ] && echo "-H Authorization: Bearer ${tok}" ) \
+    -d "$body"
+}
+
+json_get() {
+
+  python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+parts = '$2'.split('.')
+v = d
+for p in parts:
+    if isinstance(v, dict):
+        v = v.get(p, '')
+    elif isinstance(v, list):
+        try:
+            idx = int(p)
+            v = v[idx] if idx < len(v) else ''
+        except ValueError:
+            v = ''
+    else:
+        v = ''
+    if v == '':
+        break
+print(v if v != '' else '')
+" <<< "$1" 2>/dev/null || echo ""
+}
+
+passert() {
+  local tn="$1" json="$2"
+  if echo "$json" | python3 -c "
+import sys,json
+d=json.load(sys.stdin)
+exit(0 if d.get('code')==200 else 1)
+" 2>/dev/null; then
+    echo "  PASS: $tn"
+    ((PASS++))
+  else
+    local msg
+    msg=$(echo "$json" | python3 -c "
+import sys,json
+d=json.load(sys.stdin)
+print(d.get('message','no message'))
+" 2>/dev/null)
+    echo "  FAIL: $tn — $msg"
+    ((FAIL++))
+  fi
+}
+
+header "0. Health Check"
+R=$(api "/api/auth/check-phone" "" '{"phone":"13800000001"}')
+passert "Server reachable" "$R"
+
+header "1. Register User1 (parent)"
+PHONE1="138$(printf '%08d' $((RANDOM%100000000)))"
+R=$(api "/api/auth/direct-register" "" \
+  "{\"phone\":\"$PHONE1\",\"nickname\":\"TestParent\",\"role\":\"parent\"}")
+passert "Register user1" "$R"
+TOKEN1=$(json_get "$R" "data.token")
+echo "  user1: $PHONE1  id=$(json_get "$R" "data.userId")"
+
+# ── 2. Onboarding progress ───────────────────────────────────────────
+
+header "2. Onboarding Progress"
+R=$(api "/api/onboarding/progress" "$TOKEN1")
+passert "Onboarding progress" "$R"
+
+header "3. Claim REGISTER Reward"
+R=$(api "/api/onboarding/claim" "$TOKEN1" '{"taskType":"REGISTER"}')
+passert "Claim REGISTER reward" "$R"
+
+header "4. List Daily Growth Tasks"
+R=$(api "/api/growth-task/list" "$TOKEN1" '{"type":"DAILY"}')
+passert "List daily growth tasks" "$R"
+
+header "5. Get Referral Code"
+R=$(api "/api/invite/code" "$TOKEN1")
+passert "Get referral code" "$R"
+CODE=$(json_get "$R" "data")
+echo "  referral code: $CODE"
+
+header "6. Register User2 + Bind Referral"
+PHONE2="138$(printf '%08d' $((RANDOM%100000000)))"
+R=$(api "/api/auth/direct-register" "" \
+  "{\"phone\":\"$PHONE2\",\"nickname\":\"TestReferral\",\"role\":\"parent\"}")
+passert "Register user2" "$R"
+TOKEN2=$(json_get "$R" "data.token")
+echo "  user2: $PHONE2  id=$(json_get "$R" "data.userId")"
+
+R=$(api "/api/invite/bind" "$TOKEN2" "{\"referralCode\":\"$CODE\"}")
+passert "Bind referral" "$R"
+
+# ── 7. Invite milestones ─────────────────────────────────────────────
+
+header "7. Invite Milestones"
+R=$(api "/api/invite/milestone/list" "$TOKEN1")
+passert "List milestones" "$R"
+
+# ── 8. Leaderboard ───────────────────────────────────────────────────
+
+header "8. Leaderboard"
+R=$(api "/api/leaderboard/top" "")
+passert "Leaderboard top 20" "$R"
+R=$(api "/api/leaderboard/my-rank" "$TOKEN1")
+passert "My rank" "$R"
+
+# ── 9. Coupon flow ───────────────────────────────────────────────────
+
+header "9. Coupon Flow"
+R=$(api "/api/coupon/list" "$TOKEN1")
+passert "List available coupons" "$R"
+
+CID=$(json_get "$R" "data.0.id")
+if [ -n "$CID" ]; then
+  R=$(api "/api/coupon/claim" "$TOKEN1" "{\"couponId\":$CID}")
+  passert "Claim coupon #$CID" "$R"
+  UID=$(json_get "$R" "data")
+  echo "  userCouponId: $UID"
+  if [[ "$UID" =~ ^[0-9]+$ ]]; then
+    R=$(api "/api/coupon/apply" "$TOKEN1" \
+      "{\"userCouponId\":$UID,\"orderType\":\"PRODUCT\",\"orderAmount\":10000}")
+    passert "Apply coupon" "$R"
+  fi
+else
+  echo "  INFO: no coupons available — claim skipped (expected on fresh system)"
+  echo "  PASS: coupon list returned (no coupons to claim)"
+  ((PASS++))
+fi
+
+# ── Summary ──────────────────────────────────────────────────────────
+
+header "Results"
+TOTAL=$((PASS + FAIL))
+echo "  Total: $TOTAL | Passed: $PASS | Failed: $FAIL"
+[ "$FAIL" -eq 0 ] && echo "  ✅ ALL TESTS PASSED" || echo "  ❌ $FAIL TEST(S) FAILED"
+exit $(( FAIL > 0 ))