From 0642bc9c44d1c8277ada33059c4a3e6156335b30 Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期二, 06 十月 2026 14:48:37 +0800
Subject: [PATCH] feat: use role-only menu permissions

---
 emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java |  898 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 836 insertions(+), 62 deletions(-)

diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
index 7d8f9b5..c2b4797 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
@@ -1,41 +1,52 @@
 package com.emergencyrepair.system.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.emergencyrepair.business.entity.BaseEquipment;
-import com.emergencyrepair.business.entity.Contractor;
 import com.emergencyrepair.business.entity.EngineeringMajor;
 import com.emergencyrepair.business.entity.TeamMajor;
 import com.emergencyrepair.business.entity.ProductNode;
 import com.emergencyrepair.business.mapper.BaseEquipmentMapper;
-import com.emergencyrepair.business.mapper.ContractorMapper;
 import com.emergencyrepair.business.mapper.EngineeringMajorMapper;
 import com.emergencyrepair.business.mapper.ProductNodeMapper;
 import com.emergencyrepair.business.mapper.TeamMajorMapper;
 import com.emergencyrepair.common.BusinessException;
 import com.emergencyrepair.system.entity.SysMenu;
+import com.emergencyrepair.system.entity.SysLoginLog;
+import com.emergencyrepair.system.entity.SysOperationLog;
 import com.emergencyrepair.system.entity.SysRole;
 import com.emergencyrepair.system.entity.SysRoleMenu;
 import com.emergencyrepair.system.entity.SysRoleUser;
 import com.emergencyrepair.system.entity.SysTenantMenu;
 import com.emergencyrepair.system.mapper.SysMenuMapper;
+import com.emergencyrepair.system.mapper.SysLoginLogMapper;
+import com.emergencyrepair.system.mapper.SysOperationLogMapper;
 import com.emergencyrepair.system.mapper.SysRoleMapper;
 import com.emergencyrepair.system.mapper.SysRoleMenuMapper;
 import com.emergencyrepair.system.mapper.SysRoleUserMapper;
 import com.emergencyrepair.system.mapper.SysTenantMenuMapper;
 import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
+import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 @Service
@@ -46,14 +57,28 @@
     private final SysRoleMenuMapper roleMenuMapper;
     private final SysTenantMenuMapper tenantMenuMapper;
     private final SysRoleUserMapper roleUserMapper;
-    private final ContractorMapper contractorMapper;
+    private final SysLoginLogMapper loginLogMapper;
+    private final SysOperationLogMapper operationLogMapper;
+    private final ButtonPermissionCatalog buttonPermissionCatalog;
+    private final JdbcTemplate jdbcTemplate;
     private final EngineeringMajorMapper engineeringMajorMapper;
     private final TeamMajorMapper teamMajorMapper;
     private final ProductNodeMapper productNodeMapper;
     private final BaseEquipmentMapper baseEquipmentMapper;
 
     public List<SysMenu> menuTree() {
-        List<SysMenu> menus = listMenus();
+        // Buttons are permission points, not navigable nodes.
+        List<SysMenu> navigable = listMenus().stream()
+                .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
+                .collect(Collectors.toList());
+        return buildTree(navigable);
+    }
+
+    public List<SysMenu> menuTreeWithButtons() {
+        return buildTree(listMenus());
+    }
+
+    private List<SysMenu> buildTree(List<SysMenu> menus) {
         Map<Long, SysMenu> map = menus.stream().collect(Collectors.toMap(SysMenu::getId, item -> item));
         List<SysMenu> roots = new ArrayList<>();
         for (SysMenu menu : menus) {
@@ -75,6 +100,29 @@
     public SysMenu saveMenu(SysMenu menu) {
         if (!StringUtils.hasText(menu.getName())) {
             throw new BusinessException("鑿滃崟鍚嶇О涓嶈兘涓虹┖");
+        }
+        if (menu.getMenuType() == null) {
+            menu.setMenuType(menu.getId() == null ? 1 : menu.getMenuType());
+        }
+        boolean isRoot = menu.getPid() == null || menu.getPid() == 0L;
+        if (menu.getMenuType() == 2) {
+            if (isRoot) {
+                throw new BusinessException("鎸夐挳绫诲瀷鍙兘寤哄湪鑿滃崟绫诲瀷涓嬬骇");
+            }
+            SysMenu parent = menuMapper.selectById(menu.getPid());
+            if (parent == null || parent.getMenuType() == null || parent.getMenuType() != 1) {
+                throw new BusinessException("鎸夐挳绫诲瀷鍙兘寤哄湪鑿滃崟绫诲瀷涓嬬骇");
+            }
+            if (!StringUtils.hasText(menu.getPermissions())) {
+                throw new BusinessException("鎸夐挳鏉冮檺鏍囪瘑涓嶈兘涓虹┖");
+            }
+            menu.setUrl(null);
+            menu.setIcon(null);
+        } else if (!isRoot) {
+            SysMenu parent = menuMapper.selectById(menu.getPid());
+            if (parent != null && parent.getMenuType() != null && parent.getMenuType() == 2) {
+                throw new BusinessException("鎸夐挳绫诲瀷涓嬬骇涓嶈兘鍐嶅缓鑿滃崟");
+            }
         }
         LocalDateTime now = LocalDateTime.now();
         menu.setUpdateTime(now);
@@ -131,30 +179,27 @@
         roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getRoleId, id));
     }
 
-    public List<Contractor> listContractors() {
-        return contractorMapper.selectList(Wrappers.<Contractor>lambdaQuery()
-                .orderByAsc(Contractor::getSortNo).orderByAsc(Contractor::getId));
-    }
-
-    @Transactional
-    public Contractor saveContractor(Contractor contractor) {
-        if (!StringUtils.hasText(contractor.getName())) {
-            throw new BusinessException("鎵夸慨鍗曚綅鍚嶇О涓嶈兘涓虹┖");
-        }
-        LocalDateTime now = LocalDateTime.now();
-        contractor.setUpdateTime(now);
-        if (contractor.getId() == null) {
-            contractor.setCreateTime(now);
-            contractorMapper.insert(contractor);
-        } else {
-            contractorMapper.updateById(contractor);
-        }
-        return contractorMapper.selectById(contractor.getId());
-    }
-
-    @Transactional
-    public void deleteContractor(Long id) {
-        contractorMapper.deleteById(id);
+    /**
+     * Contractor units are maintained as department records with DEPT_TYPE='cj'.
+     */
+    public List<Map<String, Object>> listContractors() {
+        return jdbcTemplate.query(
+                "SELECT DEPT_ID, DEPT_NAME, ORDER_NUM, LEADER, PHONE, STATUS, DEPT_CODE FROM CSICZB_ZB.SYS_DEPT "
+                        + "WHERE DEPT_TYPE = 'cj' AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL) ORDER BY ORDER_NUM, DEPT_ID",
+                (rs, rowNum) -> {
+                    Map<String, Object> row = new LinkedHashMap<>();
+                    row.put("id", String.valueOf(rs.getLong("DEPT_ID")));
+                    row.put("name", rs.getString("DEPT_NAME"));
+                    row.put("shortName", null);
+                    row.put("nature", "鎵夸慨鍗曚綅");
+                    row.put("sortNo", rs.getObject("ORDER_NUM"));
+                    row.put("contact", rs.getString("LEADER"));
+                    row.put("phone", rs.getString("PHONE"));
+                    row.put("status", rs.getString("STATUS"));
+                    row.put("deptCode", rs.getString("DEPT_CODE"));
+                    row.put("dishonest", 0);
+                    return row;
+                });
     }
 
     public List<EngineeringMajor> listEngineeringMajors() {
@@ -201,12 +246,12 @@
     @Transactional
     public TeamMajor saveTeamMajor(TeamMajor major) {
         if (major == null || !StringUtils.hasText(major.getName())) {
-            throw new BusinessException("T闃熶笓涓氬悕绉颁笉鑳戒负绌�");
+            throw new BusinessException("rk_T闃熶笓涓氬悕绉颁笉鑳戒负绌�");
         }
         String name = major.getName().trim();
         String category = StringUtils.hasText(major.getCategory()) ? major.getCategory().trim().toUpperCase() : "";
         if (!"A".equals(category) && !"B".equals(category)) {
-            throw new BusinessException("T闃熶笓涓氬垎绫诲彧鑳芥槸A鎴朆");
+            throw new BusinessException("rk_T闃熶笓涓氬垎绫诲彧鑳芥槸A鎴朆");
         }
         LambdaQueryWrapper<TeamMajor> duplicate = Wrappers.<TeamMajor>lambdaQuery()
                 .eq(TeamMajor::getName, name);
@@ -214,7 +259,7 @@
             duplicate.ne(TeamMajor::getId, major.getId());
         }
         if (teamMajorMapper.selectCount(duplicate) > 0) {
-            throw new BusinessException("T闃熶笓涓氬悕绉板凡瀛樺湪锛�" + name);
+            throw new BusinessException("rk_T闃熶笓涓氬悕绉板凡瀛樺湪锛�" + name);
         }
         LocalDateTime now = LocalDateTime.now();
         major.setName(name);
@@ -244,9 +289,180 @@
         throw new BusinessException("鏈煡鏍戠被鍨�");
     }
 
+    public List<Map<String, Object>> selectedProductNodes(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<ProductNode> source = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .in(ProductNode::getId, ids));
+        Map<Long, ProductNode> unique = uniqueProductNodes(source);
+        List<Map<String, Object>> result = new ArrayList<>();
+        Set<Long> seen = new HashSet<>();
+        for (Long id : ids) {
+            ProductNode node = unique.get(id);
+            if (node == null || !seen.add(id)) {
+                continue;
+            }
+            List<ProductNode> path = productPath(node);
+            Map<String, Object> row = mapProduct(node, Collections.emptySet());
+            row.put("pathNames", path.stream().map(ProductNode::getName).collect(Collectors.toList()));
+            row.put("parentPathNames", path.stream()
+                    .filter(item -> item != node)
+                    .filter(item -> item.getNodeLevel() != null && item.getNodeLevel() > 2)
+                    .map(ProductNode::getName)
+                    .collect(Collectors.toList()));
+            ProductNode xNode = path.stream()
+                    .filter(item -> item.getNodeLevel() != null && item.getNodeLevel() == 2)
+                    .findFirst().orElse(null);
+            row.put("xNo", xNode == null ? null : xNode.getName());
+            result.add(row);
+        }
+        return result;
+    }
+
+    private List<Map<String, Object>> mapProductTreeNodes(List<ProductNode> source, Set<Long> forcedBranchIds) {
+        Map<Long, ProductNode> unique = uniqueProductNodes(source);
+        List<ProductNode> nodes = sortProductNodes(unique.values());
+        Set<Long> branchIds = new HashSet<>(forcedBranchIds);
+        if (!unique.isEmpty()) {
+            List<ProductNode> childRows = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                    .select(ProductNode::getParentId, ProductNode::getIsDelete)
+                    .in(ProductNode::getParentId, unique.keySet()));
+            for (ProductNode child : childRows) {
+                if (!"1".equals(child.getIsDelete()) && child.getParentId() != null) {
+                    branchIds.add(child.getParentId());
+                }
+            }
+        }
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (ProductNode node : nodes) {
+            rows.add(mapProduct(node, branchIds));
+        }
+        return rows;
+    }
+
+    private Map<Long, ProductNode> uniqueProductNodes(Collection<ProductNode> source) {
+        Map<Long, ProductNode> unique = new LinkedHashMap<>();
+        if (source == null) {
+            return unique;
+        }
+        for (ProductNode node : source) {
+            if (node.getId() == null || "1".equals(node.getIsDelete())) {
+                continue;
+            }
+            unique.putIfAbsent(node.getId(), node);
+        }
+        return unique;
+    }
+
+    private List<ProductNode> sortProductNodes(Collection<ProductNode> source) {
+        List<ProductNode> nodes = new ArrayList<>(source);
+        nodes.sort(Comparator.comparingInt(this::productNodeLevel)
+                .thenComparing(ProductNode::getSortNo, Comparator.nullsLast(Integer::compareTo))
+                .thenComparing(ProductNode::getId, Comparator.nullsLast(Long::compareTo)));
+        return nodes;
+    }
+
+    private ProductNode findProductRoot(ProductNode node) {
+        ProductNode current = node;
+        Set<Long> visited = new HashSet<>();
+        while (current != null && current.getParentId() != null && current.getParentId() != 0L
+                && visited.add(current.getId())) {
+            current = findProductNode(current.getParentId());
+        }
+        return current;
+    }
+
+    private List<ProductNode> productPath(ProductNode node) {
+        List<ProductNode> reversed = new ArrayList<>();
+        Set<Long> visited = new HashSet<>();
+        ProductNode current = node;
+        while (current != null && visited.add(current.getId())) {
+            reversed.add(current);
+            if (current.getParentId() == null || current.getParentId() == 0L) {
+                break;
+            }
+            current = findProductNode(current.getParentId());
+        }
+        Collections.reverse(reversed);
+        return reversed;
+    }
+
+    public List<Map<String, Object>> productTreeForXNo(String xNo) {
+        if (!StringUtils.hasText(xNo)) {
+            return productTreeRoots();
+        }
+        List<ProductNode> rootNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .and(wrapper -> wrapper.isNull(ProductNode::getParentId).or().eq(ProductNode::getParentId, 0L))
+                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
+        List<ProductNode> sideNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .in(ProductNode::getLayer, Arrays.asList("side", "SIDE"))
+                .eq(ProductNode::getName, xNo.trim())
+                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
+        Map<Long, ProductNode> roots = uniqueProductNodes(rootNodes);
+        Map<Long, ProductNode> sides = uniqueProductNodes(sideNodes);
+        Set<Long> branchIds = new HashSet<>();
+        branchIds.addAll(roots.keySet());
+        branchIds.addAll(sides.keySet());
+        Map<Long, Map<String, Object>> rootRows = new LinkedHashMap<>();
+        for (ProductNode root : sortProductNodes(roots.values())) {
+            rootRows.put(root.getId(), mapProduct(root, branchIds));
+        }
+        for (ProductNode side : sortProductNodes(sides.values())) {
+            ProductNode root = findProductRoot(side);
+            Map<String, Object> rootRow = root == null ? null : rootRows.get(root.getId());
+            if (rootRow != null) {
+                children(rootRow).add(mapProduct(side, branchIds));
+            }
+        }
+        return new ArrayList<>(rootRows.values());
+    }
+
+    public List<String> productXNumbers() {
+        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .in(ProductNode::getLayer, Arrays.asList("side", "SIDE"))
+                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
+        return nodes.stream()
+                .filter(node -> !"1".equals(node.getIsDelete()))
+                .map(ProductNode::getName)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .sorted(String.CASE_INSENSITIVE_ORDER)
+                .collect(Collectors.toList());
+    }
+
+    public List<Map<String, Object>> productTreeRoots() {
+        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .and(wrapper -> wrapper.isNull(ProductNode::getParentId).or().eq(ProductNode::getParentId, 0L))
+                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
+        return mapProductTreeNodes(nodes, Collections.emptySet());
+    }
+
+    public List<Map<String, Object>> productTreeChildren(Long parentId) {
+        if (parentId == null || parentId == 0L) {
+            return productTreeRoots();
+        }
+        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .eq(ProductNode::getParentId, parentId)
+                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
+        return mapProductTreeNodes(nodes, Collections.emptySet());
+    }
+
     public List<Map<String, Object>> productTree() {
         List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
-                .orderByAsc(ProductNode::getNodeLevel).orderByAsc(ProductNode::getSortNo));
+                .orderByAsc(ProductNode::getId).orderByAsc(ProductNode::getSortNo));
+        Map<Long, ProductNode> unique = new LinkedHashMap<>();
+        for (ProductNode node : nodes) {
+            if (node.getId() == null || "1".equals(node.getIsDelete())) {
+                continue;
+            }
+            unique.putIfAbsent(node.getId(), node);
+        }
+        nodes = new ArrayList<>(unique.values());
+        nodes.sort(Comparator.comparingInt(this::productNodeLevel)
+                .thenComparing(ProductNode::getSortNo, Comparator.nullsLast(Integer::compareTo))
+                .thenComparing(ProductNode::getId, Comparator.nullsLast(Long::compareTo)));
         Map<Long, Map<String, Object>> map = new LinkedHashMap<>();
         for (ProductNode node : nodes) {
             map.put(node.getId(), mapProduct(node));
@@ -284,24 +500,48 @@
 
     @Transactional
     public ProductNode saveProductNode(ProductNode node) {
-        if (!StringUtils.hasText(node.getName()) || node.getNodeLevel() == null) {
-            throw new BusinessException("鑺傜偣鍚嶇О鍜屽眰绾т笉鑳戒负绌�");
+        if (!StringUtils.hasText(node.getName())) {
+            throw new BusinessException("鑺傜偣鍚嶇О涓嶈兘涓虹┖");
         }
         LocalDateTime now = LocalDateTime.now();
-        node.setUpdateTime(now);
+        String layer = resolveProductLayer(node);
+        node.setLayer(layer);
+        node.setConnectGroup(buildConnectGroup(node.getParentId()));
+
         if (node.getId() == null) {
+            node.setId(IdWorker.getId());
+            node.setRecordId(String.valueOf(node.getId()));
+            node.setProject(StringUtils.hasText(node.getProject()) ? node.getProject().trim() : "0");
+            node.setSortNo(node.getSortNo() == null ? 1 : node.getSortNo());
+            node.setIsDelete("0");
+            node.setCreateBy(StringUtils.hasText(node.getCreateBy()) ? node.getCreateBy().trim() : "system");
             node.setCreateTime(now);
             productNodeMapper.insert(node);
         } else {
+            ProductNode current = findProductNode(node.getId());
+            if (current == null) {
+                throw new BusinessException("鑺傜偣涓嶅瓨鍦�");
+            }
+            node.setRecordId(current.getRecordId());
+            node.setProject(StringUtils.hasText(current.getProject()) ? current.getProject() : "0");
+            node.setIsDelete(StringUtils.hasText(current.getIsDelete()) ? current.getIsDelete() : "0");
+            node.setCreateBy(current.getCreateBy());
+            node.setCreateTime(current.getCreateTime());
             productNodeMapper.updateById(node);
         }
-        return productNodeMapper.selectById(node.getId());
+        return findProductNode(node.getId());
     }
 
     @Transactional
     public void deleteProductNode(Long id) {
-        assertNoChildren(productNodeMapper.selectCount(Wrappers.<ProductNode>lambdaQuery().eq(ProductNode::getParentId, id)));
-        productNodeMapper.deleteById(id);
+        assertNoChildren(productNodeMapper.selectCount(Wrappers.<ProductNode>lambdaQuery()
+                .eq(ProductNode::getParentId, id).ne(ProductNode::getIsDelete, "1")));
+        ProductNode current = findProductNode(id);
+        if (current == null) {
+            throw new BusinessException("鑺傜偣涓嶅瓨鍦�");
+        }
+        current.setIsDelete("1");
+        productNodeMapper.updateById(current);
     }
 
     @Transactional
@@ -326,45 +566,529 @@
         baseEquipmentMapper.deleteById(id);
     }
 
-    public List<Map<String, Object>> tenants() {
-        return Collections.singletonList(row("id", 1L, "name", "涓存姠淇鎴�"));
-    }
-
-    public Map<String, Object> menuPermissions(Long roleId, Long tenantId) {
-        List<Long> roleMenuIds = roleMenuMapper.selectList(Wrappers.<SysRoleMenu>lambdaQuery()
-                        .eq(roleId != null, SysRoleMenu::getRoleId, roleId))
-                .stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
-        List<Long> tenantMenuIds = tenantMenuMapper.selectList(Wrappers.<SysTenantMenu>lambdaQuery()
-                        .eq(tenantId != null, SysTenantMenu::getTenantId, tenantId))
-                .stream().map(SysTenantMenu::getMenuId).collect(Collectors.toList());
-        List<Long> effective = roleMenuIds.stream().filter(tenantMenuIds::contains).collect(Collectors.toList());
+    public Map<String, Object> menuPermissions(Long roleId) {
+        if (roleId == null) {
+            throw new BusinessException("璇烽�夋嫨瑙掕壊");
+        }
+        List<String> roleMenuIds = roleMenuMapper.selectList(Wrappers.<SysRoleMenu>lambdaQuery()
+                        .eq(SysRoleMenu::getRoleId, roleId))
+                .stream().map(SysRoleMenu::getMenuId).map(String::valueOf).collect(Collectors.toList());
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("roleMenuIds", roleMenuIds);
-        result.put("tenantMenuIds", tenantMenuIds);
-        result.put("effectiveMenuIds", effective);
+        result.put("effectiveMenuIds", roleMenuIds);
         return result;
     }
 
     @Transactional
-    public void saveMenuPermissions(Long roleId, Long tenantId, List<Long> roleMenuIds, List<Long> tenantMenuIds) {
-        if (roleId == null || tenantId == null) {
-            throw new BusinessException("璇烽�夋嫨瑙掕壊鍜岀鎴�");
+    public void saveMenuPermissions(Long roleId, List<Long> roleMenuIds) {
+        if (roleId == null) {
+            throw new BusinessException("璇烽�夋嫨瑙掕壊");
         }
         roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getRoleId, roleId));
-        tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getTenantId, tenantId));
-        for (Long menuId : roleMenuIds == null ? Collections.<Long>emptyList() : roleMenuIds) {
+        List<Long> effectiveRoleMenuIds = expandWithAncestors(roleMenuIds);
+        for (Long menuId : effectiveRoleMenuIds) {
             SysRoleMenu relation = new SysRoleMenu();
             relation.setRoleId(roleId);
             relation.setMenuId(menuId);
             relation.setCreateTime(LocalDateTime.now());
             roleMenuMapper.insert(relation);
         }
-        for (Long menuId : tenantMenuIds == null ? Collections.<Long>emptyList() : tenantMenuIds) {
-            SysTenantMenu relation = new SysTenantMenu();
-            relation.setTenantId(tenantId);
-            relation.setMenuId(menuId);
+    }
+
+    public Map<String, Object> userPage(String keyword, Long deptId, Long roleId, int current, int size) {
+        List<Object> args = new ArrayList<>();
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT u.USER_ID, u.USER_NAME, u.NICK_NAME, u.DEPT_ID, d.DEPT_NAME, u.PHONENUMBER, u.STATUS, u.LOGIN_DATE ");
+        sql.append("FROM CSICZB_ZB.SYS_USER u LEFT JOIN CSICZB_ZB.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID ");
+        sql.append("WHERE (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL) ");
+        if (StringUtils.hasText(keyword)) {
+            sql.append("AND (u.USER_NAME LIKE ? OR u.NICK_NAME LIKE ?) ");
+            String like = "%" + keyword.trim() + "%";
+            args.add(like);
+            args.add(like);
+        }
+        if (deptId != null) {
+            sql.append("AND u.DEPT_ID = ? ");
+            args.add(deptId);
+        }
+        if (roleId != null) {
+            sql.append("AND EXISTS (SELECT 1 FROM LX.T_SYS_ROLE_USER ru WHERE ru.USER_ID = u.USER_ID AND ru.ROLE_ID = ? AND ru.DEL_FLAG = 0) ");
+            args.add(roleId);
+        }
+        sql.append("ORDER BY u.USER_ID");
+        List<Map<String, Object>> all = jdbcTemplate.query(sql.toString(), args.toArray(), (rs, rowNum) -> {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("userId", String.valueOf(rs.getLong("USER_ID")));
+            row.put("userName", rs.getString("USER_NAME"));
+            row.put("nickName", rs.getString("NICK_NAME"));
+            Object dept = rs.getObject("DEPT_ID");
+            row.put("deptId", dept == null ? null : String.valueOf(rs.getLong("DEPT_ID")));
+            row.put("deptName", rs.getString("DEPT_NAME"));
+            row.put("phoneNumber", rs.getString("PHONENUMBER"));
+            row.put("status", rs.getString("STATUS"));
+            java.sql.Timestamp loginDate = rs.getTimestamp("LOGIN_DATE");
+            row.put("loginDate", loginDate == null ? null : loginDate.toLocalDateTime().toString().replace('T', ' '));
+            row.put("roles", new ArrayList<Map<String, Object>>());
+            return row;
+        });
+        Map<Long, Map<String, Object>> byId = new HashMap<>();
+        for (Map<String, Object> row : all) {
+            byId.put(Long.valueOf(String.valueOf(row.get("userId"))), row);
+        }
+        if (!byId.isEmpty()) {
+            List<Map<String, Object>> roleRows = jdbcTemplate.query(
+                    "SELECT ru.USER_ID, r.ID, r.NAME, r.CODE, r.ROLE_TYPE FROM LX.T_SYS_ROLE_USER ru JOIN LX.T_SYS_ROLE r ON r.ID = ru.ROLE_ID AND r.DEL_FLAG = 0 WHERE ru.DEL_FLAG = 0 ORDER BY r.SORT_NO",
+                    (rs, rowNum) -> {
+                        Map<String, Object> role = new LinkedHashMap<>();
+                        role.put("userId", String.valueOf(rs.getLong("USER_ID")));
+                        role.put("roleId", String.valueOf(rs.getLong("ID")));
+                        role.put("roleName", rs.getString("NAME"));
+                        role.put("roleCode", rs.getString("CODE"));
+                        role.put("roleType", rs.getString("ROLE_TYPE"));
+                        return role;
+                    });
+            for (Map<String, Object> role : roleRows) {
+                Map<String, Object> user = byId.get(Long.valueOf(String.valueOf(role.get("userId"))));
+                if (user != null) {
+                    @SuppressWarnings("unchecked")
+                    List<Map<String, Object>> roles = (List<Map<String, Object>>) user.get("roles");
+                    roles.add(role);
+                }
+            }
+        }
+        int safeSize = size <= 0 ? 20 : size;
+        int safeCurrent = current <= 0 ? 1 : current;
+        int total = all.size();
+        int from = Math.min(total, (safeCurrent - 1) * safeSize);
+        int to = Math.min(total, from + safeSize);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("records", new ArrayList<>(all.subList(from, to)));
+        result.put("total", total);
+        result.put("current", safeCurrent);
+        result.put("size", safeSize);
+        result.put("pages", (total + safeSize - 1) / safeSize);
+        return result;
+    }
+
+    public List<Map<String, Object>> depts() {
+        return jdbcTemplate.query(
+                "SELECT DEPT_ID, DEPT_NAME, PARENT_ID, ORDER_NUM, DEPT_TYPE, LEADER, PHONE, STATUS, DEPT_CODE FROM CSICZB_ZB.SYS_DEPT WHERE (DEL_FLAG = '0' OR DEL_FLAG IS NULL) ORDER BY ORDER_NUM, DEPT_ID",
+                (rs, rowNum) -> {
+                    Map<String, Object> row = new LinkedHashMap<>();
+                    row.put("id", String.valueOf(rs.getLong("DEPT_ID")));
+                    row.put("name", rs.getString("DEPT_NAME"));
+                    Object parentId = rs.getObject("PARENT_ID");
+                    row.put("parentId", parentId == null ? null : String.valueOf(rs.getLong("PARENT_ID")));
+                    row.put("orderNum", rs.getObject("ORDER_NUM"));
+                    row.put("deptType", rs.getString("DEPT_TYPE"));
+                    row.put("leader", rs.getString("LEADER"));
+                    row.put("phone", rs.getString("PHONE"));
+                    row.put("status", rs.getString("STATUS"));
+                    row.put("deptCode", rs.getString("DEPT_CODE"));
+                    return row;
+                });
+    }
+
+    public List<String> userRoleIds(Long userId) {
+        if (userId == null) {
+            return new ArrayList<>();
+        }
+        return roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
+                        .eq(SysRoleUser::getUserId, userId))
+                .stream().map(item -> String.valueOf(item.getRoleId())).collect(Collectors.toList());
+    }
+
+    public Map<String, Object> roleUsers(Long roleId) {
+        if (roleId == null || roleMapper.selectById(roleId) == null) {
+            throw new BusinessException("瑙掕壊涓嶅瓨鍦�");
+        }
+        Set<Long> assignedUserIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
+                        .eq(SysRoleUser::getRoleId, roleId))
+                .stream().map(SysRoleUser::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
+        List<Map<String, Object>> users = jdbcTemplate.query(
+                "SELECT u.USER_ID, u.USER_NAME, u.NICK_NAME, u.DEPT_ID, d.DEPT_NAME, u.STATUS " +
+                        "FROM CSICZB_ZB.SYS_USER u LEFT JOIN CSICZB_ZB.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID " +
+                        "WHERE (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL) ORDER BY u.USER_ID",
+                (rs, rowNum) -> {
+                    Long userId = rs.getLong("USER_ID");
+                    Map<String, Object> user = new LinkedHashMap<>();
+                    user.put("userId", String.valueOf(userId));
+                    user.put("userName", rs.getString("USER_NAME"));
+                    user.put("nickName", rs.getString("NICK_NAME"));
+                    Object deptId = rs.getObject("DEPT_ID");
+                    user.put("deptId", deptId == null ? null : String.valueOf(rs.getLong("DEPT_ID")));
+                    user.put("deptName", rs.getString("DEPT_NAME"));
+                    user.put("status", rs.getString("STATUS"));
+                    user.put("assigned", assignedUserIds.contains(userId));
+                    return user;
+                });
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("records", users);
+        result.put("userIds", assignedUserIds.stream().map(String::valueOf).collect(Collectors.toList()));
+        result.put("total", users.size());
+        return result;
+    }
+
+    @Transactional
+    public void saveRoleUsers(Long roleId, List<Long> userIds) {
+        if (roleId == null || roleMapper.selectById(roleId) == null) {
+            throw new BusinessException("瑙掕壊涓嶅瓨鍦�");
+        }
+        Set<Long> distinct = new LinkedHashSet<>();
+        if (userIds != null) {
+            distinct.addAll(userIds);
+        }
+        distinct.remove(null);
+        for (Long userId : distinct) {
+            Integer exists = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
+                    Integer.class, userId);
+            if (exists == null || exists == 0) {
+                throw new BusinessException("鐢ㄦ埛涓嶅瓨鍦�");
+            }
+        }
+        roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getRoleId, roleId));
+        for (Long userId : distinct) {
+            SysRoleUser relation = new SysRoleUser();
+            relation.setRoleId(roleId);
+            relation.setUserId(userId);
             relation.setCreateTime(LocalDateTime.now());
-            tenantMenuMapper.insert(relation);
+            roleUserMapper.insert(relation);
+        }
+    }
+
+    @Transactional
+    public void saveUserRoles(Long userId, List<Long> roleIds) {
+        if (userId == null) {
+            throw new BusinessException("璇烽�夋嫨鐢ㄦ埛");
+        }
+        Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
+                Integer.class, userId);
+        if (exists == null || exists == 0) {
+            throw new BusinessException("鐢ㄦ埛涓嶅瓨鍦�");
+        }
+        Set<Long> distinct = new LinkedHashSet<>();
+        if (roleIds != null) {
+            distinct.addAll(roleIds);
+        }
+        for (Long roleId : distinct) {
+            if (roleId != null && roleMapper.selectById(roleId) == null) {
+                throw new BusinessException("瑙掕壊涓嶅瓨鍦�");
+            }
+        }
+        roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getUserId, userId));
+        for (Long roleId : distinct) {
+            if (roleId == null) {
+                continue;
+            }
+            SysRoleUser relation = new SysRoleUser();
+            relation.setRoleId(roleId);
+            relation.setUserId(userId);
+            relation.setCreateTime(LocalDateTime.now());
+            roleUserMapper.insert(relation);
+        }
+    }
+
+    @Transactional
+    public void saveUserRolesBatch(List<Long> userIds, List<Long> roleIds, String mode) {
+        Set<Long> distinctUsers = new LinkedHashSet<>();
+        if (userIds != null) {
+            distinctUsers.addAll(userIds);
+        }
+        distinctUsers.remove(null);
+        if (distinctUsers.isEmpty()) {
+            throw new BusinessException("\u8bf7\u9009\u62e9\u7528\u6237");
+        }
+        Set<Long> distinctRoles = new LinkedHashSet<>();
+        if (roleIds != null) {
+            distinctRoles.addAll(roleIds);
+        }
+        distinctRoles.remove(null);
+        for (Long roleId : distinctRoles) {
+            if (roleMapper.selectById(roleId) == null) {
+                throw new BusinessException("\u89d2\u8272\u4e0d\u5b58\u5728");
+            }
+        }
+        boolean append = !"replace".equalsIgnoreCase(mode == null ? "" : mode.trim());
+        for (Long userId : distinctUsers) {
+            Integer exists = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
+                    Integer.class, userId);
+            if (exists == null || exists == 0) {
+                throw new BusinessException("\u7528\u6237\u4e0d\u5b58\u5728");
+            }
+            Set<Long> targetRoleIds = new LinkedHashSet<>();
+            if (append) {
+                targetRoleIds.addAll(roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
+                                .eq(SysRoleUser::getUserId, userId))
+                        .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList()));
+            }
+            targetRoleIds.addAll(distinctRoles);
+            roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getUserId, userId));
+            for (Long roleId : targetRoleIds) {
+                SysRoleUser relation = new SysRoleUser();
+                relation.setRoleId(roleId);
+                relation.setUserId(userId);
+                relation.setCreateTime(LocalDateTime.now());
+                roleUserMapper.insert(relation);
+            }
+        }
+    }
+
+    public List<String> roleCodes(String username) {
+        Long userId = resolveUserId(username);
+        if (userId == null) {
+            return new ArrayList<>();
+        }
+        List<Long> roleIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
+                        .eq(SysRoleUser::getUserId, userId))
+                .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList());
+        if (roleIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return roleMapper.selectBatchIds(roleIds).stream()
+                .map(SysRole::getCode).filter(StringUtils::hasText).sorted().collect(Collectors.toList());
+    }
+
+    public Set<Long> effectiveMenuIdsForUser(String username) {
+        Set<Long> result = new LinkedHashSet<>();
+        if (!StringUtils.hasText(username)) {
+            return result;
+        }
+        if ("admin".equals(username)) {
+            listMenus().forEach(menu -> result.add(menu.getId()));
+            return result;
+        }
+        Long userId = resolveUserId(username);
+        if (userId == null) {
+            return result;
+        }
+        List<Long> roleIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
+                        .eq(SysRoleUser::getUserId, userId))
+                .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList());
+        if (roleIds.isEmpty()) {
+            return result;
+        }
+        Set<Long> roleMenuIds = new LinkedHashSet<>(roleMenuMapper.selectList(Wrappers.<SysRoleMenu>lambdaQuery()
+                        .in(SysRoleMenu::getRoleId, roleIds))
+                .stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList()));
+        result.addAll(roleMenuIds);
+        return result;
+    }
+
+    public List<SysMenu> menuTreeForUser(String username) {
+        Set<Long> allowed = new LinkedHashSet<>(effectiveMenuIdsForUser(username));
+        if (allowed.isEmpty() && !"admin".equals(username)) {
+            SysMenu dashboard = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                    .eq(SysMenu::getUrl, "/dashboard").last("LIMIT 1"));
+            if (dashboard != null) {
+                allowed.add(dashboard.getId());
+            }
+        }
+        final Set<Long> allowedIds = allowed;
+        List<SysMenu> navigable = listMenus().stream()
+                .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
+                .filter(menu -> allowedIds.contains(menu.getId()))
+                .collect(Collectors.toList());
+        return buildTree(navigable);
+    }
+
+    public List<String> permissionCodes(String username) {
+        Set<Long> menuIds = effectiveMenuIdsForUser(username);
+        return listMenus().stream()
+                .filter(menu -> menuIds.contains(menu.getId()))
+                .map(SysMenu::getPermissions)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .sorted()
+                .collect(Collectors.toList());
+    }
+
+    public Map<String, Object> menuButtons(Long menuId) {
+        SysMenu menu = menuMapper.selectById(menuId);
+        if (menu == null) {
+            throw new BusinessException("鑿滃崟涓嶅瓨鍦�");
+        }
+        List<Map<String, Object>> available = new ArrayList<>();
+        for (ButtonPermissionCatalog.ButtonDefinition definition : buttonPermissionCatalog.buttonsFor(menu.getUrl())) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("code", definition.getCode());
+            row.put("name", definition.getName());
+            available.add(row);
+        }
+        List<String> selected = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
+                        .eq(SysMenu::getPid, menuId)
+                        .eq(SysMenu::getMenuType, 2))
+                .stream().map(SysMenu::getPermissions).filter(StringUtils::hasText).collect(Collectors.toList());
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("menuId", String.valueOf(menuId));
+        result.put("menuName", menu.getName());
+        result.put("available", available);
+        result.put("selected", selected);
+        return result;
+    }
+
+    @Transactional
+    public void syncMenuButtons(Long menuId, List<String> codes) {
+        SysMenu menu = menuMapper.selectById(menuId);
+        if (menu == null) {
+            throw new BusinessException("鑿滃崟涓嶅瓨鍦�");
+        }
+        if (menu.getMenuType() == null || menu.getMenuType() != 1) {
+            throw new BusinessException("鍙湁鑿滃崟绫诲瀷鎵嶈兘閰嶇疆鎸夐挳");
+        }
+        List<ButtonPermissionCatalog.ButtonDefinition> definitions = buttonPermissionCatalog.buttonsFor(menu.getUrl());
+        Map<String, ButtonPermissionCatalog.ButtonDefinition> definitionMap = new LinkedHashMap<>();
+        for (ButtonPermissionCatalog.ButtonDefinition definition : definitions) {
+            definitionMap.put(definition.getCode(), definition);
+        }
+        Set<String> wanted = new LinkedHashSet<>();
+        if (codes != null) {
+            for (String code : codes) {
+                if (StringUtils.hasText(code) && definitionMap.containsKey(code)) {
+                    wanted.add(code);
+                }
+            }
+        }
+        List<SysMenu> existing = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getPid, menuId).eq(SysMenu::getMenuType, 2));
+        Map<String, SysMenu> existingByCode = new HashMap<>();
+        for (SysMenu child : existing) {
+            existingByCode.put(child.getPermissions(), child);
+            if (!wanted.contains(child.getPermissions())) {
+                menuMapper.deleteById(child.getId());
+                roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getMenuId, child.getId()));
+                tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getMenuId, child.getId()));
+            }
+        }
+        LocalDateTime now = LocalDateTime.now();
+        for (int index = 0; index < definitions.size(); index++) {
+            ButtonPermissionCatalog.ButtonDefinition definition = definitions.get(index);
+            if (!wanted.contains(definition.getCode())) {
+                continue;
+            }
+            SysMenu child = existingByCode.get(definition.getCode());
+            if (child == null) {
+                child = new SysMenu();
+                child.setPid(menuId);
+                child.setName(definition.getName());
+                child.setPermissions(definition.getCode());
+                child.setMenuType(2);
+                child.setSortNo((index + 1) * 10);
+                child.setVisible(1);
+                child.setCreateTime(now);
+                child.setUpdateTime(now);
+                menuMapper.insert(child);
+            } else {
+                child.setName(definition.getName());
+                child.setSortNo((index + 1) * 10);
+                child.setUpdateTime(now);
+                menuMapper.updateById(child);
+            }
+        }
+    }
+
+    public Page<SysLoginLog> loginLogPage(int current, int size, String keyword, String status, String startDate, String endDate) {
+        Page<SysLoginLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
+        LambdaQueryWrapper<SysLoginLog> wrapper = Wrappers.lambdaQuery();
+        if (StringUtils.hasText(keyword)) {
+            String value = keyword.trim();
+            wrapper.and(item -> item.like(SysLoginLog::getUserName, value).or().like(SysLoginLog::getNickName, value));
+        }
+        if (StringUtils.hasText(status)) {
+            wrapper.eq(SysLoginLog::getStatus, status.trim());
+        }
+        LocalDateTime start = parseStart(startDate);
+        LocalDateTime end = parseEnd(endDate);
+        if (start != null) {
+            wrapper.ge(SysLoginLog::getLoginTime, start);
+        }
+        if (end != null) {
+            wrapper.le(SysLoginLog::getLoginTime, end);
+        }
+        wrapper.orderByDesc(SysLoginLog::getLoginTime);
+        return loginLogMapper.selectPage(page, wrapper);
+    }
+
+    public Page<SysOperationLog> operationLogPage(int current, int size, String keyword, String module, String operationType, String startDate, String endDate) {
+        Page<SysOperationLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
+        LambdaQueryWrapper<SysOperationLog> wrapper = Wrappers.lambdaQuery();
+        if (StringUtils.hasText(keyword)) {
+            String value = keyword.trim();
+            wrapper.and(item -> item.like(SysOperationLog::getUserName, value)
+                    .or().like(SysOperationLog::getNickName, value)
+                    .or().like(SysOperationLog::getRequestUri, value));
+        }
+        if (StringUtils.hasText(module)) {
+            wrapper.eq(SysOperationLog::getModule, module.trim());
+        }
+        if (StringUtils.hasText(operationType)) {
+            wrapper.eq(SysOperationLog::getOperationType, operationType.trim());
+        }
+        LocalDateTime start = parseStart(startDate);
+        LocalDateTime end = parseEnd(endDate);
+        if (start != null) {
+            wrapper.ge(SysOperationLog::getCreateTime, start);
+        }
+        if (end != null) {
+            wrapper.le(SysOperationLog::getCreateTime, end);
+        }
+        wrapper.orderByDesc(SysOperationLog::getCreateTime);
+        return operationLogMapper.selectPage(page, wrapper);
+    }
+
+    public Long resolveUserId(String username) {
+        if (!StringUtils.hasText(username)) {
+            return null;
+        }
+        List<Long> ids = jdbcTemplate.query(
+                "SELECT USER_ID FROM CSICZB_ZB.SYS_USER WHERE USER_NAME = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
+                new Object[]{username}, (rs, rowNum) -> rs.getLong(1));
+        return ids.isEmpty() ? null : ids.get(0);
+    }
+
+    private List<Long> expandWithAncestors(List<Long> ids) {
+        Set<Long> result = new LinkedHashSet<>();
+        if (ids == null) {
+            return new ArrayList<>(result);
+        }
+        for (Long id : ids) {
+            Long cursor = id;
+            int guard = 0;
+            while (cursor != null && cursor != 0L && guard++ < 32) {
+                if (!result.add(cursor)) {
+                    break;
+                }
+                SysMenu menu = menuMapper.selectById(cursor);
+                if (menu == null) {
+                    break;
+                }
+                cursor = menu.getPid();
+            }
+        }
+        return new ArrayList<>(result);
+    }
+
+    private LocalDateTime parseStart(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return LocalDate.parse(value.trim()).atStartOfDay();
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    private LocalDateTime parseEnd(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return LocalDate.parse(value.trim()).atTime(23, 59, 59);
+        } catch (Exception ex) {
+            return null;
         }
     }
 
@@ -394,20 +1118,70 @@
     }
 
     private Map<String, Object> mapProduct(ProductNode node) {
+        return mapProduct(node, Collections.emptySet());
+    }
+
+    private Map<String, Object> mapProduct(ProductNode node, Set<Long> branchIds) {
         Map<String, Object> row = new LinkedHashMap<>();
         row.put("id", String.valueOf(node.getId()));
         row.put("parentId", node.getParentId() == null ? null : String.valueOf(node.getParentId()));
         row.put("nodeLevel", node.getNodeLevel());
         row.put("nodeType", node.getNodeType());
+        row.put("layer", node.getLayer());
+        row.put("connectGroup", node.getConnectGroup());
         row.put("name", node.getName());
         row.put("code", node.getCode());
         row.put("baseEquipmentId", node.getBaseEquipmentId() == null ? null : String.valueOf(node.getBaseEquipmentId()));
+        row.put("project", node.getProject());
         row.put("sortNo", node.getSortNo());
         row.put("remark", node.getRemark());
+        row.put("isLeaf", !branchIds.contains(node.getId()));
         row.put("children", new ArrayList<>());
         return row;
     }
 
+    private int productNodeLevel(ProductNode node) {
+        Integer level = node.getNodeLevel();
+        return level == null ? Integer.MAX_VALUE : level;
+    }
+
+    private String resolveProductLayer(ProductNode node) {
+        String layer = ProductNode.layerForType(node.getNodeType());
+        if (!StringUtils.hasText(layer)) {
+            layer = ProductNode.layerForLevel(node.getNodeLevel());
+        }
+        if (!StringUtils.hasText(layer)) {
+            layer = StringUtils.hasText(node.getLayer()) ? node.getLayer().trim().toLowerCase() : null;
+        }
+        if (!StringUtils.hasText(layer)) {
+            throw new BusinessException("鑺傜偣灞傜骇涓嶈兘涓虹┖");
+        }
+        return layer;
+    }
+
+    private String buildConnectGroup(Long parentId) {
+        if (parentId == null || parentId == 0L) {
+            return "0";
+        }
+        ProductNode parent = findProductNode(parentId);
+        if (parent == null) {
+            throw new BusinessException("鐖惰妭鐐逛笉瀛樺湪");
+        }
+        String prefix = StringUtils.hasText(parent.getConnectGroup()) ? parent.getConnectGroup() : "0";
+        return prefix + "," + parent.getId();
+    }
+
+    private ProductNode findProductNode(Long id) {
+        if (id == null) {
+            return null;
+        }
+        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
+                .eq(ProductNode::getId, id)
+                .orderByAsc(ProductNode::getRecordId)
+                .last("LIMIT 1"));
+        return nodes.isEmpty() ? null : nodes.get(0);
+    }
+
     private Map<String, Object> mapBase(BaseEquipment node) {
         Map<String, Object> row = new LinkedHashMap<>();
         row.put("id", String.valueOf(node.getId()));

--
Gitblit v1.9.1