zzw
昨天 0642bc9c44d1c8277ada33059c4a3e6156335b30
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或B");
            throw new BusinessException("rk_T队专业分类只能是A或B");
        }
        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()));