zzw
昨天 943b68c17b27f3b0f9973533f832c48dd6afbb6f
feat: reorganize system menus and add error logs
5个文件已添加
11个文件已修改
1个文件已删除
992 ■■■■■ 已修改文件
HANDOFF.md 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/router/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/ErrorLogManage.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuManage.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuPermission.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/RoleManage.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/RolePermissionDialog.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/交接文件_2026.10.3.md 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -753,3 +753,31 @@
**接手注意**
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 19. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 21-22 èœå•权限入口与错误日志)
**本轮完成内容**
- éœ€æ±‚ 21:移除独立“菜单权限管理”菜单,在角色管理操作列新增“权限设置”按钮,并复用菜单/按钮权限树弹窗;旧 `/system/menu-permissions` è·¯ç”±å’Œé¡µé¢å·²åˆ é™¤ã€‚
- éœ€æ±‚ 22:系统管理新增“系统配置 / æƒé™é…ç½® / æ—¥å¿—管理”三个二级目录;组织机构、承修单位、产品结构树、设备基础库、工程专业、T队专业、三员控制归入系统配置,用户管理、角色管理、菜单管理归入权限配置,登录日志、操作日志、错误日志归入日志管理。
- é”™è¯¯æ—¥å¿—为新增功能:`GlobalExceptionHandler` æ•获未处理的非业务异常并异步写入 `LX.T_SYS_ERROR_LOG`;业务校验异常不记录。列表支持模块、异常类型、时间筛选,详情展示完整堆栈。
- æ–°åº“初始化和旧库迁移均会删除旧菜单并补齐二级目录的父子菜单关系;权限树继续保留菜单节点和按钮节点,角色切换后复选框可反显。
**主要文件**
- åŽç«¯ï¼š`emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java`、`.../mapper/SysErrorLogMapper.java`、`.../log/ErrorLogRecorder.java`、`.../common/GlobalExceptionHandler.java`、`.../bootstrap/DataInitializer.java`、`.../bootstrap/DatabaseBootstrap.java`、`.../system/controller/SystemController.java`、`.../system/service/SystemService.java`、`.../system/service/ButtonPermissionCatalog.java`。
- å‰ç«¯ï¼š`emergency-repair-web/src/views/system/RoleManage.vue`、`RolePermissionDialog.vue`、`ErrorLogManage.vue`、`emergency-repair-web/src/router/index.js`、`MenuManage.vue`;`MenuPermission.vue` å·²åˆ é™¤ã€‚
**验证结果**
- åŽç«¯ `mvn -B -DskipTests clean package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req21-22.js` é€šè¿‡ï¼šä¸‰ä¸ªäºŒçº§ç›®å½•和错误日志菜单正常显示,旧菜单不存在,角色管理“权限设置”弹窗按钮节点数为 78 ä¸”反显正常。
- äººä¸ºè®¿é—®éžæ³•错误日志详情 ID åŽï¼Œè®°å½•自动落库并可在错误日志页面查看;验证用临时记录已从数据库清理。
- æˆªå›¾ï¼š`.codex-run/role-permission-dialog-verified.png`、`.codex-run/error-log-verified.png`。
**当前运行状态(2026-10-06)**
- å‰ç«¯ `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge CDP `9222`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261006-req21-22.out.log`、`.codex-run/backend-20261006-req21-22.err.log`。
**接手注意**
- é”™è¯¯æ—¥å¿—当前覆盖进入 Spring MVC å…¨å±€å¼‚常处理的异常;启动阶段异常、异步线程中未进入该处理器的异常暂不会自动落库。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
@@ -50,6 +50,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Order(2)
@@ -88,6 +89,7 @@
        seedButtonMenus();
        ensureAdminRoleAssignment();
        ensureMajorConfigMenus();
        ensureRoleMenuAncestors();
        seedMajors();
        seedContractors();
        seedTrees();
@@ -130,16 +132,24 @@
            createMenu("report-trend", reports, "故障数/备品备件消耗年度趋势", "/reports/trend", null, 1, 50);
            Long system = createMenu("system", null, "系统管理", null, "el-icon-setting", 0, 50);
            createMenu("organization", system, "组织机构", "/system/organization", null, 1, 5);
            createMenu("contractors", system, "承修单位管理", "/system/contractors", null, 1, 10);
            createMenu("product-tree", system, "产品结构树管理", "/system/product-tree", null, 1, 20);
            createMenu("base-equipment", system, "设备基础库管理", "/system/base-equipment", null, 1, 30);
            createMenu("engineering-majors", system, "工程专业配置", "/system/engineering-majors", null, 1, 35);
            createMenu("team-majors", system, "rk_T队专业配置", "/system/team-majors", null, 1, 36);
            createMenu("roles", system, "角色管理", "/system/roles", null, 1, 40);
            createMenu("menus", system, "菜单管理", "/system/menus", null, 1, 50);
            createMenu("menu-permissions", system, "菜单权限管理", "/system/menu-permissions", null, 1, 60);
            createMenu("three-member", system, "三员控制", "/system/three-member", null, 1, 70);
            Long systemConfig = createMenu("system-config", system, "系统配置", null, "el-icon-s-tools", 0, 10);
            createMenu("organization", systemConfig, "组织机构", "/system/organization", null, 1, 10);
            createMenu("contractors", systemConfig, "承修单位管理", "/system/contractors", null, 1, 20);
            createMenu("product-tree", systemConfig, "产品结构树管理", "/system/product-tree", null, 1, 30);
            createMenu("base-equipment", systemConfig, "设备基础库管理", "/system/base-equipment", null, 1, 40);
            createMenu("engineering-majors", systemConfig, "工程专业配置", "/system/engineering-majors", null, 1, 50);
            createMenu("team-majors", systemConfig, "rk_T队专业配置", "/system/team-majors", null, 1, 60);
            createMenu("three-member", systemConfig, "三员控制", "/system/three-member", null, 1, 70);
            Long permissionConfig = createMenu("permission-config", system, "权限配置", null, "el-icon-lock", 0, 20);
            createMenu("users", permissionConfig, "用户管理", "/system/users", null, 1, 10);
            createMenu("roles", permissionConfig, "角色管理", "/system/roles", null, 1, 20);
            createMenu("menus", permissionConfig, "菜单管理", "/system/menus", null, 1, 30);
            Long logConfig = createMenu("log-config", system, "日志管理", null, "el-icon-document", 0, 30);
            createMenu("login-logs", logConfig, "登录日志", "/system/login-logs", null, 1, 10);
            createMenu("operation-logs", logConfig, "操作日志", "/system/operation-logs", null, 1, 20);
            createMenu("error-logs", logConfig, "错误日志", "/system/error-logs", null, 1, 30);
        } else {
            for (SysMenu menu : menuMapper.selectList(null)) {
                menuIds.put(menu.getUrl() == null ? "id-" + menu.getId() : menu.getUrl(), menu.getId());
@@ -178,36 +188,112 @@
        SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "系统管理")
                .last("LIMIT 1"));
        Long pid = system == null ? 0L : system.getId();
        ensureMenu(pid, "工程专业配置", "/system/engineering-majors", 35);
        ensureMenu(pid, "rk_T队专业配置", "/system/team-majors", 36);
        SysMenu config = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "系统配置")
                .eq(SysMenu::getMenuType, 0)
                .last("LIMIT 1"));
        Long pid = config == null ? (system == null ? 0L : system.getId()) : config.getId();
        ensureMenu(pid, "工程专业配置", "/system/engineering-majors", 50);
        ensureMenu(pid, "rk_T队专业配置", "/system/team-majors", 60);
    }
    private void ensureMenu(Long pid, String name, String url, int sort) {
        if (menuMapper.selectCount(Wrappers.<SysMenu>lambdaQuery().eq(SysMenu::getUrl, url)) > 0) {
            return;
    private Long ensureGroupMenu(Long pid, String name, String icon, int sort) {
        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getPid, pid)
                .eq(SysMenu::getName, name)
                .eq(SysMenu::getMenuType, 0)
                .last("LIMIT 1"));
        LocalDateTime now = LocalDateTime.now();
        if (menu == null) {
            menu = new SysMenu();
            menu.setPid(pid);
            menu.setName(name);
            menu.setIcon(icon);
            menu.setMenuType(0);
            menu.setSortNo(sort);
            menu.setVisible(1);
            menu.setCreateTime(now);
            menu.setUpdateTime(now);
            menuMapper.insert(menu);
            return menu.getId();
        }
        SysMenu menu = new SysMenu();
        boolean changed = !java.util.Objects.equals(menu.getIcon(), icon)
                || !java.util.Objects.equals(menu.getSortNo(), sort)
                || !java.util.Objects.equals(menu.getVisible(), 1);
        if (changed) {
            menu.setIcon(icon);
            menu.setSortNo(sort);
            menu.setVisible(1);
            menu.setUpdateTime(now);
            menuMapper.updateById(menu);
        }
        return menu.getId();
    }
    private Long ensureMenu(Long pid, String name, String url, int sort) {
        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getUrl, url).last("LIMIT 1"));
        LocalDateTime now = LocalDateTime.now();
        if (menu == null) {
            menu = new SysMenu();
        menu.setPid(pid);
        menu.setName(name);
        menu.setUrl(url);
        menu.setMenuType(1);
        menu.setSortNo(sort);
        menu.setVisible(1);
        menu.setCreateTime(LocalDateTime.now());
        menu.setUpdateTime(LocalDateTime.now());
            menu.setCreateTime(now);
            menu.setUpdateTime(now);
        menuMapper.insert(menu);
            return menu.getId();
        }
        boolean changed = !java.util.Objects.equals(menu.getPid(), pid)
                || !java.util.Objects.equals(menu.getName(), name)
                || !java.util.Objects.equals(menu.getSortNo(), sort)
                || !java.util.Objects.equals(menu.getMenuType(), 1)
                || !java.util.Objects.equals(menu.getVisible(), 1);
        if (changed) {
            menu.setPid(pid);
            menu.setName(name);
            menu.setMenuType(1);
            menu.setSortNo(sort);
            menu.setVisible(1);
            menu.setUpdateTime(now);
            menuMapper.updateById(menu);
        }
        return menu.getId();
    }
    private void ensureSystemMenus() {
        SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "\u7cfb\u7edf\u7ba1\u7406").last("LIMIT 1"));
        Long pid = system == null ? 0L : system.getId();
        ensureMenu(pid, "组织机构", "/system/organization", 5);
        ensureMenu(pid, "\u7528\u6237\u7ba1\u7406", "/system/users", 37);
        ensureMenu(pid, "\u767b\u5f55\u65e5\u5fd7", "/system/login-logs", 65);
        ensureMenu(pid, "\u64cd\u4f5c\u65e5\u5fd7", "/system/operation-logs", 66);
        grantAdminMenus("/system/organization", "/system/users", "/system/login-logs", "/system/operation-logs");
        Long systemConfig = ensureGroupMenu(pid, "系统配置", "el-icon-s-tools", 10);
        Long permissionConfig = ensureGroupMenu(pid, "权限配置", "el-icon-lock", 20);
        Long logConfig = ensureGroupMenu(pid, "日志管理", "el-icon-document", 30);
        ensureMenu(systemConfig, "组织机构", "/system/organization", 10);
        ensureMenu(systemConfig, "承修单位管理", "/system/contractors", 20);
        ensureMenu(systemConfig, "产品结构树管理", "/system/product-tree", 30);
        ensureMenu(systemConfig, "设备基础库管理", "/system/base-equipment", 40);
        ensureMenu(systemConfig, "工程专业配置", "/system/engineering-majors", 50);
        ensureMenu(systemConfig, "rk_T队专业配置", "/system/team-majors", 60);
        ensureMenu(systemConfig, "三员控制", "/system/three-member", 70);
        ensureMenu(permissionConfig, "用户管理", "/system/users", 10);
        ensureMenu(permissionConfig, "角色管理", "/system/roles", 20);
        ensureMenu(permissionConfig, "菜单管理", "/system/menus", 30);
        ensureMenu(logConfig, "登录日志", "/system/login-logs", 10);
        ensureMenu(logConfig, "操作日志", "/system/operation-logs", 20);
        ensureMenu(logConfig, "错误日志", "/system/error-logs", 30);
        removeMenuByUrl("/system/menu-permissions");
        grantAdminMenuIds(systemConfig, permissionConfig, logConfig);
        grantAdminMenus("/system/organization", "/system/contractors", "/system/product-tree",
                "/system/base-equipment", "/system/engineering-majors", "/system/team-majors",
                "/system/three-member", "/system/users", "/system/roles", "/system/menus",
                "/system/login-logs", "/system/operation-logs", "/system/error-logs");
    }
    private void grantAdminMenus(String... urls) {
@@ -226,6 +312,61 @@
        }
    }
    private void grantAdminMenuIds(Long... menuIds) {
        SysRole adminRole = roleMapper.selectOne(Wrappers.<SysRole>lambdaQuery()
                .eq(SysRole::getCode, "SYS_ADMIN").last("LIMIT 1"));
        if (adminRole == null) {
            return;
        }
        for (Long menuId : menuIds) {
            ensureRoleMenu(adminRole.getId(), menuId);
            ensureTenantMenu(1L, menuId);
        }
    }
    private void removeMenuByUrl(String url) {
        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getUrl, url).last("LIMIT 1"));
        if (menu == null) {
            return;
        }
        List<SysMenu> children = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getPid, menu.getId()));
        for (SysMenu child : children) {
            removeMenuRelations(child.getId());
            menuMapper.deleteById(child.getId());
        }
        removeMenuRelations(menu.getId());
        menuMapper.deleteById(menu.getId());
    }
    private void removeMenuRelations(Long menuId) {
        roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getMenuId, menuId));
        tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getMenuId, menuId));
    }
    private void ensureRoleMenuAncestors() {
        Map<Long, SysMenu> menuById = menuMapper.selectList(null).stream()
                .collect(java.util.stream.Collectors.toMap(SysMenu::getId, item -> item));
        List<SysRoleMenu> relations = new ArrayList<>(roleMenuMapper.selectList(null));
        Set<String> existing = relations.stream()
                .map(item -> item.getRoleId() + "-" + item.getMenuId())
                .collect(java.util.stream.Collectors.toSet());
        for (SysRoleMenu relation : relations) {
            SysMenu menu = menuById.get(relation.getMenuId());
            Long cursor = menu == null ? null : menu.getPid();
            int guard = 0;
            while (cursor != null && cursor != 0L && guard++ < 32) {
                String key = relation.getRoleId() + "-" + cursor;
                if (existing.add(key)) {
                    ensureRoleMenu(relation.getRoleId(), cursor);
                }
                SysMenu parent = menuById.get(cursor);
                cursor = parent == null ? null : parent.getPid();
            }
        }
    }
    /**
     * Materializes the declarative button catalog as MENU_TYPE=2 rows.
     * Existing rows are left untouched so manual menu edits are preserved.
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java
@@ -17,7 +17,8 @@
            "T_FAULT", "T_ENGINEERING_MAJOR", "T_TEAM_MAJOR", "T_CONTRACTOR",
            "T_BASE_EQUIPMENT", "T_PRODUCT_TREE", "T_WORK_ORDER", "T_COMPLETION_INFO",
            "T_ACCEPTANCE", "T_SYS_MENU", "T_SYS_ROLE", "T_SYS_ROLE_MENU",
            "T_SYS_TENANT_MENU", "T_SYS_ROLE_USER", "T_SYS_LOGIN_LOG", "T_SYS_OPERATION_LOG"
            "T_SYS_TENANT_MENU", "T_SYS_ROLE_USER", "T_SYS_LOGIN_LOG", "T_SYS_OPERATION_LOG",
            "T_SYS_ERROR_LOG"
    };
    private final JdbcTemplate jdbcTemplate;
@@ -40,6 +41,7 @@
        ensureIndex("IX_T_COMPLETION_WORK_ORDER", "CREATE INDEX LX.IX_T_COMPLETION_WORK_ORDER ON LX.T_COMPLETION_INFO(WORK_ORDER_ID)");
        ensureIndex("IX_T_SYS_LOGIN_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_LOGIN_LOG_TIME ON LX.T_SYS_LOGIN_LOG(LOGIN_TIME)");
        ensureIndex("IX_T_SYS_OPERATION_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_OPERATION_LOG_TIME ON LX.T_SYS_OPERATION_LOG(CREATE_TIME)");
        ensureIndex("IX_T_SYS_ERROR_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_ERROR_LOG_TIME ON LX.T_SYS_ERROR_LOG(ERROR_TIME)");
        ensureIndex("IX_T_SYS_ROLE_USER_USER", "CREATE INDEX LX.IX_T_SYS_ROLE_USER_USER ON LX.T_SYS_ROLE_USER(USER_ID)");
    }
@@ -122,6 +124,7 @@
        ddl.put("T_SYS_ROLE_USER", "CREATE TABLE LX.T_SYS_ROLE_USER (ID BIGINT NOT NULL PRIMARY KEY, ROLE_ID BIGINT, USER_ID BIGINT, CREATE_TIME TIMESTAMP)");
        ddl.put("T_SYS_LOGIN_LOG", "CREATE TABLE LX.T_SYS_LOGIN_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), DEPT_NAME VARCHAR(120), LOGIN_IP VARCHAR(200), STATUS VARCHAR(20), MESSAGE VARCHAR(500), LOGIN_TIME TIMESTAMP)");
        ddl.put("T_SYS_OPERATION_LOG", "CREATE TABLE LX.T_SYS_OPERATION_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), MODULE VARCHAR(100), OPERATION_TYPE VARCHAR(40), TARGET_TYPE VARCHAR(100), TARGET_ID VARCHAR(80), DESCRIPTION VARCHAR(500), REQUEST_METHOD VARCHAR(10), REQUEST_URI VARCHAR(300), RESULT VARCHAR(20), CREATE_TIME TIMESTAMP)");
        ddl.put("T_SYS_ERROR_LOG", "CREATE TABLE LX.T_SYS_ERROR_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), MODULE VARCHAR(100), REQUEST_METHOD VARCHAR(10), REQUEST_URI VARCHAR(500), EXCEPTION_TYPE VARCHAR(300), MESSAGE VARCHAR(2000), STACK_TRACE CLOB, ERROR_TIME TIMESTAMP)");
        return ddl;
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java
@@ -1,17 +1,25 @@
package com.emergencyrepair.common;
import com.emergencyrepair.system.log.ErrorLogRecorder;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
    private final ErrorLogRecorder errorLogRecorder;
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusiness(BusinessException exception) {
        return Result.error(exception.getMessage());
    }
    @ExceptionHandler(Exception.class)
    public Result<Void> handleOther(Exception exception) {
    public Result<Void> handleOther(Exception exception, HttpServletRequest request) {
        errorLogRecorder.record(exception, request);
        exception.printStackTrace();
        return Result.error(exception.getMessage() == null ? "系统异常" : exception.getMessage());
    }
emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java
@@ -8,6 +8,7 @@
import com.emergencyrepair.business.entity.TeamMajor;
import com.emergencyrepair.common.Result;
import com.emergencyrepair.system.entity.SysMenu;
import com.emergencyrepair.system.entity.SysErrorLog;
import com.emergencyrepair.system.entity.SysLoginLog;
import com.emergencyrepair.system.entity.SysOperationLog;
import com.emergencyrepair.system.entity.SysRole;
@@ -111,6 +112,22 @@
        return Result.ok(systemService.operationLogPage(current, size, keyword, module, operationType, startDate, endDate));
    }
    @GetMapping("/error-logs")
    public Result<Page<SysErrorLog>> errorLogs(@RequestParam(defaultValue = "1") int current,
                                              @RequestParam(defaultValue = "20") int size,
                                              @RequestParam(required = false) String keyword,
                                              @RequestParam(required = false) String module,
                                              @RequestParam(required = false) String exceptionType,
                                              @RequestParam(required = false) String startDate,
                                              @RequestParam(required = false) String endDate) {
        return Result.ok(systemService.errorLogPage(current, size, keyword, module, exceptionType, startDate, endDate));
    }
    @GetMapping("/error-logs/{id}")
    public Result<SysErrorLog> errorLog(@PathVariable Long id) {
        return Result.ok(systemService.errorLog(id));
    }
    @PostMapping("/menu")
    public Result<SysMenu> createMenu(@RequestBody SysMenu menu) {
        menu.setId(null);
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java
New file
@@ -0,0 +1,31 @@
package com.emergencyrepair.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("LX.T_SYS_ERROR_LOG")
public class SysErrorLog extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    @JsonSerialize(using = ToStringSerializer.class)
    @TableField("USER_ID") private Long userId;
    @TableField("USER_NAME") private String userName;
    @TableField("NICK_NAME") private String nickName;
    @TableField("MODULE") private String module;
    @TableField("REQUEST_METHOD") private String requestMethod;
    @TableField("REQUEST_URI") private String requestUri;
    @TableField("EXCEPTION_TYPE") private String exceptionType;
    @TableField("MESSAGE") private String message;
    @TableField("STACK_TRACE") private String stackTrace;
    @TableField("ERROR_TIME") private LocalDateTime errorTime;
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java
New file
@@ -0,0 +1,107 @@
package com.emergencyrepair.system.log;
import com.emergencyrepair.common.audit.CurrentUserContext;
import com.emergencyrepair.system.entity.SysErrorLog;
import com.emergencyrepair.system.mapper.SysErrorLogMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.List;
/**
 * Persists unhandled server exceptions so administrators can inspect runtime bugs.
 */
@Component
@RequiredArgsConstructor
public class ErrorLogRecorder {
    private static final int MAX_MESSAGE_LENGTH = 2000;
    private static final int MAX_STACK_LENGTH = 60000;
    private final SysErrorLogMapper errorLogMapper;
    private final JdbcTemplate jdbcTemplate;
    public void record(Exception exception, HttpServletRequest request) {
        if (exception == null) {
            return;
        }
        try {
            String username = CurrentUserContext.get();
            Long userId = resolveUserId(username);
            SysErrorLog log = new SysErrorLog();
            log.setUserId(userId);
            log.setUserName(username);
            log.setNickName(resolveNickName(userId));
            log.setModule(resolveModule(request));
            log.setRequestMethod(request == null ? null : limit(request.getMethod(), 10));
            log.setRequestUri(request == null ? null : limit(request.getRequestURI(), 500));
            log.setExceptionType(limit(exception.getClass().getName(), 300));
            log.setMessage(limit(resolveMessage(exception), MAX_MESSAGE_LENGTH));
            log.setStackTrace(limit(stackTrace(exception), MAX_STACK_LENGTH));
            log.setErrorTime(LocalDateTime.now());
            errorLogMapper.insert(log);
        } catch (Exception ignored) {
            // Error logging must never interfere with the original error response.
        }
    }
    private 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 String resolveNickName(Long userId) {
        if (userId == null) {
            return null;
        }
        List<String> names = jdbcTemplate.query(
                "SELECT NICK_NAME FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ?",
                new Object[]{userId}, (rs, rowNum) -> rs.getString(1));
        return names.isEmpty() ? null : names.get(0);
    }
    private String resolveMessage(Exception exception) {
        String message = exception.getMessage();
        return StringUtils.hasText(message) ? message : exception.getClass().getSimpleName();
    }
    private String stackTrace(Exception exception) {
        StringWriter writer = new StringWriter();
        exception.printStackTrace(new PrintWriter(writer));
        return writer.toString();
    }
    private String resolveModule(HttpServletRequest request) {
        if (request == null || !StringUtils.hasText(request.getRequestURI())) {
            return "其他";
        }
        String uri = request.getRequestURI();
        if (uri.startsWith("/api/faults") || uri.startsWith("/api/fault")) return "故障管理";
        if (uri.startsWith("/api/work-orders") || uri.startsWith("/api/completion")) return "完工管理";
        if (uri.startsWith("/api/acceptances")) return "完工验收";
        if (uri.startsWith("/api/reports")) return "统计报表";
        if (uri.startsWith("/api/system/users")) return "用户管理";
        if (uri.startsWith("/api/system/roles")) return "角色管理";
        if (uri.startsWith("/api/system/menu")) return "菜单管理";
        if (uri.startsWith("/api/system/contractors")) return "承修单位管理";
        if (uri.startsWith("/api/system")) return "系统管理";
        return "其他";
    }
    private String limit(String value, int maxLength) {
        if (value == null || value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java
New file
@@ -0,0 +1,7 @@
package com.emergencyrepair.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emergencyrepair.system.entity.SysErrorLog;
public interface SysErrorLogMapper extends BaseMapper<SysErrorLog> {
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
@@ -86,12 +86,11 @@
        put("/system/engineering-majors", def("system:major:add", "\u65b0\u589e"), def("system:major:edit", "\u7f16\u8f91"), def("system:major:delete", "\u5220\u9664"));
        put("/system/team-majors", def("system:major:add", "\u65b0\u589e"), def("system:major:edit", "\u7f16\u8f91"), def("system:major:delete", "\u5220\u9664"));
        put("/system/users", def("system:users:query", "\u67e5\u8be2"), def("system:users:assign", "\u89d2\u8272\u6388\u6743"));
        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"));
        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"), def("system:roles:assign", "\u7528\u6237\u8bbe\u7f6e"));
        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"), def("system:roles:assign", "\u7528\u6237\u8bbe\u7f6e"), def("system:roles:permission", "\u6743\u9650\u8bbe\u7f6e"));
        put("/system/menus", def("system:menus:add", "\u65b0\u589e"), def("system:menus:edit", "\u7f16\u8f91"), def("system:menus:delete", "\u5220\u9664"), def("system:menus:buttons", "\u83dc\u5355\u6309\u94ae\u540c\u6b65"));
        put("/system/menu-permissions", def("system:menu-permissions:save", "\u4fdd\u5b58\u6388\u6743"));
        put("/system/login-logs", def("system:login-logs:query", "\u67e5\u8be2"), def("system:login-logs:export", "\u5bfc\u51fa"));
        put("/system/operation-logs", def("system:operation-logs:query", "\u67e5\u8be2"), def("system:operation-logs:export", "\u5bfc\u51fa"));
        put("/system/error-logs", def("system:error-logs:query", "\u67e5\u8be2"), def("system:error-logs:detail", "\u8be6\u60c5"));
    }
    private static void put(String url, ButtonDefinition... definitions) {
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
@@ -14,6 +14,7 @@
import com.emergencyrepair.business.mapper.TeamMajorMapper;
import com.emergencyrepair.common.BusinessException;
import com.emergencyrepair.system.entity.SysMenu;
import com.emergencyrepair.system.entity.SysErrorLog;
import com.emergencyrepair.system.entity.SysLoginLog;
import com.emergencyrepair.system.entity.SysOperationLog;
import com.emergencyrepair.system.entity.SysRole;
@@ -21,6 +22,7 @@
import com.emergencyrepair.system.entity.SysRoleUser;
import com.emergencyrepair.system.entity.SysTenantMenu;
import com.emergencyrepair.system.mapper.SysMenuMapper;
import com.emergencyrepair.system.mapper.SysErrorLogMapper;
import com.emergencyrepair.system.mapper.SysLoginLogMapper;
import com.emergencyrepair.system.mapper.SysOperationLogMapper;
import com.emergencyrepair.system.mapper.SysRoleMapper;
@@ -59,6 +61,7 @@
    private final SysRoleUserMapper roleUserMapper;
    private final SysLoginLogMapper loginLogMapper;
    private final SysOperationLogMapper operationLogMapper;
    private final SysErrorLogMapper errorLogMapper;
    private final ButtonPermissionCatalog buttonPermissionCatalog;
    private final JdbcTemplate jdbcTemplate;
    private final EngineeringMajorMapper engineeringMajorMapper;
@@ -887,9 +890,10 @@
            }
        }
        final Set<Long> allowedIds = allowed;
        Set<Long> visibleIds = new LinkedHashSet<>(expandWithAncestors(new ArrayList<>(allowed)));
        List<SysMenu> navigable = listMenus().stream()
                .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
                .filter(menu -> allowedIds.contains(menu.getId()))
                .filter(menu -> visibleIds.contains(menu.getId()))
                .collect(Collectors.toList());
        return buildTree(navigable);
    }
@@ -1038,6 +1042,47 @@
        return operationLogMapper.selectPage(page, wrapper);
    }
    public Page<SysErrorLog> errorLogPage(int current, int size, String keyword, String module, String exceptionType, String startDate, String endDate) {
        Page<SysErrorLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
        LambdaQueryWrapper<SysErrorLog> wrapper = Wrappers.lambdaQuery();
        wrapper.select(SysErrorLog::getId, SysErrorLog::getUserId, SysErrorLog::getUserName,
                SysErrorLog::getNickName, SysErrorLog::getModule, SysErrorLog::getRequestMethod,
                SysErrorLog::getRequestUri, SysErrorLog::getExceptionType, SysErrorLog::getMessage,
                SysErrorLog::getErrorTime);
        if (StringUtils.hasText(keyword)) {
            String value = keyword.trim();
            wrapper.and(item -> item.like(SysErrorLog::getUserName, value)
                    .or().like(SysErrorLog::getNickName, value)
                    .or().like(SysErrorLog::getRequestUri, value)
                    .or().like(SysErrorLog::getExceptionType, value)
                    .or().like(SysErrorLog::getMessage, value));
        }
        if (StringUtils.hasText(module)) {
            wrapper.eq(SysErrorLog::getModule, module.trim());
        }
        if (StringUtils.hasText(exceptionType)) {
            wrapper.like(SysErrorLog::getExceptionType, exceptionType.trim());
        }
        LocalDateTime start = parseStart(startDate);
        LocalDateTime end = parseEnd(endDate);
        if (start != null) {
            wrapper.ge(SysErrorLog::getErrorTime, start);
        }
        if (end != null) {
            wrapper.le(SysErrorLog::getErrorTime, end);
        }
        wrapper.orderByDesc(SysErrorLog::getErrorTime);
        return errorLogMapper.selectPage(page, wrapper);
    }
    public SysErrorLog errorLog(Long id) {
        SysErrorLog log = errorLogMapper.selectById(id);
        if (log == null) {
            throw new BusinessException("错误日志不存在");
        }
        return log;
    }
    public Long resolveUserId(String username) {
        if (!StringUtils.hasText(username)) {
            return null;
emergency-repair-web/src/router/index.js
@@ -36,8 +36,8 @@
      { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue'), meta: { title: '角色管理', cacheName: 'RoleManage' } },
      { path: 'system/login-logs', component: () => import('@/views/system/LoginLogManage.vue'), meta: { title: '登录日志', cacheName: 'LoginLogManage' } },
      { path: 'system/operation-logs', component: () => import('@/views/system/OperationLogManage.vue'), meta: { title: '操作日志', cacheName: 'OperationLogManage' } },
      { path: 'system/error-logs', component: () => import('@/views/system/ErrorLogManage.vue'), meta: { title: '错误日志', cacheName: 'ErrorLogManage' } },
      { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue'), meta: { title: '菜单管理', cacheName: 'MenuManage' } },
      { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue'), meta: { title: '菜单权限', cacheName: 'MenuPermission' } },
      { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue'), meta: { title: '三员管理', cacheName: 'ThreeMember' } }
    ]
  }
emergency-repair-web/src/views/system/ErrorLogManage.vue
New file
@@ -0,0 +1,151 @@
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>错误日志</h2><p>记录程序运行中的未处理异常,便于定位和修复缺陷</p></div>
    </div>
    <div class="table-panel">
      <div class="toolbar">
        <el-input v-model="filters.keyword" clearable placeholder="账户名 / å§“名 / è¯·æ±‚地址 / å¼‚常信息" style="width:280px" @keyup.enter.native="search"></el-input>
        <el-select v-model="filters.module" clearable placeholder="模块" style="width:150px">
          <el-option v-for="item in modules" :key="item" :label="item" :value="item"></el-option>
        </el-select>
        <el-input v-model="filters.exceptionType" clearable placeholder="异常类型" style="width:220px" @keyup.enter.native="search"></el-input>
        <el-date-picker
          v-model="filters.dates"
          type="daterange"
          value-format="yyyy-MM-dd"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          style="width:260px"
        ></el-date-picker>
        <el-button type="primary" icon="el-icon-search" v-permission="'system:error-logs:query'" @click="search">查询</el-button>
        <el-button icon="el-icon-refresh-left" @click="reset">重置</el-button>
      </div>
      <el-table v-loading="loading" :data="rows" border stripe height="calc(100vh - 250px)">
        <el-table-column prop="errorTime" label="发生时间" width="180"></el-table-column>
        <el-table-column prop="userName" label="账户名" width="120"></el-table-column>
        <el-table-column prop="nickName" label="姓名" width="110"></el-table-column>
        <el-table-column prop="module" label="模块" width="120"></el-table-column>
        <el-table-column prop="requestMethod" label="方式" width="75" align="center"></el-table-column>
        <el-table-column prop="requestUri" label="请求地址" min-width="230" show-overflow-tooltip></el-table-column>
        <el-table-column prop="exceptionType" label="异常类型" min-width="220" show-overflow-tooltip></el-table-column>
        <el-table-column prop="message" label="异常信息" min-width="280" show-overflow-tooltip></el-table-column>
        <el-table-column label="操作" width="80" fixed="right">
          <template slot-scope="{ row }">
            <el-button type="text" v-permission="'system:error-logs:detail'" @click="openDetail(row)">详情</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        class="pager"
        background
        layout="total, sizes, prev, pager, next"
        :total="total"
        :current-page="current"
        :page-size="size"
        :page-sizes="[20, 50, 100]"
        @current-change="handleCurrentChange"
        @size-change="handleSizeChange"
      ></el-pagination>
    </div>
    <el-dialog title="错误详情" :visible.sync="detailVisible" width="900px" append-to-body>
      <div v-loading="detailLoading">
        <div class="detail-meta">
          <span><b>发生时间:</b>{{ detail.errorTime }}</span>
          <span><b>操作人:</b>{{ detail.userName || '-' }}{{ detail.nickName ? ' / ' + detail.nickName : '' }}</span>
          <span><b>模块:</b>{{ detail.module || '-' }}</span>
        </div>
        <div class="detail-meta">
          <span><b>请求:</b>{{ (detail.requestMethod || '') + ' ' + (detail.requestUri || '') }}</span>
        </div>
        <div class="detail-meta">
          <span><b>异常类型:</b>{{ detail.exceptionType }}</span>
        </div>
        <div class="detail-meta">
          <span><b>异常信息:</b>{{ detail.message }}</span>
        </div>
        <pre class="stack-trace">{{ detail.stackTrace || '无堆栈信息' }}</pre>
      </div>
      <template slot="footer"><el-button @click="detailVisible = false">关闭</el-button></template>
    </el-dialog>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'ErrorLogManage',
  data() {
    return {
      rows: [],
      total: 0,
      current: 1,
      size: 20,
      loading: false,
      detailVisible: false,
      detailLoading: false,
      detail: {},
      modules: ['故障管理', '完工管理', '完工验收', '统计报表', '用户管理', '角色管理', '菜单管理', '承修单位管理', '系统管理', '其他'],
      filters: { keyword: '', module: '', exceptionType: '', dates: [] }
    }
  },
  created() { this.load() },
  methods: {
    load() {
      this.loading = true
      const dates = this.filters.dates || []
      api.get('/system/error-logs', {
        params: {
          keyword: this.filters.keyword || undefined,
          module: this.filters.module || undefined,
          exceptionType: this.filters.exceptionType || undefined,
          startDate: dates[0] || undefined,
          endDate: dates[1] || undefined,
          current: this.current,
          size: this.size
        }
      }).then(res => {
        const data = res.data || {}
        this.rows = data.records || []
        this.total = data.total || 0
      }).finally(() => { this.loading = false })
    },
    search() { this.current = 1; this.load() },
    reset() { this.filters = { keyword: '', module: '', exceptionType: '', dates: [] }; this.search() },
    openDetail(row) {
      this.detailLoading = true
      this.detail = {}
      this.detailVisible = true
      api.get('/system/error-logs/' + row.id).then(res => {
        this.detail = res.data || {}
      }).finally(() => { this.detailLoading = false })
    },
    handleCurrentChange(page) { this.current = page; this.load() },
    handleSizeChange(size) { this.size = size; this.current = 1; this.load() }
  }
}
</script>
<style scoped>
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.pager { margin-top: 12px; text-align: right; }
.detail-meta { margin-bottom: 8px; color: #42566b; line-height: 22px; word-break: break-all; }
.detail-meta b { color: #26394d; }
.stack-trace {
  max-height: 520px;
  margin: 12px 0 0;
  padding: 12px;
  border: 1px solid #dce4ec;
  border-radius: 4px;
  color: #30445a;
  background: #f7f9fb;
  font-family: Consolas, "Courier New", monospace;
  font-size: 12px;
  line-height: 18px;
  white-space: pre-wrap;
  word-break: break-all;
  overflow: auto;
}
</style>
emergency-repair-web/src/views/system/MenuManage.vue
@@ -63,7 +63,7 @@
    </el-dialog>
    <el-dialog :title="'页面按钮配置 - ' + (buttonMenu.name || '')" :visible.sync="buttonDialogVisible" width="620px" append-to-body>
      <p class="dialog-tip">下列按钮来自后端按钮目录,勾选后自动生成按钮类型菜单,可在菜单权限管理中授权给角色。</p>
      <p class="dialog-tip">下列按钮来自后端按钮目录,勾选后自动生成按钮类型菜单,可在角色管理的权限设置中授权给角色。</p>
      <el-checkbox-group v-model="buttonSelected" class="button-list">
        <el-checkbox v-for="item in buttonAvailable" :key="item.code" :label="item.code" class="button-item">
          <span class="button-name">{{ item.name }}</span>
emergency-repair-web/src/views/system/MenuPermission.vue
File was deleted
emergency-repair-web/src/views/system/RoleManage.vue
@@ -1,7 +1,7 @@
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>角色管理</h2><p>维护角色信息,并可按角色批量设置所属用户</p></div>
      <div><h2>角色管理</h2><p>维护角色信息,并可按角色设置所属用户和菜单按钮权限</p></div>
      <el-button type="primary" icon="el-icon-plus" v-permission="'system:roles:add'" @click="openCreate">新增角色</el-button>
    </div>
    <div class="table-panel">
@@ -11,9 +11,10 @@
        <el-table-column prop="code" label="角色编码" width="170"></el-table-column>
        <el-table-column prop="roleType" label="角色类型" width="170"></el-table-column>
        <el-table-column prop="remark" label="备注" min-width="220" show-overflow-tooltip></el-table-column>
        <el-table-column label="操作" width="220" fixed="right">
        <el-table-column label="操作" width="300" fixed="right">
          <template slot-scope="{ row }">
            <el-button type="text" v-permission="'system:roles:assign'" @click="openUserSetting(row)">用户设置</el-button>
            <el-button type="text" v-permission="'system:roles:permission'" @click="openPermissionSetting(row)">权限设置</el-button>
            <el-button type="text" v-permission="'system:roles:edit'" @click="openEdit(row)">编辑</el-button>
            <el-button type="text" class="danger-link" v-permission="'system:roles:delete'" @click="remove(row)">删除</el-button>
          </template>
@@ -80,16 +81,21 @@
        <el-button type="primary" :loading="userSaving" v-permission="'system:roles:assign'" @click="saveRoleUsers">保存</el-button>
      </template>
    </el-dialog>
    <role-permission-dialog :visible.sync="permissionDialogVisible" :role="currentRole"></role-permission-dialog>
  </div>
</template>
<script>
import api from '@/api'
import RolePermissionDialog from './RolePermissionDialog.vue'
export default {
  name: 'RoleManage',
  components: { RolePermissionDialog },
  data() {
    return {
      rows: [],
      dialogVisible: false,
      permissionDialogVisible: false,
      saving: false,
      userDialogVisible: false,
      userLoading: false,
@@ -164,6 +170,10 @@
        this.$nextTick(this.restoreUserSelection)
      }).finally(() => { this.userLoading = false })
    },
    openPermissionSetting(row) {
      this.currentRole = { ...row }
      this.permissionDialogVisible = true
    },
    restoreUserSelection() {
      const table = this.$refs.roleUserTable
      if (!table) return
emergency-repair-web/src/views/system/RolePermissionDialog.vue
New file
@@ -0,0 +1,194 @@
<template>
  <el-dialog
    title="角色权限设置"
    :visible.sync="dialogVisible"
    width="860px"
    append-to-body
    :close-on-click-modal="false"
  >
    <div class="permission-head">
      <div>
        <span class="role-label">当前角色</span>
        <strong>{{ role.name }}</strong>
        <span class="role-code">{{ role.code }}</span>
      </div>
      <span class="selected-total">已选权限 {{ selectedRoleIds.length }} ä¸ª</span>
    </div>
    <div class="permission-toolbar">
      <el-checkbox v-model="checkAll" @change="toggleAll">全选</el-checkbox>
      <span class="permission-tip">菜单和按钮权限均可勾选,保存后切换角色会自动反显。</span>
    </div>
    <div v-loading="loading" class="permission-tree">
      <el-tree
        ref="permissionTree"
        :data="menus"
        node-key="id"
        show-checkbox
        default-expand-all
        :render-content="renderContent"
        :props="{ label: 'name', children: 'children' }"
        @check="syncCheckAllState"
      ></el-tree>
    </div>
    <template slot="footer">
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" :loading="saving" v-permission="'system:roles:permission'" @click="save">保存</el-button>
    </template>
  </el-dialog>
</template>
<script>
import api from '@/api'
export default {
  name: 'RolePermissionDialog',
  props: {
    visible: { type: Boolean, default: false },
    role: { type: Object, default: () => ({}) }
  },
  data() {
    return {
      menus: [],
      flatMenus: [],
      loading: false,
      saving: false,
      checkAll: false,
      selectedRoleIds: []
    }
  },
  computed: {
    dialogVisible: {
      get() { return this.visible },
      set(value) { this.$emit('update:visible', value) }
    },
    allMenuIds() {
      return this.flatMenus.map(item => String(item.id))
    }
  },
  watch: {
    visible(value) {
      if (value) this.open()
    },
  },
  methods: {
    open() {
      if (!this.role || !this.role.id) return
      this.loading = true
      this.checkAll = false
      this.selectedRoleIds = []
      Promise.all([
        api.get('/system/menu/tree'),
        api.get('/system/menu/list'),
        api.get('/system/menu-permissions', { params: { roleId: this.role.id } })
      ]).then(([tree, flat, permissions]) => {
        this.menus = tree.data || []
        this.flatMenus = flat.data || []
        const data = permissions.data || {}
        this.selectedRoleIds = (data.roleMenuIds || []).map(String)
        this.$nextTick(() => {
          if (!this.$refs.permissionTree) return
          this.$refs.permissionTree.setCheckedKeys(this.displayCheckedKeys(this.selectedRoleIds))
          this.syncCheckAllState()
        })
      }).finally(() => { this.loading = false })
    },
    renderContent(h, { data }) {
      const label = data.menuType === 2 ? data.name + ' [按钮 ' + (data.permissions || '') + ']' : data.name
      return h('span', { class: { 'perm-button-node': data.menuType === 2 } }, label)
    },
    displayCheckedKeys(ids) {
      const selected = new Set((ids || []).map(String))
      const children = {}
      const menuIds = new Set(this.flatMenus.map(item => String(item.id)))
      this.flatMenus.forEach(item => {
        const id = String(item.id)
        const pid = item.pid == null ? '0' : String(item.pid)
        if (!children[pid]) children[pid] = []
        children[pid].push(id)
      })
      const result = []
      const collectFullySelected = id => {
        if (!selected.has(id)) return false
        const childIds = children[id] || []
        if (!childIds.length) {
          result.push(id)
          return true
        }
        const allChildrenSelected = childIds.map(collectFullySelected).every(Boolean)
        if (allChildrenSelected) result.push(id)
        return allChildrenSelected
      }
      this.flatMenus
        .filter(item => item.pid == null || String(item.pid) === '0' || !menuIds.has(String(item.pid)))
        .forEach(item => collectFullySelected(String(item.id)))
      return result
    },
    checkedMenuIds() {
      const tree = this.$refs.permissionTree
      if (!tree) return []
      return [...new Set([...(tree.getCheckedKeys(false) || []), ...(tree.getHalfCheckedKeys() || [])].map(String))]
    },
    syncCheckAllState() {
      this.checkAll = this.checkedMenuIds().length === this.allMenuIds.length
    },
    toggleAll() {
      if (!this.$refs.permissionTree) return
      this.$refs.permissionTree.setCheckedKeys(this.checkAll ? this.allMenuIds : [])
      this.$nextTick(this.syncCheckAllState)
    },
    save() {
      const roleMenuIds = this.checkedMenuIds()
      this.saving = true
      api.post('/system/menu-permissions', { roleId: this.role.id, roleMenuIds }).then(() => {
        this.selectedRoleIds = roleMenuIds
        this.$message.success('角色权限已保存')
        this.$emit('saved')
        this.dialogVisible = false
      }).finally(() => { this.saving = false })
    }
  }
}
</script>
<style scoped>
.permission-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  color: #31465d;
}
.role-label {
  margin-right: 10px;
  color: #7b8b9c;
  font-size: 13px;
}
.role-code {
  margin-left: 8px;
  color: #93a2b1;
  font-size: 12px;
}
.selected-total {
  color: #2f658f;
  font-size: 13px;
}
.permission-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.permission-tip {
  color: #7b8a9a;
  font-size: 12px;
}
.permission-tree {
  height: 460px;
  padding: 8px 10px;
  border: 1px solid #dce4ec;
  border-radius: 4px;
  overflow: auto;
}
::v-deep .perm-button-node {
  color: #b8791f;
}
</style>
requirement/½»½ÓÎļþ_2026.10.3.md
@@ -534,3 +534,46 @@
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åä¸‰ã€2026-10-06 è”调收尾(需求 21-22)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 21 | èœå•权限管理改为角色管理的“权限设置”入口 | å·²å®Œæˆ | åˆ é™¤ç‹¬ç«‹èœå•、路由和页面;角色列表新增“权限设置”,弹窗保留菜单及按钮节点并正常反显 |
| 22 | ç³»ç»Ÿç®¡ç†æ–°å¢žäºŒçº§èœå•组,新增错误日志 | å·²å®Œæˆ | æ–°å¢žâ€œç³»ç»Ÿé…ç½® / æƒé™é…ç½® / æ—¥å¿—管理”目录并迁移菜单;错误日志记录非业务异常,支持筛选、分页和堆栈详情 |
### 13.1 ä¸»è¦å˜æ›´æ–‡ä»¶
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java`
前端:
- `emergency-repair-web/src/views/system/RoleManage.vue`
- `emergency-repair-web/src/views/system/RolePermissionDialog.vue`
- `emergency-repair-web/src/views/system/ErrorLogManage.vue`
- `emergency-repair-web/src/views/system/MenuManage.vue`
- `emergency-repair-web/src/router/index.js`
- åˆ é™¤ `emergency-repair-web/src/views/system/MenuPermission.vue`
### 13.2 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests clean package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req21-22.js` é€šè¿‡ï¼šäºŒçº§ç›®å½•和错误日志菜单显示正常,旧“菜单权限管理”不存在;角色权限弹窗按钮节点数 78,权限反显正常。
- é”™è¯¯æ—¥å¿—列表、异常类型筛选和堆栈详情页面验证通过;人为触发非法接口参数后成功落库,临时验证记录已清理。
- æˆªå›¾ï¼š`.codex-run/role-permission-dialog-verified.png`、`.codex-run/error-log-verified.png`。
### 13.3 æŽ¥æ‰‹æ³¨æ„
- é”™è¯¯æ—¥å¿—目前覆盖进入 Spring MVC å…¨å±€å¼‚常处理的异常;启动阶段异常、异步线程异常若绕过该处理器,暂不会自动记录。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。