已完成本轮迁移:管理后台、审批流和考务编排的业务 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
+2
View File
@@ -51,6 +51,8 @@ ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED=false
ADMIN_NATIVE_ARRANGEMENTS_ENABLED=false ADMIN_NATIVE_ARRANGEMENTS_ENABLED=false
# 成绩工作区、成绩录入发布、特征分、复议及成绩台账导出;必须同时启用管理端基础读取和业务读取。 # 成绩工作区、成绩录入发布、特征分、复议及成绩台账导出;必须同时启用管理端基础读取和业务读取。
ADMIN_NATIVE_RESULTS_ENABLED=false ADMIN_NATIVE_RESULTS_ENABLED=false
# 指标资格、招生账户、志愿设置、计划审核、投档录取、报到补录与退档监督。
ADMIN_NATIVE_ADMISSIONS_ENABLED=false
# 仅在首次创建空数据库时使用。部署前务必修改初始密码。 # 仅在首次创建空数据库时使用。部署前务必修改初始密码。
INITIAL_ADMIN_USERNAME=admin INITIAL_ADMIN_USERNAME=admin
+5 -4
View File
@@ -12,7 +12,7 @@
- [x] 招生公示与 HMAC 文书验真公开接口 - [x] 招生公示与 HMAC 文书验真公开接口
- [x] 登录、自主注册、Session 与 TOTP(兼容开关默认关闭) - [x] 登录、自主注册、Session 与 TOTP(兼容开关默认关闭)
- [x] 考生业务 - [x] 考生业务
- [ ] 管理后台、审批流和考务编排(管理端基础读取、组织维护、批量报名号审批、流程配置、通知公告、考点变更审批、考生/报名/缴费读取、考生账号维护、报名缴费写入、工作流调度、考试维护、准考证编排导出及成绩管理已原生化) - [x] 管理后台、审批流和考务编排
- [x] 考生志愿填报与招生录取查询 - [x] 考生志愿填报与招生录取查询
- [ ] Excel、文书和缓存 - [ ] Excel、文书和缓存
- [ ] 容器入口切换及 Node.js 后端移除 - [ ] 容器入口切换及 Node.js 后端移除
@@ -54,9 +54,9 @@ $env:AUTH_NATIVE_ENABLED = 'true'
$env:CANDIDATE_NATIVE_ENABLED = 'true' $env:CANDIDATE_NATIVE_ENABLED = 'true'
``` ```
管理后台第一批只读接口(管理上下文、仪表盘、学校、学校组织、管理员和考试列表)已经原生化,并保留超级、校级、班级管理员的权限与数据作用域。第二批覆盖学校、班级和管理员的创建与维护、管理员密码重置及自主注册开关;更新操作与审计日志在同一事务中提交,停用或重置管理员会同步失效其会话。第三批覆盖批量报名号申领的读取、提交和审批,终审会按照当前号码规则原子生成考生账号、初始密码和待补录资料。第四批覆盖报名号规则及审批流程定义的读取与维护,并保留流程层级和批量申领终审约束。第五批覆盖完整通知公告管理:超级管理员可读取手工公告与五类系统公示、创建和编辑手工公告,并控制系统公示是否公开显示;HTML 净化、显隐更新和审计日志均由 ASP.NET Core 原生处理。第六批覆盖考点考场读取、新增与修改申请、重复待审拦截及多级审批;终审会在同一事务中更新流程、审计日志和正式考点考场档案。第七批覆盖考生档案、报名记录和已审核报名的缴费记录读取,并保持三级管理员的数据作用域、证件号脱敏、考试科目及审批流投影与 Node 一致。第八批覆盖校级按班级或年级归档及恢复考生账户、超级管理员重置考生密码、三级管理员按配置流程审核考生资料;账号与流程更新、审计日志在同一事务中提交,归档和密码重置会同步失效相关 ASP.NET Core 会话。第九批覆盖报名审核流程及缴费状态更新:多步报名终审会绑定考生报名号和当前号码规则,缴费确认及撤销会维护确认时间和操作人,所有流程、业务状态与审计日志均在同一事务中提交。第十批覆盖五类业务的工作流收件箱、当前处理人同级转交以及超级管理员指定步骤和处理人的监督调整;重新打开业务时会同步重置业务状态,并阻止已生成账号的批次重复开启。第十一批覆盖超级管理员创建和修改考试、配置科目与合格线、切换发布状态及不可逆归档;考试与科目替换、归档锁定和审计日志在同一事务中提交,并在已有报名或待处理成绩复议时保留原系统的安全拦截。第十二批覆盖准考证全场预检与正式编排:支持五级混编范围、稳定种子、固定考点、逐科考场座位及四种号码规则;重新编排会在同一事务中替换整场计划、准考证、科目座位和审计日志。超级、校级、班级管理员仍按原范围读取和批量打印准考证,校级管理员可直接从 .NET 导出桌贴、门贴和考场签名单 Excel。第十三批覆盖按考试延迟加载的成绩工作区、单条及批量成绩草稿/发布、特征分维护、成绩复议多级流转和成绩台账 Excel 导出;排名、单科达线、归档锁定、数据范围和写入审计均由 ASP.NET Core 原生处理 管理后台第一至十三批已经覆盖基础读取、组织账户、批量建号、流程定义与收件箱、通知公示、考点审批、报名缴费、考试维护、准考证编排和成绩管理。第十四批补齐指标资格确认、招生学校账号、志愿设置、招生计划审核、分数优先投档、逐轮录取公示、报到补录审批、退档监督和成绩预览提交;管理后台、审批流及考务编排的业务 API 至此全部由 ASP.NET Core 原生处理。Excel 文件解析、文书生成和缓存替换仍作为独立迁移阶段
其余审批流和考务编排接口仍转发给 Node,因此管理端开关都要求原生认证和共享 Redis;各写入子功能还必须与只读开关一起启用 管理端各功能域均可独立切换;在兼容运行期仍要求原生认证和共享 Redis
```powershell ```powershell
$env:AUTH_NATIVE_ENABLED = 'true' $env:AUTH_NATIVE_ENABLED = 'true'
@@ -73,9 +73,10 @@ $env:ADMIN_NATIVE_WORKFLOW_OPERATIONS_ENABLED = 'true'
$env:ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED = 'true' $env:ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED = 'true'
$env:ADMIN_NATIVE_ARRANGEMENTS_ENABLED = 'true' $env:ADMIN_NATIVE_ARRANGEMENTS_ENABLED = 'true'
$env:ADMIN_NATIVE_RESULTS_ENABLED = 'true' $env:ADMIN_NATIVE_RESULTS_ENABLED = 'true'
$env:ADMIN_NATIVE_ADMISSIONS_ENABLED = 'true'
``` ```
旧版 `ADMIN_NATIVE_NOTICE_WRITES_ENABLED` 仍可作为兼容别名使用。`GET /health/migration` 的各 `administration.native*Enabled` 字段和 `administration.nativeRoutes` 会报告这些端点是否已切换,其中成绩对应 `administration.nativeResultsEnabled` 旧版 `ADMIN_NATIVE_NOTICE_WRITES_ENABLED` 仍可作为兼容别名使用。`GET /health/migration` 的各 `administration.native*Enabled` 字段和 `administration.nativeRoutes` 会报告这些端点是否已切换,其中成绩与招生管理域分别对应 `administration.nativeResultsEnabled``administration.nativeAdmissionsEnabled`
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试: 完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
+65
View File
@@ -485,6 +485,7 @@ try {
ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED = 'true' ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED = 'true'
ADMIN_NATIVE_ARRANGEMENTS_ENABLED = 'true' ADMIN_NATIVE_ARRANGEMENTS_ENABLED = 'true'
ADMIN_NATIVE_RESULTS_ENABLED = 'true' ADMIN_NATIVE_RESULTS_ENABLED = 'true'
ADMIN_NATIVE_ADMISSIONS_ENABLED = 'true'
ADMIN_NATIVE_ALLOW_MEMORY = 'true' ADMIN_NATIVE_ALLOW_MEMORY = 'true'
LegacyNode__Enabled = 'true' LegacyNode__Enabled = 'true'
LegacyNode__BaseUrl = $legacyBaseUrl LegacyNode__BaseUrl = $legacyBaseUrl
@@ -851,6 +852,54 @@ try {
throw 'Native admins route did not preserve the class-admin boundary' throw 'Native admins route did not preserve the class-admin boundary'
} }
$legacyAdmissions = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/admissions" -WebSession $session
$nativeAdmissions = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions" -WebSession $nativeSession
if ($nativeAdmissions.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native admission management did not use ASP.NET Core'
}
Assert-JsonEquivalent -Expected $legacyAdmissions.Content -Actual $nativeAdmissions.Content -Label 'Admission management workspace'
$schoolAdmissionsForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
if ($schoolAdmissionsForbidden.StatusCode -ne 403) {
throw 'Native admission management did not preserve the super-admin boundary'
}
$legacyQualifications = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/indicator-qualifications" -WebSession $legacySchoolSession
$nativeQualifications = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/indicator-qualifications" -WebSession $nativeSchoolSession
if ($nativeQualifications.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native indicator-qualification workspace did not use ASP.NET Core'
}
Assert-JsonEquivalent -Expected $legacyQualifications.Content -Actual $nativeQualifications.Content -Label 'Indicator qualification workspace'
$invalidAdmissionSetting = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions/missing-exam/setting" -Method Put -ContentType 'application/json' -Body '{}' -WebSession $nativeSession -SkipHttpErrorCheck
if ($invalidAdmissionSetting.StatusCode -ne 404 -or $invalidAdmissionSetting.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native admission setting did not reject a missing exam'
}
$nativeAdmissionState = $nativeAdmissions.Content | ConvertFrom-Json
$admissionExam = @($nativeAdmissionState.exams | Select-Object -First 1)[0]
$admissionSchool = @($nativeAdmissionState.admissionSchools | Select-Object -First 1)[0]
if ($null -ne $admissionExam -and $null -ne $admissionSchool) {
$invalidAdmissionPlanBody = @{
examId = $admissionExam.id
schoolId = $admissionSchool.id
categories = @(
@{
code = 'invalid-specialty'
name = '无效特长类别'
quota = 1
specialtyCategory = 'sports'
specialtyType = 'vocal_music'
indicatorAllocations = @()
}
)
} | ConvertTo-Json -Depth 6 -Compress
$invalidAdmissionPlan = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-plans" -Method Post -ContentType 'application/json' -Body $invalidAdmissionPlanBody -WebSession $nativeSession -SkipHttpErrorCheck
if ($invalidAdmissionPlan.StatusCode -ne 400 -or $invalidAdmissionPlan.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native admission plan accepted a mismatched specialty category and type'
}
}
$unsafeAdminDelete = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admins/unsafe-delete" -Method Delete -WebSession $nativeSession -SkipHttpErrorCheck
if ($unsafeAdminDelete.StatusCode -ne 405 -or $unsafeAdminDelete.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native admin management did not preserve the no-delete safety boundary'
}
$arrangementSessions = @( $arrangementSessions = @(
@{ Level = 'super'; Legacy = $session; Native = $nativeSession }, @{ Level = 'super'; Legacy = $session; Native = $nativeSession },
@{ Level = 'school'; Legacy = $legacySchoolSession; Native = $nativeSchoolSession }, @{ Level = 'school'; Legacy = $legacySchoolSession; Native = $nativeSchoolSession },
@@ -973,6 +1022,21 @@ try {
if ($invalidResult.StatusCode -ne 400) { if ($invalidResult.StatusCode -ne 400) {
throw 'Native result write accepted a score above the subject full score' throw 'Native result write accepted a score above the subject full score'
} }
$invalidResultImportBody = @{
rows = @(
@{
candidateNumber = ''
examCode = ''
subjectName = ''
score = 0
published = $false
}
)
} | ConvertTo-Json -Depth 4 -Compress
$invalidResultImport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results/import" -Method Post -ContentType 'application/json' -Body $invalidResultImportBody -WebSession $nativeSession -SkipHttpErrorCheck
if ($invalidResultImport.StatusCode -ne 400 -or $invalidResultImport.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
throw 'Native result import accepted an invalid preview row'
}
$score = [Math]::Min([double] $resultSubject.fullScore, 88.5) $score = [Math]::Min([double] $resultSubject.fullScore, 88.5)
$bulkDraftBody = @{ $bulkDraftBody = @{
examId = $arrangementExam.id examId = $arrangementExam.id
@@ -1832,6 +1896,7 @@ try {
NativeAdminExamManagement = 'passed' NativeAdminExamManagement = 'passed'
NativeAdminArrangements = 'passed' NativeAdminArrangements = 'passed'
NativeAdminResults = 'passed' NativeAdminResults = 'passed'
NativeAdminAdmissions = 'passed'
} | Format-List } | Format-List
} }
finally { finally {
@@ -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> GetAsync(string sessionToken, string examId, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken); Task<AdminEndpointResult> SaveAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveBulkAsync(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> SaveFeatureBulkAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
Task<AdminEndpointResult> SaveFeatureAsync(string sessionToken, string registrationId, 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); 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 NativeWorkflowOperationsEnabled = false,
bool NativeExamManagementEnabled = false, bool NativeExamManagementEnabled = false,
bool NativeArrangementsEnabled = false, bool NativeArrangementsEnabled = false,
bool NativeResultsEnabled = false) bool NativeResultsEnabled = false,
bool NativeAdmissionsEnabled = false)
{ {
public static AdminMigrationOptions FromEnvironment( public static AdminMigrationOptions FromEnvironment(
bool configuredNativeReadsEnabled, bool configuredNativeReadsEnabled,
@@ -30,7 +31,8 @@ public sealed record AdminMigrationOptions(
bool configuredNativeWorkflowOperationsEnabled = false, bool configuredNativeWorkflowOperationsEnabled = false,
bool configuredNativeExamManagementEnabled = false, bool configuredNativeExamManagementEnabled = false,
bool configuredNativeArrangementsEnabled = false, bool configuredNativeArrangementsEnabled = false,
bool configuredNativeResultsEnabled = false) bool configuredNativeResultsEnabled = false,
bool configuredNativeAdmissionsEnabled = false)
{ {
var readsEnabled = ParseBoolean( var readsEnabled = ParseBoolean(
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"), Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
@@ -72,17 +74,20 @@ public sealed record AdminMigrationOptions(
var resultsEnabled = ParseBoolean( var resultsEnabled = ParseBoolean(
Environment.GetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED"), Environment.GetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED"),
configuredNativeResultsEnabled); 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( throw new InvalidOperationException(
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true"); "启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
} }
if ((candidateManagementEnabled || registrationPaymentWritesEnabled || resultsEnabled) && !operationalReadsEnabled) if ((candidateManagementEnabled || registrationPaymentWritesEnabled || resultsEnabled || admissionsEnabled) && !operationalReadsEnabled)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"启用原生考生、报名或缴费写接口前必须同时设置 ADMIN_NATIVE_OPERATIONAL_READS_ENABLED=true"); "启用原生考生、报名或缴费写接口前必须同时设置 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) if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@@ -98,7 +103,7 @@ public sealed record AdminMigrationOptions(
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话"); "管理端仍有接口需要转发给 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 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( public async Task<AdminEndpointResult> SaveFeatureBulkAsync(
string sessionToken, string sessionToken,
JsonObject body, JsonObject body,
@@ -862,6 +960,8 @@ internal sealed class AdminResultService(
double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) double.TryParse(Text(node), NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
? value ? value
: double.NaN; : 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) => private static string Text(JsonNode? node) =>
node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? ""; node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? "";
private static string Clean(string value, int maximum) private static string Clean(string value, int maximum)
@@ -898,6 +998,13 @@ internal sealed class AdminResultService(
OperationalRegistration Registration, OperationalRegistration Registration,
AdminWorkflowResult? Existing, AdminWorkflowResult? Existing,
AdminWorkflowResult Result); 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 ResultSummary(bool Complete, bool? Qualified);
private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error) private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error)
{ {
@@ -67,6 +67,7 @@ public static class DependencyInjection
services.AddScoped<AdminArrangementSnapshotLoader>(); services.AddScoped<AdminArrangementSnapshotLoader>();
services.AddScoped<AdminArrangementRepository>(); services.AddScoped<AdminArrangementRepository>();
services.AddScoped<AdminResultRepository>(); services.AddScoped<AdminResultRepository>();
services.AddScoped<AdminAdmissionRepository>();
services.AddScoped<IAdminReadService, AdminReadService>(); services.AddScoped<IAdminReadService, AdminReadService>();
services.AddScoped<IAdminOrganizationService, AdminOrganizationService>(); services.AddScoped<IAdminOrganizationService, AdminOrganizationService>();
services.AddScoped<IAdminAccountBatchService, AdminAccountBatchService>(); services.AddScoped<IAdminAccountBatchService, AdminAccountBatchService>();
@@ -80,6 +81,7 @@ public static class DependencyInjection
services.AddScoped<IAdminExamManagementService, AdminExamManagementService>(); services.AddScoped<IAdminExamManagementService, AdminExamManagementService>();
services.AddScoped<IAdminArrangementService, AdminArrangementService>(); services.AddScoped<IAdminArrangementService, AdminArrangementService>();
services.AddScoped<IAdminResultService, AdminResultService>(); services.AddScoped<IAdminResultService, AdminResultService>();
services.AddScoped<IAdminAdmissionService, AdminAdmissionService>();
services.AddSingleton<NoticeContentFormatter>(); services.AddSingleton<NoticeContentFormatter>();
services.AddScoped<IPublicQueryService, PublicQueryService>(); services.AddScoped<IPublicQueryService, PublicQueryService>();
return services; return services;
@@ -181,6 +181,9 @@ public static class NativeAdminReadEndpoints
endpoints.MapPost("/api/admin/results/bulk", endpoints.MapPost("/api/admin/results/bulk",
(HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) => (HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveBulkAsync(Token(context), body, 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", endpoints.MapPost("/api/admin/feature-scores/bulk",
(HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) => (HttpContext context, JsonObject body, IAdminResultService service, CancellationToken cancellationToken) =>
Execute(context, service.SaveFeatureBulkAsync(Token(context), body, cancellationToken))); Execute(context, service.SaveFeatureBulkAsync(Token(context), body, cancellationToken)));
@@ -202,6 +205,55 @@ public static class NativeAdminReadEndpoints
context.Request.Query["examId"].ToString(), context.Request.Query["examId"].ToString(),
cancellationToken))); 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; return endpoints;
} }
@@ -234,4 +286,13 @@ public static class NativeAdminReadEndpoints
private static string Token(HttpContext context) => private static string Token(HttpContext context) =>
context.Request.Cookies.TryGetValue("hz_session", out var token) ? token : string.Empty; 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:NativeWorkflowOperationsEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeExamManagementEnabled"), builder.Configuration.GetValue<bool>("AdminMigration:NativeExamManagementEnabled"),
builder.Configuration.GetValue<bool>("AdminMigration:NativeArrangementsEnabled"), 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( builder.Services.AddEisInfrastructure(
DatabaseOptions.FromEnvironment(applicationRoot, builder.Environment.IsProduction()), DatabaseOptions.FromEnvironment(applicationRoot, builder.Environment.IsProduction()),
DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()), DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()),
@@ -124,6 +125,7 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
nativeExamManagementEnabled = adminMigrationOptions.NativeExamManagementEnabled, nativeExamManagementEnabled = adminMigrationOptions.NativeExamManagementEnabled,
nativeArrangementsEnabled = adminMigrationOptions.NativeArrangementsEnabled, nativeArrangementsEnabled = adminMigrationOptions.NativeArrangementsEnabled,
nativeResultsEnabled = adminMigrationOptions.NativeResultsEnabled, nativeResultsEnabled = adminMigrationOptions.NativeResultsEnabled,
nativeAdmissionsEnabled = adminMigrationOptions.NativeAdmissionsEnabled,
nativeRoutes = (adminMigrationOptions.NativeReadsEnabled nativeRoutes = (adminMigrationOptions.NativeReadsEnabled
? new[] { "GET context", "GET dashboard", "GET schools", "GET school-organization", "GET admins", "GET exams" } ? 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" "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() .ToArray()
}, },
features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled, candidateMigrationOptions.NativeEnabled) features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled, candidateMigrationOptions.NativeEnabled)
+2 -1
View File
@@ -22,7 +22,8 @@
"NativeWorkflowOperationsEnabled": false, "NativeWorkflowOperationsEnabled": false,
"NativeExamManagementEnabled": false, "NativeExamManagementEnabled": false,
"NativeArrangementsEnabled": false, "NativeArrangementsEnabled": false,
"NativeResultsEnabled": false "NativeResultsEnabled": false,
"NativeAdmissionsEnabled": false
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
@@ -316,6 +316,21 @@ public sealed class AdminMigrationOptionsTests
}, operationalReadsEnabled: "true", resultsEnabled: "true"); }, operationalReadsEnabled: "true", resultsEnabled: "true");
} }
[Fact]
public void EnablesAdmissionsWithOperationalReadsAndSharedSessions()
{
WithEnvironment("true", null, null, () =>
{
var options = AdminMigrationOptions.FromEnvironment(
configuredNativeReadsEnabled: false,
authenticationNativeEnabled: true,
sharesLegacySessions: true);
Assert.True(options.NativeOperationalReadsEnabled);
Assert.True(options.NativeAdmissionsEnabled);
}, operationalReadsEnabled: "true", admissionsEnabled: "true");
}
private static void WithEnvironment( private static void WithEnvironment(
string? nativeReadsEnabled, string? nativeReadsEnabled,
string? nativeOrganizationWritesEnabled, string? nativeOrganizationWritesEnabled,
@@ -332,7 +347,8 @@ public sealed class AdminMigrationOptionsTests
string? workflowOperationsEnabled = null, string? workflowOperationsEnabled = null,
string? examManagementEnabled = null, string? examManagementEnabled = null,
string? arrangementsEnabled = null, string? arrangementsEnabled = null,
string? resultsEnabled = null) string? resultsEnabled = null,
string? admissionsEnabled = null)
{ {
var previousNativeReadsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"); var previousNativeReadsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED");
var previousNativeOrganizationWritesEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED"); var previousNativeOrganizationWritesEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED");
@@ -348,6 +364,7 @@ public sealed class AdminMigrationOptionsTests
var previousExamManagementEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED"); var previousExamManagementEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED");
var previousArrangementsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED"); var previousArrangementsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED");
var previousResultsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED"); var previousResultsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED");
var previousAdmissionsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ADMISSIONS_ENABLED");
var previousAllowMemory = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY"); var previousAllowMemory = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY");
try try
{ {
@@ -365,6 +382,7 @@ public sealed class AdminMigrationOptionsTests
Environment.SetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED", examManagementEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED", examManagementEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED", arrangementsEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED", arrangementsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED", resultsEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED", resultsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ADMISSIONS_ENABLED", admissionsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", allowMemory); Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", allowMemory);
test(); test();
} }
@@ -384,6 +402,7 @@ public sealed class AdminMigrationOptionsTests
Environment.SetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED", previousExamManagementEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED", previousExamManagementEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED", previousArrangementsEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_ARRANGEMENTS_ENABLED", previousArrangementsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED", previousResultsEnabled); Environment.SetEnvironmentVariable("ADMIN_NATIVE_RESULTS_ENABLED", previousResultsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ADMISSIONS_ENABLED", previousAdmissionsEnabled);
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", previousAllowMemory); Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", previousAllowMemory);
} }
} }