# 处方 AI 一致度授权:后端只读研究 日期:2026-10-17。范围:D:/web/zyt 本地源码;没有调用生产接口、连接数据库、调用模型或运行会初始化业务配置的测试。只新增本报告,未修改业务代码。 已读取根 AGENTS.md;工作区不存在 `.trellis/`。本文最初保留“仅一致度 / 整个 AI 模块”两种边界,随后按主代理明确的实现方向整理:**仅一致度独立授权,保留 AI 报告;角色或单个后台用户命中即可;默认空名单;root 可管理,但查看一致度也必须命中白名单。** ## 1. 现有权限模型 - `server/app/adminapi/logic/auth/AuthLogic.php:91`:`getAuthByAdminId` 经 `admin_role -> system_role_menu -> system_menu` 取权限,忽略 `is_disable=1` 的菜单。多角色权限取并集。 - `server/app/common/model/auth/Admin.php:52`:`role_id` 是 `AdminRole` 关联查询结果,不是单值;新策略必须支持任一角色命中。 - 没有发现通用“用户直接拥有菜单权限”表或管理机制。`AuthLogic:109` 会追加 `QywxPromotionOperatorAccess` 专用共享页面权限,是业务特例,不宜为了一个字段的可见性扩展全局 RBAC。 - `server/app/common/service/qywx/QywxPromotionOperatorAccess.php:38`:可参考其服务端动态单用户判断、软删除过滤、菜单启用检查;不应复用该业务表。 - `server/app/common/cache/AdminAuthCache.php:73`:菜单权限缓存最长 3600 秒;`:122` 清理具体账号缓存键。若本功能仅独立策略读取而不往 AuthLogic 注入权限,便不需要变更该全局缓存;每次请求实时读取策略可立即撤销。 - `server/app/adminapi/http/middleware/AuthMiddleware.php:129` 附近:未注册为菜单的 URI 有默认放行逻辑。因此**新配置接口必须自带服务端管理鉴权**,不能只加菜单或依赖前端 `v-perms`。 一致度的最终允许条件应是:账号有效 AND 现有该动作 RBAC 权限通过 AND 原有处方/患者/来源快照数据范围通过 AND 一致度白名单命中。名单只增加指标可见资格,不授予患者或报告访问权限。root 仍按既有逻辑拥有普通 AI 动作权限,但不能绕过新的指标白名单。 ## 2. 可直接采用的存储与接口形态 `server/app/common/service/ConfigService.php:32` 支持将数组 JSON 存到 `config` 表;`:65` 读取并反序列化。已有 `setting/DesktopWorkstationController.php` 与 `DesktopWorkstationLogic.php:39` 展示独立 getConfig/setConfig 的配置管理范式。 建议仅存一条对象(名字为候选,不代表已定路由): ```json { "role_ids": [1, 7], "admin_ids": [23, 91] } ``` - 候选存储:`type=prescription_ai_visibility`, `name=agreement_policy`。 - 缺失、格式错误、空名单均解释为不可查看一致度,不回退到“全部可见”。对象包含两个键,避免 ConfigService 将空数组视为假值并走默认配置的语义造成意外。 - 写入要求两个字段均为数组;元素须正整数/规范十进制整数字符串,拒绝对象、浮点、布尔、负数及字符串拼接;去重排序,验证角色/账号实际有效;限制数量及拒绝未知字段。不要静默把任意字符串 `intval` 后授予权限。 - 一次覆盖单 JSON,避免分别保存角色、用户导致半更新。`config` 安装表没有 `(type,name)` 唯一索引(`server/public/install/db/like.sql:142`),可由幂等迁移预置这一条空策略,避免首次并发创建;不要为本功能顺带对全表增加唯一约束。 - 不存模型密钥、不新增业务表,不修改 `prescription_analysis.enabled` 或后台工作队列设置。 候选 API: | 接口 | 返回/接收字段 | 必须执行的服务端鉴权 | | --- | --- | --- | | GET `tcm.prescriptionAi/capabilities` | `{agreement_visible:boolean, can_manage_agreement_visibility:boolean}` | 登录且实时有效账号;只返回当前账号布尔能力,不泄露名单 | | GET 专用 `getVisibilityConfig` | `{role_ids:number[], admin_ids:number[]}` | root 或独立管理权限 | | POST 专用 `setVisibilityConfig` | 严格上述两数组;返回规范化后的策略 | 同一管理鉴权;必须 POST | | GET 专用 `visibilityOptions` | `roles:[{id,name}]`, `admins:[{id,name,account,role_ids}]`,可增加有界搜索/分页 | 同一管理鉴权;只投影必要字段 | 配置 API 既可设在 `PrescriptionAiController`,也可采用 `setting.prescriptionAiVisibility/getConfig|setConfig|options`;关键是最终 action/perms 一致且逻辑层鉴权。若采用一个专用“管理一致度授权”权限,所有 get/set/options 均调用共同 `requireManage`,默认不向普通角色自动授予。不要把名单加到 `ConfigController::getConfig`:该控制器 `server/app/adminapi/controller/ConfigController.php:27` 明确 `notNeedLogin=['getConfig','dict']`。 ## 3. 角色和用户选项:已有 API 与 active 字段 | 对象 | 当前可用 API | 实现与约束 | | --- | --- | --- | | 系统角色 | GET `/auth.role/all`,前端 `roleAll()` | `RoleController:118` -> `RoleLogic:181`;`SystemRole` 使用 SoftDelete,自动排除软删除;按 sort/id 降序 | | 后台用户 | GET `/auth.admin/lists?exclude_disabled=1&page_no=1&page_size=...`,前端 `adminLists()` | `AdminLists:165` 排除 disable;返回 `lists/count` 的列表封装,支持 name/account 模糊搜索 | | 后台全部用户 | 前端有 `adminAll()` wrapper `/auth.admin/all` | **后台 AdminController 没有 all action,不可直接使用此 wrapper** | 模型事实: - `Admin`:`delete_time IS NULL` 且 `disable=0` 才是活跃账号,见 `PrescriptionAiAccess::actor` 第 15 行;必要字段 `id/name/account/root`;角色经 `admin_role` 关联。 - `SystemRole`:`delete_time IS NULL`,没有发现 `disable` 或 `status` 字段;`server/public/install/db/like.sql:655` 及现有迁移仅见额外 `data_scope`。**不要对角色添加 `disable=0` 条件。** - `system_menu.is_disable` 是菜单状态,与系统角色不同。 - `AdminLists` 返回手机号、登录 IP、头像等较多字段,授权下拉建议在新管理接口中直接复用模型查询,并只投影 id/name/account/role_ids。 - `AdminLists::queryWhere` 一般默认不套 data_scope,只有显式 `apply_data_scope=1` 才启用;管理授权选项须由共同管理鉴权保护。其 role_id 过滤在该角色无成员时不会强制空集,因此若需角色联动筛选,专用 options 应正确处理空集。 - `BaseDataLists.php:102`:`page_type=1` 表示分页,其它值只是返回最大条数,不是真正无限制;不能假定一次 `page_type=0` 可取完所有后台用户。 - 即使保存时验证过 ID,查看时也只匹配实时有效账号与有效角色;被禁用/软删除账号不可使用旧 token,角色变更不可依赖 token 的历史 role_id。 ## 4. 现有处方 AI 数据保护与必须覆盖的出口 `server/app/adminapi/controller/tcm/PrescriptionAiController.php:71` 的 handle 拒绝未知入参,校验 GET/POST,调用 `PrescriptionAiAccess::actor` 重新加载有效账号,不直接信任请求内角色。`PrescriptionAiAccess.php` 还分别校验处方、诊单及来源诊单范围。应保留这些保护,并在新增 capabilities/配置入口也使用同等实时账号检查。 `PrescriptionAiLogic.php` 的重要出口: | 入口 | 当前信息 | 仅一致度授权时的处理 | | --- | --- | --- | | `statuses:22` | 列表轮询调用 `models(...,false)`,有 `score/herb_score/comparison_status/comparison_reason_code/reason/algorithm_version/dictionary_version` | 返回 `agreement_visible`;未授权时使用统一投影去掉所有比较字段,保留模型生成状态/进度 | | `reports:62` | 历史列表同样 `models(...,false)`,不能只保护 statuses | 同上;继续先按 visibleBatch 过滤再分页,不泄漏隐藏来源计数 | | `detail:107` | `models(...,true)` 解密正文,又包含医生原方快照 | 未授权仍保留报告、候选方及医生原方(按保留报告的当前范围),统一去掉比较数据;不要返回原始解密 body | | `statistics:206` | 均值、中位数、直方图、版本分层、paired_count 等全是这一指标的衍生信息 | 新白名单检查失败时直接抛 DomainException;必须在扫描/聚合前拒绝;不以 mean=null 等方式继续返回统计 | | `regenerate/retry/review` | 返回批次状态或 saved,没有返回比较正文 | 仅指标限制不应阻止既有报告操作;仍保留现有动作 RBAC、处方与来源范围 | 具体易漏点: 1. `models:285` 的数据库摘要列不仅 `score`,还包括 `herb_score`、比较状态/原因与版本。 2. `models:302-315` 现为 `array_merge(task,result,body,progress)`。加一条 `unset($model['score'])` 不够:解密 body 里的 `comparison` 含同一分数,并可能包含历史未知字段或顶层别名;原始 body 不应不加选择地 merge 到对外模型。 3. `Store::complete:348` 当前加密 body 的固定键是 `report,candidate,comparison,coverage,usage,knowledge_source,ancient_book_sources`,不是必须输出的完整 API 对象。建议按这些已知业务字段构造响应,单独按能力投影 comparison。这样历史 body 即使含 `score`, `raw`, `result`, `response`, `body`, `comparison_dump` 等未知附加键,也不会直接公开。 4. `Comparison::compare:126` 的完整 comparison 除分数外还包含 `rows`、药量 ratio/贡献、`matched_count`、doctor/candidate_count、`usage_differences` 和 `normalization` 中的 denominator/contribution_sum,足以反推指标。未授权应移除**整个 comparison 对象**。 5. `models:310` 的扁平 `reason` 取自 `body.comparison.reason`,也是比较派生信息;无结果任务的 `reason` 则是生成错误信息。应区别处理或在统一投影中使用保留的 `error_message` 展示模型失败原因,避免比较 reason 保留下来。 6. `formatBatch:348` 还输出 `comparison_type`, `baseline_eligible`, `baseline_exclusion_reasons`,也应从未授权响应中移除。`coverage_status/source_summary/missing` 属于报告资料覆盖信息,可保留。 7. 不把 `body_cipher` / `context_cipher` / `progress_cipher` / 原始 AI 调用响应增加到任何返回值。现有 Progress 采用固定字段投影并本地生成 label/notice,可继续复用;比较阶段名称本身不包含指标结果。 8. 受保护字段应该被完全省略,而非伪装为 0 或“未生成”;布尔 `agreement_visible=false` 告诉 UI 不渲染指标功能。 保留医生原方与候选方案意味着用户仍可人工比较药味;本需求保护的是系统计算的一致度与相关比较/统计数据。若用户后续改为整个模块不可见,则应在所有 AI 动作的业务入口一并拒绝,前端隐藏全部入口;不能把当前字段裁剪方案误当作整模块授权。 ## 5. MCP 与其它旁路 `server/app/mcp/catalog/review/tcm.php:185` 已开放 `detail/reports/statuses/statistics`,并说明依赖各入口内置数据范围保护。必须将授权/投影放在共享业务层,HTTP 控制器最终输出前才过滤会遗漏直接 Logic 测试或其它调用通路。新管理配置接口不应自动加入开放 MCP catalog。 对 `server/app` 的引用检索未发现这四个入口之外直接返回 `prescription_ai_result/body_cipher` 的业务出口;Store/Worker 为写入与后台计算。仍应在实现完成后检索新增调用与 MCP generated 元数据,确认统计内置保护说明匹配新规则。 ## 6. menu 迁移惯例 - 当前处方 AI 迁移 `server/database/migrations/2026_09_09_prescription_ai_analysis.sql:132`:通过现有处方列表 perms 查父级;`INSERT ... SELECT ... WHERE NOT EXISTS(perms)` 幂等创建 type A 动作。 - 原有 7 个动作分别注册 statuses/reports/detail/regenerate/retry/review/statistics;读权限继承自 patientAiReports,写权限继承自 generatePatientAiReport。**本需求不应把管理授权能力自动继承给这些读写角色。** - 现有设置页范式 `2026_08_21_desktop_workstation_update_menu.sql`:查询系统设置 type M 父级;type C 页面使用 getConfig perms/path/component,子级 type A 保存动作;时间戳 UNIX_TIMESTAMP,空 path/component 给动作,is_show=1/is_disable=0,事务包裹。 - 可参考其幂等菜单写法,但本需求管理菜单默认 root 可用;非 root 只有明确授予独立管理权限后可管理。不要复制“所有设置角色自动继承”的授予部分。 - 新 capabilities 仅返回当前身份能力且必须登录,是否注册菜单权限需与中间件设计一致;避免将自查接口变成必须先拥有一致度权限才可调用的循环依赖。 ## 7. 可复用测试模式(本次未运行) 1. **纯函数/契约测试**:`server/tests/PrescriptionAiProgressTest.php`。直接 require autoload、不初始化 App,用固定 fixture 检查允许字段;策略匹配、规范化、投影最适合此方式。 2. **无环境隔离桩**:`server/tests/DiagnosisValidateEditSceneTest.php:9` 创建 `new think\App()` 但不 initialize;在真实类加载前以 class_alias 安装内存模型/DB/Auth double。适合验证真实入口会拒绝未授权 statistics 与配置写入,避免初始化业务 .env。 3. **真实加密/数据库 API 集成**:`server/tests/PrescriptionAiQueueTest.php:5` 明确禁止 initialize,必须显式 `ZYT_AI_TEST_MYSQL_PORT`,只连接 `127.0.0.1` 空密码临时 MySQL,数据库名只允许 `prescription_ai_test_[a-f0-9]{12}`,显式注入隔离 DB/config 并最终清理;现有 fixture 已覆盖 statuses/reports/detail/statistics、结果 Cipher、失败模型和旧 body 注入。扩展时可添加 config/system_role 夹具、非 root 多角色及账号。 4. **迁移幂等**:QueueTest 重放 migration 两次并检查菜单数,可直接复用新菜单和空策略迁移的验证方式。 5. **多角色缓存边界**:`AdminMultiRoleRegressionTest.php` 说明角色 ID 去重排序及撤销角色后 token/cache 失效;新指标策略应按实时 role_id 与新策略判断,不从旧 token 缓存白名单结果。 6. **本地配置文件测试**:`PrescriptionAiConfigurationTest.php` 使用自定义 env fixture,绝不 bootstrap。可用于确认本次没有意外改变模型启用/生成配置。 最低必要用例:默认空/非法策略全部不可见(包括 root);只选角色/只选用户/任一匹配/多角色并集/其它账号不匹配;禁用或软删除账号、删除角色、实时撤销;未授权仍能读既有报告内容但 statuses/reports/detail 任何层均不存在比较字段或未知原始 body;授权用户数值保持不变;未授权 statistics 拒绝而不聚合;非管理者不能读名单、改名单或列选项;root 可管理但不会因此获得 view;已有处方/来源权限撤销仍拒绝报告。加入带唯一哨兵的嵌套 comparison/unknown raw body fixture,序列化整个响应检验哨兵不存在,不能只验证 UI 字段或顶层 score。 不建议直接运行会 `App::initialize()` 的旧测试(例如 PrescriptionAiSecretSafetyTest)来证明本特性;本功能可以完全使用上述隔离模式完成有效验证。