21 KiB
医生工作站最终发布 parity 审计
审计日期:2026-08-10
审计对象:D:\web\zyt\app 当前源码
唯一 Web 基准:D:\web\zyt\admin\src\views 及其直接引用的 src/api、src/components、src/utils
判定范围:医生桌面五页面(接诊台、我的患者、我的问诊、我的处方库、已开处方)及其 core/services/video 支撑合同。
1. 发布结论
RELEASE CANDIDATE。 原报告的 3 个 P0 与 10 个 P1 已全部关闭:
- P0:
CLOSED 3 / OPEN 0 - P1:
CLOSED 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-32:POST /upload/image|file,multipart 字段为file、cid,返回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;上传请求不继承 JSONContent-Type,boundary 由httpx生成。D:\web\zyt\app\src\doctor_workstation\services\repository.py:796-818:图片走upload/image、文件走upload/file,发送file与cid=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-113、D:\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_id与appointment_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/add与GET tcm.prescription/getByAppointment。D:\web\zyt\app\src\doctor_workstation\core\models.py:581-584,653-657,728-734:Prescription正式反序列化/序列化appointment_id、case_record。D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:861-870:Demo 同样只按 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-108、D:\web\zyt\admin\src\api\doctor.ts:19-20,50-51、D:\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 ID;source_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:端点为getDoctors、doctor.roster/lists、availableSlots、firstvisit.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
- Admin:
D:\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 唯一性检查。 - App:
D:\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。 - Repository:
D:\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-386;App 对应 fail-closed 条件在 D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:600-644。医生、排班日、服务端 slot、渠道缺一不可;状态 1/4 的今日挂号只阻止再约今天。
P1-3 诊单上下文订单、支付与退款 — CLOSED
- Admin:
D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-88,订单区只在tcm.diagnosis/patientOrders下出现。 - Admin:
D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:131-178,exact query 为context_diagnosis_id、可用时的patient_id、scene='diagnosis_edit'、page_size=10。 - App:
D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:506-549,551-563,764-827,同权限、同 query、同分页,并以 diagnosis generation/target 拒绝过期响应。 - Repository:
D:\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与 AdminD:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:89-110对齐。
P1-4 paid-order 上下文竞态与定金门槛 — CLOSED
- Admin:
D:\web\zyt\admin\src\views\consumer\prescription\index.vue:2307-2337,2435-2480,诊单变化清空支付单并重新请求,创建 body 绑定 prescription/diagnosis/pay-order。 - App:
D:\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。 - Repository:
D:\web\zyt\app\src\doctor_workstation\services\repository.py:1157-1183,paidPayOrders {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-2420:handler 提交前重新核对 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-323:run_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/scalar;worker 不再读取 QWidget。D:\web\zyt\app\tests\test_reception_parity_ui.py:269-310、D:\web\zyt\app\tests\test_patients_ui.py:231-268、D:\web\zyt\app\tests\test_prescription_security_ui.py:269-334 覆盖快照和 pending refresh。
视频后端写同样不阻塞 Qt:D:\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
- Admin:
D:\web\zyt\admin\src\utils\perm.ts:3-14,权限字符串精确匹配。 - App:
D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-183,只接受 exact、全局*、资源prefix/*;明确不把resource.action当成resource/action。 - 处方库按钮和 handlers:
D:\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,102与D:\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(客户端范围)
- Admin:
D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:390-391的tcm.diagnosis/videoQr只保护小程序二维码;同文件:1778-1780的问诊视频门槛为has_appointment && appointment_status == 1。 - App:
D:\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/payload:
D:\web\zyt\app\src\doctor_workstation\services\repository.py:1677-1717,分别是签名、startCall、bindCallRoom、endCall。
服务端仍必须最终验证通话状态和数据归属;这属于服务端安全边界,不是客户端页面缺失。
P1-9 接诊分页 — CLOSED
- Admin:
D:\web\zyt\admin\src\views\patient\reception\index.vue:212-214,296-379,PAGE_SIZE=15、按total/count持续加载和 ID 去重。 - App:
D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:196-208,288-292,513-607,609-696,同为 15 条;切 tab/搜索重置,加载更多,按 ID 合并,以 generation/query-key 拒绝旧响应。 - Repository:
D:\web\zyt\app\src\doctor_workstation\services\repository.py:668-695,透传page_no/page_size到GET 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-698:handler 再次检查 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-1488:GET 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=2;room_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-266、D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-574。 - 处方详情、诊单详情、订单详情均绑定 generation + target ID:
D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:608-705、D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:2198-2284。 - 问诊切行同时 invalidate prescription generation 并释放 busy:
D:\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 P0:0
- OPEN P1:0
- 发布阻断:无
- 最终结论:可作为医生桌面 release candidate 进入打包和部署环境验收。