215 lines
8.3 KiB
PHP
215 lines
8.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\logic\stats;
|
|
|
|
use app\common\service\tcm\RevisitVisitSlotService;
|
|
|
|
/** 指派日期队列:先圈定患者,再累计其持有期间的有效复诊订单;不改变全局诊次。 */
|
|
final class RevisitCohortCalculator
|
|
{
|
|
public const TYPE_NEW = 'new';
|
|
public const TYPE_INHERIT = 'inherit';
|
|
|
|
/**
|
|
* 日志与订单分别按 (diagnosis_id, create_time, id) 升序。
|
|
* 新指派保留全局诊次;继承每次从二诊重新起算,同一秒订单归最新指派。
|
|
* 同一医助、诊单、诊次只计一次,避免反复继承把单诊接诊率推高至 100% 以上。
|
|
*/
|
|
public static function compute(
|
|
array $logRows,
|
|
iterable $orderRows,
|
|
array $offsetMap,
|
|
array $refundRejectSet,
|
|
array $rollbackKeySet,
|
|
int $startTs,
|
|
int $endTs,
|
|
int $observedAt,
|
|
string $type
|
|
): array {
|
|
$type = $type === self::TYPE_INHERIT ? self::TYPE_INHERIT : self::TYPE_NEW;
|
|
$timeline = [];
|
|
$pairs = [];
|
|
$lastTarget = [];
|
|
foreach ($logRows as $r) {
|
|
$did = (int) ($r['diagnosis_id'] ?? 0);
|
|
$aid = (int) ($r['to_assistant_id'] ?? 0);
|
|
$from = (int) ($r['from_assistant_id'] ?? 0);
|
|
$time = (int) ($r['create_time'] ?? 0);
|
|
if ($did <= 0 || $time > $observedAt) {
|
|
continue;
|
|
}
|
|
// 从未分配重新指派时,写入侧可能把 from 回填为原医助,形成 A→A。
|
|
// 以实际时间线判断持有人是否变化,保留释放后的重新承接。
|
|
$previous = $lastTarget[$did] ?? null;
|
|
if (($previous !== null && $previous === $aid) || ($previous === null && $from === $aid)) {
|
|
continue;
|
|
}
|
|
$lastTarget[$did] = $aid;
|
|
$isRollback = isset($rollbackKeySet[RevisitRateCalculator::rollbackKey(
|
|
$did, $time, (int) ($r['operator_admin_id'] ?? 0)
|
|
)]);
|
|
$eventType = (int) ($r['is_inherit'] ?? 0) === 1 || $isRollback
|
|
? self::TYPE_INHERIT : self::TYPE_NEW;
|
|
$selected = $aid > 0 && $time >= $startTs && $time <= $endTs
|
|
&& $eventType === $type && !isset($refundRejectSet[$did]);
|
|
$timeline[$did][] = [
|
|
'assistant_id' => $aid,
|
|
'time' => $time,
|
|
'selected' => $selected,
|
|
'type' => $eventType,
|
|
];
|
|
if (!$selected) {
|
|
continue;
|
|
}
|
|
if (!isset($pairs[$aid][$did])) {
|
|
$pairs[$aid][$did] = [
|
|
'diagnosis_id' => $did,
|
|
'assistant_id' => $aid,
|
|
'assign_type' => $type,
|
|
'assign_count' => 0,
|
|
'first_assign_time' => $time,
|
|
'last_assign_time' => $time,
|
|
'is_rollback' => 0,
|
|
];
|
|
}
|
|
$pairs[$aid][$did]['assign_count']++;
|
|
$pairs[$aid][$did]['last_assign_time'] = $time;
|
|
$pairs[$aid][$did]['is_rollback'] |= $isRollback ? 1 : 0;
|
|
}
|
|
|
|
$orders = [];
|
|
$curDid = 0;
|
|
$globalSeq = 0;
|
|
$segmentSeq = 0;
|
|
$ptr = 0;
|
|
$offset = 0;
|
|
$event = null;
|
|
foreach ($orderRows as $r) {
|
|
$did = (int) ($r['diagnosis_id'] ?? 0);
|
|
$ct = (int) ($r['create_time'] ?? 0);
|
|
if ($did <= 0 || $ct > $observedAt) {
|
|
continue;
|
|
}
|
|
if ($curDid !== $did) {
|
|
$curDid = $did;
|
|
$globalSeq = 0;
|
|
$segmentSeq = 0;
|
|
$ptr = 0;
|
|
$event = null;
|
|
$offset = RevisitVisitSlotService::resolveRevisitSlotStartOffset($did, $offsetMap);
|
|
}
|
|
$globalSeq++;
|
|
$tl = $timeline[$did] ?? [];
|
|
while ($ptr < \count($tl) && $tl[$ptr]['time'] <= $ct) {
|
|
$event = $tl[$ptr++];
|
|
$segmentSeq = 0;
|
|
}
|
|
if ($event === null) {
|
|
continue;
|
|
}
|
|
$segmentSeq++;
|
|
if (!$event['selected']) {
|
|
continue;
|
|
}
|
|
$slot = $type === self::TYPE_INHERIT ? $segmentSeq + 1 : $globalSeq + $offset;
|
|
if ($slot < 2 || $slot > RevisitRateCalculator::MAX_VISIT_SLOT) {
|
|
continue;
|
|
}
|
|
$aid = $event['assistant_id'];
|
|
// 重复承接同一患者时,明细保留首次达到该诊次的订单,与人数指标逐项对账。
|
|
if (isset($orders[$aid][$slot][$did])) {
|
|
continue;
|
|
}
|
|
$r['assistant_id'] = $aid;
|
|
$r['visit_slot'] = $slot;
|
|
$r['global_visit_slot'] = $globalSeq + $offset;
|
|
$r['cohort_assign_time'] = $event['time'];
|
|
$orders[$aid][$slot][$did] = $r;
|
|
}
|
|
|
|
return ['pairsByAssistant' => $pairs, 'ordersByAssistant' => $orders, 'cohort_type' => $type];
|
|
}
|
|
|
|
/** 部门与总计按诊单去重;每个诊次保留同一患者最早一笔订单。 */
|
|
public static function scopedOrders(array $calc, array $assistantIds, int $slot = 0): array
|
|
{
|
|
$out = [];
|
|
foreach ($assistantIds as $aid) {
|
|
foreach ($calc['ordersByAssistant'][$aid] ?? [] as $s => $byDiag) {
|
|
if ($slot > 0 && $s !== $slot) {
|
|
continue;
|
|
}
|
|
foreach ($byDiag as $did => $r) {
|
|
$previous = $out[$s][$did] ?? null;
|
|
if ($previous === null || [(int) $r['create_time'], (int) $r['id']]
|
|
< [(int) $previous['create_time'], (int) $previous['id']]) {
|
|
$out[$s][$did] = $r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
public static function metricPack(array $calc, array $assistantIds, array $slots): array
|
|
{
|
|
$diagSet = [];
|
|
foreach ($assistantIds as $aid) {
|
|
foreach ($calc['pairsByAssistant'][$aid] ?? [] as $did => $_) {
|
|
$diagSet[$did] = true;
|
|
}
|
|
}
|
|
$count = \count($diagSet);
|
|
$inherit = $calc['cohort_type'] === self::TYPE_INHERIT;
|
|
$pack = [
|
|
'assigned_count' => $inherit ? 0 : $count,
|
|
'inherit_count' => $inherit ? $count : 0,
|
|
'assigned_total_count' => $count,
|
|
];
|
|
$orders = self::scopedOrders($calc, $assistantIds);
|
|
$all = array_sum(array_map('count', $orders));
|
|
foreach ($slots as $slot) {
|
|
$num = \count($orders[$slot] ?? []);
|
|
$pack['visit' . $slot . '_count'] = $num;
|
|
$pack['visit' . $slot . '_den'] = $count;
|
|
$pack['visit' . $slot . '_rate'] = $count > 0 ? round($num / $count * 100, 2) : null;
|
|
}
|
|
$pack['visit_all_count'] = $all;
|
|
$pack['visit_all_den'] = $count;
|
|
$pack['visit_all_rate'] = $count > 0 ? round($all / $count * 100, 2) : null;
|
|
|
|
return $pack;
|
|
}
|
|
|
|
/** 严格校验日期,避免非法日期被 strtotime 自动滚到次月。 */
|
|
public static function dateRange(array $params, int $now): array
|
|
{
|
|
$start = trim((string) ($params['start_date'] ?? ''));
|
|
$end = trim((string) ($params['end_date'] ?? ''));
|
|
if ($start === '' && $end === '') {
|
|
$month = trim((string) ($params['month'] ?? ''));
|
|
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) === 1) {
|
|
$start = $month . '-01';
|
|
$end = date('Y-m-t', (int) strtotime($start));
|
|
} else {
|
|
$start = date('Y-m-01', $now);
|
|
$end = date('Y-m-d', $now);
|
|
}
|
|
}
|
|
foreach ([$start, $end] as $date) {
|
|
$parsed = \DateTimeImmutable::createFromFormat('!Y-m-d', $date);
|
|
if ($parsed === false || $parsed->format('Y-m-d') !== $date) {
|
|
throw new \InvalidArgumentException('请选择有效的开始日期和结束日期');
|
|
}
|
|
}
|
|
if ($start > $end) {
|
|
throw new \InvalidArgumentException('开始日期不能晚于结束日期');
|
|
}
|
|
|
|
return [$start, $end, (int) strtotime($start . ' 00:00:00'), (int) strtotime($end . ' 23:59:59')];
|
|
}
|
|
}
|