514 lines
12 KiB
Markdown
514 lines
12 KiB
Markdown
# 合同起草功能架构说明 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. **当前实现简单高效**,直接使用模板文件
|
||
|
||
这种架构既满足当前需求,又为未来扩展留有余地。
|