# 会议状态流转 —— 设计与实现总结 > 本文档是对「会议状态机」从单一 `current_stage` 枚举重构为**事实驱动模型**的完整设计说明。 > 供后续维护、二次开发、排查问题时查阅。最后更新时间:2026-08-23。 --- ## 1. 背景与动机 ### 1.1 旧模型的问题 旧实现用 `biz_meeting.current_stage` 一个枚举字段承载整个状态机,存在三类结构性缺陷: 1. **单字段多语义**:一个字段既要表达「执行/审核/结算/冻结」等事实,又要表达「每个角色看到什么」,二者耦合,任何角色差异都要靠 `if/else` 硬编码中文标签,导致历史 bug(如 `BizDashboardController` 拿枚举码去比中文标签,统计恒为 0)。 2. **审核链路无法表达**:材料/凭证需要「两级审核」(合规 → 支持方),单枚举 `AWAITING_SPONSOR` 之类无法区分「合规审中」还是「支持方审中」,也无法区分「材料退回」与「凭证退回」。 3. **时间驱动状态靠手改**:冻结、待结算(24h)这类时间门槛没有落地字段,只能靠人工或外置脚本改状态。 ### 1.2 新模型的核心思想 **事实与展示分离**: - `biz_meeting` 只存**事实**(`is_executed`/`is_frozen`/`is_settled`/`is_finished` + 材料/凭证审核阶段 + 审核时间 + 两级审核标记)。 - 各角色看到的「阶段名称」由这些事实**实时推导**,不再是一个固定枚举投影。 > 类比:数据库不存「年龄」,只存「出生日期」,「年龄」是算出来的。这里 `biz_meeting` 只存「发生了什么」,各角色看到的「当前处于什么阶段」是算出来的。 --- ## 2. 数据模型(事实字段) ### 2.1 `biz_meeting` 新增/复用的状态事实字段 | 字段 | 类型 | 含义 | 谁来写 | |------|------|------|--------| | `is_executed` | int(0/1) | 是否执行 | 调度器 `markExecuted`(start_time 到点) | | `execute_time` | datetime | 执行时间 | 调度器 | | `is_frozen` | int(0/1) | 是否冻结 | 调度器 `markFrozen`(逾期未提交) | | `freeze_time` | datetime | 冻结时间 | 调度器 | | `is_settled` | int(0/1) | 是否结算 | 合规/管理员点击「结算」 | | `settle_time` | datetime | 结算时间 | 同上 | | `is_finished` | int(0/1) | 是否完结 | 合规/管理员点击「完结」 | | `finish_time` | datetime | 完结时间 | 同上 | | `material_audit_stage` | varchar | 材料审核阶段(4 值,见 §3) | 执行方提交 / 审核动作 | | `material_audit_time` | datetime | 材料最近一次审核动作时间 | 同上 | | `material_compliance_approved` | int(0/1) | 材料合规(一级)是否通过 | 合规审核通过时置 1 | | `voucher_audit_stage` | varchar | 凭证审核阶段(4 值) | 执行方提交 / 审核动作 | | `voucher_audit_time` | datetime | 凭证最近一次审核动作时间 | 同上 | | `voucher_compliance_approved` | int(0/1) | 凭证合规(一级)是否通过 | 合规审核通过时置 1 | | `current_stage` | varchar | **派生缓存**(9 值,见 §4) | 仅由 `StageDeriver`/控制器/调度器写,**业务代码不得直接改** | > `is_executed` 为数据库既有字段,本次直接复用,未删未重加。 > 新增字段默认值:`material_audit_stage`/`voucher_audit_stage` 默认 `'NOT_SUBMITTED'`(已 ALTER),其余 int 默认 0、时间默认 NULL。 ### 2.2 审核阶段枚举(4 值) `material_audit_stage` / `voucher_audit_stage` 共用,枚举 `MeetingAuditStageEnum`: | 值 | 中文 | 含义 | |----|------|------| | `NOT_SUBMITTED` | 未提交 | 执行方尚未提交(含提交后被退回、尚未重提的初始态) | | `SUBMITTED` | 已提交 | 已提交,处于审核链路中(合规审或支持方审) | | `APPROVED` | 审核通过 | 支持方(二级)审核通过 | | `REJECTED` | 审核驳回 | 任一级驳回,退回执行方 | > 注:用户拍板「已提交」和「待审核」**合并**为 `SUBMITTED`,因此审核阶段是 4 值而非 5 值。审核链路中「合规审中 vs 支持方审中」由 `*_compliance_approved` 布尔位区分(见 §5)。 --- ## 3. 两级审核(合规 → 支持方) `SUBMITTED` 内部再拆两级,由 `material_compliance_approved` / `voucher_compliance_approved` 标记: ``` 执行方提交 (SUBMITTED, compliance_approved=0) │ ▼ ┌─────────────────┐ │ 合规审核 (manager) │ audit-compliance └─────────────────┘ │ 通过 → compliance_approved=1 │ 拒绝 → REJECTED(退回执行方) ▼ ┌─────────────────┐ │ 支持方审核 (监察员) │ audit-supervision └─────────────────┘ │ 通过 → APPROVED │ 拒绝 → REJECTED(退回执行方,并通知) ▼ APPROVED ``` - 材料、凭证各自独立走这套两级链路,可**同时提交、同时审核**(`auditType = MATERIAL|VOUCHER|BOTH`)。 - 「材料合规通过 + 凭证也通过」后,若最晚审核时间已超 24h,则进入「待结算」。 --- ## 4. 派生逻辑(单一可信源:`StageDeriver`) 后端 `com.ruoyi.business.service.StageDeriver` 是**唯一**推导逻辑所在地,前端 `utils/meetingStage.js` 是其镜像。 ### 4.1 物理阶段(9 值,`derivePhysicalStage`) 用于 `current_stage` 缓存 + 列表筛选精确匹配,按优先级自上而下: | 优先级 | 事实条件 | 物理阶段 | |--------|----------|----------| | 1 | `is_frozen=1` | `FROZEN` 冻结中 | | 2 | `is_finished=1` 或 `is_settled=1` | `SETTLED` 已结算 | | 3 | `material_audit_stage=REJECTED` | `RECTIFYING` 待整改 | | 4 | `material=APPROVED` 且 `settlementReady` | `AWAITING_SETTLEMENT` 待结算 | | 5 | `material=APPROVED` 且未满足 24h | `SUPERVISION_APPROVED` 审核通过 | | 6 | `material=SUBMITTED` 且 `material_compliance_approved=1` | `AWAITING_SUPERVISION` 待支持方审核 | | 7 | `material=SUBMITTED` 且 `material_compliance_approved=0` | `AWAITING_COMPLIANCE` 待合规审核 | | 8 | `material=NOT_SUBMITTED` 且 `is_executed=1` | `RUNNING` 执行中 | | 9 | `material=NOT_SUBMITTED` 且 `is_executed=0` | `NOT_STARTED` 未执行 | **`settlementReady` 判据**(待结算,用户拍板口径): ```text voucher_audit_stage = APPROVED AND max(material_audit_time, voucher_audit_time) 距今 >= 24 小时(自然日) ``` 即:材料+凭证都通过,且**最晚通过的那个时间点**已超 24h。 ### 4.2 各角色展示名(`deriveDisplay`) 展示层按角色折叠,优先级自上而下: | 事实条件 | executor | sponsor(支持方) | manager(合规) | admin / doctor / expert | |----------|----------|------------------|-----------------|------------------------| | `is_frozen=1` | 冻结中 | 冻结中 | 冻结中 | 冻结中 | | `is_finished=1` | 已完结 | 已完结 | 已完结 | 已完结 | | `is_settled=1` | 已结算 | 已结算 | 已结算 | 已结算 | | `material=REJECTED` | **已退回** | 待整改 | 待整改 | 待整改 | | `material=APPROVED` 且 24h | 待结算 | 待结算 | 待结算 | 待结算 | | `material=APPROVED` 未满 24h | **审核通过** | 审核通过 | 审核通过 | 审核通过 | | `material=SUBMITTED` 且合规已过 | 待审核 | 待审核 | **审核通过** | 待审核 | | `material=SUBMITTED` 且合规审中 | 待审核 | **已执行未传材料** | 待审核 | 待审核 | | `material=NOT_SUBMITTED` 且已执行 | **执行中** | 已执行未传材料 | 已执行未传材料 | 已执行未传材料 | | `material=NOT_SUBMITTED` 且未执行 | 未执行 | 未执行 | 未执行 | 未执行 | > 关键差异点: > - **退回**:只有执行方看「已退回」,其余方看「待整改」。 > - **合规审中**:支持方(只读旁观者)看「已执行未传材料」。 > - **支持方审中**:合规(manager)看「审核通过」,其余方看「待审核」。 > - 展示名**优先按材料阶段**驱动(用户规则:材料优先),凭证阶段只在 `settlementReady` 判断中起作用。 --- ## 5. 状态机流转总图 ``` ┌─────────────────────────────┐ │ NOT_STARTED 未执行 │ └──────────────┬──────────────┘ start_time 到点(调度器 markExecuted) ▼ ┌─────────────────────────────┐ │ RUNNING 执行中 │ └──────┬───────────────┬──────┘ 执行方提交材料/凭证 │ │ end_time+submit_deadline_days 到 (submit-material/ │ │ 且 material 未提交(调度器 markFrozen) submit-voucher) │ ▼ ▼ │ ┌─────────────────┐ ┌──────────────────┐ │ │ FROZEN 冻结中 │(终止,不可提交) │ SUBMITTED 已提交 │◄┘ └─────────────────┘ │ (compliance=0) │ └────────┬─────────┘ 合规审核 (audit-compliance, manager) ├─ 拒绝 → REJECTED ────────────┐ └─ 通过 → compliance_approved=1 │ ▼ │ ┌──────────────────────┐ │ │ 支持方审核 (audit- │ │ │ supervision, 监察员) │ │ └────────┬─────────────┘ │ ├─ 拒绝 → REJECTED ─────────────┤ └─ 通过 → APPROVED │ │ │ material+voucher 都 APPROVED │ │ │ ┌─────────────────┴──┐ │ │ 未满 24h: │ │ │ SUPERVISION_APPROVED│ │ │ 审核通过 │ │ └────────┬──────────┘ │ 满 24h(调度器 markSettlementReady)│ ▼ │ ┌─────────────────────┐ │ │ AWAITING_SETTLEMENT │ │ │ 待结算 │ │ └────────┬────────────┘ │ 合规/管理员点「结算」(settle) │ ▼ │ ┌─────────────────────┐ │ │ SETTLED 已结算 │ │ └────────┬────────────┘ │ 合规/管理员点「完结」(finish) │ ▼ │ ┌─────────────────────┐ │ │ 已完结 (is_finished) │ │ └─────────────────────┘ │ │ REJECTED 退回执行方 → 执行方看「已退回」, 可重新提交 ◄──┘ (material/voucher 回到 NOT_SUBMITTED 语义, 重新走 SUBMITTED) ``` --- ## 6. 后端改动清单 | 文件 | 改动 | |------|------| | `service/StageDeriver.java`(**新增**) | 单一可信源:`derivePhysicalStage` + `deriveDisplay` + `settlementReady` | | `domain/BizMeeting.java` | 新增 12 个事实字段 + getter/setter | | `mapper/BizMeetingMapper.java` + `.xml` | resultMap/selectFields/updateByPrimaryKey 补 12 字段;调度 SQL 重写为 3 个 `update` | | `scheduler/MeetingStageScheduler.java`(**重写**) | 3 个 `@Scheduled` 每分钟任务:`markExecuted` / `markFrozen` / `markSettlementReady` | | `controller/BizMeetingController.java` | 重写审核段(见 §7);新增 `StageDeriver` 注入与 `appendAuditLog` 4 列版本 | | `service/impl/BizMeetingServiceImpl.java` | 新建会议默认 `material/voucher_audit_stage = 'NOT_SUBMITTED'`(原 `INIT`) | | `mapper/BizMeetingAuditLogMapper.java` + `.xml` | `current_stage` 拆为 4 列:`executor_stage`/`sponsor_stage`/`manager_stage`/`admin_stage` | | `controller/BizDashboardController.java` | 修复历史 bug:中文标签比对 → 枚举码比对 | ### 6.1 调度器 SQL 语义 - `markExecuted`:`start_time <= NOW()` 且 `material_audit_stage='NOT_SUBMITTED'` 且 `is_executed=0` 且 `is_frozen=0` → `is_executed=1, execute_time=NOW(), current_stage='RUNNING'`。 - `markFrozen`:`material='NOT_SUBMITTED'` 且 `date_add(end_time, interval p.submit_deadline_days day) <= NOW()` → `is_frozen=1, freeze_time=NOW(), current_stage='FROZEN'`(`submit_deadline_days` 取自 `biz_project`,join 软删过滤)。 - `markSettlementReady`:材料+凭证都 `APPROVED` 且 `greatest(material_audit_time, voucher_audit_time) <= NOW() - INTERVAL 1 DAY` → `current_stage='AWAITING_SETTLEMENT'`。 --- ## 7. 后端接口(审核链路) | 接口 | 角色 | 前置校验 | 通过后 | 拒绝后 | |------|------|----------|--------|--------| | `POST /{meetingId}/submit-material` | 执行方(`isExecutorOfProject`) | 已执行 + 未冻结 + stage∈{NOT_SUBMITTED,REJECTED} + 至少 1 条 `L_*` 材料 + 1 条 `M_*` 材料 | material→SUBMITTED, compliance=0 | — | | `POST /{meetingId}/submit-voucher` | 执行方 | 已执行 + 未冻结 + stage∈{NOT_SUBMITTED,REJECTED} + 有 `LV_PAYMENT` + `SV_PAYMENT` | voucher→SUBMITTED, compliance=0 | — | | `POST /{meetingId}/audit-compliance` | 合规(manager) | stage=SUBMITTED 且 compliance=0;拒绝需意见 | compliance=1(转入支持方) | →REJECTED(退回) | | `POST /{meetingId}/audit-supervision` | 监察员(`biz_meeting_supervisor`) | stage=SUBMITTED 且 compliance=1;拒绝需意见 | →APPROVED(材料通过时一并写监管意见) | →REJECTED(退回并通知执行方) | | `POST /{meetingId}/settle` | 合规/管理员 | 材料+凭证都 APPROVED | `is_settled=1, settle_time` | — | | `POST /{meetingId}/finish` | 合规/管理员 | `is_settled=1` | `is_finished=1, finish_time` | — | - `auditType` 支持 `MATERIAL`/`VOUCHER`/`BOTH`(多选,`resolveTypes` 展开)。 - 结算 → 完结是**两个独立的手动动作**(用户规则 1),不能跳步。 - `supervision-opinion` 旧接口已删除,支持方审批统一走 `audit-supervision`。 ### 7.1 审核日志(`biz_meeting_audit_log`) `appendAuditLog(m, auditType, result, opinion)` 不再写单一 `current_stage`,改为写 **4 个角色列**,每个记录流转那一刻各角色看到的展示态: ```text executor_stage / sponsor_stage / manager_stage / admin_stage ``` (由 `stageDeriver.deriveDisplay(role, m)` 实时生成)。旧 `current_stage` 列保留在库里但不再读写(遗留,见 §10)。 --- ## 8. 前端改动清单 | 文件 | 改动 | |------|------| | `utils/meetingStage.js`(**重写**) | 镜像 `StageDeriver`:`derivePhysicalStage` / `deriveStage` / `stageLabel` / `stageClass` / `stageTag` / `STAGE_OPTIONS` | | `views/meetings/Meetings.vue` | admin/manager/sponsor 共享列表:`stageLabel(roleSegment, row)` + `stageClass(row)`;sponsor「审批」按钮条件 `derivePhysicalStage(row)==='AWAITING_SUPERVISION'`,点击 POST `audit-supervision` | | `views/meetings/MeetingDetail.vue` | 审核 dialog 改多选 checkbox(材料/凭证);新增「结算」「完结」按钮 + `canSettle`/`canFinish` + `onSettle`/`onFinish`;`fmtAuditStage` 4 值标签 | | `views/executor/Meetings.vue` | `stageLabel('executor', row)`(列表 + 监督意见 dialog) | | `views/doctor/Meetings.vue` | `stageTag(row)` + `stageLabel('doctor', row)`;筛选下拉改用 `STAGE_OPTIONS` | > `meetingStage.js` 与后端 `StageDeriver` 是**严格镜像**:任何一处改了推导规则,另一处必须同步,否则列表显示与详情/日志不一致。 --- ## 9. 关键设计决策(拍板记录) 1. **结算 → 完结需手动点击**:`settle` 与 `finish` 是两个独立动作,合规/管理员各点一次。 2. **待结算 24h 门槛**:材料+凭证都通过且「最晚通过时间」超 24h 才进「待结算」。 3. **冻结判据**:`end_time + project.submit_deadline_days` 天;未提交判据 = `material_audit_stage == NOT_SUBMITTED`。 4. **已提交 / 待审核合并**:审核阶段 4 值(`SUBMITTED` 内部靠 `compliance_approved` 分两级)。 5. **`is_executed(0,1)` 复用**:DB 已有,直接使用,未删未重加。 6. **材料优先**:展示名优先按材料阶段驱动,凭证阶段仅参与 `settlementReady` 判断。 --- ## 10. 已知问题 / 待办 / 注意事项 ⚠️ 1. **24h 是软门槛**:`settle` 接口和前端「结算」按钮只校验「材料+凭证都 APPROVED」,**未强制校验 24h**。24h 目前只体现在展示名(「审核通过」→「待结算」)和调度器翻转 `current_stage`。若需硬校验,需给 `canSettle` + `settle` 加 `settlementReady` 判据。 2. **执行方新增「审核通过」态**:材料支持方通过后、满 24h 前,执行方看到「审核通过」,此态不在原始需求清单中,需确认是否可接受。 3. **`views/sponsor/Meetings.vue` 是死代码**:路由未引用(sponsor 复用共享 `meetings/Meetings.vue`),内含非法枚举 `AWAITING_SPONSOR`、不存在的 `auditStatus`/`auditOpinion` 字段、直接写 `current_stage` 的「结算/解冻」逻辑。建议删除。 4. **`current_stage` 是派生缓存**:仅 `StageDeriver` + 控制器转换端点 + 调度器可写,**任何业务代码不得直接 `update current_stage`**(写了也会被下次推导覆盖)。 5. **`biz_meeting_audit_log.current_stage` 遗留列**:新 mapper 已改用 4 角色列,旧列未删(按约定未执行 DROP)。 6. **未编译**:本次开发未跑 `mvn install` / `npm build`。`StageDeriver` 是新增 `@Component`,走 `.m2` jar 加载时需先 `mvn install` 才生效。 7. **前端/后端镜像同步**:`meetingStage.js` 与 `StageDeriver` 必须同步修改,改推导规则时两处一起改。 --- ## 11. 附:涉及文件速查 **后端**(`ry-api/ruoyi-business/.../business/`) - `service/StageDeriver.java`(新增) - `controller/BizMeetingController.java` - `scheduler/MeetingStageScheduler.java` - `domain/BizMeeting.java` - `mapper/BizMeetingMapper.java` + `resources/mapper/business/BizMeetingMapper.xml` - `mapper/BizMeetingAuditLogMapper.java` + `.xml` - `service/impl/BizMeetingServiceImpl.java` - `controller/BizDashboardController.java` **前端**(`ry-vue3/src/`) - `utils/meetingStage.js` - `views/meetings/Meetings.vue` - `views/meetings/MeetingDetail.vue` - `views/executor/Meetings.vue` - `views/doctor/Meetings.vue`