跳转到主内容

SeatFlow.Infrastructure 层的数据提供者、导出器、仓库、布局构建器和文件迁移系统

Infrastructure 层详解

SeatFlow.Infrastructure 是架构中负责数据访问和外部 I/O 的基础设施层。它实现了 Core 层定义的数据提供者接口,提供多种数据格式支持、导出能力和文件版本迁移。

数据提供者

学生数据可以从多种文件格式加载,统一的接口 IStudentProvider 定义在 SeatFlow.Core 中。

提供者层次

IStudentProvider (Core 接口)
├── CsvStudentProvider        ← CSV 文件读取
├── XlsxStudentProvider       ← XLSX 文件读取 (EPPlus 8)
├── JsonStudentProvider       ← JSON 文件读取
├── CompositeStudentProvider  ← 组合模式:按优先级链式尝试各提供者
└── InMemoryStudentProvider   ← 内存数据(用于引导系统注入)

CompositeStudentProvider

CompositeStudentProvider 是默认注册的主 IStudentProvider。它内部维护一个提供者列表,按注册顺序依次尝试——每个提供者检查文件扩展名是否匹配,匹配则尝试解析。这使得系统可以自动识别任意受支持的格式。

模糊字段匹配(FuzzyColumnMatcher)

FuzzyColumnMatcher (SeatFlow.Infrastructure.Providers) 是一个内部静态类,为 CSV/XLSX 导入提供智能字段识别能力。它不依赖固定模板格式,而是通过扫描单元格内容自动推测数据结构。

工作流程

  1. 扫描二维单元格网格,使用 StudentDataMapping.ResolveProperty() 匹配已知字段名
  2. 按命中位置推断布局方向(列式 Columnar / 行式 RowBased)
  3. 标准模板快速路径:字段全在第 0 行且无重复 → 直接走原有解析逻辑
  4. 非标准布局:定位数据起始位置 → 按列/行读取,含双列聚合、合并格扩展、2-连续空终止

支持的场景

  • 字段不在第 1 行(任意行位置均可)
  • 行式布局(字段在首列,数据向右延伸)
  • 双列/多列名单(同一字段名出现多次 → 自动聚合)
  • 合并单元格(XLSX)→ 字段值扩展到整个合并范围
  • 数据间隔的空行 → 连续 ≥2 个空单元格视为当前字段数据结束
  • 不对称双列名单(如 |姓名|性别|姓名|需要前排|)→ 按字段列的空间位置自动分组

双列名单空间分组算法

以重复次数最多的字段列作为锚点划定组边界。例如 |姓名|性别|姓名|需要前排|

锚点: Name 位于列 [0, 2]
组边界: Group 0 = [0, 2) 即列 0-1,  Group 1 = [2, ∞) 即列 2+
分配: Gender(列1) ∈ [0,2) → Group 0; NeedsFrontRow(列3) ∈ [2,∞) → Group 1
结果: G0=(Name[0],Gender[1])  G1=(Name[2],NeedsFrontRow[3])

同样算法适用于行式布局——以重复行号作为锚点。FindGroupIndex(pos, anchors) 方法为列/行通用。

学生数据集导入流程

用户选择文件 → IFileService.OpenFileAsync()
  → ApplicationFacade.ImportStudentsAsync(filePath)
    → CompositeStudentProvider 尝试解析
    → CsvStudentProvider / XlsxStudentProvider:
       → 构建二维网格 → FuzzyColumnMatcher.TryParse()
       → 标准模板 → 快速路径;非标准 → 模糊解析
    → JsonStudentDatasetRepository 创建数据集
    → 数据集记录在 {AppData}/Rosters/{id}.roster.json

数据写入器

支持将学生数据写回文件:

写入器 格式 用途
JsonStudentWriter JSON 数据集保存
CsvStudentWriter CSV 导出模板
XlsxStudentWriter XLSX 导出模板(EPPlus 8)

导出器

排座结果可导出为多种格式,统一实现 ISeatingPlanExporter 接口:

导出器 格式 技术
ExcelSeatingExporter XLSX EPPlus 8
CsvSeatingExporter CSV 标准 .NET I/O
PdfSeatingExporter PDF QuestPDF
ImageSeatingExporter 图片 Avalonia 渲染

布局构建器

将布局定义转换为座位实体集合:

构建器 布局类型 说明
GridLayoutBuilder Grid 网格布局(行列),按行主序创建座位
PolarLayoutBuilder Polar 极坐标布局(环形、扇形)
FreeformLayoutBuilder Freeform 自由点布局

Grid 座位排序

GridLayoutBuilder.BuildGrid行主序创建座位(外层循环:行,内层循环:列)。这确保 RandomFillStrategy 逐行(从左到右,从上到下)填充座位。对于不规则的 ColumnRowCounts 网格,maxRows = ColumnRowCounts.Max(),每列检查 r <= rowsForCol

仓库

仓库列表

仓库 存储位置 用途
JsonVenueRepository {AppData}/Venues/*.venue.json 会场定义 CRUD
JsonAppSettingsRepository {AppData}/AppSettings.json 应用设置读写
StrategyConfigFileRepository {AppData}/StrategyConfig/ 策略配置持久化
SeatingSnapshotRepository {AppData}/Assignments/{venueId}/{date}/ 排座快照管理
JsonStudentDatasetRepository {AppData}/Rosters/*.roster.json 学生数据集管理

持久化文件结构和版本

文件类型 版本 位置 包装类
Venue 1.1 {data}/Venues/*.venue.json VenueFile
Roster 1.1 {data}/Rosters/*.roster.json RosterFile
Snapshot 1.0 {data}/Assignments/{venueId}/{date}/*.json SeatingSnapshot
VenueInfo 1.0 {data}/Assignments/{venueId}/_venue.json VenueSnapshotInfo
AppSettings 1.0 {data}/AppSettings.json AppSettings
StrategyConfig 1.0 {data}/StrategyConfig/{strategyId}.config.json StrategyConfig
StrategyDatasetConfig 1.0 {data}/StrategyConfig/{strategyId}/*.config.json StrategyDatasetConfig
Seatsets 1.0 {data}/Seatsets/*.seatsets SeatsetsFile

内容哈希

VenueFile.ContentHashRosterFile.ContentHash 使用 SHA256 哈希,在保存时计算:

  • 序列化(跳过 ContentHash 字段)
  • 计算哈希 → 设置 ContentHash
  • 重新序列化(包含 ContentHash

学生数据集哈希排除 importedAt/originalFileName(不稳定的时间戳)。

文件迁移系统

所有持久化 JSON 文件在加载时通过自动迁移确保向前兼容。

迁移管道

加载文件 → 作为 JsonNode 读取
  → 提取 version 字段
  → FileMigrationService.Migrate(fileType, node, fileVersion, targetVersion)
  → 反序列化为目标类型

迁移是仅向前的——不支持版本回滚。

添加新迁移

  1. Migration/Migrators/{FileType}Migrators.cs 中添加嵌套类:
public static class VenueMigrators
{
    public sealed class Step_1_0_to_1_1 : IFileMigrator
    {
        public string FileType => "venue";
        public string FromVersion => "1.0";
        public string ToVersion => "1.1";
        public JsonNode Migrate(JsonNode root)
        {
            // 迁移逻辑:操作 JsonNode 后返回
            return root;
        }
    }
}
  1. ServiceCollectionExtensions.cs 中注册:services.AddSingleton<IFileMigrator, VenueMigrators.Step_1_0_to_1_1>()
  2. file_versions.json 中提升对应版本号
  3. Core/Models/ 对应模型类中更新默认 Version 属性
  4. 添加测试

现有迁移

  • VenueMigrators.Step_1_0_to_1_1 — 将 Grid 布局座位从列主序重排为行主序(先按 Row 排序,再按 Column 排序)

版本管理文件

SeatFlow.Infrastructure/Migration/file_versions.json 是嵌入资源,编译到程序集中。FileVersionInfo.GetCurrentVersion(fileType) 在运行时读取最新版本。scripts/version.py 脚本可统一管理各文件版本。

SeatSets 服务

SeatSets 是 SeatFlow 的数据包格式,用于打包和传输完整排座场景:

  • 打包:将会场、数据集和策略配置打包为 .seatsets 文件
  • 解包:从 .seatsets 文件还原场景
  • 校验:验证数据包的完整性和格式正确性
  • 自动导入:扫描指定目录自动导入数据包

序列化

JSON 字段约定

  • 使用 JsonNamingPolicy.CamelCase — JSON 中所有字段为小写起始
  • ClassroomLayoutDefinition.LayoutType 序列化为 同时 包含数字(layoutType: 0=Grid, 1=Polar, 2=Freeform)和字符串(layoutTypeString: "Grid"/"Polar"/"Freeform")
  • 迁移器应优先读取 layoutTypeString 以保持清晰

Seat 多态序列化

SeatJsonConverter 实现座位对象的多态序列化/反序列化:

{
  "Type": "Grid",       // 鉴别器(大写 T,字符串)
  "id": "S001",
  "row": 1,
  "column": 1,
  "type": 0,            // SeatType 枚举值(小写,数字)
  "isAvailable": true,
  "isFixed": false,
  "occupantId": null
}
  • Type(大写 T)是字符串鉴别器:"Grid" / "Polar" / "Freeform"
  • type(小写)是 SeatType 枚举的 JSON 整数值
  • 反序列化时根据 Type 字段创建对应的派生类实例

快照会场布局嵌入

快照在创建时将完整的 ClassroomLayoutDefinition 序列化到 Metadata["venueLayout"]。预览时优先读取嵌入布局;旧快照回退到加载会场文件。这使得快照自包含——编辑或删除会场文件不会破坏现有快照预览。