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