This commit is contained in:
Your Name
2026-08-10 17:29:05 +08:00
parent 2199887c07
commit 9add23e019
129 changed files with 34157 additions and 59 deletions
+820
View File
@@ -0,0 +1,820 @@
# admin 医生端源码审计
审计日期:2026-08-10
参考项目:D:\web\zyt\admin
审计方式:只读检查 Vue/TypeScript 源码、API 封装、路由守卫、Pinia store、业务组件和权限判断;未修改 admin 项目,也未把 README 当作结论来源。
## 1. 结论摘要
1. admin 是 Vue 3 + TypeScript + Vite + Element Plus + Pinia 项目。医生端并不是一套独立的静态路由:除登录、H5 诊单和只读诊单外,页面路径、标题、组件和菜单权限都由登录后 GET /adminapi/auth.admin/mySelf 返回的 menu 动态注入。
2. 医生相关页面的全局状态很少。Pinia 只持有认证用户、权限、动态菜单、全局站点配置、布局与多标签;接诊台、处方、患者、问诊列表的查询条件和业务状态均保留在各页面的 ref/reactive 中,分页统一使用 usePaging。
3. “患者列表”有两个不同实现:
- 医生/一诊工作台的“我的患者”:src/views/first_visit/my_patients/index.vue,带患者、订单、面诊进度三个工作区,服务端按当前角色和部门数据范围收窄。
- 平台注册用户列表:src/views/consumer/lists/index.vue,仅展示头像、昵称、账号、手机号、渠道和注册时间,不是医生业务患者工作台。
4. “问诊列表”也有两个相关实现:
- 挂号/问诊执行列表:src/views/tcm/appointment/list.vue,默认“今天 + 待接诊”,支持通话、视频二维码、完成、开方、取消。
- 诊单/患者业务列表:src/views/tcm/diagnosis/index.vue,围绕诊单、挂号、确认、开方、医助指派、二维码和视频旁观。
最终菜单叫什么、URL 是什么取决于后端 menu 配置,不应仅根据文件名硬编码。
5. 实际视频问诊主链是 src/components/chat-dialog/index.vue:腾讯云 Chat UIKit 单聊 + TUICallKit 音视频;通话前后还串联后端通话记录、TRTC 房间绑定、云端混流录制、可选浏览器本地录制、截屏写医生备注。src/components/video-call/index.vue 是另一套旧/独立实现,目前源码中没有被任何页面引用。
6. 处方领域要区分三类对象:
- 处方库模板:tcm.prescriptionLibrary,供医生复用药材组合。
- 已开处方:tcm.prescription,处方笺、患者、医师签名、主辅方、审核与作废。
- 处方业务订单:tcm.prescriptionOrder,收货、费用、双审、支付单、药房和物流履约;它不是支付单 zyt_order。
## 2. 关键源码与路由
### 2.1 页面定位
| 业务 | 关键源文件(绝对路径) | 路由结论 |
|---|---|---|
| 登录 | D:\web\zyt\admin\src\views\account\login.vue | 静态精确路由 /login |
| 接诊台 | D:\web\zyt\admin\src\views\patient\reception\index.vue | 动态菜单组件键应指向 patient/reception/index;实际 URL 取 menu[].paths |
| 我的处方库 | D:\web\zyt\admin\src\views\consumer\prescription\list.vue | 动态菜单组件键应指向 consumer/prescription/list;实际 URL 取 menu[].paths |
| 药品库(不是处方库) | D:\web\zyt\admin\src\views\doctor\medicine.vue | 动态菜单组件键应指向 doctor/medicine |
| 已开处方/处方管理 | D:\web\zyt\admin\src\views\consumer\prescription\index.vue | 动态菜单组件键应指向 consumer/prescription/index |
| 处方业务订单 | D:\web\zyt\admin\src\views\consumer\prescription\order_list.vue | 动态菜单组件键应指向 consumer/prescription/order_list |
| 我的患者 | D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue | 动态菜单组件键应指向 first_visit/my_patients/index |
| 平台用户列表 | D:\web\zyt\admin\src\views\consumer\lists\index.vue | 动态菜单组件键应指向 consumer/lists/index |
| 问诊/挂号列表 | D:\web\zyt\admin\src\views\tcm\appointment\list.vue | 动态菜单组件键应指向 tcm/appointment/list |
| 诊单列表 | D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue | 动态菜单组件键应指向 tcm/diagnosis/index;另有静态 H5 路由 /tcm/diagnosis/h5 |
| 诊单编辑/只读抽屉 | D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue | 被多个页面异步复用,不一定是独立菜单 |
| 患者只读详情 | D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue | 静态精确路由 /tcm/diagnosis-readonly?id=诊单ID |
| 预约视频问诊 | D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue | 抽屉组件,由诊单列表/我的患者调用 |
| 诊间开方 | D:\web\zyt\admin\src\components\tcm-prescription\index.vue | 复用组件,由问诊列表和诊单编辑调用 |
| 聊天与视频问诊 | D:\web\zyt\admin\src\components\chat-dialog\index.vue | 浮动组件,由接诊台及问诊列表调用 |
| 医助视频旁观 | D:\web\zyt\admin\src\views\tcm\diagnosis\components\AssistantWatchCallDialog.vue | 诊单列表内异步组件 |
### 2.2 动态路由机制
关键文件:
- D:\web\zyt\admin\src\router\routes.ts
- D:\web\zyt\admin\src\router\index.ts
- D:\web\zyt\admin\src\permission.ts
- D:\web\zyt\admin\src\stores\modules\user.ts
流程:
1. 常量路由只注册 /login、/403、/change-password、/bind-work-wechat、/user/setting、/doctor/progress、/tcm/diagnosis/h5 和 /tcm/diagnosis-readonly 等少数页面。
2. 登录成功后 GET /auth.admin/mySelf。
3. user store 保存 data.user、data.permissions,并把 data.menu 交给 filterAsyncRoutes。
4. 每个后端菜单项使用以下字段转为 Vue Router:
- paths:路由路径;
- componentsrc/views 下的组件键;
- name:菜单标题;
- perms:写入 route.meta.perms
- is_show:控制 meta.hidden
- is_cache:控制 keepAlive
- params:默认 query
- selectedactiveMenu
- type:目录或菜单。
5. permission.ts 把转换后的路由动态挂到根布局;第一个可见菜单成为 / 的重定向目标。
因此,本仓库源码能确定组件和静态路由,但不能单独确定接诊台、处方库、已开处方、我的患者、问诊列表的生产 URL 与菜单标题。要得到精确值,必须取得当前环境 /auth.admin/mySelf 的 menu 响应或检查服务端菜单表。
## 3. 登录、认证、权限和状态管理
### 3.1 登录链路
关键文件:
- D:\web\zyt\admin\src\views\account\login.vue
- D:\web\zyt\admin\src\api\user.ts
- D:\web\zyt\admin\src\stores\modules\user.ts
- D:\web\zyt\admin\src\utils\request\index.ts
- D:\web\zyt\admin\src\utils\auth.ts
账号密码:
- POST /login/account
- 请求:account、password、terminal=1。
- 响应被页面使用的字段:token、is_paw、need_bind_work_wechat。
- token 写入本地缓存键 token,后续请求通过请求拦截器放到 HTTP 头 token。
- “记住账号”只缓存 account,不缓存密码,缓存键为 account。
企业微信:
- GET /login/workWechatConfig,使用 enabled、corp_id、agent_id。
- 企业微信内置浏览器走 OAuthscope=snsapi_privateinfo、state=admin_login。
- 普通浏览器动态加载 https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js 显示扫码登录。
- 回调 code 通过 POST /login/workWechatLogin,参数 code、terminal=1。
- 另有 POST /auth.admin/bindWorkWechat、POST /auth.admin/unbindWorkWechat。
守卫:
- is_paw=0 强制跳转 /change-password,并通过 POST /login/changeFirstPassword 修改。
- need_bind_work_wechat=true 强制进入 /bind-work-wechat。
- 没有 token 的非白名单路由跳 /login?redirect=原地址。
- /auth.admin/mySelf 没有任何有效菜单时清认证并跳 /403。
- 响应码约定:1 成功、0 失败、-1 登录失效、10 需要绑定企微、2 打开新页面、-2 未安装。
### 3.2 Pinia 与页面状态
| Store/Hook | 文件 | 职责 |
|---|---|---|
| user | D:\web\zyt\admin\src\stores\modules\user.ts | token、userInfo、routes、perms、isPaw;登录、退出、企微登录、加载个人信息 |
| app | D:\web\zyt\admin\src\stores\modules\app.ts | 网站配置、OSS 图片地址、移动端/侧栏状态、视图刷新 |
| tabs | D:\web\zyt\admin\src\stores\modules\multipleTabs.ts | 多标签与 keep-alive 缓存 |
| setting | D:\web\zyt\admin\src\stores\modules\setting.ts | 本地布局、主题配置 |
| usePaging | D:\web\zyt\admin\src\hooks\usePaging.ts | 页码、page_size、loading、count、lists、extend;支持 silent 静默刷新 |
列表接口统一期待服务端 data 为:
- lists:当前页数组;
- count:总数;
- extend:额外统计、日期、权限范围等扩展数据。
页面内筛选和弹窗状态不进入 Pinia。这一约定适合桌面端复用:认证/权限做全局 store,业务工作台保持页面级 store 或 view-model。
### 3.3 权限判断语义
关键文件:
- D:\web\zyt\admin\src\install\directives\perms.ts
- D:\web\zyt\admin\src\utils\perm.ts
需要特别注意两套语义不同:
- v-perms 数组是“任一权限命中即可显示”(OR)。
- hasPermission 数组是“数组内每个权限都必须存在”(AND)。
- permissions 含星号时视为全部权限。
多数业务调用只传单个权限,因此差异暂时不明显;复用时不要把多权限数组在两处互换。
## 4. 业务页面审计
### 4.1 接诊台
源码:
- D:\web\zyt\admin\src\views\patient\reception\index.vue
- D:\web\zyt\admin\src\api\patient.ts
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientInfoCard.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientCaseCard.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\DailyMatrix.vue
- D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue
页面行为:
- 默认显示当天 status=1 待接诊;可切到 status=4 已过号。
- 搜索字段 patient_name;分页 page_no/page_size,固定每页 15。
- 每 5 秒静默刷新队列和已选患者详情;页面隐藏时暂停,恢复可见后立即刷新。
- 队列使用无限滚动,并按 id 去重。
- 队列行主要字段:id、patient_id、patient_name、patient_phone、diagnosis_id、doctor_id/name、assistant_id/name、appointment_date/time、gender、age、status/status_desc、has_prescription、remark。
- 详情结构按源码使用为:
- appointment:挂号;
- diagnosis:诊单/病例;
- doctor_notes:医生备注、舌苔和报告。
- 日常记录不依赖 reception 响应完整下发,而由 DailyMatrix 继续按 diagnosis_id 调 trackingWindow/trackingNotes。
操作:
- 通知医助:POST /doctor.appointment/notifyAssistant,参数 id=挂号ID。
- 发起通话:先 POST /tcm.diagnosis/getCallSignature,参数 patient_id、diagnosis_id,再打开 ChatDialog。
- 备注:POST /doctor.appointment/addDoctorNote,参数 diagnosis_id、content,可追加 tongue_images、report_files。
- 编辑病历:复用 tcm/diagnosis/edit.vue。
- 完成接诊:POST /doctor.appointment/complete,参数 id=挂号ID;页面允许 status=1 或 4。
权限:
- doctor.appointment/addDoctorNote
- tcm.diagnosis/edit
- doctor.appointment/complete
源码中的“通知医助”和“发起通话”按钮没有 v-perms;只能依赖页面菜单权限和后端接口鉴权,桌面端若拆成独立入口应补显式能力判断。
### 4.2 我的处方库
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\list.vue
- D:\web\zyt\admin\src\api\tcm.ts
- D:\web\zyt\admin\src\components\medicine-name-select\index.vue
模型与筛选:
- 查询:prescription_name、formula_type(主方/辅方)、is_public(0 仅自己、1 所有人)。
- 列表:id、prescription_name、formula_type、herbs、is_public、disable_edit、creator_id/name、create_time。
- herbs 项:medicine_id(可选)、name、dosage。
- 编辑:id、prescription_name、formula_type、herbs、is_public、disable_edit。
- disable_edit=1 表示导入模板后锁定整张处方的药材,不可增删改,只能再次导入覆盖。
接口:
- GET /tcm.prescriptionLibrary/lists
- POST /tcm.prescriptionLibrary/add
- POST /tcm.prescriptionLibrary/edit
- POST /tcm.prescriptionLibrary/delete,参数 id
- GET /tcm.prescriptionLibrary/detail,参数 id(API 已封装,但当前列表弹窗直接使用行数据)
权限:
- wcf.prescription/add
- wcf.prescription/read
- wcf.prescription/edit
- wcf.prescription/delete
所有权:
- 普通用户只可编辑/删除 creator_id 等于当前 userInfo.id 的模板。
- root=1 或 role_ids 包含 0、3 可管理全部模板。
- 诊间/已开处方导入模板时会额外传 prescribing_creator_id,通常取处方 creator_id,新增时取当前登录用户 id。
### 4.3 药品库(容易和处方库混淆)
源码:D:\web\zyt\admin\src\views\doctor\medicine.vue
接口:
- GET /doctor.medicine/lists
- POST /doctor.medicine/add
- POST /doctor.medicine/edit
- POST /doctor.medicine/delete
- GET /doctor.medicine/detail
模型:
- id、name、supplier、unit、settlement_price、retail_price、stock、image、status、remark。
- 图片上传直接 POST 到 VITE_APP_BASE_URL + /api/upload/image,并携带 token 头。
当前页面的增删改按钮没有 v-perms。它是药材主数据管理,不应直接当作“我的处方库”复刻。
### 4.4 已开处方/处方管理
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\index.vue
- D:\web\zyt\admin\src\components\tcm-prescription\index.vue
- D:\web\zyt\admin\src\api\tcm.ts
列表筛选:
- sn:处方编号模糊查;
- patient_name
- creator_ids:开方医师多选;
- audit_filterall、pending、passed、not_passed、rejected
- source_filterall、manual、system
- start_time、end_time(按创建时间)。
列表核心字段:
- id、sn、prescription_type
- is_system_auto0 手工、1 空白处方/系统代开;
- patient_name、gender、age、phone
- audit_status、audit_remark、business_prescription_audit_rejected、business_prescription_audit_remark
- void_status、void_by_name、void_time
- doctor_name、creator_id、assistant_name、prescription_date、create_time
- has_prescription_order。
处方编辑/详情模型:
- 关联:id、diagnosis_id、creator_id。
- 患者:patient_name、gender、age、visit_no、prescription_date。
- 诊断:tongue、tongue_image、pulse、pulse_condition、clinical_diagnosis。
- 药材:herbs,每项 medicine_id、name、dosage、formula_type(主方/辅方)、locked。
- 剂型/用法:prescription_type、dosage_amount、dosage_unit、dosage_bag_count、need_decoction、bags_per_dose、dose_count、dose_unit、usage_days、times_per_day、usage_instruction、usage_time、usage_way、dietary_taboo、usage_notes。
- 辅方用法 aux_usagedosage_amount、dosage_bag_count、need_decoction、bags_per_dose、times_per_day、usage_days、prescription_name(部分页面保留模板名)。
- 医师:doctor_name、doctor_signaturePNG data URL,保存前必填)。
- 可见性/审核:is_shared、visible_role_ids、audit_status、audit_time、audit_by_name、audit_remark。
状态规则:
- audit_status0 待审核、1 已通过、2 已驳回。
- 驳回处方会同时作废。
- “已通过且未作废”的有效处方不能普通编辑/删除。
- 新增时前端强制 audit_status=0;编辑保存后提示重新进入待审核。
- 诊间开方组件会保存 case_record 病历快照,并在已有 appointment_id 处方时直接进入只读查看。
- 已存在业务订单时,诊间组件禁止作废处方。
主要接口:
- GET /tcm.prescription/lists
- GET /tcm.prescription/detail,参数 id
- POST /tcm.prescription/add
- POST /tcm.prescription/edit
- POST /tcm.prescription/delete,参数 id
- POST /tcm.prescription/patchPatient,参数 id、patient_name、phone、gender
- POST /tcm.prescription/audit,参数 id、action=approve|reject、remark
- POST /tcm.prescription/void,参数 id
- GET /tcm.prescription/listByDiagnosis,参数 diagnosis_id
- GET /tcm.prescription/getByAppointment,参数 appointment_id
权限:
- cf.prescription/add、read、edit、audit、del
- tcm.prescription/patchPatient
- tcm.prescriptionLibrary/lists
- tcm.prescriptionOrder/create、lists、setShipMode
- finance.account_log/lists
- tcm.prescriptionOrder/editRemarkExtra
角色补充:消费者处方页将 root 或 role_ids 0、3 视为可审核角色;仍应以后端和 cf.prescription/audit 为最终判定。
### 4.5 处方业务订单
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\order_list.vue
- D:\web\zyt\admin\src\views\consumer\prescription\components\PrescriptionOrderDetailDrawer.vue
- D:\web\zyt\admin\src\views\consumer\prescription\components\prescription-order-utils.ts
此页面是已开处方的相邻履约域。核心字段:
- id、order_no、prescription_id、diagnosis_id
- recipient_name、recipient_phone、region、shipping_address
- fee_type、amount、internal_cost
- prescription_audit_status、payment_slip_audit_status
- fulfillment_status
- linked_pay_order_count、linked_pay_order_id、linked_pay_paid_total
- medication_days、service_channel、service_package
- express_company、tracking_number、ship_mode
- doctor_name、creator_id/name、assistant_id
- remark_extra、remark_assistant
- 药房提交号和状态。
审核状态统一为 0 待审核、1 已通过、2 已驳回。履约状态:
- 1 待双审通过
- 2 待发货
- 3 已完成
- 4 已取消
- 5 已发货
- 6 已签收
- 7 进行中
- 8 暂不制药
- 9 拒收
- 10 退款
- 11 保留药方
- 12 制药缓发
核心接口:
- GET /tcm.prescriptionOrder/lists、detail、paidPayOrders、logs、logisticsTrace、export
- POST /tcm.prescriptionOrder/create、edit、withdraw、ddcode
- POST /tcm.prescriptionOrder/auditPrescription、auditPayment、revokeRxAudit、revokePayAudit
- POST /tcm.prescriptionOrder/ship、complete、refund、requestCompletion
- POST /tcm.prescriptionOrder/addPayOrder、linkPayOrder
- POST /tcm.prescriptionOrder/patchPrescriptionPatient、patchPrescriptionUsage、updateAmount
- POST /tcm.prescriptionOrder/setShipMode、uploadToPharmacy
- POST /tcm.prescriptionOrder/submitGancaoRecipel、previewGancaoRecipel、confirmGancaoSubmission
- POST /tcm.prescriptionOrder/batchAssignAssistant、addLog
主要权限:
- tcm.prescriptionOrder/detail、edit、export、ddcode、ship、addPayOrder、complete、refund、withdraw
- tcm.prescriptionOrder/auditPrescription、auditPayment
- tcm.prescriptionOrder/setShipMode、uploadToPharmacy、editRemarkExtra
- finance.account_log/lists、prescription.order/finance
前端还存在角色级显示规则:
- role 2:医助;
- role 6:下单角色;
- role 3、8:下单筛选豁免;
- role 0、3、6:财务字段;
- role 0、3:可绕过双审后的创建人编辑锁、可批量改派;
- 业务订单处方审核角色在共享工具中为 0、3、6。
这些数字与服务端配置耦合,不宜在新客户端再次散落硬编码。
### 4.6 我的患者
首选医生端实现:
- D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue
- D:\web\zyt\admin\src\api\first_visit.ts
页面结构:
- “患者列表”“订单管理”“面诊进度”三个工作区。
- 筛选 keyword、status_filter、start_date、end_date。
- status_filterunbooked 未预约、pending_interview 待面诊、completed 已完成、missed 已过号。
- 日期快捷:今天、明天、后天、近 7 天、近 30 天、自定义。
- extend.summary 返回 today/tomorrow/day_after 计数。
- extend.dates 返回对应日期。
- extend.scope.label 直接展示后端判定的数据范围。
列表使用字段:
- diagnosis_id 或 id、source_patient_id
- patient_name、gender_desc、age、phone_masked、has_id_card
- assistant_id/name
- appointment_id、appointment_doctor_id/name、appointment_status、appointment_status_text、appointment_time_text
- revisit_count、confirmed、confirmation_text、diagnosis_date_text。
操作与接口:
- GET /firstvisit.myPatient/lists
- GET /firstvisit.myPatient/assistants
- POST /firstvisit.myPatient/assignid、assistant_id、is_inherit=0|1
- POST /firstvisit.myPatient/fillIdCardid、id_card
- POST /firstvisit.myPatient/createAppointment:预约完整参数
- POST /firstvisit.myPatient/cancelAppointmentid=挂号ID
- 订单工作区另使用 /firstvisit.myPatient/orders、orderDetail、orderEdit 及双审/发货/退款等受限代理接口。
- 面诊进度使用 GET /firstvisit.myPatient/progress。
权限:
- tcm.diagnosis/edit
- tcm.diagnosis/readonlyDetail
- tcm.diagnosis/guahao
- tcm.diagnosis/assign
服务端按当前角色与部门范围裁剪数据;前端不自行拼接 doctor_id 或 department_id 来模拟数据权限。
平台用户列表 D:\web\zyt\admin\src\views\consumer\lists\index.vue 使用 GET /user.user/lists,字段是 avatar、nickname、account、mobile、channel、create_time,只适合账号管理,不适合医生患者列表。
### 4.7 问诊/挂号列表
源码:
- D:\web\zyt\admin\src\views\tcm\appointment\list.vue
- D:\web\zyt\admin\src\api\doctor.ts
默认条件:
- status=1 待接诊;
- start_date=end_date=今天;
- date_preset=today
- 20 秒静默轮询;
- include_status_counts=1 时从 extend.status_count 一次返回各状态角标。
状态:
- 1 待接诊/已预约
- 2 已取消
- 3 已完成
- 4 已过号
筛选:
- patient_name、doctor_name
- status
- start_date、end_date、date_preset
- diagnosis_confirmed
- assistant_dept_id(选父部门含子级)。
行字段:
- id、patient_id、diagnosis_id
- patient_name、patient_phone、gender、age、height、weight
- doctor_id/name、assistant_name
- appointment_date、appointment_time、period
- diagnosis_confirmed
- has_prescription、prescription_is_system_auto、prescription_audit_status、prescription_void_status
- status/status_desc、remark。
操作:
- 编辑患者:复用诊单编辑抽屉。
- 视频二维码:生成小程序码。
- 通话:获取签名并打开 ChatDialog。
- 完成:POST /doctor.appointment/complete,可同时 POST addDoctorNote。
- 开方/查看:复用 TcmPrescription;有效已审核处方显示“查看”。
- 取消:POST /doctor.appointment/cancel。
权限:
- tcm.diagnosis/edit
- tcm.diagnosis/videoQr
- doctor.appointment/prescription
- doctor.appointment/complete
- tcm.diagnosis/kaifang
- doctor.appointment/cancel
- doctor.appointment/addDoctorNote(完成时备注能力)
角色判断:role_id 为 1 医生、2 医助;两者之外才显示医生姓名筛选。此处同时兼容 role_id 单值或数组,但其他页面多使用 role_ids,说明 user 模型尚未完全统一。
### 4.8 诊单列表
源码:
- D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue
列表筛选字段:
- keyword、diagnosis_type、syndrome_type、assistant_id
- diagnosis_confirmed、appointment_date、has_appointment
- latest_appointment_start_date/end_date/channel_source
- latest_assign_start_date/end_date
- pending_booking、completed_appointment、pending_assign
- pending_assign_order_month、pending_assign_keyword
- sort_unserved_days。
列表使用字段:
- id、patient_name、gender_desc、age
- assistant_id/assistant、assign_read_at
- appointments 或聚合的 appointment_doctor_name、appointment_time_text、appointment_status
- has_appointment、diagnosis_confirmed
- has_prescription、followup_time_text、followup_doctor_name、followup_rx_voided
- unserved_days、last_blood_record_at
- video_call_hint。
video_call_hint
- statenone、pending_room、live 等;
- label
- start_time、end_time。
操作权限:
- tcm.diagnosis/add、edit、delete、readonlyDetail
- tcm.diagnosis/assign
- tcm.diagnosis/kaifang
- tcm.diagnosis/guahao、guahaoLogList
- tcm.diagnosis/videoQr
- tcm.diagnosis/order
- tcm.diagnosis/watchCall
诊单编辑模型的核心字段:
- 标识:id、patient_id。
- 患者:patient_name、id_card、phone、gender、age、marital_status、height、weight、region。
- 诊断:diagnosis_date、diagnosis_type、syndrome_type、diabetes_type、diabetes_discovery_year、local_hospital_diagnosis、local_hospital_name。
- 指标:systolic_pressure、diastolic_pressure、fasting_blood_sugar。
- 现病史多选:appetite、water_intake、diet_condition、weight_change、body_feeling、sleep_condition、eye_condition、head_feeling、sweat_condition、skin_condition、urine_condition、stool_condition、kidney_condition、fatty_liver_degree。
- 既往史:past_history、trauma_history、surgery_history、allergy_history、family_history、pregnancy_history。
- 医疗内容:symptoms、tongue_coating、pulse、treatment_principle、prescription、doctor_advice、remark、current_medications。
- 归属与来源:assistant_id、status、create_source、show_card、external_userid。
详情响应还使用 patient_basic_locked、can_edit_patient_basic、latest_prescription_order。手机号/身份证是否显示明文由 tcm.diagnosis/phonePlain 控制;已有身份证通常只有明文权限才能修改。
诊单详情 Tab 权限:
- tcm.diagnosis/chufang:处方
- tcm.diagnosis/patientOrders:业务订单
- tcm.diagnosis/huifang:视频回放
- tcm.diagnosis/chat:聊天记录
- tcm.diagnosis/assign 或 detail:指派记录
- doctor.appointment/lists:挂号记录
- tcm.diagnosis/dailyRecord:日常记录
## 5. 视频问诊完整链路
### 5.1 预约
源码:D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue
目前预约类型只有 video。请求字段:
- patient_id
- doctor_id
- appointment_date
- period=all
- appointment_time
- appointment_type=video
- remark
- channel_source
- channel_source_detail
普通入口 POST /doctor.appointment/create;“我的患者”入口 POST /firstvisit.myPatient/createAppointment。可用时段来自 GET /doctor.appointment/availableSlots,字段 doctor_id、appointment_date、period=all,响应使用 slots[].time、slots[].available。页面还读取医生排班并限制不能重复预约当天 status=1/4 的号。
### 5.2 医生发起通话
生产主组件:D:\web\zyt\admin\src\components\chat-dialog\index.vue
1. 页面调用 open({ patientId, patientName, diagnosisId })。
2. POST /tcm.diagnosis/getCallSignature,请求 patient_id、diagnosis_id。
3. 响应实际使用:
- sdkAppId
- userId(医生 IM/TRTC user ID
- userSig
- patientUserId,缺省回退 patient_加患者ID
- assistant_id(群视频邀请)
- isLochostVod(是否启用浏览器本地录制)
4. Chat UIKit 登录并创建与 patientUserId 的 C2C 会话。
5. TUICallKitServer.init 初始化通话能力;只有成功后才显示 AudioCallPicker、VideoCallPicker 和群视频按钮。
6. beforeCalling 时 POST /tcm.diagnosis/startCall
- diagnosis_id
- patient_id
- call_type=2(视频)
7. 呼叫状态进入 calling/connected 后从 TUIStore 或 TUICallEngine 捕获 roomID/strRoomID,再 POST /tcm.diagnosis/bindCallRoom
- diagnosis_id
- room_id(字符串)
8. bindCallRoom 的响应可带 cloud_recording.started、task_id、message;源码注释说明后端在这里触发腾讯云 CreateCloudRecording,混流模式由后端负责。
9. 接通后,若 isLochostVod=true,前端从 TUICallKit 视频元素启动 MediaRecorder/Canvas 本地录制。
10. 通话结束、挂断、IM 自定义挂断消息或用户关闭窗口时先 POST /tcm.diagnosis/endCall,再完成本地视频上传并 POST attachLocalCallRecording。
群视频调用 TUICallKitServer.callsuserIDList=[patientUserId, assistant_id]type=VIDEO_CALL。
### 5.3 截屏、录制与回放
关键文件:
- D:\web\zyt\admin\src\utils\call-local-recorder.ts
- D:\web\zyt\admin\src\utils\call-video-screenshot.ts
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\CallRecordPanel.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\RecordingPlaybackBlock.vue
视频浮窗“截屏”会:
1. 抓取当前 video frame
2. 上传图片;
3. POST /doctor.appointment/addDoctorNote,把路径追加到 tongue_images。
通话记录字段:
- id、call_type1 语音、其他视为视频)
- room_id
- status:1 进行中、2 已结束、3 未接听、4 已取消
- recording_status_text
- recording_urls_list
- start_time_text、end_time_text、duration_text
接口:
- GET /tcm.diagnosis/getCallRecordsdiagnosis_id
- POST /tcm.diagnosis/attachLocalCallRecordingdiagnosis_id、file_url、可选 call_record_id
- POST /tcm.diagnosis/createManualCallRecorddiagnosis_id
- POST /tcm.diagnosis/startCloudRecordingdiagnosis_idAPI 有封装,主组件当前通过 bindCallRoom 的后端联动启动)
### 5.4 医助旁观
入口在诊单列表。只有同时满足:
- 当前 userInfo.id 等于该诊单 assistant_id
- 拥有 tcm.diagnosis/watchCall
- video_call_hint.state=live
才可真正进入。
GET /tcm.diagnosis/watchCall,参数 diagnosis_id,响应使用:
- sdkAppId
- userId
- userSig
- roomId 或 strRoomId
- patientName
旁观组件直接使用 trtc-sdk-v5 进入房间,只调用 startRemoteVideo,不开启本地摄像头或麦克风。pending_room 时显示入口提示但点击会阻止进入,等待房间号同步。
### 5.5 视频二维码
页面通过 POST /tcm.diagnosis/generateMiniProgramQrcode 生成 qrcode_url,常用字段:
- diagnosis_id
- patient_id
- doctor_id(部分入口)
- share_user_id
- mini_program_path=pages/login/login(问诊列表入口)
调用前先 GET 小程序配置并校验 app_id。
## 6. API 总表
所有 URL 会被请求层加上 baseUrl 和 adminapi 前缀;下表写的是 API 封装中的业务路径。
### 6.1 认证
| 方法 | 路径 | 关键请求/响应 |
|---|---|---|
| POST | /login/account | account、password、terminal;返回 token、is_paw、need_bind_work_wechat |
| POST | /login/workWechatLogin | code、terminal;返回同登录结果 |
| GET | /login/workWechatConfig | enabled、corp_id、agent_id |
| GET | /auth.admin/mySelf | 返回 user、permissions、menu |
| POST | /login/logout | 退出 |
| POST | /login/changeFirstPassword | password、password_confirm |
| POST | /auth.admin/bindWorkWechat | code |
### 6.2 挂号与接诊
| 方法 | 路径 | 关键字段 |
|---|---|---|
| GET | /doctor.appointment/lists | status、start_date、end_date、patient_name、doctor_name、diagnosis_confirmed、assistant_dept_id、page_no、page_size |
| GET | /doctor.appointment/reception | id=挂号ID;返回 appointment、diagnosis、doctor_notes |
| GET | /doctor.appointment/detail | id |
| GET | /doctor.appointment/availableSlots | doctor_id、appointment_date、period |
| POST | /doctor.appointment/create | patient_id、doctor_id、appointment_date/time、appointment_type、渠道等 |
| POST | /doctor.appointment/cancel | id |
| POST | /doctor.appointment/complete | id |
| POST | /doctor.appointment/notifyAssistant | id |
| POST | /doctor.appointment/addDoctorNote | diagnosis_id、content、tongue_images、report_files |
| GET | /doctor.appointment/doctorNotes | diagnosis_id |
| POST | /doctor.appointment/deleteDoctorNoteImage | note_id、image_type、image_path |
### 6.3 诊单
| 方法 | 路径 | 关键字段 |
|---|---|---|
| GET | /tcm.diagnosis/lists | 诊单列表全部筛选 + page_no/page_size;返回 lists/count/extend |
| GET | /tcm.diagnosis/detail | id |
| GET | /tcm.diagnosis/readonlyDetail | id;返回 appointment、diagnosis、unserved_days、last_blood_record_at、doctor_notes |
| POST | /tcm.diagnosis/add | 完整诊单模型 |
| POST | /tcm.diagnosis/edit | 完整诊单模型 |
| POST | /tcm.diagnosis/delete | id |
| POST | /tcm.diagnosis/assign | id、assistant_id、可选 is_inherit;批量场景由前端逐条调用 |
| GET | /tcm.diagnosis/getAssistants | 医助选项 |
| GET | /tcm.diagnosis/getDoctors | 医生选项 |
| POST | /tcm.diagnosis/checkPhone | phone 及排除 id |
| POST | /tcm.diagnosis/checkIdCard | id_card 及排除 id |
| POST | /tcm.diagnosis/fillIdCard | id、id_card |
| GET | /tcm.diagnosis/trackingWindow | id、start_date、end_date |
| GET | /tcm.diagnosis/trackingNotes | diagnosis_id |
| POST | /tcm.diagnosis/addTrackingNote | diagnosis_id、tracking_content |
### 6.4 处方与业务订单
处方、处方库、业务订单接口已在 4.2、4.4、4.5 分节完整列出。实现时必须保留三个资源命名空间,不要把 prescription、prescriptionLibrary、prescriptionOrder 合并成一个“处方”接口。
### 6.5 视频
| 方法 | 路径 | 关键字段 |
|---|---|---|
| POST | /tcm.diagnosis/getCallSignature | patient_id、diagnosis_id;返回 sdkAppId、userId、userSig、patientUserId、assistant_id、isLochostVod |
| POST | /tcm.diagnosis/startCall | diagnosis_id、patient_id、call_type |
| POST | /tcm.diagnosis/bindCallRoom | diagnosis_id、room_id |
| POST | /tcm.diagnosis/startCloudRecording | diagnosis_id |
| POST | /tcm.diagnosis/endCall | diagnosis_id |
| GET | /tcm.diagnosis/getCallRecords | diagnosis_id |
| POST | /tcm.diagnosis/attachLocalCallRecording | diagnosis_id、file_url、可选 call_record_id |
| POST | /tcm.diagnosis/createManualCallRecord | diagnosis_id |
| GET | /tcm.diagnosis/watchCall | diagnosis_id |
| POST | /tcm.diagnosis/generateMiniProgramQrcode | diagnosis_id、patient_id、doctor_id、share_user_id 等 |
## 7. 可复用约定
1. 请求协议
- baseURL 来自 VITE_APP_BASE_URL,统一 URL 前缀 adminapi。
- token 放在名为 token 的请求头,不是 Bearer Authorization。
- POST 默认把 params 转为 bodyGET 使用 params。
- 标准成功响应是 code=1,业务数据自动解包为 data。
- GET 网络失败默认最多重试 2 次,POST 不自动重试。
2. 列表协议
- 请求 page_no、page_size。
- 响应 lists、count、extend。
- 定时刷新使用 getLists({ silent: true }),避免表格白屏闪烁。
3. 标识约定
- diagnosis_id 是诊单主键。
- appointment.id 是挂号主键。
- prescription.id 是处方主键。
- prescriptionOrder.id 是处方业务订单主键。
- patientUserId 是腾讯云 IM/TRTC 用户名,通常 patient_加患者标识。
4. 隐私
- 默认手机号 3-4-4 脱敏,身份证保留前 6 后 4。
- tcm.diagnosis/phonePlain 控制诊单编辑中的明文能力。
- 数据范围由后端按角色、部门、归属医助裁剪,前端只做 UI 能力门控。
5. 复用组件
- 患者摘要/病例:PatientInfoCard、PatientCaseCard。
- 日常记录:DailyMatrix。
- 医生备注:NoteTimeline。
- 诊单编辑和只读:tcm/diagnosis/edit.vue 的 open、openViewOnly。
- 处方开立/查看:TcmPrescription 的 open、openById。
- 视频通讯:ChatDialog 的 open。
- 业务订单详情:PrescriptionOrderDetailDrawer。
6. 状态文本
- 不建议在新客户端重复定义状态映射。优先抽取 D:\web\zyt\admin\src\views\consumer\prescription\components\prescription-order-utils.ts 中的审核、履约、支付、供货和物流格式化逻辑为共享领域模块。
## 8. 未知点、歧义与风险
1. 动态菜单缺口:admin 前端仓库没有生产环境 /auth.admin/mySelf 的 menu 数据,因此接诊台、处方库、患者、问诊列表的精确 URL、菜单标题和页面级 route.meta.perms 仍未知。
2. API 类型不足:tcm.ts、doctor.ts 多数参数和返回值是 any;本文列出的响应字段来自实际页面读取,不等于完整服务端 schema。后续实现应抓取真实响应或检查服务端 DTO。
3. patient_id 语义有重载:
- 挂号行中 patient_id 常被当作诊单/患者标识;
- 我的患者 openAppointment 又把 diagnosis_id 或 id 同时写进 id 和 patient_id
- ChatDialog 则把它转换为 patient_前缀的腾讯云用户。
新客户端必须先确认数据库实体关系,不能只按字段名推断。
4. 接诊台发起通话使用 diagnosis_id || row.idrow.id 本身是挂号 ID。若后端要求真正诊单 ID,这个回退可能只在特定历史数据下成立。
5. 视频二维码参数疑点:tcm/appointment/list.vue 的一个入口把 diagnosis_id 赋为 row.doctor_id,而其他入口使用真正诊单 ID;这很可能是历史兼容或缺陷,应向后端核实后再复用。
6. 权限命名不统一:
- 处方库用 wcf.prescription/*
- 已开处方用 cf.prescription/*
- 新增能力又混用 tcm.prescription/* 与 tcm.prescriptionOrder/*。
不能按字符串前缀自动推导资源。
7. 角色配置存在差异:
- 消费者处方审核页面写死 0、3
- 业务订单共享工具写死 0、3、6
- 多处注释都声称与服务端配置一致。
最终角色应由服务端下发 capability,避免继续硬编码。
8. 权限 UI 不是安全边界:部分接诊、药品和通话按钮没有 v-perms;所有写接口必须继续依赖服务端鉴权。
9. 双 SDK 并存:
- 实际 ChatDialog 使用 @tencentcloud/call-uikit-vue
- 未引用的 video-call/index.vue 使用 @trtc/calls-uikit-vue
- 医助旁观直接使用 trtc-sdk-v5。
新项目应明确只保留一套主叫/被叫 UI SDK,并将纯 TRTC 旁观作为独立只拉流能力。
10. src/components/video-call/index.vue 当前没有被任何 Vue/TS 源码引用,不应误认为生产主链。
11. PatientCaseCard 的 caseTypeLabel 无论 consultation_type 都返回“复诊”,属于明显展示逻辑疑点。
12. 录制启动/停止部分依赖 TUICallKit 内部 store、引擎属性和方法包装,升级腾讯云 SDK 时风险较高,必须用真实双端通话、拒接、对端挂断、网络中断和房间号延迟场景回归。
13. 处方业务订单大量前端角色规则与 server/config/project.php 注释耦合;当前审计范围只有 admin 前端,无法验证服务端配置是否已同步。
## 9. 面向新医生端的建议映射
若新项目要复刻医生工作流,建议按领域而不是按现有目录命名:
- /login:复用认证协议和企业微信登录。
- /reception:复用接诊台队列、详情、5 秒静默刷新与 ChatDialog。
- /prescription-library:复用 prescriptionLibrary 模板及所有权规则。
- /prescriptions:复用 tcm.prescription 列表、审核、作废、打印/下载。
- /patients:优先复用 firstvisit.myPatient,而不是 user.user/lists。
- /consultations:复用 doctor.appointment/lists 的今天待接诊视图。
- /diagnoses:复用 tcm.diagnosis/lists 的完整诊单工作台。
- /video-consultation:主叫链路复用 ChatDialog/TUICallKit;医助旁观保持独立 TRTC 只拉流组件。
这些建议 URL 是新端的信息架构建议,不是对 admin 当前动态 URL 的断言。
+461
View File
@@ -0,0 +1,461 @@
# 医生桌面端工程架构与打包方案(Windows / macOS
> 结论先行:采用 **Python 3.12 + PySide6 Qt Widgets** 构建原生业务界面,以分层的 `httpx` API client 连接现有后端;会话、权限、离线队列和本地安全存储统一放在 core 层。视频不是整套应用的实现基础,而是独立的可选集成:只有当现有腾讯 TRTC Web 方案无法由原生 SDK 替代时,才在受限的 `QWebEngineView` 中承载单一视频页面。发布使用 **PyInstaller onedir**Windows 与 macOS 必须在各自原生 CI runner 上分别构建、签名和验收,不能交叉编译。
## 1. 已知上下文、边界与待确认项
本结论只对 `admin/package.json` 和环境配置做了最小只读核对,没有审计管理端实现。
- 管理端以 `VITE_APP_BASE_URL` 注入后端根地址;示例文件故意留空,开发示例注释仅以 `http://127.0.0.1:8080` 举例。现有环境文件还出现了 `https://css.zhenyangtang.com.cn/``https://admin.zhenyangtang.com.cn/` 和 60 秒请求超时,但这些地址可能是网关或前端站点,**不能据此认定为稳定的桌面 API 地址**。
- 管理端依赖包含 Axios、腾讯 TRTC/Call/Chat UI、`hls.js` 和 COS JS SDK。可以据此判断视频、聊天、流媒体和对象存储是潜在集成面,但不能推断接口路径、认证协议、权限码或 RTC 凭证格式。
- 桌面端不得读取或复用 Vite 环境变量,不得硬编码管理端 URL,也不得在客户端持有 COS Secret、TRTC SecretKey 或任何服务端签名密钥。
编码前必须由后端确认以下契约,并固化为 OpenAPI 或最小接口文档:
1. API 的正式 base URL、版本前缀、响应 envelope、错误码、分页和时间格式。
2. 登录协议(账号密码、短信、SSO/OIDC 或 Cookie)、access/refresh 生命周期、登出和吊销语义。
3. `/me` 等当前用户接口返回的医生身份、机构/租户、角色与细粒度权限码。
4. 预约、患者、病历、处方等写操作的幂等键、乐观锁版本号及审计要求。
5. 聊天的拉取/推送协议、断线续传游标;COS 上传应由后端提供短期预签名 URL 或临时凭证。
6. TRTC 房间、`userSig` 等凭证必须由后端短时签发;确认现有 Web 页面能否作为受支持的嵌入入口。
7. 桌面端的 CORS、代理、私有 CA、设备绑定、强制升级和最低版本策略。
在这些问题确认前可以完成壳层、接口抽象和模拟服务器,但不应猜测生产 endpoint。
## 2. 目标平台与技术选择
### 2.1 建议支持矩阵
| 项目 | 首发建议 | 说明 |
| --- | --- | --- |
| Python | CPython 3.12,固定 patch 版本 | 生命周期长,第三方包成熟;每个平台使用相同 minor |
| Windows | Windows 10 22H2 / Windows 11x86-64 | ARM64 可作为后续独立制品,不与 x64 混装 |
| macOS | macOS 13+,先 arm64,再按客户量增加 x86-64 | 当前 PySide6 wheel 的最低系统版本必须在锁版本时再次核对 |
| UI | PySide6 Qt Widgets | 医疗表单、表格、快捷键、打印和可访问性更稳定 |
| 视频 | 可选 PySide6 Addons / QtWebEngineWidgets | 只隔离承载视频页,不用 WebEngine 包住整个应用 |
| 打包 | PyInstaller onedir | 对 QtWebEngine helper、资源、签名和启动性能最稳妥 |
macOS 推荐分别产出 `arm64``x86_64` 制品。`universal2` 只有在 Python、PySide6 和所有二进制依赖均提供 universal2 slice,且真实验证签名/视频后再启用;两个单架构制品更易排障且体积更小。
### 2.2 依赖分档
建立一份代码、两种构建 profile:
- `core``PySide6-Essentials``httpx``pydantic``pydantic-settings``platformdirs``keyring``cryptography`。包含 QtCore/Gui/Widgets/Network/Sql/Svg/PrintSupport,不包含 WebEngine。
- `video`:在 core 上增加与 Essentials **完全相同版本**的 `PySide6-Addons`,从而获得 QtWebEngineWidgets、WebChannel、Multimedia 等模块。
- 开发/测试:`pytest``pytest-qt``respx``coverage``ruff``mypy``pip-audit`
- 构建:锁定 `PyInstaller` 及其 hooks 版本。以 2026-08-10 可验证组合为基线,可先验证 Python 3.12 + PySide6 Essentials/Addons 6.11.1 + PyInstaller 6.21.0;只有在两端打包 smoke test 通过后才更新锁。
不要同时安装 PyQt、PySide2 或系统级 PySide6;必须从干净虚拟环境构建。使用平台专属、带 hash 的锁文件(例如 `requirements-win-x64.lock``requirements-macos-arm64.lock`),而不是在发布任务中直接安装“最新版”。
如果所有医生都需要视频,可只发布 `video` 制品;仍保留 profile 边界,以便定位 WebEngine 问题。若视频是少数场景,可以发布 core 制品并在系统浏览器打开受支持的视频页,避免让每个安装包承担 Chromium 的体积和攻击面。
## 3. 分层架构
依赖方向固定为:`ui -> application -> domain``infrastructure` 在 composition root 中实现 domain/application 定义的 port。View 不允许直接调用 `httpx`、SQLite 或 keyring。
```text
app/
├─ pyproject.toml # 项目元数据、依赖分组、工具配置
├─ requirements/ # 各 OS/架构的发布锁及 hash
├─ src/
│ └─ zyt_doctor/
│ ├─ __main__.py # 极薄入口,只调用 bootstrap.main()
│ ├─ bootstrap.py # QApplication、配置、DI、异常钩子、主窗口
│ ├─ build_info.py # 版本、commit、channel;构建时生成
│ ├─ config/
│ │ ├─ models.py # 强类型配置及校验
│ │ └─ loader.py # defaults -> 受管配置 -> 开发环境变量
│ ├─ domain/
│ │ ├─ identity.py # Principal、Tenant、Permission
│ │ ├─ errors.py # 与 UI/HTTP 无关的错误类型
│ │ └─ ports.py # Repository、Clock、SecretStore 等协议
│ ├─ application/
│ │ ├─ commands.py # 写用例、幂等键和确认规则
│ │ ├─ queries.py # 读用例和缓存策略
│ │ └─ result.py # Result / Page / OperationState
│ ├─ core/
│ │ ├─ api/
│ │ │ ├─ client.py # httpx client、header、超时、重试
│ │ │ ├─ auth.py # token/cookie adapter 与 refresh single-flight
│ │ │ ├─ errors.py # HTTP/业务错误归一化
│ │ │ ├─ models.py # 公共 DTO
│ │ │ └─ generated/ # 若有 OpenAPI,生成代码仅放此处
│ │ ├─ session/
│ │ │ ├─ manager.py # 会话状态机、锁屏、租户切换、登出清理
│ │ │ └─ permissions.py # 权限快照与 guard
│ │ ├─ storage/
│ │ │ ├─ database.py # SQLite、migration、单写线程
│ │ │ ├─ secure_store.py # Windows Credential Manager / macOS Keychain
│ │ │ └─ cache.py # 加密缓存、TTL、容量控制
│ │ ├─ offline/
│ │ │ ├─ connectivity.py # 网络状态提示,不作为唯一真相
│ │ │ ├─ outbox.py # 离线写队列状态机
│ │ │ └─ sync.py # 重放、冲突和人工处理
│ │ ├─ jobs.py # QThreadPool 任务、取消、signal 适配
│ │ ├─ events.py # 进程内 typed event bus
│ │ ├─ logging.py # 脱敏日志和诊断包
│ │ └─ paths.py # QStandardPaths/platformdirs;绝不写 bundle
│ ├─ modules/
│ │ ├─ auth/
│ │ ├─ dashboard/
│ │ ├─ patients/
│ │ ├─ appointments/
│ │ ├─ consultations/
│ │ ├─ medical_records/
│ │ ├─ prescriptions/
│ │ ├─ chat/
│ │ ├─ followups/
│ │ └─ settings/
│ │ # 每个模块内含 domain.py、service.py、viewmodel.py、views.py、permissions.py
│ ├─ integrations/
│ │ ├─ realtime/ # WebSocket/轮询 adapter,不侵入模块
│ │ ├─ object_storage/ # 仅消费后端预签名 URL/临时凭证
│ │ └─ video/
│ │ ├─ port.py # join/leave/mute 等抽象
│ │ ├─ external_browser.py
│ │ └─ webengine.py # 唯一允许 import QtWebEngine 的文件
│ ├─ ui/
│ │ ├─ shell/ # 导航、标题栏、全局离线/会话提示
│ │ ├─ widgets/ # Loading、Empty、Error、PermissionDenied
│ │ ├─ dialogs/
│ │ └─ theme/
│ └─ resources/ # qrc、图标、字体许可、翻译、默认配置
├─ tests/
│ ├─ unit/
│ ├─ contract/
│ ├─ integration/
│ ├─ ui/
│ ├─ packaging/
│ └─ fixtures/ # 全部为合成数据,禁止生产病患数据
├─ packaging/
│ ├─ windows/doctor-core.spec
│ ├─ windows/doctor-video.spec
│ ├─ macos/doctor-core.spec
│ ├─ macos/doctor-video.spec
│ ├─ macos/entitlements.plist
│ └─ hooks/
├─ scripts/ # build、self-check、sign、notarize
└─ docs/ # API 映射、权限矩阵、发布 runbook
```
每个业务模块只公开一个 facade 和路由描述,例如 `ModuleDescriptor(id, title, permissions, view_factory)`。主壳根据权限注册菜单,模块内再对按钮和 command 做 guard;这样既不会形成一个巨型主窗口,也不会把权限判断散落在控件代码中。
## 4. API client、并发和会话
### 4.1 API client
建议使用一个长生命周期 `httpx.Client`,由 composition root 创建并注入 service。所有同步请求放入 `QThreadPool/QRunnable`,结果通过 Qt signal 回到主线程;严禁 UI 线程阻塞网络。窗口关闭或查询条件变化时取消尚未开始的任务,并忽略带旧 generation id 的晚到响应。
Client 的固定行为:
- production base URL 只允许 HTTPSHTTP 仅在 debug profile 且 host 为 localhost 时允许。
- production 允许的 API、视频和上传 host 必须来自签名/受管配置或内置 allowlist,避免篡改本地配置后窃取 token。
- 超时拆分为 connect/read/write/pool,不只设一个总数。可从 connect 10 秒、read 60 秒起步,上传/导出另设长超时。
- 每次请求加入 `Authorization`(若契约采用 bearer)、`X-Request-ID`、客户端版本、平台、时区和租户信息;不得写入日志的 header 列表默认包含 Authorization、Cookie 和所有临时凭证。
- GET/HEAD 和带服务端认可幂等键的写操作,才可对连接错误、超时、429、502、503、504 做指数退避 + jitter;遵守 `Retry-After`。验证错误、普通 4xx 和未知写请求不自动重试。
- 所有关键 mutation 生成并持久化 `Idempotency-Key`,直到收到确定结果;请求超时后的状态为“结果未知”,先按键查询/重放,不能直接再创建一条。
- 业务错误映射为稳定类型:`ValidationError``Unauthenticated``Forbidden``Conflict``RateLimited``Maintenance``TransportError``UnknownServerError`。UI 不解析后端文案。
- 支持 ETag/版本字段做乐观锁。收到 409/412 时进入冲突页,展示服务器版本与本地草稿,不做静默覆盖。
- 下载/上传流式处理并限制文件大小、MIME 和保存目录。COS 只使用后端签发的预签名 URL或短期临时凭证,绝不打包永久密钥。
若后端有 OpenAPI,生成的 models/client 放入 `core/api/generated`,外面再包一层业务 adapter;模块不得直接依赖生成器的数据结构。若无 OpenAPI,先写小而明确的 typed endpoint,不做一个接受任意 path/dict 的“万能客户端”。
实时聊天使用独立 adapter:WebSocket 可运行在一个后台 asyncio loop/thread 中,通过 signal 投递事件;实现心跳、指数重连、服务器 sequence/cursor 补拉、重复消息去重和应用休眠恢复。首版若后端没有可靠续传契约,应采用短轮询而不是假装 WebSocket 永不丢消息。
### 4.2 Session 状态机
`SessionManager` 是唯一会话真相,显式状态为:
```text
SIGNED_OUT -> AUTHENTICATING -> AUTHENTICATED
AUTHENTICATED -> REFRESHING -> AUTHENTICATED
AUTHENTICATED/REFRESHING -> LOCKED | EXPIRED | SIGNED_OUT
```
- access token 只保存在内存;需要“保持登录”时,refresh token 或可续期凭证保存在 OS keychain,不能放在 QSettings、SQLite 明文或日志。
- `keyring` 启动时必须检查实际 backend。没有 Windows Credential Manager/macOS Keychain 等安全 backend 时禁用持久登录,而不是退化到明文文件。
- 多请求同时遇到 401 时只能有一个 refresh 在飞行,其他请求等待同一个 future;refresh 失败统一切到 `EXPIRED`,避免 401 风暴。
- 登出、切换医生或切换租户时:取消网络任务、停止实时连接、退出视频、清内存 token、清空 WebEngine profile、关闭并按用户/租户清理本地缓存密钥。
- 支持工作站空闲自动锁定。解锁方式由后端安全策略决定;锁定界面不得继续显示患者姓名、通知正文或缩略图。
-`QLocalServer/QLocalSocket` 实现单实例,第二次启动只唤醒现有窗口,避免同一用户同时运行两个 outbox。
### 4.3 权限模型
权限码由后端返回并作为服务端授权的镜像,例如 `patient.read``record.write``prescription.sign`;具体字符串必须以真实契约为准。
客户端执行三层防误操作:
1. 路由层:无模块权限时不注册菜单/路由。
2. ViewModel/command 层:按钮显示与执行前都检查 `PermissionGuard.require(...)`
3. API 层:403 统一转为 `Forbidden`,刷新权限快照并提示“权限已变更”。
这些仅改善体验,真正的 RBAC/ABAC、租户隔离和审计必须由后端再次校验。不能因为客户端隐藏了按钮就省略服务端授权。对开方、签名、删除等高风险操作增加 step-up authentication 或明确二次确认,并把 request id/idempotency key 传给服务端审计。
## 5. 本地数据、离线与错误态
### 5.1 数据目录与加密
使用 `QStandardPaths``platformdirs` 获取每用户目录:Windows 通常位于 `%LOCALAPPDATA%`macOS 位于 `~/Library/Application Support`。安装目录和 `.app` bundle 始终只读;业务代码不要访问 `sys._MEIPASS`,资源通过 `importlib.resources`/Qt resource system 读取。
SQLite 使用 WAL、schema migration 和单写入 worker(或每线程独立 connection),不在线程间共享 `sqlite3.Connection`。默认只缓存必要元数据;如果确需缓存患者/病历或离线草稿:
- 使用 `cryptography` 的 AES-GCM 做版本化记录加密,随机 nonce,密钥由 OS keychain 保存;AAD 包含 tenant/user/table/record id,防止记录调包。
- outbox、cache 和密钥按机构 + 用户分区;退出账号做 crypto-erasure(删除密钥)并清理索引。SSD 上不能承诺可靠覆盖删除,因此不能用“反复覆盖文件”作为安全保证。
- 设置缓存 TTL、容量上限和最少字段;搜索索引不放诊断正文等敏感内容。
- QSettings 只保存主题、窗口大小等无敏感偏好。
- 日志不记录患者姓名、手机号、证件号、病历正文、处方内容、token、Cookie 或 URL query;提供用户确认后的脱敏诊断包。
### 5.2 离线策略
不要只依赖系统“在线/离线”事件;真正状态以最近请求结果和轻量 health check 综合判定。主壳常驻显示 `在线 / 网络不稳定 / 离线 / 服务维护`,且标明数据最后更新时间。
写操作按风险分类:
| 类别 | 离线行为 | 恢复后 |
| --- | --- | --- |
| 只读列表/详情 | 展示有时间戳的加密缓存,明显标记“可能已过期” | 后台重新验证并原子替换 |
| 普通草稿、低风险备注 | 可进入 outbox,保存幂等键、base version 和依赖 | 自动重放;冲突转人工处理 |
| 病历最终提交、开方/签方、医嘱、删除等高风险操作 | 只允许保存为本地草稿,禁止假显示“已提交” | 恢复网络后重新拉取服务端版本,由医生确认再提交 |
| 视频/实时聊天 | 显示不可用或重连,不能伪造发送成功 | 按 cursor 补拉并去重 |
Outbox 状态至少包含 `QUEUED -> SENDING -> SUCCEEDED`,以及 `NEEDS_ATTENTION``DEAD_LETTER`。保存 payload schema version、创建人/租户、幂等键、重试次数、next attempt、最后错误和服务端 base version。只自动重放白名单 action;切换用户时绝不重放前一用户队列。
### 5.3 统一错误体验
所有页面复用下列状态组件,而不是把异常 traceback 或后端原文弹给用户:
- 首次加载 skeleton;刷新时保留旧数据并显示非阻塞进度。
- 真空数据(业务上没有记录)与加载失败严格区分。
- 离线且有缓存、离线且无缓存、权限不足、登录过期、字段校验、版本冲突、维护中、未知错误各有独立文案和可行动按钮。
- 未知错误显示 request id、发生时间、“重试/复制诊断编号”,详细堆栈只进入脱敏日志。
- 全局未捕获异常写入 rotating log 并打开安全错误页;不要自动上传包含医疗数据的 crash dump。
## 6. 视频与 QtWebEngine 的可执行边界
### 6.1 首选集成顺序
1. 先确认腾讯或现有供应商是否提供受支持的 Windows/macOS 原生桌面 SDK 以及 Python 可调用层。如果维护成本可接受,原生 adapter 最可控。
2. 若现有成熟能力是 TRTC Web 页面,后端提供一个专用、窄功能、HTTPS 的 `/desktop-call` 类入口(实际路径待定),由 `QWebEngineView` 嵌入。
3. 若 SDK/UA/DRM/屏幕共享在 Qt Chromium 中不受支持,可靠回退是系统默认浏览器,不通过修改 UA 或关闭浏览器安全策略强行兼容。
不要把整套 admin 嵌入桌面壳。JS SDK 的版本和构建产物留在专用 Web 页面侧,Python 只持有 `VideoPort(join, leave, mute, device_changed)` 抽象,这样管理端升级 TRTC SDK 不要求桌面二进制同步发版。
### 6.2 安全桥接
- 桌面从后端申请一次性、短有效期的 call ticketWeb 页面再用 ticket 换房间凭证。禁止把 access token、`userSig` 长期放在 URL、日志或 localStorage。
- 使用专用 `QWebEngineProfile`。优先 off-the-record;若必须持久化设备选择,也只能保存无认证数据。通话结束时清 Cookie、HTTP cache、permissions 和页面内容。
- 导航只允许精确的 HTTPS origin/path allowlist;拦截新窗口、任意下载、`file://`、未知 scheme、跨域跳转和证书错误。证书错误 fail closed。
- 相机/麦克风权限只对当前 allowlisted 通话 origin、活跃通话和明确用户操作放行,结束即撤销。屏幕共享另做显式确认。
- 若使用 Qt WebChannelbridge 只暴露少量 typed method/signal,不暴露文件系统、shell、通用 HTTP client、token getter 或任意 Python 调用。每次调用再次校验当前 page origin 和 session/call id。
- release 不启用 remote debugging,不设置 `QTWEBENGINE_DISABLE_SANDBOX=1`,不使用 `--no-sandbox`。应用也不以管理员/root 身份运行。
### 6.3 兼容性与体积现实
`PySide6` 顶层 wheel 会同时拉入 Essentials 和 Addonscore profile 应直接依赖 `PySide6-Essentials`。WebEngine 位于 Addonswheel 和最终制品都会显著增大,不能把它当成“小插件”。最终体积以两端产物为准,不承诺一个固定数字。
QtWebEngine 使用多进程 Chromium,发布物必须保留:
- `QtWebEngineProcess` helper
- QtWebEngineCore/Widgets 库与所需平台插件;
- `qtwebengine_resources*.pak``icudtl.dat`、V8 snapshot
- `qtwebengine_locales`(至少完整验证 `zh-CN``en-US` 后才可裁剪);
- macOS framework/helper 的 bundle 结构和 entitlements。
H.264/AAC/MP3 等专有 codec 是否可用取决于 QtWebEngine 构建与许可,不能因为 `hls.js` 存在就假设一定能播放。首发验收必须覆盖真实 TRTC/WebRTC、摄像头、麦克风、扬声器切换、屏幕共享(若需求存在)、HLS/录播格式和弱网;若要自行构建启用 proprietary codecs,先完成专利/分发许可评审。
## 7. PyInstaller 构建与发布
### 7.1 为什么固定 onedir
虽然 PyInstaller 支持 onefile,但本项目默认 `onedir`
- onefile 每次启动需解压大体积 Chromium,冷启动慢,易触发杀软且临时目录空间不可控;
- WebEngine 是多进程,helper、framework、资源和 macOS 签名/沙箱都依赖正确目录结构;
- onedir 更容易做增量诊断、签名验证和 installer 管理。
用户最终仍拿到一个 `.exe` 安装程序或 `.dmg/.pkg`,无需手动管理 onedir 目录。
### 7.2 spec 设计原则
维护四个薄 spec,公共配置放 `packaging/common.py`。入口始终为 `src/zyt_doctor/__main__.py``pathex=["src"]`;只收集应用 resources 和必要 metadata。WebEngine profile 因 `integrations/video/webengine.py` 中有显式 import,触发 PyInstaller 官方 PySide6 hook;如通过 feature registry 动态加载,再显式加入这些 hidden imports
```python
VIDEO_HIDDEN_IMPORTS = [
"zyt_doctor.integrations.video.webengine",
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebChannel",
]
# core spec 中排除,且 core 构建环境根本不安装 Addons
CORE_EXCLUDES = [
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebEngineQuick",
]
```
不要手工把整个 `site-packages/PySide6` 复制进 datas,也不要用 `collect_all("PySide6")`;这会拉入无关 Qt 模块并可能破坏 hook 期望的目录。优先依赖当前锁定 PyInstaller 的 Qt hooks,仅对 self-check 证实遗漏的自有动态模块写自定义 hook。
macOS `BUNDLE` 至少设置稳定的 bundle id、版本、图标,并在 `Info.plist` 中写清:
```python
info_plist = {
"CFBundleIdentifier": "com.zhenyangtang.doctor",
"NSCameraUsageDescription": "用于医生视频问诊",
"NSMicrophoneUsageDescription": "用于医生视频问诊",
"NSHighResolutionCapable": True,
}
```
仅当有实际功能时再加入其他 TCC 权限说明。不得加入放宽 ATS 的全局例外。Windows spec 使用有版本信息的 manifest、`.ico` 和 GUI subsystem,同时保留内部异常日志;开发 smoke build 可临时打开 console。
### 7.3 构建命令骨架
Windows x64 runner
```powershell
py -3.12 -m venv .venv-build
.venv-build\Scripts\python -m pip install --require-hashes -r requirements\video-win-x64.lock
.venv-build\Scripts\python -m PyInstaller --noconfirm --clean packaging\windows\doctor-video.spec
dist\DoctorDesktop\DoctorDesktop.exe --self-check
```
macOS arm64 runner
```bash
python3.12 -m venv .venv-build
.venv-build/bin/python -m pip install --require-hashes -r requirements/video-macos-arm64.lock
.venv-build/bin/python -m PyInstaller --noconfirm --clean packaging/macos/doctor-video.spec
dist/DoctorDesktop.app/Contents/MacOS/DoctorDesktop --self-check
```
PyInstaller 不能从 Windows 生成 macOS `.app`,反之亦然。每次构建从干净环境执行 `--clean`,记录 Python/PySide6/PyInstaller/OS SDK 版本、锁文件 hash、git commit 和产物 SHA-256,保证可追溯。
### 7.4 WebEngine 自检
应用提供 `--self-check`,不访问患者数据,检查:
- build info、只读 resources、可写 data/log/cache 路径;
- keyring backend 是否安全、SQLite migration 是否可运行;
- TLS CA、production 配置和 host allowlist
- video profile 中通过 `QLibraryInfo` 定位 helper/resources/locales,禁止依赖写死的 `_internal` 路径;
- release 环境没有 `QTWEBENGINE_DISABLE_SANDBOX`/`--no-sandbox`
- GUI smoke 模式实际创建 `QWebEngineView`,加载本地无网络测试页,然后退出;真实视频另由端到端测试覆盖。
任何 helper、`.pak`、ICU、snapshot、platform plugin 或 locale 缺失都应让发布流水线失败,不在运行时静默降级。
### 7.5 Windows 发布
1. 使用固定、受控的 Windows x64 runner 构建;不要在装有多套 Qt/Anaconda 的个人机上发正式包。
2. QtWebEngine 运行依赖合适的 MSVC runtime。由安装器包含/检查 Microsoft Visual C++ RedistributableQt 官方要求的版本下限需按锁定 Qt 再核对),并在干净 VM 验证。
3. 用组织的 Authenticode 证书和 RFC 3161 时间戳签名主程序及最终 MSI/EXE 安装器;验证 `signtool verify /pa /all`
4. 安装到 Program Files,用户数据仍进 LocalAppData;普通用户可运行/升级。可用 WiX Toolset/MSIX 或 Inno Setup,选择后固定 UpgradeCode/AppUserModelID 和回滚策略。
5. 在 Windows 10/11 干净 VM 上验证安装、升级、卸载后保留/清除用户数据的明确策略、SmartScreen、企业代理、中文路径和非管理员账户。
### 7.6 macOS 发布
1. 在目标架构的 macOS runner 构建。PyInstaller 修改 Mach-O 后必须重新签名;使用 Developer ID Application 身份,不用 ad-hoc 签名发布。
2. QtWebEngine helper 是嵌套 app/process。必须保留 framework bundle 结构,并确认 helper 使用 Qt 自带的 `QtWebEngineProcess.entitlements` 所需权限签名;主 app 使用项目的 camera/microphone 权限说明和最小 entitlements。签名顺序由内向外,避免用 `codesign --deep` 掩盖错误。
3. 执行 `codesign --verify --deep --strict --verbose=2 DoctorDesktop.app``spctl --assess --type execute`;随后用 `xcrun notarytool submit ... --wait` 公证,staple ticket,再在离线干净 Mac 验证 Gatekeeper。
4. 用 DMG/PKG 或能保留 symlink 的方式分发。PyInstaller 6+ 的 POSIX bundle 广泛使用 symlink,普通 zip 若不保留 symlink 可能膨胀或破坏运行。
5. 在 Intel(若支持)与 Apple Silicon 真机上分别验证 keychain 升级连续性、摄像头/麦克风 TCC、休眠唤醒、Retina、多显示器和 WebEngine helper 签名。
### 7.7 常见打包坑清单
- **Qt binding 混装**:同环境存在 PyQt/PySide2 或系统 Qthook 收到冲突库。解决:干净 venv、只安装一种 binding、锁版本。
- **误用 onefile**:启动慢、helper/沙箱/签名问题更难复现。解决:正式版固定 onedir。
- **动态 import 未分析**:业务模块或 WebEngine 在 registry 中字符串加载。解决:显式 import 或最小 hiddenimports,并用 frozen smoke test 覆盖。
- **资源路径错误**:开发机相对路径可用,安装后 CWD 变化。解决:`importlib.resources`/qrc;用户数据用 QStandardPaths。
- **过度裁剪 Qt**:删除 `.pak`、ICU、snapshot、locale、platform plugin 后只在某些机器崩。解决:先保留 hook 输出,按真实清单与测试有证据地裁剪。
- **macOS 签名次序/entitlements 错**QtWebEngineProcess 启动即退出或 TCC 不弹窗。解决:嵌套 helper 真机测试、由内到外签、notarize/staple。
- **归档破坏 symlink**`.app` 体积暴涨或 framework 无法加载。解决:DMG/ditto 或明确保留 symlink 的归档工具。
- **GPU/远程桌面差异**WebEngine 黑屏。不要默认全局 `--disable-gpu`;收集诊断后提供经验证的软件渲染或外部浏览器 fallback。
- **codec 误判**:开发机能播 H.264,发布 wheel 不能播。解决:把真实媒体矩阵列入 artifact 验收和许可评审。
- **杀软与信誉**:大量 DLL/helper 或未签名 nightly 被拦截。解决:正式证书、时间戳、稳定 installer identity、干净 VM/主流安全软件测试。
- **升级破坏 keychain/数据**bundle id、签名 identity 或 schema 不稳定。解决:这些值从首版固定,migration 支持备份与回滚。
## 8. 安全与隐私基线
- TLS 校验永远开启。企业私有 CA 应通过受管安装进入系统 trust store;不得用 `verify=False`。如需兼容企业代理,可用 `truststore` 接入 OS 证书库并做专项测试。
- 本地配置不能提供任意 production host 重定向;敏感 token 永远不进入 URL query、clipboard、日志、analytics 或 crash report。
- HTML 病历优先用受限 `QTextBrowser`/原生富文本展示并在服务端净化;不要因为“要展示 HTML”就引入 WebEngine。任何外链由用户确认后交给系统浏览器。
- 限制剪贴板和通知中的患者信息;自动锁定后遮蔽窗口内容。截图阻止在跨平台上不可靠,不能作为合规控制。
- 所有高风险业务写操作由服务端保留不可抵赖审计;客户端日志只记录事件名、耗时、状态、request id 和脱敏技术上下文。
- 发布前完成依赖 SBOM、许可证清单和漏洞扫描。PySide6 采用 LGPLv3/GPLv3 或商业许可,QtWebEngine/Chromium/codec 还包含额外 notices;由法务确认采用的 Qt 许可与分发义务,安装包附第三方 notices。
- 自动升级若后续实现,更新 manifest 必须签名,制品必须校验 SHA-256 与平台签名,并支持回滚;首版宁可用已签名安装器提示升级,也不要执行未签名下载内容。
## 9. 测试与发布门槛
### 9.1 自动化测试分层
- **unit**:权限 guard、会话状态机、单飞 refresh、重试白名单、错误映射、缓存 TTL、加解密、outbox 状态和冲突决策。用 fake clock/random/secret store,保证确定性。
- **API contract**:以 OpenAPI schema 或后端 mock 验证字段、错误码、分页、时间和幂等语义;`respx` 模拟超时、断流、401 并发、429/Retry-After、5xx 和结果未知。
- **integration**:临时 SQLite + fake keyring + staging API,覆盖 migration、损坏缓存、磁盘满、退出清理、租户切换和代理/私有 CA。
- **UI**`pytest-qt` 验证路由权限、loading/empty/error/offline、键盘导航、取消和 late response;不要用脆弱的像素级截图替代行为断言。
- **frozen artifact**Windows/macOS 各自安装后运行 `--self-check`,启动主窗口、登录 mock/staging、访问资源、写用户目录、升级 migration、卸载。
- **视频真机**:摄像头/麦克风授权与拒绝、无设备、设备热插拔、回声设备、弱网/断网重连、休眠唤醒、屏幕共享、录播 codec、结束后权限与 Cookie 清理。
重点故障用例包括:十个请求同时 401、提交后响应丢失、服务器版本冲突、系统时钟偏差、刷新时退出、缓存被截断、keychain 被锁、两实例竞争、磁盘只读、API 维护、WebEngine helper 被杀、证书过期。测试数据必须是合成数据。
### 9.2 CI 矩阵与质量门槛
PR 阶段可并行运行 Windows x64 和 macOS arm64 的 lint/type/unit/UI headless 测试。release tag 阶段在原生 runner 生成制品并执行:
1. `ruff``mypy`、unit/contract/integration 测试全绿,覆盖率阈值重点约束 core 状态机而非 UI 行数。
2. 依赖 lock、SBOM、license、`pip-audit` 无未批准的高危项。
3. 两端 frozen self-check 与安装/升级 smoke 通过。
4. 签名、公证、hash、版本资源和 update channel 验证通过。
5. video profile 在目标硬件的人工/自动验收清单签字;core profile 证明不会意外收集 QtWebEngine。
6. staging 完成登录、权限变更、患者查询、一个低风险写操作、一个高风险确认、登出清理和离线恢复闭环。
## 10. 实施顺序与验收里程碑
### M0:契约和风险封板(约 3–5 天)
- 获取 OpenAPI/认证/权限/RTC/对象存储契约;形成 endpoint 与权限矩阵。
- 在 Windows/macOS 原型中用 QtWebEngine 打开专用测试页,验证 TRTC/WebRTC、设备权限和目标 codec。
- 决定首发是 core、video 还是“core + 外部浏览器”。
**退出条件**API base 和 auth 不再是假设;视频路线有真实 PoC,而非只证明网页能打开。
### M1:可发布骨架
- 完成 bootstrap、配置校验、日志/路径、API client、SessionManager、PermissionGuard、shell 和统一状态组件。
- mock server 下完成登录、`/me`、权限菜单、401 single-flight refresh、登出清理。
- 两端 onedir unsigned nightly 可安装并通过 self-check。
### M2:业务纵切
- 先选一个完整纵切(例如预约 -> 患者概要 -> 诊间记录草稿),按 module 结构贯穿 UI、service、API、权限和测试。
- 再并行扩展患者、病历、处方、随访、聊天;高风险操作必须有后端幂等/审计。
### M3:离线与视频
- 实现加密 cache/outbox、冲突页、断网/恢复和数据清理。
- video adapter、受限 profile、一次性 ticket、权限撤销和外部浏览器 fallback 完成。
### M4:生产发布
- 锁依赖和 runner image,完成 Windows Authenticode、macOS Developer ID/notarization、SBOM/许可证。
- 干净 VM/真机、企业代理、非管理员、升级/回滚、视频设备矩阵全部通过。
## 11. 最终架构决策摘要
1. 业务 UI 用原生 Qt WidgetsQtWebEngine 是隔离的视频实现细节,不是应用架构。
2. 后端契约、服务端授权和服务端审计是权威;桌面端只做强类型 adapter、体验 guard 和安全状态管理。
3. access token 仅在内存,长期凭证进 OS keychain;敏感离线数据加密且按用户/租户隔离。
4. 高风险医疗写操作离线时只能保存草稿,恢复后由医生确认;普通 outbox 依赖幂等键和乐观锁。
5. 发布固定 PyInstaller onedir、平台原生构建、签名和 artifact 级测试;不交叉编译,不依赖开发机“能跑”。
6. production URL、SDK secret、对象存储密钥和 RTC 签名密钥都不能硬编码进客户端。
## 参考资料
- [Qt for Python package details](https://doc.qt.io/qtforpython-6.10/package_details.html)Essentials/Addons 拆分和 wheel 内容。
- [Qt for Python 与 PyInstaller](https://doc.qt.io/qtforpython-6.10/deployment/deployment-pyinstaller.html):官方 PyInstaller 基础部署说明。
- [Qt WebEngine 部署](https://doc.qt.io/qt-6/qtwebengine-deploying.html)helper、resources、locales、macOS entitlements 等必需项。
- [Qt WebEngine features](https://doc.qt.io/qt-6/qtwebengine-features.html)WebRTC/媒体能力及专有 codec 许可提醒。
- [PyInstaller macOS multi-arch 与签名](https://pyinstaller.org/en/stable/feature-notes.html):架构 slice 和 codesign 行为。
- [PyInstaller symlink/common pitfalls](https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html)PyInstaller 6+ POSIX bundle 的 symlink 分发要求。
+320
View File
@@ -0,0 +1,320 @@
# 医生工作站最终 parity 审计
审计日期:2026-08-10
审计方式:只读源码复核 + 纯本地测试;未修改业务源码。
结论基准:`D:\web\zyt\admin\src\views` 的实际实现优先于既有 `research/parity_*.md`
## 1. 结论
当前医生桌面整体判定为 **PARTIAL**,不建议在修复 P0 前作为后台同型版本发布。
- **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 文档明显早于本轮实现,不能直接当作当前验收结果;本报告已重新逐项分类。
本次执行:
```text
.venv\Scripts\python.exe -m pytest
110 passed in 1.33s
```
全绿不能覆盖本报告的 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 修复反向改写。
## 2. 范围与判定规则
审计范围:
- 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** 提升到可发布候选。
+101
View File
@@ -0,0 +1,101 @@
# 医生工作站集成审查
审查时间:2026-08-10。范围为当前工作区中的组合根、登录到主壳链路、Remote/Demo repository、权限门控、退出/视频生命周期及 PyInstaller 入口。审查只读进行;除本报告外未修改项目文件,也未发送网络请求。
## 结论
入口链 `packaging/doctor_workstation.spec -> doctor_workstation/__main__.py -> app.main()``build_repository(..., verify=...)` 的构造签名已经对齐,Remote/Demo 的主要 CRUD 与通话方法也具有兼容签名。当前仍有 4 个高严重度和 6 个中严重度集成问题;其中浏览器视频模式目前不能真正发起通话,退出时也存在通话窗口失管和 GUI 阻塞风险。
## P1(高)
### 1. 浏览器视频模式没有把一次性通话上下文交给伴随页
- `src/doctor_workstation/video/window.py:312-326` 在服务端 `start_call` 后仅执行 `webbrowser.open(location.url)``VideoCallRequest` 中的 diagnosis、目标用户和 UserSig 均未交给浏览器。
- `video_companion/src/main.ts:208-236` 只有显式调用 `window.doctorCall.start(config)` 才会初始化 SDK 并呼叫患者,页面自身不获取票据;`video_companion/src/main.ts:281-289` 只是暴露 API 和发送 ready。
- `README.md:70-77` 又把 browser 声明为正式降级路径,并要求用一次性业务票据传递上下文,和当前实现不一致。
系统浏览器不存在 Qt WebChannel,且静态 URL 连 diagnosis_id 都没有,因此页面会一直停在“等待桌面端发起”,而后端通话记录已经开始。浏览器标签关闭也无法回告 Python,`end_call` 只能等显式退出应用。应使用后端签发、单次消费且短有效期的浏览器 handoff ticketURL 中不能放 UserSig),或受认证的本机 IPC;伴随页确认接收后再调用 `start_call`,并建立可观测的结束回调。
### 2. 通话 start/bind/end 的同步 HTTP 被直接放在 Qt GUI 线程执行
- `src/doctor_workstation/video/window.py:206-290` 直接调用 repository 的 `start_call``bind_call_room``end_call`
- browser 在 `src/doctor_workstation/video/window.py:312-320` 同步 startembedded 在 loadFinished 回调 `src/doctor_workstation/video/window.py:437-456` 同步 start,在 bridge/close 回调 `src/doctor_workstation/video/window.py:485-522` 同步 bind/end。
- Remote 实现最终执行同步 `httpx` POST`src/doctor_workstation/services/repository.py:377-402`),而配置超时可达 120 秒。
- 登出和进程退出又在主线程逐个 `call.close()``src/doctor_workstation/app.py:298-315``src/doctor_workstation/app.py:409-415`)。
弱网时打开、挂断、退出都会冻结整个界面;`end_call` 抛错时 embedded 的 `closeEvent` 甚至到不了 `event.accept()`。应把生命周期写操作放入受控 worker,并使用状态机保证单次执行;退出阶段设置短上限、记录未完成结束动作,同时先关闭媒体/UI,不能让网络请求阻塞 Qt 关闭事件。
### 3. 同一 diagnosis 的重复发起会覆盖通话句柄,导致退出后仍可能保留旧视频窗口
- `src/doctor_workstation/app.py:343-356` 没有 pending/active 去重,用户可在票据请求完成前重复发起。
- `src/doctor_workstation/app.py:390-396` 以 diagnosis_id 为唯一 key 直接覆盖旧句柄;旧窗口的 destroyed 回调还会无条件 `pop` 同一个 key,可能把较新的句柄移除。
- 登出/退出只关闭字典当前仍持有的值(`src/doctor_workstation/app.py:298-305``src/doctor_workstation/app.py:409-412`)。
结果是第一个窗口可能在切回登录页后继续持有摄像头/麦克风;反向关闭旧窗口也会让新窗口失去托管。应对 diagnosis 建立 pending/active 单飞,拒绝或先可靠关闭旧通话;销毁回调必须做对象身份判断后再移除。
### 4. WebEngine 对任意来源自动授予音视频权限,且登出没有隔离/清理 profile
- `src/doctor_workstation/video/window.py:381-390` 使用 `QWebEngineView` 的共享默认 profile,没有为单次通话建立隔离 profile。
- `src/doctor_workstation/video/window.py:405-435` 在授权回调中不校验请求 origin、当前页面 URL 或通话状态,只要 feature 名含 audio/video 就 grant。
- 没有自定义 `acceptNavigationRequest`/origin allowlist;关闭时 `src/doctor_workstation/video/window.py:519-522` 仅挂断,不撤销权限或清理 Cookie、cache、local storage。
初始 URL 虽经过 HTTPS 校验,但页面后续导航不受限制;同一进程内切换账号时 WebEngine 状态也可能复用。应使用每通话或每会话的 off-the-record profile、精确 origin/path allowlist,只在活跃通话且当前主文档来源匹配时授权,并在挂断/登出时撤销权限和清理页面/profile。
## P2(中)
### 5. API code=-1 没有接入应用级会话失效处理
- `src/doctor_workstation/services/api_client.py:277-278` 会抛出 `AuthenticationExpiredError`
- worker 只把异常交给页面自己的通用 on_error(`src/doctor_workstation/ui/widgets.py:262-293`);组合根只有用户主动点击时才执行 `_logout``src/doctor_workstation/app.py:298-315`)。
- 项目中除异常定义/抛出外没有消费 `AuthenticationExpiredError` 的代码。
token 过期后主壳仍展示已加载的患者数据和旧权限快照,各页面只显示错误,用户必须手动退出。应在统一 API/worker 边界发出 session-expired 事件,由 Controller 原子地停止轮询和视频、清 token、销毁 ShellWindow 并回到 LoginWindow。
### 6. 页面和动作权限使用过宽的 OR 兜底,并忽略后端 menu
- `src/doctor_workstation/services/repository.py:111-127` 已解析 `Session.menu`,但 `src/doctor_workstation/ui/shell.py:248-260` 只按硬编码 `NAVIGATION.permissions` 注册页面,menu 从未参与判断;这与管理端“无有效 menu 即 403”的已确认行为(`research/admin_audit.md:103-109`)不一致。
- 接诊页可仅凭 `doctor.appointment/reception` 显示,但页面首先请求 `doctor.appointment/lists``src/doctor_workstation/ui/shell.py:40-47``src/doctor_workstation/ui/pages/reception.py:347-364`)。患者页可仅凭 readonlyDetail 显示,却始终请求 firstvisit lists;问诊页可仅凭 appointment lists 显示,却始终请求 diagnosis lists`src/doctor_workstation/ui/shell.py:62-75`)。
- 通知医助和视频按钮又把基础 lists 当动作权限兜底(`src/doctor_workstation/ui/pages/reception.py:321-332``src/doctor_workstation/ui/pages/consultations.py:88-96`),所以缺少 videoQr/notifyAssistant 的账号仍看到按钮。
服务端仍是最终边界,但当前 UI 会显示必然 403 的页面/动作,也可能绕过管理员对 desktop 页面入口的隐藏意图。页面应同时受后端 menu/capability 和实际列表接口权限约束;动作只接受对应动作码或经过确认的同义码,不能用 lists 兜底。
### 7. 已开处方审核状态筛选在 UI 适配层被改成 Remote 不识别的字段
- UI 传入 status`src/doctor_workstation/ui/widgets.py:205-208` 将其改写为 `audit_status`
- Remote 只在收到 `status` 时转换成后端需要的 `audit_filter=pending|passed|rejected``src/doctor_workstation/services/repository.py:284-303`),因此实际请求会原样携带数值 `audit_status`
- Demo 特意兼容了 `audit_status``src/doctor_workstation/services/mock_repository.py:374-399`),所以演示验收不会暴露生产差异。
结果是生产环境的“待审核/已通过/已驳回”筛选可能无效或被后端拒绝。应只在 repository 内做一次 UI 值到 API DTO 的映射,并给 Remote 增加 request-parameter 合同测试。
### 8. 登录 token 在 Session 校验成功前落盘,失败路径不回滚
- `src/doctor_workstation/services/repository.py:82-90` 在调用 `auth.admin/mySelf` 前就设置并持久化 token。
- mySelf 网络失败、协议错误或 code=10 时 LoginWindow 只显示错误(`src/doctor_workstation/ui/login.py:385-390`),不会调用 repository.logout。
这会在用户从未进入有效 Session 的情况下留下内存和 keyring/JSON token,后续登录还会带着该 token 请求 login/account。应先把 token 暂存在内存,完整构建并校验 Session 后再持久化;任何异常都清理 client/token store。
### 9. “记住账号”未控制 TokenStore 中的账号落盘,且持久 token 没有启动恢复闭环
- `src/doctor_workstation/services/repository.py:87-88` 每次成功账号密码登录都把 account 传给 TokenStore,与复选框无关。
- TokenStore 会把 account 写入 JSON 元数据(`src/doctor_workstation/services/token_store.py:83-108`),而 LoginWindow 的复选框只增删 QSettings`src/doctor_workstation/ui/login.py:374-379`)。
- `ApplicationController.start()` 始终显示登录页(`src/doctor_workstation/app.py:172-176`),未调用已实现的 `restore_session`;普通关闭只 close client,不清 token`src/doctor_workstation/app.py:409-415`)。
因此取消“记住账号”仍会在磁盘留下账号;成功 token 则持续保存但下次启动完全不使用。应让 remember-account 明确控制所有账号元数据,并二选一:启动时安全验证/恢复 token,或不持久化并在关闭时清除。
### 10. 当前冻结产物落后于最新 companion,构建脚本也没有执行入口 smoke test
- 最新 `video_companion/dist/index.html:8` 引用 `index-le5ZH3pL.js`(包含 roomId/bindCallRoom 桥接),现有 `dist/DoctorWorkstation/_internal/video_companion_dist/index.html:8` 仍引用旧的 `index-BrQGJzsD.js`
- 应用已经提供 `--smoke-test``src/doctor_workstation/app.py:436-451`),但 Windows 构建只检查 helper/pak/index 文件存在(`scripts/build_windows.ps1:30-43`),macOS 同样只做静态文件和 codesign 检查(`scripts/build_macos.sh:22-37`),都不启动冻结入口。
所以当前 `dist/DoctorWorkstation` 不包含刚合并的 room binding;未来即使入口 import/bootstrap 失败,构建也可能仍打印成功。交付前应重新执行 PyInstaller,并在隔离用户目录、无真实网络的环境下运行冻结程序 `--smoke-test`,同时校验退出码和日志中无未捕获异常。
## 已核对且未发现签名断点
- `ApiClient(base_url, ..., verify=...)``build_repository(..., verify=...)``app.py` 调用一致。
- Remote/Demo 的 login 均返回 `Session`;列表、接诊、处方库 CRUD、患者/问诊列表及 `get_call_ticket/start_call/bind_call_room/end_call` 的 UI 所需参数基本对齐。
- PyInstaller spec 的入口、`src` pathex、resources 和 `video_companion_dist` 目标路径与 `resources.py``_MEIPASS` 查找规则一致。
- 最新 source companion 已产生 roomId 并调用 `bind_call_room`;本报告未把此前已修复的“问诊页交换 appointment/diagnosis ID”或“未绑定 room”列为当前问题。
## 验证限制
本轮没有重新执行 Python 测试:工作区 `.venv/Scripts/python.exe` 指向当前机器上不存在的 uv Python 基础解释器;为保持只读审查,没有重建虚拟环境。现有 `research/ui_acceptance.md` 记录的最近一次完整测试为 46 passed,但上述多项是未被现有单元测试覆盖的跨层行为。
+229
View File
@@ -0,0 +1,229 @@
# 患者列表、动态菜单与权限一致性审计
审计日期:2026-08-10
管理端唯一事实来源:`D:\web\zyt\admin\src\views\**`
Python 对照范围:`D:\web\zyt\app\src\doctor_workstation\**`
## 1. 结论摘要
| 审计项 | 结论 | 摘要 |
|---|---|---|
| “患者列表”真实业务页面识别 | **EXACT** | 医生工作站语义下的真实页面是 `first_visit/my_patients/index.vue`,不是同样含“患者列表”字样的 `doctor/progress.vue`。 |
| Python 患者列表基础查询 | **PARTIAL** | 使用同一“我的患者”业务语义、相同五种状态和服务端数据范围,但只实现了主列表的子集。 |
| 患者详情与历史关联 | **MISSING** | Python 没有进入诊单编辑/只读详情链,也没有详情请求;右侧内容直接取列表行及其 `raw`。 |
| 患者订单管理、面诊进度 | **MISSING** | 管理端同一路由内的两个完整工作区在 Python 中均不存在。 |
| 五模块主导航 | **PARTIAL** | Python 解析并保存 `mySelf.menu`,登录时只判断菜单非空;Shell 随后忽略菜单树,使用固定五项加扁平权限码。 |
| 按钮权限 | **PARTIAL** | 多数已有写操作有权限门禁,但存在 canonical 权限码偏差、额外别名放行、患者动作大量缺失;处方库所有者判断存在 fail-open。 |
| 医生角色/部门数据范围 | **PARTIAL** | 患者主列表的请求未注入任意医生 ID,保持服务端权威范围;但 Python 丢弃 `extend.scope`、汇总和 ownership 模式,且缺少订单/进度工作区。 |
判定口径:
- **EXACT**:当前证据范围内,业务入口、条件、权限或数据语义一致。
- **PARTIAL**:主链存在,但字段、筛选、动作、菜单约束或范围提示不完整/不一致。
- **MISSING**:管理端已存在的业务能力,在 Python 中未找到入口或调用链。
## 2. 证据边界
1. 本报告没有读取 `D:\web\zyt\admin\src\views` 之外的管理端文件。因此可以确认 Vue 页面实际引用的 API **函数名、参数和交互**,但不能读取 `@/api/**` 的实现来反推真实 HTTP URL。
2. `views/**` 内没有 `auth.admin/mySelf` 调用或该接口返回的真实菜单 JSON。五个主页面的 `paths/component/perms/sort/is_show/is_disable` 精确菜单记录无法从本次允许范围内恢复。
3. 管理端的菜单管理页证明动态菜单记录至少分别保存“路由路径”“组件路径”“权限字符”“是否显示”“菜单状态”:`D:\web\zyt\admin\src\views\permission\menu\edit.vue:41-94,125-150`;其中隐藏菜单仍可访问的语义写在 `D:\web\zyt\admin\src\views\permission\menu\edit.vue:135-137`。因此,不能把单一扁平权限码等同于完整路由记录。
4. 对服务端是否再次执行权限/所有权校验不作推断;本报告的 P0 是**客户端授权门禁一致性**问题。
## 3. “患者列表”真实页面识别
### 3.1 主页面:`first_visit/my_patients/index.vue`
确认依据:
- 页面直接声明“一诊 / 我的患者”,并说明“患者、挂号与诊单信息按当前角色和部门数据范围展示”:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:5-10`
- 同一路由内明确包含“患者列表 / 订单管理 / 面诊进度”三个工作区:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:15-20`
- 主表由 `myPatientLists` 驱动,并异步装载诊单编辑、预约、订单和进度组件:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:330-344,404-409`
结论:Python `PatientsPage` 应对标这一页面及其子工作区,而不应只对标某张通用患者/挂号表。
### 3.2 排除项:`doctor/progress.vue`
该文件虽在表头使用“患者列表”,但它是医生面诊进度看板:
- 页面要求先选择医生,再展示该医生挂号:`D:\web\zyt\admin\src\views\doctor\progress.vue:57-88`
- 组件名为 `doctorProgress`API 为 `doctorLists``appointmentLists``D:\web\zyt\admin\src\views\doctor\progress.vue:176-182`
- 它显式传 `role_id: 1` 拉医生,并用 `doctor_id + progress_board=1` 拉挂号;注释说明不按医生/医助角色收窄:`D:\web\zyt\admin\src\views\doctor\progress.vue:408-448,460`
因此该页面是跨医生进度看板,不是“我的患者”主数据页。把它的数据范围套到患者模块会错误扩大语义范围。
## 4. 管理端患者模块真实合同
### 4.1 主列表、筛选和服务端范围
| 能力 | 管理端证据 |
|---|---|
| 关键词 | 患者姓名、手机号、助理、医生:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:23-43` |
| 状态 | 未预约、待面诊、已完成、已过号:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:346-393` |
| 挂号日期 | 今日、明日、后天、近 7 天、近 30 天、自定义区间:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:46-69,395-401` |
| 汇总 | 今日/明日/后天预约人数来自 `pager.extend.summary/dates``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:72-88,411-416` |
| 范围提示 | 顶部和表格显示 `pager.extend.scope.label``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:90-96,417` |
| 列表字段 | 患者性别年龄/脱敏手机、归属助理、预约医生及状态、预约时间、复诊次数、确认信息、诊单日期:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:105-148` |
主列表 API 符号为 `myPatientLists`,调用参数为 `keyword/status_filter/start_date/end_date/page``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:330-336,380-408`
### 4.2 行级动作与权限码
| 动作 | 管理端权限/条件 | 证据 |
|---|---|---|
| 编辑诊单 | `tcm.diagnosis/edit` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:152,418,492-498` |
| 只读查看 | `tcm.diagnosis/readonlyDetail` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:153,419,500-510` |
| 预约/取消挂号 | `tcm.diagnosis/guahao`;取消仅状态 1/4 | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:154,179-186,420,681-696` |
| 指派/重新指派医助 | `tcm.diagnosis/assign` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:155-162,421,527-575` |
| 补全身份证 | 复用 `tcm.diagnosis/edit` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:163-170,422,578-628` |
| 诊单二维码 | 复用挂号权限,且要求有效挂号、状态 1、医生 ID | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:171-178,630-673` |
只读诊单不是列表内的摘要面板。页面先从动态路由中查找 `meta.perms === 'tcm.diagnosis/readonlyDetail'`,再携带诊单 ID 跳转:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:500-510`
### 4.3 订单管理子工作区
- 数据口径明确为“当前患者范围内”的处方业务订单,且“订单创建人不参与数据归属判断”:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:3-10`
- 支持处方审核、支付单审核、履约状态、关键词与日期筛选:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:14-79`
- 汇总包含订单数、有效金额、待审核、完成/签收、拒收数和拒收率:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:82-123,289-297`
- 列表显示订单/患者/处方与诊单/金额/双审/履约/支付单/助理/医生/创建人:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:125-188`
- 数据由 `myPatientOrderLists` 分页加载:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:234-244,282-286`
- 订单按钮以 canonical `tcm.prescriptionOrder/*` 权限和业务状态共同判定;完整矩阵在 `D:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:32-124`,包括详情、编辑、双审及撤回、快递、补支付、完成、退款、撤回、上传药房。
### 4.4 面诊进度子工作区
- 服务端通过 `extend.schedule_mode` 决定“按本人归属”或“与排班合并”,而不是客户端按角色 ID 猜测:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:7-26,206-214`
- “本人患者”语义、近七日安排、医生聚合、候诊队列均在同一工作区:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:31-117,120-178`
- API 符号为 `myPatientProgressLists`,数据范围标签来自 `extend.scope.label`,本人患者由 `row.is_self_patient` 标识:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:185-203,263,308-309`
- 每 15 秒自动刷新:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:318-325`
### 4.5 详情与历史关联链
管理端患者行会进入诊单编辑或只读详情,而后继续关联:
- 只读页真实标题为“患者信息详情”,无权限/不存在时明确空态:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:4-36`
- 基本信息、完整病历、日常记录、医生备注/舌苔/报告:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:38-72`
- 业务订单、视频回放、聊天、医助指派历史、挂号历史分别按权限展示:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-146`
- 详情由 `diagnosisReadonlyDetail({id})` 拉取,备注另用 `getDoctorNotes({diagnosis_id})` 补齐:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:151-188,209-234`
- 编辑页还提供处方病历、订单、视频、聊天、指派与挂号记录分页签:`D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue:655-759`
- 挂号历史不是列表行快照,而是按诊单 ID 单独调用 `appointmentLists`,携带 `diag_scope_relax: 1`、最多 500 条:`D:\web\zyt\admin\src\views\tcm\diagnosis\components\AppointmentRecordPanel.vue:79-89,139-152`
- 指派历史同样按诊单 ID 调用 `tcmDiagnosisAssignLogList``D:\web\zyt\admin\src\views\tcm\diagnosis\components\AssignLogPanel.vue:47-57,102-125`
- 业务订单按 `context_diagnosis_id + patient_id + scene=diagnosis_edit` 独立分页:`D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:118-148,170-179`
## 5. Python 患者模块逐项对照
| 项目 | Python 当前实现 | 判定 | 说明 |
|---|---|---|---|
| 页面身份 | 标题“我的患者”,描述服务端授权范围:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:84-92` | **EXACT** | 对标对象正确。 |
| 状态筛选 | 五种状态一致:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:94-119` | **EXACT** | 状态值与管理端一致。 |
| 关键词 | Python 提示“姓名、手机号或诊单号”:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:99-103` | **PARTIAL** | 管理端还明确支持助理/医生。 |
| 日期与汇总 | 无日期快捷项、区间和三日汇总 | **MISSING** | 管理端证据见 4.1。 |
| 表格 | 患者、性别年龄、电话、进度、医助、最近预约、复诊:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:132-196` | **PARTIAL** | 缺预约医生独立列、确认信息、诊单日期和全部操作。 |
| 列表请求 | `patients(keyword,status,page,page_size)``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:307-326` | **PARTIAL** | 无 `start_date/end_date`;服务层正确映射 `status -> status_filter`。 |
| 服务端范围 | `list_patients` 使用 `firstvisit.myPatient/lists`,不注入医生 ID`D:\web\zyt\app\src\doctor_workstation\services\repository.py:398-412` | **EXACT** | 保持服务端权威数据范围,没有客户端越权扩大证据。 |
| `extend` | `PageResult` 完整保留 `extend``D:\web\zyt\app\src\doctor_workstation\core\models.py:590-598,618-672`;页面只读 items/total`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:328-335` | **PARTIAL** | scope、summary、dates 已被解析却未消费。 |
| 患者模型 | 覆盖主列表核心字段并保留 `raw``D:\web\zyt\app\src\doctor_workstation\core\models.py:241-318` | **EXACT** | 解析层可承载主表数据。 |
| 详情 | 选择行后直接 `_render_detail(patient)``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:349-415` | **MISSING** | 没有诊单详情请求,不能等价于管理端只读/编辑页。 |
| 历史预约 | 从列表行 `appointments` 取最多 6 条:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:417-446` | **MISSING** | 管理端按诊单单独请求最多 500 条并显示更完整字段。 |
| 患者行动作 | 无编辑/只读/预约/指派/补身份证/二维码/取消挂号按钮 | **MISSING** | `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1-454` 没有动作链。 |
| 订单/进度工作区 | 无 | **MISSING** | Python 只有列表与右侧摘要。 |
补充:`get_value` 会在 dataclass 属性缺失时回退到 `.raw``D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:48-66`。因此右侧摘要“可能”显示列表响应附带的额外字段,但它仍是列表快照,不是独立详情/历史合同。
## 6. 五模块动态路由、API 与按钮权限复核
### 6.1 主路由组件与 Python 导航
| 模块 | 管理端实际组件(views 证据) | Python 固定导航权限 | 判定 |
|---|---|---|---|
| 接诊台 | `D:\web\zyt\admin\src\views\patient\reception\index.vue:1-18` | `doctor.appointment/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:40-47` | **PARTIAL** |
| 处方库 | `D:\web\zyt\admin\src\views\consumer\prescription\list.vue:1-4,234-245` | `tcm.prescriptionLibrary/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:48-54` | **PARTIAL** |
| 已开处方 | `D:\web\zyt\admin\src\views\consumer\prescription\index.vue:1-4,1697-1711` | `tcm.prescription/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:55-61` | **PARTIAL** |
| 患者 | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:5-20` | `firstvisit.myPatient/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:62-68` | **PARTIAL** |
| 问诊/诊单 | `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:726-738` | `tcm.diagnosis/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:69-75` | **PARTIAL** |
这里的 **PARTIAL** 不是说固定权限码必然错误,而是:在 `views/**` 证据范围内无法恢复五条真实 `mySelf.menu` 记录,Python 又没有使用已经解析到的菜单树来做组件/顺序/显示/停用约束。
### 6.2 `mySelf` 菜单链
1. Python 正确请求 `auth.admin/mySelf`,解析用户、权限与 menu`D:\web\zyt\app\src\doctor_workstation\services\repository.py:164-195` —— **EXACT**
2. `Session` 正确保留 `menu``D:\web\zyt\app\src\doctor_workstation\core\session.py:12-22` —— **EXACT**
3. 正式登录只检查 `session.menu` 非空,然后把权限交给 Shell`D:\web\zyt\app\src\doctor_workstation\app.py:413-443` —— **PARTIAL**
4. Shell 注册页面时遍历固定 `NAVIGATION`,只调用扁平 `has_permission`,没有读取 `session.menu``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:248-280` —— **MISSING(菜单绑定)**
实际影响:菜单中任意一项存在即可通过登录守卫;随后五模块的显示、顺序、名称及停用状态均由本地固定表决定。动态路由中的隐藏/停用/组件路径/排序语义没有落地。
### 6.3 按钮权限与功能矩阵
#### 接诊台 — **PARTIAL**
- 管理端备注、编辑病历、完成接诊分别使用 `doctor.appointment/addDoctorNote``tcm.diagnosis/edit``doctor.appointment/complete``D:\web\zyt\admin\src\views\patient\reception\index.vue:144-171`
- 管理端队列内“通知医助/发起通话”按钮本身没有 `v-perms``D:\web\zyt\admin\src\views\patient\reception\index.vue:75-90`
- Python 备注与完成权限一致,但通知增加了 `doctor.appointment/notifyAssistant`,视频增加了 `tcm.diagnosis/videoQr`;缺“编辑病历”:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:322-331`
- Python 的通知、备注、完成调用链存在:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:558-641`;管理端对应 API 调用见 `D:\web\zyt\admin\src\views\patient\reception\index.vue:460-527`
#### 处方库 — **PARTIAL,含 P0 权限门禁问题**
- 管理端 canonical 按钮码是 `wcf.prescription/add|read|edit|delete``D:\web\zyt\admin\src\views\consumer\prescription\list.vue:35-41,83-105`
- 管理端编辑/删除还要求创建人本人,或 root/角色 0、3:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:278-301`
- Python 新增/编辑/删除同时接受 canonical `wcf.*` 和额外 `tcm.prescriptionLibrary/*``D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:271-279,330-348`
- Python root/角色 0、3 的例外与管理端一致,但当 `user_id``creator_id` 缺失时直接返回可管理:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:460-472`。管理端的 `Number(row.creator_id) === Number(user.id)` 不会对缺失创建人默认放行。
#### 已开处方 — **PARTIAL/MISSING actions**
- 管理端支持业务订单跳转、新增、查看、修正患者、创建订单、编辑、审核、删除,对应权限码见 `D:\web\zyt\admin\src\views\consumer\prescription\index.vue:102-117,214-265`
- 业务订单导航依赖动态路由中的 `meta.perms === 'tcm.prescriptionOrder/lists'`,菜单不存在会拒绝跳转并提示:`D:\web\zyt\admin\src\views\consumer\prescription\index.vue:1862-1871`
- Python 只实现关键词/审核状态的列表和详情请求:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:91-188,284-340`;上述写操作与订单导航均不存在。
#### 患者 — **MISSING actions**
完整动作矩阵见第 4.2 节;Python 患者页没有对应按钮或权限检查。
#### 问诊/诊单 — **PARTIAL/MISSING actions**
- 管理端新增、批量指派、查看、诊单、开方、预约、补身份证、指派/取消指派、视频二维码、确认二维码/取消挂号、挂号日志、订单、删除的权限矩阵见 `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:154-175,342-405`
- Python 仅保留列表筛选与 `tcm.diagnosis/videoQr` 视频入口:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:101-107,110-236,264-323`
### 6.4 权限匹配语义
- `PermissionSet` 支持精确码、全局 `*``prefix/*``D:\web\zyt\app\src\doctor_workstation\core\permissions.py:24-84`OR 语义见 `D:\web\zyt\app\src\doctor_workstation\core\permissions.py:96-104`
- UI `has_permission` 额外把 `/``.` 互换后尝试匹配:`D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-180`
- 管理端视图只声明 canonical 形态,例如 `tcm.diagnosis/edit``wcf.prescription/edit`。由于本次不能读取管理端权限工具实现,不能证明管理端也接受点/斜杠变体;Python 的变体放行应判为 **PARTIAL**,不应当作已证明的兼容要求。
## 7. 医生角色与数据范围
### 7.1 已证明一致的部分
- 患者列表:管理端要求按当前角色/部门展示并把最终范围作为 `extend.scope.label` 返回;Python 调用相同业务域列表,不传任意 `doctor_id`,所以没有客户端扩大数据集的证据。判定 **EXACT(请求边界)**
- 处方库:普通医生只能管理自己创建的模板,root/角色 0、3 才可管理全部。Python 的正常 ID 分支与此一致。判定 **EXACT(正常分支)**
### 7.2 部分或缺失
- Python 不显示 `extend.scope.label`,用户无法验证当前是本人、部门还是其他服务端范围:**PARTIAL**。
- Python 缺失患者订单工作区,因而没有落实“当前患者范围内、订单创建人不参与归属”的口径:**MISSING**。
- Python 缺失面诊进度工作区,因而没有落实服务端 `schedule_mode=ownership``is_self_patient` 和近七日聚合:**MISSING**。
- 除患者模块视图明确写出的范围文字外,接诊、处方、诊单视图没有在允许证据范围内定义服务端角色/部门过滤算法。Python 使用同域列表接口且未见额外越权 `doctor_id` 注入,但无法仅凭 views 宣称五模块的后端数据范围 **EXACT**;应保留 **PARTIAL / 服务端待证**
## 8. P0 / P1 缺口
### P0
1. **处方库所有权校验 fail-open。**
Python 在当前用户 ID 或模板 `creator_id` 任一缺失时允许编辑/删除:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:460-466`;管理端仅允许严格所有者匹配或 root/角色 0、3:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:286-301`。应改成缺失即拒绝,并只在明确 root/角色例外时放行。若服务端另有强制校验,实际数据写入风险会降低,但客户端授权偏差仍已成立。
### P1
1. **患者模块只有主表子集。** 缺日期筛选、三日汇总、范围标签、确认/诊单日期列和全部行级动作。
2. **患者详情不是管理端真实详情。** Python 不请求诊单只读/编辑详情,病历摘要与最多 6 条历史预约依赖列表行快照,可能为空或陈旧。
3. **患者订单管理与面诊进度两个工作区完全缺失。** 同时丢失订单数据范围、订单操作权限矩阵、ownership 模式和 15 秒候诊刷新。
4. **Shell 忽略 `mySelf.menu`。** 已解析的动态菜单只用于“非空”登录守卫;五模块由固定表和扁平权限决定,未落实组件路径、显示/停用、排序和真实菜单成员关系。
5. **处方库权限码存在额外别名放行。** Python 接受 `tcm.prescriptionLibrary/add|edit|delete`,而视图仅证明 canonical `wcf.prescription/*`;应在拿到真实 `mySelf.permissions/menu` 样本后收敛。
6. **通用权限帮助器接受 `/`/`.` 互换。** 该兼容未被 views-only 证据证明,可能让非 canonical grant 意外点亮按钮。
7. **接诊台按钮矩阵不一致。** Python 比管理端额外门禁通知/视频,同时缺少 `tcm.diagnosis/edit` 的编辑病历动作。
8. **已开处方和问诊页均被压缩为只读子集。** 管理端已有的新增/编辑/审核/删除/订单/预约/指派等受权操作未落地。
## 9. 建议的验收优先级
1. 先修复处方库所有权 fail-open,并用“缺 `creator_id`、缺当前用户 ID、本人、他人、root、角色 0/3”六组合同测试覆盖。
2. 让 Shell 以 `Session.menu` 为主约束,再叠加权限码;不要仅以硬编码五项替代动态菜单。
3. 将患者页拆成与管理端一致的三个工作区,优先补真实只读详情/历史请求和主表行级动作。
4. 消费 `PageResult.extend.scope/summary/dates/schedule_mode`,把服务端权威范围直接呈现给医生。
5. 获取一份真实医生角色 `mySelf` 响应样本后,补做菜单 `paths/component/perms/is_show/is_disable/sort` 的精确合同测试;这一步无法由 `views/**` 单独完成。
+357
View File
@@ -0,0 +1,357 @@
# “我的处方库 / 已开处方”桌面端对齐审计
审计日期:2026-08-10
## 1. 范围、基准与结论口径
本审计只读检查以下两套实现,没有修改业务源码:
- 管理端唯一功能基准:D:/web/zyt/admin/src/views
- Python 桌面端:D:/web/zyt/app/src/doctor_workstation
管理端 API 包装文件、通用组件和分页 hook 仅用于解析“页面已经发起的调用”的 URL、参数与直接子组件行为,不把相邻页面或未被页面调用的 API 当成处方页功能。
结论标签:
- EXACT:用户可见行为、状态语义和请求合同均与基准一致;不要求布局像素一致。
- PARTIAL:仅覆盖基准的子集,或字段/状态/校验语义有差异。
- MISSING:基准存在,而 Python 没有入口或没有可达实现。
- EXACT(负向):基准明确没有该能力,Python 也没有;不能把它列为待补功能。
## 2. 真实页面定位
| 业务名 | 管理端真实 view 组件 | Python 页面 | 结论 |
|---|---|---|---|
| 我的处方库 | D:/web/zyt/admin/src/views/consumer/prescription/list.vue:1-414script name 为 prescriptionLibrary234 | D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:251-545;固定导航见 D:/web/zyt/app/src/doctor_workstation/ui/shell.py:48-54 | EXACT(组件映射) |
| 已开处方 / 处方管理 | D:/web/zyt/admin/src/views/consumer/prescription/index.vue:1-4892script name 为 prescriptionList1697 | D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:72-436;固定导航见 D:/web/zyt/app/src/doctor_workstation/ui/shell.py:55-61 | EXACT(组件映射),功能不是 EXACT |
管理端采用动态菜单,单凭 src/views 只能确定组件键应分别落到 consumer/prescription/list 与 consumer/prescription/index,不能确定生产环境最终 URL、菜单标题或 route.meta.perms。Python 则固定注册为 prescription_library / prescriptions。审计不伪造管理端生产 URL。
## 3. 直接子组件与边界
### 3.1 MedicineNameSelect
处方库在 D:/web/zyt/admin/src/views/consumer/prescription/list.vue:158-165 使用;已开处方的主方、辅方行分别在 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:695-726、736-767 使用。
真实合同:
- 远程、可过滤选择器,提示支持药材名或拼音首字母:D:/web/zyt/admin/src/components/medicine-name-select/index.vue:3-24。
- 展开或搜索时 GET /doctor.medicine/lists,参数 name、page_no=1、page_size=100、status=1:同文件 69-92URL 包装见 D:/web/zyt/admin/src/api/medicine.ts:6-8。
- 选择后同时回写 name 和 medicine_id;不能把任意自由文本当成一次有效选择:组件 95-110。
Python 处方库的 HerbRow 是两个自由文本 QLineEditmedicine_id 只会保留已有行上的值,新增行没有药材主数据选择:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:76-110。结论:PARTIAL。
### 3.2 DaterangePicker
已开处方在 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:19-23 使用。直接子组件默认 datetimerange,值格式 YYYY-MM-DD HH:mm:ss,并分别回写 start_time/end_timeD:/web/zyt/admin/src/components/daterange-picker/index.vue:1-48。
Python 没有日期范围控件:MISSING。
### 3.3 TcmDiagnosisEditView
已开处方异步加载 D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue,并在 diagnosis_id>0 时提供“查看患者诊单详情”:D:/web/zyt/admin/src/views/consumer/prescription/index.vue:566-574、1692-1693、1728、2910-2921。
直接调用 openViewOnly(id);子页面以同一套界面只读打开、加载 GET /tcm.diagnosis/detail {id},默认病历 tabD:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1263-1264、1310-1322URL 见 D:/web/zyt/admin/src/api/tcm.ts:59-62。
该只读子页仍保留全套 tab 边界:
- 病历字段整体 disableddiagnosis/edit.vue:55-67。
- 医生备注 readonly、日常记录 read-only633-670。
- 处方 tab 受 tcm.diagnosis/chufang 控制且 read-only673-686。
- 业务订单受 tcm.diagnosis/patientOrders 控制:688-702。
- 视频回放、聊天、指派、挂号分别受 tcm.diagnosis/huifang、tcm.diagnosis/chat、tcm.diagnosis/assign 或 detail、doctor.appointment/lists 控制:704-758。
Python 已开处方没有 diagnosis_id 跳转或只读诊单子页:MISSING。
### 3.4 分页
两个管理端页面均通过 usePaging 自动发送 page_no/page_size,默认 page=1、size=15,并消费 count/lists/extendD:/web/zyt/admin/src/hooks/usePaging.ts:13-49。Python 两页均默认 page_size=20,使用 PageResult/Pager
- 处方库:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:263-266、413-442。
- 已开处方:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:84-89、284-316。
参数和总数分页机制 EXACT,默认每页条数 PARTIAL15 vs 20)。
## 4. 我的处方库:管理端真实合同
### 4.1 筛选、字段与状态
- 筛选:prescription_name、formula_type=主方|辅方、is_public=0|1,查询回第一页,重置为初始值:D:/web/zyt/admin/src/views/consumer/prescription/list.vue:5-31、247-252、281-284。
- 列表字段:id、prescription_name、formula_type、herbs.length、全部 herbs 的 name/dosage(g)、is_public、disable_edit、creator_name、create_timelist.vue:44-107。
- 公开范围:0=仅自己可见,1=所有人可见:list.vue:21-26、67-72、195-205。
- disable_edit:0=可修改,1=已禁用;真实语义是“导入到已开处方后锁定整张处方药材”,不是“禁止维护这条模板自身”:list.vue:74-79、208-220。
- 普通用户只管理本人 creator_idroot=1 或 role_ids 含 0、3 可管理任意模板:list.vue:276-301。
### 4.2 查看、创建、编辑、删除、复制
- 新增:独立按钮,表单字段为 prescription_namemaxlength=100)、formula_type、herbs、is_public、disable_editlist.vue:35-41、116-230、259-267。
- 查看:有单独 read 权限按钮,复用列表行数据打开完全禁用的表单;不调用 detaillist.vue:83-87、123-129、333-343。
- 编辑:仅 owner 或管理角色可见;仍可修改 disable_edit=1 模板本身的药材:list.vue:88-106、345-355。
- 删除:确认后 POST {id}list.vue:403-409。
- 复制:页面没有复制/克隆按钮、函数或端点。导入模板属于“已开处方”的复用流程,不是复制模板 CRUD。
- 导出/打印:本页面没有。
### 4.3 药材行和校验
- 药材 DTO{medicine_id?: number, name: string, dosage: number}list.vue:259-266。
- name 必须由 MedicineNameSelect 选取并同步 IDdosage 是 min=0、precision=1、step=0.5 的数字控件:list.vue:156-190。
- 提交要求:名称必填、至少一味药、每行 name 非空、dosage>0list.vue:269-274、357-379。
### 4.4 API DTO
页面调用来源见 list.vue:234-245URL 包装见 D:/web/zyt/admin/src/api/tcm.ts:591-615。
| 方法与端点 | 页面请求参数 / body | 页面使用情况 |
|---|---|---|
| GET /tcm.prescriptionLibrary/lists | page_no、page_size、prescription_name、formula_type、is_public | 列表与分页 |
| POST /tcm.prescriptionLibrary/add | prescription_name、formula_type、herbs、is_public、disable_edit;页面对象还带重置后的 id=0 | 新增 |
| POST /tcm.prescriptionLibrary/edit | id、prescription_name、formula_type、herbs、is_public、disable_edit | 编辑 |
| POST /tcm.prescriptionLibrary/delete | {id} | 删除 |
| GET /tcm.prescriptionLibrary/detail | {id} | API 已封装,但该 view 没有调用 |
权限码为 wcf.prescription/add、read、edit、deletelist.vue:36、85、93、102。
## 5. 我的处方库:Python 逐项对比
| 项目 | Python 证据 | 结论 |
|---|---|---|
| 页面入口 | shell.py:48-54 以 tcm.prescriptionLibrary/lists 注册;页面标题见 prescription_library.py:251-282 | EXACT |
| 筛选 | Python 有关键词、主/辅方、公开范围;repository 把 keyword 改成 prescription_name,把 main/aux 改为主方/辅方:prescription_library.py:284-315、413-429repository.py:279-298、586-588 | PARTIAL:请求合同基本一致,但“名称或药材”的占位文案(291)是假的,实际只查 prescription_name |
| 分页 | page/page_size 经 invoke 转 page_no/page_sizePager 消费总数:prescription_library.py:409-442widgets.py:221-223 | PARTIAL:默认 20,基准 15 |
| 列表字段 | Python 只显示名称、类型、最多 4 味摘要、公开范围、创建人、时间:prescription_library.py:357-384 | PARTIAL:缺 id、药材数量、完整药材、disable_edit;把 create_time 列标题写成“更新时间” |
| 单独查看 | 没有 read 按钮;双击只会走 _edit_selected,且需 edit 按钮可见及 owner/管理员:prescription_library.py:330-350、385-386、479-485 | MISSING |
| 新增 | _new_template -> save -> repository addprescription_library.py:474-501repository.py:309-323 | PARTIAL:端点可用,但药材和 disable_edit 语义不完整 |
| 编辑 | 使用列表行直接打开,与基准一样不请求 detail;保存到 editprescription_library.py:479-501repository.py:325-347 | PARTIAL |
| 删除 | owner/管理员检查、确认、POST {id}prescription_library.py:454-472、507-537repository.py:349-352 | EXACT |
| 公开范围 | 复选框产生 boolrepository 规范为 0/1prescription_library.py:157-159、218-228repository.py:572-575 | EXACT |
| disable_edit | Python 把它解释成“当前模板药材不可编辑”,隐藏增删并禁用输入;而管理端只在导入已开处方后锁药材。Python 也没有切换开关,只能原样回传:prescription_library.py:117-140、167-170、218-224 | PARTIAL(语义错误) |
| 药材选择 | 自由文本 name/dosage;新行无 medicine_id,已有行才保留:prescription_library.py:76-110 | PARTIAL |
| 药材校验 | 只检查至少一行、有名称时 dosage 非空:prescription_library.py:230-248 | PARTIAL:不校验数字和 >0,且允许“10g”字符串,与基准 number DTO 不同 |
| 所有权 | user id 相同、root、role 0/3prescription_library.py:460-472 | EXACT |
| API CRUD | list/detail/add/edit/delete 均存在且 URL、id 合同正确:repository.py:279-352 | EXACT(仓储层) |
| 模型 | PrescriptionTemplate 保留 id/name/formula/herbs/public/disable/creator/time/rawto_api_dict 正规化中文方型和 0/1core/models.py:426-477 | EXACT(字段合同) |
| 权限 | add/edit/delete 接受基准 wcf.*,同时接受 tcm.prescriptionLibrary/* 别名:prescription_library.py:274-279、332-347 | PARTIAL:动作基本受控,但缺 wcf.prescription/read 的可达只读查看;额外别名不是 view 基准 |
| 复制 | 两边都没有 | EXACT(负向) |
| 导出/打印 | 两边都没有 | EXACT(负向) |
## 6. 已开处方:管理端真实合同
### 6.1 筛选与分页 DTO
页面筛选控件见 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:4-99;状态对象见 1874-1887
- sn:处方编号模糊查询。
- patient_name。
- creator_ids:医生 ID 多选;有值才发数组。
- audit_filterall、pending、passed、not_passed、rejectedall 被正规化为空字符串。
- source_filterall、manual、systemall 被正规化为空字符串。
- start_time/end_time:创建时间,格式 YYYY-MM-DD HH:mm:ss。
- 快捷日期:全部、今日、昨日、前天,切换后立即回第一页查询:index.vue:3013-3048。
最终 GET /tcm.prescription/lists 参数是 page_no、page_size 与上述字段;creator_ids 空数组会被省略:index.vue:2990-3007。默认 page_size=15。
### 6.2 列表字段、状态与可达动作
列表与按钮位于 index.vue:102-270
- 列:sn、id、订单一致性警告、prescription_type、is_system_auto1=空白处方,其他=手工)、patient_name/gender/age、综合审核状态和驳回意见、void_status、doctor_name/prescription_date、assistant_name、create_time。
- audit_status0 待审核、1 已通过、2 已驳回:index.vue:3113-3123。
- business_prescription_audit_rejected=1 时,列表主状态覆盖为“已驳回”,并分别展示业务订单与消费者处方的驳回意见:index.vue:3125-3151。
- void_status=1 单独显示“作废”。
- 已有关联业务订单时,药材为空或重名会在编号下给出警告:index.vue:3192-3216。
动作门槛:
- 查看:cf.prescription/read。
- 修正姓名/性别/手机:未作废且 tcm.prescription/patchPatient。
- 创建订单:has_prescription_order=0、未作废且 tcm.prescriptionOrder/create。
- 编辑、删除:只有“非 audit_status=1 且未作废”才出现,分别需 cf.prescription/edit、cf.prescription/del。
- 审核:audit_status=0、未作废且 cf.prescription/audit。
- 业务订单列表:tcm.prescriptionOrder/lists。
精确条件和权限见 index.vue:214-264;“已通过且未作废”判定见 3154-3157。
### 6.3 查看详情、打印与 PDF
查看先用列表行渲染,再 GET /tcm.prescription/detail {id} 覆盖:index.vue:3796-3817。
A4 处方笺显示:
- 来源、作废、消费者审核、业务订单审核及意见:index.vue:284-343。
- 日期/编号;患者姓名、性别、年龄、电话、收件信息、临床诊断:345-390。
- 主方/辅方药材,单剂 dosage 与 dose_count 计算后的总量:392-433。
- 主辅方用法、医嘱、忌口、备注、药房备注、出丸:435-448。
- 医师手写签名、医生、类型、天数/剂数、单剂量:450-481。
- 审核人、审核时间、审核意见:484-496。
打印调用 window.printindex.vue:2741-2751。PDF 用 html2canvas(scale=2) 渲染 A4,再由 jsPDF 分页并下载:index.vue:2753-2813。页面没有“列表导出 Excel”;tcm.ts:384-387 的业务订单导出 API 没有被此 view 导入或调用。
### 6.4 新增/编辑字段与药材规则
表单位于 index.vue:499-1153reactive DTO 在 2815-2862
- 关联/系统:id、diagnosis_id、creator_id、is_system_auto。
- 患者:patient_name、gender、age、visit_no、prescription_date。
- 四诊/诊断:tongue、tongue_image、pulse、pulse_condition、clinical_diagnosis。
- 药材:herbs[{medicine_id?, name, dosage, formula_type=主方|辅方, locked?}]。
- 剂型:prescription_type(浓缩水丸、饮片、颗粒、丸剂、散剂、膏方、汤剂)、dosage_amount、dosage_unit、dosage_bag_count、need_decoction、bags_per_dose、dose_count、dose_unit。
- 主方用法:usage_days、times_per_day、usage_instruction、usage_time、usage_way、dietary_taboo[]、usage_notes。
- 辅方用法 aux_usagedosage_amount、dosage_bag_count、need_decoction、bags_per_dose、times_per_day、usage_days、prescription_name。
- 医师:doctor_name、doctor_signature(手写 canvas 生成 PNG data URL,必填)。
- 隐藏状态:is_shared、visible_role_ids、audit_status/time/by/remark、业务订单审核字段。
主辅方各自使用 MedicineNameSelect 和数字 dosage;导入 disable_edit=1 模板时给行加 locked 并锁定整张处方药材:index.vue:667-813、3489-3515、3712-3732。
新增/编辑校验:
- patient_name、gender、prescription_date、clinical_diagnosis、dose_count、doctor_name、doctor_signature 必填:index.vue:2955-2988。
- 至少一味药,每行 name 非空、dosage>03920-3947。
- 新增提交前强制 audit_status=0add/edit 都把完整 editForm 展开为 body3951-3961。
- 编辑提示保存后重新进入待审核;已作废/已驳回可经编辑恢复,业务订单侧审核会重置:index.vue:514-565。
公开/共享范围的特殊事实:is_shared、visible_role_ids 虽存在于 editForm、详情回填和展开 payload2846-2847、3889-3890、3952),但模板区没有任何 v-model 控件;formatVisibleRoleNames 也未被模板调用(3348-3364)。因此该真实 view 不允许用户设置共享范围。桌面端不应凭字段存在擅自新增 UI。
### 6.5 处方库导入、粘贴导入与“复制”
- 从处方库导入对话框支持处方名称、主/辅方筛选、replace/append、10/15/20/50 分页,展示公开范围和创建人:index.vue:1539-1633。
- 请求 GET /tcm.prescriptionLibrary/lists,参数 page_no、page_size、prescription_name、formula_type、prescribing_creator_idindex.vue:3444-3468。
- prescribing_creator_id 取当前开方医生;语义是“该医生自己的模板 + 公共模板”。
- 粘贴导入解析 name/dosage,只接受 GET /doctor.medicine/lists 中 name 完全相同且唯一的药材;请求 name、page_no=1、page_size=200、status=1index.vue:3538-3709。
- replace 只替换同方型药材,append 追加;重复名仅警告。
该页面没有“复制某张已开处方”动作或端点。处方库导入与文本导入是新增/编辑中的填充方式,不等于复制已开处方。Python 也没有复制:EXACT(负向)。
### 6.6 审核联动
- POST /tcm.prescription/audit body={id, action:'approve'|'reject', remark}D:/web/zyt/admin/src/api/tcm.ts:365-372;调用见 index.vue:3367-3395。
- reject 必填 remark,成功语义为“驳回并作废处方”:index.vue:1669-1689、3373-3385。
- 返回 wecom_notify_ok=false 且有 wecom_notify_hint 时额外警告:3384-3388。
- 页面定义 root/role_ids 0、3 的审核角色辅助函数,但该函数没有被模板使用;真实按钮门槛仍是 v-perms cf.prescription/audit 加行状态:index.vue:1854-1855、247-253、3104-3110。
### 6.7 患者修正与订单联动
患者修正:
- 对话框只改 patient_name、gender、phone,明确不改审核状态,并记录到业务订单日志:index.vue:1155-1195。
- POST /tcm.prescription/patchPatient body={id,patient_name,phone,gender}index.vue:1948-1973API 类型见 tcm.ts:330-338。
订单读取与一致性:
- 编辑处方时 GET /tcm.prescriptionOrder/lists,参数 page_no=1、page_size=5、prescription_id,显示最近订单的 order_no、medication_days、remark_assistantindex.vue:769-811、2879-2907。
- 业务订单审核驳回可以覆盖处方列表状态和处方笺状态,但保留消费者处方本身的 audit_statusindex.vue:165-190、295-343、3125-3151。
从处方创建业务订单:
- 三步表单:诊单患者与收件信息;服务/发货/支付单;费用与备注:index.vue:1197-1537。
- 诊单患者远程搜索意图为 GET /tcm.diagnosis/searchPatient,参数 keyword、page_no=1、page_size=10index.vue:2384-2401API 定义在 D:/web/zyt/admin/src/api/order.ts:88-91。
- GET /tcm.prescriptionOrder/paidPayOrders {diagnosis_id} 返回可关联支付单和 deposit_min_amountindex.vue:2307-2329API 见 tcm.ts:389-398。
- POST /tcm.prescriptionOrder/create 的 body
prescription_id、diagnosis_id、recipient_name、recipient_phone、shipping_address、is_follow_up、prev_staff、service_channel、service_package(逗号拼接)、express_company、tracking_number、ship_mode、fee_type、amount、remark_extra、remark_assistant;可选 shipping_province/city/district、medication_days、internal_cost、pay_order_ids。证据:index.vue:2435-2485URL 见 tcm.ts:374-382。
- ship_mode 选择需 tcm.prescriptionOrder/setShipModeinternal_cost 需 finance.account_log/listsremark_extra 需 tcm.prescriptionOrder/editRemarkExtraindex.vue:1307-1314、1466-1488、2041-2043。
- 服务套餐来自 GET /config/dict {type:'server_order'}index.vue:2180-2187URL 见 D:/web/zyt/admin/src/api/app.ts:13-16。
## 7. 已开处方:Python 逐项对比
D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:1 已明确把本页定义为 “Read-only list and detail view for issued prescriptions”。这不是完整管理端主链。
| 项目 | Python 证据 | 结论 |
|---|---|---|
| 页面入口 | shell.py:55-61,以 tcm.prescription/lists 控制 | EXACT |
| 筛选 | 只有一个“编号/患者”关键词和 audit 0/1/2prescriptions.py:101-125、284-303 | PARTIAL |
| 请求映射 | repository 根据 RX/CF/纯数字猜 sn,否则 patient_namestatus 转 pending/passed/rejectedrepository.py:354-387 | PARTIAL:已有控件映射正确;缺 creator_ids、日期、source、not_passed;同一关键词不能同时查编号与患者 |
| 分页 | PageResult/Pager 完整 | PARTIAL:默认 20,基准 15 |
| 列表字段 | sn、patient、audit、source、doctor、prescription_dateprescriptions.py:139-180 | PARTIAL:缺 id、prescription_type、gender/age、业务/消费者驳回意见、void 独立列、assistant、create_time、订单警告 |
| 来源 | is_system_auto -> “系统代开/医生开具”:prescriptions.py:67-70 | PARTIAL:基准语义/文案是“空白处方/手工” |
| 状态 | 先作废,再业务订单驳回/消费者驳回,再通过/待审;raw fallback 可读模型未声明字段:prescriptions.py:46-60widgets.py:48-66 | PARTIAL:主标签逻辑接近,但不能分别显示两类驳回意见、审核人/时间和独立作废信息 |
| 详情请求 | 选中后 GET /tcm.prescription/detail {id}prescriptions.py:326-357repository.py:389-396 | EXACT(端点) |
| 详情字段 | 患者/性别/年龄、医生、日期、类型、剂数、药材/剂量/方型、频次/天数/方式/时间/忌口/说明和一条审核备注:prescriptions.py:360-420 | PARTIAL |
| A4 处方笺 | 无 A4 版式;缺电话/收件、临床诊断、签名、单味总量、药房备注/出丸、审核轨迹等 | MISSING |
| 新增 | 页面无按钮、无表单;RemoteRepository 无 prescription add 方法 | MISSING |
| 编辑 | 页面无按钮、无表单;缺主/辅方药材、剂型用法、签名、重新待审完整流程 | MISSING |
| 删除 | 无按钮;RemoteRepository 无 delete | MISSING |
| 患者修正 | 无 patchPatient UI/仓储方法 | MISSING |
| 审核 | 无 approve/reject UI/仓储方法;无驳回必填、作废联动、企微提示 | MISSING |
| 订单联动 | 无订单列表入口、创建订单、支付单关联、处方一致性警告、最近订单提示 | MISSING |
| 处方库导入 | 无新增/编辑,自然也没有 replace/append、锁药材、prescribing_creator_id | MISSING |
| 文本导入 | 无解析及药材完全同名验证 | MISSING |
| 药材编辑行 | 详情表只读;无 medicine_id 选择与 dosage 校验 | MISSING |
| 打印 | 无 window/Qt 打印等价能力 | MISSING |
| PDF | 无处方笺 PDF 导出 | MISSING |
| 列表 Excel | 基准页也没有 | EXACT(负向) |
| 复制已开处方 | 基准页也没有 | EXACT(负向) |
| 共享范围 UI | Python 不展示 is_shared/visible_role_ids;基准 view 也没有可见控件 | EXACT(只读 UI 的负向行为) |
| 共享字段模型 | Prescription 存 is_shared、visible_role_ids,但没有 mutationcore/models.py:519-521、574-576 | PARTIAL(数据保留) |
| 诊单详情联动 | 没有 diagnosis_id “查看诊单详情”和全 tab 只读子页 | MISSING |
| 动作权限 | 构造器保存 permissions 但页面没有任何 has_permission 动作判断:prescriptions.py:72-99 | MISSING(动作本身也缺) |
### 7.1 Python Prescription DTO 覆盖度
Prescription 模型保留绝大多数列表/详情核心字段:id、diagnosis、sn、患者、四诊、药材、剂型/主方用法、aux_usage、签名、creator/assistant、source/share/audit/void/order/create_time,并保留 rawD:/web/zyt/app/src/doctor_workstation/core/models.py:480-587。
结论:PARTIAL。
原因:
- 模型层比 UI 完整,但 UI 只消费很小子集。
- business_prescription_audit_rejected、business_prescription_audit_remark、收件/药房/出丸等不设显式 dataclass 字段,只能从 raw 兜底;get_value 确实支持 raw fallbackwidgets.py:48-66。
- RemoteRepository 只实现 GET lists/detailrepository.py:354-396;没有 add/edit/delete/patch/audit/order 的请求合同。
## 8. 权限码总表
### 8.1 我的处方库
| 能力 | 管理端真实码 | Python |
|---|---|---|
| 页面/列表 | 动态菜单实际值不能由 view 单独确定;接口是 tcm.prescriptionLibrary/lists | shell 固定 tcm.prescriptionLibrary/lists |
| 新增 | wcf.prescription/add | 接受 wcf.prescription/add 或 tcm.prescriptionLibrary/add |
| 查看 | wcf.prescription/read | MISSING |
| 编辑 | wcf.prescription/edit + owner/root/role 0,3 | 同时接受 wcf 或 tcm 别名 + 同一所有权规则 |
| 删除 | wcf.prescription/delete + owner/root/role 0,3 | 同时接受 wcf 或 tcm 别名 + 同一所有权规则 |
### 8.2 已开处方
管理端真实页面使用:
- cf.prescription/add、read、edit、audit、del。
- tcm.prescription/patchPatient。
- tcm.prescriptionLibrary/lists。
- tcm.prescriptionOrder/create、lists、setShipMode、editRemarkExtra。
- finance.account_log/lists。
- 诊单子页另用 tcm.diagnosis/chufang、patientOrders、huifang、chat、assign/detail 和 doctor.appointment/lists。
证据集中在 index.vue:102-117、214-264、667-680、1307-1314、1466-1488、1692-1728。
Python 只有页面入口 tcm.prescription/listsshell.py:55-61);PrescriptionsPage 没有上述动作,也没有动作权限判断。结论:MISSING。
## 9. 基准 view 自身的边界/风险(不计作 Python 差异)
1. 订单患者搜索在 index.vue:2391 调用 searchPatientsAPI,但 index.vue:1698-1714 的显式 import 没有该符号;真正 export 位于 api/order.ts:88-91。若工程没有为这个本地 API 符号做自动导入,远程患者搜索会失败。仅以 views 不能证明是否存在额外自动导入,故结论是“高风险未确认”,不是把不存在的合同补到 Python。
2. is_shared、visible_role_ids、roleAll、formatVisibleRoleNames 存在,但没有表单绑定;不要把死状态误判成可见“公开范围”功能。
3. userCanAudit() 定义了 root/role 0,3,但没有被模板调用;真实前端门槛是 cf.prescription/audit 和行状态,后端仍是最终边界。
4. prescriptionVoid API 虽在 tcm.ts:360-363 存在,但此 route 没有导入/直接调用;该页的可达作废路径是 audit reject 的服务端联动。
5. prescriptionOrderExport API 存在,但“已开处方” route 不调用;不能据此声称本页有 Excel 导出。
6. 两个基准页面均无“复制”能力。
## 10. P0 / P1 缺口
### P0
1. 已开处方主链整体缺失:新增、编辑、删除及其 RemoteRepository POST 合同均不存在。当前 Python 实现是明确的只读页,无法替代管理端真实路由。
2. 医疗安全关键编辑合同缺失:药材必须来自 MedicineNameSelect、主辅方和 locked 模板规则、dosage>0、剂型/用法、临床诊断、手写医生签名、编辑后重入待审,Python 全部不可达。
3. 审核/作废闭环缺失:approve/reject、驳回意见必填、reject 同时作废、两类审核意见、企微通知提示均不存在。
4. 订单联动缺失:患者修正、订单创建、支付单关联/定金门槛、发货模式权限、订单审核驳回覆盖状态、处方/订单药材一致性提示全部不存在。
5. 权限闭环缺失:已开处方除了列表入口没有任何 cf.prescription/*、patchPatient、prescriptionOrder/* 动作校验;在补动作前必须按基准精确落码,不能以 lists 权限兜底。
### P1
1. 已开处方列表筛选不完整:缺医生多选、创建时间/快捷日期、来源、not_passed;关键词还通过内容猜测 sn 或 patient_name。
2. 已开处方列表/详情展示不完整:缺业务订单警告、两类驳回意见、独立作废/审核轨迹、收件/电话、临床诊断、医生签名、药房备注/出丸及 A4 处方笺。
3. 已开处方缺打印和 A4 PDF 下载。
4. 已开处方缺 diagnosis_id 只读诊单子页及其权限化 tabs。
5. 处方库缺真正的 wcf.prescription/read 查看路径;只有有编辑权且可管理该行的用户能通过编辑对话框看完整内容。
6. 处方库药材行应改为药材远程选择与数字 dosage;当前自由文本允许合同无效值。
7. 处方库 disable_edit 语义错误且无切换开关:它应控制“导入后的已开处方药材锁定”,不应锁死模板自身编辑。
8. 处方库表格缺 id、药材数、完整明细、disable_edit,且时间列标题与数据不一致。
9. 两页默认 page_size 与基准不同(20 vs 15);属于低风险但可见的分页差异。
“复制”不是缺口:两个管理端基准页面均没有复制动作,Python 也没有。
@@ -0,0 +1,267 @@
# 接诊台 / 问诊列表:管理后台到医生桌面的逐项一致性审计
审计日期:2026-08-10
基准:`D:\web\zyt\admin\src\views` 中从现行路由可达的 Vue/TS 实现
对比对象:`D:\web\zyt\app\src\doctor_workstation` 当前 Python 实现
约束:本次只读审计;未修改 `src/`,未发网络请求。
## 1. 结论摘要
当前 Python 不是管理后台两个页面的等价移植,而是“接诊队列 + 简化问诊表 + 原生直呼视频”的医生端子集。端点骨架中,挂号列表、接诊聚合详情、通知、添加文字备注、完成接诊、诊单列表以及 `getCallSignature/startCall/bindCallRoom/endCall` 已接通;但今日队列边界、异步选人一致性、挂号状态判定、病例详情/编辑、开方、日常记录、备注媒体、复杂筛选和操作权限仍有显著差异。
发布阻断级结论:
1. **P0 — 接诊台快速切换患者会出现旧患者详情挂在新患者选择下。** 管理后台每次选人递增请求序号并同时校验序号和 `selectedId``D:\web\zyt\admin\src\views\patient\reception\index.vue:398-418`);Python 在任一详情请求进行中时直接拒绝下一次加载(`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:406-418`),旧响应只按旧 generation 接受(同文件 `:437-447`)。这会把显示、备注目标和当前队列行拆成两个患者上下文。
2. **P0 — Python 接诊队列没有“今天”条件。** 基准固定发送 `start_date=end_date=today``patient/reception/index.vue:296-305`);Python 只发送 `status/keyword/page/page_size``reception.py:344-361`)。因此历史或未来的状态 1/4 记录可能进入“今日接诊台”,并可触发通知、备注、视频和完成。
3. **P0 — 问诊列表的视频门槛读取了错误的状态域。** 基准以 `has_appointment && appointment_status===1` 为唯一可视频条件(`tcm/diagnosis/index.vue:1778-1780`H5 同口径 `index_h5.vue:949-950`)。Python 读取诊单模型的 `status`,并允许 1 或 4`consultations.py:310-317`);模型又优先取根 `status`,而不是根 `appointment_status``core/models.py:362-366,406-414`)。结果是已完成/过号记录可能仍可直呼,合法已预约记录也可能被误禁用。
其余 P1 缺口集中在:完整病例核对与只读详情、诊单编辑、从诊单开方、接诊详情字段/日常记录/备注附件、手机号脱敏、动作权限契约,以及问诊列表的筛选/状态/多挂号语义。详见第 8 节。
## 2. 判定口径与路由范围
状态含义:
- **exact**:端点、关键参数、状态门槛和结果语义均一致;不要求 Qt 与 Web 视觉相同。
- **partial**:已有对应能力,但参数、字段、权限、状态或边界行为不完整。
- **missing**:基准可达的医生核心能力在 Python 中没有入口或 repository 合同。
- **intentionally unsupported**:明确属于 H5 布局、小程序二维码或医助/后台运营角色,当前原生医生桌面选择了不同交互或没有承载;这是审计分类,不代表产品已正式批准删除。
路由证据:
- 管理后台主菜单是服务端动态路由:Vite 收集所有 `views/**/*.vue`,菜单组件名交给 `loadRouteView` 匹配(`D:\web\zyt\admin\src\router\index.ts:9-15,28-69`)。因此接诊台主文件是 `views/patient/reception/index.vue`,问诊列表 PC 主文件是 `views/tcm/diagnosis/index.vue`
- `/tcm/diagnosis/h5` 静态指向 `index_h5.vue``D:\web\zyt\admin\src\router\routes.ts:82-85`)。
- PC 问诊列表的“查看”与双击跳转隐藏路由 `/tcm/diagnosis-readonly?id=...``tcm/diagnosis/index.vue:2165-2172`),静态路由指向 `readonly.vue``router/routes.ts:87-102`)。
- `index.vue.bak` 的后缀不是 `.vue`,不会进入 `import.meta.glob('/src/views/**/*.vue')``add.vue` 没有被现行两个入口导入;两者均未作为基准。PC 虽挂载 `detail.vue`,却没有存活的 `handleDetail` 调用;该弹窗仅由 H5 卡片点击实际触发(`index_h5.vue:121,144,264-268,1013-1015`)。
Python 页面级门控与主入口一致:接诊台使用 `doctor.appointment/lists`,问诊列表使用 `tcm.diagnosis/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:40-47,69-75,248-264`),判定为 **exact**
## 3. 实际可达文件与依赖图
以下只列业务 Vue/TS 和决定行为的共享文件,不展开 Element Plus、基础 popup/upload 等纯框架组件。
### 3.1 接诊台
- `views/patient/reception/index.vue`:队列、轮询、详情、通知、完成、快捷文字备注、通话入口(导入证据 `:191-210`)。
- `views/patient/reception/components/NoteTimeline.vue`:备注文本、舌苔图片、检查报告、单附件删除(`:1-121,125-132`)。
- `views/tcm/diagnosis/components/PatientInfoCard.vue`:脱敏手机号、预约与人员摘要(`:1-41`)。
- `views/tcm/diagnosis/components/PatientCaseCard.vue`:病例全字段和生命体征阈值(`:1-124,128-153`)。
- `views/tcm/diagnosis/components/DailyMatrix.vue``DiagnosisTodoList.vue`:7/30/自定义日期窗、血糖血压/饮食/运动/跟踪备注/待办(`DailyMatrix.vue:317-332,342-470`)。
- `components/chat-dialog/index.vue``ChatMessageItem.vue` 及本地录制/截图/IM 工具:实时聊天与通话、通话落库、房间绑定、结束、可选本地录制;API 导入见 `chat-dialog/index.vue:129-175`
- `views/tcm/diagnosis/edit.vue` 及其下述诊单详情依赖:`DailyMatrix.vue``CaseRecordList.vue``CallRecordPanel.vue``RecordingPlaybackBlock.vue``RecordingVideoPlayer.vue``ImChatRecordPanel.vue``AssignLogPanel.vue``AppointmentRecordPanel.vue``NoteTimeline.vue``TrackingNoteTimeline.vue``PatientOrderList.vue``PrescriptionOrderDetailDrawer.vue``components/tcm-prescription/index.vue`(导入证据 `edit.vue:788-806`)。
### 3.2 问诊列表
- `views/tcm/diagnosis/index.vue`:PC 列表、全部筛选/角标、行操作、20 秒静默轮询(导入及异步组件 `:727-746`)。
- `views/tcm/diagnosis/index_h5.vue`:静态 H5 路由;复用同一 edit/detail/prescription/appointment/watch-call 组件(`:588-630`)。原生 Python 不需要复刻 H5 布局,标为 **intentionally unsupported**;但它证明的业务状态和动作边界仍计入对比。
- `views/tcm/diagnosis/readonly.vue`:PC 隐藏详情路由;复用 PatientInfo/Case、DailyMatrix、备注、通话回放、IM、指派、挂号、业务订单组件(`:151-167`)。
- `views/tcm/diagnosis/detail.vue`H5 卡片的简版诊单详情(`:1-165`)。
- `views/tcm/diagnosis/edit.vue`:新增/编辑/只读抽屉及所有扩展 Tab。
- `components/tcm-prescription/index.vue`:按诊单开方、查已有处方、作废、处方库导入和 PDF。
- `views/tcm/diagnosis/appointment.vue`:预约、排班、号源、渠道和今日重复挂号边界。
- `views/tcm/diagnosis/components/AssistantWatchCallDialog.vue`:医助 TRTC 只拉流旁观(`:23-26,120-219`)。
- 决定字段与边界的 TS`hooks/usePaging.ts``:13-58`)、`utils/perm.ts``:3-17`)、`install/directives/perms.ts``:13-32`)、`utils/diag-display.ts``utils/blood-thresholds.ts``utils/diabetes-discovery-display.ts`
## 4. 接诊台逐项差异
| 项目 | 基准证据 | Python 证据 | 判定 | 差异/影响 |
|---|---|---|---|---|
| 页面权限 | 动态菜单页面权限为 `doctor.appointment/lists` | `shell.py:40-47,248-264` | exact | 页面级一致。 |
| 状态队列 | 仅 status 1“待接诊”和 4“已过号”(`index.vue:33-45,235` | 两个 QTab 映射 1/4`reception.py:152-156,334-336`) | exact | 状态集合一致。 |
| 今日范围 | 每次请求带当天 `start_date/end_date``index.vue:296-305` | 未发送日期(`reception.py:344-361` | missing / P0 | 非今日记录污染工作台并暴露写动作。 |
| 搜索 | `patient_name`,清空也自动搜索(`index.vue:20-32,296-305,431-436`) | UI 只在回车/搜索按钮触发;repository 将 `keyword` 改为 `patient_name``reception.py:159-168,352-360`; `repository.py:231-236` | partial | 请求字段一致;清空不自动刷新。 |
| 分页与数量 | page size 15、另一状态独立 count、无限滚动、去重合并(`index.vue:212-213,296-318,359-379,606-620`) | 固定第一页 50 条,仅显示已加载条数(`reception.py:344-386` | partial | 50 条以上不可见;没有 waiting/passed 总数或加载更多。 |
| 轮询 | 5 秒链式轮询、倒计时、队列/详情/双 count、防重入、页面隐藏暂停/恢复立即刷新(`index.vue:254-259,539-592`) | 8 秒 QTimer,只刷新当前队列;页面 QWidget 隐藏时停(`reception.py:138-140,663-672` | partial | 无倒计时/另一状态 count;应用最小化不等价于 `document.hidden`;周期不同。 |
| 异步选人 | 每次选人递增 seq、清空详情、结果校验 seq + selected id`index.vue:398-418`) | 请求进行中拒绝新选人;旧结果仍应用(`reception.py:406-418,437-447` | missing / P0 | 可显示旧患者详情并对新选择行执行其他动作。 |
| 队列行字段 | id、patient/diagnosis/doctor/assistant、日期时间、性别年龄、status、处方、remark`index.vue:215-233`);实际显示姓名、性别年龄、过号、时间、医助(`:57-90` | 显示同一核心子集(`reception.py:50-95`) | exact | 行上显示字段基本一致。 |
| 聚合详情端点 | GET `/doctor.appointment/reception?id``api/patient.ts:9-12` | `get_reception()` 同端点/参数(`repository.py:242-250`) | exact | 传输合同一致。 |
| 患者/病例字段 | PatientInfo 显示脱敏电话、性别年龄、身高体重地区、预约、医生/客服、状态/开方、remark(`PatientInfoCard.vue:3-25`);PatientCase 显示基本信息、生命体征、糖尿病史、现病史、既往/其他病史、处方意见(`PatientCaseCard.vue:8-124`) | 仅预约时间/电话/医生/医助,加主诉、临床诊断、舌脉、治则摘要和开方提示(`reception.py:241-285,471-516` | partial / P1 | 大量临床字段缺失;无法达到基准的接诊前核对深度。 |
| 手机脱敏 | 两张卡都调用 `maskPhone``PatientInfoCard.vue:8`; `PatientCaseCard.vue:15` | 直接显示 `patient_phone/phone/phone_masked` 中第一个非空值(`reception.py:478-480` | missing / P1 | API 若下发明文即展示;未受 `tcm.diagnosis/phonePlain` 控制。 |
| 医生备注读取 | 聚合详情 `doctor_notes`,编辑/只读还会 GET `/doctor.appointment/doctorNotes` 并保留详情兜底(`readonly.vue:177-217`; `api/patient.ts:29-32` | 仅消费聚合详情中的 `doctor_notes/notes``reception.py:518-548`);Remote 无 `doctorNotes` 方法 | partial | 无独立补拉;备注元数据只显示 creator/time,忽略 `note_date` 语义。 |
| 文字备注写入 | `diagnosis_id + trimmed content`,最多 500 字,空值/无诊单拒绝(`index.vue:175-181,514-536` | 同端点和空值/诊单检查(`reception.py:575-601`; `repository.py:257-272`),但 QTextEdit 无 500 字限制 | partial | 端点 exact;客户端长度边界缺失。 |
| 舌苔/报告 | NoteTimeline 支持最多 99 个图片/文件、新增后刷新、单附件确认删除(`NoteTimeline.vue:3-40,52-101,195-227,260-268`; `api/patient.ts:19-40` | repository 可发送 `tongue_images/report_files`,但 UI 不展示附件、无上传/预览/删除入口(`repository.py:257-272`; `reception.py:524-548` | missing / P1 | 核心检查资料不可见、不可维护。 |
| 日常记录 | 只读 DailyMatrix,按诊单/患者加载(`index.vue:129-139`);7/30/自定义窗、trackingWindow/trackingNotes`DailyMatrix.vue:410-458,550-564,838-875` | 无 UI、模型和 repository 合同 | missing / P1 | 接诊时看不到血糖血压、饮食、运动、跟踪备注及阈值。 |
| 编辑病历 | `tcm.diagnosis/edit` 权限后打开 `edit.vue``index.vue:155-161,474-480` | 无按钮、无 `tcm.diagnosis/detail/edit` repository 方法 | missing / P1 | 接诊工作台不能维护病历。 |
| 通知医助 | POST `/doctor.appointment/notifyAssistant {id}`,防重入、成功后刷新队列与详情(`index.vue:460-471`; `api/patient.ts:14-17`) | 端点/防按钮重入一致,但只 toast、不刷新(`reception.py:558-573`; `repository.py:252-255` | partial | 后端动作 exact;页面不会立即反映服务端副作用。 |
| 完成接诊 | 仅 apt.status 1/4,权限 `doctor.appointment/complete`,不可逆确认,POST `{id: apt.id}` 后清选中并重载(`index.vue:162-171,492-511`; `api/doctor.ts:84-87` | 同权限、确认和端点(`reception.py:326-328,610-641`; `repository.py:274-277` | partial | 正常队列内近似 exact;受“非今日队列”和错患者详情竞态放大风险,UI 本身未再校验状态。 |
| 通话标识 | 缺 patient_id 拒绝;`diagnosis_id = row.diagnosis_id || row.id``index.vue:438-453` | app 同时要求 patient/diagnosis idpayload 优先真实 diagnosis_id`reception.py:643-661`; `app.py:496-537` | exact | ID 未互换。 |
| 通话生命周期 | ChatDialog 调 get signature、start、bind、end`chat-dialog/index.vue:641-695,996-1032,1089-1096` | Remote 同四端点(`repository.py:443-483`),FIFO 生命周期按 start→bind→end`video/lifecycle.py:184-209,211-263,265-292` | exact(核心) | Python 的核心通话落库顺序一致。 |
| 通话扩展 | ChatDialog 还有 IM、截图写备注、云/本地录像上传(`chat-dialog/index.vue:129-175,241-265,1276-1301` | 原生 companion 未提供这些管理后台附属入口 | partial | 核心视频可用;聊天、截图备注和本地录像回填不等价。 |
### 接诊台权限差异
- 基准悬浮备注/编辑/完成分别使用 `doctor.appointment/addDoctorNote``tcm.diagnosis/edit``doctor.appointment/complete``patient/reception/index.vue:146-171`)。Python 已实现备注与完成门控,但编辑缺失(`reception.py:322-331`)。
- 基准队列行“通知医助”和“发起通话”没有 `v-perms``index.vue:75-90`)。Python 额外要求 `doctor.appointment/notifyAssistant``tcm.diagnosis/videoQr``reception.py:322-325`)。后者在基准中是“小程序视频二维码”权限,不是直呼权限;这可能把拥有接诊页但没有二维码权限的医生挡在视频之外,判为 **partial / P1**
- NoteTimeline 的“添加文字备注”检查 addDoctorNote 权限,但两个媒体 picker 和删除图标只受 readonly 控制(`NoteTimeline.vue:3-40,64-99,150-151`)。这是基准自身的权限不对称,不能据此推导 Python 应无条件开放媒体写入。
## 5. 问诊列表逐项差异
### 5.1 列表、筛选与分页
| 项目 | 基准 | Python | 判定 |
|---|---|---|---|
| 列表端点 | GET `/tcm.diagnosis/lists``api/tcm.ts:3-6` | 同端点(`repository.py:414-440` | exact |
| 分页 | `usePaging` 默认 page size 15,发送 `page_no/page_size``usePaging.ts:13-21,25-48` | page size 20、PageResult 容错(`consultations.py:93-95,264-300` | partial |
| 默认范围 | PC/H5 均默认 `appointment_date=today``index.vue:2174-2182`; `index_h5.vue:1427-1437`) | 默认日期同为今天;Remote 在起止相等时转换为 `appointment_date``consultations.py:129-141,248-251,264-282`; `repository.py:420-424` | exact(日期) |
| 默认状态 | 基准默认今天的所有挂号语义,不额外传 appointment status | Python status 下拉默认 1,并转成 `appointment_status=1``consultations.py:121-128,276-279`; `repository.py:429-434` | partial / P1 |
| 关键词 | `keyword`,提示姓名/手机号(`index.vue:72-75,773-775` | 同字段(`consultations.py:116-120,276` | exact |
| 日期快捷项 | 前天、昨天、今天、明天、后天、全部;另有待预约、已完成、待分配及 9 路角标(`index.vue:963-1085`) | 单个起止日期控件和“今天”;无角标 | partial |
| 挂号/确认 | `has_appointment` 0/1`diagnosis_confirmed` 0/1`index.vue:78-97,804-833` | 无这两个筛选 | missing |
| 诊断/证型/医助 | 字典筛选(`index.vue:104-113,1158-1188` | 无 | missing |
| 最近挂号 | 起止日期 + 渠道,且与 appointment_date/未挂号互斥(`index.vue:114-149,1197-1228` | 多日 start/end 被隐式映射为 latest appointment 起止,但无渠道、无互斥提示(`repository.py:420-428` | partial |
| 最近指派 | 起止日期(`index.vue:124-133,1203-1216,1230-1232` | 无 | missing |
| 未服务排序 | `sort_unserved_days=asc/desc``index.vue:297-304,1234-1248` | 无 | missing |
| 待分配宽搜 | 有关键词时只保留 page、pending_assign 和 pending_assign_keyword,主动清空其他筛选(`index.vue:836-861,923-954` | 无 | intentionally unsupported(医助分配) |
| 定时刷新 | PC 20 秒且 document.hidden 时跳过(`index.vue:1098-1105`);H5 15 秒(`index_h5.vue:1418-1437` | 20 秒,页面 QWidget 隐藏时停(`consultations.py:238-240,325-334` | exactPC 周期)/ partial(可见性语义) |
| 路由 query id | `?id=` 会在异步 edit ref 就绪后打开编辑(`index.vue:1343-1357,2184-2188` | 无 deep-link | missing |
Python 的“状态下拉”不是基准筛选:它提供待接诊/取消/完成/过号(`consultations.py:121-127`),Remote 再把 3 映射为 `completed_appointment=1`,其他值映射成 `appointment_status``repository.py:429-434`)。基准 PC 没有该下拉;“已完成”定义为至少有一条 appointment.status=3`index.vue:1007-1020`),取消状态也不在展示映射中。因此这里只能判 **partial**,不能视为同一合同。
### 5.2 行字段与状态映射
基准 PC 行字段:
- 诊单 ID/NEW、患者姓名、性别年龄(`index.vue:191-218`)。
- `appointments[]` 多挂号;回退到 appointment_id/status/doctor/time 主字段;最近渠道(`:219-261,1747-1757`)。
- 确认状态来自 `DiagnosisViewRecord.some(is_confirmed == 1)``:262-267,1313-1317`)。
- 复诊时间/医生/处方已作废、助理、是否开方(`:268-296`)。
- `unserved_days` 与 last blood tooltip,阈值 null 灰、>=7 红、3-6 橙、<=2 绿(`:297-317,1319-1329`)。
- `video_call_hint.state` 映射 none/live/pending_room/label`:318-341,1373-1427`)。
Python 只显示预约时间、患者、状态、联系方式、医助、确认、处方、remark(`consultations.py:183-225`),判 **partial / P1**。具体错误为:
1. `Consultation` 模型没有正式字段 `has_appointment``appointment_status``appointments`、followup、unserved、last blood、video hint;虽保留 raw,但 UI 不读取这些字段(`core/models.py:322-350`)。
2. 模型从根 `status` 优先取值,只有根 status 缺失才使用首个 nested appointment.status`:362-366,406-414`)。基准明确把 appointment status 放在 `appointment_status`/`appointments[].status`,并将诊单本身的 status 用作启用状态。这是 P0 视频门槛错误的根因。
3. Python `diagnosis_confirmed` 属性只来自根 `confirmed/diagnosis_confirmed``:413-423`),不计算 `DiagnosisViewRecord`;存在把已确认显示成待确认的风险。
4. Python 联系方式列直接显示 `patient_phone/phone/phone_masked``consultations.py:195-201`),而基准 PC/H5 列表不展示手机号明文;编辑页仅 `tcm.diagnosis/phonePlain` 可保持明文(`edit.vue:822-856,1263-1277`)。判 **missing / P1(隐私)**
挂号状态基准只有:1=已预约、3=已完成、4=已过号(`index.vue:1732-1745`);取消仅允许 status 1/4,status 3 明确拒绝,且多挂号必须按具体 appointment id 操作(`:1782-1857`)。Python 把 2 显示为“已取消”,并把 4 也设为可直呼(`consultations.py:53-58,310-317`),不符合基准。
### 5.3 详情、编辑与操作
| 能力 | 基准行为与证据 | Python | 判定 |
|---|---|---|---|
| 只读病例详情 | PC 查看/双击需 `tcm.diagnosis/readonlyDetail`,路由 query id;加载 `/tcm.diagnosis/readonlyDetail` + doctorNotes`index.vue:345-354,2165-2172`; `readonly.vue:172-240`; `api/tcm.ts:8-11`) | 表格双击直接发起视频;无详情页/端点(`consultations.py:227-228,319-323` | missing / P1 |
| H5 简版详情 | 卡片头/体打开 `detail.vue`GET `/tcm.diagnosis/detail`,显示基础、病史、舌苔/报告、症状舌脉治则处方医嘱状态(`index_h5.vue:121,144`; `detail.vue:10-122,157-165` | 无 | missing |
| 完整诊单编辑 | `tcm.diagnosis/edit`GET detail 后 POST edit;新增用 add;手机号/身份证唯一性检查(`edit.vue:1263-1375`; `api/tcm.ts:44-61,77-90` | 无 repository 方法或 UI | missing / P1 |
| 编辑字段 | patient/id card/phone/gender/age/marital/height/weight/region/BP/glucose/type/status/source/card/current medicine/local diagnosis、全部现病史/既往史/其他史/symptoms/remark`edit.vue:86-624,916-970` | Consultation 只存列表摘要(`core/models.py:322-350` | missing / P1 |
| 编辑验证 | 姓名、手机、性别、年龄、空腹血糖、诊断类型、当地医院必填;手机号正则;身份证 18 位正则;病史发现最多 50(`edit.vue:1026-1175` | 无 | missing |
| 开方/查看 | `tcm.diagnosis/kaifang`;审核通过且未作废显示“查看”,否则“开方”;传 diagnosis id + appointment id`index.vue:354-363,1671-1694`) | 无问诊行开方入口;只有独立已开处方列表/详情 | missing / P1 |
| 处方边界 | 先按 appointment 查已有;临床诊断、至少一味药、药名/正剂量、无重复、手写签名必填;可作废但有业务订单时禁止(`tcm-prescription/index.vue:1829-1922,2219-2328` | Remote 仅 list/detail issued prescription`repository.py:354-397`),无 add/getByAppointment/void | missing / P1 |
| 预约 | `tcm.diagnosis/guahao`;医生排班/未来 7 天/可用时段;今天过去时段禁用;渠道必填;指定自媒体渠道补充必填;今天已有 status1/4 禁止重复约今天(`appointment.vue:239-420,423-448,535-619,669-724` | 无 | intentionally unsupported(后台/医助调度) |
| 取消挂号 | status1/4 可取消,3 拒绝,多 appointment 精确到子记录(`index.vue:1782-1857`; `api/doctor.ts:59-67` | 无 | intentionally unsupported(后台/医助调度) |
| 指派/取消指派 | `tcm.diagnosis/assign`;单条/批量、继承标志、取消传 assistant_id=0`index.vue:1532-1661` | 无 | intentionally unsupported(医助管理) |
| 视频二维码/确认二维码 | 仅 `has_appointment && appointment_status=1`;需 weapp config;权限 `videoQr`/`guahao``index.vue:390-391,1778-1780,1964-2055` | 用同一 `videoQr` 权限启动原生直呼(`consultations.py:102-106` | intentionally unsupported(二维码)+ partial(权限/状态复用错误) |
| 医助旁观 | 仅当前被指派医助 + `tcm.diagnosis/watchCall`pending_room 显示但不能进,live 才进;TRTC 只拉流(`index.vue:1380-1413`; `AssistantWatchCallDialog.vue:120-219`) | 无旁观角色/入口 | intentionally unsupported(医助角色) |
| 挂号日志 | `tcm.diagnosis/guahaoLogList`,空数组容错,显示 action/operator/summary/time`index.vue:1859-1897` | 无 | intentionally unsupported |
| 补身份证 | 15/18 位前端校验,POST fillIdCard 后自动年龄(`index.vue:1918-1961` | 无 | intentionally unsupported(后台资料维护) |
| 企微记录 | records/contact 并行、20 条分页、文字 note 新增/删除(`index.vue:2066-2160` | 无 | intentionally unsupported(后台企微归档) |
| 创建订单 | order_type、amount、remark 后生成订单二维码(`index.vue:1430-1515` | 无 | intentionally unsupported(后台运营) |
### 5.4 只读详情的权限化子区块
基准 `/tcm/diagnosis-readonly` 不是一个简单摘要,它按权限加载:
- `tcm.diagnosis/dailyRecord`DailyMatrix`readonly.vue:42-54,183`)。
- `doctor.appointment/addDoctorNote`NoteTimeline 的可写版本在 editreadonly route 固定只读(`readonly.vue:56-70`)。
- `tcm.diagnosis/patientOrders`:业务订单(`:74-88`)。
- `tcm.diagnosis/huifang`:通话录制回放(`:90-101`)。
- `tcm.diagnosis/chat`IM 记录(`:103-113`)。
- `tcm.diagnosis/assign``tcm.diagnosis/detail`:指派日志(`:115-131`)。
- `doctor.appointment/lists`:挂号记录(`:133-146`)。
Python 问诊列表没有选中详情容器,因此上述全部为 **missing**;其中病例、日常记录、备注、挂号记录属于医生核对链路,列 P1;订单、指派和后台 IM 可按角色继续列 intentionally unsupported。
## 6. API 覆盖矩阵
此表只包含从上述可达 view/component 实际导入的业务 API;端点定义证据集中在 `D:\web\zyt\admin\src\api\patient.ts:3-40``api\doctor.ts:49-87``api\tcm.ts:3-109,202-310,313-372,591-648`
| API 组 | 管理后台可达用途 | Python 状态 |
|---|---|---|
| `doctor.appointment/lists` | 接诊队列、预约/挂号记录、最近就诊/今日重复检查 | **exact 端点 / partial 参数**;接诊台漏 today |
| `doctor.appointment/reception` | 接诊聚合详情 | **exact** |
| `notifyAssistant` | 通知医助 | **exact 端点 / partial 刷新与权限** |
| `addDoctorNote` | 文字、舌苔、报告、通话截图 | **partial**;Remote 参数齐,UI 只有文字 |
| `doctorNotes`, `deleteDoctorNoteImage` | 备注补拉、附件删除 | **missing** |
| `doctor.appointment/complete` | 完成接诊 | **exact 端点 / partial 上下文边界** |
| `tcm.diagnosis/lists` | 问诊列表、全部角标 | **exact 端点 / partial filters/model** |
| `readonlyDetail`, `detail`, `add`, `edit`, `delete`, `checkPhone`, `checkIdCard`, `fillIdCard` | 详情与资料维护 | **missing** |
| `assign`, `assignLogList`, `getAssistants`, `watchCall` | 医助分配与旁观 | **intentionally unsupported** |
| `trackingWindow`, `trackingNotes`, `addTrackingNote` | 日常记录/备注窗 | **missing** |
| blood/diet/exercise add/edit | 编辑页日常记录 | **missing** |
| diagnosisTodo lists/add/cancel | 日常记录待办 | **missing** |
| `getCallSignature`, `startCall`, `bindCallRoom`, `endCall` | 原生实时视频核心 | **exact** |
| `getCallRecords`, `attachLocalCallRecording`, `createManualCallRecord` | 回放与人工补传 | **missing**(可选扩展) |
| `getImChatMessages`, `triggerImChatSync` | IM 归档 | **missing** |
| `prescription/listByDiagnosis`, `getByAppointment`, `add`, `void` | 病例处方与开方 | **missing / P1** |
| `prescription/detail`, `prescription/lists` | 查看已开处方 | **exact**,但只在独立页面 |
| `prescriptionLibrary/lists` | 开方时导入本人模板 | **exact endpoint**,但问诊开方 UI 缺失 |
| availableSlots/create/roster/lists | 预约 | **intentionally unsupported** |
| generateMiniProgramQrcode/generateOrderQrcode + weapp config | 二维码 | **intentionally unsupported**,由原生直呼替代 |
| WeChat records/contact/add/delete | 企微归档 | **intentionally unsupported** |
| prescriptionOrder lists/detail/logs | 只读业务订单 | **intentionally unsupported** |
Remote 的相关实现块只覆盖 appointment/reception/actions`D:\web\zyt\app\src\doctor_workstation\services\repository.py:226-277`)、diagnosis list 与四个 call endpoint`:414-483`);因此上述“missing”不是 UI 隐藏但 repository 已具备,而是端到端合同确实不存在。
## 7. 权限码对照
### 基准中实际出现
- 页面/读取:`doctor.appointment/lists``tcm.diagnosis/lists``tcm.diagnosis/readonlyDetail``tcm.diagnosis/detail``tcm.diagnosis/dailyRecord``tcm.diagnosis/patientOrders``tcm.diagnosis/huifang``tcm.diagnosis/chat`
- 接诊写动作:`doctor.appointment/addDoctorNote``doctor.appointment/complete``tcm.diagnosis/edit`
- 问诊列表动作:`tcm.diagnosis/add``edit``delete``assign``kaifang``guahao``videoQr``watchCall``guahaoLogList``order`
- 编辑扩展:`tcm.diagnosis/phonePlain``tcm.diagnosis/chufang``tcm.diagnosis/setRevisitSlotStartOffset``tcm.prescriptionOrder/detail`
证据:接诊 `patient/reception/index.vue:146-171`;问诊 PC `tcm/diagnosis/index.vue:318-400`readonly `readonly.vue:42-146`edit `edit.vue:674-748,822-856`PatientOrderList `components/PatientOrderList.vue:20-33,99-114`
### Python 实际门控
- 页面:两个页面码 **exact**`shell.py:40-75`)。
- 接诊:`notifyAssistant``videoQr``complete``addDoctorNote``reception.py:322-331`);缺 edit。
- 问诊:只有 `videoQr``consultations.py:102-106`);其余动作根本没有 UI。
- Python PermissionSet 本身支持 `*`、all/any`core/permissions.py:60-104`),问题在页面选择了哪些码,而不是权限容器能力。
特别注意:Web 的 `v-perms` 是数组 OR`install/directives/perms.ts:13-32`),`hasPermission()` 工具是数组 AND`utils/perm.ts:3-17`);当前页面绝大多数调用只有一个码,唯一显式 OR 的 assign/detail 是两个独立调用(`readonly.vue:117-120`),所以本次差异不受二者实现差别影响。
## 8. P0 / P1 缺口清单(按严重度排序)
### P0
1. **RACE-RECEPTION-01:快速切换患者导致详情与选择错配。** 修复验收应覆盖 A 请求未返回时选 B,A/B 任意顺序返回后 UI、备注 diagnosis_id、通知/完成 appointment_id 和视频 payload 必须全部指向 B;详见第 1、4 节证据。
2. **SCOPE-RECEPTION-02:接诊列表漏 `start_date=end_date=today`。** 必须在 UI 或 repository 的专用 reception query 中固定今日,不能让通用 appointment list 隐式决定。
3. **STATE-CALL-03:问诊视频使用 `status` 而非 `has_appointment + appointment_status`,且错误允许 4。** 模型和 UI 都需改;只修 UI 的字段名仍会被当前 dataclass 的根 status 优先级遮蔽。
### P1
1. **DETAIL-04:问诊列表没有 `readonlyDetail`/详情入口,双击反而直呼。** 医生无法先核对完整病例、日常记录、备注和挂号历史。
2. **EDIT-05:接诊台和问诊列表均无诊单详情/edit 合同。** 基准接诊台明确提供“编辑病历”。
3. **RX-06:没有从诊单开方/查看/作废的工作流。** 独立“已开处方”页面不能替代 `diagnosis_id + appointment_id` 上下文开方。
4. **RECEPTION-DETAIL-07:接诊详情只显示摘要,缺完整 PatientCase、日常记录、备注附件和附件删除。**
5. **PRIVACY-08:接诊和问诊列表可能直接显示明文手机号,未遵守基准的 mask/phonePlain 边界。**
6. **PERM-09:用 `tcm.diagnosis/videoQr` 保护原生直呼、用未在基准按钮上出现的 `doctor.appointment/notifyAssistant` 保护通知,可能让合法接诊医生缺动作;同时缺 `tcm.diagnosis/edit` 入口。** 需要后端权限清单确认专用 call permission,而不是继续借用二维码码。
7. **MODEL-10Consultation 未正式解析 appointment_status、has_appointment、多挂号、DiagnosisViewRecord、unserved/video hint 等;当前确认和状态显示不可靠。**
8. **FILTER-11:问诊筛选缺 has_appointment、confirmed、诊断类型、证型、医助、最新挂号渠道、最新指派、未服务排序及顶部 count;Python 自创 status 下拉又改变默认结果集。**
9. **BOUNDARY-12:文字备注没有 500 字客户端限制;通知成功不刷新;完成动作未在 handler 中二次验证 1/4。**
不列 P0/P1 的 intentionally unsupported 项:H5 响应式布局、小程序/订单二维码、批量指派、医助旁观、企微后台归档、后台订单创建和排班预约。若产品决定医生桌面也承担医助/运营职责,应把这些重新分类为 missing 并另立需求。
## 9. 基准自身的可疑点(不可静默照搬)
以下均是当前 `admin/src/views` 的真实行为,本报告仅记录,不用其他资料修正它:
1. 接诊台“编辑病历”把 `diag.patient_id` 传给 `edit.open('edit', id)``patient/reception/index.vue:474-476`),而 edit 将该 id 直接传给 `/tcm.diagnosis/detail``tcm/diagnosis/edit.vue:1263-1265,1288-1302`);问诊列表传的则是 `row.id``index.vue:1667-1669`)。这是明显 ID 口径冲突。
2. PC/H5 的“视频二维码”请求把 `diagnosis_id` 赋成 `row.appointment_doctor_id``index.vue:1987-1993`; `index_h5.vue:1262-1268`),而“诊单二维码”正确使用 `row.id``index.vue:2037-2042`)。Python 原生直呼当前没有复制此错误。
3. `PatientCaseCard` 的 consultation_type 三元两边都返回“复诊”(`PatientCaseCard.vue:148-151`)。
4. 接诊行通知/通话无权限指令、NoteTimeline 媒体写入未复用 addDoctorNote 门控,见第 4 节;应先确认服务端授权模型,再决定桌面行为。
## 10. 建议的最小合同测试
后续实现至少应加入不发网络的合同测试:
1. Reception request 必含 status、today start/end、patient_name、page_no/page_size。
2. A/B 详情乱序返回不会产生跨患者详情或写动作目标。
3. Consultation row 的 `status=1, appointment_status=3` 不可视频;`status=1, has_appointment=1, appointment_status=1` 可视频;status 4 一律不可从问诊列表直呼。
4. `DiagnosisViewRecord=[{is_confirmed:1}]` 显示已确认;多 appointments 保留并正确挑选 active appointment。
5. 无 phonePlain 权限时列表和详情只出现掩码。
6. readonlyDetail、edit 和 prescription 动作分别由准确权限码控制;缺码时不创建可调用控件。
7. 备注 501 字在客户端拒绝;媒体 note payload 保留 tongue/report 数组;删除附件的三字段合同固定。
+275
View File
@@ -0,0 +1,275 @@
# 腾讯云 TRTC 接入 Python / PySide6 跨平台桌面视频面诊研究
> 调研日期:2026-08-10
> 范围:Windows / macOS 桌面端;Python / PySide6 业务程序;1 对 1 视频面诊,可扩展屏幕共享。
> 来源原则:仅使用腾讯云、Tencent RTC 官方文档与官方 SDK API 文档。文中标为“工程判断/建议”的内容是根据官方能力边界作出的架构推断,并非腾讯云对 Python 或 PySide6 的兼容性承诺。
## 结论摘要
1. **腾讯云没有官方 Python 或 PySide6 TRTC 客户端 SDK。** 用户指定的[产品概述(文档 16788](https://cloud.tencent.com/document/product/647/16788)列出了 Windows、macOS、Web、Electron 等平台,没有 Python/PySide6。官方还提供了 Windows/macOS 的 **C++ Qt** 集成示例,但它不是 Python 绑定,也没有证明 PySide6 / Qt 6 ABI 兼容。
2. **推荐生产架构:PySide6 保留为业务主程序,音视频做成独立 Electron RTC companion(伴随进程/独立面诊窗口),由后端签发进房票据,PySide6 与 companion 通过本机受认证 IPC 交互。** 1 对 1 场景优先使用底层 `trtc-electron-sdk`;若要快速获得完整会议 UI、成员管理和会控,可选官方 TUIRoomKit Electron。官方明确将 TUIRoomKit Electron 用于“医疗问诊”等场景。
3. **最快的业务 MVP 是从 PySide6 打开系统浏览器中的 HTTPS Web SDK 面诊页。** 这是官方浏览器支持路径。把 Web SDK 嵌入 `QWebEngineView` 技术上有机会可行,但 QtWebEngine 不在腾讯的具名支持矩阵中;官方只说理论上支持 Chromium 56+,并要求对 WebView 等未列环境运行能力检测。因此,`QWebEngineView` 只能作为验证分支,不能在未完成双平台全量实测前作为生产承诺。
4. **若“必须在同一 PySide6 窗口内原生渲染”是硬要求,才选择 Native C++ 桥接。** 需要自行为 Windows/macOS 编译 C ABI/CPython 扩展,处理原生渲染句柄、回调线程、Qt 5/Qt 6 兼容、签名与打包;这是可落地但成本和维护风险最高的方案。
5. **生产环境的 SDKSecretKey 只能放在服务端。** 客户端仅获得短期 `UserSig`;需要面诊房间级与媒体权限控制时,开启 Advanced Permission Control 并由服务端签发 `PrivateMapKey`。房间 ID 不能视作访问控制。
## 方案对比
| 方案 | 官方支持边界 | 与 Python/PySide6 的关系 | 摄像头/麦克风/屏幕分享 | 主要风险 | 判断 |
|---|---|---|---|---|---|
| Web SDK + 系统浏览器 | 官方支持 Windows/macOS 主流浏览器;生产要求 HTTPS | PySide6 打开带一次性业务会话的 HTTPS 页面;无 Python SDK 绑定 | 官方均支持;屏幕分享受浏览器/OS 授权和用户选择器约束 | 独立浏览器窗口、业务 UI 一体化较弱 | **最快、风险低的 MVP/兜底** |
| Web SDK + `QWebEngineView` | 官方称理论支持 Chromium 56+,未列环境(含 WebView)需 `TRTC.isSupported()`/能力检测;未明确认证 QtWebEngine | JS 运行在嵌入页,通过 WebChannel/IPC 与 Python 通信(工程自建) | 需验证嵌入引擎的媒体权限、设备切换、屏幕选择与 macOS Screen Recording | 腾讯支持边界、QtWebEngine 版本与权限行为不确定 | **仅作 POC;通过准入测试后才可采用** |
| Electron SDK / TUIRoomKit Electron | 官方支持 Windows/macOSSDK 是 Node 原生模块;官方提供设备权限、打包、屏幕分享和医疗问诊组件指引 | Python 不能直接 `import`;作为独立伴随进程最清晰 | 完整支持,DOM 承载本地/远端画面;可用辅路同时保留摄像头与屏幕 | 包体较大、需维护本机 IPC、两平台分别构建签名 | **推荐生产方案** |
| Native C++ SDK + 自研桥 | 官方全平台 C++ API、Windows/macOS SDK、C++ Qt 示例 | 自行写 C ABI/pybind11/CPython 桥;腾讯不提供 Python/PySide6 绑定 | 能力最完整,包含设备管理、测试、屏幕源枚举、原生渲染与私有加密 API | C++ ABI、Qt 版本、线程、崩溃隔离、双平台构建维护 | **单窗口原生体验的二期方案** |
## 各方案适配判断
### 1. Web SDK
官方事实:
- TRTC Web SDK 是 JavaScript SDK,视频渲染目标是 HTML 元素;通过 `TRTC.create()``enterRoom()``startLocalAudio()``startLocalVideo()``startRemoteVideo()``exitRoom()``destroy()` 完成生命周期,见[Web & H5 快速接入](https://cloud.tencent.com/document/product/647/116544)。
- 官方平台页称理论上支持所有 Chromium 56+ 浏览器;对支持表之外的环境,建议用 `TRTC.isSupported()` 或[能力检测页](https://web.sdk.qcloud.com/trtc/webrtc/demo/detect/index.html)检测。快速 Demo 文档还特别提到 WebView 等环境应先检测,见[Web Demo 准备工作](https://cloud.tencent.com/document/product/647/32398)和[Web API 概览/平台要求](https://intl.cloud.tencent.com/zh/document/product/647/41664)。
- 生产环境推流和屏幕分享要求 HTTPS;HTTP 生产页只能播放,不能上麦/屏幕分享。本地 `localhost` 可用于开发。
- 设备名称和 `deviceId` 在获得摄像头/麦克风授权前可能为空;应先完成授权再展示设备详情。
- 桌面 Web 支持 `startScreenShare()`;用户可能从浏览器系统 UI 停止分享,业务必须监听分享停止事件并恢复 UI 状态,见[Web 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/35163)。
对 PySide6 的工程判断:
- 系统 Chrome/Edge/Safari 是具名支持路径,最适合快速验证服务端、UserSig、房间和媒体链路。
- `QWebEngineView` 虽基于 Chromium,但不是腾讯文档具名认证平台。只有在目标 PySide6 随附的 QtWebEngine 上同时通过能力检测和真实设备测试,才能认为本项目可用。
- 若验证嵌入方案,应把下列项目设为硬门槛:摄像头/麦克风首次授权及拒绝后恢复、设备插拔和切换、远端音频自动播放、窗口/整屏分享、从系统分享条停止、macOS Screen Recording 权限、窗口最小化/休眠恢复、打包后仍可用。
- `file://` 虽在官方表中可用,但生产仍建议加载受控 HTTPS 页面,以便版本发布、CSP、证书与安全响应集中管理。
### 2. Electron SDK / TUIRoomKit Electron
官方事实:
- 当前[Electron 快速接入](https://cloud.tencent.com/document/product/647/116549)要求 Electron 工程安装 `trtc-electron-sdk`,实际加载 `trtc_electron_sdk.node` 原生模块;支持 Windows 和 macOS,并给出了两平台不同的原生模块资源路径、摄像头/麦克风/屏幕权限检查及打包配置。
- `startLocalPreview()` 的渲染目标是 `HTMLElement`,不是 PySide `QWidget``startLocalAudio()` 可选 Speech 模式,官方说明其噪声抑制和弱网抗性更强,适合面诊语音。
- 1 对 1 视频面诊应使用 `TRTCAppSceneVideoCall`,而不是直播场景。官方将 VideoCall 定位为 1 对 1 或 300 人以内实时通话。
- [TUIRoomKit Electron 接入](https://cloud.tencent.com/document/product/647/129879)明确列出“医疗问诊”场景,并内置房间管理、音视频控制、屏幕共享、成员管理和布局。
- Electron 屏幕分享支持主路和辅路。辅路可在摄像头继续上行的同时分享屏幕;一个 TRTC 房间目前只能有一路屏幕分享,见[Electron 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/47619)。
- 官方 Electron API 还提供设备列表、设备切换、摄像头/麦克风/扬声器测试和网络测速;网络测速应在进房前进行,见[Electron SDK API](https://web.sdk.qcloud.com/trtc/electron/doc/en-us/trtc_electron_sdk/TRTCCloud.html)。
对 PySide6 的工程判断:
- Node 原生模块不能当作 Python 模块直接加载。把它塞进 PySide6 进程会引入 Node/Electron 运行时、DOM 渲染和 ABI 问题,不值得。
- 独立 RTC companion 可将故障、升级和媒体权限与 Python 主进程隔离;PySide6 只负责预约、病历、支付等业务 UI,Electron 只持有当次短期票据并负责通话。
- Windows 与 macOS 应各自在目标系统构建、签名并验证产物;不要把包含 `.node`/`.dll`/framework 的包当作纯 JS 跨平台包。
- IPC 采用本机命名管道/Unix domain socket 或 loopback WebSocket,并以每次启动随机 nonce 双向认证;不要把 `UserSig` 放在命令行参数、URL 查询串或长期日志中。此为工程安全建议,不是腾讯 SDK 的内置保证。
### 3. Native C++ SDK 与 Qt 桥接
官方事实:
- [全平台 C++ API 概览](https://cloud.tencent.com/document/product/647/32268)包含实例/回调、进退房、摄像头和远端渲染、音频、设备管理、屏幕分享、网络质量、连接恢复及私有加密等完整能力。
- 腾讯提供[Qt Windows/macOS 集成文档](https://intl.cloud.tencent.com/zh/document/product/647/39665)Windows 使用 C++ SDK 的 `liteav.lib`/DLLmacOS 引用 `TXLiteAVSDK_TRTC_Mac.framework` 的 C++ 接口。示例是 Qt Widgets/C++macOS 文档写的是 Qt 5.10+。
- macOS 需要在 `Info.plist` 声明摄像头和麦克风权限;官方 Electron 指引另外检查 `screen` 权限。
- C++ API 可枚举/切换设备并运行摄像头、麦克风和扬声器测试;屏幕分享可枚举窗口/屏幕、选择目标、设置辅路参数及包含/排除窗口。
对 PySide6 的工程判断:
- PySide6 使用 Qt 6,而官方 Qt 示例仍以 Qt 5 为基线,不能把“支持 C++ Qt”直接等同于“支持 PySide6”。
- 若采用,推荐制作**很薄的 C ABI 桥**,不要让 Python 直接调用 C++ ABI:桥内持有 `ITRTCCloud`、回调对象和 SDK 生命周期,只向 Python 暴露稳定的 opaque handle、进退房、设备、屏幕分享和事件队列。
- 视频尽量由 SDK 绑定原生子窗口/视图句柄渲染;不要默认把每帧 YUV 回调到 Python,这会显著增加跨语言复制、GIL 和掉帧风险。
- SDK 回调可能不在 Qt UI 线程,桥层必须投递到 PySide6 主线程;退出时顺序应固定为停止屏幕分享/本地采集/远端渲染、`exitRoom`、等待退房回调、移除回调、销毁实例。
- 该路线必须先验证 Qt 版本和原生窗口句柄,且建议在正式承诺前向腾讯云提交工单确认目标 SDK 版本的 Qt 6/PySide 嵌入边界。
## 推荐生产架构
```text
┌──────────────────────┐ HTTPS ┌────────────────────────┐
│ PySide6 业务主程序 │ ───────────────────▶ │ 业务后端 / RTC Ticket │
│ 预约、病历、面诊入口 │ │ 鉴权、预约授权、UserSig │
└──────────┬───────────┘ │ PrivateMapKey、结束房间 │
│ 本机受认证 IPC └────────────┬───────────┘
▼ │ TRTC REST / callback
┌──────────────────────┐ ┌─────────────▼──────────┐
│ Electron RTC companion│ ◀──── RTC 媒体 ────▶ │ 腾讯云 TRTC │
│ 设备、视频、屏幕分享 │ └────────────────────────┘
└──────────────────────┘
```
### 组件职责
**PySide6 主程序**
- 只传预约 ID/面诊动作,不生成或持久化 SDKSecretKey。
- 调用业务后端获取本次短期进房票据,启动/聚焦 RTC companion。
- 展示 companion 回传的 `preflight / joining / connected / reconnecting / ended / error` 状态。
- 业务“面诊已结束”不能只依赖窗口关闭事件,应由客户端结果、TRTC 服务端回调和后端结束动作共同收敛。
**业务后端 / RTC Ticket 服务**
- 验证当前登录用户确实是该预约的医生或患者,并验证允许进房的时间窗和预约状态。
- 服务端派生 `userId``roomId`,生成 `UserSig`;启用高级权限控制时同时生成 `PrivateMapKey`
- 返回最小票据:`sdkAppId``userId`、一种且仅一种 room ID、`userSig``expiresAt`、可选 `privateMapKey`、业务角色与 UI 权限。SDKSecretKey 永不返回。
- 提供“结束面诊”接口;需要强制结束时调用官方 `DismissRoom`/`DismissRoomByStrRoomId`,见[解散房间 API](https://cloud.tencent.com/document/api/647/50089)。
- 接收房间/媒体回调,校验签名并幂等处理。
**Electron RTC companion**
- 使用底层 `trtc-electron-sdk` 完成 1 对 1 VideoCall;若需求接近完整会议产品,则用 TUIRoomKit Electron。
- 不保存长期登录态或云密钥;窗口关闭、崩溃和正常退房均向 PySide6/后端报告。
- 所有摄像头、麦克风和屏幕分享都由用户明确操作开启,并在界面持续显示状态。
## UserSig 与房间权限
### UserSig
- [官方用户鉴权文档](https://cloud.tencent.com/document/product/647/17275)明确指出:客户端计算 UserSig 只适合 Demo。客户端代码,尤其 Web,容易被反编译;泄露 SDKSecretKey 会导致腾讯云资源被盗用。
- 正式环境流程必须是:客户端先向业务服务器请求;服务器按 `SDKAppID + UserID` 生成 UserSig;客户端仅把结果交给 SDK。官方提供 Python HMAC-SHA256 服务端示例,因此 Python 后端生成完全可行。
- 建议把 UserSig 有效期控制为“预约可加入窗口 + 最大面诊时长 + 合理重连缓冲”,并在每次重新进入房间前重新授权。腾讯官方云助手的[Web 进房说明](https://cloud.tencent.com/document/product/1715/104507)指出原始 TRTC 的 UserSig 在进房时校验、进房后到期不影响当前通话;若采用包含 IM 登录的 TUIRoomKit,还要监听 `onUserSigExpired` 并从后端续签。
- SecretKey 放在服务端密钥管理系统/受限环境变量中;禁止进入桌面包、前端 JS、崩溃转储、遥测和调试日志。
### PrivateMapKey(高级权限控制)
- UserSig 证明某 UserID 有权使用该 SDKAppID,**不等于有权进入某个面诊房间**。对视频面诊,建议评估开启[高级权限控制](https://intl.cloud.tencent.com/zh/document/product/647/35157)。
- `PrivateMapKey` 绑定 room ID 与权限位,可分别控制创建房间、进房、收发音频、收发主路视频、收发辅路(屏幕分享)。必须由服务端计算。
- 示例权限策略:
- 医生:创建/进入、收发音频、收发视频、发送/接收辅路。
- 患者:进入、收发音频、收发视频、接收辅路;若不允许患者分享,则不授予“发送辅路”。
- 若不能保证医生先进入,需要给患者也授予创建房间,或由业务规则强制医生先创建。
- 启用高级权限控制后,同一 SDKAppID 下所有用户都必须携带 PrivateMapKey;已有线上应用不能直接无迁移开启。建议新建独立 SDKAppID 做灰度验证。
## 房间与用户生命周期
官方[基本概念](https://cloud.tencent.com/document/product/647/46351)给出的关键规则:
- 不存在的房间在首个用户进入时自动创建。
- 通话模式下,所有用户主动退房后房间立即解散;所有人异常掉线时,服务端约 90 秒后清理并解散。异常等待时间仍计入用量。
- 数字 `roomId` 与字符串 `strRoomId` 是两套不同房间,不能混用;全端和服务端必须统一一种类型。
- 同一 UserID 同时进入同一房间会互踢/干扰。UserID 应由后端稳定映射;若允许同一账号多设备同时加入,应给每个设备/会话分配唯一 UserID。
- 原始 TRTC 的远端用户进出回调用于维护成员列表,不代表对方已有视频;显示远端画面必须监听 `onUserVideoAvailable`,屏幕分享监听 `onUserSubStreamAvailable`
建议客户端状态机:
```text
idle
→ device-preflight
→ ticket-issued
→ joining
→ joined(media-off)
→ media-on / screen-sharing
→ exiting
→ idle
joined ↔ reconnecting
joining/connected → kicked | room-dismissed | fatal-error → cleanup → idle
```
实施要点:
- 创建实例后先注册所有错误、进退房、远端媒体、设备变化和连接状态回调,再调用 `enterRoom`
-`onEnterRoom(result > 0)` 作为真正进房成功,不以 `enterRoom()` 函数返回或窗口已打开代替。
- 网络断开时 SDK 会自动重连;监听 `onConnectionLost``onTryToReconnect``onConnectionRecovery`。官方说明远端通常约 90 秒后才收到异常用户离开,因此业务后端不能把短时断网立即当成面诊结束,见[断线重连说明](https://intl.cloud.tencent.com/document/product/647/36057)。
- 正常结束必须成对调用 `exitRoom`;退出后停止本地媒体、停止远端渲染、移除监听并销毁实例。Web 端明确要求 `exitRoom()` 后不再使用时调用 `destroy()`
- 服务端[房间与媒体回调](https://intl.cloud.tencent.com/zh/document/product/647/39558)可能重试,且特殊网络/重进场景可能产生重复事件;回调处理必须幂等,不能只用“最后一条回调”做财务/医疗业务结论。
## 摄像头、麦克风与屏幕分享
### 通话前检查
- 列出并选择摄像头、麦克风和扬声器;运行摄像头预览、麦克风电平和扬声器测试。
- 网络测速只在进房前运行,避免影响通话质量。
- 默认音频质量使用 Speech/语音模式;先以 640×360 的保守视频档位验证弱网和 CPU,再按质量监控数据决定是否提高到 720p。
- 对权限拒绝、设备被占用、设备拔出、默认设备变化给出可恢复 UI,不应直接结束预约。
### 屏幕分享
- 默认采用**辅路**,让医生摄像头与屏幕并存;远端根据辅路可用事件订阅。
- 一个房间只允许一路屏幕分享,第二人发起前应在业务 UI 层仲裁。
- 分享前必须让用户确认目标窗口/屏幕;分享中持续显示醒目标志;无论从应用按钮还是浏览器/系统指示条停止,都要收到事件并清理本地状态。
- 医疗场景应默认不共享整个桌面,优先窗口分享。Native SDK 可用窗口排除/包含 API,避免将病历主窗口、通知或其他患者信息意外共享。
- macOS 打包必须验证 Camera、Microphone 和 Screen Recording 权限的首次授权、拒绝、系统设置中撤销及升级安装后的行为。
## 生产安全与合规边界
1. **应用隔离**:开发、测试、生产使用不同 SDKAppID;正式环境不要复用 Demo 密钥或固定房间号。
2. **最小化标识**`roomId`/`userId` 使用无语义内部 ID,不包含姓名、手机号、身份证号、诊断或预约描述;映射只保存在业务后端。
3. **后端授权**:每次签票都校验预约参与者、角色、状态和时间窗;不能只靠“知道 roomId”。高安全场景使用 PrivateMapKey。
4. **回调真实性**:生产回调使用 HTTPS;按官方算法对原始请求体做 HMAC-SHA256 校验,并校验 SDKAppID;存储事件 ID/组合键以幂等处理重试。
5. **传输与额外加密**:腾讯[信息安全说明](https://cloud.tencent.com/document/product/647/86362)说明其默认传输有私有传输协议/TLS/WSS 保护。若组织政策要求额外媒体私有加密,Native C++ API 提供 `enablePayloadPrivateEncryption`,见[媒体流私有加密](https://cloud.tencent.com/document/product/647/106173);该能力需要相应套餐,并与云端录制、旁路转推等能力存在冲突,应在架构阶段选定。Electron 官方另有 C++ 动态库形式的[自定义媒体加解密插件](https://web.sdk.qcloud.com/trtc/electron/doc/zh-cn/trtc_electron_sdk/tutorial-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9F%B3%E8%A7%86%E9%A2%91%E7%9A%84%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A0%E8%A7%A3%E5%AF%86.html),实施成本应单独评估。
6. **录制默认关闭**:只有在业务、告知同意、保存期限、访问控制和删除策略均明确后才启用。官方说明云录制文件存入客户指定的云存储;开启私有加密会限制云录制等服务。
7. **日志最小化**:不记录 UserSig、PrivateMapKey、完整 IPC 消息、病历内容或屏幕标题;支持包上传前先脱敏。SDK 日志目录应受操作系统用户权限保护并配置留存期。
8. **程序供应链**Windows 代码签名;macOS Developer ID、Hardened Runtime/必要 entitlement、Notarization;固定已验证 SDK 版本,升级先做双平台回归。
9. **网络准入**:医院/机构网络可能限制 UDP。上线前按[防火墙白名单](https://intl.cloud.tencent.com/zh/document/product/647/35164)在真实网络验证 Native/Electron 或 WebRTC 所需端口与动态域名,不要只在家庭网络测试。
10. **合规不是 SDK 自动获得**:腾讯的信息安全说明明确不是国家/行业标准承诺,强制要求建议通过书面 SLA 确认。医疗隐私、录制同意、数据驻留和留存仍需项目方做法务/安全评审。
## 最小可验证集成(推荐:Electron companion
### 验证目标
先在一个 Windows 10/11 x64 和一个受支持 macOS 真机上实现 doctor ↔ patient 互通;进入预生产前再增加同平台终端,覆盖 Windows ↔ Windows、macOS ↔ macOS。最终证明安全签票、跨平台进房、音视频、设备权限、屏幕辅路、重连、正常退房和打包后运行都成立。
### 实施顺序
1. **TRTC 开发应用**
- 新建独立开发 SDKAppID。
- 先关闭自动录制/旁路转推;高级权限控制在基础链路跑通后于同一开发应用或新应用验证。
2. **Python/任意现有后端的 ticket endpoint**
- `POST /api/appointments/{id}/rtc-ticket`
- 从登录态和预约记录派生 `userId`/room ID;使用腾讯官方 Python HMAC-SHA256 示例在服务端生成 UserSig。
- 响应中不包含 SecretKey;票据只允许用于该预约与短时间窗。
3. **最小 Electron companion**
- 安装官方 SDK,创建 `TRTCCloud` 单例并先注册回调。
- 请求相机/麦克风权限,提供摄像头、麦克风、扬声器测试。
- 使用 `TRTCAppSceneVideoCall` 进房;等待 `onEnterRoom > 0`
- 用户点击后调用 `startLocalPreview()``startLocalAudio(TRTCAudioQualitySpeech)`
- 监听远端主路/辅路可用事件并渲染;实现窗口/整屏辅路分享和停止。
- 退出时完整清理并调用 `destroyTRTCShareInstance()`
4. **PySide6 最小联动**
- “开始面诊”请求 ticket,创建带随机 IPC nonce 的 companionticket 通过受认证 IPC 发送,不放命令行。
- companion 将状态和最终错误码回传;PySide6 只展示状态并提供结束入口。
5. **后端回调**
- 配置 HTTPS 房间/媒体回调,启用自定义 callback key。
- 对原始 body 验签并幂等落库;把客户端与回调状态关联到 appointment/session ID。
6. **高级权限控制验证**
- 服务端生成 PrivateMapKey;验证错误房间、过期/错误票据、患者无屏幕上行权限均被服务端拒绝。
### 最小验收清单
- [ ] Windows ↔ Windows、macOS ↔ macOS、Windows ↔ macOS 三组均能进房。
- [ ] SDKSecretKey 不存在于 Python 包、Electron 包、JS source map、命令行和日志。
- [ ] 正确票据进房成功;错误 UserSig、错误 PrivateMapKey、非预约参与者进房失败。
- [ ] 摄像头、麦克风、扬声器可检测/切换;拒绝授权后 UI 可恢复;设备拔插不崩溃。
- [ ] 医生与患者可看到/听到对方;以首帧和首个音频回调确认,不只凭 UI 按钮状态。
- [ ] 辅路屏幕分享与摄像头并存;系统 UI 停止分享后双方状态同步;第二路分享被正确阻止。
- [ ] 网络断开时进入 reconnecting,恢复后继续通话;应用强杀后服务端约 90 秒收敛,客户端有经过验证的重新取票/进房路径。
- [ ] 重复 UserID 的互踢行为有明确提示;正常退出释放摄像头/麦克风并销毁实例。
- [ ] 回调验签失败被拒绝;腾讯回调重试不会重复结算或重复关闭预约。
- [ ] Windows 签名安装包和 macOS 签名/公证包在干净机器可运行并能申请权限。
- [ ] 医院/目标机构网络按官方白名单完成真实音视频与屏幕分享测试。
### 决策门槛
- 若 Electron companion 的包体/双窗口体验可以接受,按该架构进入生产化。
- 若产品坚持单窗口,可并行做 2 个受限 POC:
1. `QWebEngineView`:仅在上述 WebView 准入测试全部通过后采用;失败即回退 Electron。
2. Native C++ bridge:先只实现 SDK version、实例、回调、一个本地/远端原生渲染视图和完整销毁;确认 Qt 6/PySide6 稳定后再扩展设备与屏幕分享。
- 若组织要求媒体私有加密且同时要求云端录制,必须先解决官方能力冲突,不能在开发末期再补。
## 官方资料索引
- [TRTC 产品概述与平台支持(用户指定文档 16788)](https://cloud.tencent.com/document/product/647/16788)
- [TRTC 基本概念:UserID、房间与生命周期](https://cloud.tencent.com/document/product/647/46351)
- [Web & H5 快速接入](https://cloud.tencent.com/document/product/647/116544)
- [Web DemoWebView 检测、HTTPS 与防火墙要求](https://cloud.tencent.com/document/product/647/32398)
- [Web API 概览与平台支持矩阵](https://intl.cloud.tencent.com/zh/document/product/647/41664)
- [Web 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/35163)
- [Electron 快速接入、设备权限与打包](https://cloud.tencent.com/document/product/647/116549)
- [TUIRoomKit Electron:医疗问诊等场景](https://cloud.tencent.com/document/product/647/129879)
- [Electron 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/47619)
- [Electron SDK API](https://web.sdk.qcloud.com/trtc/electron/doc/en-us/trtc_electron_sdk/TRTCCloud.html)
- [C++ 全平台 API 概览](https://cloud.tencent.com/document/product/647/32268)
- [C++ Qt Windows/macOS 集成](https://intl.cloud.tencent.com/zh/document/product/647/39665)
- [UserSig 用户鉴权与官方 Python 服务端示例入口](https://cloud.tencent.com/document/product/647/17275)
- [原始 TRTC Web 进房时的 UserSig 校验说明](https://cloud.tencent.com/document/product/1715/104507)
- [PrivateMapKey 高级权限控制](https://intl.cloud.tencent.com/zh/document/product/647/35157)
- [房间与媒体服务端回调、签名和重试](https://intl.cloud.tencent.com/zh/document/product/647/39558)
- [房间解散 REST API](https://cloud.tencent.com/document/api/647/50089)
- [断线与自动重连行为](https://intl.cloud.tencent.com/document/product/647/36057)
- [屏幕分享(macOS/Native](https://cloud.tencent.com/document/product/647/32249)
- [TRTC 信息安全说明](https://cloud.tencent.com/document/product/647/86362)
- [媒体流私有加密](https://cloud.tencent.com/document/product/647/106173)
- [Native/WebRTC 防火墙白名单](https://intl.cloud.tencent.com/zh/document/product/647/35164)
+95
View File
@@ -0,0 +1,95 @@
# 医生工作站 UI 最终离屏回归
- 验收日期:2026-08-10
- 环境:Windows 11、Python 3.12.12、PySide6 6.11.1
- 目标尺寸:1280 × 800
- 最小 Shell 尺寸:1024 × 640
- 最终结论:**PASS**
## 1. 结论摘要
最新原始源码已通过完整离屏回归。验收在全新 Python 进程中执行,没有加载任何方法别名、monkey patch 或兼容垫片,并明确断言 `BusyOverlay` 不存在临时 `set_text` 属性。
`LoginWindow.submit()` 使用空账号和密码触发 Demo 默认凭据,真实执行 `DemoDoctorRepository.login()``get_current_user()`,随后由 `ApplicationController` 创建 `ShellWindow`。登录成功后 loading 正常释放、密码被清空,五个授权页面均能异步加载。
问诊列表到 Controller 再到 `DemoVideoDialog` 的信号链路、性别文本、服务器设置和 1024 × 640 最小 Shell 均通过。上一轮发现的四个问题现已全部关闭。
## 2. 验收方法
1. 使用 `QT_QPA_PLATFORM=offscreen` 创建真实 `QApplication`
2. 创建原始 `LoginWindow`,保持 Demo 模式并将账号密码留空,调用真实 `submit()`
3. 等待 `login_succeeded` 和 Controller 创建 Shell,核对认证 Session、用户、repository、loading 与密码清理状态。
4. 依次进入接诊台、我的处方库、已开处方、我的患者、问诊列表,等待后台 Worker 返回数据和详情。
5. 在问诊列表点击“发起视频问诊”,验证 `ConsultationsPage → ShellWindow.video_requested → ApplicationController._request_video → DemoVideoDialog` 完整链路。
6. 将 Shell 精确调整为 1024 × 640,检查接诊主要按钮的窗口坐标和 splitter 两侧可用宽度。
7. 关闭 Demo 后展开服务器设置,保存 HTTPS 地址与 45 秒超时,验证设置存储和两个配置变更信号。
8. 使用 `QWidget.grab()` 覆盖必要截图并逐张目检。
Qt `offscreen` 平台在本机不提供系统字体列表,因此验收进程临时用 `QFontDatabase.addApplicationFont()` 加载 `C:\Windows\Fonts\msyh.ttc`,使截图反映真实中文排版;未修改产品源码或打包配置。
## 3. 回归结果
| 范围 | 结果 | 验收证据 |
| --- | --- | --- |
| 真实 Demo 登录 | 通过 | 空账号密码成功使用 Demo 默认凭据;发出 `login_succeeded`;Session 已认证;用户为“陈医生(演示)”;loading 释放且密码清空 |
| 服务器设置按钮 | 通过 | Demo 模式下禁用;关闭 Demo 后可展开、保存、收起;地址规范化为 `https://api.example.com/adminapi`,超时为 45 秒 |
| Shell / 权限导航 | 通过 | Controller 创建接诊台、我的处方库、已开处方、我的患者、问诊列表共 5 页 |
| 接诊台 | 通过 | 待接诊 2 条,详情异步加载;性别显示“女”;1280 × 800 与 1024 × 640 均稳定 |
| 我的处方库 | 通过 | Demo 模板 2 条 |
| 已开处方 | 通过 | Demo 处方 2 条;详情显示“赵明远 · 男 · 53岁” |
| 我的患者 | 通过 | Demo 患者 3 条,首条详情正常加载 |
| 问诊列表 | 通过 | 今日待接诊 1 条,视频按钮可用 |
| 问诊 → Controller → DemoVideoDialog | 通过 | payload 为 `appointment_id=101``diagnosis_id=501``patient_id=301`Controller 以 key `501` 创建并回收窗口 |
| Demo 视频窗口 | 通过 | 980 × 660 正常渲染;计时到 `00:01`;麦克风与摄像头均可切换为关闭 |
| 1024 × 640 最小 Shell | 通过 | “完成接诊”边界为 `(883, 243, 84, 38)`,右边界 967、下边界 281,完整位于窗口内;splitter 宽度为 `[322, 430]` |
| 1280 × 800 视觉 | 通过 | 五页、登录页、服务器设置和视频窗无重叠或横向裁切 |
离屏断言摘要:
```text
runtime_adapter = false
login = authenticated / 陈医生(演示) / loading released / password cleared
server_url = https://api.example.com/adminapi
server_timeout = 45
page_rows = reception 2 / library 2 / prescriptions 2 / patients 3 / consultations 1
reception_gender = 女 · 46岁 · 患者编号 301
prescription_gender = 赵明远 · 男 · 53岁
video_ids = appointment 101 / diagnosis 501 / patient 301
video_dialog = key 501 / duration 00:01 / mic off / camera off
compact_shell = 1024 × 640 / complete_button right 967 bottom 281
pytest = 69 passed
```
## 4. 问题关闭情况
| 问题 | 状态 | 本轮证据 |
| --- | --- | --- |
| F-01 问诊列表视频 ID 错置 | 已关闭 | 实际点击后得到 101 / 501 / 301,并由 Controller 创建 `DemoVideoDialog` |
| F-02 紧凑尺寸接诊详情横向裁切 | 已关闭 | Shell 最小尺寸为 1024 × 640;该尺寸下两侧 splitter 有效,“完成接诊”完整可见 |
| F-03 性别显示内部数值 | 已关闭 | 接诊显示“女”,处方详情显示“男” |
| F-04 Demo 登录 loading 调用不存在的方法 | 已关闭 | `LoginWindow` 使用 `BusyOverlay.set_message()`;无垫片真实登录成功进入 Shell |
## 5. 截图索引
1. [登录页 1280 × 800](../artifacts/ui_acceptance/01_login_1280x800.png)
2. [接诊台 1280 × 800](../artifacts/ui_acceptance/02_reception_1280x800.png)
3. [我的处方库 1280 × 800](../artifacts/ui_acceptance/03_prescription_library_1280x800.png)
4. [已开处方 1280 × 800](../artifacts/ui_acceptance/04_prescriptions_1280x800.png)
5. [我的患者 1280 × 800](../artifacts/ui_acceptance/05_patients_1280x800.png)
6. [问诊列表 1280 × 800](../artifacts/ui_acceptance/06_consultations_1280x800.png)
7. [Demo 视频窗口 980 × 660](../artifacts/ui_acceptance/07_demo_video_980x660.png)
8. [最小 Shell 1024 × 640](../artifacts/ui_acceptance/08_shell_compact_1024x640.png)
9. [问诊视频链路成功页 1280 × 800](../artifacts/ui_acceptance/09_consultations_video_success_1280x800.png)
10. [服务器设置 1280 × 800](../artifacts/ui_acceptance/10_login_server_settings_1280x800.png)
旧的 `08_shell_compact_900x600.png``09_consultations_video_error_1280x800.png` 是历史问题证据,不属于本轮最终截图索引。
## 6. 自动化测试记录
```text
uv run --offline pytest
..................................................................... [100%]
69 passed in 0.65s
```
本轮验收没有修改 `src/` 下任何源文件;仅覆盖本报告和 `artifacts/ui_acceptance/` 下的 PNG 截图。