This commit is contained in:
gr
2026-10-15 11:03:54 +08:00
parent 70ddc1a70a
commit bb28d93cec
64 changed files with 12747 additions and 420 deletions
+438 -7
View File
@@ -1,6 +1,6 @@
<?php
// 由 php app/mcp/cli/catalog.php --write 生成,请勿手工修改;人工审核结论写在 resources.php。
// 生成时间:2026-09-24 08:54:20
// 生成时间:2026-10-14 12:06:18
return array (
'article.article/add' =>
array (
@@ -8494,6 +8494,7 @@ return array (
),
'params' =>
array (
0 => 'kind',
),
'no_login' => false,
),
@@ -9034,8 +9035,7 @@ return array (
'http' => 'POST',
'writes' =>
array (
0 => '->save(',
1 => '::create(',
0 => '::transaction(',
),
'external' =>
array (
@@ -10063,10 +10063,13 @@ return array (
19 => 'latest_appointment_channel_source',
20 => 'latest_assign_start_date',
21 => 'latest_assign_end_date',
22 => 'sort_unserved_days',
23 => 'pending_assign_keyword',
24 => 'keyword',
25 => 'pending_assign_order_month',
22 => 'revisit_state',
23 => 'revisit_days_bucket',
24 => 'sort_revisit_due',
25 => 'sort_unserved_days',
26 => 'pending_assign_keyword',
27 => 'keyword',
28 => 'pending_assign_order_month',
),
'no_login' => false,
),
@@ -10806,6 +10809,238 @@ return array (
),
'no_login' => false,
),
'tcm.followupAudio/apply' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'apply',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/audio' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'audio',
'kind' => 'other',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/capabilities' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'capabilities',
'kind' => 'other',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/create' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'create',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
0 => '::create(',
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/detail' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'detail',
'kind' => 'detail',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/lists' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'lists',
'kind' => 'report',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/retry' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'retry',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/saveDraft' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'saveDraft',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/uploadChunk' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'uploadChunk',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/uploadComplete' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'uploadComplete',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.followupAudio/uploadSession' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\FollowupAudioController',
'action' => 'uploadSession',
'kind' => 'write',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.prescription/add' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\PrescriptionController',
@@ -12396,6 +12631,202 @@ return array (
),
'no_login' => false,
),
'tcm.revisitDue/close' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'close',
'kind' => 'write',
'lists' => NULL,
'http' => 'POST',
'writes' =>
array (
0 => '->update([',
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::close',
1 => 'RevisitDueLogic::getError',
),
'params' =>
array (
0 => 'id',
1 => 'reason',
2 => 'remark',
),
'no_login' => false,
),
'tcm.revisitDue/contact' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'contact',
'kind' => 'other',
'lists' => NULL,
'http' => 'POST',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::contact',
1 => 'RevisitDueLogic::getError',
),
'params' =>
array (
0 => 'id',
1 => 'promised_date',
2 => 'remark',
),
'no_login' => false,
),
'tcm.revisitDue/lists' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'lists',
'kind' => 'list',
'lists' => 'app\\adminapi\\lists\\tcm\\RevisitDueLists',
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
),
'params' =>
array (
0 => 'assistant_id',
1 => 'dept_id',
2 => 'keyword',
3 => 'min_snooze',
4 => 'state',
5 => 'days_bucket',
),
'no_login' => false,
),
'tcm.revisitDue/logs' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'logs',
'kind' => 'report',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::logs',
1 => 'RevisitDueLogic::getError',
),
'params' =>
array (
0 => 'due_id',
),
'no_login' => false,
),
'tcm.revisitDue/pending' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'pending',
'kind' => 'other',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::pending',
),
'params' =>
array (
),
'no_login' => false,
),
'tcm.revisitDue/snooze' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'snooze',
'kind' => 'other',
'lists' => NULL,
'http' => 'POST',
'writes' =>
array (
0 => '->update([',
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::snooze',
1 => 'RevisitDueLogic::getError',
),
'params' =>
array (
0 => 'id',
),
'no_login' => false,
),
'tcm.revisitDue/snoozeAll' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'snoozeAll',
'kind' => 'other',
'lists' => NULL,
'http' => 'POST',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::snoozeAll',
),
'params' =>
array (
0 => 'ids',
),
'no_login' => false,
),
'tcm.revisitDue/summary' =>
array (
'controller' => 'app\\adminapi\\controller\\tcm\\RevisitDueController',
'action' => 'summary',
'kind' => 'report',
'lists' => NULL,
'http' => 'GET',
'writes' =>
array (
),
'external' =>
array (
),
'logic' =>
array (
0 => 'RevisitDueLogic::summary',
),
'params' =>
array (
),
'no_login' => false,
),
'tools.generator/dataTable' =>
array (
'controller' => 'app\\adminapi\\controller\\tools\\GeneratorController',
+23 -6
View File
@@ -18,8 +18,12 @@
* 子接口自身已登记时仍按自身判断(与后台一致);未登记时依次改用 Tab 权限(tabLeaderboard/tabDoctor/tabZyyt…)
* 或页面权限 fans/yeji ——后台对未登记接口不校验、只靠这些权限控制页面可见,这样不会比后台页面更宽。
* - timeout:业绩看板、医生统计、提成结算、综合转化是多张大表的聚合(后台控制器自己放宽到 120 秒),单条 SQL 超时放宽到 30 秒。
* - controller:晚于 generated.php 生成的 action(如 stats.assistantPerformance/revisitRate)显式写 controller 以通过 AiMcpUnitTest 的
* 资源存在性校验;重跑 catalog.php --write 后可去掉该键。
*/
use app\adminapi\controller\stats\AssistantPerformanceController;
$dateRange = [
'start_date' => '开始日期 YYYY-MM-DD(不传默认今天)',
'end_date' => '结束日期 YYYY-MM-DD(不传同开始日期)',
@@ -43,9 +47,18 @@ return [
],
],
'stats.assistantPerformance/revisitRate' => [
'status' => 'open', 'kind' => 'report', 'name' => '医助个人业绩 · 我的复诊接诊率',
'controller' => AssistantPerformanceController::class, 'http' => 'GET',
'note' => '仅返回当前账号本人的复诊接诊率行(RevisitRateLogic::assistantOverview:从 stats.revisitRate/overview 全量结果中按本人 assistant_id 挑出一行,不含其他医助、不含部门/合计行),口径与复诊接诊率页一致(inherit_mode 固定取配置 project.revisit_rate.default_inherit_mode,默认 slot=继承患者按继承时诊次从第 k+1 诊起计入分母)。row 字段:assigned_count 新指派数、inherit_count 继承数、assigned_total_count 被指派合计、visit{N}_count/_den/_rate 各诊次单数/分母/接诊率(den=0 为 null)、visit_all_count/_den/_rate 复诊合计(可超 100%);slots 为诊次列范围。本人非二中心医助(in_scope=false)或当月无任何被指派/成交(in_scope=true)时 row=null。结果缓存 600 秒。',
'params_allow' => [
'month' => '统计月份 YYYY-MM(默认本月)',
],
],
'stats.autoAssignLog/lists' => [
'status' => 'open', 'kind' => 'list', 'name' => '待分配诊单自动指派日志',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到全部自动指派日志(含患者姓名、手机号快照)。数据由定时任务 tcm:auto-assign-pending 写入,每条待指派诊单一行;action 1=已分配、0=未分配,tier 为医助上月二诊复诊接诊率档位(gt70/60_70/50_60),reason 为分配或不分配原因。',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到全部自动指派日志(含患者姓名、手机号快照)。数据由定时任务 tcm:auto-assign-pending 写入,每条待指派诊单一行;action 1=已分配、0=未分配,tier 为医助上月二诊复诊接诊率档位(gt70/60_70/50_60),reason 为分配或不分配原因。口径:visit2_rate=新口径(inherit_mode=slot,分母=新指派+起算诊次≤2 的继承)、visit2_rate_new_assign=旧口径(分母仅新指派)、rate_version=本批次分档所用口径(v1=旧 v2=新,由配置 project.revisit_rate.auto_assign_tier_field 决定;2026-10 前的历史行无 visit2_rate_new_assign、rate_version=v1 且 visit2_rate 即旧口径)。',
'params_allow' => [
'run_date' => '执行日期 YYYY-MM-DD(精确匹配)',
'start_date' => '执行日期起 YYYY-MM-DD',
@@ -172,39 +185,43 @@ return [
'stats.revisitRate/assignLines' => [
'status' => 'open', 'kind' => 'report', 'name' => '复诊接诊率 · 被指派明细',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助当月被指派的诊单(含患者姓名、手机号)。口径:按指派操作时间落月、非继承指派、医助×诊单去重,剔除名下有拒收/退款订单的诊单;仅统计二中心及其下级部门。不传 assistant_id/dept_id 时返回全部。',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助当月被指派的诊单(含患者姓名、手机号)。口径:按指派操作时间落月、医助×诊单去重,剔除名下有拒收/退款订单的诊单;仅统计二中心及其下级部门。行字段 assign_type=new(新指派,is_inherit=0)/inherit(继承,is_inherit=1,含历史回退自动分配行),inherit_visit_slot=继承时患者已完成诊次 k,entry_slot=起算诊次(max(2,k+1);新指派恒为 2)。部门/合计范围下诊单对任一医助为新指派即记 new,否则记 inherit 且 entry 取最小,故 kind=all 的「new 行数 + entry<=N 的 inherit 行数」= 复诊接诊率 visit{N}_den。inherit_mode=exclude 时只有 new 行(旧口径)。不传 assistant_id/dept_id 时返回全部。',
'params_allow' => [
'month' => '统计月份 YYYY-MM(默认本月)',
'dept_ids' => '部门筛选(限二中心子树),多个逗号分隔',
'assistant_id' => '只看某医助',
'dept_id' => '只看某部门分组(0=未分配部门)',
'inherit_mode' => '口径:slot=继承按继承时诊次计入分母(默认,配置 project.revisit_rate.default_inherit_mode)/ exclude=继承不计分母(旧口径,与 2026-10 前一致)',
'kind' => '行类型:new 仅新指派 / inherit 仅继承 / all 两者(默认 all)',
],
],
'stats.revisitRate/deptOptions' => [
'status' => 'open', 'kind' => 'report', 'name' => '复诊接诊率 · 部门选项',
'note' => '二中心及其下级部门(id、pid、name),供 dept_ids 参数取值。',
'note' => '二中心及其下级部门(id、pid、name),供 revisitRate 各接口 dept_ids 参数取值;不受 inherit_mode 影响。',
'params_allow' => [],
],
'stats.revisitRate/overview' => [
'status' => 'open', 'kind' => 'report', 'name' => '复诊接诊率',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助的数据。口径:当月被指派数=当月非继承指派的医助×诊单(剔除名下有拒收/退款订单的诊单);N 诊单数=当月下单且为该诊单全局第 N 笔计入业绩的业务订单(剔除取消/拒收/退款,诊次跨月累计),归属下单时的持有医助;N 诊接诊率=N 诊单数÷当月被指派数(往月指派当月成交会使比率超过 100%)。按部门→医助分组并有合计行。',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助的数据。口径 v2(每列独立分母):assigned_count=新指派数(当月 is_inherit=0 的医助×诊单,剔除名下有拒收/退款订单的诊单,语义与旧版「当月被指派数」相同);inherit_count=继承数(当月 is_inherit=1、from≠to,同诊单多次继承取最早,既新指派又继承只算新指派;含历史回退自动分配行);assigned_total_count=两者并集;visit{N}_count=当月下单且为该诊单全局第 N 笔计入业绩的业务订单(剔除取消/拒收/退款,诊次跨月累计),归属下单时的持有医助;visit{N}_den=新指派数+起算诊次≤N 的继承数(起算诊次=继承时已完成诊次+1,最小 2);visit{N}_rate=count÷den(den=0 为 null;往月指派当月成交会使比率超过 100%);visit_all_count=Σ visit{N}_count、visit_all_den=assigned_total_count、visit_all_rate=复诊合计接诊率(可超 100%);visit2_rate_new_assign=旧口径二诊率(分母仅新指派,自动分配过渡期分档用)。slots 为列范围(2~max(4,分子最大诊次,最大起算诊次),上限 50)。inherit_mode=exclude 复现旧口径:inherit_count=0、visit{N}_den=assigned_count,assigned_count/visit{N}_count/visit{N}_rate 与 2026-10 前逐值一致。继承分母依赖 is_inherit 列,2026-06 之前的继承无法回溯。按部门→医助分组并有合计行(部门/合计按诊单去重,继承起算诊次取部门内最小)。',
'params_allow' => [
'month' => '统计月份 YYYY-MM(默认本月)',
'dept_ids' => '部门筛选(限二中心子树,含下级),多个逗号分隔',
'inherit_mode' => '口径:slot=继承按继承时诊次计入每列独立分母(默认,配置 project.revisit_rate.default_inherit_mode)/ exclude=继承不计分母(旧口径)',
],
],
'stats.revisitRate/visitOrderLines' => [
'status' => 'open', 'kind' => 'report', 'name' => '复诊接诊率 · N 诊订单明细',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助的 N 诊订单(含订单号、金额、患者姓名、手机号)。与复诊接诊率“N 诊单数”同口径,可对账。',
'note' => '后台本身不按数据范围过滤:拥有该权限的账号可看到二中心全部医助的 N 诊订单(含订单号、金额、患者姓名、手机号)。与复诊接诊率“N 诊单数”(visit{N}_count) 同口径,可对账;slot=0 返回全部 N>=2 的订单,与「复诊合计单数」(visit_all_count) 对账,行字段 visit_slot 标明诊次。分子不受 inherit_mode 影响。',
'params_allow' => [
'month' => '统计月份 YYYY-MM(默认本月)',
'slot' => '诊次 N(必填,2=二诊,最大 50)',
'slot' => '诊次 N(必填,2=二诊,最大 50;0=全部 N>=2 即复诊合计)',
'dept_ids' => '部门筛选(限二中心子树),多个逗号分隔',
'assistant_id' => '只看某医助',
'dept_id' => '只看某部门分组(0=未分配部门)',
'inherit_mode' => '口径 slot|exclude(仅影响医助范围判定,不影响订单归属)',
],
],
+33
View File
@@ -1,6 +1,7 @@
<?php
/**
* AI 数据目录人工审核:中医诊单与处方(tcm.*)的全部非写、非 POST 接口(51 个)。
* 例外:复诊到期弹窗 tcm.revisitDue/* 的写接口也显式登记为 excluded(控制器晚于 generated.php 生成,需显式 controller 过单测)。
*
* 约定:
* - 按诊单/患者/记录ID取数的接口一律按「详情」调用(kind=detail):zyt_get 会把 ID 固定为单个值,
@@ -10,6 +11,7 @@
* - 'builtin' 表示接口自身已有逐条校验,函数写在各条目上方注释里。
*/
use app\adminapi\controller\tcm\RevisitDueController;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\adminapi\validate\tcm\DiagnosisValidate;
@@ -134,6 +136,37 @@ return [
'tcm.diagnosisTodo/detail' => ['status' => 'pending', 'name' => '诊单待办详情',
'reason' => '按待办ID读取任意诊单的待办,无逐条权限校验(DiagnosisTodoLogic::detail);可改用 tcm.diagnosisTodo/lists(按诊单ID,已校验诊单可见)'],
// ───────────── 复诊到期提醒(需求 4 右下角弹窗 + P2 主管团队视图,controller/tcm/RevisitDueController.php) ─────────────
// 控制器晚于 generated.php 生成,显式写 controller 以通过 AiMcpUnitTest 的资源存在性校验;重跑 catalog.php --write 后可去掉该键。
// pending 是轮询端点且「读取即写」(first_shown_at/last_shown_at + 首次弹出流水,RevisitDueLogic::markShown),只读事务下会报错回滚,
// 又含患者姓名快照,不对 AI 开放;按医助查询到期患者请走 P2 的 tcm.revisitDue/lists(主管视图,HasDataScopeFilter,待评审后开放)。
'tcm.revisitDue/pending' => ['status' => 'excluded', 'name' => '复诊到期弹窗-待办', 'controller' => RevisitDueController::class, 'http' => 'GET',
'reason' => '右下角弹窗轮询接口:读取即更新弹出时间并写「首次弹出」流水(只读事务下回滚),且返回患者姓名;到期患者查询请用 tcm.revisitDue/lists(P2 主管视图,待评审开放)'],
'tcm.revisitDue/summary' => ['status' => 'pending', 'name' => '复诊到期弹窗-计数', 'controller' => RevisitDueController::class, 'http' => 'GET', 'kind' => 'report',
'reason' => '仅返回当前登录医助本人的到期 / 预警计数(RevisitDueLogic::summary,不写库、无患者信息),供弹窗气泡;待 P2 主管视图 tcm.revisitDue/lists 评审口径后一并开放'],
// P2 主管团队视图:lists 为快照表(tcm_diagnosis_revisit_due)分页列表,RevisitDueLists 按 HasDataScopeFilter::applyDataScopeByOwner('assistant_id') 收窄,
// 不写库(无自愈),但逐行返回患者姓名快照与医助延迟行为(snooze_count/escalation_level/人工关闭原因),且 state 由 CURDATE() 推导、
// 菜单 08_add_revisit_due_team_menu.sql 默认不授予任何角色;待产品验收口径与授权范围后再开放。
'tcm.revisitDue/lists' => ['status' => 'pending', 'name' => '复诊到期跟进(主管团队视图)', 'controller' => RevisitDueController::class, 'http' => 'GET', 'kind' => 'list',
'params_allow' => [
'assistant_id' => '医助(后台账号)ID', 'dept_id' => '医助所属部门ID(含下级部门)',
'state' => '状态 due 到期未挂号/过号 | warning 预警 | booked 已约 | visited 已面诊 | contacted 已联系 | no_days 缺开药天数 | pending 未到期 | closed 人工关闭',
'days_bucket' => '开药天数档位 14|21|28|30|other', 'min_snooze' => '延迟次数下限', 'keyword' => '患者姓名(模糊)',
],
'forbid' => ['export', 'file_name', 'page_start', 'page_end'],
'reason' => '逐行返回患者姓名快照与医助「延迟/人工关闭」行为数据(越点越上心的考核依据),数据范围虽按角色 data_scope 收窄,但主管视图菜单(08_add_revisit_due_team_menu.sql)默认不授予任何角色、口径(CURDATE() 推导状态 / 人工关闭标记)尚待产品验收;验收后改 open'],
'tcm.revisitDue/logs' => ['status' => 'pending', 'name' => '复诊到期跟进-事件流水', 'controller' => RevisitDueController::class, 'http' => 'GET', 'kind' => 'detail', 'id_param' => 'due_id',
'params_allow' => [],
'reason' => '按提醒 id(due_id)返回延迟/已联系/人工关闭/企微推送等流水,含患者姓名与人工关闭说明(自由文本可能含手机号等);逐条校验(RevisitDueLogic::canViewRow:本人 / 超管 / DataScope 可见医助)已内建,待 tcm.revisitDue/lists 开放后一并评审;开放时可写 guard=builtin'],
'tcm.revisitDue/snooze' => ['status' => 'excluded', 'name' => '复诊到期弹窗-延迟', 'controller' => RevisitDueController::class, 'http' => 'POST', 'kind' => 'write',
'reason' => '写接口:延迟提醒(更新 snooze_count/next_remind_at 并写流水),AI 只读'],
'tcm.revisitDue/snoozeAll' => ['status' => 'excluded', 'name' => '复诊到期弹窗-全部延迟', 'controller' => RevisitDueController::class, 'http' => 'POST', 'kind' => 'write',
'reason' => '写接口:批量延迟提醒,AI 只读'],
'tcm.revisitDue/contact' => ['status' => 'excluded', 'name' => '复诊到期弹窗-已联系', 'controller' => RevisitDueController::class, 'http' => 'POST', 'kind' => 'write',
'reason' => '写接口:记录患者承诺复诊日(更新 promised_date/contact_count 并写流水),AI 只读'],
'tcm.revisitDue/close' => ['status' => 'excluded', 'name' => '复诊到期弹窗-人工关闭', 'controller' => RevisitDueController::class, 'http' => 'POST', 'kind' => 'write',
'reason' => '写接口:人工关闭提醒周期(closed_state=4 并写流水),AI 只读'],
// ───────────── 处方 ─────────────
'tcm.prescription/lists' => ['status' => 'open', 'kind' => 'list',
'params_allow' => ['patient_name' => '患者姓名(模糊)', 'sn' => '处方编号(模糊)',