diff --git a/apps/server/src/models/share-code/share-code.module.ts b/apps/server/src/models/share-code/share-code.module.ts new file mode 100644 index 0000000..6782366 --- /dev/null +++ b/apps/server/src/models/share-code/share-code.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { TrpcService } from '@server/trpc/trpc.service'; +import { ResourceModule } from '../resource/resource.module'; +import { ShareCodeService } from './share-code.service'; +import { ShareCodeRouter } from './share-code.router'; + +@Module({ + imports: [ResourceModule], + providers: [TrpcService, ShareCodeService, ShareCodeRouter], + exports: [ShareCodeService, ShareCodeRouter], + controllers: [], +}) +export class ShareCodeModule { } diff --git a/apps/server/src/models/share-code/share-code.router.ts b/apps/server/src/models/share-code/share-code.router.ts new file mode 100644 index 0000000..a813ef3 --- /dev/null +++ b/apps/server/src/models/share-code/share-code.router.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { ShareCodeService } from "./share-code.service"; +import { TrpcService } from "@server/trpc/trpc.service"; +import { Injectable } from "@nestjs/common"; +@Injectable() +export class ShareCodeRouter { + constructor( + private readonly shareCodeService: ShareCodeService, + private readonly trpc: TrpcService + ) {} + + router = this.trpc.router({ + generateShareCode: this.trpc.procedure + .input(z.object({ fileId: z.string() })) + .mutation(async ({ input }) => { + return this.shareCodeService.generateShareCode(input.fileId); + }), + validateAndUseCode: this.trpc.procedure + .input(z.object({ code: z.string() })) + .mutation(async ({ input }) => { + return this.shareCodeService.validateAndUseCode(input.code); + }), + getShareCodeInfo: this.trpc.procedure + .input(z.object({ code: z.string() })) + .query(async ({ input }) => { + return this.shareCodeService.getShareCodeInfo(input.code); + }), + hasActiveShareCode: this.trpc.procedure + .input(z.object({ fileId: z.string() })) + .query(async ({ input }) => { + return this.shareCodeService.hasActiveShareCode(input.fileId); + }), + getFileShareHistory: this.trpc.procedure + .input(z.object({ fileId: z.string() })) + .query(async ({ input }) => { + return this.shareCodeService.getFileShareHistory(input.fileId); + }), + getFileByShareCode: this.trpc.procedure + .input(z.object({ code: z.string() })) + .query(async ({ input }) => { + return this.shareCodeService.getFileByShareCode(input.code); + }), + generateShareCodeByFileId: this.trpc.procedure + .input(z.object({ fileId: z.string() })) + .mutation(async ({ input }) => { + return this.shareCodeService.generateShareCodeByFileId(input.fileId); + }), + }); +} \ No newline at end of file diff --git a/apps/server/src/models/share-code/share-code.service.ts b/apps/server/src/models/share-code/share-code.service.ts new file mode 100755 index 0000000..bc3be5a --- /dev/null +++ b/apps/server/src/models/share-code/share-code.service.ts @@ -0,0 +1,239 @@ +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'; + +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 }, + }); + 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 []; + } + } + + // 根据分享码获取文件 + async getFileByShareCode(code: string) { + console.log('收到验证分享码请求,code:', code); + + const shareCode = await this.validateAndUseCode(code); + console.log('验证分享码结果:', shareCode); + + if (!shareCode) { + throw new NotFoundException('分享码无效或已过期'); + } + + // 获取文件信息 + const resource = await this.resourceService.findUnique({ + where: { fileId: shareCode.fileId }, + }); + console.log('获取到的资源信息:', resource); + const { filename } = resource.meta as any as ResourceMeta + if (!resource) { + throw new NotFoundException('文件不存在'); + } + + // 直接返回正确的数据结构 + const response = { + fileId: shareCode.fileId, + fileName: filename || 'downloaded_file', + code: shareCode.code, + expiresAt: shareCode.expiresAt + }; + + console.log('返回给前端的数据:', response); // 添加日志 + return response; + } + + // 根据文件ID生成分享码 + async generateShareCodeByFileId(fileId: string) { + try { + console.log('收到生成分享码请求,fileId:', fileId); + const result = await this.generateShareCode(fileId); + console.log('生成分享码结果:', result); + return result; + } catch (error) { + console.error('生成分享码错误:', error); + return error + } + + } +} diff --git a/apps/server/src/trpc/trpc.module.ts b/apps/server/src/trpc/trpc.module.ts index 222c9c9..eab1b6c 100755 --- a/apps/server/src/trpc/trpc.module.ts +++ b/apps/server/src/trpc/trpc.module.ts @@ -14,7 +14,7 @@ import { VisitModule } from '@server/models/visit/visit.module'; import { WebSocketModule } from '@server/socket/websocket.module'; import { RoleMapModule } from '@server/models/rbac/rbac.module'; import { TransformModule } from '@server/models/transform/transform.module'; - +import { ShareCodeModule } from '@server/models/share-code/share-code.module'; import { ResourceModule } from '@server/models/resource/resource.module'; @Module({ @@ -33,6 +33,7 @@ import { ResourceModule } from '@server/models/resource/resource.module'; VisitModule, WebSocketModule, ResourceModule, + ShareCodeModule, ], controllers: [], providers: [TrpcService, TrpcRouter, Logger], diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index 7550867..14fc2c0 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -14,7 +14,7 @@ import { RoleMapRouter } from '@server/models/rbac/rolemap.router'; import { TransformRouter } from '@server/models/transform/transform.router'; import { RoleRouter } from '@server/models/rbac/role.router'; import { ResourceRouter } from '../models/resource/resource.router'; - +import { ShareCodeRouter } from '@server/models/share-code/share-code.router'; @Injectable() export class TrpcRouter { logger = new Logger(TrpcRouter.name); @@ -32,6 +32,7 @@ export class TrpcRouter { private readonly message: MessageRouter, private readonly visitor: VisitRouter, private readonly resource: ResourceRouter, + private readonly shareCode: ShareCodeRouter, ) {} getRouter() { return; @@ -49,6 +50,7 @@ export class TrpcRouter { app_config: this.app_config.router, visitor: this.visitor.router, resource: this.resource.router, + shareCode: this.shareCode.router, }); wss: WebSocketServer = undefined; diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts index b2dbd5e..fe765e9 100755 --- a/apps/server/src/upload/upload.controller.ts +++ b/apps/server/src/upload/upload.controller.ts @@ -19,6 +19,13 @@ import { Request, Response } from 'express'; import { TusService } from './tus.service'; import { ShareCodeService } from './share-code.service'; import { ResourceService } from '@server/models/resource/resource.service'; + +interface ResourceMeta { + filename: string; + filetype: string; + filesize: string; +} + @Controller('upload') export class UploadController { constructor( @@ -61,7 +68,7 @@ export class UploadController { where: { fileId: shareCode.fileId }, }); console.log('获取到的资源信息:', resource); - + const {filename} = resource.meta as any as ResourceMeta if (!resource) { throw new NotFoundException('文件不存在'); } @@ -69,7 +76,7 @@ export class UploadController { // 直接返回正确的数据结构 const response = { fileId: shareCode.fileId, - fileName:shareCode.fileName || 'downloaded_file', + fileName:filename || 'downloaded_file', code: shareCode.code, expiresAt: shareCode.expiresAt }; diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg old mode 100755 new mode 100644 index 39a6980..3b6b9f3 --- a/apps/web/public/logo.svg +++ b/apps/web/public/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/web/public/vite.svg b/apps/web/public/vite.svg old mode 100755 new mode 100644 index 78260dd..3b6b9f3 --- a/apps/web/public/vite.svg +++ b/apps/web/public/vite.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/apps/web/src/app/admin/deptsettingpage/page.tsx b/apps/web/src/app/admin/deptsettingpage/page.tsx index 15038e7..f73e5fb 100755 --- a/apps/web/src/app/admin/deptsettingpage/page.tsx +++ b/apps/web/src/app/admin/deptsettingpage/page.tsx @@ -1,10 +1,11 @@ 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 { useState, useRef, useCallback, useEffect } from "react"; +import { message, Progress, Button, Tabs, DatePicker, Form } from "antd"; import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons"; import { env } from '../../../env' +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; const { TabPane } = Tabs; export default function DeptSettingPage() { @@ -15,18 +16,11 @@ export default function DeptSettingPage() { const [isDragging, setIsDragging] = useState(false); const [expireTime, setExpireTime] = useState(null); const dropRef = useRef(null); - + const [form] = Form.useForm(); + const [currentFile, setCurrentFile] = useState([]) + const uploadFileId = Form.useWatch(["file"], form)?.[0] // 使用您的 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 { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload(); // 清除已上传文件 const handleClearFile = () => { @@ -43,7 +37,7 @@ export default function DeptSettingPage() { message.warning('只能上传一个文件,请先删除已上传的文件'); return; } - + const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 handleFileUpload( @@ -110,17 +104,17 @@ export default function DeptSettingPage() { // 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); @@ -202,150 +196,32 @@ export default function DeptSettingPage() { {/* 文件上传区域 */}
-

第一步:上传文件

- + 第一步:上传文件 {/* 如果没有已上传文件,显示上传区域 */} - {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 && ( -
-

第二步:生成分享码

- - -
- )} +
+ 第二步:生成分享码 + +
{/* 使用分享码区域 */}
-

使用分享码下载文件

+ 使用分享码下载文件 diff --git a/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx b/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx index ad5580f..bb92116 100755 --- a/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx +++ b/apps/web/src/app/admin/sharecode/sharecodegenerator.tsx @@ -1,14 +1,20 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, message } from 'antd'; import { CopyOutlined } from '@ant-design/icons'; -import {env} from '../../../env' +import { env } from '../../../env' +import { useQueryClient } from '@tanstack/react-query'; +import { getQueryKey } from '@trpc/react-query'; +import { api } from '@nice/client'; interface ShareCodeGeneratorProps { fileId: string; onSuccess?: (code: string) => void; fileName?: string; } - +interface ShareCodeResponse { + code?: string; + expiresAt?: Date; +} export const ShareCodeGenerator: React.FC = ({ fileId, onSuccess, @@ -17,53 +23,29 @@ export const ShareCodeGenerator: React.FC = ({ const [loading, setLoading] = useState(false); const [shareCode, setShareCode] = useState(''); const [expiresAt, setExpiresAt] = useState(null); - + const queryClient = useQueryClient(); + const queryKey = getQueryKey(api.term); + const [isGenerate, setIsGenerate] = useState(false); + const [currentFileId, setCurrentFileId] = useState(''); + const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({ + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey }); + }, + }); + useEffect(() => { + if (fileId !== currentFileId || !fileId) { + setIsGenerate(false); + } + setCurrentFileId(fileId); + }, [fileId]) 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('响应中没有分享码'); - } - + const data: ShareCodeResponse = await generateShareCode.mutateAsync({ fileId }); + console.log('生成分享码结果:', data); setShareCode(data.code); + setIsGenerate(true); setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null); onSuccess?.(data.code); message.success('分享码生成成功'); @@ -79,10 +61,10 @@ export const ShareCodeGenerator: React.FC = ({
{/* 添加调试信息 */} - 文件ID: {fileId} + 文件ID: {fileId ? fileId : '未选择文件'}
- {!shareCode ? ( + {!isGenerate ? ( ) : (
-
void; } @@ -11,8 +12,16 @@ export const ShareCodeValidator: React.FC = ({ }) => { const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); + const { data: result, isLoading } = api.shareCode.getFileByShareCode.useQuery( + { code: code.trim() }, + { + enabled: !!code.trim() + } + ) - const validateCode = async () => { + + + const validateCode = useCallback(() => { if (!code.trim()) { message.warning('请输入分享码'); return; @@ -20,31 +29,16 @@ export const ShareCodeValidator: React.FC = ({ 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}`); + console.log('验证分享码返回数据:', result); + onValidSuccess(result.fileId, result.fileName); + message.success(`验证成功,文件名:${result.fileName}`); } catch (error) { console.error('验证分享码失败:', error); message.error('分享码无效或已过期'); } finally { setLoading(false); } - }; + },[result]) return (
diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 2ff3fad..876c151 100755 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -86,6 +86,7 @@ export const TusUploader = ({ handleFileUpload( file, (result) => { + console.log(result) setCompletedFiles((prev) => [ ...prev, { diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index cf19439..85c3d31 100755 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -10,7 +10,7 @@ interface UploadResult { fileName: string; } -export function useTusUpload(p0?: { onSuccess: (result: UploadResult) => void; onError: (error: Error) => void; }) { +export function useTusUpload() { const [uploadProgress, setUploadProgress] = useState< Record >({}); @@ -77,6 +77,7 @@ export function useTusUpload(p0?: { onSuccess: (result: UploadResult) => void; o onSuccess: async (payload) => { if (upload.url) { const fileId = getFileId(upload.url); + //console.log(fileId) const url = getResourceUrl(upload.url); setIsUploading(false); setUploadProgress((prev) => ({ diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 3537bc0..4c56b52 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -29,10 +29,10 @@ export type CustomRouteObject = | CustomNonIndexRouteObject; export const routes: CustomRouteObject[] = [ { - path:'/', - element:, + path: "/", + element: , errorElement: , - } + }, ]; export const router = createBrowserRouter(routes);