From a5ca3a8261d06fba37eeef5eb3eaa14cbdf01afe Mon Sep 17 00:00:00 2001 From: Wren Date: Thu, 11 Sep 2025 17:25:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=88=E5=90=8C=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=80=89=E6=8B=A9PDF=E5=92=8CWord=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E5=B9=B6=E5=AE=9E=E7=8E=B0=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E9=80=BB=E8=BE=91=E5=8F=8A=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/files-upload.ts | 79 ++++++++ app/routes/files.upload.tsx | 193 +++++++++++++++++- docs/合同文档上传与接口说明.md | 357 +++++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+), 11 deletions(-) create mode 100644 docs/合同文档上传与接口说明.md diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index ada0bf2..c8160a1 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -136,6 +136,85 @@ export async function uploadFileToBinary(file: File): Promise { * @param jwtToken JWT token * @returns 上传结果 */ +/** + * 上传合同模板(用于与合同文档结构对比) + * @param file 模板文件 + * @param documentId 源合同文档ID + * @param comparisonId 已有对比记录ID(可选) + * @param jwtToken JWT token + * @returns 上传结果 + */ +export async function uploadContractTemplate( + file: File, + documentId: number, + comparisonId?: number, + jwtToken?: string +): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { + try { + console.log('【合同模板上传】开始上传模板:', { fileName: file.name, documentId, comparisonId }); + + // 创建FormData对象 + const formData = new FormData(); + + // 添加文件 + formData.append('file', file); + + // 添加上传信息 + const uploadInfo = { + document_id: documentId, + ...(comparisonId && { comparison_id: comparisonId }) + }; + + formData.append('upload_info', JSON.stringify(uploadInfo)); + + // 构建请求URL + const uploadUrl = `${UPLOAD_URL}/upload_contract_template`; + console.log('【合同模板上传】准备发送请求到服务器:', uploadUrl); + + // 设置请求头 + const headers: HeadersInit = { + 'Accept': 'application/json' + }; + + if (jwtToken) { + headers['Authorization'] = `Bearer ${jwtToken}`; + } + + // 发送请求 + const response = await fetch(uploadUrl, { + method: 'POST', + headers, + body: formData + }); + + console.log('【合同模板上传】服务器响应状态:', response.status); + + if (!response.ok) { + const errorText = await response.text(); + console.error('【合同模板上传】服务器返回错误:', errorText); + return { + error: `服务器错误: ${response.status} ${response.statusText}`, + status: response.status + }; + } + + const result = await response.json(); + console.log('【合同模板上传】服务器返回结果:', result); + + if (result.success) { + return { data: result.result }; + } else { + return { error: result.error || '合同模板上传失败' }; + } + + } catch (error) { + console.error('【合同模板上传】上传过程中发生错误:', error); + return { + error: error instanceof Error ? error.message : '合同模板上传过程中发生未知错误' + }; + } +} + /** * 合同文档追加附件并合并 * @param documentId 合同文档ID diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 34d8a3e..8fa7ae3 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -17,6 +17,7 @@ import { uploadFileToBinary, uploadDocumentToServer, appendContractAttachments, + uploadContractTemplate, type Document, type DocumentType, type FileUploadResponse, @@ -313,6 +314,11 @@ export default function FilesUpload() { const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite'); const [attachmentRemark, setAttachmentRemark] = useState(""); const [attachmentUploading, setAttachmentUploading] = useState(false); + + // 合同模板上传状态 + const [showTemplateUpload, setShowTemplateUpload] = useState(false); + const [templateFile, setTemplateFile] = useState(null); + const [templateUploading, setTemplateUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); @@ -864,6 +870,75 @@ export default function FilesUpload() { setAttachmentUploading(false); } }; + + // 处理合同模板文件选择 + const handleTemplateFileSelected = (files: FileList) => { + try { + console.log('【合同模板上传】开始处理模板文件选择, 文件数量:', files.length); + + if (files.length > 0) { + const file = files[0]; + // 验证文件类型,支持PDF和Word + const fileName = file.name.toLowerCase(); + const isValidType = + file.type === 'application/pdf' || fileName.endsWith('.pdf') || + file.type === 'application/msword' || fileName.endsWith('.doc') || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx'); + + if (isValidType) { + setTemplateFile(file); + console.log('【合同模板上传】有效文件:', file.name); + } else { + messageService.error('只支持PDF、Word格式的文件', { + title: '文件类型错误', + confirmText: '确定', + cancelText: '', + }); + } + } + } catch (error) { + console.error('【合同模板上传】处理文件选择时发生错误:', error); + } + }; + + // 处理合同模板上传 + const handleTemplateUpload = async () => { + if (!selectedDocumentId || !templateFile) { + toastService.error('请选择文档和模板文件'); + return; + } + + try { + setTemplateUploading(true); + + const result = await uploadContractTemplate( + templateFile, + selectedDocumentId, + undefined, // comparisonId + loaderData.userInfo?.token + ); + + if (result.error) { + throw new Error(result.error); + } + + toastService.success('合同模板上传成功!'); + + // 重置状态 + setTemplateFile(null); + setShowTemplateUpload(false); + setSelectedDocumentId(null); + + // 刷新文档列表 + await filterDocuments(reviewType); + + } catch (error) { + console.error('【合同模板上传】上传失败:', error); + toastService.error(error instanceof Error ? error.message : '合同模板上传失败'); + } finally { + setTemplateUploading(false); + } + }; // 检查并准备上传 const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => { @@ -1785,17 +1860,30 @@ export default function FilesUpload() { 查看 {record.type_id === 1 && record.status === DocumentStatus.PROCESSED && ( - + <> + + + )} ) @@ -2310,6 +2398,89 @@ export default function FilesUpload() { )} + + {/* 合同模板上传模态框 */} + {showTemplateUpload && ( +
+
+
+

上传合同模板

+ +
+ +
+ {/* 文档信息 */} +
+

+ 目标文档ID: {selectedDocumentId} +

+

+ 支持PDF、Word格式,用于与合同文档进行结构对比 +

+
+ + {/* 文件上传区域 */} +
+ + + {templateFile && ( +
+

+ 已选择模板文件 +

+
+ + {templateFile.name} ({formatFileSize(templateFile.size)}) +
+
+ )} +
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ )} ); } diff --git a/docs/合同文档上传与接口说明.md b/docs/合同文档上传与接口说明.md new file mode 100644 index 0000000..9114d4b --- /dev/null +++ b/docs/合同文档上传与接口说明.md @@ -0,0 +1,357 @@ +## 合同文档上传与接口说明(v2) + +本文档详细说明 DocAuditAI 中与“合同文档上传”相关的 API 接口、参数、请求示例、响应示例以及后端处理流程。路径均以后端实际路由挂载为准:所有业务接口统一前缀为 `/admin`,本文档涉及的文档管理接口统一前缀为 `/admin/documents`。 + +--- + +### 鉴权与基础信息 + +- **基础前缀**: `/admin/documents` +- **认证方式**: + - 推荐:`Authorization: Bearer `(后端中间件会将用户信息注入 `req.state.current_user`) + - 参考文档:`docs/接口认证方式统一说明.md` +- **存储后端**: MinIO(参考 `core/storage/minio_client.py` 与 `core/config.py` 中的 `MINIO_CONFIG`) +- **异步任务**: Celery,队列名称默认为 `f"{REDIS_KEY_PREFIX}_tasks"` + +--- + +### 文档类型枚举(后端基准) + +来自 `core/utils/enums.py`: + +- `1` 合同文档(代码: HT,完整中文: 合同文档) +- `2` 行政许可决定书(代码: XZXK,完整中文: 行政许可决定书) +- `3` 行政处罚决定书(代码: XZCF,完整中文: 行政处罚决定书) +- `4` 合同对比模板(代码: HTMB,完整中文: 合同对比模板) +- `99` 其他文档(代码: QT,完整中文: 其他文档) + +> 说明:客户端在 `upload_info` 中通常只需要传 `type_id`,后端会基于枚举推导短代码与中文全称。 + +--- + +## 接口清单 + +#### 1) 上传通用文档(支持合同/许可/处罚/其他) + +- 方法与路径: `POST /admin/documents/upload` +- 表单参数: + - `file`: 文件(支持 PDF、Word:.doc/.docx 会自动转换为 PDF) + - `upload_info`: JSON 字符串,示例: + ```json + { + "type_id": 1, + "document_number": "HT_20250111_120000_001", + "is_test_document": true, + "remark": "示例备注", + "evaluation_level": "普通" + } + ``` + +- cURL 示例: + ```bash + curl -X POST "http://127.0.0.1:8008/admin/documents/upload" \ + -H "Authorization: Bearer " \ + -F "file=@./samples/contract.pdf" \ + -F 'upload_info={"type_id":1,"remark":"示例备注","evaluation_level":"普通"}' + ``` + +- JavaScript fetch 示例: + ```javascript + const form = new FormData(); + form.append('file', fileInput.files[0]); + form.append('upload_info', JSON.stringify({ type_id: 1, remark: '示例备注' })); + + const res = await fetch('/admin/documents/upload', { + method: 'POST', + headers: { Authorization: 'Bearer ' + token }, + body: form + }); + const data = await res.json(); + ``` + +- 响应示例: + ```json + { + "success": true, + "result": { + "id": 123, + "file_name": "contract.pdf", + "file_size": 1024000, + "file_url": "https://minio.example.com/bucket/documents/...", + "type_id": 1, + "type_description": "HT", + "type_full_description": "合同文档", + "document_number": "HT_20250111_120000_001", + "storage_type": "minio", + "background_processing": true, + "api_version": "v2", + "is_test_document": true, + "remark": "示例备注", + "evaluation_level": "普通" + } + } + ``` + +> 说明:合同(type_id=1)、许可(2)、处罚(3)会自动投递 Celery 任务进行 OCR/抽取/评查。 + +--- + +#### 2) 上传合同模板(用于与合同文档结构对比) + +- 方法与路径: `POST /admin/documents/upload_contract_template` +- 表单参数: + - `file`: 模板 PDF/Word(Word将自动转PDF) + - `upload_info`: JSON 字符串,字段: + - `document_id`(必填):源合同文档ID + - `comparison_id`(可选):已有对比记录ID,未提供将自动创建 + +- cURL 示例: + ```bash + curl -X POST "http://127.0.0.1:8008/admin/documents/upload_contract_template" \ + -H "Authorization: Bearer " \ + -F "file=@./samples/contract_template.pdf" \ + -F 'upload_info={"document_id":123}' + ``` + +- 响应示例: + ```json + { + "success": true, + "result": { + "id": 456, // comparison_id(新建或传入) + "document_id": 789, // 模板对应的新建文档ID + "file_name": "contract_template.pdf", + "file_size": 204800, + "file_url": "https://minio.example.com/...", + "template_path": "documents/.../contract_template.pdf", + "template_contract_name": "contract_template.pdf", + "status": "Waiting", + "api_version": "v2" + } + } + ``` + +--- + +#### 3) 合同文档追加附件并合并 + +- 方法与路径: `POST /admin/documents/contracts/{document_id}/append_attachments` +- 路径参数: + - `document_id`: 合同文档ID(type_id 必须为 1) +- 表单参数: + - `files`: 多个附件(支持 PDF、Word(doc/docx)、ZIP、RAR;ZIP/RAR 内仅合并其中的 PDF) + - `merge_mode`: `overwrite`(默认,覆盖原文档路径)或 `new`(新建一条文档记录) + - `is_reprocess`: 是否触发 OCR/抽取/评查(默认 true) + - `remark`: 备注 + +- cURL 示例(支持压缩包作为附件来源): + ```bash + curl -X POST "http://127.0.0.1:8008/admin/documents/contracts/123/append_attachments" \ + -H "Authorization: Bearer " \ + -F "files=@./samples/appendix1.pdf" \ + -F "files=@./samples/attachments.zip" \ + -F "merge_mode=overwrite" -F "is_reprocess=true" -F "remark=附加材料" + ``` + +- 响应示例(new 模式): + ```json + { + "success": true, + "result": { + "id": 901, // 新文档ID + "file_name": "contract_merged_20250111123000.pdf", + "file_size": 512000, + "file_url": "https://minio.example.com/...", + "type_id": 1, + "document_number": "HT_20250111_...", + "storage_type": "minio", + "is_merged": true, + "attachments_count": 2, + "merge_mode": "new", + "background_processing": true, + "api_version": "v2" + } + } + ``` + +--- + +#### 4) 合同模板记录追加附件并合并 + +- 方法与路径: `POST /admin/documents/contract_templates/{comparison_id}/append_attachments` +- 路径参数: + - `comparison_id`: 合同结构对比记录ID +- 表单参数: + - `files`: 多个 PDF 附件 + - `is_reprocess`: 是否触发模板 OCR 与对比(默认 true) + - `remark`: 备注 + +- cURL 示例: + ```bash + curl -X POST "http://127.0.0.1:8008/admin/documents/contract_templates/456/append_attachments" \ + -H "Authorization: Bearer " \ + -F "files=@./samples/template_appendix.pdf" \ + -F "is_reprocess=true" -F "remark=补充分条款" + ``` + +- 响应示例: + ```json + { + "success": true, + "result": { + "id": 456, + "document_id": 790, // 新模板文档ID + "file_name": "template_merged_20250111123500.pdf", + "file_size": 256000, + "file_url": "https://minio.example.com/...", + "template_path": "documents/.../template_merged_20250111123500.pdf", + "status": "Waiting", + "api_version": "v2", + "is_merged": true, + "attachments_count": 1 + } + } + ``` + +--- + +#### 5) 一体化:上传并分配交叉评查任务(支持单 PDF 或压缩包) + +- 方法与路径: `POST /admin/documents/cross_review/documents/upload_and_assign` +- 表单参数: + - `file`: 单个 PDF、Word 或压缩包(zip/rar/7z/tar) + - `upload_info`: JSON 字符串(至少包含 `type_id`) + - `assign_user_ids`: JSON 数组字符串(如 `[1,2,3]`) + +- cURL 示例(批量:zip 内多个 PDF 将逐个入库并触发处理): + ```bash + curl -X POST "/admin/documents/cross_review/documents/upload_and_assign" \ + -H "Authorization: Bearer " \ + -F "file=@./samples/contracts.zip" \ + -F 'upload_info={"type_id":1,"task_name":"自动分配任务","doc_type":"合同文档"}' \ + -F 'assign_user_ids=[1,2,3]' + ``` + +- 响应示例(压缩包): + ```json + { + "success": true, + "message": "已处理3个PDF。", + "pdf_files": ["a.pdf","b.pdf","c.pdf"], + "results": [ {"success": true, "document_id": 101, ... }, ... ], + "assigned_users": { "result": { /* 任务分配详情 */ } } + } + ``` + +--- + +#### 6) 获取文档列表(按用户权限过滤) + +- 方法与路径: `GET /admin/documents/list` +- 查询参数: + - `page`(默认 1)、`page_size`(默认 20,最大 100) + - `search`(可选,模糊匹配 name 或 document_number) + - `type_id`(可选)、`status`(可选) + - 认证后端会基于 `req.state.current_user.user_id` 过滤用户可见文档 + +- cURL 示例: + ```bash + curl -G "/admin/documents/list" \ + -H "Authorization: Bearer " \ + --data-urlencode "page=1" \ + --data-urlencode "page_size=20" \ + --data-urlencode "search=合同" + ``` + +- 响应示例: + ```json + { + "total": 42, + "items": [ + { + "id": 123, + "name": "contract.pdf", + "document_number": "HT_20250111_120000_001", + "type_id": 1, + "status": "Processed", + "upload_time": "2025-01-11T12:01:02", + "created_at": "2025-01-11T12:00:00", + "updated_at": "2025-01-11T12:10:00" + } + ], + "page": 1, + "page_size": 20 + } + ``` + +--- + +## 处理流程说明(后端链路) + +> 关键实现文件:`app/routes/v2/documents/documents.py` + +1. 校验与预处理 + - 校验文件大小(`MAX_FILE_SIZE`)与类型 + - Word 自动转换为 PDF(`core/utils/office_convert.ensure_pdf_from_upload`) + - 生成安全文件名 +2. 生成存储路径并上传 MinIO + - 路径规则:`documents/{INSTANCE_NAME}/{中文类型}/{YYYY}/{MM月DD日}/{文件名_时分秒}/{文件名}` + - 上传后得到 `storage_path` 与 `file_url` +3. 写入数据库(PostgREST) + - 组装文档数据(含 `user_id`、`file_size`、`remark` 等) + - `document_service.async_insert_document` 入库,返回文档ID +4. 追加合并详细逻辑(仅合同附件追加支持压缩包) + - 入口:`POST /admin/documents/contracts/{document_id}/append_attachments` + - 校验:`document_id` 必须为合同文档(type_id=1),否则返回错误 + - 下载原 PDF:从 MinIO 将原文档 `path` 下载到本地临时文件 + - 处理附件输入: + - 支持的外层类型:`.pdf`、`.doc`、`.docx`、`.zip`、`.rar` + - 当为 PDF:直接保存到临时目录,加入合并列表 + - 当为 Word(.doc/.docx):先转换为 PDF(同单文件上传逻辑),再加入合并列表 + - 当为 ZIP/RAR:先落地为临时压缩包,再调用 `document_upload_service.extract_archive` 解压,仅收集其中的 PDF 路径加入合并列表 + - 对于其他类型:返回错误(仅支持 PDF/ZIP/RAR/Word) + - 若最终未收集到任何 PDF:返回错误 + - 合并顺序:始终使用【原PDF】+【附件PDF列表】顺序,调用 `merge_pdfs` 生成合并结果临时文件 + - 目标路径:沿用原 `path` 的父目录,文件名追加 `_merged_{时间戳}.pdf` + - 入库与更新: + - `merge_mode=overwrite`(默认):更新原文档 `path/name/file_size/status=Waiting/remark` + - `merge_mode=new`:调用文档写入流程新建记录,并带上 `remark` + - 触发处理:当 `is_reprocess=true` 时,读入合并后的字节内容,调用 `_submit_ocr_task_v2` 投递后续 OCR/抽取/评查 + - 返回:新/原文档标识、URL、大小、是否合并、附件数量、`merge_mode` 等 +5. 投递异步任务(Celery) + - 合同(1):`process_contract_ocr` + - 行政许可(2)、行政处罚(3):`process_document_ocr_v2` + - 合同模板(4):`process_contract_template_ocr` + - 队列:`{REDIS_KEY_PREFIX}_tasks`,默认过期 24h,time_limit 600s +6. 后处理与状态更新 + - OCR完成后按需进入抽取与评查 + - 文档状态:`Cutting`/`Extractioning`/`Evaluationing`/`Processed`/`Failed` + +--- + +## 错误处理与返回 + +- 统一响应模型: + - 成功:`{"success": true, "result": { ... }}` + - 失败:`{"success": false, "error": "错误消息"}` +- 常见错误: + - `400`:`upload_info` 非法、文件为空或类型不支持 + - `413`:文件过大(超过 `MAX_FILE_SIZE`) + - `500`:存储/数据库/任务投递异常 + +--- + +## 使用建议与注意事项 + +- 合同类建议始终以 PDF 上传,可减少转换耗时与兼容性问题 +- 压缩包仅支持解出 PDF 后逐一入库与处理(zip/rar/7z/tar) +- 建议合理设置 `remark` 与 `evaluation_level` 便于后续筛选 +- 若需追踪异步任务状态,请结合 Flower 与后端日志(`logs/`) + +--- + +## 变更记录 + +- 2025-09-11:首版编制,依据 `app/routes/v2/documents/documents.py` 与相关服务模块撰写 + +