feat: 🗓️ 新增任务块排期能力并完善课程与日程模型
Version: 0.1.1.dev.260207 - 新增并测试通过将任务块排进日程接口 ✅ - 批量导入课程接口增加单双周功能,支持只在单双周上课的课程 📚 - 任务块时间定位逻辑调整为「第几周-周几」模式 🧭 refactor: 🔨 重构时间与日程数据结构 - 完成绝对日期与相对时间的转换逻辑 🔄 - 后续可根据需求灵活决定时间的传入与输出类型 - 再次重构 schedule 表单结构 - 拆分为 schedule_event(单)与 schedule(多) - 建立前者对后者的一对多关系 🧩 fix: 🐛 大幅调整表结构与业务逻辑,修复大量历史遗留 bug 🔥
This commit is contained in:
@@ -52,7 +52,7 @@ func (sa *CourseHandler) AddUserCourses(c *gin.Context) {
|
||||
}
|
||||
//2.从上下文获取用户ID
|
||||
userIDInterface := c.GetInt("user_id")
|
||||
//3.调用 service 层的 AddUserCourses 方法添加课程
|
||||
//3.调用 service 层的 AddUserCoursesIntoSchedule 方法添加课程
|
||||
// 创建一个带 1 秒超时的上下文
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel() // 记得释放资源
|
||||
|
||||
@@ -117,3 +117,35 @@ func (api *TaskClassHandler) UserUpdateTaskClass(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, respond.Ok)
|
||||
}
|
||||
|
||||
func (api *TaskClassHandler) UserAddTaskClassItemIntoSchedule(c *gin.Context) {
|
||||
var req model.UserInsertTaskClassItemToScheduleRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||
return
|
||||
}
|
||||
taskID := c.Query("task_item_id")
|
||||
//将taskID转换为int
|
||||
intTaskID, err := strconv.Atoi(taskID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||
return
|
||||
}
|
||||
userIDInterface := c.GetInt("user_id")
|
||||
// 创建一个带 1 秒超时的上下文
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel() // 记得释放资源
|
||||
err = api.svc.AddTaskClassItemIntoSchedule(ctx, &req, userIDInterface, intTaskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, respond.TaskClassItemNotBelongToUser) || errors.Is(err, respond.CourseNotBelongToUser) ||
|
||||
errors.Is(err, respond.CourseAlreadyEmbeddedByOtherTaskBlock) || errors.Is(err, respond.CourseTimeNotMatch) ||
|
||||
errors.Is(err, respond.ScheduleConflict) || errors.Is(err, respond.WrongCourseID) {
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, respond.InternalError(err))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, respond.Ok)
|
||||
}
|
||||
|
||||
@@ -45,11 +45,12 @@ func Start() {
|
||||
taskRepo := dao.NewTaskDAO(db)
|
||||
courseRepo := dao.NewCourseDAO(db)
|
||||
taskClassRepo := dao.NewTaskClassDAO(db)
|
||||
scheduleRepo := dao.NewScheduleDAO(db)
|
||||
//service 层
|
||||
userService := service.NewUserService(userRepo, cacheRepo)
|
||||
taskSv := service.NewTaskService(taskRepo)
|
||||
courseService := service.NewCourseService(courseRepo)
|
||||
taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo)
|
||||
taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo)
|
||||
//api 层
|
||||
userApi := api.NewUserHandler(userService)
|
||||
taskApi := api.NewTaskHandler(taskSv)
|
||||
|
||||
@@ -31,3 +31,9 @@ redis:
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
time:
|
||||
zone: "Asia/Shanghai"
|
||||
semesterStartDate: "2024-09-09" #学期开始日期,一定要设定为周一,以便于计算周数
|
||||
semesterEndDate: "2025-01-20" #学期结束日期,一定要设定为周日,确保最后一周完整
|
||||
|
||||
|
||||
@@ -136,6 +136,30 @@ func ProcessUserGetCompleteTaskClassRequest(taskClass *model.TaskClass) (*model.
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// UserInsertTaskItemRequestToModel 用于将填入空闲时段日程的请求转换为 Schedule 模型
|
||||
func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent) {
|
||||
var schedules []model.Schedule
|
||||
for section := startSection; section <= endSection; section++ {
|
||||
req1 := &model.Schedule{
|
||||
UserID: userID, // 由调用方填充
|
||||
EmbeddedTaskID: &taskID,
|
||||
Week: req.Week,
|
||||
DayOfWeek: req.DayOfWeek,
|
||||
Section: section,
|
||||
Status: "normal",
|
||||
}
|
||||
schedules = append(schedules, *req1)
|
||||
}
|
||||
req2 := &model.ScheduleEvent{
|
||||
UserID: userID, // 由调用方填充
|
||||
Name: safeStr(item.Content), // 任务内容作为事件名称
|
||||
Type: "task",
|
||||
RelID: &item.ID, // 关联到 TaskClassItem 的 ID
|
||||
CanBeEmbedded: false, // 任务事件允许嵌入其他任务(如果需要的话)
|
||||
}
|
||||
return schedules, req2
|
||||
}
|
||||
|
||||
// --- 🛡️ 辅助工具函数:保持代码清爽并防止 Panic ---
|
||||
|
||||
func safeStr(s *string) string {
|
||||
|
||||
52
backend/conv/time.go
Normal file
52
backend/conv/time.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package conv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// DateFormat 此处定义基准学期的开始和结束日期
|
||||
const DateFormat = "2006-01-02"
|
||||
|
||||
var (
|
||||
SemesterStartDate = viper.GetString("semesterStartDate") // 从配置文件中读取学期开始日期
|
||||
SemesterEndDate = viper.GetString("semesterEndDate") // 从配置文件中读取学期结束日期
|
||||
)
|
||||
|
||||
// RealDateToRelativeDate 将绝对日期转换为相对日期(格式: "week-day")
|
||||
func RealDateToRelativeDate(realDate string) (int, int, error) {
|
||||
t, err := time.Parse(DateFormat, realDate)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
start, _ := time.Parse(DateFormat, SemesterStartDate)
|
||||
end, _ := time.Parse(DateFormat, SemesterEndDate)
|
||||
// 边界校验:日期必须在学期范围内
|
||||
if t.Before(start) || t.After(end) {
|
||||
return 0, 0, errors.New("日期超出学期范围")
|
||||
}
|
||||
// 计算天数差值(注意:24小时为一个基准天)
|
||||
days := int(t.Sub(start).Hours() / 24)
|
||||
// 计算周数和星期
|
||||
// 假设 SemesterStartDate 对应第 1 周,周 1
|
||||
week := (days / 7) + 1
|
||||
dayOfWeek := (days % 7) + 1
|
||||
return week, dayOfWeek, nil
|
||||
}
|
||||
|
||||
// RelativeDateToRealDate 将相对日期转换为绝对日期(输入格式: "week-day")
|
||||
func RelativeDateToRealDate(week, dayOfWeek int) (string, error) {
|
||||
start, _ := time.Parse(DateFormat, SemesterStartDate)
|
||||
// 核心转换逻辑:(周-1)*7 + (天-1)
|
||||
offsetDays := (week-1)*7 + (dayOfWeek - 1)
|
||||
targetDate := start.AddDate(0, 0, offsetDays)
|
||||
// 校验计算出的日期是否超出学期结束日期
|
||||
end, _ := time.Parse(DateFormat, SemesterEndDate)
|
||||
if targetDate.After(end) {
|
||||
return "", respond.TimeOutOfRangeOfThisSemester
|
||||
}
|
||||
return targetDate.Format(DateFormat), nil
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -16,9 +18,29 @@ func NewCourseDAO(db *gorm.DB) *CourseDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (dao *CourseDAO) AddUserCourses(courses []model.Schedule) error {
|
||||
if err := dao.db.Create(&courses).Error; err != nil {
|
||||
func (dao *CourseDAO) AddUserCoursesIntoSchedule(ctx context.Context, courses []model.Schedule) error {
|
||||
if err := dao.db.WithContext(ctx).Create(&courses).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dao *CourseDAO) AddUserCoursesIntoScheduleEvents(ctx context.Context, events []model.ScheduleEvent) ([]int, error) {
|
||||
if err := dao.db.WithContext(ctx).Create(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int, 0, len(events))
|
||||
for i := range events {
|
||||
ids = append(ids, int(events[i].ID))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Transaction 在同一个数据库事务中执行传入的函数,供 service 层复用(自动提交/回滚)
|
||||
// 规则:fn 返回 nil \-\> 提交;fn 返回 error 或发生 panic \-\> 回滚
|
||||
// 说明:gorm\.\(\\\*DB\)\.Transaction 会在 fn 返回 error 时回滚,并在发生 panic 时自动回滚后继续向上抛出 panic
|
||||
func (dao *CourseDAO) Transaction(fn func(txDAO *CourseDAO) error) error {
|
||||
return dao.db.Transaction(func(tx *gorm.DB) error {
|
||||
return fn(NewCourseDAO(tx))
|
||||
})
|
||||
}
|
||||
|
||||
135
backend/dao/schedule.go
Normal file
135
backend/dao/schedule.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ScheduleDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewScheduleDAO 创建TaskClassDAO实例
|
||||
func NewScheduleDAO(db *gorm.DB) *ScheduleDAO {
|
||||
return &ScheduleDAO{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) AddSchedules(schedules []model.Schedule) ([]int, error) {
|
||||
if err := dao.db.Create(&schedules).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int, len(schedules))
|
||||
for i, s := range schedules {
|
||||
ids[i] = s.ID
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) EmbedTaskIntoSchedule(startSection, endSection, dayOfWeek, week, userID, taskID int) error {
|
||||
// 仅更新指定:用户/周/星期/节次区间 的记录,将 embedded_task_id 精准写入 taskID
|
||||
res := dao.db.
|
||||
Table("schedules").
|
||||
Where("user_id = ? AND week = ? AND day_of_week = ? AND section BETWEEN ? AND ?", userID, week, dayOfWeek, startSection, endSection).
|
||||
Update("embedded_task_id", taskID)
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) GetCourseUserIDByID(ctx context.Context, courseScheduleEventID int) (int, error) {
|
||||
type row struct {
|
||||
UserID *int `gorm:"column:user_id"`
|
||||
}
|
||||
|
||||
var r row
|
||||
err := dao.db.WithContext(ctx).
|
||||
Table("schedule_events").
|
||||
Select("user_id").
|
||||
Where("id = ?", courseScheduleEventID).
|
||||
First(&r).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, respond.WrongCourseID
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if r.UserID == nil {
|
||||
return 0, respond.WrongCourseID
|
||||
}
|
||||
return *r.UserID, nil
|
||||
}
|
||||
|
||||
// IsCourseEmbeddedByOtherTaskBlock 判断课程在给定节次区间内是否已被其他任务块嵌入(用于业务限制)
|
||||
func (dao *ScheduleDAO) IsCourseEmbeddedByOtherTaskBlock(ctx context.Context, courseID, startSection, endSection int) (bool, error) {
|
||||
// 若区间非法,视为不冲突
|
||||
if startSection <= 0 || endSection <= 0 || startSection > endSection {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var cnt int64
|
||||
err := dao.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("id = ?", courseID).
|
||||
Where("section BETWEEN ? AND ?", startSection, endSection).
|
||||
Where("embedded_task_id IS NOT NULL AND embedded_task_id <> 0").
|
||||
Count(&cnt).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cnt > 0, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, week, dayOfWeek int, sections []int) (bool, error) {
|
||||
// 无节次则视为无冲突
|
||||
if len(sections) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
// 统计同一用户、同一周、同一天、且节次有交集的排程数量
|
||||
// 约定表字段:user_id, week, day_of_week, section
|
||||
var cnt int64
|
||||
err := dao.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("user_id = ? AND week = ? AND day_of_week = ?", userID, week, dayOfWeek).
|
||||
Where("section IN ?", sections).
|
||||
Count(&cnt).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cnt > 0, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEventID, week, dayOfWeek, startSection, endSection int) (bool, error) {
|
||||
// 区间非法直接不匹配
|
||||
if startSection <= 0 || endSection <= 0 || startSection > endSection {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 核对该课程事件在指定 周\+星期 下,是否存在覆盖整个节次区间的排程记录
|
||||
// 说明:此处按你当前表结构的用法(schedule\_events 存事件,schedules 存节次明细)来写:
|
||||
// schedules 里通过 schedule\_event\_id 关联到 schedule\_events.id
|
||||
var cnt int64
|
||||
err := dao.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("event_id = ?", courseScheduleEventID).
|
||||
Where("week = ? AND day_of_week = ?", week, dayOfWeek).
|
||||
Where("section BETWEEN ? AND ?", startSection, endSection).
|
||||
Count(&cnt).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 需要区间内的每一节都存在记录才算匹配
|
||||
return cnt == int64(endSection-startSection+1), nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) AddScheduleEvent(scheduleEvent *model.ScheduleEvent) (int, error) {
|
||||
if err := dao.db.Create(&scheduleEvent).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return scheduleEvent.ID, nil
|
||||
}
|
||||
@@ -138,3 +138,46 @@ func (dao *TaskClassDAO) GetCompleteTaskClassByID(ctx context.Context, id int, u
|
||||
}
|
||||
return &taskClass, nil
|
||||
}
|
||||
|
||||
func (dao *TaskClassDAO) GetTaskClassItemByID(ctx context.Context, id int) (*model.TaskClassItem, error) {
|
||||
var item model.TaskClassItem
|
||||
err := dao.db.WithContext(ctx).
|
||||
Where("id = ?", id).
|
||||
First(&item).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (dao *TaskClassDAO) GetTaskClassIDByTaskItemID(ctx context.Context, itemID int) (int, error) {
|
||||
var item model.TaskClassItem
|
||||
err := dao.db.WithContext(ctx).
|
||||
Select("category_id").
|
||||
Where("id = ?", itemID).
|
||||
First(&item).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return *item.CategoryID, nil
|
||||
}
|
||||
|
||||
func (dao *TaskClassDAO) GetTaskClassUserIDByID(ctx context.Context, taskClassID int) (int, error) {
|
||||
var taskClass model.TaskClass
|
||||
err := dao.db.WithContext(ctx).
|
||||
Select("user_id").
|
||||
Where("id = ?", taskClassID).
|
||||
First(&taskClass).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return *taskClass.UserID, nil
|
||||
}
|
||||
|
||||
func (dao *TaskClassDAO) UpdateTaskClassItemEmbeddedTime(ctx context.Context, taskID int, embeddedTime *model.TargetTime) error {
|
||||
err := dao.db.WithContext(ctx).
|
||||
Model(&model.TaskClassItem{}).
|
||||
Where("id = ?", taskID).
|
||||
Update("embedded_time", embeddedTime).Error
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ type UserCheckCourseRequest struct {
|
||||
DayOfWeek int `json:"day_of_week"`
|
||||
StartSection int `json:"start_section"`
|
||||
EndSection int `json:"end_section"`
|
||||
WeekType string `json:"week_type"`
|
||||
} `json:"arrangements"`
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
package model
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type Schedule struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
UserID int `gorm:"column:user_id;index" json:"user_id"`
|
||||
Type string `gorm:"type:enum('course','task');comment:course / task" json:"type"`
|
||||
RelID int `gorm:"column:rel_id;comment:关联 course_id 或 task_item_id" json:"rel_id"`
|
||||
EmbeddedTaskID *int `gorm:"column:embedded_task_id;index;comment:若为水课嵌入,记录任务ID" json:"embedded_task_id"`
|
||||
Week int `gorm:"column:week;uniqueIndex:idx_user_slot_atomic,priority:2;comment:周次 (1-25)" json:"week"`
|
||||
DayOfWeek int `gorm:"column:day_of_week;uniqueIndex:idx_user_slot_atomic,priority:3;comment:星期 (1-7)" json:"day_of_week"`
|
||||
Section int `gorm:"column:section;uniqueIndex:idx_user_slot_atomic,priority:4;comment:原子化节次 (1-12)" json:"section"`
|
||||
Status string `gorm:"type:enum('normal','interrupted');default:normal" json:"status"`
|
||||
CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;comment:是否允许嵌入任务" json:"can_be_embedded"`
|
||||
EventID int `gorm:"column:event_id;index:idx_event_id;not null;comment:关联元数据ID" json:"event_id"`
|
||||
UserID int `gorm:"column:user_id;uniqueIndex:idx_user_slot_atomic,priority:1;not null;comment:冗余UID方便直接查询" json:"user_id"`
|
||||
Week int `gorm:"column:week;uniqueIndex:idx_user_slot_atomic,priority:2;not null;comment:周次 (1-25)" json:"week"`
|
||||
DayOfWeek int `gorm:"column:day_of_week;uniqueIndex:idx_user_slot_atomic,priority:3;not null;comment:星期 (1-7)" json:"day_of_week"`
|
||||
Section int `gorm:"column:section;uniqueIndex:idx_user_slot_atomic,priority:4;not null;comment:原子化节次 (1-12)" json:"section"`
|
||||
EmbeddedTaskID *int `gorm:"column:embedded_task_id;comment:若为水课嵌入,记录具体的任务项ID" json:"embedded_task_id"`
|
||||
Status string `gorm:"column:status;type:enum('normal','interrupted');default:'normal';comment:状态: 正常/因故中断" json:"status"`
|
||||
}
|
||||
|
||||
func (ScheduleEvent) TableName() string { return "schedule_events" }
|
||||
|
||||
func (Schedule) TableName() string { return "schedules" }
|
||||
|
||||
@@ -64,7 +64,8 @@ type UserAddTaskClassItemRequest struct {
|
||||
|
||||
// TargetTime 表示任务块的目标时间
|
||||
type TargetTime struct {
|
||||
Date string `json:"date"` // 例: 2025-12-22
|
||||
Week int `json:"week"` // 周次
|
||||
DayOfWeek int `json:"day_of_week"` // 星期几
|
||||
SectionFrom int `json:"section_from"` // 起始节次
|
||||
SectionTo int `json:"section_to"` // 结束节次
|
||||
}
|
||||
@@ -85,6 +86,14 @@ type TaskClassSummary struct {
|
||||
TotalSlots int `json:"total_slots"`
|
||||
}
|
||||
|
||||
type UserInsertTaskClassItemToScheduleRequest struct {
|
||||
Week int `json:"week" binding:"required,min=1"`
|
||||
DayOfWeek int `json:"day_of_week" binding:"required,min=1,max=7"`
|
||||
StartSection int `json:"start_section" binding:"required,min=1"`
|
||||
EndSection int `json:"end_section" binding:"required,min=1,gtefield=StartSection"`
|
||||
EmbedCourseEventID int `json:"embed_course_event_id"` // 可选,嵌入的课程日程事件 ID
|
||||
}
|
||||
|
||||
// Value 实现 driver.Valuer 接口,负责将 TargetTime 转换为数据库存储的格式
|
||||
func (t *TargetTime) Value() (driver.Value, error) {
|
||||
if t == nil {
|
||||
|
||||
@@ -148,4 +148,39 @@ var ( //请求相关的响应
|
||||
Status: "40021",
|
||||
Info: "user task class forbidden",
|
||||
}
|
||||
|
||||
TaskClassItemNotBelongToUser = Response{ //任务类项目不属于用户
|
||||
Status: "40022",
|
||||
Info: "task class item does not belong to user",
|
||||
}
|
||||
|
||||
TimeOutOfRangeOfThisSemester = Response{ //时间超出本学期范围
|
||||
Status: "40023",
|
||||
Info: "time out of range of this semester",
|
||||
}
|
||||
|
||||
CourseNotBelongToUser = Response{ //课程不属于用户
|
||||
Status: "40024",
|
||||
Info: "course does not belong to user",
|
||||
}
|
||||
|
||||
CourseAlreadyEmbeddedByOtherTaskBlock = Response{ //课程已被其他任务块嵌入
|
||||
Status: "40025",
|
||||
Info: "course already embedded by other task block",
|
||||
}
|
||||
|
||||
ScheduleConflict = Response{ //日程冲突
|
||||
Status: "40026",
|
||||
Info: "schedule conflict",
|
||||
}
|
||||
|
||||
WrongCourseID = Response{ //课程ID错误
|
||||
Status: "40027",
|
||||
Info: "wrong course id",
|
||||
}
|
||||
|
||||
CourseTimeNotMatch = Response{ //课程时间不匹配
|
||||
Status: "40028",
|
||||
Info: "course time not match",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,6 +67,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO) *gin.Engine
|
||||
taskClassGroup.GET("/list", handlers.TaskClassHandler.UserGetTaskClassInfos)
|
||||
taskClassGroup.GET("/get", handlers.TaskClassHandler.UserGetCompleteTaskClass)
|
||||
taskClassGroup.PUT("/update", handlers.TaskClassHandler.UserUpdateTaskClass)
|
||||
taskClassGroup.POST("/insert-into-schedule", handlers.TaskClassHandler.UserAddTaskClassItemIntoSchedule)
|
||||
}
|
||||
}
|
||||
// 初始化Gin引擎
|
||||
|
||||
@@ -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,
|
||||
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)
|
||||
//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
|
||||
}
|
||||
//4.返回结果
|
||||
return nil
|
||||
// 将生成的 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
|
||||
})
|
||||
}
|
||||
|
||||
1
backend/service/schedule.go
Normal file
1
backend/service/schedule.go
Normal file
@@ -0,0 +1 @@
|
||||
package service
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user