Files
leaudit-platform-backend/docs/交叉评查/新项目交叉评查状态流转图与边界案例清单.md
T

595 lines
12 KiB
Markdown

# 新项目交叉评查状态流转图与边界案例清单
> 目标:把新项目交叉评查的状态流转、时序过程和高风险边界案例一次性说清,避免实现时出现“以为如此,实际上不是”的尴尬。
## 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()`
如果这几项都确认过,交叉评查的实现基本就不会跑偏。