This commit is contained in:
2025-12-05 00:09:32 +08:00
parent bb3d22eabf
commit 3d1dbb3f97
214 changed files with 113060 additions and 1232 deletions
+513
View File
@@ -0,0 +1,513 @@
# 合同起草功能架构说明 v2.0
## 架构调整
根据实际需求,重新设计了服务端职责和文件处理策略。
---
## 核心理念
### 模板文档已包含占位符
**前提条件**
- 模板文档已在 MinIO 上存在
- 文档中已包含约定格式的占位符(如 `{{甲方名称}}`
- 占位符配置已存储在 `contract_templates.placeholder_schema`
**因此**
- 服务端主要负责数据管理(草稿记录、占位符值)
- 文件操作交给专门的接口处理(可选)
- 前端直接与 Collabora 和 MinIO 交互
---
## 服务端职责
### ✅ 核心职责
1. **草稿记录管理**
- 创建草稿记录(`drafted_contracts` 表)
- 记录占位符填写的值(`placeholder_values` 字段)
- 更新草稿状态(draft → completed → archived
2. **数据查询服务**
- 查询用户的草稿列表
- 查询草稿详情
- 查询模板信息
3. **业务逻辑处理**
- 必填字段验证
- 权限检查(只能操作自己的草稿)
- 状态流转控制
### ❌ 非职责
1. ~~文件复制~~(由专门的文件服务接口处理)
2. ~~MinIO 直接操作~~(封装在独立模块)
3. ~~文档渲染~~Collabora Online
4. ~~占位符替换~~(前端调用 Collabora API
---
## 文件处理策略
### 策略一:直接使用模板文件(默认)
**适用场景**
- 快速上线,简单实现
- 用户访问量不高,编辑冲突概率低
- 节省存储空间
**实现方式**
```typescript
// 创建草稿时,file_path 直接使用模板路径
const draft = await createDraftContract({
templateId: 123,
title: '采购合同',
// 不传 draftFilePath
}, userId);
// draft.file_path = template.file_path
```
**工作流程**
1. 用户点击"起草合同"
2. 创建草稿记录,`file_path` = 模板路径
3. 用户在 Collabora 中编辑模板文件
4. 替换占位符
5. 保存草稿(保存 placeholder_values
6. 完成起草
**优点**
- ✅ 无需复制文件,速度快
- ✅ 节省存储空间
- ✅ 实现简单
**缺点**
- ❌ 多个用户同时编辑同一模板可能冲突
- ❌ Collabora 编辑会话可能互相干扰
---
### 策略二:复制模板文件(可选)
**适用场景**
- 需要完全隔离的草稿文件
- 多用户并发编辑同一模板
- 需要保留编辑历史
**实现方式**
```typescript
// 1. 先调用文件复制接口
const copyResponse = await fetch('/api/files/copy', {
method: 'POST',
body: JSON.stringify({
sourceFilePath: template.file_path,
targetFilePath: 'drafts/contract_123_1_1704355200000.docx'
})
});
// 2. 创建草稿记录时传递复制后的路径
const draft = await createDraftContract({
templateId: 123,
title: '采购合同',
draftFilePath: 'drafts/contract_123_1_1704355200000.docx'
}, userId);
```
**工作流程**
1. 用户点击"起草合同"
2. 前端调用 `/api/files/copy` 复制文件
3. 创建草稿记录,`file_path` = 复制后的路径
4. 用户在 Collabora 中编辑新文件
5. 替换占位符
6. 完成起草
**优点**
- ✅ 每个草稿独立文件,互不干扰
- ✅ 保留原始模板不被修改
- ✅ 可以保留编辑历史
**缺点**
- ❌ 需要复制文件,增加存储空间
- ❌ 复制操作增加响应时间
---
## 数据流架构
### 创建草稿流程
```
用户操作
前端:contract-template.detail.$id.tsx
├─ 用户点击"起草合同"
├─ 输入标题
├─ [可选] 调用 /api/files/copy 复制文件
└─ 调用 /api/contracts/draft 创建草稿
后端:api.contracts.draft.tsx
├─ 验证用户身份
├─ 解析参数(templateId, title, draftFilePath?
└─ 调用 createDraftContract
服务层:draft-service.server.ts
├─ 查询模板信息
├─ 确定文件路径(draftFilePath || template.file_path
└─ 插入 drafted_contracts 记录
数据库:drafted_contracts 表
└─ 返回新记录
前端:跳转到 /contract-draft/:draftId
```
### 起草页面流程
```
起草页面加载
contract-draft.$draftId.tsx
├─ Loader:加载草稿和模板数据
│ ├─ 查询 drafted_contracts
│ └─ 查询 contract_templates
└─ 渲染页面
├─ 左侧:FilePreview 组件
│ └─ CollaboraViewer(编辑模式)
│ └─ 加载 draft.file_path 文件
└─ 右侧:PlaceholderForm 组件
└─ 渲染表单字段(基于 template.placeholder_schema
用户填写表单
用户点击"一键替换"
handleBatchReplace()
├─ 遍历 placeholderValues
├─ 调用 collaboraRef.unoCommands.replaceAll()
│ └─ 发送 UNO 命令到 Collabora iframe
└─ 自动调用 handleSaveDraft()
用户点击"保存草稿"
handleSaveDraft()
└─ PUT /api/contracts/draft/:id/placeholders
└─ 更新 placeholder_values 字段
用户点击"完成起草"
handleComplete()
├─ 验证必填字段
├─ POST /api/contracts/draft/:id/complete
│ └─ 更新 status = 'completed'
└─ 跳转回模板列表
```
---
## API 接口设计
### 1. 创建草稿
**端点**`POST /api/contracts/draft`
**请求**
```json
{
"templateId": 123,
"title": "采购合同-20250104",
"draftFilePath": "drafts/contract_123_1_1704355200000.docx" // 可选
}
```
**处理逻辑**
1. 验证用户身份
2. 查询模板信息
3. 确定文件路径:
- 如果传了 `draftFilePath`,使用它
- 否则使用 `template.file_path`
4. 创建草稿记录
5. 返回草稿ID和路径
**响应**
```json
{
"id": 456,
"filePath": "drafts/contract_123_1_1704355200000.docx",
"title": "采购合同-20250104",
"templateId": 123
}
```
### 2. 文件复制(预留接口)
**端点**`POST /api/files/copy`
**请求**
```json
{
"sourceFilePath": "templates/contract_template.docx",
"targetFilePath": "drafts/contract_123_1_1704355200000.docx",
"bucket": "docauditai"
}
```
**实现状态**
- ✅ API 接口已创建
- ⏳ MinIO 复制逻辑待实现(预留位置)
- 📝 实现指南:`docs/minio-file-copy-implementation.md`
### 3. 更新占位符
**端点**`PUT /api/contracts/draft/:id/placeholders`
**请求**
```json
{
"placeholders": {
"甲方名称": "广东省烟草专卖局",
"合同金额": "500000"
}
}
```
### 4. 完成起草
**端点**`POST /api/contracts/draft/:id/complete`
**处理逻辑**
1. 验证用户权限
2. 更新 `status = 'completed'`
3. 更新 `updated_at`
---
## 数据库设计
### drafted_contracts 表
```sql
CREATE TABLE drafted_contracts (
id SERIAL PRIMARY KEY,
template_id INTEGER NOT NULL, -- 关联的模板ID
file_path TEXT NOT NULL, -- 文件路径(可能是模板路径或复制后的路径)
title TEXT NOT NULL, -- 合同标题
placeholder_values JSONB DEFAULT '{}', -- 占位符填写的值
status TEXT DEFAULT 'draft', -- draft | completed | archived
created_by INTEGER, -- 创建人ID
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
**字段说明**
- `file_path`
- 策略一:值为 `template.file_path`(如 `templates/contract_template.docx`
- 策略二:值为复制后的路径(如 `drafts/contract_123_1_1704355200000.docx`
- `placeholder_values`
- 存储用户填写的占位符值
- JSONB 格式,便于查询和更新
- 示例:`{"甲方名称": "广东省烟草专卖局", "合同金额": "500000"}`
---
## 前端实现
### 模板详情页
**文件**`app/routes/contract-template.detail.$id.tsx`
**关键代码**
```typescript
const handleStartDraft = async () => {
// 提示用户输入标题
const title = prompt('请输入合同标题:', defaultTitle);
if (!title) return;
// 调用创建草稿 API(策略一:不传 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,使用模板路径
})
});
const data = await response.json();
// 跳转到起草页面
navigate(`/contract-draft/${data.id}`);
};
```
### 起草页面
**文件**`app/routes/contract-draft.$draftId.tsx`
**布局**
```tsx
<div className="flex h-screen">
{/* 左侧:文档预览(60% */}
<div className="w-[60%]">
<FilePreview
ref={filePreviewRef}
fileContent={{ path: draft.file_path }}
isTemplate={false} // 编辑模式
/>
</div>
{/* 右侧:占位符表单(40% */}
<div className="w-[40%]">
<PlaceholderForm
schema={template.placeholder_schema}
values={placeholderValues}
onBatchReplace={handleBatchReplace}
onSaveDraft={handleSaveDraft}
onComplete={handleComplete}
/>
</div>
</div>
```
**批量替换逻辑**
```typescript
const handleBatchReplace = async () => {
const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
for (const [key, value] of Object.entries(placeholderValues)) {
if (value) {
await collaboraRef.unoCommands.replaceAll(`{{${key}}}`, value);
await new Promise(resolve => setTimeout(resolve, 100));
}
}
await handleSaveDraft(); // 自动保存
};
```
---
## 技术栈
### 前端
- **Remix** - 全栈框架
- **React** - UI 库
- **TypeScript** - 类型安全
- **Collabora Online** - 文档编辑器
### 后端
- **Node.js** - 运行时
- **PostgreSQL** - 数据库
- **PostgREST** - RESTful API(通过 Supabase 客户端)
- **MinIO** - 对象存储(文件管理)
### 文档处理
- **Collabora Online** - 在线编辑器
- **LibreOffice UNO** - 文档操作命令
- **react-pdf** - PDF 预览(备用)
---
## 部署清单
### ✅ 已完成
1. 数据库表结构设计
2. TypeScript 类型定义
3. 后端服务层实现
4. API 路由创建
5. 前端组件开发
6. CollaboraViewer 增强
7. 完整文档编写
### ⏳ 待完成
1. **执行数据库迁移**
```bash
psql -U postgres -d docreview
\i database/migrations/001_create_drafted_contracts.sql
```
2. **配置测试模板**
- 上传包含占位符的 Word 文档
- 配置 `placeholder_schema` 字段
3. **功能测试**
- 创建草稿流程
- 占位符替换
- 保存和完成
4. **(可选)实现文件复制**
- 参考 `docs/minio-file-copy-implementation.md`
- 安装 `minio` 依赖
- 实现 MinIO 客户端工具
- 完善 `/api/files/copy` 接口
---
## 优势总结
### 架构优势
1. **职责清晰**
- 服务端专注数据管理
- 文件操作独立封装
- 前端负责交互和编辑
2. **灵活性高**
- 支持两种文件处理策略
- 策略切换成本低
- API 预留扩展空间
3. **性能优秀**
- 直接使用模板无文件复制开销
- 前端直连 Collabora,无中间层
- 批量替换在客户端执行
4. **可维护性强**
- 代码模块化
- 类型安全(TypeScript
- 文档完整
---
## 后续规划
### Phase 1(当前)
- ✅ 基础功能实现
- ✅ 策略一(直接使用模板)
- ✅ 文档编写
### Phase 2
- [ ] 草稿列表页面
- [ ] 草稿搜索和筛选
- [ ] 错误处理完善
### Phase 3(按需)
- [ ] 实现文件复制(策略二)
- [ ] 草稿历史版本
- [ ] 导出为 PDF
### Phase 4(长期)
- [ ] 审批流程集成
- [ ] 电子签名集成
- [ ] 模板变量智能推荐
---
## 总结
重新设计后的架构更加清晰合理:
1. **服务端专注数据管理**,不负责文件复制
2. **文件处理策略灵活**,支持两种模式
3. **API 预留扩展空间**,文件复制接口可后续实现
4. **当前实现简单高效**,直接使用模板文件
这种架构既满足当前需求,又为未来扩展留有余地。