parent
e4665a16c8
commit
03d847fe68
@ -43,6 +43,10 @@ _本项目是基于[one-api](https://github.com/songquanpeng/one-api)二次开
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 本项目为个人学习使用,不保证稳定性,且不提供任何技术支持,使用者必须在遵循 OpenAI 的使用条款以及法律法规的情况下使用,不得用于非法用途。
|
||||||
|
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||||
|
|
||||||
## 功能变化
|
## 功能变化
|
||||||
|
|
||||||
- 全新的 UI 界面
|
- 全新的 UI 界面
|
||||||
|
4
common/config/payment.go
Normal file
4
common/config/payment.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
var PaymentUSDRate = 7.3
|
||||||
|
var PaymentMinAmount = 1
|
@ -14,10 +14,21 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/snowflake"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var node *snowflake.Node
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
node, err = snowflake.NewNode(1)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("snowflake.NewNode failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func OpenBrowser(url string) {
|
func OpenBrowser(url string) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -203,6 +214,14 @@ func String2Int(str string) int {
|
|||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func String2Int64(str string) int64 {
|
||||||
|
num, err := strconv.ParseInt(str, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
func IsFileExist(path string) bool {
|
func IsFileExist(path string) bool {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return err == nil || os.IsExist(err)
|
return err == nil || os.IsExist(err)
|
||||||
@ -256,3 +275,19 @@ func Marshal[T interface{}](data T) string {
|
|||||||
}
|
}
|
||||||
return string(res)
|
return string(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateTradeNo() string {
|
||||||
|
id := node.Generate()
|
||||||
|
|
||||||
|
return id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decimal(value float64, decimalPlace int) float64 {
|
||||||
|
format := fmt.Sprintf("%%.%df", decimalPlace)
|
||||||
|
value, _ = strconv.ParseFloat(fmt.Sprintf(format, value), 64)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUnixTime() int64 {
|
||||||
|
return time.Now().Unix()
|
||||||
|
}
|
||||||
|
@ -47,6 +47,8 @@ func GetStatus(c *gin.Context) {
|
|||||||
"mj_notify_enabled": config.MjNotifyEnabled,
|
"mj_notify_enabled": config.MjNotifyEnabled,
|
||||||
"chat_cache_enabled": config.ChatCacheEnabled,
|
"chat_cache_enabled": config.ChatCacheEnabled,
|
||||||
"chat_links": config.ChatLinks,
|
"chat_links": config.ChatLinks,
|
||||||
|
"PaymentUSDRate": config.PaymentUSDRate,
|
||||||
|
"PaymentMinAmount": config.PaymentMinAmount,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
223
controller/order.go
Normal file
223
controller/order.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/common/config"
|
||||||
|
"one-api/common/logger"
|
||||||
|
"one-api/common/utils"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/payment"
|
||||||
|
"one-api/payment/types"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderRequest struct {
|
||||||
|
UUID string `json:"uuid" binding:"required"`
|
||||||
|
Amount int `json:"amount" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderResponse struct {
|
||||||
|
TradeNo string `json:"trade_no"`
|
||||||
|
*types.PayRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrder
|
||||||
|
func CreateOrder(c *gin.Context) {
|
||||||
|
var orderReq OrderRequest
|
||||||
|
if err := c.ShouldBindJSON(&orderReq); err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("invalid request"))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if orderReq.Amount <= 0 || orderReq.Amount < config.PaymentMinAmount {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, fmt.Errorf("金额必须大于等于 %d", config.PaymentMinAmount))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
// 关闭用户未完成的订单
|
||||||
|
go model.CloseUnfinishedOrder()
|
||||||
|
|
||||||
|
paymentService, err := payment.NewPaymentService(orderReq.UUID)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取手续费和支付金额
|
||||||
|
fee, payMoney := calculateOrderAmount(paymentService.Payment, orderReq.Amount)
|
||||||
|
|
||||||
|
// 开始支付
|
||||||
|
tradeNo := utils.GenerateTradeNo()
|
||||||
|
payRequest, err := paymentService.Pay(tradeNo, payMoney)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("创建支付失败,请稍后再试"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
order := &model.Order{
|
||||||
|
UserId: userId,
|
||||||
|
TradeNo: tradeNo,
|
||||||
|
Amount: orderReq.Amount,
|
||||||
|
OrderAmount: payMoney,
|
||||||
|
OrderCurrency: paymentService.Payment.Currency,
|
||||||
|
Fee: fee,
|
||||||
|
Status: model.OrderStatusPending,
|
||||||
|
Quota: orderReq.Amount * int(config.QuotaPerUnit),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = order.Insert()
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("创建订单失败,请稍后再试"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderResp := &OrderResponse{
|
||||||
|
TradeNo: tradeNo,
|
||||||
|
PayRequest: payRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": orderResp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 PaymentCallback(c *gin.Context) {
|
||||||
|
uuid := c.Param("uuid")
|
||||||
|
paymentService, err := payment.NewPaymentService(uuid)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("payment not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payNotify, err := paymentService.HandleCallback(c, paymentService.Payment.Config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LockOrder(payNotify.GatewayNo)
|
||||||
|
defer UnlockOrder(payNotify.GatewayNo)
|
||||||
|
|
||||||
|
order, err := model.GetOrderByTradeNo(payNotify.TradeNo)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError(fmt.Sprintf("gateway callback failed to find order, trade_no: %s,", payNotify.TradeNo))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(order.Status, order.Status != model.OrderStatusPending)
|
||||||
|
|
||||||
|
if order.Status != model.OrderStatusPending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order.GatewayNo = payNotify.GatewayNo
|
||||||
|
order.Status = model.OrderStatusSuccess
|
||||||
|
err = order.Update()
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError(fmt.Sprintf("gateway callback failed to update order, trade_no: %s,", payNotify.TradeNo))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.IncreaseUserQuota(order.UserId, order.Quota)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError(fmt.Sprintf("gateway callback failed to increase user quota, trade_no: %s,", payNotify.TradeNo))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
model.RecordLog(order.UserId, model.LogTypeTopup, fmt.Sprintf("在线充值成功,充值quota: %d,支付金额:%.2f %s", order.Quota, order.OrderAmount, order.OrderCurrency))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckOrderStatus(c *gin.Context) {
|
||||||
|
tradeNo := c.Query("trade_no")
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
success := false
|
||||||
|
|
||||||
|
if tradeNo != "" {
|
||||||
|
order, err := model.GetUserOrder(userId, tradeNo)
|
||||||
|
if err == nil {
|
||||||
|
if order.Status == model.OrderStatusSuccess {
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": success,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateOrderAmount(payment *model.Payment, amount int) (fee, payMoney float64) {
|
||||||
|
if payment.PercentFee > 0 {
|
||||||
|
fee = utils.Decimal(float64(amount)*payment.PercentFee, 2)
|
||||||
|
} else if payment.FixedFee > 0 {
|
||||||
|
fee = payment.FixedFee
|
||||||
|
}
|
||||||
|
|
||||||
|
total := utils.Decimal(float64(amount)+fee, 2)
|
||||||
|
|
||||||
|
if payment.Currency == model.CurrencyTypeUSD {
|
||||||
|
payMoney = total
|
||||||
|
} else {
|
||||||
|
payMoney = utils.Decimal(total*config.PaymentUSDRate, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrderList(c *gin.Context) {
|
||||||
|
var params model.SearchOrderParams
|
||||||
|
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payments, err := model.GetOrderList(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payments,
|
||||||
|
})
|
||||||
|
}
|
140
controller/payment.go
Normal file
140
controller/payment.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPaymentList(c *gin.Context) {
|
||||||
|
var params model.SearchPaymentParams
|
||||||
|
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payments, err := model.GetPanymentList(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPayment(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payment, err := model.GetPaymentByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPayment(c *gin.Context) {
|
||||||
|
payment := model.Payment{}
|
||||||
|
err := c.ShouldBindJSON(&payment)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = payment.Insert()
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdatePayment(c *gin.Context) {
|
||||||
|
payment := model.Payment{}
|
||||||
|
err := c.ShouldBindJSON(&payment)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite := true
|
||||||
|
|
||||||
|
if payment.UUID == "" {
|
||||||
|
overwrite = false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = payment.Update(overwrite)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePayment(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payment := model.Payment{ID: id}
|
||||||
|
err = payment.Delete()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserPaymentList(c *gin.Context) {
|
||||||
|
payments, err := model.GetUserPaymentList()
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": payments,
|
||||||
|
})
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -35,6 +35,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -14,6 +14,8 @@ github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC
|
|||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
||||||
|
@ -154,6 +154,14 @@ func InitDB() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = db.AutoMigrate(&Payment{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.AutoMigrate(&Order{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.SysLog("database migrated")
|
logger.SysLog("database migrated")
|
||||||
err = createRootAccountIfNeed()
|
err = createRootAccountIfNeed()
|
||||||
return err
|
return err
|
||||||
|
@ -84,6 +84,9 @@ func InitOptionMap() {
|
|||||||
|
|
||||||
config.OptionMap["ChatImageRequestProxy"] = ""
|
config.OptionMap["ChatImageRequestProxy"] = ""
|
||||||
|
|
||||||
|
config.OptionMap["PaymentUSDRate"] = strconv.FormatFloat(config.PaymentUSDRate, 'f', -1, 64)
|
||||||
|
config.OptionMap["PaymentMinAmount"] = strconv.Itoa(config.PaymentMinAmount)
|
||||||
|
|
||||||
config.OptionMapRWMutex.Unlock()
|
config.OptionMapRWMutex.Unlock()
|
||||||
loadOptionsFromDatabase()
|
loadOptionsFromDatabase()
|
||||||
}
|
}
|
||||||
@ -132,6 +135,7 @@ var optionIntMap = map[string]*int{
|
|||||||
"RetryTimes": &config.RetryTimes,
|
"RetryTimes": &config.RetryTimes,
|
||||||
"RetryCooldownSeconds": &config.RetryCooldownSeconds,
|
"RetryCooldownSeconds": &config.RetryCooldownSeconds,
|
||||||
"ChatCacheExpireMinute": &config.ChatCacheExpireMinute,
|
"ChatCacheExpireMinute": &config.ChatCacheExpireMinute,
|
||||||
|
"PaymentMinAmount": &config.PaymentMinAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
var optionBoolMap = map[string]*bool{
|
var optionBoolMap = map[string]*bool{
|
||||||
@ -206,6 +210,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
config.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
|
config.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
|
||||||
case "QuotaPerUnit":
|
case "QuotaPerUnit":
|
||||||
config.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
config.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
||||||
|
case "PaymentUSDRate":
|
||||||
|
config.PaymentUSDRate, _ = strconv.ParseFloat(value, 64)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
107
model/order.go
Normal file
107
model/order.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderStatusPending OrderStatus = "pending"
|
||||||
|
OrderStatusSuccess OrderStatus = "success"
|
||||||
|
OrderStatusFailed OrderStatus = "failed"
|
||||||
|
OrderStatusClosed OrderStatus = "closed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
TradeNo string `json:"trade_no" gorm:"type:varchar(50);uniqueIndex"`
|
||||||
|
GatewayNo string `json:"gateway_no" gorm:"type:varchar(100)"`
|
||||||
|
Amount int `json:"amount" gorm:"default:0"`
|
||||||
|
OrderAmount float64 `json:"order_amount" gorm:"type:decimal(10,2);default:0"`
|
||||||
|
OrderCurrency CurrencyType `json:"order_currency" gorm:"type:varchar(16)"`
|
||||||
|
Quota int `json:"quota" gorm:"type:int;default:0"`
|
||||||
|
Fee float64 `json:"fee" gorm:"type:decimal(10,2);default:0"`
|
||||||
|
Status OrderStatus `json:"status" gorm:"type:varchar(32)"`
|
||||||
|
CreatedAt int `json:"created_at"`
|
||||||
|
UpdatedAt int `json:"-"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询并关闭未完成的订单
|
||||||
|
func CloseUnfinishedOrder() error {
|
||||||
|
// 关闭超过 3 小时未支付的订单
|
||||||
|
unixTime := time.Now().Unix() - 3*3600
|
||||||
|
return DB.Model(&Order{}).Where("status = ? AND created_at < ?", OrderStatusPending, unixTime).Update("status", OrderStatusClosed).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrderByTradeNo(tradeNo string) (*Order, error) {
|
||||||
|
var order Order
|
||||||
|
err := DB.Where("trade_no = ?", tradeNo).First(&order).Error
|
||||||
|
return &order, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserOrder(userId int, tradeNo string) (*Order, error) {
|
||||||
|
var order Order
|
||||||
|
err := DB.Where("user_id = ? AND trade_no = ?", userId, tradeNo).First(&order).Error
|
||||||
|
return &order, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) Insert() error {
|
||||||
|
return DB.Create(o).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) Update() error {
|
||||||
|
return DB.Save(o).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedOrderFields = map[string]bool{
|
||||||
|
"id": true,
|
||||||
|
"user_id": true,
|
||||||
|
"status": true,
|
||||||
|
"created_at": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchOrderParams struct {
|
||||||
|
UserId int `form:"user_id"`
|
||||||
|
TradeNo string `form:"trade_no"`
|
||||||
|
GatewayNo string `form:"gateway_no"`
|
||||||
|
Status string `form:"status"`
|
||||||
|
StartTimestamp int64 `form:"start_timestamp"`
|
||||||
|
EndTimestamp int64 `form:"end_timestamp"`
|
||||||
|
PaginationParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrderList(params *SearchOrderParams) (*DataResult[Order], error) {
|
||||||
|
var orders []*Order
|
||||||
|
|
||||||
|
db := DB.Omit("key")
|
||||||
|
|
||||||
|
if params.UserId != 0 {
|
||||||
|
db = db.Where("user_id = ?", params.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.TradeNo != "" {
|
||||||
|
db = db.Where("trade_no = ?", params.TradeNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.GatewayNo != "" {
|
||||||
|
db = db.Where("gateway_no = ?", params.GatewayNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Status != "" {
|
||||||
|
db = db.Where("status = ?", params.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.StartTimestamp != 0 {
|
||||||
|
db = db.Where("created_at >= ?", params.StartTimestamp)
|
||||||
|
}
|
||||||
|
if params.EndTimestamp != 0 {
|
||||||
|
db = db.Where("created_at <= ?", params.EndTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginateAndOrder(db, ¶ms.PaginationParams, &orders, allowedOrderFields)
|
||||||
|
}
|
110
model/payment.go
Normal file
110
model/payment.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CurrencyType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CurrencyTypeUSD CurrencyType = "USD"
|
||||||
|
CurrencyTypeCNY CurrencyType = "CNY"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payment struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Type string `json:"type" form:"type" gorm:"type:varchar(16)"`
|
||||||
|
UUID string `json:"uuid" form:"uuid" gorm:"type:char(32);uniqueIndex"`
|
||||||
|
Name string `json:"name" form:"name" gorm:"type:varchar(255); not null"`
|
||||||
|
Icon string `json:"icon" form:"icon" gorm:"type:varchar(300)"`
|
||||||
|
NotifyDomain string `json:"notify_domain" form:"notify_domain" gorm:"type:varchar(300)"`
|
||||||
|
FixedFee float64 `json:"fixed_fee" form:"fixed_fee" gorm:"type:decimal(10,2); default:0.00"`
|
||||||
|
PercentFee float64 `json:"percent_fee" form:"percent_fee" gorm:"type:decimal(10,2); default:0.00"`
|
||||||
|
Currency CurrencyType `json:"currency" form:"currency" gorm:"type:varchar(5)"`
|
||||||
|
Config string `json:"config" form:"config" gorm:"type:text"`
|
||||||
|
Sort int `json:"sort" form:"sort" gorm:"default:1"`
|
||||||
|
Enable *bool `json:"enable" form:"enable" gorm:"default:true"`
|
||||||
|
CreatedAt int64 `json:"created_at" gorm:"bigint"`
|
||||||
|
UpdatedAt int64 `json:"-" gorm:"bigint"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPaymentByID(id int) (*Payment, error) {
|
||||||
|
var payment Payment
|
||||||
|
err := DB.First(&payment, id).Error
|
||||||
|
return &payment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPaymentByUUID(uuid string) (*Payment, error) {
|
||||||
|
var payment Payment
|
||||||
|
err := DB.Where("uuid = ? AND enable = ?", uuid, true).First(&payment).Error
|
||||||
|
return &payment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedPaymentOrderFields = map[string]bool{
|
||||||
|
"id": true,
|
||||||
|
"uuid": true,
|
||||||
|
"name": true,
|
||||||
|
"type": true,
|
||||||
|
"sort": true,
|
||||||
|
"enable": true,
|
||||||
|
"created_at": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchPaymentParams struct {
|
||||||
|
Payment
|
||||||
|
PaginationParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPanymentList(params *SearchPaymentParams) (*DataResult[Payment], error) {
|
||||||
|
var payments []*Payment
|
||||||
|
|
||||||
|
db := DB.Omit("key")
|
||||||
|
|
||||||
|
if params.Type != "" {
|
||||||
|
db = db.Where("type = ?", params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Name != "" {
|
||||||
|
db = db.Where("name LIKE ?", params.Name+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.UUID != "" {
|
||||||
|
db = db.Where("uuid = ?", params.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Currency != "" {
|
||||||
|
db = db.Where("currency = ?", params.Currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginateAndOrder(db, ¶ms.PaginationParams, &payments, allowedPaymentOrderFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserPaymentList() ([]*Payment, error) {
|
||||||
|
var payments []*Payment
|
||||||
|
err := DB.Model(payments).Select("uuid, name, icon, fixed_fee, percent_fee, currency, sort").Where("enable = ?", true).Find(&payments).Error
|
||||||
|
return payments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payment) Insert() error {
|
||||||
|
p.UUID = utils.GetUUID()
|
||||||
|
return DB.Create(p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payment) Update(overwrite bool) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if overwrite {
|
||||||
|
err = DB.Model(p).Select("*").Updates(p).Error
|
||||||
|
} else {
|
||||||
|
err = DB.Model(p).Updates(p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payment) Delete() error {
|
||||||
|
return DB.Delete(p).Error
|
||||||
|
}
|
80
payment/gateway/epay/client.go
Normal file
80
payment/gateway/epay/client.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package epay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
PayDomain string `json:"pay_domain"`
|
||||||
|
PartnerID string `json:"partner_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormPay 表单支付
|
||||||
|
func (c *Client) FormPay(args *PayArgs) (string, map[string]string, error) {
|
||||||
|
formPayArgs := map[string]string{
|
||||||
|
"type": string(args.Type),
|
||||||
|
"pid": c.PartnerID,
|
||||||
|
"out_trade_no": args.OutTradeNo,
|
||||||
|
"notify_url": args.NotifyUrl,
|
||||||
|
"return_url": args.ReturnUrl,
|
||||||
|
"name": args.Name,
|
||||||
|
"money": args.Money,
|
||||||
|
// "device": string(args.Device),
|
||||||
|
}
|
||||||
|
|
||||||
|
formPayArgs["sign"] = c.Sign(formPayArgs)
|
||||||
|
formPayArgs["sign_type"] = FormArgsSignType
|
||||||
|
|
||||||
|
domain := strings.TrimSuffix(c.PayDomain, "/")
|
||||||
|
|
||||||
|
return domain + FormSubmitUrl, formPayArgs, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Verify(params map[string]string) (*PaymentResult, bool) {
|
||||||
|
sign := params["sign"]
|
||||||
|
tradeStatus := params["trade_status"]
|
||||||
|
|
||||||
|
if sign == "" || tradeStatus != TradeStatusSuccess {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if sign != c.Sign(params) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var paymentResult PaymentResult
|
||||||
|
mapstructure.Decode(params, &paymentResult)
|
||||||
|
|
||||||
|
return &paymentResult, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign 签名
|
||||||
|
func (c *Client) Sign(args map[string]string) string {
|
||||||
|
keys := make([]string, 0, len(args))
|
||||||
|
for k := range args {
|
||||||
|
if k != "sign" && k != "sign_type" && args[k] != "" {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
signStrs := make([]string, len(keys))
|
||||||
|
for i, k := range keys {
|
||||||
|
signStrs[i] = k + "=" + args[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
signStr := strings.Join(signStrs, "&") + c.Key
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
h.Write([]byte(signStr))
|
||||||
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
91
payment/gateway/epay/payment.go
Normal file
91
payment/gateway/epay/payment.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package epay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/payment/types"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Epay struct{}
|
||||||
|
|
||||||
|
type EpayConfig struct {
|
||||||
|
PayType PayType `json:"pay_type"`
|
||||||
|
Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Epay) Name() string {
|
||||||
|
return "易支付"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Epay) Pay(config *types.PayConfig, gatewayConfig string) (*types.PayRequest, error) {
|
||||||
|
epayConfig, err := getEpayConfig(gatewayConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payArgs := &PayArgs{
|
||||||
|
Type: epayConfig.PayType,
|
||||||
|
OutTradeNo: config.TradeNo,
|
||||||
|
NotifyUrl: config.NotifyURL,
|
||||||
|
ReturnUrl: config.ReturnURL,
|
||||||
|
Name: config.TradeNo,
|
||||||
|
Money: strconv.FormatFloat(config.Money, 'f', 2, 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
formPayURL, formPayArgs, err := epayConfig.FormPay(payArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payRequest := &types.PayRequest{
|
||||||
|
Type: 1,
|
||||||
|
Data: types.PayRequestData{
|
||||||
|
URL: formPayURL,
|
||||||
|
Params: &formPayArgs,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return payRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Epay) HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error) {
|
||||||
|
queryMap := make(map[string]string)
|
||||||
|
if err := c.ShouldBindQuery(&queryMap); err != nil {
|
||||||
|
c.Writer.Write([]byte("fail"))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
epayConfig, err := getEpayConfig(gatewayConfig)
|
||||||
|
if err != nil {
|
||||||
|
c.Writer.Write([]byte("fail"))
|
||||||
|
return nil, fmt.Errorf("tradeNo: %s, PaymentNo: %s, err: %v", queryMap["out_trade_no"], queryMap["trade_no"], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentResult, success := epayConfig.Verify(queryMap)
|
||||||
|
if paymentResult != nil && success {
|
||||||
|
c.Writer.Write([]byte("success"))
|
||||||
|
payNotify := &types.PayNotify{
|
||||||
|
TradeNo: paymentResult.OutTradeNo,
|
||||||
|
GatewayNo: paymentResult.TradeNo,
|
||||||
|
}
|
||||||
|
return payNotify, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Write([]byte("fail"))
|
||||||
|
return nil, fmt.Errorf("tradeNo: %s, PaymentNo: %s, Verify Sign failed", queryMap["out_trade_no"], queryMap["trade_no"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEpayConfig(gatewayConfig string) (*EpayConfig, error) {
|
||||||
|
var epayConfig EpayConfig
|
||||||
|
if err := json.Unmarshal([]byte(gatewayConfig), &epayConfig); err != nil {
|
||||||
|
return nil, errors.New("config error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &epayConfig, nil
|
||||||
|
}
|
45
payment/gateway/epay/type.go
Normal file
45
payment/gateway/epay/type.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package epay
|
||||||
|
|
||||||
|
type PayType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
EpayPay PayType = "" // 网关
|
||||||
|
Alipay PayType = "alipay" // 支付宝
|
||||||
|
Wechat PayType = "wxpay" // 微信
|
||||||
|
QQ PayType = "qqpay" // QQ
|
||||||
|
Bank PayType = "bank" // 银行
|
||||||
|
JD PayType = "jdpay" // 京东
|
||||||
|
PayPal PayType = "paypal" // PayPal
|
||||||
|
USDT PayType = "usdt" // USDT
|
||||||
|
)
|
||||||
|
|
||||||
|
// type DeviceType string
|
||||||
|
|
||||||
|
// var (
|
||||||
|
// PC DeviceType = "pc" // PC
|
||||||
|
// Mobile DeviceType = "mobile" // 移动端
|
||||||
|
// )
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormArgsSignType = "MD5"
|
||||||
|
FormSubmitUrl = "/submit.php"
|
||||||
|
TradeStatusSuccess = "TRADE_SUCCESS"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PayArgs struct {
|
||||||
|
Type PayType `json:"type,omitempty"`
|
||||||
|
OutTradeNo string `json:"out_trade_no"`
|
||||||
|
NotifyUrl string `json:"notify_url"`
|
||||||
|
ReturnUrl string `json:"return_url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Money string `json:"money"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentResult struct {
|
||||||
|
Type PayType `mapstructure:"type"`
|
||||||
|
TradeNo string `mapstructure:"trade_no"`
|
||||||
|
OutTradeNo string `mapstructure:"out_trade_no"`
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
Money string `mapstructure:"money"`
|
||||||
|
TradeStatus string `mapstructure:"trade_status"`
|
||||||
|
}
|
20
payment/payment.go
Normal file
20
payment/payment.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/payment/gateway/epay"
|
||||||
|
"one-api/payment/types"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentProcessor interface {
|
||||||
|
Name() string
|
||||||
|
Pay(config *types.PayConfig, gatewayConfig string) (*types.PayRequest, error)
|
||||||
|
HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Gateways = make(map[string]PaymentProcessor)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Gateways["epay"] = &epay.Epay{}
|
||||||
|
}
|
81
payment/service.go
Normal file
81
payment/service.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"one-api/common/config"
|
||||||
|
"one-api/common/logger"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/payment/types"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentService struct {
|
||||||
|
Payment *model.Payment
|
||||||
|
gateway PaymentProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayMoney struct {
|
||||||
|
Amount float64
|
||||||
|
Currency model.CurrencyType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaymentService(uuid string) (*PaymentService, error) {
|
||||||
|
payment, err := model.GetPaymentByUUID(uuid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("payment not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway, ok := Gateways[payment.Type]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("payment gateway not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PaymentService{
|
||||||
|
Payment: payment,
|
||||||
|
gateway: gateway,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PaymentService) Pay(tradeNo string, amount float64) (*types.PayRequest, error) {
|
||||||
|
config := &types.PayConfig{
|
||||||
|
Money: amount,
|
||||||
|
TradeNo: tradeNo,
|
||||||
|
NotifyURL: s.getNotifyURL(),
|
||||||
|
ReturnURL: s.getReturnURL(),
|
||||||
|
}
|
||||||
|
|
||||||
|
payRequest, err := s.gateway.Pay(config, s.Payment.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payRequest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PaymentService) HandleCallback(c *gin.Context, gatewayConfig string) (*types.PayNotify, error) {
|
||||||
|
payNotify, err := s.gateway.HandleCallback(c, gatewayConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysError(fmt.Sprintf("%s payment callback error: %v", s.gateway.Name(), err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return payNotify, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PaymentService) getNotifyURL() string {
|
||||||
|
notifyDomain := s.Payment.NotifyDomain
|
||||||
|
if notifyDomain == "" {
|
||||||
|
notifyDomain = config.ServerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDomain = strings.TrimSuffix(notifyDomain, "/")
|
||||||
|
return fmt.Sprintf("%s/api/payment/notify/%s", notifyDomain, s.Payment.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PaymentService) getReturnURL() string {
|
||||||
|
serverAdd := strings.TrimSuffix(config.ServerAddress, "/")
|
||||||
|
return fmt.Sprintf("%s/panel/log", serverAdd)
|
||||||
|
}
|
27
payment/types/types.go
Normal file
27
payment/types/types.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// 支付网关的通用配置
|
||||||
|
type PayConfig struct {
|
||||||
|
NotifyURL string `json:"notify_url"`
|
||||||
|
ReturnURL string `json:"return_url"`
|
||||||
|
TradeNo string `json:"trade_no"`
|
||||||
|
Money float64 `json:"money"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求支付时的数据结构
|
||||||
|
type PayRequest struct {
|
||||||
|
Type int `json:"type"` // 支付类型 1 url 2 qrcode
|
||||||
|
Data PayRequestData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayRequestData struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
Params any `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付回调时的数据结构
|
||||||
|
type PayNotify struct {
|
||||||
|
TradeNo string `json:"trade_no"`
|
||||||
|
GatewayNo string `json:"gateway_no"`
|
||||||
|
}
|
@ -31,6 +31,8 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
|
|
||||||
|
apiRouter.Any("/payment/notify/:uuid", controller.PaymentCallback)
|
||||||
|
|
||||||
userRoute := apiRouter.Group("/user")
|
userRoute := apiRouter.Group("/user")
|
||||||
{
|
{
|
||||||
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
||||||
@ -48,6 +50,9 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
selfRoute.POST("/topup", controller.TopUp)
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
selfRoute.GET("/models", relay.ListModels)
|
selfRoute.GET("/models", relay.ListModels)
|
||||||
|
selfRoute.GET("/payment", controller.GetUserPaymentList)
|
||||||
|
selfRoute.POST("/order", controller.CreateOrder)
|
||||||
|
selfRoute.GET("/order/status", controller.CheckOrderStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRoute := userRoute.Group("/")
|
adminRoute := userRoute.Group("/")
|
||||||
@ -148,6 +153,17 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paymentRoute := apiRouter.Group("/payment")
|
||||||
|
paymentRoute.Use(middleware.AdminAuth())
|
||||||
|
{
|
||||||
|
paymentRoute.GET("/order", controller.GetOrderList)
|
||||||
|
paymentRoute.GET("/", controller.GetPaymentList)
|
||||||
|
paymentRoute.GET("/:id", controller.GetPayment)
|
||||||
|
paymentRoute.POST("/", controller.AddPayment)
|
||||||
|
paymentRoute.PUT("/", controller.UpdatePayment)
|
||||||
|
paymentRoute.DELETE("/:id", controller.DeletePayment)
|
||||||
|
}
|
||||||
|
|
||||||
mjRoute := apiRouter.Group("/mj")
|
mjRoute := apiRouter.Group("/mj")
|
||||||
mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney)
|
mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney)
|
||||||
mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney)
|
mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney)
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
|
"react-qrcode-logo": "^3.0.0",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
"react-router": "6.21.3",
|
"react-router": "6.21.3",
|
||||||
"react-router-dom": "6.21.3",
|
"react-router-dom": "6.21.3",
|
||||||
@ -82,5 +83,6 @@
|
|||||||
"immutable": "^4.3.5",
|
"immutable": "^4.3.5",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"sass": "^1.70.0"
|
"sass": "^1.70.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
BIN
web/public/ali_pay.png
Normal file
BIN
web/public/ali_pay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
5
web/public/logo-loading-white.svg
Normal file
5
web/public/logo-loading-white.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg id="eKwXi4fbWI71" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 590 360" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
||||||
|
<style><![CDATA[
|
||||||
|
#eKwXi4fbWI73_to {animation: eKwXi4fbWI73_to__to 3000ms linear infinite normal forwards}@keyframes eKwXi4fbWI73_to__to { 0% {transform: translate(386.852639px,-200.193104px)} 16.666667% {transform: translate(386.848452px,-187.188246px)} 33.333333% {transform: translate(386.848452px,-200.188246px)} 50% {transform: translate(386.848452px,-187.188246px)} 66.666667% {transform: translate(386.848452px,-200.188246px)} 83.333333% {transform: translate(386.848452px,-187.188246px)} 100% {transform: translate(386.848452px,-200.188246px)}}
|
||||||
|
]]></style>
|
||||||
|
<g><g id="eKwXi4fbWI73_to" transform="translate(386.852639,-200.193104)"><ellipse rx="28.47" ry="28.14" transform="rotate(103.599994) translate(287.6,33.87)" fill="#fff"/></g><path d="M232.92,128.89c3.78,27.29-1.81,55.44-17.71,78.09-.610963.870887-.634672,2.024751-.06,2.92c1.24,1.92,2.96,5.05,5.56,4.94q5.25-.22,10.79.11c.672283.037376,1.196382.596709,1.19,1.27l-.4,42.53c-.005493.719585-.590394,1.300021-1.31,1.3q-16.77-.09-36.53.01-2.25.02-3.71-1.56-16.02-17.28-31.98-35.32c-5.13-5.8-10.18-11.16-14.86-17.59-.234342-.319666-.313973-.727248-.217488-1.113186s.359185-.710636.717488-.886814q12.88-6.32,22.13-17.12q18.18-21.23,15.08-48.84-2.66-23.7-22.4-40.46-23.43-19.9-54.88-13.86c-4.1.79-7.83,2.5-11.72,4.12q-11.86,4.94-20.59,14.64c-14.25,15.81-20.07,36.4-15.05,57.16q4.99,20.63,22.86,35.71c10.45,8.81,23.7,13.12,37.26,14.18q1.47.11,3.6,2.65c11.68,13.89,24.48,27.72,35.94,41.96.089095.111267.117261.259455.0752.39565s-.148879.242697-.2852.28435q-22.51,7.27-47.37,5.37-19.4-1.47-39.74-11.22-18.27-8.75-30.59-21.28Q20.06,208.3,10.7,183.71q-10.8-28.4-4.93-58.67c1.59-8.17,4.03-17,7.42-24.61Q18.27,89.05,24.8,79.79Q50.21,43.76,93.25,33.66q32.42-7.61,64.23,3.92q25.31,9.17,43.2,27.31c16.85,17.09,28.91,40.01,32.24,64Z" transform="translate(-.326548 38.749742)" fill="#fff"/><path d="M499.47,180.61c6.45,13.53,16.44,21.75,31.96,22q11.94.19,22.17-5.36q2.21-1.2,3.93.69q12.56,13.78,24.89,28.47q1.21,1.44,1.44,3.13c.049759.339937-.087897.680254-.36.89-1.62,1.23-3.33,2.71-5.03,3.69Q549.1,251.13,516,245.43c-20.61-3.55-39.05-15.24-51.47-32.51q-6.4-8.89-9.91-17.08c-2.62-6.12-4.73-13.3-5.41-20.08q-3.96-39.88,22.94-67.74c9.48-9.81,21.15-16.67,34.39-19.49c16.54-3.53,34.64-1.83,48.77,7.1q13.92,8.79,21.13,20.4q11.07,17.84,10.48,38.92c-.02.94-.21,1.81-.85,2.54q-7.73,8.77-18.71,20.16c-1.28,1.32-2.61,2.26-4.51,2.23q-24.45-.37-51.64-.41-5.03,0-10.84-.22c-.334463-.009876-.650686.153565-.834487.431309s-.208627.629665-.065513.928691Zm1.12-37.17q-.55,1.19-.63,2.34-.08,1.01.94,1.03q19.01.25,36.98.01.5,0,.94-.22.57-.28.44-.9-2.34-11.6-14.11-15.25-3.59-1.11-6.44-.57-13.07,2.5-18.12,13.56Z" transform="translate(-.326548 38.749742)" fill="#fff"/><path d="M312.3,100.22c-.001444.196624.116164.373518.298977.44969s.395635.036957.541023-.09969q2.76-2.64,5.82-4.31q8.45-4.62,16.71-6.57c15.81-3.72,33.58-3.2,48.2,3.95q24.49,11.98,35.05,35.76c4.66,10.5,5.44,22.96,5.5,35.35q.21,49.99-.12,88-.03,3.06-.08,6.16c-.010966.725105-.604834,1.305577-1.33,1.3q-20.22-.18-40.18-.23-3.64-.01-8.13-.44c-.537084-.051394-.94781-.505354-.95-1.05q.02-45.49-.22-92.99c-.03-6.25-1.21-13.88-5.05-18.95q-5.33-7.03-12.32-10.18c-10.99-4.93-24.52-1.84-33.13,6.37q-10.01,9.53-10.07,23.76-.11,25.46-.1,48.98c0,3.52-.06,8.31-1.1,11.68-4.37,14.04-17.31,19.5-31.04,16.77-8.22-1.64-15.07-7.75-17.62-15.62q-1.45-4.49-1.42-10.2.3-64.69.1-129.86c0-.124652.049518-.244198.13766-.33234s.207688-.13766.33234-.13766l48.46-.35c.852152-.005489,1.548978.682403,1.56,1.54l.15,11.25Z" transform="translate(-.326548 38.749742)" fill="#fff"/></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
5
web/public/logo-loading.svg
Normal file
5
web/public/logo-loading.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg id="eKwXi4fbWI71" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 590 360" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
||||||
|
<style><![CDATA[
|
||||||
|
#eKwXi4fbWI73_to {animation: eKwXi4fbWI73_to__to 3000ms linear infinite normal forwards}@keyframes eKwXi4fbWI73_to__to { 0% {transform: translate(386.852639px,-200.193104px)} 16.666667% {transform: translate(386.848452px,-187.188246px)} 33.333333% {transform: translate(386.848452px,-200.188246px)} 50% {transform: translate(386.848452px,-187.188246px)} 66.666667% {transform: translate(386.848452px,-200.188246px)} 83.333333% {transform: translate(386.848452px,-187.188246px)} 100% {transform: translate(386.848452px,-200.188246px)}}
|
||||||
|
]]></style>
|
||||||
|
<g><g id="eKwXi4fbWI73_to" transform="translate(386.852639,-200.193104)"><ellipse rx="28.47" ry="28.14" transform="rotate(103.599994) translate(287.6,33.87)" fill="#0d161f"/></g><path d="M232.92,128.89c3.78,27.29-1.81,55.44-17.71,78.09-.610963.870887-.634672,2.024751-.06,2.92c1.24,1.92,2.96,5.05,5.56,4.94q5.25-.22,10.79.11c.672283.037376,1.196382.596709,1.19,1.27l-.4,42.53c-.005493.719585-.590394,1.300021-1.31,1.3q-16.77-.09-36.53.01-2.25.02-3.71-1.56-16.02-17.28-31.98-35.32c-5.13-5.8-10.18-11.16-14.86-17.59-.234342-.319666-.313973-.727248-.217488-1.113186s.359185-.710636.717488-.886814q12.88-6.32,22.13-17.12q18.18-21.23,15.08-48.84-2.66-23.7-22.4-40.46-23.43-19.9-54.88-13.86c-4.1.79-7.83,2.5-11.72,4.12q-11.86,4.94-20.59,14.64c-14.25,15.81-20.07,36.4-15.05,57.16q4.99,20.63,22.86,35.71c10.45,8.81,23.7,13.12,37.26,14.18q1.47.11,3.6,2.65c11.68,13.89,24.48,27.72,35.94,41.96.089095.111267.117261.259455.0752.39565s-.148879.242697-.2852.28435q-22.51,7.27-47.37,5.37-19.4-1.47-39.74-11.22-18.27-8.75-30.59-21.28Q20.06,208.3,10.7,183.71q-10.8-28.4-4.93-58.67c1.59-8.17,4.03-17,7.42-24.61Q18.27,89.05,24.8,79.79Q50.21,43.76,93.25,33.66q32.42-7.61,64.23,3.92q25.31,9.17,43.2,27.31c16.85,17.09,28.91,40.01,32.24,64Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/><path d="M499.47,180.61c6.45,13.53,16.44,21.75,31.96,22q11.94.19,22.17-5.36q2.21-1.2,3.93.69q12.56,13.78,24.89,28.47q1.21,1.44,1.44,3.13c.049759.339937-.087897.680254-.36.89-1.62,1.23-3.33,2.71-5.03,3.69Q549.1,251.13,516,245.43c-20.61-3.55-39.05-15.24-51.47-32.51q-6.4-8.89-9.91-17.08c-2.62-6.12-4.73-13.3-5.41-20.08q-3.96-39.88,22.94-67.74c9.48-9.81,21.15-16.67,34.39-19.49c16.54-3.53,34.64-1.83,48.77,7.1q13.92,8.79,21.13,20.4q11.07,17.84,10.48,38.92c-.02.94-.21,1.81-.85,2.54q-7.73,8.77-18.71,20.16c-1.28,1.32-2.61,2.26-4.51,2.23q-24.45-.37-51.64-.41-5.03,0-10.84-.22c-.334463-.009876-.650686.153565-.834487.431309s-.208627.629665-.065513.928691Zm1.12-37.17q-.55,1.19-.63,2.34-.08,1.01.94,1.03q19.01.25,36.98.01.5,0,.94-.22.57-.28.44-.9-2.34-11.6-14.11-15.25-3.59-1.11-6.44-.57-13.07,2.5-18.12,13.56Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/><path d="M312.3,100.22c-.001444.196624.116164.373518.298977.44969s.395635.036957.541023-.09969q2.76-2.64,5.82-4.31q8.45-4.62,16.71-6.57c15.81-3.72,33.58-3.2,48.2,3.95q24.49,11.98,35.05,35.76c4.66,10.5,5.44,22.96,5.5,35.35q.21,49.99-.12,88-.03,3.06-.08,6.16c-.010966.725105-.604834,1.305577-1.33,1.3q-20.22-.18-40.18-.23-3.64-.01-8.13-.44c-.537084-.051394-.94781-.505354-.95-1.05q.02-45.49-.22-92.99c-.03-6.25-1.21-13.88-5.05-18.95q-5.33-7.03-12.32-10.18c-10.99-4.93-24.52-1.84-33.13,6.37q-10.01,9.53-10.07,23.76-.11,25.46-.1,48.98c0,3.52-.06,8.31-1.1,11.68-4.37,14.04-17.31,19.5-31.04,16.77-8.22-1.64-15.07-7.75-17.62-15.62q-1.45-4.49-1.42-10.2.3-64.69.1-129.86c0-.124652.049518-.244198.13766-.33234s.207688-.13766.33234-.13766l48.46-.35c.852152-.005489,1.548978.682403,1.56,1.54l.15,11.25Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
BIN
web/public/wechat_pay.png
Normal file
BIN
web/public/wechat_pay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
1
web/src/assets/images/success.svg
Normal file
1
web/src/assets/images/success.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1717527029040" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6841" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M874.119618 149.859922A510.816461 510.816461 0 0 0 511.997 0.00208a509.910462 509.910462 0 0 0-362.119618 149.857842c-199.817789 199.679789-199.817789 524.581447 0 724.260236a509.969462 509.969462 0 0 0 362.119618 149.857842A508.872463 508.872463 0 0 0 874.119618 874.120158c199.836789-199.679789 199.836789-524.581447 0-724.260236zM814.94268 378.210681L470.999043 744.132295a15.359984 15.359984 0 0 1-5.887994 4.095996c-1.751998 1.180999-2.913997 2.362998-5.276994 2.913997a34.499964 34.499964 0 0 1-13.469986 2.914997 45.547952 45.547952 0 0 1-12.897986-2.303998l-4.095996-2.363997a45.291952 45.291952 0 0 1-7.009992-4.095996l-196.902793-193.789796a34.126964 34.126964 0 0 1-10.555989-25.186973c0-9.37399 3.583996-18.74698 9.98399-25.186974a36.429962 36.429962 0 0 1 50.372947 0l169.98382 167.423824L763.389735 330.220732a37.059961 37.059961 0 0 1 50.371947-1.732998 33.647965 33.647965 0 0 1 11.165988 25.186973 35.544963 35.544963 0 0 1-9.98399 24.575974v-0.04z m0 0" fill="#52C41A" p-id="6842"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -26,7 +26,7 @@ const config = {
|
|||||||
lark_login: false,
|
lark_login: false,
|
||||||
lark_client_id: '',
|
lark_client_id: '',
|
||||||
telegram_bot: '',
|
telegram_bot: '',
|
||||||
isLoading: true, // 添加加载状态
|
isLoading: true // 添加加载状态
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ import {
|
|||||||
IconReceipt2,
|
IconReceipt2,
|
||||||
IconBrush,
|
IconBrush,
|
||||||
IconBrandGithubCopilot,
|
IconBrandGithubCopilot,
|
||||||
IconBallFootball
|
IconBallFootball,
|
||||||
|
IconBrandPaypal
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
@ -33,7 +34,8 @@ const icons = {
|
|||||||
IconReceipt2,
|
IconReceipt2,
|
||||||
IconBrush,
|
IconBrush,
|
||||||
IconBrandGithubCopilot,
|
IconBrandGithubCopilot,
|
||||||
IconBallFootball
|
IconBallFootball,
|
||||||
|
IconBrandPaypal
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||||
@ -154,6 +156,15 @@ const panel = {
|
|||||||
breadcrumbs: false,
|
breadcrumbs: false,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'payment',
|
||||||
|
title: '支付',
|
||||||
|
type: 'item',
|
||||||
|
url: '/panel/payment',
|
||||||
|
icon: icons.IconBrandPaypal,
|
||||||
|
breadcrumbs: false,
|
||||||
|
isAdmin: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'setting',
|
id: 'setting',
|
||||||
title: '设置',
|
title: '设置',
|
||||||
|
@ -19,6 +19,7 @@ const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
|||||||
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
||||||
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
||||||
const Playground = Loadable(lazy(() => import('views/Playground')));
|
const Playground = Loadable(lazy(() => import('views/Playground')));
|
||||||
|
const Payment = Loadable(lazy(() => import('views/Payment')));
|
||||||
|
|
||||||
// dashboard routing
|
// dashboard routing
|
||||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||||
@ -96,6 +97,10 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: 'playground',
|
path: 'playground',
|
||||||
element: <Playground />
|
element: <Playground />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'payment',
|
||||||
|
element: <Payment />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ const siteInfoReducer = (state = initialState, action) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...action.payload,
|
...action.payload,
|
||||||
isLoading: false, // 添加加载状态
|
isLoading: false // 添加加载状态
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
294
web/src/views/Payment/Gateway.js
Normal file
294
web/src/views/Payment/Gateway.js
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { showError, trims, showSuccess } from 'utils/common';
|
||||||
|
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
||||||
|
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
|
||||||
|
import PaymentTableRow from './component/TableRow';
|
||||||
|
import KeywordTableHead from 'ui-component/TableHead';
|
||||||
|
import TableToolBar from './component/TableToolBar';
|
||||||
|
import EditeModal from './component/EditModal';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { ITEMS_PER_PAGE } from 'constants';
|
||||||
|
import { IconRefresh, IconSearch, IconPlus } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export default function Gateway() {
|
||||||
|
const originalKeyword = {
|
||||||
|
p: 0,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
uuid: '',
|
||||||
|
currency: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [order, setOrder] = useState('desc');
|
||||||
|
const [orderBy, setOrderBy] = useState('created_at');
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||||
|
const [listCount, setListCount] = useState(0);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||||
|
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [editPaymentId, setEditPaymentId] = useState(0);
|
||||||
|
|
||||||
|
const [payment, setPayment] = useState([]);
|
||||||
|
|
||||||
|
const handleSort = (event, id) => {
|
||||||
|
const isAsc = orderBy === id && order === 'asc';
|
||||||
|
if (id !== '') {
|
||||||
|
setOrder(isAsc ? 'desc' : 'asc');
|
||||||
|
setOrderBy(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenModal = (channelId) => {
|
||||||
|
setEditPaymentId(channelId);
|
||||||
|
setOpenModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpenModal(false);
|
||||||
|
setEditPaymentId(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOkModal = (status) => {
|
||||||
|
if (status === true) {
|
||||||
|
handleCloseModal();
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event) => {
|
||||||
|
setPage(0);
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
setPage(0);
|
||||||
|
setSearchKeyword(toolBarValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolBarValue = (event) => {
|
||||||
|
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const managePayment = async (id, action, value) => {
|
||||||
|
const url = '/api/payment/';
|
||||||
|
let data = { id };
|
||||||
|
let res;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case 'delete':
|
||||||
|
res = await API.delete(url + id);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
res = await API.put(url, {
|
||||||
|
...data,
|
||||||
|
enable: value
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'sort':
|
||||||
|
res = await API.put(url, {
|
||||||
|
...data,
|
||||||
|
sort: value
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('操作成功完成!');
|
||||||
|
await handleRefresh();
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = useCallback(async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||||
|
setSearching(true);
|
||||||
|
keyword = trims(keyword);
|
||||||
|
try {
|
||||||
|
if (orderBy) {
|
||||||
|
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
||||||
|
}
|
||||||
|
const res = await API.get('/api/payment/', {
|
||||||
|
params: {
|
||||||
|
page: page + 1,
|
||||||
|
size: rowsPerPage,
|
||||||
|
order: orderBy,
|
||||||
|
...keyword
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setListCount(data.total_count);
|
||||||
|
setPayment(data.data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
setSearching(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setOrderBy('created_at');
|
||||||
|
setOrder('desc');
|
||||||
|
setToolBarValue(originalKeyword);
|
||||||
|
setSearchKeyword(originalKeyword);
|
||||||
|
setRefreshFlag(!refreshFlag);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||||
|
}, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||||
|
<Typography variant="h4">支付网关</Typography>
|
||||||
|
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||||
|
新建支付
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Card>
|
||||||
|
<Box component="form" noValidate>
|
||||||
|
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} />
|
||||||
|
</Box>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
textAlign: 'right',
|
||||||
|
height: 50,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||||
|
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||||
|
刷新/清除搜索条件
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={search} startIcon={<IconSearch width={'18px'} />}>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Container>
|
||||||
|
</Toolbar>
|
||||||
|
{searching && <LinearProgress />}
|
||||||
|
<PerfectScrollbar component="div">
|
||||||
|
<TableContainer sx={{ overflow: 'unset' }}>
|
||||||
|
<Table sx={{ minWidth: 800 }}>
|
||||||
|
<KeywordTableHead
|
||||||
|
order={order}
|
||||||
|
orderBy={orderBy}
|
||||||
|
onRequestSort={handleSort}
|
||||||
|
headLabel={[
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uuid',
|
||||||
|
label: 'UUID',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
label: '名称',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'type',
|
||||||
|
label: '类型',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'icon',
|
||||||
|
label: '图标',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fixed_fee',
|
||||||
|
label: '固定手续费',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'percent_fee',
|
||||||
|
label: '百分比手续费',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sort',
|
||||||
|
label: '排序',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enable',
|
||||||
|
label: '启用',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'created_at',
|
||||||
|
label: '创建时间',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'action',
|
||||||
|
label: '操作',
|
||||||
|
disableSort: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TableBody>
|
||||||
|
{payment.map((row, index) => (
|
||||||
|
<PaymentTableRow
|
||||||
|
item={row}
|
||||||
|
key={`${row.id}_${index}`}
|
||||||
|
managePayment={managePayment}
|
||||||
|
handleOpenModal={handleOpenModal}
|
||||||
|
setModalPaymentId={setEditPaymentId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
<TablePagination
|
||||||
|
page={page}
|
||||||
|
component="div"
|
||||||
|
count={listCount}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
rowsPerPageOptions={[10, 25, 30]}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
/>
|
||||||
|
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} paymentId={editPaymentId} />
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
220
web/src/views/Payment/Order.js
Normal file
220
web/src/views/Payment/Order.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { showError, trims } from 'utils/common';
|
||||||
|
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
||||||
|
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
|
||||||
|
import LogTableRow from './component/OrderTableRow';
|
||||||
|
import KeywordTableHead from 'ui-component/TableHead';
|
||||||
|
import TableToolBar from './component/OrderTableToolBar';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { ITEMS_PER_PAGE } from 'constants';
|
||||||
|
import { IconRefresh, IconSearch } from '@tabler/icons-react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export default function Order() {
|
||||||
|
const originalKeyword = {
|
||||||
|
p: 0,
|
||||||
|
user_id: '',
|
||||||
|
trade_no: '',
|
||||||
|
status: '',
|
||||||
|
gateway_no: '',
|
||||||
|
start_timestamp: 0,
|
||||||
|
end_timestamp: dayjs().unix() + 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [order, setOrder] = useState('desc');
|
||||||
|
const [orderBy, setOrderBy] = useState('created_at');
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||||
|
const [listCount, setListCount] = useState(0);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||||
|
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||||
|
|
||||||
|
const [orderList, setOrderList] = useState([]);
|
||||||
|
|
||||||
|
const handleSort = (event, id) => {
|
||||||
|
const isAsc = orderBy === id && order === 'asc';
|
||||||
|
if (id !== '') {
|
||||||
|
setOrder(isAsc ? 'desc' : 'asc');
|
||||||
|
setOrderBy(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event) => {
|
||||||
|
setPage(0);
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchLogs = async () => {
|
||||||
|
setPage(0);
|
||||||
|
setSearchKeyword(toolBarValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolBarValue = (event) => {
|
||||||
|
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = useCallback(async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||||
|
setSearching(true);
|
||||||
|
keyword = trims(keyword);
|
||||||
|
try {
|
||||||
|
if (orderBy) {
|
||||||
|
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
||||||
|
}
|
||||||
|
const res = await API.get('/api/payment/order', {
|
||||||
|
params: {
|
||||||
|
page: page + 1,
|
||||||
|
size: rowsPerPage,
|
||||||
|
order: orderBy,
|
||||||
|
...keyword
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setListCount(data.total_count);
|
||||||
|
setOrderList(data.data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
setSearching(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setOrderBy('created_at');
|
||||||
|
setOrder('desc');
|
||||||
|
setToolBarValue(originalKeyword);
|
||||||
|
setSearchKeyword(originalKeyword);
|
||||||
|
setRefreshFlag(!refreshFlag);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||||
|
}, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||||
|
<Typography variant="h4">日志</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Card>
|
||||||
|
<Box component="form" noValidate>
|
||||||
|
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} />
|
||||||
|
</Box>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
textAlign: 'right',
|
||||||
|
height: 50,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||||
|
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||||
|
刷新/清除搜索条件
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={searchLogs} startIcon={<IconSearch width={'18px'} />}>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Container>
|
||||||
|
</Toolbar>
|
||||||
|
{searching && <LinearProgress />}
|
||||||
|
<PerfectScrollbar component="div">
|
||||||
|
<TableContainer sx={{ overflow: 'unset' }}>
|
||||||
|
<Table sx={{ minWidth: 800 }}>
|
||||||
|
<KeywordTableHead
|
||||||
|
order={order}
|
||||||
|
orderBy={orderBy}
|
||||||
|
onRequestSort={handleSort}
|
||||||
|
headLabel={[
|
||||||
|
{
|
||||||
|
id: 'created_at',
|
||||||
|
label: '时间',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user_id',
|
||||||
|
label: '用户',
|
||||||
|
disableSort: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trade_no',
|
||||||
|
label: '订单号',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gateway_no',
|
||||||
|
label: '网关订单号',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'amount',
|
||||||
|
label: '充值金额',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fee',
|
||||||
|
label: '手续费',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order_amount',
|
||||||
|
label: '实际支付金额',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quota',
|
||||||
|
label: '到帐点数',
|
||||||
|
disableSort: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
label: '状态',
|
||||||
|
disableSort: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TableBody>
|
||||||
|
{orderList.map((row, index) => (
|
||||||
|
<LogTableRow item={row} key={`${row.id}_${index}`} />
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
<TablePagination
|
||||||
|
page={page}
|
||||||
|
component="div"
|
||||||
|
count={listCount}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
rowsPerPageOptions={[10, 25, 30]}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
359
web/src/views/Payment/component/EditModal.js
Normal file
359
web/src/views/Payment/component/EditModal.js
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
OutlinedInput,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormHelperText
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { showSuccess, showError, trims } from 'utils/common';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { PaymentType, CurrencyType, PaymentConfig } from '../type/Config';
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
is_edit: Yup.boolean(),
|
||||||
|
name: Yup.string().required('名称 不能为空'),
|
||||||
|
icon: Yup.string().required('图标 不能为空'),
|
||||||
|
fixed_fee: Yup.number().min(0, '固定手续费 不能小于 0'),
|
||||||
|
percent_fee: Yup.number().min(0, '百分比手续费 不能小于 0'),
|
||||||
|
currency: Yup.string().required('货币 不能为空')
|
||||||
|
});
|
||||||
|
|
||||||
|
const originInputs = {
|
||||||
|
is_edit: false,
|
||||||
|
type: 'epay',
|
||||||
|
uuid: '',
|
||||||
|
name: '',
|
||||||
|
icon: '',
|
||||||
|
notify_domain: '',
|
||||||
|
fixed_fee: 0,
|
||||||
|
percent_fee: 0,
|
||||||
|
currency: 'CNY',
|
||||||
|
config: {},
|
||||||
|
sort: 0,
|
||||||
|
enable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditModal = ({ open, paymentId, onCancel, onOk }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
|
|
||||||
|
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
let config = JSON.stringify(values.config);
|
||||||
|
let res;
|
||||||
|
values = trims(values);
|
||||||
|
try {
|
||||||
|
if (values.is_edit) {
|
||||||
|
res = await API.put(`/api/payment/`, { ...values, id: parseInt(paymentId), config });
|
||||||
|
} else {
|
||||||
|
res = await API.post(`/api/payment/`, { ...values, config });
|
||||||
|
}
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (values.is_edit) {
|
||||||
|
showSuccess('更新成功!');
|
||||||
|
} else {
|
||||||
|
showSuccess('创建成功!');
|
||||||
|
}
|
||||||
|
setSubmitting(false);
|
||||||
|
setStatus({ success: true });
|
||||||
|
onOk(true);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
setErrors({ submit: message });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPayment = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/payment/${paymentId}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
data.is_edit = true;
|
||||||
|
data.config = JSON.parse(data.config);
|
||||||
|
setInputs(data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paymentId) {
|
||||||
|
loadPayment().then();
|
||||||
|
} else {
|
||||||
|
setInputs(originInputs);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [paymentId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||||
|
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||||
|
{paymentId ? '编辑支付' : '新建支付'}
|
||||||
|
</DialogTitle>
|
||||||
|
<Divider />
|
||||||
|
<DialogContent>
|
||||||
|
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||||
|
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||||
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
|
<FormControl fullWidth error={Boolean(touched.type && errors.type)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-type-label">类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="channel-type-label"
|
||||||
|
label="类型"
|
||||||
|
value={values.type}
|
||||||
|
name="type"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(PaymentType).map(([value, text]) => (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{text}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{touched.type && errors.type ? (
|
||||||
|
<FormHelperText error id="helper-tex-channel-type-label">
|
||||||
|
{errors.type}
|
||||||
|
</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormHelperText id="helper-tex-channel-type-label"> 支付类型 </FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.name && errors.name)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-name-label"
|
||||||
|
label="名称"
|
||||||
|
type="text"
|
||||||
|
value={values.name}
|
||||||
|
name="name"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'name' }}
|
||||||
|
aria-describedby="helper-text-channel-name-label"
|
||||||
|
/>
|
||||||
|
{touched.name && errors.name && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-name-label">
|
||||||
|
{errors.name}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.icon && errors.icon)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-icon-label">图标</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-icon-label"
|
||||||
|
label="图标"
|
||||||
|
type="text"
|
||||||
|
value={values.icon}
|
||||||
|
name="icon"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'icon' }}
|
||||||
|
aria-describedby="helper-text-channel-icon-label"
|
||||||
|
/>
|
||||||
|
{touched.icon && errors.icon && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-icon-label">
|
||||||
|
{errors.icon}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.notify_domain && errors.notify_domain)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-notify_domain-label">回调域名</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-notify_domain-label"
|
||||||
|
label="回调域名"
|
||||||
|
type="text"
|
||||||
|
value={values.notify_domain}
|
||||||
|
name="notify_domain"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'notify_domain' }}
|
||||||
|
aria-describedby="helper-text-channel-notify_domain-label"
|
||||||
|
/>
|
||||||
|
{touched.notify_domain && errors.notify_domain ? (
|
||||||
|
<FormHelperText error id="helper-tex-notify_domain-label">
|
||||||
|
{errors.notify_domain}
|
||||||
|
</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormHelperText id="helper-tex-notify_domain-label"> 支付回调的域名,除非你自行配置过,否则保持为空 </FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.fixed_fee && errors.fixed_fee)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-fixed_fee-label">固定手续费</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-fixed_fee-label"
|
||||||
|
label="固定手续费"
|
||||||
|
type="number"
|
||||||
|
value={values.fixed_fee}
|
||||||
|
name="fixed_fee"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'fixed_fee' }}
|
||||||
|
aria-describedby="helper-text-channel-fixed_fee-label"
|
||||||
|
/>
|
||||||
|
{touched.fixed_fee && errors.fixed_fee ? (
|
||||||
|
<FormHelperText error id="helper-tex-fixed_fee-label">
|
||||||
|
{errors.fixed_fee}
|
||||||
|
</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormHelperText id="helper-tex-fixed_fee-label"> 每次支付收取固定的手续费,单位 美元 </FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.percent_fee && errors.percent_fee)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-percent_fee-label">百分比手续费</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-percent_fee-label"
|
||||||
|
label="百分比手续费"
|
||||||
|
type="number"
|
||||||
|
value={values.percent_fee}
|
||||||
|
name="percent_fee"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'percent_fee' }}
|
||||||
|
aria-describedby="helper-text-channel-percent_fee-label"
|
||||||
|
/>
|
||||||
|
{touched.percent_fee && errors.percent_fee ? (
|
||||||
|
<FormHelperText error id="helper-tex-percent_fee-label">
|
||||||
|
{errors.percent_fee}
|
||||||
|
</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormHelperText id="helper-tex-percent_fee-label"> 每次支付按百分比收取手续费,如果为5%,请填写 0.05 </FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.currency && errors.currency)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-currency-label">网关货币类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="channel-currency-label"
|
||||||
|
label="网关货币类型"
|
||||||
|
value={values.currency}
|
||||||
|
name="currency"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(CurrencyType).map(([value, text]) => (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{text}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{touched.currency && errors.currency ? (
|
||||||
|
<FormHelperText error id="helper-tex-channel-currency-label">
|
||||||
|
{errors.currency}
|
||||||
|
</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormHelperText id="helper-tex-channel-currency-label"> 该网关是收取什么货币的,请查询对应网关文档 </FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{PaymentConfig[values.type] &&
|
||||||
|
Object.keys(PaymentConfig[values.type]).map((configKey) => {
|
||||||
|
const param = PaymentConfig[values.type][configKey];
|
||||||
|
const name = `config.${configKey}`;
|
||||||
|
return param.type === 'select' ? (
|
||||||
|
<FormControl key={name} fullWidth>
|
||||||
|
<InputLabel htmlFor="channel-currency-label">{param.name}</InputLabel>
|
||||||
|
<Select
|
||||||
|
label={param.name}
|
||||||
|
value={values.config?.[configKey] || ''}
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(param.options).map((option) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<FormHelperText id="helper-tex-channel-currency-label"> {param.description} </FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormControl key={name} fullWidth sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
value={values.config?.[configKey] || ''}
|
||||||
|
label={param.name}
|
||||||
|
placeholder={param.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<FormHelperText id="helper-tex-channel-key-label"> {param.description} </FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel}>取消</Button>
|
||||||
|
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditModal;
|
||||||
|
|
||||||
|
EditModal.propTypes = {
|
||||||
|
open: PropTypes.bool,
|
||||||
|
paymentId: PropTypes.number,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onOk: PropTypes.func
|
||||||
|
};
|
45
web/src/views/Payment/component/OrderTableRow.js
Normal file
45
web/src/views/Payment/component/OrderTableRow.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { TableRow, TableCell } from '@mui/material';
|
||||||
|
|
||||||
|
import { timestamp2string } from 'utils/common';
|
||||||
|
import Label from 'ui-component/Label';
|
||||||
|
|
||||||
|
const StatusType = {
|
||||||
|
pending: { name: '待支付', value: 'pending', color: 'primary' },
|
||||||
|
success: { name: '支付成功', value: 'success', color: 'success' },
|
||||||
|
failed: { name: '支付失败', value: 'failed', color: 'error' },
|
||||||
|
closed: { name: '已关闭', value: 'closed', color: 'default' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function statusLabel(status) {
|
||||||
|
let statusOption = StatusType[status];
|
||||||
|
|
||||||
|
return <Label color={statusOption?.color || 'secondary'}> {statusOption?.name || '未知'} </Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StatusType };
|
||||||
|
|
||||||
|
export default function OrderTableRow({ item }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow tabIndex={item.id}>
|
||||||
|
<TableCell>{timestamp2string(item.created_at)}</TableCell>
|
||||||
|
<TableCell>{item.user_id}</TableCell>
|
||||||
|
<TableCell>{item.trade_no}</TableCell>
|
||||||
|
<TableCell>{item.gateway_no}</TableCell>
|
||||||
|
<TableCell>${item.amount}</TableCell>
|
||||||
|
<TableCell>${item.fee}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.order_amount} {item.order_currency}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{item.quota}</TableCell>
|
||||||
|
<TableCell>{statusLabel(item.status)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderTableRow.propTypes = {
|
||||||
|
item: PropTypes.object
|
||||||
|
};
|
138
web/src/views/Payment/component/OrderTableToolBar.js
Normal file
138
web/src/views/Payment/component/OrderTableToolBar.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { OutlinedInput, Stack, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||||
|
import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { StatusType } from './OrderTableRow';
|
||||||
|
require('dayjs/locale/zh-cn');
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function OrderTableToolBar({ filterName, handleFilterName }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor="channel-user_id-label">用户ID</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="user_id"
|
||||||
|
name="user_id"
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
label="用户ID"
|
||||||
|
value={filterName.user_id}
|
||||||
|
onChange={handleFilterName}
|
||||||
|
placeholder="用户ID"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor="channel-trade_no-label">订单号</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="trade_no"
|
||||||
|
name="trade_no"
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
label="订单号"
|
||||||
|
value={filterName.trade_no}
|
||||||
|
onChange={handleFilterName}
|
||||||
|
placeholder="订单号"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor="channel-gateway_no-label">网关订单号</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="gateway_no"
|
||||||
|
name="gateway_no"
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
label="网关订单号"
|
||||||
|
value={filterName.gateway_no}
|
||||||
|
onChange={handleFilterName}
|
||||||
|
placeholder="网关订单号"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||||
|
<DateTimePicker
|
||||||
|
label="起始时间"
|
||||||
|
ampm={false}
|
||||||
|
name="start_timestamp"
|
||||||
|
value={filterName.start_timestamp === 0 ? null : dayjs.unix(filterName.start_timestamp)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value === null) {
|
||||||
|
handleFilterName({ target: { name: 'start_timestamp', value: 0 } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleFilterName({ target: { name: 'start_timestamp', value: value.unix() } });
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
actionBar: {
|
||||||
|
actions: ['clear', 'today', 'accept']
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||||
|
<DateTimePicker
|
||||||
|
label="结束时间"
|
||||||
|
name="end_timestamp"
|
||||||
|
ampm={false}
|
||||||
|
value={filterName.end_timestamp === 0 ? null : dayjs.unix(filterName.end_timestamp)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value === null) {
|
||||||
|
handleFilterName({ target: { name: 'end_timestamp', value: 0 } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleFilterName({ target: { name: 'end_timestamp', value: value.unix() } });
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
actionBar: {
|
||||||
|
actions: ['clear', 'today', 'accept']
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl sx={{ minWidth: '22%' }}>
|
||||||
|
<InputLabel htmlFor="channel-status-label">状态</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="channel-type-label"
|
||||||
|
label="状态"
|
||||||
|
value={filterName.status}
|
||||||
|
name="status"
|
||||||
|
onChange={handleFilterName}
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(StatusType).map((option) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderTableToolBar.propTypes = {
|
||||||
|
filterName: PropTypes.object,
|
||||||
|
handleFilterName: PropTypes.func
|
||||||
|
};
|
153
web/src/views/Payment/component/TableRow.js
Normal file
153
web/src/views/Payment/component/TableRow.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Popover,
|
||||||
|
MenuItem,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { timestamp2string, showError } from 'utils/common';
|
||||||
|
import { PaymentType } from '../type/Config';
|
||||||
|
import TableSwitch from 'ui-component/Switch';
|
||||||
|
|
||||||
|
export default function PaymentTableRow({ item, managePayment, handleOpenModal, setModalPaymentId }) {
|
||||||
|
const [open, setOpen] = useState(null);
|
||||||
|
const [openDelete, setOpenDelete] = useState(false);
|
||||||
|
const [sortValve, setSort] = useState(item.sort);
|
||||||
|
|
||||||
|
const handleCloseMenu = () => {
|
||||||
|
setOpen(null);
|
||||||
|
};
|
||||||
|
const handleOpenMenu = (event) => {
|
||||||
|
setOpen(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOpen = () => {
|
||||||
|
handleCloseMenu();
|
||||||
|
setOpenDelete(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClose = () => {
|
||||||
|
setOpenDelete(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
handleCloseMenu();
|
||||||
|
await managePayment(item.id, 'delete', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = async (event) => {
|
||||||
|
const currentValue = parseInt(event.target.value);
|
||||||
|
if (isNaN(currentValue) || currentValue === sortValve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue < 0) {
|
||||||
|
showError('排序不能小于 0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await managePayment(item.id, 'sort', currentValue);
|
||||||
|
setSort(currentValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow tabIndex={item.id}>
|
||||||
|
<TableCell>{item.id}</TableCell>
|
||||||
|
<TableCell>{item.uuid}</TableCell>
|
||||||
|
<TableCell>{item.name}</TableCell>
|
||||||
|
<TableCell>{PaymentType?.[item.type] || '未知'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<img src={item.icon} alt="icon" style={{ width: '24px', height: '24px' }} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{item.fixed_fee}</TableCell>
|
||||||
|
<TableCell>{item.percent_fee}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextField
|
||||||
|
id={`sort-${item.id}`}
|
||||||
|
onBlur={handleSort}
|
||||||
|
type="number"
|
||||||
|
label="排序"
|
||||||
|
variant="standard"
|
||||||
|
defaultValue={item.sort}
|
||||||
|
inputProps={{ min: '0' }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TableSwitch
|
||||||
|
id={`switch-${item.id}`}
|
||||||
|
checked={item.enable}
|
||||||
|
onChange={() => {
|
||||||
|
managePayment(item.id, 'status', !item.enable);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{timestamp2string(item.created_at)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton onClick={handleOpenMenu} sx={{ color: 'rgb(99, 115, 129)' }}>
|
||||||
|
<IconDotsVertical />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
open={!!open}
|
||||||
|
anchorEl={open}
|
||||||
|
onClose={handleCloseMenu}
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
PaperProps={{
|
||||||
|
sx: { minWidth: 140 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseMenu();
|
||||||
|
handleOpenModal();
|
||||||
|
setModalPaymentId(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit style={{ marginRight: '16px' }} />
|
||||||
|
编辑
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onClick={handleDeleteOpen} sx={{ color: 'error.main' }}>
|
||||||
|
<IconTrash style={{ marginRight: '16px' }} />
|
||||||
|
删除
|
||||||
|
</MenuItem>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||||
|
<DialogTitle>删除通道</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>是否删除通道 {item.name}?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||||
|
<Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentTableRow.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
managePayment: PropTypes.func,
|
||||||
|
handleOpenModal: PropTypes.func,
|
||||||
|
setModalPaymentId: PropTypes.func
|
||||||
|
};
|
73
web/src/views/Payment/component/TableToolBar.js
Normal file
73
web/src/views/Payment/component/TableToolBar.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { OutlinedInput, Stack, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||||
|
import { PaymentType } from '../type/Config';
|
||||||
|
require('dayjs/locale/zh-cn');
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function TableToolBar({ filterName, handleFilterName }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
label="名称"
|
||||||
|
value={filterName.name}
|
||||||
|
onChange={handleFilterName}
|
||||||
|
placeholder="名称"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel htmlFor="channel-uuid-label">UUID</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="uuid"
|
||||||
|
name="uuid"
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
label="模型名称"
|
||||||
|
value={filterName.uuid}
|
||||||
|
onChange={handleFilterName}
|
||||||
|
placeholder="UUID"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl sx={{ minWidth: '22%' }}>
|
||||||
|
<InputLabel htmlFor="channel-type-label">类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="channel-type-label"
|
||||||
|
label="类型"
|
||||||
|
value={filterName.type}
|
||||||
|
name="type"
|
||||||
|
onChange={handleFilterName}
|
||||||
|
sx={{
|
||||||
|
minWidth: '100%'
|
||||||
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(PaymentType).map(([value, text]) => (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{text}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TableToolBar.propTypes = {
|
||||||
|
filterName: PropTypes.object,
|
||||||
|
handleFilterName: PropTypes.func
|
||||||
|
};
|
86
web/src/views/Payment/index.js
Normal file
86
web/src/views/Payment/index.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tabs, Tab, Box, Card } from '@mui/material';
|
||||||
|
import Gateway from './Gateway';
|
||||||
|
import Order from './Order';
|
||||||
|
import AdminContainer from 'ui-component/AdminContainer';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
function CustomTabPanel(props) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="tabpanel" hidden={value !== index} id={`setting-tabpanel-${index}`} aria-labelledby={`setting-tab-${index}`} {...other}>
|
||||||
|
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTabPanel.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
value: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function a11yProps(index) {
|
||||||
|
return {
|
||||||
|
id: `setting-tab-${index}`,
|
||||||
|
'aria-controls': `setting-tabpanel-${index}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Payment = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const hash = location.hash.replace('#', '');
|
||||||
|
const tabMap = useMemo(
|
||||||
|
() => ({
|
||||||
|
order: 0,
|
||||||
|
gateway: 1
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState(tabMap[hash] || 0);
|
||||||
|
|
||||||
|
const handleChange = (event, newValue) => {
|
||||||
|
setValue(newValue);
|
||||||
|
const hashArray = Object.keys(tabMap);
|
||||||
|
navigate(`#${hashArray[newValue]}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHashChange = () => {
|
||||||
|
const hash = location.hash.replace('#', '');
|
||||||
|
setValue(tabMap[hash] || 0);
|
||||||
|
};
|
||||||
|
window.addEventListener('hashchange', handleHashChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('hashchange', handleHashChange);
|
||||||
|
};
|
||||||
|
}, [location, tabMap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<AdminContainer>
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
|
<Tabs value={value} onChange={handleChange} variant="scrollable" scrollButtons="auto">
|
||||||
|
<Tab label="订单列表" {...a11yProps(0)} />
|
||||||
|
<Tab label="网关设置" {...a11yProps(1)} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<CustomTabPanel value={value} index={0}>
|
||||||
|
<Order />
|
||||||
|
</CustomTabPanel>
|
||||||
|
<CustomTabPanel value={value} index={1}>
|
||||||
|
<Gateway />
|
||||||
|
</CustomTabPanel>
|
||||||
|
</Box>
|
||||||
|
</AdminContainer>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Payment;
|
73
web/src/views/Payment/type/Config.js
Normal file
73
web/src/views/Payment/type/Config.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const PaymentType = {
|
||||||
|
epay: '易支付'
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrencyType = {
|
||||||
|
CNY: '人民币',
|
||||||
|
USD: '美元'
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaymentConfig = {
|
||||||
|
epay: {
|
||||||
|
pay_domain: {
|
||||||
|
name: '支付域名',
|
||||||
|
description: '支付域名',
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
partner_id: {
|
||||||
|
name: '商户号',
|
||||||
|
description: '商户号',
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
name: '密钥',
|
||||||
|
description: '密钥',
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
pay_type: {
|
||||||
|
name: '支付类型',
|
||||||
|
description: '支付类型,如果需要跳转到易支付收银台,请选择收银台',
|
||||||
|
type: 'select',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: '收银台',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支付宝',
|
||||||
|
value: 'alipay'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '微信',
|
||||||
|
value: 'wxpay'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QQ',
|
||||||
|
value: 'qqpay'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '京东',
|
||||||
|
value: 'jdpay'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '银联',
|
||||||
|
value: 'bank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Paypal',
|
||||||
|
value: 'paypal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'USDT',
|
||||||
|
value: 'usdt'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PaymentConfig, PaymentType, CurrencyType };
|
@ -36,7 +36,9 @@ const OperationSetting = () => {
|
|||||||
MjNotifyEnabled: '',
|
MjNotifyEnabled: '',
|
||||||
ChatCacheEnabled: '',
|
ChatCacheEnabled: '',
|
||||||
ChatCacheExpireMinute: 5,
|
ChatCacheExpireMinute: 5,
|
||||||
ChatImageRequestProxy: ''
|
ChatImageRequestProxy: '',
|
||||||
|
PaymentUSDRate: 0,
|
||||||
|
PaymentMinAmount: 1
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@ -178,6 +180,14 @@ const OperationSetting = () => {
|
|||||||
await updateOption('ChatImageRequestProxy', inputs.ChatImageRequestProxy);
|
await updateOption('ChatImageRequestProxy', inputs.ChatImageRequestProxy);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'payment':
|
||||||
|
if (originInputs['PaymentUSDRate'] !== inputs.PaymentUSDRate) {
|
||||||
|
await updateOption('PaymentUSDRate', inputs.PaymentUSDRate);
|
||||||
|
}
|
||||||
|
if (originInputs['PaymentMinAmount'] !== inputs.PaymentMinAmount) {
|
||||||
|
await updateOption('PaymentMinAmount', inputs.PaymentMinAmount);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
showSuccess('保存成功!');
|
showSuccess('保存成功!');
|
||||||
@ -532,6 +542,56 @@ const OperationSetting = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</SubCard>
|
</SubCard>
|
||||||
|
<SubCard title="支付设置">
|
||||||
|
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<Alert severity="info">
|
||||||
|
支付设置: <br />
|
||||||
|
1. 美元汇率:用于计算充值金额的美元金额 <br />
|
||||||
|
2. 最低充值金额(美元):最低充值金额,单位为美元,填写整数 <br />
|
||||||
|
3. 页面都以美元为单位计算,实际用户支付的货币,按照支付网关设置的货币进行转换 <br />
|
||||||
|
例如: A 网关设置货币为 CNY,用户支付 100 美元,那么实际支付金额为 100 * 美元汇率 <br />B 网关设置货币为 USD,用户支付 100
|
||||||
|
美元,那么实际支付金额为 100 美元
|
||||||
|
</Alert>
|
||||||
|
</FormControl>
|
||||||
|
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="PaymentUSDRate">美元汇率</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="PaymentUSDRate"
|
||||||
|
name="PaymentUSDRate"
|
||||||
|
type="number"
|
||||||
|
value={inputs.PaymentUSDRate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
label="美元汇率"
|
||||||
|
placeholder="例如:7.3"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="PaymentMinAmount">最低充值金额(美元)</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="PaymentMinAmount"
|
||||||
|
name="PaymentMinAmount"
|
||||||
|
type="number"
|
||||||
|
value={inputs.PaymentMinAmount}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
label="最低充值金额(美元)"
|
||||||
|
placeholder="例如:1,那么最低充值金额为1美元,请填写整数"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
submitConfig('payment').then();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存支付设置
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</SubCard>
|
||||||
<SubCard title="倍率设置">
|
<SubCard title="倍率设置">
|
||||||
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
|
129
web/src/views/Topup/component/PayDialog.js
Normal file
129
web/src/views/Topup/component/PayDialog.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { QRCode } from 'react-qrcode-logo';
|
||||||
|
import successSvg from 'assets/images/success.svg';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { showError } from 'utils/common';
|
||||||
|
|
||||||
|
const PayDialog = ({ open, onClose, amount, uuid }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const defaultLogo = theme.palette.mode === 'light' ? '/logo-loading.svg' : '/logo-loading-white.svg';
|
||||||
|
const [message, setMessage] = useState('正在拉起支付中...');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [qrCodeUrl, setQrCodeUrl] = useState(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [intervalId, setIntervalId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMessage('正在拉起支付中...');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
API.post('/api/user/order', {
|
||||||
|
uuid: uuid,
|
||||||
|
amount: Number(amount)
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.data.success) {
|
||||||
|
showError(response.data.message);
|
||||||
|
setLoading(false);
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, data } = response.data.data;
|
||||||
|
if (type === 1) {
|
||||||
|
setMessage('等待支付中...');
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = data.method;
|
||||||
|
form.action = data.url;
|
||||||
|
form.target = '_blank';
|
||||||
|
for (const key in data.params) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.name = key;
|
||||||
|
input.value = data.params[key];
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
document.body.removeChild(form);
|
||||||
|
} else if (type === 2) {
|
||||||
|
setQrCodeUrl(data.url);
|
||||||
|
setLoading(false);
|
||||||
|
setMessage('请扫码支付');
|
||||||
|
}
|
||||||
|
pollOrderStatus(response.data.data.trade_no);
|
||||||
|
});
|
||||||
|
}, [open, onClose, amount, uuid]);
|
||||||
|
|
||||||
|
const pollOrderStatus = (tradeNo) => {
|
||||||
|
const id = setInterval(() => {
|
||||||
|
API.get(`/api/user/order/status?trade_no=${tradeNo}`).then((response) => {
|
||||||
|
if (response.data.success) {
|
||||||
|
setMessage('支付成功');
|
||||||
|
setLoading(false);
|
||||||
|
setSuccess(true);
|
||||||
|
clearInterval(id);
|
||||||
|
setIntervalId(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
setIntervalId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} fullWidth maxWidth={'sm'} disableEscapeKeyDown>
|
||||||
|
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>支付</DialogTitle>
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
setIntervalId(null);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: (theme) => theme.palette.grey[500]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack direction="column" justifyContent="center" alignItems="center" spacing={2}>
|
||||||
|
{loading && <img src={defaultLogo} alt="loading" height="100" />}
|
||||||
|
{qrCodeUrl && (
|
||||||
|
<QRCode
|
||||||
|
value={qrCodeUrl}
|
||||||
|
size={256}
|
||||||
|
qrStyle="dots"
|
||||||
|
eyeRadius={20}
|
||||||
|
fgColor={theme.palette.primary.main}
|
||||||
|
bgColor={theme.palette.background.paper}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{success && <img src={successSvg} alt="success" height="100" />}
|
||||||
|
<Typography variant="h3">{message}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PayDialog;
|
||||||
|
|
||||||
|
PayDialog.propTypes = {
|
||||||
|
open: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
amount: PropTypes.number,
|
||||||
|
uuid: PropTypes.string
|
||||||
|
};
|
@ -1,8 +1,24 @@
|
|||||||
import { Typography, Stack, OutlinedInput, InputAdornment, Button, InputLabel, FormControl } from '@mui/material';
|
import {
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
OutlinedInput,
|
||||||
|
InputAdornment,
|
||||||
|
Button,
|
||||||
|
InputLabel,
|
||||||
|
FormControl,
|
||||||
|
useMediaQuery,
|
||||||
|
TextField,
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Divider
|
||||||
|
} from '@mui/material';
|
||||||
import { IconBuildingBank } from '@tabler/icons-react';
|
import { IconBuildingBank } from '@tabler/icons-react';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import SubCard from 'ui-component/cards/SubCard';
|
import SubCard from 'ui-component/cards/SubCard';
|
||||||
import UserCard from 'ui-component/cards/UserCard';
|
import UserCard from 'ui-component/cards/UserCard';
|
||||||
|
import AnimateButton from 'ui-component/extended/AnimateButton';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import PayDialog from './PayDialog';
|
||||||
|
|
||||||
import { API } from 'utils/api';
|
import { API } from 'utils/api';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@ -11,10 +27,17 @@ import { showError, showInfo, showSuccess, renderQuota, trims } from 'utils/comm
|
|||||||
const TopupCard = () => {
|
const TopupCard = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [redemptionCode, setRedemptionCode] = useState('');
|
const [redemptionCode, setRedemptionCode] = useState('');
|
||||||
const [topUpLink, setTopUpLink] = useState('');
|
|
||||||
const [userQuota, setUserQuota] = useState(0);
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [payment, setPayment] = useState([]);
|
||||||
|
const [selectedPayment, setSelectedPayment] = useState(null);
|
||||||
|
const [amount, setAmount] = useState(0);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const siteInfo = useSelector((state) => state.siteInfo);
|
||||||
|
|
||||||
const topUp = async () => {
|
const topUp = async () => {
|
||||||
if (redemptionCode === '') {
|
if (redemptionCode === '') {
|
||||||
showInfo('请输入充值码!');
|
showInfo('请输入充值码!');
|
||||||
@ -42,12 +65,53 @@ const TopupCard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePay = () => {
|
||||||
|
if (!selectedPayment) {
|
||||||
|
showError('请选择支付方式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0 || amount < siteInfo.PaymentMinAmount) {
|
||||||
|
showError('金额不能小于' + siteInfo.PaymentMinAmount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount > 1000000) {
|
||||||
|
showError('金额不能大于1000000');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判读金额是否是正整数
|
||||||
|
if (!/^[1-9]\d*$/.test(amount)) {
|
||||||
|
showError('请输入正整数金额');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPayment = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/user/payment`);
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
data.sort((a, b) => b.sort - a.sort);
|
||||||
|
setPayment(data);
|
||||||
|
setSelectedPayment(data[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openTopUpLink = () => {
|
const openTopUpLink = () => {
|
||||||
if (!topUpLink) {
|
if (!siteInfo.top_up_link) {
|
||||||
showError('超级管理员未设置充值链接!');
|
showError('超级管理员未设置充值链接!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.open(topUpLink, '_blank');
|
window.open(siteInfo.top_up_link, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserQuota = async () => {
|
const getUserQuota = async () => {
|
||||||
@ -64,14 +128,36 @@ const TopupCard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaymentSelect = (payment) => {
|
||||||
|
setSelectedPayment(payment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAmountChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setAmount(value);
|
||||||
|
};
|
||||||
|
const calculateFee = () => {
|
||||||
|
if (!selectedPayment) return 0;
|
||||||
|
|
||||||
|
if (selectedPayment.fixed_fee > 0) {
|
||||||
|
return Number(selectedPayment.fixed_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFloat(selectedPayment.percent_fee * Number(amount)).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotal = () => {
|
||||||
|
if (amount === 0) return 0;
|
||||||
|
|
||||||
|
let total = Number(amount) + Number(calculateFee());
|
||||||
|
if (selectedPayment && selectedPayment.currency === 'CNY') {
|
||||||
|
total = parseFloat((total * 7.3).toFixed(2));
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('siteInfo');
|
getPayment().then();
|
||||||
if (status) {
|
|
||||||
status = JSON.parse(status);
|
|
||||||
if (status.top_up_link) {
|
|
||||||
setTopUpLink(status.top_up_link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getUserQuota().then();
|
getUserQuota().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -82,10 +168,90 @@ const TopupCard = () => {
|
|||||||
<Typography variant="h4">当前额度:</Typography>
|
<Typography variant="h4">当前额度:</Typography>
|
||||||
<Typography variant="h4">{renderQuota(userQuota)}</Typography>
|
<Typography variant="h4">{renderQuota(userQuota)}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{payment.length > 0 && (
|
||||||
<SubCard
|
<SubCard
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: '40px'
|
marginTop: '40px'
|
||||||
}}
|
}}
|
||||||
|
title="在线充值"
|
||||||
|
>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{payment.map((item, index) => (
|
||||||
|
<AnimateButton key={index}>
|
||||||
|
<Button
|
||||||
|
disableElevation
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => handlePaymentSelect(item)}
|
||||||
|
sx={{
|
||||||
|
...theme.typography.LoginButton,
|
||||||
|
border: selectedPayment === item ? `1px solid ${theme.palette.primary.main}` : '1px solid transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<img src={item.icon} alt="github" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
|
||||||
|
</Box>
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
))}
|
||||||
|
<TextField label="金额" type="number" onChange={handleAmountChange} value={amount} />
|
||||||
|
<Divider />
|
||||||
|
<Grid container direction="row" justifyContent="flex-end" spacing={2}>
|
||||||
|
<Grid item xs={9}>
|
||||||
|
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||||
|
充值金额:{' '}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
${Number(amount)}
|
||||||
|
</Grid>
|
||||||
|
{selectedPayment && (selectedPayment.percent_fee > 0 || selectedPayment.fixed_fee > 0) && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={9}>
|
||||||
|
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||||
|
手续费:
|
||||||
|
{selectedPayment &&
|
||||||
|
(selectedPayment.fixed_fee > 0
|
||||||
|
? '(固定)'
|
||||||
|
: selectedPayment.percent_fee > 0
|
||||||
|
? `(${selectedPayment.percent_fee * 100}%)`
|
||||||
|
: '')}{' '}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
${calculateFee()}
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid item xs={9}>
|
||||||
|
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||||
|
实际支付金额:{' '}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
{calculateTotal()}{' '}
|
||||||
|
{selectedPayment &&
|
||||||
|
(selectedPayment.currency === 'CNY' ? `CNY (汇率:${siteInfo.PaymentUSDRate})` : selectedPayment.currency)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Divider />
|
||||||
|
<Button variant="contained" onClick={handlePay}>
|
||||||
|
充值
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<PayDialog open={open} onClose={() => setOpen(false)} amount={amount} uuid={selectedPayment.uuid} />
|
||||||
|
</SubCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SubCard
|
||||||
|
sx={{
|
||||||
|
marginTop: '40px'
|
||||||
|
}}
|
||||||
|
title="兑换码充值"
|
||||||
>
|
>
|
||||||
<FormControl fullWidth variant="outlined">
|
<FormControl fullWidth variant="outlined">
|
||||||
<InputLabel htmlFor="key">兑换码</InputLabel>
|
<InputLabel htmlFor="key">兑换码</InputLabel>
|
||||||
|
@ -6709,6 +6709,11 @@ lodash.debounce@^4.0.8:
|
|||||||
resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||||
|
|
||||||
|
lodash.isequal@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||||
|
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||||
|
|
||||||
lodash.memoize@^4.1.2:
|
lodash.memoize@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||||
@ -8077,6 +8082,11 @@ q@^1.1.2:
|
|||||||
resolved "https://registry.npmmirror.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.npmmirror.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
|
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
|
||||||
|
|
||||||
|
qrcode-generator@^1.4.4:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.npmmirror.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
|
||||||
|
integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==
|
||||||
|
|
||||||
qs@6.11.0:
|
qs@6.11.0:
|
||||||
version "6.11.0"
|
version "6.11.0"
|
||||||
resolved "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
resolved "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||||
@ -8220,6 +8230,14 @@ react-perfect-scrollbar@^1.5.8:
|
|||||||
perfect-scrollbar "^1.5.0"
|
perfect-scrollbar "^1.5.0"
|
||||||
prop-types "^15.6.1"
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
|
react-qrcode-logo@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/react-qrcode-logo/-/react-qrcode-logo-3.0.0.tgz#71cb43ddef9b338cc151800968276ca210086d11"
|
||||||
|
integrity sha512-2+vZ3GNBdUpYxIKyt6SFZsDGXa0xniyUQ0wPI4O0hJTzRjttPIx1pPnH9IWQmp/4nDMoN47IBhi3Breu1KudYw==
|
||||||
|
dependencies:
|
||||||
|
lodash.isequal "^4.5.0"
|
||||||
|
qrcode-generator "^1.4.4"
|
||||||
|
|
||||||
react-redux@^9.1.0:
|
react-redux@^9.1.0:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
resolved "https://registry.npmmirror.com/react-redux/-/react-redux-9.1.0.tgz#46a46d4cfed4e534ce5452bb39ba18e1d98a8197"
|
resolved "https://registry.npmmirror.com/react-redux/-/react-redux-9.1.0.tgz#46a46d4cfed4e534ce5452bb39ba18e1d98a8197"
|
||||||
@ -9000,7 +9018,16 @@ string-natural-compare@^3.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.npmmirror.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
|
string-width@^4.1.0, string-width@^4.2.0:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -9087,7 +9114,14 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@ -10198,7 +10232,16 @@ workbox-window@6.6.1:
|
|||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
workbox-core "6.6.1"
|
workbox-core "6.6.1"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
Loading…
Reference in New Issue
Block a user