From 7ff24056345509ad1db6826a0e6e08bde740c8ac Mon Sep 17 00:00:00 2001 From: wangkai <10903508@qq.com> Date: Sat, 24 Oct 2026 17:26:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(menu):=20=E4=BF=AE=E5=A4=8D=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E7=BC=96=E8=BE=91=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E4=B8=8E=E6=96=B0=E5=A2=9E=20500=20-=20?= =?UTF-8?q?=E6=A0=B9=E5=9B=A0=E4=B8=80=EF=BC=9AMenuService=20create/update?= =?UTF-8?q?=20=E4=BB=A5=20snake=5Fcase=20=E5=88=97=E5=90=8D=E4=BD=9C=20Seq?= =?UTF-8?q?uelize=20=E5=86=99=E5=85=A5=E9=94=AE=E8=80=8C=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E4=B8=BA=20camelCase=EF=BC=8C=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=86=99=E5=85=A5=E8=A2=AB=E9=9D=99=E9=BB=98=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=EF=BC=88=E6=9B=B4=E6=96=B0=E5=81=87=E6=88=90=E5=8A=9F?= =?UTF-8?q?=EF=BC=89=E6=88=96=20NOT=20NULL=20=E6=8A=A5=E9=94=99=EF=BC=88?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20500=EF=BC=89=EF=BC=8C=E6=94=B9=E4=B8=BA=20?= =?UTF-8?q?camelCase=20=E5=B1=9E=E6=80=A7=E5=90=8D=EF=BC=9B=E6=A0=B9?= =?UTF-8?q?=E5=9B=A0=E4=BA=8C=EF=BC=9Asys=5Fmenu=20=E5=88=97=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC=E6=8C=87=E5=90=91=E5=8E=86=E5=8F=B2=E9=81=97?= =?UTF-8?q?=E7=95=99=E9=87=8D=E5=A4=8D=E5=BA=8F=E5=88=97=20sys=5Fmenu=5Fid?= =?UTF-8?q?=5Fseq1=20=E4=B8=94=E6=9C=AA=E6=8E=A8=E8=BF=9B=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95=20id=20=E5=86=B2=E7=AA=81?= =?UTF-8?q?=EF=BC=8C=E7=94=9F=E4=BA=A7=E5=BA=93=E5=B7=B2=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E6=80=A7=20setval=20=E5=90=8C=E6=AD=A5=EF=BC=88max(id)=3D64?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=A0=B8=E5=AF=B9=20sys=5Frole/sys=5Fuser=20?= =?UTF-8?q?=E5=BA=8F=E5=88=97=EF=BC=89=EF=BC=8Cdb=5Fseed.sql=20=E5=85=A8?= =?UTF-8?q?=E9=83=A8=20setval=20=E6=94=B9=E4=B8=BA=20pg=5Fget=5Fserial=5Fs?= =?UTF-8?q?equence()=20=E5=BD=A2=E5=BC=8F=E5=85=BC=E5=AE=B9=E4=B8=8D?= =?UTF-8?q?=E5=90=8C=E7=8E=AF=E5=A2=83=E5=BA=8F=E5=88=97=E5=90=8D=EF=BC=9B?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8E=E6=B5=8F=E8=A7=88=E5=99=A8=E7=AB=AF?= =?UTF-8?q?=E5=88=B0=E7=AB=AF=E9=AA=8C=E8=AF=81=E9=80=9A=E8=BF=87=EF=BC=88?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E2=86=92=E6=94=B9=E5=90=8D=E2=86=92=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E2=86=92=E6=B8=85=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .trae/specs/fix-menu-edit-save/checklist.md | 8 ++++++ .trae/specs/fix-menu-edit-save/spec.md | 31 +++++++++++++++++++++ .trae/specs/fix-menu-edit-save/tasks.md | 16 +++++++++++ database/db_seed.sql | 14 +++++----- server/src/service/menu.service.ts | 20 ++++++------- 5 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 .trae/specs/fix-menu-edit-save/checklist.md create mode 100644 .trae/specs/fix-menu-edit-save/spec.md create mode 100644 .trae/specs/fix-menu-edit-save/tasks.md diff --git a/.trae/specs/fix-menu-edit-save/checklist.md b/.trae/specs/fix-menu-edit-save/checklist.md new file mode 100644 index 0000000..5a50fec --- /dev/null +++ b/.trae/specs/fix-menu-edit-save/checklist.md @@ -0,0 +1,8 @@ +# Checklist + +- [x] MenuService.create() 以 camelCase 属性名写入(parentId/menuName/menuPath/component/icon/sortOrder/menuType/permission/visible),父菜单 parentId=null 语义保留 +- [x] MenuService.update() 以 camelCase 属性名写入,部分字段更新不会清空未提交字段 +- [x] sys_menu 自增序列失步已修复(生产库 setval 到 max(id)=64;sys_role/sys_user 序列一并核对),db_seed.sql 的 setval 全部改为 pg_get_serial_sequence() 形式以兼容不同环境序列名 +- [x] 后端 tsc --noEmit 通过 +- [x] PUT /api/menus/:id 修改名称后重新 GET 菜单树已持久化;POST /api/menus 返回 200 且新菜单可见(测试数据已清理) +- [x] 浏览器菜单管理页:编辑名称保存成功、树实时刷新、刷新页面后名称保持;新增菜单可用 diff --git a/.trae/specs/fix-menu-edit-save/spec.md b/.trae/specs/fix-menu-edit-save/spec.md new file mode 100644 index 0000000..cb3a242 --- /dev/null +++ b/.trae/specs/fix-menu-edit-save/spec.md @@ -0,0 +1,31 @@ +# 修复菜单管理「编辑无法保存」Spec + +## Why +菜单管理页面点击「编辑」修改菜单名称后点击保存,接口返回「更新菜单成功」但数据未持久化(名称未变);新增菜单则直接返回 500。根因:`MenuService.create/update` 以 snake_case 数据库列名(`menu_name` 等)作为 Sequelize 的取值键,而实体属性是 camelCase(`menuName` 等),Sequelize 无法匹配从而静默忽略(update)或触发 NOT NULL 报错(create)。 + +## What Changes +- 修正 `server/src/service/menu.service.ts` 中 `create()` 与 `update()`:将写入 Sequelize 的键改为实体 camelCase 属性名(`parentId/menuName/menuPath/component/icon/sortOrder/menuType/permission/visible`),字段映射由实体 `field` 配置负责。 +- 前端与接口协议不变(前端已发送 camelCase body,`menu.controller.ts`、`menu.dto.ts`、`client/src/api/menu.ts` 均无需改动)。 +- **BREAKING**: 无(仅为内部映射修正,接口行为从「假成功/500」变为「真实持久化」)。 + +## Impact +- Affected specs: 系统管理域(sys_menu RBAC) +- Affected code: `server/src/service/menu.service.ts`(仅 create/update 两处取值键);连带修复后「新增菜单」「编辑菜单」均恢复可用 + +## ADDED Requirements +(无新增能力) + +## MODIFIED Requirements +### Requirement: 菜单新增与编辑持久化 +菜单管理的「新增」与「编辑」操作 SHALL 将表单值真实写入 `sys_menu` 表。 + +#### Scenario: 编辑菜单名称后保存 +- **WHEN** 用户在菜单管理中点击某菜单「编辑」,修改菜单名称并点击确定 +- **THEN** 接口返回成功,重新拉取菜单树显示新名称,且刷新页面后名称保持不变 + +#### Scenario: 新增菜单 +- **WHEN** 用户在菜单管理中点击「新增」,填写必填项并确定 +- **THEN** 接口返回 200 并持久化,菜单树中出现新菜单(而非 500) + +## REMOVED Requirements +(无) diff --git a/.trae/specs/fix-menu-edit-save/tasks.md b/.trae/specs/fix-menu-edit-save/tasks.md new file mode 100644 index 0000000..b12a341 --- /dev/null +++ b/.trae/specs/fix-menu-edit-save/tasks.md @@ -0,0 +1,16 @@ +# Tasks +- [x] Task 1: 修复 MenuService 写入键映射 + - [x] 1.1 阅读 menu.service.ts,确认 create()/update() 中 snake_case 键的位置 + - [x] 1.2 将 create() 与 update() 写入 Sequelize 的键改为实体 camelCase 属性名,并确保父子菜单 parentId=null 与 visible 处理逻辑不变 + - [x] 1.3 后端 `npx tsc --noEmit` 通过 +- [x] Task 2: 修复 sys_menu 自增序列失步(新增菜单 500 的根因) + - [x] 2.1 定位根因:列默认值指向 sys_menu_id_seq1 且停留在 2(显式 id 插入不推进序列),与已存在 id 冲突 + - [x] 2.2 生产库一次性同步序列(setval 到 max(id)=64),并顺带核对 sys_role/sys_user 序列 + - [x] 2.3 db_seed.sql 全部 setval 改为 pg_get_serial_sequence() 形式,兼容不同环境序列名 +- [x] Task 3: 验证 + - [x] 3.1 接口自测:PUT /api/menus/:id 修改菜单名称后重新 GET 树,名称已持久化 + - [x] 3.2 接口自测:POST /api/menus 新增菜单返回 200 且树中出现新菜单(自测后清理测试数据) + - [x] 3.3 浏览器验证:菜单管理页编辑名称保存后树实时刷新、刷新页面后保持 + +# Task Dependencies +- [Task 2][Task 3] depend on [Task 1] diff --git a/database/db_seed.sql b/database/db_seed.sql index ad11aa7..b284ba1 100644 --- a/database/db_seed.sql +++ b/database/db_seed.sql @@ -43,7 +43,7 @@ ON CONFLICT (id) DO UPDATE SET DELETE FROM sys_role WHERE role_code IN ('WORKER', 'CUSTOMER'); -- 重置 sys_role 自增序列 -SELECT setval('sys_role_id_seq', 2, true); +SELECT setval(pg_get_serial_sequence('sys_role', 'id'), GREATEST((SELECT MAX(id) FROM sys_role), 2), true); -- ------------------------------------------------------------ -- 2. 初始菜单(顶级菜单,显式指定 id:1-6,系统管理/运营派单为目录类型 menu_type=1,其余为 menu_type=2) @@ -185,7 +185,7 @@ ON CONFLICT (id) DO UPDATE SET visible = EXCLUDED.visible; -- 重置 sys_menu 自增序列 -SELECT setval('sys_menu_id_seq', 50, true); +SELECT setval(pg_get_serial_sequence('sys_menu', 'id'), GREATEST((SELECT MAX(id) FROM sys_menu), 50), true); -- ------------------------------------------------------------ -- 3. 初始字典(sys_dict 无业务唯一键,清空重建以支持幂等执行) @@ -252,7 +252,7 @@ ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status; -- 重置 sys_user 自增序列 -SELECT setval('sys_user_id_seq', 1, true); +SELECT setval(pg_get_serial_sequence('sys_user', 'id'), GREATEST((SELECT MAX(id) FROM sys_user), 1), true); -- ------------------------------------------------------------ -- 5. 用户-角色绑定:admin → ADMIN @@ -328,7 +328,7 @@ INSERT INTO sys_role_menu (role_id, menu_id) VALUES ON CONFLICT (role_id, menu_id) DO NOTHING; -- 重置 sys_menu 自增序列(覆盖到 61) -SELECT setval('sys_menu_id_seq', 61, true); +SELECT setval(pg_get_serial_sequence('sys_menu', 'id'), GREATEST((SELECT MAX(id) FROM sys_menu), 61), true); -- ------------------------------------------------------------ -- 8a. 设备监控下的智电改造线路台账菜单(id=61,挂设备监控 parent_id=4) @@ -386,7 +386,7 @@ INSERT INTO sys_role_menu (role_id, menu_id) VALUES ON CONFLICT (role_id, menu_id) DO NOTHING; -- 重置 sys_menu 自增序列 -SELECT setval('sys_menu_id_seq', 57, true); +SELECT setval(pg_get_serial_sequence('sys_menu', 'id'), GREATEST((SELECT MAX(id) FROM sys_menu), 57), true); -- ------------------------------------------------------------ -- 10. 通知规则扫描任务种子(sys_job,按 job_key 幂等) @@ -521,7 +521,7 @@ INSERT INTO sys_role_menu (role_id, menu_id) VALUES ON CONFLICT (role_id, menu_id) DO NOTHING; -- 重置 sys_menu 自增序列(覆盖到 62) -SELECT setval('sys_menu_id_seq', GREATEST((SELECT MAX(id) FROM sys_menu), 62), true); +SELECT setval(pg_get_serial_sequence('sys_menu', 'id'), GREATEST((SELECT MAX(id) FROM sys_menu), 62), true); -- 智电网关同步任务种子(sys_job,按 job_key 幂等;默认每 10 分钟) INSERT INTO sys_job (job_key, job_name, cron_expression, params, retry_times, retry_interval, status, remark) VALUES @@ -574,7 +574,7 @@ INSERT INTO sys_role_menu (role_id, menu_id) VALUES ON CONFLICT (role_id, menu_id) DO NOTHING; -- 重置 sys_menu 自增序列(覆盖到 64) -SELECT setval('sys_menu_id_seq', GREATEST((SELECT MAX(id) FROM sys_menu), 64), true); +SELECT setval(pg_get_serial_sequence('sys_menu', 'id'), GREATEST((SELECT MAX(id) FROM sys_menu), 64), true); COMMIT; diff --git a/server/src/service/menu.service.ts b/server/src/service/menu.service.ts index 343c562..878b103 100644 --- a/server/src/service/menu.service.ts +++ b/server/src/service/menu.service.ts @@ -43,13 +43,13 @@ export class MenuService { } const menu = await this.menuModel.create({ - parent_id: parentId, - menu_name: menuName, - menu_path: menuPath, + parentId, + menuName, + menuPath, component, icon, - sort_order: sortOrder, - menu_type: menuType, + sortOrder, + menuType, permission, visible, }); @@ -73,13 +73,13 @@ export class MenuService { } const updateData: any = {}; - if (data.parentId !== undefined) updateData.parent_id = data.parentId; - if (data.menuName !== undefined) updateData.menu_name = data.menuName; - if (data.menuPath !== undefined) updateData.menu_path = data.menuPath; + if (data.parentId !== undefined) updateData.parentId = data.parentId; + if (data.menuName !== undefined) updateData.menuName = data.menuName; + if (data.menuPath !== undefined) updateData.menuPath = data.menuPath; if (data.component !== undefined) updateData.component = data.component; if (data.icon !== undefined) updateData.icon = data.icon; - if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder; - if (data.menuType !== undefined) updateData.menu_type = data.menuType; + if (data.sortOrder !== undefined) updateData.sortOrder = data.sortOrder; + if (data.menuType !== undefined) updateData.menuType = data.menuType; if (data.permission !== undefined) updateData.permission = data.permission; if (data.visible !== undefined) updateData.visible = data.visible;