Version:0.0.5.dev.260204
feat: 🏗️ 完成任务分类创建与列表查询接口并通过测试 - 历经复杂嵌套逻辑处理 🌀 - 实现创建任务分类接口 ✅ - 实现获取任务分类列表接口 📋 - 接口测试全部通过 🧪 perf: 🚀 下个版本将为任务分类列表接口加入 Redis 缓存以提升查询速度 ⚡
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
type ApiHandlers struct {
|
type ApiHandlers struct {
|
||||||
UserHandler *UserHandler
|
UserHandler *UserHandler
|
||||||
TaskHandler *TaskHandler
|
TaskHandler *TaskHandler
|
||||||
ScheduleHandler *ScheduleHandler
|
ScheduleHandler *ScheduleHandler
|
||||||
|
TaskClassHandler *TaskClassHandler
|
||||||
}
|
}
|
||||||
|
|||||||
51
backend/api/task-class.go
Normal file
51
backend/api/task-class.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/respond"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskClassHandler struct {
|
||||||
|
svc *service.TaskClassService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskClassHandler:组装 Handler 的“工厂”
|
||||||
|
func NewTaskClassHandler(svc *service.TaskClassService) *TaskClassHandler {
|
||||||
|
return &TaskClassHandler{
|
||||||
|
svc: svc, // 把传进来的 Service 揣进口袋里
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *TaskClassHandler) UserAddTaskClass(c *gin.Context) {
|
||||||
|
var req model.UserAddTaskClassRequest
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userIDInterface := c.GetInt("user_id")
|
||||||
|
err = api.svc.AddTaskClass(&req, userIDInterface)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, respond.WrongParamType) {
|
||||||
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, respond.InternalError(err))
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, respond.Ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *TaskClassHandler) UserGetTaskClassInfos(c *gin.Context) {
|
||||||
|
userIDInterface := c.GetInt("user_id")
|
||||||
|
resp, err := api.svc.GetUserTaskClassInfos(userIDInterface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, respond.InternalError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp))
|
||||||
|
}
|
||||||
@@ -44,19 +44,23 @@ func Start() {
|
|||||||
cacheRepo := dao.NewCacheDAO(rdb)
|
cacheRepo := dao.NewCacheDAO(rdb)
|
||||||
taskRepo := dao.NewTaskDAO(db)
|
taskRepo := dao.NewTaskDAO(db)
|
||||||
scheduleRepo := dao.NewScheduleDAO(db)
|
scheduleRepo := dao.NewScheduleDAO(db)
|
||||||
|
taskClassRepo := dao.NewTaskClassDAO(db)
|
||||||
//service 层
|
//service 层
|
||||||
userService := service.NewUserService(userRepo, cacheRepo)
|
userService := service.NewUserService(userRepo, cacheRepo)
|
||||||
taskSv := service.NewTaskService(taskRepo)
|
taskSv := service.NewTaskService(taskRepo)
|
||||||
scheduleService := service.NewScheduleService(scheduleRepo)
|
scheduleService := service.NewScheduleService(scheduleRepo)
|
||||||
|
taskClassService := service.NewTaskClassService(taskClassRepo)
|
||||||
//api 层
|
//api 层
|
||||||
userApi := api.NewUserHandler(userService)
|
userApi := api.NewUserHandler(userService)
|
||||||
taskApi := api.NewTaskHandler(taskSv)
|
taskApi := api.NewTaskHandler(taskSv)
|
||||||
scheduleApi := api.NewScheduleHandler(scheduleService)
|
scheduleApi := api.NewScheduleHandler(scheduleService)
|
||||||
|
taskClassApi := api.NewTaskClassHandler(taskClassService)
|
||||||
|
|
||||||
handlers := &api.ApiHandlers{
|
handlers := &api.ApiHandlers{
|
||||||
UserHandler: userApi,
|
UserHandler: userApi,
|
||||||
TaskHandler: taskApi,
|
TaskHandler: taskApi,
|
||||||
ScheduleHandler: scheduleApi,
|
ScheduleHandler: scheduleApi,
|
||||||
|
TaskClassHandler: taskClassApi,
|
||||||
}
|
}
|
||||||
r := routers.RegisterRouters(handlers, cacheRepo)
|
r := routers.RegisterRouters(handlers, cacheRepo)
|
||||||
routers.StartEngine(r)
|
routers.StartEngine(r)
|
||||||
|
|||||||
96
backend/conv/task-class.go
Normal file
96
backend/conv/task-class.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package conv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/respond"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateLayout = "2006-01-02"
|
||||||
|
|
||||||
|
func parseDatePtr(s string) (*time.Time, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
t, err := time.ParseInLocation(dateLayout, s, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessUserAddTaskClassRequest(req *model.UserAddTaskClassRequest, userID int) (*model.TaskClass, []model.TaskClassItem, error) {
|
||||||
|
startDate, err := parseDatePtr(req.StartDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, respond.WrongParamType
|
||||||
|
}
|
||||||
|
endDate, err := parseDatePtr(req.EndDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, respond.WrongParamType
|
||||||
|
}
|
||||||
|
//1.填充section1,2
|
||||||
|
taskClass := model.TaskClass{
|
||||||
|
Name: &req.Name,
|
||||||
|
Mode: &req.Mode,
|
||||||
|
StartDate: startDate,
|
||||||
|
EndDate: endDate,
|
||||||
|
UserID: &userID,
|
||||||
|
}
|
||||||
|
//2.填充section3
|
||||||
|
taskClass.TotalSlots = &req.Config.TotalSlots
|
||||||
|
taskClass.AllowFillerCourse = &req.Config.AllowFillerCourse
|
||||||
|
taskClass.Strategy = &req.Config.Strategy
|
||||||
|
//处理 ExcludedSlots 切片为 JSON 字符串
|
||||||
|
if len(req.Config.ExcludedSlots) > 0 {
|
||||||
|
//转换为 JSON 字符串
|
||||||
|
excludedSlotsJSON := "["
|
||||||
|
for i, slot := range req.Config.ExcludedSlots {
|
||||||
|
excludedSlotsJSON += string(rune(slot + '0')) //简单转换为字符
|
||||||
|
if i != len(req.Config.ExcludedSlots)-1 {
|
||||||
|
excludedSlotsJSON += ","
|
||||||
|
}
|
||||||
|
}
|
||||||
|
excludedSlotsJSON += "]"
|
||||||
|
taskClass.ExcludedSlots = &excludedSlotsJSON
|
||||||
|
} else {
|
||||||
|
emptyJSON := "[]"
|
||||||
|
taskClass.ExcludedSlots = &emptyJSON
|
||||||
|
}
|
||||||
|
//3.开始构建 items
|
||||||
|
var items []model.TaskClassItem
|
||||||
|
for _, itemReq := range req.Items {
|
||||||
|
item := model.TaskClassItem{ //填充section 2
|
||||||
|
Order: &itemReq.Order,
|
||||||
|
Content: &itemReq.Content,
|
||||||
|
EmbeddedTime: itemReq.EmbeddedTime,
|
||||||
|
Status: nil,
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return &taskClass, items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeOrZero(t *time.Time) time.Time {
|
||||||
|
if t == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return *t
|
||||||
|
}
|
||||||
|
|
||||||
|
func TaskClassModelToResponse(taskClasses []model.TaskClass) *model.UserGetTaskClassesResponse {
|
||||||
|
var resp model.UserGetTaskClassesResponse
|
||||||
|
for _, tc := range taskClasses {
|
||||||
|
tcResp := model.TaskClassSummary{
|
||||||
|
ID: tc.ID,
|
||||||
|
Name: *tc.Name,
|
||||||
|
Mode: *tc.Mode,
|
||||||
|
StartDate: timeOrZero(tc.StartDate),
|
||||||
|
EndDate: timeOrZero(tc.EndDate),
|
||||||
|
TotalSlots: *tc.TotalSlots,
|
||||||
|
Strategy: *tc.Strategy,
|
||||||
|
}
|
||||||
|
resp.TaskClasses = append(resp.TaskClasses, tcResp)
|
||||||
|
}
|
||||||
|
return &resp
|
||||||
|
}
|
||||||
49
backend/dao/task-class.go
Normal file
49
backend/dao/task-class.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskClassDAO struct {
|
||||||
|
// 这是一个口袋,用来装数据库连接实例
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskClassDAO 创建TaskClassDAO实例
|
||||||
|
// NewTaskClassDAO 接收一个 *gorm.DB,并把它塞进结构体的口袋里
|
||||||
|
func NewTaskClassDAO(db *gorm.DB) *TaskClassDAO {
|
||||||
|
return &TaskClassDAO{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTaskClass 为指定用户添加任务类
|
||||||
|
func (dao *TaskClassDAO) AddTaskClass(taskClass *model.TaskClass) (int, error) {
|
||||||
|
err := dao.db.Create(taskClass).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return taskClass.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dao *TaskClassDAO) AddTaskClassItems(items []model.TaskClassItem) error {
|
||||||
|
return dao.db.Create(&items).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction 在一个事务中执行传入的函数,供 service 层复用(自动提交/回滚)
|
||||||
|
// 规则:fn 返回 nil -> commit;fn 返回 error 或 panic -> rollback
|
||||||
|
func (dao *TaskClassDAO) Transaction(fn func(txDAO *TaskClassDAO) error) error {
|
||||||
|
return dao.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
return fn(NewTaskClassDAO(tx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dao *TaskClassDAO) GetUserTaskClasses(userID int) ([]model.TaskClass, error) {
|
||||||
|
var taskClasses []model.TaskClass
|
||||||
|
err := dao.db.Where("user_id = ?", userID).Find(&taskClasses).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return taskClasses, nil
|
||||||
|
}
|
||||||
120
backend/model/task-class.go
Normal file
120
backend/model/task-class.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskClass struct {
|
||||||
|
//section 1
|
||||||
|
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
UserID *int `gorm:"column:user_id;index:idx_task_classes_user_id"`
|
||||||
|
//section 2
|
||||||
|
Name *string `gorm:"column:name;size:255"`
|
||||||
|
Mode *string `gorm:"column:mode;type:enum('auto','manual')"`
|
||||||
|
StartDate *time.Time `gorm:"column:start_date"`
|
||||||
|
EndDate *time.Time `gorm:"column:end_date"`
|
||||||
|
//section 3
|
||||||
|
TotalSlots *int `gorm:"column:total_slots;comment:分配的总节数"`
|
||||||
|
AllowFillerCourse *bool `gorm:"column:allow_filler_course;default:true"`
|
||||||
|
Strategy *string `gorm:"column:strategy;type:enum('steady','rapid')"`
|
||||||
|
ExcludedSlots *string `gorm:"column:excluded_slots;type:json;comment:不想要的时段切片"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 设定 TaskClass 的表名为 task_classes
|
||||||
|
func (TaskClass) TableName() string {
|
||||||
|
return "task_classes"
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAddTaskClassRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
StartDate string `json:"start_date" binding:"required"` // YYYY-MM-DD
|
||||||
|
EndDate string `json:"end_date" binding:"required"` // YYYY-MM-DD
|
||||||
|
Mode string `json:"mode" binding:"required,oneof=auto manual"`
|
||||||
|
Config UserAddTaskClassConfig `json:"config" binding:"required"`
|
||||||
|
Items []UserAddTaskClassItemRequest `json:"items" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAddTaskClassConfig struct {
|
||||||
|
TotalSlots int `json:"total_slots" binding:"required,min=1"`
|
||||||
|
AllowFillerCourse bool `json:"allow_filler_course"`
|
||||||
|
Strategy string `json:"strategy" binding:"required,oneof=steady rapid"`
|
||||||
|
ExcludedSlots []int `json:"excluded_slots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAddTaskClassItemRequest struct {
|
||||||
|
Order int `json:"order" binding:"required,min=1"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
EmbeddedTime *TargetTime `json:"embedded_time"` // 例: 2025-12-22 1-2节; nil 表示未安排
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskClassItem struct {
|
||||||
|
//section 1
|
||||||
|
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
CategoryID *int `gorm:"column:category_id"` //对应 TaskClass 的 ID
|
||||||
|
//section 2
|
||||||
|
Order *int `gorm:"column:order"`
|
||||||
|
Content *string `gorm:"column:content;type:text"`
|
||||||
|
EmbeddedTime *TargetTime `gorm:"column:embedded_time;type:json;comment:目标时间{date,section_from,section_to}"`
|
||||||
|
Status *int `gorm:"column:status;comment:1:未安排, 2:已应用"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetTime struct {
|
||||||
|
Date string `json:"date"` // 例: 2025-12-22
|
||||||
|
SectionFrom int `json:"section_from"` // 起始节次
|
||||||
|
SectionTo int `json:"section_to"` // 结束节次
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetTime) Value() (driver.Value, error) {
|
||||||
|
if t == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// 💡 关键:调用 json.Marshal 将结构体转为 []byte
|
||||||
|
// 这样 GORM 就能把这一串 JSON 存进数据库的 text/json 字段了
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TargetTime) Scan(value any) error {
|
||||||
|
if value == nil {
|
||||||
|
// 如果数据库是 NULL,保持指针对应的对象为零值即可
|
||||||
|
// 或者在业务层判断 nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
data = v
|
||||||
|
case string:
|
||||||
|
data = []byte(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("TargetTime: 不支持的扫描类型: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskClassItem) TableName() string {
|
||||||
|
return "task_items"
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskItemStatusUnscheduled = 1
|
||||||
|
TaskItemStatusApplied = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserGetTaskClassesResponse struct {
|
||||||
|
TaskClasses []TaskClassSummary `json:"task_classes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskClassSummary struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
StartDate time.Time `json:"start_date"`
|
||||||
|
EndDate time.Time `json:"end_date"`
|
||||||
|
TotalSlots int `json:"total_slots"`
|
||||||
|
}
|
||||||
@@ -60,6 +60,12 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO) *gin.Engine
|
|||||||
scheduleGroup.POST("/validate", handlers.ScheduleHandler.CheckUserCourse)
|
scheduleGroup.POST("/validate", handlers.ScheduleHandler.CheckUserCourse)
|
||||||
scheduleGroup.POST("/import-courses", handlers.ScheduleHandler.AddUserCourses)
|
scheduleGroup.POST("/import-courses", handlers.ScheduleHandler.AddUserCourses)
|
||||||
}
|
}
|
||||||
|
taskClassGroup := apiGroup.Group("/task-class")
|
||||||
|
{
|
||||||
|
taskClassGroup.Use(middleware.JWTTokenAuth(cache))
|
||||||
|
taskClassGroup.POST("/add", handlers.TaskClassHandler.UserAddTaskClass)
|
||||||
|
taskClassGroup.GET("/list", handlers.TaskClassHandler.UserGetTaskClassInfos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 初始化Gin引擎
|
// 初始化Gin引擎
|
||||||
log.Println("Routes setup completed")
|
log.Println("Routes setup completed")
|
||||||
|
|||||||
51
backend/service/task-class.go
Normal file
51
backend/service/task-class.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/LoveLosita/smartflow/backend/conv"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/dao"
|
||||||
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskClassService struct {
|
||||||
|
// 这里可以添加数据库连接或其他依赖
|
||||||
|
taskClassRepo *dao.TaskClassDAO
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskClassService(taskClassRepo *dao.TaskClassDAO) *TaskClassService {
|
||||||
|
return &TaskClassService{
|
||||||
|
taskClassRepo: taskClassRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTaskClass 为指定用户添加任务类
|
||||||
|
func (sv *TaskClassService) AddTaskClass(req *model.UserAddTaskClassRequest, userID int) error {
|
||||||
|
return sv.taskClassRepo.Transaction(func(txDAO *dao.TaskClassDAO) error {
|
||||||
|
//1.先转换
|
||||||
|
taskClass, items, err := conv.ProcessUserAddTaskClassRequest(req, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//2.插入 task class 并获取 ID
|
||||||
|
taskClassID, err := txDAO.AddTaskClass(taskClass)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//3.插入 items
|
||||||
|
for i := range items {
|
||||||
|
items[i].CategoryID = &taskClassID // 关联 task class ID
|
||||||
|
}
|
||||||
|
if err := txDAO.AddTaskClassItems(items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *TaskClassService) GetUserTaskClassInfos(userID int) (*model.UserGetTaskClassesResponse, error) {
|
||||||
|
taskClasses, err := sv.taskClassRepo.GetUserTaskClasses(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := conv.TaskClassModelToResponse(taskClasses)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user