dev
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"ioredis": "^5.4.2",
|
||||
"multer": "^2.1.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"nestjs-rate-limiter": "^3.1.0",
|
||||
"passport": "^0.7.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^2.1.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ReviewModule } from './modules/review/review.module';
|
||||
import { FinanceModule } from './modules/finance/finance.module';
|
||||
import { ActivityModule } from './modules/activity/activity.module';
|
||||
import { PlatformConfigModule } from './modules/config/config.module';
|
||||
import { UploadModule } from './modules/upload/upload.module';
|
||||
import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module';
|
||||
|
||||
@Module({
|
||||
@@ -55,6 +56,7 @@ import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module
|
||||
OrderModule,
|
||||
AdminAuthModule,
|
||||
PlatformConfigModule,
|
||||
UploadModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -6,13 +6,15 @@ if (!globalThis.crypto) {
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
@@ -21,6 +23,8 @@ async function bootstrap() {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
app.useStaticAssets(join(__dirname, '..', 'uploads'), { prefix: '/uploads/' });
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Controller, Get, Put, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ConfigService } from './config.service';
|
||||
import { JwtAuthGuard, RolesGuard, Roles } from '@/common';
|
||||
import { UploadService } from '../upload/upload.service';
|
||||
|
||||
@ApiTags('管理端-系统配置')
|
||||
@ApiBearerAuth()
|
||||
@@ -9,7 +10,10 @@ import { JwtAuthGuard, RolesGuard, Roles } from '@/common';
|
||||
@Roles('admin')
|
||||
@Controller('admin/config')
|
||||
export class ConfigController {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly uploadService: UploadService,
|
||||
) {}
|
||||
|
||||
@Get('service-fee')
|
||||
@ApiOperation({ summary: '获取服务费配置' })
|
||||
@@ -23,4 +27,17 @@ export class ConfigController {
|
||||
await this.configService.updateServiceFeeRate(body.rate);
|
||||
return this.configService.getServiceFeeConfig();
|
||||
}
|
||||
|
||||
@Get('storage')
|
||||
@ApiOperation({ summary: '获取存储配置' })
|
||||
async getStorageConfig() {
|
||||
return this.uploadService.getStorageConfig();
|
||||
}
|
||||
|
||||
@Put('storage')
|
||||
@ApiOperation({ summary: '更新存储配置' })
|
||||
async updateStorageConfig(@Body() body: Record<string, string>) {
|
||||
await this.uploadService.updateStorageConfig(body);
|
||||
return this.uploadService.getStorageConfig();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PlatformConfig } from '@/entities/platform-config.entity';
|
||||
import { ConfigService } from './config.service';
|
||||
import { ConfigController } from './config.controller';
|
||||
import { UploadModule } from '../upload/upload.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PlatformConfig])],
|
||||
imports: [TypeOrmModule.forFeature([PlatformConfig]), forwardRef(() => UploadModule)],
|
||||
controllers: [ConfigController],
|
||||
providers: [ConfigService],
|
||||
exports: [ConfigService],
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiConsumes, ApiBody } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
|
||||
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
|
||||
import { RolesGuard, Roles } from '@/common';
|
||||
import { UploadService } from './upload.service';
|
||||
|
||||
const uploadOptions = {
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (_req: any, file: Express.Multer.File, cb: any) => {
|
||||
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif|webp|bmp)$/)) {
|
||||
cb(new BadRequestException('只支持图片文件'), false);
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
};
|
||||
|
||||
@ApiTags('文件上传')
|
||||
@Controller()
|
||||
export class UploadController {
|
||||
constructor(private readonly uploadService: UploadService) {}
|
||||
|
||||
@Post('upload')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '用户上传图片' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } })
|
||||
@UseInterceptors(FileInterceptor('file', uploadOptions))
|
||||
async uploadUser(@UploadedFile() file: Express.Multer.File) {
|
||||
if (!file) throw new BadRequestException('请选择文件');
|
||||
return this.uploadService.upload(file);
|
||||
}
|
||||
|
||||
@Post('seller/upload')
|
||||
@UseGuards(SellerJwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '商家上传图片' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } })
|
||||
@UseInterceptors(FileInterceptor('file', uploadOptions))
|
||||
async uploadSeller(@UploadedFile() file: Express.Multer.File) {
|
||||
if (!file) throw new BadRequestException('请选择文件');
|
||||
return this.uploadService.upload(file);
|
||||
}
|
||||
|
||||
@Post('admin/upload')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '管理员上传图片' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } })
|
||||
@UseInterceptors(FileInterceptor('file', uploadOptions))
|
||||
async uploadAdmin(@UploadedFile() file: Express.Multer.File) {
|
||||
if (!file) throw new BadRequestException('请选择文件');
|
||||
return this.uploadService.upload(file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PlatformConfig } from '@/entities/platform-config.entity';
|
||||
import { UploadService } from './upload.service';
|
||||
import { UploadController } from './upload.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PlatformConfig])],
|
||||
controllers: [UploadController],
|
||||
providers: [UploadService],
|
||||
exports: [UploadService],
|
||||
})
|
||||
export class UploadModule {}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService as NestConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlatformConfig } from '@/entities/platform-config.entity';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export type StorageProvider = 'local' | 'tencent_cos' | 'aliyun_oss';
|
||||
|
||||
export interface UploadResult {
|
||||
url: string;
|
||||
filename: string;
|
||||
originalname: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UploadService {
|
||||
private readonly logger = new Logger(UploadService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PlatformConfig)
|
||||
private configRepo: Repository<PlatformConfig>,
|
||||
private nestConfigService: NestConfigService,
|
||||
) {}
|
||||
|
||||
async getStorageProvider(): Promise<StorageProvider> {
|
||||
const config = await this.configRepo.findOne({ where: { configKey: 'storage_provider' } });
|
||||
return (config?.configValue as StorageProvider) || 'local';
|
||||
}
|
||||
|
||||
async upload(file: Express.Multer.File): Promise<UploadResult> {
|
||||
const provider = await this.getStorageProvider();
|
||||
this.logger.log(`上传文件: ${file.originalname}, 存储方式: ${provider}`);
|
||||
|
||||
switch (provider) {
|
||||
case 'tencent_cos':
|
||||
return this.uploadTencentCos(file);
|
||||
case 'aliyun_oss':
|
||||
return this.uploadAliyunOss(file);
|
||||
default:
|
||||
return this.uploadLocal(file);
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadLocal(file: Express.Multer.File): Promise<UploadResult> {
|
||||
let uploadDir = './uploads';
|
||||
const dirConfig = await this.configRepo.findOne({ where: { configKey: 'storage_local_path' } });
|
||||
if (dirConfig?.configValue) uploadDir = dirConfig.configValue;
|
||||
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const ext = path.extname(file.originalname) || '.jpg';
|
||||
const filename = `${uuidv4()}${ext}`;
|
||||
const filepath = path.join(uploadDir, filename);
|
||||
|
||||
fs.writeFileSync(filepath, file.buffer);
|
||||
|
||||
return {
|
||||
url: `/uploads/${filename}`,
|
||||
filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
}
|
||||
|
||||
private async uploadTencentCos(file: Express.Multer.File): Promise<UploadResult> {
|
||||
try {
|
||||
// 动态导入,避免未安装时报错
|
||||
const COS = (await import('cos-nodejs-sdk-v5')).default;
|
||||
const configs = await this.getStorageConfigs('storage_cos_');
|
||||
const cos = new COS({
|
||||
SecretId: configs['secret_id'],
|
||||
SecretKey: configs['secret_key'],
|
||||
});
|
||||
|
||||
const ext = path.extname(file.originalname) || '.jpg';
|
||||
const filename = `${uuidv4()}${ext}`;
|
||||
const key = `uploads/${filename}`;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
cos.putObject(
|
||||
{
|
||||
Bucket: configs['bucket'],
|
||||
Region: configs['region'],
|
||||
Key: key,
|
||||
Body: file.buffer,
|
||||
},
|
||||
(err) => (err ? reject(err) : resolve()),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
url: `https://${configs['bucket']}.cos.${configs['region']}.myqcloud.com/${key}`,
|
||||
filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
} catch (err: any) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error('腾讯云COS SDK未安装,请运行: pnpm add cos-nodejs-sdk-v5');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadAliyunOss(file: Express.Multer.File): Promise<UploadResult> {
|
||||
try {
|
||||
const OSS = (await import('ali-oss')).default;
|
||||
const configs = await this.getStorageConfigs('storage_oss_');
|
||||
|
||||
const client = new OSS({
|
||||
region: configs['region'],
|
||||
accessKeyId: configs['access_key_id'],
|
||||
accessKeySecret: configs['access_key_secret'],
|
||||
bucket: configs['bucket'],
|
||||
});
|
||||
|
||||
const ext = path.extname(file.originalname) || '.jpg';
|
||||
const filename = `${uuidv4()}${ext}`;
|
||||
const key = `uploads/${filename}`;
|
||||
|
||||
await client.put(key, file.buffer);
|
||||
|
||||
return {
|
||||
url: `https://${configs['bucket']}.${configs['region']}.aliyuncs.com/${key}`,
|
||||
filename,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
} catch (err: any) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error('阿里云OSS SDK未安装,请运行: pnpm add ali-oss');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getStorageConfigs(prefix: string): Promise<Record<string, string>> {
|
||||
const configs = await this.configRepo.find();
|
||||
const result: Record<string, string> = {};
|
||||
for (const c of configs) {
|
||||
if (c.configKey.startsWith(prefix)) {
|
||||
const key = c.configKey.replace(prefix, '');
|
||||
result[key] = c.configValue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getStorageConfig(): Promise<Record<string, string>> {
|
||||
const configs = await this.configRepo.find();
|
||||
const result: Record<string, string> = {};
|
||||
for (const c of configs) {
|
||||
if (c.configKey.startsWith('storage_')) {
|
||||
result[c.configKey] = c.configValue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateStorageConfig(configs: Record<string, string>): Promise<void> {
|
||||
for (const [key, value] of Object.entries(configs)) {
|
||||
if (!key.startsWith('storage_')) continue;
|
||||
let config = await this.configRepo.findOne({ where: { configKey: key } });
|
||||
if (config) {
|
||||
config.configValue = value;
|
||||
} else {
|
||||
config = this.configRepo.create({ configKey: key, configValue: value });
|
||||
}
|
||||
await this.configRepo.save(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
declare module 'cos-nodejs-sdk-v5' {
|
||||
interface CosOptions {
|
||||
SecretId: string;
|
||||
SecretKey: string;
|
||||
}
|
||||
interface PutObjectParams {
|
||||
Bucket: string;
|
||||
Region: string;
|
||||
Key: string;
|
||||
Body: any;
|
||||
}
|
||||
class COS {
|
||||
constructor(options: CosOptions);
|
||||
putObject(params: PutObjectParams, callback: (err: any, data: any) => void): void;
|
||||
}
|
||||
export default COS;
|
||||
}
|
||||
|
||||
declare module 'ali-oss' {
|
||||
interface OssOptions {
|
||||
region: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
bucket: string;
|
||||
}
|
||||
class OSS {
|
||||
constructor(options: OssOptions);
|
||||
put(name: string, data: any): Promise<{ name: string; url: string }>;
|
||||
}
|
||||
export default OSS;
|
||||
}
|
||||
Reference in New Issue
Block a user