# 交叉评查模块 - 新增功能设计文档 > 本文档从《交叉评查接口文档.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`(任务参与者) | > **权限说明**: > > - **上传模板**:只有任务创建者或主要负责人可以为任务上传/更换模板 > - **对比操作**:所有任务参与者都可以触发对比、查看对比结果 ---