12 KiB
12 KiB
合同起草功能架构说明 v2.0
架构调整
根据实际需求,重新设计了服务端职责和文件处理策略。
核心理念
模板文档已包含占位符
前提条件:
- 模板文档已在 MinIO 上存在
- 文档中已包含约定格式的占位符(如
{{甲方名称}}) - 占位符配置已存储在
contract_templates.placeholder_schema
因此:
- 服务端主要负责数据管理(草稿记录、占位符值)
- 文件操作交给专门的接口处理(可选)
- 前端直接与 Collabora 和 MinIO 交互
服务端职责
✅ 核心职责
-
草稿记录管理
- 创建草稿记录(
drafted_contracts表) - 记录占位符填写的值(
placeholder_values字段) - 更新草稿状态(draft → completed → archived)
- 创建草稿记录(
-
数据查询服务
- 查询用户的草稿列表
- 查询草稿详情
- 查询模板信息
-
业务逻辑处理
- 必填字段验证
- 权限检查(只能操作自己的草稿)
- 状态流转控制
❌ 非职责
文件复制(由专门的文件服务接口处理)MinIO 直接操作(封装在独立模块)文档渲染(Collabora Online)占位符替换(前端调用 Collabora API)
文件处理策略
策略一:直接使用模板文件(默认)
适用场景:
- 快速上线,简单实现
- 用户访问量不高,编辑冲突概率低
- 节省存储空间
实现方式:
// 创建草稿时,file_path 直接使用模板路径
const draft = await createDraftContract({
templateId: 123,
title: '采购合同',
// 不传 draftFilePath
}, userId);
// draft.file_path = template.file_path
工作流程:
- 用户点击"起草合同"
- 创建草稿记录,
file_path= 模板路径 - 用户在 Collabora 中编辑模板文件
- 替换占位符
- 保存草稿(保存 placeholder_values)
- 完成起草
优点:
- ✅ 无需复制文件,速度快
- ✅ 节省存储空间
- ✅ 实现简单
缺点:
- ❌ 多个用户同时编辑同一模板可能冲突
- ❌ Collabora 编辑会话可能互相干扰
策略二:复制模板文件(可选)
适用场景:
- 需要完全隔离的草稿文件
- 多用户并发编辑同一模板
- 需要保留编辑历史
实现方式:
// 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);
工作流程:
- 用户点击"起草合同"
- 前端调用
/api/files/copy复制文件 - 创建草稿记录,
file_path= 复制后的路径 - 用户在 Collabora 中编辑新文件
- 替换占位符
- 完成起草
优点:
- ✅ 每个草稿独立文件,互不干扰
- ✅ 保留原始模板不被修改
- ✅ 可以保留编辑历史
缺点:
- ❌ 需要复制文件,增加存储空间
- ❌ 复制操作增加响应时间
数据流架构
创建草稿流程
用户操作
↓
前端: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
请求:
{
"templateId": 123,
"title": "采购合同-20250104",
"draftFilePath": "drafts/contract_123_1_1704355200000.docx" // 可选
}
处理逻辑:
- 验证用户身份
- 查询模板信息
- 确定文件路径:
- 如果传了
draftFilePath,使用它 - 否则使用
template.file_path
- 如果传了
- 创建草稿记录
- 返回草稿ID和路径
响应:
{
"id": 456,
"filePath": "drafts/contract_123_1_1704355200000.docx",
"title": "采购合同-20250104",
"templateId": 123
}
2. 文件复制(预留接口)
端点:POST /api/files/copy
请求:
{
"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
请求:
{
"placeholders": {
"甲方名称": "广东省烟草专卖局",
"合同金额": "500000"
}
}
4. 完成起草
端点:POST /api/contracts/draft/:id/complete
处理逻辑:
- 验证用户权限
- 更新
status = 'completed' - 更新
updated_at
数据库设计
drafted_contracts 表
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
关键代码:
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
布局:
<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>
批量替换逻辑:
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 预览(备用)
部署清单
✅ 已完成
- 数据库表结构设计
- TypeScript 类型定义
- 后端服务层实现
- API 路由创建
- 前端组件开发
- CollaboraViewer 增强
- 完整文档编写
⏳ 待完成
-
执行数据库迁移
psql -U postgres -d docreview \i database/migrations/001_create_drafted_contracts.sql -
配置测试模板
- 上传包含占位符的 Word 文档
- 配置
placeholder_schema字段
-
功能测试
- 创建草稿流程
- 占位符替换
- 保存和完成
-
(可选)实现文件复制
- 参考
docs/minio-file-copy-implementation.md - 安装
minio依赖 - 实现 MinIO 客户端工具
- 完善
/api/files/copy接口
- 参考
优势总结
架构优势
-
职责清晰
- 服务端专注数据管理
- 文件操作独立封装
- 前端负责交互和编辑
-
灵活性高
- 支持两种文件处理策略
- 策略切换成本低
- API 预留扩展空间
-
性能优秀
- 直接使用模板无文件复制开销
- 前端直连 Collabora,无中间层
- 批量替换在客户端执行
-
可维护性强
- 代码模块化
- 类型安全(TypeScript)
- 文档完整
后续规划
Phase 1(当前)
- ✅ 基础功能实现
- ✅ 策略一(直接使用模板)
- ✅ 文档编写
Phase 2
- 草稿列表页面
- 草稿搜索和筛选
- 错误处理完善
Phase 3(按需)
- 实现文件复制(策略二)
- 草稿历史版本
- 导出为 PDF
Phase 4(长期)
- 审批流程集成
- 电子签名集成
- 模板变量智能推荐
总结
重新设计后的架构更加清晰合理:
- 服务端专注数据管理,不负责文件复制
- 文件处理策略灵活,支持两种模式
- API 预留扩展空间,文件复制接口可后续实现
- 当前实现简单高效,直接使用模板文件
这种架构既满足当前需求,又为未来扩展留有余地。