Files
zyt/artifacts/doctor-video-role-contract.md
T
2026-10-17 16:08:24 +08:00

103 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 桌面问诊列表:医生角色视频入口契约核对
核对日期:2026-10-17。范围仅为本地源码只读审查;未访问生产系统,未修改业务代码或测试代码。
## 结论
1. **医生角色是 `role_id=1`,医助是 `role_id=2`。** `3` 不是医生角色;“我的患者”管理角色集合为 `[3,7,8]`。
2. 管理后台 PC/H5 的旁观入口均要求:**有已预约的视频挂号、当前用户等于诊单 `assistant_id`、拥有 `tcm.diagnosis/watchCall` 权限**。无入口资格时只能看通话状态。
3. 服务端 `watchCall` 业务层再次强制 `assistant_id == 当前登录 admin_id`,**没有 root 绕过**。root 仅在通用鉴权中绕过菜单权限。
4. 需要区分源码现状与本次新要求:**现有后台没有显式的“角色数组包含医生 1 即拒绝”分支**。混合医生/医助角色如果恰好被指派为助理且有权限,按现代码仍可能进入旁观;不能把这一规则误报为现有后端保证。
5. 桌面列表当前不检查角色或助理归属,仅检查 repository 视频能力、预约和通话状态,因此医生也会得到“进入视频问诊”。本次用户要求应在桌面显示和点击处都补上医生角色限制。
6. 桌面该入口实际发出 `mode="im"`,获取的是 `getCallSignature` 通话/聊天凭证,**不是**后台的 `watchCall` 纯拉流接口;不能仅根据文案把两者视为同一实现。
## 本次实施与验证
- 桌面列表对含医生角色 `1` 的账号(含混合角色及通配菜单权限)只显示 `通话中 / 接通中 / —` 等公开状态,不显示进入视频问诊;同类工具栏入口同步隐藏。
- 表格显示、选中行按钮状态和实际点击处理使用同一资格判断;直接调用旧入口或强行启用按钮也不能发出视频请求。
- 非医生账号仍须同时满足本人诊单医助归属和 `tcm.diagnosis/watchCall` 权限;超级管理员也不绕过医助归属。有效预约、live 状态与标识完整性仍由原有流程检查。
- 医生的图文预约继续显示“图文沟通”,不受视频入口限制;接诊台的医生通话流程未改动。未将既有 IM 接收流程改造成纯拉流旁观,也未修改后端接口。
- 新增角色/指派/权限矩阵回归在修改前 27 项失败,修改后全部通过;覆盖角色整数、字符串、数组、逗号字符串、对象列表及 UserProfile,以及混合角色、root、无身份、错误指派、仅二维码权限。
- 最终组合检查:165 通过,1 项既有分页组合用例失败(`test_frozen_rows_track_main_pixel_scroll_and_host_height_is_append_stable` 的请求页码为 `[1,2]`,预期 `[1,2,3]`)。恢复 Git HEAD 的两个业务模块并排除新增用例后,同一组合仍复现该失败(136 通过、1 失败)。该分页用例及整个视觉模块单独运行均通过,未修改分页实现。
- Ruff、修改范围的 `git diff --check` 通过;离屏检查已生成 [医生视角截图](doctor-video-column.png),使用本地示例数据,未连接生产服务。源码未打包发布,运行中的客户端需更新重启。
## 角色与身份数据
| 项目 | 源码契约 | 位置 |
| --- | --- | --- |
| 医生 | ID `1`;数据库角色关联按成员包含判断,而非要求只有单一角色 | `server/app/api/logic/DoctorLogic.php:96`、`server/app/adminapi/logic/firstvisit/MyPatientLogic.php:22` |
| 医助 | ID `2` | `server/app/adminapi/logic/firstvisit/MyPatientLogic.php:23` |
| 管理角色 | `[3,7,8]`,用于“我的患者”的团队范围判断 | `server/app/adminapi/logic/firstvisit/MyPatientLogic.php:24` |
| 服务端旧角色字段 | `Admin` 模型追加 `role_id`,值是当前用户所有 `admin_role.role_id` 数组 | `server/app/common/model/auth/Admin.php:29`、`:44` |
| 身份接口 | `auth.admin/mySelf` 的 `user` 包含 `id`、`root`、`role_ids`(整数数组),兼容模型追加的 `role_id` 数组;外层为 `user/menu/permissions` | `server/app/adminapi/logic/auth/AdminLogic.php:285`、`:319`、`:327` |
| 桌面模型 | `UserProfile.id`、`role_ids: tuple[int,...]`、`root: bool`、`permissions`、`raw` | `app/src/doctor_workstation/core/models.py:125` |
| 桌面转换 | 优先 `role_ids`,再 `role_id`,再 `roles`;支持数字、逗号字符串、集合/列表和 `{id: ...}` 元素,归一成整数元组;root 兼容 `is_root` | `app/src/doctor_workstation/core/models.py:94`、`:141` |
| 当前用户流向 | repository 的 `get_session()` 读取 mySelf 并建立 `UserProfile`;shell 取得 `current_user` 后传入页面,`ConsultationsPage` 已保留 `self.current_user` | `app/src/doctor_workstation/services/repository.py:902`、`app/src/doctor_workstation/ui/shell.py:899`、`:2014`、`app/src/doctor_workstation/ui/pages/consultations.py:944` |
混合角色应检查**集合包含 `1`**,不能只比较 `role_id == 1`,也不能只检查角色数组首项。现有 shell 的角色文案也以 `1 in roles` 优先显示“医生”:`app/src/doctor_workstation/ui/shell.py:1521`。
权限 `"*"` 与 root 不能互相等同。后台 root 会获得 `['*']`,拥有全部菜单权限的非 root 也可能得到 `['*']`;桌面 `PermissionSet.is_superuser` 仅表示存在全局通配符,不代表 `UserProfile.root` 为真。
- 后台授予逻辑:`server/app/adminapi/logic/auth/AuthLogic.php:56`。
- 桌面通配权限逻辑:`app/src/doctor_workstation/core/permissions.py:60`。
## 管理后台显示与点击逻辑
PC:`admin/src/views/tcm/diagnosis/index.vue`。
- `:362`:列标题为“视频旁观”。
- `:1545`:`isAssignedAssistant` 使用 `row.assistant_id ?? row.assistant` 与 `userStore.userInfo.id` 数字比较。
- `:1563`:`canWatchCallEntry = isAppointmentActiveForVideo && isAssignedAssistant && hasPermission(['tcm.diagnosis/watchCall'])`。
- `:1971`:从 `appointments` 中找 `status==1` 的有效挂号,视频条件另要求 `canAppointmentVideoCall(type)`。旧空类型按视频处理,`text/phone/unknown` 不允许旁观视频,见 `admin/src/utils/appointment-type.ts:12`。
- `:1579`:有入口资格且状态为 `live` 或 `pending_room` 时显示“进入旁观”;pending 状态点击后仅提示等待。
- `:1591`:点击重新校验入口资格,并要求状态必须为 `live`,才打开旁观弹窗。
- `:1609`:无入口资格时显示公开状态:`live=通话中`、`pending_room=接通中`、`none=—`,其他使用服务端 label。
H5:`admin/src/views/tcm/diagnosis/index_h5.vue`。
- `:210`、`:1035`、`:1040`、`:1044`、`:1075` 分别对应显示、指派关系、资格、状态和点击校验;规则与 PC 一致。
- `:1068`:仅有入口资格,或状态处于 live/pending 时才显示旁观条;无资格时为只读状态文案。与 PC 的小差别是 none 状态文本为空,且整条通常不显示。
`hasPermission` 对 `"*"` 放行,但**不会跳过助理归属**:`admin/src/utils/perm.ts:3`。
## 服务端旁观保护与超管
`server/app/adminapi/controller/tcm/DiagnosisController.php:609` 的 `watchCall()` 从 GET 读取 diagnosis_id,但把调用者身份绑定为控制器的当前登录 `$this->adminId`,不采用客户端传来的 admin_id。
`server/app/adminapi/logic/tcm/DiagnosisLogic.php:2678` 的 `getAssistantWatchRoomParams()`:
1. diagnosis_id/admin_id 必须为正数。
2. 诊单必须存在且未删除。
3. `diag.assistant_id` 必须等于 admin_id,否则“仅本诊单指派的医助可旁观该通话”。
4. 获取该诊单 `status=1` 的最新 CallRecord,room_id 必须存在,否则要求等医生接通。
5. 返回当前登录账号对应的 `doctor_{adminId}` UserSig 和房间参数。
**此函数没有读取角色或 root,也没有主动重复前端的有效视频挂号校验。** 角色/menu 权限由通用鉴权负责;归属和进行中房间由业务逻辑负责。`AuthMiddleware.php:70` 的 root 放行发生在通用菜单鉴权层,不能使 watchCall 业务层的 assistant_id 检查失效。非 root 的菜单权限检查还受菜单路由登记情况影响,不宜把权限名称硬编码成额外、已存在的业务断言。
对照通话/聊天接口:桌面 `get_call_ticket()` 调用 `tcm.diagnosis/getCallSignature`(`app/src/doctor_workstation/services/repository.py:2696`)。该接口经 `DiagnosisLogic.php:898` 的 `canManageDiagnosis` 校验患者/诊单访问关系,并解析挂号通话策略;它不是旁观资格接口。`canManageDiagnosis` 使用 `MyPatientLogic`:root 可看全部、管理角色用团队数据范围、一线医生/医助按本人接诊/归属关系;这不意味着医生能从通用问诊列表旁观其他医生。
## 桌面入口缺口
| 环节 | 当前实现 | 位置 |
| --- | --- | --- |
| 列能力 | `action_policy['video_call'] = _native_video_capable`,只证明 repository 有 get_call_ticket/start_call/bind_call_room/end_call 方法 | `app/src/doctor_workstation/ui/pages/consultations.py:177`、`:1504` |
| 表格按钮 | 有能力 + 已预约 +(图文类型或 live),显示“图文沟通”或“进入视频问诊”;只用 ID 完整性控制 enabled | `app/src/doctor_workstation/ui/diagnosis_index_widgets.py:1784` |
| 顶部通话 | 可见性只检查原生能力,enabled 来自兼容 video_button | `app/src/doctor_workstation/ui/pages/consultations.py:1385`、`:3056` |
| 兼容按钮 | “进入视频问诊”仍存在于隐藏兼容操作容器,可通过程序调用 | `app/src/doctor_workstation/ui/pages/consultations.py:1467` |
| 行点击 | 选择该行后进入统一 `_request_video()` | `app/src/doctor_workstation/ui/pages/consultations.py:2366` |
| 最终点击处理 | 检查能力、已预约、live(text 例外)、ID 完整,然后发出 video_requested;没有医生角色/助理归属/旁观权限校验 | `app/src/doctor_workstation/ui/pages/consultations.py:3499` |
| 发出内容 | `source='consultations'`、`mode='im'`,注释明确此页为助理/管理员接收现有医生邀请 | `app/src/doctor_workstation/ui/pages/consultations.py:288` |
导航目前“我的患者”为 `PatientsPage`,“问诊列表”为 `ConsultationsPage`,两者不同;本次所述“视频问诊”列实际在后者 `DiagnosisTableHost`,见 `app/src/doctor_workstation/ui/shell.py:159`。本次无需扩大为其他医生主动发起通话入口的限制。
## 本次新要求对应的修复边界
- 医生角色集合包含 `1` 时,问诊列表的视频列不显示可点击“进入视频问诊”,并在最终点击处理再次拦截,避免工具栏、隐藏兼容按钮、信号或直接调用绕过。
- 混合 `[1,2]`、`[1,3]` 等账号也属于含医生角色。用户要求是医生角色禁用该视频入口,不能依赖它是否还有其他授权角色或 `"*"` 权限来恢复入口。
- 已有后台没有“root 优先于医生拒绝”的列表旁观特例;从本次“医生角色不允许”的文字看,含医生角色的账号即使还拥有 `"*"` 也不应因通配权限恢复入口。若实现保留 root 特例,应明确这是另行产品规则,而非从现有 watchCall 业务代码推导出的例外。
- 医生主动接诊/接诊台的视频功能不在本次旁观入口限制范围内。
- 表格“图文沟通”共用同一 action_policy 和 `_request_video()`,用户反馈针对视频入口;若不另有要求,应保留 text 预约正常图文入口,避免把 `video_call=False` 当作全部沟通能力开关。
- 对不受限制的非医生账号,本次改动可保留既有流程;若还要完整对齐后台的指派助理 + watchCall 权限,则属于另外一项权限收敛,需要单独决定与验证,不能把现有 IM 接收入口直接替换成旁观拉流实现。
本文件为静态核对结果,无运行或生产授权验证结论。