已完成本轮迁移:管理后台、审批流和考务编排的业务 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:
@@ -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
@@ -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 冒烟测试:
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user