Merge pull request #2 from assimon/dev

修复:优化创建交易流程代码,将金额修改为保留3位小数点
This commit is contained in:
ashang
2022-04-04 17:52:24 +08:00
committed by GitHub
10 changed files with 144 additions and 168 deletions
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spf13/viper"
"os"
"time"
)
var (
@@ -53,7 +54,7 @@ func Init() {
}
func GetAppVersion() string {
return "0.0.1"
return "0.0.2"
}
func GetAppName() string {
@@ -90,3 +91,8 @@ func GetOrderExpirationTime() int {
}
return timer
}
func GetOrderExpirationTimeDuration() time.Duration {
timer := GetOrderExpirationTime()
return time.Minute * time.Duration(timer)
}
+2 -2
View File
@@ -7,8 +7,8 @@ require (
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-module/carbon/v2 v2.0.1
github.com/gookit/color v1.5.0
github.com/gookit/goutil v0.4.4
github.com/gookit/validate v1.2.11
github.com/gookit/goutil v0.4.6
github.com/gookit/validate v1.3.1
github.com/hibiken/asynq v0.22.1
github.com/json-iterator/go v1.1.12
github.com/labstack/echo/v4 v4.6.0
+5
View File
@@ -197,8 +197,12 @@ github.com/gookit/filter v1.1.2/go.mod h1:pVXLLDD+A8yH9GRztq2Cp7zwZocnuTUpbZs9Q+
github.com/gookit/goutil v0.3.12/go.mod h1:ITj7Lw0muhJNOX+QRa+j+HH0+RNoQVuTmZx5d5LE1vE=
github.com/gookit/goutil v0.4.4 h1:18xr8NKbs4LteyOHZfQ8g7JqTqZal801mAT6LERGdpE=
github.com/gookit/goutil v0.4.4/go.mod h1:qlGVh0PI+WnWSjYnIocfz/7tkeogxL6+EDNP1mRe+7o=
github.com/gookit/goutil v0.4.6 h1:LSATnmIyR0rV4BMj3XyaKFYtX/HbRURnPVHt6Zb5ABs=
github.com/gookit/goutil v0.4.6/go.mod h1:pq1eTibwb2wN96jrci0xy7xogWzzo9CihOQJEAvz4yQ=
github.com/gookit/validate v1.2.11 h1:zUMsezhMrW3Cy8St3cQJgCKB1ZIbKOWK8e7WMSuVIRc=
github.com/gookit/validate v1.2.11/go.mod h1:wXo0Vr+AzFUCEUCbTFXgKlPfT+V/V0wPK3zLp49jQq0=
github.com/gookit/validate v1.3.1 h1:YoxBxG5H+WucKjfEo8X+QvYOzED7ue3Mfpq6jtpIbYs=
github.com/gookit/validate v1.3.1/go.mod h1:mzcr3U5XtAlQVAgbW0wbmDGiuefU/MNuDkXFMBNb1ko=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
@@ -370,6 +374,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+20 -41
View File
@@ -8,11 +8,11 @@ import (
"github.com/assimon/luuu/model/request"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
"time"
)
var (
CacheWalletAddressToAvailableAmountKey = "WalletAddressToAvailableAmountKey:" // 钱包 - 可用金额 - 过期时间
CacheWalletAddressToOrdersKey = "WalletAddressToOrdersKey:" // 钱包 - 待支付金额 - 订单号
CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%v" // 钱包_待支付金额 : 交易号
)
// GetOrderInfoByOrderId 通过客户订单号查询订单
@@ -78,36 +78,11 @@ func UpdateOrderIsExpirationById(id uint64) error {
return err
}
// LockPayCache 锁定支付池
func LockPayCache(token, tradeId, amount string, expirationTime int64) error {
// 载入Redis
// GetTradeIdByWalletAddressAndAmount 通过钱包地址,支付金额获取交易号
func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, error) {
ctx := context.Background()
pipe := dao.Rdb.TxPipeline()
lockAvailableAmountKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
// 占用金额
pipe.HSet(ctx, lockAvailableAmountKey, amount, expirationTime)
// 标记金额对应订单号
lockWalletOrdersKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
pipe.HSet(ctx, lockWalletOrdersKey, amount, tradeId)
_, err := pipe.Exec(ctx)
return err
}
// ClearPayCache 清理支付池
func ClearPayCache(token, amount string) error {
ctx := context.Background()
lockAvailableAmountKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
lockWalletOrdersKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
pipe := dao.Rdb.TxPipeline()
pipe.HDel(ctx, lockAvailableAmountKey, amount)
pipe.HDel(ctx, lockWalletOrdersKey, amount)
_, err := pipe.Exec(ctx)
return err
}
func GetTradeIdByAmount(ctx context.Context, token, amount string) (string, error) {
cacheKey := fmt.Sprintf("%s%s", CacheWalletAddressToOrdersKey, token)
result, err := dao.Rdb.HGet(ctx, cacheKey, amount).Result()
cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount)
result, err := dao.Rdb.Get(ctx, cacheKey).Result()
if err == redis.Nil {
return "", nil
}
@@ -117,14 +92,18 @@ func GetTradeIdByAmount(ctx context.Context, token, amount string) (string, erro
return result, nil
}
func GetExpirationTimeByAmount(ctx context.Context, token, amount string) (string, error) {
cacheKey := fmt.Sprintf("%s%s", CacheWalletAddressToAvailableAmountKey, token)
result, err := dao.Rdb.HGet(ctx, cacheKey, amount).Result()
if err == redis.Nil {
return "", nil
}
if err != nil {
return "", err
}
return result, nil
// LockTransaction 锁定交易
func LockTransaction(token, tradeId string, amount float64, expirationTime time.Duration) error {
ctx := context.Background()
cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount)
err := dao.Rdb.Set(ctx, cacheKey, tradeId, expirationTime).Err()
return err
}
// UnLockTransaction 解锁交易
func UnLockTransaction(token string, amount float64) error {
ctx := context.Background()
cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount)
err := dao.Rdb.Del(ctx, cacheKey).Err()
return err
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (r CreateTransactionRequest) Translates() map[string]string {
// OrderProcessingRequest 订单处理
type OrderProcessingRequest struct {
Token string
Amount string
Amount float64
TradeId string
BlockTransactionId string
}
+56 -92
View File
@@ -1,7 +1,6 @@
package service
import (
"context"
"fmt"
"github.com/assimon/luuu/config"
"github.com/assimon/luuu/model/dao"
@@ -11,35 +10,40 @@ import (
"github.com/assimon/luuu/model/response"
"github.com/assimon/luuu/mq"
"github.com/assimon/luuu/mq/handle"
"github.com/assimon/luuu/telegram"
"github.com/assimon/luuu/util/constant"
"github.com/assimon/luuu/util/math"
"github.com/golang-module/carbon/v2"
"github.com/gookit/goutil/mathutil"
"github.com/hibiken/asynq"
"github.com/shopspring/decimal"
"math/rand"
"strconv"
"sync"
"time"
)
const (
CnyMinimumPaymentAmount = 0.01 // cny最低支付金额
UsdtMinimumPaymentAmount = 0.001 // usdt最低支付金额
UsdtAmountPerIncrement = 0.001 // usdt每次递增金额
IncrementalMaximumNumber = 100 // 最大递增次数
)
var gCreateTransactionLock sync.Mutex
// CreateTransaction 创建订单
func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateTransactionResponse, error) {
gCreateTransactionLock.Lock()
defer gCreateTransactionLock.Unlock()
// 汇率计算金额
rmb := decimal.NewFromFloat(req.Amount)
rate := decimal.NewFromFloat(config.GetUsdtRate())
amount := rmb.Div(rate).InexactFloat64()
actualAmountStr := fmt.Sprintf("%.4f", amount)
actualAmountFloat, err := strconv.ParseFloat(actualAmountStr, 64)
if err != nil {
return nil, err
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
// 按照汇率转化USDT
decimalPayAmount := decimal.NewFromFloat(payAmount)
decimalRate := decimal.NewFromFloat(config.GetUsdtRate())
decimalUsdt := decimalPayAmount.Div(decimalRate)
// cny 是否可以满足最低支付金额
if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
// 是否可以满足最低支付金额
if actualAmountFloat <= 0 {
// Usdt是否可以满足最低支付金额
if decimalUsdt.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
return nil, constant.PayAmountErr
}
// 已经存在了的交易
@@ -58,23 +62,20 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
if len(walletAddress) <= 0 {
return nil, constant.NotAvailableWalletAddress
}
availableToken, availableAmountStr, err := CalculateAvailableWalletTokenAndAmount(actualAmountStr, walletAddress)
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 3)
availableToken, availableAmount, err := CalculateAvailableWalletAndAmount(amount, walletAddress)
if err != nil {
return nil, err
}
if availableToken == "" || availableAmountStr == "" {
if availableToken == "" {
return nil, constant.NotAvailableAmountErr
}
availableAmountFloat, err := strconv.ParseFloat(availableAmountStr, 64)
if err != nil {
return nil, err
}
tx := dao.Mdb.Begin()
order := &mdb.Orders{
TradeId: GenerateCode(),
OrderId: req.OrderId,
Amount: req.Amount,
ActualAmount: availableAmountFloat,
ActualAmount: availableAmount,
Token: availableToken,
Status: mdb.StatusWaitPay,
NotifyUrl: req.NotifyUrl,
@@ -85,9 +86,8 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
tx.Rollback()
return nil, err
}
ExpirationTime := carbon.Now().AddMinutes(config.GetOrderExpirationTime()).Timestamp()
// 锁定支付池
err = data.LockPayCache(availableToken, order.TradeId, availableAmountStr, ExpirationTime)
err = data.LockTransaction(availableToken, order.TradeId, availableAmount, config.GetOrderExpirationTimeDuration())
if err != nil {
tx.Rollback()
return nil, err
@@ -95,7 +95,8 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
tx.Commit()
// 超时过期消息队列
orderExpirationQueue, _ := handle.NewOrderExpirationQueue(order.TradeId)
mq.MClient.Enqueue(orderExpirationQueue, asynq.ProcessIn(time.Minute*time.Duration(config.GetOrderExpirationTime())))
mq.MClient.Enqueue(orderExpirationQueue, asynq.ProcessIn(config.GetOrderExpirationTimeDuration()))
ExpirationTime := carbon.Now().AddMinutes(config.GetOrderExpirationTime()).Timestamp()
resp := &response.CreateTransactionResponse{
TradeId: order.TradeId,
OrderId: order.OrderId,
@@ -125,88 +126,51 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
tx.Rollback()
return err
}
err = data.ClearPayCache(req.Token, req.Amount)
tx.Commit()
order, err := data.GetOrderInfoByTradeId(req.TradeId)
// 解锁交易
err = data.UnLockTransaction(req.Token, req.Amount)
if err != nil {
tx.Rollback()
return err
}
// 回调队列
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(5))
// 发送机器人消息
msgTpl := `
<b>📢📢有新的交易支付成功!</b>
<pre>交易号:%s</pre>
<pre>订单号:%s</pre>
<pre>请求支付金额:%.4f cny</pre>
<pre>实际支付金额:%.4f usdt</pre>
<pre>钱包地址:%s</pre>
<pre>订单创建时间:%s</pre>
<pre>支付成功时间:%s</pre>
`
msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString())
telegram.SendToBot(msg)
tx.Commit()
return nil
}
func CalculateAvailableWalletTokenAndAmount(amount string, walletAddress []mdb.WalletAddress) (string, string, error) {
calculateAmountStr := amount
availableAmountStr := ""
// CalculateAvailableWalletAndAmount 计算可用钱包地址和金额
func CalculateAvailableWalletAndAmount(amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
availableToken := ""
for i := 0; i < 100; i++ {
token, err := CalculateAvailableWalletToken(calculateAmountStr, walletAddress)
if err != nil {
return "", "", err
availableAmount := amount
calculateAvailableWalletFunc := func(amount float64) (string, error) {
availableWallet := ""
for _, address := range walletAddress {
token := address.Token
result, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
if err != nil {
return "", err
}
if result == "" {
availableWallet = token
break
}
}
// 这个金额没有拿到可用的钱包,重试,金额+0.0001
return availableWallet, nil
}
for i := 0; i < IncrementalMaximumNumber; i++ {
token, err := calculateAvailableWalletFunc(availableAmount)
if err != nil {
return "", 0, err
}
// 拿不到可用钱包就累加金额
if token == "" {
x, err := decimal.NewFromString(calculateAmountStr)
if err != nil {
return "", "", err
}
y, err := decimal.NewFromString("0.0001")
if err != nil {
return "", "", err
}
calculateAmountStr = x.Add(y).String()
decimalOldAmount := decimal.NewFromFloat(availableAmount)
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
continue
}
availableAmountStr = calculateAmountStr
availableToken = token
break
}
return availableToken, availableAmountStr, nil
}
// CalculateAvailableWalletToken 计算可用钱包token
func CalculateAvailableWalletToken(payAmount string, walletAddress []mdb.WalletAddress) (string, error) {
nowTime := time.Now().Unix()
ctx := context.Background()
walletToken := ""
for _, address := range walletAddress {
result, err := data.GetExpirationTimeByAmount(ctx, address.Token, payAmount)
if err != nil {
return "", err
}
// 这个钱包金额被占用了
if result != "" {
endTime := mathutil.MustInt64(result)
// 但是过期了
if endTime < nowTime {
// 删掉过期,还能继续用这个地址
err = data.ClearPayCache(address.Token, payAmount)
if err != nil {
return "", err
}
} else {
continue
}
}
walletToken = address.Token
break
}
return walletToken, nil
return availableToken, availableAmount, nil
}
// GenerateCode 订单号生成
+34 -22
View File
@@ -1,20 +1,21 @@
package service
import (
"context"
"fmt"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/request"
"github.com/assimon/luuu/mq"
"github.com/assimon/luuu/mq/handle"
"github.com/assimon/luuu/telegram"
"github.com/assimon/luuu/util/http_client"
"github.com/assimon/luuu/util/json"
"github.com/assimon/luuu/util/log"
"github.com/golang-module/carbon/v2"
"github.com/gookit/goutil/mathutil"
"github.com/gookit/goutil/stdutil"
"github.com/hibiken/asynq"
"github.com/shopspring/decimal"
"net/http"
"sync"
"time"
)
const UsdtTrc20ApiUri = "https://apilist.tronscan.org/api/token_trc20/transfers"
@@ -90,38 +91,33 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
if trc20Resp.Total <= 0 {
return
}
ctx := context.Background()
nowTime := time.Now().Unix()
for _, transfer := range trc20Resp.TokenTransfers {
if transfer.ToAddress != token || transfer.FinalResult != "SUCCESS" {
continue
}
x, _ := decimal.NewFromString(transfer.Quant)
y, _ := decimal.NewFromString("1000000")
quant := x.Div(y).InexactFloat64()
amount := fmt.Sprintf("%.4f", quant)
result, err := data.GetExpirationTimeByAmount(ctx, token, amount)
decimalQuant, err := decimal.NewFromString(transfer.Quant)
if err != nil {
panic(err)
}
if result != "" {
expTime := mathutil.MustInt64(result)
// 但是过期了
if expTime < nowTime {
// 删掉过期
_ = data.ClearPayCache(token, amount)
continue
}
}
// 该钱包下有无匹配金额订单
tradeId, err := data.GetTradeIdByAmount(ctx, token, amount)
decimalDivisor := decimal.NewFromFloat(1000000)
amount := decimalQuant.Div(decimalDivisor).InexactFloat64()
tradeId, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
if err != nil {
panic(err)
}
if tradeId == "" {
continue
}
// 到这一步就匹配到金额了
order, err := data.GetOrderInfoByTradeId(tradeId)
if err != nil {
panic(err)
}
// 区块的确认时间必须在订单创建时间之后
createTime := order.CreatedAt.TimestampWithMillisecond()
if transfer.BlockTs < createTime {
panic("Orders cannot actually be matched")
}
// 到这一步就完全算是支付成功了
req := &request.OrderProcessingRequest{
Token: token,
TradeId: tradeId,
@@ -132,5 +128,21 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
if err != nil {
panic(err)
}
// 回调队列
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(5))
// 发送机器人消息
msgTpl := `
<b>📢📢有新的交易支付成功!</b>
<pre>交易号:%s</pre>
<pre>订单号:%s</pre>
<pre>请求支付金额:%f cny</pre>
<pre>实际支付金额:%f usdt</pre>
<pre>钱包地址:%s</pre>
<pre>订单创建时间:%s</pre>
<pre>支付成功时间:%s</pre>
`
msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString())
telegram.SendToBot(msg)
}
}
+1 -3
View File
@@ -2,7 +2,6 @@ package handle
import (
"context"
"fmt"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/mdb"
"github.com/hibiken/asynq"
@@ -28,8 +27,7 @@ func OrderExpirationHandle(ctx context.Context, t *asynq.Task) error {
if err != nil {
return err
}
actualAmount := fmt.Sprintf("%.4f", orderInfo.ActualAmount)
err = data.ClearPayCache(orderInfo.Token, actualAmount)
err = data.UnLockTransaction(orderInfo.Token, orderInfo.ActualAmount)
if err != nil {
return err
}
+2 -6
View File
@@ -1,12 +1,11 @@
package task
import (
"fmt"
"github.com/assimon/luuu/config"
"github.com/assimon/luuu/util/http_client"
"github.com/assimon/luuu/util/json"
"github.com/assimon/luuu/util/log"
"strconv"
"github.com/assimon/luuu/util/math"
"time"
)
@@ -56,10 +55,7 @@ func (r UsdtRateJob) Run() {
}
for _, points := range usdtResp.Data.Points {
if len(points.C) > 0 && points.C[0] > 0 {
config.UsdtRate, err = strconv.ParseFloat(fmt.Sprintf("%.2f", points.C[0]), 64)
if err != nil {
log.Sugar.Error("Calculate UsdtRate err:", err.Error())
}
config.UsdtRate = math.MustParsePrecFloat64(points.C[0], 2)
return
}
}
+16
View File
@@ -0,0 +1,16 @@
package math
import (
"fmt"
"strconv"
)
// MustParsePrecFloat64 按小数位截取float
func MustParsePrecFloat64(value float64, prec int) float64 {
format := fmt.Sprintf("%%.%df", prec)
v, err := strconv.ParseFloat(fmt.Sprintf(format, value), 64)
if err != nil {
return 0
}
return v
}