# MinIO 文件复制接口实现指南 ## 概述 在合同起草功能中,有两种使用模式: ### 模式一:直接使用模板文件(推荐) **适用场景**:模板文档已包含占位符,直接在原模板上编辑 **流程**: 1. 用户点击"起草合同" 2. 创建草稿记录,`file_path` 使用模板文件路径 3. 在 Collabora 中编辑模板文件 4. 替换占位符完成起草 **优点**: - 无需复制文件,速度快 - 节省存储空间 - 实现简单 **缺点**: - 多个草稿共用一个模板文件 - Collabora 编辑可能互相干扰 ### 模式二:复制模板文件(完整隔离) **适用场景**:需要完全隔离的草稿文件,避免编辑冲突 **流程**: 1. 用户点击"起草合同" 2. 前端调用 `/api/files/copy` 复制模板文件 3. 创建草稿记录,`file_path` 使用复制后的文件路径 4. 在 Collabora 中编辑新文件 5. 替换占位符完成起草 **优点**: - 每个草稿独立文件,互不干扰 - 保留原始模板不被修改 **缺点**: - 需要复制文件,增加存储空间 - 复制操作增加响应时间 --- ## 文件复制接口实现 ### API 端点 ``` POST /api/files/copy ``` ### 请求参数 ```typescript interface CopyFileRequest { sourceFilePath: string; // 源文件路径(模板文件) targetFilePath: string; // 目标文件路径(草稿文件) bucket?: string; // MinIO bucket名称(默认:docauditai) } ``` ### 响应 ```typescript interface CopyFileResponse { success: boolean; targetFilePath: string; // 复制后的文件路径 message?: string; } ``` --- ## MinIO SDK 实现 ### 1. 安装依赖 ```bash npm install minio ``` ### 2. 配置环境变量 在 `.env` 文件中添加: ```env # MinIO 配置 MINIO_ENDPOINT=10.76.244.156 MINIO_PORT=9000 MINIO_ACCESS_KEY=your_access_key MINIO_SECRET_KEY=your_secret_key MINIO_BUCKET=docauditai ``` ### 3. 创建 MinIO 客户端工具 **文件**:`app/api/minio-client.server.ts` ```typescript import { Client } from 'minio'; let minioClient: Client | null = null; /** * 获取 MinIO 客户端实例(单例) */ export function getMinioClient(): Client { if (!minioClient) { minioClient = new Client({ endPoint: process.env.MINIO_ENDPOINT || 'localhost', port: parseInt(process.env.MINIO_PORT || '9000'), useSSL: false, accessKey: process.env.MINIO_ACCESS_KEY || '', secretKey: process.env.MINIO_SECRET_KEY || '' }); } return minioClient; } /** * 复制 MinIO 对象 * @param bucket Bucket 名称 * @param sourceFilePath 源文件路径 * @param targetFilePath 目标文件路径 */ export async function copyMinioFile( bucket: string, sourceFilePath: string, targetFilePath: string ): Promise { const client = getMinioClient(); // 1. 检查源文件是否存在 try { await client.statObject(bucket, sourceFilePath); } catch (error) { throw new Error(`源文件不存在: ${sourceFilePath}`); } // 2. 复制对象 await client.copyObject( bucket, // 目标 bucket targetFilePath, // 目标路径 `/${bucket}/${sourceFilePath}`, // 源路径 null // 复制条件(可选) ); console.log(`[MinIO] 文件复制成功: ${sourceFilePath} -> ${targetFilePath}`); } /** * 检查文件是否存在 * @param bucket Bucket 名称 * @param filePath 文件路径 */ export async function fileExists( bucket: string, filePath: string ): Promise { const client = getMinioClient(); try { await client.statObject(bucket, filePath); return true; } catch (error) { return false; } } /** * 删除文件 * @param bucket Bucket 名称 * @param filePath 文件路径 */ export async function deleteMinioFile( bucket: string, filePath: string ): Promise { const client = getMinioClient(); await client.removeObject(bucket, filePath); console.log(`[MinIO] 文件删除成功: ${filePath}`); } ``` ### 4. 更新文件复制 API **文件**:`app/routes/api.files.copy.tsx` ```typescript import { copyMinioFile } from '~/api/minio-client.server'; export async function action({ request }: ActionFunctionArgs) { // ... 验证用户身份 ... const body = await request.json() as CopyFileRequest; const { sourceFilePath, targetFilePath, bucket = 'docauditai' } = body; try { // 调用 MinIO 复制函数 await copyMinioFile(bucket, sourceFilePath, targetFilePath); const response: CopyFileResponse = { success: true, targetFilePath, message: '文件复制成功' }; return json(response); } catch (error) { console.error('[Files Copy API] 文件复制失败:', error); return json( { success: false, error: error instanceof Error ? error.message : '文件复制失败' }, { status: 500 } ); } } ``` --- ## 前端集成 ### 方式一:直接使用模板(当前实现) **文件**:`app/routes/contract-template.detail.$id.tsx` ```typescript const handleStartDraft = async () => { // 不传 draftFilePath,直接使用模板路径 const response = await fetch('/api/contracts/draft', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ templateId: template.id, title: title.trim() // 不传 draftFilePath }) }); // ... }; ``` ### 方式二:复制文件后创建草稿 **文件**:`app/routes/contract-template.detail.$id.tsx` ```typescript const handleStartDraft = async () => { // 1. 生成草稿文件路径 const timestamp = Date.now(); const fileExtension = template.file_path.split('.').pop(); const targetFilePath = `drafts/contract_${template.id}_${userId}_${timestamp}.${fileExtension}`; // 2. 调用文件复制 API const copyResponse = await fetch('/api/files/copy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceFilePath: template.file_path, targetFilePath: targetFilePath }) }); if (!copyResponse.ok) { throw new Error('文件复制失败'); } // 3. 创建草稿记录(传递复制后的文件路径) const draftResponse = await fetch('/api/contracts/draft', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ templateId: template.id, title: title.trim(), draftFilePath: targetFilePath // 传递复制后的路径 }) }); // ... }; ``` --- ## 推荐实施步骤 ### 阶段一:使用模式一(已完成) - ✅ 直接使用模板文件路径 - ✅ 无需复制文件 - ✅ 快速上线验证功能 ### 阶段二:实现文件复制(按需) 如果发现模式一存在编辑冲突问题,再实施: 1. 安装 `minio` 依赖 2. 创建 `app/api/minio-client.server.ts` 3. 实现 `api.files.copy.tsx` 接口 4. 前端修改为方式二流程 5. 测试文件复制功能 --- ## 性能优化建议 ### 1. 异步复制 文件复制可以异步进行: ```typescript // 前端先创建草稿记录,后台异步复制文件 const draft = await createDraft(); // 后台任务队列处理文件复制 backgroundCopyFile(template.file_path, draftFilePath); ``` ### 2. 延迟复制 只在用户首次编辑时才复制文件: ```typescript // 创建草稿时不复制 // 用户首次打开编辑时才触发复制 // 草稿记录中添加 is_copied 字段标记 ``` ### 3. 缓存优化 对于热门模板,预先复制多个副本到缓存池。 --- ## 清理策略 ### 定期清理未完成的草稿文件 ```sql -- 创建定时任务清理7天前的草稿文件 DELETE FROM drafted_contracts WHERE status = 'draft' AND created_at < NOW() - INTERVAL '7 days'; ``` ### 删除草稿时同时删除文件 ```typescript export async function deleteDraft(draftId: number, userId: number) { // 1. 查询草稿记录 const draft = await getDraftById(draftId, userId); // 2. 如果是独立文件(不是模板路径),删除文件 if (draft.file_path.startsWith('drafts/')) { await deleteMinioFile('docauditai', draft.file_path); } // 3. 删除数据库记录 await db.from('drafted_contracts').delete().eq('id', draftId); } ``` --- ## 安全考虑 ### 1. 路径验证 ```typescript // 防止路径遍历攻击 function validateFilePath(filePath: string): boolean { // 不允许 ../ 或绝对路径 if (filePath.includes('..') || filePath.startsWith('/')) { return false; } return true; } ``` ### 2. 用户权限验证 ```typescript // 只能复制有权限的模板文件 // 只能访问自己的草稿文件 ``` ### 3. 文件大小限制 ```typescript // 限制可复制的文件大小 const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB ``` --- ## 总结 当前实现采用**模式一(直接使用模板)**,满足基本需求且实现简单。 如果后续发现需要完全隔离的草稿文件,可以按照本文档实施**文件复制功能**。 API 接口和数据结构已预留位置,实施成本低。