Files
guoju0808/会议状态流转.md
T

321 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 会议状态流转 —— 设计与实现总结
> 本文档是对「会议状态机」从单一 `current_stage` 枚举重构为**事实驱动模型**的完整设计说明。
> 供后续维护、二次开发、排查问题时查阅。最后更新时间:2026-08-24。
---
## 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 |
| `current_stage` | varchar | **派生缓存**9 值,见 §4 | 仅由 `StageDeriver`/控制器/调度器写,**业务代码不得直接改** |
> `is_executed` 为数据库既有字段,本次直接复用,未删未重加。
> 新增字段默认值:`material_audit_stage` 默认 `'NOT_SUBMITTED'`(已 ALTER),其余 int 默认 0、时间默认 NULL。
### 2.2 审核阶段枚举(4 值)
`material_audit_stage` 使用枚举 `MeetingAuditStageEnum`
| 值 | 中文 | 含义 |
|----|------|------|
| `NOT_SUBMITTED` | 未提交 | 执行方尚未提交(含提交后被退回、尚未重提的初始态) |
| `SUBMITTED` | 已提交 | 已提交,处于审核链路中(合规审或支持方审) |
| `APPROVED` | 审核通过 | 支持方(二级)审核通过 |
| `REJECTED` | 审核驳回 | 任一级驳回,退回执行方 |
> 注:用户拍板「已提交」和「待审核」**合并**为 `SUBMITTED`,因此审核阶段是 4 值而非 5 值。审核链路中「合规审中 vs 支持方审中」由 `*_compliance_approved` 布尔位区分(见 §5)。
---
## 3. 两级审核(合规 → 支持方)
`SUBMITTED` 内部再拆两级,由 `material_compliance_approved` 标记:
```
执行方提交 (SUBMITTED, compliance_approved=0)
┌─────────────────┐
│ 合规审核 (manager) │ audit-compliance
└─────────────────┘
│ 通过 → compliance_approved=1
│ 拒绝 → REJECTED(退回执行方)
┌─────────────────┐
│ 支持方审核 (监察员) │ audit-supervision
└─────────────────┘
│ 通过 → APPROVED
│ 拒绝 → REJECTED(退回执行方,并通知)
APPROVED
```
- 材料单独走这套两级链路(凭证审核链路已移除,凭证改由合规人员在结算时上传)。
- 「材料审核通过」后,若审核时间已超 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
material_audit_stage = APPROVED
AND material_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)看「审核通过」,其余方看「待审核」。
> - 展示名按材料阶段驱动。
---
## 5. 状态机流转总图
```
┌─────────────────────────────┐
│ NOT_STARTED 未执行 │
└──────────────┬──────────────┘
start_time 到点(调度器 markExecuted
┌─────────────────────────────┐
│ RUNNING 执行中 │
└──────┬───────────────┬──────┘
执行方提交材料 │ │ end_time+submit_deadline_days 到
(submit-material) │ │ 且 material 未提交(调度器 markFrozen
│ ▼
▼ │ ┌─────────────────┐
┌──────────────────┐ │ │ FROZEN 冻结中 │(终止,不可提交)
│ SUBMITTED 已提交 │◄┘ └─────────────────┘
│ (compliance=0) │
└────────┬─────────┘
合规审核 (audit-compliance, manager)
├─ 拒绝 → REJECTED ────────────┐
└─ 通过 → compliance_approved=1 │
▼ │
┌──────────────────────┐ │
│ 支持方审核 (audit- │ │
│ supervision, 监察员) │ │
└────────┬─────────────┘ │
├─ 拒绝 → REJECTED ─────────────┤
└─ 通过 → APPROVED │
│ │
material APPROVED │
│ │
┌─────────────────┴──┐ │
│ 未满 24h: │ │
│ SUPERVISION_APPROVED│ │
│ 审核通过 │ │
└────────┬──────────┘ │
满 24h(调度器 markSettlementReady)│
▼ │
┌─────────────────────┐ │
│ AWAITING_SETTLEMENT │ │
│ 待结算 │ │
└────────┬────────────┘ │
合规/管理员点「结算」(settle) │
▼ │
┌─────────────────────┐ │
│ SETTLED 已结算 │ │
└────────┬────────────┘ │
合规/管理员点「完结」(finish) │
▼ │
┌─────────────────────┐ │
│ 已完结 (is_finished) │ │
└─────────────────────┘ │
REJECTED 退回执行方 → 执行方看「已退回」, 可重新提交 ◄──┘
(material 回到 NOT_SUBMITTED 语义, 重新走 SUBMITTED)
```
---
## 6. 后端改动清单
| 文件 | 改动 |
|------|------|
| `service/StageDeriver.java`**新增** | 单一可信源:`derivePhysicalStage` + `deriveDisplay` + `settlementReady` |
| `domain/BizMeeting.java` | 新增事实字段 + getter/setter(凭证 3 字段已删,见 §12) |
| `mapper/BizMeetingMapper.java` + `.xml` | resultMap/selectFields/updateByPrimaryKey 补事实字段;调度 SQL 重写为 3 个 `update` |
| `scheduler/MeetingStageScheduler.java`**重写** | 3 个 `@Scheduled` 每分钟任务:`markExecuted` / `markFrozen` / `markSettlementReady` |
| `controller/BizMeetingController.java` | 重写审核段(见 §7);新增 `StageDeriver` 注入与 `appendAuditLog` 4 列版本 |
| `service/impl/BizMeetingServiceImpl.java` | 新建会议默认 `material_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``material_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}/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` | — |
- 结算接口已二合一:合规/管理员在结算时上传劳务凭证(LV_PAYMENT+ 会务凭证(SV_PAYMENT),缺任一报错「请上传凭证」;结算后凭证只读可见。
- 结算 → 完结是**两个独立的手动动作**(用户规则 1),不能跳步。
- `supervision-opinion` 旧接口已删除,支持方审批统一走 `audit-supervision`
### 7.1 审核日志(`biz_meeting_audit_log`
`appendAuditLog(m, auditType, result, opinion)` 不再写单一 `current_stage`,改为写 **4 个角色列**,每个记录流转那一刻各角色看到的展示态(auditType 现仅 `MATERIAL` 审核链路与 `SETTLE` 结算两类):
```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 仅材料;新增「结算」「完结」按钮(结算 dialog 内含劳务/会务凭证上传,二合一)+ `canSettle`/`canFinish` + `onSettle`/`onFinish`;凭证结算后只读展示 |
| `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. **凭证改由结算时上传**:执行方不再提交/审核凭证,凭证由合规/管理员在结算时一并上传(二合一),需同时有劳务凭证与会务凭证。
---
## 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`
---
## 12. 近期变更:凭证流程移除(2026-08-24)
**改动动机**:执行方不再保存/提交/审核凭证,凭证改为由合规人员在结算时一并上传。
1. **移除执行方凭证链路**:删除 `POST /{meetingId}/submit-voucher`;审核接口不再支持 `auditType=VOUCHER/BOTH` 多选,审核链路只剩材料一条(`auditType` 固定 `MATERIAL`)。
2. **结算二合一**`POST /{meetingId}/settle` 改为「上传凭证 + 结算」合并。合规/管理员点击结算时,必须同时上传劳务凭证(`LV_PAYMENT`)与会务凭证(`SV_PAYMENT`),缺任一报错「请上传凭证」;前置校验:材料 `APPROVED` + `fee_calc_status=1` + 未结算。
3. **凭证落库**:凭证作为 `biz_meeting_material``LABOR_VOUCHER`/`SERVICE_VOUCHER`)保存,不参与费用汇总;结算后凭证只读可见。
4. **DDL**`biz_meeting` 已 DROP 三个死列 `voucher_audit_stage` / `voucher_audit_time` / `voucher_compliance_approved`,对应 domain/mapper/service 字段与映射同步删除。