فهرست منبع

docs: 团队层级树实现计划(13 Tasks,后4+前7)

Sisyphus 1 ماه پیش
والد
کامیت
ac456febee
1فایلهای تغییر یافته به همراه1055 افزوده شده و 0 حذف شده
  1. 1055 0
      docs/superpowers/plans/2026-09-09-team-hierarchy.md

+ 1055 - 0
docs/superpowers/plans/2026-09-09-team-hierarchy.md

@@ -0,0 +1,1055 @@
+# 团队层级树与推荐关系可视化 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 实现「我的」页面推荐人卡片、团队层级入口、独立树形页面(从下往上生长),去掉富页面推荐码,新增 3 个后端接口并扩展 1 个现有接口。
+
+**架构:**
+- 后端:在 `CommissionService` 新增 `getTeamConsumption`、`getMyReferrer`、`getTeamTree`,扩展 `getTeamList`;`CommissionController` 暴露 4 个 POST 接口
+- 前端:删除 `wealth/index.vue` 推荐码横条;`profile/profile.vue` 新增推荐人卡片 + 入口按钮;新建 `profile/team-tree.vue` 树形页面(flex column-reverse + 递归组件);`utils/api.js` 新增 3 个方法;`pages.json` 注册新页面
+
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus / uni-app Vue 2 (Options API)
+
+---
+
+### 任务 1:后端 - 新增 Mapper 依赖注入
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java:39-60`
+
+- [ ] **步骤 1:编写测试用例(手工验证)**
+
+```bash
+# 编译验证
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 2:添加注入字段**
+
+在 `CommissionService` 类中已有的注入字段后追加:
+
+```java
+    @Resource
+    private ProductOrderMapper productOrderMapper;
+
+    @Resource
+    private ReferralTreeMapper referralTreeMapper;
+
+    @Resource
+    private CfReferralService cfReferralService;
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "feat: 注入 ProductOrderMapper、ReferralTreeMapper、CfReferralService 到 CommissionService"
+```
+
+---
+
+### 任务 2:后端 - 实现 `getTeamConsumption`(团队消费统计)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(在 `getTeamStats` 方法后,约第 599 行后追加)
+
+- [ ] **步骤 1:编写 curl 测试**
+
+```bash
+curl -X POST http://localhost:9082/api/commission/team/consumption \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d '{"maxLevel": 3}'
+# 预期:{"code":200,"data":{"totalConsumption":...,"levelBreakdown":[...],"totalMembers":...,"maxLevel":3}}
+```
+
+- [ ] **步骤 2:实现方法**
+
+```java
+    /**
+     * 团队总消费额分级汇总(按 referral_tree level 聚合)
+     * maxLevel 默认 3,可后台配置
+     */
+    public Map<String, Object> getTeamConsumption(Long userId, Integer maxLevel) {
+        if (maxLevel == null || maxLevel <= 0) maxLevel = 3;
+        if (maxLevel > 10) maxLevel = 10; // 安全上限
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        List<Map<String, Object>> levelBreakdown = new ArrayList<>();
+
+        // 1. 查 referral_tree: parentId = userId 且 level <= maxLevel
+        List<ReferralTree> treeNodes = referralTreeMapper.selectList(
+                new LambdaQueryWrapper<ReferralTree>()
+                        .eq(ReferralTree::getParentId, userId)
+                        .le(ReferralTree::getLevel, maxLevel)
+                        .orderByAsc(ReferralTree::getLevel)
+        );
+
+        if (treeNodes.isEmpty()) {
+            result.put("totalConsumption", 0);
+            result.put("levelBreakdown", levelBreakdown);
+            result.put("totalMembers", 0);
+            result.put("maxLevel", maxLevel);
+            return result;
+        }
+
+        // 2. 按 level 分组收集 childId
+        Map<Integer, List<Long>> levelToChildIds = new LinkedHashMap<>();
+        for (ReferralTree node : treeNodes) {
+            levelToChildIds.computeIfAbsent(node.getLevel(), k -> new ArrayList<>()).add(node.getChildId());
+        }
+
+        // 3. 查 product_orders: buyerId IN childIds 且 fulfillStatus = 'completed'
+        int totalConsumption = 0;
+        for (Map.Entry<Integer, List<Long>> entry : levelToChildIds.entrySet()) {
+            Integer level = entry.getKey();
+            List<Long> childIds = entry.getValue();
+            if (childIds.isEmpty()) continue;
+
+            LambdaQueryWrapper<ProductOrder> orderWrapper = new LambdaQueryWrapper<ProductOrder>()
+                    .in(ProductOrder::getBuyerId, childIds)
+                    .eq(ProductOrder::getFulfillStatus, "completed");
+            Long sum = productOrderMapper.selectList(orderWrapper).stream()
+                    .mapToLong(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0)
+                    .sum();
+
+            int memberCount = childIds.size();
+            totalConsumption += sum;
+
+            Map<String, Object> levelInfo = new LinkedHashMap<>();
+            levelInfo.put("level", level);
+            levelInfo.put("memberCount", memberCount);
+            levelInfo.put("consumption", (int) sum);
+            levelBreakdown.add(levelInfo);
+        }
+
+        result.put("totalConsumption", totalConsumption);
+        result.put("levelBreakdown", levelBreakdown);
+        result.put("totalMembers", treeNodes.size());
+        result.put("maxLevel", maxLevel);
+        return result;
+    }
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "feat: 实现 getTeamConsumption 团队消费统计(按 referral_tree level 聚合,fulfillStatus=completed)"
+```
+
+---
+
+### 任务 3:后端 - 实现 `getMyReferrer`(我的推荐人)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(在 getTeamConsumption 后新增)
+
+- [ ] **步骤 1:实现方法**
+
+```java
+    /**
+     * 获取我的推荐人信息(直推人)
+     */
+    public Map<String, Object> getMyReferrer(Long userId) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getReferrerId() == null) {
+            result.put("hasReferrer", false);
+            return result;
+        }
+        User referrer = userMapper.selectById(user.getReferrerId());
+        if (referrer == null) {
+            result.put("hasReferrer", false);
+            return result;
+        }
+        result.put("hasReferrer", true);
+        result.put("referrerId", referrer.getId());
+        result.put("nickname", referrer.getNickname());
+        result.put("avatar", referrer.getAvatar());
+        result.put("referralCode", referrer.getReferralCode());
+        result.put("bindTime", user.getCreatedAt()); // 以用户创建时间作为绑定时间近似
+        return result;
+    }
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "feat: 实现 getMyReferrer 获取我的直推人信息"
+```
+
+---
+
+### 任务 4:后端 - 实现 `getTeamTree`(树形扁平数据)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(在 getMyReferrer 后新增)
+
+- [ ] **步骤 1:实现方法**
+
+```java
+    /**
+     * 获取团队树形扁平数据(不含根节点,仅 level>=1 成员)
+     * 每个节点包含 memberConsumption、myCfEarnedFromMember
+     */
+    public Map<String, Object> getTeamTree(Long userId, Integer maxLevel) {
+        if (maxLevel == null || maxLevel <= 0) maxLevel = 3;
+        if (maxLevel > 10) maxLevel = 10;
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        List<Map<String, Object>> nodes = new ArrayList<>();
+
+        // 1. 查 referral_tree
+        List<ReferralTree> treeNodes = referralTreeMapper.selectList(
+                new LambdaQueryWrapper<ReferralTree>()
+                        .eq(ReferralTree::getParentId, userId)
+                        .le(ReferralTree::getLevel, maxLevel)
+                        .orderByAsc(ReferralTree::getLevel)
+        );
+
+        if (treeNodes.isEmpty()) {
+            result.put("nodes", nodes);
+            result.put("totalMembers", 0);
+            result.put("maxLevel", maxLevel);
+            return result;
+        }
+
+        // 2. 批量查用户基本信息
+        List<Long> childIds = treeNodes.stream().map(ReferralTree::getChildId).distinct().collect(Collectors.toList());
+        Map<Long, User> userMap = userMapper.selectBatchIds(childIds).stream()
+                .collect(Collectors.toMap(User::getId, u -> u));
+
+        // 3. 按成员聚合 memberConsumption(fulfillStatus=completed 订单总额)
+        Map<Long, Integer> memberConsumptionMap = new HashMap<>();
+        if (!childIds.isEmpty()) {
+            List<ProductOrder> orders = productOrderMapper.selectList(
+                    new LambdaQueryWrapper<ProductOrder>()
+                            .in(ProductOrder::getBuyerId, childIds)
+                            .eq(ProductOrder::getFulfillStatus, "completed")
+            );
+            for (ProductOrder o : orders) {
+                if (o.getBuyerId() != null && o.getTotalAmount() != null) {
+                    memberConsumptionMap.merge(o.getBuyerId(), o.getTotalAmount(), Integer::sum);
+                }
+            }
+        }
+
+        // 4. 按成员聚合 myCfEarnedFromMember(commission_records 已结算+待结算,排除 cancelled)
+        Map<Long, Integer> myCfEarnedMap = new HashMap<>();
+        if (!childIds.isEmpty()) {
+            List<CommissionRecord> records = commissionRecordMapper.selectList(
+                    new LambdaQueryWrapper<CommissionRecord>()
+                            .eq(CommissionRecord::getReferrerId, userId)
+                            .in(CommissionRecord::getBuyerId, childIds)
+                            .ne(CommissionRecord::getStatus, "cancelled")
+            );
+            for (CommissionRecord r : records) {
+                if (r.getBuyerId() != null && r.getCommissionAmount() != null) {
+                    myCfEarnedMap.merge(r.getBuyerId(), r.getCommissionAmount(), Integer::sum);
+                }
+            }
+        }
+
+        // 5. 组装 nodes(不含根节点,仅 level>=1 的成员)
+        for (ReferralTree node : treeNodes) {
+            User u = userMap.get(node.getChildId());
+            if (u == null) continue;
+
+            Map<String, Object> n = new LinkedHashMap<>();
+            n.put("userId", node.getChildId());
+            n.put("nickname", u.getNickname());
+            n.put("avatar", u.getAvatar());
+            n.put("level", node.getLevel());
+            n.put("parentId", node.getParentId());
+            n.put("referrerId", u.getReferrerId());
+            n.put("memberConsumption", memberConsumptionMap.getOrDefault(node.getChildId(), 0));
+            n.put("myCfEarnedFromMember", myCfEarnedMap.getOrDefault(node.getChildId(), 0));
+            nodes.add(n);
+        }
+
+        result.put("nodes", nodes);
+        result.put("totalMembers", nodes.size());
+        result.put("maxLevel", maxLevel);
+        return result;
+    }
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "feat: 实现 getTeamTree 树形扁平数据(含 memberConsumption、myCfEarnedFromMember)"
+```
+
+---
+
+### 任务 5:后端 - 扩展 `getTeamList`(新增消费/CF 字段)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(`getTeamList` 方法,约第 460-543 行)
+
+- [ ] **步骤 1:在 getTeamList 中,循环查询 memberConsumption 和 myCfEarnedFromMember 之前,改为预查所有成员的汇总**
+
+在现有 `getTeamList` 方法中,找到 `for (Map.Entry<Long, Map<String, Object>> entry : memberMap.entrySet())` 循环**之前**,插入预查逻辑(替换现有循环内的逐成员查询):
+
+```java
+        // 预查所有成员的 memberConsumption 和 myCfEarnedFromMember
+        Set<Long> allBuyerIds = memberMap.keySet();
+        Map<Long, Integer> memberConsumptionMap = new HashMap<>();
+        Map<Long, Integer> myCfEarnedMap = new HashMap<>();
+
+        if (!allBuyerIds.isEmpty()) {
+            // memberConsumption:该成员已完成履约订单总额
+            LambdaQueryWrapper<ProductOrder> consumptionWrapper = new LambdaQueryWrapper<ProductOrder>()
+                    .in(ProductOrder::getBuyerId, allBuyerIds)
+                    .eq(ProductOrder::getFulfillStatus, "completed");
+            for (ProductOrder o : productOrderMapper.selectList(consumptionWrapper)) {
+                if (o.getBuyerId() != null && o.getTotalAmount() != null) {
+                    memberConsumptionMap.merge(o.getBuyerId(), o.getTotalAmount(), Integer::sum);
+                }
+            }
+            // myCfEarnedFromMember:我从该成员消费获得的 CF 总额
+            LambdaQueryWrapper<CommissionRecord> cfWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                    .eq(CommissionRecord::getReferrerId, userId)
+                    .in(CommissionRecord::getBuyerId, allBuyerIds)
+                    .ne(CommissionRecord::getStatus, "cancelled");
+            if ("L1".equals(level)) cfWrapper.eq(CommissionRecord::getLevel, 1);
+            else if ("L2".equals(level)) cfWrapper.eq(CommissionRecord::getLevel, 2);
+            SortUtil.applySort(cfWrapper);
+            for (CommissionRecord r : commissionRecordMapper.selectList(cfWrapper)) {
+                if (r.getBuyerId() != null && r.getCommissionAmount() != null) {
+                    myCfEarnedMap.merge(r.getBuyerId(), r.getCommissionAmount(), Integer::sum);
+                }
+            }
+        }
+```
+
+- [ ] **步骤 2:在现有循环内部,用预查结果替换原来的逐成员查询,加入新字段**
+
+找到现有循环内:
+```java
+            int commissionEarned = commissionRecordMapper.selectList(commissionWrapper).stream()
+                    .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                    .sum();
+            memberInfo.put("commissionEarned", commissionEarned);
+```
+替换为:
+```java
+            memberInfo.put("memberConsumption", memberConsumptionMap.getOrDefault(buyerId, 0));
+            memberInfo.put("myCfEarnedFromMember", myCfEarnedMap.getOrDefault(buyerId, 0));
+            // 保留原有 commissionEarned(用于展示佣金)
+            int commissionEarned = commissionRecordMapper.selectList(commissionWrapper).stream()
+                    .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                    .sum();
+            memberInfo.put("commissionEarned", commissionEarned);
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "feat: 扩展 getTeamList 新增 memberConsumption、myCfEarnedFromMember 字段"
+```
+
+---
+
+### 任务 6:后端 - 新增 Controller 端点
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java`(在类末尾前追加 3 个方法)
+
+- [ ] **步骤 1:追加 3 个新端点**
+
+```java
+    /**
+     * 团队消费额分级汇总
+     */
+    @PostMapping("/team/consumption")
+    public Result<Map<String, Object>> teamConsumption(@RequestAttribute("userId") Long userId,
+                                                        @RequestBody Map<String, Object> params) {
+        Integer maxLevel = params.get("maxLevel") != null
+                ? ((Number) params.get("maxLevel")).intValue()
+                : 3;
+        return Result.success(commissionService.getTeamConsumption(userId, maxLevel));
+    }
+
+    /**
+     * 获取我的推荐人
+     */
+    @PostMapping("/my-referrer")
+    public Result<Map<String, Object>> myReferrer(@RequestAttribute("userId") Long userId) {
+        return Result.success(commissionService.getMyReferrer(userId));
+    }
+
+    /**
+     * 团队树形扁平数据
+     */
+    @PostMapping("/team/tree")
+    public Result<Map<String, Object>> teamTree(@RequestAttribute("userId") Long userId,
+                                                 @RequestBody Map<String, Object> params) {
+        Integer maxLevel = params.get("maxLevel") != null
+                ? ((Number) params.get("maxLevel")).intValue()
+                : 3;
+        return Result.success(commissionService.getTeamTree(userId, maxLevel));
+    }
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java
+git commit -m "feat: 新增 /api/commission/team/consumption、/my-referrer、/team/tree 接口"
+```
+
+---
+
+### 任务 7:后端 - 更新 API_REFERENCE.md
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`(在 4.21 佣金管理章节后追加)
+
+- [ ] **步骤 1:追加新接口文档**
+
+在 `### 4.21 佣金管理(/api/commission/*)` 之后新增:
+
+```markdown
+### 4.22 团队消费与推荐关系(`/api/commission/*` 扩展)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/commission/team/list` | 扩展:返回中每个成员新增 `memberConsumption`(该成员自身完成履约订单总额,分)、`myCfEarnedFromMember`(我从该成员消费获得的 CF 总额,分) |
+| `POST /api/commission/team/consumption` | 团队总消费额分级汇总,请求 `{maxLevel}`(默认 3),返回 `{totalConsumption: 分, levelBreakdown[{level, memberCount, consumption}], totalMembers, maxLevel}` |
+| `POST /api/commission/my-referrer` | 获取我的直推人信息,返回 `{hasReferrer: bool, referrerId, nickname, avatar, referralCode, bindTime}`,无推荐人时 hasReferrer=false |
+| `POST /api/commission/team/tree` | 团队树形扁平数据(不含根节点),请求 `{maxLevel}`(默认 3),返回 `{nodes[{userId, nickname, avatar, level, parentId, referrerId, memberConsumption, myCfEarnedFromMember}], totalMembers, maxLevel}` |
+```
+
+- [ ] **步骤 2:Commit**
+
+```bash
+git add docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs: 更新 API_REFERENCE.md 新增团队消费与推荐关系接口"
+```
+
+---
+
+### 任务 8:前端 - 删除富页面推荐码横条
+
+**文件:**
+- 修改:`cfc-frontend/pages/wealth/index.vue:161-165`
+
+- [ ] **步骤 1:删除模板中的 `referral-code-bar` 区块**
+
+```diff
+-       <!-- 邀请码复制条 -->
+-       <view class="referral-code-bar" v-if="wealthData.referralCode" @click="copyReferralCode">
+-         <text class="referral-code-label">我的邀请码</text>
+-         <text class="referral-code-value">{{ wealthData.referralCode }}</text>
+-         <text class="referral-code-copy">复制</text>
+-       </view>
+```
+
+- [ ] **步骤 2:删除 CSS 中 `.referral-code-bar` 相关样式**
+
+```diff
+- /* 邀请码复制条 */
+- .referral-code-bar {
+-   display: flex;
+-   align-items: center;
+-   background: #fff;
+-   margin-top: 16rpx;
+-   border-radius: 16rpx;
+-   padding: 24rpx 30rpx;
+-   box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+- }
+- .referral-code-label { font-size: 26rpx; color: #666; margin-right: 16rpx; }
+- .referral-code-value { flex: 1; font-size: 30rpx; font-weight: bold; color: #F59E0B; letter-spacing: 2rpx; }
+- .referral-code-copy { font-size: 24rpx; color: #fff; background: linear-gradient(135deg, #F59E0B, #FBBF24); padding: 8rpx 24rpx; border-radius: 20rpx; }
+```
+
+- [ ] **步骤 3:删除 methods 中的 `copyReferralCode` 方法(如不再使用)**
+
+检查 `copyReferralCode` 是否在其他地方使用,若未使用则删除。
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/pages/wealth/index.vue
+git commit -m "feat(wealth): 删除富页面推荐码横条"
+```
+
+---
+
+### 任务 9:前端 - 我的页面新增推荐人卡片 + 团队入口
+
+**文件:**
+- 修改:`cfc-frontend/pages/profile-main/profile.vue`
+
+- [ ] **步骤 1:import 部分追加**
+
+```js
+// 在现有 import 中追加
+import { getMyReferrer } from '../../utils/api.js'
+```
+
+- [ ] **步骤 2:data() 追加字段**
+
+```js
+data() {
+  return {
+    // ...existing fields...
+    myReferrer: null
+  }
+}
+```
+
+- [ ] **步骤 3:methods 新增加载与工具方法**
+
+```js
+methods: {
+  // ...existing methods...
+  async loadMyReferrer() {
+    var self = this
+    try {
+      var res = await getMyReferrer()
+      if (res && res.code === 200 && res.data && res.data.hasReferrer) {
+        self.myReferrer = res.data
+      }
+    } catch (e) {
+      console.error('获取推荐人失败', e)
+    }
+  },
+  goTeamTree() {
+    uni.navigateTo({ url: '/pages/profile/team-tree' })
+  },
+  copyReferrerCode() {
+    if (!this.myReferrer || !this.myReferrer.referralCode) return
+    uni.setClipboardData({
+      data: this.myReferrer.referralCode,
+      success: function() {
+        uni.showToast({ title: '邀请码已复制', icon: 'success' })
+      }
+    })
+  }
+}
+```
+
+- [ ] **步骤 4:onShow 中调用 loadMyReferrer**
+
+```js
+onShow() {
+  if (!this.isLoggedIn) return
+  // ...existing...
+  this.loadMyReferrer()
+}
+```
+
+- [ ] **步骤 5:模板中在「个人成就统计」section 前插入以下 HTML**
+
+```html
+<!-- 我的推荐人卡片 -->
+<view class="referrer-card" v-if="myReferrer && myReferrer.hasReferrer">
+  <view class="referrer-header">
+    <text class="referrer-title">我的推荐人</text>
+  </view>
+  <view class="referrer-content">
+    <view class="referrer-avatar-wrap">
+      <image v-if="myReferrer.avatar" class="referrer-avatar" :src="myReferrer.avatar" mode="aspectFill"></image>
+      <text v-else class="referrer-avatar-text">{{ myReferrer.nickname ? myReferrer.nickname.charAt(0) : '?' }}</text>
+    </view>
+    <view class="referrer-info">
+      <text class="referrer-name">{{ myReferrer.nickname }}</text>
+      <text class="referrer-code">邀请码:{{ myReferrer.referralCode }}</text>
+      <text class="referrer-time" v-if="myReferrer.bindTime">{{ formatReferralTime(myReferrer.bindTime) }}</text>
+    </view>
+    <view class="referrer-copy-btn" @click="copyReferrerCode">
+      <text>复制</text>
+    </view>
+  </view>
+</view>
+
+<!-- 团队层级入口 -->
+<view class="team-tree-entry" @click="goTeamTree">
+  <view class="entry-icon">🌳</view>
+  <view class="entry-text">
+    <text class="entry-title">团队层级</text>
+    <text class="entry-desc">查看团队结构与消费明细</text>
+  </view>
+  <text class="entry-arrow">›</text>
+</view>
+```
+
+- [ ] **步骤 6:添加时间格式化方法**
+
+```js
+methods: {
+  // ...existing...
+  formatReferralTime(ts) {
+    if (!ts) return ''
+    var d = ts
+    if (typeof d === 'string' && d.indexOf('T') > -1) {
+      d = d.substring(0, 16).replace('T', ' ')
+    }
+    return '绑定于 ' + d
+  }
+}
+```
+
+- [ ] **步骤 7:在 style 末尾追加样式**
+
+```css
+/* 我的推荐人卡片 */
+.referrer-card {
+  margin: 0 30rpx 20rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.referrer-header { margin-bottom: 16rpx; }
+.referrer-title { font-size: 28rpx; font-weight: 600; color: #1E293B; }
+.referrer-content { display: flex; align-items: center; gap: 20rpx; }
+.referrer-avatar-wrap {
+  width: 72rpx; height: 72rpx; border-radius: 50%; background: #FEF3C7;
+  display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0;
+}
+.referrer-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; }
+.referrer-avatar-text { font-size: 28rpx; color: #F97316; }
+.referrer-info { flex: 1; min-width: 0; }
+.referrer-name { font-size: 28rpx; font-weight: 500; color: #1E293B; display: block; }
+.referrer-code { font-size: 24rpx; color: #F97316; margin-top: 4rpx; display: block; }
+.referrer-time { font-size: 22rpx; color: #94A3B8; margin-top: 4rpx; display: block; }
+.referrer-copy-btn {
+  font-size: 26rpx; color: #F97316; padding: 8rpx 16rpx; border: 1rpx solid #FED7AA;
+  border-radius: 20rpx; flex-shrink: 0;
+}
+.team-tree-entry {
+  margin: 0 30rpx 20rpx;
+  background: linear-gradient(135deg, #FFF7ED, rgba(249,115,22,0.06));
+  border-radius: 20rpx; padding: 24rpx;
+  display: flex; align-items: center; gap: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+  border-left: 6rpx solid #F97316;
+}
+.entry-icon { font-size: 40rpx; }
+.entry-text { flex: 1; min-width: 0; }
+.entry-title { font-size: 28rpx; font-weight: 600; color: #1E293B; display: block; }
+.entry-desc { font-size: 22rpx; color: #94A3B8; margin-top: 4rpx; display: block; }
+.entry-arrow { font-size: 28rpx; color: #F97316; }
+```
+
+- [ ] **步骤 8:Commit**
+
+```bash
+git add cfc-frontend/pages/profile-main/profile.vue
+git commit -m "feat(profile): 新增我的推荐人卡片与团队层级入口"
+```
+
+---
+
+### 任务 10:前端 - 新增 api.js 方法
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`(在现有 commission 相关 export 后追加)
+
+- [ ] **步骤 1:追加 3 个 export 函数**
+
+```javascript
+export const getTeamConsumption = (maxLevel = 3) => request('/api/commission/team/consumption', 'POST', { maxLevel })
+
+export const getMyReferrer = () => request('/api/commission/my-referrer', 'POST')
+
+export const getTeamTree = (maxLevel = 3) => request('/api/commission/team/tree', 'POST', { maxLevel })
+```
+
+- [ ] **步骤 2:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat(api): 新增 getTeamConsumption、getMyReferrer、getTeamTree"
+```
+
+---
+
+### 任务 11:前端 - 新建 `pages/profile/team-tree.vue`
+
+**文件:**
+- 创建:`cfc-frontend/pages/profile/team-tree.vue`
+
+- [ ] **步骤 1:创建完整页面文件**
+
+```vue
+<template>
+  <view class="container">
+    <!-- 顶部统计卡 -->
+    <view class="stats-card">
+      <view class="stat-item">
+        <text class="stat-value orange">{{ fenToYuan(consumption.totalConsumption) }}</text>
+        <text class="stat-label">团队总消费</text>
+      </view>
+      <view class="stat-divider"></view>
+      <view class="stat-item">
+        <text class="stat-value blue">{{ consumption.totalMembers }}</text>
+        <text class="stat-label">团队人数</text>
+      </view>
+      <view class="stat-divider"></view>
+      <view class="stat-item">
+        <text class="stat-value green">{{ currentMaxLevel }}</text>
+        <text class="stat-label">统计层级</text>
+      </view>
+    </view>
+
+    <!-- 层级切换 tabs -->
+    <view class="level-tabs">
+      <view class="level-tab" :class="{ active: currentMaxLevel === 1 }" @click="switchLevel(1)">1级</view>
+      <view class="level-tab" :class="{ active: currentMaxLevel === 2 }" @click="switchLevel(2)">2级</view>
+      <view class="level-tab" :class="{ active: currentMaxLevel === 3 }" @click="switchLevel(3)">3级</view>
+      <view class="level-tab" :class="{ active: currentMaxLevel === 4 }" @click="switchLevel(4)">4级</view>
+      <view class="level-tab" :class="{ active: currentMaxLevel === 5 }" @click="switchLevel(5)">5级</view>
+    </view>
+
+    <!-- 树形区域(从下往上生长:column-reverse,L1 在底部) -->
+    <view class="tree-wrapper">
+      <view class="tree-container">
+        <TreeNode
+          v-for="root in level1Roots"
+          :key="root.userId"
+          :node="root"
+          :level="1"
+          :maxLevel="currentMaxLevel"
+        />
+      </view>
+      <view class="empty-state" v-if="!loading && level1Roots.length === 0">
+        <text>暂无团队成员</text>
+      </view>
+    </view>
+
+    <view class="loading-overlay" v-if="loading">
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getTeamTree, getTeamConsumption } from '@/utils/api.js'
+
+export default {
+  components: { TreeNode: {
+    name: 'TreeNode',
+    props: { node: Object, level: Number, maxLevel: Number },
+    data() { return { expanded: false } },
+    methods: {
+      toggle() { this.expanded = !this.expanded }
+    },
+    template: `
+      <view class="tree-node" :style="{ paddingLeft: (level > 1 ? (level - 1) * 24 : 0) + 'rpx' }">
+        <view class="node-main" @click="toggle">
+          <image v-if="node.avatar" class="node-avatar" :src="node.avatar" mode="aspectFill" />
+          <text v-else class="node-avatar-text">{{ node.nickname ? node.nickname.charAt(0) : '?' }}</text>
+          <text class="node-name">{{ node.nickname }}</text>
+          <text class="node-tag" v-if="level > 1">L{{ level }}</text>
+          <view class="node-stats">
+            <text class="node-stat consumption">消费 ¥{{ (node.memberConsumption / 100).toFixed(2) }}</text>
+            <text class="node-stat my-cf">得CF ¥{{ (node.myCfEarnedFromMember / 100).toFixed(2) }}</text>
+          </view>
+          <text class="node-arrow">{{ expanded ? '▲' : '▼' }}</text>
+        </view>
+        <view v-if="expanded && node.children && node.children.length > 0" class="children">
+          <TreeNode
+            v-for="child in node.children"
+            :key="child.userId"
+            :node="child"
+            :level="level + 1"
+            :maxLevel="maxLevel"
+          />
+        </view>
+      </view>
+    `
+  }},
+  data() {
+    return {
+      nodes: [],
+      level1Roots: [],
+      consumption: { totalConsumption: 0, totalMembers: 0, maxLevel: 3 },
+      currentMaxLevel: 3,
+      loading: false
+    }
+  },
+  onLoad() {
+    this.loadData()
+  },
+  onShow() {
+    if (!this._loadFired) {
+      this._loadFired = true
+    } else {
+      this.loadData()
+    }
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        var self = this
+        var treeRes = await getTeamTree(self.currentMaxLevel)
+        var consumptionRes = await getTeamConsumption(self.currentMaxLevel)
+        if (treeRes && treeRes.data && treeRes.data.nodes) {
+          self.nodes = treeRes.data.nodes
+          self.buildLevel1Roots()
+        }
+        if (consumptionRes && consumptionRes.data) {
+          self.consumption = consumptionRes.data
+          self.consumption.maxLevel = self.consumption.maxLevel || self.currentMaxLevel
+        }
+      } catch (e) {
+        console.error('加载团队树失败', e)
+      }
+      this.loading = false
+    },
+    switchLevel(level) {
+      if (this.currentMaxLevel === level) return
+      this.currentMaxLevel = level
+      this.loadData()
+    },
+    buildLevel1Roots() {
+      var self = this
+      // 将扁平 nodes 构建成以 L1 为根的子树
+      var nodeMap = {}
+      var l1Ids = []
+      self.nodes.forEach(function(n) {
+        nodeMap[n.userId] = { userId: n.userId, children: [] }
+        if (n.level === 1) l1Ids.push(n.userId)
+      })
+      self.nodes.forEach(function(n) {
+        if (!nodeMap[n.userId]) return
+        nodeMap[n.userId] = Object.assign({}, nodeMap[n.userId], n)
+        if (n.parentId && nodeMap[n.parentId]) {
+          nodeMap[n.parentId].children.push(nodeMap[n.userId])
+        }
+      })
+      // 按 userId 排序
+      var sortNodes = function(arr) {
+        arr.sort(function(a, b) { return (a.userId || 0) - (b.userId || 0) })
+        arr.forEach(function(c) { sortNodes(c.children) })
+      }
+      sortNodes(l1Ids.map(function(id) { return nodeMap[id] }))
+      self.level1Roots = l1Ids.map(function(id) { return nodeMap[id] })
+    },
+    fenToYuan(fen) {
+      if (fen === null || fen === undefined) return '0.00'
+      var num = Number(fen)
+      if (isNaN(num)) return '0.00'
+      return (num / 100).toFixed(2)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #FFF7ED; padding-bottom: 40rpx; }
+.stats-card {
+  background: #fff; border-radius: 20rpx; padding: 30rpx; margin: 20rpx 30rpx 16rpx;
+  display: flex; align-items: center; box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.stat-item { flex: 1; text-align: center; }
+.stat-value { font-size: 36rpx; font-weight: bold; display: block; }
+.stat-value.orange { color: #F97316; }
+.stat-value.blue { color: #0EA5E9; }
+.stat-value.green { color: #10B981; }
+.stat-label { font-size: 22rpx; color: #94A3B8; margin-top: 4rpx; display: block; }
+.stat-divider { width: 1rpx; height: 48rpx; background: #FED7AA; }
+.level-tabs {
+  display: flex; padding: 0 30rpx; margin-bottom: 16rpx; overflow-x: auto; gap: 12rpx;
+  white-space: nowrap;
+}
+.level-tab {
+  flex-shrink: 0; padding: 12rpx 24rpx; border-radius: 20rpx;
+  font-size: 24rpx; color: #64748B; background: #F1F5F9;
+}
+.level-tab.active { background: #F97316; color: #fff; font-weight: 600; }
+.tree-wrapper { padding: 0 30rpx; }
+.tree-container { min-height: 300rpx; }
+.tree-node { margin-bottom: 16rpx; }
+.node-main {
+  display: flex; align-items: center; gap: 16rpx;
+  background: #fff; border-radius: 16rpx; padding: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.node-avatar { width: 60rpx; height: 60rpx; border-radius: 50%; background: #FEF3C7; flex-shrink: 0; }
+.node-avatar-text {
+  width: 60rpx; height: 60rpx; border-radius: 50%; background: #FEF3C7;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 24rpx; color: #F97316; flex-shrink: 0;
+}
+.node-name { flex: 1; min-width: 0; font-size: 26rpx; color: #1E293B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.node-tag { font-size: 20rpx; background: #FFF7ED; color: #F97316; padding: 2rpx 8rpx; border-radius: 8rpx; flex-shrink: 0; }
+.node-stats { flex-shrink: 0; text-align: right; margin-right: 8rpx; }
+.node-stat { font-size: 20rpx; display: block; line-height: 1.5; }
+.node-stat.consumption { color: #F97316; }
+.node-stat.my-cf { color: #0EA5E9; }
+.node-arrow { font-size: 20rpx; color: #94A3B8; flex-shrink: 0; }
+.children { margin-top: 12rpx; padding-left: 32rpx; border-left: 1rpx dashed #FED7AA; }
+.empty-state { text-align: center; padding: 80rpx; color: #94A3B8; font-size: 28rpx; }
+.loading-overlay {
+  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(255,255,255,0.85); display: flex; align-items: center; justify-content: center;
+  z-index: 999; font-size: 28rpx; color: #94A3B8;
+}
+</style>
+```
+
+- [ ] **步骤 2:语法检查**
+
+```bash
+cd cfc-frontend && node --check pages/profile/team-tree.vue 2>&1 || true
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/pages/profile/team-tree.vue
+git commit -m "feat(profile): 新建 team-tree 页面(树形从下往上生长、层级切换、消费/CF明细)"
+```
+
+---
+
+### 任务 12:前端 - 注册 `pages.json` 新页面
+
+**文件:**
+- 修改:`cfc-frontend/pages.json`(在 `pages/profile` root 下的 `pages` 数组追加)
+
+- [ ] **步骤 1:找到 `pages/profile` root 并追加**
+
+在 `pages.json` 中找到 `"root": "pages/profile"` 的 section,在数组末尾追加:
+
+```json
+{
+  "path": "pages/profile/team-tree",
+  "style": {
+    "navigationBarTitleText": "团队层级",
+    "enablePullDownRefresh": false
+  }
+}
+```
+
+- [ ] **步骤 2:语法验证**
+
+```bash
+cd cfc-frontend && node -e "JSON.parse(require('fs').readFileSync('pages.json','utf8'))" && echo "JSON valid"
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/pages.json
+git commit -m "feat(pages.json): 注册 team-tree 页面"
+```
+
+---
+
+### 任务 13:联调验证 + API 审计
+
+**文件:**
+- 无新增文件
+
+- [ ] **步骤 1:编译后端**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+- [ ] **步骤 2:启动后端服务(HBuilderX 或命令行)并手动验证接口**
+
+```bash
+# 后端编译通过后启动,在微信开发者工具中打开小程序测试
+# 关键接口验证点:
+# 1. GET /api/commission/team/consumption?maxLevel=3 → 200
+# 2. POST /api/commission/my-referrer → 200
+# 3. POST /api/commission/team/tree?maxLevel=3 → 200
+# 4. POST /api/commission/team/list?level=L1 → 200, records[].memberConsumption exists
+```
+
+- [ ] **步骤 3:HBuilderX 打包小程序**
+
+在 HBuilderX 中打开 `cfc-frontend/`,运行到微信开发者工具,依次验证:
+- 富页面:无推荐码横条,有「邀请人数」
+- 我的页面:有推荐人卡片(显示昵称/头像/邀请码/复制按钮)、有「团队层级」入口
+- 点击「团队层级」→ 进入 team-tree,显示统计卡 + 层级 tabs + 树形
+- 切换层级 tabs → 树形和统计更新
+- 点击节点 → 展开/折叠
+- 无推荐人用户 → 推荐人卡片不显示
+- 空团队 → 显示「暂无团队成员」
+
+- [ ] **步骤 4:API 重复调用审计**
+
+```bash
+cd cfc-frontend && node scripts/audit-duplicate-api-calls.js
+# 预期:0 error
+```
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add -A
+git commit -m "chore: 联调验证通过 + API审计0 error"
+```
+
+---
+
+## 自检清单(实施后逐条确认)
+
+- [ ] 富页面无推荐码横条,保留「邀请人数」数字
+- [ ] 我的页面有推荐人卡片(有推荐人时显示;无推荐人时不显示)
+- [ ] 我的页面有「团队层级」入口,点击跳转 `/pages/profile/team-tree`
+- [ ] team-tree 页面:
+  - 顶部统计卡:总消费额(分转元)、人数、层级数
+  - 1-5 级切换 tabs,切换后统计和树形同步更新
+  - 树形从下往上生长(L1 在底部,L2/L3 向上延伸)
+  - 每节点显示:头像、昵称、L标签、消费额、我得CF、展开图标
+  - 点击节点展开/折叠,子节点在上方渲染,虚线连接正确
+  - 空团队显示「暂无团队成员」
+- [ ] 复制推荐码按钮正常工作
+- [ ] 后端 4 个接口响应正确,字段名与设计文档一致
+- [ ] `mvn clean compile` 通过
+- [ ] `audit-duplicate-api-calls.js` 0 error
+- [ ] API_REFERENCE.md 已同步更新
+
+---
+
+**文档结束**