595 lines
12 KiB
Markdown
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()`
|
|
|
|
如果这几项都确认过,交叉评查的实现基本就不会跑偏。
|
|
|