feat: 添加日志和邮件模块
2
.env
|
|
@ -36,7 +36,7 @@ DB_MYSQL_DATABASE = test1
|
|||
# 上传和静态文件配置
|
||||
# ========================================================================================
|
||||
# 上传文件目录
|
||||
UPLOAD_DIR = ./content/uploads
|
||||
UPLOAD_DIR = ./content/upload
|
||||
# 静态文件目录
|
||||
STATIC_DIR = ./content/html
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ lerna-debug.log*
|
|||
|
||||
# OS
|
||||
.DS_Store
|
||||
*.local
|
||||
|
||||
# Tests
|
||||
/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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
"@nestjs/devtools-integration": "^0.1.4",
|
||||
|
|
@ -54,10 +55,13 @@
|
|||
"@types/mockjs": "^1.0.7",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^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-validator": "^0.14.0",
|
||||
"dayjs": "^1.11.7",
|
||||
|
|
@ -72,10 +76,12 @@
|
|||
"multer": "1.4.5-lts.1",
|
||||
"mysql2": "^3.2.0",
|
||||
"nanoid": "^4.0.1",
|
||||
"nodemailer": "^6.9.4",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"redis": "^4.6.7",
|
||||
"source-map-support": "^0.5.20",
|
||||
"sqlite3": "^5.1.6",
|
||||
"supertest": "^6.1.3",
|
||||
|
|
@ -87,7 +93,9 @@
|
|||
"typeorm-naming-strategies": "^4.1.0",
|
||||
"typescript": "^4.3.5",
|
||||
"uuid": "^9.0.0",
|
||||
"webpack": "5"
|
||||
"webpack": "5",
|
||||
"winston": "^3.10.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
|
|
|||
4545
pnpm-lock.yaml
|
|
@ -12,6 +12,7 @@ import { AuthModule } from '@/modules/auth';
|
|||
import { UserModule } from '@/modules/user';
|
||||
import { ResponseModule } from '@/common/response';
|
||||
import { SerializationModule } from '@/common/serialization';
|
||||
import { CacheModule } from './common/cache';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -25,6 +26,11 @@ import { SerializationModule } from '@/common/serialization';
|
|||
* @description 用于记录日志
|
||||
*/
|
||||
LoggerModule,
|
||||
/**
|
||||
* 缓存模块
|
||||
* @description 用于缓存数据
|
||||
*/
|
||||
CacheModule,
|
||||
/**
|
||||
* 静态资源(全局)
|
||||
* @description 为静态页面/上传文件提供服务
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ export class BaseService {
|
|||
/**
|
||||
* 格式化分页参数
|
||||
*/
|
||||
formatPagination(page = this.config.defaultPage, size = this.config.defaultPageSize) {
|
||||
if (size == 0) {
|
||||
formatPagination(page = this.config.defaultPage, size = this.config.defaultPageSize, supportFull = false) {
|
||||
if (size == 0 && supportFull) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
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 { ConfigService } from '@/config';
|
||||
import { Logger, createLogger, format, transports } from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
|
||||
@Injectable()
|
||||
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 {
|
||||
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 { Response as _Response } from 'express';
|
||||
import { Request, Response as _Response } from 'express';
|
||||
import { Response } from './response';
|
||||
import { ResponseCode } from './response.code';
|
||||
import { LoggerService } from '../logger';
|
||||
|
||||
@Catch()
|
||||
export class AllExecptionFilter implements ExceptionFilter {
|
||||
constructor(private logger: LoggerService) {}
|
||||
|
||||
catch(exception: Error, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const response = ctx.getResponse<_Response>();
|
||||
const message = exception.message;
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ import { IsNumber, IsOptional, Min } from 'class-validator';
|
|||
|
||||
/**
|
||||
* 分页 DTO
|
||||
* @example { page: 1, size: 10 }
|
||||
* @example
|
||||
* ```
|
||||
* {
|
||||
* page: 1,
|
||||
* size: 10
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
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);
|
||||
return Response.success({
|
||||
data: list,
|
||||
meta: {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
},
|
||||
meta: { page, size, total },
|
||||
});
|
||||
}
|
||||
return Response.success({ data: list, total });
|
||||
|
|
|
|||
|
|
@ -10,26 +10,14 @@ import { ResponseInterceptor } from './response.interceptor';
|
|||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
/**
|
||||
* 全局异常过滤器
|
||||
* @description 将异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: AllExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局HTTP异常过滤器
|
||||
* @description 将HTTP异常统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExecptionFilter,
|
||||
},
|
||||
/**
|
||||
* 全局响应拦截器
|
||||
* @description 将返回值统一包装成{code, message, data, meta}格式
|
||||
*/
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const initSwagger = (app: INestApplication) => {
|
|||
.addTag('permission', '权限管理')
|
||||
.addTag('post', '文章管理')
|
||||
.addTag('upload', '文件上传')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
const options: SwaggerDocumentOptions = {
|
||||
operationIdFactory(controllerKey, methodKey) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
export class AppValidationError extends Error {
|
||||
messages: string[] = [];
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
|
||||
setMessages(errors: string[]) {
|
||||
this.messages = errors;
|
||||
export class ValidationError extends Error {
|
||||
constructor(public messages: string[]) {
|
||||
super('参数错误');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { ResponseCode } from '../response';
|
||||
import { AppValidationError } from './validation.error';
|
||||
import { ValidationError } from './validation.error';
|
||||
|
||||
@Catch(AppValidationError)
|
||||
@Catch(ValidationError)
|
||||
export class ValidationExecptionFilter implements ExceptionFilter {
|
||||
catch(exception: AppValidationError, host: ArgumentsHost) {
|
||||
catch(exception: ValidationError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const code = ResponseCode.PARAM_ERROR;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppValidationError } from './validation.error';
|
||||
import { ValidationError } from './validation.error';
|
||||
|
||||
const MessageMap = {
|
||||
const map = {
|
||||
isString: '必须为字符串',
|
||||
isNumber: '必须为数字',
|
||||
isBoolean: '必须为布尔值',
|
||||
|
|
@ -27,16 +27,14 @@ export const validationPipeFactory = () => {
|
|||
transform: true,
|
||||
whitelist: true,
|
||||
exceptionFactory: (errors) => {
|
||||
const error = new AppValidationError('参数错误');
|
||||
const messages: string[] = [];
|
||||
for (const error of errors) {
|
||||
const { property, constraints } = error;
|
||||
Object.keys(constraints).forEach((key) => {
|
||||
messages.push(MessageMap[key] ? `参数(${property})${MessageMap[key]}` : constraints[key]);
|
||||
});
|
||||
for (const [key, val] of Object.entries(constraints)) {
|
||||
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 {
|
||||
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 {
|
||||
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) {
|
||||
const secret = this.config.jwtSecret;
|
||||
const user = await this.jwtService.verifyAsync(token, { secret });
|
||||
request['user'] = user;
|
||||
request.user = user;
|
||||
}
|
||||
const metadata = [context.getClass(), context.getHandler()];
|
||||
const isPublic = this.reflector.getAllAndOverride(PUBLICK_KEY, metadata);
|
||||
if (isPublic === undefined || isPublic) {
|
||||
const targets = [context.getClass(), context.getHandler()];
|
||||
const isPublic = this.reflector.getAllAndOverride(PUBLICK_KEY, targets);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
if (isPublic !== false && request.method.toLowerCase() === 'GET') {
|
||||
if (isPublic === undefined && request.method.toUpperCase() === 'GET') {
|
||||
return true;
|
||||
}
|
||||
if (!token) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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 { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateUploadDto } from './dto/create-upload.dto';
|
||||
import { UploadService } from './upload.service';
|
||||
import { Request } from 'express';
|
||||
|
||||
@ApiTags('upload')
|
||||
@Controller('upload')
|
||||
|
|
@ -14,8 +15,9 @@ export class UploadController {
|
|||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ description: '要上传的文件', type: CreateUploadDto })
|
||||
@ApiOperation({ description: '上传文件', operationId: 'upload' })
|
||||
create(@UploadedFile() file: Express.Multer.File) {
|
||||
@ApiOperation({ description: '上传文件', operationId: 'addFile' })
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -27,19 +29,19 @@ export class UploadController {
|
|||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: '查询', operationId: 'getUpload' })
|
||||
@ApiOperation({ description: '查询', operationId: 'getFile' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.uploadService.findOne(+id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ description: '更新', operationId: 'updateUpload' })
|
||||
@ApiOperation({ description: '更新', operationId: 'updateFile' })
|
||||
update() {
|
||||
return this.uploadService.update();
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: '删除', operationId: 'delUpload' })
|
||||
@ApiOperation({ description: '删除', operationId: 'delFile' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.uploadService.remove(+id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,28 @@ import { diskStorage } from 'multer';
|
|||
import { Upload } from './entities/upload.entity';
|
||||
import { UploadController } from './upload.controller';
|
||||
import { UploadService } from './upload.service';
|
||||
import { extname, join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Upload]),
|
||||
MulterModule.registerAsync({
|
||||
useFactory: async (config: ConfigService) => {
|
||||
useFactory: (config: ConfigService) => {
|
||||
return {
|
||||
storage: diskStorage({
|
||||
destination: config.uploadDir,
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, file.originalname);
|
||||
destination: (req, file, next) => {
|
||||
const date = new Date();
|
||||
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 { InjectRepository } from '@nestjs/typeorm';
|
||||
import { parse } from 'path';
|
||||
import { extname, posix, sep, relative } from 'path';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Upload } from './entities/upload.entity';
|
||||
import { BaseService } from '@/common/base';
|
||||
|
||||
@Injectable()
|
||||
export class UploadService {
|
||||
constructor(@InjectRepository(Upload) private readonly uploadRepository: Repository<Upload>) {}
|
||||
export class UploadService extends BaseService {
|
||||
constructor(@InjectRepository(Upload) private readonly uploadRepository: Repository<Upload>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件信息
|
||||
* @param file 文件信息
|
||||
* @returns
|
||||
*/
|
||||
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({
|
||||
name: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
hash: file.filename,
|
||||
path: file.path,
|
||||
extension: parse(file.originalname).ext,
|
||||
path: uploadUrl,
|
||||
extension: extname(file.originalname),
|
||||
});
|
||||
await this.uploadRepository.save(upload);
|
||||
return upload.id;
|
||||
|
|
@ -26,7 +37,7 @@ export class UploadService {
|
|||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} upload`;
|
||||
return this.uploadRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
update() {
|
||||
|
|
@ -34,6 +45,6 @@ export class UploadService {
|
|||
}
|
||||
|
||||
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 { Post } from '@/modules/post';
|
||||
import { Role } from '@/modules/role';
|
||||
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
|
||||
import { Column, Entity, JoinTable, ManyToMany, RelationId } from 'typeorm';
|
||||
|
||||
@Entity({ orderBy: { id: 'DESC' } })
|
||||
export class User extends BaseEntity {
|
||||
|
|
@ -65,4 +65,7 @@ export class User extends BaseEntity {
|
|||
@ManyToMany(() => Role, (role) => role.user)
|
||||
@JoinTable()
|
||||
roles: Role[];
|
||||
|
||||
@RelationId('roles')
|
||||
roleIds: number[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export class UserController extends BaseController {
|
|||
|
||||
@Get(':id')
|
||||
@Version('2')
|
||||
@ApiOperation({ description: '查询用户', operationId: 'getUserv2' })
|
||||
@ApiOperation({ deprecated: true, description: '查询用户', operationId: 'getUserv2' })
|
||||
findOne(@Param('id') id: number) {
|
||||
return this.userService.findOne(+id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export class UserService extends BaseService {
|
|||
async findMany(findUserdto: FindUserDto) {
|
||||
const { nickname: _nickname, page, size } = findUserdto;
|
||||
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 } });
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ export class UserService extends BaseService {
|
|||
* 根据id查找用户
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import 'express';
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: 'development' | 'production' | 'test';
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'express' {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: number;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||