Files
leaudit-platform-frontend/docs/contract-drafting-architecture-v2.md
T
2025-12-05 00:09:32 +08:00

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