diff --git a/backend/cmd/start.go b/backend/cmd/start.go index 793cd00..f049e7c 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -7,6 +7,7 @@ import ( "github.com/LoveLosita/smartflow/backend/api" "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/inits" + "github.com/LoveLosita/smartflow/backend/middleware" "github.com/LoveLosita/smartflow/backend/pkg" "github.com/LoveLosita/smartflow/backend/routers" "github.com/LoveLosita/smartflow/backend/service" @@ -39,12 +40,17 @@ func Start() { if err != nil { log.Fatalf("Failed to connect to database: %v", err) } + rdb := inits.InitRedis() //工具包 limiter := pkg.NewRateLimiter(rdb) + //中间件 + //dao 层 - userRepo := dao.NewUserDAO(db) + cacheRepo := dao.NewCacheDAO(rdb) + _ = db.Use(middleware.NewGormCachePlugin(cacheRepo)) // 注册 GORM 插件 + userRepo := dao.NewUserDAO(db) taskRepo := dao.NewTaskDAO(db) courseRepo := dao.NewCourseDAO(db) taskClassRepo := dao.NewTaskClassDAO(db) diff --git a/backend/conv/schedule.go b/backend/conv/schedule.go index f9a4a5d..3632976 100644 --- a/backend/conv/schedule.go +++ b/backend/conv/schedule.go @@ -163,118 +163,136 @@ func SchedulesToUserTodaySchedule(schedules []model.Schedule) []model.UserTodayS return result } -func SchedulesToUserWeeklySchedule(schedules []model.Schedule) []model.UserWeekSchedule { - if len(schedules) == 0 { - return []model.UserWeekSchedule{} +func SchedulesToUserWeeklySchedule(schedules []model.Schedule) *model.UserWeekSchedule { + // 1. 初始化返回结构 (默认取第一条数据的周次) + weekDTO := &model.UserWeekSchedule{ + Week: schedules[0].Week, + Events: []model.WeeklyEventBrief{}, + } + + // 2. 建立 [天][节次] 的快速索引地图 + // indexMap[day][section] -> model.Schedule + indexMap := make(map[int]map[int]model.Schedule) + for d := 1; d <= 7; d++ { + indexMap[d] = make(map[int]model.Schedule) } - // 1. 数据预处理:按 Week 分组 - weekGroups := make(map[int][]model.Schedule) for _, s := range schedules { - weekGroups[s.Week] = append(weekGroups[s.Week], s) + indexMap[s.DayOfWeek][s.Section] = s } - var result []model.UserWeekSchedule - // 2. 遍历每一个周 - for week, weekSchedules := range weekGroups { - weekDTO := model.UserWeekSchedule{ - Week: week, - Events: []model.WeeklyEventBrief{}, - } - // 💡 核心优化:建立 [天][节次] 的快速索引地图 - // indexMap[day][section] -> model.Schedule - indexMap := make(map[int]map[int]model.Schedule) - for d := 1; d <= 7; d++ { - indexMap[d] = make(map[int]model.Schedule) - } - for _, s := range weekSchedules { - indexMap[s.DayOfWeek][s.Section] = s - } - // 3. 线性扫描 1-7 天 - for day := 1; day <= 7; day++ { - order := 1 // 每一天开始时,Order 重置 - // 4. 线性扫描 1-12 节 - for curr := 1; curr <= 12; { - // 检查当前槽位是否有课 - if slot, hasClass := indexMap[day][curr]; hasClass { - // === A 场景:有课,寻找连续边界 (Span 计算) === - end := curr - // 探测逻辑:只要 EventID 相同且在同一天,就视为同一个块 - for next := curr + 1; next <= 12; next++ { - if nextSlot, exist := indexMap[day][next]; exist && nextSlot.EventID == slot.EventID { - end = next - } else { - break - } + + // 3. 线性扫描 1-7 天 + for day := 1; day <= 7; day++ { + order := 1 // 每一天开始时,内部显示顺序重置 + + // 4. 线性扫描 1-12 节 + for curr := 1; curr <= 12; { + // 场景 A:当前槽位有课/有任务 + if slot, hasClass := indexMap[day][curr]; hasClass { + end := curr + // 探测逻辑:合并相同 EventID 的连续节次 (Span 计算) + for next := curr + 1; next <= 12; next++ { + if nextSlot, exist := indexMap[day][next]; exist && nextSlot.EventID == slot.EventID { + end = next + } else { + break } - span := end - curr + 1 - brief := model.WeeklyEventBrief{ - ID: slot.EventID, - Order: order, - DayOfWeek: day, - Name: slot.Event.Name, - Location: *slot.Event.Location, - Type: slot.Event.Type, - StartTime: sectionTimeMap[curr][0], - EndTime: sectionTimeMap[end][1], - Span: span, - } - // 提取嵌入任务信息 (如果有) - for i := curr; i <= end; i++ { - if s, exist := indexMap[day][i]; exist && s.EmbeddedTask != nil { - brief.EmbeddedTaskInfo = model.TaskBrief{ - ID: s.EmbeddedTask.ID, - Name: *s.EmbeddedTask.Content, - Type: "task", - } - break - } - } - weekDTO.Events = append(weekDTO.Events, brief) - curr = end + 1 // 指针跳跃 - order++ - } else { - // === B 场景:无课 (Type="empty") === - // 逻辑:默认按“大节”合并空位(1-2, 3-4...) - emptyEnd := curr - // 如果是奇数节且下一节也没课,则合并为一个 2 节的大空块 - if curr%2 != 0 && curr < 12 { - if _, nextHasClass := indexMap[day][curr+1]; !nextHasClass { - emptyEnd = curr + 1 - } - } - weekDTO.Events = append(weekDTO.Events, model.WeeklyEventBrief{ - ID: 0, - Order: order, - DayOfWeek: day, - Name: "无课", - Type: "empty", - StartTime: sectionTimeMap[curr][0], - EndTime: sectionTimeMap[emptyEnd][1], - Span: emptyEnd - curr + 1, - Location: "", - }) - curr = emptyEnd + 1 - order++ } + + span := end - curr + 1 + brief := model.WeeklyEventBrief{ + ID: slot.EventID, + Order: order, + DayOfWeek: day, + Name: slot.Event.Name, + Location: *slot.Event.Location, + Type: slot.Event.Type, + StartTime: sectionTimeMap[curr][0], // 使用你定义的映射表 + EndTime: sectionTimeMap[end][1], + Span: span, + } + + // 提取嵌入任务信息 (逻辑同前,探测整个 Span) + for i := curr; i <= end; i++ { + if s, exist := indexMap[day][i]; exist && s.EmbeddedTask != nil { + brief.EmbeddedTaskInfo = model.TaskBrief{ + ID: s.EmbeddedTask.ID, + Name: *s.EmbeddedTask.Content, + Type: "task", + } + break + } + } + + weekDTO.Events = append(weekDTO.Events, brief) + curr = end + 1 // 指针跳跃到下一块 + order++ + } else { + // 场景 B:无课 (Type="empty"),进行逻辑合并 + emptyEnd := curr + // 奇数节起步且下一节也空,则合并为大节 (1-2, 3-4...) + if curr%2 != 0 && curr < 12 { + if _, nextHasClass := indexMap[day][curr+1]; !nextHasClass { + emptyEnd = curr + 1 + } + } + + weekDTO.Events = append(weekDTO.Events, model.WeeklyEventBrief{ + ID: 0, + Order: order, + DayOfWeek: day, + Name: "无课", + Type: "empty", + StartTime: sectionTimeMap[curr][0], + EndTime: sectionTimeMap[emptyEnd][1], + Span: emptyEnd - curr + 1, + Location: "", + }) + curr = emptyEnd + 1 + order++ } } - result = append(result, weekDTO) } - return result + + return weekDTO } func SchedulesToRecentCompletedSchedules(schedules []model.Schedule) model.UserRecentCompletedScheduleResponse { - var result model.UserRecentCompletedScheduleResponse + // 1. 初始化结果集 + result := model.UserRecentCompletedScheduleResponse{ + Events: make([]model.RecentCompletedEventBrief, 0), + } + + if len(schedules) == 0 { + return result + } + + // 💡 核心:去重地图,key 是 EventID + seen := make(map[int]bool) + for _, s := range schedules { + // 2. 检查这个逻辑事件是否已经添加过 + if seen[s.EventID] { + continue + } + + // 3. 格式化结束时间 strTime := s.Event.EndTime.Format("2006-01-02 15:04:05") + + // 4. 构造 Brief temp := model.RecentCompletedEventBrief{ - ID: s.ID, - Name: s.Event.Name, - Type: s.Event.Type, - // CompletedTime 需要从 ScheduleEvent 的 CompletedTime 字段获取 + // 注意:这里 ID 必须改用 s.EventID (逻辑事件ID) + // 否则如果你传 s.ID,前端拿到的是原子槽位的ID,依然不唯一 + ID: s.EventID, + Name: s.Event.Name, + Type: s.Event.Type, CompletedTime: strTime, } - result.Events = append(result.Events, temp) - } - return result + result.Events = append(result.Events, temp) + + // 5. 标记该事件已处理 + seen[s.EventID] = true + } + + return result } diff --git a/backend/conv/time.go b/backend/conv/time.go index d6da20d..45d7bb3 100644 --- a/backend/conv/time.go +++ b/backend/conv/time.go @@ -2,6 +2,7 @@ package conv import ( "errors" + "fmt" "time" "github.com/LoveLosita/smartflow/backend/respond" @@ -55,3 +56,68 @@ func RelativeDateToRealDate(week, dayOfWeek int) (string, error) { } return targetDate.Format(DateFormat), nil } + +type SectionTime struct { + Start string // 第一个开始 + End string // 第一个结束 +} + +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"}, + 4: {Start: "11:10", End: "11:55"}, + 5: {Start: "14:00", End: "14:45"}, + 6: {Start: "14:55", End: "15:40"}, + 7: {Start: "16:15", End: "17:00"}, + 8: {Start: "17:10", End: "17:55"}, + 9: {Start: "19:00", End: "19:45"}, + 10: {Start: "19:55", End: "20:40"}, + 11: {Start: "20:50", End: "21:35"}, + 12: {Start: "21:45", End: "22:30"}, +} + +func RelativeTimeToRealTime(week, dayOfWeek, startSection, endSection int) (time.Time, time.Time, error) { + // 1. 安全校验 + if startSection > endSection { + return time.Time{}, time.Time{}, respond.InvalidSectionRange + } + + startTimeInfo, okStart := sectionTimeMap2[startSection] + endTimeInfo, okEnd := sectionTimeMap2[endSection] + if !okStart || !okEnd { + return time.Time{}, time.Time{}, respond.InvalidSectionNumber + } + + if week < 1 || dayOfWeek < 1 || dayOfWeek > 7 { + return time.Time{}, time.Time{}, respond.InvalidWeekOrDayOfWeek + } + + // 2. 计算目标日期 + // 偏移天数 = (周数-1)*7 + (周几-1) + daysOffset := (week-1)*7 + (dayOfWeek - 1) + TermStartDate := viper.GetString("time.semesterStartDate") // 从配置文件中读取学期开始日期 + baseDate, _ := time.Parse("2006-01-02", TermStartDate) + targetDate := baseDate.AddDate(0, 0, daysOffset) + dateStr := targetDate.Format("2006-01-02") + + // 3. 锁定时区 (Asia/Shanghai) + timeZone := viper.GetString("time.zone") // 从配置文件中读取时区 + loc, _ := time.LoadLocation(timeZone) + + // 拼接:起始节次的 Start 和 结束节次的 End + startFullStr := fmt.Sprintf("%s %s", dateStr, startTimeInfo.Start) + endFullStr := fmt.Sprintf("%s %s", dateStr, endTimeInfo.End) + + startTime, err := time.ParseInLocation("2006-01-02 15:04", startFullStr, loc) + if err != nil { + return time.Time{}, time.Time{}, err + } + + endTime, err := time.ParseInLocation("2006-01-02 15:04", endFullStr, loc) + if err != nil { + return time.Time{}, time.Time{}, err + } + + return startTime, endTime, nil +} diff --git a/backend/dao/cache.go b/backend/dao/cache.go index 5c0085b..cb968ad 100644 --- a/backend/dao/cache.go +++ b/backend/dao/cache.go @@ -47,18 +47,18 @@ func (d *CacheDAO) AddTaskClassList(ctx context.Context, userID int, list *model return d.client.Set(ctx, key, data, 30*time.Minute).Err() } -func (d *CacheDAO) GetTaskClassList(ctx context.Context, userID int) (model.UserGetTaskClassesResponse, error) { +func (d *CacheDAO) GetTaskClassList(ctx context.Context, userID int) (*model.UserGetTaskClassesResponse, error) { key := fmt.Sprintf("smartflow:task_classes:%d", userID) var resp model.UserGetTaskClassesResponse // 1. 从 Redis 获取字符串 val, err := d.client.Get(ctx, key).Result() if err != nil { // 注意:如果是 redis.Nil,交给 Service 层处理查库逻辑 - return resp, err + return &resp, err } // 2. 反序列化:将 JSON 还原回结构体 err = json.Unmarshal([]byte(val), &resp) - return resp, err + return &resp, err } func (d *CacheDAO) DeleteTaskClassList(ctx context.Context, userID int) error { @@ -136,3 +136,73 @@ func (d *CacheDAO) DeleteUserTodayScheduleFromCache(ctx context.Context, userID key := fmt.Sprintf("smartflow:today_schedule:%d", userID) return d.client.Del(ctx, key).Err() } + +func (d *CacheDAO) GetUserWeeklyScheduleFromCache(ctx context.Context, userID int, week int) (*model.UserWeekSchedule, error) { + key := fmt.Sprintf("smartflow:weekly_schedule:%d:%d", userID, week) + var schedules model.UserWeekSchedule + val, err := d.client.Get(ctx, key).Result() + if err != nil { + return nil, err // 注意:如果是 redis.Nil,交给 Service 层处理查库逻辑 + } + err = json.Unmarshal([]byte(val), &schedules) + return &schedules, err +} + +func (d *CacheDAO) SetUserWeeklyScheduleToCache(ctx context.Context, userID int, schedules *model.UserWeekSchedule) error { + key := fmt.Sprintf("smartflow:weekly_schedule:%d:%d", userID, schedules.Week) + data, err := json.Marshal(schedules) + if err != nil { + return err + } + // 设置过期时间为一天 + return d.client.Set(ctx, key, data, 24*time.Hour).Err() +} + +func (d *CacheDAO) DeleteUserWeeklyScheduleFromCache(ctx context.Context, userID int, week int) error { + key := fmt.Sprintf("smartflow:weekly_schedule:%d:%d", userID, week) + return d.client.Del(ctx, key).Err() +} + +func (d *CacheDAO) GetUserRecentCompletedSchedulesFromCache(ctx context.Context, userID, index, limit int) (model.UserRecentCompletedScheduleResponse, error) { + key := fmt.Sprintf("smartflow:recent_completed_schedules:%d:%d:%d", userID, index, limit) + var resp model.UserRecentCompletedScheduleResponse + val, err := d.client.Get(ctx, key).Result() + if err != nil { + return resp, err // 注意:如果是 redis.Nil,交给 Service 层处理查库逻辑 + } + err = json.Unmarshal([]byte(val), &resp) + return resp, err +} + +func (d *CacheDAO) SetUserRecentCompletedSchedulesToCache(ctx context.Context, userID, index, limit int, resp model.UserRecentCompletedScheduleResponse) error { + key := fmt.Sprintf("smartflow:recent_completed_schedules:%d:%d:%d", userID, index, limit) + data, err := json.Marshal(resp) + if err != nil { + return err + } + // 设置过期时间为30分钟 + return d.client.Set(ctx, key, data, 30*time.Minute).Err() +} + +func (d *CacheDAO) DeleteUserRecentCompletedSchedulesFromCache(ctx context.Context, userID int) error { + pattern := fmt.Sprintf("smartflow:recent_completed_schedules:%d:*", userID) + + var cursor uint64 + for { + keys, next, err := d.client.Scan(ctx, cursor, pattern, 500).Result() + if err != nil { + return err + } + if len(keys) > 0 { + // 用 UNLINK\(\) 异步删除,降低阻塞风险;如需强一致删除可改用 Del\(\) + if err := d.client.Unlink(ctx, keys...).Err(); err != nil { + return err + } + } + cursor = next + if cursor == 0 { + break + } + } + return nil +} diff --git a/backend/dao/schedule.go b/backend/dao/schedule.go index 18928fc..939ed91 100644 --- a/backend/dao/schedule.go +++ b/backend/dao/schedule.go @@ -463,3 +463,24 @@ func (d *ScheduleDAO) GetUserRecentCompletedSchedules(ctx context.Context, nowTi } return schedules, nil } + +func (d *ScheduleDAO) GetScheduleEventWeekByID(ctx context.Context, eventID int) (int, error) { + type row struct { + Week *int `gorm:"column:week"` + } + var r row + err := d.db.WithContext(ctx). + Table("schedules"). + Select("week"). + Where("event_id = ?", eventID). + Order("id ASC"). + Limit(1). + Scan(&r).Error + if err != nil { + return 0, err + } + if r.Week == nil { + return 0, respond.WrongScheduleEventID + } + return *r.Week, nil +} diff --git a/backend/middleware/cache_deleter.go b/backend/middleware/cache_deleter.go new file mode 100644 index 0000000..8a5270b --- /dev/null +++ b/backend/middleware/cache_deleter.go @@ -0,0 +1,103 @@ +package middleware + +import ( + "context" + "log" + "reflect" + + "github.com/LoveLosita/smartflow/backend/dao" + "github.com/LoveLosita/smartflow/backend/model" + "gorm.io/gorm" +) + +type GormCachePlugin struct { + cacheDAO *dao.CacheDAO +} + +func NewGormCachePlugin(dao *dao.CacheDAO) *GormCachePlugin { + return &GormCachePlugin{ + cacheDAO: dao, + } +} + +// Name 插件名称 +func (p *GormCachePlugin) Name() string { + return "GormCachePlugin" +} + +// Initialize 注册 GORM 钩子 +func (p *GormCachePlugin) Initialize(db *gorm.DB) error { + // 在增、删、改成功后,统一触发清理逻辑 + _ = db.Callback().Create().After("gorm:create").Register("clear_related_cache_after_create", p.afterWrite) + _ = db.Callback().Update().After("gorm:update").Register("clear_related_cache_after_update", p.afterWrite) + _ = db.Callback().Delete().After("gorm:delete").Register("clear_related_cache_after_delete", p.afterWrite) + return nil +} + +func (p *GormCachePlugin) afterWrite(db *gorm.DB) { + if db.Error != nil || db.Statement.Schema == nil { + return + } + + // 获取 Model 的真实值(剥掉所有指针) + val := reflect.Indirect(reflect.ValueOf(db.Statement.Model)) + + // 如果是切片,拿切片里元素的类型 + if val.Kind() == reflect.Slice { + if val.Len() > 0 { + p.dispatchCacheLogic(val.Index(0).Interface(), db) + } + } else { + p.dispatchCacheLogic(val.Interface(), db) + } +} + +// 根据不同的 Model 类型,调用不同的缓存失效逻辑 +func (p *GormCachePlugin) dispatchCacheLogic(modelObj interface{}, db *gorm.DB) { + switch m := modelObj.(type) { + case model.Schedule: + // 无论传的是 &s, s, 还是 &[]s,剥开后都是 model.Schedule + p.invalidScheduleCache(m.UserID, m.Week) + case model.TaskClass: + p.invalidTaskClassCache(*m.UserID) + case model.Task: + p.invalidTaskCache(m.UserID) + default: + // 只有真正没定义的模型才会到这里 + log.Printf("[GORM-Cache] No logic defined for model: %T", modelObj) + } +} + +func (p *GormCachePlugin) invalidScheduleCache(userID int, week int) { + if userID == 0 || week == 0 { + return + } + // 3. 异步执行,不阻塞主业务事务 + go func() { + // 这里调用你的 CacheDAO 删缓存 + _ = p.cacheDAO.DeleteUserWeeklyScheduleFromCache(context.Background(), userID, week) + _ = p.cacheDAO.DeleteUserTodayScheduleFromCache(context.Background(), userID) // 同时删当天的缓存,确保数据一致 + _ = p.cacheDAO.DeleteUserRecentCompletedSchedulesFromCache(context.Background(), userID) // 同时删最近完成的缓存,确保数据一致 + log.Printf("[GORM-Cache] Invalidated cache for user %d, week %d", userID, week) + }() +} + +func (p *GormCachePlugin) invalidTaskClassCache(userID int) { + if userID == 0 { + return + } + go func() { + _ = p.cacheDAO.DeleteTaskClassList(context.Background(), userID) + log.Printf("[GORM-Cache] Invalidated task class list cache for user %d", userID) + }() +} + +func (p *GormCachePlugin) invalidTaskCache(userID int) { + if userID == 0 { + return + } + go func() { + _ = p.cacheDAO.DeleteUserTasksFromCache(context.Background(), userID) + log.Printf("[GORM-Cache] Invalidated task list cache for user %d", userID) + }() +} diff --git a/backend/respond/respond.go b/backend/respond/respond.go index ff888a2..7c6b420 100644 --- a/backend/respond/respond.go +++ b/backend/respond/respond.go @@ -260,4 +260,19 @@ var ( //请求相关的响应 Status: "40040", Info: "wrong task class id", } + + InvalidSectionNumber = Response{ //无效的节次 + Status: "40041", + Info: "invalid section number", + } + + InvalidWeekOrDayOfWeek = Response{ //无效的周数或星期 + Status: "40042", + Info: "invalid week or day_of_week", + } + + InvalidSectionRange = Response{ //无效的节次范围 + Status: "40043", + Info: "invalid section range, start_section should be less than or equal to end_section", + } ) diff --git a/backend/routers/routers.go b/backend/routers/routers.go index a181f49..4c5845b 100644 --- a/backend/routers/routers.go +++ b/backend/routers/routers.go @@ -78,7 +78,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, limiter *pk scheduleGroup.GET("/today", handlers.ScheduleHandler.GetUserTodaySchedule) scheduleGroup.GET("/week", handlers.ScheduleHandler.GetUserWeeklySchedule) scheduleGroup.DELETE("/delete", middleware.IdempotencyMiddleware(cache), handlers.ScheduleHandler.DeleteScheduleEvent) - scheduleGroup.GET("/recent-completed", middleware.IdempotencyMiddleware(cache), handlers.ScheduleHandler.GetUserRecentCompletedSchedules) + scheduleGroup.GET("/recent-completed", handlers.ScheduleHandler.GetUserRecentCompletedSchedules) } } // 初始化Gin引擎 diff --git a/backend/service/schedule.go b/backend/service/schedule.go index 8c3d12c..e42e4c3 100644 --- a/backend/service/schedule.go +++ b/backend/service/schedule.go @@ -3,7 +3,6 @@ package service import ( "context" "errors" - "fmt" "time" "github.com/LoveLosita/smartflow/backend/conv" @@ -69,20 +68,22 @@ func (ss *ScheduleService) GetUserTodaySchedule(ctx context.Context, userID int) return todaySchedules, nil } -func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, week int) ([]model.UserWeekSchedule, error) { +func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, week int) (*model.UserWeekSchedule, error) { //1.先检查 week 参数是否合法 if week < 0 || week > 25 { return nil, respond.WeekOutOfRange } - //2.先检查用户id是否存在(考虑移除) - /*_, err := ss.userDAO.GetUserByID(userID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, respond.WrongUserID - } + //2.先看看缓存里有没有数据(如果有的话直接返回,没有的话继续查库) + cachedResp, err := ss.cacheDAO.GetUserWeeklyScheduleFromCache(ctx, userID, week) + if err == nil { + // 缓存命中,直接返回 + return cachedResp, nil + } + // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 + if !errors.Is(err, redis.Nil) { return nil, err - }*/ - //2.查询用户每周的日程安排 + } + //3.查询用户每周的日程安排 //如果没有传入 week 参数,则默认查询当前周的日程安排 if week == 0 { curTime := time.Now().Format("2006-01-02") @@ -97,8 +98,10 @@ func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, we return nil, err } //3.转换为前端需要的格式 - weeklySchedules := conv.SchedulesToUserWeeklySchedule(schedules) - return weeklySchedules, nil + weeklySchedule := conv.SchedulesToUserWeeklySchedule(schedules) + //4.将查询结果存入缓存,设置过期时间为一周(或者根据实际情况调整) + err = ss.cacheDAO.SetUserWeeklyScheduleToCache(ctx, userID, weeklySchedule) + return weeklySchedule, nil } func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []model.UserDeleteScheduleEvent, userID int) error { @@ -148,7 +151,6 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m return err } //下方开启事务,删除课程事件并创建新的任务事件 - //2.2.2.删除课程事件 txErr := txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) if txErr != nil { @@ -179,7 +181,6 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m // 5. 写入数据库(通过 RepoManager 统一管理事务) // 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例 // 5.1 使用事务中的 ScheduleRepo 插入 Event - // 💡 这里的 txM.Schedule 已经注入了事务句柄 eventID, txErr := txM.Schedule.AddScheduleEvent(scheduleEvent) if txErr != nil { return txErr // 触发回滚 @@ -189,12 +190,10 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m 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 // 触发回滚 } @@ -228,25 +227,34 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m if err != nil { return err } - //4.删除成功后,清除相关缓存(如果有的话),以保证数据一致性 - err = ss.cacheDAO.DeleteUserTodayScheduleFromCache(ctx, userID) - if err != nil { - // 缓存删除失败,记录日志但不影响正常返回数据 - fmt.Printf("Failed to delete user today schedule cache for userID %d: %v\n", userID, err) - } return nil } func (ss *ScheduleService) GetUserRecentCompletedSchedules(ctx context.Context, userID, index, limit int) (model.UserRecentCompletedScheduleResponse, error) { - //1.查询用户最近完成的日程安排 + //1.先查缓存 + cachedResp, err := ss.cacheDAO.GetUserRecentCompletedSchedulesFromCache(ctx, userID, index, limit) + if err == nil { + // 缓存命中,直接返回 + return cachedResp, nil + } + // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 + if !errors.Is(err, redis.Nil) { + return model.UserRecentCompletedScheduleResponse{}, err + } + //2.查询用户最近完成的日程安排 //获取现在的时间 /*nowTime := time.Now()*/ - nowTime := time.Date(2026, 6, 15, 12, 0, 0, 0, time.Local) //测试数据 + nowTime := time.Date(2026, 6, 30, 12, 0, 0, 0, time.Local) //测试数据 schedules, err := ss.scheduleDAO.GetUserRecentCompletedSchedules(ctx, nowTime, userID, index, limit) if err != nil { return model.UserRecentCompletedScheduleResponse{}, err } - //2.转换为前端需要的格式 + //3.转换为前端需要的格式 result := conv.SchedulesToRecentCompletedSchedules(schedules) + //4.将查询结果存入缓存,设置过期时间为30分钟(根据实际情况调整) + err = ss.cacheDAO.SetUserRecentCompletedSchedulesToCache(ctx, userID, index, limit, result) + if err != nil { + return model.UserRecentCompletedScheduleResponse{}, err + } return result, nil } diff --git a/backend/service/task-class.go b/backend/service/task-class.go index d26b67b..7bc38ce 100644 --- a/backend/service/task-class.go +++ b/backend/service/task-class.go @@ -58,11 +58,6 @@ func (sv *TaskClassService) AddOrUpdateTaskClass(ctx context.Context, req *model return err } - // 2) 事务提交成功后删除该用户缓存(如果有) - err := sv.cacheRepo.DeleteTaskClassList(ctx, userID) - if err != nil { - log.Printf("redis删除任务分类列表缓存失败: userID=%d err=%v", userID, err) - } return nil } @@ -71,7 +66,7 @@ func (sv *TaskClassService) GetUserTaskClassInfos(ctx context.Context, userID in list, err := sv.cacheRepo.GetTaskClassList(ctx, userID) if err == nil { //命中缓存 - return &list, nil + return list, nil } else if !errors.Is(err, redis.Nil) { //不是缓存未命中错误,说明redis可能炸了,照常放行 log.Println("redis获取任务分类列表失败:", err) } @@ -189,35 +184,18 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re 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 - }*/ // 5. 写入数据库(通过 RepoManager 统一管理事务) // 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例 if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { // 5.1 使用事务中的 ScheduleRepo 插入 Event // 💡 这里的 txM.Schedule 已经注入了事务句柄 + //此处要将req中的起始section以及第几周、星期几转换成绝对时间,存入scheduleEvent的StartTime和EndTime字段中,方便后续查询和冲突检查 + st, ed, err := conv.RelativeTimeToRealTime(req.Week, req.DayOfWeek, req.StartSection, req.EndSection) + if err != nil { + return err + } + scheduleEvent.StartTime = st + scheduleEvent.EndTime = ed eventID, err := txM.Schedule.AddScheduleEvent(scheduleEvent) if err != nil { return err // 触发回滚 @@ -242,11 +220,19 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re return err } //6.事务提交成功后,清除相关缓存(如果有的话),以保证数据一致性 - err = sv.cacheRepo.DeleteTaskClassList(ctx, userID) + /*err = sv.cacheRepo.DeleteTaskClassList(ctx, userID) if err != nil { // 缓存删除失败,记录日志但不影响正常返回数据 log.Printf("Failed to delete task class list cache for userID %d: %v", userID, err) } + err = sv.cacheRepo.DeleteUserTodayScheduleFromCache(ctx, userID) + if err != nil { + log.Printf("Failed to delete user today schedule cache for userID %d: %v", userID, err) + } + err = sv.cacheRepo.DeleteUserWeeklyScheduleFromCache(ctx, userID, req.Week) + if err != nil { + log.Printf("Failed to delete user weekly schedule cache for userID %d week %d: %v", userID, req.Week, err) + }*/ return nil } @@ -294,12 +280,6 @@ func (sv *TaskClassService) DeleteTaskClassItem(ctx context.Context, userID int, }); err != nil { return err } - //3.事务提交成功后,清除相关缓存(如果有的话),以保证数据一致性 - err = sv.cacheRepo.DeleteUserTodayScheduleFromCache(ctx, userID) - if err != nil { - // 缓存删除失败,记录日志但不影响正常返回数据 - log.Printf("Failed to delete task class list cache for userID %d: %v", userID, err) - } return nil } @@ -320,10 +300,5 @@ func (sv *TaskClassService) DeleteTaskClass(ctx context.Context, userID int, tas if err != nil { return err } - //3.事务提交成功后,清除相关缓存(如果有的话),以保证数据一致性 - err = sv.cacheRepo.DeleteTaskClassList(ctx, userID) - if err != nil { - log.Printf("Failed to delete task class list cache for userID %d: %v", userID, err) - } return nil } diff --git a/backend/service/task.go b/backend/service/task.go index d9f934d..599b41e 100644 --- a/backend/service/task.go +++ b/backend/service/task.go @@ -40,12 +40,6 @@ func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskReques } //4. 调用 conv 层进行响应转换 response := conv.ModelToUserAddTaskResponse(createdTask) - //5. 添加成功后,清除相关缓存(如果有的话),以保证数据一致性 - err = ts.cache.DeleteUserTasksFromCache(ctx, userID) - if err != nil { - // 缓存删除失败,记录日志但不影响正常返回数据 - log.Printf("Failed to delete user tasks cache for userID %d: %v", userID, err) - } return response, nil }