Files
zyt/app/research/diagnosis_release_contract_audit.md
T
2026-08-11 09:12:51 +08:00

165 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 诊单发布后端合同独立终验
- 终验日期:2026-08-11
- 审计对象:`D:/web/zyt/app` 当前工作区终态
- 对照基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/`、其直接引用的 API,以及 `D:/web/zyt/server` 当前服务端控制器/Logic
- 审计性质:只读业务与后端合同终验;未访问网络、未安装依赖、未调用线上接口;除本报告和 pytest 临时产物外,未修改业务源码或测试
## 发布结论
**PASS(限本报告题定范围)。**
| 等级 | OPEN 数 | 结论 |
|---|---:|---|
| P0 | **0** | 未发现错误对象写入、越权入口、错误 endpoint、旧异步回调落地或主数据破坏风险 |
| P1 | **0** | 题定的所有主流程均已形成真实后端闭环 |
| P2 | **0** | 前两轮发现的 Daily 既有记录不可编辑、视频只显示首条 URL/不能逐通话追加等合同缺口均已关闭 |
**AVAILABLE 后端功能是否全部对齐:是。** 在“admin 诊单页面及其直接组件/API 中、本报告明确列出的可用后端功能”这一范围内,桌面端已经全部对齐:列表查询与分页、权限和菜单、多挂号精确取消、订单创建与付款二维码、三种详情模式、Daily 新增/编辑、真实 Notes 追加与附件、处方/业务订单、视频、归档 IM,以及 Shell owner/异步代次保护均为真实调用,无伪造成功动作。
这里的 AVAILABLE 不扩张为对 `admin` 全仓所有未枚举页面和无关路由的声明。本报告也不把后端从未提供的功能算成桌面端缺口:**任意旧笔记正文覆盖、删除整条医生笔记,以及旧跟踪备注改写/整条删除,在当前 server 快照中没有 endpoint,属于后端合同限制,不计入 P0/P1/P2。**
## 合同终验矩阵
| 题定能力 | 状态 | admin / 后端合同 | 桌面端终态 |
|---|---|---|---|
| 诊单列表筛选、分页、权限、菜单 | **CLOSED** | `tcm.diagnosis/lists`;canonical 权限;服务端动态菜单 | 筛选字段、待分配宽搜、分页总数/末页回退、统计计数请求、菜单裁剪均对齐 |
| 多挂号精确取消 | **CLOSED** | `POST doctor.appointment/cancel {id}` | 每张挂号卡携带精确 appointment id,确认前后重验同一诊单和可取消状态 |
| 创建订单后生成付款二维码 | **CLOSED** | `order.order/create``order_no``tcm.diagnosis/generateOrderQrcode` | 已消费真实 `order_no`,即时请求 QR;缺 order_no / qrcode_url 时 fail-closed,可重试且有 selection/generation 防串单 |
| edit / viewOnly / readonly | **CLOSED** | edit、viewOnly 用 `detail`;独立 readonly 用 `readonlyDetail`;保存用 `edit` | mode dispatcher 与 admin 一致,viewOnly 不误打 readonly endpoint |
| Daily 新增与既有血糖/饮食/运动编辑 | **CLOSED** | 三类 `add` / `edit` endpoint | 已有单元格解析 record id 并走 update;新增走 add;权限、DTO、owner/generation 校验完整 |
| Notes 真实新增与附件 | **CLOSEDAVAILABLE** | `addDoctorNote``doctorNotes``deleteDoctorNoteImage` | 正文真实追加;舌象/报告先上传得到服务端 URI 再新增;单附件真实删除 |
| 处方与业务订单 | **CLOSED** | `tcm.prescription/add``listByDiagnosis``tcm.prescriptionOrder/lists` / `detail` 等 | 真实开方、历史处方查看、诊单上下文订单列表/详情与诊次偏移均接通 |
| 视频全量回放与逐通话上传 | **CLOSED** | `getCallRecords``createManualCallRecord``attachLocalCallRecording` | 完整消费 `recording_urls_list`;工具栏上传可建手工记录;行内上传传精确 `call_record_id` |
| IM 归档读取与同步 | **CLOSED** | `only_archived=1``triggerImChatSync` | 首次及重载均固定归档读取;真实触发同步并按 media generation 刷新 |
| owner resize / generation | **CLOSED** | UI 生命周期合同 | 每次打开重绑真实 Shelloverlay 同步 Move/Resize/Show/Close;宽屏 60%、窄屏全宽;关闭使全部异步代次失效 |
## 逐项证据
### 1. 诊单列表:筛选、分页、权限与菜单
- admin 的筛选模型及待分配宽搜在 `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:773-917`;待分配有关键词时只保留分页、`pending_assign=1``pending_assign_keyword`,没有关键词时带月份。
- 桌面端 `src/doctor_workstation/ui/pages/consultations.py:1566-1602` 发送 canonical 字段:`keyword``has_appointment``diagnosis_confirmed``diagnosis_type``syndrome_type``assistant_id`、最新挂号日期/渠道、最新分配日期、`appointment_date``pending_booking``completed_appointment``pending_assign``sort_unserved_days`。待分配宽搜规则与 admin 一致。
- `consultations.py:1604-1653``page/page_size` 传入 repository,消费真实 `total`,超末页时回退并重查;每页仅接受 15/20/30/40。`:1672-1741` 的日期/待挂号/已完成/待分配计数沿用同一筛选语义。
- production repository 在 `src/doctor_workstation/services/repository.py:1562-1589` 调用 `GET tcm.diagnosis/lists`,不丢弃当前 canonical 查询字段。
- `src/doctor_workstation/ui/shell.py:46-254``tcm.diagnosis/lists` 作为诊单导航权限,识别 `tcm/diagnosis``tcm/diagnosis/index`;生产菜单以服务端 visible/enabled/sort/children 为事实源,同时还必须通过精确 slash 权限。空生产菜单不会静默回退出诊单入口,demo 才允许本地回退。
- 页面动作同样使用 exact slash 权限并同时检查 repository capability;没有把 dot 形式或相似字符串当成授权。
结论:查询 DTO、统计 DTO、分页和动态菜单/动作门禁均与 admin 的实际合同对齐。
### 2. 多挂号精确 `doctor.appointment/cancel`
- admin 从 `D:/web/zyt/admin/src/api/doctor.ts:60` 调用 `/doctor.appointment/cancel`,诊单列表在 `index.vue:1820``:1847` 传具体挂号 `id`
- 桌面端按嵌套挂号数组生成多个操作;歧义的行级“取消挂号”仅在唯一可取消挂号时成立。`consultations.py:2040-2107` 会:
1. 按传入的精确 appointment id 查找状态可取消的挂号;
2. 绑定当前 diagnosis
3. 用户确认后再次从当前模型按同一 id 重验;
4. 最终只把该 id 传给 diagnosis 专用取消方法。
- `repository.py:1550-1555` 明确调用 `POST doctor.appointment/cancel {"id": appointment_id}`,没有误走患者工作区的 `firstvisit.myPatient/cancelAppointment`
结论:同一诊单有多条挂号时不会取消错行,超 32 位 Python 整数也不会因 UI 转换而截断;本项 CLOSED。
### 3. 创建订单 → `order_no` → `generateOrderQrcode`
- admin 的完整链在 `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:1478-1499``orderCreate` 成功后读取 `res.order_no`,随后请求 `generateOrderQrcode({order_no})`
- 桌面端 `consultations.py:2279-2329` 只有在 `tcm.diagnosis/order` 权限以及 create/QR 两个 capability 都存在时开放入口。
- `:2331-2479` 已完整串联结果:读取顶层或 `data.order_no`,创建 QR 对话框并立即请求真实 QR;缺少 `order_no` 不会伪造二维码,缺少 `qrcode_url` 显示失败;retry、当前 diagnosis/patient、权限与独立 order generation 均受保护。
- `repository.py:2035-2076` 精确调用 `order.order/create`,返回 mapping;再调用 `tcm.diagnosis/generateOrderQrcode {"order_no": ...}`,并强制要求非空 `qrcode_url`
结论:此前“订单已创建但付款 QR 闭环中断”的 P1 已关闭。
### 4. edit、viewOnly、readonly endpoint 分流
- admin edit 与 viewOnly 共用 `tcmDiagnosisDetail``D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1263-1322`;保存调用 `tcmDiagnosisEdit``:1367`)。独立 readonly 页调用 `diagnosisReadonlyDetail``readonly.vue:155``:231`
- API 定义分别是 `GET /tcm.diagnosis/detail``POST /tcm.diagnosis/edit``GET /tcm.diagnosis/readonlyDetail``D:/web/zyt/admin/src/api/tcm.ts:9-11``:50-52``:60-62`
- 桌面端 mode resolver 在 `src/doctor_workstation/ui/dialogs/diagnosis.py:1427-1544`edit 与 viewOnly 均调用普通 detail;仅独立 readonly 调用 readonlyDetail。`repository.py:1591-1626` 实现同一分流和 edit 保存。
结论:viewOnly 不再误用权限感知 readonly endpoint;三种模式 CLOSED。
### 5. Daily:新增及既有血糖/饮食/运动编辑
- admin `DailyMatrix.vue:586-648` 按日期聚合记录并保留可编辑 id`:921-1013` 点击已有单元格时载入记录,提交时按 id 在 add/edit 之间分流。
- admin API 的现存合同为:
- `tcm.bloodRecord/add` / `edit``D:/web/zyt/admin/src/api/tcm.ts:114-120`
- `tcm.dietRecord/add` / `edit``:146-152`
- `tcm.exerciseRecord/add` / `edit``:173-179`
- 桌面端 Daily matrix 已按同样规则合并同日血糖字段,并给已有血糖、饮食、运动单元格保留 record id;`diagnosis.py:2234-2338` 分别打开新增/编辑器,并按 id 调用 add 或 update。
- production repository 在 `repository.py:1758-1818` 精确实现上述六个 endpoint;编辑器保留饮食/运动既有图片,mutation 完成只在 diagnosis/patient 和 daily generation 仍匹配时刷新。
结论:此前“只能新增、不能编辑已有三类记录”的 P2 已关闭。跟踪备注仍按后端设计只允许当天追加,见后端限制章节。
### 6. Notes:真实新增、上传与单附件删除
- admin `D:/web/zyt/admin/src/api/patient.ts:20-40` 只有三条医生笔记 API`addDoctorNote``doctorNotes``deleteDoctorNoteImage`
- 桌面端 `diagnosis.py:2424-2511` 的正文新增真实调用 `add_doctor_note`;舌象/报告先调用 multipart `upload_material`,取得服务端 URI 后再把 URI 交给新增接口;单附件删除携带 note id、附件类别和服务器路径。
- production repository 在 `repository.py:906-985` 实现上传以及 `doctor.appointment/addDoctorNote``doctor.appointment/doctorNotes``doctor.appointment/deleteDoctorNoteImage`,并拒绝把本地路径当成已上传附件提交。
- 所有按钮同时受 exact permission 和 capability 裁剪,viewOnly/readonly 不出现可写入口。
结论:当前后端实际 AVAILABLE 的 Notes 能力全部对齐;没有用本地假数据或“只 toast 不落库”模拟新增/附件操作。
### 7. 处方与诊单上下文订单
- 详情页处方 Tab 会查询 `tcm.prescription/listByDiagnosis`,编辑态“开方”打开真实处方编辑器并调用 `tcm.prescription/add`;历史处方查看使用该真实列表返回的处方数据打开详情对话框。
- 订单 Tab 使用 `tcm.prescriptionOrder/lists` 并带诊单上下文,详情调用 `tcm.prescriptionOrder/detail`;诊次统计偏移调用真实 `setRevisitSlotStartOffset`
- production endpoint 证据:`repository.py:1131-1149``:1220-1265``:1633-1645`UI 调度与 capability 门禁在 `diagnosis.py:2048-2058``:2534-2689``:2791-3186`
结论:题定的处方创建/查看及业务订单列表/详情/偏移均为真实后端行为,未发现伪动作或无权限仍可提交。
### 8. 视频:全量 `recording_urls_list` 与逐 `call_record_id` 上传
- admin `CallRecordPanel.vue:32` 把完整 `row.recording_urls_list` 传给播放组件;`:150-177` 支持“上传后新建手工通话记录再绑定”,也支持向指定现有记录追加。
- 桌面端 `diagnosis.py:2899-2967` 会归一化并去重 direct URL 与完整 `recording_urls_list`,逐一提供播放入口,不再只取数组第一个元素。每个有正 id 的行提供“追加回放”,闭包携带该行精确 `call_record_id`
- `diagnosis.py:2587-2613` 将可选精确 id 交给 repository。`repository.py:1875-1943` 的顺序与 admin 一致:先上传;无 id 时创建 manual call record;最后调用 `attachLocalCallRecording`。已有 id 时不创建新记录,直接绑定该 id。
结论:此前“只展示首条 URL、不能逐通话记录追加”的两个 P2 缺口均已关闭。
### 9. IM`only_archived` 与 sync
- admin `ImChatRecordPanel.vue:148-185` 固定 `only_archived: 1` 并调用 `triggerImChatSync`
- 桌面端 lazy query 在 `diagnosis.py:2084-2097` 固定 `only_archived=True`,同步动作在 `:2615-2627` 调用真实 repository,并在当前 media generation 仍有效时重载。
- `repository.py:1945-1965` 精确发送 `GET tcm.diagnosis/getImChatMessages {diagnosis_id, only_archived: 1}``POST tcm.diagnosis/triggerImChatSync`
结论:未混入 live merge 默认值,归档读取和同步闭环 CLOSED。
### 10. Shell owner、resize 与 generation
- `diagnosis.py:1325-1413` 每次打开/显示都从当前 `parentWidget().window()` 重绑真实 owneroverlay 采用 owner 的全局原点与完整尺寸,drawer 在宽屏取 owner 60%,窄屏取全宽,并响应 owner Move/Resize/Show/Close。
- `diagnosis.py:1731-1760` 在 reject/done/owner close 时统一递增 detail、save、orders、order detail、daily、notes、media、offset 及所有 lazy-tab generation,并关闭相关子对话框/播放器。
结论:移动或缩放真实 Shell 后几何会立即同步;关闭后旧请求结果不能回写到下一次打开的诊单。
## 后端合同限制:旧笔记不能任意覆盖或整条删除
本项进行了 admin API、controller、Logic 和 `D:/web/zyt/server` 全仓关键词交叉检索,当前 server 快照的结论明确:
1. `D:/web/zyt/server/app/adminapi/controller/doctor/AppointmentController.php:168-198` 仅公开 `addDoctorNote``doctorNotes``deleteDoctorNoteImage`,随后该 controller 结束;不存在医生笔记正文 edit/update 或整条 delete action。
2. `D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-76``addOrAppend``diagnosis_id + 当天` find-or-create,并把新正文追加到当日内容,不允许任意覆盖历史正文。
3. 同一 Logic 的唯一删除逻辑 `deleteImage``:114-136`)只从 `tongue_images``report_files` 数组中删除一个附件,不删除笔记行。
4. admin API `D:/web/zyt/admin/src/api/patient.ts:20-40` 也只有上述三条接口,没有被桌面端漏接的隐藏 edit/delete API。
5. 跟踪备注同样只有 `addTrackingNote``trackingNotes` controller action`DiagnosisController.php:183-214`);`TrackingNoteLogic.php:23-95` 只有按当天追加和按诊单读取,没有历史正文覆盖或整条删除。
因此:
- “任意覆盖既有医生/跟踪笔记正文”与“删除整条笔记”是**后端合同限制**,不是客户端 P0/P1/P2。
- 当前桌面端不展示无法兑现的按钮是正确的 fail-closed 行为。
- 若产品必须新增这些能力,应先在 server 增加独立 authenticated endpoint、权限码、归属校验、审计记录、并发/版本策略和删除语义;在此之前不应由客户端通过追加接口模拟覆盖或删除。
## 离线验证
在当前 `.venv` 下使用 `QT_QPA_PLATFORM=offscreen``PYTHONDONTWRITEBYTECODE=1`,禁用 pytest cache,并把 basetemp 放到 `artifacts/pytest_release_contract_audit/`;未使用网络:
```text
python -m pytest -q -p no:cacheprovider --basetemp artifacts/pytest_release_contract_audit tests/test_diagnosis_detail_contract.py tests/test_consultations_parity_ui.py tests/test_diagnosis_drawer_visual.py tests/test_diagnosis_index_visual.py tests/test_shell_contract.py tests/test_repository_parity.py tests/test_permissions.py
118 passed
```
退出码为 0。该结果覆盖 endpoint/DTO、权限裁剪、多挂号精确取消、订单 QR 串联、三种详情模式、Daily edit、Notes、视频多 URL/逐 call id、IM 和 owner/generation 的定向回归。测试通过是回归信号;最终 AVAILABLE 判定同时依赖上面的 admin/API/server 静态合同核对,不以 mock 测试单独代替生产合同证据。
## 发布门槛判断
- **业务/后端合同门槛:通过。**
- **OPENP0 0 / P1 0 / P2 0。**
- **AVAILABLE:题定范围内全部对齐。**
- **唯一保留项:旧医生/跟踪笔记正文覆盖与整条删除需先扩展后端合同;这是明确的 server limitation,不阻断“当前 AVAILABLE 功能”发布。**