Files
smartmate/backend/service/course.go
LoveLosita 75a44f2edd Version: 0.1.2.dev.260207
feat: ⚠️ 批量导入课程接口支持冲突预检测与冲突提示

- 批量导入课程接口支持预先检测冲突
- 返回并展示具体发生冲突的课程信息 📚💥
- 补全此前规划的冲突提示功能(把大饼补上了 🍞)

refactor: 🧱 使用工作单元模式管理 dao 层事务

- 引入工作单元模式(Unit of Work)统一管理 dao 层
- 新建全局事务,使跨 repo 的 gorm 事务管理更加方便 🔁

fix: 🐛 修复将任务块添加进日程接口的多个问题

- 修复核心逻辑 bug(费了老大劲 😵‍💫)
- 补充并覆盖该接口的多种异常与错误场景测试 🧪
2026-02-07 22:08:13 +08:00

146 lines
4.5 KiB
Go

package service
import (
"context"
"strings"
"github.com/LoveLosita/smartflow/backend/conv"
"github.com/LoveLosita/smartflow/backend/dao"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/respond"
)
type CourseService struct {
// 伸出手:准备接住 DAO
courseDAO *dao.CourseDAO
scheduleDAO *dao.ScheduleDAO
}
// NewCourseService 创建 CourseService 实例
func NewCourseService(courseDAO *dao.CourseDAO, scheduleDAO *dao.ScheduleDAO) *CourseService {
return &CourseService{
courseDAO: courseDAO,
scheduleDAO: scheduleDAO,
}
}
func isUniqueViolation(err error) bool {
if err == nil {
return false
}
// 兼容常见 MySQL / PostgreSQL / SQLite 的报错关键字
// 也可以进一步精确到你的索引名 idx_user_slot_atomic
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "duplicate entry") ||
strings.Contains(msg, "unique constraint") ||
strings.Contains(msg, "unique violation") ||
strings.Contains(msg, "duplicate key") {
return true
}
return false
}
func CheckSingleCourse(req model.UserCheckCourseRequest) bool {
for _, arrangement := range req.Arrangements {
if arrangement.StartWeek > arrangement.EndWeek ||
arrangement.DayOfWeek < 1 || arrangement.DayOfWeek > 7 ||
arrangement.StartSection < 1 || arrangement.EndSection < arrangement.StartSection ||
arrangement.EndSection > 12 || arrangement.StartWeek < 1 || arrangement.EndWeek > 24 {
return false
}
}
return true
}
// AddUserCourses 添加用户课程表
func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImportCoursesRequest, userID int) ([]model.ScheduleConflictDetail, error) {
//1.先校验参数是否正确
for _, course := range req.Courses {
result := CheckSingleCourse(course)
if !result {
return nil, respond.WrongCourseInfo
}
}
//2.将前端传来的课程信息转换为 Schedule 和 ScheduleEvent 切片
var finalSchedules []model.Schedule
var finalScheduleEvents []model.ScheduleEvent
var pos []int
for _, course := range req.Courses {
// 避免取 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{
Week: week,
DayOfWeek: arrangement.DayOfWeek,
Section: section,
Status: "normal",
UserID: userID,
EventID: 0,
}
finalSchedules = append(finalSchedules, schedule)
pos = append(pos, len(finalScheduleEvents)-1)
}
}
}
}
//3.先检测是否重复插入了课程(同一周、同一天、同一节已有课程)
exists, err := ss.scheduleDAO.CheckScheduleConflict(ctx, finalSchedules)
if err != nil {
return nil, err
}
if exists {
return nil, respond.InsertCourseTwice
}
//4.再检查是否和某些非课程的日程冲突(同一周、同一天、同一节已有非课程日程),并给出具体的冲突信息
conflicts, err := ss.scheduleDAO.GetNonCourseScheduleConflicts(ctx, finalSchedules)
if err != nil {
return nil, err
}
if len(conflicts) > 0 {
ret := conv.SchedulesToScheduleConflictDetail(conflicts)
return ret, respond.ScheduleConflict
}
//5.事务:插入两个表要么都成功,要么都回滚
err = ss.courseDAO.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
})
if err != nil {
if isUniqueViolation(err) {
return nil, respond.InsertCourseTwice
}
return nil, err
}
return nil, nil
}