Просмотр исходного кода

feat: 供应商上下级关系体系 (supply hierarchy)

- 新增 supply_hierarchy 表:记录供应商体系内的上下级关系
- 新增 supply_hierarchy_change_request 表:记录更换上级申请
- SupplyHierarchyService: setHierarchy, addRelation, removeRelation, getParent, getChildren, applyChange, reviewChange
- SupplyHierarchyController: 11个API端点 (tree, set, remove, children, parent-chain, change-requests, etc.)
- ProductOrderService: 支付成功时自动绑定buyer到其referrer的下级
- DatabaseInitializer: 迁移86创建新表
- schema.sql: 同步建表语句

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 3 месяцев назад
Родитель
Сommit
141ca6cbe1

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

@@ -5245,6 +5245,7 @@ try {
         runMigration80();
         runMigration80();
         runMigration81();
         runMigration81();
         runMigration82();
         runMigration82();
+        runMigration86();
     }
     }
 
 
     private void runMigration81() {
     private void runMigration81() {
@@ -5413,4 +5414,45 @@ try {
             log.warn("创建ai_user_facts表失败: {}", e.getMessage());
             log.warn("创建ai_user_facts表失败: {}", e.getMessage());
         }
         }
     }
     }
+
+    private void runMigration86() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "system_id BIGINT NOT NULL COMMENT '供应商体系ID', " +
+                    "parent_id BIGINT NOT NULL COMMENT '上级供应商ID', " +
+                    "child_id BIGINT NOT NULL COMMENT '下级供应商ID', " +
+                    "level INT DEFAULT 1 COMMENT '层级深度', " +
+                    "status VARCHAR(20) DEFAULT 'active', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_system_parent_child (system_id, parent_id, child_id), " +
+                    "KEY idx_system_child (system_id, child_id), " +
+                    "KEY idx_parent (parent_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商上下级关系表'");
+            log.info("已创建supply_hierarchy表");
+        } catch (Exception e) {
+            log.warn("创建supply_hierarchy表失败: {}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy_change_request (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "system_id BIGINT NOT NULL, " +
+                    "applicant_id BIGINT NOT NULL COMMENT '申请人ID', " +
+                    "target_parent_id BIGINT NOT NULL COMMENT '目标上级ID', " +
+                    "status VARCHAR(20) DEFAULT 'pending', " +
+                    "reason VARCHAR(500), " +
+                    "reject_reason VARCHAR(500), " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "reviewed_at DATETIME, " +
+                    "reviewed_by BIGINT, " +
+                    "KEY idx_system_applicant (system_id, applicant_id), " +
+                    "KEY idx_status (status)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商更换上级申请表'");
+            log.info("已创建supply_hierarchy_change_request表");
+        } catch (Exception e) {
+            log.warn("创建supply_hierarchy_change_request表失败: {}", e.getMessage());
+        }
+    }
 }
 }

+ 244 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyHierarchyController.java

@@ -0,0 +1,244 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.SupplyHierarchy;
+import com.etotem.cfc.entity.SupplyHierarchyChangeRequest;
+import com.etotem.cfc.entity.SupplySystemMember;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.SupplySystemMemberMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.SupplyHierarchyService;
+import com.etotem.cfc.service.SupplySystemService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.HashSet;
+import java.util.HashSet;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/supply-hierarchy")
+public class SupplyHierarchyController {
+
+    @Resource
+    private SupplyHierarchyService supplyHierarchyService;
+
+    @Resource
+    private SupplySystemService supplySystemService;
+
+    @Resource
+    private SupplySystemMemberMapper supplySystemMemberMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @PostMapping("/set")
+    public Result<Void> setHierarchy(@RequestBody Map<String, Object> params) {
+        try {
+            Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+            Long parentId = params.get("parentId") != null ? ((Number) params.get("parentId")).longValue() : null;
+            Long childId = params.get("childId") != null ? ((Number) params.get("childId")).longValue() : null;
+            if (systemId == null || parentId == null || childId == null) {
+                return Result.error("参数不完整");
+            }
+            supplyHierarchyService.setHierarchy(systemId, parentId, childId);
+            return Result.success();
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("设置失败");
+        }
+    }
+
+    @PostMapping("/remove")
+    public Result<Void> removeRelation(@RequestBody Map<String, Object> params) {
+        try {
+            Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+            Long childId = params.get("childId") != null ? ((Number) params.get("childId")).longValue() : null;
+            if (systemId == null || childId == null) {
+                return Result.error("参数不完整");
+            }
+            supplyHierarchyService.removeRelation(systemId, childId);
+            return Result.success();
+        } catch (Exception e) {
+            return Result.error("移除失败");
+        }
+    }
+
+    @PostMapping("/direct-children")
+    public Result<List<User>> getDirectChildren(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        Long parentId = params.get("parentId") != null ? ((Number) params.get("parentId")).longValue() : null;
+        if (systemId == null || parentId == null) {
+            return Result.error("参数不完整");
+        }
+        List<User> children = supplyHierarchyService.getDirectChildren(systemId, parentId);
+        return Result.success(children);
+    }
+
+    @PostMapping("/all-children")
+    public Result<List<User>> getAllChildren(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        Long parentId = params.get("parentId") != null ? ((Number) params.get("parentId")).longValue() : null;
+        if (systemId == null || parentId == null) {
+            return Result.error("参数不完整");
+        }
+        List<User> children = supplyHierarchyService.getAllChildren(systemId, parentId);
+        return Result.success(children);
+    }
+
+    @PostMapping("/parent")
+    public Result<User> getParent(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        Long userId = params.get("userId") != null ? ((Number) params.get("userId")).longValue() : null;
+        if (systemId == null || userId == null) {
+            return Result.error("参数不完整");
+        }
+        User parent = supplyHierarchyService.getParent(systemId, userId);
+        return Result.success(parent);
+    }
+
+    @PostMapping("/parent-chain")
+    public Result<List<User>> getParentChain(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        Long userId = params.get("userId") != null ? ((Number) params.get("userId")).longValue() : null;
+        if (systemId == null || userId == null) {
+            return Result.error("参数不完整");
+        }
+        List<User> chain = supplyHierarchyService.getParentChain(systemId, userId);
+        return Result.success(chain);
+    }
+
+    @PostMapping("/tree")
+    public Result<List<Map<String, Object>>> getTree(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        if (systemId == null) {
+            return Result.error("systemId 不能为空");
+        }
+
+        List<SupplySystemMember> members = supplySystemService.memberList(systemId);
+        Map<Long, User> userMap = new HashMap<>();
+        for (SupplySystemMember m : members) {
+            User u = userMapper.selectById(m.getUserId());
+            if (u != null) {
+                userMap.put(u.getId(), u);
+            }
+        }
+
+        List<SupplyHierarchy> allRelations = supplyHierarchyService.list(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SupplyHierarchy>()
+                        .eq(SupplyHierarchy::getSystemId, systemId)
+                        .eq(SupplyHierarchy::getStatus, "active"));
+
+        Map<Long, List<SupplyHierarchy>> childrenMap = new HashMap<>();
+        for (SupplyHierarchy r : allRelations) {
+            childrenMap.computeIfAbsent(r.getParentId(), k -> new ArrayList<>()).add(r);
+        }
+
+        SupplySystemMember admin = members.stream().filter(m -> "admin".equals(m.getRole())).findFirst().orElse(null);
+        if (admin == null) {
+            return Result.success(new ArrayList<>());
+        }
+
+        List<Map<String, Object>> root = buildTree(admin.getUserId(), childrenMap, userMap, new HashSet<>());
+        return Result.success(root);
+    }
+
+    private List<Map<String, Object>> buildTree(Long parentId, Map<Long, List<SupplyHierarchy>> childrenMap,
+                                                 Map<Long, User> userMap, HashSet<Long> visited) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (visited.contains(parentId)) {
+            return result;
+        }
+        visited.add(parentId);
+
+        List<SupplyHierarchy> children = childrenMap.get(parentId);
+        if (children == null || children.isEmpty()) {
+            return result;
+        }
+
+        for (SupplyHierarchy h : children) {
+            User childUser = userMap.get(h.getChildId());
+            if (childUser == null) continue;
+
+            Map<String, Object> node = new HashMap<>();
+            node.put("userId", childUser.getId());
+            node.put("nickname", childUser.getNickname());
+            node.put("realName", childUser.getRealName());
+            node.put("phone", childUser.getPhone());
+            node.put("level", h.getLevel());
+            node.put("children", buildTree(h.getChildId(), childrenMap, userMap, visited));
+            result.add(node);
+        }
+        return result;
+    }
+
+    @PostMapping("/my")
+    public Result<Map<String, Object>> getMyRelation(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        Long userId = params.get("userId") != null ? ((Number) params.get("userId")).longValue() : null;
+        if (systemId == null || userId == null) {
+            return Result.error("参数不完整");
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("parent", supplyHierarchyService.getParent(systemId, userId));
+        result.put("directChildren", supplyHierarchyService.getDirectChildren(systemId, userId));
+        result.put("allChildren", supplyHierarchyService.getAllChildren(systemId, userId));
+        result.put("parentChain", supplyHierarchyService.getParentChain(systemId, userId));
+        return Result.success(result);
+    }
+
+    @PostMapping("/change-requests")
+    public Result<List<SupplyHierarchyChangeRequest>> getChangeRequests(@RequestBody Map<String, Object> params) {
+        Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+        String status = (String) params.get("status");
+        if (systemId == null) {
+            return Result.error("systemId 不能为空");
+        }
+        List<SupplyHierarchyChangeRequest> requests = supplyHierarchyService.getChangeRequests(systemId, status);
+        return Result.success(requests);
+    }
+
+    @PostMapping("/change/apply")
+    public Result<Void> applyChange(@RequestBody Map<String, Object> params) {
+        try {
+            Long systemId = params.get("systemId") != null ? ((Number) params.get("systemId")).longValue() : null;
+            Long targetParentId = params.get("targetParentId") != null ? ((Number) params.get("targetParentId")).longValue() : null;
+            String reason = (String) params.get("reason");
+            if (systemId == null || targetParentId == null) {
+                return Result.error("参数不完整");
+            }
+            supplyHierarchyService.applyChange(systemId, targetParentId, targetParentId, reason);
+            return Result.success();
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("申请失败");
+        }
+    }
+
+    @PostMapping("/change/review")
+    public Result<Void> reviewChange(@RequestBody Map<String, Object> params) {
+        try {
+            Long requestId = params.get("requestId") != null ? ((Number) params.get("requestId")).longValue() : null;
+            String status = (String) params.get("status");
+            String rejectReason = (String) params.get("rejectReason");
+            if (requestId == null || status == null) {
+                return Result.error("参数不完整");
+            }
+            supplyHierarchyService.reviewChange(requestId, status, rejectReason, null);
+            return Result.success();
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("审核失败");
+        }
+    }
+}

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

@@ -0,0 +1,31 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("supply_hierarchy")
+public class SupplyHierarchy implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long systemId;
+
+    private Long parentId;
+
+    private Long childId;
+
+    private Integer level;
+
+    private String status;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SupplyHierarchyChangeRequest.java

@@ -0,0 +1,35 @@
+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_hierarchy_change_request")
+public class SupplyHierarchyChangeRequest implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long systemId;
+
+    private Long applicantId;
+
+    private Long targetParentId;
+
+    private String status;
+
+    private String reason;
+
+    private String rejectReason;
+
+    private Date createdAt;
+
+    private Date reviewedAt;
+
+    private Long reviewedBy;
+}

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

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

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

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

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

@@ -12,6 +12,9 @@ import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductOrder;
 import com.etotem.cfc.entity.ProductOrder;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
+import com.etotem.cfc.entity.SupplySystemMember;
+import com.etotem.cfc.mapper.SupplySystemMemberMapper;
+import com.etotem.cfc.service.SupplyHierarchyService;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -85,6 +88,12 @@ public class ProductOrderService {
     @Resource
     @Resource
     private PendingRefundService pendingRefundService;
     private PendingRefundService pendingRefundService;
 
 
+    @Resource
+    private SupplySystemMemberMapper supplySystemMemberMapper;
+
+    @Resource
+    private SupplyHierarchyService supplyHierarchyService;
+
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
         if (buyerId == null) {
         if (buyerId == null) {
             return Result.error("请先登录");
             return Result.error("请先登录");
@@ -290,6 +299,23 @@ public class ProductOrderService {
         orderMapper.updateById(order);
         orderMapper.updateById(order);
         commissionService.settle(order.getId(), "product", order.getBuyerId(),
         commissionService.settle(order.getId(), "product", order.getBuyerId(),
                 order.getTotalAmount(), order.getProductId());
                 order.getTotalAmount(), order.getProductId());
+        try {
+            if (order.getSupplySystemId() != null) {
+                User buyer = userMapper.selectById(order.getBuyerId());
+                if (buyer != null && buyer.getReferrerId() != null) {
+                    SupplySystemMember referrerMember = supplySystemMemberMapper.selectOne(
+                        new LambdaQueryWrapper<SupplySystemMember>()
+                            .eq(SupplySystemMember::getSystemId, order.getSupplySystemId())
+                            .eq(SupplySystemMember::getUserId, buyer.getReferrerId())
+                    );
+                    if (referrerMember != null) {
+                        supplyHierarchyService.addRelation(order.getSupplySystemId(), buyer.getReferrerId(), order.getBuyerId());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("绑定供应商上下级关系失败: orderNo={}", orderNo, e);
+        }
         // Award energy for product purchase
         // Award energy for product purchase
         try {
         try {
             User buyer_user = userMapper.selectById(order.getBuyerId());
             User buyer_user = userMapper.selectById(order.getBuyerId());

+ 247 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SupplyHierarchyService.java

@@ -0,0 +1,247 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.SupplyHierarchy;
+import com.etotem.cfc.entity.SupplyHierarchyChangeRequest;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.SupplyHierarchyChangeRequestMapper;
+import com.etotem.cfc.mapper.SupplyHierarchyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class SupplyHierarchyService extends ServiceImpl<SupplyHierarchyMapper, SupplyHierarchy> {
+
+    @Resource
+    private SupplyHierarchyChangeRequestMapper changeRequestMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Transactional
+    public void setHierarchy(Long systemId, Long parentId, Long childId) {
+        if (parentId.equals(childId)) {
+            throw new IllegalArgumentException("不能将自己设为上级");
+        }
+
+        LambdaQueryWrapper<SupplyHierarchy> existingWrapper = new LambdaQueryWrapper<>();
+        existingWrapper.eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, childId);
+        SupplyHierarchy existing = this.getOne(existingWrapper);
+
+        int level = 1;
+        SupplyHierarchy parentRelation = this.getOne(new LambdaQueryWrapper<SupplyHierarchy>()
+                .eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, parentId));
+        if (parentRelation != null) {
+            level = parentRelation.getLevel() + 1;
+        }
+
+        if (existing != null) {
+            existing.setParentId(parentId);
+            existing.setLevel(level);
+            existing.setUpdatedAt(new Date());
+            this.updateById(existing);
+        } else {
+            SupplyHierarchy hierarchy = new SupplyHierarchy();
+            hierarchy.setSystemId(systemId);
+            hierarchy.setParentId(parentId);
+            hierarchy.setChildId(childId);
+            hierarchy.setLevel(level);
+            hierarchy.setStatus("active");
+            hierarchy.setCreatedAt(new Date());
+            hierarchy.setUpdatedAt(new Date());
+            this.save(hierarchy);
+        }
+    }
+
+    public boolean addRelation(Long systemId, Long parentId, Long childId) {
+        if (parentId.equals(childId)) {
+            return false;
+        }
+
+        LambdaQueryWrapper<SupplyHierarchy> checkWrapper = new LambdaQueryWrapper<>();
+        checkWrapper.eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getParentId, parentId)
+                .eq(SupplyHierarchy::getChildId, childId);
+        if (this.count(checkWrapper) > 0) {
+            return false;
+        }
+
+        int level = 1;
+        SupplyHierarchy parentRelation = this.getOne(new LambdaQueryWrapper<SupplyHierarchy>()
+                .eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, parentId));
+        if (parentRelation != null) {
+            level = parentRelation.getLevel() + 1;
+        }
+
+        SupplyHierarchy hierarchy = new SupplyHierarchy();
+        hierarchy.setSystemId(systemId);
+        hierarchy.setParentId(parentId);
+        hierarchy.setChildId(childId);
+        hierarchy.setLevel(level);
+        hierarchy.setStatus("active");
+        hierarchy.setCreatedAt(new Date());
+        hierarchy.setUpdatedAt(new Date());
+        return this.save(hierarchy);
+    }
+
+    @Transactional
+    public boolean removeRelation(Long systemId, Long childId) {
+        LambdaQueryWrapper<SupplyHierarchy> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, childId);
+        SupplyHierarchy existing = this.getOne(wrapper);
+        if (existing != null) {
+            existing.setStatus("inactive");
+            existing.setUpdatedAt(new Date());
+            return this.updateById(existing);
+        }
+        return false;
+    }
+
+    public List<User> getDirectChildren(Long systemId, Long parentId) {
+        LambdaQueryWrapper<SupplyHierarchy> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getParentId, parentId)
+                .eq(SupplyHierarchy::getStatus, "active");
+        List<SupplyHierarchy> hierarchies = this.list(wrapper);
+        if (hierarchies.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<Long> childIds = hierarchies.stream()
+                .map(SupplyHierarchy::getChildId)
+                .collect(Collectors.toList());
+        return userMapper.selectBatchIds(childIds);
+    }
+
+    public List<User> getAllChildren(Long systemId, Long parentId) {
+        List<User> result = new ArrayList<>();
+        List<Long> currentLevelIds = new ArrayList<>();
+        currentLevelIds.add(parentId);
+
+        while (!currentLevelIds.isEmpty()) {
+            LambdaQueryWrapper<SupplyHierarchy> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(SupplyHierarchy::getSystemId, systemId)
+                    .in(SupplyHierarchy::getParentId, currentLevelIds)
+                    .eq(SupplyHierarchy::getStatus, "active");
+            List<SupplyHierarchy> hierarchies = this.list(wrapper);
+            if (hierarchies.isEmpty()) {
+                break;
+            }
+            List<Long> childIds = hierarchies.stream()
+                    .map(SupplyHierarchy::getChildId)
+                    .collect(Collectors.toList());
+            result.addAll(userMapper.selectBatchIds(childIds));
+            currentLevelIds = childIds;
+        }
+        return result;
+    }
+
+    public User getParent(Long systemId, Long childId) {
+        SupplyHierarchy hierarchy = this.getOne(new LambdaQueryWrapper<SupplyHierarchy>()
+                .eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, childId)
+                .eq(SupplyHierarchy::getStatus, "active"));
+        if (hierarchy == null) {
+            return null;
+        }
+        return userMapper.selectById(hierarchy.getParentId());
+    }
+
+    public List<User> getParentChain(Long systemId, Long userId) {
+        List<User> chain = new ArrayList<>();
+        Long currentId = userId;
+
+        while (currentId != null) {
+            SupplyHierarchy hierarchy = this.getOne(new LambdaQueryWrapper<SupplyHierarchy>()
+                    .eq(SupplyHierarchy::getSystemId, systemId)
+                    .eq(SupplyHierarchy::getChildId, currentId)
+                    .eq(SupplyHierarchy::getStatus, "active"));
+            if (hierarchy == null || hierarchy.getParentId() == null) {
+                break;
+            }
+            User parent = userMapper.selectById(hierarchy.getParentId());
+            if (parent == null) {
+                break;
+            }
+            chain.add(parent);
+            currentId = parent.getId();
+        }
+        return chain;
+    }
+
+    public SupplyHierarchy getRelation(Long systemId, Long userId) {
+        return this.getOne(new LambdaQueryWrapper<SupplyHierarchy>()
+                .eq(SupplyHierarchy::getSystemId, systemId)
+                .eq(SupplyHierarchy::getChildId, userId)
+                .eq(SupplyHierarchy::getStatus, "active"));
+    }
+
+    public List<SupplyHierarchyChangeRequest> getChangeRequests(Long systemId, String status) {
+        LambdaQueryWrapper<SupplyHierarchyChangeRequest> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SupplyHierarchyChangeRequest::getSystemId, systemId);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(SupplyHierarchyChangeRequest::getStatus, status);
+        }
+        wrapper.orderByDesc(SupplyHierarchyChangeRequest::getCreatedAt);
+        return changeRequestMapper.selectList(wrapper);
+    }
+
+    @Transactional
+    public void applyChange(Long systemId, Long applicantId, Long targetParentId, String reason) {
+        if (applicantId.equals(targetParentId)) {
+            throw new IllegalArgumentException("不能将自己设为上级");
+        }
+
+        LambdaQueryWrapper<SupplyHierarchyChangeRequest> pendingWrapper = new LambdaQueryWrapper<>();
+        pendingWrapper.eq(SupplyHierarchyChangeRequest::getSystemId, systemId)
+                .eq(SupplyHierarchyChangeRequest::getApplicantId, applicantId)
+                .eq(SupplyHierarchyChangeRequest::getStatus, "pending");
+        if (changeRequestMapper.selectCount(pendingWrapper) > 0) {
+            throw new IllegalArgumentException("存在待审核的申请");
+        }
+
+        SupplyHierarchyChangeRequest request = new SupplyHierarchyChangeRequest();
+        request.setSystemId(systemId);
+        request.setApplicantId(applicantId);
+        request.setTargetParentId(targetParentId);
+        request.setReason(reason);
+        request.setStatus("pending");
+        request.setCreatedAt(new Date());
+        changeRequestMapper.insert(request);
+    }
+
+    @Transactional
+    public void reviewChange(Long requestId, String status, String rejectReason, Long reviewedBy) {
+        SupplyHierarchyChangeRequest request = changeRequestMapper.selectById(requestId);
+        if (request == null) {
+            throw new IllegalArgumentException("申请不存在");
+        }
+        if (!"pending".equals(request.getStatus())) {
+            throw new IllegalArgumentException("申请已处理");
+        }
+
+        request.setStatus(status);
+        request.setReviewedAt(new Date());
+        request.setReviewedBy(reviewedBy);
+        if ("rejected".equals(status)) {
+            request.setRejectReason(rejectReason);
+        }
+        changeRequestMapper.updateById(request);
+
+        if ("approved".equals(status)) {
+            removeRelation(request.getSystemId(), request.getApplicantId());
+            setHierarchy(request.getSystemId(), request.getTargetParentId(), request.getApplicantId());
+        }
+    }
+}

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

@@ -2792,3 +2792,32 @@ CREATE TABLE IF NOT EXISTS ai_user_facts (
   INDEX idx_user_key (user_id, fact_key),
   INDEX idx_user_key (user_id, fact_key),
   INDEX idx_conversation (source_conversation)
   INDEX idx_conversation (source_conversation)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI用户关键事实';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI用户关键事实';
+
+CREATE TABLE IF NOT EXISTS supply_hierarchy (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    system_id BIGINT NOT NULL COMMENT '供应商体系ID',
+    parent_id BIGINT NOT NULL COMMENT '上级供应商ID',
+    child_id BIGINT NOT NULL COMMENT '下级供应商ID',
+    level INT DEFAULT 1 COMMENT '层级深度',
+    status VARCHAR(20) DEFAULT 'active',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_system_parent_child (system_id, parent_id, child_id),
+    KEY idx_system_child (system_id, child_id),
+    KEY idx_parent (parent_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商上下级关系表';
+
+CREATE TABLE IF NOT EXISTS supply_hierarchy_change_request (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    system_id BIGINT NOT NULL,
+    applicant_id BIGINT NOT NULL COMMENT '申请人ID',
+    target_parent_id BIGINT NOT NULL COMMENT '目标上级ID',
+    status VARCHAR(20) DEFAULT 'pending',
+    reason VARCHAR(500),
+    reject_reason VARCHAR(500),
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    reviewed_at DATETIME,
+    reviewed_by BIGINT,
+    KEY idx_system_applicant (system_id, applicant_id),
+    KEY idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商更换上级申请表';