From 6ea61e91934339ee448170c945facf55fb56a183 Mon Sep 17 00:00:00 2001 From: ShengLong <452591453@qq.com> Date: Wed, 22 Apr 2026 16:20:52 +0800 Subject: [PATCH] update --- .../adminapi/logic/stats/ConversionLogic.php | 207 +++++++----------- .../app/adminapi/logic/tcm/DiagnosisLogic.php | 21 +- 2 files changed, 96 insertions(+), 132 deletions(-) diff --git a/server/app/adminapi/logic/stats/ConversionLogic.php b/server/app/adminapi/logic/stats/ConversionLogic.php index 390d0785..a3b96f12 100644 --- a/server/app/adminapi/logic/stats/ConversionLogic.php +++ b/server/app/adminapi/logic/stats/ConversionLogic.php @@ -10,6 +10,9 @@ use think\facade\Db; class ConversionLogic { + private const VIRTUAL_DEPT_UNBOUND_ADMIN_ID = -1; + private const VIRTUAL_DEPT_UNASSIGNED_ID = -2; + /** * @return array */ @@ -51,8 +54,7 @@ class ConversionLogic $adminToDeptIds = self::loadAdminDeptMap(); self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate); - self::hydratePaidAuditAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); - self::hydrateCompletedOrderStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); + self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); self::hydrateAccountCostStats($entities, $startDate, $endDate); if ($dimension === 'dept') { @@ -178,11 +180,17 @@ class ConversionLogic */ private static function loadEntities(string $dimension, array $params): array { - return match ($dimension) { + $entities = match ($dimension) { 'assistant' => self::loadAdminEntities(2, isset($params['assistant_id']) ? (int)$params['assistant_id'] : 0), 'doctor' => self::loadAdminEntities(1, isset($params['doctor_id']) ? (int)$params['doctor_id'] : 0), default => self::loadDeptEntities(isset($params['dept_id']) ? (int)$params['dept_id'] : 0), }; + + if ($dimension === 'dept' && (int)($params['dept_id'] ?? 0) <= 0) { + self::appendVirtualDeptEntities($entities); + } + + return $entities; } /** @@ -291,9 +299,34 @@ class ConversionLogic 'completed_order_amount' => 0.0, 'completed_order_count' => 0, 'account_cost' => 0.0, + '_virtual_bucket' => false, ]; } + /** + * @param array> $entities + */ + private static function appendVirtualDeptEntities(array &$entities): void + { + $entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID] = array_merge( + self::newEntityRow(self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID, '未绑定后台账号'), + [ + 'pid' => 0, + 'sort' => -999998, + '_virtual_bucket' => true, + ] + ); + + $entities[self::VIRTUAL_DEPT_UNASSIGNED_ID] = array_merge( + self::newEntityRow(self::VIRTUAL_DEPT_UNASSIGNED_ID, '未分配部门'), + [ + 'pid' => 0, + 'sort' => -999999, + '_virtual_bucket' => true, + ] + ); + } + /** * @return array */ @@ -327,84 +360,49 @@ class ConversionLogic int $startTimestamp, int $endTimestamp ): void { - $timeSql = '((q.external_first_add_time BETWEEN ? AND ?) OR (q.external_first_add_time = 0 AND q.create_time BETWEEN ? AND ?))'; - $bind = [$startTimestamp, $endTimestamp, $startTimestamp, $endTimestamp]; - $indexSql = self::buildJsonIndexUnionSql(32); + $rows = Db::name('qywx_external_contact_event') + ->alias('e') + ->leftJoin('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL') + ->where('e.change_type', 'add_external_contact') + ->where('e.event_time', 'between', [$startTimestamp, $endTimestamp]) + ->fieldRaw('e.user_id, a.id AS admin_id, COUNT(*) AS add_fans_count') + ->group('e.user_id, a.id') + ->select() + ->toArray(); - // 优先使用 follow_admin_ids(JSON 数组) 做 SQL 聚合,兼容不支持 JSON_TABLE 的 MySQL 版本。 - $countsByAdmin = Db::query( - << idx.n - AND {$timeSql} -GROUP BY admin_id -SQL, - $bind - ); + foreach ($rows as $row) { + $adminId = (int)($row['admin_id'] ?? 0); + $addFansCount = (int)($row['add_fans_count'] ?? 0); + + if ($dimension === 'dept' && $adminId <= 0) { + if (isset($entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID])) { + $entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['add_fans_count'] += $addFansCount; + } + continue; + } - $mergedCounts = []; - foreach ($countsByAdmin as $row) { - $adminId = (int) ($row['admin_id'] ?? 0); if ($adminId <= 0) { continue; } - $mergedCounts[$adminId] = ($mergedCounts[$adminId] ?? 0) + (int) ($row['cnt'] ?? 0); - } - // 兼容历史数据:follow_admin_ids 为空时,再从 follow_users(JSON) -> admin.work_wechat_userid 回填。 - $fallbackRows = Db::query( - << idx.n -JOIN zyt_admin a - ON a.work_wechat_userid = JSON_UNQUOTE(JSON_EXTRACT(q.follow_users, CONCAT('$[', idx.n, '].userid'))) - AND a.delete_time IS NULL -WHERE q.delete_time IS NULL - AND COALESCE(JSON_LENGTH(q.follow_admin_ids), 0) = 0 - AND {$timeSql} -GROUP BY a.id -SQL, - $bind - ); - - foreach ($fallbackRows as $row) { - $adminId = (int) ($row['admin_id'] ?? 0); - if ($adminId <= 0) { + $hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds); + if ($dimension === 'dept' && $hitEntityIds === []) { + if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) { + $entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount; + } continue; } - $mergedCounts[$adminId] = ($mergedCounts[$adminId] ?? 0) + (int) ($row['cnt'] ?? 0); - } - foreach ($mergedCounts as $adminId => $count) { - foreach (self::mapEntityIds($dimension, (int) $adminId, $entityIds, $adminToDeptIds) as $entityId) { - $entities[$entityId]['add_fans_count'] += (int) $count; + if ($hitEntityIds === []) { + continue; + } + + foreach ($hitEntityIds as $entityId) { + $entities[$entityId]['add_fans_count'] += $addFansCount; } } } - private static function buildJsonIndexUnionSql(int $maxIndex): string - { - $parts = []; - for ($i = 0; $i < $maxIndex; $i++) { - $parts[] = 'SELECT ' . $i . ' AS n'; - } - - return implode(' UNION ALL ', $parts); - } - /** * @param array> $entities * @param int[] $entityIds @@ -431,7 +429,7 @@ SQL, ->toArray(); foreach ($rows as $row) { - $diagnosisId = (int)($row['patient_id'] ?? 0); + $diagnosisId = (int)($row['diagnosis_id'] ?? 0); if ($diagnosisId <= 0) { continue; } @@ -461,7 +459,7 @@ SQL, * @param int[] $entityIds * @param array $adminToDeptIds */ - private static function hydrateCompletedOrderStats( + private static function hydrateOrderAndAmountStats( array &$entities, string $dimension, array $entityIds, @@ -474,11 +472,9 @@ SQL, ->alias('po') ->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id') ->whereNull('po.delete_time') - // 与业务订单列表接诊口径对齐:双审(处方审核、支付单审核)都通过即算接诊诊单 - ->where('po.prescription_audit_status', 1) ->where('po.payment_slip_audit_status', 1) ->where('po.update_time', 'between', [$startTimestamp, $endTimestamp]) - ->fieldRaw("{$sourceExpr} AS source_admin_id, COUNT(*) AS order_count") + ->fieldRaw("{$sourceExpr} AS source_admin_id, SUM(CASE WHEN po.prescription_audit_status = 1 THEN 1 ELSE 0 END) AS order_count, SUM(po.amount) AS total_amount") ->group($sourceExpr) ->select() ->toArray(); @@ -492,9 +488,11 @@ SQL, } $orderCount = (int)($row['order_count'] ?? 0); + $amount = round((float)($row['total_amount'] ?? 0), 2); foreach ($mappedEntityIds as $entityId) { $entities[$entityId]['completed_order_count'] += $orderCount; + $entities[$entityId]['completed_order_amount'] = round($entities[$entityId]['completed_order_amount'] + $amount, 2); } } } @@ -519,47 +517,7 @@ SQL, unset($entity); } - /** - * 诊单金额:按支付单审核通过(payment_slip_audit_status=1)的业务订单 amount 汇总 - * - * @param array> $entities - * @param int[] $entityIds - * @param array $adminToDeptIds - */ - private static function hydratePaidAuditAmountStats( - array &$entities, - string $dimension, - array $entityIds, - array $adminToDeptIds, - int $startTimestamp, - int $endTimestamp - ): void { - $sourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id'; - $rows = Db::name('tcm_prescription_order') - ->alias('po') - ->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id') - ->whereNull('po.delete_time') - ->where('po.payment_slip_audit_status', 1) - ->where('po.update_time', 'between', [$startTimestamp, $endTimestamp]) - ->fieldRaw("{$sourceExpr} AS source_admin_id, SUM(po.amount) AS total_amount") - ->group($sourceExpr) - ->select() - ->toArray(); - foreach ($rows as $row) { - $sourceAdminId = (int)($row['source_admin_id'] ?? 0); - - $mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds); - if ($mappedEntityIds === []) { - continue; - } - - $amount = round((float)($row['total_amount'] ?? 0), 2); - foreach ($mappedEntityIds as $entityId) { - $entities[$entityId]['completed_order_amount'] = round($entities[$entityId]['completed_order_amount'] + $amount, 2); - } - } - } /** * @param int[] $entityIds @@ -710,25 +668,21 @@ SQL, }); $rootRows = array_map(static fn (int $deptId): array => self::finalizeDeptNode($buildNode($deptId)), $rootIds); - $virtualRoot = count($rootRows) === 1 && !empty($rootRows[0]['children']) ? $rootRows[0] : null; + $realRootRows = array_values(array_filter($rootRows, static fn (array $row): bool => !((bool)($row['_virtual_bucket'] ?? false)))); + $virtualRoot = count($realRootRows) === 1 && !empty($realRootRows[0]['children']) ? $realRootRows[0] : null; if ($selectedDeptId > 0) { $selectedNode = self::findDeptNode($rootRows, $selectedDeptId); if ($selectedNode !== null) { - if ($virtualRoot !== null && (int)$selectedNode['id'] === (int)$virtualRoot['id']) { - $allRows = $selectedNode['children'] ?? []; - $chartRows = $selectedNode['children'] ?? []; - } else { - $allRows = [$selectedNode]; - $chartRows = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode]; - } + $allRows = [$selectedNode]; + $chartRows = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode]; } else { - $allRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? []) : $rootRows; - $chartRows = $allRows; + $allRows = $rootRows; + $chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows); } } else { - $allRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? []) : $rootRows; - $chartRows = $allRows; + $allRows = $rootRows; + $chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows); } $offset = ($pageNo - 1) * $pageSize; @@ -854,7 +808,8 @@ SQL, */ private static function buildCharts(array $rows): array { - $topRows = array_slice($rows, 0, 10); + $chartableRows = array_values(array_filter($rows, static fn (array $row): bool => !((bool)($row['_virtual_bucket'] ?? false)))); + $topRows = array_slice($chartableRows, 0, 10); $ranking = [ 'names' => [], diff --git a/server/app/adminapi/logic/tcm/DiagnosisLogic.php b/server/app/adminapi/logic/tcm/DiagnosisLogic.php index a3a2ce33..f54a9b94 100644 --- a/server/app/adminapi/logic/tcm/DiagnosisLogic.php +++ b/server/app/adminapi/logic/tcm/DiagnosisLogic.php @@ -2111,13 +2111,17 @@ class DiagnosisLogic extends BaseLogic public static function getAssistants() { try { - // 通过中间表查询角色ID为2的管理员(医助) - $assistants = \app\common\model\auth\Admin::alias('a') + // 这里不要用 Admin 模型查询,否则会触发其 append(role_id/dept_id/jobs_id) + // 每行再发多次查询,形成严重 N+1,统计页会被拖慢到十几秒。 + $assistants = \think\facade\Db::name('admin') + ->alias('a') ->join('admin_role ar', 'a.id = ar.admin_id') ->where('ar.role_id', 2) ->where('a.disable', 0) + ->whereNull('a.delete_time') ->field(['a.id', 'a.name', 'a.account']) ->order('a.id', 'asc') + ->distinct(true) ->select() ->toArray(); @@ -2126,7 +2130,8 @@ class DiagnosisLogic extends BaseLogic } $adminIds = array_column($assistants, 'id'); - $adminDepts = \app\common\model\auth\AdminDept::whereIn('admin_id', $adminIds) + $adminDepts = \think\facade\Db::name('admin_dept') + ->whereIn('admin_id', $adminIds) ->select() ->toArray(); $adminToDeptIds = []; @@ -2148,7 +2153,8 @@ class DiagnosisLogic extends BaseLogic $allDeptIds = array_keys($allDeptIds); $deptNameMap = []; if ($allDeptIds !== []) { - $deptNameMap = \app\common\model\dept\Dept::whereIn('id', $allDeptIds) + $deptNameMap = \think\facade\Db::name('dept') + ->whereIn('id', $allDeptIds) ->whereNull('delete_time') ->column('name', 'id'); } @@ -2185,13 +2191,16 @@ class DiagnosisLogic extends BaseLogic public static function getDoctors() { try { - // 通过中间表查询角色ID为1的管理员(医生) - $doctors = \app\common\model\auth\Admin::alias('a') + // 同 getAssistants,避免 Admin 模型 append 触发的 N+1。 + $doctors = \think\facade\Db::name('admin') + ->alias('a') ->join('admin_role ar', 'a.id = ar.admin_id') ->where('ar.role_id', 1) ->where('a.disable', 0) + ->whereNull('a.delete_time') ->field(['a.id', 'a.name', 'a.account']) ->order('a.id', 'asc') + ->distinct(true) ->select() ->toArray();