本阶段迁移已完成。
Excel:班级、账号、考生、缴费、考场、成绩、志愿、录取、报到及正式录取名册均改为 ClosedXML 原生处理,支持模板、校验、筛选和导入预检。[SystemWorkbook.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Spreadsheets\\SystemWorkbook.cs) 文书:录取通知书模板、成绩单/准考证/通知书业务数据与防伪验真已脱离 Node;保留现有前端排版导出效果。[AdminAdmissionService.Documents.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Administration\\AdminAdmissionService.Documents.cs) 缓存:完成 Redis 命名空间版本缓存、有界本地回退、请求合并和并发失效保护;公开数据及成绩写入后立即失效。[ApplicationCache.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Caching\\ApplicationCache.cs) 迁移清单已更新为完成。[MIGRATION.md](C:\\Users\\BI\\Documents\\EIS-dotnet\\MIGRATION.md)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Eis.Infrastructure.Caching;
|
||||
|
||||
namespace Eis.Infrastructure.Tests.Caching;
|
||||
|
||||
public sealed class ApplicationCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RememberJsonAsync_CoalescesConcurrentLocalLoads()
|
||||
{
|
||||
using var cache = new ApplicationCache(redisUrl: null);
|
||||
var calls = 0;
|
||||
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
async Task<JsonObject> Load()
|
||||
{
|
||||
Interlocked.Increment(ref calls);
|
||||
await release.Task;
|
||||
return new JsonObject { ["value"] = 42 };
|
||||
}
|
||||
|
||||
var first = cache.RememberJsonAsync("public", "home", Load);
|
||||
var second = cache.RememberJsonAsync("public", "home", Load);
|
||||
release.SetResult();
|
||||
|
||||
var values = await Task.WhenAll(first, second);
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.All(values, value => Assert.Equal(42, value["value"]!.GetValue<int>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvalidateAsync_DropsLocalNamespaceWithoutAffectingOthers()
|
||||
{
|
||||
using var cache = new ApplicationCache(redisUrl: null);
|
||||
var publicCalls = 0;
|
||||
var resultCalls = 0;
|
||||
|
||||
Task<JsonObject> PublicLoader() =>
|
||||
Task.FromResult(new JsonObject { ["generation"] = ++publicCalls });
|
||||
Task<JsonObject> ResultLoader() =>
|
||||
Task.FromResult(new JsonObject { ["generation"] = ++resultCalls });
|
||||
|
||||
_ = await cache.RememberJsonAsync("public", "home", PublicLoader);
|
||||
_ = await cache.RememberJsonAsync("results", "candidate:1", ResultLoader);
|
||||
Assert.False(await cache.InvalidateAsync("public"));
|
||||
var publicValue = await cache.RememberJsonAsync("public", "home", PublicLoader);
|
||||
var resultValue = await cache.RememberJsonAsync("results", "candidate:1", ResultLoader);
|
||||
|
||||
Assert.Equal(2, publicValue["generation"]!.GetValue<int>());
|
||||
Assert.Equal(1, resultValue["generation"]!.GetValue<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvalidateAsync_PreventsInFlightLoadFromRepopulatingOldGeneration()
|
||||
{
|
||||
using var cache = new ApplicationCache(redisUrl: null);
|
||||
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var calls = 0;
|
||||
async Task<JsonObject> SlowLoader()
|
||||
{
|
||||
var value = Interlocked.Increment(ref calls);
|
||||
await release.Task;
|
||||
return new JsonObject { ["generation"] = value };
|
||||
}
|
||||
|
||||
var staleLoad = cache.RememberJsonAsync("public", "home", SlowLoader);
|
||||
_ = await cache.InvalidateAsync("public");
|
||||
var refreshed = await cache.RememberJsonAsync(
|
||||
"public",
|
||||
"home",
|
||||
() => Task.FromResult(new JsonObject { ["generation"] = Interlocked.Increment(ref calls) }));
|
||||
release.SetResult();
|
||||
_ = await staleLoad;
|
||||
|
||||
Assert.Equal(2, refreshed["generation"]!.GetValue<int>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using ClosedXML.Excel;
|
||||
using Eis.Infrastructure.Spreadsheets;
|
||||
|
||||
namespace Eis.Infrastructure.Tests.Spreadsheets;
|
||||
|
||||
public sealed class SystemWorkbookTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdmissionReporting_RoundTripsEditableFieldsAndKeepsProfessionalLayout()
|
||||
{
|
||||
var content = SystemWorkbook.Build(
|
||||
"admission_reporting",
|
||||
[
|
||||
new JsonObject
|
||||
{
|
||||
["noticeNumber"] = "AD01-EX-2026-ZK-000001",
|
||||
["candidateNumber"] = "2026-HZ01-F-0001",
|
||||
["name"] = "张同学",
|
||||
["examCode"] = "EX-2026-ZK",
|
||||
["schoolCode"] = "AD01",
|
||||
["categoryName"] = "普通生",
|
||||
["reportingStatusCode"] = "Y",
|
||||
["reportingNote"] = "材料齐全"
|
||||
}
|
||||
],
|
||||
subtitle: "第 1 轮");
|
||||
|
||||
using var workbook = new XLWorkbook(new MemoryStream(content));
|
||||
var sheet = workbook.Worksheet("考生报到");
|
||||
|
||||
Assert.True(content.Length > 1000);
|
||||
Assert.Equal(XLColor.FromHtml("#173F60"), sheet.Cell(1, 1).Style.Fill.BackgroundColor);
|
||||
Assert.Equal(XLColor.FromHtml("#FFF3CD"), sheet.Cell(3, 7).Style.Fill.BackgroundColor);
|
||||
Assert.Equal(XLColor.FromHtml("#FFF3CD"), sheet.Cell(3, 8).Style.Fill.BackgroundColor);
|
||||
|
||||
var parsed = SystemWorkbook.Parse("admission_reporting", content);
|
||||
Assert.Single(parsed);
|
||||
Assert.Equal("Y", parsed[0]["reportingStatusCode"]!.GetValue<string>());
|
||||
Assert.Equal("材料齐全", parsed[0]["reportingNote"]!.GetValue<string>());
|
||||
Assert.Equal(3, parsed[0]["__row"]!.GetValue<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Template_ContainsExampleRowAndValidation()
|
||||
{
|
||||
var content = SystemWorkbook.Build("classes", [], template: true);
|
||||
|
||||
using var workbook = new XLWorkbook(new MemoryStream(content));
|
||||
var sheet = workbook.Worksheet("班级");
|
||||
|
||||
Assert.Equal("HZ01", sheet.Cell(3, 1).GetString());
|
||||
Assert.Equal("启用", sheet.Cell(3, 4).GetString());
|
||||
Assert.NotEmpty(sheet.DataValidations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidWorkbook_IsRejectedWithBusinessError()
|
||||
{
|
||||
var error = Assert.Throws<WorkbookException>(() =>
|
||||
SystemWorkbook.Parse("results", "not-an-xlsx"u8.ToArray()));
|
||||
|
||||
Assert.Equal(400, error.StatusCode);
|
||||
Assert.Contains("系统下载的 .xlsx 模板", error.Message, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user