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

12 KiB
Raw Blame History

合同起草功能架构说明 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

文件处理策略

策略一:直接使用模板文件(默认)

适用场景

  • 快速上线,简单实现
  • 用户访问量不高,编辑冲突概率低
  • 节省存储空间

实现方式

// 创建草稿时,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 编辑会话可能互相干扰

策略二:复制模板文件(可选)

适用场景

  • 需要完全隔离的草稿文件
  • 多用户并发编辑同一模板
  • 需要保留编辑历史

实现方式

// 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

请求

{
  "templateId": 123,
  "title": "采购合同-20250104",
  "draftFilePath": "drafts/contract_123_1_1704355200000.docx"  // 可选
}

处理逻辑

  1. 验证用户身份
  2. 查询模板信息
  3. 确定文件路径:
    • 如果传了 draftFilePath,使用它
    • 否则使用 template.file_path
  4. 创建草稿记录
  5. 返回草稿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

处理逻辑

  1. 验证用户权限
  2. 更新 status = 'completed'
  3. 更新 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 预览(备用)

部署清单

已完成

  1. 数据库表结构设计
  2. TypeScript 类型定义
  3. 后端服务层实现
  4. API 路由创建
  5. 前端组件开发
  6. CollaboraViewer 增强
  7. 完整文档编写

待完成

  1. 执行数据库迁移

    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. 当前实现简单高效,直接使用模板文件

这种架构既满足当前需求,又为未来扩展留有余地。