Version:0.0.5.dev.260204
feat: 🏗️ 完成任务分类创建与列表查询接口并通过测试 - 历经复杂嵌套逻辑处理 🌀 - 实现创建任务分类接口 ✅ - 实现获取任务分类列表接口 📋 - 接口测试全部通过 🧪 perf: 🚀 下个版本将为任务分类列表接口加入 Redis 缓存以提升查询速度 ⚡
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
package api
|
||||
|
||||
type ApiHandlers struct {
|
||||
UserHandler *UserHandler
|
||||
TaskHandler *TaskHandler
|
||||
ScheduleHandler *ScheduleHandler
|
||||
UserHandler *UserHandler
|
||||
TaskHandler *TaskHandler
|
||||
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)
|
||||
taskRepo := dao.NewTaskDAO(db)
|
||||
scheduleRepo := dao.NewScheduleDAO(db)
|
||||
taskClassRepo := dao.NewTaskClassDAO(db)
|
||||
//service 层
|
||||
userService := service.NewUserService(userRepo, cacheRepo)
|
||||
taskSv := service.NewTaskService(taskRepo)
|
||||
scheduleService := service.NewScheduleService(scheduleRepo)
|
||||
taskClassService := service.NewTaskClassService(taskClassRepo)
|
||||
//api 层
|
||||
userApi := api.NewUserHandler(userService)
|
||||
taskApi := api.NewTaskHandler(taskSv)
|
||||
scheduleApi := api.NewScheduleHandler(scheduleService)
|
||||
taskClassApi := api.NewTaskClassHandler(taskClassService)
|
||||
|
||||
handlers := &api.ApiHandlers{
|
||||
UserHandler: userApi,
|
||||
TaskHandler: taskApi,
|
||||
ScheduleHandler: scheduleApi,
|
||||
UserHandler: userApi,
|
||||
TaskHandler: taskApi,
|
||||
ScheduleHandler: scheduleApi,
|
||||
TaskClassHandler: taskClassApi,
|
||||
}
|
||||
r := routers.RegisterRouters(handlers, cacheRepo)
|
||||
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("/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引擎
|
||||
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