maxRecords = $maxRecords; } /** * 构造函数 */ public function __construct() { $this->collectorName = static::class; } /** * 收集日志 * * @return int 处理的记录数 */ public function collect(): int { try { // 添加执行时间限制,防止死循环 $startTime = time(); $maxExecutionTime = 30; // 最大执行30秒 Log::info("开始收集日志", [ 'collector' => $this->collectorName, 'start_time' => date('Y-m-d H:i:s', $startTime) ]); $result = $this->collectById(); $endTime = time(); $executionTime = $endTime - $startTime; Log::info("日志收集完成", [ 'collector' => $this->collectorName, 'processed_count' => $result, 'execution_time' => $executionTime . 's' ]); if ($executionTime > $maxExecutionTime) { Log::warning("日志收集执行时间过长", [ 'collector' => $this->collectorName, 'execution_time' => $executionTime . 's', 'max_time' => $maxExecutionTime . 's' ]); } return $result; } catch (\Exception $e) { Log::error("日志收集失败", [ 'collector' => $this->collectorName, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return 0; } } /** * 按ID收集日志(推荐方式) * * @return int 处理的记录数 */ private function collectById(): int { $lastProcessedId = $this->getLastProcessedId(); $records = $this->getNewRecords($lastProcessedId); if ($records->isEmpty()) { return 0; } $userLogs = []; $maxId = 0; foreach ($records as $record) { // 检查是否已经收集过此记录,避免重复收集 if ($this->isDuplicateRecord($this->sourceTable, $record->id)) { $maxId = max($maxId, $record->id); continue; } $userLogData = $this->convertToUserLog($record); if ($userLogData) { $userLogs[] = $userLogData; $maxId = max($maxId, $record->id); } } if (!empty($userLogs)) { // 按原始时间排序后批量保存 usort($userLogs, function($a, $b) { return strtotime($a['original_time']) <=> strtotime($b['original_time']); }); $batchResult = UserLogService::batchLog($userLogs); if (!$batchResult) { Log::error("批量保存用户日志失败", [ 'collector' => $this->collectorName, 'logs_count' => count($userLogs), 'max_id' => $maxId ]); return 0; // 保存失败时返回0,避免更新进度 } } $processedCount = count($userLogs); Log::info("ID日志收集完成", [ 'collector' => $this->collectorName, 'processed_count' => $processedCount, 'last_id' => $maxId, 'records_found' => $records->count() ]); return $processedCount; } /** * 获取新的记录(子类实现) * * @param int $lastProcessedId 上次处理的最大ID * @return \Illuminate\Database\Eloquent\Collection */ abstract protected function getNewRecords(int $lastProcessedId); /** * 公共方法:转换记录为用户日志数据 * * @param mixed $record 原始记录 * @return array|null 用户日志数据,null表示跳过 */ public function convertToUserLogPublic($record): ?array { return $this->convertToUserLog($record); } /** * 转换记录为用户日志数据(子类实现) * * @param mixed $record 原始记录 * @return array|null 用户日志数据,null表示跳过 */ abstract protected function convertToUserLog($record): ?array; /** * 获取上次处理的最大ID * 从user_logs表中查询该收集器最后处理的记录ID * * 注意:对于同一个source_table可能有多个source_type的情况(如FundLogCollector), * 需要查询该表的所有记录的最大source_id,而不是仅查询当前source_type的记录 * * @return int */ protected function getLastProcessedId(): int { try { // 查询该source_table的所有记录的最大source_id // 这样可以避免因为动态source_type导致的重复收集问题 $maxId = \App\Module\Game\Models\UserLog::where('source_table', $this->sourceTable) ->max('source_id'); return $maxId ?: 0; } catch (\Exception $e) { Log::error("获取最后处理ID失败", [ 'collector' => $this->collectorName, 'source_table' => $this->sourceTable, 'error' => $e->getMessage() ]); return 0; } } /** * 创建用户日志数据数组 * * @param int $userId 用户ID * @param string $message 日志消息 * @param int $sourceId 来源记录ID * @param string|null $originalTime 原始时间(业务发生时间),null则使用当前时间 * @return array */ protected function createUserLogData(int $userId, string $message, int $sourceId, ?string $originalTime = null): array { $now = now()->toDateTimeString(); $originalTime = $originalTime ?? $now; return [ 'user_id' => $userId, 'message' => $message, 'source_type' => $this->sourceType, 'source_id' => $sourceId, 'source_table' => $this->sourceTable, 'original_time' => $originalTime, // 原始业务时间 'collected_at' => $now, // 收集时间 'created_at' => $now, // 兼容字段 ]; } /** * 获取收集器名称 * * @return string */ public function getCollectorName(): string { return $this->collectorName; } /** * 获取源表名 * * @return string */ public function getSourceTable(): string { return $this->sourceTable; } /** * 获取源类型 * * @return string */ public function getSourceType(): string { return $this->sourceType; } /** * 获取源表的最大ID * * 子类可以重写此方法以提供更高效的实现 * 默认使用通用的DB查询方式 * * @return int */ public function getSourceTableMaxId(): int { try { // 使用通用的DB查询方式作为默认实现 $result = \Illuminate\Support\Facades\DB::table($this->sourceTable)->max('id'); return $result ? (int)$result : 0; } catch (\Exception $e) { Log::error("获取源表最大ID失败", [ 'collector' => $this->collectorName, 'source_table' => $this->sourceTable, 'error' => $e->getMessage() ]); return 0; } } /** * 检查是否为重复记录 * * 注意:不使用source_type进行检查,因为同一个source_table的记录可能有不同的source_type * 只要source_table和source_id相同,就认为是重复记录 * * @param string $sourceTable 源表名 * @param int $sourceId 源记录ID * @return bool */ protected function isDuplicateRecord(string $sourceTable, int $sourceId): bool { try { $exists = \App\Module\Game\Models\UserLog::where('source_table', $sourceTable) ->where('source_id', $sourceId) ->exists(); return $exists; } catch (\Exception $e) { Log::error("检查重复记录失败", [ 'collector' => $this->collectorName, 'source_table' => $sourceTable, 'source_id' => $sourceId, 'error' => $e->getMessage() ]); return false; } } }