# 新项目交叉评查详细业务逻辑定稿

> 目标：把新项目交叉评查的业务逻辑收敛成一份“可直接指导实现”的定稿，重点避免实现阶段把任务状态、评分语义、权限边界、版本逻辑和完成条件做错。

## 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_*` 承接协作层
- 用聚合计算得到最终展示分
- 用任务内状态管理完成进度
- 用后端统一收口权限和状态流转

只要这五点不偏，整体实现就不会跑歪。

