用户日志系统是一个高性能的日志收集和展示系统,专门用于将各模块的原始业务日志转换为用户友好的日志消息。系统采用原时间排序设计理念,确保用户看到的日志顺序与实际业务发生的时间顺序完全一致。
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 原始日志表 │ │ 收集器系统 │ │ 用户日志表 │
│ │ │ │ │ │
│ fund_logs │───▶│ FundCollector │───▶│ │
│ item_logs │───▶│ ItemCollector │───▶│ user_logs │
│ farm_logs │───▶│ FarmCollector │───▶│ │
│ point_logs │───▶│ PointCollector │───▶│ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 调度管理器 │
│ │
│ CollectorManager│
│ ScheduleService │
└─────────────────┘
传统的日志系统通常按收集时间排序,这会导致用户看到的日志顺序与实际业务发生顺序不一致。本系统采用原时间排序机制,确保:
// 收集时同时记录原始时间和收集时间
$userLogData = [
'user_id' => $record->user_id,
'message' => $this->generateMessage($record),
'source_type' => $this->sourceType,
'source_id' => $record->id,
'source_table' => $this->sourceTable,
'original_time' => $record->created_at, // 原始业务时间
'collected_at' => now(), // 收集时间
'created_at' => now(), // 兼容字段
];
每个收集器基于自增ID维护进度,确保数据完整性:
// 获取最后处理的记录ID
$lastProcessedId = $this->getLastProcessedId();
// 按ID顺序获取新记录
$records = $sourceModel::where('id', '>', $lastProcessedId)
->orderBy('id')
->limit($this->maxRecords)
->get();
收集到的记录按原始时间排序后批量保存:
// 按原始时间排序
usort($userLogs, function($a, $b) {
return strtotime($a['original_time']) <=> strtotime($b['original_time']);
});
// 批量保存
UserLogService::batchLog($userLogs);
| 特性 | 基于ID追踪 | 基于时间追踪 |
|---|---|---|
| 数据完整性 | ✅ 保证不遗漏 | ❌ 可能遗漏相同时间戳的记录 |
| 时钟容错 | ✅ 不受时钟调整影响 | ❌ 受服务器时间影响 |
| 并发安全 | ✅ 自增ID唯一 | ❌ 并发时时间戳可能重复 |
| 恢复能力 | ✅ 可精确断点续传 | ❌ 时间重叠可能重复处理 |
启动脚本 → 遍历所有收集器 → 执行收集 → 输出结果
│ │ │ │
│ │ │ └─ 统计信息
│ │ │ └─ 错误报告
│ │ │ └─ 性能指标
│ │ │
│ │ └─ 批量写入user_logs
│ │ └─ 更新进度追踪
│ │
│ └─ 获取收集进度
│ └─ 读取原始日志
│ └─ 转换消息格式
│
└─ 初始化收集器
└─ 检查系统状态
// 从user_logs表获取本收集器最新记录的时间戳
$lastTimestamp = UserLog::where('source_type', $this->sourceType)
->where('source_table', $this->sourceTable)
->max('created_at');
// 获取未收集的记录(延迟2秒 + 限制数量)
$cutoffTime = now()->subSeconds(2);
$records = $this->getNewRecordsByTime($lastTimestamp)
->where('created_at', '<=', $cutoffTime)
->limit($this->maxRecords);
foreach ($records as $record) {
$userLogData = $this->convertToUserLog($record);
if ($userLogData) {
$userLogs[] = $userLogData;
}
}
if (!empty($userLogs)) {
UserLogService::batchLog($userLogs);
}
return [
// 全局开关
'enabled' => env('GAME_USER_LOG_ENABLED', true),
// 收集器配置
'collectors' => [
'max_records_per_run' => 1000, // 单次最大处理数
'collection_interval' => 2, // 收集间隔(秒)
// 各模块开关
'fund' => ['enabled' => true],
'item' => ['enabled' => true],
'farm' => ['enabled' => true],
'point' => ['enabled' => true],
],
// 性能配置
'performance' => [
'batch_size' => 100, // 批量处理大小
'use_queue' => true, // 是否使用队列
'cache_ttl' => 86400, // 缓存TTL
],
// 清理配置
'cleanup' => [
'retention_days' => 30, // 保留天数
'auto_cleanup' => true, // 自动清理
],
];
# 用户日志系统配置
GAME_USER_LOG_ENABLED=true
GAME_USER_LOG_MAX_RECORDS=1000
GAME_USER_LOG_INTERVAL=2
GAME_USER_LOG_RETENTION_DAYS=30
# 各模块开关
GAME_USER_LOG_FUND_ENABLED=true
GAME_USER_LOG_ITEM_ENABLED=true
GAME_USER_LOG_FARM_ENABLED=true
GAME_USER_LOG_POINT_ENABLED=true
# 性能配置
GAME_USER_LOG_BATCH_SIZE=100
GAME_USER_LOG_USE_QUEUE=true
GAME_USER_LOG_CACHE_TTL=86400
CREATE TABLE `kku_user_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`message` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '日志消息内容',
`source_type` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源类型(fund, item, farm等)',
`source_id` int DEFAULT NULL COMMENT '来源记录ID',
`source_table` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源表名',
`original_time` timestamp NULL DEFAULT NULL COMMENT '原始日志时间(业务发生时间)',
`collected_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收集时间(日志收集时间)',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(兼容字段,等同于collected_at)',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_original_time` (`original_time`),
KEY `idx_collected_at` (`collected_at`),
KEY `idx_created_at` (`created_at`),
KEY `idx_source` (`source_type`,`source_id`),
KEY `idx_user_original_time` (`user_id`, `original_time`),
KEY `idx_user_collected_at` (`user_id`, `collected_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户日志表';
id): 自增主键,保证唯一性user_id): 按用户查询日志的核心索引original_time): 支持按业务发生时间查询和排序collected_at): 支持按收集时间查询和监控created_at): 保持向后兼容性source_type, source_id): 支持按来源类型和ID查询user_id, original_time): 优化用户日志按原始时间排序查询user_id, collected_at): 优化按收集时间的查询系统采用双时间戳设计,分别记录:
原始时间 (original_time): 业务实际发生的时间
收集时间 (collected_at): 日志被收集系统处理的时间
// 收集器创建日志时的时间设置
$userLogData = [
'original_time' => $record->created_at, // 使用原始业务时间
'collected_at' => now(), // 使用当前收集时间
'created_at' => now(), // 兼容字段
];
# 执行日志收集
php artisan game:collect-user-logs
# 显示详细处理过程
php artisan game:collect-user-logs --detail
# 限制单次处理记录数
php artisan game:collect-user-logs --limit=500
# 显示收集器信息
php artisan game:collect-user-logs --info
# 显示统计信息
php artisan game:collect-user-logs --statistics
显示每个收集器的详细执行过程:
🚀 开始按时间线收集用户日志...
📊 执行各收集器的日志收集...
fund: 处理了 45 条记录
item: 处理了 23 条记录
farm: 处理了 12 条记录
point: 处理了 8 条记录
✅ 收集完成,总共处理了 88 条记录,耗时 234.56ms
显示所有收集器的状态信息:
📋 收集器信息:
💰 fund: FundLogCollector (fund_logs → user_logs)
📦 item: ItemLogCollector (item_transaction_logs → user_logs)
🌾 farm: FarmLogCollector (farm_harvest_logs, farm_upgrade_logs → user_logs)
⭐ point: PointLogCollector (point_logs → user_logs)
显示系统统计信息:
📊 用户日志统计:
📝 总日志数: 12,345
📊 按类型统计:
💰 资金日志: 5,678
📦 物品日志: 3,456
🌾 农场日志: 2,345
⭐ 积分日志: 866
系统采用基于已处理记录的进度追踪机制:
user_logs表中的记录自动追踪user_logs表中对应的记录在 routes/console.php 中添加:
use Illuminate\Support\Facades\Schedule;
// 每分钟执行用户日志收集(高频收集)
Schedule::command('game:collect-user-logs --limit=100')->everyMinute();
// 每天凌晨2点清理过期日志
Schedule::command('game:clean-expired-user-logs')->dailyAt('02:00');
如果需要更高频率的收集,可以直接配置 crontab:
# 每30秒执行一次(需要注意服务器负载)
* * * * * cd /path/to/project && php artisan game:collect-user-logs --limit=50 >/dev/null 2>&1
* * * * * sleep 30; cd /path/to/project && php artisan game:collect-user-logs --limit=50 >/dev/null 2>&1
如果启用了队列处理,需要运行队列处理器:
# 启动队列处理器
php artisan queue:work --queue=default --timeout=60
# 使用 Supervisor 管理队列处理器
[program:laravel-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/project/artisan queue:work --queue=default --timeout=60
directory=/path/to/project
autostart=true
autorestart=true
user=www-data
numprocs=2
系统提供完整的健康检查功能:
// 通过服务调用
$health = UserLogScheduleService::healthCheck();
// 返回结果示例
[
'status' => 'healthy',
'checks' => [
'collectors_registered' => [
'status' => 'pass',
'count' => 4,
'details' => ['fund', 'item', 'farm', 'point']
],
'recent_logs' => [
'status' => 'info',
'count' => 156,
'message' => '最近10分钟生成了 156 条日志'
],
'database' => [
'status' => 'pass',
'message' => '数据库连接正常'
]
],
'timestamp' => '2025-06-13 11:16:04'
]
// 获取收集统计信息
$stats = UserLogScheduleService::getCollectionStats(7); // 最近7天
// 返回结果
[
'period' => '7天',
'start_date' => '2025-06-06',
'end_date' => '2025-06-13',
'total_logs' => 45678,
'daily_stats' => [
'2025-06-06' => 6543,
'2025-06-07' => 7234,
// ...
],
'source_type_stats' => [
'fund' => 18765,
'item' => 12456,
'farm' => 8765,
'point' => 5692
]
]
每次收集都会记录详细的执行时间:
[
'total_processed' => 88,
'total_execution_time' => 234.56, // 毫秒
'collectors' => [
'fund' => [
'processed_count' => 45,
'execution_time' => 123.45,
'status' => 'success'
],
// ...
]
]
系统会自动记录所有收集过程中的错误:
// 错误日志示例
Log::error("日志收集失败", [
'collector' => 'FundLogCollector',
'error' => 'Database connection timeout',
'trace' => '...',
'timestamp' => '2025-06-13 11:16:04'
]);
处理前端的日志查询请求:
// 请求参数
[
'page' => 1, // 页码
'page_size' => 20, // 每页数量
'source_type' => '', // 来源类型过滤
'start_time' => '', // 开始时间
'end_time' => '' // 结束时间
]
// 响应格式(protobuf)
message LogDataResponse {
repeated LogItem logs = 1;
int32 total = 2;
int32 page = 3;
int32 page_size = 4;
}
message LogItem {
int64 id = 1;
string message = 2;
string time = 3; // 格式化时间 "MM-dd HH:mm"
string source_type = 4;
}
处理用户清空日志请求:
// 请求:空
// 响应:标准成功响应
// 注意:实际不删除数据,只记录清理时间
提供完整的后台管理功能:
<?php
namespace App\Module\Game\Logics\UserLogCollectors;
class CustomLogCollector extends BaseLogCollector
{
protected string $sourceTable = 'custom_logs';
protected string $sourceType = 'custom';
protected function getNewRecords(int $lastProcessedId)
{
return CustomLog::where('id', '>', $lastProcessedId)
->orderBy('id')
->limit($this->maxRecords)
->get();
}
protected function getNewRecordsByTime(int $lastProcessedTimestamp)
{
$cutoffTime = now()->subSeconds(2);
return CustomLog::where('created_at', '>', date('Y-m-d H:i:s', $lastProcessedTimestamp))
->where('created_at', '<=', $cutoffTime)
->orderBy('created_at')
->orderBy('id')
->limit($this->maxRecords)
->get();
}
protected function getRecordTimestamp($record): int
{
return $record->created_at->timestamp;
}
protected function convertToUserLog($record): ?array
{
// 转换逻辑
return $this->createUserLogData(
$record->user_id,
"自定义操作:{$record->action}",
$record->id,
$record->created_at->toDateTimeString()
);
}
}
在 UserLogCollectorManager 中注册:
private function registerCollectors(): void
{
$this->collectors = [
'fund' => new FundLogCollector(),
'item' => new ItemLogCollector(),
'farm' => new FarmLogCollector(),
'point' => new PointLogCollector(),
'custom' => new CustomLogCollector(), // 新增
];
}
在 config/game_user_log.php 中添加配置:
'collectors' => [
// ...
'custom' => [
'enabled' => env('GAME_USER_LOG_CUSTOM_ENABLED', true),
],
],
'message_templates' => [
'custom' => [
'action1' => '执行了{action_name}操作',
'action2' => '完成了{task_name}任务,获得{reward}奖励',
],
],
protected function convertToUserLog($record): ?array
{
$template = config('game_user_log.message_templates.custom.' . $record->action_type);
$message = str_replace(
['{action_name}', '{task_name}', '{reward}'],
[$record->action_name, $record->task_name, $record->reward],
$template
);
return $this->createUserLogData(
$record->user_id,
$message,
$record->id,
$record->created_at->toDateTimeString()
);
}
// 使用复合索引优化用户日志查询
UserLog::where('user_id', $userId)
->where('created_at', '>=', $startTime)
->orderBy('created_at', 'desc')
->limit(20)
->get();
// 使用批量插入减少数据库连接
UserLog::insert($userLogs); // 而不是逐条插入
// 避免一次性加载大量数据
$records->chunk(100, function ($chunk) {
$this->processChunk($chunk);
});
unset($records, $userLogs); // 处理完成后释放变量
// 缓存收集进度,减少数据库查询
Cache::remember("collector_progress:{$this->sourceType}", 300, function () {
return $this->getLastProcessedTimestamp();
});
// 缓存配置信息
$config = Cache::remember('game_user_log_config', 3600, function () {
return config('game_user_log');
});
症状:命令执行但没有新日志生成
排查步骤:
# 检查收集器状态
php artisan game:collect-user-logs --info
# 检查原始日志表是否有新数据
# 检查配置是否正确
# 检查数据库连接是否正常
症状:用户日志时间顺序混乱
排查步骤:
created_at 字段是否使用原始时间症状:收集速度慢,影响系统性能
优化方案:
max_records_per_run 参数症状:收集过程中出现内存不足错误
解决方案:
// 减少批量处理大小
'batch_size' => 50, // 从100减少到50
// 增加内存限制
ini_set('memory_limit', '512M');
// 使用分块处理
$records->chunk(50, function ($chunk) {
// 处理逻辑
});
# 启用详细模式查看执行过程
php artisan game:collect-user-logs --detail
// 在收集器中添加性能监控
$startTime = microtime(true);
// 执行收集逻辑
$endTime = microtime(true);
Log::info("收集器性能", [
'collector' => $this->collectorName,
'execution_time' => ($endTime - $startTime) * 1000,
'processed_count' => $processedCount
]);
# 验证数据一致性
SELECT source_type, COUNT(*) FROM kku_user_logs GROUP BY source_type;
# 检查时间范围
SELECT MIN(created_at), MAX(created_at) FROM kku_user_logs;
用户日志系统采用原时间排序设计,确保用户看到的日志顺序与业务发生顺序完全一致。系统具有以下优势:
通过合理的配置和部署,本系统能够稳定高效地处理大量用户日志数据,为业务分析和用户体验提供强有力的支持。