From 247ae0988fd6d1aeb1a736cc8d3db36d5e37e9ce Mon Sep 17 00:00:00 2001
From: wozulong <>
Date: Fri, 22 Mar 2024 18:00:20 +0800
Subject: [PATCH] replace epay with stripe
Signed-off-by: wozulong <>
---
common/constants.go | 18 ++-
common/hash.go | 84 +++++++++++
common/logger.go | 8 +
controller/misc.go | 4 +-
controller/stripe.go | 96 ++++++++++++
controller/topup.go | 226 ++++++++++------------------
go.mod | 1 +
go.sum | 4 +
model/option.go | 30 ++--
model/topup.go | 70 ++++++++-
router/api-router.go | 7 +-
service/epay.go | 10 --
web/src/components/SystemSetting.js | 148 +++++++++++-------
web/src/helpers/utils.js | 4 +
web/src/pages/TopUp/index.js | 152 +++++++++----------
15 files changed, 533 insertions(+), 329 deletions(-)
create mode 100644 common/hash.go
create mode 100644 controller/stripe.go
delete mode 100644 service/epay.go
diff --git a/common/constants.go b/common/constants.go
index a702126..de992d4 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -11,12 +11,12 @@ import (
// Pay Settings
-var PayAddress = ""
-var CustomCallbackAddress = ""
-var EpayId = ""
-var EpayKey = ""
-var Price = 7.3
-var MinTopUp = 1
+var StripeApiSecret = ""
+var StripeWebhookSecret = ""
+var StripePriceId = ""
+var PaymentEnabled = false
+var StripeUnitPrice = 8.0
+var MinTopUp = 5
var StartTime = time.Now().Unix() // unit: second
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
@@ -188,6 +188,12 @@ const (
ChannelStatusAutoDisabled = 3
)
+const (
+ TopUpStatusPending = "pending"
+ TopUpStatusSuccess = "success"
+ TopUpStatusExpired = "expired"
+)
+
const (
ChannelTypeUnknown = 0
ChannelTypeOpenAI = 1
diff --git a/common/hash.go b/common/hash.go
new file mode 100644
index 0000000..4ba16e1
--- /dev/null
+++ b/common/hash.go
@@ -0,0 +1,84 @@
+package common
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/hex"
+ "math/rand"
+ "time"
+)
+
+func Sha256Raw(data string) []byte {
+ h := sha256.New()
+ h.Write([]byte(data))
+ return h.Sum(nil)
+}
+
+func Sha1Raw(data []byte) []byte {
+ h := sha1.New()
+ h.Write([]byte(data))
+ return h.Sum(nil)
+}
+
+func Sha1(data string) string {
+ return hex.EncodeToString(Sha1Raw([]byte(data)))
+}
+
+func HmacSha256Raw(message, key []byte) []byte {
+ h := hmac.New(sha256.New, key)
+ h.Write(message)
+ return h.Sum(nil)
+}
+
+func HmacSha256(message, key string) string {
+ return hex.EncodeToString(HmacSha256Raw([]byte(message), []byte(key)))
+}
+
+func RandomBytes(length int) []byte {
+ rand.Seed(time.Now().UnixNano())
+ b := make([]byte, length)
+ if _, err := rand.Read(b); err != nil {
+ panic(err)
+ }
+
+ return b
+}
+
+func RandomString(length int) string {
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ result := make([]byte, length)
+ randomBytes := RandomBytes(length)
+ for i := 0; i < length; i++ {
+ result[i] = chars[randomBytes[i]%byte(len(chars))]
+ }
+
+ return string(result)
+}
+
+func RandomHex(length int) string {
+ const chars = "abcdef0123456789"
+ result := make([]byte, length)
+ randomBytes := RandomBytes(length)
+ for i := 0; i < length; i++ {
+ result[i] = chars[randomBytes[i]%byte(len(chars))]
+ }
+
+ return string(result)
+}
+
+func RandomNumber(length int) string {
+ const chars = "0123456789"
+ result := make([]byte, length)
+ randomBytes := RandomBytes(length)
+ for i := 0; i < length; i++ {
+ result[i] = chars[randomBytes[i]%byte(len(chars))]
+ }
+
+ return string(result)
+}
+
+func RandomUUID() string {
+ all := RandomHex(32)
+ return all[:8] + "-" + all[8:12] + "-" + all[12:16] + "-" + all[16:20] + "-" + all[20:]
+}
diff --git a/common/logger.go b/common/logger.go
index 6162721..ac03bd1 100644
--- a/common/logger.go
+++ b/common/logger.go
@@ -98,3 +98,11 @@ func LogQuota(quota int) string {
return fmt.Sprintf("%d 点额度", quota)
}
}
+
+func LogQuotaF(quota float64) string {
+ if DisplayInCurrencyEnabled {
+ return fmt.Sprintf("$%.6f 额度", quota/QuotaPerUnit)
+ } else {
+ return fmt.Sprintf("%d 点额度", int64(quota))
+ }
+}
diff --git a/controller/misc.go b/controller/misc.go
index bf72a36..04b2c5b 100644
--- a/controller/misc.go
+++ b/controller/misc.go
@@ -47,7 +47,7 @@ func GetStatus(c *gin.Context) {
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled,
"server_address": common.ServerAddress,
- "price": common.Price,
+ "stripe_unit_price": common.StripeUnitPrice,
"min_topup": common.MinTopUp,
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
@@ -61,7 +61,7 @@ func GetStatus(c *gin.Context) {
"enable_data_export": common.DataExportEnabled,
"data_export_default_time": common.DataExportDefaultTime,
"default_collapse_sidebar": common.DefaultCollapseSidebar,
- "enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
+ "payment_enabled": common.PaymentEnabled,
"mj_notify_enabled": constant.MjNotifyEnabled,
"version": common.Version,
},
diff --git a/controller/stripe.go b/controller/stripe.go
new file mode 100644
index 0000000..30a03b6
--- /dev/null
+++ b/controller/stripe.go
@@ -0,0 +1,96 @@
+package controller
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/stripe/stripe-go/v76"
+ "github.com/stripe/stripe-go/v76/webhook"
+ "io"
+ "log"
+ "net/http"
+ "one-api/common"
+ "one-api/model"
+ "strconv"
+ "strings"
+)
+
+func StripeWebhook(c *gin.Context) {
+ payload, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ log.Printf("解析Stripe Webhook参数失败: %v\n", err)
+ c.AbortWithStatus(http.StatusServiceUnavailable)
+ return
+ }
+
+ signature := c.GetHeader("Stripe-Signature")
+ endpointSecret := common.StripeWebhookSecret
+ event, err := webhook.ConstructEvent(payload, signature, endpointSecret)
+
+ if err != nil {
+ log.Printf("Stripe Webhook验签失败: %v\n", err)
+ c.AbortWithStatus(http.StatusBadRequest)
+ return
+ }
+
+ switch event.Type {
+ case stripe.EventTypeCheckoutSessionCompleted:
+ sessionCompleted(event)
+ case stripe.EventTypeCheckoutSessionExpired:
+ sessionExpired(event)
+ default:
+ log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
+ }
+
+ c.Status(http.StatusOK)
+}
+
+func sessionCompleted(event stripe.Event) {
+ referenceId := event.GetObjectValue("client_reference_id")
+ status := event.GetObjectValue("status")
+ if "complete" != status {
+ log.Println("错误的Stripe Checkout完成状态:", status, ",", referenceId)
+ return
+ }
+
+ err := model.Recharge(referenceId)
+ if err != nil {
+ log.Println(err.Error(), referenceId)
+ return
+ }
+
+ total, _ := strconv.ParseFloat(event.GetObjectValue("amount_total"), 64)
+ currency := strings.ToUpper(event.GetObjectValue("currency"))
+ log.Printf("收到款项:%s, %.2f(%s)", referenceId, total/100, currency)
+}
+
+func sessionExpired(event stripe.Event) {
+ referenceId := event.GetObjectValue("client_reference_id")
+ status := event.GetObjectValue("status")
+ if "expired" != status {
+ log.Println("错误的Stripe Checkout过期状态:", status, ",", referenceId)
+ return
+ }
+
+ if "" == referenceId {
+ log.Println("未提供支付单号")
+ return
+ }
+
+ topUp := model.GetTopUpByTradeNo(referenceId)
+ if topUp == nil {
+ log.Println("充值订单不存在", referenceId)
+ return
+ }
+
+ if topUp.Status != common.TopUpStatusPending {
+ log.Println("充值订单状态错误", referenceId)
+ }
+
+ topUp.Status = common.TopUpStatusExpired
+ err := topUp.Update()
+ if err != nil {
+ log.Println("过期充值订单失败", referenceId, ", err:", err.Error())
+ return
+ }
+
+ log.Println("充值订单已过期", referenceId)
+}
diff --git a/controller/topup.go b/controller/topup.go
index 039c409..f786904 100644
--- a/controller/topup.go
+++ b/controller/topup.go
@@ -1,21 +1,20 @@
package controller
+import "C"
import (
"fmt"
"github.com/gin-gonic/gin"
- "github.com/samber/lo"
- epay "github.com/star-horizon/go-epay"
+ "github.com/stripe/stripe-go/v76"
+ "github.com/stripe/stripe-go/v76/checkout/session"
"log"
- "net/url"
"one-api/common"
"one-api/model"
- "one-api/service"
"strconv"
- "sync"
+ "strings"
"time"
)
-type EpayRequest struct {
+type PayRequest struct {
Amount int `json:"amount"`
PaymentMethod string `json:"payment_method"`
TopUpCode string `json:"top_up_code"`
@@ -26,177 +25,103 @@ type AmountRequest struct {
TopUpCode string `json:"top_up_code"`
}
-func GetEpayClient() *epay.Client {
- if common.PayAddress == "" || common.EpayId == "" || common.EpayKey == "" {
- return nil
+func genStripeLink(referenceId string, amount int64) (string, error) {
+ if !strings.HasPrefix(common.StripeApiSecret, "sk_") {
+ return "", fmt.Errorf("无效的Stripe API密钥")
}
- withUrl, err := epay.NewClientWithUrl(&epay.Config{
- PartnerID: common.EpayId,
- Key: common.EpayKey,
- }, common.PayAddress)
+
+ stripe.Key = common.StripeApiSecret
+
+ params := &stripe.CheckoutSessionParams{
+ ClientReferenceID: stripe.String(referenceId),
+ SuccessURL: stripe.String(common.ServerAddress + "/log"),
+ CancelURL: stripe.String(common.ServerAddress + "/topup"),
+ LineItems: []*stripe.CheckoutSessionLineItemParams{
+ {
+ Price: stripe.String(common.StripePriceId),
+ Quantity: stripe.Int64(amount),
+ },
+ },
+ Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+ }
+ result, err := session.New(params)
if err != nil {
- return nil
+ return "", err
}
- return withUrl
+
+ return result.URL, nil
}
-func GetAmount(count float64, user model.User) float64 {
- // 别问为什么用float64,问就是这么点钱没必要
- topupGroupRatio := common.GetTopupGroupRatio(user.Group)
- if topupGroupRatio == 0 {
- topupGroupRatio = 1
- }
- amount := count * common.Price * topupGroupRatio
- return amount
+func GetPayAmount(count float64) float64 {
+ return count * common.StripeUnitPrice
}
-func RequestEpay(c *gin.Context) {
- var req EpayRequest
+func GetChargedAmount(count float64, user model.User) float64 {
+ topUpGroupRatio := common.GetTopupGroupRatio(user.Group)
+ if topUpGroupRatio == 0 {
+ topUpGroupRatio = 1
+ }
+
+ return count * topUpGroupRatio
+}
+
+func RequestPayLink(c *gin.Context) {
+ var req PayRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(200, gin.H{"message": err.Error(), "data": 10})
return
}
+ if !common.PaymentEnabled {
+ c.JSON(200, gin.H{"message": "error", "data": "管理员未开启在线支付"})
+ return
+ }
+ if req.PaymentMethod != "stripe" {
+ c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
+ return
+ }
if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp), "data": 10})
return
}
+ if req.Amount > 10000 {
+ c.JSON(200, gin.H{"message": "充值数量不能大于 10000", "data": 10})
+ return
+ }
id := c.GetInt("id")
user, _ := model.GetUserById(id, false)
- payMoney := GetAmount(float64(req.Amount), *user)
+ chargedMoney := GetChargedAmount(float64(req.Amount), *user)
- var payType epay.PurchaseType
- if req.PaymentMethod == "zfb" {
- payType = epay.Alipay
- }
- if req.PaymentMethod == "wx" {
- req.PaymentMethod = "wxpay"
- payType = epay.WechatPay
- }
- callBackAddress := service.GetCallbackAddress()
- returnUrl, _ := url.Parse(common.ServerAddress + "/log")
- notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
- tradeNo := strconv.FormatInt(time.Now().Unix(), 10)
- client := GetEpayClient()
- if client == nil {
- c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
- return
- }
- uri, params, err := client.Purchase(&epay.PurchaseArgs{
- Type: payType,
- ServiceTradeNo: "A" + tradeNo,
- Name: "B" + tradeNo,
- Money: strconv.FormatFloat(payMoney, 'f', 2, 64),
- Device: epay.PC,
- NotifyUrl: notifyUrl,
- ReturnUrl: returnUrl,
- })
+ reference := fmt.Sprintf("new-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), common.RandomString(4))
+ referenceId := "ref_" + common.Sha1(reference)
+
+ payLink, err := genStripeLink(referenceId, int64(req.Amount))
if err != nil {
+ log.Println("获取Stripe Checkout支付链接失败", err)
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
return
}
+
topUp := &model.TopUp{
UserId: id,
Amount: req.Amount,
- Money: payMoney,
- TradeNo: "A" + tradeNo,
+ Money: chargedMoney,
+ TradeNo: referenceId,
CreateTime: time.Now().Unix(),
- Status: "pending",
+ 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": params, "url": uri})
-}
-
-// tradeNo lock
-var orderLocks sync.Map
-var createLock sync.Mutex
-
-// LockOrder 尝试对给定订单号加锁
-func LockOrder(tradeNo string) {
- lock, ok := orderLocks.Load(tradeNo)
- if !ok {
- createLock.Lock()
- defer createLock.Unlock()
- lock, ok = orderLocks.Load(tradeNo)
- if !ok {
- lock = new(sync.Mutex)
- orderLocks.Store(tradeNo, lock)
- }
- }
- lock.(*sync.Mutex).Lock()
-}
-
-// UnlockOrder 释放给定订单号的锁
-func UnlockOrder(tradeNo string) {
- lock, ok := orderLocks.Load(tradeNo)
- if ok {
- lock.(*sync.Mutex).Unlock()
- }
-}
-
-func EpayNotify(c *gin.Context) {
- params := lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
- r[t] = c.Request.URL.Query().Get(t)
- return r
- }, map[string]string{})
- client := GetEpayClient()
- if client == nil {
- log.Println("易支付回调失败 未找到配置信息")
- _, err := c.Writer.Write([]byte("fail"))
- if err != nil {
- log.Println("易支付回调写入失败")
- return
- }
- }
- verifyInfo, err := client.Verify(params)
- if err == nil && verifyInfo.VerifyStatus {
- _, err := c.Writer.Write([]byte("success"))
- if err != nil {
- log.Println("易支付回调写入失败")
- }
- } else {
- _, err := c.Writer.Write([]byte("fail"))
- if err != nil {
- log.Println("易支付回调写入失败")
- }
- log.Println("易支付回调签名验证失败")
- return
- }
-
- if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
- log.Println(verifyInfo)
- LockOrder(verifyInfo.ServiceTradeNo)
- defer UnlockOrder(verifyInfo.ServiceTradeNo)
- topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
- if topUp == nil {
- log.Printf("易支付回调未找到订单: %v", verifyInfo)
- return
- }
- if topUp.Status == "pending" {
- topUp.Status = "success"
- err := topUp.Update()
- if err != nil {
- log.Printf("易支付回调更新订单失败: %v", topUp)
- return
- }
- //user, _ := model.GetUserById(topUp.UserId, false)
- //user.Quota += topUp.Amount * 500000
- err = model.IncreaseUserQuota(topUp.UserId, topUp.Amount*500000)
- if err != nil {
- log.Printf("易支付回调更新用户失败: %v", topUp)
- return
- }
- log.Printf("易支付回调更新用户成功 %v", topUp)
- model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", common.LogQuota(topUp.Amount*500000), topUp.Money))
- }
- } else {
- log.Printf("易支付异常回调: %v", verifyInfo)
- }
+ c.JSON(200, gin.H{
+ "message": "success",
+ "data": gin.H{
+ "payLink": payLink,
+ },
+ })
}
func RequestAmount(c *gin.Context) {
@@ -206,12 +131,23 @@ func RequestAmount(c *gin.Context) {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return
}
+ if !common.PaymentEnabled {
+ c.JSON(200, gin.H{"message": "error", "data": "管理员未开启在线支付"})
+ return
+ }
if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp)})
return
}
id := c.GetInt("id")
user, _ := model.GetUserById(id, false)
- payMoney := GetAmount(float64(req.Amount), *user)
- c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
+ payMoney := GetPayAmount(float64(req.Amount))
+ chargedMoney := GetChargedAmount(float64(req.Amount), *user)
+ c.JSON(200, gin.H{
+ "message": "success",
+ "data": gin.H{
+ "payAmount": strconv.FormatFloat(payMoney, 'f', 2, 64),
+ "chargedAmount": strconv.FormatFloat(chargedMoney, 'f', 2, 64),
+ },
+ })
}
diff --git a/go.mod b/go.mod
index b0c7220..2ecbc52 100644
--- a/go.mod
+++ b/go.mod
@@ -59,6 +59,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/stripe/stripe-go/v76 v76.21.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
diff --git a/go.sum b/go.sum
index 5b17b48..d498440 100644
--- a/go.sum
+++ b/go.sum
@@ -153,6 +153,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stripe/stripe-go/v76 v76.21.0 h1:O3GHImHS4oUI3qWMOClHN3zAQF5/oswS/NB7leV1fsU=
+github.com/stripe/stripe-go/v76 v76.21.0/go.mod h1:rw1MxjlAKKcZ+3FOXgTHgwiOa2ya6CPq6ykpJ0Q6Po4=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -178,6 +180,7 @@ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@@ -185,6 +188,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/model/option.go b/model/option.go
index f66f94b..f8f10e1 100644
--- a/model/option.go
+++ b/model/option.go
@@ -58,11 +58,11 @@ func InitOptionMap() {
common.OptionMap["SystemName"] = common.SystemName
common.OptionMap["Logo"] = common.Logo
common.OptionMap["ServerAddress"] = ""
- common.OptionMap["PayAddress"] = ""
- common.OptionMap["CustomCallbackAddress"] = ""
- common.OptionMap["EpayId"] = ""
- common.OptionMap["EpayKey"] = ""
- common.OptionMap["Price"] = strconv.FormatFloat(common.Price, 'f', -1, 64)
+ common.OptionMap["StripeApiSecret"] = common.StripeApiSecret
+ common.OptionMap["StripeWebhookSecret"] = common.StripeWebhookSecret
+ common.OptionMap["StripePriceId"] = common.StripePriceId
+ common.OptionMap["PaymentEnabled"] = strconv.FormatBool(common.PaymentEnabled)
+ common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(common.StripeUnitPrice, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["GitHubClientId"] = ""
@@ -223,16 +223,16 @@ func updateOptionMap(key string, value string) (err error) {
common.SMTPToken = value
case "ServerAddress":
common.ServerAddress = value
- case "PayAddress":
- common.PayAddress = value
- case "CustomCallbackAddress":
- common.CustomCallbackAddress = value
- case "EpayId":
- common.EpayId = value
- case "EpayKey":
- common.EpayKey = value
- case "Price":
- common.Price, _ = strconv.ParseFloat(value, 64)
+ case "StripeApiSecret":
+ common.StripeApiSecret = value
+ case "StripeWebhookSecret":
+ common.StripeWebhookSecret = value
+ case "StripePriceId":
+ common.StripePriceId = value
+ case "PaymentEnabled":
+ common.PaymentEnabled, _ = strconv.ParseBool(value)
+ case "StripeUnitPrice":
+ common.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
case "MinTopUp":
common.MinTopUp, _ = strconv.Atoi(value)
case "TopupGroupRatio":
diff --git a/model/topup.go b/model/topup.go
index dc1cbc5..ceed01f 100644
--- a/model/topup.go
+++ b/model/topup.go
@@ -1,13 +1,21 @@
package model
+import (
+ "errors"
+ "fmt"
+ "gorm.io/gorm"
+ "one-api/common"
+)
+
type TopUp struct {
- Id int `json:"id"`
- UserId int `json:"user_id" gorm:"index"`
- Amount int `json:"amount"`
- Money float64 `json:"money"`
- TradeNo string `json:"trade_no"`
- CreateTime int64 `json:"create_time"`
- Status string `json:"status"`
+ Id int `json:"id"`
+ UserId int `json:"user_id" gorm:"index"`
+ Amount int `json:"amount"`
+ Money float64 `json:"money"`
+ TradeNo string `json:"trade_no" gorm:"unique"`
+ CreateTime int64 `json:"create_time"`
+ CompleteTime int64 `json:"complete_time"`
+ Status string `json:"status"`
}
func (topUp *TopUp) Insert() error {
@@ -41,3 +49,51 @@ func GetTopUpByTradeNo(tradeNo string) *TopUp {
}
return topUp
}
+
+func Recharge(referenceId string) (err error) {
+ if referenceId == "" {
+ return errors.New("未提供支付单号")
+ }
+
+ var quota float64
+ topUp := &TopUp{}
+
+ refCol := "`trade_no`"
+ if common.UsingPostgreSQL {
+ refCol = `"trade_no"`
+ }
+
+ err = DB.Transaction(func(tx *gorm.DB) error {
+ err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
+ if err != nil {
+ return errors.New("充值订单不存在")
+ }
+
+ if topUp.Status != common.TopUpStatusPending {
+ return errors.New("充值订单状态错误")
+ }
+
+ topUp.CompleteTime = common.GetTimestamp()
+ topUp.Status = common.TopUpStatusSuccess
+ err = tx.Save(topUp).Error
+ if err != nil {
+ return err
+ }
+
+ quota = topUp.Money * common.QuotaPerUnit
+ err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quota)).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return errors.New("充值失败," + err.Error())
+ }
+
+ RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", common.LogQuotaF(quota), topUp.Amount))
+
+ return nil
+}
diff --git a/router/api-router.go b/router/api-router.go
index 89d93e1..734e3c0 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -31,13 +31,14 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind)
+ apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
+
userRoute := apiRouter.Group("/user")
{
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
userRoute.GET("/logout", controller.Logout)
- userRoute.GET("/epay/notify", controller.EpayNotify)
selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth())
@@ -48,8 +49,8 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/aff", controller.GetAffCode)
- selfRoute.POST("/topup", controller.TopUp)
- selfRoute.POST("/pay", controller.RequestEpay)
+ selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
+ selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestPayLink)
selfRoute.POST("/amount", controller.RequestAmount)
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
}
diff --git a/service/epay.go b/service/epay.go
deleted file mode 100644
index 7ce4aad..0000000
--- a/service/epay.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package service
-
-import "one-api/common"
-
-func GetCallbackAddress() string {
- if common.CustomCallbackAddress == "" {
- return common.ServerAddress
- }
- return common.CustomCallbackAddress
-}
diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js
index ba8af09..aa9057c 100644
--- a/web/src/components/SystemSetting.js
+++ b/web/src/components/SystemSetting.js
@@ -21,13 +21,13 @@ const SystemSetting = () => {
SMTPFrom: '',
SMTPToken: '',
ServerAddress: '',
- EpayId: '',
- EpayKey: '',
- Price: 7.3,
- MinTopUp: 1,
+ StripeApiSecret: '',
+ StripeWebhookSecret: '',
+ StripePriceId: '',
+ PaymentEnabled: false,
+ StripeUnitPrice: 8.0,
+ MinTopUp: 5,
TopupGroupRatio: '',
- PayAddress: '',
- CustomCallbackAddress: '',
Footer: '',
WeChatAuthEnabled: '',
WeChatServerAddress: '',
@@ -92,6 +92,7 @@ const SystemSetting = () => {
case 'TurnstileCheckEnabled':
case 'EmailDomainRestrictionEnabled':
case 'RegisterEnabled':
+ case 'PaymentEnabled':
value = inputs[key] === 'true' ? 'false' : 'true';
break;
default:
@@ -106,9 +107,6 @@ const SystemSetting = () => {
if (key === 'EmailDomainWhitelist') {
value = value.split(',');
}
- if (key === 'Price') {
- value = parseFloat(value);
- }
setInputs((inputs) => ({
...inputs, [key]: value
}));
@@ -128,10 +126,11 @@ const SystemSetting = () => {
name === 'Notice' ||
name.startsWith('SMTP') ||
name === 'ServerAddress' ||
- name === 'EpayId' ||
- name === 'EpayKey' ||
- name === 'Price' ||
- name === 'PayAddress' ||
+ name === 'StripeApiSecret' ||
+ name === 'StripeWebhookSecret' ||
+ name === 'StripePriceId' ||
+ name === 'StripeUnitPrice' ||
+ name === 'MinTopUp' ||
name === 'GitHubClientId' ||
name === 'GitHubClientSecret' ||
name === 'LinuxDoClientId' ||
@@ -158,7 +157,7 @@ const SystemSetting = () => {
await updateOption('ServerAddress', ServerAddress);
};
- const submitPayAddress = async () => {
+ const submitPaymentConfig = async () => {
if (inputs.ServerAddress === '') {
showError('请先填写服务器地址');
return;
@@ -170,15 +169,30 @@ const SystemSetting = () => {
}
await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
}
- let PayAddress = removeTrailingSlash(inputs.PayAddress);
- await updateOption('PayAddress', PayAddress);
- if (inputs.EpayId !== '') {
- await updateOption('EpayId', inputs.EpayId);
+ let stripeApiSecret = removeTrailingSlash(inputs.StripeApiSecret);
+ if (stripeApiSecret && !stripeApiSecret.startsWith("sk_")) {
+ showError('输入了无效的Stripe API密钥');
+ return;
}
- if (inputs.EpayKey !== '') {
- await updateOption('EpayKey', inputs.EpayKey);
+ stripeApiSecret && await updateOption('StripeApiSecret', stripeApiSecret);
+
+ let stripeWebhookSecret = removeTrailingSlash(inputs.StripeWebhookSecret);
+ if (stripeWebhookSecret && !stripeWebhookSecret.startsWith("whsec_")) {
+ showError('输入了无效的Stripe Webhook签名密钥');
+ return;
}
- await updateOption('Price', '' + inputs.Price);
+ stripeWebhookSecret && await updateOption('StripeWebhookSecret', stripeWebhookSecret);
+
+ let stripePriceId = removeTrailingSlash(inputs.StripePriceId);
+ if (stripePriceId && !stripePriceId.startsWith("price_")) {
+ showError('输入了无效的Stripe 物品价格ID');
+ return;
+ }
+ await updateOption('StripePriceId', stripePriceId);
+
+ await updateOption('PaymentEnable', inputs.PaymentEnabled);
+ await updateOption('StripeUnitPrice', inputs.StripeUnitPrice);
+ await updateOption('MinTopUp', inputs.MinTopUp);
};
const submitSMTP = async () => {
@@ -318,54 +332,66 @@ const SystemSetting = () => {
更新服务器地址
{`${inputs.ServerAddress}/api/stripe/webhook`}
+ ,需要包含事件:checkout.session.completed
和 checkout.session.expired
+
充值数量:{topUpCount}$
+充值数量:{topUpCount}$(实到:{chargedAmount}$)
实付金额:{renderAmount()}
是否确认充值?
@@ -244,54 +232,50 @@ const TopUp = () => { -