Files
geekai/api/handler/admin/ppt_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

285 lines
7.1 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 (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service/ppt"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PPTHandler 管理后台 PPT 生成配置处理器
type PPTHandler struct {
handler.BaseHandler
pptService *ppt.PptService
}
// NewPPTHandler 创建管理后台 PPT 配置处理器
func NewPPTHandler(app *core.AppServer, db *gorm.DB, pptService *ppt.PptService) *PPTHandler {
return &PPTHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
pptService: pptService,
}
}
// RegisterRoutes 注册 PPT 配置相关路由
func (h *PPTHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/ppt/")
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
rg.GET("config", h.GetConfig)
rg.POST("config/update", h.UpdateConfig)
rg.GET("jobs", h.Jobs)
rg.GET("jobs/:task_id", h.JobDetail)
rg.GET("jobs/:task_id/export", h.ExportJob)
rg.GET("stats", h.Stats)
}
}
// GetConfig 获取 PPT 生成配置
func (h *PPTHandler) GetConfig(c *gin.Context) {
var cfg model.Config
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 返回一个默认空配置
resp.SUCCESS(c, types.PPTConfig{
OutlineLLMModel: "gpt-4o-mini",
MaxSlidesPerTask: 30,
PowerCostPerSlide: 0,
MaxConcurrentRequests: 3,
QPSLimit: 1,
NanoBananaModel: "nano-banana",
NanoBananaAspectRatio: "16:9",
SeedreamSize: "1920x1080",
})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
var pptConfig types.PPTConfig
err = utils.JsonDecode(cfg.Value, &pptConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
resp.SUCCESS(c, pptConfig)
}
// UpdateConfig 更新 PPT 生成配置
func (h *PPTHandler) UpdateConfig(c *gin.Context) {
var req types.PPTConfig
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
// 基础校验
if req.MaxSlidesPerTask <= 0 {
resp.ERROR(c, "单个任务最多 PPT 页数必须大于 0")
return
}
if req.PowerCostPerSlide < 0 {
resp.ERROR(c, "每张 PPT 图片消耗算力不能小于 0")
return
}
if req.MaxConcurrentRequests <= 0 {
req.MaxConcurrentRequests = 3
}
if req.QPSLimit <= 0 {
req.QPSLimit = 1
}
// 根据当前生图提供方做必填校验
switch req.ActiveImageProvider {
case types.PPTImageProviderNanoBanana:
if req.NanoBananaApiURL == "" {
resp.ERROR(c, "Nano Banana API 地址不能为空")
return
}
if req.NanoBananaApiKey == "" {
resp.ERROR(c, "Nano Banana API Key 不能为空")
return
}
case types.PPTImageProviderSeedream:
if req.SeedreamBaseURL == "" {
resp.ERROR(c, "Seedream Base URL 不能为空")
return
}
if req.SeedreamApiKey == "" {
resp.ERROR(c, "Seedream API Key 不能为空")
return
}
if req.SeedreamModel == "" {
resp.ERROR(c, "Seedream 模型 ID 不能为空")
return
}
default:
// 允许为空,未来可以扩展更多 provider
}
value := utils.JsonEncode(&req)
var cfg model.Config
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
cfg.Name = types.ConfigKeyPPT
cfg.Value = value
if err = h.DB.Create(&cfg).Error; err != nil {
resp.ERROR(c, "创建配置失败: "+err.Error())
return
}
resp.SUCCESS(c, gin.H{"message": "配置创建成功"})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
cfg.Value = value
if err = h.DB.Updates(&cfg).Error; err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
return
}
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}
// Jobs 管理后台查看 PPT 任务列表(内存任务)
func (h *PPTHandler) Jobs(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
filterUserId := h.GetInt(c, "user_id", 0)
status := h.GetTrim(c, "status")
filtered, total := h.pptService.ListAdminJobs(c.Request.Context(), page, pageSize, filterUserId, status)
jobs := make([]gin.H, 0, len(filtered))
for _, t := range filtered {
job := t.TaskSummaryMap()
job["user_id"] = t.UserID
job["error_message"] = t.ErrorMessage
jobs = append(jobs, job)
}
resp.SUCCESS(c, gin.H{
"jobs": jobs,
"total": total,
"page": page,
"page_size": pageSize,
})
}
func buildAdminPPTTaskDetail(task *ppt.Task) gin.H {
percentage := 0
if task.Total > 0 {
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
}
return gin.H{
"task_id": task.TaskID,
"user_id": task.UserID,
"status": task.Status,
"progress": gin.H{
"total_slides": task.Total,
"completed_slides": task.Completed,
"percentage": percentage,
},
"slides": task.Slides,
"error_message": task.ErrorMessage,
"content": task.Content,
"prompt": task.Prompt,
"title": task.Title,
"thumb": task.Thumb,
}
}
// JobDetail 管理后台查看指定 PPT 任务详情
func (h *PPTHandler) JobDetail(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
task, ok := h.pptService.GetTask(taskID)
if !ok {
resp.ERROR(c, "任务不存在")
return
}
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
resp.SUCCESS(c, buildAdminPPTTaskDetail(task))
}
// ExportJob 管理后台导出 PPT 任务
func (h *PPTHandler) ExportJob(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
ef, ok := ppt.ParseExportFormat(c.Query("format"))
if !ok {
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
return
}
task, exists := h.pptService.GetTask(taskID)
if !exists {
resp.ERROR(c, "任务不存在")
return
}
if task.Status != ppt.TaskStatusCompleted {
resp.ERROR(c, "仅已完成任务可导出")
return
}
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
ossCfg := types.OSSConfig{}
if h.App.SysConfig != nil {
ossCfg = h.App.SysConfig.OSS
}
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
if err != nil {
resp.ERROR(c, err.Error())
return
}
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
filename := base + ppt.ExportFileExt(ef)
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
c.Data(200, ppt.ExportMimeType(ef), data)
}
// Stats PPT 任务统计信息
func (h *PPTHandler) Stats(c *gin.Context) {
total, completed, processing, failed, pending := h.pptService.Stats()
resp.SUCCESS(c, gin.H{
"totalTasks": total,
"completedTasks": completed,
"processingTasks": processing,
"failedTasks": failed,
"pendingTasks": pending,
})
}