This commit is contained in:
Your Name
2026-10-17 16:08:24 +08:00
parent c67b3eff69
commit 56f85f7d89
57 changed files with 4170 additions and 57 deletions
@@ -0,0 +1,250 @@
<?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']];
}
}