feat: add Swagger API docs (/docs) with DTO schemas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-10 21:39:37 +08:00
parent bbc06ae775
commit 24790e8985
13 changed files with 500 additions and 78 deletions

View File

@ -30,6 +30,7 @@
"@nestjs/core": "^12.0.1", "@nestjs/core": "^12.0.1",
"@nestjs/jwt": "^12.0.2", "@nestjs/jwt": "^12.0.2",
"@nestjs/platform-express": "^12.0.1", "@nestjs/platform-express": "^12.0.1",
"@nestjs/swagger": "^12.0.2",
"@prisma/client": "^6.19.3", "@prisma/client": "^6.19.3",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

98
pnpm-lock.yaml generated
View File

@ -178,6 +178,9 @@ importers:
'@nestjs/platform-express': '@nestjs/platform-express':
specifier: ^12.0.1 specifier: ^12.0.1
version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
'@nestjs/swagger':
specifier: ^12.0.2
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3)
'@prisma/client': '@prisma/client':
specifier: ^6.19.3 specifier: ^6.19.3
version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)
@ -616,6 +619,9 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
'@microsoft/tsdoc@0.17.0':
resolution: {integrity: sha512-p68VexhnH7ojf3U27RdryUDqnKJgKaWUbCL96vJR7N0mqDuhKG5OIECk3sCPPdynxjk+yoVhRkScqtv5KQjzsw==}
'@nestjs/cli@12.0.8': '@nestjs/cli@12.0.8':
resolution: {integrity: sha512-iqm+KXb8cnpnlD4hMBf1w2/4i1GSs0DCLI7oyP4MTmwf9KmjTG1SpAjkl/D+kKuUToycBrMF4+eSwyPVdTMGKQ==} resolution: {integrity: sha512-iqm+KXb8cnpnlD4hMBf1w2/4i1GSs0DCLI7oyP4MTmwf9KmjTG1SpAjkl/D+kKuUToycBrMF4+eSwyPVdTMGKQ==}
engines: {node: '>= 20.11'} engines: {node: '>= 20.11'}
@ -683,6 +689,20 @@ packages:
peerDependencies: peerDependencies:
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
'@nestjs/mapped-types@12.0.0':
resolution: {integrity: sha512-aX7lxZcqXldtnSr0bgT2ZPQvpTpIwBMW1GMX29o4s3q02vmq6JoTYuPW0GwYXa8BiFMRX7WbGZVDA703pNqOMQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0 || ^12.0.0
class-transformer: ^0.4.0 || ^0.5.0
class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/mau@0.2.8': '@nestjs/mau@0.2.8':
resolution: {integrity: sha512-nIb0i3c/KhKEbxZfYMEKv9Mcvk5miBdZcexnvkgnAUhoMO2BUjOGT+Khf2cZzu7B51Y7v01NG2JVl/HdQEUQ6w==} resolution: {integrity: sha512-nIb0i3c/KhKEbxZfYMEKv9Mcvk5miBdZcexnvkgnAUhoMO2BUjOGT+Khf2cZzu7B51Y7v01NG2JVl/HdQEUQ6w==}
engines: {node: '>= 20.0.0'} engines: {node: '>= 20.0.0'}
@ -704,6 +724,27 @@ packages:
prettier: prettier:
optional: true optional: true
'@nestjs/swagger@12.0.2':
resolution: {integrity: sha512-/IDj6o2k1wqZybhIkMaJp58YNrAos/4TwTf3zBn/j1flKpMb5Tyfbj9sP6rpLucQ7PzHHu7n4ucmeHWEisfn6A==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0
'@nestjs/common': ^12.0.0
'@nestjs/core': ^12.0.0
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12 || ^0.2.0
typescript: ^5.5.0 || ^6.0.0
peerDependenciesMeta:
'@fastify/static':
optional: true
class-transformer:
optional: true
class-validator:
optional: true
typescript:
optional: true
'@nestjs/testing@12.1.2': '@nestjs/testing@12.1.2':
resolution: {integrity: sha512-JgetYeGHGK6iUEztXyiQLOfYLWqUgAyexLMzhuAMasgLiDgYShh/PoRESE9tzpRZo4eWKme3uSZ9DFR6Myuojw==} resolution: {integrity: sha512-JgetYeGHGK6iUEztXyiQLOfYLWqUgAyexLMzhuAMasgLiDgYShh/PoRESE9tzpRZo4eWKme3uSZ9DFR6Myuojw==}
peerDependencies: peerDependencies:
@ -1011,6 +1052,9 @@ packages:
'@rolldown/pluginutils@1.0.1': '@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@sindresorhus/is@4.6.0': '@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1160,6 +1204,9 @@ packages:
append-field@1.0.0: append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-timsort@1.0.3: array-timsort@1.0.3:
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
@ -1474,6 +1521,9 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-toolkit@1.52.0:
resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==}
esbuild@0.28.2: esbuild@0.28.2:
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1698,6 +1748,10 @@ packages:
js-tokens@10.0.0: js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-yaml@5.4.2:
resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==}
hasBin: true
json-schema-traverse@1.0.0: json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@ -2284,6 +2338,9 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
swagger-ui-dist@5.33.0:
resolution: {integrity: sha512-wpdK+m6BU5yj6pmUdMskZVTSWYG4DLglAx3sIhylloY37i8O37IrH+YEpqdXNfpaTGxILRBFzUqLF2jKqbfI7A==}
thread-stream@4.2.0: thread-stream@4.2.0:
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@ -2762,6 +2819,8 @@ snapshots:
'@lukeed/csprng@1.1.0': {} '@lukeed/csprng@1.1.0': {}
'@microsoft/tsdoc@0.17.0': {}
'@nestjs/cli@12.0.8(@types/node@24.19.1)(prettier@3.9.9)': '@nestjs/cli@12.0.8(@types/node@24.19.1)(prettier@3.9.9)':
dependencies: dependencies:
'@angular-devkit/core': 22.2.0(chokidar@5.0.0) '@angular-devkit/core': 22.2.0(chokidar@5.0.0)
@ -2818,6 +2877,14 @@ snapshots:
'@types/jsonwebtoken': 9.0.10 '@types/jsonwebtoken': 9.0.10
jsonwebtoken: 9.0.3 jsonwebtoken: 9.0.3
'@nestjs/mapped-types@12.0.0(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
reflect-metadata: 0.2.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.15.1
'@nestjs/mau@0.2.8': '@nestjs/mau@0.2.8':
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
@ -2853,6 +2920,23 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- chokidar - chokidar
'@nestjs/swagger@12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3)':
dependencies:
'@microsoft/tsdoc': 0.17.0
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types': 12.0.0(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
'@standard-schema/spec': 1.1.0
es-toolkit: 1.52.0
js-yaml: 5.4.2
path-to-regexp: 8.4.2
reflect-metadata: 0.2.2
swagger-ui-dist: 5.33.0
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.15.1
typescript: 6.0.3
'@nestjs/testing@12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(@nestjs/platform-express@12.1.2)': '@nestjs/testing@12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(@nestjs/platform-express@12.1.2)':
dependencies: dependencies:
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
@ -3028,6 +3112,8 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {} '@rolldown/pluginutils@1.0.1': {}
'@scarf/scarf@1.4.0': {}
'@sindresorhus/is@4.6.0': {} '@sindresorhus/is@4.6.0': {}
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
@ -3207,6 +3293,8 @@ snapshots:
append-field@1.0.0: {} append-field@1.0.0: {}
argparse@2.0.1: {}
array-timsort@1.0.3: {} array-timsort@1.0.3: {}
asap@2.0.6: {} asap@2.0.6: {}
@ -3479,6 +3567,8 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.4 hasown: 2.0.4
es-toolkit@1.52.0: {}
esbuild@0.28.2: esbuild@0.28.2:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.28.2 '@esbuild/aix-ppc64': 0.28.2
@ -3755,6 +3845,10 @@ snapshots:
js-tokens@10.0.0: {} js-tokens@10.0.0: {}
js-yaml@5.4.2:
dependencies:
argparse: 2.0.1
json-schema-traverse@1.0.0: {} json-schema-traverse@1.0.0: {}
json5@2.2.3: {} json5@2.2.3: {}
@ -4364,6 +4458,10 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
swagger-ui-dist@5.33.0:
dependencies:
'@scarf/scarf': 1.4.0
thread-stream@4.2.0: thread-stream@4.2.0:
dependencies: dependencies:
real-require: 1.0.0 real-require: 1.0.0

View File

@ -1,20 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto { export class LoginDto {
@ApiProperty({ description: '租户编码' })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
tenantCode!: string; tenantCode!: string;
@ApiProperty({ description: '用户名' })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
username!: string; username!: string;
@ApiProperty({ description: '密码' })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
password!: string; password!: string;
} }
export class RefreshDto { export class RefreshDto {
@ApiProperty({ description: '刷新令牌' })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
refreshToken!: string; refreshToken!: string;

View File

@ -24,6 +24,9 @@ export class JwtAuthGuard implements CanActivate {
if (isPublic) return true; if (isPublic) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const url: string = request.originalUrl ?? request.url ?? '';
if (url.startsWith('/docs')) return true;
const token = this.extractToken(request); const token = this.extractToken(request);
if (!token) throw new UnauthorizedException('未登录'); if (!token) throw new UnauthorizedException('未登录');
try { try {

View File

@ -1,13 +1,16 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsInt, IsOptional, Min } from 'class-validator'; import { IsInt, IsOptional, Min } from 'class-validator';
export class PageQuery { export class PageQuery {
@ApiPropertyOptional({ description: '页码(从 1 开始)', default: 1 })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@Min(1) @Min(1)
page = 1; page = 1;
@ApiPropertyOptional({ description: '每页条数', default: 20 })
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@ -16,6 +19,7 @@ export class PageQuery {
} }
export class IdParam { export class IdParam {
@ApiPropertyOptional({ description: '记录 ID' })
@IsOptional() @IsOptional()
id!: string; id!: string;
} }

View File

@ -1,12 +1,31 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateConfigDto { export class CreateConfigDto {
@IsString() @IsNotEmpty() key!: string; @ApiProperty({ description: '配置 key(租户内唯一)' })
@IsOptional() @IsString() value?: string; @IsString()
@IsOptional() @IsString() remark?: string; @IsNotEmpty()
key!: string;
@ApiPropertyOptional({ description: '配置值' })
@IsOptional()
@IsString()
value?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class UpdateConfigDto { export class UpdateConfigDto {
@IsOptional() @IsString() value?: string; @ApiPropertyOptional({ description: '配置值' })
@IsOptional() @IsString() remark?: string; @IsOptional()
@IsString()
value?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }

View File

@ -1,27 +1,80 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateDictTypeDto { export class CreateDictTypeDto {
@IsString() @IsNotEmpty() code!: string; @ApiProperty({ description: '字典类型编码' })
@IsString() @IsNotEmpty() name!: string; @IsString()
@IsOptional() @IsString() remark?: string; @IsNotEmpty()
code!: string;
@ApiProperty({ description: '字典类型名称' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class UpdateDictTypeDto { export class UpdateDictTypeDto {
@IsOptional() @IsString() name?: string; @ApiPropertyOptional({ description: '字典类型名称' })
@IsOptional() @IsString() status?: string; @IsOptional()
@IsOptional() @IsString() remark?: string; @IsString()
name?: string;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class CreateDictItemDto { export class CreateDictItemDto {
@IsString() @IsNotEmpty() dictTypeCode!: string; @ApiProperty({ description: '所属字典类型 code' })
@IsString() @IsNotEmpty() label!: string; @IsString()
@IsString() @IsNotEmpty() value!: string; @IsNotEmpty()
@IsOptional() @IsInt() sort?: number; dictTypeCode!: string;
@ApiProperty({ description: '标签(显示名)' })
@IsString()
@IsNotEmpty()
label!: string;
@ApiProperty({ description: '值' })
@IsString()
@IsNotEmpty()
value!: string;
@ApiPropertyOptional({ description: '排序', default: 0 })
@IsOptional()
@IsInt()
sort?: number;
} }
export class UpdateDictItemDto { export class UpdateDictItemDto {
@IsOptional() @IsString() label?: string; @ApiPropertyOptional({ description: '标签' })
@IsOptional() @IsString() value?: string; @IsOptional()
@IsOptional() @IsInt() sort?: number; @IsString()
@IsOptional() @IsString() status?: string; label?: string;
@ApiPropertyOptional({ description: '值' })
@IsOptional()
@IsString()
value?: string;
@ApiPropertyOptional({ description: '排序' })
@IsOptional()
@IsInt()
sort?: number;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
} }

View File

@ -2,6 +2,7 @@ import 'dotenv/config';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { Logger } from 'nestjs-pino'; import { Logger } from 'nestjs-pino';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module.js'; import { AppModule } from './app.module.js';
async function bootstrap(): Promise<void> { async function bootstrap(): Promise<void> {
@ -10,6 +11,15 @@ async function bootstrap(): Promise<void> {
app.enableCors(); app.enableCors();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useLogger(app.get(Logger)); app.useLogger(app.get(Logger));
const swaggerConfig = new DocumentBuilder()
.setTitle('sysmgr API')
.setDescription('通用系统管理脚手架 API 文档')
.setVersion('1.0')
.addBearerAuth()
.build();
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swaggerConfig));
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
await bootstrap(); await bootstrap();

View File

@ -1,17 +1,56 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateOrgDto { export class CreateOrgDto {
@IsString() @IsNotEmpty() code!: string; @ApiProperty({ description: '组织 code(租户内唯一)' })
@IsString() @IsNotEmpty() name!: string; @IsString()
@IsOptional() @IsString() parentCode?: string; @IsNotEmpty()
@IsOptional() @IsString() leader?: string; code!: string;
@IsOptional() @IsInt() sort?: number;
@ApiProperty({ description: '组织名称' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: '父组织 code' })
@IsOptional()
@IsString()
parentCode?: string;
@ApiPropertyOptional({ description: '负责人' })
@IsOptional()
@IsString()
leader?: string;
@ApiPropertyOptional({ description: '排序', default: 0 })
@IsOptional()
@IsInt()
sort?: number;
} }
export class UpdateOrgDto { export class UpdateOrgDto {
@IsOptional() @IsString() name?: string; @ApiPropertyOptional({ description: '组织名称' })
@IsOptional() @IsString() parentCode?: string; @IsOptional()
@IsOptional() @IsString() leader?: string; @IsString()
@IsOptional() @IsInt() sort?: number; name?: string;
@IsOptional() @IsString() status?: string;
@ApiPropertyOptional({ description: '父组织 code' })
@IsOptional()
@IsString()
parentCode?: string;
@ApiPropertyOptional({ description: '负责人' })
@IsOptional()
@IsString()
leader?: string;
@ApiPropertyOptional({ description: '排序' })
@IsOptional()
@IsInt()
sort?: number;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
} }

View File

@ -1,26 +1,101 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateResourceDto { export class CreateResourceDto {
@IsString() @IsNotEmpty() type!: string; @ApiProperty({ description: '类型:menu/button/api' })
@IsString() @IsNotEmpty() code!: string; @IsString()
@IsString() @IsNotEmpty() name!: string; @IsNotEmpty()
@IsOptional() @IsString() parentCode?: string; type!: string;
@IsOptional() @IsString() path?: string;
@IsOptional() @IsString() component?: string; @ApiProperty({ description: '资源 code(租户内唯一,兼权限标识)' })
@IsOptional() @IsString() method?: string; @IsString()
@IsOptional() @IsString() apiPath?: string; @IsNotEmpty()
@IsOptional() @IsString() icon?: string; code!: string;
@IsOptional() @IsInt() sort?: number;
@ApiProperty({ description: '名称' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: '父资源 code' })
@IsOptional()
@IsString()
parentCode?: string;
@ApiPropertyOptional({ description: '路由 path(menu)' })
@IsOptional()
@IsString()
path?: string;
@ApiPropertyOptional({ description: '前端组件(menu)' })
@IsOptional()
@IsString()
component?: string;
@ApiPropertyOptional({ description: 'HTTP method(api)' })
@IsOptional()
@IsString()
method?: string;
@ApiPropertyOptional({ description: '接口 path(api)' })
@IsOptional()
@IsString()
apiPath?: string;
@ApiPropertyOptional({ description: '图标' })
@IsOptional()
@IsString()
icon?: string;
@ApiPropertyOptional({ description: '排序', default: 0 })
@IsOptional()
@IsInt()
sort?: number;
} }
export class UpdateResourceDto { export class UpdateResourceDto {
@IsOptional() @IsString() name?: string; @ApiPropertyOptional({ description: '名称' })
@IsOptional() @IsString() parentCode?: string; @IsOptional()
@IsOptional() @IsString() path?: string; @IsString()
@IsOptional() @IsString() component?: string; name?: string;
@IsOptional() @IsString() method?: string;
@IsOptional() @IsString() apiPath?: string; @ApiPropertyOptional({ description: '父资源 code' })
@IsOptional() @IsString() icon?: string; @IsOptional()
@IsOptional() @IsInt() sort?: number; @IsString()
@IsOptional() @IsString() status?: string; parentCode?: string;
@ApiPropertyOptional({ description: '路由 path' })
@IsOptional()
@IsString()
path?: string;
@ApiPropertyOptional({ description: '前端组件' })
@IsOptional()
@IsString()
component?: string;
@ApiPropertyOptional({ description: 'HTTP method' })
@IsOptional()
@IsString()
method?: string;
@ApiPropertyOptional({ description: '接口 path' })
@IsOptional()
@IsString()
apiPath?: string;
@ApiPropertyOptional({ description: '图标' })
@IsOptional()
@IsString()
icon?: string;
@ApiPropertyOptional({ description: '排序' })
@IsOptional()
@IsInt()
sort?: number;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
} }

View File

@ -1,19 +1,53 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateRoleDto { export class CreateRoleDto {
@IsString() @IsNotEmpty() code!: string; @ApiProperty({ description: '角色编码' })
@IsString() @IsNotEmpty() name!: string; @IsString()
@IsOptional() @IsString() dataScope?: string; @IsNotEmpty()
@IsOptional() @IsString() remark?: string; code!: string;
@ApiProperty({ description: '角色名称' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: '数据权限:all/dept/dept_and_child/self/custom', default: 'all' })
@IsOptional()
@IsString()
dataScope?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class UpdateRoleDto { export class UpdateRoleDto {
@IsOptional() @IsString() name?: string; @ApiPropertyOptional({ description: '角色名称' })
@IsOptional() @IsString() dataScope?: string; @IsOptional()
@IsOptional() @IsString() status?: string; @IsString()
@IsOptional() @IsString() remark?: string; name?: string;
@ApiPropertyOptional({ description: '数据权限:all/dept/dept_and_child/self/custom' })
@IsOptional()
@IsString()
dataScope?: string;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class AssignResourcesDto { export class AssignResourcesDto {
@IsArray() @IsString({ each: true }) resourceCodes!: string[]; @ApiProperty({ description: '资源 code 列表', type: [String] })
@IsArray()
@IsString({ each: true })
resourceCodes!: string[];
} }

View File

@ -1,14 +1,41 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTenantDto { export class CreateTenantDto {
@IsString() @IsNotEmpty() code!: string; @ApiProperty({ description: '租户编码' })
@IsString() @IsNotEmpty() name!: string; @IsString()
@IsOptional() @IsString() status?: string; @IsNotEmpty()
@IsOptional() @IsString() remark?: string; code!: string;
@ApiProperty({ description: '租户名称' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: '状态:enabled/disabled', default: 'enabled' })
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }
export class UpdateTenantDto { export class UpdateTenantDto {
@IsOptional() @IsString() name?: string; @ApiPropertyOptional({ description: '租户名称' })
@IsOptional() @IsString() status?: string; @IsOptional()
@IsOptional() @IsString() remark?: string; @IsString()
name?: string;
@ApiPropertyOptional({ description: '状态:enabled/disabled' })
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: '备注' })
@IsOptional()
@IsString()
remark?: string;
} }

View File

@ -1,24 +1,78 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateUserDto { export class CreateUserDto {
@IsString() @IsNotEmpty() username!: string; @ApiProperty({ description: '用户名' })
@IsString() @IsNotEmpty() password!: string; @IsString()
@IsOptional() @IsString() nickname?: string; @IsNotEmpty()
@IsOptional() @IsString() email?: string; username!: string;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsString() avatar?: string; @ApiProperty({ description: '初始密码' })
@IsOptional() @IsString() orgCode?: string; @IsString()
@IsNotEmpty()
password!: string;
@ApiPropertyOptional({ description: '昵称' })
@IsOptional()
@IsString()
nickname?: string;
@ApiPropertyOptional({ description: '邮箱' })
@IsOptional()
@IsString()
email?: string;
@ApiPropertyOptional({ description: '手机号' })
@IsOptional()
@IsString()
phone?: string;
@ApiPropertyOptional({ description: '头像' })
@IsOptional()
@IsString()
avatar?: string;
@ApiPropertyOptional({ description: '所属部门 code' })
@IsOptional()
@IsString()
orgCode?: string;
} }
export class UpdateUserDto { export class UpdateUserDto {
@IsOptional() @IsString() nickname?: string; @ApiPropertyOptional({ description: '昵称' })
@IsOptional() @IsString() email?: string; @IsOptional()
@IsOptional() @IsString() phone?: string; @IsString()
@IsOptional() @IsString() avatar?: string; nickname?: string;
@IsOptional() @IsString() orgCode?: string;
@IsOptional() @IsString() status?: string; @ApiPropertyOptional({ description: '邮箱' })
@IsOptional()
@IsString()
email?: string;
@ApiPropertyOptional({ description: '手机号' })
@IsOptional()
@IsString()
phone?: string;
@ApiPropertyOptional({ description: '头像' })
@IsOptional()
@IsString()
avatar?: string;
@ApiPropertyOptional({ description: '所属部门 code' })
@IsOptional()
@IsString()
orgCode?: string;
@ApiPropertyOptional({ description: '状态:enabled/disabled/locked' })
@IsOptional()
@IsString()
status?: string;
} }
export class ResetPasswordDto { export class ResetPasswordDto {
@IsString() @IsNotEmpty() password!: string; @ApiProperty({ description: '新密码' })
@IsString()
@IsNotEmpty()
password!: string;
} }