topup_creem.go 14 KB

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