279 lines
11 KiB
PHP
279 lines
11 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service\tcm;
|
||
|
||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||
use think\facade\Db;
|
||
|
||
/**
|
||
* 诊次(第几诊)口径单点实现,复诊接诊率 / 诊单列表「复诊节点」/ 到期快照共用。
|
||
*
|
||
* 口径(与 RevisitRateLogic 完全一致):
|
||
* - 有效业务订单 E(D):tcm_prescription_order 中 delete_time IS NULL 且 fulfillment_status IS NULL OR NOT IN (4,9,10)
|
||
* (统一经 YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery),按 (create_time, id) 升序;
|
||
* - 偏移 off(D) = tcm_diagnosis.revisit_slot_start_offset 钳到 0~20;
|
||
* - 第 n 笔有效订单的诊次 = n + off(D);患者当前已完成诊次 current_slot = |E(D)| + off(D);待复诊诊次 next_slot = current_slot + 1;
|
||
* - 某时刻 t 患者处于第几诊 slot_at(D, t) = |{o : create_time(o) < t}| + off(D)(严格小于,与持有人规则 t <= ct 对偶)。
|
||
*
|
||
* MySQL 5.7 无窗口函数:batchCurrentSlot 先 GROUP BY 聚合,再按 (diagnosis_id, create_time) 回表,PHP 侧取 id 最大的一行。
|
||
*/
|
||
final class RevisitVisitSlotService
|
||
{
|
||
/** 偏移量上限(与 RevisitRateLogic 历史实现一致) */
|
||
public const MAX_OFFSET = 20;
|
||
|
||
/** 单次 SQL 的诊单分块大小 */
|
||
private const CHUNK_SIZE = 2000;
|
||
|
||
/**
|
||
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
|
||
* 从 RevisitRateLogic::fetchOrderSeqRows 搬出,行为不变(调用方自行分块)。
|
||
*
|
||
* @param list<int> $diagnosisIds
|
||
* @param list<string> $fields 需带 `o.` 别名前缀,如 ['o.id', 'o.diagnosis_id', 'o.create_time']
|
||
*
|
||
* @return list<array<string, mixed>>
|
||
*/
|
||
public static function fetchOrderSeqRows(array $diagnosisIds, array $fields): array
|
||
{
|
||
if ($diagnosisIds === []) {
|
||
return [];
|
||
}
|
||
$q = Db::name('tcm_prescription_order')
|
||
->alias('o')
|
||
->whereIn('o.diagnosis_id', $diagnosisIds)
|
||
->whereNull('o.delete_time');
|
||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
|
||
|
||
return $q
|
||
->field($fields)
|
||
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 诊单 => revisit_slot_start_offset(未钳制的原始值;已软删诊单不返回)。
|
||
* 从 RevisitRateLogic::fetchRevisitSlotStartOffsetMap 搬出,行为不变。
|
||
*
|
||
* @param list<int> $diagnosisIds
|
||
*
|
||
* @return array<int, int>
|
||
*/
|
||
public static function fetchRevisitSlotStartOffsetMap(array $diagnosisIds): array
|
||
{
|
||
if ($diagnosisIds === []) {
|
||
return [];
|
||
}
|
||
$out = [];
|
||
foreach (array_chunk($diagnosisIds, self::CHUNK_SIZE) as $chunk) {
|
||
$rows = Db::name('tcm_diagnosis')
|
||
->whereIn('id', $chunk)
|
||
->whereNull('delete_time')
|
||
->column('revisit_slot_start_offset', 'id');
|
||
foreach ($rows as $id => $offset) {
|
||
$out[(int) $id] = (int) $offset;
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 诊单复诊统计起始偏移(默认 0:第 1 笔实单计为一诊;统计诊次 = 实单序号 + 偏移),钳到 0~20。
|
||
* 从 RevisitRateLogic::resolveRevisitSlotStartOffset 搬出,行为不变。
|
||
*
|
||
* @param array<int, int> $map diagnosis_id => offset
|
||
*/
|
||
public static function resolveRevisitSlotStartOffset(int $diagnosisId, array $map): int
|
||
{
|
||
$offset = (int) ($map[$diagnosisId] ?? 0);
|
||
if ($offset < 0) {
|
||
$offset = 0;
|
||
}
|
||
if ($offset > self::MAX_OFFSET) {
|
||
$offset = self::MAX_OFFSET;
|
||
}
|
||
|
||
return $offset;
|
||
}
|
||
|
||
/**
|
||
* 纯函数:时刻 atTs 之前(严格小于)的有效订单数 + 偏移 = 该时刻患者已完成的诊次。
|
||
* 同一秒的订单视为「指派之后」,不计入(与持有人规则 t <= ct 对偶)。
|
||
*
|
||
* @param list<int|string> $orderCreateTimes 有效订单的 create_time 列表(顺序不限)
|
||
*/
|
||
public static function slotBefore(array $orderCreateTimes, int $offset, int $atTs): int
|
||
{
|
||
$n = 0;
|
||
foreach ($orderCreateTimes as $ct) {
|
||
if ((int) $ct < $atTs) {
|
||
$n++;
|
||
}
|
||
}
|
||
if ($offset < 0) {
|
||
$offset = 0;
|
||
}
|
||
if ($offset > self::MAX_OFFSET) {
|
||
$offset = self::MAX_OFFSET;
|
||
}
|
||
|
||
return $n + $offset;
|
||
}
|
||
|
||
/**
|
||
* 某时刻患者处于第几诊(含偏移)。诊单不存在或无订单时返回偏移值。
|
||
*/
|
||
public static function slotAtTime(int $diagnosisId, int $ts): int
|
||
{
|
||
if ($diagnosisId <= 0) {
|
||
return 0;
|
||
}
|
||
$offsetMap = self::fetchRevisitSlotStartOffsetMap([$diagnosisId]);
|
||
$offset = self::resolveRevisitSlotStartOffset($diagnosisId, $offsetMap);
|
||
$rows = self::fetchOrderSeqRows([$diagnosisId], ['o.create_time']);
|
||
$cts = [];
|
||
foreach ($rows as $r) {
|
||
$cts[] = (int) ($r['create_time'] ?? 0);
|
||
}
|
||
|
||
return self::slotBefore($cts, $offset, $ts);
|
||
}
|
||
|
||
/**
|
||
* 批量计算每个诊单的当前诊次与最新有效订单(每个入参诊单都有返回,无订单时 latest_order 为 null)。
|
||
*
|
||
* @param list<int> $diagnosisIds
|
||
*
|
||
* @return array<int, array{
|
||
* diagnosis_id:int, offset:int, valid_order_count:int, current_slot:int, next_slot:int,
|
||
* first_order_time:int, total_medication_days:int,
|
||
* latest_order:null|array{id:int, order_no:string, create_time:int, medication_days:int, prescription_id:int, tracking_number:string, fulfillment_status:int|null, creator_id:int}
|
||
* }>
|
||
*/
|
||
public static function batchCurrentSlot(array $diagnosisIds): array
|
||
{
|
||
$ids = [];
|
||
foreach ($diagnosisIds as $id) {
|
||
$id = (int) $id;
|
||
if ($id > 0) {
|
||
$ids[$id] = true;
|
||
}
|
||
}
|
||
$ids = array_keys($ids);
|
||
if ($ids === []) {
|
||
return [];
|
||
}
|
||
|
||
$offsetMap = self::fetchRevisitSlotStartOffsetMap($ids);
|
||
|
||
/** @var array<int, array{cnt:int, max_ct:int, min_ct:int, sum_days:int}> $agg */
|
||
$agg = [];
|
||
/** @var array<int, array<string, mixed>> $latest diagnosis_id => 最新有效订单行 */
|
||
$latest = [];
|
||
|
||
foreach (array_chunk($ids, self::CHUNK_SIZE) as $chunk) {
|
||
// 1. 聚合:有效订单数 / 最新与最早下单时间 / 累计开药天数
|
||
// 累计开药天数按「订单 medication_days ?: 处方 usage_days」逐单回落(处方剔除系统代开/作废/软删,一单对一方,不会放大行数)
|
||
$q = Db::name('tcm_prescription_order')
|
||
->alias('o')
|
||
->leftJoin('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.is_system_auto = 0 AND rx.void_status = 0 AND rx.delete_time IS NULL')
|
||
->whereIn('o.diagnosis_id', $chunk)
|
||
->whereNull('o.delete_time');
|
||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
|
||
$aggRows = $q
|
||
->fieldRaw('o.diagnosis_id, COUNT(*) AS cnt, MAX(o.create_time) AS max_ct, MIN(o.create_time) AS min_ct, SUM(COALESCE(NULLIF(o.medication_days, 0), NULLIF(rx.usage_days, 0), 0)) AS sum_days')
|
||
->group('o.diagnosis_id')
|
||
->select()
|
||
->toArray();
|
||
|
||
$maxCts = [];
|
||
$chunkDiagWithOrders = [];
|
||
foreach ($aggRows as $r) {
|
||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||
if ($did <= 0) {
|
||
continue;
|
||
}
|
||
$maxCt = (int) ($r['max_ct'] ?? 0);
|
||
$agg[$did] = [
|
||
'cnt' => (int) ($r['cnt'] ?? 0),
|
||
'max_ct' => $maxCt,
|
||
'min_ct' => (int) ($r['min_ct'] ?? 0),
|
||
'sum_days' => (int) ($r['sum_days'] ?? 0),
|
||
];
|
||
$maxCts[$maxCt] = true;
|
||
$chunkDiagWithOrders[] = $did;
|
||
}
|
||
if ($chunkDiagWithOrders === []) {
|
||
continue;
|
||
}
|
||
|
||
// 2. 回表:按 (diagnosis_id, create_time) 取候选行,PHP 侧过滤精确匹配并取 id 最大者
|
||
$q2 = Db::name('tcm_prescription_order')
|
||
->alias('o')
|
||
->whereIn('o.diagnosis_id', $chunkDiagWithOrders)
|
||
->whereIn('o.create_time', array_keys($maxCts))
|
||
->whereNull('o.delete_time');
|
||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q2, 'o');
|
||
$candRows = $q2
|
||
->field([
|
||
'o.id', 'o.order_no', 'o.diagnosis_id', 'o.create_time', 'o.medication_days',
|
||
'o.prescription_id', 'o.tracking_number', 'o.fulfillment_status', 'o.creator_id',
|
||
])
|
||
->select()
|
||
->toArray();
|
||
foreach ($candRows as $r) {
|
||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||
if ($did <= 0 || !isset($agg[$did])) {
|
||
continue;
|
||
}
|
||
if ((int) ($r['create_time'] ?? 0) !== $agg[$did]['max_ct']) {
|
||
continue;
|
||
}
|
||
$oid = (int) ($r['id'] ?? 0);
|
||
if (!isset($latest[$did]) || $oid > (int) ($latest[$did]['id'] ?? 0)) {
|
||
$latest[$did] = $r;
|
||
}
|
||
}
|
||
}
|
||
|
||
$out = [];
|
||
foreach ($ids as $did) {
|
||
$offset = self::resolveRevisitSlotStartOffset($did, $offsetMap);
|
||
$a = $agg[$did] ?? null;
|
||
$cnt = $a !== null ? $a['cnt'] : 0;
|
||
$currentSlot = $cnt + $offset;
|
||
$lo = $latest[$did] ?? null;
|
||
$latestOrder = null;
|
||
if ($lo !== null) {
|
||
$fs = $lo['fulfillment_status'] ?? null;
|
||
$latestOrder = [
|
||
'id' => (int) ($lo['id'] ?? 0),
|
||
'order_no' => (string) ($lo['order_no'] ?? ''),
|
||
'create_time' => (int) ($lo['create_time'] ?? 0),
|
||
'medication_days' => (int) ($lo['medication_days'] ?? 0),
|
||
'prescription_id' => (int) ($lo['prescription_id'] ?? 0),
|
||
'tracking_number' => trim((string) ($lo['tracking_number'] ?? '')),
|
||
'fulfillment_status' => $fs === null ? null : (int) $fs,
|
||
'creator_id' => (int) ($lo['creator_id'] ?? 0),
|
||
];
|
||
}
|
||
$out[$did] = [
|
||
'diagnosis_id' => $did,
|
||
'offset' => $offset,
|
||
'valid_order_count' => $cnt,
|
||
'current_slot' => $currentSlot,
|
||
'next_slot' => $currentSlot + 1,
|
||
'first_order_time' => $a !== null ? $a['min_ct'] : 0,
|
||
'total_medication_days' => $a !== null ? $a['sum_days'] : 0,
|
||
'latest_order' => $latestOrder,
|
||
];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
}
|