From d07234e183a6a7018410715ce90f88b64d339e9f Mon Sep 17 00:00:00 2001 From: LoveLosita <2810873701@qq.com> Date: Tue, 10 Feb 2026 19:51:05 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.2.0.dev.260210=20feat:=20?= =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20=E6=96=B0=E5=A2=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=8D=95=E4=B8=AA=E8=AF=BE=E7=A8=8B=E4=B8=8E=E8=A7=A3=E9=99=A4?= =?UTF-8?q?=E5=AE=89=E6=8E=92=E6=97=A5=E7=A8=8B=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 逻辑复杂,初版接口写完后才发现需求需要传切片 - 针对需求修改,通过大 for 循环和事务处理来解决问题 🔄 refactor: 🔧 移除部分冗余的用户 ID 验证逻辑 - sv/schedule.go 中,进来的 ID 已通过 redis 黑名单与 JWT 保护验证 - 去除重复的数据库查验,优化了代码流程 🛠️ refactor: 🔄 重构 API 层业务错误判断逻辑 - 抛弃了原有的手动比对方式,封装进 `respond` 包,简化判断流程 - 未来不再手动遍历数据链路,提升了开发效率 🧹 undo: ⚠️ 修复任务块添加到日程的接口问题(待修复) - 接口允许直接修改已经安排的任务时间,且重复执行时未被禁止 - 此逻辑存在问题,计划在下个版本修复 🔧 undo: ⚠️ 重测接口的幂等性与其他特性 - 当前接口幂等性等特性尚未专门测试,后续计划重测所有接口 - 测试不充分,待进一步完善 🔄 undo: ⚠️ 修复刷新 token 接口错误处理问题 - 当前接口将 token 本身的错误以 500 错误返回,需修复此问题 🛠️ --- backend/api/course.go | 9 +- backend/api/schedule.go | 43 ++++++---- backend/api/task-class.go | 32 ++------ backend/api/task.go | 20 +---- backend/api/user.go | 34 ++------ backend/auth/jwt_handler.go | 2 +- backend/cmd/start.go | 2 +- backend/dao/schedule.go | 96 ++++++++++++++++++++++ backend/dao/task-class.go | 8 ++ backend/model/schedule.go | 6 ++ backend/respond/respond.go | 30 ++++++- backend/routers/routers.go | 3 +- backend/service/schedule.go | 158 +++++++++++++++++++++++++++++++++--- 13 files changed, 334 insertions(+), 109 deletions(-) diff --git a/backend/api/course.go b/backend/api/course.go index 67bb6b8..7666e07 100644 --- a/backend/api/course.go +++ b/backend/api/course.go @@ -58,15 +58,10 @@ func (sa *CourseHandler) AddUserCourses(c *gin.Context) { defer cancel() // 记得释放资源 conflicts, err := sa.service.AddUserCourses(ctx, req, userIDInterface) if err != nil { - switch { - case errors.Is(err, respond.WrongParamType), errors.Is(err, respond.WrongCourseInfo), - errors.Is(err, respond.InsertCourseTwice): - c.JSON(http.StatusBadRequest, err) - case errors.Is(err, respond.ScheduleConflict): + if errors.Is(err, respond.ScheduleConflict) { c.JSON(http.StatusConflict, respond.RespWithData(respond.ScheduleConflict, conflicts)) - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) } + respond.DealWithError(c, err) return } //4.返回成功响应 diff --git a/backend/api/schedule.go b/backend/api/schedule.go index 320effe..0666a40 100644 --- a/backend/api/schedule.go +++ b/backend/api/schedule.go @@ -2,11 +2,11 @@ package api import ( "context" - "errors" "net/http" "strconv" "time" + "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/service" "github.com/gin-gonic/gin" @@ -31,14 +31,8 @@ func (s *ScheduleAPI) GetUserTodaySchedule(c *gin.Context) { defer cancel() // 记得释放资源 todaySchedules, err := s.scheduleService.GetUserTodaySchedule(ctx, userID) if err != nil { - switch { - case errors.Is(err, respond.WrongUserID): - c.JSON(http.StatusBadRequest, respond.WrongUserID) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - return - } + respond.DealWithError(c, err) + return } //3.返回日程安排数据给前端 c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, todaySchedules)) @@ -58,15 +52,30 @@ func (s *ScheduleAPI) GetUserWeeklySchedule(c *gin.Context) { defer cancel() // 记得释放资源 weeklySchedules, err := s.scheduleService.GetUserWeeklySchedule(ctx, userID, week) if err != nil { - switch { - case errors.Is(err, respond.WrongUserID), errors.Is(err, respond.WeekOutOfRange): - c.JSON(http.StatusBadRequest, err) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - return - } + respond.DealWithError(c, err) + return } //4.返回日程安排数据给前端 c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, weeklySchedules)) } + +func (s *ScheduleAPI) DeleteScheduleEvent(c *gin.Context) { + // 1. 从请求上下文中获取用户ID + userID := c.GetInt("user_id") + // 2. 从请求体中获取要删除的日程事件信息 + var req []model.UserDeleteScheduleEvent + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, respond.WrongParamType) + return + } + //3.调用服务层方法删除指定的日程事件 + ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second) + defer cancel() // 记得释放资源 + err := s.scheduleService.DeleteScheduleEvent(ctx, req, userID) + if err != nil { + respond.DealWithError(c, err) + return + } + //4.返回删除成功的响应给前端 + c.JSON(http.StatusOK, respond.Ok) +} diff --git a/backend/api/task-class.go b/backend/api/task-class.go index 2a2ce66..01f4f8d 100644 --- a/backend/api/task-class.go +++ b/backend/api/task-class.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "net/http" "strconv" "time" @@ -11,7 +10,6 @@ import ( "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/service" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) type TaskClassHandler struct { @@ -43,11 +41,8 @@ func (api *TaskClassHandler) UserAddTaskClass(c *gin.Context) { defer cancel() // 记得释放资源 err = api.svc.AddOrUpdateTaskClass(ctx, &req, userIDInterface, create, 0) if err != nil { - if errors.Is(err, respond.WrongParamType) { - c.JSON(http.StatusBadRequest, respond.WrongParamType) - return - } - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + respond.DealWithError(c, err) + return } c.JSON(http.StatusOK, respond.Ok) } @@ -59,7 +54,7 @@ func (api *TaskClassHandler) UserGetTaskClassInfos(c *gin.Context) { defer cancel() // 记得释放资源 resp, err := api.svc.GetUserTaskClassInfos(ctx, userIDInterface) if err != nil { - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + respond.DealWithError(c, err) return } c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) @@ -83,11 +78,7 @@ func (api *TaskClassHandler) UserGetCompleteTaskClass(c *gin.Context) { defer cancel() // 记得释放资源 resp, err := api.svc.GetUserCompleteTaskClass(ctx, userIDInterface, intTaskClassID) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - c.JSON(http.StatusNotFound, respond.UserTaskClassNotFound) - return - } - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + respond.DealWithError(c, err) return } c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) @@ -109,11 +100,8 @@ func (api *TaskClassHandler) UserUpdateTaskClass(c *gin.Context) { defer cancel() // 记得释放资源 err = api.svc.AddOrUpdateTaskClass(ctx, &req, userIDInterface, update, intTaskClassID) if err != nil { - if errors.Is(err, respond.WrongParamType) || errors.Is(err, respond.UserTaskClassForbidden) { - c.JSON(http.StatusBadRequest, err) - return - } - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + respond.DealWithError(c, err) + return } c.JSON(http.StatusOK, respond.Ok) } @@ -138,13 +126,7 @@ func (api *TaskClassHandler) UserAddTaskClassItemIntoSchedule(c *gin.Context) { 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)) + respond.DealWithError(c, err) return } c.JSON(http.StatusOK, respond.Ok) diff --git a/backend/api/task.go b/backend/api/task.go index 717cc0d..dd80e8d 100644 --- a/backend/api/task.go +++ b/backend/api/task.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "fmt" "net/http" "time" @@ -41,13 +40,8 @@ func (th *TaskHandler) AddTask(c *gin.Context) { defer cancel() // 记得释放资源 resp, err := th.svc.AddTask(ctx, &req, userID) if err != nil { - switch { - case errors.Is(err, respond.InvalidPriority): //如果是无效刷新令牌或者无效claims或者无效签名方法 - c.JSON(http.StatusBadRequest, err) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - } + respond.DealWithError(c, err) + return } //3. 返回响应 c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) @@ -62,14 +56,8 @@ func (th *TaskHandler) GetUserTasks(c *gin.Context) { defer cancel() // 记得释放资源 resp, err := th.svc.GetUserTasks(ctx, userID) if err != nil { - switch { - case errors.Is(err, respond.UserTasksEmpty): //如果任务列表为空 - c.JSON(http.StatusOK, respond.RespWithData(respond.UserTasksEmpty, []model.Task{})) //确实没错误,但是任务列表为空,返回自定义响应 - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - return - } + respond.DealWithError(c, err) + return } //3. 返回响应 c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) diff --git a/backend/api/user.go b/backend/api/user.go index 24cad80..2880a04 100644 --- a/backend/api/user.go +++ b/backend/api/user.go @@ -4,7 +4,6 @@ package api import ( "context" - "errors" "net/http" "time" @@ -40,15 +39,8 @@ func (api *UserHandler) UserRegister(c *gin.Context) { defer cancel() // 记得释放资源 retUser, err := api.svc.UserRegister(ctx, user) if err != nil { - switch { - case errors.Is(err, respond.InvalidName), errors.Is(err, respond.MissingParam), - errors.Is(err, respond.ParamTooLong): //如果是无效ID或者缺少参数的错误 - c.JSON(http.StatusBadRequest, err) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - return - } + respond.DealWithError(c, err) + return } c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, retUser)) @@ -66,14 +58,8 @@ func (api *UserHandler) UserLogin(c *gin.Context) { defer cancel() // 记得释放资源 tokens, err := api.svc.UserLogin(ctx, &req) if err != nil { - switch { - case errors.Is(err, respond.WrongName), errors.Is(err, respond.WrongPwd): //如果是无效ID或者缺少参数的错误 - c.JSON(http.StatusBadRequest, err) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - return - } + respond.DealWithError(c, err) + return } c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, tokens)) } @@ -94,14 +80,8 @@ func (api *UserHandler) RefreshTokenHandler(c *gin.Context) { defer cancel() // 记得释放资源 tokens, err := api.svc.RefreshTokenHandler(ctx, requestBody.RefreshToken) if err != nil { - switch { - case errors.Is(err, respond.InvalidRefreshToken), errors.Is(err, respond.InvalidClaims), - errors.Is(err, respond.InvalidTokenSingingMethod), errors.Is(err, respond.UserLoggedOut): //如果是无效刷新令牌或者无效claims或者无效签名方法 - c.JSON(http.StatusBadRequest, err) - return - default: - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) - } + respond.DealWithError(c, err) + return } c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, tokens)) } @@ -116,7 +96,7 @@ func (api *UserHandler) UserLogout(c *gin.Context) { defer cancel() // 记得释放资源 err := api.svc.UserLogout(ctx, cl.Jti, cl.ExpiresAt.Time) if err != nil { - c.JSON(http.StatusInternalServerError, respond.InternalError(err)) + respond.DealWithError(c, err) return } c.JSON(http.StatusOK, respond.Ok) diff --git a/backend/auth/jwt_handler.go b/backend/auth/jwt_handler.go index f74535f..0f73b87 100644 --- a/backend/auth/jwt_handler.go +++ b/backend/auth/jwt_handler.go @@ -111,7 +111,7 @@ func ValidateRefreshToken(tokenString string, cache *dao.CacheDAO) (*jwt.Token, isBlack, err := cache.IsBlacklisted(claims.Jti) if err != nil { // Redis 出错时的处理逻辑,建议报错以防“漏网之鱼” - return nil, respond.InternalError(errors.New("无法验证令牌状态")) + return nil, errors.New("无法验证令牌状态") } if isBlack { return nil, respond.UserLoggedOut // 返回你定义的“用户已登出”错误 diff --git a/backend/cmd/start.go b/backend/cmd/start.go index 90e790d..a5ce38e 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -52,7 +52,7 @@ func Start() { taskSv := service.NewTaskService(taskRepo) courseService := service.NewCourseService(courseRepo, scheduleRepo) taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo, manager) - scheduleService := service.NewScheduleService(scheduleRepo, userRepo) + scheduleService := service.NewScheduleService(scheduleRepo, userRepo, taskClassRepo, manager) //api 层 userApi := api.NewUserHandler(userService) taskApi := api.NewTaskHandler(taskSv) diff --git a/backend/dao/schedule.go b/backend/dao/schedule.go index 0f07a67..3a6232c 100644 --- a/backend/dao/schedule.go +++ b/backend/dao/schedule.go @@ -301,3 +301,99 @@ func (d *ScheduleDAO) GetUserWeeklySchedule(ctx context.Context, userID, week in return schedules, nil } + +func (d *ScheduleDAO) DeleteScheduleEventAndSchedule(ctx context.Context, eventID int, userID int) error { + //级联删除:先删 schedules,自动删 schedule_events + res := d.db.WithContext(ctx).Where("id=? AND user_id=?", eventID, userID).Delete(&model.ScheduleEvent{}) + if res.Error != nil { + return res.Error + } + if res.RowsAffected == 0 { + return respond.WrongScheduleEventID // 事件不存在或不属于该用户,统一返回错误 + } + return nil +} + +func (d *ScheduleDAO) GetScheduleTypeByEventID(ctx context.Context, eventID, userID int) (string, error) { + type row struct { + Type *string `gorm:"column:type"` + } + var r row + err := d.db.WithContext(ctx). + Table("schedule_events"). + Select("type"). + Where("id = ? AND user_id=?", eventID, userID). + First(&r).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", respond.WrongScheduleEventID // 事件不存在或不属于该用户,统一返回错误 + } + return "", err + } + if r.Type == nil { + return "", respond.WrongScheduleEventID + } + return *r.Type, nil +} + +func (d *ScheduleDAO) GetScheduleEmbeddedTaskID(ctx context.Context, eventID int) (int, error) { + // embedded_task_id 存在于 schedules 表中(按 event_id 聚合取一个非空值) + // 若该事件没有任何嵌入任务,则返回 0, nil + type row struct { + EmbeddedTaskID *int `gorm:"column:embedded_task_id"` + } + + var r row + err := d.db.WithContext(ctx). + Table("schedules"). + Select("embedded_task_id"). + Where("event_id = ?", eventID). + Where("embedded_task_id IS NOT NULL AND embedded_task_id <> 0"). + Order("id ASC"). + Limit(1). + Scan(&r).Error + if err != nil { + return 0, err + } + if r.EmbeddedTaskID == nil { // 没有任何嵌入任务 + return 0, nil + } + return *r.EmbeddedTaskID, nil +} + +func (d *ScheduleDAO) IfScheduleEventIDExists(ctx context.Context, eventID int) (bool, error) { + var count int64 + err := d.db.WithContext(ctx). + Table("schedule_events"). + Where("id = ?", eventID). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +func (d *ScheduleDAO) SetScheduleEmbeddedTaskIDToNull(ctx context.Context, eventID int) (int, error) { + // 先取出该事件当前嵌入的任务 id(若没有嵌入则返回对应业务错误) + embeddedTaskID, err := d.GetScheduleEmbeddedTaskID(ctx, eventID) + if err != nil { + return 0, err + } + if embeddedTaskID == 0 { + return 0, respond.TargetScheduleNotHaveEmbeddedTask + } + + // 将 schedules 表中指定 event_id 的 embedded_task_id 字段置空(用于解除嵌入关系) + res := d.db.WithContext(ctx). + Table("schedules"). + Where("event_id = ?", eventID). + Where("embedded_task_id IS NOT NULL AND embedded_task_id <> 0"). + Update("embedded_task_id", nil) + if res.Error != nil { + return 0, res.Error + } + if res.RowsAffected == 0 { + return 0, respond.TargetScheduleNotHaveEmbeddedTask + } + return embeddedTaskID, nil +} diff --git a/backend/dao/task-class.go b/backend/dao/task-class.go index 9b0bad5..c4f65eb 100644 --- a/backend/dao/task-class.go +++ b/backend/dao/task-class.go @@ -187,3 +187,11 @@ func (dao *TaskClassDAO) UpdateTaskClassItemEmbeddedTime(ctx context.Context, ta Update("embedded_time", embeddedTime).Error return err } + +func (dao *TaskClassDAO) DeleteTaskClassItemEmbeddedTime(ctx context.Context, taskID int) error { + err := dao.db.WithContext(ctx). + Model(&model.TaskClassItem{}). + Where("id = ?", taskID). + Update("embedded_time", nil).Error + return err +} diff --git a/backend/model/schedule.go b/backend/model/schedule.go index f61de80..618fcda 100644 --- a/backend/model/schedule.go +++ b/backend/model/schedule.go @@ -86,6 +86,12 @@ type WeeklyEventBrief struct { EmbeddedTaskInfo TaskBrief `json:"embedded_task_info,omitempty"` } +type UserDeleteScheduleEvent struct { + ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID,不是 Schedule 的 ID + DeleteCourse bool `json:"delete_course"` + DeleteEmbeddedTask bool `json:"delete_embedded_task"` +} + func (ScheduleEvent) TableName() string { return "schedule_events" } func (Schedule) TableName() string { return "schedules" } diff --git a/backend/respond/respond.go b/backend/respond/respond.go index bff1f31..18c1539 100644 --- a/backend/respond/respond.go +++ b/backend/respond/respond.go @@ -2,6 +2,13 @@ // 统一API响应格式和处理逻辑 package respond +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" +) + type Response struct { //响应结构体 Status string `json:"status"` Info string `json:"info"` @@ -13,7 +20,6 @@ type FinalResponse struct { //最终响应结构体 Data interface{} `json:"data"` } -// 实现error接口 func (r Response) Error() string { // 实现 error 接口 return r.Info } @@ -26,6 +32,18 @@ func RespWithData(response Response, data interface{}) FinalResponse { //传入 return finalResponse } +func DealWithError(c *gin.Context, err error) { //处理错误,返回对应的响应结构体 + if err == nil { + return + } + var resp Response + if errors.As(err, &resp) { + c.JSON(http.StatusBadRequest, resp) + return + } + c.JSON(http.StatusInternalServerError, InternalError(err)) +} + func InternalError(err error) Response { //服务器错误 return Response{ Status: "500", @@ -193,4 +211,14 @@ var ( //请求相关的响应 Status: "40030", Info: "week out of range", } + + WrongScheduleEventID = Response{ //日程ID错误 + Status: "40031", + Info: "wrong schedule_event id", + } + + TargetScheduleNotHaveEmbeddedTask = Response{ //目标日程没有嵌入任务 + Status: "40032", + Info: "target schedule does not have embedded task", + } ) diff --git a/backend/routers/routers.go b/backend/routers/routers.go index 5534052..c1c3c9e 100644 --- a/backend/routers/routers.go +++ b/backend/routers/routers.go @@ -37,7 +37,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO) *gin.Engine apiGroup.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{ "status": "ok", - "version": "1.0.0", + "version": "0.2.0.dev.260210", }) }) @@ -74,6 +74,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO) *gin.Engine scheduleGroup.Use(middleware.JWTTokenAuth(cache)) scheduleGroup.GET("/today", handlers.ScheduleHandler.GetUserTodaySchedule) scheduleGroup.GET("/week", handlers.ScheduleHandler.GetUserWeeklySchedule) + scheduleGroup.DELETE("/delete", handlers.ScheduleHandler.DeleteScheduleEvent) } } // 初始化Gin引擎 diff --git a/backend/service/schedule.go b/backend/service/schedule.go index 8bfc4a9..b4bee3e 100644 --- a/backend/service/schedule.go +++ b/backend/service/schedule.go @@ -2,7 +2,6 @@ package service import ( "context" - "errors" "fmt" "time" @@ -10,30 +9,33 @@ import ( "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/respond" - "gorm.io/gorm" ) type ScheduleService struct { - scheduleDAO *dao.ScheduleDAO - userDAO *dao.UserDAO + scheduleDAO *dao.ScheduleDAO + userDAO *dao.UserDAO + taskClassDAO *dao.TaskClassDAO + repoManager *dao.RepoManager // 统一管理多个 DAO 的事务 } -func NewScheduleService(scheduleDAO *dao.ScheduleDAO, userDAO *dao.UserDAO) *ScheduleService { +func NewScheduleService(scheduleDAO *dao.ScheduleDAO, userDAO *dao.UserDAO, taskClassDAO *dao.TaskClassDAO, repoManager *dao.RepoManager) *ScheduleService { return &ScheduleService{ - scheduleDAO: scheduleDAO, - userDAO: userDAO, + scheduleDAO: scheduleDAO, + userDAO: userDAO, + taskClassDAO: taskClassDAO, + repoManager: repoManager, } } func (ss *ScheduleService) GetUserTodaySchedule(ctx context.Context, userID int) ([]model.UserTodaySchedule, error) { - //1.先检查用户id是否存在 - _, err := ss.userDAO.GetUserByID(userID) + //1.先检查用户id是否存在(考虑移除) + /*_, err := ss.userDAO.GetUserByID(userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, respond.WrongUserID } return nil, err - } + }*/ //2.获取当前日期 curTime := time.Now().Format("2006-01-02") /*curTime := "2026-03-02" //测试数据*/ @@ -57,14 +59,14 @@ func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, we if week < 0 || week > 25 { return nil, respond.WeekOutOfRange } - //2.先检查用户id是否存在 - _, err := ss.userDAO.GetUserByID(userID) + //2.先检查用户id是否存在(考虑移除) + /*_, err := ss.userDAO.GetUserByID(userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, respond.WrongUserID } return nil, err - } + }*/ //2.查询用户每周的日程安排 //如果没有传入 week 参数,则默认查询当前周的日程安排 if week == 0 { @@ -83,3 +85,133 @@ func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, we weeklySchedules := conv.SchedulesToUserWeeklySchedule(schedules) return weeklySchedules, nil } + +func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []model.UserDeleteScheduleEvent, userID int) error { + err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { + for _, req := range requests { + //1.如果要删课程和嵌入的事件 + if req.DeleteEmbeddedTask && req.DeleteCourse { + //通过schedule表的embedded_task_id字段找到对应的task_id + taskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID) + if err != nil { + return err + } + //再将task_items表中对应的embedded_time字段设置为null + if taskID != 0 { + err = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) + if err != nil { + return err + } + } + //再删除课程事件和嵌入的事件(通过级联删除实现) + err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) + if err != nil { + return err + } + continue + } + //2.只删课程事件 + if req.DeleteCourse { + //2.1.检查课程是否有嵌入的任务事件 + exists, err := txM.Schedule.IfScheduleEventIDExists(ctx, req.ID) + if err != nil { + return err + } + if !exists { + return respond.WrongScheduleEventID + } + embeddedTaskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID) + if err != nil { + return err + } + //2.2.如果有,则需另外为其创建新的scheduleEvent(type=task) + //课程事件先删除后再创建任务事件 + if embeddedTaskID != 0 { + //2.2.1.先通过id取出taskClassItem详情 + taskClassItem, err := txM.TaskClass.GetTaskClassItemByID(ctx, embeddedTaskID) + if err != nil { + return err + } + //下方开启事务,删除课程事件并创建新的任务事件 + + //2.2.2.删除课程事件 + txErr := txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) + if txErr != nil { + return txErr + } + //2.2.3.再复用代码创建新的scheduleEvent,下方代码改编自AddTaskClassItemIntoSchedule函数 + //直接构造Schedule模型 + sections := make([]int, 0, taskClassItem.EmbeddedTime.SectionTo-taskClassItem.EmbeddedTime.SectionFrom+1) + // 这里的 req 主要是为了传递 Week 和 DayOfWeek,其他字段不需要了 + schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel( + &model.UserInsertTaskClassItemToScheduleRequest{ + Week: taskClassItem.EmbeddedTime.Week, + DayOfWeek: taskClassItem.EmbeddedTime.DayOfWeek}, + taskClassItem, nil, userID, taskClassItem.EmbeddedTime.SectionFrom, taskClassItem.EmbeddedTime.SectionTo) + //将节次区间转换为节次切片,方便后续检查冲突 + for section := taskClassItem.EmbeddedTime.SectionFrom; section <= taskClassItem.EmbeddedTime.SectionTo; section++ { + sections = append(sections, section) + } + //单用户不存在删除时这个格子被占用的情况,所以不检查冲突了 + /*//4.1 统一检查冲突(避免逐条查库) + conflict, err := ss.scheduleDAO.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections) + if err != nil { + return err + } + if conflict { + return respond.ScheduleConflict + }*/ + // 5. 写入数据库(通过 RepoManager 统一管理事务) + // 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例 + // 5.1 使用事务中的 ScheduleRepo 插入 Event + // 💡 这里的 txM.Schedule 已经注入了事务句柄 + eventID, txErr := txM.Schedule.AddScheduleEvent(scheduleEvent) + if txErr != nil { + return txErr // 触发回滚 + } + // 5.2 关联 ID(纯内存操作,无需 tx) + for i := range schedules { + schedules[i].EventID = eventID + } + // 5.3 使用事务中的 ScheduleRepo 批量插入原子槽位 + // 💡 如果这里因为外键或唯一索引报错,5.1 的 Event 也会被撤回 + if _, txErr = txM.Schedule.AddSchedules(schedules); txErr != nil { + return txErr // 触发回滚 + } + // 5.4 使用事务中的 TaskRepo 更新任务状态 + // 💡 这里的 txM.Task 取代了你原来的 txDAO + if txErr = txM.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, embeddedTaskID, taskClassItem.EmbeddedTime); txErr != nil { + return txErr // 触发回滚 + } + continue + } + //2.3.如果没有嵌入的事件,就直接删除课程事件 + err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) + if err != nil { + return err + } + continue + } + //3.只删嵌入的事件 + if req.DeleteEmbeddedTask { + //下面先设置schedule表的embedded_task_id字段为null,再设置task_items表的embedded_time字段为null,实现删除嵌入事件的效果 + //3.1.先将schedule表的embedded_task_id字段设置为null + taskID, txErr := txM.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, req.ID) + if txErr != nil { + return txErr + } + //3.2.再将task_items表的embedded_time字段设置为null + txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) + if txErr != nil { + return txErr + } + continue + } + } + return nil + }) + if err != nil { + return err + } + return nil +}