134 lines
9.5 KiB
PHP
134 lines
9.5 KiB
PHP
<?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";
|