Files
zyt/server/tests/RevisitRateContractTest.php
T
2026-10-15 11:03:54 +08:00

134 lines
9.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* 复诊接诊率 v2 源码契约(ReflectionMethod 读源码,不连库)。
* php server/tests/RevisitRateContractTest.php
* 守住:
* - 分子订单过滤仍走 YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery(RevisitVisitSlotService::fetchOrderSeqRows);
* - RevisitRateLogic::buildStatsCore 经 RevisitRateCalculator::compute 计算,订单序列仍经 fetchOrderSeqRows 委托;
* - 计算器中 slot<2 / >50 跳过、持有人指针 t <= ct、继承诊次用 RevisitVisitSlotService::slotBefore;
* - 旧口径集合只看 is_inherit;exclude 模式不进入 from==to / 回退豁免分支;
* - DiagnosisLogic::assign 继承时在事务内写 inherit_visit_slot(slotAtTime);
* - AutoAssignLogLogic::rollbackOne 回退仍写 is_inherit = 0(原逻辑不变);
* - AutoAssignPendingDiagnosis 以 inherit_mode=slot 取 overview 并按配置字段分档。
*/
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\stats\AutoAssignLogLogic;
use app\adminapi\logic\stats\RevisitRateCalculator;
use app\adminapi\logic\stats\RevisitRateLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\command\AutoAssignPendingDiagnosis;
use app\common\service\tcm\RevisitVisitSlotService;
$checks = 0;
function expect(bool $condition, string $message): void
{
global $checks;
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
$checks++;
}
function methodSource(string $class, string $method): string
{
$ref = (new ReflectionClass($class))->getMethod($method);
$lines = file($ref->getFileName());
if (!is_array($lines)) {
throw new RuntimeException('method source must be readable: ' . $class . '::' . $method);
}
return implode('', array_slice($lines, $ref->getStartLine() - 1, $ref->getEndLine() - $ref->getStartLine() + 1));
}
// ---------- 分子过滤口径 ----------
$fetch = methodSource(RevisitVisitSlotService::class, 'fetchOrderSeqRows');
expect(str_contains($fetch, 'YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery'), '订单序列仍走业绩有效订单过滤');
expect(str_contains($fetch, "whereNull('o.delete_time')"), '订单序列剔除软删');
expect(str_contains($fetch, "'o.create_time' => 'asc', 'o.id' => 'asc'"), '订单序列按 (create_time, id) 升序');
$slotAt = methodSource(RevisitVisitSlotService::class, 'slotAtTime');
expect(str_contains($slotAt, 'self::fetchOrderSeqRows') && str_contains($slotAt, 'self::slotBefore'), 'slotAtTime 复用同一订单序列与 slotBefore');
$slotBefore = methodSource(RevisitVisitSlotService::class, 'slotBefore');
expect(str_contains($slotBefore, '(int) $ct < $atTs'), 'slotBefore 严格小于(与持有人 t <= ct 对偶)');
$logicFetch = methodSource(RevisitRateLogic::class, 'fetchOrderSeqRows');
expect(str_contains($logicFetch, 'RevisitVisitSlotService::fetchOrderSeqRows'), 'RevisitRateLogic::fetchOrderSeqRows 委托诊次服务');
$iter = methodSource(RevisitRateLogic::class, 'iterateOrderSeqRows');
expect(str_contains($iter, 'self::fetchOrderSeqRows'), '订单生成器经委托方法取数');
// ---------- buildStatsCore 经计算器 ----------
$core = methodSource(RevisitRateLogic::class, 'buildStatsCore');
expect(str_contains($core, 'RevisitRateCalculator::compute('), 'buildStatsCore 调用 RevisitRateCalculator::compute');
expect(str_contains($core, 'self::fetchRefundOrRejectDiagnosisSet'), '仍剔除拒收/退款诊单');
expect(str_contains($core, 'RevisitRateCalculator::monthAssignedDiagIds'), '拒收/退款剔除集合覆盖继承诊单');
expect(str_contains($core, 'self::fetchRollbackKeySet'), 'slot 模式读历史回退行');
expect(str_contains($core, "'lg.from_assistant_id'") && str_contains($core, "'lg.inherit_visit_slot'"), '字段列表含 from_assistant_id / inherit_visit_slot');
expect(str_contains($core, '$inheritEntryByAssistant[$aid]') && str_contains($core, '$excludeDiagsByAssistant[$aid]'), '二中心过滤同步 unset 继承集合与旧口径集合');
expect(str_contains($core, '$diagsByAssistant + $slotOrdersByAssistant + $inheritEntryByAssistant + $excludeDiagsByAssistant'), 'universe 并集含继承医助');
$universe = methodSource(RevisitRateLogic::class, 'assistantUniverse');
expect(str_contains($universe, "'inheritEntryByAssistant'"), 'assistantUniverse 含继承集合(当月仅有继承行的医助不丢行)');
// ---------- 计算器核心不变量 ----------
$compute = methodSource(RevisitRateCalculator::class, 'compute');
expect(str_contains($compute, '$effectiveSlot < 2 || $effectiveSlot > self::MAX_VISIT_SLOT'), 'slot<2 或 >50 跳过');
expect(str_contains($compute, "\$tl[\$ptr]['t'] <= \$ct"), '持有人指针 t <= ct(同一秒归新持有人)');
expect(str_contains($compute, '$ct < $startTs || $ct > $endTs'), '分子按当月下单过滤');
expect(str_contains($compute, 'RevisitVisitSlotService::slotBefore('), '继承诊次 k 经 slotBefore 单点计算');
expect(str_contains($compute, 'RevisitVisitSlotService::resolveRevisitSlotStartOffset('), '偏移经诊次服务钳制');
expect(str_contains($compute, 'max($inheritMinNextSlot, $k + 1)'), 'entry = max(min_next_slot, k+1)');
expect(str_contains($compute, 'if (!$isInherit) {' . "\n" . ' $excludeDiagsByAssistant[$aid][$did] = true;'), '旧口径集合仅看 is_inherit=0');
expect(str_contains($compute, 'if (!$slotMode) {') && str_contains($compute, 'if ($from === $aid) {'), 'from==to 忽略仅在 slot 分支');
expect(str_contains($compute, 'isset($diagsByAssistant[$aid][$did])') && str_contains($compute, 'unset($inheritFirst[$aid][$did])'), 'NI 与 IH 同诊单只算 NI');
$pack = methodSource(RevisitRateCalculator::class, 'metricPack');
expect(str_contains($pack, "'assigned_count' => \$newAssigned") && str_contains($pack, "'inherit_count' => \$inheritCount"), '指标包键名');
expect(str_contains($pack, "'_den'") && str_contains($pack, "'visit_all_rate'") && str_contains($pack, 'self::FIELD_RATE_NEW_ASSIGN'), '新键 den / visit_all / visit2_rate_new_assign');
expect(str_contains($pack, '$den > 0 ? round($cnt / $den * 100, 2) : null'), 'den=0 → null(AutoAssign 依赖 null 语义)');
$logicPack = methodSource(RevisitRateLogic::class, 'buildMetricPack');
expect(str_contains($logicPack, 'RevisitRateCalculator::metricPack('), 'Logic 指标包委托计算器');
$overview = methodSource(RevisitRateLogic::class, 'overview');
expect(str_contains($overview, 'RevisitRateCalculator::slotRange(') && str_contains($overview, "\$ctx['maxEntry']"), 'slots 上界含 maxEntry');
expect(str_contains($overview, 'RevisitRateCalculator::mergeInheritEntries('), '部门/合计行按 §1.1 去重');
expect(str_contains($overview, "'inherit_mode' => \$ctx['inheritMode']"), '返回体含 inherit_mode');
expect(str_contains($overview, "\$b['assigned_count'] <=> \$a['assigned_count']"), '排序仍按 assigned_count');
$mode = methodSource(RevisitRateLogic::class, 'resolveInheritMode');
expect(str_contains($mode, "config('project.revisit_rate.default_inherit_mode')") && str_contains($mode, 'RevisitRateCalculator::normalizeMode('), '口径白名单读参数/配置');
$lines = methodSource(RevisitRateLogic::class, 'assignLines');
expect(str_contains($lines, "['new', 'inherit', 'all']") && str_contains($lines, "'assign_type' => 'inherit'") && str_contains($lines, "'entry_slot'"), 'assignLines 支持 kind 并输出 assign_type/entry_slot');
$visit = methodSource(RevisitRateLogic::class, 'visitOrderLines');
expect(str_contains($visit, '$slot < 0 || $slot === 1 || $slot > self::MAX_VISIT_SLOT'), 'visitOrderLines 允许 slot=0');
// ---------- 写入侧 ----------
$assign = methodSource(DiagnosisLogic::class, 'assign');
expect(str_contains($assign, 'RevisitVisitSlotService::slotAtTime($id, $now)') && str_contains($assign, "'inherit_visit_slot'") && str_contains($assign, "'inherit_visit_slot_calc_time'"), 'assign 继承时写诊次快照');
expect(strpos($assign, 'RevisitVisitSlotService::slotAtTime') > strpos($assign, 'Db::startTrans()') && strpos($assign, 'RevisitVisitSlotService::slotAtTime') < strpos($assign, 'Db::commit()'), '快照计算在事务内');
expect(str_contains($assign, 'RevisitDueHook::onAssistantChanged'), '保留复诊到期钩子');
$rollback = methodSource(AutoAssignLogLogic::class, 'rollbackOne');
// 用户要求复诊接诊率原逻辑不变:回退行保持 is_inherit=0(新口径下由 rollbackKeySet 匹配按继承处理)
expect(str_contains($rollback, "'is_inherit' => 0"), '回退写 is_inherit=0(原逻辑)');
expect(!str_contains($rollback, 'RevisitVisitSlotService::slotAtTime'), '回退不写 inherit_visit_slot 快照');
expect(str_contains($rollback, 'RevisitDueHook::onAssistantChanged'), '回退保留复诊到期钩子');
$exec = methodSource(AutoAssignPendingDiagnosis::class, 'execute');
expect(str_contains($exec, "'inherit_mode' => RevisitRateCalculator::MODE_SLOT"), '自动分配以 slot 口径取 overview');
$tierField = methodSource(AutoAssignPendingDiagnosis::class, 'resolveTierField');
expect(str_contains($tierField, "config('project.revisit_rate.auto_assign_tier_field')"), '分档字段读配置');
$logRow = methodSource(AutoAssignPendingDiagnosis::class, 'buildLogRow');
expect(str_contains($logRow, "'visit2_rate_new_assign'") && str_contains($logRow, "'rate_version' => \$this->rateVersion"), '自动指派日志写两套率与口径版本');
$apply = methodSource(AutoAssignPendingDiagnosis::class, 'applyAssignment');
expect(str_contains($apply, 'RevisitDueHook::onAssistantChanged'), '自动分配保留复诊到期钩子');
echo "RevisitRateContractTest OK ({$checks} checks)\n";