# 患者查询会话恢复:后端修复与验证 日期:2026-09-18。修改目录仅 `C:/kefu/wechat_rpa`;未修改 `C:/wechat_rpa`、未部署、未查询真实患者。测试使用合成身份、临时数据库和模拟上游响应,测试运行器阻断外部网络。 ## 根因与接口行为 桌面归档凭据与 ZYT 患者查询凭据各有有效期。`admin_backend.Database.desktop_session` 只验证和续期桌面凭据;原保存的 ZYT token 不会跟随桌面心跳续期,因此归档正常不能证明患者查询仍登录。父任务已实测 desktop/me 为 200、ZYT/session 为失效响应,患者查询上游登录超时;本分支未重复访问真实患者。 现在上游 HTTP 401、code=-1 或明确的登录过期消息会映射为: ```json {"detail":{"code":"zyt_session_expired","message":"ZYT 患者查询登录已过期,请重新验证账号。"}} ``` HTTP 状态为 409。存储的 ZYT 会话缺失或不能解密也采用这一可恢复结果。HTTP 403 或权限拒绝保持可读 403,不触发反复登录。其他患者接口上游故障仍为 502。 新增 `POST /api/v2/desktop/auth/zyt-session`,使用现有桌面 Bearer 凭据,JSON 请求体为 `{"zyt_token":"新的 ZYT token"}`,成功返回 `{"ok":true}`。仅验证为 terminal=7、active 且 `user_id` 与当前桌面账号严格一致的身份可更新。异账号返回 403。接口只更新原租户的加密 ZYT token 与当前账号权限、角色和 root 标识,不创建桌面凭据、不切换账号或租户。 上游身份服务网络错误、超时、HTTP 5xx、无效 JSON/身份数据返回 502,详情 code 为 `zyt_unavailable`,提示稍后重试;真正认证失效为 409,权限拒绝为 403。此分类复用 `zyt_auth` 保存的异常原因;没有修改该适配器或增加部署依赖。 ## 并发与权限 `archive_store.refresh_zyt_session` 在 `BEGIN IMMEDIATE` 事务内重新检查原桌面 token、session/device 撤销状态、账号启用状态、账号与设备归属、租户及身份匹配,然后一并更新权限和加密 token。上游验证期间撤销桌面会话、撤销设备或停用账号时,返回 401,不能续活。任何写入失败会回滚权限和 token,避免部分更新。 新事务采用 `expires_at >= now`;现有 `Database.desktop_session` 采用相同条件。没有修改表结构或桌面凭据续期政策。 ## 验证结果 - 首轮 60 项全通过:`test_zyt_session_recovery` 当时 18 项,以及 `test_zyt_patient_client`、`test_archive_api`、`test_admin_api.AuthTest` 既有 42 项。结果见 `backend-after.json` / `.log`。 - 补充网络分类后,恢复专项 23 项全部通过,0 失败、0 错误、0 跳过;结果见 `backend-final.json` / `.log`。总计覆盖 65 项不同测试,最后一轮仅重复变更相关专项。 - 新增覆盖:HTTP 401/403、业务过期和权限消息、缺失会话恢复、同账号更新、异账号拒绝、桌面归档仍有效、验证中撤销、设备撤销、账号停用、权限撤回和恢复、事务回滚,以及通过真实 `zyt_auth` 适配器模拟网络异常、超时、HTTP 500/502/503、坏 JSON、非对象响应、缺失身份、HTTP 401、业务过期、HTTP 403。 - 没有用真实患者查询验证恢复后的结果,没有声称生产已部署。生产验证由父任务负责。 ## 部署文件 仅四个生产文件: 1. `zyt_patient_client.py`:异常分型及上游错误分类。 2. `archive_api.py`:409/403 对外映射。 3. `archive_store.py`:缺失会话可恢复、原子刷新及再次校验。 4. `admin_api.py`:新请求体和恢复接口、身份服务故障分类。 测试文件 `test_zyt_session_recovery.py` 不必进入运行镜像。原件备份在 `before-backend/`,前后 SHA-256 在 `backend-manifest.json`。没有 `admin_backend.py`、`rbac.py`、`zyt_auth.py` 或 schema 变更。目标目录的 `admin_api.py` 有其他协议版差异,应依据备份与修改后文件定向应用本轮差异。 ## PHP 只读审计及边界 本地完整 PHP 源码位于 `C:/zyt`,由现有部署文档定位;本轮只读,未更改。它不能直接证明线上 PHP 版本完全一致。 - `server/config/project.php:36`:管理员 token 有效期 8 小时,临近过期窗口 1 小时;terminal=7 共用此政策。 - `LoginMiddleware.php:53`:过期/失效 token 在普通接口返回“登录超时,请重新登录”。第 63 行在 token 仍有效且进入窗口时调用 `AdminTokenService::overtimeToken`,续原 token,不签发另一枚 token。 - `DesktopController.php:23`:只有专用 `session` 标记免普通登录中间件限制,但方法内部仍验证身份、terminal=7 和账号状态。GET `/adminapi/desktop/session` 经过登录中间件,能保活尚未过期的 token,不能复活已过期 token。该请求首次触发续期时响应里的 expire_time 来自旧的 request.adminInfo,下一次读取才反映新有效期。 - `AdminTokenService.php` 位于 `server/app/adminapi/service/`:登录按 `(admin_id, terminal)` 找会话;过期或禁用同终端多点登录时替换 token,其他情况下复用有效 token 并续期。网页与 terminal=7 分开,不能笼统认为网页登录会踢桌面。 - 没有发现独立 refresh_token 接口;现有可用路径是仍有效时读专用 session 保活,以及过期后走已支持的登录接口重新认证。 - **IP 校验没有 terminal=7 豁免。** `AuthMiddleware.php:50` 对非免登录路由无条件比较缓存 login_ip 与当前请求 IP。专用 `desktop/session` 绕过该限制,`tcm.diagnosis/lists` 不绕过。因此新 token 可被管理服务器验证身份,不等同于必然可以从不同出口查询患者。桌面与管理服务器若共用公网出口则不一定触发;确实不同出口时可能出现“ip地址发生变化,请重新登录”。本轮没有放宽这一限制,也没有改变患者数据范围和菜单权限。 对仍有效 token,可由桌面通过专用 session 做有限保活;过期后仅恢复原账号的患者凭据。若实际部署确认不同出口被拦截,需要另行设计有明确身份和患者数据权限的受限接口,不应全局取消 IP 校验。