feat: 补齐用户管理与接口文档能力

This commit is contained in:
sunlei 2026-06-03 16:03:19 +08:00
parent 29663ef369
commit e60e98bf49
18 changed files with 406 additions and 50 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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`),

View File

@ -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,

View File

@ -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));
}

View File

@ -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<AdminDept>) {
return vbenSuccess(await this.deptService.createDept(body));
}
@Put(':id')
@ApiOperation({ summary: '编辑部门' })
async update(
@Param('id') id: string,
@Body() body: Partial<AdminDept>,
@ -39,6 +42,7 @@ export class AdminDeptController {
}
@Delete(':id')
@ApiOperation({ summary: '删除部门' })
async remove(@Param('id') id: string) {
return vbenSuccess(await this.deptService.deleteDept(id));
}

View File

@ -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<string, any>,
) {
@ -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');

View File

@ -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<AdminMenu>) {
return vbenSuccess(await this.menuService.createMenu(body));
}
@Put('system/menu/:id')
@ApiOperation({ summary: '编辑系统菜单' })
async update(
@Param('id') id: string,
@Body() body: Partial<AdminMenu>,
@ -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));
}

View File

@ -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<string, any>) {
const page = await this.roleService.getRoleList(query);
return vbenPage(page.items, page.total);
}
@Post()
@ApiOperation({ summary: '新增角色' })
async create(@Body() body: Record<string, any>) {
return vbenSuccess(await this.roleService.createRole(body));
}
@Put(':id')
@ApiOperation({ summary: '编辑角色' })
async update(
@Param('id') id: string,
@Body() body: Record<string, any>,
@ -40,6 +43,7 @@ export class AdminRoleController {
}
@Delete(':id')
@ApiOperation({ summary: '删除角色' })
async remove(@Param('id') id: string) {
return vbenSuccess(await this.roleService.deleteRole(id));
}

View File

@ -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 },

View File

@ -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<string, any>) {
const page = await this.userService.getUserList(query);
return vbenPage(page.items, page.total);
}
@Post()
@ApiOperation({ summary: '新增用户' })
async create(@Body() body: Record<string, any>) {
return vbenSuccess(await this.userService.createUser(body));
}
@Put(':id')
@ApiOperation({ summary: '编辑用户' })
async update(@Param('id') id: string, @Body() body: Record<string, any>) {
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));
}
}

View File

@ -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));
}

View File

@ -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);

View File

@ -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<AdminUser> & {
roleIds?: string[];
};
type ListQuery = Record<string, any>;
@Injectable()
export class AdminUserService {
constructor(
@InjectRepository(AdminUser)
private readonly userRepository: Repository<AdminUser>,
@InjectRepository(AdminRole)
private readonly roleRepository: Repository<AdminRole>,
@InjectRepository(AdminDept)
private readonly deptRepository: Repository<AdminDept>,
) {}
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<string>([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);
}
}
}

View File

@ -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文档' })

View File

@ -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;

View File

@ -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(

View File

@ -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": {
"@/*": [