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() 获取空座列表后自主分配。
示例:FixedSeatStrategy、FrontRowRotationStrategy、DefragStrategy
依赖策略(Dependent)
实现 IDependentSeatingStrategy 接口,manifest 中 isIndependent: false。不在外部管道中执行,而是在 RandomFillStrategy 的分配循环中作为上下文策略运行。对 RandomFill 提议的每个 (student, seat) 对进行评估,返回三种结果之一。
示例:DeskMateStrategy、GenderRestrictedSeatStrategy、NoRepeatDeskMateStrategy
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 中(含 StrategyId、StrategyDisplayName、MessageKey 和 Args),管道执行后在 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 | — | Student、Venue、Both |
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 + 嵌入会场布局