代码如下:

// ***********************************************************************************
// FileName: LogHandlerRotateComposite.cs
// Description:
// 
// Version: v1.0.0
// Creator: Jacky(jackylvm@foxmail.com)
// CreationTime: 2025-09-14 14:41:47
// ==============================================
// History update record:
// 
// ==============================================
// *************************************************************************************

using System;
using System.IO;
using System.Linq;
using UnityEngine;

namespace JackyExtendedToolSet.Runtime.Logger
{
    // 日志文件轮转
    // 结合文件大小和文件时间进行轮转
    public class LogHandlerRotateComposite : MonoBehaviour
    {
        // 每个日志文件的最大大小(64MB)
        [SerializeField] private long maxLogFileSize = 64 * 1024 * 1024;

        // 最大日志文件数量
        [SerializeField] private int maxLogFileCount = 5;

        // 日志文件保存路径
        [SerializeField] private string logDirectory = "Logs";

        // 日志文件前缀
        [SerializeField] private string logFilePrefix = "GameLog_";

        private string _logFilePath;
        private StreamWriter _currentLogWriter;
        private string _currentLogFilePath;

        /// <summary>
        /// Awake 函数会在游戏对象被实例化之后立即调用,而且在所有对象的 Start 函数调用之前执行。一般在 Awake 函数里进行以下操作:
        /// <para>1. 初始化引用:查找并存储对其他组件或者游戏对象的引用,确保后续代码能够方便地访问这些对象。</para>
        /// <para>2. 初始化设置:对脚本中的变量进行初始赋值。</para>
        /// <para>3. 建立连接:比如和数据库或者网络服务建立连接。</para>
        /// </summary>
        private void Awake()
        {
            _logFilePath = Path.Combine(Application.persistentDataPath, logDirectory);

            // 确保日志目录存在
            EnsureLogDirectoryExists();

            // 清理超出数量限制的旧日志
            CleanupOldLogs();

            // 初始化日志文件(使用现有最新的或创建新的)
            InitializeLogFile();

            DontDestroyOnLoad(gameObject);
        }

        /// <summary>
        /// Start 函数会在脚本实例被启用时调用,并且只在第一帧更新之前执行一次。它一般用于:
        /// <para>1. 依赖初始化:当某些初始化操作依赖于其他对象的 Awake 函数完成时,可以在 Start 函数里进行。</para>
        /// <para>2. 数据加载:从文件或者服务器加载数据。</para>
        /// </summary>
        private void Start()
        {
            // 注册日志回调
            Application.logMessageReceived += OnLogMessageReceived;
        }

        /// <summary>
        /// OnDestroy 函数会在以下几种情况下被调用:
        /// <para>1. 当你使用 GameObject.Destroy 方法明确销毁一个游戏对象时,OnDestroy 会在销毁操作执行前被调用。</para>
        /// <para>2. 当场景切换时,如果某个游戏对象没有设置为 DontDestroyOnLoad,那么在场景切换过程中该对象被销毁时,OnDestroy 也会被触发。</para>
        /// <para>3. 当游戏结束或应用程序关闭时,所有存在的游戏对象都会被销毁,此时也会调用它们各自的 OnDestroy 函数。</para>
        /// OnDestroy 函数适用的场景:
        /// <para>1. 资源释放:当游戏对象持有一些外部资源,如网络连接、文件句柄、数据库连接等,在对象销毁时需要确保这些资源被正确释放,避免资源泄漏。</para>
        /// <para>2. 事件注销:如果在 OnEnable 或者其他地方注册了事件监听器,为了防止出现内存泄漏,需要在 OnDestroy 中注销这些事件监听器。</para>
        /// <para>3. 数据保存:在对象销毁前,可能需要将对象的一些状态数据保存到文件或者服务器,以便后续恢复或分析。</para>
        /// </summary>
        private void OnDestroy()
        {
            // 注销回调(避免内存泄漏)
            Application.logMessageReceived -= OnLogMessageReceived;

            // 关闭当前的日志文件
            CloseCurrentLogFile();
        }

        // 日志回调函数
        void OnLogMessageReceived(string logString, string stackTrace, LogType type)
        {
            // 检查当前日志文件是否达到最大大小
            if (IsCurrentLogFileTooLarge())
            {
                CreateNewLogFile();
            }

            // 确保日志写入器有效
            if (_currentLogWriter == null)
            {
                CreateNewLogFile();
            }

            if (_currentLogWriter == null) return;

            // 写入日志信息
            try
            {
                var logEntry = $"[{DateTime.Now:HH:mm:ss}] [{type}] {logString}";
                _currentLogWriter.WriteLine(logEntry);

                // 如果是错误日志,同时写入堆栈跟踪
                if (type is LogType.Error or LogType.Exception or LogType.Assert)
                {
                    _currentLogWriter.WriteLine($"StackTrace: {stackTrace}");
                }

                _currentLogWriter.Flush();
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to write to log file: {ex.Message}");
            }
        }

        private bool IsCurrentLogFileTooLarge()
        {
            if (_currentLogWriter == null || string.IsNullOrEmpty(_currentLogFilePath) || !File.Exists(_currentLogFilePath))
            {
                return false;
            }

            try
            {
                var fileInfo = new FileInfo(_currentLogFilePath);
                return fileInfo.Length >= maxLogFileSize || fileInfo.CreationTime.Date != DateTime.Now.Date;
            }
            catch (Exception ex)
            {
                Debug.LogError($"Failed to check log file size: {ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 清理超出最大数量限制的旧日志文件
        /// </summary>
        private void CleanupOldLogs()
        {
            try
            {
                // 获取所有符合命名规则的日志文件
                var logFiles = Directory.GetFiles(_logFilePath, $"{logFilePrefix}*.log")
                    .Select(path => new FileInfo(path))
                    .OrderBy(fi => fi.CreationTime) // 按创建时间排序( oldest first )
                    .ToList();

                // 如果文件数量超过限制,删除最旧的
                while (logFiles.Count > maxLogFileCount)
                {
                    var oldestFile = logFiles.First();
                    try
                    {
                        File.Delete(oldestFile.FullName);
                        logFiles.RemoveAt(0);
                        Debug.Log($"Deleted old log file: {oldestFile.Name}");
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError($"Failed to delete old log file {oldestFile.Name}: {ex.Message}");
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogError($"Error during log cleanup: {ex.Message}");
            }
        }

        private void EnsureLogDirectoryExists()
        {
            if (!Directory.Exists(_logFilePath))
            {
                Directory.CreateDirectory(_logFilePath);
            }
        }

        /// <summary>
        /// 初始化日志文件,如果存在最新的且未满,则继续使用,否则创建新文件
        /// </summary>
        private void InitializeLogFile()
        {
            // 查找最新的日志文件
            var latestLogFile = Directory.GetFiles(_logFilePath, $"{logFilePrefix}*.log")
                .Select(path => new FileInfo(path))
                .OrderByDescending(fi => fi.CreationTime)
                .FirstOrDefault();

            // 检查最新的日志文件是否可以继续使用(未满)
            if (latestLogFile != null)
            {
                var createdTime = latestLogFile.CreationTime;
                var currentTime = DateTime.Now;
                if (latestLogFile.Length < maxLogFileSize && createdTime.Date == currentTime.Date)
                {
                    _currentLogFilePath = latestLogFile.FullName;
                    _currentLogWriter = new StreamWriter(_currentLogFilePath, true);
                    _currentLogWriter.WriteLine($"===== Log resumed at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");
                }
                else
                {
                    // 创建新的日志文件
                    CreateNewLogFile();
                }
            }
            else
            {
                // 创建新的日志文件
                CreateNewLogFile();
            }
        }

        private void CreateNewLogFile()
        {
            // 关闭当前的日志文件(如果存在)
            CloseCurrentLogFile();

            // 生成新的日志文件名(包含时间戳)
            var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
            _currentLogFilePath = Path.Combine(_logFilePath, $"{logFilePrefix}{timestamp}.log");

            // 创建新的日志文件
            _currentLogWriter = new StreamWriter(_currentLogFilePath, true);
            _currentLogWriter.WriteLine($"===== Log started at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");

            // 检查是否需要清理旧日志
            CleanupOldLogs();
        }

        private void CloseCurrentLogFile()
        {
            if (_currentLogWriter != null)
            {
                _currentLogWriter.WriteLine($"===== Log ended at {DateTime.Now:yyyy-MM-dd HH:mm:ss} =====");
                _currentLogWriter.Flush();
                _currentLogWriter.Close();
                _currentLogWriter.Dispose();
                _currentLogWriter = null;
            }
        }
    }
}