diff --git a/docs/development/dms-r3/00_source_and_scope.md b/docs/development/dms-r3/00_source_and_scope.md new file mode 100644 index 000000000..d42e25f5c --- /dev/null +++ b/docs/development/dms-r3/00_source_and_scope.md @@ -0,0 +1,87 @@ +# R.3A DMS 门禁集成审计与设计基线:来源与范围定义 + +> **系统展示名称(暂定)**:沈阳房建公寓段学员管理系统 +> **内部工程代号**:RailwaySystem +> **当前正式 Release HEAD**:`adcf930ea0a880e1faae36ff3d3e6539d0c0b8f9` +> **当前开发工作区 (Worktree)**:`/Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-r3` +> **当前分支**:`design/dms-r3`(基于 `origin/feat/cicd-pipeline`) +> **文档读取与审计日期**:2026-09-15 +> **文档完整性状态**:`DOCUMENTATION_COMPLETENESS = PARTIAL` +> **真实 DMS 平台调用**:`DMS_LIVE_API_CALLED = NO` + +--- + +## 1. 命名与工程纪律 + +1. **展示名称与工程代号隔离**: + - 对外业务展示名称统一为:**沈阳房建公寓段学员管理系统**(暂定)。 + - 内部工程代号继续保持:`RailwaySystem`。 + - **绝对红线**:严禁因为展示名称变化而重命名 Git 仓库名、Java 包名 (`com.ruoyi.train.*`)、数据库名 (`training`)、Maven 模块名 (`ruoyi-train`, `ruoyi-admin` 等) 以及内部技术路径与文件结构。 +2. **并行开发工作区分离**: + - R.2 RBAC 权限重构当前在独立 Worktree 进行开发:`/Users/wzb/Desktop/项目/RailwaySystem-worktrees/rbac-r2`(分支 `feature/rbac-r2`)。 + - **纪律**:本集成审计任务严格在独立 Worktree `/Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-r3` 进行,严禁触碰 `rbac-r2` 目录,严禁修改 `feature/rbac-r2` 分支。 +3. **只读审计与设计冻结红线**: + - 本阶段为 **R.3A 审计与设计冻结阶段**,只做技术审计、架构设计、模型比选与任务拆分。 + - **严格禁止**: + - 修改任何 Java 代码 (`*.java`) + - 修改任何 Vue/前端代码 (`*.vue`, `*.js`) + - 修改任何 SQL 脚本或执行 Migration + - 对生产/测试 Kingbase 数据库执行任何写操作 (INSERT, UPDATE, DELETE, DDL) + - 调用任何真实 DMS 登录接口或门锁操作接口 + - 使用官方文档中的示例凭据 (`appid`, `appsecret`, `token` 等) + - 执行 `git commit`, `git push`, 部署操作 + +--- + +## 2. 文档来源与凭据安全红线 + +### 2.1 外部接口文档来源 +- **文档地址**:`https://dms.dmsiot.com/V1/doc/restapi.html` +- **平台性质**:DMS 设备管理 / 智能门锁物联网平台 RESTful API 文档。 +- **文档完整性定义**:`PARTIAL`(部分接口文档)。 +- **来源纪律 (Source Discipline)**: + - 严禁假设文档已覆盖 DMS 的全部能力。 + - 审计结论严格区分为四类置信度: + - `DOCUMENTED`:官方文档中有明确的 HTTP 方法、路径、请求体和响应体说明。 + - `INFERRED`:基于文档上下文字段、枚举说明推导,但缺少独立端点描述。 + - `UNCONFIRMED`:仅在响应体或枚举中出现字段,未提供接口调用支持。 + - 官方文档中已明确定义二维码/手机钥匙接口(生成:`POST /rest/doorlock/qrcode/create`,注销:`POST /rest/qrcode/delete`),分类为 `DOCUMENTED`;但其载荷缺失与一期适用性仍为 `NEEDS_BUSINESS_CONFIRMATION`。严禁根据返回体中的其他无接口字段(如 `fingerprint: false`)脑补或声称系统已支持指纹等未定义接口。 + +### 2.2 示例凭据安全隔离红线 +- 官方文档中出现的示例 `appid`、示例 `appsecret`、示例 `token`、示例手机号、密码、卡号等: + - **仅作为文档语法结构与格式说明**。 + - **严禁**使用文档中任何示例凭据向 `dms.dmsiot.com` 发送任何网络请求(无论登录或业务测试)。 + - **严禁**将上述示例密钥提交到代码库或配置文件中。 + +--- + +## 3. 任务范围定义与交付清单 + +### 3.1 核心任务目标 +1. 完整审计 DMS REST API 文档,建立全量 API 清单与参数/状态审计表。 +2. 重新审计当前 RailwaySystem 真实代码库与 Kingbase 物理表(Dormitory, Accommodation, DormAuth)。 +3. 建立 RailwaySystem 宿舍业务与 DMS 门锁平台之间的领域映射体系。 +4. 设计厂商无关的门禁网关架构(DoorAccessGateway)与 DMS 专用适配器(DmsAdapter)。 +5. 设计生产级 Token 生命周期管理机制与高并发防击穿策略。 +6. 设计门禁授权生命周期状态机与异步 `operationId` 轮询调度器。 +7. 设计重试、网络未知状态对账、退训撤权、调房换房补偿等可靠性机制。 +8. 评估敏感数据保护方案(密码、卡号、Token 脱敏与加密)。 +9. 明确一期试点接入范围与后续 R.3B 任务 WBS 拆分。 + +### 3.2 交付物清单 +本审计任务仅在 `docs/development/dms-r3/` 目录下生成以下 15 个基线设计文档: +- `00_source_and_scope.md`(本文件:来源、范围与安全红线) +- `01_dms_api_inventory.md`(DMS API 全量资产清单与分级) +- `02_current_accommodation_audit.md`(本地住宿/宿舍/门禁模型真实代码与数据库审计) +- `03_domain_mapping.md`(业务领域模型映射与生命周期对应) +- `04_auth_and_token_strategy.md`(DMS 登录鉴权、Token 缓存与并发控制策略) +- `05_adapter_architecture.md`(集成适配器分层架构与厂商无关接口设计) +- `06_authorization_state_machine.md`(门锁授权生命周期状态机) +- `07_retry_idempotency_and_compensation.md`(重试机制、本地幂等与换房补偿设计) +- `08_security_and_audit.md`(凭据安全、日志脱敏与门禁审计轨迹) +- `09_database_mapping_proposal.md`(数据库表结构方案比选与模型演进建议) +- `10_api_and_ui_proposal.md`(内部管理接口与端到端 UI 交互原型方案) +- `11_testing_and_mock_plan.md`(WireMock 模拟测试与多并发可靠性测试计划) +- `12_implementation_task_breakdown.md`(R.3B 实施阶段 WBS 任务拆分) +- `13_open_questions.md`(供应商/业务/技术未决问题与阻塞项清单) +- `14_decision_summary.md`(技术选型与管理决策一页纸总结) diff --git a/docs/development/dms-r3/01_dms_api_inventory.md b/docs/development/dms-r3/01_dms_api_inventory.md new file mode 100644 index 000000000..0f040aa90 --- /dev/null +++ b/docs/development/dms-r3/01_dms_api_inventory.md @@ -0,0 +1,69 @@ +# 01 DMS API 全量资产清单与分级评估 (DMS API Inventory) + +> **文档来源**:`https://dms.dmsiot.com/V1/doc/restapi.html` +> **文档完整性评估**:`DOCUMENTATION_COMPLETENESS = PARTIAL` +> **接口审计原则**:严格基于官方文档文字与 Schema 表达,不臆测不存在的接口,明确标记置信度与敏感字段。 + +--- + +## 1. 范围与分级定义说明 + +| 分级标识 | 定义与采纳建议 | 典型接口范畴 | +| :--- | :--- | :--- | +| **`P0_REQUIRED`** | **一期核心闭环必备**:完成学员入住开门、退宿撤权、异步状态对账所必需的接口 | 登录获取 Token、房间查询、门锁列表、下发在线密码/卡片、删除密码/卡片、查询门锁操作状态 | +| **`P1_USEFUL`** | **高价值辅助运维接口**:用于设备健康度巡检、房源维护、现场排障,不阻断核心入住业务流 | 查询门锁电池与信号状态、楼栋/楼层列表、开门记录流水查询 | +| **`P2_FUTURE`** | **未来二期/扩展功能**:非当前学员住宿刚需,或需要额外硬件联动、现场施工配合的功能 | 取电开关全套接口(送电/断电/列表/状态)、网关状态主动巡检刷新 | +| **`OUT_OF_SCOPE`** | **高风险或不适用当前场景**:需要严苛线下流程或与当前全自动/半自动学员管理模型冲突 | 离线制卡接口(依赖现场物理制卡机与 DLL)、生成离线密码(需现场输入超长动态数字码) | +| **`UNCONFIRMED`** | **文档缺失关键接口端点**:仅在响应体或枚举中出现字段,未提供独立接口调用支持 | 手机蓝牙钥匙、指纹下发接口 | + +--- + +## 2. DMS API 全量清单 (DMS_API_INVENTORY) + +| # | Category | Operation | HTTP Method | Path | Request Fields | Response Fields | Auth | Sync/Async | operationId? | requestType? | External ID | Sensitive Fields | Relevance | Phase Recommendation | Confidence | +| :-: | :--- | :--- | :-: | :--- | :--- | :--- | :-: | :-: | :-: | :-: | :--- | :--- | :--- | :-: | :-: | +| 1 | 鉴权认证 | 平台登录获取 Token | `POST` | `/public/rest/third-party-app/token/get` | `appid`, `appsecret` | `success`, `token`, `expiresIn`, `message` | 无需 Token (凭 appid/secret) | Sync | 否 | 无 | 无 | `appid`, `appsecret`, `token` | 核心入口 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 2 | 鉴权认证 | 退出登录 | `POST` | `/public/rest/security/logout` | `{}` (空 JSON) | `success` | Cookie / URL `token` | Sync | 否 | 无 | 无 | `token` | 会话销毁 | **`P1_USEFUL`** | `DOCUMENTED` | +| 3 | 区域管理 | 获取楼栋列表 | `POST` | `/rest/place/building/list` | `building`, `offset`, `limit` | `success`, `rows` (含 `id`, `name`, `parentId`, `building`, `projectId`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 楼栋 `id` | 无 | 房源台账对照 | **`P1_USEFUL`** | `DOCUMENTED` | +| 4 | 区域管理 | 获取楼层列表 | `POST` | `/rest/place/floor/list` *(注: 文档示例为 http)* | `building`, `floor`, `offset`, `limit` | `success`, `rows` (含 `id`, `name`, `parentId`, `floor`, `building`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 楼层 `id` | 无 | 房源台账对照 | **`P1_USEFUL`** | `DOCUMENTED` | +| 5 | 区域管理 | 获取房间列表 | `POST` | `/rest/place/room/list` | `building`, `floor`, `room`, `offset`, `limit` | `success`, `rows` (含 `id`, `name`, `parentId`, `building`, `floor`, `room`, `status`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 房间 `id` (placeId) | 无 | 房间映射与校验 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 6 | 区域管理 | 精确获取房间 ID | `POST` | `/rest/place/room/get` | `building`, `floor`, `room` | `success`, `data` (含 `id`, `projectId`, `building`, `floor`, `room`), `message` | Cookie / URL `token` | Sync | 否 | 无 | 房间 `id` (placeId) | 无 | 房间快速映射绑定 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 7 | 网关管理 | 获取网关列表 | `POST` | `/rest/gateway/list` | `offset`, `limit` | `success`, `rows` (含 `id`, `projectId`, `hardwareId`, `onlineStatus` 等), `total` | Cookie / URL `token` | Sync | 否 | 无 | 网关 `id`, `hardwareId` | 无 | 设备拓扑巡检 | **`P1_USEFUL`** | `DOCUMENTED` *(onlineStatus未实现)* | +| 8 | 网关管理 | 刷新网关状态 | `POST` | `/rest/gateway/refresh-online-status` | `id`, `hardwareId` | `success`, `data` (含 `operation` 对象: `operationId`, `timeoutSeconds: 10`, `requestType: 1`) | Cookie / URL `token` | Async | 是 (`operationId`) | 1 | 网关 `id` | 操作人手机号 (`username`) | 故障排查 | **`P2_FUTURE`** | `DOCUMENTED` | +| 9 | 网关管理 | 查询网关状态 | `POST` | `/rest/gateway/query-online-status` | `operationId`, `hardwareId` | `success`, `data` (含 `operationStatus`: `success`/`undone`/`timeout`) | Cookie / URL `token` | Polling | 是 (`operationId`) | 1 | 无 | 无 | 故障排查轮询 | **`P2_FUTURE`** | `DOCUMENTED` | +| 10 | 门锁管理 | 获取门锁列表 | `POST` | `/rest/doorlock/list` | `placeId`, `offset`, `limit` | `success`, `rows` (含 `id`, `hardwareId`, `placeId`, `battery`, `signal`, `passagewayMode`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 门锁 `id`, `hardwareId` | 无 | 核心设备映射 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 11 | 门锁管理 | 查询门锁电池与信号状态 | `POST` | `/rest/doorlock/query-lock-battery` | `roomIds` (数组) | `success`, `data` (Map: `roomId` -> `[{id, battery, signal, batteryStr, signalStr}]`) | Cookie / URL `token` | Sync | 否 | 无 | 门锁 `id` | 无 | 门锁健康度监控 | **`P1_USEFUL`** | `DOCUMENTED` | +| 12 | 门锁管理 | 远程开锁 | `POST` | `/rest/doorlock/remote-unlock` | `lockId`, `hardwareId`, `canOpenAntilock` | `success`, `data.operation` (含 `operationId`, `requestType: 5`) | Cookie / URL `token` | Async | 是 (`operationId`) | 5 | 门锁 `id` | 远程触发开门操作 | 高风险应急开门 | **`P2_FUTURE` / 受控应急** | `DOCUMENTED` | +| 13 | 门锁管理 | 查询门锁操作结果 | `POST` | `/rest/doorlock/query-operation-status` | `requestType`, `operationId`, `hardwareId` | `success`, `data` (含 `operationStatus`: `success`/`failed`/`undone`/`timeout`, `resultDetails`, `message`) | Cookie / URL `token` | Polling (10s后) | 是 (`operationId`) | 1~9 | `operationId` | `resultDetails` | 异步状态对账必备 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 14 | 钥匙管理 | 下发在线密码 | `POST` | `/rest/doorlock/online-password-key/create` | `lockId`, `validStart`, `validUntil`, `holder`, `canOpenAntilock`, `phoneCountry`, `phoneNumber`, `password` (系统支持4~8位,建议6位), `subType: 4` (限时) | `success`, `data` (含钥匙 `id`, `operationId`, `lockHardwareId`, `smsStatus`) | Cookie / URL `token` | Async (需10s后对账) | 是 (`operationId`) | 1 | 钥匙 `id`, `operationId` | `password`, `phoneNumber` | 核心凭据发放 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 15 | 钥匙管理 | 查询下发在线密码结果 | `POST` | `/rest/doorlock/query-operation-status` | `requestType: 1`, `operationId`, `hardwareId` | `success`, `data.operationStatus` | Cookie / URL `token` | Polling | 是 (`operationId`) | 1 | `operationId` | 无 | 密码生效对账 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 16 | 钥匙管理 | 删除在线密码 | `POST` | `/rest/doorlock/online-password/delete` | `id` (钥匙 ID) | `success`, `data` (含 `operationId`, 钥匙状态 snapshot) | Cookie / URL `token` | Async (需10s后对账) | 是 (`operationId`) | 3 *(注: 文档示例误标为1)* | 钥匙 `id`, `operationId` | `fuzzyPassword`, `phoneNumber` | 核心退宿撤权 | **`P0_REQUIRED`** | `DOCUMENTED` | +| 17 | 钥匙管理 | 申请离线密码 | `POST` | `/rest/doorlock/offline-password/create` | `lockId`, `validStart`, `validUntil`, `subType` (1永久, 2单次, 4限时, 5清空) | `success`, `data.password` (动态纯数字码,如 "2420101197") | Cookie / URL `token` | Sync (纯算法计算) | 否 | 无 | 无 | `password` | 离线网络应急备用 | **`OUT_OF_SCOPE`** | `DOCUMENTED` | +| 18 | 钥匙管理 | 生成离线密码钥匙记录 | `POST` | `/rest/doorlock/offline-password-key/create` | `lockId`, `holder`, `validStart`, `validUntil`, `password` | `success`, `data` (含钥匙 `id`, `placeId`, `lockHardwareId`) | Cookie / URL `token` | Sync | 否 | 无 | 钥匙 `id` | `fuzzyPassword` | 离线台账补录 | **`OUT_OF_SCOPE`** | `DOCUMENTED` | +| 19 | 钥匙管理 | 下发在线卡片 | `POST` | `/rest/doorlock/online-card/create` | `lockId`, `validStart`, `validUntil`, `holder`, `canOpenAntilock`, `cardNumber` (16进制低位在前), `subType: 4` | `success`, `data` (含钥匙 `id`, `operationId`, `lockHardwareId`) | Cookie / URL `token` | Async (需10s后对账) | 是 (`operationId`) | 2 | 钥匙 `id`, `operationId` | `cardNumber` | 实体学员卡下发 | **`P1_USEFUL` / 备选** | `DOCUMENTED` | +| 20 | 钥匙管理 | 删除在线卡片 | `POST` | `/rest/doorlock/online-card/delete` | `id` (钥匙 ID) | `success`, `data` (含 `operationId`, 钥匙状态 snapshot) | Cookie / URL `token` | Async (需10s后对账) | 是 (`operationId`) | 4 *(注: 文档示例误标为1)* | 钥匙 `id`, `operationId` | `cardNumber` | 实体卡退宿注销 | **`P1_USEFUL` / 备选** | `DOCUMENTED` | +| 21 | 钥匙管理 | 生成二维码/手机钥匙 | `POST` | `/rest/doorlock/qrcode/create` | `lockId`, `validStart`, `validUntil`, `holder`, `canOpenAntilock`, `phoneCountry`, `phoneNumber` | `success`, `data` (含钥匙 `id`, 示例含数据污染) | Cookie / URL `token` | Sync | 否 | 无 | 钥匙 `id` | `phoneNumber` | 移动端二维码开锁 | **`NEEDS_BUSINESS_CONFIRMATION`** | `DOCUMENTED` | +| 22 | 钥匙管理 | 注销二维码/手机钥匙 | `POST` | `/rest/qrcode/delete` *(注: 路径无 doorlock)* | `id` (钥匙 ID) | `success` | Cookie / URL `token` | Sync | 否 | 无 | 钥匙 `id` | 无 | 二维码注销 | **`NEEDS_BUSINESS_CONFIRMATION`** | `DOCUMENTED` | +| 23 | 钥匙管理 | 生成离线卡片钥匙 | `POST` | `/rest/doorlock/offline-card/create` | `lockId`, `validStart`, `validUntil`, `holder`, `canOpenAntilock`, `cardNumber`, `subType: 4`, `offlineCardType: 2` | `success`, `data` (钥匙元数据) | Cookie / URL `token` | Sync (无需下发锁,需配合物理写卡机) | 否 | 无 | 钥匙 `id` | `cardNumber` | 传统酒店磁卡/IC卡制卡 | **`OUT_OF_SCOPE`** | `DOCUMENTED` | +| 24 | 开门记录 | 获取开门记录列表 | `POST` | `/rest/lock/unlock-record/list` *(注: 路径为 /lock/)* | `lockId`, `building`, `room`, `userName`, `startTime`, `endTime`, `offset`, `limit`, `typeList` (数组) | `success`, `rows` (含 `id`, `lockId`, `placeId`, `hardwareId`, `password`, `type`, `creationTime`, `userName`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 记录 `id` | `password`, `userName` | 考勤辅助/安全排障 | **`P1_USEFUL`** | `DOCUMENTED` | +| 25 | 取电开关 | 获取取电开关列表 | `POST` | `/rest/powerswitch/list` | `offset`, `limit` | `success`, `rows` (含开关 `id`, `projectId`, `hardwareId`, `placeId`, `building`, `room`), `total` | Cookie / URL `token` | Sync | 否 | 无 | 开关 `id`, `hardwareId` | 无 | 宿舍节能控制 | **`P2_FUTURE`** | `DOCUMENTED` | +| 26 | 取电开关 | 取电开关远程送电 | `POST` | `/rest/powerswitch/remote-poweron` | `id`, `hardwareId` | `success`, `data.operation` (含 `operationId`, `requestType: 1`) | Cookie / URL `token` | Async | 是 (`operationId`) | 1 | 开关 `id`, `operationId` | 操作人手机号 | 宿舍远程供电 | **`P2_FUTURE`** | `DOCUMENTED` | +| 27 | 取电开关 | 取电开关远程断电 | `POST` | `/rest/powerswitch/remote-poweroff` | `id`, `hardwareId` | `success`, `data.operation` (含 `operationId`, `requestType: 2`) | Cookie / URL `token` | Async | 是 (`operationId`) | 2 | 开关 `id`, `operationId` | 操作人手机号 | 宿舍远程断电 | **`P2_FUTURE`** | `DOCUMENTED` | +| 28 | 取电开关 | 查询取电开关远程断/送电结果 | `POST` | `/rest/powerswitch/query-operation-status` | `requestType` (1送电, 2断电), `operationId`, `hardwareId` | `success`, `data` (含 `operationStatus`: `success`/`undone`/`timeout`, `powerStatus`) | Cookie / URL `token` | Polling | 是 (`operationId`) | 1 / 2 | `operationId` | 无 | 取电开关对账 | **`P2_FUTURE`** | `DOCUMENTED` | + +--- + +## 3. 一期重点接口范围确认 (`Scope Freeze`) + +### 3.1 一期必须接入接口 (`P0_REQUIRED`) +1. **平台登录**:`POST /public/rest/third-party-app/token/get`(用于获取和维持通信令牌)。 +2. **房间映射**:`POST /rest/place/room/get` 及 `POST /rest/place/room/list`(建立本地宿舍与 DMS 房间 `placeId` 的权威绑定)。 +3. **门锁映射**:`POST /rest/doorlock/list`(根据 `placeId` 获取房间内所有智能门锁的 `lockId` 与 `hardwareId`)。 +4. **权限下发**:`POST /rest/doorlock/online-password-key/create`(推荐作为一期首选试点凭据下发方式)。 +5. **权限撤销**:`POST /rest/doorlock/online-password/delete`(退宿/调房/结训时主动撤销钥匙)。 +6. **异步结果查询**:`POST /rest/doorlock/query-operation-status`(所有写操作的异步 10 秒后对账保障)。 + +### 3.2 高风险与非核心功能排除说明 +- **远程开锁 (`remote-unlock`)**:属于主动设备控制,具有物理安全风险。一期**严禁**作为学员常规功能,仅设计受控管理员权限与两票制审计,标记为 `P2_FUTURE / 受控应急`。 +- **取电开关全套接口 (`powerswitch/*`)**:沈阳房建公寓段学员管理系统核心是学员学籍、住宿、报到业务,强行纳入取电开关将带来硬件施工与网络调试成本,确定归入 `P2_FUTURE`。 +- **二维码钥匙 (`qrcode/create`)**:官方文档中明确包含端点定义 (`POST /rest/doorlock/qrcode/create` 与 `POST /rest/qrcode/delete`),分类为 `DOCUMENTED`;但其响应示例存在数据污染(`typeStr: "Online Card"`)且未见二维码载荷内容,其实际方案可行性属于 `NEEDS_BUSINESS_CONFIRMATION`,一期推荐保持使用在线密码作为 Pilot 方案。 diff --git a/docs/development/dms-r3/02_current_accommodation_audit.md b/docs/development/dms-r3/02_current_accommodation_audit.md new file mode 100644 index 000000000..e492ed2b5 --- /dev/null +++ b/docs/development/dms-r3/02_current_accommodation_audit.md @@ -0,0 +1,139 @@ +# 02 本地住宿、宿舍与门禁模型现状审计 (Current Accommodation Audit) + +> **基准代码版本**:`adcf930ea0a880e1faae36ff3d3e6539d0c0b8f9` (`HEAD`) +> **审计范围**:Kingbase 物理数据库、Java 后端代码、MyBatis XML、前端 Vue 代码 +> **审计执行方式**:只读安全查询脚本 (`DbTableAuditor.java`),无任何写操作与数据破坏 + +--- + +## 1. Kingbase 数据库真实物理表事实 (Physical Table Facts) + +通过直接连接正式 Kingbase 数据库 (`jdbc:kingbase8://127.0.0.1:4321/training?currentSchema=public,sys_catalog`) 进行只读元数据审计,物理表事实如下: + +### 1.1 `train_dormitory_info` (宿舍物理资源表) +- **TABLE_EXISTS**: `true` +- **ROW_COUNT**: `1` +- **KEY_COLUMNS (14列)**: + - `dorm_id` (`varchar(32)`, `NOT NULL`, 主键 UUID) + - `dorm_building` (`varchar(50)`, `NOT NULL`, 宿舍楼栋) + - `dorm_floor` (`varchar(20)`, `NOT NULL`, 所在楼层) + - `dorm_room` (`varchar(20)`, `NOT NULL`, 房间号) + - `capacity` (`integer`, `NOT NULL`, 核定床位数, 1~8) + - `gender_type` (`smallint`, `NOT NULL`, 适用性别: 0不限, 1男, 2女) + - `lock_device_sn` (`varchar(64)`, `NULLABLE`, 智能门锁设备硬件序列号) + - `dorm_status` (`smallint`, `NOT NULL`, 房间状态: 0禁用, 1可用) + - `del_flag` (`bpchar(1)`, `NOT NULL`, 逻辑删除: 0正常, 2删除) + - `create_by`, `create_time`, `update_by`, `update_time`, `remark` (标准审计列) +- **物理约束**: 唯一约束 `uq_dorm_building_room (dorm_building, dorm_room)`。 + +### 1.2 `train_accommodation` (学员住宿安排事实表) +- **TABLE_EXISTS**: `true` +- **ROW_COUNT**: `0` +- **KEY_COLUMNS (11列)**: + - `accommodation_id` (`bigint`, `NOT NULL`, 主键 IDENTITY) + - `enrollment_id` (`bigint`, `NOT NULL`, 参训档案ID, 唯一约束 `uq_accommodation_enroll`) + - `dorm_id` (`varchar(32)`, `NOT NULL`, 分配宿舍ID, 外键 `fk_accomm_dorm` 引用 `train_dormitory_info.dorm_id`) + - `bed_no` (`varchar(20)`, `NULLABLE`, 分配床位号) + - `assigned_time` (`timestamp`, `NOT NULL`, 分配时间, 默认 `CURRENT_TIMESTAMP`) + - `accommodation_status` (`smallint`, `NOT NULL`, 住宿状态: 0预分配, 1已入住, 2已退宿) + - `create_by`, `create_time`, `update_by`, `update_time`, `remark` (标准审计列) +- **外键约束**: + - `fk_accomm_enrollment`: 引用 `train_enrollment(enrollment_id)` + - `fk_accomm_dorm`: 引用 `train_dormitory_info(dorm_id)` + +### 1.3 `train_dorm_auth` (门禁同步与下发表 - 数据库 V2 重建版本) +- **TABLE_EXISTS**: `true` +- **ROW_COUNT**: `0` +- **KEY_COLUMNS (14列)**: + - `auth_id` (`bigint`, `NOT NULL`, 主键 IDENTITY) + - `accommodation_id` (`bigint`, `NOT NULL`, 住宿安排ID, 外键 `fk_dormauth_accomm`, 唯一约束 `uq_dorm_auth_accomm`) + - `auth_start` (`timestamp`, `NULLABLE`, 权限生效开始时间) + - `auth_end` (`timestamp`, `NULLABLE`, 权限失效结束时间) + - `face_sync_status` (`smallint`, `NOT NULL`, 默认 0; 0待下发, 1已下发, 2下发失败, 3已注销) + - `retry_count` (`integer`, `NOT NULL`, 默认 0, 同步失败重试次数) + - `last_error` (`varchar(500)`, `NULLABLE`, 最后一次下发失败错误信息) + - `external_auth_id` (`varchar(100)`, `NULLABLE`, 外部门禁硬件系统下发唯一凭证ID) + - `last_sync_time` (`timestamp`, `NULLABLE`, 最近一次同步请求时间) + - `create_by`, `create_time`, `update_by`, `update_time`, `remark` (标准审计列) + +### 1.4 其它相关表事实 +- `bak_v1_train_dorm_auth`: 存在历史 V1 备份表(行数 0),包含废弃的 `student_id`, `plan_id`, `dorm_id`, `bed_no`。 +- 数据库中**不存在**任何以 `door_*`, `lock_*`, `access_*` 命名的其他物理表。 + +--- + +## 2. 真实代码层实现现状审计 (Code Implementation Facts) + +| 模块 / 类 | 当前代码存在情况 | 承担职责与现实状态 | +| :--- | :--- | :--- | +| **`TrainDormitoryInfo`** (Domain/VO/Service/Controller) | **已完整实现** | 负责物理宿舍增删改查。包含 `lockDeviceSn` 单一字符串字段;通过 SQL 实时统计 `train_accommodation` 的 `usedCount` 与计算 `remainingBeds`。 | +| **`TrainDormAuth`** (Domain/Mapper/Service/Controller) | **实现脱节 (V1 遗留代码)** | 当前 Java 实体类 `TrainDormAuth.java`、Mapper 与 Controller **依然映射的是 V1 结构**(持有 `studentId`, `planId`, `dormId`, `bedNo`, `faceSyncStatus`),**尚未重构对齐数据库 V2 的 `accommodation_id` 关联结构**! | +| **`TrainAccommodation`** (Domain/Service/Controller) | **代码层缺失** | 数据库已存在 `train_accommodation` 物理表,且 `TrainDormitoryInfoMapper.xml` 和 `TrainEnrollmentMapper.xml` 已在子查询中引用该表,但 Java 层尚无对应的 `TrainAccommodation.java` 实体及专用 Service/Controller。 | +| **前端 Vue 页面** | **未实现专用页面** | `ruoyi-ui/src/views/` 目录下尚未建立 `train/dormitory` 或 `train/auth` 前端业务界面,目前仅有大盘总览。 | + +--- + +## 3. 三大核心业务边界辨析 (Current Business Boundary) + +当前系统必须在概念模型与后续实现中**严格切分以下三个业务对象**,严禁混为一谈: + +``` ++-----------------------------------------------------------------------------+ +| A. 物理宿舍资源 (Dormitory Resource - train_dormitory_info) | +| - 物理属性:楼栋、楼层、房间号、核定床位容量、适用性别、房间可用/停用状态 | +| - 生命周期:独立于任何培训班和学员,长期相对稳定 | ++-----------------------------------------------------------------------------+ + ▲ + │ 1 间宿舍可容纳多个住宿安排 ++-----------------------------------------------------------------------------+ +| B. 住宿业务事实 (Accommodation - train_accommodation) | +| - 业务属性:某个学员参训档案 (enrollment_id) 在本次开班中分配到了哪个房间与床位 | +| - 状态流转:预分配 (0) -> 已入住 (1) -> 已退宿 (2) | +| - 事实归属:RailwaySystem 是本业务的绝对 Source of Truth (事实唯一真源) | ++-----------------------------------------------------------------------------+ + ▲ + │ 1 条住宿事实产生门禁授权要求 ++-----------------------------------------------------------------------------+ +| C. 门锁设备授权 (Door Authorization - 门禁授权模型) | +| - 设备属性:基于该住宿事实,向 DMS 门锁下发的有效时间区间与电子钥匙凭据 (密码/卡片) | +| - 状态流转:待下发 -> 平台已受理 -> 设备执行中 -> 设备生效 -> 撤销中 -> 已注销 | +| - 属性特征:依赖外部网络与硬件状态,可异步重试,失败不推翻住宿业务事实 | ++-----------------------------------------------------------------------------+ +``` + +--- + +## 4. `train_dorm_auth` 当前角色与演进方案比选 (TRAIN_DORM_AUTH_CURRENT_ROLE) + +### 4.1 当前角色定性 +在当前代码与数据库混合状态下: +- **Java 代码层**:`train_dorm_auth` 承担了“住宿分配 + 门锁序列号关联 + 人脸同步标记”的**混合式职责**。 +- **数据库 V2 迁移设计**:设计意图已将其向“纯门禁授权下发同步表”推进(引入 `accommodation_id`, `retry_count`, `last_error`, `external_auth_id`),但尚未考虑**一个房间多把锁**、**多凭据类型(密码/卡片)**以及 **DMS `operationId` 异步轮询生命周期**。 + +### 4.2 三种演进方案详细比选 + +| 评估维度 | 方案 A: 沿用并继续扩展 `train_dorm_auth` | 方案 B: 收敛 `train_dorm_auth` + 独立 `train_external_operation` | 方案 C: 新建通用 `train_door_authorization` + `train_external_operation` | +| :--- | :--- | :--- | :--- | +| **设计描述** | 在现有的 `train_dorm_auth` 上追加 `lock_id`, `operation_id`, `credential_type`, `credential_value` 等列 | 将 `train_dorm_auth` 代码升级对齐 V2(作为授权事实主表),另建异步操作流水表记录轮询与对账 | 新建专用的门禁授权表与外部操作流水表,彻底废弃/弃用命名有误导的 `train_dorm_auth` | +| **职责清晰度** | 差。混合业务授权与设备异步任务,字段臃肿且无法表达多锁与多钥匙 | 良好。主表表达“谁拥有该房间的有效授权”,流水表表达“向设备发送的异步指令状态” | 优秀。命名脱离“人脸/宿舍”局部限制,完全按照通用门禁控制抽象 | +| **与 Accommodation 关系** | 外键引用 `accommodation_id`,但多锁时难以保持 1:1 唯一约束 | 主表通过 `accommodation_id` 关联,支持按锁/凭据类型建立多条授权;流水表关联主表 | 清晰明确,一个 Accommodation 可派生多条 Authorization 凭据 | +| **DMS Operation 生命周期支持** | 极差。无法保存多次重试、多次轮询的轨迹与错误历史 | 优秀。所有异步 `operationId`、轮询重试、超时记录在流水表完整归档 | 优秀。支持完整的异步生命周期追踪与审计 | +| **多锁扩展性** | 无法支持(同一个房间若有外门锁与内门锁,会导致主键或关联冲突) | 良好(主表以 `(accommodation_id, lock_id, credential_type)` 为逻辑单元) | 优秀(原生支持一房多锁与多供应商适配) | +| **迁移与代码改动成本** | 表面成本低,但后期技术债务严重 | **适中**。数据库 V2 已就绪 `train_dorm_auth`,只需调整列定义或补充流水表 | 略高。需要新增两张物理表并做数据迁移 | +| **审计与故障追溯** | 弱(覆写 `last_error` 导致历史丢失) | **强**(流水表不可变追加,完整记录每一次操作) | **强** | + +### 4.3 演进建议结论 +1. **本轮 R.3A 不执行任何 DDL**。 +2. **架构倾向**:推荐采纳 **方案 B/C 的分层解耦思想**。绝不能将 DMS 的 `operationId` 轮询与重试直接塞在单个授权记录中覆写,必须将**“门禁业务授权事实 (Door Authorization)”**与**“外部硬件异步操作记录 (External Operation)”**彻底解耦为两层模型。 + +--- + +## 5. 本地模型漂移事实与前置对齐要求 (`LOCAL_DOMAIN_ALIGNMENT_REQUIRED`) + +### 5.1 现实漂移事实总结 +- **`LOCAL_DOMAIN_ALIGNMENT_REQUIRED = YES`** +- **当前核心矛盾**: + 1. **`train_accommodation`**:Kingbase 物理表已在 V2 迁移中正式建立,但 Java 代码层尚未建立对应的实体类、Mapper 与业务 Service,未形成闭环业务层。 + 2. **`train_dorm_auth`**:Kingbase 物理表已在 V2 中升级为以 `accommodation_id` 为外键的门禁授权结构,但 Java 实体类 `TrainDormAuth.java` 与 XML Mapper 依然停留在 V1 旧结构(持有 `student_id`, `plan_id`, `dorm_id`, `bed_no`)。 +- **执行原则**: + 在进入 R.3B 真正编写 DMS Adapter 之前,**必须先在 Wave 0 完成本地住宿与门禁授权领域模型对齐**!严禁让外部 DMS 集成适配器直接架设在当前存在严重漂移的本地模型之上。 diff --git a/docs/development/dms-r3/03_domain_mapping.md b/docs/development/dms-r3/03_domain_mapping.md new file mode 100644 index 000000000..dcae04af0 --- /dev/null +++ b/docs/development/dms-r3/03_domain_mapping.md @@ -0,0 +1,183 @@ +# 03 RailwaySystem 与 DMS 领域模型映射体系 (Domain Mapping) + +> **核心原则**:**RailwaySystem 是住宿业务事实的唯一真源 (Source of Truth)**。 +> **DMS 平台**仅作为外部门禁与物联网设备的执行端与监控端。 +> **严禁**:以 DMS 房间状态 (`status`) 直接替代 RailwaySystem 住宿占用与床位台账。 + +--- + +## 1. 核心实体对应关系总览 (Mermaid 架构图) + +```mermaid +classDiagram + direction TB + + class RailwayTrainingPlan { + +String planId "内部培训班计划ID" + +String planName + +Date trainStart + +Date trainEnd + } + + class RailwayEnrollment { + +Long enrollmentId "参训学员档案ID" + +String planId + +String studentId + +Integer reportStatus + } + + class RailwayAccommodation { + +Long accommodationId "住宿安排事实ID (Source of Truth)" + +Long enrollmentId + +String dormId + +String bedNo + +Integer accommodationStatus "0预分配, 1已入住, 2已退宿" + } + + class RailwayDormitoryInfo { + +String dormId "本地宿舍UUID" + +String dormBuilding "楼栋" + +String dormFloor "楼层" + +String dormRoom "房号" + +Integer capacity "床位容量" + } + + class ExternalResourceMapping { + +Long mappingId + +String localDormId "关联本地宿舍" + +Long dmsProjectId "DMS租户/物业站点ID" + +Long dmsPlaceId "DMS房间ID" + +String dmsBuilding + +String dmsFloor + +String dmsRoom + +Integer mappingStatus "1已绑定, 0待校验" + } + + class DmsPlaceRoom { + <> + +Long id "DMS placeId" + +Long projectId "DMS租户ID (≠ planId)" + +String building + +String floor + +String room + } + + class DmsDoorLock { + <> + +Long lockId "DMS锁ID" + +String hardwareId "锁硬件SN (如 5C9F539A)" + +Long placeId + +Integer battery + +Integer signal + } + + class DoorAuthorization { + +Long authId "本地授权凭证主记录" + +Long accommodationId + +Long dmsLockId + +String dmsHardwareId + +Long dmsKeyId "DMS返回的钥匙ID" + +String credentialType "PASSWORD / CARD" + +Timestamp validStart + +Timestamp validUntil + +Integer authStatus "PENDING / ACTIVE / REVOKED" + } + + class ExternalOperationRecord { + +Long operationLocalId + +Long authId + +String dmsOperationId "DMS异步操作流水ID" + +Integer requestType "1下发密码, 3删除密码等" + +String operationStatus "undone / success / failed / timeout" + +Integer pollCount + +Timestamp nextPollTime + } + + RailwayTrainingPlan "1" --> "0..*" RailwayEnrollment : 包含 + RailwayEnrollment "1" --> "1" RailwayAccommodation : 产生住宿安排 + RailwayAccommodation "0..*" --> "1" RailwayDormitoryInfo : 分配房间 + RailwayDormitoryInfo "1" --> "0..1" ExternalResourceMapping : 稳定映射 + ExternalResourceMapping "1" ..> "1" DmsPlaceRoom : 对应DMS物理房间 + DmsPlaceRoom "1" ..> "0..*" DmsDoorLock : 挂载智能门锁 (支持一房多锁) + RailwayAccommodation "1" --> "0..*" DoorAuthorization : 派生门锁授权 + DoorAuthorization "1" --> "0..*" ExternalOperationRecord : 追踪异步执行 +``` + +--- + +## 2. 关键实体边界与重大概念澄清 + +### 2.1 铁律:`DMS projectId ≠ RailwaySystem Training Plan ID` +- **DMS `projectId`**(示例中为 `319402846`、`3837087560`):代表 DMS 物联网平台的**物业项目/租户/公寓站点 ID**。属于系统级或物理站点级的静态上下文配置,由设备供应商分配。 +- **RailwaySystem `plan_id`**(如 `PLAN20260901`):代表铁路局/段内的**具体教学培训开班批次**。生命周期通常为几天到几周。 +- **红线**:严禁将 DMS `projectId` 错写成或动态传为学员开班 `planId`!在配置文件中应统一作为 `dms.project-id` 进行站点级静态受控配置。 + +### 2.2 铁律:本地住宿事实是唯一真源 (`Local Source of Truth`) +- DMS 的 `room.status`(0:空置, 1:已入住, 2:脏房等)仅为其平台或第三方酒店系统的简单标记,**绝对不能用于覆盖或推断 RailwaySystem 的床位与入住状态**。 +- 只有 RailwaySystem 自身的 `train_accommodation` 与 `train_enrollment` 能决定学员是否在住、床位是否已满、是否已报到。 +- DMS 门锁授权状态仅是硬件执行层面的技术属性(开门凭据是否已在锁具上成功生效)。 + +--- + +## 3. 房间与门锁映射策略深度比选 (Mapping Proposals) + +### 3.1 房间映射 (Local Dormitory ↔ DMS Place) +DMS 提供了两种查询房间的途径: +- 模糊查询列表:`POST /rest/place/room/list` +- 精确查询:`POST /rest/place/room/get`(入参:`building`, `floor`, `room`) + +| 方案 | 实现方式 | 优缺点分析 | 结论 | +| :--- | :--- | :--- | :--- | +| **方案 1:直接在 `train_dormitory_info` 加列** | 增加 `dms_place_id`, `dms_project_id`, `dms_sync_time` | **优点**:查询直观,单表读取。
**缺点**:污染本地物理宿舍模型;若未来接入其他门禁厂商(如海康、大华),列定义难以复用。 | 可作为一期过渡备选 | +| **方案 2:独立 `train_external_resource_mapping` 映射表** | 建立独立映射表:`local_resource_type ('DORMITORY')`, `local_id`, `vendor ('DMS')`, `external_id (placeId)`, `metadata_json` | **优点**:模型高度解耦;本地宿舍资源生命周期不受外部影响;天然支持多厂商与资源解绑/重绑。
**缺点**:需要多做一次关联查询。 | **推荐采纳** | + +**映射规则明确**: +- **严禁**每次下发授权时现场调用 `/rest/place/room/get` 进行文本字符串模糊匹配。网络抖动或楼栋名称微小差异(例如 "1号楼" vs "楼栋1")会导致授权链路阻断。 +- **必须**在管理端提供“房源映射绑定/同步”功能,预先在数据库中持久化 `localDormId ↔ dmsPlaceId` 的权威主键映射。 + +### 3.2 门锁映射:支持“一房多锁”模型 (`One Room to Multi-Locks`) +DMS 接口 `POST /rest/doorlock/list`(入参 `placeId`)返回的是一个门锁列表:`rows: [...]`。 +这证实了物理现实:**一个公寓房间可能没有门锁(0把)、有一把门锁(1把)、或者有多个门锁(例如外防盗门锁 + 内卧室锁,多把锁)**。 + +- **错误做法**:在 `train_dormitory_info` 中仅保留单一的 `lock_device_sn` 字符串,假设一房仅有一锁。 +- **推荐设计**: + - 系统必须能够处理一个 `placeId` 对应 `0..N` 把门锁的情况。 + - 当为学员下发该房间的开门权限时,集成适配器应查询该房间绑定的**所有有效门锁**,并为每把锁分别申请/下发开门凭据,生成对应的 `DoorAuthorization` 记录。 + +--- + +## 4. 详细字段级映射字典 (Field-Level Mapping Dictionary) + +### 4.1 宿舍/房间信息映射 +| RailwaySystem 本地字段 | DMS 对应字段 | 类型转换 / 映射规则 | 说明 | +| :--- | :--- | :--- | :--- | +| `train_dormitory_info.dorm_id` | `mapping.local_dorm_id` | `VARCHAR(32)` (UUID) | 本地主键 | +| - | `dms.place.id` / `placeId` | `BIGINT` | DMS 房间全局唯一主键 | +| `train_dormitory_info.dorm_building` | `dms.place.building` | 字符串精确比对/绑定 | 宿舍楼栋名 | +| `train_dormitory_info.dorm_floor` | `dms.place.floor` | 字符串精确比对/绑定 | 所在楼层 | +| `train_dormitory_info.dorm_room` | `dms.place.room` | 字符串精确比对/绑定 | 房间号 | +| - | `dms.place.projectId` | `BIGINT` | 系统级受控站点配置 | + +### 4.2 门锁设备信息映射 +| RailwaySystem 本地概念 | DMS 对应字段 | 类型 / 格式 | 说明 | +| :--- | :--- | :--- | :--- | +| `mapping.external_lock_id` | `doorlock.id` | `BIGINT` | DMS 门锁管理 ID | +| `train_dormitory_info.lock_device_sn` | `doorlock.hardwareId` | `VARCHAR(64)` (如 `5C9F539A`) | 门锁硬件 SN / 通信 ID | +| `device_battery` | `doorlock.battery` | `INT` (0~100, 255为未知) | 门锁电量 | +| `device_signal` | `doorlock.signal` | `INT` (-100~0, -128为未知) | 信号强度 (RSSI) | + +### 4.3 门禁授权与钥匙下发映射 (Online Password 场景) +| RailwaySystem 本地字段 | DMS 对应入参 / 字段 | 映射规则与约束 | +| :--- | :--- | :--- | +| `train_accommodation.accommodation_id` | - | 本地业务事实归属 | +| `target_lock_id` | `create.lockId` | 目标门锁的 DMS ID | +| `auth_start` (配合提早 Buffer) | `create.validStart` | 毫秒级时间戳 (`Long`) | +| `auth_end` (配合延后 Buffer) | `create.validUntil` | 毫秒级时间戳 (`Long`) | +| `student_name` | `create.holder` | 学员姓名 (可选,建议填写真实姓名) | +| - | `create.canOpenAntilock` | 固定设为 `false` (不可开反锁,保障私密安全) | +| `phone_country` | `create.phoneCountry` | 默认 `+86` | +| `student_phonenumber` | `create.phoneNumber` | 学员手机号 | +| `door_password` (一次性生成或安全受控) | `create.password` | 系统可生成4~8位纯数字密码,Pilot建议采用6位限时密码 | +| `credential_sub_type` | `create.subType` | 固定为 `4` (限时密码) | +| `external_key_id` | 返回的 `data.id` | DMS 钥匙全局 ID (退宿撤权时作为删除入参) | +| `dms_operation_id` | 返回的 `data.operationId` | DMS 异步下发任务 ID (用于10秒后状态轮询) | diff --git a/docs/development/dms-r3/04_auth_and_token_strategy.md b/docs/development/dms-r3/04_auth_and_token_strategy.md new file mode 100644 index 000000000..ba7ddb928 --- /dev/null +++ b/docs/development/dms-r3/04_auth_and_token_strategy.md @@ -0,0 +1,140 @@ +# 04 DMS 认证鉴权、Token 生命周期与高并发策略 (Auth and Token Strategy) + +> **审计基准**:DMS REST API 规范第一节《登录鉴权接口》 +> **核心风险**:**DMS 后台有限制,多次调用登录接口,只有最后一次生成的 Token 有效!** +> **设计目标**:建立零泄露凭据管理、内存安全缓存、防击穿 Single-Flight 并发锁与 401 自愈重试机制。 + +--- + +## 1. DMS 认证机制技术事实 (`DMS_AUTH_FACTS`) + +基于文档第 3~78 行的逐行事实提取: + +| 维度 | DMS 平台真实实现规则 | 对集成的要求与约束 | +| :--- | :--- | :--- | +| **登录接口** | `POST https://dms.dmsiot.com/public/rest/third-party-app/token/get` | 请求头 `Content-Type: application/json` | +| **凭据载荷** | Body 为 JSON: `{"appid": "...", "appsecret": "..."}` | 必须通过受控配置/环境变量加载,绝不硬编码 | +| **Token 有效期** | `expiresIn: 7200`(标准 2 小时,7200 秒) | 本地需跟踪到期时间戳 | +| **滑动刷新机制** | **“如果在 token 有效期内成功调用任一平台接口,则会刷新有效期,即从调用接口时间开始,2小时有效期”** | 只要系统保持定期调用或心跳,Token 可实现无感延续,无需反复调用登录接口 | +| **单点登录互斥性** | **“注意,后台有限制,多次调用登录接口,只有最后一次的 token 有效”** | **重大并发红线**:若有多个线程或集群节点并发调用登录,后发登录将立即废除前面的 Token,导致正在执行的其他请求瞬间遭遇 401 报错!必须实现单飞互斥锁 | +| **Token 传输方式** | 文档声明支持两种方式:
1. **Cookie**(放到请求 Cookie 中)
2. **URL 参数**(`?token=...`) | **不支持** `Authorization: Bearer `!严禁使用未经文档支持的 Header 格式 | +| **401 错误特征** | HTTP 状态码 `401 Unauthorized`,返回 JSON:
`{"status": 401, "error": "Unauthorized", "path": "..."}` | 统一由底层 HTTP 客户端拦截捕获,触发自动重登录恢复流 | +| **退出登录接口** | `POST https://dms.dmsiot.com/public/rest/security/logout` | 成功调用后该 Token 立即在服务端失效 | + +--- + +## 2. 生产凭据存储规范 (`Credential Policy`) + +### 2.1 敏感凭据零泄露规范 +- **禁止项**: + - 严禁将真实 `appid`、`appsecret`、`token` 写入 Git 仓库中的任何文件。 + - 严禁在 `application.yml` 或 `application-druid.yml` 中保存生产明文。 + - 严禁在代码异常堆栈、日志打印、HTTP Trace、错误提示信息中输出凭据。 +- **推荐配置格式**: + 在 `ruoyi-admin/src/main/resources/application.yml` 中仅保留占位符配置项,真实值由生产环境变量注入: + ```yaml + # DMS 智能门锁外部门禁平台配置 + dms: + base-url: ${DMS_BASE_URL:https://dms.dmsiot.com} + app-id: ${DMS_APP_ID:} + app-secret: ${DMS_APP_SECRET:} + project-id: ${DMS_PROJECT_ID:0} + token-expire-margin-seconds: 600 # 提前10分钟预防性刷新 + connect-timeout-ms: 5000 + read-timeout-ms: 10000 + ``` + +--- + +## 3. Token 传输安全策略 (`TOKEN_TRANSPORT_RECOMMENDATION`) + +### 3.1 传输模式比选与推荐 +- **Cookie 传输(首选推荐)**: + - HTTP 请求头:`Cookie: token={token_value}`。 + - **优势**:Token 位于 HTTP Header,绝不会记录在反向代理(Nginx/网关)的 `access.log` 中,也不会泄露在浏览器历史记录、Referer 标头或请求 URL 监控中。 +- **URL 传参(降级备选,需日志脱敏)**: + - 如果第三方 DMS 平台的某些 API 对 Cookie 解析存在兼容性缺陷,被迫采用 `?token=...`: + - **必须实施全局 Log Redaction**:在所有日志拦截器、HttpClient 监控日志中,通过正则表达式 `(?<=token=)[^&]+` 将所有 Token 字符串替换为 `******`。 + +--- + +## 4. `DmsTokenManager` 高并发防击穿架构 (`TOKEN_CONCURRENCY_STRATEGY`) + +### 4.1 架构设计原理 +为应对“多次登录导致先前 Token 立即失效”的严苛限制,必须采用 **进程级单例 Token 缓存 + Single-Flight 并发互斥锁 + 提前刷新 + 401 兜底自愈** 架构。 + +```mermaid +sequenceDiagram + autonumber + actor Thread1 as 业务线程 A + actor Thread2 as 业务线程 B + participant TM as DmsTokenManager + participant Cache as 本地 AtomicReference 缓存 + participant Lock as Single-Flight 并发锁 + participant DMS as DMS 鉴权服务器 + + Thread1->>TM: getToken() + Thread2->>TM: getToken() + TM->>Cache: 检查当前缓存中的 Token + alt 缓存有效 (当前时间 < 到期时间 - 10分钟) + Cache-->>TM: 返回有效 Token + TM-->>Thread1: 复用有效 Token + TM-->>Thread2: 复用有效 Token + else 缓存缺失或即将到期 + TM->>Lock: 尝试获取登录排他锁 (ReentrantLock) + activate Lock + Note over Lock: 线程 A 成功获取锁,线程 B 阻塞等待 + Thread1->>Lock: 再次双重检查缓存 (DCL) + Thread1->>DMS: POST /public/.../token/get + DMS-->>Thread1: 返回 200 OK (新 Token, expiresIn: 7200) + Thread1->>Cache: 更新缓存及过期时间戳 + Thread1-->>Lock: 释放排他锁 + deactivate Lock + Note over Lock: 线程 B 被唤醒,再次检查缓存 (DCL) + Thread2->>Cache: 命中线程 A 刚更新的 Token + Cache-->>Thread2: 返回最新 Token + TM-->>Thread1: 返回新 Token + TM-->>Thread2: 复用新 Token (无重复登录!) + end +``` + +### 4.2 核心并发控制机制说明 +1. **双重检查锁定 (Double-Checked Locking, DCL)**: + - 当多个业务请求同时探测到 Token 缺失或过期时,仅有拿到 `ReentrantLock` 的首个线程发起真实 HTTP 登录请求。 + - 处于等待队列的后续线程在获得锁后,先进行二次检查(DCL);若发现前序线程已刷新成功,则直接读取最新 Token,**绝不发起第二次远程登录**。 +2. **滑动过期时间更新 (`Sliding Expiry Tracking`)**: + - 鉴于 DMS 规定“成功调用任一平台接口会刷新 2 小时有效期”,HttpClient 拦截器在收到任意 200 响应时,通知 `DmsTokenManager.touch()`,将本地缓存的失效时间向后顺延,减少不必要的登录刷新。 +3. **预防性主动刷新 (`Proactive Refresh`)**: + - 设置 `margin = 600秒`(10分钟)。当有效剩余时间不足 10 分钟时,异步触发平滑刷新,杜绝业务请求在临界点遇到 401。 + +--- + +## 5. 401 自愈与单次安全重试设计 (`DMS 401 Recovery`) + +当由于外部原因(例如运维人员在第三方控制台手动注销、或 DMS 服务端异常重启导致 Session 丢失)出现 `401 Unauthorized` 时的处理规范: + +``` +业务请求触发调用 + │ + ▼ +使用本地 Token 调用 DMS API + │ + ├─────────────────────────────────┐ + ▼ [成功 200 OK] ▼ [失败 401 Unauthorized] +正常返回业务数据 立即令本地缓存的 Token 失效 (invalidate) +通知 TokenManager.touch() 延展寿命 │ + ▼ + 单飞排他锁获取新 Token (Single-Flight Login) + │ + ├─────────────────────────┐ + ▼ [重登录成功] ▼ [重登录失败] + 安全重试原始业务请求 (最多 1 次) 标记操作失败 (FAILED) + │ 记录审计日志 + ├──────────────┐ 不影响本地住宿业务事实 + ▼ [重试成功] ▼ [依然401] + 成功返回 熔断抛出异常 +``` + +### 5.1 恢复红线守则 +- **单次重试原则**:因 401 触发的自动重试**上限严格为 1 次**。严禁设计任何 `while(true)` 或大于 1 次的递归重试,防止由于网络异常或密码错误引发死循环或轰炸外部服务器。 +- **业务解耦原则**:若重新登录失败,DmsAdapter 应抛出封装后的 `DmsAuthenticationException`,将门禁任务状态置为 `RETRY_WAIT` 或 `FAILED`,**绝不可向上传播导致本地住宿分配主事务发生回滚**。 diff --git a/docs/development/dms-r3/05_adapter_architecture.md b/docs/development/dms-r3/05_adapter_architecture.md new file mode 100644 index 000000000..6d2f16d63 --- /dev/null +++ b/docs/development/dms-r3/05_adapter_architecture.md @@ -0,0 +1,175 @@ +# 05 门禁集成适配器架构与厂商无关接口设计 (Adapter Architecture) + +> **设计核心**:**强边界解耦与高内聚设计**。 +> 住宿业务层(Accommodation Service)对外部物联网厂商的协议细节、Token、URL、整数请求类型完全无感知。 +> 针对门禁控制定义**厂商中立的抽象网关接口 (`DoorAccessGateway`)**,DMS 专用协议转换完全收敛在 `DmsAccessAdapter`。 + +--- + +## 1. 系统分层集成架构图 (Mermaid) + +```mermaid +flowchart TD + subgraph UI_Layer["前端与接入层 (Web / Controller)"] + AC["TrainAccommodationController\n(住宿分配 / 换房 / 退宿)"] + DC["TrainDoorAccessController\n(门禁状态查询 / 重试 / 受控应急)"] + end + + subgraph Business_Layer["核心业务领域层 (Domain Service)"] + AS["TrainAccommodationService\n(维护住宿事实真源 / 状态推进)"] + AGS["DoorAccessOrchestrationService\n(授权编排: 提早下发 / 补偿 / 批量处理)"] + end + + subgraph Gateway_Boundary["厂商无关抽象边界 (Vendor-Agnostic Gateway)"] + DAG["<>\nDoorAccessGateway\n+grantAccess(GrantCommand): OperationResult\n+revokeAccess(RevokeCommand): OperationResult\n+queryOperation(QueryCommand): OperationStatusResult\n+queryLockHealth(HealthCommand): LockHealthResult\n+remoteUnlock(UnlockCommand): OperationResult"] + end + + subgraph Integration_DMS["DMS 专用集成适配层 (ruoyi-train/integration/dms)"] + ADAPTER["DmsDoorAccessGateway\n(协议转换 / 参数映射 / 状态归一化)"] + + subgraph DMS_Core["DMS 核心支撑组件"] + TM["DmsTokenManager\n(单飞互斥登录 / 缓存 / 401自愈)"] + PROP["DmsProperties\n(受控环境变量配置)"] + POLLER["DmsOperationPoller\n(Spring/Quartz 10秒对账轮询器)"] + end + + subgraph Clients["细分领域 HTTP 客户端 (Feign / RestTemplate)"] + PLC["DmsPlaceClient\n(楼栋/楼层/房间查询)"] + DLC["DmsDoorLockClient\n(门锁列表/电量信号)"] + KC["DmsKeyClient\n(在线密码/在线卡片)"] + OPC["DmsOperationClient\n(query-operation-status)"] + end + end + + subgraph Remote_DMS["外部物联网云平台 (DMS IoT Platform)"] + CLOUD["DMS REST API\n(https://dms.dmsiot.com)"] + end + + AC --> AS + DC --> AGS + AS --> AGS + AGS --> DAG + DAG <|.. ADAPTER : 实现 + ADAPTER --> TM + ADAPTER --> PROP + ADAPTER --> PLC + ADAPTER --> DLC + ADAPTER --> KC + ADAPTER --> OPC + POLLER --> DAG + PLC --> CLOUD + DLC --> CLOUD + KC --> CLOUD + OPC --> CLOUD +``` + +--- + +## 2. 厂商无关内部标准接口契约 (`Vendor-Agnostic Interface`) + +为确保沈阳房建公寓段学员管理系统未来平滑支持其他设备供应商(如海康威视、大华或铁路专用门禁系统),设计中立门禁网关接口 `DoorAccessGateway`: + +```java +package com.ruoyi.train.integration.gateway; + +import com.ruoyi.train.integration.gateway.dto.*; + +/** + * 厂商中立门禁系统服务网关接口 + * 严禁在该接口及方法签名中暴露任何具体厂商(如 DMS)的特定字段、类名或数字枚举。 + */ +public interface DoorAccessGateway { + + /** + * 下发门禁通行授权 (支持在线密码、在线卡片等凭据) + * + * @param command 统一授权下发指令 (包含目标房间、门锁标识、学员信息、时间窗口、凭据要求) + * @return 异步操作受理结果 (包含本地操作ID、外部受理ID、初步状态) + */ + AccessOperationResult grantAccess(GrantAccessCommand command); + + /** + * 撤销已生效或下发中的门禁授权 (退宿、退训、换房时调用) + * + * @param command 统一撤销指令 (包含外部钥匙ID、目标硬件标识等) + * @return 异步操作受理结果 + */ + AccessOperationResult revokeAccess(RevokeAccessCommand command); + + /** + * 统一查询异步操作执行状态 (对账轮询) + * + * @param command 操作状态查询指令 (包含 operationId, hardwareId, 原始业务类型) + * @return 归一化后的设备执行结果 (SUCCESS, UNDONE, FAILED, TIMEOUT) + */ + OperationStatusResult queryOperationStatus(QueryOperationCommand command); + + /** + * 查询指定房间/门锁的物理运行健康度 (电量、信号、在线状态) + * + * @param command 查询健康度指令 + * @return 归一化设备健康度指标 + */ + DeviceHealthResult queryDeviceHealth(QueryHealthCommand command); + + /** + * 紧急远程开锁 (受控应急功能,仅限特定权限管理员并记录审计追踪) + * + * @param command 远程开锁指令 (必须携带操作人、两票审核号、操作原因) + * @return 异步操作受理结果 + */ + AccessOperationResult remoteUnlock(RemoteUnlockCommand command); +} +``` + +--- + +## 3. 推荐工程目录结构规范 (`Package Structure`) + +后续 R.3B 实现时,建议在 `ruoyi-train` 模块中创建如下包结构,本阶段仅冻结设计,**禁止创建 Java 文件**: + +``` +ruoyi-train +└── src/main/java/com/ruoyi/train + ├── domain # 领域实体 (TrainAccommodation, TrainDormitoryInfo, etc.) + ├── service # 核心业务服务 (ITrainAccommodationService, etc.) + └── integration # 外部集成根包 + ├── gateway # 【厂商无关抽象层】 + │ ├── DoorAccessGateway.java + │ ├── dto # 统一标准入参/出参 DTO + │ │ ├── GrantAccessCommand.java + │ │ ├── RevokeAccessCommand.java + │ │ ├── AccessOperationResult.java + │ │ └── OperationStatusResult.java + │ └── enums # 标准业务枚举 (CredentialType, NormalizedOpStatus) + └── dms # 【DMS 厂商专用集成适配层】 + ├── config # 配置类 (DmsProperties, DmsConfiguration) + ├── auth # 认证与 Token 管理 (DmsTokenManager, DmsTokenResponse) + ├── client # 底层 HTTP 交互客户端 + │ ├── DmsBaseClient.java + │ ├── DmsPlaceClient.java + │ ├── DmsDoorLockClient.java + │ ├── DmsKeyClient.java + │ └── DmsOperationClient.java + ├── dto # DMS 原始报文映射 DTO (只在 integration.dms 内部可见) + │ ├── request # DMS 专用请求实体 (如 DmsOnlinePasswordCreateRequest) + │ └── response # DMS 专用响应实体 (如 DmsApiResponse, DmsKeyData) + ├── adapter # 核心适配器实现 + │ └── DmsDoorAccessGateway.java (实现 DoorAccessGateway 接口) + ├── scheduler # 异步任务与定时对账 + │ └── DmsOperationPoller.java (10秒延迟轮询与超时处理) + └── util # 专用编码工具 (CardNumberCodec: 读卡器十进制与16进制小端转换) +``` + +--- + +## 4. 适配器边界与职责隔离红线 (`Adapter Boundary Rules`) + +1. **DTO 边界完全隔离**: + - DMS 的返回体对象(如 `DmsApiResponse`)的生命周期**绝不允许超出 `com.ruoyi.train.integration.dms` 包范围**。 + - `DmsDoorAccessGateway` 必须负责将其翻译为中立的 `AccessOperationResult`,禁止向上层抛出 DMS 内部原始 JSON。 +2. **状态与数字编码归一化**: + - DMS 使用数字 `requestType`(1=下发密码, 2=在线卡片, 3=删除密码, 5=远程开锁)。业务 Service **严禁传递数字 1 或 3**,必须传递强类型枚举 `CredentialOperationType.PASSWORD_GRANT`。适配器内部完成枚举到数字 `requestType` 的转换。 + - DMS 返回的 `operationStatus` 字符串(`"undone"`, `"success"`, `"failed"`, `"timeout"`)由适配器转换为内部标准领域枚举 `NormalizedOperationStatus`。 +3. **第三方异常吞吐隔离**: + - 外部网络超时、401 故障、DMS 内部报错统一由适配器包装为 `IntegrationException` 及其子类,不得泄露第三方平台的底层堆栈至前端。 diff --git a/docs/development/dms-r3/06_authorization_state_machine.md b/docs/development/dms-r3/06_authorization_state_machine.md new file mode 100644 index 000000000..5eca97952 --- /dev/null +++ b/docs/development/dms-r3/06_authorization_state_machine.md @@ -0,0 +1,137 @@ +# 06 门锁授权与异步操作状态机设计 (Authorization State Machine) + +> **语义铁律**:**DMS HTTP 200 `success: true` 仅代表“请求已保存/已受理 (REQUEST_ACCEPTED)”,绝不代表“门锁设备执行成功 (DEVICE_SUCCESS)”!** +> **门锁生效滞后性**:门锁作为电池供电的无线物联网终端,下发指令需要经过网关转发与微功耗空中唤醒,官方明确要求**调用成功 10 秒后再行查询操作状态**。 +> **设计目标**:建立双层解耦状态机(业务授权状态 vs 设备操作状态),消除状态混淆。 + +--- + +## 1. 双层状态体系解耦定义 + +必须区分两个不同生命周期的状态机: +1. **门禁授权业务状态 (`DoorAuthorizationStatus`)**:站在学员与住宿业务视角,表达该住宿事实当前是否具备开门权限。 +2. **外部硬件操作状态 (`ExternalOperationStatus`)**:站在异步任务与设备网络视角,表达向 DMS 派发的具体单次指令的执行进展。 + +--- + +## 2. 状态映射与状态定义字典 + +### 2.1 门禁授权业务状态 (`DoorAuthorizationStatus`) +| 本地业务状态枚举 | 业务含义 | 允许的操作 | +| :--- | :--- | :--- | +| **`UNISSUED`** (待下发) | 住宿已确认,尚未向门锁发起授权下发请求 | 下发授权 (`grant`) | +| **`ISSUING`** (下发中) | 已向 DMS 发送创建钥匙请求,已获得 `operationId`,正在等待 10 秒后对账确认 | 刷新状态、等待轮询 | +| **`ACTIVE`** (已生效/成功) | 轮询确认设备返回 `operationStatus = success`,学员可正常刷卡/输密码开门 | 查看凭据、撤销授权 (`revoke`) | +| **`ISSUE_FAILED`** (下发失败) | DMS 或设备明确返回失败,或超时重试达到上限 | 重新下发 (`retry`)、人工改派房间 | +| **`REVOKING`** (撤销中) | 学员退宿/调房,已向 DMS 提交删除钥匙请求,正在等待对账确认 | 刷新状态、等待轮询 | +| **`REVOKED`** (已注销) | 轮询确认钥匙已被设备成功删除,权限已彻底关闭 | 归档留存、不可逆 | +| **`EXPIRED`** (已到达有效期) | 超过 DMS 凭据设定的 `validUntil` 有效期边界(离线过期机制需供应商确认) | 自动归档、主动撤权兜底 | + +### 2.2 外部硬件异步操作状态 (`ExternalOperationStatus`) 与 DMS 映射 +| 本地操作状态枚举 | 对应 DMS API 原始字段 | 状态说明与处理策略 | +| :--- | :--- | :--- | +| **`CREATED`** | 内部构建指令,尚未发送 | 准备调用 HTTP 客户端 | +| **`REQUESTED`** | HTTP 请求发出,等待响应 | 网络传输中 | +| **`ACCEPTED`** | DMS 返回 `success: true`,携带 `operationId` | 记录 `operationId`,调度器挂起 10 秒轮询任务 | +| **`PENDING_DEVICE`** | 轮询返回 `operationStatus: "undone"` | 设备尚未完成执行,计算下次退避轮询时间 | +| **`SUCCESS`** | 轮询返回 `operationStatus: "success"` | 终态:设备已成功落锁/删除,触发推进业务状态 | +| **`FAILED`** | 轮询返回 `operationStatus: "failed"` | 终态:设备返回明确失败,记录 `resultDetails` 与错误原因 | +| **`TIMEOUT`** | 轮询返回 `operationStatus: "timeout"` | 终态:DMS 判定设备通信超时(10秒未响应),触发重试或报警 | +| **`RETRY_WAIT`** | 遭遇可恢复异常(如网络抖动),等待下一次重试 | 依据退避算法重新放入调度队列 | + +--- + +## 3. 门锁授权端到端生命周期状态流转图 (Mermaid) + +```mermaid +stateDiagram-v2 + [*] --> UNISSUED : 住宿安排确认 (Accommodation Confirmed) + + state "授权下发阶段 (Grant Flow)" as GrantFlow { + UNISSUED --> ISSUING : 触发下发 (报到或临近入住) + + state ISSUING { + [*] --> Op_Requested : 调用 POST /online-password-key/create + Op_Requested --> Op_Accepted : DMS 返回 200 (获得 operationId) + Op_Accepted --> Op_Polling : 延迟等待 10 秒 + Op_Polling --> Op_Undone : 查询状态返回 undone + Op_Undone --> Op_Polling : 间隔 5 秒重试 (最大3次) + } + + ISSUING --> ACTIVE : 查询状态返回 success + ISSUING --> ISSUE_FAILED : 状态返回 failed 或多次 timeout + ISSUE_FAILED --> ISSUING : 管理员手动重试 / 定时补偿重试 + } + + state "正常使用阶段 (Active Use)" as ActiveUse { + ACTIVE --> ACTIVE : 门锁正常开门 / 查询电量信号 + ACTIVE --> EXPIRED : 超过 validUntil 有效期边界 + } + + state "权限注销阶段 (Revoke Flow)" as RevokeFlow { + ACTIVE --> REVOKING : 学员退宿 / 提前退训 / 换房 + ISSUE_FAILED --> REVOKING : 换房或作废 + + state REVOKING { + [*] --> Revoke_Requested : 调用 POST /online-password/delete + Revoke_Requested --> Revoke_Accepted : DMS 返回 200 (获得 operationId) + Revoke_Accepted --> Revoke_Polling : 延迟等待 10 秒 + Revoke_Polling --> Revoke_Polling : undone (重试轮询) + } + + REVOKING --> REVOKED : 查询状态返回 success + REVOKING --> REVOKE_FAILED : 状态返回 failed 或 timeout + REVOKE_FAILED --> REVOKING : 紧急人工重试撤销 + } + + REVOKED --> [*] + EXPIRED --> [*] +``` + +--- + +## 4. 关键流转分支与语义判定细节 + +### 4.1 成功下发分支 (Happy Path) +1. 触发下发:适配器调用 `/rest/doorlock/online-password-key/create`。 +2. DMS 同步响应: + ```json + { + "success": true, + "data": { + "id": 4945, + "placeId": 1320, + "lockId": 372, + "operationId": "0872887e-464b-4f44-a0cc-057871ba0f65" + } + } + ``` + **本地动作**:持久化 `external_key_id = 4945`,创建外部操作记录(`operationId = 0872887e...`, `status = ACCEPTED`),授权主表推进为 `ISSUING`。 +3. 轮询调度器等待 10 秒:调用 `/rest/doorlock/query-operation-status`(入参 `requestType = 1`, `operationId = ...`)。 +4. 轮询响应: + ```json + { + "success": true, + "data": { + "operationStatus": "success" + } + } + ``` + **本地动作**:更新操作记录为 `SUCCESS`,授权主表推进为 `ACTIVE`(生效可用)。 + +### 4.2 设备超时分支 (`Timeout Handling`) +- 若 10 秒后轮询返回: + ```json + { + "success": true, + "data": { + "operationStatus": "timeout", + "message": "Device response timeout" + } + } + ``` +- **判定规则**:门锁可能休眠过深、网关信号不佳或网关离线。 +- **系统动作**: + 1. 不得无限循环调用。 + 2. 若配置了自动补偿,可追加 1 次重试查询;若依然超时,将操作置为 `TIMEOUT`,授权主表置为 `ISSUE_FAILED`。 + 3. 页面以警告色徽标展示“设备下发超时”,提供【重新下发】按钮供管理员现场排障。 diff --git a/docs/development/dms-r3/07_retry_idempotency_and_compensation.md b/docs/development/dms-r3/07_retry_idempotency_and_compensation.md new file mode 100644 index 000000000..7d92de435 --- /dev/null +++ b/docs/development/dms-r3/07_retry_idempotency_and_compensation.md @@ -0,0 +1,155 @@ +# 07 异步轮询、失败重试、本地幂等与换房补偿设计 (Retry, Idempotency and Compensation) + +> **可靠性第一原则**:**DMS 外部设备故障绝不允许导致本地住宿安排事实发生回滚 (`DMS Unavailable Principle`)**。 +> 住宿事实分配成功后,外部设备权限下发即使遭遇网络分区、网关断电或服务不可用,也只在门禁层体现为 `PENDING` 或 `ISSUE_FAILED`,供后台调度补偿或人工排障。 + +--- + +## 1. 异步轮询调度策略 (`Polling Strategy`) + +### 1.1 严禁主线程阻塞等待 +- 官方文档明示:**“门锁的实际执行结果,务必在调用相关操作后 10 秒查询”**。 +- **严重红线**:绝对禁止在 Web 请求线程(Controller/Service)中执行 `Thread.sleep(10000)`!这会迅速耗尽 Tomcat 工作线程池,引发系统雪崩。 +- **轮询架构设计**: + ``` + Web 请求 (如确认入住) + │ + ▼ + 本地持久化 Accommodation (已入住) 与 DoorAuthorization (ISSUING) + 提交本地数据库事务 (Commit Local DB) + │ + ▼ + 异步派发调用 DMS 创建钥匙 -> 返回 operationId (耗时 ~200ms) + 本地记录 ExternalOperationRecord (status = ACCEPTED, next_poll_time = now + 10s) + 快速向前端返回 HTTP 200 (附带提示:“住宿已分配,门锁授权下发中”) + │ + ▼ + 后台调度引擎 (Quartz / Spring Scheduled ThreadPool - 专用IO线程池) + 扫描 next_poll_time <= NOW() 的任务 -> 调用 /doorlock/query-operation-status + 更新操作记录与授权记录状态 -> 若为 success 则推为 ACTIVE + ``` + +### 1.2 门锁 vs 网关轮询参数隔离 +官方文档中存在两套截然不同的轮询规约,严禁混淆: +- **门锁操作轮询(密码/卡片/远程开锁)**: + - `INITIAL_DELAY`: **10 秒**(门锁低功耗唤醒通常需 5~8 秒)。 + - `POLL_INTERVAL`: **5 秒**。 + - `MAX_POLL_ATTEMPTS`: **3 次**(总超时窗口约 25 秒)。 +- **网关在线状态轮询(网关刷新)**: + - `INITIAL_DELAY`: **3 秒**。 + - `POLL_INTERVAL`: **1 秒**。 + - `MAX_POLL_ATTEMPTS`: **5 次**。 + +--- + +## 2. 失败分类与重试策略 (`Retry Strategy`) + +**严禁**只要发生错误就盲目重新调用 `create` 接口,这会导致同一学员在门锁中堆积多张相同密码或卡片,占满锁具内存。必须对故障进行严格分类: + +```mermaid +flowchart TD + FAIL[发生异常 / 收到非成功结果] --> CHECK{判断故障类型} + + CHECK -->|A. 请求明确未发送\n(连接超时/本地校验失败)| RETRY_A[安全重试创建: 指数退避\n重试间隔 2s, 4s, 8s, 上限3次] + + CHECK -->|B. DMS明确返回业务拒绝\n(400/Invalid Room/Invalid Lock)| FAIL_B[不可恢复业务失败\n标记 ISSUE_FAILED, 记录错误日志\n停止重试, 提示管理员核对房源] + + CHECK -->|C. 成功获得 operationId\n但轮询返回 undone| WAIT_C[设备执行中: 继续等待下一次轮询\n未超上限前保持 PENDING] + + CHECK -->|D. 网络读取超时(Read Timeout)\n无法确认DMS是否已受理| RECONCILE[状态未决 (UNKNOWN)\n禁止立即重新创建!\n先执行对账查询或主动撤销后新建] +``` + +### 2.1 未决状态对账策略 (`UNKNOWN / RECONCILE`) +- 当本地向 DMS 发送 `POST /online-password-key/create` 时遭遇 `SocketTimeoutException`(网络读取超时): + - **风险**:DMS 服务端可能已经处理成功并把钥匙下发到了锁端,只是响应包丢失。若直接重发创建请求,门锁内将存在两套密码。 + - **对账方案**: + 1. 标记当前操作为 `RECONCILE_PENDING`。 + 2. 调度器调用开门钥匙列表或按房间查询当前已有钥匙。 + 3. 若能识别出刚才持有人及手机号生成的有效钥匙,则绑定该钥匙 ID 并恢复轮询。 + 4. 若确认未成功生成,方可发起二次 `create`。 + +--- + +## 3. 本地业务幂等设计 (`Idempotency Strategy`) + +- **外部约束**:DMS API 原生并不支持 `Idempotency-Key` 请求头。 +- **本地防护机制**: + - 幂等联合主键定义:`IdempotencyKey = Hash(accommodation_id + credential_type + grant_version)`。 + - 在 `train_door_authorization` 表中建立唯一约束或在 Service 缓存层加分布式互斥锁。 + - **业务效果**:无论操作员在前端如何频繁、多次点击“下发权限”按钮,对于同一个住宿安排的同一个版本,**有且仅有一条处于 `ISSUING` 或 `ACTIVE` 状态的授权记录**,绝不产生重复钥匙。 + +--- + +## 4. 业务触发与撤权决策比选 (Business Triggers) + +### 4.1 门禁授权下发触发时机比选 (`ACCESS_GRANT_TRIGGER_RECOMMENDATION`) +| 候选方案 | 业务流程说明 | 优势 | 风险与缺点 | 推荐结论 | +| :--- | :--- | :--- | :--- | :--- | +| **方案 A:住宿分配后立即下发** | 管理员排宿完成即刻向门锁下发限时密码 | 提前就绪,报到现场零等待 | 若学员临时取消参训,需频繁撤权;提前泄露密码可能导致未报到人员提前进入 | 不推荐 | +| **方案 B:报到核验确认后触发 (首选)** | 学员现场在《报到管理》通过人脸/刷卡核验后,异步触发门禁下发 | **安全严密**:确保人已到校才给钥匙;与现场报到事件紧密联动 | 报到高峰期对 DMS 带来并发请求压力,需队列平滑削峰 | **(推荐) 提交甲方确认** | +| **方案 C:临近住宿开始时间下发** | 开班前 2 小时定时扫描下发 | 自动化高 | 无法过滤未报到/缺勤学员 | 备选 | + +### 4.2 授权有效时间窗口缓冲设计 (`Grant Validity Buffer`) +- 严禁将门锁密码生效时间与培训班作息严格卡死到分钟(例如培训班 9:00 开始,密码 9:00:00 才生效)。 +- **缓冲机制 (Buffer)**: + - `accessStartOffset`(提早进房时间):建议可配置提前 **2~4 小时**(允许学员提早入住休整)。 + - `accessEndOffset`(退宿宽限时间):建议培训班结束当天可配置延后 **2~4 小时**(允许学员打包行李离开)。 + - 有效期计算公式: + $$\text{validStart} = \text{Accommodation.assignedStart} - \text{accessStartOffset}$$ + $$\text{validUntil} = \text{Accommodation.assignedEnd} + \text{accessEndOffset}$$ + +### 4.3 培训结训与学员退训撤权策略 +1. **培训计划结训 (`Plan Completion`)**: + - **主动撤权为主**: + - 流程:`Plan Complete` → 对仍处于 `ACTIVE` 状态的 `Door Authorization` 发起主动 `Revoke` → 派发 DMS `delete` 指令 → 保存 `operationId` → 后台异步轮询确认设备执行结果。 + - **`validUntil` 有效期边界兜底**: + - DMS Credential 的 `validUntil` 提供授权有效期边界;培训结束时 RailwaySystem 仍坚决主动发起撤权。 + - 设备离线情况下的本地过期机制、RTC 硬件时钟行为和离线保证级别需供应商确认(禁止假设设备端 RTC 机制已完成实物验证)。 +2. **学员中途退训 (`Withdrawal`)**: + - **强制触发主动撤销 (`WITHDRAW_REVOKE_RECOMMENDATION = IMMEDIATE_REVOKE`)**。学员一旦退训,立即将其名下处于 `ACTIVE` 的门锁授权状态置为 `REVOKING` 并派发删除指令。 +3. **学员请假离校 (`Leave`)**: + - **`LEAVE_REVOKE_RECOMMENDATION = NEEDS_BUSINESS_CONFIRMATION`**。请假期间是否收回房间门禁权限,取决于公寓管理条例(是否保留个人物品及床位),需交由业务部门决策。 + +--- + +## 5. 调房换房补偿策略 (`TRANSFER_COMPENSATION_STRATEGY`) + +在学员需要从原宿舍 A 调整至新宿舍 B 的业务场景下,涉及两把不同门锁的设备指令,**绝不可塞进一个本地数据库事务中同步执行**。 + +```mermaid +sequenceDiagram + autonumber + actor Admin as 管理员/宿管 + participant Service as 住宿调度服务 + participant DB as 本地数据库 + participant Gateway as 门禁网关 + participant DMS as DMS 平台 + + Admin->>Service: 发起换房 (学员 X: 房间 A -> 房间 B) + Service->>DB: 开启本地事务 + Service->>DB: 更新原住宿记录 A (status=已退宿/调出) + Service->>DB: 创建新住宿记录 B (status=已入住/调入) + Service->>DB: 提交本地事务 (业务事实先落盘) + + Note over Service,Gateway: 阶段一:先发新房权限 (保障学员有地方住) + Service->>Gateway: grantAccess(新房 B, 学员 X) + Gateway->>DMS: POST /online-password-key/create (锁 B) + DMS-->>Gateway: 返回 accepted (新钥匙 ID_B) + + alt 新房权限下发成功 + Note over Service,Gateway: 阶段二:安全撤销旧房权限 + Service->>Gateway: revokeAccess(旧房 A, 旧钥匙 ID_A) + Gateway->>DMS: POST /online-password/delete (锁 A) + DMS-->>Gateway: 返回 accepted (撤销流水) + Service-->>Admin: 换房完成,新房密码已生效,旧房权限已注销 + else 新房权限下发遭遇不可恢复故障 (如锁B损坏/网络中断) + Note over Service: 触发补偿流程 (Compensation) + Service->>DB: 记录调房门禁异常报警 + Service->>Service: 绝不撤销旧房权限 (保留原钥匙 A,学员不流落走廊) + Service-->>Admin: 换房业务记录已建立,但新宿舍门锁下发失败,请人工介入处理 + end +``` + +### 5.1 换房核心准则总结 +- **“先发新、后撤旧”原则**:绝不能先删除了旧房权限再去发新房。若新房门锁出现硬件故障或网络不通,学员将两头落空无法进门。 +- **降级容错策略**:新房授权失败时,保留旧房权限,并在前端控制台突出显示“门禁换房未同步”,由管理员一键重试或安排备用钥匙。 diff --git a/docs/development/dms-r3/08_security_and_audit.md b/docs/development/dms-r3/08_security_and_audit.md new file mode 100644 index 000000000..3910077ce --- /dev/null +++ b/docs/development/dms-r3/08_security_and_audit.md @@ -0,0 +1,117 @@ +# 08 敏感数据合规、凭据安全与门禁审计体系 (Security and Audit) + +> **安全最高红线**:智能门锁直接关联学员人身财产安全与公寓物理边界防线。 +> **设计原则**:**最小知悉原则、凭据零明文长期留存、全链路日志脱敏、关键动作两票制双人审计轨迹**。 + +--- + +## 1. 门禁凭据类型全面比选与首期试点推荐 + +### 1.1 四类门锁开门凭据深度对比矩阵 + +| 评估维度 | 在线临时密码 (Online Password) | 在线实体学员卡 (Online Card) | 离线算法密码 (Offline Password) | 远程网络开锁 (Remote Unlock) | +| :--- | :--- | :--- | :--- | :--- | +| **实施复杂度** | **低**(纯软件 API 交互) | **中**(需发卡设备与现场制卡) | 高(需手工向学员通知长串数字) | 低(API 触发) | +| **硬件与耗材依赖** | **零硬件依赖**(手机/短信/凭条) | **依赖物理卡片**及 USB 读写卡器 | 零硬件依赖 | 依赖集中网关长连接 | +| **是否需要学员手机号** | 是(DMS 接口要求 `phoneNumber`) | 否(仅需卡号十六进制) | 否 | 否 | +| **支持精准有效期** | **支持**(毫秒级 `validStart/Until`) | **支持**(毫秒级 `validStart/Until`) | 仅支持固定颗粒度(如天/时) | 一次性即时脉冲触发 | +| **远程主动撤权能力** | **强**(可随时调用 delete 接口撤销) | **强**(可随时调用 delete 接口撤销) | **极弱**(撤权需在锁上输入清空码) | 不适用 | +| **门锁开门审计追溯** | **强**(记录包含持有人与密码标识) | **强**(记录包含卡号与持有人) | 弱(无法区分具体使用者身份) | 强(记录远程操作日志) | +| **离线网络容灾能力** | 弱(下发与撤销时门锁必须在线) | 弱(下发与撤销时门锁必须在线) | **极强**(门锁离线依然可验证算法) | 无(断网完全无法执行) | +| **现场运维与损耗成本** | **零损耗成本** | 存在卡片丢失、补卡、消磁运维成本 | 需人工现场解决锁具失步问题 | 零物理成本,但安全风险极高 | + +### 1.2 首期接入推荐结论 (`RECOMMENDED_PILOT_CREDENTIAL`) +- **【首推试点方案】在线限时密码 (Online Password)**: + - **采纳理由**:零硬件附加成本;无需采购现场读写发卡器;完全通过软件 API 自动派发与注销;天然支持培训期间精准起止时间;开门流水完整可溯。 +- **【备选方案】在线实体卡 (Online Card)**: + - 若沈阳房建公寓段已统一配发带有芯片的学员出入证/铁路一卡通,可作为二阶段或并行备选,需配备发卡器及编码转换组件。 +- **【其他方案评估】离线密码与二维码钥匙**: + - 离线密码缺乏主动撤销能力,一期明确排除 (`OUT_OF_SCOPE`)。 + - 二维码/手机钥匙虽在官方文档中存在明确端点 (`POST /rest/doorlock/qrcode/create` 与 `POST /rest/qrcode/delete`),分类为 `DOCUMENTED`,但返回体存在字段污染且无二维码具体载荷内容,其实际钥匙方案可行性归为 `NEEDS_BUSINESS_CONFIRMATION`,一期不作为首选试点方案。 + +--- + +## 2. 在线实体卡卡号编码规约与 `CardNumberCodec` 设计 + +官方文档对在线卡片(`POST /rest/doorlock/online-card/create`)的卡号规定了非常特殊的**小端字节序十六进制转换规则**: + +``` +[读卡器读取 10 进制原始卡号] + │ + ▼ 例: 3161082660 +[转成标准 16 进制大端串] + │ + ▼ 例: BC 6A 4B 24 +[不足 8 位时高位补零] + │ + ▼ 例: 09 99 ED 24 +[低位在前的字节序逆序转换 (Little-Endian)] + │ + ▼ 例: 24 4B 6A BC (或 24 ED 99 09) +[送入 DMS API 的 cardNumber 参数] +``` + +### 2.1 架构封装纪律 +- **严禁**在 Controller 或前端编写卡号翻转逻辑。 +- 必须在 `com.ruoyi.train.integration.dms.util` 包中独立封装 `CardNumberCodec` 工具类,包含严格的正则表达式校验与单测用例覆盖(大端转小端、奇数位补零、10进制溢出校验)。 + +--- + +## 3. 敏感数据存储与展示安全策略 + +### 3.1 开门临时密码保护规范 +- **密码长度规范与零明文落库**: + - DMS 原生支持 4~8 位数字密码,系统 Pilot 建议生成 6 位限时密码。如果必须在数据库留存备查,必须采用 **AES-256-GCM** 对称加密存储(密钥由配置中心安全注入),严禁裸明文入库。 +- **学员端展示规范**: + - 学员在移动端或大屏查看门禁密码时,默认采用脱敏掩码(如 `****34`)。 + - 支持【点击查看】一次性展示:需经过二次验证,明文仅在内存中暂存并在 30 秒后自动模糊遮挡。 + +### 3.2 日志全局脱敏规则 (`Log Redaction`) +在系统的 Logback、HttpTrace 日志切面及审计日志中,必须对以下字段强制执行脱敏: +- `password` / `fuzzyPassword`:统一脱敏为 `******`。 +- `appsecret` / `token`:绝对禁止打印,统一替换为 `[REDACTED_SECRET]`。 +- `phoneNumber`:国内手机号统一脱敏为 `189****0952`。 +- `cardNumber`:卡号中间 4 位脱敏。 + +--- + +## 4. 远程开锁高风险主动控制规约 (`Remote Unlock Risk`) + +`POST /rest/doorlock/remote-unlock` 属于**最高风险的主动物理控制操作**。一旦滥用或被恶意调用,将直接打开学员宿舍房门。 + +### 4.1 控制红线要求 +1. **一期范围判定**: + - **默认不在一期学员与常规宿管界面开放**,标记为 `P2_FUTURE / 受控应急`。 +2. **安全防护机制(若未来业务强制启用)**: + - **专属高危权限标识**:定义为 `train:access:remoteUnlock`。 + - **角色范围限定**:仅限系统超级管理员或经段保卫科书面授权的当值值班长使用。严禁授予任课教师 (`Teacher`) 或学员 (`Student`)。 + - **二次身份确认**:界面弹出高危告警弹窗,强制要求操作员输入当前登录密码或动态验证码。 + - **必填操作事由**:操作前必须下拉选择或录入事由(如“消防巡检”、“医疗急救”、“钥匙失效人工放行”),且字数不得少于 10 字。 + - **全要素审计日志**:自动抓取并记录:操作人账号、操作人真实姓名、终端客户端 IP、User-Agent、目标楼栋房间、门锁硬件 SN、操作原因、DMS 返回的 `operationId` 与执行结果。 + +--- + +## 5. 门禁全生命周期审计轨迹设计 (`Audit Trail`) + +系统必须在 `train_door_access_audit`(或 RuoYi 标准 `sys_oper_log` 基础上扩展)中建立不可篡改的门禁操作流水。 + +### 5.1 审计要素清单 +每条审计记录必须完整包含以下 14 个不可变字段: +1. `audit_id`: 全局唯一审计流水号 +2. `business_type`: 业务类型(`GRANT` 授权下发, `REVOKE` 授权注销, `RETRY` 失败重试, `REMOTE_UNLOCK` 远程开锁) +3. `accommodation_id`: 关联的本地住宿事实 ID +4. `enrollment_id` & `student_name`: 目标学员信息 +5. `dorm_building`, `dorm_floor`, `dorm_room`: 目标宿舍物理位置 +6. `dms_lock_id` & `lock_hardware_id`: DMS 门锁标识 +7. `credential_type`: 凭据类别(`ONLINE_PASSWORD`, `ONLINE_CARD`) +8. `dms_key_id`: DMS 返回的钥匙全局凭据 ID +9. `dms_operation_id`: DMS 异步执行任务流水号 +10. `operation_result`: 最终结果(`SUCCESS`, `FAILED`, `TIMEOUT`) +11. `error_message`: 详细失败信息(若有) +12. `operator_id` & `operator_name`: 操作发起人(系统自动触发时标为 `SYSTEM_AUTO`) +13. `client_ip`: 发起操作的客户端 IP +14. `operate_time`: 操作发生时间戳 + +### 5.2 响应数据持久化策略 (`Response Sanitization Policy`) +- **禁止项**:绝不无脑把外部 DMS 返回的几千字节原始 JSON 直接整体塞入数据库的文本列中。 +- **要求**:仅抽取核心对账字段进行结构化持久化;若出于排障目的保留原始请求与响应报文快照,**必须先经过脱敏过滤器(剔除明文密码与密钥)**后再写入 `sanitized_response_payload`。 diff --git a/docs/development/dms-r3/09_database_mapping_proposal.md b/docs/development/dms-r3/09_database_mapping_proposal.md new file mode 100644 index 000000000..848dc83d7 --- /dev/null +++ b/docs/development/dms-r3/09_database_mapping_proposal.md @@ -0,0 +1,103 @@ +# 09 数据库持久化模型方案比选与设计基线 (Database Mapping Proposal) + +> **设计红线**:**本轮仅做架构比选与实体设计,禁止执行任何 DDL 脚本或修改生产数据库**。 +> **设计原则**:面向未来多锁扩展与多厂商接入,保持本地宿舍台账的纯粹性,业务授权与硬件异步对账分层治理。 + +--- + +## 1. 外部资源映射模型比选 (`RESOURCE_MAPPING_RECOMMENDATION`) + +### 1.1 映射关系现实挑战 +1. **一个房间可能安装多把锁**:如公寓入户门锁与套内自习室/卧室锁,单一房间可能绑定 0、1 或多把智能门锁。 +2. **外部厂商多变性**:DMS 属于第三方 SaaS/IoT 平台,未来房建公寓段若自建门禁或改用国铁集团集采设备,字段名与标识均可能改变。 +3. **资源解绑与重绑需求**:房间门锁损坏更换后,仅涉及设备 SN 变更,不应引起本地宿舍主记录的历史数据污染。 + +### 1.2 两种设计方案对比 + +| 评估维度 | 方案一:在 `train_dormitory_info` 直接加列 | 方案二:独立 `train_external_resource_mapping` 表 (推荐) | +| :--- | :--- | :--- | +| **表结构改动** | 在宿管表直接加 `dms_place_id`, `dms_lock_id`, `dms_lock_sn` 等 | 物理宿舍表保持零污染;新建独立的外部资源映射关联表 | +| **一房多锁支持** | **无法支持**(单行只能记录一把锁的 SN 和 ID) | **天然支持**(一个 `dorm_id` 可对应多行锁具映射记录) | +| **多厂商与扩展性** | 差(换厂商需继续加字段,如 `hik_channel_id`) | **优秀**(通过 `vendor_code` 区分不同厂商,复用通用映射模型) | +| **设备更换生命周期** | 弱(直接覆写破坏审计历史) | **强**(可标记旧锁为 `DEPRECATED`,新增新锁映射) | +| **迁移与实施成本** | 极低 | 低(新增一张轻量级关联表) | + +### 1.3 外部资源映射推荐方案结论 +**采纳方案二**:建立独立的外部资源映射模型。 +- **概念模型结构设计**: + ``` + train_external_resource_mapping + ├── mapping_id BIGINT (主键) + ├── local_resource_type VARCHAR(32) ('DORMITORY_ROOM', 'DORMITORY_LOCK') + ├── local_resource_id VARCHAR(32) (关联本地 dorm_id) + ├── vendor_code VARCHAR(32) ('DMS_IOT') + ├── external_project_id BIGINT (DMS 租户 projectId) + ├── external_parent_id BIGINT (DMS 楼栋/楼层 ID) + ├── external_place_id BIGINT (DMS 房间 placeId) + ├── external_device_id BIGINT (DMS 门锁 lockId) + ├── hardware_sn VARCHAR(64) (门锁硬件 SN / hardwareId) + ├── mapping_status SMALLINT (1: 已绑定有效, 0: 待核对/失效) + ├── last_sync_time TIMESTAMP (最近一次从 DMS 同步元数据的时间) + └── [标准审计列] + ``` + +--- + +## 2. 门禁授权与操作模型三大方案深度比选 + +### 2.1 方案架构与评估矩阵 + +| 评估维度 | 方案 A: 改造并继续扩展 `train_dorm_auth` | 方案 B: `train_dorm_auth` 收敛为业务授权 + 独立 `train_external_operation` (推荐) | 方案 C: 新建 `train_door_authorization` + `train_external_operation` | +| :--- | :--- | :--- | :--- | +| **模型设计说明** | 在现有的 `train_dorm_auth` 表中强行塞入 `lock_id`, `dms_key_id`, `operation_id`, `password` 等列 | 保留并对齐数据库 V2 的 `train_dorm_auth` 作为门禁授权主表;新建 `train_external_operation` 承载所有异步轮询与重试流水 | 彻底废除 `train_dorm_auth`,重新设计全套中立命名的门禁授权与外部操作表 | +| **一期开发复杂度** | 低(改单表) | **适中**(改主表结构 + 建流水表) | 略高(需处理原有表废弃与实体完全重构) | +| **语义与职责清晰度** | **严重混乱**(业务授权与设备单次请求纠缠,无法表达异步过程) | **良好**(业务授权状态与设备异步任务彻底解耦) | **极优**(概念完全脱离人脸/宿舍局限) | +| **一房多锁支持能力** | **无法支持**(`accommodation_id` 唯一约束导致无法为同一个住宿下发多把锁) | **支持**(主表唯一约束调整为 `(accommodation_id, device_mapping_id)`) | **支持**(原生复合设计) | +| **异步状态与重试审计** | **丢失历史**(每次重试都会覆写 `last_error` 和 `operation_id`) | **完整保留**(流水表每次调用追加一行,多次轮询与超时全量追踪) | **完整保留** | +| **历史兼容与代码改动** | 兼容性最高,但长期技术负债巨大 | **平衡性最佳**(沿用 V2 数据库迁移成果,仅修正 Java 实体脱节) | 需调整较多已有引用类名 | + +--- + +## 3. 推荐采纳模型设计基线 (`DOOR_AUTH_MODEL_RECOMMENDATION`) + +综合考虑沈阳房建公寓段学员管理系统目前所处的正式 Release 阶段与 V2 数据库已经上线的实际情况,**推荐采纳【方案 B】(业务授权主表收敛 + 异步操作流水表解耦)**。 + +### 3.1 实体一:门禁业务授权主模型 (`TrainDormAuth` / 业务授权主表) +承担职责:表达某个学员住宿事实在某把门锁上的**长期通行权限状态**。 +- `auth_id`: BIGINT, PK +- `accommodation_id`: BIGINT, 外键关联 `train_accommodation` (业务事实真源) +- `mapping_id`: BIGINT, 关联 `train_external_resource_mapping` (目标门锁) +- `credential_type`: VARCHAR(20) ('ONLINE_PASSWORD', 'ONLINE_CARD') +- `credential_mask`: VARCHAR(50) (脱敏凭据摘要,如 '****34' 或 '244B****') +- `external_key_id`: BIGINT (DMS 返回的钥匙实体 ID,用于注销撤权) +- `auth_start`: TIMESTAMP (权限生效起始时间) +- `auth_end`: TIMESTAMP (权限失效截止时间) +- `auth_status`: SMALLINT (0:待下发, 1:下发中, 2:已生效, 3:下发失败, 4:撤销中, 5:已注销, 6:撤销失败) +- `grant_version`: INT (授权版本号,换房或重新下发时自增,保障本地幂等) +- `[标准审计列]` (`create_by`, `create_time`, `update_by`, `update_time`) + +### 3.2 实体二:外部异步操作流水模型 (`TrainExternalOperation` / 异步操作流水表) +承担职责:跟踪向 DMS 派发的**每一次具体 HTTP 异步任务的生命周期与轮询轨迹**。 +- `operation_local_id`: BIGINT, PK +- `auth_id`: BIGINT, 外键关联授权主表 +- `business_action`: VARCHAR(32) ('GRANT_PASSWORD', 'REVOKE_PASSWORD', 'GRANT_CARD', 'REVOKE_CARD') +- `vendor_code`: VARCHAR(32) ('DMS_IOT') +- `dms_operation_id`: VARCHAR(64) (DMS 返回的 32 位 UUID 操作流水号) +- `dms_request_type`: INT (DMS 数字请求类型,如 1=下发密码, 3=删除密码) +- `hardware_id`: VARCHAR(64) (门锁硬件 SN) +- `operation_status`: VARCHAR(20) ('ACCEPTED', 'UNDONE', 'SUCCESS', 'FAILED', 'TIMEOUT') +- `poll_count`: INT (已轮询次数,初始为 0,上限通常为 3) +- `next_poll_time`: TIMESTAMP (调度器下次拉取状态的时间,初始为 `request_time + 10s`) +- `request_time`: TIMESTAMP (向 DMS 发起请求的时间) +- `complete_time`: TIMESTAMP (DMS 返回终态的时间) +- `error_code` & `error_message`: VARCHAR (错误摘要) +- `sanitized_response`: VARCHAR(1000) (脱敏后的响应快照) +- `[标准审计列]` + +--- + +## 4. 实施阶段演进建议 +在后续 R.3B 实施阶段,数据访问层的演进步骤建议为: +1. **第一步**:重构修复 Java 实体类 `TrainDormAuth.java`,彻底移除已弃用的 `student_id`, `plan_id`, `dorm_id` 字段,使其与已存在的 Kingbase 物理表字段完全对齐。 +2. **第二步**:创建 `train_external_resource_mapping` 与 `train_external_operation` 迁移脚本(遵循系统标准 Liquibase/Flyway 规范)。 +3. **第三步**:通过 Spring Data / MyBatis-Plus 完成三表关联映射。 diff --git a/docs/development/dms-r3/10_api_and_ui_proposal.md b/docs/development/dms-r3/10_api_and_ui_proposal.md new file mode 100644 index 000000000..294ddbacf --- /dev/null +++ b/docs/development/dms-r3/10_api_and_ui_proposal.md @@ -0,0 +1,117 @@ +# 10 门禁集成内部 API 与 UI 交互设计方案 (API and UI Proposal) + +> **设计声明**:本设计中所列出的 API 接口、UI 原型与权限标识均为 **R.3A 设计提案**。 +> **纪律铁律**:**当前系统中上述 API 与前端页面均未实现,严禁声称接口已存在,严禁在本轮修改前端 Vue 代码或向数据库插入 `sys_menu` 记录**。 + +--- + +## 1. RailwaySystem 内部 RESTful API 设计提案 + +后续在 `TrainDoorAccessController` 中统一发布的中立门禁管理接口: + +### 1.1 授权管理接口 +| 方法 | 路径 | 权限标识提案 | 描述与行为 | +| :--- | :--- | :--- | :--- | +| `GET` | `/train/access/status/{accommodationId}` | `train:access:query` | 查询指定住宿安排的门锁授权状态与钥匙摘要(脱敏) | +| `POST` | `/train/access/grant` | `train:access:grant` | 手动/自动为住宿安排触发门锁授权下发(支持指定密码或卡号) | +| `POST` | `/train/access/revoke/{authId}` | `train:access:revoke` | 主动撤销指定门锁的开门权限(退宿、换房、退训) | +| `POST` | `/train/access/retry/{authId}` | `train:access:retry` | 对状态处于 `ISSUE_FAILED` 或 `TIMEOUT` 的授权记录发起重新下发 | +| `GET` | `/train/access/operations/{authId}` | `train:access:operation` | 查看指定授权记录关联的全部异步操作流水与 DMS 轮询结果 | +| `POST` | `/train/access/remote-unlock` | `train:access:remoteUnlock` | **高危应急**:远程紧急开锁(必填操作原因、两票审批号) | + +### 1.2 资源映射管理接口 +| 方法 | 路径 | 权限标识提案 | 描述与行为 | +| :--- | :--- | :--- | :--- | +| `GET` | `/train/access/mapping/list` | `train:access:mapping` | 查询本地宿舍与 DMS 房间/门锁的绑定映射列表 | +| `POST` | `/train/access/mapping/sync-dms-rooms`| `train:access:mapping` | 从 DMS 平台拉取楼栋/房间列表,辅助管理员快速配对 | +| `POST` | `/train/access/mapping/bind` | `train:access:mapping` | 将本地 `dorm_id` 显式绑定至 DMS `placeId` 及 `lockId` | + +--- + +## 2. 权限体系提案 (`Permission Proposal`) + +> **并行开发纪律**:当前 R.2 团队正在分支 `feature/rbac-r2` 上进行 RBAC 体系重构,本轮**禁止修改任何 R.2 代码,严禁操作 `sys_menu`**。以下权限标识仅作为待合并提案: + +### 2.1 角色权限分配矩阵 +| 权限标识 (Proposal) | 超级管理员 (Admin) | 公寓宿管员 (DormManager) | 任课教师 (Teacher) | 参训学员 (Student) | +| :--- | :---: | :---: | :---: | :---: | +| `train:access:query` (状态查看) | ✅ | ✅ | ❌ | ✅ (仅本人数据) | +| `train:access:grant` (下发钥匙) | ✅ | ✅ | ❌ | ❌ | +| `train:access:revoke` (撤销钥匙) | ✅ | ✅ | ❌ | ❌ | +| `train:access:retry` (失败重试) | ✅ | ✅ | ❌ | ❌ | +| `train:access:operation` (查看流水) | ✅ | ✅ | ❌ | ❌ | +| `train:access:mapping` (房源映射) | ✅ | ❌ | ❌ | ❌ | +| `train:access:remoteUnlock` (应急开锁) | ✅ (受控两票制) | ❌ (默认禁止) | ❌ | ❌ | + +--- + +## 3. 管理端 UI 界面原型与交互设计 (`Admin UI Proposal`) + +### 3.1 住宿安排与门禁状态整合视图 (`Accommodation View`) +在后期的学员住宿管理页面,住宿列表表格中增加【门禁授权状态】一列,以彩色徽标直观展现: + +``` ++---------------------------------------------------------------------------------------------------------+ +| 学员姓名 | 班级计划 | 分配宿舍 | 床位 | 住宿状态 | 门锁权限状态 | 门锁凭据摘要 | 操作列 | ++---------------------------------------------------------------------------------------------------------+ +| 张三 | 铁道信号第03期 | 1号楼-201 | 1号床| 已入住 | [绿色] 已生效 | 密码: ****34 | [查看流水] [撤销权限] | +| 李四 | 铁道信号第03期 | 1号楼-201 | 2号床| 预分配 | [黄色] 下发中 | 密码: ****** | [刷新状态] | +| 王五 | 铁道车辆第01期 | 2号楼-102 | 1号床| 已入住 | [红色] 下发失败 | 密码: ****** | [重新下发] [修改排宿] | +| 赵六 | 供电检修第05期 | 2号楼-105 | 2号床| 已退宿 | [灰色] 已注销 | (已清除) | [查看历史] | ++---------------------------------------------------------------------------------------------------------+ +``` + +### 3.2 异步状态前端轮询交互规则 +1. 当操作员点击【下发权限】后,按钮进入 Loading 态,并立即将徽标置为【黄色 下发中】。 +2. 页面前端通过 `setInterval` 每 3 秒拉取一次本地 `/train/access/status/{accommodationId}` 接口。 +3. 后台轮询器完成 10 秒对账并写入数据库终态后,前端获取到最新状态自动变为【绿色 已生效】或【红色 下发失败】,并弹出 Toast 提示:“张三门锁授权已生效!”。 + +### 3.3 远程开锁高危交互弹窗 (`Remote Unlock Modal`) +- 若管理员点击受控的【远程应急开锁】按钮: + - 弹出全屏遮罩红色警告框:`【高危操作警告】您正在发起沈阳房建公寓段 1号楼-201 智能门锁远程开锁!` + - 必填项: + 1. 操作事由(下拉选择:消防逃生 / 突发医疗救治 / 机械钥匙遗失应急放行 / 现场工程检修)。 + 2. 详细说明(必须录入,不少于 10 个字)。 + 3. 当前登录管理员密码核验(输入正确方可点击确认)。 + - 点击提交后,后端记录完整的全要素审计日志。 + +--- + +## 4. 学员端个人中心视图设计 (`Student View Proposal`) + +### 4.1 学员移动端/个人中心展示边界 +- **展示要素**: + - 本次开班信息(培训班名称、起止日期)。 + - 分配宿舍(如:沈阳房建公寓段 1号楼 2层 201室 1号床)。 + - 门锁状态(如:“门禁权限已就绪”、“有效期至 2026-09-20 18:00”)。 +- **密码明文展示安全策略**: + - **原则上不长期明文暴露完整密码**。 + - 默认展示为掩码状态:`开门临时密码:****34`。 + - 提供【显示完整密码】眼睛图标按钮: + - 点击时弹出轻量级身份验证(如输入本人身份证后4位或手机验证码)。 + - 验证通过后展示完整密码明文(如 Pilot 采用的 6 位密码),并启动 30 秒倒计时,倒计时归零后自动恢复掩码状态。 + +--- + +## 5. DMS 资源映射与同步管理界面设计 (`Resource Sync UI`) + +### 5.1 房源对齐工作流 +``` +[管理员进入"门锁资源映射"界面] + │ + ▼ +[点击"从DMS拉取房源目录"] + │ + ▼ 适配器调用 POST /rest/place/room/list +[系统解析出 DMS 的 Building, Floor, Room, placeId, 挂载的 lockId, 硬件SN] + │ + ▼ +[系统依据"楼栋-楼层-房间"名称执行规则预匹配] + │ + ▼ +[管理员在界面核对预配对结果, 确认手动微调后点击"确认持久化绑定"] + │ + ▼ +[写入 train_external_resource_mapping, 完成两套系统物理基线的稳定锁定] +``` +- **核心保障**:同步操作仅建立与更新映射表,**绝不自动删除或篡改本地 `train_dormitory_info` 的基础房源台账**。本地房源依然是学员管理系统的独立主数据。 diff --git a/docs/development/dms-r3/11_testing_and_mock_plan.md b/docs/development/dms-r3/11_testing_and_mock_plan.md new file mode 100644 index 000000000..6228ca9f4 --- /dev/null +++ b/docs/development/dms-r3/11_testing_and_mock_plan.md @@ -0,0 +1,117 @@ +# 11 模拟测试体系、并发验证与测试沙箱要求 (Testing and Mock Plan) + +> **测试最高铁律**:**严禁在持续集成 (CI/CD) 或单元测试中使用真实 DMS 生产环境与真实门锁设备**! +> 门锁下发密码将直接修改物理设备存储并消耗锁端电量与无线频段。 +> 必须基于 **本地 Mock HTTP Server (WireMock / MockWebServer)** 搭建完备的多层级可复现测试套件。 + +--- + +## 1. 分层测试策略体系 (Testing Strategy) + +``` ++--------------------------------------------------------------------------------+ +| L4. 现场沙箱验收测试 (Live Sandbox E2E - 独立测试账号 + 实验台独立挂载测试锁) | ++--------------------------------------------------------------------------------+ +| L3. 契约与故障注入测试 (WireMock Failure Injection - 401/超时/宕机/乱序状态) | ++--------------------------------------------------------------------------------+ +| L2. 并发与状态机集成测试 (Concurrency & State Machine Test - 20并发Token竞争/幂等) | ++--------------------------------------------------------------------------------+ +| L1. 纯领域单元测试 (Unit Test - CardNumberCodec 编码 / 状态机转换 / DTO 转换) | ++--------------------------------------------------------------------------------+ +``` + +--- + +## 2. Mock DMS 仿真覆盖矩阵 (22 项核心场景) + +在后续 R.3B 实现阶段,集成测试套件必须基于 Mock 服务器完整覆盖以下 22 项场景: + +| # | 测试场景用例 | Mock 桩行为定义 | 预期系统断言与表现 | +| :-: | :--- | :--- | :--- | +| 1 | **Login Success** | 返回 200,包含新 Token 与 `expiresIn: 7200` | `DmsTokenManager` 缓存 Token,设置正确到期时间戳 | +| 2 | **Token Cache Reuse** | 连续 10 次请求同一接口 | 仅在第 1 次触发 Mock 登录,后续 9 次请求 Header 均复用相同 Token | +| 3 | **Token Expiry** | 模拟系统时钟快进至失效时间前 5 分钟 | 自动触发平滑预防性刷新,获取新 Token | +| 4 | **Concurrent Token Refresh** | **20 个并发业务线程**同时探测到 Token 缺失/过期 | **单飞排他锁生效**:Mock 登录接口**被且仅被调用 1 次**,20 个线程全部安全复用该 Token,无踩踏覆盖 | +| 5 | **401 Auto Relogin** | 业务请求收到 Mock 返回 `401 Unauthorized` | 自动使缓存失效,触发单飞重登录,并**自动安全重试原请求 1 次且成功** | +| 6 | **Room Lookup Normal** | 精确匹配返回单个房间 `placeId: 1320` | 正常持久化映射记录 | +| 7 | **Room Lookup Not Found** | 返回 `{"success": false, "message": "Room not found!"}` | 抛出 `RoomNotFoundException`,不阻塞其他业务 | +| 8 | **Room Multiple Results** | 模糊查询返回同名房间(多楼层同房号) | 触发精准过滤,若仍有多条抛出歧义异常提醒管理员人工介入 | +| 9 | **Lock Lookup Normal** | 返回房间挂载的 1 把锁 | 绑定该锁并派发授权 | +| 10 | **Lock Lookup Multiple** | 返回房间挂载的 2 把锁(外门+内门) | **一房多锁验证**:为该住宿安排分别生成 2 条授权记录并同时下发 | +| 11 | **Lock Lookup None** | 返回空数组(该房间未装门锁) | 抛出 `LockNotConfiguredException`,状态标记为无锁,不影响入住 | +| 12 | **Password Issue Accepted** | 调用 create 返回 200 + `operationId` | 本地授权状态置为 `ISSUING`,操作记录置为 `ACCEPTED`,**不得直接置为 ACTIVE** | +| 13 | **Password Operation Success** | 10 秒后轮询返回 `operationStatus: "success"` | 授权状态正式推进为 `ACTIVE`,操作记录置为 `SUCCESS` | +| 14 | **Password Operation Undone** | 首次轮询返回 `operationStatus: "undone"` | 保持 `ISSUING` 状态,触发第 2 次退避轮询 | +| 15 | **Password Operation Failed** | 轮询返回 `operationStatus: "failed"` | 授权状态置为 `ISSUE_FAILED`,记录详细错误原因 | +| 16 | **Password Operation Timeout** | 轮询返回 `operationStatus: "timeout"` | 轮询上限后停止,状态置为 `ISSUE_FAILED (TIMEOUT)`,页面显示重试按钮 | +| 17 | **Card Issue & Codec** | 下发 10 进制卡号 `3161082660` | 验证 Mock 实际收到的 Body 中 `cardNumber` 为小端序 `244B6ABC` | +| 18 | **Revoke Flow** | 调房或退宿时调用 delete 接口 | 正确携带 `external_key_id`,轮询成功后状态置为 `REVOKED` | +| 19 | **DMS Unavailable** | Mock 服务断开或返回 HTTP 500/502 | **住宿业务事务正常提交完成**,门禁状态置为 `ISSUE_FAILED`,记录故障日志 | +| 20 | **Unknown Result Reconcile** | 发送 create 时遭遇网络读取超时 | 触发未决对账流程,查询当前房间钥匙列表,避免重复发码 | +| 21 | **Idempotent Duplicate Grant** | 连续快速点击 5 次“下发权限” | 本地幂等生效,仅派发 1 次 DMS 请求,无重复钥匙生成 | +| 22 | **Accommodation Transfer** | 学员换房(旧房 A -> 新房 B) | 验证“先发新房 B,确认成功后再撤销旧房 A”的时序与失败补偿 | + +--- + +## 3. 关键专项验证测试设计细节 + +### 3.1 Token 并发竞争专项测试设计 (`Token Concurrency Test`) +```java +// 测试伪代码设计示意 (后续 R.3B 落地至 src/test/java) +@Test +public void testConcurrentLoginDeduplication() throws Exception { + int threadCount = 20; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + // 清空当前 Token 缓存 + dmsTokenManager.invalidate(); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + startLatch.await(); // 确保 20 个线程在瞬间同时并发冲击 + String token = dmsTokenManager.getToken(); + assertThat(token).isNotBlank(); + } finally { + doneLatch.countDown(); + } + }); + } + + startLatch.countDown(); // 发令枪响 + doneLatch.await(10, TimeUnit.SECONDS); + + // 严苛断言:Mock 服务端的登录接口在整个并发过程中被调用的次数必须精确为 1! + wireMockServer.verify(1, postRequestedFor(urlEqualTo("/public/rest/third-party-app/token/get"))); +} +``` + +### 3.2 异步状态生命周期防抢跑断言 (`Async Anti-Premature Test`) +- **断言目标**:杜绝将 HTTP 200 直接视为下发成功。 +- **验证步骤**: + 1. 调用 `doorAccessGateway.grantAccess(...)`。 + 2. 立即检查数据库中的 `train_dorm_auth` 记录: + - 断言 `auth_status == ISSUING`(下发中)。 + - 断言绝不能等于 `ACTIVE`。 + 3. 执行一次调度轮询(此时 Mock 返回 `operationStatus = undone`): + - 断言 `auth_status` 依然维持在 `ISSUING`。 + 4. 执行第二次调度轮询(此时 Mock 返回 `operationStatus = success`): + - 断言 `auth_status` 顺利跃迁为 `ACTIVE`。 + +--- + +## 4. 生产环境接入前联调沙箱要求 (`DMS_SANDBOX_REQUIREMENTS`) + +在未来正式将 RailwaySystem 部署到沈阳房建公寓段并对接真实门锁前,**必须向 DMS 供应商及公寓设备管理方书面申请独立测试沙箱环境**。 + +### 4.1 核心沙箱准入条件 +1. **独立测试租户**:必须提供专属的测试用 `appid`、`appsecret` 及 `projectId`,严禁与正在运行的既有公寓楼共用租户。 +2. **物理实验台测试锁具**: + - 必须在实验台/机房单独架设 **至少 1 台测试网关 + 2 把独立挂载的测试智能门锁**。 + - 严禁直接使用学员已经实际入住的宿舍门锁进行研发调试! +3. **测试数据隔离**: + - 在 DMS 中建立虚拟楼栋(如“联调测试楼”),配置测试房间(如“9901室”、“9902室”)。 +4. **技术支撑配合**: + - 供应商提供联调期间的技术接口人,并确认正式环境网络是否具备固定公网 IP 白名单或专网通道。 diff --git a/docs/development/dms-r3/12_implementation_task_breakdown.md b/docs/development/dms-r3/12_implementation_task_breakdown.md new file mode 100644 index 000000000..b964afe23 --- /dev/null +++ b/docs/development/dms-r3/12_implementation_task_breakdown.md @@ -0,0 +1,141 @@ +# 12 R.3B 实施阶段 WBS 任务拆分与四阶段推进路线图 (Implementation Task Breakdown) + +> **实施基准**:严格遵循本 R.3A 审计与设计冻结规范。 +> **核心演进策略**:**严禁在模型漂移之上直接开发适配器**。 +> 实施路线严格划分为 **四个推进阶段 (Wave 0 ~ Wave 3)**,确保本地领域事实先就绪,再推进外部协议适配与端到端编排。 + +--- + +## 1. 四阶段演进路线图 (Four-Wave Roadmap) + +```mermaid +flowchart TD + subgraph Wave0["Wave 0: 本地领域模型对齐 (Local Domain Alignment)"] + W0_1["TrainAccommodation Java 实体/DTO/VO 构建"] + W0_2["TrainAccommodationMapper & 业务 Service 实现"] + W0_3["TrainDormAuth Java 实体与 Mapper V2 升级清洗"] + W0_4["确立 Accommodation 与 DoorAuthorization 明确职责边界"] + end + + subgraph Wave1["Wave 1: 外部映射与操作持久化 (External Mapping & Operation Persistence)"] + W1_1["train_external_resource_mapping 物理表与领域映射"] + W1_2["train_external_operation 异步流水持久化表构建"] + W1_3["Liquibase / Flyway 增量数据库 Migration 脚本"] + W1_4["门禁与操作双层状态机模型持久化枚举与转换器"] + end + + subgraph Wave2["Wave 2: DMS 专用适配器与仿真 (DMS Adapter & Mock Test)"] + W2_1["DmsProperties 安全配置契约与受控环境变量集成"] + W2_2["DmsTokenManager 进程级单飞排他锁与 401 自愈"] + W2_3["细分 DmsClient (Place, DoorLock, Key, Operation)"] + W2_4["在线密码 / 在线卡片下发与撤销协议适配 (含 CardNumberCodec)"] + W2_5["DmsOperationPoller 10 秒延迟对账调度器"] + W2_6["WireMock 仿真服务与 22 项单测套件 (并发/异常注入)"] + end + + subgraph Wave3["Wave 3: 业务编排、交互与现场验收 (Business Orchestration & UI & Sandbox)"] + W3_1["门禁授权触发编排 (报到核验联动 / 提早进房时间 Buffer)"] + W3_2["退宿、退训强制撤权与计划结训批量注销"] + W3_3["换房补偿编排服务 (先发新房后撤旧房,异常熔断保留原房)"] + W3_4["管理端 UI: 住宿列表门禁彩色徽标、操作历史抽屉、手动重试"] + W3_5["学员端 UI: 个人中心门锁状态、有效期展示、30秒倒计时密码受控查看"] + W3_6["现场实验台测试沙箱 E2E 闭环验收与真实门锁调测"] + end + + Wave0 --> Wave1 + Wave1 --> Wave2 + Wave2 --> Wave3 +``` + +--- + +## 2. 各 Wave 任务详细定义与交付清单 + +### Wave 0: 本地领域模型对齐 (`Local Domain Alignment`) +> **前置条件**:基线处于 `adcf930e`;`LOCAL_DOMAIN_ALIGNMENT_REQUIRED = YES`。 +> **核心目标**:消除本地 Java 实体与 Kingbase 物理表 V2 的历史漂移,补齐缺失的 Accommodation 业务代码。 + +1. **TrainAccommodation Java Domain 构建**: + - 创建 `TrainAccommodation.java` 实体,映射物理表 `train_accommodation` 的全部 11 个列(`accommodation_id`, `enrollment_id`, `dorm_id`, `bed_no`, `assigned_time`, `accommodation_status` 等)。 + - 创建 `AccommodationCreateDTO`, `AccommodationVO` 等。 +2. **Mapper & Service 实现**: + - 编写 `TrainAccommodationMapper.java` 与 XML,实现排宿、入住、退宿状态流转。 + - 编写 `ITrainAccommodationService` 及其实现类,承接业务事实变更。 +3. **TrainDormAuth V2 结构升级与清洗**: + - 彻底清洗 Java 类 `TrainDormAuth.java`,移除已废弃的 V1 字段(`studentId`, `planId`, `dormId`, `bedNo`)。 + - 使其完全对齐 V2 数据库物理表(`accommodation_id`, `retry_count`, `last_error`, `external_auth_id`, `last_sync_time`)。 + - 同步修正 `TrainDormAuthMapper.xml` 中的 ResultMap 与 SQL 映射。 +4. **业务边界明确与解耦测试**: + - 编写本地单测,验证:`Dormitory Resource` (物理房源) ↔ `Accommodation` (住宿事实) ↔ `TrainDormAuth` (门禁事实) 三者职责分离。 + +--- + +### Wave 1: 外部映射与操作持久化 (`External Mapping & Operation Persistence`) +> **前置条件**:Wave 0 完成。 +> **核心目标**:建立房源/设备映射体系与异步操作流水持久化模型,为外部调用提供存储支撑。 + +1. **外部资源映射模型 (`train_external_resource_mapping`)**: + - 编写 DDL Migration 脚本,创建资源映射表,支持一房多锁及未来多厂商拓展。 + - 编写 `TrainExternalResourceMapping` 实体与 Mapper,支持按 `dorm_id` 查询关联的所有锁具。 +2. **外部异步操作流水模型 (`train_external_operation`)**: + - 创建操作流水表,记录每一次向外部设备派发的具体指令(`operation_local_id`, `dms_operation_id`, `dms_request_type`, `operation_status`, `poll_count`, `next_poll_time`)。 +3. **状态机枚举与状态映射规范**: + - 在系统内部定义门禁业务状态枚举 `DoorAuthorizationStatus` 与硬件操作状态枚举 `ExternalOperationStatus`。 + +--- + +### Wave 2: DMS 专用适配器与仿真验证 (`DMS Adapter & Mock Tests`) +> **前置条件**:Wave 1 完成。 +> **核心目标**:完成对 DMS REST API 的全套适配,并通过 WireMock 离线仿真实现 100% 自动化测试。 + +1. **安全配置与基础设施**: + - 建立 `DmsProperties`,绑定环境变量(`DMS_BASE_URL`, `DMS_APP_ID`, `DMS_APP_SECRET`, `DMS_PROJECT_ID`)。 + - 配置 HTTP 日志脱敏拦截器(掩码 Token、Secret 与 Password)。 +2. **Token 管理器与并发防击穿 (`DmsTokenManager`)**: + - 实现进程级内存 Token 缓存,实现单飞排他锁(Single-Flight),拦截 20 线程并发竞争,确保远程登录只调用一次;实现 401 自动失效重登录与最大 1 次重试。 +3. **细分领域 HTTP 客户端**: + - 封装 `DmsPlaceClient`, `DmsDoorLockClient`, `DmsKeyClient`, `DmsOperationClient`。 +4. **门锁授权网关适配器实现 (`DmsDoorAccessGateway`)**: + - 实现中立接口 `DoorAccessGateway`; + - 适配在线限时密码下发 (`/online-password-key/create`,系统支持4~8位,Pilot建议生成6位限时密码) 与注销 (`/online-password/delete`); + - 实现 `CardNumberCodec` 小端序十六进制转换; +5. **异步对账轮询调度器 (`DmsOperationPoller`)**: + - 基于 Spring Scheduled 专用线程池,实现门锁操作 10 秒后对账拉取与超时判定。 +6. **WireMock 仿真测试套件**: + - 编写包含 22 个场景的自动化集成测试用例,覆盖正常流、超时、401、并发击穿、网络读取异常对账等。 + +--- + +### Wave 3: 业务编排、交互与现场沙箱验收 (`Business Orchestration & UI & Sandbox`) +> **前置条件**:Wave 2 完成。 +> **核心目标**:实现上层业务联动、管理端与学员端界面交互,并在现场测试沙箱完成闭环验收。 + +1. **门禁授权触发时机与编排 (`DoorAccessOrchestrationService`)**: + - 联动现场《报到核验》事件:学员报到核验成功后异步触发门禁下发; + - 配置授权时间窗口 Buffer(提前进房、延后离校)。 +2. **注销与补偿服务**: + - 换房补偿:“先发新房权限、确认生效后再撤销旧房权限”,新房下发失败时保留旧房并报警; + - 学员退训强制立即撤销权限; + - 培训结训主动批量撤销钥匙,辅以 `validUntil` 有效期边界兜底。 +3. **管理端 UI 交互实现**: + - 住宿安排列表集成门禁彩色徽标(下发中/已生效/失败/已注销); + - 提供操作流水抽屉弹窗、手动重试按钮与设备电量监控视图。 +4. **学员端个人中心视图**: + - 移动端个人中心展示宿舍分配、门锁就绪状态与有效期; + - 支持经二次身份验证后查看 30 秒倒计时完整开门密码。 +5. **现场测试沙箱实物闭环验收**: + - 在甲方提供的专属测试账号与实验台测试门锁上进行全流程实物验证; + - 确认门锁离线时的 `validUntil` 本地过期与 RTC 时钟行为; + - 输出正式现场实物验收报告。 + +--- + +## 3. 工期与人力资源估算 + +| 阶段 | 核心交付成果 | 涉及模块 | 预估工时 | +| :--- | :--- | :--- | :---: | +| **Wave 0: Local Domain Alignment** | 消除本地代码与 DB 漂移,补齐 Accommodation 业务层 | 后端领域层 | 3 人天 | +| **Wave 1: External Mapping & Persistence** | 资源映射表、操作流水表及数据库 Migration | 数据库/持久层 | 2 人天 | +| **Wave 2: DMS Adapter & Mock Tests** | 协议适配器、TokenManager、轮询调度器、22项单测 | 外部集成层 | 5 人天 | +| **Wave 3: Orchestration, UI & Sandbox** | 业务编排服务、前端界面、现场沙箱实物 E2E 联调 | 业务层/前端/现场 | 5~6 人天 | +| **总计** | **R.3 门禁集成端到端正式交付上线** | **全栈闭环** | **约 15~16 人天** | diff --git a/docs/development/dms-r3/13_open_questions.md b/docs/development/dms-r3/13_open_questions.md new file mode 100644 index 000000000..998b97d08 --- /dev/null +++ b/docs/development/dms-r3/13_open_questions.md @@ -0,0 +1,48 @@ +# 13 供应商技术歧义、业务决策与阻塞性问题清单 (Open Questions) + +> **审计说明**:本清单系统性整理了在 DMS 官方文档审计、网络架构评估及业务流程梳理中发现的**技术不一致项(歧义)**与**业务决策项**。 +> **状态分类**:明确标明 `BLOCKING`(必须在编码前达成一致,否则直接阻塞实施)与 `NON_BLOCKING`(可在一期实施过程中平滑协商)。 + +--- + +## 1. DMS 官方文档自身不一致性审计 (`DMS_DOC_AMBIGUITIES`) + +审计发现了 **7 项严重的文档内部矛盾与笔误**,严禁在代码中私自掩盖,必须建立兼容防御: + +| # | 歧义分类 | 文档表面陈述 | 文档内部矛盾 / 真实示例数据 | 潜在隐患与对策 | 置信度状态 | +| :-: | :--- | :--- | :--- | :--- | :--- | +| 1 | **端点前缀版本** | 页面大标题与文档路径标注为 `/V1/doc/restapi.html` | 文档内所有接口地址实际路径均为 `https://dms.dmsiot.com/rest/...` 或 `/public/rest/...`,**完全没有 `/V1/` 前缀**! | 调用若加 `/V1` 可能会报 404。需向供应商核实实际服务根路径。 | `NEEDS_VENDOR_CONFIRMATION` | +| 2 | **协议混用 (HTTP vs HTTPS)** | 绝大多数端点为 `https://...` | 楼层列表接口明确写成了 `http://dms.dmsiot.com/rest/place/floor/list` | 生产环境若强制启用 HSTS 或反向代理重定向,HTTP 可能被拒绝或发生 301 重定向。统一配置 HTTPS。 | `DOCUMENTED (Defect)` | +| 3 | **删除密码 requestType 错乱** | 文字说明定义:`3 = 删除在线密码` | 在《删除在线密码结果》的返回示例中,`data.requestType` 赫然给出了数字 `1`(1实为下发密码)! | 若客户端强行校验返回的 `requestType == 3` 会导致对账失败。状态机只以 `operationId` 为主键比对。 | `DOCUMENTED (Defect)` | +| 4 | **二维码钥匙返回数据污染** | 标题为《生成二维码/手机钥匙》,端点存在于 `/rest/doorlock/qrcode/create` | 返回成功示例中,`typeStr` 却显示为 `"Online Card"`,且响应体**没有包含任何二维码图片Base64、URL链接或开门Token**! | 接口存在 (`DOCUMENTED`) 但载荷残缺污染,无法直接用于移动端扫码开门。适用性归为 `NEEDS_BUSINESS_CONFIRMATION`。 | `DOCUMENTED (Defect)` | +| 5 | **注销二维码路径不一致** | 其他门锁端点均位于 `/rest/doorlock/...` 下 | 注销二维码端点地址为 `https://dms.dmsiot.com/rest/qrcode/delete`(缺少 `doorlock` 层级) | URL 路由不统一。客户端需针对该端点单独定义 Path。 | `DOCUMENTED (Defect)` | +| 6 | **开门记录路径不一致** | 其他门锁端点均位于 `/rest/doorlock/...` 下 | 开门记录端点地址为 `https://dms.dmsiot.com/rest/lock/unlock-record/list`(使用的是 `/lock/` 而非 `/doorlock/`) | URL 路由不统一。 | `DOCUMENTED (Defect)` | +| 7 | **未实现死字段 (Dead Fields)** | 网关列表返回 `lastOnlineTime: null` 与 `onlineStatus: "Offline"` | 文档直接在注释中注明:`// 在线状态,暂未实现` | 绝对不能依赖静态网关列表中的 `onlineStatus` 判定设备是否在线。 | `DOCUMENTED` | + +--- + +## 2. 核心阻塞性问题清单 (`BLOCKING QUESTIONS - 共 6 项`) + +以下 6 个关键决策直接决定数据库设计、集成网络拓扑与业务流程闭环,**必须在编码前正式定稿**: + +| # | 决策核心内容 | 影响范围与备选方案 | 推荐决策结论 | 对应分类 | +| :-: | :--- | :--- | :--- | :---: | +| **Q-BLK-01** | **供应商联调测试沙箱 (Sandbox) 与实验台锁具**
供应商能否在联调前提供专属测试租户(独立 `appid`、`appsecret`、`projectId`)以及挂载在实验台的物理测试门锁与网关? | 研发与测试交付
严禁在学员现住公寓门锁实操 | **必须提供物理实验台沙箱**,包含至少 1 台测试网关与 2 把独立测试锁。 | **A. 测试沙箱** | +| **Q-BLK-02** | **一期试点采用的主要开门钥匙类型**
沈阳房建公寓段一期学员开门主凭据确定采用密码还是卡片? | 硬件投入与实施成本
1. 在线限时密码 (4~8位,建议6位)
2. 在线芯片学员卡 (需配读卡器) | **推荐选项 1(在线限时密码)**:零额外硬件成本,全软件闭环管理;实体卡作为二期备选。 | **B. 主钥匙类型** | +| **Q-BLK-03** | **门禁授权触发时点 (Grant Trigger)**
何时正式向智能门锁下发通行权限? | 现场效率与出入安全
1. 住宿预分配完成后立即下发
2. **学员现场完成《报到核验》后下发**
3. 临近开班前定时批量下发 | **推荐选项 2(报到成功后触发)**:人已到校核验确认才给开门钥匙,防止未报到人员提早进房。 | **C. 授权时点** | +| **Q-BLK-04** | **退训与请假离校撤权规则**
学员中途退训或请假离校时,门锁权限如何处理? | 公寓安防与考勤纪律
1. 退训:强制立即主动撤销权限
2. 请假:保留权限 / 临时冻结 | **推荐退训立即强制撤权**;学员短期请假物品留在宿舍,建议**默认保留权限**(需公寓方签字确认)。 | **D. 撤权规则** | +| **Q-BLK-05** | **正式环境网络链路、Base URL 与 IP 白名单**
沈阳机房生产服务器能否直接连通 `dms.dmsiot.com`?DMS 是否对调用方公网 IP 强制配置防火墙白名单? | 生产网络连通性 | 优先确认生产机房出网策略,向供应商提供固定公网出口 IP 加入白名单。 | **E. 网络连通** | +| **Q-BLK-06** | **设备离线时 `validUntil` 本地过期语义 (`DMS_LOCK_OFFLINE_EXPIRY_SEMANTICS`)**
若门锁处于离线/断网状态,在线密码与卡片到达 `validUntil` 后,是否由锁端本地 RTC 硬件时钟强制拒绝开门?门锁电池耗尽断电、更换重启或时钟漂移时本地过期保证级别如何? | 物理安全与离线兜底
公开文档未确认锁端离线过期行为 | **必须向供应商确认**:确认锁具离线 RTC 行为;系统在结训时坚决执行主动撤权 + 有效期边界双保障。 | **F. 离线过期语义** | + +--- + +## 3. 非阻塞性技术与业务协商清单 (`NON_BLOCKING QUESTIONS`) + +| # | 问题内容 | 推荐结论与技术建议 | +| :-: | :--- | :--- | +| **Q-NON-01** | **主数据权威归属**:是否使用 DMS 房间台账作为主数据覆盖本地系统? | **坚决否决**。RailwaySystem 住宿业务台账保持本地唯一真源,DMS 仅作为设备执行端。 | +| **Q-NON-02** | **并发 QPS 阈值**:学员报到高峰期 DMS 是否有调用频次与速率限制? | 请求供应商提供 QPS 指标;RailwaySystem 内部集成线程池平滑排队削峰。 | +| **Q-NON-03** | **异步回调 (Webhook)**:DMS 是否支持门锁执行完成后主动回调? | 若支持则优先接入回调;一期严格以内部 10 秒对账调度器为主。 | +| **Q-NON-04** | **开门密码学员触达渠道**:密码如何安全告知学员? | 推荐学员在移动端个人中心经二次身份验证后查看 30 秒倒计时明文,节省短信资费。 | +| **Q-NON-05** | **远程紧急开锁 (Remote Unlock)**:一期是否需要上线远程开锁? | 属于高危控制操作,一期严格排除;仅设计两票制受控应急方案备查。 | +| **Q-NON-06** | **门锁电量与信号监控**:是否需要专门的设备运行状态大盘? | 一期在宿舍列表表格中轻量展示电量与信号徽标,辅助宿管员排查低电量门锁。 | diff --git a/docs/development/dms-r3/14_decision_summary.md b/docs/development/dms-r3/14_decision_summary.md new file mode 100644 index 000000000..962e81c27 --- /dev/null +++ b/docs/development/dms-r3/14_decision_summary.md @@ -0,0 +1,108 @@ +# 14 DMS 门禁集成审计与设计决策一页纸总结 (Decision Summary) + +> **面向对象**:项目负责人、架构指导老师、业务决策评审委员会 +> **核心宗旨**:以最短篇幅、最清晰视角,讲透“现状、能力、接入边界、推荐架构与实施规划”。 + +--- + +## 1. 当前系统有什么?(Current RailwaySystem Assets) + +1. **业务与数据基线**: + - 当前生产分支基于 `adcf930e`,已完成 V2 数据库结构迁移。 + - 物理表层:`train_dormitory_info`(宿舍楼栋/房间台账)、`train_accommodation`(学员参训住宿分配事实表)、`train_dorm_auth`(门禁授权表,外键绑定 `accommodation_id`)已在 Kingbase 物理库建表。 + - 业务逻辑层:已具备完整的培训计划编排、学员报到核验、宿舍入住分配与床位容量实时统计能力。 +2. **本地模型漂移与前置对齐要求 (`LOCAL_DOMAIN_ALIGNMENT_REQUIRED = YES`)**: + - **`train_accommodation`**:DB 物理表已建,但 Java 层尚无对应的实体类、Mapper 与业务 Service,未形成完整业务层。 + - **`train_dorm_auth`**:DB 已升级为以 `accommodation_id` 为外键的 V2 结构,但 Java 实体类与 XML Mapper 仍残留 V1 字段(`student_id`, `plan_id`, `dorm_id`)。 + - **决策红线**:在真正编写外部 DMS Adapter 之前,**必须在 Wave 0 优先完成本地住宿与门禁授权领域模型对齐**,严禁将外部适配器直接架设在漂移模型上。 + +--- + +## 2. DMS 平台能提供什么?(DMS IoT Platform Capabilities) + +外部 DMS 智能门锁平台(`https://dms.dmsiot.com`)具备完整的 RESTful 接口体系: +1. **楼栋-楼层-房间-门锁全层级资产查询**(可精确匹配房间获取 `placeId`,支持一房多锁)。 +2. **多模态电子钥匙下发能力**: + - **在线限时密码 (Online Password)**:系统可生成 4~8 位纯数字密码(Pilot 建议采用 6 位限时密码),指定起止有效时间,支持远程主动注销。 + - **在线实体芯片卡 (Online Card)**:支持将学员 IC/M1 卡按小端十六进制编码下发至锁具,支持远程主动注销。 + - **二维码/手机钥匙 (QR Key)**:端点已定义 (`POST /rest/doorlock/qrcode/create` 与 `POST /rest/qrcode/delete`),分类为 `DOCUMENTED`,但返回体存在数据污染且无具体二维码载荷,适用性归为 `NEEDS_BUSINESS_CONFIRMATION`。 +3. **低功耗异步执行与对账体系**:门锁基于电池无线通信,下发指令后返回 `operationId`,要求 10 秒后通过接口查询终态。 +4. **设备运行指标**:提供门锁电池电量、信号强度及门锁开门记录流水拉取。 + +--- + +## 3. 一期建议先接什么?暂时不接什么?(Scope Boundary) + +``` ++-------------------------------------------------------------------------------+ +| 一期重点纳入 (P0 / P1) | +| 1. 认证鉴权: Token 内存单飞排他锁缓存 + 滑动刷新 + 401 单次重试自愈 | +| 2. 房源映射: 本地宿舍与 DMS placeId / lockId 的预绑定持久化 (支持一房多锁) | +| 3. 首选凭据: 【在线限时密码】(零硬件与发卡器依赖, 支持4~8位/推荐6位, 随时主动撤销) | +| 4. 异步对账: 后台调度器 10 秒后异步轮询 operationId,消除 HTTP 线程阻塞 | +| 5. 换房补偿: 严格遵循"先发新房权限、确认生效后再撤旧房"原则,避免学员无处可住 | +| 6. 结训撤权: 培训结束主动派发 Revoke 指令,同时以 validUntil 作为有效期边界兜底 | +| 7. 设备巡检: 门锁电量与信号查询 (P1),辅助宿管员发现低电量设备 | ++-------------------------------------------------------------------------------+ + ▲ + 【严格划清边界】 + ▼ ++-------------------------------------------------------------------------------+ +| 一期明确排除 (P2 / Out of Scope / Needs Confirmation) | +| 1. 取电开关全套接口 (P2): 涉及强电改造,与学员核心学籍住宿无关,暂缓接入 | +| 2. 远程网络开锁 (P2): 属于高危主动开门操作,一期严格封存,仅设计受控两票制应急方案 | +| 3. 二维码钥匙 (Needs Biz Confirm): 接口已文档化但载荷污染,一期不作首选方案 | +| 4. 离线密码与离线发卡 (Out of Scope): 缺乏可靠远程主动撤销机制,增加现场运维负担 | ++-------------------------------------------------------------------------------+ +``` + +--- + +## 4. 需要向甲方与业务负责人确认的核心问题 (Blocking Decisions) + +1. **下发触发时机 (Q-BLK-03)**: + - *推荐意见*:**学员在现场完成《报到核验》后自动触发门禁下发**。确保人已到校才给钥匙,杜绝未报到人员提前进房。 +2. **首期钥匙类型 (Q-BLK-02)**: + - *推荐意见*:首选采用**系统派发 6 位在线限时密码**(学员移动端个人中心受控查看);若后续统一下发实体卡,可平滑启用在线卡片模式。 +3. **主数据权威归属 (Q-NON-01)**: + - *推荐意见*:**坚决以 RailwaySystem 作为住宿与房间主数据**,DMS 仅作为外部执行端,严禁让 DMS 状态覆盖本地系统。 +4. **现场网络连通性与沙箱 (Q-BLK-01 & Q-BLK-05)**: + - 生产环境机房是否允许访问公网 `dms.dmsiot.com`?供应商必须提供包含测试网关与实验台门锁的**独立联调测试沙箱**。 +5. **设备离线时 `validUntil` 过期语义 (Q-BLK-06)**: + - 门锁离线断网时,到达 `validUntil` 是否由锁端本地时钟强制失效?需供应商提供硬件保证级别说明。 + +--- + +## 5. 推荐技术架构设计 + +``` +[前端 Accommodation / 个人中心] + │ + ▼ +[业务领域层: 住宿分配 / 换房补偿 / 报到联动] <--- 绝对 Source of Truth + │ + ▼ (通过强类型 DTO,完全屏蔽外部厂商细节) +[中立网关接口: DoorAccessGateway (grant / revoke / query)] + │ + ▼ (SPI 模式) +[DMS 集成适配器: DmsDoorAccessGateway] + ├── DmsTokenManager: 进程级单飞排他锁 (防并发覆盖踩踏) + ├── 细分 HTTP 客户端: PlaceClient / DoorLockClient / KeyClient / OpClient + └── DmsOperationPoller: 后台 10 秒对账调度器 (解除 HTTP 线程阻塞) + │ + ▼ (HTTPS / Cookie 传递 token) +[外部 DMS 物联网平台: https://dms.dmsiot.com] +``` + +--- + +## 6. 后续 R.3B 四阶段推进路线图 (Four Waves, 约 15~16 人天) + +1. **Wave 0: Local Domain Alignment (本地领域模型对齐,3天)**: + - 实现 `TrainAccommodation` Java 实体/Mapper/Service;升级并清洗 `TrainDormAuth` 对齐 V2 数据库;确立住宿事实与门禁授权的清晰边界。 +2. **Wave 1: External Mapping & Operation Persistence (外部映射与持久化,2天)**: + - 创建 `train_external_resource_mapping` 与 `train_external_operation` 物理表及 Migration 脚本;落地双层状态机模型。 +3. **Wave 2: DMS Adapter & Mock Tests (DMS 适配器与离线仿真,5天)**: + - 配置与环境变量绑定;`DmsTokenManager` 单飞排他锁并发控制;细分 HTTP 客户端;在线密码/卡片适配器;10 秒轮询调度器;WireMock 22 项单测全覆盖。 +4. **Wave 3: Business Orchestration & UI & Sandbox (业务编排、界面与现场验收,5~6天)**: + - 报到联动授权下发;“先发新房后撤旧房”换房补偿;退训/结训撤权;管理端门禁综合视图;学员端 30 秒倒计时密码受控查看;现场实验台沙箱 E2E 闭环验收。