feat: 对齐管理员菜单排序与默认首页
This commit is contained in:
parent
828a321bdb
commit
7056663fe0
2
API.md
2
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
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
),
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -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)');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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: [],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user