支持五级混编、稳定种子、容量预检、固定考点、逐科考场/座位及四种准考证号规则。

正式编排会事务性替换整场计划、准考证、座位和审计记录。
支持三级管理员范围读取、批量 HTML 准考证、准考证信息 Excel,以及校级考点桌贴、门贴和签名单。
核心实现见 [AdminArrangementPlanner.cs (line 7)](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Administration/AdminArrangementPlanner.cs:7)、[AdminArrangementService.cs (line 11)](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Administration/AdminArrangementService.cs:11) 和 [AdminArrangementExporter.cs (line 5)](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Infrastructure/Administration/AdminArrangementExporter.cs:5)。
新增开关 ADMIN_NATIVE_ARRANGEMENTS_ENABLED=true,说明已更新到 [MIGRATION.md (line 57)](C:/Users/BI/Documents/EIS-dotnet/MIGRATION.md:57)。
Excel 由 .NET 直接生成,使用 ClosedXML 0.105.0;该库支持服务端生成 Excel 2007+ 工作簿。ClosedXML NuGet 页面
This commit is contained in:
2026-07-23 11:25:09 +08:00 Unverified
parent 25501651e1
commit 8f83adbc1e
18 changed files with 2138 additions and 11 deletions
@@ -0,0 +1,244 @@
using ClosedXML.Excel;
namespace Eis.Infrastructure.Administration;
internal static class AdminArrangementExporter
{
private static readonly string[] InformationHeaders =
[
"考生学校", "班级", "报名号", "姓名", "证件号码", "考试代码", "考试名称", "准考证号",
"考点代码", "考点名称", "考点详细地址", "科目", "日期", "时间", "考试考场序号",
"考场通用名称", "物理场地代码", "楼栋", "楼层", "座位号"
];
public static byte[] AdmitCardInformation(
IReadOnlyList<ArrangementExportRow> rows,
string subtitle)
{
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("准考证信息");
ConfigureTable(
sheet,
$"准考证信息台账|{subtitle}",
InformationHeaders,
rows.Select(InformationValues).ToArray());
return Save(workbook);
}
public static byte[] CenterMaterials(
IReadOnlyList<ArrangementExportRow> rows,
string subtitle)
{
using var workbook = new XLWorkbook();
var sorted = rows
.OrderBy(item => item.CenterCode, StringComparer.Ordinal)
.ThenBy(item => item.SubjectDate, StringComparer.Ordinal)
.ThenBy(item => item.SubjectTime, StringComparer.Ordinal)
.ThenBy(item => item.ExamRoomCode, StringComparer.Ordinal)
.ThenBy(item => item.Seat, StringComparer.Ordinal)
.ToArray();
AddDeskStickers(workbook.Worksheets.Add("桌贴"), sorted);
var doors = sorted
.GroupBy(
item => new
{
item.ExamCode,
item.SubjectName,
item.CenterCode,
item.ExamRoomCode,
item.RoomCode
})
.Select(group => (Row: group.First(), Count: group.Count()))
.ToArray();
ConfigureTable(
workbook.Worksheets.Add("门贴"),
$"考场门贴|{subtitle}",
[
"考试", "科目", "日期", "时间", "考点", "考试考场序号",
"考场通用名称", "场地代码", "楼栋", "楼层", "人数"
],
doors.Select(item => new object[]
{
item.Row.ExamName,
item.Row.SubjectName,
item.Row.SubjectDate,
item.Row.SubjectTime,
item.Row.CenterName,
item.Row.ExamRoomCode,
item.Row.RoomName,
item.Row.RoomCode,
item.Row.Building,
item.Row.Floor,
item.Count
}).ToArray());
ConfigureTable(
workbook.Worksheets.Add("考场签名单"),
$"考场考生签名单|{subtitle}",
[
"考试", "科目", "日期", "时间", "考试考场序号", "考场通用名称",
"楼栋", "楼层", "座位号", "准考证号", "姓名", "考生学校", "班级", "考生签名"
],
sorted.Select(item => new object[]
{
item.ExamName,
item.SubjectName,
item.SubjectDate,
item.SubjectTime,
item.ExamRoomCode,
item.RoomName,
item.Building,
item.Floor,
item.Seat,
item.CardNumber,
item.CandidateName,
item.SchoolName,
item.ClassName,
""
}).ToArray());
return Save(workbook);
}
private static void AddDeskStickers(
IXLWorksheet sheet,
IReadOnlyList<ArrangementExportRow> rows)
{
sheet.ShowGridLines = false;
for (var column = 1; column <= 7; column++)
sheet.Column(column).Width = column == 4 ? 3 : 14.5;
for (var index = 0; index < rows.Count; index++)
{
var item = rows[index];
var slot = index % 2;
var group = index / 2;
var startRow = group * 9 + 1;
var startColumn = slot == 0 ? 1 : 5;
var values = new[]
{
item.ExamName,
$"{item.SubjectName}{item.SubjectDate} {item.SubjectTime}",
"考试考场序号",
item.ExamRoomCode,
$"座位号 {item.Seat}",
$"{item.RoomName}(场地 {item.RoomCode})|{item.Building} {item.Floor}",
$"{item.CandidateName}{item.SchoolName} {item.ClassName}",
$"{item.CenterName}|准考证号 {item.CardNumber}"
};
for (var offset = 0; offset < values.Length; offset++)
{
var range = sheet.Range(
startRow + offset,
startColumn,
startRow + offset,
startColumn + 2);
range.Merge();
range.Value = values[offset];
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
range.Style.Alignment.WrapText = true;
range.Style.Border.OutsideBorder = XLBorderStyleValues.Medium;
range.Style.Border.InsideBorder = XLBorderStyleValues.Hair;
sheet.Row(startRow + offset).Height = 28;
}
var title = sheet.Range(startRow, startColumn, startRow, startColumn + 2);
title.Style.Fill.BackgroundColor = XLColor.FromHtml("#173F60");
title.Style.Font.FontColor = XLColor.White;
title.Style.Font.Bold = true;
var room = sheet.Range(startRow + 3, startColumn, startRow + 3, startColumn + 2);
room.Style.Font.FontSize = 24;
room.Style.Font.Bold = true;
var seat = sheet.Range(startRow + 4, startColumn, startRow + 4, startColumn + 2);
seat.Style.Fill.BackgroundColor = XLColor.FromHtml("#FFF1C7");
seat.Style.Font.FontColor = XLColor.FromHtml("#8A4B08");
seat.Style.Font.FontSize = 18;
seat.Style.Font.Bold = true;
}
sheet.PageSetup.PageOrientation = XLPageOrientation.Portrait;
sheet.PageSetup.FitToPages(1, 0);
sheet.PageSetup.Margins.SetLeft(0.25);
sheet.PageSetup.Margins.SetRight(0.25);
sheet.PageSetup.Margins.SetTop(0.25);
sheet.PageSetup.Margins.SetBottom(0.25);
}
private static void ConfigureTable(
IXLWorksheet sheet,
string title,
IReadOnlyList<string> headers,
IReadOnlyList<object[]> rows)
{
sheet.ShowGridLines = false;
var lastColumn = headers.Count;
var titleRange = sheet.Range(1, 1, 1, lastColumn);
titleRange.Merge();
titleRange.Value = title;
titleRange.Style.Fill.BackgroundColor = XLColor.FromHtml("#173F60");
titleRange.Style.Font.FontColor = XLColor.White;
titleRange.Style.Font.FontSize = 16;
titleRange.Style.Font.Bold = true;
titleRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
sheet.Row(1).Height = 28;
for (var column = 0; column < headers.Count; column++)
{
var cell = sheet.Cell(2, column + 1);
cell.Value = headers[column];
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#2C7080");
cell.Style.Font.FontColor = XLColor.White;
cell.Style.Font.Bold = true;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
cell.Style.Alignment.WrapText = true;
}
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
{
for (var column = 0; column < headers.Count; column++)
{
var value = column < rows[rowIndex].Length ? rows[rowIndex][column] : "";
var cell = sheet.Cell(rowIndex + 3, column + 1);
if (value is int integer) cell.Value = integer;
else if (value is double number) cell.Value = number;
else cell.Value = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? "";
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
cell.Style.Alignment.WrapText = true;
cell.Style.Border.BottomBorder = XLBorderStyleValues.Hair;
cell.Style.Border.BottomBorderColor = XLColor.FromHtml("#D8E2E7");
}
}
sheet.SheetView.FreezeRows(2);
if (rows.Count > 0) sheet.Range(2, 1, rows.Count + 2, lastColumn).SetAutoFilter();
sheet.Columns().AdjustToContents(8, 40);
sheet.PageSetup.PageOrientation = XLPageOrientation.Landscape;
sheet.PageSetup.FitToPages(1, 0);
sheet.PageSetup.SetRowsToRepeatAtTop(1, 2);
}
private static object[] InformationValues(ArrangementExportRow item) =>
[
item.SchoolName,
item.ClassName,
item.CandidateNumber,
item.CandidateName,
item.IdNumber,
item.ExamCode,
item.ExamName,
item.CardNumber,
item.CenterCode,
item.CenterName,
item.CenterAddress,
item.SubjectName,
item.SubjectDate,
item.SubjectTime,
item.ExamRoomCode,
item.RoomName,
item.RoomCode,
item.Building,
item.Floor,
item.Seat
];
private static byte[] Save(XLWorkbook workbook)
{
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
}