Merge branch 'master' of https://gitee.com/v_cms/zyt
This commit is contained in:
@@ -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")
|
||||
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->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)
|
||||
->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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user