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
355 lines
8.8 KiB
Go
355 lines
8.8 KiB
Go
package handler
|
||
|
||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
// * 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/service"
|
||
"geekai/service/moderation"
|
||
"geekai/service/oss"
|
||
"geekai/service/video"
|
||
"geekai/store/model"
|
||
"geekai/store/vo"
|
||
"geekai/utils"
|
||
"geekai/utils/resp"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type VideoHandler struct {
|
||
BaseHandler
|
||
videoService *video.Service
|
||
uploader *oss.UploaderManager
|
||
userService *service.UserService
|
||
moderationManager *moderation.ServiceManager
|
||
}
|
||
|
||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *VideoHandler {
|
||
return &VideoHandler{
|
||
BaseHandler: BaseHandler{
|
||
App: app,
|
||
DB: db,
|
||
},
|
||
videoService: service,
|
||
uploader: uploader,
|
||
userService: userService,
|
||
moderationManager: moderationManager,
|
||
}
|
||
}
|
||
|
||
// RegisterRoutes 注册路由
|
||
func (h *VideoHandler) RegisterRoutes() {
|
||
group := h.App.Engine.Group("/api/video/")
|
||
|
||
// 需要用户授权的接口
|
||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||
{
|
||
group.POST("create", h.Create)
|
||
group.GET("list", h.List)
|
||
group.GET("remove", h.Remove)
|
||
group.GET("publish", h.Publish)
|
||
group.GET("power-config", h.GetPowerConfig) // 获取算力配置
|
||
group.GET("power-by-key", h.GetPowerByPriceKey) // 根据 priceKey 获取算力
|
||
}
|
||
}
|
||
|
||
type VideoTaskRequest struct {
|
||
Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora)
|
||
Model string `json:"model"` // 模型标识(带版本号:veo-2.0, sora-2.0)
|
||
Prompt string `json:"prompt"` // 提示词
|
||
Params map[string]any `json:"params"` // 模型特定参数
|
||
PriceKey string `json:"price_key"` // 价格键(如 "fixed", "5_720P" 等)
|
||
}
|
||
|
||
// Create 统一的创建视频任务接口
|
||
func (h *VideoHandler) Create(c *gin.Context) {
|
||
var data VideoTaskRequest
|
||
if err := c.ShouldBindJSON(&data); err != nil {
|
||
resp.ERROR(c, types.InvalidArgs)
|
||
return
|
||
}
|
||
|
||
// 验证必填字段
|
||
if data.Provider == "" {
|
||
resp.ERROR(c, "provider 不能为空")
|
||
return
|
||
}
|
||
if data.Model == "" {
|
||
resp.ERROR(c, "model 不能为空")
|
||
return
|
||
}
|
||
if data.Prompt == "" {
|
||
resp.ERROR(c, "prompt 不能为空")
|
||
return
|
||
}
|
||
if data.PriceKey == "" {
|
||
resp.ERROR(c, "price_key 不能为空")
|
||
return
|
||
}
|
||
|
||
// 文本审查
|
||
if h.App.SysConfig.Moderation.Enable {
|
||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||
if err != nil {
|
||
logger.Error("failed to moderate content: ", err)
|
||
}
|
||
if moderationResult.Flagged {
|
||
// 记录违规内容
|
||
moderation := model.Moderation{
|
||
UserId: h.GetLoginUserId(c),
|
||
Source: types.ModerationSourceVideo,
|
||
Input: data.Prompt,
|
||
Result: utils.JsonEncode(moderationResult),
|
||
}
|
||
err = h.DB.Create(&moderation).Error
|
||
if err != nil {
|
||
logger.Error("failed to save moderation: ", err)
|
||
}
|
||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||
return
|
||
}
|
||
}
|
||
|
||
// 获取用户信息
|
||
user, err := h.GetLoginUser(c)
|
||
if err != nil {
|
||
resp.NotAuth(c)
|
||
return
|
||
}
|
||
|
||
// 计算算力
|
||
power, err := video.CalculatePower(h.DB, data.Model, data.PriceKey)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
// 检查算力是否充足
|
||
if user.Power < power {
|
||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||
return
|
||
}
|
||
|
||
// 构建任务
|
||
userId := int(h.GetLoginUserId(c))
|
||
task := types.VideoTask{
|
||
UserId: userId,
|
||
Type: data.Provider, // provider 作为 type
|
||
Prompt: data.Prompt,
|
||
Params: data.Params,
|
||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||
}
|
||
|
||
// 插入数据库
|
||
job := model.VideoJob{
|
||
UserId: uint(userId),
|
||
Type: data.Provider,
|
||
Prompt: data.Prompt,
|
||
Power: power,
|
||
Params: utils.JsonEncode(task),
|
||
}
|
||
tx := h.DB.Create(&job)
|
||
if tx.Error != nil {
|
||
resp.ERROR(c, tx.Error.Error())
|
||
return
|
||
}
|
||
|
||
// 创建任务
|
||
task.Id = job.Id
|
||
h.videoService.PushTask(task)
|
||
|
||
// 扣减算力
|
||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||
Type: types.PowerConsume,
|
||
Model: data.Provider,
|
||
Remark: fmt.Sprintf("%s 视频生成,任务ID:%d", data.Provider, job.Id),
|
||
})
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
resp.SUCCESS(c, gin.H{"job_id": job.Id})
|
||
}
|
||
|
||
// GetPowerConfig 获取算力配置
|
||
func (h *VideoHandler) GetPowerConfig(c *gin.Context) {
|
||
config, err := video.GetVideoConfig(h.DB)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
resp.SUCCESS(c, config.VideoPowers)
|
||
}
|
||
|
||
// GetPowerByPriceKey 根据 modelKey 和 priceKey 获取算力值
|
||
func (h *VideoHandler) GetPowerByPriceKey(c *gin.Context) {
|
||
modelKey := c.Query("model_key")
|
||
priceKey := c.Query("price_key")
|
||
|
||
if modelKey == "" {
|
||
resp.ERROR(c, "model_key 不能为空")
|
||
return
|
||
}
|
||
if priceKey == "" {
|
||
resp.ERROR(c, "price_key 不能为空")
|
||
return
|
||
}
|
||
|
||
power, err := video.CalculatePower(h.DB, modelKey, priceKey)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
resp.SUCCESS(c, gin.H{"power": power})
|
||
}
|
||
|
||
func (h *VideoHandler) List(c *gin.Context) {
|
||
userId := h.GetLoginUserId(c)
|
||
t := c.Query("type")
|
||
page := h.GetInt(c, "page", 1)
|
||
pageSize := h.GetInt(c, "page_size", 20)
|
||
all := h.GetBool(c, "all")
|
||
session := h.DB.Session(&gorm.Session{})
|
||
if t != "" {
|
||
session = session.Where("type", t)
|
||
}
|
||
if all {
|
||
session = session.Where("publish", 0).Where("status", types.VideoStatusSuccess)
|
||
} else {
|
||
session = session.Where("user_id", userId)
|
||
}
|
||
// 统计总数
|
||
var total int64
|
||
session.Model(&model.VideoJob{}).Count(&total)
|
||
|
||
if page > 0 && pageSize > 0 {
|
||
offset := (page - 1) * pageSize
|
||
session = session.Offset(offset).Limit(pageSize)
|
||
}
|
||
var list []model.VideoJob
|
||
err := session.Order("id desc").Find(&list).Error
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
// 转换为 VO
|
||
items := make([]vo.VideoJob, 0)
|
||
for _, v := range list {
|
||
var item vo.VideoJob
|
||
err = utils.CopyObject(v, &item)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
item.CreatedAt = v.CreatedAt.Unix()
|
||
// 解析任务详情(用于前端展示标签)
|
||
if v.Params != "" {
|
||
task := types.VideoTask{}
|
||
if err := utils.JsonDecode(v.Params, &task); err == nil {
|
||
// 默认从 params map 中提取常用字段
|
||
if paramsMap, ok := task.Params.(map[string]any); ok {
|
||
if item.Params == nil {
|
||
item.Params = make(map[string]any)
|
||
}
|
||
if _, ok := item.Params["task_type"]; !ok {
|
||
if taskType, ok := paramsMap["task_type"]; ok {
|
||
item.Params["task_type"] = taskType
|
||
}
|
||
}
|
||
if _, ok := item.Params["model"]; !ok {
|
||
if modelKey, ok := paramsMap["model"]; ok {
|
||
item.Params["model"] = modelKey
|
||
}
|
||
}
|
||
if _, ok := item.Params["duration"]; !ok {
|
||
if duration, ok := paramsMap["duration"]; ok {
|
||
item.Params["duration"] = duration
|
||
}
|
||
}
|
||
if _, ok := item.Params["size"]; !ok {
|
||
if size, ok := paramsMap["size"]; ok {
|
||
item.Params["size"] = size
|
||
} else if size, ok := paramsMap["resolution"].(string); ok {
|
||
item.Params["size"] = size
|
||
}
|
||
}
|
||
if _, ok := item.Params["mode"]; !ok {
|
||
if mode, ok := paramsMap["mode"]; ok {
|
||
item.Params["mode"] = mode
|
||
}
|
||
}
|
||
if _, ok := item.Params["sound"]; !ok {
|
||
if sound, ok := paramsMap["sound"]; ok {
|
||
item.Params["sound"] = sound
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
items = append(items, item)
|
||
}
|
||
|
||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||
}
|
||
|
||
func (h *VideoHandler) Remove(c *gin.Context) {
|
||
id := h.GetInt(c, "id", 0)
|
||
userId := h.GetLoginUserId(c)
|
||
var job model.VideoJob
|
||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
// 只有失败的任务才能删除
|
||
if job.Status != types.VideoStatusFailed {
|
||
resp.ERROR(c, "只有失败的任务才能删除!")
|
||
return
|
||
}
|
||
|
||
// 删除任务
|
||
err = h.DB.Delete(&job).Error
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
// 删除文件
|
||
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||
|
||
resp.SUCCESS(c)
|
||
}
|
||
|
||
func (h *VideoHandler) Publish(c *gin.Context) {
|
||
id := h.GetInt(c, "id", 0)
|
||
userId := h.GetLoginUserId(c)
|
||
publish := h.GetBool(c, "publish")
|
||
var job model.VideoJob
|
||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
err = h.DB.Model(&job).UpdateColumn("publish", publish).Error
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
resp.SUCCESS(c)
|
||
}
|