create project
This commit is contained in:
136
api/handlers/ai_config.go
Normal file
136
api/handlers/ai_config.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AIConfigHandler struct {
|
||||
aiService *services.AIService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewAIConfigHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *AIConfigHandler {
|
||||
return &AIConfigHandler{
|
||||
aiService: services.NewAIService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) CreateConfig(c *gin.Context) {
|
||||
var req services.CreateAIConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.aiService.CreateConfig(&req)
|
||||
if err != nil {
|
||||
response.InternalError(c, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, config)
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) GetConfig(c *gin.Context) {
|
||||
|
||||
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的配置ID")
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.aiService.GetConfig(uint(configID))
|
||||
if err != nil {
|
||||
if err.Error() == "config not found" {
|
||||
response.NotFound(c, "配置不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, config)
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) ListConfigs(c *gin.Context) {
|
||||
|
||||
serviceType := c.Query("service_type")
|
||||
|
||||
configs, err := h.aiService.ListConfigs(serviceType)
|
||||
if err != nil {
|
||||
response.InternalError(c, "获取列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, configs)
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
|
||||
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的配置ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req services.UpdateAIConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.aiService.UpdateConfig(uint(configID), &req)
|
||||
if err != nil {
|
||||
if err.Error() == "config not found" {
|
||||
response.NotFound(c, "配置不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, config)
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) DeleteConfig(c *gin.Context) {
|
||||
|
||||
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的配置ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.aiService.DeleteConfig(uint(configID)); err != nil {
|
||||
if err.Error() == "config not found" {
|
||||
response.NotFound(c, "配置不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func (h *AIConfigHandler) TestConnection(c *gin.Context) {
|
||||
var req services.TestConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.aiService.TestConnection(&req); err != nil {
|
||||
response.BadRequest(c, "连接测试失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "连接测试成功"})
|
||||
}
|
||||
220
api/handlers/asset.go
Normal file
220
api/handlers/asset.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/domain/models"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AssetHandler struct {
|
||||
assetService *services.AssetService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewAssetHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *AssetHandler {
|
||||
return &AssetHandler{
|
||||
assetService: services.NewAssetService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AssetHandler) CreateAsset(c *gin.Context) {
|
||||
|
||||
var req services.CreateAssetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := h.assetService.CreateAsset(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to create asset", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, asset)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) UpdateAsset(c *gin.Context) {
|
||||
|
||||
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req services.UpdateAssetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := h.assetService.UpdateAsset(uint(assetID), &req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to update asset", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, asset)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) GetAsset(c *gin.Context) {
|
||||
|
||||
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := h.assetService.GetAsset(uint(assetID))
|
||||
if err != nil {
|
||||
response.NotFound(c, "素材不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, asset)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) ListAssets(c *gin.Context) {
|
||||
|
||||
var dramaID *string
|
||||
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
|
||||
dramaID = &dramaIDStr
|
||||
}
|
||||
|
||||
var episodeID *uint
|
||||
if episodeIDStr := c.Query("episode_id"); episodeIDStr != "" {
|
||||
if id, err := strconv.ParseUint(episodeIDStr, 10, 32); err == nil {
|
||||
uid := uint(id)
|
||||
episodeID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
var storyboardID *uint
|
||||
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
|
||||
if id, err := strconv.ParseUint(storyboardIDStr, 10, 32); err == nil {
|
||||
uid := uint(id)
|
||||
storyboardID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
var assetType *models.AssetType
|
||||
if typeStr := c.Query("type"); typeStr != "" {
|
||||
t := models.AssetType(typeStr)
|
||||
assetType = &t
|
||||
}
|
||||
|
||||
var isFavorite *bool
|
||||
if favoriteStr := c.Query("is_favorite"); favoriteStr != "" {
|
||||
if favoriteStr == "true" {
|
||||
fav := true
|
||||
isFavorite = &fav
|
||||
} else if favoriteStr == "false" {
|
||||
fav := false
|
||||
isFavorite = &fav
|
||||
}
|
||||
}
|
||||
|
||||
var tagIDs []uint
|
||||
if tagIDsStr := c.Query("tag_ids"); tagIDsStr != "" {
|
||||
for _, idStr := range strings.Split(tagIDsStr, ",") {
|
||||
if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil {
|
||||
tagIDs = append(tagIDs, uint(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
req := &services.ListAssetsRequest{
|
||||
DramaID: dramaID,
|
||||
EpisodeID: episodeID,
|
||||
StoryboardID: storyboardID,
|
||||
Type: assetType,
|
||||
Category: c.Query("category"),
|
||||
TagIDs: tagIDs,
|
||||
IsFavorite: isFavorite,
|
||||
Search: c.Query("search"),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
assets, total, err := h.assetService.ListAssets(req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to list assets", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithPagination(c, assets, total, page, pageSize)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) DeleteAsset(c *gin.Context) {
|
||||
|
||||
assetID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.assetService.DeleteAsset(uint(assetID)); err != nil {
|
||||
h.log.Errorw("Failed to delete asset", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) ImportFromImageGen(c *gin.Context) {
|
||||
|
||||
imageGenID, err := strconv.ParseUint(c.Param("image_gen_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := h.assetService.ImportFromImageGen(uint(imageGenID))
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to import from image gen", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, asset)
|
||||
}
|
||||
|
||||
func (h *AssetHandler) ImportFromVideoGen(c *gin.Context) {
|
||||
|
||||
videoGenID, err := strconv.ParseUint(c.Param("video_gen_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := h.assetService.ImportFromVideoGen(uint(videoGenID))
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to import from video gen", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, asset)
|
||||
}
|
||||
34
api/handlers/character_batch.go
Normal file
34
api/handlers/character_batch.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BatchGenerateCharacterImages 批量生成角色图片
|
||||
func (h *CharacterLibraryHandler) BatchGenerateCharacterImages(c *gin.Context) {
|
||||
|
||||
var req struct {
|
||||
CharacterIDs []string `json:"character_ids" binding:"required,min=1"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 限制批量生成数量
|
||||
if len(req.CharacterIDs) > 10 {
|
||||
response.BadRequest(c, "单次最多生成10个角色")
|
||||
return
|
||||
}
|
||||
|
||||
// 异步批量生成
|
||||
go h.libraryService.BatchGenerateCharacterImages(req.CharacterIDs, h.imageService, req.Model)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "批量生成任务已提交",
|
||||
"count": len(req.CharacterIDs),
|
||||
})
|
||||
}
|
||||
275
api/handlers/character_library.go
Normal file
275
api/handlers/character_library.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
services2 "github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/infrastructure/storage"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CharacterLibraryHandler struct {
|
||||
libraryService *services2.CharacterLibraryService
|
||||
imageService *services2.ImageGenerationService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewCharacterLibraryHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services2.ResourceTransferService, localStorage *storage.LocalStorage) *CharacterLibraryHandler {
|
||||
return &CharacterLibraryHandler{
|
||||
libraryService: services2.NewCharacterLibraryService(db, log),
|
||||
imageService: services2.NewImageGenerationService(db, transferService, localStorage, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// ListLibraryItems 获取角色库列表
|
||||
func (h *CharacterLibraryHandler) ListLibraryItems(c *gin.Context) {
|
||||
|
||||
var query services2.CharacterLibraryQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 || query.PageSize > 100 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
|
||||
items, total, err := h.libraryService.ListLibraryItems(&query)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to list library items", "error", err)
|
||||
response.InternalError(c, "获取角色库失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithPagination(c, items, total, query.Page, query.PageSize)
|
||||
}
|
||||
|
||||
// CreateLibraryItem 添加到角色库
|
||||
func (h *CharacterLibraryHandler) CreateLibraryItem(c *gin.Context) {
|
||||
|
||||
var req services2.CreateLibraryItemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
item, err := h.libraryService.CreateLibraryItem(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to create library item", "error", err)
|
||||
response.InternalError(c, "添加到角色库失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
// GetLibraryItem 获取角色库项详情
|
||||
func (h *CharacterLibraryHandler) GetLibraryItem(c *gin.Context) {
|
||||
|
||||
itemID := c.Param("id")
|
||||
|
||||
item, err := h.libraryService.GetLibraryItem(itemID)
|
||||
if err != nil {
|
||||
if err.Error() == "library item not found" {
|
||||
response.NotFound(c, "角色库项不存在")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to get library item", "error", err)
|
||||
response.InternalError(c, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, item)
|
||||
}
|
||||
|
||||
// DeleteLibraryItem 删除角色库项
|
||||
func (h *CharacterLibraryHandler) DeleteLibraryItem(c *gin.Context) {
|
||||
|
||||
itemID := c.Param("id")
|
||||
|
||||
if err := h.libraryService.DeleteLibraryItem(itemID); err != nil {
|
||||
if err.Error() == "library item not found" {
|
||||
response.NotFound(c, "角色库项不存在")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to delete library item", "error", err)
|
||||
response.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
// UploadCharacterImage 上传角色图片
|
||||
func (h *CharacterLibraryHandler) UploadCharacterImage(c *gin.Context) {
|
||||
|
||||
characterID := c.Param("id")
|
||||
|
||||
// TODO: 处理文件上传
|
||||
// 这里需要实现文件上传逻辑,保存到OSS或本地
|
||||
// 暂时使用简单的实现
|
||||
var req struct {
|
||||
ImageURL string `json:"image_url" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.libraryService.UploadCharacterImage(characterID, req.ImageURL); err != nil {
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权限")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to upload character image", "error", err)
|
||||
response.InternalError(c, "上传失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "上传成功"})
|
||||
}
|
||||
|
||||
// ApplyLibraryItemToCharacter 从角色库应用形象
|
||||
func (h *CharacterLibraryHandler) ApplyLibraryItemToCharacter(c *gin.Context) {
|
||||
|
||||
characterID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
LibraryItemID string `json:"library_item_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.libraryService.ApplyLibraryItemToCharacter(characterID, req.LibraryItemID); err != nil {
|
||||
if err.Error() == "library item not found" {
|
||||
response.NotFound(c, "角色库项不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权限")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to apply library item", "error", err)
|
||||
response.InternalError(c, "应用失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "应用成功"})
|
||||
}
|
||||
|
||||
// AddCharacterToLibrary 将角色添加到角色库
|
||||
func (h *CharacterLibraryHandler) AddCharacterToLibrary(c *gin.Context) {
|
||||
|
||||
characterID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Category *string `json:"category"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// 允许空body
|
||||
req.Category = nil
|
||||
}
|
||||
|
||||
item, err := h.libraryService.AddCharacterToLibrary(characterID, req.Category)
|
||||
if err != nil {
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权限")
|
||||
return
|
||||
}
|
||||
if err.Error() == "character has no image" {
|
||||
response.BadRequest(c, "角色还没有形象图片")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to add character to library", "error", err)
|
||||
response.InternalError(c, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
// UpdateCharacter 更新角色信息
|
||||
func (h *CharacterLibraryHandler) UpdateCharacter(c *gin.Context) {
|
||||
|
||||
characterID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Appearance *string `json:"appearance"`
|
||||
Personality *string `json:"personality"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.libraryService.UpdateCharacter(characterID, &req); err != nil {
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权限")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to update character", "error", err)
|
||||
response.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "更新成功"})
|
||||
}
|
||||
|
||||
// DeleteCharacter 删除单个角色
|
||||
func (h *CharacterLibraryHandler) DeleteCharacter(c *gin.Context) {
|
||||
|
||||
characterIDStr := c.Param("id")
|
||||
characterID, err := strconv.ParseUint(characterIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的角色ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.libraryService.DeleteCharacter(uint(characterID)); err != nil {
|
||||
h.log.Errorw("Failed to delete character", "error", err, "id", characterID)
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权删除此角色")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "角色已删除"})
|
||||
}
|
||||
38
api/handlers/character_library_gen.go
Normal file
38
api/handlers/character_library_gen.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GenerateCharacterImage AI生成角色形象
|
||||
func (h *CharacterLibraryHandler) GenerateCharacterImage(c *gin.Context) {
|
||||
|
||||
characterID := c.Param("id")
|
||||
|
||||
// 获取请求体中的model参数
|
||||
var req struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
imageGen, err := h.libraryService.GenerateCharacterImage(characterID, h.imageService, req.Model)
|
||||
if err != nil {
|
||||
if err.Error() == "character not found" {
|
||||
response.NotFound(c, "角色不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Forbidden(c, "无权限")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to generate character image", "error", err)
|
||||
response.InternalError(c, "生成失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "角色图片生成已启动",
|
||||
"image_generation": imageGen,
|
||||
})
|
||||
}
|
||||
310
api/handlers/drama.go
Normal file
310
api/handlers/drama.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/domain/models"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DramaHandler struct {
|
||||
db *gorm.DB
|
||||
dramaService *services.DramaService
|
||||
videoMergeService *services.VideoMergeService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewDramaHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService) *DramaHandler {
|
||||
return &DramaHandler{
|
||||
db: db,
|
||||
dramaService: services.NewDramaService(db, log),
|
||||
videoMergeService: services.NewVideoMergeService(db, transferService, cfg.Storage.LocalPath, cfg.Storage.BaseURL, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DramaHandler) CreateDrama(c *gin.Context) {
|
||||
|
||||
var req services.CreateDramaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
drama, err := h.dramaService.CreateDrama(&req)
|
||||
if err != nil {
|
||||
response.InternalError(c, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, drama)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) GetDrama(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
drama, err := h.dramaService.GetDrama(dramaID)
|
||||
if err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, drama)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) ListDramas(c *gin.Context) {
|
||||
|
||||
var query services.DramaListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 || query.PageSize > 100 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
|
||||
dramas, total, err := h.dramaService.ListDramas(&query)
|
||||
if err != nil {
|
||||
response.InternalError(c, "获取列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithPagination(c, dramas, total, query.Page, query.PageSize)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) UpdateDrama(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
var req services.UpdateDramaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
drama, err := h.dramaService.UpdateDrama(dramaID, &req)
|
||||
if err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, drama)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) DeleteDrama(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
if err := h.dramaService.DeleteDrama(dramaID); err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
|
||||
func (h *DramaHandler) GetDramaStats(c *gin.Context) {
|
||||
|
||||
stats, err := h.dramaService.GetDramaStats()
|
||||
if err != nil {
|
||||
response.InternalError(c, "获取统计失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, stats)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) SaveOutline(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
var req services.SaveOutlineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.dramaService.SaveOutline(dramaID, &req); err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "保存成功"})
|
||||
}
|
||||
|
||||
func (h *DramaHandler) GetCharacters(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
episodeID := c.Query("episode_id") // 可选:如果提供则只返回该章节的角色
|
||||
|
||||
var episodeIDPtr *string
|
||||
if episodeID != "" {
|
||||
episodeIDPtr = &episodeID
|
||||
}
|
||||
|
||||
characters, err := h.dramaService.GetCharacters(dramaID, episodeIDPtr)
|
||||
if err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
if err.Error() == "episode not found" {
|
||||
response.NotFound(c, "章节不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "获取角色失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, characters)
|
||||
}
|
||||
|
||||
func (h *DramaHandler) SaveCharacters(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
var req services.SaveCharactersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.dramaService.SaveCharacters(dramaID, &req); err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "保存成功"})
|
||||
}
|
||||
|
||||
func (h *DramaHandler) SaveEpisodes(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
var req services.SaveEpisodesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.dramaService.SaveEpisodes(dramaID, &req); err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "保存成功"})
|
||||
}
|
||||
|
||||
func (h *DramaHandler) SaveProgress(c *gin.Context) {
|
||||
|
||||
dramaID := c.Param("id")
|
||||
|
||||
var req services.SaveProgressRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.dramaService.SaveProgress(dramaID, &req); err != nil {
|
||||
if err.Error() == "drama not found" {
|
||||
response.NotFound(c, "剧本不存在")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "保存成功"})
|
||||
}
|
||||
|
||||
// FinalizeEpisode 完成集数制作(触发视频合成)
|
||||
func (h *DramaHandler) FinalizeEpisode(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
if episodeID == "" {
|
||||
response.BadRequest(c, "episode_id不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试读取时间线数据(可选)
|
||||
var timelineData *services.FinalizeEpisodeRequest
|
||||
if err := c.ShouldBindJSON(&timelineData); err != nil {
|
||||
// 如果没有请求体或解析失败,使用nil(将使用默认场景顺序)
|
||||
h.log.Warnw("No timeline data provided, will use default scene order", "error", err)
|
||||
timelineData = nil
|
||||
} else if timelineData != nil {
|
||||
h.log.Infow("Received timeline data", "clips_count", len(timelineData.Clips), "episode_id", episodeID)
|
||||
}
|
||||
|
||||
// 触发视频合成任务
|
||||
result, err := h.videoMergeService.FinalizeEpisode(episodeID, timelineData)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to finalize episode", "error", err, "episode_id", episodeID)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// DownloadEpisodeVideo 下载剧集视频
|
||||
func (h *DramaHandler) DownloadEpisodeVideo(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
if episodeID == "" {
|
||||
response.BadRequest(c, "episode_id不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询episode
|
||||
var episode models.Episode
|
||||
if err := h.db.Preload("Drama").Where("id = ?", episodeID).First(&episode).Error; err != nil {
|
||||
response.NotFound(c, "剧集不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有视频
|
||||
if episode.VideoURL == nil || *episode.VideoURL == "" {
|
||||
response.BadRequest(c, "该剧集还没有生成视频")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回视频URL,让前端重定向下载
|
||||
c.JSON(200, gin.H{
|
||||
"video_url": *episode.VideoURL,
|
||||
"title": episode.Title,
|
||||
"episode_number": episode.EpisodeNum,
|
||||
})
|
||||
}
|
||||
55
api/handlers/frame_prompt.go
Normal file
55
api/handlers/frame_prompt.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FramePromptHandler 处理帧提示词生成请求
|
||||
type FramePromptHandler struct {
|
||||
framePromptService *services.FramePromptService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
// NewFramePromptHandler 创建帧提示词处理器
|
||||
func NewFramePromptHandler(framePromptService *services.FramePromptService, log *logger.Logger) *FramePromptHandler {
|
||||
return &FramePromptHandler{
|
||||
framePromptService: framePromptService,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateFramePrompt 生成指定类型的帧提示词
|
||||
// POST /api/v1/storyboards/:id/frame-prompt
|
||||
func (h *FramePromptHandler) GenerateFramePrompt(c *gin.Context) {
|
||||
storyboardID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
FrameType string `json:"frame_type" binding:"required"` // first, key, last, panel, action
|
||||
PanelCount int `json:"panel_count"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
serviceReq := services.GenerateFramePromptRequest{
|
||||
StoryboardID: storyboardID,
|
||||
FrameType: services.FrameType(req.FrameType),
|
||||
PanelCount: req.PanelCount,
|
||||
}
|
||||
|
||||
// 生成提示词
|
||||
result, err := h.framePromptService.GenerateFramePrompt(serviceReq)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate frame prompt", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
30
api/handlers/frame_prompt_query.go
Normal file
30
api/handlers/frame_prompt_query.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/domain/models"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetStoryboardFramePrompts 查询镜头的所有帧提示词
|
||||
// GET /api/v1/storyboards/:id/frame-prompts
|
||||
func GetStoryboardFramePrompts(db *gorm.DB, log *logger.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
storyboardID := c.Param("id")
|
||||
|
||||
var framePrompts []models.FramePrompt
|
||||
if err := db.Where("storyboard_id = ?", storyboardID).
|
||||
Order("created_at DESC").
|
||||
Find(&framePrompts).Error; err != nil {
|
||||
log.Errorw("Failed to query frame prompts", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"frame_prompts": framePrompts,
|
||||
})
|
||||
}
|
||||
}
|
||||
224
api/handlers/image_generation.go
Normal file
224
api/handlers/image_generation.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/infrastructure/storage"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ImageGenerationHandler struct {
|
||||
imageService *services.ImageGenerationService
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewImageGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage) *ImageGenerationHandler {
|
||||
return &ImageGenerationHandler{
|
||||
imageService: services.NewImageGenerationService(db, transferService, localStorage, log),
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) GenerateImage(c *gin.Context) {
|
||||
|
||||
var req services.GenerateImageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
imageGen, err := h.imageService.GenerateImage(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate image", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, imageGen)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) GenerateImagesForScene(c *gin.Context) {
|
||||
|
||||
sceneID := c.Param("scene_id")
|
||||
|
||||
images, err := h.imageService.GenerateImagesForScene(sceneID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate images for scene", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, images)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) GetBackgroundsForEpisode(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
backgrounds, err := h.imageService.GetScencesForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to get backgrounds", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, backgrounds)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) ExtractBackgroundsForEpisode(c *gin.Context) {
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
// 创建异步任务
|
||||
task, err := h.taskService.CreateTask("background_extraction", episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to create task", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 启动后台goroutine处理
|
||||
go h.processBackgroundExtraction(task.ID, episodeID)
|
||||
|
||||
// 立即返回任务ID
|
||||
response.Success(c, gin.H{
|
||||
"task_id": task.ID,
|
||||
"status": "pending",
|
||||
"message": "场景提取任务已创建,正在后台处理...",
|
||||
})
|
||||
}
|
||||
|
||||
// processBackgroundExtraction 后台处理场景提取
|
||||
func (h *ImageGenerationHandler) processBackgroundExtraction(taskID, episodeID string) {
|
||||
h.log.Infow("Starting background extraction", "task_id", taskID, "episode_id", episodeID)
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始提取场景..."); err != nil {
|
||||
h.log.Errorw("Failed to update task status", "error", err)
|
||||
}
|
||||
|
||||
// 调用实际的提取逻辑
|
||||
backgrounds, err := h.imageService.ExtractBackgroundsForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to extract backgrounds", "error", err, "task_id", taskID)
|
||||
if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil {
|
||||
h.log.Errorw("Failed to update task error", "error", updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务结果
|
||||
result := gin.H{
|
||||
"backgrounds": backgrounds,
|
||||
"total": len(backgrounds),
|
||||
}
|
||||
if err := h.taskService.UpdateTaskResult(taskID, result); err != nil {
|
||||
h.log.Errorw("Failed to update task result", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Background extraction completed", "task_id", taskID, "total", len(backgrounds))
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) BatchGenerateForEpisode(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
images, err := h.imageService.BatchGenerateImagesForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to batch generate images", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, images)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) GetImageGeneration(c *gin.Context) {
|
||||
|
||||
imageGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
imageGen, err := h.imageService.GetImageGeneration(uint(imageGenID))
|
||||
if err != nil {
|
||||
response.NotFound(c, "图片生成记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, imageGen)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) ListImageGenerations(c *gin.Context) {
|
||||
var sceneID *uint
|
||||
if sceneIDStr := c.Query("scene_id"); sceneIDStr != "" {
|
||||
id, err := strconv.ParseUint(sceneIDStr, 10, 32)
|
||||
if err == nil {
|
||||
uid := uint(id)
|
||||
sceneID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
var storyboardID *uint
|
||||
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
|
||||
id, err := strconv.ParseUint(storyboardIDStr, 10, 32)
|
||||
if err == nil {
|
||||
uid := uint(id)
|
||||
storyboardID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
frameType := c.Query("frame_type")
|
||||
status := c.Query("status")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var dramaIDUint *uint
|
||||
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
|
||||
did, _ := strconv.ParseUint(dramaIDStr, 10, 32)
|
||||
didUint := uint(did)
|
||||
dramaIDUint = &didUint
|
||||
}
|
||||
|
||||
images, total, err := h.imageService.ListImageGenerations(dramaIDUint, sceneID, storyboardID, frameType, status, page, pageSize)
|
||||
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to list images", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithPagination(c, images, total, page, pageSize)
|
||||
}
|
||||
|
||||
func (h *ImageGenerationHandler) DeleteImageGeneration(c *gin.Context) {
|
||||
|
||||
imageGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.imageService.DeleteImageGeneration(uint(imageGenID)); err != nil {
|
||||
h.log.Errorw("Failed to delete image", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
75
api/handlers/scene.go
Normal file
75
api/handlers/scene.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
services2 "github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SceneHandler struct {
|
||||
sceneService *services2.StoryboardCompositionService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewSceneHandler(db *gorm.DB, log *logger.Logger, imageGenService *services2.ImageGenerationService) *SceneHandler {
|
||||
return &SceneHandler{
|
||||
sceneService: services2.NewStoryboardCompositionService(db, log, imageGenService),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SceneHandler) GetStoryboardsForEpisode(c *gin.Context) {
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
storyboards, err := h.sceneService.GetScenesForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to get storyboards for episode", "error", err, "episode_id", episodeID)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"storyboards": storyboards,
|
||||
"total": len(storyboards),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SceneHandler) UpdateScene(c *gin.Context) {
|
||||
sceneID := c.Param("scene_id")
|
||||
|
||||
var req services2.UpdateSceneRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.sceneService.UpdateScene(sceneID, &req); err != nil {
|
||||
h.log.Errorw("Failed to update scene", "error", err, "scene_id", sceneID)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Scene updated successfully"})
|
||||
}
|
||||
|
||||
func (h *SceneHandler) GenerateSceneImage(c *gin.Context) {
|
||||
var req services2.GenerateSceneImageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
imageGen, err := h.sceneService.GenerateSceneImage(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate scene image", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "Scene image generation started",
|
||||
"image_generation": imageGen,
|
||||
})
|
||||
}
|
||||
118
api/handlers/script_generation.go
Normal file
118
api/handlers/script_generation.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ScriptGenerationHandler struct {
|
||||
scriptService *services.ScriptGenerationService
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewScriptGenerationHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *ScriptGenerationHandler {
|
||||
return &ScriptGenerationHandler{
|
||||
scriptService: services.NewScriptGenerationService(db, log),
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ScriptGenerationHandler) GenerateOutline(c *gin.Context) {
|
||||
|
||||
var req services.GenerateOutlineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.scriptService.GenerateOutline(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate outline", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
func (h *ScriptGenerationHandler) GenerateCharacters(c *gin.Context) {
|
||||
var req services.GenerateCharactersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建异步任务
|
||||
task, err := h.taskService.CreateTask("character_generation", req.DramaID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to create task", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 启动后台goroutine处理
|
||||
go h.processCharacterGeneration(task.ID, &req)
|
||||
|
||||
// 立即返回任务ID
|
||||
response.Success(c, gin.H{
|
||||
"task_id": task.ID,
|
||||
"status": "pending",
|
||||
"message": "角色生成任务已创建,正在后台处理...",
|
||||
})
|
||||
}
|
||||
|
||||
// processCharacterGeneration 后台处理角色生成
|
||||
func (h *ScriptGenerationHandler) processCharacterGeneration(taskID string, req *services.GenerateCharactersRequest) {
|
||||
h.log.Infow("Starting character generation", "task_id", taskID, "drama_id", req.DramaID)
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始生成角色..."); err != nil {
|
||||
h.log.Errorw("Failed to update task status", "error", err)
|
||||
}
|
||||
|
||||
// 调用实际的生成逻辑
|
||||
characters, err := h.scriptService.GenerateCharacters(req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate characters", "error", err, "task_id", taskID)
|
||||
if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil {
|
||||
h.log.Errorw("Failed to update task error", "error", updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务结果
|
||||
result := gin.H{
|
||||
"characters": characters,
|
||||
"total": len(characters),
|
||||
}
|
||||
if err := h.taskService.UpdateTaskResult(taskID, result); err != nil {
|
||||
h.log.Errorw("Failed to update task result", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Character generation completed", "task_id", taskID, "total", len(characters))
|
||||
}
|
||||
|
||||
func (h *ScriptGenerationHandler) GenerateEpisodes(c *gin.Context) {
|
||||
|
||||
var req services.GenerateEpisodesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
episodes, err := h.scriptService.GenerateEpisodes(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate episodes", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, episodes)
|
||||
}
|
||||
96
api/handlers/storyboard.go
Normal file
96
api/handlers/storyboard.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StoryboardHandler struct {
|
||||
storyboardService *services.StoryboardService
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewStoryboardHandler(db *gorm.DB, cfg *config.Config, log *logger.Logger) *StoryboardHandler {
|
||||
return &StoryboardHandler{
|
||||
storyboardService: services.NewStoryboardService(db, log),
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateStoryboard 生成分镜头(异步)
|
||||
func (h *StoryboardHandler) GenerateStoryboard(c *gin.Context) {
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
// 创建异步任务
|
||||
task, err := h.taskService.CreateTask("storyboard_generation", episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to create task", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 启动后台goroutine处理
|
||||
go h.processStoryboardGeneration(task.ID, episodeID)
|
||||
|
||||
// 立即返回任务ID
|
||||
response.Success(c, gin.H{
|
||||
"task_id": task.ID,
|
||||
"status": "pending",
|
||||
"message": "分镜生成任务已创建,正在后台处理...",
|
||||
})
|
||||
}
|
||||
|
||||
// processStoryboardGeneration 后台处理分镜生成
|
||||
func (h *StoryboardHandler) processStoryboardGeneration(taskID, episodeID string) {
|
||||
h.log.Infow("Starting storyboard generation", "task_id", taskID, "episode_id", episodeID)
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := h.taskService.UpdateTaskStatus(taskID, "processing", 10, "开始生成分镜..."); err != nil {
|
||||
h.log.Errorw("Failed to update task status", "error", err)
|
||||
}
|
||||
|
||||
// 调用实际的生成逻辑
|
||||
result, err := h.storyboardService.GenerateStoryboard(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate storyboard", "error", err, "task_id", taskID)
|
||||
if updateErr := h.taskService.UpdateTaskError(taskID, err); updateErr != nil {
|
||||
h.log.Errorw("Failed to update task error", "error", updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务结果
|
||||
if err := h.taskService.UpdateTaskResult(taskID, result); err != nil {
|
||||
h.log.Errorw("Failed to update task result", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Storyboard generation completed", "task_id", taskID, "total", result.Total)
|
||||
}
|
||||
|
||||
// UpdateStoryboard 更新分镜
|
||||
func (h *StoryboardHandler) UpdateStoryboard(c *gin.Context) {
|
||||
storyboardID := c.Param("id")
|
||||
|
||||
var req map[string]interface{}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.storyboardService.UpdateStoryboard(storyboardID, req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to update storyboard", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Storyboard updated successfully"})
|
||||
}
|
||||
57
api/handlers/task.go
Normal file
57
api/handlers/task.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TaskHandler struct {
|
||||
taskService *services.TaskService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewTaskHandler(db *gorm.DB, log *logger.Logger) *TaskHandler {
|
||||
return &TaskHandler{
|
||||
taskService: services.NewTaskService(db, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (h *TaskHandler) GetTaskStatus(c *gin.Context) {
|
||||
taskID := c.Param("task_id")
|
||||
|
||||
task, err := h.taskService.GetTask(taskID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
response.NotFound(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
h.log.Errorw("Failed to get task", "error", err, "task_id", taskID)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, task)
|
||||
}
|
||||
|
||||
// GetResourceTasks 获取资源相关的所有任务
|
||||
func (h *TaskHandler) GetResourceTasks(c *gin.Context) {
|
||||
resourceID := c.Query("resource_id")
|
||||
if resourceID == "" {
|
||||
response.BadRequest(c, "缺少resource_id参数")
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := h.taskService.GetTasksByResource(resourceID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to get resource tasks", "error", err, "resource_id", resourceID)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, tasks)
|
||||
}
|
||||
142
api/handlers/upload.go
Normal file
142
api/handlers/upload.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
services2 "github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
uploadService *services2.UploadService
|
||||
characterLibraryService *services2.CharacterLibraryService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewUploadHandler(cfg *config.Config, log *logger.Logger, characterLibraryService *services2.CharacterLibraryService) (*UploadHandler, error) {
|
||||
uploadService, err := services2.NewUploadService(cfg, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UploadHandler{
|
||||
uploadService: uploadService,
|
||||
characterLibraryService: characterLibraryService,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadImage 上传图片
|
||||
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 检查文件类型
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// 验证是图片类型
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
|
||||
if !allowedTypes[contentType] {
|
||||
response.BadRequest(c, "只支持图片格式 (jpg, png, gif, webp)")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小 (10MB)
|
||||
if header.Size > 10*1024*1024 {
|
||||
response.BadRequest(c, "文件大小不能超过10MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 上传到MinIO
|
||||
fileURL, err := h.uploadService.UploadCharacterImage(file, header.Filename, contentType)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to upload image", "error", err)
|
||||
response.InternalError(c, "上传失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"url": fileURL,
|
||||
"filename": header.Filename,
|
||||
"size": header.Size,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadCharacterImage 上传角色图片(带角色ID)
|
||||
func (h *UploadHandler) UploadCharacterImage(c *gin.Context) {
|
||||
characterID := c.Param("id")
|
||||
|
||||
// 获取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 检查文件类型
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// 验证是图片类型
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
|
||||
if !allowedTypes[contentType] {
|
||||
response.BadRequest(c, "只支持图片格式 (jpg, png, gif, webp)")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小 (10MB)
|
||||
if header.Size > 10*1024*1024 {
|
||||
response.BadRequest(c, "文件大小不能超过10MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 上传到MinIO
|
||||
fileURL, err := h.uploadService.UploadCharacterImage(file, header.Filename, contentType)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to upload character image", "error", err)
|
||||
response.InternalError(c, "上传失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新角色的image_url字段到数据库
|
||||
err = h.characterLibraryService.UploadCharacterImage(characterID, fileURL)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to update character image_url", "error", err, "character_id", characterID)
|
||||
response.InternalError(c, "更新角色图片失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.log.Infow("Character image uploaded and saved", "character_id", characterID, "url", fileURL)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"url": fileURL,
|
||||
"filename": header.Filename,
|
||||
"size": header.Size,
|
||||
})
|
||||
}
|
||||
149
api/handlers/video_generation.go
Normal file
149
api/handlers/video_generation.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/infrastructure/storage"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VideoGenerationHandler struct {
|
||||
videoService *services.VideoGenerationService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewVideoGenerationHandler(db *gorm.DB, transferService *services.ResourceTransferService, localStorage *storage.LocalStorage, aiService *services.AIService, log *logger.Logger) *VideoGenerationHandler {
|
||||
return &VideoGenerationHandler{
|
||||
videoService: services.NewVideoGenerationService(db, transferService, localStorage, aiService, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) GenerateVideo(c *gin.Context) {
|
||||
|
||||
var req services.GenerateVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
videoGen, err := h.videoService.GenerateVideo(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate video", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, videoGen)
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) GenerateVideoFromImage(c *gin.Context) {
|
||||
|
||||
imageGenID, err := strconv.ParseUint(c.Param("image_gen_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的图片ID")
|
||||
return
|
||||
}
|
||||
|
||||
videoGen, err := h.videoService.GenerateVideoFromImage(uint(imageGenID))
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to generate video from image", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, videoGen)
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) BatchGenerateForEpisode(c *gin.Context) {
|
||||
|
||||
episodeID := c.Param("episode_id")
|
||||
|
||||
videos, err := h.videoService.BatchGenerateVideosForEpisode(episodeID)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to batch generate videos", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, videos)
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) GetVideoGeneration(c *gin.Context) {
|
||||
|
||||
videoGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
videoGen, err := h.videoService.GetVideoGeneration(uint(videoGenID))
|
||||
if err != nil {
|
||||
response.NotFound(c, "视频生成记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, videoGen)
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) ListVideoGenerations(c *gin.Context) {
|
||||
var storyboardID *uint
|
||||
// 优先使用storyboard_id参数
|
||||
if storyboardIDStr := c.Query("storyboard_id"); storyboardIDStr != "" {
|
||||
id, err := strconv.ParseUint(storyboardIDStr, 10, 32)
|
||||
if err == nil {
|
||||
uid := uint(id)
|
||||
storyboardID = &uid
|
||||
}
|
||||
}
|
||||
status := c.Query("status")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
var dramaIDUint *uint
|
||||
if dramaIDStr := c.Query("drama_id"); dramaIDStr != "" {
|
||||
did, _ := strconv.ParseUint(dramaIDStr, 10, 32)
|
||||
didUint := uint(did)
|
||||
dramaIDUint = &didUint
|
||||
}
|
||||
|
||||
// 计算offset:(page - 1) * pageSize
|
||||
offset := (page - 1) * pageSize
|
||||
videos, total, err := h.videoService.ListVideoGenerations(dramaIDUint, storyboardID, status, pageSize, offset)
|
||||
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to list videos", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithPagination(c, videos, total, page, pageSize)
|
||||
}
|
||||
|
||||
func (h *VideoGenerationHandler) DeleteVideoGeneration(c *gin.Context) {
|
||||
|
||||
videoGenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.videoService.DeleteVideoGeneration(uint(videoGenID)); err != nil {
|
||||
h.log.Errorw("Failed to delete video", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
104
api/handlers/video_merge.go
Normal file
104
api/handlers/video_merge.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
services2 "github.com/drama-generator/backend/application/services"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VideoMergeHandler struct {
|
||||
mergeService *services2.VideoMergeService
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewVideoMergeHandler(db *gorm.DB, transferService *services2.ResourceTransferService, storagePath, baseURL string, log *logger.Logger) *VideoMergeHandler {
|
||||
return &VideoMergeHandler{
|
||||
mergeService: services2.NewVideoMergeService(db, transferService, storagePath, baseURL, log),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *VideoMergeHandler) MergeVideos(c *gin.Context) {
|
||||
var req services2.MergeVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
merge, err := h.mergeService.MergeVideos(&req)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to merge videos", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "Video merge task created",
|
||||
"merge": merge,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *VideoMergeHandler) GetMerge(c *gin.Context) {
|
||||
mergeIDStr := c.Param("merge_id")
|
||||
mergeID, err := strconv.ParseUint(mergeIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid merge ID")
|
||||
return
|
||||
}
|
||||
|
||||
merge, err := h.mergeService.GetMerge(uint(mergeID))
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to get merge", "error", err)
|
||||
response.NotFound(c, "Merge not found")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"merge": merge})
|
||||
}
|
||||
|
||||
func (h *VideoMergeHandler) ListMerges(c *gin.Context) {
|
||||
episodeID := c.Query("episode_id")
|
||||
status := c.Query("status")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
var episodeIDPtr *string
|
||||
if episodeID != "" {
|
||||
episodeIDPtr = &episodeID
|
||||
}
|
||||
|
||||
merges, total, err := h.mergeService.ListMerges(episodeIDPtr, status, page, pageSize)
|
||||
if err != nil {
|
||||
h.log.Errorw("Failed to list merges", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"merges": merges,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *VideoMergeHandler) DeleteMerge(c *gin.Context) {
|
||||
mergeIDStr := c.Param("merge_id")
|
||||
mergeID, err := strconv.ParseUint(mergeIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid merge ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.mergeService.DeleteMerge(uint(mergeID)); err != nil {
|
||||
h.log.Errorw("Failed to delete merge", "error", err)
|
||||
response.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Merge deleted successfully"})
|
||||
}
|
||||
34
api/middlewares/cors.go
Normal file
34
api/middlewares/cors.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORSMiddleware(allowedOrigins []string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
allowed := false
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" || o == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
30
api/middlewares/logger.go
Normal file
30
api/middlewares/logger.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func LoggerMiddleware(log *logger.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
duration := time.Since(start)
|
||||
|
||||
log.Infow("HTTP Request",
|
||||
"method", c.Request.Method,
|
||||
"path", path,
|
||||
"query", query,
|
||||
"status", c.Writer.Status(),
|
||||
"duration", duration.Milliseconds(),
|
||||
"ip", c.ClientIP(),
|
||||
"user_agent", c.Request.UserAgent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
52
api/middlewares/ratelimit.go
Normal file
52
api/middlewares/ratelimit.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/drama-generator/backend/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
requests map[string][]time.Time
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
var limiter = &rateLimiter{
|
||||
requests: make(map[string][]time.Time),
|
||||
limit: 100,
|
||||
window: time.Minute,
|
||||
}
|
||||
|
||||
func RateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
|
||||
limiter.mu.Lock()
|
||||
defer limiter.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
requests := limiter.requests[ip]
|
||||
|
||||
var validRequests []time.Time
|
||||
for _, t := range requests {
|
||||
if now.Sub(t) < limiter.window {
|
||||
validRequests = append(validRequests, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validRequests) >= limiter.limit {
|
||||
response.Error(c, 429, "RATE_LIMIT_EXCEEDED", "请求过于频繁,请稍后再试")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
validRequests = append(validRequests, now)
|
||||
limiter.requests[ip] = validRequests
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
212
api/routes/routes.go
Normal file
212
api/routes/routes.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
handlers2 "github.com/drama-generator/backend/api/handlers"
|
||||
middlewares2 "github.com/drama-generator/backend/api/middlewares"
|
||||
services2 "github.com/drama-generator/backend/application/services"
|
||||
storage2 "github.com/drama-generator/backend/infrastructure/storage"
|
||||
"github.com/drama-generator/backend/pkg/config"
|
||||
"github.com/drama-generator/backend/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupRouter(cfg *config.Config, db *gorm.DB, log *logger.Logger, localStorage interface{}) *gin.Engine {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middlewares2.LoggerMiddleware(log))
|
||||
r.Use(middlewares2.CORSMiddleware(cfg.Server.CORSOrigins))
|
||||
|
||||
// 静态文件服务(用户上传的文件)
|
||||
r.Static("/static", cfg.Storage.LocalPath)
|
||||
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"app": cfg.App.Name,
|
||||
"version": cfg.App.Version,
|
||||
})
|
||||
})
|
||||
|
||||
aiService := services2.NewAIService(db, log)
|
||||
localStoragePtr := localStorage.(*storage2.LocalStorage)
|
||||
transferService := services2.NewResourceTransferService(db, log)
|
||||
dramaHandler := handlers2.NewDramaHandler(db, cfg, log, nil)
|
||||
aiConfigHandler := handlers2.NewAIConfigHandler(db, cfg, log)
|
||||
scriptGenHandler := handlers2.NewScriptGenerationHandler(db, cfg, log)
|
||||
imageGenService := services2.NewImageGenerationService(db, transferService, localStoragePtr, log)
|
||||
imageGenHandler := handlers2.NewImageGenerationHandler(db, cfg, log, transferService, localStoragePtr)
|
||||
videoGenHandler := handlers2.NewVideoGenerationHandler(db, transferService, localStoragePtr, aiService, log)
|
||||
videoMergeHandler := handlers2.NewVideoMergeHandler(db, nil, cfg.Storage.LocalPath, cfg.Storage.BaseURL, log)
|
||||
assetHandler := handlers2.NewAssetHandler(db, cfg, log)
|
||||
characterLibraryService := services2.NewCharacterLibraryService(db, log)
|
||||
characterLibraryHandler := handlers2.NewCharacterLibraryHandler(db, cfg, log, transferService, localStoragePtr)
|
||||
uploadHandler, err := handlers2.NewUploadHandler(cfg, log, characterLibraryService)
|
||||
if err != nil {
|
||||
log.Fatalw("Failed to create upload handler", "error", err)
|
||||
}
|
||||
storyboardHandler := handlers2.NewStoryboardHandler(db, cfg, log)
|
||||
sceneHandler := handlers2.NewSceneHandler(db, log, imageGenService)
|
||||
taskHandler := handlers2.NewTaskHandler(db, log)
|
||||
framePromptService := services2.NewFramePromptService(db, log)
|
||||
framePromptHandler := handlers2.NewFramePromptHandler(framePromptService, log)
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.Use(middlewares2.RateLimitMiddleware())
|
||||
|
||||
dramas := api.Group("/dramas")
|
||||
{
|
||||
dramas.GET("", dramaHandler.ListDramas)
|
||||
dramas.POST("", dramaHandler.CreateDrama)
|
||||
dramas.GET("/stats", dramaHandler.GetDramaStats)
|
||||
dramas.GET("/:id/characters", dramaHandler.GetCharacters)
|
||||
dramas.PUT("/:id/characters", dramaHandler.SaveCharacters)
|
||||
dramas.PUT("/:id/outline", dramaHandler.SaveOutline)
|
||||
dramas.PUT("/:id/episodes", dramaHandler.SaveEpisodes)
|
||||
dramas.PUT("/:id/progress", dramaHandler.SaveProgress)
|
||||
dramas.GET("/:id", dramaHandler.GetDrama)
|
||||
dramas.PUT("/:id", dramaHandler.UpdateDrama)
|
||||
dramas.DELETE("/:id", dramaHandler.DeleteDrama)
|
||||
}
|
||||
|
||||
aiConfigs := api.Group("/ai-configs")
|
||||
{
|
||||
aiConfigs.GET("", aiConfigHandler.ListConfigs)
|
||||
aiConfigs.POST("", aiConfigHandler.CreateConfig)
|
||||
aiConfigs.POST("/test", aiConfigHandler.TestConnection)
|
||||
aiConfigs.GET("/:id", aiConfigHandler.GetConfig)
|
||||
aiConfigs.PUT("/:id", aiConfigHandler.UpdateConfig)
|
||||
aiConfigs.DELETE("/:id", aiConfigHandler.DeleteConfig)
|
||||
}
|
||||
|
||||
generation := api.Group("/generation")
|
||||
{
|
||||
generation.POST("/outline", scriptGenHandler.GenerateOutline)
|
||||
generation.POST("/characters", scriptGenHandler.GenerateCharacters)
|
||||
generation.POST("/episodes", scriptGenHandler.GenerateEpisodes)
|
||||
}
|
||||
|
||||
// 角色库路由
|
||||
characterLibrary := api.Group("/character-library")
|
||||
{
|
||||
characterLibrary.GET("", characterLibraryHandler.ListLibraryItems)
|
||||
characterLibrary.POST("", characterLibraryHandler.CreateLibraryItem)
|
||||
characterLibrary.GET("/:id", characterLibraryHandler.GetLibraryItem)
|
||||
characterLibrary.DELETE("/:id", characterLibraryHandler.DeleteLibraryItem)
|
||||
}
|
||||
|
||||
// 角色图片相关路由
|
||||
characters := api.Group("/characters")
|
||||
{
|
||||
characters.PUT("/:id", characterLibraryHandler.UpdateCharacter)
|
||||
characters.DELETE("/:id", characterLibraryHandler.DeleteCharacter)
|
||||
characters.POST("/batch-generate-images", characterLibraryHandler.BatchGenerateCharacterImages)
|
||||
characters.POST("/:id/generate-image", characterLibraryHandler.GenerateCharacterImage)
|
||||
characters.POST("/:id/upload-image", uploadHandler.UploadCharacterImage)
|
||||
characters.PUT("/:id/image", characterLibraryHandler.UploadCharacterImage)
|
||||
characters.PUT("/:id/image-from-library", characterLibraryHandler.ApplyLibraryItemToCharacter)
|
||||
characters.POST("/:id/add-to-library", characterLibraryHandler.AddCharacterToLibrary)
|
||||
}
|
||||
|
||||
// 文件上传路由
|
||||
upload := api.Group("/upload")
|
||||
{
|
||||
upload.POST("/image", uploadHandler.UploadImage)
|
||||
}
|
||||
|
||||
// 分镜头路由
|
||||
episodes := api.Group("/episodes")
|
||||
{
|
||||
// 分镜头
|
||||
episodes.POST("/:episode_id/storyboards", storyboardHandler.GenerateStoryboard)
|
||||
episodes.GET("/:episode_id/storyboards", sceneHandler.GetStoryboardsForEpisode)
|
||||
episodes.POST("/:episode_id/finalize", dramaHandler.FinalizeEpisode)
|
||||
episodes.GET("/:episode_id/download", dramaHandler.DownloadEpisodeVideo)
|
||||
}
|
||||
|
||||
// 任务路由
|
||||
tasks := api.Group("/tasks")
|
||||
{
|
||||
tasks.GET("/:task_id", taskHandler.GetTaskStatus)
|
||||
tasks.GET("", taskHandler.GetResourceTasks)
|
||||
}
|
||||
|
||||
// 场景路由
|
||||
scenes := api.Group("/scenes")
|
||||
{
|
||||
scenes.PUT("/:scene_id", sceneHandler.UpdateScene)
|
||||
scenes.POST("/generate-image", sceneHandler.GenerateSceneImage)
|
||||
}
|
||||
|
||||
images := api.Group("/images")
|
||||
{
|
||||
images.GET("", imageGenHandler.ListImageGenerations)
|
||||
images.POST("", imageGenHandler.GenerateImage)
|
||||
images.GET("/:id", imageGenHandler.GetImageGeneration)
|
||||
images.DELETE("/:id", imageGenHandler.DeleteImageGeneration)
|
||||
images.POST("/scene/:scene_id", imageGenHandler.GenerateImagesForScene)
|
||||
images.GET("/episode/:episode_id/backgrounds", imageGenHandler.GetBackgroundsForEpisode)
|
||||
images.POST("/episode/:episode_id/backgrounds/extract", imageGenHandler.ExtractBackgroundsForEpisode)
|
||||
images.POST("/episode/:episode_id/batch", imageGenHandler.BatchGenerateForEpisode)
|
||||
}
|
||||
|
||||
videos := api.Group("/videos")
|
||||
{
|
||||
videos.GET("", videoGenHandler.ListVideoGenerations)
|
||||
videos.POST("", videoGenHandler.GenerateVideo)
|
||||
videos.GET("/:id", videoGenHandler.GetVideoGeneration)
|
||||
videos.DELETE("/:id", videoGenHandler.DeleteVideoGeneration)
|
||||
videos.POST("/image/:image_gen_id", videoGenHandler.GenerateVideoFromImage)
|
||||
videos.POST("/episode/:episode_id/batch", videoGenHandler.BatchGenerateForEpisode)
|
||||
}
|
||||
|
||||
videoMerges := api.Group("/video-merges")
|
||||
{
|
||||
videoMerges.GET("", videoMergeHandler.ListMerges)
|
||||
videoMerges.POST("", videoMergeHandler.MergeVideos)
|
||||
videoMerges.GET("/:merge_id", videoMergeHandler.GetMerge)
|
||||
videoMerges.DELETE("/:merge_id", videoMergeHandler.DeleteMerge)
|
||||
}
|
||||
|
||||
assets := api.Group("/assets")
|
||||
{
|
||||
assets.GET("", assetHandler.ListAssets)
|
||||
assets.POST("", assetHandler.CreateAsset)
|
||||
assets.GET("/:id", assetHandler.GetAsset)
|
||||
assets.PUT("/:id", assetHandler.UpdateAsset)
|
||||
assets.DELETE("/:id", assetHandler.DeleteAsset)
|
||||
assets.POST("/import/image/:image_gen_id", assetHandler.ImportFromImageGen)
|
||||
assets.POST("/import/video/:video_gen_id", assetHandler.ImportFromVideoGen)
|
||||
}
|
||||
|
||||
storyboards := api.Group("/storyboards")
|
||||
{
|
||||
storyboards.PUT("/:id", storyboardHandler.UpdateStoryboard)
|
||||
storyboards.POST("/:id/frame-prompt", framePromptHandler.GenerateFramePrompt)
|
||||
storyboards.GET("/:id/frame-prompts", handlers2.GetStoryboardFramePrompts(db, log))
|
||||
}
|
||||
}
|
||||
|
||||
// 前端静态文件服务(放在API路由之后,避免冲突)
|
||||
// 服务前端构建产物
|
||||
r.Static("/assets", "./web/dist/assets")
|
||||
r.StaticFile("/favicon.ico", "./web/dist/favicon.ico")
|
||||
|
||||
// NoRoute处理:对于所有未匹配的路由
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// 如果是API路径,返回404
|
||||
if len(path) >= 4 && path[:4] == "/api" {
|
||||
c.JSON(404, gin.H{"error": "API endpoint not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// SPA fallback - 返回index.html
|
||||
c.File("./web/dist/index.html")
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user