update dashboard
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Hyperf\DbConnection\Db;
|
||||
|
||||
/**
|
||||
* Dashboard 统计服务
|
||||
*
|
||||
* 封装 overview / trend / breakdown 三类聚合查询,
|
||||
* 使用原生 SQL 提高聚合效率,参数绑定防注入。
|
||||
*/
|
||||
class DashboardStatsService
|
||||
{
|
||||
/**
|
||||
* data_type => 实体表映射
|
||||
*/
|
||||
private const TYPE_TABLE_MAP = [
|
||||
'order' => 'orders',
|
||||
'product' => 'products',
|
||||
'refund' => 'refunds',
|
||||
];
|
||||
|
||||
/**
|
||||
* 允许的 data_type 值(含 inventory,仅统计失败数)
|
||||
*/
|
||||
public const VALID_DATA_TYPES = ['order', 'product', 'refund', 'inventory'];
|
||||
|
||||
/**
|
||||
* 允许的 group_by 值
|
||||
*/
|
||||
public const VALID_GROUP_BY = ['day', 'week', 'month'];
|
||||
|
||||
/**
|
||||
* 允许的 breakdown dimension 值
|
||||
*/
|
||||
public const VALID_DIMENSIONS = ['company', 'platform', 'store'];
|
||||
|
||||
/**
|
||||
* dimension => 表名和名称字段映射
|
||||
*/
|
||||
private const DIMENSION_TABLE_MAP = [
|
||||
'company' => ['table' => 'companies', 'name_field' => 'name'],
|
||||
'platform' => ['table' => 'platforms', 'name_field' => 'name'],
|
||||
'store' => ['table' => 'stores', 'name_field' => 'name'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取概览统计
|
||||
*
|
||||
* @return array{today: array, this_week: array, this_month: array, by_type: array}
|
||||
*/
|
||||
public function getOverview(string $scope_type, array $scope_ids): array
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$today_start = $now->copy()->startOfDay()->toDateTimeString();
|
||||
$today_end = $now->copy()->endOfDay()->toDateTimeString();
|
||||
$week_start = $now->copy()->startOfWeek()->toDateTimeString();
|
||||
$month_start = $now->copy()->startOfMonth()->toDateTimeString();
|
||||
|
||||
return [
|
||||
'today' => [
|
||||
'success' => $this->countAllSuccess($today_start, $today_end, $scope_type, $scope_ids),
|
||||
'failed' => $this->countFailed($today_start, $today_end, $scope_type, $scope_ids),
|
||||
],
|
||||
'this_week' => [
|
||||
'success' => $this->countAllSuccess($week_start, $today_end, $scope_type, $scope_ids),
|
||||
'failed' => $this->countFailed($week_start, $today_end, $scope_type, $scope_ids),
|
||||
],
|
||||
'this_month' => [
|
||||
'success' => $this->countAllSuccess($month_start, $today_end, $scope_type, $scope_ids),
|
||||
'failed' => $this->countFailed($month_start, $today_end, $scope_type, $scope_ids),
|
||||
],
|
||||
'by_type' => $this->getByTypeStats($month_start, $today_end, $scope_type, $scope_ids),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取趋势数据
|
||||
*
|
||||
* @return array<array{date: string, success: int, failed: int}>
|
||||
*/
|
||||
public function getTrend(
|
||||
string $from,
|
||||
string $to,
|
||||
string $group_by,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$trunc = $this->getDateTrunc($group_by);
|
||||
$from_dt = $from . ' 00:00:00';
|
||||
$to_dt = $to . ' 23:59:59';
|
||||
|
||||
// 成功数
|
||||
$success_map = $this->getTrendSuccess($trunc, $from_dt, $to_dt, $data_type, $scope_type, $scope_ids);
|
||||
|
||||
// 失败数
|
||||
$failed_map = $this->getTrendFailed($trunc, $from_dt, $to_dt, $data_type, $scope_type, $scope_ids);
|
||||
|
||||
// 合并所有日期
|
||||
$all_dates = array_unique(array_merge(array_keys($success_map), array_keys($failed_map)));
|
||||
sort($all_dates);
|
||||
|
||||
$result = [];
|
||||
foreach ($all_dates as $date) {
|
||||
$result[] = [
|
||||
'date' => $date,
|
||||
'success' => $success_map[$date] ?? 0,
|
||||
'failed' => $failed_map[$date] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组统计
|
||||
*
|
||||
* @return array<array{id: int, name: string, success: int, failed: int}>
|
||||
*/
|
||||
public function getBreakdown(
|
||||
string $dimension,
|
||||
string $from,
|
||||
string $to,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$from_dt = $from . ' 00:00:00';
|
||||
$to_dt = $to . ' 23:59:59';
|
||||
$dim_col = $dimension . '_id';
|
||||
$dim_config = self::DIMENSION_TABLE_MAP[$dimension];
|
||||
|
||||
// 成功数按维度分组
|
||||
$success_map = $this->getBreakdownSuccess($dim_col, $from_dt, $to_dt, $data_type, $scope_type, $scope_ids);
|
||||
|
||||
// 失败数按维度分组
|
||||
$failed_map = $this->getBreakdownFailed($dim_col, $from_dt, $to_dt, $data_type, $scope_type, $scope_ids);
|
||||
|
||||
// 合并所有维度 ID
|
||||
$all_ids = array_unique(array_merge(array_keys($success_map), array_keys($failed_map)));
|
||||
|
||||
if (empty($all_ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 查询维度名称
|
||||
$placeholders = implode(',', array_fill(0, count($all_ids), '?'));
|
||||
$name_rows = Db::select(
|
||||
"SELECT id, {$dim_config['name_field']} as name FROM {$dim_config['table']} WHERE id IN ({$placeholders})",
|
||||
array_values($all_ids)
|
||||
);
|
||||
$name_map = [];
|
||||
foreach ($name_rows as $row) {
|
||||
$name_map[$row->id] = $row->name;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($all_ids as $id) {
|
||||
$result[] = [
|
||||
'id' => (int) $id,
|
||||
'name' => $name_map[$id] ?? '',
|
||||
'success' => $success_map[$id] ?? 0,
|
||||
'failed' => $failed_map[$id] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
// 按 success 降序排列
|
||||
usort($result, fn($a, $b) => $b['success'] <=> $a['success']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计所有实体表的成功总数
|
||||
*/
|
||||
private function countAllSuccess(string $from, string $to, string $scope_type, array $scope_ids): int
|
||||
{
|
||||
$total = 0;
|
||||
foreach (self::TYPE_TABLE_MAP as $table) {
|
||||
$total += $this->countSuccess($table, $from, $to, $scope_type, $scope_ids);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计单个实体表的成功数
|
||||
*/
|
||||
private function countSuccess(string $table, string $from, string $to, string $scope_type, array $scope_ids): int
|
||||
{
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
|
||||
$sql = "SELECT COUNT(*) as cnt FROM {$table} WHERE created_at BETWEEN ? AND ?{$scope_sql}";
|
||||
$result = Db::select($sql, $bindings);
|
||||
|
||||
return (int) ($result[0]->cnt ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计失败消息数
|
||||
*/
|
||||
private function countFailed(
|
||||
string $from,
|
||||
string $to,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
?string $data_type = null,
|
||||
): int {
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
$type_sql = '';
|
||||
if ($data_type !== null) {
|
||||
$type_sql = ' AND data_type = ?';
|
||||
$bindings[] = $data_type;
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) as cnt FROM failed_messages WHERE failed_at BETWEEN ? AND ?{$scope_sql}{$type_sql}";
|
||||
$result = Db::select($sql, $bindings);
|
||||
|
||||
return (int) ($result[0]->cnt ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 data_type 分别统计成功和失败数
|
||||
*/
|
||||
private function getByTypeStats(string $from, string $to, string $scope_type, array $scope_ids): array
|
||||
{
|
||||
$stats = [];
|
||||
foreach (self::VALID_DATA_TYPES as $type) {
|
||||
$success = 0;
|
||||
if (isset(self::TYPE_TABLE_MAP[$type])) {
|
||||
$success = $this->countSuccess(self::TYPE_TABLE_MAP[$type], $from, $to, $scope_type, $scope_ids);
|
||||
}
|
||||
$failed = $this->countFailed($from, $to, $scope_type, $scope_ids, $type);
|
||||
$stats[] = [
|
||||
'data_type' => $type,
|
||||
'success' => $success,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势查询:成功数按日期分组
|
||||
*
|
||||
* @return array<string, int> date => count
|
||||
*/
|
||||
private function getTrendSuccess(
|
||||
string $trunc,
|
||||
string $from,
|
||||
string $to,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$tables = $data_type !== null && isset(self::TYPE_TABLE_MAP[$data_type])
|
||||
? [$data_type => self::TYPE_TABLE_MAP[$data_type]]
|
||||
: self::TYPE_TABLE_MAP;
|
||||
|
||||
// inventory 没有实体表,跳过
|
||||
if ($data_type === 'inventory') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$union_parts = [];
|
||||
$all_bindings = [];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
$union_parts[] = "SELECT {$trunc} as period FROM {$table} WHERE created_at BETWEEN ? AND ?{$scope_sql}";
|
||||
$all_bindings = array_merge($all_bindings, $bindings);
|
||||
}
|
||||
|
||||
if (empty($union_parts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$union_sql = implode(' UNION ALL ', $union_parts);
|
||||
$sql = "SELECT period::date::text as date, COUNT(*) as cnt FROM ({$union_sql}) sub GROUP BY period ORDER BY period";
|
||||
$rows = Db::select($sql, $all_bindings);
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[$row->date] = (int) $row->cnt;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势查询:失败数按日期分组
|
||||
*
|
||||
* @return array<string, int> date => count
|
||||
*/
|
||||
private function getTrendFailed(
|
||||
string $trunc,
|
||||
string $from,
|
||||
string $to,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
$type_sql = '';
|
||||
if ($data_type !== null) {
|
||||
$type_sql = ' AND data_type = ?';
|
||||
$bindings[] = $data_type;
|
||||
}
|
||||
|
||||
$trunc_field = str_replace('created_at', 'failed_at', $trunc);
|
||||
$sql = "SELECT {$trunc_field}::date::text as date, COUNT(*) as cnt FROM failed_messages WHERE failed_at BETWEEN ? AND ?{$scope_sql}{$type_sql} GROUP BY {$trunc_field} ORDER BY {$trunc_field}";
|
||||
$rows = Db::select($sql, $bindings);
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[$row->date] = (int) $row->cnt;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组统计:成功数按维度分组
|
||||
*
|
||||
* @return array<int, int> dimension_id => count
|
||||
*/
|
||||
private function getBreakdownSuccess(
|
||||
string $dim_col,
|
||||
string $from,
|
||||
string $to,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$tables = $data_type !== null && isset(self::TYPE_TABLE_MAP[$data_type])
|
||||
? [$data_type => self::TYPE_TABLE_MAP[$data_type]]
|
||||
: self::TYPE_TABLE_MAP;
|
||||
|
||||
if ($data_type === 'inventory') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$union_parts = [];
|
||||
$all_bindings = [];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
$union_parts[] = "SELECT {$dim_col} FROM {$table} WHERE created_at BETWEEN ? AND ?{$scope_sql}";
|
||||
$all_bindings = array_merge($all_bindings, $bindings);
|
||||
}
|
||||
|
||||
if (empty($union_parts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$union_sql = implode(' UNION ALL ', $union_parts);
|
||||
$sql = "SELECT {$dim_col} as dim_id, COUNT(*) as cnt FROM ({$union_sql}) sub GROUP BY {$dim_col}";
|
||||
$rows = Db::select($sql, $all_bindings);
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[(int) $row->dim_id] = (int) $row->cnt;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组统计:失败数按维度分组
|
||||
*
|
||||
* @return array<int, int> dimension_id => count
|
||||
*/
|
||||
private function getBreakdownFailed(
|
||||
string $dim_col,
|
||||
string $from,
|
||||
string $to,
|
||||
?string $data_type,
|
||||
string $scope_type,
|
||||
array $scope_ids,
|
||||
): array {
|
||||
$bindings = [$from, $to];
|
||||
$scope_sql = $this->buildScopeCondition($scope_type, $scope_ids, $bindings);
|
||||
$type_sql = '';
|
||||
if ($data_type !== null) {
|
||||
$type_sql = ' AND data_type = ?';
|
||||
$bindings[] = $data_type;
|
||||
}
|
||||
|
||||
$sql = "SELECT {$dim_col} as dim_id, COUNT(*) as cnt FROM failed_messages WHERE failed_at BETWEEN ? AND ?{$scope_sql}{$type_sql} GROUP BY {$dim_col}";
|
||||
$rows = Db::select($sql, $bindings);
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
if ($row->dim_id !== null) {
|
||||
$map[(int) $row->dim_id] = (int) $row->cnt;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 DataScope SQL 条件片段
|
||||
*/
|
||||
private function buildScopeCondition(string $scope_type, array $scope_ids, array &$bindings): string
|
||||
{
|
||||
if ($scope_type === 'all' || empty($scope_ids)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($scope_ids), '?'));
|
||||
|
||||
if ($scope_type === 'store') {
|
||||
$bindings = array_merge($bindings, $scope_ids);
|
||||
return " AND store_id IN ({$placeholders})";
|
||||
}
|
||||
|
||||
if ($scope_type === 'platform') {
|
||||
$bindings = array_merge($bindings, $scope_ids);
|
||||
return " AND platform_id IN ({$placeholders})";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DATE_TRUNC SQL 表达式
|
||||
*/
|
||||
private function getDateTrunc(string $group_by): string
|
||||
{
|
||||
return match ($group_by) {
|
||||
'day' => "DATE_TRUNC('day', created_at)",
|
||||
'week' => "DATE_TRUNC('week', created_at)",
|
||||
'month' => "DATE_TRUNC('month', created_at)",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user