跳转到主内容

SeatFlow.Application 层的外观模式、策略管道、命令模式、存储抽象和 DI 注册

Application 层详解

SeatFlow.Application 是 SeatFlow 的应用编排层,作为 UI 层和领域层之间的桥梁。它负责编排数据加载、策略执行、存储访问和命令历史。所有业务逻辑的协调入口均在这一层。

IApplicationFacade (外观接口)

IApplicationFacade 是 UI 层的单一入口点(外观模式),封装 40+ 方法。接口定义在 SeatFlow.Application/Interfaces/IApplicationFacade.cs,实现为 Services/ApplicationFacade.cs。代表性方法:

public interface IApplicationFacade
{
    // 数据管理
    Task<List<Student>> LoadStudentsAsync(string source, CancellationToken ct = default);
    Task<(int Rows, int Cols)> GetDataSourceDimensionsAsync(string source, CancellationToken ct = default);
    Task<IReadOnlyList<StudentDatasetInfo>> ListStudentDatasetsAsync(CancellationToken ct = default);

    // 排座执行
    Task<SeatingWorkspace> GenerateSeatingAsync(SeatingRequest request,
        IProgress<SeatingProgress>? progress = null, CancellationToken ct = default);
    Task<SeatingWorkspace> CreateEmptyWorkspaceAsync(string layoutId, string datasetId,
        CancellationToken ct = default);

    // 导出(文件路径 / 字节流,后者供浏览器下载)
    Task ExportSeatingPlanAsync(SeatingWorkspace workspace, ClassroomLayoutDefinition? layout,
        string path, ExportOptions options, CancellationToken ct = default);
    Task<byte[]> ExportSeatingPlanBytesAsync(SeatingWorkspace workspace,
        ClassroomLayoutDefinition? layout, ExportOptions options, CancellationToken ct = default);

    // 快照
    Task<IReadOnlyList<SeatingSnapshot>> GetSnapshotsAsync(string venueId, CancellationToken ct = default);
    Task<SeatingSnapshot?> CreateSnapshotAsync(string description, CancellationToken ct = default);

    // 命令历史(撤销/重做)
    Task<bool> ExecuteCommandAsync(IUndoableCommand command, CancellationToken ct = default,
        bool recordInHistory = true);
    Task<bool> UndoAsync(CancellationToken ct = default);
    Task<bool> RedoAsync(CancellationToken ct = default);

    // 策略配置
    Task<List<StrategyDisplayInfo>> GetStrategiesAsync(CancellationToken ct = default);
    Task<List<StrategyDatasetConfig>> LoadStrategyDatasetConfigsAsync(string strategyId,
        CancellationToken ct = default);

    // 数据包(SeatSets)
    Task<SeatSetsImportResult> ImportSeatSetsAsync(string filePath,
        SeatSetsExportSelection selection, IProgress<double>? progress = null, CancellationToken ct = default);

    // ... 更多方法
}

ViewModel 通过构造函数注入 IApplicationFacade,不直接依赖任何领域或基础设施类型。

StrategyExecutionPipeline (策略管道)

策略管道采用 Fill-in-Order 模型执行独立策略:

public class StrategyExecutionPipeline
{
    public StrategyExecutionPipeline(
        IEnumerable<ISeatingStrategy> strategies,
        ILogger<StrategyExecutionPipeline>? logger = null,
        bool throwOnFailure = false);

    public async Task<SeatingPlan> ExecuteAsync(
        SeatingWorkspace workspace,
        IProgress<SeatingProgress>? progress = null,
        CancellationToken cancellationToken = default)
    {
        // 内部按 Priority 降序执行每个已启用的独立策略
        // 失败时 workspace.LogError(...)(throwOnFailure=true 时抛异常)
        // 最后 return workspace.BuildSeatingPlan();
    }
}

执行流程(ApplicationFacade 在构建策略列表时已排除 visible=false 的策略,管道中仅处理已纳入的策略):

  1. 按 Priority 降序排列 IsEnabled 为 true 的独立策略
  2. 依次执行每个独立策略,所有策略操作同一个 SeatingWorkspace
  3. 依赖策略不在外部管道中执行——它们在 RandomFillStrategy 的分配循环中按上下文内部优先级评估

RandomFill 上下文循环

RandomFillStrategy 作为依赖策略的宿主,其分配循环:

while 还有未分配学生 AND 空座位:
    随机选 (student, seat)
    rerollCount = 0
    loop:
        依次调用依赖策略 EvaluateAsync (按内部 Priority 降序)
        if Reject → rerollCount++
            if >= maxRerolls 兜底强制分配
            else 换随机座位重试
        if Handled → 依赖策略已自行分配,跳过 TryAssignSeat
        if 全部 Approve → TryAssignSeat, 刷新列表

约束学生(DeskMate 分组)优先分配以减少重掷次数。逐出操作尊重先前策略(FixedSeat、FrontRowRotation)的分配结果。

命令模式

IUndoableCommand 和 CommandHistory 提供快照式撤销/重做:

public interface IUndoableCommand
{
    string Id { get; }
    Task<bool> ExecuteAsync(SeatingWorkspace workspace, CancellationToken ct = default);
    Task<bool> UndoAsync(SeatingWorkspace workspace, CancellationToken ct = default);
}

CommandHistory(Services/CommandHistory.cs)管理操作栈,支持撤销和重做导航。采用快照式实现——每个命令保存操作前后的状态快照,而非增量操作记录。ApplicationFacade.ExecuteCommandAsync 的 recordInHistory 参数供 ViewModel 在使用快照历史时传 false,避免重复累积。

存储抽象与平台差异

Application 层通过 Core 层的 ILocalDataStore 抽象访问本地数据,不直接依赖文件系统。各启动壳在 DI 中注册平台实现:

平台 实现 存储介质
桌面(SeatFlow.Desktop) FileSystemDataStore 应用数据目录文件系统
浏览器(SeatFlow.Browser) IndexedDbDataStore 浏览器 IndexedDB(库 seatflow / 存储 files)

统一通过该接口读写后,仓库、文件迁移、SeatSets 打包/解包等上层服务无需感知平台差异。平台专属能力由各壳实现并通过接口暴露:

  • 数据迁移:桌面端支持 .seatsets 文件关联与首次启动自动导入;浏览器端以打包上传/下载等价实现
  • 文件导入导出:XLSX/CSV/JSON 在两端均可用;PDF 与图片导出仅桌面可用
  • 数据目录设置:仅桌面端提供;浏览器数据保存在 IndexedDB 中

DI 注册

Application 层注册

SeatFlow.Application.Services.ServiceCollectionExtensions.AddSeatFlowApplication(...) 注册所有 Application、Core 和 Infrastructure 层的服务,有两个重载:

  • AddSeatFlowApplication(string snapshotBasePath) — 桌面路径:解析数据目录并配置 Serilog 文件日志
  • AddSeatFlowApplication(ILocalDataStore store) — WASM 路径:日志走默认 provider,不写文件

注册的服务包括:

  • 策略(内置 4 条独立策略 + 3 条依赖策略)
  • 导出器(浏览器目标不注册 PDF/图片导出器)
  • 数据提供者(CSV/XLSX/JSON + CompositeStudentProvider)
  • 仓库(会场/数据集/设置/策略配置/快照)
  • 文件迁移服务与迁移器
  • 策略执行管道
  • 外观

启动壳注册

各启动壳在共享注册之外补充平台服务:

壳 补充注册
SeatFlow.Desktop FileSystemDataStore、桌面文件/对话框服务、WatchdogService
SeatFlow.Browser IndexedDbDataStore、WebDialogService、WebFileService、BrowserConsoleLoggerProvider

共享层 UI 服务与 ViewModel 同样在启动壳中注册(两端 Program.cs 保持一致的注册顺序):

// 导航与全局行为
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<KeyboardShortcutHandler>();
services.AddSingleton<FileDropHandler>();

// 横切服务(M0/M3)
services.AddSingleton<IDialogGate, DialogGate>();
services.AddTransient<IBusyScope, BusyScope>();
services.AddSingleton<IShellLayoutService, ShellLayoutService>();
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<IDialogService, DialogService>();

// 外壳与 7 个页面 ViewModel(默认入口 = SeatingArrangement)
services.AddSingleton<MainShellViewModel>();
services.AddSingleton<SeatingArrangementViewModel>();
services.AddSingleton<MemberManagementViewModel>();
services.AddSingleton<VenueConfigurationViewModel>();
services.AddSingleton<StrategyConfigurationViewModel>();
services.AddSingleton<SnapshotHistoryViewModel>();
services.AddSingleton<SettingsViewModel>();
services.AddSingleton<AboutViewModel>();

// 引导演示数据注入契约(5 个 IGuideSeedTarget)
services.AddGuideSeedTargets();

构造注入优先:2.1.0 起所有 ViewModel 的对话框与日志均由构造函数注入,ViewModelBase.Dialog 静态状态已移除;无法获得真实 IDialogService 的场景(单测/设计时)传 NullDialogService.Instance。

日志

  • 使用 Serilog 4 + Microsoft.Extensions.Logging.ILogger<T>
  • 日志输出到文件(Serilog.Sinks.File)
  • Application 服务通过构造函数注入 ILogger<T>
  • 可选日志参数回退到 NullLogger<T>.Instance

浏览器端无法写入本地文件,由 BrowserConsoleLoggerProvider 将日志转发到浏览器 DevTools Console。

public class StrategyExecutionPipeline
{
    private readonly ILogger<StrategyExecutionPipeline> _logger;

    public StrategyExecutionPipeline(
        IEnumerable<ISeatingStrategy> strategies,
        ILogger<StrategyExecutionPipeline>? logger = null,
        bool throwOnFailure = false)
    {
        _strategies = strategies;
        _logger = logger ?? NullLogger<StrategyExecutionPipeline>.Instance;
    }
}