Version: 0.3.7.dev.260224
fix: 🧠 修复智能编排日程接口边界与分配问题
* 修复少量边界用例下“排课时间是否充足”的误判问题,完善可用时间计算逻辑
* 修复周视图返回数据存在周次数量偏差的问题,确保周维度结果与实际排课数据一致
* 修复 `steady` 模式下编排不均匀问题
* 引入“逻辑空间映射”策略,将碎片时间段进行拼接后统一计算步长
* 优化分配算法,使 `steady` 模式下课程分布达到绝对平均状态
* 提升算法在高碎片时间场景下的稳定性与均衡性
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/conv"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
@@ -25,11 +23,12 @@ type slotNode struct {
|
||||
type grid struct {
|
||||
data map[int]map[int][13]slotNode
|
||||
startWeek int
|
||||
startDay int // 🚀 新增:精确的开始星期
|
||||
startDay int
|
||||
endWeek int
|
||||
endDay int // 🚀 新增:精确的结束星期
|
||||
endDay int
|
||||
}
|
||||
|
||||
// getNode 和 setNode 是对 grid 数据结构的封装,确保我们在访问时能正确处理默认值(Free)和边界情况
|
||||
func (g *grid) getNode(w, d, s int) slotNode {
|
||||
if dayMap, ok := g.data[w]; ok {
|
||||
return dayMap[d][s]
|
||||
@@ -53,35 +52,30 @@ func (g *grid) isAvailable(w, d, s int) bool {
|
||||
}
|
||||
|
||||
// countAvailableSlots 统计指定周次范围内所有可用的原子节次总数
|
||||
func (g *grid) countAvailableSlots(startW, startD, startS int) int {
|
||||
func (g *grid) countAvailableSlots(currW, currD, currS int) int {
|
||||
count := 0
|
||||
for w := startW; w <= g.endWeek; w++ {
|
||||
if currW == 0 && currD == 0 && currS == 0 {
|
||||
currW, currD, currS = g.startWeek, g.startDay, 1
|
||||
}
|
||||
for w := currW; w <= g.endWeek; w++ {
|
||||
dayMap, hasData := g.data[w]
|
||||
for d := 1; d <= 7; d++ {
|
||||
// 🚀 头部裁剪:过滤开始日期前的天数
|
||||
if w == startW && d < startD {
|
||||
if w == currW && d < currD {
|
||||
continue
|
||||
}
|
||||
// 🚀 尾部裁剪:过滤结束日期后的天数
|
||||
if w == g.endWeek && d > g.endDay {
|
||||
break
|
||||
}
|
||||
|
||||
var dayData [13]slotNode
|
||||
if hasData {
|
||||
dayData = dayMap[d]
|
||||
}
|
||||
|
||||
for s := 1; s <= 12; s++ {
|
||||
// 🚀 头部裁剪:过滤开始节次前的槽位
|
||||
if w == startW && d == startD && s < startS {
|
||||
if w == currW && d == currD && s < currS {
|
||||
continue
|
||||
}
|
||||
// 🚀 尾部裁剪:过滤结束节次后的槽位
|
||||
if w == g.endWeek && d == g.endDay {
|
||||
break
|
||||
}
|
||||
|
||||
if dayData[s].Status == Free || dayData[s].Status == Filler {
|
||||
count++
|
||||
}
|
||||
@@ -130,6 +124,48 @@ func (g *grid) FindNextAvailable(currW, currD, currS int) (int, int, int) {
|
||||
return -1, -1, -1
|
||||
}
|
||||
|
||||
// 辅助函数:向后跳过指定数量的可用坑位
|
||||
func (g *grid) skipAvailableSlots(w, d, s, skipCount int) (int, int, int) {
|
||||
if skipCount <= 0 {
|
||||
// 即使 gap 为 0,也要至少移到下一节
|
||||
s++
|
||||
if s > 12 {
|
||||
s = 1
|
||||
d++
|
||||
if d > 7 {
|
||||
d = 1
|
||||
w++
|
||||
}
|
||||
}
|
||||
return w, d, s
|
||||
}
|
||||
found := 0
|
||||
currW, currD, currS := w, d, s+1
|
||||
for currW <= g.endWeek {
|
||||
if currS > 12 {
|
||||
currS = 1
|
||||
currD++
|
||||
if currD > 7 {
|
||||
currD = 1
|
||||
currW++
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 如果已经跳到了最后一天,不要再跳了,直接返回终点坐标
|
||||
if currW == g.endWeek && currD > g.endDay {
|
||||
return g.endWeek, g.endDay, 12
|
||||
}
|
||||
if g.isAvailable(currW, currD, currS) {
|
||||
found++
|
||||
if found > skipCount {
|
||||
return currW, currD, currS
|
||||
}
|
||||
}
|
||||
currS++
|
||||
}
|
||||
return currW, currD, currS
|
||||
}
|
||||
|
||||
func SmartPlanningMainLogic(schedules []model.Schedule, taskClass *model.TaskClass) ([]model.UserWeekSchedule, error) {
|
||||
//1.先构建时间格子
|
||||
g := buildTimeGrid(schedules, taskClass)
|
||||
@@ -147,19 +183,20 @@ func SmartPlanningMainLogic(schedules []model.Schedule, taskClass *model.TaskCla
|
||||
func buildTimeGrid(schedules []model.Schedule, taskClass *model.TaskClass) *grid {
|
||||
// 🚀 核心修正:获取精确的起始坐标
|
||||
startW, startD, _ := conv.RealDateToRelativeDate(taskClass.StartDate.Format(conv.DateFormat))
|
||||
endW, _, _ := conv.RealDateToRelativeDate(taskClass.EndDate.Format(conv.DateFormat))
|
||||
|
||||
endW, endD, _ := conv.RealDateToRelativeDate(taskClass.EndDate.Format(conv.DateFormat))
|
||||
// 将信息初始化到 grid 结构中
|
||||
g := &grid{
|
||||
data: make(map[int]map[int][13]slotNode),
|
||||
startWeek: startW,
|
||||
startDay: startD,
|
||||
endWeek: endW,
|
||||
endDay: endD,
|
||||
}
|
||||
|
||||
//标记屏蔽时段 (Blocked)
|
||||
for _, blockIdx := range taskClass.ExcludedSlots {
|
||||
sFrom, sTo := (blockIdx-1)*2+1, blockIdx*2
|
||||
for w := startW; w <= endW; w++ {
|
||||
for d := 1; d <= 7; d++ {
|
||||
for d := 1; d <= 7; d++ { //🚀 注意:这里的屏蔽是针对每天的,所以直接循环 1-7 天
|
||||
for s := sFrom; s <= sTo; s++ {
|
||||
g.setNode(w, d, s, slotNode{Status: Blocked})
|
||||
}
|
||||
@@ -174,6 +211,7 @@ func buildTimeGrid(schedules []model.Schedule, taskClass *model.TaskClass) *grid
|
||||
continue
|
||||
}
|
||||
status := Occupied
|
||||
// 只有当课程允许嵌入且当前事件支持嵌入时,才标记为 Filler
|
||||
if *taskClass.AllowFillerCourse && s.Event.CanBeEmbedded {
|
||||
status = Filler
|
||||
}
|
||||
@@ -184,7 +222,7 @@ func buildTimeGrid(schedules []model.Schedule, taskClass *model.TaskClass) *grid
|
||||
}
|
||||
|
||||
// computeAllocation 是核心函数,负责根据当前的时间格子状态和排课策略,计算出每个任务块的具体安排时间
|
||||
func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([]model.TaskClassItem, error) {
|
||||
/*func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([]model.TaskClassItem, error) {
|
||||
if len(items) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
@@ -192,11 +230,11 @@ func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([
|
||||
// 🚀 核心修正 1:获取真正的开始坐标(周、天、节)
|
||||
// 这里假设你已经通过 conv 把 StartDate 换成了 w1, d1, s1
|
||||
startW := g.startWeek
|
||||
startD := 1 // 建议从 conv 传入具体的 DayOfWeek
|
||||
startD := g.startDay // 建议从 conv 传入具体的 DayOfWeek
|
||||
startS := 1
|
||||
|
||||
// 1. 获取可用资源总量
|
||||
totalAvailable := g.countAvailableSlots(g.startWeek, 1, 1)
|
||||
totalAvailable := g.countAvailableSlots(0, 0, 0)
|
||||
// 假设每个任务块至少占用 2 个原子槽位
|
||||
totalRequired := len(items) * 2
|
||||
|
||||
@@ -253,48 +291,120 @@ func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([
|
||||
}
|
||||
// 🚀 核心改进:结果完整性校验
|
||||
if lastPlacedIndex < len(items)-1 {
|
||||
return nil, fmt.Errorf("排程中断:由于时间片碎片化,仅成功安排了 %d/%d 个任务块,请尝试扩充时间范围或删减屏蔽位", lastPlacedIndex+1, len(items))
|
||||
return nil, fmt.Errorf("排程中断:由于时间片碎片化,仅成功安排了 %d/%d 个任务块,请尝试扩充时间范围或删减屏蔽位", lastPlacedIndex+1, len(items))
|
||||
return nil, respond.TimeNotEnoughForAutoScheduling
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}*/
|
||||
|
||||
type slotCoord struct {
|
||||
w, d, s int
|
||||
}
|
||||
|
||||
// getAllAvailable 获取窗口内所有可用的原子节次坐标(逻辑一维化)
|
||||
func (g *grid) getAllAvailable() []slotCoord {
|
||||
var coords []slotCoord
|
||||
for w := g.startWeek; w <= g.endWeek; w++ {
|
||||
dayMap, hasData := g.data[w]
|
||||
for d := 1; d <= 7; d++ {
|
||||
// 边界裁剪逻辑
|
||||
if w == g.startWeek && d < g.startDay {
|
||||
continue
|
||||
}
|
||||
if w == g.endWeek && d > g.endDay {
|
||||
break
|
||||
}
|
||||
|
||||
var dayData [13]slotNode
|
||||
if hasData {
|
||||
dayData = dayMap[d]
|
||||
}
|
||||
|
||||
for s := 1; s <= 12; s++ {
|
||||
// 顺着你的逻辑,不限开始节次,但需注意状态判定
|
||||
if dayData[s].Status == Free || dayData[s].Status == Filler {
|
||||
coords = append(coords, slotCoord{w, d, s})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return coords
|
||||
}
|
||||
|
||||
func computeAllocation(g *grid, items []model.TaskClassItem, strategy string) ([]model.TaskClassItem, error) {
|
||||
if len(items) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// 1. 预处理:提取所有可用坑位
|
||||
coords := g.getAllAvailable()
|
||||
totalAvailable := len(coords)
|
||||
totalRequired := len(items) * 2 // 基础需求:每个任务 2 节
|
||||
|
||||
if totalAvailable < totalRequired {
|
||||
return nil, respond.TimeNotEnoughForAutoScheduling
|
||||
}
|
||||
|
||||
// 2. 计算精准步长
|
||||
gap := 0
|
||||
if strategy == "steady" {
|
||||
gap = (totalAvailable - totalRequired) / (len(items) + 1)
|
||||
}
|
||||
|
||||
// 3. 线性映射分配
|
||||
// cursor 是我们在逻辑切片中的“指针”
|
||||
cursor := gap
|
||||
lastPlacedIndex := -1
|
||||
|
||||
for i := range items {
|
||||
if cursor >= totalAvailable {
|
||||
break
|
||||
}
|
||||
|
||||
// 获取当前逻辑位置对应的物理坐标
|
||||
startLoc := coords[cursor]
|
||||
w, d, s := startLoc.w, startLoc.d, startLoc.s
|
||||
|
||||
// 4. 容器长度探测 (顺着你的逻辑)
|
||||
node := g.getNode(w, d, s)
|
||||
slotLen := 2
|
||||
if node.Status == Filler {
|
||||
slotLen = 1
|
||||
currID := node.EventID
|
||||
for checkS := s + 1; checkS <= 12; checkS++ {
|
||||
if next := g.getNode(w, d, checkS); next.Status == Filler && next.EventID == currID {
|
||||
slotLen++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if s == 12 || !g.isAvailable(w, d, s+1) {
|
||||
// 如果是 Free 区域,但下一节不可用,则被迫设为 1 节
|
||||
slotLen = 1
|
||||
}
|
||||
|
||||
// 回填时间
|
||||
endS := s + slotLen - 1
|
||||
items[i].EmbeddedTime = &model.TargetTime{
|
||||
SectionFrom: s, SectionTo: endS,
|
||||
Week: w, DayOfWeek: d,
|
||||
}
|
||||
|
||||
// 标记占用 (物理网格)
|
||||
for sec := s; sec <= endS; sec++ {
|
||||
g.setNode(w, d, sec, slotNode{Status: Occupied})
|
||||
}
|
||||
|
||||
// 🚀 核心进步:逻辑跳跃
|
||||
// 既然任务占用了 slotLen 节,我们在逻辑切片中也向后推 slotLen 个位置,再加 gap
|
||||
cursor += slotLen + gap
|
||||
lastPlacedIndex = i
|
||||
}
|
||||
|
||||
if lastPlacedIndex < len(items)-1 {
|
||||
return nil, respond.TimeNotEnoughForAutoScheduling
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// 辅助函数:向后跳过指定数量的可用坑位
|
||||
func (g *grid) skipAvailableSlots(w, d, s, skipCount int) (int, int, int) {
|
||||
if skipCount <= 0 {
|
||||
// 即使 gap 为 0,也要至少移到下一节
|
||||
s++
|
||||
if s > 12 {
|
||||
s = 1
|
||||
d++
|
||||
if d > 7 {
|
||||
d = 1
|
||||
w++
|
||||
}
|
||||
}
|
||||
return w, d, s
|
||||
}
|
||||
|
||||
found := 0
|
||||
currW, currD, currS := w, d, s+1
|
||||
for currW <= g.endWeek {
|
||||
if currS > 12 {
|
||||
currS = 1
|
||||
currD++
|
||||
if currD > 7 {
|
||||
currD = 1
|
||||
currW++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if g.isAvailable(currW, currD, currS) {
|
||||
found++
|
||||
if found > skipCount {
|
||||
return currW, currD, currS
|
||||
}
|
||||
}
|
||||
currS++
|
||||
}
|
||||
return currW, currD, currS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user