跳转到主内容

SeatFlow 策略管道的 Fill-in-Order 模型、独立策略与依赖策略的执行机制、三态评估模型和声明式配置系统

策略管道深度解析

概述

SeatFlow 的策略管道采用 Fill-in-Order(按序填充)模型。所有独立策略按优先级(Priority)降序执行:数值越大越先执行,先执行的策略从空座中优先挑选,后执行的在剩余空座中择优。不存在覆盖语义——先占的座位不会被后续策略推翻。IsFixed 标志是唯一的座位保护机制。

依赖策略不在外部管道中执行,而是在 RandomFillStrategy 的分配循环内按内部优先级依次评估每个 (student, seat) 对,实现细粒度的分配约束。

核心架构

独立策略管道(按 Priority 降序执行):
  FixedSeatStrategy (100) → FrontRowRotation (50) → RandomFill (1) → Defrag (0)
                                          │
                    ┌─────────────────────┐
                    │  RandomFill 分配循环   │
                    │  内嵌依赖策略(按内部优先级)│
                    │  DeskMate (50)        │
                    │  GenderRestricted (45) │
                    │  NoRepeatDeskMate (40) │
                    └─────────────────────┘

接口体系

接口 适用策略 说明
ISeatingStrategy 所有独立策略 提供 ExecuteAsync(workspace, ct),在外部管道中按 Priority 排序执行
IDependentSeatingStrategy 所有依赖策略 提供 EvaluateAsync(workspace, student, seat, context, ct),在 RandomFill 分配循环中调用
IRandomFillContext RandomFill 内部 提供 RerollCount, MaxRerolls, LogWarning/LogError
IFixedSeatCapability 需标记固定座位的策略 提供 TryMarkFixed(seatId, studentId, ...)

完整执行顺序

顺序 策略 Priority 类型 职责
第1 FixedSeatStrategy 100 独立 锁定固定座位(IsFixed=true),后续所有 GetEmptySeats 均排除
第2 FrontRowRotationStrategy 50 独立 从剩余非固定空座位中填充前排座位
DeskMateStrategy 50(上下文) 依赖 在 RandomFill 内检查每个 (student, seat) 对上的同桌分组
GenderRestrictedSeatStrategy 45(上下文) 依赖 在 RandomFill 内检查目标座位是否有性别限制
NoRepeatDeskMateStrategy 40(上下文) 依赖 在 RandomFill 内检查相邻座位是否包含历史同桌
第3 RandomFillStrategy 1 独立 + 宿主 填充剩余座位,托管依赖策略的分配循环
第4 DefragStrategy 0 独立 将后排无约束学生前移填空隙(默认禁用)

独立策略 vs 依赖策略

独立策略(Independent)

实现 ISeatingStrategy 接口,manifest 中 isIndependent: true(默认值)或省略。由 StrategyExecutionPipeline 在外部管道中按 Priority 降序执行。每个独立策略操作整个 SeatingWorkspace,通过 GetEmptySeats() 获取空座列表后自主分配。

示例FixedSeatStrategyFrontRowRotationStrategyDefragStrategy

依赖策略(Dependent)

实现 IDependentSeatingStrategy 接口,manifest 中 isIndependent: false。不在外部管道中执行,而是在 RandomFillStrategy 的分配循环中作为上下文策略运行。对 RandomFill 提议的每个 (student, seat) 对进行评估,返回三种结果之一。

示例DeskMateStrategyGenderRestrictedSeatStrategyNoRepeatDeskMateStrategy

Manifest 配置

{
  "id": "DeskMate",
  "isIndependent": false,
  "visible": true,
  "capabilities": [],
  "parameters": [],
  "codeBlocks": [
    {
      "dataType": "Both",
      "displayMode": "ValuePair",
      "showSeatPosition": false,
      "preventDuplicateInRow": true
    }
  ]
}

依赖策略三态模型

每个依赖策略的 EvaluateAsync 方法返回 DependentEvaluationResult,包含三种结果:

结果 含义 行为
Approve() 该座位对学生可接受 继续执行下一个依赖策略或进行实际分配
Reject(reason) 该座位对学生不可接受 递增重试计数,若未超限则换座重试;超限后强制分配并记录警告
Handled(message) 已自行完成分配(含连带修改) 跳过 TryAssignSeat,但后续依赖策略仍继续执行以进行检查/警告

RandomFill 分配循环算法

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

约束学生优先分配

受约束学生(DeskMate 组成员)在 RandomFill 循环中优先处理,以减少重试次数。具体的约束条件在运行时由各依赖策略确定。

重试耗尽兜底

rerollCount >= maxRerolls 时,RandomFill 强制将当前学生分配到当前座位,无论依赖策略是否同意。此时记录 LogWarning,警告信息通过策略 manifest 的 messages 字典使用 i18n 格式输出。

IsFixed 座位保护机制

FixedSeatStrategy 最先执行,标记固定座位为 IsFixed=true

// FixedSeatStrategy 内部
seat.IsFixed = true;
workspace.TryAssignSeat(seat.Id, studentId);

IsFixed 的保护作用:

  • GetEmptySeats() 自动排除 IsFixed 的座位
  • DefragStrategy 的座位扫描跳过 IsFixed 座位
  • 依赖策略的 EvaluateAsync 中,IsFixed 座位不可见

能力声明系统

策略必须在 manifest 的 capabilities 数组中声明所需能力,才能调用对应的能力接口方法。未声明的调用将被拒绝并记录警告。

{
  "capabilities": ["MarkFixedSeat"]
}
// 实现 IFixedSeatCapability 的示例
if (workspace is IFixedSeatCapability fixedCap)
{
    bool success = fixedCap.TryMarkFixed(seatId, studentId, strategyId, displayName, out var error);
}

能力常量集中在 SeatFlow.Core/Strategies/Capability.cs

常量 接口 用途
MarkFixedSeat IFixedSeatCapability 标记座位为固定座位

新增能力时,在 Capability.cs 中添加 const + 接口定义,在 SeatingWorkspace 中实现,在 IPluginWorkspace 中暴露。

策略消息系统

策略在执行期间可通过 workspace 记录警告或错误,消息模板在 manifest 的 messages 字段中声明:

// 记录警告
workspace.LogWarning(strategyId, displayName, "DeskMate_Split", groupName, memberName);

// 记录错误
workspace.LogError(strategyId, displayName, "DeskMate_NoSeats", groupName);

Manifest 消息模板

{
  "messages": {
    "DeskMate_Split": {
      "zh-CN": "同桌组({0})中的 {1} 已被前排策略分配,该组已拆散",
      "en-US": "Desk-mate group ({0}) member(s) {1} already assigned to front row, group split"
    },
    "Defrag_EffectivenessNote": {
      "zh-CN": "Defrag 策略通过后移学生可能推翻手动调整或 FrontRowRotation 的结果",
      "en-US": "Defrag strategy may override manual adjustments or FrontRowRotation results"
    }
  }
}

消息收集在 SeatingWorkspace.Messages 中(含 StrategyIdStrategyDisplayNameMessageKeyArgs),管道执行后在 UI 侧栏展示。

UI 侧栏展示

![策略消息侧栏示意] 执行完成后,UI 侧栏按策略分组展示消息。每个消息项包含策略名称、消息内容和严重级别(警告/错误)。

声明式策略配置

所有策略特定配置由 manifest JSON 文件驱动,位于 SeatFlow.Core/Strategies/Manifests/*.json

顶层字段

字段 类型 默认值 说明
visible bool true 控制策略是否参与管道。false 时从 UI 和执行中排除
isIndependent bool true false 表示依赖策略,在 RandomFill 上下文中执行
manifestVersion string "1.0" 清单格式版本,运行时兼容性检查
capabilities string[] [] 策略能力声明
parameters array [] 策略级全局参数
codeBlocks array [] 按数据集/会场的配置块

Parameters 字段类型

fieldType 描述 配置方式
NumberInput 数字输入框 minValue/maxValue 边界
TextInput 文本输入框 自由文本
ToggleSwitch 开关 true/false
Dropdown 下拉选择 枚举值
{
  "parameters": [
    {
      "name": "HistoryWindowSize",
      "fieldType": "NumberInput",
      "label": { "zh-CN": "历史窗口大小", "en-US": "History Window Size" },
      "defaultValue": 10,
      "minValue": 1,
      "maxValue": 30
    }
  ]
}

CodeBlocks 字段

字段 类型 默认值 说明
dataType enum StudentVenueBoth
displayMode enum Table(表格)、ValuePair(值对行)
showSeatPosition bool true 显示/隐藏座位位置选择器
showStudentPicker bool? 覆盖 DataType 自动检测
showVenuePicker bool? 覆盖 DataType 自动检测
studentPickerCount int 1 学生选择器数量
seatsPerDeskFromVenue bool false 从会场 GridLayoutMetadata 动态读取
preventDuplicateInRow bool false 禁止同行内学生值重复
preventDuplicateAcrossRows bool false 禁止跨行学生值重复
loadTrigger enum Both Both=两个选择器有值精确匹配;Any=任一有值模糊匹配

依赖策略示例

DeskMate(同桌分组)

{
  "id": "DeskMate",
  "isIndependent": false,
  "visible": true,
  "codeBlocks": [
    {
      "dataType": "Both",
      "displayMode": "ValuePair",
      "showSeatPosition": false,
      "preventDuplicateInRow": true,
      "seatsPerDeskFromVenue": true
    }
  ]
}
  • 在 RandomFill 分配循环中执行
  • 当学生属于同桌组时,尝试将整组分配到同一桌的相邻座位
  • 逐出仅影响 RandomFill 自身分配的学生,不触及 FixedSeat/FrontRowRotation 的结果
  • 目标座位附近空座不足时,部分分配并记录警告

GenderRestrictedSeat(性别限制)

{
  "id": "GenderRestrictedSeat",
  "isIndependent": false,
  "visible": true,
  "codeBlocks": [
    {
      "dataType": "Venue",
      "displayMode": "ValuePair",
      "showSeatPosition": true,
      "showStudentPicker": false,
      "showGenderPicker": true,
      "preventDuplicateAcrossRows": true
    }
  ]
}
  • 性别不匹配时触发重定向优化:直接将学生放入匹配性别的受限空座(Handled,不消耗重试次数)
  • 无匹配座位时 Reject,重试耗尽后强制分配并警告

NoRepeatDeskMate(避免重复同桌)

{
  "id": "NoRepeatDeskMate",
  "isIndependent": false,
  "visible": true,
  "parameters": [
    {
      "name": "HistoryWindowSize",
      "fieldType": "NumberInput",
      "label": { "zh-CN": "历史窗口大小", "en-US": "History Window Size" },
      "defaultValue": 10,
      "minValue": 1,
      "maxValue": 30
    }
  ]
}
  • 无 codeBlocks,完全依赖 NoRepeatDeskMateHistoryLoader 从历史快照加载同桌对
  • 检测到重复时 Reject 触发重试,耗尽后强制分配

独立策略示例

FixedSeat(固定座位)

{
  "id": "FixedSeat",
  "visible": true,
  "codeBlocks": [
    {
      "dataType": "Both",
      "displayMode": "ValuePair",
      "preventDuplicateAcrossRows": true
    }
  ]
}
  • Priority 100,最先执行
  • 每行含学生选择器 + 座位选择器
  • 跨行学生选择器值互斥

FrontRowRotation(前排轮换)

  • 无 codeBlocks:NeedsFrontRow 是 Student 模型属性,从 CSV/XLSX 导入
  • 按成绩选择学生后,Fisher-Yates 洗牌随机分配到前排各列

RandomFill(随机填充)

  • 无 parameters、无 codeBlocks
  • 作为依赖策略的宿主,在分配循环中承载 DeskMate、GenderRestrictedSeat、NoRepeatDeskMate

Defrag(碎片整理)

{
  "id": "Defrag",
  "visible": true,
  "isEnabled": false
}
  • "扫地僧"角色:执行时将后排无约束学生前移填空隙
  • 跨列填充,跳过 FixedSeat 和 DeskMate 组学生
  • 默认禁用,启用时记录有效性警告(可能使先前策略结果部分失效)

优先级冲突检测

系统在管道执行前进行优先级冲突检测:

  • 内置策略的 Priority 在 manifest 中定义,冲突检测在策略初始化时执行
  • 插件策略的 Priority 在插件包的 plugins-manifest.json 中定义
  • 用户可通过 UI 调整插件策略的 Priority
  • 相同 Priority 的独立策略按注册顺序执行(不保证确定性)

冲突解决规则

场景 解决方式
独立策略间座位争用 Priority 数值决定(先到先得)
依赖策略间评估顺序 内部 Priority 降序(DeskMate 50 → GenderRestricted 45 → NoRepeatDeskMate 40)
Handled 后其他依赖策略 仍继续执行以进行检查和报告
重试耗尽 强制分配 + LogWarning

Config 加载行为

匹配过滤策略

持久化配置行加载时采用"已选定则匹配,未选定则跳过"策略:

// 等效逻辑
(SelectedDataset is null || match) && (SelectedVenue is null || match)

对于 dataType: "Both",仅选择数据集时立即加载配置(会场在选定前视为通配符)。选定会场后,过滤器用两个值重新运行,精确匹配。

学生选择器延迟加载

学生选择器的选择通过 _pendingSelections 机制延迟到学生列表加载完成后,避免过早 SelectById 调用导致选择丢失。

插件策略适配

PluginStrategyAdapter

插件策略通过 PluginStrategyAdapter 适配到 SeatFlow 的管道体系:

public class PluginStrategyAdapter(IPluginSeatingStrategy pluginStrategy) : ISeatingStrategy
{
    private readonly IPluginSeatingStrategy _pluginStrategy = pluginStrategy;

    public string Id => _pluginStrategy.Id;
    public string Name => _pluginStrategy.Name;
    public int Priority { get => _pluginStrategy.Priority; set => _pluginStrategy.Priority = value; }
    public bool IsEnabled { get => _pluginStrategy.IsEnabled; set => _pluginStrategy.IsEnabled = value; }

    public async Task<StrategyExecutionResult> ExecuteAsync(SeatingWorkspace workspace, CancellationToken ct)
    {
        var pluginWorkspace = new PluginWorkspaceAdapter(workspace);
        var result = await _pluginStrategy.ExecuteAsync(pluginWorkspace, ct);
        return new StrategyExecutionResult
        {
            Success = result.Success,
            Message = result.Message
        };
    }

    public ValidationResult ValidateConfiguration()
    {
        return new ValidationResult { IsValid = true };
    }
}
  • 插件策略的 manifest 在插件包的 plugins-manifest.json(包级)和各策略子目录的 manifest.json(策略级)中声明
  • 插件通过 IPluginWorkspace 访问 workspace,使用 TryMarkFixed() 保护座位
  • 插件策略的 Priority 可由用户在 UI 中调整

插件依赖策略(未来方向)

当前 IPluginSeatingStrategy 仅支持独立策略接口。IDependentSeatingStrategy 的插件支持计划在后续版本扩展。

各策略数据依赖类型

策略 座位位置 学生 ID 会场 ID 数据集 ID 历史快照
FixedSeat Row+Col
DeskMate
GenderRestrictedSeat Row+Col
FrontRowRotation 是(快照)
NoRepeatDeskMate 是(快照)
Defrag
RandomFill

详见 策略数据持久化与容错分析

策略实现框架

实现一个独立策略

public class MyCustomStrategy : ISeatingStrategy
{
    public string Id => "MyCustom";
    public string DisplayName => "My Custom Strategy";
    public int Priority => 50;

    public async Task ExecuteAsync(SeatingWorkspace workspace, CancellationToken ct)
    {
        var emptySeats = workspace.GetEmptySeats().ToList();
        // 分配逻辑...
        foreach (var seat in emptySeats)
        {
            if (workspace.TryAssignSeat(seat.Id, studentId))
            {
                workspace.LogWarning(Id, DisplayName, "My_Assigned", studentId, seat.Id);
            }
        }
    }
}

实现一个依赖策略

public class MyDependentStrategy : IDependentSeatingStrategy
{
    public string Id => "MyDependent";
    public string DisplayName => "My Dependent Strategy";
    public int Priority => 50;

    public async Task<DependentEvaluationResult> EvaluateAsync(
        SeatingWorkspace workspace,
        Student student,
        Seat targetSeat,
        IRandomFillContext context,
        CancellationToken ct)
    {
        if (/* 条件满足 */)
            return DependentEvaluationResult.Approve();

        if (/* 可自行分配 */)
        {
            // 执行连带分配
            return DependentEvaluationResult.Handled("已自行分配至邻近座位");
        }

        // 拒绝,请求重试
        if (context.RerollCount < context.MaxRerolls)
            return DependentEvaluationResult.Reject("座位不满足要求");

        // 重试耗尽,强制批准
        context.LogWarning(Id, DisplayName, "My_ForceApprove", student.Name);
        return DependentEvaluationResult.Approve();
    }
}

DI 注册

// 独立策略
services.AddSingleton<ISeatingStrategy, MyCustomStrategy>();

// 依赖策略
services.AddSingleton<IDependentSeatingStrategy, MyDependentStrategy>();

数据持久化架构

策略数据分为三层存储:

Type A: StrategyConfig(策略全局配置)
  路径: {AppData}/StrategyConfig/{strategyId}.config.json
  内容: Priority, IsEnabled, Parameters

Type B: StrategyDatasetConfig(按数据集+会场的代码块配置)
  路径: {AppData}/StrategyConfig/{strategyId}/{dsHalf}-{vHalf}.config.json
  内容: 配置行 (StudentId, SeatRow/Column, Values[...])

Type C: 历史快照(自包含)
  路径: {basePath}/{venueId}/yyyyMMdd/*.json
  内容: SeatAssignments + 嵌入会场布局

相关文档