From 1399b38f16c2c999b66c7fdad727c4537006ad5f Mon Sep 17 00:00:00 2001 From: LoveLosita <2810873701@qq.com> Date: Tue, 24 Feb 2026 19:44:33 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.3.7.dev.260224=20fix:=20?= =?UTF-8?q?=F0=9F=A7=A0=20=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E6=97=A5=E7=A8=8B=E6=8E=A5=E5=8F=A3=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E4=B8=8E=E5=88=86=E9=85=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复少量边界用例下“排课时间是否充足”的误判问题,完善可用时间计算逻辑 * 修复周视图返回数据存在周次数量偏差的问题,确保周维度结果与实际排课数据一致 * 修复 `steady` 模式下编排不均匀问题 * 引入“逻辑空间映射”策略,将碎片时间段进行拼接后统一计算步长 * 优化分配算法,使 `steady` 模式下课程分布达到绝对平均状态 * 提升算法在高碎片时间场景下的稳定性与均衡性 --- backend/logic/smart_planning.go | 238 +++++++++++++++++++++++--------- 1 file changed, 174 insertions(+), 64 deletions(-) diff --git a/backend/logic/smart_planning.go b/backend/logic/smart_planning.go index d44dde5..7b2c7da 100644 --- a/backend/logic/smart_planning.go +++ b/backend/logic/smart_planning.go @@ -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 -}