# 老项目交叉评查逻辑梳理

> 目标：整理老项目 `docauditai` 中“交叉评查”模块的真实业务链路、数据模型、权限边界和当前新项目承接情况，便于后续迁移、重构和对照实现。

## 1. 项目路径与主要参考文件

### 1.1 老项目路径

- 老项目根目录：`/home/wren-dev/Porject/docauditai`

### 1.2 当前新项目前端路径

- 新前端目录：`/home/wren-dev/Porject/leaudit-platform/new_doc_review`

### 1.3 本次整理的核心参考文件

- 老逻辑总说明：`/home/wren-dev/Porject/docauditai/dcos/cross_review_business_logic.md`
- 老表关系图：`/home/wren-dev/Porject/docauditai/dcos/cross_review_table_relationships.md`
- 老路由主文件：`/home/wren-dev/Porject/docauditai/app/routes/v2/crossreview/cross_review.py`
- 老服务主文件：`/home/wren-dev/Porject/docauditai/services/documents/v2/cross_review_service.py`
- 历史接口文档：`new_doc_review/auth_doc/交叉评查接口文档.md`

## 2. 一句话定义

交叉评查不是重新跑 OCR、抽取、规则评查，而是围绕已经存在的评查结果做“多人协同复核”。

它的核心目标是：

- 把一批文档挂到一个交叉评查任务里
- 让任务参与人围绕具体评查点发起加分/减分提案
- 让参与人对提案投票
- 提案通过后，把分数回写到原始评查结果
- 由负责人手动确认文档评查完成，并最终推动任务完成

换句话说，这是一条“评后协作复核”流程，不是底层文档处理流水线。

## 3. 核心数据模型

交叉评查主要依赖以下几张表：

### 3.1 `cross_examination_tasks`

交叉评查任务主表。

核心字段：

- `assigner_id`：任务创建者
- `user_ids`：参与评查的用户列表
- `principal_user_ids`：主要负责人列表
- `task_status`：任务状态，常见值有 `in_progress`、`completed`
- `task_name`：任务名称
- `doc_type`：任务绑定的文档类型编码
- `task_type`：任务类型，如 `CITY`、`DISTRICT`

业务含义：

- 一个任务绑定一组参与人和一批文档
- 创建者和主要负责人拥有更高权限

### 3.2 `cross_task_document_mapping`

交叉评查任务与文档的映射表。

核心字段：

- `task_id`
- `document_id`
- `audit_status`
- `deleted_at`

业务含义：

- 一条记录表示某文档被纳入某个交叉评查任务
- `audit_status=1` 表示该文档在该任务内已确认完成
- 这里的 `audit_status` 是任务内状态，不等于 `documents.audit_status`

### 3.3 `cross_scoring_proposals`

交叉评分提案表。

核心字段：

- `document_id`
- `evaluation_point_id`
- `evaluation_result_id`
- `proposer_id`
- `proposed_score`
- `reason`
- `status`
- `deleted_at`

业务含义：

- 针对某个评查结果提出加分或减分建议
- 常见状态：`pending`、`approved`、`rejected`

### 3.4 `cross_opinion_votes`

交叉评查投票表。

核心字段：

- `proposal_id`
- `voter_id`
- `vote_type`
- `deleted_at`

业务含义：

- 参与者对提案表态
- 支持 `agree`、`disagree`、`cancel`
- 提案创建后，系统会自动给提案人补一票 `agree`

### 3.5 `evaluation_results`

旧评查结果表，是交叉评查当前的评分底座。

主要作用：

- 读取当前机器分 / 最终分 / 评查结果消息
- 在提案通过后更新 `final_score`

### 3.6 `evaluation_points`

评查点定义表。

交叉评查依赖它读取：

- 单点评查满分 `score`
- 风险级别
- `fail_message`
- `post_action`
- `suggestion_message_type`

## 4. 主业务链路

## 4.1 创建任务

核心服务：

- `assign_cross_examination_tasks()`
- 文件位置：`/home/wren-dev/Porject/docauditai/services/documents/v2/cross_review_service.py`

处理逻辑：

1. 把 `assigner_id` 强制加入 `user_ids` 并去重
2. 创建 `cross_examination_tasks`
3. 为每个文档写入 `cross_task_document_mapping`
4. 初始状态设置为 `task_status = in_progress`

补充说明：

- 在老项目里，任务创建很多时候不是单独走一个“先建任务”的管理入口
- 更常见的是在上传文档后直接触发任务分配
- 相关入口可见：`/home/wren-dev/Porject/docauditai/app/routes/v2/documents/documents.py`

## 4.2 任务列表

接口：

- `POST /tasks/user_tasks`

实现位置：

- 路由：`/home/wren-dev/Porject/docauditai/app/routes/v2/crossreview/cross_review.py`

逻辑特点：

- 按 `current_user.id = ANY(user_ids)` 查询用户参与的任务
- 进度不是看 `documents.audit_status`
- 而是看 `cross_task_document_mapping.audit_status`

返回内容通常包括：

- 任务 ID
- 任务名称
- 任务状态
- 文档类型
- 任务类型
- 创建时间
- 进度百分比
- 总文档数
- 参与地区

## 4.3 任务下文档列表

老项目实际上有两套接口思路。

### 旧列表接口

- `POST /tasks/{task_id}/documents`

特点：

- 返回任务下文档的分页列表
- 聚合每个文档的评查统计、问题摘要、得分等信息

### 新版版本归纳接口

- `GET /tasks/{task_id}/documents`

对应服务：

- `get_task_documents_with_versions()`
- 文件：`/home/wren-dev/Porject/docauditai/services/documents/v2/cross_review_service.py`

版本归纳规则：

- 同一任务内 `(name, type_id)` 相同的文档归为一组
- 组内按 `created_at` 降序排列
- 最新上传的是当前版本
- 其他的是历史版本

统计信息来源：

- 文档实体：`documents`
- 评查统计：`evaluation_results + evaluation_points`
- 得分规则：优先 `final_score`，否则按机器结果/通过结果推导

## 4.4 发起评分提案

接口：

- `POST /proposals`

路由位置：

- `/home/wren-dev/Porject/docauditai/app/routes/v2/crossreview/cross_review.py`

服务位置：

- `create_scoring_proposal()`
- `/home/wren-dev/Porject/docauditai/services/documents/v2/cross_review_service.py`

核心校验规则：

1. 用户必须有文档访问权限
2. `evaluation_result_id` 必填
3. `evaluation_result_id` 必须真实属于当前 `document_id`
4. 文档必须已纳入某个交叉评查任务
5. 提案人必须是该任务参与者
6. 同一用户不能对同一文档、同一评查点重复创建未删除提案
7. 不允许创建 0 分提案
8. 当前分数已经为 0 时，不允许继续扣分
9. 当前分数已经达到该评查点满分时，不允许继续加分

创建成功后：

1. 插入 `cross_scoring_proposals`
2. 自动给提案人插入一条 `agree` 投票
3. 立即触发提案状态检查

## 4.5 投票

接口：

- `POST /proposals/{proposal_id}/votes`

服务：

- `create_opinion_vote()`

支持动作：

- `agree`
- `disagree`
- `cancel`

主要规则：

- 只有任务参与者能投票
- 已经 `approved` 或 `rejected` 的提案不允许再投/撤销
- `cancel` 本质是把投票软删除

## 4.6 提案状态判定

内部方法：

- `_check_and_process_proposal_status()`

判定规则：

1. 找到提案关联文档
2. 取该文档所在的最新任务
3. 获取任务参与人数 `n`
4. 统计同意票 `a` 与反对票 `d`
5. 通过阈值为 `floor(n / 2) + 1`

状态流转：

- `agree >= threshold` -> `approved`
- `disagree >= threshold` -> `rejected`
- 即使剩余未投票者全部同意也无法达到阈值 -> `rejected`
- 否则保持 `pending`

这说明老逻辑走的是“绝对多数制”，不是全票制。

## 4.7 提案通过后的分数回写

内部方法：

- `_update_evaluation_result_score()`

处理逻辑：

1. 找到提案关联的 `evaluation_result_id`
2. 读取当前 `evaluation_results.final_score / machine_score`
3. 读取评查点满分 `evaluation_points.score`
4. 将 `proposed_score` 累加到最终分数
5. 做边界保护：
   - 最低不小于 0
   - 最高不超过评查点满分

结论：

- 老交叉评查的“最终结果”是直接回写旧评查结果表 `evaluation_results.final_score`

## 4.8 查看文档上的所有提案

接口：

- `POST /proposals/document`

服务：

- `get_proposals_by_document()`

典型返回字段：

- 提案 ID
- 评查点名称
- 建议分值
- 理由
- 提案人
- 已投票列表
- 同意人
- 反对人
- 待投票人
- 当前用户是否可投票
- 发现问题文案
- 提案状态

这是详情页“交叉意见区”的核心数据接口。

## 4.9 获取待处理提案列表

接口：

- `POST /proposals/details`

服务：

- `get_proposals_with_details()`

作用：

- 查当前用户需要处理的待投票提案

过滤规则：

- 只看 `pending`
- 排除自己发起的提案
- 只看自己参与任务下的文档

## 4.10 检查是否存在未投票用户

接口：

- `POST /proposals/document/check_pending_votes`

服务：

- `check_pending_votes_by_document()`

权限要求：

- 只有任务创建者或主要负责人可以调用

作用：

- 在确认完成前检查该文档下是否还有提案未完成投票

## 4.11 确认文档评查完成

接口：

- `POST /tasks/{task_id}/documents/{document_id}/complete`

服务：

- `complete_document_audit()`

主要规则：

- 只有 `assigner_id` 或 `principal_user_ids` 中的用户才有权限确认
- 确认后会把 `cross_task_document_mapping.audit_status` 改为 `1`
- 如果任务下所有文档都完成，则把 `cross_examination_tasks.task_status` 改成 `completed`

注意：

- 文档是否完成是“任务内状态”
- 任务是否完成是所有任务内文档完成后的汇总状态

## 4.12 检查当前用户是否可确认完成

接口：

- `GET /tasks/{task_id}/can-confirm`

服务：

- `check_confirm_permission()`

作用：

- 前端先调用此接口，再决定是否显示“确认完成”按钮

## 4.13 向任务补文档 / 补附件

相关接口：

- `POST /tasks/{task_id}/documents/{document_id}/append_attachments`
- `POST /tasks/{task_id}/upload_documents`

核心规则：

- 只有任务创建者或主要负责人可以操作
- 追加附件会创建新文档，而不是覆盖老文档
- 新文档会重新写入 `cross_task_document_mapping`
- 新文档默认 `audit_status = 0`
- 同名同类型文档会自动并入版本链

## 5. 权限模型

交叉评查存在三层权限。

### 5.1 文档访问权限

发起提案和查看文档提案前，会先检查用户是否有权访问该文档。

### 5.2 任务参与权限

任务参与者来自 `cross_examination_tasks.user_ids`。

参与者可以：

- 发起提案
- 对提案投票
- 查看自己参与任务下的任务、文档、提案和进度

### 5.3 高权限用户

高权限用户包括：

- 任务创建者 `assigner_id`
- 主要负责人 `principal_user_ids`

高权限用户可以：

- 确认文档评查完成
- 检查文档下是否仍有待投票人员
- 为任务文档追加附件
- 向任务继续上传新文档

## 6. 交叉评查与 PostgREST 权限扩展

老项目还有一个关键点：交叉评查不是只靠业务接口做权限控制，代理层也会扩展文档可见范围。

关键文件：

- `/home/wren-dev/Porject/docauditai/app/routes/postgrest.py`

它的作用是：

- 获取用户参与的交叉评查任务
- 提取这些任务关联的文档 ID
- 把这些文档加入用户可访问范围

这意味着：

- 用户即使不在原始文档所属部门
- 只要参与了交叉评查任务，也可能被允许跨部门访问这些任务文档

这部分是老系统交叉评查跨部门协同的重要权限补丁。

## 7. 几个必须记住的业务事实

- 交叉评查不是重新跑评查，而是消费旧 `evaluation_results`
- 提案通过后会直接回写 `evaluation_results.final_score`
- 文档完成状态落在 `cross_task_document_mapping.audit_status`
- 任务完成状态落在 `cross_examination_tasks.task_status`
- 文档完成不是投票自动完成，而是负责人手动确认
- 同名同类型文档在任务内会被视为一条版本链

## 8. 当前新项目承接情况

当前新项目前端已经有完整交叉评查页面，但其业务底座仍然大量依赖老接口和老表思路。

### 8.1 新前端页面

- `new_doc_review/app/routes/cross-checking._index.tsx`
- `new_doc_review/app/routes/cross-checking.result.tsx`
- `new_doc_review/app/routes/cross-checking.upload.tsx`

### 8.2 新前端接口封装

- `new_doc_review/app/api/cross-checking/cross-files.ts`
- `new_doc_review/app/api/cross-checking/cross-file-result.ts`
- `new_doc_review/app/api/cross-checking/cross-files-upload.ts`

### 8.3 仍在使用的老接口口径

前端仍直接调用这些接口：

- `/api/v2/cross_review/...`
- `/admin/v2/cross_review/...`
- `/admin/v2/documents/cross_review/documents/upload_and_assign`
- `/admin/cross_review/tasks/assign`

### 8.4 当前观察结论

- 新前端页面壳子已经迁到 `new_doc_review`
- 但交叉评查业务底座仍大体延续老项目 `docauditai` 的接口和数据模型
- 在 `fastapi_modules/fastapi_leaudit` 中，没有检索到一套完整的新交叉评查业务路由/服务闭环
- 只看到少量兼容读取老表的痕迹，例如 `cross_scoring_proposals`

## 9. 后续迁移时最难拆的三个耦合点

如果后续要把交叉评查彻底迁到新平台，最难拆的不是页面，而是以下 3 个耦合点：

### 9.1 提案通过后回写旧评查结果

- `cross_scoring_proposals` -> `evaluation_results.final_score`

### 9.2 任务内文档完成状态

- `cross_task_document_mapping.audit_status` 决定任务内文档是否完成

### 9.3 跨任务的文档访问权限扩展

- `app/routes/postgrest.py` 对交叉任务文档做了额外放行

## 10. 迁移判断

从当前代码和前端调用情况看，可以做出一个比较明确的判断：

- 现在的交叉评查“前端承载是新的”
- 但“业务模型、接口语义、分数回写、权限扩展”仍然主要是老系统延续下来的

因此，后续如果要迁移：

- 不能只迁页面
- 也不能只补几个接口
- 必须连同任务模型、提案模型、投票模型、评分回写模型、跨部门访问权限一起迁

## 11. 推荐继续查看的文件

如果后续要继续深挖，建议按这个顺序看：

1. `/home/wren-dev/Porject/docauditai/dcos/cross_review_business_logic.md`
2. `/home/wren-dev/Porject/docauditai/services/documents/v2/cross_review_service.py`
3. `/home/wren-dev/Porject/docauditai/app/routes/v2/crossreview/cross_review.py`
4. `new_doc_review/app/api/cross-checking/cross-files.ts`
5. `new_doc_review/app/api/cross-checking/cross-file-result.ts`

---

整理时间：`2026-05-07`
