diff --git a/API.md b/API.md index 82dcab3..d3dd9fc 100644 --- a/API.md +++ b/API.md @@ -149,6 +149,8 @@ QQBot 插件 worker 队列依赖 Redis。K8s 生产清单提供内部 Redis Serv | `PUT` | `/system/user/:id` | 更新用户 | | `DELETE` | `/system/user/:id` | 删除用户 | +系统菜单实体包含 `sort` 字段;菜单树输出按 `meta.order` 优先,其次按 `sort` 升序排列。Admin 菜单管理页面维护 `sort`,不要把普通菜单排序写进隐藏的 route meta。 + ### Dict | 方法 | 路径 | 说明 | diff --git a/sql/fix-admin-menu-meta.sql b/sql/fix-admin-menu-meta.sql index db12944..533c263 100644 --- a/sql/fix-admin-menu-meta.sql +++ b/sql/fix-admin-menu-meta.sql @@ -7,7 +7,6 @@ UPDATE `admin_menu` SET `meta` = CASE `name` WHEN 'Dashboard' THEN '{"order":-1,"title":"page.dashboard.title"}' WHEN 'Analytics' THEN '{"affixTab":true,"title":"page.dashboard.analytics"}' - WHEN 'Workspace' THEN '{"icon":"carbon:workspace","title":"page.dashboard.workspace"}' WHEN 'System' THEN '{"badge":"new","badgeType":"normal","badgeVariants":"primary","icon":"carbon:settings","order":9997,"title":"system.title"}' WHEN 'SystemRole' THEN '{"icon":"carbon:user-role","title":"system.role.title"}' WHEN 'SystemRoleCreate' THEN '{"title":"common.create"}' @@ -34,7 +33,6 @@ END WHERE `name` IN ( 'Dashboard', 'Analytics', - 'Workspace', 'System', 'SystemRole', 'SystemRoleCreate', diff --git a/sql/refactor-v3/01-seed-core.sql b/sql/refactor-v3/01-seed-core.sql index fa5fd62..f731a84 100644 --- a/sql/refactor-v3/01-seed-core.sql +++ b/sql/refactor-v3/01-seed-core.sql @@ -47,16 +47,16 @@ INSERT INTO admin_user ( status ) VALUES ( 2041700000000000002, - 'admin', + 'kwitsukasa', '123456', - 'Admin', + 'KwiTsukasa', '', 2041700000000200001, - '/workspace', + '/analytics', 'Asia/Shanghai', 1 ) ON DUPLICATE KEY UPDATE - password = VALUES(password), + username = VALUES(username), real_name = VALUES(real_name), avatar = VALUES(avatar), dept_id = VALUES(dept_id), @@ -84,7 +84,7 @@ INSERT INTO admin_menu ( 'Dashboard', '/dashboard', NULL, - '/workspace', + '/analytics', NULL, 'catalog', '{"order":-1,"title":"page.dashboard.title"}', @@ -92,15 +92,15 @@ INSERT INTO admin_menu ( 0 ), ( - 2041700000000100102, + 2041700000000100101, 2041700000000100001, - 'Workspace', - '/workspace', - '/dashboard/workspace/index', + 'Analytics', + '/analytics', + '/dashboard/analytics/index', NULL, NULL, 'menu', - '{"icon":"carbon:workspace","title":"page.dashboard.workspace"}', + '{"affixTab":true,"title":"page.dashboard.analytics"}', 1, 0 ), diff --git a/sql/vben-admin-init.sql b/sql/vben-admin-init.sql index 4a9e559..07b1693 100644 --- a/sql/vben-admin-init.sql +++ b/sql/vben-admin-init.sql @@ -210,7 +210,6 @@ INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, VALUES (2041700000000100001, 0, 'Dashboard', '/dashboard', NULL, '/analytics', NULL, 'catalog', '{"order":-1,"title":"page.dashboard.title"}', 1, 0), (2041700000000100101, 2041700000000100001, 'Analytics', '/analytics', '/dashboard/analytics/index', NULL, NULL, 'menu', '{"affixTab":true,"title":"page.dashboard.analytics"}', 1, 0), - (2041700000000100102, 2041700000000100001, 'Workspace', '/workspace', '/dashboard/workspace/index', NULL, NULL, 'menu', '{"icon":"carbon:workspace","title":"page.dashboard.workspace"}', 1, 0), (2041700000000100002, 0, 'System', '/system', NULL, NULL, NULL, 'catalog', '{"badge":"new","badgeType":"normal","badgeVariants":"primary","icon":"carbon:settings","order":9997,"title":"system.title"}', 1, 9997), (2041700000000100199, 2041700000000100002, 'SystemUser', '/system/user', '/system/user/list', NULL, 'System:User:List', 'menu', '{"icon":"carbon:user-profile","title":"system.user.title"}', 1, 0), (2041700000000120191, 2041700000000100199, 'SystemUserCreate', NULL, NULL, NULL, 'System:User:Create', 'button', '{"title":"common.create"}', 1, 0), @@ -277,7 +276,7 @@ ON DUPLICATE KEY UPDATE INSERT INTO `admin_role` (`id`, `role_code`, `name`, `remark`, `status`) VALUES (2041700000000010001, 'super', '超级管理员', '拥有所有后台权限', 1), - (2041700000000010002, 'admin', '管理员', '拥有系统管理与工作台权限', 1), + (2041700000000010002, 'admin', '管理员', '拥有系统管理与环境总览权限', 1), (2041700000000010003, 'user', '普通用户', '仅拥有基础查看权限', 1) ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), @@ -287,11 +286,9 @@ ON DUPLICATE KEY UPDATE INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `avatar`, `dept_id`, `home_path`, `timezone`, `status`) VALUES - (2041700000000000001, 'vben', '123456', 'Vben', '', 2041700000000200002, '/workspace', 'Asia/Shanghai', 1), - (2041700000000000002, 'admin', '123456', 'Admin', '', 2041700000000200001, '/workspace', 'Asia/Shanghai', 1), - (2041700000000000003, 'jack', '123456', 'Jack', '', 2041700000000200003, '/analytics', 'Asia/Shanghai', 1) + (2041700000000000002, 'kwitsukasa', '123456', 'KwiTsukasa', '', 2041700000000200001, '/analytics', 'Asia/Shanghai', 1) ON DUPLICATE KEY UPDATE - `password` = VALUES(`password`), + `username` = VALUES(`username`), `real_name` = VALUES(`real_name`), `dept_id` = VALUES(`dept_id`), `home_path` = VALUES(`home_path`), @@ -331,16 +328,12 @@ ON DUPLICATE KEY UPDATE DELETE FROM `admin_user_role` WHERE `user_id` IN ( - 2041700000000000001, - 2041700000000000002, - 2041700000000000003 + 2041700000000000002 ); INSERT INTO `admin_user_role` (`user_id`, `role_id`) VALUES - (2041700000000000001, 2041700000000010001), - (2041700000000000002, 2041700000000010002), - (2041700000000000003, 2041700000000010003); + (2041700000000000002, 2041700000000010002); DELETE FROM `admin_role_menu` WHERE `role_id` IN ( diff --git a/src/modules/admin/identity/menu/admin-menu.service.ts b/src/modules/admin/identity/menu/admin-menu.service.ts index c0c6826..07fdf8b 100644 --- a/src/modules/admin/identity/menu/admin-menu.service.ts +++ b/src/modules/admin/identity/menu/admin-menu.service.ts @@ -210,6 +210,7 @@ export class AdminMenuService { pid: data.pid || '0', redirect: data.redirect || null, status: data.status ?? 1, + sort: Number(data.sort ?? 0), type: data.type || 'menu', }; if (includeEmptyMeta || Object.keys(meta).length > 0) { @@ -321,6 +322,7 @@ export class AdminMenuService { pid: menu.pid || '0', redirect: menu.redirect, status: menu.status, + sort: menu.sort, type: menu.type, } as any; diff --git a/src/modules/admin/identity/user/admin-user.service.ts b/src/modules/admin/identity/user/admin-user.service.ts index 3056f81..0d8f014 100644 --- a/src/modules/admin/identity/user/admin-user.service.ts +++ b/src/modules/admin/identity/user/admin-user.service.ts @@ -10,6 +10,9 @@ import type { AdminUserListQuery, } from '../../contract/admin.types'; +const BUILTIN_ADMIN_USER_ID = '2041700000000000002'; +const DEFAULT_ADMIN_HOME_PATH = '/analytics'; + @Injectable() export class AdminUserService { /** @@ -97,7 +100,7 @@ export class AdminUserService { const user = this.userRepository.create({ deptId: data.deptId || null, - homePath: data.homePath || '/workspace', + homePath: data.homePath || DEFAULT_ADMIN_HOME_PATH, password: data.password || '123456', realName: data.realName, status: data.status ?? 1, @@ -156,7 +159,11 @@ export class AdminUserService { if (id === currentUserId) { throwVbenError('不能删除当前登录用户', HttpStatus.BAD_REQUEST); } - if (user.username === 'admin') { + if ( + user.id === BUILTIN_ADMIN_USER_ID || + user.username === 'admin' || + user.username === 'kwitsukasa' + ) { throwVbenError('不能删除内置管理员账号', HttpStatus.BAD_REQUEST); } @@ -183,7 +190,8 @@ export class AdminUserService { user.realName = realName; } if (data.homePath !== undefined) { - user.homePath = String(data.homePath || '').trim() || '/workspace'; + user.homePath = + String(data.homePath || '').trim() || DEFAULT_ADMIN_HOME_PATH; } if (data.avatar !== undefined) { user.avatar = String(data.avatar || '').trim(); diff --git a/test/admin/admin-menu.service.spec.ts b/test/admin/admin-menu.service.spec.ts index e02e410..59161a5 100644 --- a/test/admin/admin-menu.service.spec.ts +++ b/test/admin/admin-menu.service.spec.ts @@ -65,6 +65,75 @@ describe('AdminMenuService', () => { expect.arrayContaining(['Blog:Theme:List', 'Blog:Theme:Import']), ); }); + + it('returns and orders menus by the persisted sort field', async () => { + const service = new AdminMenuService({ + find: jest.fn(async () => [ + createMenu({ + id: '2', + meta: { title: 'Second' }, + name: 'Second', + path: '/second', + sort: 20, + type: 'menu', + }), + createMenu({ + id: '1', + meta: { title: 'First' }, + name: 'First', + path: '/first', + sort: 10, + type: 'menu', + }), + ]), + } as any); + const user = { + roles: [ + { + isDeleted: false, + roleCode: 'super', + status: 1, + }, + ], + } as AdminUser; + + await expect(service.getRouteMenus(user)).resolves.toEqual([ + expect.objectContaining({ name: 'First', sort: 10 }), + expect.objectContaining({ name: 'Second', sort: 20 }), + ]); + }); + + it('persists menu sort values from create and update inputs', async () => { + const repository = { + create: jest.fn((input) => input), + save: jest.fn(), + update: jest.fn(), + }; + const service = new AdminMenuService(repository as any); + + await service.createMenu({ + name: 'SortedMenu', + path: '/sorted', + sort: 8, + type: 'menu', + } as any); + await service.updateMenu('menu-1', { + name: 'SortedMenu', + path: '/sorted', + sort: 9, + type: 'menu', + } as any); + + expect(repository.create).toHaveBeenCalledWith( + expect.objectContaining({ sort: 8 }), + ); + expect(repository.update).toHaveBeenCalledWith( + { + id: 'menu-1', + }, + expect.objectContaining({ sort: 9 }), + ); + }); }); /** diff --git a/test/admin/admin-user-avatar-sql.spec.ts b/test/admin/admin-user-avatar-sql.spec.ts index 887581b..d483b15 100644 --- a/test/admin/admin-user-avatar-sql.spec.ts +++ b/test/admin/admin-user-avatar-sql.spec.ts @@ -10,6 +10,10 @@ describe('admin user avatar sql', () => { join(process.cwd(), 'sql/vben-admin-init.sql'), 'utf8', ); + const refactorSeedSql = readFileSync( + join(process.cwd(), 'sql/refactor-v3/01-seed-core.sql'), + 'utf8', + ); it('adds avatar column for existing databases', () => { expect(avatarSql).toContain("COLUMN_NAME = 'avatar'"); @@ -25,4 +29,17 @@ describe('admin user avatar sql', () => { ); expect(initSql).not.toContain('`avatar` = VALUES(`avatar`)'); }); + + it('seeds the renamed administrator and removes the fake workspace menu', () => { + expect(initSql).toContain("'kwitsukasa', '123456', 'KwiTsukasa'"); + expect(initSql).toContain("'/analytics', 'Asia/Shanghai'"); + expect(initSql).toContain("'Analytics', '/analytics'"); + expect(initSql).not.toContain("'vben', '123456'"); + expect(initSql).not.toContain("'jack', '123456'"); + expect(initSql).not.toContain("'Workspace'"); + expect(initSql).not.toContain("'/workspace'"); + expect(initSql).toContain('`username` = VALUES(`username`)'); + expect(initSql).not.toContain('`password` = VALUES(`password`)'); + expect(refactorSeedSql).not.toContain('password = VALUES(password)'); + }); }); diff --git a/test/admin/admin-user-profile.service.spec.ts b/test/admin/admin-user-profile.service.spec.ts index b1a02ab..d50e942 100644 --- a/test/admin/admin-user-profile.service.spec.ts +++ b/test/admin/admin-user-profile.service.spec.ts @@ -2,8 +2,10 @@ import { AdminUserService } from '../../src/modules/admin/identity/user/admin-us describe('AdminUserService profile', () => { const userRepository = { + create: jest.fn((input) => ({ ...input })), findOne: jest.fn(), save: jest.fn(), + update: jest.fn(), }; const roleRepository = { find: jest.fn(), @@ -22,10 +24,33 @@ describe('AdminUserService profile', () => { jest.clearAllMocks(); }); + it('uses environment overview as the default home path for created users', async () => { + userRepository.findOne.mockResolvedValue(null); + roleRepository.find.mockResolvedValue([]); + + await service.createUser({ + realName: '新用户', + roleIds: [], + username: 'new-user', + }); + + expect(userRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + homePath: '/analytics', + username: 'new-user', + }), + ); + expect(userRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + homePath: '/analytics', + }), + ); + }); + it('updates current profile fields including uploaded avatar url', async () => { const user = { avatar: '', - homePath: '/workspace', + homePath: '/analytics', id: '2041700000000000001', realName: '旧姓名', roles: [], @@ -61,11 +86,51 @@ describe('AdminUserService profile', () => { expect(result).toEqual(user); }); + it('falls back to environment overview when current profile clears home path', async () => { + const user = { + avatar: '', + homePath: '/profile', + id: '2041700000000000002', + realName: 'KwiTsukasa', + roles: [], + timezone: 'Asia/Shanghai', + username: 'kwitsukasa', + }; + userRepository.findOne.mockResolvedValue(user); + + await service.updateCurrentProfile(user.id, { + homePath: '', + }); + + expect(userRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + homePath: '/analytics', + }), + ); + }); + + it('prevents deleting the renamed built-in administrator account', async () => { + userRepository.findOne.mockResolvedValue({ + id: '2041700000000000002', + isDeleted: false, + username: 'kwitsukasa', + }); + + await expect(service.deleteUser('2041700000000000002')).rejects.toMatchObject( + { + response: expect.objectContaining({ + msg: '不能删除内置管理员账号', + }), + }, + ); + expect(userRepository.update).not.toHaveBeenCalled(); + }); + it('serializes avatar and userId for Admin frontend user store', () => { expect( service.serializeUser({ avatar: '/api/minio/download?objectName=avatars%2Favatar.jpg', - homePath: '/workspace', + homePath: '/analytics', id: '2041700000000000001', realName: '管理员', roles: [], @@ -74,7 +139,7 @@ describe('AdminUserService profile', () => { } as any), ).toEqual({ avatar: '/api/minio/download?objectName=avatars%2Favatar.jpg', - homePath: '/workspace', + homePath: '/analytics', id: '2041700000000000001', realName: '管理员', roles: [],