新增合同模板上传功能,支持选择PDF和Word格式文件,并实现上传逻辑及状态管理。
This commit is contained in:
@@ -136,6 +136,85 @@ export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
|||||||
* @param jwtToken JWT token
|
* @param jwtToken JWT token
|
||||||
* @returns 上传结果
|
* @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
|
* @param documentId 合同文档ID
|
||||||
|
|||||||
+182
-11
@@ -17,6 +17,7 @@ import {
|
|||||||
uploadFileToBinary,
|
uploadFileToBinary,
|
||||||
uploadDocumentToServer,
|
uploadDocumentToServer,
|
||||||
appendContractAttachments,
|
appendContractAttachments,
|
||||||
|
uploadContractTemplate,
|
||||||
type Document,
|
type Document,
|
||||||
type DocumentType,
|
type DocumentType,
|
||||||
type FileUploadResponse,
|
type FileUploadResponse,
|
||||||
@@ -313,6 +314,11 @@ export default function FilesUpload() {
|
|||||||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
||||||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||||||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// 合同模板上传状态
|
||||||
|
const [showTemplateUpload, setShowTemplateUpload] = useState<boolean>(false);
|
||||||
|
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||||||
|
const [templateUploading, setTemplateUploading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
||||||
@@ -864,6 +870,75 @@ export default function FilesUpload() {
|
|||||||
setAttachmentUploading(false);
|
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[]) => {
|
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => {
|
||||||
@@ -1785,17 +1860,30 @@ export default function FilesUpload() {
|
|||||||
查看
|
查看
|
||||||
</Button>
|
</Button>
|
||||||
{record.type_id === 1 && record.status === DocumentStatus.PROCESSED && (
|
{record.type_id === 1 && record.status === DocumentStatus.PROCESSED && (
|
||||||
<Button
|
<>
|
||||||
type="primary"
|
<Button
|
||||||
size="small"
|
type="primary"
|
||||||
icon="ri-attachment-line"
|
size="small"
|
||||||
onClick={() => {
|
icon="ri-attachment-line"
|
||||||
setSelectedDocumentId(record.id);
|
onClick={() => {
|
||||||
setShowAttachmentUpload(true);
|
setSelectedDocumentId(record.id);
|
||||||
}}
|
setShowAttachmentUpload(true);
|
||||||
>
|
}}
|
||||||
追加附件
|
>
|
||||||
</Button>
|
追加附件
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
icon="ri-file-copy-line"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDocumentId(record.id);
|
||||||
|
setShowTemplateUpload(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
上传模板
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -2310,6 +2398,89 @@ export default function FilesUpload() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 合同模板上传模态框 */}
|
||||||
|
{showTemplateUpload && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-lg mx-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">上传合同模板</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowTemplateUpload(false);
|
||||||
|
setSelectedDocumentId(null);
|
||||||
|
setTemplateFile(null);
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<i className="ri-close-line text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 文档信息 */}
|
||||||
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
支持PDF、Word格式,用于与合同文档进行结构对比
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文件上传区域 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
选择模板文件 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<UploadArea
|
||||||
|
onFilesSelected={handleTemplateFileSelected}
|
||||||
|
multiple={false}
|
||||||
|
accept=".pdf,.doc,.docx"
|
||||||
|
tipText="支持PDF、Word格式"
|
||||||
|
mainText="选择模板文件"
|
||||||
|
buttonText="选择文件"
|
||||||
|
icon="ri-file-copy-line"
|
||||||
|
/>
|
||||||
|
{templateFile && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-green-600 mb-2">
|
||||||
|
<i className="ri-checkbox-circle-line"></i> 已选择模板文件
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||||||
|
<i className="ri-file-line mr-1"></i>
|
||||||
|
{templateFile.name} ({formatFileSize(templateFile.size)})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
onClick={() => {
|
||||||
|
setShowTemplateUpload(false);
|
||||||
|
setSelectedDocumentId(null);
|
||||||
|
setTemplateFile(null);
|
||||||
|
}}
|
||||||
|
disabled={templateUploading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleTemplateUpload}
|
||||||
|
disabled={!templateFile || templateUploading}
|
||||||
|
icon={templateUploading ? "ri-loader-4-line" : "ri-upload-cloud-line"}
|
||||||
|
>
|
||||||
|
{templateUploading ? '上传中...' : '开始上传'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
## 合同文档上传与接口说明(v2)
|
||||||
|
|
||||||
|
本文档详细说明 DocAuditAI 中与“合同文档上传”相关的 API 接口、参数、请求示例、响应示例以及后端处理流程。路径均以后端实际路由挂载为准:所有业务接口统一前缀为 `/admin`,本文档涉及的文档管理接口统一前缀为 `/admin/documents`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 鉴权与基础信息
|
||||||
|
|
||||||
|
- **基础前缀**: `/admin/documents`
|
||||||
|
- **认证方式**:
|
||||||
|
- 推荐:`Authorization: Bearer <JWT>`(后端中间件会将用户信息注入 `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 <YOUR_TOKEN>" \
|
||||||
|
-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 <YOUR_TOKEN>" \
|
||||||
|
-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 <YOUR_TOKEN>" \
|
||||||
|
-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 <YOUR_TOKEN>" \
|
||||||
|
-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 <YOUR_TOKEN>" \
|
||||||
|
-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 <YOUR_TOKEN>" \
|
||||||
|
--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` 与相关服务模块撰写
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user