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'; import * as fs from 'fs' import * as path from 'path' export interface ShareCode { id: string; code: string; fileId: string; createdAt: Date; expiresAt: Date; isUsed: boolean; fileName?: string | null; } export interface GenerateShareCodeResponse { code: string; expiresAt: Date; } 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, fileName?: string, ): Promise { try { // 检查文件是否存在 const resource = await this.resourceService.findUnique({ where: { fileId }, }); this.logger.log('完整 fileId:', fileId); // 确保与前端一致 if (!resource) { throw new NotFoundException('文件不存在'); } // 生成分享码 const code = this.generateCode(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期 //const expiresAt = new Date(Date.now() + 10 * 1000); // 24小时后过期 // 查找是否已有分享码记录 const existingShareCode = await db.shareCode.findUnique({ where: { fileId }, }); if (existingShareCode) { // 更新现有记录,但保留原有文件名 await db.shareCode.update({ where: { fileId }, data: { code, expiresAt, isUsed: false, // 只在没有现有文件名且提供了新文件名时才更新文件名 ...(fileName && !existingShareCode.fileName ? { fileName } : {}), }, }); } else { // 创建新记录 await db.shareCode.create({ data: { code, fileId, expiresAt, isUsed: false, fileName: fileName || null, }, }); } this.logger.log(`Generated share code ${code} for file ${fileId}`); return { code, expiresAt, }; } catch (error) { this.logger.error('Failed to generate share code', error); throw error; } } async validateAndUseCode(code: string): Promise { try { this.logger.log(`尝试验证分享码: ${code}`); // 查找有效的分享码 const shareCode = await db.shareCode.findFirst({ where: { code, isUsed: false, expiresAt: { gt: new Date() }, }, }); this.logger.log('查询结果:', shareCode); if (!shareCode) { this.logger.log('分享码无效或已过期'); return null; } // 标记分享码为已使用 // await db.shareCode.update({ // where: { id: shareCode.id }, // data: { isUsed: true }, // }); // 记录使用日志 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; } } // 每天清理过期的分享码 //@Cron('*/30 * * * * *') @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) async cleanupExpiredShareCodes() { try { const shareCodes = await db.shareCode.findMany({ where: { OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }], } }) 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) } }, }); 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`); } catch (error) { this.logger.error('Failed to cleanup expired share codes', error); } } 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}`); } } // 获取分享码信息 async getShareCodeInfo(code: string): Promise { 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 { try { const activeCode = await db.shareCode.findFirst({ where: { fileId, isUsed: false, expiresAt: { gt: new Date() }, }, }); 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) { this.logger.log('收到验证分享码请求,code:', code); const shareCode = await this.validateAndUseCode(code); this.logger.log('验证分享码结果:', shareCode); if (!shareCode) { throw new NotFoundException('分享码无效或已过期'); } // 获取文件信息 const resource = await this.resourceService.findUnique({ where: { fileId: shareCode.fileId }, }); this.logger.log('获取到的资源信息:', resource); const { filename } = resource.meta as any as ResourceMeta const fileUrl = resource?.url if (!resource) { throw new NotFoundException('文件不存在'); } // 直接返回正确的数据结构 const response = { fileId: shareCode.fileId, fileName: filename || 'downloaded_file', code: shareCode.code, expiresAt: shareCode.expiresAt, url: fileUrl, }; this.logger.log('返回给前端的数据:', response); // 添加日志 return response; } // 根据文件ID生成分享码 async generateShareCodeByFileId(fileId: string) { try { this.logger.log('收到生成分享码请求,fileId:', fileId); const result = await this.generateShareCode(fileId); this.logger.log('生成分享码结果:', result); return result; } catch (error) { this.logger.error('生成分享码错误:', error); return error } } }