This commit is contained in:
Your Name
2026-06-10 14:25:22 +08:00
parent 1adf8ccf4b
commit 9f1801771e
7 changed files with 2020 additions and 1 deletions
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\RevisitRateLogic;
/**
* 复诊接诊率统计(按月)
*
* - GET stats.revisitRate/overview 当月 N 诊接诊率(部门 → 医助分组 + 合计)
* - GET stats.revisitRate/deptOptions 部门下拉(前端组树)
* - GET stats.revisitRate/assignLines 「被指派数」明细(按诊单聚合)
* - GET stats.revisitRate/visitOrderLines 「N 诊单数」订单明细
*/
class RevisitRateController extends BaseAdminController
{
public function overview()
{
// 涉及指派日志 + 全量订单序列扫描,放宽执行时间兜底
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::overview($params));
}
public function deptOptions()
{
return $this->data(RevisitRateLogic::deptOptions());
}
/** 「被指派数」点击下钻:诊单维度明细 */
public function assignLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::assignLines($params));
}
/** 「N 诊单数」点击下钻:具体订单明细 */
public function visitOrderLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(RevisitRateLogic::visitOrderLines($params));
}
}
@@ -0,0 +1,678 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use think\facade\Db;
/**
* 复诊接诊率统计(按月)
*
* 口径说明:
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
* 同一诊单当月被多次指派给同一医助只计 1 次。
* - 第 N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
* - 当月 N 诊接诊率 = 当月 N 诊单数 ÷ 当月被指派总数;往月指派当月成交会推高分子,故比率可能超过 100%;
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
* - 分档动态产出:N 从 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;部门筛选(dept_ids,含组织下级)按该归属部门过滤。
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
*/
class RevisitRateLogic
{
/** 至少展示到的复诊序号(二诊/三诊/四诊) */
private const MIN_VISIT_SLOT_CEILING = 4;
/** 复诊序号统计上限(防御脏数据导致列爆炸;诊次跨月累计,上限放宽) */
private const MAX_VISIT_SLOT = 50;
/** 未分配部门的占位分组 */
private const UNASSIGNED_DEPT_NAME = '未分配部门';
/**
* @param array{month?:string,dept_ids?:int[]|string} $params
*
* @return array{
* month:string,start_date:string,end_date:string,
* slots:list<int>,
* total:array<string,int|float|null>,
* rows:list<array<string,mixed>>
* }
*/
public static function overview(array $params): array
{
$ctx = self::buildStatsCore($params);
$month = $ctx['month'];
$minSlots = range(2, self::MIN_VISIT_SLOT_CEILING);
$universe = self::assistantUniverse($ctx);
if ($universe === []) {
return [
'month' => $month,
'start_date' => $ctx['startDate'],
'end_date' => $ctx['endDate'],
'slots' => $minSlots,
'total' => self::buildMetricPack(0, [], $minSlots),
'rows' => [],
];
}
// 分档:2 ~ max(4, 当月命中的最大诊次)
$maxHitSlot = 0;
foreach ($ctx['slotOrdersByAssistant'] as $slotMap) {
foreach ($slotMap as $slot => $_) {
if ((int) $slot > $maxHitSlot) {
$maxHitSlot = (int) $slot;
}
}
}
$slots = range(2, max(self::MIN_VISIT_SLOT_CEILING, $maxHitSlot));
$nameMap = Db::name('admin')
->whereIn('id', array_keys($universe))
->column('name', 'id');
// 医助行(按部门分组收集)
/** @var array<int, list<array<string, mixed>>> $assistantRowsByDept */
$assistantRowsByDept = [];
/** @var array<int, array<int, true>> $deptDiagSet 部门 => 去重被指派诊单集合 */
$deptDiagSet = [];
/** @var array<int, array<int, int>> $deptSlotCounts 部门 => [slot => 订单数] */
$deptSlotCounts = [];
/** @var array<int, true> $totalDiagSet */
$totalDiagSet = [];
/** @var array<int, int> $totalSlotCounts */
$totalSlotCounts = [];
foreach ($universe as $aid => $_) {
$deptId = (int) ($ctx['assistantDept'][$aid] ?? 0);
$diagSet = $ctx['diagsByAssistant'][$aid] ?? [];
foreach ($diagSet as $did => $_d) {
$deptDiagSet[$deptId][$did] = true;
$totalDiagSet[$did] = true;
}
$slotCounts = [];
foreach ($ctx['slotOrdersByAssistant'][$aid] ?? [] as $slot => $orders) {
$cnt = \count($orders);
$slotCounts[$slot] = $cnt;
$deptSlotCounts[$deptId][$slot] = ($deptSlotCounts[$deptId][$slot] ?? 0) + $cnt;
$totalSlotCounts[$slot] = ($totalSlotCounts[$slot] ?? 0) + $cnt;
}
$deptName = $deptId > 0
? (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId))
: self::UNASSIGNED_DEPT_NAME;
$assistantRowsByDept[$deptId][] = [
'row_key' => 'a' . $aid,
'is_dept' => 0,
'assistant_id' => (int) $aid,
'assistant_name' => (string) ($nameMap[$aid] ?? ('#' . $aid)),
'dept_id' => $deptId,
'dept_name' => $deptName,
] + self::buildMetricPack(\count($diagSet), $slotCounts, $slots);
}
// 部门行 + 子行
$rows = [];
foreach ($assistantRowsByDept as $deptId => $children) {
usort($children, static function (array $a, array $b): int {
if ($a['assigned_count'] !== $b['assigned_count']) {
return $b['assigned_count'] <=> $a['assigned_count'];
}
return strcmp((string) $a['assistant_name'], (string) $b['assistant_name']);
});
$deptName = $deptId > 0
? (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId))
: self::UNASSIGNED_DEPT_NAME;
$rows[] = [
'row_key' => 'd' . $deptId,
'is_dept' => 1,
'dept_id' => (int) $deptId,
'dept_name' => $deptName,
'assistant_count' => \count($children),
'children' => $children,
] + self::buildMetricPack(\count($deptDiagSet[$deptId] ?? []), $deptSlotCounts[$deptId] ?? [], $slots);
}
usort($rows, static function (array $a, array $b): int {
if ($a['assigned_count'] !== $b['assigned_count']) {
return $b['assigned_count'] <=> $a['assigned_count'];
}
return strcmp((string) $a['dept_name'], (string) $b['dept_name']);
});
return [
'month' => $month,
'start_date' => $ctx['startDate'],
'end_date' => $ctx['endDate'],
'slots' => $slots,
'total' => self::buildMetricPack(\count($totalDiagSet), $totalSlotCounts, $slots),
'rows' => $rows,
];
}
/**
* 被指派明细(按诊单聚合,与「被指派数」同口径可对账)。
* scopeassistant_id(医助行)/ dept_id(部门行,含 0=未分配部门)/ 都不传 = 当前部门筛选下合计。
*
* @param array{month?:string,dept_ids?:int[]|string,assistant_id?:int|string,dept_id?:int|string} $params
*
* @return array{month:string,count:int,rows:list<array<string,mixed>>}
*/
public static function assignLines(array $params): array
{
$ctx = self::buildStatsCore($params);
$assistantSet = self::applyRowScope($ctx, $params);
/** @var array<int, array{assistants: array<int, true>, assign_count: int, last_time: int}> $byDiag */
$byDiag = [];
foreach ($ctx['pairsRaw'] as $p) {
$aid = (int) $p['to_assistant_id'];
$did = (int) $p['diagnosis_id'];
if (!isset($assistantSet[$aid])) {
continue;
}
if (!isset($byDiag[$did])) {
$byDiag[$did] = ['assistants' => [], 'assign_count' => 0, 'last_time' => 0];
}
$byDiag[$did]['assistants'][$aid] = true;
$byDiag[$did]['assign_count']++;
$byDiag[$did]['last_time'] = max($byDiag[$did]['last_time'], (int) $p['create_time']);
}
if ($byDiag === []) {
return ['month' => $ctx['month'], 'count' => 0, 'rows' => []];
}
$diagInfo = self::fetchDiagnosisInfo(array_keys($byDiag));
$assistantIds = [];
foreach ($byDiag as $d) {
foreach ($d['assistants'] as $aid => $_) {
$assistantIds[$aid] = true;
}
}
$nameMap = Db::name('admin')
->whereIn('id', array_keys($assistantIds))
->column('name', 'id');
$rows = [];
foreach ($byDiag as $did => $d) {
$names = [];
foreach ($d['assistants'] as $aid => $_) {
$names[] = (string) ($nameMap[$aid] ?? ('#' . $aid));
}
$rows[] = [
'diagnosis_id' => (int) $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'assistant_names' => implode('、', $names),
'assign_count' => (int) $d['assign_count'],
'last_assign_time' => (int) $d['last_time'],
'last_assign_time_text' => $d['last_time'] > 0 ? date('Y-m-d H:i:s', $d['last_time']) : '',
];
}
usort($rows, static fn (array $a, array $b): int => $b['last_assign_time'] <=> $a['last_assign_time']);
return ['month' => $ctx['month'], 'count' => \count($rows), 'rows' => $rows];
}
/**
* N 诊订单明细:scope 内当月下单、全局序号 = slot 的具体订单(归属持有医助),与 visit{slot}_count 同口径可对账。
*
* @param array{month?:string,slot?:int|string,dept_ids?:int[]|string,assistant_id?:int|string,dept_id?:int|string} $params
*
* @return array{month:string,slot:int,count:int,rows:list<array<string,mixed>>}
*/
public static function visitOrderLines(array $params): array
{
$slot = (int) ($params['slot'] ?? 0);
if ($slot < 2 || $slot > self::MAX_VISIT_SLOT) {
return ['month' => self::normalizeMonth((string) ($params['month'] ?? '')), 'slot' => $slot, 'count' => 0, 'rows' => []];
}
$ctx = self::buildStatsCore($params);
$assistantSet = self::applyRowScope($ctx, $params);
$orderRows = [];
foreach ($assistantSet as $aid => $_) {
foreach ($ctx['slotOrdersByAssistant'][$aid][$slot] ?? [] as $r) {
$r['holder_assistant_id'] = (int) $aid;
$orderRows[] = $r;
}
}
if ($orderRows === []) {
return ['month' => $ctx['month'], 'slot' => $slot, 'count' => 0, 'rows' => []];
}
$diagIds = array_values(array_unique(array_map(
static fn (array $r): int => (int) $r['diagnosis_id'],
$orderRows
)));
$diagInfo = self::fetchDiagnosisInfo($diagIds);
$adminIds = [];
foreach ($orderRows as $r) {
if ((int) $r['creator_id'] > 0) {
$adminIds[(int) $r['creator_id']] = true;
}
$adminIds[(int) $r['holder_assistant_id']] = true;
}
$adminNames = $adminIds !== []
? Db::name('admin')->whereIn('id', array_keys($adminIds))->column('name', 'id')
: [];
$rows = [];
foreach ($orderRows as $r) {
$did = (int) $r['diagnosis_id'];
$cid = (int) $r['creator_id'];
$hid = (int) $r['holder_assistant_id'];
$ct = (int) $r['create_time'];
$rows[] = [
'order_id' => (int) $r['id'],
'order_no' => (string) ($r['order_no'] ?? ''),
'diagnosis_id' => $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'amount' => round((float) ($r['amount'] ?? 0), 2),
'create_time' => $ct,
'create_time_text' => $ct > 0 ? date('Y-m-d H:i:s', $ct) : '',
'creator_id' => $cid,
'creator_name' => $cid > 0 ? (string) ($adminNames[$cid] ?? ('#' . $cid)) : '—',
'assistant_id' => $hid,
'assistant_name' => $hid > 0 ? (string) ($adminNames[$hid] ?? ('#' . $hid)) : '—',
];
}
usort($rows, static fn (array $a, array $b): int => $b['create_time'] <=> $a['create_time']);
return ['month' => $ctx['month'], 'slot' => $slot, 'count' => \count($rows), 'rows' => $rows];
}
/**
* 部门下拉(全量未删除部门,前端组树)。
*
* @return array{rows: list<array{id:int,pid:int,name:string}>}
*/
public static function deptOptions(): array
{
$rows = Db::name('dept')
->whereNull('delete_time')
->field(['id', 'pid', 'name'])
->order('sort', 'desc')
->order('id', 'asc')
->select()
->toArray();
return [
'rows' => array_map(static fn (array $r): array => [
'id' => (int) $r['id'],
'pid' => (int) $r['pid'],
'name' => (string) $r['name'],
], $rows),
];
}
// ─────────────────────────── 内部实现 ───────────────────────────
/**
* 核心统计上下文:
* 1. 全量指派日志(≤ 月末)构建持有时间线;
* 2. 分母:当月非继承指派的「医助 × 诊单」;
* 3. 分子:曾被指派诊单的当月订单按全局序号 ≥2 归属持有医助;
* 4. 应用部门筛选(含组织下级)。
*
* @param array{month?:string,dept_ids?:int[]|string} $params
*
* @return array{
* month:string,startDate:string,endDate:string,startTs:int,endTs:int,
* pairsRaw:list<array{diagnosis_id:int,to_assistant_id:int,create_time:int}>,
* diagsByAssistant:array<int,array<int,true>>,
* slotOrdersByAssistant:array<int,array<int,list<array<string,mixed>>>>,
* assistantDept:array<int,int>,
* deptNames:array<int,string>
* }
*/
private static function buildStatsCore(array $params): array
{
$month = self::normalizeMonth((string) ($params['month'] ?? ''));
$startTs = (int) strtotime($month . '-01 00:00:00');
$endTs = (int) strtotime(date('Y-m-t', $startTs) . ' 23:59:59');
// 全量指派日志(≤ 月末,诊单未删除):含释放(to=0)与继承行,用于持有时间线
$logRows = Db::name('tcm_diagnosis_assign_log')
->alias('lg')
->join('tcm_diagnosis dg', 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->where('lg.create_time', '<=', $endTs)
->where('lg.diagnosis_id', '>', 0)
->field(['lg.id', 'lg.diagnosis_id', 'lg.to_assistant_id', 'lg.create_time', 'lg.is_inherit'])
->order(['lg.diagnosis_id' => 'asc', 'lg.create_time' => 'asc', 'lg.id' => 'asc'])
->select()
->toArray();
/** @var array<int, list<array{t:int,to:int}>> $timeline 诊单 => 持有变更时间线(升序) */
$timeline = [];
$pairsRaw = [];
/** @var array<int, array<int, true>> $diagsByAssistant 分母:医助 => 诊单集合 */
$diagsByAssistant = [];
/** @var array<int, true> $candidateDiagSet 曾被指派(to>0,含继承)的诊单 */
$candidateDiagSet = [];
foreach ($logRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0);
$aid = (int) ($r['to_assistant_id'] ?? 0);
$t = (int) ($r['create_time'] ?? 0);
$timeline[$did][] = ['t' => $t, 'to' => $aid];
if ($aid > 0) {
$candidateDiagSet[$did] = true;
if ((int) ($r['is_inherit'] ?? 0) === 0 && $t >= $startTs && $t <= $endTs) {
$pairsRaw[] = ['diagnosis_id' => $did, 'to_assistant_id' => $aid, 'create_time' => $t];
$diagsByAssistant[$aid][$did] = true;
}
}
}
// 分子:曾被指派诊单的当月订单(全局序号 ≥2),归属下单时点的持有医助
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
$slotOrdersByAssistant = [];
foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) {
$orderRows = self::fetchOrderSeqRows(
$chunk,
['o.id', 'o.order_no', 'o.diagnosis_id', 'o.create_time', 'o.amount', 'o.creator_id']
);
$curDid = 0;
$seq = 0;
$ptr = 0;
$holder = 0;
foreach ($orderRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0);
if ($did <= 0) {
continue;
}
if ($did !== $curDid) {
$curDid = $did;
$seq = 0;
$ptr = 0;
$holder = 0;
}
$seq++;
$ct = (int) ($r['create_time'] ?? 0);
// 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人
$tl = $timeline[$did] ?? [];
$tlCount = \count($tl);
while ($ptr < $tlCount && $tl[$ptr]['t'] <= $ct) {
$holder = (int) $tl[$ptr]['to'];
$ptr++;
}
if ($seq < 2 || $seq > self::MAX_VISIT_SLOT) {
continue;
}
if ($ct < $startTs || $ct > $endTs) {
continue;
}
if ($holder > 0) {
$slotOrdersByAssistant[$holder][$seq][] = $r;
}
}
}
// 医助归属部门 + 部门筛选(含组织下级)
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
$deptFilterIds = self::parseDeptIds($params['dept_ids'] ?? null);
if ($deptFilterIds !== []) {
$subtreeSet = self::expandDeptSubtreeSet($deptFilterIds);
foreach ($universeIds as $aid) {
$deptId = (int) ($assistantDept[$aid] ?? 0);
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
unset($diagsByAssistant[$aid], $slotOrdersByAssistant[$aid]);
}
}
}
return [
'month' => $month,
'startDate' => date('Y-m-d', $startTs),
'endDate' => date('Y-m-d', $endTs),
'startTs' => $startTs,
'endTs' => $endTs,
'pairsRaw' => $pairsRaw,
'diagsByAssistant' => $diagsByAssistant,
'slotOrdersByAssistant' => $slotOrdersByAssistant,
'assistantDept' => $assistantDept,
'deptNames' => $deptNames,
];
}
/**
* 统计涉及的医助全集:分母(被指派)∪ 分子(持有成交)。
*
* @param array{diagsByAssistant:array<int,array<int,true>>,slotOrdersByAssistant:array<int,array<int,list<array<string,mixed>>>>} $ctx
*
* @return array<int, true>
*/
private static function assistantUniverse(array $ctx): array
{
$set = [];
foreach (array_keys($ctx['diagsByAssistant']) as $aid) {
$set[(int) $aid] = true;
}
foreach (array_keys($ctx['slotOrdersByAssistant']) as $aid) {
$set[(int) $aid] = true;
}
return $set;
}
/**
* 行级 scopeassistant_id(医助行)优先;其次 dept_id(部门归组行,0=未分配部门);都不传 = 全部(已含部门筛选)。
*
* @return array<int, true> scope 内医助集合
*/
private static function applyRowScope(array $ctx, array $params): array
{
$assistantId = (int) ($params['assistant_id'] ?? 0);
$hasDeptScope = isset($params['dept_id']) && $params['dept_id'] !== '' && $params['dept_id'] !== null;
$deptScopeId = $hasDeptScope ? (int) $params['dept_id'] : -1;
$assistantSet = [];
foreach (self::assistantUniverse($ctx) as $aid => $_) {
if ($assistantId > 0) {
if ((int) $aid === $assistantId) {
$assistantSet[$aid] = true;
}
continue;
}
if ($hasDeptScope) {
if ((int) ($ctx['assistantDept'][$aid] ?? 0) === $deptScopeId) {
$assistantSet[$aid] = true;
}
continue;
}
$assistantSet[$aid] = true;
}
return $assistantSet;
}
/**
* 医助 → 归属部门 + 部门名称表。
*
* @param list<int> $adminIds
*
* @return array{0: array<int,int>, 1: array<int,string>}
*/
private static function buildAssistantDeptIndex(array $adminIds): array
{
if ($adminIds === []) {
return [[], []];
}
// admin_dept 为 (admin_id, dept_id) 联合主键、无自增 id;取最小 dept_id 作为归属部门保证确定性
$relRows = Db::name('admin_dept')
->whereIn('admin_id', $adminIds)
->order(['admin_id' => 'asc', 'dept_id' => 'asc'])
->field(['admin_id', 'dept_id'])
->select()
->toArray();
$deptNames = Db::name('dept')
->whereNull('delete_time')
->column('name', 'id');
$canonical = [];
foreach ($relRows as $r) {
$aid = (int) ($r['admin_id'] ?? 0);
$deptId = (int) ($r['dept_id'] ?? 0);
if ($aid <= 0 || $deptId <= 0 || isset($canonical[$aid])) {
continue;
}
if (!isset($deptNames[$deptId])) {
continue;
}
$canonical[$aid] = $deptId;
}
$names = [];
foreach ($deptNames as $id => $name) {
$names[(int) $id] = (string) $name;
}
return [$canonical, $names];
}
/**
* @param mixed $raw int[] | 逗号分隔字符串
*
* @return list<int>
*/
private static function parseDeptIds(mixed $raw): array
{
if ($raw === null || $raw === '' || $raw === []) {
return [];
}
$list = \is_array($raw) ? $raw : explode(',', (string) $raw);
return array_values(array_unique(array_filter(
array_map('intval', $list),
static fn (int $v): bool => $v > 0
)));
}
/**
* 选中部门 + 全部组织下级的 id 集合。
*
* @param list<int> $deptIds
*
* @return array<int, true>
*/
private static function expandDeptSubtreeSet(array $deptIds): array
{
$rows = Db::name('dept')
->whereNull('delete_time')
->field(['id', 'pid'])
->select()
->toArray();
$childrenByPid = [];
foreach ($rows as $r) {
$childrenByPid[(int) $r['pid']][] = (int) $r['id'];
}
$set = [];
$queue = $deptIds;
while ($queue !== []) {
$id = (int) array_shift($queue);
if ($id <= 0 || isset($set[$id])) {
continue;
}
$set[$id] = true;
foreach ($childrenByPid[$id] ?? [] as $childId) {
$queue[] = $childId;
}
}
return $set;
}
/**
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
*
* @param list<int> $diagIds
* @param list<string> $fields
*
* @return list<array<string, mixed>>
*/
private static function fetchOrderSeqRows(array $diagIds, array $fields): array
{
$q = Db::name('tcm_prescription_order')
->alias('o')
->whereIn('o.diagnosis_id', $diagIds)
->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();
}
/**
* @param list<int> $diagIds
*
* @return array<int, array{patient_name:string,phone:string}>
*/
private static function fetchDiagnosisInfo(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$rows = Db::name('tcm_diagnosis')
->whereIn('id', $diagIds)
->field(['id', 'patient_name', 'phone'])
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$out[(int) $r['id']] = [
'patient_name' => trim((string) ($r['patient_name'] ?? '')),
'phone' => trim((string) ($r['phone'] ?? '')),
];
}
return $out;
}
/**
* @param array<int, int> $slotCounts [slot => n]
* @param list<int> $slots 需输出的分档列表(保证各行键齐全)
*
* @return array<string, int|float|null>
*/
private static function buildMetricPack(int $assigned, array $slotCounts, array $slots): array
{
$pack = ['assigned_count' => $assigned];
foreach ($slots as $slot) {
$cnt = (int) ($slotCounts[$slot] ?? 0);
$pack['visit' . $slot . '_count'] = $cnt;
$pack['visit' . $slot . '_rate'] = $assigned > 0
? round($cnt / $assigned * 100, 2)
: null;
}
return $pack;
}
/** 归一化月份参数为 YYYY-MM,非法时回退当前月 */
private static function normalizeMonth(string $month): string
{
$month = trim($month);
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) === 1) {
return $month;
}
return date('Y-m');
}
}