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

16 KiB
Raw Blame History

诊单发布后端合同独立终验

  • 终验日期: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/createorder_notcm.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 addDoctorNotedoctorNotesdeleteDoctorNoteImage 正文真实追加;舌象/报告先上传得到服务端 URI 再新增;单附件真实删除
处方与业务订单 CLOSED tcm.prescription/addlistByDiagnosistcm.prescriptionOrder/lists / detail 真实开方、历史处方查看、诊单上下文订单列表/详情与诊次偏移均接通
视频全量回放与逐通话上传 CLOSED getCallRecordscreateManualCallRecordattachLocalCallRecording 完整消费 recording_urls_list;工具栏上传可建手工记录;行内上传传精确 call_record_id
IM 归档读取与同步 CLOSED only_archived=1triggerImChatSync 首次及重载均固定归档读取;真实触发同步并按 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=1pending_assign_keyword,没有关键词时带月份。
  • 桌面端 src/doctor_workstation/ui/pages/consultations.py:1566-1602 发送 canonical 字段:keywordhas_appointmentdiagnosis_confirmeddiagnosis_typesyndrome_typeassistant_id、最新挂号日期/渠道、最新分配日期、appointment_datepending_bookingcompleted_appointmentpending_assignsort_unserved_days。待分配宽搜规则与 admin 一致。
  • consultations.py:1604-1653page/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-254tcm.diagnosis/lists 作为诊单导航权限,识别 tcm/diagnosistcm/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_nogenerateOrderQrcode

  • admin 的完整链在 D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:1478-1499orderCreate 成功后读取 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 共用 tcmDiagnosisDetailD:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1263-1322;保存调用 tcmDiagnosisEdit:1367)。独立 readonly 页调用 diagnosisReadonlyDetailreadonly.vue:155:231
  • API 定义分别是 GET /tcm.diagnosis/detailPOST /tcm.diagnosis/editGET /tcm.diagnosis/readonlyDetailD:/web/zyt/admin/src/api/tcm.ts:9-11:50-52:60-62
  • 桌面端 mode resolver 在 src/doctor_workstation/ui/dialogs/diagnosis.py:1427-1544edit 与 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 / editD:/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 只有三条医生笔记 APIaddDoctorNotedoctorNotesdeleteDoctorNoteImage
  • 桌面端 diagnosis.py:2424-2511 的正文新增真实调用 add_doctor_note;舌象/报告先调用 multipart upload_material,取得服务端 URI 后再把 URI 交给新增接口;单附件删除携带 note id、附件类别和服务器路径。
  • production repository 在 repository.py:906-985 实现上传以及 doctor.appointment/addDoctorNotedoctor.appointment/doctorNotesdoctor.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-1645UI 调度与 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. IMonly_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 仅公开 addDoctorNotedoctorNotesdeleteDoctorNoteImage,随后该 controller 结束;不存在医生笔记正文 edit/update 或整条 delete action。
  2. D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-76addOrAppenddiagnosis_id + 当天 find-or-create,并把新正文追加到当日内容,不允许任意覆盖历史正文。
  3. 同一 Logic 的唯一删除逻辑 deleteImage:114-136)只从 tongue_imagesreport_files 数组中删除一个附件,不删除笔记行。
  4. admin API D:/web/zyt/admin/src/api/patient.ts:20-40 也只有上述三条接口,没有被桌面端漏接的隐藏 edit/delete API。
  5. 跟踪备注同样只有 addTrackingNotetrackingNotes controller actionDiagnosisController.php:183-214);TrackingNoteLogic.php:23-95 只有按当天追加和按诊单读取,没有历史正文覆盖或整条删除。

因此:

  • “任意覆盖既有医生/跟踪笔记正文”与“删除整条笔记”是后端合同限制,不是客户端 P0/P1/P2。
  • 当前桌面端不展示无法兑现的按钮是正确的 fail-closed 行为。
  • 若产品必须新增这些能力,应先在 server 增加独立 authenticated endpoint、权限码、归属校验、审计记录、并发/版本策略和删除语义;在此之前不应由客户端通过追加接口模拟覆盖或删除。

离线验证

在当前 .venv 下使用 QT_QPA_PLATFORM=offscreenPYTHONDONTWRITEBYTECODE=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 测试单独代替生产合同证据。

发布门槛判断

  • 业务/后端合同门槛:通过。
  • OPENP0 0 / P1 0 / P2 0。
  • AVAILABLE:题定范围内全部对齐。
  • 唯一保留项:旧医生/跟踪笔记正文覆盖与整条删除需先扩展后端合同;这是明确的 server limitation,不阻断“当前 AVAILABLE 功能”发布。