This commit is contained in:
2026-04-22 16:20:52 +08:00
parent a1a0b74596
commit 6ea61e9193
2 changed files with 96 additions and 132 deletions
@@ -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<string, mixed>
*/
@@ -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<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[]>
*/
@@ -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(
<<<SQL
SELECT
CAST(JSON_UNQUOTE(JSON_EXTRACT(q.follow_admin_ids, CONCAT('$[', idx.n, ']'))) AS UNSIGNED) AS admin_id,
COUNT(*) AS cnt
FROM zyt_qywx_external_contact q
JOIN (
{$indexSql}
) 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
);
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(
<<<SQL
SELECT
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) {
$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<int, array<string, mixed>> $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<int, int[]> $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<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
@@ -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' => [],
@@ -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();