本轮完成了完整的通知公告管理迁移:
原生 GET /api/admin/notices
原生五类系统公示聚合:计划、资格、录取、分数线、报到
原生 PATCH /api/admin/publications/{type}/{id} 显隐控制
权限、事务审计和 Node 返回结构保持一致
开关升级为 ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED
兼容旧变量 ADMIN_NATIVE_NOTICE_WRITES_ENABLED
冒烟脚本现在会自动构建 Web,避免使用过期程序集
This commit is contained in:
1 parent
9e21a277f0
commit
e845ba8d3a
13 files changed
+597
-37
No files matched your search
@@ -4,6 +4,13 @@ namespace Eis.Application.Administration;
|
||||
|
||||
public interface IAdminNoticeService
|
||||
{
|
||||
Task<AdminEndpointResult> ListAsync(string sessionToken, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> CreateAsync(string sessionToken, JsonObject body, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> UpdateAsync(string sessionToken, string noticeId, JsonObject body, CancellationToken cancellationToken);
|
||||
Task<AdminEndpointResult> UpdatePublicationVisibilityAsync(
|
||||
string sessionToken,
|
||||
string sourceType,
|
||||
string publicationId,
|
||||
JsonObject body,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ public sealed record AdminMigrationOptions(
|
||||
bool NativeOrganizationWritesEnabled = false,
|
||||
bool NativeAccountBatchesEnabled = false,
|
||||
bool NativeConfigurationEnabled = false,
|
||||
bool NativeNoticeWritesEnabled = false)
|
||||
bool NativeNoticeManagementEnabled = false)
|
||||
{
|
||||
public static AdminMigrationOptions FromEnvironment(
|
||||
bool configuredNativeReadsEnabled,
|
||||
@@ -14,7 +14,7 @@ public sealed record AdminMigrationOptions(
|
||||
bool configuredNativeOrganizationWritesEnabled = false,
|
||||
bool configuredNativeAccountBatchesEnabled = false,
|
||||
bool configuredNativeConfigurationEnabled = false,
|
||||
bool configuredNativeNoticeWritesEnabled = false)
|
||||
bool configuredNativeNoticeManagementEnabled = false)
|
||||
{
|
||||
var readsEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_READS_ENABLED"),
|
||||
@@ -28,15 +28,16 @@ public sealed record AdminMigrationOptions(
|
||||
var configurationEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_CONFIGURATION_ENABLED"),
|
||||
configuredNativeConfigurationEnabled);
|
||||
var noticeWritesEnabled = ParseBoolean(
|
||||
var noticeManagementEnabled = ParseBoolean(
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED") ??
|
||||
Environment.GetEnvironmentVariable("ADMIN_NATIVE_NOTICE_WRITES_ENABLED"),
|
||||
configuredNativeNoticeWritesEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeWritesEnabled) && !readsEnabled)
|
||||
configuredNativeNoticeManagementEnabled);
|
||||
if ((organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled) && !readsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"启用原生组织维护接口前必须同时设置 ADMIN_NATIVE_READS_ENABLED=true");
|
||||
}
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeWritesEnabled;
|
||||
var anyNativeAdminEndpointEnabled = readsEnabled || organizationWritesEnabled || accountBatchesEnabled || configurationEnabled || noticeManagementEnabled;
|
||||
if (anyNativeAdminEndpointEnabled && !authenticationNativeEnabled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -52,7 +53,7 @@ public sealed record AdminMigrationOptions(
|
||||
"管理端仍有接口需要转发给 Node;启用原生管理端接口必须配置共享 Redis 会话");
|
||||
}
|
||||
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeWritesEnabled);
|
||||
return new AdminMigrationOptions(readsEnabled, organizationWritesEnabled, accountBatchesEnabled, configurationEnabled, noticeManagementEnabled);
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string? value, bool fallback) => value?.Trim().ToLowerInvariant() switch
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Eis.Infrastructure.Data;
|
||||
|
||||
namespace Eis.Infrastructure.Administration;
|
||||
@@ -8,6 +9,28 @@ internal sealed record AdminNotice(string Id, string Title, string Summary, stri
|
||||
|
||||
internal sealed class AdminNoticeRepository(IRelationalConnectionFactory connectionFactory)
|
||||
{
|
||||
public async Task<AdminNoticeManagementSnapshot> LoadManagementAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
var notices = new List<AdminNotice>();
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "SELECT id, title, summary, content, category, pinned, status, publish_at, created_at, author FROM notices ORDER BY created_at, id";
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken)) notices.Add(Read(reader));
|
||||
}
|
||||
var records = new List<AdminPublicationRecord>();
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "SELECT id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at FROM admission_records ORDER BY created_at, id";
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken)) records.Add(ReadPublication(reader));
|
||||
}
|
||||
var examNames = await ReadNamesAsync(connection, "SELECT id, name FROM exams ORDER BY created_at, id", cancellationToken);
|
||||
var schoolNames = await ReadNamesAsync(connection, "SELECT id, name FROM schools ORDER BY name, id", cancellationToken);
|
||||
return new AdminNoticeManagementSnapshot(notices, records, examNames, schoolNames);
|
||||
}
|
||||
|
||||
public async Task<AdminNotice?> FindAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
@@ -30,6 +53,34 @@ internal sealed class AdminNoticeRepository(IRelationalConnectionFactory connect
|
||||
pinned = @pinned, status = @status, publish_at = @publishAt WHERE id = @id
|
||||
""", notice, audit, cancellationToken);
|
||||
|
||||
public async Task UpdatePublicationAsync(
|
||||
AdminPublicationRecord publication,
|
||||
AdminAuditEntry audit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "UPDATE admission_records SET payload_json = @payload, updated_at = @updatedAt WHERE id = @id";
|
||||
Add(command, "@payload", publication.Payload.ToJsonString());
|
||||
Add(command, "@updatedAt", publication.UpdatedAt);
|
||||
Add(command, "@id", publication.Id);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await InsertAuditAsync(connection, transaction, audit, cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(string sql, AdminNotice notice, AdminAuditEntry audit, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
|
||||
@@ -45,13 +96,7 @@ internal sealed class AdminNoticeRepository(IRelationalConnectionFactory connect
|
||||
Add(command, "@status", notice.Status); Add(command, "@publishAt", notice.PublishAt); Add(command, "@createdAt", notice.CreatedAt); Add(command, "@author", notice.Author);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)";
|
||||
Add(command, "@id", audit.Id); Add(command, "@actorId", audit.ActorId); Add(command, "@action", audit.Action); Add(command, "@detail", audit.Detail); Add(command, "@createdAt", audit.CreatedAt);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
await InsertAuditAsync(connection, transaction, audit, cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
@@ -65,6 +110,48 @@ internal sealed class AdminNoticeRepository(IRelationalConnectionFactory connect
|
||||
Text(reader, "id"), Text(reader, "title"), Text(reader, "summary"), Text(reader, "content"), Text(reader, "category"),
|
||||
Convert.ToInt64(reader.GetValue(reader.GetOrdinal("pinned")), CultureInfo.InvariantCulture) != 0, Text(reader, "status"),
|
||||
Optional(reader, "publish_at"), Optional(reader, "created_at"), Text(reader, "author"));
|
||||
|
||||
private static AdminPublicationRecord ReadPublication(DbDataReader reader) => new(
|
||||
Text(reader, "id"),
|
||||
Text(reader, "kind"),
|
||||
Text(reader, "exam_id"),
|
||||
Optional(reader, "user_id"),
|
||||
Optional(reader, "school_id"),
|
||||
Text(reader, "status"),
|
||||
JsonNode.Parse(Text(reader, "payload_json")) as JsonObject ?? new JsonObject(),
|
||||
Text(reader, "created_at"),
|
||||
Text(reader, "updated_at"));
|
||||
|
||||
private static async Task<IReadOnlyDictionary<string, string>> ReadNamesAsync(
|
||||
DbConnection connection,
|
||||
string sql,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var names = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken)) names[Text(reader, "id")] = Text(reader, "name");
|
||||
return names;
|
||||
}
|
||||
|
||||
private static async Task InsertAuditAsync(
|
||||
DbConnection connection,
|
||||
DbTransaction transaction,
|
||||
AdminAuditEntry audit,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = "INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (@id, @actorId, @action, @detail, @createdAt)";
|
||||
Add(command, "@id", audit.Id);
|
||||
Add(command, "@actorId", audit.ActorId);
|
||||
Add(command, "@action", audit.Action);
|
||||
Add(command, "@detail", audit.Detail);
|
||||
Add(command, "@createdAt", audit.CreatedAt);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string Text(DbDataReader reader, string name) => Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? "";
|
||||
private static string? Optional(DbDataReader reader, string name) { var index = reader.GetOrdinal(name); return reader.IsDBNull(index) ? null : Convert.ToString(reader.GetValue(index), CultureInfo.InvariantCulture); }
|
||||
private static void Add(DbCommand command, string name, object? value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value ?? DBNull.Value; command.Parameters.Add(parameter); }
|
||||
|
||||
Loaded 3 of 13 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user