mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 12:28:37 +00:00
完成 后端 部分,webo hhok 待完善
This commit is contained in:
@@ -74,6 +74,8 @@ func GetStatus(c *gin.Context) {
|
|||||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||||
|
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
|
||||||
|
"creem_products": setting.CreemProducts,
|
||||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||||
"chats": setting.Chats,
|
"chats": setting.Chats,
|
||||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||||
|
|||||||
256
controller/topup_creem.go
Normal file
256
controller/topup_creem.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/setting"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/thanhpk/randstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentMethodCreem = "creem"
|
||||||
|
)
|
||||||
|
|
||||||
|
var creemAdaptor = &CreemAdaptor{}
|
||||||
|
|
||||||
|
type CreemPayRequest struct {
|
||||||
|
ProductId string `json:"product_id"`
|
||||||
|
PaymentMethod string `json:"payment_method"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreemProduct struct {
|
||||||
|
ProductId string `json:"productId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Quota int64 `json:"quota"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreemAdaptor struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
|
||||||
|
if req.PaymentMethod != PaymentMethodCreem {
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ProductId == "" {
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "请选择产品"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析产品列表
|
||||||
|
var products []CreemProduct
|
||||||
|
err := json.Unmarshal([]byte(setting.CreemProducts), &products)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("解析Creem产品列表失败", err)
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "产品配置错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找对应的产品
|
||||||
|
var selectedProduct *CreemProduct
|
||||||
|
for _, product := range products {
|
||||||
|
if product.ProductId == req.ProductId {
|
||||||
|
selectedProduct = &product
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedProduct == nil {
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "产品不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user, _ := model.GetUserById(id, false)
|
||||||
|
|
||||||
|
reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
|
||||||
|
referenceId := "ref_" + common.Sha1([]byte(reference))
|
||||||
|
|
||||||
|
checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("获取Creem支付链接失败", err)
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
topUp := &model.TopUp{
|
||||||
|
UserId: id,
|
||||||
|
Amount: selectedProduct.Quota,
|
||||||
|
Money: selectedProduct.Price,
|
||||||
|
TradeNo: referenceId,
|
||||||
|
CreateTime: time.Now().Unix(),
|
||||||
|
Status: common.TopUpStatusPending,
|
||||||
|
}
|
||||||
|
err = topUp.Insert()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"message": "success",
|
||||||
|
"data": gin.H{
|
||||||
|
"checkout_url": checkoutUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestCreemPay(c *gin.Context) {
|
||||||
|
var req CreemPayRequest
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
creemAdaptor.RequestPay(c, &req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreemWebhookData struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data struct {
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreemWebhook(c *gin.Context) {
|
||||||
|
// 解析 webhook 数据
|
||||||
|
var webhookData CreemWebhookData
|
||||||
|
if err := c.ShouldBindJSON(&webhookData); err != nil {
|
||||||
|
log.Printf("解析Creem Webhook参数失败: %v\n", err)
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查事件类型
|
||||||
|
if webhookData.Type != "checkout.completed" {
|
||||||
|
log.Printf("忽略Creem Webhook事件类型: %s", webhookData.Type)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取引用ID
|
||||||
|
referenceId := webhookData.Data.RequestId
|
||||||
|
if referenceId == "" {
|
||||||
|
log.Println("Creem Webhook缺少request_id字段")
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付完成事件
|
||||||
|
err := model.RechargeCreem(referenceId)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Creem充值处理失败:", err.Error(), referenceId)
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem充值成功: %s", referenceId)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreemCheckoutRequest struct {
|
||||||
|
ProductId string `json:"product_id"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
Customer struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"customer"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreemCheckoutResponse struct {
|
||||||
|
CheckoutUrl string `json:"checkout_url"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func genCreemLink(referenceId string, product *CreemProduct, email string, username string) (string, error) {
|
||||||
|
if setting.CreemApiKey == "" {
|
||||||
|
return "", fmt.Errorf("未配置Creem API密钥")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据测试模式选择 API 端点
|
||||||
|
apiUrl := "https://api.creem.io/v1/checkouts"
|
||||||
|
if setting.CreemTestMode {
|
||||||
|
apiUrl = "https://test-api.creem.io/v1/checkouts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
requestData := CreemCheckoutRequest{
|
||||||
|
ProductId: product.ProductId,
|
||||||
|
RequestId: referenceId,
|
||||||
|
Customer: struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}{
|
||||||
|
Email: email,
|
||||||
|
},
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"reference_id": referenceId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化请求数据
|
||||||
|
jsonData, err := json.Marshal(requestData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("序列化请求数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTTP 请求
|
||||||
|
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("x-api-key", setting.CreemApiKey)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
log.Printf(" creem req host: %s, key %s req 【%s】", apiUrl, setting.CreemApiKey, jsonData)
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("Creem API 返回错误状态 %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var checkoutResp CreemCheckoutResponse
|
||||||
|
err = json.Unmarshal(body, &checkoutResp)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkoutResp.CheckoutUrl == "" {
|
||||||
|
return "", fmt.Errorf("Creem API 未返回支付链接")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem 支付链接创建成功: %s, 订单ID: %s", referenceId, checkoutResp.Id)
|
||||||
|
return checkoutResp.CheckoutUrl, nil
|
||||||
|
}
|
||||||
@@ -81,6 +81,9 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
||||||
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
||||||
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
||||||
|
common.OptionMap["CreemApiKey"] = setting.CreemApiKey
|
||||||
|
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
||||||
|
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
||||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||||
@@ -326,6 +329,12 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
||||||
case "StripeMinTopUp":
|
case "StripeMinTopUp":
|
||||||
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
||||||
|
case "CreemApiKey":
|
||||||
|
setting.CreemApiKey = value
|
||||||
|
case "CreemProducts":
|
||||||
|
setting.CreemProducts = value
|
||||||
|
case "CreemTestMode":
|
||||||
|
setting.CreemTestMode = value == "true"
|
||||||
case "TopupGroupRatio":
|
case "TopupGroupRatio":
|
||||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||||
case "GitHubClientId":
|
case "GitHubClientId":
|
||||||
|
|||||||
@@ -98,3 +98,52 @@ func Recharge(referenceId string, customerId string) (err error) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RechargeCreem(referenceId string) (err error) {
|
||||||
|
if referenceId == "" {
|
||||||
|
return errors.New("未提供支付单号")
|
||||||
|
}
|
||||||
|
|
||||||
|
var quota float64
|
||||||
|
topUp := &TopUp{}
|
||||||
|
|
||||||
|
refCol := "`trade_no`"
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
refCol = `"trade_no"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("充值订单不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if topUp.Status != common.TopUpStatusPending {
|
||||||
|
return errors.New("充值订单状态错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
topUp.CompleteTime = common.GetTimestamp()
|
||||||
|
topUp.Status = common.TopUpStatusSuccess
|
||||||
|
err = tx.Save(topUp).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creem 直接使用 Amount 作为充值额度
|
||||||
|
quota = float64(topUp.Amount)
|
||||||
|
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("充值失败," + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", common.FormatQuota(int(quota)), topUp.Money))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
||||||
|
|
||||||
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
||||||
|
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
||||||
|
|
||||||
userRoute := apiRouter.Group("/user")
|
userRoute := apiRouter.Group("/user")
|
||||||
{
|
{
|
||||||
@@ -64,6 +65,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
selfRoute.POST("/amount", controller.RequestAmount)
|
selfRoute.POST("/amount", controller.RequestAmount)
|
||||||
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
|
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
|
||||||
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
|
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
|
||||||
|
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
|
||||||
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
|
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
|
||||||
selfRoute.PUT("/setting", controller.UpdateUserSetting)
|
selfRoute.PUT("/setting", controller.UpdateUserSetting)
|
||||||
}
|
}
|
||||||
|
|||||||
5
setting/payment_creem.go
Normal file
5
setting/payment_creem.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
var CreemApiKey = ""
|
||||||
|
var CreemProducts = "[]"
|
||||||
|
var CreemTestMode = false
|
||||||
Reference in New Issue
Block a user