+15
-1
@@ -25,7 +25,7 @@ func main() {
|
|||||||
gin.SetMode(cfg.Server.Mode)
|
gin.SetMode(cfg.Server.Mode)
|
||||||
|
|
||||||
// 初始化 SQLite 数据库
|
// 初始化 SQLite 数据库
|
||||||
if err := db.Init(cfg.Database.DSN, &model.User{}); err != nil {
|
if err := db.Init(cfg.Database.DSN, &model.User{}, &model.Project{}, &model.ProjectStyleRecord{}, &model.Task{}); err != nil {
|
||||||
slog.Error("db init failed", "error", err)
|
slog.Error("db init failed", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,9 @@ func main() {
|
|||||||
storageSvc := service.NewStorageService(cfg.Qiniu)
|
storageSvc := service.NewStorageService(cfg.Qiniu)
|
||||||
handler.InitStorageService(storageSvc)
|
handler.InitStorageService(storageSvc)
|
||||||
|
|
||||||
|
// 初始化项目服务
|
||||||
|
service.InitProjectService(db.GetDB())
|
||||||
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mildware.Logger())
|
r.Use(mildware.Logger())
|
||||||
r.Use(mildware.Recovery())
|
r.Use(mildware.Recovery())
|
||||||
@@ -57,9 +60,20 @@ func main() {
|
|||||||
v1Auth := r.Group("/api/v1")
|
v1Auth := r.Group("/api/v1")
|
||||||
v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret))
|
v1Auth.Use(mildware.AuthMiddleware(cfg.JWT.Secret))
|
||||||
{
|
{
|
||||||
|
// 工程管理
|
||||||
|
v1Auth.GET("/projects", handler.ListProjects) // 获取工程列表
|
||||||
|
v1Auth.POST("/projects", handler.CreateProject) // 创建工程
|
||||||
|
v1Auth.GET("/projects/:projectId", handler.GetProject) // 获取工程详情
|
||||||
|
v1Auth.PUT("/projects/:projectId", handler.UpdateProject) // 更新工程信息
|
||||||
|
v1Auth.DELETE("/projects/:projectId", handler.DeleteProject) // 删除工程
|
||||||
|
v1Auth.GET("/projects/:projectId/style", handler.GetStyle) // 获取工程风格
|
||||||
|
v1Auth.PUT("/projects/:projectId/style", handler.UpdateStyle) // 更新工程风格
|
||||||
|
v1Auth.GET("/projects/:projectId/tasks", handler.GetProjectTasks) // 获取工程任务列表
|
||||||
|
// 素材生成
|
||||||
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
|
v1Auth.POST("/generate", handler.Generate) // 素材生成管线
|
||||||
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
|
v1Auth.GET("/tasks/:taskId", handler.GetTask) // 查询任务
|
||||||
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
|
v1Auth.GET("/tasks/:taskId/assets", handler.GetAssets) // 查询任务素材
|
||||||
|
// 图片编辑
|
||||||
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
|
v1Auth.POST("/images/edit", handler.EditImage) // 图片编辑
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,8 @@ func Init(dsn string, models ...any) error {
|
|||||||
|
|
||||||
return DB.AutoMigrate(models...)
|
return DB.AutoMigrate(models...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDB 获取全局数据库实例。
|
||||||
|
func GetDB() *gorm.DB {
|
||||||
|
return DB
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gen2d/internal/logger"
|
||||||
|
"gen2d/internal/model"
|
||||||
|
"gen2d/internal/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateProjectRequest 创建工程请求。
|
||||||
|
type CreateProjectRequest struct {
|
||||||
|
Name string `json:"name" binding:"required,min=1,max=100"`
|
||||||
|
Style map[string]string `json:"style"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProjectRequest 更新工程请求。
|
||||||
|
type UpdateProjectRequest struct {
|
||||||
|
Name string `json:"name" binding:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStyleRequest 更新风格请求。
|
||||||
|
type UpdateStyleRequest struct {
|
||||||
|
KvPairs map[string]string `json:"kvPairs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProject 创建工程。
|
||||||
|
func CreateProject(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateProjectRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
proj, err := projectSvc.CreateProject(c.Request.Context(), userID, req.Name, req.Style)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("create project failed", "error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "创建工程失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, model.OK(proj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProjects 获取工程列表。
|
||||||
|
func ListProjects(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStr := c.DefaultQuery("page", "1")
|
||||||
|
pageSizeStr := c.DefaultQuery("pageSize", "20")
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||||
|
if err != nil || pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
result, err := projectSvc.ListProjects(c.Request.Context(), userID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("list projects failed", "error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject 获取工程详情。
|
||||||
|
func GetProject(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
proj, err := projectSvc.GetProject(c.Request.Context(), userID, uint(projectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("get project failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程详情失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(proj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProject 更新工程信息。
|
||||||
|
func UpdateProject(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateProjectRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
proj, err := projectSvc.UpdateProject(c.Request.Context(), userID, uint(projectID), req.Name)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("update project failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "更新工程信息失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(proj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProject 删除工程。
|
||||||
|
func DeleteProject(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
err = projectSvc.DeleteProject(c.Request.Context(), userID, uint(projectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("delete project failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "删除工程失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle 获取工程风格。
|
||||||
|
func GetStyle(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
style, err := projectSvc.GetStyle(c.Request.Context(), userID, uint(projectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("get style failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取工程风格失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(map[string]interface{}{
|
||||||
|
"kvPairs": style,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStyle 更新工程风格。
|
||||||
|
func UpdateStyle(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateStyleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
err = projectSvc.UpdateStyle(c.Request.Context(), userID, uint(projectID), req.KvPairs)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("update style failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "更新工程风格失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectTasks 获取工程下的任务列表。
|
||||||
|
func GetProjectTasks(c *gin.Context) {
|
||||||
|
userID := c.GetUint("userID")
|
||||||
|
if userID == 0 {
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Fail(http.StatusUnauthorized, "未认证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Param("projectId")
|
||||||
|
projectID, err := strconv.ParseUint(projectIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "无效的工程ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSvc := service.GetProjectService()
|
||||||
|
tasks, err := projectSvc.GetTasks(c.Request.Context(), userID, uint(projectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.FromCtx(c.Request.Context()).Error("get tasks failed", "error", err)
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
c.JSON(http.StatusNotFound, model.Fail(http.StatusNotFound, "工程不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "获取任务列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.OK(tasks))
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Project 工程数据模型,对应数据库表。
|
||||||
|
type Project struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint `gorm:"index" json:"-"` // 用户ID,用于权限控制
|
||||||
|
Name string `gorm:"size:100;not null" json:"name"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectStyleRecord 工程风格记录,用于存储键值对。
|
||||||
|
type ProjectStyleRecord struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
ProjectID uint `gorm:"index" json:"-"`
|
||||||
|
Key string `gorm:"size:50;not null" json:"-"`
|
||||||
|
Value string `gorm:"size:100;not null" json:"-"`
|
||||||
|
CreatedAt time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectResponse 工程响应,包含风格键值对和元数据。
|
||||||
|
type ProjectResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Style *ProjectStyleResponse `json:"style"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||||
|
TaskCount int `json:"taskCount,omitempty"`
|
||||||
|
LastActivityAt string `json:"lastActivityAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectStyleResponse 工程风格响应。
|
||||||
|
type ProjectStyleResponse struct {
|
||||||
|
KvPairs map[string]string `json:"kvPairs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectsListResponse 工程列表响应。
|
||||||
|
type ProjectsListResponse struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Projects []ProjectResponse `json:"projects"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Task 任务数据模型,对应数据库表。
|
||||||
|
type Task struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
ProjectID uint `gorm:"index" json:"-"`
|
||||||
|
Prompt string `gorm:"type:text" json:"prompt"`
|
||||||
|
AssetType string `gorm:"size:50" json:"assetType"`
|
||||||
|
Status string `gorm:"size:20;default:'pending'" json:"status"`
|
||||||
|
Stage string `gorm:"size:50" json:"stage,omitempty"`
|
||||||
|
Progress int `gorm:"default:0" json:"progress"`
|
||||||
|
RetryCount int `gorm:"default:0" json:"retryCount"`
|
||||||
|
Error string `gorm:"type:text" json:"error,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskResponse 任务响应。
|
||||||
|
type TaskResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID string `json:"projectId"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
AssetType string `json:"assetType"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Stage string `json:"stage,omitempty"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
RetryCount int `json:"retryCount"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gen2d/internal/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
projectService *ProjectService
|
||||||
|
projectServiceOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProjectService 工程管理服务。
|
||||||
|
type ProjectService struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitProjectService 初始化工程管理服务。
|
||||||
|
func InitProjectService(db *gorm.DB) {
|
||||||
|
projectServiceOnce.Do(func() {
|
||||||
|
projectService = &ProjectService{db: db}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectService 获取工程管理服务实例。
|
||||||
|
func GetProjectService() *ProjectService {
|
||||||
|
return projectService
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProject 创建工程。
|
||||||
|
func (s *ProjectService) CreateProject(ctx context.Context, userID uint, name string, style map[string]string) (*model.ProjectResponse, error) {
|
||||||
|
project := &model.Project{
|
||||||
|
UserID: userID,
|
||||||
|
Name: name,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.WithContext(ctx).Create(project).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("create project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存风格键值对
|
||||||
|
if err := s.saveStyle(ctx, project.ID, style); err != nil {
|
||||||
|
// 回滚工程创建
|
||||||
|
s.db.WithContext(ctx).Delete(project)
|
||||||
|
return nil, fmt.Errorf("save style failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.toProjectResponse(project, style), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProjects 获取用户工程列表。
|
||||||
|
func (s *ProjectService) ListProjects(ctx context.Context, userID uint, page, pageSize int) (*model.ProjectsListResponse, error) {
|
||||||
|
var projects []model.Project
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
if err := s.db.WithContext(ctx).Model(&model.Project{}).Where("user_id = ?", userID).Count(&total).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("count projects failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&projects).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("list projects failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &model.ProjectsListResponse{
|
||||||
|
Total: int(total),
|
||||||
|
Projects: make([]model.ProjectResponse, len(projects)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range projects {
|
||||||
|
style, _ := s.getStyle(ctx, p.ID)
|
||||||
|
response.Projects[i] = *s.toProjectResponse(&p, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject 获取工程详情。
|
||||||
|
func (s *ProjectService) GetProject(ctx context.Context, userID uint, projectID uint) (*model.ProjectResponse, error) {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("project not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style, _ := s.getStyle(ctx, project.ID)
|
||||||
|
return s.toProjectResponse(&project, style), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProject 更新工程信息。
|
||||||
|
func (s *ProjectService) UpdateProject(ctx context.Context, userID uint, projectID uint, name string) (*model.ProjectResponse, error) {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("project not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Name = name
|
||||||
|
|
||||||
|
if err := s.db.WithContext(ctx).Save(&project).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("update project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style, _ := s.getStyle(ctx, project.ID)
|
||||||
|
return s.toProjectResponse(&project, style), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProject 删除工程。
|
||||||
|
func (s *ProjectService) DeleteProject(ctx context.Context, userID uint, projectID uint) error {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("project not found")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 删除风格记录
|
||||||
|
if err := tx.Where("project_id = ?", projectID).Delete(&model.ProjectStyleRecord{}).Error; err != nil {
|
||||||
|
return fmt.Errorf("delete style records failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 删除任务和素材记录
|
||||||
|
// TODO: 删除七牛云对象
|
||||||
|
|
||||||
|
// 删除工程
|
||||||
|
if err := tx.Delete(&project).Error; err != nil {
|
||||||
|
return fmt.Errorf("delete project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle 获取工程风格。
|
||||||
|
func (s *ProjectService) GetStyle(ctx context.Context, userID uint, projectID uint) (map[string]string, error) {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("project not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style, _ := s.getStyle(ctx, project.ID)
|
||||||
|
return style, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStyle 更新工程风格。
|
||||||
|
func (s *ProjectService) UpdateStyle(ctx context.Context, userID uint, projectID uint, kvPairs map[string]string) error {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("project not found")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.saveStyle(ctx, projectID, kvPairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTasks 获取工程下的任务列表。
|
||||||
|
func (s *ProjectService) GetTasks(ctx context.Context, userID uint, projectID uint) ([]model.TaskResponse, error) {
|
||||||
|
var project model.Project
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", projectID, userID).First(&project).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("project not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get project failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks []model.Task
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("project_id = ?", projectID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&tasks).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("get tasks failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make([]model.TaskResponse, len(tasks))
|
||||||
|
for i, t := range tasks {
|
||||||
|
response[i] = model.TaskResponse{
|
||||||
|
ID: fmt.Sprintf("%d", t.ID),
|
||||||
|
ProjectID: fmt.Sprintf("%d", t.ProjectID),
|
||||||
|
Prompt: t.Prompt,
|
||||||
|
AssetType: t.AssetType,
|
||||||
|
Status: t.Status,
|
||||||
|
Stage: t.Stage,
|
||||||
|
Progress: t.Progress,
|
||||||
|
RetryCount: t.RetryCount,
|
||||||
|
Error: t.Error,
|
||||||
|
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProjectService) saveStyle(ctx context.Context, projectID uint, style map[string]string) error {
|
||||||
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 删除现有风格记录
|
||||||
|
if err := tx.Where("project_id = ?", projectID).Delete(&model.ProjectStyleRecord{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入新记录
|
||||||
|
for key, value := range style {
|
||||||
|
record := &model.ProjectStyleRecord{
|
||||||
|
ProjectID: projectID,
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
if err := tx.Create(record).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProjectService) getStyle(ctx context.Context, projectID uint) (map[string]string, error) {
|
||||||
|
var records []model.ProjectStyleRecord
|
||||||
|
if err := s.db.WithContext(ctx).Where("project_id = ?", projectID).Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
style := make(map[string]string)
|
||||||
|
for _, r := range records {
|
||||||
|
style[r.Key] = r.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return style, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProjectService) toProjectResponse(project *model.Project, style map[string]string) *model.ProjectResponse {
|
||||||
|
// 获取任务数量
|
||||||
|
var taskCount int64
|
||||||
|
s.db.Model(&model.Task{}).Where("project_id = ?", project.ID).Count(&taskCount)
|
||||||
|
|
||||||
|
styleResp := &model.ProjectStyleResponse{
|
||||||
|
KvPairs: style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.ProjectResponse{
|
||||||
|
ID: fmt.Sprintf("%d", project.ID),
|
||||||
|
Name: project.Name,
|
||||||
|
Style: styleResp,
|
||||||
|
CreatedAt: project.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: project.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
TaskCount: int(taskCount),
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -217,12 +217,12 @@ POST /api/v1/prompt/optimize
|
|||||||
|
|
||||||
| 状态 | 方法 | 路径 | 说明 |
|
| 状态 | 方法 | 路径 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| [ ] | GET | `/api/v1/projects` | 获取当前用户的工程列表(前端已对接 mock) |
|
| [x] | GET | `/api/v1/projects` | 获取当前用户的工程列表(前端已对接 mock) |
|
||||||
| [ ] | POST | `/api/v1/projects` | 创建工程(前端已对接 mock) |
|
| [x] | POST | `/api/v1/projects` | 创建工程(前端已对接 mock) |
|
||||||
| [ ] | GET | `/api/v1/projects/:projectId` | 获取工程信息 |
|
| [x] | GET | `/api/v1/projects/:projectId` | 获取工程信息 |
|
||||||
| [ ] | PUT | `/api/v1/projects/:projectId` | 更新工程信息(如名称) |
|
| [x] | PUT | `/api/v1/projects/:projectId` | 更新工程信息(如名称) |
|
||||||
| [ ] | DELETE | `/api/v1/projects/:projectId` | 删除工程(级联删除任务和素材) |
|
| [x] | DELETE | `/api/v1/projects/:projectId` | 删除工程(级联删除任务和素材) |
|
||||||
| [ ] | GET | `/api/v1/projects/:projectId/tasks` | 获取工程下的任务列表 |
|
| [x] | GET | `/api/v1/projects/:projectId/tasks` | 获取工程下的任务列表 |
|
||||||
|
|
||||||
### GET /api/v1/projects
|
### GET /api/v1/projects
|
||||||
|
|
||||||
@@ -355,8 +355,8 @@ POST /api/v1/prompt/optimize
|
|||||||
|
|
||||||
| 状态 | 方法 | 路径 | 说明 |
|
| 状态 | 方法 | 路径 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| [ ] | GET | `/api/v1/projects/:projectId/style` | 获取工程风格 |
|
| [x] | GET | `/api/v1/projects/:projectId/style` | 获取工程风格 |
|
||||||
| [ ] | PUT | `/api/v1/projects/:projectId/style` | 更新工程风格 |
|
| [x] | PUT | `/api/v1/projects/:projectId/style` | 更新工程风格 |
|
||||||
|
|
||||||
请求体示例 (PUT /api/v1/projects/:projectId/style):
|
请求体示例 (PUT /api/v1/projects/:projectId/style):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { post } from './client'
|
||||||
|
|
||||||
|
export interface EditImageRequest {
|
||||||
|
image: string // base64 编码
|
||||||
|
prompt: string
|
||||||
|
count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditAssetResponse {
|
||||||
|
data: string // base64 编码
|
||||||
|
format: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditImageResponse {
|
||||||
|
assets: EditAssetResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editImage(
|
||||||
|
req: EditImageRequest,
|
||||||
|
): Promise<EditImageResponse> {
|
||||||
|
return post<EditImageResponse>('/api/v1/images/edit', req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { get } from './client'
|
||||||
|
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHealth(): Promise<HealthResponse> {
|
||||||
|
return get<HealthResponse>('/api/v1/health')
|
||||||
|
}
|
||||||
+25
-29
@@ -1,64 +1,60 @@
|
|||||||
import type { Project, Task } from './types'
|
import type { Project, Task } from './types'
|
||||||
import {
|
import { get, post, put, del } from './client'
|
||||||
mockListProjects,
|
|
||||||
mockGetProject,
|
|
||||||
mockCreateProject,
|
|
||||||
mockDeleteProject,
|
|
||||||
mockUpdateProject,
|
|
||||||
mockGetStyle,
|
|
||||||
mockSaveStyle,
|
|
||||||
mockGetTasks,
|
|
||||||
} from './mock'
|
|
||||||
|
|
||||||
const USE_MOCK = true
|
export async function listProjects(
|
||||||
|
page?: number,
|
||||||
|
pageSize?: number
|
||||||
|
): Promise<Project[]> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (page !== undefined) params.set('page', String(page))
|
||||||
|
if (pageSize !== undefined) params.set('pageSize', String(pageSize))
|
||||||
|
|
||||||
export async function listProjects(): Promise<Project[]> {
|
const url = `/api/v1/projects${params.toString() ? '?' + params : ''}`
|
||||||
if (USE_MOCK) return mockListProjects()
|
const resp = await get<{
|
||||||
throw new Error('Not implemented')
|
total: number
|
||||||
|
projects: Project[]
|
||||||
|
}>(url)
|
||||||
|
return resp.projects
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProject(projectId: string): Promise<Project> {
|
export async function getProject(projectId: string): Promise<Project> {
|
||||||
if (USE_MOCK) return mockGetProject(projectId)
|
return get<Project>(`/api/v1/projects/${projectId}`)
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProject(
|
export async function createProject(
|
||||||
name: string,
|
name: string,
|
||||||
style?: Record<string, string>
|
style?: Record<string, string>
|
||||||
): Promise<Project> {
|
): Promise<Project> {
|
||||||
if (USE_MOCK) return mockCreateProject(name, style)
|
return post<Project>('/api/v1/projects', { name, style })
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProject(projectId: string): Promise<void> {
|
export async function deleteProject(projectId: string): Promise<void> {
|
||||||
if (USE_MOCK) return mockDeleteProject(projectId)
|
return del<void>(`/api/v1/projects/${projectId}`)
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProject(
|
export async function updateProject(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: { name?: string }
|
data: { name: string }
|
||||||
): Promise<Project> {
|
): Promise<Project> {
|
||||||
if (USE_MOCK) return mockUpdateProject(projectId, data)
|
return put<Project>(`/api/v1/projects/${projectId}`, data)
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStyle(
|
export async function getStyle(
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
if (USE_MOCK) return mockGetStyle(projectId)
|
const resp = await get<{ kvPairs: Record<string, string> }>(
|
||||||
throw new Error('Not implemented')
|
`/api/v1/projects/${projectId}/style`
|
||||||
|
)
|
||||||
|
return resp.kvPairs
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveStyle(
|
export async function saveStyle(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kvPairs: Record<string, string>
|
kvPairs: Record<string, string>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (USE_MOCK) return mockSaveStyle(projectId, kvPairs)
|
return put<void>(`/api/v1/projects/${projectId}/style`, { kvPairs })
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTasks(projectId: string): Promise<Task[]> {
|
export async function getTasks(projectId: string): Promise<Task[]> {
|
||||||
if (USE_MOCK) return mockGetTasks(projectId)
|
return get<Task[]>(`/api/v1/projects/${projectId}/tasks`)
|
||||||
throw new Error('Not implemented')
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user