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