2025-04-02 21:59:19 +08:00
|
|
|
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
|
|
|
import { customAlphabet } from 'nanoid-cjs';
|
|
|
|
|
import { db } from '@nice/common';
|
|
|
|
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
|
|
|
import { ResourceService } from '@server/models/resource/resource.service';
|
2025-04-03 13:27:58 +08:00
|
|
|
|
import * as fs from 'fs'
|
|
|
|
|
import * as path from 'path'
|
2025-04-08 09:29:14 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import utc from 'dayjs/plugin/utc';
|
|
|
|
|
import timezone from 'dayjs/plugin/timezone';
|
|
|
|
|
dayjs.extend(utc);
|
|
|
|
|
dayjs.extend(timezone);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
export interface ShareCode {
|
|
|
|
|
id: string;
|
|
|
|
|
code: string;
|
|
|
|
|
fileId: string;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
expiresAt: Date;
|
|
|
|
|
isUsed: boolean;
|
|
|
|
|
fileName?: string | null;
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes: number | null;
|
2025-04-02 21:59:19 +08:00
|
|
|
|
}
|
|
|
|
|
export interface GenerateShareCodeResponse {
|
|
|
|
|
code: string;
|
|
|
|
|
expiresAt: Date;
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes: number;
|
2025-04-02 21:59:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ResourceMeta {
|
|
|
|
|
filename: string;
|
|
|
|
|
filetype: string;
|
|
|
|
|
filesize: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ShareCodeService {
|
|
|
|
|
private readonly logger = new Logger(ShareCodeService.name);
|
|
|
|
|
// 生成8位分享码,使用易读的字符
|
|
|
|
|
private readonly generateCode = customAlphabet(
|
|
|
|
|
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
|
|
|
8,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
constructor(private readonly resourceService: ResourceService) { }
|
|
|
|
|
|
|
|
|
|
async generateShareCode(
|
|
|
|
|
fileId: string,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
expiresAt: Date,
|
|
|
|
|
canUseTimes: number,
|
2025-04-02 21:59:19 +08:00
|
|
|
|
fileName?: string,
|
|
|
|
|
): Promise<GenerateShareCodeResponse> {
|
|
|
|
|
try {
|
|
|
|
|
// 检查文件是否存在
|
|
|
|
|
const resource = await this.resourceService.findUnique({
|
|
|
|
|
where: { fileId },
|
|
|
|
|
});
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('完整 fileId:', fileId); // 确保与前端一致
|
2025-04-02 21:59:19 +08:00
|
|
|
|
|
|
|
|
|
if (!resource) {
|
|
|
|
|
throw new NotFoundException('文件不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成分享码
|
|
|
|
|
const code = this.generateCode();
|
|
|
|
|
// 查找是否已有分享码记录
|
|
|
|
|
const existingShareCode = await db.shareCode.findUnique({
|
|
|
|
|
where: { fileId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existingShareCode) {
|
|
|
|
|
// 更新现有记录,但保留原有文件名
|
|
|
|
|
await db.shareCode.update({
|
|
|
|
|
where: { fileId },
|
|
|
|
|
data: {
|
|
|
|
|
code,
|
|
|
|
|
expiresAt,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes,
|
2025-04-02 21:59:19 +08:00
|
|
|
|
isUsed: false,
|
|
|
|
|
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
|
|
|
|
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 创建新记录
|
|
|
|
|
await db.shareCode.create({
|
|
|
|
|
data: {
|
|
|
|
|
code,
|
|
|
|
|
fileId,
|
|
|
|
|
expiresAt,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes,
|
2025-04-02 21:59:19 +08:00
|
|
|
|
isUsed: false,
|
|
|
|
|
fileName: fileName || null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-04-08 09:29:14 +08:00
|
|
|
|
this.logger.log(`Generated share code ${code} for file ${fileId} canUseTimes: ${canUseTimes}`);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
return {
|
|
|
|
|
code,
|
|
|
|
|
expiresAt,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes,
|
2025-04-02 21:59:19 +08:00
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to generate share code', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async validateAndUseCode(code: string): Promise<ShareCode | null> {
|
|
|
|
|
try {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log(`尝试验证分享码: ${code}`);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
|
|
|
|
|
// 查找有效的分享码
|
|
|
|
|
const shareCode = await db.shareCode.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
code,
|
|
|
|
|
isUsed: false,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
2025-04-02 21:59:19 +08:00
|
|
|
|
},
|
|
|
|
|
});
|
2025-04-08 09:29:14 +08:00
|
|
|
|
if (shareCode.canUseTimes <= 0) {
|
|
|
|
|
this.logger.log('分享码已使用次数超过限制');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
//更新已使用次数
|
|
|
|
|
await db.shareCode.update({
|
|
|
|
|
where: { id: shareCode.id },
|
|
|
|
|
data: { canUseTimes: shareCode.canUseTimes - 1 },
|
|
|
|
|
});
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('查询结果:', shareCode);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
|
|
|
|
|
if (!shareCode) {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('分享码无效或已过期');
|
2025-04-02 21:59:19 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
// 记录使用日志
|
|
|
|
|
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
|
|
|
|
|
// 返回完整的分享码信息,包括文件名
|
|
|
|
|
return shareCode;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to validate share code', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 每天清理过期的分享码
|
2025-04-03 13:27:58 +08:00
|
|
|
|
//@Cron('*/30 * * * * *')
|
2025-04-02 21:59:19 +08:00
|
|
|
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
|
|
|
async cleanupExpiredShareCodes() {
|
|
|
|
|
try {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
const shareCodes = await db.shareCode.findMany({
|
2025-04-02 21:59:19 +08:00
|
|
|
|
where: {
|
2025-04-08 09:29:14 +08:00
|
|
|
|
OR: [
|
|
|
|
|
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
|
|
|
|
|
{ isUsed: true },
|
|
|
|
|
{ canUseTimes: { lte: 0 } }
|
|
|
|
|
],
|
2025-04-03 13:27:58 +08:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
this.logger.log('需要清理的分享码:', shareCodes);
|
|
|
|
|
shareCodes.forEach(code => {
|
|
|
|
|
this.cleanupUploadFolder(code.fileId);
|
|
|
|
|
})
|
|
|
|
|
const result = await db.shareCode.deleteMany({
|
|
|
|
|
where: {
|
|
|
|
|
fileId: {
|
|
|
|
|
in: shareCodes.map(code => code.fileId)
|
|
|
|
|
}
|
2025-04-02 21:59:19 +08:00
|
|
|
|
},
|
|
|
|
|
});
|
2025-04-03 13:27:58 +08:00
|
|
|
|
const deleteResource = await this.resourceService.deleteMany({
|
|
|
|
|
where: {
|
|
|
|
|
fileId: {
|
|
|
|
|
in: shareCodes.map(code => code.fileId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
this.logger.log(`Cleaned up ${result.count} ${deleteResource.count} expired share codes`);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to cleanup expired share codes', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-03 13:27:58 +08:00
|
|
|
|
async cleanupUploadFolder(file?: string) {
|
|
|
|
|
//const uploadDir = path.join(__dirname, '../../../uploads');
|
|
|
|
|
const uploadDir = path.join('/data/uploads/', file || '');
|
|
|
|
|
this.logger.log('uploadDir:', uploadDir);
|
|
|
|
|
try {
|
|
|
|
|
if (!fs.existsSync(uploadDir)) {
|
|
|
|
|
this.logger.warn(`Upload directory does not exist: ${uploadDir}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 递归删除文件夹及其内容
|
|
|
|
|
this.deleteFolderRecursive(uploadDir);
|
|
|
|
|
this.logger.log(`Cleaned up upload folder: ${uploadDir}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('读取上传目录失败:', error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private deleteFolderRecursive(dirPath: string) {
|
|
|
|
|
if (fs.existsSync(dirPath)) {
|
|
|
|
|
fs.readdirSync(dirPath).forEach((file) => {
|
|
|
|
|
const filePath = path.join(dirPath, file);
|
|
|
|
|
if (fs.statSync(filePath).isDirectory()) {
|
|
|
|
|
// 递归删除子目录
|
|
|
|
|
this.deleteFolderRecursive(filePath);
|
|
|
|
|
} else {
|
|
|
|
|
// 删除文件
|
|
|
|
|
fs.unlinkSync(filePath);
|
|
|
|
|
this.logger.log(`Deleted file: ${filePath}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 删除空文件夹
|
|
|
|
|
fs.rmdirSync(dirPath);
|
|
|
|
|
this.logger.log(`Deleted folder: ${dirPath}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-02 21:59:19 +08:00
|
|
|
|
// 获取分享码信息
|
|
|
|
|
async getShareCodeInfo(code: string): Promise<ShareCode | null> {
|
|
|
|
|
try {
|
|
|
|
|
return await db.shareCode.findFirst({
|
|
|
|
|
where: { code },
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to get share code info', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查文件是否已经生成过分享码
|
|
|
|
|
async hasActiveShareCode(fileId: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const activeCode = await db.shareCode.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
fileId,
|
|
|
|
|
isUsed: false,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
2025-04-02 21:59:19 +08:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return !!activeCode;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to check active share code', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件的所有分享记录
|
|
|
|
|
async getFileShareHistory(fileId: string) {
|
|
|
|
|
try {
|
|
|
|
|
return await db.shareCode.findMany({
|
|
|
|
|
where: { fileId },
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('Failed to get file share history', error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据分享码获取文件
|
|
|
|
|
async getFileByShareCode(code: string) {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('收到验证分享码请求,code:', code);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
const shareCode = await this.validateAndUseCode(code);
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('验证分享码结果:', shareCode);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
|
|
|
|
|
if (!shareCode) {
|
|
|
|
|
throw new NotFoundException('分享码无效或已过期');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件信息
|
|
|
|
|
const resource = await this.resourceService.findUnique({
|
|
|
|
|
where: { fileId: shareCode.fileId },
|
|
|
|
|
});
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('获取到的资源信息:', resource);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
const { filename } = resource.meta as any as ResourceMeta
|
2025-04-03 13:27:58 +08:00
|
|
|
|
const fileUrl = resource?.url
|
2025-04-02 21:59:19 +08:00
|
|
|
|
if (!resource) {
|
|
|
|
|
throw new NotFoundException('文件不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 直接返回正确的数据结构
|
|
|
|
|
const response = {
|
|
|
|
|
fileId: shareCode.fileId,
|
|
|
|
|
fileName: filename || 'downloaded_file',
|
|
|
|
|
code: shareCode.code,
|
2025-04-03 13:27:58 +08:00
|
|
|
|
expiresAt: shareCode.expiresAt,
|
|
|
|
|
url: fileUrl,
|
2025-04-08 09:29:14 +08:00
|
|
|
|
canUseTimes: shareCode.canUseTimes - 1,
|
2025-04-02 21:59:19 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('返回给前端的数据:', response); // 添加日志
|
2025-04-02 21:59:19 +08:00
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据文件ID生成分享码
|
2025-04-08 09:29:14 +08:00
|
|
|
|
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number) {
|
2025-04-02 21:59:19 +08:00
|
|
|
|
try {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('收到生成分享码请求,fileId:', fileId);
|
2025-04-08 09:29:14 +08:00
|
|
|
|
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes);
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.log('生成分享码结果:', result);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
2025-04-03 13:27:58 +08:00
|
|
|
|
this.logger.error('生成分享码错误:', error);
|
2025-04-02 21:59:19 +08:00
|
|
|
|
return error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|