This commit is contained in:
Your Name
2026-04-22 17:30:05 +08:00
2 changed files with 97 additions and 133 deletions
@@ -10,6 +10,9 @@ use think\facade\Db;
class ConversionLogic class ConversionLogic
{ {
private const VIRTUAL_DEPT_UNBOUND_ADMIN_ID = -1;
private const VIRTUAL_DEPT_UNASSIGNED_ID = -2;
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
@@ -51,8 +54,7 @@ class ConversionLogic
$adminToDeptIds = self::loadAdminDeptMap(); $adminToDeptIds = self::loadAdminDeptMap();
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp);
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate); self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate);
self::hydratePaidAuditAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp); self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp);
self::hydrateCompletedOrderStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp);
self::hydrateAccountCostStats($entities, $startDate, $endDate); self::hydrateAccountCostStats($entities, $startDate, $endDate);
if ($dimension === 'dept') { if ($dimension === 'dept') {
@@ -178,11 +180,17 @@ class ConversionLogic
*/ */
private static function loadEntities(string $dimension, array $params): array 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), '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), '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), 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_amount' => 0.0,
'completed_order_count' => 0, 'completed_order_count' => 0,
'account_cost' => 0.0, 'account_cost' => 0.0,
'_virtual_bucket' => false,
]; ];
} }
/**
* @param array<int, array<string, mixed>> $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<int, int[]> * @return array<int, int[]>
*/ */
@@ -327,84 +360,49 @@ class ConversionLogic
int $startTimestamp, int $startTimestamp,
int $endTimestamp int $endTimestamp
): void { ): void {
$timeSql = '((q.external_first_add_time BETWEEN ? AND ?) OR (q.external_first_add_time = 0 AND q.create_time BETWEEN ? AND ?))'; $rows = Db::name('qywx_external_contact_event')
$bind = [$startTimestamp, $endTimestamp, $startTimestamp, $endTimestamp]; ->alias('e')
$indexSql = self::buildJsonIndexUnionSql(32); ->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 版本。 foreach ($rows as $row) {
$countsByAdmin = Db::query( $adminId = (int)($row['admin_id'] ?? 0);
<<<SQL $addFansCount = (int)($row['add_fans_count'] ?? 0);
SELECT
CAST(JSON_UNQUOTE(JSON_EXTRACT(q.follow_admin_ids, CONCAT('$[', idx.n, ']'))) AS UNSIGNED) AS admin_id, if ($dimension === 'dept' && $adminId <= 0) {
COUNT(*) AS cnt if (isset($entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID])) {
FROM zyt_qywx_external_contact q $entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['add_fans_count'] += $addFansCount;
JOIN ( }
{$indexSql} continue;
) AS idx }
WHERE q.delete_time IS NULL
AND COALESCE(JSON_LENGTH(q.follow_admin_ids), 0) > idx.n
AND {$timeSql}
GROUP BY admin_id
SQL,
$bind
);
$mergedCounts = [];
foreach ($countsByAdmin as $row) {
$adminId = (int) ($row['admin_id'] ?? 0);
if ($adminId <= 0) { if ($adminId <= 0) {
continue; continue;
} }
$mergedCounts[$adminId] = ($mergedCounts[$adminId] ?? 0) + (int) ($row['cnt'] ?? 0);
}
// 兼容历史数据:follow_admin_ids 为空时,再从 follow_users(JSON) -> admin.work_wechat_userid 回填。 $hitEntityIds = self::mapEntityIds($dimension, $adminId, $entityIds, $adminToDeptIds);
$fallbackRows = Db::query( if ($dimension === 'dept' && $hitEntityIds === []) {
<<<SQL if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) {
SELECT $entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount;
a.id AS admin_id, }
COUNT(*) AS cnt
FROM zyt_qywx_external_contact q
JOIN (
{$indexSql}
) AS idx
ON COALESCE(JSON_LENGTH(q.follow_users), 0) > 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) {
continue; continue;
} }
$mergedCounts[$adminId] = ($mergedCounts[$adminId] ?? 0) + (int) ($row['cnt'] ?? 0);
}
foreach ($mergedCounts as $adminId => $count) { if ($hitEntityIds === []) {
foreach (self::mapEntityIds($dimension, (int) $adminId, $entityIds, $adminToDeptIds) as $entityId) { continue;
$entities[$entityId]['add_fans_count'] += (int) $count; }
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<int, array<string, mixed>> $entities * @param array<int, array<string, mixed>> $entities
* @param int[] $entityIds * @param int[] $entityIds
@@ -431,7 +429,7 @@ SQL,
->toArray(); ->toArray();
foreach ($rows as $row) { foreach ($rows as $row) {
$diagnosisId = (int)($row['patient_id'] ?? 0); $diagnosisId = (int)($row['diagnosis_id'] ?? 0);
if ($diagnosisId <= 0) { if ($diagnosisId <= 0) {
continue; continue;
} }
@@ -461,7 +459,7 @@ SQL,
* @param int[] $entityIds * @param int[] $entityIds
* @param array<int, int[]> $adminToDeptIds * @param array<int, int[]> $adminToDeptIds
*/ */
private static function hydrateCompletedOrderStats( private static function hydrateOrderAndAmountStats(
array &$entities, array &$entities,
string $dimension, string $dimension,
array $entityIds, array $entityIds,
@@ -474,11 +472,9 @@ SQL,
->alias('po') ->alias('po')
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id') ->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id')
->whereNull('po.delete_time') ->whereNull('po.delete_time')
// 与业务订单列表接诊口径对齐:双审(处方审核、支付单审核)都通过即算接诊诊单
->where('po.prescription_audit_status', 1)
->where('po.payment_slip_audit_status', 1) ->where('po.payment_slip_audit_status', 1)
->where('po.update_time', 'between', [$startTimestamp, $endTimestamp]) ->where('po.create_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(CASE WHEN po.prescription_audit_status = 1 THEN po.amount ELSE 0 END) AS total_amount")
->group($sourceExpr) ->group($sourceExpr)
->select() ->select()
->toArray(); ->toArray();
@@ -492,9 +488,11 @@ SQL,
} }
$orderCount = (int)($row['order_count'] ?? 0); $orderCount = (int)($row['order_count'] ?? 0);
$amount = round((float)($row['total_amount'] ?? 0), 2);
foreach ($mappedEntityIds as $entityId) { foreach ($mappedEntityIds as $entityId) {
$entities[$entityId]['completed_order_count'] += $orderCount; $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); unset($entity);
} }
/**
* 诊单金额:按支付单审核通过(payment_slip_audit_status=1)的业务订单 amount 汇总
*
* @param array<int, array<string, mixed>> $entities
* @param int[] $entityIds
* @param array<int, int[]> $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 * @param int[] $entityIds
@@ -710,25 +668,21 @@ SQL,
}); });
$rootRows = array_map(static fn (int $deptId): array => self::finalizeDeptNode($buildNode($deptId)), $rootIds); $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) { if ($selectedDeptId > 0) {
$selectedNode = self::findDeptNode($rootRows, $selectedDeptId); $selectedNode = self::findDeptNode($rootRows, $selectedDeptId);
if ($selectedNode !== null) { if ($selectedNode !== null) {
if ($virtualRoot !== null && (int)$selectedNode['id'] === (int)$virtualRoot['id']) { $allRows = [$selectedNode];
$allRows = $selectedNode['children'] ?? []; $chartRows = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode];
$chartRows = $selectedNode['children'] ?? [];
} else {
$allRows = [$selectedNode];
$chartRows = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode];
}
} else { } else {
$allRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? []) : $rootRows; $allRows = $rootRows;
$chartRows = $allRows; $chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
} }
} else { } else {
$allRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? []) : $rootRows; $allRows = $rootRows;
$chartRows = $allRows; $chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
} }
$offset = ($pageNo - 1) * $pageSize; $offset = ($pageNo - 1) * $pageSize;
@@ -854,7 +808,8 @@ SQL,
*/ */
private static function buildCharts(array $rows): array 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 = [ $ranking = [
'names' => [], 'names' => [],
@@ -2111,13 +2111,17 @@ class DiagnosisLogic extends BaseLogic
public static function getAssistants() public static function getAssistants()
{ {
try { try {
// 通过中间表查询角色ID为2的管理员(医助) // 这里不要用 Admin 模型查询,否则会触发其 append(role_id/dept_id/jobs_id)
$assistants = \app\common\model\auth\Admin::alias('a') // 每行再发多次查询,形成严重 N+1,统计页会被拖慢到十几秒。
$assistants = \think\facade\Db::name('admin')
->alias('a')
->join('admin_role ar', 'a.id = ar.admin_id') ->join('admin_role ar', 'a.id = ar.admin_id')
->where('ar.role_id', 2) ->where('ar.role_id', 2)
->where('a.disable', 0) ->where('a.disable', 0)
->whereNull('a.delete_time')
->field(['a.id', 'a.name', 'a.account']) ->field(['a.id', 'a.name', 'a.account'])
->order('a.id', 'asc') ->order('a.id', 'asc')
->distinct(true)
->select() ->select()
->toArray(); ->toArray();
@@ -2126,7 +2130,8 @@ class DiagnosisLogic extends BaseLogic
} }
$adminIds = array_column($assistants, 'id'); $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() ->select()
->toArray(); ->toArray();
$adminToDeptIds = []; $adminToDeptIds = [];
@@ -2148,7 +2153,8 @@ class DiagnosisLogic extends BaseLogic
$allDeptIds = array_keys($allDeptIds); $allDeptIds = array_keys($allDeptIds);
$deptNameMap = []; $deptNameMap = [];
if ($allDeptIds !== []) { if ($allDeptIds !== []) {
$deptNameMap = \app\common\model\dept\Dept::whereIn('id', $allDeptIds) $deptNameMap = \think\facade\Db::name('dept')
->whereIn('id', $allDeptIds)
->whereNull('delete_time') ->whereNull('delete_time')
->column('name', 'id'); ->column('name', 'id');
} }
@@ -2185,13 +2191,16 @@ class DiagnosisLogic extends BaseLogic
public static function getDoctors() public static function getDoctors()
{ {
try { try {
// 通过中间表查询角色ID为1的管理员(医生) // 同 getAssistants,避免 Admin 模型 append 触发的 N+1。
$doctors = \app\common\model\auth\Admin::alias('a') $doctors = \think\facade\Db::name('admin')
->alias('a')
->join('admin_role ar', 'a.id = ar.admin_id') ->join('admin_role ar', 'a.id = ar.admin_id')
->where('ar.role_id', 1) ->where('ar.role_id', 1)
->where('a.disable', 0) ->where('a.disable', 0)
->whereNull('a.delete_time')
->field(['a.id', 'a.name', 'a.account']) ->field(['a.id', 'a.name', 'a.account'])
->order('a.id', 'asc') ->order('a.id', 'asc')
->distinct(true)
->select() ->select()
->toArray(); ->toArray();