7个文件已添加
27个文件已修改
1个文件已删除
2025 ■■■■■ 已修改文件
HANDOFF.md 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java 155 ●●●●● 补丁 | 查看 | 原始文档 | 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 29 ●●●●● 补丁 | 查看 | 原始文档 | 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 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/index.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/sensitive-words.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/AppHeader.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/ProductTreePicker.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/RepairLogo.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/layout/AppLayout.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/main.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/router/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/utils/sensitive.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/Login.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/completion/CompletionQuery.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/ErrorLogManage.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/LoginLogManage.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuManage.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuPermission.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/OperationLogManage.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/RoleManage.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/RolePermissionDialog.vue 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/交接文件_2026.10.3.md 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/改进20261003.txt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/敏感词字典.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -726,3 +726,198 @@
- åŽ†å²æ¼”ç¤ºæ´¾å·¥å•å¯èƒ½ä¿ç•™æ—§ `T_CONTRACTOR` åç§°ï¼Œéžç®¡ç†å‘˜ cj ç”¨æˆ·å¯èƒ½çœ‹ä¸åˆ°è¿™äº›åŽ†å²å•ï¼›`admin` ä¸å—单位过滤影响。
- `.codex-run`、Office é”æ–‡ä»¶ `requirement\~$原始需求.docx` å’Œ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` å±žæœ¬åœ°è”调或临时文件,不应提交。
- æœ¬è½®ä»£ç å·²æäº¤å¹¶æŽ¨é€å…¬å¸ Git ä»“库 `main` åˆ†æ”¯ï¼Œæäº¤ `757dcb3`;交接文件回填提交记录。后续启动、构建和排错命令沿用第 16 èŠ‚ã€‚
## 18. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 20 èœå•权限管理收尾)
**本轮完成内容**
- éœ€æ±‚ 20:菜单权限管理取消租户权限和角色权限的双重授权,改为单步设置角色权限。当前用户本身没有租户归属,因此不再设置租户权限。
- æƒé™æ ‘继续展示菜单节点和按钮节点,不按 `menuType` è¿‡æ»¤ï¼›åˆ‡æ¢è§’色后,已保存权限复选框正常反显。
- èœå•权限接口只保留 `roleId` / `roleMenuIds`,删除租户菜单授权及 `/api/system/tenants`。
- ä¿®å¤é›ªèб ID ç»æµè§ˆå™¨ JSON è§£æžåŽç²¾åº¦ä¸¢å¤±çš„问题:后端返回字符串 ID,前端选中态按字符串处理。
**主要文件**
- åŽç«¯ï¼š`emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.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-web/src/views/system/MenuPermission.vue`。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests compile` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ã€‚
- æŽ¥å£ï¼š`GET /api/system/menu-permissions?roleId=2106221661022490625` è¿”回字符串型 `roleMenuIds` å…± 14 æ¡ã€‚
- æµè§ˆå™¨ï¼š`node .codex-run/verify-menu-permission.js` è¾“出 `hasTenantPanel: false`、`buttonNodeCount: 76`、`checkedMatchesSaved: true`、`hasError: false`;截图 `.codex-run/menu-permission-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-menu-permission-fixed.out.log`、`.codex-run/backend-20261006-menu-permission-fixed.err.log`。
**接手注意**
- ä¸è¦æäº¤ `.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` åˆ†æ”¯ã€‚
## 20. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 23-25 ç³»ç»Ÿé…ç½®ã€å®Œå·¥æŸ¥é˜…与菜单父节点)
**本轮完成内容**
- éœ€æ±‚ 23:系统名称和版本改为关键词配置,codename åˆ†åˆ«ä¸º `rk_SystemName`、`rk_SystemVer`。登录页、页头、Logo æ›¿ä»£æ–‡æœ¬å’Œæµè§ˆå™¨æ ‡é¢˜ä½¿ç”¨é…ç½®å€¼ï¼›é¡µå¤´ç‰ˆæœ¬ä½¿ç”¨çº¢è‰²æ˜¾ç¤ºã€‚
- éœ€æ±‚ 24:新增“完工信息查阅”功能,菜单 `/completion/query`,接口 `GET /api/work-orders/completion/query`。查询结果按故障汇总,一个故障展开多张派工单;故障编号、rk_X号、设备、工程名称及损坏情况、上报单位、上报日期、序号这 7 åˆ—使用单元格纵向合并。每张派工单的完工处理情况和更换器材支持多条明细。
- éœ€æ±‚ 24 çš„æŸ¥è¯¢æ¡ä»¶åŒ…括关键词、承修单位、完工状态和完工日期。关键词匹配故障、派工单、工程编号、承修人和明细内容。
- éœ€æ±‚ 25:菜单管理编辑时,“上级菜单”改为可编辑级联选择器;编辑时排除当前菜单及其全部下级,按钮类型只能选择菜单类型父节点。
- éœ€æ±‚ 25 åŽç«¯åŒæ­¥å¢žåŠ çˆ¶èŠ‚ç‚¹å­˜åœ¨æ€§ã€è‡ªå¼•ç”¨å’Œå¾ªçŽ¯å±‚çº§æ ¡éªŒï¼Œé¿å…é€šè¿‡æŽ¥å£æž„é€ å¾ªçŽ¯èœå•æ ‘ã€‚
**主要文件**
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java`(新增)
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.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/completion/CompletionQuery.vue`(新增)
- `emergency-repair-web/src/views/system/MenuManage.vue`
- `emergency-repair-web/src/components/AppHeader.vue`、`Login.vue`、`RepairLogo.vue`、`layout/AppLayout.vue`、`router/index.js`、`utils/sensitive.js`、`main.js`
- `emergency-repair-web/public/sensitive-words.json`、`public/index.html`
- `requirement/敏感词字典.json`
**验证结果**
- åŽç«¯ `mvn -B -DskipTests compile` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req23-25.js`:页面标题和页头使用“临抢修管理系统 / ç½‘络版”,版本颜色为 `rgb(255, 107, 107)`;完工查阅显示故障 `GZ-2026-012`、处理情况和器材;菜单编辑父节点显示“故障管理”且可编辑。
- æŽ¥å£å®žæµ‹ï¼šä¸´æ—¶åŒä¸€æ•…障插入两张派工单和两条完工记录后,`GET /api/work-orders/completion/query?keyword=ZZ-MERGE-TEST-20261006` è¿”回 1 ä¸ªæ•…障、2 å¼ æ´¾å·¥å•,每条各 1 æ¡å¤„理情况和 1 æ¡å™¨æã€‚
- æµè§ˆå™¨åˆå¹¶å®žæµ‹ï¼šé¡µé¢ç”Ÿæˆ 2 å¼ æ´¾å·¥å•行,故障编号单元格只出现 1 æ¬¡ï¼Œ7 ä¸ªæ•…障信息列均有 `rowspan=2`,两张单各自的处理情况和器材均正常显示,无网络异常;截图 `.codex-run/completion-query-merge-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-req23-25-v2.out.log`、`.codex-run/backend-20261006-req23-25-v2.err.log`。
**接手注意**
- å®Œå·¥ä¿¡æ¯æŸ¥é˜…当前只汇总已经存在完工填报记录 `T_COMPLETION_INFO` çš„æ´¾å·¥å•;仅完工处理但未填报完工信息的派工单不会出现在查阅结果中。
- å®Œå·¥æŸ¥é˜…接口当前一次返回全部匹配结果,没有分页;数据量进一步增大时建议增加分页或导出策略。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 21. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 21 æƒé™è®¾ç½®èŒƒå›´åˆ‡æ¢ï¼‰
**本轮完成内容**
- è§’色管理“权限设置”弹窗增加“全部 / å·²è®¾ç½®â€åˆ‡æ¢ï¼Œäº¤äº’方式与“用户设置”保持一致。
- â€œå…¨éƒ¨â€æ˜¾ç¤ºå®Œæ•´èœå•及按钮权限树,保留“全选”和原有权限反显。
- â€œå·²è®¾ç½®â€ä»…显示当前角色已授权节点及其父级路径;没有已设置权限时显示空状态提示。
- åˆ‡æ¢èŒƒå›´æ—¶ä¼šä¿ç•™å½“前尚未保存的勾选状态,切回“全部”后恢复完整树及当前选中权限。
- å¼¹çª—每次重新打开默认回到“全部”。
**主要文件**
- `emergency-repair-web/src/views/system/RolePermissionDialog.vue`
- `requirement/改进20261003.txt`(补充需求 21 çš„范围切换口径)
**验证结果**
- å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req21-22.js` é€šè¿‡ï¼šå·¦ä¾§æ— â€œèœå•权限管理”,角色管理存在“权限设置”。
- å…¨éƒ¨è§†å›¾æ˜¾ç¤º 119 ä¸ªå¤é€‰æ¡†ã€13 ä¸ªå·²å‹¾é€‰ã€1 ä¸ªåŠé€‰ï¼›å·²è®¾ç½®è§†å›¾æ˜¾ç¤º 14 ä¸ªèŠ‚ç‚¹ï¼Œå‡ä¸ºå½“å‰å·²è®¾ç½®èŠ‚ç‚¹æˆ–çˆ¶çº§è·¯å¾„ï¼›åˆ‡å›žå…¨éƒ¨åŽå‹¾é€‰çŠ¶æ€æ¢å¤ï¼Œæ— ç½‘ç»œå¼‚å¸¸ã€‚
- æˆªå›¾ï¼š`.codex-run/role-permission-assigned-scope.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`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 22. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 26 æ—¥å¿—日期范围分隔符)
**本轮完成内容**
- ä¿®å¤ç™»å½•日志、操作日志、错误日志三个页面日期范围组件中间“至”显示不完整的问题。
- ä¸‰ä¸ªé¡µé¢çš„ `.el-range-separator` å›ºå®šä¸º `18px` å¹¶å±…中显示,避免 Element UI é»˜è®¤ç™¾åˆ†æ¯”宽度在 260px æ—¥æœŸæŽ§ä»¶ä¸­è¢«åŽ‹ç¼©è£åˆ‡ã€‚
**主要文件**
- `emergency-repair-web/src/views/system/LoginLogManage.vue`
- `emergency-repair-web/src/views/system/OperationLogManage.vue`
- `emergency-repair-web/src/views/system/ErrorLogManage.vue`
- `requirement/改进20261003.txt`(补充需求 26)
**验证结果**
- å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req26.js` é€šè¿‡ï¼šä¸‰ä¸ªæ—¥å¿—页面均找到“至”,实际宽度、`clientWidth` å’Œ `scrollWidth` å‡ä¸º `18px`,计算样式为 `flex-basis: 18px`、`min-width: 18px`、居中,无网络异常。
- æˆªå›¾ï¼š`.codex-run/login-log-date-separator.png`、`.codex-run/operation-log-date-separator.png`、`.codex-run/error-log-date-separator.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`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 23. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 27 è®¾å¤‡é€‰æ‹©èŒƒå›´ï¼‰
**本轮完成内容**
- æ–°å¢žæ•…障选择 `rk_X号`(产品结构树 `side` ç±»åž‹èŠ‚ç‚¹ï¼‰åŽï¼Œè®¾å¤‡é€‰æ‹©å™¨åªä»¥æ‰€é€‰èŠ‚ç‚¹ä¸ºæ ¹å±•ç¤ºå…¶ä¸‹çº§ï¼Œä¸å†åŒæ—¶è¿”å›žåž‹å·æ ¹èŠ‚ç‚¹å’Œæ•´æ£µäº§å“ç»“æž„æ ‘ã€‚
- è®¾å¤‡é€‰æ‹©å™¨æ‰“开锁定范围时自动展开返回的 `side` æ ¹èŠ‚ç‚¹ï¼Œé¦–å±ç›´æŽ¥æ˜¾ç¤ºç¬¬ä¸€å±‚ç³»ç»ŸèŠ‚ç‚¹ï¼ŒåŽç»­ç»§ç»­æŒ‰åŽŸé€»è¾‘æ‡’åŠ è½½ã€‚
- `xNo` ä¸ºç©ºæ—¶ä»è¿”回完整产品结构树,不影响产品结构树管理和不按 `rk_X号` ç­›é€‰çš„设备选择场景。
**主要文件**
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
- `emergency-repair-web/src/components/ProductTreePicker.vue`
- `requirement/改进20261003.txt`(补充需求 27)
**验证结果**
- åŽç«¯ `mvn -B -DskipTests compile`、`mvn -B -DskipTests package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `GET /api/system/tree/product/side-roots?xNo=203` ä¿®å¤åŽåªè¿”回 1 ä¸ªæ ¹èŠ‚ç‚¹ï¼š`id=1280000000000009729`、`name=203`、`layer=side`、`level=2`;`201 / 203 / 301 / æµ‹è¯•T` åˆ†åˆ«åªè¿”回对应节点。
- `node .codex-run/verify-req27.js` é€šè¿‡ï¼šæ ¹èŠ‚ç‚¹æ–‡æœ¬ä»…ä¸º `203`,自动展开 1 ä¸ªæ ¹èŠ‚ç‚¹ï¼Œé¦–å±æ˜¾ç¤ºå…¶ä¸‹çº§ç³»ç»ŸèŠ‚ç‚¹ï¼Œæ— ç½‘ç»œå¼‚å¸¸ã€‚
- è¯·æ±‚链路确认:`/api/system/tree/product/side-roots?xNo=203` åŽç»§ç»­è¯·æ±‚ `/api/system/tree/product/children?parentId=1280000000000009729`。截图:`.codex-run/req27-side-tree.png`。
**当前运行状态(2026-10-06)**
- å‰ç«¯ `http://localhost:8082`,PID `138164`;后端 `http://127.0.0.1:8091`,PID `148268`;DM8 `127.0.0.1:5236`;Edge CDP `9222`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
- åŽç«¯æ—¥å¿—:`.codex-run/backend-20261006-req27.out.log`、`.codex-run/backend-20261006-req27.err.log`。
**接手注意**
- éœ€æ±‚ 26 å·²æäº¤å¹¶æŽ¨é€ï¼Œæäº¤ `f651ad5`(`fix: show full date range separator in log filters`)。需求 27 å·²æäº¤å¹¶æŽ¨é€ï¼Œæäº¤ `f9114ff`(`fix: limit equipment picker to selected X number`)。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 24. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œèœå•名称被启动初始化覆盖)
**问题与修复**
- æ‰‹å·¥æŠŠç³»ç»Ÿé…ç½®ä¸‹çš„“产品结构树管理”改为“产品结构树”后,后端重启会恢复“管理”二字。
- åŽŸå› æ˜¯ `DataInitializer.ensureMenu()` æ¯æ¬¡å¯åŠ¨éƒ½ä¼šæ‹¿ä»£ç ä¸­çš„é»˜è®¤åç§°è¦†ç›–å·²å­˜åœ¨èœå•ã€‚
- å·²æ”¹ä¸ºåªåœ¨æ–°å»ºèœå•时使用默认名称;已存在菜单只修复父节点、排序、类型和可见性,名称由菜单管理页面维护。
- æ–°å»ºæ•°æ®åº“的默认菜单名称同步改为“产品结构树”。
**主要文件**
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
**验证结果**
- åŽç«¯ `mvn -B -DskipTests compile`、`mvn -B -DskipTests package` é€šè¿‡ã€‚
- å°†æ•°æ®åº“菜单 `id=2101493905517301763`、`url=/system/product-tree` çš„名称改为“产品结构树”后,使用新 jar é‡å¯åŽç«¯ï¼ŒæŽ¥å£å†æ¬¡æŸ¥è¯¢ä»ä¸ºâ€œäº§å“ç»“构树”。
- æ–°åŽç«¯ PID `151580`,监听 `8091`;日志:`.codex-run/backend-20261006-menu-name.out.log`、`.codex-run/backend-20261006-menu-name.err.log`。
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)
@@ -83,11 +84,13 @@
    public void run(String... args) {
        seedMenusAndRoles();
        ensureSystemMenus();
        ensureCompletionMenus();
        normalizeSensitiveTerms();
        normalizeEquipmentTerms();
        seedButtonMenus();
        ensureAdminRoleAssignment();
        ensureMajorConfigMenus();
        ensureRoleMenuAncestors();
        seedMajors();
        seedContractors();
        seedTrees();
@@ -114,6 +117,7 @@
            createMenu("dispatch", fault, "rk_XCB助理派单", "/fault/dispatch", null, 1, 60);
            createMenu("completion-list", fault, "完工情况处理", "/fault/completion", null, 1, 70);
            createMenu("completion-report", fault, "完工信息上报", "/completion/report", null, 1, 80);
            createMenu("completion-query", fault, "完工信息查阅", "/completion/query", null, 1, 90);
            Long acceptance = createMenu("acceptance", null, "完工验收", null, "el-icon-circle-check", 0, 30);
            createMenu("accept-application", acceptance, "验收申请", "/acceptance/application", null, 1, 10);
@@ -130,16 +134,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 +190,121 @@
        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();
        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());
        menuMapper.insert(menu);
        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(now);
            menu.setUpdateTime(now);
            menuMapper.insert(menu);
            return menu.getId();
        }
        // Existing menu names are user-managed; startup only repairs structural fields.
        boolean changed = !java.util.Objects.equals(menu.getPid(), pid)
                || !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.setMenuType(1);
            menu.setSortNo(sort);
            menu.setVisible(1);
            menu.setUpdateTime(now);
            menuMapper.updateById(menu);
        }
        return menu.getId();
    }
    private void ensureCompletionMenus() {
        SysMenu fault = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getName, "故障管理")
                .eq(SysMenu::getMenuType, 0)
                .last("LIMIT 1"));
        Long pid = fault == null ? 0L : fault.getId();
        ensureMenu(pid, "完工信息查阅", "/completion/query", 90);
        grantAdminMenus("/completion/query");
    }
    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 +323,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/business/controller/WorkOrderController.java
@@ -1,11 +1,13 @@
package com.emergencyrepair.business.controller;
import com.emergencyrepair.business.dto.Requests;
import com.emergencyrepair.business.dto.CompletionFaultQueryItem;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.WorkOrder;
import com.emergencyrepair.business.service.BusinessService;
import com.emergencyrepair.common.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -14,6 +16,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
@RestController
@@ -29,6 +32,16 @@
        return Result.ok(businessService.listWorkOrders(status, keyword, completionReportable));
    }
    @GetMapping("/completion/query")
    public Result<List<CompletionFaultQueryItem>> completionQuery(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) String completionStatus,
            @RequestParam(required = false) String contractorName,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        return Result.ok(businessService.listCompletionQueries(keyword, completionStatus, contractorName, startDate, endDate));
    }
    @GetMapping("/{id}")
    public Result<WorkOrder> detail(@PathVariable Long id) {
        return Result.ok(businessService.getWorkOrder(id));
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java
New file
@@ -0,0 +1,52 @@
package com.emergencyrepair.business.dto;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
public class CompletionFaultQueryItem {
    private String faultId;
    private String faultCode;
    @JsonProperty("xNo")
    private String xNo;
    private String equipmentName;
    private String parentSystem;
    private String engineeringMajor;
    private String teamMajor;
    private String reportUnit;
    private LocalDate reportDate;
    private String owner;
    private String engineeringNameDesc;
    private String taskStatus;
    private List<WorkOrderCompletionItem> workOrders = new ArrayList<>();
    @Data
    public static class WorkOrderCompletionItem {
        private String workOrderId;
        private String workOrderNo;
        private String contractorName;
        private String completionStatus;
        private String engineeringNo;
        private LocalDate startDate;
        private LocalDate finishDate;
        private String contractorPerson;
        @JsonProperty("tInternalConstruction")
        private String tInternalConstruction;
        private String relatedProject;
        private Integer durationDays;
        private Integer personCount;
        private String acceptanceMode;
        private String qualityInfo;
        private String reasonAnalysis;
        private String faultDescription;
        private String itemName;
        private LocalDateTime submitTime;
        private List<Requests.SolutionItem> solutions = new ArrayList<>();
        private List<Requests.MaterialItem> materials = new ArrayList<>();
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.java
@@ -108,9 +108,7 @@
    @Data
    public static class MenuPermissionRequest {
        private Long roleId;
        private Long tenantId;
        private List<Long> roleMenuIds = new ArrayList<>();
        private List<Long> tenantMenuIds = new ArrayList<>();
    }
    @Data
    public static class UserRoleRequest {
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.emergencyrepair.business.dto.Requests;
import com.emergencyrepair.business.dto.CompletionFaultQueryItem;
import com.emergencyrepair.business.entity.Acceptance;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.Fault;
@@ -32,6 +33,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -406,6 +408,159 @@
        return created;
    }
    public List<CompletionFaultQueryItem> listCompletionQueries(String keyword, String completionStatus,
                                                               String contractorName, LocalDate startDate,
                                                               LocalDate endDate) {
        List<CompletionInfo> completions = completionInfoMapper.selectList(Wrappers.<CompletionInfo>lambdaQuery()
                .orderByDesc(CompletionInfo::getSubmitTime).orderByDesc(CompletionInfo::getUpdateTime));
        if (completions.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> workOrderIds = completions.stream().map(CompletionInfo::getWorkOrderId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        if (workOrderIds.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, WorkOrder> workOrderMap = workOrderMapper.selectBatchIds(workOrderIds).stream()
                .collect(Collectors.toMap(WorkOrder::getId, item -> item));
        Set<Long> faultIds = workOrderMap.values().stream().map(WorkOrder::getFaultId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        if (faultIds.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, Fault> faultMap = faultMapper.selectBatchIds(faultIds).stream()
                .collect(Collectors.toMap(Fault::getId, item -> item));
        String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim().toLowerCase() : null;
        String normalizedContractor = StringUtils.hasText(contractorName) ? contractorName.trim().toLowerCase() : null;
        Map<Long, CompletionFaultQueryItem> grouped = new LinkedHashMap<>();
        for (CompletionInfo completion : completions) {
            WorkOrder workOrder = workOrderMap.get(completion.getWorkOrderId());
            if (workOrder == null) {
                continue;
            }
            if (StringUtils.hasText(completionStatus)
                    && !completionStatus.equalsIgnoreCase(workOrder.getCompletionStatus())) {
                continue;
            }
            if (normalizedContractor != null && !containsIgnoreCase(workOrder.getContractorName(), normalizedContractor)) {
                continue;
            }
            LocalDate completionDate = completion.getFinishDate() != null
                    ? completion.getFinishDate()
                    : (completion.getSubmitTime() == null ? null : completion.getSubmitTime().toLocalDate());
            if (startDate != null && (completionDate == null || completionDate.isBefore(startDate))) {
                continue;
            }
            if (endDate != null && (completionDate == null || completionDate.isAfter(endDate))) {
                continue;
            }
            Fault fault = faultMap.get(workOrder.getFaultId());
            if (fault == null) {
                continue;
            }
            CompletionFaultQueryItem.WorkOrderCompletionItem workOrderItem = buildCompletionWorkOrderItem(workOrder, completion);
            if (normalizedKeyword != null && !matchesCompletionKeyword(fault, workOrderItem, normalizedKeyword)) {
                continue;
            }
            CompletionFaultQueryItem item = grouped.get(fault.getId());
            if (item == null) {
                item = new CompletionFaultQueryItem();
                item.setFaultId(String.valueOf(fault.getId()));
                item.setFaultCode(fault.getFaultCode());
                item.setXNo(fault.getXNo());
                item.setEquipmentName(fault.getEquipmentName());
                item.setParentSystem(fault.getParentSystem());
                item.setEngineeringMajor(fault.getEngineeringMajor());
                item.setTeamMajor(fault.getTeamMajor());
                item.setReportUnit(fault.getReportUnit());
                item.setReportDate(fault.getReportDate());
                item.setOwner(fault.getOwner());
                item.setEngineeringNameDesc(fault.getEngineeringNameDesc());
                item.setTaskStatus(fault.getTaskStatus());
                grouped.put(fault.getId(), item);
            }
            item.getWorkOrders().add(workOrderItem);
        }
        List<CompletionFaultQueryItem> result = new ArrayList<>(grouped.values());
        result.forEach(item -> item.getWorkOrders().sort(Comparator.comparing(
                CompletionFaultQueryItem.WorkOrderCompletionItem::getWorkOrderNo,
                Comparator.nullsLast(String::compareTo))));
        result.sort(Comparator.comparing(CompletionFaultQueryItem::getReportDate,
                Comparator.nullsLast(Comparator.reverseOrder())));
        return result;
    }
    private CompletionFaultQueryItem.WorkOrderCompletionItem buildCompletionWorkOrderItem(
            WorkOrder workOrder, CompletionInfo completion) {
        CompletionFaultQueryItem.WorkOrderCompletionItem item = new CompletionFaultQueryItem.WorkOrderCompletionItem();
        item.setWorkOrderId(String.valueOf(workOrder.getId()));
        item.setWorkOrderNo(workOrder.getWorkOrderNo());
        item.setContractorName(workOrder.getContractorName());
        item.setCompletionStatus(workOrder.getCompletionStatus());
        item.setEngineeringNo(completion.getEngineeringNo());
        item.setStartDate(completion.getStartDate());
        item.setFinishDate(completion.getFinishDate());
        item.setContractorPerson(completion.getContractorPerson());
        item.setTInternalConstruction(completion.getTInternalConstruction());
        item.setRelatedProject(completion.getRelatedProject());
        item.setDurationDays(completion.getDurationDays());
        item.setPersonCount(completion.getPersonCount());
        item.setAcceptanceMode(completion.getAcceptanceMode());
        item.setQualityInfo(completion.getQualityInfo());
        item.setReasonAnalysis(completion.getReasonAnalysis());
        item.setFaultDescription(completion.getFaultDescription());
        item.setItemName(completion.getItemName());
        item.setSubmitTime(completion.getSubmitTime());
        List<Requests.SolutionItem> solutions = parseJsonList(completion.getSolutionsJson(), Requests.SolutionItem.class);
        if (solutions.isEmpty() && StringUtils.hasText(completion.getSolution())) {
            Requests.SolutionItem legacy = new Requests.SolutionItem();
            legacy.setContent(completion.getSolution());
            solutions.add(legacy);
        }
        item.setSolutions(solutions);
        item.setMaterials(parseJsonList(completion.getConsumablesJson(), Requests.MaterialItem.class));
        return item;
    }
    private boolean matchesCompletionKeyword(Fault fault, CompletionFaultQueryItem.WorkOrderCompletionItem item,
                                             String keyword) {
        if (containsIgnoreCase(fault.getFaultCode(), keyword)
                || containsIgnoreCase(fault.getXNo(), keyword)
                || containsIgnoreCase(fault.getEquipmentName(), keyword)
                || containsIgnoreCase(fault.getEngineeringNameDesc(), keyword)
                || containsIgnoreCase(fault.getReportUnit(), keyword)
                || containsIgnoreCase(item.getWorkOrderNo(), keyword)
                || containsIgnoreCase(item.getContractorName(), keyword)
                || containsIgnoreCase(item.getEngineeringNo(), keyword)
                || containsIgnoreCase(item.getItemName(), keyword)
                || containsIgnoreCase(item.getContractorPerson(), keyword)) {
            return true;
        }
        return item.getSolutions().stream().anyMatch(solution -> containsIgnoreCase(solution.getContent(), keyword))
                || item.getMaterials().stream().anyMatch(material -> containsIgnoreCase(material.getName(), keyword)
                || containsIgnoreCase(material.getSpecification(), keyword));
    }
    private boolean containsIgnoreCase(String value, String keyword) {
        return StringUtils.hasText(value) && value.toLowerCase().contains(keyword);
    }
    private <T> List<T> parseJsonList(String json, Class<T> type) {
        if (!StringUtils.hasText(json)) {
            return new ArrayList<>();
        }
        try {
            return objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, type));
        } catch (Exception exception) {
            return new ArrayList<>();
        }
    }
    public List<WorkOrder> listWorkOrders(String status, String keyword, boolean completionReportable) {
        LambdaQueryWrapper<WorkOrder> query = Wrappers.lambdaQuery();
        if (completionReportable) {
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;
@@ -109,6 +110,22 @@
                                                    @RequestParam(required = false) String startDate,
                                                    @RequestParam(required = false) String endDate) {
        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")
@@ -281,22 +298,16 @@
    }
    @GetMapping("/menu-permissions")
    public Result<Map<String, Object>> menuPermissions(@RequestParam(required = false) Long roleId,
                                                       @RequestParam(required = false) Long tenantId) {
        return Result.ok(systemService.menuPermissions(roleId, tenantId));
    public Result<Map<String, Object>> menuPermissions(@RequestParam(required = false) Long roleId) {
        return Result.ok(systemService.menuPermissions(roleId));
    }
    @PostMapping("/menu-permissions")
    public Result<Void> saveMenuPermissions(@RequestBody Requests.MenuPermissionRequest request) {
        systemService.saveMenuPermissions(request.getRoleId(), request.getTenantId(),
                request.getRoleMenuIds(), request.getTenantMenuIds());
        systemService.saveMenuPermissions(request.getRoleId(), request.getRoleMenuIds());
        return Result.ok();
    }
    @GetMapping("/tenants")
    public Result<List<Map<String, Object>>> tenants() {
        return Result.ok(systemService.tenants());
    }
    @GetMapping("/three-member")
    public Result<Map<String, Object>> threeMember() {
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
@@ -59,6 +59,8 @@
        put("/completion/report",
                def("completion:report:save", "\u4fdd\u5b58"),
                def("completion:report:submit", "\u63d0\u4ea4"));
        put("/completion/query",
                def("completion:query:query", "\u67e5\u8be2"));
        put("/acceptance/application",
                def("acceptance:application:query", "\u67e5\u8be2"),
                def("acceptance:application:apply", "\u7533\u8bf7"));
@@ -86,12 +88,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;
@@ -105,11 +108,20 @@
            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("按钮类型只能建在菜单类型下级");
        if (menu.getId() != null && !isRoot && menu.getId().equals(menu.getPid())) {
            throw new BusinessException("上级菜单不能选择当前菜单");
        }
        SysMenu parent = null;
        if (!isRoot) {
            parent = menuMapper.selectById(menu.getPid());
            if (parent == null) {
                throw new BusinessException("上级菜单不存在");
            }
            SysMenu parent = menuMapper.selectById(menu.getPid());
            if (menu.getId() != null && isMenuDescendant(menu.getPid(), menu.getId())) {
                throw new BusinessException("上级菜单不能选择当前菜单的下级");
            }
        }
        if (menu.getMenuType() == 2) {
            if (parent == null || parent.getMenuType() == null || parent.getMenuType() != 1) {
                throw new BusinessException("按钮类型只能建在菜单类型下级");
            }
@@ -118,11 +130,8 @@
            }
            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("按钮类型下级不能再建菜单");
            }
        } else if (parent != null && parent.getMenuType() != null && parent.getMenuType() == 2) {
            throw new BusinessException("按钮类型下级不能再建菜单");
        }
        LocalDateTime now = LocalDateTime.now();
        menu.setUpdateTime(now);
@@ -139,6 +148,19 @@
            menuMapper.updateById(menu);
        }
        return menuMapper.selectById(menu.getId());
    }
    private boolean isMenuDescendant(Long candidateId, Long ancestorId) {
        Long cursor = candidateId;
        int guard = 0;
        while (cursor != null && cursor != 0L && guard++ < 64) {
            if (cursor.equals(ancestorId)) {
                return true;
            }
            SysMenu current = menuMapper.selectById(cursor);
            cursor = current == null ? null : current.getPid();
        }
        return false;
    }
    @Transactional
@@ -392,30 +414,11 @@
        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());
        return mapProductTreeNodes(sideNodes, Collections.emptySet());
    }
    public List<String> productXNumbers() {
@@ -566,47 +569,32 @@
        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));
        List<Long> effectiveRoleMenuIds = expandWithAncestors(roleMenuIds);
        List<Long> effectiveTenantMenuIds = expandWithAncestors(tenantMenuIds);
        for (Long menuId : effectiveRoleMenuIds) {
            SysRoleMenu relation = new SysRoleMenu();
            relation.setRoleId(roleId);
            relation.setMenuId(menuId);
            relation.setCreateTime(LocalDateTime.now());
            roleMenuMapper.insert(relation);
        }
        for (Long menuId : effectiveTenantMenuIds) {
            SysTenantMenu relation = new SysTenantMenu();
            relation.setTenantId(tenantId);
            relation.setMenuId(menuId);
            relation.setCreateTime(LocalDateTime.now());
            tenantMenuMapper.insert(relation);
        }
    }
@@ -888,18 +876,7 @@
        Set<Long> roleMenuIds = new LinkedHashSet<>(roleMenuMapper.selectList(Wrappers.<SysRoleMenu>lambdaQuery()
                        .in(SysRoleMenu::getRoleId, roleIds))
                .stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList()));
        Set<Long> tenantMenuIds = new LinkedHashSet<>(tenantMenuMapper.selectList(Wrappers.<SysTenantMenu>lambdaQuery()
                        .eq(SysTenantMenu::getTenantId, 1L))
                .stream().map(SysTenantMenu::getMenuId).collect(Collectors.toList()));
        if (tenantMenuIds.isEmpty()) {
            result.addAll(roleMenuIds);
        } else {
            for (Long menuId : roleMenuIds) {
                if (tenantMenuIds.contains(menuId)) {
                    result.add(menuId);
                }
            }
        }
        result.addAll(roleMenuIds);
        return result;
    }
@@ -913,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);
    }
@@ -1064,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/public/index.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>临抢修管理系统</title>
  <title>rk_SystemName</title>
</head>
<body>
  <noscript>请启用 JavaScript åŽä½¿ç”¨ä¸´æŠ¢ä¿®ç®¡ç†ç³»ç»Ÿã€‚</noscript>
emergency-repair-web/public/sensitive-words.json
@@ -1,6 +1,18 @@
{
  "sensitive_words": [
    {
      "codename": "rk_SystemName",
      "real_name": "临抢修管理系统",
      "scope": "ui",
      "note": "System title shown in the login page and application header."
    },
    {
      "codename": "rk_SystemVer",
      "real_name": "网络版",
      "scope": "ui",
      "note": "System edition shown in red in the application header."
    },
    {
      "codename": "rk_X号",
      "real_name": "县号",
      "scope": "ui",
emergency-repair-web/src/components/AppHeader.vue
@@ -2,8 +2,8 @@
  <header class="app-header">
    <div class="brand">
      <repair-logo></repair-logo>
      <span>临抢修管理系统</span>
      <em>网络版</em>
      <span>rk_SystemName</span>
      <em class="system-version">rk_SystemVer</em>
      <span v-if="menuPathText" class="menu-path" :title="menuPathText">{{ menuPathText }}</span>
    </div>
    <div class="header-right">
@@ -57,6 +57,7 @@
.brand { display: flex; flex: 1; align-items: center; min-width: 0; gap: 9px; font-size: 18px; font-weight: 600; }
.brand .repair-logo { color: #7fc4ff; font-size: 25px; transform: translateY(2px); }
.brand em { padding: 1px 6px; border: 1px solid rgba(255,255,255,.35); border-radius: 3px; font-size: 11px; font-style: normal; font-weight: 400; opacity: .85; }
.brand .system-version { border-color: rgba(255, 107, 107, .65); color: #ff6b6b; opacity: 1; }
.brand > .repair-logo, .brand > span:not(.menu-path), .brand > em { flex: 0 0 auto; }
.menu-path { flex: 0 1 auto; min-width: 0; padding-left: 14px; border-left: 1px solid rgba(255,255,255,.28); overflow: hidden; color: #d8e9f7; font-size: 13px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
.header-right { display: flex; flex: 0 0 auto; align-items: center; gap: 14px; font-size: 14px; }
emergency-repair-web/src/components/ProductTreePicker.vue
@@ -91,9 +91,20 @@
          return map
        }, {})
        this.keyword = ''
        this.$nextTick(() => { this.restoreChecked(); this.syncSelected() })
        this.$nextTick(() => {
          this.expandLockedRoots(lockedXNo)
          this.restoreChecked()
          this.syncSelected()
        })
      }).finally(() => { this.loading = false })
    },
    expandLockedRoots(lockedXNo) {
      if (!lockedXNo || !this.$refs.tree) return
      this.tree.forEach(node => {
        const treeNode = this.$refs.tree.getNode(node.id)
        if (treeNode) treeNode.expand()
      })
    },
    loadNode(node, resolve) {
      if (!node || !node.data || !node.data.id) {
        resolve([])
emergency-repair-web/src/components/RepairLogo.vue
@@ -1,5 +1,5 @@
<template>
  <img class="repair-logo" :src="logoSrc" alt="临抢修管理系统">
  <img class="repair-logo" :src="logoSrc" alt="rk_SystemName">
</template>
<script>
emergency-repair-web/src/layout/AppLayout.vue
@@ -170,7 +170,7 @@
      if (path === '/fault/report') return 'el-icon-edit-outline'
      if (path.includes('/approval/') || path === '/fault/review') return 'el-icon-s-check'
      if (path === '/fault/dispatch') return 'el-icon-s-promotion'
      if (path.startsWith('/fault/completion') || path === '/completion/report') return 'el-icon-finished'
      if (path.startsWith('/fault/completion') || path.startsWith('/completion/')) return 'el-icon-finished'
      if (path.startsWith('/acceptance/')) return 'el-icon-circle-check'
      if (path.startsWith('/reports/')) return 'el-icon-data-analysis'
      if (path.startsWith('/system/')) return 'el-icon-setting'
emergency-repair-web/src/main.js
@@ -9,7 +9,7 @@
Vue.use(ElementUI, { size: 'small' })
Vue.config.productionTip = false
Vue.prototype.$appName = '临抢修管理系统'
Vue.prototype.$appName = 'rk_SystemName'
Vue.prototype.$hasPermission = function(code) {
  const raw = sessionStorage.getItem('permissions')
emergency-repair-web/src/router/index.js
@@ -20,6 +20,7 @@
      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'rk_XCB助理派单', cacheName: 'DispatchPage' } },
      { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue'), meta: { title: '完工情况处理', cacheName: 'CompletionList' } },
      { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue'), props: route => ({ workOrderId: route.query.workOrderId || null }), meta: { title: '完工信息上报', cacheName: 'CompletionReport' } },
      { path: 'completion/query', component: () => import('@/views/completion/CompletionQuery.vue'), meta: { title: '完工信息查阅', cacheName: 'CompletionQuery' } },
      { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'APPLICATION' }, meta: { title: '验收申请', stage: 'APPLICATION', cacheName: 'AcceptanceList' } },
      { path: 'acceptance/inspector', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'INSPECTOR' }, meta: { title: '质检员验收', stage: 'INSPECTOR', cacheName: 'AcceptanceList' } },
      { path: 'acceptance/leader', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'LEADER' }, meta: { title: '质检负责人验收', stage: 'LEADER', cacheName: 'AcceptanceList' } },
@@ -36,8 +37,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/utils/sensitive.js
@@ -182,6 +182,7 @@
    })
    replaceDom(document.body)
    document.title = replaceSensitiveText(document.title)
    watchDom()
  } catch (error) {
    // The application still works with development codenames if the dictionary is unavailable.
emergency-repair-web/src/views/Login.vue
@@ -4,7 +4,7 @@
      <div class="login-brand">
        <repair-logo></repair-logo>
        <div>
          <h1>临抢修管理系统</h1>
          <h1>{{ $appName }}</h1>
        </div>
      </div>
      <el-form ref="form" :model="form" :rules="rules" label-position="top" @keyup.enter.native="submit">
emergency-repair-web/src/views/completion/CompletionQuery.vue
New file
@@ -0,0 +1,209 @@
<template>
  <div class="page">
    <div class="page-title">
      <div>
        <h2>完工信息查阅</h2>
        <p>按故障汇总,同一故障的多张派工单共用故障信息,完工处理情况和更换器材按多条明细展示</p>
      </div>
      <el-tag type="info">共 {{ rows.length }} ä¸ªæ•…éšœ</el-tag>
    </div>
    <div class="toolbar">
      <el-input v-model="filters.keyword" placeholder="故障编号 / rk_X号 / æ´¾å·¥å•编号 / å·¥ç¨‹åç§° / æ‰¿ä¿®å•位" clearable style="width:380px" @keyup.enter.native="load"></el-input>
      <el-input v-model="filters.contractorName" placeholder="承修单位" clearable style="width:190px" @keyup.enter.native="load"></el-input>
      <el-select v-model="filters.completionStatus" placeholder="完工状态" clearable style="width:140px">
        <el-option v-for="(meta, key) in completionStatuses" :key="key" :label="meta.text" :value="key"></el-option>
      </el-select>
      <el-date-picker v-model="filters.completionDate" type="daterange" value-format="yyyy-MM-dd" range-separator="至" start-placeholder="完工开始日期" end-placeholder="完工结束日期" style="width:270px"></el-date-picker>
      <el-button type="primary" icon="el-icon-search" v-permission="'completion:query:query'" @click="load">查询</el-button>
      <el-button icon="el-icon-refresh" @click="reset">重置</el-button>
    </div>
    <div class="table-panel">
      <el-table v-loading="loading" :data="displayRows" border size="small" :span-method="spanMethod" row-key="rowKey">
        <el-table-column label="序号" width="62" align="center">
          <template slot-scope="{ row }">{{ row.faultIndex }}</template>
        </el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="150"></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="72"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" width="130" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringNameDesc" label="工程名称及损坏情况" min-width="190" show-overflow-tooltip></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" width="130" show-overflow-tooltip></el-table-column>
        <el-table-column prop="reportDate" label="上报日期" width="105"></el-table-column>
        <el-table-column prop="workOrderNo" label="派工单编号" width="155"></el-table-column>
        <el-table-column prop="contractorName" label="承修单位" width="150" show-overflow-tooltip></el-table-column>
        <el-table-column label="完工状态" width="100">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="completionMeta(row.completionStatus).type">{{ completionMeta(row.completionStatus).text }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="engineeringNo" label="工程编号" width="145"></el-table-column>
        <el-table-column label="开工 / å®Œå·¥" width="190">
          <template slot-scope="{ row }">{{ formatPeriod(row.startDate, row.finishDate) }}</template>
        </el-table-column>
        <el-table-column prop="contractorPerson" label="承修人" width="100"></el-table-column>
        <el-table-column label="完工处理情况" min-width="260">
          <template slot-scope="{ row }">
            <div v-if="row.solutions && row.solutions.length" class="detail-list">
              <div v-for="(item, index) in row.solutions" :key="index" class="detail-line">
                <span class="detail-index">{{ index + 1 }}</span>
                <span>{{ item.content || '-' }}</span>
              </div>
            </div>
            <span v-else class="muted">-</span>
          </template>
        </el-table-column>
        <el-table-column label="更换器材" min-width="300">
          <template slot-scope="{ row }">
            <div v-if="row.materials && row.materials.length" class="detail-list">
              <div v-for="(item, index) in row.materials" :key="index" class="material-line">
                <span class="detail-index">{{ index + 1 }}</span>
                <div>
                  <div><strong>{{ item.name || '-' }}</strong><span v-if="item.specification"> / {{ item.specification }}</span><span v-if="item.quantity != null"> Ã— {{ item.quantity }}{{ item.unit || '' }}</span></div>
                  <div class="material-meta">{{ materialMeta(item) }}</div>
                </div>
              </div>
            </div>
            <span v-else class="muted">-</span>
          </template>
        </el-table-column>
        <el-table-column prop="qualityInfo" label="修后质量情况" min-width="180" show-overflow-tooltip></el-table-column>
        <el-table-column prop="reasonAnalysis" label="原因分析" min-width="180" show-overflow-tooltip></el-table-column>
        <el-table-column prop="submitTime" label="提交时间" width="160"></el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
import api from '@/api'
import { completionStatuses, statusMeta } from '@/utils/dict'
export default {
  name: 'CompletionQuery',
  data() {
    return {
      loading: false,
      completionStatuses,
      rows: [],
      filters: {
        keyword: '',
        contractorName: '',
        completionStatus: '',
        completionDate: []
      }
    }
  },
  computed: {
    displayRows() {
      const rows = []
      this.rows.forEach((fault, faultIndex) => {
        const workOrders = fault.workOrders || []
        const total = workOrders.length || 1
        if (!workOrders.length) {
          rows.push({ ...fault, faultIndex: faultIndex + 1, faultRowspan: 1, rowKey: fault.faultId + '-empty' })
          return
        }
        workOrders.forEach((workOrder, index) => {
          rows.push({
            ...fault,
            ...workOrder,
            faultIndex: faultIndex + 1,
            faultRowspan: index === 0 ? total : 0,
            rowKey: fault.faultId + '-' + workOrder.workOrderId
          })
        })
      })
      return rows
    }
  },
  created() {
    this.load()
  },
  methods: {
    load() {
      this.loading = true
      const range = this.filters.completionDate || []
      api.get('/work-orders/completion/query', {
        params: {
          keyword: this.filters.keyword || undefined,
          contractorName: this.filters.contractorName || undefined,
          completionStatus: this.filters.completionStatus || undefined,
          startDate: range[0] || undefined,
          endDate: range[1] || undefined
        }
      }).then(res => {
        this.rows = res.data || []
      }).finally(() => {
        this.loading = false
      })
    },
    reset() {
      this.filters = { keyword: '', contractorName: '', completionStatus: '', completionDate: [] }
      this.load()
    },
    completionMeta(value) {
      return statusMeta(completionStatuses, value)
    },
    spanMethod({ row, columnIndex }) {
      if (columnIndex <= 6) {
        return row.faultRowspan === 0 ? { rowspan: 0, colspan: 0 } : { rowspan: row.faultRowspan, colspan: 1 }
      }
      return { rowspan: 1, colspan: 1 }
    },
    formatPeriod(startDate, finishDate) {
      return [startDate || '-', finishDate || '-'].join(' è‡³ ')
    },
    materialMeta(item) {
      const parts = []
      if (item.technicalParameter) parts.push(item.technicalParameter)
      if (item.source) parts.push('来源:' + item.source)
      if (item.outboundNo) parts.push('出库单:' + item.outboundNo)
      if (item.certificate) parts.push('合格证明:' + item.certificate)
      if (item.remark) parts.push('备注:' + item.remark)
      return parts.join(';') || '-'
    }
  }
}
</script>
<style scoped>
.table-panel {
  overflow-x: auto;
}
.detail-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.detail-line,
.material-line {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  line-height: 20px;
}
.material-line > div {
  min-width: 0;
}
.detail-index {
  flex: 0 0 auto;
  min-width: 16px;
  height: 16px;
  margin-top: 2px;
  border-radius: 50%;
  color: #fff;
  background: #7b8fa6;
  font-size: 11px;
  line-height: 16px;
  text-align: center;
}
.material-meta {
  color: #7b8a9a;
  font-size: 12px;
}
.muted {
  color: #a3afbc;
}
</style>
emergency-repair-web/src/views/system/ErrorLogManage.vue
New file
@@ -0,0 +1,152 @@
<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; }
.toolbar ::v-deep .el-range-separator { flex: 0 0 18px; min-width: 18px; text-align: center; }
.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/LoginLogManage.vue
@@ -92,5 +92,6 @@
</script>
<style scoped>
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.toolbar ::v-deep .el-range-separator { flex: 0 0 18px; min-width: 18px; text-align: center; }
.pager { margin-top: 12px; text-align: right; }
</style>
emergency-repair-web/src/views/system/MenuManage.vue
@@ -44,7 +44,9 @@
    <el-dialog :title="form.id ? '编辑菜单' : '新增菜单'" :visible.sync="dialogVisible" width="640px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="105px">
        <el-form-item label="上级菜单"><el-input :value="parentName" readonly></el-input></el-form-item>
        <el-form-item label="上级菜单">
          <el-cascader v-model="form.pid" :options="parentOptions" :props="parentProps" :show-all-levels="false" filterable clearable placeholder="请选择上级菜单" style="width:100%"></el-cascader>
        </el-form-item>
        <el-form-item label="菜单类型">
          <el-radio-group v-model="form.menuType">
            <el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button>
@@ -63,7 +65,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>
@@ -89,7 +91,6 @@
      dialogVisible: false,
      saving: false,
      form: {},
      parent: null,
      rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] },
      buttonDialogVisible: false,
      buttonSaving: false,
@@ -99,15 +100,25 @@
    }
  },
  computed: {
    parentName() {
      if (!this.parent) return this.form.pid && this.form.pid !== 0 ? '上级菜单' : '无(一级菜单)'
      return this.parent.name
    parentProps() {
      return { value: 'id', label: 'name', children: 'children', checkStrictly: true, emitPath: false, expandTrigger: 'hover' }
    },
    selectedParent() {
      const pid = String(this.form.pid || '0')
      if (pid === '0') return null
      return this.flat.find(item => String(item.id) === pid) || null
    },
    parentOptions() {
      const excluded = this.excludedMenuIds()
      const allowedType = this.form.menuType === 2 ? 1 : 0
      const nodes = this.buildParentOptions(this.tree, allowedType, excluded)
      return this.form.menuType === 2 ? nodes : [{ id: '0', name: '无(一级菜单)', children: nodes }]
    },
    typeOptions() {
      if (this.parent && this.parent.menuType === 1) {
      if (this.selectedParent && this.selectedParent.menuType === 1) {
        return [{ label: '按钮', value: 2 }]
      }
      if (this.parent && this.parent.menuType === 0) {
      if (this.selectedParent && this.selectedParent.menuType === 0) {
        return [{ label: '目录', value: 0 }, { label: '菜单', value: 1 }]
      }
      if (this.form.id && this.form.menuType === 2) {
@@ -131,10 +142,9 @@
      return type === 2 ? 'warning' : type === 0 ? 'info' : 'primary'
    },
    openCreate(parent) {
      this.parent = parent || null
      const menuType = parent && parent.menuType === 1 ? 2 : parent ? 1 : 0
      this.form = {
        pid: parent ? parent.id : 0,
        pid: parent ? String(parent.id) : '0',
        name: '',
        url: '',
        icon: menuType === 2 ? '' : 'el-icon-tickets',
@@ -146,8 +156,7 @@
      this.dialogVisible = true
    },
    openEdit(row) {
      this.parent = this.flat.find(item => item.id === row.pid) || null
      this.form = { ...row }
      this.form = { ...row, pid: row.pid == null || row.pid === 0 ? '0' : String(row.pid) }
      this.dialogVisible = true
    },
    save() {
@@ -158,6 +167,7 @@
          return
        }
        const payload = { ...this.form }
        if (payload.pid === null || payload.pid === undefined || payload.pid === '') payload.pid = '0'
        if (payload.menuType === 2) {
          payload.url = null
          payload.icon = null
@@ -172,6 +182,27 @@
        }).finally(() => { this.saving = false })
      })
    },
    excludedMenuIds() {
      const excluded = new Set()
      const walk = (nodes, underSelf) => {
        ;(nodes || []).forEach(node => {
          const next = underSelf || String(node.id) === String(this.form.id)
          if (next) excluded.add(String(node.id))
          walk(node.children, next)
        })
      }
      walk(this.tree, false)
      return excluded
    },
    buildParentOptions(nodes, allowedType, excluded) {
      return (nodes || [])
        .filter(node => node.menuType === allowedType && !excluded.has(String(node.id)))
        .map(node => ({
          id: String(node.id),
          name: node.name,
          children: this.buildParentOptions(node.children, allowedType, excluded)
        }))
    },
    remove(row) {
      this.$confirm(`确定删除“${row.name}”吗?`, '删除确认', { type: 'warning' })
        .then(() => api.delete('/system/menu/' + row.id))
emergency-repair-web/src/views/system/MenuPermission.vue
File was deleted
emergency-repair-web/src/views/system/OperationLogManage.vue
@@ -100,5 +100,6 @@
</script>
<style scoped>
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.toolbar ::v-deep .el-range-separator { flex: 0 0 18px; min-width: 18px; text-align: center; }
.pager { margin-top: 12px; text-align: right; }
</style>
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,229 @@
<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-radio-group v-model="permissionScope" size="small" @change="handleScopeChange">
        <el-radio-button label="all">全部</el-radio-button>
        <el-radio-button label="assigned">已设置</el-radio-button>
      </el-radio-group>
      <el-checkbox v-if="permissionScope === 'all'" v-model="checkAll" @change="toggleAll">全选</el-checkbox>
      <span class="permission-tip">{{ permissionTip }}</span>
    </div>
    <div v-loading="loading" class="permission-tree">
      <el-tree
        ref="permissionTree"
        :data="displayMenus"
        node-key="id"
        show-checkbox
        default-expand-all
        empty-text="该角色尚未设置权限"
        :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,
      permissionScope: 'all',
      selectedRoleIds: []
    }
  },
  computed: {
    dialogVisible: {
      get() { return this.visible },
      set(value) { this.$emit('update:visible', value) }
    },
    allMenuIds() {
      return this.flatMenus.map(item => String(item.id))
    },
    displayMenus() {
      if (this.permissionScope === 'all') return this.menus
      const assigned = new Set(this.selectedRoleIds.map(String))
      const filterAssigned = nodes => (nodes || []).reduce((result, node) => {
        const children = filterAssigned(node.children)
        if (assigned.has(String(node.id)) || children.length) {
          result.push({ ...node, children })
        }
        return result
      }, [])
      return filterAssigned(this.menus)
    },
    permissionTip() {
      return this.permissionScope === 'all'
        ? '菜单和按钮权限均可勾选,保存后切换角色会自动反显。'
        : '仅显示当前角色已设置的权限及其父级节点。'
    }
  },
  watch: {
    visible(value) {
      if (value) this.open()
    },
  },
  methods: {
    open() {
      if (!this.role || !this.role.id) return
      this.loading = true
      this.checkAll = false
      this.permissionScope = 'all'
      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 })
    },
    handleScopeChange() {
      this.selectedRoleIds = this.checkedMenuIds()
      this.$nextTick(() => {
        if (!this.$refs.permissionTree) return
        this.$refs.permissionTree.setCheckedKeys(this.displayCheckedKeys(this.selectedRoleIds))
        this.syncCheckAllState()
      })
    },
    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() {
      const checked = this.checkedMenuIds()
      this.selectedRoleIds = checked
      this.checkAll = checked.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
@@ -504,3 +504,216 @@
- éžç®¡ç†å‘˜å®Œå·¥å¡«æŠ¥çš„单位匹配目前按 `DEPT_NAME`,不是 `DEPT_ID`;若出现同名部门,应改为 ID ç²¾ç¡®åŒ¹é…ã€‚
- åŽ†å²æ¼”ç¤ºæ´¾å·¥å•å¯èƒ½ä»ä¿å­˜æ—§ `T_CONTRACTOR` åç§°ï¼Œéžç®¡ç†å‘˜ cj ç”¨æˆ·å¯èƒ½çœ‹ä¸åˆ°ï¼›`admin` ä¸å—单位过滤影响。
- `.codex-run`、Office é”æ–‡ä»¶ä»¥åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` æ˜¯æœ¬åœ°è”调或临时文件,不应提交。
## åäºŒã€2026-10-06 è”调收尾(需求 20)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 20 | èœå•权限管理取消租户权限,仅保留角色权限;权限复选框反显 | å·²å®Œæˆ | æƒé™æ ‘保留菜单和按钮节点;角色切换后正常反显,接口仅处理 `roleId` / `roleMenuIds` |
### 12.1 ä¸»è¦å˜æ›´æ–‡ä»¶
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.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-web/src/views/system/MenuPermission.vue`
### 12.2 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests compile` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ã€‚
- `GET /api/system/menu-permissions?roleId=2106221661022490625` è¿”回字符串型 `roleMenuIds` å…± 14 æ¡ï¼Œé¿å…æµè§ˆå™¨è§£æžé›ªèб ID æ—¶ä¸¢å¤±ç²¾åº¦ã€‚
- `node .codex-run/verify-menu-permission.js` è¾“出 `hasTenantPanel: false`、`buttonNodeCount: 76`、`checkedMatchesSaved: true`、`hasError: false`。
- é¡µé¢ä¿ç•™æŒ‰é’®èŠ‚ç‚¹ï¼Œè§’è‰²åˆ‡æ¢åŽæƒé™æ ‘å¤é€‰æ¡†åæ˜¾æ­£å¸¸ï¼›æˆªå›¾ `.codex-run/menu-permission-verified.png`。
### 12.3 æŽ¥æ‰‹æ³¨æ„
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 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` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åå››ã€2026-10-06 è”调收尾(需求 23-25)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 23 | ç³»ç»Ÿåç§°ã€ç‰ˆæœ¬æ”¹ä¸ºå…³é”®è¯é…ç½®ï¼Œç‰ˆæœ¬çº¢è‰²æ˜¾ç¤º | å·²å®Œæˆ | codename ä¸º `rk_SystemName`、`rk_SystemVer`;登录页、页头、Logo å’Œæµè§ˆå™¨æ ‡é¢˜ç»Ÿä¸€è¯»å–配置,页头版本为红色 |
| 24 | å®Œå·¥ä¿¡æ¯æŸ¥é˜…,多故障及多条完工明细展示 | å·²å®Œæˆ | å¢žåŠ  `/completion/query` å’Œ `GET /api/work-orders/completion/query`;按故障汇总,同一故障的多张派工单展开为多行,故障信息列使用 `rowspan` çºµå‘合并,处理情况和器材支持多条 |
| 25 | èœå•管理编辑时可修改父节点 | å·²å®Œæˆ | ä¸Šçº§èœå•改为级联选择器,排除自身和全部下级;后端增加父节点、自引用和循环层级校验 |
### 14.1 ä¸»è¦å˜æ›´æ–‡ä»¶
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java`(新增)
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.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/completion/CompletionQuery.vue`(新增)
- `emergency-repair-web/src/views/system/MenuManage.vue`
- `emergency-repair-web/src/components/AppHeader.vue`、`Login.vue`、`RepairLogo.vue`、`layout/AppLayout.vue`、`router/index.js`、`utils/sensitive.js`、`main.js`
- `emergency-repair-web/public/sensitive-words.json`、`public/index.html`
- `requirement/敏感词字典.json`
### 14.2 å®Œå·¥ä¿¡æ¯æŸ¥é˜…口径
- ç»“果按故障汇总,不按派工单汇总。
- ä¸€ä¸ªæ•…障有多张已填报完工信息的派工单时,表格展开为多行;序号、故障编号、rk_X号、设备名称、工程名称及损坏情况、上报单位、上报日期这 7 åˆ—共用故障行,使用单元格纵向合并。
- æ´¾å·¥å•编号、承修单位、完工状态、工程编号、开工/完工、承修人、完工处理情况、更换器材、修后质量、原因分析、提交时间按派工单展示。
- æŸ¥è¯¢æ”¯æŒå…³é”®è¯ã€æ‰¿ä¿®å•位、完工状态和完工日期;关键词同时匹配故障、派工单、工程编号、承修人、处理情况和器材。
### 14.3 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests compile` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req23-25.js` é€šè¿‡ï¼šç³»ç»Ÿåç§°ã€ç‰ˆæœ¬å’Œçº¢è‰²æ ·å¼æ­£å¸¸ï¼›å®Œå·¥æŸ¥é˜…正常;菜单编辑父节点“故障管理”可编辑。
- ä¸´æ—¶æž„造同一故障两张派工单、两条完工记录,接口返回 1 ä¸ªæ•…障节点和 2 å¼ æ´¾å·¥å•,各带处理情况和器材明细。
- æµè§ˆå™¨æ˜¾ç¤º 2 å¼ æ´¾å·¥å•行,故障编号只出现 1 æ¬¡ï¼Œ7 ä¸ªæ•…障信息列均为 `rowspan=2`;两张单的明细均正常;截图 `.codex-run/completion-query-merge-verified.png`。临时数据已清理。
- èœå•级联父节点接口实测可正常移动子菜单;选择自身或下级时被后端拒绝,临时菜单已清理。
### 14.4 å½“前服务与接手注意
- å‰ç«¯ `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge è°ƒè¯•端口 `9222`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
- å®Œå·¥æŸ¥é˜…当前只展示已有 `T_COMPLETION_INFO` å®Œå·¥å¡«æŠ¥è®°å½•的派工单,并且接口未分页。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åäº”、2026-10-06 è”调收尾(需求 21 æƒé™è®¾ç½®èŒƒå›´åˆ‡æ¢ï¼‰
| åºå· | éœ€æ±‚补充 | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 21 | æƒé™è®¾ç½®å¢žåŠ â€œå…¨éƒ¨ / å·²è®¾ç½®â€åˆ‡æ¢ | å·²å®Œæˆ | `RolePermissionDialog.vue` å¢žåŠ èŒƒå›´åˆ‡æ¢ï¼›â€œå…¨éƒ¨â€æ˜¾ç¤ºå®Œæ•´èœå•åŠæŒ‰é’®æ ‘å¹¶å¯å…¨é€‰ï¼Œâ€œå·²è®¾ç½®â€ä»…æ˜¾ç¤ºå½“å‰è§’è‰²å·²æŽˆæƒèŠ‚ç‚¹å’Œçˆ¶çº§è·¯å¾„ï¼Œåˆ‡æ¢æ—¶ä¿ç•™æœªä¿å­˜å‹¾é€‰ï¼Œé‡æ–°æ‰“å¼€é»˜è®¤â€œå…¨éƒ¨â€ |
### 15.1 ä¸»è¦å˜æ›´æ–‡ä»¶
- `emergency-repair-web/src/views/system/RolePermissionDialog.vue`
- `requirement/改进20261003.txt`(补充需求 21 çš„范围切换口径)
### 15.2 éªŒè¯ç»“æžœ
- å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req21-22.js` é€šè¿‡ï¼šå·¦ä¾§æ— â€œèœå•权限管理”,角色管理存在“权限设置”。
- å…¨éƒ¨è§†å›¾æ˜¾ç¤º 119 ä¸ªå¤é€‰æ¡†ã€13 ä¸ªå·²å‹¾é€‰ã€1 ä¸ªåŠé€‰ï¼›å·²è®¾ç½®è§†å›¾æ˜¾ç¤º 14 ä¸ªèŠ‚ç‚¹ï¼Œå‡ä¸ºå½“å‰å·²è®¾ç½®èŠ‚ç‚¹æˆ–çˆ¶çº§è·¯å¾„ï¼›åˆ‡å›žå…¨éƒ¨åŽå‹¾é€‰çŠ¶æ€æ¢å¤ï¼Œæ— ç½‘ç»œå¼‚å¸¸ã€‚
- æˆªå›¾ï¼š`.codex-run/role-permission-assigned-scope.png`。
### 15.3 æŽ¥æ‰‹æ³¨æ„
- å½“前运行:前端 `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge CDP `9222`。登录 `admin / admin123`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åå…­ã€2026-10-06 è”调收尾(需求 26 æ—¥å¿—日期范围分隔符)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 26 | æ—¥å¿—管理的日期组件两个日期中间的“至”显示不完整 | å·²å®Œæˆ | ç™»å½•日志、操作日志、错误日志的 `.el-range-separator` å›ºå®šä¸º `18px` å¹¶å±…中,三个页面实际宽度、`clientWidth`、`scrollWidth` å‡ä¸º `18px` |
### 16.1 ä¸»è¦å˜æ›´æ–‡ä»¶
- `emergency-repair-web/src/views/system/LoginLogManage.vue`
- `emergency-repair-web/src/views/system/OperationLogManage.vue`
- `emergency-repair-web/src/views/system/ErrorLogManage.vue`
- `requirement/改进20261003.txt`(补充需求 26)
### 16.2 éªŒè¯ç»“æžœ
- å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req26.js` é€šè¿‡ï¼šä¸‰ä¸ªæ—¥å¿—页面均显示“至”,无裁切,无网络异常。
- æˆªå›¾ï¼š`.codex-run/login-log-date-separator.png`、`.codex-run/operation-log-date-separator.png`、`.codex-run/error-log-date-separator.png`。
### 16.3 æŽ¥æ‰‹æ³¨æ„
- å½“前运行:前端 `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge CDP `9222`。登录 `admin / admin123`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åä¸ƒã€2026-10-06 è”调收尾(需求 27 è®¾å¤‡é€‰æ‹©èŒƒå›´ï¼‰
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 27 | æ–°å¢žæ•…障选择 `rk_X号` åŽï¼Œè®¾å¤‡é€‰æ‹©å™¨åº”只显示所选 `side` èŠ‚ç‚¹çš„ä¸‹çº§ | å·²å®Œæˆ | åŽç«¯åªè¿”回所选 `side` èŠ‚ç‚¹ä½œä¸ºæ ¹ï¼›å‰ç«¯è‡ªåŠ¨å±•å¼€è¯¥æ ¹èŠ‚ç‚¹å¹¶ç»§ç»­æ‡’åŠ è½½ä¸‹çº§ï¼ŒæŽ¥å£ä¸Žæµè§ˆå™¨éªŒè¯é€šè¿‡ |
### 17.1 ä¸»è¦å˜æ›´æ–‡ä»¶
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
- `emergency-repair-web/src/components/ProductTreePicker.vue`
- `requirement/改进20261003.txt`(补充需求 27)
### 17.2 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests compile`、`mvn -B -DskipTests package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `GET /api/system/tree/product/side-roots?xNo=203` åªè¿”回对应的 `side` æ ¹èŠ‚ç‚¹ï¼›`201 / 203 / 301 / æµ‹è¯•T` å‡åˆ†åˆ«éªŒè¯é€šè¿‡ã€‚
- `node .codex-run/verify-req27.js` é€šè¿‡ï¼šæ ¹èŠ‚ç‚¹æ–‡æœ¬ä»…ä¸º `203`,自动展开 1 ä¸ªæ ¹èŠ‚ç‚¹ï¼Œæ˜¾ç¤ºä¸‹çº§ç³»ç»ŸèŠ‚ç‚¹ï¼Œæ— ç½‘ç»œå¼‚å¸¸ã€‚
- è¯·æ±‚链路确认:`side-roots?xNo=203` åŽè¯·æ±‚ `children?parentId=1280000000000009729`。截图:`.codex-run/req27-side-tree.png`。
### 17.3 æŽ¥æ‰‹æ³¨æ„
- å½“前运行:前端 `http://localhost:8082`,PID `138164`;后端 `http://127.0.0.1:8091`,PID `148268`;DM8 `127.0.0.1:5236`;Edge CDP `9222`。登录 `admin / admin123`。
- éœ€æ±‚ 26 å·²æäº¤å¹¶æŽ¨é€ï¼Œæäº¤ `f651ad5`;需求 27 å·²æäº¤å¹¶æŽ¨é€ï¼Œæäº¤ `f9114ff`(`fix: limit equipment picker to selected X number`)。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åå…«ã€2026-10-06 è”调修复(菜单名称被启动初始化覆盖)
| é—®é¢˜ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- |
| ç³»ç»Ÿé…ç½®ä¸‹â€œäº§å“ç»“构树管理”手工改为“产品结构树”后,后端重启又加回“管理” | å·²ä¿®å¤ | `DataInitializer.ensureMenu()` ä¸å†è¦†ç›–已存在菜单名称;新建菜单默认名称改为“产品结构树”;改名后重启新 jar åç§°ä¿æŒä¸å˜ |
### 18.1 ä¸»è¦å˜æ›´æ–‡ä»¶
- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
### 18.2 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests compile`、`mvn -B -DskipTests package` é€šè¿‡ã€‚
- èœå•记录 `id=2101493905517301763`、`url=/system/product-tree` æ”¹åä¸ºâ€œäº§å“ç»“构树”后,新 jar é‡å¯å¹¶å†æ¬¡æŸ¥è¯¢ï¼Œåç§°ä»ä¸ºâ€œäº§å“ç»“构树”。
- å½“前后端 PID `151580`,监听 `http://127.0.0.1:8091`;前端仍监听 `http://localhost:8082`。
requirement/¸Ä½ø20261003.txt
@@ -26,4 +26,17 @@
17、角色管理用户设置增加一个全部/已设置切换功能
18、增加功能显示组织机构(部门树表)
19、承修单位应该到部门表里找DEPT_TYPE列为cj的记录
19、承修单位应该到部门表里找DEPT_TYPE列为cj的记录
20、菜单权限管理,双权限管理应该分2步, ç¬¬1步设置租户权限,第2步设置角色权限, ç›®å‰ç”¨æˆ·æœ¬èº«æ˜¯æ²¡æœ‰ç§Ÿæˆ·çš„,所以这个租户权限设置就没必要了,保留角色权限设置就可以了,权限检查框要反显
21、菜单权限管理,这个菜单不要,在角色管理增加一个权限设置按钮,类似用户设置那个按钮那样,也象用户设置增加个全部/已设置的切换开关
22、系统管理下面增加2级菜单组节点, ç³»ç»Ÿé…ç½®ï¼ˆé™¤äº†æƒé™é…ç½®ã€æ—¥å¿—管理管理下面哪些菜单外,其他菜单放到系统配置下面)、权限配置(用户管理、角色管理、菜单管理)、日志管理(登录日志、操作日志、错误日志)
   é”™è¯¯æ—¥å¿—为新增功能,记录程序运行出现的bug
23、系统名称、网络版等做成关键词配置,codename分别为rk_SystemName、rk_SystemVer ç‰ˆæœ¬ç”¨çº¢è‰²å­—体显示
24、增加完工信息查阅功能,在一个表展示多个故障及完工信息展示(其中完工处理情况、更换器材都是多个)
25、菜单管理编辑时,应该可以修改父节点
26、日志管理的日期组件2个日期中间那个“至”显示不完整
27、新增故障那里选了rk_X号(对应产品结构树的side类型)后,再选设备的时候还是显示整个产品结构树,应该显示锁选节点的下级节点
requirement/Ãô¸Ð´Ê×Öµä.json
@@ -1,6 +1,18 @@
{
  "sensitive_words": [
    {
      "codename": "rk_SystemName",
      "real_name": "临抢修管理系统",
      "scope": "documents,ui,code",
      "note": "系统名称,登录页、页头及浏览器标题统一使用该配置"
    },
    {
      "codename": "rk_SystemVer",
      "real_name": "网络版",
      "scope": "documents,ui,code",
      "note": "系统版本,页头中使用红色字体显示"
    },
    {
      "codename": "rk_X号",
      "real_name": "舷号",
      "scope": "documents,ui,code",