From 943b68c17b27f3b0f9973533f832c48dd6afbb6f Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期二, 06 十月 2026 15:01:53 +0800
Subject: [PATCH] feat: reorganize system menus and add error logs

---
 emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java     |   17 +
 emergency-repair-web/src/views/system/ErrorLogManage.vue                                              |  151 ++++++++
 emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java          |   10 
 emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java            |  107 ++++++
 emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java              |  203 ++++++++++-
 emergency-repair-web/src/views/system/RoleManage.vue                                                  |   14 
 emergency-repair-web/src/router/index.js                                                              |    2 
 emergency-repair-web/src/views/system/MenuManage.vue                                                  |    2 
 HANDOFF.md                                                                                            |   28 +
 /dev/null                                                                                             |  140 --------
 emergency-repair-web/src/views/system/RolePermissionDialog.vue                                        |  194 +++++++++++
 emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java |    5 
 requirement/交接文件_2026.10.3.md                                                                         |   43 ++
 emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java        |    7 
 emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java            |    5 
 emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java           |   47 ++
 emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java              |   31 +
 17 files changed, 825 insertions(+), 181 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index d7093f3..3c3b292 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -753,3 +753,31 @@
 **鎺ユ墜娉ㄦ剰**
 - 涓嶈鎻愪氦 `.codex-run`銆丱ffice 閿佹枃浠舵垨 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt`銆�
 - 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
+
+## 19. 鏀跺熬蹇収锛�2026-10-06 鍛ㄤ竴锛岄渶姹� 21-22 鑿滃崟鏉冮檺鍏ュ彛涓庨敊璇棩蹇楋級
+
+**鏈疆瀹屾垚鍐呭**
+- 闇�姹� 21锛氱Щ闄ょ嫭绔嬧�滆彍鍗曟潈闄愮鐞嗏�濊彍鍗曪紝鍦ㄨ鑹茬鐞嗘搷浣滃垪鏂板鈥滄潈闄愯缃�濇寜閽紝骞跺鐢ㄨ彍鍗�/鎸夐挳鏉冮檺鏍戝脊绐楋紱鏃� `/system/menu-permissions` 璺敱鍜岄〉闈㈠凡鍒犻櫎銆�
+- 闇�姹� 22锛氱郴缁熺鐞嗘柊澧炩�滅郴缁熼厤缃� / 鏉冮檺閰嶇疆 / 鏃ュ織绠$悊鈥濅笁涓簩绾х洰褰曪紱缁勭粐鏈烘瀯銆佹壙淇崟浣嶃�佷骇鍝佺粨鏋勬爲銆佽澶囧熀纭�搴撱�佸伐绋嬩笓涓氥�乀闃熶笓涓氥�佷笁鍛樻帶鍒跺綊鍏ョ郴缁熼厤缃紝鐢ㄦ埛绠$悊銆佽鑹茬鐞嗐�佽彍鍗曠鐞嗗綊鍏ユ潈闄愰厤缃紝鐧诲綍鏃ュ織銆佹搷浣滄棩蹇椼�侀敊璇棩蹇楀綊鍏ユ棩蹇楃鐞嗐��
+- 閿欒鏃ュ織涓烘柊澧炲姛鑳斤細`GlobalExceptionHandler` 鎹曡幏鏈鐞嗙殑闈炰笟鍔″紓甯稿苟寮傛鍐欏叆 `LX.T_SYS_ERROR_LOG`锛涗笟鍔℃牎楠屽紓甯镐笉璁板綍銆傚垪琛ㄦ敮鎸佹ā鍧椼�佸紓甯哥被鍨嬨�佹椂闂寸瓫閫夛紝璇︽儏灞曠ず瀹屾暣鍫嗘爤銆�
+- 鏂板簱鍒濆鍖栧拰鏃у簱杩佺Щ鍧囦細鍒犻櫎鏃ц彍鍗曞苟琛ラ綈浜岀骇鐩綍鐨勭埗瀛愯彍鍗曞叧绯伙紱鏉冮檺鏍戠户缁繚鐣欒彍鍗曡妭鐐瑰拰鎸夐挳鑺傜偣锛岃鑹插垏鎹㈠悗澶嶉�夋鍙弽鏄俱��
+
+**涓昏鏂囦欢**
+- 鍚庣锛歚emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java`銆乣.../mapper/SysErrorLogMapper.java`銆乣.../log/ErrorLogRecorder.java`銆乣.../common/GlobalExceptionHandler.java`銆乣.../bootstrap/DataInitializer.java`銆乣.../bootstrap/DatabaseBootstrap.java`銆乣.../system/controller/SystemController.java`銆乣.../system/service/SystemService.java`銆乣.../system/service/ButtonPermissionCatalog.java`銆�
+- 鍓嶇锛歚emergency-repair-web/src/views/system/RoleManage.vue`銆乣RolePermissionDialog.vue`銆乣ErrorLogManage.vue`銆乣emergency-repair-web/src/router/index.js`銆乣MenuManage.vue`锛沗MenuPermission.vue` 宸插垹闄ゃ��
+
+**楠岃瘉缁撴灉**
+- 鍚庣 `mvn -B -DskipTests clean package` 閫氳繃锛涘墠绔� `npm run build` 閫氳繃锛屼粎鏈夋棦鏈夊寘浣撶Н鍛婅銆�
+- `node .codex-run/verify-req21-22.js` 閫氳繃锛氫笁涓簩绾х洰褰曞拰閿欒鏃ュ織鑿滃崟姝e父鏄剧ず锛屾棫鑿滃崟涓嶅瓨鍦紝瑙掕壊绠$悊鈥滄潈闄愯缃�濆脊绐楁寜閽妭鐐规暟涓� 78 涓斿弽鏄炬甯搞��
+- 浜轰负璁块棶闈炴硶閿欒鏃ュ織璇︽儏 ID 鍚庯紝璁板綍鑷姩钀藉簱骞跺彲鍦ㄩ敊璇棩蹇楅〉闈㈡煡鐪嬶紱楠岃瘉鐢ㄤ复鏃惰褰曞凡浠庢暟鎹簱娓呯悊銆�
+- 鎴浘锛歚.codex-run/role-permission-dialog-verified.png`銆乣.codex-run/error-log-verified.png`銆�
+
+**褰撳墠杩愯鐘舵�侊紙2026-10-06锛�**
+- 鍓嶇 `http://localhost:8082`锛涘悗绔� `http://127.0.0.1:8091`锛汥M8 `127.0.0.1:5236`锛汦dge CDP `9222`銆�
+- 鐧诲綍 `admin / admin123`锛涙暟鎹簱 `SYSDBA/SYSDBA`锛屼笟鍔� schema `LX`銆�
+- 鍚庣鏃ュ織锛歚.codex-run/backend-20261006-req21-22.out.log`銆乣.codex-run/backend-20261006-req21-22.err.log`銆�
+
+**鎺ユ墜娉ㄦ剰**
+- 閿欒鏃ュ織褰撳墠瑕嗙洊杩涘叆 Spring MVC 鍏ㄥ眬寮傚父澶勭悊鐨勫紓甯革紱鍚姩闃舵寮傚父銆佸紓姝ョ嚎绋嬩腑鏈繘鍏ヨ澶勭悊鍣ㄧ殑寮傚父鏆備笉浼氳嚜鍔ㄨ惤搴撱��
+- 涓嶈鎻愪氦 `.codex-run`銆丱ffice 閿佹枃浠舵垨 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt`銆�
+- 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
index b186d98..4c5edb4 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
@@ -50,6 +50,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @Component
 @Order(2)
@@ -88,6 +89,7 @@
         seedButtonMenus();
         ensureAdminRoleAssignment();
         ensureMajorConfigMenus();
+        ensureRoleMenuAncestors();
         seedMajors();
         seedContractors();
         seedTrees();
@@ -130,16 +132,24 @@
             createMenu("report-trend", reports, "鏁呴殰鏁�/澶囧搧澶囦欢娑堣�楀勾搴﹁秼鍔�", "/reports/trend", null, 1, 50);
 
             Long system = createMenu("system", null, "绯荤粺绠$悊", null, "el-icon-setting", 0, 50);
-            createMenu("organization", system, "缁勭粐鏈烘瀯", "/system/organization", null, 1, 5);
-            createMenu("contractors", system, "鎵夸慨鍗曚綅绠$悊", "/system/contractors", null, 1, 10);
-            createMenu("product-tree", system, "浜у搧缁撴瀯鏍戠鐞�", "/system/product-tree", null, 1, 20);
-            createMenu("base-equipment", system, "璁惧鍩虹搴撶鐞�", "/system/base-equipment", null, 1, 30);
-            createMenu("engineering-majors", system, "宸ョ▼涓撲笟閰嶇疆", "/system/engineering-majors", null, 1, 35);
-            createMenu("team-majors", system, "rk_T闃熶笓涓氶厤缃�", "/system/team-majors", null, 1, 36);
-            createMenu("roles", system, "瑙掕壊绠$悊", "/system/roles", null, 1, 40);
-            createMenu("menus", system, "鑿滃崟绠$悊", "/system/menus", null, 1, 50);
-            createMenu("menu-permissions", system, "鑿滃崟鏉冮檺绠$悊", "/system/menu-permissions", null, 1, 60);
-            createMenu("three-member", system, "涓夊憳鎺у埗", "/system/three-member", null, 1, 70);
+            Long systemConfig = createMenu("system-config", system, "绯荤粺閰嶇疆", null, "el-icon-s-tools", 0, 10);
+            createMenu("organization", systemConfig, "缁勭粐鏈烘瀯", "/system/organization", null, 1, 10);
+            createMenu("contractors", systemConfig, "鎵夸慨鍗曚綅绠$悊", "/system/contractors", null, 1, 20);
+            createMenu("product-tree", systemConfig, "浜у搧缁撴瀯鏍戠鐞�", "/system/product-tree", null, 1, 30);
+            createMenu("base-equipment", systemConfig, "璁惧鍩虹搴撶鐞�", "/system/base-equipment", null, 1, 40);
+            createMenu("engineering-majors", systemConfig, "宸ョ▼涓撲笟閰嶇疆", "/system/engineering-majors", null, 1, 50);
+            createMenu("team-majors", systemConfig, "rk_T闃熶笓涓氶厤缃�", "/system/team-majors", null, 1, 60);
+            createMenu("three-member", systemConfig, "涓夊憳鎺у埗", "/system/three-member", null, 1, 70);
+
+            Long permissionConfig = createMenu("permission-config", system, "鏉冮檺閰嶇疆", null, "el-icon-lock", 0, 20);
+            createMenu("users", permissionConfig, "鐢ㄦ埛绠$悊", "/system/users", null, 1, 10);
+            createMenu("roles", permissionConfig, "瑙掕壊绠$悊", "/system/roles", null, 1, 20);
+            createMenu("menus", permissionConfig, "鑿滃崟绠$悊", "/system/menus", null, 1, 30);
+
+            Long logConfig = createMenu("log-config", system, "鏃ュ織绠$悊", null, "el-icon-document", 0, 30);
+            createMenu("login-logs", logConfig, "鐧诲綍鏃ュ織", "/system/login-logs", null, 1, 10);
+            createMenu("operation-logs", logConfig, "鎿嶄綔鏃ュ織", "/system/operation-logs", null, 1, 20);
+            createMenu("error-logs", logConfig, "閿欒鏃ュ織", "/system/error-logs", null, 1, 30);
         } else {
             for (SysMenu menu : menuMapper.selectList(null)) {
                 menuIds.put(menu.getUrl() == null ? "id-" + menu.getId() : menu.getUrl(), menu.getId());
@@ -178,36 +188,112 @@
         SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                 .eq(SysMenu::getName, "绯荤粺绠$悊")
                 .last("LIMIT 1"));
-        Long pid = system == null ? 0L : system.getId();
-        ensureMenu(pid, "宸ョ▼涓撲笟閰嶇疆", "/system/engineering-majors", 35);
-        ensureMenu(pid, "rk_T闃熶笓涓氶厤缃�", "/system/team-majors", 36);
+        SysMenu config = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getName, "绯荤粺閰嶇疆")
+                .eq(SysMenu::getMenuType, 0)
+                .last("LIMIT 1"));
+        Long pid = config == null ? (system == null ? 0L : system.getId()) : config.getId();
+        ensureMenu(pid, "宸ョ▼涓撲笟閰嶇疆", "/system/engineering-majors", 50);
+        ensureMenu(pid, "rk_T闃熶笓涓氶厤缃�", "/system/team-majors", 60);
     }
 
-    private void ensureMenu(Long pid, String name, String url, int sort) {
-        if (menuMapper.selectCount(Wrappers.<SysMenu>lambdaQuery().eq(SysMenu::getUrl, url)) > 0) {
-            return;
+    private Long ensureGroupMenu(Long pid, String name, String icon, int sort) {
+        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getPid, pid)
+                .eq(SysMenu::getName, name)
+                .eq(SysMenu::getMenuType, 0)
+                .last("LIMIT 1"));
+        LocalDateTime now = LocalDateTime.now();
+        if (menu == null) {
+            menu = new SysMenu();
+            menu.setPid(pid);
+            menu.setName(name);
+            menu.setIcon(icon);
+            menu.setMenuType(0);
+            menu.setSortNo(sort);
+            menu.setVisible(1);
+            menu.setCreateTime(now);
+            menu.setUpdateTime(now);
+            menuMapper.insert(menu);
+            return menu.getId();
         }
-        SysMenu menu = new SysMenu();
-        menu.setPid(pid);
-        menu.setName(name);
-        menu.setUrl(url);
-        menu.setMenuType(1);
-        menu.setSortNo(sort);
-        menu.setVisible(1);
-        menu.setCreateTime(LocalDateTime.now());
-        menu.setUpdateTime(LocalDateTime.now());
-        menuMapper.insert(menu);
+        boolean changed = !java.util.Objects.equals(menu.getIcon(), icon)
+                || !java.util.Objects.equals(menu.getSortNo(), sort)
+                || !java.util.Objects.equals(menu.getVisible(), 1);
+        if (changed) {
+            menu.setIcon(icon);
+            menu.setSortNo(sort);
+            menu.setVisible(1);
+            menu.setUpdateTime(now);
+            menuMapper.updateById(menu);
+        }
+        return menu.getId();
+    }
+
+    private Long ensureMenu(Long pid, String name, String url, int sort) {
+        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getUrl, url).last("LIMIT 1"));
+        LocalDateTime now = LocalDateTime.now();
+        if (menu == null) {
+            menu = new SysMenu();
+            menu.setPid(pid);
+            menu.setName(name);
+            menu.setUrl(url);
+            menu.setMenuType(1);
+            menu.setSortNo(sort);
+            menu.setVisible(1);
+            menu.setCreateTime(now);
+            menu.setUpdateTime(now);
+            menuMapper.insert(menu);
+            return menu.getId();
+        }
+        boolean changed = !java.util.Objects.equals(menu.getPid(), pid)
+                || !java.util.Objects.equals(menu.getName(), name)
+                || !java.util.Objects.equals(menu.getSortNo(), sort)
+                || !java.util.Objects.equals(menu.getMenuType(), 1)
+                || !java.util.Objects.equals(menu.getVisible(), 1);
+        if (changed) {
+            menu.setPid(pid);
+            menu.setName(name);
+            menu.setMenuType(1);
+            menu.setSortNo(sort);
+            menu.setVisible(1);
+            menu.setUpdateTime(now);
+            menuMapper.updateById(menu);
+        }
+        return menu.getId();
     }
 
     private void ensureSystemMenus() {
         SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                 .eq(SysMenu::getName, "\u7cfb\u7edf\u7ba1\u7406").last("LIMIT 1"));
         Long pid = system == null ? 0L : system.getId();
-        ensureMenu(pid, "缁勭粐鏈烘瀯", "/system/organization", 5);
-        ensureMenu(pid, "\u7528\u6237\u7ba1\u7406", "/system/users", 37);
-        ensureMenu(pid, "\u767b\u5f55\u65e5\u5fd7", "/system/login-logs", 65);
-        ensureMenu(pid, "\u64cd\u4f5c\u65e5\u5fd7", "/system/operation-logs", 66);
-        grantAdminMenus("/system/organization", "/system/users", "/system/login-logs", "/system/operation-logs");
+        Long systemConfig = ensureGroupMenu(pid, "绯荤粺閰嶇疆", "el-icon-s-tools", 10);
+        Long permissionConfig = ensureGroupMenu(pid, "鏉冮檺閰嶇疆", "el-icon-lock", 20);
+        Long logConfig = ensureGroupMenu(pid, "鏃ュ織绠$悊", "el-icon-document", 30);
+
+        ensureMenu(systemConfig, "缁勭粐鏈烘瀯", "/system/organization", 10);
+        ensureMenu(systemConfig, "鎵夸慨鍗曚綅绠$悊", "/system/contractors", 20);
+        ensureMenu(systemConfig, "浜у搧缁撴瀯鏍戠鐞�", "/system/product-tree", 30);
+        ensureMenu(systemConfig, "璁惧鍩虹搴撶鐞�", "/system/base-equipment", 40);
+        ensureMenu(systemConfig, "宸ョ▼涓撲笟閰嶇疆", "/system/engineering-majors", 50);
+        ensureMenu(systemConfig, "rk_T闃熶笓涓氶厤缃�", "/system/team-majors", 60);
+        ensureMenu(systemConfig, "涓夊憳鎺у埗", "/system/three-member", 70);
+
+        ensureMenu(permissionConfig, "鐢ㄦ埛绠$悊", "/system/users", 10);
+        ensureMenu(permissionConfig, "瑙掕壊绠$悊", "/system/roles", 20);
+        ensureMenu(permissionConfig, "鑿滃崟绠$悊", "/system/menus", 30);
+
+        ensureMenu(logConfig, "鐧诲綍鏃ュ織", "/system/login-logs", 10);
+        ensureMenu(logConfig, "鎿嶄綔鏃ュ織", "/system/operation-logs", 20);
+        ensureMenu(logConfig, "閿欒鏃ュ織", "/system/error-logs", 30);
+
+        removeMenuByUrl("/system/menu-permissions");
+        grantAdminMenuIds(systemConfig, permissionConfig, logConfig);
+        grantAdminMenus("/system/organization", "/system/contractors", "/system/product-tree",
+                "/system/base-equipment", "/system/engineering-majors", "/system/team-majors",
+                "/system/three-member", "/system/users", "/system/roles", "/system/menus",
+                "/system/login-logs", "/system/operation-logs", "/system/error-logs");
     }
 
     private void grantAdminMenus(String... urls) {
@@ -226,6 +312,61 @@
         }
     }
 
+    private void grantAdminMenuIds(Long... menuIds) {
+        SysRole adminRole = roleMapper.selectOne(Wrappers.<SysRole>lambdaQuery()
+                .eq(SysRole::getCode, "SYS_ADMIN").last("LIMIT 1"));
+        if (adminRole == null) {
+            return;
+        }
+        for (Long menuId : menuIds) {
+            ensureRoleMenu(adminRole.getId(), menuId);
+            ensureTenantMenu(1L, menuId);
+        }
+    }
+
+    private void removeMenuByUrl(String url) {
+        SysMenu menu = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getUrl, url).last("LIMIT 1"));
+        if (menu == null) {
+            return;
+        }
+        List<SysMenu> children = menuMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getPid, menu.getId()));
+        for (SysMenu child : children) {
+            removeMenuRelations(child.getId());
+            menuMapper.deleteById(child.getId());
+        }
+        removeMenuRelations(menu.getId());
+        menuMapper.deleteById(menu.getId());
+    }
+
+    private void removeMenuRelations(Long menuId) {
+        roleMenuMapper.delete(Wrappers.<SysRoleMenu>lambdaQuery().eq(SysRoleMenu::getMenuId, menuId));
+        tenantMenuMapper.delete(Wrappers.<SysTenantMenu>lambdaQuery().eq(SysTenantMenu::getMenuId, menuId));
+    }
+
+    private void ensureRoleMenuAncestors() {
+        Map<Long, SysMenu> menuById = menuMapper.selectList(null).stream()
+                .collect(java.util.stream.Collectors.toMap(SysMenu::getId, item -> item));
+        List<SysRoleMenu> relations = new ArrayList<>(roleMenuMapper.selectList(null));
+        Set<String> existing = relations.stream()
+                .map(item -> item.getRoleId() + "-" + item.getMenuId())
+                .collect(java.util.stream.Collectors.toSet());
+        for (SysRoleMenu relation : relations) {
+            SysMenu menu = menuById.get(relation.getMenuId());
+            Long cursor = menu == null ? null : menu.getPid();
+            int guard = 0;
+            while (cursor != null && cursor != 0L && guard++ < 32) {
+                String key = relation.getRoleId() + "-" + cursor;
+                if (existing.add(key)) {
+                    ensureRoleMenu(relation.getRoleId(), cursor);
+                }
+                SysMenu parent = menuById.get(cursor);
+                cursor = parent == null ? null : parent.getPid();
+            }
+        }
+    }
+
     /**
      * Materializes the declarative button catalog as MENU_TYPE=2 rows.
      * Existing rows are left untouched so manual menu edits are preserved.
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java
index d1c6d10..4797f5e 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java
@@ -17,7 +17,8 @@
             "T_FAULT", "T_ENGINEERING_MAJOR", "T_TEAM_MAJOR", "T_CONTRACTOR",
             "T_BASE_EQUIPMENT", "T_PRODUCT_TREE", "T_WORK_ORDER", "T_COMPLETION_INFO",
             "T_ACCEPTANCE", "T_SYS_MENU", "T_SYS_ROLE", "T_SYS_ROLE_MENU",
-            "T_SYS_TENANT_MENU", "T_SYS_ROLE_USER", "T_SYS_LOGIN_LOG", "T_SYS_OPERATION_LOG"
+            "T_SYS_TENANT_MENU", "T_SYS_ROLE_USER", "T_SYS_LOGIN_LOG", "T_SYS_OPERATION_LOG",
+            "T_SYS_ERROR_LOG"
     };
     private final JdbcTemplate jdbcTemplate;
 
@@ -40,6 +41,7 @@
         ensureIndex("IX_T_COMPLETION_WORK_ORDER", "CREATE INDEX LX.IX_T_COMPLETION_WORK_ORDER ON LX.T_COMPLETION_INFO(WORK_ORDER_ID)");
         ensureIndex("IX_T_SYS_LOGIN_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_LOGIN_LOG_TIME ON LX.T_SYS_LOGIN_LOG(LOGIN_TIME)");
         ensureIndex("IX_T_SYS_OPERATION_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_OPERATION_LOG_TIME ON LX.T_SYS_OPERATION_LOG(CREATE_TIME)");
+        ensureIndex("IX_T_SYS_ERROR_LOG_TIME", "CREATE INDEX LX.IX_T_SYS_ERROR_LOG_TIME ON LX.T_SYS_ERROR_LOG(ERROR_TIME)");
         ensureIndex("IX_T_SYS_ROLE_USER_USER", "CREATE INDEX LX.IX_T_SYS_ROLE_USER_USER ON LX.T_SYS_ROLE_USER(USER_ID)");
     }
 
@@ -122,6 +124,7 @@
         ddl.put("T_SYS_ROLE_USER", "CREATE TABLE LX.T_SYS_ROLE_USER (ID BIGINT NOT NULL PRIMARY KEY, ROLE_ID BIGINT, USER_ID BIGINT, CREATE_TIME TIMESTAMP)");
         ddl.put("T_SYS_LOGIN_LOG", "CREATE TABLE LX.T_SYS_LOGIN_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), DEPT_NAME VARCHAR(120), LOGIN_IP VARCHAR(200), STATUS VARCHAR(20), MESSAGE VARCHAR(500), LOGIN_TIME TIMESTAMP)");
         ddl.put("T_SYS_OPERATION_LOG", "CREATE TABLE LX.T_SYS_OPERATION_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), MODULE VARCHAR(100), OPERATION_TYPE VARCHAR(40), TARGET_TYPE VARCHAR(100), TARGET_ID VARCHAR(80), DESCRIPTION VARCHAR(500), REQUEST_METHOD VARCHAR(10), REQUEST_URI VARCHAR(300), RESULT VARCHAR(20), CREATE_TIME TIMESTAMP)");
+        ddl.put("T_SYS_ERROR_LOG", "CREATE TABLE LX.T_SYS_ERROR_LOG (ID BIGINT NOT NULL PRIMARY KEY, USER_ID BIGINT, USER_NAME VARCHAR(120), NICK_NAME VARCHAR(120), MODULE VARCHAR(100), REQUEST_METHOD VARCHAR(10), REQUEST_URI VARCHAR(500), EXCEPTION_TYPE VARCHAR(300), MESSAGE VARCHAR(2000), STACK_TRACE CLOB, ERROR_TIME TIMESTAMP)");
         return ddl;
     }
 }
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java b/emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java
index 68c3eca..5bfff63 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java
@@ -1,17 +1,25 @@
 package com.emergencyrepair.common;
 
+import com.emergencyrepair.system.log.ErrorLogRecorder;
+import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 
+import javax.servlet.http.HttpServletRequest;
+
 @RestControllerAdvice
+@RequiredArgsConstructor
 public class GlobalExceptionHandler {
+    private final ErrorLogRecorder errorLogRecorder;
+
     @ExceptionHandler(BusinessException.class)
     public Result<Void> handleBusiness(BusinessException exception) {
         return Result.error(exception.getMessage());
     }
 
     @ExceptionHandler(Exception.class)
-    public Result<Void> handleOther(Exception exception) {
+    public Result<Void> handleOther(Exception exception, HttpServletRequest request) {
+        errorLogRecorder.record(exception, request);
         exception.printStackTrace();
         return Result.error(exception.getMessage() == null ? "绯荤粺寮傚父" : exception.getMessage());
     }
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java
index c167be9..09f271c 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java
@@ -8,6 +8,7 @@
 import com.emergencyrepair.business.entity.TeamMajor;
 import com.emergencyrepair.common.Result;
 import com.emergencyrepair.system.entity.SysMenu;
+import com.emergencyrepair.system.entity.SysErrorLog;
 import com.emergencyrepair.system.entity.SysLoginLog;
 import com.emergencyrepair.system.entity.SysOperationLog;
 import com.emergencyrepair.system.entity.SysRole;
@@ -111,6 +112,22 @@
         return Result.ok(systemService.operationLogPage(current, size, keyword, module, operationType, startDate, endDate));
     }
 
+    @GetMapping("/error-logs")
+    public Result<Page<SysErrorLog>> errorLogs(@RequestParam(defaultValue = "1") int current,
+                                              @RequestParam(defaultValue = "20") int size,
+                                              @RequestParam(required = false) String keyword,
+                                              @RequestParam(required = false) String module,
+                                              @RequestParam(required = false) String exceptionType,
+                                              @RequestParam(required = false) String startDate,
+                                              @RequestParam(required = false) String endDate) {
+        return Result.ok(systemService.errorLogPage(current, size, keyword, module, exceptionType, startDate, endDate));
+    }
+
+    @GetMapping("/error-logs/{id}")
+    public Result<SysErrorLog> errorLog(@PathVariable Long id) {
+        return Result.ok(systemService.errorLog(id));
+    }
+
     @PostMapping("/menu")
     public Result<SysMenu> createMenu(@RequestBody SysMenu menu) {
         menu.setId(null);
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java
new file mode 100644
index 0000000..75dda2f
--- /dev/null
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java
@@ -0,0 +1,31 @@
+package com.emergencyrepair.system.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.emergencyrepair.common.entity.BaseAuditEntity;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("LX.T_SYS_ERROR_LOG")
+public class SysErrorLog extends BaseAuditEntity {
+    @TableId(value = "ID", type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+    @JsonSerialize(using = ToStringSerializer.class)
+    @TableField("USER_ID") private Long userId;
+    @TableField("USER_NAME") private String userName;
+    @TableField("NICK_NAME") private String nickName;
+    @TableField("MODULE") private String module;
+    @TableField("REQUEST_METHOD") private String requestMethod;
+    @TableField("REQUEST_URI") private String requestUri;
+    @TableField("EXCEPTION_TYPE") private String exceptionType;
+    @TableField("MESSAGE") private String message;
+    @TableField("STACK_TRACE") private String stackTrace;
+    @TableField("ERROR_TIME") private LocalDateTime errorTime;
+}
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java
new file mode 100644
index 0000000..6c17876
--- /dev/null
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java
@@ -0,0 +1,107 @@
+package com.emergencyrepair.system.log;
+
+import com.emergencyrepair.common.audit.CurrentUserContext;
+import com.emergencyrepair.system.entity.SysErrorLog;
+import com.emergencyrepair.system.mapper.SysErrorLogMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * Persists unhandled server exceptions so administrators can inspect runtime bugs.
+ */
+@Component
+@RequiredArgsConstructor
+public class ErrorLogRecorder {
+    private static final int MAX_MESSAGE_LENGTH = 2000;
+    private static final int MAX_STACK_LENGTH = 60000;
+
+    private final SysErrorLogMapper errorLogMapper;
+    private final JdbcTemplate jdbcTemplate;
+
+    public void record(Exception exception, HttpServletRequest request) {
+        if (exception == null) {
+            return;
+        }
+        try {
+            String username = CurrentUserContext.get();
+            Long userId = resolveUserId(username);
+            SysErrorLog log = new SysErrorLog();
+            log.setUserId(userId);
+            log.setUserName(username);
+            log.setNickName(resolveNickName(userId));
+            log.setModule(resolveModule(request));
+            log.setRequestMethod(request == null ? null : limit(request.getMethod(), 10));
+            log.setRequestUri(request == null ? null : limit(request.getRequestURI(), 500));
+            log.setExceptionType(limit(exception.getClass().getName(), 300));
+            log.setMessage(limit(resolveMessage(exception), MAX_MESSAGE_LENGTH));
+            log.setStackTrace(limit(stackTrace(exception), MAX_STACK_LENGTH));
+            log.setErrorTime(LocalDateTime.now());
+            errorLogMapper.insert(log);
+        } catch (Exception ignored) {
+            // Error logging must never interfere with the original error response.
+        }
+    }
+
+    private Long resolveUserId(String username) {
+        if (!StringUtils.hasText(username)) {
+            return null;
+        }
+        List<Long> ids = jdbcTemplate.query(
+                "SELECT USER_ID FROM CSICZB_ZB.SYS_USER WHERE USER_NAME = ? AND (DEL_FLAG = '0' OR DEL_FLAG IS NULL)",
+                new Object[]{username}, (rs, rowNum) -> rs.getLong(1));
+        return ids.isEmpty() ? null : ids.get(0);
+    }
+
+    private String resolveNickName(Long userId) {
+        if (userId == null) {
+            return null;
+        }
+        List<String> names = jdbcTemplate.query(
+                "SELECT NICK_NAME FROM CSICZB_ZB.SYS_USER WHERE USER_ID = ?",
+                new Object[]{userId}, (rs, rowNum) -> rs.getString(1));
+        return names.isEmpty() ? null : names.get(0);
+    }
+
+    private String resolveMessage(Exception exception) {
+        String message = exception.getMessage();
+        return StringUtils.hasText(message) ? message : exception.getClass().getSimpleName();
+    }
+
+    private String stackTrace(Exception exception) {
+        StringWriter writer = new StringWriter();
+        exception.printStackTrace(new PrintWriter(writer));
+        return writer.toString();
+    }
+
+    private String resolveModule(HttpServletRequest request) {
+        if (request == null || !StringUtils.hasText(request.getRequestURI())) {
+            return "鍏朵粬";
+        }
+        String uri = request.getRequestURI();
+        if (uri.startsWith("/api/faults") || uri.startsWith("/api/fault")) return "鏁呴殰绠$悊";
+        if (uri.startsWith("/api/work-orders") || uri.startsWith("/api/completion")) return "瀹屽伐绠$悊";
+        if (uri.startsWith("/api/acceptances")) return "瀹屽伐楠屾敹";
+        if (uri.startsWith("/api/reports")) return "缁熻鎶ヨ〃";
+        if (uri.startsWith("/api/system/users")) return "鐢ㄦ埛绠$悊";
+        if (uri.startsWith("/api/system/roles")) return "瑙掕壊绠$悊";
+        if (uri.startsWith("/api/system/menu")) return "鑿滃崟绠$悊";
+        if (uri.startsWith("/api/system/contractors")) return "鎵夸慨鍗曚綅绠$悊";
+        if (uri.startsWith("/api/system")) return "绯荤粺绠$悊";
+        return "鍏朵粬";
+    }
+
+    private String limit(String value, int maxLength) {
+        if (value == null || value.length() <= maxLength) {
+            return value;
+        }
+        return value.substring(0, maxLength);
+    }
+}
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java
new file mode 100644
index 0000000..5343f90
--- /dev/null
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java
@@ -0,0 +1,7 @@
+package com.emergencyrepair.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.emergencyrepair.system.entity.SysErrorLog;
+
+public interface SysErrorLogMapper extends BaseMapper<SysErrorLog> {
+}
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
index d530fa1..982796d 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
@@ -86,12 +86,11 @@
         put("/system/engineering-majors", def("system:major:add", "\u65b0\u589e"), def("system:major:edit", "\u7f16\u8f91"), def("system:major:delete", "\u5220\u9664"));
         put("/system/team-majors", def("system:major:add", "\u65b0\u589e"), def("system:major:edit", "\u7f16\u8f91"), def("system:major:delete", "\u5220\u9664"));
         put("/system/users", def("system:users:query", "\u67e5\u8be2"), def("system:users:assign", "\u89d2\u8272\u6388\u6743"));
-        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"));
-        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"), def("system:roles:assign", "\u7528\u6237\u8bbe\u7f6e"));
+        put("/system/roles", def("system:roles:add", "\u65b0\u589e"), def("system:roles:edit", "\u7f16\u8f91"), def("system:roles:delete", "\u5220\u9664"), def("system:roles:assign", "\u7528\u6237\u8bbe\u7f6e"), def("system:roles:permission", "\u6743\u9650\u8bbe\u7f6e"));
         put("/system/menus", def("system:menus:add", "\u65b0\u589e"), def("system:menus:edit", "\u7f16\u8f91"), def("system:menus:delete", "\u5220\u9664"), def("system:menus:buttons", "\u83dc\u5355\u6309\u94ae\u540c\u6b65"));
-        put("/system/menu-permissions", def("system:menu-permissions:save", "\u4fdd\u5b58\u6388\u6743"));
         put("/system/login-logs", def("system:login-logs:query", "\u67e5\u8be2"), def("system:login-logs:export", "\u5bfc\u51fa"));
         put("/system/operation-logs", def("system:operation-logs:query", "\u67e5\u8be2"), def("system:operation-logs:export", "\u5bfc\u51fa"));
+        put("/system/error-logs", def("system:error-logs:query", "\u67e5\u8be2"), def("system:error-logs:detail", "\u8be6\u60c5"));
     }
 
     private static void put(String url, ButtonDefinition... definitions) {
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
index c2b4797..9f0cfd1 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
@@ -14,6 +14,7 @@
 import com.emergencyrepair.business.mapper.TeamMajorMapper;
 import com.emergencyrepair.common.BusinessException;
 import com.emergencyrepair.system.entity.SysMenu;
+import com.emergencyrepair.system.entity.SysErrorLog;
 import com.emergencyrepair.system.entity.SysLoginLog;
 import com.emergencyrepair.system.entity.SysOperationLog;
 import com.emergencyrepair.system.entity.SysRole;
@@ -21,6 +22,7 @@
 import com.emergencyrepair.system.entity.SysRoleUser;
 import com.emergencyrepair.system.entity.SysTenantMenu;
 import com.emergencyrepair.system.mapper.SysMenuMapper;
+import com.emergencyrepair.system.mapper.SysErrorLogMapper;
 import com.emergencyrepair.system.mapper.SysLoginLogMapper;
 import com.emergencyrepair.system.mapper.SysOperationLogMapper;
 import com.emergencyrepair.system.mapper.SysRoleMapper;
@@ -59,6 +61,7 @@
     private final SysRoleUserMapper roleUserMapper;
     private final SysLoginLogMapper loginLogMapper;
     private final SysOperationLogMapper operationLogMapper;
+    private final SysErrorLogMapper errorLogMapper;
     private final ButtonPermissionCatalog buttonPermissionCatalog;
     private final JdbcTemplate jdbcTemplate;
     private final EngineeringMajorMapper engineeringMajorMapper;
@@ -887,9 +890,10 @@
             }
         }
         final Set<Long> allowedIds = allowed;
+        Set<Long> visibleIds = new LinkedHashSet<>(expandWithAncestors(new ArrayList<>(allowed)));
         List<SysMenu> navigable = listMenus().stream()
                 .filter(menu -> menu.getMenuType() == null || menu.getMenuType() != 2)
-                .filter(menu -> allowedIds.contains(menu.getId()))
+                .filter(menu -> visibleIds.contains(menu.getId()))
                 .collect(Collectors.toList());
         return buildTree(navigable);
     }
@@ -1038,6 +1042,47 @@
         return operationLogMapper.selectPage(page, wrapper);
     }
 
+    public Page<SysErrorLog> errorLogPage(int current, int size, String keyword, String module, String exceptionType, String startDate, String endDate) {
+        Page<SysErrorLog> page = new Page<>(current <= 0 ? 1 : current, size <= 0 ? 20 : size);
+        LambdaQueryWrapper<SysErrorLog> wrapper = Wrappers.lambdaQuery();
+        wrapper.select(SysErrorLog::getId, SysErrorLog::getUserId, SysErrorLog::getUserName,
+                SysErrorLog::getNickName, SysErrorLog::getModule, SysErrorLog::getRequestMethod,
+                SysErrorLog::getRequestUri, SysErrorLog::getExceptionType, SysErrorLog::getMessage,
+                SysErrorLog::getErrorTime);
+        if (StringUtils.hasText(keyword)) {
+            String value = keyword.trim();
+            wrapper.and(item -> item.like(SysErrorLog::getUserName, value)
+                    .or().like(SysErrorLog::getNickName, value)
+                    .or().like(SysErrorLog::getRequestUri, value)
+                    .or().like(SysErrorLog::getExceptionType, value)
+                    .or().like(SysErrorLog::getMessage, value));
+        }
+        if (StringUtils.hasText(module)) {
+            wrapper.eq(SysErrorLog::getModule, module.trim());
+        }
+        if (StringUtils.hasText(exceptionType)) {
+            wrapper.like(SysErrorLog::getExceptionType, exceptionType.trim());
+        }
+        LocalDateTime start = parseStart(startDate);
+        LocalDateTime end = parseEnd(endDate);
+        if (start != null) {
+            wrapper.ge(SysErrorLog::getErrorTime, start);
+        }
+        if (end != null) {
+            wrapper.le(SysErrorLog::getErrorTime, end);
+        }
+        wrapper.orderByDesc(SysErrorLog::getErrorTime);
+        return errorLogMapper.selectPage(page, wrapper);
+    }
+
+    public SysErrorLog errorLog(Long id) {
+        SysErrorLog log = errorLogMapper.selectById(id);
+        if (log == null) {
+            throw new BusinessException("閿欒鏃ュ織涓嶅瓨鍦�");
+        }
+        return log;
+    }
+
     public Long resolveUserId(String username) {
         if (!StringUtils.hasText(username)) {
             return null;
diff --git a/emergency-repair-web/src/router/index.js b/emergency-repair-web/src/router/index.js
index 3339885..5ba2af1 100644
--- a/emergency-repair-web/src/router/index.js
+++ b/emergency-repair-web/src/router/index.js
@@ -36,8 +36,8 @@
       { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue'), meta: { title: '瑙掕壊绠$悊', cacheName: 'RoleManage' } },
       { path: 'system/login-logs', component: () => import('@/views/system/LoginLogManage.vue'), meta: { title: '鐧诲綍鏃ュ織', cacheName: 'LoginLogManage' } },
       { path: 'system/operation-logs', component: () => import('@/views/system/OperationLogManage.vue'), meta: { title: '鎿嶄綔鏃ュ織', cacheName: 'OperationLogManage' } },
+      { path: 'system/error-logs', component: () => import('@/views/system/ErrorLogManage.vue'), meta: { title: '閿欒鏃ュ織', cacheName: 'ErrorLogManage' } },
       { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue'), meta: { title: '鑿滃崟绠$悊', cacheName: 'MenuManage' } },
-      { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue'), meta: { title: '鑿滃崟鏉冮檺', cacheName: 'MenuPermission' } },
       { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue'), meta: { title: '涓夊憳绠$悊', cacheName: 'ThreeMember' } }
     ]
   }
diff --git a/emergency-repair-web/src/views/system/ErrorLogManage.vue b/emergency-repair-web/src/views/system/ErrorLogManage.vue
new file mode 100644
index 0000000..2df088a
--- /dev/null
+++ b/emergency-repair-web/src/views/system/ErrorLogManage.vue
@@ -0,0 +1,151 @@
+<template>
+  <div class="page">
+    <div class="page-title">
+      <div><h2>閿欒鏃ュ織</h2><p>璁板綍绋嬪簭杩愯涓殑鏈鐞嗗紓甯革紝渚夸簬瀹氫綅鍜屼慨澶嶇己闄�</p></div>
+    </div>
+    <div class="table-panel">
+      <div class="toolbar">
+        <el-input v-model="filters.keyword" clearable placeholder="璐︽埛鍚� / 濮撳悕 / 璇锋眰鍦板潃 / 寮傚父淇℃伅" style="width:280px" @keyup.enter.native="search"></el-input>
+        <el-select v-model="filters.module" clearable placeholder="妯″潡" style="width:150px">
+          <el-option v-for="item in modules" :key="item" :label="item" :value="item"></el-option>
+        </el-select>
+        <el-input v-model="filters.exceptionType" clearable placeholder="寮傚父绫诲瀷" style="width:220px" @keyup.enter.native="search"></el-input>
+        <el-date-picker
+          v-model="filters.dates"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="鑷�"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          style="width:260px"
+        ></el-date-picker>
+        <el-button type="primary" icon="el-icon-search" v-permission="'system:error-logs:query'" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh-left" @click="reset">閲嶇疆</el-button>
+      </div>
+      <el-table v-loading="loading" :data="rows" border stripe height="calc(100vh - 250px)">
+        <el-table-column prop="errorTime" label="鍙戠敓鏃堕棿" width="180"></el-table-column>
+        <el-table-column prop="userName" label="璐︽埛鍚�" width="120"></el-table-column>
+        <el-table-column prop="nickName" label="濮撳悕" width="110"></el-table-column>
+        <el-table-column prop="module" label="妯″潡" width="120"></el-table-column>
+        <el-table-column prop="requestMethod" label="鏂瑰紡" width="75" align="center"></el-table-column>
+        <el-table-column prop="requestUri" label="璇锋眰鍦板潃" min-width="230" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="exceptionType" label="寮傚父绫诲瀷" min-width="220" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="message" label="寮傚父淇℃伅" min-width="280" show-overflow-tooltip></el-table-column>
+        <el-table-column label="鎿嶄綔" width="80" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" v-permission="'system:error-logs:detail'" @click="openDetail(row)">璇︽儏</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        class="pager"
+        background
+        layout="total, sizes, prev, pager, next"
+        :total="total"
+        :current-page="current"
+        :page-size="size"
+        :page-sizes="[20, 50, 100]"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      ></el-pagination>
+    </div>
+
+    <el-dialog title="閿欒璇︽儏" :visible.sync="detailVisible" width="900px" append-to-body>
+      <div v-loading="detailLoading">
+        <div class="detail-meta">
+          <span><b>鍙戠敓鏃堕棿锛�</b>{{ detail.errorTime }}</span>
+          <span><b>鎿嶄綔浜猴細</b>{{ detail.userName || '-' }}{{ detail.nickName ? ' / ' + detail.nickName : '' }}</span>
+          <span><b>妯″潡锛�</b>{{ detail.module || '-' }}</span>
+        </div>
+        <div class="detail-meta">
+          <span><b>璇锋眰锛�</b>{{ (detail.requestMethod || '') + ' ' + (detail.requestUri || '') }}</span>
+        </div>
+        <div class="detail-meta">
+          <span><b>寮傚父绫诲瀷锛�</b>{{ detail.exceptionType }}</span>
+        </div>
+        <div class="detail-meta">
+          <span><b>寮傚父淇℃伅锛�</b>{{ detail.message }}</span>
+        </div>
+        <pre class="stack-trace">{{ detail.stackTrace || '鏃犲爢鏍堜俊鎭�' }}</pre>
+      </div>
+      <template slot="footer"><el-button @click="detailVisible = false">鍏抽棴</el-button></template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import api from '@/api'
+export default {
+  name: 'ErrorLogManage',
+  data() {
+    return {
+      rows: [],
+      total: 0,
+      current: 1,
+      size: 20,
+      loading: false,
+      detailVisible: false,
+      detailLoading: false,
+      detail: {},
+      modules: ['鏁呴殰绠$悊', '瀹屽伐绠$悊', '瀹屽伐楠屾敹', '缁熻鎶ヨ〃', '鐢ㄦ埛绠$悊', '瑙掕壊绠$悊', '鑿滃崟绠$悊', '鎵夸慨鍗曚綅绠$悊', '绯荤粺绠$悊', '鍏朵粬'],
+      filters: { keyword: '', module: '', exceptionType: '', dates: [] }
+    }
+  },
+  created() { this.load() },
+  methods: {
+    load() {
+      this.loading = true
+      const dates = this.filters.dates || []
+      api.get('/system/error-logs', {
+        params: {
+          keyword: this.filters.keyword || undefined,
+          module: this.filters.module || undefined,
+          exceptionType: this.filters.exceptionType || undefined,
+          startDate: dates[0] || undefined,
+          endDate: dates[1] || undefined,
+          current: this.current,
+          size: this.size
+        }
+      }).then(res => {
+        const data = res.data || {}
+        this.rows = data.records || []
+        this.total = data.total || 0
+      }).finally(() => { this.loading = false })
+    },
+    search() { this.current = 1; this.load() },
+    reset() { this.filters = { keyword: '', module: '', exceptionType: '', dates: [] }; this.search() },
+    openDetail(row) {
+      this.detailLoading = true
+      this.detail = {}
+      this.detailVisible = true
+      api.get('/system/error-logs/' + row.id).then(res => {
+        this.detail = res.data || {}
+      }).finally(() => { this.detailLoading = false })
+    },
+    handleCurrentChange(page) { this.current = page; this.load() },
+    handleSizeChange(size) { this.size = size; this.current = 1; this.load() }
+  }
+}
+</script>
+
+<style scoped>
+.toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
+.pager { margin-top: 12px; text-align: right; }
+.detail-meta { margin-bottom: 8px; color: #42566b; line-height: 22px; word-break: break-all; }
+.detail-meta b { color: #26394d; }
+.stack-trace {
+  max-height: 520px;
+  margin: 12px 0 0;
+  padding: 12px;
+  border: 1px solid #dce4ec;
+  border-radius: 4px;
+  color: #30445a;
+  background: #f7f9fb;
+  font-family: Consolas, "Courier New", monospace;
+  font-size: 12px;
+  line-height: 18px;
+  white-space: pre-wrap;
+  word-break: break-all;
+  overflow: auto;
+}
+</style>
diff --git a/emergency-repair-web/src/views/system/MenuManage.vue b/emergency-repair-web/src/views/system/MenuManage.vue
index 9d1f4af..095bfda 100644
--- a/emergency-repair-web/src/views/system/MenuManage.vue
+++ b/emergency-repair-web/src/views/system/MenuManage.vue
@@ -63,7 +63,7 @@
     </el-dialog>
 
     <el-dialog :title="'椤甸潰鎸夐挳閰嶇疆 - ' + (buttonMenu.name || '')" :visible.sync="buttonDialogVisible" width="620px" append-to-body>
-      <p class="dialog-tip">涓嬪垪鎸夐挳鏉ヨ嚜鍚庣鎸夐挳鐩綍锛屽嬀閫夊悗鑷姩鐢熸垚鎸夐挳绫诲瀷鑿滃崟锛屽彲鍦ㄨ彍鍗曟潈闄愮鐞嗕腑鎺堟潈缁欒鑹层��</p>
+      <p class="dialog-tip">涓嬪垪鎸夐挳鏉ヨ嚜鍚庣鎸夐挳鐩綍锛屽嬀閫夊悗鑷姩鐢熸垚鎸夐挳绫诲瀷鑿滃崟锛屽彲鍦ㄨ鑹茬鐞嗙殑鏉冮檺璁剧疆涓巿鏉冪粰瑙掕壊銆�</p>
       <el-checkbox-group v-model="buttonSelected" class="button-list">
         <el-checkbox v-for="item in buttonAvailable" :key="item.code" :label="item.code" class="button-item">
           <span class="button-name">{{ item.name }}</span>
diff --git a/emergency-repair-web/src/views/system/MenuPermission.vue b/emergency-repair-web/src/views/system/MenuPermission.vue
deleted file mode 100644
index e69de15..0000000
--- a/emergency-repair-web/src/views/system/MenuPermission.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<template>
-  <div class="page">
-    <div class="page-title">
-      <div><h2>鑿滃崟鏉冮檺绠$悊</h2><p>鎸夎鑹茶缃彍鍗曞拰鎸夐挳鏉冮檺锛屼繚瀛樺悗鍒囨崲瑙掕壊浼氳嚜鍔ㄥ弽鏄俱��</p></div>
-    </div>
-    <div class="toolbar">
-      <span>瑙掕壊</span>
-      <el-select v-model="roleId" style="width:260px" @change="loadPermissions"><el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select>
-      <div class="spacer"></div>
-      <el-tag type="success">宸查�夋潈闄愶細{{ selectedRoleIds.length }} 涓�</el-tag>
-      <el-button type="primary" :loading="saving" @click="save">淇濆瓨瑙掕壊鏉冮檺</el-button>
-    </div>
-    <section class="permission-panel">
-      <div class="panel-head"><h3>瑙掕壊鑿滃崟鍙婃寜閽潈闄�</h3><el-checkbox v-model="roleCheckAll" @change="checkAll">鍏ㄩ��</el-checkbox></div>
-      <el-tree ref="roleTree" :data="menus" node-key="id" show-checkbox default-expand-all :render-content="renderContent" :props="{ label: 'name', children: 'children' }" @check="syncCheckAllState"></el-tree>
-    </section>
-  </div>
-</template>
-<script>
-import api from '@/api'
-export default {
-  name: 'MenuPermission',
-  data() {
-    return {
-      menus: [],
-      flatMenus: [],
-      roles: [],
-      roleId: null,
-      saving: false,
-      roleCheckAll: false,
-      selectedRoleIds: []
-    }
-  },
-  computed: {
-    allMenuIds() { return this.flatMenus.map(item => String(item.id)) }
-  },
-  created() { this.loadBase() },
-  methods: {
-    loadBase() {
-      Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list'), api.get('/system/roles')]).then(([tree, flat, roles]) => {
-        this.menus = tree.data || []
-        this.flatMenus = flat.data || []
-        this.roles = roles.data || []
-        if (!this.roleId && this.roles.length) this.roleId = this.roles[0].id
-        this.$nextTick(this.loadPermissions)
-      })
-    },
-    loadPermissions() {
-      if (!this.roleId) {
-        this.selectedRoleIds = []
-        this.roleCheckAll = false
-        if (this.$refs.roleTree) this.$refs.roleTree.setCheckedKeys([])
-        return
-      }
-      api.get('/system/menu-permissions', { params: { roleId: this.roleId } }).then(res => {
-        const data = res.data || {}
-        this.selectedRoleIds = (data.roleMenuIds || []).map(String)
-        this.$nextTick(() => {
-          this.$refs.roleTree.setCheckedKeys(this.displayCheckedKeys(this.selectedRoleIds))
-          this.syncCheckAllState()
-        })
-      })
-    },
-    renderContent(h, { data }) {
-      const label = data.menuType === 2 ? data.name + ' [鎸夐挳 ' + (data.permissions || '') + ']' : data.name
-      return h('span', { class: { 'perm-button-node': data.menuType === 2 } }, label)
-    },
-    displayCheckedKeys(ids) {
-      const selected = new Set((ids || []).map(String))
-      const children = {}
-      const menuIds = new Set(this.flatMenus.map(item => String(item.id)))
-      this.flatMenus.forEach(item => {
-        const id = String(item.id)
-        const pid = item.pid == null ? '0' : String(item.pid)
-        if (!children[pid]) children[pid] = []
-        children[pid].push(id)
-      })
-      const result = []
-      const collectFullySelected = id => {
-        if (!selected.has(id)) return false
-        const childIds = children[id] || []
-        if (!childIds.length) {
-          result.push(id)
-          return true
-        }
-        const allChildrenSelected = childIds.map(collectFullySelected).every(Boolean)
-        if (allChildrenSelected) result.push(id)
-        return allChildrenSelected
-      }
-      this.flatMenus.filter(item => item.pid == null || String(item.pid) === '0' || !menuIds.has(String(item.pid))).forEach(item => collectFullySelected(String(item.id)))
-      return result
-    },
-    checkedMenuIds(ref) { return [...new Set([...(ref.getCheckedKeys(false) || []), ...(ref.getHalfCheckedKeys() || [])].map(String))] },
-    syncCheckAllState() {
-      const ref = this.$refs.roleTree
-      if (!ref) return
-      this.roleCheckAll = this.checkedMenuIds(ref).length === this.allMenuIds.length
-    },
-    checkAll() {
-      this.$refs.roleTree.setCheckedKeys(this.roleCheckAll ? this.allMenuIds : [])
-      this.$nextTick(this.syncCheckAllState)
-    },
-    save() {
-      if (!this.roleId) {
-        this.$message.warning('璇峰厛閫夋嫨瑙掕壊')
-        return
-      }
-      const roleMenuIds = this.checkedMenuIds(this.$refs.roleTree)
-      this.saving = true
-      api.post('/system/menu-permissions', { roleId: this.roleId, roleMenuIds }).then(() => {
-        this.selectedRoleIds = roleMenuIds
-        this.$message.success('瑙掕壊鑿滃崟鏉冮檺宸蹭繚瀛�')
-      }).finally(() => { this.saving = false })
-    }
-  }
-}
-</script>
-<style scoped>
-::v-deep .perm-button-node { color: #b8791f; }
-
-.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;
-}
-.panel-head h3 {
-  margin: 0;
-  color: #31465d;
-  font-size: 15px;
-}
-</style>
diff --git a/emergency-repair-web/src/views/system/RoleManage.vue b/emergency-repair-web/src/views/system/RoleManage.vue
index 16dd95f..a711de8 100644
--- a/emergency-repair-web/src/views/system/RoleManage.vue
+++ b/emergency-repair-web/src/views/system/RoleManage.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="page">
     <div class="page-title">
-      <div><h2>瑙掕壊绠$悊</h2><p>缁存姢瑙掕壊淇℃伅锛屽苟鍙寜瑙掕壊鎵归噺璁剧疆鎵�灞炵敤鎴�</p></div>
+      <div><h2>瑙掕壊绠$悊</h2><p>缁存姢瑙掕壊淇℃伅锛屽苟鍙寜瑙掕壊璁剧疆鎵�灞炵敤鎴峰拰鑿滃崟鎸夐挳鏉冮檺</p></div>
       <el-button type="primary" icon="el-icon-plus" v-permission="'system:roles:add'" @click="openCreate">鏂板瑙掕壊</el-button>
     </div>
     <div class="table-panel">
@@ -11,9 +11,10 @@
         <el-table-column prop="code" label="瑙掕壊缂栫爜" width="170"></el-table-column>
         <el-table-column prop="roleType" label="瑙掕壊绫诲瀷" width="170"></el-table-column>
         <el-table-column prop="remark" label="澶囨敞" min-width="220" show-overflow-tooltip></el-table-column>
-        <el-table-column label="鎿嶄綔" width="220" fixed="right">
+        <el-table-column label="鎿嶄綔" width="300" fixed="right">
           <template slot-scope="{ row }">
             <el-button type="text" v-permission="'system:roles:assign'" @click="openUserSetting(row)">鐢ㄦ埛璁剧疆</el-button>
+            <el-button type="text" v-permission="'system:roles:permission'" @click="openPermissionSetting(row)">鏉冮檺璁剧疆</el-button>
             <el-button type="text" v-permission="'system:roles:edit'" @click="openEdit(row)">缂栬緫</el-button>
             <el-button type="text" class="danger-link" v-permission="'system:roles:delete'" @click="remove(row)">鍒犻櫎</el-button>
           </template>
@@ -80,16 +81,21 @@
         <el-button type="primary" :loading="userSaving" v-permission="'system:roles:assign'" @click="saveRoleUsers">淇濆瓨</el-button>
       </template>
     </el-dialog>
+
+    <role-permission-dialog :visible.sync="permissionDialogVisible" :role="currentRole"></role-permission-dialog>
   </div>
 </template>
 <script>
 import api from '@/api'
+import RolePermissionDialog from './RolePermissionDialog.vue'
 export default {
   name: 'RoleManage',
+  components: { RolePermissionDialog },
   data() {
     return {
       rows: [],
       dialogVisible: false,
+      permissionDialogVisible: false,
       saving: false,
       userDialogVisible: false,
       userLoading: false,
@@ -164,6 +170,10 @@
         this.$nextTick(this.restoreUserSelection)
       }).finally(() => { this.userLoading = false })
     },
+    openPermissionSetting(row) {
+      this.currentRole = { ...row }
+      this.permissionDialogVisible = true
+    },
     restoreUserSelection() {
       const table = this.$refs.roleUserTable
       if (!table) return
diff --git a/emergency-repair-web/src/views/system/RolePermissionDialog.vue b/emergency-repair-web/src/views/system/RolePermissionDialog.vue
new file mode 100644
index 0000000..7271f40
--- /dev/null
+++ b/emergency-repair-web/src/views/system/RolePermissionDialog.vue
@@ -0,0 +1,194 @@
+<template>
+  <el-dialog
+    title="瑙掕壊鏉冮檺璁剧疆"
+    :visible.sync="dialogVisible"
+    width="860px"
+    append-to-body
+    :close-on-click-modal="false"
+  >
+    <div class="permission-head">
+      <div>
+        <span class="role-label">褰撳墠瑙掕壊</span>
+        <strong>{{ role.name }}</strong>
+        <span class="role-code">{{ role.code }}</span>
+      </div>
+      <span class="selected-total">宸查�夋潈闄� {{ selectedRoleIds.length }} 涓�</span>
+    </div>
+    <div class="permission-toolbar">
+      <el-checkbox v-model="checkAll" @change="toggleAll">鍏ㄩ��</el-checkbox>
+      <span class="permission-tip">鑿滃崟鍜屾寜閽潈闄愬潎鍙嬀閫夛紝淇濆瓨鍚庡垏鎹㈣鑹蹭細鑷姩鍙嶆樉銆�</span>
+    </div>
+    <div v-loading="loading" class="permission-tree">
+      <el-tree
+        ref="permissionTree"
+        :data="menus"
+        node-key="id"
+        show-checkbox
+        default-expand-all
+        :render-content="renderContent"
+        :props="{ label: 'name', children: 'children' }"
+        @check="syncCheckAllState"
+      ></el-tree>
+    </div>
+    <template slot="footer">
+      <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+      <el-button type="primary" :loading="saving" v-permission="'system:roles:permission'" @click="save">淇濆瓨</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import api from '@/api'
+export default {
+  name: 'RolePermissionDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    role: { type: Object, default: () => ({}) }
+  },
+  data() {
+    return {
+      menus: [],
+      flatMenus: [],
+      loading: false,
+      saving: false,
+      checkAll: false,
+      selectedRoleIds: []
+    }
+  },
+  computed: {
+    dialogVisible: {
+      get() { return this.visible },
+      set(value) { this.$emit('update:visible', value) }
+    },
+    allMenuIds() {
+      return this.flatMenus.map(item => String(item.id))
+    }
+  },
+  watch: {
+    visible(value) {
+      if (value) this.open()
+    },
+  },
+  methods: {
+    open() {
+      if (!this.role || !this.role.id) return
+      this.loading = true
+      this.checkAll = false
+      this.selectedRoleIds = []
+      Promise.all([
+        api.get('/system/menu/tree'),
+        api.get('/system/menu/list'),
+        api.get('/system/menu-permissions', { params: { roleId: this.role.id } })
+      ]).then(([tree, flat, permissions]) => {
+        this.menus = tree.data || []
+        this.flatMenus = flat.data || []
+        const data = permissions.data || {}
+        this.selectedRoleIds = (data.roleMenuIds || []).map(String)
+        this.$nextTick(() => {
+          if (!this.$refs.permissionTree) return
+          this.$refs.permissionTree.setCheckedKeys(this.displayCheckedKeys(this.selectedRoleIds))
+          this.syncCheckAllState()
+        })
+      }).finally(() => { this.loading = false })
+    },
+    renderContent(h, { data }) {
+      const label = data.menuType === 2 ? data.name + ' [鎸夐挳 ' + (data.permissions || '') + ']' : data.name
+      return h('span', { class: { 'perm-button-node': data.menuType === 2 } }, label)
+    },
+    displayCheckedKeys(ids) {
+      const selected = new Set((ids || []).map(String))
+      const children = {}
+      const menuIds = new Set(this.flatMenus.map(item => String(item.id)))
+      this.flatMenus.forEach(item => {
+        const id = String(item.id)
+        const pid = item.pid == null ? '0' : String(item.pid)
+        if (!children[pid]) children[pid] = []
+        children[pid].push(id)
+      })
+      const result = []
+      const collectFullySelected = id => {
+        if (!selected.has(id)) return false
+        const childIds = children[id] || []
+        if (!childIds.length) {
+          result.push(id)
+          return true
+        }
+        const allChildrenSelected = childIds.map(collectFullySelected).every(Boolean)
+        if (allChildrenSelected) result.push(id)
+        return allChildrenSelected
+      }
+      this.flatMenus
+        .filter(item => item.pid == null || String(item.pid) === '0' || !menuIds.has(String(item.pid)))
+        .forEach(item => collectFullySelected(String(item.id)))
+      return result
+    },
+    checkedMenuIds() {
+      const tree = this.$refs.permissionTree
+      if (!tree) return []
+      return [...new Set([...(tree.getCheckedKeys(false) || []), ...(tree.getHalfCheckedKeys() || [])].map(String))]
+    },
+    syncCheckAllState() {
+      this.checkAll = this.checkedMenuIds().length === this.allMenuIds.length
+    },
+    toggleAll() {
+      if (!this.$refs.permissionTree) return
+      this.$refs.permissionTree.setCheckedKeys(this.checkAll ? this.allMenuIds : [])
+      this.$nextTick(this.syncCheckAllState)
+    },
+    save() {
+      const roleMenuIds = this.checkedMenuIds()
+      this.saving = true
+      api.post('/system/menu-permissions', { roleId: this.role.id, roleMenuIds }).then(() => {
+        this.selectedRoleIds = roleMenuIds
+        this.$message.success('瑙掕壊鏉冮檺宸蹭繚瀛�')
+        this.$emit('saved')
+        this.dialogVisible = false
+      }).finally(() => { this.saving = false })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.permission-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  color: #31465d;
+}
+.role-label {
+  margin-right: 10px;
+  color: #7b8b9c;
+  font-size: 13px;
+}
+.role-code {
+  margin-left: 8px;
+  color: #93a2b1;
+  font-size: 12px;
+}
+.selected-total {
+  color: #2f658f;
+  font-size: 13px;
+}
+.permission-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 10px;
+}
+.permission-tip {
+  color: #7b8a9a;
+  font-size: 12px;
+}
+.permission-tree {
+  height: 460px;
+  padding: 8px 10px;
+  border: 1px solid #dce4ec;
+  border-radius: 4px;
+  overflow: auto;
+}
+::v-deep .perm-button-node {
+  color: #b8791f;
+}
+</style>
diff --git "a/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md" "b/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
index 443f293..b1b34d0 100644
--- "a/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
+++ "b/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
@@ -534,3 +534,46 @@
 
 - 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
 - `.codex-run`銆丱ffice 閿佹枃浠跺強 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt` 浠嶄负鏈湴涓存椂鏂囦欢锛屼笉搴旀彁浜ゃ��
+
+## 鍗佷笁銆�2026-10-06 鑱旇皟鏀跺熬锛堥渶姹� 21-22锛�
+
+| 搴忓彿 | 闇�姹� | 鐘舵�� | 瀹炵幇涓庨獙璇� |
+| --- | --- | --- | --- |
+| 21 | 鑿滃崟鏉冮檺绠$悊鏀逛负瑙掕壊绠$悊鐨勨�滄潈闄愯缃�濆叆鍙� | 宸插畬鎴� | 鍒犻櫎鐙珛鑿滃崟銆佽矾鐢卞拰椤甸潰锛涜鑹插垪琛ㄦ柊澧炩�滄潈闄愯缃�濓紝寮圭獥淇濈暀鑿滃崟鍙婃寜閽妭鐐瑰苟姝e父鍙嶆樉 |
+| 22 | 绯荤粺绠$悊鏂板浜岀骇鑿滃崟缁勶紝鏂板閿欒鏃ュ織 | 宸插畬鎴� | 鏂板鈥滅郴缁熼厤缃� / 鏉冮檺閰嶇疆 / 鏃ュ織绠$悊鈥濈洰褰曞苟杩佺Щ鑿滃崟锛涢敊璇棩蹇楄褰曢潪涓氬姟寮傚父锛屾敮鎸佺瓫閫夈�佸垎椤靛拰鍫嗘爤璇︽儏 |
+
+### 13.1 涓昏鍙樻洿鏂囦欢
+
+鍚庣锛�
+
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/entity/SysErrorLog.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/mapper/SysErrorLogMapper.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/log/ErrorLogRecorder.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/common/GlobalExceptionHandler.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DatabaseBootstrap.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/controller/SystemController.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java`
+
+鍓嶇锛�
+
+- `emergency-repair-web/src/views/system/RoleManage.vue`
+- `emergency-repair-web/src/views/system/RolePermissionDialog.vue`
+- `emergency-repair-web/src/views/system/ErrorLogManage.vue`
+- `emergency-repair-web/src/views/system/MenuManage.vue`
+- `emergency-repair-web/src/router/index.js`
+- 鍒犻櫎 `emergency-repair-web/src/views/system/MenuPermission.vue`
+
+### 13.2 楠岃瘉缁撴灉
+
+- 鍚庣 `mvn -B -DskipTests clean package` 閫氳繃锛涘墠绔� `npm run build` 閫氳繃锛屼粎鏈夋棦鏈夊寘浣撶Н鍛婅銆�
+- `node .codex-run/verify-req21-22.js` 閫氳繃锛氫簩绾х洰褰曞拰閿欒鏃ュ織鑿滃崟鏄剧ず姝e父锛屾棫鈥滆彍鍗曟潈闄愮鐞嗏�濅笉瀛樺湪锛涜鑹叉潈闄愬脊绐楁寜閽妭鐐规暟 78锛屾潈闄愬弽鏄炬甯搞��
+- 閿欒鏃ュ織鍒楄〃銆佸紓甯哥被鍨嬬瓫閫夊拰鍫嗘爤璇︽儏椤甸潰楠岃瘉閫氳繃锛涗汉涓鸿Е鍙戦潪娉曟帴鍙e弬鏁板悗鎴愬姛钀藉簱锛屼复鏃堕獙璇佽褰曞凡娓呯悊銆�
+- 鎴浘锛歚.codex-run/role-permission-dialog-verified.png`銆乣.codex-run/error-log-verified.png`銆�
+
+### 13.3 鎺ユ墜娉ㄦ剰
+
+- 閿欒鏃ュ織鐩墠瑕嗙洊杩涘叆 Spring MVC 鍏ㄥ眬寮傚父澶勭悊鐨勫紓甯革紱鍚姩闃舵寮傚父銆佸紓姝ョ嚎绋嬪紓甯歌嫢缁曡繃璇ュ鐞嗗櫒锛屾殏涓嶄細鑷姩璁板綍銆�
+- 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
+- `.codex-run`銆丱ffice 閿佹枃浠跺強 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt` 浠嶄负鏈湴涓存椂鏂囦欢锛屼笉搴旀彁浜ゃ��

--
Gitblit v1.9.1