Files
leaudit-platform-frontend/docs/minio-file-copy-implementation.md
T
2025-12-05 00:09:32 +08:00

409 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<void> {
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<boolean> {
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<void> {
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 接口和数据结构已预留位置,实施成本低。