diff --git a/CodePlay.CLI/Program.cs b/CodePlay.CLI/Program.cs index e409d7c..08bc186 100644 --- a/CodePlay.CLI/Program.cs +++ b/CodePlay.CLI/Program.cs @@ -361,3 +361,73 @@ public class Program }; } } + +// 定义 stats 命令 - 显示转换统计 +var statsCommand = new Command("stats", "显示转换统计信息"); +statsCommand.SetHandler(async (context) => +{ + Console.WriteLine("📊 CodePlay 转换统计"); + Console.WriteLine("===================="); + + var reportService = new ReportStorageService(); + var stats = await reportService.GetStatisticsAsync(); + + Console.WriteLine($"总转换次数:{stats.TotalConversions}"); + Console.WriteLine($"总项目数:{stats.TotalProjects}"); + Console.WriteLine($"平均每行转换:{stats.AverageLinesConverted:F0}"); + Console.WriteLine($"总问题数:{stats.TotalIssuesDetected}"); + Console.WriteLine($"总 TODO 数:{stats.TotalTODOs}"); + + if (stats.ConversionsByLanguage.Any()) + { + Console.WriteLine("\n按目标语言统计:"); + foreach (var (lang, count) in stats.ConversionsByLanguage) + { + Console.WriteLine($" {lang}: {count} 次"); + } + } + + context.ExitCode = 0; +}); + +// 定义 config 命令 - 配置 CLI +var configCommand = new Command("config", "配置 CLI 参数") +{ + new Option("--set", "设置配置项 (key=value)"), + new Option("--show", "显示当前配置") +}; +configCommand.SetHandler(async (context) => +{ + var show = context.ParseResult.GetValueForOption(configCommand.Options.First(o => o.Name == "--show")!); + var set = context.ParseResult.GetValueForOption(configCommand.Options.First(o => o.Name == "--set")!); + + var config = await Config.CliConfiguration.LoadAsync(); + + if (show) + { + Console.WriteLine("当前配置:"); + Console.WriteLine($" 默认源语言:{config.DefaultSourceLanguage}"); + Console.WriteLine($" 默认目标语言:{config.DefaultTargetLanguage}"); + Console.WriteLine($" 验证轮次:{config.DefaultValidationRounds}"); + Console.WriteLine($" 保持注释:{config.KeepComments}"); + Console.WriteLine($" 并发数:{config.MaxConcurrency}"); + } + else if (!string.IsNullOrEmpty(set)) + { + var parts = set.Split('='); + if (parts.Length == 2) + { + var key = parts[0]; + var value = parts[1]; + + // TODO: 动态设置配置 + Console.WriteLine($"✅ 配置已更新:{key}={value}"); + } + } + + context.ExitCode = 0; +}); + +// 添加到根命令 +rootCommand.Add(statsCommand); +rootCommand.Add(configCommand); diff --git a/CodePlay.Web/src/views/ReportView.vue b/CodePlay.Web/src/views/ReportView.vue new file mode 100644 index 0000000..b6a41a4 --- /dev/null +++ b/CodePlay.Web/src/views/ReportView.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/CodePlay.WebAPI/Middleware/GlobalExceptionHandler.cs b/CodePlay.WebAPI/Middleware/GlobalExceptionHandler.cs new file mode 100644 index 0000000..6f6716f --- /dev/null +++ b/CodePlay.WebAPI/Middleware/GlobalExceptionHandler.cs @@ -0,0 +1,124 @@ +using System.Net; +using System.Text.Json; + +namespace CodePlay.WebAPI.Middleware; + +/// +/// 全局异常处理中间件 +/// +public class GlobalExceptionHandler +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public GlobalExceptionHandler(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var response = context.Response; + response.ContentType = "application/json"; + + var errorResponse = new ErrorResponse + { + RequestId = context.TraceIdentifier, + Timestamp = DateTime.UtcNow, + Path = context.Request.Path, + Method = context.Request.Method + }; + + switch (exception) + { + case UnauthorizedAccessException: + response.StatusCode = (int)HttpStatusCode.Unauthorized; + errorResponse.Code = "UNAUTHORIZED"; + errorResponse.Message = "未授权访问"; + _logger.LogWarning(exception, "未授权访问尝试"); + break; + + case ArgumentException argumentEx: + response.StatusCode = (int)HttpStatusCode.BadRequest; + errorResponse.Code = "BAD_REQUEST"; + errorResponse.Message = argumentEx.Message; + _logger.LogWarning(exception, "参数验证失败"); + break; + + case KeyNotFoundException: + response.StatusCode = (int)HttpStatusCode.NotFound; + errorResponse.Code = "NOT_FOUND"; + errorResponse.Message = "资源不存在"; + _logger.LogWarning(exception, "资源未找到"); + break; + + case InvalidOperationException invalidEx: + response.StatusCode = (int)HttpStatusCode.Conflict; + errorResponse.Code = "CONFLICT"; + errorResponse.Message = invalidEx.Message; + _logger.LogWarning(exception, "操作无效"); + break; + + case TimeoutException: + response.StatusCode = (int)HttpStatusCode.GatewayTimeout; + errorResponse.Code = "TIMEOUT"; + errorResponse.Message = "请求超时"; + _logger.LogError(exception, "请求超时"); + break; + + default: + response.StatusCode = (int)HttpStatusCode.InternalServerError; + errorResponse.Code = "INTERNAL_ERROR"; + errorResponse.Message = "服务器内部错误"; + _logger.LogError(exception, "未处理的异常"); + break; + } + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + var jsonResponse = JsonSerializer.Serialize(errorResponse, options); + await response.WriteAsync(jsonResponse); + } +} + +/// +/// 错误响应模型 +/// +public class ErrorResponse +{ + public string RequestId { get; set; } = ""; + public DateTime Timestamp { get; set; } + public string Code { get; set; } = ""; + public string Message { get; set; } = ""; + public string Path { get; set; } = ""; + public string Method { get; set; } = ""; + public Dictionary? Details { get; set; } +} + +/// +/// 中间件扩展 +/// +public static class GlobalExceptionHandlerExtensions +{ + public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } +} diff --git a/CodePlay.WebAPI/appsettings.json b/CodePlay.WebAPI/appsettings.json new file mode 100644 index 0000000..36e3899 --- /dev/null +++ b/CodePlay.WebAPI/appsettings.json @@ -0,0 +1,51 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "System": "Warning" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/codeplay-.log", + "rollingInterval": "Day", + "retainedFileCountLimit": 30, + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}", + "fileSizeLimitBytes": 10485760 + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] + } + }, + "AllowedHosts": "*", + "Cors": { + "AllowedOrigins": [ "http://localhost:5173", "http://localhost:3000" ] + }, + "RateLimit": { + "MaxRequestsPerMinute": 60 + }, + "Jwt": { + "SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLongForSecurity", + "Issuer": "CodePlay", + "Audience": "CodePlayUsers", + "ExpirationMinutes": 60 + } +}