feat: 迁移 geekai-plus v4.3.1 到开源版

- 同步 Plus v4.3.1 功能源并移除商业 License 闭环
- 更新开源镜像命名、Docker 部署版本和 geekai 数据库配置
- 补充前端 ESLint 检查配置并修复存量解析与模板问题
- 保留 JWT、管理员权限、API Key 和 OAuth 等正常鉴权机制
This commit is contained in:
RockYang
2026-09-13 14:37:12 +08:00
parent 3e66c5ac5b
commit d4fd38ab7e
96 changed files with 7164 additions and 3047 deletions
+115
View File
@@ -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)
}
+8 -7
View File
@@ -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%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
remark = fmt.Sprintf("任务失败,退回积分。任务ID%dErr: %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%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
remark = fmt.Sprintf("任务失败,退回积分。任务ID%dErr: %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,
+12 -10
View File
@@ -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)
-1
View File
@@ -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, // 长期有效
-22
View File
@@ -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)
+7
View File
@@ -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)
}
+10 -6
View File
@@ -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)
}
+188 -1
View File
@@ -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
}
+6 -2
View File
@@ -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)
+78
View File
@@ -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")
}
+10 -8
View File
@@ -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
}
-14
View File
@@ -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)
}
+81 -5
View File
@@ -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)
-6
View File
@@ -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, "创建菜单成功")
}