Files
guoju0808/会议状态流转.md
T
郭庆泰andClaude 904e28710f feat: 生产部署 + 会议全链路 (执行方分配/费用结算/签到脱敏/H5相机)
- 生产 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>
2026-08-24 01:00:09 +08:00

314 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-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`