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