本批“考生账号维护”迁移完成:
POST /api/admin/candidate-accounts/archive
POST /api/admin/candidates/{profileId}/reset-password
PATCH /api/admin/candidates/{profileId}
实现了校级按班级/年级归档与恢复、超级管理员密码重置、多级资料审批、事务审计和会话失效。核心代码见
This commit is contained in:
1 parent
95a87f0276
commit
3f555929fd
12 files changed
+600
-12
No files matched your search
@@ -39,6 +39,8 @@ ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED=false
|
|||||||
ADMIN_NATIVE_CENTERS_ENABLED=false
|
ADMIN_NATIVE_CENTERS_ENABLED=false
|
||||||
# 考生档案、报名记录与缴费记录读取;必须同时启用管理端只读接口。
|
# 考生档案、报名记录与缴费记录读取;必须同时启用管理端只读接口。
|
||||||
ADMIN_NATIVE_OPERATIONAL_READS_ENABLED=false
|
ADMIN_NATIVE_OPERATIONAL_READS_ENABLED=false
|
||||||
|
# 考生账户归档、密码重置及资料流程审核;必须同时启用考生/报名/缴费读取。
|
||||||
|
ADMIN_NATIVE_CANDIDATE_MANAGEMENT_ENABLED=false
|
||||||
|
|
||||||
# 仅在首次创建空数据库时使用。部署前务必修改初始密码。
|
# 仅在首次创建空数据库时使用。部署前务必修改初始密码。
|
||||||
INITIAL_ADMIN_USERNAME=admin
|
INITIAL_ADMIN_USERNAME=admin
|
||||||
|
|||||||
+4
-3
@@ -12,7 +12,7 @@
|
|||||||
- [x] 招生公示与 HMAC 文书验真公开接口
|
- [x] 招生公示与 HMAC 文书验真公开接口
|
||||||
- [x] 登录、自主注册、Session 与 TOTP(兼容开关默认关闭)
|
- [x] 登录、自主注册、Session 与 TOTP(兼容开关默认关闭)
|
||||||
- [x] 考生业务
|
- [x] 考生业务
|
||||||
- [ ] 管理后台、审批流和考务编排(管理端基础读取、组织维护、批量报名号审批、流程配置、通知公告、考点变更审批及考生/报名/缴费读取已原生化)
|
- [ ] 管理后台、审批流和考务编排(管理端基础读取、组织维护、批量报名号审批、流程配置、通知公告、考点变更审批、考生/报名/缴费读取及考生账号维护已原生化)
|
||||||
- [x] 考生志愿填报与招生录取查询
|
- [x] 考生志愿填报与招生录取查询
|
||||||
- [ ] Excel、文书和缓存
|
- [ ] Excel、文书和缓存
|
||||||
- [ ] 容器入口切换及 Node.js 后端移除
|
- [ ] 容器入口切换及 Node.js 后端移除
|
||||||
@@ -54,7 +54,7 @@ $env:AUTH_NATIVE_ENABLED = 'true'
|
|||||||
$env:CANDIDATE_NATIVE_ENABLED = 'true'
|
$env:CANDIDATE_NATIVE_ENABLED = 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
管理后台第一批只读接口(管理上下文、仪表盘、学校、学校组织、管理员和考试列表)已经原生化,并保留超级、校级、班级管理员的权限与数据作用域。第二批覆盖学校、班级和管理员的创建与维护、管理员密码重置及自主注册开关;更新操作与审计日志在同一事务中提交,停用或重置管理员会同步失效其会话。第三批覆盖批量报名号申领的读取、提交和审批,终审会按照当前号码规则原子生成考生账号、初始密码和待补录资料。第四批覆盖报名号规则及审批流程定义的读取与维护,并保留流程层级和批量申领终审约束。第五批覆盖完整通知公告管理:超级管理员可读取手工公告与五类系统公示、创建和编辑手工公告,并控制系统公示是否公开显示;HTML 净化、显隐更新和审计日志均由 ASP.NET Core 原生处理。第六批覆盖考点考场读取、新增与修改申请、重复待审拦截及多级审批;终审会在同一事务中更新流程、审计日志和正式考点考场档案。第七批覆盖考生档案、报名记录和已审核报名的缴费记录读取,并保持三级管理员的数据作用域、证件号脱敏、考试科目及审批流投影与 Node 一致。
|
管理后台第一批只读接口(管理上下文、仪表盘、学校、学校组织、管理员和考试列表)已经原生化,并保留超级、校级、班级管理员的权限与数据作用域。第二批覆盖学校、班级和管理员的创建与维护、管理员密码重置及自主注册开关;更新操作与审计日志在同一事务中提交,停用或重置管理员会同步失效其会话。第三批覆盖批量报名号申领的读取、提交和审批,终审会按照当前号码规则原子生成考生账号、初始密码和待补录资料。第四批覆盖报名号规则及审批流程定义的读取与维护,并保留流程层级和批量申领终审约束。第五批覆盖完整通知公告管理:超级管理员可读取手工公告与五类系统公示、创建和编辑手工公告,并控制系统公示是否公开显示;HTML 净化、显隐更新和审计日志均由 ASP.NET Core 原生处理。第六批覆盖考点考场读取、新增与修改申请、重复待审拦截及多级审批;终审会在同一事务中更新流程、审计日志和正式考点考场档案。第七批覆盖考生档案、报名记录和已审核报名的缴费记录读取,并保持三级管理员的数据作用域、证件号脱敏、考试科目及审批流投影与 Node 一致。第八批覆盖校级按班级或年级归档及恢复考生账户、超级管理员重置考生密码、三级管理员按配置流程审核考生资料;账号与流程更新、审计日志在同一事务中提交,归档和密码重置会同步失效相关 ASP.NET Core 会话。
|
||||||
|
|
||||||
其余审批流和考务编排接口仍转发给 Node,因此管理端开关都要求原生认证和共享 Redis;各写入子功能还必须与只读开关一起启用:
|
其余审批流和考务编排接口仍转发给 Node,因此管理端开关都要求原生认证和共享 Redis;各写入子功能还必须与只读开关一起启用:
|
||||||
|
|
||||||
@@ -67,9 +67,10 @@ $env:ADMIN_NATIVE_CONFIGURATION_ENABLED = 'true'
|
|||||||
$env:ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED = 'true'
|
$env:ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED = 'true'
|
||||||
$env:ADMIN_NATIVE_CENTERS_ENABLED = 'true'
|
$env:ADMIN_NATIVE_CENTERS_ENABLED = 'true'
|
||||||
$env:ADMIN_NATIVE_OPERATIONAL_READS_ENABLED = 'true'
|
$env:ADMIN_NATIVE_OPERATIONAL_READS_ENABLED = 'true'
|
||||||
|
$env:ADMIN_NATIVE_CANDIDATE_MANAGEMENT_ENABLED = 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
旧版 `ADMIN_NATIVE_NOTICE_WRITES_ENABLED` 仍可作为兼容别名使用。`GET /health/migration` 的 `administration.nativeReadsEnabled`、`administration.nativeOrganizationWritesEnabled`、`administration.nativeAccountBatchesEnabled`、`administration.nativeConfigurationEnabled`、`administration.nativeNoticeManagementEnabled`、`administration.nativeCentersEnabled`、`administration.nativeOperationalReadsEnabled` 和 `administration.nativeRoutes` 会报告这些端点是否已切换。
|
旧版 `ADMIN_NATIVE_NOTICE_WRITES_ENABLED` 仍可作为兼容别名使用。`GET /health/migration` 的 `administration.nativeReadsEnabled`、`administration.nativeOrganizationWritesEnabled`、`administration.nativeAccountBatchesEnabled`、`administration.nativeConfigurationEnabled`、`administration.nativeNoticeManagementEnabled`、`administration.nativeCentersEnabled`、`administration.nativeOperationalReadsEnabled`、`administration.nativeCandidateManagementEnabled` 和 `administration.nativeRoutes` 会报告这些端点是否已切换。
|
||||||
|
|
||||||
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
|
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
|
||||||
|
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ try {
|
|||||||
ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED = 'true'
|
ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED = 'true'
|
||||||
ADMIN_NATIVE_CENTERS_ENABLED = 'true'
|
ADMIN_NATIVE_CENTERS_ENABLED = 'true'
|
||||||
ADMIN_NATIVE_OPERATIONAL_READS_ENABLED = 'true'
|
ADMIN_NATIVE_OPERATIONAL_READS_ENABLED = 'true'
|
||||||
|
ADMIN_NATIVE_CANDIDATE_MANAGEMENT_ENABLED = 'true'
|
||||||
ADMIN_NATIVE_ALLOW_MEMORY = 'true'
|
ADMIN_NATIVE_ALLOW_MEMORY = 'true'
|
||||||
LegacyNode__Enabled = 'true'
|
LegacyNode__Enabled = 'true'
|
||||||
LegacyNode__BaseUrl = $legacyBaseUrl
|
LegacyNode__BaseUrl = $legacyBaseUrl
|
||||||
@@ -819,6 +820,92 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$archiveBody = @{
|
||||||
|
scopeType = 'class'
|
||||||
|
scopeValue = $profileUpdate.profile.classId
|
||||||
|
archived = $true
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
$superArchiveForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeSession -SkipHttpErrorCheck
|
||||||
|
if ($superArchiveForbidden.StatusCode -ne 403 -or $superArchiveForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||||
|
throw 'Native candidate archive did not preserve the school-admin boundary'
|
||||||
|
}
|
||||||
|
$classArchiveForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeClassSession -SkipHttpErrorCheck
|
||||||
|
if ($classArchiveForbidden.StatusCode -ne 403) {
|
||||||
|
throw 'Native candidate archive accepted a class administrator'
|
||||||
|
}
|
||||||
|
$archiveResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeSchoolSession
|
||||||
|
$archiveResult = $archiveResponse.Content | ConvertFrom-Json
|
||||||
|
if ($archiveResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $archiveResult.archived -ne $true -or $archiveResult.count -lt 1) {
|
||||||
|
throw 'Native candidate archive did not freeze the selected class accounts'
|
||||||
|
}
|
||||||
|
$archivedState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
||||||
|
$archivedCandidate = @($archivedState.candidates | Where-Object id -eq $profileUpdate.profile.id)[0]
|
||||||
|
if ($null -eq $archivedCandidate -or $archivedCandidate.accountArchived -ne $true) {
|
||||||
|
throw 'Native candidate archive was not reflected by the candidate read model'
|
||||||
|
}
|
||||||
|
|
||||||
|
$restoreBody = @{
|
||||||
|
scopeType = 'class'
|
||||||
|
scopeValue = $profileUpdate.profile.classId
|
||||||
|
archived = $false
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
$restoreResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $restoreBody -WebSession $nativeSchoolSession
|
||||||
|
$restoreResult = $restoreResponse.Content | ConvertFrom-Json
|
||||||
|
if ($restoreResult.archived -ne $false -or $restoreResult.count -ne $archiveResult.count) {
|
||||||
|
throw 'Native candidate archive could not restore the same class accounts'
|
||||||
|
}
|
||||||
|
|
||||||
|
$preResetSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
||||||
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody -WebSession $preResetSession | Out-Null
|
||||||
|
$schoolResetForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)/reset-password" -Method Post -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
||||||
|
if ($schoolResetForbidden.StatusCode -ne 403 -or $schoolResetForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||||
|
throw 'Native candidate password reset did not preserve the super-admin boundary'
|
||||||
|
}
|
||||||
|
$resetCandidateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)/reset-password" -Method Post -WebSession $nativeSession
|
||||||
|
$resetCandidate = $resetCandidateResponse.Content | ConvertFrom-Json
|
||||||
|
if ($resetCandidateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $resetCandidate.candidateNumber -ne $registration.registrationNumber -or $resetCandidate.temporaryPassword -notmatch '^Reset-[A-Za-z0-9_-]+$') {
|
||||||
|
throw 'Native candidate password reset did not return compatible temporary credentials'
|
||||||
|
}
|
||||||
|
$invalidatedCandidateSession = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $preResetSession
|
||||||
|
if ($null -ne $invalidatedCandidateSession.user) {
|
||||||
|
throw 'Native candidate password reset did not invalidate existing sessions'
|
||||||
|
}
|
||||||
|
$resetCandidateLoginBody = @{ username = $registration.registrationNumber; password = $resetCandidate.temporaryPassword } | ConvertTo-Json -Compress
|
||||||
|
$resetCandidateSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
||||||
|
$resetCandidateLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $resetCandidateLoginBody -WebSession $resetCandidateSession
|
||||||
|
if ($resetCandidateLogin.user.mustChangePassword -ne $true) {
|
||||||
|
throw 'Native candidate temporary password did not require a password change'
|
||||||
|
}
|
||||||
|
|
||||||
|
$invalidProfileReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)" -Method Patch -ContentType 'application/json' -Body '{"status":"invalid"}' -WebSession $nativeSession -SkipHttpErrorCheck
|
||||||
|
if ($invalidProfileReview.StatusCode -ne 400 -or $invalidProfileReview.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||||
|
throw 'Native candidate profile review accepted an invalid status'
|
||||||
|
}
|
||||||
|
$candidateManagementState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
||||||
|
$managedCandidate = @($candidateManagementState.candidates | Where-Object id -eq $profileUpdate.profile.id)[0]
|
||||||
|
if ($null -eq $managedCandidate -or $managedCandidate.workflow.status -ne 'pending') {
|
||||||
|
throw 'Candidate profile smoke data did not retain a pending review workflow'
|
||||||
|
}
|
||||||
|
$reviewIterations = 0
|
||||||
|
do {
|
||||||
|
$reviewIterations += 1
|
||||||
|
if ($reviewIterations -gt 10) {
|
||||||
|
throw 'Native candidate profile workflow did not reach a terminal state'
|
||||||
|
}
|
||||||
|
$profileReviewBody = @{ status = 'approved'; reviewNote = "原生资料审核第 $reviewIterations 步" } | ConvertTo-Json -Compress
|
||||||
|
$profileReviewResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)" -Method Patch -ContentType 'application/json' -Body $profileReviewBody -WebSession $nativeSession
|
||||||
|
if ($profileReviewResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||||
|
throw 'Candidate profile review did not use ASP.NET Core'
|
||||||
|
}
|
||||||
|
$profileReview = $profileReviewResponse.Content | ConvertFrom-Json
|
||||||
|
} while ($profileReview.workflow.status -eq 'pending')
|
||||||
|
if ($profileReview.profile.status -ne 'approved' -or $profileReview.workflow.status -ne 'approved' -or $profileReview.workflow.actions.Count -lt 2) {
|
||||||
|
throw 'Native candidate profile review did not complete its configured workflow'
|
||||||
|
}
|
||||||
|
$legacyCandidatesAfterManagement = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/candidates" -WebSession $session
|
||||||
|
$nativeCandidatesAfterManagement = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
||||||
|
Assert-JsonEquivalent -Expected $legacyCandidatesAfterManagement.Content -Actual $nativeCandidatesAfterManagement.Content -Label 'Candidate management follow-up'
|
||||||
|
|
||||||
$legacyCenters = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/centers" -WebSession $session
|
$legacyCenters = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/centers" -WebSession $session
|
||||||
$nativeCenters = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSession
|
$nativeCenters = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSession
|
||||||
if ($nativeCenters.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
if ($nativeCenters.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||||
@@ -1288,6 +1375,7 @@ try {
|
|||||||
NativeAdminNoticeManagement = 'passed'
|
NativeAdminNoticeManagement = 'passed'
|
||||||
NativeAdminCenters = 'passed'
|
NativeAdminCenters = 'passed'
|
||||||
NativeAdminOperationalReads = 'passed'
|
NativeAdminOperationalReads = 'passed'
|
||||||
|
NativeAdminCandidateManagement = 'passed'
|
||||||
} | Format-List
|
} | Format-List
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user