Version: 0.3.0.dev.260212

refactor: ♻️ 基于 gorm 钩子实现自动缓存失效机制,再也不用牵一发而动全身写删缓存逻辑了~

- 通过 gorm hook 监听 MySQL 数据变更 🧩
- 自动删除对应表相关缓存,实现缓存失效自动化 🔄
- 移除原本写在 sv 层的手动删缓存逻辑 🧹
- 解耦业务逻辑与缓存控制,结构更加清晰 

fix: 🐛 修复将任务类加入日程接口的时间字段遗漏问题

- 由于前版本 MySQL 表结构更新
- 漏写插入起始时间字段逻辑,导致500报错,现已补充 ⏱️

fix: 🐛 修复获取最近已完成任务列表接口的多个问题

- 移除不应存在的幂等键 🔁
- 修复“一个event输出多次”的问题(原因出自 dto 转换函数) 🔧

undo: ⚠️ 删除任务类接口未处理已安排任务块的解除逻辑

- 当前删除任务类时,未解除已被安排的任务块
- 该逻辑存在缺陷,计划在后续版本内修复 🛠️
This commit is contained in:
LoveLosita
2026-02-12 21:34:01 +08:00
parent 6dd1f656dc
commit 63500b3b2a
11 changed files with 451 additions and 175 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}