Files
leaudit-platform-frontend/auth_doc/交叉评查新增功能设计(2).md
LiangShiyong 1658bb1c6f feat: 1. 重构交叉评查任务的文档列表的显示,对接接口查询当前任务的文档相关信息。
2.文档上传通过接口去查询是否存在同名的文件,做上传前拦截提示。
3.交叉评查的评查结果也同步添加企查查的企业信息查询模块。
4. 封装上传附件和上传模板的模态框的组件,在交叉评查的文档列表中引入显示。
5. 交叉评查的评查结果中关于合同类型的文档同步加入结构比对的功能。
2025-12-13 07:18:37 +08:00

1242 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交叉评查模块 - 新增功能设计文档
> 本文档从《交叉评查接口文档.md》中提取关键接口设计,供开发参考。
## 开发状态
|功能 |状态 |完成日期 |
|---|---|---|
|任务文档列表(含版本归纳) |✅ 已完成 |2025-12-13 |
|追加附件 |✅ 已完成 |2025-12-13 |
|模板对比功能接口组 |✅ 已完成 |2025-12-13 |
**数据库变更**`cross_examination_tasks` 表新增 `template_comparison_id` 字段
**相关文件**
- 路由层:`app/routes/v2/crossreview/cross_review.py`
- 服务层:`services/documents/v2/cross_review_service.py`
- Celery任务:`services/documents/tasks.py`
---
### 9. 获取任务下文档列表(支持版本归纳)✅
**GET** `/api/v2/cross_review/tasks/{task_id}/documents`
获取指定任务下的文档列表,**支持版本归纳功能**。同一任务内同名且同类型的文档会被归纳为版本组,最新上传的为当前版本,其余为历史版本。**仅任务参与者可访问**。
> **数据结构对齐**: 返回格式与 `GET /admin/versions/documents-list` 保持一致,便于前端复用组件。
**权限**: `cross_review:task:read`
#### 路径参数
|参数 |类型 |说明 |
|---|---|---|
|`task_id` |int |任务ID(必须大于0 |
#### 请求参数
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`page` |int |否 |1 |页码(从1开始) |
|`page_size` |int |否 |10 |每页数量(最大100 |
|`keyword` |string |否 |null |模糊搜索关键字(匹配文件名称或文档编号) |
#### 筛选逻辑说明
`keyword` 模糊搜索:
- 使用 `ILIKE` 模糊匹配,**不区分大小写**
- 同时匹配两个字段(**OR 关系**):
- `documents.name` - 文件名称
- `documents.document_number` - 文档编号/文书号
- 筛选作用于**当前版本**,如果当前版本匹配则显示该版本组(包含所有历史版本)
**筛选与版本的关系**
```
场景:任务下有2个版本组
├── 车辆维保合同.docx (v2, v1)
└── 设备维保合同.docx (v1)
用户搜索 keyword="车辆"
└── 只返回"车辆维保合同.docx"这一组,包含v2和v1两个版本
```
#### 请求示例
**示例1:获取全部文档(分页)**
```json
{
"page": 1,
"page_size": 10
}
```
**示例2:模糊搜索**
```json
{
"page": 1,
"page_size": 10,
"keyword": "维保合同"
}
```
#### 版本归纳逻辑
**核心规则**
1. **分组条件**: 按 `(name, type_id)` 分组(同名且同类型的文档归为一组)
2. **版本排序**: 同组内按 `created_at` 降序排列
3. **当前版本**: 每组中最新上传的文档为当前版本
4. **历史版本**: 每组中其余文档为历史版本
5. **版本号计算**: 按 `created_at` 升序自动编号(1, 2, 3...),最新的版本号最大
**示例场景**
```
任务 208 关联的文档(cross_task_document_mapping:
├── document_id=2157, name="车辆维保合同范本.docx", type_id=1, created_at=2025-12-11
├── document_id=2200, name="车辆维保合同范本.docx", type_id=1, created_at=2025-12-13 ← 继续上传
└── document_id=2199, name="设备维保合同范本.docx", type_id=1, created_at=2025-12-13
按 (name, type_id) 分组后:
├── ("车辆维保合同范本.docx", 1) → 当前版本: 2200(v2), 历史版本: [2157(v1)]
└── ("设备维保合同范本.docx", 1) → 当前版本: 2199(v1), 历史版本: []
返回文档数: 2(按分组后的唯一文档计数)
```
#### 业务逻辑
1. **权限验证**
- 验证当前用户是否是任务的参与者(在 `user_ids` 中)
2. **文档查询**
-`cross_task_document_mapping` 获取任务关联的所有文档ID
- 关联 `documents` 表获取文档详情
- 应用筛选条件(类型、日期、关键字)
3. **版本分组**
-`(name, type_id)` 分组
- 每组内按 `created_at` 降序排列
- 第一个为当前版本,其余为历史版本
4. **分页处理**
- 分页基于**分组后的文档数**(唯一文档),不是原始记录数
- 例如:10个文档记录,分组后5个唯一文档,total=5
5. **评查统计**(批量计算)
- 统计当前版本和所有历史版本的评查结果
- 计算通过/警告/错误/人工审核数量
- 获取问题消息列表
- 计算最终得分和满分
#### 响应示例
```json
{
"total": 2,
"page": 1,
"page_size": 10,
"total_pages": 1,
"documents": [
{
"id": 2200,
"name": "车辆维保合同范本.docx",
"path": "/documents/2025/12/2200.docx",
"version_number": 2,
"created_at": "2025-12-13T10:00:00Z",
"status": "Processed",
"file_size": 25018,
"document_number": "HT-2024-001",
"type_id": 1,
"type_name": "合同",
"upload_time": "2025-12-13T10:00:00Z",
"audit_status": 0,
"total_evaluation_points": 55,
"pass_count": 51,
"warning_count": 1,
"error_count": 3,
"manual_count": 2,
"issue_count": 4,
"warning_messages": ["日期格式不规范"],
"error_messages": ["缺少当事人签名", "金额大写与小写不一致", "缺少合同编号"],
"issue_messages": ["缺少当事人签名", "金额大写与小写不一致"],
"manual_messages": ["骑缝章识别效果不佳,请人工核对", "签名真实性需人工确认"],
"final_score": 85.5,
"full_score": 100,
"score_summary": "85.5/100",
"score_percent": 85.5,
"total_versions": 2,
"history_versions": [
{
"id": 2157,
"version_number": 1,
"created_at": "2025-12-11T17:02:35Z",
"upload_time": "2025-12-11T17:02:35Z",
"file_size": 25018,
"path": "/documents/2025/12/2157.docx",
"status": "Failed",
"document_number": null,
"type_id": 1,
"type_name": "合同",
"audit_status": 0,
"total_evaluation_points": 55,
"pass_count": 40,
"warning_count": 5,
"error_count": 10,
"manual_count": 0,
"issue_count": 15,
"warning_messages": ["日期格式不规范", "条款顺序混乱"],
"error_messages": ["缺少甲方签名", "合同金额错误"],
"issue_messages": ["缺少甲方签名", "合同金额错误"],
"manual_messages": ["盖章位置需人工确认"],
"final_score": 72.5,
"full_score": 100,
"score_summary": "72.5/100",
"score_percent": 72.5
}
]
},
{
"id": 2199,
"name": "设备维保合同范本.docx",
"path": "/documents/2025/12/2199.docx",
"version_number": 1,
"created_at": "2025-12-13T09:30:00Z",
"status": "Processed",
"file_size": 26686,
"document_number": "SB-2024-001",
"type_id": 1,
"type_name": "合同",
"upload_time": "2025-12-13T09:30:00Z",
"audit_status": 1,
"total_evaluation_points": 55,
"pass_count": 55,
"warning_count": 0,
"error_count": 0,
"manual_count": 0,
"issue_count": 0,
"warning_messages": [],
"error_messages": [],
"issue_messages": [],
"manual_messages": [],
"final_score": 100,
"full_score": 100,
"score_summary": "100/100",
"score_percent": 100,
"total_versions": 1,
"history_versions": []
}
]
}
```
#### 数据结构定义
##### TypeScript 类型定义
```typescript
/**
* 交叉评查任务文档列表响应
*/
interface CrossReviewDocumentListResponse {
/** 总文档数(按版本分组后的唯一文档数) */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 总页数 */
total_pages: number;
/** 文档列表 */
documents: CrossReviewDocumentWithVersion[];
}
/**
* 文档信息(含版本和评查统计)
*/
interface CrossReviewDocumentWithVersion {
// ========== 基本信息 ==========
/** 当前版本的文档ID */
id: number;
/** 文档名称 */
name: string;
/** 文件存储路径 */
path: string;
/** 当前版本号(最大值,从1开始) */
version_number: number;
/** 创建时间(ISO 8601格式) */
created_at: string; // "2025-12-13T10:00:00Z"
/** 文档处理状态 */
status: "Waiting" | "Cutting" | "Extractioning" | "Evaluationing" | "Failed" | "Processed";
/** 文件大小(字节) */
file_size: number;
/** 文书号/文档编号(可为null) */
document_number: string | null;
/** 文档类型ID */
type_id: number;
/** 文档类型名称 */
type_name: string;
/** 上传时间(ISO 8601格式) */
upload_time: string;
// ========== 任务内评查状态 ==========
/**
* 任务内评查完成状态(来自 cross_task_document_mapping 表)
*
* ⚠️ 注意:此字段与 documents 表的 audit_status 含义不同!
* - cross_task_document_mapping.audit_status: 0=未评查, 1=已评查(本接口使用)
* - documents.audit_status: -2~2 的审核状态(普通文档列表使用)
*
* 0: 未评查(任务内该文档尚未完成评查)
* 1: 已评查(任务内该文档已确认完成评查)
*/
audit_status: 0 | 1;
// ========== 评查统计 ==========
/** 总评查点数 */
total_evaluation_points: number;
/** 通过的评查点数量 */
pass_count: number;
/** 警告的评查点数量(severity=warning或info */
warning_count: number;
/** 错误的评查点数量(severity=error */
error_count: number;
/** 需人工审核的评查点数量(post_action=manual */
manual_count: number;
/** 问题总数(warning_count + error_count */
issue_count: number;
// ========== 评查消息列表 ==========
/** 警告消息列表 */
warning_messages: string[];
/** 错误消息列表 */
error_messages: string[];
/** 问题消息列表(综合:警告+错误) */
issue_messages: string[];
/** 需人工确认的消息列表 */
manual_messages: string[];
// ========== 交叉评查特有:分数信息 ==========
/** 最终得分(经过提案投票调整后的分数) */
final_score: number;
/** 满分 */
full_score: number;
/** 得分摘要(如 "85.5/100" */
score_summary: string;
/** 得分百分比(0-100 */
score_percent: number;
// ========== 版本信息 ==========
/** 总版本数 */
total_versions: number;
/** 历史版本列表(按created_at降序,不包含当前版本) */
history_versions: HistoryVersion[];
}
/**
* 历史版本信息
*
* 每个历史版本都有独立的:
* - 评查统计(pass_count, warning_count, error_count, manual_count
* - 评查消息列表(warning_messages, error_messages, issue_messages, manual_messages
* - 分数信息(final_score, full_score, score_summary, score_percent
* - 任务内评查状态(audit_status
*/
interface HistoryVersion {
// ========== 基本信息 ==========
/** 历史版本的文档ID */
id: number;
/** 版本号(从1开始,数字越小越旧) */
version_number: number;
/** 创建时间(ISO 8601格式) */
created_at: string;
/** 文件大小(字节) */
file_size: number;
/** 文件存储路径 */
path: string;
/** 文档处理状态 */
status: "Waiting" | "Cutting" | "Extractioning" | "Evaluationing" | "Failed" | "Processed";
/** 文书号/文档编号(可为null) */
document_number: string | null;
/** 文档类型ID */
type_id: number;
/** 文档类型名称 */
type_name: string;
/** 上传时间(ISO 8601格式) */
upload_time: string;
// ========== 任务内评查状态(每个版本独立) ==========
/**
* 任务内评查完成状态(来自 cross_task_document_mapping 表)
* 0: 未评查
* 1: 已评查
*/
audit_status: 0 | 1;
// ========== 评查统计(每个版本独立) ==========
/** 总评查点数 */
total_evaluation_points: number;
/** 通过的评查点数量 */
pass_count: number;
/** 警告的评查点数量 */
warning_count: number;
/** 错误的评查点数量 */
error_count: number;
/** 需人工审核的评查点数量 */
manual_count: number;
/** 问题总数 */
issue_count: number;
// ========== 评查消息列表(每个版本独立) ==========
/** 警告消息列表 */
warning_messages: string[];
/** 错误消息列表 */
error_messages: string[];
/** 问题消息列表(综合:警告+错误) */
issue_messages: string[];
/** 需人工确认的消息列表 */
manual_messages: string[];
// ========== 交叉评查特有:分数信息(每个版本独立) ==========
/** 最终得分(经过提案投票调整后) */
final_score: number;
/** 满分 */
full_score: number;
/** 得分摘要(如 "85.5/100" */
score_summary: string;
/** 得分百分比(0-100 */
score_percent: number;
}
```
##### JSON Schema 定义
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CrossReviewDocumentListResponse",
"description": "交叉评查任务文档列表响应",
"type": "object",
"required": ["total", "page", "page_size", "total_pages", "documents"],
"properties": {
"total": {
"type": "integer",
"description": "总文档数(按版本分组后的唯一文档数)",
"minimum": 0
},
"page": {
"type": "integer",
"description": "当前页码",
"minimum": 1
},
"page_size": {
"type": "integer",
"description": "每页数量",
"minimum": 1,
"maximum": 100
},
"total_pages": {
"type": "integer",
"description": "总页数",
"minimum": 0
},
"documents": {
"type": "array",
"description": "文档列表",
"items": { "$ref": "#/definitions/CrossReviewDocumentWithVersion" }
}
},
"definitions": {
"CrossReviewDocumentWithVersion": {
"type": "object",
"description": "文档信息(含版本和评查统计)",
"required": [
"id", "name", "path", "version_number", "created_at", "status",
"file_size", "type_id", "type_name", "audit_status",
"total_evaluation_points", "pass_count", "warning_count", "error_count",
"manual_count", "issue_count", "warning_messages", "error_messages",
"issue_messages", "manual_messages", "final_score", "full_score",
"score_summary", "score_percent", "total_versions", "history_versions"
],
"properties": {
"id": { "type": "integer", "description": "当前版本的文档ID" },
"name": { "type": "string", "description": "文档名称" },
"path": { "type": "string", "description": "文件存储路径" },
"version_number": { "type": "integer", "description": "当前版本号", "minimum": 1 },
"created_at": { "type": "string", "format": "date-time", "description": "创建时间" },
"status": {
"type": "string",
"enum": ["Waiting", "Cutting", "Extractioning", "Evaluationing", "Failed", "Processed"],
"description": "文档处理状态"
},
"file_size": { "type": "integer", "description": "文件大小(字节)", "minimum": 0 },
"document_number": { "type": ["string", "null"], "description": "文书号/文档编号" },
"type_id": { "type": "integer", "description": "文档类型ID" },
"type_name": { "type": "string", "description": "文档类型名称" },
"upload_time": { "type": "string", "format": "date-time", "description": "上传时间" },
"audit_status": {
"type": "integer",
"enum": [0, 1],
"description": "任务内评查完成状态:0=未评查, 1=已评查(来自cross_task_document_mapping表)"
},
"total_evaluation_points": { "type": "integer", "description": "总评查点数", "minimum": 0 },
"pass_count": { "type": "integer", "description": "通过的评查点数量", "minimum": 0 },
"warning_count": { "type": "integer", "description": "警告的评查点数量", "minimum": 0 },
"error_count": { "type": "integer", "description": "错误的评查点数量", "minimum": 0 },
"manual_count": { "type": "integer", "description": "需人工审核的评查点数量", "minimum": 0 },
"issue_count": { "type": "integer", "description": "问题总数", "minimum": 0 },
"warning_messages": {
"type": "array",
"items": { "type": "string" },
"description": "警告消息列表"
},
"error_messages": {
"type": "array",
"items": { "type": "string" },
"description": "错误消息列表"
},
"issue_messages": {
"type": "array",
"items": { "type": "string" },
"description": "问题消息列表"
},
"manual_messages": {
"type": "array",
"items": { "type": "string" },
"description": "需人工确认的消息列表"
},
"final_score": { "type": "number", "description": "最终得分", "minimum": 0 },
"full_score": { "type": "number", "description": "满分", "minimum": 0 },
"score_summary": { "type": "string", "description": "得分摘要", "example": "85.5/100" },
"score_percent": { "type": "number", "description": "得分百分比", "minimum": 0, "maximum": 100 },
"total_versions": { "type": "integer", "description": "总版本数", "minimum": 1 },
"history_versions": {
"type": "array",
"items": { "$ref": "#/definitions/HistoryVersion" },
"description": "历史版本列表"
}
}
},
"HistoryVersion": {
"type": "object",
"description": "历史版本信息(每个版本独立的评查统计、消息列表、分数信息)",
"required": [
"id", "version_number", "created_at", "file_size", "path", "status",
"type_id", "type_name", "upload_time", "audit_status",
"total_evaluation_points", "pass_count", "warning_count", "error_count", "manual_count", "issue_count",
"warning_messages", "error_messages", "issue_messages", "manual_messages",
"final_score", "full_score", "score_summary", "score_percent"
],
"properties": {
"id": { "type": "integer", "description": "历史版本的文档ID" },
"version_number": { "type": "integer", "description": "版本号(从1开始,数字越小越旧)", "minimum": 1 },
"created_at": { "type": "string", "format": "date-time", "description": "创建时间" },
"file_size": { "type": "integer", "description": "文件大小(字节)", "minimum": 0 },
"path": { "type": "string", "description": "文件存储路径" },
"status": {
"type": "string",
"enum": ["Waiting", "Cutting", "Extractioning", "Evaluationing", "Failed", "Processed"],
"description": "文档处理状态"
},
"document_number": { "type": ["string", "null"], "description": "文书号/文档编号" },
"type_id": { "type": "integer", "description": "文档类型ID" },
"type_name": { "type": "string", "description": "文档类型名称" },
"upload_time": { "type": "string", "format": "date-time", "description": "上传时间" },
"audit_status": {
"type": "integer",
"enum": [0, 1],
"description": "任务内评查完成状态:0=未评查, 1=已评查(来自cross_task_document_mapping表)"
},
"total_evaluation_points": { "type": "integer", "description": "总评查点数", "minimum": 0 },
"pass_count": { "type": "integer", "description": "通过的评查点数量", "minimum": 0 },
"warning_count": { "type": "integer", "description": "警告的评查点数量", "minimum": 0 },
"error_count": { "type": "integer", "description": "错误的评查点数量", "minimum": 0 },
"manual_count": { "type": "integer", "description": "需人工审核的评查点数量", "minimum": 0 },
"issue_count": { "type": "integer", "description": "问题总数", "minimum": 0 },
"warning_messages": {
"type": "array",
"items": { "type": "string" },
"description": "警告消息列表"
},
"error_messages": {
"type": "array",
"items": { "type": "string" },
"description": "错误消息列表"
},
"issue_messages": {
"type": "array",
"items": { "type": "string" },
"description": "问题消息列表"
},
"manual_messages": {
"type": "array",
"items": { "type": "string" },
"description": "需人工确认的消息列表"
},
"final_score": { "type": "number", "description": "最终得分", "minimum": 0 },
"full_score": { "type": "number", "description": "满分", "minimum": 0 },
"score_summary": { "type": "string", "description": "得分摘要(如 85.5/100" },
"score_percent": { "type": "number", "description": "得分百分比", "minimum": 0, "maximum": 100 }
}
}
}
}
```
---
#### 返回字段说明
##### 顶层字段
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`total` |int |是 |- |总文档数(按版本分组后的唯一文档数) |
|`page` |int |是 |- |当前页码 |
|`page_size` |int |是 |- |每页数量 |
|`total_pages` |int |是 |- |总页数 |
|`documents` |array |是 |`[]` |文档列表 |
##### documents 数组元素(当前版本信息)
**基本信息字段:**
|字段 |类型 |必填 |可空 |说明 |
|---|---|---|---|---|
|`id` |int |是 |否 |当前版本的文档ID |
|`name` |string |是 |否 |文档名称 |
|`path` |string |是 |否 |文件存储路径 |
|`version_number` |int |是 |否 |当前版本号(最大值,从1开始) |
|`created_at` |datetime |是 |否 |创建时间(ISO 8601格式) |
|`status` |string |是 |否 |文档处理状态,枚举值见下方 |
|`file_size` |int |是 |否 |文件大小(字节) |
|`document_number` |string |是 |**是** |文书号/文档编号,可能为 `null` |
|`type_id` |int |是 |否 |文档类型ID |
|`type_name` |string |是 |否 |文档类型名称 |
|`upload_time` |datetime |是 |否 |上传时间(ISO 8601格式) |
**任务内评查状态字段:**
|字段 |类型 |必填 |说明 |
|---|---|---|---|
|`audit_status` |int |是 |任务内评查完成状态(来自 `cross_task_document_mapping` 表):<br>• `0` = 未评查<br>• `1` = 已评查 |
> ⚠️ **注意**:此字段与 `documents` 表的 `audit_status`-2~2 的审核状态)含义不同!本接口返回的是任务内的评查完成状态。
**评查统计字段:**
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`total_evaluation_points` |int |是 |`0` |总评查点数 |
|`pass_count` |int |是 |`0` |通过的评查点数量 |
|`warning_count` |int |是 |`0` |警告的评查点数量(severity=warning或info |
|`error_count` |int |是 |`0` |错误的评查点数量(severity=error |
|`manual_count` |int |是 |`0` |需人工审核的评查点数量(post_action=manual |
|`issue_count` |int |是 |`0` |问题总数(warning_count + error_count |
**评查消息列表字段:**
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`warning_messages` |array[string] |是 |`[]` |警告消息列表 |
|`error_messages` |array[string] |是 |`[]` |错误消息列表 |
|`issue_messages` |array[string] |是 |`[]` |问题消息列表(综合) |
|`manual_messages` |array[string] |是 |`[]` |需人工确认的消息列表 |
**分数信息字段(交叉评查特有):**
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`final_score` |float |是 |`0` |最终得分(经过提案投票调整后) |
|`full_score` |float |是 |`0` |满分 |
|`score_summary` |string |是 |`"0/0"` |得分摘要(如 "85.5/100" |
|`score_percent` |float |是 |`0` |得分百分比(0-100 |
**版本信息字段:**
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`total_versions` |int |是 |`1` |总版本数 |
|`history_versions` |array |是 |`[]` |历史版本列表 |
##### history_versions 数组元素
|字段 |类型 |必填 |可空 |说明 |
|---|---|---|---|---|
|`id` |int |是 |否 |历史版本的文档ID |
|`version_number` |int |是 |否 |版本号(从1开始,数字越小越旧) |
|`created_at` |datetime |是 |否 |创建时间 |
|`upload_time` |datetime |是 |否 |上传时间(与created_at相同) |
|`file_size` |int |是 |否 |文件大小(字节) |
|`path` |string |是 |否 |文件存储路径 |
|`status` |string |是 |否 |文档处理状态 |
|`document_number` |string |是 |**是** |文书号/文档编号 |
|`type_id` |int |是 |否 |文档类型ID |
|`type_name` |string |是 |否 |文档类型名称 |
|`audit_status` |int |是 |否 |评查完成状态(0=未评查,1=已评查),来自 cross_task_document_mapping 表 |
|`total_evaluation_points` |int |是 |否 |总评查点数 |
|`pass_count` |int |是 |否 |通过的评查点数量 |
|`warning_count` |int |是 |否 |警告的评查点数量 |
|`error_count` |int |是 |否 |错误的评查点数量 |
|`manual_count` |int |是 |否 |需人工审核的评查点数量 |
|`issue_count` |int |是 |否 |问题总数(warning_count + error_count |
|`warning_messages` |array<string> |是 |否 |警告消息列表 |
|`error_messages` |array<string> |是 |否 |错误消息列表 |
|`issue_messages` |array<string> |是 |否 |问题消息列表(warning + error 合并去重) |
|`manual_messages` |array<string> |是 |否 |需人工审核的消息列表 |
|`final_score` |float |是 |否 |最终得分 |
|`full_score` |float |是 |否 |满分 |
|`score_summary` |string |是 |否 |得分摘要(如 "85.5/100" |
|`score_percent` |float |是 |否 |得分百分比(0-100 |
---
#### 枚举值定义
##### status 文档处理状态
|值 |说明 |
|---|---|
|`Waiting` |等待处理 |
|`Cutting` |正在切分 |
|`Extractioning` |正在提取 |
|`Evaluationing` |正在评查 |
|`Failed` |处理失败 |
|`Processed` |处理完成 |
##### audit_status 评查完成状态
> **重要说明**:本接口的 `audit_status` 字段来源于 `cross_task_document_mapping` 表,
> 表示该文档在交叉评查任务中的评查完成状态,而**非** `documents` 表中的文档审核状态。
|值 |说明 |场景 |
|---|---|---|
|`0` |未评查 |任务内该文档尚未完成评查(默认状态) |
|`1` |已评查 |任务内该文档已确认完成评查 |
**与 documents.audit_status 的区别**
|字段来源 |字段含义 |取值范围 |
|---|---|---|
|`cross_task_document_mapping.audit_status` |任务内评查完成状态 |0(未评查)、1(已评查) |
|`documents.audit_status` |文档整体审核状态 |-2(警告)、-1(不通过)、0(待审核)、1(通过)、2(审核中) |
#### 错误码
|状态码 |错误信息 |说明 |
|---|---|---|
|`403` |无权访问任务:您不是该任务的参与者 |用户不是任务参与者 |
#### 与 `/admin/versions/documents-list` 的字段对照
|本接口字段 |versions接口字段 |说明 |
|---|---|---|
|`id` |`id` |一致 |
|`name` |`name` |一致 |
|`path` |`path` |一致 |
|`version_number` |`version_number` |一致 |
|`created_at` |`created_at` |一致 |
|`status` |`status` |一致 |
|`file_size` |`file_size` |一致 |
|`document_number` |`document_number` |一致 |
|`type_id` |`type_id` |一致 |
|`type_name` |`type_name` |一致 |
|`upload_time` |`upload_time` |一致 |
|`audit_status` |`audit_status` |一致 |
|`total_evaluation_points` |`total_evaluation_points` |一致 |
|`pass_count` |`pass_count` |一致 |
|`warning_count` |`warning_count` |一致 |
|`error_count` |`error_count` |一致 |
|`manual_count` |`manual_count` |一致 |
|`issue_count` |`issue_count` |一致 |
|`warning_messages` |`warning_messages` |一致 |
|`error_messages` |`error_messages` |一致 |
|`issue_messages` |`issue_messages` |一致 |
|`manual_messages` |`manual_messages` |一致 |
|`total_versions` |`total_versions` |一致 |
|`history_versions` |`history_versions` |一致 |
|`final_score` |- |**交叉评查特有** |
|`full_score` |- |**交叉评查特有** |
|`score_summary` |- |**交叉评查特有** |
|`score_percent` |- |**交叉评查特有** |
#### 前端使用说明
1. **版本展示**: 前端可复用版本管理模块的组件,根据 `total_versions > 1` 判断是否显示版本展开按钮
2. **历史版本折叠**: 默认只显示当前版本,历史版本通过折叠面板展开
3. **继续上传**: 在任务详情页提供"继续上传"入口,上传同名文件会自动归纳为新版本
4. **评查统计**: 当前版本和历史版本都有独立的评查统计,可对比不同版本的改进情况
5. **审核状态**: 每个版本有独立的 `audit_status`,历史版本的审核状态也可以查看
---
---
### 10. 确认完成文档评查
**POST** `/api/v2/cross_review/tasks/{task_id}/documents/{document_id}/complete`
确认完成对指定文档的评查,更新任务进度。**仅任务创建者或主要负责人可调用**。
**权限**: `cross_review:document:complete`
#### 路径参数
|参数 |类型 |说明 |
|---|---|---|
|`task_id` |int |任务ID(必须大于0 |
|`document_id` |int |文档ID(必须大于0 |
#### 请求体
无需请求体。
#### 业务逻辑
1. **任务存在性验证**
- 验证任务存在
2. **权限验证**(重要!)
- **只有以下用户有权确认完成**
- 任务创建者(`assigner_id`
- 主要负责人(`principal_user_ids` 数组中的用户)
- 普通任务参与者(仅在 `user_ids` 中)无权确认完成
- 建议前端先调用 `GET /tasks/{task_id}/can-confirm` 判断是否显示按钮
3. **文档归属验证**
- 验证文档确实属于该任务(在 `cross_task_document_mapping` 中)
4. **状态更新**
-`cross_task_document_mapping.audit_status` 设置为 `1`(已完成)
#### 响应示例
```json
{
"code": 0,
"success": true,
"message": "文档评查已完成",
"task_id": 1,
"document_id": 123
}
```
#### 错误码
|状态码 |错误信息 |说明 |
|---|---|---|
|`400` |任务不存在 |无效的任务ID |
|`400` |文档不在任务中 |文档不属于该任务 |
|`403` |用户不是任务创建者或主要负责人,无权确认完成 |权限不足 |
---
---
### 12. 为任务文档追加附件 ✅
**接口路径**: `POST /api/v2/cross_review/tasks/{task_id}/documents/{document_id}/append_attachments`
**接口说明**: 为交叉评查任务中的文档追加附件,创建新版本。
#### 设计方案
**核心逻辑**
- 追加附件后**创建新文档**(新版本),原文档保留
- 新文档自动关联到当前任务(`cross_task_document_mapping`
- 新文档 `audit_status = 0`(需重新评查)
- 原文档的评查结果、提案、投票保持不变
- 新文档保持相同的 `name + type_id`,触发版本分组逻辑
**复用模块**
- 底层调用现有的 `append_contract_attachments` 服务
- `merge_mode` 强制为 `"new"`(创建新文档)
#### 请求参数
**Path 参数**
|字段 |类型 |必填 |说明 |
|---|---|---|---|
|`task_id` |int |是 |任务ID |
|`document_id` |int |是 |原文档ID |
**Form 参数**
|字段 |类型 |必填 |默认值 |说明 |
|---|---|---|---|---|
|`files` |File[] |是 |- |附件列表(支持 PDF/Word/ZIP/RAR |
|`remark` |string |否 |null |备注说明 |
|`use_markdown` |bool |否 |false |Word附件是否使用Markdown处理(详见下方说明) |
#### `use_markdown` 参数详解
**作用**:控制 Word 附件(.doc/.docx)的处理方式。
|`use_markdown` |Word 附件处理方式 |适用场景 |
|---|---|---|
|`false`(默认) |Word → 转 PDF → OCR 识别 |需要保留原始排版/图片 |
|`true` |Word → 直接 Markdown 提取文本 |文本为主,避免 OCR 错误 |
**处理逻辑**
```
判断原合同类型(is_original_word = 原合同是 .doc/.docx
处理 Word 附件时:
├── if 原合同是 Word OR use_markdown=true:
│ → Word 附件直接用 Markdown 流程提取文本
│ → 跳过 OCR,准确率更高
└── else:
→ Word 转 PDF,走 OCR 流程
→ 保留原始排版和图片
```
**为什么要用 Markdown 处理?**
|对比项 |OCR 方式 |Markdown 方式 |
|---|---|---|
|**准确率** |可能有识别错误 |100%(直接提取) |
|**处理速度** |较慢(需转PDF+OCR) |快(直接解析) |
|**格式保留** |保留排版/图片 |保留表格/列表结构 |
|**适用场景** |扫描件、图片多 |纯文本、表格多 |
**建议**
- 原合同是 Word 文件时,**自动**走 Markdown(无需设置)
- 原合同是 PDF + Word 附件时,根据附件内容选择:
- 附件以文本/表格为主 → `use_markdown=true`
- 附件有重要图片/特殊排版 → `use_markdown=false`
#### 权限要求
- 当前用户必须是**任务创建者**`assigner_id`)或**主要负责人**`principal_user_ids` 数组中)
- 需要权限:`cross_review:document:append`
> ⚠️ **注意**:普通任务参与者无权追加附件,只有任务创建者或主要负责人可以操作。
#### 响应示例
```json
{
"success": true,
"result": {
"original_document_id": 2200,
"new_document_id": 2201,
"new_version_number": 3,
"task_id": 123,
"message": "附件追加成功,已创建新版本",
"background_processing": true
}
}
```
#### 处理流程
```
1. 权限校验
├── 验证 task_id 存在
├── 验证 current_user 是任务创建者或主要负责人
└── 验证 document_id 在任务的文档映射中
2. 调用现有追加附件模块
├── merge_mode = "new"(强制创建新文档)
├── 复用 PDF 合并 / Word Markdown 处理逻辑
└── 上传到 MinIO
3. 更新任务-文档映射
├── 新文档添加到 cross_task_document_mapping
├── 新文档 audit_status = 0
└── 原文档映射保留不变
4. 触发后台处理
└── Celery 任务:OCR → 抽取 → 评查
```
---
---
### 13. 任务模板对比功能 ✅
**功能说明**: 为交叉评查任务绑定合同模板,支持任务下所有合同与模板进行结构对比。
#### 设计方案
##### 关键点
> **模板不是** `documents` 表的记录,是直接存储在 `contract_structure_comparison` 表中的:
>
> - `template_contract_path` - 模板文件路径
> - `template_contract_name` - 模板文件名
> - `ocr_results` - 模板OCR结果
##### 问题分析:交叉评查需求 vs 现有设计
|维度 |现有设计 |交叉评查需求 |
|---|---|---|
|**关系** |1个源文档 : 1条模板记录 |N个文档共用1个模板 |
|**模板复用** |模板信息内嵌在记录中 |需要模板复用 |
|**document_id** |指向源文档(必填) |任务级别,不绑定单个文档 |
##### 采用方案:直接复用(方案A)
**优点**
- ✅ 不改动 `contract_structure_comparison` 表结构
- ✅ 复用现有的模板OCR、对比服务
- ✅ 实现简单
**数据库变更**
```sql
-- 任务表添加字段,指向"模板记录"
ALTER TABLE cross_examination_tasks
ADD COLUMN template_comparison_id INTEGER
REFERENCES contract_structure_comparison(id);
COMMENT ON COLUMN cross_examination_tasks.template_comparison_id
IS '任务关联的合同模板记录ID,用于结构对比';
```
**复用表**`contract_structure_comparison`(不新建表)
**实现流程**
1. 上传模板时,用任务中第一个文档的 `document_id` 创建 `contract_structure_comparison` 记录
2. 任务表保存 `template_comparison_id` 指向该记录
3. 对比时,读取模板记录的 `ocr_results`,复用现有对比逻辑
#### 13.1 上传/绑定模板到任务
**接口路径**: `POST /api/v2/cross_review/tasks/{task_id}/template`
**请求参数**
|字段 |类型 |必填 |说明 |
|---|---|---|---|
|`file` |File |是 |模板合同文件(PDF/Word |
**响应示例**
```json
{
"success": true,
"result": {
"task_id": 123,
"template_comparison_id": 190,
"template_name": "设备维保合同范本.pdf",
"template_path": "documents/mz/合同对比模板/2025/12/...",
"status": "Waiting",
"message": "模板上传成功,正在进行OCR处理"
}
}
```
**处理流程**
```
1. 权限校验(任务创建者或主要负责人)
2. 获取任务中第一个文档的 document_id
3. 调用现有 upload_contract_template 逻辑
└── 创建 contract_structure_comparison 记录
4. 更新任务表 template_comparison_id
5. 触发模板OCR任务(Celery
```
#### 13.2 获取任务模板信息
**接口路径**: `GET /api/v2/cross_review/tasks/{task_id}/template`
**响应示例**
```json
{
"success": true,
"result": {
"task_id": 123,
"template_comparison_id": 190,
"template_name": "设备维保合同范本.pdf",
"template_path": "documents/mz/合同对比模板/2025/12/...",
"status": "Processed",
"ocr_completed": true,
"created_at": "2025-12-13T10:00:00Z"
}
}
```
#### 13.3 触发文档与模板对比
**接口路径**: `POST /api/v2/cross_review/tasks/{task_id}/documents/{document_id}/compare_template`
**响应示例**
```json
{
"success": true,
"result": {
"task_id": 123,
"document_id": 2200,
"comparison_id": 191,
"status": "Processing",
"message": "对比任务已提交,请稍后查询结果"
}
}
```
**处理流程**
```
1. 权限校验(任务参与者)
2. 验证任务已绑定模板(template_comparison_id 不为空)
3. 验证模板OCR已完成(status = 'Processed'
4. 获取文档的 ocr_result
5. 获取模板的 ocr_results
6. 调用 contract_llm_compare() 执行对比
7. 保存对比结果到新的 contract_structure_comparison 记录
```
#### 13.4 获取对比结果
**接口路径**: `GET /api/v2/cross_review/tasks/{task_id}/documents/{document_id}/comparison_result`
**响应示例**
```json
{
"success": true,
"result": {
"document_id": 2200,
"document_name": "车辆维保合同.pdf",
"template_name": "设备维保合同范本.pdf",
"status": "Processed",
"comparison_results": {
"合同正文": [
{
"field": "甲方签字",
"source_page": "11",
"template_page": "11",
"status": "normal",
"details": "甲方签字字段存在(源合同第11页,模板第11页)"
},
{
"field": "合同有效期",
"source_page": "",
"template_page": "10",
"status": "missing_field",
"details": "源合同缺少合同有效期字段"
}
],
"合同落款": []
},
"summary": {
"total_fields": 45,
"matched_fields": 42,
"missing_fields": 3,
"match_rate": 93.3
}
}
}
```
#### 13.5 批量对比任务下所有文档
**接口路径**: `POST /api/v2/cross_review/tasks/{task_id}/compare_all`
**响应示例**
```json
{
"success": true,
"result": {
"task_id": 123,
"total_documents": 5,
"submitted": 5,
"message": "已提交5个文档的对比任务"
}
}
```
#### 架构图
```
┌─────────────────────────────────────────────────────────────────┐
│ 模板对比功能架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ cross_examination_tasks │
│ ┌───────────────────────────────┐ │
│ │ id: 123 │ │
│ │ task_name: "12月合同评查" │ │
│ │ template_comparison_id: 190 ──┼──────────┐ │
│ └───────────────────────────────┘ │ │
│ ▼ │
│ contract_structure_comparison │
│ ┌─────────────────────────────┐ │
│ │ id: 190 (模板主记录) │ │
│ │ document_id: 2200 (代表文档) │ │
│ │ template_contract_path: ... │ │
│ │ template_contract_name: ... │ │
│ │ ocr_results: {...} (模板OCR) │ │
│ │ status: "Processed" │ │
│ └─────────────────────────────┘ │
│ │
│ 对比时为每个文档创建独立记录: │
│ ┌─────────────────────────────┐ │
│ │ id: 191 (文档2200的对比结果) │ │
│ │ document_id: 2200 │ │
│ │ comparison_results: {...} │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ id: 192 (文档2201的对比结果) │ │
│ │ document_id: 2201 │ │
│ │ comparison_results: {...} │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
#### 权限要求
|接口 |权限 |
|---|---|
|上传模板 |`cross_review:template:upload`(任务创建者/主要负责人) |
|获取模板信息 |`cross_review:template:read`(任务参与者) |
|触发对比 |`cross_review:template:compare`(任务参与者) |
|获取对比结果 |`cross_review:template:read`(任务参与者) |
|批量对比 |`cross_review:template:compare`(任务参与者) |
> **权限说明**
>
> - **上传模板**:只有任务创建者或主要负责人可以为任务上传/更换模板
> - **对比操作**:所有任务参与者都可以触发对比、查看对比结果
---