后端: 1. 记忆系统移除 todo_hint 类型——随口记已由 Task 系统承接,todo_hint 语义重叠且无完成追踪 - 全链路清理:常量、校验、默认重要度、30 天 TTL、读取预算、LLM 抽取提示词枚举 - 总预算从四类收缩为三类(preference / constraint / fact) 2. 记忆抽取触发点从 chat-persist 移至 graph-completion——避免随口记消息被误提取为 constraint/preference - chat-persist consumer 不再自动入队 memory.extract.requested,仅负责聊天历史落库 - graph 完成后新增条件发布:检测 UsedQuickNote 标记,调用过 quick_note_create 则跳过记忆抽取 - ResetForNextRun 重置 UsedQuickNote,防止跨轮残留导致后续正常消息记忆抽取被误跳过 3. 任务类查询接口返回 items 补充数据库主键 ID(前端拖拽编排依赖此字段) 前端: 4. 排程视图新增手动编排模式——侧边栏任务块拖拽入周课表 + 悬浮删除热区 + 建议块虚线标识 - TaskClassSidebar 拖拽发起 + 预览态嵌入时间格式化(含周次/星期) - WeekPlanningBoard 外部拖入 / 内部移动 / 悬浮删除区交互 - ScheduleView 手动编排状态机(进入/退出/取消/覆盖确认)+ apply 时同步处理新增与删除
193 lines
6.8 KiB
Go
193 lines
6.8 KiB
Go
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 {
|
||
ID int `json:"id,omitempty"` // 任务块的数据库主键 ID(查询时返回,创建时可省略)
|
||
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
|
||
}
|
||
|
||
type UserInsertTaskClassItemToScheduleRequestBatch struct {
|
||
TaskClassID int `json:"task_class_id" binding:"required"`
|
||
Items []SingleTaskClassItem `json:"items" binding:"required,dive,required"`
|
||
}
|
||
|
||
type SingleTaskClassItem struct {
|
||
TaskItemID int `json:"task_item_id" binding:"required"`
|
||
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 // 已应用
|
||
)
|