|
|
@@ -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
|
|
|
+}
|