feat: 🗓️ 新增任务块排期能力并完善课程与日程模型

Version: 0.1.1.dev.260207

- 新增并测试通过将任务块排进日程接口 
- 批量导入课程接口增加单双周功能,支持只在单双周上课的课程 📚
- 任务块时间定位逻辑调整为「第几周-周几」模式 🧭

refactor: 🔨 重构时间与日程数据结构

- 完成绝对日期与相对时间的转换逻辑 🔄
  - 后续可根据需求灵活决定时间的传入与输出类型
- 再次重构 schedule 表单结构
  - 拆分为 schedule_event(单)与 schedule(多)
  - 建立前者对后者的一对多关系 🧩

fix: 🐛 大幅调整表结构与业务逻辑,修复大量历史遗留 bug 🔥
This commit is contained in:
LoveLosita
2026-02-07 16:33:30 +08:00
parent 132b7095ac
commit f4bea0576c
17 changed files with 546 additions and 40 deletions

View File

@@ -41,33 +41,61 @@ func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImpor
return respond.WrongCourseInfo
}
}
//2.转换为 Schedule 切片
var finalSchedules []model.Schedule
var finalScheduleEvents []model.ScheduleEvent
var pos []int
for _, course := range req.Courses {
var schedules []model.Schedule
// 避免取 range 迭代变量字段地址导致指针复用问题
location := course.Location
for _, arrangement := range course.Arrangements {
weekType := arrangement.WeekType
for week := arrangement.StartWeek; week <= arrangement.EndWeek; week++ {
if weekType == "odd" && week%2 == 0 {
continue
}
if weekType == "even" && week%2 != 0 {
continue
}
//2.转换为 Schedule_event 切片
scheduleEvent := model.ScheduleEvent{
UserID: userID,
Name: course.CourseName,
Location: &location,
Type: "course",
RelID: nil,
CanBeEmbedded: course.IsAllowTasks,
}
finalScheduleEvents = append(finalScheduleEvents, scheduleEvent)
//3.转换为 Schedule 切片
for section := arrangement.StartSection; section <= arrangement.EndSection; section++ {
schedule := model.Schedule{
Type: "course",
Week: week,
DayOfWeek: arrangement.DayOfWeek,
Section: section,
Status: "normal",
UserID: userID,
CanBeEmbedded: course.IsAllowTasks,
Week: week,
DayOfWeek: arrangement.DayOfWeek,
Section: section,
Status: "normal",
UserID: userID,
EventID: 0,
}
schedules = append(schedules, schedule)
finalSchedules = append(finalSchedules, schedule)
pos = append(pos, len(finalScheduleEvents)-1)
}
}
}
finalSchedules = append(finalSchedules, schedules...)
}
//3.调用 DAO 方法添加课程
err := ss.dao.AddUserCourses(finalSchedules)
if err != nil {
return err
}
//4.返回结果
return nil
//TODO 冲突处理、重复检测...预计0.2.0版本之前完成
//4.事务:插入两个表要么都成功,要么都回滚
return ss.dao.Transaction(func(txDAO *dao.CourseDAO) error {
ids, err := txDAO.AddUserCoursesIntoScheduleEvents(ctx, finalScheduleEvents)
if err != nil {
return err
}
// 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段
for i := range finalSchedules {
finalSchedules[i].EventID = ids[pos[i]]
}
if err := txDAO.AddUserCoursesIntoSchedule(ctx, finalSchedules); err != nil {
return err
}
return nil
})
}

View File

@@ -0,0 +1 @@
package service

View File

@@ -8,6 +8,7 @@ import (
"github.com/LoveLosita/smartflow/backend/conv"
"github.com/LoveLosita/smartflow/backend/dao"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/respond"
"github.com/go-redis/redis/v8"
)
@@ -15,12 +16,14 @@ type TaskClassService struct {
// 这里可以添加数据库连接或其他依赖
taskClassRepo *dao.TaskClassDAO
cacheRepo *dao.CacheDAO
scheduleRepo *dao.ScheduleDAO
}
func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO) *TaskClassService {
func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO, scheduleRepo *dao.ScheduleDAO) *TaskClassService {
return &TaskClassService{
taskClassRepo: taskClassRepo,
cacheRepo: cacheRepo,
scheduleRepo: scheduleRepo,
}
}
@@ -96,3 +99,104 @@ func (sv *TaskClassService) GetUserCompleteTaskClass(ctx context.Context, userID
}
return resp, nil
}
func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, req *model.UserInsertTaskClassItemToScheduleRequest, userID int, taskID int) error {
//1.先验证任务块归属
taskClassID, err := sv.taskClassRepo.GetTaskClassIDByTaskItemID(ctx, taskID) //通过任务块ID获取所属任务类ID
if err != nil {
return err
}
ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID
if err != nil {
return err
}
if ownerID != userID {
return respond.TaskClassItemNotBelongToUser
}
//2.取出任务块信息
taskItem, err := sv.taskClassRepo.GetTaskClassItemByID(ctx, taskID) //通过任务块ID获取任务块信息
if err != nil {
return err
}
//更新TaskClassItem的embedded_time字段
taskItem.EmbeddedTime = &model.TargetTime{
DayOfWeek: req.DayOfWeek,
Week: req.Week,
SectionFrom: req.StartSection,
SectionTo: req.EndSection,
}
//3.判断是否嵌入课程
if req.EmbedCourseEventID != 0 {
//先检查看课程是否存在、是否归属该用户以及是否已经被嵌入了其他任务块
courseOwnerID, err := sv.scheduleRepo.GetCourseUserIDByID(ctx, req.EmbedCourseEventID)
if err != nil {
return err
}
if courseOwnerID != userID {
return respond.CourseNotBelongToUser
}
//再检查用户给的时间是否和课程的时间匹配(目前逻辑是给的区间必须完全匹配)
match, err := sv.scheduleRepo.IsCourseTimeMatch(ctx, req.EmbedCourseEventID, req.Week, req.DayOfWeek, req.StartSection, req.EndSection)
if err != nil {
return err
}
if !match {
return respond.CourseTimeNotMatch
}
//查询对应时段的课程是否已被其他任务块嵌入了(目前业务限制:一个课程只能被一个任务块嵌入,但是目前设计是支持多个任务块嵌入一节课的,只要放得下)
isEmbedded, err := sv.scheduleRepo.IsCourseEmbeddedByOtherTaskBlock(ctx, req.EmbedCourseEventID, req.StartSection, req.EndSection)
if err != nil {
return err
}
if isEmbedded {
return respond.CourseAlreadyEmbeddedByOtherTaskBlock
}
//嵌入课程,直接更新日程表对应时段的 embedded_task_id 字段
err = sv.scheduleRepo.EmbedTaskIntoSchedule(req.StartSection, req.EndSection, req.DayOfWeek, req.Week, userID, taskID)
if err != nil {
return err
}
//更新任务块的 embedded_time 字段
err = sv.taskClassRepo.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime)
if err != nil {
return err
}
return nil
}
//4.否则构造Schedule模型
sections := make([]int, 0, req.EndSection-req.StartSection+1)
schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel(req, taskItem, taskID, userID, req.StartSection, req.EndSection)
//4.1 统一检查冲突(避免逐条查库)
conflict, err := sv.scheduleRepo.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections)
if err != nil {
return err
}
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
}
return nil
}