feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,15 @@
namespace PCL.Core.Logging;
/// <summary>
/// 事件/意外行为等级。
/// </summary>
public enum ActionLevel
{
TraceLog = 00,
NormalLog = 10,
Hint = 20,
HintErr = 21,
MsgBox = 30,
MsgBoxErr = 31,
MsgBoxFatal = 32,
}
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using PCL.Core.App.Localization;
namespace PCL.Core.Logging;
/// <summary>
/// 将异常信息格式化为面向用户的消息,同时不改变原始的失败原因。
/// </summary>
public static partial class ExceptionDetails
{
private const int MaxSingleMessageLength = 4096;
private const int MaxCombinedMessageLength = 8192;
/// <summary>
/// 返回异常链中去重后的消息,从最外层异常依次到内部异常。
/// </summary>
public static string GetUserReason(Exception exception)
{
ArgumentNullException.ThrowIfNull(exception);
var messages = new List<string>();
var seenMessages = new HashSet<string>(StringComparer.Ordinal);
var current = exception;
// 异常链通常很短。这里的深度限制也能避免格式异常的链破坏 UI 展示。
for (var depth = 0; current is not null && depth < 32; depth++, current = current.InnerException)
{
var message = current.Message?.Trim();
if (string.IsNullOrWhiteSpace(message) || string.Equals(message, "$$", StringComparison.Ordinal))
continue;
message = _TruncateForUser(_RedactSensitiveText(message));
if (seenMessages.Add(message))
messages.Add(message);
}
var result = string.Join(Environment.NewLine, messages);
return _TruncateForUser(result, MaxCombinedMessageLength);
}
private static string _TruncateForUser(string text, int? maxLength = null)
{
var limit = maxLength ?? MaxSingleMessageLength;
return text.Length <= limit
? text
: text[..limit] + Environment.NewLine + "…";
}
[GeneratedRegex(
"""(\b(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|password|authorization)\b\s*[:=]\s*)(?:"[^"]*"|\S+)""",
RegexOptions.IgnoreCase)]
private static partial Regex _KeyValueCredentialPattern();
[GeneratedRegex(
@"([?&](?:access_token|refresh_token|token|password|client_secret)=)[^&\s]+",
RegexOptions.IgnoreCase)]
private static partial Regex _QueryStringCredentialPattern();
[GeneratedRegex(
@"\bBearer\s+[A-Za-z0-9._~+/=-]+",
RegexOptions.IgnoreCase)]
private static partial Regex _BearerTokenPattern();
private static string _RedactSensitiveText(string text)
{
// 在保留失败原因可见的前提下,避免在普通用户界面中直接暴露明显的凭据信息。
text = _KeyValueCredentialPattern().Replace(text, "$1[redacted]");
text = _QueryStringCredentialPattern().Replace(text, "$1[redacted]");
text = _BearerTokenPattern().Replace(text, "Bearer [redacted]");
return text;
}
/// <summary>
/// 返回完整的异常文本,用于调试和日志记录。
/// </summary>
public static string GetDebugDetails(Exception exception)
{
ArgumentNullException.ThrowIfNull(exception);
return exception.ToString();
}
/// <summary>
/// 将完整的原始异常文本附加到一个稳定的本地化摘要后面。
/// </summary>
public static string Compose(string summary, Exception? exception = null)
{
return Compose(summary, exception is null ? null : GetDebugDetails(exception));
}
/// <summary>
/// 将调用方提供的详细文本附加到稳定的本地化摘要后面,而不修改该详细文本。
/// </summary>
public static string Compose(string summary, string? details)
{
ArgumentException.ThrowIfNullOrWhiteSpace(summary);
return string.IsNullOrWhiteSpace(details)
? summary
: Lang.Text("SystemDialog.Error.Detail.WithException", summary.TrimEnd(), details);
}
}
@@ -0,0 +1,52 @@
using System.Collections.Generic;
namespace PCL.Core.Logging;
/// <summary>
/// 日志等级。将十进制值 <c>% 100</c> 并显式转换可得到该等级默认对应的 <see cref="ActionLevel"/>
/// </summary>
public enum LogLevel
{
Trace = 000 + ActionLevel.TraceLog,
Debug = 100 + ActionLevel.NormalLog,
Info = 200 + ActionLevel.NormalLog,
#if TRACE
Warning = 300 + ActionLevel.HintErr,
Error = 400 + ActionLevel.MsgBoxErr,
#else
Warning = 300 + ActionLevel.NormalLog,
Error = 400 + ActionLevel.HintErr,
#endif
Fatal = 500 + ActionLevel.MsgBoxFatal,
}
public static class LogLevelExtensions
{
private static readonly Dictionary<LogLevel, string> _LevelNameMap = new()
{
[LogLevel.Trace] = "TRA",
[LogLevel.Debug] = "DBG",
[LogLevel.Info] = "INFO",
[LogLevel.Warning] = "WARN",
[LogLevel.Error] = "ERR!",
[LogLevel.Fatal] = "FTL!"
};
extension(LogLevel level)
{
public string PrintName() => _LevelNameMap[level];
public ActionLevel DefaultActionLevel() => (ActionLevel)((int)level % 100);
public LogLevel RealLevel() => (int)level switch
{
< 100 => LogLevel.Trace,
< 200 => LogLevel.Debug,
< 300 => LogLevel.Info,
< 400 => LogLevel.Warning,
< 500 => LogLevel.Error,
_ => LogLevel.Fatal,
};
public int Header() => (int)level / 100 * 100;
}
}
@@ -0,0 +1,154 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using PCL.Core.App;
using PCL.Core.App.Essentials;
using PCL.Core.App.IoC;
using PCL.Core.App.Localization;
using PCL.Core.UI;
namespace PCL.Core.Logging;
[LifecycleService(LifecycleState.Loading, Priority = int.MaxValue)]
public class LogService : ILifecycleLogService
{
private static LifecycleContext? _context;
private static Logger? _logger;
private static bool _wrapperRegistered;
private LogService()
{
_context = Lifecycle.GetContext(this);
}
private static LifecycleContext Context => _context!;
public static Logger Logger => _logger!;
public string Identifier => "log";
public string Name => "日志服务";
public bool SupportAsync => false;
public Task StartAsync()
{
Context.Trace("正在初始化 Logger 实例");
var config = new LoggerConfiguration(Path.Combine(Basics.ExecutableDirectory, "PCL", "Log"));
_logger = new Logger(config);
Context.Trace("正在注册日志事件");
LogWrapper.OnLog += _OnWrapperLog;
_wrapperRegistered = true;
return Task.CompletedTask;
}
public async Task StopAsync()
{
if (_wrapperRegistered)
LogWrapper.OnLog -= _OnWrapperLog;
if (_logger is not null)
await _logger.DisposeAsync().ConfigureAwait(false);
}
public void OnLog(LifecycleLogItem item)
{
_LogAction(item.Level, item.ActionLevel, item.ComposeMessage(), item.Message, item.Exception);
}
private static void _LogAction(
LogLevel level,
ActionLevel actionLevel,
string formatted,
string plain,
Exception? ex)
{
if (ex is not null)
TelemetryService.ReportException(ex, plain, level);
// log
#if !TRACE
if (actionLevel != ActionLevel.TraceLog)
#endif
Logger.Log(formatted);
switch (actionLevel)
{
case <= ActionLevel.NormalLog:
return;
// hint
case ActionLevel.Hint or ActionLevel.HintErr:
HintWrapper.Show(
plain,
actionLevel == ActionLevel.Hint ? HintTheme.Info : HintTheme.Error);
break;
// message box
case ActionLevel.MsgBox or ActionLevel.MsgBoxErr:
{
var theme = actionLevel == ActionLevel.MsgBoxErr
? MsgBoxTheme.Error
: MsgBoxTheme.Info;
var caption = ex is null
? null
: Lang.Text("SystemDialog.Error.Unexpected.Title");
var message = _ComposeUserError(plain, ex);
if (actionLevel == ActionLevel.MsgBoxErr)
message = Lang.Text("SystemDialog.Error.Message.WithLogExportGuidance", message);
MsgBoxWrapper.Show(message, caption, theme, false);
break;
}
// fatal message box
case ActionLevel.MsgBoxFatal:
{
var message = Lang.Text(
"SystemDialog.Fatal.Message.WithFeedbackGuidance",
_ComposeUserError(plain, ex));
MessageBox.Show(
message,
Lang.Text("SystemDialog.Fatal.Title"),
MessageBoxButton.OK,
MessageBoxImage.Error);
break;
}
}
}
private static string _ComposeUserError(string plain, Exception? exception)
{
var summary = string.IsNullOrWhiteSpace(plain)
? Lang.Text("SystemDialog.Error.Unexpected.Message")
: plain;
return exception is null
? summary
: ExceptionDetails.Compose(summary, exception);
}
private static void _OnWrapperLog(
LogLevel level,
string msg,
string? module,
Exception? ex)
{
var thread = Thread.CurrentThread.Name ?? $"#{Environment.CurrentManagedThreadId}";
if (module is not null) module = $"[{module}] ";
var result = $"[{DateTime.Now:HH:mm:ss.fff}] [{level.PrintName()}] [{thread}] {module}{msg}";
_LogAction(
level,
level.DefaultActionLevel(),
ex is null
? result
: $"{result}\n{ex}",
msg,
ex);
}
}
@@ -0,0 +1,50 @@
using System;
namespace PCL.Core.Logging;
public delegate void LogHandler(LogLevel level, string msg, string? module = null, Exception? ex = null);
public static class LogWrapper
{
public static event LogHandler? OnLog;
// Fatal: can handle exceptions
public static void Fatal(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Fatal, msg, module, ex);
public static void Fatal(Exception? ex, string msg) => Fatal(ex, null, msg);
public static void Fatal(string? module, string msg) => Fatal(null, module, msg);
public static void Fatal(string msg) => Fatal((string?)null, msg);
// Error: can handle exceptions
public static void Error(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Error, msg, module, ex);
public static void Error(Exception? ex, string msg) => Error(ex, null, msg);
public static void Error(string? module, string msg) => Error(null, module, msg);
public static void Error(string msg) => Error((string?)null, msg);
// Warn: can handle exceptions
public static void Warn(Exception? ex, string? module, string msg) => OnLog?.Invoke(LogLevel.Warning, msg, module, ex);
public static void Warn(Exception? ex, string msg) => Warn(ex, null, msg);
public static void Warn(string? module, string msg) => Warn(null, module, msg);
public static void Warn(string msg) => Warn((string?)null, msg);
// Info
public static void Info(string? module, string msg) => OnLog?.Invoke(LogLevel.Info, msg, module);
public static void Info(string msg) => Info(null, msg);
// Debug
public static void Debug(string? module, string msg) => OnLog?.Invoke(LogLevel.Debug, msg, module);
public static void Debug(string msg) => Debug(null, msg);
public static void Debug(Exception ex, string module, string message) => Debug(module, $"{message}: {ex.ToString()}");
// Trace
public static void Trace(string? module, string msg) => OnLog?.Invoke(LogLevel.Trace, msg, module);
public static void Trace(string msg) => Trace(null, msg);
public static Logger CurrentLogger => LogService.Logger;
private static readonly Lazy<LoggerFactoryAdapter> _LoggerFactory = new(static () =>
{
return new LoggerFactoryAdapter(CurrentLogger);
});
public static LoggerFactoryAdapter LoggerFactory => _LoggerFactory.Value;
}
+193
View File
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Logging;
public sealed class Logger : IAsyncDisposable
{
public Logger(LoggerConfiguration configuration)
{
Configuration = configuration;
_CreateNewFile();
_processingTask = _ProcessLogQueueAsync();
}
// Data stream
private StreamWriter? _currentStream;
private FileStream? _currentFile;
private readonly List<string> _files = [];
// Statis
private long _droppedCount;
public long DroppedLogCount => Interlocked.Read(ref _droppedCount);
// Processor
private readonly Task _processingTask;
private readonly Channel<string> _logChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
{
SingleReader = true
});
public ReadOnlyCollection<string> CurrentLogFiles => _files.AsReadOnly();
public LoggerConfiguration Configuration { get; }
private void _CreateNewFile()
{
var now = DateTime.Now;
var nameFormat = (Configuration.FileNameFormat ?? $"Launch-{now.ToString("yyyy-M-d", CultureInfo.InvariantCulture)}-{{0}}") + ".log";
var filename = nameFormat.Replace("{0}", now.ToString("HHmmssfff", CultureInfo.InvariantCulture));
var filePath = Path.Combine(Configuration.StoreFolder, filename);
_files.Add(filePath);
var lastWriter = _currentStream;
var lastFile = _currentFile;
Directory.CreateDirectory(Configuration.StoreFolder);
_currentFile = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
_currentStream = new StreamWriter(_currentFile);
_ = Task.Run(async () =>
{
if (lastWriter is not null)
try
{
await lastWriter.DisposeAsync().ConfigureAwait(false);
}
catch (Exception) { /* Don't care */ }
if (lastFile is not null)
try
{
await lastFile.DisposeAsync().ConfigureAwait(false);
}
catch (Exception) { /* Don't care */ }
if (!Configuration.AutoDeleteOldFile)
return;
var logFiles = Directory.GetFiles(
Configuration.StoreFolder,
"*.log",
SearchOption.TopDirectoryOnly);
var needToDelete = logFiles.Select(x => new FileInfo(x))
.OrderBy(x => x.CreationTime)
.Take(logFiles.Length - Configuration.MaxKeepOldFile);
foreach (var logFile in needToDelete)
logFile.Delete();
});
}
public void Trace(string message) => Log($"[{_GetTimeFormatted()}] [TRA] {message}");
public void Debug(string message) => Log($"[{_GetTimeFormatted()}] [DBG] {message}");
public void Info(string message) => Log($"[{_GetTimeFormatted()}] [INFO] {message}");
public void Warn(string message) => Log($"[{_GetTimeFormatted()}] [WARN] {message}");
public void Error(string message) => Log($"[{_GetTimeFormatted()}] [ERR!] {message}");
public void Fatal(string message) => Log($"[{_GetTimeFormatted()}] [FTL!] {message}");
private static string _GetTimeFormatted() => $"{DateTime.Now:HH:mm:ss.fff}";
public void Log(string message)
{
if (_disposed) return;
if (!_logChannel.Writer.TryWrite(message))
{
Interlocked.Increment(ref _droppedCount);
Console.WriteLine($"Log dropped error: {message}");
}
}
private async Task _ProcessLogQueueAsync()
{
const int maxBatchLines = 198;
var writeTimeout = TimeSpan.FromMilliseconds(325);
var batch = new StringBuilder(4096);
var lineCount = 0u;
var lastFlush = Stopwatch.GetTimestamp();
try
{
while (!_disposed || _logChannel.Reader.TryPeek(out _))
{
if (_logChannel.Reader.TryRead(out var message))
{
#if DEBUG
message = message.ReplaceLineBreak("\r\n");
Console.WriteLine(message);
System.Diagnostics.Debug.WriteLine(message);
#endif
batch.AppendLine(message);
lineCount++;
var elapsed = Stopwatch.GetElapsedTime(lastFlush);
if (lineCount >= maxBatchLines || elapsed > writeTimeout)
{
await DoRefreshAsync().ConfigureAwait(false);
}
}
else
{
if (lineCount != 0)
{
await DoRefreshAsync().ConfigureAwait(false);
}
await Task.Delay(80).ConfigureAwait(false);
}
}
async Task DoRefreshAsync()
{
await _DoWriteAsync(batch).ConfigureAwait(false);
batch.Clear();
lineCount = 0;
lastFlush = Stopwatch.GetTimestamp();
}
}
catch (Exception e)
{
// 出错了先干到标准输出流中吧 Orz
Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while processing log queue: {e.Message}");
throw;
}
}
private async Task _DoWriteAsync(StringBuilder ctx)
{
try
{
if (_currentFile?.Length >= Configuration.MaxFileSize)
{
_CreateNewFile();
}
await _currentStream!.WriteAsync(ctx).ConfigureAwait(false);
await _currentStream.FlushAsync().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occurred while writing log file: {e.Message}");
await File.AppendAllTextAsync(Path.Combine(Configuration.StoreFolder, "Error.log"), $"[{_GetTimeFormatted}] LogCycle Error: {e}\n");
throw;
}
}
private bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
_logChannel.Writer.Complete();
await _processingTask.ConfigureAwait(false);
if (_currentStream is not null)
await _currentStream.DisposeAsync().ConfigureAwait(false);
if (_currentFile is not null)
await _currentFile.DisposeAsync().ConfigureAwait(false);
}
}
@@ -0,0 +1,137 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace PCL.Core.Logging;
/// <summary>
/// Microsoft.Extensions.Logging.ILogger 适配器
/// 将现有的 Logger 包装为标准的 ILogger 接口实现
/// </summary>
public class LoggerAdapter(Logger logger, string categoryName) : ILogger
{
private readonly Logger _innerLogger = logger ?? throw new ArgumentNullException(nameof(logger));
private readonly string _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
private static readonly AsyncLocal<Stack<object>> _ScopeStack = new();
IDisposable ILogger.BeginScope<TState>(TState state)
{
_ScopeStack.Value ??= new Stack<object>();
_ScopeStack.Value.Push(state);
return new ScopeDisposable(state);
}
#pragma warning disable CS9113 // 参数未读。
private class ScopeDisposable(object state) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed)
return;
if (_ScopeStack.Value is { Count: > 0 })
{
#if DEBUG
var popped = _ScopeStack.Value.Pop();
if (!ReferenceEquals(popped, state))
{
throw new InvalidOperationException("Scope disposal order mismatch.");
}
#else
_ = _ScopeStack.Value.Pop();
#endif
}
_disposed = true;
}
}
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) => true;
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
return;
ArgumentNullException.ThrowIfNull(formatter);
var originalMessage = formatter(state, exception);
var sb = new StringBuilder();
// 类别名称
if (!string.IsNullOrEmpty(_categoryName))
{
sb.Append('[').Append(_categoryName).Append("] ");
}
// 事件 ID
if (eventId.Id != 0 || !string.IsNullOrEmpty(eventId.Name))
{
sb.Append("[EventId:");
if (!string.IsNullOrEmpty(eventId.Name))
{
sb.Append(eventId.Id).Append(':').Append(eventId.Name);
}
else
{
sb.Append(eventId.Id);
}
sb.Append("] ");
}
// 上下文信息
var scopeContext = _BuildScopeContext();
if (!string.IsNullOrEmpty(scopeContext))
{
sb.Append('[').Append(_categoryName).Append("] ");
}
sb.Append(originalMessage);
var finalMessage = sb.ToString();
// 日志级别
switch (logLevel)
{
case Microsoft.Extensions.Logging.LogLevel.Trace:
_innerLogger.Trace(finalMessage);
break;
case Microsoft.Extensions.Logging.LogLevel.Debug:
_innerLogger.Debug(finalMessage);
break;
case Microsoft.Extensions.Logging.LogLevel.Information:
_innerLogger.Info(finalMessage);
break;
case Microsoft.Extensions.Logging.LogLevel.Warning:
_innerLogger.Warn(finalMessage);
break;
case Microsoft.Extensions.Logging.LogLevel.Error:
_innerLogger.Error(finalMessage);
break;
case Microsoft.Extensions.Logging.LogLevel.Critical:
_innerLogger.Fatal(finalMessage);
break;
}
if (exception is not null)
{
var exceptionMessage = $"Exception: {exception}";
_innerLogger.Log($"[{_categoryName}] {exceptionMessage}");
}
}
private string _BuildScopeContext()
{
var stack = _ScopeStack.Value;
if (stack is null || stack.Count == 0)
return string.Empty;
var scopes = stack.AsEnumerable().Reverse();
return string.Join(" => ", scopes.Select(s => s.ToString()));
}
}
@@ -0,0 +1,10 @@
namespace PCL.Core.Logging;
public record LoggerConfiguration(
string StoreFolder,
long MaxFileSize = 32 * 1024 * 1024,
string? FileNameFormat = null,
bool AutoDeleteOldFile = true,
int MaxKeepOldFile = 16,
LogLevel MinLogLevel = LogLevel.Info
);
@@ -0,0 +1,107 @@
using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace PCL.Core.Logging;
public static class LoggerExtensions
{
/// <summary>
/// 创建 ILogger 实例
/// </summary>
/// <param name="logger">现有的 Logger 实例</param>
/// <param name="categoryName">日志类别名称</param>
/// <returns>ILogger 实例</returns>
public static ILogger CreateLogger(this Logger logger, string categoryName)
{
return new LoggerAdapter(logger, categoryName);
}
/// <summary>
/// 创建 ILogger 工厂
/// </summary>
/// <param name="logger">现有的 Logger 实例</param>
/// <returns>ILoggerFactory 实例</returns>
public static ILoggerFactory CreateLoggerFactory(this Logger logger)
{
return new LoggerFactoryAdapter(logger);
}
/// <summary>
/// 使用结构化日志记录的扩展方法
/// </summary>
public static void LogInformation<T0>(this ILogger logger, string message, T0 arg0)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0);
}
public static void LogInformation<T0, T1>(this ILogger logger, string message, T0 arg0, T1 arg1)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1);
}
public static void LogInformation<T0, T1, T2>(this ILogger logger, string message, T0 arg0, T1 arg1, T2 arg2)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1, arg2);
}
public static void LogWarning<T0>(this ILogger logger, string message, T0 arg0)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0);
}
public static void LogWarning<T0, T1>(this ILogger logger, string message, T0 arg0, T1 arg1)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0, arg1);
}
public static void LogError<T0>(this ILogger logger, Exception? exception, string message, T0 arg0)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0);
}
public static void LogError<T0, T1>(this ILogger logger, Exception? exception, string message, T0 arg0, T1 arg1)
{
logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0, arg1);
}
/// <summary>
/// 条件日志记录扩展方法
/// </summary>
public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, string message)
{
if (condition)
{
logger.Log(level, message);
}
}
public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, Exception? exception, string message)
{
if (condition)
{
logger.Log(level, exception, message);
}
}
/// <summary>
/// 性能计时日志记录
/// </summary>
public static IDisposable LogPerformance(this ILogger logger, string operationName)
{
logger.LogInformation("开始执行: {OperationName}", operationName);
return new PerformanceLoggerDisposable(logger, operationName);
}
private class PerformanceLoggerDisposable(ILogger logger, string operationName) : IDisposable
{
private readonly long _startTime = Stopwatch.GetTimestamp();
public void Dispose()
{
var elapsed = Stopwatch.GetElapsedTime(_startTime);
logger.LogInformation("完成执行: {OperationName}, 耗时: {ElapsedMs}ms", operationName, elapsed.TotalMilliseconds);
}
}
}
@@ -0,0 +1,34 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
namespace PCL.Core.Logging;
/// <summary>
/// ILoggerFactory 实现,用于创建 LoggerAdapter 实例
/// </summary>
public class LoggerFactoryAdapter(Logger logger) : ILoggerFactory
{
private readonly Logger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
private readonly List<IDisposable> _disposables = [];
public void AddProvider(ILoggerProvider provider)
{
_disposables.Add(provider);
// 不需要实现,因为我们只有一个固定的 Logger
}
public ILogger CreateLogger(string categoryName)
{
return new LoggerAdapter(_logger, categoryName);
}
public void Dispose()
{
foreach (var disposable in _disposables)
{
disposable.Dispose();
}
_disposables.Clear();
}
}