Files
zyt/artifacts/aux-bag-count-backend-research.md
T
2026-10-17 16:51:48 +08:00

10 KiB
Raw Blame History

业务订单辅方袋数:后端只读调查

调查日期: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 端口,不能直接用业务数据库。目前未运行这些数据库测试。

推荐从仓库根目录执行:

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 实例后,可从仓库根目录重跑:

$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 端口。