Version: 0.3.0.dev.260212
refactor: ♻️ 基于 gorm 钩子实现自动缓存失效机制,再也不用牵一发而动全身写删缓存逻辑了~ - 通过 gorm hook 监听 MySQL 数据变更 🧩 - 自动删除对应表相关缓存,实现缓存失效自动化 🔄 - 移除原本写在 sv 层的手动删缓存逻辑 🧹 - 解耦业务逻辑与缓存控制,结构更加清晰 ✅ fix: 🐛 修复将任务类加入日程接口的时间字段遗漏问题 - 由于前版本 MySQL 表结构更新 - 漏写插入起始时间字段逻辑,导致500报错,现已补充 ⏱️ fix: 🐛 修复获取最近已完成任务列表接口的多个问题 - 移除不应存在的幂等键 🔁 - 修复“一个event输出多次”的问题(原因出自 dto 转换函数) 🔧 undo: ⚠️ 删除任务类接口未处理已安排任务块的解除逻辑 - 当前删除任务类时,未解除已被安排的任务块 - 该逻辑存在缺陷,计划在后续版本内修复 🛠️
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
103
backend/middleware/cache_deleter.go
Normal file
103
backend/middleware/cache_deleter.go
Normal file
@@ -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)
|
||||
}()
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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引擎
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user