diff --git a/.gitmodules b/.gitmodules
index c0e417a..158e990 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,7 @@
[submodule "TakeoutSaaS.Docs"]
path = TakeoutSaaS.Docs
url = git@github.com:msumshk/TakeoutSaaS.Docs.git
+[submodule "src/Gateway/TakeoutSaaS.ApiGateway"]
+ path = src/Gateway/TakeoutSaaS.ApiGateway
+ url = git@github.com:msumshk/TakeoutSaaS.Gateway.git
+ branch = dev
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway b/src/Gateway/TakeoutSaaS.ApiGateway
new file mode 160000
index 0000000..2b64020
--- /dev/null
+++ b/src/Gateway/TakeoutSaaS.ApiGateway
@@ -0,0 +1 @@
+Subproject commit 2b6402058dfddc3aed6d5ff8052dcd68cf4ee632
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs
deleted file mode 100644
index 97064fb..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace TakeoutSaaS.ApiGateway.Configuration;
-
-///
-/// 网关 OpenTelemetry 导出配置。
-///
-public class GatewayOpenTelemetryOptions
-{
- ///
- /// 是否启用 OpenTelemetry。
- ///
- public bool Enabled { get; set; } = true;
-
- ///
- /// 服务名称,用于 Resource 标识。
- ///
- public string ServiceName { get; set; } = "TakeoutSaaS.ApiGateway";
-
- ///
- /// OTLP 导出端点(http/https)。
- ///
- public string? OtlpEndpoint { get; set; }
-}
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs
deleted file mode 100644
index cf633a4..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace TakeoutSaaS.ApiGateway.Configuration;
-
-///
-/// 网关限流参数配置。
-///
-public class GatewayRateLimitOptions
-{
- ///
- /// 是否开启固定窗口限流。
- ///
- public bool Enabled { get; set; } = true;
-
- ///
- /// 固定窗口内允许的最大请求数。
- ///
- public int PermitLimit { get; set; } = 300;
-
- ///
- /// 固定窗口长度(秒)。
- ///
- public int WindowSeconds { get; set; } = 60;
-
- ///
- /// 排队等待的最大请求数。
- ///
- public int QueueLimit { get; set; } = 100;
-}
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile b/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile
deleted file mode 100644
index beac7d4..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
-WORKDIR /src
-
-# 1. 先只复制 csproj 文件 (利用 Docker 缓存)
-COPY ["src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj", "src/Gateway/TakeoutSaaS.ApiGateway/"]
-
-# 2. 还原依赖 (如果 csproj 没变,这一步会直接使用缓存,瞬间完成)
-RUN dotnet restore "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj"
-
-# 3. 再复制剩余的所有源代码
-COPY . .
-
-# 4. 发布
-RUN dotnet publish "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj" -c Release -o /app/publish
-
-# --- 运行时环境 ---
-FROM mcr.microsoft.com/dotnet/aspnet:10.0
-WORKDIR /app
-COPY --from=build /app/publish .
-
-# 显式声明端口
-EXPOSE 5000
-ENV ASPNETCORE_URLS=http://+:5000
-
-ENTRYPOINT ["dotnet", "TakeoutSaaS.ApiGateway.dll"]
\ No newline at end of file
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs
deleted file mode 100644
index 6011bd9..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs
+++ /dev/null
@@ -1,215 +0,0 @@
-using Microsoft.AspNetCore.Diagnostics;
-using Microsoft.AspNetCore.HttpOverrides;
-using OpenTelemetry.Logs;
-using OpenTelemetry.Metrics;
-using OpenTelemetry.Resources;
-using OpenTelemetry.Trace;
-using Serilog;
-using System.Diagnostics;
-using System.Threading.RateLimiting;
-using TakeoutSaaS.ApiGateway.Configuration;
-
-const string CorsPolicyName = "GatewayCors";
-
-// 1. 创建构建器并配置 Serilog
-var builder = WebApplication.CreateBuilder(args);
-builder.Host.UseSerilog((context, services, loggerConfiguration) =>
-{
- loggerConfiguration
- .ReadFrom.Configuration(context.Configuration)
- .ReadFrom.Services(services)
- .Enrich.FromLogContext();
-});
-
-// 2. 配置 YARP 反向代理
-builder.Services.AddReverseProxy()
- .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
-
-// 3. 转发头部配置
-builder.Services.Configure(options =>
-{
- options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
- options.KnownIPNetworks.Clear();
- options.KnownProxies.Clear();
-});
-
-// 4. 配置 CORS
-builder.Services.AddCors(options =>
-{
- options.AddPolicy(CorsPolicyName, policy =>
- {
- policy.AllowAnyOrigin()
- .AllowAnyHeader()
- .AllowAnyMethod();
- });
-});
-
-// 5. 配置网关限流
-builder.Services.Configure(builder.Configuration.GetSection("Gateway:RateLimiting"));
-var rateLimitOptions = builder.Configuration.GetSection("Gateway:RateLimiting").Get() ?? new();
-
-if (rateLimitOptions.Enabled)
-{
- builder.Services.AddRateLimiter(options =>
- {
- options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
- options.GlobalLimiter = PartitionedRateLimiter.Create(context =>
- {
- var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
- return RateLimitPartition.GetFixedWindowLimiter(remoteIp, _ => new FixedWindowRateLimiterOptions
- {
- PermitLimit = Math.Max(1, rateLimitOptions.PermitLimit),
- Window = TimeSpan.FromSeconds(Math.Max(1, rateLimitOptions.WindowSeconds)),
- QueueLimit = Math.Max(0, rateLimitOptions.QueueLimit),
- QueueProcessingOrder = QueueProcessingOrder.OldestFirst
- });
- });
- });
-}
-
-// 6. 配置 OpenTelemetry
-var otelOptions = builder.Configuration.GetSection("OpenTelemetry").Get() ?? new();
-if (otelOptions.Enabled)
-{
- builder.Services.AddOpenTelemetry()
- // 1. 配置统一的 Resource,便于追踪定位。
- .ConfigureResource(resource => resource.AddService(otelOptions.ServiceName ?? "TakeoutSaaS.ApiGateway"))
- .WithMetrics(metrics =>
- {
- metrics.AddAspNetCoreInstrumentation()
- .AddHttpClientInstrumentation()
- .AddRuntimeInstrumentation();
-
- if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
- {
- metrics.AddOtlpExporter(options =>
- {
- options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
- });
- }
- })
- .WithTracing(tracing =>
- {
- tracing.AddAspNetCoreInstrumentation(options =>
- {
- options.RecordException = true;
- })
- .AddHttpClientInstrumentation();
-
- if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
- {
- tracing.AddOtlpExporter(options =>
- {
- options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
- });
- }
- });
-
- builder.Logging.AddOpenTelemetry(logging =>
- {
- logging.IncludeScopes = true;
- logging.ParseStateValues = true;
- if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
- {
- logging.AddOtlpExporter(options =>
- {
- options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
- });
- }
- });
-}
-
-// 7. 构建应用
-var app = builder.Build();
-
-// 8. 转发头中间件
-app.UseForwardedHeaders();
-
-// 9. 全局异常处理中间件
-app.UseExceptionHandler(errorApp =>
-{
- // 1. 捕获所有未处理异常并返回统一结构。
- errorApp.Run(async context =>
- {
- var feature = context.Features.Get();
- var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
- context.Response.StatusCode = StatusCodes.Status500InternalServerError;
- context.Response.ContentType = "application/json";
-
- var payload = new
- {
- success = false,
- code = 500,
- message = "Gateway internal error",
- traceId
- };
-
- var logger = context.RequestServices.GetRequiredService().CreateLogger("Gateway");
- logger.LogError(feature?.Error, "网关异常 {TraceId}", traceId);
- await context.Response.WriteAsJsonAsync(payload, cancellationToken: context.RequestAborted);
- });
-});
-
-// 10. 请求日志
-app.UseSerilogRequestLogging(options =>
-{
- options.MessageTemplate = "网关请求 {RequestMethod} {RequestPath} => {StatusCode} 用时 {Elapsed:0.000} 秒";
- options.GetLevel = (httpContext, elapsed, ex) => ex is not null ? Serilog.Events.LogEventLevel.Error : Serilog.Events.LogEventLevel.Information;
- options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
- {
- diagnosticContext.Set("TraceId", Activity.Current?.Id ?? httpContext.TraceIdentifier);
- diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
- };
-});
-
-// 11. CORS 与限流
-app.UseCors(CorsPolicyName);
-
-if (rateLimitOptions.Enabled)
-{
- app.UseRateLimiter();
-}
-
-// 12. 透传请求头并保证 Trace
-app.Use(async (context, next) =>
-{
- // 1. 确保请求拥有可追踪的 ID。
- if (!context.Request.Headers.ContainsKey("X-Request-Id"))
- {
- context.Request.Headers["X-Request-Id"] = Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
- }
-
- // 2. 透传租户等标识头,方便下游继续使用。
- var tenantId = context.Request.Headers["X-Tenant-Id"];
- if (!string.IsNullOrWhiteSpace(tenantId))
- {
- context.Request.Headers["X-Tenant-Id"] = tenantId;
- }
-
- var tenantCode = context.Request.Headers["X-Tenant-Code"];
- if (!string.IsNullOrWhiteSpace(tenantCode))
- {
- context.Request.Headers["X-Tenant-Code"] = tenantCode;
- }
-
- await next(context);
-});
-
-// 13. 映射反向代理与健康接口
-app.MapReverseProxy();
-
-app.MapGet("/", () => Results.Json(new
-{
- Service = "TakeoutSaaS.ApiGateway",
- Status = "OK",
- Timestamp = DateTimeOffset.UtcNow
-}));
-
-app.MapGet("/healthz", () => Results.Json(new
-{
- Service = "TakeoutSaaS.ApiGateway",
- Status = "Healthy",
- Timestamp = DateTimeOffset.UtcNow
-}));
-
-app.Run();
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json b/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json
deleted file mode 100644
index 3956499..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "profiles": {
- "TakeoutSaaS.ApiGateway": {
- "commandName": "Project",
- "launchBrowser": true,
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- },
- "applicationUrl": "https://localhost:2677;http://localhost:2683"
- }
- }
-}
\ No newline at end of file
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj
deleted file mode 100644
index 207e703..0000000
Binary files a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj and /dev/null differ
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json
deleted file mode 100644
index 56dd41a..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "OpenTelemetry": {
- "Enabled": false
- },
- "ReverseProxy": {
- "Routes": {
- "admin-route": {
- "ClusterId": "admin",
- "Match": { "Path": "/api/admin/{**catch-all}" }
- },
- "mini-route": {
- "ClusterId": "mini",
- "Match": { "Path": "/api/mini/{**catch-all}" }
- },
- "user-route": {
- "ClusterId": "user",
- "Match": { "Path": "/api/user/{**catch-all}" }
- }
- },
- "Clusters": {
- "admin": {
- "Destinations": {
- "d1": { "Address": "http://localhost:5001/" }
- }
- },
- "mini": {
- "Destinations": {
- "d1": { "Address": "http://localhost:5002/" }
- }
- },
- "user": {
- "Destinations": {
- "d1": { "Address": "http://localhost:5003/" }
- }
- }
- }
- }
-}
diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json
deleted file mode 100644
index 0c5d718..0000000
--- a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
- "Serilog": {
- "Using": [ "Serilog.Sinks.Console" ],
- "MinimumLevel": {
- "Default": "Information",
- "Override": {
- "Microsoft": "Warning",
- "System": "Warning",
- "Yarp": "Information"
- }
- },
- "WriteTo": [
- {
- "Name": "Console",
- "Args": {
- "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
- }
- }
- ],
- "Enrich": [ "FromLogContext" ],
- "Properties": {
- "Application": "TakeoutSaaS.ApiGateway"
- }
- },
- "Gateway": {
- "RateLimiting": {
- "Enabled": true,
- "PermitLimit": 300,
- "WindowSeconds": 60,
- "QueueLimit": 100
- }
- },
- "OpenTelemetry": {
- "Enabled": true,
- "ServiceName": "TakeoutSaaS.ApiGateway",
- "OtlpEndpoint": "http://localhost:4317"
- },
- "ReverseProxy": {
- "Routes": {
- "admin-route": {
- "ClusterId": "admin",
- "Match": { "Path": "/api/admin/{**catch-all}" }
- },
- "mini-route": {
- "ClusterId": "mini",
- "Match": { "Path": "/api/mini/{**catch-all}" }
- },
- "user-route": {
- "ClusterId": "user",
- "Match": { "Path": "/api/user/{**catch-all}" }
- }
- },
- "Clusters": {
- "admin": {
- "Destinations": {
- "primary": { "Address": "http://49.7.179.246:7801/" }
- }
- },
- "mini": {
- "Destinations": {
- "primary": { "Address": "http://49.7.179.246:7701/" }
- }
- },
- "user": {
- "Destinations": {
- "primary": { "Address": "http://49.7.179.246:7901/" }
- }
- }
- }
- },
- "AllowedHosts": "*"
-}