feat: 添加日志和邮件模块
2
.env
|
|
@ -36,7 +36,7 @@ DB_MYSQL_DATABASE = test1
|
||||||
# 上传和静态文件配置
|
# 上传和静态文件配置
|
||||||
# ========================================================================================
|
# ========================================================================================
|
||||||
# 上传文件目录
|
# 上传文件目录
|
||||||
UPLOAD_DIR = ./content/uploads
|
UPLOAD_DIR = ./content/upload
|
||||||
# 静态文件目录
|
# 静态文件目录
|
||||||
STATIC_DIR = ./content/html
|
STATIC_DIR = ./content/html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ lerna-debug.log*
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.local
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
/coverage
|
/coverage
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
10
package.json
|
|
@ -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": [
|
||||||
|
|
|
||||||
4545
pnpm-lock.yaml
|
|
@ -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 为静态页面/上传文件提供服务
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
@Controller('cache')
|
||||||
|
export class CacheController {
|
||||||
|
constructor(private readonly cacheService: CacheService) {}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './cache.module';
|
||||||
|
export * from './cache.service';
|
||||||
|
export * from './cache.controller';
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './mail.controller';
|
||||||
|
export * from './mail.module';
|
||||||
|
export * from './mail.service';
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
|
||||||
|
@Controller('mail')
|
||||||
|
export class MailController {
|
||||||
|
constructor(private readonly mailService: MailService) {}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export enum ResponseCode {
|
||||||
/**
|
/**
|
||||||
* 参数错误
|
* 参数错误
|
||||||
*/
|
*/
|
||||||
PARAM_ERROR = 4001,
|
PARAM_ERROR = 4005,
|
||||||
/**
|
/**
|
||||||
* 服务端未知错误
|
* 服务端未知错误
|
||||||
*/
|
*/
|
||||||
|
|
@ -21,5 +21,5 @@ export enum ResponseCode {
|
||||||
/**
|
/**
|
||||||
* 未授权
|
* 未授权
|
||||||
*/
|
*/
|
||||||
UNAUTHORIZED = 4003,
|
UNAUTHORIZED = 4001,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 error;
|
return new ValidationError(messages);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||