topup_waffo_pancake.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package controller
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/logger"
  10. "github.com/QuantumNous/new-api/model"
  11. "github.com/QuantumNous/new-api/service"
  12. "github.com/QuantumNous/new-api/setting"
  13. "github.com/QuantumNous/new-api/setting/operation_setting"
  14. "github.com/QuantumNous/new-api/setting/system_setting"
  15. "github.com/gin-gonic/gin"
  16. "github.com/shopspring/decimal"
  17. "github.com/thanhpk/randstr"
  18. )
  19. type WaffoPancakePayRequest struct {
  20. Amount int64 `json:"amount"`
  21. }
  22. func RequestWaffoPancakeAmount(c *gin.Context) {
  23. var req WaffoPancakePayRequest
  24. if err := c.ShouldBindJSON(&req); err != nil {
  25. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"})
  26. return
  27. }
  28. if req.Amount < int64(setting.WaffoPancakeMinTopUp) {
  29. c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)})
  30. return
  31. }
  32. id := c.GetInt("id")
  33. group, err := model.GetUserGroup(id, true)
  34. if err != nil {
  35. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
  36. return
  37. }
  38. payMoney := getWaffoPancakePayMoney(req.Amount, group)
  39. if payMoney <= 0.01 {
  40. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"})
  41. return
  42. }
  43. c.JSON(http.StatusOK, gin.H{"message": "success", "data": fmt.Sprintf("%.2f", payMoney)})
  44. }
  45. func getWaffoPancakePayMoney(amount int64, group string) float64 {
  46. dAmount := decimal.NewFromInt(amount)
  47. if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
  48. dAmount = dAmount.Div(decimal.NewFromFloat(common.QuotaPerUnit))
  49. }
  50. topupGroupRatio := common.GetTopupGroupRatio(group)
  51. if topupGroupRatio == 0 {
  52. topupGroupRatio = 1
  53. }
  54. discount := 1.0
  55. if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok && ds > 0 {
  56. discount = ds
  57. }
  58. payMoney := dAmount.
  59. Mul(decimal.NewFromFloat(setting.WaffoPancakeUnitPrice)).
  60. Mul(decimal.NewFromFloat(topupGroupRatio)).
  61. Mul(decimal.NewFromFloat(discount))
  62. return payMoney.InexactFloat64()
  63. }
  64. func normalizeWaffoPancakeTopUpAmount(amount int64) int64 {
  65. if operation_setting.GetQuotaDisplayType() != operation_setting.QuotaDisplayTypeTokens {
  66. return amount
  67. }
  68. normalized := decimal.NewFromInt(amount).
  69. Div(decimal.NewFromFloat(common.QuotaPerUnit)).
  70. IntPart()
  71. if normalized < 1 {
  72. return 1
  73. }
  74. return normalized
  75. }
  76. func formatWaffoPancakeAmount(payMoney float64) string {
  77. return decimal.NewFromFloat(payMoney).StringFixed(2)
  78. }
  79. func getWaffoPancakeBuyerEmail(user *model.User) string {
  80. if user != nil && strings.TrimSpace(user.Email) != "" {
  81. return user.Email
  82. }
  83. if user != nil {
  84. return fmt.Sprintf("%d@new-api.local", user.Id)
  85. }
  86. return ""
  87. }
  88. func getWaffoPancakeReturnURL() string {
  89. if strings.TrimSpace(setting.WaffoPancakeReturnURL) != "" {
  90. return setting.WaffoPancakeReturnURL
  91. }
  92. return strings.TrimRight(system_setting.ServerAddress, "/") + "/console/topup?show_history=true"
  93. }
  94. func RequestWaffoPancakePay(c *gin.Context) {
  95. if !setting.WaffoPancakeEnabled {
  96. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "Waffo Pancake 支付未启用"})
  97. return
  98. }
  99. currentWebhookKey := setting.WaffoPancakeWebhookPublicKey
  100. if setting.WaffoPancakeSandbox {
  101. currentWebhookKey = setting.WaffoPancakeWebhookTestKey
  102. }
  103. if strings.TrimSpace(setting.WaffoPancakeMerchantID) == "" ||
  104. strings.TrimSpace(setting.WaffoPancakePrivateKey) == "" ||
  105. strings.TrimSpace(currentWebhookKey) == "" ||
  106. strings.TrimSpace(setting.WaffoPancakeStoreID) == "" ||
  107. strings.TrimSpace(setting.WaffoPancakeProductID) == "" {
  108. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "Waffo Pancake 配置不完整"})
  109. return
  110. }
  111. var req WaffoPancakePayRequest
  112. if err := c.ShouldBindJSON(&req); err != nil {
  113. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"})
  114. return
  115. }
  116. if req.Amount < int64(setting.WaffoPancakeMinTopUp) {
  117. c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)})
  118. return
  119. }
  120. id := c.GetInt("id")
  121. user, err := model.GetUserById(id, false)
  122. if err != nil || user == nil {
  123. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"})
  124. return
  125. }
  126. group, err := model.GetUserGroup(id, true)
  127. if err != nil {
  128. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
  129. return
  130. }
  131. payMoney := getWaffoPancakePayMoney(req.Amount, group)
  132. if payMoney < 0.01 {
  133. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"})
  134. return
  135. }
  136. tradeNo := fmt.Sprintf("WAFFO_PANCAKE-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(6))
  137. topUp := &model.TopUp{
  138. UserId: id,
  139. Amount: normalizeWaffoPancakeTopUpAmount(req.Amount),
  140. Money: payMoney,
  141. TradeNo: tradeNo,
  142. PaymentMethod: model.PaymentMethodWaffoPancake,
  143. PaymentProvider: model.PaymentProviderWaffoPancake,
  144. CreateTime: time.Now().Unix(),
  145. Status: common.TopUpStatusPending,
  146. }
  147. if err := topUp.Insert(); err != nil {
  148. logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q", id, tradeNo, req.Amount, err.Error()))
  149. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "创建订单失败"})
  150. return
  151. }
  152. expiresInSeconds := 45 * 60
  153. session, err := service.CreateWaffoPancakeCheckoutSession(c.Request.Context(), &service.WaffoPancakeCreateSessionParams{
  154. StoreID: setting.WaffoPancakeStoreID,
  155. ProductID: setting.WaffoPancakeProductID,
  156. ProductType: "onetime",
  157. Currency: strings.ToUpper(strings.TrimSpace(setting.WaffoPancakeCurrency)),
  158. PriceSnapshot: &service.WaffoPancakePriceSnapshot{
  159. Amount: formatWaffoPancakeAmount(payMoney),
  160. TaxIncluded: false,
  161. TaxCategory: "saas",
  162. },
  163. BuyerEmail: getWaffoPancakeBuyerEmail(user),
  164. SuccessURL: getWaffoPancakeReturnURL(),
  165. ExpiresInSeconds: &expiresInSeconds,
  166. })
  167. if err != nil {
  168. logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 创建结账会话失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error()))
  169. topUp.Status = common.TopUpStatusFailed
  170. _ = topUp.Update()
  171. c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"})
  172. return
  173. }
  174. logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值订单创建成功 user_id=%d trade_no=%s session_id=%s amount=%d money=%.2f", id, tradeNo, session.SessionID, req.Amount, payMoney))
  175. c.JSON(http.StatusOK, gin.H{
  176. "message": "success",
  177. "data": gin.H{
  178. "checkout_url": session.CheckoutURL,
  179. "session_id": session.SessionID,
  180. "expires_at": session.ExpiresAt,
  181. "order_id": tradeNo,
  182. },
  183. })
  184. }
  185. func WaffoPancakeWebhook(c *gin.Context) {
  186. if !isWaffoPancakeWebhookEnabled() {
  187. logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP()))
  188. c.String(http.StatusForbidden, "webhook disabled")
  189. return
  190. }
  191. bodyBytes, err := io.ReadAll(c.Request.Body)
  192. if err != nil {
  193. logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 读取请求体失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
  194. c.String(http.StatusBadRequest, "bad request")
  195. return
  196. }
  197. signature := c.GetHeader("X-Waffo-Signature")
  198. logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 收到请求 path=%q client_ip=%s signature=%q body=%q", c.Request.RequestURI, c.ClientIP(), signature, string(bodyBytes)))
  199. event, err := service.VerifyConfiguredWaffoPancakeWebhook(string(bodyBytes), signature)
  200. if err != nil {
  201. logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 验签失败 path=%q client_ip=%s signature=%q body=%q error=%q", c.Request.RequestURI, c.ClientIP(), signature, string(bodyBytes), err.Error()))
  202. c.String(http.StatusUnauthorized, "invalid signature")
  203. return
  204. }
  205. logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 验签成功 event_type=%s event_id=%s order_id=%s client_ip=%s", event.NormalizedEventType(), event.ID, event.Data.OrderID, c.ClientIP()))
  206. if event.NormalizedEventType() != "order.completed" {
  207. c.String(http.StatusOK, "OK")
  208. return
  209. }
  210. tradeNo, err := service.ResolveWaffoPancakeTradeNo(event)
  211. if err != nil {
  212. logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 订单号映射失败 event_id=%s order_id=%s error=%q", event.ID, event.Data.OrderID, err.Error()))
  213. c.String(http.StatusOK, "OK")
  214. return
  215. }
  216. LockOrder(tradeNo)
  217. defer UnlockOrder(tradeNo)
  218. if err := model.RechargeWaffoPancake(tradeNo); err != nil {
  219. logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值处理失败 trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q", tradeNo, event.ID, event.Data.OrderID, c.ClientIP(), err.Error()))
  220. c.String(http.StatusInternalServerError, "retry")
  221. return
  222. }
  223. logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值成功 trade_no=%s event_id=%s order_id=%s client_ip=%s", tradeNo, event.ID, event.Data.OrderID, c.ClientIP()))
  224. c.String(http.StatusOK, "OK")
  225. }