Files
smartmate/backend/model/task-class.go
LoveLosita f934668838 Version: 0.3.6.dev.260223
feat: 🚀 新增智能编排日程接口与算法模块

* 新增智能编排日程接口,实现自动生成周维度课程安排
* 抽离核心算法至 `Logic` 包,统一存放调度与排课相关算法逻辑,优化项目结构分层
* 大多数用例测试通过,当前存在少量边界用例下“排课时间是否充足”的误判问题
* 返回的周视图数据在极端场景下存在数量偏差,待进一步完善边界控制

fix: 🐛 修复批量导入课程接口 500 错误

* 修复批量导入课程接口中未在 `event` 结构体填写时间字段的问题
* 解决因时间字段为空导致的服务端 500 错误,保证数据完整性

refactor: ♻️ 新增入参校验逻辑保障调度稳定性

* 在添加任务类时新增入参校验逻辑
* 避免非法数据进入调度流程,确保自动编排日程接口执行稳定

docs: 📚 更新 README 智能编排算法说明

* 补充智能编排日程算法的设计思路与实现说明

undo: ⚠️ 追加导入课程后缓存未自动失效

* 追加导入课程后未自动删除对应周安排缓存,存在数据不一致风险
* 当前未能稳定复现,计划后续定位缓存失效时序与触发条件问题
2026-02-23 21:49:46 +08:00

178 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// TaskClass 用于和数据库中的 task_classes 表进行映射
type TaskClass struct {
//section 1
ID int `gorm:"column:id;primaryKey;autoIncrement"`
UserID *int `gorm:"column:user_id;index:idx_task_classes_user_id"`
//section 2
Name *string `gorm:"column:name;size:255"`
Mode *string `gorm:"column:mode;type:enum('auto','manual')"`
StartDate *time.Time `gorm:"column:start_date"`
EndDate *time.Time `gorm:"column:end_date"`
//section 3
TotalSlots *int `gorm:"column:total_slots;comment:分配的总节数"`
AllowFillerCourse *bool `gorm:"column:allow_filler_course;default:true"`
Strategy *string `gorm:"column:strategy;type:enum('steady','rapid')"`
ExcludedSlots IntSlice `gorm:"column:excluded_slots;type:json;comment:不想要的时段切片"`
Items []TaskClassItem `gorm:"foreignKey:CategoryID;references:ID"` // 一对多关联:一个 TaskClass 有多个 TaskClassItem
}
// IntSlice 用于把 []int 以 JSON 形式存入/读出数据库 json 字段
type IntSlice []int
func (s IntSlice) Value() (driver.Value, error) {
// nil -> NULL空切片 -> "[]"
if s == nil {
return nil, nil
}
return json.Marshal([]int(s))
}
func (s *IntSlice) Scan(value any) error {
if value == nil {
*s = nil
return nil
}
var data []byte
switch v := value.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("IntSlice: 不支持的扫描类型: %T", value)
}
var out []int
if err := json.Unmarshal(data, &out); err != nil {
return err
}
*s = IntSlice(out)
return nil
}
// TaskClassItem 用于和数据库中的 task_items 表进行映射
type TaskClassItem struct {
//section 1
ID int `gorm:"column:id;primaryKey;autoIncrement"`
CategoryID *int `gorm:"column:category_id"` //对应 TaskClass 的 ID
//section 2
Order *int `gorm:"column:order"`
Content *string `gorm:"column:content;type:text"`
EmbeddedTime *TargetTime `gorm:"column:embedded_time;type:json;comment:目标时间{date,section_from,section_to}"`
Status *int `gorm:"column:status;comment:1:未安排, 2:已应用"`
}
// UserAddTaskClassRequest 用于处理用户添加任务类别的请求
type UserAddTaskClassRequest struct {
Name string `json:"name" binding:"required"`
StartDate string `json:"start_date" binding:"required"` // YYYY-MM-DD
EndDate string `json:"end_date" binding:"required"` // YYYY-MM-DD
Mode string `json:"mode" binding:"required,oneof=auto manual"`
Config UserAddTaskClassConfig `json:"config" binding:"required"`
Items []UserAddTaskClassItemRequest `json:"items" binding:"required"`
}
// UserAddTaskClassConfig 用于处理用户添加任务类别时的配置部分
type UserAddTaskClassConfig struct {
TotalSlots int `json:"total_slots" binding:"required,min=1"`
AllowFillerCourse bool `json:"allow_filler_course"`
Strategy string `json:"strategy" binding:"required,oneof=steady rapid"`
ExcludedSlots []int `json:"excluded_slots"`
}
// UserAddTaskClassItemRequest 用于处理用户添加任务类别时的任务块部分
type UserAddTaskClassItemRequest struct {
Order int `json:"order" binding:"required,min=1"`
Content string `json:"content" binding:"required"`
EmbeddedTime *TargetTime `json:"embedded_time"` // 例: 2025-12-22 1-2节; nil 表示未安排
}
// TargetTime 表示任务块的目标时间
type TargetTime struct {
Week int `json:"week"` // 周次
DayOfWeek int `json:"day_of_week"` // 星期几
SectionFrom int `json:"section_from"` // 起始节次
SectionTo int `json:"section_to"` // 结束节次
}
// UserGetTaskClassesResponse 用于返回用户的任务类列表,展示简要信息
type UserGetTaskClassesResponse struct {
TaskClasses []TaskClassSummary `json:"task_classes"`
}
// TaskClassSummary 提供任务类别的简要信息
type TaskClassSummary struct {
ID int `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
Strategy string `json:"strategy"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
TotalSlots int `json:"total_slots"`
}
type UserInsertTaskClassItemToScheduleRequest struct {
Week int `json:"week" binding:"required,min=1"`
DayOfWeek int `json:"day_of_week" binding:"required,min=1,max=7"`
StartSection int `json:"start_section" binding:"required,min=1"`
EndSection int `json:"end_section" binding:"required,min=1,gtefield=StartSection"`
EmbedCourseEventID int `json:"embed_course_event_id"` // 可选,嵌入的课程日程事件 ID
}
// Value 实现 driver.Valuer 接口,负责将 TargetTime 转换为数据库存储的格式
func (t *TargetTime) Value() (driver.Value, error) {
if t == nil {
return nil, nil
}
// 💡 关键:调用 json.Marshal 将结构体转为 []byte
// 这样 GORM 就能把这一串 JSON 存进数据库的 text/json 字段了
return json.Marshal(t)
}
// Scan 实现 sql.Scanner 接口,负责将数据库中的值转换为 TargetTime 结构体
func (t *TargetTime) Scan(value any) error {
if value == nil {
// 如果数据库是 NULL保持指针对应的对象为零值即可
// 或者在业务层判断 nil
return nil
}
var data []byte
switch v := value.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("TargetTime: 不支持的扫描类型: %T", value)
}
return json.Unmarshal(data, t)
}
// TableName 指定 TaskClass 对应的数据库表名
func (TaskClass) TableName() string {
return "task_classes"
}
// TableName 指定 TaskClassItem 对应的数据库表名
func (TaskClassItem) TableName() string {
return "task_items"
}
// 任务块状态常量
const (
TaskItemStatusUnscheduled = 1 // 未安排
TaskItemStatusApplied = 2 // 已应用
)