本批“报名审核与缴费更新”迁移完成:
PATCH /api/admin/registrations/{registrationId}
PATCH /api/admin/payments/{registrationId}
实现了多级报名审核、终审绑定报名号与号码规则、缴费确认/撤销、状态防重、考试归档锁定及事务审计
This commit is contained in:
@@ -8,7 +8,8 @@ public sealed record AdminMigrationOptions(
|
||||
bool NativeNoticeManagementEnabled = false,
|
||||
bool NativeCentersEnabled = false,
|
||||
bool NativeOperationalReadsEnabled = false,
|
||||
bool NativeCandidateManagementEnabled = false)
|
||||
bool NativeCandidateManagementEnabled = false,
|
||||
bool NativeRegistrationPaymentWritesEnabled = false)
|
||||
{
|
||||
public static AdminMigrationOptions FromEnvironment(
|
||||
bool configuredNativeReadsEnabled,
|
||||
@@ -20,7 +21,8 @@ public sealed record AdminMigrationOptions(
|
||||
bool configuredNativeNoticeManagementEnabled = false,
|
||||
bool configuredNativeCentersEnabled = false,
|
||||
bool configuredNativeOperationalReadsEnabled = false,
|
||||
bool configuredNativeCandidateManagementEnabled = false)
|
||||
bool configuredNativeCandidateManagementEnabled = false,
|
||||
bool configuredNativeRegistrationPaymentWritesEnabled = false)
|
||||
{
|
||||
var readsEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
|
||||
@@ -47,17 +49,20 @@ public sealed record AdminMigrationOptions(
|
||||
var candidateManagementEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_CANDIDATE_MANAGEMENT_ENABLED"),
|
||||
configuredNativeCandidateManagementEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled) && !readsEnabled)
|
||||
var registrationPaymentWritesEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_REGISTRATION_PAYMENT_WRITES_ENABLED"),
|
||||
configuredNativeRegistrationPaymentWritesEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled) && !readsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
|
||||
}
|
||||
if (candidateManagementEnabled && !operationalReadsEnabled)
|
||||
if ((candidateManagementEnabled || registrationPaymentWritesEnabled) && !operationalReadsEnabled)
|
||||
{
|
||||
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;
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled || centersEnabled || operationalReadsEnabled || candidateManagementEnabled || registrationPaymentWritesEnabled;
|
||||
if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -73,7 +78,7 @@ public sealed record AdminMigrationOptions(
|
||||
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话");
|
||||
}
|
||||
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeManagementEnabled, centersEnabled, operationalReadsEnabled, candidateManagementEnabled);
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeManagementEnabled, centersEnabled, operationalReadsEnabled, candidateManagementEnabled, registrationPaymentWritesEnabled);
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
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 AdminRegistrationManagementService(
|
||||
IAuthenticationStateStore authenticationState,
|
||||
AuthenticationRepository authenticationRepository,
|
||||
AdminAccountBatchSnapshotLoader workflowSnapshotLoader,
|
||||
AdminOperationalSnapshotLoader operationalSnapshotLoader,
|
||||
AdminWriteRepository repository) : IAdminRegistrationManagementService
|
||||
{
|
||||
public async Task<AdminEndpointResult> UpdatePaymentAsync(
|
||||
string sessionToken,
|
||||
string registrationId,
|
||||
JsonObject body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var user = context.User!;
|
||||
var workflow = await workflowSnapshotLoader.LoadAsync(cancellationToken);
|
||||
var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
|
||||
var registration = operational.Registrations.FirstOrDefault(item => item.Id == registrationId);
|
||||
var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration?.UserId);
|
||||
if (registration is null || profile is null || !InScope(user, profile))
|
||||
return Error(404, "缴费记录不存在或不在当前管理范围内");
|
||||
if (registration.Status != "approved") return Error(409, "报名审核通过后才能确认缴费");
|
||||
var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
|
||||
if (OptionalString(exam?.Data, "archivedAt") is not null) return Error(409, "该考试已归档,缴费记录已冻结");
|
||||
|
||||
var nextStatus = JsBoolean(body["status"]) ? Text(body["status"]) : "paid";
|
||||
if (nextStatus is not ("paid" or "unpaid")) return Error(400, "缴费状态无效");
|
||||
if (registration.PaymentStatus == nextStatus)
|
||||
return Error(409, $"该考生已经是{(nextStatus == "paid" ? "已缴费" : "待缴费")}状态");
|
||||
|
||||
var paidAt = nextStatus == "paid" ? NowIso() : null;
|
||||
var paidBy = nextStatus == "paid" ? user.Id : null;
|
||||
var name = profile.Data["name"]?.GetValue<string>();
|
||||
var registrationNumber = registration.Data["registrationNumber"]?.GetValue<string>() ?? "";
|
||||
var examName = exam?.Data["name"]?.GetValue<string>() ?? registration.ExamId;
|
||||
await repository.UpdateRegistrationPaymentAsync(
|
||||
registration.Id,
|
||||
nextStatus,
|
||||
paidAt,
|
||||
paidBy,
|
||||
Audit(
|
||||
user,
|
||||
nextStatus == "paid" ? "标记考生已缴费" : "撤销考生缴费确认",
|
||||
$"{(string.IsNullOrEmpty(name) ? registrationNumber : name)} · {examName}"),
|
||||
cancellationToken);
|
||||
return Success(new JsonObject
|
||||
{
|
||||
["ok"] = true,
|
||||
["payment"] = new JsonObject
|
||||
{
|
||||
["registrationId"] = registration.Id,
|
||||
["status"] = nextStatus,
|
||||
["paidAt"] = JsonValue.Create(paidAt),
|
||||
["paidBy"] = JsonValue.Create(paidBy),
|
||||
["paidByName"] = nextStatus == "paid" ? user.DisplayName : ""
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AdminEndpointResult> ReviewAsync(
|
||||
string sessionToken,
|
||||
string registrationId,
|
||||
JsonObject body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await ResolveAsync(sessionToken, cancellationToken);
|
||||
if (context.Error is not null) return context.Error;
|
||||
var user = context.User!;
|
||||
var workflowSnapshot = await workflowSnapshotLoader.LoadAsync(cancellationToken);
|
||||
var operational = await operationalSnapshotLoader.LoadAsync(cancellationToken);
|
||||
var registration = operational.Registrations.FirstOrDefault(item => item.Id == registrationId);
|
||||
if (registration is null) return Error(404, "报名记录不存在");
|
||||
var exam = operational.Exams.FirstOrDefault(item => item.Id == registration.ExamId);
|
||||
if (OptionalString(exam?.Data, "archivedAt") is not null) return Error(409, "该考试已归档,报名流程已冻结");
|
||||
var profile = operational.Profiles.FirstOrDefault(item => item.UserId == registration.UserId);
|
||||
if (Level(user) != "super" && (profile is null || !InScope(user, profile)))
|
||||
return Error(403, "该报名不在你的数据范围内");
|
||||
var requestedStatus = Text(body["status"]);
|
||||
if (requestedStatus is not ("approved" or "rejected")) return Error(400, "审核状态无效");
|
||||
|
||||
var instance = workflowSnapshot.Instances.FirstOrDefault(item =>
|
||||
item.BusinessType == "registration_review" && item.BusinessId == registration.Id && item.Status == "pending");
|
||||
if (instance is null) return Error(409, "当前没有待处理的报名审核流程");
|
||||
var workflow = workflowSnapshot.Workflows.FirstOrDefault(item => item.Id == instance.WorkflowId);
|
||||
var step = workflow?.Steps.FirstOrDefault(item => item.Position == instance.CurrentStep);
|
||||
if (workflow is null || step is null) return Error(409, "报名审核流程状态异常");
|
||||
if (Level(user) != "super" && (instance.AssigneeId != user.Id || step.AdminLevel != Level(user)))
|
||||
return Error(403, "该流程当前未分配给你,可由当前处理人转交");
|
||||
|
||||
var note = Clean(Text(body["reviewNote"]), 300);
|
||||
var action = new AccountWorkflowAction(
|
||||
Uid("flow_action"), instance.Id, user.Id, requestedStatus == "approved" ? "approve" : "reject",
|
||||
note, instance.AssigneeId, null, NowIso());
|
||||
var updatedInstance = instance;
|
||||
var output = registration.Data.DeepClone().AsObject();
|
||||
if (requestedStatus == "rejected")
|
||||
{
|
||||
updatedInstance = instance with { Status = "rejected", CompletedAt = NowIso(), AssigneeId = null };
|
||||
output["status"] = "rejected";
|
||||
output["reviewNote"] = note;
|
||||
output["reviewedAt"] = NowIso();
|
||||
}
|
||||
else if (instance.CurrentStep < workflow.Steps.Count)
|
||||
{
|
||||
var nextStep = workflow.Steps.FirstOrDefault(item => item.Position == instance.CurrentStep + 1);
|
||||
var nextAssignee = nextStep is null ? null : SelectAssignee(workflowSnapshot, nextStep.AdminLevel, profile);
|
||||
if (nextAssignee is null) return Error(409, $"没有可承接“{nextStep?.Name ?? "下一审批步骤"}”的管理员");
|
||||
updatedInstance = instance with { CurrentStep = instance.CurrentStep + 1, AssigneeId = nextAssignee.Id };
|
||||
action = action with { ToAssigneeId = nextAssignee.Id };
|
||||
output["status"] = "pending";
|
||||
output["reviewNote"] = note;
|
||||
}
|
||||
else
|
||||
{
|
||||
var account = workflowSnapshot.Users.FirstOrDefault(item => item.Id == registration.UserId);
|
||||
if (string.IsNullOrEmpty(account?.CandidateNumber))
|
||||
return Error(409, "考生账户尚未分配报名号,请先在报名号管理中完成分配");
|
||||
updatedInstance = instance with { Status = "approved", CompletedAt = NowIso(), AssigneeId = null };
|
||||
output["status"] = "approved";
|
||||
output["reviewNote"] = note;
|
||||
output["reviewedAt"] = NowIso();
|
||||
output["registrationNumber"] = account.CandidateNumber;
|
||||
output["numberRuleId"] = JsonValue.Create(
|
||||
workflowSnapshot.NumberRules.FirstOrDefault(item => item.Active)?.Id ?? OptionalString(output, "numberRuleId"));
|
||||
}
|
||||
|
||||
var profileName = profile?.Data["name"]?.GetValue<string>() ?? registration.UserId;
|
||||
var examName = exam?.Data["name"]?.GetValue<string>() ?? "";
|
||||
await repository.ProcessRegistrationWorkflowAsync(
|
||||
updatedInstance,
|
||||
action,
|
||||
registration.Id,
|
||||
output["status"]?.GetValue<string>() ?? registration.Status,
|
||||
output["paymentStatus"]?.GetValue<string>() ?? registration.PaymentStatus,
|
||||
OptionalString(output, "paidAt"),
|
||||
OptionalString(output, "paidBy"),
|
||||
OptionalString(output, "reviewedAt"),
|
||||
output["reviewNote"]?.GetValue<string>() ?? "",
|
||||
output["registrationNumber"]?.GetValue<string>() ?? "",
|
||||
OptionalString(output, "numberRuleId"),
|
||||
Audit(
|
||||
user,
|
||||
requestedStatus == "approved" ? "处理报名审核流程" : "退回考试报名",
|
||||
$"{profileName} · {examName}"),
|
||||
cancellationToken);
|
||||
return Success(new JsonObject
|
||||
{
|
||||
["ok"] = true,
|
||||
["registration"] = output,
|
||||
["workflow"] = WorkflowJson(workflowSnapshot, updatedInstance, action)
|
||||
});
|
||||
}
|
||||
|
||||
private static AdminUser? SelectAssignee(
|
||||
AdminAccountBatchSnapshot snapshot,
|
||||
string level,
|
||||
OperationalProfile? profile)
|
||||
{
|
||||
var pending = snapshot.Instances.Where(item => item.Status == "pending" && item.AssigneeId is not null)
|
||||
.GroupBy(item => item.AssigneeId!)
|
||||
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
|
||||
var assigned = snapshot.Actions.Where(item => item.ToAssigneeId is not null)
|
||||
.GroupBy(item => item.ToAssigneeId!)
|
||||
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
|
||||
return snapshot.Users
|
||||
.Where(item => item.Role == "admin" && item.Active && item.AdminLevel == level && level switch
|
||||
{
|
||||
"super" => true,
|
||||
"school" => profile?.SchoolId is not null && item.SchoolId == profile.SchoolId,
|
||||
"class" => profile?.SchoolId is not null && profile.ClassId is not null &&
|
||||
item.SchoolId == profile.SchoolId && item.ClassId == profile.ClassId,
|
||||
_ => false
|
||||
})
|
||||
.OrderBy(item => pending.GetValueOrDefault(item.Id))
|
||||
.ThenBy(item => assigned.GetValueOrDefault(item.Id))
|
||||
.ThenBy(item => item.CreatedAt, StringComparer.Ordinal)
|
||||
.ThenBy(item => item.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static JsonObject WorkflowJson(
|
||||
AdminAccountBatchSnapshot snapshot,
|
||||
AccountWorkflowInstance instance,
|
||||
AccountWorkflowAction action)
|
||||
{
|
||||
var workflow = snapshot.Workflows.FirstOrDefault(item => item.Id == instance.WorkflowId);
|
||||
var assignee = snapshot.Users.FirstOrDefault(item => item.Id == instance.AssigneeId);
|
||||
var actions = snapshot.Actions.Where(item => item.InstanceId == instance.Id).Append(action)
|
||||
.Select(item => ActionJson(snapshot, item)).ToArray();
|
||||
return new JsonObject
|
||||
{
|
||||
["id"] = instance.Id,
|
||||
["workflowId"] = instance.WorkflowId,
|
||||
["businessType"] = instance.BusinessType,
|
||||
["businessId"] = instance.BusinessId,
|
||||
["status"] = instance.Status,
|
||||
["currentStep"] = instance.CurrentStep,
|
||||
["assigneeId"] = JsonValue.Create(instance.AssigneeId),
|
||||
["createdAt"] = instance.CreatedAt,
|
||||
["completedAt"] = JsonValue.Create(instance.CompletedAt),
|
||||
["workflowName"] = workflow?.Name ?? "未命名流程",
|
||||
["steps"] = new JsonArray((workflow?.Steps ?? []).Select(StepJson).ToArray()),
|
||||
["currentStepDetail"] = workflow?.Steps.FirstOrDefault(item => item.Position == instance.CurrentStep) is { } currentStep ? StepJson(currentStep) : null,
|
||||
["assignee"] = assignee is null ? null : SafeUser(assignee),
|
||||
["actions"] = new JsonArray(actions)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ResolvedAdmin> ResolveAsync(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, "请先登录"));
|
||||
return user.Role == "admin" ? new(user, null) : ResolvedAdmin.Failed(Error(403, "当前账号无权执行此操作"));
|
||||
}
|
||||
|
||||
private static bool InScope(AuthenticationUser user, OperationalProfile profile) => Level(user) switch
|
||||
{
|
||||
"super" => true,
|
||||
"school" => user.SchoolId is not null && profile.SchoolId == user.SchoolId,
|
||||
_ => user.ClassId is not null && profile.ClassId == user.ClassId
|
||||
};
|
||||
|
||||
private static bool JsBoolean(JsonNode? node)
|
||||
{
|
||||
if (node is null) return false;
|
||||
if (node is not JsonValue value) return true;
|
||||
if (value.TryGetValue<bool>(out var boolean)) return boolean;
|
||||
if (value.TryGetValue<string>(out var text)) return text.Length > 0;
|
||||
if (value.TryGetValue<double>(out var number)) return number != 0 && !double.IsNaN(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? OptionalString(JsonObject? value, string property)
|
||||
{
|
||||
if (value?[property] is not JsonValue node) return null;
|
||||
return node.TryGetValue<string>(out var text) ? text : null;
|
||||
}
|
||||
|
||||
private static string Text(JsonNode? node)
|
||||
{
|
||||
if (node is null) return "";
|
||||
if (node is JsonValue value && value.TryGetValue<string>(out var text)) return text;
|
||||
return node.ToString();
|
||||
}
|
||||
|
||||
private static string Clean(string value, int maximum)
|
||||
{
|
||||
var cleaned = value.Trim();
|
||||
return cleaned[..Math.Min(cleaned.Length, maximum)];
|
||||
}
|
||||
|
||||
private static string Level(AuthenticationUser user) => user.AdminLevel ?? "super";
|
||||
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 JsonObject StepJson(AccountWorkflowStep item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["name"] = item.Name,
|
||||
["adminLevel"] = item.AdminLevel,
|
||||
["position"] = item.Position
|
||||
};
|
||||
|
||||
private static JsonObject ActionJson(AdminAccountBatchSnapshot snapshot, AccountWorkflowAction item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["instanceId"] = item.InstanceId,
|
||||
["actorId"] = JsonValue.Create(item.ActorId),
|
||||
["action"] = item.Action,
|
||||
["note"] = item.Note,
|
||||
["fromAssigneeId"] = JsonValue.Create(item.FromAssigneeId),
|
||||
["toAssigneeId"] = JsonValue.Create(item.ToAssigneeId),
|
||||
["createdAt"] = item.CreatedAt,
|
||||
["actorName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.ActorId)?.DisplayName ?? "系统",
|
||||
["fromAssigneeName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.FromAssigneeId)?.DisplayName ?? "",
|
||||
["toAssigneeName"] = snapshot.Users.FirstOrDefault(user => user.Id == item.ToAssigneeId)?.DisplayName ?? ""
|
||||
};
|
||||
|
||||
private static JsonObject SafeUser(AdminUser item) => new()
|
||||
{
|
||||
["id"] = item.Id,
|
||||
["username"] = item.Username,
|
||||
["role"] = item.Role,
|
||||
["adminLevel"] = item.Role == "admin" ? item.AdminLevel ?? "super" : null,
|
||||
["schoolId"] = JsonValue.Create(item.SchoolId),
|
||||
["classId"] = JsonValue.Create(item.ClassId),
|
||||
["displayName"] = item.DisplayName,
|
||||
["candidateNumber"] = JsonValue.Create(item.CandidateNumber),
|
||||
["mustChangePassword"] = item.MustChangePassword,
|
||||
["totpEnabled"] = item.TotpEnabled,
|
||||
["archived"] = item.ArchivedAt is not null
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,80 @@ internal sealed class AdminWriteRepository(IRelationalConnectionFactory connecti
|
||||
audit,
|
||||
cancellationToken);
|
||||
|
||||
public Task UpdateRegistrationPaymentAsync(
|
||||
string registrationId,
|
||||
string paymentStatus,
|
||||
string? paidAt,
|
||||
string? paidBy,
|
||||
AdminAuditEntry audit,
|
||||
CancellationToken cancellationToken) => ExecuteWithAuditAsync(
|
||||
new SqlOperation(
|
||||
"""
|
||||
UPDATE registrations SET payment_status = @paymentStatus, paid_at = @paidAt, paid_by = @paidBy
|
||||
WHERE id = @id
|
||||
""",
|
||||
[
|
||||
new("@paymentStatus", paymentStatus), new("@paidAt", paidAt),
|
||||
new("@paidBy", paidBy), new("@id", registrationId)
|
||||
]),
|
||||
audit,
|
||||
cancellationToken);
|
||||
|
||||
public Task ProcessRegistrationWorkflowAsync(
|
||||
AccountWorkflowInstance instance,
|
||||
AccountWorkflowAction action,
|
||||
string registrationId,
|
||||
string status,
|
||||
string paymentStatus,
|
||||
string? paidAt,
|
||||
string? paidBy,
|
||||
string? reviewedAt,
|
||||
string reviewNote,
|
||||
string registrationNumber,
|
||||
string? numberRuleId,
|
||||
AdminAuditEntry audit,
|
||||
CancellationToken cancellationToken) => ExecuteWithAuditAsync(
|
||||
[
|
||||
new SqlOperation(
|
||||
"""
|
||||
UPDATE workflow_instances SET status = @status, current_step = @currentStep,
|
||||
assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
|
||||
""",
|
||||
[
|
||||
new("@status", instance.Status), new("@currentStep", instance.CurrentStep),
|
||||
new("@assigneeId", instance.AssigneeId), new("@completedAt", instance.CompletedAt),
|
||||
new("@id", instance.Id)
|
||||
]),
|
||||
new SqlOperation(
|
||||
"""
|
||||
INSERT INTO workflow_actions (
|
||||
id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at
|
||||
) VALUES (
|
||||
@id, @instanceId, @actorId, @action, @note, @fromAssigneeId, @toAssigneeId, @createdAt
|
||||
)
|
||||
""",
|
||||
[
|
||||
new("@id", action.Id), new("@instanceId", action.InstanceId), new("@actorId", action.ActorId),
|
||||
new("@action", action.Action), new("@note", action.Note), new("@fromAssigneeId", action.FromAssigneeId),
|
||||
new("@toAssigneeId", action.ToAssigneeId), new("@createdAt", action.CreatedAt)
|
||||
]),
|
||||
new SqlOperation(
|
||||
"""
|
||||
UPDATE registrations SET status = @status, payment_status = @paymentStatus,
|
||||
paid_at = @paidAt, paid_by = @paidBy, reviewed_at = @reviewedAt,
|
||||
review_note = @reviewNote, registration_number = @registrationNumber,
|
||||
number_rule_id = @numberRuleId WHERE id = @id
|
||||
""",
|
||||
[
|
||||
new("@status", status), new("@paymentStatus", paymentStatus), new("@paidAt", paidAt),
|
||||
new("@paidBy", paidBy), new("@reviewedAt", reviewedAt), new("@reviewNote", Optional(reviewNote)),
|
||||
new("@registrationNumber", Optional(registrationNumber)), new("@numberRuleId", numberRuleId),
|
||||
new("@id", registrationId)
|
||||
])
|
||||
],
|
||||
audit,
|
||||
cancellationToken);
|
||||
|
||||
private async Task ExecuteWithAuditAsync(
|
||||
SqlOperation operation,
|
||||
AdminAuditEntry audit,
|
||||
|
||||
@@ -69,6 +69,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAdminCenterService, AdminCenterService>();
|
||||
services.AddScoped<IAdminOperationalReadService, AdminOperationalReadService>();
|
||||
services.AddScoped<IAdminCandidateManagementService, AdminCandidateManagementService>();
|
||||
services.AddScoped<IAdminRegistrationManagementService, AdminRegistrationManagementService>();
|
||||
services.AddSingleton<NoticeContentFormatter>();
|
||||
services.AddScoped<IPublicQueryService, PublicQueryService>();
|
||||
return services;
|
||||
|
||||
Reference in New Issue
Block a user