已完成管理后台第四批 ASP.NET Core 10 迁移:报名号规则与审批流程配置。
原生接口:
GET/POST /api/admin/number-rules
GET /api/admin/workflows
PUT /api/admin/workflows/{businessType}
实现包括:
报名号规则预览、创建和更新
自动停用旧规则
审批流程步骤重建
班级、校级、超级管理员层级校验
考点及批量申领流程禁止班级审批
批量申领最终步骤强制为超级管理员
配置更新与审计日志事务提交
This commit is contained in:
@@ -31,6 +31,8 @@ ADMIN_NATIVE_READS_ENABLED=false
|
||||
ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED=false
|
||||
# 批量报名号申领、审批与账号生成;必须同时启用管理端只读接口。
|
||||
ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED=false
|
||||
# 报名号规则与审批流程定义维护;必须同时启用管理端只读接口。
|
||||
ADMIN_NATIVE_CONFIGURATION_ENABLED=false
|
||||
|
||||
# 仅在首次创建空数据库时使用。部署前务必修改初始密码。
|
||||
INITIAL_ADMIN_USERNAME=admin
|
||||
|
||||
+4
-3
@@ -12,7 +12,7 @@
|
||||
- [x] 招生公示与 HMAC 文书验真公开接口
|
||||
- [x] 登录、自主注册、Session 与 TOTP(兼容开关默认关闭)
|
||||
- [x] 考生业务
|
||||
- [ ] 管理后台、审批流和考务编排(管理端读取、组织维护及批量报名号审批已原生化)
|
||||
- [ ] 管理后台、审批流和考务编排(管理端读取、组织维护、批量报名号审批及流程配置已原生化)
|
||||
- [x] 考生志愿填报与招生录取查询
|
||||
- [ ] Excel、文书和缓存
|
||||
- [ ] 容器入口切换及 Node.js 后端移除
|
||||
@@ -54,7 +54,7 @@ $env:AUTH_NATIVE_ENABLED = 'true'
|
||||
$env:CANDIDATE_NATIVE_ENABLED = 'true'
|
||||
```
|
||||
|
||||
管理后台第一批只读接口(管理上下文、仪表盘、学校、学校组织、管理员和考试列表)已经原生化,并保留超级、校级、班级管理员的权限与数据作用域。第二批覆盖学校、班级和管理员的创建与维护、管理员密码重置及自主注册开关;更新操作与审计日志在同一事务中提交,停用或重置管理员会同步失效其会话。第三批覆盖批量报名号申领的读取、提交和审批,终审会按照当前号码规则原子生成考生账号、初始密码和待补录资料。
|
||||
管理后台第一批只读接口(管理上下文、仪表盘、学校、学校组织、管理员和考试列表)已经原生化,并保留超级、校级、班级管理员的权限与数据作用域。第二批覆盖学校、班级和管理员的创建与维护、管理员密码重置及自主注册开关;更新操作与审计日志在同一事务中提交,停用或重置管理员会同步失效其会话。第三批覆盖批量报名号申领的读取、提交和审批,终审会按照当前号码规则原子生成考生账号、初始密码和待补录资料。第四批覆盖报名号规则及审批流程定义的读取与维护,并保留流程层级和批量申领终审约束。
|
||||
|
||||
其余审批流和考务编排接口仍转发给 Node,因此两个管理端开关都要求原生认证和共享 Redis;组织维护开关还必须与只读开关一起启用:
|
||||
|
||||
@@ -63,9 +63,10 @@ $env:AUTH_NATIVE_ENABLED = 'true'
|
||||
$env:ADMIN_NATIVE_READS_ENABLED = 'true'
|
||||
$env:ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED = 'true'
|
||||
$env:ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED = 'true'
|
||||
$env:ADMIN_NATIVE_CONFIGURATION_ENABLED = 'true'
|
||||
```
|
||||
|
||||
`GET /health/migration` 的 `administration.nativeReadsEnabled`、`administration.nativeOrganizationWritesEnabled`、`administration.nativeAccountBatchesEnabled` 和 `administration.nativeRoutes` 会报告这些端点是否已切换。
|
||||
`GET /health/migration` 的 `administration.nativeReadsEnabled`、`administration.nativeOrganizationWritesEnabled`、`administration.nativeAccountBatchesEnabled`、`administration.nativeConfigurationEnabled` 和 `administration.nativeRoutes` 会报告这些端点是否已切换。
|
||||
|
||||
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
|
||||
|
||||
|
||||
@@ -425,6 +425,7 @@ try {
|
||||
ADMIN_NATIVE_READS_ENABLED = 'true'
|
||||
ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED = 'true'
|
||||
ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED = 'true'
|
||||
ADMIN_NATIVE_CONFIGURATION_ENABLED = 'true'
|
||||
ADMIN_NATIVE_ALLOW_MEMORY = 'true'
|
||||
LegacyNode__Enabled = 'true'
|
||||
LegacyNode__BaseUrl = $legacyBaseUrl
|
||||
@@ -739,6 +740,54 @@ try {
|
||||
throw 'Native admins route did not preserve the class-admin boundary'
|
||||
}
|
||||
|
||||
foreach ($configurationRoute in @('number-rules', 'workflows')) {
|
||||
$legacyConfiguration = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$configurationRoute" -WebSession $session
|
||||
$nativeConfiguration = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$configurationRoute" -WebSession $nativeSession
|
||||
if ($nativeConfiguration.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||
throw "Native admin configuration route '$configurationRoute' did not use ASP.NET Core"
|
||||
}
|
||||
Assert-JsonEquivalent -Expected $legacyConfiguration.Content -Actual $nativeConfiguration.Content -Label "Admin configuration '$configurationRoute'"
|
||||
}
|
||||
foreach ($configurationRoute in @('number-rules', 'workflows')) {
|
||||
$schoolConfigurationForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$configurationRoute" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
||||
if ($schoolConfigurationForbidden.StatusCode -ne 403) {
|
||||
throw "Admin configuration route '$configurationRoute' did not preserve the super-admin boundary"
|
||||
}
|
||||
}
|
||||
|
||||
$numberRuleState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/number-rules" -WebSession $nativeSession
|
||||
$activeNumberRule = $numberRuleState.activeRule
|
||||
$numberRuleBody = @{
|
||||
id = $activeNumberRule.id
|
||||
name = '原生年度学校流水号'
|
||||
separator = $activeNumberRule.separator
|
||||
segments = @($activeNumberRule.segments | ForEach-Object {
|
||||
@{ type = $_.type; value = $_.value; width = $_.width }
|
||||
})
|
||||
} | ConvertTo-Json -Depth 5 -Compress
|
||||
$savedNumberRule = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/number-rules" -Method Post -ContentType 'application/json' -Body $numberRuleBody -WebSession $nativeSession
|
||||
if ($savedNumberRule.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($savedNumberRule.Content | ConvertFrom-Json).rule.name -ne '原生年度学校流水号') {
|
||||
throw 'Native number-rule update did not persist the requested definition'
|
||||
}
|
||||
|
||||
$workflowState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/workflows" -WebSession $nativeSession
|
||||
$accountWorkflow = @($workflowState.workflows | Where-Object { $_.businessType -eq 'candidate_account_batch' })[0]
|
||||
$invalidWorkflowBody = @{ name = '无效流程'; steps = @(@{ name = '学校终审'; adminLevel = 'school' }) } | ConvertTo-Json -Depth 4 -Compress
|
||||
$invalidWorkflow = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflows/candidate_account_batch" -Method Put -ContentType 'application/json' -Body $invalidWorkflowBody -WebSession $nativeSession -SkipHttpErrorCheck
|
||||
if ($invalidWorkflow.StatusCode -ne 400) {
|
||||
throw 'Native workflow update allowed a non-super final account-batch step'
|
||||
}
|
||||
$workflowBody = @{
|
||||
name = '原生批量报名号审批'
|
||||
steps = @($accountWorkflow.steps | ForEach-Object {
|
||||
@{ name = $_.name; adminLevel = $_.adminLevel }
|
||||
})
|
||||
} | ConvertTo-Json -Depth 5 -Compress
|
||||
$savedWorkflow = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflows/candidate_account_batch" -Method Put -ContentType 'application/json' -Body $workflowBody -WebSession $nativeSession
|
||||
if ($savedWorkflow.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($savedWorkflow.Content | ConvertFrom-Json).workflow.name -ne '原生批量报名号审批') {
|
||||
throw 'Native workflow update did not persist the account-batch definition'
|
||||
}
|
||||
|
||||
$legacySuperBatches = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/candidate-account-batches" -WebSession $session
|
||||
$nativeSuperBatches = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches" -WebSession $nativeSession
|
||||
if ($nativeSuperBatches.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||
@@ -989,6 +1038,7 @@ try {
|
||||
NativeAdminReads = 'passed'
|
||||
NativeAdminOrganizationWrites = 'passed'
|
||||
NativeAdminAccountBatches = 'passed'
|
||||
NativeAdminConfiguration = 'passed'
|
||||
} | Format-List
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Eis.Application.Administration;
|
||||
|
||||
public interface IAdminConfigurationService
|
||||
{
|
||||
Task<AdminEndpointResult> GetNumberRulesAsync(string sessionToken, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> SaveNumberRuleAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> GetWorkflowsAsync(string sessionToken, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> SaveWorkflowAsync(string sessionToken, string businessType, JsonObject body, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -13,15 +13,17 @@ internal sealed record AdminAccountBatchSnapshot(
|
||||
IReadOnlyList<AccountWorkflow> Workflows,
|
||||
IReadOnlyList<AccountWorkflowInstance> Instances,
|
||||
IReadOnlyList<AccountWorkflowAction> Actions,
|
||||
IReadOnlyList<AccountNumberRule> NumberRules);
|
||||
IReadOnlyList<AccountNumberRule> NumberRules,
|
||||
string PreviewGender,
|
||||
string? PreviewSchoolId);
|
||||
|
||||
internal sealed record AccountBatch(string Id, string SchoolId, string? RequestedBy, string Status, string ReviewNote, string CreatedAt, string? ReviewedAt);
|
||||
internal sealed record AccountBatchItem(string Id, string BatchId, string ClassId, int Position, string CandidateNumber, string InitialPassword, string? UserId, string? CreatedAt);
|
||||
internal sealed record AccountWorkflow(string Id, string BusinessType, string Name, bool Active, IReadOnlyList<AccountWorkflowStep> Steps);
|
||||
internal sealed record AccountWorkflow(string Id, string BusinessType, string Name, bool Active, string? UpdatedBy, string UpdatedAt, IReadOnlyList<AccountWorkflowStep> Steps);
|
||||
internal sealed record AccountWorkflowStep(string Id, string Name, string AdminLevel, int Position);
|
||||
internal sealed record AccountWorkflowInstance(string Id, string WorkflowId, string BusinessType, string BusinessId, string Status, int CurrentStep, string? AssigneeId, string CreatedAt, string? CompletedAt);
|
||||
internal sealed record AccountWorkflowAction(string Id, string InstanceId, string? ActorId, string Action, string Note, string? FromAssigneeId, string? ToAssigneeId, string CreatedAt);
|
||||
internal sealed record AccountNumberRule(string Id, string Name, string Separator, bool Active, IReadOnlyList<AccountNumberSegment> Segments);
|
||||
internal sealed record AccountNumberRule(string Id, string Name, string Separator, bool Active, string? CreatedBy, string UpdatedAt, IReadOnlyList<AccountNumberSegment> Segments);
|
||||
internal sealed record AccountNumberSegment(string Id, string Type, string Value, int Width, int Position);
|
||||
|
||||
internal sealed class AdminAccountBatchSnapshotLoader(IRelationalConnectionFactory connectionFactory)
|
||||
@@ -36,13 +38,15 @@ internal sealed class AdminAccountBatchSnapshotLoader(IRelationalConnectionFacto
|
||||
var items = await QueryAsync(connection, "SELECT id, batch_id, class_id, position, candidate_number, initial_password, user_id, created_at FROM candidate_account_batch_items ORDER BY batch_id, position, id", reader => new AccountBatchItem(Text(reader, "id"), Text(reader, "batch_id"), Text(reader, "class_id"), Integer(reader, "position"), Optional(reader, "candidate_number") ?? "", Optional(reader, "initial_password") ?? "", Optional(reader, "user_id"), Optional(reader, "created_at")), cancellationToken);
|
||||
var steps = await QueryAsync(connection, "SELECT id, workflow_id, name, admin_level, position FROM workflow_steps ORDER BY workflow_id, position, id", reader => new StepRow(Text(reader, "workflow_id"), new AccountWorkflowStep(Text(reader, "id"), Text(reader, "name"), Text(reader, "admin_level"), Integer(reader, "position"))), cancellationToken);
|
||||
var byWorkflow = steps.GroupBy(item => item.WorkflowId).ToDictionary(group => group.Key, group => (IReadOnlyList<AccountWorkflowStep>)group.Select(item => item.Step).ToArray(), StringComparer.Ordinal);
|
||||
var workflows = await QueryAsync(connection, "SELECT id, business_type, name, active FROM workflow_definitions ORDER BY business_type, id", reader => { var id = Text(reader, "id"); return new AccountWorkflow(id, Text(reader, "business_type"), Text(reader, "name"), Boolean(reader, "active"), byWorkflow.GetValueOrDefault(id) ?? []); }, cancellationToken);
|
||||
var workflows = await QueryAsync(connection, "SELECT id, business_type, name, active, updated_by, updated_at FROM workflow_definitions ORDER BY business_type, id", reader => { var id = Text(reader, "id"); return new AccountWorkflow(id, Text(reader, "business_type"), Text(reader, "name"), Boolean(reader, "active"), Optional(reader, "updated_by"), Text(reader, "updated_at"), byWorkflow.GetValueOrDefault(id) ?? []); }, cancellationToken);
|
||||
var instances = await QueryAsync(connection, "SELECT id, workflow_id, business_type, business_id, status, current_step, assignee_id, created_at, completed_at FROM workflow_instances ORDER BY created_at DESC, id", reader => new AccountWorkflowInstance(Text(reader, "id"), Text(reader, "workflow_id"), Text(reader, "business_type"), Text(reader, "business_id"), Text(reader, "status"), Integer(reader, "current_step"), Optional(reader, "assignee_id"), Text(reader, "created_at"), Optional(reader, "completed_at")), cancellationToken);
|
||||
var actions = await QueryAsync(connection, "SELECT id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at FROM workflow_actions ORDER BY created_at, id", reader => new AccountWorkflowAction(Text(reader, "id"), Text(reader, "instance_id"), Optional(reader, "actor_id"), Text(reader, "action"), Optional(reader, "note") ?? "", Optional(reader, "from_assignee_id"), Optional(reader, "to_assignee_id"), Text(reader, "created_at")), cancellationToken);
|
||||
var segments = await QueryAsync(connection, "SELECT id, rule_id, type, value, width, position FROM number_rule_segments ORDER BY rule_id, position, id", reader => new SegmentRow(Text(reader, "rule_id"), new AccountNumberSegment(Text(reader, "id"), Text(reader, "type"), Optional(reader, "value") ?? "", Integer(reader, "width"), Integer(reader, "position"))), cancellationToken);
|
||||
var byRule = segments.GroupBy(item => item.RuleId).ToDictionary(group => group.Key, group => (IReadOnlyList<AccountNumberSegment>)group.Select(item => item.Segment).ToArray(), StringComparer.Ordinal);
|
||||
var rules = await QueryAsync(connection, "SELECT id, name, `separator`, active FROM number_rules ORDER BY updated_at DESC, id", reader => { var id = Text(reader, "id"); return new AccountNumberRule(id, Text(reader, "name"), Text(reader, "separator"), Boolean(reader, "active"), byRule.GetValueOrDefault(id) ?? []); }, cancellationToken);
|
||||
return new(schools, classes, users, batches, items, workflows, instances, actions, rules);
|
||||
var rules = await QueryAsync(connection, "SELECT id, name, `separator`, active, created_by, updated_at FROM number_rules ORDER BY updated_at DESC, id", reader => { var id = Text(reader, "id"); return new AccountNumberRule(id, Text(reader, "name"), Text(reader, "separator"), Boolean(reader, "active"), Optional(reader, "created_by"), Text(reader, "updated_at"), byRule.GetValueOrDefault(id) ?? []); }, cancellationToken);
|
||||
var profiles = await QueryAsync(connection, "SELECT gender, school_id FROM candidate_profiles ORDER BY updated_at, id", reader => new PreviewProfile(Optional(reader, "gender") ?? "", Optional(reader, "school_id")), cancellationToken);
|
||||
var preview = profiles.FirstOrDefault();
|
||||
return new(schools, classes, users, batches, items, workflows, instances, actions, rules, preview?.Gender ?? "女", preview?.SchoolId ?? schools.FirstOrDefault()?.Id);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<T>> QueryAsync<T>(DbConnection connection, string sql, Func<DbDataReader, T> map, CancellationToken cancellationToken)
|
||||
@@ -57,4 +61,5 @@ internal sealed class AdminAccountBatchSnapshotLoader(IRelationalConnectionFacto
|
||||
private static int Integer(DbDataReader reader, string name) => Convert.ToInt32(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);
|
||||
private sealed record StepRow(string WorkflowId, AccountWorkflowStep Step);
|
||||
private sealed record SegmentRow(string RuleId, AccountNumberSegment Segment);
|
||||
private sealed record PreviewProfile(string Gender, string? SchoolId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Data.Common;
|
||||
using Eis.Infrastructure.Data;
|
||||
|
||||
namespace Eis.Infrastructure.Administration;
|
||||
|
||||
internal sealed class AdminConfigurationRepository(IRelationalConnectionFactory connectionFactory)
|
||||
{
|
||||
public Task SaveNumberRuleAsync(AccountNumberRule rule, bool isNew, AdminAuditEntry audit, CancellationToken cancellationToken)
|
||||
{
|
||||
var operations = new List<SqlOperation>
|
||||
{
|
||||
new("UPDATE number_rules SET active = 0 WHERE active = 1", [])
|
||||
};
|
||||
if (isNew)
|
||||
{
|
||||
operations.Add(new("""
|
||||
INSERT INTO number_rules (id, name, `separator`, active, created_by, updated_at)
|
||||
VALUES (@id, @name, @separator, @active, @createdBy, @updatedAt)
|
||||
""", Parameters(rule)));
|
||||
}
|
||||
else
|
||||
{
|
||||
operations.Add(new("""
|
||||
UPDATE number_rules SET name = @name, `separator` = @separator, active = @active,
|
||||
created_by = @createdBy, updated_at = @updatedAt WHERE id = @id
|
||||
""", Parameters(rule)));
|
||||
operations.Add(new("DELETE FROM number_rule_segments WHERE rule_id = @id", [new("@id", rule.Id)]));
|
||||
}
|
||||
operations.AddRange(rule.Segments.Select(segment => new SqlOperation("""
|
||||
INSERT INTO number_rule_segments (id, rule_id, position, type, value, width)
|
||||
VALUES (@id, @ruleId, @position, @type, @value, @width)
|
||||
""",
|
||||
[
|
||||
new("@id", segment.Id), new("@ruleId", rule.Id), new("@position", segment.Position),
|
||||
new("@type", segment.Type), new("@value", Optional(segment.Value)), new("@width", segment.Width)
|
||||
])));
|
||||
operations.Add(AuditOperation(audit));
|
||||
return ExecuteAsync(operations, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SaveWorkflowAsync(AccountWorkflow workflow, AdminAuditEntry audit, CancellationToken cancellationToken)
|
||||
{
|
||||
var operations = new List<SqlOperation>
|
||||
{
|
||||
new("""
|
||||
UPDATE workflow_definitions SET name = @name, active = @active,
|
||||
updated_by = @updatedBy, updated_at = @updatedAt WHERE id = @id
|
||||
""",
|
||||
[
|
||||
new("@name", workflow.Name), new("@active", workflow.Active ? 1 : 0),
|
||||
new("@updatedBy", workflow.UpdatedBy), new("@updatedAt", workflow.UpdatedAt), new("@id", workflow.Id)
|
||||
]),
|
||||
new("DELETE FROM workflow_steps WHERE workflow_id = @id", [new("@id", workflow.Id)])
|
||||
};
|
||||
operations.AddRange(workflow.Steps.Select(step => new SqlOperation("""
|
||||
INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level)
|
||||
VALUES (@id, @workflowId, @position, @name, @adminLevel)
|
||||
""",
|
||||
[
|
||||
new("@id", step.Id), new("@workflowId", workflow.Id), new("@position", step.Position),
|
||||
new("@name", step.Name), new("@adminLevel", step.AdminLevel)
|
||||
])));
|
||||
operations.Add(AuditOperation(audit));
|
||||
return ExecuteAsync(operations, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(IReadOnlyList<SqlOperation> operations, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = operation.Sql;
|
||||
foreach (var item in operation.Parameters)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = item.Name;
|
||||
parameter.Value = item.Value ?? DBNull.Value;
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SqlParameterValue> Parameters(AccountNumberRule rule) =>
|
||||
[
|
||||
new("@id", rule.Id), new("@name", rule.Name), new("@separator", rule.Separator),
|
||||
new("@active", rule.Active ? 1 : 0), new("@createdBy", rule.CreatedBy), new("@updatedAt", rule.UpdatedAt)
|
||||
];
|
||||
|
||||
private static SqlOperation AuditOperation(AdminAuditEntry audit) => new("""
|
||||
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)
|
||||
]);
|
||||
|
||||
private static string? Optional(string value) => value.Length == 0 ? null : value;
|
||||
private sealed record SqlOperation(string Sql, IReadOnlyList<SqlParameterValue> Parameters);
|
||||
private sealed record SqlParameterValue(string Name, object? Value);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Nodes;
|
||||
using Eis.Application.Administration;
|
||||
using Eis.Infrastructure.Authentication;
|
||||
|
||||
namespace Eis.Infrastructure.Administration;
|
||||
|
||||
internal sealed class AdminConfigurationService(
|
||||
IAuthenticationStateStore authenticationState,
|
||||
AuthenticationRepository authenticationRepository,
|
||||
AdminAccountBatchSnapshotLoader snapshotLoader,
|
||||
AdminConfigurationRepository repository) : IAdminConfigurationService
|
||||
{
|
||||
private static readonly HashSet<string> SegmentTypes = ["year", "school_code", "gender", "sequence", "literal"];
|
||||
private static readonly HashSet<string> AdminLevels = ["class", "school", "super"];
|
||||
private static readonly HashSet<string> WorkflowTypes = ["profile_change", "registration_review", "center_change", "candidate_account_batch", "score_appeal"];
|
||||
|
||||
public async Task<AdminEndpointResult> GetNumberRulesAsync(string sessionToken, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveSuperAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var snapshot = await snapshotLoader.LoadAsync(cancellationToken);
|
||||
var active = snapshot.NumberRules.FirstOrDefault(item => item.Active);
|
||||
var preview = active is null ? "" : GenerateCandidateNumber(snapshot, active);
|
||||
return Success(new JsonObject
|
||||
{
|
||||
["ok"] = true,
|
||||
["rules"] = new JsonArray(snapshot.NumberRules.Select(RuleJson).ToArray()),
|
||||
["activeRule"] = active is null ? null : RuleJson(active),
|
||||
["preview"] = preview
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AdminEndpointResult> SaveNumberRuleAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveSuperAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var user = context.User!;
|
||||
var requestedArray = body["segments"] as JsonArray;
|
||||
var requested = requestedArray?.Select(item => item as JsonObject).ToArray() ?? [];
|
||||
if (requested.Length == 0 || requested.Any(item => item is null || !SegmentTypes.Contains(Text(item["type"]))) ||
|
||||
!requested.Any(item => item is not null && Text(item["type"]) == "sequence"))
|
||||
return Error(400, "报名号规则至少包含一个流水号段");
|
||||
|
||||
var snapshot = await snapshotLoader.LoadAsync(cancellationToken);
|
||||
var existing = snapshot.NumberRules.FirstOrDefault(item => item.Id == Text(body["id"]));
|
||||
var name = Clean(Text(body["name"]), 80);
|
||||
var rule = new AccountNumberRule(
|
||||
existing?.Id ?? Uid("rule"),
|
||||
name.Length == 0 ? "自定义报名号规则" : name,
|
||||
Clean(Text(body["separator"]), 3),
|
||||
true,
|
||||
user.Id,
|
||||
NowIso(),
|
||||
requested.Select((item, index) => new AccountNumberSegment(
|
||||
Uid("segment"),
|
||||
Text(item!["type"]),
|
||||
Clean(Text(item["value"]), 20),
|
||||
Math.Clamp((int)NumberOrZero(item["width"]), 0, 12),
|
||||
index + 1)).ToArray());
|
||||
var audit = Audit(user, "更新报名号规则", $"{rule.Name} · {string.Join(" + ", rule.Segments.Select(item => item.Type))}");
|
||||
await repository.SaveNumberRuleAsync(rule, existing is null, audit, cancellationToken);
|
||||
return Success(new JsonObject { ["ok"] = true, ["rule"] = RuleJson(rule) });
|
||||
}
|
||||
|
||||
public async Task<AdminEndpointResult> GetWorkflowsAsync(string sessionToken, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveSuperAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var snapshot = await snapshotLoader.LoadAsync(cancellationToken);
|
||||
return Success(new JsonObject
|
||||
{
|
||||
["ok"] = true,
|
||||
["workflows"] = new JsonArray(snapshot.Workflows.Select(WorkflowJson).ToArray())
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AdminEndpointResult> SaveWorkflowAsync(
|
||||
string sessionToken,
|
||||
string businessType,
|
||||
JsonObject body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveSuperAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var user = context.User!;
|
||||
if (!WorkflowTypes.Contains(businessType)) return Error(404, "审批流程不存在");
|
||||
var snapshot = await snapshotLoader.LoadAsync(cancellationToken);
|
||||
var existing = snapshot.Workflows.FirstOrDefault(item => item.BusinessType == businessType && item.Active);
|
||||
if (existing is null) return Error(404, "审批流程不存在");
|
||||
var stepArray = body["steps"] as JsonArray;
|
||||
var requested = stepArray?.Select(item => item as JsonObject).ToArray() ?? [];
|
||||
if (requested.Length == 0 || requested.Any(item => item is null || !AdminLevels.Contains(Text(item["adminLevel"]))))
|
||||
return Error(400, "流程至少需要一个班级、校级或超级管理员审批步骤");
|
||||
if (businessType is "center_change" or "candidate_account_batch" && requested.Any(item => Text(item!["adminLevel"]) == "class"))
|
||||
return Error(400, "该业务不对应单一班级,不能配置班级管理员审批步骤");
|
||||
if (businessType == "candidate_account_batch" && Text(requested[^1]!["adminLevel"]) != "super")
|
||||
return Error(400, "批量报名号申领的最终步骤必须由超级管理员审批");
|
||||
var requestedName = Clean(Text(body["name"]), 80);
|
||||
var workflow = existing with
|
||||
{
|
||||
Name = requestedName.Length == 0 ? existing.Name : requestedName,
|
||||
UpdatedBy = user.Id,
|
||||
UpdatedAt = NowIso(),
|
||||
Steps = requested.Select((item, index) =>
|
||||
{
|
||||
var requestedStepName = Clean(Text(item!["name"]), 80);
|
||||
return new AccountWorkflowStep(
|
||||
Uid("workflow_step"),
|
||||
requestedStepName.Length == 0 ? $"第 {index + 1} 步" : requestedStepName,
|
||||
Text(item["adminLevel"]),
|
||||
index + 1);
|
||||
}).ToArray()
|
||||
};
|
||||
await repository.SaveWorkflowAsync(
|
||||
workflow,
|
||||
Audit(user, "修改审批流程", $"{workflow.Name} · {workflow.Steps.Count} 个步骤"),
|
||||
cancellationToken);
|
||||
return Success(new JsonObject { ["ok"] = true, ["workflow"] = WorkflowJson(workflow) });
|
||||
}
|
||||
|
||||
private async Task<ResolvedAdmin> ResolveSuperAsync(string token, CancellationToken cancellationToken)
|
||||
{
|
||||
if (token.Length == 0) return ResolvedAdmin.Failed(Error(401, "请先登录"));
|
||||
var userId = await authenticationState.GetSessionUserIdAsync(token);
|
||||
if (userId is null) return ResolvedAdmin.Failed(Error(401, "请先登录"));
|
||||
var user = await authenticationRepository.FindUserByIdAsync(userId, cancellationToken);
|
||||
if (user is not { Active: true, ArchivedAt: null }) return ResolvedAdmin.Failed(Error(401, "请先登录"));
|
||||
if (user.Role != "admin") return ResolvedAdmin.Failed(Error(403, "当前账号无权执行此操作"));
|
||||
return (user.AdminLevel ?? "super") == "super"
|
||||
? new ResolvedAdmin(user, null)
|
||||
: ResolvedAdmin.Failed(Error(403, "当前管理员层级无权执行此操作"));
|
||||
}
|
||||
|
||||
private static string GenerateCandidateNumber(AdminAccountBatchSnapshot snapshot, AccountNumberRule rule)
|
||||
{
|
||||
var year = DateTime.Now.Year.ToString(CultureInfo.InvariantCulture);
|
||||
var school = snapshot.Schools.FirstOrDefault(item => item.Id == snapshot.PreviewSchoolId);
|
||||
var prefix = string.Join(rule.Separator, rule.Segments
|
||||
.Where(item => item.Type != "sequence")
|
||||
.Select(item => item.Type == "year" ? year : item.Type == "school_code" ? school?.Code ?? "" : "")
|
||||
.Where(item => item.Length > 0));
|
||||
var sequence = snapshot.Users.Count(item => item.Role == "candidate" && item.CandidateNumber is not null &&
|
||||
(prefix.Length == 0 || item.CandidateNumber.StartsWith(prefix, StringComparison.Ordinal))) + 1;
|
||||
return string.Join(rule.Separator, rule.Segments.Select(segment => segment.Type switch
|
||||
{
|
||||
"year" => year[^Math.Min(year.Length, Math.Max(2, segment.Width == 0 ? 4 : segment.Width))..],
|
||||
"school_code" => school?.Code ?? "NOSCHOOL",
|
||||
"gender" => snapshot.PreviewGender == "男" ? "M" : snapshot.PreviewGender == "女" ? "F" : "X",
|
||||
"sequence" => sequence.ToString(CultureInfo.InvariantCulture).PadLeft(Math.Max(1, segment.Width == 0 ? 4 : segment.Width), '0'),
|
||||
_ => Clean(segment.Value, 20).ToUpperInvariant()
|
||||
}));
|
||||
}
|
||||
|
||||
private static JsonObject RuleJson(AccountNumberRule item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["name"] = item.Name,
|
||||
["separator"] = item.Separator,
|
||||
["active"] = item.Active,
|
||||
["createdBy"] = JsonValue.Create(item.CreatedBy),
|
||||
["updatedAt"] = item.UpdatedAt,
|
||||
["segments"] = new JsonArray(item.Segments.Select(SegmentJson).ToArray())
|
||||
};
|
||||
|
||||
private static JsonObject SegmentJson(AccountNumberSegment item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["type"] = item.Type,
|
||||
["value"] = item.Value,
|
||||
["width"] = item.Width,
|
||||
["position"] = item.Position
|
||||
};
|
||||
|
||||
private static JsonObject WorkflowJson(AccountWorkflow item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["businessType"] = item.BusinessType,
|
||||
["name"] = item.Name,
|
||||
["active"] = item.Active,
|
||||
["updatedBy"] = JsonValue.Create(item.UpdatedBy),
|
||||
["updatedAt"] = item.UpdatedAt,
|
||||
["steps"] = new JsonArray(item.Steps.Select(step => new JsonObject
|
||||
{
|
||||
["id"] = step.Id,
|
||||
["name"] = step.Name,
|
||||
["adminLevel"] = step.AdminLevel,
|
||||
["position"] = step.Position
|
||||
}).ToArray())
|
||||
};
|
||||
|
||||
private static double NumberOrZero(JsonNode? node)
|
||||
{
|
||||
var text = Text(node);
|
||||
return text.Length == 0 || !double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? 0 : value;
|
||||
}
|
||||
|
||||
private static string Text(JsonNode? node) =>
|
||||
node is JsonValue value && value.TryGetValue<string>(out var text) ? text : node?.ToString() ?? "";
|
||||
private static string Clean(string value, int maximum) { var cleaned = value.Trim(); return cleaned[..Math.Min(cleaned.Length, maximum)]; }
|
||||
private static string NowIso() => DateTimeOffset.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
|
||||
private static string Uid(string prefix) => $"{prefix}_{ToBase36(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())}_{Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(4))}";
|
||||
private static string ToBase36(long value) { const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; Span<char> buffer = stackalloc char[16]; var position = buffer.Length; do { buffer[--position] = alphabet[(int)(value % 36)]; value /= 36; } while (value > 0); return new(buffer[position..]); }
|
||||
private static AdminAuditEntry Audit(AuthenticationUser user, string action, string detail) => new(Uid("log"), user.Id, action, detail, NowIso());
|
||||
private static AdminEndpointResult Success(JsonObject body) => new(200, body);
|
||||
private static AdminEndpointResult Error(int status, string message) => new(status, new JsonObject { ["ok"] = false, ["message"] = message });
|
||||
private sealed record ResolvedAdmin(AuthenticationUser? User, AdminEndpointResult? Error) { public static ResolvedAdmin Failed(AdminEndpointResult error) => new(null, error); }
|
||||
}
|
||||
@@ -3,14 +3,16 @@ namespace Eis.Infrastructure.Administration;
|
||||
public sealed record AdminMigrationOptions(
|
||||
bool NativeReadsEnabled,
|
||||
bool NativeOrganizationWritesEnabled = false,
|
||||
bool NativeAccountBatchesEnabled = false)
|
||||
bool NativeAccountBatchesEnabled = false,
|
||||
bool NativeConfigurationEnabled = false)
|
||||
{
|
||||
public static AdminMigrationOptions FromEnvironment(
|
||||
bool configuredNativeReadsEnabled,
|
||||
bool authenticationNativeEnabled,
|
||||
bool sharesLegacySessions,
|
||||
bool configuredNativeOrganizationWritesEnabled = false,
|
||||
bool configuredNativeAccountBatchesEnabled = false)
|
||||
bool configuredNativeAccountBatchesEnabled = false,
|
||||
bool configuredNativeConfigurationEnabled = false)
|
||||
{
|
||||
var readsEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
|
||||
@@ -21,12 +23,15 @@ public sealed record AdminMigrationOptions(
|
||||
var accountBatchesEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED"),
|
||||
configuredNativeAccountBatchesEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled) && !readsEnabled)
|
||||
var configurationEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED"),
|
||||
configuredNativeConfigurationEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled) && !readsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
|
||||
}
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled;
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled;
|
||||
if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -42,7 +47,7 @@ public sealed record AdminMigrationOptions(
|
||||
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话");
|
||||
}
|
||||
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled);
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled);
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
|
||||
|
||||
@@ -56,9 +56,11 @@ public static class DependencyInjection
|
||||
services.AddScoped<AdminWriteRepository>();
|
||||
services.AddScoped<AdminAccountBatchSnapshotLoader>();
|
||||
services.AddScoped<AdminAccountBatchRepository>();
|
||||
services.AddScoped<AdminConfigurationRepository>();
|
||||
services.AddScoped<IAdminReadService, AdminReadService>();
|
||||
services.AddScoped<IAdminOrganizationService, AdminOrganizationService>();
|
||||
services.AddScoped<IAdminAccountBatchService, AdminAccountBatchService>();
|
||||
services.AddScoped<IAdminConfigurationService, AdminConfigurationService>();
|
||||
services.AddScoped<IPublicQueryService, PublicQueryService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -45,13 +45,25 @@ public static class NativeAdminReadEndpoints
|
||||
Execute(context, service.UpdateSelfRegistrationAsync(Token(context), body, cancellationToken)));
|
||||
}
|
||||
|
||||
if (!options.NativeAccountBatchesEnabled) return endpoints;
|
||||
if (options.NativeAccountBatchesEnabled)
|
||||
{
|
||||
endpoints.MapGet("/api/admin/candidate-account-batches", (HttpContext context, IAdminAccountBatchService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetAsync(Token(context), cancellationToken)));
|
||||
endpoints.MapPost("/api/admin/candidate-account-batches", (HttpContext context, JsonObject body, IAdminAccountBatchService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SubmitAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapPatch("/api/admin/candidate-account-batches/{batchId}", (HttpContext context, string batchId, JsonObject body, IAdminAccountBatchService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.ReviewAsync(Token(context), batchId, body, cancellationToken)));
|
||||
}
|
||||
|
||||
if (!options.NativeConfigurationEnabled) return endpoints;
|
||||
endpoints.MapGet("/api/admin/number-rules", (HttpContext context, IAdminConfigurationService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetNumberRulesAsync(Token(context), cancellationToken)));
|
||||
endpoints.MapPost("/api/admin/number-rules", (HttpContext context, JsonObject body, IAdminConfigurationService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveNumberRuleAsync(Token(context), body, cancellationToken)));
|
||||
endpoints.MapGet("/api/admin/workflows", (HttpContext context, IAdminConfigurationService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.GetWorkflowsAsync(Token(context), cancellationToken)));
|
||||
endpoints.MapPut("/api/admin/workflows/{businessType}", (HttpContext context, string businessType, JsonObject body, IAdminConfigurationService service, CancellationToken cancellationToken) =>
|
||||
Execute(context, service.SaveWorkflowAsync(Token(context), businessType, body, cancellationToken)));
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ var adminMigrationOptions = AdminMigrationOptions.FromEnvironment(
|
||||
authenticationOptions.NativeEnabled,
|
||||
authenticationOptions.SharesLegacySessions,
|
||||
builder.Configuration.GetValue<bool>("AdminMigration:NativeOrganizationWritesEnabled"),
|
||||
builder.Configuration.GetValue<bool>("AdminMigration:NativeAccountBatchesEnabled"));
|
||||
builder.Configuration.GetValue<bool>("AdminMigration:NativeAccountBatchesEnabled"),
|
||||
builder.Configuration.GetValue<bool>("AdminMigration:NativeConfigurationEnabled"));
|
||||
builder.Services.AddEisInfrastructure(
|
||||
DatabaseOptions.FromEnvironment(applicationRoot, builder.Environment.IsProduction()),
|
||||
DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()),
|
||||
@@ -104,6 +105,7 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
|
||||
nativeReadsEnabled = adminMigrationOptions.NativeReadsEnabled,
|
||||
nativeOrganizationWritesEnabled = adminMigrationOptions.NativeOrganizationWritesEnabled,
|
||||
nativeAccountBatchesEnabled = adminMigrationOptions.NativeAccountBatchesEnabled,
|
||||
nativeConfigurationEnabled = adminMigrationOptions.NativeConfigurationEnabled,
|
||||
nativeRoutes = (adminMigrationOptions.NativeReadsEnabled
|
||||
? new[] { "GET context", "GET dashboard", "GET schools", "GET school-organization", "GET admins", "GET exams" }
|
||||
: [])
|
||||
@@ -117,6 +119,9 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
|
||||
.Concat(adminMigrationOptions.NativeAccountBatchesEnabled
|
||||
? new[] { "GET/POST/PATCH candidate-account-batches" }
|
||||
: [])
|
||||
.Concat(adminMigrationOptions.NativeConfigurationEnabled
|
||||
? new[] { "GET/POST number-rules", "GET/PUT workflows" }
|
||||
: [])
|
||||
.ToArray()
|
||||
},
|
||||
features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled, candidateMigrationOptions.NativeEnabled)
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"AdminMigration": {
|
||||
"NativeReadsEnabled": false,
|
||||
"NativeOrganizationWritesEnabled": false,
|
||||
"NativeAccountBatchesEnabled": false
|
||||
"NativeAccountBatchesEnabled": false,
|
||||
"NativeConfigurationEnabled": false
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
||||
@@ -107,22 +107,40 @@ public sealed class AdminMigrationOptionsTests
|
||||
}, accountBatchesEnabled: "true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnablesAdminConfigurationWithReadsAndSharedSessions()
|
||||
{
|
||||
WithEnvironment("true", null, null, () =>
|
||||
{
|
||||
var options = AdminMigrationOptions.FromEnvironment(
|
||||
configuredNativeReadsEnabled: false,
|
||||
authenticationNativeEnabled: true,
|
||||
sharesLegacySessions: true);
|
||||
|
||||
Assert.True(options.NativeReadsEnabled);
|
||||
Assert.True(options.NativeConfigurationEnabled);
|
||||
}, configurationEnabled: "true");
|
||||
}
|
||||
|
||||
private static void WithEnvironment(
|
||||
string? nativeReadsEnabled,
|
||||
string? nativeOrganizationWritesEnabled,
|
||||
string? allowMemory,
|
||||
Action test,
|
||||
string? accountBatchesEnabled = null)
|
||||
string? accountBatchesEnabled = null,
|
||||
string? configurationEnabled = null)
|
||||
{
|
||||
var previousNativeReadsEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED");
|
||||
var previousNativeOrganizationWritesEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED");
|
||||
var previousAccountBatchesEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED");
|
||||
var previousConfigurationEnabled = Environment.GetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED");
|
||||
var previousAllowMemory = Environment.GetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY");
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED", nativeReadsEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED", nativeOrganizationWritesEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED", accountBatchesEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED", configurationEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", allowMemory);
|
||||
test();
|
||||
}
|
||||
@@ -131,6 +149,7 @@ public sealed class AdminMigrationOptionsTests
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED", previousNativeReadsEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED", previousNativeOrganizationWritesEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED", previousAccountBatchesEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED", previousConfigurationEnabled);
|
||||
Environment.SetEnvironmentVariable("ADMIN_NATIVE_ALLOW_MEMORY", previousAllowMemory);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user