book_manage/apps/server/src/models/share-code/share-code.service.ts

373 lines
11 KiB
TypeScript
Raw Normal View History

2025-04-02 21:59:19 +08:00
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { customAlphabet } from 'nanoid-cjs';
2025-04-09 11:40:44 +08:00
import { db, ObjectType, Prisma, Resource } from '@nice/common';
2025-04-02 21:59:19 +08:00
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';
2025-04-09 11:40:44 +08:00
import { BaseService } from '../base/base.service';
2025-04-08 09:29:14 +08:00
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()
2025-04-09 11:40:44 +08:00
export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
2025-04-02 21:59:19 +08:00
private readonly logger = new Logger(ShareCodeService.name);
// 生成8位分享码使用易读的字符
private readonly generateCode = customAlphabet(
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
8,
);
2025-04-09 11:40:44 +08:00
constructor(private readonly resourceService: ResourceService) {
super(db, ObjectType.SHARE_CODE, false);
}
2025-04-02 21:59:19 +08:00
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-09 11:40:44 +08:00
this.logger.log('完整 resource:', resource);
2025-04-02 21:59:19 +08:00
if (!resource) {
throw new NotFoundException('文件不存在');
}
2025-04-09 11:40:44 +08:00
const { filename } = resource.meta as any as ResourceMeta
2025-04-02 21:59:19 +08:00
// 生成分享码
const code = this.generateCode();
// 查找是否已有分享码记录
2025-04-09 11:40:44 +08:00
const existingShareCode = await super.findUnique({
2025-04-02 21:59:19 +08:00
where: { fileId },
});
if (existingShareCode) {
// 更新现有记录,但保留原有文件名
2025-04-09 11:40:44 +08:00
await super.update({
2025-04-02 21:59:19 +08:00
where: { fileId },
data: {
code,
expiresAt,
2025-04-08 09:29:14 +08:00
canUseTimes,
2025-04-02 21:59:19 +08:00
isUsed: false,
2025-04-09 11:40:44 +08:00
fileName: filename|| "downloaded_file",
2025-04-02 21:59:19 +08:00
},
});
} else {
// 创建新记录
2025-04-09 11:40:44 +08:00
await super.create({
2025-04-02 21:59:19 +08:00
data: {
code,
fileId,
expiresAt,
2025-04-08 09:29:14 +08:00
canUseTimes,
2025-04-02 21:59:19 +08:00
isUsed: false,
2025-04-09 11:40:44 +08:00
fileName: filename || "downloaded_file",
2025-04-02 21:59:19 +08:00
},
});
}
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
}
2025-04-09 11:40:44 +08:00
}
2025-04-02 21:59:19 +08:00
2025-04-09 11:40:44 +08:00
async getShareCodesWithResources(args: {
page?: number;
pageSize?: number;
where?: Prisma.ShareCodeWhereInput;
}): Promise<{
items: Array<ShareCode & { resource?: Resource }>;
totalPages: number;
}> {
try {
console.log('args:', args.where.OR);
// 使用include直接关联查询Resource
const { items, totalPages } = await super.findManyWithPagination({
...args,
select:{
id: true,
code: true,
fileId: true,
expiresAt: true,
fileName: true,
canUseTimes: true,
resource: {
select: {
id: true,
type: true,
url: true,
meta: true,
}
}
}
});
this.logger.log('search result:', items);
return {
items,
totalPages
};
} catch (error) {
this.logger.error('Failed to get share codes with resources', error);
throw error;
}
}
async softDeleteShareCodes(ids: string[]): Promise<any> {
try {
this.logger.log(`尝试软删除分享码IDs: ${ids.join(', ')}`);
const result = await super.softDeleteByIds(ids);
this.logger.log(`软删除分享码成功,数量: ${result.length}`);
return result;
} catch (error) {
this.logger.error('软删除分享码失败', error);
throw error;
}
}
async updateShareCode(id: string, data: Partial<ShareCode>): Promise<any> {
try {
this.logger.log(`尝试更新分享码ID: ${id},数据:`, data);
const result = await super.updateById(id, data);
this.logger.log(`更新分享码成功:`, result);
return result;
} catch (error) {
this.logger.error('更新分享码失败', error);
throw error;
}
2025-04-02 21:59:19 +08:00
}
}