初始提交:三端设计文档与 dev 代码基线

docs/:系统梳理、架构优化方案、CR-02 三端功能设计修订(含 §15 三端一致性同步与平台统计分析)、公寓端/应用端/平台端分册、公寓统计分析与排名报表、公寓端小步迭代开发计划(IT-0~IT-8)

dev/:RuoYi-Vue-master(后端)与 RuoYi-Vue2-master(前端)官方上游基线,未改动
This commit is contained in:
zfjia committed 2026-11-10 13:43:05 +08:00
commit 329e27b515
723 files changed
+99917

No files matched your search

File diff suppressed because it is too large. Load diff
+451
View File
@@ -0,0 +1,451 @@
# 11 既有数据库结构分析(KingbaseES)
> 文档性质:现状实证分析(Analysis Only)。本文所有结论来自对 `db/gygl.sql` 的结构解析与对已导入实例的只读查询(全部为 `SELECT`),**不含**数据库变更、DDL 执行、配置修改。改造建议仅为设计输入,需评审后另行排期。
## 0. 文档说明
表 0-1 采集信息
| 项 | 内容 |
| --- | --- |
| 数据来源 | `db/gygl.sql`(Navicat 导出,源库 `10.33.165.41:54321/gygl`,导出时间 2026-11-07 14:34) |
| 校验实例 | 本机 KingbaseES 导入库,`jdbc:kingbase8://127.0.0.1:54321/gygl` |
| 采集方式 | JDBC 只读查询(information_schema / pg_catalog / pg_stat_user_tables)+ dump 文本解析 |
| 库与 schema | 数据库 `gygl`,业务 schema `gygl`,表 owner 均为 `system` |
| 规模 | 40 张表、524 个字段、schema 总容量 117 MB、495,772 行数据 |
| 关联文档 | `05-数据模型.md`(表语义与状态码)、`09-迭代指引与风险清单.md`(技术债)、`10-架构优化方案-三端拆分.md`(目标归属) |
> 样本口径提醒:本机实例的数据是导出快照,业务数据只覆盖 2 个公寓(见 3.9)。行数、分布用于判断**结构缺陷**,不作为生产容量基线;容量测算需在生产库重跑附录 A 的 SQL。
## 1. 实例与库级特征
表 1-1 库级参数(实测)
| 参数 | 实测值 | 结构影响 |
| --- | --- | --- |
| 版本 | `KingbaseES V009R001C010`(`server_version` 报 12.1) | 兼容 PG 12 协议;JDBC 9.0.0 驱动可直连 |
| 兼容模式 | `database_mode = oracle` | 出现 `sys.date`、`sys_guid()`、`sysdate()`、`dual` 表等 Oracle 兼容对象 |
| 空串语义 | `ora_input_emptystr_isnull = on` | **写入 `""` 存成 `NULL`**,读取恒为 `NULL`;见 3.4 |
| 字符集 | `UTF8` | 中文数据无乱码风险 |
| MAC 扩展 | schema `sysmac`(8 张表,248 kB) | 金仓安全版强制访问控制扩展;上线前需确认是否启用策略 |
| 对象种类 | 视图 0、触发器 0、函数 0、分区表 0 | **业务逻辑全部在 Java 侧**,库内无任何约束性逻辑 |
表 1-2 schema 分布
| schema | 表数 | 容量 | 说明 |
| --- | --- | --- | --- |
| `gygl` | 40 | 117 MB | 全部业务表 |
| `sysmac` | 8 | 248 kB | 金仓安全版 MAC 元数据(空表) |
| `sys_hm` | 3 | 128 kB | 金仓自带 |
| `sys` | 1 | 40 kB | 金仓自带 |
结论:**这是一个"纯存储库"**——没有主键、没有索引、没有外键、没有触发器、没有存储过程。数据完整性、唯一性、顺序性、并发控制全部由应用代码承担,这也是下一章几乎所有缺陷的共同根因。
## 2. 表清单与规模
图 2-1 数据域与表簇
```mermaid
graph LR
A[公寓主数据<br/>gy_jbxx / gy_gyzd / gy_sjgz / gy_jcsj / gy_jrb] --> B[房间<br/>gy_rooms]
B --> C[叫乘执行<br/>gy_jcxx / gy_jcxx_his<br/>gy_bjxx / gy_bjcstj]
D[交路同步<br/>gy_syn_jcxx / _his / _status] --> C
E[人员与卡<br/>gy_ykt_ry / gy_ykt_ry_zp] --> C
F[账务<br/>gy_ykt_remainder / _allowance / _consume / _btbj / _btjs] --> E
G[设备<br/>gy_sb / gy_device / gy_sb_xt / gy_sb_zl / _his] --> C
H[权限<br/>gl_yh / gl_js / gl_qx / gl_cd / gl_yh_js / gl_js_qx]
I[日志<br/>gy_logs / gy_operation_log]
```
表 2-1 业务表清单(按数据域)
| 表 | 行数 | 大小 | 列数 | 含 gyid | 现职责 |
| --- | --- | --- | --- | --- | --- |
| `gy_jbxx` | 9 | 16 kB | 26 | ✔ | 公寓基础信息(面积、床位、负责人、热水/燃气方式) |
| `gy_gyzd` | 37 | 16 kB | 8 | ✔ | 公寓字典(网关地址、机器号、IP) |
| `gy_gzzd` | 17 | 16 kB | 3 | — | 工种字典 + 客货类型 |
| `gy_sjgz` | 17 | 16 kB | 10 | ✔ | 时间规则(入住/离寓/叫乘基准) |
| `gy_jcsj` | 7 | 16 kB | 3 | — | 叫乘时刻表(`cb/kssj/jssj`,均为字符) |
| `gy_jrb` | 2 | 16 kB | 6 | — | 节假日 |
| `gy_rooms` | 197 | 64 kB | 22 | ✔ | 房间基线 + 房态 + 分机 + 报警起点(三类语义混装) |
| `gy_jcxx` | 3,541 | 1.5 MB | **70** | ✔ | 叫乘执行主表(上帝表,见 3.5) |
| `gy_jcxx_his` | 32,505 | 12 MB | **70** | ✔ | 叫乘归档表 |
| `gy_bjxx` | 2,490 | 304 kB | 7 | ✔ | 报警记录(开始/结束时间) |
| `gy_bjcstj` | 0 | 8 kB | 12 | — | 补叫统计(**未使用**) |
| `gy_jdqkhz` | 1 | 16 kB | 9 | ✔ | 房间年度/月度汇总(近似废表) |
| `gy_rtjb` | 0 | 8 kB | 7 | ✔ | 日报(**未使用**) |
| `gy_call_duty_match_lock` | 38 | 8 kB | 1 | ✔ | 单列行锁表,用 `gyid` 做悲观锁 |
| `gy_syn_jcxx` | 1,139 | 384 kB | 15 | — | 运安交路快照 |
| `gy_syn_jcxx_his` | 2,291 | 376 kB | 12 | — | 交路归档 |
| `gy_syn_jcxx_status` | 1 | 8 kB | 2 | — | 同步状态/水位(单行开关) |
| `gy_ykt_ry` | 1,349 | 336 kB | 27 | — | 人员与卡主数据 |
| `gy_ykt_ry_zp` | 100 | **27 MB** | 2 | — | 照片(BLOB 存库) |
| `gy_ykt_remainder` | 1,253 | 224 kB | 12 | — | 余额(补助/现金/消费/补助总额) |
| `gy_ykt_allowance` | 34,182 | 4.2 MB | 11 | ✔ | 补助发放流水 |
| `gy_ykt_consume` | 35 | 16 kB | 19 | ✔ | 消费流水 |
| `gy_ykt_btbj` | 5 | 16 kB | 8 | ✔ | 补助标准(早/正/节假日) |
| `gy_ykt_btjs` | 5 | 16 kB | 7 | — | 补助计算规则 |
| `gy_sb` | 148 | 48 kB | 8 | ✔ | 设备台账 v1(`devicecode/lx`) |
| `gy_device` | 92 | 48 kB | 18 | ✔ | 设备台账 v2(`device_code/device_type`,RuoYi 风格) |
| `gy_sb_xt` | 246 | 64 kB | 9 | ✔ | 设备心跳与版本状态 |
| `gy_sb_zl` | **129,665** | 11 MB | 6 | — | 设备指令队列(**与历史同表**) |
| `gy_sb_zl_his` | 0 | 8 kB | 6 | — | 指令历史(**从未使用**) |
| `gy_logs` | **285,914** | **56 MB** | 7 | ✔ | 运行日志(无清理策略) |
| `gy_operation_log` | 0 | 8 kB | 6 | — | 操作审计(**从未写入**) |
| `gy_file` | 1 | 16 kB | 6 | — | 文件登记 |
表 2-2 支撑表清单
| 表 | 行数 | 列数 | 说明 |
| --- | --- | --- | --- |
| `gl_yh` / `gl_js` / `gl_qx` / `gl_cd` / `gl_yh_js` / `gl_js_qx` | 17 / 4 / 48 / 45 / 16 / 58 | 18 / 6 / 9 / 9 / 2 / 2 | 自研 RBAC(用户/角色/权限/菜单及关联) |
| `gen_table` / `gen_table_column` | 18 / 279 | 21 / 22 | **RuoYi 代码生成器元数据残留** |
容量结构:`gy_logs` 占 48%、`gy_ykt_ry_zp` 占 23%、`gy_jcxx_his` 占 10%、`gy_sb_zl` 占 9%——四张表吃掉全库 90%。其中两张(日志、指令)是纯追加、无清理的表。
## 3. 结构缺陷诊断
表 3-1 约束与索引现状(全库实测)
| 指标 | 数量 | 说明 |
| --- | --- | --- |
| 主键 | **0** | 40 张表无一有主键 |
| 唯一约束 | **0** | 唯一性完全依赖应用层判断 |
| 外键 | **0** | 关联完整性靠代码,无 DB 兜底 |
| 索引 | **0** | 含隐式主键索引在内,`pg_indexes` 计数为 0 |
| 序列 | 64 | 仅 4 个被列默认值引用,其余 60 个为**孤儿序列** |
| 表注释 | 3/40 | 仅 `gen_table`、`gen_table_column`、`gy_device` 有 |
| 列注释 | 60/524 | 其中仅 5 条属于业务表(`gy_jcxx` 3 条、`gy_syn_jcxx` 1 条、`gy_gzzd` 1 条) |
| 非空约束 | 5 处 | 仅 `gy_ykt_ry.ryid`、`gy_call_duty_match_lock.gyid`、`gy_syn_jcxx_status.id/sync_status`、`gy_device.device_code` |
### 3.1 无主键、无索引的连锁后果
1. **每次按 `devicecode` / `ryid` / `jcid` 查询都是全表扫描**。`gy_sb_zl` 12.9 万行、`gy_logs` 28.6 万行,心跳与指令链路是高频路径,随运行时间线性劣化。
2. **重复数据无法阻断**。实测 `gy_jcxx` 3,541 行中 `jcid` 只有 3,540 个不同值——已存在 1 组重复 `jcid`,且没有任何机制能阻止再次发生。
3. **更新语义不可靠**。`update ... where jcid=?` 命中多行时,MyBatis 返回的影响行数与业务预期不符,容易被误判为"成功"。
4. **分页与排序无稳定基准**。无主键导致同一秒内多行顺序不确定,翻页可能重复或漏行。
5. **归档与对账无法定位单行**。`gy_jcxx` → `gy_jcxx_his` 的搬迁只能按业务键或不带条件批量搬,出错后难以精确回滚。
### 3.2 类型体系混乱
表 3-2 字段类型分布(524 列)
| 类型 | 列数 | 占比 | 风险 |
| --- | --- | --- | --- |
| `varcharbyte`(金仓特有) | **276** | 53% | **全部未指定长度**(`character_maximum_length` 为 NULL),任何超长/脏数据都能写入 |
| `numeric` | 82 | 16% | 未指定精度标度,金额列与计数列同型,无精度约束 |
| `varchar` | 78 | 15% | 有长度,但风格与 `varcharbyte` 混用 |
| `timestamp` | 68 | 13% | 含 `Oracle DATE`(`sys.date`)映射,秒级精度 |
| `bpchar` | 11 | 2% | 定长字符(`'0'/'1'` 标记) |
| `text` / `int8` / `int4` / `bytea` | 9 | 2% | LOB(`nclob`/`blob`)、自增主键 |
**同一字段名在不同表里类型不一致,共 12 组**:
| 字段 | 类型变体 | 涉及表数 | 直接后果 |
| --- | --- | --- | --- |
| `id` | `numeric` / `varchar` / `varcharbyte` | 14 | 跨表 `join`、`union` 需隐式转换;索引即便补上也难生效 |
| `deleted` | `numeric` / `varcharbyte` | 4 | 逻辑删除写法不统一(`=0` 与 `='0'` 混用) |
| `gyid` | `varchar(64)` / `varcharbyte` | 17 | 长度与比较语义不一致,`gyid='TLXC'` 在不同表可能命中/不命中 |
| `kssj` / `jssj` | `timestamp` / `varcharbyte` | 3 / 3 | 同一含义的时间一半能比较、一半是字符串 |
| `sort` | `int4` / `numeric` | 4 | — |
| `lx` / `cb` / `name` / `dwmc` / `room_no` / `table_id` | 各 2 种 | — | — |
### 3.3 时间字段三种存法并存
| 存法 | 示例 | 问题 |
| --- | --- | --- |
| `timestamp`(含 `sys.date`) | `kcsj`、`jcsj`、`fjsj`、`sjrysj`、`scbzsj` | 正常,但秒级精度、无时区字段 |
| **字符串** `varcharbyte` | `gy_jcxx.yrysj`(预期入住时间)、`gy_jcxx.ddsj`、`gy_jcsj.kssj/jssj`、`gy_jbxx.zrsj/sjsj` | 无法做范围比较、无法排序、格式靠约定;共 13 处 |
| 冗余列并存 | `jcsj` 与 `jcsj_s`、`fjsj` 与 `fjsj_s`、`jcbjsj` 与 `jcbjsj_s` | 同一时刻两列,靠代码保证同步,无约束 |
### 3.4 Oracle 空串语义(高危)
实例参数 `ora_input_emptystr_isnull = on`,实测验证:
- `select count(*) from gy_jcxx where bz = ''` → **0**
- `select count(*) from gy_jcxx where bz is null` → **3541**(全表)
含义:业务代码写 `""` 入库后变成 `NULL`;反过来读出的 `NULL` 在 Java 侧可能是 `null` 也可能是 `""`,取决于取值方式。表现为:前端显示`undefined`、`StringUtils.isBlank` 与 `!= null` 判断在不同层得到不同结论、`where col <> ''` 恒不命中。三端拆分后跨库/跨端传输时这个语义差异会被放大。
### 3.5 `gy_jcxx` 上帝表
单表 **70 列**,同时承载 7 类语义:
| 语义组 | 代表列 |
| --- | --- |
| 车次与交路 | `jch`、`cc`、`cdw`、`kcsj`、`jlid`、`cclx`、`qd`、`ddcc`、`depotid` |
| 叫乘状态机 | `ryzt`、`jcsj`、`fjsj`、`jcbjsj`、`sjrysj`、`lybjsj`、`scbzsj` |
| 入住/离寓 | `yrysj`、`sjrysj`、`ylysj`、`sjlysj`、`flag_send`、`tbsj_gytofcd`、`tbsj_fcdtogy` |
| 补助与餐饮 | `cblx`、`cbje`、`sfffcb`、`dayallowance`、`is_meal3`、`isoverday` |
| 服务评分 | `kffwpf`、`stfwpf`、`jcfwpf`、`zm` |
| 报警与提醒去重 | `is_yaxx`、`is_jcbj`、`is_lybj`、`route_notice_key`、`call_timeout_notice_key`、`missed_call_notice_key` |
| 设备与录音 | `finish_sb`、`recordfile`、`hjxysj`、`fjhold` |
后 8 列(`business_id`、`ly_type`、`ly_event_id`、`ly_remark`、`ly_snapshot_url`、`route_notice_key`、`call_timeout_notice_key`、`missed_call_notice_key`)明显是后期追加,且 `ly_*` 属于"离寓事件"子域、`*_notice_key` 属于"通知去重"子域——表已演化为多子域的物理合并点。`gy_jcxx_his` 同步复制了这 70 列(但 `yappsj` 类型从 `sys.date` 漂移为 `timestamp`),归档表与主表已出现结构漂移。
### 3.6 设备台账双轨
表 3-4 两套设备表
| 项 | `gy_sb`(v1) | `gy_device`(v2) |
| --- | --- | --- |
| 行数 | 148 | 92 |
| 命名风格 | 全小写无下划线:`devicecode`/`lx`/`fjh` | 下划线 + RuoYi 审计列:`device_code`/`device_type`/`create_by` |
| 类型编码 | `lx`:3=134、1=4、5=3、6=3、2=2、空=2 | `device_type`:3=80、1=3、5=3、6=3、99=2、2=1 |
| 附加字段 | `gxsj`、`gymc` | `online_status`、`last_heartbeat`、`firmware_version`、`del_flag` |
| 关系 | — | `device_code` 与 `gy_sb.devicecode` **交集 92 行 = 完全子集** |
即:v2 的 92 个 `device_code` **全部能在 v1 中找到**(交集恰为 92),而 v1 仍有 148 行未被 v2 覆盖。两张表**同时在线、同时被读**,无外键、无同步机制。设备在线率、版本、故障统计存在两个数据源,"当前是否在线"取决于读哪张表。
### 3.7 指令队列与历史同表,且积压
表 3-5 `gy_sb_zl` 实测分布
| `deleted` | 行数 | 涉及设备数 | 时间范围 |
| --- | --- | --- | --- |
| `0`(未消费/未删除) | 72,088 | 96 | 2025-08-26 ~ 2026-11-07 |
| `1`(已消费) | 57,577 | 172 | 2025-08-26 ~ 2026-11-07 |
其中 **`deleted='0'` 且生成时间超过 1 天的记录有 71,723 条**——占未消费量的 99.5%。这说明:"取最新一条 → 置 `deleted=1`"的消费方式(见 `HeartBeatService`/设备服务)在多数情况下没有闭环:要么设备从未取走,要么取了没回写成功。同时该表 12.9 万行、零索引,每次取指令都是全表扫描 + 排序。
### 3.8 日志表无清理
`gy_logs` 28.6 万行 / 56 MB,`eventtime` 跨度 2025-05 ~ 2026-11。按 `lx` 分布:
| `lx` | 行数 |
| --- | --- |
| 运安叫乘同步汇总 | 238,386 |
| 刷脸离寓事件号缺失 / 请求 / 完成 / 抓拍缺失 | 7,655 / 7,655 / 7,649 / 7,649 |
| 运安叫乘同步跳过 | 4,472 |
| 运安定时抓取完成 | 3,470 |
| 自动叫乘任务未创建 | 2,369 |
| 叫乘时间延后 | 2,258 |
| 注册机心跳异常 | 1,797 |
| 超时未叫乘提醒 | 857 |
| 其余(匹配异常、登录、启动恢复等) | ~1,400 |
按月看,"运安叫乘同步汇总"稳定在 **每月约 4.4 万条**(2026-05 ~ 09 均在 40,980 ~ 44,640 之间)。这是"每 5 分钟 × 每公寓写一条汇总日志"的直接结果——**一条业务无异常的流水日志,占了全库 87% 的日志行数**。该表无归档、无分区、无索引,年增约 53 万行。
### 3.9 主数据与租户键(`gyid`)不一致
17 张表带 `gyid`,但各表的取值域互不匹配:
| 观测点 | 实测值 |
| --- | --- |
| `gy_jbxx`(公寓基础信息) | **9** 个:SYXC、SYNDC、SJTXC、SYXXC、DDXC、DSQXC、BYQXC、DLXC、DLBDC |
| `gy_call_duty_match_lock`(行锁表) | **37** 个业务 gyid + 1 个伪 gyid `__SYNC_JCXX__` |
| `gy_rooms`(房间) | **6** 个:TLXC 102、THXC 56、PJXC 35、TLNXC 2、CCXDC 1、**空 1** |
| `gy_jcxx`(叫乘) | **2** 个:THXC 2,110、TLXC 1,431 |
| `gy_sb.lx` 为空 / `gy_rooms.gyid` 为空 | 各存在 1~2 行 |
三个结论:
1. **主数据严重缺失**:系统实际服务的 gyid 至少 37 个,但只有 9 个在 `gy_jbxx` 有基础信息,31 个公寓缺面积/床位/负责人/时间规则来源。
2. **租户键可空**:`gy_rooms` 有 1 行 `gyid` 为 `NULL`(很可能是代码写入 `""` 后被 Oracle 空串语义转成 `NULL`),这类行在按租户过滤时会被全局漏掉或全局命中。
3. **行锁表用 `gyid` 当锁键**并塞入 `__SYNC_JCXX__` 这种伪租户,说明"公寓"与"同步任务"两个概念共用了一张锁表——三端拆分后必须把锁的粒度从"字符串键"改为"业务对象 + 版本"。
> 样本库只覆盖 THXC / TLXC 两个公寓的叫乘数据,属于导出时的数据裁剪(或该公寓数据被迁移过),不代表生产分布;但 `gyid` 的**取值域不一致**是结构性问题,与数据量无关。
### 3.10 RuoYi 残留与孤儿序列
- `gen_table`(18 行)、`gen_table_column`(279 行)是 RuoYi 代码生成器的元数据表,业务上完全不用,却占据 40 张表中的 2 张。
- 64 个序列中只有 4 个被列默认值引用:`gen_table_table_id_seq`、`gen_table_column_column_id_seq`、`gy_device_device_id_seq`、`seq_gy_allowance`。其余 60 个指向**已不存在的表**:`sys_user_*`、`sys_role_*`、`sys_menu_*`、`sys_dept_*`、`sys_job_*`、`t_oa_*`、`t_role_*`、`ykt_account_id_seq`、`ykt_consume_flow_id_seq`、`gygl_consumer_terminal_device_id_seq` 等。
这组序列是"库曾经跑过一版 RuoYi/其他业务系统、后来表被删但序列没删"的直接证据。对三端拆分的意义:**新公寓端选 RuoYi 并不是引入新范式,而是回到这条被中断过的路线上**,因此必须明确"沿用还是重建"权限表(现状 `gl_*` 是自研 RBAC,与 RuoYi `sys_*` 完全不同构)。
### 3.11 照片以 BLOB 入库
`gy_ykt_ry_zp`:100 行 / 27 MB / 平均 281.3 KB / 最大 547,778 字节,列类型 `blob`(`bytea`)。同时 `gy_ykt_ry.photourl` 又存了一份路径,代码里还有按 `serverOS` 硬编码的文件目录(`C:\project_doc\resouce\photo\` 与 `/home/gygl/resouce/photo/`)。**同一张照片有"库内 BLOB + 本地磁盘路径"两条真相**,且备份策略完全不同(逻辑备份含 BLOB,磁盘目录不在备份范围)。
### 3.12 审计与统计表空置
`gy_operation_log`(操作审计)0 行、`gy_bjcstj`(补叫统计)0 行、`gy_rtjb`(日报)0 行、`gy_sb_zl_his`(指令历史)0 行。四张表有结构无数据——**设计了但没有接线**。其中操作审计的缺失,与"值班员可改叫乘状态但无留痕"的风险直接相关。
## 4. 数据质量实证
表 4-1 抽样核对结果
| 检查项 | 结果 | 判读 |
| --- | --- | --- |
| `gy_jcxx` 主键候选 `jcid` | 3,541 行 / 3,540 个不同值 | 存在 1 组重复,无约束可阻断 |
| `gy_jcxx.kcsj` 为空 | 367 / 3,541(10.4%) | 开车时间缺失仍可入库,下游按时间排序/提醒会漏 |
| `gy_jcxx.ryzt` 分布 | 3=2,373、0=582、5=225、4=171、2=134、1=56 | `-1` 未出现;状态分布偏向"已完成/在寓",缺失败态样本 |
| `gy_jcxx.jllx` 分布 | 1=2,224、0=1,317 | 两值区分,但字段无注释、无字典表 |
| `gy_ykt_remainder` | 1,253 行 / 1,253 个 `ryid`,无重复 | 当前无重复,但无唯一约束保障 |
| `gy_rooms` 房态分布 | 3=68、1=58、4=32、2=32、5=7 | `room_status=6` 从未出现(字典有 6 值,实际只用 5 值) |
| `gy_sb_xt` 心跳 | 246 行,`updatestate` 全为 0 | 更新状态位从未被置位,与 3.7 的指令积压互相印证 |
| `gy_sb_zl.id` / `gy_logs.id` | 12.9 万 / 28.6 万,全部唯一 | 应用层 UUID 生成正确,纯靠代码保证 |
账务三表口径无法自洽(实测合计):
| 表 | 口径 | 合计 |
| --- | --- | --- |
| `gy_ykt_allowance` | 补助发放流水 | 552,295.00 元 |
| `gy_ykt_consume` | 消费流水 | 265.10 元 |
| `gy_ykt_remainder` | 当前余额(补助/现金/消费/补助总额) | 补助余额 1,228.00 元,现金余额 0.00 元 |
补助累计 55.2 万元的规模,与仅 265 元的消费流水、1,228 元的当前余额之间,**缺少任何可对账的中间口径**(无期初、无结转、无冲正、无对账批次)。这不能简单判定为"数据错误",但可以确定:**当前结构下无法证明账是对的**。
## 5. 表归属矩阵(对接文档 10 第 5 章)
表 5-1 40 张表的目标归属与改造动作
| 表 | 行数 | 目标端 | 改造动作 |
| --- | --- | --- | --- |
| `gy_jbxx` | 9 | 平台 | 升为公寓主数据,补全 37 个 gyid,版本化下发 |
| `gy_gyzd` | 37 | 平台 | 主数据(网关/机器号/IP),下发只读 |
| `gy_gzzd` | 17 | 平台 | 主数据(工种/客货类型) |
| `gy_sjgz` | 17 | 平台 | 时间规则收敛为平台唯一口径,下发 |
| `gy_jcsj` | 7 | 平台 | 计划数据 |
| `gy_jrb` | 2 | 平台 | 主数据 |
| `gy_rooms` | 197 | 平台 + 公寓 + 值班 | **拆三份**:房间基线(平台)/ 房态(公寓)/ 分机与报警起点(公寓) |
| `gy_jcxx` | 3,541 | 公寓 | **拆表**:叫乘任务 / 入住离寓 / 通知去重键,见 6.4 |
| `gy_jcxx_his` | 32,505 | 公寓 + 归档 | 按月分区;修正与主表的类型漂移 |
| `gy_bjxx` | 2,490 | 公寓 | 保留,补主键与 `gyid+时间` 索引 |
| `gy_bjcstj` | 0 | — | 退役(无数据无代码写入) |
| `gy_jdqkhz` | 1 | 平台 | 转报表视图或退役 |
| `gy_rtjb` | 0 | — | 退役 |
| `gy_call_duty_match_lock` | 38 | 公寓 | 换为行级锁表(含 `locked_at`/`owner`)或 DB advisory lock |
| `gy_syn_jcxx` | 1,139 | 平台 | 运安抓取上收平台,公寓只收下行计划 |
| `gy_syn_jcxx_his` | 2,291 | 平台 | 归档分区 |
| `gy_syn_jcxx_status` | 1 | 平台 | 改为 watermark 表(含 `last_success_at`/`cursor`) |
| `gy_ykt_ry` | 1,349 | 平台 | 人员/卡主数据,公寓本地缓存 |
| `gy_ykt_ry_zp` | 100 | 平台 | 迁对象存储,表只留 URL 与校验和 |
| `gy_ykt_remainder` | 1,253 | 平台主账 + 公寓缓存 | 双记:本地流水 + 平台主账,日对账 |
| `gy_ykt_allowance` | 34,182 | 平台 | 补助流水归平台,公寓不上报原始流水 |
| `gy_ykt_consume` | 35 | 平台(餐饮)/ 公寓(其他) | 按消费来源拆分归属 |
| `gy_ykt_btbj` | 5 | 平台 | 补助标准主数据 |
| `gy_ykt_btjs` | 5 | 平台 | 补助规则主数据 |
| `gy_sb` | 148 | 公寓 | **合并到 `gy_device`**,命名归一 |
| `gy_device` | 92 | 公寓 | 保留为唯一设备台账,补 `(gyid, device_code)` 唯一 |
| `gy_sb_xt` | 246 | 公寓 | 保留;心跳改 upsert,加 `last_heartbeat` 索引 |
| `gy_sb_zl` | 129,665 | 公寓 | **拆队列 / 历史**,见 6.4 |
| `gy_sb_zl_his` | 0 | 公寓 | 改分区表承接指令历史 |
| `gy_logs` | 285,914 | 公寓 | 按月分区 + 保留期;"同步汇总"降为采样 |
| `gy_operation_log` | 0 | 公寓 + 值班 | 启用,按端打标 |
| `gy_file` | 1 | 平台 | 对象存储索引 |
| `gl_*`(6 张) | 17/4/48/45/16/58 | 平台 | 统一账号与权限;公寓端沿用 RuoYi `sys_*` 或适配 `gl_*`,二者只能留一套 |
| `gen_table` / `gen_table_column` | 18 / 279 | — | 退役,目标库不携带 |
## 6. 目标结构建议(设计输入,未执行)
### 6.1 命名与边界
1. 保留 schema `gygl`,用**表名前缀**表达归属:`PLT_`(平台主数据/计划)、`APT_`(公寓执行事实)、`DUTY_`(值班操作)、`ARC_`(归档)。
2. 现有 `gy_*` 表在迁移期保留原名,新增列/新表用新前缀,避免一次性重命名带来的全量改代码。
3. 权限表二选一:若公寓端采用 RuoYi,则 `gl_*` 只作为平台端兼容视图存在,不再有第二份可写副本。
### 6.2 主键与索引基线
表 6-1 必补约束与索引(按优先级)
| 表 | 建议主键 / 唯一 | 建议索引 |
| --- | --- | --- |
| `gy_jcxx` / `gy_jcxx_his` | `jcid` | `(gyid, kcsj)`、`(gyid, ryzt)` |
| `gy_rooms` | `(gyid, room_no)` | `(gyid, room_status)` |
| `gy_ykt_ry` | `ryid` | `(card_id)`、`(gzzh)` |
| `gy_ykt_remainder` | `ryid` | `(cardid)` |
| `gy_ykt_allowance` | `tid` | `(gyid, czsj)`、`(ryid, czsj)` |
| `gy_ykt_consume` | `tid` | `(gyid, xfsj)`、`(orderid)` |
| `gy_device` | `(gyid, device_code)` | `(ip_addr)`、`(last_heartbeat)` |
| `gy_sb_xt` | `devicecode` | `(gyid, heartbeattime)` |
| `gy_sb_zl` | `id` | `(devicecode, deleted, generatedtime)` —— 取指令的主路径 |
| `gy_logs` | `id` | `(eventtime)` + 按月分区 |
| `gy_bjxx` | `id` | `(gyid, kssj)` |
| `gy_syn_jcxx` | `id` | `(kcsj)`、`(jch, cc)` |
| `gy_jbxx` / `gy_gyzd` / `gy_sjgz` | `gyid` | — |
| `gl_yh` / `gl_js` / `gl_qx` / `gl_cd` | 各自首列 | 关联表 `(user_id)/(role_id)` |
| `gy_call_duty_match_lock` | `gyid` | — |
### 6.3 类型归一规则
表 6-2 类型目标
| 现类型 | 目标 | 规则 |
| --- | --- | --- |
| `varcharbyte`(无长度) | `varchar(n)` | 按实测最大长度 + 30% 余量定 n;标识类统一 `varchar(32/64)` |
| 业务时间(`kssj`/`jssj`/`ddsj`/`yrysj`…) | `timestamp(3)` | 禁止再新增字符串时间;迁移时按 `YYYY-MM-DD HH24:MI:SS` 解析,解析失败行单独落错误表 |
| `sys.date` | `timestamp(0)` | 保持秒精度,避免与 `timestamp(6)` 混比 |
| 金额(`bzje`/`xfje`/`knye`/`xjye`/`xfze`/`bzze`) | `numeric(12,2)` | 统一两位小数,禁止 `numeric` 裸用 |
| 布尔标记(`deleted`/`sfjc`/`finish_*`/`is_*`/`stoped`) | `char(1)` + `CHECK IN ('0','1')` | 或 `boolean`;**同一字段名全库只允许一种表示** |
| `id`(`numeric` vs `varchar`) | `varchar(32)` 或 `bigint` | 每张表内自洽即可,但跨表引用必须同型 |
| `blob` 照片 | 移出库 | 表内只留 `photo_url` + `sha256` + `size` |
### 6.4 拆表与归档
1. **`gy_jcxx` 三分**:
- `APT_JC_TASK`:任务与状态机(`jcid/ryid/jch/cc/kcsj/jcsj/fjsj/jcbjsj/ryzt`)
- `APT_JC_STAY`:入住离寓(`yrysj/sjrysj/ylysj/sjlysj/ly_*`/`flag_send`)
- `APT_JC_NOTICE`:通知去重键(`route_notice_key/call_timeout_notice_key/missed_call_notice_key`)→ 或并入通用 `NOTICE_KEY` 表
- 物理上不再是 70 列宽表,写入口从"多处 update 同一行"变为"三个子域各自写自己的表",`JcglService` 的 4193 行才有可能拆开。
2. **`gy_sb_zl` 拆队列/历史**:`APT_DEV_CMD`(只保留未消费,带 `status/attempt/expire_at`)+ `ARC_DEV_CMD`(消费即搬走),队列表行数应稳定在"在线设备数 × 个位数"量级,当前 7.1 万条积压即为异常基线。
3. **`gy_logs` 分区**:按月 `RANGE` 分区,保留 3 个月热数据;"运安叫乘同步汇总"改为只记异常与抽样,预计日志量下降 90%。
4. **`gy_jcxx_his` / `gy_syn_jcxx_his` / `gy_sb_zl_his`** 统一进 `ARC_` 分区族,保留期与合规要求对齐。
### 6.5 迁移时的兼容陷阱
1. `varcharbyte` 无长度 → 转 `varchar(n)` 前必须先跑 `select max(length(col))` 全量核对,否则截断报错会中断迁移。
2. `ora_input_emptystr_isnull=on` → 迁移前后同一份数据 `''` 与 `NULL` 的判断结果不同;迁移脚本与校验 SQL 都要用 `coalesce(col,'')` 口径。
3. `gen_table_column` 里记录的 `is_pk` 全为 RuoYi 生成器推断值,**不能当作主键依据**——实测全库主键为 0,该字段不代表真实约束。
4. 60 个孤儿序列不要随库迁移,目标库从 0 重建,否则序列起始值会带来 ID 跳号与冲突排查成本。
5. Oracle 兼容模式与金仓安全版 MAC 策略需在目标环境确认(`database_mode`、`sysmac` 是否启用),二者会改变 SQL 行为与权限模型。
## 7. 与三端拆分方案的衔接
表 7-1 结构缺陷 → 方案措施
| 本次实测缺陷 | 对应章节 | 方案措施 |
| --- | --- | --- |
| 零主键零索引零外键(3.1) | 阶段零 基线修复 | 先补 `(gyid, 业务键)` 唯一约束与查询索引,再谈拆分 |
| 同名不同型 12 组(3.2) | 5 数据架构 / 6 接口契约 | 契约层强制类型规范;`gyid` 统一 `varchar(64)` |
| 空串即 NULL(3.4) | 6 接口契约 | 上报信封中 `null` 与 `""` 语义显式化,服务端统一 `coalesce` |
| `gy_jcxx` 70 列(3.5) | 4 领域与模块划分 | 拆三子域,状态机唯一写入口 |
| 双设备台账(3.6) | 7 设备接入 | 设备主数据唯一化,`gy_device` 为准,`gy_sb` 合并 |
| 指令积压 7.1 万(3.7) | 7.4 指令下发-ACK-重投 | 队列化 + ACK + 过期作废 + 优先级 |
| 日志月增 4.4 万(3.8) | 8 部署与运行 | 分区 + 保留期 + 异常化 |
| `gyid` 取值域不一致(3.9) | 3 端职责边界 | 公寓主数据上收,租户键非空 + 外键校验 |
| RuoYi 残留与孤儿序列(3.10) | 阶段一 新公寓端脚手架 | 明确权限表归属,目标库不携带 `gen_*` 与孤儿序列 |
| 照片双真相(3.11) | 5.3 数据归属 | 统一对象存储 + 校验和 |
| 审计表空置(3.12) | 10 风险 | 值班操作留痕纳入验收项 |
| 账务不可对账(4 节) | 5.2 一致性策略 | 主账在平台 + 本地流水 + 日对账 |
## 8. 待现场确认
1. **生产库是否真的没有索引**:本机导入库与 dump 均为 0,但需要在生产 DBA 侧执行 `\d+ gygl.gy_jcxx` 复核(导出工具理论上会带索引,需排除导出裁剪)。
2. **37 个 gyid 与 9 个 `gy_jbxx` 的差集**:是"未登记"还是"已停用"?决定主数据补录范围。
3. **样本库只有 2 个公寓的叫乘数据**:确认是导出裁剪、数据迁移,还是这两个公寓曾单独部署。
4. **`gy_sb_zl` 积压 7.1 万条的成因**:设备端未取、网关未转发,还是消费后回写失败?需查 2026-10 之后的设备侧日志。
5. **Oracle 存量公寓**:`ojdbc8` 依赖存在,确认是否存在仍直连 Oracle 的部署环境。
6. **金仓安全版 MAC 是否启用**:`sysmac` schema 为空表,若启用策略会影响后续新增表的授权模型。
## 附录 A 采集 SQL(可复现)
```sql
-- 库级
select version(), current_setting('server_encoding');
select name, setting from pg_settings where name in ('database_mode','ora_input_emptystr_isnull');
select nspname, count(*) from pg_class c join pg_namespace n on n.oid=c.relnamespace
where c.relkind='r' group by 1;
-- 约束与索引
select count(*) from pg_indexes where schemaname='gygl';
select constraint_type, count(*) from information_schema.table_constraints
where table_schema='gygl' group by 1;
select count(*) from pg_class c join pg_namespace n on n.oid=c.relnamespace
where c.relkind='S' and n.nspname='gygl';
-- 类型与注释
select udt_name, count(*), sum(case when character_maximum_length is null then 1 else 0 end)
from information_schema.columns where table_schema='gygl' group by 1 order by 2 desc;
select column_name, count(distinct udt_name) from information_schema.columns
where table_schema='gygl' group by 1 having count(distinct udt_name)>1;
-- 规模与分布
select relname, n_live_tup, pg_size_pretty(pg_total_relation_size(relid))
from pg_stat_user_tables order by pg_total_relation_size(relid) desc;
select count(*), count(distinct jcid), count(distinct gyid), min(kcsj), max(kcsj) from gygl.gy_jcxx;
select deleted, count(*), min(generatedtime), max(generatedtime) from gygl.gy_sb_zl group by 1;
select count(*) from gygl.gy_sb_zl where deleted='0' and generatedtime < now() - interval '1 day';
select to_char(eventtime,'YYYY-MM'), count(*) from gygl.gy_logs group by 1 order by 1;
```
> 本轮对实例的全部操作均为 `SELECT`,未执行任何 DDL/DML;分析用临时脚本仅存在于本机临时目录。
+108
View File
@@ -0,0 +1,108 @@
# 叫乘管理系统 梳理文档
本目录是对既有代码的结构化梳理,目的是为后续迭代提供统一的“地图”:知道每个业务落在哪个包/页面、数据怎么流转、接口和外部系统怎么对接、改动时应该看哪些文件。
> 后端:`yktgl/`;前端:`YktVue/`,见 [frontend/](./frontend/README.md)。
> 梳理基准:`main` 分支提交 `135a192`(2026-11-07)。
## 文档索引
| 文档 | 内容 | 适用场景 |
| --- | --- | --- |
| [01-系统总览.md](./01-系统总览.md) | 业务定位、角色、运行形态、技术栈、关键设计约束 | 新人入门、方案评审 |
| [02-工程结构与分层.md](./02-工程结构与分层.md) | 目录/包结构、分层约定、启动流程、公共组件 | 找代码、定落点 |
| [03-业务模块地图.md](./03-业务模块地图.md) | 各业务模块职责、控制器、服务、依赖关系 | 评估改动影响面 |
| [04-核心业务流程.md](./04-核心业务流程.md) | 入住、叫乘、复叫、报警、应答、离寓、房间、交路同步、卡务 | 改业务逻辑前必读 |
| [05-数据模型.md](./05-数据模型.md) | 核心表、字段语义、状态码、归档与锁 | 写 SQL / 改表 / 排查数据 |
| [06-接口清单.md](./06-接口清单.md) | Web 接口与设备接口清单、协议枚举 | 联调、接口扩展 |
| [07-外部集成与定时任务.md](./07-外部集成与定时任务.md) | Asterisk AMI、运安 SOAP、中心 HTTP、WebSocket、Quartz/@Scheduled | 排障、集成改造 |
| [08-部署与配置.md](./08-部署与配置.md) | Profile、数据库/Redis、资源目录、升级脚本 | 部署、环境切换 |
| [09-迭代指引与风险清单.md](./09-迭代指引与风险清单.md) | 技术债、改动落点速查、待确认问题、迭代建议 | 排期、评审 |
| [10-架构优化方案-三端拆分.md](./10-架构优化方案-三端拆分.md) | 平台端/公寓端/应用端拆分:现状评估、目标架构、职责边界、模块与数据归属、接口契约、设备接入、部署与迁移路径;含公寓端细化(RuoYi-Vue + MySQL 技术基线、应用端复用实现、语音域 Asterisk/AMI/分机/录音与配置收敛对账)及 KingbaseES→MySQL 选型与迁移 | 架构评审、重构设计 |
| [11-既有数据库结构分析.md](./11-既有数据库结构分析.md) | 实测库级参数、40 表清单与规模、零主键零索引等结构缺陷、数据质量实证、表归属矩阵、类型归一与拆表建议 | 库设计评审、迁移改造 |
| [12-三端功能规划与应用端值班台设计.md](./12-三端功能规划与应用端值班台设计.md) | v4 三端功能范围 / 模块划分 / 职责边界 / 协作关系;应用端「1 台 + 2 层」信息架构与四区值班台;交互与状态反馈、异常与容错、8 类 28 项现场及中老年适配检查项 | 界面重构评审、前端交付与验收走查 |
| [13-值班员端功能补全与常驻盯控导航设计.md](./13-值班员端功能补全与常驻盯控导航设计.md) | v5 增量:六个功能模块(换乘 / 叫乘历史查询 / 交路管理 / 房间管理 / 交班日志 / 系统告警)、统一入口与导航、常驻盯控骨架与跨模块实时告警、范围变更登记与 41 项验收 | 界面补全评审、前端实现与走查 |
| [14-叫乘功能入口整合与房间维度展示调整.md](./14-叫乘功能入口整合与房间维度展示调整.md) | v6 形态增量:六入口并入顶部按钮操作区(排在「记录与自检」之后)、六模块全部弹出窗口化且主控区不变、紧急处置区显示全部报警 / 正在执行房间、叫乘信息以房间为单位(2~4 人/房间、1~2 房间/交路)、54 项验收 | 界面整合评审、前端实现与走查 |
| [15-原系统功能对照与逐页落点.md](./15-原系统功能对照与逐页落点.md) | v7 功能增量:以 `JcXx.vue` 全文 5295 行为事实源建立追溯矩阵;补齐主台四维查询、详情原系统字段与时刻口径、六模块原系统筛选与功能对照条、吸底值班条口径;10 项原系统缺陷明确不继承;43 项验收 | 功能对齐评审、前端实现与走查 |
## 前端文档
前端梳理独立成册,入口见 [frontend/README.md](./frontend/README.md):
| 文档 | 内容 |
| --- | --- |
| [frontend/01-前端总览与技术栈.md](./frontend/01-前端总览与技术栈.md) | 四类界面、运行拓扑、技术栈、全局变量 |
| [frontend/02-工程结构与请求链路.md](./frontend/02-工程结构与请求链路.md) | 启动装配、登录守卫、axios、API 与后端映射 |
| [frontend/03-路由与页面职责.md](./frontend/03-路由与页面职责.md) | 13 条路由、入口关系、遗留页面 |
| [frontend/04-核心页面拆解-JcXx.md](./frontend/04-核心页面拆解-JcXx.md) | 值班台布局、状态、操作、时间规则、轮询 |
| [frontend/05-实时通信与语音告警.md](./frontend/05-实时通信与语音告警.md) | WebSocket/SIP、消息协议、告警与 TTS 队列 |
| [frontend/06-食堂与自助终端模块.md](./frontend/06-食堂与自助终端模块.md) | 自助登记、点餐/结算/后厨/保洁、选房与房间维护 |
| [frontend/07-构建部署与风险清单.md](./frontend/07-构建部署与风险清单.md) | 构建、环境、部署要点、测试基线、P0/P1/P2 风险 |
## 界面效果图与原型(docs/UI)
| 文件 | 说明 |
| --- | --- |
| [UI/](./UI/) | 效果图:v4 主台 12 张(A01~A13)+ v5 补全 16 张(B01~B06)+ v6 形态 18 张(`V6-` 前缀)+ **v7 当前版 20 张(`V7-` 前缀)** |
| [UI/_src/duty-console-v7.html](./UI/_src/duty-console-v7.html) | **当前版**原型:v6 形态冻结 + 按 `JcXx.vue` 补齐四维查询 / 详情字段 / 六模块功能对照 / A14 追溯矩阵;`?mod=B01~B06&ref=1&v=A14` 等参数 |
| [UI/_src/shoot-v7.cjs](./UI/_src/shoot-v7.cjs) | v7 出图 + 43 项断言(功能对照 + v4/v5/v6 全量形态回归) |
| [UI/_src/duty-console-v6.html](./UI/_src/duty-console-v6.html) | 上一版原型(v6 形态基线,保留对照 / 可复跑);`?mod=B01~B06` 等参数 |
| [UI/_src/shoot-v6.cjs](./UI/_src/shoot-v6.cjs) | v6 出图 + 54 项断言(入口排序 / 弹窗范围 / 紧急区条件 / 房间维度 + v4/v5 回归) |
| [UI/_src/duty-console-v5.html](./UI/_src/duty-console-v5.html) | 上一版原型(并列工作区,保留对照 / 可复跑);`?mod=B01~B06` 等参数 |
| [UI/_src/shoot-v5.cjs](./UI/_src/shoot-v5.cjs) | v5 出图 + 41 项断言(21 项 v4 基线回归 + 20 项常驻盯控约束) |
| [UI/_src/duty-console-v4.html](./UI/_src/duty-console-v4.html) | 上一版单文件原型(保留可复跑);URL `?v=&mode=&layout=&dlg=` 切页 / 切版本 / 切布局 / 开弹层 |
| [UI/_src/shoot-v4.cjs](./UI/_src/shoot-v4.cjs) | 批量出图 + 21 项渲染 / 交互 / 适配断言(字号、对比度、点击区、焦点、回退、降级、时钟) |
| [12-三端功能规划与应用端值班台设计.md](./12-三端功能规划与应用端值班台设计.md) | 配套规划与验收依据 |
| [14-叫乘功能入口整合与房间维度展示调整.md](./14-叫乘功能入口整合与房间维度展示调整.md) | 当前版界面形态与验收依据(v7 冻结不变) |
| [15-原系统功能对照与逐页落点.md](./15-原系统功能对照与逐页落点.md) | 当前版功能口径与逐页落点依据 |
> 旧稿归档不删除:`UI/_v1-archive/`(v1 12 张)、`UI/_v2-archive/`(v2/v3/D12 共 20 张)。
## 三端功能设计修订(CR-02,2026-11-09)
> 依据 [公寓端/00-公寓端主要功能设计.md](./公寓端/00-公寓端主要功能设计.md) 对公寓端功能的重新规划,结合 `backend/`(既有后端梳理)与 `frontend/`(既有前端梳理),对平台端 / 公寓端 / 应用端(值班员界面)三端功能设计做了整体修订。**权威文档**:[设计变更/03-三端功能设计修订(CR-02).md](./设计变更/03-三端功能设计修订(CR-02).md)。
| 修订方面 | 权威落点 | 三端分册同步 |
| --- | --- | --- |
| 覆盖范围(含 14 项冲突裁决) | CR-02 §2(表 3-2~3-5) | `平台端/01`、`公寓端/01`、`应用端/01` §0 |
| 交互协议 | CR-02 §3(四层协议栈、20 编号不新增、N-01~N-18 接口) | `公寓端/04`、`公寓端/09`、`平台端/05`、`应用端/04` §0 |
| 交互数据 | CR-02 §4(上行信封、`bizType`、payload、枚举、错误码) | 同上 |
| 模块与数据表设计 | CR-02 §5、§6、§10(新增 17 张表、3 处补列、公寓本地库统一 **59 张**) | `平台端/03`、`公寓端/03`、`应用端/03` §0 |
| Asterisk 配置数据管理补遗 | [公寓端/00](./公寓端/00-公寓端主要功能设计.md) §3:`AC-01`~`AC-05` 四类数据表的字段 / CRUD / 权限,SIP 分机离线自动告警的触发条件、级别、通知与留存;权威增量见 CR-02 §10 | `公寓端/00`、`公寓端/03`、`04`、`05`、`09` |
| 两类叫乘实现方式的业务流程 | [公寓端/05](./公寓端/05-关键流程.md) §9:SIP 语音叫乘(Asterisk 主动呼叫 → 终端接听入会 → 播放提示语音 → AMI 事件上报状态)与平板叫乘(拉取任务 → 本地定时播放 → 刷脸签到 → 结果上报公寓服务)的触发条件、交互时序、状态上报机制与四条结果反馈路径;权威登记见 CR-02 §11 | `公寓端/05`、`04`、`03`、`09`、`01`、`00` |
| 关键流程变更 | CR-02 §7(F-A~F-Q) | `公寓端/05`、`平台端/06`、`应用端/05` §0 |
| **人脸数据下发管理** | [公寓端/10-人脸数据下发管理.md](./公寓端/10-人脸数据下发管理.md):`AF-01`~`AF-06` 六项功能的触发条件、数据范围、同步与清理机制、权限隔离与异常处理;**注册链路按 CR-C25**:签到终端发起注册申请并上传人脸 → 值班员在值班台完成人员注册 → 注册成功后人脸进入公寓服务系统 → **分别**上传平台与下发公寓内设备(双路解耦、互不阻塞);人员主数据照片与特征值按 **CR-C24** 允许跨端,业务抓拍 / 录音仍不出公寓。权威登记见 CR-02 §12(CR-C24~CR-C25、F-J、R-85~R-93、Q-30~Q-35、验收 15~17) | `公寓端/10`、`00`、`01`、`03`、`04`、`05`、`09`、`应用端/04` |
| **人员入住作业流程** | [公寓端/11-人员入住作业流程.md](./公寓端/11-人员入住作业流程.md):`CI-01`~`CI-05` 五环节(注册与签到、入住申请、值班员审核与房间调整、#9 提报与平台运安匹配、#7 任务查询进入叫乘),含三条状态链、`apt_local_task` 补列结构、N-38~N-47 请求 / 响应字段、Q-36~Q-40 与验收 8 项;**"已入住"为流程态 + 业务事实双重表达,不新增 `gy_jcxx` 状态字段、不新增业务表**。权威登记见 CR-02 §13(CR-C26~CR-C30、F-K、R-95~R-103、验收 18~20) | `公寓端/11`、`00`~`09`、`平台端/05`、`06`、`应用端/01`、`04`、`05` |
| **消费、充值与菜品管理** | [公寓端/12-消费充值与菜品管理.md](./公寓端/12-消费充值与菜品管理.md):`CF-01`~`CF-03` 三流程(人员消费:选餐加菜显示金额 → 查补贴 / 现金额度 → 补贴优先、现金补足 → 上报并同步平台落入人员账户;人员线下充值:**仅线下现金、扫码只识别公寓、系统只记账**,管理员核对后计入现金额度并同步平台;公寓菜品管理:阶段性食谱发布 → 终端查看菜品并加入选择菜单 → 远程点菜仅预留入口),含四条状态链、双额度 CAS 扣减与"一笔消费只一条流水"、`gy_ykt_*` / `apt_ledger_offline` / `YKT_MEALS` 补列、N-48~N-58 请求 / 响应字段、Q-41~Q-45 与验收 8 项;**不新增业务表、不新增状态字段、不新增跨端编号与 `bizType`**。权威登记见 CR-02 §14(CR-C31~CR-C35、F-L / F-M / F-N、R-104~R-113、验收 21~23) | `公寓端/12`、`00`~`09`、`平台端/01`、`03`、`04`、`05`、`06`、`应用端/01`、`03`、`04`、`05`、`07` |
| **三端一致性同步与平台统计分析** | CR-02 §15 为权威登记(**表 3-63 变更点与影响范围**、CR-C36~CR-C42、表 3-65 四能力三端对照、**表 3-66「三端状态映射与数据同步机制」唯一权威表**、F-O / F-P / F-Q、R-114~R-125、Q-46~Q-50、验收 24~26):公寓端已完成的**平板叫乘 / 人脸签到 / 消费 / 菜品管理**在应用端与平台端同步落地业务规则、交互流程、权限控制与异常处理;平台新增 [平台端/10-公寓统计分析与排名报表.md](./平台端/10-公寓统计分析与排名报表.md)(五类指标、月 / 季 / 年报表、公寓分类 + 综合排名、`S-01`~`S-07`)。平台库新增 2 张另计(累计 5 张),公寓 **59 张**、跨端 20 编号、`v2-draft`、平台零回调不变 | `平台端/10`、`平台端/01`~`09`、`应用端/01`~`09`、`公寓端/00`~`12`、CR-02 §15 |
> `10-架构优化方案-三端拆分.md` 修订记录已追加 v1.3 / v1.4 / v1.5 / v1.6 / v1.7 / v1.8 / v1.9 / **v2.0**(三端一致性同步 + 平台统计分析,CR-02 §15);跨端接口仍为 20 个编号,不新增、不重排;平台不回调公寓;人脸模块、入住作业流程、消费 / 充值 / 菜品管理与三端一致性同步均**不新增公寓业务表**(公寓本地库仍 59 张),平台库新增 2 张 `pl_apt_stat_*` 另计(累计 5 张);新增表与补列均为设计定义,DDL 待阶段二契约冻结评审通过后执行;仅线下充值不接线上支付与回调,远程点菜仅预留入口;统计结果不回写任何业务状态。**变更点与影响范围见 CR-02 §15 表 3-63。**
### 公寓端小步迭代开发计划(2026-11-10)
| 项 | 内容 |
| --- | --- |
| 文档 | [公寓端/13-公寓端小步迭代开发计划.md](./公寓端/13-公寓端小步迭代开发计划.md)(v1.0,**实施排期设计稿,非设计变更**) |
| 依据 | `dev/RuoYi-Vue-master/`(后端)、`dev/RuoYi-Vue2-master/`(前端)实测代码基础 + `docs/10` §9、`公寓端/07` 表 7-1~7-8 |
| 范围 | `IT-0`~`IT-8` 九个小步迭代:**入住 + 叫乘核心闭环优先(P1)**;消费 / 充值 / 菜品 `IT-9`~`IT-11` **整体延后(P2)**;里程碑 `MS-0`~`MS-5`、交付物 `DV-0`~`DV-8`、设计文档 × 代码模块对齐矩阵、风险台账、跨迭代联调与测试、P2 衔接规划 |
| 权威关系 | 阶段划分 / 批次顺序 / 验收口径仍以 `docs/10` §9、`公寓端/07` 为准;业务规则 / 状态 / 接口仍以 CR-02 §11~§15 与各专册为准;`13` 只做排序、切分、对齐、衔接 |
| **变更点与影响范围** | 新增 1 篇排期文档;同步 `公寓端/00`、`公寓端/README.md`、`公寓端/07`、`公寓端/08`、`docs/README.md` 与 CR-02 表 0-2 **v1.7** / 表 0-3 索引。**不改动** CR-02 §12~§15 权威表述、不新增 `M7` / 阶段六 / `D-4` / `Q-*` / `R-*` 等既有编号;跨端 20 编号、`v2-draft`、公寓本地库 59 张表、平台零回调、统计结果不回写均不变 |
> 6 项 P0 排期前提(技术基线冲突、表数口径、前端入口、CI/测试、明文凭据、契约冻结)与 3 项口径偏离须经评审签字后方可执行,见 `公寓端/13` 表 13-4 / 表 13-6。
## 建议阅读路径
- **第一次接触项目**:01 → 02 → 03 → 04;再看前端 01 → 02 → 04。
- **要接一个需求**:03 定位模块 → 04 看流程 → 09 查落点与风险 → 06/05 查接口和表;涉及页面时读前端 04/05。
- **线上排障**:07(外部依赖/定时任务)→ 04(状态机)→ 05(数据与状态码)→ 08(配置);前端问题读前端 05/07。
- **准备重构**:02(分层)→ 09(技术债与建议)→ 10(三端拆分方案)→ 11(既有库结构分析);前端读前端 02/04/07。
- **做界面设计 / 验收**:12(三端规划)→ 13(六模块补全)→ 14(入口整合与房间维度)→ 15(原系统功能对照)→ `UI/V7-A01`(四维查询与紧急区)→ `UI/_src/duty-console-v7.html`(交互)→ `UI/V7-A14`(追溯矩阵)→ `UI/A13`(28 项走查)。
## 快速事实
- 后端:Spring Boot 2.6.13 + Java 8,默认 jar 打包(可改 WAR),上下文路径 `/yktgl`,默认端口 `8081`。
- 数据库:KingbaseES 8(兼容 Oracle 语法),Redis 存 Session。
- 多公寓:同一套代码通过“客户端 IP → 公寓”解析 `gyid`,不依赖前端传参。
- 核心链路:设备/网页入寓登记 → 生成叫乘计划(Quartz 三个任务)→ 叫乘/复叫/报警/应答 → 离寓归档。
- 最大单体:后端 `jcgl/service/JcglService.java`(约 4192 行);前端 `src/views/JcXx.vue`(5294 行)。
- 前端:Vue 2.6 + Element UI,生产静态与接口同域 `/yktgl`;登录无真实 token,公寓 `gyid` 由后端按客户端 IP 解析。
+110
View File
@@ -0,0 +1,110 @@
# 01 系统总览
## 1. 业务定位
`yktgl` 是铁路行车公寓的**叫乘(叫班)管理系统**。公寓为值乘机车的乘务员提供住宿,系统负责把“入住—叫乘—出乘—离寓”串起来,并对接机务运安系统的乘务计划,确保乘务员按时出乘。
核心业务闭环:
1. 乘务员到寓,在设备或值班端完成**入寓登记**,分配房间与床位,读取/生成叫乘计划(开车时间、叫乘时间、复叫时间、叫乘结束时间)。
2. 到点自动**叫乘**:通过电话/广播(Asterisk 会议桥 + TTS)呼叫房间和值班台;未应答触发**复叫**,仍未应答触发**报警**。
3. 乘务员**应答**后记录应答时间,结束本次叫乘。
4. 乘务员**离寓**(刷卡/人脸/值班端操作),释放房间床位,写离寓时间与归档标记。
5. 卡务侧完成**补助发放、余额、消费、现金充值**;管理端提供房间、人员、交路、统计报表、设备心跳与指令。
## 2. 使用角色
| 角色 | 使用入口 | 主要动作 |
| --- | --- | --- |
| 公寓值班员 | Web 管理端(`YktVue`) | 入寓/离寓登记、叫乘监控与人工干预、换房、房间状态、充值审核 |
| 公寓管理员 | Web 管理端 | 人员、交路、房间、设备、报表、用户权限 |
| 乘务员 | 公寓设备(叫乘屏/签到机/房间屏/消费机/售货机) | 自助登记、查询、应答、消费、充值 |
| 保洁人员 | 房间设备/管理端 | 开始/结束保洁 |
| 运安系统 | SOAP / HTTP | 下发乘务计划、人员资料 |
| 中心服务器 | HTTP | 汇总公寓人员、财务数据 |
## 3. 运行形态
系统同时支持两种部署语义,但代码是同一套:
- **公寓端**:每套公寓一套服务(或同一服务按 IP 区分公寓),连接本地 Asterisk、串口控制台、各类设备。
- **中心/汇总端**:通过 `/synData/*`、`/excel/*` 提供人员与财务数据汇总;部分环境由中心下发交路。
```mermaid
flowchart LR
subgraph 公寓现场
DEV[叫乘屏/签到机/房间屏/消费机/售货机]
WEB[值班员浏览器 YktVue]
PHONE[电话/广播终端]
CONSOLE[串口控制台]
end
subgraph 应用服务器
APP[Spring Boot yktgl<br/>context /yktgl :8081]
WS[WebSocket 服务]
QZ[Quartz / @Scheduled]
REDIS[(Redis Session)]
DB[(KingbaseES gygl)]
end
subgraph 外部系统
AST[Asterisk AMI / Conf-01]
BYXX[机务运安 SOAP]
CENTER[中心服务器 HTTP]
end
DEV -->|HTTP 心跳/业务| APP
WEB -->|HTTP + WebSocket| APP
APP --- WS
APP --- QZ
APP --- REDIS
APP --- DB
QZ -->|呼叫| AST
AST --> PHONE
AST --> CONSOLE
APP -->|WSDL PbResource| BYXX
APP <-->|人员/财务| CENTER
```
## 4. 技术栈
| 层次 | 选型 | 说明 |
| --- | --- | --- |
| 运行时 | Java 8、Spring Boot 2.6.13 | 继承 `spring-boot-starter-parent`;`pom.xml` 未声明 packaging(默认 jar),入口继承 `SpringBootServletInitializer`,也可改为 WAR 部署到外部 Tomcat |
| Web | Spring MVC、Spring WebSocket | `@ServerEndpoint` + `ServerEndpointExporter` |
| 持久化 | MyBatis-Plus 3.1.0、PageHelper 1.4.1 | Mapper XML 位于 `classpath:mapper/**/*.xml` |
| 数据库 | KingbaseES 8(`kingbase8` 8.6.0) | URL 形如 `jdbc:kingbase8://host:54321/gygl?currentSchema=gygl`;SQL 大量使用 Oracle 兼容写法(`TO_DATE`、`NVL`、`ROWNUM`、`MERGE`、`LISTAGG`、`sysdate`) |
| 缓存/会话 | Redis + Spring Session | `@EnableRedisHttpSession`,默认 30 分钟 |
| 调度 | Quartz(内存模式)+ Spring `@Scheduled` | 叫乘任务用 Quartz,补偿/清理用 `@Scheduled` |
| 电话 | asterisk-java `3.38.0-zfjia` + jSerialComm | AMI 控制会议桥,串口控制广播/话机 |
| 文档 | Springfox Swagger 2.9.2 | 仅扫描 `jlgl.controller` |
| 报表 | Apache POI 4.1.2 | `/excel/*` 导出 |
| 其他 | fastjson2、Guava、Reflections、JNA | JSON、工具、类扫描、本地库调用 |
> 注意:`pom.xml` 中 `spring-boot-starter-web` 显式写了 `3.5.4`、`spring-boot-starter-data-redis` 写了 `3.3.1`,与 Boot 2.6.13 / Java 8 不匹配。见 [09-迭代指引与风险清单.md](09-迭代指引与风险清单.md)。
## 5. 多公寓识别(重要约束)
同一套后端服务多个公寓时,公寓身份由请求来源 IP 决定:
- 入口:`ZdwhService.getUniqueGyByIp(request)` / `getGyIdByIp(request)`。
- 解析顺序:设备表(`GY_SB`)中的 IP → 公寓字典 `GY_GYZD.ip`。
- 若同一 IP 命中多个公寓,直接拒绝,避免写错公寓数据。
- 业务代码中 `gyid` 不能信任前端传入;入住、房间、充值等写操作均由服务端重新解析或校验。
相关配置:`baseConfig.gyid`、`baseConfig.gymc` 作为单公寓/默认值兜底;各 Profile 不同。
## 6. 关键设计约束(迭代时不要破坏)
1. **房间占用以有效在寓明细为准**:`live_person`、`person_name` 由 `GY_JCXX` 中 `finish_ly='0' AND sjlysj IS NULL` 的记录实时重算,不能仅做 ±1 计数。
2. **交路快照与匹配分离**:运安计划先整表落到 `GY_SYN_JCXX`(快照),再由匹配流程写入 `GY_JCXX`;匹配有全局锁和状态位,避免抓取/推送并发写坏数据。
3. **叫乘任务可恢复**:Quartz 任务按 `ryid + 类型` 建键,应用重启后由 `WakeupJobRecoveryRunner` 从库中恢复未来任务。
4. **设备指令一次性投递**:指令写入设备指令表,心跳时下发,下发后置 `deleted=1`。
5. **事务内不做外部副作用**:WebSocket 推送、创建 Quartz 任务等放在事务提交后(`afterCurrentTransactionCommits`)。
6. **离寓有证据链**:人脸离寓会保存抓拍快照、写 `GYGL_LY_EVENT`,并以 `deviceEventId` 做幂等。
## 7. 与前端/设备的关系
- Web 管理端 `YktVue` 通过 `/yktgl/*` 接口与 WebSocket 交互;后端也会把旧构建产物放在 `src/main/resources/static` 下(历史遗留,见风险清单)。
- 设备通过各自的心跳接口注册,通过业务接口上报事件;服务端通过 WebSocket 向值班端推送刷新与人员信息。
- 叫乘执行不经过设备,而是直接走 Asterisk 电话/广播链路。
+167
View File
@@ -0,0 +1,167 @@
# 02 工程结构与分层
## 1. 目录结构
仓库根目录:
```
jcgl/
├── yktgl/ # 后端(Spring Boot / Maven)
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/org/example/yktgl/...
│ │ └── resources/
│ │ ├── application.yml / application-*.yml
│ │ ├── mapper/**/*.xml
│ │ ├── wsdl/PbResource.wsdl
│ │ ├── db/ # 手工升级脚本
│ │ └── static/ # 历史前端构建产物
│ └── test/java/...
├── YktVue/ # Vue 前端
└── docs/ # 本文档目录
```
后端 Java 包(`org.example.yktgl`):
| 包 | 职责 |
| --- | --- |
| `jcgl` | 叫乘核心:入寓/离寓、叫乘执行、房间、交路匹配、补助、卡余额、统计联动 |
| `jlgl` | 交路维护:交路 CRUD、叫乘时刻、上传、跨公寓同步、车次查询 |
| `regist` | 人员资料与注册:注册查询、照片、人员增删改 |
| `recharge` | 现金充值:设备发起、值班端处理、余额变动、补助入账 |
| `pay` | 消费:设备余额查询与扣款、消费明细 |
| `MealsOrdering` | 食堂/餐食订单与消费 |
| `kcgl` | 库存/出入库/物料/成品/文件(扩展业务) |
| `display` | 登录、用户、角色、权限、菜单、部门、日志 |
| `excel` | 各类报表导出(POI) |
| `xxtj` | 信息统计:日报、进出情况、月报、补助统计 |
| `zdwh` | 字典/基础资料:公寓、房间、工种、节假日、餐补标准;IP→公寓解析 |
| `syndata` | 运安/中心数据同步、同步日志、人员同步服务 |
| `heartBeat` | 设备心跳、设备指令、设备信息 |
| `callingDevice` / `checkInDevice` / `roomDevice` / `payDevice` / `registDevice` / `vendPayDevice` | 各类设备的接入层(心跳 + 业务) |
| `clean` | 房间保洁 |
| `jgxxgl` | 叫乘报警/警示信息查询 |
| `quartz` | Quartz 工具、任务管理、叫乘任务 `WakeUpJob` |
| `schedule` | `@Scheduled` 补偿任务与启动恢复 |
| `websockets` | 各类 WebSocket 端点 |
| `common` | 通用返回体、验证码、工具、Session/WebSocket 配置 |
| `config` | 异步线程池、RestTemplate、Swagger |
| `util` | 日期、字符串工具 |
| `test` | 测试/演示接口(生产应限制) |
## 2. 分层约定与调用链
典型调用链:
```
Controller(参数/鉴权/协议编解码)
→ Service(业务编排、事务、状态机)
→ Mapper 接口(MyBatis)
→ mapper/**/*.xml(SQL)
→ KingbaseES
```
实际代码中的偏差:
- 部分 Controller 直接调用 Mapper(如设备心跳、字典查询)。
- 设备接入层通常只有一个 Service,同时承担协议解析与业务调用。
- `JcglService` 体量过大,承担了叫乘、房间、人员、补助、同步、归档等多类职责;迭代时应优先“新增小服务 + 复用其中方法”,避免继续膨胀。
### 请求与返回
- 统一返回 `common/entity/Result` 或 `ResultData`,包含成功标志、消息、数据。
- 设备接口多返回 `ResultData`;Web 接口多返回 `Result`。
- 部分统计接口直接返回 `JSONObject`(如 `/synData/synPersonData`)。
### 事务与异常
- 写操作使用 `@Transactional`;捕获异常后调用 `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()` 再返回失败。
- 叫乘任务创建、WebSocket 推送等副作用通过 `afterCurrentTransactionCommits(...)` 延迟到提交后,避免“事务回滚但消息已发/任务已建”。
- 大量 `try/catch` 将异常转成 `Result.failure(message)`,同时写业务日志;排查时需同时看 `GY_LOGS` / 同步日志 / `System.out`。
## 3. 启动流程
入口:`YktglApplication`
```java
@EnableTransactionManagement
@SpringBootApplication
@EnableScheduling
public class YktglApplication extends SpringBootServletInitializer
```
启动关键动作:
1. Spring 容器初始化,激活 `spring.profiles.active`(默认 `local`)。
2. `SessionConfig` 开启 Redis HttpSession。
3. `WebSocketConfig` 注册 `ServerEndpointExporter`,扫描 `@ServerEndpoint`(`websockets/*`)。
4. `ServerStartupListener` 在启动时做初始化(端口/环境相关)。
5. `WakeupJobRecoveryRunner` 监听 `ApplicationReadyEvent`,从 `GY_JCXX` 恢复未来叫乘任务。
6. `@Scheduled` 任务开始运行(见 07)。
## 4. 配置与 Profile
- 主配置:`application.yml`,只保留通用项与 `active: local`。
- 环境配置:`application-{byq,cc,dev,dl,local,pj,prod,sjt,tl}.yml`,各自定义端口、数据源、Redis、公寓、AMI、串口等。
- 自定义前缀:`baseConfig.*`(公寓、中心地址、同步白名单、设备密码、服务器 OS)、`callcontrol.*`(AMI、录音、串口)。
详细字段见 [08-部署与配置.md](08-部署与配置.md)。
## 5. 公共组件速查
| 组件 | 位置 | 用途 |
| --- | --- | --- |
| `Result` / `ResultData` | `common/entity` | 统一返回体 |
| `CaptchaUtil` / `CaptchaResult` | `common` | 登录验证码 |
| `IpUtil` | `common/utils` | 取真实客户端 IP |
| `HttpRequest` | `common/utils` | 内部 HTTP 调用(运安人员接口等) |
| `MD5Util` / `PasswordEncoder` | `common/utils` | 口令处理 |
| `MyBatisUtils` | `common/utils` | MyBatis 辅助 |
| `SpringUtils` | `common/utils` | 静态获取 Bean |
| `AsyncConfig` | `config` | 异步线程池(核心 10、最大 200、队列 10) |
| `RestTemplateConfig` | `config` | HTTP 客户端 |
| `SwaggerConfig` | `config` | 仅扫描 `jlgl.controller` |
| `QuartzUtils` / `SchedulerManager` | `quartz` | 任务创建/取消/重建 |
| `CallDutyMatchLock` | `jcgl/service` | 交路抓取与匹配的互斥锁(DB 行锁 + 租约) |
| `AsteriskCallUtil` | `jcgl/service` | AMI 呼叫、会议桥、录音、挂断 |
| `JcWebsocket` 等 | `websockets` | 按公寓/房间推送 |
## 6. 测试现状
`src/test/java` 下已有部分单元/切片测试(录音控制器、当前在寓交路 SQL、最近同步时间、人员同步等),但工程 `target/` 为空,未见完整构建/测试产物。迭代前建议先补齐可重复的构建与测试基线。
## 7. 关键文件索引
> 行号为当前版本近似位置,改动后请以方法名检索。
| 文件 | 关注内容 |
| --- | --- |
| `jcgl/service/JcglService.java` | 叫乘/入住/离寓/房间/补助/同步/归档主逻辑(约 4192 行) |
| ├─ 559–1010 | 房间列表、推荐、房态修改、入住提交、离寓提交 |
| ├─ 1320–1450 | `processCallDuty` / `processTempCallDuty` 状态上报 |
| ├─ 1700–2150 | 远程同步、交路匹配、提醒补偿 |
| ├─ 2400–2510 | `ryzt` 状态更新与校验 |
| ├─ 2640–2800 | 按开车时间与时间规则计算叫乘时刻 |
| ├─ 3065–3175 | 刷脸离寓 |
| ├─ 3600–3700 | 房间校验、时间线校验 |
| └─ 3880–4192 | 停止叫乘、录音、归档 |
| `jcgl/controller/JcglController.java` | `/jcgl` 全部 Web/设备接口入口 |
| `jcgl/service/AsteriskCallUtil.java` | AMI 连接、呼叫、会议桥、TTS、录音、挂断 |
| `jcgl/service/CallDutyMatchLock.java` | 同步/匹配互斥锁 |
| `jcgl/service/ByxxResourceService.java` | 运安 WSDL 调用与快照写入 |
| `quartz/SchedulerManager.java` | 叫乘任务创建/取消/重建 |
| `quartz/WakeUpJob.java` | 单个叫乘任务执行 |
| `quartz/QuartzUtils.java` | Quartz 基础设施 |
| `schedule/JcSchedule.java` | 漏叫提醒、超时离寓、归档 |
| `schedule/SynDataSchedule.java` | 运安抓取/人员同步 |
| `schedule/WakeupJobRecoveryRunner.java` | 启动恢复任务 |
| `mapper/jcgl/JcglMapper.xml` | 叫乘核心 SQL(表结构事实来源之一) |
| `mapper/jcgl/JcglRoomMapper.xml` | 房间 SQL 与推荐逻辑 |
| `mapper/jcgl/LeaveEventMapper.xml` | 离寓事件 |
| `mapper/heartBeat/HeartBeatMapper.xml` | 设备、心跳、指令 |
| `mapper/recharge/RechargeMapper.xml` | 充值申请/余额/补助 |
| `heartBeat/service/HeartBeatService.java` | 指令写入与设备维护 |
| `zdwh/service/ZdwhService.java` | IP→公寓、字典 |
| `websockets/JcWebsocket.java` | 叫乘实时推送 |
| `application.yml` / `application-*.yml` | 运行配置 |
+97
View File
@@ -0,0 +1,97 @@
# 03 业务模块地图
## 1. 模块总表
| 模块(包) | 入口 | 核心服务 | 一句话职责 | 主要数据表 |
| --- | --- | --- | --- | --- |
| `jcgl` | `JcglController` `/jcgl`、`RecordingController` | `JcglService`(4192 行)、`AsteriskCallUtil`、`ByxxResourceService`、`CallDutyMatchLock` | 叫乘主流程、入寓/离寓、房间、补助、交路匹配、归档 | `GY_JCXX`、`GY_JCXX_HIS`、`GY_ROOMS`、`GY_SYN_JCXX`、`GY_YKT_*`、`GYGL_LY_EVENT` |
| `jlgl` | `JlGlController` `/jlgl` | `JlGlService` | 交路模板维护、叫乘时刻、上传/同步、车次查询 | `GYGL_JLXX`、`GYGL_JLXX_TY`、`GYGL_JLXX_LOGS`、`GY_GYZD` |
| `regist` | `RegistController` `/regist` | `RegistService` | 人员注册信息、照片、人员资料增删改 | `GY_YKT_RY`、人员照片表 |
| `recharge` | `RechargeController` `/recharge` | `RechargeService` | 现金充值申请/处理、余额与补助入账 | `GY_YKT_CASH_REQUEST`、`GY_YKT_REMAINDER`、`GY_YKT_XJCZJL`、`GY_YKT_ALLOWANCE` |
| `pay` | `PayController` `/pay` | `PayService` | 消费扣款、消费明细与统计 | `GY_YKT_CONSUME`、`GY_YKT_REMAINDER` |
| `MealsOrdering` | `MealsController` `/Meals` | `MealsService` | 餐食/订单、房间送餐、消费 | 餐食/订单相关表 |
| `kcgl` | `kcglController` `/kcgl` | `kcglService` | 库存、出入库、物料、成品、PDF 文件 | 库存/出入库相关表 |
| `display` | `/auth`、`/user`、`/role`、`/permission`、`/menu`、`/dept` | `SysUserService` 等 | 登录、会话、RBAC、菜单、部门、日志 | `GL_YH`、`GL_JS`、`GL_QX`、`GL_CD`、`GL_YH_JS`、`GL_JS_QX`、`GY_LOGS` |
| `excel` | `ExcelExportController` `/excel` | 直接查询/POI | 叫乘、日报、补助、充值、消费等 Excel 导出 | 只读多表 |
| `xxtj` | `xxtjController` `/xxtj` | `XxTjService` | 报警次数、进出情况、日报、月报、补助统计 | 只读多表 |
| `zdwh` | `ZdwhController` `/Zdwh` | `ZdwhService` | 公寓/房间/工种/节假日/餐补字典;IP→公寓 | `GY_GYZD`、`GY_ROOMS`、`GY_GZZD`、`GY_JRB`、`GY_JBXX` |
| `syndata` | `/synData`、`/jwDataLog` | `SynDataService`、`DriverPersonSyncService`、`JwDataLogService` | 运安人员、公寓人员、财务数据;同步日志 | `GY_YKT_RY`、`GY_JCXX`、`GY_YKT_REMAINDER`、同步日志表 |
| `heartBeat` | `HeartBeatController` `/heartBeat` | `HeartBeatService` | 设备注册、心跳、指令下发、版本/配置 | `GY_SB`、`GY_SB_XT`、设备指令表 |
| `callingDevice` | `/callingDevice` | `CallingDeviceService` | 叫乘屏心跳/业务 | `GY_SB` |
| `checkInDevice` | `/checkInDevice` | `CheckInDeviceService` | 签到机心跳/业务 | `GY_SB` |
| `roomDevice` | `/roomDevice` | `RoomDeviceService` | 房间屏心跳/应答/保洁 | `GY_SB` |
| `payDevice` | `/payDevice` | `PayDeviceService` | 消费机心跳/消费 | `GY_SB` |
| `registDevice` | `/registDevice` | `RegistDeviceHeartBeatService` | 注册机心跳 | `GY_SB` |
| `vendPayDevice` | `/vendPayDevice` | `VendPayDeviceService` | 售货机支付/退款 | 订单、余额 |
| `clean` | `CleanController` `/clean` | `CleanService` | 开始/结束保洁、保洁记录 | `GY_ROOMS`、保洁记录 |
| `jgxxgl` | `JgXxGlController` `/jgxxgl` | `JgXxGlService` | 报警/警示信息查询 | `GYGL_JGXX` |
| `quartz` | — | `SchedulerManager`、`WakeUpJob` | 叫乘/复叫/叫乘结束任务 | Quartz 内存 JobStore |
| `schedule` | — | 各 `@Scheduled` | 漏叫补偿、超时离寓、归档、运安同步、快照清理 | 多表 |
| `websockets` | `/websocket/*` | — | 值班端/房间端实时推送 | — |
## 2. 模块依赖关系
```mermaid
flowchart TD
DEV[设备接入层<br/>calling/checkIn/room/pay/regist/vend] --> CORE[jcgl 核心服务]
WEB[Web 控制器<br/>jcgl/jlgl/regist/recharge/pay/xxtj/excel] --> CORE
CORE --> JL[jlgl 交路]
CORE --> ZD[zdwh 字典/IP→公寓]
CORE --> HB[heartBeat 设备指令]
CORE --> WS[websockets 推送]
CORE --> QZ[quartz 叫乘任务]
QZ --> AST[AsteriskCallUtil]
SYNC[syndata / ByxxResourceService] --> CORE
CORE --> CARD[卡务: recharge/pay/Meals]
ALL[全部模块] --> DISP[display 登录/RBAC/日志]
```
关键耦合点:
- **`jcgl` 是事实上的“领域服务层”**,其他模块大量直接调用 `JcglService`(如 `pay` 调 `checkRemainder`/`isHoliday`,`regist` 调 `processPersonDeviceInstructions`,`recharge` 调余额与补助)。
- **`zdwh` 提供公寓上下文**,几乎所有写操作都依赖 `getGyIdByIp` / `getUniqueGyByIp`。
- **`heartBeat` 提供设备指令通道**,叫乘、入住、充值、人员变更都会写入指令。
- **`websockets` 只负责推送**,业务状态不以其为准。
## 3. 按业务视角分组
### 3.1 叫乘与住宿(核心)
- 入寓登记:`submit_rydj` / `processCheckInRegistration`
- 叫乘执行:`processCallDuty`、`processTempCallDuty`、`WakeUpJob`、`AsteriskCallUtil`
- 房间:`getRoomList`、`getRoomRecommendations`、`modifyRoomStatus`、`exchangeRoom`
- 离寓:`processCheckOutRegistration`、`handleCheckOut`、`submit_ccly`
- 交路匹配:`synCallDutyInfo`、`synUnMatchJcXx`、`matchCallDutyUnderLock`
- 归档:`syncJcXxData`
### 3.2 卡务与消费
- 补助:`getAllowanceInfo`、`getTrainAllowanceInfo`、`changeRemainder`、`insert_allowance`
- 余额:`checkRemainder`、`remakeRemainder`
- 消费:`PayService`、`VendPayDeviceService`、`MealsService.updateMoney`
- 充值:`RechargeService`(设备申请 + 值班端确认)
### 3.3 基础资料与管理
- 人员:`regist` + `syndata`(运安/中心)
- 交路:`jlgl`
- 公寓/房间/工种/节假日:`zdwh`
- 用户/角色/权限/菜单:`display`
### 3.4 统计与导出
- 页面统计:`xxtj`(日报、月报、补助统计)
- Excel:`excel`(叫乘明细、日报、补助、充值、消费及汇总)
## 4. 模块边界与迭代建议
| 场景 | 建议落点 | 说明 |
| --- | --- | --- |
| 叫乘规则(时间/次数/优先级) | `JcglService` + `quartz` | 先改状态机与任务生成,再改执行器 |
| 新的设备类型 | 新建 `xxxDevice` 包 | 复用 `heartBeat` 指令与 `GY_SB` |
| 新报表 | `xxtj` 或 `excel` | 统计 SQL 放对应 Mapper XML |
| 新的卡务规则 | `recharge` / `pay` | 余额变动必须与明细同事务 |
| 交路来源新增 | `syndata` + `jlgl` | 统一落到快照表再匹配 |
| 管理端新功能 | `display` + 对应业务模块 | 权限走 RBAC 表 |
> `kcgl`(库存)与叫乘主线关系较弱,更像同工程内的扩展业务;迭代叫乘时通常无需改动。
+294
View File
@@ -0,0 +1,294 @@
# 04 核心业务流程
本文按“人”和“房间”两条主线梳理:人从入寓到离寓,房间从占用到释放;同时说明叫乘任务如何被创建、执行和补偿。
## 1. 入寓登记
入口有两类:
- 设备端:`POST /jcgl/processCheckInRegistration`(签到机)
- 值班端:`POST /jcgl/submitAll` → `JcglService.submit_rydj`
### 1.1 设备端查询/建档(`processCheckInRegistration`)
| code | 行为 |
| --- | --- |
| 1 | 按 `ryid` 查询当前在寓记录、离寓快照、人员资料,返回给设备;用于设备判断“是否已入住/是否可入住” |
| 2 | 由人员资料 + 选择的交路(`jlid`)或班次构造入住信息,通过 WebSocket `{"lx":"ry","content":...}` 推送值班端确认;房间号仅作为协议字段,不在此步写库 |
| 3 | 按 `id` 查询指定记录 |
> 设备端只负责“采集 + 推送”,真正的写入统一走值班端确认后的 `submit_rydj`。
### 1.2 值班端提交(`submit_rydj`,`@Transactional`)
按顺序执行:
1. **重复入住校验**:`isExistCallDutyInfo(ryid, gyid)`,已有在寓记录直接失败。
2. **房间校验**:按 `gyid + room_no` 精确查房,房间不存在/状态不可入住/床位已满则失败。
3. **交路处理**:
- `jllx='0'` 表示按交路入住。
- 若传了 `jlid`(固定交路),从交路表取车次、机车号、起点、到达车次/时间,回填到入住记录。
- 若 `cc` 包含“便乘/驻在”,`jllx='1'`,不生成标准叫乘;否则调用 `populateJcXxByJlXx` 生成叫乘时间。
4. **开车日期校验**:有车次时必须传 `kcrq`,且格式为 `yyyy-MM-dd`。
5. **补助计算**:
- 固定交路:`getTrainAllowanceInfo(cc, sfffcb, gyid, jlid)`。
- 非固定交路:`getAllowanceInfo(gz, sfffcb, sjrysj, gyid)`(按工种、是否发补助、入住时间规则)。
- `changeRemainder` 生成/更新余额与补助明细;节假日可能跳过余额与补助写入。
6. **写入住记录**:`insertJcxx`,`ryzt='0'`、`finish_ly='0'`、`sjrysj=入住时间`,并记录房间号。
7. **设备指令**:
- 向签到机下发指令 `5`;
- 向房间设备下发指令 `1`(刷新房间屏)。
8. **占用重算**:`room_rydj` → `refreshRoomOccupancy`,从有效在寓明细重算 `live_person`、`person_name`,再按床位计算房态。
9. **余额/补助落账**:非节假日执行 `updateRemainder` + `insert_allowance`。
10. **叫乘任务**:固定交路且 `sfjc='1'` 且非便乘/驻在时,事务提交后创建 `WakeUpJob`,并置 `is_yaxx='1'`。
11. **推送**:WebSocket `{"lx":"refresh"}`。
失败时统一回滚并写业务日志。
### 1.3 房间推荐(`getRoomRecommendations`)
- 仅对“可入住”的房间给出推荐顺序;
- 排序依据:**同行房间**(同车次/同批人)→ **历史房间**(该人员曾住过);
- 推荐结果不作为最终校验,提交时仍由房间状态与床位兜底。
## 2. 叫乘状态机
### 2.1 人员状态 `ryzt`
| 值 | 含义 | 进入方式 |
| --- | --- | --- |
| `0` | 待叫乘/未叫乘 | 入住初始化、重置交路、任务取消 |
| `1` | 叫乘中(已叫/复叫) | 任务执行、设备上报 status=1/2 |
| `2` | 叫乘报警(未应答) | 报警任务、设备上报 status=3 |
| `3` | 已应答,待离寓 | 应答(status=5)或通话结束 |
| `4` | 离寓报警(超时未离寓) | 超时检查、设备上报 status=4 |
| `5` | 正在通话 | Asterisk 接通后临时状态 |
| `-1` | 查询用合成状态(未匹配/特殊行) | SQL 构造,不落库 |
```mermaid
stateDiagram-v2
state "0 待叫乘" as S0
state "1 叫乘中" as S1
state "2 叫乘报警" as S2
state "3 已应答待离寓" as S3
state "4 离寓报警" as S4
state "5 通话中" as S5
[*] --> S0
S0 --> S1: JC 任务 / 设备 status=1
S1 --> S1: FJ 复叫 / status=2
S1 --> S5: 电话接通
S5 --> S3: 挂断 / 应答
S1 --> S2: 未应答 / status=3
S2 --> S3: 人工应答 / status=5
S3 --> S4: 超时未离寓
S3 --> [*]: 办理离寓
S4 --> [*]: 办理离寓
```
配套字段:
- `jcsj_s` / `fjsj_s` / `jcbjsj_s`:实际叫乘/复叫/报警时间。
- `hjxysj`:呼叫应答时间。
- `finish_jc`:叫乘是否结束(`1` 结束)。
- `finish_ly`:是否离寓(`1` 离寓)。
- `stoped`:是否人工停止叫乘。
- `is_jcbj` / `is_lybj`:叫乘报警/离寓报警标记。
### 2.2 设备上报(`processCallDuty`)
设备 POST `/jcgl/processCallDuty`,`status` 语义:
| status | 动作 |
| --- | --- |
| 1 | 开始叫乘:`ryzt=1`、记录 `jcsj_s` |
| 2 | 开始复叫:记录 `fjsj_s`(状态保持叫乘中) |
| 3 | 叫乘报警:若原状态 < 2 则 `ryzt=2`,`is_jcbj=1`,记录 `jcbjsj_s`,写报警记录 |
| 4 | 离寓报警:`triggerLeaveAlarm`(条件更新,防重复),写报警记录 |
| 5 | 呼叫应答:`ryzt=3`、`hjxysj`、`finish_jc=1`、`stoped=0` |
临时叫乘走 `/jcgl/processTempCallDuty`,语义相同,但会额外刷新对应房间设备。
### 2.3 自动叫乘(Quartz)
入住/修改交路时为一个人创建 **3 个一次性任务**:
| 任务 | 触发时间 | 行为 |
| --- | --- | --- |
| `JC` | `jcsj` | 开始叫乘:状态置 1,TTS 呼叫值班台 + 房间;未接通则按规则转报警 |
| `FJ` | `fjsj`(叫乘 +10 分钟) | 复叫:再次呼叫;仍未接通转报警 |
| `JCJS` | `jcbjsj`(叫乘 +15 分钟) | 叫乘结束:挂断会议桥、状态置 2、写报警记录 |
任务键:`ryid + "JC"/"FJ"/"JCJS"`,组:`gyid + fjh`;同一房间/车次会合并为一个执行组,组长优先呼叫。任务执行前会校验 `jcid/fjh/时间` 是否仍与库中一致,避免过期任务误叫。
补偿:
- `SchedulerManager` 统一创建/取消/重建;
- `WakeupJobRecoveryRunner` 在应用启动后恢复未来任务;
- 会议桥忙时延迟 60 秒再试;
- 人工“停止叫乘”会取消任务、挂断通话、置 `stoped=1`。
### 2.4 叫乘时间规则
叫乘时间统一由**开车时间 + 本地时间规则**计算(运安下发的 `jcsj` 仅保留读取,不作为最终依据):
- 规则来自 `GY_SJGZ`(按 `gyid + 单位`):
- `zjjcsjgz`:站接叫乘提前分钟数;
- `kjjcsjgz`:库出叫乘提前分钟数;
- `lysjgz`:离寓时间规则。
- `站接`:`jcsj = kcsj - zjjcsjgz`;`库出`:`jcsj = kcsj - kjjcsjgz`。
- `fjsj = jcsj + 10 分钟`;`jcbjsj = jcsj + 15 分钟`;`ylysj = jcsj + lysjgz`。
- 若计算出的叫乘时间已过期:只发 TTS 提醒,不生成报警记录(`adjustPastCallDutyTimes`)。
## 3. 运安交路同步与匹配
### 3.1 推送式同步(`POST /jcgl/synCallDutyInfo`)
1. 校验来源 IP 是否在 `baseConfig.synJcXxIp` 白名单。
2. 解析 JSON 快照,校验 `id`、`workno`、`deptime` 必填,`id` 唯一。
3. 获取 `CallDutyMatchLock.tryAcquireSync()`(数据库协调行 + 租约),避免与抓取/匹配并发。
4. `TRUNCATE GY_SYN_JCXX` → 批量插入 → `GY_SYN_JCXX_STATUS='1'`。
5. 事务提交后释放锁;返回 `{code,message,result}`。
### 3.2 主动抓取(`ByxxResourceService.fetchJlXX`)
1. 获取同步锁。
2. 加载 `classpath:/wsdl/PbResource.wsdl`,创建 SOAP 客户端。
3. 清空 `GY_SYN_JCXX`。
4. 调用运安 `getDriverPlanByTimeStr(start, end)`,把司机 1–4 的计划分别写入快照。
5. 更新同步状态为 `1`,记录日志。
> `mergeSynJcXx` 已停用:历史数据存在同一 `id` 对应多名司机,按 `id` MERGE 会重复匹配。
### 3.3 匹配(`/jcgl/synUnMatchJcXx` → `matchCallDutyUnderLock`)
前置条件:`GY_SYN_JCXX_STATUS='1'`(同步完成),且配置了 `baseConfig.gyid`(匹配只处理该公寓)。
流程:
1. 获取数据库行锁,串行匹配。
2. 查询 `jllx='0'` 的有效在寓记录,跳过人工锁定交路的记录;若配置不允许“叫乘完成后变更交路”,则跳过已完成叫乘且未离寓的人员。
3. 以人员 `card_id`(工号)匹配 `GY_SYN_JCXX.workno`。
4. 命中且交路有变化:按开车时间 + 本地规则重算叫乘/复叫/报警/离寓时间,更新当前入住记录,取消并重建 3 个 Quartz 任务;事务提交后写“交路变更/首次自动匹配/重新匹配”事件日志并 TTS 提醒。
5. 命中但交路未变:检查任务是否齐全,缺失则补建。
6. 未命中且该人有开车时间:清空交路与时间、取消任务、TTS 通知本人。
7. 汇总写同步日志,WebSocket `"all"` 刷新。
### 3.4 归档
`POST /jcgl/syncJcXxData`:
- 条件:`finish_ly='1' AND sjlysj <= sysdate - 7`,且属于当前公寓。
- 分批 500 条:复制到 `GY_JCXX_HIS` → 从 `GY_JCXX` 删除。
- 统计冲突/无效条数并写日志。
- 每日 00:00 也有自动归档任务。
## 4. 离寓
### 4.1 刷脸离寓(`processCheckOutRegistration`)
| code | 行为 |
| --- | --- |
| 1 | 按 `ryid + gyid` 查询在寓记录与人员资料,返回离寓页面数据 |
| 2 | 刷脸离寓:校验事件幂等 → 保存抓拍 → 更新离寓字段 → 写离寓事件 → 更新房间 → 推送刷新 |
code=2 的关键点:
1. **幂等**:`deviceCode + deviceEventId` 已存在则直接返回“已经处理”;缺失事件号时只记录日志、不阻断(兼容旧设备)。
2. **抓拍**:Base64 只接受 JPEG;按 `leaveSnapshot` 配置保存到本地目录,生成 `/yktgl/photo/leave-snapshots/...` URL;未开启抓拍时按 `requiredForDevice` 决定是否报错。
3. **服务评分**:请求可带 `kffwpf/jcfwpf/stfwpf`,写入离寓记录。
4. **事件证据**:`recordLeaveEvent` 写 `GYGL_LY_EVENT`(类型 `FACE`,含 `deviceEventId`、`snapshotUrl`、操作人、备注)。
5. **提交离寓**:`submit_ccly` → 更新 `GY_JCXX`(`finish_ly=1`、`sjlysj`)→ `room_ccly` 释放房间 → 房间设备刷新 → `remakeRemainder` 处理余额(如退补助)。
6. **失败回滚**:异常时删除已保存快照并回滚事务。
### 4.2 值班端离寓
`POST /jcgl/handleCheckOut` 走同一套 `submit_ccly`,离寓类型记为 `MANUAL`,无设备事件号与抓拍。
### 4.3 超时未离寓
`JcSchedule` 每 15 秒检查:`finish_jc='1'`、`ryzt='3'`、`ylysj <= now`、未触发过的记录 → `triggerLeaveAlarm`(条件更新 `ryzt=4`、`lybjsj`、`is_lybj=1`)→ 写报警记录。
## 5. 房间与保洁
### 5.1 房态
| `room_status` | 含义 | 计算/设置 |
| --- | --- | --- |
| `1` | 空闲 | 结束保洁、恢复可入住 |
| `2` | 住满 | `live_person >= bed_numbers` |
| `3` | 待清扫 | 空房(`live_person=0`),退房后的默认状态 |
| `4` | 不能入住 | 人工设置(要求房间无人) |
| `5` | 未满 | `0 < live_person < bed_numbers` |
| `6` | 保洁中 | 保洁开始;结束后回到 `1` |
约束:
- 房间占用始终由 `GY_JCXX` 有效在寓明细重算,不直接 ±1。
- 入住校验会重新计算房态,拒绝 `4`(不能入住)、`6`(保洁中)、满员房间。
- 保洁中不允许直接改房态;需先结束保洁。
### 5.2 换房(`/jcgl/exchangeRoom`)
1. 校验新房间可入住;
2. 校验人员当前房间未变化(防并发);
3. 更新入住记录 `fjh`;
4. 重算旧房、新房占用;
5. 向两个房间设备下发刷新指令。
## 6. 卡务:补助、充值、消费
### 6.1 补助
- 入住时按规则计算补助,写 `GY_YKT_ALLOWANCE` 并更新 `GY_YKT_REMAINDER` 的补助余额。
- 固定交路按车次查固定补助;非固定交路按工种/时间规则。
- 节假日策略:`isHoliday()` 命中时不发补助、不入账。
### 6.2 现金充值(`RechargeService`)
设备协议 `code`:
| code | 行为 |
| --- | --- |
| 1 | 查询余额 |
| 2 | 创建充值申请(`GY_YKT_CASH_REQUEST`,状态 `PENDING`),WebSocket `"cz"` 通知值班端 |
| 3 | 查询申请状态 |
| 4 | 取消申请(`CANCELLED`) |
值班端处理 `/recharge/handleRecharge`:
- 金额必须 `> 0` 且 `< 100`,最多 2 位小数;
- 通过条件更新把申请从 `PENDING` 置为 `COMPLETED`(与取消互斥);
- 更新余额、写充值记录、向设备下发指令 `6`。
### 6.3 消费
- 消费机:`PayService`,code 1 查余额,code 2 扣款;节假日金额按 0 处理;“内部人员”可透支。
- 售货机:`VendPayDeviceService`,code 1 查余额,code 2 下单扣款,code 3 查询结果/退款;退款时恢复余额并标记 `refunded=1`。
- 所有扣款都写 `GY_YKT_CONSUME`,并与余额更新同事务。
## 7. 设备心跳与指令
1. 设备按类型调用 `/callingDevice/heartbeat`、`/checkInDevice/heartbeat`、`/roomDevice/heartbeat`、`/payDevice/heartbeat`、`/registDevice/heartbeat`、`/vendPayDevice/heartbeat`。
2. 心跳上报:`deviceCode`、`versionName`、`deviceType`、`ip`、`deviceUpdateState`、`lastUserUpdateTime`、`lastDeviceConfigUpdateTime`、`roomNumber` 等。
3. 服务端返回:`devicePassword`、`apartmentName`、`serverTime`、`updateState`、`apkUrl`、指令列表。
4. 指令写入设备指令表,心跳时取走并置 `deleted=1`(一次性投递)。
5. 叫乘/入住/离寓/充值/人员变更都会产生指令。
## 8. 流程与代码位置对照
| 流程 | 主要方法 |
| --- | --- |
| 入住提交 | `JcglService.submit_rydj` |
| 设备入住 | `JcglService.processCheckInRegistration` |
| 叫乘上报 | `JcglService.processCallDuty` / `processTempCallDuty` |
| 叫乘执行 | `quartz/WakeUpJob`、`quartz/SchedulerManager` |
| 电话/会议桥 | `jcgl/service/AsteriskCallUtil` |
| 离寓 | `JcglService.processCheckOutRegistration` / `submit_ccly` |
| 交路推送 | `JcglService.synCallDutyInfo` |
| 交路匹配 | `JcglService.matchCallDutyUnderLock` |
| 运安抓取 | `ByxxResourceService.fetchJlXX` |
| 归档 | `JcglService.syncJcXxData` |
| 房间 | `JcglService.getRoomRecommendations` / `modifyRoomStatus` / `exchangeRoom` |
| 充值 | `RechargeService` |
| 消费 | `PayService` / `VendPayDeviceService` |
+244
View File
@@ -0,0 +1,244 @@
# 05 数据模型
> 数据库为 KingbaseES 8,默认 schema `gygl`。表名大小写混用(`GY_JCXX`、`gy_rooms`、`GYGL_JLXX`),SQL 中需按现有写法保持一致。
> 本文只列核心表与关键字段;完整字段以 `src/main/resources/mapper/**/*.xml` 为准。
## 1. 核心关系图
```mermaid
erDiagram
GY_YKT_RY ||--o{ GY_JCXX : "ryid/card_id"
GY_JCXX }o--|| GY_ROOMS : "gyid + fjh"
GY_JCXX ||--o{ GYGL_LY_EVENT : "jcid"
GY_JCXX }o--o| GYGL_JLXX : "jlid"
GY_JCXX ||--o| GY_JCXX_HIS : "归档"
GY_YKT_RY ||--o| GY_YKT_REMAINDER : "ryid"
GY_YKT_REMAINDER ||--o{ GY_YKT_ALLOWANCE : "补助"
GY_YKT_REMAINDER ||--o{ GY_YKT_CONSUME : "消费"
GY_YKT_REMAINDER ||--o{ GY_YKT_XJCZJL : "现金充值"
GY_YKT_CASH_REQUEST ||--o| GY_YKT_XJCZJL : "完成充值"
GY_SYN_JCXX }o--|| GY_SYN_JCXX_STATUS : "同步状态"
GY_GYZD ||--o{ GY_ROOMS : "gyid"
GY_GYZD ||--o{ GY_SB : "gyid"
GY_SB ||--o| GY_SB_XT : "devicecode"
```
## 2. 住宿与叫乘
### 2.1 `GY_JCXX`(当前在寓/叫乘记录,核心表)
| 分组 | 字段 | 说明 |
| --- | --- | --- |
| 主键 | `jcid` | 入住记录 ID |
| 产生方式 | —(无来源列) | `gy_jcxx` 行由入寓登记当场创建(`submit_rydj` → `insertJcxx`),**不是**平台下发;平台只对其补全 `kcsj` / `jcfs` |
| 人员 | `ryid`、`card_id`、`rymc`、`rydw`、`gzzh`、`depotid` | 人员工号/卡号/姓名/单位/工资账号 |
| 公寓 | `gyid`、`gymc` | 公寓 |
| 房间 | `fjh` | 房间号(与 `GY_ROOMS.room_no` 对应) |
| 交路 | `jlid`、`cc`、`jch`、`cclx`、`qd`、`ddcc`、`ddsj` | 交路 ID、车次、机车号、接车方式、起点、到达车次/时间 |
| 时间 | `kcsj`、`jcsj`、`fjsj`、`jcbjsj` | 开车、叫乘、复叫、叫乘结束(计划) |
| 实际时间 | `jcsj_s`、`fjsj_s`、`jcbjsj_s`、`hjxysj`、`lybjsj` | 实际叫乘/复叫/报警/应答/离寓报警 |
| 入住离寓 | `yrysj`、`sjrysj`、`ylysj`、`sjlysj` | 预定/实际入住、预定/实际离寓 |
| 状态 | `ryzt`、`finish_jc`、`finish_ly`、`stoped`、`jllx` | 见状态字典 |
| 报警 | `is_jcbj`、`is_lybj`、`alarm_event_key` | 叫乘/离寓报警标记、报警事件键 |
| 通知去重 | `route_notice_key`、`call_timeout_notice_key`、`missed_call_notice_key` | 防止重复提醒 |
| 离寓证据 | `leave_type`、`leave_event_id`、`leave_remark`、`leave_snapshot_url` | 冗余自 `GYGL_LY_EVENT`,便于查询 |
| 其他 | `is_yaxx`、`yappsj`、`is_tempCall`、`recordfile`、`cblx`、`cbje`、`sfffcb` | 运安匹配标记、临时叫乘、录音文件、补助标识 |
有效在寓判定:`finish_ly='0' AND sjlysj IS NULL`。
### 2.2 `GY_JCXX_HIS`
与 `GY_JCXX` 同构的历史归档表;另有 `ly_event_id` 等证据字段。归档条件:已离寓且 `sjlysj <= sysdate - 7`。
### 2.3 `GY_ROOMS`
| 字段 | 说明 |
| --- | --- |
| `gyid`、`room_no` | 公寓 + 房间号(联合定位) |
| `room_floor`、`room_dir`、`room_face`、`is_edge`、`is_next_floor`、`next_floor` | 楼层/朝向/相邻关系(用于推荐与排序) |
| `room_status` | 房态,见下表 |
| `bed_numbers` | 床位数 |
| `room_type`、`room_dept`、`room_order` | 房型/所属部门/排序 |
| `live_person`、`person_name` | 当前人数/姓名串(实时重算) |
| `sip_account` | 房间 SIP 账号(叫乘呼叫用) |
| `bjkssj` | 保洁开始时间 |
| `room_status` | 含义 |
| --- | --- |
| `1` | 空闲 |
| `2` | 住满 |
| `3` | 待清扫 |
| `4` | 不能入住 |
| `5` | 未满 |
| `6` | 保洁中 |
### 2.4 `GYGL_LY_EVENT`(离寓事件证据)
| 字段 | 说明 |
| --- | --- |
| `id` | 事件 ID |
| `jcid`、`gyid`、`ryid`、`fjh` | 关联入住记录 |
| `leave_type` | `FACE` / `MANUAL` 等 |
| `device_code`、`device_event_id` | 设备与事件号(`device_code + device_event_id` 幂等) |
| `snapshot_url`、`remark`、`operator_id`、`operator_name` | 抓拍、备注、操作人 |
| `occurred_at`、`received_at` | 发生/接收时间 |
### 2.5 `GYGL_JLXX`(交路模板)
`id`、`jch`、`cc`、`qd`、`kcsj`、`jcsj`、`cclx`、`ddcc`、`ddsj`、`rysj`、`lysj`、`sfpc`、`bz`、`qr_fcd`、`qr_gy`、`lrsj`、`gyid`、`gymc`、`flag_send`、`bzje`。
关联:
- `GYGL_JLXX_TY`:交路停运/调整时间段(`gyid`、`cc`、`kssj`、`jssj`)。
- `GYGL_JLXX_LOGS`:交路操作日志。
- 交路叫乘时刻不单独建表:`getJlXxJcsjList` 由 `GYGL_JLXX` 的 `kcsj`(开车时刻)、`jcsj`(叫乘时刻)、`lysj`(离寓时刻)结合传入的开车日期 `kcrq` 计算得出,`fjsj = jcsj + 10 分钟`、`jcbjsj = jcsj + 15 分钟`。
## 3. 运安同步
### 3.1 `GY_SYN_JCXX`(运安计划快照)
`GY_SYN_JCXX` 是**运安计划快照**,即“匹配要素表”(提供 `kcsj` / `jcfs` / `jch`),**不是叫乘任务的生成源**——它不承载任务、不承载状态、不承载版本。匹配键为 `ryid`(工号)+ `cc`(车次)+ `kcsj` 归日。它是整表快照(`ByxxResourceService` 先清空再写入),任何本地写入都会被下次同步覆盖,因此**不得在其中写本地占位行**。
| 字段 | 说明 |
| --- | --- |
| `id` | 计划 ID(`jobid|taskdicid`) |
| `ryid`、`ryxm`、`sfzh`、`sjh` | 司机工号/姓名/身份证/手机 |
| `jch`、`cx`、`cc` | 机车号、车型、车次 |
| `kcsj` | 开车时间(匹配后据此本地计算叫乘时间) |
| `jhxl`、`jhcqdd`、`jcfs` | 计划线路、机车缺勤地点、接车方式 |
| `h1` | 派班室/公寓辅助字段 |
| `write_time` | 写入时间 |
### 3.2 `GY_SYN_JCXX_STATUS`
单行/少量行状态表,表示快照是否可用于匹配:`0` 未同步/同步中,`1` 同步完成。
### 3.3 `GY_SYN_JCXX_HIS`
同步快照历史(按需保留/排查用)。
### 3.4 `GY_CALL_DUTY_MATCH_LOCK`
交路抓取/推送/匹配的协调锁表。使用固定协调行(如 `__SYNC_JCXX__`)持有租约,避免多实例/多线程同时 TRUNCATE 与匹配。
### 3.5 `JWDATA_SYNC_LOG` / 同步日志
记录运安抓取、推送、匹配结果与异常,页面 `/jwDataLog/*` 查询。
## 4. 人员与卡务
### 4.1 `GY_YKT_RY`(人员)
`ryid`、`card_id`、`gzzh`、`xm`、`dw`、`depotid`、`gwmc`、`gzmcnew`、`sjh`、`sfzh`、`xb`、`mz`、`zm`、`deleted`、`gxsj`、`photoUrl`。
- `deleted='1'` 为逻辑删除,查询普遍带 `nvl(deleted,'0') != '1'`。
- `gzmcnew` 是岗位归类,用于补助规则与“内部人员”判断。
### 4.2 `GY_YKT_RY_ZP`
人员照片(`ryid` + `photoBase64` BLOB)。
### 4.3 `GY_YKT_REMAINDER`(卡余额)
`ryid`、`xm`、`xjye`(现金余额)、`knye`(卡内余额/可用余额)、`xfze`(消费总额)、`bzze`(补助总额)、`xfxjje`(现金消费额)、`xfbzje`(补助消费额)。
### 4.4 `GY_YKT_ALLOWANCE`(补助明细)
`ryid`、`gyid`、`cblx`(补助类型)、`cbje`(金额)、`sfffcb`、`ryzt`、`fjh`、`jlid`、`yrysj`、`sjrysj`、`ylysj` 等。
### 4.5 `GY_YKT_CONSUME`(消费明细)
`tid`(交易号)、`ryid`、`xm`、`xfje`、`xfsj`、`gyid`、`knye`、`xjye`、`xfze`、`bzze`、`xfxjje`、`xfbzje`、`deviceCode`、`xfly`(消费来源)、`is_jrxf`(节假日消费)。
### 4.6 `GY_YKT_XJCZJL`(现金充值记录)
充值流水,关联现金充值申请。
### 4.7 `GY_YKT_CASH_REQUEST`(现金充值申请)
| 字段 | 说明 |
| --- | --- |
| `id` | 申请 ID |
| `ryid`、`gyid`、`device_code` | 人员/公寓/设备 |
| `request_state` | `PENDING` / `CANCELLED` / `COMPLETED` |
| `created_at`、`updated_at` | 创建/更新时间 |
> 申请表本身不存金额;金额由设备/值班端在处理时传入,校验后写入充值流水与余额。
状态流转:`PENDING → COMPLETED`(值班端处理)或 `PENDING → CANCELLED`(设备取消),两者通过条件更新互斥。
### 4.8 `GY_YKT_BTJS` / `GY_YKT_BTBJ`
补助结算/补助报警相关表,配合补助统计与异常检查。
## 5. 设备
### 5.1 `GY_SB`(设备)
`DEVICECODE`、`GYID`、`LX`(类型)、`jrsj`、`gxsj`、`ip`、`fjh`(房间号)。
### 5.2 `GY_SB_XT`(设备心跳/系统信息)
`devicecode`、版本、`deviceType`、`ip`、`deviceUpdateState`、`lastUserUpdateTime`、`lastDeviceConfigUpdateTime`、心跳时间等。
### 5.3 `GY_SB_ZL`(设备指令/版本)
`id`、`devicecode`、`updateState`、`generatedTime`、`apkurl`、`deleted`。
- 业务代码写入指令后,设备心跳时按 `devicecode + deleted='0'` 取**最新一条**返回,并置 `deleted='1'`。
- 因此一次心跳只消费一条指令;多条指令会随多次心跳依次下发。
## 6. 基础资料与统计
| 表 | 说明 |
| --- | --- |
| `GY_GYZD` | 公寓字典(`gyid`、`gymc`、`ip` 等) |
| `GY_GZZD` | 工种字典 |
| `GY_JRB` | 节假日日历 |
| `GY_JBXX` | 基本/班次信息 |
| `GY_SJGZ` | 时间规则:`lysjgz`、`zjjcsjgz`、`kjjcsjgz`(按公寓 + 单位)。这三个字段是**叫乘时刻的派生参数**,不是叫乘时刻本身:站接时 `jcsj = kcsj − zjjcsjgz`;库出时 `jcsj = kcsj − kjjcsjgz`;`ylysj = jcsj + lysjgz`;`fjsj = jcsj + 10 分钟`、`jcbjsj = jcsj + 15 分钟`(与接车方式无关)。改规则只影响之后新算的任务,**不回溯**已算出的任务 |
| `GY_BJCSTJ`、`GY_BJXX` | 报警次数/报警信息 |
| `GYGL_JGXX` | 叫乘/离寓警示记录(`gyid`、`fssj`、`xx_cc`、`xx_sj`、`xx_ryid`、`xx_ry`、`xxlx`、`xxms`) |
| `GY_JDQKHZ` | 进出情况汇总 |
| `GY_RTJB`、`GY_YTJB` | 日报/月报 |
| `GY_LOGS`、`GL_LOGS` | 业务/登录日志 |
| `GY_ZBY` | 值班员信息 |
## 7. 权限与用户
`GL_YH`(用户)、`GL_JS`(角色)、`GL_QX`(权限)、`GL_CD`(菜单)、`GL_YH_JS`(用户-角色)、`GL_JS_QX`(角色-权限);另有 `SYS_USER`、`SYS_USER_ROLE`(历史/兼容)。
## 8. 库存扩展(kcgl)
`GYGL_KCGL_*`:仓库、供应商、物料、成品、入库单/出库单、盘点单、领料单、价格系数等;与叫乘主线弱相关。
## 9. 状态字典汇总
| 字段 | 取值 |
| --- | --- |
| `GY_JCXX.ryzt` | 0 待叫乘;1 叫乘中;2 叫乘报警;3 已应答待离寓;4 离寓报警;5 通话中;-1 查询合成 |
| `GY_JCXX.finish_jc` | 0 未结束;1 已结束 |
| `GY_JCXX.finish_ly` | 0 在寓;1 已离寓 |
| `GY_JCXX.jllx` | 0 **可参与运安匹配**(交路未定,等待匹配);1 **已由本地确定**(固定交路入住、人工指定或调整后置位),**不参与运安匹配**。注意:1 不是“非标准/便乘/驻在”的分类标签,而是**匹配开关 + 锁定标志**;`/resetJllx` 置回 0 使其重新进入匹配池 |
| `GY_ROOMS.room_status` | 1 空闲;2 住满;3 待清扫;4 不能入住;5 未满;6 保洁中 |
| `GY_YKT_CASH_REQUEST.request_state` | PENDING / CANCELLED / COMPLETED |
| `GY_JCXX.leave_type` | FACE / MANUAL 等 |
| 同步状态 | 0 未完成;1 完成 |
## 10. 升级脚本
`yktgl/src/main/resources/db/manual/` 下为手工执行的 Kingbase 脚本,按文件名日期顺序执行:
| 脚本 | 作用 |
| --- | --- |
| `V20260921_01__leave_snapshot_evidence_kingbase.sql` | 离寓抓拍证据字段/表 |
| `V20260922_01__call_duty_notice_dedup_kingbase.sql` | 叫乘提醒去重键 |
| `V20261008_01__syn_jcxx_write_time_kingbase.sql` | 快照写入时间 |
| `V20261008_02__call_duty_match_lock_kingbase.sql` | 匹配锁表与协调行 |
| `V20261009_01__jcxx_history_archive_kingbase.sql` | 历史归档字段/索引 |
| `V20261023_01__cash_request_state_kingbase.sql` | 现金充值申请状态 |
| `V20261029_01__reconcile_tlxc_room_occupancy.sql` | 通辽公寓房间占用校对(含 preview 版本) |
另有 `db/GY_SYN_JCXX_STATUS.sql` 初始化同步状态表。
> 这些脚本不在应用启动时自动执行,升级需人工按序执行并校验。执行前务必备份。
+227
View File
@@ -0,0 +1,227 @@
# 06 接口清单
## 1. 通用约定
- 基础路径:`http://host:8081/yktgl`(`server.servlet.context-path`)。
- 返回体:
- `Result`:`{code/flag, message, data}`(Web 接口常用)
- `ResultData`:设备接口常用
- 少数接口直接返回 `JSONObject` 或字符串。
- 公寓上下文:绝大多数写接口通过客户端 IP 解析 `gyid`;少数接口显式接收 `gyid`,但服务端仍会校验。
- **方法约束宽松**:大量接口用 `@RequestMapping`,GET/POST 都可访问,部分写操作也能用 GET 触发。新增接口建议明确 `@GetMapping`/`@PostMapping`。
- 字符集:JSON 为 UTF-8;设备请求体常为单行 JSON。
## 2. 叫乘核心 `/jcgl`
| 方法 | 路径 | 用途 | 目标架构承接 |
| --- | --- | --- | --- |
| ALL | `/getJcxxByRyid` | 按人员/记录查叫乘信息 | — |
| ALL | `/getEmployeeArriveInfo` | 按单位查人员到达信息 | — |
| POST | `/getJcXx` | 条件查询在寓/叫乘列表 | — |
| POST | `/resetJllx` | 重置交路类型 | `UNLOCK_MATCH`(解锁后回到运安匹配池),由 `apt:jcxx:lockMatch` 权限点管控 |
| GET | `/getLatestSyncTime` | 当前公寓最近同步时间 | — |
| GET | `/getSjgz` | 时间规则(叫乘/离寓提前量) | — |
| ALL | `/getJlxxList` | 按车次/机车号查交路 | — |
| ALL | `/getRoomList` | 房间列表 | — |
| GET | `/getRoomRecommendations` | 入住房间推荐 | — |
| ALL | `/modifyRoomStatus` | 设置不能入住/恢复可入住 | — |
| ALL | `/submitAll` | 提交入寓登记(`submit_rydj`) | — |
| ALL | `/updateMeal3` | 更新第三餐补助 | — |
| ALL | `/getBtje_info` | 按入住时间查补助金额 | — |
| ALL | `/getBtje_infoMeal3` | 第三餐补助信息 | — |
| ALL | `/getRyxx` | 按人员查资料 | — |
| ALL | `/getRyxxByRyId` | 按人员查资料(含公寓上下文) | — |
| ALL | `/remakeRemainder` | 重算余额/补助 | — |
| ALL | `/modifyJcXx` | 修改叫乘信息并重建任务 | `POST /apt/jcxx/{jcid}/commands` 的 `ASSIGN_ELEMENT`(换交路/车次/开车时刻/接车方式)+ `ADJUST_TIME`(只调 jcsj)。语义差异:旧接口一次可改多个字段且不区分“要素”与“时刻”,新接口强制 reason、commandId 幂等、并把 jllx 置 1 锁定 |
| POST | `/getJcXxFromGy` | 从公寓端获取叫乘信息 | **方向违反“平台不回调公寓、下行一律拉”**——依据是 `docs/backend/10-架构优化方案-三端拆分.md` **表 5-1 第 4 行**(“平台端 → 公寓端|不允许直连|仅落‘待下发配置版本’,由公寓端拉取”)与**表 8-4「出站 443/tcp」行**(“单向出站,平台侧不回调”)。能力由公寓端 #8 上行(要素登记)+ #7 响应体(核验结论)反向承接;**阶段五下线**,前置条件见 `docs/backend/10` 表 9-6 修订后文字(本清单 R-08) |
| ALL | `/getZbyList` | 值班员列表 | — |
| ALL | `/getGzList` | 工种列表 | — |
| POST | `/modifyRyXx` | 修改人员资料 | — |
| POST | `/deleteRyXx` | 删除人员 | — |
| ALL | `/isHoliday` | 是否节假日 | — |
| ALL | `/getGyOptions` | 按单位取公寓选项 | — |
| POST | `/synCallDutyInfo` | 运安计划推送(整表快照) | 运安快照写入入口;目标架构在平台侧由 #6 下行替代,公寓端不再直连运安(`docs/backend/10` 表 9-6) |
| POST | `/processCheckInRegistration` | 设备入寓登记(code 1/2/3) | — |
| POST | `/processCheckOutRegistration` | 设备离寓(code 1/2) | — |
| POST | `/processCallDuty` | 叫乘设备状态上报(status 1–5) | — |
| POST | `/processTempCallDuty` | 临时叫乘状态上报 | — |
| ALL | `/getCcJcXx` | 按车次聚合叫乘信息 | — |
| ALL | `/getCcJcXx_Ls` | 历史车次叫乘信息 | — |
| ALL | `/getCallDutyInfoByDeviceCode` | 叫乘屏按设备码取人员 | — |
| ALL | `/getTempCallDutyInfoByDeviceCode` | 临时叫乘屏取人员 | — |
| ALL | `/getSynJcXx` | 分页查运安快照 | — |
| ALL | `/getRzXx` | 入住信息 | — |
| ALL | `/getRemoteSynJcXx` | 远程同步(当前为桩实现,返回空成功) | — |
| ALL | `/synUnMatchJcXx` | 触发交路匹配 | — |
| POST | `/syncJcXxData` | 手工归档 7 天前已离寓记录 | — |
| ALL | `/handleTempCalling` | 临时叫乘(人工) | — |
| ALL | `/handleRoomCall2` / `/handleRoomCallEnd2` | 房间呼叫/结束(新版) | — |
| ALL | `/handleRoomCall` / `/handleRoomCallEnd` | 房间呼叫/结束(旧版) | — |
| ALL | `/handleMultiRoomCall` / `/handleMultiRoomCallEnd` | 多房间呼叫/结束 | — |
| ALL | `/handleCheckOut` | 值班端办理离寓 | — |
| ALL | `/cancelTrainAssignment` | 取消交路并清理任务 | — |
| ALL | `/getWslRyXx` | 未匹配人员信息 | — |
| ALL | `/getRyFjXx` | 人员房间信息 | — |
| ALL | `/exchangeRoom` | 换房 | — |
| ALL | `/handleStopCalling` | 停止叫乘(需报警事件键) | — |
| ALL | `/yatest` | 运安 SOAP 连通性测试 | — |
| ALL | `/ya/list` | 触发运安计划抓取 | — |
| ALL | `/driver/{driverId}` | 查询运安司机信息 | — |
| GET | `/getRecordingConfig` | 录音回放配置(RecordingController) | — |
## 3. 交路 `/jlgl`
| 路径 | 用途 |
| --- | --- |
| `/addJlXx` | 新增交路 |
| `/modifyJlXx` | 修改交路 |
| `/getJlXxList` | 交路列表 |
| `/getJlXxJcsjList` | 交路及叫乘时刻 |
| `/delJlXx`、`/deleteJlXx` | 删除交路 |
| `/uploadJlXx` | Excel 上传交路 |
| `/getMessageFromGy`、`/getMessageFromFcd` | 从公寓/发车点获取信息 |
| `/getGyXxList`、`/getTyXxList` | 公寓/停运信息 |
| `/addTySjList` | 新增停运时间 |
| `/confirmJlXx` | 交路确认 |
| `/getTrainNumberInfo` | 车次信息 |
## 4. 人员与注册 `/regist`
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST | `/registPersonInfo` | 设备注册协议(code 1 拍照、2 查/更新照片、3 按 ID 查、4 批量增量) |
| POST | `/addPersonInfo` | 新增人员(写真 + 照片表) |
| ALL | `/getPersonelInfo` | 按手机号/姓名查人员 |
| POST | `/updatePersonelInfo` | 修改人员并下发设备指令 |
| ALL | `/deletePersonelInfo` | 逻辑删除人员并清理照片 |
## 5. 充值 `/recharge`
| 路径 | 用途 |
| --- | --- |
| `/processCashRecharge` | 设备充值协议(code 1–4) |
| `/handleRecharge` | 值班端处理充值 |
| `/autoRecharge` | 自动补助入账(对应定时器已停用) |
| `/getAllowance` | 补助查询 |
| `/getRechargeInfo` | 充值记录 |
| `/getRechargeStatisInfo` | 充值统计 |
| `/getAllowanceStatis` | 补助统计 |
## 6. 消费 `/pay`
| 路径 | 用途 |
| --- | --- |
| `/processPayment` | 设备消费协议(code 1 查余额、2 扣款) |
| `/getConsumDetailInfo` | 消费明细 |
| `/getCashConsumeStatisInfo` | 现金消费统计 |
| `/getAllowanceConsumeStatisInfo` | 补助消费统计 |
| `/getConsumeStatis` | 消费汇总 |
## 7. 设备接入
| 基础路径 | 接口 | 用途 |
| --- | --- | --- |
| `/callingDevice` | `POST /heartbeat` | 叫乘屏心跳 |
| `/checkInDevice` | `POST /heartbeat` | 签到机心跳 |
| `/roomDevice` | `POST /heartbeat` | 房间屏心跳 |
| `/payDevice` | `POST /heartbeat` | 消费机心跳 |
| `/registDevice` | `POST /heartbeat` | 注册机心跳 |
| `/vendPayDevice` | `POST /heartbeat`、`POST /processPayment` | 售货机心跳/支付 |
| `/heartBeat` | `/getDeviceInfo`、`/getDeviceStatusInfo`、`/modifyDeviceInfo`、`/modifyApkVersion` | 管理端设备查询/维护 |
### 7.1 心跳请求字段(常见)
`deviceCode`、`versionName`、`deviceType`、`ip`、`deviceUpdateState`、`lastUserUpdateTime`、`lastDeviceConfigUpdateTime`、`roomNumber`。
### 7.2 心跳响应字段
`devicePassword`、`apartmentName`、`serverTime`、`updateState`、`apkUrl`、指令列表。
### 7.3 设备业务协议
| 场景 | code/status | 说明 |
| --- | --- | --- |
| 入寓登记 | code 1/2/3 | 查询/提交/按 ID 查询 |
| 离寓 | code 1/2 | 查询/刷脸离寓 |
| 叫乘 | status 1–5 | 开始/复叫/叫乘报警/离寓报警/应答 |
| 临时叫乘 | status 1–5 | 同上,另刷新房间设备 |
| 充值 | code 1–4 | 查余额/申请/查状态/取消 |
| 消费 | code 1/2 | 查余额/扣款 |
| 售货机 | code 1/2/3 | 查余额/下单/结果或退款 |
| 注册 | code 1–4 | 拍照/查询/按 ID/批量增量 |
| 保洁 | — | 开始/结束保洁(`/clean`) |
## 8. 管理后台
### 8.1 登录与权限
| 路径 | 用途 |
| --- | --- |
| `/auth/getCaptcha`、`/auth/captchaImage` | 验证码 |
| `/auth/login` | 登录 |
| `/auth/logon` | 注册/开通 |
| `/auth/logout` | 退出 |
| `/auth/getInfo` | 当前用户信息 |
| `/auth/getMenuInfo` | 用户菜单 |
| `/user/*` | 用户增删改查、改密、启停 |
| `/role/*` | 角色与角色权限 |
| `/permission/*` | 权限树与权限维护 |
| `/menu/*` | 菜单维护 |
| `/dept/*` | 部门树、公寓部门树 |
### 8.2 基础资料 `/Zdwh`
时间规则、公寓、房间、节假日、餐补标准的新增/修改/删除/查询;公寓与房间资料是入住与推荐的基础。
### 8.3 统计与导出
| 路径 | 用途 |
| --- | --- |
| `/xxtj/getBjcs` | 报警次数 |
| `/xxtj/getJdqk` | 进出情况 |
| `/xxtj/getRtjb` | 日报 |
| `/xxtj/getYtjb` | 月报 |
| `/xxtj/getDailyApartmentReport`、`/getDailyApartmentReportFull` | 公寓日报 |
| `/xxtj/getApartmentAllowanceStatis` | 补助统计 |
| `/excel/exportJcXxExcell` | 叫乘明细 |
| `/excel/exportDailyApartmentReport` | 日报 |
| `/excel/exportJcXxExcellOld` | 旧版叫乘明细 |
| `/excel/exportAllowanceExcell` | 补助明细 |
| `/excel/exportRechargeExcell` | 充值明细 |
| `/excel/exportConsumeExcell` | 消费明细 |
| `/excel/exportRechargeStatisExcell` | 充值统计 |
| `/excel/exportAllowanceStatisExcell` | 补助统计 |
| `/excel/exportCashConsumeStatisExcell` | 现金消费统计 |
| `/excel/exportAllowanceConsumeStatisExcell` | 补助消费统计 |
### 8.4 其他
| 路径 | 用途 |
| --- | --- |
| `/clean/startCleaning`、`/endCleaning`、`/getRoomCleanInfo` | 保洁 |
| `/jgxxgl/getJgXx` | 报警/警示查询 |
| `/Meals/*` | 餐食、订单、房间、餐次列表 |
| `/kcgl/*` | 库存/出入库/物料/成品/文件 |
| `/synData/*` | 运安/公寓人员、财务数据 |
| `/jwDataLog/getSynJcXxIsOffline` | 同步状态/是否离线 |
| `/websocket/{username}`、`/jcwebsocket/{username}`、`/jlwebsocket/{username}`、`/rlywebsocket/{username}`、`/MealsWebsocket/{username}` | WebSocket 端点 |
## 9. WebSocket 消息
| 消息 | 含义 |
| --- | --- |
| `{"lx":"refresh"}` | 叫乘/房间/人员数据刷新 |
| `{"lx":"ry","content":...}` | 入寓人员信息推送值班端 |
| `{"lx":"zcxx","photoBase64":...,"deviceCode":...,"zcid":...}` | 注册拍照 |
| `{"lx":"xgxx","content":...,"newPhotoBase64":...}` | 人员资料/照片更新 |
| `{"lx":"cz"}` | 充值申请提醒 |
`JcWebsocket.sendMessage(message, gyid)` 会按公寓路由;`gyid="all"` 表示广播。
## 10. 接口扩展建议
1. 新接口明确 HTTP 方法,避免用 `@RequestMapping` 触发写操作。
2. 统一返回体,不在 Controller 里拼 `JSONObject`。
3. 前端传入的 `gyid` 只能作为提示,服务端必须重新解析/校验。
4. 设备协议新增字段时保持向后兼容:旧字段可缺省,新增字段给默认值。
5. 接口文档以本清单为基线,最终以 `JcglController` 等源码为准;Swagger 目前只扫描 `jlgl.controller`,覆盖不全。
@@ -0,0 +1,210 @@
# 07 外部集成与定时任务
## 1. Asterisk 电话/广播链路
### 1.1 角色
- 叫乘执行者:`AsteriskCallUtil`。
- 连接方式:Asterisk Manager Interface(AMI),使用定制版 `asterisk-java 3.38.0-zfjia`。
- 会议桥:`Conf-01`。
- 呼叫对象:
- 值班台:`Local/1701@from-internal`(`callcontrol.ami.console`,默认 1009/1010)。
- 房间:房间 `sip_account`(`GY_ROOMS.sip_account`)经 `Local/1901@from-internal` 呼入。
- 串口:`callcontrol.console.*`(默认 `COM1`、9600),用于广播/话机控制。
### 1.2 配置项
```yaml
callcontrol:
ami:
host: 127.0.0.1
port: 7058
user: ${AMI_USER}
pwd: ${AMI_PASSWORD}
console: 1010
callduration: 180
console:
port: COM1
baudrate: 9600
recording:
base-url: ... # 录音回放地址
```
### 1.3 一次叫乘的时序
```mermaid
sequenceDiagram
participant Q as Quartz WakeUpJob
participant S as JcglService
participant A as AsteriskCallUtil
participant AMI as Asterisk AMI
participant C as 值班台
participant R as 房间话机
Q->>S: 触发 JC/FJ/JCJS
S->>A: makeWakeup(人员, 房间SIP)
A->>A: 组装 TTS 文本
A->>AMI: 呼叫 1701(值班台)
A->>AMI: 呼叫 1901 + 房间 SIP
AMI->>C: 接通
AMI->>R: 接通并加入 Conf-01
A->>AMI: 播放 TTS
R-->>A: ConfbridgeJoin / Answered
A->>S: ryzt=5(通话中)→ 挂断后 ryzt=3(待离寓)
```
### 1.4 TTS 文案
> 尊敬的{姓名},{公寓名称}提醒您,您值乘的{车次}次,接车方式是{接车方式},开车时间{开车时间},现在时间{当前时间},请您携带好随身物品,准备出乘。
车次、接车方式、时间会做口语化格式化(`formatTrainCodeForSpeech` / `formatPickupModeForSpeech` / `formatTrainTimeForSpeech`)。
### 1.5 超时与挂断
| 常量 | 值 | 说明 |
| --- | --- | --- |
| `CALL_TIMEOUT_MS` | 30 秒 | 呼叫无应答超时 |
| `WAKEUP_ANSWERED_AUTO_HANGUP_SECONDS` | 40 秒 | 叫乘接通后自动挂断 |
| `SHOUT_CALL_ANSWERED_AUTO_HANGUP_SECONDS` | 30 秒 | 广播/喊话接通后自动挂断 |
事件监听:
- `ConfbridgeJoinEvent`:入会,记录通道与接通。
- `ConfbridgeLeaveEvent`:离会,清理跟踪。
- `NewCallerIdEvent`:回填通道/主叫。
- `HangupEvent`:挂断,若已接通且需跟踪则置 `ryzt=3`;未接通则进入未应答处理(转报警/复叫)。
录音:接通后记录录音文件名,追加到 `GY_JCXX.recordfile`;`/jcgl/getRecordingConfig` 返回回放地址。
### 1.6 停止/挂断
- `stopWakeupCall` / `stopWakeupCallAsync`:挂断该人员所有跟踪通道、值班台/房间通道、`Conf-01`。
- `hangupTrackedCallsByRyid`:只挂断跟踪到的通道。
- `isConferenceBusy`:会议桥占用时任务延迟 60 秒。
## 2. 机务运安(BYXX)SOAP
### 2.1 接入方式
- WSDL:`classpath:/wsdl/PbResource.wsdl`。
- 生成客户端:`byxx.resource.PbResource_Service` / `PbResource`。
- 服务实现:`ByxxResourceService`。
### 2.2 主要操作
| 操作 | 用途 |
| --- | --- |
| `getLocoHomeList` | 机务段列表 |
| `getDriverInfoBySectionCode` | 按段获取司机资料 |
| `getDriverInfoByWorkNo` | 按工号获取司机 |
| `getDriverPlanByTimeStr` | 按时间段获取乘务计划(叫乘快照来源) |
### 2.3 计划字段映射
| 运安字段 | 快照字段 |
| --- | --- |
| `jobid + taskdicid` | `id` |
| `driver1..driver4` | `ryid`(一条计划拆成最多 4 条人员记录) |
| `plantrainnum` | `cc` |
| `model + loco` | `jch` |
| `dispmancode` | `h1` |
| `plandeptime` | `kcsj` |
| `plancalltime` | `jcsj`(仅保留,最终叫乘时间按本地规则重算) |
| `plandepstation` | `jhcqdd` |
| `jobType`(0 库出,其他站接) | `jcfs` |
### 2.4 调度
- 每 5 分钟抓取一次(`SynDataSchedule.getSynDataJob`),窗口约 4 小时。
- 每 5 天同步一次人员(`SyncPersonData`,仅 `T` 开头机务段)。
- 抓取/匹配共用 `CallDutyMatchLock`,防止并发清表。
- **匹配只处理 `baseConfig.gyid` 配置的公寓**;一实例多公寓部署时,其他公寓不会自动匹配,需改造或拆实例。
### 2.5 已知限制
- `mergeSynJcXx` 已停用(同 `id` 多司机导致 MERGE 重复)。
- `getRemoteSynJcXx` 为桩实现,未恢复远程 HTTP 同步。
- WSDL 地址与派班室/公寓编码硬编码(“通辽南公寓”“通辽南派班室”),多公寓部署需参数化。
## 3. 中心服务器 HTTP
| 场景 | 地址/配置 | 说明 |
| --- | --- | --- |
| 人员资料拉取 | `http://10.32.112.153:52411/api/users?pagenum=N` | `SynDataService.personData`,分页拉取机务人员 |
| 人员变更推送 | `/synData/modifyPersonData` | 运安推送人员增量 |
| 公寓人员查询 | `/synData/getApartmentPersonData*` | 按公寓/工号/时间查询在寓人员 |
| 财务数据 | `/synData/synFinanceData` | 按 IP 解析公寓后返回余额数据 |
| 照片/静态资源 | `baseConfig.centerServerUrl` / `serverUrl` | 人员照片 URL 前缀 |
中心同步使用 `HttpRequest`(内部封装),未见超时/重试统一配置,排障时需关注网络与超时。
## 4. WebSocket
| 端点 | 用途 |
| --- | --- |
| `/websocket/{username}` | 通用推送(历史) |
| `/jcwebsocket/{username}` | 叫乘主界面刷新/人员信息 |
| `/jlwebsocket/{username}` | 交路界面 |
| `/rlywebsocket/{username}` | 人员/入住相关 |
| `/MealsWebsocket/{username}` | 餐食订单 |
关键消息见 [06-接口清单.md](06-接口清单.md) 第 9 节。服务端推送按 `gyid` 路由,`"all"` 广播。
## 5. Quartz 叫乘任务
| 项 | 说明 |
| --- | --- |
| Job 类 | `quartz/WakeUpJob` |
| 调度管理 | `quartz/SchedulerManager` |
| JobKey | `ryid + "JC"` / `"FJ"` / `"JCJS"` |
| TriggerGroup | `gyid + fjh` |
| 触发方式 | 一次性 cron,精确到秒 |
| 优先级 | JC=1、FJ=2、JCJS=3 |
| 并发控制 | `@DisallowConcurrentExecution` |
| 参数 | `JobParas`(人员、房间、车次、时间、jcid 等) |
| 执行前校验 | `jcid/fjh/叫乘时间` 与库中一致才执行,否则跳过 |
| 合并执行 | 同房间/同车次合并为一个执行组,组长先叫 |
| 会议桥忙 | 延迟 60 秒 |
| 恢复 | `WakeupJobRecoveryRunner` 在 `ApplicationReadyEvent` 后恢复未来任务 |
失败处理:
- 任务内部异常写业务日志,不抛到 Quartz。
- `JCJS` 挂断会议桥、置 `ryzt=2`、写报警记录。
- 人工停止叫乘会取消该人员全部任务。
## 6. Spring `@Scheduled` 清单
| 类.方法 | Cron | 作用 | 状态 |
| --- | --- | --- | --- |
| `JcSchedule.notifyMissedCallWarnings` | `5 * * * * ?`(每分钟第 5 秒) | 超时未叫乘分级提醒(第 1/10/15 分钟) | 启用 |
| `JcSchedule.triggerOverdueLeaveAlarms` | `15 * * * * ?`(每分钟第 15 秒) | 已应答但超时未离寓报警 | 启用 |
| `JcSchedule.syncJcXxData` | `0 0 0 * * ?`(Asia/Shanghai) | 归档已离寓满 7 天记录 | 启用 |
| `SynDataSchedule.getSynDataJob` | `0 */5 * * * ?` | 运安抓取 + 记录同步时间 + 触发匹配 | 启用 |
| `SynDataSchedule.synPersonDataJob` | `30 0 0 */5 * ?` | 每 5 天同步运安人员 | 启用 |
| `LeaveSnapshotCleanupSchedule.cleanupExpiredLeaveSnapshots` | `${leaveSnapshot.cleanupCron:0 30 2 * * ?}` | 清理过期离寓抓拍 | 启用 |
| `RechargeSchedule.*` | — | 自动补助/充值 | 全部注释停用 |
| `JlSchedule.*` | — | 交路定时同步 | 注释停用 |
| `LogSchedule.*` | — | 日志清理 | 注释停用 |
## 7. 并发控制与幂等
| 机制 | 位置 | 说明 |
| --- | --- | --- |
| 交路同步锁 | `CallDutyMatchLock` + `GY_CALL_DUTY_MATCH_LOCK` | 抓取/推送/匹配互斥,事务提交后释放 |
| 同步状态位 | `GY_SYN_JCXX_STATUS` | 只有 `1` 才能匹配 |
| 设备事件幂等 | `GYGL_LY_EVENT(device_code, device_event_id)` | 刷脸离寓重复请求直接返回已处理 |
| 报警去重 | `triggerLeaveAlarmIfPending` 条件更新 | 同一次入住只触发一次离寓报警 |
| 提醒去重 | `route_notice_key` / `call_timeout_notice_key` / `missed_call_notice_key` | 同一节点只提醒一次 |
| 充值互斥 | `GY_YKT_CASH_REQUEST` 条件更新 | 完成与取消只能成功一个 |
| 房间占用 | `refreshRoomOccupancy` + 有效在寓明细 | 防止计数漂移 |
| 任务过期校验 | `WakeUpJob` 执行前比对库中数据 | 防止旧任务误叫 |
## 8. 排障入口
1. **叫乘没响**:看 Quartz 任务是否存在(`SchedulerManager` 日志)→ AMI 连接 → 会议桥是否忙 → 房间 `sip_account` 是否为空 → 任务时间是否已过期被跳过。
2. **叫乘响了但状态不变**:看 AMI 事件(Join/Hangup)是否被正确匹配通道 → `GY_JCXX.ryzt` / `recordfile`。
3. **运安计划没进来**:看 5 分钟任务日志、`GY_SYN_JCXX_STATUS`、`JWDATA_SYNC_LOG`、WSDL 可达性。
4. **匹配不上**:核对 `GY_YKT_RY.card_id` 与 `GY_SYN_JCXX.workno`,以及人员是否在有效在寓状态、`jllx` 是否被人工锁定。
5. **离寓报错**:看抓拍配置 `leaveSnapshot.*`、事件幂等记录、`GYGL_LY_EVENT`。
6. **设备指令不下发**:看 `GY_SB` 是否有该设备、心跳是否带 `deviceCode`、指令是否已被置 `deleted=1`。
+191
View File
@@ -0,0 +1,191 @@
# 08 部署与配置
## 1. 构建与运行
### 1.1 环境要求
| 组件 | 要求 |
| --- | --- |
| JDK | 8 |
| Maven | 3.6+ |
| 数据库 | KingbaseES 8(部分 Profile 仍接 Oracle) |
| 缓存 | Redis(Session) |
| 电话 | Asterisk + AMI 账号 + 会议桥 `Conf-01` |
| 串口 | 广播/话机控制串口(默认 `COM1`,Linux 下为 `/dev/ttyS0` 等) |
| 文件 | 照片、离寓抓拍、录音的可读写目录 |
### 1.2 构建
```bash
cd yktgl
mvn clean package -DskipTests
```
- `pom.xml` 未声明 `<packaging>`,默认生成 jar。
- 入口继承 `SpringBootServletInitializer`,如需部署到外部 Tomcat,可改为 `<packaging>war</packaging>` 并排除内嵌容器。
- 构建前注意 `pom.xml` 中 Boot 3.x 依赖版本与 Boot 2.6.13 的兼容问题(见 09)。
### 1.3 启动
```bash
java -jar yktgl-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
```
或:
```bash
java -Dspring.profiles.active=prod -jar yktgl-0.0.1-SNAPSHOT.jar
```
默认端口 `8081`,上下文路径 `/yktgl`。
## 2. Profile 对照
| Profile | 公寓 `gyid` / `gymc` | 数据源 | 备注 |
| --- | --- | --- | --- |
| `local` | `TMXC` 图们行车公寓 | Kingbase `127.0.0.1:54321/gygl` | 本机调试,Redis/AMI/串口均本机 |
| `dev` | 未启用(注释为 `TLXC`) | Kingbase `10.32.209.76:54321/gygl` | 开发环境 |
| `prod` | `TLNXC` 通辽南行车公寓 | Kingbase `10.33.165.41:54321/gygl` | 生产 |
| `tl` | `TLXC` 通辽行车公寓 | Kingbase `10.32.209.76:54321/gygl` | 通辽 |
| `sjt` | `SJTXC` 苏家屯行车公寓 | Kingbase `10.32.209.76:54321/gygl` | 苏家屯 |
| `byq` | `BYQXC` 鲅鱼圈行车公寓 | Oracle `10.32.116.45/db3` | 仍在 Oracle |
| `cc` | `SJTXC` 苏家屯行车公寓 | Oracle `10.32.5.175/rlxt` | 仍在 Oracle,注意与 `sjt` 同名不同库 |
| `dl` | `DLXC` 大连行车公寓 | Oracle `10.32.116.45/db3` | 仍在 Oracle |
| `pj` | `PJXC` 盘锦行车公寓 | Oracle `10.32.116.45/db3` | 仍在 Oracle |
> 所有 Profile 中的数据库口令、AMI 口令、`devicePassword` 目前均为明文,且已提交到仓库。生产使用前应改为环境变量/配置中心,并轮换口令。
## 3. 关键配置项
### 3.1 服务
| 配置 | 说明 |
| --- | --- |
| `server.port` | 默认 8081 |
| `server.servlet.context-path` | `/yktgl` |
| `spring.profiles.active` | 默认 `local` |
| `spring.mvc.pathmatch.matching-strategy` | `ant_path_matcher`(兼容 Springfox) |
### 3.2 数据源与连接池
| 配置 | 说明 |
| --- | --- |
| `spring.datasource.url` | Kingbase 需带 `currentSchema=gygl` |
| `spring.datasource.driver-class-name` | `com.kingbase8.Driver` |
| `spring.datasource.hikari.*` | 最小空闲 5、最大 15、连接超时 60s、最大生命周期 600s |
### 3.3 Redis / Session
| 配置 | 说明 |
| --- | --- |
| `spring.redis.host/port` | Redis 地址 |
| `spring.session.store-type` | `redis` |
| `spring.session.timeout` | 默认 30m |
### 3.4 公寓与业务开关 `baseConfig`
| 配置 | 说明 |
| --- | --- |
| `gyid` / `gymc` | 当前公寓(单公寓/默认兜底) |
| `centerServerUrl` | 中心服务器地址(照片前缀等) |
| `serverUrl` | 服务自身地址 |
| `synJcXxIp` | 运安推送来源 IP 白名单(逗号分隔) |
| `devicePassword` | 设备心跳返回的设备密码 |
| `centralPhoneIp` | 中心电话 IP |
| `serverOS` | `windows` / `linux`,影响照片与抓拍路径 |
| `modifyyasj` | 是否允许调整运安叫乘时间 |
| `allowRouteChangeAfterCompletedCall` | 默认 `true`;叫乘完成后是否允许运安匹配继续变更交路 |
| `syncMatchWaitSeconds` | 默认 900;抓取/匹配等待同步锁的最长秒数 |
| `fjgydbs` | 分公寓/终端所属公寓段的交互标识(按需配置) |
### 3.5 电话与录音 `callcontrol`
| 配置 | 说明 |
| --- | --- |
| `callcontrol.ami.host/port` | AMI 地址 |
| `callcontrol.ami.user/pwd` | AMI 账号 |
| `callcontrol.ami.console` | 值班台分机/控制台号 |
| `callcontrol.ami.callduration` | 呼叫时长上限(秒) |
| `callcontrol.console.port/baudrate/...` | 串口参数 |
| `callcontrol.recording.base-url` | 录音回放根地址;为空时默认 `http://<ami.host>/conf/` |
| `RECORDING_BASE_URL` | 环境变量覆盖录音地址 |
### 3.6 离寓抓拍 `leaveSnapshot`
| 配置 | 默认 | 说明 |
| --- | --- | --- |
| `enabled` | `true` | 是否保存抓拍 |
| `requiredForDevice` | `false` | 设备未传抓拍是否失败(兼容旧设备) |
| `retentionDays` | `7` | 保留天数 |
| `cleanupCron` | `0 30 2 * * ?` | 每日清理 |
| `rootDirectory` | 见 Profile | 抓拍保存目录 |
| `urlPrefix` | `/yktgl/photo/leave-snapshots` | 访问前缀 |
### 3.7 静态资源与文件
| 配置 | 说明 |
| --- | --- |
| `spring.web.resources.static-locations` | `classpath:/static/` + 文件目录(Linux 常见 `/home/gygl/resouce/`) |
| 人员照片 | `.../resouce/photo/{ryid}.png`(代码中写死,Windows 为 `C:\project_doc\resouce\photo\`) |
| 离寓抓拍 | `.../resouce/photo/leave-snapshots/{gyid}/{eventId}.jpg` |
> 照片路径在 `RegistService` 中按 `serverOS` 硬编码,迁移环境时需同步修改代码或改为配置项。
## 4. 数据库升级
手工脚本位于 `yktgl/src/main/resources/db/`,按日期顺序执行:
1. `GY_SYN_JCXX_STATUS.sql`(首次部署)
2. `manual/V20260921_01__leave_snapshot_evidence_kingbase.sql`
3. `manual/V20260922_01__call_duty_notice_dedup_kingbase.sql`
4. `manual/V20261008_01__syn_jcxx_write_time_kingbase.sql`
5. `manual/V20261008_02__call_duty_match_lock_kingbase.sql`
6. `manual/V20261009_01__jcxx_history_archive_kingbase.sql`
7. `manual/V20261023_01__cash_request_state_kingbase.sql`
8. `manual/V20261029_01__reconcile_tlxc_room_occupancy_preview.sql`(先预览)
9. `manual/V20261029_01__reconcile_tlxc_room_occupancy.sql`(确认后执行)
注意:
- 脚本不会随应用启动自动执行。
- 执行前备份;先跑 preview 核对影响行数。
- 若数据库仍为 Oracle,需确认脚本语法是否兼容(现有 manual 脚本以 Kingbase 为主)。
## 5. 多公寓部署
两种方式:
1. **一套服务一个公寓**:通过 `baseConfig.gyid/gymc` 指定;设备 IP 仍参与解析。
2. **一套服务多个公寓**:依赖 `GY_SB.ip` / `GY_GYZD.ip` 做 IP→公寓映射;同一 IP 命中多个公寓会直接拒绝。
部署检查:
- 每个公寓的设备 IP 必须唯一归属。
- 反向代理必须透传真实客户端 IP(`X-Forwarded-For`),否则所有请求会被识别为代理 IP。
- WebSocket 需允许 `Upgrade`/`Connection` 头,并适当延长超时。
## 6. 上线检查清单
- [ ] JDK 8、时区 `Asia/Shanghai`、服务器时间与 NTP 同步(叫乘精确到秒)。
- [ ] Kingbase 可连,schema `gygl` 正确,升级脚本已执行。
- [ ] Redis 可连,Session 能写入。
- [ ] `baseConfig.gyid/gymc` 与当前公寓一致。
- [ ] `synJcXxIp` 包含运安推送源 IP。
- [ ] `devicePassword` 与设备端一致。
- [ ] Asterisk AMI 可连,`Conf-01` 存在,值班台/房间分机可呼通。
- [ ] 串口设备可打开(Linux 检查权限与设备名)。
- [ ] 照片/抓拍目录存在且可写,静态资源映射正确。
- [ ] 反向代理透传真实 IP,WebSocket 可升级。
- [ ] 启动日志无 Mapper/Bean 初始化错误;`WakeupJobRecoveryRunner` 已恢复任务。
- [ ] 用一个测试人员走通:入住 → 叫乘 → 应答 → 离寓 → 归档。
## 7. 运行期维护
| 事项 | 建议 |
| --- | --- |
| 日志 | 现有 `System.out` 与业务日志混杂,建议接入统一日志文件与轮转 |
| 归档 | 每日 00:00 自动归档 7 天前离寓记录;数据量大时关注执行耗时 |
| 抓拍清理 | 每日 02:30 清理过期抓拍;确认磁盘余量 |
| 同步 | 运安每 5 分钟抓取;观察 `JWDATA_SYNC_LOG` 与同步状态 |
| 备份 | 数据库 + 照片/抓拍目录 + 录音目录 |
| 口令 | 尽快外置并轮换数据库/AMI/设备口令 |
@@ -0,0 +1,94 @@
# 09 迭代指引与风险清单
## 1. 技术债与风险
### P0:会直接影响构建/上线
| 项 | 现状 | 影响 | 建议 |
| --- | --- | --- | --- |
| Spring Boot 依赖版本错配 | `pom.xml` 在 Boot 2.6.13 父工程下显式声明 `spring-boot-starter-web:3.5.4`、`spring-boot-starter-data-redis:3.3.1` | 可能拉入 Spring 6 / Jakarta 依赖,与 `javax.servlet` 代码不兼容,导致编译/运行失败 | 删除显式版本,交给父工程管理;或统一升到 Boot 3 + Jakarta(大改造)。先跑一次 `mvn clean test` 确认基线 |
| 明文口令与内部地址入库 | 各 `application-*.yml` 含数据库口令、AMI 口令、`devicePassword`、内网 IP | 已推送到远端,存在泄露风险 | 轮换口令;改用环境变量/配置中心;生产库最小权限 |
| 无根级 `.gitignore` | 仓库根目录没有 `.gitignore`,仅有 `YktVue/.gitignore` | 易误提交构建产物、日志、数据转储 | 在仓库根补充 `.gitignore`(target、logs、*.log、node_modules、dist、本地配置) |
| 构建/测试基线不明 | `target/` 为空,测试未运行,无 CI 配置 | 无法判断改动是否破坏现有功能 | 建立 CI:编译 + 单测 + 关键 SQL 校验 |
### P1:影响正确性与可维护性
| 项 | 现状 | 影响 | 建议 |
| --- | --- | --- | --- |
| `JcglService` 过大 | 约 4192 行,混合叫乘、房间、补助、交路、归档、设备指令 | 改动风险高、冲突多、难测试 | 按“叫乘执行 / 房间 / 卡务 / 交路匹配 / 归档”拆服务,先抽只读逻辑与纯函数 |
| 交路 MERGE 停用 | `mergeSynJcXx` 因同 `id` 多司机被注释 | 快照与历史数据可能长期不一致 | 改为 `id + ryid` 唯一键后恢复,并加数据清理脚本 |
| 远程同步为桩 | `/jcgl/getRemoteSynJcXx` 直接返回成功 | 调用方误以为已同步 | 恢复实现或明确返回“未启用” |
| 部分定时任务停用 | `RechargeSchedule`、`JlSchedule`、`LogSchedule` 全部注释 | 自动补助、交路同步、日志清理不生效 | 明确替代方案;若不再需要则删除代码 |
| 写接口用 `@RequestMapping` | 大量写操作接受 GET | 易被误触发、难做权限/审计 | 新接口强制 POST;存量逐步收敛 |
| 异常被吞 | 大量 `catch` 返回 `Result.failure`,仅写日志 | 故障定位困难、事务边界不清 | 统一异常处理与错误码;关键路径保留堆栈 |
| 日志不规范 | `System.out.println` 与业务日志混用 | 无法集中检索/轮转 | 统一 SLF4J + 文件轮转;敏感信息脱敏 |
| 硬编码 | 公寓名“通辽南公寓/通辽南派班室”、照片路径、内网 IP、中心地址 | 多公寓/迁移环境需改代码 | 抽到配置或字典表 |
| 交路匹配只覆盖单公寓 | `matchCallDutyUnderLock` 只处理 `baseConfig.gyid` 配置的公寓 | 一实例多公寓时,其他公寓不会自动匹配交路 | 明确部署形态;如需一实例多公寓,把 `gyid` 改为循环/列表并逐公寓加锁 |
| 房态语义不一致 | `selectRoomRecommendations` 与前端允许 `3`(待清扫)、`6`(保洁中);`validateRoomForCheckIn` 也允许 `6` | 保洁中的房间可能被入住 | 确认业务规则;若不允许,收紧 SQL 与校验 |
| 备份文件/旧构建 | `RoomLook copy.vue`、`JcXx.vue.bak_mojibake`、`resources/static` 下旧前端与 `.gz` | 干扰阅读、增大仓库 | 清理或移入 `legacy/` |
### P2:体验与稳健性
| 项 | 说明 |
| --- | --- |
| `processCallDuty` 空值判断顺序 | `ryid.equals("") || ryid == null` 在 `ryid=null` 时会先 NPE;应改为 `StringUtils.isBlank` |
| `h1.concat(...)` 结果未赋值 | `ByxxResourceService` 中拼接公寓编码无效,`h1` 实际未追加 `housecode` |
| `syncJlXX` 返回 `null` | `/jcgl/ya/list` 永远返回空数据,容易误判 |
| `personData` 空数组 | `jsonArray.getJSONObject(0)` 未判空,上游无数据时异常 |
| 时间字符串 | 全链路用字符串 + `TO_DATE`,格式/时区问题只能在运行期暴露 |
| IP→公寓 | 依赖真实客户端 IP;反向代理未透传时会识别错误 |
| 设备照片/抓拍 | Base64 体积大,需关注请求体大小、内存与磁盘 |
| 测试接口暴露 | `/test/*`、`/jcgl/yatest`、`/jcgl/driver/{id}` 可访问外部系统,生产应限制 |
## 2. 改动落点速查
| 需求 | 先看 | 可能要改 |
| --- | --- | --- |
| 调整叫乘/复叫/报警时间 | `GY_SJGZ`、`modifyJcXxBySynJcXx`、`populateJcXxByJlXx` | `JcglService`、`WakeUpJob`、`SchedulerManager` |
| 改叫乘提醒节点 | `JcglService.notifyMissedCallWarnings`、`selectMissedCallWarnings` | `JcSchedule`、SQL |
| 改叫乘语音内容 | `AsteriskCallUtil.makeWakeup` | TTS 文案、格式化函数 |
| 改房间状态规则 | `getRoomStatus`、`validateRoomForCheckIn`、`modifyRoomStatus` | `JcglRoomMapper.xml`、前端 `Room.vue` |
| 改房间推荐 | `selectRoomRecommendations` | 推荐 SQL、`getRoomRecommendations` |
| 新增设备类型 | `heartBeat`、`GY_SB`、`GY_SB_XT` | 新建 `xxxDevice` 包、心跳控制器 |
| 改设备指令 | `HeartBeatService.addDeviceInstructionInfo*` | 各业务调用点 |
| 改入住补助 | `getAllowanceInfo`、`getTrainAllowanceInfo`、`changeRemainder` | `GY_YKT_*` 表、节假日规则 |
| 改充值/消费 | `RechargeService`、`PayService`、`VendPayDeviceService` | 余额与明细同事务 |
| 改交路维护 | `JlGlService`、`GYGL_JLXX` | 上传模板、同步接口 |
| 改运安同步 | `ByxxResourceService`、`SynDataSchedule` | WSDL、字段映射、锁 |
| 改匹配规则 | `matchCallDutyUnderLock` | 工号匹配、时间重算、任务重建 |
| 改离寓/抓拍 | `processCheckOutRegistration`、`leaveSnapshot.*` | `GYGL_LY_EVENT`、文件目录 |
| 新增报表 | `xxtj` / `excel` | 对应 Mapper XML |
| 改权限/菜单 | `display`、`GL_*` | 登录、RBAC、菜单 |
| 改公寓识别 | `ZdwhService.getUniqueGyByIp` | `GY_SB`、`GY_GYZD`、代理配置 |
## 3. 建议的迭代顺序
1. **建立基线**:修复 pom 版本错配,确保能编译;跑通现有测试;加 CI。
2. **安全整改**:口令外置与轮换;生产关闭测试接口;补根 `.gitignore`;清理备份文件。
3. **稳定性**:统一日志与异常;关键路径加监控(叫乘任务创建数、AMI 连接、同步状态、快照积压)。
4. **结构治理**:拆分 `JcglService`;把硬编码公寓/路径/地址配置化;收敛写接口 HTTP 方法。
5. **业务增强**:恢复并测试 `mergeSynJcXx`、远程同步;完善多公寓参数化;补叫乘/入住/离寓的集成测试。
6. **数据治理**:归档与索引优化;房间占用对账;历史数据清理。
## 4. 待确认问题
建议与业务/现场逐条确认,确认后回写本文档:
1. **保洁中(`room_status=6`)房间是否允许入住?** 当前推荐 SQL、入住校验、前端都允许,但 `modifyRoomStatus` 又禁止修改保洁中房间。
2. **待清扫(`3`)房间是否应出现在推荐列表?** 当前会出现,可能影响值班员选房效率。
3. **便乘/驻在是否完全不叫乘?** 当前 `jllx=1` 且跳过任务;是否符合现场?
4. **运安下发的 `jcsj` 是否彻底弃用?** 当前仅保留读取,最终按本地规则重算。
5. **远程同步是否还需要?** `getRemoteSynJcXx` 目前是桩。
6. **多公寓部署形态**:一公寓一实例,还是一实例多公寓?影响 IP 映射、同步任务与锁。
7. **刷脸抓拍是否强制?** `requiredForDevice=false` 目前兼容旧设备,何时切为 `true`?
8. **Oracle Profile(byq/cc/dl/pj)是否继续维护?** 若迁移 Kingbase,需要同步 SQL 兼容性。
9. **叫乘任务恢复范围**:应用重启后恢复未来任务,是否也要补偿“重启期间已错过”的任务?当前只恢复未来任务,错过由提醒任务兜底。
10. **归档保留策略**:`GY_JCXX_HIS` 保留多久?是否需要冷备/清理?
## 5. 文档维护约定
- 本文档以代码为准;接口、表、状态码变更时同步更新对应章节。
- 新增模块时,在 [03-业务模块地图.md](03-业务模块地图.md) 增加一行,并在 [06-接口清单.md](06-接口清单.md) 补接口。
- 新增表/字段时,在 [05-数据模型.md](05-数据模型.md) 补字段与状态字典。
- 新增外部依赖/定时任务时,在 [07-外部集成与定时任务.md](07-外部集成与定时任务.md) 补时序与排障点。
- 每次迭代结束,把已解决的风险从本清单移除,并在提交说明中引用相关文档章节。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,451 @@
# 11 既有数据库结构分析(KingbaseES)
> 文档性质:现状实证分析(Analysis Only)。本文所有结论来自对 `db/gygl.sql` 的结构解析与对已导入实例的只读查询(全部为 `SELECT`),**不含**数据库变更、DDL 执行、配置修改。改造建议仅为设计输入,需评审后另行排期。
## 0. 文档说明
表 0-1 采集信息
| 项 | 内容 |
| --- | --- |
| 数据来源 | `db/gygl.sql`(Navicat 导出,源库 `10.33.165.41:54321/gygl`,导出时间 2026-11-07 14:34) |
| 校验实例 | 本机 KingbaseES 导入库,`jdbc:kingbase8://127.0.0.1:54321/gygl` |
| 采集方式 | JDBC 只读查询(information_schema / pg_catalog / pg_stat_user_tables)+ dump 文本解析 |
| 库与 schema | 数据库 `gygl`,业务 schema `gygl`,表 owner 均为 `system` |
| 规模 | 40 张表、524 个字段、schema 总容量 117 MB、495,772 行数据 |
| 关联文档 | `05-数据模型.md`(表语义与状态码)、`09-迭代指引与风险清单.md`(技术债)、`10-架构优化方案-三端拆分.md`(目标归属) |
> 样本口径提醒:本机实例的数据是导出快照,业务数据只覆盖 2 个公寓(见 3.9)。行数、分布用于判断**结构缺陷**,不作为生产容量基线;容量测算需在生产库重跑附录 A 的 SQL。
## 1. 实例与库级特征
表 1-1 库级参数(实测)
| 参数 | 实测值 | 结构影响 |
| --- | --- | --- |
| 版本 | `KingbaseES V009R001C010`(`server_version` 报 12.1) | 兼容 PG 12 协议;JDBC 9.0.0 驱动可直连 |
| 兼容模式 | `database_mode = oracle` | 出现 `sys.date`、`sys_guid()`、`sysdate()`、`dual` 表等 Oracle 兼容对象 |
| 空串语义 | `ora_input_emptystr_isnull = on` | **写入 `""` 存成 `NULL`**,读取恒为 `NULL`;见 3.4 |
| 字符集 | `UTF8` | 中文数据无乱码风险 |
| MAC 扩展 | schema `sysmac`(8 张表,248 kB) | 金仓安全版强制访问控制扩展;上线前需确认是否启用策略 |
| 对象种类 | 视图 0、触发器 0、函数 0、分区表 0 | **业务逻辑全部在 Java 侧**,库内无任何约束性逻辑 |
表 1-2 schema 分布
| schema | 表数 | 容量 | 说明 |
| --- | --- | --- | --- |
| `gygl` | 40 | 117 MB | 全部业务表 |
| `sysmac` | 8 | 248 kB | 金仓安全版 MAC 元数据(空表) |
| `sys_hm` | 3 | 128 kB | 金仓自带 |
| `sys` | 1 | 40 kB | 金仓自带 |
结论:**这是一个"纯存储库"**——没有主键、没有索引、没有外键、没有触发器、没有存储过程。数据完整性、唯一性、顺序性、并发控制全部由应用代码承担,这也是下一章几乎所有缺陷的共同根因。
## 2. 表清单与规模
图 2-1 数据域与表簇
```mermaid
graph LR
A[公寓主数据<br/>gy_jbxx / gy_gyzd / gy_sjgz / gy_jcsj / gy_jrb] --> B[房间<br/>gy_rooms]
B --> C[叫乘执行<br/>gy_jcxx / gy_jcxx_his<br/>gy_bjxx / gy_bjcstj]
D[交路同步<br/>gy_syn_jcxx / _his / _status] --> C
E[人员与卡<br/>gy_ykt_ry / gy_ykt_ry_zp] --> C
F[账务<br/>gy_ykt_remainder / _allowance / _consume / _btbj / _btjs] --> E
G[设备<br/>gy_sb / gy_device / gy_sb_xt / gy_sb_zl / _his] --> C
H[权限<br/>gl_yh / gl_js / gl_qx / gl_cd / gl_yh_js / gl_js_qx]
I[日志<br/>gy_logs / gy_operation_log]
```
表 2-1 业务表清单(按数据域)
| 表 | 行数 | 大小 | 列数 | 含 gyid | 现职责 |
| --- | --- | --- | --- | --- | --- |
| `gy_jbxx` | 9 | 16 kB | 26 | ✔ | 公寓基础信息(面积、床位、负责人、热水/燃气方式) |
| `gy_gyzd` | 37 | 16 kB | 8 | ✔ | 公寓字典(网关地址、机器号、IP) |
| `gy_gzzd` | 17 | 16 kB | 3 | — | 工种字典 + 客货类型 |
| `gy_sjgz` | 17 | 16 kB | 10 | ✔ | 时间规则(入住/离寓/叫乘基准) |
| `gy_jcsj` | 7 | 16 kB | 3 | — | 叫乘时刻表(`cb/kssj/jssj`,均为字符) |
| `gy_jrb` | 2 | 16 kB | 6 | — | 节假日 |
| `gy_rooms` | 197 | 64 kB | 22 | ✔ | 房间基线 + 房态 + 分机 + 报警起点(三类语义混装) |
| `gy_jcxx` | 3,541 | 1.5 MB | **70** | ✔ | 叫乘执行主表(上帝表,见 3.5) |
| `gy_jcxx_his` | 32,505 | 12 MB | **70** | ✔ | 叫乘归档表 |
| `gy_bjxx` | 2,490 | 304 kB | 7 | ✔ | 报警记录(开始/结束时间) |
| `gy_bjcstj` | 0 | 8 kB | 12 | — | 补叫统计(**未使用**) |
| `gy_jdqkhz` | 1 | 16 kB | 9 | ✔ | 房间年度/月度汇总(近似废表) |
| `gy_rtjb` | 0 | 8 kB | 7 | ✔ | 日报(**未使用**) |
| `gy_call_duty_match_lock` | 38 | 8 kB | 1 | ✔ | 单列行锁表,用 `gyid` 做悲观锁 |
| `gy_syn_jcxx` | 1,139 | 384 kB | 15 | — | 运安交路快照 |
| `gy_syn_jcxx_his` | 2,291 | 376 kB | 12 | — | 交路归档 |
| `gy_syn_jcxx_status` | 1 | 8 kB | 2 | — | 同步状态/水位(单行开关) |
| `gy_ykt_ry` | 1,349 | 336 kB | 27 | — | 人员与卡主数据 |
| `gy_ykt_ry_zp` | 100 | **27 MB** | 2 | — | 照片(BLOB 存库) |
| `gy_ykt_remainder` | 1,253 | 224 kB | 12 | — | 余额(补助/现金/消费/补助总额) |
| `gy_ykt_allowance` | 34,182 | 4.2 MB | 11 | ✔ | 补助发放流水 |
| `gy_ykt_consume` | 35 | 16 kB | 19 | ✔ | 消费流水 |
| `gy_ykt_btbj` | 5 | 16 kB | 8 | ✔ | 补助标准(早/正/节假日) |
| `gy_ykt_btjs` | 5 | 16 kB | 7 | — | 补助计算规则 |
| `gy_sb` | 148 | 48 kB | 8 | ✔ | 设备台账 v1(`devicecode/lx`) |
| `gy_device` | 92 | 48 kB | 18 | ✔ | 设备台账 v2(`device_code/device_type`,RuoYi 风格) |
| `gy_sb_xt` | 246 | 64 kB | 9 | ✔ | 设备心跳与版本状态 |
| `gy_sb_zl` | **129,665** | 11 MB | 6 | — | 设备指令队列(**与历史同表**) |
| `gy_sb_zl_his` | 0 | 8 kB | 6 | — | 指令历史(**从未使用**) |
| `gy_logs` | **285,914** | **56 MB** | 7 | ✔ | 运行日志(无清理策略) |
| `gy_operation_log` | 0 | 8 kB | 6 | — | 操作审计(**从未写入**) |
| `gy_file` | 1 | 16 kB | 6 | — | 文件登记 |
表 2-2 支撑表清单
| 表 | 行数 | 列数 | 说明 |
| --- | --- | --- | --- |
| `gl_yh` / `gl_js` / `gl_qx` / `gl_cd` / `gl_yh_js` / `gl_js_qx` | 17 / 4 / 48 / 45 / 16 / 58 | 18 / 6 / 9 / 9 / 2 / 2 | 自研 RBAC(用户/角色/权限/菜单及关联) |
| `gen_table` / `gen_table_column` | 18 / 279 | 21 / 22 | **RuoYi 代码生成器元数据残留** |
容量结构:`gy_logs` 占 48%、`gy_ykt_ry_zp` 占 23%、`gy_jcxx_his` 占 10%、`gy_sb_zl` 占 9%——四张表吃掉全库 90%。其中两张(日志、指令)是纯追加、无清理的表。
## 3. 结构缺陷诊断
表 3-1 约束与索引现状(全库实测)
| 指标 | 数量 | 说明 |
| --- | --- | --- |
| 主键 | **0** | 40 张表无一有主键 |
| 唯一约束 | **0** | 唯一性完全依赖应用层判断 |
| 外键 | **0** | 关联完整性靠代码,无 DB 兜底 |
| 索引 | **0** | 含隐式主键索引在内,`pg_indexes` 计数为 0 |
| 序列 | 64 | 仅 4 个被列默认值引用,其余 60 个为**孤儿序列** |
| 表注释 | 3/40 | 仅 `gen_table`、`gen_table_column`、`gy_device` 有 |
| 列注释 | 60/524 | 其中仅 5 条属于业务表(`gy_jcxx` 3 条、`gy_syn_jcxx` 1 条、`gy_gzzd` 1 条) |
| 非空约束 | 5 处 | 仅 `gy_ykt_ry.ryid`、`gy_call_duty_match_lock.gyid`、`gy_syn_jcxx_status.id/sync_status`、`gy_device.device_code` |
### 3.1 无主键、无索引的连锁后果
1. **每次按 `devicecode` / `ryid` / `jcid` 查询都是全表扫描**。`gy_sb_zl` 12.9 万行、`gy_logs` 28.6 万行,心跳与指令链路是高频路径,随运行时间线性劣化。
2. **重复数据无法阻断**。实测 `gy_jcxx` 3,541 行中 `jcid` 只有 3,540 个不同值——已存在 1 组重复 `jcid`,且没有任何机制能阻止再次发生。
3. **更新语义不可靠**。`update ... where jcid=?` 命中多行时,MyBatis 返回的影响行数与业务预期不符,容易被误判为"成功"。
4. **分页与排序无稳定基准**。无主键导致同一秒内多行顺序不确定,翻页可能重复或漏行。
5. **归档与对账无法定位单行**。`gy_jcxx` → `gy_jcxx_his` 的搬迁只能按业务键或不带条件批量搬,出错后难以精确回滚。
### 3.2 类型体系混乱
表 3-2 字段类型分布(524 列)
| 类型 | 列数 | 占比 | 风险 |
| --- | --- | --- | --- |
| `varcharbyte`(金仓特有) | **276** | 53% | **全部未指定长度**(`character_maximum_length` 为 NULL),任何超长/脏数据都能写入 |
| `numeric` | 82 | 16% | 未指定精度标度,金额列与计数列同型,无精度约束 |
| `varchar` | 78 | 15% | 有长度,但风格与 `varcharbyte` 混用 |
| `timestamp` | 68 | 13% | 含 `Oracle DATE`(`sys.date`)映射,秒级精度 |
| `bpchar` | 11 | 2% | 定长字符(`'0'/'1'` 标记) |
| `text` / `int8` / `int4` / `bytea` | 9 | 2% | LOB(`nclob`/`blob`)、自增主键 |
**同一字段名在不同表里类型不一致,共 12 组**:
| 字段 | 类型变体 | 涉及表数 | 直接后果 |
| --- | --- | --- | --- |
| `id` | `numeric` / `varchar` / `varcharbyte` | 14 | 跨表 `join`、`union` 需隐式转换;索引即便补上也难生效 |
| `deleted` | `numeric` / `varcharbyte` | 4 | 逻辑删除写法不统一(`=0` 与 `='0'` 混用) |
| `gyid` | `varchar(64)` / `varcharbyte` | 17 | 长度与比较语义不一致,`gyid='TLXC'` 在不同表可能命中/不命中 |
| `kssj` / `jssj` | `timestamp` / `varcharbyte` | 3 / 3 | 同一含义的时间一半能比较、一半是字符串 |
| `sort` | `int4` / `numeric` | 4 | — |
| `lx` / `cb` / `name` / `dwmc` / `room_no` / `table_id` | 各 2 种 | — | — |
### 3.3 时间字段三种存法并存
| 存法 | 示例 | 问题 |
| --- | --- | --- |
| `timestamp`(含 `sys.date`) | `kcsj`、`jcsj`、`fjsj`、`sjrysj`、`scbzsj` | 正常,但秒级精度、无时区字段 |
| **字符串** `varcharbyte` | `gy_jcxx.yrysj`(预期入住时间)、`gy_jcxx.ddsj`、`gy_jcsj.kssj/jssj`、`gy_jbxx.zrsj/sjsj` | 无法做范围比较、无法排序、格式靠约定;共 13 处 |
| 冗余列并存 | `jcsj` 与 `jcsj_s`、`fjsj` 与 `fjsj_s`、`jcbjsj` 与 `jcbjsj_s` | 同一时刻两列,靠代码保证同步,无约束 |
### 3.4 Oracle 空串语义(高危)
实例参数 `ora_input_emptystr_isnull = on`,实测验证:
- `select count(*) from gy_jcxx where bz = ''` → **0**
- `select count(*) from gy_jcxx where bz is null` → **3541**(全表)
含义:业务代码写 `""` 入库后变成 `NULL`;反过来读出的 `NULL` 在 Java 侧可能是 `null` 也可能是 `""`,取决于取值方式。表现为:前端显示`undefined`、`StringUtils.isBlank` 与 `!= null` 判断在不同层得到不同结论、`where col <> ''` 恒不命中。三端拆分后跨库/跨端传输时这个语义差异会被放大。
### 3.5 `gy_jcxx` 上帝表
单表 **70 列**,同时承载 7 类语义:
| 语义组 | 代表列 |
| --- | --- |
| 车次与交路 | `jch`、`cc`、`cdw`、`kcsj`、`jlid`、`cclx`、`qd`、`ddcc`、`depotid` |
| 叫乘状态机 | `ryzt`、`jcsj`、`fjsj`、`jcbjsj`、`sjrysj`、`lybjsj`、`scbzsj` |
| 入住/离寓 | `yrysj`、`sjrysj`、`ylysj`、`sjlysj`、`flag_send`、`tbsj_gytofcd`、`tbsj_fcdtogy` |
| 补助与餐饮 | `cblx`、`cbje`、`sfffcb`、`dayallowance`、`is_meal3`、`isoverday` |
| 服务评分 | `kffwpf`、`stfwpf`、`jcfwpf`、`zm` |
| 报警与提醒去重 | `is_yaxx`、`is_jcbj`、`is_lybj`、`route_notice_key`、`call_timeout_notice_key`、`missed_call_notice_key` |
| 设备与录音 | `finish_sb`、`recordfile`、`hjxysj`、`fjhold` |
后 8 列(`business_id`、`ly_type`、`ly_event_id`、`ly_remark`、`ly_snapshot_url`、`route_notice_key`、`call_timeout_notice_key`、`missed_call_notice_key`)明显是后期追加,且 `ly_*` 属于"离寓事件"子域、`*_notice_key` 属于"通知去重"子域——表已演化为多子域的物理合并点。`gy_jcxx_his` 同步复制了这 70 列(但 `yappsj` 类型从 `sys.date` 漂移为 `timestamp`),归档表与主表已出现结构漂移。
### 3.6 设备台账双轨
表 3-4 两套设备表
| 项 | `gy_sb`(v1) | `gy_device`(v2) |
| --- | --- | --- |
| 行数 | 148 | 92 |
| 命名风格 | 全小写无下划线:`devicecode`/`lx`/`fjh` | 下划线 + RuoYi 审计列:`device_code`/`device_type`/`create_by` |
| 类型编码 | `lx`:3=134、1=4、5=3、6=3、2=2、空=2 | `device_type`:3=80、1=3、5=3、6=3、99=2、2=1 |
| 附加字段 | `gxsj`、`gymc` | `online_status`、`last_heartbeat`、`firmware_version`、`del_flag` |
| 关系 | — | `device_code` 与 `gy_sb.devicecode` **交集 92 行 = 完全子集** |
即:v2 的 92 个 `device_code` **全部能在 v1 中找到**(交集恰为 92),而 v1 仍有 148 行未被 v2 覆盖。两张表**同时在线、同时被读**,无外键、无同步机制。设备在线率、版本、故障统计存在两个数据源,"当前是否在线"取决于读哪张表。
### 3.7 指令队列与历史同表,且积压
表 3-5 `gy_sb_zl` 实测分布
| `deleted` | 行数 | 涉及设备数 | 时间范围 |
| --- | --- | --- | --- |
| `0`(未消费/未删除) | 72,088 | 96 | 2025-08-26 ~ 2026-11-07 |
| `1`(已消费) | 57,577 | 172 | 2025-08-26 ~ 2026-11-07 |
其中 **`deleted='0'` 且生成时间超过 1 天的记录有 71,723 条**——占未消费量的 99.5%。这说明:"取最新一条 → 置 `deleted=1`"的消费方式(见 `HeartBeatService`/设备服务)在多数情况下没有闭环:要么设备从未取走,要么取了没回写成功。同时该表 12.9 万行、零索引,每次取指令都是全表扫描 + 排序。
### 3.8 日志表无清理
`gy_logs` 28.6 万行 / 56 MB,`eventtime` 跨度 2025-05 ~ 2026-11。按 `lx` 分布:
| `lx` | 行数 |
| --- | --- |
| 运安叫乘同步汇总 | 238,386 |
| 刷脸离寓事件号缺失 / 请求 / 完成 / 抓拍缺失 | 7,655 / 7,655 / 7,649 / 7,649 |
| 运安叫乘同步跳过 | 4,472 |
| 运安定时抓取完成 | 3,470 |
| 自动叫乘任务未创建 | 2,369 |
| 叫乘时间延后 | 2,258 |
| 注册机心跳异常 | 1,797 |
| 超时未叫乘提醒 | 857 |
| 其余(匹配异常、登录、启动恢复等) | ~1,400 |
按月看,"运安叫乘同步汇总"稳定在 **每月约 4.4 万条**(2026-05 ~ 09 均在 40,980 ~ 44,640 之间)。这是"每 5 分钟 × 每公寓写一条汇总日志"的直接结果——**一条业务无异常的流水日志,占了全库 87% 的日志行数**。该表无归档、无分区、无索引,年增约 53 万行。
### 3.9 主数据与租户键(`gyid`)不一致
17 张表带 `gyid`,但各表的取值域互不匹配:
| 观测点 | 实测值 |
| --- | --- |
| `gy_jbxx`(公寓基础信息) | **9** 个:SYXC、SYNDC、SJTXC、SYXXC、DDXC、DSQXC、BYQXC、DLXC、DLBDC |
| `gy_call_duty_match_lock`(行锁表) | **37** 个业务 gyid + 1 个伪 gyid `__SYNC_JCXX__` |
| `gy_rooms`(房间) | **6** 个:TLXC 102、THXC 56、PJXC 35、TLNXC 2、CCXDC 1、**空 1** |
| `gy_jcxx`(叫乘) | **2** 个:THXC 2,110、TLXC 1,431 |
| `gy_sb.lx` 为空 / `gy_rooms.gyid` 为空 | 各存在 1~2 行 |
三个结论:
1. **主数据严重缺失**:系统实际服务的 gyid 至少 37 个,但只有 9 个在 `gy_jbxx` 有基础信息,31 个公寓缺面积/床位/负责人/时间规则来源。
2. **租户键可空**:`gy_rooms` 有 1 行 `gyid` 为 `NULL`(很可能是代码写入 `""` 后被 Oracle 空串语义转成 `NULL`),这类行在按租户过滤时会被全局漏掉或全局命中。
3. **行锁表用 `gyid` 当锁键**并塞入 `__SYNC_JCXX__` 这种伪租户,说明"公寓"与"同步任务"两个概念共用了一张锁表——三端拆分后必须把锁的粒度从"字符串键"改为"业务对象 + 版本"。
> 样本库只覆盖 THXC / TLXC 两个公寓的叫乘数据,属于导出时的数据裁剪(或该公寓数据被迁移过),不代表生产分布;但 `gyid` 的**取值域不一致**是结构性问题,与数据量无关。
### 3.10 RuoYi 残留与孤儿序列
- `gen_table`(18 行)、`gen_table_column`(279 行)是 RuoYi 代码生成器的元数据表,业务上完全不用,却占据 40 张表中的 2 张。
- 64 个序列中只有 4 个被列默认值引用:`gen_table_table_id_seq`、`gen_table_column_column_id_seq`、`gy_device_device_id_seq`、`seq_gy_allowance`。其余 60 个指向**已不存在的表**:`sys_user_*`、`sys_role_*`、`sys_menu_*`、`sys_dept_*`、`sys_job_*`、`t_oa_*`、`t_role_*`、`ykt_account_id_seq`、`ykt_consume_flow_id_seq`、`gygl_consumer_terminal_device_id_seq` 等。
这组序列是"库曾经跑过一版 RuoYi/其他业务系统、后来表被删但序列没删"的直接证据。对三端拆分的意义:**新公寓端选 RuoYi 并不是引入新范式,而是回到这条被中断过的路线上**,因此必须明确"沿用还是重建"权限表(现状 `gl_*` 是自研 RBAC,与 RuoYi `sys_*` 完全不同构)。
### 3.11 照片以 BLOB 入库
`gy_ykt_ry_zp`:100 行 / 27 MB / 平均 281.3 KB / 最大 547,778 字节,列类型 `blob`(`bytea`)。同时 `gy_ykt_ry.photourl` 又存了一份路径,代码里还有按 `serverOS` 硬编码的文件目录(`C:\project_doc\resouce\photo\` 与 `/home/gygl/resouce/photo/`)。**同一张照片有"库内 BLOB + 本地磁盘路径"两条真相**,且备份策略完全不同(逻辑备份含 BLOB,磁盘目录不在备份范围)。
### 3.12 审计与统计表空置
`gy_operation_log`(操作审计)0 行、`gy_bjcstj`(补叫统计)0 行、`gy_rtjb`(日报)0 行、`gy_sb_zl_his`(指令历史)0 行。四张表有结构无数据——**设计了但没有接线**。其中操作审计的缺失,与"值班员可改叫乘状态但无留痕"的风险直接相关。
## 4. 数据质量实证
表 4-1 抽样核对结果
| 检查项 | 结果 | 判读 |
| --- | --- | --- |
| `gy_jcxx` 主键候选 `jcid` | 3,541 行 / 3,540 个不同值 | 存在 1 组重复,无约束可阻断 |
| `gy_jcxx.kcsj` 为空 | 367 / 3,541(10.4%) | 开车时间缺失仍可入库,下游按时间排序/提醒会漏 |
| `gy_jcxx.ryzt` 分布 | 3=2,373、0=582、5=225、4=171、2=134、1=56 | `-1` 未出现;状态分布偏向"已完成/在寓",缺失败态样本 |
| `gy_jcxx.jllx` 分布 | 1=2,224、0=1,317 | 两值区分,但字段无注释、无字典表 |
| `gy_ykt_remainder` | 1,253 行 / 1,253 个 `ryid`,无重复 | 当前无重复,但无唯一约束保障 |
| `gy_rooms` 房态分布 | 3=68、1=58、4=32、2=32、5=7 | `room_status=6` 从未出现(字典有 6 值,实际只用 5 值) |
| `gy_sb_xt` 心跳 | 246 行,`updatestate` 全为 0 | 更新状态位从未被置位,与 3.7 的指令积压互相印证 |
| `gy_sb_zl.id` / `gy_logs.id` | 12.9 万 / 28.6 万,全部唯一 | 应用层 UUID 生成正确,纯靠代码保证 |
账务三表口径无法自洽(实测合计):
| 表 | 口径 | 合计 |
| --- | --- | --- |
| `gy_ykt_allowance` | 补助发放流水 | 552,295.00 元 |
| `gy_ykt_consume` | 消费流水 | 265.10 元 |
| `gy_ykt_remainder` | 当前余额(补助/现金/消费/补助总额) | 补助余额 1,228.00 元,现金余额 0.00 元 |
补助累计 55.2 万元的规模,与仅 265 元的消费流水、1,228 元的当前余额之间,**缺少任何可对账的中间口径**(无期初、无结转、无冲正、无对账批次)。这不能简单判定为"数据错误",但可以确定:**当前结构下无法证明账是对的**。
## 5. 表归属矩阵(对接文档 10 第 5 章)
表 5-1 40 张表的目标归属与改造动作
| 表 | 行数 | 目标端 | 改造动作 |
| --- | --- | --- | --- |
| `gy_jbxx` | 9 | 平台 | 升为公寓主数据,补全 37 个 gyid,版本化下发 |
| `gy_gyzd` | 37 | 平台 | 主数据(网关/机器号/IP),下发只读 |
| `gy_gzzd` | 17 | 平台 | 主数据(工种/客货类型) |
| `gy_sjgz` | 17 | 平台 | 时间规则收敛为平台唯一口径,下发 |
| `gy_jcsj` | 7 | 平台 | 计划数据 |
| `gy_jrb` | 2 | 平台 | 主数据 |
| `gy_rooms` | 197 | 平台 + 公寓 + 值班 | **拆三份**:房间基线(平台)/ 房态(公寓)/ 分机与报警起点(公寓) |
| `gy_jcxx` | 3,541 | 公寓 | **拆表**:叫乘任务 / 入住离寓 / 通知去重键,见 6.4 |
| `gy_jcxx_his` | 32,505 | 公寓 + 归档 | 按月分区;修正与主表的类型漂移 |
| `gy_bjxx` | 2,490 | 公寓 | 保留,补主键与 `gyid+时间` 索引 |
| `gy_bjcstj` | 0 | — | 退役(无数据无代码写入) |
| `gy_jdqkhz` | 1 | 平台 | 转报表视图或退役 |
| `gy_rtjb` | 0 | — | 退役 |
| `gy_call_duty_match_lock` | 38 | 公寓 | 换为行级锁表(含 `locked_at`/`owner`)或 DB advisory lock |
| `gy_syn_jcxx` | 1,139 | 平台 | 运安抓取上收平台,公寓只收下行计划 |
| `gy_syn_jcxx_his` | 2,291 | 平台 | 归档分区 |
| `gy_syn_jcxx_status` | 1 | 平台 | 改为 watermark 表(含 `last_success_at`/`cursor`) |
| `gy_ykt_ry` | 1,349 | 平台 | 人员/卡主数据,公寓本地缓存 |
| `gy_ykt_ry_zp` | 100 | 平台 | 迁对象存储,表只留 URL 与校验和 |
| `gy_ykt_remainder` | 1,253 | 平台主账 + 公寓缓存 | 双记:本地流水 + 平台主账,日对账 |
| `gy_ykt_allowance` | 34,182 | 平台 | 补助流水归平台,公寓不上报原始流水 |
| `gy_ykt_consume` | 35 | 平台(餐饮)/ 公寓(其他) | 按消费来源拆分归属 |
| `gy_ykt_btbj` | 5 | 平台 | 补助标准主数据 |
| `gy_ykt_btjs` | 5 | 平台 | 补助规则主数据 |
| `gy_sb` | 148 | 公寓 | **合并到 `gy_device`**,命名归一 |
| `gy_device` | 92 | 公寓 | 保留为唯一设备台账,补 `(gyid, device_code)` 唯一 |
| `gy_sb_xt` | 246 | 公寓 | 保留;心跳改 upsert,加 `last_heartbeat` 索引 |
| `gy_sb_zl` | 129,665 | 公寓 | **拆队列 / 历史**,见 6.4 |
| `gy_sb_zl_his` | 0 | 公寓 | 改分区表承接指令历史 |
| `gy_logs` | 285,914 | 公寓 | 按月分区 + 保留期;"同步汇总"降为采样 |
| `gy_operation_log` | 0 | 公寓 + 值班 | 启用,按端打标 |
| `gy_file` | 1 | 平台 | 对象存储索引 |
| `gl_*`(6 张) | 17/4/48/45/16/58 | 平台 | 统一账号与权限;公寓端沿用 RuoYi `sys_*` 或适配 `gl_*`,二者只能留一套 |
| `gen_table` / `gen_table_column` | 18 / 279 | — | 退役,目标库不携带 |
## 6. 目标结构建议(设计输入,未执行)
### 6.1 命名与边界
1. 保留 schema `gygl`,用**表名前缀**表达归属:`PLT_`(平台主数据/计划)、`APT_`(公寓执行事实)、`DUTY_`(值班操作)、`ARC_`(归档)。
2. 现有 `gy_*` 表在迁移期保留原名,新增列/新表用新前缀,避免一次性重命名带来的全量改代码。
3. 权限表二选一:若公寓端采用 RuoYi,则 `gl_*` 只作为平台端兼容视图存在,不再有第二份可写副本。
### 6.2 主键与索引基线
表 6-1 必补约束与索引(按优先级)
| 表 | 建议主键 / 唯一 | 建议索引 |
| --- | --- | --- |
| `gy_jcxx` / `gy_jcxx_his` | `jcid` | `(gyid, kcsj)`、`(gyid, ryzt)` |
| `gy_rooms` | `(gyid, room_no)` | `(gyid, room_status)` |
| `gy_ykt_ry` | `ryid` | `(card_id)`、`(gzzh)` |
| `gy_ykt_remainder` | `ryid` | `(cardid)` |
| `gy_ykt_allowance` | `tid` | `(gyid, czsj)`、`(ryid, czsj)` |
| `gy_ykt_consume` | `tid` | `(gyid, xfsj)`、`(orderid)` |
| `gy_device` | `(gyid, device_code)` | `(ip_addr)`、`(last_heartbeat)` |
| `gy_sb_xt` | `devicecode` | `(gyid, heartbeattime)` |
| `gy_sb_zl` | `id` | `(devicecode, deleted, generatedtime)` —— 取指令的主路径 |
| `gy_logs` | `id` | `(eventtime)` + 按月分区 |
| `gy_bjxx` | `id` | `(gyid, kssj)` |
| `gy_syn_jcxx` | `id` | `(kcsj)`、`(jch, cc)` |
| `gy_jbxx` / `gy_gyzd` / `gy_sjgz` | `gyid` | — |
| `gl_yh` / `gl_js` / `gl_qx` / `gl_cd` | 各自首列 | 关联表 `(user_id)/(role_id)` |
| `gy_call_duty_match_lock` | `gyid` | — |
### 6.3 类型归一规则
表 6-2 类型目标
| 现类型 | 目标 | 规则 |
| --- | --- | --- |
| `varcharbyte`(无长度) | `varchar(n)` | 按实测最大长度 + 30% 余量定 n;标识类统一 `varchar(32/64)` |
| 业务时间(`kssj`/`jssj`/`ddsj`/`yrysj`…) | `timestamp(3)` | 禁止再新增字符串时间;迁移时按 `YYYY-MM-DD HH24:MI:SS` 解析,解析失败行单独落错误表 |
| `sys.date` | `timestamp(0)` | 保持秒精度,避免与 `timestamp(6)` 混比 |
| 金额(`bzje`/`xfje`/`knye`/`xjye`/`xfze`/`bzze`) | `numeric(12,2)` | 统一两位小数,禁止 `numeric` 裸用 |
| 布尔标记(`deleted`/`sfjc`/`finish_*`/`is_*`/`stoped`) | `char(1)` + `CHECK IN ('0','1')` | 或 `boolean`;**同一字段名全库只允许一种表示** |
| `id`(`numeric` vs `varchar`) | `varchar(32)` 或 `bigint` | 每张表内自洽即可,但跨表引用必须同型 |
| `blob` 照片 | 移出库 | 表内只留 `photo_url` + `sha256` + `size` |
### 6.4 拆表与归档
1. **`gy_jcxx` 三分**:
- `APT_JC_TASK`:任务与状态机(`jcid/ryid/jch/cc/kcsj/jcsj/fjsj/jcbjsj/ryzt`)
- `APT_JC_STAY`:入住离寓(`yrysj/sjrysj/ylysj/sjlysj/ly_*`/`flag_send`)
- `APT_JC_NOTICE`:通知去重键(`route_notice_key/call_timeout_notice_key/missed_call_notice_key`)→ 或并入通用 `NOTICE_KEY` 表
- 物理上不再是 70 列宽表,写入口从"多处 update 同一行"变为"三个子域各自写自己的表",`JcglService` 的 4193 行才有可能拆开。
2. **`gy_sb_zl` 拆队列/历史**:`APT_DEV_CMD`(只保留未消费,带 `status/attempt/expire_at`)+ `ARC_DEV_CMD`(消费即搬走),队列表行数应稳定在"在线设备数 × 个位数"量级,当前 7.1 万条积压即为异常基线。
3. **`gy_logs` 分区**:按月 `RANGE` 分区,保留 3 个月热数据;"运安叫乘同步汇总"改为只记异常与抽样,预计日志量下降 90%。
4. **`gy_jcxx_his` / `gy_syn_jcxx_his` / `gy_sb_zl_his`** 统一进 `ARC_` 分区族,保留期与合规要求对齐。
### 6.5 迁移时的兼容陷阱
1. `varcharbyte` 无长度 → 转 `varchar(n)` 前必须先跑 `select max(length(col))` 全量核对,否则截断报错会中断迁移。
2. `ora_input_emptystr_isnull=on` → 迁移前后同一份数据 `''` 与 `NULL` 的判断结果不同;迁移脚本与校验 SQL 都要用 `coalesce(col,'')` 口径。
3. `gen_table_column` 里记录的 `is_pk` 全为 RuoYi 生成器推断值,**不能当作主键依据**——实测全库主键为 0,该字段不代表真实约束。
4. 60 个孤儿序列不要随库迁移,目标库从 0 重建,否则序列起始值会带来 ID 跳号与冲突排查成本。
5. Oracle 兼容模式与金仓安全版 MAC 策略需在目标环境确认(`database_mode`、`sysmac` 是否启用),二者会改变 SQL 行为与权限模型。
## 7. 与三端拆分方案的衔接
表 7-1 结构缺陷 → 方案措施
| 本次实测缺陷 | 对应章节 | 方案措施 |
| --- | --- | --- |
| 零主键零索引零外键(3.1) | 阶段零 基线修复 | 先补 `(gyid, 业务键)` 唯一约束与查询索引,再谈拆分 |
| 同名不同型 12 组(3.2) | 5 数据架构 / 6 接口契约 | 契约层强制类型规范;`gyid` 统一 `varchar(64)` |
| 空串即 NULL(3.4) | 6 接口契约 | 上报信封中 `null` 与 `""` 语义显式化,服务端统一 `coalesce` |
| `gy_jcxx` 70 列(3.5) | 4 领域与模块划分 | 拆三子域,状态机唯一写入口 |
| 双设备台账(3.6) | 7 设备接入 | 设备主数据唯一化,`gy_device` 为准,`gy_sb` 合并 |
| 指令积压 7.1 万(3.7) | 7.4 指令下发-ACK-重投 | 队列化 + ACK + 过期作废 + 优先级 |
| 日志月增 4.4 万(3.8) | 8 部署与运行 | 分区 + 保留期 + 异常化 |
| `gyid` 取值域不一致(3.9) | 3 端职责边界 | 公寓主数据上收,租户键非空 + 外键校验 |
| RuoYi 残留与孤儿序列(3.10) | 阶段一 新公寓端脚手架 | 明确权限表归属,目标库不携带 `gen_*` 与孤儿序列 |
| 照片双真相(3.11) | 5.3 数据归属 | 统一对象存储 + 校验和 |
| 审计表空置(3.12) | 10 风险 | 值班操作留痕纳入验收项 |
| 账务不可对账(4 节) | 5.2 一致性策略 | 主账在平台 + 本地流水 + 日对账 |
## 8. 待现场确认
1. **生产库是否真的没有索引**:本机导入库与 dump 均为 0,但需要在生产 DBA 侧执行 `\d+ gygl.gy_jcxx` 复核(导出工具理论上会带索引,需排除导出裁剪)。
2. **37 个 gyid 与 9 个 `gy_jbxx` 的差集**:是"未登记"还是"已停用"?决定主数据补录范围。
3. **样本库只有 2 个公寓的叫乘数据**:确认是导出裁剪、数据迁移,还是这两个公寓曾单独部署。
4. **`gy_sb_zl` 积压 7.1 万条的成因**:设备端未取、网关未转发,还是消费后回写失败?需查 2026-10 之后的设备侧日志。
5. **Oracle 存量公寓**:`ojdbc8` 依赖存在,确认是否存在仍直连 Oracle 的部署环境。
6. **金仓安全版 MAC 是否启用**:`sysmac` schema 为空表,若启用策略会影响后续新增表的授权模型。
## 附录 A 采集 SQL(可复现)
```sql
-- 库级
select version(), current_setting('server_encoding');
select name, setting from pg_settings where name in ('database_mode','ora_input_emptystr_isnull');
select nspname, count(*) from pg_class c join pg_namespace n on n.oid=c.relnamespace
where c.relkind='r' group by 1;
-- 约束与索引
select count(*) from pg_indexes where schemaname='gygl';
select constraint_type, count(*) from information_schema.table_constraints
where table_schema='gygl' group by 1;
select count(*) from pg_class c join pg_namespace n on n.oid=c.relnamespace
where c.relkind='S' and n.nspname='gygl';
-- 类型与注释
select udt_name, count(*), sum(case when character_maximum_length is null then 1 else 0 end)
from information_schema.columns where table_schema='gygl' group by 1 order by 2 desc;
select column_name, count(distinct udt_name) from information_schema.columns
where table_schema='gygl' group by 1 having count(distinct udt_name)>1;
-- 规模与分布
select relname, n_live_tup, pg_size_pretty(pg_total_relation_size(relid))
from pg_stat_user_tables order by pg_total_relation_size(relid) desc;
select count(*), count(distinct jcid), count(distinct gyid), min(kcsj), max(kcsj) from gygl.gy_jcxx;
select deleted, count(*), min(generatedtime), max(generatedtime) from gygl.gy_sb_zl group by 1;
select count(*) from gygl.gy_sb_zl where deleted='0' and generatedtime < now() - interval '1 day';
select to_char(eventtime,'YYYY-MM'), count(*) from gygl.gy_logs group by 1 order by 1;
```
> 本轮对实例的全部操作均为 `SELECT`,未执行任何 DDL/DML;分析用临时脚本仅存在于本机临时目录。
+52
View File
@@ -0,0 +1,52 @@
# 叫乘管理系统 梳理文档
本目录是对既有代码的结构化梳理,目的是为后续迭代提供统一的“地图”:知道每个业务落在哪个包/页面、数据怎么流转、接口和外部系统怎么对接、改动时应该看哪些文件。
> 后端:`yktgl/`;前端:`YktVue/`,见 [frontend/](../frontend/README.md)。
> 梳理基准:`main` 分支提交 `135a192`(2026-11-07)。
## 文档索引
| 文档 | 内容 | 适用场景 |
| --- | --- | --- |
| [01-系统总览.md](01-系统总览.md) | 业务定位、角色、运行形态、技术栈、关键设计约束 | 新人入门、方案评审 |
| [02-工程结构与分层.md](02-工程结构与分层.md) | 目录/包结构、分层约定、启动流程、公共组件 | 找代码、定落点 |
| [03-业务模块地图.md](03-业务模块地图.md) | 各业务模块职责、控制器、服务、依赖关系 | 评估改动影响面 |
| [04-核心业务流程.md](04-核心业务流程.md) | 入住、叫乘、复叫、报警、应答、离寓、房间、交路同步、卡务 | 改业务逻辑前必读 |
| [05-数据模型.md](05-数据模型.md) | 核心表、字段语义、状态码、归档与锁 | 写 SQL / 改表 / 排查数据 |
| [06-接口清单.md](06-接口清单.md) | Web 接口与设备接口清单、协议枚举 | 联调、接口扩展 |
| [07-外部集成与定时任务.md](07-外部集成与定时任务.md) | Asterisk AMI、运安 SOAP、中心 HTTP、WebSocket、Quartz/@Scheduled | 排障、集成改造 |
| [08-部署与配置.md](08-部署与配置.md) | Profile、数据库/Redis、资源目录、升级脚本 | 部署、环境切换 |
| [09-迭代指引与风险清单.md](09-迭代指引与风险清单.md) | 技术债、改动落点速查、待确认问题、迭代建议 | 排期、评审 |
| [10-架构优化方案-三端拆分.md](10-架构优化方案-三端拆分.md) | 平台端/公寓端/应用端拆分:现状评估、目标架构、职责边界、模块与数据归属、接口契约、设备接入、部署与迁移路径;含公寓端细化(RuoYi-Vue + MySQL 技术基线、应用端复用实现、语音域 Asterisk/AMI/分机/录音与配置收敛对账)及 KingbaseES→MySQL 选型与迁移 | 架构评审、重构设计 |
| [11-既有数据库结构分析.md](11-既有数据库结构分析.md) | 实测库级参数、40 表清单与规模、零主键零索引等结构缺陷、数据质量实证、表归属矩阵、类型归一与拆表建议 | 库设计评审、迁移改造 |
## 前端文档
前端梳理独立成册,入口见 [frontend/README.md](../frontend/README.md):
| 文档 | 内容 |
| --- | --- |
| [frontend/01-前端总览与技术栈.md](../frontend/01-前端总览与技术栈.md) | 四类界面、运行拓扑、技术栈、全局变量 |
| [frontend/02-工程结构与请求链路.md](../frontend/02-工程结构与请求链路.md) | 启动装配、登录守卫、axios、API 与后端映射 |
| [frontend/03-路由与页面职责.md](../frontend/03-路由与页面职责.md) | 13 条路由、入口关系、遗留页面 |
| [frontend/04-核心页面拆解-JcXx.md](../frontend/04-核心页面拆解-JcXx.md) | 值班台布局、状态、操作、时间规则、轮询 |
| [frontend/05-实时通信与语音告警.md](../frontend/05-实时通信与语音告警.md) | WebSocket/SIP、消息协议、告警与 TTS 队列 |
| [frontend/06-食堂与自助终端模块.md](../frontend/06-食堂与自助终端模块.md) | 自助登记、点餐/结算/后厨/保洁、选房与房间维护 |
| [frontend/07-构建部署与风险清单.md](../frontend/07-构建部署与风险清单.md) | 构建、环境、部署要点、测试基线、P0/P1/P2 风险 |
## 建议阅读路径
- **第一次接触项目**:01 → 02 → 03 → 04;再看前端 01 → 02 → 04。
- **要接一个需求**:03 定位模块 → 04 看流程 → 09 查落点与风险 → 06/05 查接口和表;涉及页面时读前端 04/05。
- **线上排障**:07(外部依赖/定时任务)→ 04(状态机)→ 05(数据与状态码)→ 08(配置);前端问题读前端 05/07。
- **准备重构**:02(分层)→ 09(技术债与建议)→ 10(三端拆分方案)→ 11(既有库结构分析);前端读前端 02/04/07。
## 快速事实
- 后端:Spring Boot 2.6.13 + Java 8,默认 jar 打包(可改 WAR),上下文路径 `/yktgl`,默认端口 `8081`。
- 数据库:KingbaseES 8(兼容 Oracle 语法),Redis 存 Session。
- 多公寓:同一套代码通过“客户端 IP → 公寓”解析 `gyid`,不依赖前端传参。
- 核心链路:设备/网页入寓登记 → 生成叫乘计划(Quartz 三个任务)→ 叫乘/复叫/报警/应答 → 离寓归档。
- 最大单体:后端 `jcgl/service/JcglService.java`(约 4192 行);前端 `src/views/JcXx.vue`(5294 行)。
- 前端:Vue 2.6 + Element UI,生产静态与接口同域 `/yktgl`;登录无真实 token,公寓 `gyid` 由后端按客户端 IP 解析。
@@ -0,0 +1,143 @@
# 01 前端总览与技术栈
## 1. 系统定位
前端是“叫乘 / 公寓管理”系统的操作层,服务四类现场角色,共用一个 Vue 工程、一套构建产物:
| 界面 | 主要使用者 | 典型设备 | 入口路由 |
| --- | --- | --- | --- |
| 叫乘值班控制台 | 公寓值班员 | 值班室大屏 / 双屏 PC | `/jcxx`(登录后默认页) |
| 入寓自助终端 | 乘务员本人 | 触摸屏一体机(带摄像头、读卡器) | `/ykt_main` |
| 食堂终端 | 乘务员、厨师、保洁 | 点餐屏 / 后厨屏 / 保洁屏 | `/MealsOrdering`、`/paymentPlatform`、`/chefOrders`、`/roomService` |
| 房间维护与同步查询 | 值班员、管理员 | PC | 值班台弹窗 + `/synJcXx` |
前端不承担鉴权与公寓归属判断:**公寓 `gyid` 最终以后端按客户端 IP 解析的结果为准**。前端登录时把用户名写入 `localStorage.gyid`,主要用于 WebSocket 的 `{username}` 路径段和部分页面查询参数;两者不一致是本系统最常见的联调问题来源,详见 [02](./02-工程结构与请求链路.md) 与 [07](./07-构建部署与风险清单.md)。
## 2. 运行拓扑
```mermaid
flowchart LR
subgraph Browser["浏览器(现场终端)"]
Duty["值班控制台 JcXx.vue"]
Self["自助终端 shijinpeng/main.vue"]
Meal["食堂终端 zhengchuang/*"]
RoomUI["房间选择/维护 Room.vue / RoomLook.vue"]
end
subgraph SameOrigin["同域静态资源"]
Static["/yktgl/ 前端 dist"]
end
subgraph Backend["后端 yktgl(8081,context /yktgl)"]
API["REST /jcgl /jlgl /Meals /clean /recharge /heartBeat /excel /jwDataLog"]
WS["WebSocket /jcwebsocket /jlwebsocket /rlywebsocket /MealsWebsocket"]
SIPGW["SIP 客户端网关 :9004 /sipclient"]
end
PBX["Asterisk / AMI / 广播设备"]
Duty --> Static
Self --> Static
Meal --> Static
RoomUI --> Static
Duty -->|"axios baseURL /yktgl(生产)或 /dev-api→/yktgl(开发)"| API
Self -->|"axios 同源 /yktgl"| API
Meal -->|"axios 同源 /yktgl"| API
RoomUI -->|"axios 同源 /yktgl"| API
Duty -->|"ws://host/yktgl/jcwebsocket/{gyid}"| WS
Duty -->|"ws://host/yktgl/jlwebsocket/{gyid}"| WS
Self -->|"ws://host/yktgl/rlywebsocket/test33"| WS
Meal -->|"ws://host/yktgl/MealsWebsocket/test33"| WS
Duty -->|"ws://127.0.0.1:9004/sipclient/{gyid}"| SIPGW
SIPGW --> PBX
Backend --> PBX
```
要点:
- **静态资源与接口同域**。生产 `vue.config.js` 的 `publicPath` 是 `/yktgl/`([vue.config.js:21](../../YktVue/vue.config.js#L21)),axios 生产 `baseURL` 也是 `/yktgl`([request.js:17](../../YktVue/src/utils/request.js#L17)),依赖反向代理把 `/yktgl/*` 转到后端。
- **WebSocket 地址是硬编码相对当前主机**,没有走 `public/config.js` 里的 `getWebSocketUrl()`;协议固定 `ws://`,HTTPS 部署会失败。
- **SIP 地址固定本机 `127.0.0.1:9004`**,意味着值班台必须与 SIP 网关同机部署。
- 浏览器不直连 Asterisk,语音呼叫由后端/SIP 网关完成;前端只发“呼叫/停止”指令。
## 3. 技术栈
| 层次 | 选型 | 版本 | 说明 |
| --- | --- | --- | --- |
| 框架 | Vue | 2.6.12 | Options API,无 Composition API |
| 路由 | vue-router | 3.4.9 | hash 默认模式,守卫只判断本地登录痕迹 |
| 状态 | Vuex | 3.6.0 | 依赖已声明,但工程没有 `src/store`,实际未使用 |
| UI | Element UI | 2.15.13 | 大量内联样式覆盖主题 |
| 请求 | axios | 0.24.0 | 统一拦截器 + 两套实例(`request.js` / `fileRequest.js`) |
| 图表 | ECharts | 5.4.0 | 依赖已声明,业务页面基本未使用 |
| 构建 | Vue CLI / webpack | 4.4.6 / 4 | `--openssl-legacy-provider` 兼容新 Node |
| 样式 | Sass | 1.32.13 | `lang="scss"`,含全局大屏样式 |
| 工具 | js-cookie、file-saver、jsencrypt、quill、vuedraggable、vue-cropper | 见 package.json | 多数为 RuoYi 模板遗留 |
工程来源是 RuoYi-Vue 模板(`package.json` 的 repository 仍指向 RuoYi),但业务页面已大幅改造,模板的权限、字典、布局、Vuex 体系基本被剥离或未接线。
## 4. 四类界面与页面清单
```mermaid
flowchart TB
Login["/login 登录"] --> Duty["/jcxx 值班控制台"]
Duty -->|弹窗| Room["Room.vue 选房"]
Duty -->|弹窗| RoomLook["RoomLook.vue 房间维护"]
Duty -->|弹窗| History["历史叫乘 / 交路 / 人员 / 充值"]
Login --> Self["/ykt_main 自助终端"]
Login --> MealOrder["/MealsOrdering 点餐"]
Login --> Pay["/paymentPlatform 结算"]
Login --> Chef["/chefOrders 后厨"]
Login --> Clean["/roomService 保洁"]
Login --> Syn["/synJcXx 运安同步查询"]
```
页面规模与职责:
| 文件 | 行数 | 职责 | 详细说明 |
| --- | --- | --- | --- |
| [`src/views/JcXx.vue`](../../YktVue/src/views/JcXx.vue) | 5294 | 值班主界面:叫乘列表、全部操作、实时告警、SIP、历史、导出 | [04](./04-核心页面拆解-JcXx.md) |
| [`src/shijinpeng/main.vue`](../../YktVue/src/shijinpeng/main.vue) | 943 | 自助入寓/离寓登记、刷脸、餐补查询、录像 | [06](./06-食堂与自助终端模块.md) |
| [`src/zhengchuang/views/MealsOrdering.vue`](../../YktVue/src/zhengchuang/views/MealsOrdering.vue) | 988 | 乘务员自助点餐、刷脸支付、购物车 | [06](./06-食堂与自助终端模块.md) |
| [`src/zhengchuang/views/paymentPlatform.vue`](../../YktVue/src/zhengchuang/views/paymentPlatform.vue) | 570 | 自助结算金额录入 | [06](./06-食堂与自助终端模块.md) |
| [`src/zhengchuang/views/chefOrders.vue`](../../YktVue/src/zhengchuang/views/chefOrders.vue) | 532 | 后厨订单队列、完成订单、新单提示 | [06](./06-食堂与自助终端模块.md) |
| [`src/zhengchuang/views/roomService.vue`](../../YktVue/src/zhengchuang/views/roomService.vue) | 339 | 保洁房间卡片、清扫计时 | [06](./06-食堂与自助终端模块.md) |
| [`src/components/Room.vue`](../../YktVue/src/components/Room.vue) | 332 | 房间选择器,按楼层分组、推荐房、状态过滤 | [06](./06-食堂与自助终端模块.md) |
| [`src/components/RoomLook.vue`](../../YktVue/src/components/RoomLook.vue) | 618 | 房间维护:多选同状态房间、开始/结束保洁、停用/恢复 | [06](./06-食堂与自助终端模块.md) |
| [`src/views/SynJcXx.vue`](../../YktVue/src/views/SynJcXx.vue) | 101 | 运安同步叫乘数据只读查询 | [03](./03-路由与页面职责.md) |
| [`src/views/Login.vue`](../../YktVue/src/views/Login.vue) | 206 | 公寓账号登录 | [02](./02-工程结构与请求链路.md) |
## 5. 全局变量与页面差异
`public/config.js` 在 `index.html` 中以脚本方式先于应用加载,向 `window` 注入:
| 变量 | 值 | 用途 |
| --- | --- | --- |
| `apiRoot` | `window.location.host` | 拼接 WebSocket 地址;注意不含协议 |
| `gyid` | `localStorage.getItem('gyid') \|\| 'TLXC'` | 全局公寓标识兜底值 |
不同页面对这两个变量的使用并不一致:
| 页面 | WebSocket 地址 | 公寓标识 |
| --- | --- | --- |
| `JcXx.vue` | `ws://{apiRoot}/yktgl/{jc,jl}websocket/{this.gyid}` | `this.gyid = localStorage.gyid \|\| 全局 gyid`([JcXx.vue:1728](../../YktVue/src/views/JcXx.vue#L1728)) |
| `shijinpeng/main.vue` | `ws://{apiRoot}/yktgl/rlywebsocket/test33` | 页面内硬编码 `gyid: "SYBXC"`、`gymc: "沈阳站公寓"`([main.vue:291](../../YktVue/src/shijinpeng/main.vue#L291)) |
| `MealsOrdering.vue` / `paymentPlatform.vue` | `ws://{apiRoot}/yktgl/MealsWebsocket/test33` | 无 |
| `roomService.vue` | 无 | 硬编码 `gyid: "SYBXC"`([roomService.vue:190](../../YktVue/src/zhengchuang/views/roomService.vue#L190)) |
| `public/config.js` 注释 | — | 保留 PJXC / SJTXC / BYQXC 等多公寓 IP/gyid 手工切换示例 |
结论:**这个工程原本按“一套代码、多个公寓分别部署”的方式维护**,多公寓配置靠改 `config.js` 注释或改页面常量,而不是运行时配置中心。
## 6. 关键设计约束
1. **同域部署**:静态资源 `/yktgl/`、接口 `/yktgl/*`、WebSocket `/yktgl/*websocket` 必须由同一反向代理暴露。
2. **后端按 IP 识别公寓**:`/jcgl/*` 的大量接口不接收前端 `gyid`,后端用 `zdwhService.getGyIdByIp(request)` 解析(见 [后端 03](../backend/03-业务模块地图.md))。前端传的 `gyid` 主要用于推送寻址。
3. **WebSocket username 必须等于公寓 gyid**:`JcWebsocket` 只向 `username == gyid` 或 `all` 的会话发送(见 [后端 07](../backend/07-外部集成与定时任务.md))。因此值班员登录名通常就是公寓代码。
4. **无全局状态**:所有页面状态各自维护,跨页面协作靠 `localStorage`、`BroadcastChannel` 和 `location.reload()`。
5. **单机终端假设**:SIP、自助终端、食堂终端都存在本机地址或固定 `test33`/`SYBXC` 的硬编码,跨机部署需要先改代码。
## 7. 下一步
- 想看一次请求从点击到后端:读 [02-工程结构与请求链路](./02-工程结构与请求链路.md)。
- 想改叫乘主界面:直接读 [04-核心页面拆解-JcXx](./04-核心页面拆解-JcXx.md)。
- 想改实时告警:读 [05-实时通信与语音告警](./05-实时通信与语音告警.md)。
@@ -0,0 +1,277 @@
# 02 工程结构与请求链路
## 1. 目录结构
```text
YktVue/
├── public/
│ ├── index.html # 先加载 config.js,再加载应用
│ ├── config.js # 全局 apiRoot / gyid / getWebSocketUrl
│ └── favicon.ico
├── src/
│ ├── main.js # 仅装配 ElementUI + router
│ ├── App.vue # 仅 <router-view>
│ ├── router/index.js # 13 条路由 + 登录守卫
│ ├── api/
│ │ ├── login.js # /auth/captchaImage、/auth/logon
│ │ ├── jcgl/jcgl.js # 叫乘主业务接口(35 个)
│ │ ├── jlgl/jlgl.js # 交路管理接口(7 个)
│ │ └── zhengchuang/mealsOrdering.js # 食堂接口(9 个)
│ ├── utils/
│ │ ├── request.js # axios 主实例 + 拦截器 + download
│ │ ├── fileRequest.js # 导出专用 axios 实例
│ │ ├── auth.js # Cookie token + 清理登录信息
│ │ ├── ruoyi.js # tansParams / blobValidate 等模板工具
│ │ └── permission.js # 依赖不存在的 @/store,死代码
│ ├── plugins/ # RuoYi 插件(cache/download/modal/...),未安装
│ ├── components/
│ │ ├── Room.vue # 选房器
│ │ ├── RoomLook.vue # 房间维护
│ │ └── RoomLook copy.vue, media.vue # 遗留
│ ├── views/
│ │ ├── Login.vue
│ │ ├── JcXx.vue # 值班主界面(5294 行)
│ │ ├── SynJcXx.vue
│ │ ├── Home.vue / About.vue # 模板遗留
│ │ └── JcXx.vue.bak_mojibake # 遗留备份
│ ├── shijinpeng/main.vue # 自助登记终端
│ └── zhengchuang/
│ ├── views/ # 点餐、结算、后厨、保洁
│ ├── components/ # audioPlay / textSpeak
│ └── css/ # MealCss.css 等
├── tests/ # 两个纯 Node 测试脚本
├── vue.config.js
├── package.json
└── .env.development / .env.production / .env.staging
```
## 2. 启动装配
[`src/main.js`](../../YktVue/src/main.js) 只有 14 行:
```js
Vue.use(ElementUI)
Vue.prototype.$axios = axios
new Vue({ router, render: h => h(App) }).$mount('#app')
```
因此:
- **没有安装 Vuex**,也没有 `store` 目录;`package.json` 中的 `vuex` 只是模板残留。
- **没有安装 `src/plugins/index.js`**,所以 `this.$modal`、`this.$download`、`this.$tab` 等 RuoYi 原型方法不存在;`JcXx.vue` 的 `handleDelete` 仍调用了 `this.$modal.confirm`,该分支是死路径。
- **没有全局路由守卫之外的权限体系**;`utils/permission.js` 因为 import `@/store` 会失败,且没有被引用。
- `public/index.html:9` 先加载 `config.js`,所以页面里可以直接使用 `apiRoot`、`gyid` 这两个隐式全局变量。
## 3. 登录与路由守卫
### 3.1 登录时序
```mermaid
sequenceDiagram
participant U as 用户
participant L as Login.vue
participant R as request.js
participant B as 后端 /auth/logon
participant LS as localStorage/Cookie
U->>L: 输入用户名/密码
L->>R: GET /auth/logon?username&password
R->>B: baseURL + /auth/logon
B->>B: 按用户名查 DictData_gy,校验请求 IP 是否在该公寓 ip 列表
alt 校验通过
B-->>R: Result.success(LogEntity)
R-->>L: { code:'00000', data: LogEntity }
L->>LS: setToken('fake-token-<ts>')(响应无 token)
L->>LS: gyid=username, loginAccount=username
L->>L: $router.push('/jcxx')
else 用户/IP 不匹配
B-->>R: Result.failure(msg)
R-->>L: Promise.reject + Message.error
end
```
关键代码:
- 登录请求:`api/login.js` 的 `login()` 使用 GET `/auth/logon`([login.js:16](../../YktVue/src/api/login.js#L16))。
- 成功处理:`Login.vue` 判断 `response.code === '00000'`,没有 `response.token` 就写入 `fake-token-时间戳`,并把 **用户名当成 gyid**([Login.vue:83](../../YktVue/src/views/Login.vue#L83))。
- 后端契约:`/auth/logon` 只校验 `DictData_gy` 与 IP,写入 Session 的 `gyid`/`ip`,返回 `LogEntity`,不签发 token(见 [后端 06](../06-接口清单.md))。
### 3.2 守卫逻辑
[`src/router/index.js:80`](../../YktVue/src/router/index.js#L80):
```js
const isLoggedIn = token || loginAccount
if (to.path === '/login') {
isLoggedIn ? next('/jcxx') : next()
} else {
isLoggedIn ? next() : next('/login')
}
```
含义与风险:
- 登录态 = Cookie `Admin-Token` 或 `localStorage.loginAccount` 任一存在,**不校验 token 有效性**,也不校验后端 Session 是否过期。
- 退出登录只清本地信息(`clearLoginInfo()`),没有调用后端 `/auth/logout`。
- 只要用户手工写入 `loginAccount`,就能进入所有页面;真正的权限控制完全依赖后端接口和 IP 校验。
- 登录页在已登录时会强制跳 `/jcxx`,无法回到登录页切换账号,除非先清 localStorage/Cookie。
## 4. axios 请求链路
### 4.1 主实例
[`src/utils/request.js:14`](../../YktVue/src/utils/request.js#L14) 创建实例:
| 配置 | 值 | 说明 |
| --- | --- | --- |
| `baseURL` | 开发 `/dev-api`,其他 `/yktgl` | 硬编码,**不读取 `VUE_APP_BASE_API`**([request.js:17](../../YktVue/src/utils/request.js#L17)) |
| `timeout` | 10000 ms | 导出走 `fileRequest` 的 50 s |
| `Content-Type` | `application/json;charset=utf-8` | GET 参数由拦截器拼到 URL |
请求拦截器([request.js:24](../../YktVue/src/utils/request.js#L24)):
1. 若存在 Cookie token 且未显式 `isToken: false`,加 `Authorization: Bearer <token>`;由于 token 是假的,后端并不校验。
2. GET 请求用 `tansParams` 把 `params` 序列化到 URL,然后清空 `params`。
3. POST/PUT 的防重复提交逻辑只写不校验:`sessionObj` 写入后,真正比较的 `else` 分支被整段注释([request.js:54](../../YktVue/src/utils/request.js#L54))。
响应拦截器([request.js:75](../../YktVue/src/utils/request.js#L75)):
```js
const { code, msg } = response.data
if (code === '00000' || code === '11111' || code === 1) return response.data
if (response.data instanceof ArrayBuffer) return response
Message({ message: msg || '系统出错', type: 'error' })
return Promise.reject(new Error(msg || 'Error'))
```
注意:
- **`11111` 被当成成功返回**。业务层需要自己判断 `res.code === '11111'` 并提示错误(例如 `handleRechargeConfirm`、`handleUpdateJcXxConfirm`)。这与后端“`00000` 成功、`11111` 业务失败”的约定不一致,容易漏判。
- 二进制判断在成功码判断之后,且 axios 默认不会把 blob 转成 `ArrayBuffer`,所以 blob 分支实际很少命中;导出另走 `fileRequest`。
- 网络错误分支整体被块注释([request.js:94](../../YktVue/src/utils/request.js#L94)),**请求失败时不会弹全局错误提示**,多数页面 `.catch()` 为空或只 `console.log`,表现为“点了没反应”。
### 4.2 导出实例
[`src/utils/fileRequest.js:6`](../../YktVue/src/utils/fileRequest.js#L6) 固定 `baseURL: '/yktgl'`、`timeout: 50000`。开发环境没有 `/yktgl` 代理(只有 `/dev-api`),**本地导出会 404**;生产同域部署才可用。
### 4.3 环境变量现状
| 文件 | `VUE_APP_BASE_API` | 是否生效 |
| --- | --- | --- |
| `.env.development` | `/yktgl` | 否,`request.js` 写死 `/dev-api` |
| `.env.production` | `/prod-api` | 否,写死 `/yktgl` |
| `.env.staging` | `/stage-api` | 否,`build:stage` 也走写死值 |
改环境时不要只改 `.env`,必须同步改 `request.js`(以及需要时的 `fileRequest.js` 和 `vue.config.js` 代理)。
## 5. API 封装与后端映射
### 5.1 模块划分
| 模块 | 文件 | 接口数 | 用途 |
| --- | --- | --- | --- |
| 登录 | `src/api/login.js` | 2 | 验证码(未使用)、登录 |
| 叫乘主业务 | `src/api/jcgl/jcgl.js` | 35 | 列表、叫乘、房间、人员、导出、同步、时间规则 |
| 交路 | `src/api/jlgl/jlgl.js` | 7 | 交路增删改查、导入、确认 |
| 食堂 | `src/api/zhengchuang/mealsOrdering.js` | 9 | 菜品、订单、房间、消费、餐次 |
### 5.2 叫乘主业务接口映射
`src/api/jcgl/jcgl.js` 的函数与后端控制器对应关系(后端细节见 [06-接口清单](../backend/06-接口清单.md)):
| 前端函数 | 方法 | URL | 典型调用方 |
| --- | --- | --- | --- |
| `getJcXxList` | POST | `/jcgl/getCcJcXx` | 值班主列表、轮询 |
| `getJcXxList_Ls` | POST | `/jcgl/getCcJcXx_Ls` | 历史叫乘(分页/滚动加载) |
| `modifyJcXx` | POST | `/jcgl/modifyJcXx` | 修改叫乘信息 |
| `cancelTrainAssignment` | POST | `/jcgl/cancelTrainAssignment` | 取消叫乘 |
| `handleCheckOut` | POST | `/jcgl/handleCheckOut` | 出乘 |
| `handleStopCalling` | POST | `/jcgl/handleStopCalling` | 停止报警/暂停报警 |
| `handleTempCalling` | POST | `/jcgl/handleTempCalling` | 语音临叫(广播式) |
| `handleRoomCall` | POST | `/jcgl/handleRoomCall`(method=1 语音,=2 喊话) | 单房间临叫 |
| `handleRoomCallEnd` | POST | `/jcgl/handleRoomCallEnd` / `...End2` | 停止单房间呼叫 |
| `handleMultiRoomCall` | POST | `/jcgl/handleMultiRoomCall` | 多房间喊话 |
| `handleMultiRoomCallEnd` | POST | `/jcgl/handleMultiRoomCallEnd` | 停止多房间喊话 |
| `getRoomList` | GET | `/jcgl/getRoomList` | 选房、房间维护、终端 |
| `getRoomRecommendations` | GET | `/jcgl/getRoomRecommendations` | 选房推荐 |
| `modifyRoomStatus` | GET | `/jcgl/modifyRoomStatus` | 停用/恢复房间 |
| `startRoomCleaning` | POST | `/clean/startCleaning` | 开始保洁(`deviceCode` 固定 `"123123"`) |
| `endRoomCleaning` | POST | `/clean/endCleaning` | 结束保洁 |
| `getRyXxByRyId` | POST | `/jcgl/getRyxxByRyId` | 人员查询/维护 |
| `modifyRyXx` | POST | `/jcgl/modifyRyXx` | 人员维护保存 |
| `deleteRyXx` | POST | `/jcgl/deleteRyXx` | 删除人员 |
| `getWslRyXx` | POST | `/jcgl/getWslRyXx` | 未刷脸人员 |
| `getRyFjXx` | POST | `/jcgl/getRyFjXx` | 换房前查询同房间人员 |
| `exchangeRoom` | POST | `/jcgl/exchangeRoom` | 确认换房 |
| `getJlxxList` | GET | `/jcgl/getJlxxList` | 交路列表 |
| `getJlXxJcsjList` | POST | `/jlgl/getJlXxJcsjList` | 按开车日期取车次 |
| `addJlXx` / `modifyJlXx` / `delJlXx` / `deleteJlXx` / `confirmJlXx` | POST | `/jlgl/*` | 交路维护 |
| `getZbyList` / `getGzList` | GET | `/jcgl/getZbyList`、`/jcgl/getGzList` | 值班员/工种字典 |
| `getSjgz` | GET | `/jcgl/getSjgz` | 时间规则(150/120/30 分钟) |
| `isHoliday` | GET | `/jcgl/isHoliday` | 食堂节假日判断 |
| `getSynJcXx` | GET | `/jcgl/getSynJcXx` | 运安同步数据查询 |
| `getSynJcXxIsOffline` | POST | `/jwDataLog/getSynJcXxIsOffline` | 运安离线检测 |
| `getLatestSyncTime` | GET | `/jcgl/getLatestSyncTime` | 最近同步时间 |
| `getDeviceStatusInfo` | POST | `/heartBeat/getDeviceStatusInfo` | 设备心跳/在线状态 |
| `getRecordingConfig` | GET | `/jcgl/getRecordingConfig` | 录音回放配置 |
| `handleRecharge` | GET | `/recharge/handleRecharge` | 现金充值办理 |
| `exportJcXxExcell` | GET(blob) | `/excel/exportJcXxExcell` | 定时/手工导出 |
| `getBtje_info` / `getBtje_infoMeal3` / `updateMeal3` | GET/POST | `/jcgl/getBtje_info*`、`/jcgl/updateMeal3` | 餐补余额 |
| `submitAll` | GET | `/jcgl/submitAll` | 自助终端/值班台提交入寓登记 |
| `getJcxxByRyid` | POST | `/jcgl/getJcxxByRyid` | 按人员查叫乘 |
| `jcAlarm` | POST | `/jcgl/jcAlarm` | 设备报警 |
| `login` | GET | `/auth/login` | **未被页面使用**,登录走 `api/login.js` |
### 5.3 食堂接口映射
| 前端函数 | 方法 | URL |
| --- | --- | --- |
| `getMeals` | GET | `/Meals/getMeals` |
| `createOrder` | POST | `/Meals/createOrder` |
| `addOrders_MX` | POST | `/Meals/addOrders_MX` |
| `OrdersSearch` | GET | `/Meals/OrdersSearch` |
| `OrderMX_search` | GET | `/Meals/OrderMX_search` |
| `updateStatus` | POST | `/Meals/updateStatus` |
| `getRooms` | GET | `/Meals/getRooms` |
| `consume` | POST | `/Meals/consume` |
| `getCsLb` | GET | `/Meals/getCsLb` |
## 6. 响应契约与业务判断
后端统一返回 `Result`:
```json
{ "code": "00000", "msg": "...", "data": ..., "count": 123 }
```
前端使用约定:
- `code === '00000'`:成功,`data` 为业务数据,`count` 为分页总数。
- `code === '11111'`:业务失败;拦截器仍返回该对象,调用方必须自己处理。
- 其他:拦截器弹 `Message.error` 并 reject。
- 导出接口返回 blob,不经过统一响应判断。
## 7. 工程遗留与陷阱
| 现象 | 位置 | 影响 |
| --- | --- | --- |
| `JcXx.vue` 导入不存在的 `auditJlXx` | [JcXx.vue:1392](../../YktVue/src/views/JcXx.vue#L1392) | 构建告警;若将来使用会运行时报错 |
| `shijinpeng/main.vue` 导入不存在的 `getRyxx` | [main.vue:280](../../YktVue/src/shijinpeng/main.vue#L280) | 刷脸登记调用时 `TypeError: getRyxx is not a function` |
| `utils/permission.js` 依赖不存在的 `@/store` | `src/utils/permission.js` | 死代码,若被引入立即失败 |
| `App.vue` 结尾多一个字面量 `3` | [App.vue:32](../../YktVue/src/App.vue#L32) | 无效语句,构建噪音 |
| `Home.vue` 使用未注册的 `<test1>` | [Home.vue:6](../../YktVue/src/views/Home.vue#L6) | 访问 `/home` 时渲染告警 |
| `README.md` 命令写成 `npm run serve/build` | `YktVue/README.md` | 与 `package.json` 的 `dev/build:prod` 不符 |
| 防重复提交只写不判 | [request.js:51](../../YktVue/src/utils/request.js#L51) | 双击可能重复提交 |
| 请求失败无全局提示 | [request.js:94](../../YktVue/src/utils/request.js#L94) | 用户无感知,排障困难 |
| `build:stage` 没有对应接口前缀 | `.env.staging` | 构建产物仍请求 `/yktgl` |
## 8. 修改落点速查
| 需求 | 先看 |
| --- | --- |
| 新增一个后端接口 | `src/api/jcgl/jcgl.js` 增加封装 → 页面调用 |
| 改登录/切换账号 | `Login.vue`、`utils/auth.js`、`router/index.js` |
| 改请求超时/前缀/错误提示 | `utils/request.js`、`fileRequest.js`、`vue.config.js` |
| 改开发代理指向 | `vue.config.js` 的 `devServer.proxy` |
| 新增页面 | `router/index.js` + `src/views/` |
| 清理模板遗留 | `plugins/`、`utils/permission.js`、`views/Home.vue`、`About.vue`、`RoomLook copy.vue`、`JcXx.vue.bak_mojibake` |
+143
View File
@@ -0,0 +1,143 @@
# 03 路由与页面职责
## 1. 路由总表
路由定义在 [`src/router/index.js`](../../YktVue/src/router/index.js),全部为静态路由,无动态权限路由、无嵌套路由。
| # | 路径 | 名称 | 组件 | 状态 | 职责 |
| --- | --- | --- | --- | --- | --- |
| 1 | `/` | — | 重定向 `/login` | 可用 | 默认入口 |
| 2 | `/login` | `Login` | `views/Login.vue` | 可用 | 公寓账号 + 密码登录 |
| 3 | `/home` | `Home` | `views/Home.vue` | **模板遗留** | 展示 HelloWorld,含未注册组件 |
| 4 | `/MealsOrdering` | `MealsOrdering` | `zhengchuang/views/MealsOrdering.vue` | 可用(有缺陷) | 乘务员自助点餐 |
| 5 | `/paymentPlatform` | `paymentPlatform` | `zhengchuang/views/paymentPlatform.vue` | **半可用** | 自助结算金额录入,刷脸链路被注释 |
| 6 | `/chefOrders` | `chefOrders` | `zhengchuang/views/chefOrders.vue` | 可用 | 后厨订单队列 |
| 7 | `/roomService` | `roomService` | `zhengchuang/views/roomService.vue` | **半可用** | 保洁房间卡片与计时,完成不回写后端 |
| 8 | `/jcxx` | `JcXx` | `views/JcXx.vue` | 可用 | 值班控制台(主业务) |
| 9 | `/JcXx` | — | 重定向 `/jcxx` | 可用 | 大小写兼容 |
| 10 | `/about` | `About` | `views/About.vue` | 模板遗留 | 空壳页 |
| 11 | `/ykt_main` | `ykt_main` | `shijinpeng/main.vue` | 可用(有缺陷) | 自助入寓/离寓登记终端 |
| 12 | `/room` | `room` | `components/Room.vue` | 独立可访问 | 选房组件;正常由值班台弹窗内嵌 |
| 13 | `/synJcXx` | `synJcXx` | `views/SynJcXx.vue` | 可用 | 运安同步叫乘数据查询 |
## 2. 页面分组
```mermaid
flowchart TB
subgraph DutySide["值班侧"]
J["/jcxx 主控制台"]
S["/synJcXx 同步查询"]
end
subgraph CrewSide["乘务员自助"]
Y["/ykt_main 入寓登记"]
M["/MealsOrdering 点餐"]
P["/paymentPlatform 结算"]
end
subgraph CanteenSide["食堂/保洁"]
C["/chefOrders 后厨"]
R["/roomService 保洁"]
end
subgraph LegacySide["模板遗留"]
H["/home"]
A["/about"]
end
```
四组页面之间**没有前端共享状态**,协作方式只有三种:
1. 后端轮询 / WebSocket 推送;
2. `localStorage`(`gyid`、`loginAccount`、`Admin-Token`);
3. `BroadcastChannel`(同浏览器多标签页,见 [06](./06-食堂与自助终端模块.md))。
## 3. 页面职责与关键行为
### 3.1 `/login` 登录页
- 表单只有用户名、密码,没有验证码;`getCodeImg()` 虽已封装但未使用。
- 成功后写假 token、`gyid`、`loginAccount`,跳 `/jcxx`。
- 已登录用户访问 `/login` 会被守卫重定向回 `/jcxx`,无法直接切换账号。
- 详见 [02-登录与路由守卫](./02-工程结构与请求链路.md#3-登录与路由守卫)。
### 3.2 `/jcxx` 值班控制台
系统的核心页面,承担:
- 叫乘任务列表(机务/客运、其他人员两个 Tab,但 Tab 仅切换选中态,不改变查询);
- 入寓登记、修改叫乘、取消叫乘、出乘、换房、暂停报警、临叫、停止呼叫;
- 交路信息查看/确认/维护;
- 人员维护、现金充值、历史叫乘、设备状态;
- 实时告警音、运安离线报警、同步提醒弹窗与 TTS;
- 定时刷新列表、定时导出 Excel。
完整拆解见 [04-核心页面拆解-JcXx](./04-核心页面拆解-JcXx.md)。
### 3.3 `/ykt_main` 自助登记终端
- 无登录态输入,靠 `rlywebsocket/test33` 接收“有人靠近/刷脸”事件后弹出登记窗口。
- 页面内固定 `gyid: "SYBXC"`、`gymc: "沈阳站公寓"`,部署到其他公寓必须改代码。
- 依赖的 `getRyxx` 在 `api/jcgl/jcgl.js` 中不存在(实际函数名是 `getRyXxByRyId`),刷脸确认时会抛异常;即使修复,入寓分支也只广播、不调用 `/jcgl/submitAll`。
- 详见 [06](./06-食堂与自助终端模块.md)。
### 3.4 食堂四页
| 页面 | 正常业务闭环 | 当前状态 |
| --- | --- | --- |
| 点餐 `MealsOrdering` | 选餐 → 确认 → 刷脸 → `consume` 扣款 → `createOrder` + 明细 → 广播后厨 | 刷卡回调存在;餐次判断与定时器有缺陷 |
| 结算 `paymentPlatform` | 录入金额 → 刷脸 → 扣款 → 生成订单 | `initWebSocket()` 被注释,刷脸回调不会触发 |
| 后厨 `chefOrders` | 拉取待处理订单 → 查看明细 → 完成订单;收到 `2603` 广播刷新并响铃 | 可用 |
| 保洁 `roomService` | 房间卡片 → 开始保洁计时 → 完成 | “完成”只弹提示,不调用后端 |
详见 [06](./06-食堂与自助终端模块.md)。
### 3.5 `/synJcXx` 同步查询
- 只读列表:车次、机车号、开车时间、计划出勤地点、司机姓名、接车方式。
- 输入即查询(`@input`),带分页 `[5,10,15,20,30,40,50]`。
- 数据来源 `/jcgl/getSynJcXx`,是运安计划同步结果的查询视图,不参与叫乘状态流转。
### 3.6 `/room` 与房间维护
- `/room` 可直接访问 `Room.vue`,但正常使用方式是值班台弹窗:`JcXx.vue` 通过 `<Room>` 内嵌,选房后 `$emit('childByValue', room)` 回传。
- `RoomLook.vue` 没有独立路由,只作为值班台“房间信息”弹窗使用。
- 两者都属于“房间状态”操作面,详见 [06](./06-食堂与自助终端模块.md)。
## 4. 入口关系
```mermaid
flowchart LR
Login["/login"] -->|成功| Jcxx["/jcxx"]
Jcxx -->|房间信息| RoomLook["RoomLook 弹窗"]
Jcxx -->|入寓登记选房| Room["Room 弹窗"]
Jcxx -->|历史数据| History["历史叫乘弹窗"]
Jcxx -->|交路管理| Jl["交路弹窗"]
Jcxx -->|人员维护| Ry["人员维护弹窗"]
Jcxx -->|退出| Login
Login -.手工访问.-> Terminal["/ykt_main · /MealsOrdering · /paymentPlatform · /chefOrders · /roomService · /synJcXx"]
```
除 `/jcxx` 外,其余业务页面**没有导航菜单**,需要现场终端在浏览器书签/快捷方式中直接配置 URL。这是“终端型系统”的常见做法,但也意味着改路由会直接影响现场快捷方式。
## 5. 路由层面的注意事项
| 问题 | 影响 | 位置 |
| --- | --- | --- |
| 守卫只判断本地痕迹 | 可绕过;后端 Session 过期后页面仍可进入,接口才报错 | [router/index.js:85](../../YktVue/src/router/index.js#L85) |
| 没有 404 路由 | 未知路径会渲染空白 | `router/index.js` |
| 没有路由级 code-splitting 之外的懒加载策略 | 首屏仍会加载 Element UI 全量 | `main.js` |
| `/home`、`/about` 仍在路由表 | 可被访问,产生无意义页面/告警 | `router/index.js:19`、`:53` |
| 终端页面无登录页跳转保护 | 未登录访问会被送到 `/login`,终端无法自动恢复 | 守卫全局生效 |
## 6. 遗留文件清单
以下文件不被路由或组件引用,属于历史备份/模板残留,重构时可直接评估删除:
| 文件 | 说明 |
| --- | --- |
| `src/views/JcXx.vue.bak_mojibake` | 主页面乱码备份 |
| `src/components/RoomLook copy.vue` | 房间维护旧版 |
| `src/components/media.vue` | 媒体示例组件 |
| `src/components/HelloWorld.vue` | Vue CLI 模板组件(仅 `/home` 使用) |
| `src/views/Home.vue`、`src/views/About.vue` | Vue CLI 模板页面 |
| `src/plugins/*` | RuoYi 插件,未安装 |
| `src/utils/permission.js` | 依赖不存在的 `@/store` |
| `src/utils/jsencrypt.js`、`src/utils/validate.js` | 模板工具,当前业务未使用 |
+267
View File
@@ -0,0 +1,267 @@
# 04 核心页面拆解:JcXx 值班控制台
`src/views/JcXx.vue` 5294 行,模板 1–1389,脚本 1390–4532,样式 4533 之后。它是整个系统的操作中枢,下面按“布局 → 状态 → 列表 → 操作 → 时间规则 → 轮询 → 历史 → 陷阱”拆解。
## 1. 页面布局
```text
┌─────────────────────────────────────────────────────────────────────┐
│ 跑马灯(当前告警/呼叫提示) │ 大时钟 │ 房间信息 设备信息 人员维护 │
│ 机车号 车次 房间号 姓名 [查询][重置] │ │ 交路管理 历史数据 退出 │
│ │ │ 语音临叫 喊话临叫 停止呼叫 停止报警 │
├─────────────────────────────────────────────────────────────────────┤
│ Tab:机务人员和客运人员 / 其他人员 │
│ 叫乘列表(状态、到寓时间、单位、机车号、姓名、房间号、车次、开车/叫乘/ │
│ 复叫/应离寓时间、接车方式、暂停报警) │
├─────────────────────────────────────────────────────────────────────┤
│ 底部:在寓汇总 / 值班信息 │
└─────────────────────────────────────────────────────────────────────┘
弹窗:历史叫乘、离寓抓拍、设备信息、未刷脸、换房、交路、待确认交路、
新增/修改交路、修改叫乘、入寓登记、人员维护、现金充值、交接班、
房间选择、房间维护、多房间喊话、出乘确认、广播确认、临叫确认、
停止呼叫确认、运安离线告警、交路及叫乘提醒
```
| 区域 | 行号 | 说明 |
| --- | --- | --- |
| 顶部卡片 | 4–176 | 左:跑马灯 + 4 个查询框;中:每秒刷新的大时钟;右:功能按钮 |
| 列表卡片 | 178–241 | 两个 Tab(仅视觉)、主表格 |
| 主表格 | 183–240 | `height=600`、`row-style=setRowStyle`、单选高亮 |
| 底部栏 | 1178–1191 | 在寓人数汇总、值班信息 |
| 业务弹窗 | 244–1378 | 见第 7 节 |
主表格列:序号、状态、到寓时间、单位、机车号、姓名、房间号、出发车次、开车时间、叫乘时间、复叫时间、叫乘日期、应离寓时间、接车方式、操作(暂停报警)。
## 2. 页面状态
`data()` 从 1406 行开始,核心字段:
| 字段 | 行号 | 用途 |
| --- | --- | --- |
| `gyid` | 1453 | 当前公寓;`created()` 中取 `localStorage.gyid` 或全局 `gyid` |
| `apiRoot` | 1459 | WebSocket 主机名 |
| `jcxxList` | 1542 | 主列表数据 |
| `selectedRow` | 1470 | 当前选中行,所有行操作的入参 |
| `JcXxQueryParams` | 1546 | 查询条件:`gyid/jch/cc/fjh/rymc/begindt/enddt` |
| `LsJcXxQueryParams` | 1555 | 历史查询:增加 `historyTimeType/pageNum/pageSize` |
| `sjgzConfig` | 1631 | 时间规则 `{kjjcsjgz,zjjcsjgz,lysjgz}` |
| `dismissedAlarmEvents` | 1430 | 已处理告警事件 key 集合,防止重复响铃 |
| `alarmVoiceActive` | 1428 | 报警音是否正在播放 |
| `syncSpeechQueue` / `syncSpeechCurrent` | 1627 起 | TTS 播报队列 |
| `activeTempCalls` | 1644 | 本页面发起的临叫房间及类型(voice/speech) |
| `callListRequestVersion` | 1429 | 列表请求版本号,防止旧响应覆盖新数据 |
`created()`([JcXx.vue:1726](../../YktVue/src/views/JcXx.vue#L1726))设置 `gyid` 与查询参数,加载交路列表与当前时间;`mounted()`([:1784](../../YktVue/src/views/JcXx.vue#L1784))启动 WebSocket、SIP、字典、列表、时间规则和 4 个定时器。
## 3. 列表与状态
### 3.1 状态码映射
表格“状态”列([JcXx.vue:194](../../YktVue/src/views/JcXx.vue#L194)):
| `ryzt` | 显示 | 含义 | 行颜色 |
| --- | --- | --- | --- |
| `0` | 待叫乘 | 尚未到叫乘时间 | 黄色 `#FFF200` |
| `1` | 正在呼叫 | 正在叫乘/通话建立中 | 绿色 `#00FF00` |
| `2` | 叫乘报警 | 叫乘未应答 | 红色 `#F56C6C` |
| `3` | 待离寓 | 已叫乘完成,等待离寓 | 浅绿 `#B3E19D` |
| `4` | 离寓报警 | 超时应离寓 | 红色 `#F56C6C` |
| `5` | 正在通话 | 语音通道已建立 | 绿色 `#00FF00` |
| 其他 | 已入寓 | 正常在寓 | 默认 |
### 3.2 行样式与跑马灯
`setRowStyle(row)`([JcXx.vue:3723](../../YktVue/src/views/JcXx.vue#L3723))按以下优先级返回样式,并**在渲染函数里直接修改 `this.message`**:
1. `ryzt=0` 且 `missedCallTaskWarning=1`:黄色 + 闪烁动画,提示“叫乘时间已过”;
2. `ryzt=1/5`:绿色;
3. `ryzt=2/4`:红色;
4. 本页发起的临叫:绿色,提示“正在喊话/语音临叫”;
5. `jllx=1`:浅蓝 `#d9ecff`;
6. `ryzt=3`:浅绿;
7. `ryzt=0`:黄色;
8. 其他:默认。
问题:`setRowStyle` 是纯函数位,却在渲染期间写 `this.message`,会触发额外的渲染与响应式更新;表格行数多时可能造成卡顿或“跑马灯内容错行”。修改行样式逻辑时建议把消息计算移出渲染函数。
### 3.3 接车方式显示
`cclx` 为“站接”显示站接;“库接/库出”统一显示“库出”;其他为空。
## 4. 操作矩阵
所有行级操作都要求先选中行(`selectedRow`),点击行时还会向 SIP 发送一次注册状态([JcXx.vue:3231](../../YktVue/src/views/JcXx.vue#L3231))。
| 操作 | 入口 | 处理方法 | 后端接口 | 说明 |
| --- | --- | --- | --- | --- |
| 查询/重置 | 顶部 | `getJcXxList` / `resetForm` | `/jcgl/getCcJcXx` | 输入即查询;重置只清 4 个文本条件 |
| 取消叫乘 | 顶部 | `openCancelTrainAssignment` | `/jcgl/cancelTrainAssignment` | 二次确认;成功后停报警、刷新列表 |
| 修改叫乘 | 行/弹窗 | `handleModifyJcXx` → `handleUpdateJcXxConfirm` | `/jcgl/modifyJcXx` | 编辑表单与表格行分离,避免后端拒绝时已改表格 |
| 出乘 | 弹窗 | `handleCheckOut` → `handleCheckOutConfirm` | `/jcgl/handleCheckOut` | 确认后不可修改 |
| 暂停报警 | 行内按钮 | `handleStopCalling` | `/jcgl/handleStopCalling` | 仅 `ryzt=2/4` 有效;成功后把事件 key 加入已忽略集合 |
| 停止报警 | 顶部 | `stopCurrentAlarm` | 同上 | 先校验选中行必须是 `2/4` |
| 语音临叫 | 顶部 | `handleCalling` → `handleRoomCallConfirm` | SIP + `/jcgl/handleRoomCall` | 需 SIP 已连接;`callmethod=1` |
| 喊话临叫(单房间) | 顶部 | `handleCallingOld` → `handleRoomCallConfirm` | SIP + `/jcgl/handleRoomCall2` | 选中行时 `callmethod=2` |
| 喊话临叫(多房间) | 顶部 | `confirmMultiCall` | `/jcgl/handleMultiRoomCall` | 未选中行时弹出多选房间;记录 `lastCalledRooms` |
| 停止呼叫 | 顶部 | `handleRoomCallEnd` | `/jcgl/handleMultiRoomCallEnd` 或 `/jcgl/handleRoomCallEnd(2)` | 优先停止多房间,否则停止当前行 |
| 换房间 | 弹窗 | `handleChangeRoom` → `exchangeRoomConfirm` | `/jcgl/getRyFjXx`、`/jcgl/exchangeRoom` | 先列出同房间人员,再逐人设置新房间 |
| 入寓登记 | 弹窗 | `finalCheck` | `/jcgl/submitAll` | 校验房间/开车日期后提交;成功后关弹窗刷新 |
| 人员维护 | 顶部 | `handleUpdateRyXx` | `/jcgl/getRyxxByRyId`、`/jcgl/modifyRyXx`、`/jcgl/deleteRyXx` | 支持刷脸照片上传/替换 |
| 现金充值 | WebSocket 触发 | `handleRechargeConfirm` | `/recharge/handleRecharge` | 收到 `cz` 消息弹窗;`cz_cancel` 可按 id 关闭 |
| 交路管理 | 顶部 | `handleJlGlClick` | `/jcgl/getJlxxList` | 打开交路列表,可新增/修改/删除/导入 |
| 待确认交路 | 交路弹窗 | `handdleDqrButton` → `handleConfirmJlXxClick` | `/jlgl/confirmJlXx` | 按勾选 id 批量确认 |
| 历史数据 | 顶部 | `handleLsSjClick` → `loadLsJcXx` | `/jcgl/getCcJcXx_Ls` | 分页 + 滚动加载;可回放录音 |
| 设备信息 | 顶部 | `handleDeviceStatus` | `/heartBeat/getDeviceStatusInfo` | 离线设备整行标红 |
| 导出 Excel | 定时 | `exportJcXxExcell` | `/excel/exportJcXxExcell` | 每 10 分钟自动下载一次 |
| 退出 | 顶部 | `handleLogout` | — | 关 WS、清定时器、清本地登录信息 |
### 4.1 临叫的 SIP 前置动作
`handleRoomCallConfirm`([JcXx.vue:4263](../../YktVue/src/views/JcXx.vue#L4263))在调用后端前先向 SIP 发送:
```json
{
"reqAct": {
"type": "call_make",
"actId": "actid_12121212",
"params": ["sip:1001@10.32.100.166"]
}
}
```
- 目标 SIP 地址、`actId` 都是硬编码;
- `语音临叫` 按钮在 `sipclient.connected == 0` 时禁用,但喊话临叫没有同样保护;
- SIP 发送失败不会阻断后端调用,前后端状态可能不一致。
## 5. 时间规则与校验
### 5.1 规则来源
`getSjgzConfig()`([JcXx.vue:3782](../../YktVue/src/views/JcXx.vue#L3782))调用 `/jcgl/getSjgz`,取第一条:
| 字段 | 默认 | 含义 |
| --- | --- | --- |
| `kjjcsjgz` | 150 | 库出/库接提前叫乘分钟数 |
| `zjjcsjgz` | 120 | 站接提前叫乘分钟数 |
| `lysjgz` | 30 | 叫乘后到应离寓的分钟数 |
接口失败时页面回退到同一组默认值并提示。
### 5.2 计算关系
`test()`(修改叫乘,[JcXx.vue:3901](../../YktVue/src/views/JcXx.vue#L3901))与 `calculateJlxxTime()`(交路新增,[:3998](../../YktVue/src/views/JcXx.vue#L3998)):
```text
叫乘时间 jcsj = 开车时间 kcsj − 规则分钟(站接 zjjcsjgz,库出/库接/未选 kjjcsjgz)
复叫时间 fjsj = jcsj + 10 分钟
报警时间 jcbjsj = fjsj + 5 分钟
应离寓 ylysj = jcsj + lysjgz(默认 30 分钟)
```
注意:交路新增表单里的字段叫 `lysj`,修改叫乘表单里叫 `ylysj`,两者复用同一套计算,但字段名不同。
### 5.3 合法性校验
`validateCallDutyTimeline`([JcXx.vue:3852](../../YktVue/src/views/JcXx.vue#L3852))要求:
1. 五个时间全部存在且格式正确(`yyyy-MM-dd HH:mm[:ss]`);
2. 严格满足 `kcsj > ylysj > jcbjsj > fjsj > jcsj`;
3. `kcsj − jcsj < 24 小时`。
`handleJcsjChange`([:3870](../../YktVue/src/views/JcXx.vue#L3870))在手工修改叫乘时间时重算后三个时间,并要求当前开车时间仍然合法。
### 5.4 修改叫乘的提交
`handleUpdateJcXxConfirm`([JcXx.vue:3335](../../YktVue/src/views/JcXx.vue#L3335))先规范化 5 个时间字段、再校验时间线,最后提交;`00000` 关闭弹窗刷新列表,`11111` 只弹错误不关闭。
## 6. 轮询与自动行为
`mounted()` 注册的定时器:
| 定时器 | 周期 | 动作 | 副作用 |
| --- | --- | --- | --- |
| `latestSyncTimeTimer` | 30 s | `loadLatestSyncTime` → `/jcgl/getLatestSyncTime` | 更新底部“最近同步时间” |
| `minuteTimer` | 10 min | `getJcXxList` | 刷新列表并触发告警音/TTS 调度 |
| `fiveMinuteTimer` | 10 min | `exportJcXxExcell` | **自动下载 Excel 到浏览器** |
| `threeMinuteTimer` | 10 min | `getSynJcXxIsOffline` | 运安离线时弹窗并播放离线报警音 |
| `currentDateTimer` | 1 s | 更新 `currentDate` | 大时钟 |
`getJcXxList`([JcXx.vue:2262](../../YktVue/src/views/JcXx.vue#L2262))用 `callListRequestVersion` 防止旧响应覆盖:
```js
const requestVersion = ++this.callListRequestVersion
getJcXxList(params).then(res => {
if (requestVersion !== this.callListRequestVersion) return
this.jcxxList = res.data
this.syncAlarmVoice(this.jcxxList)
...
})
```
`handleStopCalling` 成功后主动 `++callListRequestVersion`,使所有在途响应失效,避免“刚停掉的报警又被旧数据拉响”。
退出/离开页面时 `destroyed`([JcXx.vue:1740](../../YktVue/src/views/JcXx.vue#L1740))清理全部定时器、队列和语音;但 `jlwebsocketclose()` / `jcwebsocketclose()` 被直接调用,而这两个方法内部会执行 `location.reload()`,因此**路由离开本页会触发整页刷新**(详见 05、07)。
## 7. 弹窗清单
| 弹窗 | 行号 | 绑定 | 说明 |
| --- | --- | --- | --- |
| 历史叫乘信息 | 244–413 | `visible_jcxx_ls` | 查询、分页、录音回放、离寓抓拍 |
| 刷脸离寓抓拍 | 415–426 | `visible_leaveSnapshot` | 图片预览 |
| 设备信息 | 428–484 | `visible_sbxx` | 离线设备标红 |
| 未刷脸人员 | 486–496 | `visible_wslRy` | 入口按钮当前被注释 |
| 换房间 | 498–518 | `visible_hfj` | 逐人选择新房间 |
| 交路信息 | 521–584 | `visible_jlxx` | 列表、增删改、导入、确认 |
| 待确认交路信息 | 595–642 | `visible_jlqrxx` | 批量确认 |
| 新增/修改交路 | 644–779 | `visible_jlxx_add` | 时间自动计算 |
| 修改叫乘信息 | 781–898 | `visible_jcxx` | 5 个时间 + 车次选择 |
| 入寓登记信息 | 900–988 | `visible_rydj` | 刷脸照片、房间选择 |
| 人员维护 | 990–1118 | `visible_ryxxwh` | 查询、编辑、照片 |
| 现金充值 | 1120–1176 | `visible_xjcz` | 金额 0–100,WebSocket 触发 |
| 交接班 | 1192–1205 | `visible_login` | 登录表单(当前值班台不使用) |
| 房间选择 | 1207–1210 | `visible_room` | 内嵌 `Room.vue` |
| 房间维护 | 1212–1215 | `visible_roomLook` | 内嵌 `RoomLook.vue` |
| 多房间喊话 | 1222–1258 | `dialogMultiCallVisible` | 房间多选 |
| 出乘确认 | 1260–1282 | `dialog1Visible` | 不可撤销提示 |
| 广播确认 | 1284–1301 | `dialog2Visible` | 所有公共区域呼叫 |
| 临叫确认 | 1303–1317 | `dialogCallVisible` | 语音/喊话文案动态 |
| 停止呼叫确认 | 1319–1333 | `dialogCallEndVisible` | — |
| 运安离线报警 | 1335–1344 | `visible_synJcXxIsOffline` | 同步异常提示 |
| 交路及叫乘提醒 | 1347–1378 | `syncDialogVisible` | 分组提醒、10 秒自动关闭、TTS |
## 8. 历史数据与录音
- `handleLsSjClick`([JcXx.vue:4312](../../YktVue/src/views/JcXx.vue#L4312))打开弹窗后 `getLsJcXx`。
- `loadLsJcXx(reset)`([:4326](../../YktVue/src/views/JcXx.vue#L4326))每次取 100 条,`lsJcxxList.length < count` 时允许继续加载;滚动到底部触发下一页。
- 历史查询支持按“入寓时间/离寓时间”切换,日期输入兼容 `8`、`815`、`0815`、`081530` 等简写(2300 行附近)。
- 录音:`getRecordingConfig()` 获取基地址,`splitRecordFiles` / `getRecordAudioUrl` / `playRecordVoice` 处理 `recordfile` 逗号分隔的多段录音。
- 离寓抓拍:`leaveSnapshotPreviewUrl` 显示照片,加载失败时显示占位。
## 9. 代码级陷阱
| 陷阱 | 位置 | 影响 |
| --- | --- | --- |
| `setRowStyle` 内改 `this.message` | :3723 | 渲染期副作用,列表大时卡顿/提示错乱 |
| `formatTime` 定义两次 | :2276、:3153 | 后者覆盖前者,易误用 |
| `destroyed` 调用 `*websocketclose()` | :1743–1744 | 方法内 `location.reload()`,离开页面即刷新 |
| WebSocket `onclose` 无条件 `location.reload()` | :2086、:2198 | 后端重启/网络抖动会导致页面反复刷新 |
| 自动导出每 10 分钟 | :1792 | 值班机持续产生下载文件,可能占满磁盘 |
| 临叫 SIP 目标硬编码 | :4272 | 换 SIP 账号/网段必须改代码 |
| 语音临叫依赖 SIP 连接,喊话不依赖 | :153、:157 | 两条路径的前置条件不一致 |
| `handleUpdateJcXxConfirm` 只处理 `00000/11111` | :3353 | 其他错误码无提示 |
| `handleTabClick` 空实现 | :3779 | 两个 Tab 不改变数据,仅是视觉分组 |
| `handleDelete` 使用未安装的 `$modal` | :3034 | 该删除入口不可用 |
## 10. 修改落点速查
| 需求 | 修改位置 |
| --- | --- |
| 增加列表列/状态 | 模板 183–240;状态映射 :194 |
| 改行颜色/闪烁规则 | `setRowStyle` :3723 |
| 增加行操作按钮 | 模板 234–239 + 新方法 |
| 改查询条件 | `JcXxQueryParams` :1546、顶部模板 21–46 |
| 改时间计算 | `test` :3901、`calculateJlxxTime` :3998、`buildFollowUpTimes` :3808 |
| 改临叫/停止逻辑 | `handleCalling*`、`handleRoomCall*` :4194–4310 |
| 改告警/TTS | `syncAlarmVoice` :2742、`enqueueSyncNotice` :2765、`playNextSyncSpeech` :2917 |
| 改轮询周期 | `mounted` :1790–1793 |
| 改历史查询 | `loadLsJcXx` :4326、历史弹窗 244–413 |
@@ -0,0 +1,284 @@
# 05 实时通信与语音告警
前端有 5 条长连接:4 条业务 WebSocket + 1 条本机 SIP WebSocket。它们都不走 axios,地址在代码里硬编码,连接生命周期与页面绑定。
## 1. 连接清单
| 连接 | 地址 | 页面 | 用途 | 断线行为 |
| --- | --- | --- | --- | --- |
| 叫乘推送 | `ws://{apiRoot}/yktgl/jcwebsocket/{gyid}` | `JcXx.vue` | 叫乘状态、入寓人员、充值、同步提醒 | `location.reload()` |
| 交路推送 | `ws://{apiRoot}/yktgl/jlwebsocket/{gyid}` | `JcXx.vue` | 交路更新提醒 | `location.reload()` |
| 人员/入住推送 | `ws://{apiRoot}/yktgl/rlywebsocket/test33` | `shijinpeng/main.vue` | 触发自助登记 | `location.reload()` |
| 食堂推送 | `ws://{apiRoot}/yktgl/MealsWebsocket/test33` | `MealsOrdering.vue`、`paymentPlatform.vue` | 刷脸/刷卡回调 | `location.reload()` |
| SIP 客户端 | `ws://127.0.0.1:9004/sipclient/{gyid}` | `JcXx.vue` | 注册、呼叫指令 | 仅日志,不重连 |
说明:
- `apiRoot = window.location.host`,由 `public/config.js` 注入;地址里再拼 `/yktgl` 上下文。
- **协议固定 `ws://`**,没有使用 `config.js` 的 `getWebSocketUrl()`(该函数支持 wss 但无人调用)。
- 业务 WebSocket 没有心跳、没有指数退避重连,关闭即整页刷新。
- `shijinpeng` 与食堂页面把 username 固定为 `test33`,多终端同时连接时后端广播会给所有 `test33` 会话,无法区分终端。
- 后端路由规则见 [后端 07](../07-外部集成与定时任务.md#4-websocket):`jcwebsocket` 只投递给 `username == gyid` 或 `all` 的会话;`jlwebsocket`、`rlywebsocket`、`MealsWebsocket` 为广播。
## 2. 叫乘推送协议(jcwebsocket)
消息体是 JSON 字符串,至少包含 `lx`(类型);`JcXx.vue:2097` 的 `switch` 分发:
| `lx` | 附带的字段 | 前端行为 | 当前状态 |
| --- | --- | --- | --- |
| `ry` | `content` | 打开“入寓登记信息”弹窗并填充 | 启用 |
| `cz` | `content`、`id`、`deviceCode` | 打开“现金充值”弹窗,记录申请 id | 启用 |
| `cz_cancel` | `id` | 仅当 id 与当前弹窗一致时关闭,并提示“乘务员已取消” | 启用 |
| `zcxx` | `photoBase64`、`deviceCode`、`zcid` | 打开“注册人员信息”,进入新增人员流程 | 启用 |
| `xgxx` | `content`、`newPhotoBase64` | 打开“修改人员信息”,带原照片/新照片 | 启用 |
| `refresh` | — | 重新拉取叫乘列表 | 启用 |
| `autoCallFailed` | `content`、`roomNo` | 入同步提醒队列(标题“超时未叫乘提醒”)+ 刷新列表 | 启用 |
| `missedCallWarning` | `content`、`roomNo` | 入同步提醒队列 | 启用 |
| `routeChangeWarning` | `content`、`routeStatus` | 原为交路变更提醒 | **已注释禁用** |
| `routeExpiredWarning` | `content`、`roomNo` | 原为运安交路超时提醒 | **已注释禁用** |
| `callTimeResetWarning` | `content`、`roomNo` | 原为叫乘时间重置提醒 | **已注释禁用** |
| 其他 | — | 忽略 | — |
后端文档 [06-接口清单](../06-接口清单.md#9-websocket-消息) 目前只列了前 5 类,`cz_cancel`、`autoCallFailed`、`missedCallWarning` 是前端实际已在处理、文档需补齐的消息。
### 2.1 人员登记链路
```mermaid
sequenceDiagram
participant D as 自助终端/设备
participant B as 后端
participant W as JcXx 值班台
D->>B: 刷脸/读卡事件
B->>W: {"lx":"zcxx", photoBase64, zcid, deviceCode}
W->>W: 打开注册人员弹窗,设置 ryxx_code='0'
W->>B: GET /jcgl/getRyxxByRyId?ryid=...
B-->>W: 人员信息(可能不存在)
W->>B: POST /jcgl/modifyRyXx?code=0
B-->>W: 保存结果
```
`xgxx` 走同一弹窗但 `ryxx_code='1'`,提交时 `code=1`。
## 3. 交路推送(jlwebsocket)
`jlwebsocketonmessage`([JcXx.vue:2081](../../YktVue/src/views/JcXx.vue#L2081))不解析消息内容,只做两件事:
```js
this.visible_jlxxtx = true
this.playVoice() // 播放 orderCompleteTip.mp3
```
页面右下角出现“交路信息已更新”的浮动提示(`bottom-right-div`),点击后打开交路弹窗。消息内容、变更类型完全依赖用户再查询。
## 4. 告警链路
### 4.1 报警音状态机
相关字段:
- `alarmVoiceActive`:是否正在播放报警音;
- `dismissedAlarmEvents`:已人工处理的事件 key 集合;
- `audioPlayerAlarm`:`<audio loop>`,`loop` 绑定 `alarmVoiceActive`。
`syncAlarmVoice(rows)`([JcXx.vue:2742](../../YktVue/src/views/JcXx.vue#L2742))每次列表刷新后执行:
```mermaid
flowchart TD
A["收到 jcxxList"] --> B{"存在 ryzt=2/4 的行?"}
B -- 否 --> C{"alarmVoiceActive?"}
C -- 是 --> D["stopAlarmVoice()"]
C -- 否 --> E["保持静音"]
B -- 是 --> F{"该行有 alarmEventKey?"}
F -- 是 --> G{"任一 key 未被 dismissed?"}
F -- 否 --> H{"stoped != '1'?"}
G -- 是 --> I["开始播放报警音"]
H -- 是 --> I
G -- 否 --> E
H -- 否 --> E
```
事件 key 来自后端字段 `alarmEventKey`,可能是逗号分隔的多个 key(`getAlarmEventKeys`)。人工停止报警成功后:
1. 调用 `/jcgl/handleStopCalling`;
2. 把该行所有 key 写入 `dismissedAlarmEvents`;
3. `++callListRequestVersion` 使在途列表响应失效;
4. 立即 `syncAlarmVoice(this.jcxxList)` 停音;
5. 再刷新列表。
这样即使后端状态尚未落库、旧列表又返回,也不会重新响铃。对应测试见 `tests/alarm-state.test.cjs`。
### 4.2 停止报警的入口
| 入口 | 方法 | 条件 |
| --- | --- | --- |
| 顶部“停止报警” | `stopCurrentAlarm` | 选中行必须是 `ryzt=2/4` |
| 行内“暂停报警” | `handleStopCalling(row)` | 同上 |
非报警状态点击“暂停报警”只提示“当前状态停止报警无效”。
## 5. 同步提醒:弹窗队列 + TTS 队列
`autoCallFailed`、`missedCallWarning` 等消息不直接弹窗,而是走两级队列。
### 5.1 入队与合并
`enqueueSyncNotice(title, content)`([JcXx.vue:2765](../../YktVue/src/views/JcXx.vue#L2765)):
1. 以 `title|content` 为 key 去重;
2. 已人工关闭过的 key(`syncDismissedNoticeKeys`)直接丢弃;
3. 800 ms 内的多条提醒合并到一次 `flushSyncNotices()`。
`flushSyncNotices`([:2782](../../YktVue/src/views/JcXx.vue#L2782)):
- 按 `title` 分组,生成 `sections: [{title, contents[]}]`;
- 每个分组进入 TTS 队列;
- 若当前已有提醒弹窗,则把新 section 合并进当前弹窗并重置 10 秒关闭计时;
- 否则入 `syncDialogQueue`,由 `showNextSyncDialog` 依次展示;
- 每次刷新都会调用 `getJcXxList()`。
### 5.2 弹窗行为
- 标题固定“交路及叫乘提醒”,展示所有 section;
- 10 秒无操作自动关闭(`startSyncDialogCloseTimer`);
- 用户手动关闭时,`handleSyncDialogBeforeClose` 把当前所有 section 的 key 写入 `syncDismissedNoticeKeys`;
- `@closed` 后展示队列中的下一条;
- 关闭后同一内容不会再打扰,直到页面刷新(集合只在 `destroyed` 清空)。
### 5.3 TTS 播报
`enqueueSyncSpeech` 把同一 title 下的 contents 用“;”拼成一段文本。播放调度 `playNextSyncSpeech`([JcXx.vue:2917](../../YktVue/src/views/JcXx.vue#L2917)):
- 使用浏览器 `SpeechSynthesisUtterance`,`lang=zh-CN`、`rate=0.9`;
- 单条播完 500 ms 后播下一条;
- 若 `isBusinessVoiceBusy()` 为真,则 2 秒后重试;
- 业务语音(叫乘/通话/临叫/页面音频/其他 TTS)优先:`getJcXxList` 发现忙时调用 `interruptSyncSpeechForBusinessVoice()`,把当前播报退回队首并 `cancel()`,等业务语音结束再播。
`isBusinessVoiceBusy`([:2896](../../YktVue/src/views/JcXx.vue#L2896))判定任意一条成立即忙:
- 列表存在 `ryzt=1/5`;
- `activeTempCalls` 非空(本页发起的临叫);
- 4 个 `<audio>` 中任意一个正在播放;
- 浏览器正在播其他 TTS 且不是同步提醒自己。
### 5.4 队列字段总览
| 字段 | 作用 |
| --- | --- |
| `syncNoticeQueue` / `syncNoticeTimer` | 800 ms 合并窗口 |
| `syncDialogQueue` / `syncCurrentDialog` / `syncDialogVisible` | 弹窗队列 |
| `syncDialogActive` / `syncDialogCloseTimer` | 弹窗互斥与 10 秒自动关闭 |
| `syncDismissedNoticeKeys` | 已关闭提醒去重 |
| `syncSpeechQueue` / `syncSpeechCurrent` / `syncSpeechRetryTimer` | TTS 队列与重试 |
## 6. 运安离线报警
`getSynJcXxIsOffline` 每 10 分钟调用 `/jwDataLog/getSynJcXxIsOffline`([JcXx.vue:4522](../../YktVue/src/views/JcXx.vue#L4522)):
```js
if (res.data == '1') {
this.visible_synJcXxIsOffline = true
this.playOfflineAlarmVoice()
}
```
- 离线弹窗标题“报警”,内容“运安计划数据同步异常,请联系技术人员处理!”;
- 离线报警音复用 `alarm.mp3`,但不循环、不参与 `alarmVoiceActive` 状态机;
- 只提示一次,不做持续告警,也不会自动关闭(需人工点关闭);
- 未做去重,10 分钟一次会重复播放,直到后端恢复。
## 7. SIP 语音通道
`initClientSocket`([JcXx.vue:2019](../../YktVue/src/views/JcXx.vue#L2019))连接 `ws://127.0.0.1:9004/sipclient/{gyid}`,连接成功后设置 `sipclient.connected = 1`。
前端只发送两类报文:
### 7.1 选中行时注册状态
```json
{
"regSta": {
"acc_no": 1,
"reg_act": 0,
"sta": 2,
"code": 200,
"expire": 180
}
}
```
在 `handleRowClick` 中发送([JcXx.vue:3241](../../YktVue/src/views/JcXx.vue#L3241)),目的疑似让 SIP 客户端切换/刷新注册账号。
### 7.2 发起呼叫
```json
{
"reqAct": {
"type": "call_make",
"actId": "actid_12121212",
"params": ["sip:1001@10.32.100.166"]
}
}
```
在 `handleRoomCallConfirm` 中发送([:4268](../../YktVue/src/views/JcXx.vue#L4268)),随后才调用后端 `/jcgl/handleRoomCall(2)`。
问题:
- 目标 `sip:1001@10.32.100.166` 与 `actId` 都是硬编码,换环境必须改代码;
- SIP 发送与后端调用没有事务关系,任一方失败都会造成“界面显示已呼叫/实际未呼叫”或相反;
- 停止呼叫只调后端,没有对应的 SIP `call_end` 报文;
- `sipclient.connected` 只在首次 `onopen` 设置,断线后不清零,按钮仍可点。
## 8. 食堂与自助终端的推送
| 页面 | 消息处理 | 结果 |
| --- | --- | --- |
| `MealsOrdering.vue` | `messageObj.code === '5' && icTag === 1` | 调用 `/Meals/consume` 扣款,成功后 `handleSuccess` 建单;失败提示“卡内余额不足” |
| `paymentPlatform.vue` | 同样的回调 | `initWebSocket()` 在 `mounted` 被注释,回调不会触发 |
| `shijinpeng/main.vue` | 任意消息 | 重置人员信息、显示“入寓登记,请刷脸!”、开始录像 |
| `chefOrders.vue` | 不连 WebSocket | 监听 `BroadcastChannel("myChannel")` 的 `"2603"` 消息刷新订单并响铃 |
详见 [06-食堂与自助终端模块](./06-食堂与自助终端模块.md)。
## 9. 连接生命周期与风险
```mermaid
sequenceDiagram
participant P as JcXx.vue
participant WS as 业务 WebSocket
P->>WS: mounted 建立连接
WS-->>P: onopen(仅日志)
WS-->>P: onmessage(业务消息)
alt 网络抖动/后端重启
WS-->>P: onclose
P->>P: location.reload()
P->>WS: 重新加载页面后重连
end
alt 退出登录/路由离开
P->>WS: close()
WS-->>P: onclose
P->>P: location.reload()(与方法内清理竞争)
end
```
| 风险 | 说明 |
| --- | --- |
| 刷新风暴 | 后端重启时所有值班台同时 `reload`,重连后若仍失败会继续刷新 |
| 无重连退避 | 没有 `setTimeout` 重连、没有最大重试次数 |
| 退出竞争 | `handleLogout` 先 `close()` 再清登录信息,`onclose` 的 `reload` 可能在不同时序触发 |
| 路由离开刷新 | `destroyed` 直接调用 `jlwebsocketclose()`/`jcwebsocketclose()`,方法内 `reload()` |
| wss 不支持 | HTTPS 部署时 `ws://` 被浏览器拦截 |
| 固定 username | 多终端共用 `test33`,消息无法定向 |
| 无消息校验 | `JSON.parse(e.data)` 无 try/catch,非法消息会中断回调 |
## 10. 修改落点
| 需求 | 位置 |
| --- | --- |
| 新增叫乘消息类型 | `jcwebsocketonmessage` :2097 |
| 改告警判定 | `syncAlarmVoice` :2742、`getAlarmEventKeys` :2739 |
| 改提醒弹窗样式/时长 | 模板 1347–1378、`startSyncDialogCloseTimer` :2859 |
| 改 TTS 语速/优先级 | `playNextSyncSpeech` :2917、`isBusinessVoiceBusy` :2896 |
| 改 WebSocket 地址/重连 | `initWebSocket` :2005、`initClientSocket` :2019 |
| 改 SIP 报文 | `handleRowClick` :3241、`handleRoomCallConfirm` :4263 |
| 改离线报警 | `getSynJcXxIsOffline` :4522 |
@@ -0,0 +1,221 @@
# 06 食堂与自助终端模块
本模块包含 6 个“终端型”界面:自助登记、点餐、结算、后厨、保洁,以及值班台复用的选房/房间维护组件。它们共同的特点是:**无导航、固定 URL、部分公寓/终端标识硬编码、状态靠整页刷新或广播同步**。
## 1. 自助登记终端 `/ykt_main`
文件:[`src/shijinpeng/main.vue`](../../YktVue/src/shijinpeng/main.vue)(943 行)。
### 1.1 定位与配置
- 面向乘务员本人,部署在公寓大厅触摸屏一体机。
- 页面内固定:
```js
gyid: "SYBXC"
gymc: "沈阳站公寓"
```
([main.vue:291](../../YktVue/src/shijinpeng/main.vue#L291))
- 后端查询房间时也使用该 `gyid`,换公寓必须改代码或改成运行时可配。
- 连接 `ws://{apiRoot}/yktgl/rlywebsocket/test33`([main.vue:379](../../YktVue/src/shijinpeng/main.vue#L379)),任何消息都会触发“入寓登记,请刷脸!”。
### 1.2 业务模式
| 模式 | 入口方法 | `ryzt` | 流程 |
| --- | --- | --- | --- |
| 入寓登记 | `rydj()` | `1` | 弹窗 → 刷脸 → `getRyxx(ryid)` 取人员 → `getBtje_info` 取餐补 → `finalCheck` **只广播 `channel_rly_jc` 并整页刷新,不调用后端** |
| 出乘离寓 | `ccly()` | `3` | 刷脸 → `getJcxxByRyid(ryid)` 取叫乘记录 → 合并房间信息 → `finalCheck` 调 `/jcgl/submitAll` → 成功广播并整页刷新 |
| 就餐补助 | `jcbz()` | `5` | 刷脸 → `getBtje_infoMeal3` → `updateMeal3` 领取第三餐补 |
入寓登记的关键代码在 `rydj_submit()`([main.vue:426](../../YktVue/src/shijinpeng/main.vue#L426)),但这行:
```js
import { ..., getRyxx, ... } from "@/api/jcgl/jcgl"
```
`jcgl.js` 并没有导出 `getRyxx`,实际函数名是 `getRyXxByRyId`。**刷脸确认时会抛 `TypeError: getRyxx is not a function`,入寓登记链路目前是断的。**
### 1.3 录像与刷脸
- 使用 `navigator.mediaDevices.getDisplayMedia` 录屏(`invokeGetDisplayMedia` / `captureScreen` / `startRecording`,[main.vue:751](../../YktVue/src/shijinpeng/main.vue#L751) 起)。
- 一体机需要允许屏幕捕获权限;普通浏览器会弹权限框,不适合无人值守。
- 录像在提交后未显式上传,`stopRecording` 的结果处理需结合后端设备协议确认。
### 1.4 房间选择
- 页面自行拉取 `getRoomList`,按 `row_sort` 分组,状态统计使用 `zd_roomStatus_groupName`。
- 可选房间规则与 `Room.vue` 类似:状态 `1/3/5/6`。
- 选择后 `select_room(item)` 把房间字段合并进 `jcxx`,`finalCheck` 时一并提交。
### 1.5 跨页广播
`finalCheck` 通过 `BroadcastChannel("channel_rly_jc")` 发送 JSON 字符串([main.vue:676](../../YktVue/src/shijinpeng/main.vue#L676)、[:685](../../YktVue/src/shijinpeng/main.vue#L685))。**仓库内没有任何监听 `channel_rly_jc` 的代码**,这条广播目前是空写;如果要让值班台实时感知,需要在 `JcXx.vue` 增加监听或改用 WebSocket。
更关键的是 `finalCheck` 的分支([main.vue:675](../../YktVue/src/shijinpeng/main.vue#L675)):
```js
if (this.jcxx.ryzt == '1') { // 入寓登记
channelObj.postMessage(...)
this.$router.go(0) // 没有 submitAll
}
if (this.jcxx.ryzt == '3') { // 出乘离寓
submitAll(this.jcxx)...
}
```
也就是说,**自助终端的入寓登记即使修好 `getRyxx`,也不会把登记写回后端**,只广播给一个不存在的监听者后刷新页面。这是比导入错误更根本的断链。
## 2. 点餐终端 `/MealsOrdering`
文件:[`src/zhengchuang/views/MealsOrdering.vue`](../../YktVue/src/zhengchuang/views/MealsOrdering.vue)(988 行)。
### 2.1 点餐流程
```mermaid
sequenceDiagram
participant U as 乘务员
participant P as MealsOrdering
participant B as 后端 /Meals
participant K as 后厨 chefOrders
U->>P: 选择主食/副食/小菜,加入购物车
U->>P: 点击确定
P->>P: 显示等待刷脸,30 秒倒计时
B-->>P: WS 推送 {code:'5', icName}
P->>B: POST /Meals/consume?ryid&xfje
alt 扣款成功
P->>B: POST /Meals/createOrder
loop 每个购物车项
P->>B: POST /Meals/addOrders_MX
end
opt 偏餐 mcb=3
P->>K: BroadcastChannel("myChannel").postMessage("2603")
end
P->>P: 显示成功,3 秒后关闭弹窗
else 余额不足
P->>U: MessageBox 提示“卡内余额不足”
end
```
关键点:
- 人脸/刷卡结果来自 `ws://{apiRoot}/yktgl/MealsWebsocket/test33`([MealsOrdering.vue:392](../../YktVue/src/zhengchuang/views/MealsOrdering.vue#L392))。
- **先扣款、后建单**,且建单与明细没有事务:`consume` 成功但 `createOrder` 失败时,钱已扣、订单不存在;`addOrders_MX` 失败被空 `catch` 吞掉,会出现订单少明细。
- 成功后 3 秒自动关闭弹窗(`timeCount3`),等待刷脸 30 秒超时则 `window.location.reload()`。
### 2.2 已知缺陷
| 缺陷 | 位置 | 影响 |
| --- | --- | --- |
| `isMealsClass()` 使用未定义的 `this.hours` | :262、:269 | 条件永远为 false,餐次永远是“偏餐”,`mcb` 恒为 3 |
| `setInterval(this.getMealsInfo, ...)`,但方法不存在 | :257 | 30 分钟后定时器报错;菜单不会自动刷新 |
| `queryParams` 中 `mcb` 重复声明 | :229、:235 | 后者覆盖前者,当前值 `null` |
| `consume()` 方法内调用未定义的 `updateMoney()` | :436 | 若走该路径会报错(实际扣款走 API 的 `consume`) |
| `destroyed` 调用 `websocketclose()` | :450 | 方法内 `location.reload()`,离开页面即刷新 |
| WebSocket 断开即刷新 | :440 附近 | 网络抖动导致点餐页反复重载 |
## 3. 结算终端 `/paymentPlatform`
文件:[`src/zhengchuang/views/paymentPlatform.vue`](../../YktVue/src/zhengchuang/views/paymentPlatform.vue)(570 行)。
- 复用点餐的菜品/订单 API,页面职责是“录入金额 + 刷脸结算”。
- `mounted()` 中 `this.initWebSocket()` 被注释([paymentPlatform.vue:212](../../YktVue/src/zhengchuang/views/paymentPlatform.vue#L212)),**刷脸回调不会触发**;`initWebSocket` 方法本身仍在(:376)。
- `isHoliday()`(:224)在节假日把金额锁定:早餐 10 元、正餐 20 元,否则允许手工输入。
- `handleConfirm` → 30 秒倒计时等待刷脸 → `handleSuccess` → `createOrder` + 明细。
- 该页只导入了 `getMeals/createOrder/addOrders_MX/getCsLb`,**完全没有调用 `/Meals/consume`**;即使恢复 WebSocket,也只是建单、不扣款。
- 结论:**当前结算页只能录入金额和生成订单,不能完成真实扣款闭环**,上线前必须确认是否启用。
## 4. 后厨终端 `/chefOrders`
文件:[`src/zhengchuang/views/chefOrders.vue`](../../YktVue/src/zhengchuang/views/chefOrders.vue)(532 行)。
- 拉取待处理订单:`OrdersSearch`(:341),查看明细:`OrderMX_search`(:349)。
- 完成订单:`updateStatus(order_id)`(:359),成功后 1 秒关闭弹窗并刷新。
- 新单提示:监听 `BroadcastChannel("myChannel")`,收到 `"2603"` 时刷新并播放 `newOrderTip.mp3`(:394–:406)。
- 缺陷:`beforeDestroy` 里新建了另一个 `BroadcastChannel` 实例再 `removeEventListener`(:412–:413),**监听器不会被移除**;在单页应用内多次进入会重复响应。
## 5. 保洁终端 `/roomService`
文件:[`src/zhengchuang/views/roomService.vue`](../../YktVue/src/zhengchuang/views/roomService.vue)(339 行)。
- 固定 `gyid: "SYBXC"`、`room_floor: "1"`([roomService.vue:190](../../YktVue/src/zhengchuang/views/roomService.vue#L190))。
- `northRoomSearch` / `southRoomSearch` 分别按 `room_face` 拉取南北两侧房间,生成房间卡片。
- 点击房间 → `roomClick` → `cleanStart` 打开弹窗并启动秒表。
- `cleanComplete()`(:260)**只弹“已完成清扫”并重置计时,不调用 `/clean/endCleaning`**,房间状态不会变化。
- 弹窗里的布草/备品/报修区域没有对应接口,属于未接线界面。
## 6. 选房与房间维护
### 6.1 `Room.vue` 选房器
- 数据:`getRoomList()`,按 `room_floor` 分组,统计各状态数量。
- 状态颜色:
| `room_status` | 含义 | 按钮类型 |
| --- | --- | --- |
| `1` | 空闲 | success |
| `2` | 住满 | info |
| `3` | 待清扫 | warning |
| `4` | 不能入住 | danger |
| `5` | 未满 | primary |
| `6` | 正在保洁 | 默认(紫色样式) |
- `select_room`([Room.vue:209](../../YktVue/src/components/Room.vue#L209))只允许 `1/3/5/6`;其他状态提示“仅状态为 1、3、5、6 的房间可选择入住”。
- 推荐房来自父组件传入的 `recommendations`,点击后按 `room_no` 回查真实房间再选中;推荐理由直接显示 `recommend_reason`。
- 确认时 `$emit('childByValue', room)`;父组件 `JcXx.vue` 的 `childByValue` 根据当前弹窗把房间合并进入寓表单或换房列表。
- 缺陷:状态 `6` 可以选择,但 `zd_roomStatus_statusName` 没有 `6` 分支,确认框会显示“房屋状态:undefined”。
### 6.2 `RoomLook.vue` 房间维护
- 数据:`getRoomList()`,状态统计包含 `6`。
- 支持**多选相同状态**的房间(`select_room`,[RoomLook.vue:279](../../YktVue/src/components/RoomLook.vue#L279)),混合状态会拒绝。
- 批量操作:
| 操作 | 方法 | 接口 | 前置条件 |
| --- | --- | --- | --- |
| 开始保洁 | `startCleanSelected` | `/clean/startCleaning` | 选中状态 `3`(待清扫) |
| 结束保洁 | `endCleanSelected` | `/clean/endCleaning` | 选中状态 `6`(正在保洁) |
| 设为不能入住 | `setSelectedRoomsUnavailable` | `/jcgl/modifyRoomStatus?zt=4` | 状态 `1/2/3/5` 且房间无人 |
| 恢复可入住 | `restoreSelectedRoomsAvailable` | `/jcgl/modifyRoomStatus?zt=1` | 选中状态 `4` |
- `batchRun` 用 `Promise.all` 并发调用,任一失败会整体 reject,但已成功的房间不会回滚;界面只显示成功提示(失败被 `catch` 吞掉)。
- 保洁时长由 `bjkssj` 计算,仅用于展示。
## 7. 跨页面广播矩阵
| 通道 | 写入方 | 读取方 | 消息 | 状态 |
| --- | --- | --- | --- | --- |
| `myChannel` | `MealsOrdering`(偏餐下单)、`paymentPlatform` | `chefOrders` | `"2603"` | 可用(后厨监听器移除有 bug) |
| `channel_rly_jc` | `shijinpeng/main.vue` | 无 | JSON 登记数据 | **空写,无人监听** |
| `localStorage.gyid` / `loginAccount` | `Login.vue` | 所有页面/路由守卫 | 登录痕迹 | 可用,但可被手工篡改 |
| `location.reload()` / `$router.go(0)` | 各终端 | — | 整页刷新 | 事实上的“状态同步”手段 |
## 8. 终端模块风险汇总
| 级别 | 问题 | 位置 |
| --- | --- | --- |
| P0 | 自助终端 `getRyxx` 未导出,且入寓分支不调 `submitAll`,登记链路双重断裂 | `shijinpeng/main.vue:280`、`:428`、`:675` |
| P0 | 结算页刷脸链接被注释,且未调用扣款接口,结算闭环不成立 | `paymentPlatform.vue:212`、`:314` |
| P1 | 点餐餐次判断恒为偏餐 | `MealsOrdering.vue:262` |
| P1 | 点餐 30 分钟定时器调用不存在的方法 | `MealsOrdering.vue:257` |
| P1 | 扣款与建单无事务,可能钱扣了没订单 | `MealsOrdering.vue:334` 起 |
| P1 | 保洁完成不回写后端 | `roomService.vue:260` |
| P1 | 公寓/终端标识硬编码(`SYBXC`、`test33`) | `main.vue:291`、`roomService.vue:190`、各 WS URL |
| P2 | 后厨广播监听器泄漏 | `chefOrders.vue:412` |
| P2 | `channel_rly_jc` 无人监听 | `main.vue:285` |
| P2 | 状态 6 选房确认文案 undefined | `Room.vue:229` |
| P2 | 批量操作无失败回滚/提示 | `RoomLook.vue:313` |
## 9. 修改落点
| 需求 | 位置 |
| --- | --- |
| 自助终端支持多公寓 | `main.vue` 的 `gyid`/`gymc`、WS 路径、`roomService.vue` 的 `queryParams.gyid` |
| 修复入寓登记 | `main.vue:280` 改导入 `getRyXxByRyId`,同步改 `:428` 调用 |
| 启用结算刷脸 | `paymentPlatform.vue:212` 取消注释,并补齐扣款调用 |
| 改点餐餐次 | `MealsOrdering.vue`/`paymentPlatform.vue` 的 `isMealsClass`,用 `minute` 计算而非 `hours` |
| 改后厨提示 | `chefOrders.vue:394` 的 `handleReceivedMessage` |
| 改保洁闭环 | `roomService.vue:260` 接 `/clean/endCleaning` |
| 改选房规则 | `Room.vue:210`、`RoomLook.vue:287`、`:318` 起 |
@@ -0,0 +1,193 @@
# 07 构建部署与风险清单
## 1. 构建脚本
[`package.json`](../../YktVue/package.json) 实际脚本:
| 命令 | 作用 | 说明 |
| --- | --- | --- |
| `npm run dev` | 开发服务器 | 端口 `8080`,`host 0.0.0.0`,启动后自动开浏览器 |
| `npm run build:prod` | 生产构建 | 输出 `dist/`,`publicPath=/yktgl/` |
| `npm run build:stage` | staging 构建 | 仍请求 `/yktgl`(环境变量未生效) |
| `npm run preview` | 预览 | 依赖 `build/index.js`,仓库中不存在该文件 |
| `npm run lint` | ESLint | `eslint --ext .js,.vue src` |
环境要求:
```json
"engines": { "node": ">=16 <19", "npm": ">=8 <11" }
```
实际脚本使用 `node --openssl-legacy-provider`(Vue CLI 4 / webpack 4 在新 OpenSSL 下的兼容做法)。Node 16 使用的是 OpenSSL 1.1.1,可能不识别该参数;建议统一到 Node 18 LTS,并锁定 npm 版本。
`YktVue/README.md` 仍写 `npm run serve` / `npm run build`,与 `package.json` 不一致,按上表为准。
## 2. 环境与地址矩阵
| 环境 | 启动方式 | 页面地址 | 接口 baseURL | WebSocket |
| --- | --- | --- | --- | --- |
| 开发 | `npm run dev` | `http://localhost:8080` | `/dev-api` → 代理到 `http://127.0.0.1:8081/yktgl` | `ws://localhost:8080/yktgl/...` → 开发服务器是否代理 WS 未配置 |
| 生产 | `npm run build:prod` | `http://host/yktgl/` | `/yktgl` | `ws://host/yktgl/...` |
| staging | `npm run build:stage` | 同生产 | `/yktgl` | 同生产 |
开发代理配置在 [`vue.config.js`](../../YktVue/vue.config.js):
```js
'/dev-api': {
target: 'http://127.0.0.1:8081/yktgl',
changeOrigin: true,
pathRewrite: { '^/dev-api': '' }
}
```
注意:
- 开发代理只配了 HTTP,没有 `ws: true`;**本地 WebSocket 实际连的是前端 dev server 的 `8080`,不会自动转到 8081**,需要浏览器插件/反向代理或直接改代码。
- `.env.*` 的 `VUE_APP_BASE_API` 全部未生效;`request.js` 写死 `/dev-api` 与 `/yktgl`。
- `fileRequest.js` 写死 `/yktgl`,开发环境导出 404。
## 3. 部署要点
仓库内没有 nginx、Dockerfile、部署脚本,部署依赖现场约定。按代码推断的最小拓扑:
```nginx
server {
listen 80;
server_name _;
# 前端静态资源
location /yktgl/ {
root /opt/app; # /opt/app/yktgl/index.html
try_files $uri $uri/ /yktgl/index.html;
}
# 后端接口与 WebSocket
location /yktgl/ {
proxy_pass http://127.0.0.1:8081/yktgl/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
```
上面的两段 `location /yktgl/` 会冲突,实际部署需要按路径拆分(例如静态资源用 `/yktgl/static/`、`/yktgl/index.html`,接口用 `/yktgl/jcgl/` 等),或让后端同时托管静态资源。**上线前必须拿到现场真实 nginx 配置**,不要照抄本示例。
关键约束:
1. **静态与接口同源**:`publicPath`、`baseURL`、WebSocket 路径都假定 `/yktgl` 由同一个 host 提供。
2. **SIP 网关必须在本机 9004**:值班台写死 `ws://127.0.0.1:9004`。
3. **HTTPS 不可用**:所有 WebSocket 都是 `ws://`,页面一旦上 HTTPS 会被浏览器按混合内容拦截。
4. **浏览器权限**:自助终端使用 `getDisplayMedia` 录屏,需要现场授权;无人值守场景要配置 kiosk 模式与自动授权。
5. **多公寓**:当前产物只能通过改 `public/config.js`、`shijinpeng/main.vue`、`roomService.vue` 的常量来切换,不能一套包多公寓运行。
## 4. 测试基线
仓库没有 jest/vitest,也没有 `npm test`。现有两个纯 Node 脚本,直接在 `YktVue/` 下运行:
```bash
node tests/alarm-state.test.cjs
node tests/recharge-cancellation.cjs
```
| 脚本 | 覆盖内容 | 当前结果(2026-11-07) |
| --- | --- | --- |
| `tests/alarm-state.test.cjs` | 告警事件去重、新事件响铃、分组行、列表响应乱序、停止报警使在途响应失效 | 5 个场景通过 |
| `tests/recharge-cancellation.cjs` | 充值弹窗按 id 匹配取消、无关取消、后端拒绝、旧成功响应、网络失败 | 5 个场景通过 |
两个脚本通过正则截取 `JcXx.vue` 的方法源码再 `new Function` 执行,**重构方法名/缩进会导致测试失效**,改 `JcXx.vue` 后要同步检查。
建议补充的测试优先级:
1. 时间规则计算与 24 小时校验(`buildFollowUpTimes`、`validateCallDutyTimeline`);
2. 时间输入规范化(`8`、`815`、`0815`、`081530`);
3. 房间可选状态过滤(`Room.vue` 的 `1/3/5/6`);
4. 请求响应码分支(`11111` 的业务处理);
5. 食堂下单失败/部分明细失败的回滚策略(需要先重构)。
## 5. 风险清单
### P0:上线/运行阻断
| # | 风险 | 证据 | 影响 | 建议 |
| --- | --- | --- | --- | --- |
| P0-1 | 登录态是本地假 token,守卫可绕过 | [Login.vue:83](../../YktVue/src/views/Login.vue#L83)、[router/index.js:85](../../YktVue/src/router/index.js#L85) | 任何人写 `localStorage.loginAccount` 即可进入;后端 Session 过期后页面仍可操作,接口才失败 | 后端签发 token/Session 标识,前端守卫校验并处理 401;退出调用后端 |
| P0-2 | WebSocket 断开即整页刷新,且 `destroyed` 主动触发 | [JcXx.vue:1743](../../YktVue/src/views/JcXx.vue#L1743)、[:2086](../../YktVue/src/views/JcXx.vue#L2086)、[:2198](../../YktVue/src/views/JcXx.vue#L2198) | 后端重启/网络抖动导致值班台反复重载,告警期间可能丢失操作 | 改为退避重连;`destroyed` 只关闭连接,不调用含 `reload` 的方法 |
| P0-3 | 自助终端导入不存在的 `getRyxx`,且入寓分支不提交后端 | [main.vue:280](../../YktVue/src/shijinpeng/main.vue#L280)、[:428](../../YktVue/src/shijinpeng/main.vue#L428)、[:675](../../YktVue/src/shijinpeng/main.vue#L675) | 入寓登记刷脸必抛异常;即使修复,也只广播不落库,终端主功能不可用 | 改为 `getRyXxByRyId`,并在 `ryzt=1` 分支调用 `/jcgl/submitAll` 后联调 |
| P0-4 | 结算页刷脸链路被注释且不扣款 | [paymentPlatform.vue:212](../../YktVue/src/zhengchuang/views/paymentPlatform.vue#L212)、[:314](../../YktVue/src/zhengchuang/views/paymentPlatform.vue#L314) | 结算闭环不成立,可能“下单不扣钱” | 确认该页是否在用;启用 WS 并调用 `/Meals/consume` |
| P0-5 | 生产强依赖 `ws://` 与本机 SIP | [JcXx.vue:2007](../../YktVue/src/views/JcXx.vue#L2007)、[:2020](../../YktVue/src/views/JcXx.vue#L2020) | HTTPS 部署失败;值班台与 SIP 网关必须同机 | 统一用 `getWebSocketUrl()`;SIP 地址配置化 |
| P0-6 | 多公寓/多终端标识硬编码 | `shijinpeng/main.vue:291`、`roomService.vue:190`、各 WS 的 `test33` | 换公寓/多终端需要改代码重打包;消息无法定向 | 改为登录/URL 参数/后端下发 |
### P1:功能错误或明显数据风险
| # | 风险 | 证据 | 影响 |
| --- | --- | --- | --- |
| P1-1 | `11111` 被拦截器当成功返回 | [request.js:79](../../YktVue/src/utils/request.js#L79) | 调用方漏判时会把业务失败当成功;各页面处理不一致 |
| P1-2 | 网络错误无全局提示,`.catch()` 多为空 | [request.js:94](../../YktVue/src/utils/request.js#L94) | 用户点按钮无反应,排障困难 |
| P1-3 | 环境变量不生效、两套 baseURL | `request.js:17`、`fileRequest.js:6` | 切环境/切域名必须改代码;开发导出失败 |
| P1-4 | 自动导出每 10 分钟 | [JcXx.vue:1792](../../YktVue/src/views/JcXx.vue#L1792) | 值班机持续下载,占磁盘、干扰浏览器 |
| P1-5 | 点餐餐次判断恒为偏餐 | [MealsOrdering.vue:262](../../YktVue/src/zhengchuang/views/MealsOrdering.vue#L262) | 早餐/正餐菜单和价格取错 |
| P1-6 | 点餐定时器调用不存在的方法 | [MealsOrdering.vue:257](../../YktVue/src/zhengchuang/views/MealsOrdering.vue#L257) | 30 分钟后报错,菜单不刷新 |
| P1-7 | 扣款与建单非原子 | [MealsOrdering.vue:334](../../YktVue/src/zhengchuang/views/MealsOrdering.vue#L334) | 钱扣了没订单,或订单缺明细 |
| P1-8 | 保洁完成不回写 | [roomService.vue:260](../../YktVue/src/zhengchuang/views/roomService.vue#L260) | 房间状态永远停在“正在保洁/待清扫” |
| P1-9 | `JcXx.vue` 5294 行单体 | `src/views/JcXx.vue` | 修改风险高、测试只能截取方法、多人协作冲突 |
| P1-10 | 防重复提交只写不判 | [request.js:51](../../YktVue/src/utils/request.js#L51) | 双击/弱网重试可能重复充值、重复叫乘 |
| P1-11 | 前端 gyid 与后端 IP 解析 gyid 双源 | `JcXx.vue:1728`、后端 `getGyIdByIp` | 登录名不是公寓代码时,推送收不到/查询与推送范围不一致 |
### P2:代码质量与遗留
| # | 风险 | 证据 |
| --- | --- | --- |
| P2-1 | `setRowStyle` 渲染期修改 `this.message` | `JcXx.vue:3723` |
| P2-2 | `formatTime` 重复定义 | `JcXx.vue:2276`、`:3153` |
| P2-3 | `JcXx.vue` 导入不存在的 `auditJlXx` | `JcXx.vue:1392` |
| P2-4 | `handleDelete` 使用未安装的 `$modal` | `JcXx.vue:3034` |
| P2-5 | `plugins/*` 未安装、`utils/permission.js` 依赖不存在的 store | `src/plugins/`、`src/utils/permission.js` |
| P2-6 | `App.vue` 结尾多字面量 `3` | `App.vue:32` |
| P2-7 | `Home.vue` 使用未注册的 `<test1>` | `Home.vue:6` |
| P2-8 | 遗留文件:`JcXx.vue.bak_mojibake`、`RoomLook copy.vue`、`media.vue` | `src/views/`、`src/components/` |
| P2-9 | 后厨 `BroadcastChannel` 监听器泄漏 | `chefOrders.vue:412` |
| P2-10 | `channel_rly_jc` 无人监听 | `main.vue:285` |
| P2-11 | 状态 6 选房确认文案 undefined | `Room.vue:229` |
| P2-12 | 批量房间操作无失败回滚/提示 | `RoomLook.vue:313` |
| P2-13 | 无 404 路由 | `router/index.js` |
| P2-14 | README 命令与实际脚本不一致 | `YktVue/README.md` |
## 6. 改动落点速查
| 需求类型 | 首选落点 | 需要同步 |
| --- | --- | --- |
| 新增业务接口 | `src/api/jcgl/jcgl.js` | 页面调用、后端文档 06 |
| 改登录/鉴权 | `Login.vue`、`utils/auth.js`、`router/index.js`、`request.js` | 后端 `/auth/*`、部署 Session |
| 改 WebSocket/告警 | `JcXx.vue` 的 `initWebSocket`、`jcwebsocketonmessage`、`syncAlarmVoice` | 后端 `Jcwebsocket`、文档 05 |
| 改叫乘操作 | `JcXx.vue` 对应 `handle*` 方法 | 后端 `JcglController/Service`、文档 04 |
| 改房间状态 | `Room.vue`、`RoomLook.vue` | `/jcgl/getRoomList`、`/clean/*`、状态码表 |
| 改自助终端 | `shijinpeng/main.vue` | `rlywebsocket`、`/jcgl/submitAll`、设备录像协议 |
| 改食堂 | `zhengchuang/views/*`、`api/zhengchuang/mealsOrdering.js` | `/Meals/*`、`MealsWebsocket`、广播协议 |
| 改构建/代理 | `vue.config.js`、`request.js`、`fileRequest.js` | nginx、`.env.*`、部署文档 |
## 7. 待确认问题
以下问题无法仅从代码判断,需要业务/现场确认:
1. `paymentPlatform` 是否仍在用?如果不用,是否下线路由和页面?
2. `shijinpeng/main.vue` 的入寓登记是否还有现场依赖?`getRyxx` 是改名遗漏还是有意停用?
3. 现场是否有 HTTPS/域名计划?如果有,WebSocket 必须改 `wss`,SIP 也要确认是否支持 TLS。
4. 自动导出 Excel 是业务要求还是调试遗留?如果是要求,应改为后端定时任务或手工导出。
5. 多公寓是否仍靠“一套代码改常量分别打包”?是否有统一配置中心计划?
6. 值班台是否会在同一浏览器打开多个页面?`location.reload()` 和 `BroadcastChannel` 的假设都基于单页/单终端。
7. 后端 `jcwebsocket` 的 `cz_cancel`、`autoCallFailed`、`missedCallWarning` 消息是否已稳定?后端文档需要同步补齐。
## 8. 建议的迭代顺序
1. **先修 P0 阻断**:自助终端导入、结算闭环确认、WebSocket 重连、鉴权方案。
2. **再修 P1 数据风险**:扣款/建单一致性、自动导出、餐次判断、时间规则测试。
3. **然后做结构治理**:拆分 `JcXx.vue`(布局 / 列表 / 告警 / 操作 / 历史),统一请求层与环境配置。
4. **最后清遗留**:删除模板页面/插件/备份文件,补 404、补测试、更新 README。
## 9. 文档维护
- 本目录与后端文档同基准提交;改接口、改路由、改 WebSocket 消息时同步更新对应章节。
- 行号会随重构漂移,检索时优先用方法名/文件名。
- 新增前端文档时在 [README](./README.md) 索引中登记,并保持与 [后端文档索引](../backend/README.md) 的交叉链接。
@@ -0,0 +1,91 @@
# 08 · JcXx 值班主界面体验优化说明
> 面向:`YktVue/src/views/JcXx.vue`(`wc -l` 5294 行)
> 新版页面:`YktVue/src/views/JcXx.optimized.vue`(5603 行 / 214423 字节,保持原 CRLF 换行)
> 原则:**视觉冻结、功能冻结,只修体验**。原文件保留不动,确认后改名替换即可。
## 一、什么没变
| 项 | 结论 | 校验 |
| --- | --- | --- |
| `<script>` | 逐字节未改,109356 字节,sha256 前缀 `5b29b0f3c7b7f708` | FUN-01 |
| 功能集合 | 22 个弹窗、15 列主表、全部按钮 / 输入框 / 事件绑定 / `v-model` / `ref` / `slot-scope` 的集合与数量一致 | FUN-02 / 03 |
| 配色 | 只使用本页既有色值:深青 `#029191` / `#0b5858`、亮黄 `#fff45c`、墨 `#2c3e50`、红 `#c45656`、`#F56C6C`、绿 `#00FF00`、选中蓝 `#1990f1`、边框 `#dcdfe6` | 人工核对 |
| 字体 | 字号基准(行内 20px、吸底 24px、跑马灯 22px、按钮 13px)与字族未改,只统一原本互相打架的断点值 | 人工核对 |
| 组件形态 | 仍是 `el-card` / `el-row` / `el-col` / `el-tabs` / `el-table` / `el-dialog` / `el-form` + 内联渐变按钮 | FUN-03 |
| 信息呈现 | 表格列与顺序、字段、状态中文词、跑马灯与吸底条文案、行内状态底色规则全部原样 | FUN-02 cols |
模板只做两类改动:① 增加语义类名 `jc-opt-root` / `jc-header-*` / `jc-dialog` / `jc-wide-dialog` / `jc-st` / `jc-user-label`;② 把 `@tab_click` 修正为 `@tab-click`(Element UI 实际派发 `tab-click`;`handleTabClick` 函数体为空,行为不变)。`<style>` 原规则全部保留,仅修正 4 处错误,其余优化集中在末尾 306 行的「体验优化层」,可整段移除回退。
## 二、优化点
### 1. 布局拥挤 / 留白失衡
- 顶部 `el-row` 固定 `height:150px`,跑马灯又加 `margin-top:40px`,查询区被挤出容器 —— 改 `height:auto + min-height:150px`,跑马灯上边距 10px,查询区内外边距统一。
- 跑马灯 `padding-left:100%` + 28s 动画从 `translateX(0)` 起步,**开屏数秒整条提示是空的** —— `animation-delay:-4s` 把相位提前(实测文字落在条内居中),文案 / 配色 / 滚动速度不变;`prefers-reduced-motion` 下同时去掉 padding,否则停动画后文字彻底不可见。
- 时间卡在 6/24 列里顶部对齐,下方空出约 100px —— 该列改 flex 垂直居中,时间卡宽度按列收敛。
- 右上 5 个按钮 `space-between` + 一个纯占位 `:span=1` 空列 + `:span=5` 用户区,窄屏下「当前用户」被挤成多行 —— 统一 8px 间距并允许换行,用户标签 `white-space:nowrap`。
### 2. 对齐与间距不一致
- `.top-left-input-row` / `.top-left-action-row` 的 `margin-top: -2` **缺单位,是无效 CSS** —— 归零,改用 10px 统一节奏。
- 弹窗内 label 宽度 25% / 20% / 96px 三种混用 —— 统一 96px 居中(沿用本页 `.jc-update-form` 已有口径),配对输入框 / 下拉 `flex:1`。
- 22 个弹窗统一挂 `jc-dialog` 类名,使 `append-to-body` 后样式仍能被收口(不扩大原全局样式的影响面),4 个 1800/1700px 宽弹窗另挂 `jc-wide-dialog`。
- 弹窗表单项、底部按钮之间补齐统一间距。
### 3. 信息层级不清
- 主表表头补 700 字重 + 2px 深青下边线;tab 标签补 600 字重。
- 状态文字补字重与对比色。行内底色(`setRowStyle`)已承担语义色,因此**坚持纯文字呈现,不改成标签/徽标**:报警态在红底上用白字,其余用墨色/灰色。
### 4. 交互反馈缺失
- 原 `.el-table__body tr:hover > td { background-color: inherit !important }` 是为让 `td` 继承 `tr` 上的行内状态色,机制必须保留,但导致**整表无 hover 反馈** —— 保留机制,叠加一层 10% 深青 `background-image` 高亮,报警红/到点黄不被覆盖。
- `.el-button[style*="linear-gradient"]:hover` 把**所有**渐变按钮强制刷成蓝色渐变(橙、深青、红全变蓝)—— 改为 `filter:brightness(1.08)` + 中性阴影,保留各自语义色。
- `.el-button:hover` 在两处被重复定义成 `-1px` / `-2px` 位移 —— 统一为 `-1px`。
- 补 `:focus` / `:focus-visible` 轮廓 + 主色光晕;禁用按钮补 `not-allowed`、降饱和,且 hover 不再位移;补 `prefers-reduced-motion` 降级。
### 5. 状态提示不明确
- **选中行原先用 `#1990f1 !important` 纯蓝底覆盖,会把报警红、到点黄整行抹掉** —— 改为「继承行内状态色 + 30% 蓝叠加 + 首列 3px 蓝条」,选中态与状态色可同时辨识。
- 右下「有新的交路信息」原是 200×200 的 `position:absolute` 灰绿大块,滚动后位置漂移、易被吸底条压住、可点击却无指针提示 —— 改 `fixed` 贴边小卡、深青描边、hover 反馈;≤1200 再收成 150px 小卡,减少对表格的遮挡。
- 设备「在线 / 掉线」、历史「叫乘报警 / 离寓报警 是 / 否」补对比色。
### 6. 不同屏幕尺寸显示不佳
- 原页只有 1200 / 768 两档断点,1440~1024 区间 9/6/9 三栏严重挤压 —— 补 1440 / 1200 / 992 / 768 四档 + `max-height:900` 矮屏档。
- ≤1440 查询区独占一行、时间卡与按钮区第二行;≤992 全部纵向分区,四维输入框 2×2;≤768 输入框单列。
- 宽弹窗补 `max-width:calc(100vw - 32px)`;矮屏下弹窗 body 与 600px 固定表高改为内部可滚动,列 / 数据 / 操作不变。
- 吸底值班条的「最新同步时间」原用 `position:absolute; left:50%` 居中,与左右两段重叠 —— 改 flex 三段;页面补 `padding-bottom:62/88px`,防止 50px 吸底条盖住表格最后一行。
## 三、原样式 4 处错误修正
| 位置 | 问题 | 修正 |
| --- | --- | --- |
| `.top-left-input-row`、`.top-left-action-row` | `margin-top: -2` 缺单位,浏览器直接忽略 | `margin-top: 0`(2 处) |
| `.el-button:hover`(第 2 处) | 与前一处 `-1px` 冲突的 `-2px` 位移 | 统一 `-1px` |
| `.el-button[style*="linear-gradient"]:hover` | 用 `background: ... !important` 覆盖全部按钮配色 | 改为亮度 + 中性阴影 |
## 四、验收结果
`docs/frontend/_src/verify_jcxx.cjs` 一次跑完 **29/29 断言通过**,零失败:
- **编译**:`vue-template-compiler` 编译前后模板均 0 error;`@babel/parser` 解析脚本通过;`sass` 编译前后样式通过。
- **功能**:脚本段逐字节一致;事件 / `v-model` / 绑定 / `ref` / `slot-scope` / `v-if` 链 / 表格列 / 标签数量集合全部一致;原样式核心规则全部保留。
- **布局**:同一份静态 DOM + 优化前后两份真实 CSS,在 1920×1080 / 1440×900 / 1366×768 / 1280×800 / 1024×768 五个视口对比 —— **优化前累计 24 项问题(顶部裁切、吸底三段重叠、右下提示被压、底部无留白、宽弹窗越界),优化后 0 项**。
- **反馈**:hover 高亮、选中行保留状态色、橙色按钮 hover 不变蓝、焦点反馈、报警文字权重、提示卡片指针、减少动效降级,7 项全通过。
- 对比图 10 张见 `docs/frontend/ui/JcXx-优化前|优化后-*.png`。
## 五、使用方式
1. 备份 `YktVue/src/views/JcXx.vue`;
2. 用 `JcXx.optimized.vue` 覆盖同名文件(或先在路由里临时指向新文件对比);
3. `npm run dev` 后重点走查:主表 hover / 选中报警行、四个宽弹窗在 1366 屏的表现、吸底值班条三段、右下交路提醒、1440 与 1024 两档缩放。
可复现工具:`docs/frontend/_src/build_jcxx_optimized.py`(构建)、`verify_jcxx.cjs`(编译 + 功能集合 + 出图断言)。
## 六、边界说明
- 布局图是「同一份静态 DOM + 两份真实 CSS」的验证快照,不是组件运行时产物;`el-table` 的列宽与横向滚动由组件运行时计算,快照用 `min-width` 模拟这一行为。
- 未做:不拆分 5294 行单体、不改任何 `<script>` 逻辑 / 接口 / 定时器、不收敛原 `<style>` 的全局作用域(避免影响其它页面)、不改表格列定义与 `height` 属性、不改任何业务文案。
- 未改动原 `JcXx.vue`、后端、路由与配置;未执行 git commit / push。
+48
View File
@@ -0,0 +1,48 @@
# 叫乘管理系统 前端梳理文档
本目录是对 `YktVue/` 前端工程的结构化梳理,与后端文档 `../README.md` 配套使用。目标同样是给后续迭代一张“地图”:页面在哪里、数据从哪来、实时消息怎么走、改动会落到哪些文件。
> 适用范围:`YktVue/`(Vue 2 + Element UI)。
> 梳理基准:`main` 分支提交 `135a192`(2026-11-07),前端有效源码约 1.39 万行(不含备份/模板遗留)。
> 后端契约以 `yktgl/` 同提交为准,详见 [后端文档索引](../README.md)。
## 文档索引
| 文档 | 内容 | 适用场景 |
| --- | --- | --- |
| [01-前端总览与技术栈.md](./01-前端总览与技术栈.md) | 系统定位、使用角色、运行拓扑、技术栈、四类界面 | 新人入门、方案评审 |
| [02-工程结构与请求链路.md](./02-工程结构与请求链路.md) | 目录结构、启动装配、登录与路由守卫、axios 链路、API 封装与后端映射 | 找代码、改接口、排网络问题 |
| [03-路由与页面职责.md](./03-路由与页面职责.md) | 13 条路由、页面分组、入口关系、遗留页面 | 评估页面改动影响面 |
| [04-核心页面拆解-JcXx.md](./04-核心页面拆解-JcXx.md) | 值班主界面布局、状态与配色、全部操作、时间规则、轮询、历史数据 | 改叫乘业务前必读 |
| [05-实时通信与语音告警.md](./05-实时通信与语音告警.md) | 4 路 WebSocket + SIP、消息协议、告警状态机、弹窗队列、TTS 队列 | 改推送/告警/语音 |
| [06-食堂与自助终端模块.md](./06-食堂与自助终端模块.md) | 自助登记终端、点餐/结算/厨师/保洁终端、房间选择与房间维护 | 改终端类需求 |
| [07-构建部署与风险清单.md](./07-构建部署与风险清单.md) | 构建脚本、环境变量、部署要点、测试基线、P0/P1/P2 风险 | 排期、上线、评审 |
| [08-JcXx页面体验优化说明.md](./08-JcXx页面体验优化说明.md) | `JcXx.vue` 体验优化版:六类问题定位、4 处样式错误修正、前后对比验收与替换方式 | 主界面体验优化、上线前走查 |
## 建议阅读路径
- **第一次接触前端**:01 → 02 → 03 → 04。
- **接一个叫乘需求**:04 看页面与操作 → 05 看消息 → 02 查接口 → 07 查风险。
- **接一个终端/食堂需求**:06 → 02(API 封装)→ 07(构建与遗留问题)。
- **线上排障**:05(WebSocket/告警)→ 02(请求 baseURL/响应码)→ 07(已知缺陷)。
- **准备重构**:02(工程结构)→ 04(单体页面)→ 07(风险与落点)。
- **做主界面体验优化**:04 看现状 → 08 看改动与验收 → `YktVue/src/views/JcXx.optimized.vue`(新页面)→ `ui/JcXx-优化后-*.png`(五档视口对比图)。
## 快速事实
- 技术栈:Vue 2.6.12 + Vue Router 3.4.9 + Element UI 2.15.13 + axios 0.24.0 + ECharts 5.4.0,Vue CLI 4.4.6 / webpack 4。
- 启动脚本是 `npm run dev`(不是 README 里写的 `npm run serve`),开发端口 `8080`,代理 `/dev-api` → `http://127.0.0.1:8081/yktgl`。
- 生产 `publicPath` 为 `/yktgl/`,接口 `baseURL` 固定 `/yktgl`,按“前后端同域反向代理”部署设计。
- 登录走后端 `/auth/logon`:只校验公寓账号与来源 IP,**后端不返回 token**,前端落一个 `fake-token-*`,并把用户名同时写入 `gyid` 与 `loginAccount`。
- 主界面 `src/views/JcXx.vue` 5294 行,是绝对的单体页面:布局、状态、告警、TTS、SIP、历史、导出全在一个文件里。
- 实时通道有 4 类业务 WebSocket(叫乘/交路/人员/食堂)外加 1 路本机 SIP WebSocket(`ws://127.0.0.1:9004/sipclient/{gyid}`)。
- 前端没有 Vuex store、没有安装 `src/plugins/*`;`utils/permission.js` 与部分 RuoYi 模板代码是死代码。
- 已有两个纯 Node 测试脚本:`tests/alarm-state.test.cjs`、`tests/recharge-cancellation.cjs`,无 jest/npm test。
## 约定与说明
- 文档中的“行号”以本提交源码为准,重构后会漂移,优先按方法名/文件名检索。
- 前端很多页面依赖全局变量 `apiRoot`、`gyid`(由 `public/config.js` 注入),不是模块导入;搜索时直接搜变量名。
- 标注“已禁用/未接线”的代码表示界面上仍存在,但当前不会触发后端动作,改动前需确认是否要恢复。
- 本目录只做梳理,不对既有代码做修正;发现的问题统一收敛在 07 的风险清单。
- 08 附带的 `JcXx.optimized.vue` 是独立新文件,`<script>` 逐字节未改、原 `JcXx.vue` 未被修改;替换前请先备份。
+505
View File
@@ -0,0 +1,505 @@
# -*- coding: utf-8 -*-
"""
由 YktVue/src/views/JcXx.vue 生成 JcXx.optimized.vue。
设计约束(与用户需求一一对应):
1. <script> 段逐字节不变 —— 功能集合 100% 保留;
2. <template> 只增加语义 class、修正一处事件名拼写,不增删任何元素/指令/绑定;
3. <style> 原规则全部保留,仅修正 4 处明显错误,其余以「追加优化层」覆盖;
4. 不引入新配色 / 新字体 / 新组件 / 新字段,全部取自本页既有设计语言。
同时输出校验与布局快照所需的中间产物到 docs/frontend/_src/。
"""
import io
import os
import re
import hashlib
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
SRC = os.path.join(ROOT, 'YktVue', 'src', 'views', 'JcXx.vue')
OUT = os.path.join(ROOT, 'YktVue', 'src', 'views', 'JcXx.optimized.vue')
WORK = os.path.join(os.path.dirname(__file__))
src = io.open(SRC, encoding='utf-8').read()
script_before = src[src.index('<script>'):src.index('</script>') + len('</script>')]
s = src
def rep(text, old, new, tag, count=1):
n = text.count(old)
assert n == count, '[%s] 期望 %d 处,实际 %d 处' % (tag, n, count)
return text.replace(old, new)
# ============================================================ 1. template:语义 class
s = rep(s, '<div class="app_contianer">',
'<div class="app_contianer jc-opt-root">', '根容器 class')
s = rep(s, '<el-row style="height: 150px;" :gutter="15">',
'<el-row class="jc-header-row" style="height: 150px;" :gutter="15">',
'顶部行 class')
s = rep(s, '<el-col :span="9" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">',
'<el-col class="jc-header-search" :span="9" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">',
'顶部左列 class')
s = rep(s, '<el-col :span="6" style="height: 100%;">',
'<el-col class="jc-header-time" :span="6" style="height: 100%;">',
'顶部中列 class')
s = rep(s, '<el-col :span="9" style="height: 100%;">',
'<el-col class="jc-header-actions" :span="9" style="height: 100%;">',
'顶部右列 class')
s = rep(s, '<span style="font-size: 12px; color: #666;width: 45%;">当前用户: {{ loginAccount }}</span>',
'<span class="jc-user-label" style="font-size: 12px; color: #666;width: 45%;">当前用户: {{ loginAccount }}</span>',
'当前用户标签 class')
# Element UI 派发的是 tab-click;原写法 tab_click 永远不会被触发(处理函数体为空,修正后行为不变)
s = rep(s, '@tab_click=handleTabClick', '@tab-click=handleTabClick', 'tab 事件名拼写')
# 状态列:仅加 class,文本与 v-if 链完全不变(行内底色已是状态语义色,
# 这里只提升文字对比与字重,不把纯文字改成标签,保持原有信息呈现方式)
old_state = """ <el-table-column prop="zt" label="状态" min-width="7%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.ryzt == '0'">待叫乘</span>
<span v-else-if="scope.row.ryzt == '1'">正在呼叫</span>
<span v-else-if="scope.row.ryzt == '5'">正在通话</span>
<span v-else-if="scope.row.ryzt == '2'">叫乘报警</span>
<span v-else-if="scope.row.ryzt == '3'">待离寓</span>
<span v-else-if="scope.row.ryzt == '4'">离寓报警</span>
<span v-else>已入寓</span>
</template>
</el-table-column>"""
new_state = """ <el-table-column prop="zt" label="状态" min-width="7%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-normal" v-if="scope.row.ryzt == '0'">待叫乘</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '1'">正在呼叫</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '5'">正在通话</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.ryzt == '2'">叫乘报警</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '3'">待离寓</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.ryzt == '4'">离寓报警</span>
<span class="jc-st jc-st-muted" v-else>已入寓</span>
</template>
</el-table-column>"""
s = rep(s, old_state, new_state, '主表状态列 class')
old_dev = """ <el-table-column prop="heartBeatTime" label="状态" min-width="8%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.isOffline == '0'">在线</span>
<span v-else-if="scope.row.isOffline == '1'">掉线</span>
</template>
</el-table-column>"""
new_dev = """ <el-table-column prop="heartBeatTime" label="状态" min-width="8%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-normal" v-if="scope.row.isOffline == '0'">在线</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.isOffline == '1'">掉线</span>
</template>
</el-table-column>"""
s = rep(s, old_dev, new_dev, '设备状态列 class')
old_hist = """ <el-table-column prop="is_jcbj" label="叫乘报警" min-width="4%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.is_jcbj == '1'">是</span>
<span v-else>否</span>
</template>
</el-table-column>
<el-table-column prop="is_lybj" label="离寓报警" min-width="4%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.is_lybj == '1'">是</span>
<span v-else>否</span>
</template>
</el-table-column>"""
new_hist = """ <el-table-column prop="is_jcbj" label="叫乘报警" min-width="4%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-alarm" v-if="scope.row.is_jcbj == '1'">是</span>
<span class="jc-st jc-st-muted" v-else>否</span>
</template>
</el-table-column>
<el-table-column prop="is_lybj" label="离寓报警" min-width="4%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-alarm" v-if="scope.row.is_lybj == '1'">是</span>
<span class="jc-st jc-st-muted" v-else>否</span>
</template>
</el-table-column>"""
s = rep(s, old_hist, new_hist, '历史报警列 class')
# 所有弹窗统一挂 jc-dialog(append-to-body 后仍在 body 下,必须靠自身 class 收口样式);
# 1800/1700px 的宽弹窗额外挂 jc-wide-dialog 以做视口约束。
dialog_re = re.compile(r'<el-dialog\b[^>]*?>', re.S)
dialog_tags = dialog_re.findall(s)
expected = s.count('<el-dialog')
assert len(dialog_tags) == expected, 'el-dialog 标签解析数 %d != %d' % (len(dialog_tags), expected)
def decorate_dialog(m):
tag = m.group(0)
wide = bool(re.search(r'width="(?:1800|1700)px"', tag))
extra = 'jc-dialog' + (' jc-wide-dialog' if wide else '')
cm = re.search(r'\bclass="([^"]*)"', tag)
if cm:
merged = extra + (' ' + cm.group(1) if cm.group(1).strip() else '')
return tag[:cm.start()] + 'class="' + merged + '"' + tag[cm.end():]
return tag.replace('<el-dialog', '<el-dialog class="' + extra + '"', 1)
s = dialog_re.sub(decorate_dialog, s)
print('弹窗装饰:%d 个(其中宽弹窗 %d 个)' % (
expected, s.count('jc-wide-dialog')))
# ============================================================ 2. style:4 处错误修正
# (a) margin-top: -2 缺单位,是无效声明(2 处)
s = rep(s, 'margin-top: -2;', 'margin-top: 0;', '无效负边距归零', count=2)
# (b) 同一 :hover 位移被重复定义为 -1px / -2px,统一为 -1px
s = rep(s, 'transform: translateY(-2px) !important;',
'transform: translateY(-1px) !important;', 'hover 位移统一')
# (c) 所有渐变按钮 hover 被强制刷成蓝色渐变,破坏橙/深青/红按钮的语义色
old_hover = """/* 查询按钮特殊样式 */
.el-button[style*="linear-gradient"]:hover {
background: linear-gradient(135deg, #66b3ff, #409EFF) !important;
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.5) !important;
}"""
new_hover = """/* 查询按钮特殊样式(修正:保留按钮自身渐变,只做亮度与阴影反馈) */
.el-button[style*="linear-gradient"]:hover {
filter: brightness(1.08) !important;
box-shadow: 0 8px 25px rgba(11, 88, 88, 0.28) !important;
}"""
s = rep(s, old_hover, new_hover, '渐变按钮 hover 配色修正')
# ============================================================ 3. 追加体验优化层
OPT_CSS = r"""
/* ==========================================================================
JcXx.vue 体验与可用性优化层 v1
--------------------------------------------------------------------------
只修六类问题:布局拥挤/留白失衡、对齐间距不一致、信息层级、
交互反馈缺失、状态提示不明确、多尺寸适配。
· 不新增/删减/替换任何功能,<script> 逐字节未改;
· 不引入新配色:所有色值取自本页原有样式
(#029191 深青 / #0b5858 深青 / #fff45c 黄 / #2c3e50 墨 /
#c45656 红 / #606266 灰 / #dcdfe6 边框 / #1990f1 选中蓝);
· 不改动字号基准、组件形态与信息呈现方式,仅统一不一致处并补齐断点;
· 新增规则统一以 .jc-opt-root / .jc-dialog 收口,不扩大原全局样式的影响面。
原样式规则完整保留在前,本层可整段移除以回到优化前效果。
========================================================================== */
/* ---- O1 顶部:固定 150px + 40px 跑马灯上边距造成的拥挤与留白失衡 ---- */
.jc-header-row{
height:auto !important;
min-height:150px;
display:flex;
flex-wrap:wrap;
align-items:stretch;
}
.jc-header-row > .el-col{
height:auto !important;
min-height:150px;
float:none;
}
.jc-header-search{
justify-content:flex-start !important;
padding:4px 0 8px;
}
.jc-header-time{
display:flex !important;
align-items:center;
justify-content:center;
}
.jc-header-time > .el-row{ width:100%; }
.jc-header-actions{ padding:2px 0; }
.top-left-marquee-row{ margin-top:10px !important; margin-bottom:8px !important; }
.top-left-search-area{
padding:8px 14px 10px !important;
justify-content:flex-start !important;
}
.top-left-input-row{ margin-top:0 !important; margin-bottom:10px !important; }
.top-left-action-row{ margin-top:0 !important; }
.form-item-container{ padding:0 6px; }
/* 原跑马灯 padding-left:100% + translateX(0) 起步,开屏数秒内整条提示为空。
用负动画延时把滚动相位提前,文案/配色/组件/速度全部不变。 */
.marquee-content{ animation-delay:-4s; }
.center-time-section{
width:calc(100% - 30px);
box-sizing:border-box;
margin:0 15px;
}
.date-time{ width:100%; }
/* 右上按钮组:等间距、窄屏可换行,避免互相挤压与文字截断 */
.jc-header-actions .el-row > .el-col > div{
flex-wrap:wrap;
gap:8px !important;
}
.jc-header-actions .el-button{ flex:0 0 auto; }
.jc-user-label{
width:auto !important;
white-space:nowrap;
color:#606266 !important;
font-size:12px !important;
}
/* ---- O2 表单/弹窗:label 宽度 25%/20%/96px 混用导致对齐不一致 ---- */
.jc-opt-root .el-form label[style*="width:25%"],
.jc-opt-root .el-form label[style*="width:20%"],
.jc-dialog .el-form label[style*="width:25%"],
.jc-dialog .el-form label[style*="width:20%"]{
flex:0 0 96px;
width:96px !important;
min-width:96px;
text-align:center;
white-space:nowrap;
line-height:20px;
}
.jc-opt-root .el-form .el-col > .el-input,
.jc-opt-root .el-form .el-col > .el-select,
.jc-opt-root .el-form .el-col > div > .el-input,
.jc-opt-root .el-form .el-col > div > .el-select,
.jc-dialog .el-form .el-col > .el-input,
.jc-dialog .el-form .el-col > .el-select,
.jc-dialog .el-form .el-col > div > .el-input,
.jc-dialog .el-form .el-col > div > .el-select{
flex:1 1 0;
min-width:0;
}
.jc-dialog .el-form-item{ margin-bottom:16px; }
.jc-dialog .el-dialog__footer .el-button + .el-button{ margin-left:12px; }
/* 宽弹窗 1800/1700px 在 1366 及以下屏幕会横向越界 */
.jc-wide-dialog .el-dialog{
max-width:calc(100vw - 32px) !important;
}
/* ---- O3 交互反馈:焦点、hover、禁用 ---- */
.el-button:hover{ transform:translateY(-1px) !important; }
/* 焦点轮廓:element-ui 自身把 outline 压掉了,这里用本页作用域 + !important 补回 */
.jc-opt-root .el-button:focus,
.jc-opt-root .el-button:focus-visible,
.jc-opt-root .el-input__inner:focus,
.jc-opt-root .el-input__inner:focus-visible,
.jc-opt-root .el-checkbox__inner:focus,
.jc-dialog .el-button:focus,
.jc-dialog .el-button:focus-visible,
.jc-dialog .el-input__inner:focus,
.jc-dialog .el-input__inner:focus-visible,
.jc-dialog .el-checkbox__inner:focus{
outline:2px solid #029191 !important;
outline-offset:2px !important;
}
/* 部分浏览器对 input 的 outline 计算值有限制,补一层主色光晕保证焦点可见 */
.jc-opt-root .el-input__inner:focus,
.jc-opt-root .el-input__inner:focus-visible,
.jc-dialog .el-input__inner:focus,
.jc-dialog .el-input__inner:focus-visible{
box-shadow:0 0 0 3px rgba(2,145,145,.22) !important;
}
.el-button.is-disabled,
.el-button[disabled]{
cursor:not-allowed !important;
opacity:.55;
filter:grayscale(.25);
}
.el-button.is-disabled:hover,
.el-button[disabled]:hover{
transform:none !important;
box-shadow:none !important;
}
/* ---- O4 表格:保留行内状态底色,补齐 hover / 选中反馈与列层级 ----
原 .el-table__body tr:hover > td 用 background-color:inherit!important
是为了让 td 继承 tr 上的行内状态色(黄/绿/红/蓝),机制必须保留;
这里只叠加一层半透明 background-image 提供 hover 反馈,不覆盖底色。 */
.jc-opt-root .main-jcxx-table .el-table__body tr:hover > td,
.jc-dialog .el-table__body tr:hover > td{
background-image:linear-gradient(rgba(2,145,145,.10), rgba(2,145,145,.10)) !important;
}
/* 选中行原先用纯蓝底 !important 覆盖,会把报警红/到点黄整行抹掉。
改为「继承行内状态色 + 叠加蓝层 + 首列蓝条」,选中态与状态色可同时辨识。 */
.jc-opt-root .el-table__body tr.current-row > td,
.jc-dialog .el-table__body tr.current-row > td,
.jc-dialog .history-jcxx-table.el-table--striped .el-table__body tr.current-row > td,
.jc-dialog .history-jcxx-table.el-table--striped .el-table__body tr.current-row.el-table__row--striped > td{
background-color:inherit !important;
background-image:linear-gradient(rgba(25,144,241,.30), rgba(25,144,241,.30)) !important;
color:inherit !important;
}
.jc-opt-root .el-table__body tr.current-row > td:first-child,
.jc-dialog .el-table__body tr.current-row > td:first-child{
box-shadow:inset 3px 0 0 #1990f1;
}
/* 状态文字:行内底色已承担语义色,这里只提升字重与对比,不改成标签 */
.jc-st{ font-weight:700; letter-spacing:.3px; }
.jc-st-normal{ color:#2c3e50; }
.jc-st-alarm{ color:#ffffff; text-shadow:0 1px 2px rgba(0,0,0,.35); }
.jc-st-muted{ color:#606266; }
.jc-opt-root .main-jcxx-table th.el-table__cell{
font-weight:700 !important;
border-bottom:2px solid #029191 !important;
vertical-align:middle;
}
.jc-opt-root .main-jcxx-table .cell{ padding-left:6px; padding-right:6px; }
.jc-opt-root .el-tabs__item{ font-weight:600; }
/* ---- O5 吸底值班条:absolute 居中在窄屏与左右内容重叠 ---- */
.jc-opt-root{ padding-bottom:62px; } /* 防止 50px 吸底条遮住表格最后一行 */
.bottom-duty-bar{
display:flex;
align-items:center;
gap:16px;
padding:0 20px;
box-sizing:border-box;
}
.bottom-stay-summary{
flex:1 1 auto;
gap:36px;
padding-left:0;
min-width:0;
}
.latest-sync-time{
position:static !important;
transform:none !important;
flex:0 1 auto;
white-space:nowrap;
}
.bottom-duty-info{
flex:1 1 auto;
display:flex;
align-items:center;
justify-content:flex-end;
gap:16px;
min-width:0;
}
.bottom-duty-info .el-button{
position:static !important;
right:auto !important;
top:auto !important;
flex:0 0 auto;
}
.bottom-duty-info > span{
margin-right:0 !important;
position:static !important;
top:auto !important;
white-space:nowrap;
}
/* ---- O6 右下角「有新的交路信息」提示:改为固定定位并补齐点击反馈 ----
原为 200x200 的 absolute 大色块,滚动后位置漂移且易被吸底条压住。 */
.bottom-right-div{
position:fixed !important;
bottom:72px;
right:20px;
width:200px;
height:auto;
box-sizing:border-box;
padding:14px 16px;
line-height:1.5;
text-align:center;
border:2px solid #029191;
box-shadow:0 6px 18px rgba(0,0,0,.18);
z-index:1200;
cursor:pointer;
transition:transform .25s ease, box-shadow .25s ease;
}
.bottom-right-div:hover{
transform:translateY(-2px);
box-shadow:0 10px 24px rgba(0,0,0,.24);
}
/* ---- O7 响应式:原页只有 1200/768 两档,且顶部 9/6/9 在中屏严重挤压 ---- */
@media (max-width:1440px){
.jc-header-row{ min-height:0; }
.jc-header-row > .el-col{ min-height:0; }
.jc-header-row > .jc-header-search{ flex:1 1 100%; width:100%; }
.jc-header-row > .jc-header-time{ flex:0 0 320px; width:320px; }
.jc-header-row > .jc-header-actions{ flex:1 1 520px; width:auto; }
.center-time-section{ min-width:0; }
}
@media (max-width:1200px){
.bottom-stay-summary{ gap:20px; }
.bottom-stay-summary,.latest-sync-time{ font-size:20px; }
.bottom-duty-info > span{ font-size:20px !important; }
/* 窄屏下 200px 卡片会压住表格右下数据,收紧为贴边小卡 */
.bottom-right-div{
width:150px;
right:8px;
bottom:62px;
padding:8px 10px;
font-size:14px !important;
line-height:1.35;
opacity:.96;
}
}
@media (max-width:992px){
.jc-header-row > .jc-header-time,
.jc-header-row > .jc-header-actions{ flex:1 1 100%; width:100%; }
.jc-header-row .top-left-input-row .el-col-6{ width:50%; }
.bottom-duty-bar{
flex-wrap:wrap;
height:auto !important;
min-height:50px;
padding:6px 12px;
gap:4px 12px;
}
.bottom-stay-summary,.latest-sync-time,.bottom-duty-info > span{
font-size:16px !important;
line-height:1.4;
}
.latest-sync-time{ flex:1 1 100%; order:3; text-align:right; }
.jc-opt-root{ padding-bottom:88px; }
.bottom-right-div{ bottom:96px; }
}
@media (max-width:768px){
.jc-header-row .top-left-input-row .el-col-6{ width:100%; }
.bottom-right-div{ width:160px; font-size:16px !important; }
}
/* 矮屏:600px 固定表高 + 50px 吸底条会溢出,改为可滚动,不改变数据/列/操作 */
@media (max-height:900px){
.jc-opt-root .main-jcxx-table{
height:auto !important;
max-height:calc(100vh - 260px);
}
.jc-opt-root .main-jcxx-table .el-table__body-wrapper{
height:auto !important;
max-height:calc(100vh - 320px);
overflow-y:auto;
}
.jc-dialog .el-dialog__body{
max-height:calc(100vh - 170px);
overflow-y:auto;
}
}
@media (prefers-reduced-motion:reduce){
.marquee-content{
animation:none !important;
padding-left:0 !important; /* 停动画时必须去掉右推,否则整条提示不可见 */
}
.init-success-content i{ animation:none !important; }
.el-button,
.bottom-right-div{ transition:none !important; }
.el-button:hover{ transform:none !important; }
}
/* ===================== 体验优化层结束 ===================== */
"""
s = rep(s, '</style>', OPT_CSS + '</style>', '追加优化层')
# ============================================================ 4. 写出
# 原文件为 CRLF;写出时保持同一换行,避免全文件 diff / 逐字节比对误报
out_text = s.replace('\n', '\r\n')
io.open(OUT, 'w', encoding='utf-8', newline='').write(out_text)
script_after = s[s.index('<script>'):s.index('</script>') + len('</script>')]
assert script_before == script_after, '<script> 段发生变化,已中止'
print('<script> 段逐字节一致:%d 字节 sha256=%s' % (
len(script_before.encode('utf-8')),
hashlib.sha256(script_before.encode('utf-8')).hexdigest()[:16]))
# 中间产物:优化前 / 优化后的 style 源(供 sass 编译与前后对比快照使用)
def extract_style(text):
a = text.index('<style')
b = text.index('</style>')
return text[text.index('>', a) + 1:b]
io.open(os.path.join(WORK, 'style-before.scss'), 'w', encoding='utf-8').write(extract_style(src))
io.open(os.path.join(WORK, 'style-after.scss'), 'w', encoding='utf-8').write(extract_style(s))
print('输出:%s(%d 行 / %d 字节 / CRLF)' % (OUT, len(s.splitlines()), len(out_text.encode('utf-8'))))
print('原文件:%d 行 / %d 字节' % (len(src.splitlines()), os.path.getsize(SRC)))
File diff suppressed because it is too large. Load diff
+839
View File
@@ -0,0 +1,839 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<title>JcXx 优化前</title>
<style>@charset "UTF-8";@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\e6a0"}.el-icon-ice-cream-square:before{content:"\e6a3"}.el-icon-lollipop:before{content:"\e6a4"}.el-icon-potato-strips:before{content:"\e6a5"}.el-icon-milk-tea:before{content:"\e6a6"}.el-icon-ice-drink:before{content:"\e6a7"}.el-icon-ice-tea:before{content:"\e6a9"}.el-icon-coffee:before{content:"\e6aa"}.el-icon-orange:before{content:"\e6ab"}.el-icon-pear:before{content:"\e6ac"}.el-icon-apple:before{content:"\e6ad"}.el-icon-cherry:before{content:"\e6ae"}.el-icon-watermelon:before{content:"\e6af"}.el-icon-grape:before{content:"\e6b0"}.el-icon-refrigerator:before{content:"\e6b1"}.el-icon-goblet-square-full:before{content:"\e6b2"}.el-icon-goblet-square:before{content:"\e6b3"}.el-icon-goblet-full:before{content:"\e6b4"}.el-icon-goblet:before{content:"\e6b5"}.el-icon-cold-drink:before{content:"\e6b6"}.el-icon-coffee-cup:before{content:"\e6b8"}.el-icon-water-cup:before{content:"\e6b9"}.el-icon-hot-water:before{content:"\e6ba"}.el-icon-ice-cream:before{content:"\e6bb"}.el-icon-dessert:before{content:"\e6bc"}.el-icon-sugar:before{content:"\e6bd"}.el-icon-tableware:before{content:"\e6be"}.el-icon-burger:before{content:"\e6bf"}.el-icon-knife-fork:before{content:"\e6c1"}.el-icon-fork-spoon:before{content:"\e6c2"}.el-icon-chicken:before{content:"\e6c3"}.el-icon-food:before{content:"\e6c4"}.el-icon-dish-1:before{content:"\e6c5"}.el-icon-dish:before{content:"\e6c6"}.el-icon-moon-night:before{content:"\e6ee"}.el-icon-moon:before{content:"\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\e6f1"}.el-icon-partly-cloudy:before{content:"\e6f2"}.el-icon-cloudy:before{content:"\e6f3"}.el-icon-sunny:before{content:"\e6f6"}.el-icon-sunset:before{content:"\e6f7"}.el-icon-sunrise-1:before{content:"\e6f8"}.el-icon-sunrise:before{content:"\e6f9"}.el-icon-heavy-rain:before{content:"\e6fa"}.el-icon-lightning:before{content:"\e6fb"}.el-icon-light-rain:before{content:"\e6fc"}.el-icon-wind-power:before{content:"\e6fd"}.el-icon-baseball:before{content:"\e712"}.el-icon-soccer:before{content:"\e713"}.el-icon-football:before{content:"\e715"}.el-icon-basketball:before{content:"\e716"}.el-icon-ship:before{content:"\e73f"}.el-icon-truck:before{content:"\e740"}.el-icon-bicycle:before{content:"\e741"}.el-icon-mobile-phone:before{content:"\e6d3"}.el-icon-service:before{content:"\e6d4"}.el-icon-key:before{content:"\e6e2"}.el-icon-unlock:before{content:"\e6e4"}.el-icon-lock:before{content:"\e6e5"}.el-icon-watch:before{content:"\e6fe"}.el-icon-watch-1:before{content:"\e6ff"}.el-icon-timer:before{content:"\e702"}.el-icon-alarm-clock:before{content:"\e703"}.el-icon-map-location:before{content:"\e704"}.el-icon-delete-location:before{content:"\e705"}.el-icon-add-location:before{content:"\e706"}.el-icon-location-information:before{content:"\e707"}.el-icon-location-outline:before{content:"\e708"}.el-icon-location:before{content:"\e79e"}.el-icon-place:before{content:"\e709"}.el-icon-discover:before{content:"\e70a"}.el-icon-first-aid-kit:before{content:"\e70b"}.el-icon-trophy-1:before{content:"\e70c"}.el-icon-trophy:before{content:"\e70d"}.el-icon-medal:before{content:"\e70e"}.el-icon-medal-1:before{content:"\e70f"}.el-icon-stopwatch:before{content:"\e710"}.el-icon-mic:before{content:"\e711"}.el-icon-copy-document:before{content:"\e718"}.el-icon-full-screen:before{content:"\e719"}.el-icon-switch-button:before{content:"\e71b"}.el-icon-aim:before{content:"\e71c"}.el-icon-crop:before{content:"\e71d"}.el-icon-odometer:before{content:"\e71e"}.el-icon-time:before{content:"\e71f"}.el-icon-bangzhu:before{content:"\e724"}.el-icon-close-notification:before{content:"\e726"}.el-icon-microphone:before{content:"\e727"}.el-icon-turn-off-microphone:before{content:"\e728"}.el-icon-position:before{content:"\e729"}.el-icon-postcard:before{content:"\e72a"}.el-icon-message:before{content:"\e72b"}.el-icon-chat-line-square:before{content:"\e72d"}.el-icon-chat-dot-square:before{content:"\e72e"}.el-icon-chat-dot-round:before{content:"\e72f"}.el-icon-chat-square:before{content:"\e730"}.el-icon-chat-line-round:before{content:"\e731"}.el-icon-chat-round:before{content:"\e732"}.el-icon-set-up:before{content:"\e733"}.el-icon-turn-off:before{content:"\e734"}.el-icon-open:before{content:"\e735"}.el-icon-connection:before{content:"\e736"}.el-icon-link:before{content:"\e737"}.el-icon-cpu:before{content:"\e738"}.el-icon-thumb:before{content:"\e739"}.el-icon-female:before{content:"\e73a"}.el-icon-male:before{content:"\e73b"}.el-icon-guide:before{content:"\e73c"}.elLine truncated
<style>@charset "UTF-8";
.marquee-container {
white-space: nowrap;
overflow: hidden;
}
.top-left-marquee-row {
margin-top: 10px;
margin-bottom: 4px;
}
.top-left-search-area {
padding-top: 6px !important;
}
.top-left-input-row {
margin-top: -2;
}
.top-left-action-row {
margin-top: -2;
}
/* 中间时间信息区域 */
.center-time-section {
flex: 0 0 auto;
background: linear-gradient(135deg, #0078d7, #107c10);
color: white;
padding: 12px 16px;
border-radius: 8px;
margin: 0 15px;
min-width: 320px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.date-time {
display: flex;
flex-direction: column;
align-items: center;
font-size: 16px;
}
.date-time .text-24 {
font-size: 26px !important;
font-weight: 600;
margin: 0;
line-height: 1.2;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.center-time-section {
min-width: 280px;
padding: 10px 14px;
}
.date-time .text-24 {
font-size: 24px !important;
}
.date-time {
font-size: 14px;
}
}
@media (max-width: 768px) {
.center-time-section {
min-width: 240px;
padding: 8px 12px;
margin: 0 10px;
}
.date-time .text-24 {
font-size: 20px !important;
}
.date-time {
font-size: 12px;
}
}
/* ============ 右下角初始化提示 ============ */
.init-success-tip {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 2000;
background: linear-gradient(135deg, #67c23a, #85ce61);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(103, 194, 58, 0.3);
animation: slideInUp 0.6s ease-out;
backdrop-filter: blur(10px);
}
.init-success-content {
display: flex;
align-items: center;
padding: 12px 20px;
color: white;
font-size: 14px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.init-success-content i {
font-size: 18px;
margin-right: 8px;
animation: successPulse 2s infinite;
}
@keyframes successPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* ============ 表单样式(缩小版) ============ */
.form-item-container {
padding: 0 8px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-label {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 6px;
display: block;
text-align: center;
}
.form-item-container .el-input {
width: 100%;
}
.form-item-container .el-input__inner {
border-radius: 6px !important;
border: 1px solid #dcdfe6 !important;
padding: 8px 12px !important;
font-size: 13px !important;
transition: all 0.3s ease !important;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
height: 32px !important;
}
.form-item-container .el-input__inner:focus {
border-color: #409EFF !important;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1) !important;
}
.form-item-container .el-input__inner:hover {
border-color: #c0c4cc !important;
}
/* 按钮行样式(缩小版)*/
.el-button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
height: 32px !important;
font-size: 13px !important;
}
.el-button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15) !important;
}
.el-button:active {
transform: translateY(0) !important;
}
.buttonFont-small {
font-size: 13px;
font-weight: 600;
position: relative;
z-index: 2;
}
/* 响应式表单设置 */
@media (max-width: 1200px) {
.form-label {
font-size: 13px;
}
.form-item-container .el-input__inner {
padding: 7px 10px !important;
font-size: 12px !important;
height: 30px !important;
}
.init-success-tip {
bottom: 20px;
right: 20px;
}
.init-success-content {
padding: 10px 16px;
font-size: 13px;
}
}
@media (max-width: 768px) {
.form-item-container {
padding: 0 4px;
}
.form-label {
font-size: 12px;
margin-bottom: 4px;
}
.form-item-container .el-input__inner {
padding: 6px 8px !important;
font-size: 11px !important;
height: 28px !important;
}
.el-button {
font-size: 11px !important;
height: 28px !important;
padding: 4px 12px !important;
}
.buttonFont-small {
font-size: 11px;
}
.init-success-tip {
bottom: 15px;
right: 15px;
}
.init-success-content {
padding: 8px 12px;
font-size: 12px;
}
.init-success-content i {
font-size: 14px;
margin-right: 6px;
}
}
.marquee-content {
display: inline-block;
animation: marquee 28s linear infinite;
padding-left: 100%;
color: #fff45c;
font-size: 22px;
font-weight: 800;
line-height: 36px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
letter-spacing: 0;
white-space: nowrap;
}
.marquee-repeat-text {
margin-left: 160px;
}
@keyframes warningBlink {
0%, 100% {
opacity: 1;
background-color: #FFF200;
}
50% {
opacity: 1;
background-color: transparent;
}
}
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-200%);
}
}
.el-table__body tr:hover > td {
background-color: inherit !important;
}
.main-jcxx-table th.el-table__cell {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
.main-jcxx-table td.el-table__cell {
padding-top: 5px !important;
padding-bottom: 5px !important;
}
.main-jcxx-table .cell {
line-height: 1.2 !important;
}
.history-jcxx-form-row {
display: flex;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
width: 100%;
column-gap: 6px;
}
.history-jcxx-form-row:first-child {
column-gap: 2px;
}
.history-jcxx-field-box {
width: 226px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-inline-label {
width: 58px;
padding-right: 8px;
font-size: 13px;
line-height: 30px;
text-align: right;
white-space: nowrap;
color: #606266;
box-sizing: border-box;
flex: 0 0 auto;
}
.history-jcxx-field-box .el-input {
width: 146px;
}
.history-jcxx-form-row + .history-jcxx-form-row {
margin-top: 2px;
}
.history-jcxx-query-button {
margin-left: 4px;
flex: 0 0 auto;
}
.history-jcxx-filter-box {
width: 230px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-filter-label {
width: 72px;
}
.history-jcxx-filter-box .el-select {
width: 140px;
}
.history-jcxx-time-box {
width: 610px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-time-label {
width: 60px;
}
.history-jcxx-time-range {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
width: 540px;
}
.history-jcxx-count {
min-width: 180px;
margin-left: 8px;
flex: 0 0 auto;
}
.history-jcxx-form .el-input__inner,
.history-jcxx-form .el-date-editor.el-input__inner,
.history-jcxx-form .el-date-editor .el-range-input,
.history-jcxx-form .el-date-editor .el-range-separator {
font-size: 12px !important;
}
.history-jcxx-form .el-input__inner,
.history-jcxx-form .el-date-editor.el-input__inner,
.history-jcxx-form .el-date-editor--datetimerange {
height: 30px !important;
}
.history-jcxx-form .el-date-editor .el-range__icon,
.history-jcxx-form .el-date-editor .el-range__close-icon {
line-height: 22px !important;
}
.jc-update-dialog .el-dialog {
max-width: calc(100vw - 32px);
}
.jc-update-form .el-row .el-col {
min-height: 40px;
}
.jc-update-form .el-row .el-col > label {
flex: 0 0 96px;
width: 96px !important;
min-width: 96px;
white-space: nowrap;
word-break: keep-all;
text-align: center;
line-height: 20px;
margin-right: 0;
}
.jc-update-form .el-row .el-col > .el-input,
.jc-update-form .el-row .el-col > .el-select {
flex: 1 1 0;
min-width: 0;
}
.jc-update-room {
display: flex;
align-items: center;
}
.jc-update-room > .el-button {
margin-left: 15px;
}
.history-jcxx-table th.el-table__cell {
padding-top: 5px !important;
padding-bottom: 5px !important;
}
.history-jcxx-table td.el-table__cell {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.history-jcxx-load-tip {
padding: 8px 0 2px;
text-align: center;
color: #909399;
font-size: 12px;
}
.history-jcxx-table .cell {
line-height: 1.15 !important;
}
.leave-snapshot-preview {
display: flex;
justify-content: center;
min-height: 180px;
}
.leave-snapshot-preview img {
max-width: 100%;
max-height: 560px;
object-fit: contain;
}
.leave-snapshot-empty {
padding: 60px 0;
text-align: center;
color: #909399;
}
.item1 {
margin-top: 10px;
}
.bottom-right-div {
width: 200px;
height: 200px;
position: absolute;
bottom: 20px;
right: 20px;
padding: 10px;
font-size: 20px;
background-color: #a6b8ac;
border-radius: 10px;
}
.Dialog_JcEvent .el-dialog {
position: fixed;
top: -100px;
left: 100px;
background-color: #dee2e6;
}
.Dialog_AlarmEvent .el-dialog {
background-color: #cc7b10;
}
.buttonFont {
font-size: 20px;
font-weight: 600;
}
body {
padding: 0 0 0 0;
}
.app_contianer {
padding: 0 0 0 0;
}
.bottom-duty-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #029191;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1000;
}
.bottom-stay-summary {
display: flex;
align-items: center;
gap: 80px;
padding-left: 28px;
}
.bottom-stay-summary,
.latest-sync-time {
color: #fff45c;
font-size: 24px;
font-weight: 800;
line-height: 50px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
white-space: nowrap;
}
.latest-sync-time {
position: absolute;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.bottom-duty-info {
flex: 1;
text-align: right;
color: #ffffff;
}
.el-table__body tr.current-row > td {
background-color: #1990f1 !important;
color: #ffffff;
}
.history-jcxx-table .el-table__body tr.current-row > td {
background-color: #0b7dda !important;
color: #ffffff !important;
}
.history-jcxx-table.el-table--striped .el-table__body tr.current-row > td,
.history-jcxx-table.el-table--striped .el-table__body tr.current-row.el-table__row--striped > td {
background-color: #0b7dda !important;
color: #ffffff !important;
}
.red-mask {
background-color: red;
}
.record-button-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.record-button {
margin-left: 0 !important;
width: 58px;
padding-left: 0 !important;
padding-right: 0 !important;
}
/* 合并后的交路及叫乘提醒。 */
.sync-warning-dialog .el-dialog {
min-width: 620px;
border-radius: 8px;
}
.sync-warning-dialog .el-dialog__header {
background: #fff7e6;
border-bottom: 1px solid #f5c26b;
text-align: center;
}
.sync-warning-dialog .el-dialog__title {
color: #b45309;
font-size: 22px;
font-weight: 700;
}
.sync-warning-content {
max-height: 56vh;
overflow-y: auto;
}
.sync-warning-section + .sync-warning-section {
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid #ebeef5;
}
.sync-warning-category {
margin-bottom: 8px;
color: #b45309;
font-size: 18px;
font-weight: 700;
}
.sync-warning-category i {
margin-right: 6px;
}
.sync-warning-item {
padding: 5px 0 5px 26px;
color: #303133;
font-size: 17px;
line-height: 1.55;
}
/* ============ 优化按钮样式 ============ */
.el-button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
}
.el-button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.4) !important;
}
.el-button:active {
transform: translateY(0) !important;
}
/* 查询按钮特殊样式 */
.el-button[style*=linear-gradient]:hover {
background: linear-gradient(135deg, #66b3ff, #409EFF) !important;
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.5) !important;
}
.el-button[style*=linear-gradient]:before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
.el-button[style*=linear-gradient]:hover:before {
left: 100%;
}
/* 响应式按钮优化 */
@media (max-width: 768px) {
.el-button[style*=margin-left] {
margin-left: 20px !important;
width: 120px;
}
}
.person-photo-panel {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 32px;
}
.person-photo-item {
width: 180px;
text-align: center;
}
.person-photo-title {
height: 24px;
line-height: 24px;
color: #606266;
font-size: 14px;
margin-bottom: 6px;
}
.person-photo-img {
width: 150px;
height: 200px;
object-fit: cover;
border: 1px solid #dcdfe6;
background: #f5f7fa;
}
.jl-add-dialog .el-dialog {
max-width: calc(100vw - 32px);
}
.jl-add-form .el-row .el-col > label,
.jl-add-form .el-row .el-col > div > label {
flex: 0 0 96px;
width: 96px !important;
min-width: 96px;
white-space: nowrap;
word-break: keep-all;
text-align: center;
line-height: 20px;
margin-right: 0;
}
.jl-add-form .el-row .el-col > .el-input,
.jl-add-form .el-row .el-col > .el-select,
.jl-add-form .el-row .el-col > .el-date-editor,
.jl-add-form .el-row .el-col > div > .el-input,
.jl-add-form .el-row .el-col > div > .el-select,
.jl-add-form .el-row .el-col > div > .el-date-editor {
flex: 1 1 0;
}
.jl-add-form .el-row .el-col > div {
width: 100%;
}
.jl-add-form .el-row .el-col > div > .el-input,
.jl-add-form .el-row .el-col > div > .el-select,
.jl-add-form .el-row .el-col > div > .el-date-editor,
.jl-add-form .el-row .el-col > div > .el-time-picker {
width: 100% !important;
min-width: 0;
}
.jl-add-form .el-row .el-col > div > .el-date-editor .el-input__inner {
width: 100%;
}</style>
<style>body{margin:0;background:#f3f6f5}#app{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei",Arial,sans-serif}
/* 快照专用:模拟 el-table 运行时的内部横向滚动,避免 15 列被静态表格压扁 */
.main-jcxx-table .el-table__header-wrapper,
.main-jcxx-table .el-table__body-wrapper{overflow-x:auto}
.main-jcxx-table table{min-width:1500px}
</style>
</head><body><div class="app_contianer jc-opt-root" id="app">
<div class="el-card"><div class="el-card__body">
<div class="el-row jc-header-row" style="height:150px;margin-left:-7.5px;margin-right:-7.5px">
<div class="el-col el-col-9 jc-header-search" style="height:100%;display:flex;flex-direction:column;justify-content:center;padding-left:7.5px;padding-right:7.5px">
<div class="el-row top-left-marquee-row" style="height:30%;margin-top:40px">
<div class="marquee-container" style="background-color:rgb(2,145,145);height:100%">
<div class="marquee-content"><span>305 房间叫乘时间已过,请值班员关注</span><span class="marquee-repeat-text">305 房间叫乘时间已过,请值班员关注</span></div>
</div>
</div>
<div class="top-left-search-area" style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:10px 20px">
<div class="el-row top-left-input-row" style="margin-bottom:15px"><div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
<div class="form-item-container"><label class="form-label">机车号</label>
<div class="el-input"><input class="el-input__inner" placeholder="请输入机车号" id="q0"></div></div></div><div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
<div class="form-item-container"><label class="form-label">车次</label>
<div class="el-input"><input class="el-input__inner" placeholder="请输入车次" id="q1"></div></div></div><div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
<div class="form-item-container"><label class="form-label">房间号</label>
<div class="el-input"><input class="el-input__inner" placeholder="请输入房间号" id="q2"></div></div></div><div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
<div class="form-item-container"><label class="form-label">姓名</label>
<div class="el-input"><input class="el-input__inner" placeholder="请输入姓名" id="q3"></div></div></div></div>
<div class="el-row top-left-action-row">
<div class="el-col el-col-24" style="display:flex;justify-content:space-between;align-items:center">
<button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;"><span class="buttonFont-small">取消叫乘</span></button>
<div style="display:flex;align-items:center;gap:12px"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;"><span class="buttonFont-small">查询</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #909399, #b1b3b8); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(144,147,153,.3); color: white;"><span class="buttonFont-small">重置</span></button></div>
</div>
</div>
</div>
</div>
<div class="el-col el-col-6 jc-header-time" style="height:100%;padding-left:7.5px;padding-right:7.5px">
<div class="el-row"><div class="center-time-section"><div class="date-time">
<p class="text-24 text-white">2026-11-09 星期一<br>09:03:21</p>
</div></div></div>
</div>
<div class="el-col el-col-9 jc-header-actions" style="height:100%;padding-left:7.5px;padding-right:7.5px">
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
<div class="el-col el-col-18"><div style="display:flex;gap:5px;justify-content:space-between;align-items:center"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;height:32px;"><span class="buttonFont-small">房间信息</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;height:32px;"><span class="buttonFont-small">设备信息</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;height:32px;"><span class="buttonFont-small">人员维护</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;height:32px;"><span class="buttonFont-small">交路管理</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;height:32px;"><span class="buttonFont-small">历史数据</span></button></div></div>
<div class="el-col el-col-1"></div>
<div class="el-col el-col-5"><div style="display:flex;align-items:center;gap:3px">
<span class="jc-user-label" style="font-size:12px;color:#666;width:45%">当前用户: 张值班</span>
<button class="el-button el-button--small" style="background: linear-gradient(135deg, #f56c6c, #ff7979); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(245,108,108,.3); color: white;height:32px;width:55%;"><span class="buttonFont-small">退出</span></button>
</div></div>
</div>
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
<div class="el-col el-col-7"><div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;width:84px;"><span class="buttonFont-small">换乘</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;width:84px;"><span class="buttonFont-small">出乘</span></button></div></div>
<div class="el-col el-col-17"><div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-start;margin-left:14px"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;"><span class="buttonFont-small">语音临叫</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;"><span class="buttonFont-small">喊话临叫</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;"><span class="buttonFont-small">停止呼叫</span></button><button class="el-button el-button--small" style="background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;height:32px;"><span class="buttonFont-small">停止报警</span></button></div></div>
</div>
</div>
</div>
</div></div>
<div class="el-card" style="margin-top:10px"><div class="el-card__body">
<div class="el-tabs el-tabs--card"><div class="el-tabs__header is-top"><div class="el-tabs__nav-wrap"><div class="el-tabs__nav-scroll">
<div class="el-tabs__nav is-top"><div class="el-tabs__item is-top is-active">机务人员和客运人员</div><div class="el-tabs__item is-top">其他人员</div></div>
</div></div></div></div>
<div class="el-table main-jcxx-table el-table--border" style="width:100%;height:600px">
<div class="el-table__header-wrapper"><table class="el-table__header" style="width:100%"><thead><tr style="height:54px"><th class="el-table__cell"><div class="cell">序号</div></th><th class="el-table__cell"><div class="cell">状态</div></th><th class="el-table__cell"><div class="cell">到寓时间</div></th><th class="el-table__cell"><div class="cell">单位</div></th><th class="el-table__cell"><div class="cell">机车号</div></th><th class="el-table__cell"><div class="cell">姓名</div></th><th class="el-table__cell"><div class="cell">房间号</div></th><th class="el-table__cell"><div class="cell">出发车次</div></th><th class="el-table__cell"><div class="cell">开车时间</div></th><th class="el-table__cell"><div class="cell">叫乘时间</div></th><th class="el-table__cell"><div class="cell">复叫时间</div></th><th class="el-table__cell"><div class="cell">叫乘日期</div></th><th class="el-table__cell"><div class="cell">应离寓时间</div></th><th class="el-table__cell"><div class="cell">接车方式</div></th><th class="el-table__cell"><div class="cell">操作</div></th></tr></thead></table></div>
<div class="el-table__body-wrapper" style="height:546px"><table class="el-table__body" style="width:100%"><tbody><tr class="el-table__row current-row" style="background-color:#F56C6C;font-size:20px;font-weight:600"><td class="el-table__cell"><div class="cell">1</div></td><td class="el-table__cell"><div class="cell"><span class="jc-st jc-st-alarm">叫乘报警</span></div></td><td class="el-table__cell"><div class="cell">11-08 21:40</div></td><td class="el-table__cell"><div class="cell">机务段</div></td><td class="el-table__cell"><div class="cell">HXD3C-0131</div></td><td class="el-table__cell"><div class="cell">冯建国</div></td><td class="el-table__cell"><div class="cell">305</div></td><td class="el-table__cell"><div class="cell">K671</div></td><td class="el-table__cell"><div class="cell">08:35</div></td><td class="el-table__cell"><div class="cell">06:35</div></td><td class="el-table__cell"><div class="cell">06:45</div></td><td class="el-table__cell"><div class="cell">11-09</div></td><td class="el-table__cell"><div class="cell">07:05</div></td><td class="el-table__cell"><div class="cell">站接</div></td><td class="el-table__cell"><div class="cell"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;height:28px;"><span class="buttonFont-small">暂停报警</span></button></div></td></tr><tr class="el-table__row" style="background-color:#00FF00;font-size:20px;font-weight:600"><td class="el-table__cell"><div class="cell">2</div></td><td class="el-table__cell"><div class="cell"><span class="jc-st jc-st-normal">正在呼叫</span></div></td><td class="el-table__cell"><div class="cell">11-08 22:05</div></td><td class="el-table__cell"><div class="cell">机务段</div></td><td class="el-table__cell"><div class="cell">HXD3C-0132</div></td><td class="el-table__cell"><div class="cell">蒋卫东</div></td><td class="el-table__cell"><div class="cell">517</div></td><td class="el-table__cell"><div class="cell">T118</div></td><td class="el-table__cell"><div class="cell">09:10</div></td><td class="el-table__cell"><div class="cell">07:10</div></td><td class="el-table__cell"><div class="cell">07:20</div></td><td class="el-table__cell"><div class="cell">11-09</div></td><td class="el-table__cell"><div class="cell">07:40</div></td><td class="el-table__cell"><div class="cell">库出</div></td><td class="el-table__cell"><div class="cell"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;height:28px;"><span class="buttonFont-small">暂停报警</span></button></div></td></tr><tr class="el-table__row" style="background-color:#FFF200;font-size:20px;font-weight:600"><td class="el-table__cell"><div class="cell">3</div></td><td class="el-table__cell"><div class="cell"><span class="jc-st jc-st-normal">待叫乘</span></div></td><td class="el-table__cell"><div class="cell">11-08 22:20</div></td><td class="el-table__cell"><div class="cell">客运段</div></td><td class="el-table__cell"><div class="cell">HXD3C-0133</div></td><td class="el-table__cell"><div class="cell">李红军</div></td><td class="el-table__cell"><div class="cell">206</div></td><td class="el-table__cell"><div class="cell">Z267</div></td><td class="el-table__cell"><div class="cell">10:05</div></td><td class="el-table__cell"><div class="cell">08:05</div></td><td class="el-table__cell"><div class="cell">08:15</div></td><td class="el-table__cell"><div class="cell">11-09</div></td><td class="el-table__cell"><div class="cell">08:35</div></td><td class="el-table__cell"><div class="cell">站接</div></td><td class="el-table__cell"><div class="cell"><button class="el-button el-button--small" style="background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;height:28px;"><span class="buttonFont-small">暂停报警</span></button></div></td></tr><tr class="el-table__row" style="background-color:#B3E19D;font-size:20px;font-weight:600"><td class="el-table__cell"><div class="cell">4</div></td><td class="el-table__cell"><div class="cell"><span class="jc-st jc-st-normal">待离寓</span></div></td><td class="el-table__cell"><div class="cell">11-08 20:15</div></td><td class="el-table__cell"><div class="cell">机务段</div></td><td class="el-table__cell"><div class="cell">HXD3C-0134</div></td><td class="el-table__cell"><div class="cell">王志强</div></td><td class="el-table__cell"><div class="cell">408</div></td><td class="el-table__cell"><div class="cell">K42</div></td><td class="el-table__cell"><div class="cell">07:40</div></td><td class="el-table__cell"><div class="cell">05:40</div></td><td claLine truncated
</div>
</div></div>
<div class="bottom-duty-bar">
<div class="bottom-stay-summary"><span>在寓班次:14</span><span>在寓人次:30</span></div>
<div class="latest-sync-time" role="status">最新同步时间:09:03:18</div>
<div class="bottom-duty-info">
<button class="el-button el-button--small" style="background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;position:relative;right:20px;top:2px;"><span class="buttonFont-small">交接班</span></button>
<span style="font-size:26px;margin-right:80px;position:relative;top:6px">值班员:张值班</span>
</div>
</div>
<div class="bottom-right-div" id="notice">有新的交路信息,请及时处理!</div>
<!-- 宽弹窗骨架:默认隐藏,验收时按需显示以测量视口约束 -->
<div class="el-dialog__wrapper jc-dialog jc-wide-dialog" id="wideWrap" style="display:none">
<div class="el-dialog" style="width:1800px"><div class="el-dialog__header"><span class="el-dialog__title">历史叫乘信息</span></div>
<div class="el-dialog__body"><div class="el-form history-jcxx-form"><div class="history-jcxx-form-row">
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">机车号</label><div class="el-input"><input class="el-input__inner"></div></div>
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">车次</label><div class="el-input"><input class="el-input__inner"></div></div>
</div></div></div>
</div>
</div>
</div></body></html>
File diff suppressed because it is too large. Load diff
+761
View File
@@ -0,0 +1,761 @@
.marquee-container {
white-space: nowrap;
overflow: hidden;
}
.top-left-marquee-row {
margin-top: 10px;
margin-bottom: 4px;
}
.top-left-search-area {
padding-top: 6px !important;
}
.top-left-input-row {
margin-top: -2;
}
.top-left-action-row {
margin-top: -2;
}
/* 中间时间信息区域 */
.center-time-section {
flex: 0 0 auto;
background: linear-gradient(135deg, #0078d7, #107c10);
color: white;
padding: 12px 16px;
border-radius: 8px;
margin: 0 15px;
min-width: 320px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.date-time {
display: flex;
flex-direction: column;
align-items: center;
font-size: 16px;
}
.date-time .text-24 {
font-size: 26px !important;
font-weight: 600;
margin: 0;
line-height: 1.2;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.center-time-section {
min-width: 280px;
padding: 10px 14px;
}
.date-time .text-24 {
font-size: 24px !important;
}
.date-time {
font-size: 14px;
}
}
@media (max-width: 768px) {
.center-time-section {
min-width: 240px;
padding: 8px 12px;
margin: 0 10px;
}
.date-time .text-24 {
font-size: 20px !important;
}
.date-time {
font-size: 12px;
}
}
/* ============ 右下角初始化提示 ============ */
.init-success-tip {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 2000;
background: linear-gradient(135deg, #67c23a, #85ce61);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(103, 194, 58, 0.3);
animation: slideInUp 0.6s ease-out;
backdrop-filter: blur(10px);
}
.init-success-content {
display: flex;
align-items: center;
padding: 12px 20px;
color: white;
font-size: 14px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.init-success-content i {
font-size: 18px;
margin-right: 8px;
animation: successPulse 2s infinite;
}
@keyframes successPulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* ============ 表单样式(缩小版) ============ */
.form-item-container {
padding: 0 8px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-label {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 6px;
display: block;
text-align: center;
}
.form-item-container .el-input {
width: 100%;
}
.form-item-container .el-input__inner {
border-radius: 6px !important;
border: 1px solid #dcdfe6 !important;
padding: 8px 12px !important;
font-size: 13px !important;
transition: all 0.3s ease !important;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
height: 32px !important;
}
.form-item-container .el-input__inner:focus {
border-color: #409EFF !important;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1) !important;
}
.form-item-container .el-input__inner:hover {
border-color: #c0c4cc !important;
}
/* 按钮行样式(缩小版)*/
.el-button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
height: 32px !important;
font-size: 13px !important;
}
.el-button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15) !important;
}
.el-button:active {
transform: translateY(0) !important;
}
.buttonFont-small {
font-size: 13px;
font-weight: 600;
position: relative;
z-index: 2;
}
/* 响应式表单设置 */
@media (max-width: 1200px) {
.form-label {
font-size: 13px;
}
.form-item-container .el-input__inner {
padding: 7px 10px !important;
font-size: 12px !important;
height: 30px !important;
}
.init-success-tip {
bottom: 20px;
right: 20px;
}
.init-success-content {
padding: 10px 16px;
font-size: 13px;
}
}
@media (max-width: 768px) {
.form-item-container {
padding: 0 4px;
}
.form-label {
font-size: 12px;
margin-bottom: 4px;
}
.form-item-container .el-input__inner {
padding: 6px 8px !important;
font-size: 11px !important;
height: 28px !important;
}
.el-button {
font-size: 11px !important;
height: 28px !important;
padding: 4px 12px !important;
}
.buttonFont-small {
font-size: 11px;
}
.init-success-tip {
bottom: 15px;
right: 15px;
}
.init-success-content {
padding: 8px 12px;
font-size: 12px;
}
.init-success-content i {
font-size: 14px;
margin-right: 6px;
}
}
.marquee-content {
display: inline-block;
animation: marquee 28s linear infinite;
padding-left: 100%;
color: #fff45c;
font-size: 22px;
font-weight: 800;
line-height: 36px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
letter-spacing: 0;
white-space: nowrap;
}
.marquee-repeat-text {
margin-left: 160px;
}
@keyframes warningBlink {
0%, 100% {
opacity: 1;
background-color: #FFF200;
}
50% {
opacity: 1;
background-color: transparent;
}
}
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-200%);
}
}
.el-table__body tr:hover > td {
background-color: inherit !important;
}
.main-jcxx-table th.el-table__cell {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
.main-jcxx-table td.el-table__cell {
padding-top: 5px !important;
padding-bottom: 5px !important;
}
.main-jcxx-table .cell {
line-height: 1.2 !important;
}
.history-jcxx-form-row {
display: flex;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
width: 100%;
column-gap: 6px;
}
.history-jcxx-form-row:first-child {
column-gap: 2px;
}
.history-jcxx-field-box {
width: 226px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-inline-label {
width: 58px;
padding-right: 8px;
font-size: 13px;
line-height: 30px;
text-align: right;
white-space: nowrap;
color: #606266;
box-sizing: border-box;
flex: 0 0 auto;
}
.history-jcxx-field-box .el-input {
width: 146px;
}
.history-jcxx-form-row + .history-jcxx-form-row {
margin-top: 2px;
}
.history-jcxx-query-button {
margin-left: 4px;
flex: 0 0 auto;
}
.history-jcxx-filter-box {
width: 230px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-filter-label {
width: 72px;
}
.history-jcxx-filter-box .el-select {
width: 140px;
}
.history-jcxx-time-box {
width: 610px;
display: flex;
align-items: center;
flex: 0 0 auto;
}
.history-jcxx-time-label {
width: 60px;
}
.history-jcxx-time-range {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
width: 540px;
}
.history-jcxx-count {
min-width: 180px;
margin-left: 8px;
flex: 0 0 auto;
}
.history-jcxx-form .el-input__inner,
.history-jcxx-form .el-date-editor.el-input__inner,
.history-jcxx-form .el-date-editor .el-range-input,
.history-jcxx-form .el-date-editor .el-range-separator {
font-size: 12px !important;
}
.history-jcxx-form .el-input__inner,
.history-jcxx-form .el-date-editor.el-input__inner,
.history-jcxx-form .el-date-editor--datetimerange {
height: 30px !important;
}
.history-jcxx-form .el-date-editor .el-range__icon,
.history-jcxx-form .el-date-editor .el-range__close-icon {
line-height: 22px !important;
}
.jc-update-dialog .el-dialog {
max-width: calc(100vw - 32px);
}
.jc-update-form .el-row .el-col {
min-height: 40px;
}
.jc-update-form .el-row .el-col > label {
flex: 0 0 96px;
width: 96px !important;
min-width: 96px;
white-space: nowrap;
word-break: keep-all;
text-align: center;
line-height: 20px;
margin-right: 0;
}
.jc-update-form .el-row .el-col > .el-input,
.jc-update-form .el-row .el-col > .el-select {
flex: 1 1 0;
min-width: 0;
}
.jc-update-room {
display: flex;
align-items: center;
}
.jc-update-room > .el-button {
margin-left: 15px;
}
.history-jcxx-table th.el-table__cell {
padding-top: 5px !important;
padding-bottom: 5px !important;
}
.history-jcxx-table td.el-table__cell {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.history-jcxx-load-tip {
padding: 8px 0 2px;
text-align: center;
color: #909399;
font-size: 12px;
}
.history-jcxx-table .cell {
line-height: 1.15 !important;
}
.leave-snapshot-preview {
display: flex;
justify-content: center;
min-height: 180px;
}
.leave-snapshot-preview img {
max-width: 100%;
max-height: 560px;
object-fit: contain;
}
.leave-snapshot-empty {
padding: 60px 0;
text-align: center;
color: #909399;
}
.item1 {
margin-top: 10px;
}
.bottom-right-div {
width: 200px;
height: 200px;
position: absolute;
bottom: 20px;
right: 20px;
// background-color: #255e39;
padding: 10px;
font-size: 20px;
background-color: #a6b8ac;
border-radius: 10px;
}
.Dialog_JcEvent .el-dialog {
position: fixed;
top: -100px;
left: 100px;
background-color: rgb(222, 226, 230);
}
.Dialog_AlarmEvent .el-dialog {
background-color: rgb(204, 123, 16);
}
.buttonFont {
font-size: 20px;
font-weight: 600;
}
body {
padding: 0 0 0 0 ;
}
.app_contianer {
padding: 0 0 0 0;
}
.bottom-duty-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: rgb(2, 145, 145);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1000;
}
.bottom-stay-summary {
display: flex;
align-items: center;
gap: 80px;
padding-left: 28px;
}
.bottom-stay-summary,
.latest-sync-time {
color: #fff45c;
font-size: 24px;
font-weight: 800;
line-height: 50px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
white-space: nowrap;
}
.latest-sync-time {
position: absolute;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.bottom-duty-info {
flex: 1;
text-align: right;
color: #ffffff;
}
.el-table__body tr.current-row > td {
background-color: #1990f1 !important;
color: #ffffff;
}
.history-jcxx-table .el-table__body tr.current-row > td {
background-color: #0b7dda !important;
color: #ffffff !important;
}
.history-jcxx-table.el-table--striped .el-table__body tr.current-row > td,
.history-jcxx-table.el-table--striped .el-table__body tr.current-row.el-table__row--striped > td {
background-color: #0b7dda !important;
color: #ffffff !important;
}
.red-mask {
background-color: red;
}
.record-button-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.record-button {
margin-left: 0 !important;
width: 58px;
padding-left: 0 !important;
padding-right: 0 !important;
}
/* 合并后的交路及叫乘提醒。 */
.sync-warning-dialog .el-dialog {
min-width: 620px;
border-radius: 8px;
}
.sync-warning-dialog .el-dialog__header {
background: #fff7e6;
border-bottom: 1px solid #f5c26b;
text-align: center;
}
.sync-warning-dialog .el-dialog__title {
color: #b45309;
font-size: 22px;
font-weight: 700;
}
.sync-warning-content {
max-height: 56vh;
overflow-y: auto;
}
.sync-warning-section + .sync-warning-section {
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid #ebeef5;
}
.sync-warning-category {
margin-bottom: 8px;
color: #b45309;
font-size: 18px;
font-weight: 700;
}
.sync-warning-category i {
margin-right: 6px;
}
.sync-warning-item {
padding: 5px 0 5px 26px;
color: #303133;
font-size: 17px;
line-height: 1.55;
}
/* ============ 优化按钮样式 ============ */
.el-button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
}
.el-button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 20px rgba(64, 158, 255, 0.4) !important;
}
.el-button:active {
transform: translateY(0) !important;
}
/* 查询按钮特殊样式 */
.el-button[style*="linear-gradient"]:hover {
background: linear-gradient(135deg, #66b3ff, #409EFF) !important;
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.5) !important;
}
.el-button[style*="linear-gradient"]:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transition: left 0.5s ease;
}
.el-button[style*="linear-gradient"]:hover:before {
left: 100%;
}
/* 响应式按钮优化 */
@media (max-width: 768px) {
.el-button[style*="margin-left"] {
margin-left: 20px !important;
width: 120px;
}
}
.person-photo-panel {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 32px;
}
.person-photo-item {
width: 180px;
text-align: center;
}
.person-photo-title {
height: 24px;
line-height: 24px;
color: #606266;
font-size: 14px;
margin-bottom: 6px;
}
.person-photo-img {
width: 150px;
height: 200px;
object-fit: cover;
border: 1px solid #dcdfe6;
background: #f5f7fa;
}
.jl-add-dialog .el-dialog {
max-width: calc(100vw - 32px);
}
.jl-add-form .el-row .el-col > label,
.jl-add-form .el-row .el-col > div > label {
flex: 0 0 96px;
width: 96px !important;
min-width: 96px;
white-space: nowrap;
word-break: keep-all;
text-align: center;
line-height: 20px;
margin-right: 0;
}
.jl-add-form .el-row .el-col > .el-input,
.jl-add-form .el-row .el-col > .el-select,
.jl-add-form .el-row .el-col > .el-date-editor,
.jl-add-form .el-row .el-col > div > .el-input,
.jl-add-form .el-row .el-col > div > .el-select,
.jl-add-form .el-row .el-col > div > .el-date-editor {
flex: 1 1 0;
}
.jl-add-form .el-row .el-col > div {
width: 100%;
}
.jl-add-form .el-row .el-col > div > .el-input,
.jl-add-form .el-row .el-col > div > .el-select,
.jl-add-form .el-row .el-col > div > .el-date-editor,
.jl-add-form .el-row .el-col > div > .el-time-picker {
width: 100% !important;
min-width: 0;
}
.jl-add-form .el-row .el-col > div > .el-date-editor .el-input__inner {
width: 100%;
}
+393
View File
@@ -0,0 +1,393 @@
/* ============================================================================
* JcXx.optimized.vue 验收脚本
* 1) 真实 SFC 编译:vue-template-compiler / @babel/parser / sass
* 2) 功能集合零变化:script 哈希 + 模板指令/列/按钮/弹窗集合比对
* 3) 优化前 / 优化后同一份静态 DOM + 两份真实 CSS,多视口截图与布局断言
* 用法:node verify_jcxx.cjs
* ==========================================================================*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ROOT = path.resolve(__dirname, '..', '..', '..');
const VUE = path.join(ROOT, 'YktVue');
const SRC = path.join(VUE, 'src', 'views', 'JcXx.vue');
const OUT = path.join(VUE, 'src', 'views', 'JcXx.optimized.vue');
const UI = path.join(ROOT, 'docs', 'frontend', 'ui');
const req = p => require(path.join(VUE, 'node_modules', p));
const compiler = req('vue-template-compiler');
const babel = req('@babel/parser');
const sass = req('sass');
let chromium;
const PW = 'C:/Users/zfjia/AppData/Roaming/npm/node_modules/@playwright/cli/node_modules/playwright';
const EXE = 'C:/Users/zfjia/AppData/Local/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-win64/chrome-headless-shell.exe';
try { ({ chromium } = require(PW)); } catch (e) { ({ chromium } = require('playwright')); }
const beforeVue = fs.readFileSync(SRC, 'utf8');
const afterVue = fs.readFileSync(OUT, 'utf8');
function parts(s) {
// 注意:模板内部有大量 <template slot-scope>,必须取最后一个 </template>
const a = s.indexOf('<template>') + '<template>'.length;
const b = s.lastIndexOf('</template>');
const c = s.indexOf('<script>'), d = s.indexOf('</script>') + 9;
const e = s.indexOf('<style'), f = s.indexOf('</style>');
return {
template: s.slice(a, b),
script: s.slice(c, d),
scriptBody: s.slice(s.indexOf('>', c) + 1, d - 9),
style: s.slice(s.indexOf('>', e) + 1, f)
};
}
const B = parts(beforeVue), A = parts(afterVue);
const sha = s => crypto.createHash('sha256').update(s).digest('hex');
let pass = 0, fail = 0;
const results = [];
function T(name, cond, detail) {
if (cond) { pass++; results.push(' PASS ' + name); }
else { fail++; results.push(' FAIL ' + name + (detail ? ' ' + detail : '')); }
}
// ---------------------------------------------------------------- 1. 编译
const rcB = compiler.compile(B.template);
const rcA = compiler.compile(A.template);
T('SFC-01 优化前模板可由 vue-template-compiler 编译', rcB.errors.length === 0, JSON.stringify(rcB.errors));
T('SFC-02 优化后模板可由 vue-template-compiler 编译', rcA.errors.length === 0, JSON.stringify(rcA.errors));
let jsErr = '';
try {
babel.parse(A.scriptBody, {
sourceType: 'module',
plugins: ['objectRestSpread', 'dynamicImport', 'optionalChaining',
'nullishCoalescingOperator', 'classProperties']
});
} catch (e) { jsErr = e.message; }
T('SFC-03 优化后 <script> 通过 Babel 语法解析', !jsErr, jsErr);
const compileSass = scss => sass.renderSync({ data: scss, outputStyle: 'expanded' }).css.toString();
const cssB = compileSass(B.style);
const cssA = compileSass(A.style);
T('SFC-04 优化前 style 可通过 sass 编译', cssB.length > 10000);
T('SFC-05 优化后 style 可通过 sass 编译', cssA.length > cssB.length);
// ---------------------------------------------------------------- 2. 功能集合
T('FUN-01 <script> 段逐字节一致(功能/接口/定时器/状态机全部保留)',
B.script === A.script && sha(B.script) === sha(A.script));
function inventory(tpl) {
const pick = re => (tpl.match(re) || []).sort();
return {
events: pick(/@[a-zA-Z][\w.-]*(?:=|\s|>)/g).map(x => x.replace(/[=>\s]/g, '')).sort(),
models: pick(/v-model(?:\.\w+)?="[^"]+"/g),
binds: pick(/:[a-zA-Z][\w.-]*="[^"]+"/g),
refs: pick(/ref="[^"]+"/g),
slots: pick(/slot-scope="[^"]+"/g),
vif: pick(/v-(?:if|else-if|else|show|for|html|once)(?:="[^"]+")?/g),
cols: pick(/<el-table-column[^>]*?\b(?:prop|label)="[^"]+"/g)
.map(x => (x.match(/\bprop="([^"]+)"/) || [, ''])[1] + '|' +
(x.match(/\blabel="([^"]+)"/) || [, ''])[1]),
tags: ['el-dialog', 'el-table', 'el-table-column', 'el-button', 'el-input',
'el-select', 'el-form', 'el-form-item', 'el-tab-pane', 'el-checkbox',
'audio', 'el-result'].map(t => t + '=' + (tpl.match(new RegExp('<' + t + '\\b', 'g')) || []).length)
};
}
const ib = inventory(B.template), ia = inventory(A.template);
Object.keys(ib).forEach(k => {
if (k === 'events') {
// 唯一预期差异:tab_click -> tab-click(Element UI 实际派发 tab-click;处理函数体为空)
const nb = ib.events.map(x => x === '@tab_click' ? '@tab-click' : x).sort();
T('FUN-02 事件绑定集合一致(仅修正 tab_click 拼写)',
JSON.stringify(nb) === JSON.stringify(ia.events),
JSON.stringify({ before: ib.events.filter(x => !ia.events.includes(x)), after: ia.events.filter(x => !nb.includes(x)) }));
} else {
T('FUN-02 ' + k + ' 集合与数量一致',
JSON.stringify(ib[k]) === JSON.stringify(ia[k]),
JSON.stringify({ before: ib[k].length, after: ia[k].length }));
}
});
T('FUN-03 未新增/删除任何按钮、表格列、弹窗',
ib.tags.join('|') === ia.tags.join('|'), ib.tags.join('|'));
T('FUN-04 所有弹窗带统一 jc-dialog 作用域类名、宽弹窗带 jc-wide-dialog',
(A.template.match(/<el-dialog/g) || []).length === (A.template.match(/class="jc-dialog/g) || []).length &&
(A.template.match(/jc-wide-dialog/g) || []).length === 4);
T('FUN-05 原样式核心规则全部保留,优化层仅做追加与 4 处修正',
['.marquee-container', '.center-time-section', '.form-item-container',
'.bottom-duty-bar', '@keyframes marquee', '.history-jcxx-form-row',
'.sync-warning-content', '.person-photo-panel', '.jl-add-form']
.every(k => A.style.includes(k)) && cssA.length > cssB.length);
// ---------------------------------------------------------------- 3. 静态布局快照
const elementCss = fs.readFileSync(
path.join(VUE, 'node_modules', 'element-ui', 'lib', 'theme-chalk', 'index.css'), 'utf8');
const ORANGE = 'background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;';
const TEAL = 'background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;';
const BLUE = 'background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;';
const GREY = 'background: linear-gradient(135deg, #909399, #b1b3b8); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(144,147,153,.3); color: white;';
const RED = 'background: linear-gradient(135deg, #f56c6c, #ff7979); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(245,108,108,.3); color: white;';
const btn = (label, style, extra) =>
`<button class="el-button el-button--small" style="${style}${extra || ''}"><span class="buttonFont-small">${label}</span></button>`;
const COLS = ['序号', '状态', '到寓时间', '单位', '机车号', '姓名', '房间号', '出发车次',
'开车时间', '叫乘时间', '复叫时间', '叫乘日期', '应离寓时间', '接车方式', '操作'];
const ROWS = [
{ bg: '#F56C6C', st: 'jc-st-alarm', v: ['1', '叫乘报警', '11-08 21:40', '机务段', 'HXD3C-0131', '冯建国', '305', 'K671', '08:35', '06:35', '06:45', '11-09', '07:05', '站接'] },
{ bg: '#00FF00', st: 'jc-st-normal', v: ['2', '正在呼叫', '11-08 22:05', '机务段', 'HXD3C-0132', '蒋卫东', '517', 'T118', '09:10', '07:10', '07:20', '11-09', '07:40', '库出'] },
{ bg: '#FFF200', st: 'jc-st-normal', v: ['3', '待叫乘', '11-08 22:20', '客运段', 'HXD3C-0133', '李红军', '206', 'Z267', '10:05', '08:05', '08:15', '11-09', '08:35', '站接'] },
{ bg: '#B3E19D', st: 'jc-st-normal', v: ['4', '待离寓', '11-08 20:15', '机务段', 'HXD3C-0134', '王志强', '408', 'K42', '07:40', '05:40', '05:50', '11-09', '06:10', '库出'] },
{ bg: '', st: 'jc-st-muted', v: ['5', '已入寓', '11-09 01:10', '客运段', 'HXD3C-0135', '赵明亮', '612', 'K671', '14:20', '12:20', '12:30', '11-09', '12:50', '站接'] }
];
function makeBody() {
const search = ['机车号', '车次', '房间号', '姓名'].map((k, i) =>
`<div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
<div class="form-item-container"><label class="form-label">${k}</label>
<div class="el-input"><input class="el-input__inner" placeholder="请输入${k}" id="q${i}"></div></div></div>`).join('');
const topBtns = ['房间信息', '设备信息', '人员维护', '交路管理', '历史数据']
.map(x => btn(x, ORANGE, 'height:32px;')).join('');
const row2Btns = ['换乘', '出乘'].map(x => btn(x, TEAL, 'height:32px;width:84px;')).join('');
const row3Btns = ['语音临叫', '喊话临叫', '停止呼叫', '停止报警']
.map(x => btn(x, TEAL, 'height:32px;')).join('');
const ths = COLS.map(c => `<th class="el-table__cell"><div class="cell">${c}</div></th>`).join('');
const trs = ROWS.map((r, ri) => {
const tds = r.v.map((v, ci) => {
let inner = v;
if (ci === 1) inner = `<span class="jc-st ${r.st}">${v}</span>`;
return `<td class="el-table__cell"><div class="cell">${inner}</div></td>`;
});
tds.push(`<td class="el-table__cell"><div class="cell">${btn('暂停报警', BLUE, 'height:28px;')}</div></td>`);
return `<tr class="el-table__row${ri === 0 ? ' current-row' : ''}" style="background-color:${r.bg};font-size:20px;font-weight:600">${tds.join('')}</tr>`;
}).join('');
return `<div class="app_contianer jc-opt-root" id="app">
<div class="el-card"><div class="el-card__body">
<div class="el-row jc-header-row" style="height:150px;margin-left:-7.5px;margin-right:-7.5px">
<div class="el-col el-col-9 jc-header-search" style="height:100%;display:flex;flex-direction:column;justify-content:center;padding-left:7.5px;padding-right:7.5px">
<div class="el-row top-left-marquee-row" style="height:30%;margin-top:40px">
<div class="marquee-container" style="background-color:rgb(2,145,145);height:100%">
<div class="marquee-content"><span>305 房间叫乘时间已过,请值班员关注</span><span class="marquee-repeat-text">305 房间叫乘时间已过,请值班员关注</span></div>
</div>
</div>
<div class="top-left-search-area" style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:10px 20px">
<div class="el-row top-left-input-row" style="margin-bottom:15px">${search}</div>
<div class="el-row top-left-action-row">
<div class="el-col el-col-24" style="display:flex;justify-content:space-between;align-items:center">
${btn('取消叫乘', ORANGE)}
<div style="display:flex;align-items:center;gap:12px">${btn('查询', BLUE)}${btn('重置', GREY)}</div>
</div>
</div>
</div>
</div>
<div class="el-col el-col-6 jc-header-time" style="height:100%;padding-left:7.5px;padding-right:7.5px">
<div class="el-row"><div class="center-time-section"><div class="date-time">
<p class="text-24 text-white">2026-11-09 星期一<br>09:03:21</p>
</div></div></div>
</div>
<div class="el-col el-col-9 jc-header-actions" style="height:100%;padding-left:7.5px;padding-right:7.5px">
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
<div class="el-col el-col-18"><div style="display:flex;gap:5px;justify-content:space-between;align-items:center">${topBtns}</div></div>
<div class="el-col el-col-1"></div>
<div class="el-col el-col-5"><div style="display:flex;align-items:center;gap:3px">
<span class="jc-user-label" style="font-size:12px;color:#666;width:45%">当前用户: 张值班</span>
${btn('退出', RED, 'height:32px;width:55%;')}
</div></div>
</div>
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
<div class="el-col el-col-7"><div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start">${row2Btns}</div></div>
<div class="el-col el-col-17"><div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-start;margin-left:14px">${row3Btns}</div></div>
</div>
</div>
</div>
</div></div>
<div class="el-card" style="margin-top:10px"><div class="el-card__body">
<div class="el-tabs el-tabs--card"><div class="el-tabs__header is-top"><div class="el-tabs__nav-wrap"><div class="el-tabs__nav-scroll">
<div class="el-tabs__nav is-top"><div class="el-tabs__item is-top is-active">机务人员和客运人员</div><div class="el-tabs__item is-top">其他人员</div></div>
</div></div></div></div>
<div class="el-table main-jcxx-table el-table--border" style="width:100%;height:600px">
<div class="el-table__header-wrapper"><table class="el-table__header" style="width:100%"><thead><tr style="height:54px">${ths}</tr></thead></table></div>
<div class="el-table__body-wrapper" style="height:546px"><table class="el-table__body" style="width:100%"><tbody>${trs}</tbody></table></div>
</div>
</div></div>
<div class="bottom-duty-bar">
<div class="bottom-stay-summary"><span>在寓班次:14</span><span>在寓人次:30</span></div>
<div class="latest-sync-time" role="status">最新同步时间:09:03:18</div>
<div class="bottom-duty-info">
${btn('交接班', ORANGE, 'position:relative;right:20px;top:2px;')}
<span style="font-size:26px;margin-right:80px;position:relative;top:6px">值班员:张值班</span>
</div>
</div>
<div class="bottom-right-div" id="notice">有新的交路信息,请及时处理!</div>
<!-- 宽弹窗骨架:默认隐藏,验收时按需显示以测量视口约束 -->
<div class="el-dialog__wrapper jc-dialog jc-wide-dialog" id="wideWrap" style="display:none">
<div class="el-dialog" style="width:1800px"><div class="el-dialog__header"><span class="el-dialog__title">历史叫乘信息</span></div>
<div class="el-dialog__body"><div class="el-form history-jcxx-form"><div class="history-jcxx-form-row">
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">机车号</label><div class="el-input"><input class="el-input__inner"></div></div>
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">车次</label><div class="el-input"><input class="el-input__inner"></div></div>
</div></div></div>
</div>
</div>
</div>`;
}
function makeHtml(css, label) {
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<title>JcXx ${label}</title>
<style>${elementCss}</style>
<style>${css}</style>
<style>body{margin:0;background:#f3f6f5}#app{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei",Arial,sans-serif}
/* 快照专用:模拟 el-table 运行时的内部横向滚动,避免 15 列被静态表格压扁 */
.main-jcxx-table .el-table__header-wrapper,
.main-jcxx-table .el-table__body-wrapper{overflow-x:auto}
.main-jcxx-table table{min-width:1500px}
</style>
</head><body>${makeBody()}</body></html>`;
}
const pb = path.join(__dirname, 'preview-before.html');
const pa = path.join(__dirname, 'preview-after.html');
fs.writeFileSync(pb, makeHtml(cssB, '优化前'), 'utf8');
fs.writeFileSync(pa, makeHtml(cssA, '优化后'), 'utf8');
const URLB = require('url').pathToFileURL(pb).href;
const URLA = require('url').pathToFileURL(pa).href;
// ---------------------------------------------------------------- 4. 多视口断言
const VIEWPORTS = [
{ w: 1920, h: 1080 }, { w: 1440, h: 900 }, { w: 1366, h: 768 },
{ w: 1280, h: 800 }, { w: 1024, h: 768 }
];
const probe = () => {
const R = s => {
const e = document.querySelector(s);
if (!e) return null;
const b = e.getBoundingClientRect();
return { t: Math.round(b.top), b: Math.round(b.bottom), l: Math.round(b.left), r: Math.round(b.right), w: Math.round(b.width), h: Math.round(b.height) };
};
const root = document.querySelector('#app');
const bar = R('.bottom-duty-bar');
const wrap = document.querySelector('#wideWrap');
wrap.style.display = 'block';
const dlg = R('.jc-wide-dialog .el-dialog');
wrap.style.display = 'none';
return {
vw: window.innerWidth, vh: window.innerHeight,
docW: document.documentElement.scrollWidth,
header: R('.jc-header-row'), search: R('.top-left-search-area'),
leftCol: R('.jc-header-search'), time: R('.center-time-section'),
actions: R('.jc-header-actions'), topBtns: R('.jc-header-actions .el-col-18 > div'),
bar, left: R('.bottom-stay-summary'), mid: R('.latest-sync-time'), right: R('.bottom-duty-info'),
notice: R('.bottom-right-div'), table: R('.main-jcxx-table'),
dlg,
paddingBottom: parseFloat(getComputedStyle(root).paddingBottom) || 0,
headerH: Math.round(document.querySelector('.jc-header-row').getBoundingClientRect().height)
};
};
(async () => {
const browser = await chromium.launch({ executablePath: EXE, headless: true });
const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const beforeIssues = {}, afterIssues = {};
for (const v of VIEWPORTS) {
const tag = `${v.w}x${v.h}`;
for (const [kind, url, bucket] of [['before', URLB, beforeIssues], ['after', URLA, afterIssues]]) {
const p = await ctx.newPage();
await p.setViewportSize({ width: v.w, height: v.h });
const errs = [];
p.on('pageerror', e => errs.push(e.message));
await p.goto(url);
await p.waitForTimeout(120);
const m = await p.evaluate(probe);
const issues = [];
if (m.docW > m.vw + 1) issues.push('横向溢出 ' + m.docW + '>' + m.vw);
if (m.search.b > m.leftCol.b + 2) issues.push('顶部查询区被裁切');
if (m.time.r > m.vw + 1) issues.push('时间卡越界');
if (m.topBtns.r > m.actions.r + 2) issues.push('右上按钮组越界');
if (m.left.r > m.mid.l + 2 || m.mid.r > m.right.l + 2) issues.push('吸底三段重叠');
if (m.notice.b > m.bar.t + 2) issues.push('右下提示被吸底条压住');
if (m.paddingBottom < 50) issues.push('底部无留白内容被遮挡');
if (m.dlg.r > m.vw + 1) issues.push('宽弹窗横向越界');
if (errs.length) issues.push('JS 错误 ' + errs.join('|'));
bucket[tag] = issues;
const shot = path.join(UI, `JcXx-${kind === 'before' ? '优化前' : '优化后'}-${tag}.png`);
await p.screenshot({ path: shot, fullPage: false });
await p.close();
}
T(`LAY-${tag} 优化后无横向溢出/裁切/重叠/遮挡/弹窗越界`,
afterIssues[tag].length === 0, JSON.stringify(afterIssues[tag]));
results.push(` └ 优化前同视口问题:${beforeIssues[tag].length ? beforeIssues[tag].join(';') : '无'}`);
}
// ------------------------------------------------------------ 交互与状态反馈
const p = await ctx.newPage();
await p.setViewportSize({ width: 1920, height: 1080 });
await p.goto(URLA);
await p.waitForTimeout(120);
await p.hover('.main-jcxx-table .el-table__body tr:nth-child(3) td');
const hoverAfter = await p.evaluate(() =>
getComputedStyle(document.querySelector('.main-jcxx-table .el-table__body tr:nth-child(3) td')).backgroundImage);
T('FB-01 表格行 hover 有可见反馈(保留行内状态底色)',
hoverAfter !== 'none' && hoverAfter.includes('gradient'), hoverAfter);
const current = await p.evaluate(() => {
const td = document.querySelector('.main-jcxx-table tr.current-row > td');
const cs = getComputedStyle(td);
return { bg: cs.backgroundColor, img: cs.backgroundImage, trBg: td.parentElement.style.backgroundColor };
});
T('FB-02 选中行不再用纯蓝底抹掉报警行状态色',
current.bg !== 'rgb(25, 144, 241)' && current.img.includes('gradient'),
JSON.stringify(current));
const grad = await p.evaluate(() => {
const b = document.querySelector('.top-left-action-row .el-button');
return getComputedStyle(b).backgroundImage;
});
await p.hover('.top-left-action-row .el-button');
const gradHover = await p.evaluate(() =>
getComputedStyle(document.querySelector('.top-left-action-row .el-button')).backgroundImage);
T('FB-03 橙色按钮 hover 不再被强制刷成蓝色渐变',
gradHover === grad && gradHover.includes('230, 162, 60'), gradHover);
const shadowBefore = await p.evaluate(() =>
getComputedStyle(document.querySelector('#q0')).boxShadow);
await p.focus('#q0');
await p.waitForTimeout(160); // headless 下 :focus 样式重算 + .3s 过渡起效
const focusFeedback = await p.evaluate(() => {
void document.body.offsetHeight; // 强制同步重排后再取计算样式
const c = getComputedStyle(document.querySelector('#q0'));
return { shadow: c.boxShadow, outlineStyle: c.outlineStyle };
});
T('FB-04 键盘焦点产生可见反馈(光晕变化 + 轮廓)',
focusFeedback.shadow !== shadowBefore && focusFeedback.outlineStyle !== 'none',
JSON.stringify({ before: shadowBefore, after: focusFeedback }));
const stateWeight = await p.evaluate(() =>
getComputedStyle(document.querySelector('.main-jcxx-table .jc-st-alarm')).fontWeight);
T('FB-05 报警状态文字权重高于普通单元格', Number(stateWeight) >= 700, stateWeight);
const noticeCursor = await p.evaluate(() =>
getComputedStyle(document.querySelector('#notice')).cursor);
T('FB-06 右下角提示有可点击指针反馈', noticeCursor === 'pointer', noticeCursor);
T('FB-07 优化层含 prefers-reduced-motion 降级', cssA.includes('prefers-reduced-motion'));
await p.close();
await browser.close();
console.log('\n================ JcXx 优化版验收 ================');
console.log(results.join('\n'));
console.log('--------------------------------------------------');
const bTotal = Object.values(beforeIssues).reduce((n, x) => n + x.length, 0);
const aTotal = Object.values(afterIssues).reduce((n, x) => n + x.length, 0);
console.log(`优化前 5 个视口累计布局问题:${bTotal} 项`);
console.log(`优化后 5 个视口累计布局问题:${aTotal} 项`);
console.log(`断言:${pass}/${pass + fail} 通过`);
console.log(`截图:${VIEWPORTS.length * 2} 张 -> ${UI}`);
console.log('==================================================');
if (fail) process.exit(1);
})().catch(e => { console.error(e); process.exit(1); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

@@ -0,0 +1,279 @@
# 00 公寓端主要功能设计(需求基线)
> 文档编号:`docs/公寓端/00`。性质:需求基线(Design Only),不写业务代码、不建表改表、不执行 DDL / DML。
> 上位依据:`docs/10-架构优化方案-三端拆分.md` §3.2 / §3.4、§5.4 表 5-3、§7.8 表 7-4;现状依据:`docs/backend/*`、`docs/frontend/*`、`docs/11-既有数据库结构分析.md`。
> **落地权威表述**:`docs/设计变更/03-三端功能设计修订(CR-02).md`(覆盖范围、交互协议、交互数据、模块与数据表设计、关键流程变更);Asterisk 配置数据管理补遗见该文档 §10,两类叫乘流程见 §11,**人脸数据下发管理与注册链路改判见 §12 及 `docs/公寓端/10`**,人员入住作业流程见 §13 及 `docs/公寓端/11`,**消费、充值与菜品管理见 §14 及 `docs/公寓端/12`**,**三端一致性同步与平台统计分析见 §15 及 `docs/平台端/10`**。本文件定义"要做什么"及数据需求,不定义代码结构。**实施排期见 `docs/公寓端/13`**(非设计变更,只做排序、切分、对齐与衔接;阶段划分与验收口径仍以 `docs/10` §9、`docs/公寓端/07` 为准)。
表 0-1 文档信息
| 项 | 内容 |
| --- | --- |
| 文档编号 | `docs/公寓端/00` |
| 文档名称 | 公寓端主要功能设计(重新规划后的需求基线) |
| 适用范围 | 铁路行车公寓叫乘与一卡通系统·公寓端 |
| 关联文档 | `docs/公寓端/01`~`13`、`docs/平台端/01`~`10`、`docs/应用端/01`~`10`、`docs/10`、`docs/11`、CR-02(§12 / §13 / §14 / §15) |
| 约束条件 | ① 三端总体定位不变;② 平台不回调公寓;③ 公寓端与应用端同 jar、同库;④ 跨端 20 个接口编号不新增;⑤ 状态字段只有 `apt.duty.state` 一个写入口 |
| 交付对象 | 需求评审、三端详细设计、契约冻结与验收 |
表 0-2 修订记录
| 版本 | 日期 | 说明 |
| --- | --- | --- |
| v0.1 | 2026-11-07 | 首次形成:14 条公寓端职责的口语化罗列(末条截断) |
| v1.0 | 2026-11-09 | 补全末条上报范围;14 条编号化为 `A-01`~`A-14`、补主责端 / 协同端 / 本期判定;补文档信息与修订记录;指向 CR-02 作为落地权威 |
| v1.1 | 2026-11-09 | 新增 §3「Asterisk 配置数据管理模块」:分机表 / 呼叫记录表 / 录音文件表 / 注册在线状态表四类数据表的字段定义、增删改查与权限控制;补充 SIP 呼叫场景下分机注册离线自动纳入设备报警的业务规则(触发条件、级别、通知、留存)。新增注册表 `apt_sip_reg_status`(CR-02 补遗 T-17),公寓本地库业务表口径 58 → 59 张 |
| v1.2 | 2026-11-09 | 新增 `docs/公寓端/10` 人脸数据下发管理模块(`AF-01`~`AF-06`)并改判 A-02 注册链路(CR-C25):**签到终端发起注册申请并上传人脸 → 值班员在值班台完成人员注册 → 注册成功后人脸进入公寓服务系统 → 分别上传平台与下发公寓内设备**;撤销"平台确认后注册才生效"的旧前置,明确人员主数据照片与特征值允许跨端(CR-C24),抓拍 / 录音仍不出公寓。不新增业务表、不新增跨端编号 |
| v1.3 | 2026-11-10 | 新增 `docs/公寓端/11`「人员入住作业流程」(`CI-01`~`CI-05`)并细化 A-03 作业链路:注册与签到 → 入住申请(房间 / 交路)→ 值班员审核(可换房)→ #9 `CHECKIN` 提报与平台运安匹配 → #7 拉取叫乘任务进入叫乘。明确"已入住"为流程态 + 业务事实双重表达(**不新增 `gy_jcxx` 状态字段**)、申请单落 `apt_local_task` 补列(**不新增业务表,59 张不变**)、端内接口 N-38~N-47;权威登记见 CR-02 §13(CR-C26~CR-C30、F-K、验收 18~20) |
| v1.4 | 2026-11-10 | 新增 `docs/公寓端/12`「消费、充值与菜品管理」(`CF-01`~`CF-03`)并细化 A-04 作业链路:食堂选餐与消费终端加菜显示金额 → 查补贴 / 现金额度 → 补贴优先扣款、不足现金补足 → 费用上报公寓端 → #10 流水同步平台并落入人员账户;线下充值仅"扫码识别 + 管理员核对记账"、不接线上支付;食谱按阶段性批次发布、终端选单确认前不落库、远程点菜仅预留入口。明确一笔消费只一条流水、四条状态链互不替代、`knye` / `xjye` 双额度设计映射(Q-43)、**不新增业务表(59 张不变)、不新增跨端编号、不新增 `bizType`**;端内接口 N-48~N-58;权威登记见 CR-02 §14(CR-C31~CR-C35、F-L / F-M / F-N、验收 21~23) |
| v1.5 | 2026-11-10 | **三端一致性同步 + 平台统计分析**(CR-02 §15):本端已完成的平板叫乘 / 人脸签到 / 消费 / 菜品管理四项能力在应用端与平台端同步落地,三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**;#13 `ARCHIVE` 摘要 `payload` 扩展 `satisfaction{}` / `roomOccupancy{}` **分组计数**,供平台以公寓为单位的分类统计(评价情况 / 房间利用周转),**不新增 `bizType`、不新增上报接口、不接明细**。平台新增 `docs/平台端/10`「公寓统计分析与排名报表」,统计结果不回写本端任何业务状态。本端四项能力权威表述不变,跨端 20 编号、**59 张表**、`v2-draft`、平台零回调不变;**变更点与影响范围见 CR-02 §15 表 3-63** |
## 1. 定位
公寓端是**执行叫乘任务和消费、充值的现场**:管现场资源、现场事实、设备与媒体,负责具体任务的执行;主数据真源与账务主账在平台,值班员在界面上的人工干预必须经公寓端领域服务落地。
## 2. 功能需求(A-01~A-14)
表 0-3 需求清单(编号与三端归属以 CR-02 表 3-1 为准)
| 编号 | 需求 | 主责端 | 协同端 | 本期 |
| --- | --- | --- | --- | --- |
| A-01 | 从平台侧获取叫乘任务,上报叫乘结果 | 公寓端 | 平台端 | 是 |
| A-02 | 获取机务人员数据,注册新增人员(新增人脸数据) | 公寓端(**签到终端申请 + 值班员确认注册 + 本地入档 + 平台 / 设备分别同步**) | 平台端(跨公寓主档真源、接收同步回执)、应用端(值班台审核) | 是 |
| A-03 | 提交入住请求与离寓请求;作业链路见 `docs/公寓端/11`(`CI-01`~`CI-05`) | 公寓端(`apt-stay` 唯一执行域) | 应用端(值班台审核 / 房间调整)、平台端(运安匹配与归集) | 是 |
| A-04 | 获取补贴,接收消费设备刷卡请求,扣除本地补贴,上报至平台消费台账、本地充值、充值上报;与平台断网期间,充值只能本地使用;**完整作业链路见 `docs/公寓端/12`(`CF-01`~`CF-03`)** | 公寓端(离线受理、`apt.ledger.local` 唯一写入) | 平台端(余额主账 `platform-ledger`)、应用端(管理员充值核对 / 食谱发布) | 是 |
| A-05 | 设备状态上报:在线、离线、报警 | 公寓端 | 平台端 | 是 |
| A-06 | 公寓侧叫乘设备任务下发:SIP 呼叫(现有基于 Asterisk 执行)、平板叫乘 | 公寓端 | — | 是 |
| A-07 | 叫乘任务派发:按叫乘任务叫乘时间顺序派发;SIP 呼叫模式由 Asterisk 组织派发并发起呼叫,平板叫乘形成叫乘任务数据、由平板拉取 | 公寓端 | — | 是 |
| A-08 | 叫乘任务状态:SIP 呼叫模式由 AMI 事件执行情况推断状态变迁;平板叫乘方式由平板上报叫乘结果(人员、刷脸、未刷脸等) | 公寓端 | 应用端 | 是 |
| A-09 | 换乘:值班员对交路可指定其换乘的开车时间、叫乘时间,换乘可能涉及需要更换房间 | 公寓端(重算与联动) | 应用端、平台端 | 是 |
| A-10 | 报警功能:叫乘报警、离寓报警(设备报警按 A-05 单列) | 公寓端 | 应用端 | 是 |
| A-11 | 公寓房间管理:房间基础台账、房间状态更新、房间入住人员情况查看 | 公寓端 | 平台端 | 是 |
| A-12 | 保洁管理:保洁人员管理、保洁记录管理 | 公寓端 | 应用端 | 是 |
| A-13 | 值班员管理:值班员排班、值班员盯控质量考核 | 应用端 | 公寓端、平台端 | 是 |
| A-14 | 数据上报管理:上报历史、续报(断网提交失败后再次报送);上报范围包括入住情况、消费情况、充值情况,并补齐叫乘结果、设备状态、报警与归档摘要 | 公寓端 | 平台端 | 是 |
> v0.1 末条原文止于"…充值情况,",v1.0 按上下文补全;补全部分不改原意,只把范围列全。
> **§3 是 A-05 / A-06 / A-08 的细化**,不新增 `A-15` 以后编号,14 项需求基线保持不变;细化项用 `AC-01`~`AC-05` 编号。
## 3. Asterisk 配置数据管理模块(A-05 / A-06 / A-08 细化)
> 现状依据:`docs/backend/10` 表 1-1「分机没有独立台账」(分机只以 `GY_ROOMS.sip_account` 单列存在,`sip_status` 从未被写入);目标设计见 `docs/10` §5.4 表 5-3(语音域 11 张表)、§7.8 表 7-4(分机资源状态机)。
> 术语沿用三端分册:表名一律小写;写入口用 `apt.voice.endpoint` / `apt.voice.event` / `apt.voice.media` / `apt.voice.ami` 模块名;权限点沿用 `apt:voice:*` 族;字段类型用 `apartment/db/mysql/*.sql` 的 MySQL 口径;枚举一律大写下划线字符串,不用数字码、不用 MySQL `ENUM`。
### 3.1 模块定位与数据表
表 0-4 模块功能项(`AC-01`~`AC-05`)
| 编号 | 功能项 | 主要数据表 | 唯一写入模块 | 需求来源 |
| --- | --- | --- | --- | --- |
| AC-01 | 分机表管理:分机台账的增删改停、房间/值班台/广播绑定、期望态收敛与对账 | `apt_sip_endpoint`、`apt_sip_binding`、`apt_trunk` | `apt.voice.endpoint` | A-06 |
| AC-02 | 呼叫记录表管理:呼叫会话、原始 AMI 事件流水、终态判定与归档 | `apt_call_session`、`apt_call_event`、`apt_call_event_his` | `apt.voice.event` / `apt.voice.ami` | A-08 |
| AC-03 | 录音文件表管理:录音索引、短期签名回放、留存与清理 | `apt_recording` | `apt.voice.media` | A-08 |
| AC-04 | 注册在线状态表管理:分机注册状态当前态与变迁流水、探测与恢复 | `apt_sip_endpoint.register_status`、**`apt_sip_reg_status`(新增)** | `apt.voice.endpoint`(当前态)/ `apt.voice.event`(流水) | A-05、A-06 |
| AC-05 | 分机状态离线告警:SIP 呼叫场景下自动判定并纳入设备报警 | `apt_device_alarm` | `apt.device.gateway`(经 `apt.duty.state` 提交命令) | A-05、A-10 |
表 0-5 四类数据表与既有语音域表的关系
| 用户称谓 | 对应表 | 状态 | 说明 |
| --- | --- | --- | --- |
| 分机表 | `apt_sip_endpoint` | 既有 | `docs/10` 表 5-3 已定义;配套 `apt_sip_binding`(绑定)、`apt_trunk`(中继)不单独成节 |
| 呼叫记录表 | `apt_call_session` | 既有 | 会话主表;明细 `apt_call_event` 只追加,归档 `apt_call_event_his` |
| 录音文件表 | `apt_recording` | 既有 | 录音索引,与会话一对多;**不存录音二进制**,文件在本地目录 |
| 注册在线状态表 | `apt_sip_reg_status` | **新增(CR-02 补遗 T-17)** | 状态变迁流水;当前态权威仍是 `apt_sip_endpoint.register_status`,两表主从关系见 §3.2 |
**主从不重复**:`apt_sip_endpoint.register_status` 是**当前态唯一权威字段**(`apt.voice.endpoint` 写入);`apt_sip_reg_status` 是**只追加的状态变迁流水**(`apt.voice.event` 写入),用于离线持续时间、告警去重与对账回溯。禁止把流水表当作当前态读取,也禁止两个写入口互写。
### 3.2 数据表字段定义
表 0-6 分机表 `apt_sip_endpoint`
| 列 | 类型 | 约束 / 默认 | 说明 |
| --- | --- | --- | --- |
| `endpoint_id` | bigint | PK | 代理键 |
| `gyid` / `extension` | varchar(32) ×2 | UK `(gyid, extension)`,NOT NULL | 分机号是业务键;IDX `(gyid, endpoint_type, enabled)`、`(gyid, room_no)` |
| `endpoint_type` | varchar(20) | NOT NULL | `ROOM` / `CONSOLE` / `BROADCAST` / `TRUNK_SIDE` |
| `room_no` | varchar(32) | 可空 | 房间号;与房间的正式关系是 `apt_sip_binding`,此列只作冗余检索 |
| `enabled` | tinyint(1) | 默认 `1` | 停用不参与叫乘 |
| `register_status` | varchar(20) | 默认 `UNKNOWN` | **本模块补充枚举**:`ONLINE` / `OFFLINE` / `UNREACHABLE` / `UNKNOWN`;当前态权威字段 |
| `last_reg_at` / `last_seen_ip` | datetime(3) / varchar(64) | 可空 | 最近注册时间与来源 IP |
| `sync_state` | varchar(20) | 默认 `PENDING` | `PENDING` / `SYNCED` / `FAILED` / `DRIFT`(表 7-4 四值);`DISABLED` / `DELETED` 用 `enabled` / `deleted` 表达,不写进本列 |
| `applied_version` / `config_version` | bigint ×2 | 默认 `0` | 本地意图版本与平台下发版本 |
| `remark` | varchar(256) | 可空 | 备注 |
| `deleted` | tinyint(1) | 默认 `0` | 逻辑删除;物理删除前二次对账 |
| `create_time` / `update_time` | datetime(3) ×2 | NOT NULL | 三时间戳口径 |
表 0-7 呼叫记录表 `apt_call_session`(明细 `apt_call_event` / `apt_call_event_his` 同 `docs/10` 表 5-3 口径)
| 列 | 类型 | 约束 / 默认 | 说明 |
| --- | --- | --- | --- |
| `session_id` | bigint | PK | 代理键 |
| `gyid` / `linked_id` | varchar(32) / varchar(128) | UK `(gyid, linked_id)`,NOT NULL | Asterisk `linkedid`;IDX `(gyid, task_id)`、`(gyid, ryid, originate_at)`、`(gyid, state, originate_at)` |
| `unique_id` / `conf_no` | varchar(128) / varchar(64) | 可空 | 通道 ID、会议桥 |
| `task_id` / `ryid` / `room_no` | varchar(64) ×3 | 可空 | 与 `gy_jcxx.jcid`、人员、房间对齐 |
| `peer_extension` / `console_extension` | varchar(32) ×2 | 可空 | 房间分机 / 值班台分机 |
| `attempt_no` | int | 默认 `1` | 复叫次数 |
| `state` | varchar(20) | NOT NULL | `CALLING` / `RINGING` / `ANSWERED` / `JOINED_CONF` / `LEFT` / `HANGUP` / `FAILED` / `TIMEOUT` / `UNKNOWN` |
| `originate_at` / `ring_at` / `answer_at` / `join_at` / `leave_at` / `hangup_at` | datetime(3) ×6 | 可空 | 事件时间线 |
| `hangup_cause` / `duration_ms` | varchar(128) / bigint | 可空 | 挂断原因与时长 |
| `recording_id` | bigint | 可空 | 关联 `apt_recording` |
| `source` | varchar(20) | 默认 `AUTO` | `AUTO` / `MANUAL` / `TEST` |
| `create_time` / `update_time` | datetime(3) ×2 | NOT NULL | — |
> `apt_call_event`:`event_seq` PK、UK `(gyid, session_id, event_type, channel, event_at_ms)`、`payload` JSON 原始事件快照,**只追加**;`apt_call_event_his` 结构同口径并增 `archived_at`。
表 0-8 录音文件表 `apt_recording`
| 列 | 类型 | 约束 / 默认 | 说明 |
| --- | --- | --- | --- |
| `recording_id` | bigint | PK | 代理键 |
| `gyid` / `file_key` | varchar(32) / varchar(256) | UK `(gyid, file_key)`,NOT NULL | Asterisk `recordfile`;IDX `(gyid, task_id)`、`(gyid, ryid, started_at)`、`(gyid, state, started_at)` |
| `session_id` / `task_id` / `ryid` | bigint / varchar(64) ×2 | 可空 | 与会话一对多 |
| `storage_kind` / `storage_path` | varchar(20) / varchar(512) | 默认 `LOCAL` | `LOCAL` / `NFS` / `HTTP`;路径只存索引,不存文件本体 |
| `url` | varchar(512) | 可空 | 回放地址;对外一律短期签名 |
| `bytes` / `duration_ms` / `codec` | bigint ×2 / varchar(20) | 可空 | 文件大小、时长、编码 |
| `started_at` | datetime(3) | NOT NULL | 录音开始时间 |
| `state` | varchar(20) | 默认 `RECORDING` | `RECORDING` / `AVAILABLE` / `MISSING` / `PURGED` |
| `retention_days` | int | 默认 `90` | D15:90 天 + 仅内网 + 导出必审计 |
| `purged_at` / `checksum` | datetime(3) / varchar(128) | 可空 | `PURGED` 只删文件、**不删索引** |
| `create_time` / `update_time` | datetime(3) ×2 | NOT NULL | — |
表 0-9 注册在线状态表 `apt_sip_reg_status`(**新增 T-17**;owner = apartment;上报源 = #11 摘要 / #24 质量摘要,明细不上报)
| 列 | 类型 | 约束 / 默认 | 说明 |
| --- | --- | --- | --- |
| `id` | bigint | PK | 代理键;业务键为 `(gyid, endpoint_id, event_seq)` |
| `gyid` / `endpoint_id` / `extension` / `room_no` | varchar(32) / bigint / varchar(32) / varchar(32) | UK `(gyid, endpoint_id, event_seq)`,NOT NULL | IDX `(gyid, state, occurred_at)`、`(gyid, endpoint_id, occurred_at)` |
| `state` / `prev_state` | varchar(20) ×2 | NOT NULL | 与 `apt_sip_endpoint.register_status` 同字典:`ONLINE` / `OFFLINE` / `UNREACHABLE` / `UNKNOWN` |
| `source` | varchar(20) | NOT NULL | `AMI_EVENT` / `QUALIFY` / `PROBE` / `MANUAL` / `RECONCILE`;双源判定见 `docs/公寓端/08`「注册状态不可靠」 |
| `event_seq` | bigint | NOT NULL,单调 | 同一分机单调递增;更正以新一条 + `biz_version+1` 体现,**不改历史行** |
| `occurred_at` / `received_at` | datetime(3) ×2 | NOT NULL | 现场发生 / 公寓入库 |
| `last_reg_at` / `offline_since` / `duration_ms` | datetime(3) ×2 / bigint | 可空 | 离线持续时间与周期起点;告警去重键依赖 `offline_since` |
| `peer_ip` / `peer_port` | varchar(64) / int | 可空 | 注册来源 |
| `ami_session_id` | bigint | 可空 | 关联 `apt_ast_ami_session`,用于 AMI 断连归因 |
| `alarm_id` | varchar(64) | 可空 | 关联 `apt_device_alarm.alarm_id`;恢复时不清空 |
| `biz_version` | bigint | 默认 `1` | 同周期更正 +1 |
| `create_time` | datetime(3) | NOT NULL | 只追加,无 `update_time` |
> 新增表全部带代理主键、`gyid` 打头的唯一键与查询索引,不复制 `docs/11` 记录的 0 主键 / 0 索引缺陷;本表为设计定义,DDL 待阶段二契约冻结评审通过后执行。
### 3.3 增删改查操作
表 0-10 四类表的 CRUD 主体、入口与约束
| 表 | 增(C) | 改(U) | 删(D) | 查(R) |
| --- | --- | --- | --- | --- |
| `apt_sip_endpoint` | 管理员 `POST /apt/voice/endpoints`,只落库 + 异步下发,返回"已受理 + 当前 `syncState`";`apt:voice:endpoint:add` | `PUT /apt/voice/endpoints/{id}` 改属性;启停走 `/status`;收敛走 `/resync`。**分机号不可原地改**(`40906`,停旧建新,旧号留 7 天) | `DELETE` 为 `deleted=1` 逻辑删除,物理删除前二次对账;保留 `apt_ast_config_version` 版本记录 | `GET /apt/voice/endpoints`(列表 + 详情);`gyid` 数据域过滤,默认按 `register_status` / `sync_state` 可筛 |
| `apt_call_session` | **无人工新增接口**:由 `apt.voice.event` 依 AMI `OriginateResponse` 等事件自动写入 | **无人工编辑接口**:只允许状态机事件推进与 5 分钟巡检判定 `UNKNOWN` 终态;不得改 `state` 外的业务事实 | **禁止单条删除**;只允许按留存期由归档任务 `apt:voice:call:archive` 受控迁 `apt_call_event` → `apt_call_event_his`(`commandId` 幂等) | `GET /apt/voice/calls`、`/{sessionId}`、`/{sessionId}/events`;导出必审计 |
| `apt_recording` | **无人工新增接口**:由 `apt.voice.media` 依事件 `recordfile` 或目录反查写入;缺文件置 `MISSING` 并告警 | 只允许 `state` 由清理任务置 `PURGED`、由回查置 `AVAILABLE`,**不改 `file_key` / `checksum`** | 禁止物理删索引;清理顺序为「先置 `PURGED` → 再删文件」,失败重试 + 告警(`docs/公寓端/05` 表 5-4) | `GET /apt/voice/recordings`、`/{id}/url` 短期签名地址;`state=PURGED` 返回 `41001` |
| `apt_sip_reg_status` | **只追加**:`apt.voice.event` 依注册事件 / `qualify` / 定时探测写一条状态变迁 | **禁止改**:更正以新一条 + `biz_version+1` 体现;当前态由 `apt.voice.endpoint` 同步到 `apt_sip_endpoint.register_status` | **禁止删单条**;按月归档(在线 90 天) | `GET /apt/voice/reg-status`(当前态)、`/{endpointId}/history`(变迁流水);`POST /apt/voice/reg-status/{endpointId}/commands` 手工重探 |
| `apt_device_alarm` | AC-05 自动判定写入 | 仅处置回写 `handled_at/by/result`(`bizVersion+1`) | 禁止删除,按留存期归档 | `GET /apt/device/alarms`(CR-02 N-17) |
表 0-11 新增 / 沿用接口(端内 `/apt/**`,**不计入跨端 20 接口**;编号沿用 CR-02 的 `N-*` 序列)
| 编号 | 方法 + 路径 | 权限点 | 幂等 | 落表 | 状态 |
| --- | --- | --- | --- | --- | --- |
| `docs/公寓端/04` 表 4-15 #1~#6 | `GET/POST/PUT/DELETE /apt/voice/endpoints*` | `apt:voice:endpoint:*` | L0~L2 | `apt_sip_endpoint` | 沿用 |
| `docs/公寓端/04` 表 4-15 #7~#9 | `GET /apt/voice/calls*` | `apt:voice:call:list` | L0 | `apt_call_session`、`apt_call_event` | 沿用 |
| `docs/公寓端/04` 表 4-15 #10~#11 | `GET /apt/voice/recordings*` | `apt:voice:recording:list/replay` | L0 | `apt_recording` | 沿用 |
| N-19 | `GET /apt/voice/reg-status` | `apt:voice:reg:list` | L0 | `apt_sip_endpoint`、`apt_sip_reg_status` | 新增 |
| N-20 | `GET /apt/voice/reg-status/{endpointId}/history` | `apt:voice:reg:query` | L0 | `apt_sip_reg_status` | 新增 |
| N-21 | `POST /apt/voice/reg-status/{endpointId}/commands`(`PROBE`) | `apt:voice:reg:probe` | **L2** `commandId` | `apt_sip_reg_status` | 新增 |
| N-22 | `POST /apt/voice/calls/archive` | `apt:voice:call:archive` | **L2** `commandId` | `apt_call_event_his` | 新增 |
| N-23 | `POST /apt/voice/recordings/commands`(`PURGE`) | `apt:voice:recording:purge` | **L2** `commandId` | `apt_recording` | 新增 |
| N-24 | `GET /apt/voice/endpoints/{id}` | `apt:voice:endpoint:list` | L0 | `apt_sip_endpoint` | 新增(补详情) |
> 路径遵循 `docs/公寓端/04` 表 4-1:不出现动词化路径;`archive` / `commands` 属非 CRUD 动作;受控命令一律带 `commandId`,状态变更不直接 `PUT` 业务状态字段。
### 3.4 权限控制要求
表 0-12 权限点矩阵(前端 `v-hasPermi` 与后端 `@PreAuthorize` 同码,登记 `sys_menu.perms`,见 `docs/公寓端/02` §4.1 / §4.2)
| 权限点 | 分机表 | 呼叫记录表 | 录音文件表 | 注册在线状态表 | 告警记录 | 可授角色 |
| --- | --- | --- | --- | --- | --- | --- |
| `apt:voice:endpoint:list` | R | — | — | R | — | 公寓管理员、值班长、值班员 |
| `apt:voice:endpoint:add/edit/changeStatus/remove` | C / U / D | — | — | — | — | **仅公寓管理员 / 系统管理员** |
| `apt:voice:call:list` | — | R | — | — | — | 值班长、值班员(只读) |
| `apt:voice:call:archive` | — | 归档 | — | — | — | 仅公寓管理员 |
| `apt:voice:recording:list/replay` | — | — | R | — | — | 值班长、值班员(回放必审计) |
| `apt:voice:recording:purge` | — | — | 清理 | — | — | 仅公寓管理员 |
| `apt:voice:reg:list/query` | R | — | — | R | — | 值班长、值班员 |
| `apt:voice:reg:probe` | — | — | — | 受控命令 | — | 值班长、公寓管理员 |
| `apt:device:alarm` | — | — | — | — | R + 处置 | 值班员(处置)、值班长 |
表 0-13 六条权限硬要求
| # | 要求 |
| --- | --- |
| 1 | **双校验**:`/apt/**` 走 RuoYi 会话 + `@PreAuthorize`,前端 `v-hasPermi` 同码;越权或跨 `gyid` 一律 `40301 APT_FORBIDDEN`,不返回脱敏后的他人数据 |
| 2 | **`gyid` 数据域锁定**:`gyid` 只从会话取,不接受前端参数;四类表所有查询 / 写入 SQL 必带 `gyid`,唯一键一律以 `gyid` 打头 |
| 3 | **事实类表只读**:呼叫记录、录音索引、注册状态流水、设备告警**不开放人工新增 / 编辑 / 删除接口**,只能由对应领域模块或带 `commandId` 的受控命令写入;任何"直接改 `state`"的接口视为设计错误 |
| 4 | **两级审计**:所有写操作、导出、回放、清理、处置同时记 `sys_oper_log` 与 `duty_operation_log`(操作人、权限点、`commandId`、前后值、结果);审计事实可查询、可导出、保留期独立(`docs/公寓端/05` §8) |
| 5 | **敏感数据**:录音回放短期签名地址 ≤ 5 分钟(D15 保守值,严于 `04` §8 的 ≤ 10 分钟)、仅内网可播;`state=PURGED` 返回 `41001`;`last_seen_ip` / `storage_path` 仅管理端可见、值班台脱敏;录音文件与人脸、抓拍一样**不上传平台** |
| 6 | **职责分离**:改动 Asterisk 资源(分机增删改停、对账)与查阅业务结果(呼叫 / 录音 / 注册 / 告警)分开授权;值班员可查看与处置告警,但不得改分机台账、不得清理录音 |
### 3.5 SIP 呼叫场景:分机注册离线自动纳入设备报警
> 适用范围:`gy_jcxx.call_mode='SIP'`(A-06 / A-08)。`TABLET` 模式不触发分机离线告警,其设备离线仍按 CR-02 F-H 走 `apt_device_alarm`,两者共用一张告警表与同一 `alarmSource=DEVICE` 口径。
表 0-14 告警触发条件(四条同时满足才判定)
| # | 条件 | 判定依据 | 不成立时 |
| --- | --- | --- | --- |
| 1 | 呼叫方式为 SIP 且任务进入派发 | `gy_jcxx.call_mode='SIP'`;Quartz 触发或人工 `POST /duty/call/{jcid}/voice` | 平板模式跳过 |
| 2 | 能解析出对应房间的**主分机** | `jcid → fjh → apt_sip_binding(bind_type='ROOM', is_primary=1, 生效区间有效) → apt_sip_endpoint` | 绑定缺失记 `apt_ast_drift` 并告警,不误报"注册离线" |
| 3 | 分机配置已就绪 | `enabled=1`、`deleted=0`、`sync_state='SYNCED'` | `PENDING` / `FAILED` / `DRIFT` 属配置未收敛,按 `40905` / 对账告警处理 |
| 4 | 注册状态非在线或已失联 | `apt_sip_endpoint.register_status ∈ {OFFLINE, UNREACHABLE, UNKNOWN}`,或 `ONLINE` 但 `last_reg_at` 早于 `now - 探测阈值`;且 `apt_sip_reg_status` 最新态非 `ONLINE`、`offline_since` 持续超过阈值 | 状态 `UNKNOWN` 且 AMI 断连时按 `P2` 记,不阻断叫乘 |
> 阈值默认:注册探测 **120 s**、离线持续 **60 s**(Q-26 待决)。**双源判定**:注册事件 + `qualify` / 定时探测,与 `docs/公寓端/08`「注册状态不可靠」的处置一致;`apt_ast_ami_session` 断连期间的 `UNKNOWN` 单列归因。
> **去重**:同一离线周期只产生 1 条告警,`alarm_event_key = hash(gyid, endpoint_id, 'SIP_OFFLINE', offline_since)`;分机恢复后再次离线生成新 key;同 key 不重复响、不重复推送(CR-02 F-H 第 1 步)。
表 0-15 告警级别(`severity`,沿用 `apt_device_alarm` 的 `P0`~`P2`)
| 级别 | 触发情形 | 值班台表现 |
| --- | --- | --- |
| `P0` | 有**待叫乘任务**(含 30 s 内即将触发)且对应房间主分机离线 | 该行置红 + 顶部横幅 + 业务优先 TTS 提醒 |
| `P1` | 无待叫乘任务,但房间 / 值班台 / 广播分机离线 | D6 设备行标红,纳入盯控列表 |
| `P2` | 状态 `UNKNOWN`(AMI 断连、`qualify` 未回)或中继侧离线 | 仅记入告警列表与健康摘要,不触发 TTS |
表 0-16 通知方式(**不新增 WS 消息名、不新增跨端接口编号**)
| # | 通道 | 内容 | 约束 |
| --- | --- | --- | --- |
| 1 | 落库 | `apt_device_alarm` 一行:`alarm_source=DEVICE`、`alarm_type=OFFLINE`、`severity` 按表 0-15,附 `endpointId`、`extension`、`roomNo`、`jcid`、`offlineSince` | `alarm_event_key` 去重;`deviceCode` 填 `apt_sip_binding` 的 `DEVICE` 关联值,无则填分机号 |
| 2 | `/ws/duty` | 复用 CR-02 表 3-9 `device.alarm`(`alarmSource`、`alarmEventKey`、`severity`);恢复时复用 `docs/应用端/04` 表 4-8 `alarm.cleared` | `seq` 单调;60 s 兜底轮询;推送只是加速 |
| 3 | 值班台 | D6 行标红 + 横幅;`P0` 经 `notice.sync` 入两级提醒队列与 TTS(业务优先) | 不新增消息名 / 枚举;`apt_tts_utterance` 只记播报结果 |
| 4 | 跨端上报 | #12 `DEVICE_ALARM`(按 `alarmSource` 分组,含 `deviceCode`)、#11 摘要 `offlineList[]`、`offlineCount`;#13 归档摘要计数、#24 质量摘要 `regRate` | 走 CR-02 payload 扩展;**平台只汇总,不回调、不下发处置指令** |
| 5 | 人工处置 | `POST /apt/device/alarms/{id}/handle`(CR-02 N-17,权限点 `apt:device:alarm`) | 回写 `handled_at/by/result`,`bizVersion+1` |
**不阻断原则**:离线告警只提示与留痕,仍按原流程发起 SIP 呼叫,失败原因记入 `apt_call_session.hangup_cause`;是否自动降级为平板 / 广播由 Q-27 决定,未决前不自动切换。告警与叫乘结论互不覆盖(`docs/公寓端/05` §4 第 3 条"录音不阻断叫乘"同口径)。
表 0-17 告警记录留存要求
| # | 对象 | 留存口径 |
| --- | --- | --- |
| 1 | `apt_device_alarm` | 在线 **180 天**(Q-28 待决),之后按月归档;索引长期保留。必须完整保留 `alarm_event_key`、`occurred_at`、`cleared_at`、`handled_at/by/result`、`biz_version`、`severity` 与关联 `jcid` / `endpointId` |
| 2 | `apt_sip_reg_status` | 状态变迁流水在线 **90 天**,按月归档;同一离线周期的 `OFFLINE` 起与 `ONLINE` 恢复必须成对可查,禁止物理删单条 |
| 3 | `apt_call_session` / `apt_call_event` | 会话索引长期保留;原始事件在线 90 天后归档到 `apt_call_event_his`(`docs/10` 表 5-3 处理关系第 5 条) |
| 4 | `apt_recording` | D15:录音文件 **90 天**(`retention_days`),`PURGED` 只删文件**不删索引**;告警记录**不随录音清理删除** |
| 5 | 审计与上报 | `sys_oper_log` + `duty_operation_log` 保留期独立;#12 / #13 摘要与 `PL_RECEIVE_LOG` 按平台口径留存;断网期间告警先落本地库 + outbox,恢复后按 `serverReceivedAt` 归日并复用原 `idempotencyKey` 续报(CR-02 F-E) |
| 6 | 恢复闭环 | 注册恢复后:写 `apt_sip_reg_status` 新流水 → `apt.voice.endpoint` 置 `register_status='ONLINE'` → 置 `apt_device_alarm.cleared_at` → 推 `alarm.cleared`。**只追加、不清零**,恢复后告警记录仍可查 |
表 0-18 本次新增决策点(沿用 `docs/应用端/10` 的决议记录格式:结论 + 决议人 + 日期三者缺一视为未决议)
| 编号 | 待决事项 | 未决默认口径 | 拍板时点 |
| --- | --- | --- | --- |
| Q-26 | 注册探测阈值与离线持续阈值 | 120 s / 60 s | 阶段二契约冻结前 |
| Q-27 | 分机离线时是否自动降级为平板 / 广播 | 不自动切换,仍发起 SIP 并记失败原因 | 阶段三试点前 |
| Q-28 | 告警记录在线留存 180 天是否合规 | 在线 180 天 + 按月归档,索引长期保留 | 阶段三 D 批开工前 |
## 4. 与三端设计的关系
1. **覆盖范围**:A-01~A-14 的三端归属、冲突裁决与分期见 CR-02 §2(表 3-2~3-5);三端 `01-功能边界.md` 已同步改判。§3 属公寓端语音与录音域(原功能域),不新增功能域编号。
2. **交互协议与数据**:跨端仍为 20 个编号(不新增),新数据走 `payload` + `bizType` 扩展;平板 / 消费机走设备侧 `/open/v1/**` 拉取;值班台走端内 `/duty/**` 与 `/ws/duty`。见 CR-02 §3、§4;本次告警通知不新增消息名与接口编号。
3. **数据表**:CR-02 新增公寓本地库 13 张表 + 本次补遗 1 张(共 14 张)、平台库 3 张表,另 3 处补列;公寓本地库业务表口径统一为 **59 张**(44 存量 + `apt_local_task` 1 + 14)。见 CR-02 §6、§10。
4. **关键流程**:双模式顺序派发、换乘、断网充值与冲正、上报历史与续报等 8 条流程变更见 CR-02 §7;本次新增分机离线告警流程 F-I 见 CR-02 §10。**A-06 / A-07 / A-08 两类叫乘实现方式的端到端流程(触发条件、交互流程、状态上报机制、结果反馈路径)以 `docs/公寓端/05` §9 为流程权威表述**,表 0-3 的需求口径不变。
5. **人脸数据下发与注册链路**:`docs/公寓端/10` 为唯一权威表述,登记见 CR-02 §12(CR-C24 / CR-C25、F-J、R-85~R-93、验收 15~17)。注册链路为「**签到终端发起注册申请并上传人脸 → 值班员在值班台完成人员注册 → 注册成功后人脸进入公寓服务系统 → 分别上传平台与下发公寓内设备**」;平台 `personRegVerdicts[]` 降级为主档同步回执,不再是注册生效前置;两条同步路径解耦并行、互不阻塞。人员主数据照片与特征值按 CR-C24 允许跨端,业务抓拍 / 录音仍不出公寓;本模块**不新增业务表**,59 张口径不变。
6. **人员入住作业流程**:`docs/公寓端/11` 为唯一权威表述,登记见 CR-02 §13(CR-C26~CR-C30、F-K、R-95~R-103、验收 18~20)。五环节为「注册与签到 → 入住申请(房间 / 交路)→ 值班员审核与房间调整 → #9 `CHECKIN` 提报与平台运安匹配 → #7 任务查询进入叫乘」;"已入住"为流程态 + 业务事实双重表达,申请单落 `apt_local_task` 补列,本流程**不新增业务表、不新增 `gy_jcxx` 状态字段、不新增跨端编号**。
7. **消费、充值与菜品管理**:`docs/公寓端/12` 为唯一权威表述,登记见 CR-02 §14(CR-C31~CR-C35、F-L / F-M / F-N、R-104~R-113、验收 21~23)。三流程为「人员消费:食堂选餐 → 消费终端加菜显示金额 → 查询补贴 / 现金额度 → 补贴优先扣款、不足现金补足 → 费用上报公寓端 → #10 流水同步平台并落入人员账户」「人员线下充值:扫公寓线下二维码 → 签到终端点击现金充值并输入金额 → 管理员核对 → 计入本人现金充值额度 → 保存至公寓端并同步平台」「公寓菜品管理:发布阶段性菜品食谱 → 消费终端查看菜品并加入选择菜单 → 预留远程点菜入口」;**仅线下充值、扫码非支付、不建支付订单与回调**,一笔消费只一条流水、四条状态链互不替代,食谱主数据归平台库 `YKT_MEALS` 补列,本流程**不新增业务表、不新增跨端编号、不新增 `bizType`、不新增状态字段**。
8. **三端一致性同步与平台统计分析**:`docs/平台端/10` 为统计 / 报表 / 排名唯一权威表述,CR-02 §15(**表 3-63 变更点与影响范围**、表 3-65 四能力三端对照、**表 3-66 三端状态映射与数据同步机制**)为登记权威。本端四项能力权威表述不变;#13 `ARCHIVE` 摘要按 `payload` 扩展 `satisfaction{}` / `roomOccupancy{}` 分组计数,供平台以公寓为单位统计评价情况与房间利用周转,**不新增 `bizType`、不新增上报接口、不接明细、不改公寓业务状态**。平台按 `gyid` 分类统计接待能力(入住 / 叫乘 / 评价 / 报警)、补贴发放、补贴消费、现金充值(**计为收入**)、房间利用周转,按月 / 季 / 年出报表并做分类 + 综合排名;统计结果**不回写**本端业务状态。跨端 20 编号、**59 张表**、`v2-draft`、平台零回调不变,平台库新增 2 张另计。
7. **待决策**:Q-20~Q-25 见 CR-02 §9,Q-26~Q-28 见本文件表 0-18,Q-29 见 CR-02 §11,Q-30~Q-35 见 `docs/公寓端/10` 表 10-29,Q-36~Q-40 见 `docs/公寓端/11` 表 11-34,Q-41~Q-45 见 `docs/公寓端/12` 表 12-33,Q-46~Q-50 见 `docs/平台端/10` 表 10-21;未决前按默认口径执行。
+160
View File
@@ -0,0 +1,160 @@
# 01 公寓端功能边界
> 追溯:`docs/10` §3.2(公寓端负责/不负责)、§3.3 表 3-1(应用端与公寓端分工)、§3.4 表 3-2(语音三端分工)、
> §4.1 表 4-1(能力归属)、§4.3 表 4-3(公寓端模块清单)、§10.2 表 10-2(D11–D16)。
> **v2 修订(CR-02,2026-11-09)**:本端覆盖范围以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §2 为权威,见下文 §0。
## 0. v2 覆盖范围修订(CR-02)
> 依据 `docs/公寓端/00` 的重新规划。本节图表用独立编号 `表 C-y`,不占用本文件 `表 1-y` 序号;原 §1~§6 与 CR-02 冲突处以 CR-02 表 3-4 为准,表 1-4 / 表 1-5 的分期已同步改判。
表 C-1 功能域由 10 个扩为 16 个(CR-02 表 3-3)
| # | 新增 / 调整域 | 本期 | 唯一写入模块 | 新增 / 启用表 | 裁决 |
| --- | --- | --- | --- | --- | --- |
| 11 | 入住 / 换房 / 离寓 | **是**(原"属后续"作废) | `apt-stay` | `gy_jcxx`、`gy_rooms`、`gygl_ly_event` | CR-C02 |
| 12 | 账务本地流水(含断网充值与扣补贴) | **是**(原 P2 作废) | `apt.ledger.local` | `apt_ledger_offline`、`gy_ykt_*` 5 张由预留转启用 | CR-C03 |
| 13 | 平板叫乘与顺序派发 | **是** | `apt-dispatch`、`apt.device.gateway` | `apt_tablet_device`、`apt_tablet_task`、`apt_tablet_call_result` | CR-C08 |
| 14 | 换乘(时刻 + 换房联动) | **是** | `apt.duty.state` + `apt-stay` | `apt_transfer_order` | CR-C09 |
| 15 | 保洁人员与记录 | **是** | `apt-stay` | `apt_clean_staff`、`apt_clean_record` | CR-C11 |
| 16 | 上报历史与续报 | **是** | `apt.report.outbox` | `apt_report_history`、`apt_report_retry_batch` | CR-C10 |
另有三项改变本端口径但不单列功能域:本地人员 / 人脸登记(`apt_person_face_reg`,CR-C04)、设备报警独立表(`apt_device_alarm`,CR-C12)、房间现场台账(`apt_master_room.building` 补列 Q-20,CR-C05)。
三条不变式:① 下行主数据快照仍只读,本地登记是独立的"待确认事实"表;② 状态字段仍只有 `apt.duty.state` 一个写入口,平板结果 / 换乘 / 报警只提交命令;③ 平台不回调公寓,跨端 20 个编号不新增(CR-C07)。
**补遗(`docs/公寓端/00` v1.1 / CR-02 §10)**:Asterisk 配置数据管理(`AC-01`~`AC-05`)**不新增功能域编号**,落在原有"语音与录音"域:分机表 / 呼叫记录表 / 录音文件表 / 注册在线状态表四类表的字段、增删改查与权限控制,以及 SIP 呼叫场景下分机注册离线自动纳入设备报警(触发条件 / 级别 / 通知 / 留存)。新增 `apt_sip_reg_status`(T-17,状态变迁流水;当前态权威仍是 `apt_sip_endpoint.register_status`),公寓本地库业务表口径 **58 → 59 张**。分机资源状态机(本文件 `03` 表 3-11)的 `sync_state` 四值不变,只补充 `register_status` 枚举 `ONLINE` / `OFFLINE` / `UNREACHABLE` / `UNKNOWN`。
**两类叫乘实现方式的流程权威表述**:`docs/公寓端/05` §9。第一类 SIP 语音叫乘由 Asterisk 主动发起呼叫、房间 SIP 终端接听入会后播放提示语音完成叫乘,状态由业务系统依 AMI 事件提交状态机命令;第二类平板叫乘由平板提前拉取任务、按 `jcsj` 本地定时播放语音、人员刷脸签到结束叫乘、结果经 `POST /open/v1/tablet/results` 上报公寓服务。两条链路共用 `apt.duty.state`、`gy_jcxx` 终态与 outbox 上行,不新增功能域、不新增跨端编号。
**人脸数据下发管理(CR-02 §12 / `docs/公寓端/10`)**:新增 `apt-face` 子域但**不新增功能域编号、不新增业务表**,落在"人员与人脸登记"域。注册链路按 **CR-C25** 改为"签到终端 `REG_APPLY` 发起申请并上传人脸 → 值班台待办 → 值班员 `CONFIRM` 完成人员注册 → 人脸进入 `apt_master_person` → **分别** #9 上传平台 / `FACE_ISSUE` 下发公寓内设备";两条同步路径解耦并行、互不阻塞,平台回执降级为主档同步回执。设备按 `gy_sb.face_scope_*` 隔离可见范围,`TRANSIENT` 离寓自动擦除,`LONG_TERM` 独立分区长期保留。人员主数据照片与特征值按 **CR-C24** 允许跨端,**业务抓拍、录音与签到现场图仍不出公寓**;公寓本地库业务表口径仍为 **59 张**。
**人员入住作业流程(CR-02 §13 / `docs/公寓端/11`)**:细化"入住 / 换房 / 离寓"功能域(`CI-01`~`CI-05`),**不新增功能域编号、不新增业务表**:签到终端扫脸核验(未注册先按 §12 完成注册)→ 选房间 / 交路提交申请 → 值班台审核与房间调整 → `apt-stay` 办理入住并 #9 `CHECKIN` 提报 → 平台按运安快照匹配、结论经 #7 响应体下行 → 公寓拉任务并复算 `jcsj` 进入叫乘。申请单落 `apt_local_task` 补列(`applyId` ↔ 预分配 `jcid` 一对一,申请阶段**不建** `gy_jcxx`);"已入住"为 `checkin_state=CHECKED_IN` 流程态 + `finish_ly='0' AND sjlysj IS NULL` 业务事实双重表达,**不新增 `gy_jcxx` 状态字段**(CR-C26);端内接口 N-38~N-47 不计入跨端 20 编号。
**消费、充值与菜品管理(CR-02 §14 / `docs/公寓端/12`)**:细化表 1-4 第 6 域「账务本地流水」与设备域,**不新增功能域编号、不新增业务表**:`CF-01` 消费终端按 `PUBLISHED` 食谱选菜并显示金额 → N-49 查补贴 / 现金额度 → "补贴优先、现金补足"双额度 CAS 扣减且一笔消费只记一条 `CONSUME` 流水 → #10 `LEDGER_OFFLINE` 上报、平台 `platform-ledger` 唯一写入口更新人员账户;`CF-02` 扫公寓线下二维码**仅作公寓识别**,签到终端提交现金充值申请、管理员二次确认后只增 `xjye`,**不接线上支付、不建支付订单与回调**;`CF-03` 食谱主数据归平台库 `YKT_MEALS` 补列、公寓经 #4 只读下发,终端"选择菜单"确认前不落库,远程点菜仅留 N-58 入口且本期恒 `40940 FEATURE_DISABLED`。`apt.ledger.local` / `platform-ledger` 各自单写入口不变,设备与前端**零余额表写权限**;端内接口 N-48~N-58 不计入跨端 20 编号,公寓本地库业务表口径仍为 **59 张**。
**三端一致性同步与平台统计分析(CR-02 §15)**:本端四能力(平板叫乘 / 人脸签到 / 消费 / 菜品管理)的功能边界与权威表述**不变**;应用端与平台端只补齐业务规则、交互流程、权限控制与异常处理,三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**。本端唯一新增职责是 **#13 `ARCHIVE` 摘要 `payload` 扩展 `satisfaction{}` / `roomOccupancy{}` 分组计数**(评价情况 / 房间利用周转),**不新增 `bizType`、不新增上报接口、不接明细、不新增功能域编号**。平台新增 `docs/平台端/10`「公寓统计分析与排名报表」按 `gyid` 分类统计并出月 / 季 / 年报表与排名,统计结果**不回写**本端任何业务状态;平台库新增 2 张 `pl_apt_stat_*` 另计,公寓本地库仍 **59 张**。**变更点与影响范围见 CR-02 §15 表 3-63。**
## 1. 定位与职责
### 1.1 一句话边界
> **现场资源与现场事实归公寓,号码规则与跨公寓统计归平台,人在界面上的干预归应用端——但任何干预都必须经公寓端领域服务落地。**
这句话是 `docs/10` §3.4 结尾"平台管号码规则与合规策略,公寓管资源、事件与媒体,应用端管人看与人工干预"的展开,
展开的关键在最后半句:应用端有界面但没有写权限的"后门",`docs/10` 表 3-1"写权限"行明确要求
"仅通过状态机服务写,禁止直连 Mapper 修改状态字段"。
### 1.2 负责(对应 `docs/10` §3.2 负责清单,逐条落到本端模块)
表 1-1 公寓端职责与落点
| # | 职责 | 落点模块 | 落点表 | 依据 |
| --- | --- | --- | --- | --- |
| 1 | 本公寓设备接入:注册、认证、心跳、指令队列、事件受理、离线缓存 | `apt-device` | `gy_sb`、`gy_sb_xt`、`apt_device_command`、`gygl_ly_event` | §3.2 负责第 1 条;§7.1~§7.5 |
| 2 | 叫乘执行全链路:任务创建、Quartz 调度、AMI 会议桥、TTS、串口广播、复叫/报警/停止、重启恢复与漏叫补偿 | `apt-dispatch` | `gy_jcxx`、`gy_bjxx`、`apt_call_session`、`apt_tts_utterance` | §3.2 负责第 2 条;§7.9 |
| 3 | Asterisk 资源全生命周期:分机与中继增删改停、注册状态、绑定关系、台账与实际配置收敛 | `apt-voice`(`endpoint`) | `apt_sip_endpoint`、`apt_sip_binding`、`apt_trunk`、`apt_ast_config_version`、`apt_ast_drift` | §3.2 负责第 3 条;表 5-3、§7.8 |
| 4 | Asterisk 事件闭环:AMI 连接自愈、事件订阅解析、通话与会议状态跟踪、失败与超时识别、录音与 TTS 回写 | `apt-voice`(`ami` / `event`) | `apt_call_event`、`apt_call_session`、`apt_ast_ami_session` | §3.2 负责第 4 条;表 7-5 |
| 5 | 会议桥与媒体:会议粒度与占用、TTS 排队、录音落盘与索引、留存与清理 | `apt-voice`(`media`) | `apt_recording`、`apt_tts_utterance` | §3.2 负责第 5 条;表 5-3、表 8-5 |
| 6 | 在寓事实与房态、换房、退房、保洁、超时离寓报警 | `apt-stay` | `gy_jcxx`、`gy_jcxx_his`、`gy_rooms`、`gy_clean_task`、`gygl_ly_event` | §3.2 负责第 6 条;`docs/05` §2 |
| 7 | 本地主数据缓存与规则快照,保证断网可叫乘 | `apt-master-client` | `apt_master_version`、`apt_master_apartment/room/person/route/rule`、`apt_holiday` | §3.2 负责第 7 条;表 5-1、§6.3 |
| 8 | 本地账务流水与离线消费受理,向平台归集 | `apt-stay`(`apt.ledger.local` 子域) | `gy_ykt_remainder`、`gy_ykt_consume`、`gy_ykt_allowance`、`gy_ykt_cash_request`、`gy_ykt_xjczjl` | §3.2 负责第 8 条;表 5-2 账务行 |
| 9 | 结果上报:执行结果、流水、告警摘要、设备摘要、归档摘要、水位 | `apt-report` | `apt_report_outbox`、`apt_report_watermark` | §3.2 负责第 9 条;§6.2、§6.3 |
| 10 | 本公寓 RBAC、菜单、操作审计 | `apt-system` + `apt-framework` | `sys_user/role/menu/dept`、`sys_oper_log`、`duty_operation_log` | `docs/10` 表 2-2 权限行(RuoYi RBAC 复用)+ 表 3-1(应用端与公寓端逻辑分工) |
| 11 | 启动自检与运行期巡检、健康探针 | `apt-ops` | `apt_ops_selfcheck` | `docs/10` 表 4-3 `apt.ops`;§7.11 表 7-7 |
| 12 | 管理类配置:交路本地复算校验、房间属性、人员缓存只读视图、设备档案 | `apt-master-client` / `apt-stay` / `apt-device` | 见第 3 节各域 | `docs/10` 表 4-3(`apt.room` / `apt.device.catalog` / `apt.master.cache`) |
### 1.3 不负责(对应 `docs/10` §3.2 不负责清单)
表 1-2 公寓端不做的事与拒绝方式
| # | 不做 | 为什么不做 | 越界请求如何拒绝 |
| --- | --- | --- | --- |
| 1 | 不直连运安(SOAP),不维护跨公寓人员全集 | §3.2 不负责第 1 条;D4 | 不引入 WSDL 依赖;任何"同步运安"入口返回 `APT_NOT_SUPPORTED` 并指向平台 |
| 2 | 不决定补助标准,不做跨公寓统计,不维护平台账号 | §3.2 不负责第 2 条;表 4-1 补助规则行 | `apt_master_rule` 只读;统计接口限定 `gyid` 单公寓;不建 `gl_*` 表 |
| 3 | 不制定园区级设备策略 | §3.2 不负责第 3 条 | 策略落 `apt_master_rule` 快照,公寓端只读执行 |
| 4 | 不制定跨公寓号段规划、中继路由策略、录音合规策略 | §3.2 不负责第 4 条;D12、D15 | 号段规则取本地快照;合规策略取最后下发版本 |
| 5 | 不经平台中转运维语音媒体流 | §3.2 不负责第 5 条;表 2-3 负面清单 | `/open/**` 只做配置与摘要,不代理 AMI、不传录音 |
| 6 | 不评估录音内容合规性,只执行留存与访问控制 | §3.2 不负责第 6 条;D15 | 只实现"留存期 + 内网 + 导出审计"三项可执行动作 |
| 7 | 不承担房间基线、余额主账、餐饮业务、库存 `kcgl` | 表 4-1;D2、D10 | 对应域不建业务表、不建 Controller;`kcgl` 明确不进公寓端 |
## 2. 与平台端、应用端的五维分界
表 1-3 三端五维对比(依据 `docs/10` 表 2-3、表 3-1、表 3-2)
| 维度 | 平台端 | 公寓端(本端) | 应用端(值班台) | 越界判定 |
| --- | --- | --- | --- | --- |
| 数据归属 | 主数据真源、计划、账务主账、跨公寓报表 | 执行事实、设备与语音资源、本地流水、快照 | 无独立数据;只在同库写 `duty_*` 操作域 | 出现"公寓端写主数据真源"或"值班端存在第二库"即越界 |
| 写入口 | 通过下行接口写平台库 | 领域服务 + 状态机单入口 | 只发命令(`duty_manual_action`) | Controller/Mapper 直改 `ryzt`、`finish_*`、`stoped` 即越界 |
| 部署形态 | 无状态多实例 + 平台主库 | 单实例同进程(`apt-admin`)+ 本地库 + Asterisk | 与公寓端同进程、同 jar、同库 | 为值班台起第二个 jar / 第二数据源即越界 |
| 路由前缀 | `/platform/v1/**`(平台侧) | `/apt/**`(管理/终端/运维)、`/open/**`(跨端与设备) | `/duty/**` | `/duty/**` 出现在设备可达清单,或 `/open/**` 复用会话鉴权即越界 |
| 账号体系 | 平台账号(`gl_*` 自研 RBAC) | 本地 RuoYi 账号(`sys_user`)+ `gyid` 扩展属性 | 复用公寓端账号,再叠加权限点 | 平台账号登录公寓端、或公寓端写平台角色表即越界 |
补充三条判定细则(把"越界"变成可检查项,进 Code Review):
1. **表级**:任何 SQL 的 `where` 缺少 `gyid`(`sys_*`/`qrtz_*` 除外)视为缺陷,见 `03` §4。
2. **字段级**:写入 `ryzt` / `finish_jc` / `finish_ly` / `stoped` 的语句只允许出现在 `apt.duty.state` 包内,
其他位置出现即越界(可用架构约束测试固定,`docs/10` §5.3 第 3 条)。
3. **接口级**:`/apt/**` 与 `/duty/**` 的 Controller 不得互相调用对方的 Controller,只能共同调用领域服务。
## 3. 功能清单(10 个域)
表 1-4 功能域清单与实现判定
| # | 功能域 | 本期是否实现 | 依赖的平台能力 | 依赖的现场资源 | 后端模块 | 对应数据表 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 交路与计划 | **是**(快照只读 + 本地复算校验;本地应急新增受限) | 交路模板下行(表 6-1 #3)、规则下行(#4) | 无(纯数据) | `apt-master-client`、`apt-dispatch` | `apt_master_route`、`apt_master_rule`、`apt_holiday`、`gy_syn_jcxx`、`gy_syn_jcxx_status`、`gy_call_duty_match_lock` |
| 2 | 房间与在寓 | **是**(房态查询 + 停用/恢复 + 入住 / 换房 / 离寓;CR-02 CR-C02 已由"后续"调整为本期) | 房间基线下行(表 6-1 #5) | 房间屏、离寓抓拍机(可选) | `apt-stay` | `apt_master_room`、`gy_rooms`、`gy_jcxx`、`gygl_ly_event`、`gy_clean_task` |
| 3 | 叫乘执行与状态机 | **是**(最小闭环:任务 → Quartz → AMI / 平板 → 状态机;双模式见 §0 表 C-1) | 计划下行(#6)、任务变更(#7)、任务回执(#8) | Asterisk + AMI、平板、值班台话机 | `apt-dispatch` | `gy_jcxx`、`gy_jcxx_his`、`gy_bjxx`、`gy_bjcstj`、`duty_manual_action`、`duty_operation_log`、`apt_tablet_*` |
| 4 | 设备网关 | **是**(档案/心跳/指令队列;平板协议按 CR-02 §3 定为"设备拉取",真实指令下发等协议确认) | 无(设备策略取快照) | 房间屏、广播终端、抓拍机、消费机、平板 | `apt-device` | `gy_sb`、`gy_sb_xt`、`gy_sb_zl`(只读兼容)、`apt_device_command`、`apt_tablet_device`、`apt_device_alarm` |
| 5 | 语音与录音 | **是**(AMI、分机台账、会话/事件、录音索引、TTS 单元) | 语音配置下行(表 6-3 #23)、质量摘要上行(#24) | Asterisk、话机、录音目录 | `apt-voice` | `apt_sip_endpoint`、`apt_sip_binding`、`apt_trunk`、`apt_call_session`、`apt_call_event`、`apt_call_event_his`、`apt_recording`、`apt_tts_utterance`、`apt_ast_config_version`、`apt_ast_drift`、`apt_ast_ami_session` |
| 6 | 账务本地流水 | **是**(CR-02 CR-C03 由 P2 提升为本期最小闭环:离线受理、本地充值、台账上报;5 张预留表转启用) | 账务归集(表 6-1 #10)、余额快照下行、离线额度(#1) | 消费机、售货机、充值终端 | `apt-stay`(`apt.ledger.local` 子域) | `gy_ykt_remainder`、`gy_ykt_consume`、`gy_ykt_allowance`、`gy_ykt_cash_request`、`gy_ykt_xjczjl`、`apt_ledger_offline` |
| 7 | 上报 outbox | **是**(本地累积 + 水位;平台未就绪时只落库不发送) | 平台接收端(表 6-1 #9~#14) | 无 | `apt-report` | `apt_report_outbox`、`apt_report_watermark` |
| 8 | 主数据快照 | **是**(版本校验 + 快照生效 + 过期降级) | 公寓/人员/房间/规则/节假日下行(表 6-1 #1~#5) | 无 | `apt-master-client` | `apt_master_version`、`apt_master_apartment`、`apt_master_room`、`apt_master_person`、`apt_master_rule`、`apt_holiday`、`apt_person_account` |
| 9 | 运维自检 | **是**(启动自检 12 项 + 5 分钟巡检 + 两个健康探针) | 无 | Asterisk、录音磁盘、串口 | `apt-ops` | `apt_ops_selfcheck` |
| 10 | RBAC 与审计 | **是**(本地账号、菜单、按钮权限点、操作审计) | 平台人员的"人员—账号映射"下表 | 无 | `apt-system`、`apt-framework` | `sys_user/role/menu/dept`、`sys_oper_log`、`duty_operation_log`、`apt_person_account` |
各域的"模块—表—接口—页面—权限点"完整映射见 `04` §3~§7 与 `07` §4。
## 4. 本期不做与后续阶段
表 1-5 分期清单与启动前置条件
| 阶段 | 做 | 不做 | 启动前置条件 |
| --- | --- | --- | --- |
| 本期(对应 `docs/10` 阶段三试点 + CR-02) | 表 1-4 全部 10 个域(含账务)+ §0 表 C-1 的 6 个新增域 | 餐饮代理;跨公寓报表;`kcgl` | 单公寓库就绪(59 张业务表口径,CR-02 §6、§10);Asterisk 与 AMI 可达;分机注册状态可判定;平板在册;离线额度 Q-21 拍板 |
| 后续 P2-1 | 补助规则引擎化、餐饮结算规则、跨公寓考核报表重建 | 平台主账改造(属平台端) | D2 定稿;结算周期确定 |
| 后续 P2-2 | 运安接入上收平台后,本地只消费计划 | 本地抓 WSDL | D4 定稿;平台计划下行可用 |
| 后续(推广前) | 多公寓部署包、注册与版本管理、监控大盘 | 平台端收敛与旧逻辑下线 | 阶段四验收(表 9-5) |
| 明确不进公寓端 | — | `kcgl` 库存、`gl_*` 平台权限表、报表视图 `V_*`、餐饮业务 | D1、D10;`apartment/db/mysql/README.md` §7 第 5 条 |
**为什么账务改为本期(CR-02 CR-C03)**:`docs/公寓端/00` A-04 把"扣本地补贴、本地充值、断网期间充值只能本地使用"列为公寓端应做的功能,原 P2 分期与之冲突且会让叫乘现场的一卡通链路缺一半。裁定为"平台主账权威不变、公寓只做**额度内离线受理**、恢复后按 `serverReceivedAt` 归集冲正"的最小闭环,把一致性风险收敛在 `apt_ledger_offline` 一张表与 #10 一个接口内;补助规则引擎化仍留 P2-1。
## 5. 待决策点对边界的影响
表 1-6 D11–D16 对边界的影响与未定时的默认执行口径
| # | 决策点 | 定了会改变什么 | 未定时的默认执行口径(本设计采用) |
| --- | --- | --- | --- |
| D11 | 会议桥粒度 | 若"每任务一个会议桥",`apt_call_session.conf_no` 成为业务键的一部分,`apt_tts_utterance.conf_no` 需按会话唯一 | **沿用单 `Conf-01`**;`conf_no` 保持单值(`varchar(32)`)且**不加唯一约束**——定稿为"每任务一桥"时每会话仍是一个桥,无需改列;同时**限制同一瞬间只允许一个房间会话**并告警(`docs/10` 表 10-3 会议桥串音行) |
| D12 | 号段与分机编号规则 | 若要求全平台唯一,新增分机时需在线冲突检测(当前仅本地快照校验) | **本地按快照规则生成**,`apt_sip_endpoint` 以 `(gyid, extension)` 唯一;跨公寓冲突由平台最终检测;不建跨公寓校验表 |
| D13 | 平台端是否一并改 MySQL | 不影响公寓端边界,只影响联调环境 | 按表 5-8 选项 A(平台保持现库),公寓端只走 HTTP,不做跨库 JOIN |
| D14 | 操作系统 | Windows 需额外验证串口、录音路径、服务化 | **按 Linux 交付**;串口与路径全部走配置项,Windows 仅作开发机 |
| D15 | 录音合规 | 留存期、告知、可访问角色、导出 | **90 天 + 仅内网 + 导出必审计**;`apt_recording.retention_days` 存值,`PURGED` 只删文件不删索引 |
| D16 | Asterisk 高可用 | 双机主备会引入注册漂移、录音共享、会议桥状态同步 | **单机 + 冷备(RTO 15 分钟)+ 降级广播**;不做双活,`apt_ops_selfcheck` 只判单机健康 |
三条与边界直接相关的提醒:
1. **D11 未定前不要给 `conf_no` 加唯一约束**,否则定稿为"每任务一个会议桥"时要重建索引(`docs/10` 表 10-2 D11 原文即此意)。
2. **D15 未定前不要开放录音外链**,`/apt/voice/recordings/{id}/url` 只发短期签名地址(表 6-3 第 16 行)。
3. **D16 未定前禁止把公寓端做成可扩容**:单实例是 `docs/10` 表 2-3 与表 8-1 的硬约束,扩容前要先完成推送广播与任务抢占改造。
## 6. 边界维护机制
| 机制 | 做法 | 触发时点 |
| --- | --- | --- |
| 表归属检查 | 每张表在 `03` §1 标注 owner 与唯一写入模块;新增表必须同时更新该表 | 每次 DDL 评审 |
| SQL 检查 | 业务 SQL 必须带 `gyid`;状态字段写入语句必须命中白名单包 | CI 静态检查 + Code Review |
| 接口检查 | 新增 `/apt/**`、`/duty/**` 接口必须声明前缀、权限点、幂等要求 | 接口评审(`04` §1 模板) |
| 依赖检查 | Maven 模块依赖单向,`apt-duty` 不被任何模块依赖 | `mvn dependency:analyze` + 架构测试 |
| 边界争议裁决 | 争议项默认"业务在平台、事实在公寓"(`docs/10` 表 10-1 第 1 行) | 需求评审 |
@@ -0,0 +1,278 @@
# 02 技术基线与工程结构
> 追溯:`docs/10` §2.4 表 2-2(技术基线)、§2.5 图 2-3 与表 2-3(复用实现)、§4.3 表 4-3(模块清单)、
> §4.4 图 4-1 与表 4-4(依赖约束)、§5.3(编码与边界约束)、§6.2(三时间戳)。
> 现状来源:`apartment/README.md` §2、§4、§5(已实测的骨架事实)。
> **§12 补遗(CR-02 / `docs/公寓端/10`)**:人脸数据下发管理 `apt-face` **不新增第 13 个后端模块**,以子域形式落在既有 `apt-master-client`(人员主档快照与媒体下载)、`apt-report`(#9 上行)、`apt-device`(指令与缓存台账)、`apt-stay`(离寓事实消费)内;`apt.face.registry` / `apt.face.sync` / `apt.face.scope` / `apt.face.dispatch` 四个子域单向依赖,不引入第二数据源、不改本节技术基线锁定值。注册链路按 CR-C25 由签到终端申请、值班台确认,端内接口 N-34~N-37 分别落 `/open/v1/face/**` 与 `/duty/**`(见 `docs/公寓端/04` §0 表 C-6)。
> **§13 补遗(CR-02 / `docs/公寓端/11`)**:人员入住作业流程的 `apt.checkin.verify` / `apt.checkin.apply` 作为 `apt-stay` 内的受控子流程,**不新增第 13 个后端模块、不引入新数据源、不改本节技术基线锁定值**;端内接口 N-38~N-47 分别落 `/open/v1/checkin/**`、`/duty/checkin/**`、`/apt/checkin/**`(见 `docs/公寓端/04` §0 表 C-7)。
> **§14 补遗(CR-02 / `docs/公寓端/12`)**:消费、充值与菜品管理的 `apt.ledger.local`(额度查询 / 双额度 CAS 扣减 / 本地账务流水)、`apt.catering`(食谱只读下发与水位)、`apt.report.outbox`(#10 上报)均作为既有模块内的子域,**不新增第 13 个后端模块、不引入新数据源、不改本节技术基线锁定值**;端内接口 N-48~N-58 分别落 `/open/v1/ledger/**`、`/open/v1/catering/**`、`/duty/ledger/**`、`/apt/ledger/**`、`/apt/catering/**`(见 `docs/公寓端/04` §0 表 C-8)。新增权限点 `apt:catering:recipe:*`、`duty:ledger:confirm` 须前后端同码并登记 `sys_menu.perms`(§4.2);不新增业务表、不新增跨端编号。
> **§15 补遗(CR-02 §15 / 三端一致性 + 平台统计)**:四能力的模块归属与子域划分不变,**不新增第 13 个后端模块、不引入新数据源、不改本节技术基线锁定值**;平台统计 / 报表 / 排名能力落在平台端既有 `platform-report` 与 `platform-worker`(**单活**),本端不新增模块、依赖或定时任务。三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**;本端只在 #13 摘要上报评价 / 房间周转分组计数,不新增接口、不新增 `bizType`、不新增业务表,统计结果不回写本端业务状态。
## 1. 技术基线与版本锁定
表 2-1 版本锁定表
| 项 | 锁定值 | 约束 | 现状核对 |
| --- | --- | --- | --- |
| JDK | 1.8(本机 `jdk1.8.0_151`) | 不用 9+ 语法;CI 与本机同版本 | 已实测用该 JDK 打包与启动成功 |
| Spring Boot | 2.5.15 | 与 RuoYi-Vue `v3.8.9` 对齐,不单独升级 | `apartment/server/pom.xml` |
| RuoYi-Vue(后端) | `v3.8.9`(commit `5e6c917`) | **框架不魔改**:鉴权链、异常处理、分页插件不改 | `apartment/README.md` §1 |
| 持久层 | MyBatis(`mapper/*.xml`) | 新模块统一 XML;不新增 MyBatis-Plus 用法 | `docs/10` 表 2-2 持久层行 |
| 数据库 | MySQL 8.0(实测环境 9.5.0) | 一公寓一库;`utf8mb4`;InnoDB | `apt_apartment`,74 张表 |
| 排序规则 | `utf8mb4_general_ci` | 跟随库与 `sys_*`,避免 JOIN 报混排 | 见 `08` §4 差-1 |
| 事务隔离 | 目标 `READ-COMMITTED`(当前实例 `REPEATABLE-READ`) | 状态机一律条件更新,不依赖隔离级别兜底 | 见 `08` §4 差-2 |
| 定时任务 | Quartz(`apt-dispatch` 内嵌) | 单实例;当前 `RAMJobStore`,`qrtz_*` 已建未启用 | `apartment/db/mysql/README.md` §7 第 1 条 |
| 缓存与会话 | 本地 Redis | 会话、令牌、验证码、指令 nonce 去重 | 已实测验证码写入 Redis |
| 语音 | `asterisk-java 3.38.0-zfjia` + AMI + `Conf-01` | 只在公寓端使用;`optional` 依赖 | `apt-voice` |
| 前端 | Vue 2.6.12 + element-ui 2.15.14 + vue-router 3.4.9 + vuex 3.6.0 + vue-cli 4.4.6 | 单工程双入口,不引第二套 UI 库 | `apartment/web/package.json` |
| 构建 | Maven(后端)/ vue-cli 4 + Node ≥ 16(前端) | 前端需 `--openssl-legacy-provider` | `apartment/README.md` §5.2 第 6 条 |
| 接口文档 | springfox 3.0.0(`swagger.version`) | 用 `@Api`/`@ApiOperation`;生产关闭 | `apartment/server/pom.xml` |
| 端口 | 后端 8086、前端 dev 8082 | 不与既有 `yktgl` 冲突 | `apartment/README.md` §2 |
**三条硬约束**(`docs/10` §2.4 结尾):
1. 框架不魔改 —— 业务差异全部落在 `apt-*` 模块,升级 RuoYi 时不需要逐处合并。
2. 一套代码两个入口 —— 不存在"公寓端服务"与"值班服务"两个 jar。
3. 语音只在公寓内闭环 —— 平台端不得中转媒体或 AMI。
## 2. 工程结构与模块划分
### 2.1 后端 12 模块
表 2-2 模块职责、包内子域与 `docs/10` 对应
| 模块 | 职责 | 包内子域(本期规划) | 对应 `docs/10` 表 4-3 |
| --- | --- | --- | --- |
| `apt-common` | 工具、常量、注解、`AjaxResult`、错误码 | `constant`、`enums`、`exception`、`annotation` | 框架支撑 |
| `apt-system` | RBAC、菜单、字典、日志 | `system`(上游原样) | 框架支撑 |
| `apt-framework` | 安全链、数据源、拦截器、异常、Swagger | `config`、`interceptor`、`web` | 框架支撑 + 路由分区接入点 |
| `apt-master-client` | 平台配置下行、快照落库、版本校验 | `master` | `apt.master.cache` |
| `apt-stay` | 房间、在寓、房态重算、保洁、账务本地流水(预留) | `room`、`stay`、`ledger` | `apt.stay`、`apt.room`、`apt.ledger.local` |
| `apt-dispatch` | 叫乘执行引擎、Quartz、状态机 | `engine`、`state`、`quartz` | `apt.duty.engine`、`apt.duty.state` |
| `apt-voice` | AMI、分机与中继台账、会话与事件、录音与 TTS | `ami`、`endpoint`、`event`、`media` | `apt.voice.ami` / `endpoint` / `event` / `media` |
| `apt-device` | 设备网关、指令队列、设备档案 | `gateway`、`command`、`catalog` | `apt.device.gateway` / `command` / `catalog` |
| `apt-duty` | 值班 API、人工干预命令、WebSocket | `view`、`command`、`ws` | `apt.duty.view` |
| `apt-report` | 上报 outbox、幂等键、重试、水位 | `outbox`、`watermark` | `apt.report.outbox` |
| `apt-ops` | 自检、健康探针、时钟偏差、磁盘、版本 | `selfcheck`、`health` | `apt.ops` |
| `apt-admin` | **唯一启动模块**:`AptApplication` + 全部配置 + 打包 | `config` | `apt-admin` |
### 2.2 依赖方向
```mermaid
flowchart TD
ADMIN["apt-admin(唯一 jar)"] --> FRAME["apt-framework"]
ADMIN --> DUTY["apt-duty"]
ADMIN --> DEV["apt-device"]
ADMIN --> OPS["apt-ops"]
ADMIN --> REP["apt-report"]
ADMIN --> MSTR["apt-master-client"]
FRAME --> SYS["apt-system"]
SYS --> COMMON["apt-common"]
DUTY --> DISP["apt-dispatch"]
DUTY --> STAY["apt-stay"]
DISP --> VOICE["apt-voice"]
DISP --> STAY
DEV --> DISP
REP --> MSTR
OPS --> VOICE
OPS --> DEV
DISP --> REP
```
图 2-1 后端模块依赖(单向,无环)
表 2-3 依赖白名单与禁止项
| 模块 | 允许依赖 | 禁止依赖 | 依据 |
| --- | --- | --- | --- |
| `apt-voice`(`ami`) | `apt-common`、配置 | 任何业务模块 | `docs/10` 表 4-4 第 1 行 |
| `apt-voice`(`endpoint`) | `apt.voice.ami`、`apt-master-client` | `apt.voice.event`、`apt.duty.*` | 表 4-4 第 2 行 |
| `apt-voice`(`event`) | `apt.voice.ami`、`apt.voice.media`、`apt.duty.state` | `apt.device.*`、`apt.report.*`、`apt.duty.view` | 表 4-4 第 3 行 |
| `apt-voice`(`media`) | 存储、`apt-common` | `apt.duty.*` | 表 4-4 第 4 行 |
| `apt-dispatch` | `apt.voice.*`、`apt.device.command`、串口 | 直连 AMI(必须经 `apt.voice`) | 表 4-4 第 5 行 |
| `apt-dispatch`(`state`) | `apt-common` | 全部语音/设备模块(只能被调用) | 表 4-4 第 6 行 |
| `apt-duty` | `apt.voice.endpoint`、`apt.voice.event`、`apt.duty.state` | 直写状态字段 | 表 4-4 第 7 行 |
| **任何模块** | — | 依赖 `apt-duty` | `docs/10` §2.5 表 2-3 模块依赖行 |
**本期新增约定(不改变上表,只是补齐管理端)**:管理端 Controller 放在各业务模块的 `controller` 包下
(如交路在 `apt-master-client`、房间在 `apt-stay`、设备在 `apt-device`、叫乘在 `apt-dispatch`、语音在 `apt-voice`),
统一挂在 `/apt/**`;`apt-duty` 只承接 `/duty/**` 与 WebSocket,**不承载 `/apt/**` 的管理页接口**。
这样 `apt-duty` 保持"可被删除而不影响管理端"的边界。
### 2.3 前端结构与复用边界
表 2-4 前端目录与约束
| 位置 | 职责 | 允许 | 禁止 |
| --- | --- | --- | --- |
| `src/entries/apt/**` | 公寓端管理界面(固定路由) | 引用 `src/shared/**` | 引用 `src/entries/duty/**` |
| `src/entries/duty/**` | 值班台界面(动态路由,后续) | 引用 `src/shared/**` | 引用 `src/entries/apt/**` |
| `src/shared/api/**` | 请求封装与接口定义 | 被两个入口引用 | 引用任一 entry |
| `src/shared/store/**` | Vuex(用户、字典、权限) | 被两个入口引用 | 引用任一 entry |
| `src/shared/utils/request.js` | axios 实例、令牌、错误提示 | 被两个入口引用 | 引用任一 entry |
| `src/shared/directive/**` | `v-hasPermi`、`v-hasRole` | 被两个入口引用 | 引用任一 entry |
| `src/shared/components/**` | 通用组件(分页、字典标签) | 被两个入口引用 | 引用任一 entry |
复用清单(`docs/10` §2.5 表 2-3 前端路由行 + `apartment/README.md` §5.1):
1. `request` / `permission` / `utils` / `plugins` / `store` / `directive` / `components` 原样复用。
2. `shared/bootstrap.js` 承接上游 `main.js` 的全局装配(Element、全局方法、全局组件、字典、图标)。
3. 入口间零交叉引用,用 `grep` 做 CI 检查(`apartment/README.md` §5.1 已实测零引用)。
### 2.4 路由生成机制(本期细化)
`docs/10` 表 2-3 规定 `entry-duty` 走 `sys_menu` 动态路由、`entry-apt` 固定路由。本期管理页全部落在 `apt` 入口,
因此菜单机制需要说明清楚,避免"用了动态路由就违反固定路由约束":
表 2-5 菜单与路由的两条通道
| 通道 | 路由来源 | 菜单渲染 | 权限点来源 | 适用 |
| --- | --- | --- | --- | --- |
| apt 入口(本期) | 前端**静态路由表**(`entries/apt/router/index.js`) | 侧边栏由本地菜单配置渲染,按 `meta.permissions` 过滤 | `sys_menu` 中登记的按钮/菜单记录 → `/getInfo` 返回的 `permissions` | 管理页(交路/房间/叫乘/人员/设备/语音/运维) |
| duty 入口(后续) | `sys_menu` 动态路由(`includeBase=true`) | 后端 `getRouters` 下发 | 同上 | 值班盯控 |
这样处理的理由:`docs/10` 表 2-3 要求 apt 入口"不受角色影响"(路由集合固定),但按钮仍必须受权限控制;
若让 apt 也走动态路由,角色变更会导致管理端路由缺失,反而违背"设备/终端页必须永远可达"的初衷。
**代价**:`sys_menu` 里登记的 apt 菜单 `component` 不参与路由渲染,只作为权限点载体,需在菜单备注中写明。
该细化列入 `08` §4 待确认(差-7)。
## 3. 分层与编码规范
### 3.1 请求链路
```mermaid
flowchart LR
C["Controller<br/>参数校验 + 转换"] --> S["Service 接口"]
S --> I["ServiceImpl<br/>事务边界 + 领域校验"]
I --> M["Mapper(XML)"]
I --> G["领域服务<br/>apt.duty.state / apt.voice.*"]
M --> DB[("MySQL")]
I --> A["审计<br/>duty_operation_log / sys_oper_log"]
```
图 2-2 分层与写链路
表 2-6 分层职责与强制项
| 层 | 做什么 | 不做什么 | 强制项 |
| --- | --- | --- | --- |
| Controller | 接参、`@Validated` 校验、DTO↔VO、返回统一体、`@ApiOperation` | 不写业务判断、不开事务、不拼 SQL | 类上 `@RestController` + 路径以 `/apt` 开头;写操作加 `@Log(title=…, businessType=…)` |
| Service(接口) | 声明领域能力 | 不放实现细节 | 方法名用业务动词(`createRoute`、`changeRoomStatus`) |
| ServiceImpl | 事务、领域规则、状态机命令组装、审计落库 | 不直接改状态字段(除 `apt.duty.state` 自身) | 写方法 `@Transactional(rollbackFor = Exception.class)`;只读标 `readOnly` |
| Mapper | 单表/视图查询、条件更新 | 不做业务判断、不跨域 JOIN | 业务 SQL 必带 `gyid`;条件更新必须 `where … and state = ?` |
| 领域服务 | 状态迁移、状态机命令、幂等 | 不依赖 Web 层对象 | 唯一写入口,见 `03` §3 |
### 3.2 对象与命名
表 2-7 对象分层
| 对象 | 用途 | 命名 | 位置 |
| --- | --- | --- | --- |
| Entity | 与表一一对应 | `Xxx`(如 `AptMasterRoute`) | `domain` |
| DTO | 入参 | `XxxCreateDTO` / `XxxUpdateDTO` / `XxxQueryDTO` | `dto` |
| VO | 出参 | `XxxVO` / `XxxDetailVO` | `vo` |
| Command | 状态机/领域命令 | `XxxCommand` | `command` |
| Query | 内部查询条件(跨表) | `XxxQuery` | `query` |
命名细则:
1. 类名用业务中文语义的英文(`Route`/`Room`/`Jcxx`/`Person`/`Device`),不用 `Data`/`Info`/`Bean` 这类空词。
2. 表名与实体名的映射遵循 MyBatis 默认驼峰规则;`apt_master_route → AptMasterRoute`、`gy_jcxx → GyJcxx`。
3. 枚举类集中在 `apt-common` 的 `enums` 包,与表里的 CHECK 取值一一对应(如 `RouteState`、`RoomStatus`、`JcxxRyzt`)。
### 3.3 统一返回、分页、校验、异常
表 2-8 接口层统一约定
| 项 | 约定 | 实现位置 |
| --- | --- | --- |
| 返回体 | 若依 `AjaxResult`(`code`/`msg`/`data`);列表用 `TableDataInfo`(`total`/`rows`/`code`/`msg`) | `apt-framework`(上游原样) |
| 分页参数 | `pageNum`(默认 1)、`pageSize`(默认 10,上限 100)、`orderByColumn`、`isAsc` | 若依 `PageDomain` + `startPage()` |
| 排序 | 未传排序时用业务默认排序(见 `04` 各接口),**不允许**按自增主键默认排序 | Service 层拼 `order by` |
| 参数校验 | JSR-303(`@NotBlank`/`@Size`/`@Pattern`/`@NotNull`)+ 业务唯一性在 Service 校验 | DTO + Service |
| 业务异常 | `throw new ServiceException(msg, code)`;全局 `GlobalExceptionHandler` 统一转 `AjaxResult` | `apt-common` + `apt-framework` |
| 错误码 | 业务码集中在 `AptErrorCode`(见 `04` §2),前缀 `APT-` | `apt-common` |
| 时间格式 | 出参 `yyyy-MM-dd HH:mm:ss`(秒级展示)、`yyyy-MM-dd HH:mm:ss.SSS`(事件/会话);入参只收 `yyyy-MM-dd HH:mm:ss` | Jackson 配置(上游原样) |
| 三时间戳 | 上报类对象必须有 `occurredAt`、`receivedAt`,平台回执补 `platformRecvAt` | `docs/10` §6.2;`apt_report_outbox` 三列 |
### 3.4 `gyid` 强制注入
表 2-9 注入点与校验点
| 环节 | 做法 | 失败表现 |
| --- | --- | --- |
| 启动 | `apt.apartment.gy-id` 必填,为空直接启动失败 | 启动异常,日志明确 |
| 登录 | `gyid` 写入 `LoginUser` 扩展属性与令牌声明 | — |
| 请求 | 拦截器从令牌取 `gyid`,放入 `AptContext`(ThreadLocal)+ 请求属性 `ATTR_PARTITION` 同级 | 取不到 → 401/403 |
| Service | 写操作从 `AptContext` 取,**不接受前端传 `gyid`**;读操作允许前端传 `gyid` 但必须与之相等 | `APT_GYID_MISMATCH` |
| Mapper | SQL 必带 `gyid = #{gyid}` | Code Review 阻断 |
| 设备 | `deviceCode + gyid` 绑定校验,跨公寓直接拒绝 | `APT_DEVICE_GYID_MISMATCH` |
| 旧兼容 | `getGyIdByIp` 只作旧设备兜底并打告警 | 告警日志 + 指标 |
依据:`docs/10` §3.3"公寓隔离方式"4 条 + §5.3 第 5 条。
## 4. 路由与鉴权
### 4.1 四前缀拦截器链
表 2-10 路由分区(已存在的骨架接点)
| 前缀 | 用途 | 鉴权方式 | 拦截器链 | 骨架现状 |
| --- | --- | --- | --- | --- |
| `/apt/**` | 管理 / 终端 / 运维 | 会话 + 权限点 | `RoutePartitionInterceptor` + Spring Security + `@PreAuthorize("@ss.hasPermi(…)")` | 已注入分区属性 `apt.route.partition` |
| `/duty/**` | 值班员 | 会话 + 权限点 + `gyid` 数据域 | 同上 + 数据域校验 | 同上 |
| `/system/**` | 若依框架端点 | 若依默认 | 若依原有链 | 同上 |
| `/open/**` | 跨端与设备 | 设备签名 / mTLS(**不复用会话**) | 独立拦截器(待实现) | 仅一个 `@Anonymous` 占位 |
现状事实:`apt-framework` 的 `AptWebMvcConfig` 已注册四条 `RoutePartitionInterceptor`,常量 `ATTR_PARTITION = "apt.route.partition"`;
`/open/**` 的签名校验、`/apt/**` 的权限点校验仍是 TODO(`apartment/README.md` §4.2、§8 第 3 条)。
### 4.2 权限点命名
格式:`{端}:{域}:{对象}:{动作}`,全小写,冒号分隔。端取 `apt`(管理)或 `duty`(值班)。
表 2-11 动作词表
| 动作 | 含义 | 对应 HTTP |
| --- | --- | --- |
| `list` | 列表查询 | GET 列表 |
| `query` | 详情查询 | GET 详情 |
| `add` | 新增 | POST |
| `edit` | 编辑 | PUT |
| `remove` | 删除(逻辑) | DELETE |
| `export` | 导出 | POST |
| `changeStatus` | 启停/状态变更 | PUT |
| `correct` | 人工修正(受控写) | POST |
| `command` | 下发设备指令 | POST |
| `replay` | 录音回放/取地址 | GET |
表 2-12 本期权限点清单(与 `04` 接口一一对应)
| 域 | 权限点 | 说明 |
| --- | --- | --- |
| 交路 | `apt:route:list`、`apt:route:query`、`apt:route:add`、`apt:route:edit`、`apt:route:changeStatus` | 本地应急新增/停用受 D7 约束 |
| 房间 | `apt:room:list`、`apt:room:query`、`apt:room:edit`、`apt:room:changeStatus` | 房态只读,`edit` 只改本地属性 |
| 叫乘 | `apt:jcxx:list`、`apt:jcxx:query`、`apt:jcxx:correct`、`apt:jcxx:changeStatus` | 状态变更经状态机;`changeStatus` 仅"停止叫乘/强制结束"类命令 |
| 人员 | `apt:person:list`、`apt:person:query`、`apt:person:export` | 快照只读;启停见 `03` §5 缺口 G-02 |
| 设备 | `apt:device:list`、`apt:device:query`、`apt:device:add`、`apt:device:edit`、`apt:device:changeStatus`、`apt:device:command` | `command` 只入队,不代表下发成功 |
| 语音 | `apt:voice:endpoint:list`、`apt:voice:endpoint:add`、`apt:voice:endpoint:edit`、`apt:voice:endpoint:changeStatus`、`apt:voice:endpoint:remove`、`apt:voice:call:list`、`apt:voice:recording:list`、`apt:voice:recording:replay` | 对齐 `docs/10` 表 6-3 第 1~17 行 |
| 上报 | `apt:report:list`、`apt:report:replay` | outbox 查看与重发 |
| 运维 | `apt:ops:health`、`apt:ops:selfcheck`、`apt:ops:reconcile` | 对齐表 6-3 第 19~21 行 |
| 值班(后续) | `duty:call:stop`、`duty:call:recall`、`duty:room:swap` | `docs/10` 表 2-3 示例,本期不实现 UI |
约束:
1. 前端 `v-hasPermi` 与后端 `@PreAuthorize` **必须同码**,不允许只做前端隐藏(`docs/10` 表 2-3 负面清单)。
2. 权限点必须在 `sys_menu` 中登记(`perms` 列),否则 `admin` 之外的角色拿不到。
3. 新增权限点必须同时更新 `表 2-12` 与 `04` 的映射表,避免"接口有、菜单没有"。
### 4.3 数据域与鉴权的关系
表 2-13 数据域与鉴权校验矩阵
| 场景 | 校验 | 边界 |
| --- | --- | --- |
| 管理端查列表 | 会话 + 权限点 + `gyid` 注入 | 只看本公寓 |
| 管理端写配置 | 会话 + 权限点 + `gyid` + 唯一性校验 | 交路/分机等有业务键 UK |
| 设备上报 | 设备签名 + `deviceCode` 与 `gyid` 绑定 | 跨公寓直接拒绝 |
| 平台下行 `/open/v1/**` | mTLS/双向令牌 + `gyId` 参数校验 | 只做配置与摘要 |
+298
View File
@@ -0,0 +1,298 @@
# 03 数据设计
> 追溯:`docs/10` §5.1 表 5-1(数据分层与归属)、§5.2 表 5-2(一致性策略)、§5.3(同库边界 6 条)、
> §5.4 表 5-3 与表 5-4(语音域表与字段口径)、§5.6 表 5-6 与表 5-9(MySQL 适配与必查清单)、
> §7.8 表 7-4(分机资源状态机)、§7.9 表 7-5(事件与状态迁移)。
> **v2 修订(CR-02,2026-11-09)**:表数口径、新增表与补列以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §6 为权威,见下文 §0。
## 0. v2 数据口径修订(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 3-y` 序号;§1 表 3-1~3-7 的 44 张归属矩阵继续有效,以下为增量。
表 C-1 表数口径统一(CR-C14)
| 口径 | 张数 | 说明 |
| --- | --- | --- |
| v1.2 本文件 §1 | 44 | 表 3-1~3-7 |
| CR-01 追加 | 1 | `apt_local_task`(CR-01 已定义,原表 3-2 未登记) |
| CR-02 新增 | 13 | 公寓 owner 11 张 + duty owner 2 张 |
| CR-02 v1.1 补遗 | 1 | `apt_sip_reg_status`(T-17,语音域注册状态流水;见 `docs/公寓端/00` §3、CR-02 §10) |
| **合计** | **59** | 不含 `sys_*` / `qrtz_*` 30 张框架表;v1.0 口径 58 张按本行理解 |
表 C-2 CR-02 新增 14 张表归属(列级定义见 CR-02 表 3-27~3-31、§10)
| 表 | owner | 唯一写入模块 | 上报源 | 本期 |
| --- | --- | --- | --- | --- |
| `apt_tablet_device` | apartment | `apt.device.gateway` | 是(#11 摘要) | 是 |
| `apt_tablet_task` | apartment | `apt-dispatch` | 否 | 是 |
| `apt_tablet_call_result` | apartment | `apt.device.gateway` → 命令 | 是(`TABLET_RESULT` / #9) | 是 |
| `apt_person_face_reg` | apartment | `apt.face.registry`(CR-C25 由 `apt-stay` 改判) | 是(`PERSON_REG` / #9,**值班员注册成功后**) | 是 |
| `apt_transfer_order` | apartment | `apt-stay`(经 `apt.duty.state`) | 是(`TRANSFER` / #8、#9) | 是 |
| `apt_clean_staff` | apartment | `apt-stay` | 否 | 是 |
| `apt_clean_record` | apartment | `apt-stay` | 是(#13 摘要) | 是 |
| `apt_device_alarm` | apartment | `apt.device.gateway` | 是(`DEVICE_ALARM` / #12) | 是 |
| `apt_report_history` | apartment | `apt.report.outbox` | 本身即上报历史 | 是 |
| `apt_report_retry_batch` | apartment | `apt.report.outbox` | 是(#14 心跳) | 是 |
| `apt_ledger_offline` | apartment | `apt.ledger.local` | 是(`LEDGER_OFFLINE` / #10) | 是 |
| `duty_roster` | duty | `apt-duty` | 否 | 是 |
| `duty_performance` | duty | `apt-duty` | 是(#13 日汇总) | 是 |
| `apt_sip_reg_status` | apartment | `apt.voice.event`(只追加) | 是(#11 / #24 摘要,明细不上报) | 是 |
表 C-3 既有表补列(设计定义,Q-20 未决前不执行 `building`)
| 表 | 补列 | 用途 |
| --- | --- | --- |
| `gy_jcxx` | `call_mode` / `dispatch_seq` | 双模式分流与按 `jcsj` 顺序派发 |
| `apt_report_outbox` | `retry_batch_id` | 关联续报批次 |
| `apt_master_room` | `building` | 楼栋(G-01 正式落点,Q-20) |
命名分区同步:表 3-8 的 `apt_*` 23 张、`duty_*` 3 张为 v1.2 口径;CR-02 后 `apt_*` 34 张、`duty_*` 5 张,§10 补遗新增 `apt_sip_reg_status` 后 `apt_*` **35 张**、`gy_*` / `gygl_*` 18 张(含 `apt_local_task` 计入 `apt_*`),合计 **59 张**。新增表全部带代理主键、以 `gyid` 打头的唯一键与查询索引,不复制 `docs/11` 记录的 0 主键 / 0 索引缺陷;状态字段单写入口(§3)继续有效。
> 语音域补遗(`docs/公寓端/00` §3.2):`apt_sip_endpoint.register_status` 枚举补充为 `ONLINE` / `OFFLINE` / `UNREACHABLE` / `UNKNOWN`(当前态唯一权威字段);`apt_sip_reg_status` 为只追加的状态变迁流水,两者主从,不构成第二真源;表 3-11 的 `sync_state` 四值状态机不变。
> 叫乘两类方式补遗(`docs/公寓端/05` §9):SIP 语音叫乘落 `apt_call_session` / `apt_call_event` / `apt_recording` / `apt_tts_utterance`,状态由 AMI 事件经状态机命令写入;平板叫乘落 `apt_tablet_task`(`jcsj` 为平板本地播放时刻、`dispatch_seq` 单调)与 `apt_tablet_call_result`(`result_id` 幂等、`applied_state` 表达生效 / 忽略),状态由平板上报结果经状态机命令写入。两者共用 `gy_jcxx.ryzt` / `finish_jc` 一套终态语义,不新增表、不新增状态字段。
> 人脸数据下发补遗(CR-02 §12 / `docs/公寓端/10`):**不新增业务表**,59 张口径不变;复用 `apt_master_person`(人员人脸主档本地权威副本)、`apt_person_face_reg`(注册申请与同步流水)、`gy_sb` / `gy_sb_xt`(设备可见范围与缓存台账)、`apt_device_command`(下发 / 擦除指令)并补列(表 10-6)。`apt_person_face_reg` 由 `apt.face.registry` 唯一写入,新增 `reg_state`(`APPLIED` / `CONFIRMED` / `REJECTED` / `ARCHIVED`)、`apply_device_code` / `confirmed_by` / `confirmed_at` 与 `upload_state` / `dispatch_state` 双状态链;注册生效点为值班员 `CONFIRM`(CR-C25),确认后由 `apt-master-client` 事务写 `apt_master_person`,该表对业务模块仍只读。
> **CR-02 §13 补遗(入住申请单)**:**不新增业务表**,59 张口径不变;入住申请单作为 `apt_local_task`(`docs/设计变更/01` 表 1-17「本地建单」表)的申请阶段记录,复用 `origin=CHECKIN_ROUTE` / `CHECKIN_PENDING` 与 `match_state` 五值,补 `apply_id`、`checkin_state`、`sync_state`、`fjh` / `fjh_final` / `room_adjust_seq`、`ryid` / `face_*` / `jlid` / `review_*` / 四个时间戳 / `biz_version` 等列(列级定义见 `docs/公寓端/11` 表 11-7,均为设计定义,DDL 待阶段二冻结,Q-40)。`apply_id` ↔ 预分配 `jcid` 一对一,**申请阶段不建 `gy_jcxx` 行**,审核通过办理入住时由 `apt-stay` 沿用同一 `jcid` 建行;`checkin_state` / `sync_state` 是流程态,**不是** `gy_jcxx` 状态字段,`ryzt` / `finish_*` 仍只经 `apt.duty.state` 条件更新。
> **CR-02 §14 补遗(消费 / 充值 / 食谱)**:**不新增业务表**,59 张口径不变;账户额度、消费流水、充值记录复用 `gy_ykt_remainder`(`knye` 补贴额度 / `xjye` 现金额度)、`gy_ykt_consume`、`gy_ykt_allowance`、`gy_ykt_cash_request`、`gy_ykt_xjczjl` 五张预留表与 `apt_ledger_offline`,食谱主数据复用平台库 `YKT_MEALS` 并补 `recipe_id` / `recipe_version` / `meal_period` / `effective_from/to` / `publish_state` / `published_*` 等列(列级定义见 `docs/公寓端/12` 表 12-7~12-10,均为设计定义,DDL 待阶段二冻结,Q-43)。`apt_ledger_offline` 补 `ledger_state`(流程态)与双额度 CAS 前后值,`gy_ykt_remainder` 补 `local_balance_version` / `reconcile_state` / `quota_ref`;**`ledger_state` 不是余额状态字段、更不是 `gy_jcxx` 状态字段**,`request_state` 沿用 `PENDING` / `COMPLETED` / `CANCELLED` 三值。食谱缓存只在消费终端本地,`YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 本期不落库;`GY_YKT_REMAINDER` 仍是唯一余额表,不新造同义账户表。
> **CR-02 §15 补遗(三端一致性 / 统计数据源)**:本端**数据结构不变、不新增业务表(59 张不变)**:#13 `ARCHIVE` 摘要按 `payload` 扩展 `satisfaction{}` / `roomOccupancy{}` 分组计数属**上报字段扩展**,不落本端新列。统计所需数据全部取自平台库 `PL_REPORT_DAILY` / `PL_LEDGER_FLOW` / `PL_RECEIVE_LOG` 及其补列(列级定义见 `docs/平台端/10` 表 10-10);新增 `pl_apt_stat_period` / `pl_apt_stat_rank` 2 张属**平台库**(另计,累计 5 张,见 `docs/平台端/03` §0 表 C-3),本端零写权限、不感知统计。三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,统计结果不回写 `gy_ykt_*` / `apt_ledger_offline` / `gy_jcxx` 等任何业务表;`apt.ledger.local` 仍是本端账务唯一写入模块。
> 现状来源:`apartment/db/mysql/*.sql`(实际 DDL,已建 44 张)与 `apartment/db/mysql/README.md`。
## 1. 表归属矩阵(v1.2 口径 44 张;CR-02 口径 59 张,增量见 §0)
约定四列含义:
- **owner**:数据归属端(`apartment` = 执行事实与本地资源;`duty` = 人工操作记录;`platform-down` = 平台下发快照,公寓只读)。
- **唯一写入模块**:允许执行 `insert`/`update` 的模块,其他模块只能读。
- **上报源**:该表是否产生上行 outbox 记录(`docs/10` 表 7-3 上报边界)。
- **本期**:是否在本期接入代码(`是`/`预留`)。
表 3-1 主数据快照域(7 张,`01_master_data.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `apt_master_version` | platform-down | `apt-master-client` | 全部业务模块(版本校验) | 否 | 是 |
| `apt_master_apartment` | platform-down | `apt-master-client` | `apt-ops`、管理端 | 否 | 是 |
| `apt_master_room` | platform-down | `apt-master-client` | `apt-stay`、管理端 | 否 | 是 |
| `apt_master_person` | platform-down | `apt-master-client` | `apt-dispatch`、`apt-duty`、管理端 | 否 | 是 |
| `apt_master_route` | platform-down | `apt-master-client` | `apt-dispatch`(复算)、管理端 | 否 | 是 |
| `apt_master_rule` | platform-down | `apt-master-client` | `apt-dispatch`、`apt-voice`、`apt-ops` | 否 | 是 |
| `apt_holiday` | platform-down | `apt-master-client` | `apt-dispatch`(复算) | 否 | 是 |
表 3-2 计划与任务域(3 张,`02_plan_task.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `gy_syn_jcxx` | apartment | `apt-master-client`(计划下行落库) | `apt-dispatch`(生成任务)、管理端 | 否 | 是 |
| `gy_syn_jcxx_status` | apartment | `apt-master-client` | `apt-ops`、管理端 | 否 | 是 |
| `gy_call_duty_match_lock` | apartment | `apt-dispatch`(租约锁) | `apt-dispatch` | 否 | 是 |
表 3-3 执行域(7 张,`03_execution.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `gy_jcxx` | apartment | `apt.duty.state`(状态字段)+ `apt-stay`(入住/离寓事实) | 管理端、`apt-duty`、`apt-report` | **是**(`DUTY_RESULT`) | 是 |
| `gy_jcxx_his` | apartment | 归档任务 | 报表、管理端历史查询 | 是(`ARCHIVE` 摘要) | 是(只读查询) |
| `gygl_ly_event` | apartment | `apt-stay`(设备与人工登记) | `apt.duty.state`、管理端 | 是 | 是 |
| `gy_rooms` | apartment | `apt-stay`(房态重算) | 管理端、`apt-duty` | 否(仅摘要) | 是 |
| `gy_clean_task` | apartment | `apt-stay` | 管理端 | 否 | 是(列表只读) |
| `gy_bjxx` | apartment | `apt-dispatch`(判定) | 管理端、`apt-duty`、`apt-report` | 是(`ALARM`) | 是 |
| `gy_bjcstj` | apartment | 统计任务(每日汇总) | 管理端、`apt-report` | 是(`ALARM` 摘要) | 是 |
表 3-4 设备域(4 张,`04_device.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `gy_sb` | apartment | `apt.device.catalog` | 管理端、`apt-report` | 是(`DEVICE_STATUS` 摘要) | 是 |
| `gy_sb_xt` | apartment | `apt.device.gateway`(心跳) | 管理端 | 是(版本/在线率摘要) | 是 |
| `gy_sb_zl` | apartment | **只读**(存量兼容,不新增写入) | 管理端(历史指令) | 否 | 是(只读) |
| `apt_device_command` | apartment | `apt.device.command` | 管理端、设备网关 | 否(指令明细不上报,表 7-3) | 是 |
表 3-5 语音域(11 张,`05_voice.sql`,对应 `docs/10` 表 5-3)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `apt_sip_endpoint` | apartment | `apt.voice.endpoint` | 管理端、`apt-voice.event` | 是(在线率摘要) | 是 |
| `apt_sip_binding` | apartment | `apt.voice.endpoint` | 管理端、`apt-stay`(换房) | 否 | 是 |
| `apt_trunk` | apartment | `apt.voice.endpoint` | 管理端 | 是(摘要) | 是 |
| `apt_call_session` | apartment | `apt.voice.event` | 管理端、`apt-duty` | 是(语音质量摘要) | 是 |
| `apt_call_event` | apartment | `apt.voice.ami`(只追加) | 排障、`apt-voice.event` | 否(表 7-3:AMI 事件不上报) | 是 |
| `apt_call_event_his` | apartment | 归档任务 | 排障 | 否 | 是(只读) |
| `apt_recording` | apartment | `apt.voice.media` | 管理端、`apt-duty` | 是(缺失数摘要) | 是 |
| `apt_tts_utterance` | apartment | `apt.voice.media` | 管理端(试听记录) | 否 | 是 |
| `apt_ast_config_version` | apartment | `apt.voice.endpoint` | 管理端 | 否 | 是 |
| `apt_ast_drift` | apartment | `apt.voice.endpoint`(对账) | 管理端 | 是(漂移数摘要) | 是 |
| `apt_ast_ami_session` | apartment | `apt.voice.ami` | 管理端、`apt-ops` | 是(重连次数摘要) | 是 |
表 3-6 账务本地域(5 张,`06_ledger.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `gy_ykt_remainder` | apartment(缓存) | `apt.ledger.local` | 管理端(后续) | 否 | 预留 |
| `gy_ykt_consume` | apartment | `apt.ledger.local` | 管理端(后续) | 是(`LEDGER`) | 预留 |
| `gy_ykt_allowance` | apartment | `apt.ledger.local` | 管理端(后续) | 是(`LEDGER`) | 预留 |
| `gy_ykt_cash_request` | apartment | `apt.ledger.local` | 管理端(后续) | 是(`LEDGER`) | 预留 |
| `gy_ykt_xjczjl` | apartment | `apt.ledger.local` | 管理端(后续) | 是(`LEDGER`) | 预留 |
表 3-7 上报 / 值班 / 运维域(7 张,`07_report_duty_ops.sql`)
| 表 | owner | 唯一写入模块 | 主要读取方 | 上报源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| `apt_report_outbox` | apartment | `apt.report.outbox` | 管理端、`apt-report` | 本身即上报源 | 是 |
| `apt_report_watermark` | apartment | `apt.report.outbox` | 管理端、`apt-ops` | 是(`heartbeat`) | 是 |
| `duty_operation_log` | duty | 所有写操作(经审计切面) | 管理端(审计查询) | 否 | 是 |
| `duty_manual_action` | duty | `apt.duty.command`(转状态机) | 管理端、`apt-duty` | 是(`DUTY_RESULT`) | 是 |
| `duty_shift_handover` | duty | `apt-duty` | 管理端 | 否 | 后续 |
| `apt_ops_selfcheck` | apartment | `apt-ops` | 管理端 | 是(健康摘要) | 是 |
| `apt_person_account` | platform-down | `apt-master-client` | `apt-system`(账号映射) | 否 | 是 |
## 2. 命名分区与命名规则
表 3-8 命名分区(`docs/10` §5.3 第 2 条)
| 前缀 | 语义 | 本期表数 | 命名规则 | 备注 |
| --- | --- | --- | --- | --- |
| `apt_*` | 公寓端新增域(语音、快照、上报、运维、主数据) | 23 | `apt_{子域}_{对象}` | 子域:`master`/`sip`/`call`/`recording`/`tts`/`ast`/`report`/`device`/`ops`/`person` |
| `gy_*` / `gygl_*` | 存量执行域(迁移友好) | 18 | 沿用存量名,不重命名 | `gygl_` 是存量历史前缀,保留;其中 4 张为本期新增但沿用 `GY_` 前缀(见 `08` 差-6) |
| `duty_*` | 值班操作域 | 3 | `duty_{对象}` | `docs/10` §3.3 写 `DUTY_OPERATION`、§5.3 写 `DUTY_*`,实际表名为小写带后缀(见 `08` §4 差-4) |
| `sys_*` / `gen_table*` / `qrtz_*` | 若依框架 | 30 | 不改 | `sys_*` 无 `gyid` |
**字段命名规则**:
1. 业务键沿用存量语义(`jcid`、`jlid`、`ryid`、`fjh`),不新造同义字段。
2. 时间列一律 `xxx_at` 或沿用存量 `xxx_sj`;存量列不改名(迁移成本高于收益)。
3. 布尔列用 `tinyint`(0/1),存量 `char(1)`(`finish_jc` 等)不改类型,避免存量数据迁移风险。
4. 金额 `decimal(12,2)`、毫秒 `bigint`(`event_at_ms`)、长文本 `text` / `json`(`docs/10` 表 5-4)。
## 3. 状态字段与单写入口
### 3.1 状态字段清单
表 3-9 状态字段与唯一写入口(`docs/10` §5.3 第 6 条)
| 表.字段 | 取值 | 唯一写入口 | 触发来源 | 条件更新要求 |
| --- | --- | --- | --- | --- |
| `gy_jcxx.ryzt` | 0 待叫乘 / 1 叫乘中 / 2 叫乘报警 / 3 已应答待离寓 / 4 离寓报警 / 5 通话中 | `apt.duty.state` | 任务触发、AMI 事件、超时判定、人工命令 | `where id=? and ryzt=?`(期望值) |
| `gy_jcxx.finish_jc` | 0 未结束 / 1 已结束 | `apt.duty.state` | 应答、叫乘报警、人工强制结束 | 同上 |
| `gy_jcxx.finish_ly` | 0 在寓 / 1 已离寓 | `apt.duty.state` | 离寓事件(设备/人脸/人工) | 同上 |
| `gy_jcxx.stoped` | 0 否 / 1 是 | `apt.duty.state` | 人工停止叫乘 | `where stoped='0'` |
| `gy_rooms.room_status` | 1 空闲 / 2 住满 / 3 待清扫 / 4 不能入住 / 5 未满 / 6 保洁中 | `apt-stay`(房态重算) | 在住人数变化、保洁、人工置位 | `where id=? and state_version=?`(乐观锁) |
| `gy_clean_task.state` | PENDING / PROCESSING / DONE / CANCELLED | `apt-stay` | 人工操作 | `where id=? and state=?` |
| `gy_bjxx.handled_at/handled_by` | — | `apt-dispatch`(写入)/ `apt-duty`(处理) | 报警处理 | `where id=? and handled_at is null` |
| `apt_sip_endpoint.sync_state` | PENDING / SYNCED / FAILED / DRIFT(**仅此 4 值**,表上有 CHECK) | `apt.voice.endpoint` | 收敛结果、对账 | `where id=? and sync_state=?` |
| `apt_device_command.state` | PENDING / SENT / ACKED / EXPIRED / FAILED | `apt.device.command` | 投递、ACK、超时 | `where command_id=? and state in (...)` |
| `apt_report_outbox.state` | PENDING / SENDING / SENT / FAILED / DEAD | `apt.report.outbox` | 发送结果 | `where id=? and state=?` |
| `apt_recording.state` | RECORDING / AVAILABLE / MISSING / PURGED | `apt.voice.media` | 事件、巡检、清理 | `where id=? and state=?` |
| `gy_ykt_cash_request.request_state` | PENDING / COMPLETED / CANCELLED | `apt.ledger.local` | 人工审核 | `where id=? and request_state='PENDING'` |
| `sys_user.status` / `sys_menu.visible` | — | `apt-system`(若依) | 管理端 | 若依原逻辑 |
### 3.2 叫乘状态机迁移
表 3-10 `gy_jcxx` 状态迁移矩阵(`ryzt` + `finish_jc` + `stoped` 组合)
| 当前态 | 事件/命令 | 目标态 | 同表写入 | 依据 |
| --- | --- | --- | --- | --- |
| 0 待叫乘 | 任务到期触发 Originate | 1 叫乘中 | `jcsj_s = now(3)` | `docs/10` 表 7-5 `OriginateResponse` |
| 0/1 | 人工"临时叫乘"命令 | 1 叫乘中 | `jcsj_s`、`is_temp_call='1'` | 表 4-3 `apt.duty.state` |
| 1 叫乘中 | `ConfbridgeJoin`(房间侧入会) | 3 已应答待离寓 | `hjxysj`、`finish_jc='1'` | `docs/10` §7.7 第 2 条、表 7-5 |
| 1 叫乘中 | 30s 无入会(本地超时) | 2 叫乘报警 | `jcbjsj_s`、`is_jcbj='1'`、写 `gy_bjxx` | `docs/10` §7.9 超时口径 |
| 2 叫乘报警 | 复叫入会 | 3 已应答待离寓 | `fjsj_s`、`hjxysj`、`finish_jc='1'` | 表 7-5 复叫 |
| 2 叫乘报警 | 人工"停止叫乘" | 2(保持)+ `stoped='1'` | `stoped` | `docs/10` 表 2-3 `duty:call:stop` |
| 3 已应答 | 会议桥通话开始 | 5 通话中 | — | 表 5-3 `apt_call_session.state` |
| 5 通话中 | 挂断 | 3 | — | 同上 |
| 3 已应答 | 超时未离寓 | 4 离寓报警 | `lybjsj`、`is_lybj='1'` | 表 4-3 `apt.duty.engine` |
| 任意 | 离寓事件(设备/人脸/人工) | 保持 `ryzt`,置离寓 | `finish_ly='1'`、`sjlysj`、`leave_type`、`leave_event_id` | `docs/05` 离寓语义 |
| 任意 | 人工"强制结束" | 保持,置结束 | `finish_jc='1'`、`finish_ly='1'` | `duty_manual_action.action_type=FORCE_FINISH` |
三条保护语义(沿用存量,`docs/10` §5.4 处理关系第 3 条):
1. **已结束叫乘不回写**:`finish_jc='1'` 后到达的迟到事件只记 `apt_call_event`,不改 `ryzt`。
2. **已离寓不回写**:`finish_ly='1'` 后不因叫乘事件回到"待叫乘"。
3. **停止叫乘不可逆至报警**:`stoped='1'` 后只允许人工"重新叫乘"命令清除,不允许超时判定自动改。
### 3.3 分机资源状态机
表 3-11 分机资源状态(`docs/10` 表 7-4 的落地映射)
| `docs/10` 状态 | 本表落点 | 本期是否实现 | 说明 |
| --- | --- | --- | --- |
| `DRAFT` | `sync_state='PENDING'` + `enabled=0` | 是(等价合并) | 本项目把"已建未下发"与"待下发"合并为 `PENDING`,减少一次状态跳转 |
| `PENDING` | `sync_state='PENDING'` | 是 | 已提交收敛任务 |
| `SYNCED` | `sync_state='SYNCED'` | 是 | 库与 Asterisk 一致 |
| `FAILED` | `sync_state='FAILED'` | 是 | 下发失败,禁止叫乘 |
| `DISABLED` | `enabled=0`(`sync_state` 不改值) | 是 | 停用不参与叫乘;表上 CHECK 只允许 4 个 `sync_state` 值 |
| `DELETED` | `deleted=1`(逻辑删除,`sync_state` 不改值) | 是 | 延迟物理删除 |
| `DRIFT` | `sync_state='DRIFT'` + `apt_ast_drift` 一行 | 是 | 对账发现,禁止叫乘 |
**与 `docs/10` 表 7-4 的差异说明**:`docs/10` 列了 7 个状态,本设计落为**"`sync_state` 4 值 + `enabled` + `deleted`"三个载体**:
`DRAFT` 与 `PENDING` 合并(`DRAFT` 只存在于"新增未保存"的瞬时态,落库即等于待下发,保留两个状态只会让前端多一个无行为差异的"草稿态");
`DISABLED`/`DELETED` 用标志位表达而非枚举值(表上 CHECK 只允许 4 个 `sync_state`,写入 `'DISABLED'` 会被数据库直接拒绝)。
语义等价、可查询性更好,差异列入 `08` §4 差-8 待确认。
## 4. `gyid` 隔离
表 3-12 四级隔离(`docs/10` §3.3)
| 级别 | 机制 | 实现点 | 失败处理 |
| --- | --- | --- | --- |
| 部署级 | `apt.apartment.gy-id` 固定,启动校验 | `AptApplication` 启动检查 | 启动失败 |
| 库级 | 一公寓一库 + 独立账号(对本库 DML,对平台库无权限) | 部署与 DBA | 连不上即启动失败 |
| 请求级 | 令牌声明 → `AptContext` → Service → SQL | 拦截器 + 上下文 + Mapper | 取不到 403 |
| 设备级 | `deviceCode + gyid` 绑定,跨公寓拒绝 | `apt-device` 网关 | `APT_DEVICE_GYID_MISMATCH` |
表 3-13 SQL 规范与反例
| 场景 | 正确写法 | 反例(视为缺陷) |
| --- | --- | --- |
| 列表查询 | `where gyid = #{gyid} and finish_ly = '0'` | `where finish_ly = '0'` |
| 详情查询 | `where gyid = #{gyid} and id = #{id}` | `where id = #{id}` |
| 条件更新 | `update gy_jcxx set ryzt=#{to} where gyid=#{gyid} and id=#{id} and ryzt=#{from}` | 先 `select` 再无条件 `update` |
| 唯一性校验 | `where gyid=#{gyid} and cc=#{cc}` | 全表查重(跨公寓误判) |
| 跨表读取 | 先查本表拿 `jcid`,再按 `jcid + gyid` 查另一表 | 无 `gyid` 的 `join` |
| 统计 | `where gyid=#{gyid} and stat_date=?` | 全库 `group by` |
表 3-14 隔离测试用例(验收必过)
| # | 用例 | 期望 |
| --- | --- | --- |
| 1 | 用 `gyid=GYA` 的令牌查 `gyid=GYB` 的房间详情 | 404 或 403,且不返回数据 |
| 2 | 请求体里伪造 `gyid=GYB` 写入 | 写入仍为 `GYA`(服务端忽略入参)或直接 403 |
| 3 | 设备用 `GYB` 的 `deviceCode` 连 `GYA` 心跳 | 403 + 告警 |
| 4 | 分页查询(第 2 页,`pageSize=100`)只返回本公寓数据 | 总数与行数均只含本公寓 |
## 5. 逻辑删除与审计字段约定
表 3-15 字段约定
| 项 | 约定 | 适用表 | 说明 |
| --- | --- | --- | --- |
| 逻辑删除 | 业务表用 `deleted tinyint not null default 0`;若依表用 `del_flag char(1)` | 分区分明,不混用 | `apt_sip_endpoint`、`apt_sip_binding` 有 `deleted` |
| 启用/停用 | `enabled tinyint not null default 1` | `apt_master_*`、`gy_sb`、`apt_sip_endpoint`、`apt_trunk` | 停用不等于删除 |
| 停用原因 | **无独立列**,落 `duty_operation_log.detail`(`module`+`action`+`reason`) | 交路/房间/设备/分机 | 见 §6 缺口 G-03 |
| 创建/更新 | `create_time datetime(3) default current_timestamp(3)`、`update_time … on update` | 业务表 | 存量 `GY_*` 同样已加 |
| 操作人 | `create_by` / `operator_id` / `operator_name` | 仅部分表有 | 统一审计以 `duty_operation_log` 为准 |
| 版本 | `source_version`(快照)、`config_version`(分机)、`state_version`(房态)、`biz_version`(叫乘) | 快照/语音/房态/叫乘 | 语义不同,不可混用 |
| 审计入口 | `@Log` 记录 `sys_oper_log`(若依)+ 业务审计落 `duty_operation_log` | 全部写接口 | 两层并存:`sys_oper_log` 记 HTTP 级,`duty_operation_log` 记业务级 |
## 6. 缺口与建议(需确认后才改库)
> 本节只列缺口与建议,**本轮不执行任何 DDL**。确认后按 `apartment/db/mysql/README.md` §7 第 7 条的约定补增量脚本
> (`08_xxx.sql`),并同步更新该 README 第 3 节清单。
表 3-16 字段级缺口
| # | 缺口 | 影响的功能 | 现状事实 | 建议方案 | 不改库的兜底 |
| --- | --- | --- | --- | --- | --- |
| G-01 | **房间无"楼栋"字段** | 房间管理按"楼栋/楼层/房号/房态"筛选 | `apt_master_room`、`gy_rooms` 均只有 `room_floor`,无 `building` | 两表各补 `building varchar(32) default null comment '楼栋'`,并加索引 `(gyid, building, room_floor)`;`apt_master_room` 需平台下行同步该字段(涉及表 6-1 #5 契约) | 本期用 `room_floor` + `room_no` 前缀筛选,页面标注"楼栋(待补)";不阻塞其他功能 |
| G-02 | **人员无本地启停字段** | 叫乘人员管理"停用/恢复" | `apt_master_person` 是平台快照,只有平台侧 `deleted` | 补 `local_enabled tinyint not null default 1` + `local_disable_reason varchar(512)`;平台全量覆盖时**不清空**这两列(下行 SQL 用 `insert … on duplicate key update` 且不更新本地列) | 本期人员只读;"停用"用 `duty_manual_action`(`action_type=DISABLE_PERSON`)记录意图,不改变可用性;或在 `apt-voice` 分机层停用该房间分机 |
| G-03 | **交路无停用原因字段** | 交路管理"停用原因记录" | `apt_master_route` 只有 `enabled` | 补 `disable_reason varchar(512)`、`disabled_at datetime(3)`、`disabled_by varchar(64)` | 用 `duty_operation_log`(`module=MASTER`、`action=ROUTE_DISABLE`、`detail` 存原因)承载,页面从审计表回显 |
| G-04 | **快照表无 `source` 列**,本地新增会被平台全量覆盖 | 交路/房间的"本地新增"(D7) | `apt_master_route`、`apt_master_room` 均无 `source`,只有 `source_version` | 两表各补 `source varchar(16) not null default 'PLATFORM'`;平台全量覆盖时只删 `source='PLATFORM'` 的行 | 本期**不做交路与房间的本地新增**(`04` 表 4-5 第 4/5 项、表 4-7 第 6 项默认关闭),只做快照的查询/启停/复算校验 |
| G-05 | **叫乘记录无"最后状态变更时间/操作人"** | 叫乘信息详情追溯 | `gy_jcxx` 有 `update_time` 但无操作人 | 补 `state_updated_at datetime(3)`、`state_updated_by varchar(64)` | 用 `duty_operation_log` + `apt_call_session` 时间戳推断;详情页展示审计流水替代 |
| G-06 | **设备类型/指令类型无 CHECK** | 设备管理字典一致性 | `gy_sb.device_type`、`apt_device_command.cmd_type` 只有默认值 | 建议**不加** CHECK(存量兼容优先,`docs/10` 表 5-4 只要求新增表加),改由 Java 枚举 + 字典校验 | 枚举白名单在 `apt-common`,Controller 校验 |
| G-07 | **分机号无"纯数字"校验** | 分机台账录入与对账比对 | `apt_sip_endpoint.extension` 只有 `char(8)`,无数字 CHECK | 应用层正则 `^\d{1,8}$` + 入库大写归一;不依赖库约束(MySQL 8.0 的 `regexp` CHECK 需生成列) | 统一在 `apt-voice` 的 DTO 校验与 Service 入口做,`04` §1 校验清单登记 |
缺口处理原则:
1. **G-01、G-02 影响本轮 5 个管理模块中的 2 个**,建议优先确认;两者都可用"不改库兜底"方式先做,但页面能力受限。
2. **G-03、G-05 不必改库**:`duty_operation_log.detail` 是 JSON,足够承载原因与上下文,且符合 `docs/10` §2.5 结尾
"值班员的每次操作落操作域"的原始意图。
3. **G-04 直接约束功能范围**:D7 未定前,交路管理只做"快照查询 + 启停 + 本地复算校验",房间管理只做"房态查询 + 停用/恢复";两者的"新增/编辑基线字段"都不开放。
4. 任何补字段都要回答三个问题:平台下行契约是否要同步改(G-01、G-02 要)、是否需要回填存量(不需要,业务表为空)、
是否影响双跑对账(`source_version` 不变则不影响)。
+468
View File
@@ -0,0 +1,468 @@
# 04 接口设计
> 追溯:`docs/10` §2.5 表 2-3(路由前缀分区)、§6.1 表 6-1(跨端接口 17 个)、§6.2(上报信封)、
> §6.3 表 6-2(幂等/重试/对账)、§6.4 表 6-3(语音域接口 25 个)。
> 本文档定义的是**公寓端自身**接口(`/apt/**`、`/duty/**`);与平台的跨端接口见 `09-平台接口约定.md`。
> **v2 修订(CR-02,2026-11-09)**:新增接口、设备侧协议与错误码以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §3、§4 为权威,见下文 §0;表 4-4 的 `40903` 常量按 CR-C13 统一,"本期不提供入住 / 换房 / 退房接口"按 CR-C02 撤销。
## 0. v2 接口与错误码修订(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 4-y` 序号;原有分域接口清单继续有效。
表 C-1 新增管理端接口(N-15~N-18,均为 `/apt/**`)
| 编号 | 方法 + 路径 | 权限点 | 幂等 | 落表 |
| --- | --- | --- | --- | --- |
| N-15 | `GET /apt/report/history` | `apt:report:list` | L0 | `apt_report_history` |
| N-16 | `POST /apt/report/retry`、`GET /apt/report/retry/{batchId}` | `apt:report:retry` | L2 `batchId` | `apt_report_retry_batch`、`apt_report_outbox` |
| N-17 | `GET /apt/device/alarms`、`POST /apt/device/alarms/{id}/handle` | `apt:device:alarm` | L1 | `apt_device_alarm` |
| N-18 | `GET /apt/tablet/devices`、`GET /apt/tablet/tasks` | `apt:tablet:list` | L0 | `apt_tablet_device`、`apt_tablet_task` |
表 C-2 设备侧协议(`/open/v1/tablet/**`,HMAC + 时间窗 + `nonce`,**设备主动拉取**)
| 编号 | 方法 + 路径 | 幂等 | 用途 |
| --- | --- | --- | --- |
| N-01 | `POST /open/v1/tablet/register` | `deviceCode` | 平板注册 |
| N-02 | `POST /open/v1/tablet/heartbeat` | L0 | 心跳 / 在线状态 |
| N-03 | `GET /open/v1/tablet/tasks?deviceCode&cursor&limit` | 游标 | 按 `dispatch_seq` 拉取任务 |
| N-04 | `POST /open/v1/tablet/results` | `resultId` | 上报结果(人员 / 刷脸 / 未刷脸) |
| N-05 | `POST /open/v1/tablet/tasks/{taskId}/ack` | `taskId + deviceCode` | 拉取回执 |
表 C-4 Asterisk 配置数据管理接口(**补遗 `docs/公寓端/00` v1.1 / CR-02 §10**;均为端内 `/apt/**`,不计入跨端 20 接口)
| 编号 | 方法 + 路径 | 权限点 | 幂等 | 落表 | 说明 |
| --- | --- | --- | --- | --- | --- |
| 表 4-15 #1~#6 | `GET/POST/PUT/DELETE /apt/voice/endpoints*` | `apt:voice:endpoint:*` | L0~L2 | `apt_sip_endpoint` | 分机表 CRUD,沿用 |
| 表 4-15 #7~#9 | `GET /apt/voice/calls*` | `apt:voice:call:list` | L0 | `apt_call_session`、`apt_call_event` | 呼叫记录只读,沿用 |
| 表 4-15 #10~#11 | `GET /apt/voice/recordings*` | `apt:voice:recording:list/replay` | L0 | `apt_recording` | 录音索引只读,沿用 |
| N-19 | `GET /apt/voice/reg-status` | `apt:voice:reg:list` | L0 | `apt_sip_endpoint`、`apt_sip_reg_status` | 当前注册态列表 |
| N-20 | `GET /apt/voice/reg-status/{endpointId}/history` | `apt:voice:reg:query` | L0 | `apt_sip_reg_status` | 状态变迁流水 |
| N-21 | `POST /apt/voice/reg-status/{endpointId}/commands`(`PROBE`) | `apt:voice:reg:probe` | **L2** `commandId` | `apt_sip_reg_status` | 手工重探 |
| N-22 | `POST /apt/voice/calls/archive` | `apt:voice:call:archive` | **L2** `commandId` | `apt_call_event_his` | 事件归档 |
| N-23 | `POST /apt/voice/recordings/commands`(`PURGE`) | `apt:voice:recording:purge` | **L2** `commandId` | `apt_recording` | 先置 `PURGED` 再删文件 |
| N-24 | `GET /apt/voice/endpoints/{id}` | `apt:voice:endpoint:list` | L0 | `apt_sip_endpoint` | 补分机详情 |
**权限硬要求**(详见 `docs/公寓端/00` §3.4 表 0-12、0-13):会话 + `@PreAuthorize` / `v-hasPermi` 同码双校验、`gyid` 只从会话取且 SQL 必带、事实类表(呼叫记录 / 录音索引 / 注册流水 / 告警)**不开放人工 CUD**、写操作两级审计、录音回放短期签名 ≤ 5 分钟且仅内网、分机台账改动与业务结果查阅职责分离。分机号原地改仍返回 `40906`。
表 C-5 平板叫乘的任务负载与结果上报契约(**流程见 `docs/公寓端/05` §9.3**,表 5-15 / 5-16)
| 接口 | 关键字段 | 口径 |
| --- | --- | --- |
| N-03 `GET /open/v1/tablet/tasks`(拉取) | `taskId`、`jcid`、`dispatchSeq`、**`jcsj`(预定播放时刻)**、`expireAt`、`roomNo`、`ryid`、`ttsTemplate` / `ttsText`、`faceVerifyRequired`、`signTimeoutSec`、`serverTime` / `clockSkewMs`、`cursor` | 服务端**提前下发、不推送呼叫指令**;按 `dispatch_seq` 顺序返回;平板按 `jcsj` 本地定时播放(CR-C08) |
| N-05 `POST /open/v1/tablet/tasks/{taskId}/ack` | `taskId`、`deviceCode`、`pulledAt` | 幂等 `taskId + deviceCode`;重复 ACK → `40916` |
| N-04 `POST /open/v1/tablet/results`(结果上报) | `resultId`、`taskId`、`jcid`、`callMode=TABLET`、`faceVerifyState`、`verifiedRyid`、`resultType`、`occurredAt` / `receivedAt` | `resultId` 幂等;HMAC + ±5 min 时间窗 + `nonce`;只传刷脸结论,**不传抓拍帧 / 人脸原图 / 比对帧**;比对所需特征值按 `docs/公寓端/10` 的设备权限范围下发;断网本地排队恢复后续传 |
> 两条链路的状态上报与结果反馈路径统一见 `docs/公寓端/05` §9.4、§9.5:SIP 由 AMI 事件经 `apt.voice.event` 提交状态机命令,平板由 N-04 经 `apt.device.gateway` 提交状态机命令,**两者都不直接改 `gy_jcxx`**;终态经 #9 / #24 上行、`/ws/duty` 反馈值班台。
表 C-3 错误码统一与新增(CR-C13)
| code | 常量 | 说明 |
| --- | --- | --- |
| 40903 | `APT_SNAPSHOT_EXPIRED` | **本端原 `APT_MASTER_VERSION_STALE` 作废**,与应用端统一 |
| 50101 | `APT_PROTOCOL_NOT_CONFIRMED` | 本端原名保持不变;应用端原 `APT_PROTOCOL_UNCONFIRMED` 作废 |
| 40913 | `APT_OFFLINE_QUOTA_EXCEEDED` | 断网本地充值 / 扣补贴超离线额度 |
| 40914 | `APT_FACE_REG_PENDING` | **注册申请未获值班员确认**(CR-C25 改判),禁止参与运安匹配与设备下发;平台同步回执未返回不再触发本码 |
| 40915 | `APT_TRANSFER_TIME_ILLEGAL` | 换乘时刻非法 |
| 40916 | `APT_TABLET_TASK_EXPIRED` | 平板任务已派发 / 过期 |
| 40917 | `APT_FACE_SCOPE_DENIED` | 设备越出可见范围查询人员 / 人脸(CR-02 §12) |
| 40918 | `APT_FACE_PERSON_DUPLICATE` | 同 `work_no` 重复注册 |
| 40919 | `APT_FACE_MEDIA_MISSING` | 照片或特征值缺失 / 格式不支持 |
| 40920 | `APT_FACE_MEDIA_CHECKSUM_MISMATCH` | 媒体校验和不符 |
| 40921 | `APT_FACE_ALGO_UNSUPPORTED` | 特征值算法不在兼容白名单 |
| 40922 | `APT_FACE_MEDIA_TOO_LARGE` | 媒体超过大小上限 |
| 40923 | `APT_FACE_NOT_SYNCED` | 人员媒体未同步完成,禁止下发设备 |
| 40924 | `APT_FACE_REG_STATE_INVALID` | 注册态不允许该操作 |
| 40926~40932 | 入住作业流程专用 | 审核态冲突 / 设备人脸缓存过期 / 房间不可住 / 核验会话过期 / 核验结论不符 / 交路无效 / 重复入住(CR-02 §13) |
| 40933 | `APT_LEDGER_QUOTA_INSUFFICIENT` | 补贴 + 现金合计仍不足,全额拒绝、不部分扣款(CR-02 §14) |
| 40934 | `APT_LEDGER_ACCOUNT_NOT_FOUND` | 人员账户额度行不存在 |
| 40935 | `APT_LEDGER_CAS_CONFLICT` | 双额度 CAS 版本冲突,重读重试一次后转人工 |
| 40936 | `APT_CATERING_RECIPE_STALE` | 食谱未发布 / 已过期 / 改价,要求终端重拉 N-48 |
| 40937 | `APT_LEDGER_PERSON_NOT_ELIGIBLE` | 人员未注册或已离寓,拒绝消费 / 充值 |
| 40938 | `APT_RECHARGE_AMOUNT_ILLEGAL` | 充值金额非法(≤ 0、≥ 上限、小数位 > 2) |
| 40939 | `APT_RECHARGE_STATE_INVALID` | 充值申请终态不可回写 / 并发确认 / 二维码跨 `gyid` |
| 40940 | `FEATURE_DISABLED` | **远程点菜未启用**(`features.remoteOrdering=false`),不落订单、不扣款 |
| 50302 | `APT_REPORT_RETRY_BUSY` | 同批次续报进行中 |
| 50303 | `APT_FACE_ERASE_TIMEOUT` | 离寓擦除超时未完成,已升级全量同步 |
**口径改判**:入住 / 换房 / 离寓本期提供,统一由 `apt-stay` 落库(CR-C02);主数据下行快照仍只读,但本地人员 / 人脸登记走独立表 `apt_person_face_reg`(CR-C04);设备协议由"未确认"改为"设备主动拉取"(CR-C08)。**CR-02 §12 追加**:注册链路按 CR-C25 改为"签到终端申请 → 值班员确认注册 → 入公寓服务系统 → 分别上传平台 / 下发设备";人员主数据照片与特征值按 CR-C24 允许跨端,业务抓拍与录音仍不出公寓。新增接口全部遵守本文件 §1 的前缀、权限点、幂等与 `data.reason` / `data.traceId` 约定;人脸模块接口 N-25~N-37 见 §0 表 C-6。
表 C-6 人脸数据下发与注册链路接口(CR-02 §12;**均为端内接口,不计入跨端 20 个编号**,完整定义见 `docs/公寓端/10` 表 10-26)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 用途 |
| --- | --- | --- | --- |
| N-25~N-29 | `/apt/face/**` | `apt:face:*` | 主档查询、重下发 / 擦除 / 全量同步、设备范围配置、缓存台账、对账 |
| N-30 | `GET /open/v1/face/persons` | 设备凭据 | 设备按权限范围拉取人员与人脸增量(含 `erase[]`、`hash`) |
| N-31 | `GET /open/v1/face/media/{ref}` | 设备凭据 + 短期签名 | 下载照片 / 特征值 |
| N-32 / N-33 | `/open/v1/face/ack`、`/open/v1/face/cache-report` | 设备凭据 | 下发 / 擦除回执、缓存摘要上报 |
| **N-34** | `POST /open/v1/face/reg-requests` | 签到终端设备凭据 | **签到终端发起注册申请并上传人脸**,只落 `APPLIED` |
| **N-35** | `GET /duty/person/face-reg/pending` | `duty:person:reg` | 值班台待审申请列表 |
| **N-36** | `POST /duty/person/face-reg/{regId}/confirm` | `duty:person:reg` + 二次确认 | **值班员完成人员注册**,入档并触发平台 / 设备双路同步 |
| **N-37** | `POST /duty/person/face-reg/{regId}/reject` | `duty:person:reg` | 值班员驳回申请 |
> CR-02 N-08 `POST /duty/person/face-reg` 的历史"提交即生效"语义由 CR-C25 撤销,收敛为 N-35 列表 + N-36 确认两步;`40914` 改判为"注册申请未获值班员确认"。注册生效后 #9 上传平台与 `FACE_ISSUE` 下发设备两条路径并行、互不阻塞。
表 C-7 人员入住作业流程接口(CR-02 §13;**均为端内接口,不计入跨端 20 个编号**,完整定义见 `docs/公寓端/11` 表 11-30)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 用途 |
| --- | --- | --- | --- | --- |
| N-38 | `POST /open/v1/checkin/verify` | 设备凭据 + HMAC | `deviceEventId` | 扫脸身份核验与注册态判定 |
| N-39 | `GET /open/v1/checkin/options` | 设备凭据 | L0(版本) | 拉取可选房间与交路 |
| N-40 | `POST /open/v1/checkin/applications` | 设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | 提交入住申请,分配 `applyId` + 预分配 `jcid` |
| N-41 | `GET /open/v1/checkin/applications/{applyId}` | 设备凭据 | L0 | 终端查询申请状态 |
| N-42 | `GET /duty/checkin/applications` | `duty:checkin:list` | L0(游标) | 值班台待审列表 |
| N-43 | `GET /duty/checkin/applications/{applyId}` | `duty:checkin:review` | L0 | 审核详情(含房态与候选房间) |
| N-44 | `POST /duty/checkin/applications/{applyId}/review` | `duty:checkin:review` + 二次确认 | **L2** `commandId` | 审核通过 / 换房通过 / 驳回,通过后办理入住 |
| N-45 | `POST /duty/checkin/applications/{applyId}/room-adjust` | `duty:checkin:adjust` | **L2** `commandId` | 房间调整(仅审核期,未建 `gy_jcxx`) |
| N-46 | `GET /duty/checkin/applications/{applyId}/timeline` | `duty:checkin:list` | L0 | 状态流转时间线 |
| N-47 | `GET /apt/checkin/{jcid}/call-task` | `apt:checkin:query` | L0 | 入住单 ↔ 叫乘任务关联查询(只读) |
> 新增错误码 `40926` 审核态冲突 / `40927` 设备人脸缓存过期 / `40928` 房间不可住 / `40929` 核验会话过期 / `40930` 核验结论不符 / `40931` 交路无效 / `40932` 重复入住(沿用 §1 分段与 `data.reason` / `data.traceId` 约定)。跨端仍为 **#9 `CHECKIN` / #6 计划 / #7 任务与匹配结论 / #8 回执**,不新增编号与 `bizType`。
表 C-8 消费、充值与菜品管理接口(CR-02 §14;**均为端内接口,不计入跨端 20 个编号**,完整定义见 `docs/公寓端/12` 表 12-29)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 用途 |
| --- | --- | --- | --- | --- |
| N-48 | `GET /open/v1/catering/recipes?deviceCode&version` | 消费终端设备凭据 | L0(版本) | 拉取当前有效阶段性食谱与菜品 |
| N-49 | `GET /open/v1/ledger/quota?ryid&deviceCode` | 消费终端设备凭据 + HMAC | L0 | 查询补贴额度与现金额度 |
| N-50 | `POST /open/v1/ledger/consume` | 消费终端设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | 提交选单与金额,触发双额度 CAS 扣减 |
| N-51 | `GET /open/v1/ledger/flows/{flowId}` | 设备凭据 | L0 | 流水状态与拆分金额查询 |
| N-52 | `POST /open/v1/ledger/recharge-requests` | 签到终端设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | 扫码后发起线下现金充值申请(**非支付**) |
| N-53 | `GET /duty/ledger/recharge-requests?state&cursor` | `duty:ledger:local` | L0(游标) | 管理员待核对列表 |
| N-54 | `POST /duty/ledger/recharge-requests/{flowId}/confirm` | `duty:ledger:confirm` + 二次确认 | **L2** `commandId` | 核对实收金额并计入现金额度 |
| N-55 | `POST /duty/ledger/recharge-requests/{flowId}/cancel` | `duty:ledger:confirm` | **L2** `commandId` | 作废申请 |
| N-56 | `GET /apt/ledger/accounts/{ryid}` | `apt:ledger:list` | L0 | 账户额度、流水与对账状态查询 |
| N-57 | `GET/POST /apt/catering/recipes`、`PUT .../{recipeId}/publish`、`PUT .../{recipeId}/revoke` | `apt:catering:recipe:*` | L2 `recipeId + version` | 公寓管理员发布 / 撤回阶段性食谱(代理到平台餐饮管理端) |
| N-58 | `POST /open/v1/catering/orders` | 设备凭据 | L2 `orderId` | **远程点菜预留入口**,本期恒 `40940 FEATURE_DISABLED` |
> 请求 / 响应字段见 `docs/公寓端/12` 表 12-14 / 12-19 / 12-23;跨端仍为 **#1 `features.remoteOrdering` / #4 `recipes[]` / #10 `LEDGER_OFFLINE` / #16 日对账**,不新增编号与 `bizType`。设备侧一律主动拉取,金额一律字符串两位小数、禁止浮点;新增错误码 `40933`~`40940` 见表 C-3。
> **CR-02 §15 补遗(三端一致性)**:四能力的端内接口编号、字段与错误码**以本文件 §0 表 C-1~C-8 为准**,应用端 / 平台端**只消费不自造**;三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**。新增统计能力全部落平台管理端 `/platform/v1/admin/stat/**`(`S-01`~`S-07`,见 `docs/平台端/10` 表 10-14),**不计入跨端 20 编号、不产生跨端副作用、不反向下发公寓**,本端无需新增上报接口;#13 `ARCHIVE` 摘要仅按 `payload` 扩展评价 / 房间周转分组计数(见 `docs/公寓端/09` §0 表 C-9),不新增 `bizType`。
## 1. 通用约定
### 1.1 路径与动作
表 4-1 RESTful 约定
| 动作 | 方法 | 路径形态 | 幂等 | 示例 |
| --- | --- | --- | --- | --- |
| 列表 | GET | `/apt/{域}/list` | 是 | `GET /apt/route/list` |
| 详情 | GET | `/apt/{域}/{id}` | 是 | `GET /apt/room/12` |
| 新增 | POST | `/apt/{域}` | 否(业务键唯一约束兜底) | `POST /apt/device` |
| 编辑 | PUT | `/apt/{域}/{id}` | 是(全量覆盖同值无副作用) | `PUT /apt/device/12` |
| 逻辑删除 | DELETE | `/apt/{域}/{id}` | 是 | `DELETE /apt/voice/endpoints/12` |
| 启停 | PUT | `/apt/{域}/{id}/status` | 是 | `PUT /apt/route/12/status` |
| 受控命令 | POST | `/apt/{域}/{id}/commands` | **否**,必须带 `commandId` | `POST /apt/jcxx/88/commands` |
| 导出 | POST | `/apt/{域}/export` | 否 | `POST /apt/person/export` |
三条硬约定:
1. **不出现动词化路径**(`/add`、`/update`、`/doStop`),除 `commands`、`export`、`reconcile`、`replay` 这类非 CRUD 动作。
2. **状态变更必须走 `commands`**,不允许 `PUT /apt/jcxx/{id}` 直接改 `ryzt`(`docs/10` §5.3 第 6 条)。
3. **启停走 `/status`**,请求体必须带 `reason`(原因),落 `duty_operation_log`(`03` §5)。
### 1.2 统一请求与响应
列表响应(若依 `TableDataInfo`):
```json
{ "code": 200, "msg": "查询成功", "rows": [], "total": 0 }
```
详情/写响应(若依 `AjaxResult`):
```json
{ "code": 200, "msg": "操作成功", "data": {} }
```
分页参数(若依 `PageDomain`):`pageNum`(默认 1)、`pageSize`(默认 10,上限 100)、`orderByColumn`、`isAsc`。
**排序约定**:前端不传排序时,后端按业务字段排序,见各接口"默认排序"列;禁止默认按 `id` 排序。
表 4-2 通用出参字段规范
| 字段类型 | 格式 | 示例 |
| --- | --- | --- |
| 时间(展示) | `yyyy-MM-dd HH:mm:ss` | `2026-11-07 09:58:12` |
| 时间(事件/会话) | `yyyy-MM-dd HH:mm:ss.SSS` | `2026-11-07 09:58:12.135` |
| 时长 | `bigint` 毫秒 | `durationMs` |
| 金额 | 字符串(避免浮点) | `"12.50"` |
| 状态 | 字符串枚举 + `xxxLabel` | `{"ryzt": 3, "ryztLabel": "已应答待离寓"}` |
| 字典 | `dictType` 标识,前端取若依字典 | `{"statusDict": "apt_route_status"}` |
### 1.3 幂等要求分级
表 4-3 幂等分级
| 级别 | 适用 | 实现 |
| --- | --- | --- |
| L0 无要求 | 纯查询 | — |
| L1 业务键唯一 | 交路 `jlid`、房间 `room_no`、设备 `device_code`、分机 `extension` | 表 UK + `ServiceException` 转 409 |
| L2 请求号幂等 | 状态命令、人工干预 | `commandId`/`actionId` 落 `duty_manual_action.action_id`(UK) |
| L3 指令幂等 | 设备指令 | `apt_device_command.command_id`(UK) |
| L4 跨端幂等 | 上报 | `apt_report_outbox.idempotency_key = hash(gyid+bizType+bizId+bizVersion)` |
前端统一加若依 `repeatSubmit` 注解防重复点击;后端不依赖前端。
## 2. 统一错误码
表 4-4 业务错误码(`AptErrorCode`)
| 码 | HTTP | 常量 | 含义 | 典型场景 |
| --- | --- | --- | --- | --- |
| 200 | 200 | `OK` | 成功 | — |
| 40001 | 400 | `APT_PARAM_INVALID` | 参数校验失败 | 必填缺失、格式错 |
| 40101 | 401 | `APT_UNAUTHORIZED` | 未登录或令牌失效 | — |
| 40301 | 403 | `APT_FORBIDDEN` | 无权限点 | 角色缺 `apt:route:edit` |
| 40302 | 403 | `APT_GYID_MISMATCH` | 数据域不匹配 | 跨公寓访问 |
| 40303 | 403 | `APT_DEVICE_GYID_MISMATCH` | 设备与公寓不匹配 | 跨公寓心跳 |
| 40401 | 404 | `APT_NOT_FOUND` | 对象不存在 | 已删除/不存在 |
| 40901 | 409 | `APT_DUPLICATE` | 业务键重复 | 同车次同交路 |
| 40902 | 409 | `APT_STATE_ILLEGAL` | 状态不允许该操作 | 已离寓继续叫乘 |
| 40903 | 409 | `APT_SNAPSHOT_EXPIRED` | 快照版本过期(CR-C13 统一常量名) | 新计划拒绝生效 |
| 40904 | 409 | `APT_SNAPSHOT_READONLY` | 快照只读,禁止本地改 | 改平台下发字段(本地登记表另见 CR-C04) |
| 40905 | 409 | `APT_SYNC_PENDING` | 资源未同步完成 | 分机 `PENDING`/`FAILED`/`DRIFT` 时叫乘 |
| 40906 | 409 | `APT_EXTENSION_IMMUTABLE` | 分机号不可改 | 改号请求 |
| 40907 | 409 | `APT_COMMAND_DUPLICATE` | 命令重复提交 | 同 `commandId` |
| 40908 | 409 | `APT_CONF_BUSY` | 会议桥被占用(D11 未定时的串行化兜底) | 单 `Conf-01` 已有在会会话时发起新叫乘(`08` 表 8-4) |
| 40913 | 409 | `APT_OFFLINE_QUOTA_EXCEEDED` | 断网本地充值 / 扣补贴超离线额度 | CR-02 |
| 40914 | 409 | `APT_FACE_REG_PENDING` | 注册申请未获**值班员确认** | CR-C25 改判;不得参与运安匹配与设备下发 |
| 40915 | 409 | `APT_TRANSFER_TIME_ILLEGAL` | 换乘时刻非法 | CR-02 |
| 40916 | 409 | `APT_TABLET_TASK_EXPIRED` | 平板任务已派发 / 过期 | CR-02 |
| 41001 | 410 | `APT_RECORDING_PURGED` | 录音已清理 | 回放已 `PURGED` |
| 50101 | 501 | `APT_PROTOCOL_NOT_CONFIRMED` | 现场协议未确认,暂不支持(CR-C13 两端统一) | 真实设备指令下发 |
| 50102 | 501 | `APT_NOT_SUPPORTED` | 边界外能力 | 请求运安同步 |
| 50301 | 503 | `APT_DEPENDENCY_UNAVAILABLE` | 依赖不可用(AMI/Asterisk/Redis) | AMI 断开时发起叫乘 |
| 50302 | 503 | `APT_REPORT_RETRY_BUSY` | 同一续报批次进行中 | CR-02 |
错误响应必须带 `data.reason`(可读原因)与 `data.traceId`,便于 `docs/10` §8.2 可观测性要求。
## 3. 交路管理
表 4-5 交路接口清单(`apt-master-client`)
| # | 方法 | 路径 | 用途 | 权限点 | 关键入参 | 关键出参 | 幂等 | 表 | 默认排序 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/route/list` | 分页列表 | `apt:route:list` | `cc`、`jlid`、`jch`、`cclx`、`enabled`、`kcsjFrom/kcsjTo`、`pageNum/pageSize` | `id`、`jlid`、`cc`、`jch`、`qd`、`kcsj`、`jcsj`、`lysj`、`cclx`、`enabled`、`sourceVersion` | L0 | `apt_master_route` | `enabled desc, jcsj asc, cc asc` |
| 2 | GET | `/apt/route/{id}` | 详情 | `apt:route:query` | `id` | 全字段 + `rules[]`(关联 `apt_master_rule` 的叫乘提前量)+ 最近停用审计 | L0 | `apt_master_route`、`apt_master_rule`、`duty_operation_log` | — |
| 3 | GET | `/apt/route/options` | 下拉选项 | `apt:route:list` | `keyword` | `[{jlid, cc, jcsj}]` | L0 | `apt_master_route` | `jcsj asc` |
| 4 | POST | `/apt/route` | 新增(**受 D7 约束,本期默认关闭**) | `apt:route:add` | `jlid`、`cc`、`jch`、`qd`、`kcsj`、`jcsj`、`lysj`、`cclx`、`bz` | `id` | L1 | `apt_master_route` | — |
| 5 | PUT | `/apt/route/{id}` | 编辑(**受 D7 约束**) | `apt:route:edit` | 同新增 | — | L1 | `apt_master_route` | — |
| 6 | PUT | `/apt/route/{id}/status` | 启停 | `apt:route:changeStatus` | `enabled`、`reason`(必填) | — | L1 | `apt_master_route` + `duty_operation_log` | — |
| 7 | POST | `/apt/route/{id}/recalc-check` | 本地复算校验(不落库) | `apt:route:query` | `baseDate` | `computedJcsj`、`computedFjsj`、`ruleVersion`、`diffMs`、`warnings[]` | L0 | `apt_master_route`、`apt_master_rule`、`apt_holiday` | — |
校验规则:
1. **必填**:`cc`(≤32)、`kcsj`(`HH:mm` 或 `HH:mm:ss`)、`jcsj`、`lysj`;`jlid` 由平台下发时必填。
2. **重复**:`(gyid, jlid)` 唯一(表 UK);同 `(cc, kcsj)` 提示重复但不强制拦截(不同交路可同车次)。
3. **时序**:`jcsj < kcsj`、`lysj <= kcsj`,违反返回 `APT_PARAM_INVALID`。
4. **停用原因**:`reason` 必填(1~512 字),落 `duty_operation_log`(`module=MASTER`、`action=ROUTE_DISABLE/ENABLE`)。
5. **只读保护**:`sourceVersion`、`createTime` 不接受入参;平台下发字段写操作返回 `APT_SNAPSHOT_READONLY`。
表 4-6 交路域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-master-client`(`master.route`) | `apt_master_route`、`apt_master_rule`、`apt_holiday`、`duty_operation_log` | 上表 1~7 | `entries/apt/views/route/RouteList.vue`、`RouteDetail.vue` | `apt:route:list/query/add/edit/changeStatus` |
## 4. 房间管理
**先明确三条事实**(避免"新增房间"被误解):
1. `apt_master_room` 是**平台下发基线**(只读),`gy_rooms` 是**运行时事实**(房态、在住人数)。
2. `gy_rooms` **没有 `enabled` 列**,只有 `room_status`(1 空闲 / 2 住满 / 3 待清扫 / 4 不能入住 / 5 未满 / 6 保洁中)。
因此"停用房间" = 房态置 `4 不能入住`,"恢复" = 触发房态重算,**不是**翻转一个开关。
3. 房态只能由 `apt-stay` 重算写(`03` §3.1),管理端只能提交"停用/恢复"命令,不能直接 `update room_status`。
表 4-7 房间接口清单(`apt-stay`)
| # | 方法 | 路径 | 用途 | 权限点 | 关键入参 | 关键出参 | 幂等 | 表 | 默认排序 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/room/list` | 房间档案 + 房态列表 | `apt:room:list` | `building`(见 G-01)、`roomFloor`、`roomNo`、`roomStatus[]`、`roomType`、`pageNum/pageSize` | `roomNo`、`roomFloor`、`roomDir`、`roomFace`、`bedNumbers`、`roomStatus`、`roomStatusLabel`、`livePerson`、`personName`、`roomType`、`roomOrder`、`sourceVersion` | L0 | `gy_rooms`(主)、`apt_master_room`(左连取基线) | `room_floor asc, room_order asc, room_no asc` |
| 2 | GET | `/apt/room/{id}` | 详情 | `apt:room:query` | `id` | 房间全字段 + `bindings[]`(分机绑定)+ `livePersons[]`(在寓人员摘要)+ `recentCleanTasks[]` | L0 | `gy_rooms`、`apt_master_room`、`apt_sip_binding`、`gy_jcxx`、`gy_clean_task` | — |
| 3 | GET | `/apt/room/status-summary` | 房态汇总(卡片) | `apt:room:list` | — | `{idle, full, partial, cleaning, disabled, total}` | L0 | `gy_rooms` | — |
| 4 | PUT | `/apt/room/{id}` | 编辑本地属性(房型/部门/排序/朝向/方位) | `apt:room:edit` | `roomType`、`roomDept`、`roomOrder`、`roomDir`、`roomFace` | — | L1 | `gy_rooms` | — |
| 5 | PUT | `/apt/room/{id}/status` | 停用 / 恢复 | `apt:room:changeStatus` | `action`(`DISABLE`/`RESTORE`)、`reason`(必填) | `roomStatus` | L2 | `gy_rooms`(经重算)+ `duty_manual_action` | — |
| 6 | POST | `/apt/room` | 新增房间(**需 G-01/G-04 补齐后开放**) | `apt:room:add` | `roomNo`、`roomFloor`、`bedNumbers`、`roomType`、`building` | `id` | L1 | `apt_master_room`(`source=LOCAL`,待补列) | — |
实现约束:
1. **筛选"楼栋"**:`apt_master_room` / `gy_rooms` 均无 `building` 列(`03` §6 G-01)。本期接口接受 `building` 参数但
在无该列时降级为 `room_floor` 前缀匹配,并在响应 `warnings[]` 中标注"楼栋字段未启用"。
2. **停用/恢复**必须走 `apt-stay` 的房态重算服务(`state_version` 乐观锁),不允许直接改 `room_status`。
3. **不越界**:入住 / 换房 / 退房(离寓)接口**本期提供**,统一由 `apt-stay` 执行域落库并只经领域服务,管理端与值班台不得直改 `gy_jcxx` 状态字段(CR-02 CR-C02;原"属后续"口径已撤销);房间详情只读展示在寓人员,房态重算仍走 `apt-stay`。
4. **与基线的关系**:`apt_master_room` 的快照字段(`bedNumbers`、`roomFloor`)如需改动,必须走平台下行(表 6-1 #5),
本地改返回 `APT_SNAPSHOT_READONLY`。
表 4-8 房间域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-stay`(`room`) | `gy_rooms`、`apt_master_room`、`apt_sip_binding`、`gy_clean_task`、`duty_manual_action` | 上表 1~6 | `entries/apt/views/room/RoomList.vue`、`RoomDetail.vue` | `apt:room:list/query/edit/changeStatus/add` |
## 5. 叫乘信息管理
**核心约束**:`gy_jcxx` 的状态字段只能由 `apt.duty.state` 写(`03` §3.2 表 3-10)。管理端页面是"只读 + 提交命令",
不存在"编辑状态"的接口。
表 4-9 叫乘接口清单(`apt-dispatch`)
| # | 方法 | 路径 | 用途 | 权限点 | 关键入参 | 关键出参 | 幂等 | 表 | 默认排序 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/jcxx/list` | 在寓/叫乘记录列表 | `apt:jcxx:list` | `ryid`、`rymc`、`fjh`、`cc`、`jlid`、`ryzt[]`、`finishJc`、`finishLy`、`stoped`、`jcsjFrom/jcsjTo`、`isTempCall`、`pageNum/pageSize` | `id`、`jcid`、`ryid`、`rymc`、`fjh`、`cc`、`jlid`、`jcsj`、`jcsjS`、`hjxysj`、`ryzt`、`ryztLabel`、`finishJc`、`finishLy`、`stoped`、`isJcbj`、`isLybj`、`recordCount` | L0 | `gy_jcxx`(+ `apt_recording` 聚合计数) | `jcsj desc, id desc` |
| 2 | GET | `/apt/jcxx/{id}` | 详情 | `apt:jcxx:query` | `id` | `gy_jcxx` 全字段 + `session`(最近会话)+ `recordings[]` + `manualActions[]` | L0 | `gy_jcxx`、`apt_call_session`、`apt_recording`、`duty_manual_action` | — |
| 3 | GET | `/apt/jcxx/{id}/timeline` | 事件时间线(排障) | `apt:jcxx:query` | `id`、`from`、`to` | `[{at, type, source, summary, refType, refId}]`(合并状态迁移、AMI 事件、人工命令、审计) | L0 | `apt_call_event`、`duty_operation_log`、`duty_manual_action`、`gy_jcxx` | `at asc` |
| 4 | POST | `/apt/jcxx/{id}/correct` | 人工修正**非状态字段** | `apt:jcxx:correct` | `commandId`、`reason`、`fjh`/`jlid`/`cc`/`leaveRemark`(白名单) | — | L2 | `gy_jcxx`(仅白名单列)+ `duty_manual_action` | — |
| 5 | POST | `/apt/jcxx/{id}/commands` | 状态机命令 | `apt:jcxx:changeStatus` | `commandId`、`command`(`STOP`/`RE_CALL`/`FORCE_FINISH`/`MARK_LEAVE`/`CANCEL`)、`reason` | `accepted`、`stateAfter`、`commandId` | L2 | `gy_jcxx`(经 `apt.duty.state`)+ `duty_manual_action` + `duty_operation_log` | — |
| 6 | GET | `/apt/jcxx/statistics` | 状态分布(看板) | `apt:jcxx:list` | `date` | `{waiting, calling, alarm, answered, leaveAlarm, talking, finished}` | L0 | `gy_jcxx` | — |
**白名单**(第 4 个接口,能改的字段只有这些,依据 `docs/10` §3.3 "值班员可写字段白名单"作为 Code Review 项):
| 字段 | 允许 | 说明 |
| --- | --- | --- |
| `fjh` | 是 | 换房修正,需同时校验房间存在 + `finish_ly='0'` |
| `jlid` / `cc` / `jch` | 是 | 交路修正,需校验交路快照存在且 `enabled=1` |
| `leave_remark` | 是 | 备注 |
| `is_temp_call` | 是(仅置 1) | 标记非标叫乘 |
| `ryzt` / `finish_jc` / `finish_ly` / `stoped` | **否** | 只能经第 5 个接口 |
| `cbje` / `sfffcb` | **否** | 补助属账务域,P2 |
**命令语义**(第 5 个接口,全部转 `duty_manual_action` 再由 `apt.duty.state` 执行):
| command | 允许的当前状态 | 效果 | 失败码 |
| --- | --- | --- | --- |
| `STOP` | `ryzt in (1,2)` 且 `finish_ly='0'` | `stoped='1'`,停止后续复叫/报警 | `APT_STATE_ILLEGAL` |
| `RE_CALL` | `stoped='1'` 或 `ryzt=2` | 清除 `stoped`,`ryzt→1`,新增一次会话 | `APT_STATE_ILLEGAL` |
| `FORCE_FINISH` | `finish_ly='0'` | `finish_jc='1'`,不置离寓(`finish_ly` 保持) | `APT_STATE_ILLEGAL` |
| `MARK_LEAVE` | `finish_ly='0'` | 写 `gygl_ly_event`(`leave_type=MANUAL`)+ `finish_ly='1'` | `APT_STATE_ILLEGAL` |
| `CANCEL` | `ryzt=0` | 取消未开始的叫乘(`stoped='1'`) | `APT_STATE_ILLEGAL` |
表 4-10 叫乘域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-dispatch`(`state` + `engine`) | `gy_jcxx`、`duty_manual_action`、`duty_operation_log`、`apt_call_session`、`apt_call_event`、`apt_recording` | 上表 1~6 | `entries/apt/views/jcxx/JcxxList.vue`、`JcxxDetail.vue`(含时间线组件) | `apt:jcxx:list/query/correct/changeStatus` |
## 6. 叫乘人员管理
**先判定数据属性**(用户要求的第一步):`apt_master_person` 的 `owner=platform-down`,是**平台下发缓存**,
`source_version` 标记版本、`deleted` 是平台侧删除标记。因此:
- **平台下发的字段一律只读**(姓名、卡号、单位、岗位、照片版本等),本地改返回 `APT_SNAPSHOT_READONLY`。
- 本地"停用/恢复"无对应列(`03` §6 G-02),本期以"只读 + 关联查询"为主,停用走 `duty_manual_action` 记录意图。
表 4-11 人员接口清单(`apt-master-client`)
| # | 方法 | 路径 | 用途 | 权限点 | 关键入参 | 关键出参 | 幂等 | 表 | 默认排序 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/person/list` | 人员列表 | `apt:person:list` | `ryid`、`xm`、`dw`、`depotid`、`gwmc`、`deleted`、`photoVersionFrom`、`pageNum/pageSize` | `id`、`ryid`、`cardId`、`xm`、`dw`、`gwmc`、`gzmcnew`、`sjh`、`photoUrl`、`photoVersion`、`sourceVersion`、`deleted` | L0 | `apt_master_person` | `xm asc, ryid asc` |
| 2 | GET | `/apt/person/{id}` | 详情 | `apt:person:query` | `id` | 全字段(身份证/手机号按权限脱敏) | L0 | `apt_master_person` | — |
| 3 | GET | `/apt/person/{ryid}/relations` | 关联信息(房间/交路/叫乘) | `apt:person:query` | `ryid`、`liveOnly` | `rooms[]`、`routes[]`、`jcxx[]`(最近 N 条)、`callSessions[]`(最近 N 条) | L0 | `gy_jcxx`、`apt_master_route`、`apt_call_session`、`gy_rooms` | `jcxx.jcsj desc` |
| 4 | POST | `/apt/person/export` | 导出 | `apt:person:export` | 同列表筛选 | Excel(若依 `ExcelUtil`) | L0 | `apt_master_person` | — |
| 5 | POST | `/apt/person/{ryid}/local-action` | 记录本地处置意图(**替代停用**,需 G-02 定稿后转正式启停) | `apt:person:edit` | `action`(`DISABLE_INTENT`/`RESTORE_INTENT`)、`reason`(必填) | `actionId` | L2 | `duty_manual_action` + `duty_operation_log` | — |
说明:第 5 个接口**不改变人员可用性**,只记录"谁在什么时候申请停用某人及原因",用于 G-02 补字段后回填。
这样既不越界改平台真源,也不丢失现场诉求。若 G-02 确认补 `local_enabled`,则该接口改为写该列并保留审计。
表 4-12 人员域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-master-client`(`master.person`) | `apt_master_person`、`apt_person_account`、`duty_manual_action` | 上表 1~5 | `entries/apt/views/person/PersonList.vue`、`PersonDetail.vue`(含关联 Tab) | `apt:person:list/query/edit/export` |
## 7. 设备管理
表 4-13 设备接口清单(`apt-device`)
| # | 方法 | 路径 | 用途 | 权限点 | 关键入参 | 关键出参 | 幂等 | 表 | 默认排序 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/device/list` | 设备档案列表 | `apt:device:list` | `deviceCode`、`deviceType[]`、`roomNo`、`onlineState`、`enabled`、`version`、`pageNum/pageSize` | `id`、`deviceCode`、`deviceType`、`roomNo`、`ip`、`version`、`onlineState`、`onlineStateLabel`、`lastSeenAt`、`enabled`、`clockSkewMs`、`diskFreeMb` | L0 | `gy_sb`、`gy_sb_xt` | `online_state asc, last_seen_at desc` |
| 2 | GET | `/apt/device/{id}` | 详情 | `apt:device:query` | `id` | 档案全字段 + `heartbeat`(`gy_sb_xt`)+ `bindings[]`(分机绑定)+ `recentCommands[]` | L0 | `gy_sb`、`gy_sb_xt`、`apt_sip_binding`、`apt_device_command` | — |
| 3 | POST | `/apt/device` | 新增档案 | `apt:device:add` | `deviceCode`、`deviceType`、`roomNo`、`ip`、`mac`、`remark` | `id` | L1 | `gy_sb` | — |
| 4 | PUT | `/apt/device/{id}` | 编辑档案 | `apt:device:edit` | 同新增(不含 `deviceCode`) | — | L1 | `gy_sb` | — |
| 5 | PUT | `/apt/device/{id}/status` | 启停 | `apt:device:changeStatus` | `enabled`、`reason`(必填) | — | L1 | `gy_sb` + `duty_operation_log` | — |
| 6 | GET | `/apt/device/{id}/heartbeat` | 心跳与系统信息 | `apt:device:query` | `id` | `version`、`appVersion`、`osVersion`、`heartbeatAt`、`clockSkewMs`、`diskFreeMb`、`memFreeMb`、`deviceUpdateState` | L0 | `gy_sb_xt` | — |
| 7 | GET | `/apt/device/commands` | 指令记录查询 | `apt:device:query` | `deviceCode`、`cmdType`、`state[]`、`from`、`to`、`pageNum/pageSize` | `commandId`、`cmdType`、`state`、`retryCount`、`sentAt`、`ackAt`、`ackResult`、`failReason` | L0 | `apt_device_command`(历史兼容 `gy_sb_zl`) | `create_time desc` |
| 8 | POST | `/apt/device/{id}/commands` | 指令入队(**不发送**) | `apt:device:command` | `commandId`、`cmdType`、`priority`、`payload`、`expireAt` | `commandId`、`state=PENDING`、`protocolConfirmed=false` | L3 | `apt_device_command` + `duty_operation_log` | — |
**指令下发的诚实边界**(用户要求"不伪造设备返回或成功状态",`docs/10` §7.3 表 7-2):
1. 第 8 个接口只做**入队**,返回 `state=PENDING` 且 `protocolConfirmed=false`。
2. 真实下发依赖三项现场条件,缺一不可:设备协议文档、设备固件支持 ACK、设备与网关连通性验证。
未确认前接口返回 `warnings: ["APT_PROTOCOL_NOT_CONFIRMED"]`,页面显示"已入队,待协议确认后下发"。
3. **禁止**把入队当成功:`state` 只有设备 ACK 后才置 `ACKED`;超时 `EXPIRED`;重试超限 `FAILED`。
4. `gy_sb_zl` 只读兼容存量,不新增写入(`03` 表 3-4)。
表 4-14 设备域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-device`(`catalog` + `command`) | `gy_sb`、`gy_sb_xt`、`gy_sb_zl`、`apt_device_command`、`apt_sip_binding` | 上表 1~8 | `entries/apt/views/device/DeviceList.vue`、`DeviceDetail.vue`(含心跳与指令 Tab) | `apt:device:list/query/add/edit/changeStatus/command` |
## 8. 语音与录音管理
对齐 `docs/10` 表 6-3,本期实现第 1~17 行中的管理/查询/录音部分;第 14、18 行(挂断、TTS 试听)依赖 Asterisk 联调后开放。
表 4-15 语音接口清单(`apt-voice`)
| # | 方法 | 路径 | 用途 | 权限点 | 表 6-3 对应 | 幂等 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/voice/endpoints` | 分机列表 | `apt:voice:endpoint:list` | #1 | L0 |
| 2 | POST | `/apt/voice/endpoints` | 新增分机(只落库,异步下发) | `apt:voice:endpoint:add` | #2 | L1 |
| 3 | PUT | `/apt/voice/endpoints/{id}` | 改属性(不含号码) | `apt:voice:endpoint:edit` | #3 | L1 |
| 4 | PUT | `/apt/voice/endpoints/{id}/status` | 启用/停用 | `apt:voice:endpoint:changeStatus` | #4 | L2 |
| 5 | DELETE | `/apt/voice/endpoints/{id}` | 逻辑删除 | `apt:voice:endpoint:remove` | #5 | L2 |
| 6 | POST | `/apt/voice/endpoints/{id}/resync` | 手工触发收敛 | `apt:voice:endpoint:changeStatus` | #8 | L2 |
| 7 | GET | `/apt/voice/calls` | 会话列表 | `apt:voice:call:list` | #11 | L0 |
| 8 | GET | `/apt/voice/calls/{sessionId}` | 会话详情 | `apt:voice:call:list` | #12 | L0 |
| 9 | GET | `/apt/voice/calls/{sessionId}/events` | 原始事件时间线 | `apt:voice:call:list` | #13 | L0 |
| 10 | GET | `/apt/voice/recordings` | 录音索引查询 | `apt:voice:recording:list` | #15 | L0 |
| 11 | GET | `/apt/voice/recordings/{id}/url` | 取短期签名地址 | `apt:voice:recording:replay` | #16 | L0 |
| 12 | GET | `/apt/voice/ast/health` | Asterisk/AMI 健康 | `apt:ops:health` | #19 | L0 |
| 13 | GET | `/apt/voice/ast/drifts` | 漂移清单 | `apt:ops:health` | #21 | L0 |
| 14 | POST | `/apt/voice/ast/reconcile` | 触发对账(`dryRun` 默认 true) | `apt:ops:reconcile` | #20 | L2 |
**三条硬约束**(来自表 6-3 的"接口设计要点"):
1. 分机新增/修改/停用一律返回"已受理 + 当前 `syncState`",不阻塞等 Asterisk。
2. 改号请求直接返回 `APT_EXTENSION_IMMUTABLE`,引导"停旧建新"。
3. 录音地址必须短期签名(`expireAt` ≤ 10 分钟),且 `state=PURGED` 时返回 `APT_RECORDING_PURGED`。
表 4-16 语音域映射
| 模块 | 表 | 接口 | 页面 | 权限点 |
| --- | --- | --- | --- | --- |
| `apt-voice`(`endpoint` / `event` / `media`) | `apt_sip_endpoint`、`apt_trunk`、`apt_call_session`、`apt_call_event`、`apt_recording`、`apt_ast_drift` | 上表 1~14 | `entries/apt/views/voice/EndpointList.vue`、`CallList.vue`、`RecordingList.vue`、`AstHealth.vue` | `apt:voice:endpoint:*`、`apt:voice:call:list`、`apt:voice:recording:list/replay`、`apt:ops:health` |
## 9. 主数据、上报与运维接口
表 4-17 支撑接口清单
| # | 方法 | 路径 | 用途 | 权限点 | 模块 | 表 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | GET | `/apt/master/versions` | 各主数据快照版本与有效期 | `apt:ops:health` | `apt-master-client` | `apt_master_version` |
| 2 | POST | `/apt/master/{masterType}/resync` | 触发全量重拉(受限,需二次确认) | `apt:ops:reconcile` | `apt-master-client` | `apt_master_version` |
| 3 | GET | `/apt/report/outbox` | outbox 列表(按状态/类型/时间) | `apt:report:list` | `apt-report` | `apt_report_outbox` |
| 4 | POST | `/apt/report/outbox/{id}/replay` | 重发单条(需审计) | `apt:report:replay` | `apt-report` | `apt_report_outbox` |
| 5 | GET | `/apt/report/watermark` | 水位与积压概览 | `apt:report:list` | `apt-report` | `apt_report_watermark` |
| 6 | GET | `/apt/ops/selfcheck` | 启动自检与巡检结果 | `apt:ops:selfcheck` | `apt-ops` | `apt_ops_selfcheck` |
| 7 | GET | `/apt/ops/health/call-ready` | 可叫乘探针(**已存在占位**,当前固定 `false`) | `@Anonymous`(监控用) | `apt-ops` | — |
## 10. 与 `docs/10` 表 6-1 / 表 6-3 的映射
表 4-18 跨端接口在本端的落点
| `docs/10` 编号 | 接口 | 公寓端落点 | 本端触发 |
| --- | --- | --- | --- |
| 表 6-1 #1 | 公寓档案与能力开关下行 | `apt-master-client` → `apt_master_apartment` | 启动 + 每日 1 次 |
| 表 6-1 #2 | 人员增量下行(含照片版本) | `apt-master-client` → `apt_master_person`(只存 `photoUrl` / `photoVersion`,本体在平台) | 每日 1 次 + 值班前增量 |
| 表 6-1 #3 / #4 / #5 | 交路 / 规则 / 房间基线下行 | `apt-master-client` → `apt_master_route/rule/room` | 定时 + 版本比对 |
| 表 6-1 #6 / #7 | 计划与任务变更下行 | `apt-master-client` → `gy_syn_jcxx` | 定时 + 变化触发 |
| 表 6-1 #8 | 任务回执上行 | `apt-report` outbox(`bizType=TASK_ACK`) | 复算完成 |
| 表 6-1 #9 | 执行结果上行 | `apt-report` outbox(`DUTY_RESULT`) | 状态机提交后 |
| 表 6-1 #10 | 账务流水上行 | `apt-report` outbox(`LEDGER`) | P2 启用 |
| 表 6-1 #11~#14 | 设备/告警/归档/心跳 | `apt-report` outbox(`DEVICE_STATUS`/`ALARM`/`ARCHIVE`/`HEARTBEAT`) | 定时聚合 |
| 表 6-3 #23 | 语音配置下行 | `/open/v1/voice/config`(已存在占位) | 启动 + 每日 1 次轮询(下行一律拉) |
| 表 6-3 #24 | 语音质量摘要上行 | `apt-report` outbox(`VOICE_SUMMARY`) | 每小时 |
| 表 6-3 #25 | 平台侧汇总查看 | 平台侧实现,公寓端不提供 | — |
| 表 6-1 #15 / #16 / #17 | 平台侧查询与补偿 | #15 平台自答、公寓端不实现;#16/#17 方向待定(F-03),本期只做本地入口(`09` 表 9-13) | — |
跨端接口的字段级定义见 `09-平台接口约定.md`。
+626
View File
@@ -0,0 +1,626 @@
# 05 关键流程
> 追溯:`docs/10` §7.7(语音链路拓扑)、§7.9 表 7-5(事件与状态迁移)、§7.11 表 7-7(自检与巡检)、
> §6.2(上报信封)、§6.3 表 6-2(幂等/重试/对账)、§5.5 表 5-5(分机配置一致性)、§3.3(人工干预边界)。
> 流程中的状态迁移以 `03` §3.2 表 3-10 为准。
> **v2 修订(CR-02,2026-11-09)**:8 条关键流程变更以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §7 为权威,见下文 §0;表 5-1 第 4 行的 `jcsj` 口径按 R-C9 同步纠正;**§12 补遗(F-F 注册链路改判、F-J 人脸数据下发)以 `docs/公寓端/10` 为流程权威表述**。
## 0. v2 流程变更索引(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 5-y` 序号;§1~§8 原流程继续有效,本节只列增量与改判。
表 C-1 本端流程变更落点(F-A~F-Q)
| 编号 | 流程 | 改动 | 涉及原章节 |
| --- | --- | --- | --- |
| F-A | 双模式与顺序派发 | §1 / §2 增 `call_mode=TABLET` 分支;按 `jcsj` 生成单调 `dispatch_seq`,平板按游标拉取 | §1、§2、§3 |
| F-B | 平板结果回写 | 状态机新增 `TABLET_RESULT` 事件源;`VERIFIED` / `UNVERIFIED` / `SKIPPED` 分支;只提交命令 | §2 |
| F-C | 换乘 | 本地重算时刻 + 换房联动 + `apt_sip_binding` 联动 + #8 / #9 上报 | §1、§7 |
| F-D | 断网本地充值 / 扣补贴 | 离线额度校验、本地余额 CAS、恢复后先派 outbox 再拉 #7、冲正 | §6 |
| F-E | 上报历史与续报 | outbox 仍为队列,新增 `apt_report_history` / `apt_report_retry_batch`,续报复用原幂等键 | §6 |
| F-F | **人员注册(v1.3 由 CR-C25 改判)** | 签到终端 `REG_APPLY` → 值班台待办 → 值班员 `CONFIRM`(生成 `ryid`、入 `apt_master_person`)→ **并行双路**:#9 上传平台 / `FACE_ISSUE` 下发公寓内设备;平台回执降级为同步回执,不再是生效前置 | `docs/公寓端/10` §4、§5 |
| F-G | 保洁 / 排班 / 考核 | 现场记录、班次与日终本地计算 | §7 |
| F-H | 设备报警分流 | 设备报警独立判定并落 `apt_device_alarm`,与叫乘 / 离寓报警分离 | §3 |
| F-I | **SIP 分机注册离线告警** | `call_mode='SIP'` 派发前按 `jcid → fjh → apt_sip_binding → apt_sip_endpoint` 解析房间主分机;注册态非 `ONLINE`(或 `last_reg_at` 超阈值)且持续超阈值时,按 `alarm_event_key` 去重写入 `apt_device_alarm`(`alarmSource=DEVICE`、`alarmType=OFFLINE`),推 `device.alarm`、#12 上报;**不阻断叫乘**,失败原因记 `apt_call_session.hangup_cause`。恢复时写新流水 + `cleared_at` + `alarm.cleared`,只追加不清零 | §1、§3、§4 |
| F-J | **人脸数据下发管理(CR-02 §12)** | 平台增量下载入库、签到设备全字段下发、消费 / 叫乘按 `gy_sb.face_scope_*` 隔离、`TRANSIENT` 离寓自动 `FACE_ERASE`、`LONG_TERM` 独立分区长期保留;三级版本与 `hash` 对账 + `FACE_FULL_SYNC` 兜底。与 F-F 的注册链路衔接 | `docs/公寓端/10` §3、§5~§10 |
| F-K | **人员入住作业流程(CR-02 §13)** | 注册与签到 → 入住申请(房间 / 交路)→ 值班员审核与房间调整(`CHECKED_IN`)→ #9 `CHECKIN` 提报与平台运安匹配(#6 / #8 / #7)→ #7 拉取叫乘任务并复算 `jcsj` 进入叫乘;"已入住"为流程态 + 业务事实双重表达,不新增 `gy_jcxx` 状态字段 | `docs/公寓端/11` §2~§8、§10、本文件 §10 |
| F-L | **人员消费(CR-02 §14)** | 终端按 `PUBLISHED` 食谱选菜并显示金额 → N-49 查补贴 / 现金额度 → "补贴优先、现金补足"双额度 CAS 扣减且一笔消费只记一条 `CONSUME` 流水 → #10 `LEDGER_OFFLINE` 上报 → 平台 `platform-ledger` 归日并更新人员账户 → #16 日对账置 `RECONCILED`;合计不足全额拒绝 | `docs/公寓端/12` §4、§7、本文件 §6(F-D)、§11 |
| F-M | **人员线下充值(CR-02 §14)** | 扫公寓线下二维码(**只识别公寓、非支付**)→ 签到终端 N-52 提交现金充值申请(申请表不存金额)→ 管理员 N-54 二次确认 → `apt.ledger.local` 只增 `xjye` 并写 `gy_ykt_xjczjl(channel=OFFLINE_CASH)` → #10 同步平台;不建支付订单与回调 | `docs/公寓端/12` §5、§7、本文件 §6(F-D)、§11 |
| F-N | **公寓菜品管理(CR-02 §14)** | 公寓管理员 N-57 发布阶段性食谱(平台库 `YKT_MEALS` 补列、`recipe_version` 单调)→ #4 `recipes[]` 下行 → 消费终端 N-48 按 `version` 主动拉取并缓存 → 点击查看菜品、加入选择菜单(确认前不落库);远程点菜仅留 N-58 入口,本期恒 `40940` | `docs/公寓端/12` §6、§7、本文件 §11 |
**口径纠正(R-C9 / CR-C06)**:`jcsj` 及其派生时刻恒由公寓按 `apt_master_rule` + `apt_holiday` 复算,平台不产出权威值;只有要素(`kcsj` / `jcfs`)与平台不一致时才以平台为准并告警。
三条不可违反规则不变:① 只经 `apt.duty.state`,任何新事件源(平板结果、换乘、报警)都只提交命令;② 条件更新带期望态;③ 已结束不回写。
> **补遗指针(CR-02 §11 / §12 / §13 / §14 / §15)**:F-A / F-B 两类叫乘实现方式的端到端流程(触发条件、交互时序、状态上报机制、结果反馈路径)见本文件 **§9**;§9 为流程权威表述,本节索引的 F-A~F-N **本端流程**骨架不变。人员注册(F-F)与人脸数据下发(F-J)的流程权威表述见 **`docs/公寓端/10` §4、§3~§10**;注册链路按 CR-C25 改判为"签到终端申请 → 值班员确认 → 入公寓服务系统 → 分别上传平台 / 下发设备",平台回执不再是注册生效前置。人员入住作业流程(F-K)见本文件 **§10** 章节指针与 **`docs/公寓端/11`** 唯一权威表述。消费 / 充值 / 菜品管理(F-L / F-M / F-N)见本文件 **§11** 章节指针与 **`docs/公寓端/12`** 唯一权威表述;三条流程共用 §6 的 F-D 账务闭环,不新造账务闭环。
> **三端一致性与平台统计(CR-02 §15)**:表 C-1 标题按全局流程编号扩至 **F-A~F-Q**,其中 F-A~F-N 本端流程骨架不变,新增 `F-O` / `F-P` / `F-Q` 为**平台侧流程**(见 `docs/平台端/06` §0 表 C-1),本端**不感知统计、不新增上报义务**;三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,四能力业务规则 / 交互 / 权限 / 异常三端对照见 §15 表 3-65。平台新增 `docs/平台端/10`「公寓统计分析与排名报表」,统计结果不回写本端任何业务状态;#13 `ARCHIVE` 摘要仅按 `payload` 扩展分组计数(见 `docs/公寓端/09` §0 表 C-9),不新增 `bizType` 与上报接口。
## 1. 计划下行与叫乘任务生成
```mermaid
sequenceDiagram
participant P as 平台端
participant M as apt-master-client
participant DB as 本地库
participant E as apt-dispatch
P->>M: GET /platform/v1/plans/duty(gyId, from, to)
M->>DB: 校验 apt_master_version 中 ROUTE/RULE 版本
alt 版本过期
M->>DB: 记 apt_master_version(FAILED, fail_reason)
M-->>P: 拒绝接收,触发全量重拉
else 版本一致
M->>DB: upsert gy_syn_jcxx(plan_key 幂等)
M->>DB: 更新 gy_syn_jcxx_status(watermark)
E->>DB: 按 apt_master_rule 复算 jcsj/fjsj/jcbjsj
E->>DB: 生成/更新 gy_jcxx(ryzt=0 待叫乘)
E->>M: 复算差异(超阈值告警)
M-->>P: POST /ack/tasks(accepted + computedCallTime)
end
```
图 5-1 计划下行与任务生成
表 5-1 步骤与失败处理
| 步骤 | 动作 | 幂等键 | 失败处理 | 依据 |
| --- | --- | --- | --- | --- |
| 1 | 拉取计划 | `gyId + planId + version` | 网络失败退避重试(1s/5s/30s/2min/10min) | 表 6-2 重试行 |
| 2 | 版本校验 | `apt_master_version(master_type, version_no)` | 版本过期 → 拒绝任务 + 全量重拉 + 告警 | 表 5-2 第 2 行 |
| 3 | 落库计划快照 | `gy_syn_jcxx.plan_key` | 冲突则按 `version` 覆盖 | `docs/10` §5.2 |
| 4 | 本地复算 | `gy_jcxx.jcid`(**= 公寓分配的任务业务键**,不是"计划侧业务键";`id` 为代理键) | **要素**(`kcsj` / `jcfs`)与平台不一致 → 以平台为准 + 告警;**执行时刻 `jcsj` 及其派生恒以本地按规则快照复算为准**(平台不产出权威 `jcsj`) | D8'、R-C9 / R-C10、CR-02 CR-C06;表 5-2 第 6 行 |
| 5 | 回执 | `taskId + version` | 回执失败不影响执行(outbox 累积) | 表 6-1 #8 |
**边界**:公寓端不做"计划转换为任务"的规则定义(`docs/10` 表 4-1 任务生成行:平台 ● / 公寓 ○ 复算校验),
只做复算与回执。
## 2. 叫乘执行(任务 → AMI → 会议桥 → 事件 → 状态机)
```mermaid
sequenceDiagram
participant Q as Quartz(apt-dispatch)
participant S as apt.duty.state
participant V as apt.voice.event
participant A as apt.voice.ami
participant AST as Asterisk
participant DB as 本地库
Q->>Q: 到期扫描 gy_jcxx(ryzt=0, stoped=0, jcsj<=now)
Q->>S: command(CALL_START, jcid)
S->>DB: 条件更新 ryzt 0→1, jcsj_s=now(3)
Q->>V: originate(room, console)
V->>A: Originate PJSIP/room + PJSIP/console
A->>AST: AMI 命令
AST-->>A: OriginateResponse(Success/Failure)
A-->>V: 事件
alt Success
AST-->>A: ConfbridgeJoin(Conf-01, PJSIP/room)
A-->>V: 入会事件
V->>DB: 写 apt_call_session(state=JOINED_CONF, join_at)
V->>S: command(ANSWERED, jcid)
S->>DB: 条件更新 ryzt→3, hjxysj, finish_jc='1'
V->>DB: 生成 apt_tts_utterance + apt_recording 索引
else Failure
A-->>V: 失败原因
V->>S: command(CALL_FAILED, jcid)
S->>DB: 条件更新 ryzt→2, jcbjsj_s, is_jcbj='1'
V->>DB: 写 gy_bjxx(NO_ANSWER)
end
```
图 5-2 叫乘执行与状态回写
表 5-2 关键判定与口径(`docs/10` §7.9)
| 判定 | 口径 | 落点 |
| --- | --- | --- |
| 接通判定 | 用 `ConfbridgeJoin`,**不是** `Answer` | `apt_call_session.join_at` |
| 无应答超时 | 30s 内无 `ConfbridgeJoin` 且通道不存在 | `ryzt→2` + `gy_bjxx(TIMEOUT)` |
| 接通即断 | 入会后 < 3s `Hangup` | 判异常中断,触发复叫 + 告警 |
| 会议桥不可用 | `ConfBridge` 应用不存在或入会失败 | 直接转值班台兜底(`/ws/duty` 推送) |
| 挂断原因 | `16` 正常 / `17` 忙 / `18` 无应答 / `19` 不可达 / `21` 拒绝,其余记"未知"+ 原值 | `apt_call_session.hangup_cause` |
| 复叫 | 复叫判定 35s;接听后自动挂断 40s;喊话 30s | `gy_jcxx.fjsj_s` |
| 事件丢失兜底 | 每 5 分钟 `core show channels concise` 与在途会话比对 | 有库无通道 → `HANGUP(UNKNOWN)`;有通道无库 → 孤儿通道告警 |
三条不可违反的规则:
1. **只经状态机**:`apt.voice.event` 不直接 `update gy_jcxx`,只提交命令给 `apt.duty.state`(`docs/10` 表 4-4)。
2. **条件更新**:所有状态更新带 `where id=? and ryzt=<期望值>`,避免"先查后改"脏写(表 10-1 状态机多写行)。
3. **已结束不回写**:`finish_jc='1'` 后迟到事件只写 `apt_call_event`(`03` §3.2 保护语义)。
## 3. 设备接入(注册 / 心跳 / 指令 ACK)
```mermaid
sequenceDiagram
participant D as 现场设备
participant G as apt.device.gateway
participant C as apt.device.command
participant DB as 本地库
D->>G: POST /open/v1/device/register(deviceCode + 激活码)
G->>DB: 写入 gy_sb(gyid + deviceCode 绑定)
G-->>D: 返回 secret(一机一密)+ serverTime
loop 每次心跳
D->>G: POST /open/v1/device/heartbeat(HMAC(secret, body))
G->>G: 校验时间窗 ±5min + nonce 去重 + gyid 绑定
G->>DB: upsert gy_sb_xt(version/ip/heartbeat_at/clock_skew_ms)
G->>C: 取待投递指令(priority, next_send_at)
C->>DB: state PENDING→SENT, sent_at
G-->>D: {serverTime, commands[]}
D->>G: POST /open/v1/device/command/ack(commandId, result)
G->>C: state SENT→ACKED, ack_at/ack_result
end
```
图 5-3 设备接入与指令闭环
表 5-3 与 `docs/10` §7.2、§7.3 的对应与本期边界
| 能力 | 目标设计(`docs/10`) | 本期实现 | 未实现时的行为 |
| --- | --- | --- | --- |
| 注册认证 | 一机一密,替换全局口令 | **否**(依赖固件与协议确认) | 保留口令模式开关;`/open/**` 仍为占位 |
| 心跳 | HMAC + 时间窗 + nonce 去重 | **否**(同上) | 只落库(`gy_sb_xt`),不校验签名 |
| 指令 | 下发—ACK—重投—过期,P0 重投 5 次不过期 | **仅入队** | `apt_device_command.state=PENDING`,不发送 |
| 状态采集 | 在线/离线、版本、故障码,按小时聚合只报摘要 | **是**(落库 + 摘要) | — |
| 离线告警 | 超阈值产生"设备离线"告警进盯控列表 | **是**(`gy_bjxx`) | — |
**诚实的边界**:在设备协议、固件 ACK 能力、连通性三项确认前,接口只写队列,不伪造 `SENT`/`ACKED`(`04` §7 第 8 条)。
页面必须显示"已入队,待协议确认"。
## 4. 语音事件、录音索引与 TTS
```mermaid
flowchart TB
A["AMI 事件到达"] --> B{"事件类型"}
B -->|OriginateResponse| C["apt_call_session: CALLING→FAILED 或继续"]
B -->|Newstate| D["CALLING→RINGING, 记 ring_at"]
B -->|ConfbridgeJoin| E["RINGING→JOINED_CONF, 记 join_at"]
B -->|ConfbridgeLeave| F["JOINED_CONF→LEFT, 记 leave_at"]
B -->|Hangup| G["任意→HANGUP, 记 hangup_at/cause/duration"]
B -->|Disconnect| H["在途会话标 UNKNOWN, apt_ast_ami_session 记断连"]
E --> I["提交状态机命令 ANSWERED"]
E --> J["生成 apt_tts_utterance(seq 递增)"]
E --> K["录音:事件带 recordfile 则写 apt_recording(AVAILABLE)"]
K --> L{"无 recordfile"}
L -->|是| M["按文件名约定反查目录 → 有则 AVAILABLE,无则 MISSING + 告警"]
G --> N["提交状态机命令 HANGUP"]
```
图 5-4 事件到业务状态与媒体索引
表 5-4 录音与 TTS 的回写规则(`docs/10` §7.9 录音/TTS 表)
| 能力 | 触发时机 | 成功判定 | 失败回写 | 本表落点 |
| --- | --- | --- | --- | --- |
| 录音 | 房间侧入会(Asterisk 侧策略) | 事件带 `recordfile` 或目录存在文件 | `state=MISSING` + 告警,**不影响叫乘结论** | `apt_recording` |
| TTS | 入会后、播放前 | `Playback` 成功且 `PLAYING→DONE` | `state=FAILED` + 原因,可人工重播(`source=TEST`) | `apt_tts_utterance` |
| 复叫 TTS | 复叫入会后 | 同上,`seq` 递增 | 同上 | 同上 |
| 清理 | 每日低峰(`0 30 2 * * ?`) | 先置 `PURGED` 再删文件 | 失败重试 + 告警 | `apt_recording` |
四条设计约束:
1. **`gy_jcxx.recordfile` 只读兼容**:新会话只写 `apt_recording`,不再拼逗号串(表 5-3 处理关系第 2 条)。
2. **TTS 不依赖内存槽位**:改用 `apt_tts_utterance` 队列,避免 `conftts` 单槽位并发覆盖(表 10-3 TTS 行)。
3. **录音不阻断叫乘**:`MISSING` 只告警,不改变 `gy_jcxx` 状态。
4. **AMI 断开**:在途会话标 `UNKNOWN`,由 5 分钟巡检判定终态(表 7-5 `AMI Disconnect` 行)。
## 5. 主数据下行与快照生效
```mermaid
flowchart LR
A["公寓端定时拉取(下行一律拉,平台不推)"] --> B["校验格式与 checksum"]
B --> C{"version > 本地?"}
C -->|否| D["丢弃 + 记日志(幂等)"]
C -->|是| E["事务:写快照 + apt_master_version(APPLIED)"]
E --> F{"apply_result"}
F -->|FAILED| G["保留旧快照 + 告警 + 不接收新任务"]
F -->|APPLIED| H["更新 apt_master_version.applied_at/expires_at"]
H --> I["通知依赖模块刷新缓存"]
I --> J{"过期未续?"}
J -->|是| K["仅降级:新计划不生效 + 告警(不阻断叫乘)"]
```
图 5-5 快照生效与过期降级
表 5-5 快照域的一致性要求(`docs/10` 表 5-1、表 5-2 第 2 行)
| 快照 | 版本键 | 生效方式 | 过期后果 |
| --- | --- | --- | --- |
| 公寓档案 | `(gyid, APARTMENT, source, version)` | 全量覆盖(单行) | 告警 |
| 房间基线 | `(gyid, ROOM, source, version)` | 全量覆盖(按 `room_no` upsert) | 新房间不生效 |
| 人员 | `(gyid, PERSON, source, version)` | 增量 upsert + `deleted` 标记 | 新人员无法叫乘(已有记录不受影响) |
| 交路 | `(gyid, ROUTE, source, version)` | 全量覆盖 | 新交路不生效,已生成任务照常执行 |
| 规则/节假日 | 同上 | 全量覆盖 | **保持最后已知版本**,仅告警 |
三条原则:
1. **快照只读**:业务模块不得写 `apt_master_*`(`03` 表 3-1)。
2. **版本不一致拒绝任务**,不是拒绝启动(`docs/10` 表 5-2 第 2 行)。
3. **过期只降级不阻断**:叫乘主链路必须能离线跑(表 7-6 第 1 行)。
## 6. 上报 outbox 与重试对账
```mermaid
sequenceDiagram
participant B as 业务模块
participant O as apt-report.outbox
participant DB as 本地库
participant P as 平台端
B->>O: enqueue(bizType, bizId, bizVersion, payload)
O->>DB: insert apt_report_outbox(watermark 自增, idempotencyKey)
loop 定时派发(按 watermark 升序)
O->>DB: 取 PENDING/FAILED 且 next_retry_at<=now(批量 ≤500)
O->>P: POST /platform/v1/report/**
alt 成功
O->>DB: state=SENT, sent_at, platform_recv_at
O->>DB: 更新 apt_report_watermark(acked_watermark)
else 失败
O->>DB: retry_count+1, next_retry_at=退避+抖动
O->>DB: 连续失败 → FAILED → DEAD + 告警
end
end
P-->>O: 日对账结果(count / sum / max watermark)
O->>DB: 差异写告警,人工触发 replay
```
图 5-6 outbox 派发与对账
表 5-6 可靠性机制(`docs/10` 表 6-2)
| 机制 | 设计 | 落点 |
| --- | --- | --- |
| 幂等 | `hash(gyid+bizType+bizId+bizVersion)` 唯一 | `uk_outbox_idem` |
| 重试 | 1s / 5s / 30s / 2min / 10min,上限 1 小时 + 抖动 | `next_retry_at` |
| 顺序 | 同 `bizId` 按 `bizVersion` 单调;乱序丢弃旧版本 | 派发前比对 `biz_version` |
| 断网 | 落本地库持续累积 | `state=PENDING` |
| 降级 | 积压超阈值只上报 P0 | 派发查询加 `priority` 过滤(`biz_type` 白名单) |
| 对账 | 日比对 `count`、`sum(amount)`、`max(watermark)` | 平台侧执行,公寓端接差异 |
**本期边界**:平台接收端未就绪时,outbox **只累积不发送**(`state` 停在 `PENDING`),这也是可验证的:
联调前置条件满足后,把派发开关打开即可,不需要改业务代码。
## 7. 值班台人工干预
```mermaid
sequenceDiagram
participant U as 值班员(duty 入口)
participant C as apt-duty.command
participant S as apt.duty.state
participant DB as 本地库
U->>C: POST /duty/call/{jcid}/commands(commandId, STOP)
C->>DB: 校验权限点 + gyid
C->>DB: insert duty_manual_action(action_id 幂等, ACCEPTED)
C->>S: 提交状态机命令
alt 命令合法
S->>DB: 条件更新 gy_jcxx(ryzt/stoped/…)
S->>DB: duty_manual_action → DONE
S->>DB: duty_operation_log(谁/何时/对谁/做了什么)
C-->>U: accepted + stateAfter
else 状态不允许
S-->>C: 拒绝(APT_STATE_ILLEGAL)
C->>DB: duty_manual_action → FAILED + result
C-->>U: 409 + 原因
end
```
图 5-7 人工干预链路(`docs/10` §2.5 结尾、§3.3 表 3-1)
表 5-7 干预动作与审计
| 动作 | 目标 | 允许的当前状态 | 审计 | 备注 |
| --- | --- | --- | --- | --- |
| `STOP` 停止叫乘 | `gy_jcxx` | `ryzt in (1,2)`、`finish_ly='0'` | `duty_operation_log` + `duty_manual_action` | — |
| `RE_CALL` 复叫 | `gy_jcxx` | `stoped='1'` 或 `ryzt=2` | 同上 | 生成新会话 |
| `FORCE_FINISH` 强制结束 | `gy_jcxx` | `finish_ly='0'` | 同上 | 不置离寓 |
| `MARK_LEAVE` 登记离寓 | `gy_jcxx` + `gygl_ly_event` | `finish_ly='0'` | 同上 | `leave_type=MANUAL` |
| `HANGUP` 挂断 | `apt_call_session` | `state in (CALLING,RINGING,JOINED_CONF)` | 同上 | 表 6-3 #14 |
**为什么必须两层表**:`duty_manual_action` 记录"命令意图与结果"(可幂等、可重试、可追责),
`duty_operation_log` 记录"审计事实"(可查询、可导出、保留期独立)。两者不合并,原因是审计表可能被归档,
而命令表需要长期保留以支持重试与幂等判断(`docs/10` §2.5 结尾)。
## 8. 启动自检与运行期巡检
表 5-8 自检项(`docs/10` 表 7-7 的落地)
| # | 检查项 | 判定 | 失败级别 | 落点 |
| --- | --- | --- | --- | --- |
| 1 | AMI 可达与登录 | TCP + `login()` | BLOCKED | `apt_ast_ami_session` / `apt_ops_selfcheck` |
| 2 | Asterisk 版本与能力 | `core show version` | DEGRADED | 同上 |
| 3 | 会议桥存在 | `Conf-01` 可用 | BLOCKED | 同上 |
| 4 | 拨号计划上下文 | `from-internal` 含 `1901`/`1701` | BLOCKED | 同上 |
| 5 | 值班台分机在册且注册 | `CONSOLE` 类型 + 注册状态 | DEGRADED | `apt_sip_endpoint` |
| 6 | 台账与 Asterisk 一致 | `pjsip show endpoints` 比对 | DEGRADED | `apt_ast_drift` |
| 7 | 录音目录可写与空间 | 试写 + 剩余 > 阈值 | DEGRADED | `apt_ops_selfcheck` |
| 8 | 录音播放地址可达 | `recording.base-url` 200 | DEGRADED | 同上 |
| 9 | TTS 通道可用 | 试播 `TEST` | DEGRADED | `apt_tts_utterance` |
| 10 | 串口可打开 | `console.port` | DEGRADED | `apt_ops_selfcheck` |
| 11 | 时钟偏差 | NTP < 1s | DEGRADED | 同上 |
| 12 | AMI 事件滞后 | `last_event_at` 差 | DEGRADED | `apt_ast_ami_session` |
**可叫乘探针的判定**:`BLOCKED` 项任一失败 → `/apt/ops/health/call-ready` 返回 `callReady=false` + 未就绪原因列表;
`DEGRADED` 项失败 → 仍 `callReady=true`,但响应带 `degraded[]`(对应表 6-3 要点 4:"健康探针分两个")。
## 9. 叫乘两类实现方式的端到端业务流程
> 追溯:`docs/公寓端/00` 表 0-3(A-06 / A-07 / A-08)、`docs/10` §7.7(语音链路拓扑)、§7.9 表 7-5(事件到业务状态)、表 7-6(本地闭环)、`docs/设计变更/03` §7.1 F-A / §7.2 F-B、§10 F-I、`docs/公寓端/04` §0 表 C-2、`docs/应用端/04` 表 4-12。
> **定位**:本节是 A-06~A-08 的**流程权威表述**。§1(任务生成)、§2(SIP 执行与状态机)、§4(语音事件与媒体)、§6(outbox)、§7(人工干预)的骨架不变,本节把"触发条件 → 交互流程 → 状态上报 → 结果反馈"串成两条端到端链路,并补齐此前未展开的**平板本地定时播放**环节。本节图表沿用本文件主序号 `表 5-9`~`5-21`、`图 5-8`~`5-9`;§0 补遗使用独立的 `表 C-y`,两者不混用。
### 9.1 方式对照与模式选择
表 5-9 两类叫乘方式对照
| 维度 | 第一类:SIP 语音叫乘 | 第二类:平板叫乘 |
| --- | --- | --- |
| 执行主体 | **Asterisk 主动发起呼叫**(`Originate`),房间 SIP 终端接听 | 平板拉取任务后**本地定时播放**语音 |
| 触发 | 服务端 Quartz 到 `jcsj` 扫描,或值班员人工发起 | 服务端生成任务 + 平板拉取 + **平板本地到 `jcsj`** |
| 应答 / 完成判据 | AMI `ConfbridgeJoin`(房间侧入会) | 平板刷脸签到 `faceVerifyState=VERIFIED` |
| 提示语音 | 入会后 `Playback(tts)`,落 `apt_tts_utterance` | 平板本地合成 / 播放,语音文件不出公寓服务端 |
| 状态来源 | 业务系统依据 **AMI 事件**推断 | 平板**上报叫乘结果** |
| 结果上报路径 | AMI → `apt.voice.event` → 状态机命令 → #8 / #9 / #24 | 平板 → `POST /open/v1/tablet/results` → `apt.device.gateway` → 状态机命令 → #9 |
| 关键表 | `apt_call_session`、`apt_call_event`、`apt_recording` | `apt_tablet_task`、`apt_tablet_call_result` |
| 失败兜底 | 30s 无入会 → `ryzt=2` + 复叫 35s;AMI 断连 5 分钟巡检比对 | 未刷脸 / 超时 → 复叫 + 告警;任务过期 `40916` 转人工 |
| 断网 | 本地闭环可用(`docs/10` 表 7-6) | 已缓存任务照常播放,结果本地排队恢复后续传 |
表 5-10 模式选择规则
| 规则 | 口径 |
| --- | --- |
| `gy_jcxx.call_mode` | 由 #7 任务变更下行或本地房间配置确定,取值 `SIP`(默认)/ `TABLET`;**一条任务只走一种方式,执行中不切换** |
| 回落 | #1 下发 `features.tabletCall=false`,或平板未激活 / 离线时,`TABLET` 一律回落 `SIP` 并记录回落原因 |
| 前置校验 | `SIP` 走 F-I 分机注册离线判定(`docs/公寓端/00` §3.5),告警**不阻断**叫乘;`TABLET` 走平板在册与心跳校验 |
| 顺序派发 | 按 `jcsj` 升序生成 `dispatch_seq`,单公寓同一时刻只派发一条(CR-02 F-A 第 3 步) |
| 共用 | 两条链路共用 `apt.duty.state` 状态机、`gy_jcxx` 状态字段、`apt_report_outbox` 上行通道与 `/ws/duty` 反馈通道 |
### 9.2 第一类:SIP 语音叫乘(Asterisk 主动呼叫)
表 5-11 触发条件(全部满足才发起)
| # | 条件 | 不满足时 |
| --- | --- | --- |
| 1 | `gy_jcxx.call_mode='SIP'` 且 `ryzt=0`、`stoped=0`、`finish_jc='0'` | 已结束任务不重复发起 |
| 2 | Quartz 到期扫描 `jcsj <= now`,或值班员 `POST /duty/call/{jcid}/voice`(`duty:call:voice`,L2 `commandId`) | 同 `commandId` 只生效一次 |
| 3 | `jcid → fjh → apt_sip_binding(ROOM, is_primary=1, 生效区间有效) → apt_sip_endpoint` 能解析到房间主分机,且 `enabled=1`、`deleted=0`、`sync_state='SYNCED'` | 绑定缺失记 `apt_ast_drift`;未收敛返回 `40905` |
| 4 | AMI 可达、`Conf-01` 会议桥存在、`from-internal` 拨号计划就绪(表 5-8 自检项 1 / 3 / 4) | `BLOCKED` → `/apt/ops/health/call-ready` 返回 `callReady=false`,转值班台兜底 |
| 5 | 分机注册态按 F-I 判定;离线只写 `apt_device_alarm` 并告警 | **不阻断**,失败原因记 `apt_call_session.hangup_cause` |
图 5-8 SIP 语音叫乘端到端时序
```mermaid
sequenceDiagram
autonumber
participant Q as Quartz / 值班员命令
participant S as apt.duty.state
participant V as apt.voice.endpoint / event
participant A as apt.voice.ami
participant AST as Asterisk
participant T as 房间 SIP 终端
participant DB as 本地库
participant W as /ws/duty + outbox
Q->>S: command(CALL_START, jcid)
S->>DB: 条件更新 ryzt 0→1, jcsj_s=now(3)
S-->>W: state.changed(0→1)
V->>A: Originate PJSIP/room + PJSIP/console
A->>AST: AMI Originate
AST->>T: SIP INVITE(Asterisk 主动发起)
AST-->>A: OriginateResponse(Success)
T-->>AST: 接听并入会
AST-->>A: ConfbridgeJoin(Conf-01, PJSIP/room)
A-->>V: 事件(写 apt_call_event)
V->>DB: apt_call_session(state=JOINED_CONF, join_at)
V->>S: command(ANSWERED, jcid)
S->>DB: ryzt→3, hjxysj, finish_jc='1'
V->>DB: apt_tts_utterance(PENDING) + apt_recording 索引
AST->>T: Playback(tts) 播放提示语音
A-->>V: Playback 结果 / Hangup
V->>DB: 更新 apt_tts_utterance(DONE/FAILED) + apt_call_session(HANGUP)
S-->>W: state.changed(1→3) + session.changed
W->>DB: outbox enqueue(#8 回执 / #9 DUTY_RESULT / #24 VOICE_SUMMARY)
```
表 5-12 SIP 语音叫乘交互流程
| 步 | 主体 | 动作 | 落表 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| 1 | Quartz / 值班员命令 | 到 `jcsj` 触发,提交 `CALL_START` 命令 | `duty_manual_action`(人工触发时) | 状态不允许 → `APT_STATE_ILLEGAL` |
| 2 | `apt.duty.state` | `where id=? and ryzt=0` 条件更新 `ryzt=1`、`jcsj_s` | `gy_jcxx` | 推 `state.changed(0→1)` |
| 3 | `apt.voice.endpoint` | 解析房间主分机,双路 `Originate`(房间 + 值班台) | `apt_sip_binding`、`apt_call_session(CALLING)` | 解析失败按表 5-11 第 3 条 |
| 4 | `apt.voice.ami` → Asterisk | 发 AMI `Originate`,Asterisk 向房间 SIP 终端发 `INVITE` | `apt_call_event` | `OriginateResponse.Failure` → `CALL_FAILED` 分支(表 5-13、表 5-20) |
| 5 | 房间 SIP 终端 | 接听并入会(`ConfbridgeJoin`,会议号 `Conf-01`) | `apt_call_session(RINGING → JOINED_CONF)` | 30s 无入会 → `TIMEOUT` |
| 6 | `apt.voice.event` | 依 AMI 事件判**应答**,提交 `ANSWERED` 命令 → 状态机置 `ryzt=3`、`finish_jc='1'`、`hjxysj` | `gy_jcxx`、`apt_call_event` | 只提交命令,不直接 `update gy_jcxx` |
| 7 | `apt.voice.media` | 入会后 `Playback(tts)` 播放提示语音,生成录音索引与 TTS 播报单元 | `apt_tts_utterance`、`apt_recording` | `FAILED` / `MISSING` 只告警,**不改叫乘结论** |
| 8 | `apt.voice.event` / `apt-report` | `Hangup` 归档会话;终态经 #8 / #9 / #24 上行;值班台经 `/ws/duty` 反馈 | `apt_call_session`、`apt_report_outbox` | AMI 断连标 `UNKNOWN`,5 分钟巡检兜底 |
表 5-13 AMI 事件 → 业务状态 → 上报与反馈映射(沿用 `docs/10` 表 7-5、本文件 表 5-2)
| AMI 事件 / 兜底 | 业务判定 | 状态机命令 → `gy_jcxx` | 上行 | 值班台反馈 |
| --- | --- | --- | --- | --- |
| `OriginateResponse.Success` + `Newstate` | 呼叫中 / 振铃 | `CALL_START`(`ryzt=1`、`jcsj_s`) | #8 回执(含 `computedCallTime`) | `session.changed` + D1 起倒计时 |
| `ConfbridgeJoin`(房间侧) | **应答 / 完成叫乘** | `ANSWERED` → `ryzt=3`、`finish_jc='1'`、`hjxysj` | #9 `DUTY_RESULT`(`ANSWERED`)、#24 摘要 | `state.changed(1→3)` + `session.changed` |
| `OriginateResponse.Failure` / 30s 无入会 | 失败 / 无应答 | `CALL_FAILED` → `ryzt=2`、`jcbjsj_s`、`is_jcbj='1'` | #9 + #12 `ALARM` | `alarm.raised(NO_ANSWER)` + 复叫 35s |
| 入会后 < 3s `Hangup` | 异常中断 | 保持终态并触发复叫 | #9 + #12 | 告警 + 行置红 |
| `Hangup`(正常) | 会话结束 | **不回写** `gy_jcxx`(已结束不回写) | #24 摘要 | `session.changed(HANGUP)` |
| `AMI Disconnect` | 在途会话 `UNKNOWN` | 不推定终态 | — | 5 分钟 `core show channels concise` 比对 |
| 录音 / TTS 失败 | 媒体异常 | **不改叫乘结论** | #24 `recordingMissing` | `notice.sync` / 告警 |
> **"接听后播放提示语音完成叫乘"的落地口径**:接通判据是 `ConfbridgeJoin`(房间侧入会),**不是** SIP `Answer`(`docs/10` §7.7 第 2 条、本文件 表 5-2)——`Answer` 只代表摘机,不代表房间内人员能听到。入会即提交 `ANSWERED` 完成叫乘,随后 `Playback(tts)` 播放提示语音,`apt_tts_utterance` 的 `DONE`/`FAILED` 与 `apt_recording.state` **不回退、不改写**叫乘终态(沿用 §4 第 3 条"录音不阻断叫乘")。
> **"叫乘状态由业务系统依据 AMI 事件进行上报"的落地口径**:`apt.voice.ami` 收原始事件 → `apt.voice.event` 依表 5-2 / 表 7-5 判业务状态 → **提交状态机命令**(唯一写入口 `apt.duty.state`)→ 条件更新 `gy_jcxx` → `apt-report` 经 #8 / #9 / #24 上行。AMI 事件本身既不直接写业务表,也不单独构成对外上报。
### 9.3 第二类:平板叫乘(拉取 + 本地定时播放 + 刷脸签到)
表 5-14 触发条件(按环节)
| 触发环节 | 条件 |
| --- | --- |
| 任务生成 | `gy_jcxx.call_mode='TABLET'`、`ryzt=0`、`stoped=0`;#1 下发 `features.tabletCall=true`;按 `jcsj` 升序生成 `dispatch_seq`,写 `apt_tablet_task(WAITING)` |
| 平板可拉取 | `apt_tablet_device.register_status=ACTIVE`、心跳在窗内;`GET /open/v1/tablet/tasks?deviceCode&cursor&limit`(N-03)按 `dispatch_seq` 顺序返回 |
| **本地播放** | 平板**提前拉取并缓存**任务,按任务中的 `jcsj`(预定时间点)由平板本地定时器触发播放;以心跳下发的 `serverTime` 校准本地时钟,`clockSkewMs` 超阈值时重拉并告警 |
| 刷脸签到 | 播放后进入签到窗口(`signTimeoutSec`,默认 60s,Q-29);刷脸 `VERIFIED` 即结束叫乘 |
| 结果上报 | `POST /open/v1/tablet/results`(N-04),`resultId` 幂等 + HMAC / 时间窗 / `nonce` |
> **拉取 ≠ 执行**:服务端只生成任务,不向平板推送呼叫指令(平台不回调公寓、设备侧一律拉取,CR-C08);"到达预定时间点后播放语音"由平板本地完成。断网时已缓存任务仍可按 `jcsj` 播放,结果写入平板本地队列,恢复后按 `resultId` 重传。
表 5-15 平板任务负载字段(N-03 响应 `tasks[]`,沿用 CR-02 表 3-8 的 `/open/v1/tablet/**`)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` / `jcid` / `dispatchSeq` | string / string / long | 任务、叫乘任务、派发序号三层键 |
| `jcsj` / `expireAt` | datetime | **预定播放时刻**(公寓本地复算值,CR-C06)与任务失效时刻 |
| `roomNo` / `ryid` / `personName` | string | 房间与人员 |
| `ttsTemplate` / `ttsText` | string | 提示语音模板与文本,平板本地合成 / 播放 |
| `faceVerifyRequired` / `signTimeoutSec` | boolean / int | 是否必须刷脸、签到窗口 |
| `serverTime` / `clockSkewMs` | datetime / long | 时钟校准(与 #14 心跳同源,见 `docs/公寓端/09` 表 9-11) |
| `cursor` | string | 拉取游标,单调递增 |
图 5-9 平板叫乘端到端时序
```mermaid
sequenceDiagram
autonumber
participant E as apt-dispatch
participant DB as 本地库
participant G as apt.device.gateway
participant T as 平板终端
participant U as 人员
participant S as apt.duty.state
participant W as /ws/duty + outbox
E->>DB: 按 jcsj 生成 apt_tablet_task(WAITING, dispatch_seq)
T->>G: GET /open/v1/tablet/tasks(cursor)【提前拉取】
G-->>T: tasks[](jcsj, ttsText, faceVerifyRequired, serverTime)
T->>G: POST /open/v1/tablet/tasks/{taskId}/ack(N-05)
G->>DB: apt_tablet_task(PULLED, pulled_at, pulled_by)
T->>T: 按 serverTime 校准,本地缓存任务
Note over T: 到达 jcsj(预定时间点)
T->>U: 本地播放提示语音
U->>T: 刷脸确认签到
alt 刷脸通过
T->>T: 生成结果(ANSWERED / VERIFIED / resultId)
else 未刷脸或超时
T->>T: 生成结果(UNVERIFIED / NO_ANSWER / resultId)
end
T->>G: POST /open/v1/tablet/results(N-04,HMAC + nonce)
G->>DB: 幂等校验 → apt_tablet_call_result(PENDING)
G->>S: 只提交状态机命令(不直改 gy_jcxx)
S->>DB: VERIFIED → ryzt=3 / finish_jc='1';否则 ryzt=2 + 复叫 + 告警
S-->>W: state.changed + tablet.result
W->>DB: outbox enqueue(#9 TABLET_RESULT)
```
表 5-16 平板叫乘交互流程
| 步 | 主体 | 动作 | 落表 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| 1 | `apt-dispatch` | 按 `jcsj` 生成任务 `WAITING` + 单调 `dispatch_seq` | `apt_tablet_task`、`gy_jcxx` | 规则快照过期 → `40903` |
| 2 | 平板 | `GET /open/v1/tablet/tasks`(N-03,游标)**提前拉取** | — | 拉取失败按退避重试,不影响本地已缓存任务 |
| 3 | 平板 → `apt.device.gateway` | `POST /open/v1/tablet/tasks/{taskId}/ack`(N-05) | `apt_tablet_task(PULLED)` | 重复 ACK → `40916` |
| 4 | 平板 | 按 `serverTime` 校准本地时钟并缓存任务 | 平板本地 | 偏差超阈值重拉 + 告警 |
| 5 | 平板 | **到达 `jcsj` 本地播放提示语音** | 平板本地 | 无音频资源 → 本地降级提示并上报 `FAILED` |
| 6 | 人员 | 在签到窗口内**通过平板刷脸确认签到** | 平板本地 | 未刷脸 / 超时 → `UNVERIFIED` / `NO_ANSWER` |
| 7 | 平板 | 生成 `resultId` 与结果(`resultType` + `faceVerifyState` + `verifiedRyid`) | 平板本地 | 只传识别结论,不传抓拍帧 / 人脸原图 / 比对帧;比对所需特征值按 `docs/公寓端/10` 的设备范围下发 |
| 8 | 平板 → `apt.device.gateway` | `POST /open/v1/tablet/results`(N-04) | `apt_tablet_call_result` | HMAC / 时间窗 / `nonce` 校验;同 `resultId` 返回首次结果 |
| 9 | `apt.device.gateway` → `apt.duty.state` | 校验幂等后**只提交状态机命令** | `apt_tablet_call_result(PENDING)` | 不直改 `gy_jcxx`;校验失败丢弃并告警 |
| 10 | `apt.duty.state` / `apt-report` | 按表 5-20 落终态,推 `tablet.result` + `state.changed`,#9 上报 `TABLET_RESULT` | `gy_jcxx`、`apt_report_outbox` | 断网进 outbox,恢复后按原幂等键续报 |
表 5-17 平板侧可靠性口径
| 项 | 口径 |
| --- | --- |
| 定时 | 以任务 `jcsj` 为准由平板本地定时器触发;`clockSkewMs` > 1s 重拉校准(表 5-8 自检项 11) |
| 断网 | 已缓存任务照常播放;结果写平板本地队列,恢复后按 `resultId` 重传,幂等不产生第二次终态 |
| 重放 | 同一 `taskId` 只播放一次(可配重播次数,Q-29);重复拉取 / ACK 返回 `40916` |
| 过期 | `now > expireAt` 或任务已被其他平板拉取 → `40916`,置 `EXPIRED` 并转人工 |
| 人脸数据 | 平板叫乘**结果**只传 `verifiedRyid` 与 `faceVerifyState`,抓拍帧与人脸原图不上报、不上传平台;比对所需特征值按 `docs/公寓端/10` 的设备权限范围下发;**人员主数据照片与特征值按 CR-C24 允许平台 ↔ 公寓双向同步**(CR-02 §12) |
| 本期边界 | 本地定时与断网续传依赖终端固件能力;协议未确认前(`50101`)只在设计层描述,不伪造成已实现(§3 表 5-3「诚实的边界」) |
### 9.4 状态上报机制(两类方式统一)
表 5-18 状态来源 → 状态机 → 上行
| 环节 | SIP 语音叫乘 | 平板叫乘 | 共同约束 |
| --- | --- | --- | --- |
| 事实来源 | AMI 事件(`apt.voice.ami`) | 平板上报 `TABLET_RESULT`(`apt.device.gateway`) | 一条任务只有一种事实来源,由 `gy_jcxx.call_mode` 标记 |
| 业务判定 | `apt.voice.event` 按表 5-2 / `docs/10` 表 7-5 映射 | 按 `faceVerifyState` + `resultType` 映射 | 都**不直接 UPDATE `gy_jcxx`** |
| 状态落地 | 提交 `CALL_START` / `ANSWERED` / `CALL_FAILED` 命令 | 提交 `ANSWERED` / `CALL_FAILED` 命令 | `apt.duty.state` 唯一写入口;`where id=? and ryzt=<期望值>`;已结束不回写 |
| 幂等 | `session_id` / `event_seq` | `resultId`(UK `(gyid, result_id)`) | 迟到 / 重复结果记 `IGNORED`,不产生第二次终态 |
| 上行 | #8 回执、#9 `DUTY_RESULT`、#24 `VOICE_SUMMARY` | #9 `TABLET_RESULT`、#8 回执(含 `dispatchSeq`)、#13 归档摘要 | `apt_report_outbox` + `idempotencyKey = hash(gyid+bizType+bizId+bizVersion)`;断网累积、`serverReceivedAt` 归日 |
| 状态字段 | `ryzt` / `finish_jc` / `hjxysj` / `jcbjsj_s` | 同左(语义一致) | 值班台与平台按同一套终态理解,不因实现方式分叉 |
三条不可违反规则(§2)继续适用。本节新增两条:
1. **方式内事实来源唯一**:SIP 任务不接受平板上报结果,平板任务不套用 AMI 事件推断;一条 `jcid` 的终态只能由一种事件源触发。
2. **跨端上报一律带 `callMode`**:#9 / #24 的 `payload` 用 `SIP` / `TABLET` 分组,平台按方式统计;不新增 `bizType`、不新增跨端接口编号、不改变 `v2-draft` 契约形态。
### 9.5 结果反馈路径
表 5-19 四条反馈路径
| # | 路径 | SIP 语音叫乘 | 平板叫乘 | 约束 |
| --- | --- | --- | --- | --- |
| 1 | 值班台(端内) | `/ws/duty` `state.changed`(0→1、1→3)、`session.changed`;异常 `alarm.raised` / `alarm.cleared` | `state.changed` + `tablet.result`;异常 `alarm.raised` | D1 行状态、D5 会议桥占用、D6 设备状态;`seq` 单调 + 60s 兜底轮询(`docs/应用端/04` 表 4-12) |
| 2 | 平台(跨端) | #8 任务回执(含 `computedCallTime`)、#9 `DUTY_RESULT`、#24 语音质量摘要、#13 归档 | #9 `TABLET_RESULT`、#8 回执(含 `dispatchSeq`)、#13 归档;**不发 #24 语音摘要** | 公寓主动推、平台不回调;`payload` 带 `callMode`;断网 outbox + 续报(CR-02 F-E) |
| 3 | 本地留痕 | `gy_jcxx`、`apt_call_session`、`apt_call_event`、`apt_recording`、`apt_tts_utterance` | `gy_jcxx`、`apt_tablet_task`、`apt_tablet_call_result` | 共用 `duty_operation_log` 审计与 `apt_report_history` 上报终态 |
| 4 | 异常兜底 | 30s 无入会 → `gy_bjxx(NO_ANSWER)` + 复叫 35s;接通即断 → 复叫 + 告警;AMI 断连 → `UNKNOWN` + 5 分钟巡检;会议桥不可用 → 值班台兜底 | `UNVERIFIED` / `SKIPPED` → 复叫 + 告警(Q-22);`NO_ANSWER` → `gy_bjxx(TIMEOUT)`;任务过期 `40916` → 转人工;平板离线 → `apt_device_alarm` | 异常同样经状态机与 outbox 上报,不另设"人工填状态"入口 |
表 5-20 终态矩阵(两类方式共用一套 `gy_jcxx` 语义)
| 情形 | 事件 / `faceVerifyState` | 状态机命令 | `gy_jcxx` | 上行 | 值班台 |
| --- | --- | --- | --- | --- | --- |
| SIP 接通 | `ConfbridgeJoin` | `ANSWERED` | `ryzt=3`、`finish_jc='1'`、`hjxysj` | #9 `DUTY_RESULT` + #24 | `state.changed(1→3)` + `session.changed` |
| SIP 无应答 | 30s 无入会 / `OriginateResponse.Failure` | `CALL_FAILED` | `ryzt=2`、`jcbjsj_s`、`is_jcbj='1'` | #9 + #12 | `alarm.raised(NO_ANSWER)` + 复叫 |
| 平板刷脸签到 | `ANSWERED` + `VERIFIED` | `ANSWERED` | `ryzt=3`、`finish_jc='1'` | #9 `TABLET_RESULT` | `tablet.result` + `state.changed` |
| 平板未刷脸 | `ANSWERED` + `UNVERIFIED` / `SKIPPED` | 保持待确认 + 复叫 | `ryzt` 不变(超窗口转 `2`) | #9 + #12 | `tablet.result` + `alarm.raised` |
| 平板无应答 | `NO_ANSWER` / `FAILED` | `CALL_FAILED` | `ryzt=2` | #9 | `alarm.raised` + 复叫 |
| 任务取消 | `CANCELLED` | `STOP`(值班台) | `stoped='1'`,`ryzt` 不变 | #9 | `state.changed` |
| 迟到 / 重复结果 | 任意 | `IGNORED` | 不回写 | 不重复上报 | 只记留痕(`applied_state=IGNORED`) |
### 9.6 与既有章节的关系与待决策
1. §1 任务生成、§2 SIP 状态机、§4 语音事件与媒体、§6 outbox、§7 人工干预继续有效;本节是两条链路的**串联视图**,不改任何状态字段写入口,也不改 `docs/10` 表 7-5 的事件映射。
2. CR-02 §7.1 F-A / §7.2 F-B 继续有效;本节补齐"平板本地定时播放"(表 5-14、5-15)与"结果反馈路径"(表 5-19、5-20)两个此前未展开的环节。
3. **不新增**跨端接口编号、`bizType`、WS 消息名或数据表;`TABLET_RESULT`、`device.alarm`、`state.changed` 等全部沿用既有契约。
4. 本期边界:平板本地定时与断网续传依赖终端固件与设备协议确认(D6 / `50101`),未确认前只作为设计定义。
表 5-21 新增决策点(沿用 `docs/应用端/10` 的决议记录格式)
| 编号 | 待决事项 | 未决默认口径 | 拍板时点 |
| --- | --- | --- | --- |
| Q-29 | 平板签到窗口 `signTimeoutSec`、语音重播次数与 `clockSkewMs` 阈值 | 60s / 1 次 / 1s | 阶段二契约冻结前 |
---
## 10. 人员入住作业流程(CR-02 §13 流程索引)
> **权威表述**:`docs/公寓端/11-人员入住作业流程.md`(`CI-01`~`CI-05`、表 11-1~11-35、图 11-1~11-3)。本节只给出与叫乘流程的衔接关系,不重复定义状态机、数据表与接口字段。
表 5-22 入住作业五环节与叫乘衔接
| 环节 | 主体 | 关键动作 | 与既有章节的关系 |
| --- | --- | --- | --- |
| CI-01 注册与签到 | 签到终端 / 值班台 | 扫脸核验;未注册先按 `docs/公寓端/10` §4 完成注册并重新核验 | 复用 F-F 注册链路;不新增注册入口 |
| CI-02 入住申请 | 签到终端 → `apt-stay` | 选房间、交路(可确定时)提交申请;`apt_local_task` 建单并预分配 `jcid` | 申请阶段**不建 `gy_jcxx` 行**(§1 任务生成口径不变) |
| CI-03 值班员审核 | 值班台 → `apt-stay` | 审核、房间调整(仅审核期)、办理入住;`checkin_state=CHECKED_IN` 并写 `gygl_ly_event(CHECKIN)` | 房间调整复用 F-C 换房联动但不产生换乘单;状态字段仍只经 `apt.duty.state` |
| CI-04 提报与匹配 | `apt-report` / 平台 | #9 `CHECKIN` 提报 → #6 运安计划 → #8 回执作匹配输入 → #7 响应体 `matchVerdict[]` | 匹配结论唯一合规载体是 #7(R-05);`match_state` 沿用正式五值 |
| CI-05 叫乘环节 | `apt-dispatch` | #7 拉任务 → 按 `apt_master_rule` 复算 `jcsj` → 回 #8 → 按 `jcsj` + `dispatch_seq` 派发 | 派发后进入 **§9** 两类叫乘流程;`jcsj` 恒由公寓复算(CR-C06) |
三条衔接约束:① `applyId` ↔ `jcid` 一对一,`gy_jcxx` 沿用预分配 `jcid`;② 只有 `CHECKED_IN` 且有效在寓(`finish_ly='0' AND sjlysj IS NULL`)才生成叫乘任务,已离寓迟到任务记 `IGNORED`;③ "已入住"不新增 `gy_jcxx` 状态字段,流程态落 `apt_local_task` 补列(CR-C26)。本补遗不新增业务表(59 张不变)、不新增跨端编号、不新增 `bizType`。
---
## 11. 消费、充值与菜品管理(CR-02 §14 流程索引)
> **权威表述**:`docs/公寓端/12-消费充值与菜品管理.md`(`CF-01`~`CF-03`、表 12-1~12-34、图 12-1~12-4)。本节只给出与既有账务闭环(§6 F-D)及主数据下行(§5)的衔接关系,不重复定义状态机、数据表与接口字段。
表 5-23 三项流程与既有章节的衔接
| 流程 | 主体 | 关键动作 | 与既有章节的关系 |
| --- | --- | --- | --- |
| CF-01 人员消费 | 消费终端 → `apt.ledger.local` → `apt-report` | 按 `PUBLISHED` 食谱选菜并显示金额 → N-49 查补贴 / 现金额度 → "补贴优先、现金补足" → 一条 `CONSUME` 流水 → #10 上报 | 复用 §6 **F-D 六步账务闭环**与 F-E 上报历史;`platform-ledger` 唯一写主账 |
| CF-02 人员线下充值 | 签到终端 → 值班台 → `apt.ledger.local` | 扫码只识别公寓 → N-52 申请(申请表不存金额)→ 管理员 N-54 二次确认 → 只增 `xjye` → #10 同步 | 同样复用 F-D;**不接线上支付**,二维码不构成资金流;断网受 #1 `offlineQuota` 约束(Q-21 / Q-42) |
| CF-03 公寓菜品管理 | 公寓管理员 → `platform.catering` → `apt.catering` → 消费终端 | N-57 发布阶段性食谱 → #4 `recipes[]` 下行 → N-48 按 `version` 主动拉取并缓存 → 点击查看菜品、加入选择菜单 | 属 §5 主数据下行的增量类型;食谱缓存只在终端本地,**不落公寓本地业务表** |
三条衔接约束:① 三条流程共用 F-D 闭环与 #10 `LEDGER_OFFLINE`,`bizId = flowId`、幂等键复用、`serverReceivedAt` 归日不变;② 一笔消费只产生一条流水,`allowanceAmount + cashAmount = abs(amount)`,不额外生成 `ALLOWANCE_DEDUCT` 以免平台 `sum(amount)` 双计;③ `apt.ledger.local` / `platform-ledger` 各自单写入口不变,本补遗不新增业务表(59 张不变)、不新增跨端编号、不新增 `bizType`。
+210
View File
@@ -0,0 +1,210 @@
# 06 部署与配置
> 追溯:`docs/10` §8.1 表 8-1(三端部署形态)、§8.2 表 8-2(非功能要求)、§8.3 表 8-3(单机资源)、
> 表 8-4(端口与防火墙)、表 8-5(录音存储与清理)、§2.5 表 2-3(部署形态行)。
> 现状来源:`apartment/README.md` §2、§4.3、§4.4、§5.3;`apartment/db/mysql/README.md` §6。
## 1. 部署形态
表 6-1 公寓端部署单元
| 组件 | 形态 | 数量 | 说明 |
| --- | --- | --- | --- |
| `apt-admin.jar` | Spring Boot 单进程 | **1** | 唯一可执行模块,含管理端 API、值班 API、叫乘引擎、设备网关 |
| 前端 | 同一构建产物 `apt.html` + `duty.html` | 1 份 | Nginx 静态托管,双入口 |
| MySQL | 本地实例,一公寓一库 `apt_apartment` | 1 | 74 张表(若依 30 + 业务 44) |
| Redis | 本地实例 | 1 | 会话、令牌、验证码、nonce 去重 |
| Asterisk | 同机 | 1 | PJSIP + `ConfBridge(Conf-01)` + `Playback(tts)` |
| Nginx | 同机 | 1 | 静态页 + `/prod-api` 反代 + 录音 HTTP 目录 |
| 串口广播 | 同机物理串口 | 0~1 | 未配置时自检降级(表 7-7 第 10 项) |
**不允许的形态**(`docs/10` 表 2-3 负面清单):第二数据源、第二启动模块、为值班台单独扩容、注册中心、消息中间件。
> **CR-02 §12 补遗(人脸模块配置)**:`apt-face` 不引入新组件、新端口或新数据源,仍随 `apt-admin.jar` 单进程部署,与上述负面清单一致。新增配置项只写变量名、不落明文:`face.photo.maxBytes` / `face.feature.maxBytes`(默认 512 KB / 64 KB,Q-33)、`face.media.signTtlSec`(≤ 600)、`face.algo.whitelist`、`face.scope.checkIntervalMin`(默认 30)、`face.erase.timeoutSec`(默认 300,Q-30)。照片与特征值加密落本地媒体目录、库内只存引用与校验和;媒体签名地址仅内网、一次一密。人员主数据人脸按 CR-C24 允许跨端,业务抓拍与录音仍不出公寓。
> **CR-02 §14 补遗(消费 / 充值 / 食谱配置)**:`apt.ledger.local` / `apt.catering` 不引入新组件、新端口或新数据源,仍随 `apt-admin.jar` 单进程部署。新增配置项只写变量名、不落明文:`ledger.amount.scale=2`、`ledger.recharge.maxAmount`(默认 100,Q-45)、`ledger.recharge.minAmount`(默认 0.01)、`ledger.cas.retryTimes=1`、`ledger.offline.quota.*`(#1 下发,缺省全 `0` 即断网不开放,Q-21 / Q-42)、`catering.recipe.cacheTtlSec`、`catering.recipe.selectTimeoutSec`(默认 300)、`features.remoteOrdering=false`。金额一律 `decimal(12,2)` / 字符串两位小数、禁止浮点;公寓线下二维码只写 `gyid` + `qrCodeId`,**不配置任何支付渠道参数**;食谱 `imageRef` 只存引用,图片本体不出平台对象存储。
> **CR-02 §15 补遗(三端一致性 / 统计部署边界)**:四能力与平台统计能力**不新增本端配置项、组件、端口或数据源**。统计生成调度归平台端 `platform-worker`(**单活** + 库级协调锁)与 `platform-report`,任务进度落平台库 `PL_JOB_*`,报表导出复用平台 `report-export` 桶(`docs/平台端/08` §1 / §4、`docs/平台端/10` 表 10-14 / 10-17)。本端只需在 #13 摘要上报评价 / 房间周转分组计数,不改 outbox / 离线额度 / 食谱缓存等既有配置,`gyid` 仍只从凭据解析。三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,统计结果不反向下发本端、不产生跨端副作用。
## 2. Profile 拆分
表 6-2 profile 与环境差异
| profile | 用途 | 数据库 | Redis | AMI | 前端代理 |
| --- | --- | --- | --- | --- | --- |
| `local` | 开发机 / 本机联调 | 本机 `apt_apartment` | 本机 | 可缺省(自检降级) | `VUE_APP_PROXY_TARGET` 指向本机后端 |
| `prod` | 公寓现场 | 现场库 | 现场 | 现场 Asterisk | Nginx 反代 `/prod-api` |
约定:
1. `spring.profiles.active=${APT_PROFILE:local}` + `spring.profiles.group{local: druid, prod: druid}`(现状)。
2. 不保留上游 `dev`/`pj`/`byq` 等 profile(`apartment/README.md` §4.3);旧公寓的差异通过环境变量而非 profile 表达。
3. **配置项全部外置**:`application-druid.yml` / `application-local.yml` / `application-prod.yml` 只写 `${ENV:default}`,不出现明文口令。
4. **数据库实例级参数(部署必做,不入应用配置)**:`transaction_isolation=READ-COMMITTED`。
原因:`docs/10` 表 5-6/5.6.1 要求显式设 `READ-COMMITTED`,MySQL 默认 `REPEATABLE-READ` 有间隙锁与幻读差异;
现状实例仍为默认值(`08` 差-2)。应用侧仍以"状态机条件更新"兜底,不单纯依赖隔离级别。
5. **字符集与排序规则**:库/表/连接统一 `utf8mb4` + `utf8mb4_general_ci`(与若依 30 张表一致,`08` 差-1),
不单表改成 `0900_ai_ci`,否则与 `sys_*` JOIN 报 `Illegal mix of collations`。
## 3. 环境变量清单
表 6-3 环境变量(只列变量名与占位默认值,不含真实值)
| 变量 | 默认占位 | 用途 | 来源 |
| --- | --- | --- | --- |
| `APT_PROFILE` | `local` | 激活 profile | 现状 |
| `APT_GY_ID` | 空(**必填,缺省启动失败**) | 公寓编码,写入令牌与上下文 | 现状;`docs/10` §3.3 |
| `APT_DB_HOST` / `APT_DB_PORT` / `APT_DB_NAME` | `127.0.0.1` / `3306` / `apt_apartment` | MySQL 连接 | 现状 |
| `APT_DB_USER` / `APT_DB_PASSWORD` | 空 | MySQL 账号(仅本库权限) | 现状 |
| `APT_DRUID_USER` / `APT_DRUID_PASSWORD` | 空 | Druid 监控台 | 现状 |
| `APT_REDIS_HOST` / `APT_REDIS_PORT` / `APT_REDIS_DB` / `APT_REDIS_PASSWORD` | `127.0.0.1` / `6379` / `0` / 空 | Redis | 现状 |
| `APT_TOKEN_SECRET` | 上游示例值(**必须覆盖**) | JWT 密钥 | 现状 |
| `APT_UPLOAD_PATH` | `${user.home}/apt/uploadPath` | 上传目录 | 现状 |
| `APT_AMI_HOST` / `APT_AMI_PORT` | `127.0.0.1` / `5038` | AMI 连接(现网配置值为 `7058`,见 `docs/10` 表 8-4) | 现状 |
| `APT_AMI_USER` / `APT_AMI_SECRET` | 空 | AMI 账号(**不写入仓库**) | 现状 |
| `APT_CONSOLE_EXT` | 空 | 值班台分机(多个用逗号) | 现状 |
| `APT_CONFERENCE` | `Conf-01` | 会议桥标识(D11 未定前不改) | 现状 |
| `APT_RECORDING_BASE_URL` | 空 | 录音 HTTP 基址 | 现状 |
| `APT_SERIAL_PORT` | 空 | 串口广播端口,空表示未配置 | 现状 |
| `APT_REPORT_DISPATCH_ENABLED` | `false` | outbox 派发开关(平台未就绪时保持关闭) | **建议新增** |
| `APT_GYID_STRICT` | `true` | 是否严格拒绝无 `gyid` 的请求(过渡期可放宽旧接口) | **建议新增** |
| `APT_RECONCILE_CRON` | `0 */30 * * * ?` | 分机对账周期(`docs/10` 表 5-5) | **建议新增** |
| `APT_CALL_TIMEOUT_MS` | `30000` | 无应答超时(`docs/10` §7.9) | **建议新增** |
| `APT_RECORDING_RETENTION_DAYS` | `90` | 录音留存(D15 未定前取保守值) | **建议新增** |
**红线**:数据库口令、AMI 口令、平台令牌、私钥一律不进仓库;仓库内不得出现内网地址(`docs/10` 表 9-1、表 10-1 安全行)。
## 4. 端口与防火墙
表 6-4 端口清单(`docs/10` 表 8-4)
| 端口/协议 | 用途 | 监听地址 | 放通范围 | 本期 |
| --- | --- | --- | --- | --- |
| `8086/tcp` | 后端 HTTP(前端 dev 直连或 Nginx 反代) | 内网地址 | 公寓内网 | 是 |
| `8082/tcp` | 前端 dev server(仅开发机) | `localhost` | 本机 | 是(仅开发) |
| `80/443/tcp` | Nginx:`/apt.html`、`/duty.html`、`/prod-api`、录音 HTTP | 内网地址 | 公寓内网 | 是 |
| `7058/tcp` | AMI(现网配置值) | `127.0.0.1` | **仅本机** | 是 |
| `5060/udp`、`5060/tcp` | SIP 信令 | 公寓服务器内网 | 公寓内网话机与广播终端 | 是 |
| `10000-20000/udp` | RTP 媒体 | 同上 | 同上 | 是 |
| `22/tcp` | 运维 SSH | 内网地址 | 仅运维网段 | 是 |
| 出站 `443/tcp` | 公寓 → 平台 HTTPS | — | 单向出站,平台不回调 | 是 |
三条硬约束(`docs/10` §8.3):
1. **AMI 不出本机**:同机部署没有理由开放 7058。
2. **SIP/RTP 不出公寓内网**:NAT 会导致单向语音。
3. **录音 HTTP 只对内网**:录音含个人信息,外发须走申请与审计(D15)。
## 5. Asterisk 同机部署要点
表 6-5 资源与配置(`docs/10` 表 8-3、表 8-5)
| 项 | 建议值 | 本设计的使用方式 |
| --- | --- | --- |
| CPU | 2 核(4 核更稳) | 会议桥混音 + 转码 + JVM + MySQL 同机 |
| 内存 | 8 GB(JVM 3 / Asterisk 1 / MySQL 2 / 系统 2) | JVM 参数按 3 GB 上限配置 |
| 磁盘 | 200 GB SSD(系统 60 / 库 30 / 录音 80 / 余量 30) | 录音与抓拍分卷,不共用(照片本体在平台 `person-photo`,本地只存 `photoUrl` / `photoVersion`) |
| 操作系统 | Linux(推荐统信/麒麟或 Rocky) | D14 未定前按 Linux 交付;Windows 仅开发机 |
| 时钟 | NTP 指向公寓内网源 | 偏差 > 1s 告警(表 7-7 第 11 项) |
| 录音目录 | `/data/asterisk/monitor/<gyid>/<yyyy>/<MM>/<dd>/` | `apt_recording.storage_path` 只存相对路径 |
| 抓拍目录 | `/data/apt/capture/<gyid>/<yyyy>/<MM>/<dd>/` | 离寓抓拍**本地留存**,与录音分卷;**不进平台对象存储**(`docs/10` §7.5 第 4 条、`docs/平台端/07` 表 7-7),清理沿用 `leaveSnapshot.retentionDays` / `cleanupCron`,平台只收索引与计数 |
| 录音容量 | 8 kHz/16 bit 单声道约 0.94 MB/分钟;197 房间、日均 60 通约 114 MB/日;90 天约 10 GB | 容量保护:> 85% 最旧优先清理;< 5 GB 停止录音 |
| 清理任务 | 每日 `0 30 2 * * ?` | 先置 `PURGED` 再删文件,失败重试 + 告警 |
| 一致性校验 | 每日抽样 10 条做"索引↔文件↔可播放"三向校验 | 写入 `apt_ops_selfcheck` |
**Asterisk 侧固定约定**(`docs/10` §7.7 链路要点,不可在公寓端代码里改):
| 约定 | 值 | 出现位置 |
| --- | --- | --- |
| 房间侧呼叫 | `PJSIP/<sip_account>`,扩展 `1901@from-internal`,带 `Call-Info: answer-after=5` | `apt.voice.event` |
| 值班台侧呼叫 | `PJSIP/<console>`,扩展 `1701@from-internal` | 同上 |
| 会议桥 | `Conf-01`(D11 未定前不改) | `apt.voice.event` |
| TTS | `Local/1701@from-internal` 发起 `Playback(<文本>,tts)` | `apt.voice.media` |
## 6. Nginx 双入口
表 6-6 站点配置要点
| 路径 | 指向 | 说明 |
| --- | --- | --- |
| `/apt.html` + `/apt/**` | `dist/apt.html` | hash 路由,不需 `try_files` 回退;若切 history 必须加回退 |
| `/duty.html` + `/duty/**` | `dist/duty.html` | 同上 |
| `/prod-api/**` | `http://127.0.0.1:8086/` | 透传真实 IP(`X-Forwarded-For`),`gyid` 走会话而非 IP |
| `/conf/**` | 录音目录 | 只对内网,禁止目录列表 |
| `/ws/duty` | 后端 WebSocket | 值班推送(后续) |
要点:
1. 前端默认 `hash` 模式(`VUE_APP_ROUTER_MODE`),多页产物在 `history` 下需按入口分别配回退(`apartment/README.md` §5.1)。
2. `/prod-api` 必须透传 `X-Forwarded-For`,但**不再用 IP 推断公寓**(`docs/10` §3.3 第 1 条)。
3. 若未来按公寓区分域名,平台端不配置到公寓的路由(表 8-1 隔离第 1 条)。
## 7. 日志与可观测性
表 6-7 指标与日志(`docs/10` 表 8-2 可观测性行)
| 类别 | 指标/日志 | 来源 |
| --- | --- | --- |
| 叫乘 | 任务创建数、执行成功率、准点率偏差 | `gy_jcxx` + 定时聚合 |
| 语音 | AMI 连接状态、重连次数、会议桥占用、呼叫成功率、录音缺失数 | `apt_ast_ami_session`、`apt_call_session`、`apt_recording` |
| 设备 | 在线率、版本分布、指令积压 | `gy_sb`、`apt_device_command` |
| 上报 | outbox 积压、失败率、水位、对账差异 | `apt_report_outbox`、`apt_report_watermark` |
| 主数据 | 快照版本、过期数、apply 失败数 | `apt_master_version` |
| 日志 | SLF4J,`traceId` + `gyId` 贯穿 | 拦截器注入 MDC |
| 健康探针 | `/apt/ops/health/call-ready`(监控用,只返回可/不可叫乘) | `apt-ops` |
| 审计 | `sys_oper_log`(HTTP 级)+ `duty_operation_log`(业务级) | 若依 + 业务切面 |
日志保留与清理不在本期实现,沿用若依默认(按 `logback.xml`),生产按磁盘容量另定。
## 8. 单实例约束与降级
表 6-8 不可水平扩容的原因与降级路径(`docs/10` 表 10-1、表 8-1)
| 约束 | 原因 | 降级/替代 |
| --- | --- | --- |
| 单实例 | Quartz 任务、会议桥状态、WebSocket 推送、SIP 注册均为进程内或单点状态 | 冷备(RTO 15 分钟),不做双活(D16) |
| Asterisk 不可用 | 会议桥与 TTS 全失效 | 降级为"串口广播 + 终端提示 + 值班台人工",不允许静默失败 |
| 平台不可用 | 主数据与上报中断 | 用最后快照执行;outbox 累积;横幅提示离线模式 |
| 录音目录满 | 无法落盘 | 停止录音但保留叫乘;`apt_recording` 标 `MISSING` |
| Redis 不可用 | 令牌与验证码失效 | 拒绝登录(安全优先),已在线的令牌按 JWT 自校验继续有效 |
| 数据库不可用 | 全站不可写 | 进程保活但 `call-ready=false`;叫乘转值班台人工 |
## 9. 安全与凭据
表 6-9 安全要求(`docs/10` 表 2-2 安全行、表 8-2 安全认证行、表 9-1)
| 项 | 要求 | 现状 |
| --- | --- | --- |
| 数据库口令 | 环境变量注入,不落盘不进仓库 | 已实现(`APT_DB_USER`/`APT_DB_PASSWORD`) |
| AMI 口令 | 环境变量注入;AMI 只监听本机 | 变量已预留,`apt-voice` 尚未持有连接 |
| JWT 密钥 | 必须覆盖默认值,长度 ≥ 32 | 已预留(`APT_TOKEN_SECRET`) |
| 设备凭据 | 一机一密 + HMAC(`docs/10` §7.2) | **未实现**(依赖固件与协议) |
| 跨端认证 | mTLS 或双向令牌 | **未实现**(平台侧未就绪) |
| 默认账号 | `admin`/`ry` 仅限开发库,生产必须改口令并回收 `ry` | `db/ruoyi/README.md` 已注明 |
| 敏感字段 | 身份证、手机号按权限脱敏输出 | 本期在 VO 层实现 |
| 录音访问 | 短期签名地址 + 导出审计 | 本期实现签名,导出审计随 D15 |
## 10. 启动与冒烟步骤(不含任何真实凭据)
```powershell
# 1) 构建(JDK 8)
mvn -f apartment\server\pom.xml -B -DskipTests clean package
# 2) 注入环境变量(值为占位,实际由部署方提供)
$env:APT_DB_USER='<db-user>'; $env:APT_DB_PASSWORD='<db-password>'
$env:APT_GY_ID='<gy-id>'; $env:APT_TOKEN_SECRET='<random-secret>'
# 3) 启动
java -jar apartment\server\apt-admin\target\apt-admin.jar --spring.profiles.active=local
# 4) 冒烟(顺序固定)
# GET /captchaImage → 200,uuid + base64 图片
# POST /login → token
# GET /getInfo → roles / permissions
# GET /apt/ops/selfcheck → 自检项列表(AMI 未配置时应为 DEGRADED/BLOCKED,而不是异常)
# GET /apt/route/list → 200 + 空列表(业务表为空属正常)
```
冒烟判定的三条原则:
1. **空数据不是失败**:业务表为空时列表返回 `total=0`,HTTP 200。
2. **依赖缺失要显式降级**:AMI 不可达时自检返回 `BLOCKED` 并给出原因,不允许抛 500。
3. **不伪造成功**:设备指令、分机收敛在未联调前一律 `PENDING`。
+175
View File
@@ -0,0 +1,175 @@
# 07 实施计划与验收
> 追溯:`docs/10` §9 表 9-1~表 9-6(阶段划分与退出条件)、§11.2 表 11-1(优先级)、§5.6.2 表 5-7(迁移步骤)、
> §5.6 表 5-9(切换前必查 12 项)、§8.2 表 8-2(非功能要求)。
> **CR-02 §12 验收追加(人脸数据下发管理)**:本端验收要点以 `docs/公寓端/10` 表 10-30(10 项)与 CR-02 §12 表 3-52(验收 15~17)为准——注册链路四段闭环与双路互不阻塞、六项功能齐备、媒体边界与 59 张表口径不破。Q-30~Q-35 未决前按 `docs/公寓端/10` 表 10-29 默认口径执行;模块只复用既有表 + 补列,DDL 仍待阶段二契约冻结评审通过后执行,不改变本文件阶段划分与批次顺序。
> **CR-02 §13 验收追加(人员入住作业流程)**:本端验收要点以 `docs/公寓端/11` 表 11-35(8 项)与 CR-02 §13 表 3-57(验收 18~20)为准——五环节闭环、"已入住"判据与 `applyId` ↔ `jcid` 一对一、边界不破。Q-36~Q-40 未决前按 `docs/公寓端/11` 表 11-34 默认口径执行;`apt_local_task` 补列为设计定义,DDL 待阶段二契约冻结,Q-40 冻结前用 `duty_operation_log` JSON 兜底留痕。
> **CR-02 §14 验收追加(消费、充值与菜品管理)**:本端验收要点以 `docs/公寓端/12` 表 12-34(8 项)与 CR-02 §14 表 3-62(验收 21~23)为准——三项流程闭环、四条状态链一致性、边界不破。Q-41~Q-45 未决前按 `docs/公寓端/12` 表 12-33 默认口径执行;`gy_ykt_*` / `apt_ledger_offline` / `YKT_MEALS` 补列均为设计定义,DDL 待阶段二契约冻结评审通过后执行,不改变本文件阶段划分与批次顺序;补列冻结前可用 `duty_operation_log` JSON 兜底留痕。
> **CR-02 §15 验收追加(三端一致性)**:本端四项能力权威表述不变,验收关注点仅为 ① #13 `ARCHIVE` 摘要 `payload` 增 `satisfaction{}` / `roomOccupancy{}` **分组计数**(不新增 `bizType` / 接口 / 明细,见 `docs/公寓端/09` §0 表 C-9);② 三端状态映射与 **CR-02 §15 表 3-66** 一致;③ 平台统计 / 报表 / 排名结果**不回写**本端任何业务状态。全局验收 24~26 见 CR-02 §15 表 3-69,统计侧验收见 `docs/平台端/10` 表 10-22;Q-46~Q-50 未决前按 `docs/平台端/10` 表 10-21 默认口径执行;**变更点与影响范围见 CR-02 §15 表 3-63**。
>
> **开发计划承接(非设计变更,2026-11-10)**:`docs/公寓端/13` 基于 `dev/` 实测上游,把本文件的 `M0`~`M6` 与阶段零~三切成 `IT-0`~`IT-8` 小步迭代(**P1 入住 / 叫乘优先**,消费 / 充值 / 菜品延后 `IT-9`~`IT-11`),并给出里程碑、交付物、对齐矩阵、风险台账与跨迭代联调测试。**本文件表 7-1~7-8 仍是阶段划分、批次顺序、验收标准与交付物的唯一权威**;`13` 只做排序与切分,冲突以本文件为准。需评审确认的 6 项 P0 前提与 3 项口径偏离见 `13` 表 13-4 / 表 13-6。
## 1. 阶段划分
表 7-1 阶段与退出条件(沿用 `docs/10` 表 9-1~9-6 的编号)
| 阶段 | 目标 | 本轮位置 | 退出条件 |
| --- | --- | --- | --- |
| 阶段零(表 9-1) | 基线修复:依赖、CI、口令外置 | **已完成**(`apartment/README.md` §6 实测) | 可稳定编译;无明文口令;测试可重复 |
| 阶段一(表 9-2) | 现状梳理与边界确认 | **本目录即产出** | 表能回答"谁写、谁读";接口能回答归属 |
| 阶段二(表 9-3) | 接口契约与数据模型设计 | **部分完成**:本地库结构已完成;跨端契约见 `09` | 契约评审通过;幂等与重试用例通过 |
| 阶段三(表 9-4) | 单公寓试点端到端跑通 | **本期实施范围** | 断网 2 小时完成"入住→叫乘→复叫→报警→应答→离寓→归档";恢复 10 分钟追平 |
| 阶段四(表 9-5) | 多公寓推广 | 后续 | 单公寓部署 ≤ 4 小时;跨 `gyid` 越权 100% 拦截 |
| 阶段五(表 9-6) | 平台收敛与旧逻辑下线 | 后续 | 公寓端不再连运安与中心 |
## 2. 本期迭代批次
表 7-2 批次与依赖(顺序即依赖顺序,不允许跳批)
| 批次 | 内容 | 交付物 | 前置依赖 | 可验证结果 |
| --- | --- | --- | --- | --- |
| **M0** | 基座:`gyid` 注入、统一错误码、审计切面、分页与 Swagger、`/apt/**` 鉴权 | `apt-framework` 拦截器 + `apt-common` 错误码 + `AptContext` | 阶段零已完成 | 无 `gyid` 的请求被拒;跨公寓访问返回 40302 |
| **M1** | 交路管理 | `apt-master-client` 交路 CRUD + 复算校验 + 页面 | M0;`apt_master_route` 有数据或 Mock | 列表/详情/启停/复算校验全通;停用必须填原因;重复 `jlid` 返回 40901 |
| **M2** | 房间管理 | `apt-stay` 房间档案 + 房态 + 停用/恢复 | M1(复用主数据快照校验) | 按楼层/房号/房态筛选正确;停用后房态为 4;恢复触发重算 |
| **M3** | 叫乘信息管理 | `apt-dispatch` 列表/详情/时间线/受控命令 | M1、M2(需人员与房间数据);状态机 `apt.duty.state` | 只读字段不可改;`STOP`/`RE_CALL` 状态迁移正确且留审计;非法状态返回 40902 |
| **M4** | 叫乘人员管理 | `apt-master-client` 人员列表/详情/关联查询 | M1、M3(关联叫乘记录) | 关联 Tab 正确;平台字段只读(改则 40904) |
| **M5** | 设备管理 | `apt-device` 档案/心跳/指令记录/入队 | M0 | 指令只入队(`PENDING`),不伪造 ACK;停用后不参与指令投递 |
| **M6** | 语音与运维 | `apt-voice` 分机/会话/录音 + `apt-ops` 自检 | M3、Asterisk 可达 | 分机改号被拒(40906);录音签名地址有效;自检 12 项落库;`call-ready` 判定正确 |
**为什么 M3 在 M4 之前**:叫乘记录的筛选依赖人员姓名(`rymc` 冗余列已存在),但人员**关联查询**依赖叫乘数据;
先做 M3 可让 M4 的关联 Tab 直接可验证,避免两次返工。
## 3. 每批次的可验证结果清单
表 7-3 验证项(每条都必须可复现)
| 批次 | 验证项 | 通过标准 |
| --- | --- | --- |
| M0 | 无令牌访问 `/apt/route/list` | 401 |
| M0 | 有令牌但缺权限点 | 40301 |
| M0 | 令牌 `gyid=A` 查 `gyid=B` 数据 | 40302,无数据泄露 |
| M0 | 异常统一返回 | `{code, msg, data.traceId}`,无堆栈外泄 |
| M1 | 列表分页 | `pageNum=1&pageSize=10` 与 `pageNum=2` 无重复无遗漏;`pageSize=1000` 被截为 100 |
| M1 | 必填校验 | 缺 `cc`/`kcsj`/`reason` 返回 40001 且提示字段名 |
| M1 | 重复校验 | 同 `jlid` 再新增返回 40901 |
| M1 | 停用审计 | `duty_operation_log` 有对应记录,`detail` 含原因 |
| M2 | 房态筛选 | 选"不能入住"只返回 `room_status=4` |
| M2 | 停用 | 房态变 4,在寓人员仍可查(不停叫乘) |
| M2 | 恢复 | 触发重算,`state_version` 递增 |
| M3 | 状态字段不可改 | `PUT /apt/jcxx/{id}` 不存在该接口;`correct` 改 `ryzt` 返回 40001 |
| M3 | `STOP` 幂等 | 同 `commandId` 重复提交只生效一次 |
| M3 | 状态非法 | 对 `finish_ly='1'` 的记录 `STOP` 返回 40902 |
| M3 | 时间线 | 含状态迁移 + AMI 事件 + 人工命令,按时间升序 |
| M4 | 快照只读 | 改 `xm` 返回 40904 |
| M4 | 关联查询 | 人员详情显示在住房间、最近交路、最近 5 条叫乘 |
| M5 | 指令入队 | 返回 `PENDING` + `protocolConfirmed=false` |
| M5 | 不伪造 | 无设备心跳时 `state` 不自动变 `SENT` |
| M6 | 改号被拒 | `PUT` 带 `extension` 返回 40906 |
| M6 | 录音地址 | 未 `PURGED` 返回 `url` + `expireAt`;`PURGED` 返回 41001 |
| M6 | 自检 | `apt_ops_selfcheck` 有 12 项记录;AMI 不可达时 `call-ready=false` 且有原因 |
## 4. 全量"模块—表—接口—页面—权限点"
表 7-4 功能域映射总表(接口明细见 `04`)
| # | 功能域 | 后端模块 | 主要表 | 接口前缀 | 页面(`entries/apt/views/…`) | 权限点前缀 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 交路与计划 | `apt-master-client`、`apt-dispatch` | `apt_master_route`、`apt_master_rule`、`apt_holiday`、`gy_syn_jcxx` | `/apt/route/**`、`/apt/plan/**` | `route/RouteList.vue`、`route/RouteDetail.vue` | `apt:route:*` |
| 2 | 房间与在寓 | `apt-stay` | `apt_master_room`、`gy_rooms`、`gy_jcxx`、`gy_clean_task` | `/apt/room/**` | `room/RoomList.vue`、`room/RoomDetail.vue` | `apt:room:*` |
| 3 | 叫乘执行与状态机 | `apt-dispatch` | `gy_jcxx`、`gy_jcxx_his`、`gy_bjxx`、`duty_manual_action` | `/apt/jcxx/**` | `jcxx/JcxxList.vue`、`jcxx/JcxxDetail.vue` | `apt:jcxx:*` |
| 4 | 设备网关 | `apt-device` | `gy_sb`、`gy_sb_xt`、`apt_device_command` | `/apt/device/**`、`/open/v1/device/**` | `device/DeviceList.vue`、`device/DeviceDetail.vue` | `apt:device:*` |
| 5 | 语音与录音 | `apt-voice` | `apt_sip_endpoint`、`apt_call_session`、`apt_recording`、`apt_ast_drift` | `/apt/voice/**` | `voice/EndpointList.vue`、`voice/CallList.vue`、`voice/RecordingList.vue` | `apt:voice:*` |
| 6 | 账务本地流水 | `apt-stay`(`ledger`) | `gy_ykt_*` | `/apt/ledger/**`(P2) | `ledger/ConsumeList.vue`(P2) | `apt:ledger:*`(P2) |
| 7 | 上报 outbox | `apt-report` | `apt_report_outbox`、`apt_report_watermark` | `/apt/report/**` | `report/OutboxList.vue` | `apt:report:*` |
| 8 | 主数据快照 | `apt-master-client` | `apt_master_version`、`apt_master_*`、`apt_holiday` | `/apt/master/**` | `master/VersionList.vue` | `apt:ops:health` |
| 9 | 运维自检 | `apt-ops` | `apt_ops_selfcheck` | `/apt/ops/**` | `ops/Selfcheck.vue` | `apt:ops:*` |
| 10 | RBAC 与审计 | `apt-system`、`apt-framework` | `sys_*`、`duty_operation_log` | `/system/**` | 若依自带页面 | `system:*` |
前端路由约定(apt 入口固定路由,`02` §2.4):
| 路由 path | 组件 | 菜单标题 | 权限点 |
| --- | --- | --- | --- |
| `/route` | `route/RouteList` | 交路管理 | `apt:route:list` |
| `/route/:id` | `route/RouteDetail` | 交路详情 | `apt:route:query` |
| `/room` | `room/RoomList` | 房间管理 | `apt:room:list` |
| `/room/:id` | `room/RoomDetail` | 房间详情 | `apt:room:query` |
| `/jcxx` | `jcxx/JcxxList` | 叫乘信息 | `apt:jcxx:list` |
| `/jcxx/:id` | `jcxx/JcxxDetail` | 叫乘详情 | `apt:jcxx:query` |
| `/person` | `person/PersonList` | 叫乘人员 | `apt:person:list` |
| `/person/:id` | `person/PersonDetail` | 人员详情 | `apt:person:query` |
| `/device` | `device/DeviceList` | 设备管理 | `apt:device:list` |
| `/device/:id` | `device/DeviceDetail` | 设备详情 | `apt:device:query` |
| `/voice/endpoint` | `voice/EndpointList` | 分机台账 | `apt:voice:endpoint:list` |
| `/voice/call` | `voice/CallList` | 通话记录 | `apt:voice:call:list` |
| `/voice/recording` | `voice/RecordingList` | 录音索引 | `apt:voice:recording:list` |
| `/ops/selfcheck` | `ops/Selfcheck` | 运行自检 | `apt:ops:selfcheck` |
| `/report/outbox` | `report/OutboxList` | 上报队列 | `apt:report:list` |
## 5. 验收标准
表 7-5 阶段三验收(`docs/10` 表 9-4 验收标准 + 表 5-9 必查项)
| # | 验收项 | 标准 | 验证方法 |
| --- | --- | --- | --- |
| 1 | 端到端主链路 | 断网 2 小时完成"入住 → 叫乘 → 复叫 → 报警 → 应答 → 离寓 → 归档" | 现场演练 + `gy_jcxx`/`apt_call_session`/`gy_jcxx_his` 取证 |
| 2 | 上报追平 | 恢复后 10 分钟内 outbox 清空且对账零差异 | `apt_report_watermark.acked_watermark = current_watermark` |
| 3 | 准点率 | 不低于现网 | 对比 `jcsj_s - jcsj` 分布 |
| 4 | 值班操作步骤 | 增加不超过 10% 且有记录 | 操作录像 + `duty_operation_log` |
| 5 | 结构规范 | 44 张业务表全部有主键、有注释、时间 `datetime(3)`、金额 `decimal(12,2)` | 已实测通过(`db/mysql/README.md` §5) |
| 6 | `gyid` 隔离 | 跨公寓用例 100% 拦截 | `03` 表 3-14 四条用例 |
| 7 | 状态单写入口 | `ryzt`/`finish_*`/`stoped` 的 UPDATE 只出现在 `apt.duty.state` | 架构测试 + 全仓 `grep` |
| 8 | 无明文凭据 | 仓库内无口令、无内网地址、无 AMI 凭据 | CI 正则扫描 |
| 9 | 可叫乘探针 | AMI 断开时 `call-ready=false` 且原因准确 | 手工停 Asterisk 验证 |
| 10 | 降级路径 | Asterisk 不可用时走"串口广播 + 值班台人工",不静默失败 | 演练 |
## 6. 测试矩阵
表 7-6 每个管理模块必须覆盖的 8 类用例
| 类型 | 用例设计 | 期望 | 覆盖模块 |
| --- | --- | --- | --- |
| 正常查询 | 无条件列表 + 单条件筛选 | 200,行数与 `total` 一致 | M1~M6 |
| 分页边界 | `pageSize=100`(上限)、`pageNum` 超末页、`pageSize=0` | 上限截断;超末页返回空列表且 `total` 正确;0 取默认值 | M1~M6 |
| 必填校验 | 缺主键业务字段、缺 `reason` | 40001,`msg` 指出字段 | M1、M2、M5、M6 |
| 重复/冲突 | 同业务键重复新增、并发同时新增 | 40901;并发时仅一条成功 | M1、M2、M5、M6 |
| 无权限 | 无权限点账号访问 | 40301 | M0~M6 |
| `gyid` 隔离 | 跨公寓读/写/设备心跳 | 40302/40303,无数据泄露 | M0~M6 |
| 状态非法 | 对终态记录执行命令、重复命令 | 40902/40907 | M3、M6 |
| 只读快照 | 改平台下发字段 | 40904 | M1、M2、M4 |
补充三类专项(不属于管理模块,但同一批次验收):
1. **幂等**:同 `commandId` 连发 3 次,只产生 1 条 `duty_manual_action`。
2. **时间戳**:事件/会话出参精确到毫秒,列表精确到秒。
3. **空数据**:业务表为空时列表 200 + `total=0`,不报错、不返回 `null`。
## 7. 联调步骤
表 7-7 逐模块联调前置与顺序
| 步骤 | 前置条件 | 操作 | 通过标志 |
| --- | --- | --- | --- |
| 1 | 库表就绪(已具备)+ 后端可启动 | 启动 `apt-admin`(`local`) | 启动日志无异常;`sys_config` 可读 |
| 2 | 登录链路 | `captchaImage → login → getInfo` | 拿到 token 与 `permissions` |
| 3 | 主数据导入 | 用脚本/Mock 向 `apt_master_*` 灌 3 条交路、5 个房间、5 个人员 | `apt_master_version` 有 `APPLIED` 记录 |
| 4 | M1 交路 | 列表 → 详情 → 启停 → 复算校验 | `03`表3-14 与 `07`表7-3 的 M1 项全过 |
| 5 | M2 房间 | 筛选 → 详情 → 停用 → 恢复 | 房态与 `state_version` 变化正确 |
| 6 | M3 叫乘 | Mock 一条 `gy_jcxx` → 列表 → 详情 → `STOP`/`RE_CALL` | 状态迁移与审计正确 |
| 7 | M4 人员 | 列表 → 详情 → 关联 Tab | 关联数据正确且快照只读 |
| 8 | M5 设备 | 建档 → 心跳(Mock)→ 指令入队 → 指令记录 | `PENDING` 不被伪造为 `ACKED` |
| 9 | M6 语音 | 分机台账(Mock Asterisk 或真实)→ 会话/录音查询 → 自检 | 改号被拒;自检落库;`call-ready` 判定正确 |
## 8. 交付物清单
表 7-8 每批次交付物
| 批次 | 代码 | 文档 | 验证记录 |
| --- | --- | --- | --- |
| M0 | 拦截器、错误码、审计切面、Swagger 分组 | 接口约定落地说明 | 8 类用例的 M0 部分 |
| M1~M6 | Controller/Service/Mapper/DTO/VO/页面/路由/权限点 | 各域的字段说明与待确认项 | 每模块的验证清单 + `curl`/页面截图 |
| 全量 | 增量 DDL(若 G-01/G-02 确认) | 更新 `db/mysql/README.md` 与本文档 | 二次执行幂等验证 |
**提交纪律**:不执行 `git commit` / `git push`,变更清单待评审(`apartment/README.md` §0)。
> `docs/公寓端/13` 表 13-32 已按 `IT-0`~`IT-8` 重排本表交付物与验证记录;两者内容一致,排期视角以 `13` 为准、验收口径以本表为准。
+196
View File
@@ -0,0 +1,196 @@
# 08 待决策与风险
> 追溯:`docs/10` §10.1 表 10-1(风险与应对)、§10.2 表 10-2(待决策点 D1–D16)、§10.3 表 10-3(语音与数据库专项风险)、
> §11.3(后续动作第 5 条:D11/D12/D15 需在阶段二冻结契约前定稿)、表 5-9(切换前必查 12 项)、表 9-1~表 9-6(阶段退出条件)。
> 本文只做"记录差异 + 给默认口径 + 请求确认",**不改写 `docs/10` 的任何既有结论**;确认后再统一修订。
> **CR-02 §12 决策点追加**:人脸数据下发管理新增 **Q-30~Q-35**(离寓擦除时限与告警级别、设备离线期间是否禁用本地人脸放行、人脸主数据保留期与删除留痕、媒体签名有效期与大小上限、`LONG_TERM` 类别标注来源、平台驳回注册结果时是否自动擦除设备缓存),完整表述与未决默认口径见 `docs/公寓端/10` 表 10-29、CR-02 §12 表 3-52;沿用本文件决议记录格式(结论 + 决议人 + 日期三者缺一视为未决议),不改写 `docs/10` 的 D1~D16 既有结论。
> **CR-02 §13 决策点追加**:人员入住作业流程新增 **Q-36~Q-40**(交路是否必填 / `CHECKIN_PENDING` 是否可直接入住、`UNMATCHED` 超时阈值与告警级别、入住后能否换房、签到终端离线能否先办后补、`apt_local_task` 补列 DDL 冻结时点),完整表述与默认口径见 `docs/公寓端/11` 表 11-34、CR-02 §13 表 3-57;与 Q-20~Q-35 并列,同样不改写 D1~D16 既有结论。
> **CR-02 §14 决策点追加**:消费、充值与菜品管理新增 **Q-41~Q-45**(补贴不足是否允许现金补足、线下充值断网是否受 `offlineQuota` 约束、食谱主数据归属与 `knye` / `xjye` 语义复核、远程点菜启用条件、充值单笔上限 / 是否双人复核 / 内部人员透支),完整表述与默认口径见 `docs/公寓端/12` 表 12-33、CR-02 §14 表 3-62;与 Q-20~Q-40 并列,同样不改写 D1~D16 既有结论。
> **CR-02 §15 决策点追加**:三端一致性同步与平台统计分析新增 **Q-46~Q-50**(评价 / 房间周转数据接入、周期时区与季度划分、统计新表与补列 DDL 冻结、综合排名权重与逆向指标、报表向应用端开放粒度),完整表述与默认口径见 `docs/平台端/10` 表 10-21、CR-02 §15 表 3-69;与 Q-20~Q-45 并列,同样不改写 D1~D16 既有结论。本端只承接 **Q-46**(#13 摘要 `payload` 分组计数上报方式),其余为平台侧口径;未决前按默认口径执行。
>
> **开发计划前提追加(非设计变更,2026-11-10)**:`docs/公寓端/13` 表 13-4 登记 6 项 **P0 排期前提**(技术基线冲突、表数口径、前端入口、CI/测试、明文凭据、契约冻结)与表 13-6 的 3 项口径偏离,均须评审签字后方可执行;表 13-24 把本文件 `D5` / `D8'` / `D3` / `D7` / `D11` / `D12` / `D15` / `D16` 与 `Q-15` / `Q-22` / `Q-25` 等既有编号映射到 `IT-0` 冻结会与后续迭代时点。**沿用既有编号、不新增 `D` / `Q` / `R`**,不改变本文件任何既有结论与默认口径。
## 1. 决策点全景(D1–D16)
表 8-1 十六个待决策点对公寓端设计的影响(建议列摘自 `docs/10` 表 10-2)
| # | 决策点 | `docs/10` 建议(摘要) | 对公寓端影响 | 未定时的默认执行口径 | 需定稿时点 |
| --- | --- | --- | --- | --- | --- |
| D1 | 餐饮归属 | 业务平台上移;扣款走账务接口,禁止公寓端直改余额 | 无 | 餐饮不进公寓端;本期无餐饮接口与页面 | 不阻塞 |
| D2 | 余额主账位置 | 平台主账 + 公寓缓存;离线允许本地记账并上报 | 中 | 本地只记流水(`gy_ykt_*`),余额以平台为准,不做本地扣减 | P2 |
| D3 | 房间主数据归属 | 平台存基线(`gyid+roomNo+床位数`),物理属性公寓维护并上报 | **高** | 本期 `apt_master_room` 全字段只读(含楼层/朝向/床位),本地不改、不上报;新建房间不开放 | M2 前 |
| D4 | 运安接入位置 | 只在平台端接入;公寓端不再持有 WSDL | 无 | 公寓端只消费 `/platform/v1/plans/duty`,不建运安客户端 | 不阻塞 |
| D5 | 是否保留"一实例多公寓" | 目标架构下取消 | **高** | 一进程一 `gyid`,`APT_GY_ID` 必填、缺省启动失败;不做运行期切换 | M0 |
| D6 | 应用端是否独立前端工程 | 与公寓端同一工程双入口 | 无 | 同工程双入口(`entries/apt` + `entries/duty`),本期只交付 apt 入口 | 不阻塞 |
| D7 | 交路人工维护落点 | 平台维护模板,公寓可"应急新增"并上报确认 | 中 | 本期不做本地应急新增(受 G-04 约束);只做查询、停用/启用、复算校验 | M1 |
| D8 | 叫乘时间以谁为准 | 平台下发"计划 + 规则版本",公寓端复算执行,不一致告警 | 中 | 以平台计划为准、本地按 `apt_master_rule` 复算;差异超阈值告警并回执 `computedCallTime` | M1 |
| D9 | Oracle 存量公寓 | 明确保留期与迁移批次 | 无 | 新端不连 Oracle,不做双库适配 | 不阻塞 |
| D10 | `kcgl` 库存边界 | 独立处置,不进入公寓端 | 无 | 公寓端不建库存表、不做库存接口 | 不阻塞 |
| D11 | 会议桥粒度 | 建议"每任务一个会议桥 + 保留 `Conf-01` 兼容与广播" | **高** | 单 `Conf-01` + **同瞬间仅允许一个房间会话**(串行化 + 告警);`conf_no` 保持单值、不加唯一约束 | 阶段二冻结契约前 |
| D12 | 号段与分机编号规则归属 | 平台定规则、公寓端落地;需明确跨公寓是否要求分机号唯一 | **高** | 分机号 `char(8)`、入库统一大写、仅本公寓内唯一 `UK(gyid, extension)`;跨公寓唯一性不做校验 | 阶段二冻结契约前 |
| D13 | 平台端是否一并改 MySQL | 短期选 A(平台不动) | 无 | 跨端只有 HTTP,无跨库 JOIN;方言问题只存在于平台侧 | 不阻塞 |
| D14 | 公寓端操作系统 | 建议统一 Linux | 中 | Linux 交付(串口/录音/服务化按 Linux 验证);Windows 仅作开发机,不出现在交付清单 | M5 前 |
| D15 | 录音合规 | 需明确留存期、告知、可访问角色、导出;未定前 90 天 + 仅内网 + 导出必审计 | **高** | 严格按"90 天 + 仅内网 + 导出必审计"执行,且签名地址有效期 ≤ 5 分钟 | 阶段二冻结契约前 |
| D16 | Asterisk 高可用方式 | 单机 + 冷备(RTO 15 分钟)+ 降级广播 | **高** | 单实例部署,不做双活;冷备只保证可恢复,不保证会话不中断 | M6 前 |
## 2. 五个关键决策点的默认口径与返工半径
表 8-2 D11/D12/D14/D15/D16 的未定默认与影响面
| 决策 | 未定时的默认执行口径 | 受影响的已定设计 | 一旦改判需返工的点 | 阻塞批次 |
| --- | --- | --- | --- | --- |
| D11 会议桥粒度 | 单 `Conf-01`;进入叫乘前先抢"全局会话锁",同一瞬间只允许一个房间在会;第二个房间排队并告警 | `apt_call_session.conf_no`、`apt_tts_utterance.conf_no` 单值存储;`APT_CONFERENCE` 环境变量 | 两张表的 `conf_no` 语义(单值→多值)、叫乘串行化逻辑、TTS 投递目标、并发容量结论 | M6 |
| D12 号段规则 | 分机号只在本公寓内唯一;`extension` 定长 8、大写归一;平台下发的号段规则存 `apt_ast_config_version(config_type='SIP', source='PLATFORM')` | `apt_sip_endpoint` UK `(gyid, extension)`;接口层禁止改号 | 若要求跨公寓唯一,需要全局号段分配与平台侧冲突检测;历史录音检索口径改变 | M6 |
| D14 操作系统 | Linux(统信/麒麟或 Rocky);串口、录音目录、systemd 服务化均按 Linux 交付 | `06` 表 6-1/表 6-5;`APT_SERIAL_PORT`;录音路径 `/data/asterisk/monitor/...` | 串口库替换(`jSerialComm`↔Windows 兼容)、路径分隔符、服务化方式、自检项 | M5 |
| D15 录音合规 | 留存 90 天(`apt_recording.retention_days` + `APT_RECORDING_RETENTION_DAYS`);仅内网可播;导出必审计;播放走签名地址 | `04` 表 4-15 第 16/17 接口;`06` §9;`apt_recording` 表 | 留存天数默认值、签名有效期、导出接口(本期没有)、审计字段是否够用 | M6 |
| D16 Asterisk 高可用 | 单机 + 冷备(RTO 15 分钟);故障时降级"串口广播 + 值班台人工";不承诺会话不中断 | `06` 表 6-8;`apt_ops_selfcheck` 的 `AMI`/`ASTERISK` 检查项 | 若改双机主备:SIP 注册漂移、录音共享存储、会议桥状态、AMI 重连判定全部要重构 | M6 |
### 2.1 D11 三个方案的可比性
表 8-3 会议桥粒度选项(供决策,不预设结论)
| 方案 | 做法 | 优点 | 代价 | 对本地库的影响 |
| --- | --- | --- | --- | --- |
| A 单 `Conf-01`(现状) | 全部房间进同一个会议 | 零 Asterisk 改造;拨号计划不动 | 串音风险高;并发上限 = 1;无法按任务隔离 | `conf_no` 单值即可,本期无需改表 |
| B 每任务一桥 | `Conf-<taskId 短码>`,`Conf-01` 保留做广播 | 隔离干净、可并发、录音天然按房间切分 | 需改拨号计划与会议桥创建/回收;Asterisk 资源占用上升 | `conf_no` 仍单值(每会话一个),但需"桥生命周期"管理 |
| C 混合(建议方向) | 叫乘走 B,广播/群呼走 `Conf-01` | 兼顾隔离与兼容 | 两套路径,测试面翻倍 | 需在 `apt_ast_config_version(config_type='CONF')` 记桥清单 |
**本设计的默认落法(A)不锁死 B/C**:`apt_call_session.conf_no` / `apt_tts_utterance.conf_no` 已是独立列,切 B 时只需改写入值来源,不改表结构;但**并发容量结论(`06` 表 6-8"单实例")与串行化逻辑必须随决策一起改**。
### 2.2 D11 未定期间的串行化兜底
表 8-4 并发叫乘的拦截点(A 方案下必须成立)
| 位置 | 规则 | 失败行为 |
| --- | --- | --- |
| `apt.duty.state` 命令入口 | 进入 `CALL_START` 前获取"全局会话锁"(`gy_call_duty_match_lock` 的 `lock_name='__CALL_SESSION__'` 租约) | 抢不到 → 任务保持 `ryzt=0`,下个调度周期重试(≤ 3 次) |
| 会议桥入会校验 | `apt_call_session` 中 `state in ('JOINED_CONF')` 且 `conf_no='Conf-01'` 的记录数 > 0 时拒绝新会话 | 返回 `40908 APT_CONF_BUSY`,页面提示"会议桥占用" |
| 超时释放 | 会话进入 `HANGUP`/`FAILED`/`TIMEOUT` 立即释放锁;异常未释放由 5 分钟巡检回收 | 巡检写 `apt_ast_drift`(`ATTR_MISMATCH`)并告警 |
## 3. 风险清单
### 3.1 通用风险(对齐 `docs/10` 表 10-1)
表 8-5 表 10-1 十二条风险在公寓端的落点
| 风险(表 10-1 原文) | 公寓端落点 | 当前状态 | 责任批次 |
| --- | --- | --- | --- |
| 职责边界不清 | `03` §1 表归属矩阵 + `01` 表 1-3 五维分界 + 越界三细则 | 已产出,待确认(差-7) | 阶段一 |
| 数据不一致 | `apt_master_version` 版本校验 + 复算回执 + outbox 日对账 | 设计完成,实现未开始 | M0/M1/M3 |
| 设备兼容 | 网关类型插件 + `capabilities` 协商 + 口令模式开关 | 仅入队,协议未确认(`04` §7 第 8 条) | M5 |
| 双写/重复上报 | `uk_outbox_idem` + `(gyid,biz_type,biz_id,biz_version)` | 表结构已具备 | M0 |
| 旧系统兼容 | 本期不做兼容适配层(新端不接管存量接口) | **未覆盖** | 阶段五 |
| 迁移回滚 | 冷备 + 按公寓切换开关 | 仅设计(`06` 表 6-8) | 阶段四 |
| 状态机多写 | `apt.duty.state` 单写入口 + 条件更新 + 架构测试 | 设计完成 | M3 |
| 时钟/时区 | NTP + `serverTime` 下发 + 三时间戳 + 偏差告警 | 表结构已具备(`event_at_ms`) | M0/M6 |
| 单实例约束被忽视 | `06` 表 6-8 明确不可扩容的 4 类进程内状态 | 已写入文档 | 阶段四 |
| 权限体系冲突 | `apt_person_account` 映射 + 值班员只走公寓端登录 | 表已建,逻辑未实现 | M0 |
| 构建基线不稳 | 阶段零已完成(`apartment/README.md` §6) | **已完成** | — |
| 安全暴露 | 口令外置、`/open/**` 独立鉴权、设备一机一密 | 部分(设备凭据未实现) | M0/M5 |
### 3.2 语音与数据库专项风险(对齐 `docs/10` 表 10-3)
表 8-6 表 10-3 十二条专项风险的应对落点
| 风险(表 10-3 原文) | 本设计的应对 | 落点 |
| --- | --- | --- |
| Asterisk 单点 | 冷备 + 自检 + 降级广播 + 明确 RTO | `06` 表 6-8、`apt_ops_selfcheck` |
| AMI 事件丢失 | 事件 + 30s 超时 + 5 分钟巡检三路判定 | `05` 表 5-2、图 5-4 |
| 分机配置漂移 | 30 分钟对账 + `apt_ast_drift` + 默认只报告 | `04` 表 4-15 第 20/21 接口 |
| 会议桥串音/串台 | 表 8-4 串行化兜底 | `05` §2 |
| TTS 文本槽位覆盖 | `apt_tts_utterance` 队列 + `seq`,不用内存槽位 | `05` 表 5-4 |
| 录音合规 | 90 天 + 仅内网 + 导出审计(D15) | `06` §9、`04` 表 4-15 |
| 录音缺失 | 文件名约定兜底 + 每日三向校验 + 不阻断叫乘 | `05` 表 5-4 |
| 注册状态不可靠 | `qualify` + 注册事件 + 定时探测双源判定 | `apt_sip_endpoint.register_status` |
| MySQL 迁移语义变化 | 表 5-9 十二项已在 `apartment/db/mysql/README.md` §5 自检 | 已完成 |
| 双库方言 | 公寓端只连 MySQL,无跨库 SQL | 依赖约束(`02` 表 2-3) |
| 号码规划冲突 | 分机号含 `gyid` 唯一 + 号段规则平台化(D12) | `apt_sip_endpoint` UK |
| 时钟偏差 | NTP + 三时间戳 + `clock_skew_ms` 告警 | `gy_sb_xt`、`apt_ast_ami_session` |
### 3.3 本方案新增的风险
表 8-7 文档与实施层面的 5 条新风险
| # | 风险 | 表现 | 应对 |
| --- | --- | --- | --- |
| R-01 | 追溯失真 | 文档引用了 `docs/10` 的小节号,但原文后续修订导致对不上 | 每份文档文首给对应关系表;`docs/10` 修订时同步回填本文 |
| R-02 | 契约漂移 | 本文接口草案与阶段二冻结版不一致,开发按草案实现 | `09` 明确"草案"状态;冻结前不写 Controller |
| R-03 | 演示数据当真实数据 | 联调用 Mock 数据被误当平台下发 | Mock 数据必须带 `source='LOCAL'` 或 `source_version=0`,页面上标识"非平台下发" |
| R-04 | 越界实现 | 管理端页面直接改 `ryzt`、直接写 `apt_master_*` | 架构测试:`grep` UPDATE 语句白名单;Code Review 检查项(`02` 表 2-13) |
| R-05 | 文档与代码不同步 | `03` §1 表归属矩阵与真实 Mapper 写入方不一致 | 每批次交付时用脚本比对"表→写入 Mapper",差异进 CI |
## 4. 与 `docs/10` 的差异清单(8 处,已确认,`docs/10` v1.2 已回填)
表 8-8 差异清单(8 处差异已全部采纳,状态见末列)
| # | `docs/10` 结论 | 本文/现状实际 | 依据 | 建议 | 影响面 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| 差-1 | 表 5-4 要求标识类列 `utf8mb4_0900_ai_ci`、密钥类 `utf8mb4_0900_as_cs`;表 5-9 第 7 项要求"每列明确排序规则,不依赖默认" | 44 张业务表统一 `utf8mb4_general_ci`(与若依 `sys_*` 一致) | `01_master_data.sql` 头注第 3 条;`apartment/db/mysql/README.md` §5 已知事项 | 保留 `general_ci` 以避免 `Illegal mix of collations`;大小写归一改在应用层(分机号/房间号入库统一大写),并在 `docs/10` 增补"排序规则以库默认统一"的例外说明 | 仅文档口径;不影响功能 | 已采纳(`docs/10` v1.2 已回填) |
| 差-2 | 表 5-6/5.6.1 要求"实例级固定 `READ-COMMITTED`" | 实测实例为默认 `REPEATABLE-READ` | `apartment/db/mysql/README.md` §5 已知事项 | 列为**部署待办**:`my.cnf` 设 `transaction_isolation=READ-COMMITTED`;同时以"状态机条件更新"兜底,不依赖隔离级别 | 部署清单(`06` §2)需补一条 | 已采纳(`docs/10` v1.2 已回填) |
| 差-3 | 表 2-2 技术基线含 `ruoyi-quartz` 与 `ruoyi-generator` 两个框架模块 | 12 个 `apt-*` 模块中,Quartz 依赖落在 `apt-dispatch`,**未引入代码生成模块** | `apt-dispatch/pom.xml`(`org.quartz-scheduler:quartz`);模块清单见 `02` 表 2-2 | 不补 generator(表结构已定稿、生成代码风格与手写不一致);确认 Quartz 归属 `apt-dispatch` | 模块清单表述 | 已采纳(`docs/10` v1.2 已回填) |
| 差-4 | §3.3 写"落操作域 `DUTY_OPERATION`";§5.3 写"应用域新增 `DUTY_*`" | 实际表名 `duty_operation_log`、`duty_manual_action`、`duty_shift_handover`(小写、带后缀) | `07_report_duty_ops.sql` | 以实际表名为准,`docs/10` 修订时更新引用;前缀语义(`duty_`)不变 | 表名引用 | 已采纳(`docs/10` v1.2 已回填) |
| 差-5 | 表 5-3 语音域表清单列 **10 张**,归档表 `APT_CALL_EVENT_HIS` 仅出现在"与既有表的处理关系"第 5 条 | 本地库实际建 **11 张**(含归档表) | `05_voice.sql`(11 张) | 表 5-3 补 1 行;属"清单未列",非口径冲突 | 语音域表数 | 已采纳(`docs/10` v1.2 已回填) |
| 差-6 | 表 5-9 第 1 项口径为"40 张存量表 + 新增 `APT_*`" | 业务表共 **44 张** = `APT_*` 23 + `GY_*` 18 + `duty_*` 3;其中 4 张为执行域**新增但用 `GY_` 前缀**(`gy_jcxx_his`、`gy_clean_task`、`gy_bjxx`、`gy_bjcstj`) | `03` 表 3-1~表 3-7;`03_execution.sql` | 接受"新增沿用 `GY_` 以便与存量报表/对账脚本对齐",把必查清单口径改为"44 张业务表 + 30 张若依表" | 必查清单口径 | 已采纳(`docs/10` v1.2 已回填) |
| 差-7 | 表 2-3 写"`entry-apt` 固定路由不受角色影响"、"不允许 `/duty/**` 出现在设备可达接口清单" | 本文细化:①管理端写 `apt/**`,值班端只读 + 受控命令 `duty/**`;②`apt-duty` 不承载 `/apt/**`;③apt 入口用前端**静态路由**,`sys_menu` 仅作权限点载体(`02` §2.4) | 表 2-3 三条负面清单的落地需要可执行规则 | 将三条细化写入 `docs/10` 表 2-3 的"实现落点"列;若不接受页面静态路由,需改为全动态路由(成本更高) | 前端路由与菜单 | 已采纳(`docs/10` v1.2 已回填) |
| 差-8 | 表 7-4 分机资源状态机为 **7 态**(含 DRAFT/DISABLED/DELETED) | 落库拆成"1 个 `sync_state`(4 值 PENDING/SYNCED/FAILED/DRIFT)+ `enabled` + `deleted`"三个载体 | `apt_sip_endpoint` 列定义 | 语义等价,映射见 `03` 表 3-11;建议在 `docs/10` 表 7-4 增加一列"落库载体" | 状态机实现 | 已采纳(`docs/10` v1.2 已回填) |
## 5. 缺口与建议(G-01~G-07)
> G-01~G-07 详见 `03` 表 3-16,此处只给"不改库的兜底"与"改库的最小变更",供决策。
表 8-9 表结构缺口与两条路径
| # | 缺口 | 影响功能 | 不改库的兜底(本期) | 若改库的最小变更 | 状态 |
| --- | --- | --- | --- | --- | --- |
| G-01 | `apt_master_room` 无楼栋列(只有 `room_floor`) | 房间按楼栋筛选 | 只按楼层/房号/房态筛选,页面不出现"楼栋" | `alter table apt_master_room add building varchar(32)`(需平台侧同字段) | 已采纳(`docs/10` v1.2 已回填;走不改库兜底) |
| G-02 | `apt_master_person` 无 `local_enabled` | 人员在本地"停用叫乘" | 用 `duty_manual_action(action_type='STOP', target_type='JCXX')` 记意图,页面标注"仅本次有效" | `add local_enabled tinyint not null default 1` | 已采纳(`docs/10` v1.2 已回填;走不改库兜底) |
| G-03 | `apt_master_route` 无 `disable_reason`/`disabled_at`/`source` | 停用必须填原因(M1 验收项) | 原因写 `duty_operation_log.detail`(JSON),详情页从审计反查 | `add disable_reason varchar(512), disabled_at datetime(3), source varchar(16)` | 已采纳(`docs/10` v1.2 已回填;走不改库兜底) |
| G-04 | 快照表(`apt_master_route`/`apt_master_room`)无 `source` 区分平台/本地 | 本地应急新增会被下次全量覆盖 | 本期**不开放**本地新增(`01` 表 1-2 第 5 行) | 补 `source` 并把全量覆盖改为"只覆盖 `source='PLATFORM'` 的行" | 待确认 |
| G-05 | `gy_jcxx` 无 `state_updated_at`/`state_updated_by` | 状态迁移的"谁在何时改"只能查审计 | 以 `duty_operation_log` + `apt_call_event` 联合还原 | `add state_updated_at datetime(3), state_updated_by varchar(64)` | 待确认 |
| G-06 | `gy_sb.device_type`、`apt_device_command.cmd_type` 无 CHECK | 非法类型可入库 | 应用层枚举校验 + 常量类集中定义 | 加 `check` 约束(需先冻结类型字典) | 待确认 |
| G-07 | `apt_sip_endpoint.extension` 只有 `char(8)`,无"纯数字"CHECK | 非数字分机号可入库 | 应用层正则 `^\d{1,8}$` + 入库大写归一;`02` §3 校验清单登记 | `check (extension regexp '^[0-9]{1,8}$')`(MySQL 8.0 `regexp_like` 需生成列) | 待确认 |
**原则**:本期优先走"不改库的兜底",避免在试点前改结构;G-01/G-03 若 M1/M2 验收硬要求,再单独走一次结构变更评审。
## 6. 待确认清单(Q-01~Q-14)
表 8-10 需要拍板的问题(不回答即按"默认"执行)
| # | 问题 | 关联 | 不回答时的默认 | 决策角色 |
| --- | --- | --- | --- | --- |
| Q-01 | 排序规则是否接受统一 `utf8mb4_general_ci`(差-1) | 表 5-9 第 7 项 | 接受,大小写归一放应用层 | 架构 + DBA |
| Q-02 | 是否把 `READ-COMMITTED` 写入部署清单(差-2) | 表 5-6 | 写入 `06` §2 作为部署必做项 | 运维 |
| Q-03 | 是否补代码生成模块(差-3) | 表 2-2 | 不补 | 架构 |
| Q-04 | 表清单口径是否修订为"44 张业务表"(差-5/差-6) | 表 5-3、表 5-9 | 按 44 张,`docs/10` 待修订 | 架构 |
| Q-05 | 前端菜单走静态路由 + 权限点载体(差-7) | 表 2-3 | 走静态路由,`sys_menu` 只做权限点 | 前端 + 架构 |
| Q-06 | 分机状态机 7 态是否按"4 值 + 2 标志位"落库(差-8) | 表 7-4 | 按 `03` 表 3-11 的映射 | 架构 |
| Q-07 | 房间是否补楼栋列(G-01) | M2 验收 | 不补,页面去掉楼栋筛选 | 产品 |
| Q-08 | 交路停用原因是否入库(G-03) | M1 验收 | 入 `duty_operation_log.detail` | 产品 |
| Q-09 | 人员本地停用是否给独立列(G-02) | `04` 表 4-11 | 用 `duty_manual_action` 记录 | 产品 |
| Q-10 | 会议桥粒度(D11) | 表 8-3 | 单 `Conf-01` + 串行化(表 8-4) | 语音负责人 |
| Q-11 | 录音留存与导出(D15) | `06` §9 | 90 天 + 仅内网 + 导出审计 | 合规 + 业务 |
| Q-12 | 设备协议与固件是否支持 ACK/签名(M5 前置) | `05` 表 5-3 | 指令只入队,页面显示"待协议确认" | 现场 + 厂家 |
| Q-13 | 平台 20 个接口的联调环境与凭据由谁提供 | `09` §6 | 先用 Mock(`09` §7),不阻塞 M0~M4 | 平台负责人 |
| Q-14 | 是否接受"本期不做旧接口兼容适配层" | 表 10-1 旧系统兼容行 | 接受,兼容层放阶段五 | 架构 |
## 7. 决策 × 批次阻塞矩阵
表 8-11 哪些决策会卡住哪个批次(`■` 强阻塞,`□` 弱影响)
| 决策/缺口 | M0 | M1 | M2 | M3 | M4 | M5 | M6 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| D5 一进程一 `gyid` | ■ | | | | | | |
| D7 交路应急新增 | | □ | | | | | |
| D8 时间以谁为准 | | ■ | | | | | |
| D3 房间主数据归属 | | | ■ | | | | |
| G-01 楼栋 / G-03 停用原因 | | ■ | ■ | | | | |
| G-02 人员本地停用 | | | | | ■ | | |
| D11 会议桥粒度 | | | | □ | | | ■ |
| D12 号段规则 | | | | | | | ■ |
| D14 操作系统 | | | | | | ■ | ■ |
| D15 录音合规 | | | | | | | ■ |
| D16 高可用 | | | | | | | ■ |
| Q-13 平台联调环境 | ■ | ■ | | | | | |
**结论**:M0~M4 的阻塞项只有 D5/D8/D3 与 G-01/G-02/G-03,均可在"不改库 + 默认口径"下先做;D11/D12/D15/D16 的定稿窗口是 M6 之前(`docs/10` §11.3 第 5 条要求"阶段二冻结契约前定稿"),因此建议**并行启动语音三项决策,不等开发进度**。
+414
View File
@@ -0,0 +1,414 @@
# 09 平台接口约定(公寓端 ↔ 平台端)
> 追溯:`docs/10` §6.1 表 6-1(17 个跨端接口)、§6.2(上报信封与三时间戳)、§6.3 表 6-2(幂等/重试/对账/断网)、
> §6.4 表 6-3 第 23~25 项(语音域跨端)、§5.1 表 5-1(数据分层与同步方向)、§5.2 表 5-2(各场景一致性)、
> §3.3(公寓身份与 `gyid`)、§7.6 表 7-3(设备域上报与不上报)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(平台不回调公寓)。
> **契约状态:v2-draft(CR-02 扩展;原 v1-draft 的 20 个编号、方向、路径不变)**。字段级冻结在阶段二(`docs/10` 表 9-3),冻结前不写 Controller(`08` R-02);扩展口径见 §0 与 `docs/设计变更/03`。
> 本文只定义"公寓端如何消费/产生"这些接口,不替平台端定实现。
> **v2 修订(CR-02,2026-11-09)**:契约状态由 `v1-draft` 升 **`v2-draft`**(字段级仍在阶段二冻结);扩展口径以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §3、§4 为权威,见下文 §0。表 9-1 的 20 个编号、方向、路径一律不变。
## 0. v2 契约修订(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 9-y` 序号;§1 表 9-1、§2 通用约定与表 9-4 平台返回码继续有效。
表 C-1 三条契约口径改判
| 项 | 旧口径 | v2 口径 | 裁决 |
| --- | --- | --- | --- |
| 接口总数 | 20 个不新增;确需新增走 v2 契约、不在本期 | **编号 / 方向 / 路径全部不变**,新数据以 `payload` + `bizType` 扩展;平板 / 消费机属设备侧 `/open/v1/**`、值班台属端内,均不计入 20 | CR-C07 |
| 下行方向 | 一律"拉" | 不变(平板任务同样由设备拉取);#16 / #17 方向仍待定(P-02 / F-03) | CR-C08 |
| 契约版本 | `v1-draft` | `v2-draft`;`docs/平台端/05` 同版本 | CR-C14 |
表 C-2 新增 `bizType`(沿用 §6.2 上行信封,不新增接口)
| bizType | 承载接口 | 需求 |
| --- | --- | --- |
| `PERSON_REG` | #9 | A-02 |
| `CHECKIN` / `CHECKOUT` | #9 | A-03 |
| `TABLET_RESULT` | #9 | A-08 |
| `TRANSFER` | #8 / #9 | A-09 |
| `LEDGER_OFFLINE` | #10 | A-04 |
| `DEVICE_ALARM` | #12 | A-05 |
| `CLEAN_RECORD` / `STAFF_PERFORMANCE_DAILY` | #13 | A-12、A-13 |
| `REPORT_RETRY` | #14(心跳携带,不进 items 批量) | A-14 |
表 C-3 下行扩展字段(响应 `payload`,不改变请求形态)
| 接口 | 扩展 |
| --- | --- |
| #1 | `offlineQuota`、`features.tabletCall` |
| #2 | `personRegVerdicts[]` |
| #3 | `transferPolicy` |
| #5 | `building` / `orientation` / `sipExtension` |
| #7 | `callMode` / `dispatchSeq` / `transferOrders[]` |
| #8 / #9 / #10 / #11 / #12 / #13 / #14 / #23 / #24 | 见 CR-02 表 3-7、表 3-20 |
表 C-4 Asterisk 补遗的跨端口径(`docs/公寓端/00` v1.1 / CR-02 §10;**不新增编号、不新增 `bizType`**)
| 接口 | 扩展 | 约束 |
| --- | --- | --- |
| #11 | `offlineList[]` 含房间分机、`offlineCount`、`regRate` | 只报异常明细;注册流水明细不上报 |
| #12 | `DEVICE_ALARM` 增 `alarmType=OFFLINE` 的 `endpointId`、`extension`、`roomNo`、`jcid`、`offlineSince` | `alarmSource=DEVICE` 按来源分组不变;`alarm_event_key` 去重 |
| #13 | 归档摘要增设备告警计数与分机离线计数 | 告警记录在线 180 天后按月归档,索引长期保留 |
| #24 | `regRate` 取 `apt_sip_endpoint.register_status` 与 `apt_sip_reg_status` 最新态 | 指标口径固定"近 1 小时"(表 9-9) |
> 平台只汇总,不回调、不下发处置指令;分机离线告警的处置由值班台经 `POST /apt/device/alarms/{id}/handle`(N-17)本地闭环。**录音文件、业务过程抓拍与签到现场图一律不上传平台**;人员主数据的照片与特征值按 CR-C24 / `docs/公寓端/10` §1.1 允许平台 ↔ 公寓双向同步,媒体本体走短期签名直传、库内只存引用与校验和(CR-02 §12)。
表 C-5 两类叫乘方式的上报口径(**流程见 `docs/公寓端/05` §9.4、§9.5**;不新增编号、不新增 `bizType`)
| 接口 | SIP 语音叫乘 | 平板叫乘 | 共同约束 |
| --- | --- | --- | --- |
| #8 任务回执 | `computedCallTime` | `dispatchSeq`、复算 `jcsj` | 回执失败不影响执行(表 9-5) |
| #9 值班结果 | `DUTY_RESULT`(`ANSWERED` / `CALL_FAILED`,源 = AMI 事件) | `TABLET_RESULT`(源 = 平板 N-04 上报) | `payload` 一律带 `callMode=SIP/TABLET`;终态才报 `FINISHED`;`idempotencyKey` 沿用 `hash(gyid+bizType+bizId+bizVersion)` |
| #12 报警 | 无应答 `NO_ANSWER` / 异常中断 | 未刷脸 / 超时 `TIMEOUT` | 终态矩阵见 `docs/公寓端/05` 表 5-20 |
| #24 语音摘要 | 发(按 `callMode` 分组,含 `regRate` / `callSuccessRate` / `recordingMissing`) | **不发**(平板不经 Asterisk / AMI) | 每小时 1 次,只报摘要指标 |
| #13 归档 | 归档会话与事件统计 | 归档平板任务与结果统计 | 只报数量与区间,不传明细 |
| #14 心跳 | `serverTime` 用于平板时钟校准 | 同左(`clockSkewMs`) | 60s 一次,平台回 `serverTime` |
> 结果反馈路径分四层:值班台(`/ws/duty`)、平台(上表)、本地留痕、异常兜底;断网一律先落本地库 + outbox,恢复后按原幂等键续报(CR-02 F-E)。平板**结果**不上报抓拍帧、人脸原图与录音文件;比对所需特征值按 `docs/公寓端/10` 的设备权限范围下发,人员主数据人脸按 CR-C24 允许跨端同步。
payload 字段字典、枚举字典(`callMode` / `faceVerifyState` / `alarmSource` / `offlineScope` / `reportState` 等)、错误码统一与幂等定序见 CR-02 表 3-10~3-22。表 9-4 的 6 类平台返回码与重试判定继续适用于所有新增 `bizType`。
表 C-6 人脸数据下发与注册链路的跨端扩展(CR-02 §12;**沿用既有编号,不新增、不重排**)
| # | 扩展 | 方向 | 用途 |
| --- | --- | --- | --- |
| 1 | `facePolicy`(范围规则、缓存上限、擦除窗口、`LONG_TERM` 白名单、算法白名单) | 下行 | 公寓端按策略执行下发与清理 |
| 2 | 人员增量 `payload` 增 `photoRef` / `photoChecksum` / `featureRef` / `featureChecksum` / `featureAlgo` / `featureVersion` / `personCategory` / `mediaUrls[]`;`personRegVerdicts[]` 改为**平台同步回执**(`ACCEPTED` / `REJECTED` + `platformVersion`) | 下行 | AF-02 下载入库、注册结果对齐 |
| 9 | `PERSON_REG` payload 增 `ryid`、`regState=CONFIRMED` 与人员主数据人脸引用 / 校验和 / 算法 / 版本;媒体本体走短期签名直传 | 上行 | **值班员注册成功后**上报(申请阶段不上报) |
| 11 | 设备摘要增 `faceCacheMismatchCount`、`unauthorizedFaceQueryCount` | 上行 | 越权与不一致观测 |
| 14 | 心跳增 `faceSyncWatermark` | 上行 | 版本与积压 |
> 注册链路按 CR-C25:签到终端申请 → 值班员确认注册 → 入 `apt_master_person` → **分别** #9 上传平台与 `FACE_ISSUE` 下发公寓内设备;两条路径独立幂等、互不阻塞,平台回执不是注册生效前置。人员主数据人脸按 CR-C24 允许跨端;抓拍、录音、签到现场图仍不出公寓。完整接口 N-25~N-37 见 `docs/公寓端/10` §13、`docs/公寓端/04` §0 表 C-6。
表 C-7 人员入住作业流程的跨端扩展(CR-02 §13;**沿用既有编号,不新增、不重排**)
| # | 扩展 | 方向 | 用途 |
| --- | --- | --- | --- |
| #9 | `CHECKIN` / `CHECKOUT` / `ROOM_SWAP` payload 增 `applyId`、`origin`(`CHECKIN_ROUTE` / `CHECKIN_PENDING`)、`cardNo` / `workNo`、`jlid` / `cc` / `jch` / `kcsj`、`ruleVersion` / `bizVersion` | 上行 | 入住事实提报;`bizId = jcid + 事件序号`,`idempotencyKey = hash(gyid + CHECKIN + jcid + eventSeq + bizVersion)` |
| #6 | 乘务计划响应 `plans[]{jlid, cc, jch, kcsj, ruleVersion, bizVersion}`、`cursor` | 下行(拉) | 平台运安快照,`platform.plan` 唯一入口 |
| #8 | 回执增 `applyId`、`cardNo` / `workNo`、`computedCallTime`、`derivedRuleVersion` | 上行 | **平台匹配的输入**,联网时下一周期必发 |
| #7 | 响应增 `matchVerdict[]{taskId, applyId, verdict, matchedPlanKey, kcsj, jcfs, matchState, rejectReason, matchedAt}`;`changes[]` 增 `applyId` / `origin` / `baseCallTime` | 下行(拉) | 任务变更 + **匹配结论唯一合规载体**(与 `replayHint` 同级,R-05) |
> 平台零回调;`match_state` 沿用正式五值 `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED`(`LOCAL_ONLY` 属 `offlineScope`);`UNMATCHED` / `LOCKED` 保留公寓上报要素不清空;`jcsj` 恒由公寓按 `apt_master_rule` 复算,`PL_TASK.jcsj` 只是基线(CR-C06)。完整请求 / 响应字段见 `docs/公寓端/11` §7、§8。
表 C-8 消费、充值与菜品管理的跨端扩展(CR-02 §14;**沿用既有编号,不新增、不重排**)
| # | 扩展 | 方向 | 用途 |
| --- | --- | --- | --- |
| #1 | 响应增 `features.remoteOrdering`(默认 `false`);`offlineQuota` 沿用 | 下行(拉) | 远程点菜特性开关;断网额度缺省全 `0` 即不开放(Q-21 / Q-42) |
| #4 | 响应增 `recipes[]{recipeId, recipeVersion, mealPeriod, effectiveFrom/To, items[]{mealId, name, price, category, sortNo, enabled, imageRef}}` + `recipeWatermark` | 下行(拉) | 阶段性食谱增量与水位;公寓只读,缓存落消费终端本地 |
| #10 | `LEDGER_OFFLINE` payload 增 `allowanceAmount` / `cashAmount`、`allowanceBefore/After` / `cashBefore/After`、`recipeId` / `recipeVersion` / `items[]`、`rechargeChannel` / `qrCodeId` / `confirmedBy`;`amount` 扣减负数、充值正数 | 上行 | 消费与充值流水同步;一笔消费只一条 `flowId`,`bizId = flowId` |
| #16 | 日对账结论回写 `RECONCILED`(本期公寓主动调用形态,P-02) | 补偿 | `offline_scope=RECONCILED` + `local_balance_version` 更新;差异 `DIFF` + `PF-5003` 阻断调账 |
> 平台零回调;`idempotencyKey = hash(gyid + bizType + flowId + bizVersion)`,归日以平台 `serverReceivedAt` 为准;`platform-ledger` 是 `GY_YKT_REMAINDER` / `PL_LEDGER_FLOW` 唯一写入口,公寓端零写权限。完整请求 / 响应字段见 `docs/公寓端/12` 表 12-26~12-28。
表 C-9 三端一致性与统计数据源的跨端扩展(CR-02 §15;**沿用既有编号,不新增、不重排**)
| # | 扩展 | 方向 | 用途 |
| --- | --- | --- | --- |
| #13 | `ARCHIVE` 摘要 payload 增 `satisfaction{good,neutral,bad,total}`、`roomOccupancy{roomAvailable,roomNights}` **分组计数** | 上行 | 供平台统计评价情况与房间利用周转;只接计数不接明细,摘要类按批覆盖、无版本语义(表 3-13) |
| #9 | `TABLET_RESULT` / `PERSON_REG` / `CHECKIN` 沿用 §11 / §12 / §13 字段 | 上行 | 叫乘人数 / 入住人数的日报数据源 |
| #10 | `LEDGER_OFFLINE` 沿用 §14 `allowanceAmount` / `cashAmount` 拆分 | 上行 | 补贴发放 / 补贴消费 / 现金充值(**计收入**)三类统计数据源 |
| #12 | `DEVICE_ALARM` 按 `alarmSource` 分组 | 上行 | 报警情况统计 |
| #4 | `recipes[]` + `recipeWatermark` 沿用 §14 | 下行(拉) | 食谱版本,不参与统计 |
> **三端状态映射与数据同步机制以 CR-02 §15 表 3-66 为唯一权威**;平台统计 / 报表 / 排名能力全部落平台管理端 `/platform/v1/admin/stat/**`(`S-01`~`S-07`,见 `docs/平台端/10` 表 10-14),**不计入跨端 20 编号、不产生跨端副作用、不反向下发公寓**,统计结果不回写本端任何业务状态。未上报的评价 / 房间周转记 `NO_DATA`,**不推断不补零**(CR-C39);20 个编号、方向、路径与 `v2-draft` 形态不变。
## 1. 范围与总表
接口总数 **20 = 表 6-1 的 17 个 + 表 6-3 的 3 个(#23~#25)**,编号沿用 `docs/10`,不重排(`docs/10` §6.4 末段)。
表 9-1 二十个跨端接口与公寓端落表
| # | 方向 | 接口 | 用途 | 公寓端主责模块 | 落表 | `docs/10` 出处 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 下行 | `GET /platform/v1/master/apartments/{gyId}` | 公寓档案与能力开关 | `apt-master-client` | `apt_master_apartment`、`apt_master_version` | 表 6-1 #1 |
| 2 | 下行 | `GET /platform/v1/master/persons?gyId&cursor&version` | 人员增量(含照片版本) | `apt-master-client` | `apt_master_person`、`apt_master_version` | 表 6-1 #2 |
| 3 | 下行 | `GET /platform/v1/master/routes?gyId&from&to` | 交路模板与叫乘时刻 | `apt-master-client` | `apt_master_route`、`apt_master_version` | 表 6-1 #3 |
| 4 | 下行 | `GET /platform/v1/master/rules?gyId` | 时间规则/节假日/餐补标准 | `apt-master-client` | `apt_master_rule`、`apt_holiday`、`apt_master_version` | 表 6-1 #4 |
| 5 | 下行 | `GET /platform/v1/master/rooms?gyId&version` | 房间基线(受 D3 约束) | `apt-master-client` | `apt_master_room`、`apt_master_version` | 表 6-1 #5 |
| 6 | 下行 | `GET /platform/v1/plans/duty?gyId&from&to` | 乘务计划(替代本地抓运安) | `apt-master-client` → `apt-dispatch` | `gy_syn_jcxx`、`gy_syn_jcxx_status`、`gy_jcxx` | 表 6-1 #6 |
| 7 | 下行 | `GET /platform/v1/tasks/changes?gyId&cursor` | 任务变更/取消增量 | `apt-dispatch` | `gy_syn_jcxx`、`gy_syn_jcxx_status` | 表 6-1 #7 |
| 8 | 上行 | `POST /platform/v1/ack/tasks` | 任务接收回执(含复算结果) | `apt-dispatch` | `apt_report_outbox`(`biz_type=TASK_ACK`,见 G-08) | 表 6-1 #8 |
| 9 | 上行 | `POST /platform/v1/report/duty-results` | 叫乘/入寓/离寓执行结果 | `apt-report` | `apt_report_outbox`、`gy_jcxx`、`gygl_ly_event` | 表 6-1 #9 |
| 10 | 上行 | `POST /platform/v1/report/ledger` | 账务流水(消费/充值/补助) | `apt-report` | `apt_report_outbox`、`gy_ykt_consume`/`gy_ykt_xjczjl`/`gy_ykt_allowance` | 表 6-1 #10 |
| 11 | 上行 | `POST /platform/v1/report/device-status` | 设备在线率/版本/故障摘要 | `apt-report` | `apt_report_outbox`、`gy_sb`、`gy_sb_xt` | 表 6-1 #11 |
| 12 | 上行 | `POST /platform/v1/report/alarms` | 叫乘/离寓报警摘要 | `apt-report` | `apt_report_outbox`、`gy_bjxx` | 表 6-1 #12 |
| 13 | 上行 | `POST /platform/v1/report/archive` | 归档摘要与数据量 | `apt-report` | `apt_report_outbox`、`gy_jcxx_his`、`apt_call_event_his` | 表 6-1 #13 |
| 14 | 上行 | `POST /platform/v1/heartbeat` | 存活、版本、水位、时钟偏差 | `apt-report` | `apt_report_watermark`、`apt_ops_selfcheck` | 表 6-1 #14 |
| 15 | 查询 | `GET /platform/v1/status/{gyId}` | 平台侧查看公寓健康度与积压 | —(**公寓端不实现**) | — | 表 6-1 #15 |
| 16 | 补偿 | `POST /platform/v1/reconcile/run?gyId&date` | 触发日对账 | 方向待定(F-03) | `apt_report_watermark` | 表 6-1 #16 |
| 17 | 补偿 | `POST /platform/v1/replay` | 指定幂等键/区间重放 | 方向待定(F-03) | `apt_report_outbox` | 表 6-1 #17 |
| 23 | 下行 | `GET /open/v1/voice/config?gyId&configType` | 语音配置(号段规则/TTS 模板/留存策略) | `apt-voice` | `apt_ast_config_version`(`source=PLATFORM`) | 表 6-3 #23 |
| 24 | 上行 | `POST /open/v1/voice/summary` | 语音质量摘要 | `apt-voice` → `apt-report` | `apt_report_outbox`(`biz_type=VOICE_SUMMARY`) | 表 6-3 #24 |
| 25 | 查询 | `GET /platform/v1/voice/endpoints?gyId` | 平台侧汇总查看(只读) | —(**公寓端不实现**) | — | 表 6-3 #25 |
**三条方向纪律**:
1. **平台不回调公寓**(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行、§6.3 断网行):公寓端**只发起出站调用**,不监听平台请求。因此 #15/#25 是平台侧接口、公寓端不实现;#16/#17 的"平台→公寓"方向与本节冲突,列入 F-03。
2. **下行一律"拉"**:八个下行接口(#1~#7、#23)由公寓端定时拉取;平台不主动推、不回调公寓(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行),只有 F-03(平台端 P-02)改判后才可能引入"平台发起"的形态。
3. **跨端不带媒体**(表 6-3 设计要点第 5 条):`/open/**` 只有配置与摘要,不上传录音、不代理 AMI。
## 2. 通用约定
表 9-2 传输与认证
| 项 | 约定 | 依据 |
| --- | --- | --- |
| 协议 | HTTPS,单向出站 443;公寓端为客户端 | `06` 表 6-4 |
| 认证 | mTLS 或双向令牌(平台未就绪,本期走占位实现) | `docs/10` 表 6-3 分组说明 |
| 公寓身份 | 请求头 `X-Apt-GyId` + 路径 `gyId` 双重携带,两者不一致直接拒绝 | §3.3(不用 IP 推断公寓) |
| 版本 | 请求头 `X-Apt-App-Version`;响应头 `X-Platform-Api-Version` | 便于灰度与兼容 |
| 幂等 | 上行请求头 `Idempotency-Key`(= 信封 `items[].idempotencyKey`,批量时用 `batchId`) | 表 6-2 幂等行 |
| 超时 | 连接 3s / 读 10s;重试用指数退避 | 表 6-2 重试行 |
表 9-3 数据表示
| 类别 | 约定 | 理由 |
| --- | --- | --- |
| 时间 | ISO-8601 带偏移(`2026-11-07T09:58:12.123+08:00`);库内 `datetime(3)` | 表 5-4 时间列口径 + §6.2 三时间戳 |
| 金额 | 字符串十进制,两位小数(`"12.00"`),不用浮点 | 表 5-9 第 5 项 `decimal(12,2)` |
| 枚举 | 大写下划线字符串;不接受数字码 | 表 5-4 枚举不用 MySQL `ENUM` |
| 主键 | 业务 ID 用 ULID/UUID 字符串,**不用自增** | 表 5-4 自增行:"跨端幂等键不能用自增" |
| 分页 | 下行用 `cursor`(不透明串);上行批量单批 ≤ 500 条且 ≤ 1MB | §6.2 协议要点第 4 条 |
| 空值 | `null` 与缺省等价;不用空串表达"无值" | 表 5-6 空串语义行 |
| 字符集 | UTF-8;标识类(分机号/房间号/工号)**入库与传输统一大写** | 表 5-4 字符集行的落地 |
表 9-4 平台返回码与重试判定(本端期望,待 F-05 确认)
| 平台码 | 含义 | 本端动作 | outbox 状态 |
| --- | --- | --- | --- |
| 2xx | 成功 | 记 `sent_at` + 回执 `serverReceivedAt` | `SENT` |
| `PL-IDEMPOTENT-REPLAY` | 重复请求,返回原结果 | 视为成功(同一 `idempotencyKey`) | `SENT` |
| `PL-VERSION-STALE` | `bizVersion` 低于已处理版本 | 丢弃并记日志(表 6-2 顺序行) | `DEAD`(不再重试) |
| `PL-GYID-UNKNOWN` | 公寓未注册 | 停止派发 + 横幅告警(配置错误,重试无意义) | `FAILED`(人工处理) |
| `PL-4xx`(其余) | 报文不合法 | 不重试,记 `last_error` | `DEAD` |
| `PL-5xx` / 超时 / 网络错 | 平台故障 | 按 1s/5s/30s/2min/10min 退避 + 抖动(上限 1 小时) | `FAILED` → 连续 N 次 `DEAD` |
**与本地错误码的分工**:本地 HTTP 接口的错误码见 `04` 表 4-4(`APT_*` 常量);跨端只用上表的平台码,两者**不混用**,避免"公寓内部错误被当成平台拒绝"。
表 9-5 上行信封字段 → `apt_report_outbox` 列映射(照 §6.2 信封逐字段落地)
| 信封字段 | 类型 | 来源/落点 | 说明 |
| --- | --- | --- | --- |
| `gyId` | string | `apt_report_outbox.gyid` | 与环境变量 `APT_GY_ID` 必须一致 |
| `appVersion` | string | 不落库(启动信息) | 由构建号注入 |
| `batchId` | string(UUID) | 不落库(日志留存) | 每次派发生成,一次批量一个 |
| `sentAt` | string | 不落库(= 请求发出时间) | 与 `serverReceivedAt` 配对算时延 |
| `watermark` | long | `apt_report_outbox.watermark` | 取本批**最大**序号;`uk_outbox_watermark` 保证单调 |
| `items[].idempotencyKey` | string | `idempotency_key` | `hash(gyid+bizType+bizId+bizVersion)` |
| `items[].bizType` | string | `biz_type` | 取值见 `04` 表 4-17 与 G-08 |
| `items[].bizId` | string | `biz_id` | 业务对象 ID(非自增主键) |
| `items[].bizVersion` | long | `biz_version` | 更正上报时 +1 |
| `items[].occurredAt` | string | `occurred_at` | 现场发生时间(业务表时间列) |
| `items[].receivedAt` | string | `received_at` | 公寓端入库时间 |
| `items[].payload` | object | `payload`(json) | 结构见 §4 各接口 |
| `serverReceivedAt`(回执带回) | string | `platform_recv_at` | 三时间戳的第三个 |
## 3. 下行接口(8 个)
表 9-6 下行请求约定
| # | 路径 | 查询参数 | 游标/版本 | 调用时机 | 失败降级 |
| --- | --- | --- | --- | --- | --- |
| 1 | `/platform/v1/master/apartments/{gyId}` | — | `version` 与本地 `apt_master_version` 比对 | 启动 + 每日 1 次 | 用最后快照;`features` 缺省按"全关"处理并告警 |
| 2 | `/platform/v1/master/persons` | `gyId`、`cursor`、`version` | `cursor` 分页;`version` 见 G-09 | 每日 1 次 + 值班前增量 | 新人员不可叫乘,已有记录不受影响(表 5-2) |
| 3 | `/platform/v1/master/routes` | `gyId`、`from`、`to` | 按日期区间全量 | 每日 1 次 + 计划前 | 已生成任务照常执行,新交路不生效 |
| 4 | `/platform/v1/master/rules` | `gyId` | 全量 | 每日 1 次 | **保持最后已知版本**,仅告警 |
| 5 | `/platform/v1/master/rooms` | `gyId`、`version` | 版本比对 | 每日 1 次 | 新房间不生效,存量不受影响 |
| 6 | `/platform/v1/plans/duty` | `gyId`、`from`、`to` | 按日期区间 | 每 30 分钟 + 计划前 | 用本地快照;超 2 小时未更新则横幅提示 |
| 7 | `/platform/v1/tasks/changes` | `gyId`、`cursor` | `cursor` 增量 | 每 5 分钟 | 停在上一游标,恢复后补拉 |
| 23 | `/open/v1/voice/config` | `gyId`、`configType` | `versionNo` + `checksum` | 启动 + 每日 1 次 | 保持最后已知配置,`apply_result=FAILED` 留痕 |
**共性规则**:每次下行写一条 `apt_master_version`(`master_type`、`version_no`、`checksum`、`item_count`、`apply_result`),失败也要写(`apply_result=FAILED` + `fail_reason`),否则"为什么没生效"无从查起(`05` 图 5-5)。
表 9-7 主数据字段映射(#1~#5、#23)
| # | 平台字段 | 本地列 | 说明 |
| --- | --- | --- | --- |
| 1 | `gyId` / `name` / `features[]` / `version` | `apt_master_apartment.gyid` / `gymc` / `features`(json) / `source_version` | `features` 存原样 JSON,不拆列(新增开关不改表) |
| 1 | `shortName`、`address`、`contact*` | `short_name`、`address`、`contact_name`、`contact_phone` | 缺省写 `null`,不写空串 |
| 2 | `ryid` / `cardId` / `deptId` | `apt_master_person.ryid` / `card_id` / `depotid` | `ryid` 为业务键,参与 `UK(gyid, ryid)` |
| 2 | `photoVersion` / `photoUrl` | `photo_version` / `photo_url` | 照片本体不入库(表 5-4 长文本行) |
| 2 | `deleted` | `deleted` | 平台侧删除标记,**不物理删本地行** |
| 2 | `xm`、`dw`、`gwmc`、`gzmcnew`、`sjh`、`sfzh`、`xb`、`mz`、`zm`、`gzzh` | 同名列 | 姓名/手机/身份证出参按权限脱敏 |
| 3 | `jlid` / `cc` / `kcsj` / `jcsj` / `lysj` | `apt_master_route.jlid` / `cc` / `kcsj` / `jcsj` / `lysj` | 时刻为模板 `time`,按基准日复算成 `datetime(3)` 才写 `gy_jcxx` |
| 3 | `jch`、`qd`、`cclx`、`ddcc`、`ddsj`、`rysj`、`sfpc`、`bzje`、`bz` | 同名列 | `bzje` 为 `decimal(12,2)` |
| 4 | `zjjcsjgz` | `apt_master_rule(rule_type='JCSJ_OFFSET', rule_key='ZJ')` | 手工/自助叫乘提前量(映射待 F-06 确认) |
| 4 | `kjjcsjgz` | `apt_master_rule(rule_type='JCSJ_OFFSET', rule_key='KJ')` | 跨叫提前量(同上) |
| 4 | `lysjgz` | `apt_master_rule(rule_type='LYSJ_OFFSET', rule_key='LY')` | 离寓提前量(同上) |
| 4 | `holidays[]` | `apt_holiday.holiday_date` / `holiday_type` / `name` | `UK(gyid, holiday_date)`,调休工作日写 `WORKDAY` |
| 5 | `roomNo` / `bedNumbers` | `apt_master_room.room_no` / `bed_numbers` | 其余物理属性(楼层/朝向/床位)本期同样只读(D3 默认口径) |
| 23 | `versionNo` / `checksum` / `payload` | `apt_ast_config_version.version_no` / `checksum` / `payload` | `config_type` 由请求参数带入;`source='PLATFORM'` |
| 23 | `payload` 内号段规则 | `apt_sip_endpoint` 生成校验(不直接写分机) | 平台只给规则,分机台账仍由本地维护(D12) |
表 9-8 计划与任务字段映射(#6/#7)
| # | 平台字段 | 本地列 | 说明 |
| --- | --- | --- | --- |
| 6 | `planId` | `gy_syn_jcxx.plan_key` | 幂等键(`UK(gyid, plan_key)`) |
| 6 | `workno` | `ryid` | 与 `apt_master_person.ryid` 对齐,匹配不到则挂"待匹配" |
| 6 | `trainNo` | `cc` | 车次 |
| 6 | `depTime` | `kcsj` | 开车时刻(复算 `jcsj` 的基准) |
| 6 | `pickupMode` | `jcfs` | 接车方式 |
| 6 | (请求 `from`/`to` 与 `depTime` 计算) | `plan_date` | 按 `kcsj` 归一日 |
| 6 | `source_version`(平台计划版本) | `source_version` | 用于"版本过期拒绝任务"(表 5-2 第 2 行) |
| 6 | 复算产物 | `gy_jcxx.jcsj` / `fjsj` / `jcbjsj` | 由 `apt_master_rule` + `apt_holiday` 计算,**不取平台值**(D8) |
| 7 | `taskId` | `gy_syn_jcxx.plan_key` | 与 #6 同一业务键空间 |
| 7 | `op` | 不落列(行为语义) | 见下表 9-9 |
| 7 | `version` | `source_version` | 旧版本丢弃并记日志 |
| 7 | `cursor` | `gy_syn_jcxx_status.watermark` | 增量游标,落单行 |
表 9-9 任务变更 `op` 语义
| `op` | 本地行为 | 幂等 | 边界 |
| --- | --- | --- | --- |
| `UPSERT` | 按 `plan_key` upsert `gy_syn_jcxx`;若已有 `gy_jcxx` 且 `ryzt=0`,重算时刻;已开始执行(`ryzt>=1`)**只更新计划快照,不改执行状态** | `plan_key` + `source_version` | 不覆盖终态记录 |
| `CANCEL` | 若 `gy_jcxx.ryzt=0` → 经 `apt.duty.state` 置 `stoped='1'` 并记原因;若已开始 → 只标记计划取消,执行照常并回执说明 | 同上 | 已离寓记录不改 |
| 其它 | 拒绝并告警(契约外的 `op` 不猜语义) | — | — |
## 4. 上行接口(8 个)
表 9-10 上行接口清单
| # | 路径 | `biz_type` | 数据来源 | 触发时机 | 回执处理 |
| --- | --- | --- | --- | --- | --- |
| 8 | `/platform/v1/ack/tasks` | `TASK_ACK` | 本次下行 #6/#7 的复算结果 | 收到计划/变更后立即 | 记 `accepted`;被拒则回滚本地任务并告警 |
| 9 | `/platform/v1/report/duty-results` | `DUTY_RESULT` | `gy_jcxx` 状态迁移 + `gygl_ly_event` | 状态变更后 ≤ 10s,或批量每 30s | 按条回报;失败条目重试 |
| 10 | `/platform/v1/report/ledger` | `LEDGER` | `gy_ykt_consume` / `gy_ykt_xjczjl` / `gy_ykt_allowance` | 流水落库后(`report_state=0`) | 成功后本地置 `report_state=1` |
| 11 | `/platform/v1/report/device-status` | `DEVICE_STATUS` | `gy_sb` + `gy_sb_xt` 按小时聚合 | 每小时 1 次 | 只报摘要,不报逐条心跳(表 7-3) |
| 12 | `/platform/v1/report/alarms` | `ALARM` | `gy_bjxx`(未上报的报警) | 报警产生后 ≤ 30s | `handled_result` 变化时用新 `bizVersion` 更正 |
| 13 | `/platform/v1/report/archive` | `ARCHIVE` | `gy_jcxx_his`、`apt_call_event_his` 的归档统计 | 归档任务完成后(每日) | 只报数量与区间,不传明细 |
| 14 | `/platform/v1/heartbeat` | —(单条,不走 items 批量;幂等按 `(gyId, sentAt)` 去重,不用 `hash` 规则) | `apt_report_watermark`、`apt_ops_selfcheck`、AMI 会话 | 每 60s | 平台回 `serverTime` → 本地算 `clockSkewMs` |
| 24 | `/open/v1/voice/summary` | `VOICE_SUMMARY` | `apt_call_session`、`apt_sip_endpoint`、`apt_recording`、`apt_ast_ami_session` | 每小时 1 次 | 只报摘要指标 |
表 9-11 payload 字段 → 来源列
| # | payload 字段 | 来源表.列 | 说明 |
| --- | --- | --- | --- |
| 9 | `events[].eventId` | `gygl_ly_event.event_id` / 由状态迁移生成(`jcid+ryzt+发生时间` 哈希) | 事件级幂等键 |
| 9 | `events[].type` | 由 `gy_jcxx.ryzt` 映射(`CALL_START`/`ANSWERED`/`LEAVE`/`ALARM`…) | 不搬 `ryzt` 数字码,跨端用语义值 |
| 9 | `events[].jcid`、`events[].ryid` | `gy_jcxx.jcid`、`gy_jcxx.ryid` | — |
| 9 | `events[].occurredAt` | `gy_jcxx.jcsj_s`/`hjxysj`/`gygl_ly_event.occurred_at` | 精确到毫秒 |
| 9 | `events[].status` | `gy_jcxx.ryzt` + `finish_jc` + `finish_ly` 组合 | 终态才上报 `FINISHED` |
| 10 | `flowId` | `gy_ykt_consume.tid` / `gy_ykt_xjczjl.flow_id` / `gy_ykt_allowance.allowance_id` | 三类流水各自业务键 |
| 10 | `type` | `CONSUME` / `RECHARGE` / `ALLOWANCE` | 对应三张来源表 |
| 10 | `ryid`、`amount`、`balanceAfter`、`deviceCode` | `ryid`、`xfje`/`czje`/`cbje`、`xjye`/`balance_after`、`device_code` | 金额传字符串 |
| 10 | 上报成功标记 | `report_state` 置 `1` | 三张表均有该列 |
| 11 | `onlineCount`、`offlineList[]`、`versions{}` | `gy_sb.online_state`、`gy_sb.device_code`、`gy_sb.version` | 只报异常设备明细 |
| 12 | `alarmId`、`type`、`jcid`、`level` | `gy_bjxx.id`(+`biz_id`)、`alarm_type`、`biz_id`、`alarm_level` | `alarmId` 用业务 ID 不用自增 |
| 13 | `period`、`archivedCount`、`from`、`to` | `gy_jcxx_his`(`biz_version` 已具备)、`apt_call_event_his.archived_at` | — |
| 14 | `outboxWatermark`、`clockSkewMs`、`appVersion` | `apt_report_watermark.current_watermark`、`gy_sb_xt.clock_skew_ms`(设备侧)/AMI 会话、构建号 | 平台据此判积压与时钟 |
| 24 | `regRate`、`callSuccessRate`、`recordingMissing`、`amiReconnects` | `apt_sip_endpoint.register_status`、`apt_call_session.state`、`apt_recording.state='MISSING'`、`apt_ast_ami_session.reconnect_count` | 指标口径固定为"近 1 小时" |
表 9-12 #8 回执字段与复算口径
| 字段 | 取值来源 | 说明 |
| --- | --- | --- |
| `taskId` | `gy_syn_jcxx.plan_key` | —— |
| `accepted` | `true` 除非复算失败/版本过期 | 版本过期 → `false` + 触发全量重拉(表 5-2) |
| `computedCallTime` | `gy_jcxx.jcsj`(本地复算值) | 平台据此比对差异(D8) |
| `ruleVersion` | 本次使用的 `apt_master_rule.source_version` | 便于平台定位"用了哪版规则" |
| (可选)`diffMs` | `computedCallTime - 平台 jcsj` | 超阈值(建议 60s)本地同时告警 |
## 5. 查询与补偿(4 个)
表 9-13 查询与补偿接口的归属
| # | 接口 | 谁发起 | 公寓端实现 | 说明 |
| --- | --- | --- | --- | --- |
| 15 | `GET /platform/v1/status/{gyId}` | 平台 | **否** | 平台用 #14 心跳与自身库作答,不需要公寓端参与 |
| 16 | `POST /platform/v1/reconcile/run` | 平台(或运维) | **方向待定(F-03)** | 与"平台不回调公寓"冲突;本期只做公寓端本地对账入口(`04` 表 4-17) |
| 17 | `POST /platform/v1/replay` | 平台 | **方向待定(F-03)** | 本期只做"按 outbox 序号重发"的本地接口 |
| 25 | `GET /platform/v1/voice/endpoints` | 平台 | **否** | 平台汇总 #24 摘要即可 |
**本地对账与重发的落地接口**(`04` 表 4-17 已列):`GET /apt/report/outbox`、`POST /apt/report/outbox/{id}/resend`、`POST /apt/report/reconcile/run`、`GET /apt/report/watermark`。跨端的 #16/#17 只有在平台提供"公寓可主动调用"的形态时才接,否则一律走本地入口 + 心跳说明。
## 6. 可靠性的落地
表 9-14 表 6-2 六个机制 → 本端实现点
| 机制(表 6-2) | 本端实现 |
| --- | --- |
| 幂等 | `uk_outbox_idem(gyid, idempotency_key)`;重复入队直接忽略 |
| 重试 | `retry_count` + `next_retry_at`(1s/5s/30s/2min/10min + 抖动,上限 1 小时);连续 10 次 → `DEAD` + 告警 |
| 顺序 | 派发按 `watermark asc`;同一 `biz_id` 只派发最大 `biz_version`,旧版本置 `DEAD` |
| 对账 | 每日 `APT_RECONCILE_CRON` 之外再跑一次上报对账,三维见下表 |
| 补偿 | 本地"按 outbox 序号重发";平台侧重放待 F-03 |
| 断网 | outbox 落库(非内存);恢复后按 `watermark` 续传,不丢不乱 |
| 平台不可用 | 下行用快照;`APT_REPORT_DISPATCH_ENABLED=false` 时只累积不派发(`06` 表 6-3) |
表 9-15 对账三维与本端取值
| 维度 | 本端取值 | 平台侧对应 | 差异处理 |
| --- | --- | --- | --- |
| `count` | `gy_jcxx` 当日迁移次数 / `gy_ykt_*` 当日笔数 | 平台已接收条数 | 差集列入"待人工处理" |
| `sum(amount)` | `gy_ykt_consume.xfje`、`gy_ykt_xjczjl.czje` 当日合计 | 平台账务合计 | 金额差异为 P0,当日必须结清 |
| `max(watermark)` | `apt_report_watermark.current_watermark` vs `acked_watermark` | 平台已确认水位 | 长期不等 → 判定漏报/卡死 |
## 7. 平台未就绪时的 Mock 与联调
表 9-16 Mock 策略(保证 M0~M4 不被平台阻塞,Q-13 默认口径)
| 项 | 做法 | 约束 |
| --- | --- | --- |
| 开关 | `APT_REPORT_DISPATCH_ENABLED=false` + `apt.master-client.mock=true`(本地 profile 专用) | 生产 profile 强制 `false`,启动校验 |
| 数据来源 | 契约样例 JSON 放测试资源目录(不入库、不进仓库敏感信息) | 样例必须与本文表 9-7/9-8 字段一一对应 |
| 落库标记 | Mock 灌入的数据 `apt_master_version.source='LOCAL'`、`source_version=0` | 页面标识"非平台下发"(`08` R-03) |
| 上行 | outbox 只累积不派发;用 `/apt/report/outbox` 页面核对 payload | 不允许"假成功"置 `SENT` |
| 契约测试 | 对 20 个接口各写 1 条"请求样例 → 落库断言" | 冻结后作为回归基线 |
表 9-17 联调顺序(与 `07` 表 7-7 对齐)
| 步 | 内容 | 完成标志 |
| --- | --- | --- |
| 1 | 与平台对齐本文 20 个接口的路径与字段名 | 差异清单签字(冻结 v1) |
| 2 | 跑 Mock:灌 3 交路 / 5 房间 / 5 人员 / 2 天计划 | `apt_master_version` 有 `APPLIED`,`gy_jcxx` 有 `ryzt=0` |
| 3 | 打通 #14 心跳(真连或 Mock) | 平台侧能看到 `outboxWatermark` 与 `clockSkewMs` |
| 4 | 打通 #9 duty-results(先影子模式:只写不生效) | 对账零差异连续 3 天(表 5-7 S5) |
| 5 | 打开 #10 ledger | 流水双向可对(P2) |
| 6 | 打开 #11/#12/#13 | 摘要类上报不阻塞主链路 |
| 7 | 打通 #23 语音配置 + #24 摘要 | `apt_ast_config_version(source=PLATFORM)` 有 `APPLIED` |
| 8 | 关闭 Mock,全量切真实平台 | 24 小时无 P0/P1(表 5-7 S6) |
## 8. 契约冻结清单(F-01~F-13)
表 9-18 冻结 v1 前必须逐条确认
| # | 待确认项 | 影响的接口 | 不确认的后果 |
| --- | --- | --- | --- |
| F-01 | 路径前缀是 `/platform/v1` 还是按端分组(如 `/open/v1`) | 全部 | 反代与网关规则返工 |
| F-02 | `gyId` 与 `X-Apt-GyId` 冲突时的优先级 | 全部 | 身份伪造风险 |
| F-03 | #16/#17 的方向(平台回调 vs 公寓主动轮询) | #16、#17 | 补偿链路无法实现 |
| F-04 | 上行批量上限与单条回报粒度(是否允许部分成功) | #9~#13 | outbox 状态机需重写 |
| F-05 | 平台返回码字典(尤其幂等重放与版本过期的码值) | 全部上行 | 重试策略误判 |
| F-06 | `zjjcsjgz`/`kjjcsjgz`/`lysjgz` 到 `apt_master_rule` 的键映射 | #4 | 复算规则取错 |
| F-07 | `pickupMode` 到 `gy_syn_jcxx.jcfs` 的取值域 | #6 | 接车方式筛选失效 |
| F-08 | 计划与任务的业务键是否统一用 `planId`/`taskId` | #6、#7、#8 | 两套键导致重复任务 |
| F-09 | `deleted=1` 的人员是否仍允许查询历史叫乘 | #2 | 页面过滤口径不一致 |
| F-10 | `features[]` 的取值域与缺失语义(全关 or 全开) | #1 | 能力开关误判 |
| F-11 | `VOICE_SUMMARY` 的指标口径与统计窗口 | #24 | 平台看板数字对不上 |
| F-12 | 时钟偏差告警阈值(建议 1000ms)与处理动作 | #14 | 漏叫/误叫难定位 |
| F-13 | 主数据下行的 `version` 是否承诺单调递增(作为增量游标的替代,G-09) | #2、#5 | 无游标可存,只能每次全量拉取 |
## 9. 安全与缺口
表 9-19 跨端安全约束
| 项 | 约束 |
| --- | --- |
| 凭据 | 令牌/证书走环境变量,**不进仓库、不落日志**(`06` §9) |
| 地址 | 平台地址只能由环境变量提供;文档与代码中不出现内网地址 |
| 日志 | 记录 `batchId`、`gyId`、`watermark`、条数、结果码;**不记录身份证、手机号全量、录音地址** |
| 重放防护 | 上行带 `batchId` + `sentAt`,平台侧建议做时间窗(±5 分钟)校验 |
| 最小化 | payload 只带对账必需字段;**业务过程抓拍、录音与签到现场图不出公寓**,平台只存索引与计数(表 6-3 要点第 5 条、`docs/平台端/07` 表 7-7);**人员主数据**照片与特征值按 CR-C24 / `docs/公寓端/10` §1.1 允许跨端,本体在平台 `person-photo` 桶 + 短期签名直传,公寓库内只存 `photoRef` / `featureRef` 与 `checksum`、`featureAlgo` / `featureVersion`(#2 / #9 扩展,CR-02 §12) |
表 9-20 本篇新增的表结构/取值缺口
| # | 缺口 | 影响 | 不改库的兜底 | 若改库 |
| --- | --- | --- | --- | --- |
| G-08 | `apt_report_outbox.biz_type` 注释只列 `DUTY_RESULT/LEDGER/DEVICE_STATUS/ALARM/ARCHIVE/VOICE_SUMMARY`,**缺 `TASK_ACK`** | #8 回执无处落 | 直接使用 `TASK_ACK`(列无 CHECK 约束,仅注释过时) | 更新列注释(`modify column`,零数据变更) |
| G-09 | 主数据增量游标无落点:`apt_master_version` 无 `cursor` 列,只有 `version_no`/`checksum` | #2 人员增量翻页 | 以 `version_no` 充当游标(要求平台 `version` 单调递增,F-13) | `add cursor_value varchar(128)` |
> G-01~G-07 见 `08` 表 8-9;本篇新增 G-08/G-09,与前七条同属"待确认后再改库",本期均可用兜底方案落地。
@@ -0,0 +1,578 @@
# 10 人脸数据下发管理
> 文档编号:`docs/公寓端/10`。性质:**设计稿(Design Only)**,不写业务代码、不建表改表、不执行 DDL / DML、不改任何配置。
> 上位依据:`docs/10-架构优化方案-三端拆分.md` §5.1 表 5-1(数据分层)、§5.3(同库边界)、§6.1 表 6-1、§6.2(上报信封)、§7.2 / §7.3(设备接入)、§7.5(媒体边界);`docs/公寓端/03` §3.3、表 3-4(设备域)、`docs/公寓端/05` §3 / §5 / §6、`docs/设计变更/03-三端功能设计修订(CR-02).md`(下称 CR-02)§2 / §3 / §4 / §6 / §10 / §11。
> **落地权威表述**:本文件是"人员人脸主数据在平台 ↔ 公寓 ↔ 设备之间注册、同步、下发、隔离与清理"的**唯一权威表述**;口径改判与施工单登记见 `CR-02 §12`(CR-C24 媒体边界、CR-C25 注册链路)。跨端仍为 20 个接口编号、契约 `v2-draft`、平台不回调公寓(CR-C07);本模块**不新增跨端编号、不新增业务表**,采用"复用既有表 + 补列 + 规则"实现。
> **CR-02 §15 关联(三端一致性)**:本文件仍是人脸注册 / 下发 / 刷脸核验的唯一权威表述;应用端与平台端按 §15 表 3-65 同步业务规则、交互流程、权限控制与异常处理,三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**(`reg_state` / `regVerdict` / `upload_state` / `dispatch_state` / `faceVerifyState` 等枚举不变)。平台统计以 #9 `PERSON_REG` 同步回执与注册态作辅助数据源,**统计结果不回写**本模块任何状态;本端不新增上报接口与 `bizType`。变更点与影响范围见 CR-02 §15 表 3-63。
> **注册链路 v1.1(CR-C25)**:人员注册由**签到终端发起申请并上传人脸**,**值班员在值班台完成人员注册**;注册成功后人脸进入公寓服务系统,再**分别**上传平台与下发公寓内设备。两条同步路径解耦并行、互不阻塞;平台回执不再是注册生效前置。
> 术语与编号:表名一律小写,字段类型取 `apartment/db/mysql/*.sql` 口径,枚举为大写下划线字符串(不用数字码、不用 MySQL `ENUM`);图表编号 `表 10-y` / `图 10-y`;功能项 `AF-01`~`AF-06`、接口 `N-25`~`N-33`(沿用 CR-02 的 `N-*` 序列)、决策点 `Q-30`~`Q-34`。
表 10-1 文档信息
| 项 | 内容 |
| --- | --- |
| 文档编号 | `docs/公寓端/10` |
| 文档名称 | 人脸数据下发管理(注册、平台同步、设备下发、权限隔离、离寓清理、长期驻留) |
| 适用范围 | 铁路行车公寓叫乘与一卡通系统·公寓端人脸数据管理模块 `apt-face` |
| 关联文档 | `docs/公寓端/00`~`09`、`docs/平台端/01`~`07`、`docs/设计变更/03` §12、`docs/10`、`docs/11` |
| 约束条件 | ① 人员档案真源在平台,公寓端只持只读快照 + 本地登记流水;② 平台不回调公寓,下行一律"拉";③ 设备侧一律"设备主动拉取",服务端不回调设备;④ `gyid` 数据域锁定;⑤ 状态字段单写入口不变 |
| 交付对象 | 需求评审、三端详细设计、契约冻结、设备联调与验收 |
表 10-2 修订记录
| 版本 | 日期 | 说明 |
| --- | --- | --- |
| v1.0 | 2026-11-09 | 首次形成:定义 `apt-face` 模块,覆盖本地注册自动推送平台、平台新增人员下载入库、签到设备查询下发、消费/叫乘设备按设备维度隔离、离寓自动擦除、长期驻留人员长期保留六项功能;明确触发条件、数据范围、同步与清理机制、权限隔离与异常处理;改判人脸特征值跨端传输口径(CR-C24) |
| v1.1 | 2026-11-09 | **注册链路改判(CR-C25)**:人员注册改为「签到终端发起注册申请并上传人脸 → 值班员在值班台完成人员注册 → 注册成功后人脸进入公寓服务系统 → 分别上传平台与下发公寓内设备」;新增 `reg_state` / `apply_device_code` / `confirmed_by` / `dispatch_state` 等状态与留痕列、注册状态机、双路解耦同步规则、N-34~N-37 接口与 Q-35;撤销"平台确认后注册才生效、确认前不得下发设备"的旧前置口径 |
---
## 1. 模块定位与对象定义
### 1.1 两类人脸对象的区分(**本模块的关键前提**)
表 10-3 人脸相关数据的分类与边界
| 类别 | 内容 | 是否跨端(平台 ↔ 公寓) | 是否下发设备 | 说明 |
| --- | --- | --- | --- | --- |
| **人员主数据人脸** | 注册照片、**特征值**、算法标识、特征版本、人员类别 | **允许**(本模块管理对象) | **允许**(按设备权限范围) | 用于身份识别的主数据,必须本地与平台一致;照片原本即由平台对象存储承载(`docs/平台端/07` §2 第 2 条、`docs/公寓端/09` 表 9-1 第 2 行"人员增量(含照片版本)") |
| **业务过程抓拍** | 叫乘抓拍、离寓抓拍、签到现场图、平板刷脸比对帧 | **禁止** | 不留存于公寓服务端 | 仍按 `docs/10` 表 6-3 要点 5、`docs/平台端/07` 表 7-7:抓拍与录音不出公寓,平台只存索引与计数 |
| **识别结果** | `faceVerifyState`、`verifiedRyid`、比对时间与分数 | 允许(业务结论) | — | 结果上报不带图像(`docs/公寓端/05` §9.3 表 5-17、CR-C22) |
> **口径改判**:CR-02 CR-C04 / 表 3-13 / 表 3-24 中"人脸特征引用与校验和可传,**原图与特征大对象不出公寓**"的表述,针对的是**业务过程抓拍与识别结果**,此前被扩大解释到人员主数据。本模块明确:人员主数据的照片与特征值**必须**双向同步(见 CR-02 §12 表 3-49 的 CR-C24);抓拍、录音、签到现场图仍不出公寓,识别结果仍不带图像。**不因本次改判放开媒体边界**。
### 1.2 模块与三端职责
表 10-4 `apt-face` 子域、职责与唯一写入口
| 子域 | 端 | 职责 | 唯一写入的表 / 列 | 依赖 |
| --- | --- | --- | --- | --- |
| `apt.face.registry` | 公寓端 | **注册申请受理与值班员确认注册**:接收签到终端申请(要素 + 人脸)、生成值班台待办、确认后生成正式 `ryid` 并写入人员主档本地副本 | `apt_person_face_reg`、`apt_master_person`(经 `apt-master-client` 事务写) | 签到终端 `/open/v1/face/reg-requests`、`duty.ws` 值班台 |
| `apt.face.sync` | 公寓端 | 注册成功后**上传平台**:#9 `PERSON_REG` 上行、媒体签名直传、平台回执与版本对齐;以及从平台**下载**新增人员入库 | `apt_report_outbox` / `apt_report_history`、`apt_master_person`、`apt_master_version` | `apt-report`、`apt-master-client` |
| `apt.face.scope` | 公寓端 | 设备可见范围规则、人员类别、下发目标集合计算 | `gy_sb.face_scope_*`、`apt_master_person.person_category` | `apt.device.catalog` |
| `apt.face.dispatch` | 公寓端 | 注册成功后**下发公寓内设备**:生成下发 / 擦除 / 全量同步指令、回执处理、缓存台账与对账 | `apt_device_command`、`gy_sb_xt.face_cache_*` | `apt.device.command`、`apt.device.gateway` |
| `platform.ingest` / `platform-master` | 平台端 | 接收注册结果并纳入跨公寓人员主档、回同步回执、维护主档增量与媒体签名地址;**不做注册准入审批** | `pl_person_reg_request`、平台人员主档 | #2 / #9 |
| 签到终端 / 设备侧固件 | 设备 | 签到终端**发起注册申请并上传人脸**;其他设备按凭据与范围拉取人员与人脸、本地缓存、执行擦除、上报缓存摘要 | 设备本地缓存(不在公寓库) | `/open/v1/face/**` |
| `duty.ws` / 值班台 | 应用端 | 展示待审申请、由**值班员核对并完成人员注册**(确认 / 驳回),回写确认留痕 | `duty_operation_log`、`duty_manual_action` | `apt.face.registry`(只提交注册命令) |
**依赖方向单向**:`apt.face.registry` → `apt.face.sync` / `apt.face.dispatch`;`apt.face.dispatch` → `apt.device.command`;值班台只提交注册命令、不直接写业务表;`apt-face` 不被 `apt.duty.*` 反向依赖。所有业务模块对 `apt_master_person` 仍只读(`docs/公寓端/03` 表 3-1、`docs/设计变更/01` 不变式 A),唯一写入口是注册确认时 `apt-master-client` 的事务写。
### 1.3 与既有流程的关系
- 注册链路按 **CR-C25** 改判为「签到终端申请 → 值班员注册 → 入公寓服务系统 → 分别上传平台 / 下发设备」;CR-02 F-F 的旧"平台确认 → #2 下行生效"流程由 §12 F-J 覆盖,历史行按覆盖关系理解。
- 主数据下行仍沿用 `docs/公寓端/05` §5(版本校验、事务写快照、`apt_master_version(APPLIED)`),本模块补充"媒体下载、校验和校验、增量变更触发下发 / 擦除";平台 `personRegVerdicts[]` 语义改为**同步回执**,不决定本地注册是否生效。
- 设备接入仍沿用 `docs/公寓端/05` §3(注册、心跳、取指令、ACK),本模块补充"人脸指令类型与缓存摘要"。
- 离寓仍以 `apt-stay` 为唯一写入口(`gy_jcxx.finish_ly` / `gygl_ly_event`),本模块只消费离寓事实生成擦除指令,**不改叫乘状态字段**。
---
## 2. 数据模型(复用既有表 + 补列,不新增业务表)
表 10-5 复用表清单
| 表 | 现有用途 | 本模块用途 | 唯一写入模块 | owner |
| --- | --- | --- | --- | --- |
| `apt_master_person` | 平台下发人员只读快照 | **人员人脸主档的本地权威副本**(照片、特征值引用、类别、版本);设备下发唯一取数源 | `apt-master-client` | `platform-down` |
| `apt_person_face_reg` | 本地人员/人脸登记流水 | **注册申请流水**(签到终端来源、值班员确认留痕、注册状态机)、上行同步状态、设备下发状态、平台同步回执 | `apt.face.registry` | apartment |
| `pl_person_reg_request` | 平台接收注册结果 | 平台侧人员人脸主档登记与**同步回执**(`ACCEPTED` / `REJECTED`),不做注册准入审批 | `platform-ingest` | platform |
| `gy_sb` | 设备档案 | 设备类别与**人脸可见范围** | `apt.device.catalog` | apartment |
| `gy_sb_xt` | 设备心跳与版本状态 | 设备人脸缓存版本、数量、摘要与最后同步时间 | `apt.device.gateway` | apartment |
| `apt_device_command` | 设备指令队列(PENDING/SENT/ACKED/EXPIRED/FAILED) | 下发 / 擦除 / 全量同步 / 范围重载指令与回执 | `apt.device.command` | apartment |
| `apt_report_outbox` / `apt_report_history` | 上行队列与终态流水 | `PERSON_REG` 上行、续报与对账 | `apt.report.outbox` | apartment |
| `apt_master_version` | 快照版本 | `(gyid, PERSON, source, version)` 人员增量水位 | `apt-master-client` | apartment |
> **为什么不加新表**:`docs/11` 指出既有库 40 表 / 0 主键 / 0 索引、单表 70 列的上帝表问题;人脸主档已有 `apt_master_person`,指令已有 `apt_device_command`,设备状态已有 `gy_sb_xt`。新增独立表会形成第三份人员真源与第四份指令队列,反而破坏一致性。补列均为设计定义,DDL 待阶段二契约冻结评审通过后执行。
表 10-6 补列定义(按表分组;均为设计定义)
| 表 | 补列 | 类型 | 约束 / 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `apt_master_person` | `photo_ref` | varchar(512) | 可空 | 本地照片引用(平台对象键 / 本地缓存路径),**不存图片二进制** |
| | `photo_checksum` | varchar(128) | 可空 | 照片 SHA-256,下载后校验 |
| | `feature_ref` | varchar(512) | 可空 | 特征值加密文件引用(不入库大对象) |
| | `feature_checksum` | varchar(128) | 可空 | 特征值校验和 |
| | `feature_algo` | varchar(32) | 可空 | 算法标识(如厂商/版本),设备兼容性判定 |
| | `feature_version` | bigint | 默认 `0` | 特征版本,单调递增,下发幂等键组成部分 |
| | `person_category` | varchar(20) | 默认 `TRANSIENT` | `TRANSIENT`(临时入住/乘务)/ `LONG_TERM`(保洁/巡检/管理长期驻留) |
| | `face_sync_state` | varchar(20) | 默认 `PENDING` | `PENDING` / `SYNCED` / `FAILED` / `STALE`;媒体下载与校验结果 |
| | `face_platform_version` | bigint | 默认 `0` | 平台侧版本,与本地对账 |
| `apt_person_face_reg` | `photo_ref` / `feature_ref` | varchar(512) ×2 | NOT NULL | 签到终端上传的人脸媒体引用;补 `photo_checksum` / `feature_checksum` 校验和列 |
| | `feature_algo` / `feature_version` | varchar(32) / bigint | 默认 `1` | 算法与版本 |
| | `person_category` | varchar(20) | 默认 `TRANSIENT` | 值班员确认时可改判为 `LONG_TERM` |
| | `reg_state` | varchar(20) | 默认 `APPLIED` | `APPLIED` / `CONFIRMED` / `REJECTED` / `ARCHIVED`;**注册状态机**(表 10-11) |
| | `apply_device_code` / `applied_at` | varchar(64) / datetime(3) | NOT NULL | 发起申请的签到终端与申请时间 |
| | `confirmed_ryid` / `confirmed_by` / `confirmed_at` | varchar(64) / varchar(64) / datetime(3) | 可空 | 值班员确认时生成正式 `ryid` 并留痕 |
| | `reject_reason` / `verdict` | varchar(256) / varchar(20) | 可空 / 默认 `PENDING` | 值班员驳回理由;`verdict` 只记平台同步回执 |
| | `upload_state` / `dispatch_state` | varchar(20) ×2 | 默认 `PENDING` | 上传平台与下发设备**两条独立状态链**,互不阻塞(CR-C25) |
| | `platform_version` | bigint | 默认 `0` | 平台同步回执后回填 |
| `gy_sb` | `device_class` | varchar(20) | 默认 `OTHER` | `CHECKIN` / `CONSUME` / `CALL` / `TABLET` / `OTHER` |
| | `face_scope_type` | varchar(24) | 默认 `NONE` | `ALL_ACTIVE` / `ROOM_BOUND` / `CATEGORY_WHITELIST` / `NONE` |
| | `face_scope_value` | varchar(256) | 可空 | 房间号 / 类别白名单 / 区域编码 |
| | `face_cache_enabled` | tinyint(1) | 默认 `0` | 是否允许缓存人脸;`TABLET` 等按任务临时拉取的设备可为 `0` |
| `gy_sb_xt` | `face_cache_version` | bigint | 默认 `0` | 设备上报的缓存版本 |
| | `face_cache_count` | int | 默认 `0` | 设备缓存人员数 |
| | `face_cache_hash` | varchar(128) | 可空 | 设备缓存集合摘要(`ryid + feature_version` 有序哈希) |
| | `last_face_sync_at` | datetime(3) | 可空 | 最后一次成功同步 / 擦除回执时间 |
| `apt_device_command` | `biz_type` | varchar(30) | 默认 `GENERIC` | `FACE_ISSUE` / `FACE_ERASE` / `FACE_FULL_SYNC` / `FACE_SCOPE_RELOAD` |
| | `biz_id` | varchar(64) | 可空 | `ryid`(单人)或批次 ID(全量) |
| | `biz_version` | bigint | 默认 `1` | 与 `feature_version` 对齐;更正时 +1 |
| | `payload` | json | 可空 | 下发字段集、擦除原因、媒体签名地址等(若既有表无此列则补) |
| `pl_person_reg_request` | `photo_ref` / `feature_ref` | string(512) ×2 | 可空 | 平台侧媒体引用 |
| | `photo_checksum` / `feature_checksum` | string(128) ×2 | 可空 | 校验和 |
| | `feature_algo` / `feature_version` / `person_category` | string(32) / long / enum_s | 默认 `1` / `TRANSIENT` | 与公寓端同字典 |
表 10-7 枚举字典
| 枚举 | 取值 | 说明 |
| --- | --- | --- |
| `regState` | `APPLIED` / `CONFIRMED` / `REJECTED` / `ARCHIVED` | 注册状态机;只有 `CONFIRMED` 才使人员进入公寓服务系统 |
| `platformVerdict` | `PENDING` / `ACCEPTED` / `REJECTED` | **平台同步回执**,不是注册批准;`REJECTED` 触发告警与值班台待办 |
| `uploadState` / `dispatchState` | `PENDING` / `SENT`(`ISSUED`) / `ACKED` / `FAILED` / `DEAD` | 上传平台与下发设备两条独立状态链,互不阻塞 |
| `personCategory` | `TRANSIENT` / `LONG_TERM` | 临时入住 / 长期驻留;决定是否参与离寓擦除 |
| `faceSyncState` | `PENDING` / `SYNCED` / `FAILED` / `STALE` | 媒体与版本同步结果 |
| `deviceClass` | `CHECKIN` / `CONSUME` / `CALL` / `TABLET` / `OTHER` | 设备类别,决定可见范围 |
| `faceScopeType` | `ALL_ACTIVE` / `ROOM_BOUND` / `CATEGORY_WHITELIST` / `NONE` | 设备可见范围规则 |
| `faceBizType` | `FACE_ISSUE` / `FACE_ERASE` / `FACE_FULL_SYNC` / `FACE_SCOPE_RELOAD` | 人脸指令类型 |
| `eraseReason` | `CHECKOUT` / `PLATFORM_DELETED` / `CATEGORY_REVOKED` / `MANUAL` / `RECONCILE` | 擦除原因,进审计 |
---
## 3. 数据流向总览
图 10-1 平台 ↔ 公寓 ↔ 设备的人脸数据流向
```mermaid
flowchart LR
subgraph DUTYUI["应用端值班台"]
DUTY["值班员审核并<br/>完成人员注册"]
end
subgraph DEV["公寓内设备(按类别隔离)"]
CK["签到终端 CHECKIN<br/>发起注册申请 / 上传人脸"]
CO["消费设备 CONSUME"]
CL["叫乘设备 CALL"]
end
subgraph AP["公寓服务系统(apt-face)"]
REG["apt.face.registry<br/>apt_person_face_reg(APPLIED → CONFIRMED)"]
MASTER["apt_master_person<br/>本地权威副本(注册成功后入档)"]
SYN["apt.face.sync<br/>outbox / 快照版本"]
SCOPE["apt.face.scope<br/>gy_sb.face_scope_*"]
DISP["apt.face.dispatch<br/>apt_device_command"]
end
subgraph PL["平台端"]
PM["人员人脸主档<br/>pl_person_reg_request / 平台主档"]
PO["对象存储<br/>照片 / 特征值(仅人员主数据)"]
end
CK -->|AF-01 ① REG_APPLY 申请 + 人脸| REG
REG -->|② 生成值班台待办| DUTY
DUTY -->|③ 值班员 CONFIRM 完成注册| REG
REG -->|④ 注册成功,人脸进入公寓服务系统| MASTER
MASTER -->|⑤a 上传平台| SYN
SYN -->|#9 PERSON_REG + 媒体签名直传| PM
SYN -->|媒体本体| PO
PM -->|AF-02 #2 人员增量 + 同步回执(拉)| SYN
PO -->|短期签名下载| SYN
MASTER -->|⑤b 下发公寓内设备| SCOPE
SCOPE -->|计算目标设备集合| DISP
DISP -->|AF-03/04 FACE_ISSUE| CK
DISP -->|AF-04 FACE_ISSUE(最小字段)| CO
DISP -->|AF-04 FACE_ISSUE(最小字段)| CL
CK -->|AF-05 缓存摘要 / ACK| DISP
CO -->|AF-05 缓存摘要 / ACK| DISP
CL -->|AF-05 缓存摘要 / ACK| DISP
DISP -->|AF-05 FACE_ERASE(仅 TRANSIENT 离寓)| CO
DISP -->|AF-05 FACE_ERASE(仅 TRANSIENT 离寓)| CL
CK -.->|AF-06 LONG_TERM 长期保留,不擦除| CK
```
表 10-8 六条数据流与处理规则
| 编号 | 功能 | 源 → 目的 | 触发 | 数据范围 | 机制 | 落点 |
| --- | --- | --- | --- | --- | --- | --- |
| AF-01 | 签到终端申请 → 值班员注册 → 入公寓服务系统 → 分别同步 | 签到终端 → 值班台 → `apt-face` → 平台 / 公寓内设备 | 签到终端提交 `REG_APPLY`;**注册生效点为值班员 `CONFIRM`** | 人员要素 + 照片 + 特征值 + 算法/版本 + 申请设备来源 | 申请落流水(`APPLIED`,不进主档)→ 值班台待办 → 值班员确认(生成 `ryid`、`CONFIRMED`)→ 事务写 `apt_master_person` → **两条解耦路径**:#9 `PERSON_REG` 上传平台 / `FACE_ISSUE` 下发设备 | `apt_person_face_reg`、`apt_master_person`、`apt_report_outbox`、`apt_device_command`、`pl_person_reg_request` |
| AF-02 | 平台新增人员下载入库 | 平台 → `apt-face` → 本地快照 | 定时/增量拉取 #2、`version` 递增 | 新增/变更人员 + 照片 + 特征值 + 类别 | 拉增量 → 下载媒体 → 校验 checksum → 事务写快照 + 版本 | `apt_master_person`、`apt_master_version` |
| AF-03 | 签到设备查询与下发 | `apt_master_person` → 签到设备 | 人员在册变更 / 设备上线 / 手工全量 | **全部有效人员**(`TRANSIENT` 在寓 + `LONG_TERM`),含照片与特征值 | 按 `feature_version` 增量下发,媒体走设备侧签名 URL,回执后更新台账 | `apt_device_command`、`gy_sb_xt` |
| AF-04 | 消费/叫乘设备按设备维度隔离 | `apt_master_person` → 消费/叫乘设备 | 同 AF-03,但按范围过滤 | **仅本设备权限范围内的在寓 `TRANSIENT` + 授权 `LONG_TERM`**,最小必要字段(不含照片) | 服务端强制拼范围条件,设备不得按 `ryid` 任意查询 | `gy_sb.face_scope_*`、N-30 |
| AF-05 | 离寓自动擦除 | 离寓事实 → 消费/叫乘设备 | `finish_ly='1'` / `gygl_ly_event(CHECKOUT)` / 平台 `deleted=1` | **仅 `TRANSIENT` 且已下发过的设备**;`LONG_TERM` 不擦除 | 生成 `FACE_ERASE` → ACK 回执 → 重投/全量同步兜底 → 缓存摘要对账 | `apt_device_command`、`gy_sb_xt` |
| AF-06 | 长期驻留人员长期保留 | 平台/本地配置 → 设备独立分区 | `person_category=LONG_TERM` 生效/撤销 | 保洁、巡检、管理等长期驻留人员 | 独立缓存分区、独立版本、不参与离寓擦除;撤销需二次确认 | `apt_master_person.person_category`、`apt_clean_staff` |
**四条硬约束**:① 平台不回调公寓,下行一律"拉"(CR-C07);② 公寓不回调设备,设备一律主动拉取指令与媒体(`docs/公寓端/05` §3 表 5-3);③ 设备下发唯一取数源是 `apt_master_person`,注册申请未获值班员确认(`reg_state=APPLIED`)**不得下发设备、不得参与运安匹配**(`40914`);④ 注册成功后"上传平台"与"下发设备"是**两条独立状态链**,平台回执不是下发前置、设备 ACK 也不是平台上报前置(CR-C25)。
---
## 4. AF-01 注册申请、值班员注册与平台 / 设备分别同步
> 本节是注册链路的**唯一权威表述**(CR-C25)。注册由**签到终端发起申请并上传人脸**,**值班员在值班台完成人员注册**;注册成功后人脸进入公寓服务系统,再**分别**上传平台与下发公寓内设备。CR-02 F-F 的旧"平台确认 → #2 下行生效"流程由本节与 §12 F-J 覆盖。
### 4.1 触发条件与数据范围
表 10-9 三段触发条件与数据范围
| 阶段 | 主体 / 触发 | 数据范围 | 前置与不触发 |
| --- | --- | --- | --- |
| ① 注册申请 | 签到终端(N-34)提交 `REG_APPLY`:现场采集**人员要素 + 人脸**并上传 | `gyid`、`work_no`、`name`、`unit_code`、照片、特征值(或仅照片由公寓端按 `feature_algo` 提取)、`apply_device_code`、`occurred_at` | 媒体缺失 / 格式不支持 / `checksum` 不符 → `40919` / `40920` 拒收;同一 `work_no` 已有未决申请 → 返回原 `regId`(幂等);已有 `CONFIRMED` 人员 → `40918`;**不进主档、不下发设备、不上报平台** |
| ② 值班员注册 | 值班台(N-35 / N-36)出现待办,值班员核对要素与照片、可修正 `unit_code` / `person_category`,点"确认注册" | `regId`、`confirmed_ryid`(确认时生成)、`confirmed_by`、`confirmed_at`、`person_category`、`feature_algo` 兼容性判定 | 需 `duty:person:reg` 权限 + 二次确认;驳回(N-37)须填 `reject_reason`;确认前参与运安匹配 / 下发设备 → `40914` |
| ③ 注册成功后分别同步 | `reg_state=CONFIRMED` 且 `apt_master_person` 事务写成功即**自动触发**两条路径 | 人员主档全量要素 + 照片 / 特征值引用与校验和 + `feature_version` + `ryid` | 无需人工点"同步";平台不可达不阻塞设备下发,设备离线不阻塞平台上报 |
> **注册生效点**:值班员 `CONFIRM`,不是平台回执。平台 `personRegVerdicts[]` / `verdict` 自此只作主档同步回执,不决定人员是否在公寓服务系统生效。
### 4.2 端到端时序
图 10-2 注册申请 → 值班员注册 → 入公寓服务系统 → 分别上传平台 / 下发设备
```mermaid
sequenceDiagram
autonumber
participant CK as 签到终端 CHECKIN
participant R as apt.face.registry
participant D as 值班台(应用端 /duty)
participant M as apt_master_person
participant S as apt.face.sync / outbox
participant P as 平台端
participant W as apt.face.dispatch
participant DV as 公寓内设备(签到/消费/叫乘)
CK->>R: ① REG_APPLY(要素 + 人脸照片 / 特征值,N-34)
R->>R: 校验 checksum,加密落盘 → photo_ref / feature_ref
R->>R: 写 apt_person_face_reg(reg_state=APPLIED, local_only=1)
R->>D: ② 推待办 reg.apply(不写主档、不下发)
D->>D: 值班员核对要素与照片、补全单位 / 类别
D->>R: ③ CONFIRM(duty:person:reg + 二次确认,N-36)
R->>R: 生成 confirmed_ryid、reg_state=CONFIRMED、local_only=0
R->>M: ④ 事务写 apt_master_person + apt_master_version(人脸进入公寓服务系统)
par ⑤a 上传平台
R->>S: 提交同步命令(upload_state=PENDING)
S->>P: 媒体签名直传 + #9 PERSON_REG(regId, ryid, 引用, checksum, algo, version)
P->>P: 校验工号唯一性 / checksum → 纳入跨公寓主档
P-->>S: 同步回执 ACCEPTED / REJECTED + platform_version
S->>R: 回填 upload_state / verdict / platform_version
and ⑤b 下发公寓内设备
R->>W: 提交下发命令(dispatch_state=PENDING)
W->>W: 按 gy_sb.face_scope_* 计算目标设备集合
W->>DV: FACE_ISSUE(设备心跳主动拉取,N-30 / N-31)
DV-->>W: ACK + face_cache_hash / count / version(N-32 / N-33)
W->>R: 回填 dispatch_state=ACKED
end
Note over S,W: 两条路径独立幂等、互不阻塞;平台不可达 / 设备离线各自排队续报
```
### 4.3 处理步骤与规则
表 10-10 处理步骤与规则(A 申请 / B 值班员注册 / C 双路同步)
| 步 | 阶段 | 主体 | 动作 | 落表 | 异常 / 补偿 |
| --- | --- | --- | --- | --- | --- |
| 1 | A | 签到终端 | 采集人员要素与人脸,按设备凭据 + HMAC 提交 `REG_APPLY` | 设备侧采集缓存 | 凭据失效 / 越 `gyid` → `40301` |
| 2 | A | `apt.face.registry` | 校验媒体完整性与 `work_no` 唯一性;加密落盘生成引用与校验和 | `apt_person_face_reg(APPLIED)` | 媒体缺失 `40919`、校验不符 `40920`、已注册 `40918`;未决重复申请返回原 `regId` |
| 3 | A | `duty.ws` | 生成值班台待办 `reg.apply`,展示要素、照片、申请设备与时间 | `duty_operation_log` | 推送失败靠 60s 兜底轮询;待办不重复生成 |
| 4 | B | 值班员 | 核对并修正要素、`person_category`,确认或驳回注册 | `duty_manual_action`、`duty_operation_log` | 无权限 `40301`;驳回须填理由,媒体按 Q-32 保留期清理,不生成 `ryid` |
| 5 | B | `apt.face.registry` | 确认时生成 `confirmed_ryid`、置 `CONFIRMED` / `local_only=0`;`apt-master-client` 事务写主档副本 | `apt_person_face_reg`、`apt_master_person`、`apt_master_version` | 事务失败整体回滚,注册态保持 `APPLIED`;算法不兼容 `40921` 可确认但标记不下发 |
| 6 | C-a | `apt.face.sync` | 媒体签名直传(≤ 10 min)+ #9 `PERSON_REG` 上行 | `apt_report_outbox` / `apt_report_history`、`pl_person_reg_request` | 断网 → outbox `PENDING`,恢复后复用原 `idempotencyKey` 续报;**不阻塞 C-b** |
| 7 | C-a | 平台端 | 校验工号唯一性与 `checksum`,纳入跨公寓主档并回 `ACCEPTED` / `REJECTED` + `platform_version` | 平台人员主档 | 回执由 #2 拉取;`REJECTED` 只告警 + 待办,**不回滚本地注册与已下发设备**(Q-35) |
| 8 | C-b | `apt.face.scope` / `apt.face.dispatch` | 按设备类别与 `face_scope_*` 计算目标集合,生成 `FACE_ISSUE` | `apt_device_command` | 设备离线 → 指令排队,上线心跳取走;范围为空则不下发并记审计 |
| 9 | C-b | 公寓内设备 | 按 `cursor` + `version` 拉取人员与人脸、校验 `hash` 后写本地缓存并 ACK | `gy_sb_xt.face_cache_*` | 回执丢失按 `commandId` 幂等重投;摘要不一致 → `FACE_FULL_SYNC` |
| 10 | C | `apt.face.registry` | 分别维护 `upload_state` / `dispatch_state`;两条链路各自终态,互不改写 | `apt_person_face_reg` | 任一条 `FAILED` 只告警并独立重试,不把另一条回退为 `PENDING` |
### 4.4 注册状态机与一致性
注册状态机(`reg_state`,唯一写入口 `apt.face.registry`):
- `APPLIED` → `CONFIRMED`(值班员确认,生成 `ryid` 并入档)或 `REJECTED`(值班员驳回,写 `reject_reason`);
- `CONFIRMED` 之后 `upload_state` / `dispatch_state` 各自 `PENDING → SENT` / `ISSUED → ACKED`,失败转 `FAILED` 重试、最终 `DEAD` 转人工;`verdict` 由平台回执单独回填;
- `REJECTED` / `ARCHIVED` 为终态,不回写 `APPLIED`;媒体按 Q-32 保留期清理并留审计。
表 10-11 注册态与平台 / 设备一致性规则
| 规则 | 口径 |
| --- | --- |
| 真源 | 平台仍是**跨公寓**人员主档真源;公寓 `apt_master_person` 是注册成功后写入的本地权威副本,`apt_person_face_reg` 只是申请与同步流水,三者不互为真源 |
| 生效点 | **值班员 `CONFIRM`**,平台确认不再是前置(CR-C25);平台版本高于本地时按 `platform_version` 覆盖本地主档,不撤销注册事实 |
| 双路解耦 | 上传平台(outbox)与下发设备(`apt_device_command`)各自幂等、各自重试;两条状态链独立落 `apt_person_face_reg`,任一失败不影响另一条 |
| 版本 | `feature_version`(本地)与 `face_platform_version`(平台)双向携带;平台版本高则以平台为准覆盖并触发重下发,本地版本高则触发补传 |
| 幂等 | 申请 `regId` + `work_no`;上行 `idempotencyKey = hash(gyid + PERSON_REG + regId + bizVersion)`;设备 `commandId = hash(gyid + deviceCode + FACE_ISSUE + ryid + bizVersion)`;媒体按 `checksum` 去重 |
| 确认前 | `reg_state=APPLIED` 的人员**不得**下发设备、不得参与运安匹配(`40914`)、不参与叫乘 |
| 平台驳回 | 本地注册保留、`verdict=REJECTED`,告警 + 值班台待办 + 暂停后续下发;是否自动擦除已下发缓存由 Q-35 拍板,未决默认**不自动擦除** |
| 冲突 | 同一 `work_no` 平台已有档案 → 平台回 `REJECTED(DUPLICATE_WORK_NO)`,本地保留注册事实与审计,不自动覆盖平台记录,由值班台人工归并 |
| 对账 | 每日按 `(gyid, ryid, feature_version, checksum)` 比对本地与平台 `count/sum/max(version)`;差异写告警并触发重拉 #2 或补传 #9 |
---
## 5. AF-02 从平台查询、下载新增人员并完成本地入库
表 10-12 触发条件与数据范围
| 项 | 口径 |
| --- | --- |
| 触发 | ① `apt-master-client` 定时拉取 #2 `GET /platform/v1/master/persons?gyId&cursor&version`;② 心跳/版本变更发现 `version` 递增;③ 管理端 `POST /apt/face/persons/commands`(`FULL_SYNC`)手工触发;④ 收到 `personRegVerdicts[]` **同步回执**后对齐 `platform_version`;⑤ 值班员注册成功后的首次平台增量对齐(已由 AF-01 写入的 `ryid` 不重复入库) |
| 数据范围 | 增量人员:`ryid`、`work_no`、姓名、`unit_code`、`person_category`、**照片**、**特征值**、`feature_algo`、`feature_version`、`photo_checksum` / `feature_checksum`、`deleted` 标记、`platform_version` |
| 不下载 | 业务抓拍、录音、叫乘现场图;这些仍不出公寓 |
表 10-13 入库步骤与规则
| 步 | 动作 | 规则 | 异常 |
| --- | --- | --- | --- |
| 1 | 拉 #2 增量 | 比较 `apt_master_version(PERSON)` 的 `version`,仅处理 `version > 本地`;重复/旧版本丢弃并记日志 | 版本回退 → 告警并请求全量 |
| 2 | 校验响应 | 校验 `gyid` 一致、`checksum` 字段齐备、`feature_algo` 在设备兼容白名单 | 缺字段 → 不入库,整批重试 |
| 3 | 下载媒体 | 用 `mediaUrls[]` 短期签名地址下载照片与特征值;**先下载到临时区、校验 checksum 通过后才入正式引用** | 超时/校验不符 → 丢弃临时文件、退避重试、`face_sync_state=FAILED` |
| 4 | 事务写快照 | 一个事务内 upsert `apt_master_person`(按 `ryid`)+ 更新 `apt_master_version(APPLIED)` + 记 `duty_operation_log` | 任一步失败整体回滚,保留旧快照 |
| 5 | 生效判定 | `face_sync_state=SYNCED` 才允许下发设备;`deleted=1` 的人员触发 AF-05 擦除 | 半同步状态不得下发,避免设备拿到不完整数据 |
| 6 | 通知依赖 | 通知 `apt.face.scope` 重算目标设备集合、`duty.ws` 推人员变更(沿用 §5「通知依赖模块刷新缓存」) | 通知失败不影响快照,靠 60s 兜底轮询 |
表 10-14 入库校验清单
| 校验 | 判据 | 失败处理 |
| --- | --- | --- |
| 必要字段 | `ryid`、姓名、`person_category`、`feature_version`、`checksum` 齐备 | `40920 APT_FACE_MEDIA_CHECKSUM_MISMATCH` 或整批重试 |
| 媒体完整性 | `SHA-256(photo) == photo_checksum`、`SHA-256(feature) == feature_checksum` | 丢弃 + 重下,连续 3 次告警 |
| 算法兼容 | `feature_algo` 在 `apt.face.algo.whitelist` 内 | `40921 APT_FACE_ALGO_UNSUPPORTED`,人员入库但**不下发设备** |
| 版本单调 | `feature_version >= 本地同 ryid 版本` | 旧版本丢弃;相同版本但内容不同 → 以 checksum 判冲突并告警 |
| 大小上限 | 单条特征值 ≤ `face.feature.maxBytes`、照片 ≤ `face.photo.maxBytes` | `40922 APT_FACE_MEDIA_TOO_LARGE` |
| `gyid` 隔离 | 所有记录 `gyid` 等于会话/设备所属公寓 | 越 `gyid` 直接 `40301` |
---
## 6. AF-03 签到设备查询人员及人脸信息并由公寓端下发
表 10-15 触发条件、范围与下发规则
| 项 | 口径 |
| --- | --- |
| 触发 | ① **AF-01 值班员注册成功且 `apt_master_person` 入档**(`reg_state=CONFIRMED`、`face_sync_state=SYNCED`)或 AF-02 平台增量入库完成;② 签到设备注册/上线或心跳报 `face_cache_version` 落后;③ 管理端 `RE_ISSUE` / `FULL_SYNC` 命令;④ 对账发现缓存摘要不一致 |
| 目标设备 | `gy_sb.device_class='CHECKIN'` 且 `face_cache_enabled=1`、`enabled=1`、`deleted=0` |
| 数据范围 | **全部有效人员**:在寓 `TRANSIENT`(`finish_ly='0'`)+ 全部 `LONG_TERM`;字段含 `ryid`、姓名、`person_category`、**照片**、**特征值**、`feature_algo`、`feature_version`、`checksum` |
| 下发方式 | 设备按 `cursor` + `version` 调用 `GET /open/v1/face/persons`(N-30)**主动拉取**;公寓端同时生成 `FACE_ISSUE` 指令入 `apt_device_command`,由设备心跳取走;媒体走 `GET /open/v1/face/media/{ref}`(N-31,短期签名、设备凭据鉴权) |
| 完整性保证 | 响应按 `face_scope_type=ALL_ACTIVE` 构造,服务端强制拼接 `gyid + (在寓 OR LONG_TERM) + deleted=0`;返回 `total`、`cursor`、`featureVersion` 与集合 `hash`;设备按 `hash` 校验完整性,不符则请求 `FACE_FULL_SYNC` |
| 回执 | 设备逐条或批量 `POST /open/v1/face/ack`(N-32)上报 `commandId` + `result`;公寓端更新 `apt_device_command.state=ACKED` 与 `gy_sb_xt.face_cache_*` |
| 异常 | 设备离线 → 指令排队(`PENDING`),上线心跳取走;回执丢失 → 按 `commandId` 幂等重投,设备重复执行无副作用;媒体下载失败 → 校验和不通过不写缓存,重试并告警 |
> **下发内容准确完整的判据**:`apt_master_person.face_sync_state=SYNCED` + `feature_version` 单调 + `checksum` 校验通过 + 服务端范围过滤 + 设备 `hash` 回执一致,五项缺一即视为未完成,不标记 `ACKED`。
---
## 7. AF-04 消费设备、叫乘设备按设备维度的数据隔离
表 10-16 设备类别 → 可见范围矩阵(服务端强制,设备不可覆盖)
| `device_class` | `face_scope_type` | 可见人员范围 | 可见字段 | 缓存 |
| --- | --- | --- | --- | --- |
| `CHECKIN` 签到设备 | `ALL_ACTIVE` | 全部有效人员(在寓 `TRANSIENT` + `LONG_TERM`) | 全字段(含照片、特征值) | 允许(含 `LONG_TERM` 独立分区) |
| `CONSUME` 消费设备 | `ROOM_BOUND` 或 `CATEGORY_WHITELIST` | 本设备绑定房间/区域的**在寓 `TRANSIENT`** + 该设备白名单内的 `LONG_TERM` | **最小必要字段**:`ryid`、姓名、`person_category`、`feature_ref` / 特征值、`feature_version`;**不含照片** | 允许;离寓即擦除 `TRANSIENT` |
| `CALL` 叫乘设备 | `ROOM_BOUND` | 本设备绑定房间的**在寓 `TRANSIENT`** + 授权 `LONG_TERM` | 同 `CONSUME` | 允许;离寓即擦除 `TRANSIENT` |
| `TABLET` 平板 | `NONE`(按任务临时) | 仅当前叫乘任务涉及人员(按 `jcid` / `taskId`) | 识别所需特征值,不含照片 | 默认 `face_cache_enabled=0`,任务结束即弃用 |
| `OTHER` | `NONE` | 不可见 | — | 禁止 |
表 10-17 三重隔离校验(任一不通过即拒绝)
| 层 | 校验 | 失败返回 |
| --- | --- | --- |
| 1 设备身份 | `deviceCode` + 激活凭据 + HMAC(secret, body) + ±5 min 时间窗 + `nonce` 去重;停用/删除设备直接拒绝 | `40301 APT_FORBIDDEN` |
| 2 数据域 | `gyid` 只从设备凭据解析,**不接受请求参数**;所有 SQL 必带 `gyid` | `40301` |
| 3 范围过滤 | 服务端按 `gy_sb.face_scope_type/value` + `device_class` 拼接 `WHERE`;设备**不得**传 `ryid`、`roomNo` 作为任意查询条件,只能传 `cursor` / `version`;越范围人员自动排除,不返回"存在但无权" | `40917 APT_FACE_SCOPE_DENIED` |
补充规则:
1. **最小必要字段**:消费/叫乘设备不返回照片;特征值按 `feature_algo` 白名单过滤,不兼容的算法不下发并告警。
2. **范围变更**:`face_scope_type/value` 变更时生成 `FACE_SCOPE_RELOAD` 指令;设备须先清空旧范围缓存再按新范围全量拉取,避免残留越权数据。
3. **审计**:每次设备拉取记 `deviceCode`、`gyid`、`cursor`、返回条数、`hash` 与结果,写 `duty_operation_log`;越权尝试单独计数并进 #11 设备摘要。
4. **跨公寓**:设备凭据绑定单一 `gyid`,跨公寓查询一律拒绝;平台不下发其他公寓人员。
---
## 8. AF-05 离寓后自动擦除设备缓存
表 10-18 触发条件与数据范围
| 项 | 口径 |
| --- | --- |
| 触发 | ① `apt-stay` 产生离寓事实:`gy_jcxx.finish_ly='1'` 或 `gygl_ly_event(CHECKOUT)` 落库;② 值班员 `MARK_LEAVE` 命令生效;③ #2 下行人员 `deleted=1`;④ 换乘/换房导致原房间不再在寓;⑤ 平台 `person_category` 由 `LONG_TERM` 改回 `TRANSIENT` 且已离寓 |
| 目标人员 | **仅 `person_category=TRANSIENT` 且已离寓**的人员;`LONG_TERM` **不擦除** |
| 目标设备 | 该人员曾成功下发过的 `CONSUME` / `CALL` / `TABLET` 设备集合(由 `apt_device_command` 中 `biz_type=FACE_ISSUE` 且 `state=ACKED` 反查),以及其绑定房间对应的设备 |
| 不擦除 | `CHECKIN` 签到设备的主档(签到需保留全部有效人员用于下次入住比对,按 `deleted` 标记自然失效)、`LONG_TERM` 分区、`apt_master_person` 快照本身(快照只标 `deleted`,不物理删) |
| 时限 | 离寓事实落库后 ≤ 30s 生成擦除指令;≤ 5 min 完成 ACK;超时告警并升级为 `FACE_FULL_SYNC`(Q-30) |
表 10-19 擦除步骤与处理规则
| 步 | 主体 | 动作 | 落表 | 异常 / 补偿 |
| --- | --- | --- | --- | --- |
| 1 | `apt.face.scope` | 消费离寓事件,按 `ryid` 反查已下发设备集合 | `apt_device_command`、`gy_sb` | 查不到下发记录 → 仍按绑定房间生成兜底擦除,避免漏擦 |
| 2 | `apt.face.dispatch` | 生成 `FACE_ERASE` 指令(`biz_id=ryid`、`erase_reason=CHECKOUT`、`biz_version+1`);`commandId = hash(gyid, deviceCode, FACE_ERASE, ryid, biz_version)` | `apt_device_command(PENDING)` | 同键幂等,重复离寓事件不生成第二条 |
| 3 | 设备(心跳取指令 / N-30 `erase[]`) | 删除本地缓存中该 `ryid` 的照片、特征值、比对索引 | 设备本地 | 设备离线 → 指令排队;上线后优先取 `FACE_ERASE`(P0 优先,重投 5 次不过期,沿用 `docs/公寓端/05` 表 5-3) |
| 4 | 设备 → N-32 | 上送 ACK(`erased=true` + 缓存新 `hash` / `count` / `version`) | `apt_device_command(ACKED)`、`gy_sb_xt` | 回执丢失 → 按 `commandId` 幂等重投,设备重复执行无副作用 |
| 5 | `apt.face.dispatch` | 更新设备缓存台账;比对期望集合与上报 `hash` | `gy_sb_xt.face_cache_*` | 不一致 → 生成 `FACE_FULL_SYNC` 覆盖式全量同步(隐含擦除) |
| 6 | 审计与告警 | 记 `duty_operation_log`(`eraseReason`、设备、时间、结果),失败项进 #11 摘要与盯控告警 | `duty_operation_log`、`apt_report_outbox` | 超时未完成 → `P1` 告警 + 值班台横幅,人工可发 `ERASE` 命令重试 |
表 10-20 兜底与对账
| 机制 | 口径 |
| --- | --- |
| 缓存摘要对账 | 设备每次心跳上送 `face_cache_hash` / `count` / `version`(N-33);公寓端按"权限范围 × 在寓状态 × `LONG_TERM` 白名单"计算期望集合并比对,每 30 min 全量比对一次 |
| 全量同步 | 差异 → `FACE_FULL_SYNC`:设备先清空对应分区,再按最新范围全量拉取;比逐条擦除更可靠,作为最终兜底 |
| 断网 | 离寓事实与擦除指令先落本地库;设备离线期间保留缓存,但**设备侧须在检测到离线时禁用本地人脸放行**(Q-31),恢复后立即取擦除指令 |
| 误擦恢复 | 人员重新入住 → 生成新 `FACE_ISSUE`;不依赖"恢复缓存",按最新快照重新下发 |
| 时效 | 擦除指令 `EXPIRED` 前一直重投;超过 Q-30 时限升级为 `P1` 告警,不自动放弃 |
---
## 9. AF-06 长期驻留人员数据的长期保留与区分管理
表 10-21 区分管理规则
| 维度 | `TRANSIENT`(临时入住/乘务) | `LONG_TERM`(保洁、巡检、管理等长期驻留) |
| --- | --- | --- |
| 标识来源 | #2 下发 `person_category` 默认 `TRANSIENT` | #2 下发或管理端按 `apt_clean_staff` / 巡检与管理人员名册标注,需 `duty:face:category` 权限 + 二次确认 |
| 设备缓存分区 | `TRANSIENT` 分区 | **`LONG_TERM` 独立分区**,与临时人员分区物理/逻辑隔离 |
| 离寓擦除 | **参与 AF-05**,离寓即擦除 | **不参与**;仅在资格撤销、人员 `deleted=1`、设备退役/范围变更时擦除 |
| 可见范围 | 按设备绑定房间/区域在寓状态 | 按 `CATEGORY_WHITELIST` 授权;未列入白名单的设备不可见,即使同为长期人员 |
| 版本与对账 | 随人员主档版本 | 独立 `LONG_TERM` 集合版本,对账时与 `TRANSIENT` 分开计算 `hash` |
| 有效期 | 随入住/离寓周期 | 长期有效;每季度随平台主档全量核对一次,发现失联/离职由平台 `deleted` 触发擦除 |
| 撤销 | — | 平台 `person_category` 变更为 `TRANSIENT` 或 `deleted=1` → 生成 `FACE_ERASE`(`erase_reason=CATEGORY_REVOKED`),**需管理端二次确认**后执行 |
| 审计 | `duty_operation_log` | 同左,且类别变更单独记一条(操作人、理由、前后值) |
> **与其他人员区分的最小实现**:`apt_master_person.person_category` + 设备侧 `cache_partition` + 独立的 `face_cache_hash_long_term`。三者共同保证"长期保留"不会被离寓清理误删,也不会被消费/叫乘设备越权看到。
---
## 10. 同步、清理与一致性机制
表 10-22 版本、幂等与顺序
| 项 | 口径 |
| --- | --- |
| 版本 | `feature_version`(人员特征)、`face_platform_version`(平台)、`face_cache_version`(设备)三级单调;同 `ryid` 只接受 `version >= 本地` 的变更 |
| 幂等键 | 上行 `idempotencyKey = hash(gyid + bizType + bizId + bizVersion)`;设备指令 `commandId = hash(gyid + deviceCode + bizType + bizId + bizVersion)`;媒体按 `checksum` 去重 |
| 注册双路 | 值班员 `CONFIRM` 后,`upload_state`(#9 上行平台)与 `dispatch_state`(`FACE_ISSUE` 下发设备)各自 `PENDING → SENT` / `ISSUED → ACKED`;两条链独立幂等、独立重试、互不阻塞,平台回执不是下发前置(CR-C25、§4.4) |
| 顺序 | 同一 `ryid` 按 `bizVersion` 单调;`FACE_ISSUE` 与 `FACE_ERASE` 并发时**以离寓事件为准**,擦除优先,迟到下发记 `IGNORED` |
| 事务 | 快照写入与 `apt_master_version` 更新在同一事务;媒体下载失败不产生半条记录 |
| 断网 | 本地先落业务表 + outbox;恢复后"先派 outbox、再拉 #7"(CR-01 §2.9 顺序不变),随后补拉 #2 人员增量 |
| 归日 | 跨端上报以 `serverReceivedAt` 为准(沿用 `docs/平台端/06` §6) |
表 10-23 一致性对账
| 层级 | 对账对象 | 周期 | 差异处理 |
| --- | --- | --- | --- |
| 平台 ↔ 公寓 | `(gyid, ryid, feature_version, checksum)` 的 `count/sum/max(version)` | 每日 + 每次 #2 增量 | 平台版本高 → 重拉并重下发;本地版本高 → 补传 `PERSON_REG` |
| 公寓 ↔ 设备 | 期望集合 `hash` vs `gy_sb_xt.face_cache_hash` | 每次心跳 + 每 30 min 全量 | 差异 → `FACE_FULL_SYNC`;持续差异 → `P1` 告警 |
| 人员状态 ↔ 缓存 | 在寓状态 / `person_category` / `deleted` | 每次离寓事件 + 每日 | 多余缓存 → `FACE_ERASE`;缺失 → `FACE_ISSUE` |
| 媒体完整性 | `photo_checksum` / `feature_checksum` | 每次下载/下发 | 不符 → 丢弃重下,连续 3 次告警 |
---
## 11. 权限隔离与安全
表 10-24 权限与安全要求
| # | 要求 |
| --- | --- |
| 1 | **双校验**:管理端 `/apt/face/**` 走 RuoYi 会话 + `@PreAuthorize`,前端 `v-hasPermi` 同码(`apt:face:list` / `apt:face:dispatch` / `apt:face:scope` / `apt:face:category` / `apt:face:reconcile`);值班台 `/duty/person/face-reg/**` 走 `duty:person:reg` + 二次确认;设备侧 `/open/v1/face/**` 走设备凭据 + HMAC + 时间窗 + `nonce` |
| 2 | **`gyid` 锁定**:`gyid` 只从会话/设备凭据解析,不接受请求参数;所有查询/写入 SQL 必带 `gyid`,唯一键以 `gyid` 打头 |
| 3 | **服务端过滤**:设备可见范围一律在服务端按 `gy_sb.face_scope_*` 拼接,设备不得指定 `ryid` / `roomNo` 越权查询;越权 `40917` |
| 4 | **媒体保护**:照片与特征值本地加密落盘,库内只存引用与校验和;跨端与设备侧媒体地址短期签名(≤ 10 min)、仅内网、一次一密;不进平台对象存储的是抓拍/录音,**人员主数据媒体按本模块允许跨端** |
| 5 | **最小必要**:消费/叫乘设备不返回照片;`TABLET` 不长期缓存;签到设备才拿全字段 |
| 6 | **审计**:注册申请、值班员确认/驳回、平台同步、入库、下发、擦除、类别变更、范围变更、对账九类动作全部记 `duty_operation_log`(操作人/设备、`commandId`、前后值、结果);失败与越权单独计数进 #11 摘要 |
| 7 | **职责分离**:人员类别标注与设备范围配置仅公寓管理员可改;值班员只能查看、审核注册与发起 `RE_ISSUE`/`ERASE` 受控命令 |
| 8 | **合规边界**:业务过程抓拍、录音、签到比对帧仍不出公寓(`docs/平台端/07` 表 7-7);识别结果上报不带图像;人脸数据保留期与删除留痕按 Q-32 待决口径执行 |
| 9 | **注册链路职责分离**:签到终端只有"发起申请 + 上传人脸"权限(N-34),**无权直接写人员主档或触发下发**;确认 / 驳回仅值班台 `duty:person:reg` 可用且必须二次确认(N-36 / N-37);`apt.face.registry` 只受理 `reg_state=APPLIED` 的确认,重复确认幂等返回原 `ryid` |
---
## 12. 异常处理策略
表 10-25 异常场景与处理
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 注册申请阶段媒体异常 | 拒收 `40919` / `40920`,不落申请流水、不生成值班台待办 | 不把不完整申请推给值班员 |
| 重复注册申请 | 同一 `work_no` 未决申请返回原 `regId`;已 `CONFIRMED` 返回 `40918` | 不生成第二个 `ryid`、不重复待办 |
| 值班员驳回 | `reg_state=REJECTED` + `reject_reason`,媒体按 Q-32 保留期清理,不生成 `ryid` | 不进主档、不触发双路同步、不下发设备 |
| 注册确认后事务失败 | 整体回滚,`reg_state` 保持 `APPLIED`,值班台待办保留可重试 | 不产生半条人员记录、不触发双路同步 |
| 平台不可达 | `upload_state=PENDING`,outbox 排队、恢复后复用原 `idempotencyKey` 续报;**`dispatch_state` 照常推进,设备下发不受影响**(CR-C25) | 不阻塞设备下发、不伪造已同步状态 |
| 平台驳回注册结果 | `verdict=REJECTED` + `reject_reason`;本地注册与已下发设备**保留**,告警 + 值班台待办 + 暂停后续下发(Q-35) | 不自动回滚注册、不自动擦除设备缓存、不覆盖平台已有档案 |
| 媒体下载失败 / checksum 不符 | 丢弃临时文件、退避重试(3 次)、`face_sync_state=FAILED` + 告警;整批不入库 | 不写入半条人员记录 |
| 特征值算法不兼容 | 人员入库但 `FACE_ALGO_UNSUPPORTED` 告警,不下发该设备;管理端可配置白名单 | 不静默截断特征值 |
| 设备离线 | 指令排队,上线心跳按优先级取走;`FACE_ERASE` 优先级高于 `FACE_ISSUE` | 不向离线设备推送、不标记 ACKED |
| 回执丢失 / 重复 | 按 `commandId` 幂等重投,设备重复执行无副作用 | 不产生第二份业务事实 |
| 设备缓存膨胀 / 摘要不一致 | `FACE_FULL_SYNC` 覆盖式重建;连续不一致升级 `P1` 告警 | 不逐条猜测差异 |
| 擦除超时 | ≤ 5 min 未完成 → `FACE_FULL_SYNC` + 值班台告警;人工可发 `ERASE` 重试 | 不自动放弃、不自动改为"已擦除" |
| 重复注册 | 同 `work_no` 返回 `40918`;平台已有时由平台驳回 `DUPLICATE_WORK_NO` | 不生成第二个 `ryid` |
| 类别误标 / 资格撤销 | 需管理端二次确认后生成 `FACE_ERASE(CATEGORY_REVOKED)`,写前后值审计 | 不由定时任务自动批量撤销 `LONG_TERM` |
| 时钟偏差 | 设备按心跳 `serverTime` 校准;偏差 > 1s 拒绝增量拉取并要求 `FACE_FULL_SYNC` | 不按设备本地时间判定版本新旧 |
| 版本回退 / 乱序 | 旧 `feature_version` 丢弃并记日志;异常回退请求全量重拉 #2 | 不用旧版本覆盖新版本 |
| 跨 `gyid` / 越权 | `40301` / `40917`,记审计与越权计数 | 不返回脱敏后的他人人员数据 |
---
## 13. 接口清单
表 10-26 新增接口(N-25~N-37;`/apt/**` 为管理端、`/duty/**` 为值班台、`/open/v1/face/**` 为设备侧,**均不计入跨端 20 接口**)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 用途 | 落点 |
| --- | --- | --- | --- | --- | --- |
| N-25 | `GET /apt/face/persons` | `apt:face:list` | L0 | 人员人脸主档查询(含注册态、同步状态、类别、版本) | `apt_master_person`、`apt_person_face_reg` |
| N-26 | `POST /apt/face/persons/{ryid}/commands`(`RE_ISSUE` / `ERASE` / `FULL_SYNC`) | `apt:face:dispatch` | **L2** `commandId` | 手工重下发 / 擦除 / 全量同步 | `apt_device_command`、`duty_manual_action` |
| N-27 | `GET /apt/face/devices/{deviceCode}/scope`、`PUT` 同路径 | `apt:face:scope` | L1 | 设备类别与可见范围配置 | `gy_sb.face_scope_*` |
| N-28 | `GET /apt/face/devices/{deviceCode}/cache` | `apt:face:list` | L0 | 设备缓存台账与对账差异 | `gy_sb_xt.face_cache_*` |
| N-29 | `POST /apt/face/reconcile` | `apt:face:reconcile` | **L2** `batchId` | 手动触发平台/设备对账与全量重建 | `apt_master_version`、`apt_device_command` |
| N-30 | `GET /open/v1/face/persons?deviceCode&cursor&version` | 设备凭据 | L0(游标) | 设备按权限范围拉取人员与人脸增量,响应含 `persons[]`、`erase[]`、`total`、`hash` | `apt_master_person`、`gy_sb` |
| N-31 | `GET /open/v1/face/media/{ref}` | 设备凭据 + 短期签名 | L0 | 下载照片 / 特征值(校验和随附) | 本地媒体区 |
| N-32 | `POST /open/v1/face/ack` | 设备凭据 | `commandId` | 下发 / 擦除回执 | `apt_device_command(ACKED)`、`gy_sb_xt` |
| N-33 | `POST /open/v1/face/cache-report` | 设备凭据 | `(deviceCode, version)` | 上报缓存 `hash` / `count` / `version` | `gy_sb_xt.face_cache_*` |
| **N-34** | `POST /open/v1/face/reg-requests`(`REG_APPLY`) | 签到终端设备凭据 | **L2** `regId` | **签到终端发起注册申请并上传人员要素与人脸**;只落 `APPLIED`,不进主档、不下发、不上报 | `apt_person_face_reg` |
| **N-35** | `GET /duty/person/face-reg/pending` | `duty:person:reg` | L0 | 值班台待审申请列表(要素、照片、申请设备、时间) | `apt_person_face_reg` |
| **N-36** | `POST /duty/person/face-reg/{regId}/confirm` | `duty:person:reg` + 二次确认 | **L2** `regId` | **值班员完成人员注册**:生成 `ryid`、入 `apt_master_person`,自动触发平台上传与设备下发两条路径 | `apt_person_face_reg`、`apt_master_person`、`duty_manual_action` |
| **N-37** | `POST /duty/person/face-reg/{regId}/reject` | `duty:person:reg` | **L2** `regId` | 值班员驳回申请(须填 `reject_reason`),媒体按 Q-32 保留期清理 | `apt_person_face_reg`、`duty_operation_log` |
> N-34~N-37 是**注册链路端内接口**,不新增跨端编号;原 CR-02 N-08 `POST /duty/person/face-reg` 保留为"值班台直接登记"的历史入口,按 CR-C25(CR-02 §12 表 3-49)收敛为 N-35 / N-36 的列表 + 确认两步,不再承担"提交即生效"。
表 10-27 跨端接口扩展(**沿用既有编号,不新增、不重排**,CR-C07)
| # | 扩展 | 方向 | 用途 |
| --- | --- | --- | --- |
| 1 | `facePolicy`(范围规则、缓存上限、擦除窗口、`LONG_TERM` 白名单、算法白名单) | 下行 | 公寓端按策略执行下发与清理 |
| 2 | 人员增量 `payload` 增 `photoRef`、`photoChecksum`、`featureRef`、`featureChecksum`、`featureAlgo`、`featureVersion`、`personCategory`、`mediaUrls[]`;`personRegVerdicts[]` 改为**平台同步回执**(`ACCEPTED` / `REJECTED` + `platformVersion`),不再是注册批准 | 下行 | AF-02 下载入库、AF-01 注册结果对齐 |
| 9 | `PERSON_REG` payload 增 `ryid`、`regState=CONFIRMED`、照片与特征值引用、校验和、算法、版本、`personCategory`;媒体本体走签名直传 | 上行 | **AF-01 值班员注册成功后**上报(`APPLIED` 阶段不上报),平台按 `regId` 去重回同步回执 |
| 11 | 设备摘要增 `faceCacheMismatchCount`、`unauthorizedFaceQueryCount` | 上行 | 越权与不一致观测 |
| 14 | 心跳增 `faceSyncWatermark` | 上行 | 版本与积压 |
表 10-28 新增错误码(沿用 `docs/公寓端/04` 表 4-4 分段)
| code | 常量 | 含义 |
| --- | --- | --- |
| 40914 | `APT_FACE_REG_PENDING` | **沿用 CR-02 同码并改判**:注册申请未获**值班员确认**,禁止参与运安匹配与设备下发(CR-C25) |
| 40917 | `APT_FACE_SCOPE_DENIED` | 设备越出可见范围查询人员/人脸 |
| 40918 | `APT_FACE_PERSON_DUPLICATE` | 同 `work_no` 重复注册 |
| 40919 | `APT_FACE_MEDIA_MISSING` | 照片或特征值缺失 / 格式不支持 |
| 40920 | `APT_FACE_MEDIA_CHECKSUM_MISMATCH` | 媒体校验和不符 |
| 40921 | `APT_FACE_ALGO_UNSUPPORTED` | 特征值算法不在兼容白名单 |
| 40922 | `APT_FACE_MEDIA_TOO_LARGE` | 媒体超过大小上限 |
| 40923 | `APT_FACE_NOT_SYNCED` | 人员媒体未同步完成,禁止下发设备(区别于 `40914` 的"注册申请未获值班员确认") |
| 40924 | `APT_FACE_REG_STATE_INVALID` | 注册态不允许该操作(确认已驳回申请、对 `CONFIRMED` 再驳回等) |
| 50303 | `APT_FACE_ERASE_TIMEOUT` | 离寓擦除超时未完成,已升级全量同步 |
---
## 14. 待决策与验收
表 10-29 新增决策点(沿用 `docs/应用端/10` 的决议记录格式:结论 + 决议人 + 日期三者缺一视为未决议)
| 编号 | 待决事项 | 未决默认口径 | 拍板时点 |
| --- | --- | --- | --- |
| Q-30 | 离寓擦除时限与告警级别 | ≤ 30s 生成、≤ 5 min 完成,超时 `P1` + `FACE_FULL_SYNC` | 阶段二契约冻结前 |
| Q-31 | 设备离线期间是否禁用本地人脸放行 | 禁用(离线只保留缓存、不放行),恢复后先取擦除指令 | 阶段三试点前 |
| Q-32 | 人脸主数据保留期与删除留痕合规口径 | 跟随平台 `deleted` 标记;本地快照与审计长期保留,设备侧按 AF-05/AF-06 清理 | 阶段三 D 批开工前 |
| Q-33 | 媒体签名有效期与大小上限 | 签名 ≤ 10 min;单条特征值/照片上限由 `face.*.maxBytes` 配置(默认 64 KB / 512 KB) | 阶段二契约冻结前 |
| Q-34 | `LONG_TERM` 类别由平台统一下发还是公寓本地标注 | 平台下发为准,本地标注需二次确认并上报平台备案 | 阶段三试点前 |
| Q-35 | 平台驳回注册结果(如 `DUPLICATE_WORK_NO`)时是否自动擦除已下发设备缓存 | **不自动擦除**:保留本地注册与设备缓存,告警 + 值班台待办 + 暂停后续下发,由值班员人工归并或撤销;未决期间不影响已完成的叫乘 / 消费业务 | 阶段三试点前 |
表 10-30 验收要点
| # | 验收项 | 判据 |
| --- | --- | --- |
| 1 | 注册链路 | 签到终端 `REG_APPLY` → 值班台待办 → 值班员 `CONFIRM` 后生成 `ryid` 并入 `apt_master_person`;申请阶段不进主档、不参与匹配、不下发设备(`40914`);确认后 **#9 上传平台与 `FACE_ISSUE` 下发设备两条路径均自动触发且互不阻塞**;平台驳回不回滚本地注册(Q-35) |
| 2 | 平台下载 | #2 增量下载照片与特征值,校验和通过才入库;版本单调,失败不产生半条记录 |
| 3 | 签到下发 | `CHECKIN` 设备拿到全部有效人员且 `hash` 与服务端一致;下发内容缺一即不标记 `ACKED` |
| 4 | 权限隔离 | `CONSUME` / `CALL` 设备只拿到本设备范围内人员且不含照片;越权查询返回 `40917`;`gyid` 不可由请求参数指定 |
| 5 | 离寓清理 | `TRANSIENT` 离寓后消费/叫乘设备缓存被擦除并有 ACK;`LONG_TERM` 分区不受影响;超时升级全量同步 |
| 6 | 长期驻留 | 保洁/巡检/管理人员长期保留、独立分区与独立对账;撤销需二次确认 |
| 7 | 一致性 | 平台 ↔ 公寓 ↔ 设备三级版本与 `hash` 对账通过;断网恢复后自动补齐,不产生重复业务事实 |
| 8 | 边界 | 抓拍、录音、签到现场图仍不出公寓;识别结果不带图像;跨端 20 编号与 `v2-draft` 契约形态不变 |
| 9 | 安全与审计 | 九类动作(含注册申请、确认、驳回、双路同步)全部留痕;媒体地址短期签名、仅内网;越权与失败计入 #11 摘要 |
| 10 | 注册权限与幂等 | 签到终端只能申请、值班台 `duty:person:reg` 才能确认 / 驳回且须二次确认;重复申请返回原 `regId`、重复确认幂等返回原 `ryid`;平台不可达时设备下发仍成功,设备离线时平台上报仍成功 |
@@ -0,0 +1,607 @@
# 11 人员入住作业流程
> 文档编号:`docs/公寓端/11`。性质:**设计稿(Design Only)**,不是接口契约冻结稿;阶段二契约冻结评审通过后才落 DDL。
> **落地权威表述**:本文件是"人员入住 → 值班员审核 → 提报平台匹配 → 生成叫乘任务 → 进入叫乘"端到端作业流程的**唯一权威表述**(`CI-01`~`CI-05`、表 11-1~11-35、图 11-1~11-3);口径裁决、施工单与验收登记见 `CR-02 §13`。
> **上位依据**:`docs/10-架构优化方案-三端拆分.md`、`docs/设计变更/01-本地建单与本地调整.md`、`docs/设计变更/03-三端功能设计修订(CR-02).md`(表 3-1 A-03、表 3-3 功能域 1、表 3-14 `CHECKIN` payload、CR-C06 / CR-C07 / CR-C25)、`docs/公寓端/10-人脸数据下发管理.md` §4。
> **硬约束**:跨端仍为 **20 个接口编号(1~17 + 23~25)**、契约 `v2-draft`、平台不回调公寓、下行一律"拉";**不新增业务表**(公寓本地库仍 59 张)、**不新增跨端编号**、`gy_jcxx` 状态字段仍只有 `apt.duty.state` 一个写入口、`jcsj` 恒由公寓按规则快照复算。
> **CR-02 §15 关联(三端一致性)**:本文件仍是入住作业流程的唯一权威表述;应用端与平台端按 §15 表 3-65 同步业务规则、交互流程、权限控制与异常处理,三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**(`match_state` 五值、`checkin_state`、`sync_state` 不变)。平台统计的入住人数取 #9 `CHECKIN` 归集后的 `PL_REPORT_DAILY.stay_count`,**统计结果不回写** `gy_jcxx` / `apt_local_task` 任何状态;本端不新增上报接口与 `bizType`。变更点与影响范围见 CR-02 §15 表 3-63。
表 11-1 文档信息
| 项 | 内容 |
| --- | --- |
| 范围 | 人员入住作业五环节:注册与签到(CI-01)、入住申请(CI-02)、值班员审核(CI-03)、数据提报与匹配(CI-04)、叫乘环节(CI-05) |
| 本端职责 | 签到终端核验与申请受理、值班台审核与房间调整、`apt-stay` 落库、`apt-report` 提报、`apt-dispatch` 拉任务与复算 |
| 协同端 | 平台端(运安计划接入、匹配结论、主档归集)、应用端(值班台审核入口) |
| 编号规则 | 功能项 `CI-01`~`CI-05`;表 `表 11-y`、图 `图 11-y` 篇内连续;端内接口 `N-38`~`N-47`(承接 `docs/公寓端/10` N-37);决策点 `Q-36`~`Q-40`;裁决 `CR-C26`~`CR-C30`、流程 `F-K`(见 CR-02 §13) |
| 术语 | 表名一律小写(`apartment/db/mysql/*.sql` 实际写法);平台库表沿用 `pl_` 前缀;枚举全大写下划线;时间统一 `ts_ms` / ISO-8601 带时区 |
表 11-2 修订记录
| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-11-10 | 首次形成:五环节端到端作业流程、三条状态链、复用表与补列结构、端内接口清单(含请求/响应字段)、跨端 #6 / #7 / #8 / #9 字段、异常与权限、Q-36~Q-40 与验收要点;明确"已入住"为流程态 + 业务事实双重表达,不新增 `gy_jcxx` 状态字段 |
---
## 1. 定位与边界
### 1.1 五个环节与三端职责
表 11-3 环节、主体与落点
| 环节 | 用户动作 | 主责端 / 模块 | 协同端 | 主要落表 | 跨端编号 |
| --- | --- | --- | --- | --- | --- |
| CI-01 注册与签到 | 签到终端扫脸核验身份;未注册人员先完成注册 | 设备侧签到终端 → `apt.face.registry` / 值班台(N-35~N-37) | 平台端(主档同步) | `apt_person_face_reg`、`apt_master_person` | #2 / #9(`PERSON_REG`) |
| CI-02 入住申请 | 选择房间、交路(可确定时)并提交申请 | 签到终端 → `apt-stay`(受控命令) | — | `apt_local_task`(申请阶段记录) | —(端内) |
| CI-03 值班员审核 | 值班台查看申请、判断是否换房、执行房间调整并置"已入住" | 应用端值班台 → `apt-stay`(唯一执行域) | — | `apt_local_task`、`gy_jcxx`、`gygl_ly_event`、`gy_rooms`、`apt_sip_binding`、`duty_manual_action` | —(端内) |
| CI-04 数据提报与匹配 | 公寓提报入住数据;平台取运安数据匹配并确定叫乘任务 | `apt-report` → `platform-ingest` / `platform.plan` | 平台端 | `apt_report_outbox`、`PL_TASK` / `PL_TASK_CHANGE`、`GY_SYN_JCXX` | **#9**(`CHECKIN`)、**#6**、**#8**、**#7** |
| CI-05 叫乘环节 | 公寓查询叫乘任务并进入叫乘 | `apt-dispatch` → `apt.duty.state` | 平台端 | `gy_syn_jcxx`、`gy_jcxx`、`apt_local_task` | **#7** / **#8** |
### 1.2 与既有流程的关系
- 注册链路**不重写**:CI-01 直接复用 `docs/公寓端/10` §4(签到终端 `REG_APPLY` → 值班台待办 → 值班员 `CONFIRM` → 入 `apt_master_person` → 分别上传平台 / 下发设备),注册生效点是值班员确认(CR-C25)。
- 入住落库**不重写**:`apt-stay` 仍是 `gy_jcxx` / `gy_rooms` / `gygl_ly_event` 的唯一写入模块(CR-02 表 3-3 功能域 1);`POST /duty/checkin` 是既有端内入口,本文件新增的 N-38~N-47 只承担"设备侧受理 / 值班台审核 / 关联查询",**不绕过执行域直改状态字段**。
- 匹配流程**不重写**:平台按 `docs/设计变更/01` §2.5~2.7 收 #8 回执作为匹配输入 → 查 `GY_SYN_JCXX` 运安快照 → 命中补 `kcsj` / `jcfs` 并写 `PL_TASK_CHANGE(op=UPSERT)` → 结论只能放 **#7 响应体** `matchVerdict[]`;`match_state` 沿用正式**五值** `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED`。
- 叫乘执行**不重写**:CI-05 只负责"任务生成与进入叫乘"的衔接,两类叫乘(SIP / 平板)的端到端流程权威表述仍是 `docs/公寓端/05` §9。
- 房间调整复用 CR-02 F-C 的换房联动(`gy_rooms` 房态重算 + `apt_sip_binding` 联动 + `ROOM_SWAP` 事件),但**审核期换房不产生换乘单**、不重算 `jcsj`。
### 1.3 不变式
1. 跨端 20 个编号、方向、路径不变;新增数据只走 `payload` 与既有 `bizType`(`CHECKIN` / `CHECKOUT` / `ROOM_SWAP`),契约 `v2-draft`(CR-C07)。
2. 平台零回调:#6 / #7 由公寓 GET 拉取,#8 / #9 由公寓 POST 上行;平台匹配结论不得另开推送通道(R-05)。
3. `gy_jcxx` 的 `ryzt` / `finish_jc` / `finish_ly` / `stoped` **只经 `apt.duty.state` 条件更新**(`where id=? and ryzt=?`);已结束不回写、已离寓不回写、`stoped` 不可逆。
4. **`gy_jcxx` 行在审核通过办理入住时由 `apt-stay` 当场创建**,申请阶段不建行;`taskId` 恒由公寓按 `jcid` 业务编码分配,`plan_key` 只作 `matchedPlanKey`。
5. `jcsj` 及派生(`fjsj = jcsj + 10 min`、`jcbjsj = jcsj + 15 min`、`ylysj = jcsj + lysjgz`)恒由公寓按本地规则快照复算;平台 `PL_TASK.jcsj` 只是基线值,不作为权威、禁止逐项覆写(CR-C06 / D8')。
6. 主数据快照 `apt_master_person` / `apt_master_room` / `apt_master_route` / `gy_syn_jcxx` 只读,唯一写入 `apt-master-client`;本地指定要素不写快照,锁定位复用 `gy_jcxx.jllx`。
7. 设备侧一律主动拉取;结果按 `resultId` / `applyId` / `commandId` 幂等;抓拍帧、人脸原图、签到现场图与录音**不出公寓**,人员主数据照片与特征值按 CR-C24 允许跨端。
8. `gyid` 只从会话 / 设备凭据解析,不接受请求参数;所有 SQL 必带 `gyid`,唯一键以 `gyid` 打头。
---
## 2. 状态流转总览
图 11-1 五环节数据流向
```mermaid
flowchart LR
subgraph DEV["签到终端 / 值班台"]
CK["签到终端 CHECKIN<br/>扫脸核验 · 选房 · 提交申请"]
DUTY["值班台 /duty<br/>审核 · 房间调整"]
end
subgraph AP["公寓服务系统"]
FACE["apt.face.registry<br/>注册申请 / 确认(10 §4)"]
STAY["apt-stay<br/>唯一执行域"]
TASK["apt_local_task<br/>申请单 / 本地要素 / 匹配态"]
JC["gy_jcxx<br/>叫乘任务行"]
ROOM["gy_rooms / gygl_ly_event<br/>房态与入住事件"]
REP["apt-report / outbox"]
DISP["apt-dispatch"]
end
subgraph PL["平台端"]
ING["platform-ingest<br/>#9 CHECKIN 接收"]
PLAN["platform.plan<br/>#6 运安计划"]
PT["PL_TASK / PL_TASK_CHANGE<br/>#7 匹配结论"]
end
CK -->|CI-01 N-38 扫脸核验| FACE
FACE -->|未注册:N-34 申请 → N-36 确认| FACE
CK -->|CI-02 N-39 房间/交路选项 · N-40 提交申请| TASK
TASK -->|待审推送| DUTY
DUTY -->|CI-03 N-44 审核 / N-45 换房| STAY
STAY -->|CHECKED_IN:建 gy_jcxx + CHECKIN 事件 + 房态| JC
STAY --> ROOM
STAY -->|CI-04 #9 CHECKIN| REP
REP --> ING
PLAN -->|#6 计划下行(拉)| DISP
ING --> PT
PT -->|#7 响应体 matchVerdict[](拉)| DISP
DISP -->|#8 回执 + 复算 jcsj| ING
DISP -->|CI-05 任务生成,进入叫乘| JC
```
表 11-4 三条状态链(互不替代)
| 链 | 字段 / 载体 | 取值 | 唯一写入口 | 说明 |
| --- | --- | --- | --- | --- |
| **申请审核态** | `apt_local_task.checkin_state` | `SUBMITTED` / `REVIEWING` / `APPROVED` / `ROOM_ADJUSTED` / `REJECTED` / `CHECKED_IN` / `CLOSED` | `apt-stay`(受控命令) | 本流程新增的**流程态**,描述申请单生命周期;不是 `gy_jcxx` 状态字段(CR-C26) |
| **同步匹配态** | `apt_local_task.sync_state` + `match_state` | `PENDING` / `REPORTED` / `MATCHED` / `UNMATCHED` / `TASK_READY`;`match_state` 五值见 §1.2 | `apt-report` / `apt-dispatch` | 描述 #9 提报、平台匹配与任务就绪;`match_state` 沿用 `docs/设计变更/01` 表 1-17 正式五值 |
| **业务事实态** | `gy_jcxx.finish_ly` + `sjlysj` + `gygl_ly_event` + `ryzt` | `finish_ly='0' AND sjlysj IS NULL` 为有效在寓;`ryzt` `0`~`5` | `apt.duty.state`(状态字段)/ `apt-stay`(事实列) | **没有独立"已入住"状态字段**;"已入住"= `checkin_state=CHECKED_IN` 且有效在寓且已写 `gygl_ly_event(CHECKIN)` |
表 11-5 全流程状态流转
| 序 | 阶段 | `checkin_state` | `sync_state` / `match_state` | 触发 | 写入口与落表 | 后继 / 异常回退 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 扫脸核验 | —(未建单) | — | N-38 返回 `personFound=true` 且 `regState=CONFIRMED` | 只读 `apt_master_person` / `apt_person_face_reg` | 未注册 → 转 `docs/公寓端/10` §4 注册流程,完成后回本步 |
| 2 | 提交申请 | `SUBMITTED` | `PENDING` / `PENDING` | N-40 提交成功,分配 `apply_id` + 预分配 `jcid` | `apt_local_task` 建单(**不建 `gy_jcxx`**) | 校验失败拒收;重复 `deviceEventId` 返回原 `apply_id` |
| 3 | 值班台接单 | `REVIEWING` | 不变 | N-43 读取详情(条件更新带期望态) | `apt_local_task`、`duty_operation_log` | 并发接单按 `biz_version` 抢锁,失败返回 `40926` |
| 4 | 审核通过 / 换房 | `APPROVED` 或 `ROOM_ADJUSTED` | 不变 | N-44 `APPROVE` / `APPROVE_WITH_ROOM_CHANGE`、N-45 房间调整 | `apt_local_task`、`gy_rooms`、`apt_sip_binding`、`gygl_ly_event(ROOM_SWAP)`、`duty_manual_action` | 房间不可住 → `40928`;驳回 → `REJECTED`(终态) |
| 5 | 办理入住 | **`CHECKED_IN`** | `PENDING` | N-44 审核命令提交后由 `apt-stay` 事务完成 | **建 `gy_jcxx` 行**(`finish_ly='0'`、`sjlysj IS NULL`、`ryzt=0`)、`gygl_ly_event(CHECKIN)`、`gy_rooms` 房态、`apt_local_task` | 事务失败整体回滚至 `APPROVED`,不产生半条入住记录 |
| 6 | 提报平台 | `CHECKED_IN` | `REPORTED` | #9 `CHECKIN` 入 outbox 并发送成功 | `apt_report_outbox` / `apt_report_history` | 断网 → outbox `PENDING`,恢复后复用原 `idempotencyKey` 续报;**不阻塞本地叫乘准备** |
| 7 | 平台匹配 | `CHECKED_IN` | `MATCHED` / `UNMATCHED` | #7 响应体带回 `matchVerdict[]` | `apt_local_task.match_state` / `matched_plan_key`、`gy_syn_jcxx` | `UNMATCHED` 保留公寓上报要素不清空;`jllx='1'` → `LOCKED`;`REJECTED` 转值班台待办 |
| 8 | 任务就绪 | `CHECKED_IN` | `TASK_READY` | #7 带回 `op=UPSERT` 且公寓按 `apt_master_rule` 复算 `jcsj` 完成并回 #8 | `gy_jcxx`(`jcsj` / `call_mode` / `dispatch_seq`)、`apt_local_task`、`gy_syn_jcxx_status` | 规则版本升级不回溯,留 `derived_rule_version` |
| 9 | 进入叫乘 | `CHECKED_IN` | `TASK_READY` | `apt-dispatch` 按 `jcsj` + `dispatch_seq` 派发 | `apt.duty.state`(`ryzt` 条件更新) | 两类方式按 `docs/公寓端/05` §9 执行,本流程不重复 |
| 10 | 离寓 / 取消 | `CLOSED` | 终态 | `apt-stay` 离寓事实或值班台取消(仅 `SUBMITTED` / `REVIEWING` 可取消) | `gy_jcxx.finish_ly='1'`、`gygl_ly_event(CHECKOUT)` | `CHECKED_IN` 后取消必须走离寓流程,不得直接改状态 |
图 11-2 申请审核状态机
```mermaid
flowchart LR
S["SUBMITTED"] --> R["REVIEWING"]
R --> A["APPROVED"]
R --> X["REJECTED"]
R --> C["CANCELLED"]
A --> RA["ROOM_ADJUSTED"]
RA --> A
A --> CI["CHECKED_IN"]
RA --> CI
CI --> CL["CLOSED"]
X --> CL
C --> CL
```
> `ROOM_ADJUSTED` 可多次往返 `APPROVED`(每次 `room_adjust_seq + 1`,只追加 `ROOM_SWAP` 事件、不覆盖历史);`REJECTED` / `CANCELLED` / `CLOSED` 为终态,不回写 `SUBMITTED`。
---
## 3. 数据模型
> **不新增业务表**:公寓本地库仍 **59 张**。入住申请单作为 `apt_local_task` 的**申请阶段记录**(`docs/设计变更/01` 表 1-17 的"本地建单"表,天然带 `origin=CHECKIN_ROUTE` / `CHECKIN_PENDING` 与 `match_state`),本流程只补列(设计定义,DDL 待阶段二冻结)。
表 11-6 复用表清单
| 表 | 库 | 唯一写入模块 | 本流程用途 | 上报 |
| --- | --- | --- | --- | --- |
| `apt_local_task` | apartment | `apt-stay`(受控命令)/ `apt-report` / `apt-dispatch`(同步态列) | **入住申请单**、房间与交路选择、审核状态机、匹配状态 | 否(经 #9 / #8) |
| `gy_jcxx` | apartment | `apt-stay`(建行/事实列)、`apt.duty.state`(状态字段) | 审核通过后创建的叫乘任务行 | 是(#9 摘要) |
| `gygl_ly_event` | apartment | `apt-stay` | `CHECKIN` / `CHECKOUT` / `ROOM_SWAP` 事件证据 | 是(#9) |
| `gy_rooms` | apartment | `apt-stay`(房态重算) | 在住人数与房态 | 否(#11 摘要) |
| `apt_sip_binding` | apartment | `apt-stay` | 房间分机绑定联动 | 否 |
| `apt_master_room` / `apt_master_route` / `apt_master_person` | apartment | `apt-master-client` | 房间基线 / 交路模板 / 人员主档(**只读**) | — |
| `apt_person_face_reg` | apartment | `apt.face.registry` | 未注册人员的注册申请与确认(`docs/公寓端/10` §4) | 是(#9 `PERSON_REG`) |
| `apt_report_outbox` / `apt_report_history` | apartment | `apt-report` | #9 `CHECKIN` 上行与续报留痕 | 是 |
| `gy_syn_jcxx` / `gy_syn_jcxx_status` | apartment | `apt-master-client` / `apt-dispatch` | #6 计划快照与 #7 变更水位 | 否 |
| `duty_manual_action` / `duty_operation_log` | duty | 应用端 | 审核 / 换房留痕(操作人、`commandId`、前后值) | 否 |
| `PL_TASK` / `PL_TASK_CHANGE` | platform | `platform.plan` / `platform-ingest` | 平台匹配结论与任务增量 | — |
表 11-7 `apt_local_task` 列级结构(既有列 + 本流程补列)
| 列 | 类型 | 约束 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| `id` / `gyid` / `task_id` | id_ulid / string(32) / string(64) | PK / UK `(gyid, task_id)` | 既有 | `task_id` = 预分配的 `jcid`;审核通过后 `gy_jcxx` 沿用同一值,**不重新分配** |
| `origin` | enum_s | NOT NULL | 既有 | `CHECKIN_ROUTE`(交路已确定)/ `CHECKIN_PENDING`(交路待平台匹配)/ `LOCAL_ASSIGN` / `LOCAL_ADJUST` / `LOCAL_TEMP_EXTRA` |
| `local_version` / `biz_version` | bigint | 默认 `1` | 既有 / **补列** | `local_version` 本地要素版本;`biz_version` 申请单乐观锁与幂等版本,单调 |
| `matched_plan_key` / `match_state` | string(128) / enum_s | 可空 / 默认 `PENDING` | 既有 | `match_state` 五值 `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED`;平台只补 `kcsj` / `jcfs` |
| `base_kcsj` / `base_jcfs` / `origin_jcsj` | ts_ms / string / ts_ms | 可空 | 既有 | 本地指定要素,`jllx_at_override` / `override_reason` / `derived_rule_version` 同源保留 |
| `apply_id` | id_ulid | UK `(gyid, apply_id)` | **补列** | 入住申请单业务键,N-40 幂等键 |
| `device_code` / `device_event_id` | string(64) ×2 | UK `(gyid, device_code, device_event_id)` | **补列** | 签到终端来源与设备侧幂等 |
| `ryid` / `work_no` / `name_snapshot` | string(64) / string(64) / string(128) | NOT NULL | **补列** | 核验通过的人员身份快照(不替代 `apt_master_person`) |
| `face_verify_state` / `verified_ryid` / `verified_at` / `feature_version` | enum_s / string(64) / datetime(3) / bigint | 可空 | **补列** | 扫脸核验结论与所用特征版本;不存抓拍帧与人脸原图 |
| `reg_id` / `reg_state_at_apply` | string(64) / enum_s | 可空 | **补列** | 关联 `apt_person_face_reg`;申请时必须是 `CONFIRMED`(CR-C25) |
| `fjh` / `fjh_final` / `room_adjust_seq` | string(32) ×2 / int | NOT NULL / 默认 `0` | **补列** | 申请房间与调整后房间;每次调整 `+1`,不覆盖历史 |
| `jlid` / `cc` / `jch` / `kcsj_snapshot` | string(64) / string(32) / string(32) / ts_ms | 可空 | **补列** | 交路(可确定时)与快照要素;不确定时 `origin=CHECKIN_PENDING` |
| `checkin_state` / `sync_state` | enum_s ×2 | 默认 `SUBMITTED` / `PENDING` | **补列** | 表 11-4 两条状态链 |
| `source` | enum_s | 默认 `DEVICE` | **补列** | `DEVICE` / `DUTY` / `SELF`(与 CR-02 表 3-14 同字典) |
| `submitted_at` / `reviewed_by` / `reviewed_at` / `review_note` | datetime(3) / string(64) / datetime(3) / string(256) | 可空 | **补列** | 提交与审核留痕 |
| `adjusted_by` / `adjusted_at` / `adjust_reason` | string(64) / datetime(3) / string(256) | 可空 | **补列** | 房间调整留痕 |
| `checked_in_at` / `reported_at` / `matched_at` / `task_ready_at` | datetime(3) ×4 | 可空 | **补列** | 四个关键时间戳,供对账与超时告警 |
| `created_at` / `updated_at` | datetime(3) ×2 | NOT NULL | 既有 | 只追加,不物理删除 |
表 11-8 关键事实表本流程相关列
| 表 | 列 | 写入时点 | 口径 |
| --- | --- | --- | --- |
| `gy_jcxx` | `jcid` / `gyid` / `ryid` / `fjh` | CI-03 办理入住建行 | `jcid` 沿用申请单预分配值;`fjh` 取 `fjh_final` |
| `gy_jcxx` | `finish_ly` / `sjlysj` / `ryzt` | 建行 / 派发时 | `finish_ly='0'`、`sjlysj IS NULL`、`ryzt=0`;状态变更只经 `apt.duty.state` 条件更新 |
| `gy_jcxx` | `jcsj` / `call_mode` / `dispatch_seq` | CI-05 任务就绪 | `jcsj` 恒由公寓按 `apt_master_rule` 复算;`call_mode` = `SIP` / `TABLET`;`dispatch_seq` 单调 |
| `gygl_ly_event` | `jcid` / `event_type` / `event_at` / `device_code` / `device_event_id` | CHECKIN / ROOM_SWAP / CHECKOUT | `event_type` 与 CR-02 表 3-14 同字典;`(device_code, device_event_id)` 幂等 |
| `gy_rooms` | `room_status` / `live_person` / `sip_account` | 入住 / 换房后房态重算 | `room_status` 六值 `1`~`6`;换房时原房 `-1`、新房 `+1` |
| `apt_report_outbox` | `biz_type` / `biz_id` / `idempotency_key` / `payload` | CI-04 | `biz_type=CHECKIN`;`biz_id = jcid + 事件序号`;`idempotencyKey = hash(gyid + CHECKIN + jcid + eventSeq + bizVersion)` |
表 11-9 枚举字典
| 枚举 | 取值 | 说明 |
| --- | --- | --- |
| `checkinState` | `SUBMITTED` / `REVIEWING` / `APPROVED` / `ROOM_ADJUSTED` / `REJECTED` / `CHECKED_IN` / `CANCELLED` / `CLOSED` | 申请审核态(表 11-4) |
| `syncState` | `PENDING` / `REPORTED` / `MATCHED` / `UNMATCHED` / `TASK_READY` | 同步匹配态 |
| `matchState` | `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED` | 沿用 `docs/设计变更/01` 表 1-17 正式五值;`LOCAL_ONLY` 属 `offlineScope`,**不是**匹配状态 |
| `origin` | `CHECKIN_ROUTE` / `CHECKIN_PENDING` / `LOCAL_ASSIGN` / `LOCAL_TEMP_EXTRA` / `LOCAL_ADJUST` | 交路可确定时取 `CHECKIN_ROUTE`,否则 `CHECKIN_PENDING` |
| `reviewVerdict` | `APPROVE` / `APPROVE_WITH_ROOM_CHANGE` / `REJECT` | N-44 审核命令 |
| `faceVerifyState` | `VERIFIED` / `UNVERIFIED` / `NOT_FOUND` | 与 CR-02 表 3-16 同字典 |
| `eventType` | `CHECKIN` / `CHECKOUT` / `ROOM_SWAP` | CR-02 表 3-14 |
| `source` | `DEVICE` / `DUTY` / `SELF` | CR-02 表 3-14 |
---
## 4. CI-01 注册与签到
### 4.1 触发条件与数据范围
表 11-10 触发与前置
| 项 | 口径 |
| --- | --- |
| 触发 | 人员在签到终端发起"扫脸办理入住";终端按设备凭据 + HMAC + ±5 min 时间窗 + `nonce` 调用 N-38 |
| 数据范围 | 设备凭据解析出的 `gyid` / `deviceCode`、`deviceEventId`、现场比对结论(`faceVerifyState` / `verifiedRyid` / `featureVersion`)、`occurredAt`;**不上传抓拍帧、人脸原图、比对帧** |
| 前置 | 设备已注册且人脸缓存为 `SYNCED`(`docs/公寓端/10` §6);同一 `(deviceCode, deviceEventId)` 未处理过 |
| 不触发 | 仅查询、心跳、缓存摘要上报;`faceVerifyState=NOT_FOUND` 且人员明确拒绝注册 |
表 11-11 身份核验步骤
| 步 | 主体 | 动作 | 落表 | 异常 / 补偿 |
| --- | --- | --- | --- | --- |
| 1 | 签到终端 | 本地 1:1 / 1:N 比对,得到 `verifiedRyid` 与 `faceVerifyState` | 设备本地缓存 | 缓存 `hash` 落后 → 先按 `docs/公寓端/10` §6 拉 `FACE_FULL_SYNC` 再比对 |
| 2 | `apt.checkin.verify` | 校验设备凭据、`gyid` 锁定、`nonce`;按 `verifiedRyid` 查 `apt_master_person` | `apt_master_person`(只读) | 越权 `40301`;凭据失效 `40101`;重放 `40901` |
| 3 | `apt.checkin.verify` | 判定注册态:`CONFIRMED` → `PROCEED`;`APPLIED` → `REG_PENDING`;无记录 → `REGISTER_REQUIRED` | `apt_person_face_reg`(只读) | `APPLIED` 不得办理入住(`40914`,CR-C25) |
| 4 | 未注册分支 | 终端展示注册引导 → N-34 `REG_APPLY` → 值班台 N-35 / N-36 完成注册 → 人脸进入公寓服务系统并按范围下发设备 | `apt_person_face_reg`、`apt_master_person`、`apt_device_command` | 注册完成后**回到步 1 重新核验**,不自动沿用本次比对结论(`docs/公寓端/10` §4) |
| 5 | 响应 | 返回人员摘要、`regState`、`nextAction`、`roomOptionsVersion`、`serverTime` | `duty_operation_log` | `LONG_TERM` 人员同样走本流程,不跳过房态校验 |
表 11-12 注册数据字段(复用 `docs/公寓端/10` 表 10-9 ①)
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `gyid` | string(32) | 是 | **只从设备凭据解析**,不接受请求参数 |
| `work_no` / `name` / `unit_code` | string | 是 | 人员要素;值班员确认时可修正 |
| `photo` / `photoChecksum` | binary / string(128) | 是 | 人脸照片本体与校验和;本体走短期签名直传 |
| `feature` / `featureChecksum` / `featureAlgo` / `featureVersion` | binary / string(128) / string(32) / long | 是 | 特征值与算法版本;仅传照片时由公寓端按 `featureAlgo` 提取 |
| `apply_device_code` / `occurred_at` | string(64) / datetime(3) | 是 | 签到终端来源与采集时间 |
| `reg_id` | string(64) | 响应 | 幂等键;同一 `work_no` 未决申请返回原值 |
### 4.2 接口:N-38 扫脸身份核验
表 11-13 N-38 `POST /open/v1/checkin/verify` 请求 / 响应字段
| 方向 | 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| 请求 | `deviceCode` / `deviceEventId` | string(64) ×2 | 是 | 设备凭据解析 + 设备侧幂等键 |
| 请求 | `faceVerifyState` / `verifiedRyid` / `featureVersion` | enum / string(64) / long | 是 | 只传识别结论与版本,**不带图像** |
| 请求 | `occurredAt` / `nonce` / `signature` | datetime / string / string | 是 | HMAC + 时间窗 + 防重放 |
| 响应 | `personFound` / `ryid` / `workNo` / `name` / `personCategory` | boolean / string ×4 | 是 | 人员摘要(`personCategory` 决定是否长期驻留) |
| 响应 | `regState` / `regId` | enum / string(64) | 是 | `CONFIRMED` / `APPLIED` / `NOT_FOUND`;`APPLIED` 时带 `regId` 供查询进度 |
| 响应 | `nextAction` | enum | 是 | `PROCEED` / `REG_PENDING` / `REGISTER_REQUIRED` / `REJECTED` |
| 响应 | `roomOptionsVersion` / `serverTime` | long / datetime | 是 | 供 N-39 带版本拉取房间 / 交路选项,避免缓存错配 |
表 11-14 CI-01 异常与错误码
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 比对未命中 `NOT_FOUND` | 返回 `REGISTER_REQUIRED` 并引导 N-34 注册 | 不自动建人员、不建申请单 |
| 注册申请未确认 `APPLIED` | `40914`(CR-C25),返回 `REG_PENDING` + `regId` | 不参与入住、不参与运安匹配 |
| 设备人脸缓存过期 | `40927` 要求 `FACE_FULL_SYNC` 后重试 | 不用过期缓存放行 |
| 抓拍上传 / 凭据越 `gyid` | `40919` / `40301` 拒收并记审计 | 不落库、不生成待办 |
| 注册后回到核验 | 重新调用 N-38,服务端重新校验注册态与缓存版本 | 不复用注册前的比对结论 |
---
## 5. CI-02 入住申请
### 5.1 申请单数据结构
表 11-15 入住申请单字段(落 `apt_local_task` 补列,对应表 11-7)
| 字段 | 类型 | 必填 | 校验规则 |
| --- | --- | --- | --- |
| `applyId` | id_ulid | 响应生成 | 服务端生成;客户端可用 `deviceEventId` 作幂等键 |
| `deviceEventId` / `deviceCode` | string(64) ×2 | **必填** | UK `(gyid, device_code, device_event_id)`;重复提交返回原 `applyId` 与快照,不新建单 |
| `ryid` / `workNo` | string(64) ×2 | **必填** | 必须来自 N-38 核验通过的人员;与 `apt_master_person` 一致;`regState=CONFIRMED` |
| `faceVerifyState` / `featureVersion` | enum / long | **必填** | 必须为 `VERIFIED`;服务端按 N-38 会话留痕复核,不接受客户端自造新值 |
| `fjh` | string(32) | **必填** | 必须存在于 `apt_master_room`,且 `gy_rooms.room_status` 允许入住(`1` 空闲 / `5` 未满),`live_person < 床位数` |
| `jlid` | string(64) | **可选** | 交路可确定时填,须存在于 `apt_master_route`;填写后 `origin=CHECKIN_ROUTE` |
| `cc` / `jch` / `kcsjSnapshot` | string(32) ×2 / datetime | 可选 | 与 `jlid` 同组;只作快照,不写 `apt_master_route` |
| `source` | enum | **必填** | `DEVICE` / `DUTY` / `SELF`;设备侧提交恒 `DEVICE`,服务端可覆盖 |
| `occurredAt` | datetime(3) | **必填** | 与 N-38 `occurredAt` 间隔超 5 min 需重新核验(`40929`) |
| `remark` | string(256) | 可选 | 备注;不参与匹配与校验 |
| `bizVersion` | long | 响应生成 | 乐观锁,审核 / 调整命令必须带期望值 |
> 未填 `jlid` 时 `origin=CHECKIN_PENDING`:入住照常办理,交路要素留待 CI-04 由平台按运安快照匹配补齐(`kcsj` / `jcfs`),**不阻塞入住**、不预置 `jcsj`。
### 5.2 接口与校验
表 11-16 N-39 / N-40 / N-41 请求 / 响应字段
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| N-39 | `GET /open/v1/checkin/options?deviceCode&version` | 设备凭据 | L0(版本) | `version`(可选,来自 N-38 `roomOptionsVersion`) | `rooms[]{fjh, bedCount, roomStatus, livePerson, sipBound}`、`routes[]{jlid, cc, jch, kcsj}`、`version`、`serverTime`;版本未变返回 `304` 语义的 `notModified=true` |
| N-40 | `POST /open/v1/checkin/applications` | 设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | 表 11-15 全部必填 + 可选字段 | `applyId`、`jcid`(预分配)、`checkinState=SUBMITTED`、`origin`、`bizVersion`、`serverTime`;重复提交返回原单快照 |
| N-41 | `GET /open/v1/checkin/applications/{applyId}` | 设备凭据 | L0 | `applyId` | `checkinState`、`syncState`、`fjh` / `fjhFinal`、`reviewNote`、`estimatedRoomStatus`、`serverTime`;**不返回人脸数据** |
表 11-17 提交校验规则(服务端按序执行)
| 序 | 校验 | 失败码 | 说明 |
| --- | --- | --- | --- |
| 1 | 设备凭据 / `gyid` 锁定 / 时间窗 / `nonce` | `40101` / `40301` / `40901` | `gyid` 不接受请求参数 |
| 2 | `deviceEventId` 幂等 | 返回原 `applyId` | 不新建单,不重复推送待办 |
| 3 | 人员已 `CONFIRMED` 且 `faceVerifyState=VERIFIED` | `40914` / `40930` | 注册未确认 / 核验结论不符 |
| 4 | N-38 会话未过期(≤ 5 min) | `40929` | 要求重新扫脸核验 |
| 5 | 房间可入住且 `live_person < 床位数` | `40928` | `room_status` 为 `2` / `3` / `4` / `6` 时拒收 |
| 6 | `jlid` 存在且与 `cc` / `jch` 自洽 | `40931` | 不写主数据快照 |
| 7 | 同一 `ryid` 无有效在寓记录(`finish_ly='0' AND sjlysj IS NULL`) | `40932` | 防重复入住;确需换房走 CI-03 房间调整 |
| 8 | 写申请单 + 预分配 `jcid` + 推值班台待办 | — | 事务内完成;**不建 `gy_jcxx` 行** |
> 断网时签到终端本地排队,恢复后续传(复用原 `deviceEventId`);服务端不因重复上报生成第二个 `applyId` 或第二次待办。
---
## 6. CI-03 值班员审核
### 6.1 审核状态机与房间调整
表 11-18 审核状态机(唯一写入口 `apt-stay`,条件更新带 `bizVersion` 期望值)
| 当前态 | 命令 | 目标态 | 前置 | 落表变更 |
| --- | --- | --- | --- | --- |
| `SUBMITTED` | N-43 读取详情 | `REVIEWING` | 值班员 `duty:checkin:review` | `reviewed_by` / 接单时间、`duty_operation_log` |
| `REVIEWING` | N-44 `APPROVE` | `APPROVED` → `CHECKED_IN` | 房间仍可住、人员仍有效 | 见下方"办理入住事务" |
| `REVIEWING` | N-44 `APPROVE_WITH_ROOM_CHANGE` | `ROOM_ADJUSTED` | 目标房间可住 | 见"房间调整变更清单";可多次往返 `APPROVED` |
| `APPROVED` / `ROOM_ADJUSTED` | N-45 房间调整 | `ROOM_ADJUSTED` | 未办理入住(`gy_jcxx` 尚未建行) | `room_adjust_seq + 1`、只追加事件 |
| `REVIEWING` / `APPROVED` | N-44 `REJECT` | `REJECTED`(终态) | 必须填 `review_note` | `review_note`、`duty_manual_action`;不建 `gy_jcxx` |
| `SUBMITTED` / `REVIEWING` | 值班台取消 | `CANCELLED`(终态) | 未办理入住 | 不建 `gy_jcxx`;终端 N-41 可见 |
| 任意终态 | 任何审核命令 | 拒绝 `40926` | — | 返回原态快照,不回写 |
**办理入住事务**(`apt-stay` 单事务,失败整体回滚至 `APPROVED`):
`checkin_state=CHECKED_IN` → 建 `gy_jcxx` 行(沿用预分配 `jcid`,`finish_ly='0'`、`sjlysj IS NULL`、`ryzt=0`、`fjh=fjh_final`)→ 写 `gygl_ly_event(CHECKIN)` → `gy_rooms` 房态重算 → `apt_sip_binding` 联动 → 写 `checked_in_at` → 生成 #9 `CHECKIN` 上报项入 `apt_report_outbox`。
表 11-19 房间调整数据变更清单
| # | 对象 | 变更 | 约束 |
| --- | --- | --- | --- |
| 1 | `apt_local_task` | `fjh_final` 更新、`room_adjust_seq + 1`、`adjusted_by` / `adjusted_at` / `adjust_reason` | **保留 `fjh` 原值**;只追加不覆盖历史 |
| 2 | `gy_rooms`(原房 / 新房) | 原房 `live_person - 1`、新房 `+ 1`,两房 `room_status` 重算 | 由 `apt-stay` 房态重算执行,值班台不直改 |
| 3 | `apt_sip_binding` | 房间主分机绑定按新 `fjh` 联动 | 复用 CR-02 F-C 换房联动;离线分机按 §10 告警,不阻断入住 |
| 4 | `gygl_ly_event` | 追加 `ROOM_SWAP` 事件(`jcid` 沿用预分配值) | `(device_code, device_event_id)` 幂等,只追加 |
| 5 | `duty_manual_action` / `duty_operation_log` | 记操作人、`commandId`、前后房间、理由 | 二次确认留痕 |
| 6 | #9 `CHECKIN` payload | 若已上报,追加 `eventType=ROOM_SWAP` 事件(`bizId = jcid + 新事件序号`) | 沿用 CR-02 表 3-14;**不新增跨端编号** |
| 7 | `jcsj` | **不因房间调整重算** | `jcsj` 由 `apt_master_rule` 按要素派生;要素未变则不动(CR-C06) |
### 6.2 接口
表 11-20 N-42~N-46 请求 / 响应字段
| 编号 | 方法 + 路径 | 权限 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| N-42 | `GET /duty/checkin/applications?state&cursor&limit` | `duty:checkin:list` | L0 | `state`(`SUBMITTED` / `REVIEWING`)、`cursor`、`limit` | `items[]{applyId, ryid, workNo, name, fjh, jlid, origin, checkinState, submittedAt, faceVerifyState}`、`cursor`、`total`、`serverTime` |
| N-43 | `GET /duty/checkin/applications/{applyId}` | `duty:checkin:review` | L0 | `applyId` | 申请单全字段 + `roomStatus` / `bedCount` / `livePerson`、`routeSnapshot`、注册态、`bizVersion`、**可选房间列表** `roomCandidates[]`;不含人脸媒体本体 |
| N-44 | `POST /duty/checkin/applications/{applyId}/review` | `duty:checkin:review` + 二次确认 | **L2** `commandId` | `verdict`(`APPROVE` / `APPROVE_WITH_ROOM_CHANGE` / `REJECT`)、`expectedBizVersion`、`reviewNote`、`targetFjh`(换房时)、`commandId` | `applyId`、`jcid`、`checkinState`、`fjhFinal`、`bizVersion`、`reviewedAt`;`APPROVE` 成功即含"已入住"标记 |
| N-45 | `POST /duty/checkin/applications/{applyId}/room-adjust` | `duty:checkin:adjust` | **L2** `commandId` | `targetFjh`、`reason`、`expectedBizVersion`、`commandId` | `applyId`、`fjh`、`fjhFinal`、`roomAdjustSeq`、`roomStatusBefore/After`、`sipBound` |
| N-46 | `GET /duty/checkin/applications/{applyId}/timeline` | `duty:checkin:list` | L0 | `applyId` | `events[]{state, fromState, at, actor, commandId, note}`、`currentState`、`currentSyncState`、`matchState` |
图 11-3 审核与办理入住时序
```mermaid
sequenceDiagram
autonumber
participant CK as 签到终端
participant S as apt-stay(受控命令)
participant T as apt_local_task
participant D as 值班台 /duty
participant J as gy_jcxx / gy_rooms / gygl_ly_event
participant R as apt-report / outbox
CK->>S: N-40 提交申请(deviceEventId)
S->>T: 建申请单 SUBMITTED + 预分配 jcid
S->>D: 推待办 checkin.apply
D->>S: N-43 读取详情(REVIEWING)
alt 需要换房
D->>S: N-45 房间调整(targetFjh + 二次确认)
S->>T: ROOM_ADJUSTED(room_adjust_seq+1)
S->>J: 房态重算 + apt_sip_binding 联动 + ROOM_SWAP 事件
end
D->>S: N-44 APPROVE(expectedBizVersion)
S->>J: 单事务:建 gy_jcxx(finish_ly=0, ryzt=0)+ CHECKIN 事件 + 房态
S->>T: CHECKED_IN + checked_in_at
S->>R: 生成 #9 CHECKIN 上报项
S-->>D: 已入住(applyId / jcid / checkinState)
S-->>CK: N-41 可查状态
```
> **职责分离**:签到终端只能提交申请、值班台只能提交审核命令,二者均**不直写** `gy_jcxx` / `gy_rooms` / `gygl_ly_event`;`apt.duty.state` 仍由业务状态机单独调用。
---
## 7. CI-04 数据提报与匹配
### 7.1 提报数据格式
表 11-21 #9 `POST /platform/v1/report/duty-results`(`bizType=CHECKIN`)payload 字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `envelope` | object | 是 | `contractVersion=v2-draft`、`gyId`(凭据解析)、`bizType=CHECKIN`、`bizId = jcid + 事件序号`、`idempotencyKey = hash(gyid + CHECKIN + jcid + eventSeq + bizVersion)`、`occurredAt`、`serverReceivedAt` |
| `jcid` / `ryid` / `roomId` | string | 是 | 与 CR-02 表 3-14 同名字段;`roomId` 取 `fjh_final` |
| `eventType` / `eventAt` / `source` | enum / datetime / enum | 是 | `CHECKIN` / `CHECKOUT` / `ROOM_SWAP`;`source=DEVICE` / `DUTY` / `SELF` |
| `applyId` / `origin` | string / enum | 是 | **本流程扩展**:关联申请单;`CHECKIN_ROUTE` / `CHECKIN_PENDING` |
| `jlid` / `cc` / `jch` / `kcsj` | string ×3 / datetime | 否 | 交路可确定时带上;不可确定时省略,由平台按运安快照补齐 |
| `cardNo` / `workNo` | string | 是 | **平台匹配输入**;与 #8 回执共同构成匹配键 |
| `faceVerifyState` | enum | 否 | `VERIFIED`;不携带任何图像 |
| `ruleVersion` / `bizVersion` | long ×2 | 是 | 本地规则版本与申请单版本,供对账与幂等 |
| `transferId` | string | 否 | 换乘联动时沿用 CR-02 表 3-14 |
> 断网时先落 `apt_report_outbox(PENDING)`,恢复后按原 `idempotencyKey` 续报(CR-02 F-E);**平台不可达不阻塞本地入住事实与叫乘准备**。
### 7.2 平台匹配规则
表 11-22 平台侧处理链路与匹配规则
| 步 | 主体 | 动作 | 跨端编号 | 落表 |
| --- | --- | --- | --- | --- |
| 1 | `platform-ingest` | 收 #9 `CHECKIN`,按 `bizId` / `idempotencyKey` 去重,落接收日志与主档归集 | #9 | `PL_RECEIVE_LOG`、平台人员入住归集 |
| 2 | `platform-worker` | 定时接运安 SOAP 乘务计划,落快照并生成 / 更新任务 | 内部 | `GY_SYN_JCXX`、`PL_TASK`、`PL_TASK_CHANGE` |
| 3 | 公寓 `apt-master-client` | **主动拉**乘务计划 | **#6** `GET /platform/v1/plans/duty?gyId&from&to` | `gy_syn_jcxx`、`gy_syn_jcxx_status` |
| 4 | 公寓 `apt-dispatch` | 回任务接收回执(含本地要素登记);**#8 是平台匹配的输入,联网时下一上报周期必发** | **#8** `POST /platform/v1/ack/tasks` | `apt_report_outbox`(`biz_type=TASK_ACK`) |
| 5 | `platform.plan` | 按 `gyid + cardNo/workNo + 日期 + 车次(cc/jch) + 交路(jlid)` 查 `GY_SYN_JCXX` 快照;命中则补 `kcsj` / `jcfs`,写 `PL_TASK_CHANGE(op=UPSERT)` | 内部 | `PL_TASK`、`PL_TASK_CHANGE` |
| 6 | 公寓 `apt-dispatch` | **主动拉**任务变更增量,匹配结论随 **#7 响应体** `matchVerdict[]` 下行(与 `replayHint` 同级) | **#7** `GET /platform/v1/tasks/changes?gyId&cursor` | `gy_syn_jcxx_status`、`apt_local_task` |
| 7 | 公寓 `apt-dispatch` | 按 `apt_master_rule` **复算 `jcsj`** 与派生时刻,回 #8 `computedCallTime`;规则升级不回溯,留 `derived_rule_version` | **#8** | `gy_jcxx`、`apt_local_task` |
匹配判定:
- **命中** → `matchVerdict.verdict=MATCHED`,`match_state=MATCHED`,补 `matchedPlanKey` / `kcsj` / `jcfs`;`PL_TASK_CHANGE(op=UPSERT)`。
- **未命中** → `UNMATCHED`:**保留公寓上报的要素不清空**、不删除既有叫乘任务(D8' 例外①);下一 #8 周期可重试。
- **本地锁定** `gy_jcxx.jllx='1'` → `LOCKED`,不覆盖本地指定要素(D8' 例外②)。
- **冲突/驳回** → `REJECTED` + `rejectReason`,转值班台待办,不自动改本地入住事实。
- **禁止**:平台生成任务行、平台产出权威 `jcsj`、平台回调公寓、为匹配结论新开推送通道。
表 11-23 #7 响应体匹配结果字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `changes[]` | array | `op=UPSERT` / `CANCEL`、`taskId`(= `jcid`)、`jlid` / `cc` / `jch`、`kcsj` / `jcfs`、`baseCallTime`(**仅基线**)、`ruleVersion`、`bizVersion` |
| `matchVerdict[]` | array | `taskId` / `applyId`、`verdict` = `MATCHED` / `UNMATCHED` / `REJECTED`、`matchedPlanKey`、`kcsj` / `jcfs`、`matchState`、`rejectReason`、`matchedAt` |
| `cursor` / `serverTime` / `replayHint` | long / datetime / object | 水位、服务端时间与重放提示;与 `docs/设计变更/02` R-05 同级 |
| `dryRun` / `replay` | boolean | 沿用 `docs/公寓端/09` 表 9-4 约定 |
表 11-24 平台侧接口字段(既有编号,不新增)
| # | 接口 | 方向 | 请求 / 响应要点 |
| --- | --- | --- | --- |
| #9 | `POST /platform/v1/report/duty-results` | 上行 | 表 11-21 全量 payload;响应 `received` / `bizId` / `serverReceivedAt` / `duplicate` |
| #6 | `GET /platform/v1/plans/duty?gyId&from&to` | 下行(拉) | 响应 `plans[]{jlid, cc, jch, kcsj, ruleVersion, bizVersion}`、`cursor`、`serverTime` |
| #8 | `POST /platform/v1/ack/tasks` | 上行 | `taskId` / `applyId` / `cardNo` / `workNo` / `cc` / `jch` / `date` / `computedCallTime` / `derivedRuleVersion`;响应 `ack` / `nextRetryHint` |
| #7 | `GET /platform/v1/tasks/changes?gyId&cursor` | 下行(拉) | 表 11-23;响应 `changes[]` / `matchVerdict[]` / `cursor` / `replayHint` |
表 11-25 匹配状态与落表映射
| `matchVerdict.verdict` | `apt_local_task.match_state` | `sync_state` | 公寓侧后续动作 |
| --- | --- | --- | --- |
| `MATCHED` | `MATCHED` | `MATCHED` → `TASK_READY` | 复算 `jcsj`、写 `gy_jcxx`、回 #8、进入 CI-05 |
| `UNMATCHED` | `UNMATCHED` | `REPORTED` | 保留本地要素,等下一 #8 周期;超时告警(Q-37) |
| —(`jllx='1'`) | `LOCKED` | `REPORTED` | 不覆盖本地锁定要素,仍可进入本地叫乘 |
| `REJECTED` | `REJECTED` | `REPORTED` | 值班台待办 + 告警,不改入住事实 |
---
## 8. CI-05 叫乘环节
### 8.1 任务查询接口与返回结构
表 11-26 #7 `GET /platform/v1/tasks/changes?gyId&cursor` 查询契约
| 项 | 口径 |
| --- | --- |
| 触发 | `apt-dispatch` 周期拉取;入住单 `sync_state ∈ {REPORTED, MATCHED}` 时提高拉取频率;心跳 / 水位变更可触发;管理端可手工 `FULL_SYNC` |
| 方向 | 公寓 **GET 拉取**,平台零回调;断网保留 `cursor` 本地水位,恢复后从上次水位续拉 |
| 幂等 | `cursor` 单调 + `bizVersion` 去重;重复 `UPSERT` 不产生第二条任务;`CANCEL` 按 `taskId` 幂等 |
| 前置 | `checkin_state=CHECKED_IN`、`gy_jcxx.finish_ly='0'` 且 `sjlysj IS NULL`;**已离寓不回写** |
表 11-27 叫乘任务返回数据结构
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `taskId` / `applyId` / `jcid` | string(64) ×3 | 是 | 三键一致:`taskId = jcid`,与申请单 `applyId` 一对一 |
| `ryid` / `workNo` / `fjh` | string ×3 | 是 | 人员与(调整后)房间;SIP 模式据此解析 `apt_sip_binding` |
| `jlid` / `cc` / `jch` / `kcsj` / `jcfs` | string ×3 / datetime / string | 是(`MATCHED` 时) | 平台补齐要素;`kcsj` 是要素不是叫乘时刻 |
| `baseCallTime` | datetime | 否 | `PL_TASK.jcsj` 基线值,**不作为权威**(CR-C06) |
| `jcsj` | datetime | 服务端复算后 | 恒由公寓按 `apt_master_rule` 复算;`fjsj` / `jcbjsj` / `ylysj` 同规则派生 |
| `callMode` / `dispatchSeq` | enum / long | 是 | `SIP` / `TABLET`;`dispatchSeq` 按 `jcsj` 单调 |
| `ruleVersion` / `derivedRuleVersion` / `bizVersion` | long ×3 | 是 | 规则版本与派发版本;版本升级不回溯 |
| `matchState` / `matchedPlanKey` | enum / string | 是 | 表 11-25 |
| `op` | enum | 是 | `UPSERT` / `CANCEL` |
表 11-28 与入住状态的关联关系
| 关联 | 判据 | 不允许 |
| --- | --- | --- |
| 申请单 → 任务行 | `apply_id` ↔ `jcid` 一对一;`gy_jcxx` 沿用预分配 `jcid` | 一单多任务行、审核前建 `gy_jcxx` |
| 入住 → 任务生成 | 仅 `checkin_state=CHECKED_IN` 且有效在寓(`finish_ly='0' AND sjlysj IS NULL`)才生成叫乘任务 | `REJECTED` / `CANCELLED` / `CLOSED` 后仍派发 |
| 匹配 → 派发 | `MATCHED` 走完整任务;`UNMATCHED` / `LOCKED` 保留本地要素仍可按本地规则派发(D8' 例外) | 因 `UNMATCHED` 清空本地要素或删除既有任务 |
| 离寓 → 终态 | `apt-stay` 写 `finish_ly='1'` 后 `checkin_state=CLOSED`,`apt.duty.state` 不再回写 | 已离寓回写叫乘状态 |
| 换房 → 任务 | `fjh_final` 决定房间与 SIP 分机;房间调整不改 `jcid`、不重算 `jcsj` | 换房生成新 `jcid` |
表 11-29 N-47 `GET /apt/checkin/{jcid}/call-task` 请求 / 响应字段
| 方向 | 字段 | 类型 | 说明 |
| --- | --- | --- | --- |
| 请求 | `jcid` / `gyid`(凭据)/ `includeTimeline` | string / string / boolean | 管理端与值班台共用,`apt:checkin:query`;`gyid` 不接受请求参数 |
| 响应 | `applyId` / `checkinState` / `syncState` / `matchState` | string / enum ×3 | 申请单与匹配态 |
| 响应 | `task` | object | 表 11-27 全量字段(`taskId` / `jcsj` / `callMode` / `dispatchSeq` / `ruleVersion` 等) |
| 响应 | `roomAdjustSeq` / `fjh` / `fjhFinal` | int / string ×2 | 换房历史摘要 |
| 响应 | `timeline[]` / `serverTime` | array / datetime | 仅 `includeTimeline=true` 时返回 |
> N-47 是**只读端内查询**,不改变状态;两类叫乘的执行流程(SIP 的 AMI 事件上报、平板的 `jcsj` 本地播放与刷脸签到)仍以 `docs/公寓端/05` §9 为权威表述。
---
## 9. 接口清单汇总
表 11-30 端内接口 N-38~N-47(**均不计入跨端 20 个编号**)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 环节 | 用途 |
| --- | --- | --- | --- | --- | --- |
| N-38 | `POST /open/v1/checkin/verify` | 设备凭据 + HMAC | `deviceEventId` | CI-01 | 扫脸身份核验与注册态判定 |
| N-39 | `GET /open/v1/checkin/options` | 设备凭据 | L0(版本) | CI-02 | 拉取可选房间与交路 |
| N-40 | `POST /open/v1/checkin/applications` | 设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | CI-02 | 提交入住申请 |
| N-41 | `GET /open/v1/checkin/applications/{applyId}` | 设备凭据 | L0 | CI-02 / CI-03 | 终端查询申请状态 |
| N-42 | `GET /duty/checkin/applications` | `duty:checkin:list` | L0(游标) | CI-03 | 值班台待审列表 |
| N-43 | `GET /duty/checkin/applications/{applyId}` | `duty:checkin:review` | L0 | CI-03 | 审核详情(含房态与候选房间) |
| N-44 | `POST /duty/checkin/applications/{applyId}/review` | `duty:checkin:review` + 二次确认 | **L2** `commandId` | CI-03 | 审核通过 / 换房通过 / 驳回,办理入住 |
| N-45 | `POST /duty/checkin/applications/{applyId}/room-adjust` | `duty:checkin:adjust` | **L2** `commandId` | CI-03 | 房间调整 |
| N-46 | `GET /duty/checkin/applications/{applyId}/timeline` | `duty:checkin:list` | L0 | CI-03 | 状态流转时间线 |
| N-47 | `GET /apt/checkin/{jcid}/call-task` | `apt:checkin:query` | L0 | CI-05 | 入住单 ↔ 叫乘任务关联查询(只读) |
> 注册相关接口 N-34~N-37 见 `docs/公寓端/10` 表 10-26;通用约定(前缀、错误码、`data.reason` / `data.traceId`、`gyid` 锁定)见 `docs/公寓端/04` §1。
表 11-31 跨端接口(**沿用既有编号,不新增、不重排**)
| # | 接口 | 方向 | 本流程用途 | 字段表 |
| --- | --- | --- | --- | --- |
| 2 | `GET /platform/v1/master/persons` | 下行 | 主档人员增量(CI-01 核验依据) | `docs/公寓端/10` 表 10-27 |
| 6 | `GET /platform/v1/plans/duty?gyId&from&to` | 下行(拉) | 运安乘务计划(CI-04) | 表 11-24 |
| 7 | `GET /platform/v1/tasks/changes?gyId&cursor` | 下行(拉) | 任务变更 + 匹配结论 + 叫乘任务查询(CI-04 / CI-05) | 表 11-23、表 11-27 |
| 8 | `POST /platform/v1/ack/tasks` | 上行 | 匹配输入与复算回执(CI-04 / CI-05) | 表 11-24 |
| 9 | `POST /platform/v1/report/duty-results` | 上行 | `CHECKIN` / `ROOM_SWAP` 入住事实(CI-04) | 表 11-21 |
| 9 | 同上,`bizType=PERSON_REG` | 上行 | 注册结果同步(CI-01) | `docs/公寓端/10` 表 10-27 |
---
## 10. 异常处理、一致性与权限
表 11-32 异常处理
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 签到终端断网 | 本地排队 `deviceEventId`,恢复后续传;N-39 用本地 `version` 兜底 | 不伪造"已提交"、不生成第二个 `applyId` |
| 值班台推送失败 | `duty.ws` 60s 兜底轮询 N-42 | 不重复待办 |
| 并发审核 / 换房 | `expectedBizVersion` 条件更新,冲突 `40926` 返回原快照 | 不用后写覆盖前写 |
| 房间在审核期间被占 | `40928` 要求重新选房(N-45) | 不超床位办理入住 |
| 办理入住事务失败 | 整体回滚至 `APPROVED`,不写 `gy_jcxx` / 事件 / 房态 | 不产生半条入住记录 |
| 平台不可达 | #9 进 outbox `PENDING`,恢复后复用原 `idempotencyKey` | 不阻塞入住事实与本地叫乘准备 |
| `UNMATCHED` 超时 | 告警 + 值班台待办,继续按本地要素派发 | 不清空本地要素、不删既有任务(D8') |
| #7 重复 / 乱序 | `cursor` 单调 + `bizVersion` 去重;`CANCEL` 按 `taskId` 幂等 | 不生成第二条任务、不回写已离寓记录 |
| 规则版本升级 | 不回溯已派生 `jcsj`,留 `derived_rule_version` | 不逐项覆写派生时刻 |
| 离寓后迟到任务 | `finish_ly='1'` 拒收,记 `IGNORED` | 不回写 `ryzt` / `finish_jc` |
表 11-33 权限与审计
| # | 要求 |
| --- | --- |
| 1 | **双校验**:管理端 `/apt/checkin/**` 走 RuoYi 会话 + `@PreAuthorize`,前端 `v-hasPermi` 同码(`apt:checkin:query`);值班台 `/duty/checkin/**` 走 `duty:checkin:list` / `duty:checkin:review` / `duty:checkin:adjust`;设备侧 `/open/v1/checkin/**` 走设备凭据 + HMAC + 时间窗 + `nonce` |
| 2 | **职责分离**:签到终端只能核验、拉选项、提交申请、查状态;值班台只能接单、审核、换房、查时间线;**二者均不直写** `gy_jcxx` / `gy_rooms` / `gygl_ly_event`,唯一执行域是 `apt-stay` |
| 3 | **`gyid` 锁定**:只从会话 / 设备凭据解析;所有 SQL 必带 `gyid`,唯一键以 `gyid` 打头 |
| 4 | **二次确认**:N-44 `APPROVE` / `REJECT` 与 N-45 房间调整必须二次确认并写 `duty_manual_action` |
| 5 | **审计**:核验、提交、接单、审核、换房、办理入住、提报、匹配、任务就绪九类动作全部记 `duty_operation_log`(操作人 / 设备、`commandId`、前后值、结果) |
| 6 | **最小必要**:N-41 / N-42 / N-43 / N-47 均不返回人脸照片或特征值本体;抓拍帧、签到现场图与录音不出公寓(CR-C24) |
| 7 | **状态保护**:`checkin_state` / `sync_state` 是流程态,**不允许**写入 `gy_jcxx.ryzt` / `finish_jc` / `finish_ly` / `stoped`;状态字段仍只经 `apt.duty.state`(CR-C26) |
---
## 11. 决策点与验收
表 11-34 待决口径(Q-36~Q-40,承接 `docs/公寓端/10` Q-35)
| 编号 | 事项 | 默认口径(未决前执行) | 拍板时点 |
| --- | --- | --- | --- |
| Q-36 | 交路是否必填 / `CHECKIN_PENDING` 是否允许直接办理入住 | 允许:交路可确定时填 `jlid`,否则 `CHECKIN_PENDING` 照常入住,由平台匹配补齐 | 阶段三试点前 |
| Q-37 | `UNMATCHED` 超时阈值与告警级别 | 超过 2 个 #8 周期仍未命中转值班台待办 + `P2` 告警,本地要素保留 | 阶段三试点前 |
| Q-38 | 房间调整是否允许在"已办理入住"后进行 | **不允许**:审核期(未建 `gy_jcxx`)才可 N-45;入住后换房走 CR-02 F-C `TRANSFER` | 契约冻结签字时 |
| Q-39 | 签到终端离线期间是否允许先办入住后补申请 | 不允许:必须先有服务端 `applyId`;离线只排队不本地放行 | 阶段三试点前 |
| Q-40 | `apt_local_task` 补列的 DDL 与 `apply_id` 索引是否随阶段二一次性冻结 | 是;补列均为设计定义,冻结前用 `duty_operation_log` JSON 兜底留痕 | 阶段二契约冻结 |
表 11-35 验收要点
| # | 验收项 | 判据 |
| --- | --- | --- |
| 1 | 注册与签到 | 未注册人员先经 N-34 → N-36 完成注册并重新核验;`APPLIED` 一律 `40914`;N-38 只传识别结论不带图像 |
| 2 | 入住申请 | 必填 / 可选字段与表 11-15 一致;8 项校验按序生效;重复 `deviceEventId` 返回原 `applyId`;申请阶段**不建** `gy_jcxx` |
| 3 | 值班员审核 | 状态机按表 11-18 迁移;并发冲突 `40926`;换房变更清单 7 项齐全且不改 `jcid` / `jcsj` |
| 4 | 已入住 | `checkin_state=CHECKED_IN` 且 `finish_ly='0' AND sjlysj IS NULL` 且已写 `gygl_ly_event(CHECKIN)`;事务失败整体回滚;**未新增 `gy_jcxx` 状态字段** |
| 5 | 提报与匹配 | #9 `CHECKIN` 字段与表 11-21 一致;#8 为匹配输入;匹配结论只经 #7 响应体 `matchVerdict[]`;`UNMATCHED` 不清空本地要素 |
| 6 | 叫乘环节 | #7 返回结构符合表 11-27;`jcsj` 由公寓复算且不由平台基线覆写;`applyId` ↔ `jcid` 一对一;已离寓迟到任务 `IGNORED` |
| 7 | 边界不破 | 跨端 20 编号、`v2-draft`、平台零回调、59 张表、`apt.duty.state` 单写入口、抓拍 / 录音不出公寓均不变 |
| 8 | 安全与审计 | 九类动作全部留痕;`gyid` 不可由请求参数指定;签到终端 / 值班台无业务表直写权限 |
@@ -0,0 +1,603 @@
# 12 消费、充值与菜品管理
> 文档编号:`docs/公寓端/12`。性质:**设计稿(Design Only)**,不是接口契约冻结稿;阶段二契约冻结评审通过后才落 DDL。
> **落地权威表述**:本文件是「人员消费、人员线下充值、公寓菜品管理」三项业务流程的**唯一权威表述**(`CF-01`~`CF-03`、表 12-1~12-34、图 12-1~12-4);口径裁决、流程增量、施工单与验收登记见 `CR-02 §14`。
> **上位依据**:`docs/10-架构优化方案-三端拆分.md`、`docs/backend/05-数据模型.md` §4 / §6、`docs/backend/04-核心业务流程.md` §6、`docs/设计变更/03-三端功能设计修订(CR-02).md`(A-04、CR-C03、表 3-15 `LEDGER_OFFLINE`、表 3-20 `offlineQuota`、表 3-30 `apt_ledger_offline`、F-D)、`docs/平台端/01` 表 C-1、`docs/平台端/03` 表 3-7、`docs/平台端/04` 表 4-9、`docs/公寓端/03` 表 3-6。
> **硬约束**:跨端仍为 **20 个接口编号(1~17 + 23~25)**、契约 `v2-draft`、平台不回调公寓;**不新增公寓本地业务表**(仍 59 张)、**不新增跨端编号、不新增 `bizType`**;`apt.ledger.local` 是公寓本地账务唯一写入模块,`platform-ledger` 是平台余额主账唯一写入口;金额一律 `decimal(12,2)` / 字符串两位小数、扣减用负数、禁止浮点。
> **CR-02 §15 关联(三端一致性 / 统计数据源)**:本文件仍是三流程唯一权威表述;三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,四能力业务规则 / 交互 / 权限 / 异常三端对照见 §15 表 3-65。平台 `docs/平台端/10` 以 `gyid` 分类统计时,本流程 #10 `LEDGER_OFFLINE` 的拆分字段(`allowanceAmount` / `cashAmount` / `rechargeChannel`)分别供**补贴消费**与**现金充值(计为收入)**取数;评价情况与房间利用周转另由 #13 `ARCHIVE` 摘要 `payload` 分组计数提供(`docs/公寓端/09` §0 表 C-9)。统计结果不回写本流程任何状态,本端不新增上报接口与 `bizType`;**变更点与影响范围见 CR-02 §15 表 3-63**。
表 12-1 文档信息
| 项 | 内容 |
| --- | --- |
| 范围 | `CF-01` 人员消费、`CF-02` 人员线下充值、`CF-03` 公寓菜品管理;覆盖完整流程、状态流转、数据结构、接口定义(请求 / 响应字段)与异常场景 |
| 本端职责 | 消费终端受理与额度查询、`apt.ledger.local` 双额度 CAS 扣减、线下充值申请与管理员核对、`apt-report` #10 上报、食谱只读下发与终端选单 |
| 协同端 | 平台端(`platform.ledger` 余额主账与日对账、`platform.catering` 食谱主数据)、应用端(管理员核对入口) |
| 编号规则 | 功能项 `CF-01`~`CF-03`;表 `表 12-y`、图 `图 12-y` 篇内连续;端内接口 `N-48`~`N-58`(承接 `docs/公寓端/11` N-47);决策点 `Q-41`~`Q-45`;裁决 `CR-C31`~`CR-C35`、流程 `F-L` / `F-M` / `F-N`(见 CR-02 §14) |
| 术语 | 表名一律小写(`apartment/db/mysql/*.sql` 实际写法),平台库沿用大写存量表名;枚举全大写下划线;时间统一 `ts_ms` / ISO-8601 带时区 |
表 12-2 修订记录
| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-11-10 | 首次形成:三项流程端到端说明、四条状态链、双额度扣减规则、复用表与补列结构、N-48~N-58 请求 / 响应字段、#10 `LEDGER_OFFLINE` 扩展字段、异常与权限、Q-41~Q-45 与验收要点;明确"仅线下充值、扫码非支付"与"远程点菜仅预留入口" |
---
## 1. 定位与边界
### 1.1 三项流程与三端职责
表 12-3 流程、主体与落点
| 功能项 | 用户动作 | 主责端 / 模块 | 协同端 | 主要落表 | 跨端编号 |
| --- | --- | --- | --- | --- | --- |
| CF-01 人员消费 | 食堂选餐 → 消费终端添加菜品显示金额 → 查补贴/现金额度 → 补贴优先扣款、不足部分现金补足 → 上报公寓端 → 同步平台落账户 | 消费终端 → `apt.ledger.local` → `apt-report` | 平台端(`platform.ledger`) | `gy_ykt_remainder`、`gy_ykt_consume`、`apt_ledger_offline`、`apt_report_outbox` | **#10**(`LEDGER_OFFLINE`)、#1 |
| CF-02 人员充值 | 扫公寓线下二维码 → 签到终端点"现金充值"输入金额 → 管理员核对 → 计入本人现金充值额度 → 保存公寓端现金额度 → 同步平台落账户 | 签到终端 → 值班台 / 管理端 → `apt.ledger.local` → `apt-report` | 平台端(`platform.ledger`) | `gy_ykt_cash_request`、`gy_ykt_xjczjl`、`gy_ykt_remainder`、`apt_ledger_offline` | **#10**(`LEDGER_OFFLINE`)、#1 |
| CF-03 公寓菜品管理 | 公寓管理员发布阶段性菜品食谱 → 消费人员在消费终端查看菜品 → 点击加入选择菜单;预留远程点菜入口 | 公寓管理员 → `platform.catering` → `apt.catering`(只读下发) → 消费终端 | 平台端(食谱主数据) | `YKT_MEALS`(平台库,补列)、`YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX`(预留) | **#4**(`recipes[]`)、**#1**(`features.remoteOrdering`) |
### 1.2 与既有流程的关系
- **账务闭环不重写**:CF-01 / CF-02 的"离线受理 → 本地余额立即可用 → 先派 outbox → #10 逐条上报 → 平台归日 → 日对账通过置 `RECONCILED`"完整沿用 CR-02 **F-D** 六步;本文件只补齐"设备侧选菜 / 扫码申请 / 管理员核对"三个前置环节与字段。
- **主账唯一写入口不变**:`platform-ledger` 是 `GY_YKT_REMAINDER` 与 `PL_LEDGER_FLOW` 的唯一写者;`platform-catering` 与消费终端对余额表**零写权限**(`docs/平台端/01` D1、`docs/平台端/03`)。
- **菜品/订单表不重写**:复用 `YKT_MEALS` / `YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` / `PL_CATERING_SETTLE`(`docs/平台端/03` 表 3-7)与 `/platform/v1/admin/catering`(`docs/平台端/04` 表 4-9);"阶段性食谱"以 `YKT_MEALS` **补列**实现,不新增表。
- **注册与入住不重写**:充值需校验人员有效在寓与注册态,沿用 `docs/公寓端/10` §4 与 `docs/公寓端/11` §4;本文件不新增人员核验链路。
- **远程点菜不实现**:仅保留特性开关与接口入口(本期恒关闭),见 §6.4。
### 1.3 不变式
1. 跨端 20 个编号、方向、路径不变;新增数据只走既有 `payload` + 既有 `bizType`(`LEDGER_OFFLINE` 已占 #10,CR-02 表 3-12)。
2. 平台零回调;#1 / #4 由公寓 GET 拉取,#10 由公寓 POST 上行;#16 日对账本期只提供公寓可主动调用形态(P-02)。
3. 公寓本地账务表唯一写入模块是 `apt.ledger.local`(`docs/公寓端/03` 表 3-6);设备侧只提交请求,不直改余额。
4. `GY_YKT_REMAINDER` 是唯一余额表,不新造同义账户表;`gy_card` 全库不存在,卡号沿用 `GY_YKT_RY.card_id`。
5. `GY_YKT_CASH_REQUEST.request_state` 沿用三值 `PENDING` / `COMPLETED` / `CANCELLED`,且**申请表本身不存金额**(金额落 `apt_ledger_offline.amount`)。
6. `offlineScope` 三值 `LOCAL_ONLY` / `SYNCED` / `RECONCILED` 沿用 CR-02 表 3-15 / 表 3-21;`LOCAL_ONLY` 不是匹配状态。
7. 幂等 `idempotencyKey = hash(gyid + bizType + bizId + bizVersion)`;`bizId = flowId`、业务 UK `(gyid, type, flow_id)`;归日以 `serverReceivedAt` 为准;`count` 按 `biz_id` 去重(R-C3)。
8. outbox 是队列、`apt_report_history` 是终态流水,两者语义不互换(CR-C10);本流程的**跨端上报态**一律沿用 CR-02 表 3-21 的 `reportState` 正式五值 `PENDING` / `SENT` / `FAILED` / `DEAD` / `RETRIED`(outbox 与 history 共用),本地 outbox 内部状态机仍按 `docs/公寓端/03` 表 3-11。
9. 设备侧一律主动拉取;抓拍、录音、签到现场图不出公寓;`gyid` 只从凭据解析、所有 SQL 必带 `gyid`。
10. **仅线下充值**:不接任何线上支付渠道、不产生支付订单与回调;扫公寓线下二维码只是**公寓识别与入口引导**,不构成资金流。
---
## 2. 状态流转总览
图 12-1 三项流程数据流向
```mermaid
flowchart LR
subgraph DEV["公寓内设备"]
CT["消费终端 CONSUME<br/>查看食谱 · 选单 · 显示金额"]
CK["签到终端 CHECKIN<br/>扫码 · 现金充值申请"]
end
subgraph AP["公寓服务系统"]
QUO["apt.ledger.local<br/>额度查询 / 双额度 CAS 扣减"]
REQ["gy_ykt_cash_request<br/>充值申请与管理员核对"]
LED["gy_ykt_* / apt_ledger_offline<br/>本地账务流水"]
OUT["apt-report / outbox"]
CAT["apt.catering<br/>食谱只读下发 / 水位"]
end
subgraph DUTY["值班台 / 管理端"]
ADM["管理员核对现金充值"]
RCP["公寓管理员发布食谱"]
end
subgraph PL["平台端"]
CATM["platform.catering<br/>YKT_MEALS 食谱主数据"]
ING["platform-ingest<br/>#10 接收"]
LEDM["platform-ledger<br/>GY_YKT_REMAINDER / PL_LEDGER_FLOW"]
end
RCP -->|发布| CATM
CATM -->|#4 recipes[](拉)| CAT
CAT -->|CF-03 N-48 食谱| CT
CT -->|CF-01 N-49 额度查询| QUO
CT -->|CF-01 N-50 消费提交| QUO
QUO -->|补贴优先 + 现金补足| LED
CK -->|CF-02 N-52 充值申请| REQ
REQ -->|待核对| ADM
ADM -->|N-54 确认| REQ
REQ -->|计入现金额度| LED
LED -->|#10 LEDGER_OFFLINE| OUT
OUT --> ING
ING --> LEDM
LEDM -->|日对账结论| OUT
```
表 12-4 四条状态链(互不替代)
| 链 | 字段 / 载体 | 取值 | 唯一写入口 | 说明 |
| --- | --- | --- | --- | --- |
| **消费流水态** | `apt_ledger_offline.ledger_state`(补列,流程态) | `SUBMITTED` / `DEDUCTED` / `REPORTED` / `RECONCILED` / `REJECTED` / `REVERSED` | `apt.ledger.local` | 描述一笔消费从终端提交到平台对账;**不是**余额状态字段 |
| **充值申请态** | `gy_ykt_cash_request.request_state` | `PENDING` / `COMPLETED` / `CANCELLED` | `apt.ledger.local` | 沿用存量三值(`docs/backend/05` §4.7),不新造 |
| **同步生命周期** | `apt_ledger_offline.offline_scope` + `reportState`(outbox / history 共用)+ `reconcile_state` | `LOCAL_ONLY` / `SYNCED` / `RECONCILED`;`PENDING` / `SENT` / `FAILED` / `DEAD` / `RETRIED`;`PENDING` / `RECONCILED` / `DIFF` | `apt.ledger.local` / `apt.report.outbox` | 沿用 CR-02 表 3-15、表 3-21、表 3-30 |
| **食谱发布态** | `YKT_MEALS.publish_state`(平台库补列) | `DRAFT` / `PUBLISHED` / `EXPIRED` / `REVOKED` | `platform.catering` | 阶段性食谱按 `recipe_id` + `recipe_version` 成组发布 |
表 12-5 全流程状态流转
| 序 | 流程 | 状态 | 触发 | 写入口与落表 | 后继 / 异常回退 |
| --- | --- | --- | --- | --- | --- |
| 1 | CF-03 | `DRAFT` | 公寓管理员新建阶段性食谱 | `YKT_MEALS`(补列) | 菜品下架 / 价格改 → 新 `recipe_version`,不改已发布版 |
| 2 | CF-03 | `PUBLISHED` | 管理员发布且 `effective_from ≤ now ≤ effective_to` | `YKT_MEALS.publish_state`、#4 增量 | 到期 → `EXPIRED`;撤回 → `REVOKED` |
| 3 | CF-01 | 终端选单(不落库) | 消费终端 N-48 拉食谱、点击加入选择菜单 | 设备本地选单缓存 | 仅本地状态,退出/超时清空 |
| 4 | CF-01 | `SUBMITTED` | N-50 提交消费(`flowId` 幂等) | `apt_ledger_offline(ledger_state=SUBMITTED)` | 校验失败 → `REJECTED`,不扣款 |
| 5 | CF-01 | `DEDUCTED` | 双额度 CAS 扣减成功、`balance_before/after` 留痕 | `gy_ykt_remainder`、`gy_ykt_consume`、`apt_ledger_offline` | CAS 冲突重读重试一次,再失败转人工 |
| 6 | CF-01 | `REPORTED` | #10 `LEDGER_OFFLINE` 入 outbox 并 `SENT` | `apt_report_outbox` / `apt_report_history` | 断网 → outbox `PENDING`,恢复后复用原 `idempotencyKey` 续报 |
| 7 | CF-01 | `RECONCILED` | 平台日对账通过 | `offline_scope=RECONCILED`、`local_balance_version` 更新 | 差异 → `DIFF`,`PF-5003` 阻断调账 |
| 8 | CF-02 | `PENDING` | N-52 扫码后输入金额提交申请 | `gy_ykt_cash_request`、`apt_ledger_offline(RECHARGE, PENDING)` | 重复 `deviceEventId` 返回原 `flowId` |
| 9 | CF-02 | `COMPLETED` | 管理员 N-54 核对确认(二次确认) | `gy_ykt_xjczjl`、`gy_ykt_remainder.xjye`、`apt_ledger_offline.balance_after` | 金额不符 → N-55 `CANCELLED`,写 `reject_reason` |
| 10 | CF-02 | `REPORTED` → `RECONCILED` | 同序 6 / 7 | 同序 6 / 7 | 同序 6 / 7 |
| 11 | 通用 | `REVERSED` / `CANCELLED` | 冲正或作废 | `apt_ledger_offline`、`duty_operation_log` | 已 `RECONCILED` 不得直接改,走 `POST /adjustments` 冲正 |
图 12-2 双额度扣减决策
```mermaid
flowchart TD
A["N-50 消费提交 amount"] --> B{"额度查询<br/>allowanceQuota / cashQuota"}
B --> C{"amount ≤ allowanceQuota?"}
C -->|是| D["allowanceAmount = amount<br/>cashAmount = 0"]
C -->|否| E["allowanceAmount = allowanceQuota<br/>cashAmount = amount - allowanceQuota"]
E --> F{"cashAmount ≤ cashQuota?"}
F -->|否| G["REJECTED 余额不足<br/>PF-5001 / 40933"]
F -->|是| H["CAS 扣减两条额度<br/>balance_before/after"]
D --> H
H --> I["一条 CONSUME 流水<br/>payload 带拆分字段"]
I --> J["#10 LEDGER_OFFLINE"]
```
> **一条流水原则**(CR-C31):一笔消费只产生**一条** `flowId` / `type=CONSUME` 流水,补贴与现金的拆分作为 `allowanceAmount` / `cashAmount` 字段随 payload 上报,不额外生成 `ALLOWANCE_DEDUCT` 流水,避免平台 `sum(amount)` 双计。`ALLOWANCE_DEDUCT` 保留给"独立退补助 / 补助调整"场景。
---
## 3. 数据模型
> **不新增公寓本地业务表**(仍 59 张):账户额度、消费流水、充值记录全部复用 `gy_ykt_*` 五张预留表与 `apt_ledger_offline`;食谱主数据复用平台库 `YKT_MEALS` 并补列。所有补列均为设计定义,DDL 待阶段二契约冻结评审(Q-43)。
表 12-6 复用表清单
| 表 | 库 | 唯一写入模块 | 本流程用途 | 上报 |
| --- | --- | --- | --- | --- |
| `gy_ykt_remainder` | apartment | `apt.ledger.local` | 账户额度本地快照(`knye` / `xjye`),CAS 扣减 | 否(经 #10 流水) |
| `gy_ykt_consume` | apartment | `apt.ledger.local` | 消费流水(`xfje` / `xfsj` / `deviceCode` / `xfly`) | 是(#10) |
| `gy_ykt_allowance` | apartment | `apt.ledger.local` | 补助发放流水(本流程只读,作为额度来源) | 是(#10) |
| `gy_ykt_cash_request` | apartment | `apt.ledger.local` | 现金充值申请与 `request_state` 三值 | 否 |
| `gy_ykt_xjczjl` | apartment | `apt.ledger.local` | 现金充值记录(`flow_id` / `czje` / `czsj`) | 是(#10) |
| `apt_ledger_offline` | apartment | `apt.ledger.local` | 离线/在线统一的本地账务流水、CAS 留痕、同步生命周期 | 是(#10) |
| `apt_report_outbox` / `apt_report_history` | apartment | `apt.report.outbox` / `apt-report` | 上行队列与终态留痕 | 是 |
| `GY_YKT_REMAINDER` / `PL_LEDGER_FLOW` | platform | `platform-ledger` | 平台余额主账与归集流水(**公寓端零写权限**) | — |
| `YKT_MEALS` / `YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` | platform | `platform.catering` | 菜品主数据、阶段性食谱补列、选单 / 订单(后两者本期预留) | — |
| `duty_manual_action` / `duty_operation_log` | duty | 应用端 | 管理员核对、食谱发布留痕 | 否 |
表 12-7 账户额度 `gy_ykt_remainder` 列级结构与补列
| 列 | 类型 | 约束 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| `ryid` / `gyid` | string(64) / string(32) | UK `(gyid, ryid)` | 存量 | 账户主键;`gyid` 打头隔离 |
| `xm` | string(128) | 可空 | 存量 | 姓名快照 |
| `knye` | decimal(12,2) | 默认 `0.00` | 存量 | **补贴可用额度**(逻辑名 `allowanceQuota`,沿用 `docs/backend/05` §4.3 与 `backend/11` 实测口径) |
| `xjye` | decimal(12,2) | 默认 `0.00` | 存量 | **现金可用额度**(逻辑名 `cashQuota`) |
| `xfze` / `bzze` / `xfxjje` / `xfbzje` | decimal(12,2) ×4 | 默认 `0.00` | 存量 | 消费总额 / 补助总额 / 现金消费额 / 补助消费额,随每笔消费累加 |
| `local_balance_version` | bigint | 默认 `1` | **补列** | CAS 版本;与 `apt_ledger_offline.local_balance_version` 一致,冲突重读重试一次 |
| `last_reconciled_at` / `reconcile_state` | datetime(3) / enum_s | 可空 / 默认 `PENDING` | **补列** | 最后对账日期与 `PENDING` / `RECONCILED` / `DIFF`(平台侧同名三值 `RECEIVED` / `RECONCILED` / `DIFF`) |
| `quota_ref` | string(64) | 可空 | **补列** | #1 下发 `offlineQuota` 批次引用,断网额度校验用 |
| `updated_at` | datetime(3) | NOT NULL | **补列** | 只追加更新,不物理删除 |
> `knye` / `xjye` 到"补贴 / 现金"的映射为**设计映射**,阶段二冻结前须按 `backend/11` 存量数据复核(写入 Q-43 一并拍板);本流程不新造同义余额字段。
表 12-8 消费流水 `gy_ykt_consume` / `apt_ledger_offline` 列级结构与补列
| 表 | 列 | 类型 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| `gy_ykt_consume` | `tid` / `flow_id` / `ryid` / `gyid` | string ×4 | 存量 / **补列** | `flow_id` 为 #10 `bizId`,UK `(gyid, flow_id)` |
| | `xfje` / `xfsj` / `device_code` / `xfly` | decimal(12,2) / datetime / string / enum_s | 存量 | 消费金额(正数)、时间、受理设备、消费来源 |
| | `knye` / `xjye` / `xfze` / `bzze` | decimal(12,2) ×4 | 存量 | 扣款后两条余额与累计额快照 |
| | `allowance_amount` / `cash_amount` | decimal(12,2) ×2 | **补列** | 本笔拆分:`allowance_amount + cash_amount = xfje`,一条流水不双计 |
| | `recipe_id` / `recipe_version` / `items` | string / bigint / json | **补列** | 关联阶段性食谱与选单明细(`meal_id` / `name` / `price` / `qty`),供对账与退单 |
| `apt_ledger_offline` | `id` / `gyid` / `type` / `flow_id` | id_ulid / string / enum_s / string | 存量 | `type` = `CONSUME` / `RECHARGE` / `ALLOWANCE_DEDUCT`;UK `(gyid, type, flow_id)` |
| | `amount` / `balance_before` / `balance_after` | decimal(12,2) ×3 | 存量 | 扣减用负数、充值用正数;CAS 前后余额留痕 |
| | `offline_scope` / `reconcile_state` | enum_s ×2 | 存量 | `LOCAL_ONLY` / `SYNCED` / `RECONCILED`;`PENDING` / `RECONCILED` / `DIFF` |
| | `local_balance_version` / `quota_ref` | bigint / string | 存量 | 本地余额版本与离线额度批次 |
| | `ledger_state` | enum_s | **补列** | 表 12-4 消费流水态(流程态,不写 `gy_jcxx` 状态字段) |
| | `allowance_before/after` / `cash_before/after` | decimal(12,2) ×4 | **补列** | 双额度各自 CAS 前后值,与 §2 决策图一致 |
| | `occurred_at` / `synced_at` / `created_at` | datetime(3) ×3 | 存量 | 归日以平台 `serverReceivedAt` 为准 |
表 12-9 充值记录 `gy_ykt_xjczjl` / `gy_ykt_cash_request` 列级结构与补列
| 表 | 列 | 类型 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| `gy_ykt_xjczjl` | `flow_id` / `ryid` / `gyid` | string ×3 | 存量 | PK `flow_id`;与 `apt_ledger_offline.flow_id` 一致 |
| | `czje` / `czsj` | decimal(12,2) / datetime(3) | 存量 | 充值金额(正数)与时间 |
| | `channel` / `confirmed_by` / `confirmed_at` | enum_s / string / datetime | **补列** | `channel` 恒 `OFFLINE_CASH`(**仅线下**);管理员核对留痕 |
| `gy_ykt_cash_request` | `id` / `ryid` / `gyid` / `device_code` | string ×4 | 存量 | 申请单;**本身不存金额**(金额在 `apt_ledger_offline.amount`) |
| | `request_state` | enum_s | 存量 | `PENDING` / `COMPLETED` / `CANCELLED`,不新造状态 |
| | `flow_id` / `device_event_id` | string ×2 | **补列** | 关联账务流水;UK `(gyid, device_code, device_event_id)` 幂等 |
| | `requested_at` / `confirmed_by` / `confirmed_at` / `reject_reason` | datetime ×2 / string / datetime / string | **补列** | 申请、核对与作废留痕 |
| | `qr_code_id` | string(64) | **补列** | 公寓线下二维码标识,**只作公寓识别,不含支付信息** |
表 12-10 食谱与菜品 `YKT_MEALS` 补列(平台库)
| 列 | 类型 | 来源 | 说明 |
| --- | --- | --- | --- |
| `meal_id` / `gyid` / `name` / `price` / `enabled` | string / string / string / decimal(12,2) / tinyint | 存量 | 菜品主键、公寓、名称、单价、启停(`docs/平台端/03` 表 3-7) |
| `recipe_id` / `recipe_version` | string(64) / bigint | **补列** | 阶段性食谱批次与版本;同批次所有菜品共享,`version` 单调 |
| `meal_period` | enum_s | **补列** | `BREAKFAST` / `LUNCH` / `DINNER` / `NIGHT` / `ANY` |
| `effective_from` / `effective_to` | datetime(3) ×2 | **补列** | 食谱生效区间;到期 `EXPIRED` |
| `publish_state` | enum_s | **补列** | `DRAFT` / `PUBLISHED` / `EXPIRED` / `REVOKED` |
| `published_by` / `published_at` | string(64) / datetime(3) | **补列** | 公寓管理员发布留痕 |
| `category` / `sort_no` / `image_ref` | string / int / string | **补列** | 分类、排序、图片引用占位;**本期不落任何图片本体**,#4 下行只带引用字符串,不进对象存储(平台侧仍只有 `person-photo` / `report-export` 两个桶,见 `docs/平台端/07` §4.1) |
> `YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 本期**不写入**:消费终端的"选择菜单"是设备本地选单,确认后才按 CF-01 生成一条消费流水;远程点菜启用后再落订单表(Q-44)。
表 12-11 枚举字典
| 枚举 | 取值 | 来源 / 说明 |
| --- | --- | --- |
| `flowType` | `CONSUME` / `RECHARGE` / `ALLOWANCE_DEDUCT` | CR-02 表 3-15,沿用不新增 |
| `offlineScope` | `LOCAL_ONLY` / `SYNCED` / `RECONCILED` | CR-02 表 3-15 / 表 3-21 |
| `reportState` | `PENDING` / `SENT` / `FAILED` / `DEAD` / `RETRIED` | CR-02 表 3-21,outbox 与 history 共用 |
| `requestState` | `PENDING` / `COMPLETED` / `CANCELLED` | `docs/backend/05` §4.7 |
| `ledgerState` | `SUBMITTED` / `DEDUCTED` / `REPORTED` / `RECONCILED` / `REJECTED` / `REVERSED` | 本流程补列(流程态) |
| `publishState` | `DRAFT` / `PUBLISHED` / `EXPIRED` / `REVOKED` | 本流程补列 |
| `mealPeriod` | `BREAKFAST` / `LUNCH` / `DINNER` / `NIGHT` / `ANY` | 本流程补列 |
| `rechargeChannel` | `OFFLINE_CASH` | **唯一取值**;不接线上支付(CR-C32) |
| `source` | `DEVICE` / `DUTY` / `SELF` | CR-02 表 3-14 |
---
## 4. CF-01 人员消费流程
### 4.1 触发条件与数据范围
表 12-12 触发与前置
| 项 | 口径 |
| --- | --- |
| 触发 | 消费终端(`gy_sb.device_class=CONSUME`)按当前 `PUBLISHED` 食谱展示菜品;人员点击加入选择菜单,终端实时显示合计金额;确认后 N-50 提交 |
| 数据范围 | `gyid`(凭据解析)、`ryid`、`deviceCode`、`deviceEventId`、选单 `items[]{mealId, name, price, qty}`、`amount`、`recipeId` / `recipeVersion`、`occurredAt` |
| 前置 | 人员已注册且有效在寓(`docs/公寓端/11` §2);食谱为 `PUBLISHED` 且在生效区间;设备凭据、HMAC、±5 min 时间窗、`nonce` 通过 |
| 不触发 | 仅浏览食谱 / 仅加入选单(未确认)、食谱 `EXPIRED` / `REVOKED`、菜品 `enabled=0`、人员已离寓 |
表 12-13 双额度扣减规则(CR-C31)
| 步 | 规则 | 字段 | 异常 |
| --- | --- | --- | --- |
| 1 | 查询补贴额度与现金额度 | `allowanceQuota = gy_ykt_remainder.knye`、`cashQuota = xjye` | 账户不存在 → `40934`;额度版本落后 → 重拉 #1 后重试 |
| 2 | 优先全额补贴 | `amount ≤ allowanceQuota` → `allowanceAmount = amount`、`cashAmount = 0` | — |
| 3 | 补贴不足则现金补足 | `allowanceAmount = allowanceQuota`、`cashAmount = amount - allowanceQuota` | `cashAmount > cashQuota` → `REJECTED` + `PF-5001` / `40933`,**不部分扣款** |
| 4 | CAS 扣减 | `where ryid=? and local_balance_version=?`;更新 `knye` / `xjye` 与四个累计额 | 冲突重读重试一次,再失败转人工(`40935`) |
| 5 | 记一条流水 | `type=CONSUME`、`amount = -xfje`(payload 负数)、`allowanceAmount` + `cashAmount = xfje` | 不生成第二条 `ALLOWANCE_DEDUCT`,不双计 |
| 6 | 断网额度校验 | `offlineScope=LOCAL_ONLY` 时校验 #1 `offlineQuota.{singleLimit, dailyLimit, maxOutstanding}` | 超限 `40913`,本地余额立即可用但不写平台主账 |
| 7 | 上报与对账 | #10 `LEDGER_OFFLINE`;平台按 `serverReceivedAt` 归日;日对账通过置 `RECONCILED` | `PF-5002` 重复流水、`PF-5003` 对账未结清 |
### 4.2 接口
表 12-14 N-49 / N-50 / N-51 请求 / 响应字段
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| N-49 | `GET /open/v1/ledger/quota?ryid&deviceCode` | 设备凭据 + HMAC | L0 | `ryid`(须与设备可见范围匹配)、`deviceCode` | `ryid`、`allowanceQuota` / `cashQuota`(字符串两位小数)、`localBalanceVersion`、`quotaRef`、`reconcileState`、`lastReconciledAt`、`serverTime` |
| N-50 | `POST /open/v1/ledger/consume` | 设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | `ryid`、`items[]{mealId, name, price, qty}`、`amount`、`recipeId` / `recipeVersion`、`deviceEventId`、`occurredAt`、`nonce` / `signature` | `flowId`、`ledgerState=DEDUCTED`、`allowanceAmount` / `cashAmount`、`allowanceAfter` / `cashAfter`、`offlineScope`、`localBalanceVersion`、`reportState`、`serverTime` |
| N-51 | `GET /open/v1/ledger/flows/{flowId}` | 设备凭据 | L0 | `flowId` | `flowId`、`type`、`ledgerState`、`offlineScope`、`reportState`、`reconcileState`、`amount` / `allowanceAmount` / `cashAmount`、`syncedAt` |
表 12-15 消费状态机(条件更新带 `ledger_state` 期望值)
| 当前态 | 事件 | 目标态 | 落表 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| — | N-50 提交通过校验 | `SUBMITTED` | `apt_ledger_offline` | 校验失败 → `REJECTED`,不扣款 |
| `SUBMITTED` | CAS 扣减成功 | `DEDUCTED` | `gy_ykt_remainder`、`gy_ykt_consume`、`apt_ledger_offline` | CAS 冲突重试一次,再失败转人工 |
| `DEDUCTED` | #10 入 outbox 并 `SENT` | `REPORTED` | `apt_report_outbox` / `apt_report_history` | 断网 `PENDING` 排队,恢复复用原幂等键 |
| `REPORTED` | 平台日对账通过 | `RECONCILED` | `offline_scope` / `reconcile_state` / `local_balance_version` | 差异 `DIFF` + `PF-5003`,阻断调账 |
| `REPORTED` / `RECONCILED` | 人工冲正 | `REVERSED` | `duty_manual_action`、`POST /adjustments` | **不直接改余额表**,走 `platform-ledger` 冲正 |
图 12-3 消费端到端时序
```mermaid
sequenceDiagram
autonumber
participant CT as 消费终端 CONSUME
participant AP as apt.ledger.local
participant AC as apt.catering
participant L as gy_ykt_* / apt_ledger_offline
participant O as apt-report / outbox
participant P as platform-ingest → platform-ledger
CT->>AC: CF-03 N-48 拉当前食谱(设备主动拉取)
AC-->>CT: recipes[] + version(终端缓存)
CT->>CT: 点击菜品加入选择菜单,显示合计金额
CT->>AP: N-49 查询补贴额度与现金额度
AP-->>CT: allowanceQuota / cashQuota / localBalanceVersion
CT->>AP: N-50 提交消费(flowId, items, amount)
AP->>AP: 补贴优先 → 现金补足(图 12-2)
AP->>L: CAS 扣减 + 记一条 CONSUME 流水(DEDUCTED)
AP->>O: 生成 #10 LEDGER_OFFLINE
O->>P: 上报(idempotencyKey = hash(gyid+bizType+flowId+bizVersion))
P->>P: 按 serverReceivedAt 归日,落 PL_LEDGER_FLOW / GY_YKT_REMAINDER
P-->>O: 接收回执 / 日对账结论
O->>L: REPORTED → RECONCILED(offline_scope / local_balance_version)
AP-->>CT: N-51 可查流水状态
```
表 12-16 CF-01 异常与错误码
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 余额不足(补贴 + 现金仍不足) | `REJECTED` + `PF-5001` / `40933` | 不部分扣款、不透支(内部人员透支口径见 Q-45) |
| 补贴刚好为 0 | `allowanceAmount=0`,全额走现金 | 不生成 `ALLOWANCE_DEDUCT` 流水 |
| CAS 版本冲突 | 重读额度重试一次,再失败 `40935` 转人工 | 不覆盖较新版本、不丢 `balance_before/after` |
| 断网且超 `offlineQuota` | `40913`,本地余额立即可用但不写平台主账 | 不伪造 `SYNCED` |
| 重复提交 `deviceEventId` | 返回原 `flowId` 与快照 | 不产生第二笔扣款 |
| 平台重复流水 / 幂等重放 | `PF-5002` 业务重复 vs `PL-IDEMPOTENT-REPLAY` 幂等重放分别处理 | 不把两者混为一谈 |
| 食谱过期 / 菜品下架 / 价格变更 | `40936` / `PF-6001`,要求终端重拉食谱 | 不用旧价格扣款 |
| 已离寓人员消费 | `40937` 拒收 | 不改 `finish_ly` / `ryzt` |
| 已 `RECONCILED` 需冲正 | 走 `POST /adjustments` + 审批留痕 | 不直改 `GY_YKT_REMAINDER` |
---
## 5. CF-02 人员充值流程
### 5.1 线下充值边界
表 12-17 "仅线下充值、仅记账"口径(CR-C32)
| 项 | 口径 |
| --- | --- |
| 扫码含义 | 公寓**线下二维码**只携带 `gyid` + `qrCodeId`,用于识别公寓并引导到签到终端;**不携带支付链接、不发起任何线上支付** |
| 资金动作 | 现金由管理员线下收取并核对;系统只做**记账**,不接微信/支付宝/网银、不建支付订单、不处理支付回调 |
| 金额载体 | `gy_ykt_cash_request` **不存金额**(沿用存量口径);金额落 `apt_ledger_offline.amount`(`type=RECHARGE`,正数)与 `gy_ykt_xjczjl.czje` |
| 额度落点 | 管理员确认后由 `apt.ledger.local` 更新 `gy_ykt_remainder.xjye`(现金额度)并记 `balance_before/after` |
| 同步 | 与 CF-01 共用 #10 `LEDGER_OFFLINE` + F-D 六步;平台 `platform-ledger` 落 `GY_YKT_REMAINDER` / `PL_LEDGER_FLOW` |
| 断网 | 本地记账受 #1 `offlineQuota` 约束(缺省全 `0` 即断网不开放,Q-21 / Q-42),超限 `40913` |
表 12-18 充值申请状态机(复用 `request_state` 三值)
| 当前态 | 事件 | 目标态 | 落表 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| — | N-52 扫码后输入金额提交 | `PENDING` | `gy_ykt_cash_request`、`apt_ledger_offline(SUBMITTED)` | 金额非法 → `40938`;重复 `deviceEventId` 返回原 `flowId` |
| `PENDING` | 管理员 N-54 核对确认(`duty:ledger:confirm` + 二次确认) | `COMPLETED` | `gy_ykt_xjczjl`、`gy_ykt_remainder.xjye`、`apt_ledger_offline(DEDUCTED)` | 金额与实收不符 → N-55 `CANCELLED` |
| `PENDING` | 管理员 N-55 作废 / 超时 | `CANCELLED` | `reject_reason`、`duty_operation_log` | 不写额度、不生成充值记录 |
| `COMPLETED` | #10 上报 / 日对账 | `REPORTED` → `RECONCILED` | `apt_report_outbox` / `history` | 同 CF-01 序 6 / 7 |
| 终态 | 任何再次确认 | 拒绝 `40939` | — | 返回原态快照,不重复入账 |
表 12-19 N-52~N-55 请求 / 响应字段
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| N-52 | `POST /open/v1/ledger/recharge-requests` | 签到终端设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | `ryid`、`amount`(字符串两位小数,正数)、`qrCodeId`、`deviceEventId`、`occurredAt`、`nonce` / `signature` | `flowId`、`requestState=PENDING`、`amount`、`qrCodeId`、`requestedAt`、`serverTime` |
| N-53 | `GET /duty/ledger/recharge-requests?state&cursor` | `duty:ledger:local` | L0 | `state=PENDING`、`cursor`、`limit` | `items[]{flowId, ryid, workNo, name, amount, qrCodeId, requestedAt, deviceCode}`、`cursor`、`total` |
| N-54 | `POST /duty/ledger/recharge-requests/{flowId}/confirm` | `duty:ledger:confirm` + 二次确认 | **L2** `commandId` | `flowId`、`expectedAmount`、`confirmedAmount`、`expectedRequestState`、`commandId` | `flowId`、`requestState=COMPLETED`、`cashQuotaAfter`、`localBalanceVersion`、`confirmedBy` / `confirmedAt`、`reportState` |
| N-55 | `POST /duty/ledger/recharge-requests/{flowId}/cancel` | `duty:ledger:confirm` | **L2** `commandId` | `flowId`、`reason`、`commandId` | `flowId`、`requestState=CANCELLED`、`rejectReason` |
表 12-20 管理员确认后的数据变更清单
| # | 对象 | 变更 | 约束 |
| --- | --- | --- | --- |
| 1 | `gy_ykt_cash_request` | `request_state=COMPLETED`、回填 `confirmed_by` / `confirmed_at` | 条件更新带期望态 `PENDING`,并发冲突 `40939` |
| 2 | `apt_ledger_offline` | `type=RECHARGE`、`amount=+confirmedAmount`、`ledger_state=DEDUCTED`、`balance_before/after`、`cash_before/after` | 金额必须与 N-52 一致;不一致走 N-55 |
| 3 | `gy_ykt_xjczjl` | 新增 `flow_id` / `czje` / `czsj`、`channel=OFFLINE_CASH`、`confirmed_by` | 一笔申请一条充值记录 |
| 4 | `gy_ykt_remainder` | `xjye += confirmedAmount`、`local_balance_version + 1`、更新 `updated_at` | CAS 条件更新;只增现金额度,**不改 `knye` 补贴额度** |
| 5 | `apt_report_outbox` | 生成 #10 `LEDGER_OFFLINE` 上报项(`bizId=flowId`) | 断网 `PENDING` 排队,恢复复用原 `idempotencyKey` |
| 6 | `duty_manual_action` / `duty_operation_log` | 记核对人、`commandId`、申请金额 / 实收金额、前后额度 | 二次确认留痕,越权 `40301` |
图 12-4 线下充值端到端时序
```mermaid
sequenceDiagram
autonumber
participant CK as 签到终端 CHECKIN
participant AP as apt.ledger.local
participant D as 管理员(值班台 /duty)
participant L as gy_ykt_* / apt_ledger_offline
participant O as apt-report / outbox
participant P as platform-ledger
CK->>AP: 扫公寓线下二维码 → N-52 现金充值申请(amount)
AP->>L: request_state=PENDING(申请表不存金额)
AP->>D: 推待办 recharge.request
D->>AP: N-53 查看待核对列表
D->>AP: N-54 核对实收金额并二次确认
AP->>L: COMPLETED + xjye 增加 + gy_ykt_xjczjl 记录
AP->>O: 生成 #10 LEDGER_OFFLINE(RECHARGE)
O->>P: 上报并归日
P-->>O: 日对账结论 → RECONCILED
AP-->>CK: 可查额度与流水状态
Note over CK,P: 全程无线上支付渠道、无支付订单与回调
```
表 12-21 CF-02 异常与错误码
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 金额为 0 / 负数 / 超上限 / 小数位 > 2 | `40938` 拒收(沿用 `backend/04` "金额 > 0 且 < 100") | 不建申请单、不排队 |
| 二维码无效 / 跨 `gyid` | `40301` / `40939` 拒收并记审计 | 不解析任何支付参数 |
| 重复扫码提交 | 返回原 `flowId`(`deviceEventId` 幂等) | 不生成第二笔充值 |
| 实收金额与申请不符 | N-55 `CANCELLED` + `reject_reason`,重新发起申请 | 不按申请金额直接入账 |
| 并发确认 | 条件更新冲突 `40939` | 不重复入账、不改终态 |
| 断网超 `offlineQuota` | `40913` | 不伪造已同步、不写平台主账 |
| 已 `RECONCILED` 需冲正 | `POST /adjustments` + 审批 | 不直改余额表、不改申请单终态 |
| 线上支付请求(若误接) | 直接拒绝,不提供该入口 | 不建支付订单、不监听回调 |
---
## 6. CF-03 公寓菜品管理
### 6.1 发布与查看
表 12-22 食谱发布状态机(平台库 `YKT_MEALS.publish_state`)
| 当前态 | 事件 | 目标态 | 落表 / 编号 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| — | 公寓管理员新建食谱(选菜品、餐次、生效区间) | `DRAFT` | `YKT_MEALS` 补列 | 菜品 `enabled=0` 不得加入 |
| `DRAFT` | 发布(二次确认) | `PUBLISHED` | `recipe_version` 单调、`published_by/at`;#4 增量 | 区间重叠 → `PF-6004`(规则重叠沿用 `PF-5004` 语义)或拒绝发布 |
| `PUBLISHED` | 到达 `effective_to` | `EXPIRED` | 定时任务置态 | 终端重拉后不再展示 |
| `PUBLISHED` | 管理员撤回 | `REVOKED` | `duty_manual_action` | 已发生的消费流水不受影响 |
| 任意已发布态 | 改价 / 下架菜品 | 新 `recipe_version` | `YKT_MEALS` 新版本 | **不改**已发布版本,避免历史对账错配 |
> **主数据归属(CR-C33)**:食谱主数据在**平台库 `YKT_MEALS`**(补列),由公寓管理员经 `/platform/v1/admin/catering` 的 `recipes` 资源发布并按 `gyid` 隔离;公寓端 `apt.catering` 通过 **#4 下行 `recipes[]`** 拉增量与水位,**食谱缓存落在消费终端本地,不落公寓本地业务表**(59 张不变)。Q-43 未决前按此默认口径执行。
表 12-23 N-48 / N-57 / N-58 请求 / 响应字段
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| N-48 | `GET /open/v1/catering/recipes?deviceCode&version` | 消费终端设备凭据 | L0(版本) | `version`(可选,来自上次响应) | `recipes[]{recipeId, recipeVersion, mealPeriod, effectiveFrom, effectiveTo, publishState, items[]{mealId, name, price, category, sortNo, enabled, imageRef}}`、`version`、`serverTime`;版本未变返回 `notModified=true` |
| N-57 | `GET /apt/catering/recipes`、`POST /apt/catering/recipes`、`PUT /apt/catering/recipes/{recipeId}/publish`、`PUT /apt/catering/recipes/{recipeId}/revoke` | `apt:catering:recipe:*` | L2 `recipeId` + `version` | `recipeId`、`mealPeriod`、`effectiveFrom/To`、`items[]`、`expectedVersion` | `recipeId`、`recipeVersion`、`publishState`、`publishedBy/At`、`itemCount`、`serverTime` |
| N-58 | `POST /open/v1/catering/orders`(**远程点菜预留入口**) | 设备凭据 | L2 `orderId` | `ryid`、`items[]`、`recipeId`、`deliveryType` | **本期恒返回 `40940 FEATURE_DISABLED`** 且 `features.remoteOrdering=false`;不落订单、不扣款 |
> N-57 是公寓端管理入口,后端代理到平台餐饮管理端 `/platform/v1/admin/catering/recipes`;权限点 `apt:catering:recipe:list` / `create` / `publish` / `revoke`,前端 `v-hasPermi` 与后端 `@PreAuthorize` 同码并登记 `sys_menu.perms`。
表 12-24 阶段性食谱数据结构
| 字段 | 类型 | 必填 | 校验规则 |
| --- | --- | --- | --- |
| `recipeId` / `recipeVersion` | string(64) / bigint | 是 | 服务端生成;`version` 单调,同 `recipeId` 不允许覆盖已发布版 |
| `gyid` | string(32) | 是 | 只从会话解析,不接受请求参数 |
| `mealPeriod` | enum_s | 是 | `BREAKFAST` / `LUNCH` / `DINNER` / `NIGHT` / `ANY` |
| `effectiveFrom` / `effectiveTo` | datetime(3) ×2 | 是 | `from < to`;同 `gyid + mealPeriod` 区间重叠拒绝发布 |
| `publishState` | enum_s | 是 | 表 12-22 状态机 |
| `items[]` | array | 是 | 至少 1 项;每项 `mealId` 存在且 `enabled=1`、`price > 0` 且两位小数、`qty` 上限按 Q-45 |
| `imageRef` | string(512) | 否 | 只存引用占位;**本期不上传 / 不下载图片本体**,不新增对象存储桶,`docs/平台端/07` §4.1「只有两个桶」的边界不变 |
| `publishedBy` / `publishedAt` | string / datetime | 发布时 | 二次确认留痕 |
### 6.2 选单与远程点菜预留
- **选择菜单**:消费终端本地维护(设备主动拉取食谱 → 点击加入 → 实时显示合计金额),**确认前不落库**;确认后只按 CF-01 生成一条消费流水。终端退出 / 超时(默认 5 min)清空选单。
- **远程点菜入口**:#1 下行 `features.remoteOrdering`(默认 `false`)+ N-58 接口骨架 + `YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 两表预留;本期**不实现下单、不落订单、不扣款**,启用条件与配送 / 后厨 / 结算规则见 Q-44 与 `平台端/09` P-07。
- **价格一致性**:N-50 提交时服务端按 `recipeId + recipeVersion` 重算金额,与终端 `amount` 不一致 → `40936` 要求重拉食谱,防止改价期间错扣。
表 12-25 CF-03 异常与错误码
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 食谱未发布 / 已过期 / 已撤回 | `40936` + 终端重拉 N-48 | 不展示、不允许加入选单 |
| 菜品下架 / 价格变更 | `PF-6001` / `40936`,按服务端 `recipeVersion` 重算 | 不用终端旧价扣款 |
| 区间重叠发布 | 拒绝发布并提示调整 `effectiveFrom/To` | 不产生第二个同时生效的同餐次食谱 |
| 改价影响已发布版 | 生成新 `recipe_version` | 不改历史版本、不改已发生流水 |
| 远程点菜误调用 | `40940 FEATURE_DISABLED` | 不落订单、不扣款、不改额度 |
| 设备食谱缓存落后 | N-48 按 `version` 增量刷新 | 不用过期缓存计算金额 |
---
## 7. 提报与平台同步(CF-01 / CF-02 共用)
表 12-26 #10 `POST /platform/v1/report/ledger`(`bizType=LEDGER_OFFLINE`)payload 字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `envelope` | object | 是 | `contractVersion=v2-draft`、`gyId`、`bizType=LEDGER_OFFLINE`、`bizId=flowId`、`idempotencyKey=hash(gyid+bizType+flowId+bizVersion)`、`occurredAt`、`serverReceivedAt` |
| `flowId` / `type` | string / enum | 是 | 沿用 CR-02 表 3-15;`CONSUME` / `RECHARGE`(`ALLOWANCE_DEDUCT` 不用于消费拆分) |
| `amount` | string(2) | 是 | 字符串十进制两位小数;**扣减用负数、充值用正数**,禁止浮点 |
| `ryid` / `cardNo` / `deviceCode` | string ×3 | 是 | 账户、`GY_YKT_RY.card_id`、受理设备 |
| `offlineScope` / `localBalanceVersion` / `quotaRef` | enum / bigint / string | 是 | 沿用 F-D;断网期间恒 `LOCAL_ONLY` |
| `allowanceAmount` / `cashAmount` | string(2) ×2 | `CONSUME` 必填 | **本流程扩展**:拆分金额,`allowanceAmount + cashAmount = abs(amount)` |
| `allowanceBefore/After` / `cashBefore/After` | string(2) ×4 | 是 | **本流程扩展**:双额度 CAS 前后值,供平台对账 |
| `recipeId` / `recipeVersion` / `items[]` | string / bigint / array | 否 | **本流程扩展**:消费关联食谱与选单明细,不含图像本体 |
| `rechargeChannel` / `qrCodeId` / `confirmedBy` | enum / string / string | `RECHARGE` 必填 | **本流程扩展**:`OFFLINE_CASH` + 线下二维码标识 + 管理员核对留痕 |
| `syncedAt` | datetime | 否 | 归日仍以平台 `serverReceivedAt` 为准(CR-02 表 3-11) |
表 12-27 平台侧处理链路与落表
| 步 | 主体 | 动作 | 编号 | 落表 |
| --- | --- | --- | --- | --- |
| 1 | `platform-ingest` | 按 `idempotencyKey` 去重,落接收日志 | #10 | `PL_RECEIVE_LOG` |
| 2 | `platform-ledger` | 按 `serverReceivedAt` 归日;业务 UK `(gyid, type, flow_id)` 去重;`sum(amount)` 对账 | #10 | `PL_LEDGER_FLOW`(`reconcile_state` = `RECEIVED` / `RECONCILED` / `DIFF`) |
| 3 | `platform-ledger` | **唯一写入口**更新余额主账;消费按 `allowanceAmount` / `cashAmount` 分别冲减 `knye` / `xjye`,充值只增 `xjye` | 内部 | `GY_YKT_REMAINDER` |
| 4 | 公寓 `apt-report` | 日对账通过后把 `offline_scope` 置 `RECONCILED`、更新 `local_balance_version` | #16(本期公寓主动调用形态) | `apt_ledger_offline`、`gy_ykt_remainder` |
| 5 | 值班台 | 推 `ledger.offline` WS 消息(`flowId` / `offlineScope` / `localBalanceVersion` / `quotaRemain` / `seq`) | 端内 | `duty_operation_log` |
表 12-28 对账与冲正规则(沿用,不新造)
| 项 | 口径 |
| --- | --- |
| 三维比对 | `count`(按 `biz_id` 去重,R-C3)/ `sum(amount)` / `max(watermark)`;差异落 `PL_RECONCILE_DIFF` |
| 归日 | 平台 `serverReceivedAt`;公寓 `occurredAt` 不参与日切 |
| 重复语义 | `PF-5002` 业务重复流水 vs `PL-IDEMPOTENT-REPLAY` 幂等重放,两者不同 |
| 阻断 | 当日 `sum(amount)` 不等即 P0,`PF-5003` 阻断所有调账入口 |
| 冲正 | 只允许 `POST /adjustments`(带原因 + 审批人)经 `platform-ledger` 冲正,**不改余额表字段**、不改申请单终态 |
| 续报 | 断网恢复先派 outbox 再拉 #7(CR-01 §2.9),复用原 `idempotencyKey`,不产生新业务事实 |
---
## 8. 接口清单汇总
表 12-29 端内接口 N-48~N-58(**均不计入跨端 20 个编号**)
| 编号 | 方法 + 路径 | 权限 / 凭据 | 幂等 | 流程 | 用途 |
| --- | --- | --- | --- | --- | --- |
| N-48 | `GET /open/v1/catering/recipes` | 消费终端设备凭据 | L0(版本) | CF-03 / CF-01 | 拉取当前有效阶段性食谱与菜品 |
| N-49 | `GET /open/v1/ledger/quota` | 消费终端设备凭据 | L0 | CF-01 | 查询补贴额度与现金额度 |
| N-50 | `POST /open/v1/ledger/consume` | 消费终端设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | CF-01 | 提交选单与金额,触发双额度扣减 |
| N-51 | `GET /open/v1/ledger/flows/{flowId}` | 设备凭据 | L0 | CF-01 / CF-02 | 流水状态与拆分金额查询 |
| N-52 | `POST /open/v1/ledger/recharge-requests` | 签到终端设备凭据 + HMAC | **L2** `(gyid, deviceCode, deviceEventId)` | CF-02 | 扫码后发起线下现金充值申请 |
| N-53 | `GET /duty/ledger/recharge-requests` | `duty:ledger:local` | L0(游标) | CF-02 | 管理员待核对列表 |
| N-54 | `POST /duty/ledger/recharge-requests/{flowId}/confirm` | `duty:ledger:confirm` + 二次确认 | **L2** `commandId` | CF-02 | 核对实收金额并计入现金额度 |
| N-55 | `POST /duty/ledger/recharge-requests/{flowId}/cancel` | `duty:ledger:confirm` | **L2** `commandId` | CF-02 | 作废申请 |
| N-56 | `GET /apt/ledger/accounts/{ryid}` | `apt:ledger:list` | L0 | CF-01 / CF-02 | 账户额度、流水与对账状态查询 |
| N-57 | `GET/POST /apt/catering/recipes`、`PUT .../{recipeId}/publish`、`PUT .../{recipeId}/revoke` | `apt:catering:recipe:*` | L2 `recipeId + version` | CF-03 | 公寓管理员发布 / 撤回阶段性食谱 |
| N-58 | `POST /open/v1/catering/orders` | 设备凭据 | L2 `orderId` | CF-03 | **远程点菜预留入口**,本期恒 `40940` |
> 通用约定(前缀、HMAC + 时间窗 + `nonce`、错误码分段、`data.reason` / `data.traceId`、`gyid` 锁定)见 `docs/公寓端/04` §1;既有 N-09 / N-10(`duty:ledger:local`)保持不变,本文件不重复登记。
表 12-30 跨端接口(**沿用既有编号,不新增、不重排**)
| # | 接口 | 方向 | 本流程扩展 | 字段表 |
| --- | --- | --- | --- | --- |
| 1 | `GET /platform/v1/master/apartments/{gyId}` | 下行(拉) | `offlineQuota` 沿用;新增 `features.remoteOrdering`(默认 `false`) | CR-02 表 3-20 |
| 4 | `GET /platform/v1/master/rules?gyId` | 下行(拉) | 响应增 `recipes[]{recipeId, recipeVersion, mealPeriod, effectiveFrom/To, items[]}` + `recipeWatermark` | 表 12-24 |
| 10 | `POST /platform/v1/report/ledger` | 上行 | `LEDGER_OFFLINE` payload 扩展(拆分金额 / CAS 值 / 食谱 / 线下充值标记) | 表 12-26 |
| 16 | `POST /platform/v1/reconcile/run?gyId&date` | 补偿(本期公寓主动调用形态,P-02) | 日对账结论回写 `RECONCILED` | 表 12-28 |
---
## 9. 异常、一致性与权限
表 12-31 异常总表(三项流程一致口径)
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 设备断网 | 本地额度内受理,`offlineScope=LOCAL_ONLY`、outbox `PENDING`,恢复复用原 `idempotencyKey` 续报 | 不伪造 `SYNCED`、不产生新业务事实 |
| 超离线额度 | `40913 APT_OFFLINE_QUOTA_EXCEEDED`(#1 `offlineQuota` 缺省全 0 即断网不开放) | 不放行本地扣款 |
| 余额不足 | `REJECTED` + `PF-5001` / `40933` | 不部分扣款、不透支 |
| CAS 冲突 | 重读重试一次,再失败 `40935` 转人工 | 不覆盖新版本、不留半条余额 |
| 金额非法 | `40938` 拒收(> 0、< 100、≤ 2 位小数) | 不建申请单 / 流水 |
| 并发确认 / 重复提交 | 条件更新 + `commandId` / `deviceEventId` 幂等,冲突 `40939` | 不重复入账、不重复扣款 |
| 平台重复 / 幂等重放 | `PF-5002` 与 `PL-IDEMPOTENT-REPLAY` 分别处理 | 不混用两种语义 |
| 对账差异 | `DIFF` + `PF-5003` 阻断调账,人工走 `POST /adjustments` | 不直改余额表 |
| 食谱 / 价格失效 | `40936` / `PF-6001`,终端重拉 N-48 | 不用旧价 / 已下架菜品扣款 |
| 已离寓 / 未注册 | `40937` 拒收 | 不改 `finish_ly` / `ryzt`、不改人脸主档 |
| 远程点菜误调用 | `40940 FEATURE_DISABLED` | 不落订单、不扣款 |
| 越权 / 跨 `gyid` | `40301` 拒收并记审计 | 不接受请求参数指定 `gyid` |
表 12-32 权限与审计
| # | 要求 |
| --- | --- |
| 1 | **双校验**:`/apt/**` 走 RuoYi 会话 + `@PreAuthorize`,前端 `v-hasPermi` 同码;`/duty/**` 走 `duty:ledger:*`;`/open/v1/**` 走设备凭据 + HMAC + 时间窗 + `nonce`。新增权限点 `apt:catering:recipe:*`、`duty:ledger:confirm` 必须登记 `sys_menu.perms`(`docs/公寓端/02` §4.2) |
| 2 | **职责分离**:消费终端 / 签到终端只能提交请求与查询,管理员负责核对与发布,`apt.ledger.local` 落库,`platform-ledger` 写主账;设备与前端**零余额表写权限** |
| 3 | **二次确认**:N-54 充值确认、N-57 食谱发布 / 撤回必须二次确认并写 `duty_manual_action` |
| 4 | **审计**:选单确认、额度查询(异常时)、扣减、充值申请、核对、作废、发布、撤回、上报、对账十类动作全部记 `duty_operation_log`(操作人 / 设备、`flowId` / `commandId`、前后额度、结果) |
| 5 | **最小必要**:N-49 / N-53 / N-56 不返回人脸媒体;食谱只返回 `imageRef` 引用;抓拍、录音、签到现场图不出公寓 |
| 6 | **`gyid` 锁定**:只从会话 / 设备凭据解析;所有 SQL 必带 `gyid`,唯一键以 `gyid` 打头 |
| 7 | **金额安全**:一律字符串两位小数 / `decimal(12,2)`,禁止浮点;服务端按 `recipeVersion` 重算金额,不信任终端合计 |
---
## 10. 决策点与验收
表 12-33 待决口径(Q-41~Q-45,承接 `docs/公寓端/11` Q-40)
| 编号 | 事项 | 默认口径(未决前执行) | 拍板时点 |
| --- | --- | --- | --- |
| Q-41 | 补贴不足时是否允许现金补足 | **允许**(本流程核心规则);两者合计仍不足则全额拒绝,不部分扣款 | 阶段二契约冻结前 |
| Q-42 | 线下充值在断网时是否受 `offlineQuota` 约束 | **是**;缺省全 `0` 即断网不开放本地充值记账(沿用 Q-21) | 阶段三试点前 |
| Q-43 | 食谱主数据归属与 `knye` / `xjye` 语义复核 | 平台库 `YKT_MEALS` 补列 + 公寓只读下发;`knye`=补贴 / `xjye`=现金为设计映射,须按 `backend/11` 存量数据复核 | 阶段二契约冻结前 |
| Q-44 | 远程点菜启用条件 | 本期 `features.remoteOrdering=false`,仅留 N-58 入口与两表预留;后厨 / 配送 / 结算规则待 `平台端/09` P-07 定稿 | 后续迭代 |
| Q-45 | 充值单笔上限、是否需要双人复核、内部人员透支 | 单笔 `> 0 且 < 100`(沿用 `backend/04`)、二次确认即可、不允许透支 | 阶段三试点前 |
表 12-34 验收要点
| # | 验收项 | 判据 |
| --- | --- | --- |
| 1 | 消费流程 | 终端按 `PUBLISHED` 食谱选菜并显示金额;N-49 返回两条额度;扣减严格"补贴优先、现金补足";一笔消费只产生一条 `flowId` 且 `allowanceAmount + cashAmount = abs(amount)`;CAS 前后值留痕 |
| 2 | 充值流程 | 扫码只识别公寓、无支付订单与回调;申请表不存金额;管理员二次确认后只增 `xjye`;`channel=OFFLINE_CASH`;`request_state` 沿用三值、终态不可回写 |
| 3 | 菜品管理 | 阶段性食谱按 `recipe_id + version + period + 区间` 发布;终端只看到当前有效版;选单确认前不落库;N-58 恒 `40940` 且 `features.remoteOrdering=false` |
| 4 | 提报与同步 | #10 `LEDGER_OFFLINE` 字段与表 12-26 一致;`bizId=flowId`、`idempotencyKey` 复用;归日以 `serverReceivedAt`;日对账通过才置 `RECONCILED`;差异 `PF-5003` 阻断调账 |
| 5 | 状态一致性 | 四条状态链(表 12-4)互不替代;`ledger_state` / `request_state` / `offline_scope` / `publish_state` 与 §4~§6 状态机、§9 异常表逐项对应;终态不回写 |
| 6 | 异常完整性 | 余额不足、CAS 冲突、断网超额度、重复提交、对账差异、食谱改价、远程点菜误调用、越权均有明确错误码与"不做什么" |
| 7 | 边界不破 | 跨端 20 编号、`v2-draft`、平台零回调、**59 张表**、不新增 `bizType`、`apt.ledger.local` / `platform-ledger` 单写入口不变 |
| 8 | 安全与审计 | 十类动作全部留痕;新增权限点前后端同码并登记 `sys_menu.perms`;`gyid` 不可由请求参数指定;金额禁止浮点、服务端重算 |
@@ -0,0 +1,468 @@
# 13 公寓端小步迭代开发计划(入住·叫乘优先,消费充值延后)
> 文档编号:`docs/公寓端/13`。性质:**实施排期设计稿**,不是设计变更、不改写任何既有权威表述;本文件回答"按什么顺序写代码、每步怎么验、消费充值何时接",不回答"业务规则是什么"。
> **权威关系**:业务规则 / 状态 / 接口 / 验收口径一律以 `docs/设计变更/03`(CR-02 §11~§15)、`docs/公寓端/05` §9~§11、`docs/公寓端/10`、`11`、`12`、`docs/公寓端/09` 为准;阶段与批次以 `docs/10` §9 表 9-1~9-6、`docs/公寓端/07` 表 7-1~7-8 为准。本文件只做**排序、切分、对齐、衔接**,冲突时以上述文件为准。
> **上位依据**:`dev/RuoYi-Vue-master/`(后端)、`dev/RuoYi-Vue2-master/`(前端)实测代码基础;`docs/10` §9 / §11.2 表 11-1 / §10.2 表 10-2;`docs/公寓端/02`、`03`、`04`、`06`、`07`、`08`、`09`;`docs/设计变更/01`(CR-01)、`03`(CR-02);`docs/应用端/07`、`docs/平台端/08`、`09`。
> **硬约束**:不新增 `M7` / 阶段六 / `D-4` 等既有编号体系;跨端仍 **20 个接口编号**、契约 `v2-draft`、平台零回调;公寓本地库业务表 **59 张**口径不变;`apt.duty.state` 仍是业务终态唯一写入口;**冻结前不写 Controller**(`公寓端/08` R-02);DDL 待**阶段二契约冻结评审通过**后执行(Q-40 / Q-43 / Q-48);本轮不执行 `git commit` / `git push`。
表 13-1 文档信息
| 项 | 内容 |
| --- | --- |
| 范围 | 公寓端 `IT-0`~`IT-8` 九个小步迭代 + 延后 `IT-9`~`IT-11`(消费 / 充值 / 菜品);含迭代划分、里程碑、交付物、对齐矩阵、风险台账、跨迭代联调与测试、衔接规划 |
| 优先级 | **P1 入住 + 叫乘核心闭环优先**(`IT-3`~`IT-5`);**P2 消费 / 充值 / 菜品整体延后**(`IT-9`~`IT-11`) |
| 编号规则 | 迭代 `IT-y`、里程碑 `MS-y`、风险 `IR-y`、交付物 `DV-y`(**均为本文件内编号,不占用** `M0`~`M6` / `D-1`~`D-3` / `Q-*` / `P-*` / `PR-*` / `R-*`) |
| 术语 | 「核心闭环」= 人脸注册与核验 → 入住申请 / 审核 / 办理 → #9 提报与平台匹配 → 叫乘任务 → 状态机执行 → outbox 上报;「延后域」= 消费 `CF-01`、线下充值 `CF-02`、菜品管理 `CF-03` |
表 13-2 修订记录
| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-11-10 | 首次形成:基于 `dev/` 实测上游代码与既有阶段 / 批次口径,切分 9 个小步迭代;登记 6 项 P0 排期前提与技术基线偏离;给出里程碑、交付物、风险台账、跨迭代联调测试与消费充值衔接规划 |
---
## 1. 现状基线与排期前提
### 1.1 `dev/` 代码基础实测
表 13-3 `dev/` 现状(**本计划的物理起点**)
| 项 | 实测结果 | 相对路径 / 证据 |
| --- | --- | --- |
| 后端 | RuoYi **多模块 Maven**,`3.9.2`,JDK 17,Spring Boot `4.1.0`,MyBatis `4.1.0`,druid `1.2.28`,springdoc `3.1.0`;6 模块 `ruoyi-admin`(唯一启动 jar,端口 8080)/ `framework` / `system` / `common` / `quartz` / `generator` | `dev/RuoYi-Vue-master/pom.xml`、`ruoyi-admin/src/main/resources/application.yml` |
| 前端 | Vue `2.6.12` + element-ui `2.15.14` + vue-router `3.4.9` + vuex `3.6.0` + **vue-cli 4.4.6**(无 vite);单入口 `src/main.js` + `src/permission.js` 全量 `sys_menu` 动态路由;devServer 80,代理 8080 | `dev/RuoYi-Vue2-master/package.json`、`vue.config.js`、`src/router/index.js` |
| DDL | 仅 **31 张**:`sql/ry_20260417.sql` 20 张 `sys_`/`gen_`、`sql/quartz.sql` 11 张 `QRTZ_`,库名 `ry-vue` | `dev/RuoYi-Vue-master/sql/`、`application-druid.yml` |
| 业务代码 | **全部为净新增**:`gy_jcxx` / `gy_rooms` / `gy_ykt_*` / `apt_*` / `duty_*` 表 0 张;入住 / 叫乘 / 交路 / 房间 / 消费充值 / 菜品 Controller·Service·Mapper 全无;`apt-*` 模块目录 0 命中 | 265 个 `package` 全为 `com.ruoyi.*`;25 个 Controller 全为 `Sys*`/`Gen*`/监控类 |
| 外部能力 | Asterisk / AMI、SIP、人脸、WebSocket、`/open/v1/**`、设备签名 / mTLS **全部 0 命中**;pom 无对应依赖 | `dev/RuoYi-Vue-master/*/pom.xml`、`SecurityConfig.java` |
| 可直接复用 | RBAC(`sys_menu` + `@PreAuthorize("@ss.hasPermi")` + `v-hasPermi` 47 处)、`@Log` + `LogAspect` 审计、代码生成 `ruoyi-generator`、Quartz 基座(`ScheduleConfig` **整类被注释 = 默认 RAMJobStore**,qrtz 表已建未用)、文件上传、springdoc(**`packages-to-scan` 仅 `...controller.tool`**) | `ruoyi-framework`、`ruoyi-common`、`ruoyi-quartz`、`ruoyi-generator` |
> **结论**:`docs/公寓端/07` 表 7-2 的 `M0`~`M6` 不是"改造已有骨架",而是**从零新增业务模块**;设计文档引用的 `apartment/README.md`、`apartment/db/mysql` 44 张表骨架在工作区**不存在**。因此 `IT-0` 必须承担"基线决策 + 建库 + 脚手架 + CI/测试"的前置工作,否则 `IT-1` 无法开工。
### 1.2 设计与代码的差距(P0 排期前提)
表 13-4 六项 P0 前提与推荐处置(**均须评审签字,本计划不擅自改设计**)
| # | 差距 | 设计锁定值 | `dev` 实测 | 推荐处置 | 阻塞迭代 |
| --- | --- | --- | --- | --- | --- |
| `IR-01` | **技术基线冲突** | JDK 1.8 / Boot 2.5.15 / RuoYi 3.8.9 / springfox 3.0.0 / 后端 8086 / 前端 8082 | JDK 17 / Boot 4.1.0 / 3.9.2 / springdoc 3.1.0 / 8080 / 80 | **推荐方案 B:修订 `docs/公寓端/02` §1 基线锁定值以匹配上游**,走设计变更评审登记;理由:上游未改动、"框架不魔改",降级 pom 回退官方版本风险更高。方案 A(降级 dev)列为备选。Swagger 注解由 `@Api` 改 `@Tag/@Operation` | `IT-0` |
| `IR-02` | **表数口径冲突** | 44 存量 / 74 `apt_apartment` / CR 后 **59 张** | 31 张纯 `sys_`/`gen_`/`QRTZ_` | 以 **59 张为设计口径**,产出分域增量 DDL(DDL 与回填分离、回填可重放,CR-02 表 3-33);`07` 表 7-5 第 5 条"44 张已实测通过"在当前 `dev` 不成立,**改为 IT-0 的验收项**;改 `application-druid.yml` 库名与凭据 | `IT-0`、`IT-1` |
| `IR-03` | **前端入口冲突** | `src/entries/{apt,duty}` 双入口 + `src/shared/**` + apt 固定路由 | 单 `main.js` + 全量动态路由 | `IT-0` 只建 **`apt` 单入口脚手架**,`duty` 入口随 `应用端` D-1~D-3 再开;`sys_menu` 只作权限点载体、apt 业务路由走 `constantRoutes` 固定注册 | `IT-0`、`IT-6` |
| `IR-04` | **无 CI / 测试工程** | `07` §6 表 7-6 的 8 类用例 + 3 类专项 | 无 workflows、无 `src/test`、无 `spring-boot-starter-test` / surefire | `IT-0` 补 `spring-boot-starter-test`、JUnit5 + MockMvc、CI 工作流(编译 / 单测 / 明文凭据正则扫描 / 架构测试);每迭代把表 7-6 用例沉淀为回归基线 | `IT-0` |
| `IR-05` | **明文凭据** | `07` 表 7-5 第 8 条「仓库无明文口令」 | `application-druid.yml` `root/password`、druid 控制台 `ruoyi/123456`;`application.yml` `token.secret` | `IT-0` 全部外置到环境变量 / 配置中心,CI 加正则扫描卡口;阶段零"已完成"结论在 `dev` 上**重新验收** | `IT-0` |
| `IR-06` | **契约与 DDL 未冻结** | 阶段二退出前不写 Controller(R-02);Q-40/Q-43/Q-48 待决 | 无 | `IT-0` 组织冻结会:F-01~F-14 冻结清单、`D5`/`D8'`/`D3`/`D17`、`P-02`/`P-03`/`P-06`/`P-08`、`Q-15`/`Q-22`/`Q-25`;未决前用 `duty_operation_log` JSON 兜底留痕(Q-40),平台侧 Q-48 用 `PL_JOB_*` JSON 兜底 | `IT-0`~`IT-4` |
### 1.3 优先级依据与口径偏离登记
表 13-5 「消费充值延后」的权威依据
| 依据 | 原文口径 | 对本计划的作用 |
| --- | --- | --- |
| `docs/10` §11.2 表 11-1 **P2-1** | 「账务:平台主账 + 公寓流水 + 日对账;餐饮改走账务接口」,理由「**数据一致性风险最高的一环**」 | 消费 / 充值排 P2,不进 `IT-0`~`IT-8` |
| `docs/10` §11.2 表 11-1 **P1-3** | 「叫乘状态机单一入口 + 盯控台」 | 叫乘进 P1 核心闭环 |
| `docs/10` §9 表 9-4 阶段三 | 「断网 2 小时完成『入住→叫乘→复叫→报警→应答→离寓→归档』」 | 核心闭环的业务边界 |
| `公寓端/07` 表 7-4 第 6 行 | 账务本地流水三处标 **P2**:`/apt/ledger/**`、`ledger/ConsumeList.vue`、`apt:ledger:*` | 本期不开账务接口 / 页面 / 权限点 |
| `公寓端/07` 表 7-2 + §2 | M0→M1→M2→**M3 叫乘**→M4→M5→M6,「顺序即依赖顺序,不允许跳批」 | 迭代依赖链不得跳批 |
| CR-02 §2.4 表 3-5 | 后续 **P2-1**:补助规则引擎化、餐饮结算规则、跨公寓考核报表 | 延后域挂 P2-1 |
| `公寓端/09` 表 9-17 第 5 步 | 「打开 #10 ledger(**P2**)」 | 联调顺序天然把账务放后,延后不改既有 8 步 |
| `平台端/09` P-07 / Q-44 | 餐饮域归属「阶段五」;`features.remoteOrdering=false`、N-58 恒 `40940` | 菜品与远程点菜一并延后 |
表 13-6 口径偏离登记(**须评审确认,不改写既有文档**)
| # | 偏离 | 说明 | 建议处置 |
| --- | --- | --- | --- |
| 1 | CR-02 `R-53` / `R-62` 已撤销「本期不提供入住 / 换房 / 退房接口」「不做任何账务动作」,即消费充值在**设计范围**内;但 `07` 表 7-4 仍标 P2、§14 抬头明确「不改变本文件阶段划分与批次顺序」 | 本计划把消费充值**实现**排 `IT-9`~`IT-11`,属"设计已覆盖、实现延后",不是删需求 | 在 CR-02 修订记录登记 v1.7 指向本文件;不改 §12~§15 任何权威表述 |
| 2 | `07` 表 7-1 称阶段零「已完成」、表 7-5 第 5 条称「44 张表已实测通过」 | 该结论基于工作区不存在的 `apartment/` 骨架,在 `dev/` 上不成立 | 阶段零在 `dev` 上**重新验收**(`IT-0`),44 张表改由 `IT-0` 增量 DDL 交付;不删除既有结论,只登记适用前提 |
| 3 | 设计按 `apartment/` 12 模块命名;`dev` 只有 6 个 `ruoyi-*` 模块 | 模块名与包根不同 | 采用「**若依模块承载 + 业务包分层**」映射(表 13-21),不新建 12 个 Maven 模块,避免破坏上游构建 |
---
## 2. 迭代划分
### 2.1 总览
表 13-7 迭代总览(`IT-0`~`IT-8` 为本期,`IT-9`~`IT-11` 为延后衔接)
| 迭代 | 名称 | 目标 | 范围(含 / 不含) | 前置依赖 | 建议周期 | 对应既有批次 / 阶段 |
| --- | --- | --- | --- | --- | --- | --- |
| `IT-0` | 基线对齐与契约冻结 | 把 `dev` 上游变成可开工的工程底座,冻结契约 | **含**:基线决策、分域增量 DDL、`apt` 单入口脚手架、CI/测试骨架、凭据外置、F-01~F-14 与 D5/D8'/D3 签字;**不含**:任何业务 Controller | 无(起点) | 3 周 | 阶段零重验 + 阶段二(表 9-3)主体 |
| `IT-1` | 运行时基座 | `gyid` 隔离、错误码、审计、`/apt/**` 鉴权成型 | **含** `AptContext`、拦截器、`AptErrorCode`、审计切面、分页、springdoc 分组;**不含**业务表 CRUD | `IT-0` | 2 周 | **M0** |
| `IT-2` | 主数据与房间 | 交路 / 房间 / 主数据快照可读可管 | **含**交路 CRUD + 复算校验、房间档案 + 房态、Mock 主数据下行;**不含**人员关联 Tab、叫乘 | `IT-1`;`D8'`/`D3` 已签 | 3 周 | **M1 + M2** |
| `IT-3` | 人脸注册与签到核验(最小闭环) | 支撑入住 `CI-01` 的刷脸核验前置 | **含** N-34 `REG_APPLY` → N-36 值班员确认 → `apt_master_person` 入档 → #9 `PERSON_REG` 同步回执 → 刷脸核验会话;**不含**设备批量下发 / 离寓擦除 / 媒体签名(挪至 `IT-7`) | `IT-1`、`IT-2`(房间 / 人员主档);CR-C25 链路 | 2 周 | §12 `F-F` / `F-J` 最小集 |
| `IT-4` | **入住作业核心** | 跑通 `CI-01`~`CI-05` 五环节 | **含**入住申请(`apt_local_task` 补列)、值班员审核与换房、`apt-stay` 办理入住建 `gy_jcxx`、#9 `CHECKIN` 提报、#7 `matchVerdict[]` 拉取、任务生成与 `jcsj` 复算;N-38~N-47;**不含**消费、账务 | `IT-2`、`IT-3`;Q-40 DDL 或 JSON 兜底 | 3 周 | §13 `F-K` + **M3 前置** |
| `IT-5` | **叫乘执行与状态机** | `apt.duty.state` 单一入口 + 平板 / SIP 双模式最小集 | **含** `STOP`/`RE_CALL` 命令、状态时间线、`jcsj` 复算、平板 `TABLET_RESULT`(#9)/`UNVERIFIED` 复叫、SIP 降级回落;**不含**录音索引 / 12 项自检 | `IT-4`;M3 状态机、`D8'` | 3 周 | **M3** + §11 `F-A` / `F-B` |
| `IT-6` | 上报续传 + 值班台 / 人员 / 设备 | 断网可跑、恢复可追平;值班台"看 + 干预" | **含** outbox + 水位 + #9/#8/#13 续报、人员关联 Tab(M4)、设备网关指令入队(M5)、应用端 `D-1`~`D-3` 最小集;**不含** #10 `LEDGER_OFFLINE` | `IT-5`;Q-21/Q-22/Q-23 | 3 周 | `M4` / `M5` / `D-1`~`D-3` |
| `IT-7` | 语音运维 + 人脸补齐 | 可叫乘探针、降级路径、人脸设备侧补齐 | **含**分机台账 / 会话 / 录音索引、`apt-ops` 12 项自检、`call-ready`、Asterisk 降级;人脸设备下发 / 擦除 / 媒体边界 | `IT-5`;`D11`/`D12`/`D15`/`D16` 已定稿、Asterisk 可达 | 3 周 | **M6** + §12 补齐 |
| `IT-8` | 单公寓试点与灰度 | 阶段三退出 | **含**断网 2h 演练、恢复 10min 追平、灰度与回滚演练、阶段三 10 项验收;**不含**多公寓推广 | `IT-0`~`IT-7` 全通过 | 2 周 | 阶段三(表 9-4)退出 |
| `IT-9`~`IT-11` | **消费 / 充值 / 菜品(延后)** | 见 §8 衔接规划 | `CF-01` / `CF-02` / `CF-03` | 见 §8 表 13-28 | 待排 | 后续 **P2-1** |
图 13-1 依赖链(**顺序即依赖顺序,不允许跳批**)
```mermaid
flowchart LR
I0["IT-0 基线/契约冻结"] --> I1["IT-1 基座 M0"]
I1 --> I2["IT-2 主数据 M1+M2"]
I2 --> I3["IT-3 人脸注册最小闭环"]
I3 --> I4["IT-4 入住核心 CI-01~05"]
I4 --> I5["IT-5 叫乘状态机 M3"]
I5 --> I6["IT-6 上报/值班台/设备"]
I5 --> I7["IT-7 语音运维 M6"]
I6 --> I8["IT-8 试点与灰度"]
I7 --> I8
I8 -. "P2 进入条件满足后" .-> I9["IT-9~11 消费/充值/菜品"]
```
### 2.2 拆分逻辑
表 13-8 业务优先级与依赖顺序(为什么这样排)
| 序 | 判断 | 依据 |
| --- | --- | --- |
| 1 | 先 `IT-0` 再任何编码 | 阶段二冻结前不写 Controller(`公寓端/08` R-02);`dev` 无表 / 无 CI / 有明文口令,阶段零需重验 |
| 2 | 基座 `M0` → 主数据 `M1`+`M2` | `07` 表 7-2 明确 M2 依赖 M1、M3 依赖 M1+M2;`D5`→M0、`D8'`→M1、`D3`→M2 为强阻塞(`公寓端/08` 表 8-11) |
| 3 | **人脸注册最小闭环先于入住** | `CI-01` 要求"未注册先按 §12 完成注册"(CR-C25);把 §12 切成"最小闭环(`IT-3`)+ 设备侧补齐(`IT-7`)"是本期最重要的小步切分,避免人脸媒体 / 擦除阻塞入住 |
| 4 | **入住 `IT-4` → 叫乘 `IT-5`** | 叫乘任务只在 `CHECKED_IN` 且有效在寓时生成(`公寓端/05` §10 衔接约束②);`applyId` ↔ 预分配 `jcid` 一对一由入住侧产生 |
| 5 | 叫乘先于人员关联 `M4` | `07` §2「为什么 M3 在 M4 之前」:先做 M3 让 M4 关联 Tab 直接可验证,避免两次返工 |
| 6 | 上报 `IT-6` 紧随叫乘 | 断网 2h / 恢复 10min 追平是阶段三退出条件;outbox 不能等语音做完才补 |
| 7 | 语音 `IT-7` 并行于 `IT-6` | `D11`/`D12`/`D15`/`D16` 可并行定稿(`公寓端/08` 结论「语音三项并行启动、不等开发进度」) |
| 8 | 消费 / 充值 / 菜品整体延后 | 表 13-5 的 P2-1「数据一致性风险最高」;且 #10 在 `公寓端/09` 表 9-17 联调第 5 步本就标 P2 |
表 13-9 与既有编号体系的映射(**不新增 M7 / 阶段六 / D-4**)
| 本计划迭代 | 既有批次 / 阶段 | 应用端子批次 | 全局验收项 |
| --- | --- | --- | --- |
| `IT-0` | 阶段零重验 + 阶段二(表 9-3) | — | 表 3-37、F-01~F-14 冻结清单 |
| `IT-1` | M0 | —(D-1 不得早于 M3) | 表 7-3 M0 项 |
| `IT-2` | M1 + M2 | — | 表 7-3 M1/M2 项 |
| `IT-3` | §12 F-F / F-J 最小集 | — | CR-02 §12 验收 15~17(最小集) |
| `IT-4` | §13 F-K | — | **§13 表 3-57 验收 18~20** |
| `IT-5` | M3 + §11 F-A / F-B | **D-1 起**(不得早于 M3) | 表 7-3 M3 项、§11 验收 |
| `IT-6` | M4 / M5 + outbox | **D-1~D-3** | 表 7-5 第 1/2 项、表 7-7 步骤 1~8 |
| `IT-7` | M6 | D-2 / D-3 收口 | 表 7-3 M6 项、§12 验收 15~17(补齐) |
| `IT-8` | 阶段三退出(表 9-4) | — | 表 7-5 全 10 项 |
| `IT-9`~`IT-11` | 后续 P2-1 | — | §14 表 3-62 验收 21~23、§15 表 3-69 验收 25 |
---
## 3. 各迭代明细
> 每个迭代的**验收标准**均须可复现;"对齐文档"列给出该迭代必须逐条核对的权威位置,"对齐代码模块"列给出 `dev` 实际承载路径。跨迭代回归基线见 §7。
表 13-10 `IT-0` 基线对齐与契约冻结(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 把 `dev` 上游变成可开工底座,完成阶段二契约冻结 |
| 范围 | 表 13-4 六项 P0 处置;`IR-01` 基线决策评审、59 张分域增量 DDL(DDL 与回填分离、可重放)、`apt` 单入口脚手架 + 固定路由、CI/测试骨架、凭据外置、F-01~F-14 与 `D5`/`D8'`/`D3`/`D17`/`P-02`/`P-03`/`P-06`/`P-08` 签字 |
| 依赖 | 无;但需产品 / 架构 / 平台 / 应用端四方评审 |
| 对齐文档 | `docs/10` §9 表 9-1/9-3、表 10-2、§11.3;`公寓端/02` §1/§2.4;`公寓端/08` 表 8-1/8-11;`公寓端/09` 表 9-18;CR-01 §5/§6、CR-02 表 3-33/表 3-36 |
| 对齐代码模块 | `dev/RuoYi-Vue-master/pom.xml`、`ruoyi-admin/src/main/resources/{application.yml,application-druid.yml}`、`ruoyi-framework/.../SecurityConfig.java`、`SwaggerConfig.java`、`ruoyi-quartz/.../ScheduleConfig.java`、`dev/RuoYi-Vue2-master/{vue.config.js,src/router,src/permission.js}` |
| 交付物 `DV-0` | 基线偏离评审纪要、分域增量 DDL 脚本 + 回填脚本、脚手架、CI 工作流与测试骨架、冻结清单签字版、明文凭据扫描报告 |
| 验收标准 | ① 主干可稳定编译、单测可重复执行;② 仓库 CI 正则扫描无口令 / 内网地址 / AMI 凭据;③ 59 张表脚本二次执行幂等;④ `sys_config` 可读、`ruoyi-admin`(设计名 `apt-admin`)以 `local` profile 启动无异常;⑤ F-01~F-14 逐条签认(含 CR-01 建议的 F-14);⑥ `IR-01` 基线方案 A/B 二选一形成书面结论 |
| 退出 / Go-NoGo | 契约冻结签字完成;**未冻结不进入 `IT-1`**(R-02) |
表 13-11 `IT-1` 运行时基座 `M0`(2 周)
| 项 | 内容 |
| --- | --- |
| 目标 | `gyid` 隔离 / 统一错误码 / 审计 / 鉴权成型,让后续每域"只写业务" |
| 范围 | `AptContext`(`gyid` 只从会话 / 设备凭据解析)、四前缀拦截器、`AptErrorCode` 与 `{code,msg,data.traceId}`、`@Log` 扩展审计切面、分页上限 100、springdoc 分组扫 `com.ruoyi.apt.**`、`/apt/**` 鉴权;业务表只读查询骨架 |
| 依赖 | `IT-0`;`D5`(一进程一 `gyid`)已签 |
| 对齐文档 | `公寓端/04` §1/§2、`公寓端/03` §5 四级隔离、`公寓端/07` 表 7-3 M0 项、表 7-6 8 类用例 |
| 对齐代码模块 | `ruoyi-framework`(`SecurityConfig`、`WebMvcConfig`)、`ruoyi-common`(`AjaxResult`/异常体系)、`ruoyi-admin`(启动类 + Swagger 分组)、`ruoyi-system`(`sys_menu` 权限点种子) |
| 交付物 `DV-1` | 拦截器 + 错误码 + 审计切面 + `AptContext` + 分页封装 + Swagger 分组 + 权限点种子 SQL + M0 用例脚本 |
| 验收标准 | 表 7-3 M0 四项全过:无令牌 `/apt/route/list` → 401;有令牌缺权限点 → 40301;`gyid=A` 查 `gyid=B` → 40302 且无数据泄露;异常统一返回无堆栈外泄;`pageSize=1000` 截为 100;架构测试断言所有业务 Mapper SQL 带 `gyid` |
表 13-12 `IT-2` 主数据与房间 `M1+M2`(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 交路、房间、主数据快照可读可管,为入住提供房间与交路要素 |
| 范围 | 交路 CRUD + 复算校验 + 启停原因;房间档案 + 房态 + 停用/恢复 + `state_version`;#1~#4 主数据下行 Mock(`source='LOCAL'`、页面标"非平台下发") |
| 依赖 | `IT-1`;`D8'`(叫乘时刻口径)、`D3`(房间主数据)、`D7` 已签 |
| 对齐文档 | `公寓端/07` 表 7-3 M1/M2 项、表 7-4 第 1/2/8 域;`公寓端/09` §7 表 9-16 Mock 策略;CR-01 §2.8 Q8(交路批量预置) |
| 对齐代码模块 | 新建 `com.ruoyi.apt.master.*`(`ruoyi-system` 内业务包)、`com.ruoyi.apt.stay.*`;`ruoyi-generator` 生成 CRUD 骨架;`RuoYi-Vue2-master/src/entries/apt/views/{route,room}` |
| 交付物 `DV-2` | Controller/Service/Mapper/DTO/VO/页面/路由/权限点;Mock 灌数脚本(3 交路 / 5 房间 / 5 人员 / 2 天计划);表 7-6 的 M1/M2 用例记录 |
| 验收标准 | 缺 `cc`/`kcsj`/`reason` → 40001 且指出字段;同 `jlid` 重复 → 40901;停用审计 `duty_operation_log` 含原因;房态筛选只返回 `room_status=4`;停用后恢复触发重算且 `state_version` 递增;快照字段改 `xm` → 40904;Mock 数据标记 `source=LOCAL` / `source_version=0` |
表 13-13 `IT-3` 人脸注册与签到核验最小闭环(2 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 支撑入住 `CI-01` 的刷脸核验,且不被人脸媒体/擦除复杂度阻塞 |
| 范围 | **含** N-34 `REG_APPLY` → N-35 待审 → N-36 值班员 `CONFIRM`(生效点)→ `apt_master_person` 入档 → #9 `PERSON_REG` 同步 + #2 `personRegVerdicts[]` 回执 → 签到终端刷脸核验会话(`faceVerifyState`);**不含**设备批量下发 `FACE_ISSUE`、离寓擦除、媒体签名与对象存储、长期驻留保留(挪 `IT-7`) |
| 依赖 | `IT-1`、`IT-2`(人员 / 房间主档);CR-C25 注册链路、CR-C24 媒体边界 |
| 对齐文档 | `公寓端/10` §4 注册链路与状态机、表 10-26/10-27;CR-02 §12(CR-C24/CR-C25、验收 15);`公寓端/11` §4 `CI-01`;Q-30/Q-35 |
| 对齐代码模块 | `com.ruoyi.apt.face.*`(`ruoyi-system` 内)、`com.ruoyi.apt.stay.*` 核验会话;设备侧 `/open/v1/face/**` 匿名分区 + 设备凭据校验(`SecurityConfig` 扩展,不放行全量) |
| 交付物 `DV-3` | 注册申请 / 审核 / 主档 / 同步回执最小链路、核验会话接口、注册态与回执态字段、`40914`/`40917`~`40924` 错误码落地 |
| 验收标准 | ① 签到终端只能申请,值班台 `duty:person:reg` + 二次确认才可确认 / 驳回;② `APPLIED` 阶段不上传平台、不参与匹配 / 下发(`40914`);③ 重复确认按 `commandId` 幂等返回原 `ryid`;④ 平台 `REJECTED` **不回滚**本地注册(Q-35),只告警 + 待办;⑤ `faceVerifyState` 三值 `VERIFIED`/`UNVERIFIED`/`SKIPPED` 与 `公寓端/05` §9 一致;⑥ 抓拍帧 / 人脸原图 / 比对帧不出终端 |
表 13-14 `IT-4` **入住作业核心** `CI-01`~`CI-05`(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 跑通"注册签到 → 入住申请 → 值班员审核 → 提报匹配 → 叫乘任务"五环节 |
| 范围 | **含** `apt_local_task` 申请阶段补列(Q-40 冻结前用 `duty_operation_log` JSON 兜底)、N-38~N-47、值班员审核 / 换房 / 驳回、`apt-stay` 单事务建 `gy_jcxx` 行、#9 `CHECKIN` 提报、#8 回执作匹配输入、#7 `matchVerdict[]` 拉取、`jcsj` 公寓复算、`gygl_ly_event(CHECKIN)`;**不含**账务 / 消费 / 充值 |
| 依赖 | `IT-2`、`IT-3`;`D8'`/`D17`/`Q-15` 已定;`PL_TASK`/`PL_TASK_ACK` 字段对齐(CR-01 §5 阶段二挂载点) |
| 对齐文档 | `公寓端/11` 全文(表 11-1~11-35、图 11-1~11-3);CR-02 §13 表 3-57 **验收 18~20**;`公寓端/05` §10;`公寓端/09` §0 表 C-7;`docs/设计变更/01` §2.4 / §5 |
| 对齐代码模块 | `com.ruoyi.apt.stay.*`(`apt-stay` 子域)、`com.ruoyi.apt.dispatch.*` 任务生成、`com.ruoyi.apt.report.*` #9 上行、`com.ruoyi.apt.master.*` #7 拉取;前端 `entries/apt/views/checkin/**`、`entries/duty/views/checkin/**` |
| 交付物 `DV-4` | 申请单 / 审核 / 办理 / 提报 / 匹配全链路代码与页面、增量 DDL(或 JSON 兜底)、`applyId`↔`jcid` 映射与唯一约束、Q-36~Q-40 默认口径配置 |
| 验收标准 | **§13 验收 18~20 全过**:① 五环节闭环,`CHECKIN` 提报 → #7 `matchVerdict` → 拉任务并复算 `jcsj`;② "已入住" = `checkin_state=CHECKED_IN` 流程态 + `finish_ly='0' AND sjlysj IS NULL` 业务事实 + `gygl_ly_event(CHECKIN)`,**不新增 `gy_jcxx` 状态字段**;③ `applyId` ↔ 预分配 `jcid` 一对一,申请阶段**不建** `gy_jcxx` 行,审核通过由 `apt-stay` 单事务建行;④ `match_state` 五值、`UNMATCHED`/`LOCKED` 不清空本地要素;⑤ `PL_TASK.jcsj` 仅作基线,权威值恒由公寓复算(CR-C06);⑥ 边界不破:59 张表、20 编号、`v2-draft`、平台零回调 |
表 13-15 `IT-5` **叫乘执行与状态机** `M3`(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | `apt.duty.state` 成为业务终态**唯一**写入口,平板 / SIP 双模式最小闭环 |
| 范围 | **含** `STOP`/`RE_CALL` 受控命令 + `commandId` 幂等、状态时间线(状态迁移 + AMI 事件 + 人工命令按时间升序)、`jcsj` 复算、平板 `TABLET_RESULT`(#9)与 `UNVERIFIED` 复叫(Q-22)、SIP 降级回落(CR-C23)、只读字段不可改;**不含**录音索引、`apt-ops` 12 项自检 |
| 依赖 | `IT-4`;M3 状态机、`D8'`、Asterisk 或 Mock 可达;应用端 `D-1` **不得早于本迭代** |
| 对齐文档 | `公寓端/07` 表 7-3 M3 项 / 表 7-4 第 3 域 / 表 7-5 第 7 项;`公寓端/05` §9(F-A/F-B)、§11;CR-02 §11;`应用端/07` §1/§2(`D-1`) |
| 对齐代码模块 | `com.ruoyi.apt.dispatch.*`(状态机唯一入口)、`ruoyi-quartz`(定时触发;**须启用 `ScheduleConfig` 或显式登记仍用 RAMJobStore**)、`com.ruoyi.apt.device.*` 平板结果接入、`com.ruoyi.apt.voice.*` 最小 SIP 适配 |
| 交付物 `DV-5` | 状态机 + 命令层 + 时间线 + 平板/SIP 结果接入 + 页面(列表/详情/时间线/命令)+ 架构测试(全仓 grep 状态字段 UPDATE 只出现在 `apt.duty.state`) |
| 验收标准 | 表 7-3 M3 四项:`PUT /apt/jcxx/{id}` 不存在、`correct` 改 `ryzt` → 40001;同 `commandId` 重复 `STOP` 只生效一次;对 `finish_ly='1'` 的 `STOP` → 40902;时间线三类事件齐全;平板 `UNVERIFIED` 保持待确认 + 复叫、不判完成;迟到 / 重复结果 `IGNORED`;平板链路**不发 #24**;平台不可达时按最后快照继续执行 |
表 13-16 `IT-6` 上报续传 + 值班台 / 人员 / 设备(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 断网可跑、恢复可追平;值班台"看 + 干预"上线 |
| 范围 | **含** outbox(队列)+ 终态流水不互换、水位与 #14 心跳、#9/#8/#13 续报复用原 `idempotencyKey`、人员关联 Tab(M4)、设备网关指令入队(M5)、应用端 `D-1`~`D-3` 最小集;**不含** #10 `LEDGER_OFFLINE`(P2) |
| 依赖 | `IT-5`;Q-21(离线额度缺省全 0)/Q-22/Q-23/Q-25 |
| 对齐文档 | `公寓端/07` 表 7-5 第 1/2 项、表 7-7 步骤 1~8;`公寓端/09` §7 表 9-16 Mock 策略、表 9-17 联调 8 步;`平台端/05` 表 5-2;CR-02 表 3-21 / 表 3-30;`应用端/07` 表 7-6 / 7-7 / 7-8 |
| 对齐代码模块 | `com.ruoyi.apt.report.*`(outbox / watermark / 续报)、`com.ruoyi.apt.master.*` 人员关联、`com.ruoyi.apt.device.*` 指令队列、`RuoYi-Vue2-master/src/entries/duty/**`(`duty` 入口本期开出) |
| 交付物 `DV-6` | outbox + 水位 + 续报链路、M4/M5 代码与页面、`duty` 入口脚手架、断网演练记录、20 接口各 1 条"请求样例 → 落库断言"回归基线 |
| 验收标准 | 断网 2h 主链路不中断;恢复 ≤10min outbox 清空、`acked_watermark = current_watermark`、`count` 零差异、`max(watermark)` 追平(CR-01 表 1-23 含**离线指定要素 / 离线调整时刻**用例);`PL-IDEMPOTENT-REPLAY` 视为成功且不产生新业务事实;M5 指令只入队 `PENDING`、无心跳不伪造 `ACKED`;M4 关联 Tab 正确且快照只读 40904;值班台命令幂等 `duplicated=true`、越界不可写、时钟偏差 ≤5s |
表 13-17 `IT-7` 语音运维 + 人脸设备侧补齐(3 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 可叫乘探针、降级路径、人脸设备侧能力补齐(与 `IT-6` 并行) |
| 范围 | 分机台账 / 会话 / 录音索引、`apt-ops` 12 项自检、`call-ready`、Asterisk 不可用走"串口广播 + 值班台人工";§12 剩余部分:`FACE_ISSUE` 设备下发、离寓擦除、媒体签名、`LONG_TERM` 保留 |
| 依赖 | `IT-5`;`D11`/`D12`/`D15`/`D16` 定稿、Asterisk 可达;Q-30~Q-35 |
| 对齐文档 | `公寓端/07` 表 7-3 M6 项 / 表 7-5 第 9/10 项;`公寓端/10` §5~§12、CR-02 §12 验收 15~17;`公寓端/06` §5/§8 表 6-8;`docs/10` 表 10-2 D16 |
| 对齐代码模块 | `com.ruoyi.apt.voice.*`(新增 `asterisk-java` 依赖)、`com.ruoyi.apt.ops.*`、`RuoYi-Vue2-master/src/entries/apt/views/{voice,ops}`、`ruoyi-common` 文件上传能力(录音索引只存引用) |
| 交付物 `DV-7` | 语音 / 自检 / 人脸下发擦除代码与页面、12 项自检落库、降级演练记录、依赖变更清单 |
| 验收标准 | 改号被拒 40906;录音签名地址有效、`PURGED` 返回 41001;自检 12 项落库且 AMI 不可达时 `call-ready=false` 且原因准确;Asterisk 不可用走串口广播 + 人工、不静默失败;人脸"上传平台 / 下发设备"双路解耦并行互不阻塞;擦除超时 `50303` 升级全量同步;业务抓拍 / 录音不出公寓 |
表 13-18 `IT-8` 单公寓试点与灰度(2 周)
| 项 | 内容 |
| --- | --- |
| 目标 | 阶段三退出,形成可推广基线 |
| 范围 | 断网 2h 全链路演练、恢复 10min 追平、准点率对比、值班操作步骤 ≤10%、灰度与回滚演练、#9 影子模式转正式、关闭 Mock 切真实平台 |
| 依赖 | `IT-0`~`IT-7` 全通过;`PR-09` 本期**只开 1 个公寓**;`P-08` 白名单就绪 |
| 对齐文档 | `公寓端/07` 表 7-5(10 项)、表 7-6(8 类 + 3 专项)、表 7-7(9 步);`docs/10` 表 9-4;`平台端/08` 表 8-5 灰度回滚;`公寓端/09` 表 9-17 第 4/8 步;`平台端/09` PR-02/PR-09/PR-10 |
| 对齐代码模块 | 全部 `com.ruoyi.apt.*`;CI 回归流水线;`application-{local,prod}.yml` 配置外置 |
| 交付物 `DV-8` | 阶段三验收报告、演练取证(`gy_jcxx`/`apt_call_session`/`gy_jcxx_his`)、灰度 / 回滚记录、回归基线全绿报告、遗留问题清单 |
| 验收标准 | 表 7-5 全 10 项通过;#9 影子模式"只写不生效"→ 对账零差异连续 3 天后转正式;关闭 Mock 后 24h 无 P0/P1;`gyid` 跨公寓用例 100% 拦截;状态单写入口架构测试通过;仓库无明文凭据;回滚演练通过(**回滚是回退业务形态,不是回滚数据**);旧页 `JcXx.vue` 保留至阶段五、不得先删 |
---
## 4. 里程碑与节奏建议
表 13-19 里程碑与 Go/No-Go
| 里程碑 | 时点 | 可验证结果 | Go/No-Go 判据 | 关联迭代 |
| --- | --- | --- | --- | --- |
| `MS-0` 工程底座就绪 | 第 3 周末 | `apt-admin` 可启动、59 张表脚本幂等、CI 全绿、无明文口令 | **契约冻结签字**;未签字不进 `IT-1` | `IT-0` |
| `MS-1` 基座与主数据可管 | 第 8 周末 | 交路 / 房间 CRUD、Mock 下行、`gyid` 隔离 100% | 表 7-3 M0~M2 项全过 | `IT-1`~`IT-2` |
| `MS-2` **入住 + 叫乘核心闭环可演示** | 第 16 周末 | 单公寓、Mock 平台下跑通"刷脸核验 → 入住 → 审核 → 提报匹配 → 叫乘 → 状态机" | **§13 验收 18~20 + 表 7-3 M3 项全过**;这是本期最重要的可演示节点 | `IT-3`~`IT-5` |
| `MS-3` 断网闭环与值班台 | 第 19 周末 | 断网 2h 主链路、恢复 10min 追平、值班台看 + 干预 | 表 7-5 第 1/2 项、CR-01 表 1-23 全过 | `IT-6` |
| `MS-4` 语音与运维补齐 | 第 22 周末 | `call-ready` 探针、降级路径、人脸设备侧 | 表 7-3 M6 项、§12 验收 15~17 全过 | `IT-7` |
| `MS-5` **阶段三试点退出** | 第 24 周末 | 全部 10 项验收、灰度与回滚演练、回归基线全绿 | `docs/10` 表 9-4 退出条件;**此后才评估 P2 进入条件** | `IT-8` |
| `MS-6` 账务域(延后) | 待排 | 消费 / 充值 / 菜品闭环 | 见 §8 表 13-28 进入条件 | `IT-9`~`IT-11` |
表 13-20 节奏建议
| 项 | 建议 | 理由 |
| --- | --- | --- |
| 单迭代节奏 | **按迭代分档**:`IT-1` / `IT-3` / `IT-8` 为 **2 周**(开发与回归同窗),`IT-0` / `IT-2` / `IT-4`~`IT-7` 为 **3 周**(2 周开发 + 1 周联调 / 回归 / 决策会,含评审窗口);与表 13-7「建议周期」逐行一致 | 小步快跑且每步留回归窗口;与 `07` 表 7-7「逐模块联调」一致 |
| 总周期 | `IT-0`~`IT-8` **串行保守基线约 24 周**(6 个月),表 13-19 的里程碑时点按此排;核心闭环 `MS-2` 在**第 16 周** | 让"入住 + 叫乘"在半年周期内尽早可演示 |
| 并行度 | `IT-6` 与 `IT-7` 可在 `IT-5` 后并行(两组人);若并行,`MS-4` 可提前至第 19 周、`MS-5` 不早于第 21 周,**Go/No-Go 判据与灰度观察窗不变**;语音三项决策 `D11` / `D12` / `D15` / `D16` 并行定稿、不等开发 | `公寓端/08` 表 8-11 结论「语音三项并行启动」;表 13-19 的 24 周为**不并行时的保守口径** |
| 决策会 | 每迭代末 30 分钟口径会;`IT-0` 末尾契约冻结会(半天);`MS-2` 后一次 P2 进入条件评估 | `应用端/08` 表 8-7「D-2 阻塞最密集,须在 D-1 验收前开 30 分钟决策会」 |
| 缓冲 | 每 3 个迭代留 1 周缓冲,用于 DDL 冻结、Asterisk 环境、平台 Mock 对齐 | `IR-01`/`IR-02`/`IR-06` 均为外部依赖 |
| 反模式 | 不跳批、不先写 Controller、不在 `IT-0`~`IT-8` 开 `/apt/ledger/**`、不为赶进度关闭 Mock 校验 | `公寓端/09` 抬头 R-02、表 9-16 「生产强制 `false` 启动校验」 |
---
## 5. 对齐矩阵:设计文档 × 代码模块 × 迭代
表 13-21 设计模块 → `dev` 承载位置 → 落地迭代(**不新建 12 个 Maven 模块**)
| 设计模块 / 子域 | `dev` 承载路径(业务包分层) | 复用若依能力 | 主要表 | 落地迭代 |
| --- | --- | --- | --- | --- |
| `apt-framework` / `apt-common` | `ruoyi-framework`(`com.ruoyi.framework.*`)、`ruoyi-common`(`com.ruoyi.common.*`) | `SecurityConfig`、`@PreAuthorize`、`AjaxResult`、`LogAspect`、分页 | `sys_*` | `IT-0`、`IT-1` |
| `apt-admin` | `ruoyi-admin`(`RuoYiApplication`,唯一 jar) | 启动 / Swagger / profile | — | `IT-0` |
| `apt-system` | `ruoyi-system`(`com.ruoyi.system.*`) | `sys_menu` / `sys_user` / RBAC | `sys_*` | `IT-0`、`IT-1` |
| `apt-master-client` | `ruoyi-system` 新增 `com.ruoyi.apt.master.*` | `ruoyi-generator` 生成 CRUD | `apt_master_route` / `rule` / `room` / `person` / `version`、`gy_syn_jcxx` | `IT-2`、`IT-6` |
| `apt-stay`(含 `apt.checkin.*`) | `ruoyi-system` 新增 `com.ruoyi.apt.stay.*` | 事务、`@Log` | `apt_local_task`(补列)、`gy_jcxx`、`gy_rooms`、`gygl_ly_event` | `IT-2`、`IT-4` |
| `apt.face.registry` / `apt-face` | `ruoyi-system` 新增 `com.ruoyi.apt.face.*` | 文件上传(仅引用)、`Anonymous` 注解 | `apt_master_person`、`apt_person_face_reg` | `IT-3`、`IT-7` |
| `apt-dispatch`(`apt.duty.state`) | `ruoyi-system` 新增 `com.ruoyi.apt.dispatch.*` + `ruoyi-quartz` | Quartz 定时(须处理 `ScheduleConfig` 被注释) | `gy_jcxx`、`gy_jcxx_his`、`duty_manual_action` | `IT-4`、`IT-5` |
| `apt-device` | `ruoyi-system` 新增 `com.ruoyi.apt.device.*` | 无(净新增) | `gy_sb`、`apt_device_command` | `IT-5`、`IT-6` |
| `apt-voice` | `ruoyi-system` 新增 `com.ruoyi.apt.voice.*`(新增 `asterisk-java` 依赖) | 文件上传(录音索引) | `apt_sip_endpoint`、`apt_call_session`、`apt_recording` | `IT-5`、`IT-7` |
| `apt-report`(outbox) | `ruoyi-system` 新增 `com.ruoyi.apt.report.*` | 无(净新增) | `apt_report_outbox` / `history` / `watermark` | `IT-4`、`IT-6` |
| `apt-ops` | `ruoyi-admin` 现有 `monitor/{Cache,Server}Controller` 扩展 | oshi 监控 | `apt_ops_selfcheck` | `IT-7` |
| `apt.ledger.local`(**延后**) | `ruoyi-system` 预留 `com.ruoyi.apt.ledger.*`(**本期不建包**) | 无 | `gy_ykt_*`、`apt_ledger_offline` | `IT-9`~`IT-11` |
| 前端 `apt` 入口 | `RuoYi-Vue2-master/src/entries/apt/**`(`IT-0` 建) | `v-hasPermi`、`src/utils/request.js`、动态路由权限点 | — | `IT-0`~`IT-8` |
| 前端 `duty` 入口 | `RuoYi-Vue2-master/src/entries/duty/**` | 同上 | — | `IT-6` |
表 13-22 各迭代必须逐条核对的权威文档(**验收引用以此为准**)
| 迭代 | 主键文档 | 验收表 / 条款 |
| --- | --- | --- |
| `IT-0` | `docs/10` §9/§11.3、`公寓端/02`、`08`、`09`、CR-01 §5/§6、CR-02 表 3-33/3-36 | 表 9-1/9-3、表 8-11、表 9-18、F-01~F-14 |
| `IT-1`~`IT-2` | `公寓端/04` §1/§2、`03` §5、`07` 表 7-2~7-4、`09` 表 9-16 | 表 7-3 M0~M2、表 7-6 8 类用例 |
| `IT-3` | `公寓端/10` §4、`11` §4、CR-02 §12 | 表 10-26/10-27、验收 15、Q-30/Q-35 |
| `IT-4` | `公寓端/11` 全文、`05` §10、`09` 表 C-7、CR-02 §13 | **表 3-57 验收 18~20**、表 11-35 |
| `IT-5` | `公寓端/05` §9/§11、`07` 表 7-3/7-5、`应用端/07` §1/§2 | 表 7-3 M3、表 7-5 第 7 项 |
| `IT-6` | `公寓端/07` 表 7-5/7-7、`09` 表 9-16/9-17、`平台端/05` 表 5-2、`应用端/07` 表 7-6~7-8 | 表 7-5 第 1/2 项、CR-01 表 1-23 |
| `IT-7` | `公寓端/07` 表 7-3/7-5、`10` §5~§12、`06` §5/§8 | 表 7-3 M6、表 7-5 第 9/10 项、§12 验收 15~17 |
| `IT-8` | `docs/10` 表 9-4、`公寓端/07` 表 7-5~7-8、`平台端/08` 表 8-5、`平台端/09` PR-02/09/10 | 表 7-5 全 10 项 |
| `IT-9`~`IT-11` | `公寓端/12` 全文、`09` 表 C-8/C-9、`平台端/10`、CR-02 §14/§15 | 表 12-34、表 3-62 验收 21~23、表 3-69 验收 25 |
---
## 6. 风险台账与应对
表 13-23 风险台账(`IR-y` 为本文件内编号,不占用既有 `R-*` / `PR-*` / `Q-*`)
| 编号 | 风险 | 等级 | 触发时点 | 应对方式 | 兜底 / 不做什么 |
| --- | --- | --- | --- | --- | --- |
| `IR-01` | 技术基线冲突(JDK17/Boot4.1 vs 设计 JDK8/Boot2.5) | **P0** | `IT-0` | 推荐方案 B 修订设计基线锁定值并走变更评审;方案 A(降级 dev)备选;Swagger 注解改 springdoc | 不擅自改 `docs/公寓端/02`;未签字不写业务代码 |
| `IR-02` | 表数口径冲突 / 无业务表 | **P0** | `IT-0`~`IT-1` | 分域增量 DDL,DDL 与回填分离、回填可重放;二次执行幂等验收;改库名与凭据 | 不改 59 张设计口径;不在业务代码里直接 `CREATE TABLE` |
| `IR-03` | 前端双入口 / 动态路由与固定路由冲突 | P1 | `IT-0`、`IT-6` | `apt` 单入口先行、`duty` 随 `D-1` 开出;`sys_menu` 仅作权限点载体 | 不魔改 `permission.js` 全局动态路由 |
| `IR-04` | 无 CI / 测试工程,8 类用例无承载 | P1 | `IT-0` | 补 starter-test + MockMvc + CI 工作流 + 架构测试;每迭代沉淀回归基线 | 不以"手工点点"替代自动化回归 |
| `IR-05` | 明文口令 / token.secret | P1 | `IT-0` | 全部外置 + CI 正则扫描卡口 | 阶段零"已完成"在 `dev` 重新验收,不沿用旧结论 |
| `IR-06` | 契约 / DDL 未冻结(F-01~F-14、D5/D8'/D3、Q-40) | **P0** | `IT-0`~`IT-4` | 冻结会签字;冻结前不写 Controller(R-02) | Q-40 用 `duty_operation_log` JSON 兜底;平台 Q-48 用 `PL_JOB_*` JSON 兜底 |
| `IR-07` | `dev` Quartz `ScheduleConfig` 被注释 = RAMJobStore | P1 | `IT-5` | 叫乘定时前显式决策:启用 JDBC JobStore 或登记单实例 RAM 的风险 | 多实例部署前必须解决;本期单公寓单实例 |
| `IR-08` | Asterisk / AMI 依赖缺失且 `D11`/`D12`/`D15`/`D16` 未决 | P1 | `IT-5`~`IT-7` | 三项决策并行定稿;`IT-5` 用 Mock 结果接入、`IT-7` 才接真实 AMI | 不静默失败;Asterisk 不可用走串口广播 + 人工 |
| `IR-09` | 人脸媒体 / 设备擦除复杂度阻塞入住 | P1 | `IT-3` | §12 切分为「最小闭环(`IT-3`)+ 设备侧补齐(`IT-7`)」 | `IT-3` 不接对象存储、不做批量下发;抓拍帧不出终端 |
| `IR-10` | 平台 / 运安不可用导致 M0~M4 被阻塞 | P1 | `IT-1`~`IT-6` | `公寓端/09` 表 9-16 Mock 策略:`APT_REPORT_DISPATCH_ENABLED=false` + `apt.master-client.mock=true` 仅 local profile | Mock 数据标 `source=LOCAL`、outbox 只累积不派发、**不允许假成功置 `SENT`**;生产强制 `false` 启动校验 |
| `IR-11` | 断网 / 时钟偏差 / 乱序导致状态错乱 | P1 | `IT-6`、`IT-8` | 三时间戳 + `serverReceivedAt` 归日 + 幂等键 + 条件更新;表 7-6 三类专项 | `occurredAt` 不判日切;任务类不套 `LEDGER` 的「差值 ≤10 分钟」门槛 |
| `IR-12` | 旧页 / 旧接口双跑期间数据分叉 | P2 | `IT-8` 后 | `平台端/09` PR-02:「一侧只读」或按 `gyId` 白名单互斥;PR-09 本期只开 1 个公寓 | 旧页 `JcXx.vue`、旧 `/jcgl/**` 保留至**阶段五**才下线,不得先删 |
| `IR-13` | 消费充值延后导致 `#10` / 账务统计无数据 | P2 | `IT-8` 后 | `平台端/10` 账务三类指标标 `data_status=NO_DATA` / `BLOCKED`,报表注明"数据未接入" | **不推断、不补零、不以其他表代填**(CR-C39、表 10-9) |
| `IR-14` | 范围蔓延:把 P2 账务提前塞进核心迭代 | P1 | 全周期 | 表 13-28 进入条件 + `IT-8` 前的范围卡口评审 | 本期不开 `/apt/ledger/**`、不建 `com.ruoyi.apt.ledger` 包、不建支付订单 |
表 13-24 P0 决策清单与阻塞关系(沿用既有编号,不新增 `D`/`Q`/`P`)
| 决策 | 内容 | 阻塞 | 拍板时点 |
| --- | --- | --- | --- |
| `D5` | 一进程一 `gyid` | `IT-1`(M0) | `IT-0` 冻结会 |
| `D8'` / `D17` | `jcsj` 恒由公寓复算、主键与匹配键分离 | `IT-2`(M1)、`IT-4` | `IT-0` 冻结会 |
| `D3` | 房间主数据 | `IT-2`(M2) | `IT-0` 冻结会 |
| `D7` | 交路应急新增 | `IT-2` | `IT-0` 冻结会 |
| `F-01`~`F-14` | 20 接口冻结清单(F-14 为 CR-01 建议追加) | 全部 Controller | `IT-0` 冻结会 |
| `P-02`/`P-03`/`P-06`/`P-08` | 平台侧方向 / 过渡 / 白名单 | `IT-6`、`IT-8` | 阶段二之前(`平台端/09` 表 9-4) |
| `Q-15`/`Q-22`/`Q-25` | 阈值、权限点、准点率三档;平板复叫 | `IT-5`、`IT-6` | 阶段三试点前 |
| `D11`/`D12`/`D15`/`D16` | 语音与 Asterisk HA | `IT-7` | 阶段二冻结前 / M6 前 |
| `D1`/`D2` | 餐饮归属、余额主账位置 | **不阻塞本期**,但阻塞 `IT-9` | P2 进入条件评估时 |
---
## 7. 跨迭代联调与测试安排
表 13-25 联调顺序(承接 `公寓端/07` 表 7-7 九步 / `公寓端/09` 表 9-17 八步,**#10 保持 P2 位置不变**)
| 步 | 联调内容 | 前置 | 通过标志 | 所在迭代 |
| --- | --- | --- | --- | --- |
| 1 | 20 接口路径 / 字段对齐 → 差异清单签字(冻结 v1) | `IT-0` | F-01~F-14 签字 | `IT-0` |
| 2 | 启动 `apt-admin`(`local`)→ 登录链路 `captchaImage→login→getInfo` | `IT-1` | 启动无异常、拿到 token 与 permissions | `IT-1` |
| 3 | Mock 灌 3 交路 / 5 房间 / 5 人员 / 2 天计划 | `IT-2` | `apt_master_version` 有 `APPLIED`、标 `LOCAL` | `IT-2` |
| 4 | 交路 → 房间 → 人脸注册 → 入住申请 / 审核 / 办理 | `IT-3`、`IT-4` | 表 7-3 M1/M2、§13 验收 18~20 全过 | `IT-3`~`IT-4` |
| 5 | #14 心跳 → `outboxWatermark` / `clockSkewMs` | `IT-6` | 水位与时钟偏差可读 | `IT-6` |
| 6 | #9 `duty-results` **影子模式「只写不生效」→ 对账零差异连续 3 天** | `IT-6` / `IT-8` | 零差异 | `IT-6` 起、`IT-8` 收口 |
| 7 | **打开 #10 `LEDGER_OFFLINE`(P2)** | P2 进入条件 | 账务日对账零差异 | **延后 `IT-9`,本期跳过** |
| 8 | #11/#12/#13 摘要(含 §15 的 `satisfaction{}` / `roomOccupancy{}` 分组计数) | `IT-6` | 摘要不阻塞主链路;缺报标 `NO_DATA` | `IT-6`~`IT-8` |
| 9 | #23/#24 语音域;关闭 Mock 切真实平台 | `IT-7`、`IT-8` | 24h 无 P0/P1 | `IT-7`~`IT-8` |
表 13-26 测试分层与跨迭代回归基线
| 层 | 内容 | 承载 | 每迭代动作 | 来源 |
| --- | --- | --- | --- | --- |
| L1 单元 | Service / 状态机 / 复算 / 双额度(延后) | JUnit5 + Mockito(新增 starter-test) | 新增用例,覆盖率门槛按域设定 | `IR-04` |
| L2 接口 | Controller / 错误码 / 鉴权 / 分页 / `gyid` | MockMvc | **表 7-6 的 8 类用例逐域执行**:正常查询、分页边界、必填、重复冲突、无权限、`gyid` 隔离、状态非法、只读快照 | `公寓端/07` 表 7-6 |
| L3 专项 | 幂等(同 `commandId` 连发 3 次只 1 条)、时间戳(事件毫秒 / 列表秒)、空数据(200 + `total=0` 不返回 `null`) | 集成测试 | 每迭代固定执行 | 表 7-6 补充三类专项 |
| L4 架构 | 状态字段 UPDATE 只出现在 `apt.duty.state`;所有业务 SQL 带 `gyid`;无明文口令 | CI 静态扫描 + ArchUnit | **每迭代必跑,失败即阻断合入** | 表 7-5 第 6/7/8 项 |
| L5 契约 | 20 接口各 1 条「请求样例 → 落库断言」 | 契约测试 | 冻结后作回归基线,跨迭代不删只增 | `公寓端/09` §7 表 9-16 / 表 9-18 |
| L6 端到端 | 断网 2h + 恢复 10min + 灰度回滚 | 现场演练 | `MS-3` / `MS-5` 各一次,取证归档 | 表 7-5、CR-01 表 1-23 |
| L7 应用端 | 命令幂等 `duplicated=true`、越界不可写、告警不误报、时钟 ≤5s | `应用端/07` 表 7-6/7-7 | `IT-6` 起纳入回归 | `应用端/07` |
表 13-27 跨迭代回归与 Mock 切换纪律
| 项 | 规则 |
| --- | --- |
| 回归范围 | **累积式**:`IT-n` 必须回归 `IT-0`~`IT-(n-1)` 的全部 L2/L3/L4/L5 基线,不允许"只测本迭代" |
| 基线冻结 | `IT-0` 产出 20 接口请求样例 → 落库断言;每迭代新增只追加、不修改既有断言 |
| Mock 开关 | `APT_REPORT_DISPATCH_ENABLED=false` + `apt.master-client.mock=true` **仅 local profile**;生产启动强制校验 `false`,否则拒绝启动 |
| Mock 数据标注 | `apt_master_version.source='LOCAL'` / `source_version=0`,页面标"非平台下发";outbox 只累积不派发、禁止假成功置 `SENT` |
| 影子模式 | #9 `duty-results` 先"只写不生效",**对账零差异连续 3 天**才转正式(`公寓端/09` 表 9-17 第 4 步) |
| 切换顺序 | 先切本地库与状态机(M3 / `IT-5`)→ 再切值班台(`D-2` / `IT-6`)→ 最后摘旧页 `JcXx.vue`(**阶段五**);旧页不得先删 |
| 灰度与回滚 | `平台端/08` 表 8-5:域级 `master→person→route→plan→ledger→report`(本期不到 `ledger`);公寓级 `gyId` 白名单先 1 个公寓跑满 1 个结算周期;**回滚是回退业务形态,不是回滚数据** |
| 联调环境 | 单公寓单实例(`PR-09`);`platform-worker` 单活;Asterisk 用 Mock 至 `IT-7` |
---
## 8. 后续消费 / 充值 / 菜品模块衔接规划
### 8.1 延后范围与进入条件
表 13-28 延后域与 **P2 进入条件**(全部满足才启动 `IT-9`)
| 项 | 内容 |
| --- | --- |
| 延后范围 | `CF-01` 人员消费(N-49~N-51、双额度 CAS、一条 `CONSUME` 流水)、`CF-02` 人员线下充值(N-52~N-55、管理员二次确认、只增 `xjye`)、`CF-03` 公寓菜品管理(N-48 / N-57 / #4 `recipes[]`);对应 `公寓端/12` §4~§6、四条状态链、`40933`~`40940`、`S-01`~`S-07` 报表消费端 |
| 进入条件 1 | `MS-5` 阶段三验收 10 项全过,单公寓试点稳定 **≥1 个结算周期**(`平台端/08` §8) |
| 进入条件 2 | `D1`(餐饮归属)/ `D2`(余额主账位置)归属**先签字**(不阻塞本期,但阻塞账务实现);`P-07` 餐饮域定稿 |
| 进入条件 3 | `Q-41`~`Q-45` 拍板(补贴不足现金补足、离线充值受 `offlineQuota` 约束、`knye`/`xjye` 语义与 `YKT_MEALS` 补列复核、远程点菜启用条件、单笔上限 / 双人复核 / 内部人员透支) |
| 进入条件 4 | `Q-43` / `Q-48` DDL 契约冻结:`gy_ykt_*` 五张预留表、`apt_ledger_offline`、`YKT_MEALS`、`PL_REPORT_DAILY` / `PL_LEDGER_FLOW` 补列、2 张 `pl_apt_stat_*` |
| 进入条件 5 | 平台 #10 `LEDGER_OFFLINE` 影子模式"只写不生效"→ **对账零差异连续 3 天**;`platform-ledger` 余额主账就绪 |
| 进入条件 6 | `#1 offlineQuota` 下发链路与 `features.remoteOrdering=false` 开关可用;日对账 #16 与 `POST /adjustments` 冲正通道验收通过 |
### 8.2 本期「预埋但不上线」清单
表 13-29 为避免 `IT-9` 返工,本期必须预埋 / 必须避免
| 类别 | 本期预埋(不做实现) | 本期**不做**(防止范围蔓延) |
| --- | --- | --- |
| 数据 | 59 张表 DDL 中保留 `gy_ykt_*` / `apt_ledger_offline` 的**表结构与补列定义**(建表不开放读写);报表统计账务三类指标按 `NO_DATA` / `BLOCKED` 输出 | 不写账务业务数据、不以其他表代填、不推断不补零 |
| 接口 | #1 响应保留 `offlineQuota`(缺省全 `0`)、`features.remoteOrdering=false`;#13 摘要 `payload` 预留 `satisfaction{}` / `roomOccupancy{}` 分组计数 | 不开 `/apt/ledger/**`、不开 `/open/v1/ledger/**`(N-49~N-55)、不建支付订单 / 回调 |
| 代码 | 包结构预留 `com.ruoyi.apt.ledger`(**不建包**);错误码段 `40933`~`40940` 登记在 `公寓端/04` 表 C-3 但不启用 | 不建 `apt.ledger.local` 服务、不引支付 SDK、不写 `YKT_MEALS_ORDERING` / `ORDERS_MX` |
| 权限 | `apt:ledger:*` / `duty:ledger:*` / `platform:stat:*` 权限点在 `sys_menu` **登记但置灰**(`公寓端/07` 表 7-4 第 6 行 P2) | 不授予任何角色、不出现在值班台菜单 |
| 报表 | `平台端/10` 五类指标中账务三类按 `data_status=NO_DATA` / `BLOCKED` 输出,报表注明"数据未接入";`CASH_RECHARGE_INCOME` 口径(计收入、不与消费抵销)先冻结 | 不参与排名、不对 `NO_DATA` 公寓补零、不以 `pl_staff_performance_daily` 代填 |
| 边界 | 统计结果不回写任何业务状态;三个单写入口不变;平台库 2 张 `pl_apt_stat_*` 在本期(`IT-0`~`IT-8`)**不建**(Q-48 冻结后再建,见 `平台端/03`、`平台端/10`) | 不新增 `bizType`、不新增跨端编号、不新增对象存储桶 |
### 8.3 衔接迭代建议
表 13-30 `IT-9`~`IT-11`(**待排,仅给顺序与验收,不占本期周期**)
| 迭代 | 目标 | 范围 | 依赖 | 验收要点 |
| --- | --- | --- | --- | --- |
| `IT-9` 账务底座 | `apt.ledger.local` 与 `platform-ledger` 对齐 | `gy_ykt_*` / `apt_ledger_offline` 补列落库、`#1 offlineQuota`、#10 影子转正式、#16 日对账、`POST /adjustments` 冲正 | 表 13-28 全部进入条件 | 日对账 `count`/`sum(amount)`/`max(watermark)` 三维零差异;`PF-5003` 阻断调账;`PL-VERSION-STALE` 置 `DEAD` |
| `IT-10` 消费 + 线下充值 | `CF-01` / `CF-02` 闭环 | N-49~N-55、补贴优先 / 现金补足双额度 CAS、**一笔消费只一条 `CONSUME` 流水**、管理员二次确认、只增 `xjye`、值班台核对 / 作废 | `IT-9`;Q-41/Q-42/Q-45 | **§14 表 3-62 验收 21~23**:三项流程闭环、四条状态链互不替代、十类动作留痕、设备与前端零余额表写权限;`CASH_RECHARGE_INCOME` 计收入不与消费抵销(CR-C37) |
| `IT-11` 菜品 + 统计收敛 | `CF-03` 与平台报表打通 | N-48 / N-57、#4 `recipes[]`、`YKT_MEALS` 补列、`publish_state`、N-58 远程点菜**仅预留**(`40940`);`平台端/10` 五类指标与月/季/年排名正式启用 | `IT-10`;P-07 / Q-44 / Q-46~Q-50 | §14 验收 21~23 + **§15 表 3-69 验收 25/26**;食谱缓存只在终端本地、确认前不落库;缺报 `NO_DATA` 不补零;不新增桶 / `bizType` / 跨端编号 |
表 13-31 衔接期的三个不变式(防止 P2 上线破坏核心闭环)
| # | 不变式 |
| --- | --- |
| 1 | 账务域上线**不得改变** `apt.duty.state` 单写入口、`gy_jcxx` 状态字段数、20 个跨端编号与 `v2-draft` 契约 |
| 2 | 统计与排名结果**不回写** `gy_jcxx` / `apt_local_task` / `GY_YKT_REMAINDER` / `YKT_MEALS` 等任何业务表;`apt.duty.state` / `platform-ledger` / `platform-report` 三个单写入口不变 |
| 3 | 核心闭环的 L2~L5 回归基线在 `IT-9`~`IT-11` 期间**持续全绿**;任一账务变更导致入住 / 叫乘回归失败,先回滚账务域开关(回退业务形态,不回滚数据) |
---
## 9. 交付物与提交纪律
表 13-32 交付物清单(承接 `公寓端/07` 表 7-8,按本计划迭代重排)
| 迭代 | 代码 | 文档 / 配置 | 验证记录 |
| --- | --- | --- | --- |
| `IT-0` | 脚手架、CI/测试骨架、凭据外置、`apt` 入口 | 基线偏离评审纪要、分域增量 DDL + 回填脚本、冻结清单签字版、CI 配置 | 编译 / 单测 / 明文扫描 / DDL 二次执行幂等报告 |
| `IT-1`~`IT-2` | 拦截器、错误码、审计、交路 / 房间 CRUD 与页面 | 接口约定落地说明、字段说明与待确认项、权限点种子 | 表 7-3 M0~M2 + 表 7-6 8 类用例记录、`curl` / 页面截图 |
| `IT-3`~`IT-5` | 人脸最小闭环、入住五环节、状态机与平板 / SIP 结果接入 | `applyId`↔`jcid` 映射说明、状态机与命令说明、Q-36~Q-40 默认口径配置 | §13 验收 18~20、表 7-3 M3、架构测试报告 |
| `IT-6`~`IT-7` | outbox / 水位 / 续报、人员 / 设备、`duty` 入口、语音 / 自检 / 人脸设备侧 | Mock 策略说明、20 接口请求样例 → 落库断言基线、依赖变更清单 | 断网演练取证、表 7-7 联调 9 步记录、表 7-6/7-7 应用端用例 |
| `IT-8` | 全量回归修复 | 阶段三验收报告、灰度 / 回滚记录、遗留问题清单、P2 进入条件评估纪要 | 表 7-5 全 10 项、L2~L5 全绿报告 |
| `IT-9`~`IT-11` | 账务 / 充值 / 菜品 / 统计(延后) | `公寓端/12` 与 `平台端/10` 的落地说明 | §14 表 3-62、§15 表 3-69 验收记录 |
**提交纪律**:本轮只产出设计与排期文档,不写业务代码、不建表改表、不执行 DDL / DML、不执行 `git commit` / `git push`;`dev/` 两套上游保持未改动,所有新增落在后续迭代的独立分支并由评审合并。本文件**不改变** `docs/公寓端/07` 的 M0~M6 批次顺序、`docs/10` 的阶段零~五划分与 D1~D16 待决状态;表 13-6 的三项口径偏离须经评审确认后方可执行。
+139
View File
@@ -0,0 +1,139 @@
# 公寓端设计文档索引
本目录是"公寓端"的功能边界梳理与开发设计文档集合,上位依据为 `docs/10-架构优化方案-三端拆分.md`(**v2.0**)及
`docs/01`~`docs/10`、`docs/frontend/*`、已交付的 `apartment/README.md` 与 `apartment/db/mysql/README.md`。
**CR-02 修订**:`docs/公寓端/00` 重新规划了公寓端功能,其覆盖范围、交互协议与数据、模块与数据表设计、关键流程变更以
`docs/设计变更/03-三端功能设计修订(CR-02).md` 为权威增量;本目录各篇 §0 已同步改判,冲突处以 CR-02 表 3-4 为准。
**§12 补遗**:人脸数据下发管理以 `docs/公寓端/10` 为唯一权威表述;注册链路按 CR-C25 为「签到终端申请 → 值班员注册 → 入公寓服务系统 → 分别上传平台 / 下发公寓内设备」,人员主数据人脸按 CR-C24 允许跨端、业务抓拍与录音仍不出公寓;不新增业务表与跨端编号。
**§13 补遗**:人员入住作业流程以 `docs/公寓端/11` 为唯一权威表述(注册与签到 → 入住申请 → 值班员审核 → 提报与平台匹配 → 进入叫乘);"已入住"为流程态 + 业务事实双重表达,申请单落 `apt_local_task` 补列,不新增 `gy_jcxx` 状态字段、不新增业务表、不新增跨端编号。
**§14 补遗**:消费、充值与菜品管理以 `docs/公寓端/12` 为唯一权威表述(人员消费 / 人员线下充值 / 公寓菜品管理);仅线下充值、扫码只识别公寓、不接线上支付与回调,食谱主数据归平台库 `YKT_MEALS` 补列并由公寓只读下发,远程点菜仅预留入口(N-58 恒 `40940`);不新增业务表、不新增跨端编号、不新增 `bizType`。
**§15 补遗**:三端一致性同步以 **CR-02 §15 表 3-66(状态映射与数据同步机制)为唯一权威**,本端四项能力(平板叫乘 / 人脸签到 / 消费 / 菜品管理)的权威表述不变,应用端与平台端同步补齐业务规则、交互流程、权限控制与异常处理;#13 `ARCHIVE` 摘要增 `satisfaction{}` / `roomOccupancy{}` 分组计数供平台统计,**不新增 `bizType` 与上报接口**。平台新增 `docs/平台端/10`「公寓统计分析与排名报表」(五类指标、月 / 季 / 年报表、公寓分类 + 综合排名),统计结果不回写本端任何业务状态。上位依据升至 **v2.0**,变更点与影响范围见 CR-02 §15 表 3-63。
**开发计划(2026-11-10,非设计变更)**:`docs/公寓端/13`「公寓端小步迭代开发计划」基于 `dev/` 的 RuoYi-Vue / Vue2 / MySQL 上游实测代码基础,把本目录设计成果切成 `IT-0`~`IT-8` 九个小步迭代,**P1 入住 + 叫乘核心闭环优先**、**P2 消费 / 充值 / 菜品整体延后**(`IT-9`~`IT-11`);登记 6 项 P0 排期前提、里程碑 `MS-0`~`MS-5`、交付物、设计文档 × 代码模块对齐矩阵、风险台账与跨迭代联调测试。`docs/10` §9 与本目录 `07` 仍是阶段划分、批次顺序与验收口径的权威,`13` 只做排序、切分、对齐与衔接;CR-02 §12~§15 权威表述、59 张表、20 个跨端编号与 `v2-draft` 均不变。变更点与影响范围见 CR-02 表 0-2 的 **v1.7** 登记行。
**本轮只产出设计文档**:不写业务代码、不建表改表、不执行 DDL/DML;本轮对 `docs/` 下既有设计文档的修改仅限口径改判与增量章节。
## 0. 文档清单
| 文件 | 主题 | 对应请求项 |
| --- | --- | --- |
| `README.md` | 索引、编号约定、全局对应关系、一页结论、与 `docs/平台端/` 的编号对应 | 输出形式 |
| `00-公寓端主要功能设计.md` | **需求基线**:14 项功能编号化 `A-01`~`A-14`、主责端 / 协同端 / 本期判定 | CR-02 输入 |
| `01-功能边界.md` | 定位与职责、三端五维分界、**16 个功能域清单**(§0 为 v2 增量)、本期不做、待决策对边界的影响 | 文档一 + CR-02 |
| `02-技术基线与工程结构.md` | 版本锁定、12 模块职责与依赖、前端双入口边界、分层与编码规范、路由与权限点 | 文档二 1~4 |
| `03-数据设计.md` | 表归属矩阵(**59 张口径**)、语音域字段与注册状态流水、命名分区、状态单写入口、`gyid` 隔离、逻辑删除与审计、缺口与建议 | 文档二 5 + CR-02 §6、§10 |
| `04-接口设计.md` | 通用约定与错误码、分域接口清单、设备侧 `/open/v1/tablet/**`、四个核心域映射;§0 表 C-1~C-8 为 CR-02 增量接口与错误码(N-15~N-58、40913~40940) | 文档二 6 + CR-02 §3、§10~§14 |
| `05-关键流程.md` | 叫乘执行(SIP / 平板双模式)、设备接入、语音与录音、主数据下行、上报 outbox 与续报、值班人工干预;**§9 两类叫乘实现方式的端到端流程**(触发条件 / 交互时序 / 状态上报 / 结果反馈)、**§10 入住作业流程索引**(F-K)、**§11 消费 / 充值 / 菜品管理索引**(F-L / F-M / F-N) | 文档二 7 + CR-02 §7、§11、§13、§14 |
| `06-部署与配置.md` | profile、环境变量、端口与防火墙、Asterisk 同机、Nginx 双入口、容量 | 文档二 8~9 |
| `07-实施计划与验收.md` | 阶段里程碑、可验证结果、验收标准、测试矩阵、全量映射表 | 文档二 10 + 交付说明 |
| `08-待决策与风险.md` | D11–D16、风险清单、与 `docs/10` 的差异、需确认决策点(CR-02 新增 Q-20~Q-50,其中 Q-30~Q-35 见 `10`、Q-36~Q-40 见 `11`、Q-41~Q-45 见 `12`、**Q-46~Q-50 见 `docs/平台端/10` 表 10-21**) | 文档二 11 + 约束 4 |
| `09-平台接口约定.md` | 跨端 **20 个编号 + v2-draft payload / `bizType` 扩展**、上报信封、幂等、Mock 与联调顺序;§0 表 C-1~C-8 为各补遗的跨端字段扩展(含 §14 的 #1 / #4 / #10 / #16) | 文档二 12 + CR-02 §3、§4、§10~§14 |
| `10-人脸数据下发管理.md` | **人脸数据下发管理**:注册链路(签到终端申请 → 值班员注册 → 入公寓服务系统 → 分别上传平台 / 下发设备)、平台下载入库、签到设备下发、消费 / 叫乘按设备范围隔离、离寓自动擦除、长期驻留保留;`AF-01`~`AF-06`、N-25~N-37、Q-30~Q-35、表 10-1~10-30 | CR-02 §12(CR-C24 / CR-C25、F-J) |
| `11-人员入住作业流程.md` | **人员入住作业流程**:注册与签到、入住申请(房间 / 交路)、值班员审核与房间调整、#9 提报与平台运安匹配、#7 任务查询进入叫乘;三条状态链、`apt_local_task` 补列、N-38~N-47 请求 / 响应字段、Q-36~Q-40、表 11-1~11-35 | CR-02 §13(CR-C26~CR-C30、F-K、验收 18~20) |
| `12-消费充值与菜品管理.md` | **消费、充值与菜品管理**:人员消费(选餐加菜显示金额 → 查双额度 → 补贴优先 / 现金补足 → 上报并同步平台账户)、人员线下充值(扫码识别 → 管理员核对 → 计入现金额度 → 同步平台)、公寓菜品管理(阶段性食谱发布 → 终端查看并加入选择菜单 → 远程点菜预留);四条状态链、`gy_ykt_*` / `YKT_MEALS` 补列、N-48~N-58 请求 / 响应字段、Q-41~Q-45、表 12-1~12-34 | CR-02 §14(CR-C31~CR-C35、F-L / F-M / F-N、验收 21~23) |
| `13-公寓端小步迭代开发计划.md` | **实施排期设计稿(非设计变更)**:`dev/` 实测上游、`IT-0`~`IT-8` 迭代目标 / 范围 / 依赖 / 验收、里程碑 `MS-0`~`MS-5`、交付物 `DV-0`~`DV-8`、设计文档 × 代码模块对齐矩阵、6 项 P0 排期前提与 `IR-01`~`IR-14` 风险台账、跨迭代联调与回归基线、`IT-9`~`IT-11` 消费 / 充值 / 菜品衔接;表 13-1~13-32、图 13-1 | 开发排期与联调(**阶段 / 验收口径仍以 `07` 为准**) |
| `../设计变更/03-三端功能设计修订(CR-02).md` | **权威增量**:三端覆盖范围、交互协议、交互数据、模块与数据表、关键流程变更、施工单 | CR-02 |
## 1. 编号与引用约定
1. **本文档编号独立**:本目录内图、表统一用 `图 x-y` / `表 x-y`(`x` 为文件序号,`y` 为该文件内序号),
例如 `表 3-2` 指 `03-数据设计.md` 的第 2 张表。
2. **引用 `docs/10` 必须带文档名前缀**,例如 `docs/10 表 5-3`、`docs/10 §7.9`,避免与本文档编号混淆。
3. **表名一律小写**(与 `apartment/db/mysql/*.sql` 脚本的实际写法一致);`docs/10` 原文用大写表名处,
本文档首次出现时以"`APT_SIP_ENDPOINT`(`apt_sip_endpoint`)"形式给出对照,后文只用小写。
部署机的 `lower_case_table_names` 尚未核实,已列入 `docs/10` 表 5-9 第 11 项待查。
4. **不出现真实口令、内网地址、Asterisk 凭据**:配置项只写变量名与占位默认值。
5. **引用 `docs/平台端/` 同样带文档名前缀**,例如 `docs/平台端/07` 表 7-7;平台端 `P-01~P-11` / `PR-01~PR-10` 与本目录 `F-01~F-13` / `G-01~G-09` / `差-1~差-8` 的对应关系见 §6 表 0-3。
## 2. 与 `docs/10` 的全局对应关系
表 0-1 本文档 → `docs/10` 追溯表
| 本文档章节 | 引用的 `docs/10` 小节 / 表号 | 引用用途 |
| --- | --- | --- |
| `01` §1 定位 | §3.2;表 3-1 | 公寓端"负责 / 不负责"逐条落到本端 |
| `01` §2 五维分界 | 表 2-3(复用维度);表 3-1(逻辑分工);§3.4 表 3-2(语音三端分工) | 数据归属、写入口、部署形态、路由、账号五个维度 |
| `01` §3 功能清单 | §4.3 表 4-3;§5.1 表 5-1;`apartment/db/mysql/README.md` §3 | 10 个功能域与表归属 |
| `01` §5 待决策影响 | §10.2 表 10-2(D11–D16) | 未定时的默认执行口径 |
| `02` §1 技术基线 | §2.4 表 2-2 | 版本锁定与"框架不魔改" |
| `02` §2 工程结构 | §2.5 图 2-3、表 2-3;§4.4 图 4-1、表 4-4 | 12 模块与单向依赖 |
| `02` §3 分层规范 | §5.3;§6.2 | Controller/Service/Mapper、`gyid` 注入、三时间戳 |
| `02` §4 路由与鉴权 | §2.5 表 2-3(后端路由行、菜单与权限点行) | 四前缀与权限点命名 |
| `03` §1 表归属 | §5.1 表 5-1;§5.3 第 1~6 条;§5.4 表 5-3 | 44 张业务表 owner 与写入口 |
| `03` §2 命名分区 | §5.3 第 2 条 | `APT_*` / `GY_*` / `DUTY_*` |
| `03` §3 状态写入口 | §5.3 第 6 条;§7.8 表 7-4 | 状态字段单写入口与分机资源状态机 |
| `03` §4 `gyid` 隔离 | §3.3"公寓隔离方式"4 条;§5.3 第 5 条 | 部署 / 库 / 请求 / 设备四级 |
| `03` §6 缺口 | §5.4 表 5-4;§5.6.1 表 5-6;§10.2 | 字段级缺口与补表建议 |
| `04` 全部 | §6.1 表 6-1;§6.4 表 6-3 | 接口清单与跨端接口编号 |
| `04` §2 错误码 | §6.2;§6.3 表 6-2 | 上报语义与可靠性机制的口径延伸 |
| `05` 全部 | §7.7~§7.11;`docs/10` §6.3 | 链路、事件、自检、上报 |
| `06` 全部 | §8.1~§8.3、表 8-1~表 8-5 | 部署形态、资源、端口、录音存储 |
| `07` 全部 | §9 表 9-1~表 9-6;§11.2 表 11-1 | 阶段划分与优先级 |
| `08` 全部 | §10.1 表 10-1;§10.2 表 10-2;§10.3 表 10-3 | 风险与决策点编号沿用 |
| `09` 全部 | §6.1 表 6-1;§6.2;§6.3 表 6-2;§6.4 表 6-3 第 23~25 行 | 跨端接口由 17 + 3 = 20 个 |
## 3. 一页结论
表 0-2 本轮设计的 8 条结论
| # | 结论 | 依据 |
| --- | --- | --- |
| 1 | 公寓端是"执行 + 现场接入"端,一句话边界:**现场资源与事实归公寓,主数据与跨公寓统计归平台,人工干预归应用端但必须经公寓领域服务** | `docs/10` §3.2、表 3-1、§3.4 一句话边界 |
| 2 | 公寓端与管理端不拆服务:同一 jar(`apt-admin`)、同一库、同一进程;前端同一工程双入口 | `docs/10` §2.5 表 2-3 负面清单 |
| 3 | 管理类功能(本目录重点)统一走 `/apt/**`;值班类功能走 `/duty/**`;两者共用同一领域服务,不写两套实现 | `docs/10` §2.5 表 2-3 后端路由行 |
| 4 | v1.2 **已建齐 44 张**业务表,可支撑交路 / 房间 / 叫乘 / 人员 / 设备五类管理的只读与受控写;CR-01 追加 1 张、CR-02 新增 13 张、v1.1 补遗新增 1 张(`apt_sip_reg_status`)后**设计口径统一为 59 张**(新增表均为设计定义,DDL 待阶段二契约冻结评审通过后执行);**CR-02 §12 人脸数据下发模块不新增业务表**,59 张口径不变,仅复用既有表 + 补列 + 规则;**§13 人员入住作业流程同样不新增业务表**(申请单落 `apt_local_task` 补列、不新增 `gy_jcxx` 状态字段);**§14 消费 / 充值 / 菜品管理同样不新增业务表**(复用 `gy_ykt_*` 五张预留表 + `apt_ledger_offline` + 平台库 `YKT_MEALS` 补列,食谱缓存只在终端本地、不落公寓业务表,不新增 `bizType` 与状态字段);**§15 三端一致性同步与平台统计分析同样不新增公寓业务表**(平台库新增 `pl_apt_stat_*` 2 张另计、累计 5 张,统计结果不回写本端业务状态);**有 9 处字段/取值缺口需要确认**(G-01~G-09,见 `03` §6 与 `08` §5、`09` §9),均可先用"不改库的兜底"落地 | `apartment/db/mysql/README.md` §3;本文档 `03` §6、`08` 表 8-9、`09` 表 9-20、`10` 表 10-5 / 10-6;CR-02 §6(表 3-25~3-33)、§10、§12 |
| 5 | 状态字段(`ryzt`/`finish_jc`/`finish_ly`/`stoped`)只有 `apt.duty.state` 一个写入口,管理与值班页面都只能提交命令 | `docs/10` §5.3 第 6 条、表 4-4 |
| 6 | 语音域 11 张表与 `docs/10` 表 5-3 一一对应;分机号不可原地改号,增删改停一律"先库后 Asterisk + 异步收敛" | `docs/10` 表 5-3、表 5-5、§7.8 |
| 7 | 跨端接口冻结为 20 个(表 6-1 的 17 个 + 表 6-3 的 3 个语音跨端),上报统一 outbox + 幂等键 + 三时间戳 | `docs/10` 表 6-1、表 6-3、§6.2、§6.3 |
| 8 | 与 `docs/10` 存在 **8 处差异**(排序规则、隔离级别、代码生成器、表名、语音表数、表数量口径、管理端写入口与前端菜单通道、分机状态机落库),全部列入 `08` §4 表 8-8(差-1~差-8)待确认,**不擅自改写原结论** | 本文档 `08` §4 |
## 4. 阅读顺序
```mermaid
flowchart LR
A["README(索引与结论)"] --> B["01 功能边界"]
B --> C["02 技术基线与工程结构"]
C --> D["03 数据设计"]
D --> E["04 接口设计"]
E --> F["05 关键流程"]
F --> G["06 部署与配置"]
G --> H["07 实施计划与验收"]
H --> I["08 待决策与风险"]
E --> J["09 平台接口约定"]
J --> K["10 人脸数据下发管理"]
K --> L["11 人员入住作业流程"]
L --> M["12 消费、充值与菜品管理"]
M --> N["13 小步迭代开发计划(排期 / 联调)"]
```
图 0-1 阅读路径
评审建议:先看 `01` §3 的功能清单与 `04` 的映射表确认"范围与落点",再看 `03` §6 的缺口与 `08` §4 的差异确认"改不改库",
最后看 `07` 的里程碑确认"先做什么"、`13` 的 `IT-0`~`IT-8` 确认"按什么顺序做、每步怎么验"。
## 5. 术语
沿用 `docs/10` 附录 A 表 A-1,本文档只补三个本项目特有说法:
| 术语 | 说明 |
| --- | --- |
| 管理端页面 | 本目录指公寓端的后台管理界面,属于前端 `apt` 入口、后端 `/apt/**`,面向公寓管理员与运维,不面向值班员 |
| 值班台 | 前端 `duty` 入口、后端 `/duty/**`,面向值班员的盯控与人工干预,本轮不展开 UI,但接口与流程在 `04`/`05` 中给出契约 |
| 受控写 | 写操作必须经领域服务/状态机校验,Controller 与前端都不得直接改状态字段(`docs/10` §5.3 第 6 条) |
## 6. 与 `docs/平台端/` 的编号对应
平台端 `docs/平台端/09-待决策与风险.md` 表 9-3 已把 `P` / `PR` 编号映射到本目录的 `F` / `G` / `差` / 表号。本节给出同一份对应关系的本端视角,保证两侧“能互相找到”;**不要求一一映射**,平台侧自决项本端不重复登记。
表 0-3 平台端 `P` / `PR` 编号 → 本目录落点
| 平台端编号 | 本目录对应 | 关系 |
| --- | --- | --- |
| P-02 | `09` 表 9-18 F-03 | **同一件事**(#16/#17 方向);平台端推荐 A(保持“公寓主动调用平台”),定稿前本端一律走 `09` 表 9-13 的本地入口 |
| P-05 | `08` 表 8-9 G-01 / G-02 / G-03 | 不改动 G 结论(本期不改库兜底);仅约定“将来确需加字段”走独立评审 |
| P-06 | `09` 表 9-18 F-13 | **同一件事**(`version` 单调性);平台端推荐“数据库序列”,本端把 `version` 当增量游标(G-09 兜底)的前提 |
| P-08 | `09` 表 9-18 F-02 | **同一件事**(`gyId` 优先级);平台端终态 C、过渡 A(两值必须一致) |
| P-09 | `09` 表 9-7(#1 字段映射) | 契约相关:余额快照随 #1 下发还是独立下发未定,定稿后需回填表 9-7 |
| PR-09 | `09` 表 9-19 | 公寓隔离失效风险;本端约束已落到表 9-19 |
**不构成对应关系的部分**:P-01(前端入口)、P-03(`PL_` 前缀)、P-07(餐饮域归属)、P-10(管理命名空间)、P-11(平台端 HA)属平台侧自决,本端无需登记;PR-01~PR-08、PR-10 主要承接 `docs/10` D 系列与 `docs/09` 存量风险(见平台端表 9-3),与 `G` / `差` 系列无一一映射,反查以表 9-3 为准(其中 PR-01 若随 D13 改 MySQL,字符集与隔离级别按平台端 `README` 的差-1 行沿用本目录口径)。
+14
View File
@@ -0,0 +1,14 @@
const http = require('http'), fs = require('fs'), path = require('path');
const root = __dirname;
const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };
http.createServer((req, res) => {
let u = decodeURIComponent(req.url.split('?')[0]);
if (u === '/') u = '/duty-console.html';
const fp = path.join(root, u);
if (!fp.startsWith(root)) { res.writeHead(403); return res.end('403'); }
fs.readFile(fp, (e, d) => {
if (e) { res.writeHead(404); return res.end('404'); }
res.writeHead(200, { 'Content-Type': types[path.extname(fp)] || 'application/octet-stream' });
res.end(d);
});
}).listen(8899, '127.0.0.1', () => console.log('serving http://127.0.0.1:8899/'));
+780
View File
@@ -0,0 +1,780 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="data:,">
<title>公寓叫乘值班台</title>
<!--
输出路径:docs/公寓端/ui/duty-console-v2.html
面向人群:适老版 = 45 岁以上值班员(大字 / 大按钮 / 高对比);正常版 = 熟悉系统的值班员(紧凑高密度)
设计目标:一句话看懂、一次点击完成;危险操作二次确认 + 明确反馈
两套版本 + 两套布局(卡片 / 列表)共用同一份数据、同一套字段与同一组操作,仅密度与排布不同
取舍:保留深青 / 金 / 红的品牌识别,去掉跑马灯、闪烁、侧栏图标、11 列密集表格、
详情时间线、录音、排序、分页、批量、快捷键、下拉筛选、状态演示等干扰项
数据:单一 TASKS 数组派生全部计数、紧急卡片与任务;姓名脱敏,无真实凭据与内网地址
技术:单文件、零依赖、零外链、无构建;版本与布局选择存于 localStorage
-->
<style>
/* ============================================================
1. 设计令牌:同一套配色 / 状态色,两版只覆盖密度相关变量
适老版(默认):正文 18px 起、按钮 56px 起、3px 粗边、高对比深字浅底
============================================================ */
:root{
--brand:#026B69; --brand-dark:#024F4E;
--gold:#A66000; --red:#B3261E;
--ink:#17201F; --ink2:#404B49;
--bg:#F3F6F5; --card:#FFFFFF; --line:#C6D0CE;
--tint-warn:#FFF2D6; --tint-danger:#FBE3E1; --tint-ok:#E1F2E8;
--tint-live:#DCEDEB; --tint-done:#EEF1F0;
--fg-warn:#6B4700; --fg-danger:#8C1D18; --fg-ok:#155C36; --fg-done:#495652;
--fs-sm:18px; --fs-md:20px; --fs-lg:24px; --fs-xl:32px;
--bd:3px; --bd2:2px; --r:12px; --tap:56px; --tap-big:64px; --field:60px;
--pad-card:16px 18px; --gap:16px; --foot-pad:140px;
--grid-cols:2; --alert-cols:3; --ops-col:330px;
}
/* 正常版:信息、状态、操作完全一致,只压缩字号 / 间距 / 点击区并增加同屏条目 */
body[data-mode="normal"]{
--fs-sm:13px; --fs-md:14px; --fs-lg:16px; --fs-xl:22px;
--bd:1px; --bd2:1px; --r:8px; --tap:36px; --tap-big:40px; --field:40px;
--pad-card:12px 14px; --gap:10px; --foot-pad:80px;
--grid-cols:3; --alert-cols:3; --ops-col:230px;
}
body[data-mode="normal"] .hd{padding:12px 16px;margin-bottom:14px;border-radius:10px}
body[data-mode="normal"] .hd .mark{width:38px;height:38px;border-radius:8px}
body[data-mode="normal"] .hd .mark svg{width:22px;height:22px}
body[data-mode="normal"] .hd .acts{padding-top:12px}
body[data-mode="normal"] .btn{padding:0 14px;border-radius:7px}
body[data-mode="normal"] .sec{margin-bottom:18px}
body[data-mode="normal"] .seg,body[data-mode="normal"] .mode,body[data-mode="normal"] .view{min-width:0}
body[data-mode="normal"] .task .acts .btn{min-width:0}
body[data-mode="normal"] .acard .ic{width:32px;height:32px;font-size:20px;border-radius:7px}
body[data-mode="normal"] .calm .ic{width:32px;height:32px;font-size:18px;border-radius:7px}
body[data-mode="normal"] .field input,body[data-mode="normal"] .reason{min-height:var(--field)}
body[data-mode="normal"] .dlg-hd{padding:14px 18px}
body[data-mode="normal"] .dlg-bd{padding:14px 18px;gap:12px}
body[data-mode="normal"] .dlg-ft{padding:12px 18px 16px}
body[data-mode="normal"] .dlg-ft .btn{min-width:150px}
body[data-mode="normal"] .foot{padding:8px 20px}
body[data-mode="normal"] .foot .stat b{font-size:20px}
body[data-mode="normal"] .toast{padding:10px 18px}
body[data-mode="normal"] .empty{padding:26px 20px}
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{
background:var(--bg);color:var(--ink);font-size:var(--fs-sm);line-height:1.6;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Source Han Sans SC",sans-serif;
-webkit-font-smoothing:antialiased;
}
button,input{font-family:inherit}
button{cursor:pointer}
button:focus-visible,input:focus-visible{outline:4px solid #0B5FFF;outline-offset:2px}
.app{max-width:1480px;margin:0 auto;padding:20px 24px var(--foot-pad)}
/* ===== 顶栏:位置、时间、人、系统状态、三个明确按钮 ===== */
.hd{background:var(--card);border:var(--bd) solid var(--line);border-radius:16px;padding:18px 22px;display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-bottom:20px}
.hd .brand{display:flex;align-items:center;gap:14px;min-width:0}
.hd .mark{width:52px;height:52px;border-radius:12px;background:var(--brand);display:grid;place-items:center;flex:none}
.hd .mark svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.hd h1{font-size:var(--fs-xl);font-weight:800;line-height:1.2}
.hd .sub{font-size:var(--fs-sm);color:var(--ink2);margin-top:2px}
.hd .meta{margin-left:auto;display:flex;flex-wrap:wrap;gap:14px;align-items:center}
.hd .clock{font-size:var(--fs-lg);font-weight:800;white-space:nowrap;font-variant-numeric:tabular-nums}
.hd .who{font-size:var(--fs-sm);font-weight:700;color:var(--ink2)}
.status{display:inline-flex;align-items:center;gap:8px;font-size:var(--fs-sm);font-weight:800;color:var(--fg-ok);background:var(--tint-ok);border:var(--bd2) solid #A8D5BC;border-radius:999px;padding:6px 14px;white-space:nowrap}
.hd .acts{width:100%;display:flex;flex-wrap:wrap;gap:12px;border-top:var(--bd) solid var(--line);padding-top:16px;align-items:center}
/* 版本切换:两版下都常驻可见,文字明确、不用图标 */
.hd .modes{margin-left:auto;display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.hd .modes .lbl{font-size:var(--fs-sm);font-weight:800;color:var(--ink2);white-space:nowrap}
.modebtns{display:flex;flex-wrap:wrap;gap:8px}
.hd .acts .modes .btn{flex:0 0 auto;min-width:0}
.mode{min-width:118px}
/* ===== 按钮:实心、大字、56px 起(正常版 36px 起) ===== */
.btn{min-height:var(--tap);padding:0 22px;border-radius:10px;border:var(--bd) solid transparent;font-size:var(--fs-md);font-weight:800;display:inline-flex;align-items:center;justify-content:center;gap:8px;white-space:nowrap;transition:background-color .12s,border-color .12s}
.btn.primary{background:var(--brand);color:#fff;border-color:var(--brand)}
.btn.primary:hover{background:var(--brand-dark);border-color:var(--brand-dark)}
.btn.gold{background:var(--gold);color:#fff;border-color:var(--gold)}
.btn.gold:hover{background:#8A4E00;border-color:#8A4E00}
.btn.red{background:var(--red);color:#fff;border-color:var(--red)}
.btn.red:hover{background:#8C1D18;border-color:#8C1D18}
.btn.neutral{background:#fff;color:var(--ink);border-color:var(--line)}
.btn.neutral:hover{background:#EAF0EF}
.btn.outline-red{background:#fff;color:var(--red);border-color:var(--red)}
.btn.outline-red:hover{background:var(--tint-danger)}
.btn.big{min-height:var(--tap-big);font-size:var(--fs-lg);padding:0 26px}
.btn:active{transform:translateY(1px)}
/* ===== 区块标题:一句话说明;布局切换常驻可见 ===== */
.sec{margin-bottom:24px}
.sec-hd{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px;margin-bottom:12px}
.sec-hd h2{font-size:var(--fs-lg);font-weight:800}
.sec-hd .hint{font-size:var(--fs-sm);color:var(--ink2)}
.sec-hd .viewbtns{margin-left:auto;display:flex;flex-wrap:wrap;gap:8px;align-self:center}
.view{min-width:126px}
.count{font-size:var(--fs-sm);font-weight:800;color:var(--ink);background:#fff;border:var(--bd2) solid var(--line);border-radius:999px;padding:2px 12px}
/* ===== 需马上处理:紧急卡片网格,静态展示、不滚动、不闪烁 ===== */
.alerts{display:grid;grid-template-columns:repeat(var(--alert-cols),minmax(0,1fr));gap:var(--gap)}
.acard{display:flex;flex-direction:column;gap:12px;border:var(--bd) solid;border-radius:var(--r);padding:var(--pad-card)}
.acard.danger{background:var(--tint-danger);border-color:#D98880}
.acard.warn{background:var(--tint-warn);border-color:#DDB36B}
.acard .top{display:flex;align-items:center;gap:12px}
.acard .ic{width:46px;height:46px;border-radius:10px;display:grid;place-items:center;font-size:28px;font-weight:800;color:#fff;background:var(--red);flex:none}
.acard.warn .ic{background:var(--gold)}
.acard .who{font-size:var(--fs-lg);font-weight:800;line-height:1.3}
.acard .title{font-size:var(--fs-md);font-weight:800;line-height:1.45}
.acard .desc{font-size:var(--fs-sm);color:var(--ink2);line-height:1.5;flex:1}
.acard .btn{width:100%;min-width:0}
.calm{display:flex;align-items:center;gap:14px;background:var(--tint-ok);border:var(--bd) solid #A8D5BC;border-radius:var(--r);padding:16px 18px}
.calm .ic{width:46px;height:46px;border-radius:10px;display:grid;place-items:center;font-size:26px;font-weight:800;color:#fff;background:var(--fg-ok);flex:none}
.calm b{display:block;font-size:var(--fs-lg);font-weight:800}
.calm span{font-size:var(--fs-sm);color:var(--ink2)}
/* ===== 筛选:三个常驻大按钮,不用下拉 ===== */
.toolbar{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-bottom:16px}
.segs{display:flex;flex-wrap:wrap;gap:10px}
.seg{min-width:150px}
.search{display:flex;flex-wrap:wrap;gap:10px;flex:1;min-width:300px}
.search input{flex:1;min-width:240px;min-height:var(--tap);border:var(--bd) solid var(--line);border-radius:10px;padding:0 16px;font-size:var(--fs-md);color:var(--ink);background:#fff}
.search input::placeholder{color:#5C6A67}
.search input:focus{border-color:var(--brand)}
/* ===== 任务卡:一卡一任务,一个主按钮 ===== */
.grid{display:grid;grid-template-columns:repeat(var(--grid-cols),minmax(0,1fr));gap:var(--gap)}
.task{background:var(--card);border:var(--bd) solid var(--line);border-radius:14px;padding:var(--pad-card);display:flex;flex-direction:column;gap:10px}
.task.is-danger{border-color:#D98880;background:#FFFAFA}
.task.is-warn{border-color:#DDB36B;background:#FFFDF7}
.task .row1{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.task .room{font-size:var(--fs-xl);font-weight:800;line-height:1;font-variant-numeric:tabular-nums}
.task .room small{font-size:var(--fs-sm);font-weight:800;margin-left:4px}
.badge{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);font-weight:800;border:var(--bd2) solid;border-radius:999px;padding:4px 14px;white-space:nowrap}
.badge.warn{background:var(--tint-warn);color:var(--fg-warn);border-color:#DDB36B}
.badge.live{background:var(--tint-live);color:var(--brand-dark);border-color:#8FC3BF}
.badge.danger{background:var(--tint-danger);color:var(--fg-danger);border-color:#D98880}
.badge.ok{background:var(--tint-ok);color:var(--fg-ok);border-color:#A8D5BC}
.badge.done{background:var(--tint-done);color:var(--fg-done);border-color:var(--line)}
.task .name{font-size:var(--fs-lg);font-weight:800}
.task .name span{font-size:var(--fs-sm);font-weight:700;color:var(--ink2);margin-left:10px}
.task .meta{font-size:var(--fs-sm);color:var(--ink2);font-variant-numeric:tabular-nums}
.task .note{font-size:var(--fs-sm);font-weight:800;color:var(--ink2);min-height:29px}
.task .note.danger{color:var(--fg-danger)}
.task .acts{display:flex;flex-wrap:wrap;gap:10px;margin-top:2px}
.task .acts .btn{flex:1;min-width:160px}
.task .acts .btn.outline-red{flex:0 0 auto;min-width:150px}
.noact{font-size:var(--fs-sm);font-weight:800;color:var(--fg-done);background:var(--tint-done);border:var(--bd2) solid var(--line);border-radius:10px;padding:12px 14px}
.empty{background:#fff;border:var(--bd) dashed var(--line);border-radius:14px;padding:40px 24px;text-align:center}
.empty .big{font-size:var(--fs-xl);font-weight:800}
.empty p{font-size:var(--fs-sm);color:var(--ink2);margin:8px 0 18px}
/* ===== 列表布局:字段、状态与操作与卡片完全一致,只改为横向排布 ===== */
.list{background:var(--card);border:var(--bd) solid var(--line);border-radius:14px;overflow:hidden}
.lhead,.lrow{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1.35fr) minmax(0,1.8fr) minmax(0,1.1fr) minmax(0,2fr) var(--ops-col);gap:12px;align-items:center;padding:12px 16px}
.lhead{background:var(--tint-live);border-bottom:var(--bd) solid var(--line);font-size:var(--fs-sm);font-weight:800;color:var(--brand-dark)}
.lrow{border-bottom:var(--bd) solid var(--line)}
.lrow:last-child{border-bottom:0}
.lrow.is-danger{background:#FFFAFA;box-shadow:inset 5px 0 0 #D98880}
.lrow.is-warn{background:#FFFDF7;box-shadow:inset 5px 0 0 #DDB36B}
.lrow .room{font-size:var(--fs-lg);font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap}
.lrow .room small{font-size:var(--fs-sm);font-weight:800;margin-left:3px}
.lrow .badge{margin-left:0;justify-self:start}
.lrow .name{font-size:var(--fs-md);font-weight:800}
.lrow .name span{font-size:var(--fs-sm);font-weight:700;color:var(--ink2);margin-left:8px}
.lrow .meta{font-size:var(--fs-sm);color:var(--ink2);font-variant-numeric:tabular-nums}
.lrow .note{font-size:var(--fs-sm);font-weight:800;color:var(--ink2)}
.lrow .note.danger{color:var(--fg-danger)}
.lrow .acts{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}
.lrow .acts .btn{min-width:0;padding:0 14px}
.lrow .noact{font-size:var(--fs-sm);text-align:center;padding:8px 10px}
.lcap{display:none}
/* ===== 吸底状态条:三个数字 + 一条永久可见的最近操作 ===== */
.foot{position:sticky;bottom:0;z-index:20;background:#fff;border-top:var(--bd) solid var(--line);padding:12px 24px;display:flex;flex-wrap:wrap;gap:16px;align-items:center;box-shadow:0 -6px 18px rgba(23,32,31,.07)}
.foot .stats{display:flex;flex-wrap:wrap;gap:22px}
.foot .stat{font-size:var(--fs-sm);font-weight:800;color:var(--ink2);display:flex;align-items:baseline;gap:6px}
.foot .stat b{font-size:28px;font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums}
.foot .last{margin-left:auto;font-size:var(--fs-sm);font-weight:800;color:var(--brand-dark);background:var(--tint-live);border:var(--bd2) solid #8FC3BF;border-radius:10px;padding:8px 14px;max-width:100%}
/* ===== 弹窗:大字号、大按钮、取消按钮优先获得焦点 ===== */
.mask{position:fixed;inset:0;background:rgba(16,26,25,.62);display:none;align-items:center;justify-content:center;z-index:80;padding:16px}
.mask.on{display:flex}
.dlg{background:#fff;border:var(--bd) solid var(--line);border-radius:16px;width:min(640px,100%);max-height:calc(100vh - 32px);overflow:auto;box-shadow:0 24px 60px rgba(16,26,25,.3)}
.dlg-hd{font-size:var(--fs-lg);font-weight:800;padding:20px 24px;border-bottom:var(--bd) solid var(--line)}
.dlg-bd{padding:20px 24px;display:flex;flex-direction:column;gap:16px}
.dlg-ft{padding:16px 24px 22px;display:flex;flex-wrap:wrap;gap:12px;border-top:var(--bd) solid var(--line)}
.dlg-ft .btn{flex:1;min-width:210px}
.target{background:var(--tint-live);border:var(--bd) solid #8FC3BF;border-radius:var(--r);padding:14px 16px}
.target b{display:block;font-size:var(--fs-lg);font-weight:800}
.target span{font-size:var(--fs-sm);color:var(--ink2)}
.warntext{font-size:var(--fs-md);font-weight:800;color:var(--fg-danger);background:var(--tint-danger);border:var(--bd2) solid #D98880;border-radius:10px;padding:12px 14px;line-height:1.5}
.field label{display:block;font-size:var(--fs-md);font-weight:800;margin-bottom:8px}
.field input{width:100%;min-height:var(--field);border:var(--bd) solid var(--line);border-radius:10px;padding:0 14px;font-size:var(--fs-md);color:var(--ink);background:#fff}
.field input::placeholder{color:#5C6A67}
.field input:focus{border-color:var(--brand)}
.reasons{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.reason{min-height:var(--field);border:var(--bd) solid var(--line);border-radius:10px;background:#fff;font-size:var(--fs-md);font-weight:800;color:var(--ink)}
.reason[aria-pressed="true"]{background:var(--brand);border-color:var(--brand);color:#fff}
.err{font-size:var(--fs-md);font-weight:800;color:var(--fg-danger);display:none}
.err.on{display:block}
.note{font-size:var(--fs-sm);font-weight:700;color:var(--ink2);background:var(--tint-done);border-radius:10px;padding:12px 14px;line-height:1.5}
/* ===== 反馈:不走马灯、不自动淡出太快,4.5 秒 + 永久最近操作 ===== */
.toast{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:100;background:#163A38;color:#fff;font-size:var(--fs-lg);font-weight:800;padding:14px 24px;border-radius:12px;box-shadow:0 16px 40px rgba(16,26,25,.35);max-width:calc(100vw - 24px);display:none;text-align:center;line-height:1.4}
.toast.on{display:block}
/* ===== 响应式:两版都只改列数 / 堆叠,适老版字号不缩水、不用横向手势 ===== */
@media (max-width:1000px){
:root{--grid-cols:1;--alert-cols:1}
body[data-mode="normal"]{--grid-cols:2;--alert-cols:2}
.hd .meta{margin-left:0;width:100%}
.foot .last{margin-left:0;width:100%}
.sec-hd .viewbtns{margin-left:0;width:100%}
.lhead{display:none}
.lrow{grid-template-columns:1fr;gap:6px;padding:14px 16px}
.lcap{display:inline;font-weight:800;color:var(--ink2);margin-right:8px;white-space:nowrap}
.lrow .acts{justify-content:flex-start}
}
@media (max-width:640px){
:root{--grid-cols:1;--alert-cols:1;--foot-pad:150px}
body[data-mode="normal"]{--grid-cols:1;--alert-cols:1;--foot-pad:96px}
.app{padding:12px 12px var(--foot-pad)}
.hd{padding:14px;gap:12px}
.hd h1{font-size:26px}
body[data-mode="normal"] .hd h1{font-size:18px}
.hd .meta{gap:8px 14px}
.hd .clock{font-size:var(--fs-sm)}
.hd .acts{padding-top:12px;gap:8px}
.hd .acts .btn{flex:1 1 calc(33.333% - 6px);min-width:0;padding:0 8px;font-size:var(--fs-sm)}
.hd .acts .modes{width:100%;margin-left:0}
.hd .acts .modebtns{flex:1}
.hd .acts .modes .btn{flex:1}
.view{flex:1 1 calc(50% - 6px);min-width:0}
.seg{flex:1 1 calc(50% - 6px);min-width:0}
.search input{min-width:100%}
.search .btn{flex:1}
.task .acts .btn,.lrow .acts .btn{flex:1 1 100%}
.foot{padding:8px 12px;gap:8px}
.foot .stats{gap:10px}
.foot .stat b{font-size:24px}
body[data-mode="normal"] .foot .stat b{font-size:18px}
.foot .last{font-size:var(--fs-sm);padding:6px 10px}
.reasons{grid-template-columns:1fr}
.dlg-ft .btn{min-width:100%}
.dlg-hd,.dlg-bd,.dlg-ft{padding-left:16px;padding-right:16px}
}
@media (prefers-reduced-motion: reduce){
.btn{transition:none}
.btn:active{transform:none}
}
</style>
</head>
<body data-mode="senior">
<script>try{if(localStorage.getItem('duty.mode')==='normal')document.body.setAttribute('data-mode','normal');}catch(e){}</script>
<div class="app">
<!-- ============ 顶栏 ============ -->
<header class="hd">
<div class="brand">
<div class="mark" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-5h6v5"/></svg></div>
<div>
<h1>叫乘值班台</h1>
<div class="sub">找到房间,点大按钮即可。红色按钮会再确认一次。</div>
</div>
</div>
<div class="meta">
<span class="status" id="sysStatus">✓ 系统正常,可以正常叫乘</span>
<span class="clock" id="clock">11月7日 星期六 08:36 · 白班</span>
<span class="who">值班员 刘*</span>
</div>
<div class="acts">
<button class="btn primary" data-act="refresh">刷新列表</button>
<button class="btn gold" data-act="temp">临时叫乘</button>
<button class="btn neutral" data-act="handover">交接班</button>
<div class="modes" role="group" aria-label="界面版本">
<span class="lbl">界面版本</span>
<div class="modebtns" id="modes"></div>
</div>
</div>
</header>
<!-- ============ 需马上处理(卡片) ============ -->
<section class="sec" aria-labelledby="hAlert">
<div class="sec-hd"><h2 id="hAlert">需要马上处理</h2><span class="count" id="alertN">0 条</span>
<span class="hint">以卡片显示最紧急的 3 条,处理完自动消失。</span></div>
<div id="alerts"></div>
</section>
<!-- ============ 任务(卡片 / 列表可切换) ============ -->
<section class="sec" aria-labelledby="hTasks">
<div class="sec-hd"><h2 id="hTasks">叫乘任务</h2>
<span class="hint">房间、姓名、时刻、状态和操作都会完整显示。</span>
<div class="viewbtns" id="viewBtns" role="group" aria-label="任务展示方式"></div>
</div>
<div class="toolbar">
<div class="segs" id="segs" role="group" aria-label="任务筛选"></div>
<div class="search">
<label class="sr-only" for="q" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">搜索房间号或姓名</label>
<input id="q" type="text" inputmode="text" placeholder="输入房间号或姓名,例如 305">
<button class="btn neutral" data-act="clear">清空</button>
</div>
</div>
<div id="list"></div>
</section>
</div>
<!-- ============ 吸底状态条 ============ -->
<footer class="foot">
<div class="stats">
<span class="stat">待叫乘 <b id="sTodo">0</b> 条</span>
<span class="stat">呼叫与通话 <b id="sCall">0</b> 条</span>
<span class="stat">报警 <b id="sAlarm">0</b> 条</span>
</div>
<div class="last" id="lastAction" role="status" aria-live="polite">最近操作:暂无,点上方按钮开始</div>
</footer>
<!-- ============ 二次确认(停止叫乘 / 交接班) ============ -->
<div class="mask" id="maskConfirm">
<div class="dlg" role="dialog" aria-modal="true" aria-labelledby="dlgTitle">
<div class="dlg-hd" id="dlgTitle">确认停止叫乘?</div>
<div class="dlg-bd" id="dlgBody"></div>
<div class="dlg-ft">
<button class="btn neutral" data-act="cancelDialog" id="dlgCancel">取消,不做改动</button>
<button class="btn red" data-act="confirmDialog" id="dlgOk">确认停止叫乘</button>
</div>
</div>
</div>
<!-- ============ 临时叫乘 ============ -->
<div class="mask" id="maskTmp">
<div class="dlg" role="dialog" aria-modal="true" aria-labelledby="tmpTitle">
<div class="dlg-hd" id="tmpTitle">临时叫乘登记</div>
<div class="dlg-bd">
<div class="field"><label for="tmpRoom">房间号(必填)</label><input id="tmpRoom" placeholder="例如:305"></div>
<div class="field"><label for="tmpTime">叫乘时刻(必填)</label><input id="tmpTime" placeholder="例如:09:05"></div>
<div class="field"><label for="tmpCc">车次(可不填)</label><input id="tmpCc" placeholder="例如:G1234"></div>
<div class="err" id="tmpErr"></div>
<div class="note">开车时刻按「叫乘时刻 + 30 分钟」自动计算。登记后会出现在「需处理」列表最上面。</div>
</div>
<div class="dlg-ft">
<button class="btn neutral" data-act="closeTmp">取消</button>
<button class="btn primary" data-act="doTmp">登记并立即叫乘</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
/* ============================================================
单一数据源:提醒条、筛选计数、底部数字、任务卡全部由此派生
状态:todo 待叫乘 / calling 呼叫中 / talking 通话中 / alarm 报警
leave 待离寓 / in 已入寓 / stop 已停止 / done 已完成
============================================================ */
var NOW = 8 * 60 + 36;
var DATE = '11月7日 星期六';
var ELAPSED = 0;
var TASKS = [
{ jcid:'JC20261107002', fjh:'305', rymc:'李*', cc:'G5678', kcsj:'09:00', jcsj:'08:30', st:'todo', overdue:true, alarm:'' },
{ jcid:'JC20261107003', fjh:'302', rymc:'周*', cc:'G2210', kcsj:'09:10', jcsj:'08:40', st:'calling', overdue:false, alarm:'' },
{ jcid:'JC20261107017', fjh:'345', rymc:'林*', cc:'G6699', kcsj:'09:48', jcsj:'09:18', st:'calling', overdue:false, alarm:'' },
{ jcid:'JC20261107004', fjh:'307', rymc:'吴*', cc:'G8812', kcsj:'09:16', jcsj:'08:46', st:'talking', overdue:false, alarm:'' },
{ jcid:'JC20261107005', fjh:'310', rymc:'郑*', cc:'K1524', kcsj:'09:22', jcsj:'08:52', st:'alarm', overdue:false, alarm:'jc' },
{ jcid:'JC20261107006', fjh:'315', rymc:'冯*', cc:'G4521', kcsj:'09:30', jcsj:'09:00', st:'leave', overdue:false, alarm:'' },
{ jcid:'JC20261107007', fjh:'318', rymc:'钱*', cc:'G7702', kcsj:'09:26', jcsj:'08:56', st:'alarm', overdue:false, alarm:'ly' },
{ jcid:'JC20261107001', fjh:'301', rymc:'张*', cc:'G1234', kcsj:'10:08', jcsj:'09:38', st:'todo', overdue:false, alarm:'' },
{ jcid:'JC20261107013', fjh:'328', rymc:'何*', cc:'G9922', kcsj:'10:32', jcsj:'10:02', st:'todo', overdue:false, alarm:'' },
{ jcid:'JC20261107014', fjh:'330', rymc:'陈*', cc:'K2216', kcsj:'10:45', jcsj:'10:15', st:'todo', overdue:false, alarm:'' },
{ jcid:'JC20261107015', fjh:'336', rymc:'杨*', cc:'G3344', kcsj:'10:58', jcsj:'10:28', st:'todo', overdue:false, alarm:'' },
{ jcid:'JC20261107016', fjh:'340', rymc:'黄*', cc:'D4402', kcsj:'11:10', jcsj:'10:40', st:'todo', overdue:false, alarm:'' },
{ jcid:'JC20261106010', fjh:'312', rymc:'许*', cc:'K8802', kcsj:'09:05', jcsj:'08:35', st:'stop', overdue:false, alarm:'' },
{ jcid:'JC20261106011', fjh:'316', rymc:'朱*', cc:'G1122', kcsj:'08:50', jcsj:'08:20', st:'done', overdue:false, alarm:'' },
{ jcid:'JC20261106012', fjh:'322', rymc:'秦*', cc:'G5566', kcsj:'09:00', jcsj:'08:30', st:'done', overdue:false, alarm:'' },
{ jcid:'JC20261106008', fjh:'205', rymc:'孙*', cc:'D3306', kcsj:'11:20', jcsj:'', st:'in', overdue:false, alarm:'' },
{ jcid:'JC20261106009', fjh:'208', rymc:'赵*', cc:'D3308', kcsj:'12:05', jcsj:'', st:'in', overdue:false, alarm:'' },
{ jcid:'JC20261106005', fjh:'350', rymc:'徐*', cc:'K7733', kcsj:'13:20', jcsj:'', st:'in', overdue:false, alarm:'' }
];
var FILTER = 'need';
var SEARCH = '';
var LAST = '';
var TOAST_TMR = null;
var CF = { kind:'', jcid:'', reason:'' };
var REASONS = ['本人已到站','已自行离寓','设备故障','其他原因'];
/* 界面版本 / 展示布局:只改变密度与排布,不触碰任务数据、筛选、弹窗与操作状态 */
function loadPref(k, d, allow){
try{ var v = localStorage.getItem(k); return allow.indexOf(v) >= 0 ? v : d; }catch(e){ return d; }
}
function savePref(k, v){ try{ localStorage.setItem(k, v); }catch(e){} }
var MODE = loadPref('duty.mode','senior',['senior','normal']);
var LAYOUT = loadPref('duty.layout','card',['card','list']);
function applyMode(){
document.body.setAttribute('data-mode', MODE);
document.title = '公寓叫乘值班台 · ' + (MODE === 'senior' ? '适老版' : '正常版');
}
function focusBtn(attr, val){
var el = document.querySelector('[' + attr + '="' + val + '"]');
if(el) el.focus({ preventScroll:true }); /* 恢复焦点但不打断已保持的滚动位置 */
}
function byId(id){ return document.getElementById(id); }
function pad(n){ return n < 10 ? '0' + n : '' + n; }
function toMin(s){ if(!s) return null; var a = s.split(':'); return (+a[0]) * 60 + (+a[1]); }
function hhmm(m){ return pad(Math.floor(m / 60)) + ':' + pad(m % 60); }
function nowMin(){ return NOW + Math.floor(ELAPSED / 60); }
function esc(s){ return String(s == null ? '' : s).replace(/[&<>"']/g,function(c){
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
}); }
function taskById(id){ for(var i=0;i<TASKS.length;i++){ if(TASKS[i].jcid === id) return TASKS[i]; } return null; }
/* ---------- 派生口径 ---------- */
function isNeed(t){ return ['todo','calling','talking','alarm','leave'].indexOf(t.st) >= 0; }
function isDone(t){ return t.st === 'done' || t.st === 'stop'; }
function isUrgent(t){ return (t.st === 'todo' && t.overdue) || t.alarm === 'jc' || t.alarm === 'ly'; }
function stats(){
var s = { todo:0, call:0, alarm:0, need:0, done:0, urgent:0 };
TASKS.forEach(function(t){
if(t.st === 'todo') s.todo++;
if(t.st === 'calling' || t.st === 'talking') s.call++;
if(t.alarm) s.alarm++;
if(isNeed(t)) s.need++;
if(isDone(t)) s.done++;
if(isUrgent(t)) s.urgent++;
});
return s;
}
function statusOf(t){
if(t.alarm === 'jc') return { text:'! 叫乘报警', cls:'danger' };
if(t.alarm === 'ly') return { text:'! 离寓报警', cls:'danger' };
var m = {
todo:{ text:'● 待叫乘', cls:'warn' }, calling:{ text:'● 呼叫中', cls:'live' },
talking:{ text:'● 通话中', cls:'live' }, alarm:{ text:'! 报警', cls:'danger' },
leave:{ text:'● 待离寓', cls:'ok' }, in:{ text:'○ 已入寓', cls:'done' },
stop:{ text:'× 已停止', cls:'done' }, done:{ text:'✓ 已完成', cls:'ok' }
};
return m[t.st];
}
function primaryOf(t){
if(t.st === 'todo') return { label:'立即叫乘', act:'respond', cls:'primary' };
if(t.st === 'calling') return { label:'确认接通', act:'confirm', cls:'primary' };
if(t.st === 'talking') return { label:'登记离寓', act:'leave', cls:'primary' };
if(t.st === 'alarm') return t.alarm === 'jc'
? { label:'复叫', act:'recall', cls:'gold' }
: { label:'登记离寓', act:'leave', cls:'primary' };
if(t.st === 'leave') return { label:'登记离寓', act:'leave', cls:'primary' };
if(t.st === 'stop') return { label:'重新叫乘', act:'recall', cls:'gold' };
return null;
}
function showStop(t){ return ['calling','talking','alarm','leave'].indexOf(t.st) >= 0; }
function noteOf(t){
if(t.st === 'todo' && t.overdue) return { text:'已过叫乘时间 ' + (nowMin() - toMin(t.jcsj)) + ' 分钟,请尽快叫乘', danger:true };
if(t.st === 'todo') return { text:'距叫乘还有 ' + Math.max(0, toMin(t.jcsj) - nowMin()) + ' 分钟', danger:false };
if(t.st === 'calling') return { text:'正在呼叫,请等待对方接听', danger:false };
if(t.st === 'talking') return { text:'通话中,确认后登记离寓', danger:false };
if(t.alarm === 'jc') return { text:'30 秒无人接听,建议复叫或人工联系', danger:true };
if(t.alarm === 'ly') return { text:'应离寓时间已过,请确认本人情况', danger:true };
if(t.st === 'leave') return { text:'已应答,等待登记离寓', danger:false };
if(t.st === 'stop') return { text:'已停止,不再自动叫乘', danger:false };
if(t.st === 'done') return { text:'叫乘已完成', danger:false };
return { text:'已入寓,等待生成叫乘任务', danger:false };
}
function urgentTitle(t){
if(t.st === 'todo' && t.overdue) return '叫乘时间已过 ' + (nowMin() - toMin(t.jcsj)) + ' 分钟';
if(t.alarm === 'jc') return '叫乘报警 · 无人接听';
return '离寓报警 · 未登记离寓';
}
function urgentHint(t){
if(t.st === 'todo' && t.overdue) return '开车 ' + (t.kcsj || '—') + ',越早处理越稳妥';
if(t.alarm === 'jc') return '叫乘 ' + (t.jcsj || '—') + ',30 秒内未接通';
return '开车 ' + (t.kcsj || '—') + ',请确认本人是否已离开';
}
function visibleTasks(){
var q = SEARCH.toLowerCase();
var list = TASKS.filter(function(t){
var ok = FILTER === 'all' ? true : (FILTER === 'need' ? isNeed(t) : isDone(t));
if(!ok) return false;
if(q && (t.fjh + ' ' + t.rymc + ' ' + t.cc).toLowerCase().indexOf(q) < 0) return false;
return true;
});
var rank = function(t){ return isUrgent(t) ? 0 : (isNeed(t) ? 1 : 2); };
list.sort(function(a,b){
var r = rank(a) - rank(b); if(r) return r;
var x = a.jcsj ? toMin(a.jcsj) : 99999, y = b.jcsj ? toMin(b.jcsj) : 99999;
return x - y;
});
return list;
}
/* ---------- 片段:两种版本 / 两种布局共用同一份字段与动作 ---------- */
function modeHTML(){
return [['senior','适老版'],['normal','正常版']].map(function(d){
var on = MODE === d[0];
return '<button class="btn mode ' + (on ? 'primary' : 'neutral') + '" data-act="mode" data-m="' + d[0] + '"' +
' aria-pressed="' + on + '">' + (on ? '✓ ' : '') + d[1] + '</button>';
}).join('');
}
function viewHTML(){
return [['card','卡片显示'],['list','列表显示']].map(function(d){
var on = LAYOUT === d[0];
return '<button class="btn view ' + (on ? 'primary' : 'neutral') + '" data-act="view" data-v="' + d[0] + '"' +
' aria-pressed="' + on + '">' + (on ? '✓ ' : '') + d[1] + '</button>';
}).join('');
}
function segHTML(s){
var defs = [['need','需处理',s.need],['all','全部',TASKS.length],['done','已完成',s.done]];
return defs.map(function(d){
var on = FILTER === d[0];
return '<button class="btn seg ' + (on ? 'primary' : 'neutral') + '" data-act="filter" data-f="' + d[0] + '"' +
' aria-pressed="' + on + '">' + (on ? '✓ ' : '') + d[1] + ' ' + d[2] + '</button>';
}).join('');
}
function alertsHTML(){
var u = TASKS.filter(isUrgent).slice(0,3);
if(!u.length){
return '<div class="calm"><div class="ic" aria-hidden="true">✓</div><div>' +
'<b>当前没有需要马上处理的事项</b><span>有新报警或超时任务时,会自动出现在这里。</span></div></div>';
}
return '<div class="alerts">' + u.map(function(t){
var p = primaryOf(t);
return '<article class="acard ' + (t.alarm !== '' ? 'danger' : 'warn') + '">' +
'<div class="top"><div class="ic" aria-hidden="true">!</div>' +
'<div class="who">' + esc(t.fjh) + ' 房 ' + esc(t.rymc) + '</div></div>' +
'<div class="title">' + urgentTitle(t) + '</div>' +
'<div class="desc">' + urgentHint(t) + '<br>车次 ' + esc(t.cc) + '</div>' +
(p ? '<button class="btn big ' + p.cls + '" data-act="' + p.act + '" data-jcid="' + t.jcid + '">' + p.label + '</button>' : '') +
'</article>';
}).join('') + '</div>';
}
function statusCls(t){
return ((t.st === 'todo' && t.overdue) || t.alarm) ? ' is-danger' : (t.st === 'todo' ? ' is-warn' : '');
}
function actionsHTML(t, p){
if(p){
var h = '<div class="acts"><button class="btn ' + p.cls + '" data-act="' + p.act + '" data-jcid="' + t.jcid + '">' + p.label + '</button>';
if(showStop(t)) h += '<button class="btn outline-red" data-act="stop" data-jcid="' + t.jcid + '">停止叫乘</button>';
return h + '</div>';
}
return '<div class="noact">' + (t.st === 'in' ? '已入寓,等待叫乘任务' : '已结束,无需操作') + '</div>';
}
function cardHTML(t){
var st = statusOf(t), n = noteOf(t), p = primaryOf(t);
var h = '<article class="task' + statusCls(t) + '">';
h += '<div class="row1"><div class="room">' + esc(t.fjh) + '<small>房</small></div>' +
'<span class="badge ' + st.cls + '">' + st.text + '</span></div>';
h += '<div class="name">' + esc(t.rymc) + '<span>' + esc(t.cc) + '</span></div>';
h += '<div class="meta">叫乘 ' + (t.jcsj || '—') + ' | 开车 ' + (t.kcsj || '—') + '</div>';
h += '<div class="note' + (n.danger ? ' danger' : '') + '">' + n.text + '</div>';
return h + actionsHTML(t,p) + '</article>';
}
function rowHTML(t){
var st = statusOf(t), n = noteOf(t), p = primaryOf(t);
var h = '<article class="lrow' + statusCls(t) + '">';
h += '<div><span class="lcap">房间</span><span class="room">' + esc(t.fjh) + '<small>房</small></span></div>';
h += '<div><span class="lcap">人员</span><span class="name">' + esc(t.rymc) + '<span>' + esc(t.cc) + '</span></span></div>';
h += '<div><span class="lcap">时刻</span><span class="meta">叫乘 ' + (t.jcsj || '—') + ' | 开车 ' + (t.kcsj || '—') + '</span></div>';
h += '<div><span class="lcap">状态</span><span class="badge ' + st.cls + '">' + st.text + '</span></div>';
h += '<div><span class="lcap">提示</span><span class="note' + (n.danger ? ' danger' : '') + '">' + n.text + '</span></div>';
h += '<div><span class="lcap">操作</span>' + actionsHTML(t,p) + '</div>';
return h + '</article>';
}
function listHTML(list){
var head = '<div class="lhead"><div>房间</div><div>人员 / 车次</div><div>叫乘 / 开车</div>' +
'<div>状态</div><div>处理提示</div><div>操作</div></div>';
return '<div class="list">' + head + list.map(rowHTML).join('') + '</div>';
}
function emptyHTML(){
return '<div class="empty"><div class="big">这里暂时没有任务</div>' +
'<p>可以点上方「全部」查看所有任务,或点「清空」去掉搜索条件。</p>' +
'<button class="btn primary" data-act="filter" data-f="all">查看全部任务</button></div>';
}
/* ---------- 渲染 ---------- */
function render(keepView){
var s = stats();
var y = window.scrollY;
byId('clock').textContent = DATE + ' ' + hhmm(nowMin()) + ' · 白班';
byId('sTodo').textContent = s.todo;
byId('sCall').textContent = s.call;
byId('sAlarm').textContent = s.alarm;
byId('alertN').textContent = TASKS.filter(isUrgent).length + ' 条';
byId('modes').innerHTML = modeHTML();
byId('viewBtns').innerHTML = viewHTML();
byId('segs').innerHTML = segHTML(s);
byId('alerts').innerHTML = alertsHTML();
var list = visibleTasks();
byId('list').innerHTML = list.length
? (LAYOUT === 'list' ? listHTML(list) : '<div class="grid">' + list.map(cardHTML).join('') + '</div>')
: emptyHTML();
byId('lastAction').textContent = LAST ? '最近操作:' + LAST : '最近操作:暂无,点上方按钮开始';
/* 切换版本 / 布局后若新内容变短,退到任务区而不是页顶,保证用户仍看得到刚切换的结果 */
var maxY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
if(keepView && y > maxY){
var listTop = byId('list').getBoundingClientRect().top + window.scrollY - 12;
window.scrollTo(0, Math.max(0, Math.min(listTop, maxY)));
}else{
window.scrollTo(0,y);
}
}
/* ---------- 反馈 ---------- */
function toast(msg){
var el = byId('toast');
el.textContent = msg; el.classList.add('on');
clearTimeout(TOAST_TMR);
TOAST_TMR = setTimeout(function(){ el.classList.remove('on'); }, 4500);
}
function setLast(msg){ LAST = hhmm(nowMin()) + ' ' + msg; }
/* ---------- 一次点击完成的常规操作 ---------- */
function doAction(act,jcid){
var t = taskById(jcid); if(!t) return;
var who = t.fjh + ' 房 ' + t.rymc;
if(act === 'respond'){ t.st = 'calling'; t.overdue = false; toast('已叫乘:' + who + ',正在呼叫'); setLast('已叫乘 ' + who); }
if(act === 'recall'){ t.st = 'calling'; t.alarm = ''; t.overdue = false; toast('已复叫:' + who + ',正在重新呼叫'); setLast('已复叫 ' + who); }
if(act === 'confirm'){ t.st = 'talking'; toast('已确认接通:' + who + ',请提醒本人准备出乘'); setLast('确认接通 ' + who); }
if(act === 'leave'){ t.st = 'done'; t.alarm = ''; toast('已登记离寓:' + who + ',任务已完成'); setLast('登记离寓 ' + who); }
render();
}
/* ---------- 二次确认 ---------- */
function closeDialog(){ byId('maskConfirm').classList.remove('on'); }
function openConfirm(kind,jcid){
CF = { kind:kind, jcid:jcid, reason:'' };
var s = stats();
if(kind === 'stop'){
var t = taskById(jcid); if(!t) return;
byId('dlgTitle').textContent = '确认停止叫乘?';
byId('dlgBody').innerHTML =
'<div class="target"><b>' + t.fjh + ' 房 · ' + esc(t.rymc) + '</b>' +
'<span>车次 ' + esc(t.cc) + ' | 叫乘 ' + (t.jcsj || '—') + ' | 开车 ' + (t.kcsj || '—') + '</span></div>' +
'<div class="warntext">停止后不再自动叫乘,也不会再提醒。此操作会记录,请确认无误后再点。</div>' +
'<div class="field"><span style="display:block;font-size:var(--fs-md);font-weight:800;margin-bottom:8px">请选择停止原因(必选)</span>' +
'<div class="reasons">' + REASONS.map(function(r){
return '<button type="button" class="reason" data-act="reason" data-reason="' + r + '" aria-pressed="false">' + r + '</button>';
}).join('') + '</div></div>' +
'<div class="field" id="reasonOther" style="display:none"><label for="reasonText">简单写下原因</label>' +
'<input id="reasonText" placeholder="例如:本人已自行到站"></div>' +
'<div class="err" id="dlgErr">请先点选一个原因,再确认停止。</div>';
byId('dlgCancel').textContent = '取消,不做改动';
byId('dlgOk').textContent = '确认停止叫乘';
byId('dlgOk').className = 'btn red';
}else{
byId('dlgTitle').textContent = '确认交接班?';
byId('dlgBody').innerHTML =
'<div class="target"><b>当前还有 ' + s.need + ' 条任务需要处理</b>' +
'<span>其中 ' + s.urgent + ' 条需要马上处理,会一并交给下一位值班员。</span></div>' +
'<div class="warntext">交接后本班次结束,未处理事项会带到下一班。此操作会记录。</div>' +
'<div class="err" id="dlgErr"></div>';
byId('dlgCancel').textContent = '取消,继续值班';
byId('dlgOk').textContent = '确认交接班';
byId('dlgOk').className = 'btn primary';
}
byId('dlgErr').classList.remove('on');
byId('maskConfirm').classList.add('on');
setTimeout(function(){ byId('dlgCancel').focus(); }, 0); /* 默认焦点在取消,避免误按回车直接确认 */
}
function doConfirm(){
if(CF.kind === 'stop'){
var err = byId('dlgErr');
if(!CF.reason){ err.textContent = '请先点选一个原因,再确认停止。'; err.classList.add('on'); return; }
if(CF.reason === '其他原因' && byId('reasonText').value.trim().length < 2){
err.textContent = '请简单写下原因,至少 2 个字。'; err.classList.add('on'); return;
}
var t = taskById(CF.jcid);
t.st = 'stop'; t.alarm = ''; t.overdue = false;
closeDialog();
toast('已停止 ' + t.fjh + ' 房 ' + t.rymc + ' 的叫乘');
setLast('停止叫乘 ' + t.fjh + ' 房 ' + t.rymc + '(' + CF.reason + ')');
}else{
closeDialog();
toast('已完成交接班,带入 ' + stats().urgent + ' 条未处理事项');
setLast('完成交接班');
}
render();
}
/* ---------- 临时叫乘 ---------- */
function openTmp(){
['tmpRoom','tmpTime','tmpCc'].forEach(function(id){ byId(id).value = ''; });
byId('tmpErr').classList.remove('on');
byId('maskTmp').classList.add('on');
setTimeout(function(){ byId('tmpRoom').focus(); },0);
}
function closeTmp(){ byId('maskTmp').classList.remove('on'); }
function doTmp(){
var room = byId('tmpRoom').value.trim();
var time = byId('tmpTime').value.trim();
var cc = esc(byId('tmpCc').value.trim() || '临时');
var err = byId('tmpErr');
if(!/^\d{3,4}$/.test(room)){ err.textContent = '房间号请填 3–4 位数字,例如 305。'; err.classList.add('on'); byId('tmpRoom').focus(); return; }
if(!/^([01]\d|2[0-3]):[0-5]\d$/.test(time)){ err.textContent = '叫乘时刻请按 09:05 这样的格式填写。'; err.classList.add('on'); byId('tmpTime').focus(); return; }
var m = toMin(time);
TASKS.unshift({
jcid:'JC202611070' + (19 + TASKS.length), fjh:room, rymc:'临*', cc:cc,
kcsj:hhmm(m + 30), jcsj:time, st:'todo', overdue:(m < nowMin()), alarm:''
});
FILTER = 'need'; SEARCH = ''; byId('q').value = '';
closeTmp();
toast('已登记临时叫乘:' + room + ' 房 ' + time + ',请点「立即叫乘」');
setLast('登记临时叫乘 ' + room + ' 房');
render();
}
/* ---------- 事件:全部为可见按钮,无快捷键、无手势、无悬停菜单 ---------- */
document.addEventListener('click',function(e){
var b = e.target.closest('[data-act]'); if(!b) return;
var act = b.getAttribute('data-act'), jcid = b.getAttribute('data-jcid');
switch(act){
case 'mode': {
var nm = b.getAttribute('data-m');
if(MODE === nm) break;
MODE = nm; savePref('duty.mode', MODE); applyMode();
/* 只给即时反馈,不写「最近操作」,避免覆盖值班员的业务操作记录 */
toast('已切换到' + (MODE === 'senior' ? '适老版(大字大按钮)' : '正常版(紧凑显示)') + ',任务、筛选和操作记录保持不变');
render(true); focusBtn('data-m', MODE);
break;
}
case 'view': {
var nv = b.getAttribute('data-v');
if(LAYOUT === nv) break;
LAYOUT = nv; savePref('duty.layout', LAYOUT);
/* 同上:布局切换也不改写「最近操作」 */
toast('已切换到' + (LAYOUT === 'card' ? '卡片显示' : '列表显示') + ',任务内容完全相同');
render(true); focusBtn('data-v', LAYOUT);
break;
}
case 'filter': FILTER = b.getAttribute('data-f'); render(); break;
case 'clear': SEARCH = ''; byId('q').value = ''; render(); byId('q').focus(); break;
case 'refresh': render(); toast('列表已刷新'); setLast('刷新了任务列表'); render(); break;
case 'temp': openTmp(); break;
case 'closeTmp': closeTmp(); toast('已取消,未做任何改动'); break;
case 'doTmp': doTmp(); break;
case 'handover': openConfirm('handover',''); break;
case 'respond': case 'recall': case 'confirm': case 'leave': doAction(act,jcid); break;
case 'stop': openConfirm('stop',jcid); break;
case 'reason':
CF.reason = b.getAttribute('data-reason');
document.querySelectorAll('.reason').forEach(function(el){
el.setAttribute('aria-pressed', el.getAttribute('data-reason') === CF.reason ? 'true' : 'false');
});
byId('reasonOther').style.display = CF.reason === '其他原因' ? 'block' : 'none';
byId('dlgErr').classList.remove('on');
break;
case 'cancelDialog': closeDialog(); toast('已取消,未做任何改动'); break;
case 'confirmDialog': doConfirm(); break;
}
});
document.addEventListener('input',function(e){
if(e.target.id === 'q'){ SEARCH = e.target.value.trim(); render(); }
});
document.addEventListener('keydown',function(e){
if(e.key === 'Escape'){ closeDialog(); closeTmp(); }
});
/* 每 30 秒更新一次分钟级时钟与倒计时;不显示秒数、不闪烁、不滚动 */
setInterval(function(){ ELAPSED += 30; render(); }, 30000);
applyMode();
render();
</script>
</body>
</html>
+206
View File
@@ -0,0 +1,206 @@
# 01 平台端功能边界
> 追溯:`docs/10` §3.1(平台端负责 / 不负责)、§3.3 表 3-1(应用端与公寓端的逻辑分工)、
> §3.4 表 3-2(语音 / 分机 / 事件的三端分工)、§4.1 表 4-1(能力归属决策表)、§4.2 表 4-2(平台端模块清单)、
> §8.1 表 8-1(三端部署形态)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(平台不回调公寓、下行一律拉)、§9(阶段五“平台端收敛与旧逻辑下线”)、§11.1(总体结论)。
> 现状来源:`docs/01` §3 运行形态、`docs/02` 工程结构、`docs/03` 业务模块地图、`docs/05` 数据模型、`docs/07` 外部集成。
> **v2 修订(CR-02,2026-11-09)**:本端覆盖范围以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §2 为权威,见下文 §0。
## 0. v2 覆盖范围修订(CR-02)
> 依据 `docs/公寓端/00` 的重新规划。本节图表用独立编号 `表 C-y`,不占用本文件 `表 1-y` 序号;原 §1~§6 与 CR-02 冲突处以 CR-02 表 3-4 为准。
表 C-1 平台端新增 / 调整职责(● 主责 ○ 参与 — 不做)
| 能力 | 平台端 | 公寓端 | 应用端 | 裁定 |
| --- | --- | --- | --- | --- |
| 入住 / 离寓 / 换房 | ○ 归集 | ● 执行 | ○ 发起 | CR-C02:本期实现,撤销公寓端"属后续" |
| 消费 / 充值 / 补贴(含断网) | ● 主账、规则、离线额度 | ● 本地受理 | ○ 登记 / 查询 | CR-C03:账务提升本期;主账唯一写入口仍 `platform-ledger` |
| 人员与人脸本地登记 | ● 真源与确认 | ● 本地登记(`local_only`) | ○ 查看 | CR-C04:确认前不参与运安匹配 |
| 房间现场台账 | ○ 基线 | ● 台账与房态 | ○ 查询 | CR-C05:`building` 补列 Q-20 待决 |
| 平板叫乘 / 顺序派发 | — | ● | ○ 查看 | CR-C08:设备侧拉取,不构成跨端接口 |
| 换乘 | ○ 接收变更 | ● 重算与联动 | ● 发起 | CR-C09:平台不产出权威 `jcsj` |
| 排班 / 盯控质量考核 | ○ 跨公寓汇总 | ○ 事实来源 | ● 排班与考核 | CR-C11:平台只汇总、不重算公寓明细 |
| 叫乘时刻 `jcsj` | — | ● 恒由公寓复算 | — | CR-C06(沿用 R-C9 / R-C10):`PL_TASK.jcsj` 仅为基线值 |
| 跨端接口 | ○ 20 个编号不新增 | ● 拉 + 推 | — | CR-C07:新数据走 `payload` / `bizType` 扩展 |
**三条边界不变**:不接入现场设备、不执行叫乘、不回调公寓(表 1-3 四条约束继续有效)。新增接收侧职责只落在 `platform.ingest`(`bizType` 分流与登记确认)、`platform.ledger`(离线额度与冲正)、`platform.report`(考核与上报历史汇总)三个模块内,模块清单见 CR-02 表 3-23;表 1-4 中"消费受理归公寓"与"余额主账在平台"两条按 CR-C03 合并理解,不再视为冲突。
> **CR-02 §14 补遗(消费 / 充值 / 食谱,权威表述 `docs/公寓端/12`)**:`platform.ledger` 仍是 `GY_YKT_REMAINDER` / `PL_LEDGER_FLOW` 的**唯一写入口**,按 #10 `LEDGER_OFFLINE` 逐条归日并依 `allowanceAmount` / `cashAmount` 分别冲减补贴 / 现金额度(充值只增现金),公寓端零写权限;`platform.catering` 承载阶段性食谱主数据(`YKT_MEALS` 补列)与 #4 `recipes[]` 下行,公寓只只读拉取。平台**不接线上支付、不产生支付订单与回调**(充值仅线下现金记账),**不实现远程点菜**(`features.remoteOrdering=false`)。`platform-ingest` 按 `idempotencyKey` 去重,`PF-5002` 业务重复与 `PL-IDEMPOTENT-REPLAY` 幂等重放分别处理;当日 `sum(amount)` 不等即 `PF-5003` 阻断调账,冲正只走 `POST /adjustments`。20 个编号、平台零回调、公寓本地库 59 张口径不变。
> **CR-02 §15 补遗(三端一致性 + 统计能力归属)**:四能力在平台端的职责按 **CR-02 §15 表 3-65 / 3-66** 收口——**平板叫乘**:`platform-ingest` 按 `idempotencyKey` 去重收 #9 `TABLET_RESULT` / #8 回执 → `PL_TASK_ACK` / `PL_RECEIVE_LOG`,**#24 `VOICE_SUMMARY` 仅 SIP 链路**,平台不生成任务行、不产出权威 `jcsj`(CR-C06);**人脸签到与注册**:`pl_person_reg_request` 回**同步回执** `ACCEPTED` / `REJECTED`、不做准入审批、平台驳回不决定公寓侧生效(CR-C25 / Q-35);**消费**:`platform-ledger` 唯一写主账,日对账 `DIFF` + `PF-5003` 阻断调账、冲正只走 `POST /adjustments`;**菜品管理**:`platform.catering` 写 `YKT_MEALS` 补列、#4 只维护当前有效版本、不实现远程点菜。新增 `platform-report` 的**公寓分类统计 / 月·季·年报表 / 公寓排名**能力(唯一权威 `docs/平台端/10`),数据源一律为平台库已归集表(`PL_REPORT_DAILY` / `PL_LEDGER_FLOW` / `PL_RECEIVE_LOG`),**不重算公寓明细、不回查公寓库、不产生跨端副作用**。权限点 `platform:stat:list` / `rank` / `generate` / `export` 四级,`gyIds[]` 由会话范围解析且 SQL 层带范围;异常 `PF-7003` 未封账 / `PF-7004` 越权 / `PF-7005` 修订冲突 / `PF-5003` 对账阻断,缺报 `NO_DATA` **不推断不补零**。跨端 20 编号、平台零回调、公寓 59 张表不变;平台库新增 2 张 `pl_apt_stat_*` 另计(累计 **5 张**),`platform-report` 是其唯一写入方。
## 1. 定位
平台端是**公寓叫乘体系里的“真源与规则中心”**:它保管“谁是谁、该干什么、钱怎么算、跨公寓看什么”,并把结果以**版本化快照**的形式交给公寓端执行;它不接触现场设备、不参与叫乘执行、不直连公寓库。
一句话边界:
> **平台端管“真源与规则”,公寓端管“资源与执行”,应用端管“人看与人工干预”。**
> 平台端可以决定“某个乘务员明天 06:12 应该被叫”,但不能决定“这通电话打给哪个分机、走哪条中继、录音存哪里”——那是公寓端的事。
图 1-1 平台端的逻辑边界与四个交互面
```mermaid
flowchart TB
subgraph PLAT["平台端(本篇范围)"]
M["主数据真源<br/>公寓 / 房间基线 / 人员 / 交路 / 规则"]
P["计划与任务<br/>运安计划 / 任务生成 / 变更"]
L["账务主账<br/>补助规则 / 余额 / 流水归集 / 对账"]
C["餐饮业务<br/>菜品 / 订单 / 后厨 / 结算"]
R["跨公寓报表<br/>统计 / 导出 / 执行质量"]
I["上报接收<br/>幂等 / 水位 / 重放 / 对账"]
S["账号与审计<br/>RBAC / 操作审计 / 接口审计"]
O["公寓注册与运维<br/>版本 / 健康度 / 配置下发版本"]
end
APT["公寓端(一公寓一实例)<br/>设备网关 / 叫乘引擎 / 语音域 / 本地库"]
DUTY["应用端(与公寓端同进程)<br/>值班盯控 / 人工干预"]
EXT1["运安 SOAP"]
EXT2["中心汇总 / 财务"]
EXT3["对象存储(人员照片 / 报表导出)"]
APT -->|"① 出站拉:主数据 / 计划 / 规则 / 语音配置"| PLAT
APT -->|"② 出站推:结果 / 流水 / 摘要 / 心跳"| PLAT
DUTY -.->|"同进程领域调用,不经平台"| APT
PLAT --> EXT1
PLAT --> EXT2
PLAT --> EXT3
APT -.->|"录音 / 抓拍不出公寓,仅上索引与计数"| PLAT
```
图中两条实线箭头**都由公寓端发起**:方向 ② 虽然语义是“平台接收”,但 HTTP 连接同样由公寓端建。平台端不存在任何指向公寓端的出站调用,这是 `docs/backend/10-架构优化方案-三端拆分.md` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行与 §2.2 拓扑要点第 1 条的硬约束。对象存储只承载**人员主数据**照片 / 特征值与报表导出;按 CR-C24 / `docs/公寓端/10` §1.1,人员主数据人脸允许平台 ↔ 公寓双向同步,媒体本体走短期签名直传、公寓库内只存引用与校验和;**业务过程抓拍、签到现场图与录音不出公寓**,平台只存索引与计数(`docs/10` 表 6-3 设计要点第 5 条、本目录 `07` 表 7-7、CR-02 §12)。
## 2. 负责 / 不负责
### 2.1 负责(对齐 `docs/10` §3.1,逐条给出落点模块)
| # | 职责 | 落点模块(`docs/10` 表 4-2) | 本期 | 说明 |
| --- | --- | --- | --- | --- |
| 1 | 公寓档案、房间基线(`gyid + roomNo + 床位数`)、人员主数据与照片、交路模板与叫乘时刻、时间规则、节假日、餐补 / 补助标准 | `platform.master`、`platform.person`、`platform.route` | 是 | 房间“物理属性”仍由公寓维护并上报,平台只存基线(D3) |
| 2 | 运安计划的**唯一入口**(替代各公寓分别抓取),并把计划转换为叫乘任务 / 计划快照下发对应公寓 | `platform.plan` | 是 | 同时解除“通辽南”硬编码导致的多公寓匹配障碍(D4) |
| 3 | 账务主账:补助发放规则、余额主账、消费 / 充值 / 补助流水归集、跨端对账 | `platform.ledger` | 是(归集与对账)/ 后续(补助规则引擎化) | 餐饮扣款必须经账务接口,禁止直改余额(D1) |
| 4 | 餐饮业务(菜品、订单、后厨、结算规则) | `platform.catering` | 后续 | 结算规则先接账务接口,页面迁移排后 |
| 5 | 账号、角色、权限、操作审计、跨公寓报表、设备在线率与执行质量监控、告警汇总 | `platform.iam`、`platform.report`、`platform.ops` | 是(IAM / ops)/ 后续(报表重建) | 与公寓端 RuoYi RBAC 通过“人员—账号映射”打通,不合并表 |
| 6 | 接收各公寓执行结果与异常补偿请求,维护每公寓水位与对账结果 | `platform.ingest` | 是 | 新增能力,现状只有 `GY_SYN_JCXX_STATUS` 这一雏形 |
| 7 | 语音侧的**规则与合规策略**(号段规则、TTS 模板、录音留存与导出策略)与质量摘要汇总 | `platform.master`(规则)+ `platform.ingest`(摘要) | 是 | 平台只给规则与看汇总,不持有分机台账、不代理 AMI(`docs/10` 表 3-2、表 6-3 要点第 5 条) |
### 2.2 不负责(对齐 `docs/10` §3.1)
1. **不接入现场设备**、不下发设备指令、不持有设备心跳流水(设备域全在公寓端 `apt.device.*`)。
2. **不执行叫乘**:不连 AMI、不控串口、不播 TTS、不判会议桥占用。
3. **不直连公寓本地库**,不做跨库 JOIN,不在平台库写“需要公寓实时数据才能算”的 SQL。
4. **不承担房间实时房态**:只保留基线,实时占用以公寓端 `gy_rooms` / `gy_jcxx` 为准。
5. **不代理语音媒体流**:录音与业务过程抓拍不出公寓、不上传平台对象存储(`docs/10` 表 6-3 设计要点第 5 条);**人员主数据照片与特征值不属媒体流**,按 CR-C24 / `docs/公寓端/10` §1.1 允许经 #2 / #9 同步与对象存储承载(CR-02 §12)。
6. **不回调公寓端**:不向公寓开放入站端口、不主动推送指令(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)。
## 3. 三端分工
表 1-1 三端职责分工(沿用 `docs/10` 表 3-1 口径,补写平台端列)
| 维度 | 平台端(本篇) | 公寓端(执行域) | 应用端(操作域) |
| --- | --- | --- | --- |
| 表归属 | 主数据真源 `GY_*`(owner=platform)、新增 `PL_*`、餐饮与报表表 | 执行与设备表 `gy_*` / `gygl_*`、语音 `apt_*`、上报 `apt_report_*` | `duty_*`(审计、人工干预、交接班) |
| 写权限 | 只写平台库;对公寓库**零权限** | 仅执行引擎 / 设备网关写状态字段 | 仅通过状态机服务写,禁止直连 Mapper 改状态 |
| 部署形态 | 无状态多实例 + Nginx + 平台主库 + Redis + 对象存储 | 单实例同进程(含应用端),本地库 + 本地 Redis + Asterisk | 同公寓端 JVM 内的 `apt-duty` |
| 前端形态 | **待决**(P-01):沿用现有管理页 or 新建 `platform` 入口 | 同一前端工程 `apt` 入口(静态路由) | 同一前端工程 `duty` 入口(`sys_menu` 驱动) |
| 鉴权 | 平台账号体系(`GL_*` 现状 → RuoYi `sys_*` 目标,P-04) | 设备用设备凭据;值班员用本地 RuoYi 账号 | 复用公寓端账号 + 数据范围锁定 `gyid` |
| 数据边界 | 通过 20 个跨端接口交换数据,无共享库 | 通过出站 HTTPS 拉取 / 推送 | 与公寓端同库,无跨端调用 |
| 谁决定叫乘时刻 | 给“计划 + 规则版本”(D8) | **本地复算**并回执 `computedCallTime` | 只能人工改写单条任务,不能改规则 |
表 1-2 语音链路上的三端分工(摘自 `docs/10` 表 3-2,只保留平台端参与的行,● 主责 ○ 参与 — 不参与)
| 环节 | 平台端 | 公寓端 | 应用端 | 平台端的具体动作 |
| --- | --- | --- | --- | --- |
| 号段与编号规则 | ● | ○ 取规则快照 | — | 维护“房间号 ↔ 分机号”规则并通过 #23 下发;**不创建分机** |
| 中继与路由策略 | ● 定策略 | ● 落地配置 | — | 给策略与合规约束,不碰 `pjsip.conf` |
| 分机资源增删改停 | — | ● | ○ 界面发起 | 不参与;仅在报表里看总量与漂移数 |
| 分机与房间 / 值班台 / 广播绑定 | — | ● | ○ 界面发起 | 不参与(绑定是本地事实) |
| 注册状态与在线判定 | ○ 汇总 | ● 采集 | ● 展示 | 只接收 #24 的 `regRate` 摘要 |
| 叫乘发起与会议桥 | — | ● | ○ 人工触发 | 不参与 |
| TTS 文本生成 | — | ● | ○ 单次改写 | 只下发模板(#23 的 `payload`),不生成文本 |
| 录音落盘与索引 | — | ● | ○ 回放 | 不接收录音文件,只收缺失数摘要 |
| 录音留存与访问控制 | ● 定策略 | ● 执行 | ○ 申请导出 | 给留存期与导出策略(D15 未定前按 90 天 / 仅内网 / 导出必审计) |
| 语音质量与故障 | ○ 汇总 | ● 判定 | ● 展示 | 只接收摘要并做跨公寓对比 |
| 外线呼入业务 | — | ● 观测 / 关联 | ○ 查看 | 不参与(拨号计划在本地) |
表 1-3 “平台不回调公寓”的四条边界约束(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)
| # | 约束 | 落地要求 | 违反后的故障形态 |
| --- | --- | --- | --- |
| 1 | 平台端不配置到公寓的路由 | 跨端仅允许“公寓 → 平台”出站 443;平台侧 Nginx 不写公寓 upstream | 公寓处在 NAT 后,回调必然超时,任务永久停在“待下发” |
| 2 | 下行一律“拉”,不是“推” | 8 个下行接口(#1~#7、#23)由公寓按周期拉取;平台只维护“当前有效版本” | 若改成推送,平台需保存公寓在线状态与重试队列,等于自建消息中间件 |
| 3 | 补偿接口方向待定(#16 / #17) | 本期平台只提供“公寓可主动调用”的形态;否则公寓走本地 outbox 重发 | 已登记为 P-02,未定前不得实现“平台发起对账” |
| 4 | 单公寓单实例约束不得被平台打破 | 平台不假设“可横向扩容的公寓端”,也不要求公寓端提供集群地址 | 平台若按多实例设计重试,会与公寓端进程内 WebSocket / Quartz 冲突 |
## 4. 能力归属
表 1-4 能力归属(`docs/10` 表 4-1 的**平台端切片**,● 主责 ○ 参与 / 缓存 — 不参与)
| 能力 | 平台端 | 平台端的具体职责边界 |
| --- | --- | --- |
| 公寓 / 房间基线 | ● | 存 `gyid + roomNo + 床位数` 基线;楼层 / 朝向 / SIP 由公寓维护后上报,回写前不覆盖本地值 |
| 人员主数据 / 照片 | ● | 全量真源 + 照片版本号;照片本体存对象存储,公寓按版本拉取 |
| 交路模板 / 叫乘时刻 | ● | 模板与叫乘 / 复叫 / 离寓时刻的真源;公寓可上报“现场临时交路”,平台确认后版本化(D7) |
| 时间规则 / 节假日 / 餐补 | ● | 按 `gyid` 差异化,必须版本化下发;规则变更不得影响已下发任务的复算结果 |
| 运安计划接入 | ● | 唯一接入方,持有 WSDL;公寓端不再连运安 |
| 任务生成(计划 → 叫乘时刻) | ●(给规则)/ ○(复算) | 平台给“计划 + 规则版本”,公寓复算;差异以平台为准并告警(D8) |
| 叫乘任务执行 | — | 完全不参与 |
| 叫乘状态机 | — | 只在接收侧记录最终状态,不改写 |
| 入寓 / 离寓 / 换房 / 房态 | — | 不参与;仅从上报中统计在寓人数用于报表 |
| 设备接入 / 指令 / 心跳 | — | 只看摘要(在线率、版本分布、故障数) |
| 分机与中继资源 | ○ 规则快照 | 只定号段规则;台账、绑定、收敛、对账全在公寓端 |
| 语音事件与录音 | ○ 摘要 | 只收质量摘要;不接收事件明细与录音 |
| 补助规则与发放 | ● | 规则在平台;**发放动作**由公寓在入住时触发并上报(平台记账,不反向命令公寓) |
| 余额主账 | ● | 平台主账权威;公寓缓存带“版本 + 最后对账日期” |
| 消费受理(消费机 / 售货机) | — | 现场必须离线可用,归公寓端 |
| 餐饮订单 / 后厨 | ● | 业务在平台;公寓端只做终端接入与转发 |
| 报警与告警 | ○ 汇总 | 判定阈值在公寓端,平台只收摘要并做跨公寓趋势 |
| 归档 | — | 公寓本地归档;平台只收归档摘要(数量与区间) |
| 统计报表 | ○ 跨公寓 / ●(平台报表域) | 公寓出运行报表,平台出跨公寓报表;平台不重算公寓明细 |
| 账号 / RBAC / 审计 | ● 平台侧 | 两套 RBAC 通过“人员—账号映射”打通,不合并表 |
| 库存 `kcgl` | — | 独立处置,不进入公寓端也不纳入本期平台范围(D10) |
## 5. 模块划分
表 1-5 平台端 10 个模块(对齐 `docs/10` 表 4-2,补“边界说明”列)
| 模块 | 职责 | 现状来源 | 边界说明(不做的事) |
| --- | --- | --- | --- |
| `platform.master` | 公寓、房间基线、工种、时间规则、节假日、餐补标准 | `zdwh` | 不维护房间实时房态;不维护分机台账 |
| `platform.person` | 人员主数据、照片、增量分发 | `regist` + `syndata` 人员部分 | 不维护人员在公寓的本地启停(G-02 兜底走公寓审计) |
| `platform.route` | 交路模板、叫乘时刻、停运调整、Excel 上传 | `jlgl` | 不做交路匹配(匹配是公寓端复算职责) |
| `platform.plan` | 运安 SOAP 接入、计划快照、任务生成与下发 | `ByxxResourceService` + `SynDataSchedule` | 不执行叫乘;不做会议桥占用判断 |
| `platform.ledger` | 补助规则、余额主账、流水归集、对账 | `recharge` / `pay` 账务部分 | 不发起消费扣款(受理在公寓端) |
| `platform.catering` | 菜品、订单、后厨、结算 | `MealsOrdering` | 不直接改余额,必须走 `platform.ledger` 接口 |
| `platform.report` | 跨公寓报表、导出、执行质量 | `xxtj` + `excel` | 不重算公寓明细,只聚合已上报数据 |
| `platform.ingest` | 上报接收、幂等、水位、重放、对账 | 新增 | 不主动拉取公寓数据(除 #16 / #17 待定形态) |
| `platform.iam` | 账号、角色、权限、审计 | `display` | 不接管公寓端本地账号(两套 RBAC 并存) |
| `platform.ops` | 公寓注册、版本、健康度、配置下发版本 | 新增 | 不向公寓下发“命令”,只维护“当前有效版本” |
模块之间的三条硬规则:
1. **`platform.ingest` 是唯一入口**:任何来自公寓的数据都必须经 ingest 落库,其他模块只读它落下的结果,不得各自解析上行报文。
2. **`platform.ledger` 是余额唯一写入口**:`platform.catering`、`platform.master`(补助规则)都只能调用 ledger 的服务,不得直连余额表。
3. **`platform.plan` 不写主数据**:生成任务所需的规则与交路只能读 `master` / `route` 的已发布版本,避免“任务用的是草稿规则”。
## 6. 本期范围与不做清单
表 1-6 本期范围(阶段划分沿用 `docs/10` §9 表 9-1~表 9-6,共阶段零至阶段五)
| 类别 | 内容 | 理由 | 对应决策 / 依据 |
| --- | --- | --- | --- |
| **本期做** | 20 个跨端接口的 v1 契约冻结 + Mock 服务(下行 8 个、上行 8 个、补偿 2 个、平台自答 2 个) | 契约不冻结,公寓端阶段二无法开工 | `docs/10` 表 9-3、`docs/公寓端/09` §7 |
| **本期做** | `platform.master` / `person` / `route` 的服务化拆分与版本下发(含 `version` 单调性,见 P-06) | 主数据是全部下行接口的前提 | 表 4-2、`docs/公寓端/09` F-13 |
| **本期做** | `platform.plan`:运安接入上收 + 计划快照 + 任务生成 + 变更(`UPSERT` / `CANCEL`) | 现状“每公寓各自抓运安”是多公寓推广的硬障碍 | D4、`docs/公寓端/09` 表 9-9 |
| **本期做** | `platform.ingest`:幂等接收、水位、对账、重放(按幂等键 / 区间) | 上行链路的正确性全部依赖它 | 表 6-2、`docs/公寓端/09` 表 9-14 |
| **本期做** | `platform.ledger`:流水归集 + 余额主账 + 日对账 | 现状“餐饮直改余额”是最高风险项 | D1 / D2、`docs/10` 表 10-1 |
| **本期做** | `platform.ops`:公寓注册、版本、健康度(承接 #14 心跳与 #15 查询) | #15 由平台自答,必须有健康度数据 | 表 6-1 #14 / #15 |
| **后续阶段** | `platform.catering` 全量迁移(桌面端页面 + 结算规则引擎) | 牵动现场点餐终端,与叫乘试点并行风险高 | D1、`docs/10` 表 9-6 |
| **后续阶段** | `platform.report` 重建(跨公寓统计与数仓化,替代 `xxtj` / `excel` 直连公寓库) | 依赖上行数据积累,至少一个结算周期后才准 | `docs/10` 表 9-6 |
| **后续阶段** | `platform.iam` 的目标态(平台 `GL_*` → RuoYi `sys_*`),与公寓端账号映射 | 与平台端是否改 MySQL(D13)强相关,不宜同期 | D13、P-04 |
| **后续阶段** | 平台端独立前端入口(若 P-01 判定需要) | 本期先用现有管理页 + 新接口并行 | P-01 |
| **不做** | 库存 `kcgl` 纳入平台业务域 | 与叫乘主线弱相关,避免污染平台模型 | D10 |
| **不做** | 平台代理语音媒体 / 建立平台侧 AMI 中继 | 违反“语音本地闭环”与“不带媒体”两条约束 | `docs/10` §2.4 硬约束 3、表 6-3 要点 5 |
| **不做** | 平台直连公寓库或跨库 JOIN | 破坏“一公寓一库”隔离 | `docs/10` §5.3 第 4 条 |
| **不做** | 平台回调公寓(含“平台推配置”“平台触发对账”) | 与 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行冲突;#16 / #17 待 P-02 定 | `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行、本目录表 1-3 |
| **不做** | 保留 `/jcgl/yatest`、`/jcgl/driver/{id}`、`/test` 等测试入口 | 生产暴露外部系统调用入口,属安全红线 | `docs/10` 表 1-1、§11.2 P0-2 |
@@ -0,0 +1,194 @@
# 02 平台端工程结构与技术基线
> 追溯:`docs/10` §2.2 图 2-2 与拓扑要点、§2.3 表 2-1(三端调用关系)、§2.4 表 2-2(技术基线)、
> §2.5 表 2-3(复用维度与实现落点)、§4.2 表 4-2(平台端模块清单)、§8.1 表 8-1(部署形态)、§9 阶段零。
> 现状来源:`docs/01` §3~§4(运行形态与技术栈)、`docs/02` §1~§4(工程结构、分层、配置)、
> `docs/09` §1 P0 与 §3(迭代顺序)、`docs/frontend/03`(路由与页面职责)。
> **CR-02 §15 补遗(worker 任务清单与技术基线不变)**:`platform-worker` **单活 + 库级协调锁**不变,定时任务清单追加:① 四能力上行归集(#9 / #10 / #12 / #13)的接收侧补偿与水位收敛;② 每日 `PL_REPORT_DAILY` 生成;③ 月 / 季 / 年 `pl_apt_stat_period` rollup 与 `pl_apt_stat_rank` 排名快照(`docs/平台端/10` 表 10-5 / 10-15);④ 次日修订重算(`revision_no+1`);⑤ 异步报表导出。任务进度一律落既有 `PL_JOB_*`,**不引入消息中间件、不新增模块、不改 §2 技术基线锁定值**;生成 > 10 min 按 `gyid` 拆批(`docs/平台端/08` §4)。统计与四能力不新增跨端调用、不新增 `bizType`;`platform-report` 是 `pl_apt_stat_*` 唯一写入方、`platform-ledger` 仍是余额主账唯一写者;三端状态映射以 CR-02 §15 表 3-66 为唯一权威。
## 1. 运行单元与服务形态
平台端是**一个可多实例部署的 Web 服务 + 一个主库 + 对象存储**,没有设备侧入口、没有 AMI 客户端、没有本地媒体目录。
图 2-1 平台端运行单元与外部接口
```mermaid
flowchart TB
subgraph EDGE["接入层(中心机房)"]
LB["Nginx / 网关<br/>TLS 终止 + 路径分区"]
end
subgraph APP["应用层(无状态,2 实例起步)"]
PA["platform-api-1<br/>/platform/v1/** + /open/v1/**"]
PB["platform-api-2"]
WK["platform-worker<br/>计划抓取 / 报表加工 / 对账 / 重放"]
end
subgraph STORE["存储层"]
DB[("平台主库<br/>KingbaseES(D13:短期 A)")]
RD[("Redis<br/>会话 / 幂等短缓存 / 限流")]
OS[("对象存储<br/>人员照片 / 报表导出")]
end
subgraph EXT["外部系统"]
BYXX["机务运安<br/>SOAP / PbResource.wsdl"]
FIN["中心汇总 / 财务<br/>HTTPS"]
end
APT["公寓端实例(每公寓 1 个)<br/>只做出站调用"]
APT -->|"出站 443:拉主数据 / 推结果"| LB
LB --> PA
LB --> PB
PA --- DB
PB --- DB
PA --- RD
PB --- RD
PA --- OS
WK --- DB
WK -->|"定时抓取计划"| BYXX
WK -->|"定时汇总 / 按需查询"| FIN
PA -.->|"反向不可达"| APT
```
三条形态约束(与 `docs/10` §2.2 拓扑要点、表 8-1 一致):
1. **平台端只被调用,不主动调用公寓端**:图中 `PA -.-> APT` 用虚线标注“反向不可达”,平台侧 Nginx 不配置任何公寓 upstream。所有跨端流量都由公寓端建连。
2. **应用层无状态**:会话、幂等短缓存、限流计数落 Redis;任务进度落主库 `PL_JOB_*` 类表。因此可横向扩容、可滚动发布。
3. **平台端不承载任何现场媒体**:对象存储只放“人员照片、报表导出”两类;录音、离寓抓拍与 AMI 事件**永不进入平台**,抓拍与录音只在公寓本地留存、平台只存索引与计数(`docs/10` 表 6-3 设计要点第 5 条、本目录 `07` 表 7-7)。
## 2. 技术基线
表 2-1 平台端技术基线(数据库项按 D13 的“短期 A”展开,其余为本文约束)
| 项 | 平台端基线 | 依据 / 说明 | 与现状的关系 | 状态 |
| --- | --- | --- | --- | --- |
| 运行时 | JDK 1.8、Spring Boot 2.5.x | 与公寓端 RuoYi-Vue 3.8.9 的框架世代对齐,便于同一套依赖治理与人员复用 | 现状 `yktgl` 为单模块 Boot 2.6.13;`pom.xml` 存在 `spring-boot-starter-web:3.5.4` 等版本错配,属阶段零必修项(`docs/09` §1 P0) | 确定 |
| 应用框架 | **待决(P-04)**:RuoYi-Vue 多模块 / 沿用 `yktgl` 单模块演进 | 若选 RuoYi,则按 `docs/10` 表 2-2 口径:只加业务模块、不改框架鉴权链、**不引入 `ruoyi-generator`**(差-3) | 现状 `yktgl` 已有 `common` / `config` / `display` 等雏形,可直接演进 | 待决 |
| 持久层 | MyBatis(XML)+ 分页插件;存量 MyBatis-Plus 仅过渡 | 与表 2-2 一致;新增模块不引入第二套 ORM | 现状 `yktgl` 为 MyBatis-Plus 3.1.0 + XML 混用 | 确定 |
| 数据库 | **按选项 A 展开**:平台端保持现有 KingbaseES;如后续改 MySQL(D13 选 B / C)按表 5-8 前置条件评估 | `docs/10` §5.6.3 表 5-8 推荐 A 作为第一阶段;本文只给**方言无关的逻辑类型族**(见 `03` 表 3-16) | 现库 `V009R001C010`(Oracle 兼容模式) | 本期确定(A);中期待定(D13) |
| 字符集与排序 | 沿用现库;若改 MySQL 则全库 `utf8mb4_general_ci`,**不逐列指定 `_ai_ci` / `_as_cs`**(差-1) | `docs/10` 表 5-4「字符集与排序」行、表 5-9 第 7 项;理由是与 `sys_*` JOIN 时避免 `Illegal mix of collations` | 现库为库级 UTF8 | 确定(条件生效) |
| 缓存 / 会话 | Redis:会话、令牌、`Idempotency-Key` 短缓存、限流计数 | 平台多实例必须有共享态 | 现状已有 Redis(`spring-session-data-redis`) | 确定 |
| 调度 | 独立 `platform-worker` 进程或同包的 `@Scheduled` + 库级任务表 | **不引入消息中间件**(与 `docs/10` 表 2-2 “部署不引入注册中心/消息中间件”一致);需要“至少一次”的语义用“库表 + 状态机 + 重试”实现 | 现状 `SynDataSchedule` / `JcSchedule` 为同进程 `@Scheduled` | 确定 |
| 跨端协议 | HTTPS + JSON;`/platform/v1/**` 走服务令牌或 mTLS;`/open/v1/**` 走双向令牌 | `docs/公寓端/09` 表 9-2、表 9-19 | 现状为内网 HTTP 明文调用 | 确定 |
| 鉴权 | 平台自有账号体系;现状 `GL_*`,目标 RuoYi `sys_*`(与 P-04 绑定) | `docs/10` 表 4-1「账号/RBAC/审计」行:两套 RBAC 用“人员—账号映射”打通,**不合并表** | 现状 `display` + `GL_*` 自研 RBAC | 待决(P-04) |
| 对象存储 | S3 兼容或 NFS 挂载,二者择一;**只放两类文件**:人员照片、报表导出 | `docs/10` 表 5-1「平台主数据」行:照片在对象存储;表 6-3 要点 5:录音不上平台;桶划分见 `07` 表 7-7 | 现状照片以 Base64 存 `GY_YKT_RY_ZP`,属反模式 | 确定(载体待定) |
| 前端 | **待决(P-01)**:复用现有 `YktVue` 管理页 / 新建 `platform` 入口 / 本期只出 API | 见 §6;若新建入口,按差-7 口径:`platform` 入口用**前端静态路由**,`sys_menu` 仅作权限点载体 | 现状 `YktVue` 无平台管理页(`docs/frontend/03` 13 条静态路由中无 `platform` 分组) | 待决 |
| 报文格式 | JSON;时间 ISO-8601 带偏移;金额字符串两位小数;枚举大写下划线 | `docs/公寓端/09` 表 9-3(与表 5-4、表 5-9 第 5 项对齐) | 现状时间全链路字符串 + `TO_DATE` | 确定 |
| 部署 | 单 jar + Nginx + 主库 + Redis + 对象存储;对外仅 443 | `docs/10` 表 8-1「平台端」行 | — | 确定 |
基线的三条硬约束:
1. **不为了平台端改造公寓端框架**:平台端与公寓端只通过 20 个跨端接口耦合,两边可以各自选框架世代(P-04 未定时尤其如此)。
2. **平台端不为“跨库查询”开口子**:需要公寓数据时一律经 `platform.ingest` 的落库结果读,不建到公寓库的数据源(`docs/10` §5.3 第 4 条)。
3. **平台端的表数口径与公寓端无关**:`docs/10` 表 5-9 第 1 项的“44 张业务表”是**公寓端本地库**口径(`APT_*` 23 + `GY_*`/`gygl_*` 18 + `duty_*` 3),不得用来描述平台库;平台库表清单见 `03` 表 3-4~3-9。
## 3. Maven 模块与包结构
平台端按 `docs/10` 表 4-2 的 10 个模块落成 Maven 模块,模块名即包前缀,编译期即可阻止“跨模块乱调”。
表 2-2 平台端模块、制品与职责
| 模块 | 制品形态 | 职责 | 允许依赖 | 现状来源 |
| --- | --- | --- | --- | --- |
| `platform-common` | jar | 统一返回体、错误码、时间/金额工具、`gyId` 上下文、脱敏 | 无(只依赖三方库) | `yktgl/common`、`util` |
| `platform-framework` | jar | 拦截器链(鉴权、`gyId` 校验、幂等键解析、审计落库)、异常处理、分页 | `platform-common` | `yktgl/config`、`display` 的鉴权部分 |
| `platform-master` | jar | 公寓、房间基线、工种、时间规则、节假日、餐补标准 | `common`、`framework` | `zdwh` |
| `platform-person` | jar | 人员主数据、照片版本、增量分发 | `common`、`framework`、`master` | `regist` + `syndata` 人员部分 |
| `platform-route` | jar | 交路模板、叫乘时刻、停运调整、Excel 上传 | `common`、`framework`、`master` | `jlgl` |
| `platform-plan` | jar | 运安 SOAP 接入、计划快照、任务生成、变更与取消 | `common`、`framework`、`master`、`route`、`person` | `ByxxResourceService`、`SynDataSchedule` |
| `platform-ledger` | jar | 补助规则、余额主账、流水归集、日对账 | `common`、`framework`、`person` | `recharge`、`pay` 账务部分 |
| `platform-catering` | jar | 菜品、订单、后厨、结算 | `common`、`framework`、`ledger`(**只能调服务,不碰余额表**) | `MealsOrdering` |
| `platform-ingest` | jar | 上报接收、幂等、水位、对账、重放 | `common`、`framework`、`ledger`、`plan` | 新增(现状只有 `GY_SYN_JCXX_STATUS` 雏形) |
| `platform-report` | jar | 跨公寓报表、导出、执行质量 | `common`、`framework`、`ingest`(读落库结果) | `xxtj`、`excel` |
| `platform-iam` | jar | 账号、角色、权限、操作审计、接口审计 | `common`、`framework` | `display` |
| `platform-ops` | jar | 公寓注册、版本、健康度、配置下发版本 | `common`、`framework`、`master` | 新增 |
| `platform-admin` | **唯一可执行 jar** | 启动类、`application-*.yml`、路由装配、Swagger | 全部业务模块 | `yktgl` 启动类 |
| `platform-worker`(可同 jar 分 profile 启动) | 可执行 jar | 计划抓取、报表加工、对账、重放、留存清理 | 同 `platform-admin` | `yktgl/schedule` |
表 2-3 包内分层与命名约定
| 层 | 包路径 | 职责 | 禁止 |
| --- | --- | --- | --- |
| 接口层 | `...platform.<domain>.controller` | 协议编解码、鉴权注解、参数校验 | 写业务分支、直接调 Mapper |
| 应用层 | `...platform.<domain>.service` | 编排、事务、状态校验、幂等入口 | 跨域直接改对方表 |
| 领域层 | `...platform.<domain>.domain` | 聚合、状态机、规则计算(纯函数优先) | 依赖 Spring Web |
| 持久层 | `...platform.<domain>.mapper` + `resources/mapper/<domain>/*.xml` | SQL | 在 XML 里写业务判断 |
| 契约层 | `...platform.<domain>.dto` | 请求 / 响应 / 上行信封对象 | 直接复用实体做对外契约 |
约定三条:
1. **实体不做跨模块共享**:`platform-plan` 需要人员信息时调 `platform-person` 的服务,不 import 对方 `entity`。
2. **对外 DTO 与库表解耦**:跨端契约字段名以 `05` 为准,允许与库列名不同(如 `planId` ↔ `plan_key`)。
3. **包前缀与模块一一对应**,Code Review 以“包路径是否越界”作为第一道检查。
## 4. 依赖方向
表 2-4 模块依赖矩阵(→ 表示“允许依赖”,— 表示禁止)
| 模块 | master | person | route | plan | ledger | catering | ingest | report | iam | ops |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| master | — | — | — | — | — | — | — | — | — | — |
| person | → | — | — | — | — | — | — | — | — | — |
| route | → | — | — | — | — | — | — | — | — | — |
| plan | → | → | → | — | — | — | — | — | — | — |
| ledger | — | → | — | — | — | — | — | — | — | — |
| catering | — | — | — | — | → | — | — | — | — | — |
| ingest | — | — | — | → | → | — | — | — | — | — |
| report | — | — | — | — | — | — | → | — | — | — |
| iam | — | — | — | — | — | — | — | — | — | — |
| ops | → | — | — | — | — | — | — | — | — | — |
矩阵背后的三条业务规则(与 `01` §5 一致,此处给出工程落点):
1. **`platform-ingest` 是上行唯一入口**:只有它可以解析公寓上行报文;`plan` / `ledger` 通过它调用以读取“已落库的上报结果”,本身不实现 HTTP 接收。
2. **`platform-ledger` 是余额唯一写入口**:`catering` 只允许依赖 `ledger` 的 `LedgerService`,不允许出现 `UPDATE GY_YKT_REMAINDER`。
3. **`platform-plan` 只读 `master` / `route` 的已发布版本**:生成任务时不得读取“草稿态”规则,避免任务与发布版本脱钩。
## 5. 与 `yktgl` 的收敛映射
平台端不是新建一切,而是把 `yktgl` 里的“中心能力”搬正、把“现场能力”留下去。
表 2-5 `yktgl` 包 → 平台端模块 → 处置
| `yktgl` 包 | 目标归属 | 处置 | 阶段 | 备注 |
| --- | --- | --- | --- | --- |
| `zdwh`(公寓字典、房间、工种、节假日、餐补) | `platform-master` / 公寓端 `apt.room` | 拆 | 阶段五 | 房间物理属性留公寓,基线归平台(D3) |
| `regist` | `platform-person` | 迁 | 阶段五 | 改为“本地写 + 上报中心” |
| `jlgl` | `platform-route` | 迁 | 阶段五 | 从“中心拉取”改为标准下行 #3 |
| `syndata`、`ByxxResourceService`、`SynDataSchedule` | `platform-plan` | 迁 | 阶段五 | 公寓端不再持有 WSDL(D4) |
| `recharge` / `pay` | `platform-ledger`(主账)+ 公寓端 `apt.ledger.local`(缓存) | 拆 | 阶段五 | 余额主账平台,离线消费本地 |
| `MealsOrdering` | `platform-catering` | 迁 | 后续阶段 | 结算改走账务接口(D1) |
| `xxtj`、`excel` | `platform-report` | 迁 | 后续阶段 | 依赖上行数据积累一个结算周期 |
| `display` | `platform-iam` / 公寓端 `apt-system` | 拆 | 后续阶段 | 两套 RBAC 并存,用人员—账号映射打通 |
| `jcgl`、`quartz`、`heartBeat`、各 `*Device`、`clean`、`websockets` | 公寓端 | **留** | — | 平台端一行都不迁 |
| `kcgl` | 不进入平台 | 冻结 | — | D10:独立处置 |
| `test`、`/jcgl/yatest`、`/jcgl/driver/{id}` | 无 | 下线 | 阶段零 | 生产暴露外部系统调用入口,属安全红线(`docs/10` 表 1-1、§11.2 P0-2) |
| `common` / `config` / `util` | 两侧各留一份 | 复制 | 阶段二 | 不共享 jar,避免版本互相绑架 |
**迁移期的兼容要求**:新旧通道并行期间,平台端新接口必须保留旧路径的兼容适配层(`docs/10` §6.1 末段),不得直接改旧签名;旧接口调用量归零以网关日志为准(`docs/10` 表 9-6 验收标准)。
## 6. 平台端前端归属
现状事实:`YktVue` 的 13 条静态路由(`docs/frontend/03`)里没有任何“平台管理”分组,也没有跨公寓聚合页;现有页面全部围绕单公寓的值班与终端场景。
表 2-6 平台端前端归属的三种方案(P-01 待决,本文不预定)
| 方案 | 做法 | 优点 | 代价 / 风险 | 适用判断 |
| --- | --- | --- | --- | --- |
| A 复用现有 `YktVue` 管理页 | 在现有工程新增 `platform` 路由分组与页面,后端切换到 `/platform/v1/**` | 零新增工程;值班员与管理员同一个 URL | 现有工程是**单公寓语境**(`localStorage.gyid` 兜底、无公寓切换器),跨公寓页要额外加公寓选择器;两类用户会话语义混在一起 | 仅在平台功能以“单公寓配置维护”为主时成立 |
| B 新建独立 `platform` 前端工程 | 独立仓库或独立目录,独立构建、独立发布域名 | 与公寓端解耦,可独立灰度;跨公寓视角天然成立 | 多一套构建与发布;共用组件需复制或抽包 | 平台要出“跨公寓监控/报表”时基本必选 |
| C 本期只出 API,不出前端 | 平台能力先由 DBA/运维脚本与 Postman 使用 | 最省人力,最快冻结契约 | 无管理入口,配置类数据靠脚本维护,长期不可持续 | 仅适合阶段二~阶段三的过渡期 |
差-7 口径的适用(无论选 A 还是 B):`platform` 入口若沿用 RuoYi 菜单体系,**路由用前端静态路由表声明**,`sys_menu` 只作权限点载体,不参与路由生成;按钮级一律后端校验,前端隐藏不等于放行(`docs/10` 表 2-3「前端路由」行)。
## 7. CI 与质量基线
阶段零必须先建立可重复的构建,否则“拆分正确性”无从验收(`docs/10` 表 9-1、表 11-1 P0-1)。平台端的门禁清单:
1. **编译门禁**:`mvn -q clean verify` 必须通过;依赖版本由父 POM 统一管理,禁止子模块显式写 `spring-boot-starter-*` 版本(现状错配的根因)。
2. **方言门禁**:平台库若保持 KingbaseES,则 SQL 里禁止出现 MySQL 专有函数;反之亦然。用“两边各跑一遍关键 SQL”的矩阵测试兜底(`docs/10` 表 10-3「双库方言」行)。
3. **越权门禁**:每条跨端写接口必须有 `gyId` 参数与 `X-Apt-GyId` 一致性校验用例;缺一条即视为缺陷(`docs/10` §3.3「公寓隔离方式」)。
4. **幂等门禁**:上行接口的重复投递、乱序、断网续传三类用例必须常驻回归(`docs/10` 表 9-3 验收标准)。
5. **契约门禁**:20 个跨端接口的请求样例 → 落库断言各 1 条,契约变更必须同时改样例与断言(`docs/公寓端/09` §7 Mock 与联调)。
6. **安全门禁**:仓库不出现明文口令与内网地址;日志不打印身份证、手机号全量、对象存储签名地址(`docs/公寓端/09` 表 9-19)。
+384
View File
@@ -0,0 +1,384 @@
# 03 平台端数据设计
> 追溯:`docs/10` §5.1 表 5-1(数据分层、归属与同步方向)、§5.2 表 5-2(各场景一致性策略)、
> §5.3(同库边界防混乱 6 条)、§5.6.3 表 5-8(平台端数据库三选项)、表 5-9(切换前必查清单)、
> §6.2(上报信封与三时间戳)、§6.3 表 6-2(幂等 / 重试 / 对账 / 断网)、§3.3(公寓隔离方式)。
> 现状来源:`docs/05` 数据模型(表名与状态字典)、`docs/11` 既有数据库结构分析、`docs/07` §2 运安字段映射。
> 对端依据:`docs/公寓端/03` 表 3-8(44 张表命名分区)、表 3-9(状态字段单写入口)。
本文只给**表清单、归属、约束与状态口径**,不提供 DDL;建表语句在阶段二随契约冻结产出(`docs/10` §5.4 末段口径)。
> **v2 修订(CR-02,2026-11-09)**:新增 3 张表与 `PL_TASK.jcsj` 口径以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §6 为权威,见下文 §0。
## 0. v2 数据口径修订(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 3-y` 序号;§1~§10 原有表清单与方言口径继续有效。
表 C-1 平台库新增 3 张表(列级定义见 CR-02 表 3-31)
| 表 | 唯一写入模块 | 用途 | 口径来源 |
| --- | --- | --- | --- |
| `pl_person_reg_request` | `platform-ingest` | **接收注册成功后的主档同步结果并回同步回执**(#2 下行 `personRegVerdicts[]`:`ACCEPTED` / `REJECTED`);不做注册准入审批(CR-C25) | A-02、CR-C25、§12 |
| `pl_offline_quota` | `platform-ledger` | 离线充值 / 扣补贴额度批次,#1 下发 `offlineQuota` | A-04、CR-C03 |
| `pl_staff_performance_daily` | `platform-report` | 跨公寓盯控质量考核汇总,只接收不重算明细 | A-13、CR-C11 |
表 C-2 两处口径改判
| 项 | 旧口径 | v2 口径 | 裁决 |
| --- | --- | --- | --- |
| `PL_TASK.jcsj` | "平台计算值"(表 3-5);图 6-2"按 `rule_version` 计算应叫时刻" | 平台只存"按匹配要素派生的**基线** `jcsj`",**不是权威应叫时刻**;权威值恒由公寓复算并随 #8 回执 `computedCallTime` 上报 | CR-C06(R-C9 / R-C10) |
| "44 张业务表"引用 | 本目录 README 表数量行引用公寓端 44 张 | 44 是**公寓本地库**口径;CR-02 后为 58 张、§10 补遗后为 **59 张**(+ `apt_sip_reg_status`),与平台库无关;平台库新增 3 张另计 | CR-C14、CR-02 §10 |
平台新增表按本文表 3-16 的**逻辑类型族**给出(`id_ulid` / `decimal(12,2)` / `ts_ms` / `enum_s`),D13 若选 B / C 迁 MySQL 只换物理类型、不改语义;全部带主键、`gyid` 打头的唯一键与接收时间列,归日仍以 `serverReceivedAt` 为准。
> **CR-02 §12 补遗**:`pl_person_reg_request` 补 `photo_ref` / `feature_ref` / `photo_checksum` / `feature_checksum` / `feature_algo` / `feature_version` / `person_category` 列(设计定义,DDL 待阶段二契约冻结)。人员主数据照片与特征值按 **CR-C24** 允许跨端、媒体本体落平台对象存储,平台库只存引用与校验和;业务抓拍、签到现场图与录音仍不入库。注册成功后由 #9 送达、平台回**同步回执**(CR-C25),不做注册准入审批。本补遗**不新增平台表**,公寓本地库仍 59 张。
> **CR-02 §14 补遗(消费 / 充值 / 食谱)**:`YKT_MEALS` 补 `recipe_id` / `recipe_version` / `meal_period` / `effective_from` / `effective_to` / `publish_state`(`DRAFT` / `PUBLISHED` / `EXPIRED` / `REVOKED`)/ `published_by` / `published_at` / `category` / `sort_no` / `image_ref`(列级定义见 `docs/公寓端/12` 表 12-10,均为设计定义,DDL 待阶段二契约冻结,Q-43);`GY_YKT_REMAINDER` 仍是**唯一余额表**、`PL_LEDGER_FLOW` 承接归集流水,消费按 `allowanceAmount` / `cashAmount` 分别冲减 `knye` / `xjye`、充值只增 `xjye`。`YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 本期**不写入**(远程点菜仅预留,Q-44)。本补遗**不新增平台表**,公寓本地库仍 59 张,余额主账唯一写入口 `platform-ledger` 不变。
> **CR-02 §15 补遗(统计新表与补列)**:平台库**新增 2 张表(另计,累计 3 + 2 = 5 张)**:`pl_apt_stat_period`(`platform-report` 唯一写;UK `(gyid, period_type, period_key, metric_code, revision_no)`;`state` `DRAFT` / `FINAL` / `REVISED`、`data_status` `OK` / `NO_DATA` / `BLOCKED`)、`pl_apt_stat_rank`(UK `(gyid, period_type, period_key, rank_type, revision_no)`;`RANK()` 跳号快照)。任务进度复用 `PL_JOB_*`、导出复用 `report-export` 桶,不再增表。既有表补列:`PL_REPORT_DAILY` 增 `satisfaction_good/neutral/bad/total`、`alarm_call/leave/device`、`room_available` / `room_nights`、`source_version` / `revision_no`;`PL_LEDGER_FLOW` 增 `allowance_amount` / `cash_amount` / `recharge_channel`(承接 §14 #10 payload 拆分)。列级定义见 `docs/平台端/10` 表 10-7 / 10-8 / 10-10,均为**设计定义,DDL 待阶段二契约冻结**(Q-48)。表 C-1 / C-2「平台库新增 3 张另计」按本补遗理解为**累计 5 张**;公寓本地库仍 **59 张**,`platform-ledger` 仍是余额主账唯一写者,统计模块对账务表**只读**,统计结果不回写任何业务状态。
表 C-3 平台库新增表累计(CR-02 §15)
| 批次 | 新增表 | 唯一写入模块 | 累计 |
| --- | --- | --- | --- |
| v2(CR-02 §6,表 C-1) | `pl_person_reg_request` / `pl_offline_quota` / `pl_staff_performance_daily` | `platform-ingest` / `platform-ledger` / `platform-report` | 3 |
| §15(本补遗) | `pl_apt_stat_period` / `pl_apt_stat_rank` | `platform-report` | **5** |
## 1. 数据分层与归属
图 3-1 平台端六个数据域与流转方向
```mermaid
flowchart LR
subgraph IN["入站(外部 → 平台)"]
SOAP["运安 SOAP<br/>乘务计划"]
FIN["中心汇总 / 财务<br/>人员与账务汇总"]
UP["公寓上行<br/>#8~#14、#24"]
end
subgraph PLATDB["平台主库(owner=platform)"]
D1["主数据域<br/>公寓 / 房间基线 / 人员 / 交路 / 规则"]
D2["计划与任务域<br/>计划快照 / 任务 / 变更 / 回执"]
D3["账务域<br/>余额主账 / 归集流水 / 补助规则"]
D4["餐饮域<br/>菜品 / 订单 / 结算"]
D5["汇总统筹域<br/>水位 / 幂等接收 / 对账 / 报表"]
D6["账号与运维域<br/>平台账号 / 审计 / 公寓注册"]
end
subgraph OUT["出站(平台 → 公寓,全部由公寓发起拉取)"]
DL["#1~#7、#23<br/>版本化快照"]
end
SOAP --> D2
FIN --> D1
FIN --> D3
UP --> D5
D1 --> DL
D2 --> DL
D5 --> D3
D3 --> D4
D4 --> D3
D5 --> D6
```
表 3-1 九类数据的分层、归属与平台端落点(沿用 `docs/10` 表 5-1,补“平台端落点”列)
| 数据层 | 典型数据 | 归属 | 平台端落点 | 同步方向 |
| --- | --- | --- | --- | --- |
| 平台主数据 | 公寓、房间基线、人员、照片、交路、时间规则、节假日、餐补标准 | 平台 | 主数据域(表 3-3);照片本体在对象存储 | 下行版本化增量(#1~#5) |
| 平台计划数据 | 运安计划快照、生成的叫乘任务与时刻、变更与取消 | 平台 | 计划与任务域(表 3-4) | 下行(#6、#7) |
| 公寓执行数据 | `GY_JCXX`、`GYGL_LY_EVENT`、房态、保洁、报警 | 公寓 | **不落库**,只保留上行聚合结果 | 本地强一致;摘要上行 |
| 设备数据 | `GY_SB`、`GY_SB_XT`、指令队列、心跳 | 公寓 | **不落库**,只保留摘要(在线率 / 版本 / 故障数) | 仅上行摘要(#11) |
| 账务数据 | 补助明细、消费 / 充值流水、余额 | 主账平台 / 流水双记 | 账务域(表 3-5) | 流水上行(#10);对账后余额快照下行 |
| 归档数据 | `GY_JCXX_HIS`、离寓事件历史 | 公寓 | 只收摘要(数量与区间,#13) | 上行归档摘要 |
| 账号与审计 | 账号、角色、权限、操作审计 | 平台侧 / 公寓侧各自 | 账号与运维域(表 3-8) | 人员—账号映射下行 |
| 语音与媒体数据 | 分机与中继台账、会话、录音索引、TTS | 公寓 | 只保留策略(`PL_VOICE_POLICY`)与摘要(#24) | 配置下行(#23);摘要上行 |
| 汇总与报表 | 跨公寓统计、财务汇总 | 平台 | 汇总统筹域(表 3-7) | 由上行数据加工 |
### 1.1 归属判据(争议时用)
1. **产生事实的一方拥有它**:房间有没有人住、电话有没有接通、余额扣了多少,事实由公寓产生,平台只收结果。
2. **制定规则的一方拥有它**:补助标准、叫乘提前量、号段规则、留存期,规则由平台定,公寓取快照。
3. **两者都归平台时,事实仍以公寓为准**:如“余额”,主账在平台,但“某笔扣款是否真的发生”以公寓流水为证据(`docs/10` 表 5-2「账务」行)。
### 1.2 与公寓端的同名表关系(重要)
`GY_*` 表名在两侧**都存在但语义不同**:
表 3-2 平台库与公寓库同名表的语义差异
| 表 | 平台库语义 | 公寓库语义 | 纪律 |
| --- | --- | --- | --- |
| `GY_ROOMS` | 房间**基线**(`gyid + room_no + bed_numbers` 为权威;物理属性只读留存) | 房间**全量事实**(房态、`live_person`、`sip_account`、保洁时间) | 平台不得用公寓的 `room_status` 做业务判断 |
| `GY_YKT_REMAINDER` | **余额主账**(权威) | 余额**缓存**(带版本 + 最后对账日期) | 平台只在归集与对账事务内写 |
| `GY_SYN_JCXX` | 运安计划**唯一快照** | 平台下发的计划**副本**(`plan_key` 关联) | 公寓不抓运安,只消费 |
| `GYGL_JLXX` | 交路**模板真源** | 模板**快照** + 本地应急新增 | 本地新增须上报确认(D7) |
| `GY_YKT_RY` | 人员**全量真源** | 增量**镜像** | 平台删除只置 `deleted`,不物理删本地行 |
**禁用纪律**:禁止把两侧同名表当同一张表对待,禁止跨库 JOIN,禁止用“表名相同”推导“结构相同”(`docs/10` §5.3 第 4 条)。
### 1.3 44 张表口径的适用范围
`docs/10` 表 5-9 第 1 项的“44 张业务表(`APT_*` 23 + `GY_*`/`gygl_*` 18 + `duty_*` 3)+ 30 张若依 `sys_*`”是**公寓端本地库**的验收口径(`docs/公寓端/03` 表 3-8)。本文引用的平台库表清单**不适用该数字**,两者不可互相校验。
## 2. 命名分区
表 3-3 平台端表命名分区与判定规则
| 分区 | 前缀 / 形态 | 含义 | owner | 允许的写入模块 | 本文落点 |
| --- | --- | --- | --- | --- | --- |
| 存量真源区 | `GY_*` / `GYGL_*` / `GL_*` | 现状已承担“中心真源”职责的表,迁到平台库后保留原名 | `platform` | 对应 `platform.<domain>` 模块 | 表 3-4 ~ 表 3-7、表 3-9 |
| 平台新增区 | `PL_*` | 平台独有、无存量对应物的表(注册、版本、幂等、水位、对账、审计) | `platform` | 单一模块持有(见表内“写入口”列) | 表 3-4 ~ 表 3-9 |
| 餐饮区 | `YKT_MEALS*` | 食堂业务既有表,保持原名避免数据迁移 | `platform` | `platform-catering` | 表 3-7 |
| 报表 / 只读视图区 | `V_*` | 跨域聚合视图,只读 | — | 仅 `platform-report` 建视图 | 表 3-8 |
| **禁止出现** | `APT_*`、`duty_*`、`apt_report_*` | 这些是公寓端 / 应用端私有表 | `apartment` / `duty` | — | 出现即视为边界破坏 |
三条命名纪律(对齐 `docs/10` §5.3 第 1~3 条):
1. **一张表只有一个 owner 与一个主写模块**;`owner` 与写入模块清单纳入表清单维护,越权写入进 Code Review 检查项。
2. **新增表一律 `PL_` 前缀**,不再往 `GY_*` 里加表,避免与存量语义纠缠(存量表的**加列**同样受 G-01~G-03 约束,见 §4.3)。
3. **`P-03`(命名分区是否采用 `PL_` 前缀)仍是待决项**;若评审改为“沿用 `GY_` 前缀”,则本表 3-3 ~ 表 3-8 的表名整体替换,语义不变。
## 3. 平台端表清单
清单口径:`存量迁移` = 现状已存在、迁到平台库;`新增` = 平台端新表。字段只列关键列,类型口径见 §8。
### 3.1 主数据域(`platform.master` / `person` / `route`)
表 3-4 主数据域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `GY_GYZD` | 存量迁移 | 公寓档案(`gyid`、`gymc`、`ip`) | `gyid`、`gymc`、`ip`、`bz` | PK `gyid` | `platform-master` |
| `GY_ROOMS` | 存量迁移(**只保留基线列**) | 房间基线 | `gyid`、`room_no`、`bed_numbers`、`deleted` | UK `(gyid, room_no)` | `platform-master` |
| `GY_GZZD` | 存量迁移 | 工种字典 | `gzdm`、`gzmc` | PK 字典码 | `platform-master` |
| `GY_JRB` | 存量迁移 | 节假日日历 | `rq`、`lx`(`HOLIDAY` / `WORKDAY`) | UK `(gyid, rq)` | `platform-master` |
| `GY_JBXX` | 存量迁移 | 班次 / 基本资料 | 按现状 | — | `platform-master` |
| `GY_SJGZ` | 存量迁移 | 时间规则(`lysjgz`、`zjjcsjgz`、`kjjcsjgz`,按公寓 + 单位) | `gyid`、`dw`、三条规则值 | UK `(gyid, dw)` | `platform-master` |
| `GY_YKT_RY` | 存量迁移 | 人员主数据 | `ryid`、`card_id`、`xm`、`dw`、`gwmc`、`gzmcnew`、`sjh`、`sfzh`、`deleted`、`gxsj`、`photo_url` | UK `(ryid)` | `platform-person` |
| `GY_YKT_RY_ZP` | 存量迁移(**改造**) | 人员照片索引 | `ryid`、`photo_version`、`storage_key`、`checksum` | PK `(ryid)` | `platform-person` |
| `GYGL_JLXX` | 存量迁移 | 交路模板 | `jlid`、`cc`、`kcsj`、`jcsj`、`lysj`、`cclx`、`sfpc`、`bzje`、`gyid` | UK `(gyid, jlid)` | `platform-route` |
| `GYGL_JLXX_TY` | 存量迁移 | 交路停运 / 调整 | `gyid`、`cc`、`kssj`、`jssj` | UK `(gyid, cc, kssj)` | `platform-route` |
| `GYGL_JLXX_LOGS` | 存量迁移 | 交路操作日志 | 按现状 | — | `platform-route`(只追加) |
| `GYGL_JW_RY` | 存量迁移(待盘点) | 机务人员基础表,交路匹配的历史来源 | 按现状 | — | `platform-person`(只读) |
| `ZD_DW` / `zd_zqmc` | 存量迁移 | 单位 / 站区字典 | 按现状 | — | `platform-master` |
| `PL_APARTMENT` | 新增 | 公寓注册(能力开关、协议版本、接入凭据指纹) | `gyid`、`state`、`features`(JSON)、`api_version`、`token_fingerprint`、`registered_at` | PK `gyid`、UK `token_fingerprint` | `platform-ops` |
| `PL_MASTER_VERSION` | 新增 | 主数据版本台账(每次发布一条) | `gyid`、`master_type`、`version_no`、`checksum`、`item_count`、`published_at`、`published_by` | UK `(gyid, master_type, version_no)` | `platform-master` / `person` / `route`(各自类型) |
| `PL_MASTER_SNAPSHOT` | 新增 | 主数据整包快照(补拉与审计用) | `gyid`、`master_type`、`version_no`、`payload`(JSON)、`created_at` | UK `(gyid, master_type, version_no)` | 同上(与版本台账同事务写) |
| `PL_VOICE_POLICY` | 新增 | 语音策略(号段规则、TTS 模板、留存期、导出策略) | `gyid`、`policy_type`、`version_no`、`payload`(JSON)、`effective_from` | UK `(gyid, policy_type, version_no)` | `platform-master` |
### 3.2 计划与任务域(`platform.plan`)
表 3-5 计划与任务域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `GY_SYN_JCXX` | 存量迁移 | 运安计划快照(**唯一入站快照**) | `id`(`jobid` + `taskdicid`)、`ryid`、`cc`、`jch`、`kcsj`、`jcfs`、`h1`、`write_time` | UK `(gyid, id, ryid)` ← 修复现状“同 id 多司机 MERGE 冲突”(`docs/09` §1 P1) | `platform-plan` |
| `GY_SYN_JCXX_STATUS` | 存量迁移 | 抓取 / 匹配状态位 | `gyid`、`status`(0/1)、`last_sync_at` | PK `gyid` | `platform-plan` |
| `GY_SYN_JCXX_HIS` | 存量迁移 | 快照历史(排障用) | 与 `GY_SYN_JCXX` 同构 + `archived_at` | — | `platform-plan`(归档任务) |
| `GY_CALL_DUTY_MATCH_LOCK` | 存量迁移 | 抓取 / 匹配协调锁 | 协调行、租约截止时间 | PK 协调行 | `platform-plan` |
| `JWDATA_SYNC_LOG` | 存量迁移 | 运安抓取 / 匹配日志 | 时间、结果、异常 | — | `platform-plan`(只追加) |
| `PL_TASK` | 新增 | 平台侧任务视图(计划 → 应叫任务,含规则版本) | `task_id`、`gyid`、`plan_key`、`ryid`、`jcsj`(平台计算值)、`rule_version`、`status`、`source_version` | PK `task_id`、UK `(gyid, plan_key)` | `platform-plan` |
| `PL_TASK_CHANGE` | 新增 | 任务变更流水(`UPSERT` / `CANCEL`),#7 的增量源 | `change_seq`、`gyid`、`task_id`、`op`、`version`、`changed_at` | PK `change_seq`、UK `(gyid, task_id, version)` | `platform-plan`(只追加) |
| `PL_TASK_ACK` | 新增 | 公寓回执(#8):复算值与差异 | `ack_id`、`gyid`、`task_id`、`accepted`、`computed_call_time`、`rule_version`、`diff_ms`、`received_at` | PK `ack_id`、UK `(gyid, task_id, rule_version)` | `platform-ingest`(接收)/ `platform-plan`(读) |
### 3.3 账务域(`platform.ledger`)
表 3-6 账务域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `GY_YKT_REMAINDER` | 存量迁移 | 余额主账 | `ryid`、`xjye`、`knye`、`xfze`、`bzze`、`xfxjje`、`xfbzje`、`balance_version`、`last_reconciled_on` | PK `ryid` | `platform-ledger`(**唯一写入口**) |
| `GY_YKT_ALLOWANCE` | 存量迁移 | 补助明细 | `ryid`、`gyid`、`cblx`、`cbje`、`yjlx`、`jlid` | UK `(gyid, ryid, jlid, cblx)` | `platform-ledger` |
| `GY_YKT_CONSUME` | 存量迁移 | 消费明细(归集) | `tid`、`ryid`、`xfje`、`xfsj`、`gyid`、`device_code`、`xfly` | PK `tid` | `platform-ledger`(由 #10 归集) |
| `GY_YKT_XJCZJL` | 存量迁移 | 现金充值流水 | `flow_id`、`ryid`、`czje`、`czsj`、`gyid` | PK `flow_id` | `platform-ledger` |
| `GY_YKT_CASH_REQUEST` | 存量迁移 | 现金充值申请 | `id`、`ryid`、`gyid`、`device_code`、`request_state` | PK `id` | `platform-ledger`(条件更新) |
| `GY_YKT_BTJS` / `GY_YKT_BTBJ` | 存量迁移 | 补助结算 / 补助报警 | 按现状 | — | `platform-ledger` |
| `PL_LEDGER_FLOW` | 新增 | 归集后的**统一流水账**(三类流水归一,供对账与报表) | `flow_id`、`gyid`、`type`(`CONSUME`/`RECHARGE`/`ALLOWANCE`)、`ryid`、`amount`、`balance_after`、`occurred_at`、`biz_version`、`reconcile_state` | PK `flow_id`、UK `(gyid, type, flow_id)` | `platform-ingest`(写入)→ `platform-ledger`(状态推进) |
| `PL_BALANCE_SNAPSHOT` | 新增 | 对账后的余额快照(下行给公寓做缓存基线) | `gyid`、`ryid`、`balance`、`snapshot_version`、`reconciled_on` | UK `(gyid, ryid, snapshot_version)` | `platform-ledger` |
| `PL_ALLOWANCE_RULE` | 新增 | 补助规则(类型、工种、节假日系数、生效区间) | `rule_id`、`gyid`、`cblx`、`gzmc`、`amount`、`effective_from`、`effective_to`、`version_no` | PK `rule_id` | `platform-ledger`(与 `platform-master` 联合评审后发布) |
### 3.4 餐饮域(`platform.catering`)
表 3-7 餐饮域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `YKT_MEALS` | 存量迁移 | 菜品 / 套餐主数据 | `meal_id`、`gyid`、`name`、`price`、`enabled` | PK `meal_id` | `platform-catering` |
| `YKT_MEALS_ORDERING` | 存量迁移 | 订单主表 | `order_id`、`gyid`、`ryid`、`amount`、`state`、`ordered_at` | PK `order_id` | `platform-catering` |
| `YKT_MEALS_ORDERS_MX` | 存量迁移 | 订单明细 | `order_id`、`meal_id`、`qty`、`price` | UK `(order_id, meal_id)` | `platform-catering` |
| `PL_CATERING_SETTLE` | 新增 | 结算批次(按公寓 + 日) | `settle_id`、`gyid`、`settle_date`、`amount`、`state`、`ledger_batch_id` | UK `(gyid, settle_date)` | `platform-catering` → 调 `platform-ledger` |
**硬约束**:`platform-catering` 对 `GY_YKT_REMAINDER` **零写权限**。扣款一律调 `platform-ledger` 的服务并落 `PL_LEDGER_FLOW`,这是 D1 的落地点(`docs/10` 表 4-1「餐饮订单/后厨」行、表 10-1「职责边界不清」行)。
### 3.5 上报、水位与对账域(`platform.ingest`)
表 3-8 上报 / 水位 / 对账 / 报表域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `PL_RECEIVE_LOG` | 新增 | 上行幂等表(**幂等唯一性落点**) | `idempotency_key`、`gyid`、`biz_type`、`biz_id`、`biz_version`、`occurred_at`、`received_at`、`server_received_at`、`result_code`、`payload_hash` | UK `(gyid, idempotency_key)`、UK `(gyid, biz_type, biz_id, biz_version)` | `platform-ingest` |
| `PL_WATERMARK` | 新增 | 每公寓每通道水位与积压 | `gyid`、`channel`、`acked_watermark`、`max_seen_watermark`、`pending_count`、`updated_at` | UK `(gyid, channel)` | `platform-ingest` |
| `PL_RECONCILE_BATCH` | 新增 | 日对账批次 | `batch_id`、`gyid`、`biz_date`、`state`、`count_local`、`count_platform`、`sum_local`、`sum_platform`、`max_watermark` | UK `(gyid, biz_date)` | `platform-ingest` |
| `PL_RECONCILE_DIFF` | 新增 | 对账差异明细 | `diff_id`、`batch_id`、`diff_type`(`MISSING`/`EXTRA`/`AMOUNT`/`WATERMARK`)、`biz_key`、`expected`、`actual`、`state` | PK `diff_id` | `platform-ingest` |
| `PL_REPLAY_JOB` | 新增 | 重放作业(区间或幂等键) | `replay_id`、`gyid`、`biz_type`、`from_watermark`、`to_watermark`、`state`、`created_by` | PK `replay_id` | `platform-ingest` |
| `PL_REPORT_DAILY` | 新增 | 跨公寓日汇总(报表底座) | `gyid`、`stat_date`、`stay_count`、`call_count`、`miss_rate`、`device_online_rate`、`ledger_amount` | UK `(gyid, stat_date)` | `platform-report`(只读 `ingest` 结果加工) |
| `V_CALL_QUALITY` | 新增(视图) | 跨公寓执行质量视图 | 由 `PL_REPORT_DAILY` + `PL_TASK_ACK` 聚合 | — | 只读 |
### 3.6 账号、运维与审计域(`platform.iam` / `ops`)
表 3-9 账号 / 运维 / 审计域表清单
| 表 | 类别 | 用途 | 关键列 | 唯一约束 | 写入口 |
| --- | --- | --- | --- | --- | --- |
| `GL_YH` / `GL_JS` / `GL_QX` / `GL_CD` / `GL_YH_JS` / `GL_JS_QX` | 存量迁移 | 平台账号与 RBAC | 按现状 | 按现状 | `platform-iam` |
| `GL_LOGS` / `GY_LOGS` | 存量迁移 | 登录日志 / 业务日志 | 按现状 | — | `platform-iam`、各模块只追加 |
| `PL_ACCOUNT_PERSON_MAP` | 新增 | 人员—账号映射(打通两套 RBAC,**不合表**) | `gyid`、`ryid`、`platform_user_id`、`apt_user_id`、`mapped_at` | UK `(gyid, ryid)` | `platform-iam` |
| `PL_AUDIT_OP` | 新增 | 平台操作审计(谁改了什么版本) | `audit_id`、`operator`、`module`、`action`、`target_type`、`target_id`、`before_hash`、`after_hash`、`at` | PK `audit_id` | `platform-framework`(统一拦截) |
| `PL_AUDIT_API` | 新增 | 跨端接口审计(请求 / 响应摘要 + 幂等键) | `audit_id`、`gyid`、`path`、`method`、`idempotency_key`、`batch_id`、`result_code`、`latency_ms`、`at` | PK `audit_id` | `platform-framework` |
| `PL_APARTMENT_HEALTH` | 新增 | 公寓健康度快照(#14 落点) | `gyid`、`last_heartbeat_at`、`app_version`、`outbox_watermark`、`clock_skew_ms`、`state` | PK `(gyid, snapshot_at)` | `platform-ops` |
| `PL_CONFIG_VERSION` | 新增 | 配置下发版本(平台只落“当前有效版本”,不推送) | `gyid`、`config_type`、`version_no`、`checksum`、`effective_from` | UK `(gyid, config_type, version_no)` | `platform-ops` |
## 4. 状态字段与唯一写入口
表 3-10 平台侧状态字段、取值与唯一写入口
| 表.字段 | 取值 | 唯一写入口 | 迁移规则 | 备注 |
| --- | --- | --- | --- | --- |
| `PL_TASK.status` | `PLANNED` / `DISPATCHED` / `ACKED` / `EXECUTING` / `FINISHED` / `CANCELLED` | `platform-plan`(`DISPATCHED` 由拉取行为推进,`EXECUTING` / `FINISHED` 由 #9 推进) | `PLANNED → DISPATCHED → ACKED → EXECUTING → FINISHED`;任意非终态 `→ CANCELLED`;`FINISHED` 为终态不可回退 | 条件更新(`WHERE status=?`),不先查后改 |
| `PL_TASK_CHANGE.op` | `UPSERT` / `CANCEL` | `platform-plan` | 只追加,不更新 | 契约外的 `op` 直接拒绝(`docs/公寓端/09` 表 9-9) |
| `PL_RECEIVE_LOG.result_code` | `OK` / `PL-*` 错误码 | `platform-ingest` | 只追加 | 重复键返回原 `result_code`,不覆盖 |
| `PL_LEDGER_FLOW.reconcile_state` | `RECEIVED` / `RECONCILED` / `DIFF` | `platform-ledger` | `RECEIVED → RECONCILED` 或 `→ DIFF`;`DIFF` 经人工处理后回 `RECONCILED` | 金额差异为 P0,当日必须结清 |
| `PL_RECONCILE_BATCH.state` | `RUNNING` / `MATCHED` / `DIFF_FOUND` / `CLOSED` | `platform-ingest` | `RUNNING → MATCHED` 或 `→ DIFF_FOUND → CLOSED` | 一个 `(gyid, biz_date)` 只允许一个活跃批次 |
| `PL_REPLAY_JOB.state` | `PENDING` / `RUNNING` / `DONE` / `FAILED` | `platform-ingest` | 单向推进,`FAILED` 需人工重建 | 重放不得改写已 `FINISHED` 的业务状态 |
| `PL_APARTMENT.state` | `REGISTERED` / `SUSPENDED` / `RETIRED` | `platform-ops` | `REGISTERED ⇄ SUSPENDED`;`→ RETIRED` 单向 | `SUSPENDED` 时下行返回“停止派发”、上行仍接收(避免丢流水) |
| `PL_MASTER_VERSION.apply_result` | `PENDING` / `APPLIED` / `FAILED`(**公寓侧回写**) | 平台侧只读 | 由 `apt_master_version.apply_result` 汇总而来 | 平台侧诊断字段,不是平台写入 |
| `GY_SYN_JCXX_STATUS.status` | `0` 未同步 / `1` 可匹配 | `platform-plan` | 抓取与匹配互斥(沿用 `GY_CALL_DUTY_MATCH_LOCK`) | 保留现状语义,不扩展数字码 |
### 4.1 状态机三条纪律
1. **一字段一写入口**:跨模块只能通过服务方法推进,禁止在别的模块 Mapper 里 `UPDATE ... SET status=`。
2. **条件更新优先**:所有状态迁移用 `UPDATE ... WHERE status = <期望前置状态>` 并校验影响行数,失败即视为并发冲突(`docs/10` 表 5-6「事务与锁」行的落地)。
3. **终态不可回退**:`FINISHED` / `RETIRED` / `CLOSED` 为终态,需要更正时新建记录,不原地改。
### 4.2 平台侧与公寓侧状态的分工
表 3-11 平台侧与公寓侧状态的分工
| 概念 | 平台侧 | 公寓侧 | 权威方 |
| --- | --- | --- | --- |
| 任务是否已下发 | `PL_TASK.status = DISPATCHED` | 无对应字段(拉取行为即事实) | 平台(据拉取日志推断,允许滞后) |
| 任务是否被接受 | `PL_TASK_ACK.accepted` | 回执生成 | 公寓(平台只记账) |
| 叫乘是否完成 | `PL_TASK.status = FINISHED` | `gy_jcxx.ryzt`、`finish_jc` | **公寓**(平台不写 `ryzt`) |
| 余额 | `GY_YKT_REMAINDER` | `gy_ykt_remainder`(缓存) | 平台(对账后权威) |
### 4.3 与 G-01 / G-02 / G-03 的边界
`docs/10` v1.2 与 `docs/公寓端/08` 表 8-9 已确认:楼栋、人员本地启停、交路停用原因三处**本期走“不改库兜底”、暂不加字段**。平台端因此:
1. **不得顺手补列**:`GY_ROOMS` 不新增楼栋列,`GY_YKT_RY` 不新增本地启停列,`GYGL_JLXX` 不新增停用原因列。
2. 需要这些信息的平台侧功能(如按楼栋统计房间数),一律以“无此维度”呈现或走口径说明,不做隐式推断。
3. 若后续确需补列,走 `09` 的 **P-05 单独评审**,不与本期拆分混提。
## 5. 多公寓(`gyid`)隔离与注册
表 3-12 平台侧四级隔离(对齐 `docs/10` §3.3「公寓隔离方式」)
| 层次 | 平台端做法 | 落地要求 | 违反后的故障形态 |
| --- | --- | --- | --- |
| 请求级 | 跨端请求头 `X-Apt-GyId` + 路径 / 查询参数 `gyId` 双携带,不一致直接拒绝 | 拦截器统一校验;不一致返回 `PL-GYID-MISMATCH` | 身份伪造:A 公寓可写 B 公寓数据 |
| 会话级 | 平台管理端用户在会话里持有“数据范围 `gyId` 集合”;跨公寓操作用显式切换,不靠参数 | 越权访问返回 403 并记 `PL_AUDIT_OP` | 管理员误操作改错公寓配置 |
| 行级 | 所有平台表以 `gyid` 作为复合主键 / 唯一键的第一列(无 `gyid` 语义的全局字典表除外) | 新建表的 UK 强制 `` `(gyid, ...)` ``;缺 `gyid` 条件的 SQL 视为缺陷 | 统计与对账串公寓 |
| 库级 | 平台主库只服务平台;**公寓库账号对平台库零权限,平台账号对公寓库零权限** | 双库各建独立账号;平台不配置公寓数据源 | 跨库 JOIN 与“顺手修数据”,边界彻底失效 |
**公寓注册**(新增能力,`platform-ops`):
1. 注册动作:运维在平台录入 `gyid` + 名称 + 能力开关 + 接入凭据(令牌 / 证书指纹),状态置 `REGISTERED`。
2. 准入校验:下行请求先校验 `gyid` 是否 `REGISTERED`;未注册返回 `PL-GYID-UNKNOWN`,公寓按 `docs/公寓端/09` 表 9-4 停止派发并横幅告警。
3. **多公寓注册的展开方式仍是待决项(P-08)**:现网用 `GY_GYZD.ip` 做“一实例多公寓”的识别,目标架构改为“一公寓一实例 + 部署期固定 `gyId`”(`docs/10` §3.3 第 1 条、D5)。平台侧的 `PL_APARTMENT` 只解决“平台认识哪些公寓”,不解决“公寓如何自证身份”,后者依赖 P-08。
## 6. 同步方向与版本快照
表 3-13 平台侧同步方向、载体与版本语义
| 数据域 | 方向 | 触发方式 | 载体 | 版本语义 | 失败降级 |
| --- | --- | --- | --- | --- | --- |
| 公寓档案(#1) | 平台 → 公寓 | 公寓启动 + 每日 1 次 | `PL_MASTER_VERSION(APARTMENT)` | `version` 单调递增;`features` 缺失按“全关” | 公寓用最后快照,告警 |
| 人员(#2) | 平台 → 公寓 | 每日 1 次 + 值班前增量 | `PL_MASTER_VERSION(PERSON)` + `cursor` | `version` 兼作游标(G-09 兜底,P-06 要求单调) | 新人员不可叫乘,存量不受影响 |
| 交路(#3) | 平台 → 公寓 | 每日 1 次 + 计划前 | `PL_MASTER_VERSION(ROUTE)` | 按日期区间全量,区间内以 `version` 定序 | 已生成任务照常执行 |
| 规则 / 节假日(#4) | 平台 → 公寓 | 每日 1 次 | `PL_MASTER_VERSION(RULE)` | 全量;**已下发任务的复算结果不因新规则回溯变化** | 保持最后已知版本 + 告警 |
| 房间基线(#5) | 平台 → 公寓 | 每日 1 次 | `PL_MASTER_VERSION(ROOM)` | `version` 比对 | 新房间不生效 |
| 计划(#6) | 平台 → 公寓 | 公寓每 30 分钟 + 计划前 | `GY_SYN_JCXX`(平台侧快照)+ `source_version` | 计划带 `planId` 幂等键 | 公寓用本地快照;超 2 小时未更新横幅提示 |
| 任务变更(#7) | 平台 → 公寓 | 公寓每 5 分钟增量 | `PL_TASK_CHANGE`(`change_seq` 即游标) | 同 `taskId` 按 `version` 单调,旧版丢弃 | 停在上一游标,恢复后补拉 |
| 语音配置(#23) | 平台 → 公寓 | 公寓启动 + 每日 1 次 | `PL_VOICE_POLICY` | `versionNo` + `checksum` 双校验 | 保持最后已知配置,`apply_result=FAILED` 留痕 |
| 执行结果(#9) | 公寓 → 平台 | 状态变更 ≤10s 或每 30s 批量 | `PL_RECEIVE_LOG` + `PL_TASK` | `bizVersion` 递增用于更正 | outbox 续传 |
| 账务流水(#10) | 公寓 → 平台 | 流水落库后 | `PL_LEDGER_FLOW` | `idempotencyKey` 唯一 | outbox 续传;金额差异 P0 |
| 余额快照 | 平台 → 公寓 | 每日对账后 | `PL_BALANCE_SNAPSHOT`(随 #1 或独立配置版本下发,**载体待 P-09 定**) | `snapshot_version` 单调 | 公寓缓存超期则限制大额操作 |
| 摘要类(#11~#13、#24) | 公寓 → 平台 | 每小时 / 报警后 ≤30s / 归档后 | `PL_REPORT_DAILY` 相关 | 无版本语义,按批覆盖 | 不阻塞主链路 |
| 心跳(#14) | 公寓 → 平台 | 每 60s | `PL_APARTMENT_HEALTH` | 无 | 缺心跳超阈值 → 健康度告警 |
### 6.1 版本快照机制(平台侧)
1. **发布即快照**:`PL_MASTER_VERSION` 与 `PL_MASTER_SNAPSHOT` 在同一事务写入,快照存整包 JSON。这样公寓“补拉同一版本”时平台不需要重算,也便于事后审计“当时下发的是什么”。
2. **版本单调(P-06 待决)**:契约要求 `version` 单调递增(`docs/公寓端/09` F-13),否则公寓无法用 `version_no` 当游标(G-09 兜底失效)。本文按“单调递增”设计,但该项在 F-13 / P-06 定稿前不作承诺。
3. **不回溯**:新规则不重算已下发任务的 `jcsj`,避免“同一任务在不同时刻算出两个叫乘时间”。差异只在 #8 回执的 `diffMs` 中体现。
4. **快照只增不改**:同一 `(gyid, master_type, version_no)` 只允许一条,内容不可变;需要更正就发布新版本。
### 6.2 上行侧的三时间戳与水位
平台写入 `PL_RECEIVE_LOG` 时必须三方并存(`docs/10` §6.2 第 1 条):
表 3-14 三时间戳的来源与平台用途
| 时间戳 | 来源 | 平台用途 |
| --- | --- | --- |
| `occurred_at` | 公寓信封 `items[].occurredAt` | 业务时间轴、报表口径 |
| `received_at` | 公寓信封 `items[].receivedAt` | 判“公寓入库到发出”的延迟 |
| `server_received_at` | 平台接收时刻(回执带回 `serverReceivedAt`) | 判“网络 + 平台处理”延迟;账务日切以此为准 |
水位落在 `PL_WATERMARK`:`max_seen_watermark` 记录已见最大序号,`acked_watermark` 记录“已处理完成”的最大序号。两者长期不等即判定漏报或卡死(`docs/公寓端/09` 表 9-15)。
## 7. 归档与留存
表 3-15 平台侧归档与留存策略(合规项依赖 D15,未定前按表内默认值)
| 数据 | 在线留存 | 归档方式 | 归档后可用性 | 依据 |
| --- | --- | --- | --- | --- |
| `PL_RECEIVE_LOG` | 90 天 | 按月归档到 `PL_RECEIVE_LOG_HIS`(同构) | 只读查询;重放不依赖归档(重放按 `PL_WATERMARK` + outbox 侧) | 幂等表只是短期去重窗口,长期去重靠业务表 UK |
| `PL_MASTER_SNAPSHOT` | 最近 5 个版本 | 超出部分导出对象存储(压缩包 + `checksum`) | 需人工回捞 | 补拉只需当前版本 |
| `PL_AUDIT_OP` / `PL_AUDIT_API` | 3 年 | 按年归档,不删除 | 只读审计查询 | 审计留存通常按会计 / 监管周期 |
| `PL_LEDGER_FLOW` | **不清理** | 按会计年度分区(若库支持),否则按年归档副本 | 全量可查 | 账务类数据不得缩短留存,属平台侧自定策略(会计 / 审计惯例,与 D15 录音合规无关);流水双记口径见 `docs/10` 表 5-1「账务数据」行 |
| `PL_RECONCILE_BATCH` / `PL_RECONCILE_DIFF` | 2 年 | 按年归档 | 只读 | 差异工单需可追溯 |
| `GY_SYN_JCXX` | 30 天 | 转 `GY_SYN_JCXX_HIS` | 只读 | 计划快照只服务当前窗口(公寓侧窗口约 4 小时,平台留 30 天足够排障) |
| `GY_JCXX_HIS` 等公寓归档表 | **平台不持有** | — | 需要时按 #13 摘要 + 人工回捞 | 归档归公寓(`docs/10` 表 5-1「归档数据」行) |
| 对象存储(人员照片 / 报表导出) | 照片随人员生命周期;导出报表按结算周期保留 | 生命周期规则自动转冷 / 删除 | 索引仍可查,文件不可取时标注 | 平台桶只此两类(`07` 表 7-7);叫乘抓拍与录音**不进平台对象存储**,抓拍留存按公寓侧 `leaveSnapshot.retentionDays` |
**留存的三条纪律**:
1. **平台不承接公寓的归档义务**:公寓本地归档、平台只收摘要;平台侧不留 `GY_JCXX` 明细。
2. **录音永不进平台**:`APT_RECORDING` 的索引与文件都在公寓;平台只收 `recordingMissing` 计数(`docs/10` 表 6-3 要点第 5 条)。
3. **清理必须可审计**:任何删除动作先置状态、再删数据,并写 `PL_AUDIT_OP`。
## 8. 类型与方言口径
平台库当前为 KingbaseES,是否改 MySQL 待 D13(`docs/10` 表 5-8)。为让迁移“只换物理类型、不改语义”,本文只给**逻辑类型族**。
表 3-16 逻辑类型族与方言映射
| 逻辑类型族 | 含义 | KingbaseES 落法 | MySQL 落法 | 约束 |
| --- | --- | --- | --- | --- |
| `id_ulid` | 业务主键(跨端幂等键的组成部分) | `varchar(32)` | `varchar(32)` | 一律 ULID / UUID 字符串,**不用自增**(`docs/10` 表 5-4 自增行) |
| `id_surrogate` | 单库内代理主键(仅本表内部使用) | `bigint` + 序列 | `bigint AUTO_INCREMENT` | 不得出现在跨端契约 |
| `code_32` | 编码类(`gyid`、房间号、分机号、工号、车次) | `varchar(32)` | `varchar(32)` | 入库与传输统一大写(`docs/公寓端/09` 表 9-3 末行) |
| `name_128` | 名称类(公寓名、人名、岗位名) | `varchar(128)` | `varchar(128)` | — |
| `remark_512` | 备注类 | `varchar(512)` | `varchar(512)` | — |
| `money` | 金额 | `numeric(12,2)` | `decimal(12,2)` | 传输用字符串两位小数,禁止浮点 |
| `ratio` | 比率(成功率、在线率) | `numeric(6,4)` | `decimal(6,4)` | — |
| `count_big` | 计数 | `bigint` | `bigint` | — |
| `ts_ms` | 时间点(含毫秒) | `timestamp(3)`(Oracle 兼容模式下按现状) | `datetime(3)` | **不用 `timestamp`(MySQL 侧)**,避免会话时区影响 |
| `date_only` | 纯日期(业务日、节假日) | `date` | `date` | 账务日切以平台时间为准 |
| `enum_str` | 枚举 | `varchar(16)` + CHECK | `varchar(16)` + CHECK | 大写下划线;不用 MySQL `ENUM` |
| `json_doc` | 结构化载荷(`features`、`payload`) | `text`(现状无 JSON 类型) | `json` | MySQL 侧不能有默认值、不能直接建索引 |
三条方言纪律:
1. **空串归一为 `NULL`**:现库 `ora_input_emptystr_isnull=on`,MySQL 保留空串;平台侧统一在实体转换层把 `""` 归一为 `NULL`,否则迁移后 `= ''` 与 `IS NULL` 结果会变(`docs/10` 表 5-6「空串语义」行)。
2. **金额精度统一 `decimal(12,2)`**:现状金额精度不一致,实测三表合计无法对账(`docs/10` 表 5-6「数值与金额」行)。
3. **排序规则不逐列指定**:若改 MySQL,全库与 `sys_*` 对齐为 `utf8mb4_general_ci`,大小写归一在应用层做(差-1)。
+246
View File
@@ -0,0 +1,246 @@
# 04 平台端自身接口设计
> 追溯:`docs/10` §6.1 表 6-1(跨端接口)、§6.2(上报协议)、§6.3 表 6-2(可靠性机制)、§4.2 表 4-2(模块清单)。
> 契约基线:`docs/公寓端/09` 表 9-2~表 9-5(通用约定与返回码)、表 9-19(安全约束)。
> 说明:**跨端 20 个接口不在本篇**,见 `05-跨端接口对齐.md`;本篇只定义平台端自己人用的接口。
## 1. 接口分层与命名空间
平台端有三类 HTTP 入口,按路径前缀隔离、按拦截器链隔离,避免"公寓端能调到管理接口"。
表 4-1 接口分层、路径前缀与鉴权
| 层 | 路径前缀 | 调用方 | 鉴权 | 是否对外 | 本篇 / 对端 |
| --- | --- | --- | --- | --- | --- |
| 跨端下行 / 上行 | `/platform/v1/master/**`、`/plans/**`、`/tasks/**`、`/ack/**`、`/report/**`、`/heartbeat` 等 | 公寓端 | 服务令牌或 mTLS + `X-Apt-GyId` | 是(仅公寓网段 / 白名单) | `05` |
| 跨端语音 | `/open/v1/voice/**` | 公寓端 | 双向令牌 | 是 | `05` |
| **平台管理** | `/platform/v1/admin/**` | 平台管理端(浏览器) | 会话 + 权限点 + 数据范围 `gyId` | 否(仅办公网 / VPN) | **本篇** |
| 平台内部 | 进程内服务调用 | `platform-worker` 等 | 无(同进程 / 内网 RPC) | 否 | 本文不定义 |
选择 `/platform/v1/admin/**` 作为管理命名空间的原因:
1. 20 个跨端接口已占用 `/platform/v1/master/...` 等名词,管理接口若也放在 `/master`,会与 #1 的 `GET /platform/v1/master/apartments/{gyId}` 语义混淆(一个是"给公寓的整包",一个是"给人看的列表")。
2. 网关可按前缀做鉴权与限流隔离,误配也不会把管理接口暴露给公寓端。
3. 该命名空间属本篇新增约定,登记为 **P-10**(若评审要求改为按端分组如 `/platform-admin/v1/**`,仅换前缀,语义不变)。
## 2. 通用约定
表 4-2 管理接口通用约定
| 项 | 约定 | 理由 / 依据 |
| --- | --- | --- |
| 方法语义 | `GET` 查、`POST` 创建或动作、`PUT` 全量改、`PATCH` 局部改、`DELETE` 逻辑删 | 存量写操作大量用 `@RequestMapping` 接受 GET(`docs/09` §1 P1),新接口一律收敛方法 |
| 公寓维度 | 路径或参数必带 `gyId`;跨公寓聚合接口使用 `gyIds[]` 数组 | 行级隔离要求在 SQL 层可验证(`03` 表 3-12) |
| 分页 | `pageNum` / `pageSize`(默认 1 / 20,上限 200)+ `total` 返回 | 与 RuoYi 分页习惯一致,便于前端复用 |
| 排序 | `orderBy` 白名单枚举(不接受任意列名) | 防注入;也与"排序规则不逐列指定"解耦 |
| 时间 | 请求 / 响应均 ISO-8601 带偏移;库内毫秒精度 | `docs/公寓端/09` 表 9-3 |
| 金额 | 字符串两位小数 | 同上 |
| 枚举 | 大写下划线字符串,不接受数字码 | 同上 |
| 版本控制 | 更新类接口必须带 `version`(乐观锁),冲突返回 `PF-0003` | 平台多实例 + 人机并发,先查后改必然丢更新 |
| 幂等 | 创建类接口接受 `Idempotency-Key`;动作类接口接受 `requestId` | 与跨端同一套语义,避免两套心智 |
| 返回体 | `{ code, msg, data, traceId }`;`code = "PF-xxxx"` 或 `"0"` 表示成功 | 与公寓端 `Result` 结构心智一致 |
| 审计 | 所有写接口自动落 `PL_AUDIT_OP`(操作人、目标、前后哈希) | `docs/10` 表 8-2「审计日志」行 |
表 4-3 错误码分段
| 段 | 域 | 示例 | 说明 |
| --- | --- | --- | --- |
| `PF-0xxx` | 通用 / 框架 | `PF-0001` 参数不合法、`PF-0002` 无权限、`PF-0003` 版本冲突、`PF-0004` 资源不存在、`PF-0005` 状态不允许、`PF-0006` 重复请求 | 由 `platform-framework` 统一抛出 |
| `PF-1xxx` | master | `PF-1001` 公寓未注册、`PF-1002` 房间基线冲突、`PF-1003` 规则版本被引用不可删 | — |
| `PF-2xxx` | person | `PF-2001` 工号重复、`PF-2002` 照片版本落后、`PF-2003` 已删除人员禁止改 | — |
| `PF-3xxx` | route | `PF-3001` 交路被任务引用、`PF-3002` 停运区间重叠、`PF-3003` 上传模板不匹配 | — |
| `PF-4xxx` | plan | `PF-4001` 运安不可达、`PF-4002` 计划窗口超限、`PF-4003` 任务已终态不可改、`PF-4004` 规则版本缺失 | — |
| `PF-5xxx` | ledger | `PF-5001` 余额不足、`PF-5002` 流水重复、`PF-5003` 对账未结清禁止调账、`PF-5004` 补助规则重叠 | — |
| `PF-6xxx` | catering | `PF-6001` 菜品下架、`PF-6002` 结算批次已关账、`PF-6003` 扣款需走账务接口 | 见 P-07 |
| `PF-7xxx` | report | `PF-7001` 数据未就绪、`PF-7002` 导出超上限 | — |
| `PF-8xxx` | ingest | `PF-8001` 水位不可回退、`PF-8002` 重放区间非法、`PF-8003` 对账批次进行中 | — |
| `PF-9xxx` | iam / ops | `PF-9001` 账号已绑定、`PF-9002` 权限点不存在、`PF-9003` 公寓已退役 | — |
**与跨端错误码的分工**:跨端只用 `PL-*`(`PL-IDEMPOTENT-REPLAY` / `PL-VERSION-STALE` / `PL-GYID-UNKNOWN` / `PL-GYID-MISMATCH` 等,见 `05` 表 5-9);管理端只用 `PF-*`。两套字典不混用,避免"管理端报错被当成公寓端被拒"。
## 3. `platform.master` 域(`/platform/v1/admin/master`)
表 4-4 主数据域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/apartments` | 公寓列表(含注册状态、能力开关摘要) | 天然幂等 | `GY_GYZD` + `PL_APARTMENT` |
| `POST` | `/apartments` | 注册公寓并签发接入凭据 | 需 `Idempotency-Key`(同一 `gyid` 二次创建返回 `PF-1001`) | `PL_APARTMENT` |
| `PATCH` | `/apartments/{gyId}` | 改名称 / 能力开关 / 状态(`REGISTERED`/`SUSPENDED`/`RETIRED`) | 条件幂等(带 `version`) | `PL_APARTMENT` |
| `POST` | `/apartments/{gyId}/rotate-credential` | 轮换接入凭据(旧凭据保留一个过渡期) | 非幂等,二次调用生成新凭据并审计 | `PL_APARTMENT` + `PL_AUDIT_OP` |
| `GET` | `/rooms` | 房间基线列表(`gyId` 必填) | 天然幂等 | `GY_ROOMS`(基线列) |
| `POST` | `/rooms/baseline` | 批量导入 / 校正房间基线(`roomNo`、`bedNumbers`) | 需 `Idempotency-Key`;按 `(gyId, roomNo)` upsert | `GY_ROOMS` |
| `GET` | `/rules` | 时间规则与餐补标准(按 `gyId` + 单位) | 天然幂等 | `GY_SJGZ` |
| `PUT` | `/rules` | 发布规则新版本 | 条件幂等(`version_no` 单调,冲突返回 `PF-0003`) | `GY_SJGZ` + `PL_MASTER_VERSION` |
| `GET` | `/holidays` | 节假日 / 调休日历(按年) | 天然幂等 | `GY_JRB` |
| `PUT` | `/holidays` | 导入年度日历 | 全量覆盖式幂等(同一年重复导入结果一致) | `GY_JRB` |
| `GET` | `/dicts/{type}` | 工种 / 单位 / 站区字典 | 天然幂等 | `GY_GZZD`、`ZD_DW`、`zd_zqmc` |
| `GET` | `/versions` | 主数据版本台账(按 `gyId` + `masterType`) | 天然幂等 | `PL_MASTER_VERSION` |
| `GET` | `/versions/{versionNo}/snapshot` | 取某版本快照(排障 / 审计) | 天然幂等 | `PL_MASTER_SNAPSHOT` |
| `POST` | `/versions/{versionNo}/publish` | 将草稿版本置为"当前有效版本" | 需 `Idempotency-Key`;重复发布返回原结果 | `PL_MASTER_VERSION` |
**发布语义**:`publish` 之后公寓下一次拉取才生效,平台**不做推送**。这一条是 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行的直接体现,不可为"让配置立刻生效"而加回调。
## 4. `platform.person` 域(`/platform/v1/admin/person`)
表 4-5 人员域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/persons` | 人员查询(工号 / 姓名 / 单位 / 卡号,脱敏出参) | 天然幂等 | `GY_YKT_RY` |
| `POST` | `/persons` | 新增人员(`ryid` 为业务键) | 需 `Idempotency-Key`;重复 `ryid` 返回 `PF-2001` | `GY_YKT_RY` |
| `PUT` | `/persons/{ryid}` | 修改人员资料(不改 `ryid`) | 条件幂等(`version` = `gxsj`) | `GY_YKT_RY` |
| `DELETE` | `/persons/{ryid}` | 逻辑删除(`deleted=1`,**不物理删本地行**) | 幂等(重复删除返回成功) | `GY_YKT_RY` |
| `POST` | `/persons/{ryid}/photo` | 上传照片并生成新 `photoVersion` | 需 `Idempotency-Key`(同一文件 `checksum` 复用旧版本) | `GY_YKT_RY_ZP` + 对象存储 |
| `GET` | `/persons/{ryid}/photo-url` | 取短期签名地址 | 天然幂等(签名可重取) | 对象存储 |
| `POST` | `/persons/import` | 批量导入(Excel / 中心同步结果) | 需 `Idempotency-Key`;按 `ryid` upsert | `GY_YKT_RY` |
| `GET` | `/persons/delta` | 查看本次增量(供排障:"这条为什么没下去") | 天然幂等 | `PL_MASTER_VERSION`(`PERSON`) |
| `GET` | `/account-maps` | 人员—账号映射列表 | 天然幂等 | `PL_ACCOUNT_PERSON_MAP` |
| `POST` | `/account-maps` | 建立 / 更新映射(打通两套 RBAC) | 需 `Idempotency-Key`;UK `(gyid, ryid)` 冲突返回 `PF-9001` | `PL_ACCOUNT_PERSON_MAP` |
**脱敏要求**:列表默认返回姓名首字 + `*`、手机号中间四位打码、身份证只回后四位;完整值需单独权限点 `platform:person:read:full` 且必留审计(`docs/公寓端/09` 表 9-19「最小化」行)。
## 5. `platform.route` 域(`/platform/v1/admin/route`)
表 4-6 交路域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/routes` | 交路模板列表(按 `gyId`、车次、生效区间) | 天然幂等 | `GYGL_JLXX` |
| `POST` | `/routes` | 新增交路模板 | 需 `Idempotency-Key`;UK `(gyid, jlid)` | `GYGL_JLXX` |
| `PUT` | `/routes/{jlid}` | 修改模板(被 `PL_TASK` 引用时返回 `PF-3001`,需先停用) | 条件幂等(`version`) | `GYGL_JLXX` |
| `POST` | `/routes/{jlid}/suspend` | 停运 / 调整(写区间) | 需 `Idempotency-Key`;区间重叠返回 `PF-3002` | `GYGL_JLXX_TY` |
| `POST` | `/routes/import` | Excel 上传(模板校验失败返回 `PF-3003` 并回传错误行) | 需 `Idempotency-Key`;整体事务,失败不落半批 | `GYGL_JLXX` |
| `GET` | `/routes/{jlid}/computed-times` | 按基准日试算 `jcsj`/`fjsj`/`jcbjsj`(只算不写) | 天然幂等(纯函数) | 无(计算结果) |
| `POST` | `/routes/{jlid}/publish` | 发布交路版本 | 需 `Idempotency-Key` | `PL_MASTER_VERSION`(`ROUTE`) |
| `GET` | `/routes/logs` | 交路操作日志 | 天然幂等 | `GYGL_JLXX_LOGS` |
**与 D7 的关系**:公寓可"应急新增"现场临时交路并上报,平台收到后进入**待确认队列**;确认动作走 `POST /routes`(或 `PUT`),确认前后都不影响已下发任务。
## 6. `platform.plan` 域(`/platform/v1/admin/plan`)
表 4-7 计划与任务域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/plans` | 运安计划快照查询(窗口默认 4 小时,上限 7 天) | 天然幂等 | `GY_SYN_JCXX` |
| `POST` | `/plans/fetch` | 手工触发一次运安抓取(排障用) | 非幂等,但受协调锁保护(同时只允许一个抓取) | `GY_SYN_JCXX` + `JWDATA_SYNC_LOG` |
| `GET` | `/plans/sync-logs` | 抓取 / 匹配日志 | 天然幂等 | `JWDATA_SYNC_LOG` |
| `POST` | `/tasks/generate` | 由计划生成任务(指定 `gyId` + 日期区间 + 规则版本) | 需 `Idempotency-Key`;同 `plan_key` 重复生成返回原任务 | `PL_TASK` + `PL_TASK_CHANGE` |
| `GET` | `/tasks` | 任务列表(状态、人员、车次、差异标记) | 天然幂等 | `PL_TASK` |
| `PATCH` | `/tasks/{taskId}` | 人工修正单条任务时刻(**只允许 `PLANNED`/`ACKED`**) | 条件幂等(`version`);终态返回 `PF-4003` | `PL_TASK` + `PL_TASK_CHANGE`(`UPSERT`) |
| `POST` | `/tasks/{taskId}/cancel` | 取消任务 | 需 `Idempotency-Key`;幂等(重复取消返回成功) | `PL_TASK` + `PL_TASK_CHANGE`(`CANCEL`) |
| `GET` | `/tasks/{taskId}/acks` | 回执与差异(`computedCallTime` vs 平台 `jcsj`) | 天然幂等 | `PL_TASK_ACK` |
| `GET` | `/tasks/diff-report` | 差异汇总(超阈值任务清单) | 天然幂等 | `PL_TASK_ACK` + `PL_TASK` |
**边界**:平台**不执行**任务,`PL_TASK.status` 只反映"平台认为的状态",真实执行状态以 #9 上报为准;两者长期不一致时按 `PR-02` 处理,不做平台侧主动探测。
## 7. `platform.ledger` 域(`/platform/v1/admin/ledger`)
表 4-8 账务域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/balances` | 余额主账查询(按人员 / 公寓 / 单位) | 天然幂等 | `GY_YKT_REMAINDER` |
| `GET` | `/balances/{ryid}` | 单人余额 + 版本 + 最后对账日期 | 天然幂等 | 同上 |
| `GET` | `/flows` | 归集流水查询(类型 / 时间 / 金额区间) | 天然幂等 | `PL_LEDGER_FLOW` |
| `POST` | `/adjustments` | 人工调账(必须带原因、审批人) | 需 `Idempotency-Key`;重复提交返回原结果 | `PL_LEDGER_FLOW`(`type=ADJUST`)+ `GY_YKT_REMAINDER` |
| `GET` | `/allowance-rules` | 补助规则列表 | 天然幂等 | `PL_ALLOWANCE_RULE` |
| `PUT` | `/allowance-rules` | 发布补助规则版本(区间重叠返回 `PF-5004`) | 条件幂等(`version_no`) | `PL_ALLOWANCE_RULE` |
| `POST` | `/allowance-rules/simulate` | 试算(不落账,返回命中规则与金额) | 天然幂等(纯计算) | 无 |
| `GET` | `/reconcile/batches` | 日对账批次列表 | 天然幂等 | `PL_RECONCILE_BATCH` |
| `GET` | `/reconcile/batches/{batchId}/diffs` | 差异明细 | 天然幂等 | `PL_RECONCILE_DIFF` |
| `POST` | `/reconcile/batches/{batchId}/close` | 结清批次(存在 `AMOUNT` 差异时拒绝,返回 `PF-5003`) | 条件幂等 | `PL_RECONCILE_BATCH` |
**唯一写入口的落地**:余额只能由 `LedgerService` 改;`adjustments` 也必须经 `LedgerService`,禁止任何"补数据脚本"直接 `UPDATE GY_YKT_REMAINDER`。
## 8. `platform.catering` 域(`/platform/v1/admin/catering`)
表 4-9 餐饮域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/meals` | 菜品 / 套餐列表 | 天然幂等 | `YKT_MEALS` |
| `POST` | `/meals` | 新增菜品 | 需 `Idempotency-Key` | `YKT_MEALS` |
| `PUT` | `/meals/{mealId}` | 修改 / 上下架(下架后下单返回 `PF-6001`) | 条件幂等 | `YKT_MEALS` |
| `GET` | `/orders` | 订单查询 | 天然幂等 | `YKT_MEALS_ORDERING` + `YKT_MEALS_ORDERS_MX` |
| `POST` | `/orders/{orderId}/refund` | 退单(走 `platform-ledger` 冲正,不改余额表) | 需 `Idempotency-Key`;重复退单返回原结果 | `YKT_MEALS_ORDERING` + `PL_LEDGER_FLOW` |
| `POST` | `/settle/run` | 生成日结算批次 | 需 `Idempotency-Key`;同 `(gyid, settle_date)` 重复返回原批次 | `PL_CATERING_SETTLE` |
| `POST` | `/settle/{settleId}/close` | 关账(已关账返回 `PF-6002`) | 条件幂等 | `PL_CATERING_SETTLE` |
**P-07 待决**:餐饮域现有表只有 `YKT_MEALS` / `YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 三张(`docs/05` 未收录,据仓库 mapper 实测),缺少"后厨状态、结算规则、设备维度"等结构;本篇只按最小集出接口,表结构盘点与命名分区在 P-07 定稿后补。
> **CR-02 §14 补遗(阶段性食谱资源)**:`platform.catering` 在表 4-9 既有 `/meals` 资源之外,增 `/recipes` 资源(`GET /recipes`、`POST /recipes`、`PUT /recipes/{recipeId}/publish`、`PUT /recipes/{recipeId}/revoke`),由公寓管理员经 N-57 代理调用,按 `gyid` 隔离、发布需二次确认、`recipe_version` 单调;下行按 #4 `recipes[]` + `recipeWatermark` 由公寓主动拉取。**本期不新增 `/orders` 写入路径**:`features.remoteOrdering=false`,远程点菜仅保留接口骨架,下单恒 `40940 FEATURE_DISABLED` / `PF-6001`;P-07 的后厨 / 配送 / 结算规则仍待定稿。本补遗不改动表 4-9 既有接口与幂等语义,不新增跨端编号。
## 9. `platform.report` 域(`/platform/v1/admin/report`)
表 4-10 报表域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/summary/daily` | 跨公寓日汇总(在寓、叫乘、准点率、在线率、账务额) | 天然幂等 | `PL_REPORT_DAILY` |
| `GET` | `/summary/quality` | 执行质量(应答率、复叫率、报警率、上报差异数) | 天然幂等 | `V_CALL_QUALITY` |
| `GET` | `/compare` | 多公寓对比(`gyIds[]`) | 天然幂等 | `PL_REPORT_DAILY` |
| `POST` | `/exports` | 发起异步导出(超过行数上限返回 `PF-7002`) | 需 `Idempotency-Key`;同参数同窗口复用任务 | 对象存储 + `PL_AUDIT_OP` |
| `GET` | `/exports/{exportId}` | 导出任务状态与下载地址(短期签名) | 天然幂等 | 同上 |
| `GET` | `/jobs/status` | 报表加工任务进度(数据未就绪返回 `PF-7001`) | 天然幂等 | `PL_JOB_*`(若启用) |
**边界**:报表只聚合已上报数据,**不重算公寓明细**(`docs/10` 表 4-1「统计报表」行)。任何"补算"需求都转为"让公寓重报"。
> **CR-02 §15 补遗(公寓统计 / 报表 / 排名管理接口)**:`platform-report` 新增 `/platform/v1/admin/stat/**` 管理端接口 `S-01`~`S-07`(表 4-13),**不计入跨端 20 编号、不产生跨端副作用**;每个接口在表 4-10 与 `docs/平台端/03` §0 表 C-3 均可找到落表,符合 §12 三条接口纪律。新增错误码 `PF-7003` 周期未封账 / `PF-7004` `gyIds[]` 越权 / `PF-7005` 修订版本冲突,沿用 `PF-7001` 不输出半成品、`PF-7002` 导出失败。权限点 `platform:stat:list` / `rank` / `generate` / `export` 四级;`S-03` / `S-06` 必带 `Idempotency-Key`。权威表述见 `docs/平台端/10` 表 10-14 / 10-17 / 10-18。
表 4-13 公寓统计 / 报表 / 排名管理接口(CR-02 §15;`S-01`~`S-07`,**均为平台管理端接口**)
| 编号 | 方法 + 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| S-01 | `GET /stat/periods` | 按 `gyIds[]` + `periodType` + `periodKey` + `metricClass` 查五类周期指标 | 天然幂等 | `pl_apt_stat_period`、`PL_REPORT_DAILY` |
| S-02 | `GET /stat/ranks` | 按分类 / 综合排名读快照(`revisionNo`) | 天然幂等 | `pl_apt_stat_rank` |
| S-03 | `POST /stat/generate` | 生成 / 重算周期指标(修订需 `reason`) | 需 `Idempotency-Key`;同 key 复用原 `jobId` | `pl_apt_stat_period`、`PL_JOB_*` |
| S-04 | `GET /stat/jobs/{jobId}` | 生成任务进度与失败码 | 天然幂等 | `PL_JOB_*` |
| S-05 | `GET /stat/metrics` | 指标字典(公式、单位、数据源、方向、权重版本) | 天然幂等 | `pl_apt_stat_period`(字典派生) |
| S-06 | `POST /stat/exports` | 异步导出 XLSX / PDF(`reportType=APT_STAT`) | 需 `Idempotency-Key` | `report-export` + `PL_AUDIT_OP` |
| S-07 | `GET /stat/exports/{exportId}` | 导出状态与短期签名地址 | 天然幂等 | 同上 |
> `S-06` / `S-07` 复用表 4-10 `/exports` 通道与 `report-export` 桶,不新增桶;`gyIds[]` 必须从会话数据范围解析且 **SQL 层带范围**,空 / 越权 `PF-7004`;未封账周期 `PF-7003` 不输出半成品。
## 10. `platform.ingest` 域(`/platform/v1/admin/ingest`)
表 4-11 上报、水位与对账域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/watermarks` | 各公寓各通道水位与积压 | 天然幂等 | `PL_WATERMARK` |
| `GET` | `/receives` | 上报接收记录(按幂等键 / 批次 / 时间) | 天然幂等 | `PL_RECEIVE_LOG` |
| `GET` | `/receives/{idempotencyKey}` | 单条上报详情(含 `payload_hash`) | 天然幂等 | `PL_RECEIVE_LOG` |
| `POST` | `/replay` | 发起重放(区间或幂等键列表);水位回退返回 `PF-8001` | 需 `Idempotency-Key`;非法区间返回 `PF-8002` | `PL_REPLAY_JOB` |
| `GET` | `/replays` | 重放作业列表与结果 | 天然幂等 | `PL_REPLAY_JOB` |
| `POST` | `/reconcile/run` | 发起某公寓某日的对账(批次进行中返回 `PF-8003`) | 需 `Idempotency-Key`;同 `(gyid, biz_date)` 复用 | `PL_RECONCILE_BATCH` |
| `GET` | `/reconcile/{batchId}` | 对账结果与三维指标 | 天然幂等 | `PL_RECONCILE_BATCH` |
**与跨端 #16 / #17 的区分**:`/platform/v1/admin/ingest/replay` 是**平台人自己**发起的重放;跨端 `POST /platform/v1/replay`(#17)的发起方与方向待 P-02,两者不合并、不互相替代。
## 11. `platform.iam` 与 `platform.ops` 域
表 4-12 账号、审计与运维域管理接口
| 方法 | 路径 | 用途 | 幂等语义 | 落表 |
| --- | --- | --- | --- | --- |
| `GET` | `/admin/iam/users` | 平台账号列表(`GL_*` 现状 → RuoYi `sys_*` 目标,P-04) | 天然幂等 | `GL_YH` |
| `POST` | `/admin/iam/users` | 新增平台账号 | 需 `Idempotency-Key` | `GL_YH` |
| `PUT` | `/admin/iam/users/{id}/roles` | 角色分配 | 全量覆盖式幂等 | `GL_YH_JS` |
| `GET` | `/admin/iam/permissions` | 权限点清单(`sys_menu` 或 `GL_QX`) | 天然幂等 | `GL_QX` |
| `GET` | `/admin/iam/audits/op` | 操作审计查询 | 天然幂等 | `PL_AUDIT_OP` |
| `GET` | `/admin/iam/audits/api` | 跨端接口审计查询(按 `gyId` / 幂等键) | 天然幂等 | `PL_AUDIT_API` |
| `GET` | `/admin/ops/apartments/health` | 公寓健康度大盘(#14 落库结果) | 天然幂等 | `PL_APARTMENT_HEALTH` |
| `GET` | `/admin/ops/apartments/{gyId}/health` | 单公寓健康度 + 积压 + 时钟偏差 | 天然幂等 | `PL_APARTMENT_HEALTH` + `PL_WATERMARK` |
| `GET` | `/admin/ops/config-versions` | 配置下发版本(只读"当前有效版本") | 天然幂等 | `PL_CONFIG_VERSION` |
| `POST` | `/admin/ops/config-versions` | 登记新配置版本(**不推送**,等公寓拉取) | 需 `Idempotency-Key` | `PL_CONFIG_VERSION` |
| `GET` | `/admin/ops/policies/voice` | 语音策略查询 | 天然幂等 | `PL_VOICE_POLICY` |
| `PUT` | `/admin/ops/policies/voice` | 发布语音策略新版本(留存期、导出策略) | 条件幂等(`version_no`) | `PL_VOICE_POLICY` |
| `GET` | `/admin/ops/jobs` | 后台任务(抓取 / 报表 / 对账 / 清理)运行状态 | 天然幂等 | `PL_JOB_*` |
## 12. 三条接口纪律
1. **管理接口一律不产生跨端副作用**:`publish` / `config-versions` 只把"当前有效版本"落库,绝不触发对公寓的 HTTP 调用。这条是 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行在接口层的强制落点。
2. **写接口必须有 `version` 或 `Idempotency-Key` 二者之一**:既没有乐观锁也没有幂等键的写接口,评审直接退回。
3. **接口清单与表清单同源**:`04` 每新增一个接口,必须在 `03` 找到落表;找不到落表的接口视为设计未完成(这是 `README` §5 要求的"接口—落点"一致性检查)。
+239
View File
@@ -0,0 +1,239 @@
# 05 跨端接口对齐(平台侧)
> 追溯:`docs/10` §6.1 表 6-1(17 个跨端接口)、§6.2(上报信封与三时间戳)、§6.3 表 6-2(幂等 / 重试 / 对账 / 断网)、
> §6.4 表 6-3 第 23~25 项(语音域跨端)、§6.4 末段(17 变 20、不另立 v2)、§8.1 表 8-1(三端部署形态)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(平台不回调公寓)、
> §3.3(公寓身份与 `gyid`)。
> 对端权威表述:`docs/公寓端/09-平台接口约定.md`(表 9-1 二十接口总表、表 9-4 返回码、表 9-18 F-01~F-13)。
> 契约状态:**v2-draft**(CR-02 扩展,与 `docs/公寓端/09` 同版本)。本文**不新增接口、不重排编号**,只补平台侧实现口径;扩展口径见 §0。
> **v2 修订(CR-02,2026-11-09)**:`docs/设计变更/03-三端功能设计修订(CR-02).md` §3、§4 为权威表述,见下文 §0;§1 第 2 条"确需新增则走 v2 契约、不在本期"已按 CR-C07 改判。
## 0. v2 契约扩展(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 5-y` 序号;表 5-1 的 20 个编号、方向、路径与 §2 通用约定继续有效。
表 C-1 既有编号上的 payload 扩展(平台侧接收 / 下发落点)
| # | 扩展内容 | 平台侧主责模块 | 落表 |
| --- | --- | --- | --- |
| 1 | 响应增 `offlineQuota`、`features.tabletCall` | `platform-master` / `platform-ledger` | `pl_offline_quota`、`PL_APARTMENT` |
| 2 | 响应增 `personRegVerdicts[]`(**平台同步回执** `ACCEPTED` / `REJECTED` + `platformVersion`,不是注册批准)与人员增量 `photoRef` / `featureRef` / `checksum` / `featureAlgo` / `featureVersion` / `personCategory` / `mediaUrls[]` | `platform-master` | `pl_person_reg_request`、平台人员主档与对象存储 |
| 3 | `transferPolicy` | `platform-route` | `GYGL_JLXX`(模板) |
| 5 | `building` / `orientation` / `sipExtension` | `platform-master` | `GY_ROOMS`(基线列) |
| 7 | `callMode` / `dispatchSeq` / `transferOrders[]` | `platform-plan` | `PL_TASK_CHANGE` |
| 8 / 9 | `dispatchSeq`、`transferRef`;新 `bizType` 见 表 C-2 | `platform-ingest` → `platform-plan` | `PL_RECEIVE_LOG`、`PL_TASK_ACK`、`PL_TASK` |
| 10 | `LEDGER_OFFLINE`(`offlineScope` / `localBalanceVersion` / `quotaRef`) | `platform-ingest` → `platform-ledger` | `PL_LEDGER_FLOW`、`pl_offline_quota` |
| 11 / 12 | `tabletCount`、`alarmSource` 分组、`deviceCode` | `platform-report` | `PL_REPORT_DAILY` |
| 13 / 14 | `cleanRecords[]`、`staffPerformance[]`、`reportHistory` 计数、`pendingRetryBatches` | `platform-report` / `platform-ops` | `PL_REPORT_DAILY`、`PL_APARTMENT_HEALTH` |
| 23 / 24 | `configType=TABLET`;摘要按 `callMode` 分组 | `platform-master` / `platform-ingest` | `PL_VOICE_POLICY`、`PL_RECEIVE_LOG` |
表 C-2 新增 `bizType` 在 `PL_RECEIVE_LOG` 的分流(不新增接口编号)
| bizType | 接收接口 | 幂等键补充 | 处理模块 |
| --- | --- | --- | --- |
| `PERSON_REG` | #9 | `regId` | `platform-ingest` → `platform-master`(校验工号唯一性 / `checksum` 并回**同步回执**,不做注册准入审批;CR-C25) |
| `CHECKIN` / `CHECKOUT` / `TABLET_RESULT` / `TRANSFER` | #9 | 沿用 #9 | `platform-plan` |
| `LEDGER_OFFLINE` | #10 | UK `(gyid, type, flow_id)` | `platform-ledger` |
| `DEVICE_ALARM` | #12 | `alarmId` + `bizVersion` | `platform-report` |
| `CLEAN_RECORD` / `STAFF_PERFORMANCE_DAILY` | #13 | `(gyid, date, ryid)` 覆盖 | `platform-report` |
| `REPORT_RETRY` | #14 | `batchId`(心跳,不进 items) | `platform-ops` |
三条对齐要求不变(编号不重排、#15 / #25 平台自答、幂等 + 三时间戳 + 水位 / 对账 / 重放收口);**新增的是第 2 条的例外说明**:新增需求一律先落到既有接口的 `payload` 与 `bizType`,平板 / 消费机走设备侧、值班台走端内,均不构成新跨端编号。表 5-3 / 表 5-4 的幂等与重试机制、表 5-6 的 `count` 按 `biz_id` 去重(R-C3)继续适用。
> **CR-02 §12 补遗(人脸主数据同步)**:#9 `PERSON_REG` 的 payload 增 `ryid`、`regState=CONFIRMED`、照片 / 特征值引用与校验和、`featureAlgo` / `featureVersion` / `personCategory`,媒体本体走短期签名直传;**仅在值班员确认注册成功后上报**,申请阶段(`APPLIED`)不上报。平台按 `regId` 去重回同步回执 `ACCEPTED` / `REJECTED`,由公寓拉 #2 取回;回执不决定公寓侧注册是否生效,平台驳回不回滚本地注册(Q-35)。20 个编号、方向、路径与 `v2-draft` 形态不变。
> **CR-02 §13 补遗(入住事实与匹配结论)**:#9 `CHECKIN` / `ROOM_SWAP` payload 增 `applyId`、`origin`、`cardNo` / `workNo`、`jlid` / `cc` / `jch` / `kcsj`、`ruleVersion` / `bizVersion`(`bizId = jcid + 事件序号`);`platform-ingest` 按 `idempotencyKey` 去重并归集。**#8 回执是平台匹配的输入**,`platform.plan` 查 `GY_SYN_JCXX` 运安快照命中后补 `kcsj` / `jcfs` 并写 `PL_TASK_CHANGE(op=UPSERT)`;结论只经 **#7 响应体 `matchVerdict[]`** 下行,`match_state` 沿用正式五值、`UNMATCHED` / `LOCKED` 不清空公寓上报要素。`PL_TASK.jcsj` 仍只是基线,权威 `jcsj` 恒由公寓复算(CR-C06)。字段详见 `docs/公寓端/11` §7、§8 与 `docs/公寓端/09` §0 表 C-7。
> **CR-02 §14 补遗(#10 账务 payload 扩展)**:#1 响应增 `features.remoteOrdering`(默认 `false`);#4 响应增 `recipes[]` + `recipeWatermark`;#10 `LEDGER_OFFLINE` payload 增 `allowanceAmount` / `cashAmount`、`allowanceBefore/After` / `cashBefore/After`、`recipeId` / `recipeVersion` / `items[]`、`rechargeChannel=OFFLINE_CASH` / `qrCodeId` / `confirmedBy`;#16 日对账结论回写 `RECONCILED`(本期公寓主动调用形态,P-02)。`amount` 字符串两位小数、扣减负数、充值正数,`bizId = flowId`,`idempotencyKey = hash(gyid + bizType + flowId + bizVersion)`;**一笔消费只一条 `flowId`**,拆分字段不另生成 `ALLOWANCE_DEDUCT`,避免 `sum(amount)` 双计。字段详见 `docs/公寓端/12` 表 12-26、`docs/公寓端/09` §0 表 C-8。20 个编号、方向、路径与 `v2-draft` 形态不变。
> **CR-02 §15 补遗(#13 摘要扩展与三端状态映射)**:#13 `ARCHIVE` 摘要 `payload` 增 `satisfaction{good,neutral,bad,total}` 与 `roomOccupancy{roomAvailable,roomNights}` **分组计数**(CR-C39),供评价情况与房间利用周转统计;**不新增 `bizType`、不新增接口编号、不接明细**,摘要类按批覆盖、无版本语义(表 3-13)。平台只把计数落 `PL_REPORT_DAILY` 补列,`pl_apt_stat_period` 由日报 rollup;未上报则 `NO_DATA`,**不推断不补零**。统计能力全部落平台管理端 `/platform/v1/admin/stat/**`,**不产生跨端副作用**;20 个编号、方向、路径与 `v2-draft` 形态不变。
表 C-3 平台端视角三端状态映射(CR-02 §15;**唯一权威为 CR-02 §15 表 3-66**,本表只投影平台侧落点)
| 能力 / 环节 | 平台侧接收 / 状态 | 公寓端权威状态 | 同步载体 | 应用端 | 回退 |
| --- | --- | --- | --- | --- | --- |
| 平板叫乘结果 | `PL_RECEIVE_LOG`、`PL_TASK_ACK`;`PL_TASK.status` `PLANNED`→`DISPATCHED`→`ACKED`→`EXECUTING`→`FINISHED` / `CANCELLED`;`PL_REPORT_DAILY.call_count` | `faceVerifyState` / `callResultType` / `applied_state`;`gy_jcxx.ryzt` / `finish_jc` 经 `apt.duty.state` 条件更新 | #9 `TABLET_RESULT`、#8 `TASK_ACK`、#13 `ARCHIVE`;**#24 仅 SIP** | 只读 `tablet.result` / `state.changed`,只提交复叫命令 | 迟到 / 重复 `IGNORED`;`UNVERIFIED` 不判完成;平台不生成任务行、不产出权威 `jcsj` |
| 人脸注册 | `pl_person_reg_request`、`regVerdict` `PENDING`→`ACCEPTED` / `REJECTED`(同步回执);`platform_version` 经 #2 取回 | `reg_state` `APPLIED`→`CONFIRMED` / `REJECTED` / `ARCHIVED`;生效点 = 值班员确认 | #9 `PERSON_REG`(+ `person-photo` 短期签名直传)、#2 `personRegVerdicts[]` | N-35 / N-36 / N-37 只提交命令 | `REJECTED` 只告警 + 待办,**不回滚**本地注册与已下发设备(Q-35);不做准入审批 |
| 入住匹配 | `GY_SYN_JCXX`、`PL_TASK` / `PL_TASK_CHANGE(op=UPSERT)`、`PL_REPORT_DAILY.stay_count` | `match_state` 五值、`checkin_state`;`jcsj` 恒由公寓复算 | #9 `CHECKIN`、#6 计划、#8 回执(匹配输入)、#7 `matchVerdict[]` | 只读审核与匹配结论 | `UNMATCHED` / `LOCKED` 不清空上报要素;`LOCAL_ONLY` 属 `offlineScope` |
| 账务 | `PL_LEDGER_FLOW.reconcile_state` `RECEIVED`→`RECONCILED` / `DIFF`;`PL_RECONCILE_BATCH.state`;`GY_YKT_REMAINDER`(`platform-ledger` 唯一写) | `ledger_state` / `offline_scope` / `reportState`;`request_state` 三值 | #10 `LEDGER_OFFLINE`、#16 日对账 | 只读额度 / 流水 / 待核对列表 | `DIFF` + `PF-5003` 阻断调账,冲正只走 `POST /adjustments` |
| 食谱 | `YKT_MEALS`(`platform.catering` 唯一写)、#4 只维护当前有效版本 | `publish_state` 四值;终端选单本地态、确认前不落库 | #4 `recipes[]` + `recipeWatermark`(公寓 GET 拉) | 只读食谱;N-57 只提交代理命令 | 改价 / 下架生成新 `recipe_version`;`40940` 远程点菜不开放 |
| 统计 | `PL_REPORT_DAILY`(冻结不可改)→ `pl_apt_stat_period.state` `DRAFT`→`FINAL`→`REVISED` + `revision_no`;`pl_apt_stat_rank` 快照 | **不感知统计**(业务状态一律不变) | 无跨端交互 | 只读 `FINAL` / `REVISED` 快照(Q-50) | 未封账 `PF-7003`;`DIFF` → `BLOCKED`;修正走次日重算 + 新 `revision_no` |
> 三端同步总口径见 CR-02 §15 表 3-66 注:下行公寓主动 GET 拉、上行公寓 POST 推、平台零回调;`idempotencyKey` / 三时间戳 / `serverReceivedAt` 归日 / outbox 与终态流水不互换 / 对账三维去重,全部沿用既有口径。统计结果**不回写**任何业务状态。
## 1. 总表(20 个,编号沿用 `docs/10`)
表 5-1 二十个跨端接口与平台侧落点
| # | 方向 | 接口 | 用途 | 平台侧主责模块 | 平台侧落表 | 幂等键 | `docs/10` 出处 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 下行 | `GET /platform/v1/master/apartments/{gyId}` | 公寓档案与能力开关 | `platform-master` | `PL_APARTMENT`、`PL_MASTER_VERSION`、`PL_MASTER_SNAPSHOT` | 无(读) | 表 6-1 #1 |
| 2 | 下行 | `GET /platform/v1/master/persons?gyId&cursor&version` | 人员增量(含照片版本) | `platform-person` | `GY_YKT_RY`、`GY_YKT_RY_ZP`、`PL_MASTER_VERSION` | 无(读;`cursor` 分页) | 表 6-1 #2 |
| 3 | 下行 | `GET /platform/v1/master/routes?gyId&from&to` | 交路模板与叫乘时刻 | `platform-route` | `GYGL_JLXX`、`GYGL_JLXX_TY`、`PL_MASTER_VERSION` | 无(读;区间内 `version` 定序) | 表 6-1 #3 |
| 4 | 下行 | `GET /platform/v1/master/rules?gyId` | 时间规则 / 节假日 / 餐补标准 | `platform-master` | `GY_SJGZ`、`GY_JRB`、`PL_MASTER_VERSION` | 无(读;全量) | 表 6-1 #4 |
| 5 | 下行 | `GET /platform/v1/master/rooms?gyId&version` | 房间基线(受 D3 约束) | `platform-master` | `GY_ROOMS`(基线列)、`PL_MASTER_VERSION` | 无(读) | 表 6-1 #5 |
| 6 | 下行 | `GET /platform/v1/plans/duty?gyId&from&to` | 乘务计划(替代本地抓运安) | `platform-plan` | `GY_SYN_JCXX`、`PL_TASK` | `planId`(= `plan_key`) | 表 6-1 #6 |
| 7 | 下行 | `GET /platform/v1/tasks/changes?gyId&cursor` | 任务变更 / 取消增量 | `platform-plan` | `PL_TASK_CHANGE` | `(taskId, version)` | 表 6-1 #7 |
| 8 | 上行 | `POST /platform/v1/ack/tasks` | 任务接收回执(含复算结果) | `platform-ingest` → `platform-plan` | `PL_RECEIVE_LOG`(`biz_type=TASK_ACK`)、`PL_TASK_ACK` | `idempotencyKey`(含 `bizType=TASK_ACK`) | 表 6-1 #8 |
| 9 | 上行 | `POST /platform/v1/report/duty-results` | 叫乘 / 入寓 / 离寓执行结果 | `platform-ingest` → `platform-plan` | `PL_RECEIVE_LOG`(`DUTY_RESULT`)、`PL_TASK`(状态推进) | `idempotencyKey` | 表 6-1 #9 |
| 10 | 上行 | `POST /platform/v1/report/ledger` | 账务流水(消费 / 充值 / 补助) | `platform-ingest` → `platform-ledger` | `PL_RECEIVE_LOG`(`LEDGER`)、`PL_LEDGER_FLOW`、`GY_YKT_*` | `idempotencyKey`;业务侧 UK `(gyid, type, flow_id)` | 表 6-1 #10 |
| 11 | 上行 | `POST /platform/v1/report/device-status` | 设备在线率 / 版本 / 故障摘要 | `platform-ingest` → `platform-report` | `PL_RECEIVE_LOG`(`DEVICE_STATUS`)、`PL_REPORT_DAILY` | `idempotencyKey`(按小时聚合,天然收敛) | 表 6-1 #11 |
| 12 | 上行 | `POST /platform/v1/report/alarms` | 叫乘 / 离寓报警摘要 | `platform-ingest` → `platform-report` | `PL_RECEIVE_LOG`(`ALARM`) | `idempotencyKey`;`handled_result` 变更时 `bizVersion+1` | 表 6-1 #12 |
| 13 | 上行 | `POST /platform/v1/report/archive` | 归档摘要与数据量 | `platform-ingest` → `platform-report` | `PL_RECEIVE_LOG`(`ARCHIVE`) | `idempotencyKey` | 表 6-1 #13 |
| 14 | 上行 | `POST /platform/v1/heartbeat` | 存活、版本、水位、时钟偏差 | `platform-ops` | `PL_APARTMENT_HEALTH`、`PL_WATERMARK` | **不走 items 批量**;`(gyId, sentAt)` 去重 | 表 6-1 #14 |
| 15 | 查询 | `GET /platform/v1/status/{gyId}` | 平台侧查看公寓健康度与积压 | `platform-ops`(**平台自答**) | `PL_APARTMENT_HEALTH`、`PL_WATERMARK` | 无(读) | 表 6-1 #15 |
| 16 | 补偿 | `POST /platform/v1/reconcile/run?gyId&date` | 触发日对账 | **方向待定(P-02)** | `PL_RECONCILE_BATCH` | `(gyId, date)` | 表 6-1 #16 |
| 17 | 补偿 | `POST /platform/v1/replay` | 指定幂等键 / 区间重放 | **方向待定(P-02)** | `PL_REPLAY_JOB` | `(gyId, bizType, from, to)` | 表 6-1 #17 |
| 23 | 下行 | `GET /open/v1/voice/config?gyId&configType` | 语音配置(号段规则 / TTS 模板 / 留存策略) | `platform-master` | `PL_VOICE_POLICY`、`PL_CONFIG_VERSION` | 无(读;`versionNo` + `checksum`) | 表 6-3 #23 |
| 24 | 上行 | `POST /open/v1/voice/summary` | 语音质量摘要 | `platform-ingest` → `platform-report` | `PL_RECEIVE_LOG`(`VOICE_SUMMARY`) | `idempotencyKey` | 表 6-3 #24 |
| 25 | 查询 | `GET /platform/v1/voice/endpoints?gyId` | 平台侧汇总查看(只读) | `platform-report`(**平台自答**) | `PL_REPORT_DAILY`(摘要字段) | 无(读) | 表 6-3 #25 |
**三条不可改动的对齐要求**(与 `docs/公寓端/09` §1 完全一致):
1. **编号不重排**:`1~17` 与 `23~25` 保持间隙,间隙是 `docs/10` 表 6-3 的 18~22 号(公寓端本地运维接口,不跨端)。不得为了“连续好看”重编。
2. **总数 20 不新增**:`docs/10` §6.4 末段已声明“跨端接口总数由 17 变 20,不另立 v2”(编号不另立,与契约版本 `v2-draft` 不是同一概念)。新增需求一律落到既有接口的 `payload` 与新 `bizType` 内(CR-02 CR-C07 / §0 表 C-1、C-2);平板 / 消费机走设备侧 `/open/v1/**`、值班台走端内 `/duty/**`,均**不构成新跨端编号**。原“确需新增则走 v2 契约,不在本期”一句已撤销。
3. **#15 / #25 由平台自答**:这两个接口只在平台侧存在,公寓端不实现(`docs/公寓端/09` 表 9-13)。平台侧不得把它们实现成“向公寓查询”。
## 2. 通用约定(与对端逐条一致)
表 5-2 传输、认证与数据表示
| 项 | 约定 | 平台侧落地 | 与对端一致性 |
| --- | --- | --- | --- |
| 协议 | HTTPS,单向出站 443;公寓端为客户端 | 平台只开放 443 入站;不配置公寓 upstream | 一致(`docs/公寓端/09` 表 9-2) |
| 认证 | mTLS 或双向令牌 | 网关层校验证书指纹 / 令牌,映射到 `PL_APARTMENT.token_fingerprint` | 一致;本期占位实现(契约冻结后补) |
| 公寓身份 | 请求头 `X-Apt-GyId` + 路径/参数 `gyId` 双携带 | 两值不一致返回 `PL-GYID-MISMATCH`,并记 `PL_AUDIT_API` | 一致;F-02 优先级未定时**以“两者必须一致”**规避歧义 |
| 版本协商 | 请求头 `X-Apt-App-Version`;响应头 `X-Platform-Api-Version` | 平台按 `api_version` 做兼容分支,不认识的主版本拒绝 | 一致 |
| 幂等 | 上行 `Idempotency-Key`(批量用 `batchId`) | 落 `PL_RECEIVE_LOG`,UK `(gyid, idempotency_key)` | 一致 |
| 超时 | 连接 3s / 读 10s | 平台侧自身对外部系统(运安 / 财务)用同样的 3s / 10s 口径 | 一致 |
| 时间 | ISO-8601 带偏移;库内毫秒 | `ts_ms` 类型族(`03` 表 3-16) | 一致 |
| 金额 | 字符串两位小数 | 库内 `numeric(12,2)` / `decimal(12,2)`,出参 `toPlainString()` | 一致 |
| 枚举 | 大写下划线字符串 | 平台不做数字码映射(存量 `ryzt` 数字码**不跨端**) | 一致 |
| 主键 | 业务 ID 用 ULID/UUID 字符串,不用自增 | `id_ulid` 类型族 | 一致 |
| 批量 | 单批 ≤ 500 条且 ≤ 1MB,失败按条回报 | 平台逐条返回 `items[].resultCode`,不整批回滚 | 一致(F-04 未定时按“允许部分成功”实现) |
| 字符集 | UTF-8;标识类入库与传输统一大写 | 平台侧在 DTO 边界统一 `toUpperCase()` | 一致 |
## 3. 幂等键与三时间戳
表 5-3 幂等键的生成、校验与平台侧落库
| 环节 | 规则 | 平台侧实现 |
| --- | --- | --- |
| 生成(公寓侧) | `idempotencyKey = hash(gyId + bizType + bizId + bizVersion)` | 平台不重算、只校验格式与长度;不信任公寓自造的“非哈希值” |
| 幂等判重(平台侧) | `(gyid, idempotency_key)` 唯一;同时校验 `(gyid, biz_type, biz_id, biz_version)` | 命中返回原 `resultCode` + 原 `serverReceivedAt`,不重复执行业务副作用 |
| 版本单调 | 同一 `bizId` 只接受递增的 `bizVersion`;旧版本返回 `PL-VERSION-STALE` | `PL_RECEIVE_LOG` 按 `(gyid, biz_type, biz_id)` 取 `MAX(biz_version)` 比较 |
| 更正上报 | `handled_result`、状态终态等变化时 `bizVersion + 1` | 新版覆盖业务表的“当前值”,旧版留痕在 `PL_RECEIVE_LOG` |
| 批量内重复 | 同一批 `items[]` 内出现重复键 | 平台按“先到先处理、后到返回原结果”处理,不整批拒绝 |
表 5-4 三时间戳的来源、校验与用途
| 时间戳 | 来源 | 平台校验 | 用途 | 异常处理 |
| --- | --- | --- | --- | --- |
| `occurredAt` | 公寓:现场发生时间 | 不晚于 `receivedAt`,且不早于业务上界(如任务创建时间) | 业务时间轴、报表口径 | 超出容差记 `WARN` 但仍接收(迟到数据必须能落库) |
| `receivedAt` | 公寓:本地入库时间 | 与 `occurredAt` 差值 ≤ 10 分钟(可配) | 判公寓入库到发出的延迟 | 超阈值记告警,不拒绝 |
| `serverReceivedAt` | 平台:接收时刻 | 由平台生成,回执带回 | 判网络与平台处理延迟;**账务日切以此为准** | 不可由公寓传入,传入即忽略 |
**重放防护**:平台对上行做 `sentAt` 时间窗校验(±5 分钟,`docs/公寓端/09` 表 9-19「重放防护」行);窗外的批次返回 `PL-REPLAY-WINDOW` 并记审计,避免历史批次被重放。
## 4. 重试、对账与断网(平台侧视角)
表 5-5 六个可靠性机制的平台侧落点
| 机制 | 平台侧设计 | 落点 | 与对端配合 |
| --- | --- | --- | --- |
| 幂等 | 唯一约束 + 原结果返回 | `PL_RECEIVE_LOG` | 对端 outbox 记 `SENT`,不重复入队 |
| 重试 | **平台不重试公寓**(不回调);平台只对“外部系统”(运安、财务)重试 | `platform-worker` 的重试策略 | 公寓侧按 1s/5s/30s/2min/10min + 抖动重试 |
| 顺序 | 按 `bizVersion` 单调;乱序丢弃旧版并记 `PL-VERSION-STALE` | `PL_RECEIVE_LOG` | 公寓侧按 `watermark asc` 派发 |
| 对账 | 三维比对:`count` / `sum(amount)` / `max(watermark)` | `PL_RECONCILE_BATCH`、`PL_RECONCILE_DIFF` | 公寓侧 `POST /apt/report/reconcile/run` 或跨端 #16(P-02) |
| 补偿 | 平台侧重放(`PL_REPLAY_JOB`);公寓侧重发(outbox 序号) | `PL_REPLAY_JOB` | 双侧均可发起,但**平台侧发起不等于平台调公寓**:平台只改“待公寓拉取/重发”的语义边界见 §5 |
| 断网 | 下行给最后成功版本;上行按水位续传 | `PL_WATERMARK` | 公寓 outbox 落本地库 |
**“平台侧重放”的实现边界(重要)**:`PL_REPLAY_JOB` 的语义是“平台把该区间标记为需公寓重发,并在公寓下次拉取时通过 #7 的响应体 `replayHint` 告知”——即仍是**公寓发起、平台应答**。若 P-02 判定 #17 允许平台主动调用公寓,则此边界改写;未定前不得实现“平台直连公寓触发重放”。
表 5-6 对账三维与平台侧取值
| 维度 | 平台侧取值 | 差异判定 | 处理 |
| --- | --- | --- | --- |
| `count` | `PL_RECEIVE_LOG` 当日去重后条数(按 `biz_type`) | 与公寓 `count` 不等 | 生成 `MISSING` / `EXTRA` 差异,进人工队列 |
| `sum(amount)` | `PL_LEDGER_FLOW` 当日 `amount` 合计 | 不等即为 **P0** | 当日必须结清,未结清禁止调账(`PF-5003`) |
| `max(watermark)` | `PL_WATERMARK.max_seen_watermark` | 与 `acked_watermark` 长期不等 | 判漏报或卡死;触发重放或运维介入 |
## 5. 方向纪律与 P-02
表 5-7 方向纪律与四个特殊编号的处理
> 方向纪律依据:`docs/backend/10` **表 5-1 第 4 行**(平台端 → 公寓端不允许直连,仅落“待下发配置版本”、由公寓端拉取)+ **表 8-4「出站 443/tcp」行**(单向出站,平台侧不回调)。
| 编号 | `docs/10` 声明的方向 | 与方向纪律的关系 | 本期平台侧实现 | 状态 |
| --- | --- | --- | --- | --- |
| #1~#7 | 下行(平台 → 公寓) | 不冲突:契约上是“平台提供”,实现上是“公寓拉取” | 只提供 `GET`,平台不主动投递 | 已定 |
| #8~#14、#24 | 上行(公寓 → 平台) | 不冲突 | 只接受 `POST`,平台不主动查询 | 已定 |
| #15 | 查询(平台侧) | 不冲突:公寓不参与 | 平台用 `PL_APARTMENT_HEALTH` + `PL_WATERMARK` 自答 | 已定 |
| #16 / #17 | 补偿(`docs/10` 表 6-1 未写明发起方) | **潜在冲突**:若判定为“平台调公寓”,直接违反表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行 | 本期只提供“公寓可主动调用”的形态(对端 `docs/公寓端/09` 表 9-13 同口径);平台侧不实现主动发起 | **P-02 待决** |
| #23 | 下行 | 不冲突 | 只提供 `GET` | 已定 |
| #25 | 查询(平台侧) | 不冲突 | 平台用 #24 摘要自答 | 已定 |
P-02 的三种可能结论与影响(本文不做选择):
表 5-8 P-02 的三种可能结论与影响
| 结论 | 平台侧改动 | 公寓侧改动 | 与方向纪律 |
| --- | --- | --- | --- |
| A 保持“公寓主动调用” | 无(现状) | 无(已实现本地入口) | 完全一致 |
| B 改为“平台回调公寓” | 需新增出站通道、凭据、重试队列 | 需开放入站端口 + 鉴权 | **冲突**,须先改 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行 |
| C 双形态(默认 A,B 仅内网应急) | 增加一个可关闭的出站通道 | 增加可关闭的入站端点 | 有条件冲突,需在 `docs/10` 显式开例外 |
## 6. 平台返回码
表 5-9 平台侧返回码字典(`PL-*`,跨端专用;与 `PF-*` 管理码不混用)
| 返回码 | HTTP | 含义 | 平台侧动作 | 公寓侧期望动作(`docs/公寓端/09` 表 9-4) |
| --- | --- | --- | --- | --- |
| (2xx) | 200 / 201 | 成功 | 落库并回 `serverReceivedAt` | `SENT` |
| `PL-IDEMPOTENT-REPLAY` | 200 | 重复请求,返回原结果 | 查 `PL_RECEIVE_LOG` 返回原 `resultCode` | 视为成功 |
| `PL-VERSION-STALE` | 409 | `bizVersion` 低于已处理版本 | 丢弃并记 `PL_AUDIT_API` | `DEAD`(不再重试) |
| `PL-GYID-UNKNOWN` | 403 | 公寓未注册 / 已退役 | 拒绝并告警 | `FAILED`(人工处理) |
| `PL-GYID-MISMATCH` | 400 | `X-Apt-GyId` 与 `gyId` 不一致 | 拒绝并记审计 | 停止派发 + 横幅告警(配置错误) |
| `PL-REPLAY-WINDOW` | 400 | `sentAt` 超出 ±5 分钟窗口 | 拒绝并记审计 | 人工确认后重发 |
| `PL-CURSOR-EXPIRED` | 400 | #7 / #2 的 `cursor` 已超出平台保留范围(已归档) | 返回推荐的全量区间(`from` / `to`) | 改走 #6 全区间重拉,不无限重试 |
| `PL-PAYLOAD-INVALID` | 400 | 报文结构 / 枚举 / 金额格式不合法 | 返回字段级错误列表 | `DEAD` + 记 `last_error` |
| `PL-PARTIAL-FAILED` | 200 | 批量部分失败 | 返回逐条 `resultCode` | 只重发失败条目 |
| `PL-RATE-LIMITED` | 429 | 超过限流 | 返回 `Retry-After` | 按退避重试 |
| `PL-INTERNAL` | 500 | 平台内部错误 | 记 `traceId` 便于对账 | 按指数退避重试 |
**F-05 未定前的处理**:码值文字(`PL-IDEMPOTENT-REPLAY` 等)在本篇与对端保持一致,但**数值型码位**不承诺;契约冻结时统一转成数字码或保持字符串,二选一,避免两套并存。
**与对端表 9-4 的差异说明**:对端表 9-4 只列了 6 类(含 `PL-4xx` / `PL-5xx` 泛类)。本篇把它细化为 11 条具体码,其中 `PL-GYID-MISMATCH` / `PL-REPLAY-WINDOW` / `PL-PAYLOAD-INVALID` / `PL-CURSOR-EXPIRED` 是**平台侧细化**,语义落在对端的 `PL-4xx` 泛类内,不改变对端的处置动作;`PL-PARTIAL-FAILED` / `PL-RATE-LIMITED` / `PL-INTERNAL` 同理。冻结时由 F-05 一次性定稿,**不新增接口**。
## 7. 逐条核对清单
表 5-10 20 个接口的对齐核对(评审时逐行签认)
| # | 路径一致 | 方向一致 | 幂等键定义一致 | 平台侧有落表 | 结论 |
| --- | --- | --- | --- | --- | --- |
| 1 | ✔ | ✔ 下行 | 不适用(读) | ✔ `PL_APARTMENT` | 对齐 |
| 2 | ✔ | ✔ 下行 | 不适用(读) | ✔ `GY_YKT_RY` | 对齐(`version` 兼作游标待 F-13) |
| 3 | ✔ | ✔ 下行 | 不适用(读) | ✔ `GYGL_JLXX` | 对齐 |
| 4 | ✔ | ✔ 下行 | 不适用(读) | ✔ `GY_SJGZ` / `GY_JRB` | 对齐(键映射待 F-06) |
| 5 | ✔ | ✔ 下行 | 不适用(读) | ✔ `GY_ROOMS` | 对齐(字段范围待 D3) |
| 6 | ✔ | ✔ 下行 | ✔ `planId` | ✔ `GY_SYN_JCXX` / `PL_TASK` | 对齐(`pickupMode` 取值域待 F-07) |
| 7 | ✔ | ✔ 下行 | ✔ `(taskId, version)` | ✔ `PL_TASK_CHANGE` | 对齐 |
| 8 | ✔ | ✔ 上行 | ✔ `bizType=TASK_ACK` | ✔ `PL_TASK_ACK` | 对齐(`TASK_ACK` 列注释缺,对端 G-08) |
| 9 | ✔ | ✔ 上行 | ✔ | ✔ `PL_TASK` | 对齐 |
| 10 | ✔ | ✔ 上行 | ✔ + 业务 UK | ✔ `PL_LEDGER_FLOW` | 对齐 |
| 11 | ✔ | ✔ 上行 | ✔ | ✔ `PL_REPORT_DAILY` | 对齐 |
| 12 | ✔ | ✔ 上行 | ✔(含更正) | ✔ `PL_RECEIVE_LOG` | 对齐 |
| 13 | ✔ | ✔ 上行 | ✔ | ✔ `PL_RECEIVE_LOG` | 对齐 |
| 14 | ✔ | ✔ 上行 | ✔ `(gyId, sentAt)` | ✔ `PL_APARTMENT_HEALTH` | 对齐 |
| 15 | ✔ | ✔ 平台自答 | 不适用 | ✔ | 对齐 |
| 16 | ✔ | **待 P-02** | ✔ `(gyId, date)` | ✔ `PL_RECONCILE_BATCH` | 方向待决 |
| 17 | ✔ | **待 P-02** | ✔ `(gyId, bizType, from, to)` | ✔ `PL_REPLAY_JOB` | 方向待决 |
| 23 | ✔ | ✔ 下行 | 不适用(读) | ✔ `PL_VOICE_POLICY` | 对齐 |
| 24 | ✔ | ✔ 上行 | ✔ | ✔ `PL_RECEIVE_LOG` | 对齐 |
| 25 | ✔ | ✔ 平台自答 | 不适用 | ✔ `PL_REPORT_DAILY` | 对齐 |
## 8. 契约冻结前的待办(与对端 F 系列对应)
1. **F-01 路径前缀**:本篇沿用 `/platform/v1` 与 `/open/v1` 双前缀(与 `docs/10` 表 6-1 / 表 6-3 原文一致)。若统一为单前缀,20 条路径同时改,平台侧网关规则一并改。
2. **F-02 `gyId` 冲突优先级**:本篇按“必须一致”规避;若改为“以路径为准”,需同步改拦截器与审计口径。
3. **F-08 业务键统一**:`planId` / `taskId` 目前是同键空间(对端表 9-8、9-9 已按 `plan_key` 落),冻结时要在契约上显式写明,否则会出现两套键导致重复任务。
4. **F-13 / P-06 `version` 单调**:这是 G-09 兜底成立的前提,也是 #2 / #5 增量拉取的基础;不承诺单调就必须每次全量拉。
5. **F-11 `VOICE_SUMMARY` 口径**:`regRate` / `callSuccessRate` 的统计窗口固定“近 1 小时”,平台侧报表按同一窗口聚合,否则看板数字对不上。
6. **对端 G-08 / G-09**:均属“对端表结构缺口、本期用兜底”,平台侧不因对端缺口改变契约。
+386
View File
@@ -0,0 +1,386 @@
# 06 关键流程(含与公寓端交互)
> 追溯:`docs/10` §5.2 表 5-2(各场景一致性策略)、§6.2(上报信封与三时间戳)、§6.3 表 6-2(幂等 / 重试 / 对账 / 断网)、
> §8.1 表 8-1(三端部署形态)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(“平台端不回调公寓端”)、§7 表 7-1~表 7-4(定时任务与并发幂等)。
> 接口依据:本目录 `05` 表 5-1(20 个跨端接口);对端权威:`docs/公寓端/09` 表 9-9(`op` 语义)、表 9-12(回执字段)、表 9-14~表 9-15(可靠性)。
> 外部系统依据:`docs/07` §2(运安 SOAP)、§3(中心服务器 HTTP)、§6(`@Scheduled` 清单)。
本篇六条流程的每一步都必须能回答五个问题:**谁触发、谁参与、按什么顺序、幂等与失败怎么补、断网后从哪里续**。任何一条流程图里如果出现“平台 → 公寓”的实线箭头,即为设计错误(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)。
> **v2 修订(CR-02,2026-11-09)**:4 条流程变更以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §7 为权威,见下文 §0;六条 F 流程的骨架与"平台不回调公寓"不变。
## 0. v2 流程变更索引(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 6-y` 序号;§2 F1~§7 F6 原流程继续有效。
表 C-1 平台侧流程变更落点
| 编号 | 流程 | 平台侧改动 | 涉及原流程 | 约束 |
| --- | --- | --- | --- | --- |
| F-C | 换乘 | #8 / #9 接收 `TRANSFER` 变更(含 `recomputedCallTime`),只记录不作为权威时刻;驳回结论由 #7 下行、由公寓自己置 `REJECTED` | §3 F2、§4 F3 | 不是平台回调(CR-C06) |
| F-D | 断网本地充值 / 扣补贴 | #10 收 `LEDGER_OFFLINE`,按 `serverReceivedAt` 归日、UK `(gyid,type,flow_id)` 去重;日对账通过后出冲正快照 | §6 F5 | 主账唯一写入口 `platform-ledger` 不变;对账未结清禁止调账 |
| F-E | 上报历史与续报 | #14 心跳收 `pendingRetryBatches`;续报复用原 `idempotencyKey`,返回 `PL-IDEMPOTENT-REPLAY` 视为成功 | §5 F4 | 不新增接口编号、不产生新业务事实 |
| F-G | 保洁 / 排班 / 考核 | #13 收 `CLEAN_RECORD` / `STAFF_PERFORMANCE_DAILY` 日汇总,写 `pl_staff_performance_daily` 与 `PL_REPORT_DAILY` | §7 F6 | 平台只汇总,不重算公寓明细 |
| F-A | 平板派发与顺序执行 | #9 收 `TABLET_RESULT`、#8 回执作匹配输入,只记 `PL_TASK_ACK` / `PL_RECEIVE_LOG`;报表取 `PL_REPORT_DAILY.call_count`,不重算明细 | §4 F3、§7 F6 | 平台不生成任务行、不产出权威 `jcsj`(CR-C06);**#24 仅 SIP**;`UNVERIFIED` 不判完成,迟到 / 重复 `IGNORED` |
| F-B | SIP 语音叫乘 | #24 收 `VOICE_SUMMARY`(仅 SIP)、#13 归档;摘要按 `callMode` 分组 | §4 F3 | 平板链路不发 #24;录音不出公寓 |
| F-J | 人脸注册结果接收 | #9 `PERSON_REG` 去重后回**同步回执**(由公寓拉 #2 取回),媒体本体走短期签名直传 `person-photo` | §4 F3、§2 F1 | 不做准入审批;平台驳回不回滚本地注册(Q-35);业务抓拍 / 签到现场图 / 录音不出公寓 |
| F-L | 人员消费 | #10 `LEDGER_OFFLINE` 一条 `CONSUME` 流水按 `allowance_amount` / `cash_amount` 归集;`platform-ledger` **唯一写主账** | §6 F5 | `PF-5002` 业务重复 vs `PL-IDEMPOTENT-REPLAY` 幂等重放分流;`DIFF` + `PF-5003` 阻断调账 |
| F-M | 人员线下充值 | #10 `RECHARGE`(正数,`rechargeChannel=OFFLINE_CASH`)→ `PL_LEDGER_FLOW`;统计 `CASH_RECHARGE_INCOME` **计为收入** | §6 F5 | 不接支付渠道、不建订单与回调;充值不与消费抵销(CR-C37) |
| F-N | 公寓菜品管理 | `platform.catering` 写 `YKT_MEALS` 补列,#4 `recipes[]` 只维护当前有效版本 | §2 F1 | 改价 / 下架生成新 `recipe_version` 不改已发布版;`40940` 远程点菜不开放;`image_ref` 只作引用占位 |
| F-O | 三端状态对齐 | 四能力事件只归集、不回写公寓业务状态;映射唯一权威 **CR-02 §15 表 3-66** | §4 F3、§6 F5 | 平台零回调;统计不产生跨端副作用 |
| F-P | 周期统计生成与封账 | `platform-worker` 单活 → `platform-report` 由日报 + 流水 rollup 月 / 季 / 年五类指标 → `FINAL`;修正走 `revision_no+1` | §7 F6 | 未封账 `PF-7003`;`DIFF` → `BLOCKED`;缺报 `NO_DATA` 不推断不补零;`PF-7001` 不输出半成品 |
| F-Q | 公寓排名与报表输出 | `RANK()` 跳号生成五类 + 综合排名快照 → S-01 / S-02 查询 → S-06 异步导出 | §7 F6 | `gyIds[]` SQL 层带范围、`PF-7004` 越权;`PF-7005` 修订冲突;`PF-7002` 导出失败;不新增桶 |
**归日与顺序口径不变**:账务归日以 `serverReceivedAt` 为准;断网恢复后公寓端"先派 outbox、再拉 #7"(CR-01 §2.9);`count` 按 `biz_id` 去重(R-C3)。平台侧不得为上述新数据引入任何"平台 → 公寓"的实线箭头。
> **CR-02 §12 补遗(注册结果接收)**:人员注册由**签到终端申请并上传人脸**、**值班员在值班台确认注册**,注册成功后公寓端才经 #9 `PERSON_REG` 上报;平台 `platform-ingest` 校验工号唯一性与 `checksum` 后纳入跨公寓主档并回同步回执(由公寓拉 #2 取回),**不做注册准入审批、不决定公寓侧是否生效**(CR-C25)。人员主数据照片与特征值按 CR-C24 允许跨端,媒体本体走短期签名直传、落平台对象存储,平台库只存引用与校验和;业务抓拍与录音仍不出公寓。公寓侧"上传平台"与"下发公寓内设备"两条路径解耦并行,平台不可达不阻塞设备下发;平台仍零回调、不新增接口编号。
> **CR-02 §13 补遗(入住 → 匹配 → 叫乘)**:入住作业五环节中平台只承担"接收 #9 `CHECKIN`、接入运安计划、产出匹配结论、经 #7 给出任务增量";`apt-stay` 在值班员审核通过时才建 `gy_jcxx` 行(申请阶段不建行),`applyId` ↔ `jcid` 一对一。平台不生成任务行、不产出权威 `jcsj`、不回调公寓;`jcsj` 由公寓按 `apt_master_rule` 复算后经 #8 回执带回 `computedCallTime`。`match_state` 沿用五值,`UNMATCHED` / `LOCKED` 保留公寓要素不清空(D8')。完整链路见 `docs/公寓端/11` §7、§8。
> **CR-02 §14 补遗(F-L / F-M 账务归集、F-N 食谱下行)**:消费 / 充值由公寓经 #10 `LEDGER_OFFLINE` 逐条上报,`platform-ingest` 按 `idempotencyKey` 去重落 `PL_RECEIVE_LOG`,`platform-ledger` 按 `serverReceivedAt` 归日、以业务 UK `(gyid, type, flow_id)` 去重并**唯一写入口**更新 `GY_YKT_REMAINDER` / `PL_LEDGER_FLOW`;日对账按 `count` / `sum(amount)` / `max(watermark)` 三维比对,差异落 `PL_RECONCILE_DIFF` 且 `PF-5003` 阻断调账,冲正只走 `POST /adjustments`。F-N 食谱由 `platform.catering` 发布并按 #4 `recipes[]` 下行,平台**不推、不回调**;平台不接支付渠道、不建支付订单与回调、不实现远程点菜。完整链路见 `docs/公寓端/12` §7、表 12-27 / 12-28。
> **CR-02 §15 补遗(三端一致性与统计异常分支)**:四能力与统计链路的异常处理按 CR-02 §15 表 3-65 / 3-66 与 `docs/平台端/10` 表 10-18 收口:平板 `UNVERIFIED` / `SKIPPED` 保持待确认 + 复叫(Q-22),迟到 / 重复 `IGNORED`,重复 ACK `40916`;人脸注册平台 `REJECTED` 只告警 + 待办、**不回滚**本地(Q-35),媒体类 `40917`~`40924` / `50303` 不自动重试;账务 `DIFF` + `PF-5003` 阻断调账、冲正只走 `POST /adjustments`,`PF-5002` 业务重复与 `PL-IDEMPOTENT-REPLAY` 幂等重放分流;食谱 `40936` / `PF-6001` 要求终端重拉、改价不改已发布版;统计未封账 `PF-7003`、`gyIds[]` 越权 `PF-7004`、修订冲突 `PF-7005`、导出失败 `PF-7002`,缺报 `NO_DATA` **不推断不补零**。平台对以上异常**一律不回调公寓、不反向补数、不重算明细**,只记水位 / 日报 / 待办 / 审计;统计结果不改任何业务状态。
## 1. 流程总表与共同骨架
表 6-1 六条关键流程的触发、参与方与落点
| # | 流程 | 触发方式 | 参与方 | 涉及跨端接口 | 平台侧落表 | 本篇 |
| --- | --- | --- | --- | --- | --- | --- |
| F1 | 主数据下行 | 公寓按周期(默认 15 分钟)主动拉;或收到 #7 的 `replayHint` / 心跳回执中的 `masterStale` 标记后立即拉 | 公寓端 → 平台端 | #1、#2、#3、#4、#5、#23(全部下行) | `PL_MASTER_VERSION`、`PL_MASTER_SNAPSHOT`、`GY_*` 主数据 | §2、图 6-1、表 6-2 |
| F2 | 计划下行与任务生成 | 平台 `platform-worker` 定时(默认 5 分钟)抓运安;公寓按周期拉计划与变更 | 运安 SOAP → 平台端 → 公寓端 | #6、#7(下行) | `GY_SYN_JCXX`、`PL_TASK`、`PL_TASK_CHANGE` | §3、图 6-2、表 6-3 |
| F3 | 任务回执与执行结果上报 | 公寓侧事件驱动(收到任务、叫乘完成、入寓、离寓) | 公寓端 → 平台端 | #8、#9(上行) | `PL_RECEIVE_LOG`、`PL_TASK_ACK`、`PL_TASK` | §4、图 6-3、表 6-4 |
| F4 | 水位 / 对账 / 重放 | 心跳周期(默认 1 分钟)+ 日切 + 人工触发 | 公寓端 ↔ 平台端 | #14、#16、#17、#7 | `PL_WATERMARK`、`PL_RECONCILE_BATCH`、`PL_RECONCILE_DIFF`、`PL_REPLAY_JOB` | §5、图 6-4、表 6-5 |
| F5 | 账务归集与日对账 | 每笔业务事件(消费 / 充值 / 补助)+ 每日日切 | 公寓端 → 平台端 → 财务 | #10(上行) | `PL_LEDGER_FLOW`、`PL_BALANCE_SNAPSHOT`、`GY_YKT_*` | §6、图 6-5、表 6-6 |
| F6 | 跨公寓报表汇总 | 平台定时(每小时增量 / 每日全量);管理端按需查询 | 公寓端 → 平台端 → 管理端 | #11、#12、#13、#24、#25 | `PL_RECEIVE_LOG`、`PL_REPORT_DAILY`、`V_CALL_QUALITY` | §7、图 6-6、表 6-7 |
**四条共同骨架**(六条流程都遵守,后续各节不再重复展开):
1. **连接方向单向**:所有跨端 HTTP 连接由公寓端发起。平台端只做“应答”(下行 `GET` 返回数据、上行 `POST` 返回逐条结果),不存在平台主动建连。
2. **平台侧重试只对“外部系统”生效**:运安 SOAP、中心汇总、财务、对象存储。**平台不重试公寓**(`05` 表 5-5)。
3. **每个写动作都有幂等键**:上行用 `Idempotency-Key`(批量用 `batchId`),下行读无幂等键但用 `version` 定序,平台内部任务用 `(gyid, biz)` 业务唯一键。
4. **每个失败都有归属**:能自动补的进重试队列(上限 1 小时,1s / 5s / 30s / 2min / 10min + 抖动),补不了的落 `DEAD` 进人工队列(`PL_RECONCILE_DIFF` 或 `PL_REPLAY_JOB`),不静默丢弃。
## 2. F1 主数据下行
**触发**:公寓端本地调度(默认 15 分钟)逐个拉 #1~#5、#23;或平台在 #7 响应体给出 `replayHint.masterStale = true`、或心跳回执带“主数据版本落后”标记时立即触发一次。
**参与方**:公寓端(发起方、落快照)、平台端 `platform-master` / `platform-person` / `platform-route`(应答方、版本台账)、对象存储(照片签名地址)。
图 6-1 主数据下行时序(全部为公寓主动拉取)
```mermaid
sequenceDiagram
autonumber
participant APT as 公寓端
participant GW as 平台网关
participant MS as platform-master/person/route
participant OBJ as 对象存储
APT->>GW: GET #1 /master/apartments/{gyId}
GW->>MS: 校验 X-Apt-GyId 与路径 gyId 一致
MS-->>GW: 档案 + 能力开关 + masterVersion 清单
GW-->>APT: 200 全量(小表直接整包)
APT->>GW: GET #2 /master/persons?gyId&cursor&version
GW->>MS: 按 cursor 取增量(version 之后)
MS-->>GW: items[] + nextCursor + hasMore
GW-->>APT: 200 分页增量(单页 ≤500 条)
APT->>GW: GET #3 /master/routes?gyId&from&to
APT->>GW: GET #4 /master/rules?gyId
APT->>GW: GET #5 /master/rooms?gyId&version
APT->>GW: GET #23 /open/v1/voice/config?gyId&configType
APT->>OBJ: 按 photoUrl(短期签名)下载照片本体
Note over APT,OBJ: 媒体不经平台转发,平台只给签名地址
APT->>APT: 本地事务落快照,写 lastMasterVersion
```
**时序要点**:
1. 每个域独立版本(`masterType` ∈ `APARTMENT` / `PERSON` / `ROUTE` / `RULES` / `ROOMS` / `VOICE`),公寓按域记录 `lastMasterVersion`,**不必全量重拉**。
2. 首拉(`lastMasterVersion` 为空)按全量返回;此后按 `version` 增量。平台对“非常旧的 `version`”返回全量(判定为重新初始化)。
3. 照片只回 `photoVersion` + `checksum` + 短期签名 `photoUrl`,**本体走对象存储直连**,不经平台应用层(`docs/10` §6.3 设计要点“跨端不带媒体”)。
4. `GY_ROOMS` 平台侧只提供**基线列**(`roomNo`、`bedNumbers`);房态、`live_person`、`sip_account` 属公寓事实,不在下行范围内(`03` 表 3-2)。
5. 删除语义一律为“逻辑删除标记”:平台下 `deleted = 1` 的条目,公寓只标删不改历史行(`03` 表 3-4 `GY_YKT_RY` 写入口纪律)。
表 6-2 F1 主数据下行的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| 公寓发起拉取 | 无幂等键(只读);同一 `(gyId, masterType, version)` 重复拉结果一致 | 平台 5xx / 超时 | 公寓侧按 1s/5s/30s/2min/10min 重试 | 断网期间公寓**继续用本地快照服务**,不阻塞叫乘 | — |
| 平台应答增量 | `version` 单调递增(P-06);同版本重发返回同一 `checksum` | 增量构造失败 | 平台回退为**全量返回**并记 `WARN`,不让公寓卡住 | 恢复后从 `lastMasterVersion` 续拉 | `PL_MASTER_VERSION` |
| 版本发布 | `publish` 带 `Idempotency-Key`,重复发布返回原结果 | 发布到一半实例不可用 | 版本台账与快照**同事务**写,未提交即整体不可见 | — | `PL_MASTER_VERSION`、`PL_MASTER_SNAPSHOT` |
| 快照落库 | 公寓侧按 `masterType` 整域替换(事务内) | 校验和不符 | 公寓丢弃本次并重拉;平台记“快照不一致”告警 | 恢复后重拉该域 | `PL_MASTER_SNAPSHOT` |
| 照片下载 | 按 `checksum` 去重,已存在不重下 | 对象存储不可用 | 平台侧重试(对对象存储重试是允许的);仍失败则本条 `photoUrl` 置空并置 `photoPending` | 恢复后公寓按 `photoPending` 补拉 | `GY_YKT_RY_ZP` + 对象存储 |
**不允许的简化**:为“让配置立刻生效”而在发布后主动调公寓刷新。平台不做推送(`01` 表 1-3 第 1 条)。
## 3. F2 计划下行与任务生成
**触发**:平台侧 `platform-worker` 定时(默认每 5 分钟,抓取窗口 4 小时,沿用 `docs/07` §2 现状口径)调用运安 SOAP 取乘务计划 → 落 `GY_SYN_JCXX` 快照 → 按规则版本生成 / 更新 `PL_TASK` 并追加 `PL_TASK_CHANGE`。公寓端按周期拉 #6(区间计划)与 #7(变更增量)。
**参与方**:运安 SOAP(数据源)、平台端 `platform-plan`(唯一写入口)、公寓端(消费方)。
图 6-2 计划下行与任务生成(含运安抓取与变更增量)
```mermaid
sequenceDiagram
autonumber
participant SCH as platform-worker 定时器
participant SOAP as 运安 SOAP
participant PL as platform-plan
participant DB as 平台主库
participant APT as 公寓端
SCH->>PL: 触发抓取(协调锁,单实例执行)
PL->>DB: 申请协调行(无则等待)
PL->>SOAP: getDriverPlanByTimeStr(now, now+4h)
SOAP-->>PL: 乘务计划条目
PL->>DB: upsert GY_SYN_JCXX(UK gyid+id+ryid)
PL->>DB: 按 rule_version 计算应叫时刻 → upsert PL_TASK
PL->>DB: 差异写 PL_TASK_CHANGE(op=UPSERT,version+1)
Note over PL,DB: 抓取失败只记 JWDATA_SYNC_LOG,不回滚已入库快照
APT->>PL: GET #6 /plans/duty?gyId&from&to
PL-->>APT: 该区间计划快照(按 planId 定序)
APT->>PL: GET #7 /tasks/changes?gyId&cursor
PL-->>APT: changes[](UPSERT/CANCEL)+ nextCursor + replayHint
APT->>APT: 本地生成 / 更新叫乘任务,回执走 #8
```
**时序要点**:
1. **抓取单实例**:沿用 `GY_CALL_DUTY_MATCH_LOCK` 协调锁,多实例部署时只有持锁实例抓取,其余跳过本轮;抓取成功/失败都写 `JWDATA_SYNC_LOG`(`docs/07` §2、§7)。
2. **平台侧只处理登记在案的公寓**:现状“只处理 `baseConfig.gyid`”改为“按 `PL_APARTMENT.state = REGISTERED` 逐公寓抓取”(P-08 未定前,先保持单公寓配置项兼容)。
3. **任务键**:`plan_key` = 运安计划条目的稳定业务键(`jobid` + `taskdicid` + `ryid` 派生),`PL_TASK` 以 `(gyid, plan_key)` 唯一,重复生成返回原任务。
4. **变更只增不改**:`PL_TASK_CHANGE` 只追加,`version` 单调;#7 按 `cursor`(= `change_seq`)增量返回,`CANCEL` 单独成条,不做“原地改状态”。
5. **平台侧人工改时刻**只允许在 `PLANNED` / `ACKED` 状态(`04` 表 4-7),已终态改必返 `PF-4003`。
6. **`replayHint` 是唯一“平台表达意愿”的通道**:平台需要公寓补做某件事时,只能写进 #7 的响应体,由公寓下次拉取时自行决定(见 §5)。
表 6-3 F2 计划下行与任务生成的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| 运安抓取 | 协调锁 + 窗口幂等(同窗口重复抓取结果一致) | SOAP 超时 / 返回空 | 记 `JWDATA_SYNC_LOG`,本轮跳过;进入下一轮,**不补抓历史窗口**(历史窗口由 #6 区间查询覆盖) | — | `JWDATA_SYNC_LOG`、`GY_CALL_DUTY_MATCH_LOCK` |
| 计划快照落库 | UK `(gyid, id, ryid)` 修现状“同 id 多司机 MERGE 冲突” | 主键冲突 | upsert 覆盖;冲突行落 `GY_SYN_JCXX_HIS` 留痕 | — | `GY_SYN_JCXX`、`GY_SYN_JCXX_HIS` |
| 任务生成 | `(gyid, plan_key)` 唯一 + `Idempotency-Key` | 规则版本缺失 | 返回 `PF-4004`,该 `gyId` 本轮不生成(不猜默认规则) | — | `PL_TASK` |
| 变更增量 | `(gyid, task_id, version)` 唯一,`version` 单调 | 公寓 `cursor` 过期(平台已归档) | 平台回 `PL-CURSOR-EXPIRED`,公寓改走 #6 全区间重拉 | 恢复后按 `cursor` 续拉;`cursor` 失效走 #6 | `PL_TASK_CHANGE` |
| 公寓消费变更 | 按 `version` 幂等(同版本重复应用结果一致) | 本地应用失败 | 不推进 `cursor`,下次重拉同批 | 断网期间用本地已下发任务继续叫乘 | `PL_TASK_CHANGE` |
## 4. F3 任务回执与执行结果上报
**触发**:公寓侧事件驱动——收到 / 拒绝任务时发 #8;叫乘完成、入寓、离寓、超时未接时发 #9;`handled_result` 或终态变化时 `bizVersion + 1` 重发。
**参与方**:公寓端(发起方)、平台端 `platform-ingest`(接收与幂等)、`platform-plan`(状态推进)。
图 6-3 回执与执行结果上报(上行,含幂等判重与部分成功)
```mermaid
sequenceDiagram
autonumber
participant APT as 公寓端
participant IG as platform-ingest
participant DB as PL_RECEIVE_LOG
participant PLAN as platform-plan
APT->>IG: POST #8 /ack/tasks(batchId + items[],≤500 条 / ≤1MB)
IG->>DB: 逐条查 (gyid, idempotency_key)
alt 命中幂等
DB-->>IG: 原 resultCode + 原 serverReceivedAt
IG-->>APT: 200 items[].resultCode = PL-IDEMPOTENT-REPLAY
else 新键
IG->>PLAN: 写 PL_TASK_ACK(computedCallTime vs 平台 jcsj)
PLAN->>PLAN: 任务状态 PLANNED → ACKED
IG->>DB: 落接收记录 + 推进 watermark
IG-->>APT: 200 items[].resultCode = 0
end
APT->>IG: POST #9 /report/duty-results(叫乘 / 入寓 / 离寓)
IG->>IG: 校验 bizVersion 不倒退
IG->>PLAN: 状态推进(ACKED → CALLED / ENTERED / LEFT)
IG-->>APT: 200 逐条结果
Note over APT,IG: 平台不回头问“这条到底成没成”,一切以 items[] 结果为准
```
**时序要点**:
1. **按条回报、允许部分成功**(F-04 未定前的实现口径):不整批回滚,`items[]` 逐条给 `resultCode`;公寓只对失败条目重试。
2. **`serverReceivedAt` 由平台生成**,公寓传入即忽略(`05` 表 5-4);账务日切以它为准。
3. **乱序处理**:同一 `bizId` 只接受递增 `bizVersion`,旧版本返回 `PL-VERSION-STALE` 且**不执行业务副作用**(不写 `PL_TASK`,只落 `PL_RECEIVE_LOG` 留痕)。
4. **重放防护**:批次 `sentAt` 超出 ±5 分钟窗口返回 `PL-REPLAY-WINDOW` 并记审计(`05` §3 末段)。
5. **回执差异可见**:`PL_TASK_ACK.computedCallTime` 与平台 `jcsj` 的差值超阈值进 `GET /tasks/diff-report`(`04` 表 4-7),供规则版本调优,**不自动回写规则**。
6. **状态推进单向**:`PL_TASK.status` 六态只前进不回退;回退需求走人工修正任务(新增变更条),不直接改状态列(`03` 表 3-10)。
表 6-4 F3 回执与结果上报的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| #8 任务回执 | `idempotencyKey` + `(gyid, biz_type, biz_id, biz_version)` | 平台 5xx | 公寓按 1s/5s/30s/2min/10min 重试,超阈置 `DEAD` 进人工队列 | 公寓 outbox 落本地库,恢复后按 `watermark asc` 续传 | `PL_RECEIVE_LOG`、`PL_TASK_ACK` |
| #9 执行结果 | 同上;`bizVersion` 单调 | 乱序到达 | 旧版本回 `PL-VERSION-STALE`,公寓不重试(视为已过期) | 同上 | `PL_RECEIVE_LOG`、`PL_TASK` |
| 状态推进 | 单向六态,CAS(`where status = 期望值 and version = n`) | 并发两实例推进 | 失败方重读后重试一次;仍失败则放弃并告警(不回退) | — | `PL_TASK` |
| 重复批次 | `batchId` 相同视为同一批,返回原结果集合 | 批内部分重复键 | 先到先处理、后到返回原结果,**不整批拒绝**(`05` 表 5-3) | — | `PL_RECEIVE_LOG` |
| 长期积压 | 水位长期不前进 | 公寓长期离线 | 平台侧不主动联系;由管理端 `GET /admin/ops/apartments/{gyId}/health` 发现 | 恢复后按水位续传,平台按 `PL_REPLAY_JOB` 给 `replayHint` | `PL_WATERMARK` |
## 5. F4 水位 / 对账 / 重放
**触发**:心跳(默认 1 分钟,#14)携带 `sentAt`、`maxSeenWatermark`、`ackedWatermark`、时钟偏差;日切(每日 00:05 默认)由公寓发起 #16;人工在管理端判定差异后发起 #17。
**参与方**:公寓端(发起)、平台端 `platform-ingest` / `platform-ops`(接收与计算)、管理端(人工确认)。
图 6-4 水位推进、日对账与重放的闭环
```mermaid
sequenceDiagram
autonumber
participant APT as 公寓端
participant OPS as platform-ops
participant IG as platform-ingest
participant RC as PL_RECONCILE_BATCH
participant RJ as PL_REPLAY_JOB
APT->>OPS: POST #14 /heartbeat(gyId, sentAt, 水位, 时钟偏差)
OPS->>OPS: 按 (gyId, sentAt) 去重,更新健康度
OPS-->>APT: 200 + 主数据版本落后标记(若有)
APT->>IG: POST #16 /reconcile/run?gyId&date
IG->>RC: 建批次(RUNNING),三维统计平台侧值
IG->>RC: 比对公寓上报值 count / sum(amount) / max(watermark)
alt 三维全部一致
IG-->>APT: 200 一致,批次 CLOSED
else 存在差异
IG->>RC: 写 PL_RECONCILE_DIFF(MISSING / EXTRA / STALE)
IG-->>APT: 200 差异清单(供公寓侧重发)
Note over IG,RC: 差异同时进管理端人工队列
end
APT->>IG: POST #17 /replay(区间或幂等键)
IG->>RJ: 建重放作业(PENDING)
Note over IG,APT: 平台不主动投递,只把 replayHint 写进 #7 响应
APT->>IG: GET #7 /tasks/changes?gyId&cursor
IG-->>APT: 附 replayHint(需重发区间)
```
**时序要点**:
1. **水位三值**:`maxSeenWatermark`(公寓已产生最大序号)、`ackedWatermark`(平台已确认最大序号)、平台 `max_seen_watermark`(平台实际收到最大序号)。三者关系是排查漏报的唯一依据(`03` 表 3-8 `PL_WATERMARK`)。
2. **心跳不走批量信封**:`(gyId, sentAt)` 去重,重复心跳不产生新记录;时钟偏差超阈值记告警,但**不拒绝心跳**(否则公寓会误判自己被拉黑)。
3. **对账三维缺一不可**:`count` 判漏报、`sum(amount)` 判金额、`max(watermark)` 判卡死;只有 `count` 相等不算对账完成(`05` 表 5-6)。
4. **差异分类与处置**:`MISSING`(公寓有平台无)→ 公寓重发;`EXTRA`(平台有公寓无)→ 平台按 `LOGIC_DELETE` 标记并保留审计;`STALE`(版本倒退)→ 平台丢弃旧版并告警。
5. **重放的实现边界**:`PL_REPLAY_JOB` 只表达“这段时间需要重发”,通道仍是 #7 的 `replayHint`。**平台不得直连公寓触发重放**——这是 P-02 未定前不可跨越的线(`05` 表 5-8 方案 B 被标为“与方向纪律冲突”,依据是 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)。
6. **#16 / #17 方向待定(P-02)**:本期按“公寓主动调用平台”(方案 A)实现;若评审改为方案 C(双形态),只增加平台侧内部触发入口(`/platform/v1/admin/ingest/reconcile`,`04` 表 4-11),**不改变跨端编号**。
表 6-5 F4 水位、对账与重放的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| 心跳 | `(gyId, sentAt)` 去重 | 心跳丢失 | 平台按“最后心跳时间 + 阈值”判离线,不主动探活 | 恢复后首条心跳带回积压水位 | `PL_APARTMENT_HEALTH`、`PL_WATERMARK` |
| 水位推进 | 只增不减(回退返 `PF-8001`) | 水位跳变 | 记差异 + 告警,按 `max_seen` 与 `acked` 差值定位漏报区间 | 恢复后按 `ackedWatermark` 续传 | `PL_WATERMARK` |
| 日对账 | `(gyId, date)` 唯一,重复触发返回同批次 | 批次进行中再次触发 | 返回 `PF-8003`(不并发跑同一 `gyId + date`) | 断网期间由公寓侧记录待对账日期,恢复后补跑 | `PL_RECONCILE_BATCH` |
| 差异处置 | 按 `(gyId, date, biz_type, biz_id)` 唯一 | 差异长期未清 | 超过阈值(默认 3 个结算日)升级为 P0,禁止调账(`PF-5003`) | — | `PL_RECONCILE_DIFF` |
| 重放 | `(gyId, bizType, from, to)` 唯一,重复请求返回原作业 | 区间非法 | 返回 `PF-8002` 并回显合法区间边界 | 恢复后公寓按 `replayHint` 重发 | `PL_REPLAY_JOB` |
## 6. F5 账务归集与日对账
**触发**:每笔消费 / 充值 / 补助发放产生一条 #10 上报;每日日切后跑一次金额对账(`sum(amount)` 维度)。
**参与方**:公寓端(流水产生方)、平台端 `platform-ingest`(接收)、`platform-ledger`(主账唯一写入口)、财务系统(汇总下发)。
图 6-5 账务归集与日对账(流水上行 → 主账 → 快照下行)
```mermaid
sequenceDiagram
autonumber
participant APT as 公寓端
participant IG as platform-ingest
participant LED as platform-ledger
participant FIN as 财务 / 中心汇总
participant DB as PL_LEDGER_FLOW
APT->>IG: POST #10 /report/ledger(type, flowId, amount, occurredAt)
IG->>IG: 幂等判重 + bizVersion 校验
IG->>LED: 交给主账域(唯一写入口)
LED->>DB: 写流水(UK gyid+type+flow_id)
LED->>LED: 更新余额主账(同事务,CAS on version)
LED-->>IG: 成功 / 余额不足 PF-5001
IG-->>APT: 200 逐条结果
Note over LED,DB: 日切:以 serverReceivedAt 归日,不用 occurredAt
LED->>DB: 统计当日 sum(amount) 并按 biz_type 分组
LED->>LED: 与公寓侧 sum 比对,不一致即 P0
LED->>FIN: 汇总下发(日终)
FIN-->>LED: 回执确认
LED->>LED: 生成 PL_BALANCE_SNAPSHOT(供下行)
```
**时序要点**:
1. **主账只有一个写入口**:`platform-ledger` 是 `GY_YKT_REMAINDER`(平台库主账)与 `PL_LEDGER_FLOW` 的唯一写者;`platform-catering` 扣款必须走账务接口,不得直写余额(`03` 表 3-7 硬约束、`PF-6003`)。
2. **金额用字符串两位小数进出、库内定点数**:`numeric(12,2)` / `decimal(12,2)`,出参 `toPlainString()`,禁止浮点(`03` 表 3-16)。
3. **归日口径固定为 `serverReceivedAt`**:避免公寓本地时钟或补传导致跨日错账;`occurredAt` 只用作业时间轴(`05` 表 5-4)。
4. **对账未结清禁止调账**:当日 `sum(amount)` 不等即为 P0,`PF-5003` 阻断所有调账入口,直到 `PL_RECONCILE_DIFF` 清零。
5. **余额快照下行**(P-09 未定):现行方案是把对账后的余额作为 `#1` 档案响应的附属快照返回(不新增接口);若评审要求独立接口,只能走 v2 契约。
6. **重复流水**:`(gyid, type, flow_id)` 唯一,命中返回 `PF-5002`;但**幂等重放**(同 `idempotencyKey`)返回原结果,两者语义不同(前者是业务重复,后者是传输重试)。
表 6-6 F5 账务归集与日对账的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| 流水上报 | `idempotencyKey` + UK `(gyid, type, flow_id)` | 余额不足 | 返回 `PF-5001`,公寓侧置该笔失败并进人工 | 断网期间公寓本地先记账(本地强一致),恢复后按水位补传 | `PL_LEDGER_FLOW` |
| 余额更新 | CAS on `version`,失败即重读重算 | 并发扣款 | 乐观锁冲突后重试一次;再失败返回 `PF-5001` 语义 | — | `GY_YKT_REMAINDER` |
| 日对账 | `(gyId, date)` 唯一批次 | 金额不一致 | 全量冻结调账(`PF-5003`),差异进人工队列 | 恢复后补跑该日期批次 | `PL_RECONCILE_BATCH` |
| 财务汇总下发 | 按 `(gyId, date)` 幂等,重发覆盖同日结果 | 财务不可达 | 平台 worker 重试(对财务重试允许),上限 1 小时后 `DEAD` 并告警 | — | `PL_LEDGER_FLOW` 汇总视图 |
| 余额快照生成 | 按 `(gyid, snapshotDate)` 唯一 | 生成晚于公寓拉取 | 公寓下次拉取拿到新快照,幂等覆盖 | — | `PL_BALANCE_SNAPSHOT` |
## 7. F6 跨公寓报表汇总
**触发**:平台定时——每小时增量汇总 #11(设备状态)/ #12(报警)/#24(语音摘要),每日全量生成 `PL_REPORT_DAILY`;管理端按需读 #25 或报表接口。
**参与方**:公寓端(摘要生产者)、平台端 `platform-ingest` → `platform-report`、管理端(消费者)。
图 6-6 跨公寓报表汇总(摘要上行 → 平台加工 → 管理端只读)
```mermaid
sequenceDiagram
autonumber
participant APT as 公寓端
participant IG as platform-ingest
participant RPT as platform-report
participant ADM as 管理端
APT->>IG: POST #11 /report/device-status(按小时聚合)
APT->>IG: POST #12 /report/alarms(handled_result 变更时 bizVersion+1)
APT->>IG: POST #13 /report/archive(归档摘要与数据量)
APT->>IG: POST #24 /open/v1/voice/summary(语音质量摘要)
IG->>RPT: 落接收记录,投递汇总任务
RPT->>RPT: 每小时增量聚合,每日生成 PL_REPORT_DAILY
ADM->>RPT: GET /platform/v1/admin/report/**(跨公寓聚合,带 gyIds[])
RPT-->>ADM: 统计结果(来自 V_* 视图与日报表)
ADM->>RPT: GET #25 /platform/v1/voice/endpoints?gyId(平台自答)
RPT-->>ADM: 语音摘要(不查公寓)
```
**时序要点**:
1. **报表只读上行摘要,不回查公寓**:`platform-report` 的全部数据来源是 `PL_RECEIVE_LOG` / `PL_TASK` / `PL_LEDGER_FLOW` / `PL_REPORT_DAILY`。报告里任何一个数字都必须能追溯到某条上行记录(`03` 表 3-8)。
2. **#25 由平台自答**:公寓端不实现该接口(`docs/公寓端/09` 表 9-13);平台不得把它实现成“向公寓查询语音端点”。
3. **#11 天然收敛**:按小时聚合,同小时重复上报覆盖同一行;#12 报警在人工处置结果变化时 `bizVersion + 1` 重发,平台按版本取最新。
4. **跨公寓聚合必须在 SQL 层带 `gyId` 范围**:管理端未授权的 `gyId` 一律过滤,不允许“先查全量再在应用层过滤”(`03` 表 3-12 行级隔离)。
5. **归档摘要只用于容量与留存对账**,不含明细;平台不接收归档明细数据(`03` 表 3-15)。
6. **报表口径冻结**:`PL_REPORT_DAILY` 一旦生成不可改,修正走“次日重算 + 修订标记”,避免历史报表被静默篡改。
表 6-7 F6 跨公寓报表汇总的幂等、失败补偿与断网恢复
| 环节 | 幂等 / 定序 | 失败场景 | 补偿动作 | 断网恢复 | 落表 |
| --- | --- | --- | --- | --- | --- |
| 摘要上行 | `idempotencyKey`;#11 按小时天然收敛 | 平台 5xx | 公寓重试;#11 重试覆盖同小时行 | 断网后按水位补传,迟到数据仍归原小时 | `PL_RECEIVE_LOG` |
| 增量聚合 | 按 `(gyid, hour, metric)` 唯一 upsert | 某公寓数据迟到 | 下一轮聚合自动补入;日报生成前设置数据就绪检查(`PF-7001`) | 恢复后补跑该公寓该小时 | `PL_REPORT_DAILY` |
| 日报生成 | 按 `(gyid, reportDate)` 唯一,重复生成幂等 | 数据未就绪 | 返回 `PF-7001`,不生成半成品;等下一轮 | — | `PL_REPORT_DAILY` |
| 报表导出 | 天然幂等(只读);导出上限 `PF-7002` | 数据量大 | 转异步导出并给下载链接 | — | `V_*`、`PL_REPORT_DAILY` |
| 语音摘要汇总 | `idempotencyKey` | 摘要缺失 | 报表标“语音数据缺报”,不推断、不补零 | 恢复后补传 | `PL_RECEIVE_LOG` |
## 8. 六条流程的统一收口
1. **方向不可逆**:本目录六张时序图中,只有公寓 → 平台的实线;平台 → 公寓只以“响应体 + `replayHint`”形式出现。评审时以此作为“是否违反表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行”的检查方法。
2. **幂等键三层**:传输层 `Idempotency-Key` / `batchId`;业务层 `(gyid, type, flow_id)`、`(gyid, plan_key)`、`(gyid, task_id, version)`;内部层 `(gyid, date)` 批次。三层都要唯一约束,不能只靠应用判重。
3. **失败有归属**:可重试 → 1s/5s/30s/2min/10min + 抖动、上限 1 小时;不可重试 → `DEAD` + 人工队列;不可判定 → 进对账差异,由数据说话。
4. **断网只降级不丢事**:公寓用本地快照继续叫乘、本地先记账、outbox 落本地库;平台侧用“水位 + 对账”在恢复后收敛,不靠平台主动探活。
5. **与 `docs/公寓端/05` 的对称关系**:对端从“公寓视角”写同一批流程(本地任务生成、outbox、断网续传)。两侧对同一事件的幂等键、时间戳、状态机必须逐字一致,冲突时以 `05` 表 5-1 的契约编号为准并同步修订。
+211
View File
@@ -0,0 +1,211 @@
# 07 与外部系统的接口
> 追溯:`docs/10` §2.3 表 2-1(三端与外部系统调用关系)、§5.1 表 5-1(数据分层与来源)、§7(外部集成与定时任务)、
> §8.2 表 8-2(非功能要求)、表 8-5(录音存储)、§9(阶段五“外部集成的责任上收”)。
> 现状依据:`docs/07-外部集成与定时任务.md` §2(运安 SOAP)、§3(中心服务器 HTTP)、§5(Quartz)、§6(`@Scheduled`)、§8(排障)。
> 约束:平台端**只对“外部系统”重试,不对公寓端重试**(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行、本目录 `05` 表 5-5)。
本篇回答五个问题:**跟谁连、用什么协议与报文、多久调一次、怎么鉴权、挂了怎么办**。凡是现状里没有明确记录的项目,一律标注“待确认”,不按推测补全(见 `09` 的 PR 系列)。
## 1. 外部系统全景
图 7-1 平台端与外部系统的调用关系(含现有边界)
```mermaid
flowchart LR
subgraph EXT["外部系统(平台之上游)"]
SOAP["运安 SOAP<br/>乘务计划 / 司机信息"]
CENTER["中心汇总服务<br/>人员与账务下发"]
FIN["财务系统<br/>日终汇总"]
OSS["对象存储<br/>人员照片 / 导出文件"]
end
subgraph PL["平台端"]
PLAN["platform-plan"]
PERSON["platform-person"]
LEDGER["platform-ledger"]
RPT["platform-report"]
INFRA["platform-common<br/>HTTP 客户端 / 重试 / 审计"]
end
subgraph APT["公寓端(不在平台调用范围内)"]
A1["本地执行 / 录音 / 抓拍"]
end
SOAP -->|"平台主动调用"| PLAN
CENTER -->|"平台主动拉取"| PERSON
CENTER --->|"推送 / 平台接收"| PERSON
FIN <-->|"平台推送汇总"| LEDGER
PL <-->|"读写"| OSS
INFRA -.->|"统一超时 / 重试 / 熔断"| SOAP
INFRA -.-> CENTER
INFRA -.-> FIN
PL -.->|"反向不可达"| APT
A1 -.->|"录音 / 抓拍不出公寓"| A1
```
**两条硬边界**:
1. **平台 → 公寓的反向调用不存在**(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)。图中 `PL ⇢ APT` 用虚线并标“反向不可达”,是本篇唯一的负向约束。
2. **业务过程抓拍与录音不出公寓**(`docs/10` 表 6-3 设计要点第 5 条、表 8-5)。平台的对象存储**不承载**录音本体、叫乘抓拍与签到现场图,只承载**人员主数据**照片 / 特征值与导出报表文件;按 **CR-C24** / `docs/公寓端/10` §1.1,人员主数据人脸允许平台 ↔ 公寓双向同步,媒体本体走短期签名直传、公寓库内只存引用与校验和(CR-02 §12)。
表 7-1 外部系统清单与责任边界
| 外部系统 | 承担的事实 | 协议 | 平台侧消费模块 | 平台侧职责边界 | 依据 |
| --- | --- | --- | --- | --- | --- |
| 运安系统(SOAP) | 乘务计划、司机基础信息、司机交路 | SOAP 1.1 / WSDL | `platform-plan`、`platform-person` | 只读消费;不向其回写任何业务结果 | `docs/07` §2 |
| 中心汇总服务 | 人员全量、人员变更、财务数据 | HTTP(JSON / 表单) | `platform-person`、`platform-ledger` | 双向:拉取人员、推送变更通知与财务汇总 | `docs/07` §3 |
| 财务系统 | 日终账务汇总 | HTTP(JSON) | `platform-ledger` | 平台推送汇总,财务回执确认;平台不改财务凭证 | `docs/10` 表 5-1「账务数据」行 |
| 对象存储 | 照片本体、导出文件 | S3 兼容 / HTTP 签名 URL | `platform-person`、`platform-report` | 只存“平台拥有的对象”;不存录音、不存抓拍 | `docs/10` 表 8-5 |
| 短信 / 语音网关(若有) | 通知下发 | **待确认** | — | 现状未在平台侧使用;本期不引入 | `docs/07` §6 |
| NTP | 时钟同步 | NTP | 全端 | 平台侧与公寓侧各自同步;平台以自身时钟生成 `serverReceivedAt` | `05` 表 5-4 |
## 2. 运安 SOAP
### 2.1 接口与调用口径
表 7-2 运安 SOAP 的四个操作与平台侧调用口径
| 操作 | 用途 | 关键入参 | 平台侧调用频率 | 平台侧落点 | 失败降级 |
| --- | --- | --- | --- | --- | --- |
| `getDriverPlanByTimeStr` | 取区间乘务计划 | 起止时间(窗口 4 小时,每 5 分钟一轮) | 每 5 分钟一轮;每次覆盖 `now ~ now+4h` | `GY_SYN_JCXX`、`PL_TASK` | 记 `JWDATA_SYNC_LOG`,本轮跳过;下一轮自然补回(滚动窗口重叠 4 小时) |
| `getDriverInfoByWorkNo` | 按工号取司机信息 | 工号 | 人员增量时按需(每 5 天全量校准) | `GY_YKT_RY`(只补差异字段) | 该人员本轮跳过并标 `personPending`,不阻断其他人 |
| `getDriverInfoBySectionCode` | 按区段取司机信息 | 区段码 | 与人员全量校准同周期 | `GY_YKT_RY`、`GYGL_JW_RY` | 同上 |
| `getLocoHomeList` | 取机务段 / 归属列表 | 无或段码 | 低频(默认每日一次) | `GY_GZZD`、`ZD_DW`、`zd_zqmc` | 沿用上次成功结果,记告警 |
**WSDL 来源**:`classpath:/wsdl/PbResource.wsdl`(现状在 `yktgl` 内,拆分后归属 `platform-plan` 的 resources)。平台侧不改 WSDL,不在运行时动态解析远端 WSDL(避免上游改契约直接打挂平台)。
### 2.2 报文与字段
表 7-3 运安计划的关键字段与平台侧口径
| 运安字段 | 平台落点 | 平台侧处理 | 注意 |
| --- | --- | --- | --- |
| `jobid` + `taskdicid` | `GY_SYN_JCXX.id`(组合键) | 原样拼接为稳定业务键 | 单条计划可能对应多司机,**不能只按 `id` 去重** |
| `ryid`(工号) | `GY_SYN_JCXX.ryid`、`PL_TASK.ryid` | 原样 | 与 `GY_YKT_RY.ryid` 对齐;对不上则进待匹配队列 |
| `cc`(车次) | `GY_SYN_JCXX.cc`、`PL_TASK` | 原样 | 与交路模板 `GYGL_JLXX.cc` 匹配 |
| `jch`(交路号) | `GY_SYN_JCXX.jch` | 原样 | 交路模板匹配键之一 |
| `kcsj`(开车时间) | `GY_SYN_JCXX.kcsj` | 原样(本地时区) | 与规则计算出的叫乘时刻分开存放 |
| `jcfs`(叫乘方式) | `GY_SYN_JCXX.jcfs` | 原样 | 影响是否生成叫乘任务 |
| `h1`(备用 / 房间相关) | `GY_SYN_JCXX.h1` | 原样 | 现状语义见 `docs/07` §2 字段映射 |
| `write_time` | `GY_SYN_JCXX.write_time` | 原样 | 上游数据版本参考;平台 `source_version` 以此派生 |
| (空值 / 空串) | 各列 | **统一归一为空**(`03` 表 3-16 方言纪律) | 上游常回 `""` 而非 `null`,直接入库会导致唯一键失效 |
字段级映射的完整 9 条沿用 `docs/07` §2,平台侧不改口径;如需补字段,走 `03` 的 P-05 评审(不得顺手加列)。
### 2.3 鉴权、频率与并发
表 7-4 运安接入的鉴权、频率与并发保护
| 项 | 现状 | 平台侧设计 | 待确认 |
| --- | --- | --- | --- |
| 鉴权 | 内网直连,`docs/07` §2 未记录 WS-Security 凭据 | 沿用“内网白名单 + 网络层隔离”,不新增 SOAP 层凭据;若上游要求凭据,凭据只入配置中心 / 环境变量,**不入库不入库表** | PR-05 |
| 频率 | 计划每 5 分钟、人员每 5 天(`docs/07` §2) | 频率做成配置项,最小粒度 1 分钟;多公寓时**按公寓串行,不并发打上游** | — |
| 并发保护 | `GY_CALL_DUTY_MATCH_LOCK` 协调锁 | 沿用协调锁;持锁方抓取,其余实例跳过本轮(`06` §3) | — |
| 窗口 | 4 小时滚动 | 保持 4 小时;窗口重叠确保漏一轮也能补回 | — |
| 多公寓 | 现状只处理 `baseConfig.gyid` | 改为按 `PL_APARTMENT.state = REGISTERED` 逐个处理;租户级上游参数从公寓档案取 | P-08 |
| 超时 | 现状无统一超时(`docs/07` §8 排障项) | 统一连接 3s / 读 10s(与跨端口径一致),整体请求上限 30s | — |
### 2.4 失败与降级
1. **上游不可达**:本轮记 `JWDATA_SYNC_LOG`(结果=`FAILED` + 异常摘要),**不回滚已入库的计划**,不阻断公寓拉取(公寓拿到的仍是上一轮已生成的版本)。
2. **上游返回空**:视为“该窗口无计划”,不视为失败;但连续 3 轮为空时告警(可能是上游故障伪装成空集)。
3. **上游字段缺失**:整条跳过并记“不可用条目”,不落半条记录(避免 `PL_TASK` 生成半截任务)。
4. **重试策略**:只对网络类错误重试(最多 3 次,5s / 30s / 2min),业务错误不重试。**不进入 1 小时长重试队列**——因为滚动窗口本身就会自然重试(`06` 表 6-3)。
5. **不向运安回写**:平台对运安是只读单向;任何“回传叫乘结果”的需求一律拒绝(运安不是执行结果的接收方)。
## 3. 中心汇总服务与财务
### 3.1 接口清单
表 7-5 中心汇总 / 财务接口与平台侧调用口径
| 接口(现状路径) | 用途 | 方向 | 平台侧消费模块 | 频率 | 失败降级 |
| --- | --- | --- | --- | --- | --- |
| `GET /api/users?pagenum=N` | 分页拉取人员全量 | 平台拉 | `platform-person` | 每 5 天全量校准(分页拉全) | 断点续页;失败从当前页重试 |
| `GET /synData/getApartmentPersonData*` | 拉取公寓相关人员数据 | 平台拉 | `platform-person` | 与全量校准同周期 | 该公寓本轮跳过并告警 |
| `POST /synData/modifyPersonData` | 推送人员变更通知 | 平台推 | `platform-person` | 事件驱动(人员新增 / 变更 / 删除后) | 进平台重试队列(1s/5s/30s/2min/10min,上限 1 小时) |
| `POST /synData/synFinanceData` | 推送财务汇总数据 | 平台推 | `platform-ledger` | 日终一次(与日对账同一批次) | 重试后仍失败 → `DEAD` + 告警 + 人工补推 |
**现状问题(必须在平台侧修掉)**:`docs/07` §3 明确记录“无统一超时重试”。平台侧统一收口到 `platform-common` 的 HTTP 客户端:默认连接 3s / 读 10s、幂等接口可重试、非幂等接口靠 `requestId` 去重(对应 PR-06)。
### 3.2 鉴权与凭据
表 7-6 外部接口的鉴权方式与凭据管理
| 系统 | 现状鉴权 | 平台侧做法 | 禁止 |
| --- | --- | --- | --- |
| 运安 SOAP | 内网直连(未见显式凭据) | 网络白名单;凭据(若有)走配置中心 | 把内部地址与口令写进库表(`docs/09` §1 P0 第 2 项) |
| 中心汇总 | 现状未见 token 机制 | 若上游提供令牌,放环境变量;无令牌则依赖内网隔离 | 同上 |
| 财务 | 待确认 | 同上 | 同上 |
| 对象存储 | AccessKey / 临时签名 | AccessKey 只进配置中心;对外只发短期签名 URL | 把 AccessKey 写进前端或接口出参 |
**统一要求**:所有外部凭据满足三条——① 只存配置中心 / 环境变量,不进库、不进代码、不进日志;② 轮换不影响正在跑的实例(热加载);③ 每次外部调用落 `PL_AUDIT_API`(只记请求标识与耗时,不记凭据与完整报文)。
### 3.3 报文形态与幂等
1. **请求形态**:中心汇总为 `application/json`;若上游只支持表单(`application/x-www-form-urlencoded`),在客户端做适配层,**不把表单格式扩散到业务代码**。
2. **幂等**:推送类接口(`modifyPersonData`、`synFinanceData`)必须带 `requestId`(= 平台侧业务键 + 版本),重试时复用同一 `requestId`;拉取类接口天然幂等。
3. **响应约定**:只认“HTTP 2xx + 业务码成功”为成功;HTTP 2xx 但业务码失败**不重试**(属业务拒绝,重试无意义)。
4. **数据归属**:中心汇总是“人员与财务数据的上游”,但平台库是**平台自己的真源**——上游给的数据经平台规则加工后才下行给公寓,**不允许公寓直连中心汇总**。
## 4. 对象存储
### 4.1 桶划分与内容边界
表 7-7 对象存储的桶划分与内容边界
| 桶 / 前缀 | 内容 | 生产者 | 消费者 | 保留期 | 是否跨端 |
| --- | --- | --- | --- | --- | --- |
| `person-photo` | 人员照片本体 | `platform-person` | 公寓端(按签名 URL 直连下载) | 长期(随人员生命周期 + 归档期) | 是(只给 URL,不转发本体) |
| `report-export` | 报表导出文件 | `platform-report` | 管理端 | 短期(默认 7 天,可配) | 否 |
| `apt-capture` | 叫乘抓拍 | **公寓端本地**(不上平台) | 公寓端 | 由公寓端留存策略决定 | 否 |
| `apt-recording` | 录音本体 | **公寓端本地**(不上平台) | 公寓端 | 由公寓端留存策略决定 | 否 |
| `voice-index` | 录音索引摘要 | 公寓端上报(#24 只带上游摘要,不带对象) | 平台只存摘要字段 | 与摘要同期 | 否(索引不进对象存储) |
**边界声明**:`apt-capture` / `apt-recording` 出现在平台侧桶清单中即为边界破坏。平台侧的对象存储**只有两个桶**:`person-photo`、`report-export`。
> **CR-02 §14 补遗(食谱图片引用)**:公寓菜品管理(CF-03)的 `YKT_MEALS.image_ref` **只是引用占位**,本期不上传 / 不下载图片本体、不新增任何对象存储桶;#4 `recipes[]` 下行也只带引用字符串。平台侧仍只有上述两个桶,人员主数据人脸按 CR-C24 的跨端许可不变,业务抓拍、签到现场图与录音仍不出公寓。权威表述见 `docs/公寓端/12` §3、§6。
> **CR-02 §15 补遗(统计导出与媒体边界)**:公寓统计报表导出复用既有 `report-export` 桶与短期签名(默认 7 天,§4.3),**不新增对象存储桶、不放长签名、不返回内网绝对路径**;平台侧仍只有 `person-photo` / `report-export` 两个桶。四能力上行归集沿用 #9 / #10 / #12 / #13,不新增外部集成;#13 `ARCHIVE` 摘要 `payload` 扩展 `satisfaction{}` / `roomOccupancy{}` **分组计数**(CR-C39),平台**只接计数不接明细**、不接业务抓拍 / 签到现场图 / 录音。平台零回调不变,统计结果不反向下发公寓;权威表述见 `docs/平台端/10` §5、表 10-19。
### 4.2 调用形态与签名
1. **读**:平台生成短期签名 URL(默认 10 分钟,可配),公寓端与浏览器**直连对象存储下载**,不经平台应用层转发。
2. **写**:平台内部上传(服务端 SDK),对象键 = `{gyid}/{ryid}/{photoVersion}.{ext}`,与 `GY_YKT_RY_ZP.storage_key` 一一对应。
3. **去重**:以 `checksum`(SHA-256)为去重键,同 `checksum` 复用旧对象、只更新版本号(`04` 表 4-5 `POST /persons/{ryid}/photo`)。
4. **一致性**:先写对象、再写库;库写失败时对象成为孤儿,由每日巡检清理(孤儿对象保留 7 天后删除,可配置)。
### 4.3 失败与降级
| 场景 | 处理 | 影响 |
| --- | --- | --- |
| 上传失败 | 平台重试 3 次(5s / 30s / 2min);仍失败则本次照片更新整体回滚(库不写 `photo_version`) | 人员照片停留旧版本,公寓继续用旧照片(不阻塞叫乘) |
| 签名 URL 生成失败 | 返回接口时 `photoUrl` 置空 + `photoPending = true` | 公寓按 `photoPending` 下次补拉 |
| 对象存储不可用 | 只影响照片与导出;**不影响叫乘、账务、对账** | 降级为“无照片”视图 |
| 对象被误删 | 按 `checksum` 从源系统(中心汇总)重取;无源则标 `photo_missing` 并告警 | 需要人工介入(PR-07) |
## 5. 定时任务与外部调用的对应关系
表 7-8 平台侧定时任务与外部系统的映射
| 任务 | 现状归属 | 平台侧归属 | 频率 | 外部依赖 | 并发保护 |
| --- | --- | --- | --- | --- | --- |
| 运安计划抓取 | `yktgl` `@Scheduled` | `platform-worker`(`platform-plan`) | 每 5 分钟 | 运安 SOAP | 协调锁 |
| 运安人员校准 | `yktgl` `@Scheduled` | `platform-worker`(`platform-person`) | 每 5 天 | 运安 SOAP | 同一把锁(与计划不并发) |
| 中心人员全量 | `yktgl` `@Scheduled` | `platform-worker`(`platform-person`) | 每 5 天 | 中心汇总 | 单实例执行 |
| 财务汇总推送 | `yktgl` `@Scheduled` | `platform-worker`(`platform-ledger`) | 日终 | 财务 | 按 `(gyid, date)` 幂等 |
| 主数据版本清理 | 无 | `platform-worker`(`platform-master`) | 每周 | 无 | 只清理超期草稿,不删已发布版本 |
| 报表日报生成 | 部分在 `yktgl` | `platform-worker`(`platform-report`) | 每小时增量 / 每日全量 | 无(只用平台库) | 按 `(gyid, reportDate)` 幂等 |
| 归档摘要汇总 | 无 | `platform-worker`(`platform-report`) | 每日 | 无 | 只读 |
**Quartz vs `@Scheduled`**:现状两套并存(`docs/07` §5 / §6)。平台侧只保留一套(推荐 `@Scheduled` + 数据库协调锁,理由:集群规模小、无需动态调度界面),另一套在阶段零下线(`02` 表 2-5)。
## 6. 外部集成的统一纪律
1. **单向只读优先**:能只读的绝不写(运安全程只读);必须写回的(`modifyPersonData`、`synFinanceData`)必须带 `requestId`。
2. **超时是所有外部调用的强制项**:无超时的外部调用等于给平台留一个挂死点(PR-06)。默认 3s / 10s,特殊接口单独配并写明理由。
3. **重试只对幂等 + 可重试错误**:网络错误、5xx、超时重试;4xx 与业务拒绝不重试。
4. **熔断与隔离**:任一外部系统连续失败达阈值(默认 10 次 / 5 分钟)打开熔断,后续调用快速失败并降级,避免线程池被拖死。
5. **凭据三不**:不入库、不进代码、不进日志(表 7-6)。
6. **外部系统的数据不是“平台真源”**:上游数据经平台规则加工后才成为可下行数据;`GY_SYN_JCXX` 是**平台自己的快照表**,不是运安库的镜像。
7. **责任边界一句话**:平台对“取到的数据是否被正确加工、是否被正确下发、是否被正确对账”负责;对“上游数据本身是否正确”只负责发现与告警,不负责修正。
+191
View File
@@ -0,0 +1,191 @@
# 08 部署与运行
> 追溯:`docs/10` §8.1 表 8-1(三端部署形态)、§8.2 表 8-2(非功能要求)、表 8-3(单机资源)、表 8-4(端口)、
> 表 8-5(录音存储)、§5.6.3 表 5-8(平台端数据库三选项)+ 表 5-9(切换前必查清单)、§10.2 表 10-2(仅 D13:平台端是否一并改 MySQL)。
> 说明:平台端 HA 策略在 `docs/10` 表 10-2 中**无对应决策点**(D16 是公寓端 Asterisk 高可用),本篇登记为新增待决点 **P-11**(`09` 表 9-1)。
> 现状依据:`docs/07` §8(排障)、`docs/09` §2(改动落点速查)、`docs/frontend/07-构建部署与风险清单.md`。
> 提示:本篇的 `表 8-1` 指**本目录** `08` 篇的第 1 张表;引用 `docs/10` 的表时一律写全“`docs/10` 表 8-1”。
**本篇不生成任何配置文件**(约束:本轮只产出设计文档)。所有配置项只给“分组、来源、是否必须外置、变更是否需要重启”,不给具体 `application.yml` 片段。
> **CR-02 §15 补遗(部署与运行)**:四能力上行归集与统计周期汇总的生成调度归 `platform-worker` **单活**(§1),`platform-api` 不持有定时任务;两类任务共用既有 `PL_JOB_*` 任务表与库级协调锁,**不引入消息中间件、不新增部署单元或端口**。异步报表导出 > 10 min 按 `gyid` 拆批(§4);worker 双跑由协调锁防重,`Idempotency-Key` 重复返回原 `jobId`;`S-03` 未封账 `PF-7003`、`S-06` 导出失败 `PF-7002`。权限点 `platform:stat:*` 与 `gyIds[]` 数据范围随会话解析,配置项仍只给分组 / 是否外置 / 是否重启,不新增配置片段;统计能力不改变平台端 HA 待决点 P-11 的范围。权威表述见 `docs/平台端/10` 表 10-17 / 10-18。
## 1. 部署形态
图 8-1 平台端的部署拓扑与外部依赖
```mermaid
flowchart TB
subgraph EDGE["接入层"]
NG["Nginx / 网关<br/>443 入站 + admin 前缀隔离"]
end
subgraph APP["应用层(无状态,可水平扩)"]
A1["platform-api-1"]
A2["platform-api-2"]
W["platform-worker(单活)"]
end
subgraph DATA["数据层"]
DB[("平台主库<br/>KingbaseES 或 MySQL(D13:短期 A)")]
RD[("Redis<br/>会话 / 限流 / 锁")]
OSS[("对象存储<br/>person-photo / report-export")]
end
subgraph UP["上游"]
SOAP["运安 SOAP"]
CEN["中心汇总"]
FIN["财务"]
end
NG --> A1
NG --> A2
A1 --> DB
A2 --> DB
W --> DB
A1 --> RD
A2 --> RD
W --> RD
A1 --> OSS
W --> SOAP
W --> CEN
W --> FIN
NG -.->|"仅 /platform/v1/admin/**"| ADM["管理端浏览器"]
NG -.->|"仅 /platform/v1/** + /open/v1/**"| APT["公寓端(出站发起)"]
```
表 8-1 平台端部署单元与伸缩方式
| 部署单元 | 数量(本期基线) | 是否有状态 | 伸缩方式 | 说明 |
| --- | --- | --- | --- | --- |
| 网关 / Nginx | 2(主备) | 无 | 前置 LB | 按前缀把 `/platform/v1/admin/**` 与跨端前缀分流;跨端前缀只对公寓网段 / 白名单开放 |
| `platform-api` | 2 | 无 | 水平加实例 | 会话进 Redis;不存本地文件;不持有定时任务 |
| `platform-worker` | 1(**单活**) | 无(状态在库) | 主备切换 | 运安抓取、报表生成、重试队列、重放作业全部在这里;靠数据库协调锁保证不双跑 |
| 平台主库 | 1 主 + 1 备(待决 P-11) | 有 | 主备 / 只读副本(待决) | 选型见 §6;**本期不定稿** |
| Redis | 1 主 + 1 备 | 有(可丢) | 主备 | 只放会话、限流计数、短期锁;**不作为业务真源** |
| 对象存储 | 外部服务 | 有 | 由服务方保证 | 只用两个桶(`07` 表 7-7) |
**三条形态约束**(与 `02` §1 一致,此处给出运维侧含义):
1. **只被调用不主动调用公寓**:平台侧防火墙**不配置**任何指向公寓内网的出站规则;运维巡检时以“是否存在指向公寓的 outbound 规则”作为违规检查项。
2. **应用层无状态**:`platform-api` 可随时被杀掉重启,不依赖本地磁盘;日志写 stdout + 集中采集,不在本地留业务日志。
3. **不持有现场数据**:平台侧磁盘与对象存储**不得出现**录音、抓拍文件;容量规划按“文本 + 照片 + 报表导出”估算。
## 2. 非功能要求
表 8-2 平台端非功能指标(沿用 `docs/10` 表 8-2 口径,补平台侧可测项)
| 维度 | 指标 | 平台侧可测方式 | 备注 |
| --- | --- | --- | --- |
| 下行可用性 | 月度 ≥ 99.5% | 网关 5xx 比例 + 网关可用性 | 公寓断网时平台仍须可用,不能因公寓离线而自身降级 |
| 下行时延 | `GET` 主数据 P95 ≤ 300ms(单页 ≤500 条) | 应用埋点 + 慢查询日志 | 全量首拉允许 P95 ≤ 2s |
| 上行吞吐 | 单公寓峰值 20 req/s,平台侧合计按公寓数线性预留 | 网关 QPS + `PL_RECEIVE_LOG` 写入速率 | 批量 ≤500 条 / ≤1MB,单条处理 ≤ 5ms |
| 上行时延 | 接收接口 P95 ≤ 200ms(不含落库后异步加工) | 埋点 | 加工走异步,不阻塞应答 |
| 幂等命中率 | 可观测(命中不报错) | `PL-IDEMPOTENT-REPLAY` 计数 | 命中率异常高说明公寓重试配置有误 |
| 报表生成 | 日全量 ≤ 10 分钟(按 20 公寓预算) | worker 任务耗时 | 超时则拆批 |
| 数据量 | 保留期内主库 ≤ 500GB(按 20 公寓 / 3 年预算) | 库容量监控 | 超限触发归档策略复核(`03` 表 3-15) |
| 恢复目标 | RPO ≤ 15 分钟、RTO ≤ 2 小时(**待决 P-11**) | 备份演练记录 | 本期只写要求,不定稿方案 |
| 时钟精度 | 与 NTP 偏差 ≤ 1s | 各节点 NTP 监控 | `serverReceivedAt` 依赖平台自身时钟 |
**容量口径提醒**:以上按“≤20 个公寓、每公寓日均上行 5 万条”估算;超过该量级须重新做容量评审,并重新评估“单活 worker”是否够用。
## 3. 配置外置
表 8-3 配置分组、来源与变更影响
| 分组 | 典型配置项 | 存放位置 | 变更是否重启 | 是否允许入库 |
| --- | --- | --- | --- | --- |
| 数据源与连接池 | 主库 URL、账号、池大小;Redis 地址 | 环境变量 / 配置中心 | 是 | 否 |
| 跨端契约 | 支持的 `api_version` 列表、批量上限、`sentAt` 窗口(±5 分钟) | 配置中心 | 否(热加载) | 否 |
| 外部系统 | 运安 WSDL 定位与端点、中心汇总 baseURL、财务 baseURL、超时 / 重试参数 | 配置中心 | 部分需要(端点热加载,超时热加载) | 否 |
| 凭据 | 对象存储 AccessKey、上游令牌、DB 口令 | **仅**环境变量 / 密钥管理 | 是(或支持轮换热加载) | **严禁**(`docs/09` §1 P0 第 2 项) |
| 业务阈值 | 对账容差、水位不前进阈值、报表就绪判定、重试上限 1 小时 | 配置中心 | 否 | 可选(阈值类可入库便于审计) |
| 功能开关 | 各公寓能力开关(`features`)、域级灰度开关 | **入库** `PL_APARTMENT.features` + `PL_CONFIG_VERSION` | 否 | 是(需审计) |
**三条配置纪律**:
1. **地址与口令不进库、不进代码、不进日志**(PR-05)。`docs/09` §1 P0 已把“明文口令与内部地址入库”列为存量红线,平台侧不得继承。
2. **同一份配置只有一个真源**:阈值与开关在库里(要审计、要按公寓生效),连接与凭据在配置中心(要轮换)。两者不重叠。
3. **配置变更要可回滚**:`PL_CONFIG_VERSION` 记录版本与生效时间,回滚 = 恢复上一版本 + 重新发布,不做“手工改回去”。
## 4. 可观测性
表 8-4 关键指标、告警阈值与处置
| 域 | 指标 | 告警阈值(默认) | 处置 |
| --- | --- | --- | --- |
| 公寓在线 | `PL_APARTMENT_HEALTH.last_heartbeat_at` 距今 | 超过 5 分钟(心跳 1 分钟 × 5) | 告警;连续 30 分钟升级;**不主动探活** |
| 水位停滞 | `max_seen_watermark` 与 `acked_watermark` 差值 | 超过 1000 且持续 10 分钟 | 告警 + 生成待重放提示(`replayHint`) |
| 上行失败 | `PL_RECEIVE_LOG` 中非 `0` 结果占比 | 5 分钟内 > 2% | 告警;查是否为契约不兼容(版本协商) |
| 幂等命中 | `PL-IDEMPOTENT-REPLAY` 计数 | 5 分钟内 > 500 | 告警;查公寓重试参数 |
| 对账差异 | `PL_RECONCILE_DIFF` 未结清条数 | > 0 且跨 1 个日切 | 告警;`sum` 维度差异立刻 P0 |
| 外部系统 | 运安 / 中心 / 财务调用失败率 | 5 分钟内 > 50% | 熔断 + 告警;确认是否上游故障 |
| 任务积压 | worker 队列深度 | > 10000 | 扩容或降频;查是否卡在单条记录 |
| 库 | 慢查询(> 1s)、连接池占用 | 慢查询 5 分钟内 > 10 条 | 告警;补索引或改查询 |
| 业务正确性 | `PL_TASK` 终态回退次数 | > 0 | **立即 P0**(状态机被绕过,`03` 表 3-10) |
| 边界违规 | 平台库出现 `APT_*` / `duty_*` 表、对象存储出现 `apt-*` 前缀 | 巡检发现即告警 | **立即 P0**(边界破坏,`03` 表 3-3) |
**日志三条要求**:
1. 结构化 JSON,必带 `traceId` / `gyId` / 模块 / 接口;跨端请求的 `traceId` 由平台生成并在响应头返回,供公寓侧关联。
2. **不打凭据、不打完整报文**:人员敏感字段(身份证、手机号)在日志中按 `04` §4 脱敏规则处理。
3. 审计日志(`PL_AUDIT_OP` / `PL_AUDIT_API`)与业务日志分离存储,审计保留期长于业务日志(`03` 表 3-15)。
## 5. 灰度与回滚
表 8-5 灰度维度、顺序与回滚动作
| 维度 | 做法 | 影响面 | 回滚动作 | 前置条件 |
| --- | --- | --- | --- | --- |
| 域级灰度 | 按 `masterType` / 业务域逐个切:先 `master`(只读)→ `person` → `route` → `plan` → `ledger` → `report` | 单个下行域 | 关该域灰度开关,公寓回退到本地存量逻辑 | 契约已冻结(`05` §8) |
| 公寓级灰度 | 按 `gyId` 白名单,先 1 个公寓跑满 1 个结算周期 | 单公寓 | 把该 `gyId` 移出白名单 | 该公寓可回退(`yktgl` 旧链路仍在线,阶段零不下线) |
| 接口级灰度 | 新 `api_version` 只对声明 `X-Apt-App-Version` 的公寓生效 | 单接口 | 调小支持的版本区间 | 版本协商已实现(`05` 表 5-2) |
| 数据库切换(D13) | **双写 → 校验 → 读切 → 写切 → 下线旧库** | 全平台 | 任一步失败回到上一步,旧库保留只读 | `docs/10` 表 5-9 的 12 项必查清单全部通过 |
**回滚是“回退业务形态”,不是“回滚数据”**:平台库的 `PL_RECEIVE_LOG` / `PL_LEDGER_FLOW` 一旦写入不再回滚(审计要求),回滚只改变“谁提供服务、公寓读哪一侧”。这是平台端与普通单体应用最大的运维差异。
## 6. 数据库选型未定项下的部署约束
`docs/10` §5.6.3 表 5-8 给了三个选项,而表 10-2 的 D13 建议是“**短期选 A(平台不动)、中期满足三个前置条件后再议 B/C**”。因此本篇按“**A 即本期基线、B/C 未定**”展开,不擅自定稿中期方案,只写“无论选哪个都必须满足”的约束。
表 8-6 三个数据库选项对部署的影响与不变项
| 选项 | 对部署的影响 | 迁移成本 | 不变项(三选项共同) |
| --- | --- | --- | --- |
| A:平台保持现有 KingbaseES | 与现状一致,运维技能可复用;公寓端 MySQL、平台端 KingbaseES 双栈并存 | 低 | ① 连接层用同一套 JDBC 抽象与方言开关;② 逻辑类型族不变(`03` 表 3-16);③ 空串归一、金额定点、不逐列指定排序规则 |
| B:平台迁 MySQL(与公寓端同栈) | 只需一种 DBA 技能;但需处理存量数据迁移与 SQL 方言差异 | 中高 | 同上 |
| C:平台迁 MySQL 并合并实例 | 最省机器,但把“平台故障”与“公寓业务”耦合,**与三端拆分初衷冲突** | 高 | 同上 |
**B/C 方案未定前的三条部署约束**(A 方案下同样适用):
1. **不写方言相关的 DDL**:本轮不产出建表语句;阶段二按最终选型生成,且字符集口径遵循差-1(若为 MySQL,保留 `utf8mb4_general_ci`,不逐列指定 `_ai_ci` / `_as_cs`)。
2. **不依赖数据库特有功能**:不用存储过程、不用触发器、不用数据库级定时器;分页、JSON 处理、序列生成全部在应用层做(保证 A/B 可切换)。
3. **备份与恢复按“可换库”设计**:备份产物必须能恢复到另一种数据库(逻辑备份优先于物理备份),否则选型一旦变更就得重做运维体系。
**平台端 HA 策略同样待决(P-11)**:表 8-1 的“主备”只是本期的**最低要求**,不是最终方案。`docs/10` 表 10-2 的 16 条决策点里没有“平台端 HA”这一项(D9 是 Oracle 存量公寓、D16 是公寓端 Asterisk 高可用),故本篇把它登记为新增决策点 P-11(`09` 表 9-1)。若 P-11 定为集群 / 多活,需重新评估 worker 单活与协调锁的实现方式(协调锁依赖单库主节点,多活下要换成分布式锁并重新定义“单活”语义)。
## 7. 运行期运维清单
表 8-7 日常运维动作与频次
| 动作 | 频次 | 责任 | 异常处理 |
| --- | --- | --- | --- |
| 公寓健康巡检(`GET #15` 或管理端) | 每日 | 平台运维 | 离线公寓登记工单,**不主动拨号 / 不主动连** |
| 对账差异清零 | 每个日切 | 平台账务 | 未清零则调账冻结(`PF-5003`) |
| 重放作业清空 | 每日 | 平台运维 | 长期 `PENDING` 的作业人工判断是否作废 |
| 孤儿对象清理 | 每周 | 平台运维 | 保留 7 天后删除(`07` §4.2) |
| 归档摘要核对 | 每月 | 平台运维 | 与公寓侧数据量对不上则查归档策略 |
| 凭据轮换 | 每季度(可配) | 平台安全 | 轮换留过渡期,旧凭据保留 24 小时 |
| 备份恢复演练 | 每半年 | 平台运维 | 演练失败即视为 P0 |
## 8. 与 `docs/10` 阶段划分的对应
阶段名与内容沿用 `docs/10` §9 表 9-1~表 9-6(**共阶段零至阶段五 6 个阶段**,无“阶段六”)。
| `docs/10` 阶段 | 本篇涉及的部署动作 | 上线判据 |
| --- | --- | --- |
| 阶段零:基线修复(表 9-1) | 平台侧不改部署;先修 `pom.xml` 依赖错配、凭据外置、CI 六门禁(`02` §7) | 可重复构建;仓库无明文口令与内网地址 |
| 阶段一:现状梳理与边界确认(表 9-2) | 不部署;本篇作为归属矩阵的输入参与评审 | 归属矩阵与本篇结论通过签字 |
| 阶段二:接口契约与数据模型设计(表 9-3) | 部署 Mock 服务(独立命名空间,不入生产流量);建表脚本按 D13 的 A 方案准备 | 20 个接口可被公寓端联调(`docs/公寓端/09` §7);表结构与 `03` 一致 |
| 阶段三:公寓端试点(1 个公寓)(表 9-4) | 部署 `platform-api` 双实例 + 网关分流;worker 接管运安抓取;开上行入口与对账批次 | 试点公寓连续 7 天主数据无差异;1 个结算周期 `sum(amount)` 零差异 |
| 阶段四:多公寓推广(表 9-5) | 按 `gyId` 逐个加白名单;HA 按 P-11 的结论落地;开财务推送 | 单公寓故障不影响其他公寓;`gyId` 隔离验证通过 |
| 阶段五:平台端收敛与旧逻辑下线(表 9-6) | 下线 `yktgl` 的运安抓取、`xxtj` / `excel` 直连公寓库、旧路径兼容层 | 旧接口调用量归零(以网关日志为准,`02` §5);旧链路停用无业务投诉 |
+129
View File
@@ -0,0 +1,129 @@
# 09 待决策与风险
> 追溯:`docs/10` §10.1 表 10-1(12 项风险)、§10.2 表 10-2(D1–D16 决策清单)、§10.3 表 10-3(收敛路径)、
> §5.6.3 表 5-8(数据库三选项)、§6.4 末段(17 变 20 不另立 v2)。
> 相邻编号:`docs/公寓端/08` 的 Q 系列(待确认问题)、F 系列(契约冻结清单)、差系列(8 处差异)、G 系列(缺口)。
> 存量风险:`docs/09-迭代指引与风险清单.md` §1(P0 / P1 / P2 三档)、§4(10 条待确认问题)。
**编号纪律**:本篇新增两套编号——**P-01~P-11**(决策点)、**PR-01~PR-10**(风险)。它们**不占用**且**不修改** `docs/10` 的 D1–D16、`docs/公寓端/08` 的 Q / F / 差 / G、`docs/09` 的 P0 / P1 / P2。同一件事若已有旧编号,本篇只做映射(表 9-3),不重新定义。
> **CR-02 §15 决策承接**:平台端**不新增 `P` / `PR` 编号**,四能力与统计的待决口径一律承接全局 Q 系列——Q-22(平板 `UNVERIFIED` 复叫)、Q-30~Q-35(人脸)、Q-36~Q-40(入住)、Q-41~Q-45(消费 / 充值 / 食谱),**Q-46~Q-50(评价与房间周转接入、周期时区、DDL 冻结、综合排名权重、报表开放粒度)**;默认口径见 `docs/平台端/10` 表 10-21、CR-02 §15 表 3-69。统计能力的新增风险按既有 **PR-10(跨公寓报表聚合口径归属)** 承接:`PL_REPORT_DAILY` 一旦生成不可改,修正走次日重算 + 修订标记,不重算公寓明细、不跨库 JOIN;缺报 `NO_DATA` 不推断不补零;跨公寓聚合须 SQL 层带 `gyIds[]`。P-07(餐饮域表名与归属)继续作为远程点菜启用条件的前置(Q-44),本补遗不另立风险项。
## 1. 新增决策点(P 系列)
表 9-1 平台端新增决策点清单
| 编号 | 决策内容 | 选项 | 推荐 | 影响面 | 阻塞阶段 | 已有旧编号 |
| --- | --- | --- | --- | --- | --- | --- |
| **P-01** | 平台端前端归属:复用现有 `YktVue` 管理页 / 新建 `platform` 入口 / 本期只出 API | A 复用 `YktVue`(在 `views` 下加平台端页面,路由静态注册);B 新建独立 `platform` 前端工程;C 本期只出 API,页面后续再做 | **C → A**(先 API,联调期用 Mock / Swagger;管理页面在阶段三与 A 一起上) | 前端工程、部署、`sys_menu` 权限点 | 阶段三 | `docs/10` 表 2-3 前端行(只给了 `apt` 入口口径,未定 `platform`) |
| **P-02** | #16 / #17 的方向:只允许公寓发起 / 允许平台"回调"公寓 / 双形态 | A 保持"公寓主动调用平台"(与方向纪律一致,依据 `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行);B 平台直连公寓(**与方向纪律冲突,同上依据**);C 双形态(跨端仍 A,平台内部另有触发入口) | **A**,若运维确有"平台侧发起"需求则退让到 **C**(内部入口 `/platform/v1/admin/ingest/**`,不新增跨端编号) | 对账与重放的触发方、防火墙规则 | 阶段二(契约冻结前) | `docs/公寓端/09` F-03(方向待定) |
| **P-03** | 平台新增表是否统一 `PL_` 前缀 | A 统一 `PL_`(存量 `GY_*` 原名保留);B 全部沿用 `GY_` 前缀 | **A**:两类表语义不同(真源 vs 平台自有),前缀是最低成本的隔离手段 | 全部新建表的命名、`03` 表 3-3~3-9 | 阶段二(建表前) | `docs/10` §5.3 第 2 条(要求"不新增 `GY_` 表",未定前缀) |
| **P-04** | 平台端是否采用 RuoYi-Vue 多模块,还是沿用 `yktgl` 单模块演进 | A RuoYi-Vue 多模块(`platform-common/framework/system/.../admin`);B 沿用 `yktgl` 单模块按包拆分 | **B 起步、A 收口**:阶段零按包拆分(不动构建),阶段二随契约冻结切多模块 | 工程结构、CI、依赖管理 | 阶段零 | `docs/10` 表 2-2 后端框架行(只约束"不引入 `ruoyi-generator`"= 差-3) |
| **P-05** | G-01 / G-02 / G-03 若将来必须加字段,走什么评审 | A 走独立评审(影响面清单 + 双端兼容期);B 随常规迭代加列 | **A**:三处都跨两端(楼栋、人员本地启停、交路停用原因),随手上列会破坏"本期不改库"的口径 | `GY_ROOMS`、`GY_YKT_RY`、`GYGL_JLXX`、公寓端表结构 | 后续阶段 | `docs/公寓端/08` 表 8-9(G-01~G-03 已定"不改库兜底") |
| **P-06** | 主数据 `version` 的生成与单调性保证 | A 数据库序列 / 单表计数器(全局单调);B 时间戳派生(`yyyyMMddHHmmss + 序号`);C 内容哈希 | **A**:单调可判定、可比较大小(B 在时钟回拨与并发下会重号) | `PL_MASTER_VERSION.version_no`、所有下行的"版本落后"判断 | 阶段二 | `docs/公寓端/09` F-13(版本单调性未定) |
| **P-07** | 餐饮域表名与归属盘点(`YKT_MEALS*` 是否迁平台) | A 三个 `YKT_MEALS*` 表整体迁平台库(保持原名);B 只迁结算相关,点餐留在公寓端;C 餐饮本期不动 | **A**:餐饮已在平台侧(`yktgl`),迁移比重写便宜;但扣款必须走账务接口(`PF-6003`) | `YKT_MEALS`、`YKT_MEALS_ORDERING`、`YKT_MEALS_ORDERS_MX`、`PL_CATERING_SETTLE` | 阶段五 | `docs/10` 表 4-1 餐饮行(归属已定,表清单未定) |
| **P-08** | 公寓注册与自证身份(`gyid` 从哪来、凭据怎么发) | A 路径 / 参数 `gyId` 与请求头 `X-Apt-GyId` 必须一致;B 以请求头为准(路径可缺);C 以 token 内嵌 `gyid` 为准(不信任外部传入) | **C 为终态、A 为过渡**:C 需要网关与签发体系配套,本期先按 A 实现(`PL-GYID-MISMATCH`),C 在阶段四(多公寓推广)切 | 所有跨端接口的鉴权与租户隔离 | 阶段二(过渡)/ 阶段四(终态) | `docs/公寓端/09` F-02(`gyId` 优先级待定) |
| **P-09** | 对账后余额快照的下行载体 | A 挂在 #1 档案响应的附属快照(不新增接口);B 独立接口(须走 v2 契约);C 不做快照,公寓只上报不接收 | **A**:不破坏"20 个接口不新增"的口径;C 会让公寓侧对账缺一方基准 | #1 响应结构、`PL_BALANCE_SNAPSHOT`、公寓侧对账逻辑 | 阶段四 | `docs/10` §6.4 末段(不另立 v2) |
| **P-10** | 平台管理接口命名空间 | A `/platform/v1/admin/**`;B `/platform-admin/v1/**`(按端分组);C 与跨端同前缀靠 method / 权限区分 | **A**:前缀隔离最直观,网关可按前缀做鉴权与限流;B 仅换前缀不改语义 | 网关路由、前端 baseURL、权限点命名 | 阶段三(试点期需排障页面) | 无(本篇新增) |
| **P-11** | 平台端部署形态与 HA 策略(单主备 / 集群主从 / 多活) | A 主备(本期最低要求);B 集群主从(共享存储 + 主从切换);C 多活(需重做 worker 单活与协调锁) | **A 上线、B 随多公寓推广**;C 不建议(协调锁、单活 worker、报表一致性都会返工) | worker 单活定义、协调锁实现、RPO / RTO | 阶段四(多公寓推广) | 无(`docs/10` 表 10-2 无对应项:D9 是 Oracle 存量公寓、D16 是公寓端 Asterisk HA) |
**拍板方式**:逐条在评审会上结论化("采纳 A / 采纳 C / 延后"),结论回填本表并同步 `docs/10`;**不在本目录另开新文档**。
> **CR-02 §14 关联(Q-44 / 远程点菜)**:`docs/公寓端/12` 把远程点菜的启用条件(后厨 / 配送 / 结算规则)挂到 **P-07** 定稿之后;本期 `features.remoteOrdering=false`、N-58 恒 `40940 FEATURE_DISABLED`,`YKT_MEALS_ORDERING` / `YKT_MEALS_ORDERS_MX` 不写入,阶段性食谱只以 `YKT_MEALS` 补列实现。本补遗不新增表、不新增跨端编号,也不改变 P-07 的推荐结论与阶段五阻塞时点。
## 2. 新增风险(PR 系列)
表 9-2 平台端新增风险清单(等级:P0 阻断上线 / P1 影响正确性 / P2 影响效率)
| 编号 | 风险 | 触发条件 | 影响 | 等级 | 缓解措施 | 归属 |
| --- | --- | --- | --- | --- | --- | --- |
| **PR-01** | 平台库在 KingbaseES 与 MySQL 之间摇摆,导致两套 SQL 并存 | D13 的中期方案(B/C)被提前启动,或开发各自按偏好写方言 | 迁移成本翻倍;`03` 表 3-16 的类型族失效 | P1 | 本期按 D13 的“短期 A”执行;未启动 B/C 前只写方言无关 SQL(`08` §6 三条约束) | 平台架构 |
| **PR-02** | `yktgl` 拆分期双跑:同一能力在新旧两处都在跑,路由不一致 | 阶段零"不下线旧逻辑"与阶段三"启用新链路"重叠期过长 | 出现两套结果(双份叫乘任务、双份流水) | **P0** | 双跑必须"一侧只读":旧链路降为只读校验,或按 `gyId` 白名单互斥(`08` 表 8-5) | 平台实施 |
| **PR-03** | `PL_*` 与存量 `GY_*` 混写(一个业务动作写两张表且不在同一事务) | 开发图省事,在 `platform-catering` 里直接改 `GY_YKT_REMAINDER` | 主账被绕过,对账永远对不平 | **P0** | 唯一写入口进 Code Review 清单;`03` 表 3-10 的"写入口"列作为审查依据 | 平台开发 |
| **PR-04** | 运安 SOAP 不稳定或口径漂移(字段含义变化、返回空伪装成无计划) | 上游改版、网络抖动 | 计划缺失 → 漏叫乘;且难在平台侧发现 | P1 | 窗口滚动重叠 4 小时 + 连续 3 轮空集告警;字段为空整条跳过(`07` §2.4) | 平台集成 |
| **PR-05** | 外部凭据与内部地址管理失控(明文入库 / 进代码 / 进日志) | 沿用 `yktgl` 现状写法 | 安全红线;合规不通过 | **P0** | 凭据三不(不入库、不进代码、不进日志);上线前做配置扫描(`08` 表 8-3) | 平台安全 |
| **PR-06** | 中心汇总 / 财务接口无统一超时与重试,平台线程被拖死 | 上游慢响应(现状即如此,`docs/07` §3) | 上行接口连带不可用 | **P0** | 统一 3s / 10s 超时 + 幂等重试 + 熔断(`07` §3.1、§6) | 平台集成 |
| **PR-07** | 照片版本与对象存储不一致(库里是新版本,对象是旧的;或对象成孤儿) | 先写库后传对象 / 上传失败未回滚 | 公寓拿到坏照片或空照片 | P2 | **先写对象再写库**;`checksum` 去重;孤儿巡检(`07` §4.2 / §4.3) | 平台开发 |
| **PR-08** | 对账差异长期不清导致调账永久冻结 | 差异处置无 SLA,人工队列堆积 | 账务不可用,业务停摆 | **P0** | 差异超 3 个结算日升级 P0;`sum(amount)` 差异当日必清(`06` 表 6-6) | 平台账务 |
| **PR-09** | 多公寓注册与身份自证缺失,越权访问他人 `gyId` 数据 | P-08 未定即开工多公寓 | 数据串租户,审计无法追溯 | **P0** | 本期只开 1 个公寓;行级隔离在 SQL 层可验证(`03` 表 3-12);P-08 定稿前不加白名单公寓 | 平台架构 |
| **PR-10** | 平台报表口径与公寓端统计不一致(同一指标两个数) | 两侧各自实现统计(`xxtj` vs 平台报表) | 管理决策失真、互相不认账 | P1 | 报表只从 `PL_REPORT_DAILY` 出数;旧 `xxtj` / `excel` 直连公寓库在阶段五下线(`08` §8) | 平台报表 |
## 3. 与存量编号的映射
表 9-3 本篇编号与 `docs/10` D 系列、`docs/公寓端/08` Q / F / 差 / G 的对应关系
| 本篇编号 | 对应旧编号 | 关系 |
| --- | --- | --- |
| P-01 | 无(`docs/10` 表 2-3 只定 `apt` 入口) | 新决策:`platform` 前端入口未定 |
| P-02 | `docs/公寓端/09` F-03 | **同一件事**,本篇从平台侧给出 A / B / C 与推荐,结论需两侧同步 |
| P-03 | `docs/10` §5.3 第 1~2 条 | 落实要求,补"前缀选型"这一未定项 |
| P-04 | `docs/10` 表 2-2 后端框架行(差-3) | 差-3 只约束"不引入 `ruoyi-generator`",本篇不改该口径,只定"是否用 RuoYi" |
| P-05 | `docs/公寓端/08` G-01 / G-02 / G-03(表 8-9) | **不改动 G 系列结论**(本期不改库兜底),只补"将来要加字段时的评审路径" |
| P-06 | `docs/公寓端/09` F-13 | 同一件事(版本单调性),本篇给生成方式选型 |
| P-07 | 无 | 新决策:餐饮域表清单与迁移边界 |
| P-08 | `docs/公寓端/09` F-02 | 同一件事(`gyId` 优先级),本篇给终态方案 C |
| P-09 | `docs/10` §6.4 末段(不新增接口) | 在该约束下的载体选择,不新增接口 |
| P-10 | 无 | 新约定:管理命名空间 |
| P-11 | 无(`docs/10` 表 10-2 的 D9 是 Oracle 存量公寓、D16 是公寓端 Asterisk HA,均非平台 HA) | 新决策:平台端 HA 与部署形态 |
| PR-01 | `docs/10` D13(表 10-2) | D13 已给“短期 A”倾向;本期按 A,B/C 一旦启动即触发本风险 |
| PR-02 | `docs/10` 表 9-1~表 9-6(阶段划分)、表 10-3 | 阶段重叠期风险 |
| PR-03 | `docs/10` §5.3 第 1 条(一表一 owner) | 纪律失守的具体形态 |
| PR-04 | `docs/10` 表 10-1(外部集成类风险) | 平台侧承接项 |
| PR-05 | `docs/09` §1 P0 第 2 项(明文口令与内部地址入库) | 存量红线,平台侧不得继承 |
| PR-06 | `docs/07` §3「无统一超时重试」、`docs/09` §1 P1 | 存量缺陷,平台侧必须修 |
| PR-07 | 无 | 平台侧新增(照片对象一致性) |
| PR-08 | `docs/10` 表 5-2 账务行、本目录 `06` §6 | 一致性策略的失败模式 |
| PR-09 | `docs/10` §3.3 公寓隔离、`docs/公寓端/09` 表 9-19 | 隔离机制的失败模式 |
| PR-10 | `docs/10` 表 9-6(阶段五:平台端收敛与旧逻辑下线)、本目录 `03` 表 3-8 | 报表收敛失败模式 |
**不重复计数**:同一个问题在 `docs/10` 已有 D 编号的,本篇一律“映射 + 下游风险”,**不新增决策点**。因此 P 系列只有 11 条,其中真正“全新”的是 P-01、P-03、P-07、P-10、P-11 五条。
## 4. 批次阻塞矩阵
表 9-4 各阶段(沿用 `docs/10` §9 表 9-1~表 9-6,共阶段零至阶段五)的阻塞决策点
| 阶段 | 内容 | 必须拍板 | 可延后 | 阻塞后果 |
| --- | --- | --- | --- | --- |
| 阶段零(表 9-1) | 基线修复:`pom.xml` 依赖、凭据外置、CI 基线 | P-04(模块形态起步方案) | P-03、P-10 | 不定 P-04 则包结构无法冻结 |
| 阶段一(表 9-2) | 现状梳理与边界确认(归属矩阵、接口归属清单) | —(本篇作为输入) | P-01、P-05、P-07、P-11 | 归属矩阵不签字则后续全部返工 |
| 阶段二(表 9-3) | 接口契约与数据模型设计 + Mock + 建表 | **P-02、P-03、P-06、P-08(过渡方案)** | P-05、P-07 | P-02 不定 → #16/#17 无法冻结;P-03 不定 → 建表无法开工 |
| 阶段三(表 9-4) | 公寓端试点(1 个公寓)+ 管理页面 | **P-01、P-10** | P-09、P-11 | P-01 不定 → 试点期排障页面无处落 |
| 阶段四(表 9-5) | 多公寓推广 + 财务推送 | **P-08(终态)、P-09、P-11**、PR-09 的隔离验证 | — | P-11 不定 → 第二个公寓无法做 HA 验收 |
| 阶段五(表 9-6) | 平台端收敛与旧逻辑下线 | **P-07**、PR-02 的互斥方案 | — | P-07 不定 → 餐饮表归属不清 |
**一句话结论**:**阶段二之前必须拍板 P-02、P-03、P-06、P-08(过渡)**。这四项不定,契约与建表都无法冻结;P-09 / P-11 最晚在阶段四前给结论(阶段三试点即可用单主备过渡)。
## 5. 存量风险对照(`docs/09`)
`docs/09-迭代指引与风险清单.md` 记录的是**代码级存量风险**,与平台端设计输入无关,但平台侧会继承其中一部分。此处只做"是否被平台端继承"的对照。
表 9-5 存量风险对平台端的影响
| `docs/09` 位置 | 存量问题 | 平台端是否继承 | 处理 |
| --- | --- | --- | --- |
| §1 P0 第 1 项 | Spring Boot 依赖版本错配 | **是**(同一份 `pom.xml` 血缘) | 阶段零统一依赖管理,作为 CI 门禁项(`02` §7) |
| §1 P0 第 2 项 | 明文口令与内部地址入库 | **是**(风险最高) | 平台侧凭据三不(PR-05、`08` 表 8-3);上线前配置扫描 |
| §1 P0 第 3 项 | 无根级 `.gitignore` | **是**(同仓库) | 阶段零补根级忽略规则(`02` §7 门禁) |
| §1 P0 第 4 项 | 构建 / 测试基线不明 | **是** | 阶段零建立"可重复构建 + 最小测试集"(`02` §7 六门禁) |
| §1 P1(11 项) | 代码质量与规范类问题 | **部分继承** | 按"是否落在迁往平台端的模块"逐条筛:迁走的随 `platform-*` 修,留下的归公寓端(`02` 表 2-5) |
| §1 P2(8 项) | 优化类问题 | **多数不继承** | 只把与平台接口 / 报表口径相关的纳入 PR-10 |
| §4(10 条待确认问题) | 需求口径未定 | **部分继承** | 与平台端相关的按 P 系列重新登记(表 9-3);其余归公寓端,不重复编号 |
**处理原则**:映射只做“认领”,不做“改写”。`docs/09` 的 P0 / P1 / P2 编号与内容保持不变,平台端在这张表里只是标注“我承接哪几条、在哪一阶段修”。
## 6. 仍需确认的决策点(交付清单)
按紧急程度排序,评审时按此顺序拍板:
1. **P-02(#16 / #17 方向)** —— 直接决定方向纪律(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)是否被突破,最高优先级。
2. **P-08(`gyId` 自证身份,过渡方案 A)** —— 决定多公寓能否开工。
3. **P-03(`PL_` 前缀)** —— 决定阶段二建表的全部命名。
4. **P-06(`version` 单调性)** —— 决定下行“版本落后”判断是否可靠。
5. **D13(平台端是否一并改 MySQL,`docs/10` 表 10-2)** —— D13 已建议“短期 A、中期 B/C”,本篇按 A 展开(`08` §6);启动 B/C 前必须先过 `docs/10` 表 5-9 的 12 项必查清单。
6. **P-01(前端归属)/ P-10(管理命名空间)** —— 决定管理页面工程与网关路由。
7. **P-09(余额快照载体)** —— 决定 `#1` 响应结构与公寓侧对账基准。
8. **P-07(餐饮域归属盘点)** —— 决定阶段五的迁移范围。
9. **P-04(模块形态起步方案)** —— 决定阶段零的包结构冻结方式。
10. **P-05(G 系列加字段的评审路径)** —— 本期不触发,定机制即可。
11. **P-11(平台端 HA 与部署形态)** —— 本期按“主备”最低要求推进(`08` 表 8-1),最终方案在阶段四前定;`docs/10` 表 10-2 无对应项(D9 为 Oracle 存量公寓、D16 为公寓端 Asterisk HA)。
**评审产出要求**:每条结论写"采纳某选项 + 生效阶段 + 需要同步修改的文档位置"。涉及跨端契约的(P-02、P-06、P-08、P-09)必须与 `docs/公寓端/09` 同步改,且**先改契约、再改实现**。
@@ -0,0 +1,428 @@
# 10 公寓统计分析与排名报表
> 文档编号:`docs/平台端/10`。性质:**设计稿(Design Only)**,不是接口契约冻结稿;阶段二契约冻结评审通过后才落 DDL。
> **落地权威表述**:本文件是「以公寓为单位的分类统计、周期报表与公寓排名」的**唯一权威表述**(`PS-01`~`PS-05`、表 10-1~10-22、图 10-1~10-3);口径裁决、三端状态映射、变更点与影响范围、施工单及验收登记见 `CR-02 §15`。
> **上位依据**:`docs/平台端/01` 表 1-4 / 表 1-5、`docs/平台端/03` 表 3-6 / 3-8 / 3-10 / 3-16、`docs/平台端/04` 表 4-10 与 §12 三条接口纪律、`docs/平台端/06` §7(F6 跨公寓报表汇总)与 §8、`docs/平台端/08` §1(worker 单活)、`docs/平台端/09` PR-10 / P-07、`docs/设计变更/03`(CR-02 表 3-12 `bizType`、表 3-21 枚举、§14 账务扩展)。
> **硬约束**:跨端仍为 **20 个接口编号(1~17 + 23~25)**、契约 `v2-draft`、平台不回调公寓;本节全部能力落在**平台管理端** `/platform/v1/admin/stat/**`,**不新增跨端编号、不新增 `bizType`**;公寓本地库业务表仍 **59 张**(新增统计表属平台库,另计);平台**只聚合已上报数据,不重算公寓明细**(表 4-10 边界、F6 要点 1、PR-10);所有跨公寓聚合在 SQL 层带 `gyIds[]` 范围(F6 要点 4、`docs/平台端/03` §5 四级隔离)。金额一律 `decimal(12,2)` / 字符串两位小数、比率 `decimal(6,4)`、禁止浮点。
表 10-1 文档信息
| 项 | 内容 |
| --- | --- |
| 范围 | `PS-01` 指标口径、`PS-02` 周期汇总、`PS-03` 公寓排名、`PS-04` 报表生成与导出、`PS-05` 权限 / 异常 / 一致性;覆盖业务规则、交互流程、权限控制、异常处理、数据同步与状态映射 |
| 本端职责 | `platform-report` 为**唯一生成方**;`platform-worker` 为**唯一调度方**;`platform.iam` 管数据范围;其余模块零写入口 |
| 协同端 | 公寓端(经既有 #9 / #10 / #11 / #12 / #13 / #24 上行归集,不再新增上报义务)、应用端(只读报表入口,不产出统计) |
| 编号规则 | 功能项 `PS-01`~`PS-05`;表 `表 10-y`、图 `图 10-y` 篇内连续;管理端接口 `S-01`~`S-07`;决策点 `Q-46`~`Q-50`;裁决 `CR-C36`~`CR-C42`、流程 `F-O` / `F-P` / `F-Q`(见 CR-02 §15) |
| 术语 | 表名小写为设计命名(`pl_apt_stat_*`),存量平台表沿用 `PL_*` / `GY_*` 大写;`gyid` 只从会话解析;周期键 `periodKey` 格式固定 |
表 10-2 修订记录
| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-11-10 | 首次形成:五类指标口径与数据源、月 / 季 / 年周期汇总、公寓分类排名、报表生成与导出、权限与异常、三端状态映射、Q-46~Q-50 与验收 8 项;明确"评价情况 / 房间周转"依赖 #13 摘要扩展、现金充值**计为收入**、`PL_REPORT_DAILY` 冻结后次日重算 + 修订标记 |
---
## 1. 定位与边界
### 1.1 五类统计与落点
表 10-3 统计分类、指标与权威数据源
| 分类 | 功能项 | 指标 | 权威数据源(平台库) | 上行来源 | 本期 |
| --- | --- | --- | --- | --- | --- |
| 公寓接待能力 | `PS-01` | 入住人数 `STAY_COUNT`、叫乘人数 `CALL_COUNT`、评价情况 `SATISFACTION_*`、报警情况 `ALARM_*` | `PL_REPORT_DAILY`(补列)、`PL_RECEIVE_LOG`(按 `biz_type` 计数)、`pl_staff_performance_daily` | #9 `CHECKIN` / `TABLET_RESULT`、#8 `TASK_ACK`、#12 `DEVICE_ALARM`、#13 `ARCHIVE` 摘要 | 是 |
| 补贴发放情况 | `PS-01` | `ALLOWANCE_ISSUE_AMOUNT` / `ALLOWANCE_ISSUE_COUNT` | `PL_LEDGER_FLOW`(`type=ALLOWANCE`)、`PL_ALLOWANCE_RULE` | #10 `LEDGER_OFFLINE` | 是 |
| 补贴消费情况 | `PS-01` | `ALLOWANCE_CONSUME_AMOUNT` / `ALLOWANCE_CONSUME_COUNT` | `PL_LEDGER_FLOW`(`type=CONSUME` 的 `allowance_amount` 补列) | #10 `LEDGER_OFFLINE`(§14 `allowanceAmount` / `cashAmount`) | 是 |
| 现金充值情况(**计为收入**) | `PS-01` | `CASH_RECHARGE_INCOME` / `CASH_RECHARGE_COUNT` | `PL_LEDGER_FLOW`(`type=RECHARGE`) | #10 `LEDGER_OFFLINE`(`rechargeChannel=OFFLINE_CASH`) | 是 |
| 房间利用周转情况 | `PS-01` | `ROOM_NIGHTS` / `ROOM_TURNOVER_RATE` / `ROOM_AVAILABLE` | `PL_REPORT_DAILY`(补列) | #13 `ARCHIVE` 摘要 `roomOccupancy` | 是 |
### 1.2 与既有报表能力的关系
- **不重建、只扩展**:`platform.report` 已有 `GET /summary/daily`、`GET /summary/quality`、`GET /compare`、`POST /exports`(表 4-10)与 `PL_REPORT_DAILY` / `V_CALL_QUALITY` 底座(表 3-8)。本文件在其上补「分类周期指标 + 排名 + 报表周期」,**不替换**日报口径,也不改 `PL_REPORT_DAILY` 既有字段语义。
- **不重算明细**:日报由 #11 / #12 / #24 小时增量 + 每日全量生成(F6),本节月 / 季 / 年汇总**只从 `PL_REPORT_DAILY` + `PL_LEDGER_FLOW` + `PL_RECEIVE_LOG` rollup**,禁止回查公寓库、禁止按 `apt_local_task` / `gy_jcxx` 明细重算(PR-10、表 4-10 边界)。
- **报表冻结纪律沿用**:`PL_REPORT_DAILY` 一旦生成不可改;修正走**次日重算 + 修订标记**(F6 要点 6),本节的 `revision_no` 是该纪律在周期层的落地。
- **调度归属**:`platform-worker` **单活**(`docs/平台端/08` §1),`platform-api` 不持有定时任务;任务进度复用既有 `PL_JOB_*`,不引入消息中间件(`docs/平台端/02` §2)。
### 1.3 不变式
1. 跨端 20 个编号、方向、路径不变;统计能力全部落在平台管理端 `/platform/v1/admin/stat/**`,**不产生跨端副作用**(表 4-10 §12 纪律①)。
2. 新增数据一律走既有 `payload` / `bizType` 扩展;`#13 ARCHIVE` 摘要补 `satisfaction` 与 `roomOccupancy` 分组计数,**不新增 `bizType`、不新增接口编号、不要求公寓端上报明细**。
3. 平台零回调;公寓端仍"主动拉 + 主动推",统计结果不反向下发公寓。
4. 所有平台表 UK 以 `gyid` 为第一列;跨公寓查询必须在 **SQL 层**带 `gyIds[]`,不允许先查全量再应用层过滤(F6 要点 4)。
5. `platform-report` 是 `pl_apt_stat_period` / `pl_apt_stat_rank` 的**唯一写入方**;`platform-ledger` 仍是余额主账唯一写者;统计模块对账务表**只读**。
6. 归日以平台 `serverReceivedAt` 为准;`occurredAt` 只作业务时间轴,不参与日切(CR-02 表 3-11 / 表 3-14)。
7. `amount` 一律字符串两位小数 / `decimal(12,2)`,比率 `decimal(6,4)`;**缺报不推断、不补零**(F6 表 6-7 语音缺报口径推广到全部指标)。
8. 公寓本地库业务表仍 **59 张**;本节新增 2 张 `pl_apt_stat_*` 属**平台库**,按 `docs/平台端/03` §0 表 C-1 / C-2「平台库新增表另计」执行,不纳入 59 张校验。
9. `PL_REPORT_DAILY` 冻结不可原地改;周期修正只能新 `revision_no` + `state=REVISED`,历史版本保留可追溯。
10. 统计不成为业务准入门槛:排名与报表结果**不回写** `gy_jcxx` / `apt_local_task` / `GY_YKT_REMAINDER` 等任何业务表。
---
## 2. 状态流转总览
图 10-1 数据流与生成链路
```mermaid
flowchart LR
subgraph AP["公寓服务系统"]
DEV["平板 / 签到终端 / 消费终端"]
OUT["apt-report / outbox"]
end
subgraph PL["平台端"]
ING["platform-ingest<br/>#9/#10/#12/#13 去重"]
JOB["platform-worker(单活)<br/>PL_JOB_* 协调锁"]
DAILY["PL_REPORT_DAILY<br/>日报底座(冻结不可改)"]
FLOW["PL_LEDGER_FLOW<br/>账务归集流水"]
LOG["PL_RECEIVE_LOG<br/>bizType 计数"]
PER["pl_apt_stat_period<br/>周期指标(revision_no)"]
RANK["pl_apt_stat_rank<br/>分类排名快照"]
REP["platform-report<br/>/admin/stat/**"]
EXP["report-export 桶"]
end
subgraph DUTY["应用端 / 管理端"]
UI["值班台 / 管理端报表页(只读)"]
end
DEV -->|上行| OUT
OUT -->|20 个既有编号| ING
ING --> LOG
ING -->|#11/#12/#24 小时增量 + 日全量| DAILY
ING -->|#10 归日| FLOW
JOB -->|日切后触发| DAILY
DAILY -->|rollup 月/季/年| PER
FLOW -->|rollup 账务三类| PER
LOG -->|缺报判定 / 计数| PER
PER -->|RANK() 跳号| RANK
PER --> REP
RANK --> REP
REP --> UI
REP --> EXP
```
表 10-4 三条状态链(互不替代)
| 链 | 字段 / 载体 | 取值 | 唯一写入口 | 说明 |
| --- | --- | --- | --- | --- |
| **日报生成态** | `PL_REPORT_DAILY` + `PL_JOB_*.state` | `PENDING` / `RUNNING` / `DONE` / `FAILED`(任务态);日报行**一旦生成即冻结** | `platform-worker` → `platform-report` | 沿用表 3-10 / 表 6-7;`PF-7001` 不输出半成品 |
| **周期指标态** | `pl_apt_stat_period.state` + `revision_no` | `DRAFT` / `FINAL` / `REVISED`;`revision_no` 从 `1` 单调 | `platform-report` | `FINAL` 后不可改;修正走新 `revision_no` 且旧行标 `REVISED` |
| **排名快照态** | `pl_apt_stat_rank.revision_no` + `generated_at` | 与周期指标同版;无独立业务状态字段 | `platform-report` | 排名不随查询实时重算,只按 `revision_no` 读快照 |
表 10-5 全流程状态流转
| 序 | 流程 | 状态 | 触发 | 写入口与落表 | 后继 / 异常回退 |
| --- | --- | --- | --- | --- | --- |
| 1 | 上行归集 | `RECEIVED` | #9 / #10 / #12 / #13 到达 | `platform-ingest` → `PL_RECEIVE_LOG` | `idempotencyKey` 重复 → `PL-IDEMPOTENT-REPLAY`;版本落后 → `PL-VERSION-STALE` |
| 2 | 日报 | `RUNNING` → `DONE` | 每日日切后 `platform-worker` 全量 | `PL_REPORT_DAILY`、`PL_JOB_*` | 缺报指标 `NO_DATA`,**不推断、不补零**;超时 > 10 min 拆批 |
| 3 | 账务日对账 | `RECONCILED` / `DIFF` | `platform-ledger` 三维比对 | `PL_RECONCILE_BATCH`、`PL_LEDGER_FLOW.reconcile_state` | `DIFF` → `PF-5003` 阻断调账,该 `gyid` 当期账务类指标**不封账、不参与排名** |
| 4 | 周期汇总 | `DRAFT` | 月 / 季 / 年周期到达或手工 `S-03` 生成 | `pl_apt_stat_period` | 源日报未 `DONE` → `PF-7003` 周期未封账,任务 `FAILED` |
| 5 | 周期汇总 | `FINAL` | 源数据齐备 + 对账无 `DIFF` | `pl_apt_stat_period.state=FINAL` | 生成后不可原地改 |
| 6 | 修正 | `REVISED` + 新 `revision_no` | 次日重算 / 人工发起修订 | 新行 + 旧行标 `REVISED` | 同 `(gyid, periodType, periodKey, revisionNo)` 幂等,重复返回原结果 |
| 7 | 排名 | 快照生成 | 周期指标 `FINAL` 后 | `pl_apt_stat_rank` | 同分同名次(`RANK()` 跳号);`gyIds[]` 越权 → `PF-7004` |
| 8 | 导出 | `PENDING` → `DONE` / `FAILED` | `S-06` 异步导出 | `report-export` 桶、`PL_JOB_*` | 失败 `PF-7002`,短期签名默认 7 天 |
图 10-2 周期汇总与排名判定
```mermaid
flowchart TD
A["周期到达 / S-03 生成请求"] --> B{"gyIds[] 在会话数据范围内?"}
B -->|否| Z1["PF-7004 拒绝"]
B -->|是| C{"PL_JOB_* 已有同 key 运行中?"}
C -->|是| Z2["返回原 jobId(幂等)"]
C -->|否| D["建 job(RUNNING)+ 协调锁"]
D --> E{"源日报 DONE 且当日对账无 DIFF?"}
E -->|否| Z3["FAILED / PF-7003<br/>账务类指标不封账"]
E -->|是| F["rollup 五类指标 → DRAFT"]
F --> G{"全部指标源齐备?"}
G -->|否| H["缺报项标 NO_DATA<br/>不推断不补零"]
G -->|是| I["FINAL + revision_no=1"]
H --> I
I --> J["RANK() 生成分类排名快照"]
J --> K["S-01 / S-02 可查;S-06 可导出"]
```
---
## 3. 数据模型
> **仅新增 2 张平台库表**(另计,不纳入公寓本地库 59 张);任务进度复用 `PL_JOB_*`,导出复用 `report-export` 桶,日报沿用 `PL_REPORT_DAILY`。所有补列均为设计定义,DDL 待阶段二契约冻结评审(Q-48)。
表 10-6 复用表与新增表清单
| 表 | 库 | 唯一写入模块 | 本流程用途 | 本模块权限 |
| --- | --- | --- | --- | --- |
| `PL_REPORT_DAILY` | platform | `platform-report` | 日报底座;入住 / 叫乘 / 报警 / 房间周转 / 评价的分类日值 | 只读(补列由日报生成时写入) |
| `PL_RECEIVE_LOG` | platform | `platform-ingest` | 按 `biz_type` 计数、缺报判定、幂等去重核对 | 只读 |
| `PL_LEDGER_FLOW` | platform | `platform-ledger` | 补贴发放 / 补贴消费 / 现金充值三类金额与笔数 | **只读** |
| `PL_RECONCILE_BATCH` / `PL_RECONCILE_DIFF` | platform | `platform-ledger` | 封账前置判据(`DIFF` 阻断) | 只读 |
| `pl_staff_performance_daily` | platform | `platform-report` | 评价情况的辅助交叉校验(不替代评价计数) | 只读 |
| `PL_TASK` / `PL_TASK_ACK` | platform | `platform.plan` / `platform-ingest` | 叫乘人数交叉校验 | 只读 |
| `PL_JOB_*` | platform | `platform-worker` | 生成 / 导出任务状态与协调锁 | 只读 |
| `pl_apt_stat_period` | platform | **`platform-report`** | **新增**:按公寓 × 周期 × 指标的分类汇总(含修订版本) | 读写 |
| `pl_apt_stat_rank` | platform | **`platform-report`** | **新增**:分类排名快照 | 读写 |
表 10-7 `pl_apt_stat_period` 列级结构(新增)
| 列 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | id_ulid | PK | 代理主键 |
| `gyid` | string(32) | NOT NULL,UK 第一列 | 公寓隔离;**只从会话 / 归集数据解析** |
| `period_type` | enum_s | `MONTH` / `QUARTER` / `YEAR` | 周期类型 |
| `period_key` | string(16) | `YYYY-MM` / `YYYY-Qn` / `YYYY` | 周期键,格式固定,服务端生成 |
| `metric_class` | enum_s | `RECEPTION` / `ALLOWANCE_ISSUE` / `ALLOWANCE_CONSUME` / `CASH_RECHARGE_INCOME` / `ROOM_TURNOVER` | 五类统计 |
| `metric_code` | enum_s | 见表 10-9 | 指标编码 |
| `metric_value` | decimal(18,2) | 可空 | 金额 / 人数 / 笔数;`NULL` 表示 `NO_DATA`(不写 0) |
| `metric_unit` | enum_s | `PERSON` / `COUNT` / `AMOUNT` / `RATIO` / `ROOM_NIGHT` | 单位;比率另以 `decimal(6,4)` 精度约束 |
| `data_status` | enum_s | `OK` / `NO_DATA` / `BLOCKED` | `NO_DATA` 缺报、`BLOCKED` 对账 `DIFF` 未封账 |
| `source_version` | bigint | 默认 `1` | 源日报 / 流水版本水位,用于对账追溯 |
| `state` | enum_s | `DRAFT` / `FINAL` / `REVISED` | 表 10-4 周期指标态;`FINAL` 不可原地改 |
| `revision_no` | bigint | 默认 `1`,单调 | 修正走 `+1`,旧行标 `REVISED` |
| `generated_at` / `created_at` | datetime(3) ×2 | NOT NULL | 生成 / 落库时间 |
| | | **UK** `(gyid, period_type, period_key, metric_code, revision_no)`;**IDX** `(gyid, period_type, period_key, metric_class, state)` | 幂等与周期查询 |
表 10-8 `pl_apt_stat_rank` 列级结构(新增)
| 列 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | id_ulid | PK | 代理主键 |
| `gyid` | string(32) | NOT NULL,UK 第一列 | 公寓隔离 |
| `period_type` / `period_key` | enum_s / string(16) | 同表 10-7 | 周期 |
| `rank_type` | enum_s | `RECEPTION` / `ALLOWANCE_ISSUE` / `ALLOWANCE_CONSUME` / `CASH_RECHARGE_INCOME` / `ROOM_TURNOVER` / `COMPOSITE` | 分类排名 + 综合排名 |
| `rank_no` | int | NOT NULL | `RANK()` 跳号;同分同名次 |
| `score` | decimal(18,4) | 可空 | 排名依据分值;综合排名权重见 Q-49 |
| `tie_count` | int | 默认 `1` | 并列公寓数 |
| `revision_no` / `generated_at` | bigint / datetime(3) | 与周期指标同版 | 快照版本一致,不实时重算 |
| | | **UK** `(gyid, period_type, period_key, rank_type, revision_no)`;**IDX** `(period_type, period_key, rank_type, rank_no)` | 榜单查询 |
表 10-9 指标字典(五类)
| 分类 | 指标编码 | 单位 | 口径公式 | 数据源 | 缺报处理 |
| --- | --- | --- | --- | --- | --- |
| `RECEPTION` 接待能力 | `STAY_COUNT` | PERSON | `sum(PL_REPORT_DAILY.stay_count)` | 日报 #9 `CHECKIN` | `NO_DATA` |
| | `CALL_COUNT` | PERSON | `sum(PL_REPORT_DAILY.call_count)` | 日报 #8 / #9 `TABLET_RESULT`、#24 | `NO_DATA` |
| | `SATISFACTION_GOOD` / `SATISFACTION_NEUTRAL` / `SATISFACTION_BAD` / `SATISFACTION_TOTAL` | COUNT | `sum(PL_REPORT_DAILY.satisfaction_* 补列)` | **#13 `ARCHIVE` 摘要 `satisfaction{}` 扩展** | `NO_DATA`;参评数为 0 时不计算好评率 |
| | `SATISFACTION_RATE` | RATIO | `GOOD / TOTAL`,`decimal(6,4)` | 同上 | `TOTAL=0` → `NO_DATA`,不记 0 |
| | `ALARM_COUNT` / `ALARM_CALL` / `ALARM_LEAVE` / `ALARM_DEVICE` | COUNT | #12 按 `alarmSource` 分组计数 | 日报补列 + `PL_RECEIVE_LOG` | `NO_DATA` |
| `ALLOWANCE_ISSUE` 补贴发放 | `ALLOWANCE_ISSUE_AMOUNT` | AMOUNT | `sum(amount) where type=ALLOWANCE`,正数 | `PL_LEDGER_FLOW` | 对账 `DIFF` → `BLOCKED` |
| | `ALLOWANCE_ISSUE_COUNT` | COUNT | `count(flow_id)` 按 `biz_id` 去重(R-C3) | 同上 | `BLOCKED` |
| `ALLOWANCE_CONSUME` 补贴消费 | `ALLOWANCE_CONSUME_AMOUNT` | AMOUNT | `sum(allowance_amount) where type=CONSUME` | `PL_LEDGER_FLOW.allowance_amount`(补列,CR-02 §14) | `BLOCKED` |
| | `ALLOWANCE_CONSUME_COUNT` | COUNT | 同上笔数 | 同上 | `BLOCKED` |
| `CASH_RECHARGE_INCOME` 现金充值(收入) | `CASH_RECHARGE_INCOME` | AMOUNT | `sum(amount) where type=RECHARGE`,正数 | `PL_LEDGER_FLOW` | `BLOCKED` |
| | `CASH_RECHARGE_COUNT` | COUNT | 同上笔数 | 同上 | `BLOCKED` |
| `ROOM_TURNOVER` 房间利用周转 | `ROOM_AVAILABLE` / `ROOM_NIGHTS` | ROOM_NIGHT | `sum(PL_REPORT_DAILY.room_available / room_nights 补列)` | **#13 `ARCHIVE` 摘要 `roomOccupancy{}` 扩展** | `NO_DATA` |
| | `ROOM_TURNOVER_RATE` | RATIO | `ROOM_NIGHTS / ROOM_AVAILABLE`,`decimal(6,4)` | 同上 | 分母 0 → `NO_DATA` |
> **现金充值计为收入**(CR-C37):`CASH_RECHARGE_INCOME` 是**期间流入额**,按 `sum(amount)` 正数统计,**不与消费净额抵销、不冲减补贴发放、不记入余额负债**;与 `GY_YKT_REMAINDER` 的余额快照是两个口径,禁止互相校验。
> **补贴消费拆分**(CR-C38):一笔消费只一条 `flow_id`(CR-C31),补贴部分取 `allowance_amount`、现金部分取 `cash_amount`;`ALLOWANCE_CONSUME_AMOUNT` 只取前者,避免与 `sum(amount)` 混用导致双计。
> **评价与房间周转的数据来源**(CR-C39):两项均由公寓端在既有 **#13 `ARCHIVE` 日归档摘要**中按 `payload` 扩展分组计数上报(`satisfaction{}` / `roomOccupancy{}`,摘要类按批覆盖、无版本语义,表 3-13),平台**只接计数不接明细、不新增 `bizType`**;若公寓端本期未上报,指标标 `NO_DATA` 并在报表注明"数据未接入",**不推断、不补零、不以 `pl_staff_performance_daily` 代填**(Q-46)。
表 10-10 既有表补列(设计定义,DDL 待阶段二冻结)
| 表 | 补列 | 用途 |
| --- | --- | --- |
| `PL_REPORT_DAILY` | `satisfaction_good` / `satisfaction_neutral` / `satisfaction_bad` / `satisfaction_total` | #13 摘要评价分组计数 |
| | `alarm_call` / `alarm_leave` / `alarm_device` | #12 按 `alarmSource` 分组计数 |
| | `room_available` / `room_nights` | #13 摘要房间可用夜 / 占用夜 |
| | `source_version` / `revision_no` | 日报水位与修订标记(F6 要点 6 落地) |
| `PL_LEDGER_FLOW` | `allowance_amount` / `cash_amount` / `recharge_channel` | 承接 CR-02 §14 #10 payload 拆分字段,供分类统计 |
表 10-11 枚举字典
| 枚举 | 取值 | 来源 / 说明 |
| --- | --- | --- |
| `periodType` | `MONTH` / `QUARTER` / `YEAR` | 本流程定义;`periodKey` = `YYYY-MM` / `YYYY-Qn` / `YYYY` |
| `metricClass` | `RECEPTION` / `ALLOWANCE_ISSUE` / `ALLOWANCE_CONSUME` / `CASH_RECHARGE_INCOME` / `ROOM_TURNOVER` | 五类统计 |
| `dataStatus` | `OK` / `NO_DATA` / `BLOCKED` | 缺报与对账阻断,不写 0 |
| `statState` | `DRAFT` / `FINAL` / `REVISED` | 周期指标态 |
| `rankType` | 五类 + `COMPOSITE` | 分类排名 + 综合排名(Q-49) |
| `jobState` | `PENDING` / `RUNNING` / `DONE` / `FAILED` | 沿用 `PL_JOB_*` / `PL_REPLAY_JOB.state`(表 3-10) |
| `reconcileState` | `RECEIVED` / `RECONCILED` / `DIFF` | 沿用 `PL_LEDGER_FLOW.reconcile_state`,不新造 |
---
## 4. PS-01~PS-03 指标、周期与排名
### 4.1 业务规则
表 10-12 周期口径(CR-C36)
| 项 | 口径 |
| --- | --- |
| 日切 | 以平台 `serverReceivedAt` 归日;公寓 `occurredAt` 不参与日切(CR-02 表 3-11) |
| 月 | `periodType=MONTH`、`periodKey=YYYY-MM`;自然月 1 日 00:00 至月末 24:00(平台时区) |
| 季 | `periodType=QUARTER`、`periodKey=YYYY-Qn`(Q1=1~3 月,Q2=4~6,Q3=7~9,Q4=10~12) |
| 年 | `periodType=YEAR`、`periodKey=YYYY`;自然年 |
| 生成方式 | 月 / 季 / 年**一律由日报 rollup**,不直接扫明细表;周期边界以日报 `stat_date` 归属 |
| 封账前置 | 期内所有日报 `DONE` 且账务日对账无 `DIFF`;否则 `PF-7003`,账务类指标 `BLOCKED`、不参与排名 |
| 修正 | `PL_REPORT_DAILY` 冻结不可改 → 次日重算生成新 `revision_no`,旧行标 `REVISED`,历史版本可追溯 |
表 10-13 排名规则(CR-C40)
| 项 | 口径 |
| --- | --- |
| 维度 | 以 `gyid`(公寓)为单位,对五类指标与 `COMPOSITE` 分别排名 |
| 名次序 | `RANK()` **跳号**、同分同名次,`tie_count` 记并列数;不使用 `ROW_NUMBER()` 人为拆开并列 |
| 范围 | `gyIds[]` 必须由会话数据范围解析,**SQL 层带范围**(F6 要点 4);空 / 越权 → `PF-7004` |
| 参与条件 | 仅 `state=FINAL`、`data_status=OK/BLOCKED` 之外的有效指标;`NO_DATA` 公寓**不参与该指标排名**并在榜单标注 |
| 快照 | 排名按 `revision_no` 存快照,查询不实时重算;周期修正后生成新版本快照 |
| 综合排名 | `COMPOSITE` 权重默认五类等权(Q-49 待决),缺类时按剩余类归一化,须在报表注明权重版本 |
| 方向 | 报警数、投诉类指标为**逆向指标**(越小越优),默认按"越小越靠前",Q-49 拍板前按此执行 |
### 4.2 接口
表 10-14 管理端接口 `S-01`~`S-07`(`/platform/v1/admin/stat/**`,**不计入跨端 20 编号**)
| 编号 | 方法 + 路径 | 权限点 | 幂等 | 请求字段 | 响应字段 |
| --- | --- | --- | --- | --- | --- |
| S-01 | `GET /stat/periods` | `platform:stat:list` | L0 | `gyIds[]`(会话范围)、`periodType`、`periodKey`、`metricClass`、`revisionNo`(可选) | `items[]{gyid, gyName, periodType, periodKey, metricClass, metricCode, metricValue, metricUnit, dataStatus, state, revisionNo}`、`revisionNo`、`serverTime` |
| S-02 | `GET /stat/ranks` | `platform:stat:rank` | L0 | `gyIds[]`、`periodType`、`periodKey`、`rankType`、`revisionNo` | `items[]{rankNo, gyid, gyName, score, tieCount, dataStatus}`、`weightVersion`、`revisionNo` |
| S-03 | `POST /stat/generate` | `platform:stat:generate` | **L2** `Idempotency-Key` | `periodType`、`periodKey`、`gyIds[]`、`reason`(修订时必填)、`Idempotency-Key` | `jobId`、`state=PENDING`、`periodType` / `periodKey`、`idempotentReplay` |
| S-04 | `GET /stat/jobs/{jobId}` | `platform:stat:list` | L0 | `jobId` | `jobId`、`state`、`progress`、`startedAt` / `finishedAt`、`errorCode`、`revisionNo` |
| S-05 | `GET /stat/metrics` | `platform:stat:list` | L0 | 无 | `metrics[]{metricClass, metricCode, metricUnit, formula, dataSource, direction}`、`weightVersion` |
| S-06 | `POST /stat/exports` | `platform:stat:export` | **L2** `Idempotency-Key` | `periodType`、`periodKey`、`gyIds[]`、`rankTypes[]`、`format`(`XLSX` / `PDF`)、`Idempotency-Key` | `exportId`、`state=PENDING` |
| S-07 | `GET /stat/exports/{exportId}` | `platform:stat:export` | L0 | `exportId` | `exportId`、`state`、`fileRef`、`signUrl`(短期签名)、`expiresAt` |
> 接口纪律(表 4-10 §12):① 管理接口**不产生跨端副作用**,只落 `pl_apt_stat_*` / `PL_JOB_*` / `report-export`;② 写接口 S-03 / S-06 必须带 `Idempotency-Key`;③ 每个接口在 `docs/平台端/03` 都能找到落表(表 10-6)。导出复用既有 `/exports` 通道与 `report-export` 桶,不新增桶。
表 10-15 生成状态机(条件更新带 `state` 期望值)
| 当前态 | 事件 | 目标态 | 落表 | 失败 / 补偿 |
| --- | --- | --- | --- | --- |
| — | S-03 提交且 `gyIds[]` 合法 | `PENDING` → `RUNNING` | `PL_JOB_*` | 同 key 运行中 → 返回原 `jobId`;越权 `PF-7004` |
| `RUNNING` | 源日报 `DONE` + 对账无 `DIFF` | `DRAFT` | `pl_apt_stat_period` | 未封账 `PF-7003` → `FAILED`;`DIFF` → 账务类 `BLOCKED` |
| `DRAFT` | 指标齐备校验通过 | `FINAL` | `pl_apt_stat_period` | 缺报项标 `NO_DATA`,不推断不补零 |
| `FINAL` | 排名生成 | 排名快照 | `pl_apt_stat_rank` | 并列按 `RANK()` 跳号;`NO_DATA` 公寓不参与 |
| `FINAL` / `REVISED` | 次日重算 / 人工修订 | 新 `revision_no` + 旧行 `REVISED` | 两张 `pl_apt_stat_*` | 版本冲突 `PF-7005`;不原地改 `FINAL` 行 |
| 任意 | worker 双跑 / 超时 | `FAILED` | `PL_JOB_*` | 库级协调锁防双跑;> 10 min 拆批(`docs/平台端/08` §4) |
图 10-3 生成与导出时序
```mermaid
sequenceDiagram
autonumber
participant W as platform-worker(单活)
participant R as platform-report
participant D as PL_REPORT_DAILY / PL_LEDGER_FLOW
participant S as pl_apt_stat_period / rank
participant U as 管理端 / 应用端(只读)
participant E as report-export
W->>R: 周期到达(月/季/年)或 S-03 触发
R->>D: 只读日报与账务流水(SQL 层带 gyIds[])
D-->>R: 日值与三类金额
R->>R: 校验封账(日报 DONE + 对账无 DIFF)
R->>S: rollup 五类指标 → FINAL(revision_no=1)
R->>S: RANK() 生成分类 / 综合排名快照
U->>R: S-01 查周期指标 / S-02 查排名
R-->>U: 按 revision_no 读快照(不实时重算)
U->>R: S-06 导出(Idempotency-Key)
R->>E: 异步生成 XLSX / PDF,短期签名 7 天
E-->>U: S-07 取 fileRef / signUrl
Note over W,E: 全程不回查公寓库、不重算明细、不产生跨端副作用
```
---
## 5. PS-04 报表输出与 PS-05 权限、异常、一致性
### 5.1 报表输出
表 10-16 报表清单与内容
| 报表 | 周期 | 内容 | 出口 |
| --- | --- | --- | --- |
| 公寓分类统计表 | 月 / 季 / 年 | 按 `gyid` × 五类指标展开;含 `data_status` / `revision_no` / 权重版本 | S-01、S-06 |
| 公寓接待能力报表 | 月 / 季 / 年 | 入住人数、叫乘人数、评价(好 / 中 / 差 / 参评 / 好评率)、报警(总 / 叫乘 / 离寓 / 设备) | S-01、S-06 |
| 补贴发放与消费报表 | 月 / 季 / 年 | 发放额 / 笔数、补贴消费额 / 笔数、补贴使用率(`ALLOWANCE_CONSUME / ALLOWANCE_ISSUE`) | S-01、S-06 |
| 现金充值收入报表 | 月 / 季 / 年 | 充值额(**计收入**)/ 笔数、与消费额分列不抵销 | S-01、S-06 |
| 房间利用周转报表 | 月 / 季 / 年 | 可用房间夜、占用房间夜、周转率 | S-01、S-06 |
| 公寓排名榜 | 月 / 季 / 年 | 五类分类排名 + 综合排名,含并列标记与 `NO_DATA` 标注 | S-02、S-06 |
> 所有报表在页脚注明:数据来源(`PL_REPORT_DAILY` / `PL_LEDGER_FLOW` / #13 摘要)、`periodKey`、`revisionNo`、`weightVersion`、生成时间与 `gyIds[]` 范围;**不展示未封账周期**(`PF-7003`)。
### 5.2 权限与审计
表 10-17 权限与审计
| # | 要求 |
| --- | --- |
| 1 | **数据范围前置**:`gyIds[]` 从会话数据范围解析,不接受"不传即全量";SQL 层带范围,空 / 越权 `PF-7004` 并记审计(`docs/平台端/03` §5、F6 要点 4) |
| 2 | **权限点**:`platform:stat:list` / `rank` / `generate` / `export` 四级;生成与导出需二次确认,导出仅返回短期签名地址 |
| 3 | **职责分离**:`platform-worker` 只调度、`platform-report` 只生成、`platform.iam` 只管范围;统计模块对 `PL_LEDGER_FLOW` / `GY_YKT_REMAINDER` **只读**,永不写余额(`docs/平台端/03` 表 3-6) |
| 4 | **只读消费端**:应用端值班台与管理端报表页只读 S-01 / S-02 / S-05 / S-07;不得经 `/duty/**` 触发 S-03 / S-06,也不得写 `pl_apt_stat_*` |
| 5 | **审计**:生成、修订、导出、排名查询四类动作记 `PL_JOB_*` / 平台操作日志(操作人、`gyIds[]`、`periodKey`、`revisionNo`、`Idempotency-Key`、结果) |
| 6 | **最小必要**:报表不含人员姓名、工号、人脸引用、业务抓拍;评价与房间周转只收分组计数,**不收明细** |
| 7 | **金额安全**:`decimal(12,2)` / 字符串两位小数、比率 `decimal(6,4)`,禁止浮点;`CASH_RECHARGE_INCOME` 与余额快照禁止互相校验 |
### 5.3 异常
表 10-18 异常与错误码
| 场景 | 处理 | 不做什么 |
| --- | --- | --- |
| 日报未 `DONE` / 周期未封账 | `PF-7003`、任务 `FAILED` | 不输出半成品(`PF-7001`)、不用 `DRAFT` 数据出报表 |
| 日对账 `DIFF` | 账务三类 `BLOCKED`、`PF-5003` 阻断调账,该 `gyid` 不参与账务类排名 | 不直改余额表、不按未对账金额排名 |
| 指标缺报(评价 / 房间周转 / 语音等) | `data_status=NO_DATA`,报表注明"数据未接入" | **不推断、不补零、不以其他表代填**(F6 表 6-7) |
| `gyIds[]` 为空 / 越权 / 含跨租户 | `PF-7004` 拒收并记审计 | 不先查全量再应用层过滤 |
| 生成任务双跑 / 重复提交 | 库级协调锁 + `Idempotency-Key`,重复返回原 `jobId` | 不产生两份周期指标 |
| 生成超时 > 10 min | 按 `gyid` 拆批(`docs/平台端/08` §4) | 不放宽"单活"约束、不引入消息中间件 |
| 修订版本冲突 | `PF-7005`,返回当前 `revisionNo` | 不原地改 `FINAL` 行、不删历史版本 |
| 导出失败 / 签名过期 | `PF-7002`,重新导出复用原 `Idempotency-Key` | 不返回内网绝对路径、不放长签名 |
| 公寓端断网 / 上行积压 | 按 `serverReceivedAt` 归日、水位收敛;缺日报则该期 `NO_DATA` | 不回调公寓、不反向补数、不改 `PL_REPORT_DAILY` 冻结行 |
| `PL_RECEIVE_LOG` 幂等重放 | `PL-IDEMPOTENT-REPLAY` 视为成功 | 不与 `PF-5002` 业务重复混用 |
| 公寓端业务表直连请求 | 拒绝(PR-10:旧 `xxtj` / `excel` 直连公寓库阶段五下线) | 不重算公寓明细、不跨库 JOIN |
---
## 6. 三端一致性与数据同步
> **权威映射表见 `CR-02 §15` 表 3-65**(公寓端 → 上行 / 下行编号 → 平台端 → 应用端全量对照)。本节只给统计链路的本端落点。
表 10-19 统计数据源与跨端编号映射
| 统计分类 | 公寓端业务事实 | 上行编号 + `bizType` | 平台端归集落表 | 应用端可见性 |
| --- | --- | --- | --- | --- |
| 入住人数 | `gygl_ly_event(CHECKIN)`、#9 `CHECKIN` 提报 | #9 / `CHECKIN` | `PL_RECEIVE_LOG` → `PL_REPORT_DAILY.stay_count` | 值班台只读入住台账,不产出统计 |
| 叫乘人数 | 平板 N-04 `TABLET_RESULT` / SIP AMI 结论 | #9 `TABLET_RESULT`、#8 `TASK_ACK`、#24 `VOICE_SUMMARY`(仅 SIP) | `PL_TASK_ACK` → `PL_REPORT_DAILY.call_count` | `/ws/duty` `tablet.result` / `state.changed` 只读 |
| 评价情况 | 公寓端评价分组计数(#13 摘要扩展) | #13 `ARCHIVE` payload `satisfaction{}` | `PL_REPORT_DAILY.satisfaction_*`(补列) | 只读报表,不采集、不写回 |
| 报警情况 | `apt_device_alarm`、`gy_bjxx` | #12 `DEVICE_ALARM`(按 `alarmSource` 分组)、#11 摘要 | `PL_REPORT_DAILY.alarm_*` | 值班台处置入口(只提交命令) |
| 补贴发放 / 补贴消费 / 现金充值 | `apt_ledger_offline` 一条流水 + 拆分字段 | #10 `LEDGER_OFFLINE` | `PL_LEDGER_FLOW`(`reconcile_state`) | 值班台只读额度 / 流水,充值核对只提交命令 |
| 房间利用周转 | 房态与入住夜数(#13 摘要扩展) | #13 `ARCHIVE` payload `roomOccupancy{}` | `PL_REPORT_DAILY.room_available / room_nights`(补列) | 值班台只读房态,不产出统计 |
表 10-20 同步机制与状态映射(统计侧)
| 环节 | 公寓端状态 | 同步载体 | 平台端状态 | 应用端状态 | 回退 |
| --- | --- | --- | --- | --- | --- |
| 上行受理 | outbox `PENDING` → `SENT` | #9 / #10 / #12 / #13,`idempotencyKey` | `PL_RECEIVE_LOG` `RECEIVED` | 待办 / 只读视图 | 断网续报复用原幂等键,不产生新业务事实 |
| 归日 | `occurredAt` / `syncedAt` | 以 `serverReceivedAt` 归日 | 日报 `stat_date` | — | 跨日迟到不回改已冻结日报 |
| 对账 | `offline_scope=LOCAL_ONLY` → `SYNCED` → `RECONCILED` | #16 日对账(公寓主动调用,P-02) | `PL_LEDGER_FLOW.reconcile_state` `RECEIVED` → `RECONCILED` / `DIFF`;`PL_RECONCILE_BATCH.state` | 只读对账状态 | `DIFF` + `PF-5003` 阻断,账务类指标 `BLOCKED` |
| 周期汇总 | 无(不感知统计) | 无跨端交互 | `pl_apt_stat_period.state` `DRAFT` → `FINAL` → `REVISED` | 只读 `FINAL` / `REVISED` 快照 | 次日重算 + `revision_no+1` |
| 排名 | 无(不感知排名) | 无跨端交互 | `pl_apt_stat_rank` 快照 | 只读榜单 | 修正后新版本快照,旧版可追溯 |
> **关键一致性约束**:统计结果的任何变化都**不反向改变公寓端业务状态**(`ledger_state` / `reg_state` / `match_state` / `ryzt` 等一律不变);三端状态映射的完整枚举与终态规则以 `CR-02 §15` 表 3-65 为准。
---
## 7. 决策点与验收
表 10-21 待决口径(Q-46~Q-50,承接 `docs/公寓端/12` Q-45)
| 编号 | 事项 | 默认口径(未决前执行) | 拍板时点 |
| --- | --- | --- | --- |
| Q-46 | 评价与房间周转的数据接入方式 | 公寓端在 **#13 `ARCHIVE` 摘要**按 `payload` 扩展分组计数;未上报则 `NO_DATA`,不推断不补零、不以 `pl_staff_performance_daily` 代填 | 阶段二契约冻结前 |
| Q-47 | 报表周期时区与季度划分 | 平台统一时区;季按自然季度 `YYYY-Qn`;跨时区公寓按 `gyid` 配置,未决前统一平台时区 | 阶段二契约冻结前 |
| Q-48 | `pl_apt_stat_*` 两张新表与 `PL_REPORT_DAILY` / `PL_LEDGER_FLOW` 补列 DDL 冻结时点 | 设计定义,DDL 待阶段二契约冻结评审通过后执行;冻结前可用 `PL_JOB_*` JSON 兜底留痕 | 阶段二契约冻结前 |
| Q-49 | 综合排名权重与逆向指标处理 | 五类等权、缺类归一化;报警 / 投诉类为逆向指标(越小越优);`RANK()` 跳号 | 阶段三试点前 |
| Q-50 | 统计报表是否向应用端值班台开放、开放到什么粒度 | 默认**只读**开放到公寓级分类统计与排名,不开放跨公寓明细下钻、不开放人员级数据 | 阶段三试点前 |
表 10-22 验收要点
| # | 验收项 | 判据 |
| --- | --- | --- |
| 1 | 五类指标齐备 | 入住 / 叫乘 / 评价 / 报警、补贴发放、补贴消费、现金充值(计收入)、房间周转五类均有明确公式、数据源、单位与缺报处理;`CASH_RECHARGE_INCOME` 不与消费抵销 |
| 2 | 周期报表 | 月 / 季 / 年三种 `periodType` 均可生成,日切以 `serverReceivedAt`,周期一律由日报 rollup,不重算公寓明细 |
| 3 | 公寓排名 | 按 `gyid` 对五类 + 综合分别排名,`RANK()` 跳号、并列同名次、`NO_DATA` 不参与;`gyIds[]` 在 SQL 层生效 |
| 4 | 封账与修订 | 未封账 `PF-7003`、`DIFF` 阻断排名;`PL_REPORT_DAILY` 冻结不可改,修正走次日重算 + `revision_no+1`,历史版本可追溯 |
| 5 | 权限与审计 | 四类权限点前后端同码;`gyIds[]` 越权 `PF-7004`;生成 / 修订 / 导出 / 排名查询全部留痕;统计模块对账务表只读 |
| 6 | 异常完整性 | 缺报 `NO_DATA` 不补零、双跑幂等、超时拆批、版本冲突 `PF-7005`、导出失败 `PF-7002`、断网按水位收敛均有明确处理与"不做什么" |
| 7 | 边界不破 | 跨端 20 编号、`v2-draft`、平台零回调、公寓 **59 张表**、不新增 `bizType`、平台库新增 2 张另计;管理接口不产生跨端副作用 |
| 8 | 三端一致性 | 表 10-19 / 10-20 与 `CR-02 §15` 表 3-65 逐项对应;统计结果不回写公寓端 / 应用端任何业务状态;金额 `decimal(12,2)`、比率 `decimal(6,4)`、禁止浮点 |
+128
View File
@@ -0,0 +1,128 @@
# 平台端功能边界与开发设计(索引)
> 追溯:`docs/10-架构优化方案-三端拆分.md`(v1.2,2026-11-07 回填)§3.1(平台端负责 / 不负责)、
> §4.1 表 4-1(能力归属)、§4.2 表 4-2(平台端模块清单)、§5.1 表 5-1(数据分层与归属)、
> §5.2 表 5-2(一致性策略)、§5.3(同库边界 6 条)、§5.6.3 表 5-8(平台端数据库三选项)、
> §6.1 表 6-1(17 个跨端接口)、§6.2(上报信封与三时间戳)、§6.3 表 6-2(幂等 / 重试 / 对账 / 断网)、
> §6.4 表 6-3 第 23~25 项、§8.1 表 8-1(三端部署形态)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(“平台不回调公寓”)、§10.1 表 10-1、§10.2 表 10-2(D1–D16)。
> 对端依据:`docs/公寓端/09-平台接口约定.md`(20 接口逐条、F-01~F-13 冻结清单、G-08 / G-09 缺口)。
> 结论依据:`docs/公寓端/08-待决策与风险.md` 表 8-8(8 处差异已采纳)、表 8-9(G-01~G-07 状态)。
> 状态:设计稿 v1.0 + **CR-02 修订 v2(2026-11-09)**。**只产出设计文档**:不写业务代码、不建表改表、不执行 DDL / DML、不改任何配置。
> **v2 权威增量**:`docs/设计变更/03-三端功能设计修订(CR-02).md`。下表与 §0 一页结论中与 CR-02 冲突处以 CR-02 表 3-4 为准:
> ① 表 0-3 表数量行的"44 张业务表"是**公寓本地库**口径,CR-02 后为 58 张、§10 补遗(新增 `apt_sip_reg_status`)后为 **59 张**;平台库新增 `pl_person_reg_request` / `pl_offline_quota` / `pl_staff_performance_daily` 3 张另计,**§15 补遗再新增 `pl_apt_stat_period` / `pl_apt_stat_rank` 2 张,累计 5 张**(见 `03` §0 表 C-3)。
> ④ 补遗(`docs/公寓端/00` v1.1 / CR-02 §10):SIP 分机离线告警经 #11 / #12 / #13 / #24 的 payload 扩展上报,平台只汇总、不回调、不下发处置指令;录音文件与注册流水明细不上报。
> ② 结论 2 的"20 个跨端接口"编号 / 方向 / 路径不变,新增数据走 `payload` + `bizType` 扩展(`v2-draft`),平板 / 值班台协议不计入 20;`docs/平台端/05` §1 第 2 条"确需新增走 v2 契约、不在本期"已撤销。
> ③ 平台侧新增职责只落在 `platform.ingest` / `platform.ledger` / `platform.report`,见 `docs/平台端/01` §0、`03` §0、`05` §0、`06` §0;`PL_TASK.jcsj` 仅为基线值,权威时刻由公寓复算(CR-C06)。
> ⑤ 补遗(CR-02 §12 / `docs/公寓端/10`):人脸数据下发管理以 `docs/公寓端/10` 为权威;**人员主数据**照片与特征值按 CR-C24 允许平台 ↔ 公寓双向同步(对象存储承载 + 短期签名直传,公寓库内只存引用与校验和),业务抓拍 / 签到现场图 / 录音仍不出公寓;注册链路按 CR-C25 由**值班员确认**生效,平台 `personRegVerdicts[]` 降级为**主档同步回执**、不做注册准入审批,两条同步路径(#9 上行 / 设备下发)解耦并行。不新增业务表与跨端编号,公寓本地库仍 59 张。
> ⑥ 补遗(CR-02 §13 / `docs/公寓端/11`):入住事实上报固定 #9 `CHECKIN`(+ `applyId` / `origin` / `cardNo` 等字段),平台接运安快照完成匹配,结论只经 #7 响应体 `matchVerdict[]` 下行;#8 回执是匹配输入,`PL_TASK.jcsj` 仍只是基线、权威 `jcsj` 恒由公寓复算。平台不生成任务行、不回调公寓;不新增业务表与跨端编号,公寓本地库仍 59 张。
> ⑦ 补遗(CR-02 §14 / `docs/公寓端/12`):消费、充值与菜品管理以 `docs/公寓端/12` 为权威;`platform.ledger` 仍是余额主账唯一写入口,#10 `LEDGER_OFFLINE` 按双额度拆分字段归集、日对账通过才置 `RECONCILED`;`platform.catering` 承载 `YKT_MEALS` 食谱补列与 #4 `recipes[]` 下行。平台**不接线上支付与回调、不实现远程点菜**(`features.remoteOrdering=false`);不新增业务表、不新增跨端编号与 `bizType`,公寓本地库仍 59 张。
> ⑧ 补遗(CR-02 §15 / **三端一致性同步 + 平台统计分析**):公寓端已完成的**平板叫乘、人脸签到、消费、菜品管理**四项能力在平台端同步落地业务规则、交互流程、权限控制与异常处理;三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,平台侧投影见 `05` §0 表 C-3。新增 `docs/平台端/10`「公寓统计分析与排名报表」:以 `gyid` 为单位统计接待能力(入住 / 叫乘 / 评价 / 报警)、补贴发放、补贴消费、现金充值(**计为收入**)、房间利用周转五类指标,按月 / 季 / 年生成报表并对各公寓分类 + 综合排名;管理端接口 `S-01`~`S-07` 落 `/platform/v1/admin/stat/**`,**不计入跨端 20 编号、不产生跨端副作用**。平台库新增 `pl_apt_stat_period` / `pl_apt_stat_rank` 2 张(累计 **5 张**,另计),公寓本地库仍 **59 张**;不新增 `bizType`、不新增公寓业务表、不新增对象存储桶,统计结果不回写任何业务状态。变更点与影响范围见 **CR-02 §15 表 3-63**。
## 0. 一页结论
1. **平台端不是新系统,是“把现状的中心能力显式化”**:主数据、运安计划、账务主账、餐饮、跨公寓报表、账号与审计六类归平台(`docs/10` 表 1-2 已逐模块标注)。本期动作是“从 `yktgl` 里剥离职责边界 + 补 4 个缺失能力(`platform.ingest` / `platform.ops` / 版本快照 / 幂等接收)”,不是重写。
2. **平台端与公寓端只有“拉 + 推”两个方向**:公寓主动出站拉主数据与计划、推结果与摘要;平台端**永不回调公寓端**(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行)。因此 20 个跨端接口中,#15 / #25 由平台自答(公寓不实现),#16 / #17 的“平台 → 公寓”方向与方向纪律冲突、列入待确认(沿用 `docs/公寓端/09` 的 F-03,本篇编号 P-02)。
3. **平台端数据库的中期方案未定**(`docs/10` §5.6.3 表 5-8 的 A / B / C,对应 D13;D13 已建议“短期 A、中期再议 B/C”):本篇按**选项 A(平台保持现有 KingbaseES)**展开,但所有新增表只给“方言无关的逻辑类型族”,迁 MySQL 时不改语义、只换物理类型。
4. **平台端表分两类,同名不同义**:沿用存量真源表(`GY_*` / `GL_*`,`owner=platform`)与新增平台表(`PL_*`)。同一个 `GY_*` 表名在平台库是“真源”、在公寓库是“执行事实 / 快照”,**禁止跨库 JOIN、禁止同名表当同一张表对待**(本目录 `03` 表 3-3 显式声明)。
5. **六条关键流程全部以“幂等键 + 三时间戳 + 水位 / 对账 + 重放”收口**(`docs/10` §6.2 与表 6-2),任何一条都不得为了“实现方便”退化成平台回调公寓。
## 1. 文档清单
表 0-1 平台端设计文档清单与本篇主题
| 篇 | 主题 | 本文图表 | 主要对应 `docs/10` |
| --- | --- | --- | --- |
| `README.md` | 索引、追溯矩阵、一页结论 | 图 0-1、表 0-1、表 0-2 | §0、§11.3 |
| `01-功能边界.md` | 定位与一句话边界、负责 / 不负责、三端分工、能力归属、10 个模块、本期范围 | 图 1-1、表 1-1~1-6 | §3.1、§3.3 表 3-1、§3.4 表 3-2、§4.1 表 4-1、§4.2 表 4-2 |
| `02-工程结构与技术基线.md` | 服务形态、技术基线、Maven 模块与包、依赖方向、与 `yktgl` 收敛映射、前端归属、CI 基线 | 图 2-1、表 2-1~2-6 | §2.4 表 2-2、§4.2 表 4-2、§9 阶段零 |
| `03-数据设计.md` | 数据分层归属、命名分区、平台端表清单、状态字段与单写入口、多公寓隔离与注册、同步方向与版本快照、归档留存、方言口径 | 图 3-1、表 3-1~3-16 | §5.1 表 5-1、§5.2 表 5-2、§5.3、§5.6 |
| `04-接口设计.md` | 平台端自身接口 `/platform/v1/**` 按域分组(方法 / 路径 / 用途 / 幂等语义 / 错误码分段);表 4-13 为统计管理接口 `S-01`~`S-07` | 表 4-1~4-13 | §6.1、§6.2、§6.3 |
| `05-跨端接口对齐.md` | 20 个跨端接口逐条对齐(方向、编号、幂等键、三时间戳、重试、对账),不新增、不重排 | 表 5-1~5-10 | §6.1 表 6-1、§6.2、§6.3 表 6-2、§6.4 表 6-3 |
| `06-关键流程.md` | 主数据下行、计划下行与任务生成、回执与结果上报、水位 / 对账 / 重放、账务归集与日对账、跨公寓报表汇总 | 图 6-1~6-6、表 6-1~6-7 | §5.2、§6.2、§6.3、表 5-1 第 4 行、表 8-4「出站 443/tcp」行 |
| `07-外部集成.md` | 运安 SOAP、中心汇总 / 财务、对象存储(**人员主数据照片 / 特征值**与报表导出;业务抓拍、签到现场图与录音不出公寓,CR-C24) | 图 7-1、表 7-1~7-8 | §2.3 表 2-1、§5.1 表 5-1、§9 阶段五 |
| `08-部署与运行.md` | 部署形态、非功能要求、配置外置、可观测性、灰度与回滚 | 图 8-1、表 8-1~8-7 | §8.1 表 8-1、§8.2 表 8-2、§5.6.3 表 5-8、§9 表 9-1~9-6 |
| `09-待决策与风险.md` | 新增决策点 P-01~P-11、新增风险 PR-01~PR-10、与存量编号映射、批次阻塞矩阵、存量风险对照 | 表 9-1~9-5 | §10.1 表 10-1、§10.2 表 10-2、§10.3 表 10-3 |
| `10-公寓统计分析与排名报表.md` | **公寓分类统计 / 月·季·年报表 / 公寓排名**:五类指标口径与数据源、周期汇总与封账、`RANK()` 排名快照、`S-01`~`S-07` 接口、`pl_apt_stat_*` 两张新表与补列、权限与异常、三端数据源映射、Q-46~Q-50 与验收 8 项;**唯一权威表述** | 表 10-1~10-22、图 10-1~10-3 | CR-02 §15(表 3-63~3-69)、`docs/平台端/03` 表 C-3、`04` 表 4-13、`06` 表 C-1 |
## 2. README 的图表编号规则(全目录共用)
1. **表 x-y / 图 x-y 中的 x = 文档序号**(README 用 0,`01` 用 1,…,`09` 用 9),y 在各篇内从 1 连续递增。
2. 因此本目录的 `表 3-1` 指 `03-数据设计.md` 的第 1 张表,**与 `docs/10` 的表 3-1(应用端与公寓端的逻辑分工)不是同一张表**。跨文档引用一律写明来源,如“`docs/10` 表 3-1”。
3. 本目录不新增 `docs/10` 的图表编号,也不改动 `docs/10` 附录 B 的索引。
图 0-1 三端文档与职责的对应关系
```mermaid
flowchart LR
subgraph D10["docs/10 三端拆分(v1.2)"]
C10["第 3 章 端职责边界<br/>第 4 章 模块划分<br/>第 5 章 数据架构<br/>第 6 章 接口契约"]
end
subgraph PL["docs/平台端(本篇)"]
P1["01 功能边界"]
P2["02 工程结构"]
P3["03 数据设计"]
P4["04 自身接口"]
P5["05 跨端 20 接口"]
P6["06 关键流程"]
P7["07 外部集成"]
P8["08 部署运行"]
P9["09 决策与风险"]
end
subgraph AP["docs/公寓端(已定稿)"]
A1["01 功能边界"]
A3["03 数据设计(44 张表)"]
A5["05 关键流程"]
A9["09 平台接口约定(20 接口)"]
A8["08 待决策与风险"]
end
C10 --> PL
C10 --> AP
P5 <--> A9
P3 <--> A3
P6 <--> A5
P9 <--> A8
```
## 3. 追溯矩阵(结论 → 依据)
表 0-2 本篇关键结论的依据位置
| 本篇结论 | 依据 | 依据位置 |
| --- | --- | --- |
| 平台端负责 / 不负责清单 | `docs/10` §3.1 | 正文“负责 / 不负责”两条列表 |
| 三端分工口径不变 | `docs/10` §3.3 表 3-1、§3.4 表 3-2 | 本目录 `01` 表 1-1、表 1-2 沿用,不改写 |
| 10 个模块与职责 | `docs/10` §4.2 表 4-2 | 本目录 `01` 表 1-4、`02` 表 2-2 |
| 技术基线(RuoYi + Vue + MySQL / KingbaseES) | `docs/10` §2.4 表 2-2、§5.6.3 表 5-8 | 本目录 `02` 表 2-1,数据库项按 D13 标注待决 |
| 数据分层与归属 | `docs/10` §5.1 表 5-1 | 本目录 `03` 表 3-1 |
| 一致性策略 | `docs/10` §5.2 表 5-2 | 本目录 `03` 表 3-13、`05` 表 5-5 |
| 平台端表清单与分区 | `docs/10` §5.3 第 1~2 条、`docs/05` 数据模型、`docs/11` 表分析 | 本目录 `03` 表 3-2~3-9 |
| 20 个跨端接口不重排 | `docs/10` §6.1 表 6-1、§6.4 表 6-3、§6.4 末段 | 本目录 `05` 表 5-1 |
| 幂等 / 三时间戳 / 重试 / 对账 | `docs/10` §6.2、§6.3 表 6-2 | 本目录 `05` 表 5-3、表 5-4 |
| 平台不回调公寓 | `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行、§2.2 拓扑要点第 1 条 | 本目录 `01` 表 1-3、`05` 表 5-7、`06` 全文 |
| 数据库选型 / HA / 多公寓注册待决 | `docs/10` §5.6.3 表 5-8、表 10-2(D13) | 本目录 `08` 表 8-6、`09` 表 9-1(平台 HA 在表 10-2 无对应项,登记为 P-11) |
| 公寓端 44 张表口径 | `docs/公寓端/03` 表 3-8、`docs/10` 表 5-9 第 1 项(v1.2) | 本目录 `03` §1.3(引用,不改写) |
| 语音域跨端 3 个接口 | `docs/10` §6.4 表 6-3 #23~#25 | 本目录 `05` 表 5-1 第 23~25 行 |
**依据文档范围的核对**:`docs/10` 表 0-1「关联文档」行引用的 `docs/01`~`docs/09` 在当前仓库中**均存在**(`docs/` 顶层为 `01`~`11` + `frontend/` + `公寓端/` + `平台端/`),无失效引用。(补充:`docs/10` §11.3 第 4 条要求在 `docs/README.md` 与 `docs/09-迭代指引与风险清单.md` 中为本方案建立索引,该动作属 `docs/10` 的落地事项,不在本目录职责内,本篇不代为执行、也不修改 `docs/` 下既有文件。)其中 `docs/09-迭代指引与风险清单.md` 记录的是**代码级存量风险**(§1 的 P0/P1/P2 三档、§4 的 10 条待确认问题),不是平台端的设计输入缺口;本篇在 `09` 表 9-5 把这些存量项按“对平台端的影响”重新对照,编号用 PR 系列,不与 `docs/10` 表 10-1 重复计数。
## 4. 已确认口径的遵循声明
以下口径在 `docs/10` v1.2 中已确认,本篇**只引用、不得反向改写**:
表 0-3 已确认口径与在本目录的落点
| 口径 | 确认来源 | 对本目录的约束 |
| --- | --- | --- |
| 差-1:保留 `utf8mb4_general_ci`,不逐列指定 `_ai_ci` / `_as_cs` | `docs/10` 表 5-4「字符集与排序」行、表 5-9 第 7 项 | 该口径只约束**公寓端 MySQL**;平台端若仍为 KingbaseES 则不适用,若改 MySQL(D13 选 B / C)则**同样沿用 `general_ci`**。见 `03` 表 3-16 |
| 差-3:不引入 `ruoyi-generator` | `docs/10` 表 2-2「后端框架」行 | 平台端若采用 RuoYi(P-04 待决),同样不引入代码生成模块 |
| 差-7:`apt` 入口用前端静态路由,`sys_menu` 仅权限点载体 | `docs/10` 表 2-3「前端路由」行 | 平台端前端若有 `platform` 入口,按同一口径处理;见 `02` §6 |
| G-01 / G-02 / G-03:本期走“不改库兜底”,暂不加字段 | `docs/10` v1.2 回填说明、`docs/公寓端/08` 表 8-9 | 平台端**不得**以“顺手补列”的方式处理楼栋、人员本地启停、交路停用原因三处;需要时走 `09` 的 P-05 单独评审 |
| 表数量:v1.2 为 44 张业务表(`APT_*` 23 + `GY_*` / `gygl_*` 18 + `duty_*` 3)+ 30 张若依 `sys_*`;**CR-02 后公寓本地库设计口径为 59 张**(+ `apt_local_task` 1 + CR-02 新增 13 + 补遗 `apt_sip_reg_status` 1),见 CR-02 §6、§10 | `docs/10` 表 5-9 第 1 项(v1.2)、`docs/公寓端/03` 表 3-8、CR-02 表 3-25 | 该口径是**公寓端本地库**的表数;平台库表数不在其中,本篇不引用该数字描述平台库;CR-02 新增表均为设计定义,DDL 待阶段二评审 |
## 5. 阅读顺序与使用方式
1. 先读 `01-功能边界.md`:确认“哪些能力归平台、哪些不归”,这是后续所有设计的判据。
2. 再读 `03-数据设计.md` 与 `05-跨端接口对齐.md`:一份定数据归属,一份定契约;两者必须一起评审,否则会出现“接口有字段但没有落点”。
3. `04` 与 `06` 是实施输入:`04` 给接口清单,`06` 给时序与失败补偿。
4. `07` / `08` 面向实施与运维;`09` 是决策入口,评审时按 `09` 表 9-1 逐条拍板,拍板结论回填 `docs/10` 与本目录,不另行新建文档。
5. 与 `docs/公寓端/` 的对称关系:`01`↔`01`、`03`↔`03`、`05`↔`09`、`06`↔`05`、`08`↔`06`、`09`↔`08`。两侧编号不必一一对应,但同一件事只能有一份权威表述,冲突时以“先冻结的一方”为准并同步另一方。
+259
View File
@@ -0,0 +1,259 @@
# 01 应用端(值班员端)功能边界
> 追溯:`docs/10` §3.3(应用端负责/不负责)、表 3-1(应用端与公寓端逻辑分工)、§2.5 表 2-3(路由与鉴权)、
> §4.3 表 4-3(`apt.duty.view`/`apt.duty.command`/`apt.duty.state`)、§5.3 第 6 条(状态单写入口)、§7.4(两级告警输出)、
> §11.1 现状基线("应用端应以此页为骨架做角色化拆分")、表 10-2(`D6`/`D7`/`D8`/`D11`/`D15`/`D16`)。
> 对端:`docs/公寓端/01` 表 1-3/表 1-4、`docs/公寓端/ui/README` 表 1.1、`docs/平台端/01` 表 1-1、CR-01 表 1-4~表 1-8。
> 现状:`YktVue/src/views/JcXx.vue`、`docs/frontend/04` 表(操作矩阵)、`docs/frontend/05`。
> **v2 修订(CR-02,2026-11-09)**:本端覆盖范围以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §2 为权威,见下文 §0;§5 表 1-9 的"不做任何账务动作"已按 CR-C03 撤销。
## 0. v2 覆盖范围修订(CR-02)
> 依据 `docs/公寓端/00` 的重新规划。本节图表用独立编号 `表 C-y`,不占用本文件 `表 1-y` 序号;原 §1~§6 与 CR-02 冲突处以 CR-02 表 3-4 为准。
表 C-1 功能域由 9 个扩为 13 个
| # | 新增域 | 页面 | 本期 | 入口 / 命令 | 落表 | 裁决 |
| --- | --- | --- | --- | --- | --- | --- |
| 10 | 换乘发起 | D1(只读弹层 + 操作区) | **是** | `TRANSFER` 受控命令(`commandId` + 原因) | `apt_transfer_order`、`duty_manual_action` | CR-C09 |
| 11 | 平板结果查看与处置 | D1 | **是** | `/ws/duty` `tablet.result` | 只读 `apt_tablet_call_result`、`gy_jcxx` | CR-C08 |
| 12 | 本地充值登记 / 本地余额查询(**本地口径**) | D8 | **是**(原"置灰"撤销) | `POST /duty/ledger/local-recharge`、`GET /duty/ledger/local-balance` | `apt_ledger_offline` | CR-C03 |
| 13 | 排班与盯控质量考核 | D7 / D10 | **是** | `duty:roster:*`、`duty:performance:*` | `duty_roster`、`duty_performance` | CR-C11 |
另增三处入口:保洁人员 / 记录登记(A-12)、上报积压与续报查看(A-14)、**人员注册审核与确认**(A-02,CR-C25)。`duty_shift_handover` 仍按原口径"后续",**不借本次启用**(CR-C11)。
> **CR-02 §12 补遗(注册审核职责)**:人员注册由**签到终端**发起 `REG_APPLY` 申请并上传人脸,值班台只做待审核对与**确认注册**(`duty:person:reg` + 二次确认,N-35 / N-36 / N-37),不直接新建人员、不写 `apt_master_person`。值班员确认后生成 `ryid`、人脸进入公寓服务系统,再分别上传平台与下发公寓内设备;平台回执不是注册生效前置。本端**不新增功能域编号**,仍无业务状态直写权限;`40914` 改判为"注册申请未获值班员确认"。完整链路见 `docs/公寓端/10` §4。
> **CR-02 §13 补遗(入住审核)**:值班台新增入住申请待审列表、详情、审核通过 / 换房通过 / 驳回与房间调整(N-42~N-46,`duty:checkin:*`);只提交命令,由 `apt-stay` 办理入住并建 `gy_jcxx` 行。审核通过时"已入住"为 `checkin_state=CHECKED_IN` 流程态 + `finish_ly='0' AND sjlysj IS NULL` 业务事实双重表达,**不新增 `gy_jcxx` 状态字段**(CR-C26);房间调整仅审核期允许(Q-38)。本端不新增功能域编号,仍无业务状态直写权限。
> **CR-02 §14 补遗(充值核对与食谱发布)**:值班台新增线下现金充值待核对列表与确认 / 作废(N-53~N-55,`duty:ledger:confirm` + 二次确认),只提交命令、由公寓端 `apt.ledger.local` 更新额度,本端**零余额表写权限**;公寓管理员发布 / 撤回阶段性食谱走 N-57(`apt:catering:recipe:*`)。扫码二维码只识别公寓、系统只记账,**本端不提供任何线上支付入口、不建支付订单与回调**。本端不新增功能域编号,仍无业务状态直写权限;不新增表与跨端编号。完整链路见 `docs/公寓端/12` §5、§6。
> **CR-02 §15 补遗(三端一致性同步)**:四能力在本端的职责边界按 **CR-02 §15 表 3-65 / 表 3-66** 收口——① **平板叫乘**:只读 `/ws/duty` `tablet.result` / `state.changed`,`UNVERIFIED` 标"未刷脸"并按 Q-22 触发复叫提示,值班员只发 `duty:call:*` 命令;断网结果本地缓存,恢复按 `resultId` 幂等续传,`EXPIRED` 任务不重派、平板离线回落 `SIP`。② **人脸签到与注册**:N-35 待审 / N-36 确认 / N-37 驳回(`duty:person:reg` + 二次确认),注册生效点是值班员 `CONFIRM`、平台回执不决定本地生效,人脸相关表一律只读。③ **消费**:只读账户额度(N-56)与流水(N-51),**无消费提交权限**,值班台只在 §14 F-M 承担线下充值核对。④ **菜品管理**:公寓管理员经 N-57 只提交代理命令,值班台只读食谱与选单;`40940 FEATURE_DISABLED` 不提供下单入口。四能力本端**一律不直写** `gy_jcxx` / `apt_tablet_*` / `apt_person_face_reg` / `apt_master_person` / `gy_ykt_*` / `YKT_MEALS`,只写 `duty_*` 操作域;异常码 `40913` / `40914` / `40916` / `40917`~`40924` / `40926`~`40940` / `50303` **只消费不自造**。平台统计报表对本端按 Q-50 默认**只读到公寓级分类统计与排名**,不开放跨公寓明细下钻与人员级数据;本端不写 `pl_apt_stat_*`、不触发 `S-03` / `S-06`。
**不变式**:本端仍无业务状态直写权限;所有新增动作只提交命令给 `apt.duty.command` 或公寓端领域服务,本端只写 `duty_*` 操作域;平台主账与跨公寓统计不在本端。表 1-6 的 20 个既有操作入口映射继续有效,其中第 14 项"现金充值置灰"改判为"本地充值登记(额度内、本地口径)"。
## 1. 定位与职责
### 1.1 一句话边界
> **应用端管"人看与人工干预":把"该谁叫、叫到哪一步、哪条要人管"压进一屏,把值班员的每一次干预变成一条可审计的命令——但界面没有写权限后门,任何干预都必须经公寓端领域服务落地。**
这句话是 `docs/10` §3.4 结尾"平台管号码规则与合规策略,公寓管资源、事件与媒体,应用端管人看与人工干预"的展开,展开的关键在最后半句:
`docs/10` 表 3-1"写权限"行明确要求"仅通过状态机服务写,禁止直连 Mapper 修改状态字段",`docs/公寓端/01` §1.1 已把这条写成"应用端有界面但没有写权限的后门"。
图 1-1 应用端的逻辑边界
```mermaid
flowchart TB
subgraph DUTY["应用端 apt-duty(同 JVM · 同库)"]
V["apt.duty.view<br/>盯控列表 / 详情 / 时间线"]
C["apt.duty.command<br/>人工干预命令编排"]
S["apt.duty.state<br/>状态字段唯一写入口"]
W["duty WebSocket<br/>/ws/duty 推送"]
A["duty_* 操作域<br/>审计 / 命令 / 交接"]
end
APT["公寓端领域服务<br/>apt-stay / apt-dispatch / apt-voice / apt-master-client"]
DB[("本地库 MySQL<br/>gy_* / apt_* / duty_*")]
AST["Asterisk + AMI"]
V -->|"只读"| DB
C -->|"提交命令,不写字段"| S
C -->|"换房 / 入寓 / 离寓 / 挂断"| APT
S -->|"条件更新 ryzt/finish_*/stoped"| DB
S -->|"订阅事件"| AST
A --> DB
S --> W
APT --> DB
V -.->|"不得反向依赖"| APT
```
图中三条硬约束:①`apt-duty` 可以依赖 `apt-stay`/`apt-dispatch`/`apt-voice` 的**服务接口**,**任何模块不得反向依赖 `apt-duty`**(`docs/公寓端/02` 表 2-2);
②所有状态字段写操作只出现在 `apt.duty.state` 包内;③跨端数据(平台)不由本端直接访问,一律经公寓端的 `#7`/`#8` 通道。
### 1.2 负责(对应 `docs/10` §3.3 负责清单,逐条落到本端)
表 1-1 应用端职责与落点
| # | 职责 | 后端落点 | 前端页面 | 主要表 | 依据 |
| --- | --- | --- | --- | --- | --- |
| 1 | 叫乘盯控:任务列表、报警、应答确认、临时叫乘、停止叫乘 | `apt.duty.view`、`apt.duty.command` | D1 盯控台、D2 详情抽屉 | `gy_jcxx`、`gy_bjxx`(只读)、`duty_manual_action` | §3.3 负责第 1 条 |
| 2 | 房间 / 多房间喊话,录音回放入口 | `apt.voice.endpoint`、`apt.voice.media`(调用方) | D1、D5 | `apt_call_session`、`apt_recording`(只读) | §3.3 负责第 1 条 |
| 3 | 房间与房态:选房、换房、停用 / 恢复 | `apt-stay`(调用方) | D3、D4 | `gy_rooms`、`gy_jcxx`、`gy_clean_task` | §3.3 负责第 2 条 |
| 4 | 保洁开始 / 结束 | `apt-stay`(调用方) | D4 | `gy_clean_task` | §3.3 负责第 2 条 |
| 5 | 在寓人员查看 | `apt.duty.view` | D1、D3 | `gy_jcxx`(`finish_ly='0'`) | §3.3 负责第 2 条 |
| 6 | 人工干预与审核:充值确认、资料修正、交路人工指定与重置、备注 | `apt.duty.command` + 各域服务 | D1、D8 | `duty_manual_action`、`duty_operation_log` | §3.3 负责第 3 条 |
| 7 | 操作留痕:每次干预的意图、原因、结果、操作人 | `apt.duty.command`(审计切面) | D8、D9 | `duty_operation_log` | §3.3 负责第 3 条;DDL 表注释 |
| 8 | 要素 / 时刻的本地指定与调整(CR-01 新增能力) | `apt.duty.command` → `apt.jcxx.local` | D1(行内"指定要素 / 调整时刻") | `gy_jcxx`(要素列)、`apt_local_task` | CR-01 表 1-7;`docs/平台端/01` 表 1-1 末行 |
| 9 | 实时提示:状态变化推送、报警音、TTS 播报、同步提醒 | `duty WebSocket`(`/ws/duty`) | D1、D6 | 无(内存态 + 推送) | §7.4 两级告警输出 |
| 10 | 班次交接(**后续**,本期只留表与页面占位) | `apt-duty` | D7 | `duty_shift_handover` | `docs/公寓端/03` 表 3-7("后续") |
### 1.3 不负责(对应 `docs/10` §3.3 不负责清单)
表 1-2 应用端不做的事与拒绝方式
| # | 不做 | 为什么不做 | 越界请求如何拒绝 |
| --- | --- | --- | --- |
| 1 | **不直接改设备指令表**(`apt_device_command`) | §3.3 不负责第 1 条:设备动作属执行域 | 值班台无该表写权限;需要时发广播/喊话,由 `apt-device` 入队 |
| 2 | **不直接改叫乘任务表**(`gy_jcxx` 的要素列与状态列) | §3.3 不负责第 1 条;`docs/公寓端/01` 表 1-3 写入口行 | 只暴露 `commands`;不存在 `PUT /duty/jcxx/{jcid}` 这种接口 |
| 3 | 不持有独立数据库 | §3.3 不负责第 2 条 | 无独立数据源;`AptContext` 只有 `gyid` |
| 4 | 不引入第三套服务 / 不做独立鉴权中心 | §3.3 不负责第 2 条;D6 | 不新建 Spring Boot 应用;登录复用 `/login` |
| 5 | 不承担跨公寓查询 | §3.3 不负责第 3 条 | 列表强制带 `gyid`;跨公寓返回 `40302` |
| 6 | 不改人员主数据、不改交路模板、不改运安快照 | CR-01 不变式 A;`docs/公寓端/03` 表 3-1/表 3-2 | 写 `apt_master_person`/`apt_master_route`/`gy_syn_jcxx` 返回 `40904 APT_SNAPSHOT_READONLY` |
| 7 | 不判定"钱"(补助标准、余额主账、餐饮结算) | `docs/平台端/01` 表 1-4 补助规则行、余额主账行 | 现金充值只做"确认动作"并留痕,余额由平台记账(P2 前不开放) |
| 8 | 不改号码规则、不定录音合规策略 | `docs/公寓端/01` 表 1-2 第 4 条 | 分机改号返回 `40906`;录音只回放不导出外带(D15) |
| 9 | 不锁定 / 不解除运安匹配(`LOCK_MATCH`/`UNLOCK_MATCH`) | CR-01 表 1-7:该权限归公寓管理员 | 值班台无该权限点;请求返回 `40301`,并提示"请联系公寓管理员" |
## 2. 与平台端、公寓端的分界
表 1-3 三端五维分界(**口径取自** `docs/公寓端/01` 表 1-3,本端只补"越界判定"列的检查方法)
| 维度 | 平台端 | 公寓端 | 应用端(本端) | 越界判定(可检查) |
| --- | --- | --- | --- | --- |
| 数据归属 | 主数据真源、计划、账务主账、跨公寓报表 | 执行事实、设备与语音资源、本地流水、快照 | 无独立数据;只在同库写 `duty_*` 操作域 | 出现第二数据源、或本端模块出现 `insert into apt_master_*` |
| 写入口 | 通过下行接口写平台库 | 领域服务 + 状态机单入口 | 只发命令(`duty_manual_action`) | `grep` 到 `apt-duty` 包内 `update gy_jcxx set ryzt` 即越界 |
| 部署形态 | 无状态多实例 + 平台主库 | 单实例同进程(`apt-admin`)+ 本地库 + Asterisk | 与公寓端同进程、同 jar、同库 | 出现 `apt-duty` 独立 `SpringBootApplication` 或独立 `pom` 打包 |
| 路由前缀 | `/platform/v1/**` | `/apt/**`、`/open/**` | `/duty/**` | `/duty/**` 出现在设备可达清单,或 `/open/**` 复用会话 |
| 账号体系 | 平台账号 | 本地 RuoYi 账号 + `gyid` | 复用公寓端账号 + 权限点 | 出现 `duty_user`/`duty_role` 表即越界 |
### 2.1 三类使用者在同一库上的切面
本端不是"另一套权限体系",而是在公寓端 RuoYi RBAC 之上按**角色 + 权限点 + 数据范围**切出三个视图。同一个 `gy_jcxx` 行,三类人的可见列与可发命令不同:
表 1-4 角色 × 能力切面
| 能力 | 值班员(`duty` 入口) | 公寓管理员(`apt` 入口) | 平台管理员(`platform` 入口) |
| --- | --- | --- | --- |
| 看本公寓叫乘列表 / 详情 / 时间线 | ✅ | ✅ | ❌(跨公寓看平台报表) |
| 看其他公寓数据 | ❌ `40302` | ❌ `40302` | ✅ 汇总与统计 |
| 发状态命令(`STOP`/`RE_CALL`/`FORCE_FINISH`/`MARK_LEAVE`/`CANCEL`/`HANGUP`) | ✅ | ✅ | ❌ |
| `ASSIGN_ELEMENT` / `ADJUST_TIME` | ✅(CR-01 表 1-7) | ✅ | ❌ |
| `LOCK_MATCH` / `UNLOCK_MATCH` | ❌ | ✅ | ❌ |
| 改 `gy_jcxx` 非状态字段(`leave_remark` 等白名单) | ⚠️ 仅 `leave_remark` | ✅ 白名单全量 | ❌ |
| 改人员主数据 / 交路模板 / 时间规则 | ❌ `40904` | ❌ `40904` | ✅ |
| 查 `duty_operation_log` | 本人记录 | 全量 | ❌(只收摘要) |
| 触发 `#8` 上报 | ❌(由 `apt-report` outbox 自动) | ❌ | ❌ |
**为什么值班员不能改 `gy_jcxx` 的 `fjh`/`jlid`/`cc`/`jch`**:这四列在 `docs/公寓端/04` 表 4-9 的 `correct` 白名单里是**允许**的,但那是给管理端的"数据校正"能力;
值班员的现场诉求是"这条的要素不对,我要指定/调整",对应的是 `ASSIGN_ELEMENT` 与 `ADJUST_TIME` 两条命令(带原因、带留痕、带平台核验),不是直接改列。
把"现场处置"与"数据纠错"分开,才能让 CR-01 的"锁定 + 上报 + 核验"闭环不被绕过。该收紧写入 `08` 表 8-1 的 `Q-18` 待确认。
## 3. 功能清单(9 个域)
表 1-5 功能域清单与实现判定
| # | 功能域 | 页面 | 本期 | 依赖的公寓端能力 | 本端模块 | 对应数据表 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 叫乘盯控 | D1、D2 | **是**(核心) | `apt.duty.state`、`gy_jcxx` 只读、`/ws/duty` | `apt.duty.view` / `apt.duty.command` | `gy_jcxx`、`gy_bjxx`(只读)、`duty_manual_action` |
| 2 | 房间与房态 | D3、D4 | **是**(查询 + 换房 + 停用/恢复 + 保洁) | `apt-stay` | `apt.duty.view`(调 `apt-stay`) | `gy_rooms`、`gy_jcxx`、`gy_clean_task` |
| 3 | 人工干预与要素调整 | D1、D8 | **是**(8 个命令 + 要素/时刻) | `apt.duty.state`、`apt.jcxx.local` | `apt.duty.command` | `duty_manual_action`、`apt_local_task`(经公寓端服务) |
| 4 | 语音与录音回放 | D5 | **是**(会议桥占用、会话、回放;降级可置灰) | `apt.voice.endpoint`、`apt.voice.media` | `apt.duty.view`(调 `apt-voice`) | `apt_call_session`、`apt_recording`(只读) |
| 5 | 设备与告警处置 | D6 | **是**(在线率、离线清单、报警确认) | `apt-device`、`apt.dispatch`(报警判定) | `apt.duty.view` | `gy_sb`、`gy_bjxx`(`handled_at` 可写) |
| 6 | 班次交接 | D7 | **否**(后续;表已建,页面占位) | 无 | `apt-duty` | `duty_shift_handover` |
| 7 | 操作审计 | D9 | **是**(本人可查;管理员全量) | 审计切面 | `apt-duty` | `duty_operation_log` |
| 8 | 健康与自检(值班视角只读) | D10 | **是**(只读 12 项 + `callReady`) | `apt-ops` | `apt.duty.view` | `apt_ops_selfcheck` |
| 9 | 实时推送与告警提示(横切) | 全页 | **是** | AMI 事件、状态机事件 | `duty.ws` | 无(内存态 + 推送) |
各域的"模块—表—接口—页面—权限点"完整映射见 `04` §2 与 `07` §5。
## 4. 既有值班台操作 → 目标态落点(一个都不丢)
现状 `JcXx.vue` 是唯一真实实现(5294 行),`docs/10` §1.1 明确"应用端应以此页为骨架做角色化拆分,而非另起一套"。
下表把该页 **20 个操作入口**逐条映射到目标态的页面、接口与落点模块;"变更性质"一列说明是**原样承接**、**换实现**还是**收缩/置灰**。
表 1-6 20 个操作入口的映射(现状来源:`YktVue/src/api/jcgl/jcgl.js`、`docs/frontend/04` §4)
| # | 现状操作 | 现状接口 | 目标页面 | 目标接口 | 落点模块 | 变更性质 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 查询 / 重置 | `GET /jcgl/getCcJcXx` | D1 | `GET /duty/jcxx` | `apt.duty.view` | 原样承接(去轮询改推送 + 保留 10 min 兜底) |
| 2 | 取消叫乘 | `POST /jcgl/cancelTrainAssignment` | D1 / D2 | `POST /duty/jcxx/{jcid}/commands`(`CANCEL`) | `apt.duty.command` | 换实现(命令化 + 幂等 + 原因) |
| 3 | 修改叫乘 | `POST /jcgl/modifyJcXx` | D1 / D2 | 同上(`ADJUST_TIME` + `PATCH /duty/jcxx/{jcid}` 仅 `leave_remark`) | `apt.jcxx.local` | 换实现(`jcsj` 唯一基准,派生重算) |
| 4 | 出乘 | `POST /jcgl/handleCheckOut` | D1 | `POST /duty/jcxx/{jcid}/commands`(`FORCE_FINISH`,`finish_ly='0'` 时) | `apt.duty.state` | 换实现(命令化) |
| 5 | 暂停报警(行内) | `POST /jcgl/handleStopCalling` | D1 | `STOP` + `POST /duty/bjxx/{id}/handle` | `apt.duty.state` | 换实现(两件事拆开:停叫 + 报警处置) |
| 6 | 停止报警(顶部) | 同上 | D1 | 同上 | `apt.duty.state` | 换实现(前置校验保留:仅 `ryzt=2/4`) |
| 7 | 语音临叫(单房间) | SIP + `POST /jcgl/handleRoomCall` | D1 | `POST /duty/call/{jcid}/voice` | `apt.voice.endpoint` | 换实现(SIP 目标由服务端分配,不再硬编码) |
| 8 | 喊话临叫(单房间) | SIP + `POST /jcgl/handleRoomCall2` | D1 | `POST /duty/call/{jcid}/broadcast` | `apt.voice.endpoint` | 换实现(同上) |
| 9 | 喊话临叫(多房间) | `POST /jcgl/handleMultiRoomCall` | D1 | `POST /duty/call/broadcast`(`roomIds[]`) | `apt.voice.endpoint` | 原样承接(补会议桥占用校验 `40908`) |
| 10 | 停止呼叫 | `POST /jcgl/handleMultiRoomCallEnd` / `handleRoomCallEnd` | D1 / D5 | `POST /duty/call/{sessionId}/hangup`(`HANGUP`) | `apt.voice.endpoint` | 换实现(会话级,不再按"多房间/当前行"猜) |
| 11 | 换房间 | `GET /jcgl/getRyFjXx` + `POST /jcgl/exchangeRoom` | D3 | `GET /duty/room/{roomId}/occupants`、`POST /duty/room/swap` | `apt-stay` | 原样承接(写 `apt_sip_binding` 联动) |
| 12 | 入寓登记 | `POST /jcgl/submitAll` | D3("入寓"面板) | `POST /duty/checkin` | `apt-stay`(`submit_rydj` 承接) | 换实现(**建行在 `apt-stay`,本端不写 `gy_jcxx`**;见 CR-01 S1) |
| 13 | 人员维护 | `GET /jcgl/getRyxxByRyId`、`POST /jcgl/modifyRyXx`、`/deleteRyXx` | D3 / D8 | `GET /duty/person/{ryid}`、`POST /duty/person/{ryid}/correction` | `apt-master-client`(只读)+ `apt.duty.command`(留痕) | **收缩**:人员快照只读,本端只记"修正申请",不写主数据 |
| 14 | 现金充值 | `POST /recharge/handleRecharge` | D8 | `POST /duty/ledger/local-recharge`、`GET /duty/ledger/local-balance` | `apt.ledger.local`(本期最小闭环) | **换实现(原置灰撤销)**:CR-02 CR-C03 改为"额度内本地充值登记 + 本地余额查询(本地口径)";平台余额主账仍归 `platform-ledger`,断网期间本地可用 |
| 15 | 交路管理 | `GET /jcgl/getJlxxList` | D1(只读弹层) | `GET /duty/route/{jlid}` | `apt-master-client` | **收缩**:只读;新增/编辑归管理端(D7 + G-04) |
| 16 | 待确认交路 | `POST /jlgl/confirmJlXx` | D1(只读提示) | — | `apt-master-client` | **收缩**:确认动作归管理端 |
| 17 | 历史数据 | `GET /jcgl/getCcJcXx_Ls` | D2 / D9 | `GET /duty/jcxx/history`、`GET /duty/audit` | `apt.duty.view` | 原样承接(分页 + 录音回放) |
| 18 | 设备信息 | `GET /heartBeat/getDeviceStatusInfo` | D6 | `GET /duty/device` | `apt-device` | 原样承接(离线整行标红) |
| 19 | 导出 Excel | `GET /excel/exportJcXxExcell` | D1(手动按钮) | `POST /duty/jcxx/export` | `apt.duty.view` | **换实现**:取消每 10 分钟自动下载,改手动 + 审计(陷阱见 §4.2) |
| 20 | 退出 | — | 全页 | `POST /logout` | `apt-system` | 原样承接(必须移除 `location.reload()` 的隐式副作用) |
### 4.1 保留下来的现状约束(新实现必须继续满足)
表 1-7 从现状继承的业务规则
| # | 规则 | 现状证据 | 目标态落点 |
| --- | --- | --- | --- |
| 1 | 状态码 6 值与配色:0 待叫 / 1 叫乘中 / 2 叫乘报警 / 3 待离寓 / 4 离寓报警 / 5 通话中 | `docs/frontend/04` §3.1 | `04` 表 4-2;颜色 + 文字**双编码**(不能只靠颜色) |
| 2 | 报警音状态机:`alarmVoiceActive` + `dismissedAlarmEvents`,停叫后同 key 不再响 | `docs/frontend/05` §4.1 | `05` §5;服务端下发 `alarmEventKey` |
| 3 | 同步提醒两级队列:800 ms 合并 + 10 s 自动关闭 + TTS 业务优先 | `docs/frontend/05` §5 | `05` §5.2 |
| 4 | 列表请求版本号防旧响应覆盖(`callListRequestVersion`) | `docs/frontend/04` §2 | 保留;改用 `seq` 字段由服务端回带 |
| 5 | 时间线校验 `kcsj > ylysj > jcbjsj > fjsj > jcsj` | `validateCallDutyTimeline` | **替换为** `jcsj ≤ fjsj ≤ jcbjsj < kcsj`(CR-01 表 1-3 #6) |
| 6 | 时间规则 `kjjcsjgz=150`、`zjjcsjgz=120`、`lysjgz=30` | `GET /jcgl/getSjgz` | 取 `apt_master_rule` 快照(只读,本端不改) |
### 4.2 现状陷阱清单(不得带进新实现)
表 1-8 现状 10 条代码级陷阱与目标态对策
| # | 陷阱 | 影响 | 目标态对策 |
| --- | --- | --- | --- |
| 1 | `setRowStyle` 在渲染函数里写 `this.message` | 额外渲染,行多时卡顿、跑马灯错行 | 行状态与消息解耦:服务端给 `displayState` + `noticeText`,前端纯函数渲染 |
| 2 | WebSocket `onclose` 里 `location.reload()` | 断线即整页刷新,正在填的二次确认丢失 | 指数退避重连(1/2/4/8/16 s,上限 30 s),刷新按钮人工触发 |
| 3 | 每 10 分钟自动下载 Excel | 浏览器无提示下载,磁盘堆积 | 改手动导出 + 导出审计(`duty_operation_log`) |
| 4 | SIP 目标 `sip:1001@10.32.100.166` 与 `actId` 硬编码 | 换环境必须改代码 | 服务端按 `jcid → fjh` 解析分机号,前端不再持有地址 |
| 5 | SIP 发送与后端调用无事务关系 | 界面"已呼叫"/实际未呼 或相反 | 以服务端 `apt_call_session` 为准,前端只展示;SIP 失败不阻断记录 |
| 6 | 停止呼叫只调后端,无 SIP `call_end` | Asterisk 侧残留会话 | `HANGUP` 命令内部同时下发 AMI 挂断 |
| 7 | `sipclient.connected` 断线后不清零 | 按钮仍可点 | 以 `apt_sip_endpoint.register_status` + AMI 双源判定 |
| 8 | `formatTime` 重复定义、`handleDelete` 用了未安装的 `$modal` | 行为不确定 / 运行时报错 | 统一 `src/shared/utils`;`$modal` 改为 `el-messagebox` 封装 |
| 9 | 5 个定时器(30 s / 10 min ×3 / 1 s)各自拉数据 | 刷新风暴、首屏慢 | 只保留 1 个 1 s 时钟 + 1 个 60 s 兜底轮询,其余改推送 |
| 10 | 运安离线提醒 10 分钟重复弹且不去重 | 反复打扰 | 去重 + 单次提示 + 恢复后自动清除(`apt_syn_jcxx_status`) |
## 5. 本期不做与置灰
表 1-9 分期与置灰清单
| 类别 | 内容 | 理由 / 依据 |
| --- | --- | --- |
| **本期做(原置灰撤销)** | 本地充值登记、本地余额查询(**本地口径**)、断网额度内受理 | CR-02 CR-C03;表 1-6 第 14 项同步改判;入口见 §0 表 C-1 |
| **本期不做** | 平台余额主账改写、跨公寓账务查询、补助规则引擎化 | 主账权威仍在平台(`docs/平台端/06` §6);规则引擎化属 P2-1 |
| **本期不做** | 班次交接(D7 页面只留占位) | `docs/公寓端/03` 表 3-7 标"后续" |
| **本期不做** | 跨公寓查询、跨公寓对比 | `docs/10` §3.3 不负责第 3 条 |
| **本期不做** | 本地应急新增交路 / 新增房间 | D7 落点是"对既有 jcxx 指定要素"(CR-01 §2.8),不是插交路行;G-04 无 `source` 列 |
| **本期不做** | 分机号修改 | `40906`;改号需先停用再新增(`docs/公寓端/04`) |
| **本期不做** | 录音外链 / 导出外带 | D15 未定稿,默认"90 天 + 仅内网 + 导出必审计" |
| **本期不做** | 第 2 个会议桥(并发) | D11 未定稿,默认单 `Conf-01` + 串行化,冲突 `40908` |
| **置灰** | 语音临叫 / 喊话按钮 | Asterisk 或 AMI 不可用时置灰,`title` 写"走串口广播 + 人工登记" |
| **置灰** | `LOCK_MATCH` / `UNLOCK_MATCH` 入口 | 权限归公寓管理员(表 1-4) |
| **置灰** | 人员资料"保存" | 只读快照,仅"提交修正申请"(`40904` 兜底) |
| **置灰** | 交路"新增 / 编辑 / 确认" | 归管理端;本端只读弹层 |
## 6. 待决策点对边界的影响
表 1-10 与本端相关的 7 个决策点
| # | 决策点 | 定了会改变什么 | 未定时的默认执行口径(本设计采用) |
| --- | --- | --- | --- |
| D6 | 应用端是否独立前端工程 | 若独立,需第二套构建与登录态 | **同一工程双入口**(`entries/apt` + `entries/duty`),本期只交付 `duty` 入口页面,不新建工程 |
| D7 | 交路人工维护落点 | 若开放本地应急新增,D1 需加"新建交路"入口 | **不开放**;"应急"落点=对既有 `gy_jcxx` 指定要素并锁定(CR-01 §2.8) |
| D8 | 叫乘时间以谁为准 | 若按原文"平台下发计划含 `jcsj`",`ADJUST_TIME` 将失去依据 | **按 CR-01 `D8'`**:平台给要素(`kcsj`/`jcfs`),公寓算 `jcsj`;本端只改 `jcsj` 并重算派生 |
| D11 | 会议桥粒度 | 若"每任务一桥",D5 的"占用 1/1"改成按桥列表 | **单 `Conf-01` + 串行化**;D5 只展示 1 路通话中,冲突提示 `40908` |
| D15 | 录音合规 | 放不放开外链 / 导出 | **不放开**;D5 只做短期签名回放(≤ 5 分钟) |
| D16 | Asterisk 高可用 | 降级横幅文案与恢复判定 | **单机 + 冷备(RTO 15 分钟)+ 降级广播**;横幅固定"串口广播 + 终端提示 + 值班台人工" |
| `Q-15` | 权限点前缀与调整幅度阈值 | `duty:jcxx:adjustTime` 是否复用 `apt:jcxx:changeStatus`;30 min 二次确认阈值 | 按 **`duty:jcxx:adjustTime` 独立权限点** + **`|Δ|≤30min` 一次确认、`>30min` 二次确认**先做,评审后回填 |
## 7. 边界维护机制
表 1-11 四道检查
| 机制 | 做法 | 触发时点 |
| --- | --- | --- |
| 表归属检查 | `duty_*` 只由 `apt-duty` 写;`gy_*` 由公寓端写;本端出现的 `update gy_*` 一律视为缺陷 | 每次 DDL / Mapper 评审 |
| SQL 检查 | 业务 SQL 必带 `gyid`;`ryzt`/`finish_jc`/`finish_ly`/`stoped` 的 UPDATE 只允许在 `apt.duty.state` 包内 | CI 静态检查 + Code Review |
| 接口检查 | 新增 `/duty/**` 必须声明权限点、幂等要求、错误码;**不得出现 `PUT /duty/jcxx/{jcid}`** 这类整行更新 | 接口评审(`04` §1 模板) |
| 依赖检查 | `apt-duty` 单向依赖公寓端服务接口;`grep` 到任一模块 `import ...apt.duty.*` 即失败 | `mvn dependency:analyze` + 架构测试(ArchUnit 规则见 `02` §3) |
@@ -0,0 +1,210 @@
# 02 技术基线与工程结构
> 追溯:`docs/10` §2.4 表 2-2(技术基线)、§2.5 图 2-3(工程形态)与表 2-3(路由/菜单/鉴权)、§4.3 表 4-3(`apt.duty.*`)、
> §4.4 表 4-4(模块依赖)、§2.5 末段(应用端只能通过领域服务写)。
> 对端:`docs/公寓端/02` 表 2-2(12 模块)、表 2-4(前端双入口)、表 2-5(两条菜单通道)、表 2-11/2-12(权限点)、`docs/公寓端/08` 差-7。
> 现状:`apartment/server/apt-duty/**`(仅占位)、`apartment/web/src/entries/duty/{router,views}`(空目录)、`YktVue/src/router/index.js`(现有路由无权限点)。
> **CR-02 §15 补遗(三端一致性 / 权限落点)**:平板叫乘、人脸签到、消费、菜品管理四项能力**不新增本端模块、不改本节技术基线锁定值**,入口复用既有 `/duty/**` 命令通道与双入口前端。权限点沿用 `duty:call:*`、`duty:person:reg`、`duty:checkin:*`、`duty:ledger:*` 与公寓管理端 `apt:catering:recipe:*`,**前后端同码**(`v-hasPermi` / `@PreAuthorize`)并登记 `sys_menu.perms`;统计报表只读入口使用平台端 `platform:stat:*`,**本端不持有该权限点、不写 `pl_apt_stat_*`**。三端状态映射以 CR-02 §15 表 3-66 为唯一权威。
## 1. 技术基线
表 2-1 本端锁定的技术基线(与公寓端**完全同源**,不单独选型)
| 项 | 版本 / 选型 | 说明 | 依据 |
| --- | --- | --- | --- |
| JDK | 1.8(`maven.compiler.source/target=1.8`) | 与公寓端同构建产物,不允许本端用 11+ 语法 | `docs/公寓端/02` 表 2-2 |
| Spring Boot | 2.5.15 | 同父 POM | 同上 |
| 框架 | RuoYi-Vue 3.8.9(`apt-framework`/`apt-system`) | **不魔改框架、不引入代码生成器**(差-3) | `docs/公寓端/08` 差-3 |
| ORM | MyBatis + XML(不用 MyBatis-Plus 动态表名) | 状态字段的条件更新必须是显式 SQL | `docs/10` §5.3 第 6 条 |
| 数据库 | MySQL 8.0,`utf8mb4_general_ci`,`datetime(3)` | 与公寓本地库一致:v1.2 已建 44 张 + CR-01 1 张 + CR-02 新增 13 张 + §10 补遗 1 张 `apt_sip_reg_status`(设计定义,DDL 待阶段二评审),统一 59 张口径;不新建库、不新建 schema | `docs/公寓端/03` 表 3-8、CR-02 §6 |
| 调度 | Quartz **单实例** | 叫乘触发在 `apt-dispatch`;本端**不注册任何 Job** | `docs/公寓端/02` 表 2-2 |
| 缓存 / 会话 | Redis(共享公寓端实例) | 本端不新建 Redis 库;会话键前缀 `duty:` | `docs/10` §2.5 表 2-3 |
| 语音 | asterisk-java 3.38.0-zfjia(AMI)+ 本地 SIP 客户端 | AMI 由 `apt-voice` 独占,本端**不直连 AMI** | `docs/公寓端/01` 表 1-1 #4 |
| 前端 | Vue 2.6.12 + element-ui 2.15.14 + axios | 与 `entry-apt` 同工程同依赖 | `docs/公寓端/02` 表 2-4 |
| 实时 | Spring `WebSocketHandler`(非 STOMP) | 收敛为 `/ws/duty` 一个端点 | `docs/10` §2.5 |
| 构建 | Maven 多模块,产物 `apt-admin.jar` | 本端**不产出独立可执行 jar** | `docs/公寓端/02` 表 2-2 |
三条"不引入":**不引入新的 MQ**(断网靠 outbox)、**不引入新的鉴权框架**(复用 RuoYi 会话 + 权限点)、**不引入前端状态持久化库**(本端状态都是服务端真源)。
## 2. 后端模块结构
`apt-duty` 是 12 个 `apt-*` 模块之一,包结构固定为三块(`docs/公寓端/02` 表 2-2:"值班 API、人工干预命令、WebSocket|`view`、`command`、`ws`")。
图 2-1 `apt-duty` 内部结构与对外依赖
```mermaid
flowchart LR
subgraph DUTY["apt-duty 模块"]
W["web<br/>DutyJcxxController<br/>DutyCallController<br/>DutyRoomController<br/>DutyAuditController"]
V["view<br/>DutyJcxxQueryService<br/>DutyTimelineService"]
C["command<br/>DutyCommandService<br/>ManualActionRepository"]
WS["ws<br/>DutyWebSocketHandler<br/>DutyPushService"]
A["audit<br/>DutyOperationAspect"]
end
STATE["apt.duty.state<br/>(公寓端 apt-dispatch 内)"]
STAY["apt.stay<br/>checkin / swap / clean"]
VOICE["apt.voice.endpoint<br/>apt.voice.media"]
REPO["apt.common<br/>AptContext / 错误码 / 分页"]
W --> V
W --> C
C --> STATE
C --> STAY
C --> VOICE
V --> REPO
C --> A
STATE --> WS
W --> REPO
```
表 2-2 包职责与"不许做的事"
| 包 | 职责 | 对外暴露 | **不许做** |
| --- | --- | --- | --- |
| `web` | `/duty/**` Controller、参数校验、DTO/VO 转换 | REST 接口 | 不许直接调 Mapper、不许写业务分支 |
| `view` | 盯控列表、详情、时间线、只读聚合查询 | `DutyViewService` 接口 | 不许有 `insert`/`update`/`delete` |
| `command` | 命令编排:`duty_manual_action` 落库 → 调 `apt.duty.state` → 回写结果 | `DutyCommandService` | 不许自己 `update gy_jcxx` |
| `ws` | `/ws/duty` 连接管理、按 `gyid` 定向推送、心跳与背压 | `DutyPushService` | 不许在 `ws` 包内做业务查询 |
| `audit` | `@DutyLog` 切面 → `duty_operation_log` | 注解 + 切面 | 不许吞异常后仍记 `success` |
## 3. 依赖白名单与架构测试
表 2-3 依赖规则(`docs/公寓端/02` 表 2-2 的白名单 + 本端补充)
| 方向 | 允许 | 禁止 | 违反后果 |
| --- | --- | --- | --- |
| `apt-duty` → 公寓端服务 | `apt.duty.state`、`apt.voice.endpoint`、`apt.voice.event`、`apt.stay`(入住/换房/保洁)、`apt.master.cache`(只读) | 直接依赖 `apt-device` 的指令写入、`apt-report` 的 outbox 写 | 引入即破坏"执行域唯一写入口" |
| 任一模块 → `apt-duty` | **无** | 全部禁止(含 `apt-admin` 启动类以外) | 编译期即可发现;ArchUnit 规则拦截 |
| `apt-duty` → 前端资源 | 无(后端不产页面) | — | — |
| `apt-duty` → 外部系统 | 无(不连平台、不连运安、不连 AMI) | 全部禁止 | 出现 `HttpClient` 调平台即越界 |
表 2-4 必须存在的 6 条 ArchUnit 断言(CI 阻断项)
| # | 规则 | 理由 |
| --- | --- | --- |
| 1 | `..apt.duty..` 之外的类不得 `import ..apt.duty..` | 应用端不得被反向依赖 |
| 2 | `..apt.duty.view..` 与 `..apt.duty.web..` 不得调用 `*Mapper.insert*/update*/delete*` | 只读层不许有写 |
| 3 | `update gy_jcxx set ryzt|finish_jc|finish_ly|stoped` 只允许出现在 `..apt.duty.state..`(源文件级 grep 断言) | `docs/10` §5.3 第 6 条 |
| 4 | `..apt.duty..` 不得直接引用 `javax.websocket`/`org.asteriskjava` | AMI 归 `apt-voice` |
| 5 | `..apt.duty.web..` 的类名必须以 `Duty` 开头且 `@RequestMapping` 前缀 `/duty/` | 路由前缀可检查 |
| 6 | 所有 `/duty/**` 的写接口必须带 `@PreAuthorize("@ss.hasPermi('duty:...')")` | 权限点不可漏 |
## 4. 前端边界
表 2-5 `entry-duty` 目录与引用规则(`docs/公寓端/02` 表 2-4)
| 目录 | 内容 | 可引用 | 禁止引用 |
| --- | --- | --- | --- |
| `src/entries/duty/router/` | 本入口路由(由 `sys_menu` 动态生成,只留兜底) | `src/shared/**` | `src/entries/apt/**` |
| `src/entries/duty/views/` | D1~D10 页面 | `src/shared/components`、`src/shared/api` | `src/entries/apt/views/**` |
| `src/entries/duty/components/` | 盯控卡片、状态徽标、倒计时、告警条 | `src/shared/**` | 同上 |
| `src/entries/duty/api/` | `/duty/**` 请求封装 | `src/shared/request` | 公寓端 `/apt/**` 封装 |
| `src/shared/**` | 图表、字典、时间工具、导出、WebSocket 客户端 | 无 | — |
图 2-2 双入口的依赖方向
```mermaid
flowchart TB
SH["src/shared<br/>(共用:request / ws / dict / utils)"]
APT["src/entries/apt<br/>管理端 16 页"]
DUTY["src/entries/duty<br/>值班台 D1~D10"]
APT --> SH
DUTY --> SH
APT -.x.-> DUTY
DUTY -.x.-> APT
```
**现状事实**:`apartment/web/src/entries/duty/router/` 与 `views/` 目前是**空目录**,`apartment/server/apt-duty/` 只有占位类。
即"值班台目前只有旧系统 `YktVue/src/views/JcXx.vue` 一份真实实现",新端一行业务代码都没有。本目录的文档是该模块的**首份设计输入**。
## 5. 路由与菜单
`docs/公寓端/02` 表 2-5 定下两条菜单通道,本端走的是**动态**那条(`docs/公寓端/08` 差-7 已登记):
表 2-6 两条通道对比
| 项 | `entry-apt`(管理端) | `entry-duty`(值班台,本端) |
| --- | --- | --- |
| 路由来源 | 前端**静态路由表** | `sys_menu` **动态生成** |
| `sys_menu` 角色 | 仅作权限点载体 | 路由 + 权限点双载体 |
| 加页面要做什么 | 改路由文件 + 发版 | 插 `sys_menu` 行(`menu_type='C'`)+ 角色授权,无需改前端代码 |
| 风险 | 路由与权限易脱节 | 菜单配置错会导致"页面存在但进不去" |
| 依据 | `docs/公寓端/02` §2.4 | `docs/10` §2.5 表 2-3;`docs/公寓端/08` 差-7 |
表 2-7 D1~D10 的路由约定(`component` 为 `src/entries/duty/views/` 下的相对路径)
| # | 页面 | 路由 | 组件 | 权限点 | 出稿 |
| --- | --- | --- | --- | --- | --- |
| D1 | 叫乘盯控台 | `/duty/jcxx` | `jcxx/DutyConsole.vue` | `duty:jcxx:list` | ✅ |
| D2 | 任务详情(抽屉) | `/duty/jcxx/:jcid` | `jcxx/DutyConsoleDetail.vue`(抽屉,非独立页) | `duty:jcxx:query` | ✅(内嵌 D1) |
| D3 | 房间与房态 | `/duty/room` | `room/DutyRoomList.vue` | `duty:room:list` | 待出稿 |
| D4 | 保洁任务 | `/duty/room/clean` | `room/DutyCleanList.vue` | `duty:room:clean` | 待出稿 |
| D5 | 语音与通话 | `/duty/voice` | `voice/DutyCallList.vue` | `duty:voice:list` | 待出稿 |
| D6 | 设备与告警 | `/duty/device` | `device/DutyDeviceList.vue` | `duty:device:list` | 待出稿 |
| D7 | 班次交接 | `/duty/handover` | `handover/DutyHandover.vue` | `duty:handover:*` | 待出稿(本期置灰) |
| D8 | 人工干预记录 | `/duty/manual` | `manual/DutyManualList.vue` | `duty:manual:list` | 待出稿 |
| D9 | 操作审计 | `/duty/audit` | `audit/DutyAuditList.vue` | `duty:audit:list` | 待出稿 |
| D10 | 健康与自检 | `/duty/health` | `health/DutyHealth.vue` | `duty:health:view` | 待出稿 |
**两条路由纪律**:①`/duty/:pathMatch(.*)*` 必须有兜底到 `/duty/jcxx` 的重定向(防止菜单删了但书签还在时白屏);②**任何 `/duty/**` 路由不得出现在设备可达清单**,`/open/**` 一律不复用会话(`docs/10` 表 2-3)。
## 6. 拦截器链与鉴权
表 2-8 `/duty/**` 的请求处理链(`docs/10` §2.5 表 2-3:"会话 + 权限点 + `gyid` 数据域")
| 序 | 环节 | 实现 | 失败返回 |
| --- | --- | --- | --- |
| 1 | Nginx | `location /duty` 与 `/apt` 指向**同一个 SPA**(`try_files … /index.html`) | 静态资源 404 |
| 2 | 前端守卫 | 无 token → `/login`;有 token 但菜单未加载 → 先拉 `getRouters` | 跳登录 |
| 3 | 会话校验 | RuoYi `JwtAuthenticationTokenFilter`,与公寓端同一套 | `40101` |
| 4 | 权限点 | `@PreAuthorize("@ss.hasPermi('duty:...')")` | `40301` |
| 5 | `gyid` 数据域 | `AptContext` 从令牌取 `gyid`,**忽略请求体里的 gyid** | `40302` |
| 6 | 幂等 | 写接口校验 `commandId`/`action_id`(L2 级别,见 `04` §1.4) | `40907` |
| 7 | 审计 | `@DutyLog` 落 `duty_operation_log` | 不影响主流程(记 `WARN`) |
三条"不做":**不做独立登录页**(复用 `/login`)、**不做独立 token 签发**、**不做独立角色体系**(`sys_role` 里加 `duty_operator` 角色即可)。
## 7. 权限点清单
沿用 `{端}:{域}:{对象}:{动作}` 格式(`docs/公寓端/02` 表 2-11)。本端前缀统一 `duty:`,与公寓端 `apt:` 并列,**不复用 `apt:` 前缀**(理由见 `08` 表 8-1 `Q-15`)。
表 2-9 `duty:*` 权限点清单(12 个动作 + 6 个查询)
| 权限点 | 用途 | 对应接口 | 现状对应操作 |
| --- | --- | --- | --- |
| `duty:jcxx:list` | 盯控列表 | `GET /duty/jcxx` | 查询 |
| `duty:jcxx:query` | 详情 + 时间线 | `GET /duty/jcxx/{jcid}` | 行点击 |
| `duty:jcxx:export` | 手动导出 | `POST /duty/jcxx/export` | 导出 Excel |
| `duty:jcxx:remark` | 改 `leave_remark` | `PATCH /duty/jcxx/{jcid}` | 备注 |
| `duty:call:stop` | 停止叫乘 | `commands{STOP}` | 暂停报警 / 停止报警 |
| `duty:call:recall` | 复叫 | `commands{RE_CALL}` | (现状无独立入口) |
| `duty:call:cancel` | 取消任务 | `commands{CANCEL}` | 取消叫乘 |
| `duty:call:forceFinish` | 强制结束 | `commands{FORCE_FINISH}` | 出乘 |
| `duty:call:markLeave` | 登记离寓 | `commands{MARK_LEAVE}` | (现状走 `submitAll` 之后) |
| `duty:call:voice` | 语音临叫 | `POST /duty/call/{jcid}/voice` | 语音临叫 |
| `duty:call:broadcast` | 喊话 / 多房间喊话 | `POST /duty/call/broadcast` | 喊话临叫 |
| `duty:call:hangup` | 挂断会话 | `POST /duty/call/{sessionId}/hangup` | 停止呼叫 |
| `duty:jcxx:assignElement` | 指定要素(CR-01) | `commands{ASSIGN_ELEMENT}` | 无(新增能力) |
| `duty:jcxx:adjustTime` | 调整时刻(CR-01) | `commands{ADJUST_TIME}` | 修改叫乘 |
| `duty:room:list` / `duty:room:swap` / `duty:room:clean` | 房态 / 换房 / 保洁 | `/duty/room/**` | 换房间、房间信息 |
| `duty:device:list` / `duty:voice:list` / `duty:health:view` | 只读三页 | `/duty/device`、`/duty/voice`、`/duty/health` | 设备信息 |
| `duty:manual:list` / `duty:audit:list` / `duty:handover:*` | 记录与交接 | `/duty/manual`、`/duty/audit`、`/duty/handover` | 历史数据 |
**三条命名纪律**:①**不存在** `duty:jcxx:lockMatch`(归 `apt:jcxx:lockMatch`);②**不存在** `duty:*` 的删除类权限点(本端无删除业务);③`duty:jcxx:adjustTime` 与 `apt:jcxx:changeStatus` 的取舍登记为 `Q-15`,未定前按独立权限点实现(撤回成本低)。
## 8. 分层与编码规范
表 2-10 本端编码约束(延续 `docs/公寓端/02` §3,只列本端差异)
| 项 | 约束 | 理由 |
| --- | --- | --- |
| Controller 粒度 | 一个页面一个 Controller(`DutyJcxxController`/`DutyCallController`/…),路径前缀 `/duty/{域}` | 便于按菜单授权 |
| Controller 互调 | `/duty/**` 与 `/apt/**` 的 Controller **不得互相调用**,只能共同调领域服务(`docs/公寓端/01` 表 1-3 细则 3) | 防止"绕开鉴权" |
| 时间格式 | 入参出参统一 `yyyy-MM-dd HH:mm:ss`;事件类精确到毫秒(`yyyy-MM-dd HH:mm:ss.SSS`) | `docs/公寓端/07` 表 7-6 专项 2 |
| 空值约定 | 列表为空返回 `200 + total:0`,不返回 `null` | 同上 专项 3 |
| 错误信息 | `msg` 必须含**字段名/业务对象名**,禁止只返回"操作失败" | 值班员要在 3 秒内知道改哪里 |
| 前端请求 | 所有写请求必须带 `commandId`(前端生成 `uuid`,重试复用) | L2 幂等 |
| 前端轮询 | 只允许 1 个 1 s 时钟 + 1 个 60 s 兜底轮询;其余靠 `/ws/duty` | `01` 表 1-8 第 9 条 |
| 前端告警音 | 单例 `Audio` + 统一 `AlarmVoiceController`,禁止页面各自 `new Audio()` | `docs/frontend/05` §4.1 的两处独立实现 |
| 前端 TTS | 单队列 + 业务语音优先(`isBusinessVoiceBusy`) | `docs/frontend/05` §5.3 |
+296
View File
@@ -0,0 +1,296 @@
# 03 数据设计
> 追溯:`docs/10` §5.1 表 5-1(数据分层与归属)、§5.3 第 2/5/6 条(命名分区、`gyid` 隔离、状态单写入口)、§5.4 表 5-4(字段口径)、§7.9 表 7-5(状态迁移)。
> 对端:`docs/公寓端/03` 表 3-1~表 3-16(表归属、命名分区、状态写入口、缺口)、`docs/公寓端/04` 表 4-9,CR-01 表 1-5/表 1-6/表 1-17。
> 实际 DDL:`apartment/db/mysql/07_report_duty_ops.sql`(`duty_*` 三表与 `apt_ops_selfcheck`)、`03_execution.sql`(`gy_jcxx` 等)、`02_plan_task.sql`。
> **v2 修订(CR-02,2026-11-09)**:新增 2 张自有表与可写边界以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §6 为权威,见下文 §0。
## 0. v2 数据口径修订(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 3-y` 序号;§1~§9 原有列定义与只读依赖继续有效。
表 C-1 新增 2 张自有表(列级定义见 CR-02 表 3-29)
| 表 | owner | 唯一写入模块 | 上报源 | 本期 |
| --- | --- | --- | --- | --- |
| `duty_roster` | duty | `apt-duty` | 否 | 是 |
| `duty_performance` | duty | `apt-duty` | 是(#13 日汇总) | 是 |
表 C-2 可写 / 只读边界(增量)
| 类别 | 表 |
| --- | --- |
| 本端可写(自有或经领域服务) | `duty_roster`、`duty_performance`、`duty_manual_action`、`duty_operation_log` |
| 只读 | `apt_tablet_task` / `apt_tablet_call_result`、`apt_transfer_order`、`apt_ledger_offline`、`apt_report_history` / `apt_report_retry_batch`、`apt_clean_staff` / `apt_clean_record`、`apt_person_face_reg`、`apt_device_alarm` |
| 仍不可写 | `apt_master_*`、`gy_syn_jcxx`、`apt_device_command`、`apt_report_outbox`、`gy_jcxx` 状态字段(`ryzt` / `finish_*` / `stoped`) |
> **CR-02 §12 补遗**:`apt_person_face_reg`、`apt_master_person`、`gy_sb.face_scope_*`、`gy_sb_xt.face_cache_*` 对本端**一律只读**;值班员注册确认只经 N-36 提交命令,由 `apt.face.registry` 与 `apt-master-client` 事务写入,本端仍只写 `duty_*` 操作域(`duty_manual_action`、`duty_operation_log`)。人脸模块**不新增表**,公寓本地库仍 59 张;人员主数据照片与特征值按 CR-C24 允许跨端,业务抓拍与录音仍不出公寓。
> **CR-02 §13 补遗**:`apt_local_task` 的入住申请单补列、`gy_jcxx` / `gy_rooms` / `gygl_ly_event` 对本端**一律只读**;值班台 N-42~N-46 只提交审核命令,由 `apt-stay` 办理入住并建 `gy_jcxx` 行。入住作业流程同样**不新增表**(59 张不变),本端不新增 `gy_jcxx` 状态字段。
> **CR-02 §14 补遗**:`gy_ykt_remainder` / `gy_ykt_consume` / `gy_ykt_cash_request` / `gy_ykt_xjczjl` / `apt_ledger_offline` 与平台库 `YKT_MEALS` 对本端**一律只读**;管理员充值核对只经 N-54 / N-55 提交命令,由 `apt.ledger.local` 写入;食谱发布只经 N-57 代理到平台餐饮管理端。本端仍只写 `duty_*` 操作域(`duty_manual_action`、`duty_operation_log`)。消费 / 充值 / 菜品管理同样**不新增表**(59 张不变),本端无余额与食谱主数据写权限。
表 C-3 本端视角三端状态映射(CR-02 §15;**唯一权威为 CR-02 §15 表 3-66**,本表只投影本端可见 / 可写,不另立第二真源)
| 能力 | 本端只读字段 / 取值 | 对端权威状态 | 同步载体 | 本端可写 | 终态与回退 |
| --- | --- | --- | --- | --- | --- |
| 平板叫乘 | `faceVerifyState` `VERIFIED` / `UNVERIFIED` / `SKIPPED`;`callResultType` `ANSWERED` / `NO_ANSWER` / `FAILED` / `CANCELLED`;`applied_state` `PENDING` / `APPLIED` / `IGNORED`;`pull_state` `WAITING` / `PULLED` / `DONE` / `EXPIRED` | 公寓端 `gy_jcxx.ryzt` / `finish_jc`(`apt.duty.state` 唯一写);平台端 `PL_TASK_ACK` / `PL_REPORT_DAILY.call_count` | #9 `TABLET_RESULT`、#8 `TASK_ACK`、#13 `ARCHIVE`;`/ws/duty` `tablet.result`(端内) | **无**(只提交复叫命令) | `VERIFIED`→`ANSWERED`→`ryzt=3` / `finish_jc='1'` 终态不回写;`UNVERIFIED` / `SKIPPED` 保持待确认 + 复叫;迟到 / 重复 `IGNORED`;存量 `ryzt` 数字码不跨端 |
| 人脸签到与注册 | `reg_state` `APPLIED` / `CONFIRMED` / `REJECTED` / `ARCHIVED`;`regVerdict` `PENDING` / `ACCEPTED` / `REJECTED`;`upload_state` / `dispatch_state` 双链 | 公寓端 `apt_master_person`(生效点 = 值班员 `CONFIRM`);平台端 `pl_person_reg_request` 同步回执 | #9 `PERSON_REG`、#2 `personRegVerdicts[]`;端内 `FACE_ISSUE` / `FACE_ERASE` | `duty_manual_action` / `duty_operation_log`(命令留痕) | 平台 `REJECTED` **不回滚**本地注册与已下发设备(Q-35);`CONFIRMED` 前 `40914` 禁止匹配 / 下发 |
| 消费 | `ledger_state` `SUBMITTED` / `DEDUCTED` / `REPORTED` / `RECONCILED` / `REJECTED` / `REVERSED`;`offline_scope` `LOCAL_ONLY` / `SYNCED` / `RECONCILED`;`reportState` 五值 | 公寓端 `apt_ledger_offline` / `gy_ykt_remainder`;平台端 `PL_LEDGER_FLOW.reconcile_state` `RECEIVED` / `RECONCILED` / `DIFF` | #10 `LEDGER_OFFLINE`、#16 日对账 | **零余额表写权限**(仅 N-54 / N-55 充值核对命令) | `DIFF` + `PF-5003` 阻断调账;冲正只走 `POST /adjustments`;`RECONCILED` 后不得直接改余额 |
| 菜品管理 | `publish_state` `DRAFT` / `PUBLISHED` / `EXPIRED` / `REVOKED`;`recipe_version` 单调 | 平台库 `YKT_MEALS`(`platform.catering` 唯一写) | #4 `recipes[]` + `recipeWatermark`(公寓 GET 拉) | N-57 只提交**代理命令**,不写 `YKT_MEALS` | 改价 / 下架生成新版本不改已发布版;远程点菜 `features.remoteOrdering=false` 恒 `40940` |
| 平台统计 | `pl_apt_stat_period.state` `DRAFT` / `FINAL` / `REVISED`、`data_status` `OK` / `NO_DATA` / `BLOCKED`;`pl_apt_stat_rank.rank_no` | 平台端 `platform-report` 唯一写;**无跨端交互** | S-01 / S-02(平台管理端只读) | **无**(Q-50 默认只读公寓级) | 统计结果不回写本端 / 公寓端任何业务状态;修正走 `revision_no+1` |
> 本端仍**只写 `duty_*` 三张自有表**(`duty_operation_log` / `duty_manual_action` / `duty_shift_handover`,后者本期不实现);表 C-3 中全部业务字段均为只读投影。三端完整枚举、同步载体、幂等与三时间戳口径见 CR-02 §15 表 3-66。
§1 "自有 3 张"口径更新为**自有 5 张**(含仍不启用的 `duty_shift_handover` 共 6 张定义,本期启用 5 张);公寓本地库业务表口径统一 **59 张**(CR-02 表 3-25、§10 补遗 `apt_sip_reg_status`)。新增表全部带主键、`gyid` 打头的唯一键,考核指标由本地从事实表计算、平台只汇总(`source=LOCAL_CALC`)。
## 1. 表归属:本端只"自有 3 张、经服务写 2 处、只读 9 张"
本端没有独立库。下表把与本端相关的表按"本端能不能写"分三类,**这是本端最需要被检查的一张表**。
表 3-1 表归属与写入方
| 类别 | 表 | owner | 谁能写 | 本端能做 | 依据 |
| --- | --- | --- | --- | --- | --- |
| **A 本端自有** | `duty_operation_log` | `duty` | 审计切面(`apt-duty`) | 追加 + 查询 | `docs/公寓端/03` 表 3-7 |
| **A 本端自有** | `duty_manual_action` | `duty` | `apt.duty.command` | 追加 + 回写 `state`/`result` | 同上 |
| **A 本端自有** | `duty_shift_handover` | `duty` | `apt-duty`(本期不实现) | 后续 | 同上 |
| **B 经服务写** | `gy_jcxx`(状态列 4 个) | `apartment` | **仅 `apt.duty.state`** | 发命令,不写字段 | `docs/公寓端/03` 表 3-9 |
| **B 经服务写** | `gy_jcxx`(要素/时刻列) | `apartment` | **仅 `apt.jcxx.local`**(经 `ASSIGN_ELEMENT`/`ADJUST_TIME`) | 发命令,不写字段 | CR-01 表 1-4、表 1-17 |
| **B 经服务写** | `apt_local_task` | `apartment` | **仅 `apt.jcxx.local`** | 只读查看(`match_state`/`verdict_state`) | CR-01 表 1-17 |
| **B 经服务写** | `gy_bjxx`(`handled_at`/`handled_by`) | `apartment` | `apt.duty.command`(条件更新)+ `apt-dispatch`(写入) | 处置报警 | `docs/公寓端/03` 表 3-9 第 7 行 |
| **B 经服务写** | `gygl_ly_event`(`leave_type=MANUAL`) | `apartment` | `apt-stay`(`MARK_LEAVE` 触发) | 发命令 | `docs/公寓端/04` 命令语义表 |
| **B 经服务写** | `gy_rooms.room_status` / `gy_clean_task.state` | `apartment` | `apt-stay` | 发请求 | `docs/公寓端/03` 表 3-9 |
| **C 只读** | `gy_jcxx_his` | `apartment` | 归档任务 | 历史查询 | 表 3-3 |
| **C 只读** | `apt_call_session` / `apt_call_event` | `apartment` | `apt.voice.event` | D5 展示 | 表 3-5 |
| **C 只读** | `apt_recording` | `apartment` | `apt.voice.media` | 回放(签名地址) | 表 3-5 |
| **C 只读** | `apt_sip_endpoint` | `apartment` | `apt.voice.endpoint` | D5 展示注册状态 | 表 3-5 |
| **C 只读** | `gy_sb` / `gy_sb_xt` | `apartment` | `apt.device.*` | D6 展示 | 表 3-4 |
| **C 只读** | `apt_ops_selfcheck` | `apartment` | `apt-ops` | D10 展示 | 表 3-7 |
| **C 只读** | `apt_master_rule` / `apt_master_route` | `platform-down` | `apt-master-client` | 读时间规则 / 交路 | 表 3-1 |
| **C 只读** | `apt_master_person` | `platform-down` | `apt-master-client` | 人员只读 | 表 3-1 |
| **C 只读** | `apt_report_watermark` | `apartment` | `apt.report.outbox` | 断网横幅 | 表 3-7 |
**三条判据**:①A 类之外的写都经服务,`apt-duty` 包内出现 `update gy_*` 即越界;②`apt_local_task` 虽由本端命令**触发**,但写入方是公寓端 `apt.jcxx.local`,本端只有读权限;③`gy_bjxx.handled_*` 是本端唯一能写的执行域字段,且必须带 `where id=? and handled_at is null`。
## 2. `duty_operation_log`(操作审计,业务级)
表 3-2 列定义(来源:`07_report_duty_ops.sql` 第 63-81 行,逐列照抄)
| 列 | 类型 | 语义 | 本端写入规则 |
| --- | --- | --- | --- |
| `id` | bigint AI | 代理主键 | 自增 |
| `gyid` | varchar(32) | 公寓编码,隔离第一列 | 取自 `AptContext`,**不接受入参** |
| `operator_id` | varchar(64) | 操作人 ID | 取自会话 `userId` |
| `operator_name` | varchar(64) | 操作人姓名 | 取自会话 `userName`(冗余存,防改名后查不到) |
| `module` | varchar(32) | 模块枚举:`DUTY`/`VOICE`/`ROOM`/`LEDGER`/`DEVICE`/`MASTER` | 本端固定 `DUTY`(换房同时记 `ROOM` 一条) |
| `action` | varchar(64) | 动作:`RECALL`/`STOP`/`HANGUP`/`CHECK_IN`/`CHECK_OUT` 等 | 与命令名对齐,**大小写统一大写** |
| `biz_type` | varchar(32) | 业务类型 | `JCXX`/`SESSION`/`ROOM`/`ALARM`/`PERSON` |
| `biz_id` | varchar(128) | 业务 ID | `jcid` / `sessionId` / `roomId` / `bjxxId` |
| `detail` | json | 请求与结果快照 | **必须含** `commandId`、`reason`、`before`、`after`、`result` |
| `client_ip` | varchar(64) | 客户端 IP | 取 `X-Forwarded-For` 首段(经 Nginx) |
| `occurred_at` | datetime(3) | 发生时间 | 服务端时间,**不用前端时间** |
| `create_time` | datetime(3) | 创建时间 | 默认值 |
索引:`idx_oplog_time(gyid, occurred_at)`、`idx_oplog_biz(gyid, biz_type, biz_id)`、`idx_oplog_operator(gyid, operator_id, occurred_at)`。
三条约定:
1. **两层审计并存**:`sys_oper_log`(若依 `@Log`,HTTP 级)+ `duty_operation_log`(业务级,`docs/公寓端/03` 表 3-15)。D9 页面查后者,前者只做合规兜底。
2. **本表只追加不更新**:命令结果变化写**新行**(`action=EFFECT`),不改原行,保证"意图"与"结果"可分别追溯。
3. **保留期**:与 `docs/10` 表 8-2"值班员操作审计保留期与查询入口在应用端可查"一致;具体天数登记为 `Q-19`(见 `08` 表 8-1),未定前与平台审计同口径,**不做自动清理**。
## 3. `duty_manual_action`(人工干预命令,命令级)
表 3-3 列定义(来源:`07_report_duty_ops.sql` 第 86-106 行)
| 列 | 类型 | 语义 | 本端写入规则 |
| --- | --- | --- | --- |
| `id` | bigint AI | 代理主键 | 自增 |
| `gyid` | varchar(32) | 公寓编码 | 取自 `AptContext` |
| `action_id` | varchar(64) | 干预业务 ID(**幂等键**) | = 请求的 `commandId`;UK 保证只生效一次 |
| `action_type` | varchar(32) | 干预类型 | 见下方 §3.1 的 10 个取值 |
| `target_type` | varchar(16) | `JCXX`/`SESSION`/`CLEAN`/`TASK` | 状态命令用 `JCXX`,挂断用 `SESSION` |
| `target_id` | varchar(128) | 目标 ID | `jcid` / `sessionId` |
| `reason` | varchar(512) | 干预原因 | **危险动作必填**(`STOP`/`FORCE_FINISH`/`CANCEL`/`ASSIGN_ELEMENT`/`ADJUST_TIME`) |
| `operator_id` / `operator_name` | varchar(64) | 操作人 | 取自会话 |
| `state` | varchar(16) | `ACCEPTED`/`DONE`/`FAILED` | 提交成功 `ACCEPTED` → 状态机返回后置 `DONE`/`FAILED` |
| `result` | varchar(512) | 处理结果 / 失败原因 | 失败时必须写错误码 + 中文说明 |
| `occurred_at` | datetime(3) | 发起时间 | 服务端时间 |
| `finished_at` | datetime(3) | 完成时间 | 状态机返回时写 |
| `create_time` | datetime(3) | 创建时间 | 默认值 |
约束:`uk_manual_action(gyid, action_id)`、`idx_manual_target(gyid, target_type, target_id)`、`idx_manual_state(gyid, state, occurred_at)`。
### 3.1 `action_type` 的 10 个取值与本端可用性
表 3-4 命令与落库取值
| # | `action_type` | 是否状态迁移 | 本端可否发 | `target_type` | 落库 `state` 终值 |
| --- | --- | --- | --- | --- | --- |
| 1 | `STOP` | 是(`stoped='1'`) | ✅ | `JCXX` | `DONE` |
| 2 | `RE_CALL` | 是(清 `stoped`、重排) | ✅ | `JCXX` | `DONE` |
| 3 | `FORCE_FINISH` | 是(`finish_jc='1'`+`finish_ly='1'`) | ✅ | `JCXX` | `DONE` |
| 4 | `MARK_LEAVE` | 是(写 `gygl_ly_event`) | ✅ | `JCXX` | `DONE` |
| 5 | `CANCEL` | 是(`ryzt=0` → `stoped='1'`) | ✅ | `JCXX` | `DONE` |
| 6 | `HANGUP` | 否(会话级) | ✅ | `SESSION` | `DONE` |
| 7 | `ASSIGN_ELEMENT` | 否(要素写 + 锁定) | ✅ | `JCXX` | `DONE` |
| 8 | `ADJUST_TIME` | 否(时刻写 + 派生重算) | ✅ | `JCXX` | `DONE` |
| 9 | `LOCK_MATCH` | 否(`jllx='1'`) | ❌ 管理员 | `JCXX` | — |
| 10 | `UNLOCK_MATCH` | 否(`jllx='0'`) | ❌ 管理员 | `JCXX` | — |
**与现状 DDL 的差异(已登记)**:`action_type` 的列注释只列了 `RECALL/STOP/FORCE_FINISH/HANGUP/CANCEL` 五个,实际需要 10 个。
列本身是 `varchar(32)` 且**无 CHECK**,因此不需要改表,只需在 DDL 注释与字典中补齐(登记为 `G-D01`,见 §10)。
另:本端用 `RE_CALL` 而 DDL 注释写 `RECALL`,**以本文档 `RE_CALL` 为准**(与 `docs/公寓端/04`、`docs/公寓端/05` 表 5-7 一致),DDL 注释同步修正。
### 3.2 为什么必须"两张表"而不是一张
`docs/公寓端/05` §7 已给出理由,本端补一条"值班视角"的:
| 表 | 记什么 | 谁在看 | 可归档 | 能否改历史 |
| --- | --- | --- | --- | --- |
| `duty_manual_action` | 命令**意图与结果**:谁按了哪个按钮、为什么、状态机认不认 | 当班值班员、平台核验、CR-01 对账 | 按季度归档(保留 1 年) | 不改,只追加新版本行 |
| `duty_operation_log` | 审计**事实**:请求参数、前后值、IP、耗时 | 审计、事后追责、G-03/G-05 的兜底载体 | 按合规要求(`Q-19`) | 不改,只追加 |
**合成一张表的后果**:①`detail` 是 `json`,无法被"按命令类型统计采纳率"这类查询高效利用;②归档策略不同(一个 1 年、一个按合规),合表后无法分别清理;
③CR-01 的 `#8` 上报需要"命令级"粒度(`localVersion` 与 `overrideReason`),审计表不该被上行链路读取。
## 4. `duty_shift_handover`(交接班,本期不实现)
表 3-5 列定义(来源:`07_report_duty_ops.sql` 第 111-127 行)
| 列 | 类型 | 语义 | 本期 |
| --- | --- | --- | --- |
| `id` | bigint AI | 主键 | — |
| `gyid` | varchar(32) | 公寓编码 | — |
| `shift_id` | varchar(64) | 班次业务 ID | UK `(gyid, shift_id)` |
| `handover_at` | datetime(3) | 交接时间 | — |
| `from_operator` / `to_operator` | varchar(64) | 交班 / 接班人 | — |
| `on_duty_count` | int | 交班时在寓人数 | 取自 `gy_jcxx where finish_ly='0'` |
| `pending_snapshot` | json | 待办快照(未完成叫乘 / 未处理报警) | 结构见 `04` 表 4-11 |
| `remark` | varchar(1024) | 交接说明 | — |
| `confirmed_at` | datetime(3) | 接班人确认时间 | 为空表示"未签收" |
| `create_time` | datetime(3) | 创建时间 | — |
**本期结论**:`docs/公寓端/03` 表 3-7 标"后续",因此 **D7 页面本期置灰**(`01` 表 1-9),表结构保留不动。
理由:交接班不是孤立页面——它要求"未结事项"可判定(哪些算未结),而"未结"的定义依赖 `duty_manual_action` 的 `state` 与 `gy_bjxx.handled_at` 两个来源稳定运行一个班次后才有意义。
## 5. `apt_local_task`(CR-01 新增,本端只读)
表 3-6 本端可见的字段(完整定义见 CR-01 表 1-17,本端**不维护**该表)
| 列 | 本端用途 | 可见性 |
| --- | --- | --- |
| `task_id` | 与 `jcid` 同源的任务业务键 | 只读(D2 展示) |
| `origin` | `CHECKIN_ROUTE`/`CHECKIN_PENDING`/`LOCAL_ASSIGN`/`LOCAL_TEMP_EXTRA`/`LOCAL_ADJUST` | 只读(D1 徽标"本地指定/本地调整") |
| `local_version` | 本地修订序号,等于 `#8` 的 `bizVersion` | 只读(D2 展示"修订 3 次") |
| `match_state` | `PENDING`/`MATCHED`/`UNMATCHED`/`LOCKED`/`REJECTED` | 只读(**D1 必须有"运安未匹配"标识**) |
| `verdict_state` | `PENDING_VERIFY`/`VERIFIED`/`OVERRIDDEN`/`REJECTED` | 只读(D2 展示"平台待核验 / 已核验") |
| `override_reason` / `override_by` / `override_at` | 指定/调整的留痕 | 只读(D8 列表) |
| `origin_jcsj` | 首次调整前的 `jcsj` | 只读(D2 "原叫乘时间") |
| `derived_rule_version` | 派生所用规则版本 | 只读(排障) |
**边界**:本端**不写**该表。值班员发 `ASSIGN_ELEMENT`/`ADJUST_TIME` 后,由 `apt.jcxx.local` 写 `gy_jcxx` 要素列 + `apt_local_task` 治理行,并在同一事务内 `local_version+1`。
**锁定状态只以 `gy_jcxx.jllx` 为准**(`0` 可参与运安匹配 / `1` 已由本地确定),`apt_local_task.jllx_at_override` 只是留痕(CR-01 表 1-3 #13)。
**`match_state` 在 D1 上的呈现规则**(本端唯一需要做的判断):
表 3-7 `match_state` × `jllx` 的展示组合
| `match_state` | `jllx` | D1 徽标 | D1 行操作可用性 |
| --- | --- | --- | --- |
| `MATCHED` | `0` | 无(正常) | 全部可用 |
| `UNMATCHED` | `0` | ⚠️ "运安未匹配" | 全部可用,但"指定要素"按钮**高亮**(这是主要出路) |
| `LOCKED` | `1` | 🔒 "本地锁定" | `ASSIGN_ELEMENT`/`ADJUST_TIME` 可用;`LOCK_MATCH`/`UNLOCK_MATCH` 不出现 |
| `PENDING` | `0` | ⏳ "待匹配" | `ADJUST_TIME` 需先有 `jcsj`,否则置灰 |
| `REJECTED` | 任意 | ⛔ "平台已驳回" | 只保留查看与 `CANCEL`,状态命令置灰 |
## 6. 可写字段白名单(本端最需要被守住的一张表)
表 3-8 本端可达的写入点
| # | 字段 | 写入方式 | 前置条件 | 必须留痕 | 越界特征 |
| --- | --- | --- | --- | --- | --- |
| 1 | `gy_jcxx.ryzt` | `apt.duty.state`(命令) | 见 `04` 表 4-4 | `duty_manual_action` + `duty_operation_log` | 本端出现 `set ryzt=` |
| 2 | `gy_jcxx.finish_jc` / `finish_ly` | 同上 | 同上 | 同上 | 同上 |
| 3 | `gy_jcxx.stoped` | 同上 | `ryzt in (1,2)` 或 `ryzt=0` | 同上 | 同上 |
| 4 | `gy_jcxx.jcsj` / `fjsj` / `jcbjsj` / `ylysj` | `ADJUST_TIME`(`apt.jcxx.local`) | `ryzt=0` 且 `finish_jc='0'`;`jcsj ≤ fjsj ≤ jcbjsj < kcsj` | `override_reason` 必填 + `local_version+1` | 本端出现 `set jcsj=` |
| 5 | `gy_jcxx.jlid` / `cc` / `jch` / `kcsj` / `cclx` | `ASSIGN_ELEMENT`(`apt.jcxx.local`) | `finish_ly='0'` 且 `jllx='0'` | 同上 | 本端出现 `set cc=` |
| 6 | `gy_jcxx.is_temp_call` | 临叫命令内部写入,只允许置 `'1'` | 有 `sessionId` | `duty_manual_action` | 允许置 `'0'` 即越界 |
| 7 | `gy_jcxx.leave_remark` | `PATCH /duty/jcxx/{jcid}` | 任意在寓行 | `duty_operation_log.detail` | 出现其他列即越界 |
| 8 | `gy_bjxx.handled_at` / `handled_by` | `apt.duty.command` | `where id=? and handled_at is null` | `duty_operation_log` | 无 `handled_at is null` 条件 |
| 9 | `gygl_ly_event`(新增行) | `MARK_LEAVE` → `apt-stay` | `finish_ly='0'` | `duty_manual_action` | 本端直接 `insert` |
| 10 | `duty_operation_log` / `duty_manual_action` | 本端自有 | — | — | — |
**不可写清单**(出现即为缺陷):`gy_jcxx.jllx`(属 `LOCK_MATCH`)、`gy_jcxx.cbje`/`sfffcb`(补助金额)、`gy_jcxx.sjrysj`(入住时间,属 `apt-stay`)、`apt_master_*` 全部、`gy_syn_jcxx` 全部、`apt_device_command` 全部、`apt_report_outbox` 全部。
## 7. 时间派生:`jcsj` 是唯一基准
表 3-9 两条派生路径(CR-01 表 1-5 的展开)
| 项 | 路径 A · 交路模板(`populateJcXxByJlXx`) | 路径 B · 运安匹配(`modifyJcXxBySynJcXx`) |
| --- | --- | --- |
| 触发 | 入住时选了本公寓固定交路 | 平台按运安数据补全 `kcsj` + `cclx` |
| `jcsj` | 直接取 `GYGL_JLXX.jcsj` 拼开车日期 | `= kcsj − zjjcsjgz`(站接)或 `− kjjcsjgz`(库出/库接) |
| `ylysj` | 取 `GYGL_JLXX.lysj` | `= jcsj + lysjgz` |
| `fjsj` | `= jcsj + 10 min` | 同 |
| `jcbjsj` | `= jcsj + 15 min`(= `fjsj + 5`) | 同 |
| 跨零点 | `jcsj`/`lysj` 的时刻晚于 `kcsj` 时刻则整体减 1 天 | 由 `kcsj − 偏移` 自然落在前一天 |
| `jllx` | 置 `'1'`(不参与运安匹配) | 保持 `'0'` |
表 3-10 `ADJUST_TIME` 后的重算规则(**只允许改 `jcsj` 一个输入**)
| 场景 | `fjsj` | `jcbjsj` | `ylysj` |
| --- | --- | --- | --- |
| 路径 A | 重算 `jcsj + 10 min` | 重算 `jcsj + 15 min` | **不变**(仍取模板 `lysj`) |
| 路径 B | 重算 `jcsj + 10 min` | 重算 `jcsj + 15 min` | 重算 `jcsj + lysjgz` |
| 校验 | 四条时间必须满足 `jcsj ≤ fjsj ≤ jcbjsj < kcsj`,否则 `40910 APT_TIME_ILLEGAL` | | |
三条"禁止":①禁止在 `ADJUST_TIME` 里逐个覆写 `fjsj`/`jcbjsj`(会与规则版本脱钩);②禁止把 `kcsj` 当成前端可编辑字段(它属 `ASSIGN_ELEMENT`);
③禁止在 `ryzt ≥ 1` 或 `finish_jc='1'` 的任务上调整(返回 `40910`,`Q-15` 若定专码则改为专码)。
**时间规则只读**:`kjjcsjgz`/`zjjcsjgz`/`lysjgz` 取 `apt_master_rule` 快照,本端**不改**。现状 `GET /jcgl/getSjgz` 的"接口失败回退默认值"做法要取消——
改为启动自检时校验快照存在性,缺失进 `D10` 的 `DEGRADED`,而不是让前端猜一个默认值(现状默认 `150/120/30` 与规则可能不一致)。
## 8. `gyid` 隔离与审计约定
表 3-11 四级隔离在本端的落点(沿用 `docs/公寓端/03` 表 3-12)
| 级别 | 本端机制 | 失败处理 |
| --- | --- | --- |
| 部署级 | 与公寓端同一 `apt.apartment.gy-id` | 启动失败(不是本端能单独决定的事) |
| 库级 | 同一数据源,本端无独立账号 | — |
| 请求级 | 令牌 → `AptContext` → Service → SQL;`/duty/**` 全链路带 `gyid` | `40302` |
| 页面级 | 前端不持有 `gyid`,不拼进 URL | 无(避免被篡改后"看起来正常") |
表 3-12 本端四条 `gyid` 用例(验收必过,与 `docs/公寓端/03` 表 3-14 同构)
| # | 用例 | 期望 |
| --- | --- | --- |
| 1 | `gyid=GYA` 的令牌查 `GYB` 的 `jcid` | `40401` 或 `40302`,不返回任何字段 |
| 2 | 请求体伪造 `gyid=GYB` 发命令 | 仍写 `GYA`(服务端忽略入参)或直接 `40302` |
| 3 | 跨公寓拉 `/ws/duty` | 连不上或推不到(按 `gyid` 定向) |
| 4 | 审计表按 `gyid` 分页 | 总数与行数都只含本公寓 |
表 3-13 审计与逻辑删除约定(沿用 `docs/公寓端/03` 表 3-15)
| 项 | 本端约定 |
| --- | --- |
| 逻辑删除 | 本端**无删除业务**;`duty_*` 三表不设 `deleted` 列 |
| 启用/停用 | 本端无(值班员不启停资源) |
| 停用原因 | 本端不涉及;`G-03` 的原因落 `duty_operation_log.detail`,本端是**承载方**不是发起方 |
| 版本 | 本端只读 `local_version`/`biz_version`,不自造版本号 |
| 审计两层 | `sys_oper_log`(HTTP)+ `duty_operation_log`(业务),本端两个都写 |
## 9. 缺口(本端视角,登记不改库)
表 3-14 缺口与兜底
| # | 缺口 | 影响 | 不改库的兜底(本期) | 若改库的最小变更 |
| --- | --- | --- | --- | --- |
| `G-D01` | `duty_manual_action.action_type` 列注释只列 5 个取值,实际需 10 个 | 字典与文档不一致,新人按注释写会漏 | 列无 CHECK,直接写;字典放 `apt-common` 枚举 `DutyActionType` | 仅改列注释(无需 DDL 变更) |
| `G-D02` | `duty_manual_action.state` DDL 为 `ACCEPTED/DONE/FAILED`,`docs/公寓端/ui/README` 写 `ACCEPTED/SKIPPED` | 界面状态与库不一致 | **以 DDL 为准**(三值);`SKIPPED` 语义用 `FAILED + result=SKIPPED_*` 表达 | 无(口径修正,登记为 `差-D1`) |
| `G-D03` | `gy_jcxx` 无 `state_updated_at`/`state_updated_by`(公寓端 `G-05`) | D2 的"谁在何时改的状态"只能靠审计反查 | `duty_operation_log` + `apt_call_session` 联合还原,D2 展示"推断值"并标注 | 公寓端 `G-05` 的 `add state_updated_at/state_updated_by` |
| `G-D04` | `apt_master_person` 无本地启停(公寓端 `G-02`) | 值班员想"今天不叫这个人" | 用 `duty_manual_action(action_type='STOP', target_type='JCXX')` 记意图,D1 标"仅本次有效";**不提供"停用人员"按钮** | 公寓端 `G-02` 的 `local_enabled` |
| `G-D05` | `duty_shift_handover.pending_snapshot` 无结构约束(json) | D7 实现时字段可能漂移 | 结构在 `04` 表 4-11 定死,写库前用 DTO 序列化 | 无(json 列足够) |
| `G-D06` | 无"值班台操作耗时"字段 | 验收表 7-5 第 4 项要求"操作步骤不增加超 10% 且有记录",耗时无法直接统计 | `detail` 里记 `startedAt`/`finishedAt`;或从 `sys_oper_log` 的 `cost_time` 取 | `add cost_ms int`(二期优化) |
**原则**:`G-D01`/`G-D02`/`G-D05` 只改文档与字典,不动表;`G-D03`/`G-D04` 的根在公寓端,本端承接兜底、不单独立项改库;`G-D06` 列入二期。
+336
View File
@@ -0,0 +1,336 @@
# 04 接口设计
> 追溯:`docs/10` §6.1 表 6-1(跨端 20 接口,**本端不新增编号**)、§6.2(错误语义)、§2.5 表 2-3(`/duty/**` 路由)、§4.3 表 4-3(`apt.duty.view`/`command`/`state`)。
> 对端:`docs/公寓端/04` 表 4-1(RESTful 约定)、表 4-4(错误码)、表 4-9(叫乘接口)、命令语义表、`docs/公寓端/05` 表 5-7。
> CR-01:表 1-7(角色×动作)、表 1-8(权限点)、表 1-19(接口字段变更)。
> 现状:`YktVue/src/api/jcgl/jcgl.js`(408 行,全部旧接口)、`docs/frontend/04` §4 操作矩阵。
> **v2 修订(CR-02,2026-11-09)**:新增接口、WS 消息与错误码以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §3、§4 为权威,见下文 §0;表 4-4 的 `50101` 常量按 CR-C13 统一。
## 0. v2 接口与消息扩展(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 4-y` 序号;表 4-1~4-5 的通用约定与 28 REST + 1 WS 端点继续有效。
表 C-1 新增接口 N-06~N-14(端内 `/duty/**`,**不计入跨端 20 接口**)
| 编号 | 方法 + 路径 | 权限点 | 幂等 | 落点 | 需求 |
| --- | --- | --- | --- | --- | --- |
| N-06 | `POST /duty/jcxx/{jcid}/commands`(`TRANSFER`) | `duty:jcxx:transfer` | L2 `commandId` | `apt_transfer_order`、`duty_manual_action` | A-09 |
| N-07 | `GET /duty/transfer`、`GET /duty/transfer/{transferId}` | `duty:jcxx:query` | L0 | `apt_transfer_order`(只读) | A-09 |
| N-08(v1.3 由 CR-C25 改判) | `GET /duty/person/face-reg/pending`(N-35)、`POST /duty/person/face-reg/{regId}/confirm`(N-36)、`POST /duty/person/face-reg/{regId}/reject`(N-37) | `duty:person:reg` + 二次确认 | L2 `regId` | `apt_person_face_reg`、`apt_master_person`、`duty_manual_action` | A-02 |
| N-09 | `POST /duty/ledger/local-recharge` | `duty:ledger:local` | L2 `flowId` | `apt_ledger_offline` | A-04 |
| N-10 | `GET /duty/ledger/local-balance?ryid` | `duty:ledger:local` | L0 | `apt_ledger_offline`(本地口径) | A-04 |
| N-11 | `POST /duty/clean/staff`、`GET /duty/clean/staff` | `duty:clean:staff` | L1 | `apt_clean_staff` | A-12 |
| N-12 | `POST /duty/clean/records`、`GET /duty/clean/records` | `duty:clean:record` | L2 `recordId` | `apt_clean_record`、`gy_clean_task` | A-12 |
| N-13 | `POST /duty/roster`、`GET /duty/roster` | `duty:roster:*` | L1 `shiftId` | `duty_roster` | A-13 |
| N-14 | `GET /duty/performance`、`POST /duty/performance/recalc` | `duty:performance:*` | L1 `(gyid, date)` | `duty_performance` | A-13 |
> **N-08 注册链路口径(CR-C25)**:注册由**签到终端** `POST /open/v1/face/reg-requests`(N-34,`docs/公寓端/10` §13)发起申请并上传人脸,值班台只做待审核对与**确认注册**;确认后生成 `ryid`、人脸进入公寓服务系统 `apt_master_person`,再**分别** #9 上传平台与 `FACE_ISSUE` 下发公寓内设备。原"提交即生效"语义撤销,平台回执不再是注册生效前置;`40914` 改判为"注册申请未获值班员确认"。
> **CR-02 §13 补遗(入住审核接口)**:值班台新增 N-42 待审列表、N-43 审核详情、N-44 审核命令(`APPROVE` / `APPROVE_WITH_ROOM_CHANGE` / `REJECT`)、N-45 房间调整、N-46 状态时间线,权限 `duty:checkin:*`、L2 `commandId` 幂等 + 二次确认;完整请求 / 响应字段见 `docs/公寓端/11` 表 11-20、`docs/公寓端/04` §0 表 C-7。审核命令只提交给 `apt-stay`,本端不写 `apt_local_task` / `gy_jcxx`。
> **CR-02 §14 补遗(充值核对与食谱接口)**:值班台新增 N-53 `GET /duty/ledger/recharge-requests`、N-54 `POST /duty/ledger/recharge-requests/{flowId}/confirm`、N-55 `POST /duty/ledger/recharge-requests/{flowId}/cancel`,权限 `duty:ledger:*`、L2 `commandId` 幂等 + 二次确认;公寓管理端 N-57 发布 / 撤回食谱。完整请求 / 响应字段见 `docs/公寓端/12` 表 12-19 / 12-23、`docs/公寓端/04` §0 表 C-8。命令只提交给 `apt.ledger.local` / `platform.catering`,本端不写 `gy_ykt_*` / `apt_ledger_offline` / `YKT_MEALS`;**不提供支付下单与回调接口**。
表 C-3 四能力接口 / 消息字段(CR-02 §15 同步落地;**均为公寓端权威接口,本端只消费或只提交命令**)
| 能力 | 接口 / 消息 | 方向 | 关键字段 | 本端动作 | 权限 / 幂等 |
| --- | --- | --- | --- | --- | --- |
| 平板叫乘 | `GET /open/v1/tablet/tasks`(N-03)、`POST .../ack`(N-05)、`POST /open/v1/tablet/results`(N-04) | 设备 ↔ 公寓端 | `taskId` / `jcid` / `dispatchSeq` / `jcsj` / `serverTime` / `cursor`;`resultId` / `callMode=TABLET` / `faceVerifyState` / `verifiedRyid` / `resultType` / `occurredAt` | **不直接调用**;经 `/ws/duty` 消费 `tablet.result` + `state.changed` | 设备凭据 + HMAC + ±5 min + `nonce`;`resultId` 幂等;只传刷脸结论,**不传抓拍帧 / 人脸原图 / 比对帧** |
| 平板叫乘 | `/ws/duty` `tablet.result` / `state.changed` | 公寓端 → 值班台 | `jcid` / `taskId` / `faceVerifyState` / `verifiedRyid` / `resultType` / `appliedState` / `occurredAt` / `seq` | 行徽标展示"已刷脸 / 未刷脸 / 未验证";`UNVERIFIED` 按 Q-22 提示复叫;**只提交命令** | `duty:call:*`;断网按 `seq` 去重 + 60 s 兜底轮询 |
| 人脸签到与注册 | N-34 `REG_APPLY`、N-35 待审列表、N-36 确认、N-37 驳回 | 签到终端 / 值班台 → 公寓端 | `regId` / `ryid` / `workNo` / `photoRef` / `featureRef` / `checksum` / `regState` / `rejectReason` / `commandId` | N-35 查看、N-36 确认、N-37 驳回;**只提交命令**,不写 `apt_master_person` | `duty:person:reg` + 二次确认;L2 `commandId`;平台回执不决定本地生效 |
| 消费 | N-49 额度、N-50 提交、N-51 流水、N-56 账户 | 消费终端 → 公寓端;值班台只读 | `ryid` / `allowanceQuota` / `cashQuota` / `localBalanceVersion` / `flowId` / `allowanceAmount` / `cashAmount` / `ledgerState` / `reconcileState` | **只读**额度与流水;**无消费提交权限** | 设备凭据;`apt:ledger:list`(本端只读);`duty:ledger:*` 仅查询 |
| 菜品管理 | N-48 食谱、N-57 发布 / 撤回 | 消费终端 / 管理端 ↔ 公寓端 → 平台 | `recipeId` / `recipeVersion` / `mealPeriod` / `effectiveFrom/To` / `publishState` / `items[]` / `imageRef` | 值班台只读食谱与选单;管理员 N-57 只提交**代理命令** | `apt:catering:recipe:*` + 二次确认;`recipeId + version` 幂等;不写 `YKT_MEALS` |
| 平台统计 | `S-01` / `S-02` / `S-05` / `S-07`(`/platform/v1/admin/stat/**`) | 平台管理端 → 只读消费端 | `gyIds[]` / `periodType` / `periodKey` / `metricClass` / `rankType` / `revisionNo` | 按 Q-50 **只读公寓级分类统计与排名**,不触发 `S-03` / `S-06` | `platform:stat:list` / `rank`(平台侧权限,本端不持有) |
> `faceVerifyState` 正式枚举为 `VERIFIED` / `UNVERIFIED` / `SKIPPED`(CR-02 表 3-21);`callResultType` 为 `ANSWERED` / `NO_ANSWER` / `FAILED` / `CANCELLED`;`callMode` 为 `SIP` / `TABLET`,平板链路**不发 #24**(#24 仅 SIP `VOICE_SUMMARY`)。三端状态映射以 CR-02 §15 表 3-66 为准。
表 C-4 错误码登记(**只消费不自造**;全部沿用公寓端 / 平台端既有定义,本端仅做展示与处置)
| 码段 | 常量 / 含义 | 本端处置 |
| --- | --- | --- |
| `40913` | `APT_OFFLINE_QUOTA_EXCEEDED` 断网超离线额度 | 行内提示"本地额度用尽",不重试扣款 |
| `40914` | `APT_FACE_REG_PENDING` 注册申请未获值班员确认 | 引导至 N-35 待审列表,禁止匹配 / 下发 |
| `40916` | `APT_TABLET_TASK_EXPIRED` 平板任务已派发 / 过期 | 不重派,展示当前真实状态 |
| `40917`~`40924` | 人脸范围 / 重复 / 媒体 / 算法 / 大小 / 未同步 / 注册态 | 按 `data.reason` 展示,媒体类不自动重试 |
| `40926`~`40932` | 入住审核态 / 缓存过期 / 房间 / 核验会话 / 结论 / 交路 / 重复入住 | 刷新详情后重提,不绕过审核 |
| `40933`~`40940` | 额度不足 / 账户不存在 / CAS 冲突 / 食谱失效 / 人员不适格 / 金额非法 / 申请终态 / `FEATURE_DISABLED` | 余额不足不部分扣款;`40936` 提示重拉食谱;`40940` 不展示下单入口 |
| `50303` | `APT_FACE_ERASE_TIMEOUT` 离寓擦除超时 | 值班台待办 + 告警,不自动重复下发 |
| `PF-5001` / `PF-5002` / `PF-5003` / `PF-6001` | 余额不足 / 业务重复 / 对账阻断 / 菜品不可用 | 只展示,不调账;`PF-5003` 期间账务类报表标 `BLOCKED` |
| `PF-7003` / `PF-7004` / `PF-7005` | 周期未封账 / `gyIds[]` 越权 / 修订版本冲突 | 报表页提示"本期未封账"或"无数据权限",不降级为全量查询 |
> 通用约定(前缀、`data.reason` / `data.traceId`、`gyid` 锁定、双校验)见 §1 / §2;本端**不新增错误码段**,与 `docs/应用端/09` §7「错误码集合只消费不自造」一致。
表 C-2 `/ws/duty` 新增 5 类消息(现有 12 类不变,`seq` 单调)
| 消息名 | 主要字段 | 触发 |
| --- | --- | --- |
| `tablet.result` | `jcid`、`taskId`、`faceVerifyState`、`verifiedRyid`、`occurredAt` | 平板结果回传 |
| `transfer.changed` | `transferId`、`newJcsj`、`newRoomId`、`state` | 换乘生效 / 驳回 |
| `device.alarm` | `alarmId`、`deviceCode`、`alarmSource`、`alarmEventKey` | 设备报警 |
| `ledger.offline` | `flowId`、`offlineScope`、`localBalanceVersion`、`quotaRemain` | 断网本地受理 |
| `report.backlog` | `pending`、`failed`、`retryBatchId`、`lastReportHistoryAt` | 上报积压 / 续报完成 |
表 C-3 错误码统一与新增(CR-C13)
| code | 常量 | 说明 |
| --- | --- | --- |
| 40903 | `APT_SNAPSHOT_EXPIRED` | 本端原名保持不变;公寓端 `APT_MASTER_VERSION_STALE` 作废 |
| 50101 | `APT_PROTOCOL_NOT_CONFIRMED` | **本端原 `APT_PROTOCOL_UNCONFIRMED` 作废**,改用公寓端口径 |
| 40913 | `APT_OFFLINE_QUOTA_EXCEEDED` | 断网本地充值 / 扣补贴超额度 |
| 40914 | `APT_FACE_REG_PENDING` | 注册申请未获**值班员确认**(CR-C25 改判),禁止参与运安匹配与设备下发 |
| 40915 | `APT_TRANSFER_TIME_ILLEGAL` | 换乘时刻非法 |
| 40916 | `APT_TABLET_TASK_EXPIRED` | 平板任务已派发 / 过期 |
| 50302 | `APT_REPORT_RETRY_BUSY` | 同批次续报进行中 |
新增接口仍遵守表 4-3 的幂等级别与表 4-6 的命令前置校验;`TRANSFER` 作为第 11 个受控命令加入命令矩阵,不改其余 10 个命令语义。
## 1. 通用约定
表 4-1 RESTful 与原语约定(延续 `docs/公寓端/04` 表 4-1,只列本端差异)
| 项 | 约定 | 说明 |
| --- | --- | --- |
| 前缀 | `/duty/**` | 与 `/apt/**` 并列,**同一后端、同一领域服务**,不同门面 |
| 资源命名 | `/{域}/{对象}`,对象用**业务键** `jcid` | 不用数据库自增 `id`(`docs/公寓端/03` 表 3-13 冲突-B) |
| 列表 | `GET /duty/{域}` + `pageNum/pageSize`(上限 100)+ 筛选 | 无条件列表必须带默认时间窗(当日 + 未完成) |
| 详情 | `GET /duty/{域}/{业务键}` | 不存在返回 `40401`,不返回 `null` 的 200 |
| 局部更新 | `PATCH`,**只允许白名单字段** | 本端唯一 `PATCH` 是 `leave_remark` |
| 命令 | `POST /duty/jcxx/{jcid}/commands`,body 带 `commandType` + `commandId` | **受控命令**:不改整行、必须幂等、必须留痕 |
| 时间格式 | 入参出参 `yyyy-MM-dd HH:mm:ss`;事件与时间线 `yyyy-MM-dd HH:mm:ss.SSS` | `docs/公寓端/07` 表 7-6 专项 2 |
| 空值 | 列表空 → `200 + total:0`;字段空 → 返回 `null` 不省略键 | 同上 专项 3 |
| 单号 | `commandId` 由前端生成(`uuid v4`),重试**复用同一个** | L2 幂等 |
表 4-2 `ryzt` 状态与 D1 呈现(沿用 `docs/frontend/04` §3.1,补"本端可发命令")
| `ryzt` | 显示 | 含义 | 颜色(+ 文字双编码) | 可发命令 |
| --- | --- | --- | --- | --- |
| 0 | 待叫乘 | 尚未到叫乘时间 | 黄 `#FFF200` + "待叫" | `CANCEL`、`ADJUST_TIME`、`ASSIGN_ELEMENT`、临叫 |
| 1 | 正在呼叫 | 呼叫进行中 | 绿 `#00FF00` + "呼叫中" | `STOP`、`HANGUP` |
| 2 | 叫乘报警 | 超时未接通 | 红 `#F56C6C` + "无应答" | `STOP`、`RE_CALL` |
| 3 | 待离寓 | 已应答待离寓 | 浅绿 `#B3E19D` + "待离寓" | `FORCE_FINISH`、`MARK_LEAVE` |
| 4 | 离寓报警 | 超时应离寓 | 红 `#F56C6C` + "离寓报警" | `MARK_LEAVE`、`STOP` |
| 5 | 正在通话 | 会议桥已建立 | 绿 `#00FF00` + "通话中" | `HANGUP` |
| 其他 | 已入寓 | 正常在寓 | 默认 + "在寓" | `FORCE_FINISH`、`MARK_LEAVE` |
表 4-3 幂等级别(本端不引入 L3/L4)
| 级别 | 适用 | 实现 | 重试语义 | 本端接口 |
| --- | --- | --- | --- | --- |
| L0 | 纯查询 | 无 | 随便重试 | 全部 `GET` |
| L1 | 可重复的写 | 结果幂等(`update … where 条件`) | 重试结果一致 | `PATCH` 备注、报警处置、保洁、换房 |
| L2 | 受控命令 | `action_id` + `uk_manual_action` + 状态机前置校验 | 同 `commandId` 只生效一次,第二次返回首次结果并带 `duplicated=true` | `/commands`、临叫、喊话、挂断、入寓登记 |
表 4-4 错误码(沿用 `docs/公寓端/04` 表 4-4,补齐 CR-01 四个新码;**本端不自造错误码**)
| code | 常量 | 含义 | 本端典型场景 |
| --- | --- | --- | --- |
| 40001 | `APT_PARAM_INVALID` | 参数校验失败 | 缺 `reason`、`commandId` 为空、时间格式错 |
| 40101 | `APT_UNAUTHORIZED` | 未登录 / 会话过期 | 值班台长时间未操作 |
| 40301 | `APT_FORBIDDEN` | 无权限点 | 值班员发 `LOCK_MATCH` |
| 40302 | `APT_GYID_MISMATCH` | `gyid` 不匹配 | 伪造 `jcid` 越权 |
| 40401 | `APT_NOT_FOUND` | 记录不存在 | `jcid` 已被归档到 `gy_jcxx_his` |
| 40901 | `APT_DUPLICATE` | 唯一键冲突 | 换房目标房已满 |
| 40902 | `APT_STATE_ILLEGAL` | 状态非法 | 对 `finish_ly='1'` 发 `STOP` |
| 40903 | `APT_SNAPSHOT_EXPIRED` | 快照过期 | `apt_master_rule` 过期导致时间规则不可用 |
| 40904 | `APT_SNAPSHOT_READONLY` | 快照只读 | 值班台试图改人员资料 |
| 40905 | `APT_SYNC_PENDING` | 同步未完成 | 主数据首次下发未完成 |
| 40906 | `APT_EXTENSION_IMMUTABLE` | 分机号不可改 | 语音页试图改号 |
| 40907 | `APT_COMMAND_DUPLICATE` | 命令重复(与首次结果冲突时) | 同 `commandId` 但参数不同 |
| 40908 | `APT_CONF_BUSY` | 会议桥忙 | 并发临叫(单 `Conf-01`) |
| **40909** | `APT_PERSON_NOT_IN_HOUSE` | 人员未在寓(无有效 `gy_jcxx` 行) | 对已离寓的人 `ASSIGN_ELEMENT` |
| **40910** | `APT_TIME_ILLEGAL` | 时间线非法 | `jcsj > jcbjsj`、或 `ryzt ≥ 1` 时调整 |
| **40911** | `APT_MATCH_LOCKED` | 匹配已锁定 | 对 `jllx='1'` 的行再 `ASSIGN_ELEMENT` |
| **40912** | `APT_CARD_ID_MISSING` | 工号缺失,无法匹配运安 | `ASSIGN_ELEMENT` 时该人无 `ryid` |
| 41001 | `APT_RECORDING_PURGED` | 录音已清理 | D5 回放已过留存期 |
| 50101 | `APT_PROTOCOL_NOT_CONFIRMED` | 协议未确认(CR-02 CR-C13 统一常量名) | 设备指令只入队 |
| 50102 | `APT_NOT_SUPPORTED` | 边界外能力 | 本端请求"同步运安" |
| 50301 | `APT_DEPENDENCY_UNAVAILABLE` | 依赖不可用 | Asterisk / AMI 不可达 |
## 2. 接口总览(28 个 REST + 1 个 WebSocket 端点)
表 4-5 分域接口清单
| # | 域 | 方法 + 路径 | 权限点 | 幂等 | 落点模块 |
| --- | --- | --- | --- | --- | --- |
| 1 | 盯控 | `GET /duty/jcxx` | `duty:jcxx:list` | L0 | `apt.duty.view` |
| 2 | 盯控 | `GET /duty/jcxx/{jcid}` | `duty:jcxx:query` | L0 | `apt.duty.view` |
| 3 | 盯控 | `GET /duty/jcxx/{jcid}/timeline` | `duty:jcxx:query` | L0 | `apt.duty.view` |
| 4 | 盯控 | `GET /duty/jcxx/history` | `duty:jcxx:query` | L0 | `apt.duty.view` |
| 5 | 盯控 | `POST /duty/jcxx/export` | `duty:jcxx:export` | L1 | `apt.duty.view` |
| 6 | 盯控 | `PATCH /duty/jcxx/{jcid}` | `duty:jcxx:remark` | L1 | `apt.duty.command` |
| 7 | 命令 | `POST /duty/jcxx/{jcid}/commands` | 按命令见 表 4-6 | **L2** | `apt.duty.command` → `apt.duty.state` |
| 8 | 呼叫 | `POST /duty/call/{jcid}/voice` | `duty:call:voice` | L2 | `apt.voice.endpoint` |
| 9 | 呼叫 | `POST /duty/call/{jcid}/broadcast` | `duty:call:broadcast` | L2 | `apt.voice.endpoint` |
| 10 | 呼叫 | `POST /duty/call/broadcast` | `duty:call:broadcast` | L2 | `apt.voice.endpoint` |
| 11 | 呼叫 | `POST /duty/call/{sessionId}/hangup` | `duty:call:hangup` | L2 | `apt.voice.endpoint` |
| 12 | 语音 | `GET /duty/call/sessions` | `duty:voice:list` | L0 | `apt.duty.view` |
| 13 | 语音 | `GET /duty/call/{sessionId}/recording` | `duty:voice:list` | L0 | `apt.voice.media` |
| 14 | 房间 | `GET /duty/room` | `duty:room:list` | L0 | `apt.duty.view` → `apt-stay` |
| 15 | 房间 | `GET /duty/room/{roomId}/occupants` | `duty:room:list` | L0 | `apt-stay` |
| 16 | 房间 | `POST /duty/room/swap` | `duty:room:swap` | L2 | `apt-stay` |
| 17 | 入住 | `POST /duty/checkin` | `duty:room:swap` | L2 | `apt-stay`(`submit_rydj`) |
| 18 | 保洁 | `POST /duty/room/clean` | `duty:room:clean` | L1 | `apt-stay` |
| 19 | 设备 | `GET /duty/device` | `duty:device:list` | L0 | `apt.duty.view` → `apt-device` |
| 20 | 报警 | `GET /duty/alarm` | `duty:device:list` | L0 | `apt.duty.view` |
| 21 | 报警 | `POST /duty/bjxx/{bjxxId}/handle` | `duty:call:stop` | L1 | `apt.duty.command` |
| 22 | 人员 | `GET /duty/person/{ryid}` | `duty:jcxx:query` | L0 | `apt-master-client`(只读) |
| 23 | 人员 | `POST /duty/person/{ryid}/correction` | `duty:manual:list` | L2 | `apt.duty.command`(留痕) |
| 24 | 交路 | `GET /duty/route/{jlid}` | `duty:jcxx:query` | L0 | `apt-master-client`(只读) |
| 25 | 干预记录 | `GET /duty/manual` | `duty:manual:list` | L0 | `apt-duty` |
| 26 | 审计 | `GET /duty/audit` | `duty:audit:list` | L0 | `apt-duty` |
| 27 | 健康 | `GET /duty/health` | `duty:health:view` | L0 | `apt-ops`(只读) |
| 28 | 交接 | `POST /duty/handover`、`GET /duty/handover` | `duty:handover:*` | L2 | `apt-duty`(**本期置灰**) |
| 29 | 推送 | `GET /ws/duty`(WebSocket) | 会话即可 | — | `duty.ws` |
## 3. 命令:本端唯一的"写业务状态"通道
### 3.1 命令矩阵
表 4-6 10 个命令的允许态、目标态与拒绝方式
| # | `commandType` | 允许态(前置校验) | 目标态 / 副作用 | 权限点 | 本端 | 失败码 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | `STOP` | `ryzt in (1,2)` 且 `finish_ly='0'` | `stoped='1'`;`ryzt=2` 保持不变(`docs/公寓端/03` 三条保护语义第 3 条);同时释放会议桥 | `duty:call:stop` | ✅ | `40902` |
| 2 | `RE_CALL` | `stoped='1'` 或 `ryzt=2` | 清 `stoped`,`ryzt=1`,`fjsj_s=now(3)`,重排叫乘 | `duty:call:recall` | ✅ | `40902` |
| 3 | `FORCE_FINISH` | `finish_ly='0'` | `finish_jc='1'` + `finish_ly='1'`,写 `gygl_ly_event`(`leave_type=FORCE`) | `duty:call:forceFinish` | ✅ | `40902` |
| 4 | `MARK_LEAVE` | `finish_ly='0'` | 写 `gygl_ly_event`(`leave_type=MANUAL`)+ `finish_ly='1'` | `duty:call:markLeave` | ✅ | `40902` |
| 5 | `CANCEL` | `ryzt=0` | `stoped='1'`(不是删除行) | `duty:call:cancel` | ✅ | `40902` |
| 6 | `HANGUP` | 会话 `state in ('CALLING','JOINED_CONF')` | 会话结束 + AMI 挂断;`ryzt` 由状态机按"是否已应答"回退到 3 或 2 | `duty:call:hangup` | ✅ | `40902`/`40401` |
| 7 | `ASSIGN_ELEMENT` | `finish_ly='0'` 且 `jllx='0'`;`ryid` 非空 | 写 `jlid`/`cc`/`jch`/`kcsj`/`cclx`;按 §3.3 重算时刻;`jllx='1'`;`apt_local_task` `origin=LOCAL_ASSIGN`、`local_version+1` | `duty:jcxx:assignElement` | ✅ | `40909`/`40911`/`40912`/`40910` |
| 8 | `ADJUST_TIME` | `ryzt=0` 且 `finish_jc='0'`;已存在 `jcsj` | 只改 `jcsj`,派生重算(§3.3);`apt_local_task` `origin=LOCAL_ADJUST`、`local_version+1` | `duty:jcxx:adjustTime` | ✅ | `40910`/`40902` |
| 9 | `LOCK_MATCH` | `finish_ly='0'` 且 `jllx='0'` | `jllx='1'` | `apt:jcxx:lockMatch` | ❌ 管理员 | `40301` |
| 10 | `UNLOCK_MATCH` | `jllx='1'` | `jllx='0'`,重新进入运安匹配池 | `apt:jcxx:unlockMatch` | ❌ 管理员 | `40301` |
**"值班台可用 8 个"的口径**:上表第 1~8 行。第 9/10 行在本端**不注册权限点**,请求到达即 `40301`(`01` 表 1-2 第 9 条)。
### 3.2 请求 / 响应
表 4-7 `POST /duty/jcxx/{jcid}/commands`
| 项 | 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| Header | `Authorization` | string | 是 | 会话令牌 |
| Path | `jcid` | string | 是 | 业务键 |
| Body | `commandType` | enum | 是 | 表 4-6 的 1~8 |
| Body | `commandId` | string(64) | 是 | 幂等键;重试复用 |
| Body | `reason` | string(512) | 条件 | `STOP`/`FORCE_FINISH`/`CANCEL`/`ASSIGN_ELEMENT`/`ADJUST_TIME` **必填** |
| Body | `expectedState` | int | 否 | 乐观校验:与当前 `ryzt` 不符则 `40902`(防"看到的和改的不是同一条") |
| Body | `payload` | object | 条件 | `ASSIGN_ELEMENT`/`ADJUST_TIME` 用,见 表 4-8 |
| 响应 | `actionId` | string | 是 | = `commandId` |
| 响应 | `state` | enum | 是 | `DONE`/`FAILED` |
| 响应 | `duplicated` | bool | 是 | 是否命中幂等(第二次请求为 `true`,`state` 返回首次结果) |
| 响应 | `ryztBefore` / `ryztAfter` | int | 是 | 便于前端精确更新单行,不必整表刷新 |
| 响应 | `pushSeq` | long | 是 | 推送序号;前端用它丢弃乱序消息 |
表 4-8 两个要素命令的 `payload` 与校验
| 命令 | `payload` 字段 | 类型 | 必填 | 校验 |
| --- | --- | --- | --- | --- |
| `ASSIGN_ELEMENT` | `jlid` | string | 否 | 存在则校验属本公寓且 `enabled=1` |
| | `cc` | string | 否 | 1~16 字符;与 `jch` 至少给一个 |
| | `jch` | string | 否 | 同上 |
| | `kcsj` | datetime | 条件 | 给 `kcsj` 时必给 `cclx`(否则算不出 `jcsj`) |
| | `cclx` | enum | 条件 | `站接`/`库出`/`库接`;决定用 `zjjcsjgz` 还是 `kjjcsjgz` |
| `ADJUST_TIME` | `jcsj` | datetime | 是 | 与现值差 `|Δ|≤30 min` 一次确认、`>30 min` 要求 `confirmTwice=true`;新值在过去也要求二次确认 |
| | `confirmTwice` | bool | 否 | 二次确认标记,缺失且需二次时返回 `40001` |
### 3.3 时刻重算(服务端唯一实现)
```text
输入:jcsj(新)、kcsj、cclx、路径(A/B)、lysjgz
fjsj = jcsj + 10 min
jcbjsj = jcsj + 15 min // 即 fjsj + 5 min
ylysj = 路径A ? 模板 lysj(不变) : jcsj + lysjgz
断言:jcsj ≤ fjsj ≤ jcbjsj < kcsj,否则 40910
```
三条实现要求:①这段逻辑只允许有一份实现(`apt.jcxx.local` 的纯函数),前端**不得**自行计算并提交派生值;②`ADJUST_TIME` 的 `payload` 里只接受 `jcsj`,提交 `fjsj` 等字段一律 `40001`(防逐项覆写);③重算时把 `derived_rule_version` 一并写进 `apt_local_task`(CR-01 表 1-17)。
## 4. 其余分域接口
表 4-9 房间与在寓(D3/D4)
| # | 接口 | 关键入参 | 出参要点 | 落点 |
| --- | --- | --- | --- | --- |
| 14 | `GET /duty/room` | `roomFloor`、`roomNo`、`roomStatus` | 6 种房态计数 `{idle,full,partial,cleaning,disabled,total}`;**不出现"楼栋"筛选**(G-01) | `apt-stay` |
| 15 | `GET /duty/room/{roomId}/occupants` | — | 在住人员(`rymc`/`ryid`/`jcid`/`sjrysj`/`is_temp_call`) | `apt-stay` |
| 16 | `POST /duty/room/swap` | `fromRoomId`、`toRoomId`、`jcids[]`、`reason`、`commandId` | 逐人换房结果 `{jcid, ok, code}`;目标房满 → `40901`;成功后联动 `apt_sip_binding` | `apt-stay` |
| 17 | `POST /duty/checkin` | 人员、`roomId`、`jlid`(可选)、`kcsj`(可选)、`cclx`(可选) | 返回新 `jcid`;**建行由 `apt-stay.submit_rydj` 完成**(CR-01 S1) | `apt-stay` |
| 18 | `POST /duty/room/clean` | `roomId`、`action=START/END`、`commandId` | `gy_clean_task.state` 变化 | `apt-stay` |
表 4-10 语音与呼叫(D1/D5)
| # | 接口 | 关键入参 | 拒绝条件 | 落点 |
| --- | --- | --- | --- | --- |
| 8 | `POST /duty/call/{jcid}/voice` | `commandId` | 分机未注册 `50301`;会议桥忙 `40908`;`finish_ly='1'` `40902` | `apt.voice.endpoint` |
| 9 | `POST /duty/call/{jcid}/broadcast` | `commandId`、`text`(可选,TTS 单次改写) | 同上;TTS 文本 ≤ 200 字,超长 `40001` | `apt.voice.endpoint` |
| 10 | `POST /duty/call/broadcast` | `roomIds[]`(≤ 20)、`text`、`commandId` | 房间不存在 `40401`;超 20 个 `40001` | `apt.voice.endpoint` |
| 11 | `POST /duty/call/{sessionId}/hangup` | `commandId`、`reason` | 会话已结束 `40902` | `apt.voice.endpoint` |
| 12 | `GET /duty/call/sessions` | `state`、时间窗 | 单 `Conf-01` 下"通话中"最多 1 条(D11) | `apt.duty.view` |
| 13 | `GET /duty/call/{sessionId}/recording` | — | `state=PURGED` → `41001`;返回 `url`(签名 ≤ 5 min)+ `expireAt` | `apt.voice.media` |
**临叫与状态命令的关系**:临叫(第 8/9/10)**不是**状态迁移命令,它创建会话并置 `is_temp_call='1'`;对已有任务行的临叫会顺带触发 `ryzt=1`。
但"停止临叫"走 `HANGUP`(第 11),**不走** `STOP`——`STOP` 是"停止这一趟的叫乘"(业务决定),`HANGUP` 是"挂断这通呼叫"(技术动作)。两者混用会导致"停了一通电话却把整趟叫乘关掉"。
表 4-11 交接班 `pending_snapshot` 结构(D7,本期置灰)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `unfinishedCount` | int | `finish_ly='0'` 的 `gy_jcxx` 条数 |
| `pendingCallCount` | int | `ryzt in (0,1)` 的条数 |
| `alarmCount` | int | `gy_bjxx` 中 `handled_at is null` 的条数 |
| `localLockedCount` | int | `gy_jcxx.jllx='1'` 条数 |
| `unmatchedCount` | int | `apt_local_task.match_state='UNMATCHED'` 条数 |
| `outboxBacklog` | int | `apt_report_watermark.pending_count` |
| `items[]` | array | `{jcid, rymc, ryzt, jcsj, issue}`,最多 50 条,按 `jcsj` 升序 |
| `degraded[]` | array | 当前 `DEGRADED` 的自检项编码(来自 `apt_ops_selfcheck`) |
## 5. `/ws/duty` 推送协议
现状有 4 条业务 WebSocket(`/yktgl/jcwebsocket/{gyid}`、`jlwebsocket`、`rlywebsocket/test33`、`MealsWebsocket/test33`),目标态收敛为 `/ws/duty` 与 `/ws/device` 两个端点(`docs/10` §2.5)。
本端只消费 `/ws/duty`。
表 4-12 消息类型(`type` 字段;`seq` 单调递增用于丢弃乱序)
| `type` | 载荷 | 前端行为 | 现状对应 |
| --- | --- | --- | --- |
| `state.changed` | `jcid`、`ryztBefore`、`ryztAfter`、`actionId` | 只更新该行;不整表刷新 | `refresh` |
| `alarm.raised` | `jcid`、`alarmType`(`NO_ANSWER`/`LEAVE_OVERTIME`)、`alarmEventKey` | 入告警音状态机 + 置红 | 由列表轮询推导 |
| `alarm.cleared` | `jcid`、`alarmEventKey` | 停该行告警音 | 同上 |
| `session.changed` | `sessionId`、`state`、`jcid`、`confNo` | D1 行状态 + D5 会议桥占用 | — |
| `match.changed` | `jcid`、`matchState`、`localVersion` | 更新"运安未匹配 / 本地锁定"徽标 | — |
| `notice.sync` | `title`、`contents[]` | 入同步提醒两级队列 + TTS | `autoCallFailed`/`missedCallWarning` |
| `checkin.request` | `content`、`photoBase64`、`deviceCode`、`zcid` | 打开入寓登记面板 | `zcxx`/`ry`/`xgxx` |
| `recharge.request` | `content`、`id`、`deviceCode` | 打开充值面板(**本期置灰**) | `cz` |
| `recharge.cancelled` | `id` | 关闭同 id 面板 | `cz_cancel` |
| `device.changed` | `deviceCode`、`online` | D6 行标红/恢复 | — |
| `outbox.backlog` | `pendingCount`、`ackedWatermark` | 顶部"已累积 N 条待上报"横幅 | — |
| `ping` / `pong` | `ts` | 30 s 心跳;2 次无 pong 触发重连 | — |
三条硬约束:①**禁用**现状 3 类消息(`routeChangeWarning`/`routeExpiredWarning`/`callTimeResetWarning`,`docs/frontend/05` 表已标注"已注释禁用"),新协议不保留;
②断线重连必须指数退避(1/2/4/8/16 s,上限 30 s),**不得** `location.reload()`;③推送只是"加速",**必须有 60 s 兜底轮询**,因为单实例进程内推送在重启时会丢消息。
## 6. 与 `/apt/**` 的关系
表 4-13 同一领域服务的两个门面(`docs/公寓端/01` 表 1-3 细则 3:Controller 之间不得互调)
| 能力 | `/apt/**`(管理端) | `/duty/**`(本端) | 是否同一服务 |
| --- | --- | --- | --- |
| 叫乘列表 | `GET /apt/jcxx` | `GET /duty/jcxx` | ✅ `apt.duty.view`(同一实现,不同筛选默认值) |
| 叫乘详情 | `GET /apt/jcxx/{id}` | `GET /duty/jcxx/{jcid}` | ✅ 同上 |
| 状态命令 | `POST /apt/jcxx/{id}/commands` | `POST /duty/jcxx/{jcid}/commands` | ✅ `apt.duty.state` |
| 要素命令 | `POST /apt/jcxx/{id}/commands`(4 类) | `POST /duty/jcxx/{jcid}/commands`(2 类) | ✅ `apt.jcxx.local`,**权限点不同** |
| 非状态字段校正 | `PATCH /apt/jcxx/{id}`(白名单 6 列) | `PATCH /duty/jcxx/{jcid}`(**仅 `leave_remark`**) | ⚠️ 同一服务,**字段白名单收紧** |
| 房间与房态 | `GET /apt/room`、`POST /apt/room/{id}/status` | `GET /duty/room`、`POST /duty/room/swap` | ⚠️ 换房只有本端有(现场需要),停用/恢复只有管理端有 |
| 人员 | `GET /apt/person`(只读) | `GET /duty/person/{ryid}`(只读)+ 修正申请 | ⚠️ 本端多"修正申请"留痕 |
| 录音 | `GET /apt/voice/recordings/{id}/url` | `GET /duty/call/{sessionId}/recording` | ✅ `apt.voice.media` |
| 审计 | `GET /duty/audit`(管理端也能查,权限点不同) | 同 | ✅ 同一接口,数据域不同 |
**一条禁止**:值班台**不得**直接调用 `/apt/**` 的 HTTP 接口(哪怕是同一进程)。原因不是性能,是**鉴权语义**:`/apt/**` 的门面假定调用者具备管理端权限点,绕过它会形成"值班员借管理员身份做事"的漏洞。
表 4-14 审计 / 健康 / 导出
| # | 接口 | 关键约定 |
| --- | --- | --- |
| 5 | `POST /duty/jcxx/export` | 服务端生成并返回下载票据(不直出文件流),导出动作**必写审计**;取消"每 10 分钟自动导出" |
| 25 | `GET /duty/manual` | 按 `action_type`/`operator`/时间筛选;`state` 三值(`ACCEPTED`/`DONE`/`FAILED`) |
| 26 | `GET /duty/audit` | 值班员只看本人(`operator_id=self`);管理员看全量(`duty:audit:all`) |
| 27 | `GET /duty/health` | 返回 12 项 + `callReady` + `degraded[]`;**只读**,重测入口归管理端(`apt:ops:selfcheck`) |
| 29 | `GET /ws/duty` | 握手时校验会话与 `gyid`;不提供 `?gyid=` 入参 |
+356
View File
@@ -0,0 +1,356 @@
# 05 关键流程
> 追溯:`docs/10` §7.4(告警两级输出)、§7.7(叫乘链路)、§7.9 表 7-5(事件与状态迁移)、§7.11(自检)、§6.3(断网与重试)、§5.3 第 6 条。
> 对端:`docs/公寓端/05` 表 5-7(干预动作)、表 5-8(自检 12 项)、图 5-4(三路判定)、`docs/公寓端/04` 命令语义表、`docs/公寓端/08` 表 8-4(串行化兜底)。
> CR-01:§0.1 五步链路、图 1-1(状态与版本流转)、§2.7(匹配与锁定规则)。
> 现状:`docs/frontend/05`(告警与 TTS 队列)、`docs/frontend/04` §6(5 个定时器)。
> **v2 修订(CR-02,2026-11-09)**:4 条流程变更与断网能力补充以 `docs/设计变更/03-三端功能设计修订(CR-02).md` §7 为权威,见下文 §0;原有 §1~§9 流程与"先派 outbox、再拉 #7"恢复顺序不变。
## 0. v2 流程变更索引(CR-02)
> 本节图表用独立编号 `表 C-y`,不占用本文件 `表 5-y` 序号。
表 C-1 本端流程变更落点
| 编号 | 流程 | 本端动作 | 涉及原章节 | 约束 |
| --- | --- | --- | --- | --- |
| F-B | 平板结果查看与处置 | 收 `tablet.result` 展示刷脸状态;`UNVERIFIED` 行标"未刷脸"并按 Q-22 触发复叫提示;值班员只发命令 | §1、§5 | 不直改 `gy_jcxx` |
| F-C | 换乘 | D1 选交路 → 指定换乘开车 / 叫乘时间、可选新房间 → `TRANSFER` 命令 → 展示 `transfer.changed` | §3 | 时刻非法 `40915`;`jcsj` 由公寓复算 |
| F-D | 断网本地充值 | D8 登记本地充值、查本地余额;额度内立即可用,超额度 `40913` | §6 | 只写本地,不改平台主账 |
| F-G | 保洁 / 排班 / 考核 | 登记保洁记录、维护排班、日终查看本地计算的考核指标 | §1、§7 | `duty_shift_handover` 仍不启用 |
| F-F | **人员注册审核与确认(CR-02 §12 / CR-C25)** | 收 `reg.apply` 待办 → 值班员核对要素与照片 → 提交 `CONFIRM` / `REJECT` 命令(N-36 / N-37)→ 展示注册态、平台同步态与设备下发结果 | §1、§7 | 只提交命令,不直写 `apt_master_person` / `gy_jcxx`;需 `duty:person:reg` + 二次确认;确认后双路同步由公寓端自动触发,本端不参与 |
| F-K | **人员入住审核与叫乘衔接(CR-02 §13 / `docs/公寓端/11`)** | 收 `checkin.apply` 待办 → N-43 查看详情 → N-44 审核 / N-45 换房 → 展示 `CHECKED_IN` 与匹配 / 任务就绪状态 | §1、§7 | 只提交命令,不直写 `apt_local_task` / `gy_jcxx`;需二次确认;平台匹配与 `jcsj` 复算由公寓端执行,本端不参与 |
| F-M | **线下充值核对(CR-02 §14 / `docs/公寓端/12`)** | 收 `recharge.request` 待办 → N-53 待核对列表 → 核对实收金额 → N-54 二次确认 / N-55 作废 → 展示 `COMPLETED` 与现金额度 | §1、§6 | 只提交命令,不直写 `gy_ykt_remainder` / `apt_ledger_offline`;需 `duty:ledger:confirm` + 二次确认;**无支付下单与回调**,额度变更由 `apt.ledger.local` 执行 |
| F-N | **阶段性食谱发布(CR-02 §14 / `docs/公寓端/12`)** | 公寓管理员 N-57 新建 / 发布 / 撤回阶段性食谱 → 展示 `publishState` 与 `recipeVersion` | §1 | 只提交代理命令,不写本地业务表;发布需二次确认;改价 / 下架生成新版本,`features.remoteOrdering=false` 本期不开放下单 |
| F-A | **平板派发与顺序执行(CR-02 §11 / §15 同步落地)** | 只读任务视图(按 `dispatch_seq` + `jcsj` 排序),不推送呼叫指令、不参与派发 | §1、§6 | 平板提前拉取、本地到点播放;`EXPIRED` 不重派;平板离线回落 `SIP`(CR-C23);本端无任务写动作 |
| F-J | **人脸数据下发与设备范围(CR-02 §12 / §15 同步落地)** | 只读注册态 / 平台同步态 / 设备缓存台账,接收擦除超时 `50303` 待办 | §1、§7 | 双路同步由公寓端自动触发、本端不参与;平台 `REJECTED` 不触发本地回滚(Q-35);抓拍帧 / 人脸原图 / 比对帧不出终端 |
| F-L | **人员消费(CR-02 §14 / §15 同步落地)** | 只读账户额度(N-56)与流水(N-51),展示 `ledgerState` / `reconcileState`;**无消费提交权限** | §1、§6 | 消费受理在消费终端侧;余额不足不部分扣款;`PF-5003` 期间账务类报表标 `BLOCKED`,本端不调账 |
| F-O / F-P / F-Q | **三端状态对齐与平台统计(CR-02 §15 / `docs/平台端/10`)** | 只读公寓级分类统计与排名(Q-50),不触发生成 / 导出 | §1、§6 | 统计无进程内调用、不产生跨端副作用;本端不写 `pl_apt_stat_*`、不开放跨公寓明细下钻 |
表 C-2 §6 断网能力清单补充(断网 2 小时内)
| 能力 | 可用性 | 说明 |
| --- | --- | --- |
| 本地充值登记 / 本地余额查询 | ✅(额度内) | `apt_ledger_offline`,恢复后按 `serverReceivedAt` 归集 |
| 平板叫乘任务与结果查看 | ✅ | 设备侧拉取,本地库可读 |
| 换乘(含换房) | ✅ | 只用本地库与规则快照 |
| 上报历史 / 续报查看 | ✅(本地) | 平台核验结论仍需恢复后到 |
| 排班与盯控质量考核 | ✅(本地计算) | 跨公寓对比归平台 |
恢复顺序仍为"先派 outbox、再拉 #7";`matchVerdict` 的 `REJECTED` 仍由公寓端自己在 #7 响应里读取并置 `stoped='1'`,不是平台回调(§8 第 6 条不变)。
## 1. 盯控主链路
图 5-1 从"任务到期"到"值班员看到并处置"
```mermaid
sequenceDiagram
autonumber
participant Q as Quartz(apt-dispatch)
participant S as apt.duty.state
participant A as Asterisk / AMI
participant D as 本地库
participant W as /ws/duty
participant U as D1 盯控台
Q->>S: 到期触发 Originate(jcid)
S->>D: update gy_jcxx set ryzt=1, jcsj_s=now(3) where id=? and ryzt=0
S-->>W: state.changed(0→1)
W-->>U: 只更新该行 + 起倒计时
A-->>S: AMI 事件(OriginateResponse / ConfbridgeJoin)
alt ConfbridgeJoin
S->>D: ryzt=3, finish_jc='1', hjxysj=now(3)
S-->>W: state.changed(1→3)
else 30s 无入会
S->>D: ryzt=2, jcbjsj_s, is_jcbj='1' + insert gy_bjxx
S-->>W: alarm.raised(NO_ANSWER)
W-->>U: 行置红 + 报警音 + TTS 提示
end
U->>S: POST /duty/jcxx/{jcid}/commands(STOP,含 commandId + reason)
S->>D: insert duty_manual_action(ACCEPTED) → update stoped='1' → 回写 DONE
S-->>W: state.changed(stoped=1) + alarm.cleared
W-->>U: 停该行报警音
```
三条读图要点:
1. **值班员看到的每一行变化,都有一个服务端"事实来源"**(状态机条件更新 / AMI 事件 / 命令),前端不做状态推导。
2. **`STOP` 之后 `ryzt=2` 不变**(`docs/公寓端/03` 三条保护语义第 3 条"停止叫乘不可逆至报警"),只是 `stoped='1'`;`RE_CALL` 才能把 `ryzt` 拉回 1。
3. **接通判据是 `ConfbridgeJoin`**,不是 SIP `Answer`(`docs/10` §7.7 第 2 条)。`Answer` 只代表话机摘机,不代表人在房间里能听到。
## 2. 人工干预链路(本端最核心的一条)
图 5-2 一次干预的三次落库与一次推送
```mermaid
sequenceDiagram
autonumber
participant U as 值班台(D1/D2)
participant C as DutyCommandService
participant R as duty_manual_action
participant S as apt.duty.state
participant G as gy_jcxx
participant L as duty_operation_log
participant W as /ws/duty
U->>C: POST /duty/jcxx/{jcid}/commands(commandId, type, reason)
C->>C: 权限点 + gyid + expectedState 校验
C->>R: insert(ACCEPTED) ← uk(gyid, action_id) 命中则直接返回首次结果
C->>S: 提交命令(携带 actionId)
S->>G: 条件更新(where id=? and ryzt=?)
alt 更新成功
S-->>C: 新状态
C->>R: update state=DONE, finished_at=now(3)
else 条件不匹配
S-->>C: 40902
C->>R: update state=FAILED, result='40902 ...'
end
C->>L: insert(module=DUTY, action=type, detail={commandId,reason,before,after,result})
C-->>U: {actionId, state, duplicated, ryztBefore, ryztAfter, pushSeq}
S-->>W: state.changed
```
表 5-1 三层职责与"少一层会怎样"
| 层 | 记什么 | 少了的后果 |
| --- | --- | --- |
| `duty_manual_action` | 命令意图与结果(含幂等键、原因、操作人) | 无法重试、无法去重、无法统计"值班员干预采纳率" |
| `apt.duty.state` | 状态字段的条件更新(`where ryzt=?`) | 并发下状态会被覆盖,"停止叫乘"可能被迟到事件改回报警 |
| `duty_operation_log` | 审计事实(请求快照、IP、耗时) | 事后追责只能靠应用日志,保留期不受控 |
**顺序不可颠倒**:先落 `duty_manual_action` 再调状态机。反过来的后果是"状态已变但命令没记下来",一旦前端超时重试就会重复执行(因为找不到幂等键)。
## 3. 本地指定要素与调整时刻(CR-01 的值班员段)
CR-01 图 1-1 画了公寓端与平台端的完整流转;本端只负责其中"人按下按钮"这一步,其余(`#8` 上报、`#7` 拉取、`matchVerdict` 消费)都发生在公寓端后台,**值班台不参与**。
图 5-3 值班员的两个要素动作
```mermaid
flowchart LR
A["D1 行操作<br/>指定要素 / 调整时刻"] --> B{"操作类型"}
B -- ASSIGN_ELEMENT --> C["校验:finish_ly=0<br/>jllx=0<br/>ryid 非空"]
B -- ADJUST_TIME --> D["校验:ryzt=0<br/>finish_jc=0<br/>jcsj 存在"]
C --> E["二次确认 + reason 必填"]
D --> F{"|Δ| ≤ 30 min ?"}
F -- 是 --> G["一次确认 + reason"]
F -- 否 --> H["二次确认 + reason"]
E --> I["apt.jcxx.local:写要素列<br/>重算 jcsj/fjsj/jcbjsj/ylysj<br/>jllx=1,local_version+1"]
G --> I
H --> I
I --> J["apt_local_task:origin / override_* / derived_rule_version"]
J --> K["outbox 入队(#8 要素登记,bizVersion=local_version)"]
K --> L["D1 徽标:本地锁定 / 待核验"]
```
表 5-2 两个动作的六项差异(**最容易做错的地方**)
| 项 | `ASSIGN_ELEMENT` | `ADJUST_TIME` |
| --- | --- | --- |
| 改什么 | 要素:`jlid`/`cc`/`jch`/`kcsj`/`cclx` | 只改 `jcsj` 一个值 |
| 谁来算 `jcsj` | 服务端按路径 A/B 从要素算 | 服务端按 `jcsj` 算派生 |
| 是否锁定 | **是**(`jllx='1'`) | **否**(`jllx` 不动) |
| 允许态 | `finish_ly='0'` 且 `jllx='0'` | `ryzt=0` 且 `finish_jc='0'` |
| 冲突码 | `40911`(已锁定)、`40909`(未在寓)、`40912`(缺工号) | `40910`(时间线非法 / 状态不允许) |
| 上报影响 | `#8` 的 `kcsj`/`jcfs` 字段取值变,`accepted` 可能为 `false` | `#8` 的 `computedCallTime` 变,`diffMs` 变大 |
**为什么 `ADJUST_TIME` 不锁定**:调整时刻是"这一趟就是早/晚 20 分钟",要素(车次、开车时刻)仍然可以由平台后续匹配修正;
一旦锁定要素,平台再匹配到正确交路也无法纠正,会把"时间微调"变成"数据永久分叉"。这条区分直接来自 CR-01 表 1-7 与 §2.7.2 的规则矩阵。
**值班员对 `ADJUST_TIME` 的三个"看不到但必须成立"的约束**:①重算后 `ylysj` 在路径 A 下**不变**(仍取模板 `lysj`)、路径 B 下随动;②派生一律用当前 `apt_master_rule` 快照的 `lysjgz`,快照过期时命令直接 `40903`,不允许"用旧规则算一遍";③`derived_rule_version` 必须落库,否则规则升级后无法解释"为什么这条的 `fjsj` 是 15 分钟而不是 10 分钟"。
## 4. 临叫、喊话与降级
图 5-4 临叫的三条路径
```mermaid
flowchart TB
S["值班员选行或选房间"] --> T{"场景"}
T -- 单房间语音临叫 --> U1["POST /duty/call/{jcid}/voice"]
T -- 单房间喊话 --> U2["POST /duty/call/{jcid}/broadcast"]
T -- 多房间喊话 --> U3["POST /duty/call/broadcast(roomIds[])"]
U1 --> V{"前置检查"}
U2 --> V
U3 --> V
V -- 分机未注册 --> X1["50301 依赖不可用<br/>提示:请值班员用电话联系"]
V -- 会议桥忙 --> X2["40908 会议桥忙<br/>提示:当前有 1 路通话,请先挂断"]
V -- 通过 --> Y["apt.voice.endpoint 创建会话<br/>is_temp_call='1'"]
Y --> Z["AMI Originate → 入 Conf-01"]
Z --> W1["session.changed 推送"]
V -- Asterisk 全不可用 --> X3["按钮置灰 + 替代路径"]
```
表 5-3 降级矩阵(**平台断网与语音故障必须区别对待**)
| 故障 | 叫乘是否继续 | 值班台表现 | 恢复后 |
| --- | --- | --- | --- |
| 平台不可达(`#7`/`#8` 超时) | **继续**(本地库 + 本地规则快照) | 顶部横幅"已累积 N 条待上报",**不禁用任何按钮** | outbox 按 `watermark asc` 续传,≤ 10 min 追平 |
| 运安数据未匹配 | **继续**(保留本地要素,CR-01 §2.7.2 第 3 行) | 行徽标"运安未匹配","指定要素"按钮高亮 | 下次 `#7` 拉到结论后自动消失 |
| AMI 不可达 | **降级**(Quartz 仍触发,但无事件回执) | 语音类按钮置灰;提示"走串口广播 + 值班台人工登记" | 5 min 巡检恢复后自动解禁 |
| Asterisk 不可达 | **降级**(同上) | 同上 + D10 显示 `callReady=false` | 同上 |
| 数据库不可用 | **停止**(无法写状态) | 全页只读 + 明确提示"叫乘已暂停,请联系运维" | 启动自检通过后恢复 |
| 会话过期 | 停止本页操作 | 跳登录,**保留当前筛选条件**(`sessionStorage`) | 登录后回到原筛选 |
**一条原则**:降级是"换一条路",不是"关掉整个页面"。`docs/公寓端/ui/README` §4 硬约束 2 已写死"只置灰语音类按钮,保留登记结果路径"——因为叫乘这件事的最终目的不是"电话打通",而是"人按时到车"。
## 5. 告警与 TTS
本端是**告警的主要输出端**(`docs/10` §7.4 两级输出中的"本地即时告警")。
表 5-4 三条音/提示通道的优先级与互斥
| 通道 | 触发 | 是否循环 | 优先级 | 停止方式 |
| --- | --- | --- | --- | --- |
| 报警音(`alarm.mp3`) | `ryzt in (2,4)` 且该事件 key 未被 dismiss | 是(`loop`) | 最高 | 人工 `STOP` / `STOP+报警处置` 成功后写入 `dismissedAlarmEvents` |
| 业务语音(叫乘提示、通话) | 状态机事件、通话建立 | 否 | 高 | 播完自停 |
| TTS 同步提醒 | `notice.sync`(`autoCallFailed`/`missedCallWarning`) | 否 | 低(**业务语音优先**) | 播完 / 手动关闭弹窗 |
表 5-5 从现状继承的四条判定(`docs/frontend/05` §4.1、§5.3)
| # | 规则 | 为什么必须保留 |
| --- | --- | --- |
| 1 | 事件 key 来自服务端 `alarmEventKey`,可为多个(逗号分隔) | 同一行可能同时"叫乘报警 + 迟到",key 不同才能分别 dismiss |
| 2 | 停叫成功后**先**把 key 写入 `dismissedAlarmEvents` 并立即 `syncAlarmVoice()`,**再**刷新列表 | 否则旧列表后到会重新响铃(现状就是用 `callListRequestVersion` 解决这个竞态) |
| 3 | `isBusinessVoiceBusy` = `ryzt in (1,5)` ∨ 有临叫 ∨ 有 `<audio>` 在播 ∨ 浏览器在播其他 TTS | 避免"值班员正在通话,TTS 插进来" |
| 4 | 同步提醒 800 ms 合并 + 10 s 自动关闭 + 关闭后同 key 不再打扰 | 现状踩过"10 条提醒刷屏"的坑 |
**改造要求**:现状"关闭的 key 只在 `destroyed` 清空"要改为**按班次清空**(或按自然日),否则一个班 8 小时下来集合会无限增长;
同时"运安离线提醒每 10 分钟重复弹、不去重"的问题必须修掉(`01` 表 1-8 第 10 条)。
## 6. 断网 / 依赖不可用
表 5-6 断网期间的可用能力清单(验收时逐条演练)
| 能力 | 断网 2 小时内 | 说明 |
| --- | --- | --- |
| 看盯控列表 | ✅ | 本地库查询 |
| 叫乘 / 复叫 / 报警 | ✅ | Quartz + AMI 都在本地 |
| 停止叫乘 / 强制结束 / 标记离寓 | ✅ | 命令只写本地库 |
| 指定要素 / 调整时刻 | ✅ | CR-01 §2.9:"完全允许",只用本地库与规则快照 |
| 换房 / 入寓登记 / 保洁 | ✅ | `apt-stay` 本地 |
| 录音回放 | ✅ | 录音文件在本地磁盘 |
| 看主数据(人员/交路/房间) | ✅(用快照) | 快照过期时页面标"数据截止 yyyy-MM-dd HH:mm" |
| 上报平台 | ⏸ 进 outbox | 顶部横幅显示积压数 |
| 看平台核验结论(`matchVerdict`) | ❌ | 恢复后随 `#7` 一起到 |
**恢复顺序不能颠倒**(CR-01 §2.9):**先派 outbox,再拉 `#7`**。先让平台知道本地要素,平台才能给出正确匹配;反过来的后果是"平台用旧要素判成 UNMATCHED,接着又把本地的正确要素覆盖掉"。
## 7. 交接班(本期不实现,只定契约)
表 5-7 D7 的设计意图(用于二期,本期页面置灰)
| 步骤 | 动作 | 数据 |
| --- | --- | --- |
| 1 | 交班人打开 D7 | 系统实时计算 `pending_snapshot`(`04` 表 4-11) |
| 2 | 填写交接说明 | `remark` |
| 3 | 提交 | `insert duty_shift_handover`(`confirmed_at` 为空) |
| 4 | 接班人签收 | `update … set confirmed_at=now(3) where gyid=? and shift_id=? and confirmed_at is null` |
| 5 | 未签收的班次 | D7 顶部红条提示"上一个班次未签收" |
**为什么本期不做**:`docs/公寓端/03` 表 3-7 标"后续"。"未结事项"的判定依赖 `duty_manual_action.state` 与 `gy_bjxx.handled_at` 两个来源稳定运行至少一个班次,否则 `pending_snapshot` 里的数字没人敢信。
## 8. 失败与补偿
表 5-8 六种失败形态与处理(每条都要有可观测信号)
| # | 失败 | 用户看到 | 系统动作 | 可观测 |
| --- | --- | --- | --- | --- |
| 1 | 命令超时(前端 10 s 未返回) | "处理中,请勿重复点击",按钮 loading | 前端重试**复用同一 `commandId`**;服务端返回首次结果 | `duty_manual_action` 1 条 |
| 2 | 状态已被别处改变 | "该任务状态已变为 XX,请刷新" | `expectedState` 不匹配 → `40902`;返回当前真实状态 | 审计 `action=EFFECT` |
| 3 | 会议桥忙 | "当前有 1 路通话,请先挂断" | `40908`,不重试 | `apt_call_session` 计数 |
| 4 | AMI 不可达 | "语音不可用,请用电话联系或串口广播" | `50301`;按钮置灰 | `apt_ops_selfcheck` `AMI=FAIL` |
| 5 | 推送断连 | 顶部黄条"实时通道断开,正在重连" | 指数退避重连 + 60 s 兜底轮询 | 前端埋点(本地日志) |
| 6 | 平台核验驳回(`REJECTED`) | 行徽标"平台已驳回",D2 显示 `verdict_note` | 状态机置 `stoped='1'` + 告警 | `apt_local_task.verdict_state` |
**第 6 条要特别说明**:这是 CR-01 唯一一条"平台结论能反向影响执行"的路径,但它**不是平台回调**——
它是公寓端在 `#7` 响应体里读到 `matchVerdict[].verdict=REJECTED` 后,**由公寓端自己**调 `apt.duty.state` 置 `stoped='1'`(CR-01 表 1-13 最后一行)。
值班台只是把结果展示出来,不参与判定,也不上报。
## 9. 值班台视角的自检(只读)
表 5-9 D10 展示规则(12 项明细见 `docs/公寓端/05` 表 5-8,本端不重复列举)
| `callReady` | 判定 | D10 表现 | 对值班台的影响 |
| --- | --- | --- | --- |
| `true` 且 `degraded[]` 为空 | 全绿 | 12/12 正常 | 无 |
| `true` 但 `degraded[]` 非空 | 有降级项 | 列出降级项 + 影响范围 | 相关按钮可置灰,但**不禁用叫乘** |
| `false` | 存在 `BLOCKED` 项 | 顶部红条"当前不可叫乘" + 原因 | 语音按钮全灰,保留"人工登记"路径 |
**两条纪律**:①D10 **只读**,"重测"按钮归管理端(`apt:ops:selfcheck`),因为重测会触发 Asterisk/AMI 探测,值班员不该在叫乘高峰按它;
②`callReady=false` 时**不能**让整个 D1 变成只读——状态机与本地库仍然可用,值班员还要能"手工标记离寓"。
---
## 10. 三端一致性作业视图(CR-02 §15)
> **权威关系**:三端状态映射与数据同步机制以 **CR-02 §15 表 3-66** 为唯一权威;四能力业务规则 / 交互 / 权限 / 异常对照见 §15 表 3-65;本端数据视角映射见 `docs/应用端/03` §0 表 C-3;统计专册见 `docs/平台端/10`。本节只给值班台视角的时序、权限与异常处置,**不另立映射、不改写公寓端权威表述**。
图 5-10 四能力三端交互时序(值班台视角)
```mermaid
sequenceDiagram
autonumber
participant DEV as 公寓内设备
participant AP as 公寓服务系统
participant PL as 平台端
participant D as 值班台(应用端)
DEV->>AP: 平板拉取任务 / 刷脸签到 / 消费提交 / 扫码充值
AP->>AP: 状态机命令 + 领域服务落库(唯一写入口)
AP->>D: /ws/duty tablet.result / state.changed / 待办
D->>AP: 只提交命令(审核 / 确认 / 复叫 / 核对 / 发布)
AP->>PL: #9 / #10 / #12 / #13 上行(outbox + 幂等键)
PL->>PL: 去重归集 → PL_REPORT_DAILY / PL_LEDGER_FLOW
PL->>PL: 日对账 → platform-report 周期汇总与排名
D->>PL: 只读 S-01 / S-02 公寓级报表(Q-50)
Note over AP,PL: 平台零回调;下行一律公寓主动拉;统计不反向下发
```
表 5-10 四能力交互流程(触发 → 三端动作 → 反馈)
| 能力 | 触发 | 公寓端 | 平台端 | 本端(值班台) | 反馈路径 |
| --- | --- | --- | --- | --- | --- |
| 平板叫乘 | Quartz 到 `jcsj` / 人工发起 | 生成 `apt_tablet_task(WAITING)` + `dispatch_seq`;平板本地播放、刷脸 → N-04 结果 → `apt.duty.state` 条件更新 | 收 #9 `TABLET_RESULT` / #8 回执 → `PL_TASK_ACK`;报表只取 `PL_REPORT_DAILY` | 只读 `tablet.result` / `state.changed`;`UNVERIFIED` 复叫提示;只提交命令 | `/ws/duty` → 值班台行徽标;#13 归档;**平板链路不发 #24** |
| 人脸签到与注册 | 签到终端 `REG_APPLY` | `APPLIED` → 值班员确认 → `apt_master_person` 入档 → #9 上传平台 / `FACE_ISSUE` 下发设备(并行双路) | 回同步回执 `ACCEPTED` / `REJECTED`(不做准入审批);`person-photo` 桶存媒体 | N-35 待审 → N-36 确认 / N-37 驳回(二次确认) | 值班台展示注册态 / 同步态 / 设备台账;平台驳回只告警 + 待办,不回滚本地 |
| 消费 | 消费终端确认选单 | N-49 双额度 → 补贴优先 / 现金补足 CAS → 一条 `CONSUME` 流水 → #10 | `platform-ledger` 唯一写主账、日对账 → 统计只读 `PL_LEDGER_FLOW` | 只读额度(N-56)与流水(N-51);**无提交权限** | 值班台只读;异常按 `40933`~`40935` / `PF-5003` 展示,不调账 |
| 菜品管理 | 公寓管理员发布食谱 | #4 拉 `recipes[]` → N-48 终端缓存 → 选单本地、确认前不落库 | `platform.catering` 写 `YKT_MEALS` 补列;#4 只维护当前有效版本 | 值班台只读食谱;管理员 N-57 只提交代理命令 | `40936` 提示重拉;`40940` 不展示下单入口 |
| 统计报表 | 周期到达(月 / 季 / 年) | **不感知统计** | `platform-worker` 单活 → `platform-report` rollup → `pl_apt_stat_*` + 排名快照 | 只读 S-01 / S-02(公寓级,Q-50) | 无跨端交互;统计结果不回写任何业务状态 |
表 5-11 权限控制矩阵(本端视角)
| 能力 | 本端权限点 | 可读 | 可写 | 禁止 |
| --- | --- | --- | --- | --- |
| 平板叫乘 | `duty:call:*` | `/ws/duty` 消息、任务视图、刷脸结论 | 仅复叫 / 处置**命令** | 直写 `gy_jcxx` / `apt_tablet_*`;接收抓拍帧 |
| 人脸签到与注册 | `duty:person:reg`(+ 二次确认) | 待审列表、注册态、同步态、设备台账 | `duty_manual_action` / `duty_operation_log` | 直写 `apt_master_person` / `apt_person_face_reg`;平台回执触发本地回滚 |
| 消费 / 充值核对 | `duty:ledger:*`、`duty:ledger:confirm`(+ 二次确认) | 额度、流水、对账状态、待核对列表 | 仅核对 / 作废**命令** | 消费提交、余额表写权限、支付下单与回调 |
| 菜品管理 | `apt:catering:recipe:*`(管理端) | 食谱、选单、`publishState` | 仅发布 / 撤回**代理命令** | 写 `YKT_MEALS`、开放远程点菜入口 |
| 平台统计 | 平台侧 `platform:stat:list` / `rank`(本端不持有) | 公寓级分类统计与排名(Q-50) | **无** | 触发 `S-03` / `S-06`、跨公寓明细下钻、人员级数据 |
> 全部权限点前后端同码(`v-hasPermi` / `@PreAuthorize`)并登记 `sys_menu.perms`(`docs/应用端/02` §4.2);`gyid` 只从会话解析,所有 SQL 必带 `gyid`;四类写操作均需二次确认 + 条件更新 + 审计留痕。
表 5-12 异常处理清单(本端处置,不自造错误码)
| 场景 | 本端表现 | 本端动作 | 不做什么 |
| --- | --- | --- | --- |
| 平板 `UNVERIFIED` / `SKIPPED` | 行徽标"未刷脸" | 按 Q-22 提示复叫,只提交命令 | 不改 `ryzt` / `finish_jc`、不自动判完成 |
| 平板结果迟到 / 重复 | `applied_state=IGNORED` | 按 `seq` / `resultId` 去重展示 | 不重复弹窗、不重派 `EXPIRED` 任务 |
| 断网 / WS 断连 | 顶部黄条 + 60 s 兜底轮询 | 结果本地缓存,恢复按 `resultId` 幂等续传 | 不伪造 `SYNCED`、不产生新业务事实 |
| 注册未确认 / 媒体异常 | `40914` / `40917`~`40924` | 引导至 N-35 待审或按 `data.reason` 展示 | 不参与匹配、不下发设备、不自动重试媒体 |
| 平台驳回注册 | 待办 + 告警 | 值班员人工归并 | **不回滚**本地注册与已下发设备(Q-35) |
| 余额不足 / CAS 冲突 | `40933` / `40935` | 展示失败原因,提示重试或转人工 | 不部分扣款、不调账 |
| 对账差异 | `PF-5003` | 账务类报表标 `BLOCKED`,展示"本期未封账" | 不改余额、不按未对账金额排名 |
| 食谱改价 / 下架 | `40936` / `PF-6001` | 提示重拉 N-48 食谱 | 不用旧价、不展示已下架菜品 |
| 远程点菜误调用 | `40940 FEATURE_DISABLED` | 不展示下单入口 | 不落订单、不扣款 |
| 统计越权 / 未封账 | `PF-7004` / `PF-7003` | 提示"无数据权限" / "本期未封账" | 不降级为全量查询、不输出半成品 |
表 5-13 状态映射与同步口径指针
| 项 | 权威位置 | 本端落点 |
| --- | --- | --- |
| 四能力全量状态枚举、同步载体、终态与回退 | **CR-02 §15 表 3-66** | `docs/应用端/03` §0 表 C-3(本端视角投影) |
| 业务规则 / 交互 / 权限 / 异常三端对照 | CR-02 §15 表 3-65 | 本节表 5-10~5-12 |
| 接口字段与错误码 | `docs/应用端/04` §0 表 C-3 / C-4 | 本节表 5-12 |
| 同步机制(幂等键 / 三时间戳 / outbox / 对账三维) | CR-02 §15 表 3-66 注 | 断网与续报见本文件 §6 |
| 统计周期 / 排名 / 修订 | `docs/平台端/10` 表 10-4 / 10-5 / 10-15 | 只读快照,不写 `pl_apt_stat_*` |
> 三条不变:① `apt.duty.state` 仍是业务终态唯一写入口,本端只提交命令;② 平台零回调、下行一律公寓主动拉;③ 统计结果与排名**不回写**本端 / 公寓端任何业务状态。
+145
View File
@@ -0,0 +1,145 @@
# 06 部署与配置
> 追溯:`docs/10` §2.5 表 2-3(不允许为应用端单独扩容)、§8.1 表 8-1(三端部署形态)、§8.2 表 8-2(非功能要求与审计保留期)、§8.3(资源)。
> 对端:`docs/公寓端/06`(profile、环境变量、端口、Nginx 双入口、容量)、`docs/公寓端/08` 表 8-2(D14 操作系统、D16 高可用)。
> 现状:`YktVue/vue.config.js`、`YktVue/public/config.js`、`docs/frontend/05` §1(5 条长连接均为 `ws://`,无 wss)。
> **CR-02 §15 补遗(三端一致性 / 部署影响)**:四能力与平台统计报表**不新增本端部署单元、端口或长连接**;`tablet.result` / `state.changed` 沿用既有 `/ws/duty`,断网期间结果缓存与 60 s 兜底轮询沿用本文件既有降级策略,不新增配置项。统计能力在平台端 `platform-worker`(**单活** + 库级协调锁)与 `platform-report` 内实现(`docs/平台端/08` §1 / §4),本端只读其管理端接口(Q-50);不改 Nginx 双入口、容量口径与前端静态资源部署。
## 1. 部署形态:本端没有"自己的部署"
表 6-1 三端部署形态(沿用 `docs/10` 表 8-1)
| 端 | 进程 | 库 | 前端 | 本端是否独立 |
| --- | --- | --- | --- | --- |
| 平台端 | 无状态多实例 + Nginx | 平台主库 + Redis + 对象存储 | 待决(`P-01`) | — |
| 公寓端 | 单实例 `apt-admin.jar` + Asterisk | 本地 MySQL + 本地 Redis | 同一 SPA 的 `apt` 入口 | — |
| **应用端** | **同一个 `apt-admin.jar`** | **同一个本地 MySQL** | **同一个 SPA 的 `duty` 入口** | **否** |
图 6-1 单机上的一次请求与一条推送
```mermaid
flowchart LR
B["值班台浏览器<br/>(值班工作站)"] --> N["Nginx<br/>location /duty → 同一 SPA"]
N --> J["apt-admin.jar<br/>apt-duty 模块"]
J --> DB[("MySQL 本地库")]
J --> R[("Redis")]
J --> AMI["Asterisk AMI<br/>(经 apt-voice)"]
J -.->|"/ws/duty 长连接"| B
SIP["本机 SIP 客户端<br/>(值班工作站)"] --- B
```
**三条推论**:①本端**没有独立的部署清单**,公寓端的 `06` 完整体就是本端的部署体;②本端**不能单独扩容**(`docs/10` §2.5),
因为 `/ws/duty` 的会话表与推送队列都是进程内状态,多实例会导致"消息推到了另一台";③本端**不能单独升级**,与公寓端同版本号、同发布窗口。
## 2. 静态资源与 Nginx
表 6-2 两条 location(与 `docs/公寓端/06` 的 `/apt` 完全对称)
| 项 | 配置要点 | 说明 |
| --- | --- | --- |
| 静态资源 | `location /duty { try_files $uri $uri/ /index.html; }` | 与 `/apt` 指向**同一份 `dist`**,不是第二次构建 |
| 入口区分 | 由前端构建产物的 `entries/duty.html`(或运行时按路径切入口)决定 | 两种实现方式,见下方说明 |
| 接口代理 | `location /duty/api`(或统一 `/api` 前缀)→ `http://127.0.0.1:{port}` | **不要**把 `/duty/**` 的 REST 也交给 SPA 兜底,否则 404 会返回 HTML |
| WebSocket | `location /ws/duty { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }` | 必须显式开 upgrade,否则握手 400 |
| 超时 | `proxy_read_timeout 3600s`(WebSocket 专用 location) | 默认 60 s 会导致每分钟左右断连 |
| 缓存 | `index.html` 不缓存;`*.js/*.css` 长缓存 + 内容哈希 | 值班台"发布后必须刷新到新版",`index.html` 不能缓存 |
**入口切分的两种实现**(`Q-16` 待定,见 `08` 表 8-1):
1. **多页面构建**:`vue.config.js` 的 `pages` 配 `apt` 与 `duty` 两个入口,Nginx 各自 `try_files … /duty.html`。优点:首屏只加载本入口代码;缺点:构建配置复杂。
2. **运行时按路径切**:单一 `index.html`,`main.js` 读 `location.pathname.startsWith('/duty')` 决定注册哪些路由与菜单。优点:构建简单;缺点:首屏会带上另一端代码。
未定前按**方式 1** 设计(与 `docs/公寓端/02` 表 2-4 的"双入口"表述一致),因为它能真正做到"值班台不加载管理端页面"。
## 3. 环境变量
表 6-3 本端配置项(只写变量名与占位默认值,**不出现真实地址与凭据**)
| 变量 | 默认值 | 作用 | 变更影响 |
| --- | --- | --- | --- |
| `APT_DUTY_WS_PATH` | `/ws/duty` | 推送端点路径 | 改了前端要同步 |
| `APT_DUTY_PUSH_QUEUE_CAPACITY` | `256` | 单连接推送队列长度,满了丢弃最旧并推 `outbox.backlog` 补偿 | 太小会丢状态变化,靠 60 s 轮询兜底 |
| `APT_DUTY_FALLBACK_POLL_SECONDS` | `60` | 兜底轮询周期 | 不能小于 30(会与叫乘节奏冲突) |
| `APT_DUTY_CLOCK_SKEW_ALERT_MS` | `5000` | 浏览器与服务器时钟偏差告警阈值(`docs/10` 表 8-2) | 超过即 D1 顶部黄条 |
| `APT_DUTY_COMMAND_TIMEOUT_SECONDS` | `10` | 前端命令超时(只影响提示,不影响服务端执行) | 太短会误报"失败" |
| `APT_DUTY_ADJUST_TIME_CONFIRM_MINUTES` | `30` | 调整幅度超过该值需二次确认(`Q-15`) | 与 `04` 表 4-8 的校验一致 |
| `APT_DUTY_ALARM_SOUND_ENABLED` | `true` | 报警音开关(夜班可关,但必须留可见红条) | 关闭后合规风险由现场承担 |
| `APT_DUTY_TTS_ENABLED` | `true` | TTS 开关 | 同上 |
| `APT_DUTY_EXPORT_MAX_ROWS` | `5000` | 单次导出上限 | 超限改为异步导出(二期) |
| `APT_DUTY_AUDIT_RETENTION_DAYS` | `365`(`Q-19` 待定) | `duty_operation_log` 保留期 | 与平台审计口径对齐前不自动清理 |
| `APT_DUTY_HANDOVER_ENABLED` | `false` | D7 交接班开关(本期关闭) | 打开需先完成 `duty_shift_handover` 逻辑 |
**三条纪律**:①所有变量由 `application-{profile}.yml` 读取,**不硬编码**;②本端**没有独立的 profile**,沿用公寓端的 `local`/`test`/`prod`;
③`APT_DUTY_ADJUST_TIME_CONFIRM_MINUTES` 必须与后端的 `ADJUST_TIME` 校验读同一个值,避免"前端要二次确认、后端不校验"。
## 4. 值班工作站要求
本端有一半的"界面"不在浏览器里:语音依赖工作站上的 SIP 客户端与话机。这是与公寓管理端最大的部署差异。
表 6-4 工作站要求
| 项 | 要求 | 说明 |
| --- | --- | --- |
| 操作系统 | 与公寓端一致(D14:Linux 交付;Windows 仅开发机) | 值班工作站可用 Windows,但要单独验证 |
| 浏览器 | Chromium 内核(Chrome / Edge)≥ 100 | 需要 `SpeechSynthesis`、`Audio` 自动播放(首次交互解锁) |
| 分辨率 | ≥ 1920×1080 | D1 设计基线(`docs/公寓端/ui/README` §0) |
| 音频 | **必须有独立输出设备**,不能与系统提示音混用 | 报警音需要能被听见 |
| SIP 客户端 | 本机运行,`ws://127.0.0.1:9004/sipclient/{gyid}` | 现状硬编码(`01` 表 1-8 第 4 条);目标态改为服务端分配分机号,地址仍在本机 |
| 话机 | 值班台分机在 `apt_sip_endpoint` 在册且 `sync_state='SYNCED'` | 未注册则语音按钮置灰 |
| 网络 | 工作站到服务器内网可达;到互联网**不要求** | 平台断网不影响本端 |
| 时钟 | NTP 同步,偏差 ≤ 5 s | 超过则 D1 黄条告警(`docs/10` 表 8-2) |
| 打印机(可选) | 交接班单、异常清单打印 | 二期 |
**一条硬提醒**:浏览器自动播放策略会导致"第一次进页面时报警音不响"。现状是靠用户点击才解锁;新实现必须在登录成功后的首次交互里**主动解锁一次音频**,
并在 D10 里显示"音频已就绪/未就绪",否则值班员会以为"系统没报警"。
## 5. 容量与并发
表 6-5 本端的五个容量边界
| 边界 | 值 | 来源 | 超限表现 |
| --- | --- | --- | --- |
| 并发会话(会议桥) | **1**(单 `Conf-01`) | D11 未定稿;`docs/公寓端/08` 表 8-4 | `40908 APT_CONF_BUSY`;值班员需先挂断 |
| 同时在线值班台 | 建议 ≤ 3(同公寓多机看同一数据) | 推送为广播到本 `gyid` 的所有会话 | 无硬限制,但操作会互相"抢"(`expectedState` 会拦住) |
| 单次列表返回 | ≤ 100 行(分页上限) | `docs/公寓端/07` 表 7-3 M1 项 | 服务端截断并返回真实 `total` |
| 单次导出 | ≤ 5000 行 | `APT_DUTY_EXPORT_MAX_ROWS` | 提示"请缩小时间范围" |
| 多房间喊话 | ≤ 20 个房间 | `04` 表 4-10 | `40001` |
**为什么"同时在线 ≤ 3 只是建议"而不是硬限制**:值班台不是独占资源,硬限制会做出"第二个人登不上"的荒谬效果。
真正的并发保护在 `expectedState`(看到的状态与提交时不一致就拒绝)与 `commandId` 幂等上,不在登录数上。
## 6. 版本与发布
表 6-6 发布纪律
| 项 | 规则 | 理由 |
| --- | --- | --- |
| 版本号 | 与公寓端**完全相同**(同一个 `apt-admin.jar` 版本) | 物理上分不开 |
| 发布窗口 | 与公寓端**同一个窗口** | 同上 |
| 灰度 | 不支持按端灰度(只能按公寓灰度) | 单实例 |
| 数据库变更 | 本端**不产生任何 DDL** | 只有 `duty_*` 三张表,已建 |
| 前端缓存 | 发布后 `index.html` 必须即时失效 | 否则值班员还在用旧版,命令字段不匹配 |
| 回滚 | 回滚 jar + 回滚前端 `dist`,`duty_*` 数据不回滚(只追加) | 审计与命令记录必须保留 |
| 停机影响 | 停机期间无法盯控,叫乘由 Quartz 继续(同一进程 → **实际会一起停**) | 因此发布必须安排在无叫乘任务的时段 |
**最后一条要点出来**:因为本端与叫乘引擎同进程,"发布值班台"= "重启叫乘引擎"。所以发布窗口的选择不是 IT 问题而是**业务问题**,
必须避开叫乘高峰;这也正是 `docs/10` §2.5 不允许把应用端拆成独立服务的现实原因之一(拆开后要保证两边版本兼容,反而更复杂)。
## 7. 上线前检查(12 项)
表 6-7 切换前必查(与 `docs/公寓端/07` 表 7-5 对齐,只列本端新增/相关的)
| # | 检查项 | 通过标准 |
| --- | --- | --- |
| 1 | `/duty` 静态资源可达 | 直接访问 `/duty/jcxx` 返回 SPA 而不是 404 |
| 2 | `/ws/duty` 握手成功 | 浏览器 Network 中 101 Switching Protocols |
| 3 | 断线重连 | 手动断网 30 s,恢复后 5 s 内自动连上,**页面未刷新** |
| 4 | 音频解锁 | 首次登录后报警音能响(用测试按钮验证) |
| 5 | `gyid` 隔离 | 跨公寓用例 4 条全过(`03` 表 3-12) |
| 6 | 权限点 | 无 `duty:call:stop` 的账号点"停止叫乘"返回 `40301` |
| 7 | 幂等 | 同 `commandId` 连点 3 次,`duty_manual_action` 只有 1 条 |
| 8 | 时区与时钟 | 工作站与服务器偏差 ≤ 5 s,D1 无黄条 |
| 9 | 降级演练 | 停 Asterisk:语音按钮灰、叫乘仍可"人工登记" |
| 10 | 断网演练 | 断平台 2 h:叫乘全程正常,恢复 ≤ 10 min 追平 |
| 11 | 导出审计 | 点导出后 `duty_operation_log` 有 `action=EXPORT` |
| 12 | 无明文凭据 | 仓库与配置中无口令、无内网地址、无 AMI 凭据 |
+197
View File
@@ -0,0 +1,197 @@
# 07 实施计划与验收
> 追溯:`docs/10` §9 表 9-1~表 9-6(阶段与退出条件)、§11.2 表 11-1(优先级)、§11.3(后续动作)。
> 对端:`docs/公寓端/07` 表 7-2(M0~M6 批次)、表 7-5(10 项验收)、表 7-6(测试矩阵)、表 7-7(联调步骤)。
> CR-01:表 1-21(阶段挂载)、表 1-23(验收四件套)、§2.9(断网与恢复顺序)。
> 页面依据:`docs/公寓端/ui/README` 表 1.1(D1~D10 清单与出稿状态)、§4(置灰硬约束)。
> 现状:`docs/frontend/04` §4(20 个操作入口)、§6(5 个定时器)。
> **CR-02 §12 验收追加**:人员注册审核与确认(N-35~N-37)挂 **D-1 命令通道**,不新增子批次或阶段。验收判据见 `docs/公寓端/10` 表 10-30 第 1 / 10 项与 CR-02 §12 表 3-52(验收 15~17):签到终端只能申请、值班台 `duty:person:reg` + 二次确认才能确认 / 驳回;重复确认幂等返回原 `ryid`;本端不直写 `apt_master_person` / `gy_jcxx`。Q-30~Q-35 未决前按 `docs/公寓端/10` 表 10-29 默认口径执行。
> **CR-02 §13 / §14 验收追加**:入住审核(N-42~N-46)与线下充值核对(N-53~N-55)均挂 **D-1 命令通道**,不新增子批次或阶段。验收判据见 `docs/公寓端/11` 表 11-35、CR-02 §13 表 3-57(验收 18~20),以及 `docs/公寓端/12` 表 12-34、CR-02 §14 表 3-62(验收 21~23):审核 / 核对均需二次确认且只提交命令;本端不直写 `apt_local_task` / `gy_jcxx` / `gy_ykt_*`;食谱发布走代理命令、不落本地业务表。Q-36~Q-45 未决前按各专册默认口径执行。
> **CR-02 §15 验收追加(三端一致性)**:四能力同步落地挂 **D-1 命令通道**,不新增 M7 / 阶段六。本端验收判据:① 平板叫乘 `tablet.result` / `state.changed` 只读展示,`UNVERIFIED` 复叫提示与 `resultId` 幂等续传生效,不直写 `gy_jcxx` / `apt_tablet_*`;② 人脸注册 N-35 / N-36 / N-37 二次确认、只提交命令,平台 `REJECTED` 不触发本地回滚(Q-35);③ 消费本端只读额度与流水、无消费提交权限;④ 菜品管理值班台只读、N-57 只提交代理命令,`40940` 不展示下单入口;⑤ 四能力错误码按 `04` §0 表 C-4 只消费不自造;⑥ 统计报表按 Q-50 只读到公寓级、不开放下钻、不写 `pl_apt_stat_*`;⑦ 三端状态映射与 `docs/应用端/03` §0 表 C-3、CR-02 §15 表 3-66 逐项一致。全局验收 24~26 见 CR-02 §15 表 3-69,权威变更点与影响范围见 §15 表 3-63。
## 1. 挂载点:本端不新造阶段
本端与公寓端**同进程、同库、同版本**(`06` 表 6-1),因此不存在"独立立项、独立排期"的空间。实施计划只做一件事:把本端工作挂到 `docs/10` 的既有阶段与 `docs/公寓端/07` 的既有批次上,不新增 M7,也不新增"阶段六"。
表 7-1 本端在既有计划中的落点
| `docs/10` 阶段 | 公寓端批次(表 7-2) | 本端子批次 | 本端交付内容 | 为什么挂这里 |
| --- | --- | --- | --- | --- |
| 阶段零(表 9-1) | — | 无 | 无(本端不引入新依赖、新中间件) | 基线修复已在公寓端完成(`apartment/README.md` §6) |
| 阶段一(表 9-2) | — | **本目录即产出** | 功能边界、工程结构、数据、接口、流程、部署 | 边界确认是前置,不占开发批次 |
| 阶段二(表 9-3) | — | 契约冻结(`09` §7 `F-D01~F-D10`) | 进程内服务契约、`/ws/duty` 消息字典、命令 payload | 阶段二冻结契约前必须定稿(`docs/10` §11.3 第 5 条) |
| 阶段三(表 9-4) | **M3**(叫乘信息管理 + 状态机) | **D-1** 值守链路底座 | 命令通道、6 个状态命令、`/ws/duty`、兜底轮询、审计切面 | 命令通道就是 `apt.duty.state` 的门面,必须与状态机同批次 |
| 阶段三(表 9-4) | **M3 + M6** | **D-2** 盯控台 D1/D2 | 列表/详情/时间线/导出/告警音/TTS/降级横幅 | 盯控台是"门面",需要状态机(M3)与语音(M6)双侧就绪 |
| 阶段三(表 9-4) | **M2 + M5 + M6** | **D-3** 其余 8 页 | D3~D10;D7 置灰、D5 受 D15 约束 | 房态依赖 M2、设备依赖 M5、语音依赖 M6,不能提前 |
| 阶段四(表 9-5) | 多公寓推广 | 无新增 | 只做"多公寓下的值班台差异验证"(并发、时区、号段) | 本端不单独扩容,无独立部署动作 |
| 阶段五(表 9-6) | 平台收敛 | 无新增 | 旧 `JcXx.vue` 下线、旧接口摘除 | 与公寓端旧逻辑下线同一动作 |
三条挂载纪律:
1. **不新增 M 批次**:本端所有代码都落在 M0~M6 的交付物里,`apt-duty` 模块的 pom 与包结构在 M0 就建好,业务类随 D-1/D-2/D-3 增量提交。
2. **D-1 不得早于 M3**:`apt.duty.state` 尚未具备条件更新时,"值班台能停叫"是假象。反过来,M3 完成而未做 D-1 也不影响公寓端验收——管理端有自己的受控命令入口(`/apt/jcxx/**`)。
3. **要素命令(`ASSIGN_ELEMENT`/`ADJUST_TIME`)不得早于 CR-01 公寓端落地**:CR-01 的公寓端部分挂载见 `docs/设计变更/01` 表 1-21;本端的 `apt.jcxx.local` 调用与它同批次,提前做只能对着空实现写 UI。
## 2. 三个子批次
表 7-2 子批次拆分(顺序即依赖顺序,不允许交叉)
| 子批次 | 名称 | 挂载 | 交付内容 | 前置依赖 | 可验证结果 |
| --- | --- | --- | --- | --- | --- |
| **D-1** | 值守链路底座 | M0 建骨架 → M3 接状态机 | `AptContext` 复用、`duty:*` 权限点注册、`@DutyLog` 审计切面、`POST /duty/jcxx/{jcid}/commands`(`STOP`/`RE_CALL`/`FORCE_FINISH`/`MARK_LEAVE`/`CANCEL`)、`/ws/duty` 端点 + `seq`、60 s 兜底轮询 | M3 的状态机可执行命令 | 同 `commandId` 连发 3 次只产生 1 条 `duty_manual_action`;非法态返回 `40902`;断网 30 s 后自动重连且页面不刷新 |
| **D-2** | 盯控台 D1/D2 | M3 + M6 | `GET /duty/jcxx`(推送 + 兜底)、`{jcid}`、`{jcid}/timeline`、`PATCH {jcid}`(仅 `leave_remark`)、`POST /duty/jcxx/export`、`POST /duty/bjxx/{id}/handle`、语音临叫/喊话/多房间/挂断(`40908` 校验)、`ASSIGN_ELEMENT`/`ADJUST_TIME`、报警音单例 + TTS 单队列、降级横幅 + 时钟偏差黄条 | D-1;M6 的 `apt.voice.endpoint` 可用;CR-01 的 `apt.jcxx.local` 可用 | D1 上 8 个命令全部可达且留痕;改成"每小时自动下载 Excel"不再出现;Asterisk 停掉后语音按钮置灰但登记路径仍通 |
| **D-3** | 其余 8 页 | M2 + M5 + M6 | D3 房态与换房、D4 保洁、D5 语音会话与回放、D6 设备与告警、D7 交接班(**置灰**)、D8 人工干预记录、D9 操作审计、D10 健康自检 | D-2(共用推送与告警基础设施);M2/M5/M6 对应域就绪 | D3 换房写 `apt_sip_binding` 联动;D5 单会话占用提示正确;D6 离线整行标红;D10 `callReady=false` 时给出原因 |
**为什么把 `ASSIGN_ELEMENT`/`ADJUST_TIME` 放在 D-2 而不是 D-1**:它们是"值班员对既有任务改要素/改时刻",属盯控台的填写与二次确认(`05` 图 5-3),与界面强耦合;而 D-1 只保证"命令能正确执行一次",两者测试面完全不同。
**为什么 D7 放在 D-3 而不是砍掉**:`duty_shift_handover` 表已建、`pending_snapshot` 结构已在 `04` 表 4-11 定死,页面留着占位才能保证"表结构与契约不漂移"(`03` 表 3-14 `G-D05`);功能开关 `APT_DUTY_HANDOVER_ENABLED=false`(`06` 表 6-3)。
## 3. 逐条验证项
表 7-3 每个子批次的验证项(每条都必须可复现,不接受"看代码是对的")
| 子批次 | 验证项 | 通过标准 |
| --- | --- | --- |
| D-1 | 无令牌访问 `/duty/jcxx` | `40101` |
| D-1 | 有令牌缺 `duty:call:stop` 发 `STOP` | `40301`,且 `duty_manual_action` **无新增行** |
| D-1 | 令牌 `gyid=A` 发 `gyid=B` 的 `jcid` | `40302`/`40401`,无字段泄露(`03` 表 3-12 第 1 条) |
| D-1 | 请求体伪造 `gyid` | 仍写 `A`(`03` 表 3-12 第 2 条) |
| D-1 | 同 `commandId` 连发 3 次 | `duty_manual_action` 只有 1 行;第 2/3 次返回 `duplicated=true` 且 `state` 为首次结果 |
| D-1 | 同 `commandId` 但 `commandType` 不同 | `40907` |
| D-1 | 对 `finish_ly='1'` 发 `STOP` | `40902`,`gy_jcxx` 无变化 |
| D-1 | `expectedState` 与当前 `ryzt` 不符 | `40902`,提示"该行已被他人处理,请刷新" |
| D-1 | `STOP` 后查 `gy_jcxx` | `stoped='1'` 且 **`ryzt` 仍为 2**(不被改回 0/1) |
| D-1 | `/ws/duty` 握手 | 101 Switching Protocols;跨 `gyid` 连不上(`03` 表 3-12 第 3 条) |
| D-1 | 手动断网 30 s | 5 s 内自动重连,**页面未刷新**,`commandId` 未重置 |
| D-1 | 审计 | 每次命令 `duty_operation_log` 有 1 行,`detail` 含 `commandId`/`reason`/`before`/`after` |
| D-2 | D1 列表首屏 | 默认时间窗"当日 + 未完成";`pageSize>100` 被截为 100 且 `total` 为真实值 |
| D-2 | 推送乱序 | 构造 `seq` 倒退消息,前端丢弃,行状态不回退 |
| D-2 | 推送断流 90 s | 兜底轮询把状态补齐,页面出现"数据可能滞后"提示且可手动刷新 |
| D-2 | `ADJUST_TIME` 幅度 ≤ 30 min | 一次确认 + `reason` 必填,成功 |
| D-2 | `ADJUST_TIME` 幅度 > 30 min | 二次确认;未确认不发请求(网络面板无该请求) |
| D-2 | `ADJUST_TIME` 后查派生 | `fjsj = jcsj+10min`、`jcbjsj = jcsj+15min`;路径 A 下 `ylysj` **不变**(`03` 表 3-10) |
| D-2 | `ADJUST_TIME` 把 `jcsj` 调到 `jcbjsj` 之后 | `40910`,`gy_jcxx` 无变化 |
| D-2 | `ADJUST_TIME` 是否锁定 | `jllx` **保持原值**(不置 1) |
| D-2 | `ASSIGN_ELEMENT` 成功 | `jllx='1'`、`local_version+1`、`apt_local_task.origin='LOCAL_ASSIGN'` |
| D-2 | 对 `jllx='1'` 再 `ASSIGN_ELEMENT` | `40911` |
| D-2 | 人员无 `ryid` 时 `ASSIGN_ELEMENT` | `40912` |
| D-2 | 并发临叫 | 第二次返回 `40908`,页面提示"当前有 1 路通话,请先挂断" |
| D-2 | 停 Asterisk | 语音类按钮置灰且 `title` 写"串口广播 + 人工登记";`STOP`/`MARK_LEAVE` 仍可用 |
| D-2 | 报警音 | 同一行连续两次报警(不同 `alarmEventKey`)能分别响、分别停;停叫后不因列表回包重新响 |
| D-2 | 导出 | `POST /duty/jcxx/export` 触发后 `duty_operation_log.action='EXPORT'`;超过 5000 行提示缩小范围;**无任何自动下载** |
| D-3 | 换房 | `gy_jcxx.fjh` 变更 + `apt_sip_binding` 联动;目标房已满 `40901` |
| D-3 | 入寓登记 | 行由 `apt-stay` 的 `submit_rydj` 产生;本端**无** `update gy_jcxx` 语句(架构测试) |
| D-3 | D5 会话列表 | 只显示本人会话 + 本公寓会话;录音 `PURGED` 返回 `41001` |
| D-3 | D6 报警处置 | `gy_bjxx.handled_at`/`handled_by` 被写;`ryzt` 不变 |
| D-3 | D7 置灰 | 直接访问 `/duty/handover` 显示"本期未开放",无写请求 |
| D-3 | D10 自检 | 12 项来源可点开;`callReady=false` 时给出原因(AMI/Asterisk 具体哪一项) |
## 4. 页面交付顺序
表 7-4 D1~D10 的交付顺序与理由
| 序 | 页面 | 子批次 | 为什么排在这里 |
| --- | --- | --- | --- |
| 1 | **D1 叫乘盯控台** | D-2 | 唯一"每天都在用"的页面;也是现状 `JcXx.vue` 的骨架(`docs/10` §1.1) |
| 2 | **D2 任务详情(抽屉)** | D-2 | D1 的附属;时间线是事后追责的唯一入口 |
| 3 | **D6 设备与告警** | D-3 | 与 D1 共用告警通道;先做可让"报警为什么没响"在 D1 上线前就能定位 |
| 4 | **D3 房间与房态** | D-3 | 依赖 M2;值班员一天里第二常用的动作(换房) |
| 5 | **D8 人工干预记录** | D-3 | D1 上线后立刻有人问"刚才谁停的";先有列表再有分析 |
| 6 | **D9 操作审计** | D-3 | 与 D8 同一套查询,几乎零额外成本 |
| 7 | **D10 健康与自检** | D-3 | 值班员自救入口;排在 D8/D9 之后是因为它是"只读展示" |
| 8 | **D5 语音与通话** | D-3 | 依赖 M6;且多数字段在 D1 已可见,独立页是"补全"而非"阻塞" |
| 9 | **D4 保洁任务** | D-3 | 频次最低;且可由公寓管理端代做 |
| 10 | **D7 班次交接** | D-3 | 本期置灰;最后做以保证不影响前面 9 页的排期 |
**一条反直觉的排序**:D6 排在 D3 之前。理由是值班台出问题时,"是不是设备/语音链路坏了"必须先能回答;若 D6 晚于 D3,D1 上线初期任何异常都会退化成"叫运维来看",把 D1 的可信度打掉。
## 5. 全量"域—模块—表—接口—页面—权限点"映射
表 7-5 功能域映射总表(接口明细见 `04` 表 4-5,页面见 `02` 表 2-7)
| # | 功能域 | 后端模块(`apt-duty` 内包 + 被调模块) | 主要表(读 / 写) | 接口前缀 | 页面(`entries/duty/views/…`) | 权限点前缀 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 叫乘盯控 | `view` → `apt.duty.view` | 读 `gy_jcxx`、`gy_bjxx`、`apt_local_task` / 写 无 | `/duty/jcxx` | `jcxx/DutyConsole.vue`、`jcxx/DutyConsoleDetail.vue` | `duty:jcxx:*` |
| 2 | 人工干预(命令) | `command` → `apt.duty.state` | 写 `duty_manual_action`、`duty_operation_log` / 经服务写 `gy_jcxx` 状态列 | `/duty/jcxx/{jcid}/commands` | D1 行操作、D2 底部动作条 | `duty:call:*` |
| 3 | 要素与时刻 | `command` → `apt.jcxx.local` | 写 `duty_manual_action` / 经服务写 `gy_jcxx` 要素列、`apt_local_task` | 同上(`ASSIGN_ELEMENT`/`ADJUST_TIME`) | D1 弹层 | `duty:jcxx:assignElement`、`duty:jcxx:adjustTime` |
| 4 | 语音与通话 | `view`/`command` → `apt.voice.endpoint`、`apt.voice.media` | 读 `apt_call_session`、`apt_recording`、`apt_sip_endpoint` | `/duty/call/**` | D5、D1 语音按钮 | `duty:call:voice`、`duty:call:broadcast`、`duty:call:hangup`、`duty:voice:list` |
| 5 | 房间与房态 | `view`/`command` → `apt-stay` | 读 `gy_rooms`、`gy_jcxx` / 写 `gy_clean_task`(经服务) | `/duty/room/**` | `room/DutyRoomList.vue` | `duty:room:list`、`duty:room:swap`、`duty:room:clean` |
| 6 | 设备与告警 | `view` → `apt-device`、`apt.dispatch` | 读 `gy_sb`、`gy_bjxx` / 写 `gy_bjxx.handled_*` | `/duty/device`、`/duty/alarm` | `device/DutyDeviceList.vue` | `duty:device:list`、`duty:call:stop` |
| 7 | 记录与审计 | `audit` | 读 `duty_manual_action`、`duty_operation_log` | `/duty/manual`、`/duty/audit` | `manual/DutyManualList.vue`、`audit/DutyAuditList.vue` | `duty:manual:list`、`duty:audit:list` |
| 8 | 健康自检(只读) | `view` → `apt-ops` | 读 `apt_ops_selfcheck`、`apt_report_watermark` | `/duty/health` | `health/DutyHealth.vue` | `duty:health:view` |
| 9 | 班次交接(置灰) | `command` | 写 `duty_shift_handover` | `/duty/handover` | `handover/DutyHandover.vue` | `duty:handover:*` |
| 10 | 实时推送(横切) | `ws` | 无(内存态 + `seq`) | `/ws/duty` | 全页 | 会话即可 |
表 7-5 的两个读法约定:①"写"列只允许出现 `duty_*` 与 `gy_bjxx.handled_*`,其余一律"经服务"(`03` 表 3-8);②权限点前缀列出现 `apt:` 即为越界(本端不注册 `apt:jcxx:lockMatch`)。
## 6. 验收标准
表 7-6 验收项(承接 `docs/公寓端/07` 表 7-5,只列与本端有关的 8 项 + 本端新增 4 项)
| # | 验收项 | 标准 | 验证方法 | 来源 |
| --- | --- | --- | --- | --- |
| 1 | 端到端主链路 | 断网 2 小时完成"入住 → 叫乘 → 复叫 → 报警 → 应答 → 离寓 → 归档",全程在 D1 上可观察 | 现场演练 + `gy_jcxx`/`apt_call_session` 取证 | 公寓端表 7-5 第 1 项 |
| 2 | **值班操作步骤** | **增加不超过 10%,且有记录** | 操作录像逐步骤计数 + `duty_operation_log` 对照 | 公寓端表 7-5 第 4 项 |
| 3 | **状态单写入口** | `ryzt`/`finish_jc`/`finish_ly`/`stoped` 的 `update` 只出现在 `apt.duty.state` | ArchUnit + 全仓 `grep` | 公寓端表 7-5 第 7 项 |
| 4 | **降级路径** | Asterisk 不可用时走"串口广播 + 值班台人工登记",**不静默失败、不禁用整页** | 手工停 Asterisk 演练 | 公寓端表 7-5 第 10 项 |
| 5 | 上报追平 | 恢复后 10 min 内 outbox 清空,值班台横幅消失 | `apt_report_watermark` | 公寓端表 7-5 第 2 项 |
| 6 | `gyid` 隔离 | 4 条用例 100% 拦截 | `03` 表 3-12 | 公寓端表 7-5 第 6 项 |
| 7 | 可叫乘探针 | `callReady=false` 时 D10 给出唯一原因 | 手工停 Asterisk | 公寓端表 7-5 第 9 项 |
| 8 | 无明文凭据 | 仓库与配置无口令、无内网地址、无 AMI 凭据 | CI 正则扫描 | 公寓端表 7-5 第 8 项 |
| 9 | **命令幂等** | 同 `commandId` 连发 3 次 → 1 条 `duty_manual_action`,第 2/3 次 `duplicated=true` | 自动化用例 | 本端新增 |
| 10 | **越界不可写** | 页面发出的写请求里不存在 `PUT /duty/jcxx/{jcid}`、不存在改 `ryzt` 的字段 | 抓包 + 接口清单比对 | 本端新增(`01` 表 1-11) |
| 11 | **告警不误报** | 停叫成功后 10 min 内同一 `alarmEventKey` 不再响;列表回包不触发重响 | 手工 + 日志取证 | 本端新增(`05` 表 5-5) |
| 12 | **时钟一致** | 工作站与服务器偏差 ≤ 5 s,超标时 D1 顶部黄条且可继续操作 | 改系统时间验证 | 本端新增(`06` 表 6-7 第 8 项) |
**第 2 项的落地方式**:验收不是"数点击次数",而是选 5 条真实任务(叫乘成功 2 / 报警 1 / 离寓 1 / 取消 1),由老值班员连续盲操作,记录"从看到行到动作完成"的秒数,与自己班的基线均值比较;同时用 `duty_operation_log` 的 `created_at` 差值交叉验证。**若超过 10%,优先砍字段而不是砍留痕**——`reason` 必填是新加的负担,可以按命令分级(`STOP` 必填、`HANGUP` 选填)。
## 7. 测试矩阵
表 7-7 沿用 `docs/公寓端/07` 表 7-6 的 8 类,补本端 5 类
| 类型 | 用例设计 | 期望 | 覆盖 |
| --- | --- | --- | --- |
| 正常查询 | 无条件列表 + 单条件筛选 | 200,行数与 `total` 一致 | D1/D3/D5/D6/D8/D9 |
| 分页边界 | `pageSize=100`、超末页、`pageSize=0` | 截断 / 空列表 + 正确 `total` / 取默认 | 全部列表接口 |
| 必填校验 | 缺 `commandId`、缺 `reason`、时间格式错 | 40001,`msg` 指名字段 | `/commands`、`/call/**`、`/room/swap` |
| 重复/冲突 | 同 `commandId`、并发同 `jcid`、会议桥占用 | `duplicated` / `40902` / `40908` | D-1、D-2 |
| 无权限 | 缺 `duty:*` 权限点 | `40301`,且无副作用 | 全部写接口 |
| `gyid` 隔离 | 跨公寓读/写/推送/审计分页 | `40302`/`40401`,推送不跨 | 全部 |
| 状态非法 | 对终态发命令、越态发命令 | `40902`,`gy_jcxx` 无变化 | D-1、D-2 |
| 只读快照 | 改 `apt_master_*`、改 `gy_jcxx` 要素(不经命令) | `40904`/接口不存在 | D3、D8 |
| **时间派生** | `ADJUST_TIME` 后四条时刻全组合(含跨零点、路径 A/B) | 等式成立;路径 A 的 `ylysj` 不变 | D-2 |
| **推送与兜底** | `seq` 倒退、`seq` 跳号、断流 90 s、重连后补 | 丢弃倒退、跳号触发补拉、兜底补齐 | D-1 |
| **告警状态机** | 多 `alarmEventKey` 分别 dismiss、列表回包竞态 | 不重响、不漏响 | D-2 |
| **降级** | 停 Asterisk / 停 AMI / 停平台 / 停库 | 按 `05` 表 5-3 六行表现 | D-2、D-3 |
| **无人值守** | 浏览器 8 h 不刷新、`<audio>` 自动播放被拦 | 首交互解锁;D10 显示"音频未就绪" | D-2、D-3 |
补充三类与公寓端同一套的专项(不属于本端,但同批次验收):①幂等;②时间戳精度(事件毫秒、列表秒);③空数据(200 + `total:0`,不返回 `null`)。
## 8. 联调与切换顺序
表 7-8 从"能跑"到"敢用"的 6 步
| 步 | 前置 | 操作 | 通过标志 |
| --- | --- | --- | --- |
| 1 | `apt-admin` 可启动(`local`) | 访问 `/duty/jcxx`,确认返回 SPA 而非 404 | Nginx 两条 location 正确(`06` 表 6-2) |
| 2 | 登录可通 | `captchaImage → login → getInfo`,检查 `permissions` 含 `duty:*` | 菜单由 `sys_menu` 生成,`/duty/room` 等路由可进 |
| 3 | D-1 | 用 Mock 的 `gy_jcxx` 行发 6 个状态命令 | `duty_manual_action` 与 `gy_jcxx` 双向一致 |
| 4 | D-2 | 接真实 Asterisk 做一次"叫乘 → 应答 → 挂断" | `ConfbridgeJoin` 判通;`session.changed` 推送正确 |
| 5 | D-2 | 停平台 + 停 Asterisk 各演练一次 | 断网 2 h 叫乘正常;语音按钮置灰但登记可达 |
| 6 | D-3 | 逐页走查 + 交接班置灰验证 | 10 页全可达;D7 无写请求 |
**切换顺序(与公寓端同步)**:先切本地库与状态机(M3),再切值班台(D-2),最后摘旧页(`JcXx.vue`)。**旧页不得先删**——新值班台首次上线期间,旧页是唯一回退路径;摘除动作放到阶段五,且必须在旧接口(`/jcgl/**`)下线之后。
## 9. 交付物与提交纪律
表 7-9 子批次交付物
| 子批次 | 代码 | 文档 | 验证记录 |
| --- | --- | --- | --- |
| D-1 | `apt-duty` 骨架、`command` 包、`ws` 包、`audit` 切面、`DutyActionType` 枚举、`sys_menu` 初始化脚本 | `04` §3 命令矩阵落地说明、`09` 契约冻结记录 | 表 7-3 的 D-1 段 + `curl` 输出 |
| D-2 | `view` 包、`DutyJcxxController`、D1/D2 页面与告警/TTS 控制类 | D1 字段说明、二次确认阈值说明(`Q-15`) | 表 7-3 的 D-2 段 + 操作录像片段 |
| D-3 | 其余 8 页与对应 Controller | 各页字段说明与置灰原因 | 表 7-3 的 D-3 段 + 每页截图 |
| 全量 | 无 DDL(`duty_*` 三表已建) | 回填 `README` 表 0-1 的对应说明与 `08` 表 8-6 的待确认结论 | 表 7-6 的 12 项验收结论 |
**提交纪律**:不执行 `git commit` / `git push`;本轮只产出文档,变更清单待评审。代码阶段每个子批次单独 PR,且 PR 描述必须引用本文件对应的表号(便于回溯"这个按钮为什么长这样")。
+126
View File
@@ -0,0 +1,126 @@
# 08 待决策与风险
> 追溯:`docs/10` §10.1 表 10-1(风险与应对)、§10.2 表 10-2(D1~D16)、§10.3 表 10-3(语音与数据库专项)、§11.3 第 5 条(阶段二冻结契约前定稿)。
> 对端:`docs/公寓端/08` 表 8-1(十六个决策)、表 8-8(差-1~差-8)、表 8-9(G-01~G-07)、表 8-10(Q-01~Q-14)。
> CR-01:表 1-22(`D7` 补注 / `D8'` / `D17` / `F-14` / `G-10` / `Q-15`)、表 1-24(冲突 A~H)、表 1-25(自我检查)。
> 本文只做"记录差异 + 给默认口径 + 请求确认",**不改写任何既有结论**;确认后统一修订。
> **CR-02 §15 决策承接**:本端**不新增 `D` / `Q` 编号**,四能力与平台统计的待决口径一律承接全局编号——Q-22(平板 `UNVERIFIED` 复叫)、Q-29(签到窗口 / 时钟阈值)、Q-30~Q-35(人脸擦除 / 保留 / 媒体 / 平台驳回)、Q-36~Q-40(入住审核)、Q-41~Q-45(消费 / 充值 / 食谱)、**Q-46~Q-50(评价与房间周转接入、周期时区、DDL 冻结、综合排名权重、报表开放粒度)**;完整默认口径见 `docs/平台端/10` 表 10-21 与 CR-02 §15 表 3-69。未决前按各专册默认口径执行,本端只消费结论、不自造默认值。统计能力对本端的新增风险为"只读范围越权",按 `PF-7004` 与 `docs/平台端/10` 表 10-17 的四级数据范围约束闭环,不登记为新的 `R-D*` 风险项。
## 1. 决策点全景
本端**不新增 `D` 编号**:与值班台有关的七个决策点全部沿用既有编号(`D6`/`D7`/`D8'`/`D11`/`D15`/`D16`/`D17`),差异与缺口另用 `差-D*` 与 `G-D*` 前缀,避免"同一件事两个决策入口"(CR-01 表 1-22 对 `P-12` 的处理方式同理)。
表 8-1 与本端相关的 7 个决策点 + 5 个待确认问题
| 编号 | 议题 | `docs/10` / CR-01 的建议 | 对本端的影响 | 未定时的默认执行口径(本设计采用) | 需定稿时点 |
| --- | --- | --- | --- | --- | --- |
| **D6** | 应用端是否独立前端工程 | 与公寓端同一工程双入口 | 中 | **同工程双入口**(`entries/apt` + `entries/duty`),本期只交付 `duty` 入口页面,不新建工程、不新增 login | 不阻塞 |
| **D7** | 交路人工维护落点 | 平台维护模板,公寓可"应急新增"并上报确认 | 中 | **不开放本地应急新增**(受 `G-04` 约束);"应急"落点 = 对既有 `gy_jcxx` 指定要素并锁定(CR-01 §2.8 路线 A 主用);D1 交路弹层只读 | M1(公寓端) |
| **`D8'`** | 叫乘时间以谁为准(CR-01 修订版) | 平台给要素(`kcsj`/`jcfs`),**`jcsj` 恒由公寓算** | **高** | 按 `D8'`:本端 `ADJUST_TIME` **只改 `jcsj`**,派生时刻由服务端按 `apt_master_rule` 快照重算;**禁止**前端自算或逐项覆写 | 阶段二冻结前 |
| **D11** | 会议桥粒度 | 建议"每任务一桥 + 保留 `Conf-01` 兼容" | **高** | 单 `Conf-01` + 串行化;D5 只展示 1 路通话;第二个请求 `40908`;"同时在线值班台 ≤ 3"仅为建议(`06` 表 6-5) | 阶段二冻结前 |
| **D15** | 录音合规 | 留存期、告知、可访问角色、导出待明确 | **高** | 90 天 + 仅内网 + 导出必审计 + 签名地址 ≤ 5 分钟;D5 **不做**外链与外带(`01` 表 1-9) | 阶段二冻结前 |
| **D16** | Asterisk 高可用方式 | 单机 + 冷备(RTO 15 分钟)+ 降级广播 | **高** | 横幅文案固定"串口广播 + 终端提示 + 值班台人工登记";降级只置灰语音按钮,**不禁用整页** | M6(公寓端) |
| **D17**(CR-01 新增) | 叫乘任务主键与匹配键分离 | `taskId` 由公寓分配(= `jcid` 业务编码),`planKey` 只作匹配键、可空 | 中 | 本端只把 `jcid` 当业务键(`04` 表 4-1);`planKey`/`apt_local_task` 只读展示,页面不出现"平台任务号"概念 | 阶段二 `S2.1'` 前 |
| **`Q-15`** | 权限点前缀 + 调整幅度分级 + 准点率三档 | CR-01 建议:`duty:jcxx:adjustTime` 或复用 `apt:jcxx:changeStatus`;30 分钟阈值;`≤30s`/`30~120s`/`>120s` | 中 | 按**独立权限点** `duty:jcxx:adjustTime`(`02` 表 2-9)+ **`\|Δ\|≤30min` 一次确认、`>30min` 二次确认**(`06` 表 6-3 的 `APT_DUTY_ADJUST_TIME_CONFIRM_MINUTES`)先做;准点率口径不影响本端页面 | M3 开工前(本端 D-1) |
| **`Q-16`**(本端新增) | 前端入口切分方式 | 无(脚手架问题) | 中 | 按**多页面构建**(`vue.config.js` 的 `pages`),`/duty` 与 `/apt` 各自 `try_files`;理由:值班台不加载管理端代码(`06` §2) | M0 建脚手架时 |
| **`Q-17`**(本端新增) | 临时叫乘是否绕过 `jcsj` 队列 | 无 | 中 | **不绕过**:临叫不改 `jcsj`、不改 `ryzt`、不占用叫乘队列,只创建一个 `is_temp_call='1'` 的语音会话;`STOP` **不能**停临叫(必须走 `HANGUP`)(`04` 表 4-10) | 本端 D-2 开工前 |
| **`Q-18`**(本端新增) | 值班员可写字段是否再收紧 | 无 | **高** | 值班员**不可**直接改 `fjh`/`jlid`/`cc`/`jch`/`kcsj`,只能发 `ASSIGN_ELEMENT`;`PATCH` 只允许 `leave_remark`(`03` 表 3-8、`04` 表 4-1) | 本端 D-2 开工前 |
| **`Q-19`**(本端新增) | `duty_operation_log` 保留期 | `docs/10` 表 8-2 只给"审计保留期"要求,未给天数 | 中 | **365 天 + 不自动清理**(`APT_DUTY_AUDIT_RETENTION_DAYS`);与平台审计口径对齐前只做统计不做删除 | 上线前 |
**为什么 `Q-16`~`Q-19` 值得单独立号**:这四项都是"实现方会自己猜"的参数——入口怎么切、临叫算不算状态命令、能改哪些字段、审计留多久。它们不在 `docs/10` 的 D 序列里(属工程与运营口径),但一旦猜错,返工都在本端,且都不会被架构评审自然覆盖。
## 2. 默认口径与返工半径
表 8-2 五个高影响决策的未定默认与改判代价
| 决策 | 未定默认 | 受影响的已定设计 | 一旦改判需返工的点 | 阻塞子批次 |
| --- | --- | --- | --- | --- |
| `D8'` | 平台给要素、公寓算 `jcsj`;本端只改 `jcsj` | `03` 表 3-9/表 3-10、`04` 表 4-8、`05` 图 5-3 | 若回到"平台下发 `jcsj`":`ADJUST_TIME` 失去依据,二次确认与 `40910` 全部作废;`apt_local_task.derived_rule_version` 无意义 | D-2 |
| `D11` | 单 `Conf-01` + 串行化 | `06` 表 6-5、`05` 图 5-4、`04` 表 4-10 | 若"每任务一桥":D5 从"1/1 占用"改成"桥列表 + 每桥会话数";`40908` 语义变"该桥忙"而非"全局忙";并发容量结论作废 | D-2(语音部分) |
| `D15` | 90 天 + 仅内网 + 导出必审计 | `01` 表 1-9、`04` 表 4-10 回放接口 | 若放开导出:需新增导出接口 + 水印 + 下载审计;若缩短留存:`41001` 的触发条件与页面文案全变 | D-3(D5) |
| `Q-18` | 值班员只发命令,不改要素列 | `03` 表 3-8、`04` 表 4-1、`01` 表 1-4 | 若允许直接改 `fjh`:`apt.duty.state` 之外的第二个写入口出现,ArchUnit 的第 1 条断言失效,`05` 图 5-2 的三层顺序全部要改 | D-2 |
| `Q-17` | 临叫不改状态、不进队列 | `04` 表 4-10、`05` 图 5-4 | 若临叫要"记成一次叫乘":需写 `gy_jcxx` 状态列(违反单写入口)或新增 `action_type`;`STOP` 与 `HANGUP` 的语义边界变模糊 | D-2 |
**一条判断原则**:凡"是否需要第二个状态写入口"的决策(`Q-18`),一律按**最严**默认;凡"是否要多存一份数据"的决策(`Q-19`),一律按**不删**默认。前者保护架构,后者保护追责能力,两者的返工成本都不对称。
## 3. 风险清单
表 8-3 通用风险(承接 `docs/10` 表 10-1)与本端新增风险(`R-D*`)
| # | 风险 | 本端落点 / 应对 | 当前状态 | 责任子批次 |
| --- | --- | --- | --- | --- |
| 通用-1 | 职责边界不清 | `01` 表 1-1/表 1-2/表 1-3 + 表 1-11 四道检查 | 已产出,待评审 | 阶段一 |
| 通用-2 | 状态机多写 | `apt.duty.state` 单写入口 + `command` 包内不出现 `update gy_*` + ArchUnit 6 条(`02` 表 2-4) | 设计完成,实现未开始 | D-1 |
| 通用-3 | 双写/重复上报 | `commandId` + `uk_manual_action` + `duplicated=true` 回放首次结果 | 表结构已具备(DDL 实测) | D-1 |
| 通用-4 | 时钟/时区 | 工作站 NTP ≤ 5 s + `APT_DUTY_CLOCK_SKEW_ALERT_MS` + D1 黄条 | 设计完成 | D-2 |
| 通用-5 | 单实例约束被忽视 | `06` §1 三条推论(不能单独扩容/升级/发布) | 已写入文档 | 阶段四 |
| 通用-6 | 权限体系冲突 | 复用公寓端账号 + `duty:*` 权限点 + `gyid` 数据域(`02` 表 2-8) | 设计完成 | D-1 |
| 通用-7 | 安全暴露 | 配置无明文、`/duty/**` 不对外、`/open/**` 不复用会话 | 部分(CI 扫描未接入) | D-1 |
| 通用-8 | 旧系统兼容 | 旧 `JcXx.vue` 保留至阶段五,期间双跑 | **未覆盖**(无兼容适配层) | 阶段五 |
| `R-D1` | **告警漏响** | 值班员以为"系统没报警",实际是浏览器自动播放被拦或 TTS 队列被业务语音长期占用 → 登录后首交互主动解锁一次音频;D10 显示"音频已就绪/未就绪"(`06` §4);`isBusinessVoiceBusy` 只拦 TTS 不拦报警音(`05` 表 5-5) | 设计完成 | D-2 |
| `R-D2` | **推送静默失效** | `/ws/duty` 表面连接正常但不再推(中间代理超时),页面数字停住 → 服务端 `ping` 心跳 + 前端 60 s 兜底轮询 + `seq` 跳号检测,三者任一异常都出黄条(`04` 表 4-12) | 设计完成 | D-1 |
| `R-D3` | **幂等键被复用** | 前端把 `commandId` 存成模块级变量,换行后仍是旧值,导致"第二条命令"被判重复 → `commandId` 在**每次提交时生成**、重试才复用;`04` 表 4-3 的 L2 实现必须是"请求级"而不是"页面级" | 需在 Code Review 设卡 | D-1 |
| `R-D4` | **操作步骤变长** | `reason` 必填 + 二次确认使"停叫"从 2 步变 4 步,验收第 2 项(≤10%)不过 → 命令分级:`STOP`/`FORCE_FINISH` 必填 `reason`,`HANGUP` 类选填;二次确认只在 `\|Δ\|>30min` 或改要素时出现(`05` 表 5-2) | 待验收验证 | D-2 |
| `R-D5` | **交接班表空转** | `duty_shift_handover` 有表无数据,二期接手时字段含义已失传 → 本期页面置灰,但 `pending_snapshot` 结构在 `04` 表 4-11 定死并进冻结清单 `F-D07` | 设计完成 | D-3 |
| `R-D6` | **跨公寓串看** | 多台值班台共用浏览器缓存 token,切 `gyid` 后看到旧数据 → 令牌与 `gyid` 绑定,切换必须重新登录;前端不缓存列表(只缓存筛选条件,`sessionStorage`) | 设计完成 | D-1 |
## 4. 与既有文档的差异(差-D1~差-D4)
表 8-4 四处差异(**不改写对端结论**,只登记本端口径)
| # | 既有结论 | 本端实际 / 建议 | 依据 | 影响面 | 状态 |
| --- | --- | --- | --- | --- | --- |
| `差-D1` | `docs/公寓端/ui/README` 写 `duty_manual_action.state` 为 `ACCEPTED/SKIPPED`;DDL 为 `ACCEPTED/DONE/FAILED` 三值 | **以 DDL 为准**(三值);`SKIPPED` 的语义用 `state='FAILED' + result='SKIPPED_*'` 表达,不新增枚举 | `apartment/db/mysql/07_report_duty_ops.sql`(`duty_manual_action` 列定义);`03` 表 3-3、表 3-14 `G-D02` | 仅口径;D8 的"跳过"筛选需按 `result` 前缀查 | 待与 `docs/公寓端/ui/README` 对齐(改文档不改表) |
| `差-D2` | `docs/10` 表 3-1 与 `docs/公寓端/01` 表 1-3 只写"应用端可写 `duty_*`",未限定"不可直写 `gy_jcxx` 要素列" | 本端**收紧**:值班员对 `fjh`/`jlid`/`cc`/`jch`/`kcsj` 只能发 `ASSIGN_ELEMENT`,`PATCH` 仅 `leave_remark`;`gy_*` 唯一可写是 `gy_bjxx.handled_*` | `03` 表 3-8 白名单;`01` 表 1-2 第 2 条 | 若无此收紧,`apt.duty.state` 不再是唯一状态入口 | 待确认(`Q-18`) |
| `差-D3` | `docs/10` 表 2-3 与 `docs/公寓端/02` 表 2-5 定"`entry-apt` 用前端**静态路由**,`sys_menu` 仅作权限点载体" | 本端 `entry-duty` 走 `sys_menu` **动态路由**(路由 + 权限点双载体);已由 `docs/公寓端/08` 差-7 覆盖"是否全动态"的问题,本端补"两个入口不同策略"这一事实 | `02` 表 2-6;`docs/公寓端/08` 差-7 | 加页面的动作不同:`duty` 侧插 `sys_menu` 行即可,`apt` 侧要改代码发版 | 已登记(沿用差-7 结论) |
| `差-D4` | `docs/10` 表 8-2 只要求"审计可查、保留期明确",未给天数;`docs/公寓端/08` 表 8-1 未列此项 | 本端默认 **365 天 + 不自动清理**,并挂 `Q-19`;与平台审计口径对齐前**只统计不删除** | `06` 表 6-3(`APT_DUTY_AUDIT_RETENTION_DAYS`);`03` 表 3-2 | 保留期若改为 90 天,D9 的时间范围选项与 `41001` 类比逻辑要同步 | 待确认(`Q-19`) |
## 5. 缺口承接(G-D01~G-D06)
表 8-5 六处缺口与两条路径(明细见 `03` 表 3-14,此处只给决策建议)
| # | 缺口 | 根在哪 | 本期走"不改库兜底"是否够用 | 若改库的最小变更 | 建议 |
| --- | --- | --- | --- | --- | --- |
| `G-D01` | `action_type` 列注释只列 5 个取值 | 本端文档 | **够**(列无 CHECK,字典放 `DutyActionType` 枚举) | 只改列注释 | 兜底 |
| `G-D02` | `state` 三值 vs 文档 `SKIPPED` | 本端文档 | **够**(`FAILED + result` 前缀) | 无 | 兜底(口径修正为 `差-D1`) |
| `G-D03` | `gy_jcxx` 无 `state_updated_at/by` | 公寓端 `G-05` | **够但体验差**:D2 展示"推断值"并标注 | 公寓端加两列 | 承接兜底,不单独立项 |
| `G-D04` | 人员无本地启停 | 公寓端 `G-02` | **够**:用 `STOP` 记"仅本次有效",**不提供"停用人员"按钮** | 公寓端加 `local_enabled` | 承接兜底 |
| `G-D05` | `pending_snapshot` 无结构约束 | 本端文档 | **够**(DTO 序列化 + `04` 表 4-11 定死结构) | 无 | 兜底 + 进冻结清单 `F-D07` |
| `G-D06` | 无操作耗时字段 | 本端 | **勉强够**:`detail` 记 `startedAt/finishedAt`,或取 `sys_oper_log.cost_time` | `add cost_ms int` | 二期 |
**与验收第 2 项的关系**:`G-D06` 不是"优化项"而是**验收取证手段**。若 `detail` 里没有起止时间,表 7-6 第 2 项"操作步骤不增加超 10%"就只能靠录像人工数,无法批量复核。因此二期之前,`detail` 必须至少记 `uiStartedAt`(前端点击时)与 `createdAt`(落库时)。
## 6. 待确认清单
表 8-6 需要拍板的 5 个问题(不回答即按"默认"执行)
| # | 问题 | 关联 | 不回答时的默认 | 决策角色 |
| --- | --- | --- | --- | --- |
| `Q-15` | ①`duty:jcxx:adjustTime` 独立是否可接受?②调整幅度阈值 30 min?③准点率三档用 `≤30s/30~120s/>120s`? | `02` 表 2-9、`04` 表 4-8、`06` 表 6-3、CR-01 §2.10 | 独立权限点 + 30 min + 三档 | 产品 + 架构 |
| `Q-16` | 前端入口用"多页面构建"还是"运行时按路径切"? | `06` §2 | 多页面构建(`pages: { apt, duty }`) | 前端 + 架构 |
| `Q-17` | 临时叫乘是否必须记成一次"叫乘事件"(而非纯语音会话)? | `04` 表 4-10、`05` 图 5-4 | 不记;纯语音会话,`is_temp_call='1'` | 产品 + 语音负责人 |
| `Q-18` | 值班员是否允许直接编辑 `fjh`(不经 `ASSIGN_ELEMENT`)? | `03` 表 3-8、`01` 表 1-4 | 不允许;只能发命令 | 产品 + 架构 |
| `Q-19` | `duty_operation_log` 保留期定多少?是否与平台审计统一? | `06` 表 6-3、`03` 表 3-2 | 365 天 + 不自动清理 | 合规 + 运维 |
**另有两项不是"待确认"而是"待对齐"**:①`差-D1` 的 `SKIPPED` 口径需与 `docs/公寓端/ui/README` 同步修订(改文档,不改表);②`docs/10` 表 2-3 若采纳 `差-D3`,需在"实现落点"列注明两个前端入口的路由策略不同。
## 7. 决策 × 子批次阻塞矩阵
表 8-7 哪些决策会卡住哪个子批次(`■` 强阻塞,`□` 弱影响)
| 决策 / 缺口 | 契约冻结(阶段二) | D-1 | D-2 | D-3 |
| --- | --- | --- | --- | --- |
| `D8'` 时间以谁为准 | ■ | | ■ | |
| `D17` 主键与匹配键分离 | ■ | □ | □ | |
| `Q-15` 权限点与阈值 | | ■ | ■ | |
| `Q-16` 入口切分 | | ■ | ■ | ■ |
| `Q-17` 临叫语义 | | | ■ | □ |
| `Q-18` 字段白名单 | | | ■ | □ |
| `Q-19` 审计保留期 | | □ | | ■ |
| `D11` 会议桥粒度 | ■ | | ■ | ■ |
| `D15` 录音合规 | ■ | | | ■ |
| `D16` 高可用与降级文案 | | | ■ | □ |
| `差-D1` `state` 三值 | | □ | | ■ |
| `G-D06` 操作耗时取证 | | | □ | ■ |
**结论**:D-1 的阻塞项只有 `Q-15`/`Q-16` 两项,都可按默认口径先开工(撤回成本低:权限点是数据配置、入口是构建配置);D-2 是阻塞最密集的批次(`D8'`/`Q-15`/`Q-17`/`Q-18`),必须在 D-1 验收前完成一次 30 分钟的决策会;`D11`/`D15` 的定稿窗口是阶段二契约冻结前,因此建议**与公寓端语音三项决策同批拍板,不等本端排期**。
+157
View File
@@ -0,0 +1,157 @@
# 09 公寓端内部契约(进程内)
> 追溯:`docs/10` §4.3 表 4-3(`apt.duty.view` / `apt.duty.command` / `apt.duty.state` 三个服务)、§5.3 第 6 条(状态单写入口)、§4.4 表 4-4(单向依赖)。
> 对端:`docs/公寓端/09-平台接口约定.md`(**跨端** HTTP 契约,20 个接口)、`docs/平台端/05-跨端接口对齐.md`。
> CR-01:表 1-7(角色×动作)、表 1-8(权限点)、表 1-19(接口字段变更)、表 1-22(`F-14` 同口径的冻结方式)。
> 现状:`YktVue/src/api/jcgl/jcgl.js`(旧 HTTP 接口)、`JcglService`(旧实现,将被状态机替代)。
> **CR-02 §15 补遗(进程内契约边界)**:四能力的进程内契约严格分层——值班台 → `apt.duty.view` / `apt.duty.command`(只读 + 提交命令)→ 领域服务(`apt.duty.state`、`apt.face.registry`、`apt.ledger.local`、`platform.catering` 代理),**禁止越过领域服务直写业务表**;平板结果、注册确认、充值核对、食谱发布四类命令均需条件更新 + 二次确认。错误码集合沿用公寓端定义、本端"只消费不自造"(§7 冻结清单)。三端状态映射与数据同步机制以 **CR-02 §15 表 3-66** 为唯一权威,本文件不另立映射;统计链路无进程内调用、不产生跨端副作用。
## 1. 范围:这是进程内契约,不是跨端接口
本端与公寓端在**同一个 JVM** 里(`06` 表 6-1),所以值班台与领域服务之间不需要 HTTP。本文件定义的是一组 **Java 接口(进程内方法契约)**,用于回答三个问题:谁可以调、调完保证什么、改了要通知谁。
表 9-1 三层契约的分工(三份文件不互相替代)
| 文件 | 契约类型 | 载体 | 谁消费 | 变更成本 |
| --- | --- | --- | --- | --- |
| `04-接口设计.md` | 值班台 ↔ `apt-duty` 的 **HTTP 契约**(28 REST + 1 WS) | `/duty/**`、`/ws/duty` | 前端 `entry-duty` | 中(要同步前端与菜单) |
| **本文件 `09`** | `apt-duty` ↔ 公寓端领域服务的 **进程内契约** | Java 接口 + 事件 | 后端各模块 | **高**(编译期即断,跨模块同时改) |
| `docs/公寓端/09` | 公寓端 ↔ 平台端的 **跨端契约**(20 个接口) | HTTPS `/platform/v1/**`、`/open/v1/**` | 平台端 | 最高(两侧独立发版) |
**一条纪律**:`apt-duty` **不得**为了"少一次跨模块调用"而复制领域逻辑。典型反例是"在前端或 `view` 包里按 `jcsj + 10min` 自己算 `fjsj`"——`05` §3 已把重算定为服务端唯一实现(`04` §3.3),本文件把这条落成 `F-D05`。
## 2. 三个核心契约
表 9-2 `apt.duty.view` / `apt.duty.command` / `apt.duty.state` 的职责与方法
| 契约 | 职责 | 允许被谁调 | 关键方法(签名摘自设计,非最终代码) |
| --- | --- | --- | --- |
| `apt.duty.view` | 只读装配:把 `gy_jcxx` + 报警 + 会话 + 本地任务拼成 D1/D2 需要的行 | `apt-duty` 的 `web` 包 | `DutyRowPage page(long gyid, DutyQuery q)`、`DutyDetail detail(long gyid, String jcid)`、`List<DutyEvent> timeline(long gyid, String jcid)` |
| `apt.duty.command` | 受控命令门面:校验 → 落 `duty_manual_action` → 交状态机/领域服务 → 回写结果 → 审计 | `apt-duty` 的 `web` 包(**唯一**入口) | `CommandResult submit(long gyid, String jcid, DutyCommand cmd)` |
| `apt.duty.state` | **状态字段唯一写入口**:`ryzt`/`finish_jc`/`finish_ly`/`stoped` 的条件更新 | `apt.duty.command`、`apt-dispatch`(拟人化的定时链路)、`apt-report`(只读) | `StateChange apply(long gyid, String jcid, StateOp op, int expectedRyzt)`、`boolean canTransit(int from, StateOp op)` |
三条不可让渡的约定:
1. **`apt.duty.command` 是命令的唯一入口**。任何模块(包括 `apt-dispatch` 的定时任务)**不得**绕过它直接调 `apt.duty.state` 做"人工语义"的动作;定时链路可以用 `apply`,但携带的是 `StateOp.CALL_START` 这类**系统操作**,不是 `STOP`/`FORCE_FINISH`。
2. **`expectedRyzt` 必填**。没有它,"看到的和改的不是同一条"就无从拦起;`apt.duty.state` 内部以 `update … where id=? and ryzt=?` 实现,影响行数为 0 即返回 `40902`。
3. **`submit` 内的顺序固定**:先落 `duty_manual_action`(`ACCEPTED`)→ 再调下游 → 再回写 `DONE`/`FAILED` → 最后写 `duty_operation_log`。反序会丢幂等键(`05` 表 5-1)。
命令对象的字段与 `04` 表 4-7 完全对应(`commandType`/`commandId`/`reason`/`expectedState`/`payload`/`operatorId`),本文件不重复列。`commandId` 在进程内改名 `actionId`(与 DDL `uk_manual_action(gyid, action_id)` 对齐)。
## 3. 要素与时刻(`apt.jcxx.local`)
表 9-3 CR-01 的本地建单/本地调整契约(**值班台不直写要素列**)
| 方法 | 语义 | 前置校验(服务内) | 副作用 | 失败码 |
| --- | --- | --- | --- | --- |
| `LocalTaskView assignElement(long gyid, String jcid, ElementAssign assign, String actionId)` | 指定车次/车次号/开车时刻/车次类型,并**锁定**该任务 | `finish_ly='0'`、`jllx='0'`、`ryid` 非空、要素至少给一个 | 写要素列;按 `apt_master_rule` 快照重算 `jcsj`/`fjsj`/`jcbjsj`/`ylysj`;`jllx='1'`;`local_version+1`;写 `apt_local_task`(`origin='LOCAL_ASSIGN'`、`override_*`、`derived_rule_version`);outbox 入队 `#8`(`bizVersion=local_version`) | `40909`/`40911`/`40912`/`40910` |
| `LocalTaskView adjustTime(long gyid, String jcid, Instant newJcsj, String reason, String actionId)` | **只改** `jcsj`,派生时刻按规则重算 | `ryzt=0`、`finish_jc='0'`、`jcsj` 已存在 | 重算三个派生时刻;**`jllx` 不动**;`local_version+1`;`apt_local_task`(`origin='LOCAL_ADJUST'`);outbox 入队 | `40910`/`40902`/`40903` |
| `LocalTaskView lock(long gyid, String jcid, String reason)` | 锁定匹配(`jllx='1'`),**值班台无权限** | `finish_ly='0'`、`jllx='0'` | 置 `jllx='1'` | `40911` |
| `LocalTaskView unlock(long gyid, String jcid, String reason)` | 解除锁定,重回运安匹配池,**值班台无权限** | `jllx='1'` | 置 `jllx='0'`,并将 `match_state` 置 `PENDING` | `40902` |
**两条契约级约束**:
1. **`jcsj` 只能由本契约计算**。`assignElement` 由要素反推、`adjustTime` 由入参给定,两者都必须走同一个 `TimeDerivation` 组件(读 `apt_master_rule` 快照 + `apt_holiday`),并校验 `jcsj ≤ fjsj ≤ jcbjsj < kcsj`(`03` 表 3-10)。快照过期直接 `40903`,**不允许**用默认偏移量兜底。
2. **`ADJUST_TIME` 不写 `jllx`**。原因见 `05` §3:锁定会让"时间微调"变成"数据永久分叉",平台后续匹配到正确交路也无法纠正。
## 4. 居住域(`apt-stay`)
表 9-4 值班台会调到的居住域方法
| 方法 | 调用方 | 语义 | 副作用 | 失败码 |
| --- | --- | --- | --- | --- |
| `CheckinResult checkin(long gyid, CheckinForm form, String actionId)` | `apt.duty.command` | 入寓登记(**`gy_jcxx` 行的产生点**) | 写人员在寓记录 + `gy_jcxx` 初始行(`ryzt=0`)+ 房间占用;派发叫乘计划 | `40901`(房满)/`40001` |
| `SwapResult swapRoom(long gyid, String jcid, String targetRoomId, String actionId)` | `apt.duty.command` | 换房 | 改 `gy_jcxx.fjh`;联动 `apt_sip_binding`(分机随房走);触发时刻重算 | `40901`/`40902` |
| `void markLeave(long gyid, String jcid, LeaveType type, String actionId)` | `apt.duty.state` | 登记离寓(`MANUAL` / `FORCE`) | 写 `gygl_ly_event` + `finish_ly='1'`;释放房间与分机绑定 | `40902` |
| `CleanTask createClean(long gyid, String roomId, String actionId)` | `apt.duty.command` | 保洁任务 | 写 `gy_clean_task` | `40901` |
| `RoomPage rooms(long gyid, RoomQuery q)` | `apt.duty.view` | 房态查询 | 无 | — |
**关键区分(最容易实现错的地方)**:`checkin` 是"建行",`markLeave` 是"改行"。值班台上两者都在 D3 页面出现,但**权限归属完全不同**——`checkin` 由 `apt-stay` 负责建 `gy_jcxx` 行(CR-01 S1),而 `markLeave` 虽然由 `apt.duty.state` 发起,**`gygl_ly_event` 的写入仍由 `apt-stay` 完成**。值班台两处都不直接写执行域表。
## 5. 语音域(`apt.voice.endpoint` / `apt.voice.media`)
表 9-5 语音域方法
| 方法 | 调用方 | 语义 | 副作用 | 失败码 |
| --- | --- | --- | --- | --- |
| `CallSession voiceCall(long gyid, String jcid, String actionId)` | `apt.duty.command` | 单房间语音临叫 | 建 `apt_call_session`(`is_temp_call='1'`);AMI `Originate` 入 `Conf-01`;**不改 `ryzt`** | `40908`/`50301`/`40906` |
| `CallSession broadcast(long gyid, List<String> roomIds, BroadcastForm f, String actionId)` | 同上 | 喊话 / 多房间喊话(≤ 20 间) | 同上,多个会话 | `40908`/`40001`/`50301` |
| `void hangup(long gyid, String sessionId, String actionId)` | 同上 | 挂断会话(**临叫的"停止"走这里,不走 `STOP`**) | 会话置结束 + AMI 挂断;`ryzt` 由状态机按"是否已应答"回退 3 或 2 | `40401`/`40902` |
| `SessionPage sessions(long gyid, SessionQuery q)` | `apt.duty.view` | 会话列表(单 `Conf-01` 占用情况) | 无 | — |
| `RecordingUrl recording(long gyid, String sessionId)` | `apt.duty.view` | 短期签名回放地址 | 无(`PURGED` 直接拒) | `41001` |
**为什么临叫必须走 `apt.voice.endpoint` 而不是 `apt.duty.state`**:临叫**不改变任务状态**。若把它记成一次叫乘,会出现"值班员点了语音临叫,D1 行变成叫乘中,Quartz 又触发一次正式叫乘"的双重呼叫。这也是 `Q-17` 要拍板的原因(`08` 表 8-1)。
## 6. 事件订阅
表 9-6 状态机事件 → `/ws/duty` 消息的映射(**订阅方只有 `apt-duty` 的 `ws` 包**)
| 事件(发布方) | 触发源 | `/ws/duty` 消息 | 载荷关键字段 | 前端动作 |
| --- | --- | --- | --- | --- |
| `DutyStateChanged`(`apt.duty.state`) | 任何 `apply` 成功 | `state.changed` | `jcid`、`ryztBefore`、`ryztAfter`、`stoped`、`finishJc`、`finishLy`、`seq` | 只更新该行,不整表刷新 |
| `DutyAlarmRaised`(`apt.dispatch`) | 30 s 无入会 / 超 15 min 未离寓 | `alarm.raised` | `jcid`、`alarmType`、`alarmEventKey`、`seq` | 行置红 + 报警音 + TTS |
| `DutyAlarmCleared`(`apt.duty.state`) | `STOP`/`MARK_LEAVE` 成功 | `alarm.cleared` | `jcid`、`alarmEventKey`、`seq` | 停该行报警音 |
| `CallSessionChanged`(`apt.voice.endpoint`) | 会话建立 / 挂断 / 入会 | `session.changed` | `sessionId`、`state`、`confNo`、`seq` | D5 与 D1 语音角标 |
| `MatchStateChanged`(`apt.jcxx.local`) | `match_state` 变化 | `match.changed` | `jcid`、`matchState`、`matchVersion`、`seq` | 行徽标(锁 / 未匹配 / 待核验) |
| `NoticeSync`(`apt-master-client`) | 运安同步结果 | `notice.sync` | `autoCallFailed`、`missedCallWarning`、`key` | 800 ms 合并的 TTS 提醒 |
| `DeviceChanged`(`apt-device`) | 在线状态变化 | `device.changed` | `deviceCode`、`onlineState` | D6 行状态 |
**三条事件契约纪律**:
1. **`seq` 单调递增且全局唯一**(同一 `gyid` 内)。前端据此丢乱序、检测跳号;跳号即触发一次兜底拉取(`04` 表 4-12)。
2. **事件只带"变了什么",不带整行**。整行由 `GET /duty/jcxx/{jcid}` 或兜底列表补齐,避免"事件里带了大字段导致推送队列打满"(`06` 表 6-3 的 `PUSH_QUEUE_CAPACITY`)。
3. **不允许跨 `gyid` 广播**。`ws` 包按 `gyid` 建会话分组,跨公寓会话直接拒绝(`03` 表 3-12 第 3 条)。
## 7. 冻结清单(`F-D01~F-D10`)
阶段二冻结的就是这十条。**冻结 = 签认后修改必须走 §9 流程**,不是"文档写完了"。
表 9-7 进程内契约冻结项
| # | 冻结项 | 冻结内容 | 为什么必须冻 | 对应 `docs/公寓端/09` |
| --- | --- | --- | --- | --- |
| `F-D01` | 状态字段写入口 | `ryzt`/`finish_jc`/`finish_ly`/`stoped` 只允许 `apt.duty.state` 更新 | 一旦出现第二个入口,"停止叫乘被迟到事件改回"必然复现 | 与 `F-01`(跨端幂等)不同层,不可互替 |
| `F-D02` | 命令幂等键 | `actionId`(= 前端 `commandId`)+ `uk_manual_action(gyid, action_id)`;重复请求回放首次结果 | 断网重试、双击、刷新都会重发;没有它就没有"操作一次"的语义 | — |
| `F-D03` | `ryzt` 六值语义 | 0 待叫 / 1 叫乘中 / 2 报警 / 3 待离寓 / 4 离寓报警 / 5 通话中;其他值视为"在寓" | 前端配色、可发命令、`expectedRyzt` 全部依赖它 | — |
| `F-D04` | `STOP` 的副作用边界 | 只置 `stoped='1'`,**`ryzt` 保持 2 不变**;不写 `finish_*` | 若把 `STOP` 写成"恢复待叫",值班员的"停止"会被下个周期重新触发 | 见 `docs/公寓端/03` 三条保护语义第 3 条 |
| `F-D05` | 时间派生规则 | `jcsj` 唯一基准;`fjsj=jcsj+10min`、`jcbjsj=jcsj+15min`;`ylysj` 路径 A 不变 / 路径 B `=jcsj+lysjgz`;校验 `jcsj≤fjsj≤jcbjsj<kcsj` | 前端、报表、平台对账三方都按它算;改一处等于改三处 | 与 `D8'` 绑定 |
| `F-D06` | 锁定语义 | `ASSIGN_ELEMENT` 置 `jllx='1'`;`ADJUST_TIME` **不动 `jllx`** | 锁定的副作用是"平台不能再纠正",只能由人决定 | 与 CR-01 `F-14` 的 `matchVerdict` 语义配套 |
| `F-D07` | `pending_snapshot` 结构 | `04` 表 4-11 的 8 个字段,json 键名不可改 | 本期不实现交接班,但结构一旦漂移,二期拿不到可比的历史语义 | — |
| `F-D08` | `/ws/duty` 消息字典 | `04` 表 4-12 的 12 种消息 + `seq` 语义 + 60 s 兜底 | 前端按消息名分支,改名等于前后端同时崩 | — |
| `F-D09` | 错误码集合 | `04` 表 4-4 的 20 个;本端**只消费不自造** | 自造码会让"40902 是什么意思"在三个端各有解释 | 与跨端平台码严格分开(`docs/公寓端/09` 表 9-4) |
| `F-D10` | `gyid` 注入方式 | 从令牌取,**忽略任何入参 `gyid`**;SQL 必带 `gyid` | 这是四级隔离的最后一道;一旦允许入参覆盖,跨公寓用例全废 | — |
**冻结清单的使用方式**:D-1 开工前逐条签字;每条在 Code Review 时对应一条检查项(`F-D01` 对应 ArchUnit 断言、`F-D02` 对应"连点 3 次只有 1 行"用例、`F-D05` 对应时间派生用例)。签认记录挂在阶段二产出里,**不进 `duty_*` 表**(表里只放操作事实,不放契约文本)。
## 8. 与 `/apt/**` 门面的差异
同一个领域服务有两个 HTTP 门面(`/apt/**` 与 `/duty/**`),差异必须写清楚,否则"两套接口都能停叫"会变成"两套权限"。
表 9-8 两个门面的分工
| 项 | `/apt/**`(管理端) | `/duty/**`(值班台) |
| --- | --- | --- |
| 调用者 | 公寓管理员 | 值班员 |
| 命令集合 | 10 个(含 `LOCK_MATCH`/`UNLOCK_MATCH`) | **8 个**(不含两个锁命令,`04` 表 4-6) |
| 权限点前缀 | `apt:*` | `duty:*` |
| 可见页面 | D1~D10 + A1~A18(共 28 页,见 `docs/公寓端/ui/README` 表 1.1) | **10 页**(`02` 表 2-7) |
| 批量能力 | 有(批量启停、批量导入) | **无**(一次一行,避免"选错整批") |
| 删除能力 | 逻辑删除(快照表除外) | **无**(`03` 表 3-13) |
| 主数据写入 | 只读快照(`40904`),应急新增受 `D7` 约束 | 只读(`40904`),无新增 |
| 共用部分 | **同一领域服务、同一状态机、同一 `duty_manual_action` 幂等表、同一审计切面** | 同左 |
**一条底线**:两个门面的 Controller **不得互相调用**(`02` 表 2-10)。若某功能两边都要,就把逻辑沉到领域服务,而不是让 `/duty/**` 去调 `/apt/**` 的 Controller——否则鉴权会被绕开(设备可达的 `/open/**` 也会顺着这条链被污染)。
## 9. 契约变更流程
表 9-9 改一条冻结项要做什么
| 步 | 动作 | 产出 |
| --- | --- | --- |
| 1 | 提"契约变更"说明:改哪一条(`F-D*`)、为什么、影响哪些模块与页面 | 变更单(含前后签名 diff) |
| 2 | 检查是否触及跨端(`docs/公寓端/09` 的 `F-01~F-14`);触及则先改跨端契约,再改本契约 | 跨端变更记录 |
| 3 | 同步改三处:本文件表 9-7 + `04`(若影响 HTTP)+ `05`(若影响流程) | 文档 diff |
| 4 | 评估已上线的 `duty_manual_action` 历史数据是否可解释(如 `F-D03` 改状态码含义,历史行会"说谎") | 结论写入变更单;不可解释则禁止改 |
| 5 | 评审通过后更新冻结签字,并在下一次迭代开工前完成代码 | 新签字 |
**最容易忽略的是第 4 步**:`duty_manual_action` 只追加不更新(`03` 表 3-2),所以任何"改含义"的变更都会让历史数据按新语义被误读。典型例子是给 `action_type` 增加别名——老行不认新名,看板统计会突然出现"未分类"。因此 `F-D03`(`ryzt` 六值)与 `F-D09`(错误码)属**只可增不可改语义**的条目,变更单里必须显式写明"新增枚举值,不复用旧值"。
+260
View File
@@ -0,0 +1,260 @@
# 10 决策单:Q-15 ~ Q-19
> 用途:把 `08-待决策与风险.md` 表 8-6 的 5 个待确认问题整理成**可直接上会**的决策单。一项一议,每项固定五段:背景 / 可选项与影响 / 默认建议 / 谁拍板 / 阻塞批次。
> 来源:`08` 表 8-1(5 个待确认问题)、表 8-6(默认与决策角色)、表 8-7(决策 × 子批次矩阵);`07` 表 7-2(D-1/D-2/D-3 子批次)、表 7-3(验证项)。
> 纪律:**编号不变、口径沿用**。本单不新增决策编号,也不改写 `08` 的默认建议;会上若改变默认,回填 `08` 表 8-6 与本单表 10-8,不在此处另立结论。
## 0. 怎么用这张单子
1. 五个议题全部来自 `08` 表 8-6,**不回答即按"默认建议"执行**(`08` 表 8-6 的"不回答时的默认"列已约定),所以会议的目标不是"必须讨论完",而是"把需要改的挑出来"。
2. 每个议题的"阻塞批次"指 `07` 表 7-2 的 D-1(值守链路底座)、D-2(盯控台 D1/D2)、D-3(其余 8 页);`■` = 不拍板就不能开工,`□` = 可以开工但会影响验收口径。
3. 会上只做三件事:确认或否定默认、给附加条件、指派回填人。技术方案细节不在会上展开(各节已给到"改哪个文件的哪张表")。
4. 相邻的 `D6`/`D8'`/`D11`/`D15`/`D16`/`D17` **不在本单范围**:它们有各自的决策窗口(多为阶段二契约冻结前),且主要由公寓端与平台端主导。若本次顺带拍定,只登记到 `08` 表 8-1,不占本单编号。
## 1. 速览
表 10-1 五个议题、默认结论与阻塞关系
| 编号 | 一句话议题 | 默认建议 | 谁拍板 | 阻塞批次 | 最晚决策点 | 建议时长 |
| --- | --- | --- | --- | --- | --- | --- |
| `Q-15` | 权限点是否独立 + 调整幅度阈值 + 准点率三档 | 独立 `duty:jcxx:adjustTime` / 30 min 一次确认 / `≤30s·30~120s·>120s` | 产品 + 架构 | D-1 `■`、D-2 `■` | D-1 首次提测前 | 15 min |
| `Q-16` | 前端入口:多页面构建 还是 运行时按路径切 | 多页面构建(`pages: { apt, duty }`) | 前端 + 架构 | D-1 `■`、D-2 `■`、D-3 `■` | D-1 脚手架提交前 | 15 min |
| `Q-17` | 临时叫乘算不算事件 + 临叫是否写 `ryzt` | 不算:纯语音 + 留痕,不写 `ryzt` | 产品 + 语音负责人 | D-2 `■`、D-3 `□` | D-2 语音模块开工前 | 10 min |
| `Q-18` | 值班员能否直接改 `fjh` | 不能:只走 `POST /duty/room/swap` | 产品 + 架构 | D-2 `■`、D-3 `□` | D-2 字段白名单定稿前 | 10 min |
| `Q-19` | `duty_operation_log` 保留期 | 365 天 + 不自动清理(只统计告警) | 合规 + 运维 | D-1 `□`、D-3 `■` | D-3 开工前 | 5 min |
表 10-2 议程(70 分钟,含缓冲)
| 时段 | 内容 | 产出 |
| --- | --- | --- |
| 0–5 min | 规则说明:五项的默认口径已写在 `08` 表 8-6,"不表态即默认" | 确认到场人与有权拍板人齐备 |
| 5–20 min | `Q-15` 三个子问题(15c 若无异议可一句带过) | 15a/15b/15c 三个结论 |
| 20–35 min | `Q-16` 入口切分 | 结论 + 谁改脚手架 |
| 35–45 min | `Q-17` 临叫语义(17a 记不记 / 17b 写不写 `ryzt`) | 两个结论 + 是否新增 `action_type` |
| 45–55 min | `Q-18` 字段白名单 | 结论 + 是否维持 `差-D2` |
| 55–60 min | `Q-19` 保留期 | 结论 + 清理动作是否进 D-3 |
| 60–70 min | 复核与回填分工(表 10-8、表 10-10) | 五人签字与回填人 |
## 2. Q-15 权限点粒度与调整阈值
### 2.1 背景
`Q-15` 实际是**三个互不相同的参数**被合在一个编号下(CR-01 表 1-22 把它们并成一条挂出),会上必须拆开表态,否则会出现"整体通过但某一条没人认"的情况:
- **15a 权限点粒度**:CR-01 提了两种写法——独立 `duty:jcxx:adjustTime`,或复用 `apt:jcxx:changeStatus`。`02` 表 2-9 目前按"独立"写了 18 个 `duty:*` 权限点,但没被确认。
- **15b 调整幅度阈值**:`ADJUST_TIME` 超过多少分钟需要二次确认。该值同时是后端校验与前端弹窗的开关(`06` 表 6-3 的 `APT_DUTY_ADJUST_TIME_CONFIRM_MINUTES`)。它直接决定 `07` 表 7-6 第 2 项"值班操作步骤增加不超过 10%"能否达标。
- **15c 准点率三档**:CR-01 §2.10 注明"文档中从未固定",只建议 `≤ 30 s` 准点 / `30~120 s` 迟到 / `> 120 s` 失败。本端页面不显示准点率,但验收时要用它解释"不低于现网"。
为什么必须现在定:权限点是**数据**(`sys_menu` 行 + 角色授权),改名要逐台设备重配;阈值是**验收口径**,改了要回归全部命令用例;准点率是**报表口径**,三端共用一个解释。
### 2.2 可选项与影响
表 10-3 Q-15 三个子问题的选项对比
| 子问题 | 选项 | 做法 | 收益 | 代价 / 风险 | 影响批次 |
| --- | --- | --- | --- | --- | --- |
| 15a | **A 独立**(建议) | `duty:jcxx:adjustTime` + `duty:jcxx:assignElement` 两个权限点 | 可对"能改时刻但不能改要素"单独授权(两者副作用不同,见 `F-D06`);审计可按权限点分辨入口 | `sys_menu` 多 2 行,初始化脚本要维护 18 个 `duty:*` | D-1 `■` |
| 15a | B 复用 `apt:jcxx:changeStatus` | 值班台与管理员共用一个权限点 | 权限点少 | `apt:*` 与 `duty:*` 混用 → 值班员可能被误授管理员能力;`changeStatus` 语义不含要素 | D-1 `■` |
| 15a | C 合并为 `duty:jcxx:modify` | 两个要素命令共用一个点 | 授权最简 | `ASSIGN_ELEMENT` 会锁 `jllx`、`ADJUST_TIME` 不锁,副作用不同却无法分权;审计只能靠 payload 反推 | D-1 `■` |
| 15b | **A 固定 30 min**(建议) | `\|Δ\| ≤ 30 min` 一次确认,`> 30 min` 二次确认 | 与 `06` 表 6-3 的默认值一致;实现与验收最省 | 极端情况下 31 min 的误输入只多一次确认,保护有限 | D-2 `■` |
| 15b | B 三级 | `≤10` 直接提交、`10~30` 一次确认、`>30` 二次确认 | 小改动不打扰,最贴合"步骤不增加" | 多一条分支 + 需产品给三个值;前端要三套提示文案 | D-2 `■` |
| 15b | C 一律二次确认 | 任何调整都要两次确认 | 误改风险最低 | **直接顶破** `07` 表 7-6 第 2 项(每次调整 4 步) | D-2 `■` |
| 15c | **A 采纳 CR-01 三档**(建议) | `≤30 s` 准点 / `30~120 s` 迟到 / `>120 s` 失败 | 三端一个口径;验收可直接引用 | 准点率会低于"现网无口径"时的观感值 | 不阻塞 |
| 15c | B 不设固定分档 | 仍只写"不低于现网" | 不改任何文档 | 验收时"不低于"无法计算,会退化成口头判断 | 不阻塞 |
### 2.3 默认建议
**15a → A(独立权限点)、15b → A(30 min)、15c → A(三档)**,即维持 `02` 表 2-9、`04` 表 4-8、`06` 表 6-3 已写的默认。
一句话理由:15a 的独立是因为两个要素命令**副作用不同**(一个锁 `jllx`、一个不锁),同权限点会让"能不能改要素"无法分权;15b 的 30 min 是在"防误改"与"步骤不膨胀"之间的最小实施点,若现场反馈过于频繁,二期改 B 只需加一个分支;15c 不涉及本端代码,纯粹是把验收口径钉住。
### 2.4 谁拍板与阻塞
- **拍板人**:产品(15b、15c)+ 架构(15a);15c 追加数据/报表负责人确认口径。
- **阻塞**:**D-1 `■`**(权限点要在 D-1 注册进 `sys_menu`)、**D-2 `■`**(二次确认弹窗与按钮可见性)、D-3 无关。
- **决策窗口**:D-1 首次提测前。越过则权限点要改名并重配全部已部署环境。
## 3. Q-16 前端入口切分方式
### 3.1 背景
`docs/10` 表 2-3 只定了"同一 SPA、两个入口",没说入口是**构建期**切还是**运行期**切。`06` §2 列了两种实现并声明"未定前按多页面构建设计",但这是设计默认,不是决议。
影响面比看起来大:入口方式决定 `vue.config.js` 结构、HTML 模板数量、Nginx `try_files` 写法、路由 `base`、以及"值班台首屏加载多少代码"。这个决定越晚改越贵——一旦页面写了几十个,切换方式要连带改所有 import 路径与部署脚本。
### 3.2 可选项与影响
表 10-4 Q-16 三个选项对比
| 选项 | 做法 | 收益 | 代价 / 风险 | 影响批次 |
| --- | --- | --- | --- | --- |
| **A 多页面构建**(建议) | `vue.config.js` 配 `pages: { apt, duty }`,两个 HTML 模板;Nginx 各自 `try_files … /duty.html` | 首屏**只加载本入口代码**,值班台不被管理端 24 页拖慢;与 `D6`"同工程双入口"完全一致 | 构建配置复杂:公共依赖要去重(`splitChunks`)、本地开发要起两个访问路径、两个入口的 `publicPath` 要分别验证 | D-1 `■`、D-2 `■`、D-3 `■` |
| B 运行时按路径切 | 单 `index.html`,`main.js` 读 `location.pathname` 决定注册哪些路由与菜单 | 构建最简单,一套配置 | 首屏会带上另一端代码;`main.js` 出现"按路径分支"的隐式约定,新人漏改就白屏;`/duty` 与 `/apt` 的公共依赖无法按入口裁剪 | D-1 `■`、D-2 `■`、D-3 `■` |
| C 两个独立工程 | 完全拆开 `apt-web` 与 `duty-web` | 物理隔离最彻底 | 等于把 `D6` 改判为"独立前端工程":两套 `node_modules`、两套构建、两套发版,而后端仍同 jar,运维上零收益 | 超出本单范围(须先改 `D6`) |
### 3.3 默认建议
**A 多页面构建**。理由:`06` §2 已按 A 设计,且 A 是唯一能做到"值班台不加载管理端页面"的方式——值班台工作站多为办公机,首屏体积是实际体验问题。选 C 需先推翻 `D6`,不在本次议题内。
**给实施的一条附加要求**(无论选 A 或 B 都要):`entries/duty/**` 与 `entries/apt/**` 之间**禁止互相 import**,只用 `src/shared/**`;用 eslint `no-restricted-imports` 兜住,与 `02` 表 2-5 的目录规则配套。
### 3.4 谁拍板与阻塞
- **拍板人**:前端 + 架构。
- **阻塞**:**D-1 `■`**(脚手架)、**D-2 `■`**、**D-3 `■`**(三批页面都受入口方式影响)。
- **决策窗口**:D-1 脚手架提交前。越过则要改构建配置 + Nginx + 全部页面路径。
## 4. Q-17 临时叫乘的语义
### 4.1 背景
现状三个接口(`handleRoomCall` / `handleRoomCall2` / `handleMultiRoomCall`)是"直接拨号、不改任务状态"(`01` 表 1-6 第 7~9 行)。目标态把临叫收进 `apt.voice.endpoint`,但**没有文档定过"要不要同时记成一次叫乘"**。
争议的真实来源:如果不记,D1 行上看不出"刚才有人临叫过",事后只能查语音会话;如果记,Quartz 到点会再叫一次,出现"同一任务两次呼叫",且必须在某处抑制——而抑制逻辑本身又是一处隐式状态。
**上会前必须先澄清的一处内部矛盾**(本单实测发现,`Q-17` 真正的技术分水岭,下称 17b):
- `04` 表 4-10 末尾写:"对已有任务行的临叫**会顺带触发 `ryzt=1`**";
- `05` 图 5-4 只写 `is_temp_call='1'`,**没有**改 `ryzt`;
- `05` 表 5-5 第 3 条把"有临叫"列为**独立于 `ryzt in (1,5)`** 的判据——如果临叫本身就置 `ryzt=1`,这条判据就冗余了;
- `08` 表 8-1 的默认口径是"**不改 `ryzt`**"。
三处不一致。后果是实在的:**临叫若把 `ryzt` 置 1,Quartz 的复叫/报警定时器会认为该任务已在叫乘中**,要么抑制正式叫乘(人没叫到车),要么重复呼叫(同一人被叫两次)。所以 17a(记不记事件)与 17b(写不写 `ryzt`)必须分开表态,不能一句话带过。
### 4.2 可选项与影响
表 10-5 Q-17 两个子问题(17a 记不记 / 17b 写不写)的选项对比
| 子问题 | 选项 | 做法 | 收益 | 代价 / 风险 | 影响批次 |
| --- | --- | --- | --- | --- | --- |
| 17a | **A 纯语音 + 留痕**(建议) | 建会话 `is_temp_call='1'`;不改 `jcsj`、不进叫乘队列;**仍走 `apt.duty.command` 落 `duty_manual_action`**(`action_type` 新增 `TEMP_CALL`) | 状态单一写入口(`F-D01`)保持;D8 能查到"某人临叫过",审计不断裂 | 需在 `DutyActionType` 增一个枚举值(`G-D01` 已允许增枚举);D1 行本身不显示"临叫中"(靠 D5 或语音角标) | D-2 `■` |
| 17a | B 记为叫乘事件 | 写 `ryzt` 或新增"临叫中"状态,参与准点率与叫乘统计 | D1 行最直观;统计口径统一 | 与 Quartz 正式叫乘冲突(双叫),需额外的抑制逻辑;违反 `F-D01`;`STOP` 与 `HANGUP` 语义变模糊 | D-2 `■`、D-3 `□` |
| 17a | C 只拨号不留痕 | 前端直调 `apt.voice.endpoint`,绕过 `apt.duty.command` | 实现最快,链路最短 | 审计断裂(值班员做过的事查不到),违反 `01` 表 1-6"每个操作入口都有落点";无法统计临叫次数 | 不可接受 |
| 17b | **A 不写 `ryzt`**(建议) | 临叫全程不改 `ryzt`;"有临叫"靠 `apt_call_session` 与 `is_temp_call` 表达 | Quartz 的复叫/报警定时器行为完全不受影响;`05` 表 5-5 第 3 条判据成立 | 必须先把 `04` 表 4-10 末尾那句改成一致表述,否则实现方按"置 1"写 | D-2 `■` |
| 17b | B 写 `ryzt=1` | 临叫即置叫乘中,挂断后复位 | D1 行状态最直观,无需额外角标 | 人工动作直接篡改 Quartz 的**状态输入**:要么抑制正式叫乘(人没叫到车),要么重复呼叫 | D-2 `■` |
### 4.3 默认建议
**17a → A(纯语音 + 留痕)、17b → A(不写 `ryzt`)**。理由:临叫的业务目的是"让人听到",不是"改变任务状态";把它记成状态会让"叫乘"这个词同时指两件事,后续所有判断(准点率、复叫、报警)都要额外区分——而 17b 选 B 更严重:`ryzt` 是 Quartz 的**状态输入**,临叫写它就是让人工动作篡改定时器判据。
**附带两条实施约束**(写进 `04` 表 4-10):
1. 临叫的**停止**必须走 `HANGUP`,**不能**走 `STOP`——否则会把正式任务停掉(`04` 表 4-10 已写)。
2. 临叫占用 `Conf-01`,所以并发临叫第二个请求返回 `40908`,与正式叫乘**共用同一把会话锁**(`08` 表 8-2 D11 行)。
### 4.4 谁拍板与阻塞
- **拍板人**:产品 + 语音负责人。
- **阻塞**:**D-2 `■`**(语音模块与 D1 行状态)、**D-3 `□`**(D5 会话列表的筛选)。
- **决策窗口**:D-2 语音模块开工前。越过则改判 B 要重做 D1 行状态语义,且语音与叫乘两条链路都要回归。
## 5. Q-18 值班员可写字段是否再收紧
### 5.1 背景
现状 `modifyRyXx` / `exchangeRoom` 等接口允许直接改 `fjh`。值班员现场最常见的诉求恰恰是"房间号不对"。
目标态出现了两条语义不同的路径:
- `ASSIGN_ELEMENT` 改的是**要素**(`jlid`/`cc`/`jch`/`kcsj`/`cclx`),**不含 `fjh`**,副作用是**锁定匹配**(`jllx='1'`);
- `POST /duty/room/swap` 改的是**房间**(`fjh`),副作用是**分机随房联动**(`apt_sip_binding`)。
如果允许 `PATCH fjh`,就会出现第二个写入口,且**绕过分机联动**——叫乘仍打旧房间分机,这是"看起来改对了、实际叫错人"的典型故障。
**先排除一个易混项**:`fjh` 是**房间号**,与 `01` 表 1-9 里"分机号修改本期不做(`40906`)"是两件事——后者改的是 SIP 分机号。本项只议房间号。
### 5.2 可选项与影响
表 10-6 Q-18 三个选项对比
| 选项 | 做法 | 收益 | 代价 / 风险 | 影响批次 |
| --- | --- | --- | --- | --- |
| **A 不允许**(建议) | `fjh` 只能通过 `POST /duty/room/swap`;`PATCH /duty/jcxx/{jcid}` 仅 `leave_remark` | 单一写入口;分机随房联动完整;`差-D2` 结论成立 | 改房号要切到 D3(但换房本来就属 D3);值班员会问"为什么不能在这改" | D-2 `■`、D-3 `□` |
| B 允许 `PATCH fjh` | 在 D1 行内直接改房间号 | 少一步 | `apt_sip_binding` 不同步 → 打错房间;ArchUnit 第 1 条断言失效;`03` 表 3-8 白名单作废 | D-2 `■` |
| C 在 `ASSIGN_ELEMENT` 里带 `fjh` | 要素命令 payload 增加 `fjh` | 一次提交解决两件事 | 换房会同时**锁定匹配**并触发 `#8` 要素上报 → `matchState` 与上报语义混乱(换房不该改变要素) | D-2 `■` |
### 5.3 默认建议
**A**。理由:`fjh` 不是要素,它是"人在哪个房间",改它必然要同步分机绑定与房间占用,这属于 `apt-stay` 的职责,不属于要素调整。
**给产品的一条替代方案**:如果现场"改房号"频次确实高,正确做法是**在 D1 行内嵌 D3 的换房弹层**(调同一个 `swapRoom`),而不是放宽字段白名单。这样"少一步"与"联动完整"可以同时满足。
### 5.4 谁拍板与阻塞
- **拍板人**:产品 + 架构。
- **阻塞**:**D-2 `■`**(`PATCH` 白名单与 D1 操作入口)、**D-3 `□`**(D3 换房交互)。
- **决策窗口**:D-2 字段白名单定稿前。越过则要放开 ArchUnit 断言并在别处补分机联动,返工面大。
## 6. Q-19 审计保留期
### 6.1 背景
`duty_operation_log` 只追加不更新(`03` 表 3-2),是事后追责的唯一业务级证据(另一层是 RuoYi 的 `sys_oper_log`,只能看 HTTP 痕迹)。`docs/10` 表 8-2 只要求"审计可查、保留期明确",**从未给天数**。
量级估算:单公寓每天按 ≤ 数百次值班操作,一年 ≤ 10 万行,索引 `(gyid, created_at)` 下磁盘不是约束。所以这个决策的实质是**追责能力 vs 运维洁癖**,不是容量问题。关键在于:清理是**不可逆**动作,删掉就再也无法复盘。
### 6.2 可选项与影响
表 10-7 Q-19 三个选项对比
| 选项 | 做法 | 收益 | 代价 / 风险 | 影响批次 |
| --- | --- | --- | --- | --- |
| **A 365 天 + 不自动清理**(建议) | `APT_DUTY_AUDIT_RETENTION_DAYS=365`;到期只统计并出告警,物理清理需单独审批 | 跨季度核查有据可查;`08` §2"不删"原则一致 | D9 默认时间范围必须限制(默认当月),否则首屏慢 | D-1 `□`、D-3 `■` |
| B 90 天 + 定时清理 | 与录音留存期(D15)对齐,定时任务物理删除 | 与录音口径一致,空间可控 | "上季度那次停叫是谁做的"永久失去;与 `03` 表 3-2"只追加"的语义形成例外 | D-3 `■` |
| C 与平台审计统一 | 天数待平台给,本端跟随 | 三端口径一致,最不容易被质疑 | 平台口径未定 → 本端被阻塞;本端不能等 | D-3 `■` |
### 6.3 默认建议
**A(365 天,不自动清理)**。理由:审计表是"只追加"的证据链,删除是唯一不可逆动作,在没有任何合规文件要求缩短的前提下,"少删"是安全侧。
**一条落地要求**:到期数据只打告警(进 D10 自检项),物理清理由运维按次审批执行;这样即便将来改判 B,也只是把"审批"变成"定时",不需要改表结构。
### 6.4 谁拍板与阻塞
- **拍板人**:合规 + 运维。
- **阻塞**:**D-1 `□`**(写入不受影响)、**D-3 `■`**(D9 的时间范围选项与统计任务)。
- **决策窗口**:D-3 开工前。越过则若定 90 天,清理任务要挤进 D-3 排期;若定统一口径,整个 D-3 的 D9 页要等平台。
## 7. 决议记录
表 10-8 会议结论(**会上现场填写**,未填即视为"按默认执行")
| 编号 | 子问题 | 决议结论 | 是否偏离默认 | 附加条件 | 决议人 | 日期 |
| --- | --- | --- | --- | --- | --- | --- |
| `Q-15` | 15a 权限点粒度 | | 否(默认 A 独立) | | | |
| `Q-15` | 15b 调整幅度阈值 | | 否(默认 A 30 min) | | | |
| `Q-15` | 15c 准点率三档 | | 否(默认 A 三档) | | | |
| `Q-16` | 入口切分方式 | | 否(默认 A 多页面构建) | | | |
| `Q-17` | 17a 是否记为事件 | | 否(默认 A 纯语音 + 留痕) | | | |
| `Q-17` | 17b 是否写 `ryzt` | | 否(默认 A 不写) | | | |
| `Q-18` | `fjh` 可写性 | | 否(默认 A 不允许) | | | |
| `Q-19` | 审计保留期 | | 否(默认 A 365 天不删) | | | |
**填写规则**:①"决议结论"只写选项字母 + 一句限定语(例:`A,但阈值改 20 min`);②"附加条件"写必须同时满足的前提(例:`前提是 D1 内嵌换房弹层`);③三者必须齐(结论、决议人、日期),缺一视为未决议,按默认执行并不追溯。
## 8. 未决兜底与最晚窗口
表 10-9 不拍板会发生什么
| 编号 | 不表态时立即采用 | 最晚决策点 | 越过窗口的后果 |
| --- | --- | --- | --- |
| `Q-15` | 独立权限点 / 30 min / 三档 | D-1 首次提测前 | 权限点改名要动 `sys_menu` 与初始化脚本并重配已部署环境;阈值改动要回归全部命令用例 |
| `Q-16` | 多页面构建 | D-1 脚手架提交前 | 要改构建配置 + Nginx + 路由 `base` + 全部页面路径 |
| `Q-17` | 纯语音 + 留痕,且不写 `ryzt` | D-2 语音模块开工前 | 改判要重做 D1 行状态语义,语音与叫乘两条链路都回归;且 `04` 表 4-10 与 `05` 的表述必须同步改,否则实现方必按错的那句写 |
| `Q-18` | 不允许 | D-2 字段白名单定稿前 | 改判要放开 ArchUnit 断言,并在别处补 `apt_sip_binding` 联动 |
| `Q-19` | 365 天 + 不删 | D-3 开工前 | 定 90 天则清理任务挤进 D-3;定统一口径则 D9 整页等平台 |
**一条兜底原则**(与 `08` §2 一致):涉及"是否出现第二个写入口"的(`Q-18`)按最严默认,涉及"是否要多存数据"的(`Q-19`)按不删默认。这两类返工成本不对称——架构破了要重构,数据删了无法恢复。
## 9. 会后回填清单
表 10-10 结论落点(**谁回填、填到哪一行**)
| 编号 | 回填文件与位置 | 回填人 |
| --- | --- | --- |
| `Q-15` | `08` 表 8-1("需定稿时点"列标"已定稿")与表 8-6(新增"结论"列);`02` 表 2-9(权限点);`04` 表 4-8(阈值校验);`06` 表 6-3(`APT_DUTY_ADJUST_TIME_CONFIRM_MINUTES`) | 后端 + 产品 |
| `Q-16` | `06` §2(删掉"未定前按…"的过渡句);`02` 表 2-5(目录规则);`07` 表 7-2 的 D-1 前置 | 前端 |
| `Q-17` | `04` 表 4-10(含末尾"触发 `ryzt=1`"那句按 17b 结论**改写成一致表述**);`03` 表 3-3 列注释与表 3-4 取值(采纳 A 则新增 `TEMP_CALL`);`05` 图 5-4 与表 5-5 第 3 条 | 后端 + 语音 |
| `Q-18` | `03` 表 3-8(白名单);`04` 表 4-1(`PATCH` 范围);`01` 表 1-4;`08` 表 8-4 `差-D2` | 后端 + 架构 |
| `Q-19` | `06` 表 6-3(`APT_DUTY_AUDIT_RETENTION_DAYS`);`03` 表 3-2;`07` 表 7-3 的 D-3 验证项;`08` 表 8-4 `差-D4` | 运维 + 合规 |
| 全部 | `08` 表 8-6 新增"结论"列并指向表 10-8(该表现有列为 `#`/问题/关联/不回答时的默认/决策角色,**无状态列**);`README` 表 0-1 已含本单行,不再重复添加 | 文档负责人 |
**回填纪律**:①回填只改"状态/默认"与相关参数,**不改已定结论**(若结论本身变了,走 `09` §9 的契约变更流程);②表 10-8 与 `08` 表 8-6 新增的结论列必须一致,出现分歧以表 10-8 为准并在 `08` 标注;③回填完成前,代码按默认口径写,不等待。
+144
View File
@@ -0,0 +1,144 @@
# 应用端(值班员端)设计文档索引
本目录是"应用端 = 值班员端 = 值班台"的功能边界梳理与开发设计文档集合,补齐 `docs/公寓端/`(00~12)与 `docs/平台端/`(01~10)之外的第三端。
唯一上位依据:`docs/10-架构优化方案-三端拆分.md`(§3.3 应用端负责/不负责、表 3-1 应用端与公寓端分工、§2.5 表 2-3 路由与菜单、§4.3 表 4-3 `apt.duty.*`)。
对端依据:`docs/公寓端/`(`01`/`03`/`04`/`05`/`ui`)、`docs/平台端/`(`01`/`05`)、`docs/设计变更/01-本地建单与本地调整.md`(CR-01 v2)。
现状依据:`YktVue/src/views/JcXx.vue`(5294 行,唯一真实值班台实现)、`YktVue/src/api/jcgl/jcgl.js`(408 行)、`docs/frontend/04`、`docs/frontend/05`。
**本轮只产出设计文档**:不写业务代码、不建表改表、不执行 DDL/DML;`docs/` 下既有文件、`yktgl/`、`YktVue/`、`apartment/` 三处代码目录一律不改动,不执行 `git commit` / `git push`。
> **v2 修订(CR-02,2026-11-09)**:权威增量见 `docs/设计变更/03-三端功能设计修订(CR-02).md`。本目录变更:`01` §0(功能域 9→13,含换乘 / 平板结果 / 本地充值登记 / 排班考核;§5「不做任何账务动作」撤销)、`03` §0(自有 3→5 张表:`duty_roster` / `duty_performance`)、`04` §0(N-06~N-14 接口、5 类 WS 消息、错误码统一)、`05` §0(F-B / F-C / F-D / F-G 流程与断网能力补充)。
> 口径同步:`duty_shift_handover` 仍按"后续"不启用;本端仍无业务状态直写权限;公寓本地库业务表统一 **59 张**(CR-02 §10 补遗新增 `apt_sip_reg_status`);跨端 20 接口编号不新增。表 0-3 结论 10「本端不新增跨端接口编号」继续成立。
> **补遗(`docs/公寓端/00` v1.1)**:SIP 分机离线告警复用 `/ws/duty` `device.alarm` 与 `alarm.cleared`(表 4-8),**不新增 WS 消息名**;值班台只做查看与处置,不写 `apt_sip_endpoint` / `apt_sip_reg_status` / `apt_device_alarm`。
> **补遗(CR-02 §12 / `docs/公寓端/10`)**:人员注册由**签到终端**发起申请并上传人脸,值班台 `04` N-08 收敛为 N-35 待审列表 + N-36 确认 / N-37 驳回两步(`duty:person:reg` + 二次确认);值班员确认后生成 `ryid`、人脸进入公寓服务系统,再分别上传平台与下发公寓内设备。平台回执不再是注册生效前置,`40914` 改判为"注册申请未获值班员确认";值班台仍无业务状态直写权限。
> **补遗(CR-02 §13 / `docs/公寓端/11`)**:人员入住作业流程中值班台承担申请审核与房间调整(N-42~N-46,`duty:checkin:*`),审核通过由公寓端 `apt-stay` 办理入住;"已入住"为流程态 + 业务事实双重表达,本端不新增 `gy_jcxx` 状态字段、无业务状态直写权限。
> **补遗(CR-02 §14 / `docs/公寓端/12`)**:消费、充值与菜品管理中值班台承担线下现金充值核对与作废(N-53~N-55,`duty:ledger:*`),公寓管理员承担阶段性食谱发布 / 撤回(N-57),二者**只提交命令、零余额表写权限**;仅线下充值、扫码非支付,本端不提供线上支付与回调入口;远程点菜仅预留、本期恒 `40940 FEATURE_DISABLED`。不新增表、不新增跨端编号。
> **补遗(CR-02 §15 / 三端一致性同步)**:公寓端已完成的**平板叫乘、人脸签到、消费、菜品管理**四项能力在本端统一落地业务规则、交互流程、权限控制与异常处理;三端状态映射与数据同步机制以 **CR-02 §15 表 3-66 为唯一权威**,本端投影见 `03` §0 表 C-3、接口字段与错误码见 `04` §0 表 C-3 / C-4、作业时序与异常处置见 `05` §10。平台新增 `docs/平台端/10`「公寓统计分析与排名报表」,本端按 Q-50 **只读到公寓级分类统计与排名**,不触发 `S-03` / `S-06`、不写 `pl_apt_stat_*`、不开放跨公寓明细下钻;统计结果不回写任何业务状态。本端仍只写 `duty_*` 操作域,错误码"只消费不自造",跨端 20 编号与公寓 59 张表口径不变。变更点与影响范围见 CR-02 §15 表 3-63。
## 0. 文档清单
表 0-1 本目录文件与主题
| 文件 | 主题 | 与其他两端的衔接 |
| --- | --- | --- |
| `README.md` | 索引、编号约定、一页结论、三端速查、与两端编号对应 | 对照 `docs/公寓端/README` 表 0-1、`docs/平台端/README` |
| `01-功能边界.md` | 一句话边界、负责/不负责、三端五维分界、9 个功能域、既有操作矩阵映射、本期不做与置灰、待决策影响 | 沿用 `docs/公寓端/01` 表 1-3,不改写 |
| `02-技术基线与工程结构.md` | 技术基线、`apt-duty` 模块结构、依赖白名单、`entry-duty` 前端边界、动态路由、拦截器链、权限点 | 对照 `docs/公寓端/02` 表 2-2/表 2-4/表 2-5 |
| `03-数据设计.md` | `duty_*` 三表逐列、`apt_local_task` 归属、只读依赖表、可写字段白名单、时间派生、`gyid` 隔离、缺口;§0 表 C-3 **本端视角三端状态映射** | 对照 `docs/公寓端/03` 表 3-7/表 3-9/表 3-10;映射权威 CR-02 §15 表 3-66 |
| `04-接口设计.md` | 通用约定与错误码、`/duty/**` 分域接口清单、命令矩阵、`/ws/duty` 协议、与 `/apt/**` 对应关系;§0 表 C-3 四能力接口字段、表 C-4 错误码登记 | 对照 `docs/公寓端/04` 表 4-4/表 4-9、CR-02 §15 表 3-65 |
| `05-关键流程.md` | 盯控主链路、人工干预链路、要素指定与时刻调整、告警与 TTS、临叫与降级、断网、交接班;**§10 三端一致性作业视图**(交互时序、权限、异常、映射指针) | 对照 `docs/公寓端/05` 表 5-7、CR-01 图 1-1、`docs/平台端/10` |
| `06-部署与配置.md` | 与公寓端同部署单元、Nginx `/duty`、环境变量、工作站要求、容量与并发、同版本发布 | 对照 `docs/公寓端/06` |
| `07-实施计划与验收.md` | 挂载点(不新造阶段)、子批次 D-1~D-3、页面交付顺序、验收标准、测试矩阵、映射总表 | 对照 `docs/公寓端/07` 表 7-2/表 7-5 |
| `08-待决策与风险.md` | 决策点、新增 `Q-16`/`Q-17`、风险清单、与 `docs/10` 的差异、缺口登记、决策×批次矩阵 | 对照 `docs/公寓端/08` 表 8-1/表 8-10 |
| `09-公寓端内部契约.md` | 进程内服务契约(**不是**跨端 20 接口)、方法签名、事件订阅、冻结清单 `F-D01~F-D10` | 区别于 `docs/公寓端/09-平台接口约定.md` |
| `10-决策单-Q15至Q19.md` | 过会用决策单:`Q-15`~`Q-19` 五项的背景、可选项、各方影响、默认建议、拍板人、阻塞批次、决议记录与回填清单 | 是 `08` 表 8-1/表 8-6/表 8-7 的**执行版**;结论回填 `08`,不改其对端口径 |
## 1. 编号与引用约定
1. **本文档编号独立**:本目录内图、表统一用 `图 x-y` / `表 x-y`(`x` 为文件序号,`y` 为该文件内序号),README 用 `0`。例如 `表 4-9` 指 `04-接口设计.md` 的第 9 张表。
2. **引用其他文档必须带文档名前缀**:`docs/10 表 3-1`、`docs/公寓端/03` 表 3-10、`docs/平台端/01` 表 1-1、CR-01 表 1-7。避免与本文档编号混淆。
3. **表名一律小写**:与 `apartment/db/mysql/*.sql` 实际写法一致。`docs/10` 原文的大写形式(`DUTY_OPERATION`、`DUTY_*`)首次出现时以"`DUTY_OPERATION_LOG`(`duty_operation_log`)"给对照,后文只用小写。
4. **命令名一律大写**:`STOP`、`RE_CALL`、`FORCE_FINISH`、`MARK_LEAVE`、`CANCEL`、`HANGUP`、`ASSIGN_ELEMENT`、`ADJUST_TIME`、`LOCK_MATCH`、`UNLOCK_MATCH`。
5. **不出现真实口令、内网地址、Asterisk 凭据、完整手机号**:配置只写变量名与占位默认值。
6. **现状代码引用格式**:`JcXx.vue:2742`(可点击路径见 `docs/frontend/04`),本文档只引用方法名与 `docs/frontend/*` 已核实的结论,不重复贴代码。
## 2. 与 `docs/10` 的全局对应
表 0-2 本文档 → `docs/10` 追溯表
| 本文档 | 引用的 `docs/10` 小节 / 表号 | 用途 |
| --- | --- | --- |
| `01` §1~§2 | §3.3(应用端负责/不负责);表 3-1(逻辑分工) | 逐条落到 `apt-duty` 与 `/duty/**` |
| `01` §3 | 表 2-3(三端路由与鉴权);§3.4 | 五维分界与账号体系 |
| `01` §4 | §4.3 表 4-3(`apt.duty.view` / `apt.duty.state` / `apt.duty.command`) | 9 个功能域与模块落点 |
| `01` §6 | 表 10-2(`D6`/`D7`/`D8`/`D11`/`D15`/`D16`) | 未定决策的默认执行口径 |
| `02` §1~§3 | §2.4 表 2-2;§2.5 图 2-3、表 2-3;§4.4 表 4-4 | 版本锁定、模块依赖、单向依赖 |
| `02` §5 | §2.5 表 2-3(菜单与权限点行) | `sys_menu` 动态路由与 `duty:*` |
| `03` §1~§4 | §5.1 表 5-1;§5.3 第 2/5/6 条;§5.4 表 5-4 | 表归属、命名分区、`gyid` 隔离、状态单入口 |
| `03` §5~§6 | §5.3 第 6 条;§7.9 表 7-5 | 时间派生与状态迁移 |
| `04` 全部 | §6.1 表 6-1(跨端编号不涉及本端);§6.2 | 本端只做进程内调用,不新增跨端编号 |
| `05` 全部 | §7.4(两级告警输出);§7.7~§7.9;§7.11 | 告警、状态机、自检 |
| `06` 全部 | §2.5 表 2-3;§8.1 表 8-1;§8.2 表 8-2 | 同部署单元、同版本、审计保留期 |
| `07` 全部 | §9 表 9-1~9-6;§11.1 P1-3;§11.2 表 11-1 | 阶段挂载与优先级 |
| `08` 全部 | §10.1 表 10-1;§10.2 表 10-2;§10.3 表 10-3 | 风险与决策编号沿用 |
| `09` 全部 | §5.3 第 6 条;§4.3 表 4-3 | 进程内契约的冻结口径 |
## 3. 一页结论
表 0-3 本轮设计的 10 条结论
| # | 结论 | 依据 |
| --- | --- | --- |
| 1 | 应用端**不是第三套服务**:与公寓端同 jar(`apt-admin`)、同库、同 JVM,只是一个 Maven 模块 `apt-duty` + 一个前端入口 `entry-duty` + 一个路由前缀 `/duty/**` | `docs/10` §2.5 表 2-3、表 8-1;`docs/公寓端/01` 表 1-3 |
| 2 | 一句话边界:**应用端管"人看与人工干预",但任何干预必须经公寓端领域服务落地,自己没有写权限后门** | `docs/10` §3.4 结尾、表 3-1 写权限行 |
| 3 | 状态字段(`ryzt`/`finish_jc`/`finish_ly`/`stoped`)只有 `apt.duty.state` 一个写入口;值班台只提交命令,`duty_manual_action` 用 `action_id` 做幂等 | `docs/10` §5.3 第 6 条;`docs/公寓端/03` 表 3-9;DDL `uk_manual_action` |
| 4 | 值班台可用 **8 个命令** = 6 个状态命令(`STOP`/`RE_CALL`/`FORCE_FINISH`/`MARK_LEAVE`/`CANCEL`/`HANGUP`)+ 2 个要素命令(`ASSIGN_ELEMENT`/`ADJUST_TIME`);`LOCK_MATCH`/`UNLOCK_MATCH` 归公寓管理员 | `docs/公寓端/04` 命令语义表、`docs/公寓端/05` 表 5-7;CR-01 表 1-7 |
| 5 | `jcsj` 是唯一可调基准,`fjsj = jcsj+10min`、`jcbjsj = jcsj+15min`、`ylysj` 按路径 A 不变 / 路径 B `= jcsj + lysjgz`;**禁止逐项覆写派生时刻** | CR-01 表 1-5、表 1-6;`JcglService.validateCallDutyTimeline` |
| 6 | 接通判据是 `ConfbridgeJoin` 而非 `Answer`;30 s 无入会判 `ryzt=2`、35 s 复叫、接听后 40 s 自动挂断;默认单 `Conf-01`,并发返回 `40908` | `docs/10` §7.7 第 2 条、表 7-5;`docs/公寓端/08` 表 8-4 |
| 7 | 平台断网 ≠ 值班台不可用:叫乘、复叫、报警、录音照常,只有上报进 outbox;Asterisk 不可用 ≠ 整页禁用,只置灰语音按钮并给出"串口广播 + 人工"替代路径 | `docs/公寓端/ui/README` §4 硬约束 1/2;`docs/公寓端/07` 表 7-5 第 10 项 |
| 8 | 前端路由走 `sys_menu` 动态生成(与 `entry-apt` 静态路由相反),`sys_menu` 同时是权限点载体;这是与 `docs/10` 表 2-3 的**已登记差异(差-7)**在本端的落法 | `docs/10` 表 2-3;`docs/公寓端/02` 表 2-5、`docs/公寓端/08` 差-7 |
| 9 | 页面骨架以现状 `JcXx.vue` 拆分,**不另起一套**:D1 叫乘盯控台 = 主表格 + 20 个操作入口 + 5 个定时器 的角色化重构;D1/D2 已出稿 | `docs/10` §1.1("应以此页为骨架做角色化拆分");`docs/公寓端/ui/README` 表 1.1 |
| 10 | 本端**不新增跨端接口编号**:所有"要素/时刻/任务"数据仍由公寓端经 `#7`/`#8` 与平台交换;值班台只在本地库内写 `duty_*` 与触发领域命令 | CR-01 §1.2 不变式 C;`docs/平台端/05` 表 5-1 |
## 4. 三端边界速查
表 0-4 五维分界(**沿用** `docs/公寓端/01` 表 1-3 与 `docs/平台端/01` 表 1-1 的口径,本端列不改写,只补"本端落点")
| 维度 | 平台端 | 公寓端 | 应用端(本端) | 本端落点 |
| --- | --- | --- | --- | --- |
| 数据归属 | 主数据真源、计划、账务主账、跨公寓报表 | 执行事实、设备与语音资源、本地流水、快照 | 无独立数据;只在同库写 `duty_*` 操作域 | `duty_operation_log` / `duty_manual_action` / `duty_shift_handover` |
| 写入口 | 通过下行接口写平台库 | 领域服务 + 状态机单入口 | 只发命令(`duty_manual_action`) | `apt.duty.command` → `apt.duty.state` |
| 部署形态 | 无状态多实例 + 平台主库 | 单实例同进程(`apt-admin`)+ 本地库 + Asterisk | 与公寓端同进程、同 jar、同库 | `apt-duty` 模块,**不单独扩容** |
| 路由前缀 | `/platform/v1/**` | `/apt/**`、`/open/**` | `/duty/**` | WebSocket 为 `/ws/duty` |
| 账号体系 | 平台账号(`gl_*` → `sys_*`) | 本地 RuoYi 账号 + `gyid` 扩展属性 | 复用公寓端账号,再叠加权限点 | `sys_user` + `gyid` 数据域锁定 |
## 5. 阅读顺序
```mermaid
flowchart LR
A["README 索引与结论"] --> B["01 功能边界"]
B --> C["02 技术基线与工程结构"]
C --> D["03 数据设计"]
D --> E["04 接口设计"]
E --> F["05 关键流程"]
F --> G["06 部署与配置"]
G --> H["07 实施计划与验收"]
H --> I["08 待决策与风险"]
E --> J["09 公寓端内部契约"]
I --> K["10 决策单 Q-15~Q-19"]
```
图 0-1 阅读路径
评审建议:先看 `01` 表 1-6 的 20 行操作矩阵(既有 → 目标态)确认"一个功能都不丢",再看 `03` §4 的可写字段白名单与 `04` §3 的命令矩阵确认"越界不可能发生",最后看 `07` 的挂载点确认"不新造阶段"。
## 6. 术语
沿用 `docs/10` 附录 A 表 A-1;本端补 5 个说法:
表 0-5 本端术语
| 术语 | 说明 |
| --- | --- |
| 应用端 | 与公寓端同代码同库、面向值班员的盯控与操作界面;**不是**独立服务或独立库(`docs/10` 附录 A) |
| 值班台 | 前端 `duty` 入口 + 后端 `/duty/**` 的合称;`docs/公寓端/README` §5 已定义,本端展开其页面与接口 |
| 操作域 | `duty_*` 三张表:记"谁在什么时候想做什么、结果如何、是否交接",不记业务状态终值 |
| 执行域 | `gy_jcxx` / `gy_bjxx` / `gy_rooms` 等,owner 是公寓端;本端**只读 + 提交命令** |
| 命令 | 值班员提交的受控意图(含 `commandId`),由 `apt.duty.state` 解释为状态迁移;不等同于"直接改字段" |
## 7. 与 `docs/公寓端/`、`docs/平台端/` 的编号对应
表 0-6 三端编号互查
| 本端编号 | 公寓端对应 | 平台端对应 | 关系 |
| --- | --- | --- | --- |
| `01` 表 1-3(五维分界) | `01` 表 1-3 | `01` 表 1-1 | **同一张表的三端视角**,本端不改写口径 |
| `03` 表 3-2~3-4(`duty_*`) | `03` 表 3-7 | `01` 表 1-1 表归属行 | **同一批表**,公寓端已登记,本端补列级说明 |
| `04` 表 4-4(命令矩阵) | `04` 命令语义表、`05` 表 5-7 | — | 本端补"哪个角色可发哪个命令" |
| `04` 表 4-8(要素命令) | CR-01 表 1-7、表 1-8 | CR-01 表 1-19 | 本端补权限点与错误码落点 |
| `05` 图 5-2(要素/时刻) | CR-01 图 1-1 | CR-01 §2.7.3 `D8'` | 本端只画"值班员这一段" |
| `08` 表 8-4(差异 `差-D1~差-D4`) | `08` 表 8-8(差-1~差-8) | `09` 表 9-3(P/PR) | 本端差异独立编号,**不改写**对端结论 |
| `09` 表 9-7(`F-D01~F-D10`) | `09` 表 9-18(`F-01~F-14`) | `05` 表 5-10 | 前者是**进程内**契约,后者是**跨端**契约,两者不互相替代 |
**不构成对应关系的部分**:平台端 `P-01~P-11`、`PR-01~PR-10` 属平台侧自决,本端不登记;公寓端 `G-01~G-10` 中影响本端的只有 `G-02`(人员本地停用)与 `G-05`(无 `state_updated_by`),本端在 `03` §7 与 `08` §5 承接,结论不改。
@@ -0,0 +1,636 @@
# 01 设计变更 CR-01(v2):公寓端本地建单与本地调整
> 追溯(上位):`docs/10` §9 表 9-1~表 9-6(阶段划分)、表 6-1(跨端接口)、表 10-2(`D1~D16`)、表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行(平台不回调公寓)。
> 追溯(对端):`docs/公寓端/01` 表 1-3/表 1-4、`03` 表 3-1~表 3-16、`04` 表 4-1/表 4-4/表 4-9/表 4-10、`05` 表 5-1/表 5-7、`07` 表 7-2/表 7-5、`08` 表 8-1/表 8-9/表 8-10、`09` 表 9-8~表 9-18。
> 追溯(平台):`docs/平台端/01` 表 1-1/表 1-4、`03` 表 3-2/表 3-5/表 3-10/表 3-11/表 3-13、`04` 表 4-7、`05` 表 5-1/表 5-3/表 5-6、`06` §3/§4、`09` 表 9-1。
> 追溯(现状代码,本版新增):`yktgl/` 的 `jcgl` 模块——`JcglService.submit_rydj`(入寓登记建行)、`populateJcXxByJlXx`(交路模板派生)、`matchCallDutyUnderLock`(运安匹配)、`modifyJcXxBySynJcXx`(运安要素派生)、`modifyJcXx`(人工改时刻)、`validateCallDutyTimeline`(时刻先后校验);`JcglMapper.xml` 的 `insertJcxx`/`resetJcXxSql`/`resetJllxByJcid`;`JcglController` 的 `/jcgl/modifyJcXx`、`/jcgl/resetJllx`、`/jcgl/synCallDutyInfo`、`/jcgl/getJcXxFromGy`。
> 追溯(现状文档):`docs/backend/05-数据模型` §2.1(`GY_JCXX` 列)、§2.5(`GYGL_JLXX` 派生)、§3.1(`GY_SYN_JCXX` 运安快照)、§6(`GY_SJGZ` 时间规则)、§9(状态字典);`docs/backend/06-接口清单` 的 `/modifyJcXx`、`/resetJllx`、`/synCallDutyInfo`。
>
> **文档状态:CR 提议稿(`v2-cr-draft`),未评审、未冻结,不构成对既有设计文档的修改**。本轮只产出变更说明与修订建议,不改任何既有文档、不动 `yktgl/`、`YktVue/`、`apartment/` 三个代码目录,不执行 `git commit` / `git push`。
> 路径约定:`docs/10`、`docs/05` 等为既有文档体系沿用的简写,当前实体位于 `docs/backend/10-…`、`docs/backend/05-…`;`docs/平台端/`、`docs/公寓端/`、`docs/开发计划/` 为实际路径。
> **v2 由来**:v1 建立在"jcxx 由平台下发、公寓只复算"的假设上。经业务澄清与代码核对,该假设不成立(见 §0)。v2 按真实链路重写全部关键结论,`表 1-3` 逐条列出被修订的条目与修订后口径。
## 0. 业务链路纠正(v1 → v2 的根本性修订)
### 0.1 真实链路
叫乘信息(jcxx,落地为 `GY_JCXX`)的**数据源头在公寓端**。真实顺序是:
> **公寓端本地写入 → 上报平台 → 平台按运安数据匹配补全要素 → 公寓端拉取 → 本地确定执行时刻**
表 1-1 五步链路与代码证据
| 步 | 动作 | 发生端 | 代码 / 表证据 | 方向 |
| --- | --- | --- | --- | --- |
| S1 | 人员办理入寓登记,**当场在本地库写入一条 `GY_JCXX` 行**(`ryid`/`rymc`/`fjh`/`sjrysj`/`jlid`/`cc`/`kcsj` 等) | 公寓端 | `JcglService.submit_rydj` → `jcglmapper.insertJcxx`(`JcglMapper.xml` 的 `insertJcxx`,35 列) | 本地,无跨端 |
| S2 | 把新增 / 变更的 jcxx **上报平台** | 公寓端 → 平台 | 上行 `#8`(现状对应旧接口 `/jcgl/getJcXxFromGy` 的**反向**替代,见 §8 冲突-H) | 公寓 → 平台 |
| S3 | 平台拿运安数据(`GY_SYN_JCXX` 迁入平台侧的运安计划快照)为该条 jcxx **匹配并补全 `kcsj`(开车时刻)与 `jcfs`(接车方式)** | 平台端 | `GY_SYN_JCXX`(`id`/`ryid`/`cc`/`kcsj`/`jcfs`/`jch`);现状匹配逻辑在公寓 `matchCallDutyUnderLock`,新架构迁 `platform-plan` | 平台内部 |
| S4 | 公寓端**主动拉取**补全结果(平台不回调公寓) | 公寓端 → 平台 | 下行 `#6`(区间计划)/ `#7`(变更增量);现状对应 `/jcgl/synCallDutyInfo` 整表快照 | 公寓 → 平台(拉) |
| S5 | 公寓端据补全后的要素,按**本地时间规则**确定执行时刻:`jcsj` 及其派生 `fjsj`/`jcbjsj`/`ylysj` | 公寓端 | `modifyJcXxBySynJcXx`(运安路径)、`populateJcXxByJlXx`(交路模板路径)、`GY_SJGZ.lysjgz/zjjcsjgz/kjjcsjgz` | 本地,无跨端 |
**两条派生路径(现状代码,必须都承认)**:
- **路径 A · 交路模板**(`populateJcXxByJlXx`):入住时选了本公寓固定交路 → 直接用 `GYGL_JLXX` 的 `kcsj`/`jcsj`/`lysj` 拼开车日期;`jllx` 置 `'1'`;**不参与运安匹配**。
- **路径 B · 运安匹配**(`modifyJcXxBySynJcXx`):运安给 `kcsj` + `jcfs` → 公寓按 `GY_SJGZ` 时间规则的 `zjjcsjgz`(站接)/ `kjjcsjgz`(库出)反推 `jcsj`。
**结论**:平台**从不产生 `jcsj`**,只产生"要素"(`kcsj`/`jcfs`);`jcsj` 永远由公寓端算。这与 v1 的"平台下发计划、公寓复算"表述接近,但**"计划里含 `jcsj`"是错的**,且**jcxx 行的产生时机在入住登记、不在计划下发**。
### 0.2 v1 被推翻的三条假设
表 1-2 v1 假设与纠正
| # | v1 的假设 | 实际情况(代码证据) | 影响 |
| --- | --- | --- | --- |
| 1 | "`gy_jcxx` 行的唯一生成路径是'平台计划 → 复算'" | 行由**入寓登记**当场插入(`submit_rydj` → `insertJcxx`);计划只提供 `kcsj`/`jcfs` 要素 | 诉求 1 的"新建"不是"凭空造行",而是"给已有行补要素并锁定" |
| 2 | "本地建单 = 插一条新任务行" | 已入寓人员**本来就有行**,只是 `kcsj` 为空(未选交路且运安未匹配到)→ 没有 `jcsj` → 排不出叫乘任务 | 缺口从"建行"改为"补要素 + 防被清空" |
| 3 | "`jcsj` 由交路模板按规则复算,`lysj` 不随动" | 路径 B 下 `jcsj = kcsj − 偏移(jcfs)`,`ylysj = jcsj + lysjgz`(**随动**);路径 A 下 `jcsj`/`lysj` 都来自模板且都按跨零点减 1 天 | 时刻口径必须按两条路径分别写,不能只写一条 |
### 0.3 需要修订的条目与修订后口径(逐条)
表 1-3 v1 → v2 修订对照(本节即"列出需修订的条目 + 修订后口径"的正文,后续各节是修订后口径的完整展开)
> 注:「v1 位置」列中的表号是 **v1 文档**的编号,与本文 v2 表号不同源,勿按 v2 表号回查。
| # | v1 位置 | v1 口径(须废止) | v2 修订后口径 | 依据 |
| --- | --- | --- | --- | --- |
| 1 | §0 问题陈述 | 运安计划是叫乘任务唯一上游;`gy_jcxx` 唯一生成路径是"计划 → 复算" | jcxx 由入寓登记在公寓产生;运安计划只是**要素源**(`kcsj`/`jcfs`);`jcsj` 恒由公寓按本地时间规则算 | `表 1-1` S1/S3/S5 |
| 2 | §0 缺口 1 | "无 `POST`,无法新建任务" | 行已存在,缺口是"**本地指定要素并锁定不被运安清空**"的入口 | `resetJcXxSql`(未匹配时清空 `kcsj/jcsj/fjsj/jcbjsj/ylysj`) |
| 3 | §0 缺口 2 | "`correct` 白名单不含 `jcsj`,无调整入口" | 旧系统 `POST /jcgl/modifyJcXx` **已能改时刻并重建任务**;新架构 `apt` 侧接口未承接它 | `JcglController` L200-205、`JcglService.modifyJcXx` L956-1009 |
| 4 | §1.1 一句话范围 | "补上本地建单与本地调整" | "把'本地指定/调整叫乘要素'纳入**平台匹配核验闭环**,并保证不被运安匹配清空、断网可用" | 用户业务澄清 |
| 5 | §2.2(v1 表 1-2,派生) | `jcsj` 由模板复算;`kcsj`/`lysj` 不随动;`fjsj=jcsj+10`;`jcbjsj=jcsj+15` | **分两条路径**(表 1-5):A 路径 `jcsj`/`lysj` 来自模板;B 路径 `jcsj = kcsj − 偏移(jcfs)`、`ylysj = jcsj + lysjgz`;两路径 `fjsj=jcsj+10`、`jcbjsj=jcsj+15` | `populateJcXxByJlXx` L852-878、`modifyJcXxBySynJcXx` L2699-2740 |
| 6 | §2.2 校验规则 | `jcsj < kcsj` 且 `|kcsj−jcsj| ≤ 24h` | 沿用既有 `validateCallDutyTimeline`:`jcsj ≤ fjsj ≤ jcbjsj < kcsj`;跨零点由派生规则处理,不设 24h 人为上限 | `JcglService.validateCallDutyTimeline` L3673 |
| 7 | §2.4 `gy_syn_jcxx` 定性 | "平台下发的计划**副本**" | "**运安计划快照(匹配要素表)**"——下行写入方仍唯一,但它是**匹配输入**,不是任务生成源;本地建单**不写它**(结论不变,理由更新) | `docs/backend/05` §3.1、`ByxxResourceService` |
| 8 | §2.5 上报载体 | "`#8` 语义只覆盖复算,需扩展" | `#8` 本就是"公寓产生的叫乘结果上行";扩展为"**叫乘要素登记 / 变更**",并承接旧 `getJcXxFromGy` 被反转后的通道 | `JcglController` `/getJcXxFromGy`(原为中心推给公寓,违反方向) |
| 9 | §2.5 `taskId` 键空间 | 平台任务 `taskId=plan_key`;本地建单 `"L"+ULID` | **`taskId` 一律由公寓分配**(取 `jcid` 的业务编码);`plan_key` 降级为 `matchedPlanKey`(**匹配键,非主键**) | `表 1-1` S1(行由公寓产生) |
| 10 | §2.6 `PL_TASK.source` | 双来源 `PLATFORM`/`APARTMENT` | 任务**恒为公寓来源**;改记 `matchState`(`MATCHED`/`UNMATCHED`/`LOCKED`/`LOCAL_ONLY`)与 `origin`(`CHECKIN_ROUTE`/`CHECKIN_PENDING`/`LOCAL_ASSIGN`/`LOCAL_ADJUST`) | 同上 |
| 11 | §2.6 `PL_TASK_CHANGE` | "**不回写**(防回环)" | **反转**:平台**匹配补全结果必须回写** `PL_TASK_CHANGE`(这是平台新产生的事实);仅禁止"把公寓上报的要素原样回写" | `表 1-1` S3 |
| 12 | §2.7 `D8'` | "平台下发'计划+规则版本',公寓复算,默认平台为准 + 两类例外" | "平台下发'运安要素(`kcsj`/`jcfs`)+ 时间规则版本',公寓算 `jcsj` 并回执;本地指定/调整以 `jllx=1` 锁定并上报;平台核验结论经 `#7` 响应体回传;差异默认以平台匹配为准,锁定任务例外" | 全文见 §2.7.3 |
| 13 | §2.7 锁定位 | 新增 `local_locked` 列 | **不新增**:复用既有 `GY_JCXX.jllx`(`0` 可参与匹配 / `1` 已由本地确定),解锁沿用 `/jcgl/resetJllx` 语义 | `insertJcxx` `jllx='1'`、`resetJllxByJcid`、`matchCallDutyUnderLock` 只查 `jllx='0'` |
| 14 | §2.9 跨零点 | "以 `kcsj` 归日" | `plan_date` 以 `kcsj` 归日(不变);**派生时刻按 `jcsj`/`lysj` 时刻晚于 `kcsj` 则减 1 天**(既有实现) | `populateJcXxByJlXx` L858-866 |
| 15 | §2.10 指标分组 | `PLATFORM`/`PLATFORM+ADJUSTED`/`APARTMENT` | 按 `matchState` 分组:`MATCHED`/`UNMATCHED_LOCAL`/`LOCKED` + 合计 | 与 #10 一致 |
| 16 | §3(v1 表 1-10) | `apt_local_task` 含 `local_locked` | 删除该列;锁定状态**只以 `gy_jcxx.jllx` 为准**,侧表仅存 `jllx_at_override` 留痕 | #13 |
| 17 | §4(v1 表 1-13) | 未列 `jllx` 语义、`GY_SJGZ` 时间规则、`getJcXxFromGy` 下线、匹配职责迁移 | 新增 4 行(表 1-20 第 47~50 行) | 表 1-1 |
| 18 | §6 `D17` | "叫乘任务的双来源与 `taskId` 键空间" | 改题:**"叫乘任务主键与匹配键的分离"**(`taskId` 由公寓分配、`planKey` 只作匹配键) | #9 |
## 1. 变更范围与三条不变式
### 1.1 一句话范围
> 在**不新增跨端接口、不改变方向纪律、不把人员主数据变成可写**的前提下,让公寓端能对**已入寓人员的叫乘要素**做本地指定与本地调整,并保证这批要素:①不被运安匹配流程清空;②经 `#8` 上报平台做匹配核验与登记;③平台核验结论经 `#7` 响应体回传;④断网时本地仍可先行生效。
### 1.2 三条不变式
1. **不变式 A:人员档案仍是平台真源,本地只读。** 本地建单/调整的对象是"叫乘要素与时刻"(`gy_jcxx` 的 `jlid`/`cc`/`jch`/`kcsj`/`jcfs`/`jcsj`/`fjsj`/`jcbjsj`/`ylysj` + `apt_local_task` 治理行),**不是** `apt_master_person` 的任何列。写人员快照仍返回 `40904 APT_SNAPSHOT_READONLY`。
2. **不变式 B:方向纪律不变。** 只有"公寓发起、平台应答";平台核验结论只写在 `#7` 响应体里(与 `replayHint` 同级),平台侧不新增任何出站通道。**旧接口 `/jcgl/getJcXxFromGy`(中心把 data 推给公寓)必须下线**,其能力由公寓主动上报 `#8` 承接。
3. **不变式 C:跨端接口总数仍为 20(下行 8 / 上行 8 / 查询补偿 4)。** 本 CR 只扩展 `#8` 的 payload 与 `#7` 的响应体,不增编号、不重排。
## 2. 十个设计问题的逐条结论
### 2.1 Q1 操作对象边界
**结论:可写的是"叫乘要素与时刻";人员、交路模板、运安快照都不可本地写。**
表 1-4 对象可写性与拒绝方式
| 对象 | 本地可写 | 依据与拒绝方式 |
| --- | --- | --- |
| `gy_jcxx`:`jcsj`/`fjsj`/`jcbjsj`/`ylysj` | **是** | 经受控命令 `ADJUST_TIME`,见 §2.3;派生时刻必须按表 1-6 重算,不可逐项填写 |
| `gy_jcxx`:`jlid`/`cc`/`jch`/`kcsj`/`cclx`(要素) | **是** | 经受控命令 `ASSIGN_ELEMENT`(本地指定要素);`jcsj` 由要素按规则重算 |
| `gy_jcxx`:`jllx`(锁定位) | **是** | `1` = 已由本地确定,不参与运安匹配;`0` = 可参与匹配。经受控命令 `LOCK_MATCH`/`UNLOCK_MATCH` |
| `apt_local_task`(本 CR 新增治理表) | **是** | 见 §3 表 1-17 |
| `apt_master_person`(人员快照) | **否** | `40904 APT_SNAPSHOT_READONLY` |
| `apt_master_route` / `gy_syn_jcxx`(交路模板 / 运安快照) | **否** | 唯一写入方为 `apt-master-client`;本地指定要素**不写快照**(理由见 §2.4) |
**"已入寓登记"的可执行判据**(沿用 §0 的真实链路,判据从"能否建行"改为"能否对行做要素操作"):
1. 存在 `gy_jcxx where gyid=? and ryid=? and finish_ly='0' and sjlysj is null`(有效在寓),否则 `40909 APT_PERSON_NOT_IN_HOUSE`;
2. `ryid` 在 `apt_master_person` 且 `deleted=0`,否则 `40401`(`data.reason=PERSON_NOT_IN_SNAPSHOT`);
3. `card_id`(工号)非空——**匹配键依赖它**(`GY_SYN_JCXX.ryid` 存的是工号,`GY_JCXX.ryid` 是本地人员 UUID,`JcglService` L1863-1865 已注明该映射),否则 `40912 APT_CARD_ID_MISSING`。
> 注意:**没有"为某人新建 `gy_jcxx` 行"这条路径**。行由入寓登记产生(`submit_rydj`),这是"入住事实",不是"叫乘配置"。若人员尚未入寓,任何"建单"诉求都应先走入住登记——这条边界必须在接口层用 `40909` 挡住。
### 2.2 Q2 时刻口径
**结论:按既有两条派生路径执行;`jcsj` 是唯一可调基准,`fjsj`/`jcbjsj`/`ylysj` 一律重算;`kcsj` 只能通过"指定要素"改变且需平台核验;不允许逐项覆写。**
表 1-5 两条派生路径(现状代码口径,不变)
| 路径 | 触发 | `kcsj` 来源 | `jcfs`(接车方式)来源 | `jcsj` | `fjsj` | `jcbjsj` | `ylysj` |
| --- | --- | --- | --- | --- | --- | --- | --- |
| **A 交路模板**(`populateJcXxByJlXx`) | 入住时选本公寓固定交路(`jlid`) | `GYGL_JLXX.kcsj` + 开车日期 | `GYGL_JLXX.cclx` | `GYGL_JLXX.jcsj` + 开车日期(晚于 `kcsj` 则减 1 天) | `jcsj + 10 min` | `jcsj + 15 min` | `GYGL_JLXX.lysj` + 开车日期(晚于 `kcsj` 则减 1 天) |
| **B 运安匹配**(`modifyJcXxBySynJcXx`) | 运安快照匹配到(按工号+车次+`kcsj`) | 运安 `kcsj` | 运安 `jcfs`(`库接`归一为`库出`) | `kcsj − 偏移`:`站接`取 `GY_SJGZ.zjjcsjgz`、`库出`取 `GY_SJGZ.kjjcsjgz` | `jcsj + 10 min` | `jcsj + 15 min` | `jcsj + GY_SJGZ.lysjgz` |
表 1-6 调整 `jcsj` 后的随动关系(按路径)
| 时刻 | 调整 `jcsj` 时 | 允许独立填写 | 说明 |
| --- | --- | --- | --- |
| `jcsj` | **变**(唯一可调基准) | 是 | 调整即 `local_version + 1` |
| `fjsj` | **重算 = `jcsj + 10 min`** | 否 | 两路径同 |
| `jcbjsj` | **重算 = `jcsj + 15 min`** | 否 | 两路径同 |
| `ylysj` | 路径 A:**不变**(来自模板);路径 B:**重算 = `jcsj + lysjgz`** | 否 | v1 笼统写"不变"是错的 |
| `kcsj` | **不变** | 只有 `ASSIGN_ELEMENT` 能改,且改后 `jcsj` 随之重算 | 改 `kcsj` 必须重跑派生 |
| `jlid`/`cc`/`jch` | **不变**(改它们属"换交路",走 `ASSIGN_ELEMENT` 重建) | — | 换交路 = 换要素集,不做局部改 |
**为什么不许逐项覆写**:`fjsj`/`jcbjsj`/`ylysj` 与 `jcsj` 是纯函数关系(`± 10/15/lysjgz` 分钟)。若允许逐项写,就出现"基准 + 派生"两套事实源,规则升级后无法判断哪版才对,也无法在 `#8` 里用一个 `computedCallTime` 说清结果。保持"单基准 + 纯函数派生",回执才构成**可复算的证据**。
**校验规则**(违反返回 `40910 APT_TIME_ILLEGAL`):沿用既有 `validateCallDutyTimeline`——`jcsj ≤ fjsj ≤ jcbjsj < kcsj`(`JcglService` L3673)。补充两条:`Δ=0` 视为幂等成功(不产生新版本);`ryzt ≥ 1` 或 `finish_jc='1'` 的任务拒绝调整(沿用 `09` 表 9-9 `UPSERT` 行"已开始执行只更新计划快照,不改执行状态"的精神)。
**派生规则变更后是否回溯**:**不回溯**。沿用平台端已冻结口径(`docs/平台端/03` §6.1 第 3 条"新规则不重算已下发任务的 `jcsj`")。因此 `apt_local_task` 必须记 `derived_rule_version`(= 计算派生时刻生效的 `GY_SJGZ` 规则版本),让"这一行按哪版规则算出"永远可答。
### 2.3 Q3 操作主体与权限
**结论:值班员可调整时刻与要素,管理员可锁定/解锁,平台管理员只做核验裁决;权限点新增 3 个。**
表 1-7 角色 × 动作矩阵
| 动作 | 值班员(`/duty/**`) | 公寓管理员(`/apt/**`) | 平台管理员(`/platform/v1/admin/**`) |
| --- | --- | --- | --- |
| 本地指定要素(`ASSIGN_ELEMENT`) | **允许**(现场最常见:临时改车次/开车时刻/接车方式) | 允许 | 不允许(平台不生成 jcxx) |
| 调整叫乘时刻(`ADJUST_TIME`) | **允许** | 允许 | 不允许 |
| 锁定 / 解锁(`LOCK_MATCH`/`UNLOCK_MATCH`) | 不允许 | **允许** | 不允许(只能申请覆盖,见 §2.7) |
| 核验裁决(`ACCEPT`/`REJECT`) | 不允许 | 不允许 | **允许**:`PATCH /tasks/{taskId}` |
| 改人员 / 交路快照 | 不允许 | 不允许 | 不允许(真源侧维护) |
表 1-8 权限点
| 权限点 | 用途 | 挂载位置 |
| --- | --- | --- |
| `apt:jcxx:assignElement` | 本地指定要素 | `/apt/jcxx/{jcid}/commands` 的 `ASSIGN_ELEMENT` 分支 |
| `apt:jcxx:adjustTime` | 调整时刻 | 同上 `ADJUST_TIME` 分支;值班台 `duty:jcxx:adjustTime`(或复用 `apt:jcxx:changeStatus`,见 `Q-15`) |
| `apt:jcxx:lockMatch` | 锁定/解锁 | 同上 `LOCK_MATCH`/`UNLOCK_MATCH` 分支 |
**二次确认与留痕**(不设审批流——公寓端断网必须可用,任何"等审批"都会退化为人工电话兜底,与阶段三"断网 2 小时闭环"冲突):
| 情形 | 确认级别 | 留痕 |
| --- | --- | --- |
| 指定要素(改 `jlid`/`cc`/`kcsj`/`jcfs`) | 二次确认 + `reason` 必填 | `apt_local_task` + `duty_manual_action`(`commandId` 幂等)+ `duty_operation_log` |
| 已锁定任务再次指定要素 | **禁止**,须先解锁(防止"锁了还改"的自相矛盾) | 返回 `40911 APT_MATCH_LOCKED` |
| 调整时刻 `|Δ| ≤ 30 min` 且新值在未来 | 一次确认 + `reason` 必填 | 同上 |
| 调整时刻 `|Δ| > 30 min` 或新值在过去 | 二次确认 | 同上 |
### 2.4 Q4 公寓端数据落点
**结论:`gy_jcxx` 承担执行事实(零结构变更,`jllx` 复用为锁定位);治理信息落新表 `apt_local_task`;`gy_syn_jcxx` 是运安快照,本地建单不写它。**
表 1-9 落点对照
| 需求要素 | 落点 | 处理方式 |
| --- | --- | --- |
| 锁定(本地值优先、不参与匹配) | **`gy_jcxx.jllx='1'`(复用既有)** | 零新增;解锁沿用 `/jcgl/resetJllx` 语义(`resetJllxByJcid` 置回 `'0'`) |
| `origin`(来源细分) | `apt_local_task.origin` | 新增侧列 |
| `local_version`(本地修订序号) | `apt_local_task.local_version` | 新增,同时充当 `#8` 的 `bizVersion` |
| `override_reason` / `override_by` / `override_at` | `apt_local_task` 三列 | 新增 |
| `jllx` 历史(锁定发生时点的值) | `apt_local_task.jllx_at_override` | 新增(侧表只留痕、不重复存权威值) |
| "本地占位计划"写 `gy_syn_jcxx` | **不写** | 见下 |
**为什么不在 `gy_jcxx` 上加 `local_version` 等列**:`gy_jcxx` 属 44 张存量表(`公寓端/03` 表 3-8),纪律是"本期不改存量表结构,缺口先登记、先兜底"(G-01~G-09 均如此)。`apt_local_task` 走 `apt_*` 新增分区,以 `08_local_task.sql` 增量脚本落地,与既有 23 张 `apt_*` 表同规格。
**为什么不写 `gy_syn_jcxx`**(理由与 v1 不同,需重写):它的定位是**运安计划快照(匹配要素表)**(`docs/backend/05` §3.1,列仅 `id`/`ryid`/`cc`/`kcsj`/`jcfs`/`jch`/`write_time`),不是"任务计划"。写入本地占位行会带来三个具体问题:① 它参与匹配(按工号+车次+`kcsj`),占位行伪造 `kcsj` 会让匹配结果失真;② 它是整表快照(`ByxxResourceService` 先 TRUNCATE 再写入),任何本地行都会被下次同步清掉;③ 它没有 `source_version`/`local_version`,无法参与版本校验。本地指定所需的要素直接落在 `gy_jcxx` 的既有列上(本就存在),无需中间态。
### 2.5 Q5 上报载体
**结论:复用 `#8`(`biz_type=TASK_ACK`),语义扩展为"叫乘要素登记 / 变更";不新开接口;平台核验结论经 `#7` 响应体回传。**
表 1-10 候选载体比较
| 候选 | 语义匹配度 | 幂等键可用性 | 结论 |
| --- | --- | --- | --- |
| **`#8` `/platform/v1/ack/tasks`** | **高**:已有 `computedCallTime`、`ruleVersion`、`accepted`、`diffMs` | `hash(gyId + bizType + bizId + bizVersion)`,`bizId`=`taskId`、`bizVersion`=`local_version` | **采用** |
| `#9` `/report/duty-results` | 低:payload 是 `events[]`(`CALL_START`/`ANSWERED`/`LEAVE`),语义是执行事件,无"要素"字段 | 事件级幂等键为 `jcid+ryzt+时间` 哈希 | **不采用**:会把"要素变更"塞进执行事件序列 |
| 新开上行接口(如 `/report/jcxx-register`) | 高 | 需重新定义 | **不采用**:违反"总数 20 不新增"(`平台端/05` §1 约束 2),且打破"上行 8 个"冻结计数 |
| 保留旧 `/jcgl/getJcXxFromGy`(中心推给公寓) | 方向相反 | — | **必须下线**:违反"平台不回调公寓"(`docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行);其能力由 `#8` 承接 |
表 1-11 `taskId` 键空间与三时间戳口径
| 项 | 口径 |
| --- | --- |
| `taskId` 分配方 | **公寓端**(因为 `gy_jcxx` 行由入寓登记产生) |
| `taskId` 取值 | `jcid` 的业务化编码;要求**全平台唯一且可追溯回 `jcid`**,不因换版/重发改变 |
| `matchedPlanKey` | 平台匹配到的运安计划键(`GY_SYN_JCXX.id` = `jobid|taskdicid`),**只作匹配键**,不作主键、不作幂等键 |
| `idempotencyKey` | `hash(gyId + "TASK_ACK" + taskId + bizVersion)`,`bizVersion = local_version`(首次上报为 1,每次要素/时刻变更 +1) |
| `occurredAt` | **业务决策时刻** = `apt_local_task.override_at`(要素/时刻被确认的那一刻)。**不是 `jcsj`**,也不是 `sjrysj` |
| `receivedAt` | 本地入库时刻(`gy_jcxx` / `apt_local_task` 写入时刻) |
| `serverReceivedAt` | 平台接收时刻,由平台生成、回执带回(`平台端/05` 表 5-4) |
**断网导致三时间戳拉大是正常的**:断网 2 小时内指定要素,`serverReceivedAt − occurredAt` 可达 2 小时以上。因此明确规定:**平台不得用 `occurredAt` 判日切或判迟到**,任务类不设"差值 ≤ 10 分钟"门槛(该门槛只适用于 `LEDGER`,见 `平台端/05` 表 5-4)。时间轴以 `occurredAt` 呈现、以 `serverReceivedAt` 归批。
**上报的必要性(回应"不得脱离平台匹配")**:`#8` 不是可选的对账附属品,它是**平台匹配的输入**——平台拿它去运安快照里找 `kcsj`/`jcfs`。所以"公寓先本地生效、事后再报"只在**断网**时成立;联网状态下 `#8` 必须在要素生效后的下一个上报周期内发出,否则平台侧该任务长期处于 `UNMATCHED`,管理端会亮灯。
### 2.6 Q6 平台侧落点
**结论:`PL_TASK` 接受公寓来源任务(`task_id` 由公寓给);`PL_TASK_CHANGE` **必须回写**(平台匹配结果);对账三维按"任务类"重新界定。**
表 1-12 平台侧语义变更
| 对象 | 变更 | 理由 |
| --- | --- | --- |
| `PL_TASK.task_id` | 改由**公寓分配**(= `jcid` 业务编码);平台不生成 | 行由入寓登记产生(表 1-1 S1) |
| `PL_TASK.plan_key` → `matched_plan_key` | **降级为匹配键**,可空 | 运安未覆盖的人群**永远没有** `plan_key`,不能拿它当主键,否则这批人无处落地 |
| `PL_TASK.source` | **删除**;改 `match_state` + `origin` | 任务恒为公寓来源,"双来源"是伪维度 |
| `PL_TASK.jcsj` | **不再由平台产生**;仅保留 `computed_call_time`(公寓回执值) | `jcsj` 恒由公寓算(表 1-5) |
| `PL_TASK.kcsj` / `jcfs` | 新增/保留,存**平台匹配补全后**的要素 | 这是平台唯一的产出 |
| `PL_TASK_CHANGE` | **回写**:`op=UPSERT`,`version+1`,来源 = 平台匹配 | v1 的"不回写"是错的:匹配补全是**平台新产生的事实**,不回写公寓就拉不到 |
| `PL_TASK_CHANGE` 的禁区 | 仍**禁止**把公寓上报的要素原样回写 | 这才是真正的回环(`#7` 下发 → 公寓应用 → 又上报 → 又下发) |
| `PL_TASK.status` 六态 | **不变**;公寓来源任务初态取 `ACKED` | 少一个状态值就少一处"两套状态机"隐患 |
**匹配与核验的唯一闭环**:平台收到 `#8` → 按 `card_id` + 日期 + 车次查运安快照 → 命中则补 `kcsj`/`jcfs` 并写 `PL_TASK_CHANGE`(`op=UPSERT`)→ 公寓下次拉 `#7` 得到增量 → 公寓按规则重算 `jcsj` 并回执 `#8`(`computedCallTime`/`diffMs`);未命中则标 `match_state=UNMATCHED`,**保留公寓上报的要素**(不清空),并在 `#7` 响应体给 `matchVerdict`。
**对账三维如何覆盖**:
| 维度 | 既有口径(`平台端/05` 表 5-6 / `公寓端/09` 表 9-15) | 本 CR 的覆盖方式 | 为什么必须这样改 |
| --- | --- | --- | --- |
| `count` | `PL_RECEIVE_LOG` 当日去重后条数 vs 公寓 `count` | **按 `biz_id`(= `taskId`)去重后比对**;公寓侧基数取 `apt_local_task` 当日 `override_at` 命中的 `distinct task_id` | 每次变更 `bizVersion+1` 产生**新的** `idempotencyKey`,不去重则 `count` 永远大于公寓侧 |
| `sum(amount)` | `PL_LEDGER_FLOW.amount` 合计 | **不适用**(任务无金额);补助仍经 `#10` 独立对账 | 强行对任务类算 `sum(amount)` 会得到恒为 0 的假维度 |
| `max(watermark)` | `PL_WATERMARK.max_seen_watermark` vs `acked_watermark` | **完全沿用**(要素上报同样写 outbox、同样占水位) | 无需改动 |
| 建议第四维(仅任务类) | — | `max(local_version)` per `taskId` | 判"平台收到的最后一次变更属哪一版",`count`/`watermark` 都答不了 |
### 2.7 Q7 冲突仲裁与 D8 修订(最关键)
`D8` 原文:"叫乘时间以谁为准 → **平台下发'计划 + 规则版本',公寓端复算执行,不一致告警**"(`docs/10` 表 10-2)。按 §0 的真实链路,这句话**方向没错,但"计划"一词把 `jcsj` 也包进去了,导致"平台在管执行时刻"的误读**。必须显式修订,不静默绕过。
#### 2.7.1 两个版本号不可跨比较
| 版本号 | 谁产生 | 作用域 | 比较规则 |
| --- | --- | --- | --- |
| `match_version` | 平台(运安匹配结果版本,随 `PL_TASK_CHANGE.version` 递增) | 平台匹配结论 | 只与同 `taskId` 的其它 `match_version` 比大小 |
| `local_version` | 公寓(本地修订序号) | 本地要素/时刻版本 | 只与同 `taskId` 的其它 `local_version` 比大小,同时充当上报 `bizVersion` |
**禁止**用 `local_version` 与 `match_version` 比较得出"谁更新"——两者是不同坐标系(一个是"我改了第几版",一个是"平台核验到第几版")。这条写进契约(表 1-22 的 `F-14`)。
#### 2.7.2 匹配与锁定规则矩阵
表 1-13 平台匹配结果到达时的处理
| `jllx` | 平台匹配结果 | 处理 | 上报 / 留痕 |
| --- | --- | --- | --- |
| `0`(未锁,可匹配) | `MATCHED`(要素与本地不同) | **以平台要素为准**:覆盖 `kcsj`/`jcfs`(+ `cc`/`jch`),按表 1-6 重算 `jcsj`/`fjsj`/`jcbjsj`/`ylysj`,`local_version+1` | `#8`,`accepted=true`,`computedCallTime`=新值 |
| `0` | `MATCHED`(要素与本地相同) | 只刷新 `match_version`,不改时刻,`local_version` 不变 | 不产生 `#8`(无变化) |
| `0` | `UNMATCHED`(运安无此车次/此人) | **保留本地要素**,不执行 `resetJcXxForCurrentStay`;记录 `match_state=UNMATCHED` | `#8` 已在登记时发出,此处无需重发 |
| `1`(已锁) | `MATCHED`(要素不同) | **保留本地值**(D8' 例外②)。`kcsj`/`jcfs` 不动,只记 `match_version` 与 `base_matched_plan_key` 以便追溯 | `#8`,`accepted=false`,`reason=LOCAL_MATCH_LOCKED`;平台落 `PL_TASK_ACK.match_locked=1`,进管理端待办 |
| `1` | `UNMATCHED` | 保持本地值,`match_state=LOCKED` | 无需重报 |
| `0` | 平台管理员 `REJECT`(显式裁决) | 置 `stoped='1'`(经 `apt.duty.state`)+ `verdict_state=REJECTED` + 告警 | `#9` 自然产生状态事件 |
**"锁定"是否必要:必要,且已经存在。** 现场场景是最硬的论据:某客运班组临时指定了开车时刻与交路,若平台 5 分钟后用一条运安匹配把它顶回去,值班员会认为系统"自己乱改",进而退回手工电话叫乘。锁定的语义是"**我承担这个决定的后果,直到有人显式推翻我**",而不是"我比平台权威"。
**关键事实**:既有代码里 `jllx` **已经是**这个锁定位——`populateJcXxByJlXx` 固定交路入住后置 `'1'`,运安匹配只查 `jllx='0'` 的候选(`matchCallDutyUnderLock` L1842),`resetJllxByJcid` 可置回 `'0'` 重新进入匹配池。**v1 误以为要新造 `local_locked`,实际只需澄清 `jllx` 的语义并停止滥用它**(见 §8 冲突-F)。
#### 2.7.3 D8 修订建议(显式写出)
> **建议修订 D8 的口径**(`docs/10` 表 10-2 D8 行),修订后文字:
>
> **D8'**:叫乘时刻的**要素**(开车时刻 `kcsj`、接车方式 `jcfs`)以平台 + 运安匹配为基准,平台下发"运安要素 + 时间规则版本";**执行时刻 `jcsj` 及其派生(`fjsj`/`jcbjsj`/`ylysj`)一律由公寓端按本地时间规则计算**,公寓复算后回执 `computedCallTime`;要素差异超阈值告警。以下两类必须留痕、必须上报、必须进对账差异清单,不得静默覆盖:
> ① `jllx='1'` 的**本地锁定任务**(本地指定的交路/开车时刻/接车方式),本地要素优先,直至有人显式解锁后回归平台匹配值;
> ② **平台未匹配到运安计划**(`match_state=UNMATCHED`)的任务,保留本地要素,**不得因未匹配而清空既有叫乘任务**(现状 `resetJcXxForCurrentStay` 的清空行为须废止或加锁保护)。
>
> 两类例外的共同约束:`fjsj`/`jcbjsj`/`ylysj` 仍必须由 `jcsj` 按规则派生(不可独立覆写);派生所用 `rule_version` 必须留痕;规则升级不回溯既有任务。
**相比 v1 `D8'` 的三处修正**:① 把"平台下发计划"精确为"平台下发**运安要素**",明确 `jcsj` 不来自平台;② 锁定标志从"新字段 `local_locked`"改为"复用 `jllx`";③ 新增例外②(未匹配不清空),这是现场最痛的一条,v1 完全没提。
#### 2.7.4 状态与版本流转图
图 1-1 本地指定 / 本地调整的状态与版本流转
```mermaid
flowchart TB
subgraph APT["公寓端(本地库 · 单实例 · 断网可用)"]
direction TB
A0["A0 入住登记完成<br/>GY_JCXX 行已存在<br/>jllx 为 0 或 1"]
A1["A1 待匹配<br/>kcsj 为空<br/>无叫乘任务"]
A2["A2 要素齐备<br/>kcsj 与 jcfs 有值"]
A3["A3 叫乘任务已排<br/>jcsj 与派生时刻齐备"]
A4["A4 本地指定要素<br/>jllx 置 1<br/>local_version 加 1"]
A5["A5 已调整时刻<br/>local_version 加 1"]
A6["A6 回归平台要素<br/>local_version 加 1"]
A7["A7 已执行或终态"]
D1{"是否已有<br/>可用要素"}
D2{"jllx 是否为 1"}
D3{"消费 matchVerdict"}
end
subgraph PLAT["平台端(平台库 · 无状态多实例)"]
direction TB
P1["P1 PL_TASK<br/>task_id 来自公寓<br/>match_state 待定"]
P2["P2 运安计划快照<br/>按工号加日期加车次匹配"]
P3["P3 PL_TASK_CHANGE<br/>op 为 UPSERT<br/>version 加 1"]
P4["P4 七号接口响应体<br/>matchVerdict 数组"]
end
A0 -- "出站八号接口 · 要素登记" --> P1
P1 -- "按匹配键查运安" --> P2
P2 -- "补全 kcsj 与 jcfs" --> P1
P1 -- "增量落表" --> P3
P3 -- "变更记录" --> P4
A0 -- "判断要素" --> D1
D1 -- "已齐备" --> A2
D1 -- "为空" --> A1
A1 -- "本地指定要素" --> A4
A2 -- "拉取六号与七号接口" --> P3
A2 -- "拉取七号接口 · 响应体自带 matchVerdict" --> P4
A2 -- "解析本次响应体" --> D3
A2 -- "调整 jcsj · 重算派生" --> A5
A4 -- "出站八号接口 · bizVersion 取 local_version" --> P1
A5 -- "出站八号接口 · bizVersion 取 local_version" --> P1
P1 -- "匹配结果回传" --> D2
D2 -- "jllx 为 0" --> A6
D2 -- "jllx 为 1" --> A5
D3 -- "REJECTED" --> A7
A6 -- "按派生规则重算时刻" --> A3
A5 -- "按派生规则重算时刻" --> A3
A4 -- "按派生规则重算时刻" --> A3
A3 -- "到期叫乘" --> A7
```
**读图要点(评审按这四条检查)**:
1. 全部跨边界实线都是"公寓 → 平台"(`A0/A4/A5 → P1`、`A2 → P3`、`A2 → P4`),**不存在任何"平台 → 公寓"箭头**;
2. `P4 matchVerdict` 不是平台发起的推送,它是 `A2` 那一次 `GET #7` 的**响应体内容**,与 `replayHint` 同级(`平台端/06` §3 时序要点第 6 条"`replayHint` 是唯一'平台表达意愿'的通道");
3. `A3` 有三条入边(`A4`/`A5`/`A6`),说明**无论要素从哪来,执行路径只有一条**(`apt.duty.state` 单写入口,`公寓端/03` 表 3-9);
4. `D2` 的分叉(`jllx` 0 → `A6` 回归、1 → 保留)是 D8' 两个例外的图形化表达,不是两条并列业务流。
### 2.8 Q8 客运固定交路的两条路线
表 1-14 两条路线的适用条件、优缺点与推荐
| 维度 | 路线 A:平台在 `#3` 交路模板预置 | 路线 B:公寓本地"临时交路"(指定要素) |
| --- | --- | --- |
| 落地方式 | 平台 `platform-route` 维护 `GYGL_JLXX`(`jlid`+`cc`+`kcsj`+`jcsj`+`lysj`+`cclx`)→ `#3` 下发 → 公寓 `apt_master_route` 快照 → 入住时选 `jlid` → 走派生路径 A | 入住时不选 `jlid`,或选后用 `ASSIGN_ELEMENT` 直接填 `cc`/`kcsj`/`jcfs` → 走派生路径 B → `jllx='1'` 锁定 → `#8` 上报 |
| 适用条件 | 交路可枚举、排班稳定、需跨公寓一致口径(**客运固定交路的主渠道**) | 临时加开/临客、本公寓特有交路、平台来不及配置、断网期间 |
| 优点 | 真源唯一、版本化、可对账、`jlid` 可参与补助与统计 | 现场不被阻塞;零平台配置滞后 |
| 缺点 | 新交路依赖平台配置节奏;平台需维护大量客运交路 | 存在"影子交路",易漂移;需确认闭环;不参与 `#3` 版本体系 |
| 是否写 `apt_master_route` | 是(平台下发,唯一写入方 `apt-master-client`) | **否**——G-04 已明确该表无 `source` 列、本地新增会被全量覆盖(`公寓端/08` 表 8-9) |
| **推荐** | **主用**:阶段三试点前,把客运班组的固定交路批量预置进 `#3` | **应急**:仅用于 A 覆盖不到的情形,强制上报 + 平台设"采纳率"指标,避免长期漂移 |
**与 `D7` 的关系**:`D7` 原文"平台维护模板,公寓可'应急新增'并上报确认"(`docs/10` 表 10-2)**不需要改文字**,本 CR 只把它的落地口径写死:**"应急新增"的落点是"对既有 jcxx 指定要素并锁定",不是"往交路模板表插行"**。这个澄清必要——`公寓端/08` 表 8-1 的 `D7` 默认口径写的是"本期不做本地应急新增(受 G-04 约束)",若照此执行 `D7` 就等于"永远不做"。建议在 `D7` 行末补注:"应急新增落 `gy_jcxx` 既有列 + `apt_local_task` 侧表,不写交路快照表;平台确认后由 `platform-route` 版本化为正式模板。"
### 2.9 Q9 边界场景
表 1-15 九个边界场景的可执行处理
| 场景 | 处理规则 | 落点 / 校验 |
| --- | --- | --- |
| 临时加开 / 临客 | 走路线 B,`origin=LOCAL_TEMP_EXTRA`;`reason` 必填;`jllx='1'` | `apt_local_task.origin`;`gy_jcxx.jllx='1'` |
| **跨零点**(叫乘与开车不同日) | `plan_date` 以 `kcsj` 归日;派生时**若 `jcsj`/`lysj` 的时刻晚于 `kcsj` 的时刻,则整体减 1 天**(既有实现,`populateJcXxByJlXx` L858-866)。禁止按 `jcsj` 归日 | 派生函数内处理,不单独存 `plan_date` |
| 同一人员多车次 | `taskId`(= `jcid`)天然不同;允许并存 | 叫乘引擎按 `jcsj` 升序排队;排队冲突返回 `40908 APT_CONF_BUSY`(已有码) |
| 多公寓同一交路 | `jlid` 在 `(gyid, jlid)` 唯一(`公寓端/04` 表 4-5 校验 2);`idempotencyKey` 含 `gyId`,不跨公寓碰撞 | `公寓端/03` 表 3-12 四级隔离 |
| **断网 2 小时内指定要素 / 调整** | **完全允许**:只用本地库与本地 `GY_SJGZ` 规则快照,不依赖任何下行;`#8` 进 outbox(`PENDING`),恢复后按 `watermark asc` 续传 | 阶段三验收:恢复后 ≤ 10 分钟追平且对账零差异(`公寓端/07` 表 7-5 第 2 项) |
| **运安未匹配到该人/该车次** | **必须保留本地要素与叫乘任务**,不执行 `resetJcXxForCurrentStay`;标 `match_state=UNMATCHED` 并在值班台提示"未匹配运安" | 修订点:现状 `matchCallDutyUnderLock` 的 `unmatchedNum` 分支会清空(L1965-1991),须加 `jllx='1'` 保护 |
| 断网期间平台侧又产生了匹配结果 | 恢复后**先派 outbox 再拉 `#7`**(顺序不能颠倒:先让平台知道本地要素,才能给出正确匹配);交错以"后发生覆盖"为准,每次 `local_version+1` | `平台端/05` 表 5-3 |
| 重复投递 | 同 `idempotencyKey` 命中 `PL_RECEIVE_LOG` → `PL-IDEMPOTENT-REPLAY`,不重复执行业务副作用 | `平台端/05` 表 5-3 |
| 乱序(旧 `bizVersion` 后到) | 平台返 `PL-VERSION-STALE`,公寓置 outbox `DEAD` 且**不重试** | `公寓端/09` 表 9-4/表 9-14 |
| 指定要素后人员离寓 | 不改 `finish_ly`;离寓走既有 `CANCEL` 语义(`ryzt=0` → `stoped='1'`),本地与平台任务同规则 | 沿用 `公寓端/09` 表 9-9 `CANCEL` 行 |
### 2.10 Q10 指标口径
**结论:计入准点率分母,并按 `matchState` 分三组 + 合计。**
表 1-16 指标口径:分母、分组、阈值与报表落点
| 项 | 口径 |
| --- | --- |
| 准点率定义 | `onTimeRate = count(jcsj_s − jcsj ≤ 阈值) / count(当日到期任务)`,沿用 `公寓端/07` 表 7-5 第 3 项"对比 `jcsj_s − jcsj` 分布" |
| 分子/分母是否含本地指定 | **都含**,但分组统计 |
| 三个分组 | `MATCHED`(运安匹配到、未锁)/ `UNMATCHED_LOCAL`(运安未覆盖、本地要素)/ `LOCKED`(本地指定并锁定) |
| 为什么计入 | 若排除,会激励"把难叫的人标成本地要素"来刷指标(古德哈特定律);排除后指标失去对现场真实体验的度量能力 |
| 为什么分组 | 若只给合计,平台侧无法判断"准点率下降是运安匹配问题还是现场指定问题",也无法评估"采纳率"是否健康 |
| 阈值 | **文档中从未固定**(`公寓端/07` 表 7-5、`docs/10` 表 9-4 都只说"不低于现网")。建议冻结三档:`≤ 30 s` 准点 / `30~120 s` 迟到 / `> 120 s` 失败,作为 `Q-15` 挂出,**本 CR 不擅自定值** |
| 报表落点 | 优先走视图 `V_CALL_QUALITY`(`平台端/03` 表 3-8 已有),**不给 `PL_REPORT_DAILY` 加列**;若评审要求固化到日报,再加 `call_count_local`、`on_time_rate_local` 两列 |
## 3. 新增 / 变更字段清单
表 1-17 公寓端新增表 `apt_local_task`(`apt_*` 命名分区,随 `08_local_task.sql` 增量脚本落地)
| 字段 | 类型 | 语义 | 必填 | 默认值 |
| --- | --- | --- | --- | --- |
| `id` | bigint | 代理主键(仅本表内部,不跨端) | 是 | 自增 |
| `gyid` | varchar(32) | 公寓标识(隔离第一列,`公寓端/03` 表 3-13) | 是 | 取自 `AptContext` |
| `task_id` | varchar(32) | 任务业务键 = `jcid` 业务化编码(公寓分配) | 是 | — |
| `jcid` | varchar(32) | 关联 `gy_jcxx` 行标识 | 是 | — |
| `origin` | varchar(32) | `CHECKIN_ROUTE` / `CHECKIN_PENDING` / `LOCAL_ASSIGN` / `LOCAL_TEMP_EXTRA` / `LOCAL_ADJUST` | 是 | `CHECKIN_PENDING` |
| `local_version` | bigint | 本地修订序号,首次上报为 1,每次指定/调整/回归 +1;等于 `#8` 的 `bizVersion` | 是 | 1 |
| `matched_plan_key` | varchar(64) | 平台匹配到的运安计划键(`jobid|taskdicid`) | 否 | `null` |
| `match_state` | varchar(16) | `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED` | 是 | `PENDING` |
| `base_kcsj` | datetime(3) | 最近一次由平台补全的 `kcsj`(用于算 `diffMs`) | 否 | `null` |
| `base_jcfs` | varchar(16) | 最近一次由平台补全的 `jcfs` | 否 | `null` |
| `jllx_at_override` | tinyint | 变更发生时 `gy_jcxx.jllx` 的值(留痕,不重复存权威值) | 是 | 0 |
| `origin_jcsj` | datetime(3) | **首次**调整前的 `jcsj`(后续不覆盖,保留原值溯源) | 否 | `null` |
| `derived_rule_version` | varchar(32) | 计算派生时刻所用 `GY_SJGZ` 规则版本 | 否 | `null` |
| `override_reason` | varchar(512) | 指定/调整原因(1~512 字) | 是 | — |
| `override_by` | varchar(64) | 操作人(`sys_user.user_name`) | 是 | — |
| `override_at` | datetime(3) | 操作确认时刻,= 上行 `occurredAt` | 是 | — |
| `verdict_state` | varchar(16) | `PENDING_VERIFY` / `VERIFIED` / `OVERRIDDEN` / `REJECTED` | 是 | `PENDING_VERIFY` |
| `verdict_at` | datetime(3) | 平台裁决被消费的时刻 | 否 | `null` |
| `verdict_note` | varchar(512) | 平台裁决说明(`matchVerdict[].note`) | 否 | `null` |
| `create_time` / `update_time` | datetime(3) | 审计(`公寓端/03` 表 3-15 约定) | 是 | `current_timestamp(3)` |
约束:`UK(gyid, task_id)`、`UK(gyid, jcid)`、`IDX(gyid, match_state)`、`IDX(gyid, override_at)`。
**不存锁定字段**:锁定以 `gy_jcxx.jllx` 为准(表 1-3 #13)。
**不存上报状态**:是否已送达以 `apt_report_outbox` 为准(`公寓端/03` 表 3-7),本表不设 `report_state`,避免双写。
表 1-18 平台端表列变更(`PL_TASK` / `PL_TASK_ACK` 均为新增表,尚未建 DDL,改清单即改文档)
| 表 | 字段 | 类型 | 语义 | 必填 | 默认值 |
| --- | --- | --- | --- | --- | --- |
| `PL_TASK` | `task_id` | `id_ulid`(沿用) | **由公寓分配**(= `jcid` 业务化编码) | 是 | — |
| `PL_TASK` | `matched_plan_key` | `enum_str`(`varchar(64)`) | 运安计划键(`jobid|taskdicid`),**匹配键非主键** | 否 | `null` |
| `PL_TASK` | `match_state` | `enum_str`(`varchar(16)`) | `PENDING` / `MATCHED` / `UNMATCHED` / `LOCKED` / `REJECTED` | 是 | `PENDING` |
| `PL_TASK` | `origin` | `enum_str`(`varchar(32)`) | 同 `apt_local_task.origin` | 是 | `CHECKIN_PENDING` |
| `PL_TASK` | `kcsj` | `datetime(3)` | 平台匹配补全的开车时刻 | 否 | `null` |
| `PL_TASK` | `jcfs` | `enum_str`(`varchar(16)`) | 平台匹配补全的接车方式(`站接`/`库出`) | 否 | `null` |
| `PL_TASK` | `local_version` | `count_big` | 公寓上报的本地修订序号 | 是 | 0 |
| `PL_TASK_ACK` | `origin` / `local_version` / `match_locked` | 枚举 / `count_big` / `tinyint` | 回执来源、本地版本、回执时是否锁定 | 是 | — / — / 0 |
| `PL_TASK_ACK` | `adjust_delta_ms` | `count_big` | 本地 `jcsj` − 平台可推导基线(毫秒,可负) | 否 | `null` |
类型族按 `平台端/03` 表 3-16;**不改** `PL_TASK.status` 六态、**不改** `PL_TASK_CHANGE.op` 两值、**不动** `PL_RECEIVE_LOG`。**删除** v1 提出的 `PL_TASK.source`(见 §2.6)。
表 1-19 接口字段变更(只扩既有接口,不新增编号)
| 接口 | 位置 | 字段 | 类型 | 语义 | 必填 |
| --- | --- | --- | --- | --- | --- |
| `#8` `/platform/v1/ack/tasks` | `items[]` | `taskId` | string | = `jcid` 业务化编码 | 是 |
| `#8` | `items[]` | `origin` | string | 来源枚举(表 1-17) | 是 |
| `#8` | `items[]` | `localVersion` | long | 本地修订序号 | 是 |
| `#8` | `items[]` | `cardId` | string | 工号(**匹配键**,平台按它查运安快照) | 是 |
| `#8` | `items[]` | `cc` / `jch` | string | 车次 / 机车号(匹配键) | 否 |
| `#8` | `items[]` | `kcsj` | string | 公寓已知的开车时刻(空表示待平台补全) | 否 |
| `#8` | `items[]` | `jcfs` | string | 公寓已知的接车方式 | 否 |
| `#8` | `items[]` | `computedCallTime` | string | 公寓算出的 `jcsj`(沿用字段) | 是 |
| `#8` | `items[]` | `ruleVersion` / `diffMs` | string / long | 派生规则版本 / 与基线差(沿用字段) | 是 / 否 |
| `#8` | `items[]` | `accepted` | bool | 锁定拒绝平台覆盖时为 `false` | 是 |
| `#8` | `items[]` | `overrideReason` | string(≤512) | 指定/调整原因 | 指定/调整时必填 |
| `#7` `/platform/v1/tasks/changes` | 响应体(与 `replayHint` 同级) | `matchVerdict[]` | array | 平台对本地指定/调整的核验结论 | 否(无结论时缺省) |
| `#7` | `matchVerdict[]` | `taskId` | string | 目标任务 | 是 |
| `#7` | `matchVerdict[]` | `verdict` | string | `MATCHED` / `UNMATCHED` / `REJECTED` | 是 |
| `#7` | `matchVerdict[]` | `localVersion` | long | 结论针对的本地版本(防串版) | 是 |
| `#7` | `matchVerdict[]` | `matchedPlanKey` / `kcsj` / `jcfs` | string | 匹配到的运安键与要素 | 否 |
| `#7` | `matchVerdict[]` | `decidedAt` | string | 平台裁决时刻(ISO-8601 带偏移) | 是 |
**不改的接口**:`#6`、`#9`~`#14`、`#23`~`#25`、`#16`/`#17`(`P-02` 未定)。下行仍为 8 个。**新下线**:旧 `/jcgl/getJcXxFromGy`(见 `表 1-20` 第 50 行)。
## 4. 文档修订建议(逐篇 / 逐节 / 逐表)
表 1-20 修订清单(**本轮只建议,不修改**)
| # | 文档 | 位置 | 类型 | 变更内容 |
| --- | --- | --- | --- | --- |
| 1 | `docs/10` | 表 10-2 `D8` 行 | 修订 | 改为 §2.7.3 的 `D8'` 全文 |
| 2 | `docs/10` | 表 10-2 `D7` 行 | 补充 | 行末补"应急新增落 `gy_jcxx` 既有列 + `apt_local_task` 侧表,不写交路快照表" |
| 3 | `docs/10` | 表 10-2 | 新增 | 新增 `D17`:叫乘任务**主键与匹配键分离**(`taskId` 由公寓分配、`planKey` 只作匹配键) |
| 4 | `docs/10` | 表 6-1 `#8` 行 | 补充 | 用途补"含公寓产生的叫乘要素登记与变更" |
| 5 | `docs/10` | 表 6-1 `#7` 行 | 补充 | 响应体说明补 `matchVerdict[]` |
| 6 | `docs/10` | 表 9-3(阶段二产出行) | 补充 | 产出清单补"叫乘要素登记契约:匹配键、`matchState`、`matchVerdict`、`jllx` 锁定语义" |
| 7 | `docs/10` | 表 9-4(阶段三产出行) | 补充 | 验收补"断网 2 小时内可指定要素/调整,恢复后 ≤ 10 分钟追平" |
| 8 | `docs/10` | 表 9-6(阶段五产出行) | 补充 | 下线前置:旧"手工/临时叫乘"入口下线前,本地指定要素必须已覆盖等价能力 |
| 9 | `docs/平台端/03` | 表 3-5(计划与任务域) | 修订 | `PL_TASK` 删 `source`/`jcsj` 语义改、加 `matched_plan_key`/`match_state`/`kcsj`/`jcfs`/`local_version`;`PL_TASK_ACK` 加 4 列(表 1-18) |
| 10 | `docs/平台端/03` | 表 3-10(状态单写入口) | 补充 | `PL_TASK.status` 备注补"公寓来源任务自 `ACKED` 起,不新增状态值" |
| 11 | `docs/平台端/03` | 表 3-11(平台/公寓状态分工) | 补充 | 新增"任务来源与主键"一行:`taskId` 公寓给、`matchState` 平台给 |
| 12 | `docs/平台端/03` | 表 3-13(同步方向与版本语义) | 修订 | `#8` 行补"要素登记"语义;`#7` 行补 `matchVerdict`;新增"`match_version` 与 `local_version` 不可跨比较" |
| 13 | `docs/平台端/05` | 表 5-1(20 接口) | 补充 | `#8` 用途、`#7` 响应体说明(同 #4/#5) |
| 14 | `docs/平台端/05` | 表 5-3(幂等键) | 补充 | 补"`bizId`=`taskId`(公寓分配),`bizVersion`=`local_version`" |
| 15 | `docs/平台端/05` | 表 5-6(对账三维) | 修订 | `count` 行补"按 `biz_id` 去重后比对";新增"任务类不含 `sum(amount)`"注 |
| 16 | `docs/平台端/05` | 表 5-10(逐条核对) | 补充 | `#7`/`#8` 行结论补注 |
| 17 | `docs/平台端/06` | §3 时序要点 | 补充 | 新增"平台匹配补全结果**必须**回写 `PL_TASK_CHANGE`;公寓上报原值**不得**回写" |
| 18 | `docs/平台端/06` | §4 时序要点 + 图 6-3 | 补充 | `#8` 承接要素登记;图注补 `matchVerdict` 只在 `#7` 响应体 |
| 19 | `docs/平台端/01` | 表 1-1 末行、表 1-4 任务生成行 | 修订 | "谁决定叫乘时刻"改为"平台给**要素**(`kcsj`/`jcfs`)+ 规则版本,公寓算 `jcsj`";"任务生成"行补"任务行由公寓入寓登记产生" |
| 20 | `docs/平台端/09` | 表 9-1 | 补充 | 注明本议题裁决权在跨端(`D17`/`F-14`),**不单开 `P-12`**,理由见表 1-22 |
| 21 | `docs/公寓端/03` | 表 3-2(计划与任务域) | 修订 | 3 张 → 4 张,加 `apt_local_task`;**并补注 `gy_syn_jcxx` 是"匹配要素表",不是任务生成源** |
| 22 | `docs/公寓端/03` | 表 3-8(命名分区) | 修订 | `apt_*` 本期表数 23 → 24;44 → 45 张(连带 `docs/10` 表 5-9 第 1 项口径) |
| 23 | `docs/公寓端/03` | 表 3-15(字段约定) | 补充 | 版本列清单补 `local_version`(与 `source_version`/`biz_version` 语义不同,不可混用) |
| 24 | `docs/公寓端/03` | 表 3-16 | 新增 | 新增 `G-10`:`gy_jcxx` 缺 `local_version`/`origin`/`override_*`;**锁定语义已由 `jllx` 承担,不再新增列** |
| 25 | `docs/公寓端/04` | 表 4-1(RESTful 约定) | 补充 | "受控命令"行补 `ASSIGN_ELEMENT`/`ADJUST_TIME`/`LOCK_MATCH`/`UNLOCK_MATCH` 属"受控但非状态迁移" |
| 26 | `docs/公寓端/04` | 表 4-4(错误码) | 新增 | `40909 APT_PERSON_NOT_IN_HOUSE`、`40910 APT_TIME_ILLEGAL`、`40911 APT_MATCH_LOCKED`、`40912 APT_CARD_ID_MISSING` |
| 27 | `docs/公寓端/04` | 表 4-9(叫乘接口) | 新增 | 新增第 7 项 `POST /apt/jcxx/{jcid}/commands`(四类命令)、第 8 项 `POST /duty/jcxx/{jcid}/commands`(值班台) |
| 28 | `docs/公寓端/04` | 表 4-9 `correct` 白名单 | 补充 | 明确"`jcsj` **不在** `correct` 白名单,改时刻走 `commands` 的 `ADJUST_TIME`" |
| 29 | `docs/公寓端/04` | 表 4-10(叫乘域映射) | 修订 | 权限点补 `apt:jcxx:assignElement/adjustTime/lockMatch`(+ `duty:jcxx:adjustTime` 或复用 `changeStatus`,见 `Q-15`) |
| 30 | `docs/公寓端/05` | §1(计划下行与任务生成) | 新增小节 | 新增"§1.1 叫乘信息的产生与匹配",含图 1-1 与表 1-5 |
| 31 | `docs/公寓端/05` | 表 5-7(干预动作) | 新增行 | `ASSIGN_ELEMENT`(允许态:`finish_ly='0'` 且未锁)、`ADJUST_TIME`(`ryzt=0` 且 `finish_jc='0'`)、`LOCK_MATCH`/`UNLOCK_MATCH` |
| 32 | `docs/公寓端/07` | 表 7-2 `M3` 行 | 补充 | 交付物补 `apt_local_task` + 四类命令;验证结果补"调整后派生时刻正确" |
| 33 | `docs/公寓端/07` | 表 7-3(M3 验证项) | 新增行 | 新增 4 条:指定要素成功/未入寓被拒/调整后 `fjsj=jcsj+10min`/锁定后平台不覆盖 |
| 34 | `docs/公寓端/07` | 表 7-5 第 3 项 | 修订 | 准点率补"按 `matchState` 分三组 + 合计" |
| 35 | `docs/公寓端/08` | 表 8-1 `D7`/`D8` 行 | 修订 | 未定默认口径更新为 §2.7.3 与 §2.8 |
| 36 | `docs/公寓端/08` | 表 8-9 | 新增 | `G-10` |
| 37 | `docs/公寓端/08` | 表 8-10 | 新增 | `Q-15`(调整幅度分级阈值 + 准点率三档阈值 + 值班台权限点前缀) |
| 38 | `docs/公寓端/08` | 表 8-11(决策×批次) | 修订 | `D8` 行已是 `■`(M1 强阻塞);`D7` 行由 `M1 □` 提为 `M3 ■` |
| 39 | `docs/公寓端/09` | 表 9-8(`#6`/`#7` 映射) | 补充 | 补 `taskId` 分配方(公寓)与 `matchedPlanKey` 语义 |
| 40 | `docs/公寓端/09` | 表 9-10(上行清单)`#8` 行 | 修订 | 数据来源补"公寓入寓登记产生的 jcxx 与本地指定/调整" |
| 41 | `docs/公寓端/09` | 表 9-11(payload 字段) | 新增行 | `origin`/`localVersion`/`cardId`/`cc`/`jch`/`kcsj`/`jcfs`/`overrideReason` |
| 42 | `docs/公寓端/09` | 表 9-12(`#8` 回执字段) | 补充 | 补"要素登记/变更"用法与 `accepted=false` 的两种原因码 |
| 43 | `docs/公寓端/09` | 表 9-15(对账三维) | 修订 | `count` 行补"按 `biz_id` 去重" |
| 44 | `docs/公寓端/09` | 表 9-18 | 新增 | 新增 `F-14`(`taskId` 分配方、`matchVerdict` 语义、两版本不可跨比较) |
| 45 | `docs/公寓端/01` | 表 1-4 第 1/3 行 | 补充 | "本地应急新增受限"改为"本地指定要素落 `gy_jcxx` 既有列 + 侧表,不改交路快照" |
| 46 | `docs/backend/05-数据模型` | §2.1 `GY_JCXX` 列、§9 状态字典 | 修订 | 补 `jllx` 的真实语义(`0` 可参与匹配 / `1` 已由本地确定);补"jcxx 由入寓登记产生" |
| 47 | `docs/backend/05-数据模型` | §3.1 `GY_SYN_JCXX` | 补充 | 明确它是**匹配要素表**(提供 `kcsj`/`jcfs`),不是任务生成源;补"匹配按工号+车次+`kcsj`" |
| 48 | `docs/backend/05-数据模型` | §6 `GY_SJGZ` | 补充 | 明确 `zjjcsjgz`/`kjjcsjgz`/`lysjgz` 是 `jcsj`/`ylysj` 的派生参数(表 1-5 路径 B) |
| 49 | `docs/backend/06-接口清单` | `/modifyJcXx`、`/resetJllx`、`/synCallDutyInfo` | 补充 | 标注三者的新架构承接关系(`ASSIGN_ELEMENT`+`ADJUST_TIME` / `UNLOCK_MATCH` / `#6`+`#7`) |
| 50 | `docs/backend/06-接口清单`、`docs/10` 表 9-6 | `/getJcXxFromGy` | 移除 | 标注"违反平台不回调公寓,由 `#8` 承接,阶段五下线" |
| 51 | `docs/开发计划/`(上轮产出) | `README` 表 0-3 阶段三行、`02` §1.1 `S3.2`、`03` 表 3-8 `#8` 行、`04` 表 4-1 | 补充 | **可选、非必须**:把本 CR 步骤挂到 `S3.2`,并补 `D17`/`F-14`/`G-10`/`Q-15` 到决策表 |
> 51 行里,**1~8 是上位文档(`docs/10`)**,9~20 是平台端,21~45 是公寓端,46~50 是现状后端文档(`docs/backend/`),51 是计划的连带更新。真正改结构的只有 **1、3、9、21、24、26、27、44、46** 这 9 行。
## 5. 阶段挂载(不新造阶段、不合并阶段)
表 1-21 与 `docs/10` §9 表 9-1~表 9-6 的对应关系
| 阶段(`docs/10` 表号) | 本 CR 的挂载内容 | 挂载依据(原文) |
| --- | --- | --- |
| 阶段零(表 9-1) | **无** | 只做编译/CI/凭据基线,与本议题无关 |
| 阶段一(表 9-2) | **登记**:把"要素匹配职责从公寓迁平台""`getJcXxFromGy` 下线""`jllx` 语义澄清"列入现状清单的待决策问题 | 表 9-2"待决策问题清单(见 10.2)" |
| **阶段二(表 9-3)** | **主体挂载点**。契约冻结前必须完成:`#8` payload 扩展、`#7` 响应体 `matchVerdict`、`taskId` 分配方与 `matchedPlanKey`、`origin`/`matchState` 枚举、三时间戳口径、`PL_TASK`/`PL_TASK_ACK` 列、`apt_local_task` 表设计、`D7` 补注 / `D8'` 修订 / `D17`+`F-14`+`G-10`+`Q-15` 拍板 | 表 9-3"跨端接口契约…上报信封与 outbox 表设计…任务下发/回执模型";`平台端/05` §8"契约冻结前的待办" |
| 阶段三(表 9-4) | M3 批次落地(`apt_local_task` DDL + 四类命令 + `#8` 上报 + `#7` `matchVerdict` 消费 + 匹配职责迁移 + 未匹配不清空改造);断网演练加"离线指定要素/调整"用例 | 表 9-4"断网 2 小时仍完成…;恢复后 10 分钟内上报追平";`公寓端/07` 表 7-2 `M3` |
| 阶段四(表 9-5) | 多公寓:本地临时交路"平台采纳 → 版本化"流程、大盘补"本地指定数/锁定数/采纳率"、跨公寓同 `jlid` 口径 | 表 9-5"监控大盘(在线率、上报积压、对账差异)" |
| 阶段五(表 9-6) | **下线前置条件**:旧 `jcgl` 的 `/modifyJcXx`、`/processTempCallDuty`、`/getJcXxFromGy` 下线前,本地指定要素 + `#8` 上报必须已覆盖等价能力并验收;否则"旧接口调用量归零"无法达成 | 表 9-6"旧接口调用量归零(按网关日志统计)" |
**阶段二细化步骤(作为 `docs/开发计划/` 的补充,不新造阶段)**:
- `S2.1'` 契约补充评审:`#8` payload + `#7` `matchVerdict` + `taskId` 分配方与 `matchedPlanKey`(与 `S2.1` 契约冻结同批);
- `S2.2'` 表清单/DDL 说明补充:`PL_TASK`/`PL_TASK_ACK` 列 + `apt_local_task`(与 `S2.2` 建表清单定稿同批);
- `S2.3'` 匹配职责迁移说明:`matchCallDutyUnderLock` 的匹配算法(工号+车次+`kcsj`)迁到 `platform-plan`,并在公寓侧保留"未匹配不清空"的兜底;
- `S2.5'` 决策评审:`D7` 补注、`D8'` 修订、`D17`/`F-14`/`G-10`/`Q-15` 四项新编号拍板(与 `S2.5` 决策挂载同批);
- 四项都必须在**阶段二退出前**完成,否则阶段三 M3 批次无法开工(`D8` 本就是 `M1` 强阻塞项,`公寓端/08` 表 8-11)。
## 6. 决策登记
表 1-22 `D7` / `D8` 修订建议与新增编号建议
| 编号 | 现状 | 建议 | 理由(必须有,不写"觉得需要") |
| --- | --- | --- | --- |
| **`D7`** | "平台维护模板,公寓可'应急新增'并上报确认"(`docs/10` 表 10-2) | **不改结论,补落点**:行末加"应急新增落 `gy_jcxx` 既有列 + `apt_local_task` 侧表,不写交路快照表;平台确认后由 `platform-route` 版本化为正式模板" | `公寓端/08` 表 8-1 的 `D7` 默认口径是"本期不做本地应急新增(受 G-04 约束)",与上位原文互相抵触;补落点后两者都成立,且不依赖 G-04 的 DDL |
| **`D8`** | "平台下发'计划 + 规则版本',公寓端复算执行,不一致告警" | **修订为 `D8'`**(全文见 §2.7.3):把"计划"精确为"运安要素(`kcsj`/`jcfs`)",明确 `jcsj` 恒由公寓算;加两类例外(本地锁定 / 未匹配不清空)+ 留痕与对账出口 | 现行措辞把执行时刻也划给平台,与真实链路不符;不修订则本 CR 只能在"违反 D8"与"不做"之间二选一 |
| **`D17`**(建议新增,挂 `docs/10` 表 10-2 续) | 不存在 | **需要**:定义"叫乘任务**主键与匹配键分离**"——`taskId` 由公寓分配(= `jcid` 业务编码),`planKey` 只作匹配键,可空 | `D1~D16` 无一覆盖"任务行由公寓产生、平台只做匹配"这件事;若把 `plan_key` 当主键,运安未覆盖的客运班组将**无处落地**,正是本议题要解决的人群 |
| **`F-14`**(建议新增,挂 `docs/公寓端/09` 表 9-18 续) | `F-01~F-13` | **需要**:`taskId` 分配方 + `matchVerdict` 语义 + `match_version` 与 `local_version` 不可跨比较 | `F-01~F-13` 是阶段二**逐条签认**的冻结清单(`平台端/05` 表 5-10 同口径),不进清单就等于没冻结 |
| **`G-10`**(建议新增,挂 `docs/公寓端/03` 表 3-16 与 `08` 表 8-9) | `G-01~G-09` | **需要**:`gy_jcxx` 缺 `local_version`/`origin`/`override_*`,以侧表 `apt_local_task` 规避;**锁定语义已由 `jllx` 承担,不再新增锁定列** | `G-01~G-09` 是"缺口 → 不改库兜底"的登记处;不登记会出现"为什么多一张表"的疑问,也无法在阶段二评审时追溯 |
| **`Q-15`**(建议新增,挂 `docs/公寓端/08` 表 8-10) | `Q-01~Q-14` | **需要**:①调整幅度分级阈值(建议 30 分钟);②准点率三档阈值(建议 `≤30s`/`30~120s`/`>120s`);③值班台权限点前缀用 `duty:jcxx:adjustTime` 还是复用 `apt:jcxx:changeStatus` | 三项都是"必须有值才能验收"的参数,文档中从未固定;不列出来就会被实现方自行猜测 |
| **`P-12`**(平台端) | — | **不建议新增** | `P-01~P-11` 都是平台侧**实现形态**类决策(工程形态/前端归属/HA/命名空间);本议题裁决权在跨端(`D17`/`F-14`),单开 `P` 编号会让"同一件事两个决策入口" |
**决策方与时点**:
| 编号 | 决策方 | 最晚时点(用阶段/步骤表述) |
| --- | --- | --- |
| `D7`(补注) | 平台架构 + 公寓架构 + 产品 | 阶段二 `S2.1'` 前 |
| `D8'`(修订) | 业务方 + 两侧架构 | 阶段二 `S2.1'` 前(原 `D8` 已是 `M1` 强阻塞,`公寓端/08` 表 8-11) |
| `D17` | 平台架构 + 公寓架构(两侧同步) | 阶段二 `S2.1'` 前 |
| `F-14` | 两侧架构联合 | 阶段二 `S2.1'` 契约冻结签字时 |
| `G-10` | 公寓架构 + DBA | 阶段二 `S2.2'` 建表清单定稿前 |
| `Q-15` | 产品 + 架构 | 阶段三 `S3.2`(M3 批次)开工前 |
## 7. 验收清单(四件套)
表 1-23 三条诉求 × 设计条目 / 落表 / 接口 / 验证方法
| 诉求 | 设计条目 | 落表 | 接口 | 验证方法(可复现) |
| --- | --- | --- | --- | --- |
| **1 本地建单(= 本地指定要素并锁定)** | §2.1(对象边界与判据)+ §2.4(落点)+ §2.8(路线 B) | `gy_jcxx`(`jlid`/`cc`/`jch`/`kcsj`/`cclx` + `jllx='1'`)、`apt_local_task`(`origin=LOCAL_ASSIGN`)、`PL_TASK`(`match_state` 待定)、`PL_TASK_ACK` | `POST /apt/jcxx/{jcid}/commands`(`ASSIGN_ELEMENT`);`#8` | ① 断开平台(`APT_REPORT_DISPATCH_ENABLED=false`)→ 为未匹配人员指定 `cc`/`kcsj`/`jcfs` → `gy_jcxx` 的 `jcsj`/`fjsj`/`jcbjsj`/`ylysj` 按表 1-5 路径 B 算出且满足 `jcsj ≤ fjsj ≤ jcbjsj < kcsj`;② 手动触发运安匹配 → 该行**不被清空**(`jllx='1'` 保护);③ 打开派发 → outbox 转 `SENT`;④ 平台管理端按 `gyId` 可查 `taskId`/`match_state`;⑤ `count`(按 `taskId` 去重)零差异 |
| **2 本地调整时刻** | §2.2(时刻口径)+ §2.3(权限与确认) | `apt_local_task`(`origin_jcsj`/`local_version`/`override_*`/`derived_rule_version`)、`gy_jcxx`(`jcsj`/`fjsj`/`jcbjsj`/`ylysj`) | `ADJUST_TIME`(经 `/apt/jcxx/{jcid}/commands` 与 `/duty/jcxx/{jcid}/commands`)+ `#8`(`bizVersion+1`) | ① 把 `jcsj` 调 20 分钟 → 路径 A 下 `fjsj=jcsj+10min`、`jcbjsj=jcsj+15min`、`ylysj` 不变;路径 B 下 `ylysj=jcsj+lysjgz`;② `local_version` 由 1 变 2;③ 同 `commandId` 连发 3 次只生效 1 次;④ 平台 `PL_TASK_ACK` 出现 2 条(`bizVersion` 1/2),`adjust_delta_ms = 1200000` |
| **3 上报可查 / 可对账 / 可报表 / 方向纪律** | §2.5(载体与三时间戳)+ §2.6(平台落点与对账)+ §2.10(指标) | `PL_RECEIVE_LOG`、`PL_TASK_ACK`、`PL_TASK`、`V_CALL_QUALITY` | `#8`(上行)、`#7` 响应体 `matchVerdict`、`#14`(水位不变) | ① 平台可出"每公寓本地指定数 / 锁定量 / 匹配率 / 采纳率"报表;② `count`(按 `taskId` 去重)、`max(watermark)` 零差异,`sum(amount)` 不适用;③ 全仓 grep `平台推送`/`平台回调`/`平台触发`/`打包上传` 仅出现在禁止性表述;④ mermaid 图无"平台 → 公寓"箭头;⑤ 下行仍 8、上行仍 8;⑥ 旧 `/jcgl/getJcXxFromGy` 已无调用 |
**共同的负面用例(必须一起验)**:
| 场景 | 期望 |
| --- | --- |
| 人员未入寓(无有效 `gy_jcxx` 行)即请求指定要素 | `40909 APT_PERSON_NOT_IN_HOUSE` |
| `card_id` 为空(无法匹配运安) | `40912 APT_CARD_ID_MISSING` |
| 指定后违反 `jcsj ≤ fjsj ≤ jcbjsj < kcsj` | `40910 APT_TIME_ILLEGAL` |
| 对已锁定任务再次 `ASSIGN_ELEMENT` | `40911 APT_MATCH_LOCKED`,提示先解锁 |
| 对 `ryzt ≥ 1` 或 `finish_jc='1'` 的任务调整 | `40910 APT_TIME_ILLEGAL`(或按 `Q-15` 结论给专码) |
| 运安未匹配到该人/该车次 | `gy_jcxx` 的 `kcsj` 与叫乘任务**保留**,`match_state=UNMATCHED`,值班台亮提示 |
| 已锁定任务收到平台 `MATCHED`(要素不同) | `#8` `accepted=false`,`reason=LOCAL_MATCH_LOCKED`,平台 `PL_TASK_ACK.match_locked=1` |
| 重复提交同 `commandId` | 只生效 1 次(`duty_manual_action.action_id` UK) |
| 断网 2 小时指定 3 条、调整 5 次 | 恢复后 ≤ 10 分钟 outbox 清空,`count` 零差异,`max(watermark)` 追平 |
## 8. 连带发现的口径不一致(只登记,不修改)
表 1-24 编写本 CR 过程中发现的不一致(本轮**只报告**,处理另立一轮)
| # | 甲侧出处与表述 | 乙侧出处与表述 | 影响 | 本 CR 的临时口径 |
| --- | --- | --- | --- | --- |
| 冲突-A | `docs/backend/05` §2.1:`GY_JCXX` 主键为 `jcid`(入住记录 ID) | `公寓端/05` 表 5-1 步骤 4:`gy_jcxx.jcid`(**= 计划侧业务键**) | `taskId` 取谁直接影响 `#8` 的 `bizId` | 本 CR 统一 `taskId` = `jcid` 业务化编码(行由入住产生);由 `F-14` 冻结时澄清 |
| 冲突-B | `公寓端/03` 表 3-9:条件更新写作 `where id=? and ryzt=?` | `docs/backend/05` §2.1:主键 `jcid`;`公寓端/04` 表 4-9 出参同时给 `id` 与 `jcid` | 无法判断 `id` 是代理键还是 `jcid` 别名 | 一律以 `jcid` 作业务键、`id` 作代理键;DDL 时以实际表结构为准 |
| 冲突-C | `平台端/05` 表 5-6 与 `公寓端/09` 表 9-15:`count` = "去重后条数"(未说明按什么去重) | 变更上报会 `bizVersion+1` 产生新 `idempotencyKey`(`平台端/05` 表 5-3"更正上报"行) | 按 `idempotencyKey` 去重时,同一任务多次变更被计成多条 → 与公寓侧基数永远不等 | 按 `biz_id`(`taskId`)去重;该修订对 `LEDGER` 同样适用 |
| 冲突-D | `公寓端/08` 表 8-1 `D7` 行默认口径:"本期不做本地应急新增(受 G-04 约束)" | `docs/10` 表 10-2 `D7` 原文:"平台维护模板,公寓可'应急新增'并上报确认" | 上位允许、下位禁止,实际等于"永远不做" | 按表 1-22 的 `D7` 补注处理 |
| 冲突-E | `平台端/01` 表 1-1 末行:"应用端…**只能人工改写单条任务,不能改规则**" | `公寓端/04` 表 4-9:`correct` 白名单不含 `jcsj`,`commands` 五条命令无一条改时刻;但旧系统 `POST /jcgl/modifyJcXx` 已能改 | 上位承诺的能力在新架构接口层无落点 | 本 CR 新增 `ADJUST_TIME`/`ASSIGN_ELEMENT` 补上该落点 |
| **冲突-F** | `docs/backend/05` §9 状态字典:`jllx` = "0 按交路;1 非标准/便乘/驻在/人工修改" | 代码:固定交路入住 `populateJcXxByJlXx` 即置 `'1'`;运安匹配只查 `jllx='0'`;`resetJllxByJcid` 置回 `'0'` | 按文档读会把"固定交路"误判为"非标准";且看不出它其实是**匹配开关** | 本 CR 采用代码语义:"`0` 可参与运安匹配 / `1` 已由本地确定(不参与匹配)";建议阶段二修订 `docs/backend/05`(表 1-20 第 46 行) |
| **冲突-G** | `公寓端/03` 表 3-2:`gy_syn_jcxx` = `apt-master-client`(计划下行落库) | `docs/backend/05` §3.1:`GY_SYN_JCXX` = 运安计划快照(含 `kcsj`/`jcfs`) | 前者像"任务来源",后者是"匹配要素";若按前者理解,会重犯 v1 的错误(以为任务由它生成) | 本 CR 采用后者:它是**匹配要素表**;建议阶段二在表 3-2 补注(表 1-20 第 21 行) |
| **冲突-H** | `docs/backend/06` 接口清单:`POST /jcgl/getJcXxFromGy` = "接收公寓同步的叫乘数据"(实为中心把数据**推给**公寓) | `docs/backend/10` 表 5-1 第 4 行 + 表 8-4「出站 443/tcp」行:"平台不回调公寓、下行一律拉" | 旧接口方向与新纪律相反,现状却仍在用 | 本 CR 判定其**必须下线**,能力由 `#8` 反向承接(表 1-20 第 50 行);阶段五下线前须完成等价能力验收 |
> 冲突 A~H 与缺口 E **本轮一律不修改**,只登记。其中 C 影响面超出本议题(涉及所有更正类上报),建议单独一轮修订 `平台端/05` 表 5-6 与 `公寓端/09` 表 9-15;F/H 涉及现有生产接口语义,需与运行方确认后再生效。
## 9. 本 CR 的自我检查
表 1-25 检查项与结论
| # | 检查项 | 方法 | 结论 |
| --- | --- | --- | --- |
| 1 | 是否与业务澄清的五步链路一致 | 逐条比对 `表 1-1` 与 §2.5/§2.6 | 一致:产生在公寓(`insertJcxx`)、匹配在平台、`jcsj` 恒由公寓算、下行一律拉 |
| 2 | 三条核心诉求是否都有可执行落点 | 逐条对照表 1-23 四件套 | 3/3 有设计条目 + 落表 + 接口 + 验证方法;无"原则上允许、实现上无路径"的条目 |
| 3 | 是否破坏三处冻结口径 | 检查媒体、对象存储、接口计数 | ①不涉及抓拍/录音;②未新增对象存储桶;③下行仍 8、上行仍 8(只扩 `#7` 响应体与 `#8` payload) |
| 4 | 是否出现"平台 → 公寓"方向 | 读图要点 4 条 + 全篇检索 | 图 1-1 跨边界实线全部为公寓 → 平台;平台核验结论仅以 `#7` 响应体形式出现;`平台推送/平台回调/平台触发/打包上传` 只出现在禁止性表述 |
| 5 | `#16`/`#17` 是否被误用 | 检索全文 | 全程未使用;`P-02` 未定前仍按"只做公寓端本地入口" |
| 6 | 是否新造或合并阶段 | 对照 `docs/10` §9 表 9-1~9-6 | 6 个阶段名与序号原样沿用;细化只以步骤(`S2.1'`/`S2.2'`/`S2.3'`/`S2.5'`)出现 |
| 7 | 是否臆造工期与人力 | 检索日历日期、"人月" | 无日历日期、无人月;只用"阶段 / 步骤 / 批次" |
| 8 | 是否静默绕过 `D7`/`D8` | 检查 §6 与 §8 | `D7` 显式补注、`D8` 显式给出修订全文;冲突-D/G 已登记 |
| 9 | 代码依据是否可回溯 | 每个结论是否挂到具体文件/函数 | §0 表 1-1、§2.2 表 1-5、§2.7 表 1-13、§8 冲突-F/G/H 均标注了函数名或接口路径 |
| 10 | 图表编号是否连续 | 按文件核对 | `表 1-1 ~ 表 1-25`、`图 1-1`,与文件序号 1 一致,无跳号 |
| 11 | 是否改动既有文档与代码目录 | `git status` + 文件 `LastWriteTime` | 仅重写 `docs/设计变更/01-本地建单与本地调整.md`;`docs/` 既有文件与 `yktgl/`、`YktVue/`、`apartment/` 零写入;未执行 `git commit` / `git push` |
---
**一句话总结**:jcxx 的产生端在公寓(入寓登记当场建行),平台只做"拿运安数据补 `kcsj`/`jcfs`"这一步,`jcsj` 及其派生永远由公寓按本地时间规则算。因此本 CR 不新增接口、不改人员主数据、不做平台出站通道,用"`gy_jcxx` 既有列承载要素 + `jllx` 承担锁定 + `apt_local_task` 承载治理 + `#8` 上行做要素登记与核验 + `#7` 响应体回传匹配结论"五件套,把 `D8` 从"平台管到执行时刻"修订为"平台管要素、公寓管时刻、锁定与未匹配两类显式例外",让客运班组的固定交路叫乘在运安未覆盖与断网两种条件下都能成立。
@@ -0,0 +1,439 @@
# 文档修订清单(可执行)
> **来源**:`docs/设计变更/01-本地建单与本地调整.md` 表 1-20(51 行修订建议)+ 表 1-24(冲突 A~H 登记)。
> **本文件的定位**:把上述两表转成**拿着就能逐行改文档**的施工单。只产出文档修订建议——不改 `yktgl/`、`YktVue/`、`apartment/` 三个代码目录,不执行 `git commit` / `git push`。
> **本文件不改任何既有文档**。§2~§7 全部是"待执行"条目;执行方另立一轮按其逐条落笔。
---
## 0. 使用说明
### 0.1 三列语义
| 列 | 含义 | 写作要求 |
| --- | --- | --- |
| **原文口径** | 被修订文档里的**现有原文**(引号内为逐字摘录,`→` 后为位置说明) | 必须能在目标文件中 `Ctrl+F` 找到;不得改写、不得概括 |
| **修订后口径** | **可直接替换上栏文字**的新文本 | 能整段粘贴;含表格行时连 `\|` 一起给;不含"建议""可考虑"等模糊词 |
| **修订理由** | 为什么必须改,以及不改的后果 | 必须指向具体事实(上下位矛盾 / 代码行为 / 编号断裂 / 验收不可判) |
### 0.2 编号规则
- 主清单编号 **`R-01`~`R-51` = 表 1-20 的第 1~51 行**,编号与源行号严格相同,便于双向回查。
- 冲突项 **`R-C1`~`R-C11`** = 表 1-24 的冲突 A~H + 本轮新发现 I/J/K。
- 路径项 **`R-P1`~`R-P5`** = 路径简写替换。
- 因按**执行批次**排列,编号会出现跳跃(例如 R-46 排在 R-09 之前)。这是刻意的:批次反映依赖顺序,编号反映来源行号。
- 本文件图表编号用 `表 2-y`(`x` = 文件序号 2,沿用本目录与 `docs/平台端/` 共同的编号约定),因此与 `docs/设计变更/01-本地建单与本地调整.md` 的 `表 1-y` 不冲突,也与 `docs/backend/10-架构优化方案-三端拆分.md` 的表号不冲突。
### 0.3 路径基线(本文件自身遵守)
本文件**一律使用迁移后的实际路径**,不使用 `docs/10` 式简写。实际目录结构为:
```
docs/
├── backend/ 01~11 + README.md (由原 docs/01~11 迁入)
├── frontend/ 01~07 + README.md (未迁移,原位)
├── 公寓端/ 01~09 + README.md
├── 平台端/ 01~09 + README.md
├── 开发计划/ 01~04 + README.md
├── 设计变更/ 01、02(本文件)
└── (docs/ 顶层无任何散落 md 文件;原 docs/README.md 已随迁移成为 docs/backend/README.md)
```
**唯一例外**:引言"原文口径"列必须保留原文里的简写(如 `docs/10`),否则就不是原文。此类位置一律加括注「原文简写」。替换动作见 §8。
### 0.4 执行纪律
1. 只改 `docs/` 下的 markdown;三个代码目录零写入。
2. 不 `git commit`、不 `git push`;改动留工作区待评审。
3. 不臆造工期与人力;时点只用「阶段 / 步骤 / 批次」表述。
4. 三处冻结口径零反向表述:`平台推送`、`平台回调`、`平台触发`、`打包上传` 只允许出现在**禁止性表述**中。
5. 每改完一批,跑一次 §10 的机器可查项。
---
## 1. 修订总览
### 表 2-1 按执行批次与目标文件分组
| 批次 | 目标文件 | 涉及条目 | 条数 | 其中改结构 |
| --- | --- | --- | --- | --- |
| **A1**(上位依据,先改) | `docs/backend/10-架构优化方案-三端拆分.md` | R-01~R-08 | 8 | R-01、R-03 |
| **A2**(现状后端,与 A1 同批) | `docs/backend/05-数据模型.md` | R-46~R-48 | 3 | R-46 |
| **A2** | `docs/backend/06-接口清单.md` | R-49~R-50 | 2 | — |
| **B**(平台端) | `docs/平台端/03-数据设计.md` | R-09~R-12 | 4 | R-09 |
| **B** | `docs/平台端/05-跨端接口对齐.md` | R-13~R-16 | 4 | — |
| **B** | `docs/平台端/06-关键流程.md` | R-17~R-18 | 2 | — |
| **B** | `docs/平台端/01-功能边界.md` | R-19 | 1 | — |
| **B** | `docs/平台端/09-待决策与风险.md` | R-20 | 1 | — |
| **C**(公寓端) | `docs/公寓端/03-数据设计.md` | R-21~R-24 | 4 | R-21、R-22 |
| **C** | `docs/公寓端/04-接口设计.md` | R-25~R-29 | 5 | R-26、R-27 |
| **C** | `docs/公寓端/05-关键流程.md` | R-30~R-31 | 2 | — |
| **C** | `docs/公寓端/07-实施计划与验收.md` | R-32~R-34 | 3 | — |
| **C** | `docs/公寓端/08-待决策与风险.md` | R-35~R-38 | 4 | R-38 |
| **C** | `docs/公寓端/09-平台接口约定.md` | R-39~R-44 | 6 | R-44 |
| **C** | `docs/公寓端/01-功能边界.md` | R-45 | 1 | — |
| **D**(可选) | `docs/开发计划/`(4 篇) | R-51 | 1 | — |
| **E**(冲突裁定) | 跨 8 篇 | R-C1~R-C11 | 11 | — |
| **F**(路径) | 全 `docs/`(27 个文件,484 处) | R-P1~R-P5 | 5 | — |
| **合计** | | | **67** | **9** |
> 51 行原建议 → **67 条可执行条目**:多出的 **16 条**来自 ①表 1-24 八个冲突转条目(`R-C1`~`R-C8`)②本轮新发现三条(`R-C9`~`R-C11`)③路径替换独立成条(`R-P1`~`R-P5`)。
### 表 2-2 执行顺序与依赖
| 步骤 | 做什么 | 依赖 | 为什么这个顺序 |
| --- | --- | --- | --- |
| 第 1 步 | 批次 A1:改 `docs/backend/10` R-01~R-08 | 无 | 它是唯一上位依据;下游 8 篇都引用它,先改上游才能让下游"引用有据" |
| 第 2 步 | 批次 A2:改 `docs/backend/05`、`06` R-46~R-50 | A1 的 R-01(`D8'` 全文) | R-46 的 `jllx` 语义要与 `D8'` 的例外① 对齐;R-50 的引证要用 A1 修正后的表号 |
| 第 3 步 | 批次 E 的 **R-C8 引用纠正** | A1 | 该纠正是"全仓统一替换",必须在上游定稿后一次性扫,否则改两遍 |
| 第 4 步 | 批次 B:平台端 R-09~R-20 | A1、A2 | 平台端表设计引用 `docs/backend/05` 的表名与 `docs/backend/10` 的 `D8'` |
| 第 5 步 | 批次 C:公寓端 R-21~R-45 | A2、B 的 R-09/R-13/R-14 | 公寓端 `#8` 字段以平台端表 5-1/5-3 为对端契约;`taskId` 分配方两侧必须同口径 |
| 第 6 步 | 批次 E 其余(R-C1~R-C7、R-C9~R-C11) | B、C | 冲突裁定要在两侧新口径落笔后才有"甲/乙两侧原文"可比 |
| 第 7 步 | 批次 F:路径替换 R-P1~R-P5 | 全部 | 最后做,避免前面加的文字又要再改一次 |
| 第 8 步 | 批次 D:R-51(可选) | 全部 | 计划是排期文档,设计定稿后再挂 |
### 表 2-3 必改 / 可选
| 等级 | 条目 | 判定依据 |
| --- | --- | --- |
| **必改(阻塞契约冻结)** | R-01、R-03、R-04、R-05、R-09、R-14、R-21、R-24、R-26、R-27、R-39、R-41、R-44 | 涉及主键、幂等键、接口字段、错误码、缺口编号;不改则阶段二契约无法冻结 |
| **必改(消除上下位矛盾)** | R-02、R-19、R-35、R-C4、R-C6、R-C9、R-C10 | 同一件事在两处结论相反,评审必被追问 |
| **必改(编号与引用正确性)** | R-24、R-44、R-C8、R-P1~R-P5 | 断号、失效路径、引用不精确 |
| **建议改(可读性与可验收性)** | 其余 | 不改不阻塞,但会让实现方自行猜值 |
| **可选** | R-51 | 仅排期文档,不影响设计正确性 |
---
## 2. 批次 A1:`docs/backend/10-架构优化方案-三端拆分.md`
### 2.1 决策登记(§10.2 表 10-2)
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-01** | §10.2 表 10-2,`D8` 行 | 修订 | `\| D8 \| 叫乘时间以谁为准 \| 平台下发“计划 + 规则版本”,公寓端复算执行,不一致告警 \|` | `\| D8 \| 叫乘时刻的要素与执行时刻各由谁决定 \| **要素**(开车时刻 `kcsj`、接车方式 `jcfs`)以平台 + 运安匹配为基准,平台下发“运安要素 + 时间规则版本”;**执行时刻 `jcsj`** 及其派生(`fjsj`/`jcbjsj`/`ylysj`)一律由公寓端按本地时间规则计算,复算后回执 `computedCallTime`;要素差异超阈值告警。以下两类必须留痕、必须上报、必须进对账差异清单,不得静默覆盖:① `jllx='1'` 的**本地锁定任务**,本地要素优先,直至显式解锁后回归平台匹配值;② **平台未匹配到运安计划**(`match_state=UNMATCHED`)的任务,保留本地要素,**不得因未匹配而清空既有叫乘任务** \|` | 现行措辞里的"计划"一词把 `jcsj` 也包了进去,读起来是"平台在管执行时刻",与"jcxx 行由入寓登记产生、平台只补要素"的真实链路相反。不修订则本议题只能在"违反 `D8`"与"不做"之间二选一。全文见 `docs/设计变更/01-本地建单与本地调整.md` §2.7.3 |
| **R-02** | §10.2 表 10-2,`D7` 行 | 补充 | `\| D7 \| 交路人工维护落点 \| 平台维护模板,公寓可“应急新增”并上报确认 \|` | 行末补:`。应急新增的落点是“对既有 `gy_jcxx` 行指定要素并锁定(`jllx='1'`)”,落 `gy_jcxx` 既有列 + 侧表 `apt_local_task`,**不写交路快照表**(`apt_master_route` 无 `source` 列,本地新增会被全量覆盖,见 `docs/公寓端/08-待决策与风险.md` 表 8-9 `G-04`);平台确认后由 `platform-route` 版本化为正式模板` | `docs/公寓端/08-待决策与风险.md` 表 8-1 的 `D7` 默认口径是"本期不做本地应急新增(受 `G-04` 约束)"。上位允许、下位禁止,合起来实际等于"永远不做"。补落点后两者同时成立,且不依赖 `G-04` 的 DDL 变更 |
| **R-03** | §10.2 表 10-2,`D16` 行之后 | 新增 | `D1`~`D16` 共 16 行,无 `D17` | 追加一行:`\| D17 \| 叫乘任务的主键与匹配键是否分离 \| **分离**:`taskId` 由公寓分配(= `gy_jcxx.jcid` 业务化编码),`planKey` 只作运安匹配键、可空。理由:`gy_jcxx` 行由入寓登记产生,运安未覆盖的客运班组永远没有 `plan_key`;若把 `plan_key` 当主键,这批人无处落地 \|` | `D1`~`D16` 无一覆盖"任务行由公寓产生、平台只做匹配"这件事。主键选择直接决定 `#8` 的 `bizId` 与幂等键;不定则两侧会各写一套 |
> `D8` 行的"谁决定"与"以谁为准"在本文中一律指**要素**与**执行时刻**两个不同对象,不再混用。
### 2.2 跨端接口契约(§6.1 表 6-1)
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-04** | §6.1 表 6-1,`#8` 行 | 补充 | `\| 8 \| 上行 \| POST /platform/v1/ack/tasks \| 任务接收回执(含复算结果) \| taskId、accepted、computedCallTime、ruleVersion \|` | 用途列改为:`任务接收回执 + **公寓产生的叫乘要素登记与变更**(含复算结果)`;字段列改为:`taskId、accepted、computedCallTime、ruleVersion、origin、localVersion、cardId、cc、jch、kcsj、jcfs、overrideReason`(其中 `kcsj`/`jcfs` 为公寓已知值,空表示待平台补全) | `#8` 同时是**平台匹配的输入**——平台拿它去运安快照里查 `kcsj`/`jcfs`。只写"回执"会让实现方把它当纯下游产物,公寓上报的要素无处登记。**接口编号、方向、路径一概不改**,只扩用途与字段 |
| **R-05** | §6.1 表 6-1,`#7` 行 | 补充 | `\| 7 \| 下行 \| GET /platform/v1/tasks/changes?gyId&cursor \| 任务变更/取消增量 \| taskId、op=UPSERT/CANCEL、version \|` | 用途列补:`;响应体与 replayHint 同级返回 matchVerdict[](平台对本地指定/调整的核验结论)`;字段列补:`matchVerdict[] =(taskId、verdict、localVersion、matchedPlanKey、kcsj、jcfs、decidedAt)` | 平台核验结论必须有回传通道,而"平台不回调公寓"禁止新开推送方向。`#7` 是公寓主动调用的唯一增量通道,其**响应体**是唯一合规载体(与 `replayHint` 同理)。**下行接口数仍为 8** |
### 2.3 阶段产出与验收(§9 表 9-3 / 9-4 / 9-6)
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-06** | §9 表 9-3「主要产出」行 | 补充 | `跨端接口契约(表 6-1 的 17 个 + 表 6-3 的语音域 3 个,含错误码、幂等语义、限流);上报信封与 outbox 表设计;主数据版本与快照表设计;任务下发/回执模型;平台主账与公寓流水模型;旧接口兼容适配层方案` | 在"任务下发/回执模型"后插入:`;**叫乘要素登记契约**(匹配键 `cardId`/`cc`/`jch`/`kcsj`;`matchState` 五值 `PENDING`/`MATCHED`/`UNMATCHED`/`LOCKED`/`REJECTED`;`matchVerdict.verdict` 三值 `MATCHED`/`UNMATCHED`/`REJECTED`;`localVersion` 与 `matchVersion` 不可跨比较;`jllx` 承载锁定语义)` | 阶段二是**契约冻结**阶段。本议题的实质是"任务行由公寓产生、要素由平台补全",不进入产出清单就无法在契约冻结时签认,等于没冻结 |
| **R-07** | §9 表 9-4「验收标准」行 | 补充 | `断网 2 小时仍完成“入住 → 叫乘 → 复叫 → 报警 → 应答 → 离寓 → 归档”;恢复后 10 分钟内上报追平且对账零差异;叫乘准点率不低于现网;值班员操作步骤增加不超过 10% 且有记录` | 在"断网 2 小时仍完成…归档"后插入:`(演练须含**离线指定要素**与**离线调整时刻**两类用例:恢复后 ≤ 10 分钟 outbox 清空、`count` 零差异、`max(watermark)` 追平)` | 断网时"本地建单 / 本地调整"是现场唯一可用路径。原验收只验读取,不验写入——断网 2 小时能"看"却无法"改",等于没有闭环 |
| **R-08** | §9 表 9-6「主要产出」+「验收标准」 | 补充 | 主要产出:`syndata / ByxxResourceService / SynDataSchedule 从公寓端下线;…`;验收标准:`…;旧接口调用量归零(按网关日志统计);回滚演练通过` | 主要产出末尾补:`;旧 \`/jcgl/modifyJcXx\`、\`/jcgl/processTempCallDuty\`、\`/jcgl/getJcXxFromGy\` 下线(方向与新纪律相反,见 \`docs/backend/06-接口清单.md\`)`;验收标准在"旧接口调用量归零"前补前置条件:`旧“手工/临时叫乘”入口下线前,**本地指定要素 + #8 要素登记必须已覆盖等价能力并通过阶段三验收**,否则本项不可判“归零”` | 这三个旧接口是现场**当前唯一**的补录手段。新能力未覆盖就下线,等于删掉现场唯一的兜底;"调用量归零"会变成"没人能叫乘了所以没人调" |
---
## 3. 批次 A2:`docs/backend/05-数据模型.md`、`docs/backend/06-接口清单.md`
### 3.1 `docs/backend/05-数据模型.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-46** | §2.1 `GY_JCXX` 表 + §9 状态字典 | 修订(改结构) | §2.1 时间行:`\| 时间 \| kcsj、jcsj、fjsj、jcbjsj \| 开车、叫乘、复叫、叫乘结束(计划) \|`<br>§9:`\| GY_JCXX.jllx \| 0 按交路;1 非标准/便乘/驻在/人工修改 \|` | §2.1 增一行:`\| 产生方式 \| —(无来源列) \| gy_jcxx 行由入寓登记当场创建(submit_rydj → insertJcxx),**不是**平台下发;平台只对其补全 kcsj / jcfs \|`<br>§9 `jllx` 行改为:`\| GY_JCXX.jllx \| 0 **可参与运安匹配**(交路未定,等待匹配);1 **已由本地确定**(固定交路入住、人工指定或调整后置位),**不参与运安匹配**。注意:1 不是“非标准/便乘/驻在”的分类标签,而是**匹配开关 + 锁定标志**;/resetJllx 置回 0 使其重新进入匹配池 \|` | ①现行字典会把"固定交路入住"误读成"非标准",而这恰恰是客运班组的主流场景;②它看不出 `jllx` 是**匹配开关**——代码里 `populateJcXxByJlXx` 固定交路入住即置 `'1'`、`matchCallDutyUnderLock` 只取 `jllx='0'`、`resetJllxByJcid` 置回 `'0'`。③"从哪来"缺失,是 v1 把 jcxx 当成平台下发的根源 |
| **R-47** | §3.1 `GY_SYN_JCXX` | 补充 | `\| kcsj \| 开车时间(匹配后据此本地计算叫乘时间) \|`(表内逐列) | 在表前加一段:`**GY_SYN_JCXX 是运安计划快照,即“匹配要素表”(提供 kcsj / jcfs / jch),不是叫乘任务的生成源**——它不承载任务、不承载状态、不承载版本。匹配键为 `ryid`(工号)+ `cc`(车次)+ `kcsj` 归日。它是整表快照(ByxxResourceService 先清空再写入),任何本地写入都会被下次同步覆盖,因此**不得在其中写本地占位行**` | `docs/公寓端/03-数据设计.md` 表 3-2 把它标注为"`apt-master-client`(计划下行落库)→ 生成任务",字面上像"任务来源"。按此理解会重犯"任务由计划表生成"的错误,并导致在该表写本地占位行——占位行会污染匹配(按 `kcsj` 匹配)、会被下次全量同步清掉 |
| **R-48** | §6 `GY_SJGZ` | 补充 | `\| GY_SJGZ \| 时间规则:lysjgz、zjjcsjgz、kjjcsjgz(按公寓 + 单位) \|` | 补:`这三个字段是**叫乘时刻的派生参数**,不是叫乘时刻本身:站接时 jcsj = kcsj − zjjcsjgz;库出时 jcsj = kcsj − kjjcsjgz;ylysj = jcsj + lysjgz;fjsj = jcsj + 10 分钟、jcbjsj = jcsj + 15 分钟(与接车方式无关)。改规则只影响之后新算的任务,**不回溯**已算出的任务` | 文档只列字段名不列公式,导致"规则版本"无法落成可复算的契约。平台侧 `rule_version` 留痕、公寓侧 `derived_rule_version` 都要以这三个参数为版本对象;不写公式就没人能验证"这行按哪版规则算出" |
### 3.2 `docs/backend/06-接口清单.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-49** | 接口清单 `/modifyJcXx`、`/resetJllx`、`/synCallDutyInfo` 三行 | 补充 | `\| POST \| /resetJllx \| 重置交路类型 \|`<br>`\| ALL \| /modifyJcXx \| 修改叫乘信息并重建任务 \|`<br>`\| POST \| /synCallDutyInfo \| 运安计划推送(整表快照) \|` | 新增一列「目标架构承接」,三行分别填:<br>`/modifyJcXx` → `POST /apt/jcxx/{jcid}/commands` 的 `ASSIGN_ELEMENT`(换交路/车次/开车时刻/接车方式)+ `ADJUST_TIME`(只调 jcsj)。语义差异:旧接口一次可改多个字段且不区分“要素”与“时刻”,新接口强制 reason、commandId 幂等、并把 jllx 置 1 锁定`<br>`/resetJllx` → `UNLOCK_MATCH`(解锁后回到运安匹配池),由 `apt:jcxx:lockMatch` 权限点管控`<br>`/synCallDutyInfo` → `运安快照写入入口;目标架构在平台侧由 #6 下行替代,公寓端不再直连运安(docs/backend/10 表 9-6)` | 这三个接口是新能力的**语义前身**。不标承接关系,评审无法判断"新接口是否等价、旧接口能否下线";R-08 的"下线前置条件"也就无从核对 |
| **R-50** | 接口清单 `/getJcXxFromGy` 行 | 移除(标注入) | `\| POST \| /getJcXxFromGy \| 从公寓端获取叫乘信息 \|`(**接口名说"从公寓获取",实现是中心把数据推给公寓**) | 行内标注:`**方向违反“平台不回调公寓、下行一律拉”**——依据是 \`docs/backend/10-架构优化方案-三端拆分.md\` **表 5-1 第 4 行**(“平台端 → 公寓端|不允许直连|仅落‘待下发配置版本’,由公寓端拉取”)与**表 8-4「出站 443/tcp」行**(“单向出站,平台侧不回调”)。能力由公寓端 #8 上行(要素登记)+ #7 响应体(核验结论)反向承接;**阶段五下线**,前置条件见 `docs/backend/10` 表 9-6 修订后文字(本清单 R-08)` | 接口名与实现方向相反,文档只记名字会让接手人误判方向。**原表 1-24 冲突-H 引"`docs/10` 表 8-1"不精确**——表 8-1 是"三端部署形态",不含该表述;正确依据是表 5-1 第 4 行 + 表 8-4 出站行(详见 R-C8) |
---
## 4. 批次 B:`docs/平台端/` 五篇
### 4.1 `docs/平台端/03-数据设计.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-09** | §5.3 表 3-5,`PL_TASK` 与 `PL_TASK_ACK` 两行 | 修订(改结构) | `\| PL_TASK \| 新增 \| 平台侧任务视图(计划 → 应叫任务,含规则版本) \| task_id、gyid、plan_key、ryid、jcsj(平台计算值)、rule_version、status、source_version \| PK task_id、UK (gyid, plan_key) \|`<br>`\| PL_TASK_ACK \| 新增 \| 公寓回执(#8):复算值与差异 \| ack_id、gyid、task_id、accepted、computed_call_time、rule_version、diff_ms、received_at \| PK ack_id、UK (gyid, task_id, rule_version) \|` | **`PL_TASK`**:语义改为 `平台侧任务视图(接受公寓登记的任务行,承载匹配要素与匹配状态)`;列改为 `task_id(**公寓分配** = jcid 业务编码)、gyid、ryid、matched_plan_key(**匹配键,可空**)、match_state、origin、matched_kcsj、matched_jcfs、local_version、rule_version、status、source_version`;约束改为 `PK task_id、UK (gyid, task_id)、IDX (gyid, match_state)`。<br>**`PL_TASK_ACK`** 列补 `origin、local_version、match_locked、adjust_delta_ms`(保留 `diff_ms` 作为“公寓复算值与平台基线之差”)。<br>**删除**:`plan_key` 作 UK、`jcsj(平台计算值)`。 | ①`plan_key` 作 UK 会让"运安未覆盖的客运班组"无处落地——而这批人正是本议题要解决的人群(`D17`);②`jcsj(平台计算值)`直接违反 `D8'`(执行时刻恒由公寓算),保留它就有两个"叫乘时刻"的事实源;③回执要能回答"这版差异是锁定的还是没匹配到",必须加 `match_locked`;④本地主动调整量(`adjust_delta_ms`)与复算差异(`diff_ms`)是两种口径,混用会让差异报表失真 |
| **R-10** | §5.4 表 3-10,`PL_TASK.status` 行 | 补充 | `\| PL_TASK.status \| PLANNED / DISPATCHED / ACKED / EXECUTING / FINISHED / CANCELLED(六态) \| platform-plan \|…` | 备注补:`公寓来源任务(task_id 由公寓分配)**自 ACKED 起**登记,**不新增状态值**;PLANNED / DISPATCHED 只对“平台已生成但公寓尚未回执”的中间态成立,公寓来源任务不经过这两个态` | 不写明起点,实现方会为"公寓先建单"再补一个 `RECEIVED` 之类的态,出现两套状态机——这正是 `docs/backend/10` 风险清单第 7 行"状态机多写"要防的事 |
| **R-11** | §5.4 表 3-11(平台/公寓状态分工) | 新增 | 现有行含:`叫乘是否完成 \| PL_TASK.status = FINISHED \| gy_jcxx.ryzt、finish_jc \| **公寓**(平台不写 ryzt)` | 新增两行:<br>`\| 任务是否存在、主键是什么 \| PL_TASK.task_id(**接受公寓分配值**) \| gy_jcxx.jcid 业务编码 → apt_local_task.task_id \| **公寓**(行由入寓登记产生) \|`<br>`\| 运安要素是否匹配到 \| PL_TASK.match_state \| 只读消费 \| **平台**(公寓不得自行置 MATCHED) \|` | 不改这两行,读者会以为"接受公寓分配的主键"意味着平台仍决定"有哪些任务";且不写"公寓不得自行置 `MATCHED`",两侧都可能写 `match_state` |
| **R-12** | §6 表 3-13(同步方向与版本语义) | 修订 | `#8` 行、`#7` 行按"任务变更 / 任务接收回执"表述;无版本坐标系说明 | `#8` 行用途补 `要素登记/变更(不只是回执)`;`#7` 行补 `响应体含 matchVerdict[];平台核验结论只走响应体,不推送`;新增一行:`\| 版本坐标系 \| — \| — \| match_version(平台:随 PL_TASK_CHANGE.version 递增)与 local_version(公寓:本地修订序号,充当 #8.bizVersion)**语义不同,不可跨比较** \| 同 docs/公寓端/03-数据设计.md 表 3-15 \| 混比会得出错误的“谁更新”结论 \|` | 两个版本号是不同坐标系——一个是"平台核验到第几版",一个是"本地改了第几版"。不写明会被实现方当成同一序列比大小,直接导致"本地改动被平台旧结论覆盖" |
### 4.2 `docs/平台端/05-跨端接口对齐.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-13** | §3 表 5-1,`#7`/`#8` 两行 | 补充 | `\| 6 \| 下行 \| GET /platform/v1/plans/duty?gyId&from&to \| 乘务计划(替代本地抓运安) \|…`<br>`\| 8 \| 上行 \| POST /platform/v1/ack/tasks \| 任务接收回执(含复算结果) \|…\| idempotencyKey(含 bizType=TASK_ACK) \| 表 6-1 #8 \|` | `#8` 用途列补 `+ 公寓叫乘要素登记/变更`;落表列补 `PL_TASK(match_state / local_version 更新)`;`#7` 用途列补 `;响应体含 matchVerdict[]` | 与 R-04/R-05 同口径。`docs/平台端/05` 是"20 接口逐条对齐表",是阶段二签认的凭据;上位改了而这张表不改,签认结果就与契约不一致 |
| **R-14** | §3 表 5-3(幂等键) | 补充 | `idempotencyKey = hash(gyId + bizType + bizId + bizVersion)`(四分量公式,未指明任务类的取值) | 补注:`任务类:bizType = TASK_ACK;bizId = taskId(**公寓分配**);bizVersion = local_version(首次 1,每次要素/时刻变更 +1)。注意:local_version 与 match_version **不可互相代入**` | 四分量只给公式不给取值,实现方会各自选择 `bizId`/`bizVersion` 的实际来源,结果是"同一次变更生成两个键"或"两次变更撞成一个键"——两种都会让幂等失效且难复现 |
| **R-15** | §5 表 5-6(对账三维)`count` 行 | 修订 | `\| count \| PL_RECEIVE_LOG 当日去重后条数(按 biz_type) \| 与公寓 count 不等 \| 生成 MISSING / EXTRA 差异,进人工队列 \|` | `\| count \| PL_RECEIVE_LOG 当日**按 biz_id(= task_id)去重**后的条数(按 biz_type) \| 与公寓侧 distinct task_id 基数不等 \| 差集进人工队列;**任务类不适用 sum(amount)**——任务无金额,强行算恒为 0 \|` | 变更上报每次 `local_version+1` 会产生**新的** `idempotencyKey`。按 `idempotencyKey` 去重会把同一任务的多次变更计成多条,与公寓侧基数**永远不等**(表 1-24 冲突-C)。这个口径对 `LEDGER` 同样适用,但 `LEDGER` 的修订需另立一轮(影响面超出本议题) |
| **R-16** | §5 表 5-10(逐条核对)`#6`/`#7`/`#8` 行 | 补充 | `\| 6 \| ✔ \| ✔ 下行 \| ✔ planId \| ✔ GY_SYN_JCXX / PL_TASK \| 对齐(pickupMode 取值域待 F-07) \|`<br>`\| 8 \| ✔ \| ✔ 上行 \| ✔ bizType=TASK_ACK \| ✔ PL_TASK_ACK \| 对齐(TASK_ACK 列注释缺,对端 G-08) \|` | `#6` 结论补:`pickupMode 与 jcfs 按“同义”处理,取值域仍待 F-07`;`#7` 结论补:`响应体 matchVerdict 待 F-14 冻结(docs/公寓端/09 表 9-18)`;`#8` 结论补:`扩字段后须同步补 biz_type 注释(G-08)` | 结论列是"逐条签认"的结果记录。上位扩了字段而结论列不更新,签认就等于对旧契约签的字 |
### 4.3 `docs/平台端/06-关键流程.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-17** | §3 触发段 + 时序要点 | 补充 | `平台侧 platform-worker 定时…调用运安 SOAP 取乘务计划 → 落 GY_SYN_JCXX 快照 → 按规则版本生成 / 更新 PL_TASK 并追加 PL_TASK_CHANGE` | 改为:`平台侧 platform-worker 定时…调用运安 SOAP 取乘务计划 → 落 GY_SYN_JCXX(**匹配要素快照**)。**收到 #8 要素登记后**,按 cardId + 日期 + 车次匹配运安,补 kcsj / jcfs → 更新 PL_TASK.match_state 并追加 PL_TASK_CHANGE(op=UPSERT,version+1)`。时序要点新增一条:`平台匹配补全结果**必须**回写 PL_TASK_CHANGE——这是平台新产生的事实,不回写公寓就拉不到;公寓上报的原值**不得**原样回写,那才是真正的回环` | 原文把"生成 / 更新 `PL_TASK`"放在定时任务里,读起来像"平台定时生成任务"。真实链路的触发是 `#8`:没有公寓上报的要素,平台不知道要匹配谁 |
| **R-18** | §4 图 6-3 + 时序要点 5 | 修订 | 图 6-3:`IG->>PLAN: 写 PL_TASK_ACK(computedCallTime vs 平台 jcsj)`、`PLAN->>PLAN: 任务状态 PLANNED → ACKED`<br>要点 5:`回执差异可见:PL_TASK_ACK.computedCallTime 与平台 jcsj 的差值超阈值进 GET /tasks/diff-report` | 图 6-3 改为:`IG->>PLAN: 写 PL_TASK_ACK(computedCallTime vs 平台匹配要素派生的基线 jcsj)`、`PLAN->>PLAN: PL_TASK.status UNMATCHED / ACKED(公寓来源任务自 ACKED 起)`;图注补:`matchVerdict 只在 #7 **响应体**中返回,不是平台发起的推送`。要点 5 改为:`回执差异可见:PL_TASK_ACK.adjust_delta_ms(= 本地 jcsj − 平台匹配要素按规则派生的基线)超阈值进 GET /tasks/diff-report;复算差异另记 diff_ms` | R-09 已删除 `PL_TASK.jcsj`,原文的 `vs 平台 jcsj` 失去所指。且 `diffMs`(复算差异)与 `adjust_delta_ms`(本地主动调整量)是两个口径,原文混用会让差异报表无法归因 |
### 4.4 `docs/平台端/01-功能边界.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-19** | §3 表 1-1 末行 + §4 表 1-4「任务生成」行 | 修订 | 表 1-1 末行:`\| 谁决定叫乘时刻 \| 给“计划 + 规则版本”(D8) \| **本地复算**并回执 computedCallTime \| 只能人工改写单条任务,不能改规则 \|`<br>表 1-4:`\| 任务生成(计划 → 叫乘时刻) \| ●(给规则)/ ○(复算) \| 平台给“计划 + 规则版本”,公寓复算;差异以平台为准并告警(D8) \|` | 表 1-1 末行改为:`\| 谁决定叫乘时刻 \| 给**运安要素**(kcsj / jcfs)+ 时间规则版本(D8') \| **算 jcsj** 并回执 computedCallTime \| 只能对单条任务做**核验裁决**(ACCEPT / REJECT),不能改规则、不能改 jcsj \|`<br>表 1-4 拆成两行:`\| 任务行产生 \| ○(仅接受登记) \| ●(入寓登记当场建行;taskId 由公寓分配) \| 不产生任务 \|` 与 `\| 任务要素补全(匹配 → kcsj / jcfs) \| ● \| —(只读消费) \| — \|` | ①"给计划"含 `jcsj` 歧义,与 R-01 同源;②"任务生成"这一行让人以为平台生成任务;③"只能人工改写单条任务"在 `docs/公寓端/04-接口设计.md` 表 4-9 里**没有落点**——`correct` 白名单不含 `jcsj`/`kcsj`/`jcfs`,五条 `commands` 无一条改时刻(表 1-24 冲突-E)。上位承诺的能力在接口层不存在 |
### 4.5 `docs/平台端/09-待决策与风险.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-20** | §2 表 9-1 末尾 + §5 说明 | 补充 | `P-01`~`P-11`,共 11 条;§5:「同一个问题在 `docs/10`(原文简写,应为 `docs/backend/10`)已有 D 编号的,本篇一律“映射 + 下游风险”,**不新增决策点**。因此 P 系列只有 11 条」 | 表 9-1 末尾增一行说明(**不新增 P 编号**):`\| — \| 叫乘要素匹配职责与任务主键 \| **不在本篇新开 P 编号**:裁决权在跨端 D17(docs/backend/10 表 10-2)与 F-14(docs/公寓端/09 表 9-18);本篇只承接下游风险(PL_TASK 主键落法、match_state 语义) \| 阶段二 S2.1' 前 \|` | `P-01~P-11` 都是平台侧**实现形态**类决策(工程形态 / 前端归属 / HA / 命名空间)。本议题裁决权在跨端,单开 `P-12` 会出现"同一件事两个决策入口",并违反本篇自己定下的"不新增决策点"规则 |
---
## 5. 批次 C:`docs/公寓端/` 七篇
### 5.1 `docs/公寓端/03-数据设计.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-21** | §1 表 3-2(计划与任务域) | 修订(改结构) | `计划与任务域(3 张,02_plan_task.sql)`;行:`\| gy_syn_jcxx \| apartment \| apt-master-client(计划下行落库) \| apt-dispatch(生成任务)、管理端 \| 否 \| 是 \|` | 表头改为 `计划与任务域(4 张,02_plan_task.sql + 08_local_task.sql)`;新增一行:`\| apt_local_task \| apartment \| apt-dispatch(受控命令)、apt-report(只读上报态) \| apt-dispatch、管理端、值班台 \| 否 \| 是 \|`;表下补注:`**gy_syn_jcxx 是运安匹配要素表(提供 kcsj / jcfs),不是叫乘任务的生成源**。任务行由 gy_jcxx(入寓登记当场创建)产生;apt_local_task 只存治理信息(origin / local_version / override_* / verdict_*),不重复存要素与状态` | 现表述"计划下行落库 → 生成任务"把 `gy_syn_jcxx` 定位成任务来源,与 `docs/backend/05-数据模型.md` §3.1「运安计划快照」冲突(表 1-24 冲突-G)。按前者理解会重犯"任务由计划表生成"的错误,并导致在该表写本地占位行 |
| **R-22** | §2 表 3-8(命名分区) | 修订 | `apt_* 23 / gy_*·gygl_* 18 / duty_* 3 / sys_* 30`,合计 `44 张业务表` | `apt_*` 23 → **24**(新增 `apt_local_task`);业务表 44 → **45**。**必须同批修改** `docs/backend/10-架构优化方案-三端拆分.md` 表 5-9 第 1 项为:`45 张业务表(APT_* 24 + GY_* / gygl_* 18 + duty_* 3)+ 30 张若依 sys_* 表` | 新增治理表不进表数口径,会让 `docs/backend/10` 表 5-9、`docs/平台端/03-数据设计.md` §1.3(引用该口径)、`docs/开发计划/` 三处同时不一致。这两处是**成对修改**,不可只改一处 |
| **R-23** | §6 表 3-15(字段约定)版本行 | 补充 | `\| 版本 \| source_version(快照)、config_version(分机)、state_version(房态)、biz_version(叫乘) \| 语义不同,不可混用 \|` | 补两项:`、local_version(叫乘本地修订序号,= #8.bizVersion)、match_version(平台匹配结论版本,只读消费)` | 本议题新增两个版本号,不登记就会出现"同一个任务两个版本值谁大"的判断错误。`match_version` 与 `local_version` 尤其危险——见 R-12 |
| **R-24** | §6 表 3-16 + §5(`docs/公寓端/08` 表 8-9) | 新增(**挂载点已纠正**) | 表 3-16 只到 `G-07`;表 8-9 只到 `G-07`。`G-08`/`G-09` 实际在 `docs/公寓端/09-平台接口约定.md` 表 9-20 | **`G-10` 的正式落点是 `docs/公寓端/09-平台接口约定.md` 表 9-20 续(紧接 `G-09`)**,内容:`\| G-10 \| gy_jcxx 缺 local_version / origin / override_* \| 本轮不改存量表;以侧表 apt_local_task 规避;**锁定语义已由 gy_jcxx.jllx 承担,不再新增锁定列** \| 阶段二 S2.2' 建表清单定稿前 \|`。<br>同时在 `docs/公寓端/03-数据设计.md` 表 3-16 与 `docs/公寓端/08-待决策与风险.md` 表 8-9 各补一行交叉引用:`\| G-10 \| 见 docs/公寓端/09-平台接口约定.md 表 9-20 \|` | **这是对表 1-20 第 24 行与表 1-22 `G-10` 行挂载点的纠正**:那两处写的"挂 `03` 表 3-16 与 `08` 表 8-9"不可执行——两张表都只到 `G-07`,把 `G-10` 接在 `G-07` 后面会断号(`G-08`/`G-09` 在另一篇)。正确做法是续写在 `G-09` 之后,另两篇只放交叉引用 |
### 5.2 `docs/公寓端/04-接口设计.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-25** | §2 表 4-1(RESTful 约定)「受控命令」行 | 补充 | `\| 受控命令 \| POST \| /apt/{域}/{id}/commands \| **否**,必须带 commandId \| POST /apt/jcxx/88/commands \|` | 备注补:`受控命令分两类:**状态迁移类**(STOP / RE_CALL / FORCE_FINISH / MARK_LEAVE / CANCEL / HANGUP,经 apt.duty.state)与**受控但非状态迁移类**(ASSIGN_ELEMENT / ADJUST_TIME / LOCK_MATCH / UNLOCK_MATCH,改叫乘要素与时刻,**不迁移状态**)。两类都必须带 commandId 幂等;ASSIGN_ELEMENT / ADJUST_TIME 的 reason 必填` | `表 4-9` 现有五条命令全是状态迁移类。把"改开车时刻"硬塞进 `correct` 或状态命令,会破坏"状态字段只经 `apt.duty.state` 单写入口"的纪律——这正是 `docs/backend/10` 风险清单第 7 行要防的 |
| **R-26** | §2 表 4-4(错误码)`409xx` 段 | 新增(改结构) | 错误码段 `40001`~`50301`,`409xx` 段止于 `40908 APT_CONF_BUSY`,**无** `40909`~`40912` | 在 `40908` 后追加四行:<br>`\| 40909 \| APT_PERSON_NOT_IN_HOUSE \| 人员当前不在寓(无有效 gy_jcxx 行:finish_ly='0' 且 sjlysj is null 不成立) \| 先走入住登记;**不存在“为某人建 jcxx 行”的接口** \|`<br>`\| 40910 \| APT_TIME_ILLEGAL \| 时刻关系非法(违反 jcsj ≤ fjsj ≤ jcbjsj < kcsj)或任务已开始执行(ryzt ≥ 1 / finish_jc='1') \| 检查时间顺序;已执行任务不允许调整 \|`<br>`\| 40911 \| APT_MATCH_LOCKED \| 任务已锁定(jllx='1'),再发 ASSIGN_ELEMENT 被拒 \| 先 UNLOCK_MATCH \|`<br>`\| 40912 \| APT_CARD_ID_MISSING \| gy_jcxx.card_id(工号)为空,无法参与运安匹配 \| 补工号后重试;要素仍可本地生效,但平台侧恒为 UNMATCHED \|` | 这四类失败是"本地建单"路径的**必经分支**。没码只能返 400/500,前端无法分支提示,`docs/公寓端/07` 表 7-3 的验收用例也无法断言(表 1-23 的九条负面用例全部依赖这四个码) |
| **R-27** | §3 表 4-9(叫乘接口)新增第 7、8 项 | 新增(改结构) | 表 4-9 共 6 项;第 4 项 `correct` 改非状态字段;第 5 项 `commands` 五条 = `STOP`/`RE_CALL`/`FORCE_FINISH`/`MARK_LEAVE`/`CANCEL` | 追加两行:<br>`\| 7 \| POST \| /apt/jcxx/{jcid}/commands \| 叫乘受控命令(管理端):ASSIGN_ELEMENT / ADJUST_TIME / LOCK_MATCH / UNLOCK_MATCH \| apt:jcxx:assignElement、apt:jcxx:adjustTime、apt:jcxx:lockMatch(按 command 分支鉴权) \| commandId、command、reason(ASSIGN_ELEMENT / ADJUST_TIME 必填)、jlid / cc / jch / kcsj / jcfs(ASSIGN_ELEMENT)、jcsj(ADJUST_TIME) \| local_version、matchState、重算后的 jcsj / fjsj / jcbjsj / ylysj \| L2 \|`<br>`\| 8 \| POST \| /duty/jcxx/{jcid}/commands \| 同上(值班台子集):ASSIGN_ELEMENT / ADJUST_TIME \| duty:jcxx:adjustTime 或复用 apt:jcxx:changeStatus(见 Q-15) \| 同上 \| 同上 \| L2 \|` | 现有六项里,`correct` 白名单不含 `jcsj`/`kcsj`/`jcfs`,`commands` 五条全是状态迁移——**"改开车时刻/接车方式"和"调叫乘时刻"在新接口层没有任何落点**,而旧系统 `/jcgl/modifyJcXx` 已能做(冲突-E)。这是本议题的核心缺口 |
| **R-28** | §3 表 4-9 `correct` 白名单(禁改清单) | 补充 | 白名单:`commandId`、`reason`、`fjh`/`jlid`/`cc`/`leaveRemark`;禁改清单:`ryzt` / `finish_jc` / `finish_ly` / `stoped` → `**否**` → `只能经第 5 个接口` | 禁改清单追加一行:`\| jcsj / kcsj / jcfs / fjsj / jcbjsj / ylysj \| **否** \| 改开车时刻/接车方式走第 7 项的 ASSIGN_ELEMENT(触发派生重算);只调叫乘时刻走 ADJUST_TIME;**派生时刻不可独立覆写** \|` | 不显式写"不可改",实现方会把它加进 `correct` 白名单,形成"基准 + 派生"两套事实源,`computedCallTime` 就无法作为可复算的证据。且 `fjsj`/`jcbjsj`/`ylysj` 与 `jcsj` 是纯函数关系,逐项写必然漂移 |
| **R-29** | §4 表 4-10(叫乘域映射)权限点 | 修订 | 权限点列:`apt:jcxx:list` / `query` / `correct` / `changeStatus` | 补 `apt:jcxx:assignElement`、`apt:jcxx:adjustTime`、`apt:jcxx:lockMatch`;值班台侧补 `duty:jcxx:adjustTime`(或复用 `apt:jcxx:changeStatus`,待 `Q-15` 定) | 权限点不落,新接口只能复用 `changeStatus`,等于把"改时刻"和"改状态"授予同一批人,与 `docs/设计变更/01-本地建单与本地调整.md` 表 1-7 的角色矩阵冲突(值班员可改时刻、不可改状态) |
### 5.3 `docs/公寓端/05-关键流程.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-30** | §1 标题 + §1 前置新增小节 + 图 5-1 mermaid | 新增小节 | 标题:`计划下行与叫乘任务生成`;mermaid:`E->>DB: 按 apt_master_rule 复算 jcsj/fjsj/jcbjsj`、`E->>DB: 生成/更新 gy_jcxx(ryzt=0 待叫乘)` | ① 标题改为 `**叫乘信息的产生与叫乘任务生成**`;② 在 §1 之前新增 `### 1.1 叫乘信息的产生与匹配`,内容为四步链路:`入寓登记当场建 gy_jcxx 行(submit_rydj → insertJcxx,行由此产生)` → `#8 上报要素(cardId / cc / kcsj / jcfs)` → `平台按 cardId + 日期 + 车次匹配运安,补 kcsj / jcfs,写 PL_TASK_CHANGE` → `公寓拉 #7 增量,按本地 GY_SJGZ 算 jcsj / fjsj / jcbjsj / ylysj`(图与表**引用** `docs/设计变更/01-本地建单与本地调整.md` 图 1-1、表 1-5,不复制);③ mermaid 中 `生成/更新 gy_jcxx` 改为 `为已有 gy_jcxx 行补要素并排任务(**不新建行**)` | 现标题"计划下行与任务生成"暗示任务由下行计划生成,与"行由入寓登记产生"相反。不新增 §1.1,读者会先按 v1 的错误模型读完后面全部内容,再难纠正 |
| **R-31** | §4 表 5-7(干预动作) | 新增行 | 表 5-7 五个动作:`STOP` / `RE_CALL` / `FORCE_FINISH` / `MARK_LEAVE` / `HANGUP` | 追加三行:<br>`\| ASSIGN_ELEMENT \| 允许态:finish_ly='0' 且 jllx='0'(已锁定须先 UNLOCK_MATCH) \| 改 jlid / cc / jch / kcsj / jcfs,置 jllx='1',重算派生,local_version+1,#8 上报 \| apt_local_task、duty_manual_action、duty_operation_log \|`<br>`\| ADJUST_TIME \| 允许态:ryzt=0 且 finish_jc='0' \| 只改 jcsj;重算 fjsj = jcsj + 10min、jcbjsj = jcsj + 15min、ylysj(路径 A 不变 / 路径 B = jcsj + lysjgz);local_version+1,#8 上报 \| 同上 \|`<br>`\| LOCK_MATCH / UNLOCK_MATCH \| 管理员权限 \| 改 gy_jcxx.jllx(1 / 0);解锁后回归平台匹配池,local_version+1 \| 同上 \|` | 表 5-7 是值班台/管理端动作的**权威清单**。不含新动作,前端与后端无法对齐"哪些按钮存在",`docs/公寓端/07` 表 7-2 也无法引用它做验收 |
### 5.4 `docs/公寓端/07-实施计划与验收.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-32** | §3 表 7-2 `M3` 行 | 补充 | `\| **M3** \| 叫乘信息管理 \| apt-dispatch 列表/详情/时间线/受控命令 \| M1、M2… \| 状态机 apt.duty.state \| 只读字段不可改;STOP/RE_CALL 状态迁移正确且留审计;非法状态返回 40902 \|` | 交付物补:`+ apt_local_task(含 UK(gyid,task_id) 与 override_* / verdict_*)+ 四类命令(ASSIGN_ELEMENT / ADJUST_TIME / LOCK_MATCH / UNLOCK_MATCH)+ #8 要素登记 + #7 matchVerdict 消费`;验证结果补:`调整后派生时刻正确(fjsj = jcsj + 10min、jcbjsj = jcsj + 15min);local_version 单调递增;锁定后平台不覆盖` | `M3` 是叫乘信息管理的唯一批次。不补交付物,本议题的两条核心诉求(本地建单 / 本地调整)在排期上**不存在** |
| **R-33** | §3 表 7-3(M3 验证项) | 新增行 | 表 7-3 `M3` 四条:状态字段不可改 / `STOP` 幂等 / 状态非法 `40902` / 时间线 | 追加四条:<br>`\| 5 \| 指定要素成功 \| 未匹配人员指定 cc / kcsj / jcfs → 派生时刻按路径 B 算出且满足 jcsj ≤ fjsj ≤ jcbjsj < kcsj;jllx='1';#8 outbox 转 SENT \|`<br>`\| 6 \| 未入寓被拒 \| 无有效 gy_jcxx 行请求指定要素 → 40909;card_id 空 → 40912 \|`<br>`\| 7 \| 调整后派生正确 \| jcsj 调 20 分钟 → fjsj / jcbjsj 随动;路径 B 下 ylysj 随动、路径 A 下 ylysj 不变;local_version +1 \|`<br>`\| 8 \| 锁定后平台不覆盖 \| 已锁任务收到 MATCHED(要素不同)→ 本地不动,#8 accepted=false、reason=LOCAL_MATCH_LOCKED,平台 PL_TASK_ACK.match_locked=1 \|` | 表 7-3 是 `M3` 的验收清单。缺这四条,"本地建单 / 本地调整"在验收上不可见——开发做完了也无法证明 |
| **R-34** | §4 表 7-5 第 3 项(准点率) | 修订 | `\| 3 \| 准点率 \| 不低于现网 \| 对比 jcsj_s - jcsj 分布 \|` | `\| 3 \| 准点率 \| 不低于现网,且**按 matchState 分三组 + 合计**给出 \| 对比 jcsj_s − jcsj 分布;分组:MATCHED(匹配到未锁)/ UNMATCHED_LOCAL(未覆盖、本地要素)/ LOCKED(本地指定并锁定);三档阈值 ≤30s / 30~120s / >120s 待 Q-15 冻结 \|` | 不分组的合计无法区分"运安匹配问题"还是"现场指定问题",也无法评估本地指定的采纳率是否健康。且排除本地指定会激励"把难叫的人标成本地要素"来刷指标(古德哈特定律) |
### 5.5 `docs/公寓端/08-待决策与风险.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-35** | §2 表 8-1,`D7` / `D8` 两行 | 修订 | `D7`:`平台维护模板,公寓可“应急新增”并上报确认 \| 中 \| 本期不做本地应急新增(受 G-04 约束);只做查询、停用/启用、复算校验 \| M1`<br>`D8`:`平台下发“计划 + 规则版本”,公寓端复算执行,不一致告警 \| 中 \| 以平台计划为准、本地按 apt_master_rule 复算;差异超阈值告警并回执 computedCallTime \| M1` | `D7` 默认口径改为:`**本期做**:本地“应急新增” = 对既有 gy_jcxx 行指定要素并锁定(jllx='1'),落 gy_jcxx 既有列 + 侧表 apt_local_task,**不写交路快照表**(apt_master_route 无 source 列,G-04 约束仍在);平台确认后由 platform-route 版本化为正式模板` → 批次由 `M1` 改为 `M3`。<br>`D8` 默认口径改为:`**要素以平台匹配为准,执行时刻恒由公寓算**:按 apt_master_rule 复算 jcsj 及派生;jllx='1' 锁定任务本地要素优先;match_state=UNMATCHED 时保留本地要素不清空;差异超阈值告警并回执 computedCallTime` | `D7` 现文"本期不做"与上位 `docs/backend/10` 表 10-2 `D7`"可应急新增"**直接抵触**(冲突-D),合起来等于永久不做。`D8` 现文"以平台计划为准"仍把 `jcsj` 划给平台,与 R-01 同源 |
| **R-36** | §5 表 8-9 | 补充(**交叉引用,非新增 `G-10`**) | 表 8-9 共 7 行:`G-01`~`G-07` | 追加一行交叉引用:`\| G-10 \| gy_jcxx 缺 local_version / origin / override_* \| 见 docs/公寓端/09-平台接口约定.md 表 9-20 \|` | 见 R-24:`G-10` 的正式落点在 `09` 表 9-20 续(`G-08`/`G-09` 之后)。本表只到 `G-07`,直接续写会断号 |
| **R-37** | §6 表 8-10 | 新增 | `Q-01`~`Q-14`,共 14 条 | 追加一行:`\| Q-15 \| 三个验收参数与权限点前缀未固定 \| ① 调整幅度分级阈值(建议 30 分钟:\|Δ\| ≤ 30min 一次确认、> 30min 二次确认);② 准点率三档阈值(建议 ≤30s / 30~120s / >120s);③ 值班台权限点用 duty:jcxx:adjustTime 还是复用 apt:jcxx:changeStatus \| 三项都必须有值才能验收,现有文档从未固定 \| 阶段三 S3.2(M3 批次)开工前 \|` | 这三项不列出,实现方会自行猜值,验收时无法判定通过与否(`docs/公寓端/07` 表 7-5、`docs/backend/10` 表 9-4 都只说"不低于现网") |
| **R-38** | §7 表 8-11(决策 × 批次) | 修订(改结构) | `\| D7 交路应急新增 \| \| □ \| \| \| \| \| \|`(M1 弱);`\| D8 时间以谁为准 \| \| ■ \| \| \| \| \| \|`(M1 强) | `D7` 行由 `M1 □` 提为 **`M3 ■`**;`D8` 行保持 `M1 ■` 并补注 `按 D8' 全文执行,见 docs/backend/10 表 10-2` | `D7` 现在是"本期不做",所以 `M1` 只表示"知道不做";改为"本期做"后,它的落点就是 `M3` 的 `ASSIGN_ELEMENT`,变成 `M3` 的强阻塞项。矩阵不更新会漏排(`D7` 的补注也就要等到 M3 才拍板,而它偏偏是 M3 的前置) |
### 5.6 `docs/公寓端/09-平台接口约定.md`
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-39** | §6 表 9-8 第 6、7 行 | 补充 | 第 6 行:`\| 6 \| 复算产物 \| gy_jcxx.jcsj / fjsj / jcbjsj \| 由 apt_master_rule + apt_holiday 计算,**不取平台值**(D8) \|`<br>第 7 行:`\| 7 \| taskId \| gy_syn_jcxx.plan_key \| 与 #6 同一业务键空间 \|` | 第 6 行补:`;补充:taskId 分配方为**公寓**(= jcid 业务编码),平台只接受、不生成`<br>第 7 行改为:`\| 7 \| taskId / matchedPlanKey \| taskId = 公寓分配的任务业务键(= jcid 业务化编码);matchedPlanKey = 平台匹配到的 gy_syn_jcxx 计划键,**可空** \| **不是同一键空间**(F-08):未匹配任务只有 taskId,没有 planKey \|` | 第 7 行现文"`taskId` = `gy_syn_jcxx.plan_key`"在"运安未匹配"场景下直接把 `taskId` 变成空值——这是冲突-A 的**真正根源**(表 1-24 只记了症状:`docs/backend/05` §2.1 主键 vs `公寓端/05` 表 5-1)。修正此处,冲突-A 自动消解 |
| **R-40** | §7 表 9-10 `#8` 行 | 修订 | `\| 8 \| /platform/v1/ack/tasks \| TASK_ACK \| 本次下行 #6/#7 的复算结果 \| 收到计划/变更后立即 \| 记 accepted;被拒则回滚本地任务并告警 \|` | 数据来源列改为:`**公寓入寓登记产生的 jcxx 要素与本地指定/调整**(不只是“#6/#7 的复算结果”)`;触发时机列改为:`要素生效后下一个上报周期内发出;断网时进 outbox,恢复后按 watermark asc 续传` | 原表述把 `#8` 定义成"下行的回执",但平台匹配的**输入**正是 `#8`。不先有 `#8`,`#6`/`#7` 就没有可回执的内容——这是循环依赖,会把"本地建单"卡死在第一步 |
| **R-41** | §7 表 9-11(payload 字段) | 新增行 | 表 9-11 字段清单,**无** `origin` / `localVersion` / `cardId` / `cc` / `jch` / `kcsj` / `jcfs` / `overrideReason` | 追加 8 行:`origin`(必填,来源枚举)、`localVersion`(必填,long)、`cardId`(必填,**匹配键**)、`cc` / `jch`(选填,匹配键)、`kcsj`(选填,公寓已知开车时刻,空 = 待平台补全)、`jcfs`(选填)、`overrideReason`(指定/调整时必填,≤512) | 不补这 8 个字段,平台**无法匹配**(缺 `cardId`/`cc`),也无法区分"本地主动改"与"复算一致"(缺 `origin`/`localVersion`/`overrideReason`)。这是 `#8` 从"回执"升级为"要素登记"的实质内容 |
| **R-42** | §7 表 9-12(`#8` 回执字段) | 补充 | `\| taskId \| gy_syn_jcxx.plan_key \| —— \|`;`\| accepted \| true 除非复算失败/版本过期 \| … \|`;`\|(可选)diffMs \| computedCallTime - 平台 jcsj \| 超阈值(建议 60s)本地同时告警 \|` | `taskId` 来源改为 `公寓分配的 taskId(= jcid 业务编码)`;新增 `matchedPlanKey` 一行(`平台匹配到的运安键,可空`);`accepted` 补 `false 的两种原因码:LOCAL_MATCH_LOCKED(本地已锁定,平台要素不覆盖)、VERSION_STALE(bizVersion 落后,平台返 PL-VERSION-STALE)`;`diffMs` 定义改为 `computedCallTime − 平台匹配要素按规则派生的基线`,并新增 `adjustDeltaMs`(`本地主动调整量`) | `taskId = plan_key` 在未匹配场景下不成立(R-39)。两种 `accepted=false` 不区分,平台无法判断"该人工裁决"还是"该让公寓重报";`diffMs` 与 `adjustDeltaMs` 混用会让差异报表无法归因 |
| **R-43** | §8 表 9-15(对账三维)`count` 行 | 修订 | `\| count \| gy_jcxx 当日迁移次数 / gy_ykt_* 当日笔数 \| 平台已接收条数 \| 差集列入“待人工处理” \|` | `\| count \| gy_jcxx 当日要素变更的 **distinct task_id** 基数 / gy_ykt_* 当日笔数 \| 平台 PL_RECEIVE_LOG 当日**按 biz_id 去重**后的条数 \| 差集列入“待人工处理”;**任务类不适用 sum(amount)** \|` | 与 R-15 同口径(冲突-C)。**两侧必须同时改**——只改平台侧或只改公寓侧,对账恒不等且无法定位哪侧错 |
| **R-44** | §9 表 9-18(F 系列冻结清单) | 新增(改结构) | `F-01`~`F-13` 共 13 条;其中 `F-08`:`F-08 计划与任务的业务键是否统一用 planId / taskId \| #6、#7、#8 \| 两套键导致重复任务` | ① `F-08` 结论改为:`**不统一**:主键用 taskId(公寓分配),匹配键用 matchedPlanKey(可空)`;② 追加:`\| F-14 \| 任务主键分配方、matchVerdict 语义、两版本不可跨比较 \| #7、#8 \| ① taskId 由公寓分配(= jcid 业务编码),matchedPlanKey 只作匹配键、可空;② matchVerdict[].verdict 取 MATCHED / UNMATCHED / REJECTED,且必带 localVersion 防串版;③ match_version 与 local_version 不可跨比较 \| 未冻结则实现方各自定义,#8 幂等键会漂移 \|` | `F-01`~`F-13` 是阶段二**逐条签认**的冻结清单(`docs/平台端/05-跨端接口对齐.md` 表 5-10 同口径)。不进清单就等于没冻结;`F-08` 只写"是否统一"不给结论,签认时无结论可签 |
| **R-45** | §1 表 1-4 第 1、3 行(`docs/公寓端/01-功能边界.md`) | 补充 | 第 1 行:`\| 1 \| 交路与计划 \| **是**(快照只读 + 本地复算校验;本地应急新增受限) \|…`<br>第 3 行:`\| 3 \| 叫乘执行与状态机 \| **是**(最小闭环:任务 → Quartz → AMI → 会议桥 → 状态机) \|…` | 第 1 行「本地应急新增受限」改为:`本地应急新增 = 对既有 gy_jcxx 行指定要素并锁定,落既有列 + apt_local_task 侧表,**不改交路快照表**`<br>第 3 行能力列补:`+ 本地指定要素 / 调整时刻(ASSIGN_ELEMENT / ADJUST_TIME)` | `"受限"`一词无法指导实现(是"不能做"还是"有限度地做"?)。第 3 行不含本地建单/调整,会让"叫乘执行"这个功能域漏掉本议题的两条核心诉求,导致范围评审时被判定"不在本期" |
---
## 6. 批次 D:`docs/开发计划/`(**可选、非必须**)
| 编号 | 位置 | 类型 | 原文口径 | 修订后口径 | 修订理由 |
| --- | --- | --- | --- | --- | --- |
| **R-51** | ① `docs/开发计划/README.md` 表 0-3 阶段三行<br>② `docs/开发计划/02-逐阶段计划-阶段三至五.md` §1 的 `S3.2`<br>③ `docs/开发计划/03-汇总表-产出验证与排期.md` 表 3-8(表 C)`#8` 行<br>④ `docs/开发计划/04-决策点与口径冲突.md` 表 4-1 | 补充 | 表 0-3 阶段三细化步骤:`S3.1 公寓端 M0 基座 → S3.2 M1~M4(交路/房间/叫乘/人员)→ …`;表 C `#8` 行按"任务接收回执"排期 | ② `S3.2` 内容补:`叫乘受控命令(ASSIGN_ELEMENT / ADJUST_TIME / LOCK_MATCH / UNLOCK_MATCH)+ apt_local_task 建表与 DDL + #8 要素登记字段 + #7 matchVerdict 消费`;③ 表 C `#8` 行用途改"要素登记/变更",并补一行"新增字段的契约冻结落在阶段二 `S2.1'`";④ 决策表补 `D17`(阶段二 `S2.1'` 前)、`F-14`(阶段二契约冻结签字时)、`G-10`(阶段二 `S2.2'` 建表清单定稿前)、`Q-15`(阶段三 `S3.2` 开工前)四个挂载点 | 计划是排期依据。不挂则 `M3` 批次的实际工作量被低估(四类命令 + 一张表 + 两处字段扩展),决策表的拍板时点也没有对应条目。**可选,因为不影响设计文档的正确性**;但若跳过,`S3.2` 的准入判据会与实际不符 |
---
## 7. 批次 E:表 1-24 冲突 A~H 转可执行条目
> 表 1-24 的八个冲突在 `docs/设计变更/01-本地建单与本地调整.md` 中**只登记、未处理**。本节把它们转成"改哪一篇、改成什么"。另补本轮新发现三条(`R-C9`~`R-C11`)。
### 7.1 逐条裁定
| 编号 | 冲突 | 甲侧出处与原文 | 乙侧出处与原文 | 裁定(唯一口径) | 落地条目 |
| --- | --- | --- | --- | --- | --- |
| **R-C1** | A:`taskId` 取谁 | `docs/backend/05-数据模型.md` §2.1:`GY_JCXX` 主键为 `jcid`(入住记录 ID) | `docs/公寓端/05-关键流程.md` 表 5-1 第 4 行:`gy_jcxx.jcid`(**= 计划侧业务键**) | **`taskId` = `jcid` 的业务化编码,行由入住产生。**`jcid` 是入住记录 ID,**不是**"计划侧业务键"——乙侧括注错误 | R-39(消解根源:`09` 表 9-8 第 7 行)、R-44(`F-14` 冻结)、R-C9(同处措辞) |
| **R-C2** | B:`id` 还是 `jcid` | `docs/公寓端/03-数据设计.md` 表 3-9:条件更新写作 `where id=? and ryzt=?` | `docs/backend/05-数据模型.md` §2.1 主键 `jcid`;`docs/公寓端/04-接口设计.md` 表 4-9 出参同时给 `id` 与 `jcid` | **`jcid` 作业务键、`id` 作代理键**;条件更新里的 `id` 指代理键。DDL 时以实际表结构为准。无需改结论,只需在 `03` 表 3-9 该行补注 | R-C2 自身:`docs/公寓端/03-数据设计.md` 表 3-9 该行备注补 `(id 为代理键;业务键为 jcid,跨端与 #8 一律用 jcid)` |
| **R-C3** | C:`count` 去重维度 | `docs/平台端/05-跨端接口对齐.md` 表 5-6 + `docs/公寓端/09-平台接口约定.md` 表 9-15:`count` = "去重后条数"(**未说明按什么去重**) | `docs/平台端/05` 表 5-3:变更上报 `bizVersion+1` 产生**新的** `idempotencyKey` | **按 `biz_id`(= `taskId`)去重**。按 `idempotencyKey` 去重会把同一任务的多次变更计成多条,与公寓侧基数永远不等 | R-15(平台侧)、R-43(公寓侧)——**两侧必须同批改**。`LEDGER` 的同类问题另立一轮,不在本清单 |
| **R-C4** | D:`D7` 上下位抵触 | `docs/backend/10-架构优化方案-三端拆分.md` 表 10-2 `D7`:`平台维护模板,公寓可“应急新增”并上报确认` | `docs/公寓端/08-待决策与风险.md` 表 8-1 `D7`:`本期不做本地应急新增(受 G-04 约束)` | **上位允许、下位禁止,合起来等于"永远不做"。**裁定:`D7` 结论不改(上位正确),补落点——"应急新增" = 对既有 `gy_jcxx` 行指定要素并锁定,**不是往交路模板表插行** | R-02(上位补落点)、R-35(下位改默认口径 + 批次 `M1`→`M3`)、R-38(矩阵改 `■`) |
| **R-C5** | E:改时刻能力无落点 | `docs/平台端/01-功能边界.md` 表 1-1 末行:应用端`**只能人工改写单条任务,不能改规则**` | `docs/公寓端/04-接口设计.md` 表 4-9:`correct` 白名单不含 `jcsj`,`commands` 五条命令**无一条改时刻**;但旧系统 `POST /jcgl/modifyJcXx` 已能改 | **上位承诺的能力在新接口层不存在。**裁定:新增 `ADJUST_TIME` / `ASSIGN_ELEMENT` 补上落点,而不是把上位那句话删掉(现场确实需要改) | R-27(新增第 7/8 项接口)、R-28(白名单禁改 + 指向新命令)、R-25(受控命令分两类)、R-19(上位措辞精确化) |
| **R-C6** | F:`jllx` 语义 | `docs/backend/05-数据模型.md` §9:`GY_JCXX.jllx \| 0 按交路;1 非标准/便乘/驻在/人工修改` | 代码:固定交路入住 `populateJcXxByJlXx` 即置 `'1'`;运安匹配只查 `jllx='0'`;`resetJllxByJcid` 置回 `'0'` | **采用代码语义**:`0` 可参与运安匹配 / `1` 已由本地确定(不参与匹配)。`1` **不是**"非标准/便乘/驻在"的分类标签,而是**匹配开关 + 锁定标志** | R-46(§9 字典改写)、R-01(例外① 引用该语义)。此条涉及现有生产接口含义,`/resetJllx` 的语义说明需与运行方确认后再生效 |
| **R-C7** | G:`gy_syn_jcxx` 定位 | `docs/公寓端/03-数据设计.md` 表 3-2:`apt-master-client(计划下行落库)` → `apt-dispatch(生成任务)` | `docs/backend/05-数据模型.md` §3.1:`GY_SYN_JCXX` = 运安计划快照(含 `kcsj`/`jcfs`) | **采用后者:它是匹配要素表,不是任务生成源。**若按前者理解,会重犯 v1"任务由计划表生成"的错误 | R-47(`05` §3.1 补定位)、R-21(`03` 表 3-2 补注 + 新增 `apt_local_task` 行) |
| **R-C8** | H:`/getJcXxFromGy` 方向 | `docs/backend/06-接口清单.md`:`POST /jcgl/getJcXxFromGy` = "从公寓端获取叫乘信息"(**实现是中心把数据推给公寓**) | 原引"`docs/10` 表 8-1:平台不回调公寓、下行一律拉"——**引用不精确,见 7.2** | **必须下线**,能力由 `#8` 反向承接。上线新能力并验收通过后才可下线(阶段五) | R-50(接口行标注)、R-08(下线前置条件)、7.2 的引用纠正 |
| **R-C9** | I(**本轮新增**):`jcsj` 以谁为准,两篇正相反 | `docs/公寓端/05-关键流程.md` 表 5-1 第 4 行:`复算结果与平台不一致 → **以平台为准** + 告警` | `docs/公寓端/09-平台接口约定.md` 表 9-8 第 6 行:`由 apt_master_rule + apt_holiday 计算,**不取平台值**(D8)` | **乙侧正确。**`jcsj` 恒由公寓算;"以平台为准"只适用于**要素**(`kcsj`/`jcfs`),不适用于 `jcsj` | 见 7.3 逐字替换 |
| **R-C10** | J(**本轮新增**):平台侧仍保留"平台计算 `jcsj`" | `docs/平台端/03-数据设计.md` 表 3-5:`PL_TASK.jcsj(平台计算值)`;`docs/平台端/06-关键流程.md` 图 6-2:`按 rule_version 计算应叫时刻 → upsert PL_TASK` | `docs/backend/10` 表 10-2 `D8'`(R-01 修订后):`jcsj 及其派生一律由公寓端计算` | **平台只存"匹配要素按规则派生的基线 `jcsj`",不存"应叫时刻"作为权威值。**两套事实源并存会让 `diff_ms` 失去参照 | R-09(删 `jcsj(平台计算值)`)、R-18(图 6-3 与要点 5 改口径) |
| **R-C11** | K(**本轮新增**):缺口编号区间口径不一致 | `docs/公寓端/README.md` §3 结论 4 与 §6:`G-01~G-09` | `docs/公寓端/03-数据设计.md` 表 3-16、`docs/公寓端/08-待决策与风险.md` 表 8-9:**只有 `G-01~G-07`**;`G-08`/`G-09` 在 `docs/公寓端/09-平台接口约定.md` 表 9-20 | **统一表述为"`G-01`~`G-09`,其中 `G-01`~`G-07` 见 `03` 表 3-16 与 `08` 表 8-9,`G-08`/`G-09` 见 `09` 表 9-20,`G-10` 见 `09` 表 9-20 续"**;不得在任何一处写成"`03` 表 3-16 含 `G-01~G-09`" | R-24(`G-10` 挂载点)、R-36(`08` 交叉引用);`docs/公寓端/README.md` 两处按右栏口径改写 |
### 7.2 R-C8 附:引用纠正(**全仓统一替换**)
表 1-24 冲突-H 与表 1-20 第 50 行都写"`docs/10` 表 8-1:平台不回调公寓、下行一律拉"。**该引用不精确**:
| 项 | 实际内容 |
| --- | --- |
| `docs/backend/10-架构优化方案-三端拆分.md` **表 8-1** | 三端**部署形态**——不含"平台不回调公寓"的表述 |
| `docs/backend/10-架构优化方案-三端拆分.md` **表 5-1 第 4 行** | `平台端 → 公寓端 \| 不允许直连 \| — \| 仅落“待下发配置版本”,由公寓端拉取` ← **这是"下行一律拉"的唯一第一手出处** |
| `docs/backend/10-架构优化方案-三端拆分.md` **表 8-4「出站 443/tcp」行** | `出站 443/tcp \| 公寓 → 平台 HTTPS \| — \| 单向出站,平台侧不回调` ← **这是"平台侧不回调"的唯一第一手出处** |
**统一替换口径**:凡出现「`docs/10`(原文简写)表 8-1:平台不回调公寓 / 下行一律拉」的位置,一律改为:
> 「`docs/backend/10-架构优化方案-三端拆分.md` **表 5-1 第 4 行**(平台端 → 公寓端不允许直连,仅落待下发配置版本、由公寓端拉取)+ **表 8-4 出站 443 行**(单向出站,平台侧不回调);表 8-1(三端部署形态)仅作形态佐证」
**已识别需替换的位置**(执行时全仓 `Ctrl+F` 复核):
| # | 文件 | 位置 |
| --- | --- | --- |
| 1 | `docs/设计变更/01-本地建单与本地调整.md` | 表 1-24 冲突-H 行;§2.5 表 1-10 末行「`docs/10` 表 8-1」 |
| 2 | `docs/设计变更/01-本地建单与本地调整.md` | 表 1-21 阶段挂载依据列(若引用) |
| 3 | `docs/开发计划/README.md` | §4 口径纪律第 3 条 |
| 4 | `docs/平台端/01-功能边界.md` | 表 1-3 标题行、表 1-6「不做」行 |
| 5 | `docs/平台端/README.md` | 追溯矩阵「平台不回调公寓」行、§0 结论 2 |
| 6 | `docs/平台端/05-跨端接口对齐.md`、`docs/平台端/06-关键流程.md` | 「平台不回调公寓」的任一引用处 |
| 7 | `docs/公寓端/09-平台接口约定.md` | §1 第 1~2 条(若有表 8-1 引用) |
### 7.3 R-C9 逐字替换
**目标**:`docs/公寓端/05-关键流程.md` 表 5-1 第 4 行。
原文:
```
| 4 | 本地复算 | `gy_jcxx.jcid`(= 计划侧业务键) | 复算结果与平台不一致 → **以平台为准** + 告警 | D8;表 5-2 第 6 行 |
```
修订后:
```
| 4 | 本地复算 | `gy_jcxx.jcid`(**= 公寓分配的任务业务键**,不是“计划侧业务键”) | **要素**(`kcsj`/`jcfs`)与平台不一致 → 以平台为准 + 告警;**执行时刻 `jcsj` 恒以本地复算为准**(平台不产出 `jcsj`) | D8';表 5-2 第 6 行 |
```
**理由**:①"计划侧业务键"这个括注是冲突-A 的传播源——`jcid` 是入住记录 ID,与运安计划无关;②"以平台为准"与 `docs/公寓端/09-平台接口约定.md` 表 9-8 第 6 行"不取平台值"直接相反,两篇同一件事两个结论,评审必被追问;③不区分"要素"与"执行时刻",等于承认平台产出 `jcsj`,与 `D8'` 冲突。
---
## 8. 批次 F:路径简写 → 迁移后实际路径
### 表 2-4 简写映射表
| 简写(v1 遗留) | 迁移后实际路径 | 备注 |
| --- | --- | --- |
| `docs/01` | `docs/backend/01-系统总览.md` | |
| `docs/02` | `docs/backend/02-工程结构与分层.md` | |
| `docs/03` | `docs/backend/03-业务模块地图.md` | |
| `docs/04` | `docs/backend/04-核心业务流程.md` | |
| `docs/05` | `docs/backend/05-数据模型.md` | 本清单 R-46~R-48 的目标 |
| `docs/06` | `docs/backend/06-接口清单.md` | 本清单 R-49~R-50 的目标 |
| `docs/07` | `docs/backend/07-外部集成与定时任务.md` | |
| `docs/08` | `docs/backend/08-部署与配置.md` | |
| `docs/09` | `docs/backend/09-迭代指引与风险清单.md` | ⚠️ 易混:`docs/平台端/09`、`docs/公寓端/09` 是**另外两篇**,不在替换范围 |
| `docs/10` | `docs/backend/10-架构优化方案-三端拆分.md` | 全仓引用量最大(425 处) |
| `docs/11` | `docs/backend/11-既有数据库结构分析.md` | |
| `docs/frontend/*` | `docs/frontend/*` | **未迁移,原位**,不需替换 |
| `docs/README.md` | `docs/backend/README.md`(**迁移前叫 `docs/README.md`**,现顶层已无此文件) | 见 R-P2 |
| `docs/01`~`docs/10`(区间写法) | `docs/backend/01`~`docs/backend/11` | 实际为 11 篇,不是 10 篇 |
### 8.1 替换规则(两条,任选其一,**不可混用**)
| 方案 | 规则 | 优点 | 缺点 |
| --- | --- | --- | --- |
| **A(推荐)** | `docs/(0[1-9]\|1[01])(?![\d-])` → `docs/backend/$1`,共 **8 条**(01/02/03/05/07/09/10/11 各有命中,04/06/08 本轮无命中但一并纳入规则) | 机械可批量、不会因标题记忆出错、替换后仍是可读的相对路径 | 路径不含文件名,需在各 README 补一句"`docs/backend/NN` 指 `docs/backend/NN-<标题>.md`" |
| **B(全路径)** | 逐处换成完整文件名 | 可读性最好 | 484 处需逐处判断指向哪一篇,且部分上下文有歧义(见下表风险) |
**采用 A 方案**,并在各目录 README 的"引用约定"节补一句:
> `docs/backend/NN` 指 `docs/backend/NN-<标题>.md`(标题见 `docs/backend/README.md` 索引);与本目录短号 `NN`(如 `docs/平台端/03-数据设计.md` 的 `03`)**不是同一篇**,引用本目录仍须带目录名。
**替换边界(关键)**:只替换 `docs/` 后**直接跟两位数字**的形式。**不替换** `docs/backend/NN`、`docs/frontend/NN`、`docs/平台端/NN`、`docs/公寓端/NN`、`docs/开发计划/NN`、`docs/设计变更/NN`。
### 8.2 分布统计(替换前盘点)
合计 **484 处**,分布如下。
| 简写 | 处数 |
| --- | --- |
| `docs/10` | 425 |
| `docs/09` | 22 |
| `docs/07` | 19 |
| `docs/05` | 7 |
| `docs/01` | 4 |
| `docs/11` | 4 |
| `docs/02` | 2 |
| `docs/03` | 1 |
| 文件(前 12) | 处数 |
| --- | --- |
| `docs/平台端/README.md` | 41 |
| `docs/平台端/09-待决策与风险.md` | 38 |
| `docs/平台端/01-功能边界.md` | 29 |
| `docs/公寓端/08-待决策与风险.md` | 28 |
| `docs/设计变更/01-本地建单与本地调整.md` | 26 |
| `docs/平台端/02-工程结构与技术基线.md` | 25 |
| `docs/公寓端/README.md` | 25 |
| `docs/平台端/03-数据设计.md` | 24 |
| `docs/开发计划/README.md` | 22 |
| `docs/开发计划/01-逐阶段计划-阶段零至二.md` | 21 |
| `docs/公寓端/03-数据设计.md` | 20 |
| `docs/公寓端/06-部署与配置.md` | 18 |
其余 15 个文件合计约 167 处(`docs/平台端/07`、`docs/开发计划/02`、`docs/公寓端/01`、`docs/开发计划/04`、`docs/公寓端/02`、`docs/平台端/08`、`docs/公寓端/05`、`docs/开发计划/03`、`docs/平台端/05`、`docs/公寓端/04`、`docs/公寓端/09`、`docs/平台端/04`、`docs/平台端/06`、`docs/backend/10`、`docs/公寓端/07`)。
### 8.3 逐条任务
| 编号 | 范围 | 内容 | 验收 |
| --- | --- | --- | --- |
| **R-P1** | 全 `docs/`(27 个文件,484 处) | 按 §8.1 方案 A 规则批量替换;替换后逐文件抽查 3 处,确认指向正确篇目 | 正则 `(?<!backend/)docs/(0[1-9]\|1[01])(?![\d-/])` 命中数归零 |
| **R-P2** | `docs/backend/10-架构优化方案-三端拆分.md` §11.3 第 4 条;`docs/平台端/README.md` §3 末段 | **`docs/README.md` 已迁移为 `docs/backend/README.md`**(`git status` 显示为 `R docs/README.md -> docs/backend/README.md`),顶层不再有 `docs/README.md`。原文「在 `docs/README.md` 与 `docs/09-迭代指引与风险清单.md` 中建立索引」→ 改为「在 `docs/backend/README.md`(现存唯一顶层索引)与 `docs/backend/09-迭代指引与风险清单.md` 中建立索引」。`docs/平台端/README.md` §3 末段「`docs/` 顶层为 `01`~`11` + `frontend/` + `公寓端/` + `平台端/`」→ 改为「`docs/backend/` 为 `01`~`11`;另有 `docs/frontend/`、`docs/公寓端/`、`docs/平台端/`、`docs/开发计划/`、`docs/设计变更/`;`docs/` 顶层无散落 md 文件」 | 全仓不再有指向 `docs/README.md` 的引用;`Test-Path docs/README.md` 为 False,`Test-Path docs/backend/README.md` 为 True |
| **R-P3** | `docs/公寓端/README.md` §1 引言 | 原文「`docs/01`~`docs/10`、`docs/frontend/*`」→ 改为「`docs/backend/01`~`docs/backend/11`、`docs/frontend/*`」 | 篇数由 10 改为 11(实际有 11 篇) |
| **R-P4** | `docs/开发计划/README.md` §5 第 2 条 | 原文「引用上位文档**必须带文档名前缀**:`docs/10 表 9-3`、`docs/平台端/09` 表 9-4」→ 改为「`docs/backend/10` 表 9-3、`docs/平台端/09` 表 9-4」 | 示例本身合规(否则规则与示例自相矛盾) |
| **R-P5** | 各目录 README「引用约定」节 | 按 §8.1 补一句「`docs/backend/NN` 指 `docs/backend/NN-<标题>.md`……与本目录短号 `NN` 不是同一篇」 | 6 个 README(`backend`、`frontend`、`公寓端`、`平台端`、`开发计划`、`设计变更`)都有该说明 |
---
## 9. 自检
| # | 检查项 | 方法 | 结论 |
| --- | --- | --- | --- |
| 1 | 是否覆盖表 1-20 全部 51 行 | 编号 `R-01`~`R-51` 与源行号一一对应,无缺号 | 覆盖 51/51 |
| 2 | 是否覆盖表 1-24 全部 8 个冲突 | `R-C1`~`R-C8` + 新发现 `R-C9`~`R-C11` | 覆盖 8/8 + 3 新增 |
| 3 | 每条是否有「原文 → 修订后 → 理由」三要素 | 逐行核对 §2~§6 表格的六列 | **65/67** 条严格符合;另 2 条(`R-24`、`R-36`)属"挂载点纠正"类,原文列填的是事实状态("表 3-16 只到 `G-07`")而非被修订的句子 |
| 4 | 是否使用 v1 路径简写 | 在本文件内搜 `docs/(0[1-9]\|1[01])` | 仅出现在 §0.3 说明、§8 替换表、以及"原文口径"列的引文中(后者必须原样保留并已加括注) |
| 5 | 是否改动了三个代码目录 | `git status -- yktgl YktVue apartment` | 本清单零写入。唯一命中的 `apartment/web/build/index.js`(`A`,已暂存)是会话开始前既存状态,其 `LastWriteTime` 为 18:16,早于本清单创建时间 21:22 |
| 6 | 是否执行了 `git commit` / `git push` | 无相关命令 | 未执行 |
| 7 | 是否臆造工期与人力 | 检索日历日期、"人月"、"人天" | 无;时点只用「阶段 / 步骤 / 批次」 |
| 8 | 三处冻结口径是否被反向表述 | 检索 `平台推送` / `平台回调` / `平台触发` / `打包上传` | 仅出现在禁止性表述与"违反此纪律"的判定句里 |
| 9 | 是否新增接口 | 统计下行/上行接口数 | 未新增;`#7` 只扩响应体、`#8` 只扩 payload,下行仍 8、上行仍 8,总数仍 20 |
| 10 | 是否新造或合并阶段 | 对照 `docs/backend/10` §9 表 9-1~9-6 | 未新造未合并;只出现 `S2.1'`/`S2.2'`/`S2.5'` 步骤级细化(沿用表 1-21) |
| 11 | 表数口径是否三处一致 | `docs/backend/10` 表 5-9 第 1 项、`docs/公寓端/03-数据设计.md` 表 3-8、`docs/平台端/README.md` §4 | 执行 R-22 后三处均为 45 张业务表 |
### 9.1 执行后的机器可查项
| 命令 | 期望 |
| --- | --- |
| `Select-String -Path docs/**/*.md -Pattern 'docs/(0[1-9]\|1[01])(?![\d-])'` | 0 命中(R-P1 完成) |
| `Test-Path docs/README.md` 与 `Test-Path docs/backend/README.md` | 前者 False;后者 True(R-P2 前提已核实:原顶层索引已随迁移改为 `docs/backend/README.md`) |
| `Select-String -Path docs/**/*.md -Pattern 'PL_TASK.*jcsj'` | 0 命中(R-09 完成) |
| `Select-String -Path docs/**/*.md -Pattern 'G-10'` | 命中位置只有 `docs/公寓端/09-平台接口约定.md` 表 9-20(定义)+ `03`/`08`(交叉引用)(R-24 完成) |
| `git status --short` | 只有 `docs/` 下的 markdown 变更 |
---
## 10. 一句话说明
这 **67** 条不是"文字润色",而是三类硬修订:**①把"谁产生任务、谁给要素、谁算时刻"这三件事在 11 篇文档里统一成同一句话**(`R-01`/`R-03`/`R-21`/`R-39`);**②把上位承诺的能力在接口层补齐落点**(`R-04`/`R-05`/`R-26`/`R-27`/`R-28`,否则 `docs/平台端/01-功能边界.md` 表 1-1 的"可人工改写单条任务"在新接口层无处执行);**③把 484 处失效路径与 3 处不精确引用收干净**。前两类不做,阶段二契约冻结时会被否;第三类不做,评审时点了链接跳不过去。
File diff suppressed because it is too large. Load diff