Files
geekai/api/handler/admin/video_handler.go
T
RockYang 9ccff4efbc feat(release): migrate GeekAI v4.3.0 to open source
- Sync backend and frontend from GeekAI Plus v4.3.0

- Remove commercial License flows and update open-source deployment defaults

- Preserve Docker Compose deployment and bump image tags to v4.3.0

BREAKING CHANGE: commercial License configuration and related endpoints are removed
2026-08-11 14:51:20 +08:00

270 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// VideoHandler 管理后台视频生成处理器
type VideoHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
// NewVideoHandler 创建管理后台视频生成处理器
func NewVideoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *VideoHandler {
return &VideoHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
userService: userService,
uploader: manager,
}
}
// RegisterRoutes 注册视频生成管理后台路由
func (h *VideoHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/video/")
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
rg.GET("config", h.GetConfig)
rg.POST("config/update", h.UpdateConfig)
rg.POST("list", h.Videos)
rg.GET("remove", h.Remove)
}
}
// GetConfig 获取视频生成配置
func (h *VideoHandler) GetConfig(c *gin.Context) {
var config model.Config
err := h.DB.Where("name", types.ConfigKeyVideo).First(&config).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 返回空配置
resp.SUCCESS(c, types.VideoConfig{
ApiURL: "",
ApiKey: "",
VideoPowers: make(map[string]types.VideoModelPower),
})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
var videoConfig types.VideoConfig
err = utils.JsonDecode(config.Value, &videoConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
resp.SUCCESS(c, videoConfig)
}
// UpdateConfig 更新视频生成配置
func (h *VideoHandler) UpdateConfig(c *gin.Context) {
var req types.VideoConfig
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
// 验证必填字段
if req.ApiURL == "" {
resp.ERROR(c, "API地址不能为空")
return
}
if req.ApiKey == "" {
resp.ERROR(c, "API密钥不能为空")
return
}
// 验证算力配置
if len(req.VideoPowers) == 0 {
resp.ERROR(c, "请至少配置一个模型的算力")
return
}
// 新的价格配置方式直接使用 power_config 中的 key(如 "fixed"、"5_720P" 等)
// 不再区分固定收费和按秒收费,所有价格配置都在 power_config 中
for key, modelPower := range req.VideoPowers {
// 验证 provider
if modelPower.Provider == "" {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 provider 不能为空", key))
return
}
// 验证 model
if modelPower.Model == "" {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 model 不能为空", key))
return
}
// 验证 power_config
if len(modelPower.PowerConfig) == 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config 不能为空", key))
return
}
// 验证 power_config 中的值必须大于0
for configKey, configValue := range modelPower.PowerConfig {
if configValue <= 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config.%s 必须大于0", key, configKey))
return
}
}
}
// 保存配置
tx := h.DB.Begin()
value := utils.JsonEncode(&req)
var exist model.Config
tx.Where("name", types.ConfigKeyVideo).First(&exist)
if exist.Id > 0 {
exist.Value = value
err := tx.Updates(&exist).Error
if err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
tx.Rollback()
return
}
} else {
exist.Name = types.ConfigKeyVideo
exist.Value = value
err := tx.Create(&exist).Error
if err != nil {
resp.ERROR(c, "创建配置失败: "+err.Error())
tx.Rollback()
return
}
}
tx.Commit()
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}
type videoQuery struct {
Type string `json:"type"` // 任务类型 luma, keling
Status string `json:"status"` // 任务状态 pending, in_progress, downloading, success, failed
Prompt string `json:"prompt"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// Videos 视频任务列表
func (h *VideoHandler) Videos(c *gin.Context) {
var data videoQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Type != "" {
session = session.Where("type", data.Type)
}
if data.Status != "" {
session = session.Where("status", data.Status)
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.VideoJob{}).Count(&total)
var list []model.VideoJob
var items = make([]vo.VideoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.VideoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *VideoHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tab := c.Query("tab")
tx := h.DB.Begin()
var md, remark, fileURL string
var power, userId int
var needRefund bool
switch tab {
case "luma", "keling":
var job model.VideoJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = job.Type
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("视频任务失败,退回算力。任务ID%d,Err: %s", job.Id, job.ErrMsg)
needRefund = job.Status != types.VideoStatusSuccess
fileURL = job.VideoURL
default:
resp.ERROR(c, types.InvalidArgs)
return
}
if needRefund {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove file
err := h.uploader.GetUploadHandler().Delete(fileURL)
if err != nil {
logger.Error("remove file failed: ", err)
}
resp.SUCCESS(c)
}