diff --git a/backend/api/course.go b/backend/api/course.go index bc77df7..d0635bc 100644 --- a/backend/api/course.go +++ b/backend/api/course.go @@ -52,7 +52,7 @@ func (sa *CourseHandler) AddUserCourses(c *gin.Context) { } //2.从上下文获取用户ID userIDInterface := c.GetInt("user_id") - //3.调用 service 层的 AddUserCourses 方法添加课程 + //3.调用 service 层的 AddUserCoursesIntoSchedule 方法添加课程 // 创建一个带 1 秒超时的上下文 ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second) defer cancel() // 记得释放资源 diff --git a/backend/api/task-class.go b/backend/api/task-class.go index 99b21fa..2a2ce66 100644 --- a/backend/api/task-class.go +++ b/backend/api/task-class.go @@ -117,3 +117,35 @@ func (api *TaskClassHandler) UserUpdateTaskClass(c *gin.Context) { } c.JSON(http.StatusOK, respond.Ok) } + +func (api *TaskClassHandler) UserAddTaskClassItemIntoSchedule(c *gin.Context) { + var req model.UserInsertTaskClassItemToScheduleRequest + err := c.ShouldBindJSON(&req) + if err != nil { + c.JSON(http.StatusBadRequest, respond.WrongParamType) + return + } + taskID := c.Query("task_item_id") + //将taskID转换为int + intTaskID, err := strconv.Atoi(taskID) + if err != nil { + c.JSON(http.StatusBadRequest, respond.WrongParamType) + return + } + userIDInterface := c.GetInt("user_id") + // 创建一个带 1 秒超时的上下文 + ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second) + defer cancel() // 记得释放资源 + err = api.svc.AddTaskClassItemIntoSchedule(ctx, &req, userIDInterface, intTaskID) + if err != nil { + if errors.Is(err, respond.TaskClassItemNotBelongToUser) || errors.Is(err, respond.CourseNotBelongToUser) || + errors.Is(err, respond.CourseAlreadyEmbeddedByOtherTaskBlock) || errors.Is(err, respond.CourseTimeNotMatch) || + errors.Is(err, respond.ScheduleConflict) || errors.Is(err, respond.WrongCourseID) { + c.JSON(http.StatusBadRequest, err) + return + } + c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + return + } + c.JSON(http.StatusOK, respond.Ok) +} diff --git a/backend/cmd/start.go b/backend/cmd/start.go index daa269b..ced8946 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -45,11 +45,12 @@ func Start() { taskRepo := dao.NewTaskDAO(db) courseRepo := dao.NewCourseDAO(db) taskClassRepo := dao.NewTaskClassDAO(db) + scheduleRepo := dao.NewScheduleDAO(db) //service 层 userService := service.NewUserService(userRepo, cacheRepo) taskSv := service.NewTaskService(taskRepo) courseService := service.NewCourseService(courseRepo) - taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo) + taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo) //api 层 userApi := api.NewUserHandler(userService) taskApi := api.NewTaskHandler(taskSv) diff --git a/backend/config.yaml b/backend/config.yaml index f1e6b7b..20193b6 100644 --- a/backend/config.yaml +++ b/backend/config.yaml @@ -31,3 +31,9 @@ redis: port: 6379 password: "" db: 0 + +time: + zone: "Asia/Shanghai" + semesterStartDate: "2024-09-09" #学期开始日期,一定要设定为周一,以便于计算周数 + semesterEndDate: "2025-01-20" #学期结束日期,一定要设定为周日,确保最后一周完整 + diff --git a/backend/conv/task-class.go b/backend/conv/task-class.go index 7c3a3a0..521fb02 100644 --- a/backend/conv/task-class.go +++ b/backend/conv/task-class.go @@ -136,6 +136,30 @@ func ProcessUserGetCompleteTaskClassRequest(taskClass *model.TaskClass) (*model. return req, nil } +// UserInsertTaskItemRequestToModel 用于将填入空闲时段日程的请求转换为 Schedule 模型 +func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent) { + var schedules []model.Schedule + for section := startSection; section <= endSection; section++ { + req1 := &model.Schedule{ + UserID: userID, // 由调用方填充 + EmbeddedTaskID: &taskID, + Week: req.Week, + DayOfWeek: req.DayOfWeek, + Section: section, + Status: "normal", + } + schedules = append(schedules, *req1) + } + req2 := &model.ScheduleEvent{ + UserID: userID, // 由调用方填充 + Name: safeStr(item.Content), // 任务内容作为事件名称 + Type: "task", + RelID: &item.ID, // 关联到 TaskClassItem 的 ID + CanBeEmbedded: false, // 任务事件允许嵌入其他任务(如果需要的话) + } + return schedules, req2 +} + // --- 🛡️ 辅助工具函数:保持代码清爽并防止 Panic --- func safeStr(s *string) string { diff --git a/backend/conv/time.go b/backend/conv/time.go new file mode 100644 index 0000000..f3b7a2a --- /dev/null +++ b/backend/conv/time.go @@ -0,0 +1,52 @@ +package conv + +import ( + "errors" + "time" + + "github.com/LoveLosita/smartflow/backend/respond" + "github.com/spf13/viper" +) + +// DateFormat 此处定义基准学期的开始和结束日期 +const DateFormat = "2006-01-02" + +var ( + SemesterStartDate = viper.GetString("semesterStartDate") // 从配置文件中读取学期开始日期 + SemesterEndDate = viper.GetString("semesterEndDate") // 从配置文件中读取学期结束日期 +) + +// RealDateToRelativeDate 将绝对日期转换为相对日期(格式: "week-day") +func RealDateToRelativeDate(realDate string) (int, int, error) { + t, err := time.Parse(DateFormat, realDate) + if err != nil { + return 0, 0, err + } + start, _ := time.Parse(DateFormat, SemesterStartDate) + end, _ := time.Parse(DateFormat, SemesterEndDate) + // 边界校验:日期必须在学期范围内 + if t.Before(start) || t.After(end) { + return 0, 0, errors.New("日期超出学期范围") + } + // 计算天数差值(注意:24小时为一个基准天) + days := int(t.Sub(start).Hours() / 24) + // 计算周数和星期 + // 假设 SemesterStartDate 对应第 1 周,周 1 + week := (days / 7) + 1 + dayOfWeek := (days % 7) + 1 + return week, dayOfWeek, nil +} + +// RelativeDateToRealDate 将相对日期转换为绝对日期(输入格式: "week-day") +func RelativeDateToRealDate(week, dayOfWeek int) (string, error) { + start, _ := time.Parse(DateFormat, SemesterStartDate) + // 核心转换逻辑:(周-1)*7 + (天-1) + offsetDays := (week-1)*7 + (dayOfWeek - 1) + targetDate := start.AddDate(0, 0, offsetDays) + // 校验计算出的日期是否超出学期结束日期 + end, _ := time.Parse(DateFormat, SemesterEndDate) + if targetDate.After(end) { + return "", respond.TimeOutOfRangeOfThisSemester + } + return targetDate.Format(DateFormat), nil +} diff --git a/backend/dao/course.go b/backend/dao/course.go index 705c95b..94b2524 100644 --- a/backend/dao/course.go +++ b/backend/dao/course.go @@ -1,6 +1,8 @@ package dao import ( + "context" + "github.com/LoveLosita/smartflow/backend/model" "gorm.io/gorm" ) @@ -16,9 +18,29 @@ func NewCourseDAO(db *gorm.DB) *CourseDAO { } } -func (dao *CourseDAO) AddUserCourses(courses []model.Schedule) error { - if err := dao.db.Create(&courses).Error; err != nil { +func (dao *CourseDAO) AddUserCoursesIntoSchedule(ctx context.Context, courses []model.Schedule) error { + if err := dao.db.WithContext(ctx).Create(&courses).Error; err != nil { return err } return nil } + +func (dao *CourseDAO) AddUserCoursesIntoScheduleEvents(ctx context.Context, events []model.ScheduleEvent) ([]int, error) { + if err := dao.db.WithContext(ctx).Create(&events).Error; err != nil { + return nil, err + } + ids := make([]int, 0, len(events)) + for i := range events { + ids = append(ids, int(events[i].ID)) + } + return ids, nil +} + +// Transaction 在同一个数据库事务中执行传入的函数,供 service 层复用(自动提交/回滚) +// 规则:fn 返回 nil \-\> 提交;fn 返回 error 或发生 panic \-\> 回滚 +// 说明:gorm\.\(\\\*DB\)\.Transaction 会在 fn 返回 error 时回滚,并在发生 panic 时自动回滚后继续向上抛出 panic +func (dao *CourseDAO) Transaction(fn func(txDAO *CourseDAO) error) error { + return dao.db.Transaction(func(tx *gorm.DB) error { + return fn(NewCourseDAO(tx)) + }) +} diff --git a/backend/dao/schedule.go b/backend/dao/schedule.go new file mode 100644 index 0000000..6cb561e --- /dev/null +++ b/backend/dao/schedule.go @@ -0,0 +1,135 @@ +package dao + +import ( + "context" + "errors" + + "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/respond" + "gorm.io/gorm" +) + +type ScheduleDAO struct { + db *gorm.DB +} + +// NewScheduleDAO 创建TaskClassDAO实例 +func NewScheduleDAO(db *gorm.DB) *ScheduleDAO { + return &ScheduleDAO{ + db: db, + } +} + +func (dao *ScheduleDAO) AddSchedules(schedules []model.Schedule) ([]int, error) { + if err := dao.db.Create(&schedules).Error; err != nil { + return nil, err + } + ids := make([]int, len(schedules)) + for i, s := range schedules { + ids[i] = s.ID + } + return ids, nil +} + +func (dao *ScheduleDAO) EmbedTaskIntoSchedule(startSection, endSection, dayOfWeek, week, userID, taskID int) error { + // 仅更新指定:用户/周/星期/节次区间 的记录,将 embedded_task_id 精准写入 taskID + res := dao.db. + Table("schedules"). + Where("user_id = ? AND week = ? AND day_of_week = ? AND section BETWEEN ? AND ?", userID, week, dayOfWeek, startSection, endSection). + Update("embedded_task_id", taskID) + + return res.Error +} + +func (dao *ScheduleDAO) GetCourseUserIDByID(ctx context.Context, courseScheduleEventID int) (int, error) { + type row struct { + UserID *int `gorm:"column:user_id"` + } + + var r row + err := dao.db.WithContext(ctx). + Table("schedule_events"). + Select("user_id"). + Where("id = ?", courseScheduleEventID). + First(&r).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, respond.WrongCourseID + } + return 0, err + } + if r.UserID == nil { + return 0, respond.WrongCourseID + } + return *r.UserID, nil +} + +// IsCourseEmbeddedByOtherTaskBlock 判断课程在给定节次区间内是否已被其他任务块嵌入(用于业务限制) +func (dao *ScheduleDAO) IsCourseEmbeddedByOtherTaskBlock(ctx context.Context, courseID, startSection, endSection int) (bool, error) { + // 若区间非法,视为不冲突 + if startSection <= 0 || endSection <= 0 || startSection > endSection { + return false, nil + } + + var cnt int64 + err := dao.db.WithContext(ctx). + Table("schedules"). + Where("id = ?", courseID). + Where("section BETWEEN ? AND ?", startSection, endSection). + Where("embedded_task_id IS NOT NULL AND embedded_task_id <> 0"). + Count(&cnt).Error + if err != nil { + return false, err + } + return cnt > 0, nil +} + +func (dao *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, week, dayOfWeek int, sections []int) (bool, error) { + // 无节次则视为无冲突 + if len(sections) == 0 { + return false, nil + } + // 统计同一用户、同一周、同一天、且节次有交集的排程数量 + // 约定表字段:user_id, week, day_of_week, section + var cnt int64 + err := dao.db.WithContext(ctx). + Table("schedules"). + Where("user_id = ? AND week = ? AND day_of_week = ?", userID, week, dayOfWeek). + Where("section IN ?", sections). + Count(&cnt).Error + if err != nil { + return false, err + } + return cnt > 0, nil +} + +func (dao *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEventID, week, dayOfWeek, startSection, endSection int) (bool, error) { + // 区间非法直接不匹配 + if startSection <= 0 || endSection <= 0 || startSection > endSection { + return false, nil + } + + // 核对该课程事件在指定 周\+星期 下,是否存在覆盖整个节次区间的排程记录 + // 说明:此处按你当前表结构的用法(schedule\_events 存事件,schedules 存节次明细)来写: + // schedules 里通过 schedule\_event\_id 关联到 schedule\_events.id + var cnt int64 + err := dao.db.WithContext(ctx). + Table("schedules"). + Where("event_id = ?", courseScheduleEventID). + Where("week = ? AND day_of_week = ?", week, dayOfWeek). + Where("section BETWEEN ? AND ?", startSection, endSection). + Count(&cnt).Error + if err != nil { + return false, err + } + + // 需要区间内的每一节都存在记录才算匹配 + return cnt == int64(endSection-startSection+1), nil +} + +func (dao *ScheduleDAO) AddScheduleEvent(scheduleEvent *model.ScheduleEvent) (int, error) { + if err := dao.db.Create(&scheduleEvent).Error; err != nil { + return 0, err + } + return scheduleEvent.ID, nil +} diff --git a/backend/dao/task-class.go b/backend/dao/task-class.go index 6122d45..a18fbcd 100644 --- a/backend/dao/task-class.go +++ b/backend/dao/task-class.go @@ -138,3 +138,46 @@ func (dao *TaskClassDAO) GetCompleteTaskClassByID(ctx context.Context, id int, u } return &taskClass, nil } + +func (dao *TaskClassDAO) GetTaskClassItemByID(ctx context.Context, id int) (*model.TaskClassItem, error) { + var item model.TaskClassItem + err := dao.db.WithContext(ctx). + Where("id = ?", id). + First(&item).Error + if err != nil { + return nil, err + } + return &item, nil +} + +func (dao *TaskClassDAO) GetTaskClassIDByTaskItemID(ctx context.Context, itemID int) (int, error) { + var item model.TaskClassItem + err := dao.db.WithContext(ctx). + Select("category_id"). + Where("id = ?", itemID). + First(&item).Error + if err != nil { + return 0, err + } + return *item.CategoryID, nil +} + +func (dao *TaskClassDAO) GetTaskClassUserIDByID(ctx context.Context, taskClassID int) (int, error) { + var taskClass model.TaskClass + err := dao.db.WithContext(ctx). + Select("user_id"). + Where("id = ?", taskClassID). + First(&taskClass).Error + if err != nil { + return 0, err + } + return *taskClass.UserID, nil +} + +func (dao *TaskClassDAO) UpdateTaskClassItemEmbeddedTime(ctx context.Context, taskID int, embeddedTime *model.TargetTime) error { + err := dao.db.WithContext(ctx). + Model(&model.TaskClassItem{}). + Where("id = ?", taskID). + Update("embedded_time", embeddedTime).Error + return err +} diff --git a/backend/model/course.go b/backend/model/course.go index b04c360..6e3f69b 100644 --- a/backend/model/course.go +++ b/backend/model/course.go @@ -9,10 +9,11 @@ type UserCheckCourseRequest struct { Location string `json:"location"` IsAllowTasks bool `json:"is_allow_tasks"` Arrangements []struct { - StartWeek int `json:"start_week"` - EndWeek int `json:"end_week"` - DayOfWeek int `json:"day_of_week"` - StartSection int `json:"start_section"` - EndSection int `json:"end_section"` + StartWeek int `json:"start_week"` + EndWeek int `json:"end_week"` + DayOfWeek int `json:"day_of_week"` + StartSection int `json:"start_section"` + EndSection int `json:"end_section"` + WeekType string `json:"week_type"` } `json:"arrangements"` } diff --git a/backend/model/schedule.go b/backend/model/schedule.go index 55ce168..c381e64 100644 --- a/backend/model/schedule.go +++ b/backend/model/schedule.go @@ -1,14 +1,26 @@ package model +type ScheduleEvent struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + UserID int `gorm:"column:user_id;index:idx_user_events;not null" json:"user_id"` + Name string `gorm:"column:name;type:varchar(255);not null;comment:课程或任务名称" json:"name"` + Location *string `gorm:"column:location;type:varchar(255);default:'';comment:地点 (教学楼/会议室)" json:"location"` + Type string `gorm:"column:type;type:enum('course','task');not null;comment:日程类型" json:"type"` + RelID *int `gorm:"column:rel_id;comment:关联原始数据ID (如教务系统的课程ID)" json:"rel_id"` + CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;default:0;comment:是否允许在此时段嵌入其他任务" json:"can_be_embedded"` +} + type Schedule struct { ID int `gorm:"primaryKey;autoIncrement" json:"id"` - UserID int `gorm:"column:user_id;index" json:"user_id"` - Type string `gorm:"type:enum('course','task');comment:course / task" json:"type"` - RelID int `gorm:"column:rel_id;comment:关联 course_id 或 task_item_id" json:"rel_id"` - EmbeddedTaskID *int `gorm:"column:embedded_task_id;index;comment:若为水课嵌入,记录任务ID" json:"embedded_task_id"` - Week int `gorm:"column:week;uniqueIndex:idx_user_slot_atomic,priority:2;comment:周次 (1-25)" json:"week"` - DayOfWeek int `gorm:"column:day_of_week;uniqueIndex:idx_user_slot_atomic,priority:3;comment:星期 (1-7)" json:"day_of_week"` - Section int `gorm:"column:section;uniqueIndex:idx_user_slot_atomic,priority:4;comment:原子化节次 (1-12)" json:"section"` - Status string `gorm:"type:enum('normal','interrupted');default:normal" json:"status"` - CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;comment:是否允许嵌入任务" json:"can_be_embedded"` + EventID int `gorm:"column:event_id;index:idx_event_id;not null;comment:关联元数据ID" json:"event_id"` + UserID int `gorm:"column:user_id;uniqueIndex:idx_user_slot_atomic,priority:1;not null;comment:冗余UID方便直接查询" json:"user_id"` + Week int `gorm:"column:week;uniqueIndex:idx_user_slot_atomic,priority:2;not null;comment:周次 (1-25)" json:"week"` + DayOfWeek int `gorm:"column:day_of_week;uniqueIndex:idx_user_slot_atomic,priority:3;not null;comment:星期 (1-7)" json:"day_of_week"` + Section int `gorm:"column:section;uniqueIndex:idx_user_slot_atomic,priority:4;not null;comment:原子化节次 (1-12)" json:"section"` + EmbeddedTaskID *int `gorm:"column:embedded_task_id;comment:若为水课嵌入,记录具体的任务项ID" json:"embedded_task_id"` + Status string `gorm:"column:status;type:enum('normal','interrupted');default:'normal';comment:状态: 正常/因故中断" json:"status"` } + +func (ScheduleEvent) TableName() string { return "schedule_events" } + +func (Schedule) TableName() string { return "schedules" } diff --git a/backend/model/task-class.go b/backend/model/task-class.go index f7dec23..5e480a5 100644 --- a/backend/model/task-class.go +++ b/backend/model/task-class.go @@ -64,9 +64,10 @@ type UserAddTaskClassItemRequest struct { // TargetTime 表示任务块的目标时间 type TargetTime struct { - Date string `json:"date"` // 例: 2025-12-22 - SectionFrom int `json:"section_from"` // 起始节次 - SectionTo int `json:"section_to"` // 结束节次 + Week int `json:"week"` // 周次 + DayOfWeek int `json:"day_of_week"` // 星期几 + SectionFrom int `json:"section_from"` // 起始节次 + SectionTo int `json:"section_to"` // 结束节次 } // UserGetTaskClassesResponse 用于返回用户的任务类列表,展示简要信息 @@ -85,6 +86,14 @@ type TaskClassSummary struct { 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 { diff --git a/backend/respond/respond.go b/backend/respond/respond.go index 9104a4f..7c51b84 100644 --- a/backend/respond/respond.go +++ b/backend/respond/respond.go @@ -148,4 +148,39 @@ var ( //请求相关的响应 Status: "40021", Info: "user task class forbidden", } + + TaskClassItemNotBelongToUser = Response{ //任务类项目不属于用户 + Status: "40022", + Info: "task class item does not belong to user", + } + + TimeOutOfRangeOfThisSemester = Response{ //时间超出本学期范围 + Status: "40023", + Info: "time out of range of this semester", + } + + CourseNotBelongToUser = Response{ //课程不属于用户 + Status: "40024", + Info: "course does not belong to user", + } + + CourseAlreadyEmbeddedByOtherTaskBlock = Response{ //课程已被其他任务块嵌入 + Status: "40025", + Info: "course already embedded by other task block", + } + + ScheduleConflict = Response{ //日程冲突 + Status: "40026", + Info: "schedule conflict", + } + + WrongCourseID = Response{ //课程ID错误 + Status: "40027", + Info: "wrong course id", + } + + CourseTimeNotMatch = Response{ //课程时间不匹配 + Status: "40028", + Info: "course time not match", + } ) diff --git a/backend/routers/routers.go b/backend/routers/routers.go index c82bf88..0677967 100644 --- a/backend/routers/routers.go +++ b/backend/routers/routers.go @@ -67,6 +67,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO) *gin.Engine taskClassGroup.GET("/list", handlers.TaskClassHandler.UserGetTaskClassInfos) taskClassGroup.GET("/get", handlers.TaskClassHandler.UserGetCompleteTaskClass) taskClassGroup.PUT("/update", handlers.TaskClassHandler.UserUpdateTaskClass) + taskClassGroup.POST("/insert-into-schedule", handlers.TaskClassHandler.UserAddTaskClassItemIntoSchedule) } } // 初始化Gin引擎 diff --git a/backend/service/course.go b/backend/service/course.go index 1b831d3..20e3daa 100644 --- a/backend/service/course.go +++ b/backend/service/course.go @@ -41,33 +41,61 @@ func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImpor return respond.WrongCourseInfo } } - //2.转换为 Schedule 切片 var finalSchedules []model.Schedule + var finalScheduleEvents []model.ScheduleEvent + var pos []int for _, course := range req.Courses { - var schedules []model.Schedule + // 避免取 range 迭代变量字段地址导致指针复用问题 + location := course.Location for _, arrangement := range course.Arrangements { + weekType := arrangement.WeekType for week := arrangement.StartWeek; week <= arrangement.EndWeek; week++ { + if weekType == "odd" && week%2 == 0 { + continue + } + if weekType == "even" && week%2 != 0 { + continue + } + //2.转换为 Schedule_event 切片 + scheduleEvent := model.ScheduleEvent{ + UserID: userID, + Name: course.CourseName, + Location: &location, + Type: "course", + RelID: nil, + CanBeEmbedded: course.IsAllowTasks, + } + finalScheduleEvents = append(finalScheduleEvents, scheduleEvent) + //3.转换为 Schedule 切片 for section := arrangement.StartSection; section <= arrangement.EndSection; section++ { schedule := model.Schedule{ - Type: "course", - Week: week, - DayOfWeek: arrangement.DayOfWeek, - Section: section, - Status: "normal", - UserID: userID, - CanBeEmbedded: course.IsAllowTasks, + Week: week, + DayOfWeek: arrangement.DayOfWeek, + Section: section, + Status: "normal", + UserID: userID, + EventID: 0, } - schedules = append(schedules, schedule) + finalSchedules = append(finalSchedules, schedule) + pos = append(pos, len(finalScheduleEvents)-1) } } } - finalSchedules = append(finalSchedules, schedules...) } - //3.调用 DAO 方法添加课程 - err := ss.dao.AddUserCourses(finalSchedules) - if err != nil { - return err - } - //4.返回结果 - return nil + //TODO 冲突处理、重复检测...预计0.2.0版本之前完成 + //4.事务:插入两个表要么都成功,要么都回滚 + return ss.dao.Transaction(func(txDAO *dao.CourseDAO) error { + ids, err := txDAO.AddUserCoursesIntoScheduleEvents(ctx, finalScheduleEvents) + if err != nil { + return err + } + // 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段 + for i := range finalSchedules { + finalSchedules[i].EventID = ids[pos[i]] + } + if err := txDAO.AddUserCoursesIntoSchedule(ctx, finalSchedules); err != nil { + return err + } + return nil + }) } diff --git a/backend/service/schedule.go b/backend/service/schedule.go new file mode 100644 index 0000000..6d43c33 --- /dev/null +++ b/backend/service/schedule.go @@ -0,0 +1 @@ +package service diff --git a/backend/service/task-class.go b/backend/service/task-class.go index 6884d4e..5dc46e7 100644 --- a/backend/service/task-class.go +++ b/backend/service/task-class.go @@ -8,6 +8,7 @@ import ( "github.com/LoveLosita/smartflow/backend/conv" "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/respond" "github.com/go-redis/redis/v8" ) @@ -15,12 +16,14 @@ type TaskClassService struct { // 这里可以添加数据库连接或其他依赖 taskClassRepo *dao.TaskClassDAO cacheRepo *dao.CacheDAO + scheduleRepo *dao.ScheduleDAO } -func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO) *TaskClassService { +func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO, scheduleRepo *dao.ScheduleDAO) *TaskClassService { return &TaskClassService{ taskClassRepo: taskClassRepo, cacheRepo: cacheRepo, + scheduleRepo: scheduleRepo, } } @@ -96,3 +99,104 @@ func (sv *TaskClassService) GetUserCompleteTaskClass(ctx context.Context, userID } return resp, nil } + +func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, req *model.UserInsertTaskClassItemToScheduleRequest, userID int, taskID int) error { + //1.先验证任务块归属 + taskClassID, err := sv.taskClassRepo.GetTaskClassIDByTaskItemID(ctx, taskID) //通过任务块ID获取所属任务类ID + if err != nil { + return err + } + ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID + if err != nil { + return err + } + if ownerID != userID { + return respond.TaskClassItemNotBelongToUser + } + //2.取出任务块信息 + taskItem, err := sv.taskClassRepo.GetTaskClassItemByID(ctx, taskID) //通过任务块ID获取任务块信息 + if err != nil { + return err + } + //更新TaskClassItem的embedded_time字段 + taskItem.EmbeddedTime = &model.TargetTime{ + DayOfWeek: req.DayOfWeek, + Week: req.Week, + SectionFrom: req.StartSection, + SectionTo: req.EndSection, + } + //3.判断是否嵌入课程 + if req.EmbedCourseEventID != 0 { + //先检查看课程是否存在、是否归属该用户以及是否已经被嵌入了其他任务块 + courseOwnerID, err := sv.scheduleRepo.GetCourseUserIDByID(ctx, req.EmbedCourseEventID) + if err != nil { + return err + } + if courseOwnerID != userID { + return respond.CourseNotBelongToUser + } + //再检查用户给的时间是否和课程的时间匹配(目前逻辑是给的区间必须完全匹配) + match, err := sv.scheduleRepo.IsCourseTimeMatch(ctx, req.EmbedCourseEventID, req.Week, req.DayOfWeek, req.StartSection, req.EndSection) + if err != nil { + return err + } + if !match { + return respond.CourseTimeNotMatch + } + //查询对应时段的课程是否已被其他任务块嵌入了(目前业务限制:一个课程只能被一个任务块嵌入,但是目前设计是支持多个任务块嵌入一节课的,只要放得下) + isEmbedded, err := sv.scheduleRepo.IsCourseEmbeddedByOtherTaskBlock(ctx, req.EmbedCourseEventID, req.StartSection, req.EndSection) + if err != nil { + return err + } + if isEmbedded { + return respond.CourseAlreadyEmbeddedByOtherTaskBlock + } + //嵌入课程,直接更新日程表对应时段的 embedded_task_id 字段 + err = sv.scheduleRepo.EmbedTaskIntoSchedule(req.StartSection, req.EndSection, req.DayOfWeek, req.Week, userID, taskID) + if err != nil { + return err + } + //更新任务块的 embedded_time 字段 + err = sv.taskClassRepo.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime) + if err != nil { + return err + } + return nil + } + //4.否则构造Schedule模型 + sections := make([]int, 0, req.EndSection-req.StartSection+1) + schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel(req, taskItem, taskID, userID, req.StartSection, req.EndSection) + //4.1 统一检查冲突(避免逐条查库) + conflict, err := sv.scheduleRepo.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections) + if err != nil { + return err + } + if conflict { + return respond.ScheduleConflict + } + //5.写入数据库(事务) + if err := sv.taskClassRepo.Transaction(func(txDAO *dao.TaskClassDAO) error { + //5.1 先将任务块插入scheduleEvent表,获取生成的ID后再插入schedule表(因为schedule表有外键关联) + id, err := sv.scheduleRepo.AddScheduleEvent(scheduleEvent) + if err != nil { + return err + } + //5.2 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段 + for i := range schedules { + schedules[i].EventID = id + } + //5.3 插入 Schedule 表 + _, err = sv.scheduleRepo.AddSchedules(schedules) + if err != nil { + return err + } + //5.4 更新任务块的 embedded_time 字段 + if err := txDAO.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime); err != nil { + return err + } + return nil + }); err != nil { + return err + } + return nil +}