- 生产 nginx 三路径: ry-vue3 /hg/, ry-h5 /camera/, API /hg-api, .env 分环境 - 签约链接/摄像头 base-url 走 yml, 不再硬编码 /hg 与 localhost - 新增 biz_project_executor_assign (镜像 sponsor_assign) 执行方项目级分配 - 会议费用后台汇总 FeeCalcScheduler + 结算回写项目金额 + 状态流转调度 - 签到表拍照高斯模糊脱敏 extra_oss_url, 新增 PosterService/StageDeriver - 清理 biz_support_intent 旧表 (6 Java/XML 删除) - ry-h5 相机黑屏修复: 显式 video.play() + 动态 apiBase 上传 - 资源: simhei 字体 / logo.png / qrcode_1.png / favicon.ico Co-Authored-By: Claude <noreply@anthropic.com>
314 lines
20 KiB
Markdown
314 lines
20 KiB
Markdown
# 会议状态流转 —— 设计与实现总结
|
||
|
||
> 本文档是对「会议状态机」从单一 `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`
|