zzw
3 天以前 757dcb3fcad665eaef93074a4981c4134f7a027e
feat: complete system management and repair workflow updates

- add user, role, menu button, organization, login log, and operation log management
- switch product tree and contractor data sources
- complete fault numbering, audit fields, logical deletion, and repair flow fixes
- document requirements 1-19 and the 2026-10-04 integration handoff
17个文件已添加
60个文件已修改
4599 ■■■■■ 已修改文件
HANDOFF.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/pom.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/auth/AuthController.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Acceptance.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/BaseEquipment.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/CompletionInfo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Contractor.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/EngineeringMajor.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Fault.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/ProductNode.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/TeamMajor.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/WorkOrder.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/ReportService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/AuditMetaObjectHandler.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/CurrentUserContext.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/CurrentUserInterceptor.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/common/entity/BaseAuditEntity.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/config/WebConfig.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysLoginLog.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysMenu.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysOperationLog.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRole.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRoleMenu.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRoleUser.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysTenantMenu.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/log/OperationLogInterceptor.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysLoginLogMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysOperationLogMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java 880 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/resources/mapper/ReportMapper.xml 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/sensitive-words.json 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/api/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/MenuNode.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/ProductTreePicker.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/RepairLogo.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/layout/AppLayout.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/main.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/router/index.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/utils/dict.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/utils/sensitive.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/Dashboard.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/Login.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/acceptance/AcceptanceList.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/completion/CompletionList.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/completion/CompletionReport.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/DispatchPage.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/FaultEditDialog.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/FaultReport.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/SimpleApproval.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/fault/XcbReview.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/report/ReportPage.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/ContractorManage.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/LoginLogManage.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuManage.vue 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuPermission.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/OperationLogManage.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/OrganizationManage.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/RoleManage.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/TeamMajorManage.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/TreeManage.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/UserManage.vue 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/交接文件_2026.10.3.md 506 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/决策表_待确认项.md 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/前44页需求梳理.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/工程状态机设计.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/改进20261003.txt 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/敏感词字典.json 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/界面还原.html 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -289,14 +289,14 @@
### 0.10 2026-09-28 ä¸“业配置与故障编号自动生成(本次会话,已实现并验证)
用户本次给出的 4 æ¡æ–°éœ€æ±‚:① ç³»ç»Ÿç®¡ç†å¢žåŠ â€œå·¥ç¨‹ä¸“ä¸šé…ç½®â€ï¼ˆå­—æ®µä»…åç§°ï¼‰ï¼›â‘¡ ç³»ç»Ÿç®¡ç†å¢žåŠ â€œT队专业配置”(字段名称、分类 A/B);③ æ•…障编号不再手工输入,改为按 `潜1基-X号/A-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼›â‘£ æ´¾å•号为 `故障编号/nn`,承修单位序号从 01 èµ·ã€‚
用户本次给出的 4 æ¡æ–°éœ€æ±‚:① ç³»ç»Ÿç®¡ç†å¢žåŠ â€œå·¥ç¨‹ä¸“ä¸šé…ç½®â€ï¼ˆå­—æ®µä»…åç§°ï¼‰ï¼›â‘¡ ç³»ç»Ÿç®¡ç†å¢žåŠ â€œT队专业配置”(字段名称、分类 A/B);③ æ•…障编号不再手工输入,改为按 `固定前缀-X号/A-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼›â‘£ æ´¾å•号为 `故障编号/nn`,承修单位序号从 01 èµ·ã€‚
已实现:
- â€œå·¥ç¨‹ä¸“业配置”:菜单 `/system/engineering-majors`,字段仅“名称”;表 `LX.T_ENGINEERING_MAJOR(ID, NAME, CREATE_TIME, UPDATE_TIME)`;接口 `/api/system/engineering-majors`(GET/POST/PUT/DELETE)。
- â€œT队专业配置”:菜单 `/system/team-majors`,字段“名称、分类(A/B)”;表 `LX.T_TEAM_MAJOR(ID, NAME, CATEGORY, CREATE_TIME, UPDATE_TIME)`;接口 `/api/system/team-majors`(GET/POST/PUT/DELETE)。
- ä¸¤å¤„配置均校验名称非空与重名;T队专业分类只允许 A/B。启动时 `DataInitializer.ensureMajorConfigMenus` å¹‚等补菜单,`seedMajors` é¢„置演示数据:工程专业 åŠ¨åŠ›/电气/液压/辅机;T队专业 åŠ¨åŠ›=A、液压=A、电气=B、辅机=B(默认拆分,可在页面修改)。
- æ•…障编号自动生成:`BusinessService.generateFaultCode`,格式 `潜1基-X号/分类-YYMMDD-nnn`。
  - å‰ç¼€å›ºå®š `潜1基-`;X号取 3 ä½æ•°å­—(`X01` â†’ `001`)。
- æ•…障编号自动生成:`BusinessService.generateFaultCode`,格式 `固定前缀-X号/分类-YYMMDD-nnn`。
  - å‰ç¼€å›ºå®š `固定前缀-`;X号取 3 ä½æ•°å­—(`X01` â†’ `001`)。
  - åˆ†ç±»å–“T队专业配置”的 A/B;未配置或分类非法时保存报错并提示先行维护。
  - YYMMDD å–上报日期;nnn ä¸ºâ€œåŒä¸€ X号 + åŒä¸€ T队专业 + åŒä¸€ä¸ŠæŠ¥æ—¥æœŸâ€çš„序号,从 001 èµ·ã€‚
  - æ–°å»ºè‡ªåŠ¨ç”Ÿæˆï¼›ä¿®æ”¹æ—¶å¼ºåˆ¶æ²¿ç”¨åŽŸç¼–å·ï¼ˆæ”¹ T队专业/日期也不重新生成)。
@@ -309,8 +309,8 @@
验证(2026-09-28):
- åŽç«¯ `mvn -q -DskipTests package` é€šè¿‡ï¼›å‰ç«¯ `vue-cli-service build` é€šè¿‡ï¼ˆä»…既有包体积提示)。
- æŽ¥å£ï¼šå·¥ç¨‹ä¸“业、T队专业各返回 4 æ¡ï¼›`/api/system/menu/nav` ç³»ç»Ÿç®¡ç†ä¸‹æ–°å¢žä¸¤é¡¹ï¼ˆæŽ’序 35/36)。
- è‡ªåŠ¨ç¼–å·ï¼š`X02 + åŠ¨åŠ›ä¸“ä¸š(A) + 2026-09-28` â†’ `潜1基-002/A-260928-001`、`潜1基-002/A-260928-002`;换 T队专业(电气 B)→ `潜1基-002/B-260928-001`;换上报日期 â†’ `潜1基-001/A-260929-001`;修改故障改 T队专业后编号保持不变。
- æ´¾å•端到端:一条故障分配两个承修单位,生成 `潜1基-002/B-260928-001/01`、`.../02`。测试数据已从 `LX.T_FAULT`、`LX.T_WORK_ORDER` æ¸…理。
- è‡ªåŠ¨ç¼–å·ï¼š`X02 + åŠ¨åŠ›ä¸“ä¸š(A) + 2026-09-28` â†’ `固定前缀-002/A-260928-001`、`固定前缀-002/A-260928-002`;换 T队专业(电气 B)→ `固定前缀-002/B-260928-001`;换上报日期 â†’ `固定前缀-001/A-260929-001`;修改故障改 T队专业后编号保持不变。
- æ´¾å•端到端:一条故障分配两个承修单位,生成 `固定前缀-002/B-260928-001/01`、`.../02`。测试数据已从 `LX.T_FAULT`、`LX.T_WORK_ORDER` æ¸…理。
- æµè§ˆå™¨ï¼šä¸¤ä¸ªæ–°é…ç½®é¡µæ­£å¸¸æ¸²æŸ“;故障上报新增表单“故障编号”只读且占位“保存后自动生成”,T队专业/工程专业下拉为配置项;修复后控制台无新增报错。
- åŽ†å²æ¼”ç¤ºæ•…éšœä»ä½¿ç”¨ `GZ-2026-0xx` æ—§ç¼–号(未迁移,避免影响已派工单引用);新建故障一律使用新规则。
- æ–‡æ¡£å·²åŒæ­¥ï¼š`requirement\决策表_待确认项.md`、`前44页需求梳理.md`、`工程状态机设计.md`、`界面还原.html`,以及首页公告“派单规则”文案。
@@ -646,7 +646,7 @@
**本次会话完成(细节见 0.10 / 0.11)**
- å†³ç­–表 B1–B4 æŒ‰å»ºè®®å…³é—­å¹¶è½åœ°ï¼šç³»ç»Ÿç®¡ç†æ–°å¢žâ€œå·¥ç¨‹ä¸“业配置”(字段:名称)、“T队专业配置”(字段:名称、分类 A/B)。
- æ•…障编号改为自动生成:`潜1基-X号/A-YYMMDD-nnn`(X号 3 ä½æ•°å­—,A=T队专业 A/B,nnn ä¸ºåŒä¸€ X号+同一专业+同一上报日期内从 001 é€’增);派单号=`故障编号/nn`(承修单位序号,从 01 èµ·ï¼‰ã€‚
- æ•…障编号改为自动生成:`固定前缀-X号/A-YYMMDD-nnn`(X号 3 ä½æ•°å­—,A=T队专业 A/B,nnn ä¸ºåŒä¸€ X号+同一专业+同一上报日期内从 001 é€’增);派单号=`故障编号/nn`(承修单位序号,从 01 èµ·ï¼‰ã€‚
- æ•…障上报页:任务状态标签与任务状态下拉框拆成两行(标签换到下一行)。
- å®Œå·¥ä¿¡æ¯ä¸ŠæŠ¥é¡µï¼ˆAXU15):修后质量情况独占整行;项目名称与验收方式同一行;故障说明、原因分析各独占整行;“处理解决情况”改为可多行的表格(序号/处理解决情况);维修器材表列改为序号、名称、规格型号、技术参数、数量、计量单位、合格证明、出库单号、来源、备注。后端新增 `LX.T_COMPLETION_INFO.SOLUTIONS_JSON` ä¸Ž `MaterialItem.certificate`,并继续兼容原 `SOLUTION` å­—段。
- é¡¹ç›®çº³å…¥å…¬å¸ Git ä»“库:`http://admin@61.183.254.94:3000/r/EmergencyRepair.git`,分支 `main`,首提交 `5305262`(103 ä¸ªæ–‡ä»¶ï¼‰ï¼ŒéšåŽ `0bacdcf` è¡¥è®°è¿œç«¯ä¿¡æ¯ã€‚
@@ -697,3 +697,32 @@
2. `sensitive.js` æ”¹åŠ¨å·²èŽ·ç¡®è®¤å¹¶éšæœ¬æ¬¡æäº¤æŽ¨é€ï¼›`requirement\~$原始需求.docx` æ˜¯ Word é”æ–‡ä»¶ï¼Œä¸è¦æäº¤ã€‚
3. éœ€æ±‚侧待确认项不变:决策表 B6–B10、AXU15 å­—段来源与可改性、完工验收各环节角色与打印输出、修后质量评级与 AXU14 T员评分字段的关系。
4. åŽç»­æ”¹åŠ¨å®ŒæˆåŽæŽ¨é€åˆ°å…¬å¸ Git ä»“库 `main` åˆ†æ”¯ã€‚
## 17. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-04 å‘¨å…­ï¼Œéœ€æ±‚ 11 / 12 / 17-19 è”调收尾)
**本轮完成内容**
- éœ€æ±‚ 11:完工情况处理将状态改为“完工”时,只更新派工单状态,不再跳转“完工信息填报”。完工处理与完工填报由不同用户分别操作。
- éœ€æ±‚ 12:完工信息填报的派工单选择列表只展示“已在完工情况处理中确认完工、属于当前登录用户所属单位、且尚未填报”的派工单,并增加工程名称(项目名称)显示。
- éœ€æ±‚ 17:角色管理的“用户设置”增加“全部 / å·²è®¾ç½®â€è¿‡æ»¤åˆ‡æ¢ã€‚
- éœ€æ±‚ 18:新增“组织机构”部门树表页面,菜单 `/system/organization`,页面 `OrganizationManage.vue`,数据来自 `CSICZB_ZB.SYS_DEPT`,后端接口 `/api/system/depts`。当前环境返回 34 ä¸ªéƒ¨é—¨ï¼Œæ”¯æŒåç§°ã€ç¼–码、负责人、部门类型筛选。
- éœ€æ±‚ 19:承修单位改为读取 `SYS_DEPT.DEPT_TYPE='cj'` çš„部门记录,部门 ID ä½œä¸ºæ‰¿ä¿®å•位 ID;承修单位接口保留 GET,页面改为只读。当前环境返回 7 ä¸ª cj éƒ¨é—¨ï¼ŒXCB审核、派单均使用 cj éƒ¨é—¨ ID/名称。
**主要文件**
- åŽç«¯ï¼š`emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.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-web/src/views/system/OrganizationManage.vue`、`emergency-repair-web/src/views/system/ContractorManage.vue`、`emergency-repair-web/src/views/completion/CompletionReport.vue`、`emergency-repair-web/src/views/system/RoleManage.vue`、`emergency-repair-web/src/router/index.js`。
**验证结果**
- åŽç«¯ `mvn -B -DskipTests clean package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ã€‚
- é¡µé¢éªŒè¯è„šæœ¬å®Œæ•´é€šè¿‡ï¼š`node .codex-run/verify-items-11-12-17.js`、`node .codex-run/verify-items-18-19.js`;截图见 `.codex-run/organization-verified.png`、`.codex-run/contractor-readonly-verified.png`、`.codex-run/completion-report-verified.png`、`.codex-run/role-scope-verified.png`。
- ç«¯åˆ°ç«¯è”调使用临时故障 `固定前缀-088/A-261004-001`,跑通上报、XCB审核分配“厂家1/厂家2”、CMB/JCC三级审批、派单。最终故障保存 `contractorIds = 1303144990487465986,1303145150793764866`、`contractorNames = åނ家1、厂家2`,生成两张派工单,均保存 cj éƒ¨é—¨ ID/名称。
- å®Œå·¥å¡«æŠ¥å¯è§æ€§éªŒè¯ï¼š`cjyh1-1` ä»…看到厂家1派工单,`cjyh2-1` ä»…看到厂家2派工单,`admin` å¯çœ‹åˆ°ä¸¤å¼ ã€‚临时 `T_FAULT` / `T_WORK_ORDER` æµ‹è¯•数据已清理。
**当前运行状态(2026-10-04)**
- å‰ç«¯ï¼š`http://localhost:8082`;后端:`http://127.0.0.1:8091`;DM8:`127.0.0.1:5236`;headless Edge è°ƒè¯•实例端口 `9222`。
- ç™»å½•:`admin / admin123`;数据库:`SYSDBA/SYSDBA`,业务 schema `LX`。
**接手注意事项**
- éžç®¡ç†å‘˜ç”¨æˆ·çš„完工填报派工单单位匹配当前按 `DEPT_NAME`,不是按 `DEPT_ID`。本次验证通过;如存在同名部门,应改为按部门 ID ç²¾ç¡®åŒ¹é…ã€‚
- åŽ†å²æ¼”ç¤ºæ´¾å·¥å•å¯èƒ½ä¿ç•™æ—§ `T_CONTRACTOR` åç§°ï¼Œéžç®¡ç†å‘˜ cj ç”¨æˆ·å¯èƒ½çœ‹ä¸åˆ°è¿™äº›åŽ†å²å•ï¼›`admin` ä¸å—单位过滤影响。
- `.codex-run`、Office é”æ–‡ä»¶ `requirement\~$原始需求.docx` å’Œ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` å±žæœ¬åœ°è”调或临时文件,不应提交。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥æ–‡æ¡£æäº¤å¹¶æŽ¨é€å…¬å¸ Git ä»“库 `main` åˆ†æ”¯ï¼›åŽç»­å¯åŠ¨ã€æž„å»ºå’ŒæŽ’é”™å‘½ä»¤æ²¿ç”¨ç¬¬ 16 èŠ‚ã€‚
emergency-repair-server/pom.xml
@@ -29,6 +29,10 @@
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
emergency-repair-server/src/main/java/com/emergencyrepair/auth/AuthController.java
@@ -1,16 +1,23 @@
package com.emergencyrepair.auth;
import com.emergencyrepair.common.Result;
import com.emergencyrepair.system.entity.SysLoginLog;
import com.emergencyrepair.system.mapper.SysLoginLogMapper;
import com.emergencyrepair.system.service.SystemService;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -20,44 +27,148 @@
@RequiredArgsConstructor
public class AuthController {
    private final JdbcTemplate jdbcTemplate;
    private final SysLoginLogMapper loginLogMapper;
    private final SystemService systemService;
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    @PostMapping("/login")
    public Result<Map<String, Object>> login(@RequestBody Map<String, String> body) {
        String username = body.get("username");
        String password = body.get("password");
        if (!"admin".equals(username) || (!"admin".equals(password) && !"admin123".equals(password))) {
            return Result.error("用户名或密码错误,第一版请使用 admin/admin123");
    public Result<Map<String, Object>> login(@RequestBody Map<String, String> body, HttpServletRequest request) {
        String username = body == null || body.get("username") == null ? "" : body.get("username").trim();
        String password = body == null || body.get("password") == null ? "" : body.get("password");
        Map<String, Object> user = findUser(username);
        boolean success = false;
        String message = "用户名或密码错误";
        if (user != null) {
            if (isDisabled(user)) {
                message = "账号已停用";
            } else if (matches(password, stringValue(user.get("password")))) {
                success = true;
            } else if ("admin".equals(username) && ("admin".equals(password) || "admin123".equals(password))) {
                // Keep the original demo credential working for the built-in administrator.
                success = true;
            }
        }
        recordLogin(username, user, request, success, success ? "登录成功" : message);
        if (!success) {
            return Result.error(message);
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("token", "demo-admin-token");
        data.put("username", "admin");
        data.put("realName", "系统管理员");
        data.put("deptName", resolveDepartmentName(username));
        data.put("token", "demo-" + username + "-token");
        data.put("username", username);
        data.put("realName", StringUtils.hasText(stringValue(user.get("nickName")))
                ? stringValue(user.get("nickName")) : username);
        data.put("deptName", stringValue(user.get("deptName")));
        data.put("roles", systemService.roleCodes(username));
        data.put("permissions", systemService.permissionCodes(username));
        return Result.ok(data);
    }
    @GetMapping("/me")
    public Result<Map<String, Object>> me() {
    public Result<Map<String, Object>> me(@RequestHeader(value = "X-Username", required = false) String headerUser,
                                          @RequestHeader(value = "Authorization", required = false) String authorization) {
        String username = currentUsername(headerUser, authorization);
        Map<String, Object> user = findUser(username);
        if (user == null) {
            return Result.error("登录状态已失效");
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("username", "admin");
        data.put("realName", "系统管理员");
        data.put("deptName", resolveDepartmentName("admin"));
        data.put("roles", new String[]{"SYSTEM_ADMIN"});
        data.put("username", username);
        data.put("realName", StringUtils.hasText(stringValue(user.get("nickName")))
                ? stringValue(user.get("nickName")) : username);
        data.put("deptName", stringValue(user.get("deptName")));
        data.put("roles", systemService.roleCodes(username));
        data.put("permissions", systemService.permissionCodes(username));
        return Result.ok(data);
    }
    private String resolveDepartmentName(String username) {
    private Map<String, Object> findUser(String username) {
        if (!StringUtils.hasText(username)) {
            return "";
            return null;
        }
        try {
            List<String> names = jdbcTemplate.query(
                    "SELECT d.DEPT_NAME FROM LX.SYS_USER u LEFT JOIN LX.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID WHERE u.USER_NAME = ? AND u.DEL_FLAG = '0'",
            List<Map<String, Object>> rows = jdbcTemplate.query(
                    "SELECT u.USER_ID, u.USER_NAME, u.NICK_NAME, u.PASSWORD, u.STATUS, d.DEPT_NAME " +
                            "FROM LX.SYS_USER u LEFT JOIN LX.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID " +
                            "WHERE u.USER_NAME = ? AND (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL)",
                    new Object[]{username},
                    (rs, rowNum) -> rs.getString(1));
            return names.isEmpty() || names.get(0) == null ? "" : names.get(0).trim();
                    (rs, rowNum) -> {
                        Map<String, Object> row = new LinkedHashMap<>();
                        row.put("userId", rs.getLong("USER_ID"));
                        row.put("userName", rs.getString("USER_NAME"));
                        row.put("nickName", rs.getString("NICK_NAME"));
                        row.put("password", rs.getString("PASSWORD"));
                        row.put("status", rs.getString("STATUS"));
                        row.put("deptName", rs.getString("DEPT_NAME"));
                        return row;
                    });
            return rows.isEmpty() ? null : rows.get(0);
        } catch (DataAccessException ex) {
            return null;
        }
    }
    private boolean matches(String rawPassword, String encodedPassword) {
        if (!StringUtils.hasText(rawPassword) || !StringUtils.hasText(encodedPassword)) {
            return false;
        }
        try {
            return passwordEncoder.matches(rawPassword, encodedPassword.trim());
        } catch (Exception ex) {
            return false;
        }
    }
    private boolean isDisabled(Map<String, Object> user) {
        return "1".equals(stringValue(user.get("status")));
    }
    private String stringValue(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
    private String currentUsername(String headerUser, String authorization) {
        if (StringUtils.hasText(headerUser)) {
            return headerUser.trim();
        }
        if (!StringUtils.hasText(authorization)) {
            return "";
        }
        String token = authorization.trim();
        if (token.startsWith("Bearer ")) {
            token = token.substring(7).trim();
        }
        if (token.startsWith("demo-") && token.endsWith("-token")) {
            return token.substring(5, token.length() - 6);
        }
        return token;
    }
    private void recordLogin(String username, Map<String, Object> user, HttpServletRequest request,
                             boolean success, String message) {
        try {
            SysLoginLog log = new SysLoginLog();
            log.setUserId(user == null ? null : (Long) user.get("userId"));
            log.setUserName(username);
            log.setNickName(user == null ? null : stringValue(user.get("nickName")));
            log.setDeptName(user == null ? "" : stringValue(user.get("deptName")));
            log.setLoginIp(clientIp(request));
            log.setStatus(success ? "SUCCESS" : "FAIL");
            log.setMessage(message);
            log.setLoginTime(LocalDateTime.now());
            loginLogMapper.insert(log);
        } catch (Exception ignored) {
            // Login must not fail because the audit table is unavailable.
        }
    }
    private String clientIp(HttpServletRequest request) {
        if (request == null) {
            return "";
        }
        String forwarded = request.getHeader("X-Forwarded-For");
        if (StringUtils.hasText(forwarded)) {
            return forwarded.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
@@ -5,7 +5,6 @@
import com.emergencyrepair.business.entity.Acceptance;
import com.emergencyrepair.business.entity.BaseEquipment;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.Contractor;
import com.emergencyrepair.business.entity.EngineeringMajor;
import com.emergencyrepair.business.entity.Fault;
import com.emergencyrepair.business.entity.ProductNode;
@@ -14,7 +13,6 @@
import com.emergencyrepair.business.mapper.AcceptanceMapper;
import com.emergencyrepair.business.mapper.BaseEquipmentMapper;
import com.emergencyrepair.business.mapper.CompletionInfoMapper;
import com.emergencyrepair.business.mapper.ContractorMapper;
import com.emergencyrepair.business.mapper.EngineeringMajorMapper;
import com.emergencyrepair.business.mapper.FaultMapper;
import com.emergencyrepair.business.mapper.ProductNodeMapper;
@@ -34,10 +32,12 @@
import com.emergencyrepair.system.mapper.SysRoleMenuMapper;
import com.emergencyrepair.system.mapper.SysRoleUserMapper;
import com.emergencyrepair.system.mapper.SysTenantMenuMapper;
import com.emergencyrepair.system.service.ButtonPermissionCatalog;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -60,7 +60,6 @@
    private final SysRoleMenuMapper roleMenuMapper;
    private final SysTenantMenuMapper tenantMenuMapper;
    private final SysRoleUserMapper roleUserMapper;
    private final ContractorMapper contractorMapper;
    private final EngineeringMajorMapper engineeringMajorMapper;
    private final TeamMajorMapper teamMajorMapper;
    private final CompletionInfoMapper completionInfoMapper;
@@ -71,22 +70,27 @@
    private final AcceptanceMapper acceptanceMapper;
    private final BusinessService businessService;
    private final ObjectMapper objectMapper;
    private final JdbcTemplate jdbcTemplate;
    private final ButtonPermissionCatalog buttonPermissionCatalog;
    private final Map<String, Long> menuIds = new HashMap<>();
    private final Map<String, Long> baseIds = new HashMap<>();
    private final Map<String, Long> productDeviceIds = new HashMap<>();
    private final List<Long> contractorIds = new ArrayList<>();
    private final Map<Long, String> contractorNames = new HashMap<>();
    @Override
    @Transactional
    public void run(String... args) {
        seedMenusAndRoles();
        ensureSystemMenus();
        normalizeSensitiveTerms();
        normalizeEquipmentTerms();
        seedButtonMenus();
        ensureAdminRoleAssignment();
        ensureMajorConfigMenus();
        seedMajors();
        seedContractors();
        seedTrees();
        indexProductDevices();
        ensureDemoProductDevices();
        indexProductDevices();
        backfillFaultXNoFromEquipment();
        linkFaultEquipment();
@@ -103,11 +107,11 @@
            createMenu("dashboard", null, "首页门户", "/dashboard", "el-icon-s-home", 1, 10);
            Long fault = createMenu("fault", null, "故障管理", null, "el-icon-warning-outline", 0, 20);
            createMenu("fault-report", fault, "故障上报", "/fault/report", null, 1, 10);
            createMenu("xcb-review", fault, "XCB助理审核", "/fault/review", null, 1, 20);
            createMenu("cmb-approval", fault, "CMB领导审批", "/fault/approval/cmb", null, 1, 30);
            createMenu("jcc-assistant", fault, "JCC助理审批", "/fault/approval/jcc-assistant", null, 1, 40);
            createMenu("jcc-director", fault, "JCC处长审批", "/fault/approval/jcc-director", null, 1, 50);
            createMenu("dispatch", fault, "XCB助理派单", "/fault/dispatch", null, 1, 60);
            createMenu("xcb-review", fault, "rk_XCB助理审核", "/fault/review", null, 1, 20);
            createMenu("cmb-approval", fault, "rk_CMB领导审批", "/fault/approval/cmb", null, 1, 30);
            createMenu("jcc-assistant", fault, "rk_JCC助理审批", "/fault/approval/jcc-assistant", null, 1, 40);
            createMenu("jcc-director", fault, "rk_JCC处长审批", "/fault/approval/jcc-director", null, 1, 50);
            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);
@@ -119,18 +123,19 @@
            createMenu("accept-rating", acceptance, "修后质量评级", "/acceptance/rating", null, 1, 50);
            Long reports = createMenu("reports", null, "统计报表", null, "el-icon-data-analysis", 0, 40);
            createMenu("report-overview", reports, "装备故障总体分析", "/reports/overview", null, 1, 10);
            createMenu("report-overview", reports, "设备故障总体分析", "/reports/overview", null, 1, 10);
            createMenu("report-top", reports, "故障高发设备Top", "/reports/top-equipment", null, 1, 20);
            createMenu("report-team", reports, "按T统计", "/reports/by-team", null, 1, 30);
            createMenu("report-team", reports, "rk_按T统计", "/reports/by-team", null, 1, 30);
            createMenu("report-contractor", reports, "厂家维修情况", "/reports/contractors", null, 1, 40);
            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, "T队专业配置", "/system/team-majors", null, 1, 36);
            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);
@@ -175,7 +180,7 @@
                .last("LIMIT 1"));
        Long pid = system == null ? 0L : system.getId();
        ensureMenu(pid, "工程专业配置", "/system/engineering-majors", 35);
        ensureMenu(pid, "T队专业配置", "/system/team-majors", 36);
        ensureMenu(pid, "rk_T队专业配置", "/system/team-majors", 36);
    }
    private void ensureMenu(Long pid, String name, String url, int sort) {
@@ -192,6 +197,134 @@
        menu.setCreateTime(LocalDateTime.now());
        menu.setUpdateTime(LocalDateTime.now());
        menuMapper.insert(menu);
    }
    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");
    }
    private void grantAdminMenus(String... urls) {
        SysRole adminRole = roleMapper.selectOne(Wrappers.<SysRole>lambdaQuery()
                .eq(SysRole::getCode, "SYS_ADMIN").last("LIMIT 1"));
        if (adminRole == null) {
            return;
        }
        for (String url : urls) {
            SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                    .eq(SysMenu::getUrl, url).last("LIMIT 1"));
            if (menu != null) {
                ensureRoleMenu(adminRole.getId(), menu.getId());
                ensureTenantMenu(1L, menu.getId());
            }
        }
    }
    /**
     * Materializes the declarative button catalog as MENU_TYPE=2 rows.
     * Existing rows are left untouched so manual menu edits are preserved.
     */
    private void seedButtonMenus() {
        SysRole adminRole = roleMapper.selectOne(Wrappers.<SysRole>lambdaQuery()
                .eq(SysRole::getCode, "SYS_ADMIN").last("LIMIT 1"));
        for (Map.Entry<String, List<ButtonPermissionCatalog.ButtonDefinition>> entry : buttonPermissionCatalog.catalog().entrySet()) {
            SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                    .eq(SysMenu::getUrl, entry.getKey()).last("LIMIT 1"));
            if (menu == null || menu.getMenuType() == null || menu.getMenuType() != 1) {
                continue;
            }
            List<ButtonPermissionCatalog.ButtonDefinition> definitions = entry.getValue();
            for (int index = 0; index < definitions.size(); index++) {
                ButtonPermissionCatalog.ButtonDefinition definition = definitions.get(index);
                SysMenu existing = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                        .eq(SysMenu::getPid, menu.getId())
                        .eq(SysMenu::getMenuType, 2)
                        .eq(SysMenu::getPermissions, definition.getCode())
                        .last("LIMIT 1"));
                if (existing != null) {
                    continue;
                }
                SysMenu child = new SysMenu();
                child.setPid(menu.getId());
                child.setName(definition.getName());
                child.setPermissions(definition.getCode());
                child.setMenuType(2);
                child.setSortNo((index + 1) * 10);
                child.setVisible(1);
                child.setCreateTime(LocalDateTime.now());
                child.setUpdateTime(LocalDateTime.now());
                menuMapper.insert(child);
                if (adminRole != null) {
                    ensureRoleMenu(adminRole.getId(), child.getId());
                }
                ensureTenantMenu(1L, child.getId());
            }
        }
    }
    private void ensureRoleMenu(Long roleId, Long menuId) {
        if (roleId == null || menuId == null) {
            return;
        }
        long count = roleMenuMapper.selectCount(Wrappers.<SysRoleMenu>lambdaQuery()
                .eq(SysRoleMenu::getRoleId, roleId).eq(SysRoleMenu::getMenuId, menuId));
        if (count == 0) {
            SysRoleMenu relation = new SysRoleMenu();
            relation.setRoleId(roleId);
            relation.setMenuId(menuId);
            relation.setCreateTime(LocalDateTime.now());
            roleMenuMapper.insert(relation);
        }
    }
    private void ensureTenantMenu(Long tenantId, Long menuId) {
        if (tenantId == null || menuId == null) {
            return;
        }
        long count = tenantMenuMapper.selectCount(Wrappers.<SysTenantMenu>lambdaQuery()
                .eq(SysTenantMenu::getTenantId, tenantId).eq(SysTenantMenu::getMenuId, menuId));
        if (count == 0) {
            SysTenantMenu relation = new SysTenantMenu();
            relation.setTenantId(tenantId);
            relation.setMenuId(menuId);
            relation.setCreateTime(LocalDateTime.now());
            tenantMenuMapper.insert(relation);
        }
    }
    private void ensureAdminRoleAssignment() {
        SysRole adminRole = roleMapper.selectOne(Wrappers.<SysRole>lambdaQuery()
                .eq(SysRole::getCode, "SYS_ADMIN").last("LIMIT 1"));
        if (adminRole == null) {
            return;
        }
        List<Long> ids = jdbcTemplate.query(
                "SELECT USER_ID FROM CSICZB_ZB.SYS_USER WHERE USER_NAME = 'admin' AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
                (rs, rowNum) -> rs.getLong(1));
        if (!ids.isEmpty()) {
            Long userId = ids.get(0);
            long count = roleUserMapper.selectCount(Wrappers.<SysRoleUser>lambdaQuery()
                    .eq(SysRoleUser::getRoleId, adminRole.getId())
                    .eq(SysRoleUser::getUserId, userId));
            if (count == 0) {
                SysRoleUser relation = new SysRoleUser();
                relation.setRoleId(adminRole.getId());
                relation.setUserId(userId);
                relation.setCreateTime(LocalDateTime.now());
                roleUserMapper.insert(relation);
            }
        }
        Integer placeholder = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = 1", Integer.class);
        if (placeholder == null || placeholder == 0) {
            roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getUserId, 1L));
        }
    }
    /**
@@ -228,42 +361,26 @@
    }
    private void seedContractors() {
        if (contractorMapper.selectCount(null) > 0) {
            return;
        }
        String[][] rows = {
                {"北方机电修理厂", "北方机电", "厂家", "1", "å¼ å·¥", "0"},
                {"海城动力设备厂", "海城动力", "厂家", "2", "李工", "0"},
                {"联合电气维修中心", "联合电气", "厂家", "3", "王工", "0"},
                {"华远机械修造厂", "华远机械", "厂家", "4", "赵工", "0"},
                {"江海自动化服务部", "江海自动化", "服务商", "5", "周工", "0"},
                {"远航甲板设备公司", "远航甲板", "厂家", "6", "陈工", "0"},
                {"精工液压维修厂", "精工液压", "厂家", "7", "刘工", "0"},
                {"启明控制设备厂", "启明控制", "厂家", "8", "孙工", "0"},
                {"恒信管系工程部", "恒信管系", "服务商", "9", "吴工", "1"},
                {"新城综合维修中心", "新城维修", "综合单位", "10", "郑工", "0"}
        };
        for (String[] row : rows) {
            Contractor contractor = new Contractor();
            contractor.setName(row[0]);
            contractor.setShortName(row[1]);
            contractor.setNature(row[2]);
            contractor.setSortNo(Integer.valueOf(row[3]));
            contractor.setContact(row[4]);
            contractor.setDishonest(Integer.valueOf(row[5]));
            contractor.setCreateTime(LocalDateTime.now());
            contractor.setUpdateTime(LocalDateTime.now());
            contractorMapper.insert(contractor);
            contractorIds.add(contractor.getId());
        contractorIds.clear();
        contractorNames.clear();
        List<Map<String, Object>> rows = jdbcTemplate.query(
                "SELECT DEPT_ID, DEPT_NAME FROM CSICZB_ZB.SYS_DEPT " +
                        "WHERE DEPT_TYPE = 'cj' AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL) ORDER BY ORDER_NUM, DEPT_ID",
                (rs, rowNum) -> {
                    Map<String, Object> item = new LinkedHashMap<>();
                    item.put("id", rs.getLong("DEPT_ID"));
                    item.put("name", rs.getString("DEPT_NAME"));
                    return item;
                });
        for (Map<String, Object> item : rows) {
            Long id = ((Number) item.get("id")).longValue();
            contractorIds.add(id);
            contractorNames.put(id, String.valueOf(item.get("name")));
        }
    }
    private void seedTrees() {
        if (productNodeMapper.selectCount(null) == 0) {
            ProductNode model = product("LX-A型综合保障平台", "LX-A", null, 1, "MODEL", null, 1);
            ProductNode x01 = product("X01", "X01", model.getId(), 2, "X_NO", null, 1);
            ProductNode x02 = product("X02", "X02", model.getId(), 2, "X_NO", null, 2);
            ProductNode x03 = product("X03", "X03", model.getId(), 2, "X_NO", null, 3);
        if (baseEquipmentMapper.selectCount(null) == 0) {
            Long power = base("动力系统", "BASE-POWER", null, 1, 1);
            Long electric = base("电气系统", "BASE-ELECTRIC", null, 1, 2);
            Long deck = base("甲板系统", "BASE-DECK", null, 1, 3);
@@ -279,20 +396,6 @@
            Long controlDevice = base("集中控制柜", "DEV-CONTROL", control, 3, 1);
            Long hydraulicDevice = base("液压泵组", "DEV-HYD", hydraulic, 3, 1);
            Long compressorDevice = base("空气压缩机组", "DEV-AIR", auxiliary, 3, 1);
            addProductDevice(x01, "动力系统", "SYS-POWER", "推进子系统", "SUB-PROP", "主推进装置", "DEV-PROP", propulsionDevice, 1);
            addProductDevice(x01, "电气系统", "SYS-ELECTRIC", "电力子系统", "SUB-GRID", "主配电装置", "DEV-DIST", distributionDevice, 2);
            addProductDevice(x01, "甲板系统", "SYS-DECK", "液压子系统", "SUB-HYD", "液压泵组", "DEV-HYD", hydraulicDevice, 3);
            addProductDevice(x02, "动力系统", "SYS-POWER", "传动子系统", "SUB-TRANS", "减速齿轮箱", "DEV-GEAR", gearDevice, 1);
            addProductDevice(x02, "电气系统", "SYS-ELECTRIC", "控制子系统", "SUB-CTRL", "集中控制柜", "DEV-CONTROL", controlDevice, 2);
            addProductDevice(x02, "甲板系统", "SYS-DECK", "辅机子系统", "SUB-AUX", "空气压缩机组", "DEV-AIR", compressorDevice, 3);
            addProductDevice(x03, "动力系统", "SYS-POWER", "推进子系统", "SUB-PROP", "主推进装置", "DEV-PROP", propulsionDevice, 1);
            addProductDevice(x03, "电气系统", "SYS-ELECTRIC", "电力子系统", "SUB-GRID", "主配电装置", "DEV-DIST", distributionDevice, 2);
            addProductDevice(x03, "甲板系统", "SYS-DECK", "液压子系统", "SUB-HYD", "液压泵组", "DEV-HYD", hydraulicDevice, 3);
            addProductDevice(x01, "电气系统", "SYS-ELECTRIC", "控制子系统", "SUB-CTRL", "集中控制柜", "DEV-CONTROL", controlDevice, 4);
            addProductDevice(x02, "动力系统", "SYS-POWER", "推进子系统", "SUB-PROP", "主推进装置改进型", "DEV-PROP-X02", propulsionDevice, 4);
            addProductDevice(x03, "电气系统", "SYS-ELECTRIC", "控制子系统", "SUB-CTRL", "集中控制柜", "DEV-CONTROL", controlDevice, 4);
            addProductDevice(x03, "甲板系统", "SYS-DECK", "辅机子系统", "SUB-AUX", "空气压缩机组", "DEV-AIR", compressorDevice, 4);
        }
    }
@@ -336,8 +439,13 @@
        Fault fault = baseFault(code, xNo, equipment, team, nature, date, description);
        fault.setTaskStatus(status);
        if (!StatusConstants.Task.UNREPORTED.equals(status)) {
            List<Long> ids = Arrays.asList(contractorIds.get(Math.abs(code.hashCode()) % 7), contractorIds.get((Math.abs(code.hashCode()) + 1) % 7));
            String names = contractorMapper.selectBatchIds(ids).stream().map(Contractor::getName).collect(java.util.stream.Collectors.joining("、"));
            int size = contractorIds.size();
            List<Long> ids = Arrays.asList(
                    contractorIds.get(Math.abs(code.hashCode()) % size),
                    contractorIds.get((Math.abs(code.hashCode()) + 1) % size));
            String names = ids.stream().map(contractorNames::get)
                    .filter(java.util.Objects::nonNull)
                    .collect(java.util.stream.Collectors.joining("、"));
            fault.setContractorIds(ids.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(",")));
            fault.setContractorNames(names);
            fault.setEngineeringNo("LX-" + xNo + "-" + date.toString().replace("-", "").substring(2) + "-001");
@@ -350,9 +458,8 @@
        Fault fault = baseFault(code, xNo, equipment, team, nature, date, description);
        fault.setTaskStatus(StatusConstants.Task.JCC_DIRECTOR_APPROVED);
        Long contractorId = contractorIds.get(contractorIndex % contractorIds.size());
        Contractor contractor = contractorMapper.selectById(contractorId);
        fault.setContractorIds(String.valueOf(contractorId));
        fault.setContractorNames(contractor.getName());
        fault.setContractorNames(contractorNames.get(contractorId));
        fault.setEngineeringNo("LX-" + xNo + "-" + date.toString().replace("-", "").substring(2) + "-001");
        fault.setXcbReviewTime(LocalDateTime.now().minusDays(5));
        faultMapper.insert(fault);
@@ -460,45 +567,7 @@
        }
    }
    private void ensureDemoProductDevices() {
        loadBaseIds();
        ensureProductDevice("X01", "电气系统", "SYS-ELECTRIC", "控制子系统", "SUB-CTRL", "集中控制柜", "DEV-CONTROL", "DEV-CONTROL", 4);
        ensureProductDevice("X02", "动力系统", "SYS-POWER", "推进子系统", "SUB-PROP", "主推进装置改进型", "DEV-PROP-X02", "DEV-PROP", 4);
        ensureProductDevice("X03", "电气系统", "SYS-ELECTRIC", "控制子系统", "SUB-CTRL", "集中控制柜", "DEV-CONTROL", "DEV-CONTROL", 4);
        ensureProductDevice("X03", "甲板系统", "SYS-DECK", "辅机子系统", "SUB-AUX", "空气压缩机组", "DEV-AIR", "DEV-AIR", 4);
    }
    private void ensureProductDevice(String xNo, String system1, String code1, String system2, String code2,
                                     String device, String deviceCode, String baseCode, int sort) {
        if (productDeviceIds.containsKey(deviceKey(xNo, device))) {
            return;
        }
        ProductNode xNode = productNode(2, xNo);
        Long baseId = baseIds.get(baseCode);
        if (xNode == null || baseId == null) {
            return;
        }
        addProductDevice(xNode, system1, code1, system2, code2, device, deviceCode, baseId, sort);
    }
    private void loadBaseIds() {
        if (!baseIds.isEmpty()) {
            return;
        }
        for (BaseEquipment node : baseEquipmentMapper.selectList(null)) {
            if (StringUtils.hasText(node.getCode())) {
                baseIds.putIfAbsent(node.getCode(), node.getId());
            }
        }
    }
    private ProductNode productNode(int level, String name) {
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .eq(ProductNode::getNodeLevel, level)
                .eq(ProductNode::getName, name)
                .last("LIMIT 1"));
        return nodes.isEmpty() ? null : nodes.get(0);
    }
    private void backfillFaultXNoFromEquipment() {
        List<ProductNode> nodes = productNodeMapper.selectList(null);
@@ -607,6 +676,20 @@
        }
    }
    private void normalizeSensitiveTerms() {
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, 'XCB', 'rk_XCB') WHERE NAME LIKE '%XCB%' AND NAME NOT LIKE '%rk_XCB%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, 'CMB', 'rk_CMB') WHERE NAME LIKE '%CMB%' AND NAME NOT LIKE '%rk_CMB%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, 'JCC', 'rk_JCC') WHERE NAME LIKE '%JCC%' AND NAME NOT LIKE '%rk_JCC%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, 'T队', 'rk_T队') WHERE NAME LIKE '%T队%' AND NAME NOT LIKE '%rk_T队%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, 'T员', 'rk_T员') WHERE NAME LIKE '%T员%' AND NAME NOT LIKE '%rk_T员%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, '按T统计', 'rk_按T统计') WHERE NAME LIKE '%按T统计%' AND NAME NOT LIKE '%rk_按T统计%'");
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, '涉H', 'rk_涉H') WHERE NAME LIKE '%涉H%' AND NAME NOT LIKE '%rk_涉H%'");
    }
    private void normalizeEquipmentTerms() {
        jdbcTemplate.update("UPDATE LX.T_SYS_MENU SET NAME = REPLACE(NAME, '\u88c5\u5907', '\u8bbe\u5907') WHERE NAME LIKE '%\u88c5\u5907%'");
    }
    private void normalizeParentSystemSeparators() {
        for (Fault fault : faultMapper.selectList(null)) {
            String normalized = normalizeParentSystem(fault.getParentSystem());
@@ -656,9 +739,9 @@
        fault.setParentSystem(equipment.contains("液压") ? "甲板系统" : equipment.contains("控制") ? "电气系统" : "动力系统");
        fault.setEngineeringMajor(team);
        fault.setTeamMajor(team);
        fault.setReportUnit(team + "T队");
        fault.setReportUnit(team + "rk_T队");
        fault.setReportDate(date);
        fault.setOwner("T员" + xNo);
        fault.setOwner("rk_T员" + xNo);
        fault.setFaultImpact("供气".equals(team) ? "供气" : "运行");
        fault.setFaultNature(nature);
        fault.setEngineeringNameDesc(description);
@@ -747,29 +830,6 @@
        node.setCreateTime(LocalDateTime.now());
        node.setUpdateTime(LocalDateTime.now());
        baseEquipmentMapper.insert(node);
        baseIds.put(code, node.getId());
        return node.getId();
    }
    private ProductNode product(String name, String code, Long parentId, int level, String type, Long baseEquipmentId, int sort) {
        ProductNode node = new ProductNode();
        node.setParentId(parentId == null ? 0L : parentId);
        node.setNodeLevel(level);
        node.setNodeType(type);
        node.setName(name);
        node.setCode(code);
        node.setBaseEquipmentId(baseEquipmentId);
        node.setSortNo(sort);
        node.setCreateTime(LocalDateTime.now());
        node.setUpdateTime(LocalDateTime.now());
        productNodeMapper.insert(node);
        return node;
    }
    private void addProductDevice(ProductNode xNode, String system1, String code1, String system2, String code2,
                                  String device, String code, Long baseId, int sort) {
        ProductNode node1 = product(system1, code1 + "-" + xNode.getCode(), xNode.getId(), 3, "SYSTEM1", null, sort);
        ProductNode node2 = product(system2, code2 + "-" + xNode.getCode(), node1.getId(), 4, "SYSTEM2", null, 1);
        product(device, code + "-" + xNode.getCode(), node2.getId(), 5, "DEVICE", baseId, 1);
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java
@@ -13,6 +13,12 @@
@Order(1)
@RequiredArgsConstructor
public class DatabaseBootstrap implements CommandLineRunner {
    private static final String[] AUDIT_TABLES = {
            "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"
    };
    private final JdbcTemplate jdbcTemplate;
    @Override
@@ -23,6 +29,7 @@
                jdbcTemplate.execute(entry.getValue());
            }
        }
        ensureAuditColumns();
        renameColumnIfNeeded("T_FAULT", "FAULT_DESCRIPTION", "ENGINEERING_NAME_DESC", "ALTER TABLE LX.T_FAULT RENAME COLUMN FAULT_DESCRIPTION TO ENGINEERING_NAME_DESC");
        ensureColumn("T_FAULT", "EQUIPMENT_IDS", "ALTER TABLE LX.T_FAULT ADD EQUIPMENT_IDS VARCHAR(2000)");
        ensureColumn("T_COMPLETION_INFO", "CONSUMABLE_COUNT", "ALTER TABLE LX.T_COMPLETION_INFO ADD CONSUMABLE_COUNT INT");
@@ -31,8 +38,22 @@
        ensureIndex("UX_T_WORK_ORDER_NO", "CREATE UNIQUE INDEX LX.UX_T_WORK_ORDER_NO ON LX.T_WORK_ORDER(WORK_ORDER_NO)");
        ensureIndex("IX_T_WORK_ORDER_FAULT_ID", "CREATE INDEX LX.IX_T_WORK_ORDER_FAULT_ID ON LX.T_WORK_ORDER(FAULT_ID)");
        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_ROLE_USER_USER", "CREATE INDEX LX.IX_T_SYS_ROLE_USER_USER ON LX.T_SYS_ROLE_USER(USER_ID)");
    }
    private void ensureAuditColumns() {
        for (String tableName : AUDIT_TABLES) {
            if (!tableExists(tableName)) {
                continue;
            }
            ensureColumn(tableName, "CREATE_BY", "ALTER TABLE LX." + tableName + " ADD CREATE_BY VARCHAR(120)");
            ensureColumn(tableName, "UPDATE_BY", "ALTER TABLE LX." + tableName + " ADD UPDATE_BY VARCHAR(120)");
            ensureColumn(tableName, "DEL_FLAG", "ALTER TABLE LX." + tableName + " ADD DEL_FLAG INT DEFAULT 0");
            jdbcTemplate.update("UPDATE LX." + tableName + " SET DEL_FLAG = 0 WHERE DEL_FLAG IS NULL");
        }
    }
    private void ensureSchema() {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM ALL_USERS WHERE USERNAME = ?", Integer.class, "LX");
@@ -99,6 +120,8 @@
        ddl.put("T_SYS_ROLE_MENU", "CREATE TABLE LX.T_SYS_ROLE_MENU (ID BIGINT NOT NULL PRIMARY KEY, ROLE_ID BIGINT, MENU_ID BIGINT, CREATE_TIME TIMESTAMP)");
        ddl.put("T_SYS_TENANT_MENU", "CREATE TABLE LX.T_SYS_TENANT_MENU (ID BIGINT NOT NULL PRIMARY KEY, TENANT_ID BIGINT, MENU_ID BIGINT, CREATE_TIME TIMESTAMP)");
        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)");
        return ddl;
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java
@@ -24,8 +24,9 @@
    @GetMapping
    public Result<List<WorkOrder>> list(@RequestParam(required = false) String status,
                                        @RequestParam(required = false) String keyword) {
        return Result.ok(businessService.listWorkOrders(status, keyword));
                                        @RequestParam(required = false) String keyword,
                                        @RequestParam(required = false, defaultValue = "false") boolean completionReportable) {
        return Result.ok(businessService.listWorkOrders(status, keyword, completionReportable));
    }
    @GetMapping("/{id}")
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/Requests.java
@@ -112,4 +112,26 @@
        private List<Long> roleMenuIds = new ArrayList<>();
        private List<Long> tenantMenuIds = new ArrayList<>();
    }
    @Data
    public static class UserRoleRequest {
        private List<Long> roleIds = new ArrayList<>();
    }
    @Data
    public static class BatchUserRoleRequest {
        private List<Long> userIds = new ArrayList<>();
        private List<Long> roleIds = new ArrayList<>();
        private String mode = "append";
    }
    @Data
    public static class RoleUserRequest {
        private List<Long> userIds = new ArrayList<>();
    }
    @Data
    public static class ButtonSyncRequest {
        private Long menuId;
        private List<String> codes = new ArrayList<>();
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Acceptance.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -13,7 +14,7 @@
@Data
@TableName("LX.T_ACCEPTANCE")
public class Acceptance {
public class Acceptance extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/BaseEquipment.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_BASE_EQUIPMENT")
public class BaseEquipment {
public class BaseEquipment extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/CompletionInfo.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -14,7 +15,7 @@
@Data
@TableName("LX.T_COMPLETION_INFO")
public class CompletionInfo {
public class CompletionInfo extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Contractor.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_CONTRACTOR")
public class Contractor {
public class Contractor extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/EngineeringMajor.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_ENGINEERING_MAJOR")
public class EngineeringMajor {
public class EngineeringMajor extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/Fault.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -14,7 +15,7 @@
@Data
@TableName("LX.T_FAULT")
public class Fault {
public class Fault extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/ProductNode.java
@@ -9,23 +9,182 @@
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Locale;
@Data
@TableName("LX.T_PRODUCT_TREE")
@TableName("LX.ZT_BOAT_TREE")
public class ProductNode {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    /**
     * Business node id. The external tree uses INFO_ID for the node and
     * CONNECT_ID for its parent, while ID is the physical relation row id.
     */
    @TableId(value = "INFO_ID", type = IdType.INPUT)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    @TableField("ID")
    private String recordId;
    @JsonSerialize(using = ToStringSerializer.class)
    @TableField("PARENT_ID") private Long parentId;
    @TableField("NODE_LEVEL") private Integer nodeLevel;
    @TableField("NODE_TYPE") private String nodeType;
    @TableField("NAME") private String name;
    @TableField("CODE") private String code;
    @JsonSerialize(using = ToStringSerializer.class)
    @TableField("BASE_EQUIPMENT_ID") private Long baseEquipmentId;
    @TableField("SORT_NO") private Integer sortNo;
    @TableField("REMARK") private String remark;
    @TableField("CREATE_TIME") private LocalDateTime createTime;
    @TableField("UPDATE_TIME") private LocalDateTime updateTime;
    @TableField("CONNECT_ID")
    private Long parentId;
    @TableField("CONNECT_GROUP")
    private String connectGroup;
    @TableField("LABEL")
    private String name;
    @TableField("LAYER")
    private String layer;
    @TableField("PROJECT")
    private String project;
    @TableField("SORT")
    private Integer sortNo;
    @TableField("ISDELETE")
    private String isDelete;
    @TableField("CREATE_BY")
    private String createBy;
    @TableField("CREATE_TIME")
    private LocalDateTime createTime;
    @TableField(exist = false)
    private Integer nodeLevel;
    @TableField(exist = false)
    private String nodeType;
    @TableField(exist = false)
    private String code;
    @TableField(exist = false)
    private Long baseEquipmentId;
    @TableField(exist = false)
    private String remark;
    @TableField(exist = false)
    private LocalDateTime updateTime;
    public Integer getNodeLevel() {
        if (nodeLevel != null) {
            return nodeLevel;
        }
        return levelForLayer(layer);
    }
    public void setNodeLevel(Integer nodeLevel) {
        this.nodeLevel = nodeLevel;
        if (layer == null && nodeLevel != null) {
            layer = layerForLevel(nodeLevel);
        }
    }
    public String getNodeType() {
        if (nodeType != null) {
            return nodeType;
        }
        return typeForLayer(layer);
    }
    public void setNodeType(String nodeType) {
        this.nodeType = nodeType;
        if (layer == null && nodeType != null) {
            layer = layerForType(nodeType);
        }
    }
    public static Integer levelForLayer(String value) {
        String layer = normalize(value);
        if ("model".equals(layer)) {
            return 1;
        }
        if ("side".equals(layer)) {
            return 2;
        }
        if ("system1".equals(layer)) {
            return 3;
        }
        if ("system2".equals(layer)) {
            return 4;
        }
        if ("equipment".equals(layer)) {
            return 5;
        }
        return layer.isEmpty() ? null : 6;
    }
    public static String typeForLayer(String value) {
        Integer level = levelForLayer(value);
        if (level == null) {
            return null;
        }
        if (level == 1) {
            return "MODEL";
        }
        if (level == 2) {
            return "X_NO";
        }
        if (level == 3) {
            return "SYSTEM1";
        }
        if (level == 4) {
            return "SYSTEM2";
        }
        if (level == 5) {
            return "DEVICE";
        }
        return normalize(value).toUpperCase(Locale.ROOT);
    }
    public static String layerForLevel(Integer level) {
        if (level == null) {
            return null;
        }
        if (level == 1) {
            return "model";
        }
        if (level == 2) {
            return "side";
        }
        if (level == 3) {
            return "system1";
        }
        if (level == 4) {
            return "system2";
        }
        if (level == 5) {
            return "equipment";
        }
        return null;
    }
    public static String layerForType(String type) {
        String value = normalize(type);
        if ("MODEL".equals(value)) {
            return "model";
        }
        if ("X_NO".equals(value) || "XNUMBER".equals(value)) {
            return "side";
        }
        if ("SYSTEM1".equals(value)) {
            return "system1";
        }
        if ("SYSTEM2".equals(value)) {
            return "system2";
        }
        if ("DEVICE".equals(value) || "EQUIPMENT".equals(value)) {
            return "equipment";
        }
        return value.isEmpty() ? null : value.toLowerCase(Locale.ROOT);
    }
    private static String normalize(String value) {
        return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/TeamMajor.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_TEAM_MAJOR")
public class TeamMajor {
public class TeamMajor extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/WorkOrder.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.business.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -14,7 +15,7 @@
@Data
@TableName("LX.T_WORK_ORDER")
public class WorkOrder {
public class WorkOrder extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
@@ -6,20 +6,21 @@
import com.emergencyrepair.business.dto.Requests;
import com.emergencyrepair.business.entity.Acceptance;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.Contractor;
import com.emergencyrepair.business.entity.Fault;
import com.emergencyrepair.business.entity.TeamMajor;
import com.emergencyrepair.business.entity.WorkOrder;
import com.emergencyrepair.business.mapper.AcceptanceMapper;
import com.emergencyrepair.business.mapper.CompletionInfoMapper;
import com.emergencyrepair.business.mapper.ContractorMapper;
import com.emergencyrepair.business.mapper.FaultMapper;
import com.emergencyrepair.business.mapper.TeamMajorMapper;
import com.emergencyrepair.business.mapper.WorkOrderMapper;
import com.emergencyrepair.common.BusinessException;
import com.emergencyrepair.common.StatusConstants;
import com.emergencyrepair.common.audit.CurrentUserContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -30,24 +31,26 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Collection;
import java.util.Objects;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class BusinessService {
    private static final DateTimeFormatter FAULT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
    private static final String FAULT_CODE_PREFIX = "\u6f5c1\u57fa-";
    private static final Object FAULT_CODE_LOCK = new Object();
    @Value("${fault-code.prefix:\u6f5c1\u57fa-}")
    private String faultCodePrefix;
    private final FaultMapper faultMapper;
    private final JdbcTemplate jdbcTemplate;
    private final TeamMajorMapper teamMajorMapper;
    private final WorkOrderMapper workOrderMapper;
    private final ContractorMapper contractorMapper;
    private final CompletionInfoMapper completionInfoMapper;
    private final AcceptanceMapper acceptanceMapper;
    private final ObjectMapper objectMapper;
@@ -168,26 +171,30 @@
    private String generateFaultCode(Fault fault) {
        String xNo = normalizeXNo(fault.getXNo());
        if (xNo == null) {
            throw new BusinessException("请选择X号,格式为3位数字");
            throw new BusinessException("请选择rk_X号,格式为3位数字");
        }
        if (!StringUtils.hasText(fault.getTeamMajor())) {
            throw new BusinessException("请选择T队专业");
            throw new BusinessException("请选择rk_T队专业");
        }
        TeamMajor teamMajor = teamMajorMapper.selectOne(Wrappers.<TeamMajor>lambdaQuery()
                .eq(TeamMajor::getName, fault.getTeamMajor().trim())
                .last("LIMIT 1"));
        if (teamMajor == null) {
            throw new BusinessException("T队专业未配置,请先在系统管理 > T队专业配置中维护:" + fault.getTeamMajor());
            throw new BusinessException("rk_T队专业未配置,请先在系统管理 > rk_T队专业配置中维护:" + fault.getTeamMajor());
        }
        String category = teamMajor.getCategory() == null ? "" : teamMajor.getCategory().trim().toUpperCase();
        if (!"A".equals(category) && !"B".equals(category)) {
            throw new BusinessException("T队专业分类只能为A或B:" + fault.getTeamMajor());
            throw new BusinessException("rk_T队专业分类只能为A或B:" + fault.getTeamMajor());
        }
        String prefix = FAULT_CODE_PREFIX + xNo + "/" + category + "-" + fault.getReportDate().format(FAULT_DATE_FORMAT) + "-";
        String prefix = faultCodePrefix + xNo + "/" + category + "-" + fault.getReportDate().format(FAULT_DATE_FORMAT) + "-";
        int next = 1;
        for (Fault item : faultMapper.selectList(Wrappers.<Fault>lambdaQuery()
                .likeRight(Fault::getFaultCode, prefix))) {
            next = Math.max(next, parseFaultSequence(item.getFaultCode(), prefix) + 1);
        // Include logically deleted rows so a generated code never collides with
        // the unique index on FAULT_CODE.
        List<String> existingCodes = jdbcTemplate.query(
                "SELECT FAULT_CODE FROM LX.T_FAULT WHERE FAULT_CODE LIKE ?",
                new Object[]{prefix + "%"}, (rs, rowNum) -> rs.getString(1));
        for (String existingCode : existingCodes) {
            next = Math.max(next, parseFaultSequence(existingCode, prefix) + 1);
        }
        return prefix + String.format("%03d", next);
    }
@@ -244,7 +251,7 @@
            throw new BusinessException("请填写销项理由");
        }
        String cancelledBy = StringUtils.hasText(request.getCancelledBy())
                ? request.getCancelledBy().trim() : "XCB助理";
                ? request.getCancelledBy().trim() : "rk_XCB助理";
        for (Long id : request.getIds()) {
            Fault fault = requireFault(id);
            if (!StatusConstants.Task.REPORTED_XCB.equals(fault.getTaskStatus())
@@ -281,16 +288,16 @@
        if (request.getContractorIds() == null || request.getContractorIds().isEmpty()) {
            throw new BusinessException("至少分配一个承修单位");
        }
        List<Contractor> contractors = contractorMapper.selectBatchIds(request.getContractorIds());
        Map<Long, Contractor> contractorMap = contractors.stream()
                .collect(Collectors.toMap(Contractor::getId, Function.identity()));
        Map<Long, String> contractorMap = contractorNamesByIds(request.getContractorIds());
        if (contractorMap.isEmpty()) {
            throw new BusinessException("承修单位不存在或未在部门表中配置");
        }
        String contractorIds = request.getContractorIds().stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        String contractorNames = request.getContractorIds().stream()
                .map(contractorMap::get)
                .filter(item -> item != null)
                .map(Contractor::getName)
                .filter(Objects::nonNull)
                .collect(Collectors.joining("、"));
        int index = 0;
@@ -298,7 +305,7 @@
            Fault fault = requireFault(id);
            if (!StatusConstants.Task.REPORTED_XCB.equals(fault.getTaskStatus())
                    && !StatusConstants.Task.RETURNED.equals(fault.getTaskStatus())) {
                throw new BusinessException("故障 " + fault.getFaultCode() + " å½“前状态不允许XCB助理审核");
                throw new BusinessException("故障 " + fault.getFaultCode() + " å½“前状态不允许rk_XCB助理审核");
            }
            index++;
            fault.setContractorIds(contractorIds);
@@ -325,21 +332,21 @@
            LocalDateTime now = LocalDateTime.now();
            switch (request.getStage()) {
                case "CMB":
                    requireStatus(fault, StatusConstants.Task.XCB_REVIEWED, "CMB领导审批");
                    requireStatus(fault, StatusConstants.Task.XCB_REVIEWED, "rk_CMB领导审批");
                    fault.setCmbOpinion(request.getOpinion());
                    fault.setCmbResult(pass ? "通过" : "不通过");
                    fault.setCmbTime(now);
                    fault.setTaskStatus(pass ? StatusConstants.Task.CMB_APPROVED : StatusConstants.Task.RETURNED);
                    break;
                case "JCC_ASSISTANT":
                    requireStatus(fault, StatusConstants.Task.CMB_APPROVED, "JCC助理审批");
                    requireStatus(fault, StatusConstants.Task.CMB_APPROVED, "rk_JCC助理审批");
                    fault.setJccAssistantOpinion(request.getOpinion());
                    fault.setJccAssistantResult(pass ? "通过" : "不通过");
                    fault.setJccAssistantTime(now);
                    fault.setTaskStatus(pass ? StatusConstants.Task.JCC_ASSISTANT_APPROVED : StatusConstants.Task.RETURNED);
                    break;
                case "JCC_DIRECTOR":
                    requireStatus(fault, StatusConstants.Task.JCC_ASSISTANT_APPROVED, "JCC处长审批");
                    requireStatus(fault, StatusConstants.Task.JCC_ASSISTANT_APPROVED, "rk_JCC处长审批");
                    fault.setJccDirectorOpinion(request.getOpinion());
                    fault.setJccDirectorResult(pass ? "通过" : "不通过");
                    fault.setJccDirectorTime(now);
@@ -373,19 +380,17 @@
            if (exists > 0) {
                throw new BusinessException("故障 " + fault.getFaultCode() + " å·²ç»æ´¾å•");
            }
            List<Contractor> contractors = contractorMapper.selectBatchIds(contractorIds);
            Map<Long, Contractor> contractorMap = contractors.stream()
                    .collect(Collectors.toMap(Contractor::getId, Function.identity()));
            Map<Long, String> contractorMap = contractorNamesByIds(contractorIds);
            int sequence = 1;
            for (Long contractorId : contractorIds) {
                Contractor contractor = contractorMap.get(contractorId);
                if (contractor == null) {
                String contractorName = contractorMap.get(contractorId);
                if (contractorName == null) {
                    continue;
                }
                WorkOrder workOrder = workOrderFromFault(fault);
                workOrder.setWorkOrderNo(fault.getFaultCode() + "/" + String.format("%02d", sequence++));
                workOrder.setContractorId(contractor.getId());
                workOrder.setContractorName(contractor.getName());
                workOrder.setContractorId(contractorId);
                workOrder.setContractorName(contractorName);
                workOrder.setCompletionStatus(StatusConstants.Completion.UNFINISHED);
                workOrder.setDispatchTime(LocalDateTime.now());
                workOrder.setCreateTime(LocalDateTime.now());
@@ -401,9 +406,19 @@
        return created;
    }
    public List<WorkOrder> listWorkOrders(String status, String keyword) {
    public List<WorkOrder> listWorkOrders(String status, String keyword, boolean completionReportable) {
        LambdaQueryWrapper<WorkOrder> query = Wrappers.lambdaQuery();
        if (StringUtils.hasText(status)) {
        if (completionReportable) {
            query.eq(WorkOrder::getCompletionStatus, StatusConstants.Completion.COMPLETED)
                    .notInSql(WorkOrder::getId, "SELECT WORK_ORDER_ID FROM LX.T_COMPLETION_INFO WHERE DEL_FLAG = 0 OR DEL_FLAG IS NULL");
            List<String> unitNames = currentUserContractorNames();
            if (unitNames != null) {
                if (unitNames.isEmpty()) {
                    return Collections.emptyList();
                }
                query.in(WorkOrder::getContractorName, unitNames);
            }
        } else if (StringUtils.hasText(status)) {
            query.in(WorkOrder::getCompletionStatus, Arrays.asList(status.split(",")));
        }
        if (StringUtils.hasText(keyword)) {
@@ -417,6 +432,54 @@
        return workOrderMapper.selectList(query);
    }
    /**
     * Returns null when the current user sees every unit (system administrator).
     * Otherwise returns the contractor names matching the logged-in user's department.
     */
    private List<String> currentUserContractorNames() {
        String username = CurrentUserContext.get();
        if (!StringUtils.hasText(username) || "admin".equalsIgnoreCase(username)) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.query(
                "SELECT d.DEPT_NAME, d.DEPT_TYPE FROM CSICZB_ZB.SYS_USER u "
                        + "LEFT JOIN CSICZB_ZB.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID "
                        + "WHERE u.USER_NAME = ? AND (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL)",
                new Object[]{username}, (rs, rowNum) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("deptName", rs.getString("DEPT_NAME"));
                    row.put("deptType", rs.getString("DEPT_TYPE"));
                    return row;
                });
        if (rows.isEmpty() || !StringUtils.hasText((String) rows.get(0).get("deptName"))) {
            return Collections.emptyList();
        }
        Map<String, Object> dept = rows.get(0);
        if (!"cj".equalsIgnoreCase(String.valueOf(dept.get("deptType")))) {
            return Collections.emptyList();
        }
        return Collections.singletonList(((String) dept.get("deptName")).trim());
    }
    private Map<Long, String> contractorNamesByIds(Collection<Long> ids) {
        Map<Long, String> result = new LinkedHashMap<>();
        if (ids == null || ids.isEmpty()) {
            return result;
        }
        String placeholders = ids.stream().map(item -> "?").collect(Collectors.joining(","));
        List<Object> args = new ArrayList<>(ids);
        return jdbcTemplate.query(
                "SELECT DEPT_ID, DEPT_NAME FROM CSICZB_ZB.SYS_DEPT "
                        + "WHERE DEPT_TYPE = 'cj' AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL) "
                        + "AND DEPT_ID IN (" + placeholders + ")",
                args.toArray(), rs -> {
                    while (rs.next()) {
                        result.put(rs.getLong("DEPT_ID"), rs.getString("DEPT_NAME"));
                    }
                    return result;
                });
    }
    public WorkOrder getWorkOrder(Long id) {
        return workOrderMapper.selectById(id);
    }
@@ -424,13 +487,15 @@
    @Transactional
    public void updateCompletionStatus(Requests.StatusRequest request) {
        WorkOrder workOrder = requireWorkOrder(request.getId());
        if (StatusConstants.Completion.COMPLETED.equals(workOrder.getCompletionStatus())) {
        if (StatusConstants.Completion.COMPLETED.equals(workOrder.getCompletionStatus())
                && !StatusConstants.Completion.COMPLETED.equals(request.getStatus())) {
            throw new BusinessException("已完工派工单不能退回普通完工状态");
        }
        if (!Arrays.asList(
                StatusConstants.Completion.UNFINISHED,
                StatusConstants.Completion.WAIT_PARTS,
                StatusConstants.Completion.WAIT_TEST).contains(request.getStatus())) {
                StatusConstants.Completion.WAIT_TEST,
                StatusConstants.Completion.COMPLETED).contains(request.getStatus())) {
            throw new BusinessException("完工状态不合法");
        }
        workOrder.setCompletionStatus(request.getStatus());
@@ -453,21 +518,22 @@
    @Transactional
    public CompletionInfo submitCompletion(Requests.CompletionRequest request) {
        WorkOrder workOrder = requireWorkOrder(request.getWorkOrderId());
        if (StatusConstants.Completion.COMPLETED.equals(workOrder.getCompletionStatus())) {
        CompletionInfo info = completionInfoMapper.selectOne(Wrappers.<CompletionInfo>lambdaQuery()
                .eq(CompletionInfo::getWorkOrderId, workOrder.getId()).last("LIMIT 1"));
        if (info != null) {
            throw new BusinessException("该派工单已完工上报,不能重复提交");
        }
        if (!StatusConstants.Completion.COMPLETED.equals(workOrder.getCompletionStatus())) {
            throw new BusinessException("该派工单尚未在完工情况处理中确认完工,不能填报完工信息");
        }
        LocalDateTime now = LocalDateTime.now();
        workOrder.setCompletionStatus(StatusConstants.Completion.COMPLETED);
        workOrder.setUpdateTime(now);
        workOrderMapper.updateById(workOrder);
        CompletionInfo info = completionInfoMapper.selectOne(Wrappers.<CompletionInfo>lambdaQuery()
                .eq(CompletionInfo::getWorkOrderId, workOrder.getId()).last("LIMIT 1"));
        if (info == null) {
            info = new CompletionInfo();
            info.setWorkOrderId(workOrder.getId());
            info.setCreateTime(now);
        }
        info.setWorkOrderNo(workOrder.getWorkOrderNo());
        info.setXNo(workOrder.getXNo());
        info.setContractorName(workOrder.getContractorName());
@@ -713,7 +779,7 @@
            ids.add(fault.getEquipmentId());
        }
        if (ids.isEmpty()) {
            throw new BusinessException("请选择装备名称");
            throw new BusinessException("请选择设备名称");
        }
        List<Long> normalized = new ArrayList<>(ids);
        fault.setEquipmentIds(normalized.stream().map(String::valueOf).collect(Collectors.joining(",")));
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/ReportService.java
@@ -24,7 +24,7 @@
        report.setChart(Arrays.asList(
                metric("工程总数", report.getEngineeringTotal()),
                metric("重大故障", report.getMajorFault()),
                metric("涉H统计", report.getHRelated()),
                metric("rk_涉H统计", report.getHRelated()),
                metric("监测项目", report.getMonitorItem()),
                metric("未完工", report.getUnfinished()),
                metric("已完工", report.getCompleted()),
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/AuditMetaObjectHandler.java
New file
@@ -0,0 +1,24 @@
package com.emergencyrepair.common.audit;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
/**
 * Populates create/update actor and logical-delete state automatically.
 */
@Component
public class AuditMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        String username = CurrentUserContext.get();
        strictInsertFill(metaObject, "createBy", String.class, username);
        strictInsertFill(metaObject, "updateBy", String.class, username);
        strictInsertFill(metaObject, "delFlag", Integer.class, 0);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        strictUpdateFill(metaObject, "updateBy", String.class, CurrentUserContext.get());
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/CurrentUserContext.java
New file
@@ -0,0 +1,24 @@
package com.emergencyrepair.common.audit;
/**
 * Carries the request user through the current request thread for MyBatis fills.
 */
public final class CurrentUserContext {
    private static final ThreadLocal<String> USERNAME = new ThreadLocal<>();
    private CurrentUserContext() {
    }
    public static void set(String username) {
        USERNAME.set(username);
    }
    public static String get() {
        String username = USERNAME.get();
        return username == null || username.trim().isEmpty() ? "admin" : username.trim();
    }
    public static void clear() {
        USERNAME.remove();
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/common/audit/CurrentUserInterceptor.java
New file
@@ -0,0 +1,42 @@
package com.emergencyrepair.common.audit;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CurrentUserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        CurrentUserContext.set(resolveUsername(request));
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception exception) {
        CurrentUserContext.clear();
    }
    private String resolveUsername(HttpServletRequest request) {
        String username = request.getHeader("X-Username");
        if (StringUtils.hasText(username)) {
            return username.trim();
        }
        String authorization = request.getHeader("Authorization");
        if (!StringUtils.hasText(authorization)) {
            return "admin";
        }
        String token = authorization.trim();
        if (token.startsWith("Bearer ")) {
            token = token.substring(7).trim();
        }
        if (token.startsWith("demo-") && token.endsWith("-token")) {
            token = token.substring(5, token.length() - 6);
        }
        return StringUtils.hasText(token) ? token : "admin";
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/common/entity/BaseAuditEntity.java
New file
@@ -0,0 +1,22 @@
package com.emergencyrepair.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
/**
 * Shared audit and logical-delete fields for application-owned tables.
 */
@Data
public abstract class BaseAuditEntity {
    @TableField(value = "CREATE_BY", fill = FieldFill.INSERT)
    private String createBy;
    @TableField(value = "UPDATE_BY", fill = FieldFill.INSERT_UPDATE)
    private String updateBy;
    @TableLogic(value = "0", delval = "1")
    @TableField("DEL_FLAG")
    private Integer delFlag;
}
emergency-repair-server/src/main/java/com/emergencyrepair/config/WebConfig.java
@@ -1,11 +1,25 @@
package com.emergencyrepair.config;
import com.emergencyrepair.common.audit.CurrentUserInterceptor;
import org.springframework.context.annotation.Configuration;
import com.emergencyrepair.system.log.OperationLogInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final CurrentUserInterceptor currentUserInterceptor;
    private final OperationLogInterceptor operationLogInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(currentUserInterceptor).addPathPatterns("/api/**").order(0);
        registry.addInterceptor(operationLogInterceptor).addPathPatterns("/api/**");
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java
@@ -1,13 +1,15 @@
package com.emergencyrepair.system.controller;
import com.emergencyrepair.business.dto.Requests;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.emergencyrepair.business.entity.BaseEquipment;
import com.emergencyrepair.business.entity.Contractor;
import com.emergencyrepair.business.entity.EngineeringMajor;
import com.emergencyrepair.business.entity.ProductNode;
import com.emergencyrepair.business.entity.TeamMajor;
import com.emergencyrepair.common.Result;
import com.emergencyrepair.system.entity.SysMenu;
import com.emergencyrepair.system.entity.SysLoginLog;
import com.emergencyrepair.system.entity.SysOperationLog;
import com.emergencyrepair.system.entity.SysRole;
import com.emergencyrepair.system.service.SystemService;
import lombok.RequiredArgsConstructor;
@@ -19,6 +21,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@@ -31,13 +34,81 @@
    private final SystemService systemService;
    @GetMapping("/menu/nav")
    public Result<List<SysMenu>> nav() {
        return Result.ok(systemService.menuTree());
    public Result<List<SysMenu>> nav(@RequestHeader(value = "X-Username", required = false) String username) {
        return Result.ok(systemService.menuTreeForUser(username));
    }
    @GetMapping("/menu/list")
    public Result<List<SysMenu>> menuList() {
        return Result.ok(systemService.listMenus());
    }
    @GetMapping("/menu/tree")
    public Result<List<SysMenu>> menuTreeWithButtons() {
        return Result.ok(systemService.menuTreeWithButtons());
    }
    @GetMapping("/menu/buttons")
    public Result<Map<String, Object>> menuButtons(@RequestParam Long menuId) {
        return Result.ok(systemService.menuButtons(menuId));
    }
    @PostMapping("/menu/buttons")
    public Result<Void> syncMenuButtons(@RequestBody Requests.ButtonSyncRequest request) {
        systemService.syncMenuButtons(request.getMenuId(), request.getCodes());
        return Result.ok();
    }
    @GetMapping("/users")
    public Result<Map<String, Object>> users(@RequestParam(required = false) String keyword,
                                            @RequestParam(required = false) Long deptId,
                                            @RequestParam(required = false) Long roleId,
                                            @RequestParam(defaultValue = "1") int current,
                                            @RequestParam(defaultValue = "20") int size) {
        return Result.ok(systemService.userPage(keyword, deptId, roleId, current, size));
    }
    @GetMapping("/users/{userId}/roles")
    public Result<List<String>> userRoles(@PathVariable Long userId) {
        return Result.ok(systemService.userRoleIds(userId));
    }
    @PutMapping("/users/{userId}/roles")
    public Result<Void> saveUserRoles(@PathVariable Long userId, @RequestBody Requests.UserRoleRequest request) {
        systemService.saveUserRoles(userId, request.getRoleIds());
        return Result.ok();
    }
    @PutMapping("/users/roles/batch")
    public Result<Void> saveBatchUserRoles(@RequestBody Requests.BatchUserRoleRequest request) {
        systemService.saveUserRolesBatch(request.getUserIds(), request.getRoleIds(), request.getMode());
        return Result.ok();
    }
    @GetMapping("/depts")
    public Result<List<Map<String, Object>>> depts() {
        return Result.ok(systemService.depts());
    }
    @GetMapping("/login-logs")
    public Result<Page<SysLoginLog>> loginLogs(@RequestParam(defaultValue = "1") int current,
                                                @RequestParam(defaultValue = "20") int size,
                                                @RequestParam(required = false) String keyword,
                                                @RequestParam(required = false) String status,
                                                @RequestParam(required = false) String startDate,
                                                @RequestParam(required = false) String endDate) {
        return Result.ok(systemService.loginLogPage(current, size, keyword, status, startDate, endDate));
    }
    @GetMapping("/operation-logs")
    public Result<Page<SysOperationLog>> operationLogs(@RequestParam(defaultValue = "1") int current,
                                                    @RequestParam(defaultValue = "20") int size,
                                                    @RequestParam(required = false) String keyword,
                                                    @RequestParam(required = false) String module,
                                                    @RequestParam(required = false) String operationType,
                                                    @RequestParam(required = false) String startDate,
                                                    @RequestParam(required = false) String endDate) {
        return Result.ok(systemService.operationLogPage(current, size, keyword, module, operationType, startDate, endDate));
    }
    @PostMapping("/menu")
@@ -81,27 +152,20 @@
        return Result.ok();
    }
    @GetMapping("/contractors")
    public Result<List<Contractor>> contractors() {
        return Result.ok(systemService.listContractors());
    @GetMapping("/roles/{roleId}/users")
    public Result<Map<String, Object>> roleUsers(@PathVariable Long roleId) {
        return Result.ok(systemService.roleUsers(roleId));
    }
    @PostMapping("/contractors")
    public Result<Contractor> createContractor(@RequestBody Contractor contractor) {
        contractor.setId(null);
        return Result.ok(systemService.saveContractor(contractor));
    }
    @PutMapping("/contractors/{id}")
    public Result<Contractor> updateContractor(@PathVariable Long id, @RequestBody Contractor contractor) {
        contractor.setId(id);
        return Result.ok(systemService.saveContractor(contractor));
    }
    @DeleteMapping("/contractors/{id}")
    public Result<Void> deleteContractor(@PathVariable Long id) {
        systemService.deleteContractor(id);
    @PutMapping("/roles/{roleId}/users")
    public Result<Void> saveRoleUsers(@PathVariable Long roleId, @RequestBody Requests.RoleUserRequest request) {
        systemService.saveRoleUsers(roleId, request.getUserIds());
        return Result.ok();
    }
    @GetMapping("/contractors")
    public Result<List<Map<String, Object>>> contractors() {
        return Result.ok(systemService.listContractors());
    }
    @GetMapping("/engineering-majors")
@@ -155,6 +219,31 @@
        return Result.ok(systemService.tree(type));
    }
    @GetMapping("/tree/product/roots")
    public Result<List<Map<String, Object>>> productTreeRoots() {
        return Result.ok(systemService.productTreeRoots());
    }
    @GetMapping("/tree/product/children")
    public Result<List<Map<String, Object>>> productTreeChildren(@RequestParam Long parentId) {
        return Result.ok(systemService.productTreeChildren(parentId));
    }
    @GetMapping("/tree/product/side-roots")
    public Result<List<Map<String, Object>>> productTreeForXNo(@RequestParam(required = false) String xNo) {
        return Result.ok(systemService.productTreeForXNo(xNo));
    }
    @GetMapping("/tree/product/x-numbers")
    public Result<List<String>> productXNumbers() {
        return Result.ok(systemService.productXNumbers());
    }
    @GetMapping("/tree/product/selected")
    public Result<List<Map<String, Object>>> selectedProductNodes(@RequestParam List<Long> ids) {
        return Result.ok(systemService.selectedProductNodes(ids));
    }
    @PostMapping("/tree/product")
    public Result<ProductNode> createProduct(@RequestBody ProductNode node) {
        node.setId(null);
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysLoginLog.java
New file
@@ -0,0 +1,29 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
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.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_LOGIN_LOG")
public class SysLoginLog 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("DEPT_NAME") private String deptName;
    @TableField("LOGIN_IP") private String loginIp;
    @TableField("STATUS") private String status;
    @TableField("MESSAGE") private String message;
    @TableField("LOGIN_TIME") private LocalDateTime loginTime;
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysMenu.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -14,7 +15,7 @@
@Data
@TableName("LX.T_SYS_MENU")
public class SysMenu {
public class SysMenu extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysOperationLog.java
New file
@@ -0,0 +1,33 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
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.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_OPERATION_LOG")
public class SysOperationLog 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("OPERATION_TYPE") private String operationType;
    @TableField("TARGET_TYPE") private String targetType;
    @TableField("TARGET_ID") private String targetId;
    @TableField("DESCRIPTION") private String description;
    @TableField("REQUEST_METHOD") private String requestMethod;
    @TableField("REQUEST_URI") private String requestUri;
    @TableField("RESULT") private String result;
    @TableField("CREATE_TIME") private LocalDateTime createTime;
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRole.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_SYS_ROLE")
public class SysRole {
public class SysRole extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRoleMenu.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_SYS_ROLE_MENU")
public class SysRoleMenu {
public class SysRoleMenu extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysRoleUser.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_SYS_ROLE_USER")
public class SysRoleUser {
public class SysRoleUser extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysTenantMenu.java
@@ -1,5 +1,6 @@
package com.emergencyrepair.system.entity;
import com.emergencyrepair.common.entity.BaseAuditEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -12,7 +13,7 @@
@Data
@TableName("LX.T_SYS_TENANT_MENU")
public class SysTenantMenu {
public class SysTenantMenu extends BaseAuditEntity {
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
emergency-repair-server/src/main/java/com/emergencyrepair/system/log/OperationLogInterceptor.java
New file
@@ -0,0 +1,112 @@
package com.emergencyrepair.system.log;
import com.emergencyrepair.system.entity.SysOperationLog;
import com.emergencyrepair.common.audit.CurrentUserContext;
import com.emergencyrepair.system.mapper.SysOperationLogMapper;
import com.emergencyrepair.system.service.SystemService;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * Records add/update/delete calls. Login is covered by the login log.
 */
@Component
@RequiredArgsConstructor
public class OperationLogInterceptor implements HandlerInterceptor {
    private static final Pattern TARGET_ID = Pattern.compile(".*/(\\d{6,})(?:/.*)?$");
    private final SysOperationLogMapper operationLogMapper;
    private final SystemService systemService;
    private final JdbcTemplate jdbcTemplate;
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception exception) {
        String method = request.getMethod();
        if (!"POST".equals(method) && !"PUT".equals(method) && !"DELETE".equals(method)) {
            return;
        }
        String uri = request.getRequestURI();
        if (!StringUtils.hasText(uri) || !uri.startsWith("/api/") || uri.startsWith("/api/auth/login")) {
            return;
        }
        try {
            String username = CurrentUserContext.get();
            SysOperationLog log = new SysOperationLog();
            log.setUserName(username);
            Long userId = systemService.resolveUserId(username);
            log.setUserId(userId);
            log.setNickName(resolveNickName(userId));
            log.setModule(resolveModule(uri));
            log.setOperationType(resolveOperationType(uri, method));
            log.setTargetType(resolveTargetType(uri));
            log.setTargetId(resolveTargetId(uri));
            log.setDescription(method + " " + uri);
            log.setRequestMethod(method);
            log.setRequestUri(uri);
            log.setResult(exception == null && response.getStatus() < 400 ? "\u6210\u529f" : "\u5931\u8d25");
            log.setCreateTime(LocalDateTime.now());
            operationLogMapper.insert(log);
        } catch (Exception ignored) {
            // Logging must never break the business request.
        }
    }
    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 resolveModule(String uri) {
        if (uri.startsWith("/api/faults") || uri.startsWith("/api/fault")) return "\u6545\u969c\u7ba1\u7406";
        if (uri.startsWith("/api/work-orders") || uri.startsWith("/api/completion")) return "\u5b8c\u5de5\u7ba1\u7406";
        if (uri.startsWith("/api/acceptances")) return "\u5b8c\u5de5\u9a8c\u6536";
        if (uri.startsWith("/api/reports")) return "\u7edf\u8ba1\u62a5\u8868";
        if (uri.startsWith("/api/system/users")) return "\u7528\u6237\u7ba1\u7406";
        if (uri.startsWith("/api/system/roles")) return "\u89d2\u8272\u7ba1\u7406";
        if (uri.startsWith("/api/system/menu")) return "\u83dc\u5355\u7ba1\u7406";
        if (uri.startsWith("/api/system/contractors")) return "\u627f\u4fee\u5355\u4f4d\u7ba1\u7406";
        if (uri.startsWith("/api/system")) return "\u7cfb\u7edf\u7ba1\u7406";
        return "\u5176\u4ed6";
    }
    private String resolveOperationType(String uri, String method) {
        if (uri.contains("/dispatch")) return "\u6d3e\u5355";
        if (uri.contains("/approve") || uri.contains("/approval") || uri.contains("/review")) return "\u5ba1\u6279";
        if (uri.contains("/cancel")) return "\u9500\u9879";
        if (uri.contains("/submit")) return "\u63d0\u4ea4";
        if (uri.contains("/apply")) return "\u7533\u8bf7";
        if (uri.contains("/print")) return "\u6253\u5370";
        if (uri.contains("/rating")) return "\u8bc4\u7ea7";
        if (uri.contains("/roles") || uri.contains("/menu-permissions")) return "\u6388\u6743";
        if (uri.contains("/buttons")) return "\u83dc\u5355\u6309\u94ae\u914d\u7f6e";
        if ("POST".equals(method)) return "\u65b0\u589e";
        if ("PUT".equals(method)) return "\u4fee\u6539";
        return "\u5220\u9664";
    }
    private String resolveTargetType(String uri) {
        String[] parts = uri.replace("/api/", "").split("/");
        return parts.length == 0 ? "" : parts[0];
    }
    private String resolveTargetId(String uri) {
        Matcher matcher = TARGET_ID.matcher(uri);
        return matcher.matches() ? matcher.group(1) : null;
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysLoginLogMapper.java
New file
@@ -0,0 +1,7 @@
package com.emergencyrepair.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emergencyrepair.system.entity.SysLoginLog;
public interface SysLoginLogMapper extends BaseMapper<SysLoginLog> {
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysOperationLogMapper.java
New file
@@ -0,0 +1,7 @@
package com.emergencyrepair.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emergencyrepair.system.entity.SysOperationLog;
public interface SysOperationLogMapper extends BaseMapper<SysOperationLog> {
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
New file
@@ -0,0 +1,117 @@
package com.emergencyrepair.system.service;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * Declarative catalog of the operation buttons each page exposes.
 * Menu management uses it to create MENU_TYPE=2 entries automatically.
 */
@Component
public class ButtonPermissionCatalog {
    @Data
    @AllArgsConstructor
    public static class ButtonDefinition {
        private String code;
        private String name;
    }
    private static final Map<String, List<ButtonDefinition>> CATALOG = new LinkedHashMap<>();
    static {
        put("/dashboard",
                def("dashboard:query", "\u67e5\u8be2"));
        put("/fault/report",
                def("fault:report:query", "\u67e5\u8be2"),
                def("fault:report:add", "\u65b0\u589e"),
                def("fault:report:edit", "\u4fee\u6539"),
                def("fault:report:delete", "\u5220\u9664"),
                def("fault:report:export", "\u5bfc\u51fa"),
                def("fault:report:report", "\u4e0a\u62a5"));
        put("/fault/review",
                def("fault:review:query", "\u67e5\u8be2"),
                def("fault:review:audit", "\u5ba1\u6838"),
                def("fault:review:edit", "\u4fee\u6539"),
                def("fault:review:cancel", "\u9500\u9879"));
        put("/fault/approval/cmb",
                def("fault:approval:query", "\u67e5\u8be2"),
                def("fault:approval:approve", "\u5ba1\u6279"));
        put("/fault/approval/jcc-assistant",
                def("fault:approval:query", "\u67e5\u8be2"),
                def("fault:approval:approve", "\u5ba1\u6279"));
        put("/fault/approval/jcc-director",
                def("fault:approval:query", "\u67e5\u8be2"),
                def("fault:approval:approve", "\u5ba1\u6279"));
        put("/fault/dispatch",
                def("fault:dispatch:query", "\u67e5\u8be2"),
                def("fault:dispatch:dispatch", "\u4e00\u952e\u6d3e\u5355"));
        put("/fault/completion",
                def("fault:completion:query", "\u67e5\u8be2"),
                def("fault:completion:status", "\u5b8c\u5de5\u72b6\u6001\u8bbe\u7f6e"),
                def("fault:completion:score", "rk_T\u5458\u8bc4\u5206"));
        put("/completion/report",
                def("completion:report:save", "\u4fdd\u5b58"),
                def("completion:report:submit", "\u63d0\u4ea4"));
        put("/acceptance/application",
                def("acceptance:application:query", "\u67e5\u8be2"),
                def("acceptance:application:apply", "\u7533\u8bf7"));
        put("/acceptance/inspector",
                def("acceptance:inspector:query", "\u67e5\u8be2"),
                def("acceptance:inspector:pass", "\u901a\u8fc7"),
                def("acceptance:inspector:reject", "\u6253\u56de"));
        put("/acceptance/leader",
                def("acceptance:leader:query", "\u67e5\u8be2"),
                def("acceptance:leader:pass", "\u901a\u8fc7"),
                def("acceptance:leader:reject", "\u6253\u56de"));
        put("/acceptance/print",
                def("acceptance:print:query", "\u67e5\u8be2"),
                def("acceptance:print:print", "\u6253\u5370"));
        put("/acceptance/rating",
                def("acceptance:rating:query", "\u67e5\u8be2"),
                def("acceptance:rating:grade", "\u8bc4\u7ea7"));
        put("/reports/overview", def("report:common:query", "\u67e5\u8be2"), def("report:common:export", "\u5bfc\u51fa"));
        put("/reports/top-equipment", def("report:common:query", "\u67e5\u8be2"), def("report:common:export", "\u5bfc\u51fa"));
        put("/reports/by-team", def("report:common:query", "\u67e5\u8be2"), def("report:common:export", "\u5bfc\u51fa"));
        put("/reports/contractors", def("report:common:query", "\u67e5\u8be2"), def("report:common:export", "\u5bfc\u51fa"));
        put("/reports/trend", def("report:common:query", "\u67e5\u8be2"), def("report:common:export", "\u5bfc\u51fa"));
        put("/system/product-tree", def("system:tree:add", "\u65b0\u589e"), def("system:tree:edit", "\u7f16\u8f91"), def("system:tree:delete", "\u5220\u9664"));
        put("/system/base-equipment", def("system:tree:add", "\u65b0\u589e"), def("system:tree:edit", "\u7f16\u8f91"), def("system:tree:delete", "\u5220\u9664"));
        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/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"));
    }
    private static void put(String url, ButtonDefinition... definitions) {
        List<ButtonDefinition> list = new ArrayList<>();
        Collections.addAll(list, definitions);
        CATALOG.put(url, Collections.unmodifiableList(list));
    }
    private static ButtonDefinition def(String code, String name) {
        return new ButtonDefinition(code, name);
    }
    public List<ButtonDefinition> buttonsFor(String url) {
        if (url == null) {
            return Collections.emptyList();
        }
        return CATALOG.getOrDefault(url, Collections.emptyList());
    }
    public Map<String, List<ButtonDefinition>> catalog() {
        return CATALOG;
    }
}
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
@@ -1,41 +1,52 @@
package com.emergencyrepair.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.emergencyrepair.business.entity.BaseEquipment;
import com.emergencyrepair.business.entity.Contractor;
import com.emergencyrepair.business.entity.EngineeringMajor;
import com.emergencyrepair.business.entity.TeamMajor;
import com.emergencyrepair.business.entity.ProductNode;
import com.emergencyrepair.business.mapper.BaseEquipmentMapper;
import com.emergencyrepair.business.mapper.ContractorMapper;
import com.emergencyrepair.business.mapper.EngineeringMajorMapper;
import com.emergencyrepair.business.mapper.ProductNodeMapper;
import com.emergencyrepair.business.mapper.TeamMajorMapper;
import com.emergencyrepair.common.BusinessException;
import com.emergencyrepair.system.entity.SysMenu;
import com.emergencyrepair.system.entity.SysLoginLog;
import com.emergencyrepair.system.entity.SysOperationLog;
import com.emergencyrepair.system.entity.SysRole;
import com.emergencyrepair.system.entity.SysRoleMenu;
import com.emergencyrepair.system.entity.SysRoleUser;
import com.emergencyrepair.system.entity.SysTenantMenu;
import com.emergencyrepair.system.mapper.SysMenuMapper;
import com.emergencyrepair.system.mapper.SysLoginLogMapper;
import com.emergencyrepair.system.mapper.SysOperationLogMapper;
import com.emergencyrepair.system.mapper.SysRoleMapper;
import com.emergencyrepair.system.mapper.SysRoleMenuMapper;
import com.emergencyrepair.system.mapper.SysRoleUserMapper;
import com.emergencyrepair.system.mapper.SysTenantMenuMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@@ -46,14 +57,28 @@
    private final SysRoleMenuMapper roleMenuMapper;
    private final SysTenantMenuMapper tenantMenuMapper;
    private final SysRoleUserMapper roleUserMapper;
    private final ContractorMapper contractorMapper;
    private final SysLoginLogMapper loginLogMapper;
    private final SysOperationLogMapper operationLogMapper;
    private final ButtonPermissionCatalog buttonPermissionCatalog;
    private final JdbcTemplate jdbcTemplate;
    private final EngineeringMajorMapper engineeringMajorMapper;
    private final TeamMajorMapper teamMajorMapper;
    private final ProductNodeMapper productNodeMapper;
    private final BaseEquipmentMapper baseEquipmentMapper;
    public List<SysMenu> menuTree() {
        List<SysMenu> menus = listMenus();
        // Buttons are permission points, not navigable nodes.
        List<SysMenu> navigable = listMenus().stream()
                .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
                .collect(Collectors.toList());
        return buildTree(navigable);
    }
    public List<SysMenu> menuTreeWithButtons() {
        return buildTree(listMenus());
    }
    private List<SysMenu> buildTree(List<SysMenu> menus) {
        Map<Long, SysMenu> map = menus.stream().collect(Collectors.toMap(SysMenu::getId, item -> item));
        List<SysMenu> roots = new ArrayList<>();
        for (SysMenu menu : menus) {
@@ -75,6 +100,29 @@
    public SysMenu saveMenu(SysMenu menu) {
        if (!StringUtils.hasText(menu.getName())) {
            throw new BusinessException("菜单名称不能为空");
        }
        if (menu.getMenuType() == null) {
            menu.setMenuType(menu.getId() == null ? 1 : menu.getMenuType());
        }
        boolean isRoot = menu.getPid() == null || menu.getPid() == 0L;
        if (menu.getMenuType() == 2) {
            if (isRoot) {
                throw new BusinessException("按钮类型只能建在菜单类型下级");
            }
            SysMenu parent = menuMapper.selectById(menu.getPid());
            if (parent == null || parent.getMenuType() == null || parent.getMenuType() != 1) {
                throw new BusinessException("按钮类型只能建在菜单类型下级");
            }
            if (!StringUtils.hasText(menu.getPermissions())) {
                throw new BusinessException("按钮权限标识不能为空");
            }
            menu.setUrl(null);
            menu.setIcon(null);
        } else if (!isRoot) {
            SysMenu parent = menuMapper.selectById(menu.getPid());
            if (parent != null && parent.getMenuType() != null && parent.getMenuType() == 2) {
                throw new BusinessException("按钮类型下级不能再建菜单");
            }
        }
        LocalDateTime now = LocalDateTime.now();
        menu.setUpdateTime(now);
@@ -131,30 +179,27 @@
        roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getRoleId, id));
    }
    public List<Contractor> listContractors() {
        return contractorMapper.selectList(Wrappers.<Contractor>lambdaQuery()
                .orderByAsc(Contractor::getSortNo).orderByAsc(Contractor::getId));
    }
    @Transactional
    public Contractor saveContractor(Contractor contractor) {
        if (!StringUtils.hasText(contractor.getName())) {
            throw new BusinessException("承修单位名称不能为空");
        }
        LocalDateTime now = LocalDateTime.now();
        contractor.setUpdateTime(now);
        if (contractor.getId() == null) {
            contractor.setCreateTime(now);
            contractorMapper.insert(contractor);
        } else {
            contractorMapper.updateById(contractor);
        }
        return contractorMapper.selectById(contractor.getId());
    }
    @Transactional
    public void deleteContractor(Long id) {
        contractorMapper.deleteById(id);
    /**
     * Contractor units are maintained as department records with DEPT_TYPE='cj'.
     */
    public List<Map<String, Object>> listContractors() {
        return jdbcTemplate.query(
                "SELECT DEPT_ID, DEPT_NAME, ORDER_NUM, LEADER, PHONE, STATUS, DEPT_CODE FROM CSICZB_ZB.SYS_DEPT "
                        + "WHERE DEPT_TYPE = 'cj' AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL) ORDER BY ORDER_NUM, DEPT_ID",
                (rs, rowNum) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("id", String.valueOf(rs.getLong("DEPT_ID")));
                    row.put("name", rs.getString("DEPT_NAME"));
                    row.put("shortName", null);
                    row.put("nature", "承修单位");
                    row.put("sortNo", rs.getObject("ORDER_NUM"));
                    row.put("contact", rs.getString("LEADER"));
                    row.put("phone", rs.getString("PHONE"));
                    row.put("status", rs.getString("STATUS"));
                    row.put("deptCode", rs.getString("DEPT_CODE"));
                    row.put("dishonest", 0);
                    return row;
                });
    }
    public List<EngineeringMajor> listEngineeringMajors() {
@@ -201,12 +246,12 @@
    @Transactional
    public TeamMajor saveTeamMajor(TeamMajor major) {
        if (major == null || !StringUtils.hasText(major.getName())) {
            throw new BusinessException("T队专业名称不能为空");
            throw new BusinessException("rk_T队专业名称不能为空");
        }
        String name = major.getName().trim();
        String category = StringUtils.hasText(major.getCategory()) ? major.getCategory().trim().toUpperCase() : "";
        if (!"A".equals(category) && !"B".equals(category)) {
            throw new BusinessException("T队专业分类只能是A或B");
            throw new BusinessException("rk_T队专业分类只能是A或B");
        }
        LambdaQueryWrapper<TeamMajor> duplicate = Wrappers.<TeamMajor>lambdaQuery()
                .eq(TeamMajor::getName, name);
@@ -214,7 +259,7 @@
            duplicate.ne(TeamMajor::getId, major.getId());
        }
        if (teamMajorMapper.selectCount(duplicate) > 0) {
            throw new BusinessException("T队专业名称已存在:" + name);
            throw new BusinessException("rk_T队专业名称已存在:" + name);
        }
        LocalDateTime now = LocalDateTime.now();
        major.setName(name);
@@ -244,9 +289,180 @@
        throw new BusinessException("未知树类型");
    }
    public List<Map<String, Object>> selectedProductNodes(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyList();
        }
        List<ProductNode> source = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .in(ProductNode::getId, ids));
        Map<Long, ProductNode> unique = uniqueProductNodes(source);
        List<Map<String, Object>> result = new ArrayList<>();
        Set<Long> seen = new HashSet<>();
        for (Long id : ids) {
            ProductNode node = unique.get(id);
            if (node == null || !seen.add(id)) {
                continue;
            }
            List<ProductNode> path = productPath(node);
            Map<String, Object> row = mapProduct(node, Collections.emptySet());
            row.put("pathNames", path.stream().map(ProductNode::getName).collect(Collectors.toList()));
            row.put("parentPathNames", path.stream()
                    .filter(item -> item != node)
                    .filter(item -> item.getNodeLevel() != null && item.getNodeLevel() > 2)
                    .map(ProductNode::getName)
                    .collect(Collectors.toList()));
            ProductNode xNode = path.stream()
                    .filter(item -> item.getNodeLevel() != null && item.getNodeLevel() == 2)
                    .findFirst().orElse(null);
            row.put("xNo", xNode == null ? null : xNode.getName());
            result.add(row);
        }
        return result;
    }
    private List<Map<String, Object>> mapProductTreeNodes(List<ProductNode> source, Set<Long> forcedBranchIds) {
        Map<Long, ProductNode> unique = uniqueProductNodes(source);
        List<ProductNode> nodes = sortProductNodes(unique.values());
        Set<Long> branchIds = new HashSet<>(forcedBranchIds);
        if (!unique.isEmpty()) {
            List<ProductNode> childRows = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                    .select(ProductNode::getParentId, ProductNode::getIsDelete)
                    .in(ProductNode::getParentId, unique.keySet()));
            for (ProductNode child : childRows) {
                if (!"1".equals(child.getIsDelete()) && child.getParentId() != null) {
                    branchIds.add(child.getParentId());
                }
            }
        }
        List<Map<String, Object>> rows = new ArrayList<>();
        for (ProductNode node : nodes) {
            rows.add(mapProduct(node, branchIds));
        }
        return rows;
    }
    private Map<Long, ProductNode> uniqueProductNodes(Collection<ProductNode> source) {
        Map<Long, ProductNode> unique = new LinkedHashMap<>();
        if (source == null) {
            return unique;
        }
        for (ProductNode node : source) {
            if (node.getId() == null || "1".equals(node.getIsDelete())) {
                continue;
            }
            unique.putIfAbsent(node.getId(), node);
        }
        return unique;
    }
    private List<ProductNode> sortProductNodes(Collection<ProductNode> source) {
        List<ProductNode> nodes = new ArrayList<>(source);
        nodes.sort(Comparator.comparingInt(this::productNodeLevel)
                .thenComparing(ProductNode::getSortNo, Comparator.nullsLast(Integer::compareTo))
                .thenComparing(ProductNode::getId, Comparator.nullsLast(Long::compareTo)));
        return nodes;
    }
    private ProductNode findProductRoot(ProductNode node) {
        ProductNode current = node;
        Set<Long> visited = new HashSet<>();
        while (current != null && current.getParentId() != null && current.getParentId() != 0L
                && visited.add(current.getId())) {
            current = findProductNode(current.getParentId());
        }
        return current;
    }
    private List<ProductNode> productPath(ProductNode node) {
        List<ProductNode> reversed = new ArrayList<>();
        Set<Long> visited = new HashSet<>();
        ProductNode current = node;
        while (current != null && visited.add(current.getId())) {
            reversed.add(current);
            if (current.getParentId() == null || current.getParentId() == 0L) {
                break;
            }
            current = findProductNode(current.getParentId());
        }
        Collections.reverse(reversed);
        return reversed;
    }
    public List<Map<String, Object>> productTreeForXNo(String xNo) {
        if (!StringUtils.hasText(xNo)) {
            return productTreeRoots();
        }
        List<ProductNode> rootNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .and(wrapper -> wrapper.isNull(ProductNode::getParentId).or().eq(ProductNode::getParentId, 0L))
                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
        List<ProductNode> sideNodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .in(ProductNode::getLayer, Arrays.asList("side", "SIDE"))
                .eq(ProductNode::getName, xNo.trim())
                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
        Map<Long, ProductNode> roots = uniqueProductNodes(rootNodes);
        Map<Long, ProductNode> sides = uniqueProductNodes(sideNodes);
        Set<Long> branchIds = new HashSet<>();
        branchIds.addAll(roots.keySet());
        branchIds.addAll(sides.keySet());
        Map<Long, Map<String, Object>> rootRows = new LinkedHashMap<>();
        for (ProductNode root : sortProductNodes(roots.values())) {
            rootRows.put(root.getId(), mapProduct(root, branchIds));
        }
        for (ProductNode side : sortProductNodes(sides.values())) {
            ProductNode root = findProductRoot(side);
            Map<String, Object> rootRow = root == null ? null : rootRows.get(root.getId());
            if (rootRow != null) {
                children(rootRow).add(mapProduct(side, branchIds));
            }
        }
        return new ArrayList<>(rootRows.values());
    }
    public List<String> productXNumbers() {
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .in(ProductNode::getLayer, Arrays.asList("side", "SIDE"))
                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
        return nodes.stream()
                .filter(node -> !"1".equals(node.getIsDelete()))
                .map(ProductNode::getName)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .sorted(String.CASE_INSENSITIVE_ORDER)
                .collect(Collectors.toList());
    }
    public List<Map<String, Object>> productTreeRoots() {
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .and(wrapper -> wrapper.isNull(ProductNode::getParentId).or().eq(ProductNode::getParentId, 0L))
                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
        return mapProductTreeNodes(nodes, Collections.emptySet());
    }
    public List<Map<String, Object>> productTreeChildren(Long parentId) {
        if (parentId == null || parentId == 0L) {
            return productTreeRoots();
        }
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .eq(ProductNode::getParentId, parentId)
                .orderByAsc(ProductNode::getSortNo).orderByAsc(ProductNode::getId));
        return mapProductTreeNodes(nodes, Collections.emptySet());
    }
    public List<Map<String, Object>> productTree() {
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .orderByAsc(ProductNode::getNodeLevel).orderByAsc(ProductNode::getSortNo));
                .orderByAsc(ProductNode::getId).orderByAsc(ProductNode::getSortNo));
        Map<Long, ProductNode> unique = new LinkedHashMap<>();
        for (ProductNode node : nodes) {
            if (node.getId() == null || "1".equals(node.getIsDelete())) {
                continue;
            }
            unique.putIfAbsent(node.getId(), node);
        }
        nodes = new ArrayList<>(unique.values());
        nodes.sort(Comparator.comparingInt(this::productNodeLevel)
                .thenComparing(ProductNode::getSortNo, Comparator.nullsLast(Integer::compareTo))
                .thenComparing(ProductNode::getId, Comparator.nullsLast(Long::compareTo)));
        Map<Long, Map<String, Object>> map = new LinkedHashMap<>();
        for (ProductNode node : nodes) {
            map.put(node.getId(), mapProduct(node));
@@ -284,24 +500,48 @@
    @Transactional
    public ProductNode saveProductNode(ProductNode node) {
        if (!StringUtils.hasText(node.getName()) || node.getNodeLevel() == null) {
            throw new BusinessException("节点名称和层级不能为空");
        if (!StringUtils.hasText(node.getName())) {
            throw new BusinessException("节点名称不能为空");
        }
        LocalDateTime now = LocalDateTime.now();
        node.setUpdateTime(now);
        String layer = resolveProductLayer(node);
        node.setLayer(layer);
        node.setConnectGroup(buildConnectGroup(node.getParentId()));
        if (node.getId() == null) {
            node.setId(IdWorker.getId());
            node.setRecordId(String.valueOf(node.getId()));
            node.setProject(StringUtils.hasText(node.getProject()) ? node.getProject().trim() : "0");
            node.setSortNo(node.getSortNo() == null ? 1 : node.getSortNo());
            node.setIsDelete("0");
            node.setCreateBy(StringUtils.hasText(node.getCreateBy()) ? node.getCreateBy().trim() : "system");
            node.setCreateTime(now);
            productNodeMapper.insert(node);
        } else {
            ProductNode current = findProductNode(node.getId());
            if (current == null) {
                throw new BusinessException("节点不存在");
            }
            node.setRecordId(current.getRecordId());
            node.setProject(StringUtils.hasText(current.getProject()) ? current.getProject() : "0");
            node.setIsDelete(StringUtils.hasText(current.getIsDelete()) ? current.getIsDelete() : "0");
            node.setCreateBy(current.getCreateBy());
            node.setCreateTime(current.getCreateTime());
            productNodeMapper.updateById(node);
        }
        return productNodeMapper.selectById(node.getId());
        return findProductNode(node.getId());
    }
    @Transactional
    public void deleteProductNode(Long id) {
        assertNoChildren(productNodeMapper.selectCount(Wrappers.<ProductNode>lambdaQuery().eq(ProductNode::getParentId, id)));
        productNodeMapper.deleteById(id);
        assertNoChildren(productNodeMapper.selectCount(Wrappers.<ProductNode>lambdaQuery()
                .eq(ProductNode::getParentId, id).ne(ProductNode::getIsDelete, "1")));
        ProductNode current = findProductNode(id);
        if (current == null) {
            throw new BusinessException("节点不存在");
        }
        current.setIsDelete("1");
        productNodeMapper.updateById(current);
    }
    @Transactional
@@ -352,19 +592,529 @@
        }
        roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getRoleId, roleId));
        tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getTenantId, tenantId));
        for (Long menuId : roleMenuIds == null ? Collections.<Long>emptyList() : roleMenuIds) {
        List<Long> effectiveRoleMenuIds = expandWithAncestors(roleMenuIds);
        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 : tenantMenuIds == null ? Collections.<Long>emptyList() : tenantMenuIds) {
        for (Long menuId : effectiveTenantMenuIds) {
            SysTenantMenu relation = new SysTenantMenu();
            relation.setTenantId(tenantId);
            relation.setMenuId(menuId);
            relation.setCreateTime(LocalDateTime.now());
            tenantMenuMapper.insert(relation);
        }
    }
    public Map<String, Object> userPage(String keyword, Long deptId, Long roleId, int current, int size) {
        List<Object> args = new ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT u.USER_ID, u.USER_NAME, u.NICK_NAME, u.DEPT_ID, d.DEPT_NAME, u.PHONENUMBER, u.STATUS, u.LOGIN_DATE ");
        sql.append("FROM CSICZB_ZB.SYS_USER u LEFT JOIN CSICZB_ZB.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID ");
        sql.append("WHERE (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL) ");
        if (StringUtils.hasText(keyword)) {
            sql.append("AND (u.USER_NAME LIKE ? OR u.NICK_NAME LIKE ?) ");
            String like = "%" + keyword.trim() + "%";
            args.add(like);
            args.add(like);
        }
        if (deptId != null) {
            sql.append("AND u.DEPT_ID = ? ");
            args.add(deptId);
        }
        if (roleId != null) {
            sql.append("AND EXISTS (SELECT 1 FROM LX.T_SYS_ROLE_USER ru WHERE ru.USER_ID = u.USER_ID AND ru.ROLE_ID = ? AND ru.DEL_FLAG = 0) ");
            args.add(roleId);
        }
        sql.append("ORDER BY u.USER_ID");
        List<Map<String, Object>> all = jdbcTemplate.query(sql.toString(), args.toArray(), (rs, rowNum) -> {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("userId", String.valueOf(rs.getLong("USER_ID")));
            row.put("userName", rs.getString("USER_NAME"));
            row.put("nickName", rs.getString("NICK_NAME"));
            Object dept = rs.getObject("DEPT_ID");
            row.put("deptId", dept == null ? null : String.valueOf(rs.getLong("DEPT_ID")));
            row.put("deptName", rs.getString("DEPT_NAME"));
            row.put("phoneNumber", rs.getString("PHONENUMBER"));
            row.put("status", rs.getString("STATUS"));
            java.sql.Timestamp loginDate = rs.getTimestamp("LOGIN_DATE");
            row.put("loginDate", loginDate == null ? null : loginDate.toLocalDateTime().toString().replace('T', ' '));
            row.put("roles", new ArrayList<Map<String, Object>>());
            return row;
        });
        Map<Long, Map<String, Object>> byId = new HashMap<>();
        for (Map<String, Object> row : all) {
            byId.put(Long.valueOf(String.valueOf(row.get("userId"))), row);
        }
        if (!byId.isEmpty()) {
            List<Map<String, Object>> roleRows = jdbcTemplate.query(
                    "SELECT ru.USER_ID, r.ID, r.NAME, r.CODE, r.ROLE_TYPE FROM LX.T_SYS_ROLE_USER ru JOIN LX.T_SYS_ROLE r ON r.ID = ru.ROLE_ID AND r.DEL_FLAG = 0 WHERE ru.DEL_FLAG = 0 ORDER BY r.SORT_NO",
                    (rs, rowNum) -> {
                        Map<String, Object> role = new LinkedHashMap<>();
                        role.put("userId", String.valueOf(rs.getLong("USER_ID")));
                        role.put("roleId", String.valueOf(rs.getLong("ID")));
                        role.put("roleName", rs.getString("NAME"));
                        role.put("roleCode", rs.getString("CODE"));
                        role.put("roleType", rs.getString("ROLE_TYPE"));
                        return role;
                    });
            for (Map<String, Object> role : roleRows) {
                Map<String, Object> user = byId.get(Long.valueOf(String.valueOf(role.get("userId"))));
                if (user != null) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> roles = (List<Map<String, Object>>) user.get("roles");
                    roles.add(role);
                }
            }
        }
        int safeSize = size <= 0 ? 20 : size;
        int safeCurrent = current <= 0 ? 1 : current;
        int total = all.size();
        int from = Math.min(total, (safeCurrent - 1) * safeSize);
        int to = Math.min(total, from + safeSize);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("records", new ArrayList<>(all.subList(from, to)));
        result.put("total", total);
        result.put("current", safeCurrent);
        result.put("size", safeSize);
        result.put("pages", (total + safeSize - 1) / safeSize);
        return result;
    }
    public List<Map<String, Object>> depts() {
        return jdbcTemplate.query(
                "SELECT DEPT_ID, DEPT_NAME, PARENT_ID, ORDER_NUM, DEPT_TYPE, LEADER, PHONE, STATUS, DEPT_CODE FROM CSICZB_ZB.SYS_DEPT WHERE (DEL_FLAG = '0' OR DEL_FLAG IS NULL) ORDER BY ORDER_NUM, DEPT_ID",
                (rs, rowNum) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("id", String.valueOf(rs.getLong("DEPT_ID")));
                    row.put("name", rs.getString("DEPT_NAME"));
                    Object parentId = rs.getObject("PARENT_ID");
                    row.put("parentId", parentId == null ? null : String.valueOf(rs.getLong("PARENT_ID")));
                    row.put("orderNum", rs.getObject("ORDER_NUM"));
                    row.put("deptType", rs.getString("DEPT_TYPE"));
                    row.put("leader", rs.getString("LEADER"));
                    row.put("phone", rs.getString("PHONE"));
                    row.put("status", rs.getString("STATUS"));
                    row.put("deptCode", rs.getString("DEPT_CODE"));
                    return row;
                });
    }
    public List<String> userRoleIds(Long userId) {
        if (userId == null) {
            return new ArrayList<>();
        }
        return roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
                        .eq(SysRoleUser::getUserId, userId))
                .stream().map(item -> String.valueOf(item.getRoleId())).collect(Collectors.toList());
    }
    public Map<String, Object> roleUsers(Long roleId) {
        if (roleId == null || roleMapper.selectById(roleId) == null) {
            throw new BusinessException("角色不存在");
        }
        Set<Long> assignedUserIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
                        .eq(SysRoleUser::getRoleId, roleId))
                .stream().map(SysRoleUser::getUserId).collect(Collectors.toCollection(LinkedHashSet::new));
        List<Map<String, Object>> users = jdbcTemplate.query(
                "SELECT u.USER_ID, u.USER_NAME, u.NICK_NAME, u.DEPT_ID, d.DEPT_NAME, u.STATUS " +
                        "FROM CSICZB_ZB.SYS_USER u LEFT JOIN CSICZB_ZB.SYS_DEPT d ON d.DEPT_ID = u.DEPT_ID " +
                        "WHERE (u.DEL_FLAG = '0' OR u.DEL_FLAG IS NULL) ORDER BY u.USER_ID",
                (rs, rowNum) -> {
                    Long userId = rs.getLong("USER_ID");
                    Map<String, Object> user = new LinkedHashMap<>();
                    user.put("userId", String.valueOf(userId));
                    user.put("userName", rs.getString("USER_NAME"));
                    user.put("nickName", rs.getString("NICK_NAME"));
                    Object deptId = rs.getObject("DEPT_ID");
                    user.put("deptId", deptId == null ? null : String.valueOf(rs.getLong("DEPT_ID")));
                    user.put("deptName", rs.getString("DEPT_NAME"));
                    user.put("status", rs.getString("STATUS"));
                    user.put("assigned", assignedUserIds.contains(userId));
                    return user;
                });
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("records", users);
        result.put("userIds", assignedUserIds.stream().map(String::valueOf).collect(Collectors.toList()));
        result.put("total", users.size());
        return result;
    }
    @Transactional
    public void saveRoleUsers(Long roleId, List<Long> userIds) {
        if (roleId == null || roleMapper.selectById(roleId) == null) {
            throw new BusinessException("角色不存在");
        }
        Set<Long> distinct = new LinkedHashSet<>();
        if (userIds != null) {
            distinct.addAll(userIds);
        }
        distinct.remove(null);
        for (Long userId : distinct) {
            Integer exists = jdbcTemplate.queryForObject(
                    "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
                    Integer.class, userId);
            if (exists == null || exists == 0) {
                throw new BusinessException("用户不存在");
            }
        }
        roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getRoleId, roleId));
        for (Long userId : distinct) {
            SysRoleUser relation = new SysRoleUser();
            relation.setRoleId(roleId);
            relation.setUserId(userId);
            relation.setCreateTime(LocalDateTime.now());
            roleUserMapper.insert(relation);
        }
    }
    @Transactional
    public void saveUserRoles(Long userId, List<Long> roleIds) {
        if (userId == null) {
            throw new BusinessException("请选择用户");
        }
        Integer exists = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
                Integer.class, userId);
        if (exists == null || exists == 0) {
            throw new BusinessException("用户不存在");
        }
        Set<Long> distinct = new LinkedHashSet<>();
        if (roleIds != null) {
            distinct.addAll(roleIds);
        }
        for (Long roleId : distinct) {
            if (roleId != null && roleMapper.selectById(roleId) == null) {
                throw new BusinessException("角色不存在");
            }
        }
        roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getUserId, userId));
        for (Long roleId : distinct) {
            if (roleId == null) {
                continue;
            }
            SysRoleUser relation = new SysRoleUser();
            relation.setRoleId(roleId);
            relation.setUserId(userId);
            relation.setCreateTime(LocalDateTime.now());
            roleUserMapper.insert(relation);
        }
    }
    @Transactional
    public void saveUserRolesBatch(List<Long> userIds, List<Long> roleIds, String mode) {
        Set<Long> distinctUsers = new LinkedHashSet<>();
        if (userIds != null) {
            distinctUsers.addAll(userIds);
        }
        distinctUsers.remove(null);
        if (distinctUsers.isEmpty()) {
            throw new BusinessException("\u8bf7\u9009\u62e9\u7528\u6237");
        }
        Set<Long> distinctRoles = new LinkedHashSet<>();
        if (roleIds != null) {
            distinctRoles.addAll(roleIds);
        }
        distinctRoles.remove(null);
        for (Long roleId : distinctRoles) {
            if (roleMapper.selectById(roleId) == null) {
                throw new BusinessException("\u89d2\u8272\u4e0d\u5b58\u5728");
            }
        }
        boolean append = !"replace".equalsIgnoreCase(mode == null ? "" : mode.trim());
        for (Long userId : distinctUsers) {
            Integer exists = jdbcTemplate.queryForObject(
                    "SELECT COUNT(*) FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
                    Integer.class, userId);
            if (exists == null || exists == 0) {
                throw new BusinessException("\u7528\u6237\u4e0d\u5b58\u5728");
            }
            Set<Long> targetRoleIds = new LinkedHashSet<>();
            if (append) {
                targetRoleIds.addAll(roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
                                .eq(SysRoleUser::getUserId, userId))
                        .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList()));
            }
            targetRoleIds.addAll(distinctRoles);
            roleUserMapper.delete(Wrappers.<SysRoleUser>lambdaQuery().eq(SysRoleUser::getUserId, userId));
            for (Long roleId : targetRoleIds) {
                SysRoleUser relation = new SysRoleUser();
                relation.setRoleId(roleId);
                relation.setUserId(userId);
                relation.setCreateTime(LocalDateTime.now());
                roleUserMapper.insert(relation);
            }
        }
    }
    public List<String> roleCodes(String username) {
        Long userId = resolveUserId(username);
        if (userId == null) {
            return new ArrayList<>();
        }
        List<Long> roleIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
                        .eq(SysRoleUser::getUserId, userId))
                .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList());
        if (roleIds.isEmpty()) {
            return new ArrayList<>();
        }
        return roleMapper.selectBatchIds(roleIds).stream()
                .map(SysRole::getCode).filter(StringUtils::hasText).sorted().collect(Collectors.toList());
    }
    public Set<Long> effectiveMenuIdsForUser(String username) {
        Set<Long> result = new LinkedHashSet<>();
        if (!StringUtils.hasText(username)) {
            return result;
        }
        if ("admin".equals(username)) {
            listMenus().forEach(menu -> result.add(menu.getId()));
            return result;
        }
        Long userId = resolveUserId(username);
        if (userId == null) {
            return result;
        }
        List<Long> roleIds = roleUserMapper.selectList(Wrappers.<SysRoleUser>lambdaQuery()
                        .eq(SysRoleUser::getUserId, userId))
                .stream().map(SysRoleUser::getRoleId).collect(Collectors.toList());
        if (roleIds.isEmpty()) {
            return result;
        }
        Set<Long> roleMenuIds = new LinkedHashSet<>(roleMenuMapper.selectList(Wrappers.<SysRoleMenu>lambdaQuery()
                        .in(SysRoleMenu::getRoleId, roleIds))
                .stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList()));
        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);
                }
            }
        }
        return result;
    }
    public List<SysMenu> menuTreeForUser(String username) {
        Set<Long> allowed = new LinkedHashSet<>(effectiveMenuIdsForUser(username));
        if (allowed.isEmpty() && !"admin".equals(username)) {
            SysMenu dashboard = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                    .eq(SysMenu::getUrl, "/dashboard").last("LIMIT 1"));
            if (dashboard != null) {
                allowed.add(dashboard.getId());
            }
        }
        final Set<Long> allowedIds = allowed;
        List<SysMenu> navigable = listMenus().stream()
                .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
                .filter(menu -> allowedIds.contains(menu.getId()))
                .collect(Collectors.toList());
        return buildTree(navigable);
    }
    public List<String> permissionCodes(String username) {
        Set<Long> menuIds = effectiveMenuIdsForUser(username);
        return listMenus().stream()
                .filter(menu -> menuIds.contains(menu.getId()))
                .map(SysMenu::getPermissions)
                .filter(StringUtils::hasText)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
    }
    public Map<String, Object> menuButtons(Long menuId) {
        SysMenu menu = menuMapper.selectById(menuId);
        if (menu == null) {
            throw new BusinessException("菜单不存在");
        }
        List<Map<String, Object>> available = new ArrayList<>();
        for (ButtonPermissionCatalog.ButtonDefinition definition : buttonPermissionCatalog.buttonsFor(menu.getUrl())) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("code", definition.getCode());
            row.put("name", definition.getName());
            available.add(row);
        }
        List<String> selected = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
                        .eq(SysMenu::getPid, menuId)
                        .eq(SysMenu::getMenuType, 2))
                .stream().map(SysMenu::getPermissions).filter(StringUtils::hasText).collect(Collectors.toList());
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("menuId", String.valueOf(menuId));
        result.put("menuName", menu.getName());
        result.put("available", available);
        result.put("selected", selected);
        return result;
    }
    @Transactional
    public void syncMenuButtons(Long menuId, List<String> codes) {
        SysMenu menu = menuMapper.selectById(menuId);
        if (menu == null) {
            throw new BusinessException("菜单不存在");
        }
        if (menu.getMenuType() == null || menu.getMenuType() != 1) {
            throw new BusinessException("只有菜单类型才能配置按钮");
        }
        List<ButtonPermissionCatalog.ButtonDefinition> definitions = buttonPermissionCatalog.buttonsFor(menu.getUrl());
        Map<String, ButtonPermissionCatalog.ButtonDefinition> definitionMap = new LinkedHashMap<>();
        for (ButtonPermissionCatalog.ButtonDefinition definition : definitions) {
            definitionMap.put(definition.getCode(), definition);
        }
        Set<String> wanted = new LinkedHashSet<>();
        if (codes != null) {
            for (String code : codes) {
                if (StringUtils.hasText(code) && definitionMap.containsKey(code)) {
                    wanted.add(code);
                }
            }
        }
        List<SysMenu> existing = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
                .eq(SysMenu::getPid, menuId).eq(SysMenu::getMenuType, 2));
        Map<String, SysMenu> existingByCode = new HashMap<>();
        for (SysMenu child : existing) {
            existingByCode.put(child.getPermissions(), child);
            if (!wanted.contains(child.getPermissions())) {
                menuMapper.deleteById(child.getId());
                roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getMenuId, child.getId()));
                tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getMenuId, child.getId()));
            }
        }
        LocalDateTime now = LocalDateTime.now();
        for (int index = 0; index < definitions.size(); index++) {
            ButtonPermissionCatalog.ButtonDefinition definition = definitions.get(index);
            if (!wanted.contains(definition.getCode())) {
                continue;
            }
            SysMenu child = existingByCode.get(definition.getCode());
            if (child == null) {
                child = new SysMenu();
                child.setPid(menuId);
                child.setName(definition.getName());
                child.setPermissions(definition.getCode());
                child.setMenuType(2);
                child.setSortNo((index + 1) * 10);
                child.setVisible(1);
                child.setCreateTime(now);
                child.setUpdateTime(now);
                menuMapper.insert(child);
            } else {
                child.setName(definition.getName());
                child.setSortNo((index + 1) * 10);
                child.setUpdateTime(now);
                menuMapper.updateById(child);
            }
        }
    }
    public Page<SysLoginLog> loginLogPage(int current, int size, String keyword, String status, String startDate, String endDate) {
        Page<SysLoginLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
        LambdaQueryWrapper<SysLoginLog> wrapper = Wrappers.lambdaQuery();
        if (StringUtils.hasText(keyword)) {
            String value = keyword.trim();
            wrapper.and(item -> item.like(SysLoginLog::getUserName, value).or().like(SysLoginLog::getNickName, value));
        }
        if (StringUtils.hasText(status)) {
            wrapper.eq(SysLoginLog::getStatus, status.trim());
        }
        LocalDateTime start = parseStart(startDate);
        LocalDateTime end = parseEnd(endDate);
        if (start != null) {
            wrapper.ge(SysLoginLog::getLoginTime, start);
        }
        if (end != null) {
            wrapper.le(SysLoginLog::getLoginTime, end);
        }
        wrapper.orderByDesc(SysLoginLog::getLoginTime);
        return loginLogMapper.selectPage(page, wrapper);
    }
    public Page<SysOperationLog> operationLogPage(int current, int size, String keyword, String module, String operationType, String startDate, String endDate) {
        Page<SysOperationLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
        LambdaQueryWrapper<SysOperationLog> wrapper = Wrappers.lambdaQuery();
        if (StringUtils.hasText(keyword)) {
            String value = keyword.trim();
            wrapper.and(item -> item.like(SysOperationLog::getUserName, value)
                    .or().like(SysOperationLog::getNickName, value)
                    .or().like(SysOperationLog::getRequestUri, value));
        }
        if (StringUtils.hasText(module)) {
            wrapper.eq(SysOperationLog::getModule, module.trim());
        }
        if (StringUtils.hasText(operationType)) {
            wrapper.eq(SysOperationLog::getOperationType, operationType.trim());
        }
        LocalDateTime start = parseStart(startDate);
        LocalDateTime end = parseEnd(endDate);
        if (start != null) {
            wrapper.ge(SysOperationLog::getCreateTime, start);
        }
        if (end != null) {
            wrapper.le(SysOperationLog::getCreateTime, end);
        }
        wrapper.orderByDesc(SysOperationLog::getCreateTime);
        return operationLogMapper.selectPage(page, wrapper);
    }
    public Long resolveUserId(String username) {
        if (!StringUtils.hasText(username)) {
            return null;
        }
        List<Long> ids = jdbcTemplate.query(
                "SELECT USER_ID FROM CSICZB_ZB.SYS_USER WHERE USER_NAME = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
                new Object[]{username}, (rs, rowNum) -> rs.getLong(1));
        return ids.isEmpty() ? null : ids.get(0);
    }
    private List<Long> expandWithAncestors(List<Long> ids) {
        Set<Long> result = new LinkedHashSet<>();
        if (ids == null) {
            return new ArrayList<>(result);
        }
        for (Long id : ids) {
            Long cursor = id;
            int guard = 0;
            while (cursor != null && cursor != 0L && guard++ < 32) {
                if (!result.add(cursor)) {
                    break;
                }
                SysMenu menu = menuMapper.selectById(cursor);
                if (menu == null) {
                    break;
                }
                cursor = menu.getPid();
            }
        }
        return new ArrayList<>(result);
    }
    private LocalDateTime parseStart(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return LocalDate.parse(value.trim()).atStartOfDay();
        } catch (Exception ex) {
            return null;
        }
    }
    private LocalDateTime parseEnd(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return LocalDate.parse(value.trim()).atTime(23, 59, 59);
        } catch (Exception ex) {
            return null;
        }
    }
@@ -394,20 +1144,70 @@
    }
    private Map<String, Object> mapProduct(ProductNode node) {
        return mapProduct(node, Collections.emptySet());
    }
    private Map<String, Object> mapProduct(ProductNode node, Set<Long> branchIds) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("id", String.valueOf(node.getId()));
        row.put("parentId", node.getParentId() == null ? null : String.valueOf(node.getParentId()));
        row.put("nodeLevel", node.getNodeLevel());
        row.put("nodeType", node.getNodeType());
        row.put("layer", node.getLayer());
        row.put("connectGroup", node.getConnectGroup());
        row.put("name", node.getName());
        row.put("code", node.getCode());
        row.put("baseEquipmentId", node.getBaseEquipmentId() == null ? null : String.valueOf(node.getBaseEquipmentId()));
        row.put("project", node.getProject());
        row.put("sortNo", node.getSortNo());
        row.put("remark", node.getRemark());
        row.put("isLeaf", !branchIds.contains(node.getId()));
        row.put("children", new ArrayList<>());
        return row;
    }
    private int productNodeLevel(ProductNode node) {
        Integer level = node.getNodeLevel();
        return level == null ? Integer.MAX_VALUE : level;
    }
    private String resolveProductLayer(ProductNode node) {
        String layer = ProductNode.layerForType(node.getNodeType());
        if (!StringUtils.hasText(layer)) {
            layer = ProductNode.layerForLevel(node.getNodeLevel());
        }
        if (!StringUtils.hasText(layer)) {
            layer = StringUtils.hasText(node.getLayer()) ? node.getLayer().trim().toLowerCase() : null;
        }
        if (!StringUtils.hasText(layer)) {
            throw new BusinessException("节点层级不能为空");
        }
        return layer;
    }
    private String buildConnectGroup(Long parentId) {
        if (parentId == null || parentId == 0L) {
            return "0";
        }
        ProductNode parent = findProductNode(parentId);
        if (parent == null) {
            throw new BusinessException("父节点不存在");
        }
        String prefix = StringUtils.hasText(parent.getConnectGroup()) ? parent.getConnectGroup() : "0";
        return prefix + "," + parent.getId();
    }
    private ProductNode findProductNode(Long id) {
        if (id == null) {
            return null;
        }
        List<ProductNode> nodes = productNodeMapper.selectList(Wrappers.<ProductNode>lambdaQuery()
                .eq(ProductNode::getId, id)
                .orderByAsc(ProductNode::getRecordId)
                .last("LIMIT 1"));
        return nodes.isEmpty() ? null : nodes.get(0);
    }
    private Map<String, Object> mapBase(BaseEquipment node) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("id", String.valueOf(node.getId()));
emergency-repair-server/src/main/resources/application.yml
@@ -1,3 +1,5 @@
fault-code:
  prefix: "固定前缀-"
server:
  port: 8091
  servlet:
emergency-repair-server/src/main/resources/mapper/ReportMapper.xml
@@ -53,12 +53,14 @@
                   NVL(SUM(CASE WHEN IS_MONITOR_ITEM = 1 THEN 1 ELSE 0 END), 0) AS MONITOR_ITEM,
                   NVL(SUM(CASE WHEN TASK_STATUS = 'CANCELLED' THEN 1 ELSE 0 END), 0) AS CANCELLED
            FROM LX.T_FAULT
             WHERE DEL_FLAG = 0
        ) f
        CROSS JOIN (
            SELECT NVL(SUM(CASE WHEN COMPLETION_STATUS IS NULL OR COMPLETION_STATUS &lt;&gt; 'COMPLETED' THEN 1 ELSE 0 END), 0) AS UNFINISHED,
                   NVL(SUM(CASE WHEN COMPLETION_STATUS = 'COMPLETED' THEN 1 ELSE 0 END), 0) AS COMPLETED,
                   NVL(SUM(CASE WHEN COMPLETION_STATUS = 'WAIT_TEST' THEN 1 ELSE 0 END), 0) AS WAIT_TEST
            FROM LX.T_WORK_ORDER
             WHERE DEL_FLAG = 0
        ) w
    </select>
@@ -77,6 +79,7 @@
                  OR (NVL(f.EQUIPMENT_IDS, '') = '' AND f.EQUIPMENT_ID = p.ID)
             )
            JOIN LX.T_BASE_EQUIPMENT b ON b.ID = p.BASE_EQUIPMENT_ID
             WHERE f.DEL_FLAG = 0 AND p.DEL_FLAG = 0 AND b.DEL_FLAG = 0
        ),
        splitter AS (
            SELECT LEVEL AS N
@@ -87,6 +90,7 @@
                    '、'
                )), 0) + 1
                FROM LX.T_FAULT
                 WHERE DEL_FLAG = 0
            )
        ),
        fallback_names AS (
@@ -103,6 +107,7 @@
                  REPLACE(REPLACE(REPLACE(NVL(f.EQUIPMENT_NAME, ''), ',', '、'), ',', '、'), CHR(10), '、'),
                  '、'
              ) + 1
             WHERE f.DEL_FLAG = 0
        ),
        fallback_link AS (
            SELECT DISTINCT fn.FAULT_ID,
@@ -114,13 +119,15 @@
              ON p5.NODE_LEVEL = 5
             AND p5.NAME = fn.EQUIPMENT_NAME
             AND p5.BASE_EQUIPMENT_ID IS NOT NULL
            JOIN LX.T_PRODUCT_TREE p4 ON p4.ID = p5.PARENT_ID
            JOIN LX.T_PRODUCT_TREE p3 ON p3.ID = p4.PARENT_ID
              AND p5.DEL_FLAG = 0
             JOIN LX.T_PRODUCT_TREE p4 ON p4.ID = p5.PARENT_ID AND p4.DEL_FLAG = 0
             JOIN LX.T_PRODUCT_TREE p3 ON p3.ID = p4.PARENT_ID AND p3.DEL_FLAG = 0
            JOIN LX.T_PRODUCT_TREE x
              ON x.ID = p3.PARENT_ID
             AND x.NODE_LEVEL = 2
             AND x.NAME = f.X_NO
            JOIN LX.T_BASE_EQUIPMENT b ON b.ID = p5.BASE_EQUIPMENT_ID
              AND x.DEL_FLAG = 0
             JOIN LX.T_BASE_EQUIPMENT b ON b.ID = p5.BASE_EQUIPMENT_ID AND b.DEL_FLAG = 0
            WHERE NOT EXISTS (
                SELECT 1
                FROM explicit_link el
@@ -166,27 +173,29 @@
    <select id="selectByTeam" resultMap="teamResultMap">
        WITH fault_agg AS (
            SELECT CASE
                       WHEN TRIM(TEAM_MAJOR) IS NULL OR TRIM(TEAM_MAJOR) = '' THEN '未填写T队专业'
                       WHEN TRIM(TEAM_MAJOR) IS NULL OR TRIM(TEAM_MAJOR) = '' THEN '未填写rk_T队专业'
                       ELSE TRIM(TEAM_MAJOR)
                   END AS NAME,
                   COUNT(*) AS FAULT_COUNT
            FROM LX.T_FAULT
             WHERE DEL_FLAG = 0
            GROUP BY CASE
                         WHEN TRIM(TEAM_MAJOR) IS NULL OR TRIM(TEAM_MAJOR) = '' THEN '未填写T队专业'
                         WHEN TRIM(TEAM_MAJOR) IS NULL OR TRIM(TEAM_MAJOR) = '' THEN '未填写rk_T队专业'
                         ELSE TRIM(TEAM_MAJOR)
                     END
        ),
        parts_agg AS (
            SELECT CASE
                       WHEN f.TEAM_MAJOR IS NULL OR TRIM(f.TEAM_MAJOR) = '' THEN '未填写T队专业'
                       WHEN f.TEAM_MAJOR IS NULL OR TRIM(f.TEAM_MAJOR) = '' THEN '未填写rk_T队专业'
                       ELSE TRIM(f.TEAM_MAJOR)
                   END AS NAME,
                   NVL(SUM(NVL(ci.CONSUMABLE_COUNT, 0)), 0) AS PARTS_COUNT
            FROM LX.T_COMPLETION_INFO ci
            JOIN LX.T_WORK_ORDER w ON w.ID = ci.WORK_ORDER_ID
            LEFT JOIN LX.T_FAULT f ON f.ID = w.FAULT_ID
             JOIN LX.T_WORK_ORDER w ON w.ID = ci.WORK_ORDER_ID AND w.DEL_FLAG = 0
             LEFT JOIN LX.T_FAULT f ON f.ID = w.FAULT_ID AND f.DEL_FLAG = 0
             WHERE ci.DEL_FLAG = 0
            GROUP BY CASE
                         WHEN f.TEAM_MAJOR IS NULL OR TRIM(f.TEAM_MAJOR) = '' THEN '未填写T队专业'
                         WHEN f.TEAM_MAJOR IS NULL OR TRIM(f.TEAM_MAJOR) = '' THEN '未填写rk_T队专业'
                         ELSE TRIM(f.TEAM_MAJOR)
                     END
        ),
@@ -211,6 +220,7 @@
               NVL(SUM(CASE WHEN COMPLETION_STATUS = 'COMPLETED' THEN 1 ELSE 0 END), 0) AS COMPLETED_COUNT,
               NVL(ROUND(AVG(SCORE), 1), 0) AS AVERAGE_SCORE
        FROM LX.T_WORK_ORDER
         WHERE DEL_FLAG = 0
        GROUP BY CONTRACTOR_NAME
        ORDER BY TOTAL_COUNT DESC, CONTRACTOR_NAME ASC
    </select>
@@ -220,16 +230,16 @@
            SELECT EXTRACT(YEAR FROM REPORT_DATE) AS REPORT_YEAR,
                   COUNT(*) AS FAULT_COUNT
            FROM LX.T_FAULT
            WHERE REPORT_DATE IS NOT NULL
             WHERE DEL_FLAG = 0 AND REPORT_DATE IS NOT NULL
            GROUP BY EXTRACT(YEAR FROM REPORT_DATE)
        ),
        parts_agg AS (
            SELECT EXTRACT(YEAR FROM f.REPORT_DATE) AS REPORT_YEAR,
                   NVL(SUM(NVL(ci.CONSUMABLE_COUNT, 0)), 0) AS PARTS_COUNT
            FROM LX.T_COMPLETION_INFO ci
            JOIN LX.T_WORK_ORDER w ON w.ID = ci.WORK_ORDER_ID
            JOIN LX.T_FAULT f ON f.ID = w.FAULT_ID
            WHERE f.REPORT_DATE IS NOT NULL
             JOIN LX.T_WORK_ORDER w ON w.ID = ci.WORK_ORDER_ID AND w.DEL_FLAG = 0
             JOIN LX.T_FAULT f ON f.ID = w.FAULT_ID AND f.DEL_FLAG = 0
             WHERE ci.DEL_FLAG = 0 AND f.REPORT_DATE IS NOT NULL
            GROUP BY EXTRACT(YEAR FROM f.REPORT_DATE)
        ),
        report_years AS (
emergency-repair-web/public/logo.png
emergency-repair-web/public/sensitive-words.json
@@ -1,49 +1,49 @@
{
  "sensitive_words": [
    {
      "codename": "X号",
      "codename": "rk_X号",
      "real_name": "县号",
      "scope": "ui",
      "note": "Development mode displays the codename. Replace real_name only in the deployed production dictionary."
    },
    {
      "codename": "JC",
      "codename": "rk_JC",
      "real_name": "JC",
      "scope": "ui",
      "note": "JC信息 = JC information."
    },
    {
      "codename": "JCC",
      "codename": "rk_JCC",
      "real_name": "JCC",
      "scope": "ui",
      "note": "JCC assistant, director, and acceptance roles."
    },
    {
      "codename": "CMB",
      "codename": "rk_CMB",
      "real_name": "CMB",
      "scope": "ui",
      "note": "CMB leader approval role."
    },
    {
      "codename": "T",
      "real_name": "T",
      "scope": "ui",
      "note": "Word root. T队专业, T内施工, and T员 all use the codename in development."
    },
    {
      "codename": "TD",
      "codename": "rk_TD",
      "real_name": "TD",
      "scope": "ui",
      "note": "Home page statistic column."
    },
    {
      "codename": "XCB",
      "codename": "rk_T",
      "real_name": "T",
      "scope": "ui",
      "note": "Word root. T队专业, T内施工, and T员 all use the codename in development."
    },
    {
      "codename": "rk_XCB",
      "real_name": "修船版",
      "scope": "ui",
      "note": "XCB assistant review and dispatch."
    },
    {
      "codename": "H",
      "codename": "rk_H",
      "real_name": "H",
      "scope": "ui",
      "note": "Development mode displays H only. Replace real_name after production deployment."
@@ -51,6 +51,7 @@
  ],
  "config": {
    "environment": "development",
    "rule": "Build and deploy the normal development package. After deployment, manually replace the real_name values in the deployed dist/sensitive-words.json file in the production environment. Do not rebuild the frontend after that replacement."
    "rule": "Build and deploy the normal development package. After deployment, manually replace the real_name values in the deployed dist/sensitive-words.json file in the production environment. Do not rebuild the frontend after that replacement.",
    "code_pre": "ABC"
  }
}
emergency-repair-web/src/api/index.js
@@ -9,6 +9,8 @@
api.interceptors.request.use(config => {
  const token = sessionStorage.getItem('token')
  if (token) config.headers.Authorization = 'Bearer ' + token
  const username = sessionStorage.getItem('username')
  if (username) config.headers['X-Username'] = username
  return config
})
emergency-repair-web/src/components/MenuNode.vue
@@ -1,12 +1,12 @@
<template>
  <el-submenu v-if="item.children && item.children.length" :index="String(item.id)">
  <el-submenu v-if="item.menuType !== 2 && item.children && item.children.length" :index="String(item.id)">
    <template slot="title">
      <i :class="item.icon || 'el-icon-menu'"></i>
      <span slot="title">{{ item.name }}</span>
    </template>
    <menu-node v-for="child in item.children" :key="child.id" :item="child"></menu-node>
  </el-submenu>
  <el-menu-item v-else-if="item.url" :index="item.url">
  <el-menu-item v-else-if="item.menuType !== 2 && item.url" :index="item.url">
    <i :class="item.icon || 'el-icon-tickets'"></i>
    <span slot="title">{{ item.name }}</span>
  </el-menu-item>
emergency-repair-web/src/components/ProductTreePicker.vue
@@ -7,7 +7,9 @@
          ref="tree"
          :data="tree"
          node-key="id"
          default-expand-all
          v-loading="loading"
          lazy
          :load="loadNode"
          show-checkbox
          check-strictly
          :filter-node-method="filterNode"
@@ -51,7 +53,7 @@
    selectedIds: { type: Array, default: () => [] }
  },
  data() {
    return { localVisible: false, keyword: '', tree: [], checkedCount: 0, selectedDevices: [] }
    return { localVisible: false, keyword: '', tree: [], checkedCount: 0, selectedDevices: [], selectedMap: {}, loading: false }
  },
  computed: {
    treeProps() {
@@ -74,52 +76,71 @@
  methods: {
    load() {
      const lockedXNo = String(this.xNo || '').trim()
      api.get('/system/tree', { params: { type: 'product' } }).then(res => {
        const source = res.data || []
        this.tree = lockedXNo ? this.filterTreeByXNo(source, lockedXNo) : source
      this.loading = true
      const ids = (this.selectedIds || []).map(String).filter(Boolean)
      const treeRequest = lockedXNo
        ? api.get('/system/tree/product/side-roots', { params: { xNo: lockedXNo } })
        : api.get('/system/tree/product/roots')
      const selectedRequest = ids.length
        ? api.get('/system/tree/product/selected', { params: { ids: ids.join(',') } })
        : Promise.resolve({ data: [] })
      Promise.all([treeRequest, selectedRequest]).then(([treeRes, selectedRes]) => {
        this.tree = treeRes.data || []
        this.selectedMap = (selectedRes.data || []).reduce((map, node) => {
          map[String(node.id)] = node
          return map
        }, {})
        this.keyword = ''
        this.$nextTick(this.restoreChecked)
      })
        this.$nextTick(() => { this.restoreChecked(); this.syncSelected() })
      }).finally(() => { this.loading = false })
    },
    filterTreeByXNo(nodes, xNo) {
      const result = []
      ;(nodes || []).forEach(node => {
        if (Number(node.nodeLevel) === 2) {
          if (String(node.name || '').trim() === xNo) result.push(node)
    loadNode(node, resolve) {
      if (!node || !node.data || !node.data.id) {
        resolve([])
          return
        }
        const children = this.filterTreeByXNo(node.children || [], xNo)
        if (children.length || node.nodeType === 'DEVICE') {
          result.push(children.length ? { ...node, children } : node)
        }
      api.get('/system/tree/product/children', { params: { parentId: node.data.id } })
        .then(res => {
          resolve(res.data || [])
          this.$nextTick(this.restoreChecked)
      })
      return result
        .catch(() => resolve([]))
    },
    restoreChecked() {
      if (!this.$refs.tree) return
      const ids = (this.selectedIds || []).map(String).filter(Boolean)
      const ids = Object.keys(this.selectedMap).filter(id => Boolean(this.$refs.tree.getNode(id)))
      this.$refs.tree.setCheckedKeys(ids)
      this.syncSelected()
    },
    handleCheck(node) {
      const tree = this.$refs.tree
      const treeNode = tree && tree.getNode(node.id)
      this.syncSelected()
      if (treeNode && treeNode.checked) {
        const selected = this.deviceNodes().map(item => this.decorateNode(item))
        const xNos = Array.from(new Set(selected.map(item => item.xNo).filter(Boolean)))
        const xNos = Array.from(new Set(this.selectedDevices.map(item => item.xNo).filter(Boolean)))
        if (xNos.length > 1) {
          tree.setChecked(node.id, false, true)
          this.$message.warning('同一故障只能选择同一个X号下的设备')
        }
      }
      this.syncSelected()
          this.$message.warning('同一故障只能选择同一个rk_X号下的设备')
        }
      }
    },
    syncSelected() {
      const selected = this.deviceNodes().map(node => this.decorateNode(node))
      this.selectedDevices = selected
      this.checkedCount = selected.length
      const tree = this.$refs.tree
      if (!tree) return
      const selectedMap = { ...this.selectedMap }
      const checked = tree.getCheckedNodes().filter(node => node.nodeType === 'DEVICE')
      const checkedIds = new Set(checked.map(node => String(node.id)))
      Object.keys(selectedMap).forEach(id => {
        if (tree.getNode(id) && !checkedIds.has(id)) delete selectedMap[id]
      })
      checked.forEach(node => { selectedMap[String(node.id)] = this.decorateNode(node) })
      this.selectedMap = selectedMap
      this.selectedDevices = Object.values(selectedMap)
      this.checkedCount = this.selectedDevices.length
    },
    decorateNode(node) {
      if (!this.$refs.tree) return { ...node }
      const pathNodes = []
      let current = this.$refs.tree.getNode(node.id)
      while (current && current.data) {
@@ -134,8 +155,10 @@
      return { ...node, pathNames: pathNodes.map(item => item.name), parentPathNames, xNo: xNode ? xNode.name : '' }
    },
    removeSelected(device) {
      if (!this.$refs.tree) return
      this.$refs.tree.setChecked(device.id, false, true)
      const selectedMap = { ...this.selectedMap }
      delete selectedMap[String(device.id)]
      this.selectedMap = selectedMap
      if (this.$refs.tree && this.$refs.tree.getNode(device.id)) this.$refs.tree.setChecked(device.id, false, true)
      this.syncSelected()
    },
    deviceNodes() {
@@ -147,12 +170,12 @@
      return (data.name || '').includes(value) || (data.code || '').includes(value)
    },
    confirm() {
      const nodes = this.deviceNodes()
      const nodes = this.selectedDevices
      if (!nodes.length) return this.$message.warning('请选择设备')
      const selected = nodes.map(node => this.decorateNode(node))
      const selected = nodes
      const xNos = Array.from(new Set(selected.map(item => item.xNo).filter(Boolean)))
      if (xNos.length > 1) {
        return this.$message.warning('同一故障只能选择同一个X号下的设备')
        return this.$message.warning('同一故障只能选择同一个rk_X号下的设备')
      }
      this.$emit('select', selected)
      this.localVisible = false
emergency-repair-web/src/components/RepairLogo.vue
@@ -1,32 +1,23 @@
<template>
  <svg class="repair-logo" viewBox="0 0 30 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <rect class="screwdriver-handle" x="5" y="2" width="3.2" height="6" rx="1.2"/>
    <path d="M5.7 4.2h1.8"/>
    <path d="M5.7 6.2h1.8"/>
    <path d="M6.6 8v7"/>
    <path d="M5.6 15h2"/>
    <path d="M5.6 17.5h2"/>
    <g transform="translate(5 -2) scale(.82)">
      <path vector-effect="non-scaling-stroke" d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
    </g>
  </svg>
  <img class="repair-logo" :src="logoSrc" alt="临抢修管理系统">
</template>
<script>
export default {
  name: 'RepairLogo'
  name: 'RepairLogo',
  computed: {
    logoSrc() {
      return (process.env.BASE_URL || '/') + 'logo.png'
    }
  }
}
</script>
<style scoped>
.repair-logo {
  display: block;
  width: 1.30em;
  height: 1.03em;
  overflow: visible;
}
.screwdriver-handle {
  fill: currentColor;
  fill-opacity: .16;
  width: 1.2em;
  height: 1.2em;
  object-fit: contain;
}
</style>
emergency-repair-web/src/layout/AppLayout.vue
@@ -65,9 +65,9 @@
}
const REPORT_TITLES = {
  overview: '装备故障总体分析',
  overview: '设备故障总体分析',
  'top-equipment': '故障高发设备Top',
  'by-team': '按T统计',
  'by-team': 'rk_按T统计',
  contractors: '厂家维修情况',
  trend: '年度趋势'
}
@@ -259,9 +259,12 @@
  overflow-wrap: anywhere;
  word-break: break-word;
}
.mdi-tabbar { display: flex; flex: 0 0 40px; align-items: stretch; height: 40px; padding-top: 5px; border-bottom: 1px solid #cfdae5; background: #e3eaf1; }
.mdi-tabs-scroll { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; }
.mdi-tabs { display: inline-flex; align-items: flex-end; min-width: 100%; height: 100%; padding: 0 4px; }
.mdi-tabbar { display: flex; flex: 0 0 50px; align-items: stretch; height: 50px; padding-top: 5px; border-bottom: 1px solid #cfdae5; background: #e3eaf1; }
.mdi-tabs-scroll { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; scrollbar-color: #9fb1c1 transparent; }
.mdi-tabs-scroll::-webkit-scrollbar { height: 6px; }
.mdi-tabs-scroll::-webkit-scrollbar-track { background: transparent; }
.mdi-tabs-scroll::-webkit-scrollbar-thumb { border-radius: 6px; background: #9fb1c1; }
.mdi-tabs { display: inline-flex; align-items: flex-end; min-width: 100%; height: 35px; padding: 0 4px; }
.mdi-tab { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; max-width: 220px; height: 34px; margin-right: 2px; padding: 0 9px; border: 1px solid transparent; border-bottom: 0; border-radius: 4px 4px 0 0; color: #5c6f82; background: rgba(255, 255, 255, .42); cursor: pointer; font-size: 13px; user-select: none; }
.mdi-tab:hover { color: #245275; background: rgba(255, 255, 255, .72); }
.mdi-tab.is-active { height: 35px; color: #1f3348; background: #eef2f6; border-color: #cfdae5; box-shadow: inset 0 2px 0 #2c7fb8; }
emergency-repair-web/src/main.js
@@ -11,6 +11,26 @@
Vue.prototype.$appName = '临抢修管理系统'
Vue.prototype.$hasPermission = function(code) {
  const raw = sessionStorage.getItem('permissions')
  if (raw === null) return true
  try {
    const list = JSON.parse(raw)
    if (!Array.isArray(list)) return true
    return list.indexOf(code) >= 0
  } catch (e) {
    return true
  }
}
Vue.directive('permission', {
  inserted(el, binding) {
    if (binding.value && !Vue.prototype.$hasPermission(binding.value) && el.parentNode) {
      el.parentNode.removeChild(el)
    }
  }
})
new Vue({
  router,
  render: h => h(App)
emergency-repair-web/src/router/index.js
@@ -13,11 +13,11 @@
    children: [
      { path: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '首页门户', cacheName: 'Dashboard' } },
      { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue'), meta: { title: '故障上报', cacheName: 'FaultReport' } },
      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'XCB助理审核', cacheName: 'XcbReview' } },
      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'CMB领导审批', stage: 'CMB', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'JCC助理审批', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'JCC处长审批', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } },
      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'XCB助理派单', cacheName: 'DispatchPage' } },
      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'rk_XCB助理审核', cacheName: 'XcbReview' } },
      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'rk_CMB领导审批', stage: 'CMB', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'rk_JCC助理审批', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } },
      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'rk_JCC处长审批', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } },
      { 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: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'APPLICATION' }, meta: { title: '验收申请', stage: 'APPLICATION', cacheName: 'AcceptanceList' } },
@@ -27,11 +27,15 @@
      { path: 'acceptance/rating', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'RATING' }, meta: { title: '修后质量评级', stage: 'RATING', cacheName: 'AcceptanceList' } },
      { path: 'reports/:type', component: () => import('@/views/report/ReportPage.vue'), props: route => ({ type: route.params.type }), meta: { title: '统计报表', cacheName: 'ReportPage' } },
      { path: 'system/contractors', component: () => import('@/views/system/ContractorManage.vue'), meta: { title: '承修单位管理', cacheName: 'ContractorManage' } },
      { path: 'system/organization', component: () => import('@/views/system/OrganizationManage.vue'), meta: { title: '组织机构', cacheName: 'OrganizationManage' } },
      { path: 'system/product-tree', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'product' }, meta: { title: '产品结构树管理', treeType: 'product', cacheName: 'TreeManage' } },
      { path: 'system/base-equipment', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'base' }, meta: { title: '设备基础库管理', treeType: 'base', cacheName: 'TreeManage' } },
      { path: 'system/engineering-majors', component: () => import('@/views/system/EngineeringMajorManage.vue'), meta: { title: '工程专业配置', cacheName: 'EngineeringMajorManage' } },
      { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue'), meta: { title: 'T队专业配置', cacheName: 'TeamMajorManage' } },
      { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue'), meta: { title: 'rk_T队专业配置', cacheName: 'TeamMajorManage' } },
      { path: 'system/users', component: () => import('@/views/system/UserManage.vue'), meta: { title: '用户管理', cacheName: 'UserManage' } },
      { 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/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/dict.js
@@ -1,11 +1,11 @@
export const taskStatuses = {
  UNREPORTED: { text: '未上报', type: 'info' },
  REPORTED_XCB: { text: '已上报XCB', type: 'warning' },
  REPORTED_XCB: { text: '已上报rk_XCB', type: 'warning' },
  RETURNED: { text: '上级打回', type: 'danger' },
  XCB_REVIEWED: { text: 'XCB助理审核完成', type: 'primary' },
  CMB_APPROVED: { text: 'CMB领导审批完成', type: 'success' },
  JCC_ASSISTANT_APPROVED: { text: 'JCC助理审批完成', type: 'success' },
  JCC_DIRECTOR_APPROVED: { text: 'JCC处长已阅', type: 'success' },
  XCB_REVIEWED: { text: 'rk_XCB助理审核完成', type: 'primary' },
  CMB_APPROVED: { text: 'rk_CMB领导审批完成', type: 'success' },
  JCC_ASSISTANT_APPROVED: { text: 'rk_JCC助理审批完成', type: 'success' },
  JCC_DIRECTOR_APPROVED: { text: 'rk_JCC处长已阅', type: 'success' },
  DISPATCHED: { text: '已派单', type: 'primary' },
  CANCELLED: { text: '已销项', type: 'info' }
}
emergency-repair-web/src/utils/sensitive.js
@@ -143,6 +143,24 @@
  })
}
function expandPrefixedAliases(items) {
  const expanded = []
  const seen = {}
  items.forEach(function (item) {
    const names = [item.codename]
    if (item.codename.indexOf('rk_') === 0 && item.codename.length > 3) {
      names.push(item.codename.substring(3))
    }
    names.forEach(function (codename) {
      if (!codename || seen[codename]) {
        return
      }
      seen[codename] = true
      expanded.push({ codename: codename, real_name: item.real_name })
    })
  })
  return expanded
}
export async function installSensitiveWords() {
  try {
    const baseUrl = process.env.BASE_URL || '/'
@@ -158,7 +176,8 @@
      .filter(function (item) {
        return item && item.codename && isConfigured(item.real_name, item.codename)
      })
      .sort(function (left, right) {
    replacements = expandPrefixedAliases(replacements).sort(function (left, right) {
        return right.codename.length - left.codename.length
      })
emergency-repair-web/src/views/Dashboard.vue
@@ -19,7 +19,7 @@
    <div class="dashboard-grid">
      <section class="dashboard-panel chart-panel">
        <div class="panel-head">
          <h3>装备故障总体分析</h3>
          <h3>设备故障总体分析</h3>
          <span>上方柱状图 / ä¸‹æ–¹ç»Ÿè®¡</span>
        </div>
        <div ref="overviewChart" class="chart-box"></div>
@@ -28,7 +28,7 @@
        <div class="panel-head"><h3>常用功能</h3></div>
        <div class="quick-list">
          <button @click="$router.push('/fault/report')"><i class="el-icon-edit-outline"></i><span>故障上报</span></button>
          <button @click="$router.push('/fault/review')"><i class="el-icon-s-check"></i><span>XCB助理审核</span></button>
          <button @click="$router.push('/fault/review')"><i class="el-icon-s-check"></i><span>rk_XCB助理审核</span></button>
          <button @click="$router.push('/fault/dispatch')"><i class="el-icon-s-promotion"></i><span>一键派单</span></button>
          <button @click="$router.push('/fault/completion')"><i class="el-icon-finished"></i><span>完工处理</span></button>
          <button @click="$router.push('/acceptance/application')"><i class="el-icon-document-checked"></i><span>验收申请</span></button>
@@ -39,16 +39,16 @@
    <section class="dashboard-panel">
      <div class="panel-head">
        <h3>按X号统计</h3>
        <span>按X号汇总当前任务和完工状态</span>
        <h3>按rk_X号统计</h3>
        <span>按rk_X号汇总当前任务和完工状态</span>
      </div>
      <el-table :data="xStats" border size="small">
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="currentStatus" label="当前状态" width="110">
          <template slot-scope="{ row }"><el-tag size="mini" type="info">{{ row.currentStatus }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="berth" label="停靠泊位" width="120"></el-table-column>
        <el-table-column prop="team" label="TD" min-width="130"></el-table-column>
        <el-table-column prop="team" label="rk_TD" min-width="130"></el-table-column>
        <el-table-column prop="total" label="工程总数" align="center"></el-table-column>
        <el-table-column prop="cancelled" label="销项总数" align="center"></el-table-column>
        <el-table-column prop="major" label="重大故障" align="center"></el-table-column>
@@ -111,7 +111,7 @@
      return [
        { label: '工程总数', value: this.overview.engineeringTotal || 0, note: '当前故障任务' },
        { label: '重大故障', value: this.overview.majorFault || 0, note: '需重点关注' },
        { label: '涉H统计', value: this.overview.hRelated || 0, note: '敏感标识任务' },
        { label: '涉rk_H统计', value: this.overview.hRelated || 0, note: '敏感标识任务' },
        { label: '未完工', value: this.overview.unfinished || 0, note: '派工单口径' },
        { label: '完工待试验', value: this.overview.waitTest || 0, note: '待试验工单' },
        { label: '已完工', value: this.overview.completed || 0, note: '可进入验收' }
emergency-repair-web/src/views/Login.vue
@@ -16,7 +16,7 @@
        </el-form-item>
        <el-button type="primary" class="login-btn" :loading="loading" @click="submit">登录系统</el-button>
      </el-form>
      <div class="login-tip">第一版暂不启用角色控制,使用 admin / admin123 ä½“验全部功能。</div>
      <div class="login-tip">请使用已分配的系统账号登录。</div>
    </div>
  </div>
</template>
@@ -31,7 +31,7 @@
  data() {
    return {
      loading: false,
      form: { username: 'admin', password: 'admin123' },
      form: { username: '', password: '' },
      rules: {
        username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
        password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
@@ -48,6 +48,8 @@
          sessionStorage.setItem('username', res.data.username)
          sessionStorage.setItem('realName', res.data.realName)
          sessionStorage.setItem('deptName', res.data.deptName || '')
          sessionStorage.setItem('roles', JSON.stringify(res.data.roles || []))
          sessionStorage.setItem('permissions', JSON.stringify(res.data.permissions || []))
          this.$router.push('/dashboard')
        }).finally(() => { this.loading = false })
      })
emergency-repair-web/src/views/acceptance/AcceptanceList.vue
@@ -9,7 +9,7 @@
    </div>
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="派工单编号 / X号 / æ‰¿ä¿®å•位" clearable style="width:280px" @keyup.enter.native="load"></el-input>
      <el-input v-model="keyword" placeholder="派工单编号 / rk_X号 / æ‰¿ä¿®å•位" clearable style="width:280px" @keyup.enter.native="load"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <div class="spacer"></div>
      <el-tag type="info" size="small">仅完工状态的派工单可进入验收流程</el-tag>
@@ -21,7 +21,7 @@
        <el-table-column prop="status" label="验收状态" width="155" fixed>
          <template slot-scope="{ row }"><el-tag size="mini" :type="acceptanceMeta(row.status).type">{{ acceptanceMeta(row.status).text }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="contractorName" label="承修单位" min-width="170"></el-table-column>
        <el-table-column prop="stage" label="当前环节" width="150">
          <template slot-scope="{ row }">{{ stageText(row.stage) }}</template>
emergency-repair-web/src/views/completion/CompletionList.vue
@@ -3,7 +3,7 @@
    <div class="page-title">
      <div>
        <h2>完工情况处理</h2>
        <p>每一行代表一张派工单,只允许勾选一行;完工状态由业务按钮维护</p>
        <p>每一行代表一张派工单,只允许勾选一行;点“完工”只更新完工状态,完工信息由承修单位用户另行填报</p>
      </div>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
    </div>
@@ -19,11 +19,11 @@
    </div>
    <div class="toolbar action-toolbar">
      <el-button type="success" icon="el-icon-finished" :disabled="!selectedRow || selectedRow.completionStatus === 'COMPLETED'" @click="goCompletion">完工</el-button>
      <el-button type="success" icon="el-icon-finished" :disabled="!selectedRow || selectedRow.completionStatus === 'COMPLETED'" @click="setStatus('COMPLETED')">完工</el-button>
      <el-button type="primary" icon="el-icon-time" :disabled="!canSetStatus" @click="setStatus('WAIT_TEST')">完工待试验</el-button>
      <el-button icon="el-icon-refresh-left" :disabled="!canSetStatus" @click="setStatus('UNFINISHED')">未完工</el-button>
      <el-button type="warning" icon="el-icon-box" :disabled="!canSetStatus" @click="setStatus('WAIT_PARTS')">待备件</el-button>
      <el-button type="primary" plain icon="el-icon-star-on" :disabled="!selectedRow" @click="openScore">T员评分</el-button>
      <el-button type="primary" plain icon="el-icon-star-on" :disabled="!selectedRow" @click="openScore">rk_T员评分</el-button>
    </div>
    <div class="table-panel">
@@ -35,15 +35,15 @@
        <el-table-column prop="completionStatus" label="完工状态" width="115" fixed>
          <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="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="contractorName" label="派单单位" min-width="160"></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="T队专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="parentSystem" label="所属上级系统" min-width="130"></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="120"></el-table-column>
        <el-table-column prop="reportDate" label="上报时间" width="110"></el-table-column>
        <el-table-column prop="reviewTime" label="XCB审核时间" width="160">
        <el-table-column prop="reviewTime" label="rk_XCB审核时间" width="160">
          <template slot-scope="{ row }">{{ formatTime(row.reviewTime) }}</template>
        </el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
@@ -59,7 +59,7 @@
      </el-table>
    </div>
    <el-dialog title="T员评分" :visible.sync="scoreVisible" width="520px" append-to-body>
    <el-dialog title="rk_T员评分" :visible.sync="scoreVisible" width="520px" append-to-body>
      <el-form :model="scoreForm" label-width="100px">
        <el-form-item label="派工单"><b>{{ scoreForm.workOrderNo }}</b></el-form-item>
        <el-form-item label="修后质量评分"><el-rate v-model="scoreForm.score" show-text :texts="['很差','较差','一般','良好','优秀']"></el-rate></el-form-item>
@@ -117,9 +117,6 @@
        this.$message.success('完工状态已更新')
        this.load()
      }).catch(() => {})
    },
    goCompletion() {
      this.$router.push({ path: '/completion/report', query: { workOrderId: this.selectedRow.id } })
    },
    openScore() {
      this.scoreForm = {
emergency-repair-web/src/views/completion/CompletionReport.vue
@@ -3,15 +3,15 @@
    <div class="page-title">
      <div>
        <h2>完工信息上报</h2>
        <p>从完工情况处理页点“完工”进入;提交成功后派工单完工状态写为“完工”,并进入验收申请</p>
        <p>仅显示已在完工情况处理中确认完工、分配给本单位且尚未填报的派工单;提交后进入完工验收流程</p>
      </div>
      <el-button icon="el-icon-back" @click="$router.push('/fault/completion')">返回完工处理</el-button>
    </div>
    <div class="toolbar">
      <span>派工单</span>
      <el-select v-model="form.workOrderId" filterable placeholder="请选择派工单" style="width:360px" :disabled="!!routeWorkOrderId" @change="loadSelected">
        <el-option v-for="item in selectableOrders" :key="item.id" :label="item.workOrderNo + ' - ' + item.contractorName" :value="item.id"></el-option>
      <el-select v-model="form.workOrderId" filterable placeholder="请选择派工单" style="width:560px" :disabled="!!routeWorkOrderId" no-data-text="没有可填报的派工单(需已确认完工、分配给本单位且尚未填报)" @change="loadSelected">
        <el-option v-for="item in selectableOrders" :key="item.id" :label="item.workOrderNo + ' - ' + (item.engineeringNameDesc || '无工程名称') + ' - ' + item.contractorName" :value="item.id"></el-option>
      </el-select>
      <div class="spacer"></div>
      <el-tag v-if="workOrder" size="small" type="info">{{ workOrder.xNo }} Â· {{ workOrder.equipmentName }}</el-tag>
@@ -21,13 +21,13 @@
      <section class="form-section">
        <h3>基础信息</h3>
        <el-row :gutter="16">
          <el-col :span="8"><el-form-item label="X号"><el-input v-model="form.xNo" readonly></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="rk_X号"><el-input v-model="form.xNo" readonly></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="承修单位"><el-input v-model="form.contractorName" readonly></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="工程编号"><el-input v-model="form.engineeringNo"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="开工时间"><el-date-picker v-model="form.startDate" type="date" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="完工时间"><el-date-picker v-model="form.finishDate" type="date" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="承修人"><el-input v-model="form.contractorPerson"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="T内施工"><el-input v-model="form.tInternalConstruction"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="rk_T内施工"><el-input v-model="form.tInternalConstruction"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="牵连工程"><el-input v-model="form.relatedProject"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="完工耗时(天)"><el-input-number v-model="form.durationDays" :min="0" style="width:100%"></el-input-number></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="承修人数"><el-input-number v-model="form.personCount" :min="0" style="width:100%"></el-input-number></el-form-item></el-col>
@@ -141,7 +141,7 @@
      }
    },
    loadSelectable() {
      api.get('/work-orders', { params: { status: 'UNFINISHED,WAIT_PARTS,WAIT_TEST' } }).then(res => { this.selectableOrders = res.data || [] })
      api.get('/work-orders', { params: { completionReportable: true } }).then(res => { this.selectableOrders = res.data || [] })
    },
    loadSelected() {
      if (!this.form.workOrderId) return
emergency-repair-web/src/views/fault/DispatchPage.vue
@@ -2,14 +2,14 @@
  <div class="page">
    <div class="page-title">
      <div>
        <h2>XCB助理派单</h2>
        <p>显示JCC处长审批完成且未派单的故障,按承修单位一键生成派工单</p>
        <h2>rk_XCB助理派单</h2>
        <p>显示rk_JCC处长审批完成且未派单的故障,按承修单位一键生成派工单</p>
      </div>
      <el-button type="primary" icon="el-icon-s-promotion" :disabled="selection.length === 0" @click="dispatch">一键派单</el-button>
    </div>
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="故障编号 / è£…备名称 / å·¥ç¨‹åç§°åŠæŸåæƒ…况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-input v-model="keyword" placeholder="故障编号 / è®¾å¤‡åç§° / å·¥ç¨‹åç§°åŠæŸåæƒ…况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
      <div class="spacer"></div>
@@ -22,17 +22,17 @@
      <el-table :data="rows" border stripe height="calc(100vh - 325px)" @selection-change="selection = $event">
        <el-table-column type="selection" width="46"></el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column>
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="parentSystem" label="所属上级系统" min-width="130"></el-table-column>
        <el-table-column prop="contractorNames" label="承修单位" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="120"></el-table-column>
        <el-table-column prop="reportDate" label="上报时间" width="110"></el-table-column>
        <el-table-column prop="xcbReviewTime" label="XCB审核时间" width="160">
        <el-table-column prop="xcbReviewTime" label="rk_XCB审核时间" width="160">
          <template slot-scope="{ row }">{{ formatTime(row.xcbReviewTime) }}</template>
        </el-table-column>
        <el-table-column prop="teamMajor" label="T队专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
        <el-table-column prop="requestedFinishDate" label="要求完工日期" width="120"></el-table-column>
        <el-table-column prop="faultImpact" label="故障影响" width="100"></el-table-column>
@@ -47,7 +47,7 @@
        <el-table-column prop="workOrderNo" label="派工单编号" width="160"></el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180"></el-table-column>
        <el-table-column prop="contractorName" label="承修单位" min-width="180"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="completionStatus" label="完工状态" width="100">
          <template slot-scope="{ row }"><el-tag size="mini" type="info">{{ completionMeta(row.completionStatus).text }}</el-tag></template>
        </el-table-column>
emergency-repair-web/src/views/fault/FaultEditDialog.vue
@@ -5,14 +5,14 @@
        <el-col :span="8"><el-form-item label="故障编号"><el-input v-model="form.faultCode" readonly placeholder="保存后自动生成"></el-input></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="上报单位"><el-input v-model="form.reportUnit"></el-input></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="上报日期"><el-date-picker v-model="form.reportDate" type="date" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="X号"><el-select v-model="form.xNo" filterable placeholder="请选择X号" style="width:100%"><el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option></el-select></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="rk_X号"><el-select v-model="form.xNo" filterable placeholder="请选择rk_X号" style="width:100%"><el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option></el-select></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="负责人"><el-input v-model="form.owner"></el-input></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="T队专业"><el-select v-model="form.teamMajor" style="width:100%"><el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="rk_T队专业"><el-select v-model="form.teamMajor" style="width:100%"><el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="工程专业"><el-select v-model="form.engineeringMajor" style="width:100%"><el-option v-for="item in engineeringMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
        <el-col :span="16">
          <el-form-item label="装备名称" prop="equipmentName">
          <el-form-item label="设备名称" prop="equipmentName">
            <el-input v-model="form.equipmentName" readonly :title="form.equipmentName" placeholder="点击右侧按钮从产品结构树选设备,可多选">
              <el-button slot="append" icon="el-icon-s-grid" @click="pickerVisible = true">选择装备</el-button>
              <el-button slot="append" icon="el-icon-s-grid" @click="pickerVisible = true">选择设备</el-button>
            </el-input>
            <div v-if="selectedEquipmentIds.length" class="field-tip">已选 {{ selectedEquipmentIds.length }} å°è®¾å¤‡</div>
          </el-form-item>
@@ -25,7 +25,7 @@
        <el-col :span="8"><el-form-item label="是否自修"><el-switch v-model="form.isSelfRepair" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
      </el-row>
      <el-row :gutter="16">
        <el-col :span="8"><el-form-item label="是否涉H"><el-switch v-model="form.isHRelated" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="是否涉rk_H"><el-switch v-model="form.isHRelated" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="是否监测项目"><el-switch v-model="form.isMonitorItem" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
        <el-col :span="8"><el-form-item label="监测数值"><el-input v-model="form.monitorValue" :disabled="form.isMonitorItem !== 1"></el-input></el-form-item></el-col>
        <el-col :span="24"><el-form-item label="重大故障描述"><el-input v-model="form.majorFaultDesc" type="textarea" :rows="2"></el-input></el-form-item></el-col>
@@ -63,7 +63,7 @@
      natures: ['一般', '重大'],
      form: {},
      rules: {
        equipmentName: [{ required: true, message: '请选择装备名称', trigger: 'change' }],
        equipmentName: [{ required: true, message: '请选择设备名称', trigger: 'change' }],
        engineeringNameDesc: [{ required: true, message: '请填写工程名称及损坏情况', trigger: 'blur' }]
      }
    }
@@ -98,18 +98,7 @@
      api.get('/system/engineering-majors').then(res => { this.engineeringMajors = res.data || [] })
    },
    loadXNumbers() {
      api.get('/system/tree', { params: { type: 'product' } }).then(res => {
        const values = []
        const walk = nodes => {
          if (!nodes) return
          nodes.forEach(node => {
            if (node.nodeType === 'X_NO' || node.nodeLevel === 2) values.push(node.name)
            walk(node.children)
          })
        }
        walk(res.data)
        this.xNumbers = Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b, 'zh-CN'))
      })
      api.get('/system/tree/product/x-numbers').then(res => { this.xNumbers = res.data || [] })
    },
    onEquipmentSelected(nodes) {
      const selected = Array.isArray(nodes) ? nodes : [nodes]
emergency-repair-web/src/views/fault/FaultReport.vue
@@ -3,17 +3,17 @@
    <div class="page-title">
      <div>
        <h2>故障上报</h2>
        <p>T员录入装备信息和修理要求,故障编号保存后按规则自动生成</p>
        <p>rk_T员录入设备信息和修理要求,故障编号保存后按规则自动生成</p>
      </div>
      <el-button icon="el-icon-plus" type="primary" @click="openCreate">新增故障</el-button>
    </div>
    <div class="toolbar query-toolbar">
      <span class="filter-label">X号</span>
      <span class="filter-label">rk_X号</span>
      <el-select v-model="query.xNo" placeholder="全部" clearable style="width:100px">
        <el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option>
      </el-select>
      <span class="filter-label">T队专业</span>
      <span class="filter-label">rk_T队专业</span>
      <el-select v-model="query.teamMajor" placeholder="全部" clearable style="width:130px">
        <el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option>
      </el-select>
@@ -40,7 +40,7 @@
      <el-select v-model="query.status" placeholder="全部" clearable style="width:160px">
        <el-option v-for="(meta, key) in taskStatuses" :key="key" :label="meta.text" :value="key"></el-option>
      </el-select>
      <el-input v-model="query.keyword" placeholder="故障编号/装备名称/工程名称及损坏情况" clearable style="width:320px" @keyup.enter.native="search"></el-input>
      <el-input v-model="query.keyword" placeholder="故障编号/设备名称/工程名称及损坏情况" clearable style="width:320px" @keyup.enter.native="search"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
      <el-button icon="el-icon-refresh-left" @click="resetQuery">重置</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
@@ -62,13 +62,13 @@
          <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column>
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="parentSystem" label="所属上级系统" min-width="120"></el-table-column>
        <el-table-column prop="engineeringNameDesc" label="工程名称及损坏情况" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="repairRequirement" label="修理技术要求" min-width="200" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="T队专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="130"></el-table-column>
        <el-table-column prop="reportDate" label="上报日期" width="110" align="center"></el-table-column>
@@ -78,7 +78,7 @@
        <el-table-column label="是否自修" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isSelfRepair) }}</template>
        </el-table-column>
        <el-table-column label="是否涉H" width="90" align="center">
        <el-table-column label="是否涉rk_H" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isHRelated) }}</template>
        </el-table-column>
        <el-table-column label="监测项目" width="90" align="center">
@@ -109,14 +109,14 @@
          <el-col :span="8"><el-form-item label="故障编号"><el-input v-model="form.faultCode" readonly placeholder="保存后自动生成"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="上报单位"><el-input v-model="form.reportUnit"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="上报日期"><el-date-picker v-model="form.reportDate" type="date" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="X号"><el-select v-model="form.xNo" filterable placeholder="请选择X号" style="width:100%"><el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option></el-select></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="rk_X号"><el-select v-model="form.xNo" filterable placeholder="请选择rk_X号" style="width:100%"><el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option></el-select></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="负责人"><el-input v-model="form.owner"></el-input></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="T队专业"><el-select v-model="form.teamMajor" style="width:100%"><el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="rk_T队专业"><el-select v-model="form.teamMajor" style="width:100%"><el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="工程专业"><el-select v-model="form.engineeringMajor" style="width:100%"><el-option v-for="item in engineeringMajors" :key="item.id" :label="item.name" :value="item.name"></el-option></el-select></el-form-item></el-col>
          <el-col :span="16">
            <el-form-item label="装备名称" prop="equipmentName">
            <el-form-item label="设备名称" prop="equipmentName">
              <el-input v-model="form.equipmentName" readonly :title="form.equipmentName" placeholder="点击右侧按钮从产品结构树选设备,可多选">
                <el-button slot="append" icon="el-icon-s-grid" @click="pickerVisible = true">选择装备</el-button>
                <el-button slot="append" icon="el-icon-s-grid" @click="pickerVisible = true">选择设备</el-button>
              </el-input>
              <div v-if="selectedEquipmentIds.length" class="field-tip">已选 {{ selectedEquipmentIds.length }} å°è®¾å¤‡</div>
            </el-form-item>
@@ -129,7 +129,7 @@
          <el-col :span="8"><el-form-item label="是否自修"><el-switch v-model="form.isSelfRepair" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
        </el-row>
        <el-row :gutter="16">
          <el-col :span="8"><el-form-item label="是否涉H"><el-switch v-model="form.isHRelated" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="是否涉rk_H"><el-switch v-model="form.isHRelated" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="是否监测项目"><el-switch v-model="form.isMonitorItem" :active-value="1" :inactive-value="0"></el-switch></el-form-item></el-col>
          <el-col :span="8"><el-form-item label="监测数值"><el-input v-model="form.monitorValue" :disabled="form.isMonitorItem !== 1"></el-input></el-form-item></el-col>
          <el-col :span="24"><el-form-item label="重大故障描述"><el-input v-model="form.majorFaultDesc" type="textarea" :rows="2"></el-input></el-form-item></el-col>
@@ -171,7 +171,7 @@
      natures: ['一般', '重大'],
      form: {},
      rules: {
        equipmentName: [{ required: true, message: '请选择装备名称', trigger: 'change' }],
        equipmentName: [{ required: true, message: '请选择设备名称', trigger: 'change' }],
        engineeringNameDesc: [{ required: true, message: '请填写工程名称及损坏情况', trigger: 'blur' }]
      }
    }
@@ -212,18 +212,7 @@
      })
    },
    loadXNumbers() {
      api.get('/system/tree', { params: { type: 'product' } }).then(res => {
        const values = []
        const walk = nodes => {
          if (!nodes) return
          nodes.forEach(node => {
            if (node.nodeType === 'X_NO' || node.nodeLevel === 2) values.push(node.name)
            walk(node.children)
          })
        }
        walk(res.data)
        this.xNumbers = Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b, 'zh-CN'))
      })
      api.get('/system/tree/product/x-numbers').then(res => { this.xNumbers = res.data || [] })
    },
    queryParams() {
      const range = this.query.reportDateRange || []
@@ -282,7 +271,7 @@
        reportUnit: sessionStorage.getItem('deptName') || '',
        reportDate: today,
        xNo: '',
        owner: sessionStorage.getItem('realName') || 'T员',
        owner: sessionStorage.getItem('realName') || 'rk_T员',
        teamMajor: (this.teamMajors[0] || {}).name || '',
        engineeringMajor: (this.engineeringMajors[0] || {}).name || '',
        equipmentIds: '',
@@ -361,7 +350,7 @@
    exportCsv() {
      api.get('/faults', { params: this.queryParams() }).then(res => {
        const rows = res.data || []
        const headers = ['故障编号', 'X号', '装备名称', '任务状态', '工程名称及损坏情况', '上报单位', '上报日期']
        const headers = ['故障编号', 'rk_X号', '设备名称', '任务状态', '工程名称及损坏情况', '上报单位', '上报日期']
        const lines = [headers.join(',')].concat(rows.map(row => [
          row.faultCode, row.xNo, row.equipmentName, this.taskMeta(row.taskStatus).text,
          row.engineeringNameDesc, row.reportUnit, row.reportDate
emergency-repair-web/src/views/fault/SimpleApproval.vue
@@ -9,7 +9,7 @@
    </div>
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="故障编号 / è£…备名称 / å·¥ç¨‹åç§°åŠæŸåæƒ…况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-input v-model="keyword" placeholder="故障编号 / è®¾å¤‡åç§° / å·¥ç¨‹åç§°åŠæŸåæƒ…况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-button icon="el-icon-search" type="primary" @click="load">查询</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
      <div class="spacer"></div>
@@ -23,13 +23,13 @@
          <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column>
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="parentSystem" label="所属上级系统" min-width="120"></el-table-column>
        <el-table-column prop="engineeringNameDesc" label="工程名称及损坏情况" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="repairRequirement" label="修理技术要求" min-width="200" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="T队专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="130"></el-table-column>
        <el-table-column prop="reportDate" label="上报日期" width="110" align="center"></el-table-column>
@@ -39,7 +39,7 @@
        <el-table-column label="是否自修" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isSelfRepair) }}</template>
        </el-table-column>
        <el-table-column label="是否涉H" width="90" align="center">
        <el-table-column label="是否涉rk_H" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isHRelated) }}</template>
        </el-table-column>
        <el-table-column label="监测项目" width="90" align="center">
@@ -66,7 +66,7 @@
        <el-form-item label="审批意见">
          <el-input v-model="form.opinion" type="textarea" :rows="3" placeholder="请填写审批意见"></el-input>
        </el-form-item>
        <el-alert v-if="form.result === 'REJECT'" :closable="false" type="warning" show-icon title="审批不通过时直接退回XCB助理,任务状态变为“上级打回”。"></el-alert>
        <el-alert v-if="form.result === 'REJECT'" :closable="false" type="warning" show-icon title="审批不通过时直接退回rk_XCB助理,任务状态变为“上级打回”。"></el-alert>
      </el-form>
      <template slot="footer">
        <el-button @click="dialogVisible = false">取消</el-button>
@@ -89,13 +89,13 @@
  },
  computed: {
    pageTitle() {
      return { CMB: 'CMB领导审批', JCC_ASSISTANT: 'JCC助理审批', JCC_DIRECTOR: 'JCC处长审批' }[this.stage]
      return { CMB: 'rk_CMB领导审批', JCC_ASSISTANT: 'rk_JCC助理审批', JCC_DIRECTOR: 'rk_JCC处长审批' }[this.stage]
    },
    subtitle() {
      return {
        CMB: '对XCB助理审核完成的故障进行批量通过或退回',
        JCC_ASSISTANT: '对CMB领导审批完成的故障进行批量通过或退回',
        JCC_DIRECTOR: '对JCC助理审批完成的故障进行最终审批,通过后进入派单'
        CMB: '对rk_XCB助理审核完成的故障进行批量通过或退回',
        JCC_ASSISTANT: '对rk_CMB领导审批完成的故障进行批量通过或退回',
        JCC_DIRECTOR: '对rk_JCC助理审批完成的故障进行最终审批,通过后进入派单'
      }[this.stage]
    },
    status() {
emergency-repair-web/src/views/fault/XcbReview.vue
@@ -2,17 +2,17 @@
  <div class="page">
    <div class="page-title">
      <div>
        <h2>XCB助理审核</h2>
        <p>分配承修单位、跟踪技术队和要求完工日期,审核后进入CMB领导审批</p>
        <h2>rk_XCB助理审核</h2>
        <p>分配承修单位、跟踪技术队和要求完工日期,审核后进入rk_CMB领导审批</p>
      </div>
    </div>
    <div class="toolbar query-toolbar">
      <span class="filter-label">X号</span>
      <span class="filter-label">rk_X号</span>
      <el-select v-model="query.xNo" placeholder="全部" clearable style="width:100px">
        <el-option v-for="item in xNumbers" :key="item" :label="item" :value="item"></el-option>
      </el-select>
      <span class="filter-label">T队专业</span>
      <span class="filter-label">rk_T队专业</span>
      <el-select v-model="query.teamMajor" placeholder="全部" clearable style="width:130px">
        <el-option v-for="item in teamMajors" :key="item.id" :label="item.name" :value="item.name"></el-option>
      </el-select>
@@ -46,7 +46,7 @@
      <el-select v-model="query.status" placeholder="全部" clearable style="width:180px">
        <el-option v-for="(meta, key) in taskStatuses" :key="key" :label="meta.text" :value="key"></el-option>
      </el-select>
      <el-input v-model="query.keyword" placeholder="故障编号/装备名称/工程名称及损坏情况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-input v-model="query.keyword" placeholder="故障编号/设备名称/工程名称及损坏情况" clearable style="width:320px" @keyup.enter.native="load"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <el-button icon="el-icon-refresh-left" @click="resetQuery">重置</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
@@ -67,13 +67,13 @@
          <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column>
        <el-table-column prop="xNo" label="X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="装备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="xNo" label="rk_X号" width="64"></el-table-column>
        <el-table-column prop="equipmentName" label="设备名称" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="parentSystem" label="所属上级系统" min-width="120"></el-table-column>
        <el-table-column prop="engineeringNameDesc" label="工程名称及损坏情况" min-width="210" show-overflow-tooltip></el-table-column>
        <el-table-column prop="repairRequirement" label="修理技术要求" min-width="200" show-overflow-tooltip></el-table-column>
        <el-table-column prop="engineeringMajor" label="工程专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="T队专业" width="100"></el-table-column>
        <el-table-column prop="teamMajor" label="rk_T队专业" width="100"></el-table-column>
        <el-table-column prop="owner" label="负责人" width="90"></el-table-column>
        <el-table-column prop="reportUnit" label="上报单位" min-width="130"></el-table-column>
        <el-table-column prop="reportDate" label="上报日期" width="110" align="center"></el-table-column>
@@ -83,7 +83,7 @@
        <el-table-column label="是否自修" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isSelfRepair) }}</template>
        </el-table-column>
        <el-table-column label="是否涉H" width="90" align="center">
        <el-table-column label="是否涉rk_H" width="90" align="center">
          <template slot-scope="{ row }">{{ yesNo(row.isHRelated) }}</template>
        </el-table-column>
        <el-table-column label="监测项目" width="90" align="center">
@@ -97,7 +97,7 @@
    <fault-edit-dialog :visible.sync="editVisible" :fault="editingFault" @saved="onFaultSaved"></fault-edit-dialog>
    <el-dialog title="XCB助理审核" :visible.sync="dialogVisible" width="820px" append-to-body>
    <el-dialog title="rk_XCB助理审核" :visible.sync="dialogVisible" width="820px" append-to-body>
      <el-alert :closable="false" type="info" show-icon title="承修单位可多选。排序前7个作为常用承修单位优先显示,失信单位标记提醒。" style="margin-bottom:14px"></el-alert>
      <el-form :model="form" label-width="105px">
        <el-form-item label="已选故障">
@@ -252,18 +252,7 @@
      api.get('/system/engineering-majors').then(res => { this.engineeringMajors = res.data || [] })
    },
    loadXNumbers() {
      api.get('/system/tree', { params: { type: 'product' } }).then(res => {
        const values = []
        const walk = nodes => {
          if (!nodes) return
          nodes.forEach(node => {
            if (node.nodeType === 'X_NO' || node.nodeLevel === 2) values.push(node.name)
            walk(node.children)
          })
        }
        walk(res.data)
        this.xNumbers = Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b, 'zh-CN'))
      })
      api.get('/system/tree/product/x-numbers').then(res => { this.xNumbers = res.data || [] })
    },
    editSelected() {
      if (this.selection.length !== 1) return
@@ -303,7 +292,7 @@
      api.post('/faults/cancel', {
        ids: this.selection.map(item => item.id),
        reason: this.cancelReason.trim(),
        cancelledBy: sessionStorage.getItem('realName') || 'XCB助理'
        cancelledBy: sessionStorage.getItem('realName') || 'rk_XCB助理'
      }).then(() => {
        this.$message.success('销项完成')
        this.cancelVisible = false
emergency-repair-web/src/views/report/ReportPage.vue
@@ -11,7 +11,7 @@
    <div v-if="type === 'overview'" class="stat-grid">
      <div class="stat-card"><div class="label">工程总数</div><div class="value">{{ stats.engineeringTotal || 0 }}</div></div>
      <div class="stat-card"><div class="label">重大故障</div><div class="value">{{ stats.majorFault || 0 }}</div></div>
      <div class="stat-card"><div class="label">涉H统计</div><div class="value">{{ stats.hRelated || 0 }}</div></div>
      <div class="stat-card"><div class="label">涉rk_H统计</div><div class="value">{{ stats.hRelated || 0 }}</div></div>
      <div class="stat-card"><div class="label">监测项目</div><div class="value">{{ stats.monitorItem || 0 }}</div></div>
      <div class="stat-card"><div class="label">未完工</div><div class="value">{{ stats.unfinished || 0 }}</div></div>
      <div class="stat-card"><div class="label">已完工</div><div class="value">{{ stats.completed || 0 }}</div></div>
@@ -47,9 +47,9 @@
  computed: {
    title() {
      return {
        overview: '装备故障总体分析',
        overview: '设备故障总体分析',
        'top-equipment': '故障高发设备Top',
        'by-team': '按T统计',
        'by-team': 'rk_按T统计',
        contractors: '厂家维修情况',
        trend: '故障数 / å¤‡å“å¤‡ä»¶æ¶ˆè€—年度趋势'
      }[this.type] || '统计报表'
@@ -76,7 +76,7 @@
        { prop: 'value', label: '故障数', width: 100 }
      ]
      if (this.type === 'by-team') return [
        { prop: 'name', label: 'T队专业', width: 150 },
        { prop: 'name', label: 'rk_T队专业', width: 150 },
        { prop: 'faultCount', label: '故障数' },
        { prop: 'partsCount', label: '备品备件消耗' }
      ]
emergency-repair-web/src/views/system/ContractorManage.vue
@@ -3,52 +3,52 @@
    <div class="page-title">
      <div>
        <h2>承修单位管理</h2>
        <p>排序用于XCB助理审核时显示前7个常用承修单位</p>
        <p>数据来源:组织机构部门表(DEPT_TYPE = cj)</p>
      </div>
      <el-button type="primary" icon="el-icon-plus" @click="openCreate">新增承修单位</el-button>
    </div>
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="名称 / ç®€ç§° / è”系人" clearable style="width:260px"></el-input>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
    </div>
    <div class="toolbar">
      <el-input
        v-model="keyword"
        placeholder="名称 / éƒ¨é—¨ç¼–码 / è´Ÿè´£äºº"
        clearable
        style="width:280px"
        @keyup.enter.native="load"
      ></el-input>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <div class="spacer"></div>
      <span class="summary">共 {{ filteredRows.length }} ä¸ªæ‰¿ä¿®å•位</span>
    </div>
    <div class="table-panel">
      <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)">
      <el-table
        v-loading="loading"
        :data="filteredRows"
        border
        stripe
        height="calc(100vh - 245px)"
      >
        <el-table-column prop="sortNo" label="排序" width="80" align="center"></el-table-column>
        <el-table-column label="常用" width="80" align="center">
          <template slot-scope="{ $index }"><el-tag v-if="$index < 7" size="mini" type="success">前7</el-tag><span v-else>-</span></template>
          <template slot-scope="{ $index }">
            <el-tag v-if="$index < 7" size="mini" type="success">前7</el-tag>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="name" label="名称" min-width="220"></el-table-column>
        <el-table-column prop="shortName" label="简称" width="150"></el-table-column>
        <el-table-column prop="nature" label="单位性质" width="130"></el-table-column>
        <el-table-column prop="contact" label="联系人" width="120"></el-table-column>
        <el-table-column prop="dishonest" label="是否失信单位" width="130">
          <template slot-scope="{ row }"><el-tag size="mini" :type="row.dishonest === 1 ? 'danger' : 'success'">{{ row.dishonest === 1 ? '是' : '否' }}</el-tag></template>
        </el-table-column>
        <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip></el-table-column>
        <el-table-column label="操作" width="130" fixed="right">
        <el-table-column prop="name" label="名称" min-width="240" show-overflow-tooltip></el-table-column>
        <el-table-column prop="deptCode" label="部门编码" width="160"></el-table-column>
        <el-table-column prop="contact" label="负责人" width="120"></el-table-column>
        <el-table-column prop="phone" label="联系电话" width="160"></el-table-column>
        <el-table-column label="状态" width="100" align="center">
          <template slot-scope="{ row }">
            <el-button type="text" @click="openEdit(row)">编辑</el-button>
            <el-button type="text" class="danger-link" @click="remove(row)">删除</el-button>
            <el-tag size="mini" :type="row.status === '1' ? 'info' : 'success'">
              {{ row.status === '1' ? '停用' : '正常' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-dialog :title="form.id ? '编辑承修单位' : '新增承修单位'" :visible.sync="dialogVisible" width="620px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
        <el-form-item label="名称" prop="name"><el-input v-model="form.name"></el-input></el-form-item>
        <el-form-item label="简称" prop="shortName"><el-input v-model="form.shortName"></el-input></el-form-item>
        <el-form-item label="单位性质"><el-select v-model="form.nature" style="width:100%"><el-option v-for="item in natures" :key="item" :label="item" :value="item"></el-option></el-select></el-form-item>
        <el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="1" style="width:180px"></el-input-number></el-form-item>
        <el-form-item label="联系人"><el-input v-model="form.contact"></el-input></el-form-item>
        <el-form-item label="是否失信单位"><el-switch v-model="form.dishonest" :active-value="1" :inactive-value="0"></el-switch></el-form-item>
        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3"></el-input></el-form-item>
      </el-form>
      <template slot="footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
    </el-dialog>
  </div>
</template>
<script>
@@ -57,42 +57,37 @@
  name: 'ContractorManage',
  data() {
    return {
      rows: [], keyword: '', dialogVisible: false, saving: false,
      natures: ['厂家', '服务商', '综合单位', 'T队', '其他'],
      form: {},
      rules: {
        name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
        shortName: [{ required: true, message: '请输入简称', trigger: 'blur' }]
      }
      loading: false,
      rows: [],
      keyword: ''
    }
  },
  computed: {
    filteredRows() {
      if (!this.keyword) return this.rows
      const text = this.keyword.toLowerCase()
      return this.rows.filter(item => [item.name, item.shortName, item.contact].some(value => String(value || '').toLowerCase().includes(text)))
      const keyword = this.keyword.trim().toLowerCase()
      if (!keyword) return this.rows
      return this.rows.filter(item => [item.name, item.deptCode, item.contact]
        .some(value => String(value || '').toLowerCase().includes(keyword)))
    }
  },
  created() { this.load() },
  methods: {
    load() { api.get('/system/contractors').then(res => { this.rows = res.data || [] }) },
    blank() { return { name: '', shortName: '', nature: '厂家', sortNo: this.rows.length + 1, contact: '', dishonest: 0, remark: '' } },
    openCreate() { this.form = this.blank(); this.dialogVisible = true },
    openEdit(row) { this.form = { ...row }; this.dialogVisible = true },
    save() {
      this.$refs.form.validate(valid => {
        if (!valid) return
        this.saving = true
        const request = this.form.id ? api.put('/system/contractors/' + this.form.id, this.form) : api.post('/system/contractors', this.form)
        request.then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.load() }).finally(() => { this.saving = false })
      })
  created() {
    this.load()
    },
    remove(row) {
      this.$confirm(`确定删除“${row.name}”吗?`, '删除确认', { type: 'warning' }).then(() => api.delete('/system/contractors/' + row.id)).then(() => {
        this.$message.success('删除成功'); this.load()
      }).catch(() => {})
  methods: {
    load() {
      this.loading = true
      api.get('/system/contractors').then(res => {
        this.rows = res.data || []
      }).finally(() => {
        this.loading = false
      })
    }
  }
}
</script>
<style scoped>.danger-link { color: #d9534f; }</style>
<style scoped>
.summary {
  color: #6d7f91;
  font-size: 13px;
}
</style>
emergency-repair-web/src/views/system/LoginLogManage.vue
New file
@@ -0,0 +1,96 @@
<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:200px" @keyup.enter.native="search"></el-input>
        <el-select v-model="filters.status" clearable placeholder="登录结果" style="width:140px">
          <el-option label="成功" value="SUCCESS"></el-option>
          <el-option label="失败" value="FAIL"></el-option>
        </el-select>
        <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:login-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="loginTime" label="登录时间" width="180"></el-table-column>
        <el-table-column prop="userName" label="账户名" min-width="140"></el-table-column>
        <el-table-column prop="nickName" label="姓名" min-width="140"></el-table-column>
        <el-table-column prop="deptName" label="所属部门" min-width="180" show-overflow-tooltip></el-table-column>
        <el-table-column prop="loginIp" label="登录IP" min-width="150"></el-table-column>
        <el-table-column label="结果" width="90" align="center">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="row.status === 'SUCCESS' ? 'success' : 'danger'">{{ row.status === 'SUCCESS' ? '成功' : '失败' }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="message" label="说明" min-width="180" show-overflow-tooltip></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>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'LoginLogManage',
  data() {
    return {
      rows: [],
      total: 0,
      current: 1,
      size: 20,
      loading: false,
      filters: { keyword: '', status: '', dates: [] }
    }
  },
  created() { this.load() },
  methods: {
    load() {
      this.loading = true
      const dates = this.filters.dates || []
      api.get('/system/login-logs', {
        params: {
          keyword: this.filters.keyword || undefined,
          status: this.filters.status || 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: '', status: '', dates: [] }; this.search() },
    handleCurrentChange(page) { this.current = page; this.load() },
    handleSizeChange(size) { this.size = size; this.current = 1; this.load() }
  }
}
</script>
<style scoped>
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.pager { margin-top: 12px; text-align: right; }
</style>
emergency-repair-web/src/views/system/MenuManage.vue
@@ -1,33 +1,80 @@
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>菜单管理</h2><p>菜单为动态配置,左侧导航直接读取这里的数据</p></div>
      <el-button type="primary" icon="el-icon-plus" @click="openCreate(null)">新增一级菜单</el-button>
      <div><h2>菜单管理</h2><p>支持目录、菜单、按钮三种类型;按钮只能挂在菜单下级,用于配置页面操作权限</p></div>
      <el-button type="primary" icon="el-icon-plus" v-permission="'system:menus:add'" @click="openCreate(null)">新增一级菜单</el-button>
    </div>
    <div class="table-panel">
      <el-table :data="tree" row-key="id" border default-expand-all :tree-props="{ children: 'children' }">
        <el-table-column prop="name" label="菜单名称" min-width="220"></el-table-column>
        <el-table-column prop="icon" label="图标" width="200"><template slot-scope="{ row }"><span class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span></template></el-table-column>
        <el-table-column prop="url" label="路由地址" min-width="240"></el-table-column>
        <el-table-column prop="permissions" label="权限标识" min-width="170"></el-table-column>
        <el-table-column prop="menuType" label="类型" width="90"><template slot-scope="{ row }"><el-tag size="mini" :type="row.menuType === 0 ? 'info' : 'primary'">{{ row.menuType === 0 ? '目录' : '菜单' }}</el-tag></template></el-table-column>
        <el-table-column prop="icon" label="图标" width="180">
          <template slot-scope="{ row }">
            <span v-if="row.menuType !== 2" class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span>
            <span v-else class="muted">-</span>
          </template>
        </el-table-column>
        <el-table-column prop="url" label="路由地址" min-width="220">
          <template slot-scope="{ row }">
            <span v-if="row.menuType !== 2">{{ row.url }}</span>
            <span v-else class="muted">-</span>
          </template>
        </el-table-column>
        <el-table-column prop="permissions" label="权限标识" min-width="200"></el-table-column>
        <el-table-column label="类型" width="90">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="typeTag(row.menuType)">{{ typeName(row.menuType) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="sortNo" label="排序" width="80" align="center"></el-table-column>
        <el-table-column prop="visible" label="显示" width="80" align="center"><template slot-scope="{ row }"><el-tag size="mini" :type="row.visible === 0 ? 'info' : 'success'">{{ row.visible === 0 ? '隐藏' : '显示' }}</el-tag></template></el-table-column>
        <el-table-column label="操作" width="200" fixed="right"><template slot-scope="{ row }"><el-button type="text" @click="openCreate(row)">新增下级</el-button><el-button type="text" @click="openEdit(row)">编辑</el-button><el-button type="text" class="danger-link" @click="remove(row)">删除</el-button></template></el-table-column>
        <el-table-column label="显示" width="80" align="center">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="row.visible === 0 ? 'info' : 'success'">{{ row.visible === 0 ? '隐藏' : '显示' }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="270" fixed="right">
          <template slot-scope="{ row }">
            <el-button v-if="row.menuType !== 2" type="text" v-permission="'system:menus:add'" @click="openCreate(row)">新增下级</el-button>
            <el-button v-if="row.menuType === 1" type="text" v-permission="'system:menus:buttons'" @click="openButtons(row)">页面按钮</el-button>
            <el-button type="text" v-permission="'system:menus:edit'" @click="openEdit(row)">编辑</el-button>
            <el-button type="text" class="danger-link" v-permission="'system:menus:delete'" @click="remove(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <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-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>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="菜单名称" prop="name"><el-input v-model="form.name"></el-input></el-form-item>
        <el-form-item label="菜单类型"><el-radio-group v-model="form.menuType"><el-radio-button :label="0">目录</el-radio-button><el-radio-button :label="1">菜单</el-radio-button></el-radio-group></el-form-item>
        <el-form-item label="路由地址"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item>
        <el-form-item label="图标"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item>
        <el-form-item label="权限标识"><el-input v-model="form.permissions"></el-input></el-form-item>
        <el-form-item v-if="form.menuType !== 2" label="路由地址"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item>
        <el-form-item v-if="form.menuType !== 2" label="图标"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item>
        <el-form-item label="权限标识" :prop="form.menuType === 2 ? 'permissions' : ''">
          <el-input v-model="form.permissions" :placeholder="form.menuType === 2 ? '例如 fault:report:add' : '可选,例如 fault-report'"></el-input>
        </el-form-item>
        <el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="1" style="width:160px"></el-input-number></el-form-item>
        <el-form-item label="是否显示"><el-switch v-model="form.visible" :active-value="1" :inactive-value="0"></el-switch></el-form-item>
      </el-form>
      <template slot="footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
    </el-dialog>
    <el-dialog :title="'页面按钮配置 - ' + (buttonMenu.name || '')" :visible.sync="buttonDialogVisible" width="620px" append-to-body>
      <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>
          <span class="button-code">{{ item.code }}</span>
        </el-checkbox>
      </el-checkbox-group>
      <div v-if="!buttonAvailable.length" class="empty-tip">该页面暂未登记可用按钮</div>
      <template slot="footer">
        <el-button @click="buttonDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="buttonSaving" @click="saveButtons">保存按钮</el-button>
      </template>
    </el-dialog>
  </div>
</template>
@@ -35,19 +82,130 @@
import api from '@/api'
export default {
  name: 'MenuManage',
  data() { return { tree: [], flat: [], dialogVisible: false, saving: false, form: {}, rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] } } },
  computed: { parentName() { const parent = this.flat.find(item => item.id === this.form.pid); return parent ? parent.name : '无(一级菜单)' } },
  data() {
    return {
      tree: [],
      flat: [],
      dialogVisible: false,
      saving: false,
      form: {},
      parent: null,
      rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] },
      buttonDialogVisible: false,
      buttonSaving: false,
      buttonMenu: {},
      buttonAvailable: [],
      buttonSelected: []
    }
  },
  computed: {
    parentName() {
      if (!this.parent) return this.form.pid && this.form.pid !== 0 ? '上级菜单' : '无(一级菜单)'
      return this.parent.name
    },
    typeOptions() {
      if (this.parent && this.parent.menuType === 1) {
        return [{ label: '按钮', value: 2 }]
      }
      if (this.parent && this.parent.menuType === 0) {
        return [{ label: '目录', value: 0 }, { label: '菜单', value: 1 }]
      }
      if (this.form.id && this.form.menuType === 2) {
        return [{ label: '按钮', value: 2 }]
      }
      return [{ label: '目录', value: 0 }, { label: '菜单', value: 1 }]
    }
  },
  created() { this.load() },
  methods: {
    load() { Promise.all([api.get('/system/menu/nav'), api.get('/system/menu/list')]).then(([tree, list]) => { this.tree = tree.data || []; this.flat = list.data || [] }) },
    openCreate(parent) { this.form = { pid: parent ? parent.id : 0, name: '', url: '', icon: 'el-icon-tickets', menuType: parent ? 1 : 0, permissions: '', sortNo: 10, visible: 1 }; this.dialogVisible = true },
    openEdit(row) { this.form = { ...row }; this.dialogVisible = true },
    save() { this.$refs.form.validate(valid => { if (!valid) return; this.saving = true; const request = this.form.id ? api.put('/system/menu/' + this.form.id, this.form) : api.post('/system/menu', this.form); request.then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.load() }).finally(() => { this.saving = false }) }) },
    remove(row) { this.$confirm(`确定删除菜单“${row.name}”吗?`, '删除确认', { type: 'warning' }).then(() => api.delete('/system/menu/' + row.id)).then(() => { this.$message.success('删除成功'); this.load() }).catch(() => {}) }
    load() {
      Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list')]).then(([tree, list]) => {
        this.tree = tree.data || []
        this.flat = list.data || []
      })
    },
    typeName(type) {
      return type === 2 ? '按钮' : type === 0 ? '目录' : '菜单'
    },
    typeTag(type) {
      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,
        name: '',
        url: '',
        icon: menuType === 2 ? '' : 'el-icon-tickets',
        menuType,
        permissions: '',
        sortNo: 10,
        visible: 1
      }
      this.dialogVisible = true
    },
    openEdit(row) {
      this.parent = this.flat.find(item => item.id === row.pid) || null
      this.form = { ...row }
      this.dialogVisible = true
    },
    save() {
      this.$refs.form.validate(valid => {
        if (!valid) return
        if (this.form.menuType === 2 && !this.form.permissions) {
          this.$message.error('按钮权限标识不能为空')
          return
        }
        const payload = { ...this.form }
        if (payload.menuType === 2) {
          payload.url = null
          payload.icon = null
        }
        delete payload.children
        this.saving = true
        const request = payload.id ? api.put('/system/menu/' + payload.id, payload) : api.post('/system/menu', payload)
        request.then(() => {
          this.$message.success('保存成功')
          this.dialogVisible = false
          this.load()
        }).finally(() => { this.saving = false })
      })
    },
    remove(row) {
      this.$confirm(`确定删除“${row.name}”吗?`, '删除确认', { type: 'warning' })
        .then(() => api.delete('/system/menu/' + row.id))
        .then(() => { this.$message.success('删除成功'); this.load() })
        .catch(() => {})
    },
    openButtons(row) {
      this.buttonMenu = row
      api.get('/system/menu/buttons', { params: { menuId: row.id } }).then(res => {
        const data = res.data || {}
        this.buttonAvailable = data.available || []
        this.buttonSelected = data.selected || []
        this.buttonDialogVisible = true
      })
    },
    saveButtons() {
      this.buttonSaving = true
      api.post('/system/menu/buttons', { menuId: this.buttonMenu.id, codes: this.buttonSelected }).then(() => {
        this.$message.success('页面按钮已同步')
        this.buttonDialogVisible = false
        this.load()
      }).finally(() => { this.buttonSaving = false })
    }
  }
}
</script>
<style scoped>
.danger-link { color: #d9534f; }
.icon-cell { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.muted { color: #a3afbc; }
.dialog-tip { margin: 0 0 12px; color: #738296; font-size: 13px; }
.button-list { display: flex; flex-direction: column; gap: 2px; }
.button-item { margin-right: 0; }
.button-name { color: #31465d; }
.button-code { margin-left: 8px; color: #93a2b1; font-size: 12px; }
.empty-tip { padding: 30px 0; color: #93a2b1; text-align: center; }
</style>
emergency-repair-web/src/views/system/MenuPermission.vue
@@ -15,11 +15,11 @@
    <div class="permission-grid">
      <section class="permission-panel">
        <div class="panel-head"><h3>租户菜单授权</h3><el-checkbox v-model="tenantCheckAll" @change="checkAll('tenant')">全选</el-checkbox></div>
        <el-tree ref="tenantTree" :data="menus" node-key="id" show-checkbox check-strictly default-expand-all :props="{ label: 'name', children: 'children' }"></el-tree>
        <el-tree ref="tenantTree" :data="menus" node-key="id" show-checkbox default-expand-all :render-content="renderContent" :props="{ label: 'name', children: 'children' }" @check="syncCheckAllState('tenant')"></el-tree>
      </section>
      <section class="permission-panel">
        <div class="panel-head"><h3>角色菜单授权</h3><el-checkbox v-model="roleCheckAll" @change="checkAll('role')">全选</el-checkbox></div>
        <el-tree ref="roleTree" :data="menus" node-key="id" show-checkbox check-strictly default-expand-all :props="{ label: 'name', children: 'children' }"></el-tree>
        <el-tree ref="roleTree" :data="menus" node-key="id" show-checkbox default-expand-all :render-content="renderContent" :props="{ label: 'name', children: 'children' }" @check="syncCheckAllState('role')"></el-tree>
      </section>
      <section class="permission-panel intersection-panel">
        <div class="panel-head"><h3>交集预览</h3></div>
@@ -34,14 +34,16 @@
  name: 'MenuPermission',
  data() { return { menus: [], flatMenus: [], roles: [], tenants: [], tenantId: 1, roleId: null, saving: false, tenantCheckAll: false, roleCheckAll: false, selectedTenantIds: [], selectedRoleIds: [] } },
  computed: {
    allMenuIds() { return this.flatMenus.map(item => item.id) },
    allMenuIds() { return this.flatMenus.map(item => String(item.id)) },
    parentMenuIds() { const ids = new Set(); this.flatMenus.forEach(item => ids.add(String(item.pid))); return ids },
    leafMenuIds() { return this.flatMenus.filter(item => !this.parentMenuIds.has(String(item.id))).map(item => String(item.id)) },
    effectiveCount() { return this.effectiveMenus.length },
    effectiveMenus() { return this.flatMenus.filter(item => this.selectedTenantIds.includes(item.id) && this.selectedRoleIds.includes(item.id)) }
    effectiveMenus() { return this.flatMenus.filter(item => this.selectedTenantIds.map(String).includes(String(item.id)) && this.selectedRoleIds.map(String).includes(String(item.id))) }
  },
  created() { this.loadBase() },
  methods: {
    loadBase() {
      Promise.all([api.get('/system/menu/nav'), api.get('/system/menu/list'), api.get('/system/roles'), api.get('/system/tenants')]).then(([tree, flat, roles, tenants]) => {
      Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list'), api.get('/system/roles'), api.get('/system/tenants')]).then(([tree, flat, roles, tenants]) => {
        this.menus = tree.data || []; this.flatMenus = flat.data || []; this.roles = roles.data || []; this.tenants = tenants.data || []
        if (!this.roleId && this.roles.length) this.roleId = this.roles[0].id
        this.$nextTick(this.loadPermissions)
@@ -54,19 +56,28 @@
        this.selectedTenantIds = data.tenantMenuIds || []
        this.selectedRoleIds = data.roleMenuIds || []
        this.$nextTick(() => {
          this.$refs.tenantTree.setCheckedKeys(this.selectedTenantIds)
          this.$refs.roleTree.setCheckedKeys(this.selectedRoleIds)
          this.$refs.tenantTree.setCheckedKeys(this.leafKeys(this.selectedTenantIds))
          this.$refs.roleTree.setCheckedKeys(this.leafKeys(this.selectedRoleIds))
          this.syncCheckAllState('tenant'); this.syncCheckAllState('role')
        })
      })
    },
    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)
    },
    leafKeys(ids) { const leafIds = new Set(this.leafMenuIds); return (ids || []).map(String).filter(id => leafIds.has(id)) },
    checkedMenuIds(ref) { return [...new Set([...(ref.getCheckedKeys(false) || []), ...(ref.getHalfCheckedKeys() || [])].map(String))] },
    syncCheckAllState(target) { const ref = target === 'tenant' ? this.$refs.tenantTree : this.$refs.roleTree; if (!ref) return; const checked = this.checkedMenuIds(ref).length === this.allMenuIds.length; if (target === 'tenant') this.tenantCheckAll = checked; if (target === 'role') this.roleCheckAll = checked },
    checkAll(target) {
      const checked = target === 'tenant' ? this.tenantCheckAll : this.roleCheckAll
      const ref = target === 'tenant' ? this.$refs.tenantTree : this.$refs.roleTree
      ref.setCheckedKeys(checked ? this.allMenuIds : [])
      this.$nextTick(() => this.syncCheckAllState(target))
    },
    save() {
      const tenantMenuIds = this.$refs.tenantTree.getCheckedKeys()
      const roleMenuIds = this.$refs.roleTree.getCheckedKeys()
      const tenantMenuIds = this.checkedMenuIds(this.$refs.tenantTree)
      const roleMenuIds = this.checkedMenuIds(this.$refs.roleTree)
      this.saving = true
      api.post('/system/menu-permissions', { tenantId: this.tenantId, roleId: this.roleId, tenantMenuIds, roleMenuIds }).then(() => {
        this.selectedTenantIds = tenantMenuIds; this.selectedRoleIds = roleMenuIds; this.$message.success('菜单权限已保存')
@@ -76,6 +87,8 @@
}
</script>
<style scoped>
::v-deep .perm-button-node { color: #b8791f; }
.permission-grid { display: grid; grid-template-columns: 1fr 1fr 320px; gap: 12px; }
.permission-panel { min-height: calc(100vh - 220px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; }
.panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
emergency-repair-web/src/views/system/OperationLogManage.vue
New file
@@ -0,0 +1,104 @@
<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:230px" @keyup.enter.native="search"></el-input>
        <el-select v-model="filters.module" clearable placeholder="模块" style="width:160px">
          <el-option v-for="item in modules" :key="item" :label="item" :value="item"></el-option>
        </el-select>
        <el-select v-model="filters.operationType" clearable placeholder="操作类型" style="width:150px">
          <el-option v-for="item in operationTypes" :key="item" :label="item" :value="item"></el-option>
        </el-select>
        <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:operation-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="createTime" label="操作时间" width="180"></el-table-column>
        <el-table-column prop="userName" label="账户名" width="130"></el-table-column>
        <el-table-column prop="nickName" label="姓名" width="120"></el-table-column>
        <el-table-column prop="module" label="模块" width="130"></el-table-column>
        <el-table-column prop="operationType" label="操作类型" width="110" align="center"></el-table-column>
        <el-table-column prop="targetType" label="目标类型" width="120"></el-table-column>
        <el-table-column prop="targetId" label="目标ID" width="180" show-overflow-tooltip></el-table-column>
        <el-table-column label="结果" width="80" align="center">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="row.result === '成功' ? 'success' : 'danger'">{{ row.result }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="requestMethod" label="方式" width="80" align="center"></el-table-column>
        <el-table-column prop="requestUri" label="请求地址" min-width="240" show-overflow-tooltip></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>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'OperationLogManage',
  data() {
    return {
      rows: [],
      total: 0,
      current: 1,
      size: 20,
      loading: false,
      modules: ['故障管理', '完工管理', '完工验收', '统计报表', '用户管理', '角色管理', '菜单管理', '承修单位管理', '系统管理', '其他'],
      operationTypes: ['新增', '修改', '删除', '授权', '审批', '派单', '销项', '提交', '申请', '打印', '评级', '菜单按钮配置'],
      filters: { keyword: '', module: '', operationType: '', dates: [] }
    }
  },
  created() { this.load() },
  methods: {
    load() {
      this.loading = true
      const dates = this.filters.dates || []
      api.get('/system/operation-logs', {
        params: {
          keyword: this.filters.keyword || undefined,
          module: this.filters.module || undefined,
          operationType: this.filters.operationType || 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: '', operationType: '', dates: [] }; this.search() },
    handleCurrentChange(page) { this.current = page; this.load() },
    handleSizeChange(size) { this.size = size; this.current = 1; this.load() }
  }
}
</script>
<style scoped>
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.pager { margin-top: 12px; text-align: right; }
</style>
emergency-repair-web/src/views/system/OrganizationManage.vue
New file
@@ -0,0 +1,127 @@
<template>
  <div class="page">
    <div class="page-title">
      <div>
        <h2>组织机构</h2>
        <p>部门树表来源于 SYS_DEPT;DEPT_TYPE ä¸º cj çš„部门即承修单位</p>
      </div>
      <el-button icon="el-icon-refresh" @click="load">刷新</el-button>
    </div>
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="部门名称 / éƒ¨é—¨ç¼–码 / è´Ÿè´£äºº" clearable style="width:280px"></el-input>
      <el-select v-model="deptType" clearable placeholder="部门类型" style="width:160px">
        <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
      </el-select>
      <el-button type="primary" icon="el-icon-search" @click="load">查询</el-button>
      <div class="spacer"></div>
      <span class="summary">共 {{ rows.length }} ä¸ªéƒ¨é—¨ï¼Œå…¶ä¸­æ‰¿ä¿®å•位 {{ contractorCount }} ä¸ª</span>
    </div>
    <div class="table-panel">
      <el-table
        v-loading="loading"
        :data="filteredTree"
        row-key="id"
        border
        stripe
        default-expand-all
        height="calc(100vh - 245px)"
        :tree-props="{ children: 'children' }"
      >
        <el-table-column prop="name" label="部门名称" min-width="260"></el-table-column>
        <el-table-column prop="deptCode" label="部门编码" width="150"></el-table-column>
        <el-table-column label="部门类型" width="130" align="center">
          <template slot-scope="{ row }">
            <el-tag v-if="row.deptType === 'cj'" size="mini" type="success">承修单位</el-tag>
            <el-tag v-else size="mini" type="info">{{ row.deptType || '-' }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="leader" label="负责人" width="120"></el-table-column>
        <el-table-column prop="phone" label="联系电话" width="150"></el-table-column>
        <el-table-column prop="orderNum" label="排序" width="80" align="center"></el-table-column>
        <el-table-column label="状态" width="90" align="center">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="row.status === '1' ? 'info' : 'success'">{{ row.status === '1' ? '停用' : '正常' }}</el-tag>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'OrganizationManage',
  data() {
    return {
      loading: false,
      rows: [],
      keyword: '',
      deptType: ''
    }
  },
  computed: {
    typeOptions() {
      const values = Array.from(new Set(this.rows.map(item => item.deptType).filter(Boolean))).sort()
      return values.map(value => ({ value, label: value === 'cj' ? '承修单位' : value }))
    },
    contractorCount() {
      return this.rows.filter(item => item.deptType === 'cj').length
    },
    tree() {
      return this.buildTree(this.rows)
    },
    filteredTree() {
      const keyword = this.keyword.trim().toLowerCase()
      const type = this.deptType
      const hit = node => {
        const textOk = !keyword || [node.name, node.deptCode, node.leader]
          .some(value => String(value || '').toLowerCase().includes(keyword))
        const typeOk = !type || node.deptType === type
        return textOk && typeOk
      }
      const walk = nodes => nodes.reduce((result, node) => {
        const children = walk(node.children || [])
        if (hit(node) || children.length) {
          result.push({ ...node, children })
        }
        return result
      }, [])
      return walk(this.tree)
    }
  },
  created() { this.load() },
  methods: {
    load() {
      this.loading = true
      api.get('/system/depts').then(res => {
        this.rows = res.data || []
      }).finally(() => { this.loading = false })
    },
    buildTree(rows) {
      const nodes = rows.map(row => ({ ...row, children: [] }))
      const map = new Map(nodes.map(node => [String(node.id), node]))
      const roots = []
      nodes.forEach(node => {
        const parentId = node.parentId == null ? null : String(node.parentId)
        if (parentId && parentId !== String(node.id) && map.has(parentId)) {
          map.get(parentId).children.push(node)
        } else {
          roots.push(node)
        }
      })
      const sortNodes = list => {
        list.sort((left, right) => (Number(left.orderNum) || 0) - (Number(right.orderNum) || 0)
          || String(left.name || '').localeCompare(String(right.name || ''), 'zh'))
        list.forEach(item => sortNodes(item.children || []))
      }
      sortNodes(roots)
      return roots
    }
  }
}
</script>
<style scoped>
.summary { color: #6d7f91; font-size: 13px; }
</style>
emergency-repair-web/src/views/system/RoleManage.vue
@@ -1,8 +1,8 @@
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>角色管理</h2><p>第一版暂不启用角色控制,页面和数据结构为后续权限启用预留</p></div>
      <el-button type="primary" icon="el-icon-plus" @click="openCreate">新增角色</el-button>
      <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">
      <el-table :data="rows" border stripe height="calc(100vh - 190px)">
@@ -10,19 +10,75 @@
        <el-table-column prop="name" label="角色名称" min-width="180"></el-table-column>
        <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="260" show-overflow-tooltip></el-table-column>
        <el-table-column label="操作" width="130" fixed="right"><template slot-scope="{ row }"><el-button type="text" @click="openEdit(row)">编辑</el-button><el-button type="text" class="danger-link" @click="remove(row)">删除</el-button></template></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">
          <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:edit'" @click="openEdit(row)">编辑</el-button>
            <el-button type="text" class="danger-link" v-permission="'system:roles:delete'" @click="remove(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-dialog :title="form.id ? '编辑角色' : '新增角色'" :visible.sync="dialogVisible" width="560px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="95px">
        <el-form-item label="角色名称" prop="name"><el-input v-model="form.name"></el-input></el-form-item>
        <el-form-item label="角色编码" prop="code"><el-input v-model="form.code"></el-input></el-form-item>
        <el-form-item label="角色类型"><el-select v-model="form.roleType" style="width:100%"><el-option label="业务角色" value="BUSINESS"></el-option><el-option label="系统管理员" value="SYSTEM_ADMIN"></el-option><el-option label="安全保密管理员" value="SECURITY_ADMIN"></el-option><el-option label="安全审计管理员" value="AUDIT_ADMIN"></el-option></el-select></el-form-item>
        <el-form-item label="角色类型">
          <el-select v-model="form.roleType" style="width:100%">
            <el-option label="业务角色" value="BUSINESS"></el-option>
            <el-option label="系统管理员" value="SYSTEM_ADMIN"></el-option>
            <el-option label="安全保密管理员" value="SECURITY_ADMIN"></el-option>
            <el-option label="安全审计管理员" value="AUDIT_ADMIN"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="1" style="width:160px"></el-input-number></el-form-item>
        <el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3"></el-input></el-form-item>
      </el-form>
      <template slot="footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
    </el-dialog>
    <el-dialog title="角色用户设置" :visible.sync="userDialogVisible" width="820px" append-to-body :close-on-click-modal="false">
      <div class="user-setting-head">
        <div>
          <span class="role-label">当前角色</span>
          <strong>{{ currentRole.name }}</strong>
          <span class="role-code">{{ currentRole.code }}</span>
        </div>
        <span class="selected-total">已选择 {{ selectedUserIds.length }} äºº</span>
      </div>
      <div class="user-setting-toolbar">
        <el-radio-group v-model="userScope" size="small">
          <el-radio-button label="all">全部</el-radio-button>
          <el-radio-button label="assigned">已设置</el-radio-button>
        </el-radio-group>
        <el-input v-model="userKeyword" clearable prefix-icon="el-icon-search" placeholder="用户名 / è´¦æˆ·å / éƒ¨é—¨" style="width: 280px"></el-input>
      </div>
      <el-table
        ref="roleUserTable"
        v-loading="userLoading"
        :data="filteredUsers"
        row-key="userId"
        height="430"
        border
        stripe
        @selection-change="handleUserSelectionChange"
      >
        <el-table-column type="selection" reserve-selection width="48" align="center"></el-table-column>
        <el-table-column prop="nickName" label="用户名" min-width="120"></el-table-column>
        <el-table-column prop="userName" label="账户名" min-width="140"></el-table-column>
        <el-table-column prop="deptName" label="所属部门" min-width="180" show-overflow-tooltip></el-table-column>
        <el-table-column label="状态" width="80" align="center">
          <template slot-scope="{ row }">
            <el-tag size="mini" :type="row.status === '1' ? 'info' : 'success'">{{ row.status === '1' ? '停用' : '正常' }}</el-tag>
          </template>
        </el-table-column>
      </el-table>
      <template slot="footer">
        <el-button @click="userDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="userSaving" v-permission="'system:roles:assign'" @click="saveRoleUsers">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
@@ -30,15 +86,124 @@
import api from '@/api'
export default {
  name: 'RoleManage',
  data() { return { rows: [], dialogVisible: false, saving: false, form: {}, rules: { name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }], code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }] } } },
  data() {
    return {
      rows: [],
      dialogVisible: false,
      saving: false,
      userDialogVisible: false,
      userLoading: false,
      userSaving: false,
      userKeyword: '',
      userScope: 'all',
      roleUsers: [],
      selectedUserIds: [],
      currentRole: {},
      form: {},
      rules: {
        name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
        code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }]
      }
    }
  },
  computed: {
    filteredUsers() {
      const assigned = new Set(this.selectedUserIds.map(String))
      const users = this.userScope === 'assigned'
        ? this.roleUsers.filter(item => assigned.has(String(item.userId)))
        : this.roleUsers
      const keyword = this.userKeyword.trim().toLowerCase()
      if (!keyword) return users
      return users.filter(item => [item.nickName, item.userName, item.deptName]
        .some(value => String(value || '').toLowerCase().includes(keyword)))
    }
  },
  created() { this.load() },
  methods: {
    load() { api.get('/system/roles').then(res => { this.rows = res.data || [] }) },
    openCreate() { this.form = { name: '', code: '', roleType: 'BUSINESS', sortNo: this.rows.length + 1, remark: '' }; this.dialogVisible = true },
    openEdit(row) { this.form = { ...row }; this.dialogVisible = true },
    save() { this.$refs.form.validate(valid => { if (!valid) return; this.saving = true; const request = this.form.id ? api.put('/system/roles/' + this.form.id, this.form) : api.post('/system/roles', this.form); request.then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.load() }).finally(() => { this.saving = false }) }) },
    remove(row) { this.$confirm(`确定删除角色“${row.name}”吗?`, '删除确认', { type: 'warning' }).then(() => api.delete('/system/roles/' + row.id)).then(() => { this.$message.success('删除成功'); this.load() }).catch(() => {}) }
    load() {
      api.get('/system/roles').then(res => { this.rows = res.data || [] })
    },
    openCreate() {
      this.form = { name: '', code: '', roleType: 'BUSINESS', sortNo: this.rows.length + 1, remark: '' }
      this.dialogVisible = true
    },
    openEdit(row) {
      this.form = { ...row }
      this.dialogVisible = true
    },
    save() {
      this.$refs.form.validate(valid => {
        if (!valid) return
        this.saving = true
        const request = this.form.id ? api.put('/system/roles/' + this.form.id, this.form) : api.post('/system/roles', this.form)
        request.then(() => {
          this.$message.success('保存成功')
          this.dialogVisible = false
          this.load()
        }).finally(() => { this.saving = false })
      })
    },
    remove(row) {
      this.$confirm(`确定删除角色“${row.name}”吗?`, '删除确认', { type: 'warning' })
        .then(() => api.delete('/system/roles/' + row.id))
        .then(() => { this.$message.success('删除成功'); this.load() })
        .catch(() => {})
    },
    openUserSetting(row) {
      this.currentRole = { ...row }
      this.userKeyword = ''
      this.userScope = 'all'
      this.selectedUserIds = []
      this.roleUsers = []
      this.userDialogVisible = true
      this.userLoading = true
      api.get('/system/roles/' + row.id + '/users').then(res => {
        const data = res.data || {}
        this.roleUsers = data.records || []
        this.selectedUserIds = (data.userIds || []).map(String)
        this.$nextTick(this.restoreUserSelection)
      }).finally(() => { this.userLoading = false })
    },
    restoreUserSelection() {
      const table = this.$refs.roleUserTable
      if (!table) return
      table.clearSelection()
      const selected = new Set(this.selectedUserIds)
      this.roleUsers.forEach(row => {
        if (selected.has(String(row.userId))) table.toggleRowSelection(row, true)
      })
      this.$nextTick(() => {
        table.clearSelection()
        this.roleUsers.forEach(row => {
          if (selected.has(String(row.userId))) table.toggleRowSelection(row, true)
        })
        this.selectedUserIds = Array.from(selected)
      })
    },
    handleUserSelectionChange(selection) {
      this.selectedUserIds = (selection || []).map(item => String(item.userId))
    },
    saveRoleUsers() {
      const userIds = this.selectedUserIds.slice()
      const message = userIds.length
        ? `确定将“${this.currentRole.name}”角色设置为选中的 ${userIds.length} ä¸ªç”¨æˆ·å—?`
        : `未选择用户,将清空“${this.currentRole.name}”角色的全部用户,是否继续?`
      this.$confirm(message, '角色用户设置', { type: 'warning' }).then(() => {
        this.userSaving = true
        api.put('/system/roles/' + this.currentRole.id + '/users', { userIds }).then(() => {
          this.$message.success('角色用户设置已保存')
          this.userDialogVisible = false
        }).finally(() => { this.userSaving = false })
      }).catch(() => {})
    }
  }
}
</script>
<style scoped>.danger-link { color: #d9534f; }</style>
<style scoped>
.danger-link { color: #d9534f; }
.user-setting-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; }
.user-setting-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
</style>
emergency-repair-web/src/views/system/TeamMajorManage.vue
@@ -2,10 +2,10 @@
  <div class="page">
    <div class="page-title">
      <div>
        <h2>T队专业配置</h2>
        <h2>rk_T队专业配置</h2>
        <p>分类取值A、B,用于自动生成故障编号</p>
      </div>
      <el-button type="primary" icon="el-icon-plus" @click="openCreate">新增T队专业</el-button>
      <el-button type="primary" icon="el-icon-plus" @click="openCreate">新增rk_T队专业</el-button>
    </div>
    <div class="toolbar">
@@ -30,9 +30,9 @@
      </el-table>
    </div>
    <el-dialog :title="form.id ? '编辑T队专业' : '新增T队专业'" :visible.sync="dialogVisible" width="520px" append-to-body>
    <el-dialog :title="form.id ? '编辑rk_T队专业' : '新增rk_T队专业'" :visible.sync="dialogVisible" width="520px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="名称" prop="name"><el-input v-model="form.name" placeholder="请输入T队专业名称"></el-input></el-form-item>
        <el-form-item label="名称" prop="name"><el-input v-model="form.name" placeholder="请输入rk_T队专业名称"></el-input></el-form-item>
        <el-form-item label="分类" prop="category">
          <el-radio-group v-model="form.category">
            <el-radio-button label="A">A</el-radio-button>
emergency-repair-web/src/views/system/TreeManage.vue
@@ -3,7 +3,7 @@
    <div class="page-title">
      <div>
        <h2>{{ isProduct ? '产品结构树管理' : '设备基础库管理' }}</h2>
        <p>{{ isProduct ? '型号 â†’ X号 â†’ ä¸€çº§ç³»ç»Ÿ â†’ äºŒçº§ç³»ç»Ÿ â†’ è®¾å¤‡' : '一级系统 â†’ äºŒçº§ç³»ç»Ÿ â†’ è®¾å¤‡' }}</p>
        <p>{{ isProduct ? '型号 â†’ rk_X号 â†’ ä¸€çº§ç³»ç»Ÿ â†’ äºŒçº§ç³»ç»Ÿ â†’ è®¾å¤‡' : '一级系统 â†’ äºŒçº§ç³»ç»Ÿ â†’ è®¾å¤‡' }}</p>
      </div>
      <div>
        <el-button icon="el-icon-plus" @click="addRoot">新增一级节点</el-button>
@@ -18,7 +18,10 @@
          ref="tree"
          :data="tree"
          node-key="id"
          default-expand-all
          v-loading="loading"
          :lazy="isProduct"
          :load="loadNode"
          :default-expand-all="!isProduct"
          highlight-current
          :filter-node-method="filterNode"
          :props="{ label: 'name', children: 'children' }"
@@ -27,7 +30,7 @@
          <span slot-scope="{ data }" class="tree-node">
            <span class="level-badge">{{ levelText(data.nodeLevel) }}</span>
            <span>{{ data.name }}</span>
            <small>{{ data.code }}</small>
            <small v-if="!isProduct && data.code">{{ data.code }}</small>
          </span>
        </el-tree>
      </section>
@@ -44,19 +47,14 @@
          <el-form :model="form" label-width="100px">
            <el-form-item label="节点层级"><el-tag type="info">{{ levelText(form.nodeLevel) }}</el-tag></el-form-item>
            <el-form-item label="名称"><el-input v-model="form.name"></el-input></el-form-item>
            <el-form-item label="编码"><el-input v-model="form.code"></el-input></el-form-item>
            <el-form-item v-if="!isProduct" label="编码"><el-input v-model="form.code"></el-input></el-form-item>
            <el-form-item v-if="isProduct" label="节点类型">
              <el-select v-model="form.nodeType" :disabled="form.nodeLevel < 5" style="width:100%">
                <el-option label="型号" value="MODEL"></el-option>
                <el-option label="X号" value="X_NO"></el-option>
                <el-option label="rk_X号" value="X_NO"></el-option>
                <el-option label="一级系统" value="SYSTEM1"></el-option>
                <el-option label="二级系统" value="SYSTEM2"></el-option>
                <el-option label="设备" value="DEVICE"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item v-if="isProduct && form.nodeLevel === 5" label="基础设备">
              <el-select v-model="form.baseEquipmentId" filterable placeholder="关联设备基础库设备" style="width:100%">
                <el-option v-for="item in baseDevices" :key="item.id" :label="item.path + ' / ' + item.name" :value="item.id"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="1" style="width:180px"></el-input-number></el-form-item>
@@ -77,12 +75,12 @@
    treeType: { type: String, required: true }
  },
  data() {
    return { tree: [], keyword: '', form: {}, saving: false, baseDevices: [] }
    return { tree: [], keyword: '', form: {}, saving: false, loading: false }
  },
  computed: {
    isProduct() { return this.treeType === 'product' },
    maxLevel() { return this.isProduct ? 5 : 3 },
    levelNames() { return this.isProduct ? ['型号', 'X号', '一级系统', '二级系统', '设备'] : ['一级系统', '二级系统', '设备'] }
    levelNames() { return this.isProduct ? ['型号', 'rk_X号', '一级系统', '二级系统', '设备'] : ['一级系统', '二级系统', '设备'] }
  },
  watch: {
    keyword(value) { this.$refs.tree && this.$refs.tree.filter(value) },
@@ -91,7 +89,20 @@
  created() { this.load() },
  methods: {
    load() {
      api.get('/system/tree', { params: { type: this.treeType } }).then(res => { this.tree = res.data || [] })
      this.loading = true
      const request = this.isProduct
        ? api.get('/system/tree/product/roots')
        : api.get('/system/tree', { params: { type: this.treeType } })
      request.then(res => { this.tree = res.data || [] }).finally(() => { this.loading = false })
    },
    loadNode(node, resolve) {
      if (!this.isProduct || !node || !node.data || !node.data.id) {
        resolve([])
        return
      }
      api.get('/system/tree/product/children', { params: { parentId: node.data.id } })
        .then(res => resolve(res.data || []))
        .catch(() => resolve([]))
    },
    levelText(level) { return this.levelNames[(level || 1) - 1] || ('第' + level + '级') },
    filterNode(value, data) {
@@ -100,31 +111,16 @@
    },
    selectNode(node) {
      this.form = { ...node }
      if (this.isProduct && node.nodeLevel === 5) this.loadBaseDevices()
    },
    nodeTypeForLevel(level) {
      return this.isProduct ? ['MODEL', 'X_NO', 'SYSTEM1', 'SYSTEM2', 'DEVICE'][level - 1] : 'BASE_' + level
    },
    addRoot() {
      this.form = { parentId: 0, nodeLevel: 1, nodeType: this.nodeTypeForLevel(1), name: '', code: '', sortNo: 1, remark: '' }
      if (this.isProduct) this.form.baseEquipmentId = null
    },
    addChild() {
      const level = (this.form.nodeLevel || 1) + 1
      this.form = { parentId: this.form.id, nodeLevel: level, nodeType: this.nodeTypeForLevel(level), name: '', code: '', sortNo: 1, remark: '', baseEquipmentId: null }
      if (this.isProduct && level === 5) this.loadBaseDevices()
    },
    loadBaseDevices() {
      api.get('/system/tree', { params: { type: 'base' } }).then(res => { this.baseDevices = this.flattenBase(res.data || [], []) })
    },
    flattenBase(nodes, parents) {
      const result = []
      nodes.forEach(node => {
        const path = parents.concat(node.name)
        if (node.nodeLevel === 3) result.push({ ...node, path: path.slice(0, -1).join(' / ') })
        if (node.children) result.push(...this.flattenBase(node.children, path))
      })
      return result
      this.form = { parentId: this.form.id, nodeLevel: level, nodeType: this.nodeTypeForLevel(level), name: '', code: '', sortNo: 1, remark: '' }
    },
    save() {
      if (!this.form.name) return this.$message.warning('请输入节点名称')
emergency-repair-web/src/views/system/UserManage.vue
New file
@@ -0,0 +1,253 @@
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>用户管理</h2><p>左侧查询用户,右侧设置该用户拥有的角色,一个用户可设置多个角色</p></div>
    </div>
    <div class="user-layout">
      <section class="user-left">
        <div class="toolbar">
          <el-button type="primary" plain icon="el-icon-s-check" :disabled="!selectedRows.length" v-permission="'system:users:assign'" @click="openBatchDialog">批量设置角色</el-button>
          <span v-if="selectedRows.length" class="selected-count">已选 {{ selectedRows.length }} äºº</span>
          <el-input v-model="filters.keyword" clearable placeholder="用户名 / è´¦æˆ·å" style="width:190px" @keyup.enter.native="search"></el-input>
          <el-select v-model="filters.deptId" clearable filterable placeholder="所属部门" style="width:200px">
            <el-option v-for="item in depts" :key="item.id" :label="item.name" :value="item.id"></el-option>
          </el-select>
          <el-select v-model="filters.roleId" clearable placeholder="角色" style="width:170px">
            <el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id"></el-option>
          </el-select>
          <el-button type="primary" icon="el-icon-search" v-permission="'system:users:query'" @click="search">查询</el-button>
          <el-button icon="el-icon-refresh-left" @click="reset">重置</el-button>
        </div>
        <el-table ref="userTable" row-key="userId"
          v-loading="loading"
          :data="rows"
          border
          stripe
          highlight-current-row
          height="calc(100vh - 250px)"
          @selection-change="selectedRows = $event"
          @current-change="selectUser"
        >
          <el-table-column type="selection" width="46" align="center"></el-table-column>
          <el-table-column prop="nickName" label="用户名" min-width="110"></el-table-column>
          <el-table-column prop="userName" label="账户名" min-width="140"></el-table-column>
          <el-table-column prop="deptName" label="所属部门" min-width="180" show-overflow-tooltip></el-table-column>
          <el-table-column label="角色权限" min-width="220">
            <template slot-scope="{ row }">
              <el-tag v-for="role in row.roles" :key="role.roleId" size="mini" class="role-tag">{{ role.roleName }}</el-tag>
              <span v-if="!row.roles || !row.roles.length" class="muted">未分配</span>
            </template>
          </el-table-column>
          <el-table-column prop="phoneNumber" label="手机号" width="130"></el-table-column>
          <el-table-column label="状态" width="80" align="center">
            <template slot-scope="{ row }">
              <el-tag size="mini" :type="isDisabled(row) ? 'info' : 'success'">{{ isDisabled(row) ? '停用' : '正常' }}</el-tag>
            </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>
      </section>
      <section class="user-right">
        <div class="panel-head">
          <h3>角色授权</h3>
          <span v-if="selected" class="selected-user">{{ selected.nickName || selected.userName }}({{ selected.userName }})</span>
        </div>
        <div v-if="!selected" class="empty-tip">请选择左侧用户</div>
        <template v-else>
          <el-checkbox-group v-model="checkedRoleIds" class="role-list">
            <el-checkbox v-for="item in roles" :key="item.id" :label="item.id" class="role-item">
              <span class="role-name">{{ item.name }}</span>
              <span class="role-code">{{ item.code }}</span>
            </el-checkbox>
          </el-checkbox-group>
          <el-button
            type="primary"
            icon="el-icon-check"
            class="save-button"
            v-permission="'system:users:assign'"
            :loading="saving"
            @click="saveRoles"
          >保存授权</el-button>
        </template>
      </section>
    </div>
      <el-dialog title="批量设置角色" :visible.sync="batchVisible" width="520px" append-to-body :close-on-click-modal="false">
        <div class="batch-summary">已选择 {{ selectedRows.length }} ä¸ªç”¨æˆ·</div>
        <el-radio-group v-model="batchMode" class="batch-mode">
          <el-radio label="append">追加角色</el-radio>
          <el-radio label="replace">覆盖角色</el-radio>
        </el-radio-group>
        <el-alert :title="batchMode === 'append' ? '保留所选用户原有角色,并追加勾选角色' : '使用勾选角色覆盖所选用户的原有角色'" type="warning" :closable="false" show-icon></el-alert>
        <el-checkbox-group v-model="batchRoleIds" class="batch-role-list">
          <el-checkbox v-for="item in roles" :key="item.id" :label="item.id" class="role-item">
            <span class="role-name">{{ item.name }}</span>
            <span class="role-code">{{ item.code }}</span>
          </el-checkbox>
        </el-checkbox-group>
        <span slot="footer">
          <el-button @click="batchVisible = false">取消</el-button>
          <el-button type="primary" :loading="batchSaving" v-permission="'system:users:assign'" @click="saveBatchRoles">保存</el-button>
        </span>
      </el-dialog>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'UserManage',
  data() {
    return {
      rows: [],
      total: 0,
      current: 1,
      size: 20,
      loading: false,
      saving: false,
      batchSaving: false,
      batchVisible: false,
      batchRoleIds: [],
      batchMode: 'append',
      selectedRows: [],
      depts: [],
      roles: [],
      selected: null,
      checkedRoleIds: [],
      filters: { keyword: '', deptId: null, roleId: null }
    }
  },
  created() {
    this.loadRoles()
    this.loadDepts()
    this.load()
  },
  methods: {
    load() {
      this.clearSelection()
      this.loading = true
      api.get('/system/users', {
        params: {
          keyword: this.filters.keyword || undefined,
          deptId: this.filters.deptId || undefined,
          roleId: this.filters.roleId || undefined,
          current: this.current,
          size: this.size
        }
      }).then(res => {
        const data = res.data || {}
        this.rows = data.records || []
        this.total = data.total || 0
        if (this.selected) {
          const matched = this.rows.find(item => item.userId === this.selected.userId)
          this.selected = matched || null
          if (!matched) this.checkedRoleIds = []
        }
      }).finally(() => { this.loading = false })
    },
    loadRoles() {
      api.get('/system/roles').then(res => { this.roles = res.data || [] })
    },
    loadDepts() {
      api.get('/system/depts').then(res => { this.depts = res.data || [] })
    },
    search() {
      this.current = 1
      this.load()
    },
    reset() {
      this.filters = { keyword: '', deptId: null, roleId: null }
      this.search()
    },
    handleCurrentChange(page) {
      this.current = page
      this.load()
    },
    handleSizeChange(size) {
      this.size = size
      this.current = 1
      this.load()
    },
    clearSelection() {
      this.selectedRows = []
      this.$nextTick(() => { if (this.$refs.userTable) this.$refs.userTable.clearSelection() })
    },
    openBatchDialog() {
      if (!this.selectedRows.length) return
      this.batchRoleIds = []
      this.batchMode = 'append'
      this.batchVisible = true
    },
    saveBatchRoles() {
      if (!this.selectedRows.length) return
      const userIds = this.selectedRows.map(item => item.userId)
      const roleIds = this.batchRoleIds.slice()
      if (this.batchMode === 'append' && !roleIds.length) {
        this.$message.warning('请选择要追加的角色')
        return
      }
      const message = this.batchMode === 'append'
        ? '将向 ' + userIds.length + ' ä¸ªç”¨æˆ·è¿½åŠ æ‰€é€‰è§’è‰²ï¼Œæ˜¯å¦ç»§ç»­ï¼Ÿ'
        : (roleIds.length ? '将使用所选角色覆盖 ' + userIds.length + ' ä¸ªç”¨æˆ·çš„现有角色,是否继续?' : '未选择角色,将清空 ' + userIds.length + ' ä¸ªç”¨æˆ·çš„全部角色,是否继续?')
      this.$confirm(message, '批量设置角色', { type: 'warning' }).then(() => {
        this.batchSaving = true
        api.put('/system/users/roles/batch', { userIds, roleIds, mode: this.batchMode }).then(() => {
          this.$message.success('批量角色设置成功')
          this.batchVisible = false
          this.load()
          if (this.selected) this.selectUser(this.selected)
        }).finally(() => { this.batchSaving = false })
      }).catch(() => {})
    },
    selectUser(row) {
      this.selected = row || null
      this.checkedRoleIds = []
      if (!row) return
      api.get('/system/users/' + row.userId + '/roles').then(res => {
        this.checkedRoleIds = res.data || []
      })
    },
    saveRoles() {
      if (!this.selected) return
      this.saving = true
      api.put('/system/users/' + this.selected.userId + '/roles', { roleIds: this.checkedRoleIds }).then(() => {
        this.$message.success('角色授权已保存')
        this.load()
      }).finally(() => { this.saving = false })
    },
    isDisabled(row) {
      return row.status === '1'
    }
  }
}
</script>
<style scoped>
.user-layout { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 12px; }
.user-left, .user-right { min-width: 0; padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; }
.user-right { min-height: calc(100vh - 190px); }
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.selected-count { align-self: center; color: #5c6f82; font-size: 12px; }
.batch-summary { margin-bottom: 10px; color: #31465d; font-size: 14px; }
.batch-role-list { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 12px; max-height: 320px; margin-top: 14px; overflow: auto; }
.pager { margin-top: 12px; text-align: right; }
.panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #edf1f5; }
.panel-head h3 { margin: 0; color: #31465d; font-size: 15px; }
.selected-user { color: #5c6f82; font-size: 13px; }
.empty-tip { padding: 60px 0; color: #93a2b1; text-align: center; }
.role-list { display: flex; flex-direction: column; gap: 4px; max-height: calc(100vh - 320px); overflow: auto; }
.role-item { display: flex; align-items: center; height: 34px; margin-right: 0; }
.role-name { color: #31465d; }
.role-code { margin-left: 8px; color: #93a2b1; font-size: 12px; }
.save-button { width: 100%; margin-top: 16px; }
.role-tag { margin: 2px 4px 2px 0; }
.muted { color: #a3afbc; }
@media (max-width: 1100px) { .user-layout { grid-template-columns: 1fr; } }
</style>
requirement/½»½ÓÎļþ_2026.10.3.md
New file
@@ -0,0 +1,506 @@
# ä¸´æŠ¢ä¿®ç®¡ç†ç³»ç»Ÿäº¤æŽ¥æ–‡ä»¶ï¼ˆ2026.10.3)
## åŸºæœ¬ä¿¡æ¯
- ä»“库目录:`D:\projectJava\lqx`
- Git åˆ†æ”¯ï¼š`main`
- åŸºçº¿æäº¤ï¼š`f17c76c fix: load only active report data`
- åŽŸå§‹éœ€æ±‚æ–‡ä»¶ï¼š`requirement/改进20261003.txt`
- äº¤æŽ¥æ•´ç†æ—¶é—´ï¼š2026-10-04
- æœ¬è½®å·¥ä½œæ—¥æœŸï¼š2026-10-03
- æäº¤çŠ¶æ€ï¼šéœ€æ±‚ 11、12、17-19 å·²å®Œæˆå¹¶å®Œæˆæµè§ˆå™¨ã€æŽ¥å£åŠç«¯åˆ°ç«¯è”调,本轮代码与本文档将统一提交并推送 `main` åˆ†æ”¯ã€‚
- æ³¨æ„äº‹é¡¹ï¼š`requirement/改进20261003.txt` æ˜¯ç”¨æˆ·åŽŸå§‹éœ€æ±‚è®°å½•ï¼Œæœ¬æ¬¡æœªä¿®æ”¹ã€‚
> 2026-10-04 æ›´æ–°ï¼šéœ€æ±‚ 11、12、17-19 å·²å®Œæˆå¹¶å®Œæˆæµè§ˆå™¨ã€æŽ¥å£åŠç«¯åˆ°ç«¯è”调;本轮代码与交接文档将统一提交并推送 `main` åˆ†æ”¯ã€‚`DEPT_NAME` åŒ¹é…ç­‰å‰©ä½™é£Žé™©è§ç¬¬åä¸€èŠ‚ã€‚
## ä¸€ã€éœ€æ±‚完成情况
| åºå· | éœ€æ±‚ | çŠ¶æ€ | ä¸»è¦å®žçް |
| --- | --- | --- | --- |
| 1 | logo ä½¿ç”¨ `public/logo.png` | å·²å®Œæˆ | `RepairLogo.vue` è¯»å– `${BASE_URL}logo.png`,文件位于 `emergency-repair-web/public/logo.png` |
| 2 | ç”¨æˆ·ç®¡ç†ï¼Œå·¦å³ç»“构,左侧用户表格、右侧角色复选及反显 | å·²å®Œæˆ | æ–°å¢ž `UserManage.vue`;支持用户名、账户名、部门、角色过滤;支持一人多角色 |
| 3 | ç™»å½•日志、操作日志 | å·²å®Œæˆ | æ–°å¢žç™»å½•日志和操作日志页面、实体、Mapper;操作日志记录新增、修改、删除请求 |
| 4 | èœå•管理增加按钮类型,按钮只能挂在菜单下级 | å·²å®Œæˆ | `MenuManage.vue`、`ButtonPermissionCatalog.java`;页面按钮可自动获取并同步为 `MENU_TYPE=2` |
| 5 | ä»£ç ä¸­çš„“装备”改成“设备” | å·²å®Œæˆ | å‰ç«¯ã€åŽç«¯æºç æ£€æŸ¥æ— â€œè£…备”匹配;初始化菜单也有替换逻辑 |
| 6 | å¤šæ–‡æ¡£æ ‡ç­¾æ¨ªå‘滚动时高度不足、文字显示不全 | å·²å®Œæˆ | `AppLayout.vue` è°ƒæ•´å¤šæ–‡æ¡£æ ‡ç­¾æ ã€æ»šåŠ¨æ¡å’Œæ ‡ç­¾é«˜åº¦ |
| 7 | æ”¾å¼€æƒé™ï¼Œå…¶ä»–用户也可以登录 | å·²å®Œæˆ | `AuthController` æŸ¥è¯¢å¤–部用户表,不再只允许 admin;其他有效用户可登录 |
| 8 | èœå•权限管理父子节点复选联动 | å·²å®Œæˆ | `MenuPermission.vue` ä½¿ç”¨ Element UI é»˜è®¤çº§è”复选逻辑,并在保存时包含半选父节点 |
| 9 | ç”¨æˆ·ç®¡ç†æ‰¹é‡è®¾ç½®è§’色;角色管理增加角色用户设置 | å·²å®Œæˆ | ç”¨æˆ·ç®¡ç†æ”¯æŒâ€œè¿½åŠ è§’è‰²â€å’Œâ€œè¦†ç›–è§’è‰²â€ï¼›è§’è‰²ç®¡ç†æ”¯æŒâ€œç”¨æˆ·è®¾ç½®â€å’Œè§’è‰²ç”¨æˆ·åæ˜¾ |
| 10 | äº§å“ç»“构树改为同义词表 `ZT_BOAT_TREE` | å·²å®Œæˆï¼Œä»æœ‰å¾…确认事项 | å·²æ˜ å°„ `LABEL`、`INFO_ID`、`CONNECT_ID`、`CONNECT_GROUP`、`LAYER`;已改为懒加载 |
## äºŒã€ç”¨æˆ·ä¸Žè§’色授权
### 2.1 ç”¨æˆ·ç®¡ç†
页面文件:
- `emergency-repair-web/src/views/system/UserManage.vue`
功能:
- å·¦ä¾§ç”¨æˆ·è¡¨æ ¼æ˜¾ç¤ºï¼š
  - ç”¨æˆ·å
  - è´¦æˆ·å
  - æ‰€å±žéƒ¨é—¨
  - è§’色权限
  - æ‰‹æœºå·
  - çŠ¶æ€
- æ”¯æŒæŒ‰ç”¨æˆ·åã€è´¦æˆ·åã€éƒ¨é—¨ã€è§’色过滤。
- é€‰æ‹©å·¦ä¾§ç”¨æˆ·åŽï¼Œå³ä¾§å¤é€‰æ˜¾ç¤ºè¯¥ç”¨æˆ·æ‹¥æœ‰çš„角色。
- ä¸€ä¸ªç”¨æˆ·å¯ä»¥åŒæ—¶æ‹¥æœ‰å¤šä¸ªè§’色。
- æ”¯æŒå¤šé€‰ç”¨æˆ·åŽâ€œæ‰¹é‡è®¾ç½®è§’色”:
  - `append`:保留原角色,追加勾选角色。
  - `replace`:使用勾选角色覆盖原角色。
- `replace` ä¸”未选择角色时,会清空所选用户的全部角色。
相关接口:
- `GET /api/system/users`
- `GET /api/system/users/{userId}/roles`
- `PUT /api/system/users/{userId}/roles`
- `PUT /api/system/users/roles/batch`
### 2.2 è§’色管理
页面文件:
- `emergency-repair-web/src/views/system/RoleManage.vue`
功能:
- æ¯è¡Œè§’色增加“用户设置”。
- å¼¹çª—中可过滤用户,复选并反显当前角色所属用户。
- ä¿å­˜è§’色用户关系时只替换该角色的用户关系,不删除这些用户拥有的其他角色。
相关接口:
- `GET /api/system/roles/{roleId}/users`
- `PUT /api/system/roles/{roleId}/users`
服务实现:
- `SystemService.roleUsers(...)`
- `SystemService.saveRoleUsers(...)`
按钮权限:
- `system:roles:assign`
## ä¸‰ã€æ—¥å¿—功能
### 3.1 ç™»å½•日志
实现位置:
- `emergency-repair-server/src/main/java/com/emergencyrepair/auth/AuthController.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysLoginLog.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysLoginLogMapper.java`
- `emergency-repair-web/src/views/system/LoginLogManage.vue`
记录内容:
- ç”¨æˆ· ID
- è´¦æˆ·å
- ç”¨æˆ·å
- éƒ¨é—¨
- ç™»å½• IP
- ç™»å½•状态
- ç™»å½•结果说明
- ç™»å½•æ—¶é—´
接口:
- `GET /api/system/login-logs`
### 3.2 æ“ä½œæ—¥å¿—
实现位置:
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/log/OperationLogInterceptor.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysOperationLog.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysOperationLogMapper.java`
- `emergency-repair-web/src/views/system/OperationLogManage.vue`
记录规则:
- æ‹¦æˆª `/api/**` ä¸‹çš„ `POST`、`PUT`、`DELETE` è¯·æ±‚。
- ç™»å½•接口排除,由登录日志负责。
- è®°å½•模块、操作类型、目标类型、目标 ID、请求方式、请求 URI、成功/失败、操作人和时间。
- ä¸šåŠ¡è¯·æ±‚å¤±è´¥æ—¶ï¼Œæ—¥å¿—å†™å…¥å¼‚å¸¸ä¸ä¼šå½±å“åŽŸä¸šåŠ¡è¯·æ±‚ã€‚
- å½“前操作日志用户取请求头 `X-Username`;如果调用方没有传该请求头,会回退记录为 `admin`。手机端或第三方调用时需要注意补齐该请求头。
接口:
- `GET /api/system/operation-logs`
### 3.3 æ—¥å¿—表
由 `DatabaseBootstrap` åˆ›å»ºï¼š
- `LX.T_SYS_LOGIN_LOG`
- `LX.T_SYS_OPERATION_LOG`
索引:
- `IX_T_SYS_LOGIN_LOG_TIME`
- `IX_T_SYS_OPERATION_LOG_TIME`
- `IX_T_SYS_ROLE_USER_USER`
## å››ã€èœå•与按钮权限
页面文件:
- `emergency-repair-web/src/views/system/MenuManage.vue`
- `emergency-repair-web/src/views/system/MenuPermission.vue`
按钮目录:
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java`
规则:
- èœå•类型:
  - `0`:目录
  - `1`:菜单
  - `2`:按钮
- æŒ‰é’®åªèƒ½æŒ‚在菜单类型 `1` çš„下级。
- èœå•页面点击“页面按钮”后,后端从按钮目录自动列出该页面可配置按钮。
- å‹¾é€‰å¹¶ä¿å­˜åŽï¼Œè‡ªåŠ¨ç”ŸæˆæŒ‰é’®ç±»åž‹èœå•ã€‚
- æŒ‰é’®ç±»åž‹èœå•可继续在菜单权限管理中授权给角色。
- èœå•权限树使用父子节点联动,保存时同时收集已选中节点和半选父节点。
相关接口:
- `GET /api/system/menu/buttons`
- `POST /api/system/menu/buttons`
- `GET /api/system/menu-permissions`
- `POST /api/system/menu-permissions`
## äº”、产品结构树
### 5.1 æ•°æ®æº
实体文件:
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/ProductNode.java`
映射表:
```text
LX.ZT_BOAT_TREE
```
物理表:
```text
CSICZB_ZB.ZT_BOAT_TREE
```
已确认 `CSICZB` å’Œ `LX` ä¸‹å‡æœ‰åŒä¹‰è¯ã€‚
字段映射:
| å¤–部表字段 | Java å­—段 | è¯´æ˜Ž |
| --- | --- | --- |
| `INFO_ID` | `id` | ä¸šåŠ¡èŠ‚ç‚¹ä¸»é”® |
| `CONNECT_ID` | `parentId` | çˆ¶èŠ‚ç‚¹ ID |
| `CONNECT_GROUP` | `connectGroup` | ç¥–辈路径 |
| `LABEL` | `name` | è®¾å¤‡åç§° |
| `LAYER` | `layer` | èŠ‚ç‚¹ç±»åž‹ |
| `SORT` | `sortNo` | æŽ’序 |
| `ISDELETE` | `isDelete` | åˆ é™¤æ ‡è®° |
| `ID` | `recordId` | ç‰©ç†å…³ç³»è¡Œ ID |
已知层级兼容映射:
| `LAYER` | å‰ç«¯å±‚级 | å‰ç«¯ç±»åž‹ |
| --- | --- | --- |
| `model` | 1 | `MODEL` |
| `side` | 2 | `X_NO` |
| `system1` | 3 | `SYSTEM1` |
| `system2` | 4 | `SYSTEM2` |
| `equipment` | 5 | `DEVICE` |
| å…¶ä»–值 | 6 | åŽŸå§‹å€¼å¤§å†™ |
### 5.2 å®žé™…数据情况
2026-10-03 DM8 å®žæµ‹ï¼š
- ç‰©ç†è¡Œæ•°ï¼š12974
- åŽ»é‡åŽ `INFO_ID`:12972
- é‡å¤ `INFO_ID`:2 ä¸ª
- æ ¹èŠ‚ç‚¹ï¼š2 ä¸ª
- `equipment`:12798
- `system1`:161
- `side`:9
- `model`:2
- æœªçŸ¥å±‚级:`ttt`、`sss`、`rrrr`,各 1 ä¸ª
示例根节点:
- `INFO_ID=1280000000000006486`
- åç§°ï¼š`型号s44444`
- `LAYER=model`
### 5.3 æ‡’加载改造
由于 12972 ä¸ªèŠ‚ç‚¹ä¸€æ¬¡æ€§åŠ è½½å’Œæ¸²æŸ“ä¼šå¯¼è‡´æµè§ˆå™¨å¡æ­»ï¼Œäº§å“æ ‘å·²ç»æ”¹ä¸ºæŒ‰å±•å¼€å±‚çº§æ‡’åŠ è½½ã€‚
新增接口:
- `GET /api/system/tree/product/roots`
- `GET /api/system/tree/product/children?parentId={id}`
- `GET /api/system/tree/product/side-roots?xNo={xNo}`
- `GET /api/system/tree/product/x-numbers`
- `GET /api/system/tree/product/selected?ids={id1,id2}`
前端使用位置:
- `TreeManage.vue`:产品树管理
- `ProductTreePicker.vue`:故障上报和故障编辑中的设备选择
- `FaultReport.vue`
- `FaultEditDialog.vue`
- `XcbReview.vue`
处理结果:
- äº§å“æ ‘管理首屏只返回 2 ä¸ªæ ¹èŠ‚ç‚¹ã€‚
- å±•开根节点时只请求直接子节点。
- æ•…障页面 X å·ä¸å†éåŽ†å®Œæ•´æ ‘ï¼Œç›´æŽ¥è¯·æ±‚ X å·åˆ—表。
- ç¼–辑故障时通过“已选设备”接口反显设备名称和路径。
- äº§å“æ ‘管理隐藏了外部表不存在的“编码”字段。
- äº§å“èŠ‚ç‚¹åˆ é™¤é‡‡ç”¨è½¯åˆ é™¤ï¼Œè®¾ç½® `ISDELETE=1`。
### 5.4 äº§å“æ ‘待确认事项
用户已说明:实际结构并非严格按 `model -> side -> system1 -> system2 -> equipment` æŽ’列,可能缺少 `system2`,且 `system2` ä¸‹ä¹Ÿå¯èƒ½æŒ‚非 `equipment` ç±»åž‹èŠ‚ç‚¹ã€‚
目前实现仍有以下约束:
- æœªçŸ¥ `LAYER` ç›®å‰ç»Ÿä¸€æ˜ å°„为第 6 çº§ã€‚
- å‰ç«¯å½“前最大编辑层级固定为 5,第 6 çº§èŠ‚ç‚¹é€šå¸¸åªèƒ½æŸ¥çœ‹ï¼Œç‚¹å‡»â€œæ–°å¢žä¸‹çº§â€çš„æ¡ä»¶ä¸æ»¡è¶³ã€‚
- éœ€è¦ç”¨æˆ·åŽç»­ç¡®è®¤æœªçŸ¥å±‚级是只读展示、归入“其他”,还是允许继续配置下级。
- äº§å“æ ‘写入的是外部表同义词,正式环境需确认数据库账号对 `CSICZB_ZB.ZT_BOAT_TREE` çš„增删改权限。
- `DatabaseBootstrap` ä¸­ä»ä¿ç•™æ—§ `LX.T_PRODUCT_TREE` å»ºè¡¨å®šä¹‰ï¼Œä½† `ProductNode` å·²ä¸å†ä½¿ç”¨æ—§è¡¨ï¼Œ`DataInitializer` ä¹Ÿå·²åœæ­¢åˆå§‹åŒ–产品树演示数据。
## å…­ã€æž„建与运行
### 6.1 çŽ¯å¢ƒ
- Java:`C:\Program Files\Java\jdk1.8.0_172`
- Maven:`D:\solidDir\apache-maven-3.6.1\bin\mvn.cmd`
- Node/npm:`C:\Program Files\nodejs\npm.cmd`
- åŽç«¯ç«¯å£ï¼š`8091`
- å‰ç«¯ç«¯å£ï¼š`8082`
- DM8:`127.0.0.1:5236`
- DM8 æ•°æ®åº“账号:`SYSDBA / SYSDBA`
- ç™»å½•示例:`admin / admin` æˆ– `admin / admin123`
### 6.2 åŽç«¯æž„建
```powershell
$env:JAVA_HOME='C:\Program Files\Java\jdk1.8.0_172'
Set-Location 'D:\projectJava\lqx\emergency-repair-server'
& 'D:\solidDir\apache-maven-3.6.1\bin\mvn.cmd' -B -DskipTests clean package
```
构建结果:成功。
JAR è·¯å¾„:
```text
D:\projectJava\lqx\emergency-repair-server\target\emergency-repair-server-1.0.0-SNAPSHOT.jar
```
启动示例:
```powershell
Start-Process `
  -FilePath 'C:\Program Files\Java\jdk1.8.0_172\bin\java.exe' `
  -ArgumentList '-jar','D:\projectJava\lqx\emergency-repair-server\target\emergency-repair-server-1.0.0-SNAPSHOT.jar' `
  -WorkingDirectory 'D:\projectJava\lqx\emergency-repair-server' `
  -WindowStyle Hidden `
  -PassThru
```
### 6.3 å‰ç«¯æž„建
```powershell
Set-Location 'D:\projectJava\lqx\emergency-repair-web'
& 'C:\Program Files\nodejs\npm.cmd' run build
```
构建结果:成功。
构建告警:
- `chunk-5be386f4` çº¦ 1.01 MiB。
- `chunk-vendors` çº¦ 946 KiB。
- å±žäºŽåŒ…体积告警,不阻塞运行。
启动开发服务:
```powershell
Set-Location 'D:\projectJava\lqx\emergency-repair-web'
& 'C:\Program Files\nodejs\npm.cmd' run serve
```
前端代理配置:
- `/api` ä»£ç†åˆ° `http://127.0.0.1:8091`
- å‰ç«¯åœ°å€ï¼š`http://localhost:8082`
## ä¸ƒã€å·²å®ŒæˆéªŒè¯
### 7.1 ç¼–译验证
- åŽç«¯ï¼š`mvn -B -DskipTests clean package` é€šè¿‡ã€‚
- å‰ç«¯ï¼š`npm run build` é€šè¿‡ã€‚
### 7.2 æµè§ˆå™¨éªŒè¯
角色管理:
- â€œç”¨æˆ·è®¾ç½®â€æŒ‰é’®å­˜åœ¨ã€‚
- å¼¹çª—反显 44 ä¸ªç”¨æˆ·ã€‚
- é¦–个角色选中 6 äººã€‚
用户管理:
- â€œæ‰¹é‡è®¾ç½®è§’色”按钮存在。
- è§’色授权面板存在。
- è¡¨æ ¼è¿”回 20 è¡Œã€‚
产品树:
- é¦–屏 2 ä¸ªæ ¹èŠ‚ç‚¹ã€‚
- å±•开第一个根节点后显示 5 ä¸ªç›´æŽ¥å­èŠ‚ç‚¹ã€‚
- X å·æ•°é‡ 9 ä¸ªã€‚
- å·²é€‰è®¾å¤‡æŽ¥å£èƒ½è¿”回名称和完整路径。
- äº§å“æ ‘管理和故障选择设备弹窗均可正常加载,未出现整树渲染卡死。
验证截图和脚本:
- `.codex-run/role-users-verified.png`
- `.codex-run/user-manage-verified.png`
- `.codex-run/product-tree-verified.png`
- `.codex-run/product-tree-lazy-verified.png`
- `.codex-run/verify-ui.js`
- `.codex-run/verify-lazy-tree.js`
- `.codex-run/verify-picker.js`
### 7.3 äº¤æŽ¥æ—¶è¿›ç¨‹çŠ¶æ€
2026-10-04 12:28 é‡æ–°æž„建并启动后,当前运行状态:`8082` å‰ç«¯å¼€å‘服务监听中,`8091` åŽç«¯ç›‘听中。启动日志位于 `.codex-run/backend.out.log` å’Œ `.codex-run/backend.err.log`。
2026-10-03 æœ€åŽéªŒè¯æ—¶æ›¾è¿è¡Œï¼š
- åŽç«¯ç«¯å£ï¼š`8091`
- å‰ç«¯ç«¯å£ï¼š`8082`
- Edge è°ƒè¯•端口:`9222`
重新联调时按第六节命令启动后端和前端。
## å…«ã€ä¸»è¦å˜æ›´æ–‡ä»¶
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/auth/AuthController.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/business/dto/Requests.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/business/entity/ProductNode.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-server/src/main/java/com/emergencyrepair/system/log/OperationLogInterceptor.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysLoginLog.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysOperationLog.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysLoginLogMapper.java`
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysOperationLogMapper.java`
前端:
- `emergency-repair-web/public/logo.png`
- `emergency-repair-web/src/components/RepairLogo.vue`
- `emergency-repair-web/src/components/ProductTreePicker.vue`
- `emergency-repair-web/src/layout/AppLayout.vue`
- `emergency-repair-web/src/router/index.js`
- `emergency-repair-web/src/views/system/UserManage.vue`
- `emergency-repair-web/src/views/system/RoleManage.vue`
- `emergency-repair-web/src/views/system/MenuManage.vue`
- `emergency-repair-web/src/views/system/MenuPermission.vue`
- `emergency-repair-web/src/views/system/TreeManage.vue`
- `emergency-repair-web/src/views/system/LoginLogManage.vue`
- `emergency-repair-web/src/views/system/OperationLogManage.vue`
- `emergency-repair-web/src/views/fault/FaultReport.vue`
- `emergency-repair-web/src/views/fault/FaultEditDialog.vue`
- `emergency-repair-web/src/views/fault/XcbReview.vue`
## ä¹ã€æŽ¥æ‰‹å»ºè®®
1. å…ˆç¡®è®¤ `git status`,不要回退当前未提交修改。
2. å…ˆå¯åŠ¨åŽç«¯ï¼Œå†å¯åŠ¨å‰ç«¯ï¼Œç¡®è®¤ `8091` å’Œ `8082` å¯è®¿é—®ã€‚
3. äº§å“æ ‘后续优先确认未知 `LAYER` çš„展示和编辑规则。
4. æ­£å¼è”调时检查操作日志是否能拿到真实 `X-Username`,避免全部回退为 `admin`。
5. å¦‚果要把产品树写入外部同义词表,先确认数据库账号权限和外部系统对 `ISDELETE` è½¯åˆ é™¤çš„约定。
## åã€2026-10-04 è¡¥å……需求(13-16)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 13 | æ•æ„Ÿè¯ codename å¢žåŠ  `rk_` å‰ç¼€ | å·²å®Œæˆ | `public/sensitive-words.json`、`requirement/敏感词字典.json` å·²æ›´æ–°ï¼›èœå•及页面旧文案已迁移为 `rk_`;前端保留无前缀别名兼容 |
| 14 | æ•…障编号前缀后台可配置 | å·²å®Œæˆ | `application.yml` æ–°å¢ž `fault-code.prefix`,默认 `固定前缀-`;实测编号 `固定前缀-001/A-261004-001` |
| 15 | æ‰€æœ‰è¡¨è‡ªåŠ¨è®°å½•åˆ›å»ºäººã€ä¿®æ”¹äºº | å·²å®Œæˆ | æ–°å¢ž `BaseAuditEntity`、`CurrentUserInterceptor`、`AuditMetaObjectHandler`;实测创建写入 `codex-create-user`,修改写入 `codex-update-user` |
| 16 | æ‰€æœ‰è¡¨é€»è¾‘删除并自动过滤 | å·²å®Œæˆ | è‡ªå»ºè¡¨æ–°å¢ž `DEL_FLAG`,实体继承 `@TableLogic`;报表 SQL å·²å¢žåŠ è¿‡æ»¤ï¼›å®žæµ‹é€»è¾‘åˆ é™¤åŽåˆ—è¡¨æŸ¥è¯¢è¿”å›ž 0 æ¡ |
补充说明:
- äº§å“ç»“构树外部同义词表 `LX.ZT_BOAT_TREE` æ²¿ç”¨è‡ªå·±çš„ `ISDELETE`,不额外增加 `UPDATE_BY`、`DEL_FLAG`。
- æ•…障编号序号查询会包含逻辑删除记录,避免唯一索引冲突。
- æ“ä½œæ—¥å¿—新增、修改、删除均已实测落库。
## åä¸€ã€2026-10-04 è”调收尾(11、12、17-19)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 11 | å®Œå·¥æƒ…况处理改为“完工”时不跳转完工信息填报 | å·²å®Œæˆ | å®Œå·¥å¤„理只更新派工单状态;完工处理与完工填报由不同用户分别操作 |
| 12 | å®Œå·¥ä¿¡æ¯å¡«æŠ¥åªæ˜¾ç¤ºå·²å®Œå·¥ã€æœ¬å•位、未填报派工单,并显示工程/项目名称 | å·²å®Œæˆ | é€‰æ‹©åˆ—表按“完工状态 + ç™»å½•用户所属单位 + å°šæœªå¡«æŠ¥â€é™åˆ¶ï¼Œå¹¶æ˜¾ç¤ºå·¥ç¨‹åç§° |
| 17 | è§’色管理用户设置增加“全部 / å·²è®¾ç½®â€åˆ‡æ¢ | å·²å®Œæˆ | `RoleManage.vue` å¢žåŠ èŒƒå›´è¿‡æ»¤ï¼Œå·²è®¾ç½®å’Œå…¨éƒ¨åˆ—è¡¨å¯åˆ‡æ¢ |
| 18 | å¢žåŠ ç»„ç»‡æœºæž„ï¼ˆéƒ¨é—¨æ ‘è¡¨ï¼‰åŠŸèƒ½ | å·²å®Œæˆ | èœå• `/system/organization`,页面 `OrganizationManage.vue`,数据来自 `CSICZB_ZB.SYS_DEPT`,接口 `/api/system/depts`;当前 34 ä¸ªéƒ¨é—¨ |
| 19 | æ‰¿ä¿®å•位改为查询部门表中 `DEPT_TYPE='cj'` çš„记录 | å·²å®Œæˆ | `SystemService.listContractors()` æŸ¥è¯¢ cj éƒ¨é—¨ï¼Œéƒ¨é—¨ ID/名称用于审核和派单;承修单位页只读;当前 7 ä¸ª cj éƒ¨é—¨ |
### 11.1 ä¸»è¦å˜æ›´æ–‡ä»¶
后端:
- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.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-web/src/views/system/OrganizationManage.vue`
- `emergency-repair-web/src/views/system/ContractorManage.vue`
- `emergency-repair-web/src/views/completion/CompletionReport.vue`
- `emergency-repair-web/src/views/system/RoleManage.vue`
- `emergency-repair-web/src/router/index.js`
### 11.2 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests clean package` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ã€‚
- `node .codex-run/verify-items-11-12-17.js`、`node .codex-run/verify-items-18-19.js` é€šè¿‡ã€‚
- æˆªå›¾ï¼š`.codex-run/organization-verified.png`、`.codex-run/contractor-readonly-verified.png`、`.codex-run/completion-report-verified.png`、`.codex-run/role-scope-verified.png`。
- ç«¯åˆ°ç«¯ä¸´æ—¶æ•…éšœ `固定前缀-088/A-261004-001` å·²è·‘通上报、XCB审核分配“厂家1/厂家2”、CMB/JCC三级审批和派单;生成两张派工单,承修单位为 cj éƒ¨é—¨ ID/名称。
- å®Œå·¥å¡«æŠ¥å¯è§æ€§ï¼š`cjyh1-1` ä»…见厂家1派工单,`cjyh2-1` ä»…见厂家2派工单,`admin` è§ä¸¤å¼ ï¼›ä¸´æ—¶ `T_FAULT`、`T_WORK_ORDER` æ•°æ®å·²æ¸…理。
### 11.3 å½“前服务
- å‰ç«¯ `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge è°ƒè¯•端口 `9222`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
### 11.4 æŽ¥æ‰‹æ³¨æ„
- éžç®¡ç†å‘˜å®Œå·¥å¡«æŠ¥çš„单位匹配目前按 `DEPT_NAME`,不是 `DEPT_ID`;若出现同名部门,应改为 ID ç²¾ç¡®åŒ¹é…ã€‚
- åŽ†å²æ¼”ç¤ºæ´¾å·¥å•å¯èƒ½ä»ä¿å­˜æ—§ `T_CONTRACTOR` åç§°ï¼Œéžç®¡ç†å‘˜ cj ç”¨æˆ·å¯èƒ½çœ‹ä¸åˆ°ï¼›`admin` ä¸å—单位过滤影响。
- `.codex-run`、Office é”æ–‡ä»¶ä»¥åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` æ˜¯æœ¬åœ°è”调或临时文件,不应提交。
requirement/¾ö²ß±í_´ýÈ·ÈÏÏî.md
@@ -17,7 +17,7 @@
>
> **2026-09-28 ç”¨æˆ·ç¡®è®¤ï¼ˆB1-B4)**:派工单“完工”与首页“已完工”为同一统计状态;首页与派工单的“完工待试验”口径一致;“待备件”允许直接“完工”,由 AXU15 æäº¤æˆåŠŸåŽå†™å…¥ï¼›â€œå®Œå·¥å¾…è¯•éªŒâ€è¯•éªŒä¸é€šè¿‡æ—¶å›žåˆ°â€œæœªå®Œå·¥â€ï¼Œä¸å•è®¾â€œè¿”å·¥â€çŠ¶æ€ï¼ŒåŽŸå› å†™å…¥ä¿®åŽè´¨é‡æ„è§æˆ–å¤‡æ³¨ã€‚
>
> **2026-09-28 ç”¨æˆ·ç¡®è®¤ï¼ˆB5 ä¹‹åŽçš„第 4 æ¡æ–°éœ€æ±‚)**:故障编号不再人工填写,改为系统自动生成,格式 `潜1基-X号/分类-YYMMDD-nnn`(X号为3位数字;分类取T队专业配置的 A/B;YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸï¼›nnn ä¸ºåŒä¸€ X号 + åŒä¸€ T队专业 + åŒä¸€ä¸ŠæŠ¥æ—¥æœŸçš„序号,从 001 èµ·ï¼‰ã€‚系统管理新增“工程专业配置”(字段仅名称)与“T队专业配置”(字段为名称、分类A/B);T队专业与工程专业的取值均改为从配置读取,不再前端硬编码。派单号保持“故障编号/nn”(承修单位序号从 01 èµ·ï¼‰ã€‚
> **2026-09-28 ç”¨æˆ·ç¡®è®¤ï¼ˆB5 ä¹‹åŽçš„第 4 æ¡æ–°éœ€æ±‚)**:故障编号不再人工填写,改为系统自动生成,格式 `固定前缀-X号/分类-YYMMDD-nnn`(X号为3位数字;分类取T队专业配置的 A/B;YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸï¼›nnn ä¸ºåŒä¸€ X号 + åŒä¸€ T队专业 + åŒä¸€ä¸ŠæŠ¥æ—¥æœŸçš„序号,从 001 èµ·ï¼‰ã€‚系统管理新增“工程专业配置”(字段仅名称)与“T队专业配置”(字段为名称、分类A/B);T队专业与工程专业的取值均改为从配置读取,不再前端硬编码。派单号保持“故障编号/nn”(承修单位序号从 01 èµ·ï¼‰ã€‚
> 
> **2026-09-17 ç”¨æˆ·ç¡®è®¤**:AXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µå…±é€šï¼Œä¿ç•™åˆ—表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;界面、菜单和按钮统一使用“审批”,JCC处长页面及菜单统一写“JCC处长审批”;故障信息增加 CMB领导、JCC助理、JCC处长三组审批意见、审批结果(通过/不通过)、审批时间字段;增加销项理由、销项人、销项时间字段,故障上报阶段的销项统一由XCB助理执行;销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。
>
@@ -32,12 +32,12 @@
| AXU0 é¦–页门户 | å·²æ ¸å¯¹ 2026-09-09:顶部按钮区(右侧:器材审核/JC信息)+ ä¸­éƒ¨æŒ‰X号统计表(11列,示例2行,首行当前状态=小修)+ åº•部三栏(公告/共享资源下载/常用功能);“自筹器材审核”“X号当前信息”不属本页已删;菜单树另行单独核对;TD=艇队已确认;公告/共享资源下载/常用功能=占位标签;顶部按钮(器材审核/JC信息)内容后续再议 | å·²æ ¸å¯¹ 2026-09-09 |
| AXU1 æµç¨‹æ€»è§ˆ | **已核对 2026-09-11;2026-09-20 æ›´æ–°éªŒæ”¶æµç¨‹**:第一版不按原始需求,改为「简化流程 + åªåšç½‘络版」;确认流程 = T员故障上报 â†’ XCB助理审核(分配多个承修单位、拆单、生成工程单号)→ CMB领导审批 â†’ JCC助理审批 â†’ JCC处长审批 â†’ XCB助理派单(未派单故障勾选后一键派单,按承修单位生成派工单)→ åŽ‚å®¶çº¿ä¸‹ä¿®ç† â†’ åŽ‚å®¶ä¸ŠæŠ¥å®Œå·¥ä¿¡æ¯ â†’ éªŒæ”¶ç”³è¯· â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级 â†’ éªŒæ”¶å®Œæˆ | å·²æ ¸å¯¹ 2026-09-11 / éªŒæ”¶é“¾ 2026-09-20 |
| AXU2 æ•°æ®åŒ…导入 | ã€å·²è§£å†³ 2026-09-14】只做网络版、无导入功能 â†’ æœ¬é¡µæ•´ä½“取消、菜单项“数据包导入”删除,无需核对 | å·²å–消 2026-09-14 |
| AXU3 æ•…障上报 | **已核对 2026-09-16;2026-09-28 æ•…障编号改为系统自动生成**:故障编号由系统按 `潜1基-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œä¸å†äººå·¥å¡«å†™ï¼ˆåŽŸ 2026-09-20 çš„“人工填写”口径作废);字段/按钮通过;列表增加“上报日期”“上报单位”;任务状态=全局七个值(未上报 / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅);故障影响=可配置字典(示例“供气”),故障性质=可配置字典(示例“重大”);“填写故障分析报告”第一版暂不实现;新增改表单式,带默认值、下拉字段和产品结构树选择;停泊地字段取消;删除限制=未上报可删除;已上报不可删除,任务状态为“上级打回”时除外 | å·²æ ¸å¯¹ 2026-09-16 |
| AXU3 æ•…障上报 | **已核对 2026-09-16;2026-09-28 æ•…障编号改为系统自动生成**:故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œä¸å†äººå·¥å¡«å†™ï¼ˆåŽŸ 2026-09-20 çš„“人工填写”口径作废);字段/按钮通过;列表增加“上报日期”“上报单位”;任务状态=全局七个值(未上报 / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅);故障影响=可配置字典(示例“供气”),故障性质=可配置字典(示例“重大”);“填写故障分析报告”第一版暂不实现;新增改表单式,带默认值、下拉字段和产品结构树选择;停泊地字段取消;删除限制=未上报可删除;已上报不可删除,任务状态为“上级打回”时除外 | å·²æ ¸å¯¹ 2026-09-16 |
| AXU4 XCB助理审核 | **已核对 2026-09-16;2026-09-17 è¡¥å……**:本页即第 2 æ­¥ XCB助理审核;页面及菜单名称统一写“XCB助理审核”,页面内“审批”文案统一改为“审核”;分配多个承修单位、按承修单位拆单、生成工程单号均在本环节;分配承修单位时,候选列表来自“系统管理 > æ‰¿ä¿®å•位管理”,按“排序”取前7个常用承修单位显示;通过后即可领器材;后续派单页显示未派单故障,勾选后一键派单并按承修单位生成派工单;查询=工程通用条件+承修单位/故障性质/任务状态;去“添加进度备注”“CMB已阅”按钮;跟踪技术队加入故障信息且单选;“要求完工时间”统一为“要求完工日期”;任务状态=全局七个值;“上级打回”为独立状态值,与“已上报XCB”业务语义等价但流转轨迹分开;XCB本环节审核不通过时不转“上级打回”:内容不合格走“修改”,不作为故障处理走“销项”;故障上报阶段的销项统一由XCB助理执行,记录销项理由、销项人、销项时间;销项人自动取当前登录XCB助理,销项时间自动取系统当前时间 | å·²æ ¸å¯¹ 2026-09-16/17 |
| AXU5/6/7 ä¸‰ä¸ªå®¡æ‰¹é¡µ | **已核对 2026-09-17**:三页共用同一套界面和功能;保留列表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;界面、菜单和按钮统一使用“审批”,AXU7 ç»Ÿä¸€å†™â€œJCC处长审批”;三页不通过均直接退回XCB助理、任务状态为“上级打回”;故障信息增加三组审批意见、审批结果、审批时间字段 | å·²æ ¸å¯¹ 2026-09-17 |
| AXU8 å·¥ç¨‹ç®¡ç†æ€»è§ˆ | ç”¨æˆ·è¦æ±‚暂缓核对(2026-09-20);状态筛选形式后续处理 | æš‚缓 2026-09-20 |
| AXU9 CMB领导已阅 | ã€æš‚不处理 2026-09-20】不纳入当前需求,状态列含义无需继续确认 | æš‚不处理 |
| AXU10 æ´¾å•管理 | **已确认 2026-09-20**:入口在 JCC处长审批后;显示未派单故障;勾选后一键派单;按承修单位生成派工单;编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外原截图其他业务按钮暂不需要;故障编号由系统自动生成(`潜1基-X号/分类-YYMMDD-nnn`)。 | å·²ç¡®è®¤ 2026-09-20 |
| AXU10 æ´¾å•管理 | **已确认 2026-09-20**:入口在 JCC处长审批后;显示未派单故障;勾选后一键派单;按承修单位生成派工单;编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外原截图其他业务按钮暂不需要;故障编号由系统自动生成(`固定前缀-X号/分类-YYMMDD-nnn`)。 | å·²ç¡®è®¤ 2026-09-20 |
| AXU11/12/13 | ã€æš‚不处理 2026-09-20】不纳入当前需求 | æš‚不处理 |
| AXU14 å®Œå·¥æƒ…况处理 | **已确认 2026-09-20**:派单后进入本页;每一行=1个派工单,只允许勾选1行;去掉“工程单单号”,增加“完工状态”;完工状态固定为“未完工(默认)/待备件/完工待试验/完工”;去掉“导出”“添加进度备注”“销项”;勾选1行后可用“未完工/待备件/完工待试验”按钮设置状态;T员评分把修后质量评分和修后质量意见写回所选派工单;“完工”按钮仅跳转 AXU15“完工信息上报”;AXU15 æˆåŠŸæäº¤åŽï¼Œå°†å…³è”æ´¾å·¥å•å®Œå·¥çŠ¶æ€å†™ä¸ºâ€œå®Œå·¥â€ã€‚<br>【待确认】操作角色、各状态按钮启用条件、跳转 AXU15 æ—¶æ˜¯å¦ä¼ å…¥é€‰ä¸­çš„æ´¾å·¥å• | å·²ç¡®è®¤ 2026-09-20(部分细节待确认) |
| å®Œå·¥éªŒæ”¶æµç¨‹ï¼ˆ5菜单,无独立截图) | **已确认 2026-09-20**:AXU15 æˆåŠŸæäº¤åŽä¾æ¬¡è¿›å…¥éªŒæ”¶ç”³è¯· â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级;仅质检员验收、质检负责人验收提供“通过/打回”;质检员验收打回验收申请,质检负责人验收打回质检员验收;验收申请、完工验收打印输出、修后质量评级为简单动作。<br>【待确认】各环节操作角色、打印输出行为、修后质量评级与 AXU14 è¯„分字段的关系 | å·²ç¡®è®¤ 2026-09-20(细节待确认) |
@@ -78,7 +78,7 @@
16. ã€å·²è§£å†³ 2026-09-16】XCB助理审核不通过后的处理:不转“上级打回”;内容不合格时由XCB助理通过“修改”按钮修正内容,不作为故障处理时点“销项”。
17. ã€å·²è§£å†³ 2026-09-17】AXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µå…±é€šæŒ‰é’®ã€å®¡æ‰¹ç”¨è¯­ã€å®¡æ‰¹å­—段组,以及“JCC处长审批”名称统一。
18. ã€å·²è§£å†³ 2026-09-17】故障上报阶段销项统一由XCB助理执行;故障信息增加销项理由、销项人、销项时间字段;销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。
19. ã€æ›´æ–° 2026-09-28】故障编号改为系统自动生成,规则 `潜1基-X号/分类-YYMMDD-nnn`(X号3位数字、分类取T队专业配置A/B、nnn æŒ‰åŒä¸€ X号+同一T队专业+同一上报日期从 001 é€’增);2026-09-20 çš„“人工填写、系统不自动生成”口径作废。
19. ã€æ›´æ–° 2026-09-28】故障编号改为系统自动生成,规则 `固定前缀-X号/分类-YYMMDD-nnn`(X号3位数字、分类取T队专业配置A/B、nnn æŒ‰åŒä¸€ X号+同一T队专业+同一上报日期从 001 é€’增);2026-09-20 çš„“人工填写、系统不自动生成”口径作废。
20. ã€å·²è§£å†³ 2026-09-20】AXU14 å®Œå·¥æƒ…况处理:位于派单之后;每一行=1个派工单,只允许勾选1行;去掉“工程单单号”,增加“完工状态”;完工状态4种值=未完工(默认)/待备件/完工待试验/完工;去掉导出、添加进度备注、销项;勾选1行后可用“未完工/待备件/完工待试验”按钮设置状态;T员评分写回修后质量评分和意见;“完工”按钮仅跳转 AXU15;AXU15 æˆåŠŸæäº¤åŽï¼Œå°†å…³è”æ´¾å·¥å•å®Œå·¥çŠ¶æ€å†™ä¸ºâ€œå®Œå·¥â€ã€‚
21. ã€AXU14 å¾…确认】操作角色、各状态设置按钮的启用条件、跳转 AXU15 æ—¶æ˜¯å¦ä¼ å…¥é€‰ä¸­çš„æ´¾å·¥å•。
22. ã€å·²è§£å†³ 2026-09-20】完工验收流程为5个菜单:验收申请、质检员验收、质检负责人验收、完工验收打印输出、修后质量评级;只有质检员验收和质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收;其余3个菜单为简单动作。
requirement/ǰ44Ò³ÐèÇóÊáÀí.md
@@ -5,7 +5,7 @@
> ç³»ç»Ÿè®¾è®¡åŽŸåˆ™ï¼ˆç”¨æˆ·ç¡®è®¤ 2026-08-25):不搞专门的流程引擎;审批链与工程状态均用业务表上的**状态字段**管理流转,由业务操作触发并在代码中校验合法性。
> æŠ€æœ¯è·¯çº¿ï¼ˆç”¨æˆ·ç¡®è®¤ 2026-08-28 / 2026-09-20):前端 **Vue 2 + Element UI**,后端 **Spring Boot + JDK 8 + MyBatis-Plus 3.5.3**,参考 `MyProject\trafficAudit`。数据库仅开发**达梦8(DM8)**,不开发 MySQL;地址 `127.0.0.1:5236`,用户名 `SYSDBA`,密码 `SYSDBA`,数据库大小写敏感;表名和字段名统一使用大写。临抢修模式 `LX` éœ€è¦æ–°å»ºï¼›`SYS_DEPT`、`SYS_USER` ä½¿ç”¨ `CSICZB_ZB` æ¨¡å¼ä¸‹çš„同名表并通过同义词访问。详细约束见 `requirement\技术路线.md`。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-16):AXU3 æ•…障上报、AXU4 XCB助理审核已核对并固化;任务状态统一为七个值(原六态 + ä¸Šçº§æ‰“回);H å·²çº³å…¥æ•æ„Ÿè¯å­—典,非密材料统一写“是否涉H”。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-20):JCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,派工单编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外,原 AXU10 å…¶ä»–业务按钮暂不需要;AXU8 æš‚缓核对;故障编号由系统按 `潜1基-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼ˆ2026-09-28 å£å¾„更新,替代原“人工填写”),故障信息保留“故障编号”字段。AXU14“完工情况处理”位于派单之后;每一行为1个派工单,只允许勾选1行;列表去掉“工程单单号”、增加“完工状态”;去掉“导出”“添加进度备注”“销项”按钮;派工单完工状态固定为“未完工(默认)/待备件/完工待试验/完工”四种;勾选后可用三个状态按钮设置“未完工/待备件/完工待试验”,“完工”按钮只跳转AXU15,不立即改状态;AXU15成功提交后将关联派工单完工状态写为“完工”;随后进入完工验收流程:验收申请 â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级;仅质检员验收、质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收,其余3个菜单为简单动作;T员评分写回修后质量评分和意见。另确认:系统管理一级菜单包含承修单位管理、产品结构树管理、设备基础库管理;承修单位字段为名称、简称、单位性质、排序、联系人、是否失信单位,排序用于XCB助理审核时显示前7个常用承修单位;产品结构树为型号→X号→一级系统→二级系统→设备的5级树,设备基础库为一级系统→二级系统→设备的3级树;产品结构树设备为实例,设备基础库设备为类。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-20):JCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,派工单编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外,原 AXU10 å…¶ä»–业务按钮暂不需要;AXU8 æš‚缓核对;故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼ˆ2026-09-28 å£å¾„更新,替代原“人工填写”),故障信息保留“故障编号”字段。AXU14“完工情况处理”位于派单之后;每一行为1个派工单,只允许勾选1行;列表去掉“工程单单号”、增加“完工状态”;去掉“导出”“添加进度备注”“销项”按钮;派工单完工状态固定为“未完工(默认)/待备件/完工待试验/完工”四种;勾选后可用三个状态按钮设置“未完工/待备件/完工待试验”,“完工”按钮只跳转AXU15,不立即改状态;AXU15成功提交后将关联派工单完工状态写为“完工”;随后进入完工验收流程:验收申请 â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级;仅质检员验收、质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收,其余3个菜单为简单动作;T员评分写回修后质量评分和意见。另确认:系统管理一级菜单包含承修单位管理、产品结构树管理、设备基础库管理;承修单位字段为名称、简称、单位性质、排序、联系人、是否失信单位,排序用于XCB助理审核时显示前7个常用承修单位;产品结构树为型号→X号→一级系统→二级系统→设备的5级树,设备基础库为一级系统→二级系统→设备的3级树;产品结构树设备为实例,设备基础库设备为类。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-17):AXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µæŒ‰å…±é€šè§„则核对完成;保留列表、刷新、批量审批,去掉修改、CMB已阅、销项、添加进度备注;统一“审批”用语和“JCC处长审批”名称;故障信息增加三组审批意见、审批结果、审批时间字段,以及销项理由、销项人、销项时间字段。故障上报阶段的销项统一由XCB助理执行。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-20 ç»Ÿè®¡æŠ¥è¡¨ï¼‰ï¼šæ–°å¢žä¸€çº§èœå•“统计报表”,识别装备故障总体分析、故障高发设备 Top、按T统计、故障类型分布、厂家维修情况、故障数/备品备件消耗年度趋势6项功能,其中“故障类型分布”因需求暂不明确而暂缓;装备故障总体分析使用“监测项目”;厂家与承修单位同义;系统管理中的“基础设备库”统一更名为“设备基础库管理”。
> æ›´æ–°è¯´æ˜Žï¼ˆ2026-09-28 B1-B4):派工单“完工”与首页“已完工”为同一统计状态;首页与派工单的“完工待试验”口径一致;“待备件”可直接“完工”;“完工待试验”试验不通过回“未完工”,不新增“返工”状态。
@@ -125,7 +125,7 @@
- åˆ é™¤é™åˆ¶ï¼ˆ2026-09-16 ç¡®è®¤ï¼‰ï¼šæœªä¸ŠæŠ¥çš„æ•…障可以删除;已上报的故障不能删除;任务状态为“上级打回”时仍允许删除。
- ã€å·²ç¡®è®¤ 2026-09-16】第一版暂不实现“填写故障分析报告”,业务逻辑待后续明确。
- åˆ—表:工程通用列 + ä¸ŠæŠ¥æ—¥æœŸã€ä¸ŠæŠ¥å•位 + ä»»åŠ¡çŠ¶æ€ã€‚
- æ•…障编号(2026-09-28 ç¡®è®¤ï¼Œæ›¿ä»£ 2026-09-20 å£å¾„):由系统自动生成,规则 `潜1基-X号/分类-YYMMDD-nnn`,其中 X号为3位数字、分类取“T队专业配置”的 A/B、YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€nnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号(从 001 èµ·ï¼‰ï¼›ç”ŸæˆåŽä¸å¯ä¿®æ”¹ï¼Œç”¨äºŽç”Ÿæˆæ´¾å·¥å•编号。
- æ•…障编号(2026-09-28 ç¡®è®¤ï¼Œæ›¿ä»£ 2026-09-20 å£å¾„):由系统自动生成,规则 `固定前缀-X号/分类-YYMMDD-nnn`,其中 X号为3位数字、分类取“T队专业配置”的 A/B、YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€nnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号(从 001 èµ·ï¼‰ï¼›ç”ŸæˆåŽä¸å¯ä¿®æ”¹ï¼Œç”¨äºŽç”Ÿæˆæ´¾å·¥å•编号。
- ä»»åŠ¡çŠ¶æ€ç»Ÿä¸€å–å€¼ï¼ˆ2026-09-16 ç¡®è®¤ï¼‰ï¼šæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。
- æ–°å¢ž/编辑表单字段:故障编号(只读,保存后自动生成)、X号、上报单位、T队专业、负责人、上报日期、工程专业、装备名称、分机/部位、所属上级系统、工程名称、修理技术要求、跟踪技术队(单选:一队/二队/三队)、故障影响、故障性质、重大故障描述、是否自修、是否涉H、监测项目、监测数值、要求完工日期、备注。
- ã€å·²ç¡®è®¤ 2026-09-16】新增故障改为表单式录入,不使用原需求的表格式录入。
@@ -302,7 +302,7 @@
18. ã€å·²è§£å†³ 2026-09-16】XCB助理审核不通过后的处理:不转“上级打回”;内容不合格时由XCB助理通过“修改”按钮修正内容,不作为故障处理时点“销项”。
19. ã€å·²è§£å†³ 2026-09-17】AXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µå…±é€šæŒ‰é’®ã€å®¡æ‰¹ç”¨è¯­åŠä¸‰ç»„审批字段。
20. ã€å·²è§£å†³ 2026-09-17】故障上报阶段销项统一由XCB助理执行;故障信息增加销项理由、销项人、销项时间字段;销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。
21. ã€æ›´æ–° 2026-09-28】故障编号由系统自动生成(`潜1基-X号/分类-YYMMDD-nnn`);系统管理新增“工程专业配置”“T队专业配置”,两处下拉改由配置读取。
21. ã€æ›´æ–° 2026-09-28】故障编号由系统自动生成(`固定前缀-X号/分类-YYMMDD-nnn`);系统管理新增“工程专业配置”“T队专业配置”,两处下拉改由配置读取。
22. ã€AXU14 å¾…确认】本页操作角色;各状态设置按钮的启用条件;跳转 AXU15 æ—¶æ˜¯å¦ä¼ å…¥é€‰ä¸­çš„æ´¾å·¥å•。
23. ã€å·²è§£å†³ 2026-09-20】完工验收流程为5个菜单:验收申请、质检员验收、质检负责人验收、完工验收打印输出、修后质量评级。只有质检员验收和质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收;验收申请、完工验收打印输出、修后质量评级为简单动作。
24. ã€å®Œå·¥éªŒæ”¶å¾…确认】各环节操作角色;完工验收打印输出的实际打印行为;“修后质量评级”与 AXU14“T员评分”字段的关系。
requirement/¹¤³Ì״̬»úÉè¼Æ.md
@@ -6,7 +6,7 @@
> 2026-09-16 æ›´æ–°ï¼šä»»åŠ¡çŠ¶æ€ç»Ÿä¸€ä¸ºä¸ƒä¸ªå€¼ï¼šâ€œæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅”;AXU4 æ‰¿ä¿®å•位分配、拆单、工程单号生成均在 XCB助理审核环节完成。
> 2026-09-17 æ›´æ–°ï¼šAXU5/6/7 ä¸‰å¼ å®¡æ‰¹é¡µç»Ÿä¸€ä¿ç•™åˆ—表、刷新、批量审批,去掉修改、CMB已阅、销项、添加进度备注;统一“审批”用语和“JCC处长审批”名称;故障信息增加三组审批字段。
> 2026-09-20 èŒƒå›´æ”¶å£ï¼šAXU9、AXU11–AXU13、AXU16 æš‚不处理,不纳入当前需求;AXU8 ç»§ç»­æš‚缓。
> 2026-09-20 æ›´æ–°ï¼šJCC处长审批完成后直接进入 XCB助理派单页面;页面显示未派单故障,勾选后一键派单,按承修单位生成派工单;编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外原截图其他业务按钮暂不需要;故障编号由系统按 `潜1基-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼ˆ2026-09-28 æ›´æ–°ï¼Œæ›¿ä»£åŽŸâ€œäººå·¥å¡«å†™â€ï¼‰ã€‚AXU8 æš‚缓核对。AXU14 ä½äºŽæ´¾å•之后,每一行=1个派工单,只允许勾选1行;完工状态固定为“未完工(默认)/待备件/完工待试验/完工”;去掉导出、添加进度备注、销项按钮;T员评分写回修后质量评分和意见;“完工”按钮仅跳转 AXU15;AXU15 æˆåŠŸæäº¤åŽï¼Œå°†å…³è”æ´¾å·¥å•å®Œå·¥çŠ¶æ€å†™ä¸ºâ€œå®Œå·¥â€ï¼ŒéšåŽè¿›å…¥5菜单完工验收流程:验收申请 â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级;仅质检员验收、质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收;验收申请、完工验收打印输出、修后质量评级为简单动作。
> 2026-09-20 æ›´æ–°ï¼šJCC处长审批完成后直接进入 XCB助理派单页面;页面显示未派单故障,勾选后一键派单,按承修单位生成派工单;编号为故障编号 + `/01`、`/02` ç­‰ï¼›é™¤â€œä¸€é”®æ´¾å•”外原截图其他业务按钮暂不需要;故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼ˆ2026-09-28 æ›´æ–°ï¼Œæ›¿ä»£åŽŸâ€œäººå·¥å¡«å†™â€ï¼‰ã€‚AXU8 æš‚缓核对。AXU14 ä½äºŽæ´¾å•之后,每一行=1个派工单,只允许勾选1行;完工状态固定为“未完工(默认)/待备件/完工待试验/完工”;去掉导出、添加进度备注、销项按钮;T员评分写回修后质量评分和意见;“完工”按钮仅跳转 AXU15;AXU15 æˆåŠŸæäº¤åŽï¼Œå°†å…³è”æ´¾å·¥å•å®Œå·¥çŠ¶æ€å†™ä¸ºâ€œå®Œå·¥â€ï¼ŒéšåŽè¿›å…¥5菜单完工验收流程:验收申请 â†’ è´¨æ£€å‘˜éªŒæ”¶ â†’ è´¨æ£€è´Ÿè´£äººéªŒæ”¶ â†’ å®Œå·¥éªŒæ”¶æ‰“印输出 â†’ ä¿®åŽè´¨é‡è¯„级;仅质检员验收、质检负责人验收提供通过/打回,质检员打回验收申请,质检负责人打回质检员验收;验收申请、完工验收打印输出、修后质量评级为简单动作。
> 2026-09-28 æ›´æ–°ï¼ˆB1-B4):派工单“完工”与首页“已完工”为同一统计状态;两处“完工待试验”口径一致;“待备件”允许直接“完工”;“完工待试验”试验不通过回“未完工”,不新增“返工”状态。
> æ€§è´¨ï¼šéœ€æ±‚梳理阶段的**设计草案**,待用户确认后固化进需求清单。
@@ -106,7 +106,7 @@
- **任务状态**:派单不改变任务状态,仍然保持“JCC处长已阅”;“未派单/已派单”是派单环节的独立查询口径。
- **关联规则**:XCB助理审核仍负责分配承修单位、拆单并生成工程单号;派单时不再重新分配或修改承修单位。
- **常用承修单位显示**:XCB助理审核分配承修单位时,候选列表来自“系统管理 > æ‰¿ä¿®å•位管理”,按“排序”排序后显示前7个常用承修单位。
- **故障编号来源**(2026-09-28 æ›´æ–°ï¼‰ï¼šç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼Œè§„åˆ™ `潜1基-X号/分类-YYMMDD-nnn`(X号3位数字、分类取“T队专业配置”的 A/B、YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€nnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号,从 001 èµ·ï¼‰ï¼›æ•…障信息保留“故障编号”字段,生成后不可修改。
- **故障编号来源**(2026-09-28 æ›´æ–°ï¼‰ï¼šç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼Œè§„åˆ™ `固定前缀-X号/分类-YYMMDD-nnn`(X号3位数字、分类取“T队专业配置”的 A/B、YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€nnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号,从 001 èµ·ï¼‰ï¼›æ•…障信息保留“故障编号”字段,生成后不可修改。
## 3. å·¥ç¨‹çŠ¶æ€æœºï¼ˆæ‰§è¡Œé“¾è·¯ï¼‰
@@ -228,7 +228,7 @@
13. ã€å·²è§£å†³ 2026-09-16】XCB助理审核不通过后的处理:不转“上级打回”;内容不合格时由XCB助理通过“修改”按钮修正内容,不作为故障处理时点“销项”。“上级打回”仅适用于CMB领导、JCC助理、JCC处长退回XCB助理。
14. ã€å·²è§£å†³ 2026-09-17】AXU5/6/7 å…±é€šæ“ä½œã€å®¡æ‰¹ç”¨è¯­å’Œå®¡æ‰¹å­—段组:保留列表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;统一“审批”用语和“JCC处长审批”名称;增加 CMB领导/JCC助理/JCC处长三组审批意见、审批结果、审批时间字段。
15. ã€å·²è§£å†³ 2026-09-17】故障上报阶段销项:统一由XCB助理执行;故障信息增加销项理由、销项人、销项时间字段;销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。
16. ã€æ›´æ–° 2026-09-28】故障编号由系统自动生成(`潜1基-X号/分类-YYMMDD-nnn`);系统管理新增“工程专业配置”“T队专业配置”,T队专业/工程专业下拉改由配置读取。
16. ã€æ›´æ–° 2026-09-28】故障编号由系统自动生成(`固定前缀-X号/分类-YYMMDD-nnn`);系统管理新增“工程专业配置”“T队专业配置”,T队专业/工程专业下拉改由配置读取。
17. AXU14 æ“ä½œè§’色、各状态设置按钮的启用条件,以及跳转 AXU15 æ—¶æ˜¯å¦ä¼ å…¥é€‰ä¸­çš„æ´¾å·¥å•。
18. å®Œå·¥éªŒæ”¶æµç¨‹å„环节操作角色;完工验收打印输出的实际打印行为;“修后质量评级”与 AXU14“T员评分”字段的关系。
requirement/¸Ä½ø20261003.txt
New file
@@ -0,0 +1,29 @@
1、logo ç”¨public目录中的logo.png文件
2 ã€æ·»åŠ ç”¨æˆ·ç®¡ç†åŠŸèƒ½ï¼Œåˆ†å·¦å³ç»“æž„ï¼Œå·¦è¾¹è¡¨æ ¼æ˜¾ç¤ºæ‰€æœ‰ç”¨æˆ·ä¿¡æ¯ï¼ˆç”¨æˆ·åï¼Œè´¦æˆ·åï¼Œæ‰€å±žéƒ¨é—¨ï¼Œè§’è‰²æƒé™ç­‰ä¿¡æ¯ï¼‰ï¼Œå¯è¿‡æ»¤ï¼›å³è¾¹æ˜¾ç¤ºå¾…æ£€æŸ¥æ¡†çš„è§’è‰²åˆ—è¡¨ï¼Œå¯ä»¥è®¾ç½®å’Œåæ˜¾ç”¨æˆ·æ‹¥æœ‰çš„è§’è‰²ï¼Œä¸€ä¸ªç”¨æˆ·å¯ä»¥è®¾ç½®å¤šä¸ªè§’è‰²
3 ã€ç³»ç»Ÿè®°å½•用户登录日志、操作日志;操作日志主要记录添加记录或修改记录的日志
4、菜单管理的类型除了目录和菜单外,增加一个按钮类型,这个只能在菜单类型的下级,用于设置用户拥有上级菜单页面的按钮操作权限,有哪些按钮是否可以自动获取
5、代码中的“装备”字样改成“设备”
6、工作区的多文档切换标签,当打开文档多的时候出现横向滚动条的时候标签高度不够,文字显示不全
7、放开权限别的用户也可以登录
8、菜单权限管理,检查框父子节点改成联动的
9、用户管理增加批量设置角色;角色管理那里增加角色用户设置
10、产品结构树表改成同义词表ZT_BOAT_TREE,label是设备名称,info_id是设备主键id,CONNECT_ID是父节点id,CONNECT_GROUP是祖辈节点路径,LAYER是节点类型(model、side、system1、system2、equipment等) ä¸æ˜¯ä¸¥æ ¼æŒ‰ç…§è¿™ä¸ªé¡ºåºæ¥çš„, å¯èƒ½æ²¡æœ‰system2层,system2下挂的可能不是equipment类型,可能是别的类型,回头我再整理下
11、完工情况处理那里,状态修改完工的时候,不要跳转到完工信息填报,因为不是同一个用户操作的
      ç‚¹å®Œå·¥åªæ˜¯æŠŠçŠ¶æ€å®Œå·¥ï¼Œå®Œå·¥ä¿¡æ¯å¡«æŠ¥æ˜¯ç”±åˆ«çš„ç”¨æˆ·å¡«å†™çš„
12 ã€å®Œå·¥ä¿¡æ¯å¡«æŠ¥é‚£é‡Œï¼Œæ´¾å·¥å•选择列表,一是在完工处理情况那里已经确定完工状态的,二就是要求分给登录用户所属单位派工单,另外多显示一个工程名称(或项目名称)信息
17、角色管理用户设置增加一个全部/已设置切换功能
13、sensitive_words的codename的值前面加"rk_"前缀,并修改现在代码的相应的文本
14、生成故障编号中的固定前缀在后台配置文件中配置
15、所有表记录加创建人、修改人,添加修改时系统自动填写
16、所有表加逻辑删除字段,删除都是逻辑删除,查询的时候过滤掉逻辑删除的字段
17、角色管理用户设置增加一个全部/已设置切换功能
18、增加功能显示组织机构(部门树表)
19、承修单位应该到部门表里找DEPT_TYPE列为cj的记录
requirement/Ãô¸Ð´Ê×Öµä.json
@@ -1,49 +1,49 @@
{
  "sensitive_words": [
    {
      "codename": "X号",
      "codename": "rk_X号",
      "real_name": "舷号",
      "scope": "documents,ui,code",
      "note": "舰船编号"
    },
    {
      "codename": "JC",
      "codename": "rk_JC",
      "real_name": "舰船",
      "scope": "documents,ui,code",
      "note": "如 JC信息=舰船信息"
    },
    {
      "codename": "JCC",
      "codename": "rk_JCC",
      "real_name": "舰船处",
      "scope": "documents,ui,code",
      "note": "审批角色:JCC助理/JCC处长/JCC验收"
    },
    {
      "codename": "CMB",
      "codename": "rk_CMB",
      "real_name": "参谋部",
      "scope": "documents,ui,code",
      "note": "审批角色:CMB领导"
    },
    {
      "codename": "T",
      "codename": "rk_T",
      "real_name": "艇",
      "scope": "documents,ui,code",
      "note": "词根代称,组合词自动替换(T队专业、T内施工等)"
    },
    {
      "codename": "TD",
      "codename": "rk_TD",
      "real_name": "艇队",
      "scope": "documents,ui,code",
      "note": "如首页统计表TD列(X号所属艇队)"
    },
    {
      "codename": "XCB",
      "codename": "rk_XCB",
      "real_name": "修船办",
      "scope": "documents,ui,code",
      "note": "原大队助理审核角色,现页面及菜单统一称XCB助理审核"
    },
    {
      "codename": "H",
      "codename": "rk_H",
      "real_name": "[production-only sensitive value]",
      "scope": "documents,ui,code",
      "note": "AXU3 åŠåŽç»­å­—段“是否涉H”;开发环境只输出代称"
requirement/½çÃæ»¹Ô­.html
@@ -18,7 +18,7 @@
</style></head><body>
<h1>临抢修软件 Â· ç•Œé¢è¯†åˆ«è¿˜åŽŸï¼ˆåŽŸå§‹éœ€æ±‚.docx å‰ 44 é¡µï¼‰</h1>
<p style="font-size:13px;color:#555">用途:与原始文档逐张核对 OCR/识别结果。黄色高亮 = å¾…确认/OCR疑误。表格列为横向排列,不代表原图实际列顺序。</p>
<div class="note">全局口径(2026-09-11 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç¬¬ä¸€ç‰ˆ<mark>不按原始需求</mark>实现,改为 â‘  <b>简化业务流程</b> + â‘¡ <b>只提供网络版、不提供离线单机版</b>;简化流程见 AXU1 å¡ç‰‡ã€‚所有“单机版填写/离线填写/数据包导入导出”相关功能均不进入第一版。<mark>2026-09-20 ç”¨æˆ·ç¡®è®¤ï¼šJCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,编号为故障编号 + `/01`、`/02` ç­‰ã€‚除“一键派单”外,原 AXU10 å…¶ä»–业务按钮暂不需要。故障编号由系统按 `潜1基-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œæ•…éšœä¿¡æ¯ä¿ç•™â€œæ•…éšœç¼–å·â€å­—æ®µï¼ˆ2026-09-28 æ›´æ–°ï¼‰ã€‚</mark><mark>2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼šç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â€œæŠ¥ä»·ç®¡ç†â€èœå•与模块整体不做(报价处理移出主流程);艇长环节不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›AXU2 æ•°æ®åŒ…导入无需核对。</mark></div>
<div class="note">全局口径(2026-09-11 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç¬¬ä¸€ç‰ˆ<mark>不按原始需求</mark>实现,改为 â‘  <b>简化业务流程</b> + â‘¡ <b>只提供网络版、不提供离线单机版</b>;简化流程见 AXU1 å¡ç‰‡ã€‚所有“单机版填写/离线填写/数据包导入导出”相关功能均不进入第一版。<mark>2026-09-20 ç”¨æˆ·ç¡®è®¤ï¼šJCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后点击“一键派单”;按承修单位生成派工单,编号为故障编号 + `/01`、`/02` ç­‰ã€‚除“一键派单”外,原 AXU10 å…¶ä»–业务按钮暂不需要。故障编号由系统按 `固定前缀-X号/分类-YYMMDD-nnn` è‡ªåŠ¨ç”Ÿæˆï¼Œæ•…éšœä¿¡æ¯ä¿ç•™â€œæ•…éšœç¼–å·â€å­—æ®µï¼ˆ2026-09-28 æ›´æ–°ï¼‰ã€‚</mark><mark>2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼šç¦»çº¿/数据包相关功能整体取消、对应菜单项一并删除(C-12 å·²è§£å†³ï¼‰ï¼›â€œæŠ¥ä»·ç®¡ç†â€èœå•与模块整体不做(报价处理移出主流程);艇长环节不纳入第一版;JCC åŠ©ç†å®¡æ‰¹ä¸ºå¿…ç»çŽ¯èŠ‚ï¼›AXU2 æ•°æ®åŒ…导入无需核对。</mark></div>
<div class="note"><b>范围收口(2026-09-20):</b><mark>AXU9、AXU11、AXU12、AXU13、AXU16 æš‚不处理,不纳入当前需求与后续核对;AXU8 ç»§ç»­æš‚缓。</mark> åŽŸå§‹æˆªå›¾æ–‡å­—ä»ä¿ç•™ï¼Œä¾›èµ„æ–™è¿½æº¯ã€‚</div>
<h2>AXUnn â†” èœå•名 â†” æ–‡æ¡£é¡µ å¯¹ç…§è¡¨</h2>
<table><tr><th>截图编号</th><th>菜单名/页面名</th><th>文档页</th></tr><tr><td><b>AXU0</b></td><td>首页门户</td><td>文档页 5</td></tr>
@@ -59,7 +59,7 @@
<div class="note2">① ä¸å†åŒ…含“艇长批量审批/艇长审批”,直接从 T员故障上报开始;② å–消“单机版填写/离线填写数据包 + å†…网导入/导出数据包”;③ å–消“XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写)”环节,改为厂家直接上报完工信息;④ æŠ¥ä»·å¤„理不再作为主流程环节(“报价管理”菜单是否保留待确认);⑤ JCC助理审批确认为流程必经环节(原截图流程图未单列)。</div>
<div class="note2">原始截图流程(存档参考,已不采用):故障上报数据包 â†’ XCB助理审核(可直接销项)→ CMB领导审批(可打回上一步)→ JCC处长审批 â†’ XCB助理派单(打印派工单:同X号+同上报日期+同承修单位合成一个派工单)→ åŽ‚å®¶çº¿ä¸‹ä¿®ç† â†’ XCB助理完工处理(导出完工项目/单机版填写/厂家离线填写/内网上报)→ æ‰“印完工验收单;T员验收 â†’ æŠ¥ä»·å¤„理(导出待报价项目 â†’ å•机版填写/导入 â†’ ä¿®èˆ¹åŠžåŠ©ç†å®¡ä»· â†’ JCC审价)。</div>
<div class="note2">术语(2026-09-14 ç”¨æˆ·ç¡®è®¤ï¼‰ï¼šç»Ÿä¸€å†™â€œT员”(T=艇代称,T员=艇员);原文档菜单的旧写法已作废,评分与角色名一并统一为“T员”。</div>
</section><section class="card"><h3>AXU2 Â· æ•°æ®åŒ…导入(文档页 8)</h3><div class="note">导入故障上报数据包文件</div><div class="label">按钮/操作</div><div class="chips"><span class="td">选择文件</span><span class="td">导入数据包</span></div><div class="note2">导入结果统计</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">统计分类</span><span class="td">数量</span><span class="td">明细</span></div><div class="note2">统计分类明细:项目数量 / æ¶‰åŠå·¥ç¨‹ä¸“业 / æ¶‰åŠT队专业 / æ¶‰åŠè£…备</div><div class="note2">【已确认 2026-09-14】只做网络版、无导入功能 â†’ æœ¬é¡µæ•´ä½“取消,菜单项“数据包导入”删除,无需核对。</div></section><section class="card"><h3>AXU3 Â· æ•…障上报(文档页 9)</h3><div class="note">页面标题:列表</div><div class="label">查询条件</div><div class="chips"><span class="td">X号</span><span class="td">T队专业</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">上报日期(起)—(至)</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">新增</span><span class="td">上报</span><span class="td">修改</span><span class="td">删除</span><span class="td">导出</span></div><div class="note2">【已确认 2026-09-16】字段与按钮通读无误;“填写故障分析报告”第一版暂不实现,业务逻辑待后续明确。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">故障编号</span><span class="td">任务状态</span><span class="td">上报日期</span><span class="td">上报单位</span><span class="td">装备名称</span><span class="td">所属上级系统</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态(2026-09-16 ç¡®è®¤ï¼Œå…¨å±€ä¸ƒä¸ªå€¼ï¼‰ï¼šæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="note2">列表表头增加“上报日期”“上报单位”(2026-09-16 ç¡®è®¤ï¼‰ã€‚</div><div class="note2">删除限制(2026-09-16 ç¡®è®¤ï¼‰ï¼šæœªä¸ŠæŠ¥çš„æ•…障可以删除;已上报的故障不能删除;任务状态为“上级打回”时仍允许删除。</div><div class="note2">审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="note2">销项字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šé”€é¡¹ç†ç”±ã€é”€é¡¹äººã€é”€é¡¹æ—¶é—´ï¼›æ•…障上报阶段的销项统一由XCB助理执行,T队和其他审批角色不提供销项操作。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div><div class="label">新增/编辑表单字段(2026-09-16 ç¡®è®¤ï¼šæ–°å¢žæ”¹ä¸ºè¡¨å•式,原需求表格式已调整):</div><div class="chips"><span class="td">故障编号(只读,自动生成)</span><span class="td">X号</span><span class="td">上报单位</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">上报日期</span><span class="td">工程专业</span><span class="td">装备名称</span><span class="td">分机/部位</span><span class="td">所属上级系统</span><span class="td">工程名称</span><span class="td">修理技术要求</span><span class="td">故障影响<mark>可配置字典;示例值:供气;其他值待确认</mark></span><span class="td">故障性质<mark>可配置字典;示例值:重大;其他值待确认</mark></span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">字段调整(2026-09-16 ç¡®è®¤ï¼‰ï¼šåœæ³Šåœ°å­—段不保留。</div><div class="note2">故障编号(2026-09-28 ç¡®è®¤ï¼Œæ›¿ä»£ 2026-09-20 å£å¾„):由系统自动生成,规则 <b>潜1基-X号/分类-YYMMDD-nnn</b>;X号取3位数字,分类取“T队专业配置”的 A/B,YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸï¼Œnnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号(从 001 èµ·ï¼‰ã€‚表单中该字段只读,显示“保存后自动生成”。</div><div class="label">新增表单交互(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="note2">点击“新增”弹出表单,不使用原需求的表格式录入。</div><div class="note2">默认值:上报单位=登录用户所属单位;T队专业=登录用户所属专业;负责人=登录用户;上报日期=当前日期。</div><div class="note2">下拉字段:T队专业、工程专业、故障影响、故障性质。</div><div class="note2">点击“装备名称”打开产品结构树;选择装备(设备)后,自动填入“装备名称”和“所属上级系统”。</div><div class="note2">H ä¸ºæ•æ„Ÿè¯å­—典代称,字段统一写“是否涉H”。</div></section><section class="card"><h3>AXU4 Â· XCB助理审核(文档页 10)</h3><div class="note">页面定位:简化流程第 2 æ­¥â€œXCB助理审核”。</div><div class="note">命名口径(2026-09-16 ç¡®è®¤ï¼‰ï¼šé¡µé¢åŠèœå•名称统一写“XCB助理审核”,页面内原“审批”文案统一改为“审核”。</div><div class="note2">【已确认 2026-09-16】本环节完成:分配承修单位、按承修单位拆单、生成工程单号;XCB助理审核完成后即可领器材。JCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后一键派单并按承修单位生成派工单,不再分配或修改承修单位。</div><div class="label">业务规则</div><div class="note2">一个故障单可以分配多个承修单位;审核完成后按承修单位拆成多个工程单走后续流程。</div><div class="note2">工程单号在本环节生成,规则:固定-X号/班组类别-YYMMDD-厂家代号/序号,其中 YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€‚</div><div class="note2">本步确定是否监测项目、监测数值等。</div><div class="label">查询条件</div><div class="chips"><span class="td">工程通用条件</span><span class="td">承修单位</span><span class="td">故障性质</span><span class="td">任务状态</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">修改</span><span class="td">销项(仅XCB助理)</span><span class="td">审核通过/不通过</span><span class="td"><mark>已去掉:添加进度备注、CMB已阅</mark></span></div><div class="note2">任务状态统一采用全局七个值:未上报 / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="label">审核表单字段(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="chips"><span class="td">T队专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">装备名称</span><span class="td">上报日期</span><span class="td">工程名称</span><span class="td">承修单位(可分配多个;候选来自“系统管理 > æ‰¿ä¿®å•位管理”,按“排序”取前7个常用承修单位显示)</span><span class="td">跟踪技术队(从故障信息带出,单选:一队/二队/三队)</span><span class="td">要求完工日期</span><span class="td">故障性质</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">是否监测项目</span><span class="td">重大故障描述</span><span class="td">监测数据值</span><span class="td">备注</span></div><div class="note2">审核表单包含通过、不通过、关闭。XCB助理审核不通过时不转“上级打回”:内容不合格由XCB助理通过“修改”按钮自行修正;不作为故障处理时点“销项”。</div><div class="note2">销项权限与字段(2026-09-17 ç¡®è®¤ï¼‰ï¼šæ•…障上报阶段的销项统一由XCB助理执行;记录销项理由、销项人、销项时间。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div></section><section class="card"><h3>AXU5 Â· CMB领导审批(文档页 11)</h3><div class="note">列表 + æ‰¹é‡å®¡æ‰¹</div><div class="note2">AXU5/6/7 å…±é€šï¼ˆ2026-09-17 ç¡®è®¤ï¼‰ï¼šä¿ç•™åˆ—表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;三页统一使用“审批”用语。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态:统一采用全局七个值;本页审批不通过时直接转为“上级打回”并退回XCB助理。</div><div class="note2">故障信息审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="label">审批表单:</div><div class="chips"><span class="td">审批意见</span><span class="td">关闭 / é€šè¿‡ / ä¸é€šè¿‡</span></div></section><section class="card"><h3>AXU6 Â· JCC助理审批(文档页 12)</h3><div class="note">与 AXU5 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰</div><div class="note2">上级打回规则:JCC助理审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU7 Â· JCC处长审批(文档页 13)</h3><div class="note">与 AXU5/6 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰ï¼›é¡µé¢åŠèœå•名称统一为“JCC处长审批”</div><div class="note2">上级打回规则:JCC处长审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU8 Â· ä¸´æŠ¢ä¿®å·¥ç¨‹ç®¡ç†ï¼ˆæ€»è§ˆï¼‰ï¼ˆæ–‡æ¡£é¡µ 14)</h3><div class="note">页面说明:“类似已派工程单,角色不一样,手工操作”</div><div class="label">状态入口 / ç»Ÿè®¡</div><div class="chips"><span class="td">完工</span><span class="td">未完工</span><span class="td">待试验</span><span class="td">待备件</span><span class="td"><mark>已确认为工程状态筛选入口(按钮/页签),形式待定</mark></span></div><div class="note2">下方示意性表格:表头仅 Column1/Column2/Column3 + 2 è¡Œç©ºè¡Œï¼ˆä½œè€…未填字段)——按状态筛选展示工程列表,字段后续自行设计</div></section><section class="card"><h3>AXU9 Â· CMB领导已阅(文档页 15)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">页面说明:“查看CMB领导已批阅的”工程</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span><span class="td">JCC助理审批(否)</span><span class="td">JCC处长审批</span><span class="td">审批完成</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">销项</span></div><div class="note2"><mark>JCC助理审批/JCC处长审批/审批完成 ä¸‰åˆ—含义待确认</mark></div><div class="note2">流程位置(2026-09-20 ç¡®è®¤ï¼‰ï¼šJCC处长审批完成后直接进入XCB助理派单;AXU9不作为必经流程节点。</div></section><section class="card"><h3>AXU10 Â· æ´¾å•管理(文档页 16;2026-09-20 æŒ‰ç”¨æˆ·å£å¾„重定义)</h3>
</section><section class="card"><h3>AXU2 Â· æ•°æ®åŒ…导入(文档页 8)</h3><div class="note">导入故障上报数据包文件</div><div class="label">按钮/操作</div><div class="chips"><span class="td">选择文件</span><span class="td">导入数据包</span></div><div class="note2">导入结果统计</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">统计分类</span><span class="td">数量</span><span class="td">明细</span></div><div class="note2">统计分类明细:项目数量 / æ¶‰åŠå·¥ç¨‹ä¸“业 / æ¶‰åŠT队专业 / æ¶‰åŠè£…备</div><div class="note2">【已确认 2026-09-14】只做网络版、无导入功能 â†’ æœ¬é¡µæ•´ä½“取消,菜单项“数据包导入”删除,无需核对。</div></section><section class="card"><h3>AXU3 Â· æ•…障上报(文档页 9)</h3><div class="note">页面标题:列表</div><div class="label">查询条件</div><div class="chips"><span class="td">X号</span><span class="td">T队专业</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">上报日期(起)—(至)</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">新增</span><span class="td">上报</span><span class="td">修改</span><span class="td">删除</span><span class="td">导出</span></div><div class="note2">【已确认 2026-09-16】字段与按钮通读无误;“填写故障分析报告”第一版暂不实现,业务逻辑待后续明确。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">故障编号</span><span class="td">任务状态</span><span class="td">上报日期</span><span class="td">上报单位</span><span class="td">装备名称</span><span class="td">所属上级系统</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态(2026-09-16 ç¡®è®¤ï¼Œå…¨å±€ä¸ƒä¸ªå€¼ï¼‰ï¼šæœªä¸ŠæŠ¥ / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="note2">列表表头增加“上报日期”“上报单位”(2026-09-16 ç¡®è®¤ï¼‰ã€‚</div><div class="note2">删除限制(2026-09-16 ç¡®è®¤ï¼‰ï¼šæœªä¸ŠæŠ¥çš„æ•…障可以删除;已上报的故障不能删除;任务状态为“上级打回”时仍允许删除。</div><div class="note2">审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="note2">销项字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šé”€é¡¹ç†ç”±ã€é”€é¡¹äººã€é”€é¡¹æ—¶é—´ï¼›æ•…障上报阶段的销项统一由XCB助理执行,T队和其他审批角色不提供销项操作。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div><div class="label">新增/编辑表单字段(2026-09-16 ç¡®è®¤ï¼šæ–°å¢žæ”¹ä¸ºè¡¨å•式,原需求表格式已调整):</div><div class="chips"><span class="td">故障编号(只读,自动生成)</span><span class="td">X号</span><span class="td">上报单位</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">上报日期</span><span class="td">工程专业</span><span class="td">装备名称</span><span class="td">分机/部位</span><span class="td">所属上级系统</span><span class="td">工程名称</span><span class="td">修理技术要求</span><span class="td">故障影响<mark>可配置字典;示例值:供气;其他值待确认</mark></span><span class="td">故障性质<mark>可配置字典;示例值:重大;其他值待确认</mark></span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">字段调整(2026-09-16 ç¡®è®¤ï¼‰ï¼šåœæ³Šåœ°å­—段不保留。</div><div class="note2">故障编号(2026-09-28 ç¡®è®¤ï¼Œæ›¿ä»£ 2026-09-20 å£å¾„):由系统自动生成,规则 <b>固定前缀-X号/分类-YYMMDD-nnn</b>;X号取3位数字,分类取“T队专业配置”的 A/B,YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸï¼Œnnn ä¸ºåŒä¸€ X号+同一T队专业+同一上报日期的序号(从 001 èµ·ï¼‰ã€‚表单中该字段只读,显示“保存后自动生成”。</div><div class="label">新增表单交互(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="note2">点击“新增”弹出表单,不使用原需求的表格式录入。</div><div class="note2">默认值:上报单位=登录用户所属单位;T队专业=登录用户所属专业;负责人=登录用户;上报日期=当前日期。</div><div class="note2">下拉字段:T队专业、工程专业、故障影响、故障性质。</div><div class="note2">点击“装备名称”打开产品结构树;选择装备(设备)后,自动填入“装备名称”和“所属上级系统”。</div><div class="note2">H ä¸ºæ•æ„Ÿè¯å­—典代称,字段统一写“是否涉H”。</div></section><section class="card"><h3>AXU4 Â· XCB助理审核(文档页 10)</h3><div class="note">页面定位:简化流程第 2 æ­¥â€œXCB助理审核”。</div><div class="note">命名口径(2026-09-16 ç¡®è®¤ï¼‰ï¼šé¡µé¢åŠèœå•名称统一写“XCB助理审核”,页面内原“审批”文案统一改为“审核”。</div><div class="note2">【已确认 2026-09-16】本环节完成:分配承修单位、按承修单位拆单、生成工程单号;XCB助理审核完成后即可领器材。JCC处长审批完成后直接进入XCB助理派单页面;页面显示未派单故障,勾选后一键派单并按承修单位生成派工单,不再分配或修改承修单位。</div><div class="label">业务规则</div><div class="note2">一个故障单可以分配多个承修单位;审核完成后按承修单位拆成多个工程单走后续流程。</div><div class="note2">工程单号在本环节生成,规则:固定-X号/班组类别-YYMMDD-厂家代号/序号,其中 YYMMDD ä¸ºä¸ŠæŠ¥æ—¥æœŸã€‚</div><div class="note2">本步确定是否监测项目、监测数值等。</div><div class="label">查询条件</div><div class="chips"><span class="td">工程通用条件</span><span class="td">承修单位</span><span class="td">故障性质</span><span class="td">任务状态</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">查询</span><span class="td">刷新</span><span class="td">修改</span><span class="td">销项(仅XCB助理)</span><span class="td">审核通过/不通过</span><span class="td"><mark>已去掉:添加进度备注、CMB已阅</mark></span></div><div class="note2">任务状态统一采用全局七个值:未上报 / å·²ä¸ŠæŠ¥XCB / ä¸Šçº§æ‰“回 / XCB助理审核完成 / CMB领导审批完成 / JCC助理审批完成 / JCC处长已阅。</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="label">审核表单字段(2026-09-16 ç¡®è®¤ï¼‰ï¼š</div><div class="chips"><span class="td">T队专业</span><span class="td">上报单位</span><span class="td">负责人</span><span class="td">装备名称</span><span class="td">上报日期</span><span class="td">工程名称</span><span class="td">承修单位(可分配多个;候选来自“系统管理 > æ‰¿ä¿®å•位管理”,按“排序”取前7个常用承修单位显示)</span><span class="td">跟踪技术队(从故障信息带出,单选:一队/二队/三队)</span><span class="td">要求完工日期</span><span class="td">故障性质</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">是否监测项目</span><span class="td">重大故障描述</span><span class="td">监测数据值</span><span class="td">备注</span></div><div class="note2">审核表单包含通过、不通过、关闭。XCB助理审核不通过时不转“上级打回”:内容不合格由XCB助理通过“修改”按钮自行修正;不作为故障处理时点“销项”。</div><div class="note2">销项权限与字段(2026-09-17 ç¡®è®¤ï¼‰ï¼šæ•…障上报阶段的销项统一由XCB助理执行;记录销项理由、销项人、销项时间。销项人自动取当前登录XCB助理,销项时间自动取系统当前时间。</div></section><section class="card"><h3>AXU5 Â· CMB领导审批(文档页 11)</h3><div class="note">列表 + æ‰¹é‡å®¡æ‰¹</div><div class="note2">AXU5/6/7 å…±é€šï¼ˆ2026-09-17 ç¡®è®¤ï¼‰ï¼šä¿ç•™åˆ—表、刷新、批量审批;去掉修改、CMB已阅、销项、添加进度备注;三页统一使用“审批”用语。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div><div class="note2">任务状态:统一采用全局七个值;本页审批不通过时直接转为“上级打回”并退回XCB助理。</div><div class="note2">故障信息审批字段组(2026-09-17 ç¡®è®¤ï¼‰ï¼šCMB领导审批意见、CMB领导审批结果(通过/不通过)、CMB领导审批时间;JCC助理审批意见、JCC助理审批结果(通过/不通过)、JCC助理审批时间;JCC处长审批意见、JCC处长审批结果(通过/不通过)、JCC处长审批时间。</div><div class="label">审批表单:</div><div class="chips"><span class="td">审批意见</span><span class="td">关闭 / é€šè¿‡ / ä¸é€šè¿‡</span></div></section><section class="card"><h3>AXU6 Â· JCC助理审批(文档页 12)</h3><div class="note">与 AXU5 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰</div><div class="note2">上级打回规则:JCC助理审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU7 Â· JCC处长审批(文档页 13)</h3><div class="note">与 AXU5/6 å…±ç”¨åŒä¸€å¥—界面和功能(2026-09-17 ç¡®è®¤ï¼‰ï¼›é¡µé¢åŠèœå•名称统一为“JCC处长审批”</div><div class="note2">上级打回规则:JCC处长审批不通过时直接退回XCB助理,任务状态转为“上级打回”,不逐级打回。</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">刷新</span><span class="td">批量审批</span><span class="td"><mark>已去掉:修改、CMB已阅、销项、添加进度备注</mark></span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">操作</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU8 Â· ä¸´æŠ¢ä¿®å·¥ç¨‹ç®¡ç†ï¼ˆæ€»è§ˆï¼‰ï¼ˆæ–‡æ¡£é¡µ 14)</h3><div class="note">页面说明:“类似已派工程单,角色不一样,手工操作”</div><div class="label">状态入口 / ç»Ÿè®¡</div><div class="chips"><span class="td">完工</span><span class="td">未完工</span><span class="td">待试验</span><span class="td">待备件</span><span class="td"><mark>已确认为工程状态筛选入口(按钮/页签),形式待定</mark></span></div><div class="note2">下方示意性表格:表头仅 Column1/Column2/Column3 + 2 è¡Œç©ºè¡Œï¼ˆä½œè€…未填字段)——按状态筛选展示工程列表,字段后续自行设计</div></section><section class="card"><h3>AXU9 Â· CMB领导已阅(文档页 15)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">页面说明:“查看CMB领导已批阅的”工程</div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">任务状态</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span><span class="td">JCC助理审批(否)</span><span class="td">JCC处长审批</span><span class="td">审批完成</span></div><div class="label">按钮/操作</div><div class="chips"><span class="td">销项</span></div><div class="note2"><mark>JCC助理审批/JCC处长审批/审批完成 ä¸‰åˆ—含义待确认</mark></div><div class="note2">流程位置(2026-09-20 ç¡®è®¤ï¼‰ï¼šJCC处长审批完成后直接进入XCB助理派单;AXU9不作为必经流程节点。</div></section><section class="card"><h3>AXU10 Â· æ´¾å•管理(文档页 16;2026-09-20 æŒ‰ç”¨æˆ·å£å¾„重定义)</h3>
<div class="note2">页面入口:JCC处长审批完成后,流程直接进入 XCB助理派单页面。</div>
<div class="note2">页面内容:显示“未派单”的故障一览。</div>
<div class="note2">操作:勾选一个或多个故障,点击“一键派单”。除“一键派单”外,原截图中的修改、生成临修工程单、删除亚复(疑为清除重复)等业务按钮暂不需要。</div>
@@ -72,7 +72,7 @@
<div class="chips"><span class="td">一键派单(原OCR“键派单”)</span><span class="td"><mark>修改、生成临修工程单、删除亚复(疑为“清除重复”)暂不需要</mark></span></div>
<div class="label">原截图列表列(存档参考)</div>
<div class="chips"><span class="td">序号</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">上报时间</span><span class="td">修船办审核时间</span><span class="td">承修单位</span><span class="td">X号</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div>
<div class="note2">故障编号来源(2026-09-28 æ›´æ–°ï¼‰ï¼šç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼ˆæ½œ1基-X号/分类-YYMMDD-nnn),派工单编号为“故障编号/01、/02…”。第一版实际页面以“未派单故障列表 + å‹¾é€‰ + ä¸€é”®æ´¾å•”为准。</div>
<div class="note2">故障编号来源(2026-09-28 æ›´æ–°ï¼‰ï¼šç”±ç³»ç»Ÿè‡ªåŠ¨ç”Ÿæˆï¼ˆå›ºå®šå‰ç¼€-X号/分类-YYMMDD-nnn),派工单编号为“故障编号/01、/02…”。第一版实际页面以“未派单故障列表 + å‹¾é€‰ + ä¸€é”®æ´¾å•”为准。</div>
</section><section class="card"><h3>AXU11 Â· å·²æ´¾å·¥ç¨‹ï¼ˆæ–‡æ¡£é¡µ 17)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">撤销等管理</div><div class="label">按钮/操作</div><div class="chips"><span class="td">列表</span><span class="td">批量撤销</span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">上报时间</span><span class="td">修船办审核时间</span><span class="td">派单单位</span><span class="td">项目数量</span><span class="td">撤销</span><span class="td">详情</span></div><div class="note2">行操作:撤销派单 / æŸ¥çœ‹è¯¦æƒ…</div></section><section class="card"><h3>AXU12 Â· å·²æ´¾å·¥ç¨‹å•(文档页 18)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">完工管理</div><div class="label">按钮/操作</div><div class="chips"><span class="td">刷新</span><span class="td">完工</span><span class="td">完工待试验</span><span class="td">未完工</span><span class="td">待备件</span><span class="td">导出工程单</span><span class="td">添加进度备注</span><span class="td">销项</span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">工程单单号</span><span class="td">派单单号</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">上报时间</span><span class="td">修船办审核时间</span><span class="td">派单单位</span><span class="td">X号</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span></div></section><section class="card"><h3>AXU13 Â· å·¥ç¨‹å•打印输出(文档页 19)</h3><div class="note"><mark>暂不处理 2026-09-20:不纳入当前需求与后续核对。</mark></div><div class="note">页面备注:</div><div class="note2">1. æ‰¹é‡æ‰“印输出,输出简单项目信息,供报价使用</div><div class="note2">2. å•条导出也可能有多条信息,这里是按派工单列表的表格</div><div class="label">按钮/操作</div><div class="chips"><span class="td">刷新</span><span class="td">批量导出完工验收单</span><span class="td">批量导出完工单</span><span class="td">导出待完工数据包<mark>2026-09-14:已确认取消(离线数据包,C-12)</mark></span><span class="td">财务四联单</span><span class="td">完工验收单</span><span class="td">工程单</span></div><div class="label">列表列(表头)</div><div class="chips"><span class="td">序号</span><span class="td">详情</span><span class="td">工程单单号</span><span class="td">派单单号</span><span class="td">工程专业</span><span class="td">上报单位</span><span class="td">上报时间</span><span class="td">修船办审核时间</span><span class="td">派单单位</span><span class="td">X号</span><span class="td">所属上级系统</span><span class="td">装备名称</span><span class="td">工程名称及损坏情况</span><span class="td">修理技术要求</span><span class="td">工程专业</span><span class="td">T队专业</span><span class="td">跟踪技术队</span><span class="td">负责人</span><span class="td">故障影响</span><span class="td">故障性质</span><span class="td">重大故障描述</span><span class="td">是否自修</span><span class="td">是否涉H</span><span class="td">监测项目</span><span class="td">监测数值</span><span class="td">要求完工日期</span><span class="td">备注</span><span class="td">操作:查看 / å¯¼å‡º</span></div></section><section class="card"><h3>AXU14 Â· å®Œå·¥æƒ…况处理(文档页 20;2026-09-20 æŒ‰ç”¨æˆ·å£å¾„重定义)</h3>
<div class="note2">页面位置:派单后进入本页。每一行代表1个派工单,只允许勾选1行。</div>
<div class="note2">列表字段调整:去掉“工程单单号”,增加“完工状态”。派生工单时,完工状态默认值为“未完工”。</div>