From fca4004024ad057655c54b3ce5d85112ef516931 Mon Sep 17 00:00:00 2001 From: LoveLosita <2810873701@qq.com> Date: Thu, 26 Feb 2026 20:07:55 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.3.9.dev.260226=20fix:=20?= =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD=E6=8E=92?= =?UTF-8?q?=E7=A8=8B=E6=8E=A5=E5=8F=A3=E4=BB=BB=E5=8A=A1=E5=9D=97=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=BC=BA=E5=A4=B1=E4=B8=8E=E6=95=B0=E6=8D=AE=E6=8F=92?= =?UTF-8?q?=E5=85=A5=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复智能排程接口返回的任务块信息缺失问题,确保任务数据完整返回 * 修复 `UserInsertTaskItemRequestToModel` DTO 函数未填入起始时间字段的问题,解决多个接口插入数据时出现 500 错误 * 错误源自上次更新“获取最近完成动态任务接口”时,未同步更改数据库字段对应逻辑 * 将智能排程接口的 `ctx` 超时恢复为 1 秒,优化接口响应性能 feat: 🎯 新增正式应用日程接口 * 新增正式应用日程接口,并完成功能测试,确保业务流程无异常 --- backend/api/schedule.go | 5 +- backend/api/task-class.go | 19 +++ backend/conv/schedule.go | 1 + backend/conv/task-class.go | 10 +- backend/conv/time.go | 6 +- backend/dao/schedule.go | 52 ++++++++ backend/dao/task-class.go | 67 +++++++++++ backend/model/task-class.go | 14 +++ backend/respond/respond.go | 9 ++ backend/routers/routers.go | 1 + backend/service/schedule.go | 5 +- backend/service/task-class.go | 218 +++++++++++++++++++++++++++++++++- 12 files changed, 397 insertions(+), 10 deletions(-) diff --git a/backend/api/schedule.go b/backend/api/schedule.go index d203ded..75e0f45 100644 --- a/backend/api/schedule.go +++ b/backend/api/schedule.go @@ -155,9 +155,8 @@ func (s *ScheduleAPI) SmartPlanning(c *gin.Context) { return } //3.调用服务层方法进行智能规划 - /*ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)*/ - ctx := context.Background() - /*defer cancel() // 记得释放资源*/ + ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second) + defer cancel() // 记得释放资源 res, err := s.scheduleService.SmartPlanning(ctx, userID, intTaskClassID) if err != nil { respond.DealWithError(c, err) diff --git a/backend/api/task-class.go b/backend/api/task-class.go index 74ab7a6..818eb53 100644 --- a/backend/api/task-class.go +++ b/backend/api/task-class.go @@ -171,3 +171,22 @@ func (api *TaskClassHandler) DeleteTaskClass(c *gin.Context) { } c.JSON(http.StatusOK, respond.Ok) } + +func (api *TaskClassHandler) UserInsertBatchTaskClassItemsIntoSchedule(c *gin.Context) { + var req model.UserInsertTaskClassItemToScheduleRequestBatch + err := c.ShouldBindJSON(&req) + if err != nil { + c.JSON(http.StatusBadRequest, respond.WrongParamType) + return + } + userID := c.GetInt("user_id") + // 创建一个带 1 秒超时的上下文 + ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second) + defer cancel() // 记得释放资源 + err = api.svc.BatchApplyPlans(ctx, req.TaskClassID, userID, &req) + if err != nil { + respond.DealWithError(c, err) + return + } + c.JSON(http.StatusOK, respond.Ok) +} diff --git a/backend/conv/schedule.go b/backend/conv/schedule.go index 0cadcc3..ccf002e 100644 --- a/backend/conv/schedule.go +++ b/backend/conv/schedule.go @@ -474,6 +474,7 @@ func buildBrief(slot slotInfo, day, start, end, span, order int) model.WeeklyEve brief.Name = *slot.plan.Content brief.Type = "task" brief.Status = "suggested" // 标记为建议状态 + brief.ID = slot.plan.ID // 虚日程的 ID 直接使用 TaskClassItem 的 ID,方便前端追踪和操作 } return brief diff --git a/backend/conv/task-class.go b/backend/conv/task-class.go index 7046e19..63476cd 100644 --- a/backend/conv/task-class.go +++ b/backend/conv/task-class.go @@ -138,7 +138,7 @@ func ProcessUserGetCompleteTaskClassRequest(taskClass *model.TaskClass) (*model. } // UserInsertTaskItemRequestToModel 用于将填入空闲时段日程的请求转换为 Schedule 模型 -func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID *int, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent) { +func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID *int, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent, error) { var schedules []model.Schedule for section := startSection; section <= endSection; section++ { req1 := &model.Schedule{ @@ -151,14 +151,20 @@ func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToSchedu } schedules = append(schedules, *req1) } + startTime, endTime, err := RelativeTimeToRealTime(req.Week, req.DayOfWeek, startSection, endSection) + if err != nil { + return nil, nil, err + } req2 := &model.ScheduleEvent{ UserID: userID, // 由调用方填充 Name: safeStr(item.Content), // 任务内容作为事件名称 Type: "task", RelID: &item.ID, // 关联到 TaskClassItem 的 ID CanBeEmbedded: false, // 任务事件允许嵌入其他任务(如果需要的话) + StartTime: startTime, + EndTime: endTime, } - return schedules, req2 + return schedules, req2, nil } // --- 🛡️ 辅助工具函数:保持代码清爽并防止 Panic --- diff --git a/backend/conv/time.go b/backend/conv/time.go index 76ed0e3..cd6713e 100644 --- a/backend/conv/time.go +++ b/backend/conv/time.go @@ -62,7 +62,7 @@ type SectionTime struct { End string // 第一个结束 } -var sectionTimeMap2 = map[int]SectionTime{ +var SectionTimeMap2 = map[int]SectionTime{ 1: {Start: "08:00", End: "08:45"}, 2: {Start: "08:55", End: "09:40"}, 3: {Start: "10:15", End: "11:00"}, @@ -83,8 +83,8 @@ func RelativeTimeToRealTime(week, dayOfWeek, startSection, endSection int) (time return time.Time{}, time.Time{}, respond.InvalidSectionRange } - startTimeInfo, okStart := sectionTimeMap2[startSection] - endTimeInfo, okEnd := sectionTimeMap2[endSection] + startTimeInfo, okStart := SectionTimeMap2[startSection] + endTimeInfo, okEnd := SectionTimeMap2[endSection] if !okStart || !okEnd { return time.Time{}, time.Time{}, respond.InvalidSectionNumber } diff --git a/backend/dao/schedule.go b/backend/dao/schedule.go index e75ac00..cdb07ec 100644 --- a/backend/dao/schedule.go +++ b/backend/dao/schedule.go @@ -573,3 +573,55 @@ func (d *ScheduleDAO) GetUserSchedulesByTimeRange(ctx context.Context, userID in } return schedules, nil } + +func (d *ScheduleDAO) BatchEmbedTaskIntoSchedule(ctx context.Context, eventIDs, taskItemIDs []int) error { + if len(eventIDs) == 0 { + return nil + } + if len(eventIDs) != len(taskItemIDs) { + return fmt.Errorf("eventIDs length != taskItemIDs length") + } + + db := d.db.WithContext(ctx) + + for i, eventID := range eventIDs { + taskItemID := taskItemIDs[i] + + // 1) 校验该 event 是否为 course + var typ string + if err := db. + Table("schedule_events"). + Select("type"). + Where("id = ?", eventID). + Scan(&typ).Error; err != nil { + return err + } + if typ != "course" { + continue + } + + // 2) 一 event 对多 schedules:批量写入 embedded_task_id + if err := db. + Table("schedules"). + Where("event_id = ?", eventID). + Update("embedded_task_id", taskItemID).Error; err != nil { + return err + } + } + + return nil +} + +func (d *ScheduleDAO) InsertScheduleEvents(ctx context.Context, events []model.ScheduleEvent) ([]int, error) { + if len(events) == 0 { + return nil, nil + } + if err := d.db.WithContext(ctx).Create(&events).Error; err != nil { + return nil, err + } + ids := make([]int, len(events)) + for i, e := range events { + ids[i] = e.ID + } + return ids, nil +} diff --git a/backend/dao/task-class.go b/backend/dao/task-class.go index e9040e1..9ba7d90 100644 --- a/backend/dao/task-class.go +++ b/backend/dao/task-class.go @@ -212,6 +212,21 @@ func (dao *TaskClassDAO) IfTaskClassItemArranged(ctx context.Context, taskID int return item.EmbeddedTime != nil, nil } +func (dao *TaskClassDAO) BatchCheckIfTaskClassItemsArranged(ctx context.Context, itemIDs []int) (bool, error) { + if len(itemIDs) == 0 { + return false, nil + } + var count int64 + err := dao.db.WithContext(ctx). + Model(&model.TaskClassItem{}). + Where("id IN ? AND embedded_time IS NOT NULL", itemIDs). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + func (dao *TaskClassDAO) DeleteTaskClassItemByID(ctx context.Context, id int) error { err := dao.db.WithContext(ctx). Where("id = ?", id). @@ -231,3 +246,55 @@ func (dao *TaskClassDAO) DeleteTaskClassByID(ctx context.Context, id int) error } return nil } + +func (dao *TaskClassDAO) BatchUpdateTaskClassItemEmbeddedTime(ctx context.Context, itemIDs []int, updates []*model.TargetTime) error { + if len(itemIDs) == 0 { + return nil + } + if len(itemIDs) != len(updates) { + return errors.New("itemIDs length mismatch updates length") + } + + // 单条 SQL 批量更新:UPDATE ... SET embedded_time = CASE id WHEN ? THEN ? ... END WHERE id IN (?) + caseSQL := "CASE id" + args := make([]any, 0, len(itemIDs)*2) + for i, id := range itemIDs { + caseSQL += " WHEN ? THEN ?" + args = append(args, id, updates[i]) + } + caseSQL += " END" + + res := dao.db.WithContext(ctx). + Model(&model.TaskClassItem{}). + Where("id IN ?", itemIDs). + Update("embedded_time", gorm.Expr(caseSQL, args...)) + + return res.Error +} + +func (dao *TaskClassDAO) ValidateTaskItemIDsBelongToTaskClass(ctx context.Context, taskClassID int, itemIDs []int) (bool, error) { + if len(itemIDs) == 0 { + return true, nil + } + + var count int64 + err := dao.db.WithContext(ctx). + Model(&model.TaskClassItem{}). + Where("id IN ? AND category_id = ?", itemIDs, taskClassID). + Count(&count).Error + if err != nil { + return false, err + } + return count == int64(len(itemIDs)), nil +} + +func (dao *TaskClassDAO) GetTaskClassItemsByIDs(ctx context.Context, itemIDs []int) ([]model.TaskClassItem, error) { + var items []model.TaskClassItem + err := dao.db.WithContext(ctx). + Where("id IN ?", itemIDs). + Find(&items).Error + if err != nil { + return nil, err + } + return items, nil +} diff --git a/backend/model/task-class.go b/backend/model/task-class.go index cd0d95e..ba01fc2 100644 --- a/backend/model/task-class.go +++ b/backend/model/task-class.go @@ -129,6 +129,20 @@ type UserInsertTaskClassItemToScheduleRequest struct { 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 { diff --git a/backend/respond/respond.go b/backend/respond/respond.go index dc8ea9a..a4b1f7e 100644 --- a/backend/respond/respond.go +++ b/backend/respond/respond.go @@ -304,4 +304,13 @@ var ( //请求相关的响应 Status: "40047", Info: "time not enough for auto scheduling", } + TaskClassItemNotBelongToTaskClass = Response{ //任务类项目不属于任务类 + Status: "40048", + Info: "task class item does not belong to task class", + } + + TaskClassItemTryingToInsertOutOfTimeRange = Response{ //任务类项目试图插入超出时间范围 + Status: "40049", + Info: "task class item trying to insert out of time range", + } ) diff --git a/backend/routers/routers.go b/backend/routers/routers.go index d6c6253..dfa49ac 100644 --- a/backend/routers/routers.go +++ b/backend/routers/routers.go @@ -71,6 +71,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, limiter *pk taskClassGroup.POST("/insert-into-schedule", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserAddTaskClassItemIntoSchedule) taskClassGroup.DELETE("/delete-item", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClassItem) taskClassGroup.DELETE("/delete-class", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClass) + taskClassGroup.PUT("/apply-batch-into-schedule", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserInsertBatchTaskClassItemsIntoSchedule) } scheduleGroup := apiGroup.Group("/schedule") { diff --git a/backend/service/schedule.go b/backend/service/schedule.go index bba4dbf..c96ed91 100644 --- a/backend/service/schedule.go +++ b/backend/service/schedule.go @@ -163,11 +163,14 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m //直接构造Schedule模型 sections := make([]int, 0, taskClassItem.EmbeddedTime.SectionTo-taskClassItem.EmbeddedTime.SectionFrom+1) // 这里的 req 主要是为了传递 Week 和 DayOfWeek,其他字段不需要了 - schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel( + schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel( &model.UserInsertTaskClassItemToScheduleRequest{ Week: taskClassItem.EmbeddedTime.Week, DayOfWeek: taskClassItem.EmbeddedTime.DayOfWeek}, taskClassItem, nil, userID, taskClassItem.EmbeddedTime.SectionFrom, taskClassItem.EmbeddedTime.SectionTo) + if err != nil { + return err + } //将节次区间转换为节次切片,方便后续检查冲突 for section := taskClassItem.EmbeddedTime.SectionFrom; section <= taskClassItem.EmbeddedTime.SectionTo; section++ { sections = append(sections, section) diff --git a/backend/service/task-class.go b/backend/service/task-class.go index 8cff78c..f3da581 100644 --- a/backend/service/task-class.go +++ b/backend/service/task-class.go @@ -3,7 +3,9 @@ package service import ( "context" "errors" + "fmt" "log" + "sort" "time" "github.com/LoveLosita/smartflow/backend/conv" @@ -192,7 +194,10 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re } //4.否则构造Schedule模型 sections := make([]int, 0, req.EndSection-req.StartSection+1) - schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel(req, taskItem, nil, userID, req.StartSection, req.EndSection) + schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel(req, taskItem, nil, userID, req.StartSection, req.EndSection) + if err != nil { + return err + } //将节次区间转换为节次切片,方便后续检查冲突 for section := req.StartSection; section <= req.EndSection; section++ { sections = append(sections, section) @@ -309,3 +314,214 @@ func (sv *TaskClassService) DeleteTaskClass(ctx context.Context, userID int, tas } return nil } + +func (sv *TaskClassService) BatchApplyPlans(ctx context.Context, taskClassID int, userID int, plans *model.UserInsertTaskClassItemToScheduleRequestBatch) error { + //1.通过任务类id获取任务类详情 + taskClass, err := sv.taskClassRepo.GetCompleteTaskClassByID(ctx, taskClassID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return respond.WrongTaskClassID + } + return err + } + //2.校验任务类的参数是否合法 + if taskClass == nil { + return respond.WrongTaskClassID + } + if *taskClass.Mode != "auto" { + return respond.TaskClassModeNotAuto + } + //3.获取任务类安排的时间范围内的全部周数信息(左右边界不足一周的情况也要算作一周),用于下方冲突检查 + startWeekTime := conv.CalculateFirstDayOfWeek(*taskClass.StartDate) + endWeekTime := conv.CalculateLastDayOfWeek(*taskClass.EndDate) + schedules, err := sv.scheduleRepo.GetUserSchedulesByTimeRange(ctx, userID, startWeekTime, endWeekTime) + if err != nil { + return err + } + startWeek, _, err := conv.RealDateToRelativeDate(startWeekTime.Format("2006-01-02")) + if err != nil { + return err + } + endWeek, _, err := conv.RealDateToRelativeDate(endWeekTime.Format("2006-01-02")) + if err != nil { + return err + } + //4.统一检查冲突(避免逐条查库) + //先将日程放入一个map中,key是"周-星期-节次",value是课程信息,方便后续检查冲突 + courseMap := make(map[string]model.Schedule) + for _, schedule := range schedules { + key := fmt.Sprintf("%d-%d-%d", schedule.Week, schedule.DayOfWeek, schedule.Section) + courseMap[key] = schedule + } + //再遍历每个任务块的安排时间,检查是否和课程冲突(目前逻辑是只要有一个时段冲突就算冲突,后续可以优化为统计冲突的时段数量,或者提供具体的冲突时段信息) + for _, plan := range plans.Items { + if plan.Week < startWeek || plan.Week > endWeek { + return respond.TaskClassItemTryingToInsertOutOfTimeRange + } + for section := plan.StartSection; section <= plan.EndSection; section++ { + key := fmt.Sprintf("%d-%d-%d", plan.Week, plan.DayOfWeek, section) + // 如果课程存在,并且满足以下任一条件则认为冲突: + // 1. 课程时段已经被其他任务块嵌入了(不允许多个任务块嵌入同一课程) + // 2. 当前时段的课的EventID与用户计划中指定的EmbedCourseEventID不匹配(说明用户计划要嵌入的课程和当前时段的课不是同一节) + // 3. 用户计划中没有指定EmbedCourseEventID(即EmbedCourseEventID为0),但当前时段有课(不允许在有课的时段安排任务块) + // 4. 当前时段的课不允许被嵌入(即使用户计划中指定了EmbedCourseEventID,但如果课程本身不允许被嵌入了,也算冲突) + if course, exists := courseMap[key]; exists && ((plan.EmbedCourseEventID != 0 && course.EmbeddedTask != nil) || + (plan.EmbedCourseEventID != course.EventID) || plan.EmbedCourseEventID == 0 || !course.Event.CanBeEmbedded) { + return respond.ScheduleConflict + } + } + } + //5.分流批量写入数据库(通过 RepoManager 统一管理事务) + //先分流 + toEmbed := make([]model.SingleTaskClassItem, 0) //需要嵌入课程的任务块 + toNormal := make([]model.SingleTaskClassItem, 0) //需要新建日程的任务块 + for _, item := range plans.Items { + if item.EmbedCourseEventID != 0 { + toEmbed = append(toEmbed, item) + } else { + toNormal = append(toNormal, item) + } + } + //再开事务批量写库 + if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { + //5.1 先处理需要嵌入课程的任务块 + //先提取出需要嵌入的课程ID和TaskItemID列表 + courseIDs := make([]int, 0, len(toEmbed)) + for _, item := range toEmbed { + courseIDs = append(courseIDs, item.EmbedCourseEventID) + } + itemIDs := make([]int, 0, len(toEmbed)) + for _, item := range toEmbed { + itemIDs = append(itemIDs, item.TaskItemID) + } + //检查任务块本身是否已经被安排 + result, err := sv.taskClassRepo.BatchCheckIfTaskClassItemsArranged(ctx, itemIDs) + if err != nil { + return err + } + if result { + return respond.TaskClassItemAlreadyArranged + } + //验证一下plans中的taskItemID确实都属于这个用户和这个任务类(避免用户恶意构造请求把别的用户的任务块或者不属于任何任务类的任务块也安排了) + //同时也能检查是否重复 + result, err = sv.taskClassRepo.ValidateTaskItemIDsBelongToTaskClass(ctx, taskClassID, itemIDs) + if err != nil { + return err + } + if !result { + return respond.TaskClassItemNotBelongToTaskClass + } + //批量更新日程表中对应课程的embedded_task_id字段(目前业务限制:一个课程只能被一个任务块嵌入了,所以直接批量更新,不用担心覆盖问题) + err = txM.Schedule.BatchEmbedTaskIntoSchedule(ctx, courseIDs, itemIDs) + if err != nil { + return err + } + //批量更新任务块的embedded_time字段 + targetTimes := make([]*model.TargetTime, 0, len(toEmbed)) + for _, item := range toEmbed { + targetTimes = append(targetTimes, &model.TargetTime{ + DayOfWeek: item.DayOfWeek, + Week: item.Week, + SectionFrom: item.StartSection, + SectionTo: item.EndSection, + }) + } + err = txM.TaskClass.BatchUpdateTaskClassItemEmbeddedTime(ctx, itemIDs, targetTimes) + if err != nil { + return err + } + //5.2 再处理需要新建日程的任务块 + //先提取出需要新建日程的任务块ID列表 + normalItemIDs := make([]int, 0, len(toNormal)) + for _, item := range toNormal { + normalItemIDs = append(normalItemIDs, item.TaskItemID) + } + //验证一下plans中的taskItemID确实都属于这个任务类(避免用户恶意构造请求把别的用户的任务块或者不属于任何任务类的任务块也安排了) + result, err = sv.taskClassRepo.ValidateTaskItemIDsBelongToTaskClass(ctx, taskClassID, normalItemIDs) + if err != nil { + return err + } + if !result { + return respond.TaskClassItemNotBelongToTaskClass + } + //批量提取TaskItems + taskItems, err := txM.TaskClass.GetTaskClassItemsByIDs(ctx, normalItemIDs) + if err != nil { + return err + } + if len(taskItems) != len(normalItemIDs) { + log.Printf("警告:批量提取任务块时,返回的任务块数量与请求中的任务块ID数量不匹配,可能存在数据问题。请求ID数量:%d,返回任务块数量:%d", len(normalItemIDs), len(taskItems)) + return respond.InternalError(errors.New("返回的任务块数量与请求中的任务块ID数量不匹配,可能存在数据问题")) + } + //将toNormal按照TaskItemID升序排序,将taskItems也按照ID升序排序,保证一一对应关系(上面已经检查过重复) + //如果请求中的任务块ID有重复,这里就无法保证一一对应关系了,后续可以考虑在请求层面加一个校验,拒绝包含重复任务块ID的请求 + sort.SliceStable(toNormal, func(i, j int) bool { + return toNormal[i].TaskItemID < toNormal[j].TaskItemID + }) + sort.SliceStable(taskItems, func(i, j int) bool { + return taskItems[i].ID < taskItems[j].ID + }) + //开始构建event和schedules + finalSchedules := make([]model.Schedule, 0) //最终要插入数据库的Schedule切片 + finalScheduleEvents := make([]model.ScheduleEvent, 0) //最终要插入数据库的ScheduleEvent切片 + pos := make([]int, 0) //记录每个任务块对应的Schedule在finalSchedules中的位置,方便后续批量插入数据库后回填EventID + for i := 0; i < len(toNormal); i++ { + item := toNormal[i] + taskItem := taskItems[i] + if item.StartSection < 1 || item.EndSection > 12 || item.StartSection > item.EndSection { + return respond.InvalidSectionRange + } + schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel(&model.UserInsertTaskClassItemToScheduleRequest{ + Week: item.Week, + DayOfWeek: item.DayOfWeek, + StartSection: item.StartSection, + EndSection: item.EndSection, + EmbedCourseEventID: 0, //不嵌入课程 + }, &taskItem, nil, userID, item.StartSection, item.EndSection) + if err != nil { + return err + } + finalScheduleEvents = append(finalScheduleEvents, *scheduleEvent) + for range schedules { + pos = append(pos, len(finalScheduleEvents)-1) + } + finalSchedules = append(finalSchedules, schedules...) + } + //最后批量插入数据库 + //先插入ScheduleEvent表,获取生成的EventID,再批量插入Schedule表,最后批量更新TaskClassItem的embedded_time字段 + ids, err := txM.Schedule.InsertScheduleEvents(ctx, finalScheduleEvents) + if err != nil { + return err + } + // 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段 + for i := range finalSchedules { + finalSchedules[i].EventID = ids[pos[i]] + } + if _, err = txM.Schedule.AddSchedules(finalSchedules); err != nil { + return err + } + //批量更新任务块的embedded_time字段 + targetTimes = make([]*model.TargetTime, 0, len(toEmbed)) + for _, item := range toNormal { + targetTimes = append(targetTimes, &model.TargetTime{ + DayOfWeek: item.DayOfWeek, + Week: item.Week, + SectionFrom: item.StartSection, + SectionTo: item.EndSection, + }) + } + //提取出所有需要更新的任务块ID + itemIDs = make([]int, 0, len(toNormal)) + for _, item := range toNormal { + itemIDs = append(itemIDs, item.TaskItemID) + } + err = txM.TaskClass.BatchUpdateTaskClassItemEmbeddedTime(ctx, itemIDs, targetTimes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + return nil +}