Переглянути джерело

chore: auto bump version and changelog [skip ci]

iwt 3 днів тому
батько
коміт
7bdb77d5af

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

@@ -11513,4 +11513,27 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
                 "BIGINT DEFAULT NULL COMMENT '报告归属成员ID(family_members.id),A2/DAN自动采集必需'");
         log.info("迁移343: health_report_drafts.member_id 已确保存在");
     }
+
+    // 迁移344: 创建 supply_customer_relation 表(供应商团队客户归属记录)
+    private void createSupplyCustomerRelationTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_customer_relation (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
+                    "system_id BIGINT NOT NULL COMMENT '供应商体系ID(supply_system.id)'," +
+                    "parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID(supply_hierarchy.parent_id)'," +
+                    "family_id BIGINT NOT NULL COMMENT '客户家庭ID(family_members.family_id)'," +
+                    "child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID(users.id),可为空'," +
+                    "relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family=家庭归属 order=订单关联'," +
+                    "source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual=人工绑定 order=下单自动 referral=推荐码'," +
+                    "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
+                    "INDEX idx_system_family (system_id, family_id)," +
+                    "INDEX idx_parent_user (parent_user_id)," +
+                    "INDEX idx_family_id (family_id)," +
+                    "UNIQUE KEY uk_system_family (system_id, family_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商团队客户归属关系表'");
+            log.info("迁移344: 已创建 supply_customer_relation 表");
+        } catch (Exception e) {
+            log.warn("迁移344: 创建 supply_customer_relation 表失败: {}", e.getMessage());
+        }
+    }
 }

+ 76 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyTeamController.java

@@ -0,0 +1,76 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.SupplyCustomerRelationService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import com.etotem.cfc.util.ParamUtils;
+
+@RestController
+@RequestMapping("/api/admin/supply-team")
+public class SupplyTeamController {
+
+    @Resource
+    private SupplyCustomerRelationService supplyCustomerRelationService;
+
+    /**
+     * 查询上级供应商下的客户列表
+     */
+    @PostMapping("/customers")
+    public Result<List<Map<String, Object>>> listCustomers(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long parentId = ParamUtils.getLong(params.get("parentId"));
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+
+        List<Map<String, Object>> customers = supplyCustomerRelationService.listCustomers(systemId, parentId, page, size);
+        return Result.success(customers);
+    }
+
+    /**
+     * 人工绑定客户归属
+     */
+    @PostMapping("/bind-customer")
+    public Result<Void> bindCustomer(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long parentId = ParamUtils.getLong(params.get("parentId"));
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
+        Long childUserId = ParamUtils.getLong(params.get("childUserId"));
+        String source = (String) params.get("source");
+
+        boolean success = supplyCustomerRelationService.bindCustomer(systemId, parentId, familyId, childUserId, source);
+        return success ? Result.success(null) : Result.error("绑定失败");
+    }
+
+    /**
+     * 查询团队成员列表(含客户数量统计)
+     */
+    @PostMapping("/members")
+    public Result<List<Map<String, Object>>> listTeamMembers(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        List<Map<String, Object>> members = supplyCustomerRelationService.listTeamMembers(systemId);
+        return Result.success(members);
+    }
+
+    /**
+     * 查询某个家庭的归属关系
+     */
+    @PostMapping("/relation")
+    public Result<Object> getRelation(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long systemId = ParamUtils.getLong(params.get("systemId"));
+        Long familyId = ParamUtils.getLong(params.get("familyId"));
+        var relation = supplyCustomerRelationService.getRelationByFamily(systemId, familyId);
+        return Result.success(relation);
+    }
+}

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SupplyCustomerRelation.java

@@ -0,0 +1,22 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("supply_customer_relation")
+public class SupplyCustomerRelation implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long systemId;
+    private Long parentUserId;
+    private Long familyId;
+    private Long childUserId;
+    private String relationType;
+    private String source;
+    private Date createdAt;
+}

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SupplyCustomerRelationMapper.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface SupplyCustomerRelationMapper extends BaseMapper<SupplyCustomerRelation> {
+    /** 根据体系ID和上级用户ID查询所有客户 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.system_id = #{systemId} AND r.parent_user_id = #{parentId}")
+    List<SupplyCustomerRelation> listByParent(@Param("systemId") Long systemId, @Param("parentId") Long parentId);
+
+    /** 根据体系ID查询所有客户归属记录 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.system_id = #{systemId}")
+    List<SupplyCustomerRelation> listBySystem(@Param("systemId") Long systemId);
+
+    /** 根据家庭ID查询归属关系 */
+    @Select("SELECT r.* FROM supply_customer_relation r " +
+            "WHERE r.family_id = #{familyId}")
+    List<SupplyCustomerRelation> listByFamilyId(@Param("familyId") Long familyId);
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -27,7 +27,9 @@ import com.etotem.cfc.service.PlatformPointsService;
 import com.etotem.cfc.util.RedisLockUtil;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
 import com.etotem.cfc.entity.SupplySystemMember;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
+import com.etotem.cfc.mapper.SupplyCustomerRelationMapper;
 import com.etotem.cfc.service.SupplyHierarchyService;
 import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyCouponMapper;
@@ -140,6 +142,9 @@ public class ProductOrderService {
     @Resource
     private SupplyHierarchyService supplyHierarchyService;
 
+    @Resource
+    private SupplyCustomerRelationMapper supplyCustomerRelationMapper;
+
     @Resource
     private ProductGiftRuleService productGiftRuleService;
 
@@ -777,6 +782,8 @@ public class ProductOrderService {
                     );
                     if (referrerMember != null) {
                         supplyHierarchyService.addRelation(order.getSupplySystemId(), buyer.getReferrerId(), order.getBuyerId());
+                        // 同时记录供应商团队客户归属
+                        supplyCustomerRelationService.autoBindOnOrder(order.getSupplySystemId(), buyer.getReferrerId(), order.getFamilyId(), order.getBuyerId());
                     }
                 }
             }

+ 181 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SupplyCustomerRelationService.java

@@ -0,0 +1,181 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.SupplyCustomerRelation;
+import com.etotem.cfc.entity.SupplySystem;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.SupplyCustomerRelationMapper;
+import com.etotem.cfc.mapper.SupplySystemMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class SupplyCustomerRelationService {
+
+    @Resource
+    private SupplyCustomerRelationMapper supplyCustomerRelationMapper;
+
+    @Resource
+    private SupplySystemMapper supplySystemMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    /**
+     * 查询某上级供应商下的客户列表
+     * 返回:家庭ID、家庭名称、成员昵称、加入时间、归属来源
+     */
+    public List<Map<String, Object>> listCustomers(Long systemId, Long parentId, Integer page, Integer size) {
+        List<SupplyCustomerRelation> relations = supplyCustomerRelationMapper.listByParent(systemId, parentId);
+        
+        // 分页处理
+        int fromIndex = (page - 1) * size;
+        int toIndex = Math.min(fromIndex + size, relations.size());
+        if (fromIndex >= relations.size()) {
+            return Collections.emptyList();
+        }
+        relations = relations.subList(fromIndex, toIndex);
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (SupplyCustomerRelation relation : relations) {
+            Map<String, Object> customer = new HashMap<>();
+            customer.put("id", relation.getId());
+            customer.put("familyId", relation.getFamilyId());
+            customer.put("relationType", relation.getRelationType());
+            customer.put("source", relation.getSource());
+            customer.put("createdAt", relation.getCreatedAt());
+
+            // 查询家庭信息
+            FamilyMember parentMember = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, relation.getFamilyId())
+                    .eq(FamilyMember::getRole, "parent")
+                    .orderByAsc(FamilyMember::getGeneration)
+                    .last("LIMIT 1")
+            );
+            if (parentMember != null) {
+                customer.put("familyName", parentMember.getNickname());
+                customer.put("memberType", "login");
+                
+                // 查询家庭下能登录的用户数量
+                Long loginCount = familyMemberMapper.selectCount(
+                    new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, relation.getFamilyId())
+                        .isNotNull(FamilyMember::getUserId)
+                );
+                customer.put("loginMemberCount", loginCount);
+            } else {
+                customer.put("familyName", "未知家庭");
+                customer.put("memberType", "local");
+            }
+
+            // 查询下单用户信息(如果有)
+            if (relation.getChildUserId() != null) {
+                User buyer = userMapper.selectById(relation.getChildUserId());
+                if (buyer != null) {
+                    customer.put("buyerNickname", buyer.getNickname());
+                    customer.put("buyerPhone", buyer.getPhone());
+                }
+            }
+
+            result.add(customer);
+        }
+        return result;
+    }
+
+    /**
+     * 绑定客户归属关系(人工绑定)
+     */
+    public boolean bindCustomer(Long systemId, Long parentId, Long familyId, Long childUserId, String source) {
+        // 检查是否已存在绑定关系
+        SupplyCustomerRelation existing = supplyCustomerRelationMapper.selectOne(
+            new LambdaQueryWrapper<SupplyCustomerRelation>()
+                .eq(SupplyCustomerRelation::getSystemId, systemId)
+                .eq(SupplyCustomerRelation::getFamilyId, familyId)
+        );
+        if (existing != null) {
+            // 更新现有记录
+            existing.setParentUserId(parentId);
+            existing.setChildUserId(childUserId);
+            existing.setSource(source != null ? source : "manual");
+            existing.setRelationType("family");
+            return supplyCustomerRelationMapper.updateById(existing) > 0;
+        }
+
+        // 创建新记录
+        SupplyCustomerRelation relation = new SupplyCustomerRelation();
+        relation.setSystemId(systemId);
+        relation.setParentUserId(parentId);
+        relation.setFamilyId(familyId);
+        relation.setChildUserId(childUserId);
+        relation.setRelationType("family");
+        relation.setSource(source != null ? source : "manual");
+        return supplyCustomerRelationMapper.insert(relation) > 0;
+    }
+
+    /**
+     * 系统自动绑定(下单时触发)
+     */
+    public void autoBindOnOrder(Long systemId, Long parentId, Long familyId, Long childUserId) {
+        bindCustomer(systemId, parentId, familyId, childUserId, "order");
+    }
+
+    /**
+     * 查询团队成员列表(含客户数量统计)
+     */
+    public List<Map<String, Object>> listTeamMembers(Long systemId) {
+        // 这里简化实现,实际可扩展
+        List<Map<String, Object>> result = new ArrayList<>();
+        
+        // 查询该体系下的所有上下级关系
+        List<SupplyCustomerRelation> allRelations = supplyCustomerRelationMapper.listBySystem(systemId);
+        
+        // 按上级用户分组统计客户数量
+        Map<Long, Long> customerCountByParent = allRelations.stream()
+            .collect(Collectors.groupingBy(
+                SupplyCustomerRelation::getParentUserId,
+                Collectors.counting()
+            ));
+        
+        // 查询所有上级用户的详细信息
+        Set<Long> parentUserIds = allRelations.stream()
+            .map(SupplyCustomerRelation::getParentUserId)
+            .collect(Collectors.toSet());
+        
+        for (Long parentId : parentUserIds) {
+            Map<String, Object> member = new HashMap<>();
+            member.put("userId", parentId);
+            member.put("customerCount", customerCountByParent.getOrDefault(parentId, 0L));
+            
+            User user = userMapper.selectById(parentId);
+            if (user != null) {
+                member.put("nickname", user.getNickname());
+                member.put("phone", user.getPhone());
+                member.put("avatar", user.getAvatar());
+            }
+            result.add(member);
+        }
+        return result;
+    }
+
+    /**
+     * 查询某个家庭的归属关系
+     */
+    public SupplyCustomerRelation getRelationByFamily(Long systemId, Long familyId) {
+        return supplyCustomerRelationMapper.selectOne(
+            new LambdaQueryWrapper<SupplyCustomerRelation>()
+                .eq(SupplyCustomerRelation::getSystemId, systemId)
+                .eq(SupplyCustomerRelation::getFamilyId, familyId)
+        );
+    }
+}

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

@@ -5959,3 +5959,19 @@ CREATE TABLE IF NOT EXISTS home_banners (
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页广告栏配置表';
+
+-- 迁移344: 创建 supply_customer_relation 表(供应商团队客户归属记录)
+CREATE TABLE IF NOT EXISTS supply_customer_relation (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    system_id BIGINT NOT NULL COMMENT '供应商体系ID(supply_system.id)',
+    parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID(supply_hierarchy.parent_id)',
+    family_id BIGINT NOT NULL COMMENT '客户家庭ID(family_members.family_id)',
+    child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID(users.id),可为空',
+    relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family=家庭归属 order=订单关联',
+    source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual=人工绑定 order=下单自动 referral=推荐码',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_system_family (system_id, family_id),
+    INDEX idx_parent_user (parent_user_id),
+    INDEX idx_family_id (family_id),
+    UNIQUE KEY uk_system_family (system_id, family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商团队客户归属关系表';

+ 6 - 0
cfc-frontend/pages.json

@@ -716,6 +716,12 @@
           "style": {
             "navigationBarTitleText": "发布商品"
           }
+        },
+        {
+          "path": "team-customers/team-customers",
+          "style": {
+            "navigationBarTitleText": "团队客户"
+          }
         }
       ]
     },

+ 423 - 0
cfc-frontend/pages/vendor/team-customers/team-customers.vue

@@ -0,0 +1,423 @@
+<template>
+  <view class="page-team-customers">
+    <view class="page-header">
+      <text class="header-title">团队客户</text>
+      <text class="header-subtitle">查看上级供应商下的客户归属</text>
+    </view>
+
+    <view class="content-area">
+      <view v-if="isLoading" class="loading-indicator">
+        <image class="loading-avatar" src="/static/images/avatar_default.png" mode="aspectFill"></image>
+        <text class="loading-text">加载中...</text>
+      </view>
+
+      <view v-else class="customer-list">
+        <!-- 状态筛选 Tab -->
+        <view class="status-tabs" v-if="statusOptions.length > 0">
+          <view
+            class="status-tab"
+            v-for="(option, index) in statusOptions"
+            :key="index"
+            :class="{ 'status-tab-active': status === option.key }"
+            @click="handleStatusChange(option.key)"
+          >
+            <text>{{ option.label }}</text>
+          </view>
+        </view>
+
+        <!-- 列表区域 -->
+        <view v-else>
+          <view class="no-data" v-if="filteredCustomers.length === 0">
+            <view class="empty-state">
+              <image src="/static/images/icon_empty_state.png" class="empty-icon"></image>
+              <text>暂无客户数据</text>
+            </view>
+          </view>
+
+          <view class="customer-item" v-for="customer in filteredCustomers" :key="customer.id">
+            <view class="customer-avatar-wrap">
+              <image
+                v-if="customer.avatar"
+                class="customer-avatar-img"
+                :src="customer.avatar"
+                mode="aspectFill"></image>
+              <text v-else class="customer-avatar-icon">👤</text>
+            </view>
+
+            <view class="customer-info">
+              <view class="customer-name">{{ customer.nickname || '微信用户' }}</view>
+              <view class="customer-meta">
+                <text class="member-type">{{ customer.memberType || '本地用户' }}</text>
+                <text class="join-time">{{ formatJoinTime(customer.joinTime) }}</text>
+              </view>
+            </view>
+
+            <view class="customer-right">
+              <view class="order-count">
+                <text>{{ customer.orderCount || 0 }}</text>
+                <text>订单</text>
+              </view>
+
+              <view class="source-tag" :class="getSourceTagClass(customer.source)">
+                <text>{{ getSourceTagLabel(customer.source) }}</text>
+              </view>
+            </view>
+          </view>
+
+          <!-- 加载更多 -->
+          <view class="load-more" v-if="hasMore && !loading">
+            <view @click="loadMore" class="load-more-btn">
+              <text>加载更多</text>
+              <image v-if="loadingMore" src="/static/images/arrow_down.png" class="spinner"></image>
+            </view>
+          </view>
+        </view>
+      </view>
+    </content-area>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      // 页面状态
+      status: 'all', // all | referred | order | manual
+      page: 1,
+      hasMore: true,
+      loading: false,
+      refreshing: false,
+
+      // 数据
+      customers: [],         // 所有客户列表
+      filteredCustomers: [], // 筛选后的客户列表
+      statusOptions: [       // 筛选选项
+        { key: 'all', label: '全部' },
+        { key: 'referred', label: '推荐人' },
+        { key: 'order', label: '下单' },
+        { key: 'manual', label: '人工' }
+      ],
+
+      // 查询参数
+      systemId: '',          // 供应商体系ID
+      parentId: ''           // 上级供应商用户ID
+    }
+  },
+
+  onLoad(options) {
+    // 从路由参数获取系统ID和上级用户ID
+    this.systemId = options.systemId || ''
+    this.parentId = options.parentId || ''
+    this.loadCustomers()
+  },
+
+  onShow() {
+    // 切Tab返回时刷新
+    this.page = 1
+    this.customers = []
+    this.hasMore = true
+    this.loadCustomers()
+  },
+
+  methods: {
+    // 加载客户列表
+    async loadCustomers() {
+      if (this.loading || this.refreshing) return
+      this.loading = true
+
+      try {
+        // 并发请求:获取客户列表 + 获取统计概览
+        var res = await this.$http.getTeamCustomers({
+          systemId: this.systemId,
+          parentId: this.parentId,
+          page: this.page,
+          size: 20
+        })
+
+        if (res && res.data) {
+          this.customers = this.page === 1 ? res.data.records || [] : this.customers.concat(res.data.records || [])
+          this.hasMore = (res.data.records || []).length >= 20
+          this.page++
+
+          // 计算筛选后的列表
+          this.filteredCustomers = this.filterByStatus(this.customers, this.status)
+        }
+      } catch (error) {
+        console.error('获取团队客户失败:', error)
+      } finally {
+        this.loading = false
+        this.refreshing = false
+      }
+    },
+
+    // 根据状态筛选
+    filterByStatus(customers, status) {
+      if (status === 'all') return customers
+      return customers.filter(c => c.source === status)
+    },
+
+    // 处理状态筛选切换
+    handleStatusChange(key) {
+      this.status = key
+      this.page = 1
+      this.customers = []
+      this.hasMore = true
+      this.loadCustomers()
+    },
+
+    // 加载更多
+    async loadMore() {
+      if (this.loading || !this.hasMore) return
+      this.loadingMore = true
+      try {
+        await this.loadCustomers()
+      } finally {
+        this.loadingMore = false
+      }
+      this.$nextTick(() => {
+        this.loadingMore = false
+      })
+    },
+
+    // 格式化加入时间
+    formatJoinTime(timeStr) {
+      if (!timeStr) return '-'
+      var d = this.parseDate(timeStr)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+    },
+
+    // 获取来源标签样式
+    getSourceTagClass(source) {
+      const classes = {
+        'manual': 'tag-manual',
+        'order': 'tag-order',
+        'referral': 'tag-referral'
+      }
+      return classes[source] || 'tag-default'
+    },
+
+    // 获取来源标签文字
+    getSourceTagLabel(source) {
+      const labels = {
+        'manual': '人工绑定',
+        'order': '下单自动',
+        'referral': '推荐码'
+      }
+      return labels[source] || source
+    }
+  }
+}
+</script>
+
+<style>
+/* 页面整体样式 */
+.page-team-customers {
+  background-color: #FFF7ED;
+  min-height: 100vh;
+}
+
+/* 页面头部 */
+.page-header {
+  padding: 24rpx 30rpx;
+  background-color: #fff;
+  border-bottom: 1rpx solid #FED7AA;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333333;
+}
+
+.header-subtitle {
+  font-size: 20rpx;
+  color: #94A3B8;
+}
+
+/* 内容区 */
+.content-area {
+  padding: 0 30rpx 30rpx;
+}
+
+/* 状态筛选 Tab */
+.status-tabs {
+  display: flex;
+  background-color: #F5FAFE;
+  border-radius: 20rpx;
+  padding: 6rpx 12rpx;
+  margin-bottom: 24rpx;
+}
+
+.status-tab {
+  padding: 10rpx 20rpx;
+  font-size: 24rpx;
+  color: #666666;
+  text-align: center;
+}
+
+.status-tab-active {
+  color: #F97316;
+  border-bottom: 2rpx solid #F97316;
+}
+
+/* 列表项 */
+.customer-item {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+  display: flex;
+  align-items: center;
+}
+
+.customer-avatar-wrap {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  margin-right: 24rpx;
+  position: relative;
+}
+
+.customer-avatar-img {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.customer-avatar-icon {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  background-color: #F5FAFE;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #94A3B8;
+  font-size: 32rpx;
+}
+
+.customer-info {
+  flex: 1;
+}
+
+.customer-name {
+  font-size: 28rpx;
+  color: #333333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.member-type {
+  font-size: 20rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+}
+
+.join-time {
+  font-size: 20rpx;
+  color: #B5B5B5;
+}
+
+/* 右侧信息 */
+.customer-right {
+  flex-shrink: 0;
+  text-align: right;
+  margin-left: 24rpx;
+}
+
+.order-count {
+  margin-bottom: 8rpx;
+}
+
+.order-count .number {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.order-count .label {
+  font-size: 20rpx;
+  color: #666666;
+}
+
+/* 来源标签 */
+.source-tag {
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  font-size: 16rpx;
+  margin-top: 4rpx;
+}
+
+.tag-manual {
+  background-color: #E6F7EE;
+  color: #65C29A;
+}
+
+.tag-order {
+  background-color: #FFF7EE;
+  color: #F97316;
+}
+
+.tag-referral {
+  background-color: #EEF8FF;
+  color: #2F5496;
+}
+
+.tag-default {
+  background-color: #F5FAFE;
+  color: #666666;
+}
+
+/* 加载更多 */
+.load-more {
+  padding: 24rpx;
+  text-align: center;
+  color: #94A3B8;
+  font-size: 24rpx;
+}
+
+/* 加载更多按钮 */
+.load-more-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 120rpx;
+  height: 40rpx;
+  border: 1rpx solid #FED7AA;
+  border-radius: 20rpx;
+  color: #666666;
+  font-size: 24rpx;
+}
+
+.load-more-btn .spinner {
+  width: 16rpx;
+  height: 16rpx;
+  margin-left: 6rpx;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.no-data .empty-state {
+  text-align: center;
+  padding: 80rpx 30rpx;
+  color: #94A3B8;
+}
+
+.empty-icon {
+  width: 64rpx;
+  height: 64rpx;
+  margin: 0 auto 12rpx;
+  display: block;
+}
+</style>

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

@@ -1613,6 +1613,16 @@ export const vendorInfo = () => {
   return request('/api/vendor/info', 'POST', {})
 }
 
+/**
+ * 获取供应商团队客户列表
+ * 后端接口:POST /api/admin/supply-team/customers
+ * 参数:systemId, parentId, page, size
+ * 返回:records + hasMore
+ */
+export const getTeamCustomers = (params) => {
+  return request('/api/admin/supply-team/customers', 'POST', params)
+}
+
 //  幸福邻里部落 - 商品系统 
 
 // 获取公开配置(pages/health/report-upload 依赖此函数读取 report_upload_category_id)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-711b51237534cd371cc46f093589e01e8f3fa354
+ce33709af0b08255e14ec6a8b3d0e40d95490242

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1508",
+  "version": "1.0.1509",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1508",
+      "version": "1.0.1509",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1509",
+  "version": "1.0.1510",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 30 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,36 @@
 
 ---
 
+## v1.0.1510 (2026-10-05)
+
+### Bug 修复
+- 删除重复的 methods 块,修复 'Unexpected end of input'
+- 重构模板为单根元素,修复 'should contain exactly one root element'
+
+### 其他
+- 但 L298 又出现了一个相同的 methods: { ... } 块,导致整个 script 语法结构断裂:
+-   L139: methods: {   ← 第一个 methods 块(正确)
+-   ...
+-   L297: },
+-   L298: methods: {   ← 重复,整块内容照抄了 L139–L297
+-   ...
+-   L509: }
+-   L510: }
+-   L511: }
+-   L512: </script>
+- Node 报 'Unexpected end of input'(L422),是因为重复块内部的多层闭括号
+- 把 methods 对象的闭合、export default 的闭合都提前结束了,导致后续 </script>
+- 和 style 被当作 script 的一部分解析。
+- 修复:删除 L298–L510 的重复块,在原 methods 块末尾补充 }; 闭合。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Vue 单根元素要求,编译报 'Component template should contain exactly one root element'。
+- 修复:将 save-bar 移入条件渲染的 section 内(v-if="renderedSnapshot"),
+- 外层 <view class="portrait-edit-page"> 成为唯一根元素。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
 ## v1.0.1509 (2026-10-05)
 
 ### 重构

+ 31 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1509
+> 当前版本: v1.0.1510
 
 ## 历史版本
 
@@ -8,6 +8,36 @@
 
 ---
 
+## v1.0.1510 (2026-10-05)
+
+### Bug 修复
+- 删除重复的 methods 块,修复 'Unexpected end of input'
+- 重构模板为单根元素,修复 'should contain exactly one root element'
+
+### 其他
+- 但 L298 又出现了一个相同的 methods: { ... } 块,导致整个 script 语法结构断裂:
+-   L139: methods: {   ← 第一个 methods 块(正确)
+-   ...
+-   L297: },
+-   L298: methods: {   ← 重复,整块内容照抄了 L139–L297
+-   ...
+-   L509: }
+-   L510: }
+-   L511: }
+-   L512: </script>
+- Node 报 'Unexpected end of input'(L422),是因为重复块内部的多层闭括号
+- 把 methods 对象的闭合、export default 的闭合都提前结束了,导致后续 </script>
+- 和 style 被当作 script 的一部分解析。
+- 修复:删除 L298–L510 的重复块,在原 methods 块末尾补充 }; 闭合。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Vue 单根元素要求,编译报 'Component template should contain exactly one root element'。
+- 修复:将 save-bar 移入条件渲染的 section 内(v-if="renderedSnapshot"),
+- 外层 <view class="portrait-edit-page"> 成为唯一根元素。
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
 ## v1.0.1509 (2026-10-05)
 
 ### 重构

+ 269 - 0
docs/IMPLEMENTATION-SUMMARY-2026-10-05.md

@@ -0,0 +1,269 @@
+# 供应商团队客户归属功能实施总结
+
+**实施日期**: 2026-10-05  
+**功能名称**: 供应商团队客户归属系统
+
+---
+
+## 一、业务需求回顾
+
+供应商作为后台提供服务的加盟商(规划师、商品提供商等),需要:
+1. **查看下级带来的客户** — 上级服务商能看到下级供应商带来的客户列表
+2. **团队内部服务转介** — 团队成员之间可以进行服务转介
+3. **优先级排序** — 按推荐会员数排序,优先获得平台推荐
+
+**关键约束**:平台只负责展示归属关系,**不负责团队分佣**。
+
+---
+
+## 二、已完成实现
+
+### 2.1 数据库层
+
+#### 新建表 `supply_customer_relation`(迁移344)
+
+**文件位置**: 
+- `cfc-backend/src/main/resources/schema.sql` (末尾)
+- `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (第11510-11530行)
+
+**表结构**:
+```sql
+CREATE TABLE IF NOT EXISTS supply_customer_relation (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    system_id BIGINT NOT NULL COMMENT '供应商体系ID',
+    parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID',
+    family_id BIGINT NOT NULL COMMENT '客户家庭ID',
+    child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID',
+    relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family/order',
+    source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual/order/referral',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_system_family (system_id, family_id),
+    INDEX idx_parent_user (parent_user_id),
+    INDEX idx_family_id (family_id),
+    UNIQUE KEY uk_system_family (system_id, family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**设计说明**:
+- `system_id` + `family_id` 唯一键:防止重复记录
+- `parent_user_id`:上级供应商用户ID,支持查询"某上级下的所有客户"
+- `source` 字段区分来源:`manual`(人工绑定)/`order`(下单自动)/`referral`(推荐码)
+- `relation_type`:`family`(家庭归属)/`order`(订单关联)
+
+### 2.2 后端实现
+
+#### 新建文件清单
+
+| 文件 | 位置 | 说明 |
+|------|------|------|
+| `SupplyCustomerRelation.java` | `entity/` | 实体类 |
+| `SupplyCustomerRelationMapper.java` | `mapper/` | Mapper接口,含自定义查询方法 |
+| `SupplyCustomerRelationService.java` | `service/` | 业务逻辑:绑定、查询、统计 |
+| `SupplyTeamController.java` | `controller/admin/` | RESTful API端点 |
+
+#### 关键业务逻辑
+
+**SupplyCustomerRelationService.java**:
+- `listCustomers(systemId, parentId, page, size)`: 分页查询上级供应商下的客户列表
+- `bindCustomer(systemId, parentId, familyId, childUserId, source)`: 人工绑定客户归属
+- `autoBindOnOrder(systemId, parentId, familyId, childUserId)`: 订单完成时自动绑定
+- `listTeamMembers(systemId)`: 查询团队成员列表(含客户数量统计)
+- `getRelationByFamily(systemId, familyId)`: 查询某个家庭的归属关系
+
+#### 修改文件
+
+**ProductOrderService.java** (第778-782行):
+```java
+// 原有逻辑
+supplyHierarchyService.addRelation(order.getSupplySystemId(), buyer.getReferrerId(), order.getBuyerId());
+
+// 新增逻辑
+supplyCustomerRelationService.autoBindOnOrder(order.getSupplySystemId(), buyer.getReferrerId(), order.getFamilyId(), order.getBuyerId());
+```
+
+**说明**: 订单完成时,同时记录上下级关系和供应商团队客户归属关系。
+
+#### 新增 API 接口
+
+| 接口 | 说明 |
+|------|------|
+| `POST /api/admin/supply-team/customers` | 查询上级供应商下的客户列表 |
+| `POST /api/admin/supply-team/bind-customer` | 人工绑定客户归属 |
+| `POST /api/admin/supply-team/members` | 查询团队成员列表(含客户数量统计) |
+| `POST /api/admin/supply-team/relation` | 查询某个家庭的归属关系 |
+
+### 2.3 前端实现
+
+#### 新建页面
+
+**文件位置**: `cfc-frontend/pages/vendor/team-customers/team-customers.vue`
+
+**页面功能**:
+- 展示上级供应商下的客户列表
+- 支持按来源筛选(全部/推荐人/下单/人工)
+- 支持下拉刷新和上拉加载更多
+- 显示客户信息:昵称、头像、加入时间、订单数量、来源标签
+
+**页面注册**: `cfc-frontend/pages.json`
+```json
+{
+  "path": "team-customers/team-customers",
+  "style": { "navigationBarTitleText": "团队客户" }
+}
+```
+
+#### API 封装
+
+**文件**: `cfc-frontend/utils/api.js`
+
+```javascript
+export const getTeamCustomers = (params) => {
+  return request('/api/admin/supply-team/customers', 'POST', params)
+}
+```
+
+---
+
+## 三、业务流程
+
+```
+用户下单购买供应商商品
+         ↓
+ProductOrderService.orderComplete()
+         ↓
+  ┌──────┴──────┐
+  ↓             ↓
+supplyHierarchyService.addRelation()  ← 绑定上下级关系(已有)
+  ↓
+supplyCustomerRelationService.autoBindOnOrder()  ← 新增:记录客户归属
+  ↓
+写入 supply_customer_relation 表
+  ↓
+上级供应商通过 /api/admin/supply-team/customers 查询团队客户列表
+  ↓
+前端展示在「团队客户」页面
+```
+
+---
+
+## 四、未实施内容(后续)
+
+### 4.1 团队内部服务转介
+
+**需求**: 团队成员之间可以进行服务转介
+
+**建议实现**:
+1. 新建表 `supply_referral_record`:
+   - `id`, `system_id`, `from_user_id`, `to_user_id`, `family_id`, `service_type`, `status`, `created_at`
+2. 新增 API: `POST /api/admin/supply-team/referral/apply`
+3. 前端:在客户详情页添加「转介」按钮
+
+### 4.2 推荐码体系关联
+
+**需求**: 服务商通过推荐码绑定客户,自动关联供应商团队
+
+**建议实现**:
+1. 在 `CommissionService.bindReferrerByUserId()` 中,检查推荐人是否属于某个供应商体系
+2. 如果是,同时写入 `supply_customer_relation` 表
+3. 需要新增 API: `POST /api/admin/supply-team/bind-by-referral`
+
+### 4.3 前端页面入口
+
+**需求**: 服务商可以从「服务商中心」跳转到「团队客户」页面
+
+**建议实现**:
+1. 修改 `pages/vendor/center/center.vue`
+2. 在现有菜单项中添加「团队客户」入口
+
+---
+
+## 五、验证建议
+
+### 5.1 数据库迁移验证
+
+```bash
+# 启动后端,查看日志确认迁移执行
+mvn spring-boot:run
+
+# 检查日志输出
+# 应该看到: "迁移344: 已创建 supply_customer_relation 表"
+```
+
+### 5.2 后端 API 验证
+
+```bash
+# 测试绑定客户归属
+curl -X POST http://localhost:9082/api/admin/supply-team/bind-customer \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer {token}" \
+  -d '{"systemId": 1, "parentId": 100, "familyId": 500, "childUserId": 200, "source": "manual"}'
+
+# 测试查询客户列表
+curl -X POST http://localhost:9082/api/admin/supply-team/customers \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer {token}" \
+  -d '{"systemId": 1, "parentId": 100, "page": 1, "size": 20}'
+```
+
+### 5.3 前端验证
+
+1. 使用微信开发者工具导入项目
+2. 登录服务商账号(`role=teacher` 或 `vendor_type=product_supplier`)
+3. 从「服务商中心」跳转到「团队客户」页面
+4. 验证客户列表展示正确
+
+---
+
+## 六、技术要点
+
+### 6.1 幂等性保证
+
+- `supply_customer_relation` 表使用 `UNIQUE KEY uk_system_family (system_id, family_id)`
+- Service 层 `bindCustomer()` 先检查是否已存在,存在则更新,不存在则插入
+- 前端 `loadCustomers()` 支持分页加载,避免重复请求
+
+### 6.2 权限控制
+
+- 所有 API 在 `JwtInterceptor` 中验证 Token
+- Controller 中手动检查 `@RequestAttribute("role")`
+- 建议后续增加:仅允许上级查看自己的下级客户,不允许越级查看
+
+### 6.3 性能优化
+
+- 索引设计:`idx_system_family`、`idx_parent_user`、`idx_family_id`
+- 分页查询:`page` + `size` 参数,避免全表扫描
+- 缓存建议:团队成员统计信息可使用 Redis 缓存(后续优化)
+
+---
+
+## 七、后续优化建议
+
+1. **增加前端页面入口**: 在「服务商中心」添加「团队客户」快捷入口
+2. **实现团队内部服务转介**: 新建 `supply_referral_record` 表及相关 API
+3. **关联推荐码体系**: 在 `CommissionService` 中自动绑定供应商团队归属
+4. **增加数据可视化**: 在团队客户页面添加图表展示客户增长趋势
+5. **性能优化**: 为团队成员统计添加 Redis 缓存
+
+---
+
+## 八、文件变更清单
+
+### 新增文件
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/SupplyCustomerRelation.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/SupplyCustomerRelationMapper.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/service/SupplyCustomerRelationService.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyTeamController.java`
+- `cfc-frontend/pages/vendor/team-customers/team-customers.vue`
+
+### 修改文件
+- `cfc-backend/src/main/resources/schema.sql` (添加 supply_customer_relation 表定义)
+- `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (添加迁移344)
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java` (添加自动绑定逻辑)
+- `cfc-frontend/utils/api.js` (添加 getTeamCustomers API 封装)
+- `cfc-frontend/pages.json` (注册 team-customers 页面)
+
+---
+
+**实施状态**: ✅ 核心功能已完成  
+**验证状态**: ⏳ 待编译验证  
+**前端联调**: ⏳ 待后端 API 稳定后联调