This commit is contained in:
Your Name
2026-08-10 17:46:04 +08:00
parent 9add23e019
commit c3ceb0dd0f
338 changed files with 554 additions and 614 deletions
+249 -309
View File
@@ -1,320 +1,260 @@
# 医生工作站最终 parity 审计
# 医生工作站最终发布 parity 审计
审计日期:2026-08-10
审计方式:只读源码复核 + 纯本地测试;未修改业务源码。
结论基准:`D:\web\zyt\admin\src\views` 的实际实现优先于既有 `research/parity_*.md`
审计日期:2026-08-10
## 1. 结论
审计对象:`D:\web\zyt\app` 当前源码
当前医生桌面整体判定为 **PARTIAL**,不建议在修复 P0 前作为后台同型版本发布。
唯一 Web 基准:`D:\web\zyt\admin\src\views` 及其直接引用的 `src/api``src/components``src/utils`
判定范围:医生桌面五页面(接诊台、我的患者、我的问诊、我的处方库、已开处方)及其 `core/services/video` 支撑合同。
- **P03 项**
1. 接诊附件没有上传步骤,本机绝对路径被作为附件 URL 发给服务端。
2. 问诊开方在当前 appointment 无处方或查询失败时按 diagnosis 回退,可显示、编辑或作废另一挂号的处方。
3. “我的患者”预约把 `source_patient_id` 优先写入 `patient_id`,而管理端该端点明确要求诊单 ID,可能预约到错误上下文或被后端拒绝。
- **Repository 方法存在性:EXACT**。当前五页及相关对话框实际调用的 canonical 方法均同时存在于 Protocol、Remote、Demo,未发现不存在或拼错的方法名;兼容别名均能解析到有效方法。
- **动态菜单:EXACT(限定五个受支持页面)**。非 Demo 会话使用服务端菜单,遵守显示、禁用、排序、路由及 canonical permission;未把未实现的后台运营页面伪装成本地入口。
- **医生活跃视频 eligibilityEXACT;权限语义:PARTIAL**。状态与三个业务 ID 已对齐,但问诊页错误复用了小程序二维码权限 `tcm.diagnosis/videoQr`
- 既有三份 parity 文档明显早于本轮实现,不能直接当作当前验收结果;本报告已重新逐项分类。
## 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;上传请求不继承 JSON `Content-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` 与 Admin `D:\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/scalarworker 不再读取 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` | **EXACT** |
| 医生备注 | `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|edit` | `{id}`、唯一性 DTO、完整医生相关字段 | **EXACT** |
| 诊单上下文订单 | `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. 验证记录
执行命令与结果:
```text
.venv\Scripts\python.exe -m pytest
110 passed in 1.33s
.\.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
```
全绿不能覆盖本报告的 P0`D:\web\zyt\app\tests\test_reception_parity_ui.py:263-306` 当前把本地绝对路径进入备注 payload 当成正确行为;`D:\web\zyt\app\tests\test_consultations_parity_ui.py:241-264` 当前把 appointment miss 后回退 diagnosis 旧处方当成正确行为。这两组测试需要随 P0 修复反向改写
测试数量明细:`12+11+12+2+1+16+2+12+4+7+10+11+12+10+18 = 140`
## 2. 范围与判定规则
## 9. 最终门槛
审计范围:
- Python 五页:
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py`
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py`
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py`
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py`
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py`
- 与五页直接相连的 `ui/dialogs/diagnosis.py``ui/dialogs/prescription.py``ui/widgets.py``ui/shell.py``core/models.py``services/*`
- 管理端唯一事实基准:`D:\web\zyt\admin\src\views`,必要时追到其直接使用的 `src/components``src/api`
明确排除:企业微信后台运营、转化/统计后台、医助专属旁观、小程序二维码本身、其他角色专属批量运营能力。医助旁观因此判为 **N/A / intentionally excluded**,不计 MISSING。
判定含义:
- **EXACT**:端点、DTO、可见字段/筛选/动作、权限及状态门槛在医生桌面范围内等价。
- **PARTIAL**:主链存在,但字段、上下文、权限、状态门槛、分页或异步一致性不完整。
- **MISSING**:管理端医生主链存在,而 Python 没有可用实现。
## 3. 页面与横切能力总表
| 范围 | 判定 | 已对齐 | 主要偏差 |
|---|---|---|---|
| 接诊台 | **PARTIAL** | 今日范围;等待/过号状态 1/4;详情、通知医助、备注读取/删除、完成接诊;直呼 ID | 附件真实上传 **MISSING/P0**;固定只取前 50 条;附件无预览/打开 |
| 我的患者 | **PARTIAL** | 患者/订单/面诊进度三工作区;订单筛选、summary/scope、字段、状态动作矩阵与 canonical permissions | 预约上下文 **P0**;预约表单、诊单详情、上下文订单不完整;写操作竞态 |
| 我的问诊 | **PARTIAL** | 列表、详情/编辑/删除端点;字典/医助;开方/作废入口;视频状态与 ID | appointment 处方回退 **P0**;诊单编辑只覆盖小字段子集;部分医生行操作/筛选缺失;视频权限码错误 |
| 我的处方库 | 功能 **EXACT** / 运行时 **PARTIAL** | 筛选、15 条分页、字段、查看/新增/编辑/删除、远程药材、校验、owner/root/role 行条件;`disable_edit` 语义正确 | 通用权限 helper 接受非 canonical 别名;worker 读 Qt 控件;加载中 refresh 被丢弃 |
| 已开处方 | **PARTIAL** | 完整筛选与主要字段;详情、CRUD、患者修正、审核、作废、订单创建/查看、A4 打印、PDF;状态动作矩阵 | 诊单详情授权边界;建单支付单竞态;诊单上下文订单与部分业务字段;重复药材校验 |
| Repository Protocol/Remote/Demo 方法 | **EXACT** | 五页实际 canonical 调用全部存在且签名兼容 | 上传素材方法 **MISSING**Demo 问诊筛选语义不完整 |
| 动态菜单 | **EXACT** | 使用服务端 menu;显示/禁用/排序/路由/权限;只注册受支持页面 | 无发布阻断偏差 |
| 视频端点与 eligibility | **PARTIAL** | ticket/start/bind/end 方法存在;问诊 `has_appointment && status==1`;接诊今日状态域;ID 分离 | 原生直呼错误复用 `videoQr` 权限;服务端仍须最终复核当前状态/归属 |
## 4. P0 发布阻断
### P0-1 接诊附件不是上传,而是泄漏并保存本机路径 — MISSING
管理端合同是严格的两阶段流程:
1. `POST /upload/image``POST /upload/file`multipart 字段为 `file``cid=0`,返回服务器 `uri/url``D:\web\zyt\admin\src\api\file.ts:7-33`
2. 素材选择器只把上传成功的服务器地址交给业务组件:`D:\web\zyt\admin\src\components\material\picker.vue:260-283`
3. 再调用 `POST /doctor.appointment/addDoctorNote`payload 为 `{diagnosis_id, content?, tongue_images?: string[], report_files?: string[]}``D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue:195-227`
Python 只是用文件选择器保存 `Path(raw_path)`,界面虽写“待上传”,但没有任何上传请求:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1291-1346`。保存时本机路径被原样放进 `tongue_images/report_files``D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1358-1393`Remote repository 直接 JSON 透传:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:709-730``ApiClient` 只有 JSON 请求并固定 `Content-Type: application/json`,没有 multipart`D:\web\zyt\app\src\doctor_workstation\services\api_client.py:108-180,211-220`
影响:服务端会收到 `C:\Users\...\report.pdf` 一类不可共享路径,其他终端无法访问,同时泄漏本机目录。已有 notes 读取与删除端点正确,但不能补救创建时的坏数据:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:732-759`
可执行修复:
1.`ApiClient` 增加独立 multipart 方法,不沿用 JSON `Content-Type`,由 httpx 生成 boundary。
2. 在 repository 增加 `upload_material(path, material_type, cid=0)`;图片走 `/upload/image`,报告走 `/upload/file`
3. 所有素材上传成功后才调用 `addDoctorNote`;最终 DTO 必须拒绝盘符路径、UNC、`file://`
4. 部分失败不提交本地路径,逐文件提示;必要时清理已上传但未关联素材。
5.`D:\web\zyt\app\tests\test_reception_parity_ui.py:263-306` 改成 multipart + 最终 JSON 双阶段测试,并断言最终 JSON 只含服务器地址。
### P0-2 当前 appointment 处方 miss/异常后回退 diagnosis 旧处方 — PARTIAL
管理端从问诊行传 `diagnosis_id=row.id``appointment_id=row.appointment_id``D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:1685-1694`。处方组件只以 `GET /tcm.prescription/getByAppointment {appointment_id}` 判断当前挂号是否已有处方;空结果进入当前挂号的新建流程,再用 `GET /tcm.diagnosis/detail {id}` 生成病历快照:`D:\web\zyt\admin\src\components\tcm-prescription\index.vue:1829-1907`。保存明确发送 `diagnosis_id``appointment_id``case_record``D:\web\zyt\admin\src\components\tcm-prescription\index.vue:2219-2268`
Python 已有正确的两个 repository 端点:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:994-1016`。但当前 appointment 查询为空,甚至 401/403/网络异常时,都会继续按 diagnosis 查询并选择“最新一张”:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1179-1216`。选中的 fallback 随后可被展示或作废:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1250-1283,1350-1383`。Demo 的 `get_prescription_by_appointment` 也仅按 diagnosis 返回首张,忽略 appointment`D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:752-760`
同时,新建只从列表行拼少量患者字段:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1300-1347`;编辑器 payload 没有完整 round-trip `appointment_id/case_record``D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1304-1370`;模型也没有正式保留这两个字段:`D:\web\zyt\app\src\doctor_workstation\core\models.py:580-646,728-771`
影响:同一 diagnosis 的 appointment A 有处方、B 无处方时,B 可误看、误编辑或误作废 A;网络/权限错误也被错误解释为“允许回退”。
可执行修复:
1. `appointment_id > 0` 时只允许 `getByAppointment` 决定当前处方;空结果新建 B,异常 fail-closed 并提示,绝不按 diagnosis 自动回退。
2. diagnosis 级历史只能做独立只读历史列表,不能成为查看/编辑/作废目标选择器。
3. 新建前调用 `get_diagnosis_detail`,把不可变 `case_record``diagnosis_id + appointment_id` 一起提交。
4.`Prescription` 增加并完整序列化 `appointment_id``case_record`Demo 按 appointment 精确匹配。
5. 反向改写 `D:\web\zyt\app\tests\test_consultations_parity_ui.py:241-264`:A 有处方、B 无处方时 B 必须新建 B;查询异常不得回退或作废 A。
### P0-3 “我的患者”预约使用了错误的 `patient_id` 语义 — PARTIAL
管理端在打开预约框时刻意把 `patient_id` 覆盖成 `diagnosis_id || id``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:513-518`。预约组件把该值设为 `patientInfo.id``D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:392-402`,并提交到 `POST /firstvisit.myPatient/createAppointment``D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:701-719`
Python payload 虽计算了 `diagnosis_id`,却让 `patient_id` 优先取 `source_patient_id``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:266-279`。Remote 不作转换,直接把整个 body 发给同一端点:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1318-1325`。管理端仅在视频 ticket 中使用 `source_patient_id`,同时保留 diagnosis ID,证明两者不是同一语义:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:642-661`
影响:只要列表同时返回 `diagnosis_id``source_patient_id`Python 就与实际端点 DTO 不同,可能绑定错误患者上下文或被后端拒绝。
可执行修复:
1. `firstvisit.myPatient/createAppointment``patient_id` 固定使用当前 `diagnosis_id`,不要用 `source_patient_id`
2. repository 为该端点定义显式 DTO,避免“完整字典透传”隐藏字段语义错误。
3. 增加 `diagnosis_id != source_patient_id` 的合同测试,断言 body 的 `patient_id == diagnosis_id`
4. 视频 ticket 继续使用独立的真实 patient ID,不把本修复扩散到视频 DTO。
## 5. P1 高优先级缺口
### P1-1 诊单详情/编辑只是字段子集,且未落实隐私权限 — PARTIAL
管理端根据 `tcm.diagnosis/phonePlain` 决定明文手机号,并在无权时先对手机号、身份证脱敏:`D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue:822-856,1263-1274`。Python `DiagnosisDialog` 构造函数不接 permissions`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:53-65`,直接渲染电话等字段:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:317-352`。编辑仅有 9 个文本字段:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:150-174`,保存也只回传这组子集:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:446-464`
修复:把 permission set 传入对话框;明文手机严格要求 `tcm.diagnosis/phonePlain`;身份证同样 fail-closed;按管理端 DTO 补齐患者基本信息、生命体征、病史/四诊/诊断字段及电话/身份证唯一性检查;对后端声明不可编辑的基础字段锁定。
### P1-2 患者预约表单缺排班、号源和关键 DTO 字段 — PARTIAL
管理端加载医生列表、未来 7 天排班、可用时间段,并检查当天重复预约:`D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:423-447,495-625`;提交要求 `appointment_type``channel_source``channel_source_detail``D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:683-719`。Python 只提供自由输入医生 ID、任意日期/时间/period/remarkpayload 缺上述字段:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:220-279`
修复:用后端医生/排班/可用时间段数据驱动选择器;限制日期和可预约 slot;增加 appointment type、channel source/detail;提交前与服务端均做重复预约校验。与 P0-3 一起建立 exact DTO 测试。
### P1-3 诊单上下文订单缺失,支付/退款字段缩水 — PARTIAL
管理端诊单只读页按 `tcm.diagnosis/patientOrders` 显示订单,并请求 `GET /tcm.prescriptionOrder/lists {context_diagnosis_id, patient_id, scene:'diagnosis_edit'}``D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-88``D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:118-179`。Python `DiagnosisDialog` 只有病历、备注、挂号、指派四个 tab:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:53-99,381-386`,虽然 repository 已有订单列表端点:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1018-1029`
此外,Python 补支付单把 `pay_remark``completion_request=0``pay_create_type=fubei` 写死:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:2048-2076`,而管理端由用户明确选择:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderActionHost.vue:573-587`。Python 强制填写退款金额:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:2108-2131`;管理端允许省略:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderActionHost.vue:610-621`
修复:给 `DiagnosisDialog` 传 permissions,按 canonical `tcm.diagnosis/patientOrders` 增加只读订单 tab并使用 exact context DTO;补支付单暴露三项业务字段;退款增加“不指定金额”。
### P1-4 处方建单可在门槛未加载时提交,并可混入旧诊单支付单 — PARTIAL
Python 把 `deposit_min_amount` 初始化为 0,创建按钮立即可用:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1793-1827``paidPayOrders` 只在初始化时异步加载,没有 generation/diagnosis ID 回验:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1928-1979`diagnosis ID 又可编辑。最终 payload 可把新 diagnosis ID 与旧 `pay_order_ids/deposit_min_amount` 组合:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1989-2054`。管理端在诊单变化时清空并重载支付单:`D:\web\zyt\admin\src\views\consumer\prescription\index.vue:2307-2409`
修复:来自处方的 diagnosis ID 设为只读,或用 `/tcm.diagnosis/searchPatient` 受控选择器;变化时立即清空支付单并禁提交;捕获 `(generation, diagnosis_id)`,仅应用同上下文响应;加载成功后才启用保存;服务端再校验处方、diagnosis、每个支付单的归属和定金门槛。
### P1-5 患者页写操作 latest-wins 会吞掉已执行 mutation 的回调 — PARTIAL
`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1696-1728` 为所有写操作共用 `_action_generation`,但未串行化或禁用其他动作。动作 B 启动后,动作 A 即使已在服务端成功或失败,其回调也会被丢弃,可能不刷新、不提示,界面与服务端不一致。
修复:按 operation/entity 维护 pending token,或串行化并禁用动作;任何成功 mutation 都必须触发最终一致性 refresh;generation 只能决定消息落点,不能取消写后 reconcile。补“两个不同订单动作乱序完成”的测试。
### P1-6 QRunnable 工作线程读取 Qt 控件 — PARTIAL
`run_async` 的函数实际在工作线程执行:`D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:257-319`。以下 worker lambda 仍调用 `.text()``.currentData()` 或 QWidget 属性:
- 接诊状态:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:502-525`
- 患者列表:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:825-845`
- 患者订单:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1114-1129`
- 处方库:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:220-238`
- 模板导入:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:736-755`
修复:所有控件值在 GUI 线程先快照成不可变 query/page DTOworker 只执行 `repository.method(**query)`。这样既满足 Qt 线程约束,也保证 generation 对应的条件不会在执行中变化。
### P1-7 通用 permission helper 会把非 canonical 点号别名当成授权 — PARTIAL
canonical 权限常量采用 slash 形式,例如 `wcf.prescription/add``cf.prescription/edit` 对应代码定义:`D:\web\zyt\app\src\doctor_workstation\services\repository.py:33-52`。但通用 `has_permission` 会生成 slash/dot 互换别名:`D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-180`,因此仅持有 `cf.prescription.edit` 也可能通过 `cf.prescription/edit` 门槛。处方库和已开处方使用了该 helper:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:101-161``D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:282-391,809-815`。shell、患者、问诊已采用 exact/wildcard 语义:`D:\web\zyt\app\src\doctor_workstation\ui\shell.py:109-142``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:110-143`
修复:全项目统一 exact + `*`/`prefix/*` helper;删除点/斜杠互换;增加“只有 alias grant 时按钮必须隐藏且 handler 必须拒绝”的测试。服务端权限仍是最终防线。
### P1-8 原生医生直呼错误复用小程序二维码权限 — PARTIAL
医生直呼状态已经与管理端一致:管理端仅在 `has_appointment && appointment_status===1` 时启用:`D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:1778-1780`Python 同样分离并复核 appointment/patient/diagnosis ID`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:142-183,1094-1122,1413-1427`。接诊台的今日状态 1/4 和 ID 也正确:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1542-1564``D:\web\zyt\app\src\doctor_workstation\services\repository.py:640-692`
偏差是问诊原生直呼受 `tcm.diagnosis/videoQr` 控制:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:625-630,1413-1415`;管理端该权限只保护“小程序视频二维码”:`D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:390-391`。这还造成接诊台和问诊页对同一原生直呼的授权不一致。
修复:与后端确认独立原生直呼 permission(例如 `tcm.diagnosis/startCall`)并让两入口统一;若没有独立 grant,则不复用 `videoQr`,由 call endpoint 的后端授权兜底。ticket/start/bind/end 的服务端必须再次验证 appointment 当前状态及 diagnosis/patient 归属。医助 `watchCall` 继续排除。
### P1-9 接诊队列固定前 50 条,无继续加载 — PARTIAL
管理端每页 15 条并持续加载,同时维护其他队列计数:`D:\web\zyt\admin\src\views\patient\reception\index.vue:213,296-379`。Python 每次只请求 `page_no=1,page_size=50``D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:502-525`,没有分页或 infinite scroll。忙时第 51 位及以后患者不可达。
修复:按管理端实现 `page_no/page_size=15` 累加加载,并以 total 判定是否继续;切 tab/搜索重置页码和 items;用 generation + pending refresh 保证旧页不污染新筛选。
### P1-10 已开处方可打开诊单详情,但没有诊单权限门槛 — PARTIAL
处方页无条件给详情对话框启用诊单入口:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:633-664`,处方详情对话框据此显示按钮:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1641-1669`。这条数据入口未检查 `tcm.diagnosis/readonlyDetail` 或产品定义的等价授权。
修复:显式传入 permission set;按钮可见与 handler 双重校验 canonical 诊单只读权限;无权限时不发 `get_diagnosis_detail`。补只有 `cf.prescription/read` 而无诊单权限的拒绝测试。
## 6. Endpoint、DTO 与动作复核
| 业务 | 管理端/服务端合同 | Python | 判定 |
|---|---|---|---|
| 接诊队列 | `GET doctor.appointment/lists``status,start_date,end_date,page_no,page_size,patient_name` | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:640-692`;今日和 1/4 强约束 | DTO **EXACT**;分页 **PARTIAL** |
| 接诊详情/完成 | `detail``doctorNotify``completeAppointment` | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:694-765` | **EXACT** |
| 医生备注 | `doctorNotes/addDoctorNote/deleteDoctorNoteImage`;素材 URL 先上传 | 读/增/删端点存在,但缺 `/upload/image|file` | **MISSING/P0** |
| 我的患者列表 | `GET firstvisit.myPatient/lists`keyword/status/date/page | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1088-1102` | **EXACT** |
| 患者订单/进度 | `orders``faceToFaceProgress` + scope/summary | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1104-1128` | **EXACT** |
| 患者订单动作 | detail/edit、两类审核/撤销、支付、物流、完成、退款、撤回、上传药房 | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1130-1287`;状态矩阵 `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1211-1289` | 主链 **EXACT**;支付/退款表单 **PARTIAL** |
| 患者预约 | `POST firstvisit.myPatient/createAppointment``patient_id` 实为当前 diagnosis ID,另含医生/日期/时间/type/channel | Remote `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1318-1325` 透传缩水且错误 DTO | **PARTIAL/P0** |
| 问诊列表/诊单 CRUD | `tcm.diagnosis/lists|detail|add|edit|delete` | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1339-1418` | endpoint **EXACT**UI 字段 **PARTIAL** |
| appointment 处方上下文 | `getByAppointment`;空则当前 appointment 新建;保存 diagnosis/appointment/case_record | 端点存在,但错误 diagnosis fallback | **PARTIAL/P0** |
| 处方库 | `tcm.prescriptionLibrary/lists|detail|add|edit|delete``doctor.medicine/lists` | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:766-864` | **EXACT** |
| 已开处方 | `tcm.prescription/lists|detail|add|edit|delete|patchPatient|audit|void` | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:866-1016` | endpoint/状态动作 **EXACT**;上下文/权限 **PARTIAL** |
| 处方订单 | lists/detail/create/paidPayOrders | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1018-1080` | endpoint **EXACT**;建单异步 **PARTIAL** |
| 视频 | `getCallSignature/startCall/bindCallRoom/endCall`IDs 分离 | `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1575-1615` | endpoint/eligibility **EXACT**permission **PARTIAL** |
## 7. 筛选、字段、分页和行状态门槛
### 7.1 接诊台
- **EXACT**:今日 `start_date=end_date`、等待/过号状态 1/4、姓名筛选、选中患者详情与完成前重取详情。Python:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:488-558,591-750,1474-1529`
- **PARTIAL**:页面固定前 50 条;备注附件只能显示名称/删除,无管理端的图片预览/报告打开。Python:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1091-1142`;管理端:`D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue:52-101`
### 7.2 我的患者
- **EXACT**:患者列表 filters、page size 15、summary/scope;订单 keyword、处方审核、支付审核、履约状态、日期与分页;订单列和 action matrix。Python`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:547-688,825-874,913-1289,1292-1595`。管理端动作基准:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:32-124`
- **PARTIAL**:预约和诊单详情/编辑字段;诊单上下文订单;支付/退款输入;写操作回调竞态。
### 7.3 我的问诊
- **EXACT**:核心列表/详情 CRUD endpoints、canonical `tcm.diagnosis/add|edit|delete|readonlyDetail|kaifang`、视频 eligibility 与 ID。
- **PARTIAL**Python filters/columns 位于 `D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:649-660,875-904`;管理端基准在 `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:191-341,775-798,878-887`。Python 发出管理端不存在的 `consultation_type`,缺 doctor-relevant 的未接诊天数排序;预约、补身份证等管理端行入口没有在本页呈现。指派/医助专属动作不因本页缺失计发布阻断,因为它们不是医生桌面主链或已在患者页提供。
- **P0**:处方上下文不得按 diagnosis 自动选最近处方。
### 7.4 我的处方库
- **EXACT**:处方名/剂型/公开范围筛选、15 条分页、处方名/剂型/功效/归属/创建人/时间字段、只读/新增/编辑/删除、远程药材选择、剂量校验、owner/root/role(0/3) 行条件。Python`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:106-193,267-376`;管理端:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:1-111,260-301,333-390`
- **EXACT**`disable_edit` 是导入后药材行的保存锁,不是“处方库模板禁止编辑”的行权限。Python:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:554-585`
- **PARTIAL**canonical permission alias 与异步控件读取。
### 7.5 已开处方
- **EXACT**:SN、患者、审核状态、来源、日期、医生筛选;15 条分页;详情、CRUD、患者修正、审核通过/驳回备注、作废、订单创建/列表、A4 打印、PDF。Python`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:297-847``D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:955-1715`
- **EXACT**:编辑/删除/审核/患者修正/建单的行状态门槛与管理端 action matrix 等价。Python`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:569-577,799-841`;管理端:`D:\web\zyt\admin\src\views\consumer\prescription\index.vue:214-263,3154-3157`
- **PARTIAL**:编辑器没有管理端保存前的重复药材名拒绝。Python校验:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1397-1417`;管理端:`D:\web\zyt\admin\src\components\tcm-prescription\index.vue:2228-2231`。处方建单及诊单详情权限另见 P1。
## 8. Repository 调用存在性审计
结论:**EXACT;没有发现 UI 调用不存在/错误命名的 canonical repository 方法。**
- 接诊别名最终映射到 `list_appointments/get_reception`notes、tracking、complete 均存在。
- 患者列表、订单、进度、详情、所有订单动作、预约/取消、指派/身份证均在 Protocol/Remote/Demo 存在。
- 问诊列表、字典、医助、诊单 CRUD、appointment/diagnosis 处方查询、开方/作废均存在。
- 处方库 CRUD、药材检索均存在。
- 已开处方 CRUD、患者修正、审核、作废、诊单详情、订单 CRUD/paid orders 均存在。
定义集中于 `D:\web\zyt\app\src\doctor_workstation\services\repository.py:56-425`Remote 实现在 `D:\web\zyt\app\src\doctor_workstation\services\repository.py:640-1615`Demo 实现在 `D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:97-1515``D:\web\zyt\app\src\doctor_workstation\services\demo_repository.py:1-5` 只是重导出 Demo 类。
兼容调用器 `D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:183-248` 会静默删除未知 kwargs。当前没有因此丢掉必需字段,但这会掩盖未来拼写错误,判 **P2**:把允许删除的冗余键改为显式 adapter/allowlist,测试环境对其他未知键报错。
## 9. 动态菜单与 canonical permissions
### 动态菜单 — EXACT
- 支持页面注册表:`D:\web\zyt\app\src\doctor_workstation\ui\shell.py:41-106`
- 服务端节点 flatten、显示/禁用、排序:`D:\web\zyt\app\src\doctor_workstation\ui\shell.py:145-197`
- component/path 匹配和受支持页面解析:`D:\web\zyt\app\src\doctor_workstation\ui\shell.py:200-249`
- 非 Demo 会话取 menu 并解析:`D:\web\zyt\app\src\doctor_workstation\app.py:413-443``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:264-301,436-460`
仅渲染本地已经实现的五个页面是本轮明确范围,不把其余 admin 路由判 MISSING。不存在“拿静态菜单覆盖后端菜单”的旧问题。
### Permissions — PARTIAL
患者、问诊、shell 使用 exact/wildcard;处方库和已开处方仍经通用 alias helper。所有按钮可见性还必须在 action handler 再检查同一 canonical 权限,不能只靠隐藏按钮。优先修复 P1-7 与 P1-10。
## 10. 异步与竞态审计
已正确做 generation/目标校验的主链包括:接诊队列与详情、患者列表/助手/订单详情、问诊列表/计数/字典选项/处方上下文、诊单对话框加载保存、药材搜索、模板列表、订单列表。
仍需处理:
| 优先级 | 问题 | 证据 | 修复 |
|---|---|---|---|
| P1 | 患者 mutations 共用 latest-wins generation | `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1696-1728` | 串行化或 per-operation token;所有成功写入都 reconcile |
| P1 | 处方建单支付单/定金无上下文 generation | `D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1928-2054` | `(generation,diagnosis_id)` 回验;加载期间禁提交 |
| P1 | worker 直接读 QWidget | `D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:257-319` 及 P1-6 列表 | GUI 线程快照不可变 query |
| P1 | 切换问诊行会 invalidate generation 但可能保留 busy | `D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1094-1105,1234-1292` | invalidation 同时结束旧 busy,或 active token/cancel;确保新行按钮可恢复 |
| P2 | 处方库/处方列表 loading 时直接丢 refresh | `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:220-238``D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-529` | pending refresh 或并发请求 + generation;快照 filters |
| P2 | 处方诊单详情、订单详情缺目标 ID generation | `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:653-664``D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:2178-2196` | `_detail_generation` + target ID;加载期间禁重复点击 |
## 11. 低优先级与 Demo 差异
- **P2 / Demo filters**UI 发 `diagnosis_confirmed` 等筛选:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:875-904`Demo 只处理少数键且读成 `confirmed``D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:1243-1268`。Demo 字典除 `server_order` 外也为空:`D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:851-860`。补齐 UI 暴露的筛选和四组演示字典。
- **P2 / raw detail**:已开处方的订单列表详情是通用字段展示,管理端跳向完整订单路由。若桌面产品要求在本应用闭环,需实现 typed detail;否则应明确“只读摘要”范围。
- **P2 / refresh**:处方库与已开处方在请求过程中改筛选/翻页可能显示旧条件结果,见异步表。
## 12. 既有 parity 文档复核
### `parity_reception_consultations.md`
`D:\web\zyt\app\research\parity_reception_consultations.md:14-16,81,148-154` 中“未限定今日/队列详情竞态/视频状态错误/无附件 UI/无开方”等描述大多已经关闭。当前真实结论是:今日和状态 **EXACT**、详情竞态主要链路 **EXACT**、视频 eligibility **EXACT**、开方入口已存在;附件 UI 已存在但上传协议 **MISSING/P0**
### `parity_patients_permissions.md`
`D:\web\zyt\app\research\parity_patients_permissions.md:13-14,124-127,200-216` 中“订单工作区和动态菜单完全缺失”已关闭。当前三工作区、订单状态矩阵、summary/scope 和动态菜单均存在;仍开放的是预约 DTO/P0、诊单详情隐私、诊单上下文订单和写操作竞态。
### `parity_prescriptions.md`
`D:\web\zyt\app\research\parity_prescriptions.md:255-285,337-343` 把已开处方描述成只读,已过期。当前 `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:253-393,588-847` 已有 CRUD、审核、患者修正、订单、打印/PDF。处方库旧文档的所有权 fail-open 也已修成缺 ID 默认拒绝:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:267-279`。仍开放的是 P0 处方上下文、P1 order async/权限/重复药材等。
## 13. 建议修复顺序与验收门槛
1. **先修 P0-1 附件上传**:没有 multipart 和服务器 URL 的实现不得开放附件提交。
2. **再修 P0-2 处方上下文**appointment authoritative、异常 fail-closed、完整 `case_record`;删除 diagnosis 自动 fallback。
3. **修 P0-3 患者预约 ID**exact DTO + diagnosis/source patient 分离测试。
4. **随后修权限和订单上下文**P1-1、P1-3、P1-4、P1-7、P1-8、P1-10。
5. **最后收口异步/分页**per-operation mutation、Qt query snapshot、接诊分页、pending refresh。
发布验收至少应新增以下反例:
- 本机盘符/UNC/`file://` 永远不能进入 `addDoctorNote` JSON。
- 同 diagnosis 两个 appointments 时,B 的 miss/异常永远不能展示或作废 A 的处方。
- `diagnosis_id != source_patient_id` 时,预约 body 的 `patient_id` 必须等于 diagnosis ID;视频 body 保持真实 patient ID。
- 只有点号 alias permission 时,slash canonical action 必须拒绝。
- 两个患者订单 mutation 乱序完成后,UI 必须最终与服务端一致。
- 诊单切换前返回的 paid-order 响应不能进入新诊单 payload,门槛未加载时不能提交。
- 超过 50 位的今日接诊队列仍可继续加载。
完成以上 P0 并替换两条错误预期测试后,才可把整体结论从 **PARTIAL** 提升到可发布候选。
- **OPEN P00**
- **OPEN P10**
- **发布阻断:无**
- **最终结论:可作为医生桌面 release candidate 进入打包和部署环境验收。**