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

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

View File

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