This commit is contained in:
Your Name
2026-10-17 16:51:48 +08:00
parent 56f85f7d89
commit ba2e2e4087
41 changed files with 2851 additions and 96 deletions
@@ -0,0 +1,99 @@
# 业务订单辅方袋数:后端只读调查
调查日期:2026-10-17。范围为业务订单详情的服用参数修改链路;未修改业务代码、未读取业务数据库或配置中的敏感信息。仓库 `.trellis/` 不存在,已按现有代码约定调查。
## 结论
辅方「5g · 1袋」中的袋数已经持久化在 `zyt_tcm_prescription.aux_usage.dosage_bag_count`,无需新增数据库字段。业务订单的修改入口是 `POST /tcm.prescriptionOrder/patchPrescriptionUsage`,当前只更新辅方每天次数和开立天数,没有接受或保存辅方袋数。建议沿用此入口增加可选参数 `aux_dosage_bag_count`,限制为 1~5 的整数;未传时保留旧袋数,兼容现有调用方。
## 字段与读写链路
- `server/sql/1.9.20260520/add_prescription_aux_usage.sql`:处方表的 `aux_usage` 是已有 JSON 列。
- `server/app/common/model/tcm/Prescription.php:21`:`aux_usage` 配置为 JSON,关联数组读写。主方袋数为处方独立字段 `dosage_bag_count`;辅方袋数位于 JSON 中。`bags_per_dose` 是另一个字段,不应用来保存本次截图的袋数。
- `server/app/adminapi/logic/tcm/PrescriptionLogic.php:183`:`normalizeAuxUsage()` 维护 `dosage_amount`、`dosage_bag_count`、`need_decoction`、`bags_per_dose`、`times_per_day`、`usage_days`、`prescription_name`。`normalizeDosageBagCount()` 将袋数约束到 1~5,默认 1。
- `server/app/adminapi/validate/tcm/PrescriptionValidate.php:15`:通用处方主方袋数采用 `integer|between:1,5`,可复用该业务口径。
- `server/app/adminapi/controller/tcm/PrescriptionOrderController.php:240`:调用 `post()->goCheck('patchPrescriptionUsage')`,再传入管理员身份调用逻辑层。
- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php:5770`:公开方法进入受保护事务,内部 `patchPrescriptionUsageLocked()` 加载订单与关联处方。当前合并旧 `aux_usage` 后仅改 `times_per_day`、`usage_days`,其余键保持不变。
- 同文件 `detail():1212` 复用处方详情,导出函数 `formatUsageSegmentForExport():3756` 和 `normalizeAuxUsageForExport():3807` 已读取辅方 `dosage_bag_count`。保存正确 JSON 键后,无需另建展示或导出字段。
- 辅方存在性以 `herbs[].formula_type === '辅方'` 判定;残留 `aux_usage` 不代表仍有辅方。
## 校验现状与必须注意的问题
当前业务逻辑强制主方次数 1~6、主方开立天数 1~999、订单服用天数 1~999;存在辅方时要求辅方次数及开立天数,并分别检查 1~6、1~999。
发现既有验证器场景命名问题:`PrescriptionOrderValidate.php:136` 定义的是 `patchPrescriptionUsage()`,而非 `scenePatchPrescriptionUsage()`。ThinkPHP `vendor/topthink/framework/src/think/Validate.php:1673` 的 `getScene()` 只调用 `scene` 前缀方法。因此控制器当前场景校验不会执行该方法中的 `append()`;静态 `$scene` 虽然列了用量字段,但这些字段没有基础 `$rule`,实际主要只校验已有的 `id` 规则。逻辑层先 `(int)` 转换再判断范围,也会把部分非整数输入截断为整数。
已用只加载 Composer 和内存验证器的 PHP 探针复现:同一组非法次数/天数参数,`scene('patchPrescriptionUsage')->check(...)` 返回 `true`,手动先调用旧 `patchPrescriptionUsage()->check(...)` 返回 `false`。探针不初始化业务应用、不连接数据库。
最小修复应正确使用 `scenePatchPrescriptionUsage()`,把新参数加进场景并追加 `integer|between:1,5`。业务逻辑还应在保存前验证袋数确实为整数并在范围内,避免直接调用逻辑方法时发生小数截断。无需改变其他接口的验证场景。
## 权限、状态与事务
- 现有接口权限为 `tcm.prescriptionOrder/patchPrescriptionUsage`,菜单迁移在 `server/sql/1.9.20260708/add_prescription_order_patch_usage_menu.sql`;新增袋数应沿用此权限。
- `AuthMiddleware` 根据菜单 URI 校验管理员权限;现有框架对未注册在菜单表的普通 URI 存在历史放行逻辑,因此部署应确保已有菜单迁移已执行。本次无需新增权限或迁移。
- 逻辑层先检查订单存在、`canAccessOrder()`、药房快照可变、订单未取消、已关联有效处方,以及 `PrescriptionLogic::canViewPrescription()`。
- `canAccessOrder()` 沿用当前全量角色、处方订单菜单访问、订单创建人、开方人授权或数据域可见创建人等口径;处方可见性另检查超管、创建人、医助、共享、可见角色及现有相关权限等。
- `LockedPharmacySnapshotMutation::execute()` 在 `Db::transaction()` 中锁订单行及药房提交声明行;内部返回 `false` 会导致回滚。新增字段必须留在此现有事务内部。
- `PharmacyRemoteSnapshotPolicy` 在已有药房远端单号、提交时间或声明状态为 `PENDING`、`UNKNOWN`、`PENDING_RECONCILE`、`SUCCESS` 时锁定修改。新增袋数不可绕开该限制。
## 日志
当前写入 `zyt_tcm_prescription_order_log`,`action = patch_rx_usage`,摘要为主方次数/天数、辅方次数/天数及订单天数的前后值。建议在原辅方段落加上「袋数 旧值 → 新值」,沿用当前日志动作,确保能回溯这次更改。
`writeLog():5723` 默认是非严格日志,日志保存失败会吞掉异常;本接口沿用该默认行为。是否改成严格日志属于额外行为变更,不是实现辅方袋数所必需。
## 最小修改建议
1. 修改 `PrescriptionOrderValidate` 的正确场景方法,仅对新增可选 `aux_dosage_bag_count` 检查整数及 1~5 范围。
2. 在 `patchPrescriptionUsageLocked()` 中,存在辅方且新参数已提供时才更新 `aux_usage['dosage_bag_count']`;继续复制旧 JSON,保留用量、名称、代煎等所有其他键。不修改主方 `dosage_bag_count`。
3. 缺少新参数时保留旧袋数;无辅方时沿用现有忽略辅方修改的逻辑。不因编辑次数或天数而把旧袋数重置为默认值。
4. 扩展 `patch_rx_usage` 摘要记录袋数前后值。
5. 验证有效 1、5,非法 0、6、小数、非数字,省略字段兼容,JSON 其他键保留,无辅方不产生新辅方用法,权限/取消/药房锁定失败,以及保存后回读与日志。参数校验至少要通过真实 `scene('patchPrescriptionUsage')` 路径测试,不能仅直接调用场景方法。
## 现有测试与本次验证
- 未发现针对辅方袋数修改或 `patch_rx_usage` 摘要的既有功能测试。
- `server/tests/pharmacy/run.php:809` 对本接口的受保护事务、错误清理及回滚信息有源码契约检查;它不验证真实辅方袋数持久化。
- `server/tests/pharmacy/prescription_order_export_name_regression.php` 覆盖辅方存在性及名称回退,本次运行 **7 项通过**。
- `php tests/pharmacy/run.php` 本次基线运行在 `run.php:870` 失败,提示 `tracking correction must append a strict operation log containing old and new logistics values`。这是快递修改日志的源码文本断言,调查时未修改任何后端业务代码,应与本次新增功能分开判断。
- 真实 ORM/事务测试可参考 `PrescriptionOrderReleaseAndTimeTest.php` 或 `PrescriptionOrderTrackingAuditTest.php` 的临时数据库夹具;它们要求显式指定隔离 MySQL 端口,不能直接用业务数据库。目前未运行这些数据库测试。
推荐从仓库根目录执行:
```powershell
php -l server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php
php -l server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
php server/tests/pharmacy/run.php
php server/tests/pharmacy/prescription_order_export_name_regression.php
```
若补充独立辅方袋数行为测试,应使用可控临时数据库或现有内存替身,覆盖上述范围、合并保存及权限/状态拒绝;测试命令随新测试实际路径确定。已有真实 ORM 测试仅在已准备隔离 MySQL 时运行,并按对应文件头说明设置环境变量。
## 实施结果(2026-10-17)
调查完成后,按授权实现了最小后端扩展:
- `PrescriptionOrderValidate` 已改为正确的 `scenePatchPrescriptionUsage()`,新增可选 `aux_dosage_bag_count`。使用场景闭包严格验证 1~5 整数/整数字符串;缺少字段可通过,但显式空值、布尔值、小数或其他类型会拒绝,避免框架普通可选规则跳过空值。
- `PrescriptionOrderLogic` 仅在处方含辅方且 `prescription_type` 为「浓缩水丸」时处理新袋数,逻辑层独立重复严格校验;合并原 JSON 保存该键,省略字段保留原值,其他处方类型和无辅方处方忽略袋数变更。原有主方、次数/天数、权限、取消限制、药房锁及事务边界继续沿用。
- 在 `patch_rx_usage` 原日志中增加「辅方袋数 X袋 → Y袋」;只在实际提供并处理袋数字段时记录,不改变原日志失败容错行为。
- 控制器仅更新注释。未新增数据库迁移,未修改本分工之外的前端代码。
新增 `server/tests/PrescriptionOrderAuxBagCountTest.php`,直接运行真实场景验证器和公开业务逻辑入口,使用独立本地 MySQL 随机临时库,结束后自动删除该库;不加载业务数据库配置。覆盖整数边界、非法输入、JSON/主方字段保留、保存后数据库及模型回读、旧请求省略字段、旧 JSON 无袋数字段、非浓缩水丸、无辅方及残留 JSON、日志、权限/取消/药房快照拒绝,以及处方已保存后订单更新失败的真实事务回滚。
验证结果:
- 新增真实验证器/ORM/事务测试:**83 项断言通过**。
- 相关 4 个 PHP 文件语法检查通过。
- 辅方导出名称回归:**7 项通过**。
- 3 个修改过的现有 PHP 文件 `git -c core.whitespace=cr-at-eol diff --check` 通过。
- 上文记录的药房套件快递日志文本断言基线失败未扩大修改。
在已准备隔离 MySQL 实例后,可从仓库根目录重跑:
```powershell
$env:ZYT_AUX_BAG_TEST_MYSQL_PORT='13483'
php server/tests/PrescriptionOrderAuxBagCountTest.php
php server/tests/pharmacy/prescription_order_export_name_regression.php
```
本次测试使用 `.codex-tasks/aux-bag-count-mysql/data` 的专用实例,绑定本机回环地址与 13483 端口。测试会拒绝未指定端口或默认 3306 端口。
@@ -0,0 +1,32 @@
# 处方 AI 一致度可见范围:后台实现
日期:2026-10-17。范围仅后台新文件与本报告、验证截图;没有修改 `server/` 或桌面 `app/`,没有提交、发布或连接生产接口。已读根 `AGENTS.md`,当前工作区无 `.trellis/`。
## 交付文件
- `admin/src/views/setting/prescription_ai_visibility/index.vue`:独立配置页面,沿用现有 Element Plus 设置表单与底部保存栏。
- `admin/src/api/setting/prescription_ai_visibility.ts`:专用 capabilities、配置读取/保存、角色与用户选项接口及类型;对配置响应进行运行时校验。
- `admin/tests/prescription-ai-visibility.test.cjs`:8 项 Node 测试,编译真实 Vue SFC、运行真实 setup 状态,并模拟 API;不调用真实后端。
- `artifacts/prescription-ai-visibility-admin-local.png`:本地模拟浏览器截图。
## 行为
角色多选可搜索名称;用户多选标签包含姓名与账号,因此均可搜索。选项完全来自 `tcm.prescriptionAi/visibilityOptions`,不额外请求角色列表或管理员列表接口。当前已配置但不在活跃选项中的 ID 以明确占位标签保留;只有用户显式移除后保存才取消该项。
页面明确说明:角色或指定用户任一命中即可;两份名单均为空时所有账号(包括 root)均隐藏一致度、逐味对照与一致度统计;root 可管理策略,但管理权限不自动赋予查看权。AI 报告继续按原有权限开放,已有报告不受影响,原有患者/处方数据范围继续适用。
先读取 `capabilities.can_manage_visibility`,只有严格为 `true` 才读取配置与选项。两者全部成功后才填入并启用表单;首次失败不展示空表单,重新加载失败保留既有选择并禁用保存。保存期间禁用输入、重载及重复提交;失败保留当前选择并给出重试/重新读取提示;成功应用后端返回的规范化名单。配置响应缺字段、含非数字/非正整数 ID 等异常会拒绝,不会降级成可保存的空策略。
后端菜单需指向 `setting/prescription_ai_visibility/index`,管理权限为 `tcm.prescriptionAi/manageVisibility`;菜单迁移及服务端自鉴权由后端任务实现。
## 验证
1. `node --test tests/prescription-ai-visibility.test.cjs`:8/8 通过。覆盖固定接口契约、配置坏响应、pending/失败禁止保存、选项失败不部分覆盖策略、缺失 ID 保留、姓名账号标签、空名单与规范化回显、防重复提交、保存失败保留、管理与查看能力分离、真实模板编译。
2. 对新增 API、Vue 页面、测试执行 Prettier `--check`:通过。
3. 针对新增 API 与页面的临时 `vue-tsc` 项目(严格模板,使用真实 Element Plus/Vue 类型,隔离 request/feedback 的外部依赖):通过,无类型错误。
4. 全后台 `vue-tsc --noEmit --pretty false --incremental false --composite false`:退出 2,报告 61 项新文件范围外的错误;没有错误涉及 `prescription_ai_visibility`。完整日志位于 `%TEMP%/zyt-prescription-ai-visibility-vue-tsc.log`。本任务没有扩大修改范围修复这些错误。
5. 独立 `127.0.0.1:5568` Vite 浏览器模拟,使用真实新页面、Element Plus、后台样式与 FooterBtns;所有 API 均映射到本地假数据模块,没有加载主应用或真实网络请求。确认成功读取、缺失 ID 标签、账号 `li.assistant` 搜索、保存 pending 禁用、保存成功提示、加载失败禁用保存、保存失败保留修改、空名单警示及无管理权限状态。
未运行 `npm run build`,避免其自带 release 脚本;实际 Vite 浏览器编译已成功。临时验证 harness 位于忽略的 `admin/node_modules/.cache/prescription-ai-visibility/`,不属于业务交付。
![本地模拟配置页](./prescription-ai-visibility-admin-local.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,63 @@
# 处方 AI 一致度授权:后端实施记录
日期:2026-10-17。仅修改 server 后端及本报告;未提交、未部署、未连接生产数据库或模型服务。
## 行为
- 新服务 `server/app/common/service/prescriptionai/PrescriptionAiVisibility.php` 使用 ConfigService 一条 JSON:`type=prescription_ai_visibility` / `name=agreement_policy`,字段仅 `role_ids:int[]`、`admin_ids:int[]`。
- 默认空、缺失或格式错误策略全部不可查看一致度。角色或单用户命中任一即可;查看没有 root、通配符或管理权限豁免。
- 账号每次重新检查 `disable=0`、`delete_time IS NULL`;角色关系实时查询并排除软删除角色。配置读取无长期缓存,授权或撤销下次请求立即生效。
- 独立管理权限为 `tcm.prescriptionAi/manageVisibility`。root 可管理,其余账号须持此权限;三个管理入口均在服务端统一验证,防止未注册路由中间件默认放行产生越权。所有入口仍由控制器重载有效账号,拒绝未知参数与错误 HTTP 方法。
- statuses 顶层及各 item、reports 顶层及各 batch、detail 顶层均返回 `agreement_visible`。未授权时共享 Logic 投影删除整个 comparison、扁平 score/herb_score、比较状态/原因/版本、比较类型/基线等派生数据;失败模型生成错误原因保留。
- full 模型正文使用固定字段及嵌套结构投影,拒绝历史正文未知 raw/result/score、调试对象、伪造 progress 等旁路。报告、候选方案、来源与覆盖说明、进度、医生原方、复核意见保留。
- statistics 在扫描/聚合之前拒绝没有白名单资格的账号;原有动作 RBAC 和处方/患者/来源范围校验仍执行。后台分析、比较、手动重试与复核的既有权限流程未加指标限制。
## 固定接口
| HTTP | `tcm.prescriptionAi/` 后 action | 数据 |
| --- | --- | --- |
| GET | capabilities | `{agreement_visible:boolean, can_manage_visibility:boolean}`,登录账号自查 |
| GET | getVisibilityConfig | `{role_ids:number[],admin_ids:number[]}`,仅管理者 |
| POST | setVisibilityConfig | 接收且仅接收上述两数组,返回去重排序后的策略;仅管理者 |
| GET | visibilityOptions | `{roles:[{id,name}],admins:[{id,name,account}]}`,仅管理者 |
选项及保存列表上界:500 个角色、5000 个后台用户。选项只返回活跃记录,超过上界明确报错,不静默截断。保存只验证新提交的名单,因此失效 ID 可从旧名单中删除后保存,不会被旧数据阻塞。角色无 disable/status 列,按软删除判断。
## 迁移
新增 `server/database/migrations/2026_10_17_prescription_ai_agreement_visibility.sql`:
- 幂等创建系统设置下 type C 的“处方AI一致度授权”菜单,perms 为 `tcm.prescriptionAi/manageVisibility`,component 为 `setting/prescription_ai_visibility/index`。
- 幂等预置一条空 JSON 策略,不自动授权任何非 root 角色,不填入查看白名单。
- 不新增业务表,不改变既有报告或队列数据。上线时需要先执行该迁移;本次仅在隔离测试数据库中执行并重放验证。
## 修改文件
1. `server/app/common/service/prescriptionai/PrescriptionAiVisibility.php`(新):策略、管理校验、实时匹配、有限选项、输出投影。
2. `server/app/adminapi/controller/tcm/PrescriptionAiController.php`:四个接口。
3. `server/app/adminapi/logic/tcm/PrescriptionAiLogic.php`:统一能力标记、指标裁剪、统计授权、正文投影。
4. `server/database/migrations/2026_10_17_prescription_ai_agreement_visibility.sql`(新):幂等菜单及空策略。
5. `server/tests/PrescriptionAiVisibilityTest.php`(新):无应用初始化的纯离线策略/投影回归。
6. `server/tests/PrescriptionAiQueueTest.php`:隔离真实 ORM、Cipher、Logic、Controller 及迁移回归。
## 验证结果
PHP 8.2.9;新增/修改的 5 个 PHP 文件全部 `php -l` 通过。`git diff --check -- server` 通过。全仓 diffcheck 中另有并行桌面端文件换行问题,已通知主代理,本任务未修改 app 文件。
纯离线:
- `php server/tests/PrescriptionAiVisibilityTest.php`:22 checks。
- `php server/tests/PrescriptionAiProgressTest.php`:38 checks。
- `php server/tests/PrescriptionAiComparisonTest.php`:266 checks。
- `php server/tests/PrescriptionAiStatisticsTest.php`:64 checks。
- `php server/tests/PrescriptionAiConfigurationTest.php`:通过。
隔离 MySQL:主代理建立的 `127.0.0.1:33447`,root 空密码,显式 `ZYT_AI_TEST_MYSQL_PORT=33447`。这些脚本不 App::initialize,不读生产 .env,创建并最终删除各自唯一 `prescription_ai_test_*` 临时库:
- `php server/tests/PrescriptionAiQueueTest.php`:113 checks。
- `php server/tests/PrescriptionAiQueueTest.php --legacy-progress-schema`:111 checks。
- `php server/tests/PrescriptionAiPipelineTest.php`:125 checks。
新增集成覆盖:默认空名单含 root 拒绝查看、root 管理与查看分离、独立非 root 管理权限、GET/POST/未知字段、伪 root/星号 token、配置越权拒绝、角色/个人任一授权、多角色、即时撤销、软删除角色、禁用/删除账号、无效策略、活跃最小选项与超限、菜单禁用即时撤销、allowlist 不替代动作 RBAC、真实加密旧正文注入及嵌套旁路、三个读取出口完整裁剪、统计拒绝、保留报告/候选/原方/复核/失败原因、限制指标后仍可重试/复核、迁移执行两次。
报告是实现交付,不表示生产迁移已经执行。测试 MySQL 进程由主代理负责最终关闭。
@@ -0,0 +1,118 @@
# 处方 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)来证明本特性;本功能可以完全使用上述隔离模式完成有效验证。
@@ -0,0 +1,57 @@
# 处方 AI 一致度可见范围:桌面实现交接
日期:2026-10-17。范围仅 `app/` 与本报告/离线截图;未修改 `admin/`、`server/`,未 commit、部署或调用生产接口。
## 完成行为
- 新增 `DoctorRepository` / `RemoteDoctorRepository.get_prescription_ai_capabilities()`,GET `tcm.prescriptionAi/capabilities`。
- 一致度只接受服务端明确的 `agreement_visible is True`。没有本地 root、角色 ID、`*`、管理策略能力的豁免;缺失字段、非布尔 true、能力请求失败均拒绝。
- 列表把 AI 状态与第 12 列一致度分离:状态仍按现有权限及分析开关加载,能力查询不阻塞状态请求。分数另需 capability + statuses 顶层 flag + 每批次 flag;统计按钮另需原 `statistics` 权限。
- 列表隐藏/重显、刷新都会重查能力;重新授权会重新取已完成批次的分数,避免从已清理的缓存展示空白。
- 共用报告(包括诊单入口)要求 reports 顶层、历史批次、detail 均明确授权。一致度受限时仍保留报告正文、模型筛选、AI 候选原文、来源与资料缺口、处理进度、历史批次选择与分页、重新分析、重试和复核。
- 受限时总览使用原有复核清单作为“报告复核”安全骨架,隐藏结论、组成归属、剂量图、风险剂量图及差异/共识计数。原“候选与逐味”导航改为“AI 候选处方”,跳到现有候选原文页;逐味页、比较原文和历史趋势不可导航进入。
- 顶部保留模型状态、资料状态与失败重试行,只隐藏分数、重合度、差值和仪表刻度;因此没有丢失原来附着在仪表卡上的重试按钮。
- 未授权数据进入缓存前清除 comparison、score、herb_score、比较状态/类型、算法版本及相关聚合元数据。撤权和读取失败同时清空旧数字、图表数据、比较 HTML、历史图/表、tooltip、accessibility 值和派生计数。
- 请求 token / generation 拒绝旧异步响应恢复分数;主题重建保持能力状态和复核草稿。
- 统计对话框直接构造也先请求 capability;未明确授权不发 statistics 请求。每次查询先清图,能力/统计失败、隐藏窗口、旧响应都会清理或拒绝旧结果。
## 修改文件
- `app/src/doctor_workstation/services/repository.py`:能力接口协议及远程实现。
- `app/src/doctor_workstation/ui/pages/prescriptions.py`:独立列/统计能力、列表缓存清理、异步 guard、受限入口文案。
- `app/src/doctor_workstation/ui/dialogs/issued_prescription_ai.py`:共享数据清理、报告各入口统一切换、统计 capability 门禁。
- `app/src/doctor_workstation/ui/dialogs/issued_prescription_ai_console.py`:仪表指标切换,同时保留状态与重试。
- `app/src/doctor_workstation/ui/dialogs/issued_prescription_ai_workspace.py`:复核安全骨架;旧行先清文本、hide、detach、delete,防窗口瞬闪和残留轴控件。
- `app/tests/test_issued_prescription_ai.py`:既有授权 fixture 明确 true;新增拒绝/缺失字段/非布尔 true、root 不豁免、撤权、失败、旧回调、主题、列表重显与统计直接入口回归。
- `app/scripts/render_issued_prescription_ai.py`:原有授权预览 fixture 明确 true。
- `app/scripts/render_prescription_ai_visibility.py`:可重复生成授权/受限离线截图。
变更行使用 LF,未改变的行按 HEAD 保留原始行尾 bytes,避免仓库原有混合换行导致整文件 diff。
## 验证
1. 核心 AI 测试,249 项通过(独立进程):
`app/.venv/Scripts/python.exe -m pytest app/tests/test_issued_prescription_ai.py app/tests/test_issued_prescription_ai_workspace.py app/tests/test_issued_prescription_ai_pages.py app/tests/test_issued_prescription_ai_comparison.py -q --tb=short`
2. 外围权限与处方界面测试,32 项通过(独立进程):
`app/.venv/Scripts/python.exe -m pytest app/tests/test_permissions.py app/tests/test_prescription_security_ui.py app/tests/test_prescription_list_density.py app/tests/test_prescriptions_tech_blue.py -q --tb=short`
3. Ruff 对上述 8 个变更 Python 文件检查通过。
4. `git diff --check -- app` 通过。
5. 新截图脚本离线执行通过;已目视检查受限列表、正文、候选原文、复核/重试和统计拒绝截图。
联合运行这 281 项时,核心及前面外围测试共 272 项无失败后,最后一组 `ShellWindow` 交互测试出现 Qt 同进程状态阻塞,已中断该联合进程;同一外围组单独执行通过。先前定位到的两个真实回归(旧控件瞬闪、旧轴残留)均已修复,相关定向测试通过。本交接采用上述两个独立进程验证结果,不宣称联合进程完整通过。
## 截图
目录:`artifacts/prescription-ai-visibility/`。全部为内存演示数据,无生产读取。
- `list-authorized.png` / `list-restricted.png`
- `report-authorized.png` / `report-restricted.png`
- `review-restricted.png`:复核清单、表单、保存与模型重试仍可用。
- `candidate-restricted.png`:候选原文可读,无贡献/一致度。
- `statistics-restricted.png`:直接统计入口拒绝,空图表且未请求统计。
服务端仍需按既定策略在 read API 屏蔽结构化比较,并在 statistics 强制校验;桌面不会从角色或登录快照重建授权。授权策略变更在下一次能力/状态/报告读取时更新显示,当前没有新增服务端推送通道。
@@ -0,0 +1,113 @@
# 已开处方 AI 一致度可见范围:前端只读研究
日期:2026-10-17。范围:桌面 `app/`、后台 `admin/` 的入口、登录元数据、配置承载点和测试;未修改业务代码,未调用任何生产接口。已阅读根目录 `AGENTS.md`;当前工作区 `.trellis/workflow.md` 不存在。
主任务已确定实施默认:仅一致度独立授权、保留报告;新独立设置页;角色或个人任一命中允许;空名单全隐藏;root 可管理策略,但查看分数也必须命中名单。API 使用 `GET tcm.prescriptionAi/capabilities -> {agreement_visible: bool}`,statuses/reports 顶层及每批次附该 flag,detail 顶层附 flag;未授权剔除 score/herb_score/comparison,statistics 拒绝。下文早期建议中的命名/承载页/初始行为均以上述约定为准。
## 结论
- 截图对应桌面 `PrescriptionsPage` 的第 12 列 `与 AI 一致度`。当前列显隐只跟随服务端 AI 总开关 `enabled`,没有按角色或个人控制一致度的能力。
- 现有权限只拆分 `statuses/reports/detail/statistics/regenerate/retry/review`。`reports + detail` 共同允许打开报告,不区分“能看报告”和“能看分数”。单靠收回现有权限无法实现“保留 AI 报告、隐藏一致度”。
- 仅隐藏列表列不完整:报告顶部双模型分数、逐味原文、总览结论、历史趋势、医生统计也展示一致度。
- 桌面已经从 `auth.admin/mySelf` 得到用户 ID、多个角色 ID、root 和权限集合;但运行中的会话权限不适合单独承担可见范围判定。推荐服务端每次读 AI 数据返回当前账号的明确 capability,并在响应中屏蔽未授权分数;桌面统一使用这个判定渲染。
- 后台没有现成处方 AI 配置页。最小配置入口可放现有“医生工作站”设置页单独配置区;该页目前专门管理升级包,建议新建独立读写配置接口,而不是把授权策略挂到其免登录更新检查响应。
## 桌面入口与显示面
| 位置 | 当前行为 | 仅一致度控制的接入点 |
| --- | --- | --- |
| `app/src/doctor_workstation/ui/pages/prescriptions.py:1050` | 顶部 `AI 一致度统计`;当前只查 `tcm.prescriptionAi/statistics` | 叠加一致度 capability;响应拒绝时清空既有统计 |
| 同文件 `:1200`、`:1583`、`:1765`、`:1773` | 工具栏 AI 报告、每行报告按钮、右键 `AI 报告 / 逐味对照` | 若保留报告应继续可用;文案可缩成 `AI 报告`,避免暗示全部对比可见 |
| 同文件 `:1255`–`:1273` | 第 11 列 AI 状态、第 12 列与 AI 一致度;两列初始隐藏 | 分离两列显隐,不能继续用 `_set_ai_columns` 同时切换 |
| 同文件 `:1289`、`:1344` | 单击两列打开同一报告;自定义 cell widget 的事件过滤也处理两列 | 隐藏分数时保留状态打开报告的行为 |
| 同文件 `:1647`–`:1762` | 只给可见处方批量拉 `statuses`,缓存后渲染;`enabled` 同时开启两列 | 读 capability、清掉旧第 12 列文本/tooltip/accessible text 及缓存中的分数,保留状态轮询 |
| `app/src/doctor_workstation/ui/dialogs/diagnosis.py:1080`、`:1726`、`:1983` | 诊单只读页也能按 `diagnosis_id` 打开已开处方 AI 历史 | 必须走同一共享报告权限与 capability |
| `app/src/doctor_workstation/ui/dialogs/issued_prescription_ai.py:401`、`:1817` | `can_open_issued_ai` + `present_issued_prescription_ai` 是共享报告入口 | 保留 reports/detail guard,另设 `can_view_agreement`;不要把报告 guard 改成分数 guard |
| 同文件 `:664`、`:1377`–`:1417`、`:1459` | `AgreementBar`、model score、可访问名/tooltip、原文 `comparison` 都有分数 | 分数不授权时不构建/隐藏分数区域,不填分数到隐藏控件,并清理原文内容 |
| 同文件 `:1210`;`issued_prescription_ai_pages.py:1156`、`:1191` | `HistoryPage` 从 `reports.lists` 渲染历史分数趋势,兼容 `comparison.score` 与模型顶层 `score` | 历史仍可选;关闭趋势分数,服务端历史摘要也需要去分数 |
| `issued_prescription_ai_workspace.py:674`–`:687` | 总览结论有双方一致率、纯药味重合度和百分点差 | 一致度受限时不得拼出这些结论 |
| `issued_prescription_ai_pages.py:364`、`:464` | 候选与逐味页说明算法、读取贡献行 | 要确认“仅一致度”是否包括每味贡献;至少不能泄露 contribution 与算法分子/分母 |
| `issued_prescription_ai_console.py:260`、`:295`、`:378` | 顶部仪表、无障碍名称、纯药味重合度 | 与主报告 capability 保持一致 |
| `issued_prescription_ai.py:1705`–`:1813`;`issued_prescription_ai_pages.py:1374` | 医生统计对话框;分布、均值、中位数、明细 HTML | 现有 statistics API 权限以外还应要求一致度可见;拒绝后整个统计关闭 |
补充:原处方详情 `dialogs/prescription.py` 没有独立已开处方一致度入口;目前可见入口集中在上述列表和诊单共享报告。后台 `admin/src/views/consumer/prescription/components/PrescriptionAiReportDialog.vue` 是另一个旧的处方解读报告流程,当前 `admin/src/views` / `admin/src/api` 没搜到 `tcm.prescriptionAi` 或一致度 UI,不应把它误当成本次双模型已开处方分析界面。
## 请求边界与元数据
`app/src/doctor_workstation/services/repository.py:1609`–`:1648` 透传以下服务端字典,新增 capability 无需先扩大类型 DTO:
- `statuses(ids)`:模型顶层 `score`、`comparison_status`,用于列表。
- `reports(prescription_id XOR diagnosis_id)`:历史摘要;传入 `page_no/page_size`。
- `detail(batch_id)`:完整 model `comparison`,用于报告全部页面。
- `statistics(date_from,date_to,doctor_id?)`:独立统计读接口。
- `regenerate/retry/review` 是显式写接口,此需求若只限制读分数,不必连带禁止已有 AI 生成和复核动作。
现有登录承载:
- `core/models.py:127`–`:176` 的 `UserProfile` 已包含 `id`、`role_ids`、`root`、`permissions`、完整 `raw`;多角色解析支持 `role_ids` / `role_id` / `roles`。
- `services/repository.py:902`–`:927` 的 `get_session()` 读取 `auth.admin/mySelf`,构造 `PermissionSet` 并存 session。
- `core/permissions.py:63`–`:83` 支持全局 `*` 与路由前缀 `/*`。`ui/widgets.py` 的 `has_permission` 已统一前端权限语义,新增功能不要另写角色硬编码。
- 后台 `admin/src/stores/modules/user.ts:110` 左右同样读取 `data.user/data.permissions/data.menu`。服务端 `AdminLogic.php:319` 已填 `role_ids`。
- 注意:`mySelf` 的 metadata 只是登录时快照;服务端改授权后应通过 AI read 响应刷新 capability,不能等用户下次登录才隐藏分数。
## 后台配置与选择器复用
### 可承载配置的页面
`admin/src/views/setting/desktop_workstation/index.vue` 有成熟 `el-form` + `getData` + `handleSubmit` + `footer-btns` 模式,当前管理升级策略;API 在 `admin/src/api/setting/desktop_workstation.ts`,对应 `setting.desktop_workstation/getConfig,setConfig`。可新增独立“处方 AI 一致度可见范围”设置区,提供角色多选 + 用户多选,并明确 OR 语义(命中任一角色,或被指定用户,均可见)。保存权限宜独立管理,避免升级包操作员默认拥有授权分配能力。
若希望未来扩展整个 AI 模块范围,新建 `setting/prescription_ai` 页面更清晰;当前没有可直接扩展的同名页。`ai_mcp/grant` 只管理员工外部 AI 助手令牌撤销,并非本功能授权。
### 角色与用户
- `admin/src/api/perms/role.ts:8` 的 `roleAll({})` → `/auth.role/all` 可复用。角色多选 UI 在 `views/permission/admin/edit.vue:73`–`:91`,`optionsData.role` 来自 `roleAll`;处方页 `consumer/prescription/index.vue:3303` 也有获取及失败降级模式。
- 当前角色配置 `views/permission/role/auth.vue` 是菜单树,保存 `menu_id` 到 `roleEdit`;适合给配置页面授予管理权限,但无法直接表达个人白名单。
- `admin/src/api/perms/admin.ts:4` 的 `adminLists` → `/auth.admin/lists` 是已实现的人员来源。`AdminLists.php:82` 支持 `name/account` 搜索,`:158` 支持角色筛选,`:165` 支持 `exclude_disabled=1`,列表带 `id/name/account/role_id`。建议用户多选下拉做姓名/账号远程分页,显示 `姓名(账号)`。
- **不要直接复用 `adminAll`**:前端虽导出了 `/auth.admin/all` 包装,但当前 `AdminController.php` 无 `all()`,全仓也没找到该路径映射。
- `admin/src/components` 没有通用后台用户多选组件。可用 Element Plus `el-select multiple filterable remote` 现有模式(处方页已有远程患者选择,但其数据类型和接口不能直接当员工选择器)。
- 现有处方模型 `visible_role_ids` 是处方审核/数据可见范围,不是分数可见范围;不要复用该字段造成整个处方行被隐藏。
## 仅一致度、保留 AI 报告的最小实现建议
1. 服务端策略保留为独立 role IDs + admin IDs;服务端返回 `can_view_agreement`(名称仅建议)而不是让客户端拼白名单。root 的默认行为、未配置的初始行为须与产品决策一致。
2. `statuses/reports/detail` 每个响应显式带当前 capability;未授权时剔除顶层/嵌套 `score`、`herb_score`、逐味 `contribution` 以及对应聚合分母,避免旧客户端或缓存旁路。统计接口直接拒绝未授权读。`enabled` 仍只代表分析功能总开关。
3. 桌面以独立布尔能力驱动第 12 列和统计按钮;第 11 列分析状态、AI 报告按钮与 reports/detail 权限保持现状。`_set_ai_columns` 应拆开或接受第二个参数。改变能力或请求失败时清理旧数据,不能只 `hide()`。
4. 共享报告统一把 capability 传至 AgreementBar / Workspace / HistoryPage / CandidatesPage;受限时保留完整报告、候选处方、来源、进度、复核与历史批次选择,但去掉一致度数字、趋势、贡献和相关说明。完整报告存量文本是否可能内嵌一致度,需要服务端研究确认;如允许模型原文包含此数值,仅过滤结构化字段不够。
5. 新后台配置区复用现有表单样式与 roleAll;人员选择用 adminLists 或专门受配置管理权限保护的最小字段 options 接口,避免配置管理员为了选人还必须拥有完整管理员列表权限。
待用户确认:“仅一致度”是只控制分数/重合度/贡献统计,还是连逐味剂量比较也一起隐藏;后者需要隐藏总览和候选逐味相关导航,但仍可保留报告正文。整个 AI 模块方案则简单得多:按新策略门禁所有 AI 入口和 API;不能在未确认时用此方案替代仅一致度方案。
## 可复用测试
本次只读研究未运行测试。现有测试使用内存 fake repository + `run_async` immediate fixture,不需要生产 API。
- `app/tests/test_issued_prescription_ai.py:440` `test_list_batches_visible_ids_and_stops_on_hide`:可延伸验证受限账号仍拉状态、只隐藏第 12 列。
- 同文件 `:462`、`:500`、`:525`、`:564`:空处方、总开关关闭、缺少 statuses 权限的入口及列显隐基线。
- 同文件 `:428` `test_view_permission_cannot_mutate`:保持 reports/detail 与写权限解耦。
- 同文件 `:1178` `test_access_revocation_clears_reports_and_all_progress`:可延伸为只撤销分数能力后清空数字,但正文保留。
- 同文件 `:596`、`:796`、`:977`:统计 null/版本分层/排除原因;补未授权统计不发请求且清空旧图。
- 同文件 `:626` `test_repository_exact_read_and_mutation_contracts`:可加服务端 capability 透传及不误改 mutation 请求。
- 同文件 `:697`、`:714`、`:1236`:追加列表缓存、右键目标、tooltip;可防分数在缓存/无障碍文本中残留。
- `app/tests/test_issued_prescription_ai_pages.py:288`–`:326`:历史读取嵌套和扁平两种分数;`:361`–`:418`:统计图表。新增受限 capability 时两种数据形状都不显示分数。
- `app/tests/test_issued_prescription_ai_workspace.py:239`:不捏造一致率差值;可加受限时不生成现有分数结论。
- `app/tests/test_permissions.py`:`*`、前缀通配符、AND/OR、空权限语义;如果新增菜单按钮权限应保持兼容。
- `app/tests/test_repository_parity.py`:可复用登录/session 的用户 ID、role_ids 解析测试结构。
- 后台当前无处方 AI 授权配置测试;`admin/tests/*.test.cjs` 是 Node 内建测试模式,`admin/package.json` 有 `type-check`,没有统一 test script。应新增只针对配置读写归一化、空选择与人员搜索的必要测试,避免写只镜像模板的快照测试。
## 实施交接:最小切换与 fixture
1. `issued_prescription_ai.py` 增加统一 `_apply_agreement_visibility()`(建议名)。`_build_ui` 默认隐藏;`_history_ready` / `_detail_ready` 应用 `agreement_visible is True`。缺失 flag 按 false;主题切换 `_switch_theme` 会重建 UI,必须继承能力并重新应用。从有权切成无权时,先清空旧 payload/图,再渲染正文。
2. 顶部 `self.agreement` 与 `self.agreement_note` 一起隐藏。注意每个模型的 **retry 按钮目前经 `self.agreement.attach_action(key,retry)` 放在仪表区域**(`:891`);如要求保留重试,需要移到进度页/独立状态区域。
3. 导航为 `NAV_PRIMARY`(`:68`),`self.nav_buttons` 是 `self.rail.buttons`(`:738`);overview/per_herb/history 都不是普通可见 tab,而是 rail 控制隐藏 tabBar 的 `QTabWidget`。隐藏导航后还要给 `_goto`(`:1065`)加拒绝/回落到 report,防 `_open_supporting_report`(`:1097`)和主题恢复旧页绕过。
4. 最小完整隔离方式:受限时隐藏 overview 与 per_herb 导航/页面、comparison_raw 原文页;默认切 report。报告原文、candidate、sources/gaps、pipeline、original 继续可用。`HistoryPage` 可保持批次列表但隐藏图和 3/4 分数列;更省改动可隐藏 history 页面,顶部 `self.history` 批次选择与分页仍保持完整历史访问。
5. **不能直接隐藏 overview 就结束**:`review_model` 与完整 review 表单在 `:898`–`:899` 被 reparent 到 `comparison_panel.checklist`。隐藏总览会丢失现有复核 UI。若保留复核,需把 selector + review widget 移到正文旁/独立复核页;或保留 overview 骨架,只隐藏 `conclusions/attribution/doses/risks` 及 comparison 链接,保留 checklist。`PrescriptionReviewWorkspace` 的这些已命名属性在 `issued_prescription_ai_workspace.py:1159`–`:1173`,有 `set_review_selector/set_review_widget`(`:1245`、`:1252`)。
6. `StepRail` 的 `focus_values`(`issued_prescription_ai_console.py:475`)与 `_render_console` 的 badges/focus 含差异、共识药味等派生计数。若 comparison 整体移除,清空/隐藏相关焦点行,避免把它们误显示为 0。保留来源缺口和报告数量即可。
7. `HistoryPage`(pages.py:1145)左 card 当前是局部变量 `left`;如仅隐藏趋势需将其保存为实例属性。表格 3/4 列是两模型分数,5 列算法;`strata` 和 `note` 也有“分数可以比较”等文案,受限时应清理。`set_history` 即使拿到剔除 comparison 的历史,也会保留这些文案,不能依赖没有数字自然隐藏。
8. `CandidatesPage` 的 `cards` 显示原方/候选处方;下方表格同时混合剂量与贡献。若保留候选卡片,则只隐藏/跳过逐味 body;否则整页隐藏最少。候选独立原文页 `tab_pages['candidate']` 依旧能保留原始候选数据。
9. `issued_prescription_ai.py:1414`–`:1417` 的 comparison HTML 应清空并跳过填充,而非仅藏页面。主界面 `model_views[*]['score']` 的 text/tooltip/accessibility、AgreementBar 内 gauge 和历史 `TrendBars` 都要拿空数据重置,避免 capability 撤销后留下旧值。
可直接扩展 fixture:
- `app/tests/test_issued_prescription_ai.py:24` 的 `batch()`,`:45` 的 `Repository`、`:86` 的 `no_network`(拒绝 socket)、`:95` 的 `immediate`;Repository 加 `agreement_visible` 属性和 capabilities 方法,read 字典统一附 flag。该组合最适合测从有权到无权的异步回调、缓存清理和主题重建。
- `app/tests/test_issued_prescription_ai_pages.py:35` 的 `batch(**overrides)`、`:66` 的 `per_herb`、`history` fixture,可用同一批次先 true 再 false,确认 score fallback 两种形状都无残留。
- `app/tests/test_issued_prescription_ai_workspace.py:25` 的 `batch(count)` 与 `:43` 的 `workspace` fixture 带真实保存逐味 identity 和 review slot;适合确认受限骨架不影响 review、report links 与布局。
@@ -0,0 +1,51 @@
# 处方 AI 一致度授权:验收记录
日期:2026-10-17。
## 用户确认范围
仅控制“与 AI 一致度”、一致度详情(逐味对照、贡献、差异等)与一致度统计,保留 AI 分析报告。授权对象可以是指定系统角色,也可以是具体后台用户,任一命中即可。
## 完成行为
- 新增后台“系统设置 → 处方AI一致度授权”页面,使用角色、用户两个可搜索多选框配置名单。
- 默认空名单对所有账号隐藏,包括 root;管理策略的权限与查看分数的权限分开。
- 服务端每次读取检查有效账号与实时角色归属。原有报告动作权限、患者/处方/来源范围继续执行,白名单不会扩大数据范围。
- 未授权账号的状态、历史和详情响应移除一致度及其派生数据,统计请求在聚合前拒绝;模型报告、AI 候选方案、进度、来源、重试、重新分析与复核保留。
- 桌面按服务端明确的布尔能力控制列、详情、导航和统计;缺失、失败按不可见处理。撤权后清理旧图表、比较原文、缓存、提示与可访问数据,旧异步回调不会恢复分数。
- 最终复核补齐了桌面缓存中比较原因、原因码、药材字典版本与基线序列化字段的清理,并增加保留报告/生成失败说明、不修改原输入的回归。
策略变更在下一次能力/状态/报告读取时反映到桌面;没有新增推送通道。
## 验证
| 范围 | 实际结果 |
| --- | --- |
| 最终桌面核心 AI 组合(主代理复跑) | 250 passed,14.57s,exit 0 |
| 桌面外围权限/处方界面(独立进程) | 32 passed |
| 后台实际 Vue setup/API 模拟(主代理复跑) | 8/8 passed |
| PHP 纯离线权限/投影(主代理复跑) | 22 checks passed |
| 隔离 MySQL 真实 ORM/Cipher/Logic/Controller(主代理复跑) | 113 checks passed |
| 后端 worker Pipeline / 旧进度 schema | 125 / 111 checks passed |
| 后端比较、统计、进度、配置回归 | 通过,详见后端报告 |
| Ruff、PHP lint、限定任务文件 diff 检查 | 通过 |
| 本地后台浏览器与桌面离屏截图 | 已检查授权/受限、报告保留、重试/复核、统计拒绝 |
临时 MySQL 仅绑定 127.0.0.1:33447,独立初始化且不读取生产配置;临时数据库由测试清理,进程已由主代理正常关闭。未调用生产数据库、模型接口或发布流程。
已知验证边界:将桌面核心与 ShellWindow 外围组放在同一进程运行时,最后一组发生 Qt 阻塞;分进程全部通过。后台全量类型检查有 61 项范围外错误,新增文件的严格模板类型检查通过。当前工作区还有其它任务在修改订单辅方袋数,本验收没有修改其文件;最终 diff 检查限定本任务文件。
## 上线与配置
代码尚未部署。需要执行 [幂等迁移](../server/database/migrations/2026_10_17_prescription_ai_agreement_visibility.sql),部署服务端及后台,并更新桌面端。迁移创建菜单和空策略,不自动把任何角色或账号加入查看名单。
管理员进入“系统设置 → 处方AI一致度授权”,选择角色或用户并保存后,相应账号在下次读取时显示一致度。已有处方和报告无需重新生成。
## 交付与预览
- [后端实施与测试](prescription-ai-visibility-backend-implementation.md)
- [后台实施与测试](prescription-ai-visibility-admin-implementation.md)
- [桌面实施与测试](prescription-ai-visibility-desktop-implementation.md)
- [后台授权配置预览](prescription-ai-visibility-admin-local.png)
- [受限账号处方列表](prescription-ai-visibility/list-restricted.png)
- [受限账号保留的报告](prescription-ai-visibility/report-restricted.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB