Files
zyt/server/app/adminapi/logic/stats/RevisitCohortLogic.php
T
2026-10-17 16:08:24 +08:00

251 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use think\facade\Db;
/** 指派 / 继承独立报表。旧按月计算器继续服务自动分配与个人业绩。 */
class RevisitCohortLogic
{
use RevisitCohortDataSource;
public static function overview(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$ctx = self::cohortContext($params, $type);
$calc = $ctx['calc'];
$aids = array_keys($calc['pairsByAssistant']);
$names = $aids === [] ? [] : Db::name('admin')->whereIn('id', $aids)->column('name', 'id');
$maxSlot = 4;
foreach ($calc['ordersByAssistant'] as $bySlot) {
foreach ($bySlot as $slot => $_) {
$maxSlot = max($maxSlot, $slot);
}
}
$slots = range(2, $maxSlot);
$byDept = [];
foreach ($aids as $aid) {
$deptId = $ctx['assistantDept'][$aid];
$byDept[$deptId][] = $aid;
}
$rows = [];
foreach ($byDept as $deptId => $deptAids) {
$children = [];
$deptName = (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId));
foreach ($deptAids as $aid) {
$children[] = [
'row_key' => 'a' . $aid,
'is_dept' => 0,
'dept_id' => $deptId,
'dept_name' => $deptName,
'assistant_id' => $aid,
'assistant_name' => (string) ($names[$aid] ?? ('#' . $aid)),
] + RevisitCohortCalculator::metricPack($calc, [$aid], $slots);
}
usort($children, static fn (array $a, array $b): int =>
($b['assigned_total_count'] <=> $a['assigned_total_count'])
?: strcmp($a['assistant_name'], $b['assistant_name']));
$rows[] = [
'row_key' => 'd' . $deptId,
'is_dept' => 1,
'dept_id' => $deptId,
'dept_name' => $deptName,
'assistant_count' => \count($children),
'children' => $children,
] + RevisitCohortCalculator::metricPack($calc, $deptAids, $slots);
}
usort($rows, static fn (array $a, array $b): int =>
($b['assigned_total_count'] <=> $a['assigned_total_count'])
?: strcmp($a['dept_name'], $b['dept_name']));
return self::metadata($ctx) + [
'slots' => $slots,
'total' => RevisitCohortCalculator::metricPack($calc, $aids, $slots),
'rows' => $rows,
];
}
public static function assignLines(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$ctx = self::cohortContext($params, $type);
$aids = self::scopedAssistantIds($ctx, $params);
$byDiag = [];
foreach ($aids as $aid) {
foreach ($ctx['calc']['pairsByAssistant'][$aid] ?? [] as $did => $pair) {
if (!isset($byDiag[$did])) {
$byDiag[$did] = $pair + ['assistants' => []];
$byDiag[$did]['assign_count'] = 0;
}
$byDiag[$did]['assistants'][$aid] = true;
$byDiag[$did]['assign_count'] += $pair['assign_count'];
$byDiag[$did]['first_assign_time'] = min($byDiag[$did]['first_assign_time'], $pair['first_assign_time']);
$byDiag[$did]['last_assign_time'] = max($byDiag[$did]['last_assign_time'], $pair['last_assign_time']);
$byDiag[$did]['is_rollback'] |= $pair['is_rollback'];
}
}
$diagInfo = self::fetchDiagnosisInfo(array_keys($byDiag));
$names = $aids === [] ? [] : Db::name('admin')->whereIn('id', $aids)->column('name', 'id');
$rows = [];
foreach ($byDiag as $did => $pair) {
$assistantNames = [];
foreach ($pair['assistants'] as $aid => $_) {
$assistantNames[] = (string) ($names[$aid] ?? ('#' . $aid));
}
$rows[] = [
'diagnosis_id' => $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'assistant_names' => implode('、', $assistantNames),
'assign_count' => $pair['assign_count'],
'first_assign_time_text' => date('Y-m-d H:i:s', $pair['first_assign_time']),
'last_assign_time' => $pair['last_assign_time'],
'last_assign_time_text' => date('Y-m-d H:i:s', $pair['last_assign_time']),
'assign_type' => $ctx['calc']['cohort_type'],
'is_rollback' => $pair['is_rollback'],
];
}
usort($rows, static fn (array $a, array $b): int => $b['last_assign_time'] <=> $a['last_assign_time']);
return self::metadata($ctx) + ['count' => \count($rows), 'rows' => $rows];
}
public static function visitOrderLines(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$slot = (int) ($params['slot'] ?? 0);
if ($slot < 0 || $slot === 1 || $slot > RevisitRateCalculator::MAX_VISIT_SLOT) {
throw new \InvalidArgumentException('诊次须为 2~50,或 0(复诊合计)');
}
$ctx = self::cohortContext($params, $type);
$orders = RevisitCohortCalculator::scopedOrders($ctx['calc'], self::scopedAssistantIds($ctx, $params), $slot);
$flat = [];
$adminIds = [];
$diagIds = [];
foreach ($orders as $byDiag) {
foreach ($byDiag as $did => $r) {
$flat[] = $r;
$diagIds[$did] = true;
$adminIds[(int) $r['assistant_id']] = true;
if ((int) ($r['creator_id'] ?? 0) > 0) {
$adminIds[(int) $r['creator_id']] = true;
}
}
}
$diagInfo = self::fetchDiagnosisInfo(array_keys($diagIds));
$names = $adminIds === [] ? [] : Db::name('admin')->whereIn('id', array_keys($adminIds))->column('name', 'id');
$rows = [];
foreach ($flat as $r) {
$did = (int) $r['diagnosis_id'];
$aid = (int) $r['assistant_id'];
$cid = (int) ($r['creator_id'] ?? 0);
$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' => (int) $r['create_time'],
'create_time_text' => date('Y-m-d H:i:s', (int) $r['create_time']),
'assistant_id' => $aid,
'assistant_name' => (string) ($names[$aid] ?? ('#' . $aid)),
'creator_name' => $cid > 0 ? (string) ($names[$cid] ?? ('#' . $cid)) : '—',
'visit_slot' => (int) $r['visit_slot'],
'global_visit_slot' => (int) $r['global_visit_slot'],
'cohort_assign_time_text' => date('Y-m-d H:i:s', (int) $r['cohort_assign_time']),
];
}
usort($rows, static fn (array $a, array $b): int => $b['create_time'] <=> $a['create_time']);
return self::metadata($ctx) + ['slot' => $slot, 'count' => \count($rows), 'rows' => $rows];
}
/** 保留部门下拉结构,补充二中心内医助;不影响复诊到期页对父类 deptOptions 的复用。 */
public static function deptOptions(): array
{
$options = self::cohortDeptOptions();
$deptSet = self::resolveDeptFilterSet(null);
if ($deptSet === []) {
return $options + ['assistants' => []];
}
$ids = Db::name('admin_dept')->whereIn('dept_id', array_keys($deptSet))->group('admin_id')->column('admin_id');
[$assistantDept] = self::buildAssistantDeptIndex($ids);
$admins = $ids === [] ? [] : Db::name('admin')->whereIn('id', $ids)->whereNull('delete_time')
->field(['id', 'name'])->order('name', 'asc')->select()->toArray();
$assistants = [];
foreach ($admins as $admin) {
$deptId = (int) ($assistantDept[(int) $admin['id']] ?? 0);
if (isset($deptSet[$deptId])) {
$assistants[] = ['id' => (int) $admin['id'], 'name' => (string) $admin['name'], 'dept_id' => $deptId];
}
}
return $options + ['assistants' => $assistants];
}
private static function cohortContext(array $params, string $type): array
{
$now = time();
[$start, $end, $startTs, $endTs] = RevisitCohortCalculator::dateRange($params, $now);
// 先按指派周期圈定诊单,再读取其完整时间线;避免小范围查询拉取全库历史日志。
$candidateQuery = 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', '>=', $startTs)->where('lg.create_time', '<=', min($endTs, $now))
->where('lg.to_assistant_id', '>', 0)->where('lg.diagnosis_id', '>', 0);
$filterAid = (int) ($params['assistant_id'] ?? 0);
if ($filterAid > 0) {
$candidateQuery->where('lg.to_assistant_id', $filterAid);
}
$candidateIds = array_map('intval', $candidateQuery->group('lg.diagnosis_id')->column('lg.diagnosis_id'));
$logs = [];
foreach (array_chunk($candidateIds, 2000) as $chunk) {
$chunkLogs = Db::name('tcm_diagnosis_assign_log')->alias('lg')
->whereIn('lg.diagnosis_id', $chunk)->where('lg.create_time', '<=', $now)
->field(['lg.id', 'lg.diagnosis_id', 'lg.from_assistant_id', 'lg.to_assistant_id',
'lg.operator_admin_id', 'lg.create_time', 'lg.is_inherit'])
->order(['lg.diagnosis_id' => 'asc', 'lg.create_time' => 'asc', 'lg.id' => 'asc'])
->select()->toArray();
array_push($logs, ...$chunkLogs);
}
$calc = RevisitCohortCalculator::compute(
$logs,
self::iterateOrderSeqRows($candidateIds, ['o.id', 'o.order_no', 'o.diagnosis_id', 'o.create_time', 'o.amount', 'o.creator_id']),
self::fetchRevisitSlotStartOffsetMap($candidateIds),
self::fetchRefundOrRejectDiagnosisSet($candidateIds),
self::fetchRollbackKeySet($startTs, min($endTs, $now)),
$startTs, $endTs, $now, $type
);
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex(array_keys($calc['pairsByAssistant']));
$deptSet = self::resolveDeptFilterSet($params['dept_ids'] ?? null);
foreach (array_keys($calc['pairsByAssistant']) as $aid) {
if (!isset($deptSet[$assistantDept[$aid] ?? 0]) || ($filterAid > 0 && $filterAid !== $aid)) {
unset($calc['pairsByAssistant'][$aid], $calc['ordersByAssistant'][$aid]);
}
}
return ['calc' => $calc, 'assistantDept' => $assistantDept, 'deptNames' => $deptNames,
'start_date' => $start, 'end_date' => $end, 'observed_until' => date('Y-m-d H:i:s', $now)];
}
private static function scopedAssistantIds(array $ctx, array $params): array
{
$aids = array_keys($ctx['calc']['pairsByAssistant']);
$scopeAid = (int) ($params['scope_assistant_id'] ?? 0);
if ($scopeAid > 0) {
return \in_array($scopeAid, $aids, true) ? [$scopeAid] : [];
}
if (isset($params['dept_id']) && $params['dept_id'] !== '') {
$deptId = (int) $params['dept_id'];
$aids = array_values(array_filter($aids, static fn (int $aid): bool => ($ctx['assistantDept'][$aid] ?? 0) === $deptId));
}
return $aids;
}
private static function metadata(array $ctx): array
{
return ['start_date' => $ctx['start_date'], 'end_date' => $ctx['end_date'],
'observed_until' => $ctx['observed_until'], 'cohort_type' => $ctx['calc']['cohort_type']];
}
}