add
This commit is contained in:
parent
48c22c9348
commit
bed7a4e690
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable ,Logger} from '@nestjs/common';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserProfile,
|
UserProfile,
|
||||||
db,
|
db,
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
|
private readonly logger = new Logger(ResourceService.name);
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db, ObjectType.RESOURCE);
|
super(db, ObjectType.RESOURCE);
|
||||||
}
|
}
|
||||||
|
@ -33,4 +35,53 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 添加保存文件名的方法
|
||||||
|
async saveFileName(fileId: string, fileName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`尝试保存文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
|
||||||
|
// 首先检查是否已存在 ShareCode 记录
|
||||||
|
const existingShareCode = await db.shareCode.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingShareCode) {
|
||||||
|
// 如果记录存在,更新文件名
|
||||||
|
await db.shareCode.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: { fileName },
|
||||||
|
});
|
||||||
|
this.logger.log(`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
} else {
|
||||||
|
// 如果记录不存在,创建新记录
|
||||||
|
await db.shareCode.create({
|
||||||
|
data: {
|
||||||
|
fileId,
|
||||||
|
fileName,
|
||||||
|
code: null, // 这里可以设置为 null 或生成一个临时码
|
||||||
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24小时后过期
|
||||||
|
isUsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`保存文件名失败,文件ID: ${fileId}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加获取文件名的方法
|
||||||
|
async getFileName(fileId: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const shareCode = await db.shareCode.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
select: { fileName: true },
|
||||||
|
});
|
||||||
|
return shareCode?.fileName || null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get filename for ${fileId}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class ShareCodeService {
|
||||||
|
|
||||||
constructor(private readonly resourceService: ResourceService) {}
|
constructor(private readonly resourceService: ResourceService) {}
|
||||||
|
|
||||||
async generateShareCode(fileId: string): Promise<GenerateShareCodeResponse> {
|
async generateShareCode(fileId: string, fileName?: string): Promise<GenerateShareCodeResponse> {
|
||||||
try {
|
try {
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
const resource = await this.resourceService.findUnique({
|
const resource = await this.resourceService.findUnique({
|
||||||
|
@ -32,15 +32,36 @@ export class ShareCodeService {
|
||||||
const code = this.generateCode();
|
const code = this.generateCode();
|
||||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
|
||||||
|
|
||||||
// 创建分享码记录
|
// 查找是否已有分享码记录
|
||||||
await db.shareCode.create({
|
const existingShareCode = await db.shareCode.findUnique({
|
||||||
data: {
|
where: { fileId },
|
||||||
code,
|
|
||||||
fileId,
|
|
||||||
expiresAt,
|
|
||||||
isUsed: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingShareCode) {
|
||||||
|
// 更新现有记录
|
||||||
|
await db.shareCode.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: {
|
||||||
|
fileName: fileName || null,
|
||||||
|
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}`);
|
this.logger.log(`Generated share code ${code} for file ${fileId}`);
|
||||||
|
|
||||||
|
@ -56,6 +77,8 @@ export class ShareCodeService {
|
||||||
|
|
||||||
async validateAndUseCode(code: string): Promise<string | null> {
|
async validateAndUseCode(code: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
console.log(`尝试验证分享码: ${code}`);
|
||||||
|
|
||||||
// 查找有效的分享码
|
// 查找有效的分享码
|
||||||
const shareCode = await db.shareCode.findFirst({
|
const shareCode = await db.shareCode.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -65,7 +88,10 @@ export class ShareCodeService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('查询结果:', shareCode);
|
||||||
|
|
||||||
if (!shareCode) {
|
if (!shareCode) {
|
||||||
|
console.log('分享码无效或已过期');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface ShareCode {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
isUsed: boolean;
|
isUsed: boolean;
|
||||||
|
fileName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateShareCodeResponse {
|
export interface GenerateShareCodeResponse {
|
||||||
|
|
|
@ -13,15 +13,18 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Body,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
import { ShareCodeService } from './share-code.service';
|
import { ShareCodeService } from './share-code.service';
|
||||||
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tusService: TusService,
|
private readonly tusService: TusService,
|
||||||
private readonly shareCodeService: ShareCodeService,
|
private readonly shareCodeService: ShareCodeService,
|
||||||
|
private readonly resourceService: ResourceService,
|
||||||
) {}
|
) {}
|
||||||
// @Post()
|
// @Post()
|
||||||
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
|
@ -42,6 +45,28 @@ export class UploadController {
|
||||||
async handlePost(@Req() req: Request, @Res() res: Response) {
|
async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
}
|
}
|
||||||
|
@Get('share/:code')
|
||||||
|
async validateShareCode(@Param('code') code: string) {
|
||||||
|
const fileId = await this.shareCodeService.validateAndUseCode(code);
|
||||||
|
if (!fileId) {
|
||||||
|
throw new NotFoundException('分享码无效或已过期');
|
||||||
|
}
|
||||||
|
return { fileId };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('share/info/:code')
|
||||||
|
async getShareCodeInfo(@Param('code') code: string) {
|
||||||
|
const info = await this.shareCodeService.getShareCodeInfo(code);
|
||||||
|
if (!info) {
|
||||||
|
throw new NotFoundException('分享码不存在');
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('share/history/:fileId')
|
||||||
|
async getFileShareHistory(@Param('fileId') fileId: string) {
|
||||||
|
return this.shareCodeService.getFileShareHistory(fileId);
|
||||||
|
}
|
||||||
@Get('/*')
|
@Get('/*')
|
||||||
async handleGet(@Req() req: Request, @Res() res: Response) {
|
async handleGet(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
|
@ -76,26 +101,61 @@ export class UploadController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('share/:code')
|
@Post('filename')
|
||||||
async validateShareCode(@Param('code') code: string) {
|
async saveFileName(@Body() data: { fileId: string; fileName: string }) {
|
||||||
const fileId = await this.shareCodeService.validateAndUseCode(code);
|
try {
|
||||||
if (!fileId) {
|
console.log('收到保存文件名请求:', data);
|
||||||
throw new NotFoundException('分享码无效或已过期');
|
|
||||||
|
// 检查参数
|
||||||
|
if (!data.fileId || !data.fileName) {
|
||||||
|
throw new HttpException(
|
||||||
|
{ message: '缺少必要参数' },
|
||||||
|
HttpStatus.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 保存文件名
|
||||||
|
await this.resourceService.saveFileName(data.fileId, data.fileName);
|
||||||
|
console.log('文件名保存成功:', data.fileName, '对应文件ID:', data.fileId);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件名失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
message: '保存文件名失败',
|
||||||
|
error: (error instanceof Error) ? error.message : String(error)
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return { fileId };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('share/info/:code')
|
@Get('download/:fileId')
|
||||||
async getShareCodeInfo(@Param('code') code: string) {
|
async downloadFile(@Param('fileId') fileId: string, @Res() res: Response) {
|
||||||
const info = await this.shareCodeService.getShareCodeInfo(code);
|
try {
|
||||||
if (!info) {
|
// 获取文件信息
|
||||||
throw new NotFoundException('分享码不存在');
|
const resource = await this.resourceService.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始文件名
|
||||||
|
const fileName = await this.resourceService.getFileName(fileId) || 'downloaded-file';
|
||||||
|
|
||||||
|
// 设置响应头,包含原始文件名
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(fileName)}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 其他下载逻辑...
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 错误处理...
|
||||||
}
|
}
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('share/history/:fileId')
|
|
||||||
async getFileShareHistory(@Param('fileId') fileId: string) {
|
|
||||||
return this.shareCodeService.getFileShareHistory(fileId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,6 @@
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"hls.js": "^1.5.18",
|
"hls.js": "^1.5.18",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"mind-elixir": "workspace:^",
|
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"quill": "2.0.3",
|
"quill": "2.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|
|
@ -9,6 +9,7 @@ const { TabPane } = Tabs;
|
||||||
|
|
||||||
export default function DeptSettingPage() {
|
export default function DeptSettingPage() {
|
||||||
const [uploadedFileId, setUploadedFileId] = useState<string>('');
|
const [uploadedFileId, setUploadedFileId] = useState<string>('');
|
||||||
|
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 使用您的 useTusUpload hook
|
// 使用您的 useTusUpload hook
|
||||||
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
|
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
|
||||||
|
@ -24,10 +25,47 @@ export default function DeptSettingPage() {
|
||||||
// 处理文件上传
|
// 处理文件上传
|
||||||
const handleFileSelect = async (file: File) => {
|
const handleFileSelect = async (file: File) => {
|
||||||
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
||||||
|
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
file,
|
file,
|
||||||
(result) => {
|
async (result) => {
|
||||||
setUploadedFileId(result.fileId);
|
setUploadedFileId(result.fileId);
|
||||||
|
|
||||||
|
// 在前端保存文件名映射(用于当前会话)
|
||||||
|
setFileNameMap(prev => ({
|
||||||
|
...prev,
|
||||||
|
[result.fileId]: file.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 上传成功后保存原始文件名到数据库
|
||||||
|
try {
|
||||||
|
console.log('正在保存文件名到数据库:', file.name, '对应文件ID:', result.fileId);
|
||||||
|
|
||||||
|
const response = await fetch('/api/upload/filename', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileId: result.fileId,
|
||||||
|
fileName: file.name
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log('保存文件名响应:', response.status, responseText);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('保存文件名失败:', responseText);
|
||||||
|
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
|
||||||
|
} else {
|
||||||
|
console.log('文件名保存成功:', file.name);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件名请求失败:', error);
|
||||||
|
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
|
||||||
|
}
|
||||||
|
|
||||||
message.success('文件上传成功');
|
message.success('文件上传成功');
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -54,20 +92,38 @@ export default function DeptSettingPage() {
|
||||||
|
|
||||||
// 获取文件名
|
// 获取文件名
|
||||||
const contentDisposition = response.headers.get('content-disposition');
|
const contentDisposition = response.headers.get('content-disposition');
|
||||||
let filename = 'downloaded-file';
|
console.log('Content-Disposition 头:', contentDisposition);
|
||||||
|
|
||||||
|
let filename = fileNameMap[fileId] || 'downloaded-file'; // 优先使用本地缓存的文件名
|
||||||
|
|
||||||
if (contentDisposition) {
|
if (contentDisposition) {
|
||||||
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
|
// 改进文件名提取逻辑
|
||||||
if (matches != null && matches[1]) {
|
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
|
||||||
|
const matches = filenameRegex.exec(contentDisposition);
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
// 移除引号并解码 URL 编码的文件名
|
||||||
filename = matches[1].replace(/['"]/g, '');
|
filename = matches[1].replace(/['"]/g, '');
|
||||||
|
try {
|
||||||
|
// 尝试解码 URL 编码的文件名
|
||||||
|
filename = decodeURIComponent(filename);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('文件名解码失败,使用原始文件名');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('提取的文件名:', filename);
|
||||||
|
|
||||||
// 创建下载链接
|
// 创建下载链接
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
|
|
||||||
|
// 使用获取到的文件名或本地缓存的文件名
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
|
@ -21,13 +21,13 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
console.log('开始生成分享码,fileId:', fileId);
|
console.log('开始生成分享码,fileId:', fileId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/upload/share/${fileId}`, {
|
const response = await fetch(`http://localhost:3000/upload/share/${fileId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
// headers: {
|
||||||
'Content-Type': 'application/json',
|
// 'Content-Type': 'application/json',
|
||||||
},
|
// },
|
||||||
body: JSON.stringify({
|
// body: JSON.stringify({
|
||||||
})
|
// })
|
||||||
});
|
});
|
||||||
console.log('Current fileId:', fileId); // 确保 fileId 有效
|
console.log('Current fileId:', fileId); // 确保 fileId 有效
|
||||||
console.log('请求URL:', `/upload/share/${fileId}`);
|
console.log('请求URL:', `/upload/share/${fileId}`);
|
||||||
|
|
|
@ -20,13 +20,12 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/upload/share/${code.trim()}`);
|
const response = await fetch(`http://localhost:3000/upload/share/${code.trim()}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.message || '分享码无效或已过期');
|
throw new Error(errorData.message || '分享码无效或已过期');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
onValidSuccess(data.fileId);
|
onValidSuccess(data.fileId);
|
||||||
message.success('验证成功');
|
message.success('验证成功');
|
||||||
|
|
|
@ -539,12 +539,12 @@ model TrainPlan {
|
||||||
|
|
||||||
model ShareCode {
|
model ShareCode {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
code String @unique
|
code String? @unique
|
||||||
fileId String
|
fileId String? @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
expiresAt DateTime
|
expiresAt DateTime? @map("expires_at")
|
||||||
isUsed Boolean @default(false)
|
isUsed Boolean? @default(false)
|
||||||
|
fileName String? @map("file_name")
|
||||||
@@index([code])
|
@@index([code])
|
||||||
@@index([fileId])
|
@@index([fileId])
|
||||||
@@index([expiresAt])
|
@@index([expiresAt])
|
||||||
|
|
1529
pnpm-lock.yaml
1529
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue