16 KiB
诊单发布后端合同独立终验
- 终验日期: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会:- 按传入的精确 appointment id 查找状态可取消的挂号;
- 绑定当前 diagnosis;
- 用户确认后再次从当前模型按同一 id 重验;
- 最终只把该 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;舌象/报告先调用 multipartupload_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 快照的结论明确:
D:/web/zyt/server/app/adminapi/controller/doctor/AppointmentController.php:168-198仅公开addDoctorNote、doctorNotes、deleteDoctorNoteImage,随后该 controller 结束;不存在医生笔记正文 edit/update 或整条 delete action。D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-76的addOrAppend按diagnosis_id + 当天find-or-create,并把新正文追加到当日内容,不允许任意覆盖历史正文。- 同一 Logic 的唯一删除逻辑
deleteImage(:114-136)只从tongue_images或report_files数组中删除一个附件,不删除笔记行。 - admin API
D:/web/zyt/admin/src/api/patient.ts:20-40也只有上述三条接口,没有被桌面端漏接的隐藏 edit/delete API。 - 跟踪备注同样只有
addTrackingNote与trackingNotescontroller 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/;未使用网络:
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 功能”发布。