Files
smartmate/backend/service/task-class.go
LoveLosita dad1eade93 Version: 0.3.2.dev.260214
feat:  新增获取当前正在进行的任务接口并完成测试

- 若当前为上课时间,返回当前任务 📚
- 若当前为下课/空闲时间,返回下一个任务 ➡️
- 若存在嵌入任务,支持嵌套返回结构 🧩
- 接口已测试通过 🧪

docs: 📝 小幅更新 README

fix: 🐛 修复获取最近已完成任务列表接口的遗漏问题

- 修复无法获取嵌入在课程中的任务问题 🔧

fix: 🐛 修复删除日程接口的字段遗漏问题

- 若删除的是单独任务块,补充删除 task_item 表的 embedded_time 字段
- 避免数据残留与状态异常 
2026-02-14 21:46:02 +08:00

291 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"log"
"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"
"gorm.io/gorm"
)
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, manager *dao.RepoManager) *TaskClassService {
return &TaskClassService{
taskClassRepo: taskClassRepo,
cacheRepo: cacheRepo,
scheduleRepo: scheduleRepo,
repoManager: manager,
}
}
// AddOrUpdateTaskClass 为指定用户添加任务类
func (sv *TaskClassService) AddOrUpdateTaskClass(ctx context.Context, req *model.UserAddTaskClassRequest, userID int, method int, targetTaskClassID int) error {
// 1) 先写数据库(事务内)
if err := sv.taskClassRepo.Transaction(func(txDAO *dao.TaskClassDAO) error {
taskClass, items, err := conv.ProcessUserAddTaskClassRequest(req, userID)
if err != nil {
return err
}
if method == 1 { // 更新操作
taskClass.ID = targetTaskClassID
}
taskClassID, err := txDAO.AddOrUpdateTaskClass(userID, taskClass)
if err != nil {
return err
}
for i := range items {
items[i].CategoryID = &taskClassID
}
if err := txDAO.AddOrUpdateTaskClassItems(userID, items); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
func (sv *TaskClassService) GetUserTaskClassInfos(ctx context.Context, userID int) (*model.UserGetTaskClassesResponse, error) {
//1.先查询redis
list, err := sv.cacheRepo.GetTaskClassList(ctx, userID)
if err == nil {
//命中缓存
return list, nil
} else if !errors.Is(err, redis.Nil) { //不是缓存未命中错误说明redis可能炸了照常放行
log.Println("redis获取任务分类列表失败:", err)
}
//2.缓存未命中,查询数据库
taskClasses, err := sv.taskClassRepo.GetUserTaskClasses(userID)
if err != nil {
return nil, err
}
resp := conv.TaskClassModelToResponse(taskClasses)
//3.写入缓存
err = sv.cacheRepo.AddTaskClassList(ctx, userID, resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (sv *TaskClassService) GetUserCompleteTaskClass(ctx context.Context, userID int, taskClassID int) (*model.UserAddTaskClassRequest, error) {
//1.查询数据库
taskClass, err := sv.taskClassRepo.GetCompleteTaskClassByID(ctx, taskClassID, userID)
if err != nil {
return nil, err
}
//2.转换为响应结构体
resp, err := conv.ProcessUserGetCompleteTaskClassRequest(taskClass)
if err != nil {
return nil, err
}
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.再检查任务块本身是否已经被安排
result, err := sv.taskClassRepo.IfTaskClassItemArranged(ctx, taskID)
if err != nil {
return err
}
if result {
return respond.TaskClassItemAlreadyArranged
}
//3.取出任务块信息
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, 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 {
return err
}
if conflict {
return respond.ScheduleConflict
}
// 5. 写入数据库(通过 RepoManager 统一管理事务)
// 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例
if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
// 5.1 使用事务中的 ScheduleRepo 插入 Event
// 💡 这里的 txM.Schedule 已经注入了事务句柄
//此处要将req中的起始section以及第几周、星期几转换成绝对时间存入scheduleEvent的StartTime和EndTime字段中方便后续查询和冲突检查
st, ed, err := conv.RelativeTimeToRealTime(req.Week, req.DayOfWeek, req.StartSection, req.EndSection)
if err != nil {
return err
}
scheduleEvent.StartTime = st
scheduleEvent.EndTime = ed
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
}
func (sv *TaskClassService) DeleteTaskClassItem(ctx context.Context, userID int, taskItemID int) error {
//1.先验证任务块归属
taskClassID, err := sv.taskClassRepo.GetTaskClassIDByTaskItemID(ctx, taskItemID) //通过任务块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.如果该任务块已经被安排了,先解除安排,再删除任务块(事务)
if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
//2.1.先检查该任务块是否已经被安排了
arranged, err := txM.TaskClass.IfTaskClassItemArranged(ctx, taskItemID)
if err != nil {
return err
}
if arranged {
//2.2.如果已经被安排了,先解除安排
//先扫schedules找到该task_item_id并删除
_, txErr := txM.Schedule.FindEmbeddedTaskIDAndDeleteIt(ctx, taskItemID)
//2.3.再将task_items表的embedded_time字段设置为null
txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskItemID)
if txErr != nil {
return txErr
}
//再删除schedule_event表中对应的事件
txErr = txM.Schedule.DeleteScheduleEventByTaskItemID(ctx, taskItemID)
if txErr != nil {
return txErr
}
}
//2.4.最后删除任务块
err = txM.TaskClass.DeleteTaskClassItemByID(ctx, taskItemID)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
func (sv *TaskClassService) DeleteTaskClass(ctx context.Context, userID int, taskClassID int) error {
//1.先验证任务类归属
ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return respond.WrongTaskClassID
}
return err
}
if ownerID != userID {
return respond.TaskClassNotBelongToUser
}
//2.删除任务类(事务)
err = sv.taskClassRepo.DeleteTaskClassByID(ctx, taskClassID)
if err != nil {
return err
}
return nil
}