all in
This commit is contained in:
@@ -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. **当前实现简单高效**,直接使用模板文件
|
||||
|
||||
这种架构既满足当前需求,又为未来扩展留有余地。
|
||||
Reference in New Issue
Block a user