Version: 0.1.2.dev.260207
feat: ⚠️ 批量导入课程接口支持冲突预检测与冲突提示 - 批量导入课程接口支持预先检测冲突 - 返回并展示具体发生冲突的课程信息 📚💥 - 补全此前规划的冲突提示功能(把大饼补上了 🍞) refactor: 🧱 使用工作单元模式管理 dao 层事务 - 引入工作单元模式(Unit of Work)统一管理 dao 层 - 新建全局事务,使跨 repo 的 gorm 事务管理更加方便 🔁 fix: 🐛 修复将任务块添加进日程接口的多个问题 - 修复核心逻辑 bug(费了老大劲 😵💫) - 补充并覆盖该接口的多种异常与错误场景测试 🧪
This commit is contained in:
@@ -56,11 +56,14 @@ func (sa *CourseHandler) AddUserCourses(c *gin.Context) {
|
||||
// 创建一个带 1 秒超时的上下文
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel() // 记得释放资源
|
||||
err = sa.service.AddUserCourses(ctx, req, userIDInterface)
|
||||
conflicts, err := sa.service.AddUserCourses(ctx, req, userIDInterface)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, respond.WrongParamType), errors.Is(err, respond.WrongCourseInfo):
|
||||
case errors.Is(err, respond.WrongParamType), errors.Is(err, respond.WrongCourseInfo),
|
||||
errors.Is(err, respond.InsertCourseTwice):
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
case errors.Is(err, respond.ScheduleConflict):
|
||||
c.JSON(http.StatusConflict, respond.RespWithData(respond.ScheduleConflict, conflicts))
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, respond.InternalError(err))
|
||||
}
|
||||
|
||||
@@ -46,11 +46,12 @@ func Start() {
|
||||
courseRepo := dao.NewCourseDAO(db)
|
||||
taskClassRepo := dao.NewTaskClassDAO(db)
|
||||
scheduleRepo := dao.NewScheduleDAO(db)
|
||||
manager := dao.NewManager(db)
|
||||
//service 层
|
||||
userService := service.NewUserService(userRepo, cacheRepo)
|
||||
taskSv := service.NewTaskService(taskRepo)
|
||||
courseService := service.NewCourseService(courseRepo)
|
||||
taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo)
|
||||
courseService := service.NewCourseService(courseRepo, scheduleRepo)
|
||||
taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo, manager)
|
||||
//api 层
|
||||
userApi := api.NewUserHandler(userService)
|
||||
taskApi := api.NewTaskHandler(taskSv)
|
||||
|
||||
61
backend/conv/schedule.go
Normal file
61
backend/conv/schedule.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package conv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
)
|
||||
|
||||
import "sort"
|
||||
|
||||
func SchedulesToScheduleConflictDetail(schedules []model.Schedule) []model.ScheduleConflictDetail {
|
||||
if len(schedules) == 0 {
|
||||
return []model.ScheduleConflictDetail{}
|
||||
}
|
||||
// 1. 使用 Map 进行逻辑分组
|
||||
// Key 格式: EventID-Week-Day (防止同一事件在不同天出现时被混为一谈)
|
||||
groups := make(map[string]*model.ScheduleConflictDetail)
|
||||
for _, s := range schedules {
|
||||
key := fmt.Sprintf("%d-%d-%d", s.EventID, s.Week, s.DayOfWeek)
|
||||
|
||||
if _, ok := groups[key]; !ok {
|
||||
// 初始化该分组
|
||||
groups[key] = &model.ScheduleConflictDetail{
|
||||
EventID: s.EventID,
|
||||
Name: s.Event.Name,
|
||||
Location: *s.Event.Location, // 假设字段是 *string
|
||||
Type: s.Event.Type,
|
||||
Week: s.Week,
|
||||
DayOfWeek: s.DayOfWeek,
|
||||
}
|
||||
}
|
||||
// 将当前节次加入数组
|
||||
groups[key].Sections = append(groups[key].Sections, s.Section)
|
||||
}
|
||||
|
||||
// 2. 处理每个分组的区间逻辑
|
||||
res := make([]model.ScheduleConflictDetail, 0, len(groups))
|
||||
for _, detail := range groups {
|
||||
// 排序节次,例如把 [3, 1, 2] 变成 [1, 2, 3]
|
||||
sort.Ints(detail.Sections)
|
||||
|
||||
// 最小值即起始,最大值即结束
|
||||
detail.StartSection = detail.Sections[0]
|
||||
detail.EndSection = detail.Sections[len(detail.Sections)-1]
|
||||
|
||||
res = append(res, *detail)
|
||||
}
|
||||
|
||||
// 3. 可选:对结果集按时间排序,让前端收到的 DTO 也是有序的
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
if res[i].Week != res[j].Week {
|
||||
return res[i].Week < res[j].Week
|
||||
}
|
||||
if res[i].DayOfWeek != res[j].DayOfWeek {
|
||||
return res[i].DayOfWeek < res[j].DayOfWeek
|
||||
}
|
||||
return res[i].StartSection < res[j].StartSection
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -137,12 +137,12 @@ func ProcessUserGetCompleteTaskClassRequest(taskClass *model.TaskClass) (*model.
|
||||
}
|
||||
|
||||
// UserInsertTaskItemRequestToModel 用于将填入空闲时段日程的请求转换为 Schedule 模型
|
||||
func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent) {
|
||||
func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID *int, 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,
|
||||
UserID: userID,
|
||||
EmbeddedTaskID: taskID,
|
||||
Week: req.Week,
|
||||
DayOfWeek: req.DayOfWeek,
|
||||
Section: section,
|
||||
|
||||
44
backend/dao/base.go
Normal file
44
backend/dao/base.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RepoManager 囊括了所有的 Repo
|
||||
type RepoManager struct {
|
||||
db *gorm.DB
|
||||
Schedule *ScheduleDAO
|
||||
Task *TaskDAO
|
||||
Course *CourseDAO
|
||||
TaskClass *TaskClassDAO
|
||||
User *UserDAO
|
||||
}
|
||||
|
||||
func NewManager(db *gorm.DB) *RepoManager {
|
||||
return &RepoManager{
|
||||
db: db,
|
||||
Schedule: NewScheduleDAO(db),
|
||||
Task: NewTaskDAO(db),
|
||||
Course: NewCourseDAO(db),
|
||||
TaskClass: NewTaskClassDAO(db),
|
||||
User: NewUserDAO(db),
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction 核心函数:开启一个带事务的“新管理器”
|
||||
func (m *RepoManager) Transaction(ctx context.Context, fn func(txM *RepoManager) error) error {
|
||||
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 💡 关键:创建一个新的 RepoManager,里面的 Repo 全部注入这个 tx 句柄
|
||||
txM := &RepoManager{
|
||||
db: tx,
|
||||
Schedule: m.Schedule.WithTx(tx),
|
||||
Task: m.Task.WithTx(tx),
|
||||
TaskClass: m.TaskClass.WithTx(tx),
|
||||
Course: m.Course.WithTx(tx),
|
||||
User: m.User.WithTx(tx),
|
||||
}
|
||||
return fn(txM)
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,10 @@ func NewCourseDAO(db *gorm.DB) *CourseDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CourseDAO) WithTx(tx *gorm.DB) *CourseDAO {
|
||||
return &CourseDAO{db: tx}
|
||||
}
|
||||
|
||||
func (dao *CourseDAO) AddUserCoursesIntoSchedule(ctx context.Context, courses []model.Schedule) error {
|
||||
if err := dao.db.WithContext(ctx).Create(&courses).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
@@ -20,8 +21,12 @@ func NewScheduleDAO(db *gorm.DB) *ScheduleDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) AddSchedules(schedules []model.Schedule) ([]int, error) {
|
||||
if err := dao.db.Create(&schedules).Error; err != nil {
|
||||
func (d *ScheduleDAO) WithTx(tx *gorm.DB) *ScheduleDAO {
|
||||
return &ScheduleDAO{db: tx}
|
||||
}
|
||||
|
||||
func (d *ScheduleDAO) AddSchedules(schedules []model.Schedule) ([]int, error) {
|
||||
if err := d.db.Create(&schedules).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int, len(schedules))
|
||||
@@ -31,9 +36,9 @@ func (dao *ScheduleDAO) AddSchedules(schedules []model.Schedule) ([]int, error)
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) EmbedTaskIntoSchedule(startSection, endSection, dayOfWeek, week, userID, taskID int) error {
|
||||
func (d *ScheduleDAO) EmbedTaskIntoSchedule(startSection, endSection, dayOfWeek, week, userID, taskID int) error {
|
||||
// 仅更新指定:用户/周/星期/节次区间 的记录,将 embedded_task_id 精准写入 taskID
|
||||
res := dao.db.
|
||||
res := d.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)
|
||||
@@ -41,13 +46,13 @@ func (dao *ScheduleDAO) EmbedTaskIntoSchedule(startSection, endSection, dayOfWee
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) GetCourseUserIDByID(ctx context.Context, courseScheduleEventID int) (int, error) {
|
||||
func (d *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).
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedule_events").
|
||||
Select("user_id").
|
||||
Where("id = ?", courseScheduleEventID).
|
||||
@@ -65,14 +70,14 @@ func (dao *ScheduleDAO) GetCourseUserIDByID(ctx context.Context, courseScheduleE
|
||||
}
|
||||
|
||||
// IsCourseEmbeddedByOtherTaskBlock 判断课程在给定节次区间内是否已被其他任务块嵌入(用于业务限制)
|
||||
func (dao *ScheduleDAO) IsCourseEmbeddedByOtherTaskBlock(ctx context.Context, courseID, startSection, endSection int) (bool, error) {
|
||||
func (d *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).
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("id = ?", courseID).
|
||||
Where("section BETWEEN ? AND ?", startSection, endSection).
|
||||
@@ -84,7 +89,7 @@ func (dao *ScheduleDAO) IsCourseEmbeddedByOtherTaskBlock(ctx context.Context, co
|
||||
return cnt > 0, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, week, dayOfWeek int, sections []int) (bool, error) {
|
||||
func (d *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, week, dayOfWeek int, sections []int) (bool, error) {
|
||||
// 无节次则视为无冲突
|
||||
if len(sections) == 0 {
|
||||
return false, nil
|
||||
@@ -92,7 +97,7 @@ func (dao *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, wee
|
||||
// 统计同一用户、同一周、同一天、且节次有交集的排程数量
|
||||
// 约定表字段:user_id, week, day_of_week, section
|
||||
var cnt int64
|
||||
err := dao.db.WithContext(ctx).
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("user_id = ? AND week = ? AND day_of_week = ?", userID, week, dayOfWeek).
|
||||
Where("section IN ?", sections).
|
||||
@@ -103,7 +108,7 @@ func (dao *ScheduleDAO) HasUserScheduleConflict(ctx context.Context, userID, wee
|
||||
return cnt > 0, nil
|
||||
}
|
||||
|
||||
func (dao *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEventID, week, dayOfWeek, startSection, endSection int) (bool, error) {
|
||||
func (d *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEventID, week, dayOfWeek, startSection, endSection int) (bool, error) {
|
||||
// 区间非法直接不匹配
|
||||
if startSection <= 0 || endSection <= 0 || startSection > endSection {
|
||||
return false, nil
|
||||
@@ -113,7 +118,7 @@ func (dao *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEve
|
||||
// 说明:此处按你当前表结构的用法(schedule\_events 存事件,schedules 存节次明细)来写:
|
||||
// schedules 里通过 schedule\_event\_id 关联到 schedule\_events.id
|
||||
var cnt int64
|
||||
err := dao.db.WithContext(ctx).
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Where("event_id = ?", courseScheduleEventID).
|
||||
Where("week = ? AND day_of_week = ?", week, dayOfWeek).
|
||||
@@ -127,9 +132,137 @@ func (dao *ScheduleDAO) IsCourseTimeMatch(ctx context.Context, courseScheduleEve
|
||||
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 {
|
||||
func (d *ScheduleDAO) AddScheduleEvent(scheduleEvent *model.ScheduleEvent) (int, error) {
|
||||
if err := d.db.Create(&scheduleEvent).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return scheduleEvent.ID, nil
|
||||
}
|
||||
|
||||
// CheckScheduleConflict 检查给定的 Schedule 切片中是否存在课程的冲突(即同一用户、同一周、同一天、且节次有交集的记录,并且只管课程,不管其它任务类型)
|
||||
func (d *ScheduleDAO) CheckScheduleConflict(ctx context.Context, schedules []model.Schedule) (bool, error) {
|
||||
if len(schedules) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 聚合:同一 user/week/day 的节次去重后一次性查库
|
||||
type key struct {
|
||||
UserID int
|
||||
Week int
|
||||
DayOfWeek int
|
||||
}
|
||||
groups := make(map[key]map[int]struct{})
|
||||
|
||||
for _, s := range schedules {
|
||||
// 基础字段不合法直接跳过(按不冲突处理)
|
||||
if s.UserID <= 0 || s.Week <= 0 || s.DayOfWeek <= 0 || s.Section <= 0 {
|
||||
continue
|
||||
}
|
||||
k := key{UserID: s.UserID, Week: s.Week, DayOfWeek: s.DayOfWeek}
|
||||
if _, ok := groups[k]; !ok {
|
||||
groups[k] = make(map[int]struct{})
|
||||
}
|
||||
groups[k][s.Section] = struct{}{}
|
||||
}
|
||||
|
||||
for k, set := range groups {
|
||||
if len(set) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sections := make([]int, 0, len(set))
|
||||
for sec := range set {
|
||||
sections = append(sections, sec)
|
||||
}
|
||||
|
||||
// 仅判断“课程(type=course)”是否冲突:
|
||||
// schedules.event_id -> schedule_events.id,再用 schedule_events.type 过滤
|
||||
var cnt int64
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedules s").
|
||||
Joins("JOIN schedule_events e ON e.id = s.event_id").
|
||||
Where("s.user_id = ? AND s.week = ? AND s.day_of_week = ?", k.UserID, k.Week, k.DayOfWeek).
|
||||
Where("s.section IN ?", sections).
|
||||
Where("e.type = ?", "course").
|
||||
Count(&cnt).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *ScheduleDAO) GetNonCourseScheduleConflicts(ctx context.Context, newSchedules []model.Schedule) ([]model.Schedule, error) {
|
||||
if len(newSchedules) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 1. 构建指纹图:用于快速比对坐标
|
||||
userID := newSchedules[0].UserID
|
||||
weeksMap := make(map[int]bool)
|
||||
newSlotsFingerprints := make(map[string]bool)
|
||||
|
||||
for _, s := range newSchedules {
|
||||
weeksMap[s.Week] = true
|
||||
key := fmt.Sprintf("%d-%d-%d", s.Week, s.DayOfWeek, s.Section)
|
||||
newSlotsFingerprints[key] = true
|
||||
}
|
||||
|
||||
weeks := make([]int, 0, len(weeksMap))
|
||||
for w := range weeksMap {
|
||||
weeks = append(weeks, w)
|
||||
}
|
||||
|
||||
// 2. 第一步:定义一个临时小结构体,精准捞取坐标和 EventID
|
||||
type simpleSlot struct {
|
||||
EventID int
|
||||
Week int
|
||||
DayOfWeek int
|
||||
Section int
|
||||
}
|
||||
var candidates []simpleSlot
|
||||
|
||||
// 💡 这里的逻辑:只查索引覆盖到的字段,速度极快
|
||||
err := d.db.WithContext(ctx).
|
||||
Table("schedules").
|
||||
Select("schedules.event_id, schedules.week, schedules.day_of_week, schedules.section").
|
||||
Joins("JOIN schedule_events ON schedule_events.id = schedules.event_id").
|
||||
Where("schedules.user_id = ? AND schedules.week IN ? AND schedule_events.type != ?", userID, weeks, "course").
|
||||
Scan(&candidates).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 筛选出真正碰撞的 EventID
|
||||
eventIDMap := make(map[int]bool)
|
||||
for _, s := range candidates {
|
||||
key := fmt.Sprintf("%d-%d-%d", s.Week, s.DayOfWeek, s.Section)
|
||||
if newSlotsFingerprints[key] {
|
||||
eventIDMap[s.EventID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(eventIDMap) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 4. 第二步:“抄全家”——根据碰撞到的 ID 捞出这些任务的所有原子槽位
|
||||
var ids []int
|
||||
for id := range eventIDMap {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var fullConflicts []model.Schedule
|
||||
// 💡 关键:这里必须 Preload("Event"),这样 DTO 才有名称显示
|
||||
err = d.db.WithContext(ctx).
|
||||
Preload("Event").
|
||||
Where("event_id IN ?", ids).
|
||||
Find(&fullConflicts).Error
|
||||
|
||||
return fullConflicts, err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ func NewTaskClassDAO(db *gorm.DB) *TaskClassDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (dao *TaskClassDAO) WithTx(tx *gorm.DB) *TaskClassDAO {
|
||||
return &TaskClassDAO{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// AddOrUpdateTaskClass 为指定用户添加/更新任务类(防越权:更新时限定 user_id)
|
||||
func (dao *TaskClassDAO) AddOrUpdateTaskClass(userID int, taskClass *model.TaskClass) (int, error) {
|
||||
// 不信任入参里的 UserID,强制使用当前登录用户
|
||||
|
||||
@@ -19,6 +19,10 @@ func NewTaskDAO(db *gorm.DB) *TaskDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TaskDAO) WithTx(tx *gorm.DB) *TaskDAO {
|
||||
return &TaskDAO{db: tx}
|
||||
}
|
||||
|
||||
// AddTask 为指定用户添加任务
|
||||
func (dao *TaskDAO) AddTask(req *model.Task) (*model.Task, error) {
|
||||
if err := dao.db.Create(req).Error; err != nil {
|
||||
|
||||
@@ -23,6 +23,10 @@ func NewUserDAO(db *gorm.DB) *UserDAO {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserDAO) WithTx(tx *gorm.DB) *UserDAO {
|
||||
return &UserDAO{db: tx}
|
||||
}
|
||||
|
||||
// Create 创建新用户
|
||||
// 插入新用户信息到数据库
|
||||
func (dao *UserDAO) Create(username, phoneNumber, password string) (*model.User, error) {
|
||||
|
||||
@@ -19,6 +19,26 @@ type Schedule struct {
|
||||
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"`
|
||||
// 💡 必须加上这一行,告诉 GORM 如何关联元数据
|
||||
Event ScheduleEvent `gorm:"foreignKey:EventID" json:"event"`
|
||||
}
|
||||
|
||||
type ScheduleConflictDetail struct {
|
||||
EventID int `json:"event_id"`
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"`
|
||||
DayOfWeek int `json:"day_of_week"`
|
||||
Week int `json:"week"`
|
||||
Sections []int `json:"sections"`
|
||||
StartSection int `json:"start_section"`
|
||||
EndSection int `json:"end_section"`
|
||||
Type string `json:"type"`
|
||||
EmbeddedTasks []ScheduleEmbeddedTask `json:"embedded_tasks"`
|
||||
}
|
||||
|
||||
type ScheduleEmbeddedTask struct {
|
||||
Section int `json:"section"`
|
||||
TaskID int `json:"task_id"`
|
||||
}
|
||||
|
||||
func (ScheduleEvent) TableName() string { return "schedule_events" }
|
||||
|
||||
@@ -183,4 +183,9 @@ var ( //请求相关的响应
|
||||
Status: "40028",
|
||||
Info: "course time not match",
|
||||
}
|
||||
|
||||
InsertCourseTwice = Response{ //重复插入课程
|
||||
Status: "40029",
|
||||
Info: "insert course twice",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,9 @@ 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"
|
||||
@@ -10,16 +12,34 @@ import (
|
||||
|
||||
type CourseService struct {
|
||||
// 伸出手:准备接住 DAO
|
||||
dao *dao.CourseDAO
|
||||
courseDAO *dao.CourseDAO
|
||||
scheduleDAO *dao.ScheduleDAO
|
||||
}
|
||||
|
||||
// NewCourseService 创建 CourseService 实例
|
||||
func NewCourseService(dao *dao.CourseDAO) *CourseService {
|
||||
func NewCourseService(courseDAO *dao.CourseDAO, scheduleDAO *dao.ScheduleDAO) *CourseService {
|
||||
return &CourseService{
|
||||
dao: dao,
|
||||
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 ||
|
||||
@@ -33,14 +53,15 @@ func CheckSingleCourse(req model.UserCheckCourseRequest) bool {
|
||||
}
|
||||
|
||||
// AddUserCourses 添加用户课程表
|
||||
func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImportCoursesRequest, userID int) error {
|
||||
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 respond.WrongCourseInfo
|
||||
return nil, respond.WrongCourseInfo
|
||||
}
|
||||
}
|
||||
//2.将前端传来的课程信息转换为 Schedule 和 ScheduleEvent 切片
|
||||
var finalSchedules []model.Schedule
|
||||
var finalScheduleEvents []model.ScheduleEvent
|
||||
var pos []int
|
||||
@@ -82,9 +103,25 @@ func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImpor
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO 冲突处理、重复检测...预计0.2.0版本之前完成
|
||||
//4.事务:插入两个表要么都成功,要么都回滚
|
||||
return ss.dao.Transaction(func(txDAO *dao.CourseDAO) error {
|
||||
//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
|
||||
@@ -98,4 +135,11 @@ func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImpor
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
return nil, respond.InsertCourseTwice
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -17,13 +17,15 @@ type TaskClassService struct {
|
||||
taskClassRepo *dao.TaskClassDAO
|
||||
cacheRepo *dao.CacheDAO
|
||||
scheduleRepo *dao.ScheduleDAO
|
||||
repoManager *dao.RepoManager // 统一管理多个 DAO 的事务
|
||||
}
|
||||
|
||||
func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO, scheduleRepo *dao.ScheduleDAO) *TaskClassService {
|
||||
func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO, scheduleRepo *dao.ScheduleDAO, manager *dao.RepoManager) *TaskClassService {
|
||||
return &TaskClassService{
|
||||
taskClassRepo: taskClassRepo,
|
||||
cacheRepo: cacheRepo,
|
||||
scheduleRepo: scheduleRepo,
|
||||
repoManager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +167,11 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re
|
||||
}
|
||||
//4.否则构造Schedule模型
|
||||
sections := make([]int, 0, req.EndSection-req.StartSection+1)
|
||||
schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel(req, taskItem, taskID, userID, req.StartSection, req.EndSection)
|
||||
schedules, scheduleEvent := conv.UserInsertTaskItemRequestToModel(req, taskItem, nil, userID, req.StartSection, req.EndSection)
|
||||
//将节次区间转换为节次切片,方便后续检查冲突
|
||||
for section := req.StartSection; section <= req.EndSection; section++ {
|
||||
sections = append(sections, section)
|
||||
}
|
||||
//4.1 统一检查冲突(避免逐条查库)
|
||||
conflict, err := sv.scheduleRepo.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections)
|
||||
if err != nil {
|
||||
@@ -175,7 +181,7 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re
|
||||
return respond.ScheduleConflict
|
||||
}
|
||||
//5.写入数据库(事务)
|
||||
if err := sv.taskClassRepo.Transaction(func(txDAO *dao.TaskClassDAO) error {
|
||||
/*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 {
|
||||
@@ -197,6 +203,34 @@ func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, re
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}*/
|
||||
// 5. 写入数据库(通过 RepoManager 统一管理事务)
|
||||
// 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例
|
||||
if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
|
||||
// 5.1 使用事务中的 ScheduleRepo 插入 Event
|
||||
// 💡 这里的 txM.Schedule 已经注入了事务句柄
|
||||
eventID, err := txM.Schedule.AddScheduleEvent(scheduleEvent)
|
||||
if err != nil {
|
||||
return err // 触发回滚
|
||||
}
|
||||
// 5.2 关联 ID(纯内存操作,无需 tx)
|
||||
for i := range schedules {
|
||||
schedules[i].EventID = eventID
|
||||
}
|
||||
// 5.3 使用事务中的 ScheduleRepo 批量插入原子槽位
|
||||
// 💡 如果这里因为外键或唯一索引报错,5.1 的 Event 也会被撤回
|
||||
if _, err = txM.Schedule.AddSchedules(schedules); err != nil {
|
||||
return err // 触发回滚
|
||||
}
|
||||
// 5.4 使用事务中的 TaskRepo 更新任务状态
|
||||
// 💡 这里的 txM.Task 取代了你原来的 txDAO
|
||||
if err := txM.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime); err != nil {
|
||||
return err // 触发回滚
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
// 这里处理最终的错误返回,比如 respond.Error
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskReques
|
||||
if taskModel.Priority < 1 || taskModel.Priority >= 5 {
|
||||
return nil, respond.InvalidPriority
|
||||
}
|
||||
//3. 调用 dao 层进行数据持久化
|
||||
//3. 调用 courseDAO 层进行数据持久化
|
||||
createdTask, err := ts.dao.AddTask(taskModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -39,7 +39,7 @@ func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskReques
|
||||
}
|
||||
|
||||
func (ts *TaskService) GetUserTasks(ctx context.Context, userID int) ([]model.GetUserTaskResp, error) {
|
||||
//1. 调用 dao 层获取数据
|
||||
//1. 调用 courseDAO 层获取数据
|
||||
tasks, err := ts.dao.GetTasksByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user