更新
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
<?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')];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user