mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-12 18:50:58 +00:00
9ccff4efbc
- 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
270 lines
6.7 KiB
Go
270 lines
6.7 KiB
Go
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)
|
||
}
|