本轮迁移已完成,生产运行链路现已切换为纯 ASP.NET Core 10。
主要完成: 补齐招生学校计划、投档审核、报到、扫码、补录等原生接口。 新增 SQLite/MySQL 空库初始化及默认审批流、号码规则。 删除 Node 兼容代理,未知 API 直接返回原生 404。 Docker、Compose、Gitea CI 全部切换到 Eis.Web.dll。 CKEditor 已固化到 Web 发布资源,不再依赖 node_modules。 新增纯 .NET 冒烟脚本:[smoke-dotnet-native.ps1 (line 1)](C:/Users/BI/Documents/EIS-dotnet/scripts/smoke-dotnet-native.ps1:1)。 迁移状态已更新:[MIGRATION.md (line 14)](C:/Users/BI/Documents/EIS-dotnet/MIGRATION.md:14)。 容器入口见 [Dockerfile (line 31)](C:/Users/BI/Documents/EIS-dotnet/Dockerfile:31)。
This commit is contained in:
@@ -222,6 +222,15 @@ public static class NativeAdminReadEndpoints
|
||||
|
||||
if (options.NativeAdmissionsEnabled)
|
||||
{
|
||||
endpoints.MapGet("/api/admission/context",
|
||||
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAdmissionSchoolContextAsync(Token(context), cancellationToken)));
|
||||
endpoints.MapGet("/api/admission/plans",
|
||||
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAdmissionSchoolPlansAsync(Token(context), cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/plans",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveAdmissionSchoolPlanAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapGet("/api/admission/notice-template",
|
||||
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAdmissionSchoolNoticeTemplateAsync(Token(context), cancellationToken)));
|
||||
@@ -248,6 +257,21 @@ public static class NativeAdminReadEndpoints
|
||||
context.Request.Query["examId"].ToString(),
|
||||
await ReadWorkbookAsync(context.Request, cancellationToken),
|
||||
cancellationToken)));
|
||||
endpoints.MapPut("/api/admission/reporting/draft",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveAdmissionSchoolReportingDraftAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/reporting/scan-preview",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.PreviewAdmissionSchoolReportingScanAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/reporting/scan",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveAdmissionSchoolReportingScanAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/reporting/submit",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SubmitAdmissionSchoolReportingAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/reporting/decision",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveAdmissionSchoolReportingDecisionAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapGet("/api/admission/placements",
|
||||
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAdmissionSchoolPlacementsAsync(Token(context), cancellationToken)));
|
||||
@@ -259,6 +283,12 @@ public static class NativeAdminReadEndpoints
|
||||
Token(context),
|
||||
context.Request.Query["examId"].ToString(),
|
||||
cancellationToken)));
|
||||
endpoints.MapPost("/api/admission/placements/bulk",
|
||||
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.ReviewAdmissionSchoolPlacementsAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPatch("/api/admission/placements/{placementId}",
|
||||
(HttpContext context, string placementId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.ReviewAdmissionSchoolPlacementAsync(Token(context), placementId, body, cancellationToken)));
|
||||
endpoints.MapGet("/api/admin/admissions",
|
||||
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAsync(Token(context), cancellationToken)));
|
||||
|
||||
@@ -15,8 +15,5 @@
|
||||
<Content Include="..\..\src\client\**\*.mjs" Link="wwwroot\src\client\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\src\data\china-regions.mjs" Link="wwwroot\src\data\china-regions.mjs" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\src\data\specialty-types.mjs" Link="wwwroot\src\data\specialty-types.mjs" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\node_modules\ckeditor5\dist\browser\ckeditor5.js" Link="wwwroot\vendor\ckeditor5\ckeditor5.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\browser\ckeditor5.js')" />
|
||||
<Content Include="..\..\node_modules\ckeditor5\dist\browser\ckeditor5.css" Link="wwwroot\vendor\ckeditor5\ckeditor5.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\browser\ckeditor5.css')" />
|
||||
<Content Include="..\..\node_modules\ckeditor5\dist\translations\zh-cn.js" Link="wwwroot\vendor\ckeditor5\translations\zh-cn.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\translations\zh-cn.js')" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace Eis.Web.Frontend;
|
||||
@@ -22,11 +21,12 @@ public static class FrontendAssets
|
||||
|
||||
public static void MapFrontendAssets(this WebApplication app)
|
||||
{
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles(CreateStaticOptions());
|
||||
|
||||
var repositoryRoot = FindRepositoryRoot(app.Environment.ContentRootPath);
|
||||
if (repositoryRoot is null)
|
||||
{
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles(CreateStaticOptions());
|
||||
app.MapFallbackToFile("index.html");
|
||||
return;
|
||||
}
|
||||
@@ -42,23 +42,6 @@ public static class FrontendAssets
|
||||
MapFile(app, "/styles.css", Path.Combine(repositoryRoot, "styles.css"), "text/css; charset=utf-8");
|
||||
MapFile(app, "/app.js", Path.Combine(repositoryRoot, "app.js"), "text/javascript; charset=utf-8");
|
||||
|
||||
var ckeditorRoot = Path.Combine(repositoryRoot, "node_modules", "ckeditor5", "dist");
|
||||
if (Directory.Exists(ckeditorRoot))
|
||||
{
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "browser")),
|
||||
RequestPath = "/vendor/ckeditor5",
|
||||
OnPrepareResponse = SetNoCache
|
||||
});
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "translations")),
|
||||
RequestPath = "/vendor/ckeditor5/translations",
|
||||
OnPrepareResponse = SetNoCache
|
||||
});
|
||||
}
|
||||
|
||||
var indexPath = Path.Combine(repositoryRoot, "index.html");
|
||||
MapFile(app, "/", indexPath, "text/html; charset=utf-8");
|
||||
MapFile(app, "/index.html", indexPath, "text/html; charset=utf-8");
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Eis.Web.Legacy;
|
||||
|
||||
public sealed class LegacyApiProxy(
|
||||
HttpClient httpClient,
|
||||
IOptions<LegacyNodeOptions> options,
|
||||
ILogger<LegacyApiProxy> logger)
|
||||
{
|
||||
private static readonly HashSet<string> HopByHopHeaders = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"TE",
|
||||
"Trailer",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade"
|
||||
};
|
||||
|
||||
private readonly LegacyNodeOptions _options = options.Value;
|
||||
|
||||
public async Task ForwardAsync(HttpContext context)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status501NotImplemented;
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
ok = false,
|
||||
message = "该接口尚未迁移到 ASP.NET Core",
|
||||
migration = new { native = false, legacyProxyEnabled = false }
|
||||
}, context.RequestAborted);
|
||||
return;
|
||||
}
|
||||
|
||||
var target = new Uri(_options.BaseUrl, $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}");
|
||||
using var outbound = CreateRequest(context, target);
|
||||
|
||||
try
|
||||
{
|
||||
using var upstream = await httpClient.SendAsync(
|
||||
outbound,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
context.RequestAborted);
|
||||
|
||||
context.Response.StatusCode = (int)upstream.StatusCode;
|
||||
CopyResponseHeaders(upstream, context.Response);
|
||||
|
||||
if (context.Request.Method != HttpMethods.Head && upstream.StatusCode != HttpStatusCode.NoContent)
|
||||
{
|
||||
await upstream.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Legacy Node API at {Target} is unavailable", target);
|
||||
if (context.Response.HasStarted)
|
||||
{
|
||||
context.Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
ok = false,
|
||||
message = "迁移期间的旧版 API 服务暂时不可用",
|
||||
migration = new { native = false, legacyProxyEnabled = true }
|
||||
}, context.RequestAborted);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await httpClient.GetAsync("api/public/home", cancellationToken);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateRequest(HttpContext context, Uri target)
|
||||
{
|
||||
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), target);
|
||||
var hasBody = context.Request.ContentLength > 0 || context.Request.Headers.TransferEncoding.Count > 0;
|
||||
if (hasBody)
|
||||
{
|
||||
request.Content = new StreamContent(context.Request.Body);
|
||||
}
|
||||
|
||||
foreach (var (name, values) in context.Request.Headers)
|
||||
{
|
||||
if (name.Equals("Host", StringComparison.OrdinalIgnoreCase) || HopByHopHeaders.Contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var valueArray = values.ToArray();
|
||||
if (!request.Headers.TryAddWithoutValidation(name, valueArray) && request.Content is not null)
|
||||
{
|
||||
request.Content.Headers.TryAddWithoutValidation(name, valueArray);
|
||||
}
|
||||
}
|
||||
|
||||
request.Headers.TryAddWithoutValidation("X-Forwarded-Host", context.Request.Host.Value);
|
||||
request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", context.Request.Scheme);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static void CopyResponseHeaders(HttpResponseMessage upstream, HttpResponse response)
|
||||
{
|
||||
foreach (var header in upstream.Headers.Concat(upstream.Content.Headers))
|
||||
{
|
||||
if (!HopByHopHeaders.Contains(header.Key))
|
||||
{
|
||||
response.Headers.Append(header.Key, header.Value.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
response.Headers.Remove("transfer-encoding");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Eis.Web.Legacy;
|
||||
|
||||
public sealed class LegacyNodeOptions
|
||||
{
|
||||
public const string SectionName = "LegacyNode";
|
||||
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
public Uri BaseUrl { get; init; } = new("http://127.0.0.1:4174");
|
||||
}
|
||||
+14
-25
@@ -1,4 +1,3 @@
|
||||
using System.Net;
|
||||
using Eis.Infrastructure.Authentication;
|
||||
using Eis.Infrastructure.Administration;
|
||||
using Eis.Infrastructure.Candidate;
|
||||
@@ -13,7 +12,6 @@ using Eis.Web.Authentication;
|
||||
using Eis.Web.Administration;
|
||||
using Eis.Web.Candidate;
|
||||
using Eis.Web.Frontend;
|
||||
using Eis.Web.Legacy;
|
||||
using Eis.Web.Public;
|
||||
|
||||
var applicationRoot = ApplicationPaths.FindApplicationRoot();
|
||||
@@ -21,19 +19,6 @@ EnvironmentFile.Load(applicationRoot);
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);
|
||||
builder.Services.Configure<LegacyNodeOptions>(builder.Configuration.GetSection(LegacyNodeOptions.SectionName));
|
||||
builder.Services.AddHttpClient<LegacyApiProxy>((services, client) =>
|
||||
{
|
||||
var options = services.GetRequiredService<Microsoft.Extensions.Options.IOptions<LegacyNodeOptions>>().Value;
|
||||
client.BaseAddress = options.BaseUrl;
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Eis.AspNetCore.Migration/1.0");
|
||||
}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
AutomaticDecompression = DecompressionMethods.None,
|
||||
UseCookies = false
|
||||
});
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddSingleton<IPublicSiteConfiguration, PublicSiteConfiguration>();
|
||||
var authenticationOptions = AuthenticationOptions.FromEnvironment(
|
||||
@@ -68,6 +53,7 @@ builder.Services.AddEisInfrastructure(
|
||||
adminMigrationOptions);
|
||||
|
||||
var app = builder.Build();
|
||||
await app.Services.InitializeEisDatabaseAsync();
|
||||
app.Services.EnsureNativeAuthenticationReady(authenticationOptions);
|
||||
|
||||
app.UseExceptionHandler();
|
||||
@@ -86,17 +72,13 @@ app.MapGet("/health/live", () => Results.Json(new
|
||||
framework = ".NET 10"
|
||||
}));
|
||||
|
||||
app.MapGet("/health/migration", async (
|
||||
LegacyApiProxy proxy,
|
||||
IApplicationCache cache,
|
||||
CancellationToken cancellationToken) =>
|
||||
app.MapGet("/health/migration", (IApplicationCache cache) =>
|
||||
{
|
||||
var legacyAvailable = await proxy.IsAvailableAsync(cancellationToken);
|
||||
var statusCode = legacyAvailable ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||
return Results.Json(new
|
||||
{
|
||||
status = legacyAvailable ? "healthy" : "degraded",
|
||||
legacyApiAvailable = legacyAvailable,
|
||||
status = "healthy",
|
||||
legacyApiAvailable = false,
|
||||
legacyApiRemoved = true,
|
||||
cache = new
|
||||
{
|
||||
status = cache.Status,
|
||||
@@ -202,7 +184,7 @@ app.MapGet("/health/migration", async (
|
||||
.ToArray()
|
||||
},
|
||||
features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled, candidateMigrationOptions.NativeEnabled)
|
||||
}, statusCode: statusCode);
|
||||
}, statusCode: StatusCodes.Status200OK);
|
||||
});
|
||||
|
||||
app.MapNativePublicEndpoints();
|
||||
@@ -220,7 +202,14 @@ string[] methods =
|
||||
HttpMethods.Delete,
|
||||
HttpMethods.Options
|
||||
];
|
||||
app.MapMethods("/api/{**path}", methods, (HttpContext context, LegacyApiProxy proxy) => proxy.ForwardAsync(context));
|
||||
app.MapMethods("/api/{**path}", methods, (HttpContext context) =>
|
||||
{
|
||||
context.Response.Headers.CacheControl = "no-store";
|
||||
context.Response.Headers["X-EIS-Implementation"] = "aspnet-core";
|
||||
return Results.Json(
|
||||
new { ok = false, message = "API 接口不存在" },
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
});
|
||||
|
||||
app.MapFrontendAssets();
|
||||
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
{
|
||||
"LegacyNode": {
|
||||
"Enabled": true,
|
||||
"BaseUrl": "http://127.0.0.1:4174"
|
||||
},
|
||||
"AuthenticationMigration": {
|
||||
"NativeEnabled": false
|
||||
"NativeEnabled": true
|
||||
},
|
||||
"CandidateMigration": {
|
||||
"NativeEnabled": false
|
||||
"NativeEnabled": true
|
||||
},
|
||||
"AdminMigration": {
|
||||
"NativeReadsEnabled": false,
|
||||
"NativeOrganizationWritesEnabled": false,
|
||||
"NativeAccountBatchesEnabled": false,
|
||||
"NativeConfigurationEnabled": false,
|
||||
"NativeNoticeManagementEnabled": false,
|
||||
"NativeCentersEnabled": false,
|
||||
"NativeOperationalReadsEnabled": false,
|
||||
"NativeCandidateManagementEnabled": false,
|
||||
"NativeRegistrationPaymentWritesEnabled": false,
|
||||
"NativeWorkflowOperationsEnabled": false,
|
||||
"NativeExamManagementEnabled": false,
|
||||
"NativeArrangementsEnabled": false,
|
||||
"NativeResultsEnabled": false,
|
||||
"NativeAdmissionsEnabled": false
|
||||
"NativeReadsEnabled": true,
|
||||
"NativeOrganizationWritesEnabled": true,
|
||||
"NativeAccountBatchesEnabled": true,
|
||||
"NativeConfigurationEnabled": true,
|
||||
"NativeNoticeManagementEnabled": true,
|
||||
"NativeCentersEnabled": true,
|
||||
"NativeOperationalReadsEnabled": true,
|
||||
"NativeCandidateManagementEnabled": true,
|
||||
"NativeRegistrationPaymentWritesEnabled": true,
|
||||
"NativeWorkflowOperationsEnabled": true,
|
||||
"NativeExamManagementEnabled": true,
|
||||
"NativeArrangementsEnabled": true,
|
||||
"NativeResultsEnabled": true,
|
||||
"NativeAdmissionsEnabled": true
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+188
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user