feat: migrate cross review to v3 leaudit flow

This commit is contained in:
wren
2026-05-07 18:18:59 +08:00
parent 35e0c45c42
commit 1c84209f38
23 changed files with 5172 additions and 39 deletions
@@ -0,0 +1,562 @@
# 新项目交叉评查实现方案设计稿
> 目标:基于 `leaudit-platform` 当前代码结构,给出交叉评查在新项目中的推荐实现方案,并明确数据模型、接口设计、目录落点、迁移顺序与实施边界。
## 1. 背景判断
结合当前仓库代码可以确认:
- 前端交叉评查页面已经存在于 `new_doc_review`
- RBAC 路由中已经存在 `/cross-checking`
- 新平台文档与评查底座已经切到 `leaudit_*`
- 但交叉评查协作层仍未完整迁入 `fastapi_modules/fastapi_leaudit`
当前现状更接近:
- 页面壳子是新的
- 文档与评查引擎底座是新的
- 交叉评查业务逻辑仍然大量沿用老系统思维和老接口口径
因此,交叉评查在新项目中的核心任务,不是“新做一个页面”,而是把“协作复核层”从老系统迁到新平台。
## 2. 总体设计原则
推荐遵循以下原则:
### 2.1 交叉评查是协作层,不是评查引擎层
交叉评查不负责:
- OCR
- 文本抽取
- 规则执行
- 原始机器评查结果生成
交叉评查只负责:
- 任务
- 参与人
- 文档挂载
- 提案
- 投票
- 完成确认
- 结果展示叠加
### 2.2 原始评查结果尽量不可变
不建议像老系统那样把通过提案直接回写到旧 `evaluation_results.final_score`
新平台里更合理的方式是:
- `leaudit_rule_results` 保留机器评查真相
- `leaudit_review_point_audits` 保留人工审核覆盖
- `leaudit_cross_review_proposals` 保留交叉评查加减分提案
- 页面展示时动态聚合出最终展示分数
### 2.3 交叉评查单独建模,不污染 LeAudit 核心表
推荐新增专属表:
- `leaudit_cross_review_tasks`
- `leaudit_cross_review_task_members`
- `leaudit_cross_review_task_documents`
- `leaudit_cross_review_proposals`
- `leaudit_cross_review_votes`
### 2.4 版本归纳使用新平台版本能力
不要继续使用老系统“同名 + 同类型推断版本”的方案作为主路径。
新平台已有:
- `leaudit_documents.versionGroupKey`
- `leaudit_documents.versionNo`
- `leaudit_documents.previousVersionId`
- `leaudit_documents.rootVersionId`
- `leaudit_documents.isLatestVersion`
交叉评查列表应优先基于这些字段实现版本归纳。
### 2.5 权限必须后端收口
前端可以决定是否显示按钮,但最终权限校验必须在后端 service 层完成。
不能继续依赖前端自己拼 PostgREST 查询判断权限。
## 3. 新项目里可直接复用的基础能力
## 3.1 文档主链路
现有能力:
- 上传文档:`fastapi_modules/fastapi_leaudit/controllers/documentController.py`
- 文档服务:`fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py`
- 文档主表:`fastapi_modules/fastapi_leaudit/models/leauditDocument.py`
- 文档文件表:`fastapi_modules/fastapi_leaudit/models/leauditDocumentFile.py`
可复用点:
- 文档上传
- OSS 存储
- 文档版本管理
- 附件追加
- 文档详情读取
## 3.2 评查运行与结果主链路
现有能力:
- 评查运行主表:`fastapi_modules/fastapi_leaudit/models/leauditAuditRun.py`
- 评查运行控制器:`fastapi_modules/fastapi_leaudit/controllers/auditController.py`
- 评查详情聚合:`DocumentServiceImpl.GetReviewPoints()`
可复用点:
- 评查运行追踪
- 详情页规则结果聚合
- 人工审核结果叠加
## 3.3 详情页结果聚合
现有 VO
- `fastapi_modules/fastapi_leaudit/domian/vo/reviewPointVo.py`
现有能力:
- `ReviewPointsAggregateVO`
- `scoring_proposals`
- `ReviewPointResultVO.finalScore`
- `ReviewPointResultVO.machineScore`
这意味着:
- 详情页主结构已经可承载交叉评查提案数据
- 只需把“提案来源”和“最终分数计算方式”替换为新模型
## 4. 目标架构
推荐架构分成两层:
### 4.1 LeAudit 评查结果层
职责:
- 文档上传
- 规则执行
- 机器评查结果生成
- 人工审核覆盖记录
主要表:
- `leaudit_documents`
- `leaudit_document_files`
- `leaudit_audit_runs`
- `leaudit_rule_results`
- `leaudit_review_point_audits`
### 4.2 Cross Review 协作层
职责:
- 交叉评查任务
- 成员管理
- 任务文档挂载
- 加减分提案
- 投票
- 文档完成确认
- 协作态进度管理
主要表:
- `leaudit_cross_review_tasks`
- `leaudit_cross_review_task_members`
- `leaudit_cross_review_task_documents`
- `leaudit_cross_review_proposals`
- `leaudit_cross_review_votes`
## 5. 新数据模型设计
## 5.1 `leaudit_cross_review_tasks`
任务主表。
建议字段:
- `id`
- `task_name`
- `task_type`
- `doc_type_id`
- `doc_type_code`
- `assigner_id`
- `status`
- `created_at`
- `updated_at`
- `deleted_at`
说明:
- `status` 建议值:`pending``in_progress``completed`
- `doc_type_id` 可用于快速关联新平台文档类型
- `doc_type_code` 用于兼容前端和历史口径
## 5.2 `leaudit_cross_review_task_members`
任务成员表。
建议字段:
- `id`
- `task_id`
- `user_id`
- `member_role`
- `created_at`
- `updated_at`
- `deleted_at`
说明:
- `member_role` 建议值:`participant``principal`
- 任务创建者不建议仅放数组,应显式保存在任务主表 `assigner_id`
## 5.3 `leaudit_cross_review_task_documents`
任务文档表。
建议字段:
- `id`
- `task_id`
- `document_id`
- `audit_status`
- `created_at`
- `updated_at`
- `deleted_at`
说明:
- `audit_status=0` 表示未完成
- `audit_status=1` 表示已确认完成
- 这是任务内状态,不是文档全局状态
## 5.4 `leaudit_cross_review_proposals`
提案表。
建议字段:
- `id`
- `task_id`
- `document_id`
- `rule_result_id`
- `proposer_id`
- `proposed_score_delta`
- `reason`
- `status`
- `created_at`
- `updated_at`
- `deleted_at`
说明:
- `rule_result_id` 应关联 `leaudit_rule_results.id`
- `proposed_score_delta` 使用增量值,正数加分,负数扣分
- `status` 建议值:`pending``approved``rejected``cancelled`
## 5.5 `leaudit_cross_review_votes`
投票表。
建议字段:
- `id`
- `proposal_id`
- `voter_id`
- `vote_type`
- `created_at`
- `updated_at`
- `deleted_at`
说明:
- `vote_type` 建议值:`agree``disagree`
- `cancel` 建议用软删除表示,不必单独存为最终值
## 6. 关键业务规则如何映射到新平台
## 6.1 创建任务
目标保留的老规则:
- 创建者强制加入任务成员
- 可指定普通参与人和主要负责人
- 任务创建时可同时挂一批文档
新实现建议:
- 创建任务时先写 `leaudit_cross_review_tasks`
- 再写 `leaudit_cross_review_task_members`
- 再写 `leaudit_cross_review_task_documents`
## 6.2 文档访问校验
目标保留的老规则:
- 只有任务成员才能看任务文档
- 提案必须绑定任务内文档
新实现建议:
- 不再让前端自己查 `cross_task_document_mapping`
- 提供统一后端方法:
- `CheckTaskMember(taskId, userId)`
- `CheckTaskDocument(taskId, documentId)`
- `CheckTaskDocumentRuleResult(documentId, ruleResultId)`
## 6.3 提案创建
目标保留的老规则:
- 必须是任务参与者
- 同一用户不能对同一评查点重复创建有效提案
- 不允许 0 分提案
- 当前分数为 0 时不能继续扣
- 当前已满分时不能继续加
新实现建议:
- 通过 `rule_result_id` 读取对应规则结果
- 计算当前展示分数
- 校验是否还能继续加减
- 写入 `leaudit_cross_review_proposals`
- 自动插入一条提案人同意票
## 6.4 投票与状态流转
目标保留的老规则:
- 采用绝对多数制
- 阈值为 `floor(n / 2) + 1`
新实现建议:
- `n` 基于 `leaudit_cross_review_task_members` 统计活跃成员
- 同意达到阈值 -> `approved`
- 反对达到阈值 -> `rejected`
- 剩余票全部同意也无法通过 -> `rejected`
## 6.5 文档完成确认
目标保留的老规则:
- 只有创建者或负责人可确认
- 文档完成改的是任务内状态
- 全部文档完成后任务才完成
新实现建议:
- 确认文档完成时更新 `leaudit_cross_review_task_documents.audit_status`
- 若同任务下所有未删除文档都为完成,则更新 `leaudit_cross_review_tasks.status = completed`
## 6.6 分数计算
这是新实现里最重要的变化。
### 不建议
- 直接更新 `leaudit_rule_results.score`
- 直接写回机器结果源字段
### 建议
按聚合方式计算展示分数:
- `machineScore`:来自 `leaudit_rule_results`
- `auditOverride`:来自 `leaudit_review_point_audits`
- `crossReviewDelta`:来自所有已通过的 `leaudit_cross_review_proposals`
- `finalScore`:在详情页聚合阶段动态计算
建议公式:
1. 先根据机器结果与人工审核确定基础结果
2. 基础结果换算出基础得分
3. 加上 `approved` 提案累计增量
4. 最终再做上下限裁剪:
- 不低于 0
- 不高于规则满分
## 7. 详情页如何接入新实现
当前详情页主入口已经存在:
- `fastapi_modules/fastapi_leaudit/controllers/documentController.py`
- `GET /v3/review-points/{DocumentId}`
建议做法:
### 7.1 保留 `GetReviewPoints()` 作为主入口
这样前端详情页无需大改。
### 7.2 修改 `_loadScoringProposals()`
当前只是兼容读取旧 `cross_scoring_proposals`
建议改为:
- 优先读取 `leaudit_cross_review_proposals`
- 若尚未迁移完成,可保留旧表兼容兜底一段时间
### 7.3 修改评查点聚合逻辑
`_loadReviewPointResults()` 或其上层聚合逻辑中:
- 读取单点所有已批准提案的分值增量
- 计算 `finalScore`
- 继续把提案列表放入 `scoring_proposals`
### 7.4 前端详情页不用重构为新的页面协议
保持 `ReviewPointsAggregateVO` 不变或小幅扩展即可。
## 8. 任务列表和任务文档列表如何实现
## 8.1 任务列表
返回建议字段:
- `task_id`
- `task_name`
- `task_status`
- `doc_type`
- `task_type`
- `task_created_at`
- `progress`
- `total_documents`
- `evaluation_region`
实现来源:
- 任务主表
- 成员表
- 任务文档表
## 8.2 任务文档列表
建议按以下逻辑组织:
1. 先取任务绑定文档
2. 通过 `leaudit_documents.versionGroupKey` 做版本归组
3. 当前版本优先取 `isLatestVersion = true`
4. 统计信息来自 `leaudit_rule_results`
5. 最终展示分数叠加 `approved` 提案增量
## 8.3 不建议继续使用旧版“同名同类型猜版本”
因为新平台已经有更稳定的版本链字段,不必再依赖脆弱的文件名规则。
## 9. 上传与追加附件的实现建议
## 9.1 推荐长期方案
采用“文档系统”和“交叉评查系统”解耦:
1. 先用通用文档接口上传
2. 拿到 `documentId`
3. 再把文档挂入交叉任务
优点:
- 复用现有 `DocumentServiceImpl.Upload`
- 文档生命周期统一
- 附件处理逻辑不重复
## 9.2 推荐短期兼容方案
为兼容当前前端,可先提供聚合接口:
- `UploadAndCreateTask`
- `UploadDocumentsToTask`
- `AppendTaskDocumentAttachments`
这些接口内部再调用:
- `DocumentService.Upload`
- `DocumentService.AppendAttachments`
这样前端先不需要大改。
## 10. 权限设计
建议保留现有 RBAC 权限点:
- `cross_review:task:read`
- `cross_review:task:create`
- `cross_review:progress:view`
- `cross_review:proposal:create`
- `cross_review:proposal:read`
- `cross_review:proposal:delete`
- `cross_review:proposal:vote`
- `cross_review:document:complete`
同时叠加业务层权限判断:
- 是否任务成员
- 是否创建者
- 是否主要负责人
- 文档是否属于任务
- 评查点结果是否属于该文档
## 11. 推荐实施顺序
### 第一阶段:先建协作层表和后端接口
目标:
- 新建 `leaudit_cross_review_*`
- 新建 `CrossReviewController / Service / DTO / VO`
- 打通任务、提案、投票、完成确认
### 第二阶段:详情页切新提案逻辑
目标:
- `GetReviewPoints()` 改读新提案表
- 动态计算最终分数
- 详情页不再依赖旧 `cross_scoring_proposals`
### 第三阶段:任务文档列表切新版本底座
目标:
- 改用 `leaudit_documents` 版本字段归组
- 任务内分数与统计改读新平台结果
### 第四阶段:前端路径和历史兼容收口
目标:
- 逐步下线 `/admin/v2/cross_review/*` 历史兼容口径
- 清理前端直查旧表逻辑
- 清理旧 `cross_*` 兼容读取
## 12. 风险与注意事项
### 12.1 最大风险:最终分数语义变化
如果继续沿用老系统“直接回写”的做法,会污染新平台结果底座。
建议统一改为聚合展示分数。
### 12.2 第二风险:前端隐式依赖旧接口字段
当前 `new_doc_review/app/api/cross-checking/*` 里仍存在很多历史字段名和旧路径假设。
新后端第一期最好兼容这些字段,避免前端同步改动过大。
### 12.3 第三风险:权限校验分散
当前部分前端仍尝试自行查表判断权限。
迁移时应尽快把权限判断收敛到后端。
@@ -0,0 +1,450 @@
# 新项目交叉评查新表结构与接口清单
> 目标:给出交叉评查在新平台中的建议表结构、接口清单以及代码文件落点,便于直接拆任务实施。
## 1. 推荐新增表
## 1.1 `leaudit_cross_review_tasks`
用途:
- 交叉评查任务主表
建议字段:
```sql
id bigint primary key,
task_name varchar(255) not null,
task_type varchar(32) not null,
doc_type_id bigint null,
doc_type_code varchar(64) null,
assigner_id bigint not null,
status varchar(32) not null default 'in_progress',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
```
建议索引:
- `idx_lcrt_assigner_id`
- `idx_lcrt_status`
- `idx_lcrt_doc_type_id`
## 1.2 `leaudit_cross_review_task_members`
用途:
- 任务参与人和负责人关系表
建议字段:
```sql
id bigint primary key,
task_id bigint not null,
user_id bigint not null,
member_role varchar(32) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
```
约束建议:
- 唯一约束:`(task_id, user_id, deleted_at is null)` 语义上唯一
建议索引:
- `idx_lcrtm_task_id`
- `idx_lcrtm_user_id`
- `idx_lcrtm_role`
## 1.3 `leaudit_cross_review_task_documents`
用途:
- 任务绑定文档
建议字段:
```sql
id bigint primary key,
task_id bigint not null,
document_id bigint not null,
audit_status integer not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
```
说明:
- `audit_status=0` 未完成
- `audit_status=1` 已完成
建议索引:
- `idx_lcrtd_task_id`
- `idx_lcrtd_document_id`
- `idx_lcrtd_task_status`
## 1.4 `leaudit_cross_review_proposals`
用途:
- 交叉评查加减分提案
建议字段:
```sql
id bigint primary key,
task_id bigint not null,
document_id bigint not null,
rule_result_id bigint not null,
proposer_id bigint not null,
proposed_score_delta numeric(10,2) not null,
reason text not null,
status varchar(32) not null default 'pending',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
```
建议索引:
- `idx_lcrp_task_id`
- `idx_lcrp_document_id`
- `idx_lcrp_rule_result_id`
- `idx_lcrp_proposer_id`
- `idx_lcrp_status`
业务唯一性建议:
- 同一用户对同一 `document_id + rule_result_id` 同时只能存在一条有效提案
## 1.5 `leaudit_cross_review_votes`
用途:
- 提案投票
建议字段:
```sql
id bigint primary key,
proposal_id bigint not null,
voter_id bigint not null,
vote_type varchar(16) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz null
```
建议索引:
- `idx_lcrv_proposal_id`
- `idx_lcrv_voter_id`
业务唯一性建议:
- 同一提案、同一用户仅保留一条有效投票
## 2. 推荐后端文件落点
## 2.1 Models
建议新增:
- `fastapi_modules/fastapi_leaudit/models/leauditCrossReviewTask.py`
- `fastapi_modules/fastapi_leaudit/models/leauditCrossReviewTaskMember.py`
- `fastapi_modules/fastapi_leaudit/models/leauditCrossReviewTaskDocument.py`
- `fastapi_modules/fastapi_leaudit/models/leauditCrossReviewProposal.py`
- `fastapi_modules/fastapi_leaudit/models/leauditCrossReviewVote.py`
## 2.2 DTO
建议新增:
- `fastapi_modules/fastapi_leaudit/domian/Dto/crossReviewDto.py`
建议包含:
- `CrossReviewTaskCreateDTO`
- `CrossReviewProposalCreateDTO`
- `CrossReviewVoteDTO`
- `CrossReviewTaskDocumentQueryDTO`
## 2.3 VO
建议新增:
- `fastapi_modules/fastapi_leaudit/domian/vo/crossReviewVo.py`
建议包含:
- `CrossReviewTaskItemVO`
- `CrossReviewTaskPageVO`
- `CrossReviewTaskDocumentVO`
- `CrossReviewTaskDocumentPageVO`
- `CrossReviewProposalVO`
- `CrossReviewProposalPageVO`
- `CrossReviewPermissionVO`
- `CrossReviewCompleteVO`
## 2.4 Service
建议新增:
- `fastapi_modules/fastapi_leaudit/services/crossReviewService.py`
- `fastapi_modules/fastapi_leaudit/services/impl/crossReviewServiceImpl.py`
## 2.5 Controller
建议新增:
- `fastapi_modules/fastapi_leaudit/controllers/crossReviewController.py`
## 3. 推荐服务接口
建议在 `crossReviewService.py` 中定义以下能力:
- `CreateTask(...)`
- `GetUserTasks(...)`
- `GetTaskDocuments(...)`
- `CanConfirmTaskDocument(...)`
- `CompleteTaskDocument(...)`
- `CreateProposal(...)`
- `VoteProposal(...)`
- `CancelProposal(...)`
- `GetDocumentProposals(...)`
- `GetPendingProposalDetails(...)`
- `CheckPendingVotesByDocument(...)`
- `UploadDocumentsToTask(...)`
- `AppendTaskDocumentAttachments(...)`
- `LoadApprovedProposalDeltas(...)`
## 4. 推荐接口清单
## 4.1 任务接口
### `POST /api/v2/cross_review/tasks`
用途:
- 创建交叉评查任务
请求体建议:
```json
{
"document_ids": [101, 102],
"user_ids": [11, 12],
"principal_user_ids": [21],
"task_name": "市局间交叉评查-合同类",
"doc_type_id": 3,
"doc_type_code": "XZCF",
"task_type": "CITY"
}
```
### `POST /api/v2/cross_review/tasks/user_tasks`
用途:
- 获取当前用户参与的任务列表
### `GET /api/v2/cross_review/tasks/{taskId}/documents`
用途:
- 获取任务下文档列表
- 支持版本归纳
查询参数建议:
- `page`
- `page_size`
- `keyword`
### `GET /api/v2/cross_review/tasks/{taskId}/can-confirm`
用途:
- 检查当前用户是否能确认文档评查完成
### `POST /api/v2/cross_review/tasks/{taskId}/documents/{documentId}/complete`
用途:
- 确认任务内文档完成
### `POST /api/v2/cross_review/tasks/{taskId}/upload_documents`
用途:
- 向已有任务追加新文档
### `POST /api/v2/cross_review/tasks/{taskId}/documents/{documentId}/append_attachments`
用途:
- 给任务文档追加附件并生成新版本
## 4.2 提案接口
### `POST /api/v2/cross_review/proposals`
用途:
- 创建提案
请求体建议:
```json
{
"task_id": 1,
"document_id": 101,
"rule_result_id": 10001,
"proposed_score_delta": -5,
"reason": "该点应扣 5 分"
}
```
### `POST /api/v2/cross_review/proposals/{proposalId}/votes`
用途:
- 对提案投票
请求体建议:
```json
{
"vote_type": "agree"
}
```
### `DELETE /api/v2/cross_review/proposals/{proposalId}`
用途:
- 撤销提案
### `POST /api/v2/cross_review/proposals/document`
用途:
- 获取某文档的提案列表
请求体建议:
```json
{
"document_id": 101,
"page": 1,
"page_size": 10
}
```
### `POST /api/v2/cross_review/proposals/details`
用途:
- 获取当前用户待处理提案列表
### `POST /api/v2/cross_review/proposals/document/check_pending_votes`
用途:
- 检查某文档是否仍有提案未完成投票
## 5. 与现有 `DocumentServiceImpl` 的衔接点
## 5.1 详情页提案列表
当前衔接点:
- `DocumentServiceImpl._loadScoringProposals()`
建议:
- 改成读 `leaudit_cross_review_proposals`
- 后续移除对旧 `cross_scoring_proposals` 的直接依赖
## 5.2 最终得分计算
当前衔接点:
- `DocumentServiceImpl._loadReviewPointResults()`
建议:
- 增加按 `rule_result_id` 聚合已通过提案分数的逻辑
- 返回前写入 `ReviewPointResultVO.finalScore`
## 5.3 文档上传与附件追加
建议复用:
- `DocumentServiceImpl.Upload()`
- `DocumentServiceImpl.AppendAttachments()`
交叉评查只负责:
- 上传后把文档挂入任务
- 维护任务内状态和版本展示
## 6. 建议前端改造策略
## 6.1 第一阶段
前端尽量少改,只调整 API 基址和字段映射。
重点文件:
- `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`
## 6.2 第二阶段
逐步清理这些旧依赖:
- 前端直接查 `cross_examination_tasks`
- 前端直接查 `cross_task_document_mapping`
- 前端假设旧路径 `/admin/cross_review/*`
## 7. 推荐实施拆解
### P0:表结构与模型
- 建新表
- 建模型
- 建基础迁移
### P1:任务与提案接口
- 创建任务
- 任务列表
- 文档列表
- 提案创建
- 投票
- 完成确认
### P2:详情页聚合接入
- `GetReviewPoints()` 改接新提案表
- 动态计算最终分数
### P3:上传与版本链闭环
- 任务上传文档
- 追加附件
- 版本归纳改用 `versionGroupKey`
### P4:清理旧兼容
- 清理旧表直查
- 清理前端旧路径
- 清理旧逻辑兜底
@@ -0,0 +1,594 @@
# 新项目交叉评查状态流转图与边界案例清单
> 目标:把新项目交叉评查的状态流转、时序过程和高风险边界案例一次性说清,避免实现时出现“以为如此,实际上不是”的尴尬。
## 1. 为什么需要这份文档
交叉评查最容易出错的地方,不是 CRUD,而是:
- 状态什么时候变
- 谁能触发状态变更
- 分数什么时候生效
- 文档什么时候算完成
- 任务什么时候算完成
- 新版本文档进来后旧版本怎么处理
这些地方一旦实现错了,前端看起来也许还能跑,但业务上会很尴尬。
所以这份文档专门回答三类问题:
- 状态怎么流转
- 链路怎么串起来
- 边界情况怎么处理
## 2. 五类核心状态
建议在实现时,把状态拆成五套,不要混用。
### 2.1 文档处理状态
来源:
- `leaudit_documents.processing_status`
- `leaudit_audit_runs.status`
这套状态表示:
- 文档是否完成机器评查
它不表示:
- 是否完成交叉评查
### 2.2 任务状态
来源:
- `leaudit_cross_review_tasks.status`
建议值:
- `pending`
- `in_progress`
- `completed`
这套状态表示:
- 任务层面的协作进度
### 2.3 任务文档状态
来源:
- `leaudit_cross_review_task_documents.audit_status`
建议值:
- `0`:未完成
- `1`:已完成
这套状态表示:
- 某个文档在某个任务里是否被负责人确认完成
### 2.4 提案状态
来源:
- `leaudit_cross_review_proposals.status`
建议值:
- `pending`
- `approved`
- `rejected`
- `cancelled`
### 2.5 投票状态
来源:
- `leaudit_cross_review_votes.vote_type`
建议值:
- `agree`
- `disagree`
取消投票建议通过软删除表达,不额外引入 `cancel` 作为终态值。
## 3. 任务状态流转图
```text
创建任务
|
v
in_progress
|
| 所有任务文档 audit_status = 1
v
completed
```
### 3.1 任务创建后状态
- 默认直接进入 `in_progress`
### 3.2 任务完成条件
只有当:
- 该任务下所有未删除任务文档都为 `audit_status = 1`
才允许变为:
- `completed`
### 3.3 明确不是任务完成条件的事件
以下事件都不能直接导致任务完成:
- 文档机器评查跑完
- 所有提案都投票结束
- 没有提案
- 所有人都看过详情页
- 追加了新附件
## 4. 任务文档状态流转图
```text
文档被挂入任务
|
v
audit_status = 0
|
| assigner 或 principal 确认完成
v
audit_status = 1
```
### 4.1 进入任务默认状态
- 新挂入任务的文档默认 `audit_status = 0`
### 4.2 谁能把 `0` 改成 `1`
只有:
- `assigner`
- `principal`
普通参与人不允许确认完成。
### 4.3 新版本文档进入任务后的状态
如果文档追加附件或上传了新版本:
- 新文档进入任务后必须重新记为 `audit_status = 0`
原因:
- 新版本代表新的评查对象
- 不能继承旧版本的“已完成”
## 5. 提案状态流转图
```text
创建提案
|
v
pending
| \
| \
| \ 提案人主动撤销
| \
| v
| cancelled
|
| agree 票达到阈值
v
approved
pending
|
| disagree 票达到阈值
| 或剩余票全部同意也无法通过
v
rejected
```
### 5.1 初始状态
- 提案创建后默认 `pending`
### 5.2 通过条件
若:
- `agree >= floor(n / 2) + 1`
则:
- `approved`
其中 `n` 为任务有效成员总数。
### 5.3 否决条件
若:
- `disagree >= floor(n / 2) + 1`
则:
- `rejected`
### 5.4 提前否决条件
若:
- 即使所有剩余未投票成员都改投同意,提案也不可能过阈值
则:
- 直接 `rejected`
### 5.5 撤销条件
只有提案人可以撤销,且必须满足:
- 当前状态仍为 `pending`
撤销后:
- 提案变为 `cancelled`
### 5.6 终态不可再操作
提案一旦进入:
- `approved`
- `rejected`
- `cancelled`
即视为终态:
- 不允许再投票
- 不允许再撤销
- 不允许再改状态
## 6. 投票时序图
```text
参与人 -> CrossReviewController: POST /proposals/{id}/votes
CrossReviewController -> CrossReviewService: VoteProposal()
CrossReviewService -> DB: 校验提案存在且状态= pending
CrossReviewService -> DB: 校验当前用户是任务成员
CrossReviewService -> DB: 写入或更新投票
CrossReviewService -> DB: 重新统计 agree/disagree/remaining
CrossReviewService -> DB: 如达阈值则更新 proposal.status
CrossReviewService --> CrossReviewController: 返回 proposal_status
CrossReviewController --> 参与人: success + 最新状态
```
### 6.1 投票后必须立即判定状态
不要把“投票成功”和“状态判定”拆成异步延迟任务。
推荐:
- 每次投票后同步判定提案状态
原因:
- 前端能立刻拿到最新状态
- 避免多人并发时出现短暂假状态
## 7. 提案创建时序图
```text
参与人 -> CrossReviewController: POST /proposals
CrossReviewController -> CrossReviewService: CreateProposal()
CrossReviewService -> DB: 校验任务/文档/规则结果关系
CrossReviewService -> DB: 校验用户是否为任务成员
CrossReviewService -> DB: 校验是否重复提案
CrossReviewService -> DB: 校验分值边界
CrossReviewService -> DB: 写入 proposal(status=pending)
CrossReviewService -> DB: 自动写入 proposer agree 投票
CrossReviewService -> DB: 判定 proposal 是否已直接过阈值
CrossReviewService --> CrossReviewController: proposal + latest_status
CrossReviewController --> 参与人: success
```
### 7.1 自动同意票后的特殊情况
如果任务成员数很少,例如:
- 只有 1 人
那么提案创建后可能因为自动同意票立即通过。
这是允许的,只要符合阈值规则。
## 8. 文档完成确认时序图
```text
负责人 -> CrossReviewController: POST /tasks/{taskId}/documents/{documentId}/complete
CrossReviewController -> CrossReviewService: CompleteTaskDocument()
CrossReviewService -> DB: 校验任务存在
CrossReviewService -> DB: 校验用户是 assigner 或 principal
CrossReviewService -> DB: 校验文档属于该任务
CrossReviewService -> DB: 更新 task_document.audit_status = 1
CrossReviewService -> DB: 统计任务下是否所有文档已完成
CrossReviewService -> DB: 若是,则更新 task.status = completed
CrossReviewService --> CrossReviewController: document completed / task completed
CrossReviewController --> 负责人: success
```
### 8.1 确认完成时是否强制要求“所有提案已投完”
不建议强阻断。
建议策略:
- 后端检测是否还有 `pending` 提案或未投票成员
- 返回提示信息
- 由前端给出二次确认
原因:
- 真实业务里负责人可能需要“带风险确认”
- 如果完全强阻断,会把流程卡死
## 9. 详情页分数计算时序图
```text
前端详情页 -> DocumentController: GET /v3/review-points/{documentId}
DocumentController -> DocumentServiceImpl: GetReviewPoints()
DocumentServiceImpl -> DB: 读取 leaudit_rule_results
DocumentServiceImpl -> DB: 读取 leaudit_review_point_audits
DocumentServiceImpl -> DB: 读取 approved proposals
DocumentServiceImpl: 计算 baseScore
DocumentServiceImpl: 计算 crossDelta
DocumentServiceImpl: 计算 finalScore = clamp(baseScore + crossDelta)
DocumentServiceImpl --> DocumentController: ReviewPointsAggregateVO
DocumentController --> 前端详情页: data + stats + scoring_proposals
```
### 9.1 分数生效时点
提案只有在:
- `approved`
后,才计入 `crossDelta`
以下状态都不计入:
- `pending`
- `rejected`
- `cancelled`
## 10. 版本链时序图
```text
用户上传新版本/追加附件
|
v
DocumentService 创建新 leaudit_document
|
v
新文档进入原 versionGroupKey
|
v
CrossReviewService 将新 document_id 挂入任务
|
v
task_document.audit_status = 0
|
v
任务列表按 versionGroupKey 归组展示
```
### 10.1 旧版本是否自动失效
不建议把旧版本任务记录自动删掉。
建议:
- 保留历史版本
- 页面默认展示最新版本
- 历史版本折叠展示
### 10.2 新版本是否自动继承提案
不建议继承。
原因:
- 提案是针对具体 `rule_result_id`
- 新版本会产生新的 `rule_result_id`
- 旧提案不能自动迁移到新结果上
## 11. 边界案例清单
下面这些情况,后端必须提前定好策略。
## 11.1 只有 1 个任务成员
### 情况
- 创建者自己给自己建任务,且没有别的成员
### 正确处理
- 阈值 = 1
- 提案创建后自动同意票即可直接 `approved`
### 不要出错
- 不要硬编码“至少两人才能交叉评查”
## 11.2 只有 2 个任务成员
### 情况
- A 发起提案
- A 自动同意
- B 反对
### 正确处理
- 成员数 `n=2`
- 阈值 `floor(2/2)+1 = 2`
- A 同意数 1,不足通过
- B 反对数 1,也不足否决
- 但此时无剩余票,提案已不可能达到 2 票同意
- 应直接 `rejected`
### 不要出错
- 不要把它一直挂在 `pending`
## 11.3 提案人重复点击提交
### 情况
- 网络慢,用户多次点提交
### 正确处理
- 通过唯一性规则防重
- 返回“已存在有效提案”
### 不要出错
- 不要产生两条同时有效的同目标提案
## 11.4 文档已有新版本,但旧版本已完成
### 情况
- 旧版本在任务里已确认完成
- 新版本追加进来
### 正确处理
- 新版本独立新增一条 `task_document`
- `audit_status = 0`
- 旧版本保持历史完成状态
### 不要出错
- 不要把新版本直接继承旧版本的完成状态
## 11.5 提案通过后又有人想继续改
### 情况
- 某条提案已经 `approved`
- 又有人想针对同一个规则点继续提分或扣分
### 正确处理
- 允许新建后续提案,但必须满足唯一性规则
- 即同一用户不能重复,但不同用户可继续提出新的有效提案
### 不要出错
- 不要错误地把“某点已有 approved 提案”理解成“该点以后都不能再提案”
## 11.6 文档没有任何提案
### 情况
- 任务成员只看文档,不提出任何提案
### 正确处理
- 允许负责人直接确认完成
### 不要出错
- 不要把“没有提案”当成“不能完成”
## 11.7 存在 `pending` 提案,但负责人仍要完成
### 情况
- 业务上要强行收口
### 正确处理
- 推荐允许,但返回 warning 信息
### 不要出错
- 不要一刀切强阻断,除非产品明确要求
## 11.8 文档重跑评查后,旧提案怎么办
### 情况
- 同一个文档重新触发 audit run
- 产生新 `rule_result_id`
### 正确处理建议
- 提案绑定的是具体 `rule_result_id`
- 如果是同一 document 的新 run,建议只对“当前有效 run”的结果展示可新建提案
- 旧 run 上的提案保留历史,不再作为主展示对象
### 不要出错
- 不要把旧 run 的提案直接混到新 run 的点位上
## 11.9 删除任务成员后,历史票怎么办
### 情况
- 某成员中途被移出任务
### 正确处理建议
- 历史投票保留
- 后续阈值计算以当前有效成员还是任务创建时成员为准,必须提前定死
### 推荐口径
- 阈值按“当前有效成员数”计算
### 注意
- 这个规则一旦确定,就不能前后漂移
## 12. 推荐的最终业务口径
为了避免后续实现反复改口,建议最终统一采用以下口径:
- 任务默认 `in_progress`
- 文档进入任务默认未完成
- 文档完成只能由创建者或负责人确认
- 任务完成由“所有任务文档完成”推导
- 提案默认 `pending`
- 提案自动给提案人一票同意
- 提案采用绝对多数制
- 提案通过后只影响展示层最终分,不改机器原始结果
- 版本归纳优先使用 `versionGroupKey`
- 权限由后端统一判断
## 13. 开发前最后核对清单
开始写代码前,建议逐项确认:
- 是否已经把五类状态拆开
- 是否已经明确最终分不是回写机器结果
- 是否已经明确任务完成只看任务文档状态
- 是否已经明确新版本进任务后默认未完成
- 是否已经明确提案绑定到 `rule_result_id`
- 是否已经明确提案通过阈值算法
- 是否已经明确权限是 RBAC + 业务归属双校验
- 是否已经明确详情页主入口仍可复用 `GetReviewPoints()`
如果这几项都确认过,交叉评查的实现基本就不会跑偏。
@@ -0,0 +1,771 @@
# 新项目交叉评查详细业务逻辑定稿
> 目标:把新项目交叉评查的业务逻辑收敛成一份“可直接指导实现”的定稿,重点避免实现阶段把任务状态、评分语义、权限边界、版本逻辑和完成条件做错。
## 1. 定位
交叉评查在新项目中必须被视为:
- 一层独立的“协作复核业务”
- 建立在 `leaudit` 已有文档、评查运行、规则结果之上
- 不负责重新生成底层规则结果
- 只负责任务协作、提案、投票、完成确认和展示分数叠加
因此,交叉评查不是评查引擎的一部分,而是评查结果之上的二次协作层。
## 2. 核心业务对象
新项目中交叉评查至少包含五类核心对象:
### 2.1 任务
任务是交叉评查的协作容器,负责绑定:
- 一组参与人
- 一组主要负责人
- 一批文档
- 一个文档类型语义
- 一个任务状态
任务是交叉评查权限判断的顶层边界。
### 2.2 任务成员
任务成员分为两类:
- `participant`:普通参与人
- `principal`:主要负责人
另有一个特殊角色:
- `assigner`:任务创建者
实现上建议:
- `assigner` 保存在任务主表
- `participant/principal` 保存在成员表
### 2.3 任务文档
任务文档表示:
- 哪些文档进入了交叉评查任务
- 每个文档在该任务内的完成状态
注意:
- 任务文档状态是“任务内状态”
- 不等于文档全局状态
- 不等于底层评查运行状态
### 2.4 提案
提案表示:
- 某个参与人针对某个“规则结果”提出的分数调整建议
提案的本质是:
- 绑定到具体 `rule_result`
- 以“增量分值”表达加分或扣分
### 2.5 投票
投票表示:
- 任务成员对某条提案的同意或反对
投票只作用于提案状态流转,不直接改文档状态。
## 3. 不可搞错的几个真相源
实现阶段最容易尴尬的就是“到底哪张表是真相源”。
这里必须定死:
### 3.1 文档真相源
- `leaudit_documents`
### 3.2 文档文件与版本真相源
- `leaudit_document_files`
- `leaudit_documents.versionGroupKey`
- `leaudit_documents.versionNo`
- `leaudit_documents.previousVersionId`
- `leaudit_documents.rootVersionId`
- `leaudit_documents.isLatestVersion`
### 3.3 机器评查结果真相源
- `leaudit_rule_results`
### 3.4 人工审核覆盖真相源
- `leaudit_review_point_audits`
### 3.5 交叉评查协作真相源
- `leaudit_cross_review_tasks`
- `leaudit_cross_review_task_members`
- `leaudit_cross_review_task_documents`
- `leaudit_cross_review_proposals`
- `leaudit_cross_review_votes`
## 4. 明确禁止的实现方式
以下做法不要再用:
### 4.1 不要直接修改 `leaudit_rule_results` 原始分数
原因:
- 会污染机器评查原始结果
- 会让重新跑批、调试、审计对账变得混乱
### 4.2 不要把交叉评查完成状态写回文档全局状态
原因:
- 交叉评查完成只是任务内完成
- 同一文档可能同时出现在多个任务或多个业务视角下
### 4.3 不要靠前端自行拼表判断权限
原因:
- 字段口径容易漂移
- 任务成员、负责人和创建者语义容易搞混
- 前端无法作为权限真相源
### 4.4 不要再用“同名同类型猜版本”作为主逻辑
原因:
- 新平台已经有正式版本字段
- 同名同类型只能作为兼容或兜底
## 5. 任务业务逻辑定稿
## 5.1 创建任务
### 输入
- 文档 ID 列表
- 参与人 ID 列表
- 主要负责人 ID 列表
- 任务名称
- 任务类型
- 文档类型信息
### 必须规则
1. `assigner_id` 必须自动加入任务成员集合
2. 成员列表必须去重
3. 主要负责人必须同时是任务成员
4. 所有文档必须真实存在
5. 所有文档必须可被当前用户纳入交叉评查
6. 初始状态必须是 `in_progress`
### 输出
- 任务主记录
- 任务成员记录
- 任务文档记录
### 注意
- 创建任务不等于重新跑评查
- 默认不触发新的机器评查
- 如果文档上传接口内包含自动评查,是上传链路行为,不是任务创建行为
## 5.2 任务列表
任务列表只返回当前用户参与的任务。
### 当前用户可见任务的判定
用户满足以下任一条件即可见:
-`assigner`
- 是成员表里的 `participant`
- 是成员表里的 `principal`
### 返回字段必须稳定
- `task_id`
- `task_name`
- `task_status`
- `doc_type`
- `task_type`
- `task_created_at`
- `progress`
- `total_documents`
- `evaluation_region`
### 进度定义
`progress = 已完成任务文档数 / 任务文档总数 * 100`
这里的“已完成”只看:
- `leaudit_cross_review_task_documents.audit_status`
不看:
- `leaudit_documents.audit_status`
- `leaudit_audit_runs.status`
- `leaudit_rule_results`
## 5.3 任务状态
任务状态建议保留三态:
- `pending`
- `in_progress`
- `completed`
但如果当前产品没有明确“待启动”态,实际上可以只使用:
- `in_progress`
- `completed`
### 状态流转规则
- 创建任务 -> `in_progress`
- 所有任务内文档完成 -> `completed`
- 只要还有未完成文档 -> 保持 `in_progress`
### 禁止的隐式流转
不要因为:
- 提案全部投完
- 文档所有规则点都看过
- 有人上传了附件
就自动把任务置完成。
任务完成只能由“所有任务内文档被确认完成”推导出来。
## 6. 任务文档业务逻辑定稿
## 6.1 文档进入任务
文档进入任务只有两种方式:
- 创建任务时挂入
- 向已有任务追加文档
### 文档进入任务后默认状态
- `audit_status = 0`
即:
- 新纳入任务的文档,默认都还未完成交叉评查
## 6.2 文档完成的真正含义
任务内文档“完成”表示:
- 负责人或创建者确认:该文档在这个任务内的交叉评查工作结束
它不表示:
- 文档机器评查已完成
- 所有人都看过
- 所有提案都被通过
- 文档在全局业务上彻底完结
## 6.3 谁可以确认文档完成
只有:
- 任务创建者 `assigner`
- 主要负责人 `principal`
普通 `participant` 不可确认完成。
## 6.4 确认完成前的业务建议
建议在后端允许确认前做提醒型校验,而不是强阻断:
- 文档下是否还有 `pending` 提案
- 是否存在未投票成员
推荐策略:
- 默认允许负责人确认完成
- 如果仍有 `pending` 提案,返回告警信息给前端二次确认
这样更贴近真实业务,不会因为个别滞后投票把流程卡死。
## 7. 提案业务逻辑定稿
## 7.1 提案的最小绑定单位
提案必须绑定到:
- 一个任务
- 一个文档
- 一个规则结果 `rule_result_id`
不要只绑定:
- 文档 + 评查点名称
- 文档 + 规则名
因为这些都不够稳定。
## 7.2 谁可以提案
只有该任务成员可以提案。
### 必须满足
1. 用户是任务成员
2. 文档属于该任务
3. `rule_result_id` 属于该文档
4. 当前提案目标不是已删除/无效结果
## 7.3 提案唯一性规则
建议规则:
- 同一用户对同一任务下同一文档的同一 `rule_result_id`
- 同时只能存在一条“有效提案”
有效提案指:
- `pending`
- `approved`
- `rejected`
如果需要允许历史重提,建议前提是:
- 原提案已 `cancelled`
- 或被软删
## 7.4 提案分值规则
### 必须禁止
- `0` 分提案
### 必须校验
1. 当前分值已为 `0` 时,不允许继续扣分
2. 当前分值已达该规则满分时,不允许继续加分
3. 提案增量应用后,理论结果不能越界
边界是:
- 最低 `0`
- 最高该规则满分
## 7.5 提案创建后的自动动作
提案创建成功后:
1. 自动写入一条提案人 `agree` 投票
2. 立刻触发提案状态判定
## 8. 投票业务逻辑定稿
## 8.1 谁可以投票
只有该任务成员可以投票。
### 额外限制
- 非成员不能投
- 已删除提案不能投
- 已结束提案不能投
## 8.2 提案人能否投票
建议保留老逻辑:
- 提案创建后自动算提案人一票同意
这意味着:
- 提案人不需要再手动投一次
- 后续若要改票,只能通过显式撤销/重投逻辑支持
## 8.3 投票值
推荐只保留:
- `agree`
- `disagree`
“取消投票”不作为状态值保存,而是:
- 将该投票软删
## 8.4 一人一票
同一提案、同一成员:
- 同一时刻只能存在一条有效投票
如重复投票:
- 覆盖旧投票类型
## 9. 提案状态机定稿
提案状态建议固定为:
- `pending`
- `approved`
- `rejected`
- `cancelled`
## 9.1 创建后的初始状态
- `pending`
## 9.2 通过规则
设任务有效成员数为 `n`
通过阈值:
- `threshold = floor(n / 2) + 1`
若:
- `agree >= threshold`
则:
- `proposal.status = approved`
## 9.3 否决规则
若:
- `disagree >= threshold`
则:
- `proposal.status = rejected`
## 9.4 提前否决规则
若:
- 即使所有剩余未投票者都改投同意,`agree` 也达不到 `threshold`
则:
- `proposal.status = rejected`
## 9.5 撤销规则
只有提案人可撤销,且只允许撤销:
- `pending`
撤销后:
- `status = cancelled`
- 同时软删或停用关联投票
## 9.6 提案终态不可再操作
对于:
- `approved`
- `rejected`
- `cancelled`
不允许再继续投票或修改状态。
## 10. 分数语义定稿
这是实现中最容易“做着做着又回到老系统”的地方,必须说清楚。
## 10.1 三层分数
新项目详情页涉及三层分数:
### 机器分
来自:
- `leaudit_rule_results`
含义:
- 规则执行后机器给出的原始结果分值
### 人工审核分
来自:
- `leaudit_review_point_audits`
含义:
- 人工对规则结果的覆盖性判断
### 交叉评查调整分
来自:
- 所有已通过的 `leaudit_cross_review_proposals`
含义:
- 协作层加减分累计值
## 10.2 推荐最终展示分数计算
建议按以下顺序:
1. 先计算基础分 `baseScore`
2. 再叠加交叉评查增量 `crossDelta`
3. 再做边界裁剪
### `baseScore` 计算建议
若存在人工审核覆盖:
- 按人工审核后的通过/不通过结果计算基础分
否则:
- 按机器结果计算基础分
### `crossDelta`
取该 `rule_result_id` 下所有:
- `status = approved`
- `deleted_at is null`
提案的 `proposed_score_delta` 累加值
### `finalScore`
`finalScore = clamp(baseScore + crossDelta, 0, ruleMaxScore)`
## 10.3 绝对禁止
不要:
- 直接更新 `leaudit_rule_results.score`
- 直接把聚合后的最终分落回机器结果表
## 11. 详情页聚合逻辑定稿
当前详情页主入口已经是:
- `DocumentServiceImpl.GetReviewPoints()`
这条主链路可以保留。
## 11.1 详情页返回内容应包含
- 规则点列表
- 单点机器分
- 单点人工审核覆盖结果
- 单点最终展示分
- 交叉评查提案列表
- 文档级统计信息
## 11.2 提案列表读取规则
提案列表优先从:
- `leaudit_cross_review_proposals`
读取。
如果需要历史兼容,可短期允许:
- 新表优先
-`cross_scoring_proposals` 兜底
但这必须是过渡方案,不是长期方案。
## 11.3 文档总分统计
文档总分建议由每个规则点的最终展示分累加得到。
即:
- 总分不是单独存储字段
- 而是聚合计算结果
## 12. 版本归纳逻辑定稿
## 12.1 主规则
任务文档列表必须优先基于:
- `versionGroupKey`
归组。
### 当前版本判定
优先使用:
- `isLatestVersion = true`
### 历史版本排序
按:
- `versionNo desc`
或时间倒序
## 12.2 兜底规则
只有在极端兼容场景下,才允许用:
- `normalizedName + typeId`
做辅助兜底。
## 12.3 追加附件后的语义
追加附件如果产生新文档,则该新文档:
- 应进入同一版本链
- 自动挂到当前任务
- 默认 `audit_status = 0`
原因:
- 新版本进入任务后,需要重新做交叉评查确认
## 13. 权限逻辑定稿
## 13.1 权限判断分两层
### 第一层:RBAC 权限点
例如:
- `cross_review:task:read`
- `cross_review:task:create`
- `cross_review:proposal:create`
- `cross_review:proposal:vote`
- `cross_review:document:complete`
### 第二层:业务归属校验
例如:
- 是否是该任务成员
- 是否是负责人
- 文档是否属于该任务
- 规则结果是否属于该文档
两层都通过,才允许操作。
## 13.2 读权限
用户可读某任务,当且仅当:
-`assigner`
- 或是任务成员
## 13.3 写权限
### 创建提案
- 任务成员即可
### 投票
- 任务成员即可
### 撤销提案
- 仅提案人
### 确认文档完成
-`assigner``principal`
### 上传任务文档 / 追加附件
- 建议仅 `assigner``principal`
## 14. 实施时的接口行为定稿
## 14.1 后端必须兜住这些错误
- 文档不存在
- 文档不属于任务
- 规则结果不属于文档
- 用户不是任务成员
- 用户不是负责人
- 提案重复
- 提案已结束
- 分值越界
## 14.2 后端返回建议
每个关键接口尽量返回:
- `success`
- `message`
- 必要结果对象
- 必要告警信息
例如确认完成时可以带:
- `has_pending_proposals`
- `pending_proposal_count`
- `has_pending_votes`
## 15. 推荐开发顺序
为了避免做到一半逻辑漂移,建议严格按以下顺序:
### 第一步
先落表结构和状态定义。
### 第二步
实现 service 层纯业务逻辑:
- 任务
- 提案
- 投票
- 完成确认
### 第三步
实现 controller 接口。
### 第四步
把详情页聚合接到新提案表。
### 第五步
再做上传、附件、版本链整合。
## 16. 最终一句话定稿
新项目交叉评查的正确实现方式应该是:
-`leaudit_*` 作为文档与评查结果底座
-`leaudit_cross_review_*` 承接协作层
- 用聚合计算得到最终展示分
- 用任务内状态管理完成进度
- 用后端统一收口权限和状态流转
只要这五点不偏,整体实现就不会跑歪。
@@ -0,0 +1,568 @@
# 老项目交叉评查逻辑梳理
> 目标:整理老项目 `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`