feat: 添加日志和邮件模块

master
luoer 2023-08-03 17:25:34 +08:00
parent 2ec5aac04c
commit 5e18102d61
41 changed files with 588 additions and 4333 deletions

2
.env
View File

@ -36,7 +36,7 @@ DB_MYSQL_DATABASE = test1
# 上传和静态文件配置 # 上传和静态文件配置
# ======================================================================================== # ========================================================================================
# 上传文件目录 # 上传文件目录
UPLOAD_DIR = ./content/uploads UPLOAD_DIR = ./content/upload
# 静态文件目录 # 静态文件目录
STATIC_DIR = ./content/html STATIC_DIR = ./content/html

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ lerna-debug.log*
# OS # OS
.DS_Store .DS_Store
*.local
# Tests # Tests
/coverage /coverage

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -37,6 +37,7 @@
"rxjs": "^7.2.0" "rxjs": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/cli": "^9.0.0", "@nestjs/cli": "^9.0.0",
"@nestjs/config": "^2.3.1", "@nestjs/config": "^2.3.1",
"@nestjs/devtools-integration": "^0.1.4", "@nestjs/devtools-integration": "^0.1.4",
@ -54,10 +55,13 @@
"@types/mockjs": "^1.0.7", "@types/mockjs": "^1.0.7",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/nodemailer": "^6.4.9",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"cache-manager": "^5.2.3",
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
@ -72,10 +76,12 @@
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"mysql2": "^3.2.0", "mysql2": "^3.2.0",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"nodemailer": "^6.9.4",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"redis": "^4.6.7",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.6",
"supertest": "^6.1.3", "supertest": "^6.1.3",
@ -87,7 +93,9 @@
"typeorm-naming-strategies": "^4.1.0", "typeorm-naming-strategies": "^4.1.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"webpack": "5" "webpack": "5",
"winston": "^3.10.0",
"winston-daily-rotate-file": "^4.7.1"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import { AuthModule } from '@/modules/auth';
import { UserModule } from '@/modules/user'; import { UserModule } from '@/modules/user';
import { ResponseModule } from '@/common/response'; import { ResponseModule } from '@/common/response';
import { SerializationModule } from '@/common/serialization'; import { SerializationModule } from '@/common/serialization';
import { CacheModule } from './common/cache';
@Module({ @Module({
imports: [ imports: [
@ -25,6 +26,11 @@ import { SerializationModule } from '@/common/serialization';
* @description * @description
*/ */
LoggerModule, LoggerModule,
/**
*
* @description
*/
CacheModule,
/** /**
* () * ()
* @description / * @description /

View File

@ -14,13 +14,13 @@ export class BaseService {
/** /**
* *
*/ */
formatPagination(page = this.config.defaultPage, size = this.config.defaultPageSize) { formatPagination(page = this.config.defaultPage, size = this.config.defaultPageSize, supportFull = false) {
if (size == 0) { if (size == 0 && supportFull) {
return {}; return {};
} }
return { return {
skip: (page - 1) * size, skip: (page - 1) * size,
take: size, take: size == 0 ? this.config.defaultPageSize : size,
}; };
} }
} }

7
src/common/cache/cache.controller.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { CacheService } from './cache.service';
@Controller('cache')
export class CacheController {
constructor(private readonly cacheService: CacheService) {}
}

30
src/common/cache/cache.module.ts vendored Normal file
View File

@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { CacheController } from './cache.controller';
import { CacheModule as _CacheModule } from '@nestjs/cache-manager';
import { ConfigService } from '@/config';
import { redisStore } from 'cache-manager-redis-store';
@Module({
imports: [
_CacheModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const { host, port } = config.redis;
return {
// store: () =>
// redisStore({
// commandsQueueMaxLength: 1000,
// socket: { host, port },
// }) as any,
db: 0,
ttl: 600,
};
},
}),
],
controllers: [CacheController],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}

16
src/common/cache/cache.service.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
get(key: string) {
return this.cache.get(key);
}
set(key: string, value: any, ttl?: number) {
return this.cache.set(key, value, ttl);
}
}

3
src/common/cache/index.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export * from './cache.module';
export * from './cache.service';
export * from './cache.controller';

View File

@ -1,9 +1,58 @@
import { ConsoleLogger, Injectable } from '@nestjs/common'; import { Injectable, ConsoleLogger } from '@nestjs/common';
import { dayjs } from '@/libs'; import { dayjs } from '@/libs';
import { ConfigService } from '@/config';
import { Logger, createLogger, format, transports } from 'winston';
import 'winston-daily-rotate-file';
@Injectable() @Injectable()
export class LoggerService extends ConsoleLogger { export class LoggerService extends ConsoleLogger {
/**
* Winston
*/
protected logger: Logger;
/**
*
*/
constructor(private config: ConfigService) {
super();
const { combine, timestamp, printf } = format;
const printLine = printf(({ level, message, timestamp, context }: any) => {
return `[Nest] ${process.pid} ${timestamp} ${level} [${context || this.context}]: ${message}`;
});
this.logger = createLogger({
format: combine(timestamp({ format: dayjs.DATETIME }), printLine),
transports: [
new transports.DailyRotateFile({
level: 'info',
dirname: this.config.logDir,
filename: '%DATE%.info.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: combine(timestamp({ format: dayjs.DATETIME }), printLine),
}),
],
});
}
/**
*
*/
protected getTimestamp(): string { protected getTimestamp(): string {
return dayjs().format(); return dayjs().format();
} }
/**
* Info
*/
log(message: unknown, context?: unknown, ...rest: unknown[]) {
super.log(message, context);
this.logger.info({
message,
context: context || this.context,
...rest,
});
}
} }

3
src/common/mail/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './mail.controller';
export * from './mail.module';
export * from './mail.service';

View File

@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { MailService } from './mail.service';
@Controller('mail')
export class MailController {
constructor(private readonly mailService: MailService) {}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.service';
import { MailController } from './mail.controller';
@Module({
controllers: [MailController],
providers: [MailService],
})
export class MailModule {}

View File

@ -0,0 +1,38 @@
import { ConfigService } from '@/config';
import { Injectable } from '@nestjs/common';
import nodemailer from 'nodemailer';
@Injectable()
export class MailService {
/**
* NodeMailer
*/
protected mailer: nodemailer.Transporter;
/**
*
*/
constructor(private config: ConfigService) {
const { host, port, user, pass } = this.config.smtp;
this.mailer = nodemailer.createTransport({
host,
port,
auth: { user, pass },
});
}
/**
*
* @param to
* @param subject
* @param html
*/
sendMail(to: string, subject: string, html: string) {
return this.mailer.sendMail({
from: `${this.config.title} <${this.config.smtp.user}>`,
to,
subject,
html,
});
}
}

View File

@ -1,16 +1,20 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import { Response as _Response } from 'express'; import { Request, Response as _Response } from 'express';
import { Response } from './response'; import { Response } from './response';
import { ResponseCode } from './response.code'; import { ResponseCode } from './response.code';
import { LoggerService } from '../logger';
@Catch() @Catch()
export class AllExecptionFilter implements ExceptionFilter { export class AllExecptionFilter implements ExceptionFilter {
constructor(private logger: LoggerService) {}
catch(exception: Error, host: ArgumentsHost) { catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp(); const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<_Response>(); const response = ctx.getResponse<_Response>();
const message = exception.message; const message = exception.message;
const code = ResponseCode.UNKNOWN_ERROR; const code = ResponseCode.UNKNOWN_ERROR;
console.log(exception); this.logger.error(exception, `${request.method} ${request.url}`);
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json(Response.create({ code, message, data: null })); response.status(HttpStatus.INTERNAL_SERVER_ERROR).json(Response.create({ code, message, data: null }));
} }
} }

View File

@ -3,7 +3,13 @@ import { IsNumber, IsOptional, Min } from 'class-validator';
/** /**
* DTO * DTO
* @example { page: 1, size: 10 } * @example
* ```
* {
* page: 1,
* size: 10
* }
* ```
*/ */
export class PaginationDto { export class PaginationDto {
/** /**

View File

@ -13,7 +13,7 @@ export enum ResponseCode {
/** /**
* *
*/ */
PARAM_ERROR = 4001, PARAM_ERROR = 4005,
/** /**
* *
*/ */
@ -21,5 +21,5 @@ export enum ResponseCode {
/** /**
* *
*/ */
UNAUTHORIZED = 4003, UNAUTHORIZED = 4001,
} }

View File

@ -27,11 +27,7 @@ export class ResponseInterceptor implements NestInterceptor {
const size = Number(request.query.size || this.config.defaultPageSize); const size = Number(request.query.size || this.config.defaultPageSize);
return Response.success({ return Response.success({
data: list, data: list,
meta: { meta: { page, size, total },
page,
size,
total,
},
}); });
} }
return Response.success({ data: list, total }); return Response.success({ data: list, total });

View File

@ -10,26 +10,14 @@ import { ResponseInterceptor } from './response.interceptor';
*/ */
@Module({ @Module({
providers: [ providers: [
/**
*
* @description {code, message, data, meta}
*/
{ {
provide: APP_FILTER, provide: APP_FILTER,
useClass: AllExecptionFilter, useClass: AllExecptionFilter,
}, },
/**
* HTTP
* @description HTTP{code, message, data, meta}
*/
{ {
provide: APP_FILTER, provide: APP_FILTER,
useClass: HttpExecptionFilter, useClass: HttpExecptionFilter,
}, },
/**
*
* @description {code, message, data, meta}
*/
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor, useClass: ResponseInterceptor,

View File

@ -20,6 +20,7 @@ export const initSwagger = (app: INestApplication) => {
.addTag('permission', '权限管理') .addTag('permission', '权限管理')
.addTag('post', '文章管理') .addTag('post', '文章管理')
.addTag('upload', '文件上传') .addTag('upload', '文件上传')
.addBearerAuth()
.build(); .build();
const options: SwaggerDocumentOptions = { const options: SwaggerDocumentOptions = {
operationIdFactory(controllerKey, methodKey) { operationIdFactory(controllerKey, methodKey) {

View File

@ -1,12 +1,5 @@
export class AppValidationError extends Error { export class ValidationError extends Error {
messages: string[] = []; constructor(public messages: string[]) {
super('参数错误');
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
setMessages(errors: string[]) {
this.messages = errors;
} }
} }

View File

@ -1,11 +1,11 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { ResponseCode } from '../response'; import { ResponseCode } from '../response';
import { AppValidationError } from './validation.error'; import { ValidationError } from './validation.error';
@Catch(AppValidationError) @Catch(ValidationError)
export class ValidationExecptionFilter implements ExceptionFilter { export class ValidationExecptionFilter implements ExceptionFilter {
catch(exception: AppValidationError, host: ArgumentsHost) { catch(exception: ValidationError, host: ArgumentsHost) {
const ctx = host.switchToHttp(); const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>(); const response = ctx.getResponse<Response>();
const code = ResponseCode.PARAM_ERROR; const code = ResponseCode.PARAM_ERROR;

View File

@ -1,7 +1,7 @@
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { AppValidationError } from './validation.error'; import { ValidationError } from './validation.error';
const MessageMap = { const map = {
isString: '必须为字符串', isString: '必须为字符串',
isNumber: '必须为数字', isNumber: '必须为数字',
isBoolean: '必须为布尔值', isBoolean: '必须为布尔值',
@ -27,16 +27,14 @@ export const validationPipeFactory = () => {
transform: true, transform: true,
whitelist: true, whitelist: true,
exceptionFactory: (errors) => { exceptionFactory: (errors) => {
const error = new AppValidationError('参数错误');
const messages: string[] = []; const messages: string[] = [];
for (const error of errors) { for (const error of errors) {
const { property, constraints } = error; const { property, constraints } = error;
Object.keys(constraints).forEach((key) => { for (const [key, val] of Object.entries(constraints)) {
messages.push(MessageMap[key] ? `参数(${property})${MessageMap[key]}` : constraints[key]); messages.push(map[key] ? `参数(${property})${map[key]}` : val);
}); }
} }
error.setMessages(messages); return new ValidationError(messages);
return error;
}, },
}); });
}; };

View File

@ -100,10 +100,10 @@ export class ConfigService {
/** /**
* *
* @default './content/uploads' * @default './content/upload'
*/ */
get uploadDir(): string { get uploadDir(): string {
return this.config.get('UPLOAD_DIR', './content/uploads'); return this.config.get('UPLOAD_DIR', './content/upload');
} }
/** /**
@ -153,4 +153,25 @@ export class ConfigService {
get logDir(): string { get logDir(): string {
return this.config.get('LOG_DIR', './content/logs'); return this.config.get('LOG_DIR', './content/logs');
} }
/**
* SMTP
*/
get smtp() {
const host: string = this.config.get('SMTP_HOST');
const port = Number(this.config.get('SMTP_PORT'));
const user: string = this.config.get('SMTP_USER');
const pass: string = this.config.get('SMTP_PASS');
return { host, port, user, pass };
}
/**
* Redis
*/
get redis() {
const host: string = this.config.get('REDIS_HOST', 'localhost');
const port = Number(this.config.get('REDIS_PORT', 6379));
const pass: string = this.config.get('REDIS_PASS', '');
return { host, port, pass };
}
} }

View File

@ -15,14 +15,14 @@ export class JwtGuard implements CanActivate {
if (token) { if (token) {
const secret = this.config.jwtSecret; const secret = this.config.jwtSecret;
const user = await this.jwtService.verifyAsync(token, { secret }); const user = await this.jwtService.verifyAsync(token, { secret });
request['user'] = user; request.user = user;
} }
const metadata = [context.getClass(), context.getHandler()]; const targets = [context.getClass(), context.getHandler()];
const isPublic = this.reflector.getAllAndOverride(PUBLICK_KEY, metadata); const isPublic = this.reflector.getAllAndOverride(PUBLICK_KEY, targets);
if (isPublic === undefined || isPublic) { if (isPublic) {
return true; return true;
} }
if (isPublic !== false && request.method.toLowerCase() === 'GET') { if (isPublic === undefined && request.method.toUpperCase() === 'GET') {
return true; return true;
} }
if (!token) { if (!token) {

View File

@ -1,9 +1,10 @@
import { Respond } from '@/common/response'; import { Respond } from '@/common/response';
import { Controller, Delete, Get, Param, Patch, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Controller, Delete, Get, Ip, Param, Patch, Post, Req, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateUploadDto } from './dto/create-upload.dto'; import { CreateUploadDto } from './dto/create-upload.dto';
import { UploadService } from './upload.service'; import { UploadService } from './upload.service';
import { Request } from 'express';
@ApiTags('upload') @ApiTags('upload')
@Controller('upload') @Controller('upload')
@ -14,8 +15,9 @@ export class UploadController {
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ description: '要上传的文件', type: CreateUploadDto }) @ApiBody({ description: '要上传的文件', type: CreateUploadDto })
@ApiOperation({ description: '上传文件', operationId: 'upload' }) @ApiOperation({ description: '上传文件', operationId: 'addFile' })
create(@UploadedFile() file: Express.Multer.File) { create(@UploadedFile() file: Express.Multer.File, @Req() req: Request, @Ip() ip: string) {
console.log(`ip: ${ip}, req: ${JSON.stringify(req.user)}`);
return this.uploadService.create(file); return this.uploadService.create(file);
} }
@ -27,19 +29,19 @@ export class UploadController {
} }
@Get(':id') @Get(':id')
@ApiOperation({ description: '查询', operationId: 'getUpload' }) @ApiOperation({ description: '查询', operationId: 'getFile' })
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.uploadService.findOne(+id); return this.uploadService.findOne(+id);
} }
@Patch(':id') @Patch(':id')
@ApiOperation({ description: '更新', operationId: 'updateUpload' }) @ApiOperation({ description: '更新', operationId: 'updateFile' })
update() { update() {
return this.uploadService.update(); return this.uploadService.update();
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ description: '删除', operationId: 'delUpload' }) @ApiOperation({ description: '删除', operationId: 'delFile' })
remove(@Param('id') id: string) { remove(@Param('id') id: string) {
return this.uploadService.remove(+id); return this.uploadService.remove(+id);
} }

View File

@ -6,17 +6,28 @@ import { diskStorage } from 'multer';
import { Upload } from './entities/upload.entity'; import { Upload } from './entities/upload.entity';
import { UploadController } from './upload.controller'; import { UploadController } from './upload.controller';
import { UploadService } from './upload.service'; import { UploadService } from './upload.service';
import { extname, join } from 'path';
import { existsSync, mkdirSync } from 'fs';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Upload]), TypeOrmModule.forFeature([Upload]),
MulterModule.registerAsync({ MulterModule.registerAsync({
useFactory: async (config: ConfigService) => { useFactory: (config: ConfigService) => {
return { return {
storage: diskStorage({ storage: diskStorage({
destination: config.uploadDir, destination: (req, file, next) => {
filename: (req, file, cb) => { const date = new Date();
cb(null, file.originalname); const year = date.getFullYear();
const month = date.getMonth() + 1;
const dest = join(config.uploadDir, year.toString(), month.toString().padStart(2, '0'));
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
next(null, dest);
},
filename: (req, file, next) => {
next(null, Date.now() + extname(file.originalname));
}, },
}), }),
}; };

View File

@ -1,21 +1,32 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { parse } from 'path'; import { extname, posix, sep, relative } from 'path';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Upload } from './entities/upload.entity'; import { Upload } from './entities/upload.entity';
import { BaseService } from '@/common/base';
@Injectable() @Injectable()
export class UploadService { export class UploadService extends BaseService {
constructor(@InjectRepository(Upload) private readonly uploadRepository: Repository<Upload>) {} constructor(@InjectRepository(Upload) private readonly uploadRepository: Repository<Upload>) {
super();
}
/**
*
* @param file
* @returns
*/
async create(file: Express.Multer.File) { async create(file: Express.Multer.File) {
const path = relative(this.config.uploadDir, file.path).split(sep).join(posix.sep);
const uploadPrefix = this.config.uploadPrefix;
const uploadUrl = `${uploadPrefix}/${path}`;
const upload = this.uploadRepository.create({ const upload = this.uploadRepository.create({
name: file.originalname, name: file.originalname,
mimetype: file.mimetype, mimetype: file.mimetype,
size: file.size, size: file.size,
hash: file.filename, hash: file.filename,
path: file.path, path: uploadUrl,
extension: parse(file.originalname).ext, extension: extname(file.originalname),
}); });
await this.uploadRepository.save(upload); await this.uploadRepository.save(upload);
return upload.id; return upload.id;
@ -26,7 +37,7 @@ export class UploadService {
} }
findOne(id: number) { findOne(id: number) {
return `This action returns a #${id} upload`; return this.uploadRepository.findOne({ where: { id } });
} }
update() { update() {
@ -34,6 +45,6 @@ export class UploadService {
} }
remove(id: number) { remove(id: number) {
return `This action removes a #${id} upload`; return this.uploadRepository.softDelete(id);
} }
} }

View File

@ -3,7 +3,7 @@ import { Exclude } from 'class-transformer';
import { BaseEntity } from '@/database'; import { BaseEntity } from '@/database';
import { Post } from '@/modules/post'; import { Post } from '@/modules/post';
import { Role } from '@/modules/role'; import { Role } from '@/modules/role';
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm'; import { Column, Entity, JoinTable, ManyToMany, RelationId } from 'typeorm';
@Entity({ orderBy: { id: 'DESC' } }) @Entity({ orderBy: { id: 'DESC' } })
export class User extends BaseEntity { export class User extends BaseEntity {
@ -65,4 +65,7 @@ export class User extends BaseEntity {
@ManyToMany(() => Role, (role) => role.user) @ManyToMany(() => Role, (role) => role.user)
@JoinTable() @JoinTable()
roles: Role[]; roles: Role[];
@RelationId('roles')
roleIds: number[];
} }

View File

@ -31,7 +31,7 @@ export class UserController extends BaseController {
@Get(':id') @Get(':id')
@Version('2') @Version('2')
@ApiOperation({ description: '查询用户', operationId: 'getUserv2' }) @ApiOperation({ deprecated: true, description: '查询用户', operationId: 'getUserv2' })
findOne(@Param('id') id: number) { findOne(@Param('id') id: number) {
return this.userService.findOne(+id); return this.userService.findOne(+id);
} }

View File

@ -31,7 +31,7 @@ export class UserService extends BaseService {
async findMany(findUserdto: FindUserDto) { async findMany(findUserdto: FindUserDto) {
const { nickname: _nickname, page, size } = findUserdto; const { nickname: _nickname, page, size } = findUserdto;
const nickname = _nickname && Like(`%${_nickname}%`); const nickname = _nickname && Like(`%${_nickname}%`);
const { skip, take } = this.formatPagination(page, size); const { skip, take } = this.formatPagination(page, size, true);
return this.userRepository.findAndCount({ skip, take, where: { nickname } }); return this.userRepository.findAndCount({ skip, take, where: { nickname } });
} }
@ -39,7 +39,7 @@ export class UserService extends BaseService {
* id * id
*/ */
findOne(idOrOptions: number | Partial<User>) { findOne(idOrOptions: number | Partial<User>) {
const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : idOrOptions; const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any);
return this.userRepository.findOne({ where }); return this.userRepository.findOne({ where });
} }

11
src/types/env.d.ts vendored
View File

@ -1,5 +1,16 @@
import 'express';
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test'; NODE_ENV: 'development' | 'production' | 'test';
} }
} }
declare module 'express' {
interface Request {
user?: {
id: number;
username: string;
};
}
}