更新
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 复诊接诊率 v2 计算器(RevisitRateCalculator)纯函数单测,不连库。
|
||||
* php server/tests/RevisitRateInheritSlotTest.php
|
||||
* 覆盖(方案 §1.1 / §11 需求 1):
|
||||
* 示例计算(9 月 B 继承 k=3 → entry=4)、k 计算与同一秒对偶、from==to 忽略、多次继承取最早、
|
||||
* NI 与 IH 同诊单只算 NI、k=0 归二诊、历史回退行按继承处理、拒收/退款剔除同时作用于继承、
|
||||
* 部门去重与 entry 取 min、slots 上界含 maxEntry、entry>50 只进合计分母、
|
||||
* exclude 模式退化(IH=∅、不忽略 from==to、不豁免回退行、den=assigned_count)、visit2_rate_new_assign 两种模式一致。
|
||||
*/
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\logic\stats\RevisitRateCalculator as C;
|
||||
|
||||
$checks = 0;
|
||||
function expect(bool $condition, string $message): void
|
||||
{
|
||||
global $checks;
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
$checks++;
|
||||
}
|
||||
|
||||
$ts = static fn (string $dt): int => (int) strtotime($dt);
|
||||
$monthRange = static function (string $month) use ($ts): array {
|
||||
$start = $ts($month . '-01 00:00:00');
|
||||
|
||||
return [$start, $ts(date('Y-m-t', $start) . ' 23:59:59')];
|
||||
};
|
||||
|
||||
/** 日志行构造:id 自增,默认 operator=100 */
|
||||
$logSeq = 0;
|
||||
$log = static function (int $did, int $from, int $to, string $dt, int $inherit = 0, int $op = 100) use (&$logSeq, $ts): array {
|
||||
return [
|
||||
'id' => ++$logSeq,
|
||||
'diagnosis_id' => $did,
|
||||
'from_assistant_id' => $from,
|
||||
'to_assistant_id' => $to,
|
||||
'operator_admin_id' => $op,
|
||||
'create_time' => $ts($dt),
|
||||
'is_inherit' => $inherit,
|
||||
'inherit_visit_slot' => 0,
|
||||
];
|
||||
};
|
||||
$orderSeq = 0;
|
||||
$order = static function (int $did, string $dt) use (&$orderSeq, $ts): array {
|
||||
return ['id' => ++$orderSeq, 'order_no' => 'N' . $orderSeq, 'diagnosis_id' => $did, 'create_time' => $ts($dt), 'amount' => 100, 'creator_id' => 0];
|
||||
};
|
||||
/** 日志/订单按计算器要求排序 */
|
||||
$sortLogs = static function (array $rows): array {
|
||||
usort($rows, static fn (array $a, array $b): int => [$a['diagnosis_id'], $a['create_time'], $a['id']] <=> [$b['diagnosis_id'], $b['create_time'], $b['id']]);
|
||||
|
||||
return $rows;
|
||||
};
|
||||
$sortOrders = static function (array $rows): array {
|
||||
usort($rows, static fn (array $a, array $b): int => [$a['diagnosis_id'], $a['create_time'], $a['id']] <=> [$b['diagnosis_id'], $b['create_time'], $b['id']]);
|
||||
|
||||
return $rows;
|
||||
};
|
||||
$slotCounts = static function (array $calc, int $aid): array {
|
||||
$out = [];
|
||||
foreach ($calc['slotOrdersByAssistant'][$aid] ?? [] as $slot => $orders) {
|
||||
$out[$slot] = \count($orders);
|
||||
}
|
||||
|
||||
return $out;
|
||||
};
|
||||
|
||||
// ───────────────────────── 示例计算(§1.1):A 持有 D 三单,09-10 继承给 B,09-25 B 名下第 4 单 ─────────────────────────
|
||||
$A = 1;
|
||||
$B = 2;
|
||||
$D = 10;
|
||||
[$s9, $e9] = $monthRange('2026-09');
|
||||
$logs = $sortLogs([
|
||||
$log($D, 0, $A, '2026-05-01 09:00:00'),
|
||||
$log($D, $A, $B, '2026-09-10 10:00:00', 1),
|
||||
]);
|
||||
$orders = $sortOrders([
|
||||
$order($D, '2026-05-03 10:00:00'),
|
||||
$order($D, '2026-06-12 10:00:00'),
|
||||
$order($D, '2026-08-20 10:00:00'),
|
||||
$order($D, '2026-09-25 10:00:00'),
|
||||
]);
|
||||
$calc = C::compute($logs, $orders, [], [], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect($calc['inheritMode'] === 'slot', '模式归一化');
|
||||
expect(!isset($calc['diagsByAssistant'][$B]), 'B 无新指派');
|
||||
expect(($calc['inheritEntryByAssistant'][$B][$D] ?? null) === 4, 'k=3 → entry=4');
|
||||
expect(\count($calc['inheritPairs']) === 1 && $calc['inheritPairs'][0]['inherit_visit_slot'] === 3 && $calc['inheritPairs'][0]['entry_slot'] === 4, 'inheritPairs 带 k/entry');
|
||||
expect(($slotCounts($calc, $B)[4] ?? 0) === 1, '09-25 第 4 单归 B 的 visit4');
|
||||
expect(!isset($calc['slotOrdersByAssistant'][$A]), 'A 9 月无成交');
|
||||
expect($calc['maxEntry'] === 4, 'maxEntry=4');
|
||||
$slots = C::slotRange($calc['slotOrdersByAssistant'], $calc['maxEntry']);
|
||||
expect($slots === [2, 3, 4], 'slots 2~4');
|
||||
$packB = C::metricPack(0, $calc['inheritEntryByAssistant'][$B], $slotCounts($calc, $B), $slots, 0);
|
||||
expect($packB['assigned_count'] === 0 && $packB['inherit_count'] === 1 && $packB['assigned_total_count'] === 1, 'B 新指派 0 / 继承 1 / 合计 1');
|
||||
expect($packB['visit2_den'] === 0 && $packB['visit3_den'] === 0 && $packB['visit4_den'] === 1, 'visit2/3_den 不含 D,visit4_den 含 D');
|
||||
expect($packB['visit2_rate'] === null && $packB['visit4_count'] === 1 && $packB['visit4_rate'] === 100.0, 'visit2_rate=null、visit4_rate=100');
|
||||
expect($packB['visit_all_count'] === 1 && $packB['visit_all_den'] === 1 && $packB['visit_all_rate'] === 100.0, '合计列 1/1=100');
|
||||
expect($packB[C::FIELD_RATE_NEW_ASSIGN] === null, 'B 旧口径新指派 0 → null');
|
||||
|
||||
// exclude 模式退化:B assigned_count=0、visit4_count=1、visit4_rate=null(与线上现状一致)
|
||||
$calcEx = C::compute($logs, $orders, [], [], [], $s9, $e9, C::MODE_EXCLUDE);
|
||||
expect($calcEx['inheritEntryByAssistant'] === [] && $calcEx['inheritPairs'] === [] && $calcEx['maxEntry'] === 0, 'exclude:IH=∅');
|
||||
expect(($slotCounts($calcEx, $B)[4] ?? 0) === 1, 'exclude:分子不变');
|
||||
$packBEx = C::metricPack(0, [], $slotCounts($calcEx, $B), C::slotRange($calcEx['slotOrdersByAssistant'], 0), 0);
|
||||
expect($packBEx['assigned_count'] === 0 && $packBEx['visit4_count'] === 1 && $packBEx['visit4_rate'] === null, 'exclude:B assigned=0、visit4_rate=null');
|
||||
expect($packBEx['inherit_count'] === 0 && $packBEx['visit4_den'] === 0 && $packBEx['visit_all_den'] === 0, 'exclude:新键存在但为 0');
|
||||
|
||||
// 变体:继承时无任何有效订单(k=0)→ entry=2,与新指派等价
|
||||
$calc0 = C::compute(
|
||||
$sortLogs([$log(11, 0, $A, '2026-08-01 09:00:00'), $log(11, $A, $B, '2026-09-10 10:00:00', 1)]),
|
||||
$sortOrders([$order(11, '2026-09-20 10:00:00')]),
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
$s9,
|
||||
$e9,
|
||||
C::MODE_SLOT
|
||||
);
|
||||
expect(($calc0['inheritEntryByAssistant'][$B][11] ?? null) === 2, 'k=0 → entry=2');
|
||||
expect($calc0['inheritPairs'][0]['inherit_visit_slot'] === 0, 'k=0 记录');
|
||||
|
||||
// 变体:订单与继承同一秒 → 订单归 B 且不计入 k
|
||||
$calcSame = C::compute(
|
||||
$sortLogs([$log(12, 0, $A, '2026-05-01 09:00:00'), $log(12, $A, $B, '2026-09-10 10:00:00', 1)]),
|
||||
$sortOrders([$order(12, '2026-08-01 10:00:00'), $order(12, '2026-09-10 10:00:00')]),
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
$s9,
|
||||
$e9,
|
||||
C::MODE_SLOT
|
||||
);
|
||||
expect(($calcSame['inheritEntryByAssistant'][$B][12] ?? null) === 2, '同一秒:k=1(不含同秒订单)→ entry=2');
|
||||
expect(($slotCounts($calcSame, $B)[2] ?? 0) === 1 && !isset($calcSame['slotOrdersByAssistant'][$A]), '同一秒:订单归新持有人 B');
|
||||
|
||||
// 偏移:offset=2,继承前 1 单 → k=3,entry=4
|
||||
$calcOff = C::compute(
|
||||
$sortLogs([$log(13, 0, $A, '2026-05-01 09:00:00'), $log(13, $A, $B, '2026-09-10 10:00:00', 1)]),
|
||||
$sortOrders([$order(13, '2026-08-01 10:00:00')]),
|
||||
[13 => 2],
|
||||
[],
|
||||
[],
|
||||
$s9,
|
||||
$e9,
|
||||
C::MODE_SLOT
|
||||
);
|
||||
expect(($calcOff['inheritEntryByAssistant'][$B][13] ?? null) === 4, '偏移 2 + 1 单 → k=3 → entry=4');
|
||||
|
||||
// ───────────────────────── from == to 忽略(仅 slot);exclude 不忽略 ─────────────────────────
|
||||
$logsSelf = $sortLogs([
|
||||
$log(20, 0, $A, '2026-09-02 09:00:00'),
|
||||
$log(20, $A, $A, '2026-09-05 09:00:00'), // 指派给自己(非继承)
|
||||
$log(21, $B, $B, '2026-09-06 09:00:00', 1), // 继承给自己
|
||||
]);
|
||||
$calcSelf = C::compute($logsSelf, [], [], [], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect(isset($calcSelf['diagsByAssistant'][$A][20]) && \count($calcSelf['pairsRaw']) === 1, 'slot:from==to 的非继承行忽略,不重复计 pairsRaw');
|
||||
expect(!isset($calcSelf['inheritEntryByAssistant'][$B]), 'slot:from==to 的继承行忽略');
|
||||
expect(isset($calcSelf['excludeDiagsByAssistant'][$A][20]) && !isset($calcSelf['excludeDiagsByAssistant'][$B]), '旧口径集合:is_inherit=0 计入、=1 不计');
|
||||
$calcSelfEx = C::compute($logsSelf, [], [], [], [], $s9, $e9, C::MODE_EXCLUDE);
|
||||
expect(\count($calcSelfEx['pairsRaw']) === 2 && isset($calcSelfEx['diagsByAssistant'][$A][20]), 'exclude:from==to 行照旧计入 pairsRaw');
|
||||
expect($calcSelfEx['diagsByAssistant'] === $calcSelfEx['excludeDiagsByAssistant'], 'exclude:NI === NI_exclude');
|
||||
|
||||
// ───────────────────────── 多次继承取最早;NI 与 IH 同诊单只算 NI ─────────────────────────
|
||||
$logsMulti = $sortLogs([
|
||||
$log(30, 0, $A, '2026-05-01 09:00:00'),
|
||||
$log(30, $A, $B, '2026-09-03 09:00:00', 1), // 最早继承:此时 1 单 → k=1 → entry=2
|
||||
$log(30, $B, $A, '2026-09-10 09:00:00'),
|
||||
$log(30, $A, $B, '2026-09-20 09:00:00', 1), // 第二次继承:此时 2 单 → k=2 → entry=3(应忽略)
|
||||
$log(31, 0, $B, '2026-09-04 09:00:00'), // 31 新指派给 B
|
||||
$log(31, $B, $A, '2026-09-05 09:00:00'),
|
||||
$log(31, $A, $B, '2026-09-06 09:00:00', 1), // 31 又继承给 B → 只算 NI
|
||||
]);
|
||||
$ordersMulti = $sortOrders([$order(30, '2026-08-01 10:00:00'), $order(30, '2026-09-15 10:00:00')]);
|
||||
$calcMulti = C::compute($logsMulti, $ordersMulti, [], [], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect(($calcMulti['inheritEntryByAssistant'][$B][30] ?? null) === 2, '多次继承取最早(entry=2 而非 3)');
|
||||
$pair30 = array_values(array_filter($calcMulti['inheritPairs'], static fn (array $p): bool => $p['diagnosis_id'] === 30))[0];
|
||||
expect($pair30['assign_count'] === 2 && $pair30['inherit_visit_slot'] === 1, '继承行 assign_count=2、k 取最早');
|
||||
expect(isset($calcMulti['diagsByAssistant'][$B][31]) && !isset($calcMulti['inheritEntryByAssistant'][$B][31]), '既新指派又继承给同一医助 → 只算 NI');
|
||||
expect(isset($calcMulti['diagsByAssistant'][$A][30]) && isset($calcMulti['diagsByAssistant'][$A][31]), 'A 的两条非继承指派计 NI(A)');
|
||||
// 09-15 的第 2 单:持有人为 09-10 指派后的 A
|
||||
expect(($slotCounts($calcMulti, $A)[2] ?? 0) === 1 && !isset($calcMulti['slotOrdersByAssistant'][$B]), '持有人按时间线:09-15 订单归 A');
|
||||
|
||||
// ───────────────────────── 历史回退行按继承处理(仅 slot) ─────────────────────────
|
||||
$rbTime = '2026-09-12 09:00:00';
|
||||
$logsRb = $sortLogs([
|
||||
$log(40, 0, 5, '2026-05-01 09:00:00'), // 一中心 5 持有
|
||||
$log(40, 5, $B, '2026-09-01 09:00:00', 0, 0), // 系统自动分配给 B
|
||||
$log(40, $B, 5, $rbTime, 0, 77), // 回退(历史行 is_inherit=0,operator=77)
|
||||
]);
|
||||
$rbKeys = [C::rollbackKey(40, $ts($rbTime), 77) => true];
|
||||
$ordersRb = $sortOrders([$order(40, '2026-06-01 10:00:00'), $order(40, '2026-07-01 10:00:00')]);
|
||||
$calcRb = C::compute($logsRb, $ordersRb, [], [], $rbKeys, $s9, $e9, C::MODE_SLOT);
|
||||
expect(isset($calcRb['diagsByAssistant'][$B][40]), '自动分配行计 NI(B)');
|
||||
expect(!isset($calcRb['diagsByAssistant'][5]) && ($calcRb['inheritEntryByAssistant'][5][40] ?? null) === 3, '回退行按继承:k=2 → entry=3');
|
||||
expect($calcRb['inheritPairs'][0]['is_rollback'] === 1, '回退行标记 is_rollback');
|
||||
$calcRbEx = C::compute($logsRb, $ordersRb, [], [], $rbKeys, $s9, $e9, C::MODE_EXCLUDE);
|
||||
expect(isset($calcRbEx['diagsByAssistant'][5][40]) && $calcRbEx['inheritEntryByAssistant'] === [], 'exclude:回退行不豁免,照旧计 NI(5)');
|
||||
$calcRbNoKey = C::compute($logsRb, $ordersRb, [], [], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect(isset($calcRbNoKey['diagsByAssistant'][5][40]), '无回退键集合时按普通新指派');
|
||||
|
||||
// ───────────────────────── 拒收/退款剔除同时作用于 NI / IH / NI_exclude ─────────────────────────
|
||||
$calcR = C::compute($logs, $orders, [], [$D => true], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect($calcR['inheritEntryByAssistant'] === [] && $calcR['inheritPairs'] === [], 'R 剔除继承诊单');
|
||||
$calcR2 = C::compute($logsSelf, [], [], [20 => true], [], $s9, $e9, C::MODE_SLOT);
|
||||
expect($calcR2['diagsByAssistant'] === [] && $calcR2['excludeDiagsByAssistant'] === [] && $calcR2['pairsRaw'] === [], 'R 剔除 NI 与 NI_exclude');
|
||||
|
||||
// ───────────────────────── 部门去重与 entry 取 min ─────────────────────────
|
||||
$C_ = 3;
|
||||
$ni = [$A => [100 => true, 101 => true], $B => [101 => true, 102 => true], $C_ => []];
|
||||
$ih = [$A => [200 => 5, 102 => 3], $B => [200 => 3, 201 => 4], $C_ => [100 => 2]];
|
||||
[$niSet, $ihEntries] = C::mergeInheritEntries($ni, $ih, [$A, $B, $C_]);
|
||||
expect(\count($niSet) === 3, 'NI_dept 去重:100/101/102');
|
||||
expect(!isset($ihEntries[102]) && !isset($ihEntries[100]), 'IH_dept 剔除已在 NI_dept 的诊单');
|
||||
expect(($ihEntries[200] ?? null) === 3 && ($ihEntries[201] ?? null) === 4, 'entry 取部门内最小');
|
||||
$deptPack = C::metricPack(\count($niSet), $ihEntries, [2 => 4, 3 => 2, 4 => 1, 5 => 1], [2, 3, 4, 5], 3);
|
||||
expect($deptPack['visit2_den'] === 3 && $deptPack['visit3_den'] === 4 && $deptPack['visit4_den'] === 5 && $deptPack['visit5_den'] === 5, '部门分母逐列累计');
|
||||
expect($deptPack['assigned_total_count'] === 5 && $deptPack['visit_all_count'] === 8 && $deptPack['visit_all_rate'] === 160.0, '合计列:8/5=160%(可超 100%)');
|
||||
expect($deptPack[C::FIELD_RATE_NEW_ASSIGN] === round(4 / 3 * 100, 2), 'visit2_rate_new_assign = Num_2 / NI_exclude');
|
||||
// 子行之和 >= 部门去重
|
||||
$packA = C::metricPack(2, $ih[$A], [], [2, 3, 4, 5], 2);
|
||||
$packBd = C::metricPack(2, $ih[$B], [], [2, 3, 4, 5], 2);
|
||||
$packC = C::metricPack(0, $ih[$C_], [], [2, 3, 4, 5], 0);
|
||||
expect($packA['visit5_den'] + $packBd['visit5_den'] + $packC['visit5_den'] >= $deptPack['visit5_den'], '部门 den <= 子行之和');
|
||||
expect(C::unionDiagSets($ni, [$A, $B]) === [100 => true, 101 => true, 102 => true], 'unionDiagSets');
|
||||
|
||||
// ───────────────────────── slots 上界含 maxEntry;entry>50 只进合计分母 ─────────────────────────
|
||||
expect(C::slotRange([], 7) === range(2, 7), '只有继承到第 6 诊(entry=7)也显示到七诊列');
|
||||
expect(C::slotRange([1 => [9 => [[]]]], 0) === range(2, 9), '分子最大 slot 决定上界');
|
||||
expect(C::slotRange([], 0) === [2, 3, 4], '最少到四诊');
|
||||
expect(C::slotRange([], 99) === range(2, 50), '上界封顶 50');
|
||||
$big = C::compute(
|
||||
$sortLogs([$log(50, 0, $A, '2026-01-01 09:00:00'), $log(50, $A, $B, '2026-09-10 10:00:00', 1)]),
|
||||
$sortOrders([$order(50, '2026-08-01 10:00:00')]),
|
||||
[50 => 20],
|
||||
[],
|
||||
[],
|
||||
$s9,
|
||||
$e9,
|
||||
C::MODE_SLOT
|
||||
);
|
||||
expect(($big['inheritEntryByAssistant'][$B][50] ?? null) === 22 && $big['maxEntry'] === 22, '偏移 20 + 1 单 → entry 22');
|
||||
$packBig = C::metricPack(0, [50 => 60], [], [2, 3], 0);
|
||||
expect($packBig['visit2_den'] === 0 && $packBig['inherit_count'] === 1 && $packBig['visit_all_den'] === 1, 'entry>50:不进诊次列、只进合计分母');
|
||||
|
||||
// ───────────────────────── 辅助函数 ─────────────────────────
|
||||
expect(C::normalizeMode('EXCLUDE') === 'exclude' && C::normalizeMode('bogus') === 'slot' && C::normalizeMode('', 'exclude') === 'exclude', 'normalizeMode 白名单与默认');
|
||||
expect(C::monthAssignedDiagIds($logsMulti, $s9, $e9) === [30, 31], 'monthAssignedDiagIds 含继承行、按月过滤');
|
||||
expect(C::candidateDiagIds($logsRb) === [40], 'candidateDiagIds');
|
||||
$packEmpty = C::metricPack(0, [], [], [2, 3, 4], 0);
|
||||
expect($packEmpty['visit2_rate'] === null && $packEmpty['visit_all_rate'] === null && $packEmpty[C::FIELD_RATE_NEW_ASSIGN] === null, '分母 0 → null');
|
||||
|
||||
echo "RevisitRateInheritSlotTest OK ({$checks} checks)\n";
|
||||
Reference in New Issue
Block a user