mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-09-16 18:27:13 +00:00
feat: 迁移 geekai-plus v4.3.1 到开源版
- 同步 Plus v4.3.1 功能源并移除商业 License 闭环 - 更新开源镜像命名、Docker 部署版本和 geekai 数据库配置 - 补充前端 ESLint 检查配置并修复存量解析与模板问题 - 保留 JWT、管理员权限、API Key 和 OAuth 等正常鉴权机制
This commit is contained in:
@@ -9,6 +9,8 @@ package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
@@ -31,6 +33,7 @@ type ConfigHandler struct {
|
||||
alipayService *payment.AlipayService
|
||||
wxpayService *payment.WxPayService
|
||||
epayService *payment.EPayService
|
||||
stripeService *payment.StripeService
|
||||
smsManager *sms.SmsManager
|
||||
uploaderManager *oss.UploaderManager
|
||||
smtpService *service.SmtpService
|
||||
@@ -46,6 +49,7 @@ func NewConfigHandler(
|
||||
alipayService *payment.AlipayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
epayService *payment.EPayService,
|
||||
stripeService *payment.StripeService,
|
||||
smsManager *sms.SmsManager,
|
||||
uploaderManager *oss.UploaderManager,
|
||||
smtpService *service.SmtpService,
|
||||
@@ -59,6 +63,7 @@ func NewConfigHandler(
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
epayService: epayService,
|
||||
stripeService: stripeService,
|
||||
smsManager: smsManager,
|
||||
uploaderManager: uploaderManager,
|
||||
smtpService: smtpService,
|
||||
@@ -89,6 +94,9 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg.POST("update/smtp", h.UpdateStmp)
|
||||
rg.GET("get", h.Get)
|
||||
rg.POST("update/wx_gzh", h.UpdateWxGzh)
|
||||
rg.POST("update/wx_gzh_menu", h.UpdateWxGzhMenu)
|
||||
rg.POST("wx_gzh/menu/publish", h.PublishWxGzhMenu)
|
||||
rg.GET("wx_gzh/menu/query", h.QueryWxGzhMenu)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +290,9 @@ func (h *ConfigHandler) UpdatePayment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if data.Stripe.Enabled {
|
||||
h.stripeService.UpdateConfig(&data.Stripe)
|
||||
}
|
||||
|
||||
h.sysConfig.Payment = data
|
||||
resp.SUCCESS(c, data)
|
||||
@@ -335,6 +346,10 @@ func (h *ConfigHandler) UpdateStmp(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if data.Host == "" || data.Port <= 0 {
|
||||
resp.ERROR(c, "邮件服务器地址和端口不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySmtp, data)
|
||||
if err != nil {
|
||||
@@ -422,3 +437,103 @@ func (h *ConfigHandler) UpdateWxGzh(c *gin.Context) {
|
||||
h.sysConfig.WxGzh = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateWxGzhMenu 保存公众号自定义菜单草稿(不调用微信接口)。
|
||||
func (h *ConfigHandler) UpdateWxGzhMenu(c *gin.Context) {
|
||||
var data types.WxGzhMenuConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if data.Button == nil {
|
||||
data.Button = []types.WxGzhMenuButton{}
|
||||
}
|
||||
if err := h.Update(types.ConfigKeyWxGzhMenu, data); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) loadWxGzhCredentials() (appID, secret string, err error) {
|
||||
var row model.Config
|
||||
if err = h.DB.Where("name", types.ConfigKeyWxGzh).First(&row).Error; err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var wx types.WxGzhConfig
|
||||
if err = utils.JsonDecode(row.Value, &wx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return strings.TrimSpace(wx.AppId), strings.TrimSpace(wx.Secret), nil
|
||||
}
|
||||
|
||||
// PublishWxGzhMenu 将草稿菜单同步到微信公众平台(menu/create)。
|
||||
func (h *ConfigHandler) PublishWxGzhMenu(c *gin.Context) {
|
||||
appID, secret, err := h.loadWxGzhCredentials()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
resp.ERROR(c, "请先配置微信公众号 AppID 与 AppSecret")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if appID == "" || secret == "" {
|
||||
resp.ERROR(c, "请先配置微信公众号 AppID 与 AppSecret")
|
||||
return
|
||||
}
|
||||
|
||||
var menu types.WxGzhMenuConfig
|
||||
var menuRow model.Config
|
||||
if err := h.DB.Where("name", types.ConfigKeyWxGzhMenu).First(&menuRow).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := utils.JsonDecode(menuRow.Value, &menu); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if menu.Button == nil {
|
||||
menu.Button = []types.WxGzhMenuButton{}
|
||||
}
|
||||
|
||||
if err := h.wxGzhService.PublishCustomMenu(c.Request.Context(), appID, secret, menu); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"message": "已同步到微信"})
|
||||
}
|
||||
|
||||
// QueryWxGzhMenu 从微信拉取当前菜单并写入草稿 wx_gzh_menu。
|
||||
func (h *ConfigHandler) QueryWxGzhMenu(c *gin.Context) {
|
||||
appID, secret, err := h.loadWxGzhCredentials()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
resp.ERROR(c, "请先配置微信公众号 AppID 与 AppSecret")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if appID == "" || secret == "" {
|
||||
resp.ERROR(c, "请先配置微信公众号 AppID 与 AppSecret")
|
||||
return
|
||||
}
|
||||
|
||||
menuCfg, err := h.wxGzhService.GetCustomMenuFromWechat(c.Request.Context(), appID, secret)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if menuCfg.Button == nil {
|
||||
menuCfg.Button = []types.WxGzhMenuButton{}
|
||||
}
|
||||
if err := h.Update(types.ConfigKeyWxGzhMenu, menuCfg); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, menuCfg)
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ func (h *ImageHandler) ImageList(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
job.Publish = item.Publish != 0
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
@@ -148,8 +149,8 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, imgURL string
|
||||
var power, userId, progress int
|
||||
var md, remark, imgURL, status string
|
||||
var power, userId int
|
||||
switch tab {
|
||||
case "mj":
|
||||
var job model.MidJourneyJob
|
||||
@@ -161,8 +162,8 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
md = "mid-journey"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
remark = fmt.Sprintf("任务失败,退回积分。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
status = job.Status
|
||||
imgURL = job.ImgURL
|
||||
case "image":
|
||||
var job model.ImageJob
|
||||
@@ -176,15 +177,15 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
md = "image-generation"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
remark = fmt.Sprintf("任务失败,退回积分。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
status = job.Status
|
||||
imgURL = job.ImgURL
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
if status != model.ImageStatusSuccess {
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
|
||||
@@ -41,12 +41,13 @@ func (h *ProductHandler) RegisterRoutes() {
|
||||
|
||||
func (h *ProductHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
StripePrice float64 `json:"stripe_price"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -54,10 +55,11 @@ func (h *ProductHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
item := model.Product{
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
StripePrice: data.StripePrice,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
|
||||
@@ -243,7 +243,6 @@ func (h *UserHandler) ImportUsers(c *gin.Context) {
|
||||
Salt: salt,
|
||||
Power: power,
|
||||
Status: status,
|
||||
ChatRoles: utils.JsonEncode([]string{}),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode([]int{}),
|
||||
ExpiredTime: 0, // 长期有效
|
||||
|
||||
@@ -40,7 +40,6 @@ func (h *ChatAppHandler) RegisterRoutes() {
|
||||
group.POST("create", h.Create)
|
||||
group.POST("copy", h.Copy)
|
||||
group.POST("update", h.UpdateApp)
|
||||
group.POST("workspace", h.UpdateWorkArea)
|
||||
group.POST("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
@@ -217,27 +216,6 @@ func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
|
||||
resp.SUCCESS(c, nil)
|
||||
}
|
||||
|
||||
// UpdateWorkArea 更新用户工作区应用列表(存为应用 id 数组)
|
||||
func (h *ChatAppHandler) UpdateWorkArea(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if err := h.DB.Model(&model.User{}).Where("id = ?", userId).Update("chat_roles_json", utils.JsonEncode(body.Ids)).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, nil)
|
||||
}
|
||||
|
||||
// Remove 删除用户智能体(仅允许删除自己创建的)
|
||||
func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
@@ -70,6 +70,13 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
delete(value, "token")
|
||||
delete(value, "encoding_aes_key")
|
||||
}
|
||||
if key == types.ConfigKeySystem {
|
||||
if value == nil {
|
||||
value = make(map[string]any)
|
||||
}
|
||||
// 支付开关在 payment 配置中,前端会员页仅需是否展示 Stripe
|
||||
value["stripe_pay_enabled"] = h.sysConfig.Payment.Stripe.Enabled
|
||||
}
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +127,12 @@ func (h *ImageJobHandler) Image(c *gin.Context) {
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.ImageJob{
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: chatModel.Power,
|
||||
Params: utils.JsonEncode(task),
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: chatModel.Power,
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
Params: utils.JsonEncode(task),
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
@@ -189,9 +191,9 @@ func (h *ImageJobHandler) getData(finish bool, userId uint, page int, pageSize i
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
session = session.Where("status IN ?", []string{model.ImageStatusSuccess, model.ImageStatusFailed}).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
session = session.Where("status IN ?", []string{model.ImageStatusPending, model.ImageStatusInProgress, model.ImageStatusDownloading}).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
@@ -221,6 +223,8 @@ func (h *ImageJobHandler) getData(finish bool, userId uint, page int, pageSize i
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
// CopyObject skips int→bool (model.Publish vs vo.Publish); keep list in sync with DB
|
||||
job.Publish = item.Publish != 0
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if jimeng.IsSeedanceReqKey(req.ReqKey) {
|
||||
if err := h.validateSeedanceRequest(&req); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
|
||||
@@ -83,7 +89,7 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
if req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||
if !jimeng.IsSeedanceReqKey(req.ReqKey) && req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||
resp.ERROR(c, "提示词和图片不能同时为空")
|
||||
return
|
||||
}
|
||||
@@ -315,6 +321,9 @@ func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
|
||||
if req.Duration == 0 {
|
||||
return 0, errors.New("视频时长不能为0")
|
||||
}
|
||||
if req.Duration == -1 {
|
||||
return basePower, nil
|
||||
}
|
||||
return basePower * req.Duration, nil
|
||||
case types.JMTaskTypeVirtualHuman:
|
||||
if req.AudioURL == "" {
|
||||
@@ -354,3 +363,181 @@ func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||
"powers": config.Powers,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *JimengHandler) validateSeedanceRequest(req *types.JimengTaskRequest) error {
|
||||
if req.Duration != 0 && req.Duration != -1 {
|
||||
if req.Duration < 4 || req.Duration > 15 {
|
||||
return errors.New("Seedance 视频时长必须在 4-15 秒之间,或使用 -1 智能时长")
|
||||
}
|
||||
}
|
||||
|
||||
if req.AspectRatio != "" {
|
||||
allowedRatio := map[string]bool{
|
||||
"16:9": true, "4:3": true, "1:1": true, "3:4": true, "9:16": true, "21:9": true, "adaptive": true,
|
||||
}
|
||||
if !allowedRatio[req.AspectRatio] {
|
||||
return errors.New("不支持的视频比例参数")
|
||||
}
|
||||
}
|
||||
|
||||
if req.Resolution != "" {
|
||||
allowedResolution := map[string]bool{"480p": true, "720p": true, "1080p": true}
|
||||
if !allowedResolution[req.Resolution] {
|
||||
return errors.New("不支持的视频分辨率参数")
|
||||
}
|
||||
}
|
||||
|
||||
if req.ReqKey == "doubao-seedance-2-0-fast-260128" && req.Resolution == "1080p" {
|
||||
return errors.New("Seedance 2.0 Fast 不支持 1080p")
|
||||
}
|
||||
|
||||
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
|
||||
if req.VideoURL != "" || req.AudioURL != "" {
|
||||
return errors.New("当前 Seedance 模型不支持视频或音频输入")
|
||||
}
|
||||
}
|
||||
|
||||
content := req.Content
|
||||
if len(content) == 0 {
|
||||
content = h.buildCompatSeedanceContent(req)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return errors.New("Seedance 至少需要文本、图片或视频其中之一")
|
||||
}
|
||||
|
||||
imageRoleCount := map[string]int{}
|
||||
hasImageOrVideo := false
|
||||
hasAudio := false
|
||||
hasText := false
|
||||
|
||||
for _, item := range content {
|
||||
switch item.Type {
|
||||
case "text":
|
||||
if item.Text == "" {
|
||||
return errors.New("文本输入不能为空")
|
||||
}
|
||||
hasText = true
|
||||
case "image_url":
|
||||
if item.ImageURL == nil || item.ImageURL.URL == "" {
|
||||
return errors.New("图片输入缺少 url")
|
||||
}
|
||||
role := item.Role
|
||||
if role == "" {
|
||||
role = "first_frame"
|
||||
}
|
||||
if role != "first_frame" && role != "last_frame" && role != "reference_image" {
|
||||
return errors.New("图片 role 仅支持 first_frame、last_frame、reference_image")
|
||||
}
|
||||
imageRoleCount[role]++
|
||||
hasImageOrVideo = true
|
||||
case "video_url":
|
||||
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
|
||||
return errors.New("当前 Seedance 模型不支持视频输入")
|
||||
}
|
||||
if item.VideoURL == nil || item.VideoURL.URL == "" {
|
||||
return errors.New("视频输入缺少 url")
|
||||
}
|
||||
if item.Role != "" && item.Role != "reference_video" {
|
||||
return errors.New("视频 role 仅支持 reference_video")
|
||||
}
|
||||
hasImageOrVideo = true
|
||||
case "audio_url":
|
||||
if !h.seedanceSupportsVideoAudio(req.ReqKey) {
|
||||
return errors.New("当前 Seedance 模型不支持音频输入")
|
||||
}
|
||||
if item.AudioURL == nil || item.AudioURL.URL == "" {
|
||||
return errors.New("音频输入缺少 url")
|
||||
}
|
||||
if item.Role != "" && item.Role != "reference_audio" {
|
||||
return errors.New("音频 role 仅支持 reference_audio")
|
||||
}
|
||||
hasAudio = true
|
||||
default:
|
||||
return fmt.Errorf("不支持的 Seedance content 类型: %s", item.Type)
|
||||
}
|
||||
}
|
||||
|
||||
if hasAudio && !hasImageOrVideo {
|
||||
return errors.New("音频不可单独输入,必须搭配图片或视频")
|
||||
}
|
||||
if imageRoleCount["first_frame"] > 1 {
|
||||
return errors.New("first_frame 最多只能上传 1 张")
|
||||
}
|
||||
if imageRoleCount["last_frame"] > 1 {
|
||||
return errors.New("last_frame 最多只能上传 1 张")
|
||||
}
|
||||
if imageRoleCount["reference_image"] > 9 {
|
||||
return errors.New("reference_image 最多支持 9 张")
|
||||
}
|
||||
if imageRoleCount["reference_image"] > 0 && (imageRoleCount["first_frame"] > 0 || imageRoleCount["last_frame"] > 0) {
|
||||
return errors.New("reference_image 模式不能与 first_frame/last_frame 混用")
|
||||
}
|
||||
if imageRoleCount["last_frame"] > 0 && imageRoleCount["first_frame"] == 0 {
|
||||
return errors.New("使用 last_frame 时必须同时提供 first_frame")
|
||||
}
|
||||
if imageRoleCount["first_frame"] > 0 && imageRoleCount["last_frame"] == 0 && imageRoleCount["reference_image"] == 0 && !hasText {
|
||||
return errors.New("图生视频至少需要提示词或补充参考素材")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *JimengHandler) seedanceSupportsVideoAudio(reqKey string) bool {
|
||||
return reqKey == "doubao-seedance-2-0-260128" || reqKey == "doubao-seedance-2-0-fast-260128"
|
||||
}
|
||||
|
||||
func (h *JimengHandler) buildCompatSeedanceContent(req *types.JimengTaskRequest) []types.JMContentItem {
|
||||
content := make([]types.JMContentItem, 0, 4)
|
||||
if req.Prompt != "" {
|
||||
content = append(content, types.JMContentItem{
|
||||
Type: "text",
|
||||
Text: req.Prompt,
|
||||
})
|
||||
}
|
||||
|
||||
if len(req.ImageUrls) > 0 {
|
||||
for index, imageURL := range req.ImageUrls {
|
||||
if imageURL == "" {
|
||||
continue
|
||||
}
|
||||
role := "reference_image"
|
||||
if len(req.ImageUrls) == 1 {
|
||||
role = "first_frame"
|
||||
} else if len(req.ImageUrls) == 2 {
|
||||
if index == 0 {
|
||||
role = "first_frame"
|
||||
} else {
|
||||
role = "last_frame"
|
||||
}
|
||||
}
|
||||
content = append(content, types.JMContentItem{
|
||||
Type: "image_url",
|
||||
ImageURL: &types.JMAssetRef{
|
||||
URL: imageURL,
|
||||
},
|
||||
Role: role,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if req.VideoURL != "" {
|
||||
content = append(content, types.JMContentItem{
|
||||
Type: "video_url",
|
||||
VideoURL: &types.JMAssetRef{
|
||||
URL: req.VideoURL,
|
||||
},
|
||||
Role: "reference_video",
|
||||
})
|
||||
}
|
||||
|
||||
if req.AudioURL != "" {
|
||||
content = append(content, types.JMContentItem{
|
||||
Type: "audio_url",
|
||||
AudioURL: &types.JMAssetRef{
|
||||
URL: req.AudioURL,
|
||||
},
|
||||
Role: "reference_audio",
|
||||
})
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
|
||||
Power: power,
|
||||
@@ -318,6 +319,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -375,6 +377,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -449,6 +452,7 @@ func (h *MidJourneyHandler) Modal(c *gin.Context) {
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
@@ -509,9 +513,9 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
session = session.Where("status IN ?", []string{model.ImageStatusSuccess, model.ImageStatusFailed}).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
session = session.Where("status IN ?", []string{model.ImageStatusPending, model.ImageStatusInProgress, model.ImageStatusDownloading}).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
|
||||
@@ -37,6 +37,7 @@ type PaymentHandler struct {
|
||||
alipayService *payment.AlipayService
|
||||
epayService *payment.EPayService
|
||||
wxpayService *payment.WxPayService
|
||||
stripeService *payment.StripeService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
@@ -49,6 +50,7 @@ func NewPaymentHandler(
|
||||
alipayService *payment.AlipayService,
|
||||
geekPayService *payment.EPayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
stripeService *payment.StripeService,
|
||||
db *gorm.DB,
|
||||
userService *service.UserService,
|
||||
snowflake *service.Snowflake,
|
||||
@@ -58,6 +60,7 @@ func NewPaymentHandler(
|
||||
alipayService: alipayService,
|
||||
epayService: geekPayService,
|
||||
wxpayService: wxpayService,
|
||||
stripeService: stripeService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
@@ -78,6 +81,7 @@ func (h *PaymentHandler) RegisterRoutes() {
|
||||
rg.POST("notify/alipay", h.AlipayNotify)
|
||||
rg.GET("notify/epay", h.EPayNotify)
|
||||
rg.POST("notify/wxpay", h.WxpayNotify)
|
||||
rg.POST("notify/stripe", h.StripeNotify)
|
||||
|
||||
// 需要用户登录的接口
|
||||
rg.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
@@ -143,6 +147,8 @@ func (h *PaymentHandler) SyncOrders() error {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
case payment.PayChannelStripe:
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单已关闭
|
||||
@@ -204,6 +210,7 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
|
||||
}
|
||||
|
||||
amount := product.Price
|
||||
stripeAmount := product.StripePrice
|
||||
var payURL, notifyURL string
|
||||
switch data.PayWay {
|
||||
case "wxpay":
|
||||
@@ -311,6 +318,57 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
case "stripe":
|
||||
if h.config.Stripe.Enabled {
|
||||
logger.Debugf("Stripe,%+v", data)
|
||||
data.Channel = payment.PayChannelStripe
|
||||
if h.config.Stripe.Domain != "" {
|
||||
data.Domain = h.config.Stripe.Domain
|
||||
}
|
||||
if stripeAmount <= 0 {
|
||||
resp.ERROR(c, "当前产品未配置 Stripe 价格")
|
||||
return
|
||||
}
|
||||
successURL := fmt.Sprintf("%s/member", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
Device: data.Device,
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%.2f", stripeAmount),
|
||||
Currency: h.config.Stripe.Currency,
|
||||
ReturnURL: successURL,
|
||||
}
|
||||
payURL, err = h.stripeService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate stripe checkout session: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else if h.config.Epay.Enabled {
|
||||
logger.Debugf("Stripe fall back to epay,%+v", data)
|
||||
data.Channel = payment.PayChannelEpay
|
||||
if h.config.Epay.Domain != "" {
|
||||
data.Domain = h.config.Epay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: data.PayWay,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
r, err := h.epayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
payURL = r
|
||||
} else {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
default:
|
||||
resp.ERROR(c, "不支持的支付渠道")
|
||||
return
|
||||
@@ -489,3 +547,23 @@ func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// StripeNotify Stripe 支付异步回调
|
||||
func (h *PaymentHandler) StripeNotify(c *gin.Context) {
|
||||
orderInfo, err := h.stripeService.TradeVerify(c.Request)
|
||||
logger.Infof("收到 Stripe 订单支付回调:%+v", orderInfo)
|
||||
if err != nil {
|
||||
logger.Errorf("订单校验失败:%v", err)
|
||||
c.String(http.StatusBadRequest, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
@@ -153,15 +153,15 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
Params: vo.SunoParam{
|
||||
Prompt: data.Prompt,
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
Tags: data.Tags,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Lyrics: data.Lyrics,
|
||||
Model: data.Model,
|
||||
Tags: data.Tags,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Lyrics: data.Lyrics,
|
||||
Model: data.Model,
|
||||
},
|
||||
Title: data.Title,
|
||||
Type: data.Type,
|
||||
@@ -169,6 +169,8 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
RefTaskId: data.RefTaskId,
|
||||
Power: h.App.SysConfig.Base.SunoPower,
|
||||
SongId: utils.RandString(32),
|
||||
Status: model.ImageStatusPending,
|
||||
Progress: 0,
|
||||
}
|
||||
if data.Lyrics != "" {
|
||||
job.Prompt = data.Lyrics
|
||||
@@ -266,7 +268,7 @@ func (h *SunoHandler) Remove(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 只有失败或者已完成的任务可以删除
|
||||
if !(job.Progress == service.FailTaskProgress || job.Progress == 100) {
|
||||
if job.Status != model.ImageStatusFailed && job.Status != model.ImageStatusSuccess {
|
||||
resp.ERROR(c, "只有失败和超时(10分钟)的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -488,20 +488,6 @@ func (h *UserHandler) Session(c *gin.Context) {
|
||||
h.DB.Model(&user).UpdateColumn("vip", false)
|
||||
}
|
||||
userVo.Id = user.Id
|
||||
// 工作区应用 ID 列表(历史可能为 key 数组,仅解析数字 ID)
|
||||
if user.ChatRoles != "" {
|
||||
var raw []interface{}
|
||||
if utils.JsonDecode(user.ChatRoles, &raw) == nil {
|
||||
for _, v := range raw {
|
||||
if n, ok := v.(float64); ok && n >= 0 {
|
||||
userVo.ChatRoles = append(userVo.ChatRoles, uint(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if userVo.ChatRoles == nil {
|
||||
userVo.ChatRoles = []uint{}
|
||||
}
|
||||
resp.SUCCESS(c, userVo)
|
||||
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ func (h *VideoHandler) RegisterRoutes() {
|
||||
{
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("tasks", h.TaskList)
|
||||
group.GET("works", h.WorkList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.GET("power-config", h.GetPowerConfig) // 获取算力配置
|
||||
@@ -151,11 +153,13 @@ func (h *VideoHandler) Create(c *gin.Context) {
|
||||
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: uint(userId),
|
||||
Type: data.Provider,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
Params: utils.JsonEncode(task),
|
||||
UserId: uint(userId),
|
||||
Type: data.Provider,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
Params: utils.JsonEncode(task),
|
||||
Status: types.VideoStatusPending,
|
||||
Progress: 0,
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
@@ -305,6 +309,78 @@ func (h *VideoHandler) List(c *gin.Context) {
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
// TaskList 任务列表:仅返回进行中的任务(pending / in_progress)
|
||||
func (h *VideoHandler) TaskList(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
t := c.Query("type")
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
session = session.Where("status IN ?", []string{types.VideoStatusPending, types.VideoStatusInProgress})
|
||||
|
||||
var list []model.VideoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]vo.VideoJob, 0, len(list))
|
||||
for _, v := range list {
|
||||
var item vo.VideoJob
|
||||
if err := utils.CopyObject(v, &item); err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, items)
|
||||
}
|
||||
|
||||
// WorkList 作品列表:仅返回 downloading / success / failed
|
||||
func (h *VideoHandler) WorkList(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
t := c.Query("type")
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
session = session.Where(
|
||||
"status IN ?",
|
||||
[]string{types.VideoStatusDownloading, types.VideoStatusSuccess, types.VideoStatusFailed},
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
items := make([]vo.VideoJob, 0, len(list))
|
||||
for _, v := range list {
|
||||
var item vo.VideoJob
|
||||
if err := utils.CopyObject(v, &item); err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
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)
|
||||
|
||||
@@ -87,9 +87,3 @@ func validateSignature(signature, token, timestamp, nonce string) bool {
|
||||
// 4. 与 signature 比对
|
||||
return hashed == signature
|
||||
}
|
||||
|
||||
// 创建微信菜单
|
||||
func (h *WxGzhHandler) CreateMenu(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, "创建菜单成功")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user