已完成本轮迁移:管理后台、审批流和考务编排的业务 API 已全部由 ASP.NET Core 10 原生处理。

主要完成:
管理端招生工作台、指标资格、招生学校账号。
招生计划审核、分数优先多志愿投档、退档监督。
逐轮录取公示、报到审批、补录轮次推进、最低录取线公示。
成绩预览导入与管理员账号禁止物理删除。
新增迁移开关及健康状态:[MIGRATION.md (line 57)](C:\\Users\\BI\\Documents\\EIS-dotnet\\MIGRATION.md:57)
核心实现:[AdminAdmissionService.cs (line 10)](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Administration\\AdminAdmissionService.cs:10)
原生路由:[NativeAdminReadEndpoints.cs (line 209)](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Web\\Administration\\NativeAdminReadEndpoints.cs:209)
This commit is contained in:
2026-07-23 13:08:17 +08:00 Unverified
parent d47c614b53
commit 4df6303153
14 changed files with 1810 additions and 13 deletions
@@ -0,0 +1,20 @@
using System.Text.Json.Nodes;
namespace Eis.Application.Administration;
public interface IAdminAdmissionService
{
Task<AdminEndpointResult> GetAsync(string sessionToken, CancellationToken cancellationToken);
Task<AdminEndpointResult> GetQualificationsAsync(string sessionToken, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveQualificationAsync(string sessionToken, string examId, string userId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveQualificationsAsync(string sessionToken, string examId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> CreateAccountAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> UpdateAccountAsync(string sessionToken, string accountId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ResetAccountPasswordAsync(string sessionToken, string accountId, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveSettingAsync(string sessionToken, string examId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SavePlanAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ReviewPlanAsync(string sessionToken, string planId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> RunActionAsync(string sessionToken, string examId, string action, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ReviewReportingAsync(string sessionToken, string recordId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ReviewWithdrawalAsync(string sessionToken, string placementId, JsonObject body, CancellationToken cancellationToken);
}
@@ -7,6 +7,7 @@ public interface IAdminResultService
Task<AdminEndpointResult> GetAsync(string sessionToken, string examId, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveBulkAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ImportAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveFeatureBulkAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveFeatureAsync(string sessionToken, string registrationId, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> ReviewAppealAsync(string sessionToken, string resultId, JsonObject body, CancellationToken cancellationToken);
@@ -0,0 +1,133 @@
using System.Data.Common;
using Eis.Infrastructure.Candidate;
using Eis.Infrastructure.Data;
namespace Eis.Infrastructure.Administration;
internal sealed class AdminAdmissionRepository(IRelationalConnectionFactory connectionFactory)
{
public Task SaveRecordsAsync(
IReadOnlyList<CandidateAdmissionRecord> records,
AdminAuditEntry audit,
CancellationToken cancellationToken) =>
TransactionAsync(async (connection, transaction) =>
{
foreach (var record in records)
{
await ExecuteAsync(connection, transaction, "DELETE FROM admission_records WHERE id = @id",
[new("@id", record.Id)], cancellationToken);
await ExecuteAsync(connection, transaction,
"""
INSERT INTO admission_records (
id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at
) VALUES (
@id, @kind, @examId, @userId, @schoolId, @status, @payload, @createdAt, @updatedAt
)
""",
[
new("@id", record.Id), new("@kind", record.Kind), new("@examId", record.ExamId),
new("@userId", record.UserId), new("@schoolId", record.SchoolId),
new("@status", record.Status), new("@payload", record.Payload.ToJsonString()),
new("@createdAt", record.CreatedAt), new("@updatedAt", record.UpdatedAt)
], cancellationToken);
}
await InsertAuditAsync(connection, transaction, audit, cancellationToken);
}, cancellationToken);
public Task CreateAccountAsync(
AdminUser account,
string passwordHash,
AdminAuditEntry audit,
CancellationToken cancellationToken) =>
TransactionAsync(async (connection, transaction) =>
{
await ExecuteAsync(connection, transaction,
"""
INSERT INTO users (
id, username, password_hash, role, admin_level, school_id, class_id,
display_name, active, must_change_password, created_at
) VALUES (
@id, @username, @passwordHash, 'admission_school', NULL, @schoolId, NULL,
@displayName, 1, 0, @createdAt
)
""",
[
new("@id", account.Id), new("@username", account.Username), new("@passwordHash", passwordHash),
new("@schoolId", account.SchoolId), new("@displayName", account.DisplayName),
new("@createdAt", account.CreatedAt)
], cancellationToken);
await InsertAuditAsync(connection, transaction, audit, cancellationToken);
}, cancellationToken);
public Task UpdateAccountAsync(
string accountId,
string displayName,
bool active,
string? passwordHash,
AdminAuditEntry audit,
CancellationToken cancellationToken) =>
TransactionAsync(async (connection, transaction) =>
{
await ExecuteAsync(connection, transaction,
passwordHash is null
? "UPDATE users SET display_name = @displayName, active = @active WHERE id = @id"
: "UPDATE users SET display_name = @displayName, active = @active, password_hash = @passwordHash WHERE id = @id",
[
new("@displayName", displayName), new("@active", active ? 1 : 0),
new("@passwordHash", passwordHash), new("@id", accountId)
], cancellationToken);
await InsertAuditAsync(connection, transaction, audit, cancellationToken);
}, cancellationToken);
private async Task TransactionAsync(
Func<DbConnection, DbTransaction, Task> operation,
CancellationToken cancellationToken)
{
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
await operation(connection, transaction);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
private static Task InsertAuditAsync(
DbConnection connection,
DbTransaction transaction,
AdminAuditEntry audit,
CancellationToken cancellationToken) =>
ExecuteAsync(connection, transaction,
"INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)",
[
new("@id", audit.Id), new("@actorId", audit.ActorId), new("@action", audit.Action),
new("@detail", audit.Detail), new("@createdAt", audit.CreatedAt)
], cancellationToken);
private static async Task ExecuteAsync(
DbConnection connection,
DbTransaction transaction,
string sql,
IReadOnlyList<SqlValue> values,
CancellationToken cancellationToken)
{
await using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = sql;
foreach (var value in values)
{
var parameter = command.CreateParameter();
parameter.ParameterName = value.Name;
parameter.Value = value.Value ?? DBNull.Value;
command.Parameters.Add(parameter);
}
await command.ExecuteNonQueryAsync(cancellationToken);
}
private sealed record SqlValue(string Name, object? Value);
}
File diff suppressed because it is too large Load Diff
@@ -13,7 +13,8 @@ public sealed record AdminMigrationOptions(
bool NativeWorkflowOperationsEnabled = false,
bool NativeExamManagementEnabled = false,
bool NativeArrangementsEnabled = false,
bool NativeResultsEnabled = false)
bool NativeResultsEnabled = false,
bool NativeAdmissionsEnabled = false)
{
public static AdminMigrationOptions FromEnvironment(
bool configuredNativeReadsEnabled,
@@ -30,7 +31,8 @@ public sealed record AdminMigrationOptions(
bool configuredNativeWorkflowOperationsEnabled = false,
bool configuredNativeExamManagementEnabled = false,
bool configuredNativeArrangementsEnabled = false,
bool configuredNativeResultsEnabled = false)
bool configuredNativeResultsEnabled = false,
bool configuredNativeAdmissionsEnabled = false)
{
var readsEnabled = ParseBoolean(
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
@@ -72,17 +74,20 @@ public sealed record AdminMigrationOptions(
var resultsEnabled = ParseBoolean(
Environment.GetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED"),
configuredNativeResultsEnabled);
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled || workflowOperationsEnabled || examManagementEnabled || arrangementsEnabled || resultsEnabled) && !readsEnabled)
var admissionsEnabled = ParseBoolean(
Environment.GetEnvironmentVariable("ADMIN_NATIVE_ADMISSIONS_ENABLED"),
configuredNativeAdmissionsEnabled);
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled || workflowOperationsEnabled || examManagementEnabled || arrangementsEnabled || resultsEnabled || admissionsEnabled) && !readsEnabled)
{
throw new InvalidOperationException(
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
}
if ((candidateManagementEnabled || registrationPaymentWritesEnabled || resultsEnabled) && !operationalReadsEnabled)
if ((candidateManagementEnabled || registrationPaymentWritesEnabled || resultsEnabled || admissionsEnabled) && !operationalReadsEnabled)
{
throw new InvalidOperationException(
"启用原生考生、报名或缴费写接口前必须同时设置 ADMIN_NATIVE_OPERATIONAL_READS_ENABLED=true");
}
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled || workflowOperationsEnabled || examManagementEnabled || arrangementsEnabled || resultsEnabled;
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled || workflowOperationsEnabled || examManagementEnabled || arrangementsEnabled || resultsEnabled || admissionsEnabled;
if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
{
throw new InvalidOperationException(
@@ -98,7 +103,7 @@ public sealed record AdminMigrationOptions(
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话");
}
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeManagementEnabled, centersEnabled, operationalReadsEnabled, candidateManagementEnabled, registrationPaymentWritesEnabled, workflowOperationsEnabled, examManagementEnabled, arrangementsEnabled, resultsEnabled);
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeManagementEnabled, centersEnabled, operationalReadsEnabled, candidateManagementEnabled, registrationPaymentWritesEnabled, workflowOperationsEnabled, examManagementEnabled, arrangementsEnabled, resultsEnabled, admissionsEnabled);
}
private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
@@ -244,6 +244,104 @@ internal sealed class AdminResultService(
});
}
public async Task<AdminEndpointResult> ImportAsync(
string sessionToken,
JsonObject body,
CancellationToken cancellationToken)
{
var context = await ResolveAsync(sessionToken, superOnly: true, cancellationToken);
if (context.Error is not null) return context.Error;
var user = context.User!;
var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
var allResults = (await resultSnapshotLoader.LoadAsync(cancellationToken)).ToList();
var rows = body["rows"] as JsonArray ?? [];
var prepared = new List<PreparedImport>();
var seen = new HashSet<string>(StringComparer.Ordinal);
for (var index = 0; index < rows.Count; index++)
{
var row = rows[index] as JsonObject ?? new JsonObject();
var sourceRow = Integer(row["__row"] ?? row["sourceRow"], index + 3);
var candidateNumber = Clean(Text(row["candidateNumber"]), 120);
var examCode = Clean(Text(row["examCode"]), 60);
var subjectName = Clean(Text(row["subjectName"]), 50);
var account = operational.Users.FirstOrDefault(item => item.CandidateNumber == candidateNumber);
var exam = operational.Exams.FirstOrDefault(item =>
Text(item.Data["code"]).Equals(examCode, StringComparison.OrdinalIgnoreCase));
var registration = operational.Registrations.FirstOrDefault(item =>
item.UserId == account?.Id && item.ExamId == exam?.Id && item.Status == "approved");
var subject = exam?.Subjects.FirstOrDefault(item =>
Text(item["name"]).Equals(subjectName, StringComparison.OrdinalIgnoreCase));
string? message = null;
if (candidateNumber.Length == 0) message = "报名号不能为空";
else if (account is null) message = "报名号不存在";
else if (examCode.Length == 0) message = "考试代码不能为空";
else if (exam is null) message = "考试代码不存在";
else if (exam.Data["archivedAt"] is not null) message = "该考试已归档,成绩已永久锁定";
else if (subjectName.Length == 0) message = "科目不能为空";
else if (subject is null) message = "该考试中不存在此科目";
else if (registration is null) message = "该考生没有已通过的本场考试报名";
else if (!SubjectIds(registration).Contains(Text(subject["id"]))) message = "该考生未报考此科目";
var score = Number(row["score"]);
if (message is null && (!double.IsFinite(score) || score < 0 || score > Number(subject?["fullScore"])))
message = $"成绩须在 0—{AdminResultScoring.Format(Number(subject?["fullScore"]))} 之间";
var publishText = Text(row["published"]);
bool published;
if (row["published"] is JsonValue publishedValue &&
publishedValue.TryGetValue<bool>(out var publishedBoolean))
published = publishedBoolean;
else if (publishText is "" or "不发布")
published = false;
else if (publishText == "发布")
published = true;
else
{
published = false;
message = "发布状态只能是“发布”或“不发布”";
}
var key = registration is null || subject is null
? $"{candidateNumber}|{examCode}|{subjectName}"
: $"{registration.Id}|{Text(subject["id"])}";
if (!seen.Add(key) && message is null) message = "同一考生、考试和科目在文件中重复";
if (message is not null) return Error(400, $"第 {sourceRow} 行:{message};请返回预览修正后重试");
var existing = allResults.FirstOrDefault(item =>
item.RegistrationId == registration!.Id && item.SubjectId == Text(subject!["id"]));
var now = NowIso();
var result = new AdminWorkflowResult(
existing?.Id ?? Uid("result"), registration!.Id, Text(subject!["id"]), score,
published ? "" : "待发布", published, now, published ? existing?.PublishedAt ?? now : null);
Replace(allResults, result);
prepared.Add(new(registration, exam!, subject!, existing, result, candidateNumber));
}
var writes = prepared.Select(item =>
{
var result = item.Result with
{
Grade = item.Result.Published ? AdminResultScoring.Rank(allResults, item.Result).Grade : "待发布"
};
return new ResultWrite(
result,
item.Existing is null,
Audit(user, result.Published ? "Excel 批量发布成绩" : "Excel 批量保存成绩",
$"{item.CandidateNumber} · {Text(item.Exam.Data["name"])} · {Text(item.Subject["name"])} · {AdminResultScoring.Format(result.Score)}"));
}).ToArray();
await repository.SaveManyAsync(writes, cancellationToken);
var create = prepared.Count(item => item.Existing is null);
return Success(new JsonObject
{
["ok"] = true,
["count"] = writes.Length,
["summary"] = new JsonObject
{
["total"] = writes.Length,
["valid"] = writes.Length,
["invalid"] = 0,
["create"] = create,
["update"] = writes.Length - create,
["publish"] = prepared.Count(item => item.Result.Published)
}
});
}
public async Task<AdminEndpointResult> SaveFeatureBulkAsync(
string sessionToken,
JsonObject body,
@@ -862,6 +960,8 @@ internal sealed class AdminResultService(
double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
? value
: double.NaN;
private static int Integer(JsonNode? node, int fallback = 0) =>
int.TryParse(Text(node), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
private static string Text(JsonNode? node) =>
node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? "";
private static string Clean(string value, int maximum)
@@ -898,6 +998,13 @@ internal sealed class AdminResultService(
OperationalRegistration Registration,
AdminWorkflowResult? Existing,
AdminWorkflowResult Result);
private sealed record PreparedImport(
OperationalRegistration Registration,
OperationalExam Exam,
JsonObject Subject,
AdminWorkflowResult? Existing,
AdminWorkflowResult Result,
string CandidateNumber);
private sealed record ResultSummary(bool Complete, bool? Qualified);
private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error)
{
@@ -67,6 +67,7 @@ public static class DependencyInjection
services.AddScoped<AdminArrangementSnapshotLoader>();
services.AddScoped<AdminArrangementRepository>();
services.AddScoped<AdminResultRepository>();
services.AddScoped<AdminAdmissionRepository>();
services.AddScoped<IAdminReadService, AdminReadService>();
services.AddScoped<IAdminOrganizationService, AdminOrganizationService>();
services.AddScoped<IAdminAccountBatchService, AdminAccountBatchService>();
@@ -80,6 +81,7 @@ public static class DependencyInjection
services.AddScoped<IAdminExamManagementService, AdminExamManagementService>();
services.AddScoped<IAdminArrangementService, AdminArrangementService>();
services.AddScoped<IAdminResultService, AdminResultService>();
services.AddScoped<IAdminAdmissionService, AdminAdmissionService>();
services.AddSingleton<NoticeContentFormatter>();
services.AddScoped<IPublicQueryService, PublicQueryService>();
return services;
@@ -181,6 +181,9 @@ public static class NativeAdminReadEndpoints
endpoints.MapPost("/api/admin/results/bulk",
(HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveBulkAsync(Token(context), body, cancellationToken)));
endpoints.MapPost("/api/admin/results/import",
(HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) =>
Execute(context, service.ImportAsync(Token(context), body, cancellationToken)));
endpoints.MapPost("/api/admin/feature-scores/bulk",
(HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveFeatureBulkAsync(Token(context), body, cancellationToken)));
@@ -202,6 +205,55 @@ public static class NativeAdminReadEndpoints
context.Request.Query["examId"].ToString(),
cancellationToken)));
}
if (options.NativeAdmissionsEnabled)
{
endpoints.MapGet("/api/admin/admissions",
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.GetAsync(Token(context), cancellationToken)));
endpoints.MapGet("/api/admin/indicator-qualifications",
(HttpContext context, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.GetQualificationsAsync(Token(context), cancellationToken)));
endpoints.MapPut("/api/admin/indicator-qualifications/{examId}/bulk",
(HttpContext context, string examId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveQualificationsAsync(Token(context), examId, body, cancellationToken)));
endpoints.MapPut("/api/admin/indicator-qualifications/{examId}/{userId}",
(HttpContext context, string examId, string userId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveQualificationAsync(Token(context), examId, userId, body, cancellationToken)));
endpoints.MapPost("/api/admin/admission-school-accounts",
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.CreateAccountAsync(Token(context), body, cancellationToken)));
endpoints.MapPatch("/api/admin/admission-school-accounts/{accountId}",
(HttpContext context, string accountId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.UpdateAccountAsync(Token(context), accountId, body, cancellationToken)));
endpoints.MapPost("/api/admin/admission-school-accounts/{accountId}/reset-password",
(HttpContext context, string accountId, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.ResetAccountPasswordAsync(Token(context), accountId, cancellationToken)));
endpoints.MapPut("/api/admin/admissions/{examId}/setting",
(HttpContext context, string examId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveSettingAsync(Token(context), examId, body, cancellationToken)));
endpoints.MapPost("/api/admin/admission-plans",
(HttpContext context, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.SavePlanAsync(Token(context), body, cancellationToken)));
endpoints.MapPatch("/api/admin/admission-plans/{planId}",
(HttpContext context, string planId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.ReviewPlanAsync(Token(context), planId, body, cancellationToken)));
endpoints.MapPost("/api/admin/admissions/{examId}/{action:regex(^(match|finalize|supplementary)$)}",
(HttpContext context, string examId, string action, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.RunActionAsync(Token(context), examId, action, body, cancellationToken)));
endpoints.MapPatch("/api/admin/admission-reporting/{recordId}",
(HttpContext context, string recordId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.ReviewReportingAsync(Token(context), recordId, body, cancellationToken)));
endpoints.MapPatch("/api/admin/admission-withdrawals/{placementId}",
(HttpContext context, string placementId, JsonObject body, IAdminAdmissionService service, CancellationToken cancellationToken) =>
Execute(context, service.ReviewWithdrawalAsync(Token(context), placementId, body, cancellationToken)));
}
if (options.NativeReadsEnabled)
{
endpoints.MapDelete("/api/admin/admins/{**path}", NativeDeleteNotAllowed);
endpoints.MapDelete("/api/admin/candidates/{**path}", NativeDeleteNotAllowed);
endpoints.MapDelete("/api/admin/candidate-accounts/{**path}", NativeDeleteNotAllowed);
}
return endpoints;
}
@@ -234,4 +286,13 @@ public static class NativeAdminReadEndpoints
private static string Token(HttpContext context) =>
context.Request.Cookies.TryGetValue("hz_session", out var token) ? token : string.Empty;
private static IResult NativeDeleteNotAllowed(HttpContext context)
{
context.Response.Headers.CacheControl = "no-store";
context.Response.Headers["X-EIS-Implementation"] = "aspnet-core";
return Results.Json(
new JsonObject { ["ok"] = false, ["message"] = "账户不得删除;考生账户请由校方归档,管理员账户可停用" },
statusCode: StatusCodes.Status405MethodNotAllowed);
}
}
+12 -1
View File
@@ -57,7 +57,8 @@ var adminMigrationOptions = AdminMigrationOptions.FromEnvironment(
builder.Configuration.GetValue<bool>("AdminMigration:NativeWorkflowOperationsEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeExamManagementEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeArrangementsEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeResultsEnabled"));
builder.Configuration.GetValue<bool>("AdminMigration:NativeResultsEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeAdmissionsEnabled"));
builder.Services.AddEisInfrastructure(
DatabaseOptions.FromEnvironment(applicationRoot, builder.Environment.IsProduction()),
DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()),
@@ -124,6 +125,7 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
nativeExamManagementEnabled = adminMigrationOptions.NativeExamManagementEnabled,
nativeArrangementsEnabled = adminMigrationOptions.NativeArrangementsEnabled,
nativeResultsEnabled = adminMigrationOptions.NativeResultsEnabled,
nativeAdmissionsEnabled = adminMigrationOptions.NativeAdmissionsEnabled,
nativeRoutes = (adminMigrationOptions.NativeReadsEnabled
? new[] { "GET context", "GET dashboard", "GET schools", "GET school-organization", "GET admins", "GET exams" }
: [])
@@ -175,6 +177,15 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
"PATCH score appeals", "GET result Excel export"
}
: [])
.Concat(adminMigrationOptions.NativeAdmissionsEnabled
? new[]
{
"GET admissions/indicator qualifications", "PUT qualification single/bulk",
"POST/PATCH/reset admission-school accounts", "PUT admission settings",
"POST/PATCH admission plans", "POST admission match/finalize/supplementary",
"PATCH admission reporting/withdrawals"
}
: [])
.ToArray()
},
features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled, candidateMigrationOptions.NativeEnabled)
+2 -1
View File
@@ -22,7 +22,8 @@
"NativeWorkflowOperationsEnabled": false,
"NativeExamManagementEnabled": false,
"NativeArrangementsEnabled": false,
"NativeResultsEnabled": false
"NativeResultsEnabled": false,
"NativeAdmissionsEnabled": false
},
"Logging": {
"LogLevel": {