UserLog.md 23 KB

用户日志系统 - 原时间排序版本

系统概述

用户日志系统是一个高性能的日志收集和展示系统,专门用于将各模块的原始业务日志转换为用户友好的日志消息。系统采用原时间排序设计理念,确保用户看到的日志顺序与实际业务发生的时间顺序完全一致。

核心特性

  • 原时间排序:日志按业务发生的原始时间排序,而非收集时间
  • 多模块支持:支持资金、物品、农场、积分等多个业务模块
  • 高性能收集:采用批量处理和时间线追踪,支持高频数据收集
  • 实时性保证:2秒延迟收集机制,平衡实时性和系统稳定性
  • 完整监控:提供详细的收集统计和健康检查功能

系统架构

整体架构图

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   原始日志表     │    │   收集器系统     │    │   用户日志表     │
│                │    │                │    │                │
│ fund_logs      │───▶│ FundCollector  │───▶│                │
│ item_logs      │───▶│ ItemCollector  │───▶│   user_logs    │
│ farm_logs      │───▶│ FarmCollector  │───▶│                │
│ point_logs     │───▶│ PointCollector │───▶│                │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   调度管理器     │
                    │                │
                    │ CollectorManager│
                    │ ScheduleService │
                    └─────────────────┘

核心组件

1. 收集器系统 (Collectors)

  • BaseLogCollector: 收集器基类,提供通用功能
  • FundLogCollector: 资金日志收集器
  • ItemLogCollector: 物品日志收集器
  • FarmLogCollector: 农场日志收集器
  • PointLogCollector: 积分日志收集器

2. 管理层 (Management)

  • UserLogCollectorManager: 收集器管理器,统一调度所有收集器
  • UserLogScheduleService: 调度服务,提供同步和异步执行
  • UserLogService: 对外服务接口

3. 数据层 (Data)

  • UserLog: 用户日志模型
  • UserLogLogic: 日志业务逻辑
  • 用户日志表: 统一的日志存储表

4. 控制层 (Control)

  • CollectUserLogsCommand: 命令行工具
  • UserLogController: 后台管理界面
  • LogDataHandler: 前端接口处理

原时间排序实现

设计理念

传统的日志系统通常按收集时间排序,这会导致用户看到的日志顺序与实际业务发生顺序不一致。本系统采用原时间排序机制,确保:

  1. 时间一致性:日志显示顺序 = 业务发生顺序
  2. 跨模块同步:不同模块的日志能够按统一时间线排序
  3. 数据完整性:基于ID进度追踪,确保不遗漏任何记录

实现机制

1. 双时间戳设计

// 收集时同时记录原始时间和收集时间
$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(),                   // 兼容字段
];

2. ID进度追踪

每个收集器基于自增ID维护进度,确保数据完整性:

// 获取最后处理的记录ID
$lastProcessedId = $this->getLastProcessedId();

// 按ID顺序获取新记录
$records = $sourceModel::where('id', '>', $lastProcessedId)
    ->orderBy('id')
    ->limit($this->maxRecords)
    ->get();

3. 原时间排序

收集到的记录按原始时间排序后批量保存:

// 按原始时间排序
usort($userLogs, function($a, $b) {
    return strtotime($a['original_time']) <=> strtotime($b['original_time']);
});

// 批量保存
UserLogService::batchLog($userLogs);

进度追踪优势

基于ID vs 基于时间

特性 基于ID追踪 基于时间追踪
数据完整性 ✅ 保证不遗漏 ❌ 可能遗漏相同时间戳的记录
时钟容错 ✅ 不受时钟调整影响 ❌ 受服务器时间影响
并发安全 ✅ 自增ID唯一 ❌ 并发时时间戳可能重复
恢复能力 ✅ 可精确断点续传 ❌ 时间重叠可能重复处理

收集流程详解

主流程

启动脚本 → 遍历所有收集器 → 执行收集 → 输出结果
    │              │              │         │
    │              │              │         └─ 统计信息
    │              │              │         └─ 错误报告
    │              │              │         └─ 性能指标
    │              │              │
    │              │              └─ 批量写入user_logs
    │              │              └─ 更新进度追踪
    │              │
    │              └─ 获取收集进度
    │              └─ 读取原始日志
    │              └─ 转换消息格式
    │
    └─ 初始化收集器
    └─ 检查系统状态

单个收集器流程

1. 进度确认阶段

// 从user_logs表获取本收集器最新记录的时间戳
$lastTimestamp = UserLog::where('source_type', $this->sourceType)
    ->where('source_table', $this->sourceTable)
    ->max('created_at');

2. 数据获取阶段

// 获取未收集的记录(延迟2秒 + 限制数量)
$cutoffTime = now()->subSeconds(2);
$records = $this->getNewRecordsByTime($lastTimestamp)
    ->where('created_at', '<=', $cutoffTime)
    ->limit($this->maxRecords);

3. 消息转换阶段

foreach ($records as $record) {
    $userLogData = $this->convertToUserLog($record);
    if ($userLogData) {
        $userLogs[] = $userLogData;
    }
}

4. 批量保存阶段

if (!empty($userLogs)) {
    UserLogService::batchLog($userLogs);
}

配置系统

主配置文件 (config/game_user_log.php)

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

数据库设计

用户日志表 (kku_user_logs)

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='用户日志表';

索引设计说明

  1. 主键索引 (id): 自增主键,保证唯一性
  2. 用户索引 (user_id): 按用户查询日志的核心索引
  3. 原始时间索引 (original_time): 支持按业务发生时间查询和排序
  4. 收集时间索引 (collected_at): 支持按收集时间查询和监控
  5. 兼容时间索引 (created_at): 保持向后兼容性
  6. 来源索引 (source_type, source_id): 支持按来源类型和ID查询
  7. 复合索引 (user_id, original_time): 优化用户日志按原始时间排序查询
  8. 复合索引 (user_id, collected_at): 优化按收集时间的查询

字段说明

  • original_time: 存储业务发生的原始时间,用于排序和展示
  • collected_at: 存储日志收集时间,用于监控和追踪收集进度
  • created_at: 兼容字段,等同于collected_at,保持向后兼容性
  • source_type: 来源模块类型,如 fund、item、farm、point
  • source_table: 原始日志表名,便于追溯和调试
  • message: 用户友好的日志消息,已经过格式化处理

时间字段设计理念

双时间戳设计

系统采用双时间戳设计,分别记录:

  1. 原始时间 (original_time): 业务实际发生的时间

    • 用于用户界面展示和排序
    • 确保日志顺序与业务发生顺序一致
    • 支持跨模块的统一时间线
  2. 收集时间 (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

命令选项详解

--detail 详细模式

显示每个收集器的详细执行过程:

🚀 开始按时间线收集用户日志...
📊 执行各收集器的日志收集...
  fund: 处理了 45 条记录
  item: 处理了 23 条记录
  farm: 处理了 12 条记录
  point: 处理了 8 条记录
✅ 收集完成,总共处理了 88 条记录,耗时 234.56ms

--info 信息模式

显示所有收集器的状态信息:

📋 收集器信息:
  💰 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)

--statistics 统计模式

显示系统统计信息:

📊 用户日志统计:
  📝 总日志数: 12,345
  📊 按类型统计:
    💰 资金日志: 5,678
    📦 物品日志: 3,456
    🌾 农场日志: 2,345
    ⭐ 积分日志: 866

进度管理

系统采用基于已处理记录的进度追踪机制:

  • 进度通过user_logs表中的记录自动追踪
  • 每个收集器会查找最后处理的记录,从该位置继续收集
  • 无需手动重置,系统会自动从上次中断的位置继续
  • 如需重新收集,可直接删除user_logs表中对应的记录

计划任务配置

Laravel 调度器配置

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 配置

如果需要更高频率的收集,可以直接配置 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'
]);

API 接口

前端日志查询接口

LogDataHandler

处理前端的日志查询请求:

// 请求参数
[
    '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;
}

ClearLogHandler

处理用户清空日志请求:

// 请求:空
// 响应:标准成功响应
// 注意:实际不删除数据,只记录清理时间

后台管理接口

UserLogController

提供完整的后台管理功能:

  • 列表查询:支持按用户、类型、时间范围筛选
  • 详情查看:查看单条日志的详细信息
  • 批量操作:批量查看和分析(不支持删除)
  • 统计分析:提供各种统计图表

扩展开发

添加新的收集器

1. 创建收集器类

<?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()
        );
    }
}

2. 注册收集器

UserLogCollectorManager 中注册:

private function registerCollectors(): void
{
    $this->collectors = [
        'fund' => new FundLogCollector(),
        'item' => new ItemLogCollector(),
        'farm' => new FarmLogCollector(),
        'point' => new PointLogCollector(),
        'custom' => new CustomLogCollector(), // 新增
    ];
}

3. 添加配置

config/game_user_log.php 中添加配置:

'collectors' => [
    // ...
    'custom' => [
        'enabled' => env('GAME_USER_LOG_CUSTOM_ENABLED', true),
    ],
],

自定义消息模板

1. 配置模板

'message_templates' => [
    'custom' => [
        'action1' => '执行了{action_name}操作',
        'action2' => '完成了{task_name}任务,获得{reward}奖励',
    ],
],

2. 使用模板

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()
    );
}

性能优化

数据库优化

1. 索引优化

  • 确保所有查询都有对应的索引
  • 定期分析慢查询并优化
  • 考虑分区表设计(按时间分区)

2. 查询优化

// 使用复合索引优化用户日志查询
UserLog::where('user_id', $userId)
    ->where('created_at', '>=', $startTime)
    ->orderBy('created_at', 'desc')
    ->limit(20)
    ->get();

3. 批量处理优化

// 使用批量插入减少数据库连接
UserLog::insert($userLogs); // 而不是逐条插入

内存优化

1. 分批处理

// 避免一次性加载大量数据
$records->chunk(100, function ($chunk) {
    $this->processChunk($chunk);
});

2. 及时释放内存

unset($records, $userLogs); // 处理完成后释放变量

缓存优化

1. 进度缓存

// 缓存收集进度,减少数据库查询
Cache::remember("collector_progress:{$this->sourceType}", 300, function () {
    return $this->getLastProcessedTimestamp();
});

2. 配置缓存

// 缓存配置信息
$config = Cache::remember('game_user_log_config', 3600, function () {
    return config('game_user_log');
});

故障排除

常见问题

1. 收集器不工作

症状:命令执行但没有新日志生成

排查步骤

# 检查收集器状态
php artisan game:collect-user-logs --info

# 检查原始日志表是否有新数据
# 检查配置是否正确
# 检查数据库连接是否正常

2. 时间顺序错误

症状:用户日志时间顺序混乱

排查步骤

  • 检查 created_at 字段是否使用原始时间
  • 检查时区设置是否正确
  • 检查延迟收集是否生效

3. 性能问题

症状:收集速度慢,影响系统性能

优化方案

  • 调整 max_records_per_run 参数
  • 启用队列异步处理
  • 优化数据库索引
  • 增加服务器资源

4. 内存溢出

症状:收集过程中出现内存不足错误

解决方案

// 减少批量处理大小
'batch_size' => 50, // 从100减少到50

// 增加内存限制
ini_set('memory_limit', '512M');

// 使用分块处理
$records->chunk(50, function ($chunk) {
    // 处理逻辑
});

调试工具

1. 详细日志

# 启用详细模式查看执行过程
php artisan game:collect-user-logs --detail

2. 性能分析

// 在收集器中添加性能监控
$startTime = microtime(true);
// 执行收集逻辑
$endTime = microtime(true);
Log::info("收集器性能", [
    'collector' => $this->collectorName,
    'execution_time' => ($endTime - $startTime) * 1000,
    'processed_count' => $processedCount
]);

3. 数据验证

# 验证数据一致性
SELECT source_type, COUNT(*) FROM kku_user_logs GROUP BY source_type;

# 检查时间范围
SELECT MIN(created_at), MAX(created_at) FROM kku_user_logs;

总结

用户日志系统采用原时间排序设计,确保用户看到的日志顺序与业务发生顺序完全一致。系统具有以下优势:

核心优势

  1. 时间一致性:日志按业务原始时间排序,保证时序正确性
  2. 高性能:批量处理 + 时间线追踪,支持高频数据收集
  3. 模块化设计:收集器独立运行,易于扩展和维护
  4. 完整监控:提供详细的统计和健康检查功能
  5. 灵活配置:支持多种配置方式和环境变量

技术特点

  • 延迟收集:2秒延迟机制,平衡实时性和稳定性
  • 进度追踪:基于时间戳的进度管理,支持断点续传
  • 批量处理:减少数据库连接,提升处理效率
  • 错误恢复:完善的异常处理和错误恢复机制

适用场景

  • 游戏用户行为日志收集
  • 业务操作审计日志
  • 多模块统一日志展示
  • 实时数据分析和监控

通过合理的配置和部署,本系统能够稳定高效地处理大量用户日志数据,为业务分析和用户体验提供强有力的支持。