Version: 0.2.6.dev.260211

feat: 🕒 为 schedule_events 表新增 start_time 与 end_time 字段

- 新增 start_time 与 end_time 两列
- 支持最近已完成任务列表接口
- 为后续获取当前正在进行的任务接口做准备 🚧

feat:  新增最近已完成任务列表接口并通过测试

- 完成接口实现与测试 🧪
- 当前 sv 层使用测试时间进行逻辑验证
- ⚠️ 生产环境需改回使用当前时间
This commit is contained in:
LoveLosita
2026-02-11 21:08:50 +08:00
parent e5a4114202
commit 6dd1f656dc
6 changed files with 100 additions and 88 deletions

View File

@@ -79,3 +79,30 @@ func (s *ScheduleAPI) DeleteScheduleEvent(c *gin.Context) {
//4.返回删除成功的响应给前端
c.JSON(http.StatusOK, respond.Ok)
}
func (s *ScheduleAPI) GetUserRecentCompletedSchedules(c *gin.Context) {
// 1. 从请求上下文中获取用户ID以及其他查询参数如 index 和 limit
userID := c.GetInt("user_id")
index := c.Query("index")
limit := c.Query("limit")
intIndex, err := strconv.Atoi(index)
if err != nil {
c.JSON(http.StatusBadRequest, respond.WrongParamType)
return
}
intLimit, err := strconv.Atoi(limit)
if err != nil {
c.JSON(http.StatusBadRequest, respond.WrongParamType)
return
}
//2.调用服务层方法获取用户最近完成的日程事件
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
defer cancel() // 记得释放资源
completedSchedules, err := s.scheduleService.GetUserRecentCompletedSchedules(ctx, userID, intIndex, intLimit)
if err != nil {
respond.DealWithError(c, err)
return
}
//3.返回数据给前端
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, completedSchedules))
}

View File

@@ -71,87 +71,6 @@ var sectionTimeMap = map[int][2]string{
11: {"20:50", "21:35"}, 12: {"21:45", "22:30"},
}
/*func SchedulesToUserTodaySchedule(schedules []model.Schedule) []model.UserTodaySchedule {
if len(schedules) == 0 {
return []model.UserTodaySchedule{}
}
// 1. 按周次和星期进行第一层分组 (map[week][day]schedules)
// 虽然是“今日日程”,但传入的切片可能包含多天,这样写兼容性更好
dayGroups := make(map[string][]model.Schedule)
for _, s := range schedules {
dayKey := fmt.Sprintf("%d-%d", s.Week, s.DayOfWeek)
dayGroups[dayKey] = append(dayGroups[dayKey], s)
}
var result []model.UserTodaySchedule
for _, daySchedules := range dayGroups {
todayDTO := model.UserTodaySchedule{
Week: daySchedules[0].Week,
DayOfWeek: daySchedules[0].DayOfWeek,
Events: []model.EventBrief{},
}
// 2. 在每一天内部,按 EventID 进行第二层聚合(把连续的节次拼成一个 Event
eventGroups := make(map[int][]model.Schedule)
for _, s := range daySchedules {
eventGroups[s.EventID] = append(eventGroups[s.EventID], s)
}
// 3. 遍历每个 Event 组,转化为 EventBrief
var tempEvents []model.EventBrief
for eventID, slots := range eventGroups {
// 对当前 Event 的所有原子槽位按节次排序
sort.Slice(slots, func(i, j int) bool {
return slots[i].Section < slots[j].Section
})
firstSlot := slots[0]
lastSlot := slots[len(slots)-1]
brief := model.EventBrief{
ID: eventID,
Name: firstSlot.Event.Name,
Location: *firstSlot.Event.Location,
Type: firstSlot.Event.Type,
StartTime: sectionTimeMap[firstSlot.Section][0], // 取第一节的开始时间
EndTime: sectionTimeMap[lastSlot.Section][1], // 取最后一节的结束时间
}
// 💡 4. 检查是否有嵌入任务 (Embedded Task)
// 逻辑:只要这个时间块中任何一个原子槽位带了 EmbeddedTaskID就提取它
// 注意:这里假设你在 DAO 层也 Preload 了 Task 信息,或者在此处仅填 ID
for _, slot := range slots {
if slot.EmbeddedTaskID != nil {
brief.EmbeddedTaskInfo = model.TaskBrief{
ID: *slot.EmbeddedTaskID,
Name: *slot.EmbeddedTask.Content,
Type: "task",
}
break // 找到一个即代表该块已占用
}
}
tempEvents = append(tempEvents, brief)
}
// 5. 对结果进行排序(按开始时间),并分配 Order
sort.Slice(tempEvents, func(i, j int) bool {
return tempEvents[i].StartTime < tempEvents[j].StartTime
})
for i := range tempEvents {
tempEvents[i].Order = i + 1
}
todayDTO.Events = tempEvents
result = append(result, todayDTO)
}
return result
}*/
func SchedulesToUserTodaySchedule(schedules []model.Schedule) []model.UserTodaySchedule {
if len(schedules) == 0 {
return []model.UserTodaySchedule{}
@@ -342,3 +261,20 @@ func SchedulesToUserWeeklySchedule(schedules []model.Schedule) []model.UserWeekS
}
return result
}
func SchedulesToRecentCompletedSchedules(schedules []model.Schedule) model.UserRecentCompletedScheduleResponse {
var result model.UserRecentCompletedScheduleResponse
for _, s := range schedules {
strTime := s.Event.EndTime.Format("2006-01-02 15:04:05")
temp := model.RecentCompletedEventBrief{
ID: s.ID,
Name: s.Event.Name,
Type: s.Event.Type,
// CompletedTime 需要从 ScheduleEvent 的 CompletedTime 字段获取
CompletedTime: strTime,
}
result.Events = append(result.Events, temp)
}
return result
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/respond"
@@ -444,3 +445,21 @@ func (d *ScheduleDAO) DeleteScheduleEventByTaskItemID(ctx context.Context, taskI
}
return nil
}
func (d *ScheduleDAO) GetUserRecentCompletedSchedules(ctx context.Context, nowTime time.Time, userID int, index, limit int) ([]model.Schedule, error) {
var schedules []model.Schedule
err := d.db.WithContext(ctx).
Preload("Event").
Preload("EmbeddedTask").
Joins("JOIN schedule_events ON schedule_events.id = schedules.event_id").
Where("schedules.user_id = ? AND schedule_events.type = ? AND schedule_events.end_time < ?",
userID, "task", nowTime).
Order("schedule_events.end_time DESC"). // 命中索引
Offset(index).
Limit(limit).
Find(&schedules).Error
if err != nil {
return nil, err
}
return schedules, nil
}

View File

@@ -1,13 +1,17 @@
package model
import "time"
type ScheduleEvent struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int `gorm:"column:user_id;index:idx_user_events;not null" json:"user_id"`
Name string `gorm:"column:name;type:varchar(255);not null;comment:课程或任务名称" json:"name"`
Location *string `gorm:"column:location;type:varchar(255);default:'';comment:地点 (教学楼/会议室)" json:"location"`
Type string `gorm:"column:type;type:enum('course','task');not null;comment:日程类型" json:"type"`
RelID *int `gorm:"column:rel_id;comment:关联原始数据ID (如教务系统的课程ID)" json:"rel_id"`
CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;default:0;comment:是否允许在此时段嵌入其他任务" json:"can_be_embedded"`
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int `gorm:"column:user_id;index:idx_user_events;not null" json:"user_id"`
Name string `gorm:"column:name;type:varchar(255);not null;comment:课程或任务名称" json:"name"`
Location *string `gorm:"column:location;type:varchar(255);default:'';comment:地点 (教学楼/会议室)" json:"location"`
Type string `gorm:"column:type;type:enum('course','task');not null;comment:日程类型" json:"type"`
RelID *int `gorm:"column:rel_id;comment:关联原始数据ID (如教务系统的课程ID)" json:"rel_id"`
CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;default:0;comment:是否允许在此时段嵌入其他任务" json:"can_be_embedded"`
StartTime time.Time `gorm:"column:start_time;type:time;comment:开始时间" json:"start_time"`
EndTime time.Time `gorm:"column:end_time;type:time;comment:结束时间" json:"end_time"`
}
type Schedule struct {
@@ -92,6 +96,17 @@ type UserDeleteScheduleEvent struct {
DeleteEmbeddedTask bool `json:"delete_embedded_task"`
}
type UserRecentCompletedScheduleResponse struct {
Events []RecentCompletedEventBrief `json:"events"`
}
type RecentCompletedEventBrief struct {
ID int `json:"id"` //如果是嵌入的任务事件这个ID是TaskClassItem的ID如果是课程事件这个ID是ScheduleEvent的ID
Name string `json:"name"`
Type string `json:"type"`
CompletedTime string `json:"completed_time"`
}
func (ScheduleEvent) TableName() string { return "schedule_events" }
func (Schedule) TableName() string { return "schedules" }

View File

@@ -78,6 +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)
}
}
// 初始化Gin引擎

View File

@@ -236,3 +236,17 @@ func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []m
}
return nil
}
func (ss *ScheduleService) GetUserRecentCompletedSchedules(ctx context.Context, userID, index, limit int) (model.UserRecentCompletedScheduleResponse, error) {
//1.查询用户最近完成的日程安排
//获取现在的时间
/*nowTime := time.Now()*/
nowTime := time.Date(2026, 6, 15, 12, 0, 0, 0, time.Local) //测试数据
schedules, err := ss.scheduleDAO.GetUserRecentCompletedSchedules(ctx, nowTime, userID, index, limit)
if err != nil {
return model.UserRecentCompletedScheduleResponse{}, err
}
//2.转换为前端需要的格式
result := conv.SchedulesToRecentCompletedSchedules(schedules)
return result, nil
}