面向 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)
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java:39-60
[ ] 步骤 1:编写测试用例(手工验证)
# 编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 2:添加注入字段
在 CommissionService 类中已有的注入字段后追加:
@Resource
private ProductOrderMapper productOrderMapper;
@Resource
private ReferralTreeMapper referralTreeMapper;
@Resource
private CfReferralService cfReferralService;
[ ] 步骤 3:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "feat: 注入 ProductOrderMapper、ReferralTreeMapper、CfReferralService 到 CommissionService"
getTeamConsumption(团队消费统计)文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java(在 getTeamStats 方法后,约第 599 行后追加)
[ ] 步骤 1:编写 curl 测试
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:实现方法
/**
* 团队总消费额分级汇总(按 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:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "feat: 实现 getTeamConsumption 团队消费统计(按 referral_tree level 聚合,fulfillStatus=completed)"
getMyReferrer(我的推荐人)文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java(在 getTeamConsumption 后新增)
[ ] 步骤 1:实现方法
/**
* 获取我的推荐人信息(直推人)
*/
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:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "feat: 实现 getMyReferrer 获取我的直推人信息"
getTeamTree(树形扁平数据)文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java(在 getMyReferrer 后新增)
[ ] 步骤 1:实现方法
/**
* 获取团队树形扁平数据(不含根节点,仅 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:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "feat: 实现 getTeamTree 树形扁平数据(含 memberConsumption、myCfEarnedFromMember)"
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()) 循环之前,插入预查逻辑(替换现有循环内的逐成员查询):
// 预查所有成员的 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);
}
}
}
找到现有循环内:
int commissionEarned = commissionRecordMapper.selectList(commissionWrapper).stream()
.mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
.sum();
memberInfo.put("commissionEarned", commissionEarned);
替换为:
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:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 4:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
git commit -m "feat: 扩展 getTeamList 新增 memberConsumption、myCfEarnedFromMember 字段"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java(在类末尾前追加 3 个方法)
[ ] 步骤 1:追加 3 个新端点
/**
* 团队消费额分级汇总
*/
@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:编译验证
cd cfc-backend && mvn clean compile
[ ] 步骤 3:Commit
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 接口"
文件:
修改:docs/superpowers/api/API_REFERENCE.md(在 4.21 佣金管理章节后追加)
[ ] 步骤 1:追加新接口文档
在 ### 4.21 佣金管理(/api/commission/*) 之后新增:
### 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
git add docs/superpowers/api/API_REFERENCE.md
git commit -m "docs: 更新 API_REFERENCE.md 新增团队消费与推荐关系接口"
文件:
修改:cfc-frontend/pages/wealth/index.vue:161-165
[ ] 步骤 1:删除模板中的 referral-code-bar 区块
- <!-- 邀请码复制条 -->
- <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 相关样式
- /* 邀请码复制条 */
- .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
git add cfc-frontend/pages/wealth/index.vue
git commit -m "feat(wealth): 删除富页面推荐码横条"
文件:
修改:cfc-frontend/pages/profile-main/profile.vue
[ ] 步骤 1:import 部分追加
// 在现有 import 中追加
import { getMyReferrer } from '../../utils/api.js'
[ ] 步骤 2:data() 追加字段
data() {
return {
// ...existing fields...
myReferrer: null
}
}
[ ] 步骤 3:methods 新增加载与工具方法
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
onShow() {
if (!this.isLoggedIn) return
// ...existing...
this.loadMyReferrer()
}
[ ] 步骤 5:模板中在「个人成就统计」section 前插入以下 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:添加时间格式化方法
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 末尾追加样式
/* 我的推荐人卡片 */
.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
git add cfc-frontend/pages/profile-main/profile.vue
git commit -m "feat(profile): 新增我的推荐人卡片与团队层级入口"
文件:
修改:cfc-frontend/utils/api.js(在现有 commission 相关 export 后追加)
[ ] 步骤 1:追加 3 个 export 函数
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
git add cfc-frontend/utils/api.js
git commit -m "feat(api): 新增 getTeamConsumption、getMyReferrer、getTeamTree"
pages/profile/team-tree.vue文件:
创建:cfc-frontend/pages/profile/team-tree.vue
[ ] 步骤 1:创建完整页面文件
<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:语法检查
cd cfc-frontend && node --check pages/profile/team-tree.vue 2>&1 || true
[ ] 步骤 3:Commit
git add cfc-frontend/pages/profile/team-tree.vue
git commit -m "feat(profile): 新建 team-tree 页面(树形从下往上生长、层级切换、消费/CF明细)"
pages.json 新页面文件:
修改:cfc-frontend/pages.json(在 pages/profile root 下的 pages 数组追加)
[ ] 步骤 1:找到 pages/profile root 并追加
在 pages.json 中找到 "root": "pages/profile" 的 section,在数组末尾追加:
{
"path": "pages/profile/team-tree",
"style": {
"navigationBarTitleText": "团队层级",
"enablePullDownRefresh": false
}
}
[ ] 步骤 2:语法验证
cd cfc-frontend && node -e "JSON.parse(require('fs').readFileSync('pages.json','utf8'))" && echo "JSON valid"
[ ] 步骤 3:Commit
git add cfc-frontend/pages.json
git commit -m "feat(pages.json): 注册 team-tree 页面"
文件:
无新增文件
[ ] 步骤 1:编译后端
cd cfc-backend && mvn clean compile
[ ] 步骤 2:启动后端服务(HBuilderX 或命令行)并手动验证接口
# 后端编译通过后启动,在微信开发者工具中打开小程序测试
# 关键接口验证点:
# 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/,运行到微信开发者工具,依次验证:
空团队 → 显示「暂无团队成员」
[ ] 步骤 4:API 重复调用审计
cd cfc-frontend && node scripts/audit-duplicate-api-calls.js
# 预期:0 error
[ ] 步骤 5:Commit
git add -A
git commit -m "chore: 联调验证通过 + API审计0 error"
/pages/profile/team-treemvn clean compile 通过audit-duplicate-api-calls.js 0 error文档结束