|
@@ -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 已同步更新
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**文档结束**
|