# 业务订单辅方袋数:后端只读调查 调查日期: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 端口。