用户日志系统是一个高性能的日志收集和展示系统,专门用于将各模块的原始业务日志转换为用户友好的日志消息。系统采用原时间排序设计理念,确保用户看到的日志顺序与实际业务发生的时间顺序完全一致。
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 原始日志表 │ │ 收集器系统 │ │ 用户日志表 │
│ │ │ │ │ │
│ 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);
}
用户日志系统的配置已迁移到数据库,存储在 kku_game_configs 表中,支持运行时动态修改。
| 配置键 | 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
user_log.enabled |
用户日志系统启用 | 布尔值 | true | 控制整个用户日志系统是否启用 |
user_log.auto_collect_enabled |
自动收集日志启用 | 布尔值 | true | 控制是否允许自动收集用户日志 |
user_log.max_records_per_run |
单次最大处理记录数 | 整数 | 1000 | 每次收集日志时的最大处理记录数 |
user_log.collection_interval |
收集间隔 | 整数 | 2 | 日志收集的间隔时间(秒) |
user_log.retention_days |
日志保留天数 | 整数 | 30 | 用户日志的保留天数 |
user_log.auto_cleanup |
自动清理启用 | 布尔值 | true | 是否启用自动清理过期日志 |
use App\Module\Game\Services\GameConfigService;
// 获取完整的用户日志配置
$config = GameConfigService::getUserLogConfig();
// 配置结构示例
[
'enabled' => true,
'auto_collect_enabled' => true,
'max_records_per_run' => 1000,
'collection_interval' => 2,
'retention_days' => 30,
'auto_cleanup' => true,
]
可以通过后台管理界面修改配置:
game-system-configsCREATE 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');
注意:默认情况下计划任务已被注释,需要根据实际需求启用。系统通过数据库配置 user_log.auto_collect_enabled 控制是否允许自动收集。
如果需要更高频率的收集,可以直接配置 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 game:collect-user-logs
# 通过计划任务定时执行
# 在 routes/console.php 中配置调度任务
系统提供基本的健康检查功能:
# 检查收集器状态
php artisan game:collect-user-logs --info
# 查看统计信息
php artisan game:collect-user-logs --statistics
通过命令行工具可以检查:
# 查看收集统计信息
php artisan game:collect-user-logs --statistics
# 输出示例
📊 收集器统计信息:
收集器数量: 5
- fund: ⚙️ 资金日志收集器
- item: ⚙️ 物品日志收集器
- farm_harvest: 🌾 农场收获日志收集器
- farm_upgrade: 🌾 农场升级日志收集器
- point: ⭐ 积分日志收集器
📋 用户日志表统计:
📝 总日志数: 119163
🕐 最新日志时间: 2025-06-22 23:35:56
🕐 最旧日志时间: 2025-06-21 16:30:12
每次收集都会记录详细的执行时间:
[
'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(), // 新增
];
}
在数据库中添加配置项:
INSERT INTO `kku_game_configs` (`key`, `name`, `description`, `group`, `type`, `value`, `default_value`, `sort_order`, `remark`) VALUES
('user_log.custom_enabled', '自定义收集器启用', '控制自定义收集器是否启用', 'user_log', 1, '1', '1', 70, '');
或通过后台管理界面添加配置项。
'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();
});
// 配置自动缓存,通过GameConfigService访问
$config = GameConfigService::getUserLogConfig();
// 手动清除配置缓存(如果需要)
GameConfigLogic::clearCache('user_log.enabled');
为了保持系统整洁,以下组件已被移除:
config/game_user_log.php - 配置已迁移到数据库UserLogScheduleService - 未被实际使用,功能已由命令行工具替代CollectUserLogJob - 异步队列任务,未被实际使用Request_RequestUserLogData - 已被新命名空间替代Request_RequestUserClearLog - 已被新命名空间替代如果您的代码中引用了上述废弃组件,请按以下方式迁移:
GameConfigService::getUserLogConfig() 替代文件配置CollectUserLogsCommand 或 UserLogCollectorManagerUserLogCollectorManager症状:命令执行但没有新日志生成
排查步骤:
# 检查收集器状态
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;
用户日志系统采用原时间排序设计,确保用户看到的日志顺序与业务发生顺序完全一致。系统具有以下优势:
通过合理的配置和部署,本系统能够稳定高效地处理大量用户日志数据,为业务分析和用户体验提供强有力的支持。