Commit a0f846f2 by luoqi

fix(越权): 三个读接口不校验 clinicId —— 主管能看到别家诊所的名册/负载/池子矩阵

产品在测试机上发现工作台发了两个 workload 请求、clinicId 不一样。
查下去不只是"多发一个",是**那一发真的取回了别家的数据**。

── 服务端(真问题)──────────────────────────────────────────
`/plans/assignments/agents`、`/plans/assignments/workload`、`/plans/matrix`
三个读接口拿着查询串里的 clinicId 直接查,不校验是否在 scope 内。
实测:北京朝阳公园的主管(clinicIds=[66701845…])带杭州大厦的 id 请求,
拿到 200 + 那家 26 位客服的**姓名与在手负载**;矩阵同样能拿到完整患者量分布。

️ 写路径一直是拦的(create 里那句 includes 判断),所以分不走别家的人 ——
   但名册是员工姓名、矩阵是患者量分布,读一样不能敞。

闸抽成 common/decorators/resolve-clinic-id:不传取第一个诊所,传了必须在范围内,
范围外抛 Forbidden(10107)并列出真实 id —— 不能"当成这个诊所没人"返回 0,
0 是合法答案,静默返回会让助手拿着 0 去解释"为什么这批人是空的"(违 T14)。
集团级(clinicIds 为空)原样放行, 别把空数组当成没权限。

MCP 里原来抄了一份一模一样的实现 —— 抄一份的直接后果就是补的时候只补了一边。
现在两边共用一份。

── 前端(触发源)────────────────────────────────────────────
第一帧 user.clinicIds 还是 undefined(JWT 里没有这一项,只能等 /auth/session),
visibleClinics 回落到"字典里的全部诊所" → 锁了第一个「杭州大厦」→ 发出那一发。
session 回来后自己纠正成朝阳公园,所以肉眼只看见"发了两个请求"。

新增 clinicScopeReady():undefined=还没加载 / []=集团级,两者必须分开 ——
 别改 visibleClinics 的回落语义去顺手修,那会让集团级用户的筛选器空掉。
实测改后:进工作台只发 1 个 workload,clinicId 正确。

️ 前端等待与服务端闸是**两层**,缺一不可:查询串是用户可改的。

测试:回归从"grep MCP 源码"改成锁**闸只有一份实现** + **每个收 clinicId 的
REST 读接口都过闸**(按 @Query('clinicId') 出现次数比对),这正是漏掉的那一类。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 15b347e3
Pipeline #3550 failed in 0 seconds