| package controller |
|
|
| import ( |
| "bytes" |
| "crypto/hmac" |
| "crypto/sha256" |
| "encoding/hex" |
| "encoding/json" |
| "fmt" |
| "github.com/QuantumNous/new-api/common" |
| "github.com/QuantumNous/new-api/model" |
| "github.com/QuantumNous/new-api/setting" |
| "io" |
| "log" |
| "net/http" |
| "time" |
|
|
| "github.com/gin-gonic/gin" |
| "github.com/thanhpk/randstr" |
| ) |
|
|
| const ( |
| PaymentMethodCreem = "creem" |
| CreemSignatureHeader = "creem-signature" |
| ) |
|
|
| var creemAdaptor = &CreemAdaptor{} |
|
|
| |
| func generateCreemSignature(payload string, secret string) string { |
| h := hmac.New(sha256.New, []byte(secret)) |
| h.Write([]byte(payload)) |
| return hex.EncodeToString(h.Sum(nil)) |
| } |
|
|
| |
| func verifyCreemSignature(payload string, signature string, secret string) bool { |
| if secret == "" { |
| log.Printf("Creem webhook secret not set") |
| if setting.CreemTestMode { |
| log.Printf("Skip Creem webhook sign verify in test mode") |
| return true |
| } |
| return false |
| } |
|
|
| expectedSignature := generateCreemSignature(payload, secret) |
| return hmac.Equal([]byte(signature), []byte(expectedSignature)) |
| } |
|
|
| 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)) |
|
|
| |
| 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 { |
| log.Printf("创建Creem订单失败: %v", err) |
| c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) |
| return |
| } |
|
|
| |
| checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username) |
| if err != nil { |
| log.Printf("获取Creem支付链接失败: %v", err) |
| c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) |
| return |
| } |
|
|
| log.Printf("Creem订单创建成功 - 用户ID: %d, 订单号: %s, 产品: %s, 充值额度: %d, 支付金额: %.2f", |
| id, referenceId, selectedProduct.Name, selectedProduct.Quota, selectedProduct.Price) |
|
|
| c.JSON(200, gin.H{ |
| "message": "success", |
| "data": gin.H{ |
| "checkout_url": checkoutUrl, |
| "order_id": referenceId, |
| }, |
| }) |
| } |
|
|
| func RequestCreemPay(c *gin.Context) { |
| var req CreemPayRequest |
|
|
| |
| bodyBytes, err := io.ReadAll(c.Request.Body) |
| if err != nil { |
| log.Printf("read creem pay req body err: %v", err) |
| c.JSON(200, gin.H{"message": "error", "data": "read query error"}) |
| return |
| } |
|
|
| |
| log.Printf("creem pay request body: %s", string(bodyBytes)) |
|
|
| |
| c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) |
|
|
| err = c.ShouldBindJSON(&req) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) |
| return |
| } |
| creemAdaptor.RequestPay(c, &req) |
| } |
|
|
| |
| type CreemWebhookEvent struct { |
| Id string `json:"id"` |
| EventType string `json:"eventType"` |
| CreatedAt int64 `json:"created_at"` |
| Object struct { |
| Id string `json:"id"` |
| Object string `json:"object"` |
| RequestId string `json:"request_id"` |
| Order struct { |
| Object string `json:"object"` |
| Id string `json:"id"` |
| Customer string `json:"customer"` |
| Product string `json:"product"` |
| Amount int `json:"amount"` |
| Currency string `json:"currency"` |
| SubTotal int `json:"sub_total"` |
| TaxAmount int `json:"tax_amount"` |
| AmountDue int `json:"amount_due"` |
| AmountPaid int `json:"amount_paid"` |
| Status string `json:"status"` |
| Type string `json:"type"` |
| Transaction string `json:"transaction"` |
| CreatedAt string `json:"created_at"` |
| UpdatedAt string `json:"updated_at"` |
| Mode string `json:"mode"` |
| } `json:"order"` |
| Product struct { |
| Id string `json:"id"` |
| Object string `json:"object"` |
| Name string `json:"name"` |
| Description string `json:"description"` |
| Price int `json:"price"` |
| Currency string `json:"currency"` |
| BillingType string `json:"billing_type"` |
| BillingPeriod string `json:"billing_period"` |
| Status string `json:"status"` |
| TaxMode string `json:"tax_mode"` |
| TaxCategory string `json:"tax_category"` |
| DefaultSuccessUrl *string `json:"default_success_url"` |
| CreatedAt string `json:"created_at"` |
| UpdatedAt string `json:"updated_at"` |
| Mode string `json:"mode"` |
| } `json:"product"` |
| Units int `json:"units"` |
| Customer struct { |
| Id string `json:"id"` |
| Object string `json:"object"` |
| Email string `json:"email"` |
| Name string `json:"name"` |
| Country string `json:"country"` |
| CreatedAt string `json:"created_at"` |
| UpdatedAt string `json:"updated_at"` |
| Mode string `json:"mode"` |
| } `json:"customer"` |
| Status string `json:"status"` |
| Metadata map[string]string `json:"metadata"` |
| Mode string `json:"mode"` |
| } `json:"object"` |
| } |
|
|
| |
| 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) { |
| |
| bodyBytes, err := io.ReadAll(c.Request.Body) |
| if err != nil { |
| log.Printf("读取Creem Webhook请求body失败: %v", err) |
| c.AbortWithStatus(http.StatusBadRequest) |
| return |
| } |
|
|
| |
| signature := c.GetHeader(CreemSignatureHeader) |
|
|
| |
| log.Printf("Creem Webhook - URI: %s", c.Request.RequestURI) |
| if setting.CreemTestMode { |
| log.Printf("Creem Webhook - Signature: %s , Body: %s", signature, bodyBytes) |
| } else if signature == "" { |
| log.Printf("Creem Webhook缺少签名头") |
| c.AbortWithStatus(http.StatusUnauthorized) |
| return |
| } |
|
|
| |
| if !verifyCreemSignature(string(bodyBytes), signature, setting.CreemWebhookSecret) { |
| log.Printf("Creem Webhook签名验证失败") |
| c.AbortWithStatus(http.StatusUnauthorized) |
| return |
| } |
|
|
| log.Printf("Creem Webhook签名验证成功") |
|
|
| |
| c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) |
|
|
| |
| var webhookEvent CreemWebhookEvent |
| if err := c.ShouldBindJSON(&webhookEvent); err != nil { |
| log.Printf("解析Creem Webhook参数失败: %v", err) |
| c.AbortWithStatus(http.StatusBadRequest) |
| return |
| } |
|
|
| log.Printf("Creem Webhook解析成功 - EventType: %s, EventId: %s", webhookEvent.EventType, webhookEvent.Id) |
|
|
| |
| switch webhookEvent.EventType { |
| case "checkout.completed": |
| handleCheckoutCompleted(c, &webhookEvent) |
| default: |
| log.Printf("忽略Creem Webhook事件类型: %s", webhookEvent.EventType) |
| c.Status(http.StatusOK) |
| } |
| } |
|
|
| |
| func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) { |
| |
| if event.Object.Order.Status != "paid" { |
| log.Printf("订单状态不是已支付: %s, 跳过处理", event.Object.Order.Status) |
| c.Status(http.StatusOK) |
| return |
| } |
|
|
| |
| referenceId := event.Object.RequestId |
| if referenceId == "" { |
| log.Println("Creem Webhook缺少request_id字段") |
| c.AbortWithStatus(http.StatusBadRequest) |
| return |
| } |
|
|
| |
| if event.Object.Order.Type != "onetime" { |
| log.Printf("暂不支持的订单类型: %s, 跳过处理", event.Object.Order.Type) |
| c.Status(http.StatusOK) |
| return |
| } |
|
|
| |
| log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: <redacted>, 产品: %s", |
| referenceId, |
| event.Object.Order.Id, |
| event.Object.Order.AmountPaid, |
| event.Object.Order.Currency, |
| event.Object.Product.Name) |
|
|
| |
| topUp := model.GetTopUpByTradeNo(referenceId) |
| if topUp == nil { |
| log.Printf("Creem充值订单不存在: %s", referenceId) |
| c.AbortWithStatus(http.StatusBadRequest) |
| return |
| } |
|
|
| if topUp.Status != common.TopUpStatusPending { |
| log.Printf("Creem充值订单状态错误: %s, 当前状态: %s", referenceId, topUp.Status) |
| c.Status(http.StatusOK) |
| return |
| } |
|
|
| |
| customerEmail := event.Object.Customer.Email |
| customerName := event.Object.Customer.Name |
|
|
| |
| if customerEmail == "" { |
| log.Printf("警告:Creem回调中客户邮箱为空 - 订单号: %s", referenceId) |
| } |
| if customerName == "" { |
| log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId) |
| } |
|
|
| err := model.RechargeCreem(referenceId, customerEmail, customerName) |
| if err != nil { |
| log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId) |
| c.AbortWithStatus(http.StatusInternalServerError) |
| return |
| } |
|
|
| log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f", |
| referenceId, topUp.Amount, topUp.Money) |
| 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密钥") |
| } |
|
|
| |
| apiUrl := "https://api.creem.io/v1/checkouts" |
| if setting.CreemTestMode { |
| apiUrl = "https://test-api.creem.io/v1/checkouts" |
| log.Printf("使用Creem测试环境: %s", apiUrl) |
| } |
|
|
| |
| requestData := CreemCheckoutRequest{ |
| ProductId: product.ProductId, |
| RequestId: referenceId, |
| Customer: struct { |
| Email string `json:"email"` |
| }{ |
| Email: email, |
| }, |
| Metadata: map[string]string{ |
| "username": username, |
| "reference_id": referenceId, |
| "product_name": product.Name, |
| "quota": fmt.Sprintf("%d", product.Quota), |
| }, |
| } |
|
|
| |
| jsonData, err := json.Marshal(requestData) |
| if err != nil { |
| return "", fmt.Errorf("序列化请求数据失败: %v", err) |
| } |
|
|
| |
| 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) |
|
|
| log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s", |
| apiUrl, product.ProductId, email, referenceId) |
|
|
| |
| 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() |
|
|
| |
| body, err := io.ReadAll(resp.Body) |
| if err != nil { |
| return "", fmt.Errorf("读取响应失败: %v", err) |
| } |
|
|
| log.Printf("Creem API resp - status code: %d, resp: %s", resp.StatusCode, string(body)) |
|
|
| |
| if resp.StatusCode/100 != 2 { |
| return "", fmt.Errorf("Creem API http status %d ", resp.StatusCode) |
| } |
| |
| var checkoutResp CreemCheckoutResponse |
| err = json.Unmarshal(body, &checkoutResp) |
| if err != nil { |
| return "", fmt.Errorf("解析响应失败: %v", err) |
| } |
|
|
| if checkoutResp.CheckoutUrl == "" { |
| return "", fmt.Errorf("Creem API resp no checkout url ") |
| } |
|
|
| log.Printf("Creem 支付链接创建成功 - 订单号: %s, 支付链接: %s", referenceId, checkoutResp.CheckoutUrl) |
| return checkoutResp.CheckoutUrl, nil |
| } |
|
|