实体表映射 */ 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 */ 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 */ 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 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 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 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 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)", }; } }