From e60e98bf496210bcff9acded80babdc4a278def0 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 3 Jun 2026 16:03:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=B8=8E=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pnpm-lock.yaml | 33 +-- sql/vben-admin-init.sql | 57 ++++- src/admin/admin.module.ts | 2 + src/admin/auth/admin-auth.controller.ts | 6 +- src/admin/dept/admin-dept.controller.ts | 6 +- src/admin/example/admin-example.controller.ts | 8 +- src/admin/menu/admin-menu.controller.ts | 9 +- src/admin/role/admin-role.controller.ts | 6 +- .../timezone/admin-timezone.controller.ts | 5 +- .../user/admin-user-manage.controller.ts | 51 ++++ src/admin/user/admin-user.controller.ts | 3 +- src/admin/user/admin-user.entity.ts | 19 ++ src/admin/user/admin-user.service.ts | 230 +++++++++++++++++- src/app.controller.ts | 3 - src/main.ts | 4 +- .../ff14Market/qqbot-ff14-client.service.ts | 9 +- tsconfig.json | 3 + 18 files changed, 406 insertions(+), 50 deletions(-) create mode 100644 src/admin/user/admin-user-manage.controller.ts diff --git a/package.json b/package.json index 8e22d1c..2af4b1c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@kwitsukasa/knife4j-swagger-vue3": "0.1.2", "@nestjs/common": "^9.4.3", "@nestjs/config": "^2.3.4", "@nestjs/core": "^9.4.3", @@ -37,7 +38,6 @@ "lodash": "^4.17.21", "mqtt": "^5.15.1", "mysql2": "^3.22.3", - "nestjs-knife4j-plus": "^1.0.9", "nestjs-minio-client": "^2.2.0", "reflect-metadata": "^0.1.14", "rxjs": "^7.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 273bf66..cd89dc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@kwitsukasa/knife4j-swagger-vue3': + specifier: 0.1.2 + version: 0.1.2(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/common': specifier: ^9.4.3 version: 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -41,9 +44,6 @@ importers: mysql2: specifier: ^3.22.3 version: 3.22.3(@types/node@18.11.18) - nestjs-knife4j-plus: - specifier: ^1.0.9 - version: 1.0.9(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2) nestjs-minio-client: specifier: ^2.2.0 version: 2.2.0(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3) @@ -554,6 +554,11 @@ packages: '@js-joda/core@5.7.0': resolution: {integrity: sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==} + '@kwitsukasa/knife4j-swagger-vue3@0.1.2': + resolution: {integrity: sha512-OsVKUo/URe1zONjW4ZmxXIdRiVHLNufkjYYDi1DuZqXm0a743NlezAAb12wwOajwkyyUNCCgs6NP5x6WkK8NEw==} + peerDependencies: + '@nestjs/common': '*' + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -2664,19 +2669,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nestjs-knife4j-plus@1.0.9: - resolution: {integrity: sha512-MgMrfXgJfRgwW43BbMAKvInxluuvdNjSiYmkBvQvUawJMwn6+Op4W8/iYG1Q7u4D7mefWtM4zwVA7gBHErWCzg==} - peerDependencies: - '@fastify/static': '*' - '@nestjs/common': '*' - express: '*' - fastify: '*' - peerDependenciesMeta: - '@fastify/static': - optional: true - fastify: - optional: true - nestjs-minio-client@2.2.0: resolution: {integrity: sha512-mz1vfJq/7YfSyVCIeZwOCfIfBz+msI9QynHS2QGO9GB+tVNnQOYta8PxFsH9tMxN7gNrjrf5jXsEIpgBB1oTeA==} peerDependencies: @@ -4422,6 +4414,10 @@ snapshots: '@js-joda/core@5.7.0': optional: true + '@kwitsukasa/knife4j-swagger-vue3@0.1.2(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@lukeed/csprng@1.1.0': {} '@microsoft/tsdoc@0.15.1': {} @@ -7074,11 +7070,6 @@ snapshots: neo-async@2.6.2: {} - nestjs-knife4j-plus@1.0.9(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2): - dependencies: - '@nestjs/common': 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) - express: 4.18.2 - nestjs-minio-client@2.2.0(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3): dependencies: '@nestjs/common': 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) diff --git a/sql/vben-admin-init.sql b/sql/vben-admin-init.sql index 6a4f8a4..24a0fee 100644 --- a/sql/vben-admin-init.sql +++ b/sql/vben-admin-init.sql @@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `admin_user` ( `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `real_name` varchar(255) NOT NULL, + `dept_id` bigint DEFAULT NULL, `home_path` varchar(255) NOT NULL DEFAULT '', `timezone` varchar(255) NOT NULL DEFAULT 'Asia/Shanghai', `status` int NOT NULL DEFAULT 1, @@ -52,7 +53,8 @@ CREATE TABLE IF NOT EXISTS `admin_user` ( `create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), - UNIQUE KEY `uk_admin_user_username` (`username`) + UNIQUE KEY `uk_admin_user_username` (`username`), + KEY `idx_admin_user_dept_id` (`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `admin_dept` ( @@ -68,6 +70,38 @@ CREATE TABLE IF NOT EXISTS `admin_dept` ( KEY `idx_admin_dept_pid` (`pid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +SET @admin_user_dept_id_exists := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'admin_user' + AND COLUMN_NAME = 'dept_id' +); +SET @admin_user_dept_id_sql := IF( + @admin_user_dept_id_exists = 0, + 'ALTER TABLE `admin_user` ADD COLUMN `dept_id` bigint NULL AFTER `real_name`', + 'SELECT 1' +); +PREPARE admin_user_dept_id_stmt FROM @admin_user_dept_id_sql; +EXECUTE admin_user_dept_id_stmt; +DEALLOCATE PREPARE admin_user_dept_id_stmt; + +SET @admin_user_dept_idx_exists := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'admin_user' + AND INDEX_NAME = 'idx_admin_user_dept_id' +); +SET @admin_user_dept_idx_sql := IF( + @admin_user_dept_idx_exists = 0, + 'ALTER TABLE `admin_user` ADD INDEX `idx_admin_user_dept_id` (`dept_id`)', + 'SELECT 1' +); +PREPARE admin_user_dept_idx_stmt FROM @admin_user_dept_idx_sql; +EXECUTE admin_user_dept_idx_stmt; +DEALLOCATE PREPARE admin_user_dept_idx_stmt; + CREATE TABLE IF NOT EXISTS `admin_dict` ( `id` bigint NOT NULL, `dict_code` varchar(255) NOT NULL, @@ -125,19 +159,23 @@ VALUES (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), - (2041700000000100200, 2041700000000100002, 'SystemRole', '/system/role', '/system/role/list', NULL, 'System:Role:List', 'menu', '{"icon":"carbon:user-role","title":"system.role.title"}', 1, 0), + (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), + (2041700000000120192, 2041700000000100199, 'SystemUserEdit', NULL, NULL, NULL, 'System:User:Edit', 'button', '{"title":"common.edit"}', 1, 0), + (2041700000000120193, 2041700000000100199, 'SystemUserDelete', NULL, NULL, NULL, 'System:User:Delete', 'button', '{"title":"common.delete"}', 1, 0), + (2041700000000100200, 2041700000000100002, 'SystemRole', '/system/role', '/system/role/list', NULL, 'System:Role:List', 'menu', '{"icon":"carbon:user-role","title":"system.role.title"}', 1, 1), (2041700000000120001, 2041700000000100200, 'SystemRoleCreate', NULL, NULL, NULL, 'System:Role:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120002, 2041700000000100200, 'SystemRoleEdit', NULL, NULL, NULL, 'System:Role:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120003, 2041700000000100200, 'SystemRoleDelete', NULL, NULL, NULL, 'System:Role:Delete', 'button', '{"title":"common.delete"}', 1, 0), - (2041700000000100201, 2041700000000100002, 'SystemMenu', '/system/menu', '/system/menu/list', NULL, 'System:Menu:List', 'menu', '{"icon":"carbon:menu","title":"system.menu.title"}', 1, 0), + (2041700000000100201, 2041700000000100002, 'SystemMenu', '/system/menu', '/system/menu/list', NULL, 'System:Menu:List', 'menu', '{"icon":"carbon:menu","title":"system.menu.title"}', 1, 2), (2041700000000120101, 2041700000000100201, 'SystemMenuCreate', NULL, NULL, NULL, 'System:Menu:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120102, 2041700000000100201, 'SystemMenuEdit', NULL, NULL, NULL, 'System:Menu:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120103, 2041700000000100201, 'SystemMenuDelete', NULL, NULL, NULL, 'System:Menu:Delete', 'button', '{"title":"common.delete"}', 1, 0), - (2041700000000100202, 2041700000000100002, 'SystemDept', '/system/dept', '/system/dept/list', NULL, 'System:Dept:List', 'menu', '{"icon":"carbon:container-services","title":"system.dept.title"}', 1, 0), + (2041700000000100202, 2041700000000100002, 'SystemDept', '/system/dept', '/system/dept/list', NULL, 'System:Dept:List', 'menu', '{"icon":"carbon:container-services","title":"system.dept.title"}', 1, 3), (2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0), - (2041700000000100203, 2041700000000100002, 'SystemKtTableDemo', '/system/ktTableDemo', '/system/ktTableDemo/list', NULL, 'System:KtTableDemo:List', 'menu', '{"icon":"lucide:table-2","title":"system.ktTableDemo.title"}', 1, 3), + (2041700000000100203, 2041700000000100002, 'SystemKtTableDemo', '/system/ktTableDemo', '/system/ktTableDemo/list', NULL, 'System:KtTableDemo:List', 'menu', '{"icon":"lucide:table-2","title":"system.ktTableDemo.title"}', 1, 4), (2041700000000120204, 2041700000000100203, 'SystemKtTableDemoCreate', NULL, NULL, NULL, 'System:KtTableDemo:Create', 'button', '{"title":"common.create"}', 1, 0), (2041700000000120205, 2041700000000100203, 'SystemKtTableDemoEdit', NULL, NULL, NULL, 'System:KtTableDemo:Edit', 'button', '{"title":"common.edit"}', 1, 0), (2041700000000120206, 2041700000000100203, 'SystemKtTableDemoDelete', NULL, NULL, NULL, 'System:KtTableDemo:Delete', 'button', '{"title":"common.delete"}', 1, 0), @@ -181,14 +219,15 @@ ON DUPLICATE KEY UPDATE `status` = VALUES(`status`), `is_deleted` = 0; -INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `home_path`, `timezone`, `status`) +INSERT INTO `admin_user` (`id`, `username`, `password`, `real_name`, `dept_id`, `home_path`, `timezone`, `status`) VALUES - (2041700000000000001, 'vben', '123456', 'Vben', '/workspace', 'Asia/Shanghai', 1), - (2041700000000000002, 'admin', '123456', 'Admin', '/workspace', 'Asia/Shanghai', 1), - (2041700000000000003, 'jack', '123456', 'Jack', '/analytics', 'Asia/Shanghai', 1) + (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) ON DUPLICATE KEY UPDATE `password` = VALUES(`password`), `real_name` = VALUES(`real_name`), + `dept_id` = VALUES(`dept_id`), `home_path` = VALUES(`home_path`), `timezone` = VALUES(`timezone`), `status` = VALUES(`status`), diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index c6ae621..9d76399 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -18,6 +18,7 @@ import { AdminRole } from './role/admin-role.entity'; import { AdminRoleService } from './role/admin-role.service'; import { AdminTimezoneController } from './timezone/admin-timezone.controller'; import { AdminTimezoneService } from './timezone/admin-timezone.service'; +import { AdminUserManageController } from './user/admin-user-manage.controller'; import { AdminUserController } from './user/admin-user.controller'; import { AdminUser } from './user/admin-user.entity'; import { AdminUserService } from './user/admin-user.service'; @@ -42,6 +43,7 @@ import { WordpressModule } from '@/wordpress/wordpress.module'; controllers: [ AdminAuthController, AdminUserController, + AdminUserManageController, AdminMenuController, AdminRoleController, AdminDeptController, diff --git a/src/admin/auth/admin-auth.controller.ts b/src/admin/auth/admin-auth.controller.ts index 7f21b01..d51f376 100644 --- a/src/admin/auth/admin-auth.controller.ts +++ b/src/admin/auth/admin-auth.controller.ts @@ -7,7 +7,7 @@ import { Res, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Request, Response } from 'express'; import { CurrentAdminUser, Public, vbenSuccess } from '@/common'; import { AdminMenuService } from '../menu/admin-menu.service'; @@ -29,6 +29,7 @@ export class AdminAuthController { ) {} @Post('auth/login') + @ApiOperation({ summary: 'Admin 用户登录' }) @Public() async login( @Body() body: { password?: string; username?: string }, @@ -63,6 +64,7 @@ export class AdminAuthController { } @Post('auth/refresh') + @ApiOperation({ summary: '刷新 Admin 访问令牌' }) @Public() async refresh( @Req() req: Request, @@ -76,6 +78,7 @@ export class AdminAuthController { } @Post('auth/logout') + @ApiOperation({ summary: 'Admin 用户退出登录' }) @Public() logout(@Res({ passthrough: true }) res: Response) { this.authService.clearAccessTokenCookie(res); @@ -85,6 +88,7 @@ export class AdminAuthController { } @Get('auth/codes') + @ApiOperation({ summary: '获取当前用户按钮权限码' }) async getAccessCodes(@CurrentAdminUser() user: AdminUser) { return vbenSuccess(await this.menuService.getAccessCodes(user)); } diff --git a/src/admin/dept/admin-dept.controller.ts b/src/admin/dept/admin-dept.controller.ts index f5d713c..d821db2 100644 --- a/src/admin/dept/admin-dept.controller.ts +++ b/src/admin/dept/admin-dept.controller.ts @@ -8,7 +8,7 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { vbenSuccess } from '@/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminDept } from './admin-dept.entity'; @@ -21,16 +21,19 @@ export class AdminDeptController { constructor(private readonly deptService: AdminDeptService) {} @Get('list') + @ApiOperation({ summary: '获取部门列表' }) async list() { return vbenSuccess(await this.deptService.getDeptList()); } @Post() + @ApiOperation({ summary: '新增部门' }) async create(@Body() body: Partial) { return vbenSuccess(await this.deptService.createDept(body)); } @Put(':id') + @ApiOperation({ summary: '编辑部门' }) async update( @Param('id') id: string, @Body() body: Partial, @@ -39,6 +42,7 @@ export class AdminDeptController { } @Delete(':id') + @ApiOperation({ summary: '删除部门' }) async remove(@Param('id') id: string) { return vbenSuccess(await this.deptService.deleteDept(id)); } diff --git a/src/admin/example/admin-example.controller.ts b/src/admin/example/admin-example.controller.ts index 9985596..2721b4d 100644 --- a/src/admin/example/admin-example.controller.ts +++ b/src/admin/example/admin-example.controller.ts @@ -9,7 +9,7 @@ import { UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Public, vbenPage, vbenSuccess } from '@/common'; @@ -73,6 +73,7 @@ export class AdminExampleController { constructor(private readonly minioClientService: MinioClientService) {} @Post('upload') + @ApiOperation({ summary: '上传示例文件' }) @UseInterceptors(FileInterceptor('file')) async upload(@UploadedFile() file: MinioUploadFile) { const result = await this.minioClientService.uploadObject({ file }); @@ -83,6 +84,7 @@ export class AdminExampleController { } @Get('table/list') + @ApiOperation({ summary: '获取示例表格分页列表' }) async tableList( @Query() query: Record, ) { @@ -95,6 +97,7 @@ export class AdminExampleController { } @Get('status') + @ApiOperation({ summary: '返回指定状态码示例' }) @Public() status(@Query('status') status: string, @Res() res: Response) { const code = Number(status) || 200; @@ -112,6 +115,7 @@ export class AdminExampleController { } @Get('demo/bigint') + @ApiOperation({ summary: '获取 BigInt 序列化示例' }) async bigint(@Res() res: Response) { res.setHeader('Content-Type', 'application/json'); res.send(`{ @@ -135,12 +139,14 @@ export class AdminExampleController { } @Get('test') + @ApiOperation({ summary: 'GET 测试接口' }) @Public() testGet() { return vbenSuccess('Test get handler'); } @Post('test') + @ApiOperation({ summary: 'POST 测试接口' }) @Public() testPost() { return vbenSuccess('Test post handler'); diff --git a/src/admin/menu/admin-menu.controller.ts b/src/admin/menu/admin-menu.controller.ts index 372d066..ba4f6ba 100644 --- a/src/admin/menu/admin-menu.controller.ts +++ b/src/admin/menu/admin-menu.controller.ts @@ -9,7 +9,7 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentAdminUser, vbenSuccess } from '@/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminUser } from '../user/admin-user.entity'; @@ -23,16 +23,19 @@ export class AdminMenuController { constructor(private readonly menuService: AdminMenuService) {} @Get('menu/all') + @ApiOperation({ summary: '获取当前用户路由菜单' }) async all(@CurrentAdminUser() user: AdminUser) { return vbenSuccess(await this.menuService.getRouteMenus(user)); } @Get('system/menu/list') + @ApiOperation({ summary: '获取系统菜单列表' }) async list() { return vbenSuccess(await this.menuService.getMenuList()); } @Get('system/menu/name-exists') + @ApiOperation({ summary: '校验菜单名称是否存在' }) async nameExists( @Query('name') name: string, @Query('id') id?: string, @@ -41,6 +44,7 @@ export class AdminMenuController { } @Get('system/menu/path-exists') + @ApiOperation({ summary: '校验菜单路径是否存在' }) async pathExists( @Query('path') path: string, @Query('id') id?: string, @@ -49,11 +53,13 @@ export class AdminMenuController { } @Post('system/menu') + @ApiOperation({ summary: '新增系统菜单' }) async create(@Body() body: Partial) { return vbenSuccess(await this.menuService.createMenu(body)); } @Put('system/menu/:id') + @ApiOperation({ summary: '编辑系统菜单' }) async update( @Param('id') id: string, @Body() body: Partial, @@ -62,6 +68,7 @@ export class AdminMenuController { } @Delete('system/menu/:id') + @ApiOperation({ summary: '删除系统菜单' }) async remove(@Param('id') id: string) { return vbenSuccess(await this.menuService.deleteMenu(id)); } diff --git a/src/admin/role/admin-role.controller.ts b/src/admin/role/admin-role.controller.ts index 5a99f46..c9ad79a 100644 --- a/src/admin/role/admin-role.controller.ts +++ b/src/admin/role/admin-role.controller.ts @@ -9,7 +9,7 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { vbenPage, vbenSuccess } from '@/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminRoleService } from './admin-role.service'; @@ -21,17 +21,20 @@ export class AdminRoleController { constructor(private readonly roleService: AdminRoleService) {} @Get('list') + @ApiOperation({ summary: '获取角色分页列表' }) async list(@Query() query: Record) { const page = await this.roleService.getRoleList(query); return vbenPage(page.items, page.total); } @Post() + @ApiOperation({ summary: '新增角色' }) async create(@Body() body: Record) { return vbenSuccess(await this.roleService.createRole(body)); } @Put(':id') + @ApiOperation({ summary: '编辑角色' }) async update( @Param('id') id: string, @Body() body: Record, @@ -40,6 +43,7 @@ export class AdminRoleController { } @Delete(':id') + @ApiOperation({ summary: '删除角色' }) async remove(@Param('id') id: string) { return vbenSuccess(await this.roleService.deleteRole(id)); } diff --git a/src/admin/timezone/admin-timezone.controller.ts b/src/admin/timezone/admin-timezone.controller.ts index 11efda2..c410970 100644 --- a/src/admin/timezone/admin-timezone.controller.ts +++ b/src/admin/timezone/admin-timezone.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentAdminUser, vbenSuccess } from '@/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminUser } from '../user/admin-user.entity'; @@ -20,16 +20,19 @@ export class AdminTimezoneController { constructor(private readonly timezoneService: AdminTimezoneService) {} @Get('getTimezoneOptions') + @ApiOperation({ summary: '获取时区选项' }) getOptions() { return vbenSuccess(TIMEZONE_OPTIONS); } @Get('getTimezone') + @ApiOperation({ summary: '获取当前用户时区' }) async getTimezone(@CurrentAdminUser() user: AdminUser) { return vbenSuccess(await this.timezoneService.getTimezone(user)); } @Post('setTimezone') + @ApiOperation({ summary: '设置当前用户时区' }) async setTimezone( @CurrentAdminUser() user: AdminUser, @Body() body: { timezone?: string }, diff --git a/src/admin/user/admin-user-manage.controller.ts b/src/admin/user/admin-user-manage.controller.ts new file mode 100644 index 0000000..c077e15 --- /dev/null +++ b/src/admin/user/admin-user-manage.controller.ts @@ -0,0 +1,51 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentAdminUser, vbenPage, vbenSuccess } from '@/common'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { AdminUser } from './admin-user.entity'; +import { AdminUserService } from './admin-user.service'; + +@ApiTags('Admin - 用户管理') +@Controller('system/user') +@UseGuards(JwtAuthGuard) +export class AdminUserManageController { + constructor(private readonly userService: AdminUserService) {} + + @Get('list') + @ApiOperation({ summary: '获取用户分页列表' }) + async list(@Query() query: Record) { + const page = await this.userService.getUserList(query); + return vbenPage(page.items, page.total); + } + + @Post() + @ApiOperation({ summary: '新增用户' }) + async create(@Body() body: Record) { + return vbenSuccess(await this.userService.createUser(body)); + } + + @Put(':id') + @ApiOperation({ summary: '编辑用户' }) + async update(@Param('id') id: string, @Body() body: Record) { + return vbenSuccess(await this.userService.updateUser(id, body)); + } + + @Delete(':id') + @ApiOperation({ summary: '删除用户' }) + async remove( + @Param('id') id: string, + @CurrentAdminUser() currentUser: AdminUser, + ) { + return vbenSuccess(await this.userService.deleteUser(id, currentUser?.id)); + } +} diff --git a/src/admin/user/admin-user.controller.ts b/src/admin/user/admin-user.controller.ts index 53c117c..e365840 100644 --- a/src/admin/user/admin-user.controller.ts +++ b/src/admin/user/admin-user.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentAdminUser, vbenSuccess } from '@/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminUser } from './admin-user.entity'; @@ -12,6 +12,7 @@ export class AdminUserController { constructor(private readonly userService: AdminUserService) {} @Get('info') + @ApiOperation({ summary: '获取当前用户信息' }) async info(@CurrentAdminUser() user: AdminUser) { return vbenSuccess(this.userService.serializeUser(user)); } diff --git a/src/admin/user/admin-user.entity.ts b/src/admin/user/admin-user.entity.ts index f37cd66..cf15447 100644 --- a/src/admin/user/admin-user.entity.ts +++ b/src/admin/user/admin-user.entity.ts @@ -3,12 +3,15 @@ import { Column, CreateDateColumn, Entity, + JoinColumn, JoinTable, ManyToMany, + ManyToOne, PrimaryColumn, UpdateDateColumn, } from 'typeorm'; import { ensureSnowflakeId } from '@/common'; +import { AdminDept } from '../dept/admin-dept.entity'; import { AdminRole } from '../role/admin-role.entity'; @Entity('admin_user') @@ -31,6 +34,13 @@ export class AdminUser { }) realName: string; + @Column({ + name: 'dept_id', + nullable: true, + type: 'bigint', + }) + deptId?: string | null; + @Column({ default: '', name: 'home_path', @@ -79,6 +89,15 @@ export class AdminUser { }) roles: AdminRole[]; + @ManyToOne(() => AdminDept, { + eager: true, + nullable: true, + }) + @JoinColumn({ + name: 'dept_id', + }) + dept?: AdminDept | null; + @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/admin/user/admin-user.service.ts b/src/admin/user/admin-user.service.ts index c59ed24..b7ba53f 100644 --- a/src/admin/user/admin-user.service.ts +++ b/src/admin/user/admin-user.service.ts @@ -1,8 +1,152 @@ -import { Injectable } from '@nestjs/common'; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Not, Repository } from 'typeorm'; +import { throwVbenError } from '@/common'; +import { AdminDept } from '../dept/admin-dept.entity'; +import { AdminRole } from '../role/admin-role.entity'; import { AdminUser } from './admin-user.entity'; +type UserInput = Partial & { + roleIds?: string[]; +}; + +type ListQuery = Record; + @Injectable() export class AdminUserService { + constructor( + @InjectRepository(AdminUser) + private readonly userRepository: Repository, + @InjectRepository(AdminRole) + private readonly roleRepository: Repository, + @InjectRepository(AdminDept) + private readonly deptRepository: Repository, + ) {} + + async getUserList(query: ListQuery) { + const page = Number(query.page || 1); + const pageSize = Number(query.pageSize || 20); + const builder = this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'role') + .leftJoinAndSelect('user.dept', 'dept') + .where('user.isDeleted = :isDeleted', { isDeleted: false }); + + if (query.id) { + builder.andWhere('user.id LIKE :id', { id: `%${query.id}%` }); + } + if (query.username) { + builder.andWhere('user.username LIKE :username', { + username: `%${query.username}%`, + }); + } + if (query.realName) { + builder.andWhere('user.realName LIKE :realName', { + realName: `%${query.realName}%`, + }); + } + if (['0', '1'].includes(String(query.status))) { + builder.andWhere('user.status = :status', { + status: Number(query.status), + }); + } + if (query.deptId) { + const deptIds = await this.collectDeptIds(String(query.deptId)); + builder.andWhere('user.deptId IN (:...deptIds)', { deptIds }); + } + if (query.roleId) { + builder.andWhere('role.id = :roleId', { roleId: String(query.roleId) }); + } + if (query.startTime) { + builder.andWhere('user.createTime >= :startTime', { + startTime: query.startTime, + }); + } + if (query.endTime) { + builder.andWhere('user.createTime <= :endTime', { + endTime: query.endTime, + }); + } + + const [users, total] = await builder + .orderBy('user.createTime', 'ASC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + + return { + items: users.map((user) => this.serializeUserForList(user)), + total, + }; + } + + async createUser(data: UserInput) { + await this.ensureUsernameAvailable(String(data.username || '')); + + const user = this.userRepository.create({ + deptId: data.deptId || null, + homePath: data.homePath || '/workspace', + password: data.password || '123456', + realName: data.realName, + status: data.status ?? 1, + timezone: data.timezone || 'Asia/Shanghai', + username: data.username, + }); + user.roles = await this.findRolesByIds(data.roleIds || []); + await this.userRepository.save(user); + return null; + } + + async updateUser(id: string, data: UserInput) { + const user = await this.userRepository.findOne({ + relations: ['roles'], + where: { + id, + isDeleted: false, + }, + }); + if (!user) throwVbenError('用户不存在', HttpStatus.BAD_REQUEST); + + if (data.username !== undefined && data.username !== user.username) { + await this.ensureUsernameAvailable(data.username, id); + user.username = data.username; + } + if (data.password) user.password = data.password; + if (data.deptId !== undefined) user.deptId = data.deptId || null; + if (data.realName !== undefined) user.realName = data.realName; + if (data.homePath !== undefined) user.homePath = data.homePath; + if (data.timezone !== undefined) user.timezone = data.timezone; + if (data.status !== undefined) user.status = data.status; + if (data.roleIds) user.roles = await this.findRolesByIds(data.roleIds); + + await this.userRepository.save(user); + return null; + } + + async deleteUser(id: string, currentUserId?: string) { + const user = await this.userRepository.findOne({ + where: { + id, + isDeleted: false, + }, + }); + if (!user) throwVbenError('用户不存在', HttpStatus.BAD_REQUEST); + if (id === currentUserId) { + throwVbenError('不能删除当前登录用户', HttpStatus.BAD_REQUEST); + } + if (user.username === 'admin') { + throwVbenError('不能删除内置管理员账号', HttpStatus.BAD_REQUEST); + } + + await this.userRepository.update( + { id }, + { + isDeleted: true, + }, + ); + return null; + } + serializeUser(user: AdminUser) { return { homePath: user.homePath, @@ -14,4 +158,88 @@ export class AdminUserService { username: user.username, }; } + + private serializeUserForList(user: AdminUser) { + const activeRoles = (user.roles || []).filter((role) => !role.isDeleted); + return { + createTime: user.createTime, + dept: user.dept + ? { + id: user.dept.id, + name: user.dept.name, + } + : null, + deptId: user.deptId, + deptName: user.dept?.name || '', + homePath: user.homePath, + id: user.id, + realName: user.realName, + roleIds: activeRoles.map((role) => role.id), + roleNames: activeRoles.map((role) => role.name), + roles: activeRoles.map((role) => ({ + id: role.id, + name: role.name, + roleCode: role.roleCode, + status: role.status, + })), + status: user.status, + timezone: user.timezone, + updateTime: user.updateTime, + username: user.username, + }; + } + + private async findRolesByIds(ids: string[]) { + const normalizedIds = ids.map((id) => String(id)).filter(Boolean); + if (!normalizedIds.length) return []; + return this.roleRepository.find({ + where: normalizedIds.map((id) => ({ + id, + isDeleted: false, + })), + }); + } + + private async collectDeptIds(deptId: string) { + if (deptId === '0') return ['0']; + + const depts = await this.deptRepository.find({ + where: { + isDeleted: false, + }, + }); + const result = new Set([deptId]); + let changed = true; + + while (changed) { + changed = false; + for (const dept of depts) { + if (result.has(String(dept.pid)) && !result.has(dept.id)) { + result.add(dept.id); + changed = true; + } + } + } + + return Array.from(result); + } + + private async ensureUsernameAvailable(username: string, ignoreId?: string) { + if (!username.trim()) { + throwVbenError('用户名不能为空', HttpStatus.BAD_REQUEST); + } + + const where = ignoreId + ? { + id: Not(ignoreId), + username, + } + : { + username, + }; + const existing = await this.userRepository.findOne({ where }); + if (existing) { + throwVbenError('用户名已存在', HttpStatus.BAD_REQUEST); + } + } } diff --git a/src/app.controller.ts b/src/app.controller.ts index 471d0f2..1c1356c 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -4,13 +4,10 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { AppService } from './app.service'; @ApiTags('基础能力 - 根入口') @Controller() export class AppController { - constructor(private readonly appService: AppService) {} - @Get() @Redirect('/api#/', 301) @ApiOperation({ summary: '重定向到Swagger文档' }) diff --git a/src/main.ts b/src/main.ts index 87e31f2..61c9f70 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,8 +3,8 @@ import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import type { OpenAPIObject } from '@nestjs/swagger'; import { urlencoded, json } from 'express'; -import { knife4jSetup } from 'nestjs-knife4j-plus'; -import type { Service } from 'nestjs-knife4j-plus'; +import { knife4jSetup } from '@kwitsukasa/knife4j-swagger-vue3'; +import type { Service } from '@kwitsukasa/knife4j-swagger-vue3'; import { applySwaggerResponseExamples } from './common'; type SwaggerPathMatcher = (path: string) => boolean; diff --git a/src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts b/src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts index e36c6bf..3d98bf4 100644 --- a/src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts +++ b/src/qqbot/plugins/ff14Market/qqbot-ff14-client.service.ts @@ -300,11 +300,11 @@ export class QqbotFf14ClientService { } const fuzzyItems = await this.searchItemsByLanguage(keyword, language, '~'); - const fuzzyItem = this.pickSingleFuzzySearchItem(keyword, fuzzyItems); + const fuzzyItem = this.pickSingleFuzzySearchItem(fuzzyItems); if (fuzzyItem || language === 'en') return fuzzyItem; const enFuzzyItems = await this.searchItemsByLanguage(keyword, 'en', '~'); - return this.pickSingleFuzzySearchItem(keyword, enFuzzyItems); + return this.pickSingleFuzzySearchItem(enFuzzyItems); } private async searchItemsByLanguage( @@ -337,10 +337,7 @@ export class QqbotFf14ClientService { return items[0]; } - private pickSingleFuzzySearchItem( - keyword: string, - items: XivapiSearchItem[], - ) { + private pickSingleFuzzySearchItem(items: XivapiSearchItem[]) { if (items.length <= 1) return items[0]; throw new Error( `找到多个相似物品,请输入更完整名称或物品 ID:${this.formatSearchCandidates( diff --git a/tsconfig.json b/tsconfig.json index 95054ec..1494043 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "ignoreDeprecations": "6.0", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, @@ -15,6 +16,8 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, + "noUnusedLocals": true, + "noUnusedParameters": true, "noFallthroughCasesInSwitch": false, "paths": { "@/*": [