# 新项目交叉评查详细业务逻辑定稿 > 目标:把新项目交叉评查的业务逻辑收敛成一份“可直接指导实现”的定稿,重点避免实现阶段把任务状态、评分语义、权限边界、版本逻辑和完成条件做错。 ## 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_*` 承接协作层 - 用聚合计算得到最终展示分 - 用任务内状态管理完成进度 - 用后端统一收口权限和状态流转 只要这五点不偏,整体实现就不会跑歪。