# 交叉评查模块 - 新增功能设计文档
> 本文档从《交叉评查接口文档.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` 表):
• `0` = 未评查
• `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 |是 |否 |警告消息列表 |
|`error_messages` |array |是 |否 |错误消息列表 |
|`issue_messages` |array |是 |否 |问题消息列表(warning + error 合并去重) |
|`manual_messages` |array |是 |否 |需人工审核的消息列表 |
|`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`(任务参与者) |
> **权限说明**:
>
> - **上传模板**:只有任务创建者或主要负责人可以为任务上传/更换模板
> - **对比操作**:所有任务参与者都可以触发对比、查看对比结果
---