$diagnosisIds * @param list $fields 需带 `o.` 别名前缀,如 ['o.id', 'o.diagnosis_id', 'o.create_time'] * * @return list> */ 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 $diagnosisIds * * @return array */ 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 $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 $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 $diagnosisIds * * @return array */ 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 $agg */ $agg = []; /** @var array> $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; } }