new-api/controller/topup_creem.go
2025-09-08 23:07:05 +08:00

257 lines
6.2 KiB
Go

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
}