2026-01-09 18:14:35 +08:00
|
|
|
|
import { Injectable, BadRequestException } from '@nestjs/common';
|
|
|
|
|
|
import { ConfigService } from '@nestjs/config';
|
2026-01-13 11:11:49 +08:00
|
|
|
|
import { OssService } from '../oss/oss.service';
|
2026-01-09 18:14:35 +08:00
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
import { randomBytes } from 'crypto';
|
|
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class UploadService {
|
|
|
|
|
|
private readonly uploadDir: string;
|
2026-01-13 11:11:49 +08:00
|
|
|
|
private readonly useOss: boolean;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-13 11:11:49 +08:00
|
|
|
|
constructor(
|
|
|
|
|
|
private configService: ConfigService,
|
|
|
|
|
|
private ossService: OssService,
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 本地上传文件存储目录(作为 COS 的备用方案)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
this.uploadDir = path.join(process.cwd(), 'uploads');
|
2026-01-13 11:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 确保本地上传目录存在
|
2026-01-09 18:14:35 +08:00
|
|
|
|
if (!fs.existsSync(this.uploadDir)) {
|
|
|
|
|
|
fs.mkdirSync(this.uploadDir, { recursive: true });
|
|
|
|
|
|
}
|
2026-01-13 11:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否使用 COS
|
|
|
|
|
|
this.useOss = this.ossService.isEnabled();
|
|
|
|
|
|
|
|
|
|
|
|
if (this.useOss) {
|
|
|
|
|
|
console.log('文件上传将使用腾讯云 COS');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('文件上传将使用本地存储,目录:', this.uploadDir);
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async uploadFile(
|
|
|
|
|
|
file: Express.Multer.File,
|
|
|
|
|
|
tenantId?: number,
|
|
|
|
|
|
userId?: number,
|
|
|
|
|
|
): Promise<{ url: string; fileName: string; size: number }> {
|
|
|
|
|
|
if (!file) {
|
|
|
|
|
|
throw new BadRequestException('文件不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 11:11:49 +08:00
|
|
|
|
// 优先使用 COS
|
|
|
|
|
|
if (this.useOss) {
|
|
|
|
|
|
return this.uploadToOss(file, tenantId, userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 备用方案:本地存储
|
|
|
|
|
|
return this.uploadToLocal(file, tenantId, userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 上传到腾讯云 COS
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async uploadToOss(
|
|
|
|
|
|
file: Express.Multer.File,
|
|
|
|
|
|
tenantId?: number,
|
|
|
|
|
|
userId?: number,
|
|
|
|
|
|
): Promise<{ url: string; fileName: string; size: number }> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await this.ossService.uploadFile(
|
|
|
|
|
|
file.buffer,
|
|
|
|
|
|
file.originalname,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
url: result.url,
|
|
|
|
|
|
fileName: result.fileName,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('COS 上传失败,尝试本地存储:', error.message);
|
|
|
|
|
|
// COS 失败时回退到本地存储
|
|
|
|
|
|
return this.uploadToLocal(file, tenantId, userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 上传到本地存储
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async uploadToLocal(
|
|
|
|
|
|
file: Express.Multer.File,
|
|
|
|
|
|
tenantId?: number,
|
|
|
|
|
|
userId?: number,
|
|
|
|
|
|
): Promise<{ url: string; fileName: string; size: number }> {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 生成唯一文件名
|
|
|
|
|
|
const fileExt = path.extname(file.originalname);
|
|
|
|
|
|
const uniqueId = randomBytes(16).toString('hex');
|
|
|
|
|
|
const fileName = `${uniqueId}${fileExt}`;
|
2026-01-13 11:11:49 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 根据租户ID和用户ID创建目录结构:uploads/tenantId/userId/
|
|
|
|
|
|
let targetDir = this.uploadDir;
|
|
|
|
|
|
if (tenantId) {
|
|
|
|
|
|
targetDir = path.join(targetDir, `tenant_${tenantId}`);
|
|
|
|
|
|
if (!fs.existsSync(targetDir)) {
|
|
|
|
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
|
|
|
|
}
|
2026-01-13 11:11:49 +08:00
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
if (userId) {
|
|
|
|
|
|
targetDir = path.join(targetDir, `user_${userId}`);
|
|
|
|
|
|
if (!fs.existsSync(targetDir)) {
|
|
|
|
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 文件保存路径
|
|
|
|
|
|
const filePath = path.join(targetDir, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存文件
|
|
|
|
|
|
fs.writeFileSync(filePath, file.buffer);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成访问URL
|
|
|
|
|
|
// 格式:/api/uploads/tenantId/userId/fileName 或 /api/uploads/fileName
|
|
|
|
|
|
let urlPath = '/api/uploads';
|
|
|
|
|
|
if (tenantId) {
|
|
|
|
|
|
urlPath += `/tenant_${tenantId}`;
|
|
|
|
|
|
if (userId) {
|
|
|
|
|
|
urlPath += `/user_${userId}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
urlPath += `/${fileName}`;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
url: urlPath,
|
|
|
|
|
|
fileName: file.originalname,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|