From 0008f405fe57d50db63b1d9605e3285cdc2eb9b0 Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 2 Apr 2025 16:14:30 +0800 Subject: [PATCH] rht --- .../src/models/resource/resource.service.ts | 53 ++- apps/server/src/upload/share-code.service.ts | 173 +++++++++ apps/server/src/upload/types.ts | 23 ++ apps/server/src/upload/upload.controller.ts | 135 ++++++- apps/server/src/upload/upload.module.ts | 4 +- .../src/app/admin/deptsettingpage/page.tsx | 357 ++++++++++++++++++ .../sharecode/ShareCodeCenerator.module.css | 68 ++++ .../sharecode/ShareCodeValidator.module.css | 12 + .../admin/sharecode/sharecodegenerator.tsx | 135 +++++++ .../admin/sharecode/sharecodevalidator.tsx | 69 ++++ apps/web/src/hooks/useTusUpload.ts | 44 +-- apps/web/src/routes/index.tsx | 139 +------ config/nginx/conf.d/web.conf | 4 +- packages/common/prisma/schema.prisma | 13 + 14 files changed, 1063 insertions(+), 166 deletions(-) create mode 100755 apps/server/src/upload/share-code.service.ts create mode 100755 apps/web/src/app/admin/deptsettingpage/page.tsx create mode 100755 apps/web/src/app/admin/sharecode/ShareCodeCenerator.module.css create mode 100755 apps/web/src/app/admin/sharecode/ShareCodeValidator.module.css create mode 100755 apps/web/src/app/admin/sharecode/sharecodegenerator.tsx create mode 100755 apps/web/src/app/admin/sharecode/sharecodevalidator.tsx diff --git a/apps/server/src/models/resource/resource.service.ts b/apps/server/src/models/resource/resource.service.ts index 3a35a7e..86b69aa 100755 --- a/apps/server/src/models/resource/resource.service.ts +++ b/apps/server/src/models/resource/resource.service.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable ,Logger} from '@nestjs/common'; import { BaseService } from '../base/base.service'; + import { UserProfile, db, @@ -11,6 +12,7 @@ import { @Injectable() export class ResourceService extends BaseService { + private readonly logger = new Logger(ResourceService.name); constructor() { super(db, ObjectType.RESOURCE); } @@ -33,4 +35,53 @@ export class ResourceService extends BaseService { }, }); } + // 添加保存文件名的方法 + async saveFileName(fileId: string, fileName: string): Promise { + 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 { + 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; + } + } } diff --git a/apps/server/src/upload/share-code.service.ts b/apps/server/src/upload/share-code.service.ts new file mode 100755 index 0000000..f92cb50 --- /dev/null +++ b/apps/server/src/upload/share-code.service.ts @@ -0,0 +1,173 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { customAlphabet } from 'nanoid-cjs'; +import { db } from '@nice/common'; +import { ShareCode, GenerateShareCodeResponse } from './types'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { ResourceService } from '@server/models/resource/resource.service'; + +@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 }, + }); + console.log('完整 fileId:', fileId); // 确保与前端一致 + + if (!resource) { + throw new NotFoundException('文件不存在'); + } + + // 生成分享码 + const code = this.generateCode(); + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 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 { + console.log(`尝试验证分享码: ${code}`); + + // 查找有效的分享码 + const shareCode = await db.shareCode.findFirst({ + where: { + code, + isUsed: false, + expiresAt: { gt: new Date() }, + }, + }); + + console.log('查询结果:', shareCode); + + if (!shareCode) { + console.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(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async cleanupExpiredShareCodes() { + try { + const result = await db.shareCode.deleteMany({ + where: { + OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }], + }, + }); + + this.logger.log(`Cleaned up ${result.count} expired share codes`); + } catch (error) { + this.logger.error('Failed to cleanup expired share codes', error); + } + } + + // 获取分享码信息 + 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 []; + } + } +} diff --git a/apps/server/src/upload/types.ts b/apps/server/src/upload/types.ts index 2140ebc..b841871 100755 --- a/apps/server/src/upload/types.ts +++ b/apps/server/src/upload/types.ts @@ -20,6 +20,22 @@ export interface UploadLock { clientId: string; timestamp: number; } + +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; +} + // 添加重试机制,处理临时网络问题 // 实现定期清理过期的临时文件 // 添加文件完整性校验 @@ -27,3 +43,10 @@ export interface UploadLock { // 添加并发限制,防止系统资源耗尽 // 实现文件去重功能,避免重复上传 // 添加日志记录和监控机制 +// 添加文件类型限制 +// 添加文件大小限制 +// 添加文件上传时间限制 +// 添加文件上传速度限制 +// 添加文件上传队列管理 +// 添加文件上传断点续传 +// 添加文件上传进度条 diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts index f014c42..b2dbd5e 100755 --- a/apps/server/src/upload/upload.controller.ts +++ b/apps/server/src/upload/upload.controller.ts @@ -10,13 +10,22 @@ import { Delete, Head, Options, + NotFoundException, + HttpException, + HttpStatus, + Body, } from '@nestjs/common'; import { Request, Response } from 'express'; import { TusService } from './tus.service'; - +import { ShareCodeService } from './share-code.service'; +import { ResourceService } from '@server/models/resource/resource.service'; @Controller('upload') export class UploadController { - constructor(private readonly tusService: TusService) {} + constructor( + private readonly tusService: TusService, + private readonly shareCodeService: ShareCodeService, + private readonly resourceService: ResourceService, + ) {} // @Post() // async handlePost(@Req() req: Request, @Res() res: Response) { // return this.tusService.handleTus(req, res); @@ -36,6 +45,52 @@ export class UploadController { async handlePost(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); } + @Get('share/:code') + async validateShareCode(@Param('code') code: string) { + console.log('收到验证分享码请求,code:', code); + + const shareCode = await this.shareCodeService.validateAndUseCode(code); + console.log('验证分享码结果:', shareCode); + + if (!shareCode) { + throw new NotFoundException('分享码无效或已过期'); + } + + // 获取文件信息 + const resource = await this.resourceService.findUnique({ + where: { fileId: shareCode.fileId }, + }); + console.log('获取到的资源信息:', resource); + + if (!resource) { + throw new NotFoundException('文件不存在'); + } + + // 直接返回正确的数据结构 + const response = { + fileId: shareCode.fileId, + fileName:shareCode.fileName || 'downloaded_file', + code: shareCode.code, + expiresAt: shareCode.expiresAt + }; + + console.log('返回给前端的数据:', response); // 添加日志 + return response; + } + + @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('/*') async handleGet(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); @@ -51,4 +106,80 @@ export class UploadController { async handleUpload(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); } + @Post('share/:fileId(*)') + async generateShareCode(@Param('fileId') fileId: string) { + try { + console.log('收到生成分享码请求,fileId:', fileId); + const result = await this.shareCodeService.generateShareCode(fileId); + console.log('生成分享码结果:', result); + return result; + } catch (error) { + console.error('生成分享码错误:', error); + throw new HttpException( + { + message: (error as Error).message || '生成分享码失败', + error: 'SHARE_CODE_GENERATION_FAILED' + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + @Post('filename') + async saveFileName(@Body() data: { fileId: string; fileName: string }) { + try { + console.log('收到保存文件名请求:', data); + + // 检查参数 + 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 + ); + } + } + + @Get('download/:fileId') + async downloadFile(@Param('fileId') fileId: string, @Res() res: Response) { + try { + // 获取文件信息 + 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) { + // 错误处理... + } + } + } diff --git a/apps/server/src/upload/upload.module.ts b/apps/server/src/upload/upload.module.ts index 6c8e1b0..e1a6a56 100755 --- a/apps/server/src/upload/upload.module.ts +++ b/apps/server/src/upload/upload.module.ts @@ -3,7 +3,7 @@ import { UploadController } from './upload.controller'; import { BullModule } from '@nestjs/bullmq'; import { TusService } from './tus.service'; import { ResourceModule } from '@server/models/resource/resource.module'; - +import { ShareCodeService } from './share-code.service'; @Module({ imports: [ BullModule.registerQueue({ @@ -12,6 +12,6 @@ import { ResourceModule } from '@server/models/resource/resource.module'; ResourceModule, ], controllers: [UploadController], - providers: [TusService], + providers: [TusService, ShareCodeService], }) export class UploadModule {} diff --git a/apps/web/src/app/admin/deptsettingpage/page.tsx b/apps/web/src/app/admin/deptsettingpage/page.tsx new file mode 100755 index 0000000..15038e7 --- /dev/null +++ b/apps/web/src/app/admin/deptsettingpage/page.tsx @@ -0,0 +1,357 @@ +import { useTusUpload } from "@web/src/hooks/useTusUpload"; +import { ShareCodeGenerator } from "../sharecode/sharecodegenerator"; +import { ShareCodeValidator } from "../sharecode/sharecodevalidator"; +import { useState, useRef, useCallback } from "react"; +import { message, Progress, Button, Tabs, DatePicker } from "antd"; +import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons"; +import { env } from '../../../env' +const { TabPane } = Tabs; + +export default function DeptSettingPage() { + const [uploadedFileId, setUploadedFileId] = useState(''); + const [uploadedFileName, setUploadedFileName] = useState(''); + const [fileNameMap, setFileNameMap] = useState>({}); + const [uploadedFiles, setUploadedFiles] = useState<{ id: string, name: string }[]>([]); + const [isDragging, setIsDragging] = useState(false); + const [expireTime, setExpireTime] = useState(null); + const dropRef = useRef(null); + + // 使用您的 useTusUpload hook + const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({ + onSuccess: (result) => { + setUploadedFileId(result.fileId); + setUploadedFileName(result.fileName); + message.success('文件上传成功'); + }, + onError: (error: Error) => { + message.error('上传失败:' + error.message); + } + }); + + // 清除已上传文件 + const handleClearFile = () => { + setUploadedFileId(''); + setUploadedFileName(''); + setUploadedFiles([]); + setFileNameMap({}); + }; + + // 处理文件上传 + const handleFileSelect = async (file: File) => { + // 限制:如果已有上传文件,则提示用户 + if (uploadedFiles.length > 0) { + message.warning('只能上传一个文件,请先删除已上传的文件'); + return; + } + + const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 + + handleFileUpload( + file, + async (result) => { + setUploadedFileId(result.fileId); + setUploadedFileName(result.fileName); + + // 添加到已上传文件列表 + setUploadedFiles([{ id: result.fileId, name: file.name }]); + + // 在前端保存文件名映射(用于当前会话) + setFileNameMap({ + [result.fileId]: file.name + }); + + // 上传成功后保存原始文件名到数据库 + try { + console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId); + + const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/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('文件上传成功'); + }, + (error) => { + message.error('上传失败:' + error.message); + }, + fileKey + ); + }; + + // 处理多个文件上传 - 已移除 + // const handleFilesUpload = (file: File) => { + // handleFileSelect(file); + // }; + + // 处理文件删除 + const handleDeleteFile = async (fileId: string) => { + try { + // 可以添加删除文件的API调用 + // const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, { + // method: 'DELETE' + // }); + + // if (!response.ok) { + // throw new Error('删除文件失败'); + // } + + // 无论服务器删除是否成功,前端都需要更新状态 + setUploadedFiles([]); + setUploadedFileId(''); + setUploadedFileName(''); + setFileNameMap({}); + + message.success('文件已删除'); + } catch (error) { + console.error('删除文件错误:', error); + message.error('删除文件失败'); + } + }; + + // 拖拽相关处理函数 + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + handleFileSelect(e.dataTransfer.files[0]); + }, []); + + + // 处理分享码生成成功 + const handleShareSuccess = (code: string) => { + message.success('分享码生成成功:' + code); + // 可以在这里添加其他逻辑,比如保存到历史记录 + }; + + // 处理分享码验证成功 + const handleValidSuccess = async (fileId: string, fileName: string) => { + try { + // 构建下载URL(包含文件名参数) + const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(fileName)}`; + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error('文件下载失败'); + } + + // 创建下载链接 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // 直接使用传入的 fileName + link.download = fileName; + + // 触发下载 + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.success('文件下载开始'); + } catch (error) { + console.error('下载失败:', error); + message.error('文件下载失败'); + } + }; + + + return ( +
+ 文件分享中心 + + + {/* 文件上传区域 */} +
+

第一步:上传文件

+ + {/* 如果没有已上传文件,显示上传区域 */} + {uploadedFiles.length === 0 ? ( +
+ +

点击或拖拽文件到此区域进行上传

+

只能上传单个文件

+ + { + const file = e.target.files?.[0]; + if (file) { + handleFileSelect(file); + } + }} + disabled={isUploading} + /> + +
+ ) : ( +
+
+

+ 您已上传文件,请继续下一步生成分享码 +

+
+
+ )} + + {/* 已上传文件列表 */} + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((file) => ( +
+
+
+ +
+ {file.name} +
+
+ ))} +
+ )} + + {isUploading && ( +
+ +
+ )} + + {uploadError && ( +
+ {uploadError} +
+ )} +
+ + {/* 生成分享码区域 */} + {uploadedFileId && ( +
+

第二步:生成分享码

+ + +
+ )} +
+ + {/* 使用分享码区域 */} + +
+

使用分享码下载文件

+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/admin/sharecode/ShareCodeCenerator.module.css b/apps/web/src/app/admin/sharecode/ShareCodeCenerator.module.css new file mode 100755 index 0000000..9972735 --- /dev/null +++ b/apps/web/src/app/admin/sharecode/ShareCodeCenerator.module.css @@ -0,0 +1,68 @@ +.container { + padding: 20px; + border-radius: 8px; + background-color: #f8f9fa; + } + + .generateButton { + width: 100%; + padding: 12px; + border: none; + border-radius: 6px; + background-color: #1890ff; + color: white; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; + } + + .generateButton:hover { + background-color: #40a9ff; + } + + .generateButton:disabled { + background-color: #d9d9d9; + cursor: not-allowed; + } + + .codeDisplay { + text-align: center; + } + + .codeWrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin: 16px 0; + } + + .code { + font-size: 24px; + font-weight: bold; + letter-spacing: 2px; + color: #1890ff; + padding: 8px 16px; + background-color: #e6f7ff; + border-radius: 4px; + } + + .copyButton { + border: none; + background: none; + cursor: pointer; + color: #1890ff; + font-size: 18px; + padding: 4px; + } + + .expireInfo { + color: #666; + margin: 8px 0; + } + + .hint { + color: #ff4d4f; + margin: 8px 0; + font-size: 14px; + } \ No newline at end of file diff --git a/apps/web/src/app/admin/sharecode/ShareCodeValidator.module.css b/apps/web/src/app/admin/sharecode/ShareCodeValidator.module.css new file mode 100755 index 0000000..440a145 --- /dev/null +++ b/apps/web/src/app/admin/sharecode/ShareCodeValidator.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + gap: 12px; + padding: 20px; + border-radius: 8px; + background-color: #f8f9fa; + } + + .input { + font-size: 16px; + text-transform: uppercase; + } \ No newline at end of file diff --git a/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx b/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx new file mode 100755 index 0000000..ad5580f --- /dev/null +++ b/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx @@ -0,0 +1,135 @@ + +import React, { useState } from 'react'; +import { Button, message } from 'antd'; +import { CopyOutlined } from '@ant-design/icons'; +import {env} from '../../../env' +interface ShareCodeGeneratorProps { + fileId: string; + onSuccess?: (code: string) => void; + fileName?: string; +} + +export const ShareCodeGenerator: React.FC = ({ + fileId, + onSuccess, + fileName, +}) => { + const [loading, setLoading] = useState(false); + const [shareCode, setShareCode] = useState(''); + const [expiresAt, setExpiresAt] = useState(null); + + const generateCode = async () => { + setLoading(true); + console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName); + + try { + const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${fileId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileId + }) + }); + console.log('Current fileId:', fileId); // 确保 fileId 有效 + console.log('请求URL:', `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${fileId}`); + console.log('API响应状态:', response.status); + + const responseText = await response.text(); + console.log('API原始响应:', responseText); + + if (!response.ok) { + throw new Error(`请求失败: ${response.status} ${responseText || '无错误信息'}`); + } + + // 确保响应不为空 + if (!responseText) { + throw new Error('服务器返回空响应'); + } + + // 尝试解析JSON + let data; + try { + data = JSON.parse(responseText); + } catch (e) { + console.error('解析响应JSON失败:', e); + throw new Error('服务器响应格式错误'); + } + + console.log('解析后的响应数据:', data); // 调试日志 + + if (!data.code) { + throw new Error('响应中没有分享码'); + } + + setShareCode(data.code); + setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null); + onSuccess?.(data.code); + message.success('分享码生成成功'); + } catch (error) { + console.error('生成分享码错误:', error); + message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* 添加调试信息 */} + 文件ID: {fileId} +
+ + {!shareCode ? ( + + ) : ( +
+
+ + {shareCode} + +
+ {expiresAt ? ( +
+ 有效期至: {expiresAt.toLocaleString()} +
+ ) : ( +
+ 未获取到有效期信息 +
+ )} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/admin/sharecode/sharecodevalidator.tsx b/apps/web/src/app/admin/sharecode/sharecodevalidator.tsx new file mode 100755 index 0000000..2a3bf25 --- /dev/null +++ b/apps/web/src/app/admin/sharecode/sharecodevalidator.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import { Input, Button, message } from 'antd'; +import styles from './ShareCodeValidator.module.css'; +import {env} from '../../../env' +interface ShareCodeValidatorProps { + onValidSuccess: (fileId: string, fileName: string) => void; +} + +export const ShareCodeValidator: React.FC = ({ + onValidSuccess, +}) => { + const [code, setCode] = useState(''); + const [loading, setLoading] = useState(false); + + const validateCode = async () => { + if (!code.trim()) { + message.warning('请输入分享码'); + return; + } + + setLoading(true); + try { + const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${code.trim()}`); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || '分享码无效或已过期'); + } + + const data = await response.json(); + console.log('验证分享码返回数据:', data); + + if (!data.fileId) { + throw new Error('未找到文件ID'); + } + + const fileName = data.fileName || 'downloaded_file'; + + onValidSuccess(data.fileId, fileName); + message.success(`验证成功,文件名:${fileName}`); + } catch (error) { + console.error('验证分享码失败:', error); + message.error('分享码无效或已过期'); + } finally { + setLoading(false); + } + }; + + return ( +
+ setCode(e.target.value.toUpperCase())} + placeholder="请输入分享码" + maxLength={8} + onPressEnter={validateCode} + /> + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index 0337110..cf19439 100755 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -7,9 +7,10 @@ interface UploadResult { compressedUrl: string; url: string; fileId: string; + fileName: string; } -export function useTusUpload() { +export function useTusUpload(p0?: { onSuccess: (result: UploadResult) => void; onError: (error: Error) => void; }) { const [uploadProgress, setUploadProgress] = useState< Record >({}); @@ -74,36 +75,31 @@ export function useTusUpload() { })); }, onSuccess: async (payload) => { - try { - if (upload.url) { - const fileId = getFileId(upload.url); - const url = getResourceUrl(upload.url); - setIsUploading(false); - setUploadProgress((prev) => ({ - ...prev, - [fileKey]: 100, - })); - onSuccess({ - compressedUrl: getCompressedImageUrl(url), - url, - fileId, - }); - } - } catch (error) { - const err = - error instanceof Error - ? error - : new Error("Unknown error"); + if (upload.url) { + const fileId = getFileId(upload.url); + const url = getResourceUrl(upload.url); setIsUploading(false); - setUploadError(err.message); - onError(err); + setUploadProgress((prev) => ({ + ...prev, + [fileKey]: 100, + })); + onSuccess({ + compressedUrl: getCompressedImageUrl(url), + url, + fileId, + fileName: uploadFile.name, + }); } }, onError: (error) => { + const err = + error instanceof Error + ? error + : new Error("Unknown error"); setIsUploading(false); setUploadError(error.message); console.log(error); - onError(error); + onError(err); }, }); upload.start(); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e48165d..3537bc0 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -6,25 +6,7 @@ import { useParams, } from "react-router-dom"; import ErrorPage from "../app/error"; -import WithAuth from "../components/utils/with-auth"; -import LoginPage from "../app/login"; -import HomePage from "../app/main/home/page"; -import { CourseDetailPage } from "../app/main/course/detail/page"; -import { CourseBasicForm } from "../components/models/course/editor/form/CourseBasicForm"; -import CourseContentForm from "../components/models/course/editor/form/CourseContentForm/CourseContentForm"; -import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout"; -import { MainLayout } from "../app/main/layout/MainLayout"; -import CoursesPage from "../app/main/courses/page"; -import PathPage from "../app/main/path/page"; -import { adminRoute } from "./admin-route"; -import PathEditorPage from "../app/main/path/editor/page"; - -import { CoursePreview } from "../app/main/course/preview/page"; -import MyLearningPage from "../app/main/my-learning/page"; -import MyDutyPage from "../app/main/my-duty/page"; -import MyPathPage from "../app/main/my-path/page"; -import SearchPage from "../app/main/search/page"; -import MyDutyPathPage from "../app/main/my-duty-path/page"; +import DeptSettingPage from "../app/admin/deptsettingpage/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -47,123 +29,10 @@ export type CustomRouteObject = | CustomNonIndexRouteObject; export const routes: CustomRouteObject[] = [ { - path: "/", + path:'/', + element:, errorElement: , - handle: { - crumb() { - return 主页; - }, - }, - children: [ - { - element: , - children: [ - { - index: true, - element: , - }, - { - path: "path", - children: [ - { - index: true, - element: , - }, - { - path: "editor/:id?", - element: ( - - ), - }, - ], - }, - { - path: "courses", - element: , - }, - { - path: "my-path", - element: ( - - - - ), - }, - { - path: "my-duty-path", - element: ( - - - - ), - }, - { - path: "my-duty", - element: ( - - - - ), - }, - { - path: "my-learning", - element: ( - - - - ), - }, - { - path: "search", - element: , - }, - { - path: "course/:id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 - element: , - }, - ], - }, - - { - path: "course", - children: [ - { - path: ":id?/editor", - element: ( - - - - ), - children: [ - { - index: true, - element: ( - - - - ), - }, - - { - path: "content", - element: ( - - - - ), - }, - ], - }, - ], - }, - adminRoute, - ], - }, - { - path: "/login", - breadcrumb: "登录", - element: , - }, + } ]; export const router = createBrowserRouter(routes); diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index 88fa2c9..8f9683a 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -2,7 +2,7 @@ server { # 监听80端口 listen 80; # 服务器域名/IP地址,使用环境变量 - server_name host.docker.internal; + server_name 192.168.43.206; # 基础性能优化配置 # 启用tcp_nopush以优化数据发送 @@ -100,7 +100,7 @@ server { # 仅供内部使用 internal; # 代理到认证服务 - proxy_pass http://host.docker.internal:3006/auth/file; + proxy_pass http://192.168.43.206:3006/auth/file; # 请求优化:不传递请求体 proxy_pass_request_body off; diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index ed02c77..5a8f27f 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -418,3 +418,16 @@ model Person { gender Boolean animals Animal[] } +model ShareCode { + id String @id @default(cuid()) + code String? @unique + fileId String? @unique + createdAt DateTime @default(now()) + expiresAt DateTime? @map("expires_at") + isUsed Boolean? @default(false) + fileName String? @map("file_name") + + @@index([code]) + @@index([fileId]) + @@index([expiresAt]) +} \ No newline at end of file