Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

472 righe
14 KiB

  1. package controller
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/model"
  12. "github.com/QuantumNous/new-api/setting"
  13. "io"
  14. "log"
  15. "net/http"
  16. "time"
  17. "github.com/gin-gonic/gin"
  18. "github.com/thanhpk/randstr"
  19. )
  20. const (
  21. PaymentMethodCreem = "creem"
  22. CreemSignatureHeader = "creem-signature"
  23. )
  24. var creemAdaptor = &CreemAdaptor{}
  25. // 生成HMAC-SHA256签名
  26. func generateCreemSignature(payload string, secret string) string {
  27. h := hmac.New(sha256.New, []byte(secret))
  28. h.Write([]byte(payload))
  29. return hex.EncodeToString(h.Sum(nil))
  30. }
  31. // 验证Creem webhook签名
  32. func verifyCreemSignature(payload string, signature string, secret string) bool {
  33. if secret == "" {
  34. log.Printf("Creem webhook secret not set")
  35. if setting.CreemTestMode {
  36. log.Printf("Skip Creem webhook sign verify in test mode")
  37. return true
  38. }
  39. return false
  40. }
  41. expectedSignature := generateCreemSignature(payload, secret)
  42. return hmac.Equal([]byte(signature), []byte(expectedSignature))
  43. }
  44. type CreemPayRequest struct {
  45. ProductId string `json:"product_id"`
  46. PaymentMethod string `json:"payment_method"`
  47. }
  48. type CreemProduct struct {
  49. ProductId string `json:"productId"`
  50. Name string `json:"name"`
  51. Price float64 `json:"price"`
  52. Currency string `json:"currency"`
  53. Quota int64 `json:"quota"`
  54. }
  55. type CreemAdaptor struct {
  56. }
  57. func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
  58. if req.PaymentMethod != PaymentMethodCreem {
  59. c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
  60. return
  61. }
  62. if req.ProductId == "" {
  63. c.JSON(200, gin.H{"message": "error", "data": "请选择产品"})
  64. return
  65. }
  66. // 解析产品列表
  67. var products []CreemProduct
  68. err := json.Unmarshal([]byte(setting.CreemProducts), &products)
  69. if err != nil {
  70. log.Println("解析Creem产品列表失败", err)
  71. c.JSON(200, gin.H{"message": "error", "data": "产品配置错误"})
  72. return
  73. }
  74. // 查找对应的产品
  75. var selectedProduct *CreemProduct
  76. for _, product := range products {
  77. if product.ProductId == req.ProductId {
  78. selectedProduct = &product
  79. break
  80. }
  81. }
  82. if selectedProduct == nil {
  83. c.JSON(200, gin.H{"message": "error", "data": "产品不存在"})
  84. return
  85. }
  86. id := c.GetInt("id")
  87. user, _ := model.GetUserById(id, false)
  88. // 生成唯一的订单引用ID
  89. reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
  90. referenceId := "ref_" + common.Sha1([]byte(reference))
  91. // 先创建订单记录,使用产品配置的金额和充值额度
  92. topUp := &model.TopUp{
  93. UserId: id,
  94. Amount: selectedProduct.Quota, // 充值额度
  95. Money: selectedProduct.Price, // 支付金额
  96. TradeNo: referenceId,
  97. PaymentProvider: model.PaymentProviderCreem,
  98. CreateTime: time.Now().Unix(),
  99. Status: common.TopUpStatusPending,
  100. }
  101. err = topUp.Insert()
  102. if err != nil {
  103. log.Printf("创建Creem订单失败: %v", err)
  104. c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
  105. return
  106. }
  107. // 创建支付链接,传入用户邮箱
  108. checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username)
  109. if err != nil {
  110. log.Printf("获取Creem支付链接失败: %v", err)
  111. c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
  112. return
  113. }
  114. log.Printf("Creem订单创建成功 - 用户ID: %d, 订单号: %s, 产品: %s, 充值额度: %d, 支付金额: %.2f",
  115. id, referenceId, selectedProduct.Name, selectedProduct.Quota, selectedProduct.Price)
  116. c.JSON(200, gin.H{
  117. "message": "success",
  118. "data": gin.H{
  119. "checkout_url": checkoutUrl,
  120. "order_id": referenceId,
  121. },
  122. })
  123. }
  124. func RequestCreemPay(c *gin.Context) {
  125. var req CreemPayRequest
  126. // 读取body内容用于打印,同时保留原始数据供后续使用
  127. bodyBytes, err := io.ReadAll(c.Request.Body)
  128. if err != nil {
  129. log.Printf("read creem pay req body err: %v", err)
  130. c.JSON(200, gin.H{"message": "error", "data": "read query error"})
  131. return
  132. }
  133. // 打印body内容
  134. log.Printf("creem pay request body: %s", string(bodyBytes))
  135. // 重新设置body供后续的ShouldBindJSON使用
  136. c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
  137. err = c.ShouldBindJSON(&req)
  138. if err != nil {
  139. c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
  140. return
  141. }
  142. creemAdaptor.RequestPay(c, &req)
  143. }
  144. // 新的Creem Webhook结构体,匹配实际的webhook数据格式
  145. type CreemWebhookEvent struct {
  146. Id string `json:"id"`
  147. EventType string `json:"eventType"`
  148. CreatedAt int64 `json:"created_at"`
  149. Object struct {
  150. Id string `json:"id"`
  151. Object string `json:"object"`
  152. RequestId string `json:"request_id"`
  153. Order struct {
  154. Object string `json:"object"`
  155. Id string `json:"id"`
  156. Customer string `json:"customer"`
  157. Product string `json:"product"`
  158. Amount int `json:"amount"`
  159. Currency string `json:"currency"`
  160. SubTotal int `json:"sub_total"`
  161. TaxAmount int `json:"tax_amount"`
  162. AmountDue int `json:"amount_due"`
  163. AmountPaid int `json:"amount_paid"`
  164. Status string `json:"status"`
  165. Type string `json:"type"`
  166. Transaction string `json:"transaction"`
  167. CreatedAt string `json:"created_at"`
  168. UpdatedAt string `json:"updated_at"`
  169. Mode string `json:"mode"`
  170. } `json:"order"`
  171. Product struct {
  172. Id string `json:"id"`
  173. Object string `json:"object"`
  174. Name string `json:"name"`
  175. Description string `json:"description"`
  176. Price int `json:"price"`
  177. Currency string `json:"currency"`
  178. BillingType string `json:"billing_type"`
  179. BillingPeriod string `json:"billing_period"`
  180. Status string `json:"status"`
  181. TaxMode string `json:"tax_mode"`
  182. TaxCategory string `json:"tax_category"`
  183. DefaultSuccessUrl *string `json:"default_success_url"`
  184. CreatedAt string `json:"created_at"`
  185. UpdatedAt string `json:"updated_at"`
  186. Mode string `json:"mode"`
  187. } `json:"product"`
  188. Units int `json:"units"`
  189. Customer struct {
  190. Id string `json:"id"`
  191. Object string `json:"object"`
  192. Email string `json:"email"`
  193. Name string `json:"name"`
  194. Country string `json:"country"`
  195. CreatedAt string `json:"created_at"`
  196. UpdatedAt string `json:"updated_at"`
  197. Mode string `json:"mode"`
  198. } `json:"customer"`
  199. Status string `json:"status"`
  200. Metadata map[string]string `json:"metadata"`
  201. Mode string `json:"mode"`
  202. } `json:"object"`
  203. }
  204. func CreemWebhook(c *gin.Context) {
  205. if !isCreemWebhookEnabled() {
  206. log.Printf("Creem webhook 被拒绝: webhook 未配置或已禁用 (client_ip=%s)\n", c.ClientIP())
  207. c.AbortWithStatus(http.StatusForbidden)
  208. return
  209. }
  210. // 读取body内容用于打印,同时保留原始数据供后续使用
  211. bodyBytes, err := io.ReadAll(c.Request.Body)
  212. if err != nil {
  213. log.Printf("读取Creem Webhook请求body失败: %v", err)
  214. c.AbortWithStatus(http.StatusBadRequest)
  215. return
  216. }
  217. // 获取签名头
  218. signature := c.GetHeader(CreemSignatureHeader)
  219. // 打印关键信息(避免输出完整敏感payload)
  220. log.Printf("Creem Webhook - URI: %s", c.Request.RequestURI)
  221. if setting.CreemTestMode {
  222. log.Printf("Creem Webhook - Signature: %s , Body: %s", signature, bodyBytes)
  223. } else if signature == "" {
  224. log.Printf("Creem Webhook缺少签名头")
  225. c.AbortWithStatus(http.StatusUnauthorized)
  226. return
  227. }
  228. // 验证签名
  229. if !verifyCreemSignature(string(bodyBytes), signature, setting.CreemWebhookSecret) {
  230. log.Printf("Creem Webhook签名验证失败")
  231. c.AbortWithStatus(http.StatusUnauthorized)
  232. return
  233. }
  234. log.Printf("Creem Webhook签名验证成功")
  235. // 重新设置body供后续的ShouldBindJSON使用
  236. c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
  237. // 解析新格式的webhook数据
  238. var webhookEvent CreemWebhookEvent
  239. if err := c.ShouldBindJSON(&webhookEvent); err != nil {
  240. log.Printf("解析Creem Webhook参数失败: %v", err)
  241. c.AbortWithStatus(http.StatusBadRequest)
  242. return
  243. }
  244. log.Printf("Creem Webhook解析成功 - EventType: %s, EventId: %s", webhookEvent.EventType, webhookEvent.Id)
  245. // 根据事件类型处理不同的webhook
  246. switch webhookEvent.EventType {
  247. case "checkout.completed":
  248. handleCheckoutCompleted(c, &webhookEvent)
  249. default:
  250. log.Printf("忽略Creem Webhook事件类型: %s", webhookEvent.EventType)
  251. c.Status(http.StatusOK)
  252. }
  253. }
  254. // 处理支付完成事件
  255. func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) {
  256. // 验证订单状态
  257. if event.Object.Order.Status != "paid" {
  258. log.Printf("订单状态不是已支付: %s, 跳过处理", event.Object.Order.Status)
  259. c.Status(http.StatusOK)
  260. return
  261. }
  262. // 获取引用ID(这是我们创建订单时传递的request_id)
  263. referenceId := event.Object.RequestId
  264. if referenceId == "" {
  265. log.Println("Creem Webhook缺少request_id字段")
  266. c.AbortWithStatus(http.StatusBadRequest)
  267. return
  268. }
  269. // Try complete subscription order first
  270. LockOrder(referenceId)
  271. defer UnlockOrder(referenceId)
  272. if err := model.CompleteSubscriptionOrder(referenceId, common.GetJsonString(event)); err == nil {
  273. c.Status(http.StatusOK)
  274. return
  275. } else if err != nil && !errors.Is(err, model.ErrSubscriptionOrderNotFound) {
  276. log.Printf("Creem订阅订单处理失败: %s, 订单号: %s", err.Error(), referenceId)
  277. c.AbortWithStatus(http.StatusInternalServerError)
  278. return
  279. }
  280. // 验证订单类型,目前只处理一次性付款(充值)
  281. if event.Object.Order.Type != "onetime" {
  282. log.Printf("暂不支持的订单类型: %s, 跳过处理", event.Object.Order.Type)
  283. c.Status(http.StatusOK)
  284. return
  285. }
  286. // 记录详细的支付信息
  287. log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: <redacted>, 产品: %s",
  288. referenceId,
  289. event.Object.Order.Id,
  290. event.Object.Order.AmountPaid,
  291. event.Object.Order.Currency,
  292. event.Object.Product.Name)
  293. // 查询本地订单确认存在
  294. topUp := model.GetTopUpByTradeNo(referenceId)
  295. if topUp == nil {
  296. log.Printf("Creem充值订单不存在: %s", referenceId)
  297. c.AbortWithStatus(http.StatusBadRequest)
  298. return
  299. }
  300. if topUp.Status != common.TopUpStatusPending {
  301. log.Printf("Creem充值订单状态错误: %s, 当前状态: %s", referenceId, topUp.Status)
  302. c.Status(http.StatusOK) // 已处理过的订单,返回成功避免重复处理
  303. return
  304. }
  305. // 处理充值,传入客户邮箱和姓名信息
  306. customerEmail := event.Object.Customer.Email
  307. customerName := event.Object.Customer.Name
  308. // 防护性检查,确保邮箱和姓名不为空字符串
  309. if customerEmail == "" {
  310. log.Printf("警告:Creem回调中客户邮箱为空 - 订单号: %s", referenceId)
  311. }
  312. if customerName == "" {
  313. log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId)
  314. }
  315. err := model.RechargeCreem(referenceId, customerEmail, customerName)
  316. if err != nil {
  317. log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
  318. c.AbortWithStatus(http.StatusInternalServerError)
  319. return
  320. }
  321. log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f",
  322. referenceId, topUp.Amount, topUp.Money)
  323. c.Status(http.StatusOK)
  324. }
  325. type CreemCheckoutRequest struct {
  326. ProductId string `json:"product_id"`
  327. RequestId string `json:"request_id"`
  328. Customer struct {
  329. Email string `json:"email"`
  330. } `json:"customer"`
  331. Metadata map[string]string `json:"metadata,omitempty"`
  332. }
  333. type CreemCheckoutResponse struct {
  334. CheckoutUrl string `json:"checkout_url"`
  335. Id string `json:"id"`
  336. }
  337. func genCreemLink(referenceId string, product *CreemProduct, email string, username string) (string, error) {
  338. if setting.CreemApiKey == "" {
  339. return "", fmt.Errorf("未配置Creem API密钥")
  340. }
  341. // 根据测试模式选择 API 端点
  342. apiUrl := "https://api.creem.io/v1/checkouts"
  343. if setting.CreemTestMode {
  344. apiUrl = "https://test-api.creem.io/v1/checkouts"
  345. log.Printf("使用Creem测试环境: %s", apiUrl)
  346. }
  347. // 构建请求数据,确保包含用户邮箱
  348. requestData := CreemCheckoutRequest{
  349. ProductId: product.ProductId,
  350. RequestId: referenceId, // 这个作为订单ID传递给Creem
  351. Customer: struct {
  352. Email string `json:"email"`
  353. }{
  354. Email: email, // 用户邮箱会在支付页面预填充
  355. },
  356. Metadata: map[string]string{
  357. "username": username,
  358. "reference_id": referenceId,
  359. "product_name": product.Name,
  360. "quota": fmt.Sprintf("%d", product.Quota),
  361. },
  362. }
  363. // 序列化请求数据
  364. jsonData, err := json.Marshal(requestData)
  365. if err != nil {
  366. return "", fmt.Errorf("序列化请求数据失败: %v", err)
  367. }
  368. // 创建 HTTP 请求
  369. req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData))
  370. if err != nil {
  371. return "", fmt.Errorf("创建HTTP请求失败: %v", err)
  372. }
  373. // 设置请求头
  374. req.Header.Set("Content-Type", "application/json")
  375. req.Header.Set("x-api-key", setting.CreemApiKey)
  376. log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s",
  377. apiUrl, product.ProductId, email, referenceId)
  378. // 发送请求
  379. client := &http.Client{
  380. Timeout: 30 * time.Second,
  381. }
  382. resp, err := client.Do(req)
  383. if err != nil {
  384. return "", fmt.Errorf("发送HTTP请求失败: %v", err)
  385. }
  386. defer resp.Body.Close()
  387. // 读取响应
  388. body, err := io.ReadAll(resp.Body)
  389. if err != nil {
  390. return "", fmt.Errorf("读取响应失败: %v", err)
  391. }
  392. log.Printf("Creem API resp - status code: %d, resp: %s", resp.StatusCode, string(body))
  393. // 检查响应状态
  394. if resp.StatusCode/100 != 2 {
  395. return "", fmt.Errorf("Creem API http status %d ", resp.StatusCode)
  396. }
  397. // 解析响应
  398. var checkoutResp CreemCheckoutResponse
  399. err = json.Unmarshal(body, &checkoutResp)
  400. if err != nil {
  401. return "", fmt.Errorf("解析响应失败: %v", err)
  402. }
  403. if checkoutResp.CheckoutUrl == "" {
  404. return "", fmt.Errorf("Creem API resp no checkout url ")
  405. }
  406. log.Printf("Creem 支付链接创建成功 - 订单号: %s, 支付链接: %s", referenceId, checkoutResp.CheckoutUrl)
  407. return checkoutResp.CheckoutUrl, nil
  408. }