refactor: replace redis runtime and queues with sqlite-backed scheduler
This commit is contained in:
@@ -9,7 +9,7 @@ func Init() {
|
||||
log.Fatalf("DBInit err: %v", err)
|
||||
}
|
||||
|
||||
if err := RedisInit(); err != nil {
|
||||
log.Fatalf("RedisInit err: %v", err)
|
||||
if err := RuntimeInit(); err != nil {
|
||||
log.Fatalf("RuntimeInit err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Rdb *redis.Client
|
||||
|
||||
func RedisInit() error {
|
||||
options := redis.Options{
|
||||
Addr: fmt.Sprintf(
|
||||
"%s:%s",
|
||||
viper.GetString("redis_host"),
|
||||
viper.GetString("redis_port")), // Redis地址
|
||||
DB: viper.GetInt("redis_db"), // Redis库
|
||||
PoolSize: viper.GetInt("redis_pool_size"), // Redis连接池大小
|
||||
MaxRetries: viper.GetInt("redis_max_retries"), // 最大重试次数
|
||||
IdleTimeout: time.Second * time.Duration(viper.GetInt("redis_idle_timeout")), // 空闲链接超时时间
|
||||
}
|
||||
if viper.GetString("redis_passwd") != "" {
|
||||
options.Password = viper.GetString("redis_passwd")
|
||||
}
|
||||
Rdb = redis.NewClient(&options)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
pong, err := Rdb.Ping(ctx).Result()
|
||||
if err == redis.Nil {
|
||||
log.Sugar.Debug("[store_redis] Nil reply returned by Rdb when key does not exist.")
|
||||
} else if err != nil {
|
||||
color.Red.Printf("[store_redis] redis connRdb err,err=%s", err)
|
||||
// panic(err)
|
||||
// time.Sleep(10 * time.Second)
|
||||
// RedisInit()
|
||||
return err
|
||||
} else {
|
||||
log.Sugar.Debug("[store_redis] redis connRdb success,suc=%s", pong)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/gookit/color"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
var RuntimeDB *gorm.DB
|
||||
|
||||
func RuntimeInit() error {
|
||||
var err error
|
||||
runtimePath := config.GetRuntimeSqlitePath()
|
||||
color.Green.Printf("[runtime_db] sqlite filename: %s\n", runtimePath)
|
||||
RuntimeDB, err = openDB(runtimePath, &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true,
|
||||
},
|
||||
Logger: logger.Default.LogMode(logger.Error),
|
||||
})
|
||||
if err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite open DB,err=%s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDB, err := RuntimeDB.DB()
|
||||
if err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite get DB,err=%s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
concurrency := config.GetQueueConcurrency()
|
||||
if concurrency < 2 {
|
||||
concurrency = 2
|
||||
}
|
||||
if concurrency > 16 {
|
||||
concurrency = 16
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(concurrency)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
|
||||
if err = RuntimeDB.Exec("PRAGMA journal_mode=WAL;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.Exec("PRAGMA synchronous=NORMAL;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.Exec("PRAGMA busy_timeout=5000;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = sqlDB.Ping(); err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite connDB err:%s", err.Error())
|
||||
return err
|
||||
}
|
||||
if err = RuntimeDB.AutoMigrate(&mdb.TransactionLock{}); err != nil {
|
||||
color.Red.Printf("[runtime_db] sqlite migrate DB(TransactionLock),err=%s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Sugar.Debug("[runtime_db] sqlite connDB success")
|
||||
return nil
|
||||
}
|
||||
+107
-54
@@ -1,109 +1,162 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%v" // 钱包_待支付金额 : 交易号
|
||||
)
|
||||
var ErrTransactionLocked = errors.New("transaction amount is already locked")
|
||||
|
||||
// GetOrderInfoByOrderId 通过客户订单号查询订单
|
||||
func normalizeLockAmount(amount float64) (int64, string) {
|
||||
value := decimal.NewFromFloat(amount).Round(2)
|
||||
return value.Shift(2).IntPart(), value.StringFixed(2)
|
||||
}
|
||||
|
||||
// GetOrderInfoByOrderId fetches an order by merchant order id.
|
||||
func GetOrderInfoByOrderId(orderId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).Limit(1).Find(order, "order_id = ?", orderId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// GetOrderInfoByTradeId 通过交易号查询订单
|
||||
// GetOrderInfoByTradeId fetches an order by epusdt trade id.
|
||||
func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).Limit(1).Find(order, "trade_id = ?", tradeId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// CreateOrderWithTransaction 事务创建订单
|
||||
// CreateOrderWithTransaction creates an order in the active database transaction.
|
||||
func CreateOrderWithTransaction(tx *gorm.DB, order *mdb.Orders) error {
|
||||
err := tx.Model(order).Create(order).Error
|
||||
return err
|
||||
return tx.Model(order).Create(order).Error
|
||||
}
|
||||
|
||||
// GetOrderByBlockIdWithTransaction 通过区块获取订单
|
||||
// GetOrderByBlockIdWithTransaction fetches an order by blockchain tx id.
|
||||
func GetOrderByBlockIdWithTransaction(tx *gorm.DB, blockId string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := tx.Model(order).Limit(1).Find(order, "block_transaction_id = ?", blockId).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// OrderSuccessWithTransaction 事务支付成功
|
||||
func OrderSuccessWithTransaction(tx *gorm.DB, req *request.OrderProcessingRequest) error {
|
||||
err := tx.Model(&mdb.Orders{}).Where("trade_id = ?", req.TradeId).Updates(map[string]interface{}{
|
||||
"block_transaction_id": req.BlockTransactionId,
|
||||
"status": mdb.StatusPaySuccess,
|
||||
"callback_confirm": mdb.CallBackConfirmNo,
|
||||
}).Error
|
||||
return err
|
||||
// OrderSuccessWithTransaction marks an order as paid only if it is still waiting for payment.
|
||||
func OrderSuccessWithTransaction(tx *gorm.DB, req *request.OrderProcessingRequest) (bool, error) {
|
||||
result := tx.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", req.TradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Updates(map[string]interface{}{
|
||||
"block_transaction_id": req.BlockTransactionId,
|
||||
"status": mdb.StatusPaySuccess,
|
||||
"callback_confirm": mdb.CallBackConfirmNo,
|
||||
})
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// GetPendingCallbackOrders 查询出等待回调的订单
|
||||
func GetPendingCallbackOrders() ([]mdb.Orders, error) {
|
||||
// GetPendingCallbackOrders returns orders that still need callback delivery.
|
||||
func GetPendingCallbackOrders(maxRetry int, limit int) ([]mdb.Orders, error) {
|
||||
var orders []mdb.Orders
|
||||
err := dao.Mdb.Model(orders).
|
||||
Where("callback_num < ?", 5).
|
||||
query := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("callback_num <= ?", maxRetry).
|
||||
Where("callback_confirm = ?", mdb.CallBackConfirmNo).
|
||||
Where("status = ?", mdb.StatusPaySuccess).
|
||||
Find(&orders).Error
|
||||
Order("updated_at asc")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
err := query.Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// SaveCallBackOrdersResp 保存订单回调结果
|
||||
// SaveCallBackOrdersResp persists a callback attempt result.
|
||||
func SaveCallBackOrdersResp(order *mdb.Orders) error {
|
||||
err := dao.Mdb.Model(order).Where("id = ?", order.ID).Updates(map[string]interface{}{
|
||||
"callback_num": gorm.Expr("callback_num + ?", 1),
|
||||
"callback_confirm": order.CallBackConfirm,
|
||||
}).Error
|
||||
return err
|
||||
return dao.Mdb.Model(order).
|
||||
Where("id = ?", order.ID).
|
||||
Where("callback_confirm = ?", mdb.CallBackConfirmNo).
|
||||
Updates(map[string]interface{}{
|
||||
"callback_num": gorm.Expr("callback_num + ?", 1),
|
||||
"callback_confirm": order.CallBackConfirm,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateOrderIsExpirationById 通过id设置订单过期
|
||||
func UpdateOrderIsExpirationById(id uint64) error {
|
||||
err := dao.Mdb.Model(mdb.Orders{}).Where("id = ?", id).Update("status", mdb.StatusExpired).Error
|
||||
return err
|
||||
// UpdateOrderIsExpirationById expires an order only if it is still pending and already timed out.
|
||||
func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, error) {
|
||||
result := dao.Mdb.Model(mdb.Orders{}).
|
||||
Where("id = ?", id).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Where("created_at <= ?", expirationCutoff).
|
||||
Update("status", mdb.StatusExpired)
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// GetTradeIdByWalletAddressAndAmount 通过钱包地址,支付金额获取交易号
|
||||
// GetTradeIdByWalletAddressAndAmount resolves the reserved trade id by token and amount.
|
||||
func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, error) {
|
||||
ctx := context.Background()
|
||||
cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount)
|
||||
result, err := dao.Rdb.Get(ctx, cacheKey).Result()
|
||||
if err == redis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
var lock mdb.TransactionLock
|
||||
err := dao.RuntimeDB.Model(&mdb.TransactionLock{}).
|
||||
Where("token = ?", token).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at > ?", time.Now()).
|
||||
Limit(1).
|
||||
Find(&lock).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
if lock.ID <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
return lock.TradeId, nil
|
||||
}
|
||||
|
||||
// LockTransaction 锁定交易
|
||||
// LockTransaction reserves a token+amount pair in sqlite until expiration.
|
||||
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
|
||||
scaledAmount, amountText := normalizeLockAmount(amount)
|
||||
now := time.Now()
|
||||
lock := &mdb.TransactionLock{
|
||||
Token: token,
|
||||
AmountScaled: scaledAmount,
|
||||
AmountText: amountText,
|
||||
TradeId: tradeId,
|
||||
ExpiresAt: now.Add(expirationTime),
|
||||
}
|
||||
|
||||
return dao.RuntimeDB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("token = ?", token).
|
||||
Where("amount_scaled = ?", scaledAmount).
|
||||
Where("expires_at <= ?", now).
|
||||
Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("trade_id = ?", tradeId).Delete(&mdb.TransactionLock{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(lock)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrTransactionLocked
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UnLockTransaction 解锁交易
|
||||
// UnLockTransaction releases the reservation for token+amount.
|
||||
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
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
return dao.RuntimeDB.Where("token = ?", token).Where("amount_scaled = ?", scaledAmount).Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
func UnLockTransactionByTradeId(tradeId string) error {
|
||||
return dao.RuntimeDB.Where("trade_id = ?", tradeId).Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
func CleanupExpiredTransactionLocks() error {
|
||||
return dao.RuntimeDB.Where("expires_at <= ?", time.Now()).Delete(&mdb.TransactionLock{}).Error
|
||||
}
|
||||
|
||||
+11
-12
@@ -9,21 +9,20 @@ const (
|
||||
)
|
||||
|
||||
type Orders struct {
|
||||
TradeId string `gorm:"column:trade_id" json:"trade_id"` // epusdt订单号
|
||||
OrderId string `gorm:"column:order_id" json:"order_id"` // 客户交易id
|
||||
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id"` // 区块id
|
||||
Amount float64 `gorm:"column:amount" json:"amount"` // 订单金额,保留4位小数
|
||||
ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
||||
Token string `gorm:"column:token" json:"token"` // 所属钱包地址
|
||||
Status int `gorm:"column:status;default:1" json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"` // 异步回调地址
|
||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"` // 同步回调地址
|
||||
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"` // 回调次数
|
||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"` // 回调是否已确认 1是 2否
|
||||
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id"`
|
||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id"`
|
||||
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id"`
|
||||
Amount float64 `gorm:"column:amount" json:"amount"`
|
||||
ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"`
|
||||
Token string `gorm:"column:token" json:"token"`
|
||||
Status int `gorm:"column:status;default:1" json:"status"`
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"`
|
||||
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"`
|
||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"`
|
||||
BaseModel
|
||||
}
|
||||
|
||||
// TableName sets the insert table name for this struct type
|
||||
func (o *Orders) TableName() string {
|
||||
return "orders"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package mdb
|
||||
|
||||
import "time"
|
||||
|
||||
type TransactionLock struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Token string `gorm:"column:token;uniqueIndex:transaction_lock_token_amount_uindex,priority:1" json:"token"`
|
||||
AmountScaled int64 `gorm:"column:amount_scaled;uniqueIndex:transaction_lock_token_amount_uindex,priority:2" json:"amount_scaled"`
|
||||
AmountText string `gorm:"column:amount_text" json:"amount_text"`
|
||||
TradeId string `gorm:"column:trade_id;index:transaction_lock_trade_id_index" json:"trade_id"`
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;index:transaction_lock_expires_at_index" json:"expires_at"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (t *TransactionLock) TableName() string {
|
||||
return "transaction_lock"
|
||||
}
|
||||
@@ -5,14 +5,12 @@ const (
|
||||
TokenStatusDisable = 2
|
||||
)
|
||||
|
||||
// WalletAddress 钱包表
|
||||
type WalletAddress struct {
|
||||
Token string `gorm:"index:wallet_address_token_index;column:token" json:"token"` // 钱包token
|
||||
Status int64 `gorm:"column:status;default:1" json:"status"` // 1:启用 2:禁用
|
||||
Token string `gorm:"column:token;uniqueIndex:wallet_address_token_uindex" json:"token"`
|
||||
Status int64 `gorm:"column:status;default:1" json:"status"`
|
||||
BaseModel
|
||||
}
|
||||
|
||||
// TableName sets the insert table name for this struct type
|
||||
func (w *WalletAddress) TableName() string {
|
||||
return "wallet_address"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
@@ -12,42 +13,39 @@ import (
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/assimon/luuu/model/response"
|
||||
"github.com/assimon/luuu/mq"
|
||||
"github.com/assimon/luuu/mq/handle"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/assimon/luuu/util/math"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
const (
|
||||
CnyMinimumPaymentAmount = 0.01 // cny最低支付金额
|
||||
UsdtMinimumPaymentAmount = 0.01 // usdt最低支付金额
|
||||
UsdtAmountPerIncrement = 0.01 // usdt每次递增金额
|
||||
IncrementalMaximumNumber = 100 // 最大递增次数
|
||||
CnyMinimumPaymentAmount = 0.01
|
||||
UsdtMinimumPaymentAmount = 0.01
|
||||
UsdtAmountPerIncrement = 0.01
|
||||
IncrementalMaximumNumber = 100
|
||||
)
|
||||
|
||||
var gCreateTransactionLock sync.Mutex
|
||||
var gOrderProcessingLock sync.Mutex
|
||||
|
||||
// CreateTransaction 创建订单
|
||||
// CreateTransaction creates a new payment order.
|
||||
func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateTransactionResponse, error) {
|
||||
gCreateTransactionLock.Lock()
|
||||
defer gCreateTransactionLock.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
// Usdt是否可以满足最低支付金额
|
||||
if decimalUsdt.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
// 已经存在了的交易
|
||||
|
||||
exist, err := data.GetOrderInfoByOrderId(req.OrderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -55,7 +53,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
if exist.ID > 0 {
|
||||
return nil, constant.OrderAlreadyExists
|
||||
}
|
||||
// 有无可用钱包
|
||||
|
||||
walletAddress, err := data.GetAvailableWalletAddress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,17 +61,20 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
if len(walletAddress) <= 0 {
|
||||
return nil, constant.NotAvailableWalletAddress
|
||||
}
|
||||
|
||||
tradeId := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2)
|
||||
availableToken, availableAmount, err := CalculateAvailableWalletAndAmount(amount, walletAddress)
|
||||
availableToken, availableAmount, err := ReserveAvailableWalletAndAmount(tradeId, amount, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if availableToken == "" {
|
||||
return nil, constant.NotAvailableAmountErr
|
||||
}
|
||||
|
||||
tx := dao.Mdb.Begin()
|
||||
order := &mdb.Orders{
|
||||
TradeId: GenerateCode(),
|
||||
TradeId: tradeId,
|
||||
OrderId: req.OrderId,
|
||||
Amount: req.Amount,
|
||||
ActualAmount: availableAmount,
|
||||
@@ -82,88 +83,90 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
NotifyUrl: req.NotifyUrl,
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
}
|
||||
err = data.CreateOrderWithTransaction(tx, order)
|
||||
if err != nil {
|
||||
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransaction(availableToken, availableAmount)
|
||||
return nil, err
|
||||
}
|
||||
// 锁定支付池
|
||||
err = data.LockTransaction(availableToken, order.TradeId, availableAmount, config.GetOrderExpirationTimeDuration())
|
||||
if err != nil {
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransaction(availableToken, availableAmount)
|
||||
return nil, err
|
||||
}
|
||||
tx.Commit()
|
||||
// 超时过期消息队列
|
||||
orderExpirationQueue, _ := handle.NewOrderExpirationQueue(order.TradeId)
|
||||
mq.MClient.Enqueue(orderExpirationQueue, asynq.ProcessIn(config.GetOrderExpirationTimeDuration()),
|
||||
asynq.Retention(config.GetOrderExpirationTimeDuration()),
|
||||
)
|
||||
ExpirationTime := carbon.Now().AddMinutes(config.GetOrderExpirationTime()).Timestamp()
|
||||
|
||||
expirationTime := carbon.Now().AddMinutes(config.GetOrderExpirationTime()).Timestamp()
|
||||
resp := &response.CreateTransactionResponse{
|
||||
TradeId: order.TradeId,
|
||||
OrderId: order.OrderId,
|
||||
Amount: order.Amount,
|
||||
ActualAmount: order.ActualAmount,
|
||||
Token: order.Token,
|
||||
ExpirationTime: ExpirationTime,
|
||||
ExpirationTime: expirationTime,
|
||||
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// OrderProcessing 成功处理订单
|
||||
// OrderProcessing marks an order as paid and releases its sqlite reservation.
|
||||
func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
gOrderProcessingLock.Lock()
|
||||
defer gOrderProcessingLock.Unlock()
|
||||
|
||||
tx := dao.Mdb.Begin()
|
||||
exist, err := data.GetOrderByBlockIdWithTransaction(tx, req.BlockTransactionId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if exist.ID > 0 {
|
||||
tx.Rollback()
|
||||
return constant.OrderBlockAlreadyProcess
|
||||
}
|
||||
// 标记订单成功
|
||||
err = data.OrderSuccessWithTransaction(tx, req)
|
||||
|
||||
updated, err := data.OrderSuccessWithTransaction(tx, req)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
// 解锁交易
|
||||
err = data.UnLockTransaction(req.Token, req.Amount)
|
||||
if err != nil {
|
||||
if !updated {
|
||||
tx.Rollback()
|
||||
return constant.OrderStatusConflict
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
if err = data.UnLockTransaction(req.Token, req.Amount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock transaction after pay success failed, trade_id=%s, err=%v", req.TradeId, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalculateAvailableWalletAndAmount 计算可用钱包地址和金额
|
||||
func CalculateAvailableWalletAndAmount(amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||
// ReserveAvailableWalletAndAmount finds and locks a token+amount pair.
|
||||
func ReserveAvailableWalletAndAmount(tradeId string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||
availableToken := ""
|
||||
availableAmount := amount
|
||||
calculateAvailableWalletFunc := func(amount float64) (string, error) {
|
||||
availableWallet := ""
|
||||
|
||||
tryLockWalletFunc := func(targetAmount float64) (string, error) {
|
||||
for _, address := range walletAddress {
|
||||
token := address.Token
|
||||
result, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
err := data.LockTransaction(address.Token, tradeId, targetAmount, config.GetOrderExpirationTimeDuration())
|
||||
if err == nil {
|
||||
return address.Token, nil
|
||||
}
|
||||
if result == "" {
|
||||
availableWallet = token
|
||||
break
|
||||
if errors.Is(err, data.ErrTransactionLocked) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return availableWallet, nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
for i := 0; i < IncrementalMaximumNumber; i++ {
|
||||
token, err := calculateAvailableWalletFunc(availableAmount)
|
||||
token, err := tryLockWalletFunc(availableAmount)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
// 拿不到可用钱包就累加金额
|
||||
if token == "" {
|
||||
decimalOldAmount := decimal.NewFromFloat(availableAmount)
|
||||
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
|
||||
@@ -176,15 +179,14 @@ func CalculateAvailableWalletAndAmount(amount float64, walletAddress []mdb.Walle
|
||||
return availableToken, availableAmount, nil
|
||||
}
|
||||
|
||||
// GenerateCode 订单号生成
|
||||
// GenerateCode creates a unique trade id.
|
||||
func GenerateCode() string {
|
||||
date := time.Now().Format("20060102")
|
||||
r := rand.Intn(1000)
|
||||
code := fmt.Sprintf("%s%d%03d", date, time.Now().UnixNano()/1e6, r)
|
||||
return code
|
||||
return fmt.Sprintf("%s%d%03d", date, time.Now().UnixNano()/1e6, r)
|
||||
}
|
||||
|
||||
// GetOrderInfoByTradeId 通过交易号获取订单
|
||||
// GetOrderInfoByTradeId returns a validated order.
|
||||
func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
||||
order, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/assimon/luuu/internal/testutil"
|
||||
"github.com/assimon/luuu/model/dao"
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/model/mdb"
|
||||
"github.com/assimon/luuu/model/request"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
)
|
||||
|
||||
func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
req1 := &request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
req2 := &request.CreateTransactionRequest{
|
||||
OrderId: "order_2",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
}
|
||||
|
||||
resp1, err := CreateTransaction(req1)
|
||||
if err != nil {
|
||||
t.Fatalf("create first transaction: %v", err)
|
||||
}
|
||||
resp2, err := CreateTransaction(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("create second transaction: %v", err)
|
||||
}
|
||||
|
||||
if got := fmt.Sprintf("%.2f", resp1.ActualAmount); got != "1.00" {
|
||||
t.Fatalf("first actual amount = %s, want 1.00", got)
|
||||
}
|
||||
if got := fmt.Sprintf("%.2f", resp2.ActualAmount); got != "1.01" {
|
||||
t.Fatalf("second actual amount = %s, want 1.01", got)
|
||||
}
|
||||
if resp1.Token != "wallet_1" || resp2.Token != "wallet_1" {
|
||||
t.Fatalf("unexpected wallet tokens: %s, %s", resp1.Token, resp2.Token)
|
||||
}
|
||||
|
||||
tradeID1, err := data.GetTradeIdByWalletAddressAndAmount(resp1.Token, resp1.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get first runtime lock: %v", err)
|
||||
}
|
||||
if tradeID1 != resp1.TradeId {
|
||||
t.Fatalf("first runtime lock = %s, want %s", tradeID1, resp1.TradeId)
|
||||
}
|
||||
|
||||
tradeID2, err := data.GetTradeIdByWalletAddressAndAmount(resp2.Token, resp2.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get second runtime lock: %v", err)
|
||||
}
|
||||
if tradeID2 != resp2.TradeId {
|
||||
t.Fatalf("second runtime lock = %s, want %s", tradeID2, resp2.TradeId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessingMarksPaidAndReleasesLock(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
BlockTransactionId: "block_1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("order processing: %v", err)
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("get order by trade id: %v", err)
|
||||
}
|
||||
if order.Status != mdb.StatusPaySuccess {
|
||||
t.Fatalf("order status = %d, want %d", order.Status, mdb.StatusPaySuccess)
|
||||
}
|
||||
if order.CallBackConfirm != mdb.CallBackConfirmNo {
|
||||
t.Fatalf("callback confirm = %d, want %d", order.CallBackConfirm, mdb.CallBackConfirmNo)
|
||||
}
|
||||
if order.BlockTransactionId != "block_1" {
|
||||
t.Fatalf("block transaction id = %s, want block_1", order.BlockTransactionId)
|
||||
}
|
||||
|
||||
tradeID, err := data.GetTradeIdByWalletAddressAndAmount(resp.Token, resp.ActualAmount)
|
||||
if err != nil {
|
||||
t.Fatalf("get runtime lock after processing: %v", err)
|
||||
}
|
||||
if tradeID != "" {
|
||||
t.Fatalf("runtime lock still exists: %s", tradeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessingRejectsDuplicateBlockForSameOrder(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
|
||||
req := &request.OrderProcessingRequest{
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
BlockTransactionId: "block_1",
|
||||
}
|
||||
if err = OrderProcessing(req); err != nil {
|
||||
t.Fatalf("first order processing: %v", err)
|
||||
}
|
||||
|
||||
err = OrderProcessing(req)
|
||||
if err != constant.OrderBlockAlreadyProcess {
|
||||
t.Fatalf("second order processing error = %v, want %v", err, constant.OrderBlockAlreadyProcess)
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order after duplicate block: %v", err)
|
||||
}
|
||||
if order.Status != mdb.StatusPaySuccess {
|
||||
t.Fatalf("order status after duplicate block = %d, want %d", order.Status, mdb.StatusPaySuccess)
|
||||
}
|
||||
if order.BlockTransactionId != "block_1" {
|
||||
t.Fatalf("order block transaction id after duplicate block = %s, want block_1", order.BlockTransactionId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessingDoesNotReviveExpiredOrder(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction: %v", err)
|
||||
}
|
||||
|
||||
if err = dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", resp.TradeId).
|
||||
Update("status", mdb.StatusExpired).Error; err != nil {
|
||||
t.Fatalf("force order expired: %v", err)
|
||||
}
|
||||
|
||||
err = OrderProcessing(&request.OrderProcessingRequest{
|
||||
Token: resp.Token,
|
||||
TradeId: resp.TradeId,
|
||||
Amount: resp.ActualAmount,
|
||||
BlockTransactionId: "block_expired",
|
||||
})
|
||||
if err != constant.OrderStatusConflict {
|
||||
t.Fatalf("order processing error = %v, want %v", err, constant.OrderStatusConflict)
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
|
||||
if err != nil {
|
||||
t.Fatalf("reload expired order: %v", err)
|
||||
}
|
||||
if order.Status != mdb.StatusExpired {
|
||||
t.Fatalf("expired order status = %d, want %d", order.Status, mdb.StatusExpired)
|
||||
}
|
||||
if order.BlockTransactionId != "" {
|
||||
t.Fatalf("expired order block transaction id = %s, want empty", order.BlockTransactionId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderProcessingOnlyOneOrderClaimsABlockTransaction(t *testing.T) {
|
||||
cleanup := testutil.SetupTestDatabases(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := data.AddWalletAddress("wallet_1"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
if _, err := data.AddWalletAddress("wallet_2"); err != nil {
|
||||
t.Fatalf("add wallet: %v", err)
|
||||
}
|
||||
|
||||
resp1, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_1",
|
||||
Amount: 1,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create first transaction: %v", err)
|
||||
}
|
||||
resp2, err := CreateTransaction(&request.CreateTransactionRequest{
|
||||
OrderId: "order_2",
|
||||
Amount: 2,
|
||||
NotifyUrl: "https://merchant.example/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create second transaction: %v", err)
|
||||
}
|
||||
|
||||
start := make(chan struct{})
|
||||
errs := make(chan error, 2)
|
||||
var wg sync.WaitGroup
|
||||
for _, tc := range []struct {
|
||||
token string
|
||||
tradeID string
|
||||
amount float64
|
||||
}{
|
||||
{token: resp1.Token, tradeID: resp1.TradeId, amount: resp1.ActualAmount},
|
||||
{token: resp2.Token, tradeID: resp2.TradeId, amount: resp2.ActualAmount},
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func(token, tradeID string, amount float64) {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
errs <- OrderProcessing(&request.OrderProcessingRequest{
|
||||
Token: token,
|
||||
TradeId: tradeID,
|
||||
Amount: amount,
|
||||
BlockTransactionId: "shared_block",
|
||||
})
|
||||
}(tc.token, tc.tradeID, tc.amount)
|
||||
}
|
||||
|
||||
close(start)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
var successCount int
|
||||
var duplicateCount int
|
||||
for err := range errs {
|
||||
switch err {
|
||||
case nil:
|
||||
successCount++
|
||||
case constant.OrderBlockAlreadyProcess:
|
||||
duplicateCount++
|
||||
default:
|
||||
t.Fatalf("unexpected order processing error: %v", err)
|
||||
}
|
||||
}
|
||||
if successCount != 1 || duplicateCount != 1 {
|
||||
t.Fatalf("success=%d duplicate=%d, want 1 and 1", successCount, duplicateCount)
|
||||
}
|
||||
|
||||
orders := []struct {
|
||||
tradeID string
|
||||
}{
|
||||
{tradeID: resp1.TradeId},
|
||||
{tradeID: resp2.TradeId},
|
||||
}
|
||||
var paidCount int
|
||||
var pendingCount int
|
||||
for _, item := range orders {
|
||||
order, err := data.GetOrderInfoByTradeId(item.tradeID)
|
||||
if err != nil {
|
||||
t.Fatalf("reload order %s: %v", item.tradeID, err)
|
||||
}
|
||||
switch order.Status {
|
||||
case mdb.StatusPaySuccess:
|
||||
paidCount++
|
||||
if order.BlockTransactionId != "shared_block" {
|
||||
t.Fatalf("paid order block transaction id = %s, want shared_block", order.BlockTransactionId)
|
||||
}
|
||||
case mdb.StatusWaitPay:
|
||||
pendingCount++
|
||||
if order.BlockTransactionId != "" {
|
||||
t.Fatalf("pending order block transaction id = %s, want empty", order.BlockTransactionId)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected order status for %s: %d", item.tradeID, order.Status)
|
||||
}
|
||||
}
|
||||
if paidCount != 1 || pendingCount != 1 {
|
||||
t.Fatalf("paid=%d pending=%d, want 1 and 1", paidCount, pendingCount)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,20 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"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/constant"
|
||||
"github.com/assimon/luuu/util/http_client"
|
||||
"github.com/assimon/luuu/util/json"
|
||||
"github.com/assimon/luuu/util/log"
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/gookit/goutil/stdutil"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
@@ -64,7 +60,7 @@ type Data struct {
|
||||
Direction int `json:"direction"`
|
||||
}
|
||||
|
||||
// Trc20CallBack trc20回调
|
||||
// Trc20CallBack polls transfers for one wallet and matches them to active orders.
|
||||
func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
@@ -72,6 +68,7 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
log.Sugar.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
client := http_client.GetHttpClient()
|
||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||
endTime := carbon.Now().TimestampMilli()
|
||||
@@ -92,18 +89,20 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var trc20Resp UsdtTrc20Resp
|
||||
err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp)
|
||||
if err != nil {
|
||||
if err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if trc20Resp.PageSize <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, transfer := range trc20Resp.Data {
|
||||
if transfer.To != token || transfer.ContractRet != "SUCCESS" {
|
||||
continue
|
||||
}
|
||||
|
||||
decimalQuant, err := decimal.NewFromString(transfer.Amount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -117,43 +116,39 @@ func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
||||
if tradeId == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
order, err := data.GetOrderInfoByTradeId(tradeId)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 区块的确认时间必须在订单创建时间之后
|
||||
createTime := order.CreatedAt.TimestampMilli()
|
||||
if transfer.BlockTimestamp < createTime {
|
||||
panic("Orders cannot actually be matched")
|
||||
panic("orders cannot actually be matched")
|
||||
}
|
||||
// 到这一步就完全算是支付成功了
|
||||
|
||||
req := &request.OrderProcessingRequest{
|
||||
Token: token,
|
||||
TradeId: tradeId,
|
||||
Amount: amount,
|
||||
BlockTransactionId: transfer.Hash,
|
||||
}
|
||||
err = OrderProcessing(req)
|
||||
if err != nil {
|
||||
if err = OrderProcessing(req); err != nil {
|
||||
if errors.Is(err, constant.OrderBlockAlreadyProcess) || errors.Is(err, constant.OrderStatusConflict) {
|
||||
log.Sugar.Infof("[task] skip already resolved transfer, trade_id=%s, block_transaction_id=%s, err=%v", tradeId, transfer.Hash, err)
|
||||
continue
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
// 回调队列
|
||||
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
|
||||
orderNoticeMaxRetry := viper.GetInt("order_notice_max_retry")
|
||||
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(orderNoticeMaxRetry),
|
||||
asynq.Retention(config.GetOrderExpirationTimeDuration()),
|
||||
)
|
||||
// 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>
|
||||
<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)
|
||||
|
||||
Reference in New Issue
Block a user