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

12 KiB

新项目交叉评查状态流转图与边界案例清单

目标:把新项目交叉评查的状态流转、时序过程和高风险边界案例一次性说清,避免实现时出现“以为如此,实际上不是”的尴尬。

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. 任务状态流转图

创建任务
  |
  v
in_progress
  |
  | 所有任务文档 audit_status = 1
  v
completed

3.1 任务创建后状态

  • 默认直接进入 in_progress

3.2 任务完成条件

只有当:

  • 该任务下所有未删除任务文档都为 audit_status = 1

才允许变为:

  • completed

3.3 明确不是任务完成条件的事件

以下事件都不能直接导致任务完成:

  • 文档机器评查跑完
  • 所有提案都投票结束
  • 没有提案
  • 所有人都看过详情页
  • 追加了新附件

4. 任务文档状态流转图

文档被挂入任务
  |
  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. 提案状态流转图

创建提案
  |
  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. 投票时序图

参与人 -> 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. 提案创建时序图

参与人 -> 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. 文档完成确认时序图

负责人 -> 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. 详情页分数计算时序图

前端详情页 -> 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. 版本链时序图

用户上传新版本/追加附件
  |
  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()

如果这几项都确认过,交叉评查的实现基本就不会跑偏。