# 诊单发布后端合同独立终验 - 终验日期: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 真实新增与附件 | **CLOSED(AVAILABLE)** | `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 生命周期合同 | 每次打开重绑真实 Shell;overlay 同步 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()` 重绑真实 owner;overlay 采用 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 测试单独代替生产合同证据。 ## 发布门槛判断 - **业务/后端合同门槛:通过。** - **OPEN:P0 0 / P1 0 / P2 0。** - **AVAILABLE:题定范围内全部对齐。** - **唯一保留项:旧医生/跟踪笔记正文覆盖与整条删除需先扩展后端合同;这是明确的 server limitation,不阻断“当前 AVAILABLE 功能”发布。**