Files
zyt/app/research/final_parity_audit.md
T
2026-08-10 17:46:04 +08:00

21 KiB
Raw Blame History

医生工作站最终发布 parity 审计

审计日期:2026-08-10

审计对象:D:\web\zyt\app 当前源码

唯一 Web 基准:D:\web\zyt\admin\src\views 及其直接引用的 src/apisrc/componentssrc/utils 判定范围:医生桌面五页面(接诊台、我的患者、我的问诊、我的处方库、已开处方)及其 core/services/video 支撑合同。

1. 发布结论

RELEASE CANDIDATE。 原报告的 3 个 P0 与 10 个 P1 已全部关闭:

  • P0CLOSED 3 / OPEN 0
  • P1CLOSED 10 / OPEN 0
  • 当前未发现医生桌面范围内的发布阻断项。
  • 处方预约上下文、附件上传和预约 ID 语义等高风险路径均有反例测试。
  • 全量测试:140 项通过
  • 静态检查:ruff check src tests 通过;ruff format --check src tests 显示 53 files already formatted

本结论是客户端源码、管理端基准和纯本地测试合同的发布复审结论;服务端生产权限、数据归属和真实第三方视频可用性仍应由部署环境验收。

2. OPEN / CLOSED 总表

编号 原问题 最终状态 parity 结论
P0-1 接诊附件未 multipart 上传并可能泄漏本机路径 CLOSED EXACT
P0-2 appointment 处方查询回退 diagnosis 历史处方,缺 case_record CLOSED EXACT / fail-closed 加固
P0-3 我的患者预约误用真实患者 ID,排班 DTO 不完整 CLOSED EXACT
P1-1 DiagnosisDialog 字段缩水、隐私与唯一性检查缺失 CLOSED EXACT
P1-2 预约表单缺医生排班、服务端号源和关键字段 CLOSED EXACT
P1-3 诊单上下文订单缺失,支付/退款字段缩水 CLOSED EXACT
P1-4 建单 paid-order/定金未加载或串诊单竞态 CLOSED EXACT / fail-closed 加固
P1-5 患者写操作 latest-wins 吞掉已执行 mutation 回调 CLOSED EXACT
P1-6 QRunnable worker 读取 QWidget CLOSED EXACT
P1-7 permission helper 接受点号/斜杠别名,handler 缺二次门槛 CLOSED EXACT
P1-8 原生医生直呼复用小程序 videoQr 权限 CLOSED EXACT(客户端)
P1-9 接诊队列固定前 50 条 CLOSED EXACT
P1-10 已开处方无 readonlyDetail 权限仍请求诊单 CLOSED EXACT

3. P0 逐项复核

P0-1 multipart、两阶段提交与安全路径 — CLOSED

Admin 合同:

  • D:\web\zyt\admin\src\api\file.ts:9-32POST /upload/image|filemultipart 字段为 filecid,返回 uri/url
  • D:\web\zyt\admin\src\components\material\picker.vue:260-283:业务组件只接收上传后的服务端地址。
  • D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue:195-225:最终备注 DTO 为 diagnosis_id/content/tongue_images/report_files
  • D:\web\zyt\admin\src\api\patient.ts:20-26:最终提交 POST /doctor.appointment/addDoctorNote

App 证据:

  • D:\web\zyt\app\src\doctor_workstation\services\api_client.py:134-157,172-196,249-270:真实 multipart;上传请求不继承 JSON Content-Typeboundary 由 httpx 生成。
  • D:\web\zyt\app\src\doctor_workstation\services\repository.py:796-818:图片走 upload/image、文件走 upload/file,发送 filecid=0
  • D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1485-1518,1520-1573:GUI 线程先快照本地选择;worker 按顺序上传全部材料,全部成功后才提交备注。
  • D:\web\zyt\app\src\doctor_workstation\services\repository.py:825-848,1781-1831:上传响应和最终 JSON 双层拒绝盘符路径、UNC、file: 与反斜杠本机路径;空 uri/url 也拒绝。
  • D:\web\zyt\app\tests\test_api_client.py:77-113D:\web\zyt\app\tests\test_reception_parity_ui.py:462-544:验证 multipart boundary、服务端 URL-only JSON 和任一上传失败时绝不提交备注。

结论:本地文件只作为上传输入,不进入 addDoctorNote JSON;原 P0 已关闭。

P0-2 appointment 唯一权威与完整 case_record — CLOSED

Admin 合同:

  • D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:1685-1694:问诊行明确传入 diagnosis_idappointment_id
  • D:\web\zyt\admin\src\components\tcm-prescription\index.vue:1850-1863:首先请求 GET tcm.prescription/getByAppointment {appointment_id}
  • 同文件 :1865-1895,2251-2267:无当前挂号处方时从诊单详情生成病例快照,保存 diagnosis_id/appointment_id/case_record

App 证据:

  • D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1168-1191:唯一查询源是 get_prescription_by_appointment,不存在 diagnosis 历史回退。
  • 同文件 :1193-1209,1227-1241,1300-1307:仅空结果进入新建;查询异常直接 fail-closed;病例快照返回的 diagnosis ID 必须匹配当前行。
  • 同文件 :1315-1375:编辑种子和最终创建 DTO 都强制覆盖当前 diagnosis_id/appointment_id 并深拷贝完整 case_record
  • D:\web\zyt\app\src\doctor_workstation\services\repository.py:1030-1039,1118-1132:端点分别为 POST tcm.prescription/addGET tcm.prescription/getByAppointment
  • D:\web\zyt\app\src\doctor_workstation\core\models.py:581-584,653-657,728-734Prescription 正式反序列化/序列化 appointment_idcase_record
  • D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:861-870Demo 同样只按 appointment 精确匹配。
  • D:\web\zyt\app\tests\test_consultations_parity_ui.py:239-348:覆盖 miss、异常、禁止 diagnosis fallback、ID 与不可变病例快照。

说明:Admin 在预约查询异常处吞错后可能继续新建;App 选择更安全的异常 fail-closed,但没有改变 appointment 作为唯一处方权威的业务合同。

P0-3 预约 ID、排班和提交 DTO — CLOSED

Admin 合同:

  • D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:513-518:预约组件的 patient_id 被明确覆盖为 diagnosis_id || id
  • D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:392-402,494-628:加载医生、未来 7 天有效排班、服务端可用号源。
  • 同文件 :669-720:提交 patient_id,doctor_id,appointment_date,period,appointment_time,appointment_type,remark,channel_source,channel_source_detail
  • 端点定义:D:\web\zyt\admin\src\api\tcm.ts:107-108D:\web\zyt\admin\src\api\doctor.ts:19-20,50-51D:\web\zyt\admin\src\api\first_visit.ts:120-121

App 证据:

  • D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:219-251,319-337:预约上下文和今日重复检查均使用 diagnosis IDsource_patient_id 只保留给视频。
  • 同文件 :410-486,495-585:请求未来 7 天排班和 availableSlots,只接受可用、未过期、有配额的时段。
  • 同文件 :600-644:所有必填门槛与 Admin DTO 对齐,patient_id == diagnosis_id;特定自媒体渠道要求补充字段。
  • 同文件 :2208-2223:确认后通过 book_patient_appointment 提交完整快照 DTO。
  • D:\web\zyt\app\src\doctor_workstation\services\repository.py:722-784,1428-1433,1570-1573:端点为 getDoctorsdoctor.roster/listsavailableSlotsfirstvisit.myPatient/createAppointment
  • D:\web\zyt\app\tests\test_patients_ui.py:276-343:明确断言 diagnosis ID 与真实 patient ID 不同且 body 使用 diagnosis ID,并核对排班/号源 query 和完整 body。

App 额外保留 diagnosis_id 作为显式上下文键;服务端仍收到 Admin 所需全部字段,未再混淆 ID。

4. P1 逐项复核

P1-1 DiagnosisDialog 隐私、字段 DTO 与唯一性 — CLOSED

  • AdminD:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue:823-856,916-970,1013-1132,1263-1275,1331-1367,明文受 tcm.diagnosis/phonePlain 控制,基础字段受服务端锁定,并调用 phone/id-card 唯一性检查。
  • AppD:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:42-131,213-241,426-450,接收/继承 permissions,覆盖患者基本信息、生命体征、病史、四诊和诊断字段。
  • App:同文件 :595-689,缺权限时 phone/id-card 从 seed 到详情均 fail-closed 脱敏,且不把掩码写回服务端;patient_basic_locked/can_edit_patient_basic=false 时锁定基础字段。
  • App:同文件 :829-903,保存前校验格式并调用 checkPhone {phone,id}checkIdCard {id_card,id},然后 update_diagnosis
  • RepositoryD:\web\zyt\app\src\doctor_workstation\services\repository.py:1474-1509,1575-1583
  • 测试:D:\web\zyt\app\tests\test_prescription_security_ui.py:132-191

P1-2 预约医生、排班、号源与字段 — CLOSED

P0-3 已给出完整证据。补充:Admin 的可提交条件在 D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:377-386App 对应 fail-closed 条件在 D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:600-644。医生、排班日、服务端 slot、渠道缺一不可;状态 1/4 的今日挂号只阻止再约今天。

P1-3 诊单上下文订单、支付与退款 — CLOSED

  • AdminD:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-88,订单区只在 tcm.diagnosis/patientOrders 下出现。
  • AdminD:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:131-178exact query 为 context_diagnosis_id、可用时的 patient_idscene='diagnosis_edit'page_size=10
  • AppD:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:506-549,551-563,764-827,同权限、同 query、同分页,并以 diagnosis generation/target 拒绝过期响应。
  • RepositoryD:\web\zyt\app\src\doctor_workstation\services\repository.py:1134-1145,透传到 GET tcm.prescriptionOrder/lists
  • 支付/退款:D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:647-747,支付包含 order_type/pay_amount/pay_remark/completion_request/pay_create_type;退款包含原因和可选 refund_amount
  • Admin 对应:D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderActionHost.vue:573-621
  • 状态门槛:App D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1687-1757,2415-2420,2520-2578 与 Admin D:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:89-110 对齐。

P1-4 paid-order 上下文竞态与定金门槛 — CLOSED

  • AdminD:\web\zyt\admin\src\views\consumer\prescription\index.vue:2307-2337,2435-2480,诊单变化清空支付单并重新请求,创建 body 绑定 prescription/diagnosis/pay-order。
  • AppD:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1800-1847,1945-1950,已有处方的 diagnosis ID 锁定,初始保存禁用。
  • App:同文件 :1965-2060,变化立即清空旧支付单/定金;响应必须同时匹配 generation、请求 diagnosis、当前 diagnosis。
  • App:同文件 :2070-2139,加载中、失败或未就绪均禁止提交;门槛开启时必须选支付单且金额不低于 deposit_min_amount
  • RepositoryD:\web\zyt\app\src\doctor_workstation\services\repository.py:1157-1183paidPayOrders {diagnosis_id}prescriptionOrder/create
  • 测试:D:\web\zyt\app\tests\test_prescription_security_ui.py:227-260

P1-5 患者 mutation 不丢回调 — CLOSED

  • D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:2161-2202:每次写操作有独立 pending token;完成顺序不会使已执行 mutation 的 success/error 回调失效。
  • 同文件 :2183-2206:每个服务端成功都 reconcile 患者、订单、面诊进度三工作区。
  • 同文件 :1687-1757,2415-2420handler 提交前重新核对 canonical permission 和行状态。
  • D:\web\zyt\app\tests\test_patients_ui.py:373-398:两个 mutation 乱序完成仍触发两次 reconcile。

P1-6 QRunnable 只接收 GUI 快照 — CLOSED

  • D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:260-323run_async 的函数运行在 QRunnable。
  • 接诊:D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:522-607
  • 患者列表/订单:D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1293-1318,1587-1615
  • 处方库:D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:221-247
  • 模板导入:D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:759-780
  • 已开处方:D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-540

上述位置均在 GUI 线程先读取 .text().currentData()、日期/页码等,再向 worker 传纯 query/scalarworker 不再读取 QWidget。D:\web\zyt\app\tests\test_reception_parity_ui.py:269-310D:\web\zyt\app\tests\test_patients_ui.py:231-268D:\web\zyt\app\tests\test_prescription_security_ui.py:269-334 覆盖快照和 pending refresh。

视频后端写同样不阻塞 QtD:\web\zyt\app\src\doctor_workstation\video\lifecycle.py:84-153,156-304 以每通话 FIFO daemon worker 有序执行 start -> bind -> end,提供最长 5 秒的有限等待;D:\web\zyt\app\src\doctor_workstation\video\window.py:515-548 的窗口关闭只排队结束,不同步阻塞 GUI。

P1-7 canonical permission exact 与 handler 二次门槛 — CLOSED

  • AdminD:\web\zyt\admin\src\utils\perm.ts:3-14,权限字符串精确匹配。
  • AppD:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-183,只接受 exact、全局 *、资源 prefix/*;明确不把 resource.action 当成 resource/action
  • 处方库按钮和 handlersD:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:100-164,291-392
  • 已开处方查看、增改删、患者修正、审核、建单、订单列表 handlers:D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:642-689,707-907
  • Canonical action 基准:D:\web\zyt\admin\src\views\consumer\prescription\list.vue:36,85,93,102D:\web\zyt\admin\src\views\consumer\prescription\index.vue:107,112,216,232,242,250,260
  • 测试:D:\web\zyt\app\tests\test_prescription_security_ui.py:123-129,仅有点号 alias 时 slash action 被拒绝。

P1-8 视频 eligibility 与权限语义 — CLOSED(客户端范围)

  • AdminD:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:390-391tcm.diagnosis/videoQr 只保护小程序二维码;同文件 :1778-1780 的问诊视频门槛为 has_appointment && appointment_status == 1
  • AppD:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:166-183,1106-1111,1441-1453,使用相同 eligibility,且 appointment/patient/diagnosis 三 ID 分离、均需有效。
  • UI 原生直呼不再受 videoQr 门槛;测试见 D:\web\zyt\app\tests\test_consultations_parity_ui.py:86-92,390-403
  • Remote endpoint/payloadD:\web\zyt\app\src\doctor_workstation\services\repository.py:1677-1717,分别是签名、startCallbindCallRoomendCall

服务端仍必须最终验证通话状态和数据归属;这属于服务端安全边界,不是客户端页面缺失。

P1-9 接诊分页 — CLOSED

  • AdminD:\web\zyt\admin\src\views\patient\reception\index.vue:212-214,296-379PAGE_SIZE=15、按 total/count 持续加载和 ID 去重。
  • AppD:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:196-208,288-292,513-607,609-696,同为 15 条;切 tab/搜索重置,加载更多,按 ID 合并,以 generation/query-key 拒绝旧响应。
  • RepositoryD:\web\zyt\app\src\doctor_workstation\services\repository.py:668-695,透传 page_no/page_sizeGET doctor.appointment/lists
  • 测试:D:\web\zyt\app\tests\test_reception_parity_ui.py:179-260,覆盖跨页追加、筛选重置与 total 边界。

第 51 位之后的今日患者已可达,旧“固定前 50 条”结论失效。

P1-10 处方进入诊单的权限与只读端点 — CLOSED

  • D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:642-659:处方详情先要求 cf.prescription/read;诊单按钮另要求 tcm.diagnosis/readonlyDetail
  • 同文件 :661-698handler 再次检查 canonical 权限和 diagnosis ID,明确调用 readonly endpoint;无权时不发请求。
  • D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1668-1696:详情按钮按同一权限 boolean 隐藏。
  • D:\web\zyt\app\src\doctor_workstation\services\repository.py:1474-1488GET tcm.diagnosis/readonlyDetail {id}
  • D:\web\zyt\app\tests\test_prescription_security_ui.py:335-388:无权限不请求,并拒绝过期 target 响应。

5. Endpoint / DTO 最终合同

能力 Endpoint 关键 query/body 状态
接诊队列 GET doctor.appointment/lists status,start_date,end_date,patient_name,page_no,page_size EXACT
材料上传 `POST upload/image file` multipart file,cid
医生备注 POST doctor.appointment/addDoctorNote diagnosis_id,content,tongue_images,report_files,仅服务器 URI EXACT
当前挂号处方 GET tcm.prescription/getByAppointment appointment_id EXACT
新建处方 POST tcm.prescription/add diagnosis_id,appointment_id,case_record + 完整处方字段 EXACT
预约医生 GET tcm.diagnosis/getDoctors EXACT
排班 GET doctor.roster/lists doctor_id,start_date,end_date,status,page_no,page_size EXACT
可用号源 GET doctor.appointment/availableSlots doctor_id,appointment_date,period='all' EXACT
我的患者预约 POST firstvisit.myPatient/createAppointment Admin 九字段;patient_id == diagnosis_id EXACT
诊单详情/编辑 `GET detail readonlyDetail; POST checkPhone checkIdCard
诊单上下文订单 GET tcm.prescriptionOrder/lists context_diagnosis_id,patient_id?,scene='diagnosis_edit',page_no,page_size EXACT
建单支付单 GET tcm.prescriptionOrder/paidPayOrders diagnosis_id EXACT
创建业务订单 POST tcm.prescriptionOrder/create prescription_id,diagnosis_id,pay_order_ids? + 收货/服务/金额字段 EXACT
视频生命周期 getCallSignature/startCall/bindCallRoom/endCall 三 ID 分离;call_type=2room_id EXACT(客户端)

D:\web\zyt\app\src\doctor_workstation\services\repository.py、Protocol、Remote、Demo 和五页面调用名进行复核,当前新增调用均存在;未发现不存在或错误命名的 repository 方法。关键端点合同由 D:\web\zyt\app\tests\test_repository_parity.py:127-153,157-267,287-353 覆盖。

6. 异步、刷新与目标绑定

  • 列表请求均以 generation 拒绝旧响应;接诊还绑定 query-key 和 appointment target。
  • 处方库/已开处方在 loading 中收到刷新会记录 pending refresh,而不是静默丢失:D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:221-266D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-574
  • 处方详情、诊单详情、订单详情均绑定 generation + target IDD:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:608-705D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:2198-2284
  • 问诊切行同时 invalidate prescription generation 并释放 busyD:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1085-1111
  • Demo 的问诊筛选、字典、appointment-authoritative 处方及 mutation 均有真实离线语义:D:\web\zyt\app\tests\test_mock_repository.py:140-178,300-396

上述原 P2 风险也已关闭;不影响本次 P0/P1 发布判断。

7. 动态菜单与医生桌面范围

动态菜单仍以会话 menu 为权威,并要求菜单节点与当前 canonical permission 同时满足:D:\web\zyt\app\src\doctor_workstation\ui\shell.py:41-80,195-249。五个本地页面之外的节点不会被错误映射;非 Demo 且服务端 menu 为空时不会自行放宽页面。

下列功能明确不属于本次医生桌面同型范围,因此不计为 MISSING:

  • 患者小程序/H5、消费者端自助下单和二维码页面。
  • 医助 watchCall、医助专属批量分配与其他角色工作台。
  • 企业微信后台运营、素材运营、客户标签、统计/转化后台。
  • 药房、财务、物流、审核员等角色的完整运营路由;医生页只保留其被授权的订单动作。
  • 管理端 videoQr 小程序二维码能力;桌面原生直呼使用后端 ticket 与服务端最终授权。

8. 验证记录

执行命令与结果:

.\.venv\Scripts\python.exe -m pytest -o addopts='' --tb=short
collected 140 items
140 passed in 1.49s

.\.venv\Scripts\python.exe -m pytest --collect-only -q
140 tests collected across 15 test files

.\.venv\Scripts\python.exe -m ruff check src tests
All checks passed!

.\.venv\Scripts\python.exe -m ruff format --check src tests
53 files already formatted

测试数量明细:12+11+12+2+1+16+2+12+4+7+10+11+12+10+18 = 140

9. 最终门槛

  • OPEN P00
  • OPEN P10
  • 发布阻断:无
  • 最终结论:可作为医生桌面 release candidate 进入打包和部署环境验收。