using FluentValidation; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.Extensions.Caching.StackExchangeRedis; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; using TakeoutSaaS.Application.Messaging.Extensions; using TakeoutSaaS.Application.Sms.Extensions; using TakeoutSaaS.Application.Storage.Extensions; using TakeoutSaaS.Module.Messaging.Extensions; using TakeoutSaaS.Module.Sms.Extensions; using TakeoutSaaS.Module.Storage.Extensions; using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Kernel.Ids; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; using System.Reflection; // 1. 创建构建器与日志模板 var builder = WebApplication.CreateBuilder(args); const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; // 2. 注册雪花 ID 生成器与 Serilog builder.Services.AddSingleton(_ => new SnowflakeIdGenerator()); builder.Host.UseSerilog((_, _, configuration) => { configuration .Enrich.FromLogContext() .Enrich.WithProperty("Service", "MiniApi") .WriteTo.Console(outputTemplate: logTemplate) .WriteTo.File( "logs/mini-api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, shared: true, outputTemplate: logTemplate); }); // 3. 注册通用 Web 能力与 Swagger builder.Services.AddSharedWebCore(); builder.Services.AddSharedSwagger(options => { options.Title = "外卖SaaS - 小程序端"; options.Description = "小程序 API 文档"; options.EnableAuthorization = true; }); builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); // 4. 注册 Redis 分布式缓存,供验证码等功能使用 var redisConnection = builder.Configuration.GetValue("Redis"); builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = redisConnection; }); // 4. 注册多租户与业务模块 builder.Services.AddTenantResolution(builder.Configuration); builder.Services.AddStorageModule(builder.Configuration); builder.Services.AddStorageApplication(); builder.Services.AddSmsModule(builder.Configuration); builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); builder.Services.AddHealthChecks(); // 5. 配置 OpenTelemetry 采集 var otelSection = builder.Configuration.GetSection("Otel"); var otelEndpoint = otelSection.GetValue("Endpoint"); var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource.AddService( serviceName: "TakeoutSaaS.MiniApi", serviceVersion: "1.0.0", serviceInstanceId: Environment.MachineName)) .WithTracing(tracing => { tracing .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddEntityFrameworkCoreInstrumentation(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { tracing.AddOtlpExporter(exporter => { exporter.Endpoint = new Uri(otelEndpoint); }); } if (useConsoleExporter) { tracing.AddConsoleExporter(); } }) .WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddPrometheusExporter(); if (!string.IsNullOrWhiteSpace(otelEndpoint)) { metrics.AddOtlpExporter(exporter => { exporter.Endpoint = new Uri(otelEndpoint); }); } if (useConsoleExporter) { metrics.AddConsoleExporter(); } }); // 6. 配置 CORS var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); builder.Services.AddCors(options => { options.AddPolicy("MiniApiCors", policy => { ConfigureCorsPolicy(policy, miniOrigins); }); }); // 7. 构建应用并配置中间件管道 var app = builder.Build(); app.UseCors("MiniApiCors"); app.UseTenantResolution(); app.UseSharedWebCore(); app.UseSharedSwagger(); app.MapHealthChecks("/healthz"); app.MapPrometheusScrapingEndpoint(); app.MapControllers(); app.Run(); // 8. 解析配置中的 CORS 域名 static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) { var origins = configuration.GetSection(sectionKey).Get(); return origins? .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? []; } // 9. 构建 CORS 策略 static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) { if (origins.Length == 0) { policy.AllowAnyOrigin(); } else { policy.WithOrigins(origins) .AllowCredentials(); } policy .AllowAnyHeader() .AllowAnyMethod(); }