From 938396d82bc05a75cddbf5881fde6a067ed5b9fa Mon Sep 17 00:00:00 2001 From: line-6000 Date: Mon, 30 Mar 2026 16:19:10 +0800 Subject: [PATCH] feat: refactor payment system with multi-currency support and TRX native transfer detection - Add multi-currency exchange rate API integration (GetRateForCoin) - Implement native TRX transfer detection alongside TRC20 USDT transfers - Replace deprecated Tronscan API with Trongrid API for better reliability - Add TRON Grid API key configuration support - Refactor order service to support multiple tokens and currencies - Update wallet address locking/unlocking to track token type - Add base58 and gjson dependencies for crypto operations - Optimize SQLite connection with WAL mode and busy timeout - Remove deprecated USDT rate job (replaced by dynamic rate API) - Update order data model to include currency and receive address fields - Enhance TRC20 callback with improved error handling and logging --- src/.env.example | 5 +- src/config/config.go | 75 ++++- src/crypto/tron.go | 30 ++ src/go.mod | 4 + src/go.sum | 10 + src/model/dao/mdb_sqlite_cgo.go | 6 +- src/model/dao/mdb_sqlite_pure.go | 6 +- src/model/data/order_data.go | 22 +- src/model/data/wallet_address_data.go | 14 +- src/model/mdb/orders_mdb.go | 4 +- src/model/mdb/wallet_address_mdb.go | 4 +- src/model/request/order_request.go | 6 + src/model/response/order_response.go | 7 +- src/model/response/pay_response.go | 3 +- src/model/service/order_service.go | 59 ++-- src/model/service/pay_service.go | 1 + src/model/service/task_service.go | 362 ++++++++++++++++++------ src/mq/handle/callback_queue.go | 1 + src/mq/handle/order_expiration_queue.go | 2 +- src/task/listen.go | 16 +- src/task/listen_trc20_job.go | 13 +- src/task/usdt_rate_job.go | 62 ---- src/telegram/handle.go | 6 +- src/telegram/telegram.go | 9 +- 24 files changed, 505 insertions(+), 222 deletions(-) create mode 100644 src/crypto/tron.go delete mode 100644 src/task/usdt_rate_job.go diff --git a/src/.env.example b/src/.env.example index 2748862..935c44c 100644 --- a/src/.env.example +++ b/src/.env.example @@ -77,4 +77,7 @@ order_expiration_time=10 #订单回调失败最大重试次数 order_notice_max_retry=0 #强制汇率(设置此参数后每笔交易将按照此汇率计算,例如:6.4) -forced_usdt_rate= \ No newline at end of file +forced_usdt_rate= +# 汇率接口url +api_rate_url= +tron_grid_api_key= diff --git a/src/config/config.go b/src/config/config.go index 0708260..52001d3 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -2,22 +2,28 @@ package config import ( "fmt" - "github.com/spf13/viper" + "log" "net/url" "os" "time" + + "github.com/assimon/luuu/util/http_client" + "github.com/spf13/viper" + "github.com/tidwall/gjson" ) var ( - AppDebug bool - MysqlDns string - RuntimePath string - LogSavePath string - StaticPath string - TgBotToken string - TgProxy string - TgManage int64 - UsdtRate float64 + AppDebug bool + MysqlDns string + RuntimePath string + LogSavePath string + StaticPath string + TgBotToken string + TgProxy string + TgManage int64 + UsdtRate float64 + RateApiUrl string + TRON_GRID_API_KEY string ) func Init() { @@ -52,6 +58,9 @@ func Init() { TgBotToken = viper.GetString("tg_bot_token") TgProxy = viper.GetString("tg_proxy") TgManage = viper.GetInt64("tg_manage") + + GetRateApiUrl() + TRON_GRID_API_KEY = viper.GetString("tron_grid_api_key") } func GetAppVersion() string { @@ -74,6 +83,52 @@ func GetApiAuthToken() string { return viper.GetString("api_auth_token") } +func GetRateApiUrl() string { + url := viper.GetString("api_rate_url") + urlFromEnv := os.Getenv("API_RATE_URL") + if url == "" && urlFromEnv != "" { + url = urlFromEnv + } + if url == "" { + log.Println("api_rate_url is empty") + } + RateApiUrl = url + return url +} + +func GetRateForCoin(coin string, base string) float64 { + client := http_client.GetHttpClient() + baseUrl := RateApiUrl + if baseUrl == "" { + log.Printf("rate api url is empty") + return 0.0 + } + if baseUrl[len(baseUrl)-1] != '/' { + baseUrl += "/" + } + url := baseUrl + fmt.Sprintf("%s.json", base) + + fmt.Println("call rate api url:", url) + + resp, err := client.R().Get(url) + if err != nil { + log.Printf("call rate api error: %s", err.Error()) + return 0.0 + } + + targetRate := 0.0 + + gjson.GetBytes(resp.Body(), base).ForEach(func(key, value gjson.Result) bool { + if key.String() == coin { + targetRate = value.Float() + return false + } + return true + }) + return targetRate + +} + func GetUsdtRate() float64 { forcedUsdtRate := viper.GetFloat64("forced_usdt_rate") if forcedUsdtRate > 0 { diff --git a/src/crypto/tron.go b/src/crypto/tron.go new file mode 100644 index 0000000..abf36f3 --- /dev/null +++ b/src/crypto/tron.go @@ -0,0 +1,30 @@ +package crypto + +import ( + "crypto/sha256" + + "github.com/shengdoushi/base58" +) + +const AddressLength = 20 +const PrefixMainnet = 0x41 + +// Encode returns the Base58 encoding of input using the Bitcoin alphabet. +func Encode(input []byte) string { + return base58.Encode(input, base58.BitcoinAlphabet) +} + +// EncodeCheck returns the Base58Check encoding of input (Base58 with a 4-byte SHA-256d checksum). +func EncodeCheck(input []byte) string { + h256h0 := sha256.New() + h256h0.Write(input) + h0 := h256h0.Sum(nil) + + h256h1 := sha256.New() + h256h1.Write(h0) + h1 := h256h1.Sum(nil) + + inputCheck := append(append([]byte(nil), input...), h1[:4]...) + + return Encode(inputCheck) +} diff --git a/src/go.mod b/src/go.mod index 0bf4fb8..a12b28b 100644 --- a/src/go.mod +++ b/src/go.mod @@ -17,9 +17,11 @@ require ( github.com/natefinch/lumberjack v2.0.0+incompatible github.com/robfig/cron/v3 v3.0.1 github.com/satori/go.uuid v1.2.0 + github.com/shengdoushi/base58 v1.0.0 github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.9.0 + github.com/tidwall/gjson v1.18.0 go.uber.org/zap v1.17.0 gopkg.in/telebot.v3 v3.0.0 gorm.io/driver/mysql v1.5.1 @@ -62,6 +64,8 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect diff --git a/src/go.sum b/src/go.sum index 7349ac0..350062c 100644 --- a/src/go.sum +++ b/src/go.sum @@ -380,6 +380,8 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= +github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -413,6 +415,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= diff --git a/src/model/dao/mdb_sqlite_cgo.go b/src/model/dao/mdb_sqlite_cgo.go index e9c7460..da9e05d 100644 --- a/src/model/dao/mdb_sqlite_cgo.go +++ b/src/model/dao/mdb_sqlite_cgo.go @@ -8,5 +8,9 @@ import ( ) func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) { - return gorm.Open(sqlite.Open(dsn), cfg) + db, err := gorm.Open(sqlite.Open(dsn+"?_journal_mode=WAL&_busy_timeout=5000"), cfg) + if err != nil { + return nil, err + } + return db, nil } diff --git a/src/model/dao/mdb_sqlite_pure.go b/src/model/dao/mdb_sqlite_pure.go index 666a9ca..2f1e9ec 100644 --- a/src/model/dao/mdb_sqlite_pure.go +++ b/src/model/dao/mdb_sqlite_pure.go @@ -8,5 +8,9 @@ import ( ) func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) { - return gorm.Open(sqlite.Open(dsn), cfg) + db, err := gorm.Open(sqlite.Open(dsn+"?_journal_mode=WAL&_busy_timeout=5000"), cfg) + if err != nil { + return nil, err + } + return db, nil } diff --git a/src/model/data/order_data.go b/src/model/data/order_data.go index d66c5b1..30bb76f 100644 --- a/src/model/data/order_data.go +++ b/src/model/data/order_data.go @@ -3,16 +3,19 @@ package data import ( "context" "fmt" + "strings" + "time" + "github.com/assimon/luuu/model/dao" "github.com/assimon/luuu/model/mdb" "github.com/assimon/luuu/model/request" + "github.com/assimon/luuu/util/log" "github.com/go-redis/redis/v8" "gorm.io/gorm" - "time" ) var ( - CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%v" // 钱包_待支付金额 : 交易号 + CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%s_%v" // 钱包_币种_待支付金额 : 交易号 ) // GetOrderInfoByOrderId 通过客户订单号查询订单 @@ -78,10 +81,10 @@ func UpdateOrderIsExpirationById(id uint64) error { return err } -// GetTradeIdByWalletAddressAndAmount 通过钱包地址,支付金额获取交易号 -func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, error) { +// GetTradeIdByWalletAddressAndAmountAndToken 通过钱包地址、币种、支付金额获取交易号 +func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, amount float64) (string, error) { ctx := context.Background() - cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount) + cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, address, strings.ToUpper(token), amount) result, err := dao.Rdb.Get(ctx, cacheKey).Result() if err == redis.Nil { return "", nil @@ -93,17 +96,18 @@ func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, e } // LockTransaction 锁定交易 -func LockTransaction(token, tradeId string, amount float64, expirationTime time.Duration) error { +func LockTransaction(address, token, tradeId string, amount float64, expirationTime time.Duration) error { ctx := context.Background() - cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount) + cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, address, strings.ToUpper(token), amount) + log.Sugar.Infof("LockTransaction: cacheKey=%s, tradeId=%s, expirationTime=%v", cacheKey, tradeId, expirationTime) err := dao.Rdb.Set(ctx, cacheKey, tradeId, expirationTime).Err() return err } // UnLockTransaction 解锁交易 -func UnLockTransaction(token string, amount float64) error { +func UnLockTransaction(address string, token string, amount float64) error { ctx := context.Background() - cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, token, amount) + cacheKey := fmt.Sprintf(CacheWalletAddressWithAmountToTradeIdKey, address, strings.ToUpper(token), amount) err := dao.Rdb.Del(ctx, cacheKey).Err() return err } diff --git a/src/model/data/wallet_address_data.go b/src/model/data/wallet_address_data.go index eea107f..40d9b11 100644 --- a/src/model/data/wallet_address_data.go +++ b/src/model/data/wallet_address_data.go @@ -7,8 +7,8 @@ import ( ) // AddWalletAddress 创建钱包 -func AddWalletAddress(token string) (*mdb.WalletAddress, error) { - exist, err := GetWalletAddressByToken(token) +func AddWalletAddress(address string) (*mdb.WalletAddress, error) { + exist, err := GetWalletAddressByToken(address) if err != nil { return nil, err } @@ -16,17 +16,17 @@ func AddWalletAddress(token string) (*mdb.WalletAddress, error) { return nil, constant.WalletAddressAlreadyExists } walletAddress := &mdb.WalletAddress{ - Token: token, - Status: mdb.TokenStatusEnable, + Address: address, + Status: mdb.TokenStatusEnable, } err = dao.Mdb.Create(walletAddress).Error return walletAddress, err } -// GetWalletAddressByToken 通过钱包地址获取token -func GetWalletAddressByToken(token string) (*mdb.WalletAddress, error) { +// GetWalletAddressByToken 通过钱包地址获取address +func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) { walletAddress := new(mdb.WalletAddress) - err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "token = ?", token).Error + err := dao.Mdb.Model(walletAddress).Limit(1).Find(walletAddress, "address = ?", address).Error return walletAddress, err } diff --git a/src/model/mdb/orders_mdb.go b/src/model/mdb/orders_mdb.go index 6b54047..9d22a05 100644 --- a/src/model/mdb/orders_mdb.go +++ b/src/model/mdb/orders_mdb.go @@ -13,8 +13,10 @@ type Orders struct { 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位小数 + Currency string `gorm:"column:currency" json:"currency"` // 订单货币类型 CNY USD...... ActualAmount float64 `gorm:"column:actual_amount" json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数 - Token string `gorm:"column:token" json:"token"` // 所属钱包地址 + ReceiveAddress string `gorm:"column:receive_address" json:"receive_address"` // 所属钱包地址 + Token string `gorm:"column:token" json:"token"` // 所属币种 TRX USDT...... 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"` // 同步回调地址 diff --git a/src/model/mdb/wallet_address_mdb.go b/src/model/mdb/wallet_address_mdb.go index d12d767..f61b39f 100644 --- a/src/model/mdb/wallet_address_mdb.go +++ b/src/model/mdb/wallet_address_mdb.go @@ -7,8 +7,8 @@ const ( // 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:禁用 + Address string `gorm:"index:wallet_address_index;column:address" json:"address"` // 钱包地址 + Status int64 `gorm:"column:status;default:1" json:"status"` // 1:启用 2:禁用 BaseModel } diff --git a/src/model/request/order_request.go b/src/model/request/order_request.go index 3fdf049..1123ef2 100644 --- a/src/model/request/order_request.go +++ b/src/model/request/order_request.go @@ -5,6 +5,8 @@ import "github.com/gookit/validate" // CreateTransactionRequest 创建交易请求 type CreateTransactionRequest struct { OrderId string `json:"order_id" validate:"required|maxLen:32"` + Currency string `json:"currency" validate:"required"` + Token string `json:"token" validate:"required"` Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"` NotifyUrl string `json:"notify_url" validate:"required"` Signature string `json:"signature" validate:"required"` @@ -14,6 +16,8 @@ type CreateTransactionRequest struct { func (r CreateTransactionRequest) Translates() map[string]string { return validate.MS{ "OrderId": "订单号", + "Currency": "货币", + "Token": "币种", "Amount": "支付金额", "NotifyUrl": "异步回调网址", "Signature": "签名", @@ -22,6 +26,8 @@ func (r CreateTransactionRequest) Translates() map[string]string { // OrderProcessingRequest 订单处理 type OrderProcessingRequest struct { + ReceiveAddress string + Currency string Token string Amount float64 TradeId string diff --git a/src/model/response/order_response.go b/src/model/response/order_response.go index 0bb18c9..a04902f 100644 --- a/src/model/response/order_response.go +++ b/src/model/response/order_response.go @@ -5,8 +5,10 @@ type CreateTransactionResponse struct { TradeId string `json:"trade_id"` // epusdt订单号 OrderId string `json:"order_id"` // 客户交易id Amount float64 `json:"amount"` // 订单金额,保留4位小数 + Currency string `json:"currency"` // 订单货币类型 CNY USD...... ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数 - Token string `json:"token"` // 收款钱包地址 + ReceiveAddress string `json:"receive_address"` // 收款钱包地址 + Token string `json:"token"` // 所属币种 TRX USDT...... ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳 PaymentUrl string `json:"payment_url"` // 收银台地址 } @@ -17,7 +19,8 @@ type OrderNotifyResponse struct { OrderId string `json:"order_id"` // 客户交易id Amount float64 `json:"amount"` // 订单金额,保留4位小数 ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数 - Token string `json:"token"` // 收款钱包地址 + ReceiveAddress string `json:"receive_address"` // 收款钱包地址 + Token string `json:"token"` // 所属币种 TRX USDT...... BlockTransactionId string `json:"block_transaction_id"` // 区块id Signature string `json:"signature"` // 签名 Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期 diff --git a/src/model/response/pay_response.go b/src/model/response/pay_response.go index 7d12e56..7aa8990 100644 --- a/src/model/response/pay_response.go +++ b/src/model/response/pay_response.go @@ -3,7 +3,8 @@ package response type CheckoutCounterResponse struct { TradeId string `json:"trade_id"` // epusdt订单号 ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数 - Token string `json:"token"` // 收款钱包地址 + ReceiveAddress string `json:"receive_address"` // 收款钱包地址 + Token string `json:"token"` // 所属币种 TRX USDT...... ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳 RedirectUrl string `json:"redirect_url"` } diff --git a/src/model/service/order_service.go b/src/model/service/order_service.go index 3173924..4d6cfd7 100644 --- a/src/model/service/order_service.go +++ b/src/model/service/order_service.go @@ -3,6 +3,7 @@ package service import ( "fmt" "math/rand" + "strings" "sync" "time" @@ -35,10 +36,14 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT gCreateTransactionLock.Lock() defer gCreateTransactionLock.Unlock() payAmount := math.MustParsePrecFloat64(req.Amount, 2) - // 按照汇率转化USDT + // 按照汇率转化接收货币 + // coin + coin := strings.ToLower(req.Token) + currency := strings.ToLower(req.Currency) + coinRateBaseCurrency := config.GetRateForCoin(coin, currency) decimalPayAmount := decimal.NewFromFloat(payAmount) - decimalRate := decimal.NewFromFloat(config.GetUsdtRate()) - decimalUsdt := decimalPayAmount.Div(decimalRate) + decimalRate := decimal.NewFromFloat(coinRateBaseCurrency) + decimalUsdt := decimalPayAmount.Mul(decimalRate) // cny 是否可以满足最低支付金额 if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 { return nil, constant.PayAmountErr @@ -64,23 +69,25 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT return nil, constant.NotAvailableWalletAddress } amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2) - availableToken, availableAmount, err := CalculateAvailableWalletAndAmount(amount, walletAddress) + availableAddress, availableAmount, err := CalculateAvailableWalletAndAmount(amount, req.Token, walletAddress) if err != nil { return nil, err } - if availableToken == "" { + if availableAddress == "" { return nil, constant.NotAvailableAmountErr } tx := dao.Mdb.Begin() order := &mdb.Orders{ - TradeId: GenerateCode(), - OrderId: req.OrderId, - Amount: req.Amount, - ActualAmount: availableAmount, - Token: availableToken, - Status: mdb.StatusWaitPay, - NotifyUrl: req.NotifyUrl, - RedirectUrl: req.RedirectUrl, + TradeId: GenerateCode(), + OrderId: req.OrderId, + Amount: req.Amount, + Currency: req.Currency, + ActualAmount: availableAmount, + ReceiveAddress: availableAddress, + Token: req.Token, + Status: mdb.StatusWaitPay, + NotifyUrl: req.NotifyUrl, + RedirectUrl: req.RedirectUrl, } err = data.CreateOrderWithTransaction(tx, order) if err != nil { @@ -88,7 +95,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT return nil, err } // 锁定支付池 - err = data.LockTransaction(availableToken, order.TradeId, availableAmount, config.GetOrderExpirationTimeDuration()) + err = data.LockTransaction(availableAddress, req.Token, order.TradeId, availableAmount, config.GetOrderExpirationTimeDuration()) if err != nil { tx.Rollback() return nil, err @@ -104,7 +111,9 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT TradeId: order.TradeId, OrderId: order.OrderId, Amount: order.Amount, + Currency: order.Currency, ActualAmount: order.ActualAmount, + ReceiveAddress: order.ReceiveAddress, Token: order.Token, ExpirationTime: ExpirationTime, PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId), @@ -130,7 +139,7 @@ func OrderProcessing(req *request.OrderProcessingRequest) error { return err } // 解锁交易 - err = data.UnLockTransaction(req.Token, req.Amount) + err = data.UnLockTransaction(req.ReceiveAddress, req.Token, req.Amount) if err != nil { tx.Rollback() return err @@ -140,40 +149,40 @@ func OrderProcessing(req *request.OrderProcessingRequest) error { } // CalculateAvailableWalletAndAmount 计算可用钱包地址和金额 -func CalculateAvailableWalletAndAmount(amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) { - availableToken := "" +func CalculateAvailableWalletAndAmount(amount float64, token string, walletAddress []mdb.WalletAddress) (string, float64, error) { + availableAddress := "" availableAmount := amount calculateAvailableWalletFunc := func(amount float64) (string, error) { availableWallet := "" - for _, address := range walletAddress { - token := address.Token - result, err := data.GetTradeIdByWalletAddressAndAmount(token, amount) + for _, addr := range walletAddress { + walletAddr := addr.Address + result, err := data.GetTradeIdByWalletAddressAndAmountAndToken(walletAddr, token, amount) if err != nil { return "", err } if result == "" { - availableWallet = token + availableWallet = walletAddr break } } return availableWallet, nil } for i := 0; i < IncrementalMaximumNumber; i++ { - token, err := calculateAvailableWalletFunc(availableAmount) + wallet, err := calculateAvailableWalletFunc(availableAmount) if err != nil { return "", 0, err } // 拿不到可用钱包就累加金额 - if token == "" { + if wallet == "" { decimalOldAmount := decimal.NewFromFloat(availableAmount) decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement) availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64() continue } - availableToken = token + availableAddress = wallet break } - return availableToken, availableAmount, nil + return availableAddress, availableAmount, nil } // GenerateCode 订单号生成 diff --git a/src/model/service/pay_service.go b/src/model/service/pay_service.go index 902220f..e336ece 100644 --- a/src/model/service/pay_service.go +++ b/src/model/service/pay_service.go @@ -21,6 +21,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo resp := &response.CheckoutCounterResponse{ TradeId: orderInfo.TradeId, ActualAmount: orderInfo.ActualAmount, + ReceiveAddress: orderInfo.ReceiveAddress, Token: orderInfo.Token, ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(), RedirectUrl: orderInfo.RedirectUrl, diff --git a/src/model/service/task_service.go b/src/model/service/task_service.go index f5c8b17..a5e16f7 100644 --- a/src/model/service/task_service.go +++ b/src/model/service/task_service.go @@ -1,161 +1,347 @@ package service import ( + "encoding/hex" "fmt" "net/http" + "strings" "sync" - "github.com/assimon/luuu/config" - "github.com/spf13/viper" + tron "github.com/assimon/luuu/crypto" + "github.com/hibiken/asynq" + "github.com/shopspring/decimal" + "github.com/spf13/viper" + "github.com/tidwall/gjson" + + "github.com/assimon/luuu/config" "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/assimon/luuu/util/math" "github.com/dromara/carbon/v2" "github.com/gookit/goutil/stdutil" - "github.com/hibiken/asynq" - "github.com/shopspring/decimal" ) -const UsdtTrc20ApiUri = "https://apilist.tronscanapi.com/api/transfer/trc20" - -type UsdtTrc20Resp struct { - PageSize int `json:"page_size"` - Code int `json:"code"` - Data []Data `json:"data"` -} - -type TokenInfo struct { - TokenID string `json:"tokenId"` - TokenAbbr string `json:"tokenAbbr"` - TokenName string `json:"tokenName"` - TokenDecimal int `json:"tokenDecimal"` - TokenCanShow int `json:"tokenCanShow"` - TokenType string `json:"tokenType"` - TokenLogo string `json:"tokenLogo"` - TokenLevel string `json:"tokenLevel"` - IssuerAddr string `json:"issuerAddr"` - Vip bool `json:"vip"` -} - -type Data struct { - Amount string `json:"amount"` - ApprovalAmount string `json:"approval_amount"` - BlockTimestamp int64 `json:"block_timestamp"` - Block int `json:"block"` - From string `json:"from"` - To string `json:"to"` - Hash string `json:"hash"` - Confirmed int `json:"confirmed"` - ContractType string `json:"contract_type"` - ContracTType int `json:"contractType"` - Revert int `json:"revert"` - ContractRet string `json:"contract_ret"` - EventType string `json:"event_type"` - IssueAddress string `json:"issue_address"` - Decimals int `json:"decimals"` - TokenName string `json:"token_name"` - ID string `json:"id"` - Direction int `json:"direction"` -} +const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" // Trc20CallBack trc20回调 -func Trc20CallBack(token string, wg *sync.WaitGroup) { +func Trc20CallBack(address string, wg *sync.WaitGroup) { defer wg.Done() defer func() { if err := recover(); err != nil { log.Sugar.Error(err) } }() + + var innerWg sync.WaitGroup + innerWg.Add(2) + go checkTrxTransfers(address, &innerWg) + go checkTrc20Transfers(address, &innerWg) + innerWg.Wait() +} + +// checkTrxTransfers 查询 TRX 原生转账 +func checkTrxTransfers(address string, wg *sync.WaitGroup) { + defer wg.Done() + defer func() { + if err := recover(); err != nil { + log.Sugar.Errorf("[TRX][%s] panic recovered: %v", address, err) + } + }() + client := http_client.GetHttpClient() startTime := carbon.Now().AddHours(-24).TimestampMilli() endTime := carbon.Now().TimestampMilli() + + url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions", address) + log.Sugar.Debugf("checkTrxTransfers URL: %s, from %d to %d", url, startTime, endTime) + resp, err := client.R().SetQueryParams(map[string]string{ - "sort": "-timestamp", - "limit": "50", - "start": "0", - "direction": "2", - "db_version": "1", - "trc20Id": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", - "address": token, - "start_timestamp": stdutil.ToString(startTime), - "end_timestamp": stdutil.ToString(endTime), - }).Get(UsdtTrc20ApiUri) + "order_by": "block_timestamp,desc", + "limit": "100", + // "only_confirmed": "true", + "only_to": "true", + "min_timestamp": stdutil.ToString(startTime), + "max_timestamp": stdutil.ToString(endTime), + }).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url) if err != nil { panic(err) } if resp.StatusCode() != http.StatusOK { - panic(err) + panic(fmt.Sprintf("TRX API returned status %d", resp.StatusCode())) } - var trc20Resp UsdtTrc20Resp - err = json.Cjson.Unmarshal(resp.Body(), &trc20Resp) - if err != nil { - panic(err) + + log.Sugar.Debugf("Raw request URL: %s", resp.Request.URL) + + success := gjson.GetBytes(resp.Body(), "success").Bool() + if !success { + panic("TRX API response indicates failure") } - if trc20Resp.PageSize <= 0 { + dataArray := gjson.GetBytes(resp.Body(), "data").Array() + log.Sugar.Infof("[TRX][%s] API返回 %d 条交易记录", address, len(dataArray)) + if len(dataArray) == 0 { + log.Sugar.Infof("[TRX][%s] 没有找到任何交易记录,跳过", address) return } - for _, transfer := range trc20Resp.Data { - if transfer.To != token || transfer.ContractRet != "SUCCESS" { + + for i, transfer := range dataArray { + transferType := transfer.Get("raw_data.contract.0.type").String() + if transferType != "TransferContract" { + log.Sugar.Debugf("[TRX][%s] 第%d条: 类型=%s, 非TransferContract, 跳过", address, i, transferType) continue } - decimalQuant, err := decimal.NewFromString(transfer.Amount) - if err != nil { - panic(err) + + contractRet := transfer.Get("ret.0.contractRet").String() + if contractRet != "SUCCESS" { + log.Sugar.Infof("[TRX][%s] 第%d条: contractRet=%s, 非SUCCESS, 跳过", address, i, contractRet) + continue } - decimalDivisor := decimal.NewFromFloat(1000000) - amount := decimalQuant.Div(decimalDivisor).InexactFloat64() - tradeId, err := data.GetTradeIdByWalletAddressAndAmount(token, amount) + + toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String() + toBytes, err := hex.DecodeString(toAddressHex) + if err != nil { + log.Sugar.Errorf("[TRX][%s] 第%d条: 解码地址失败: %v", address, i, err) + continue + } + toAddress := tron.EncodeCheck(toBytes) + if toAddress != address { + log.Sugar.Debugf("[TRX][%s] 第%d条: 目标地址=%s, 不匹配, 跳过", address, i, toAddress) + continue + } + + rawAmount := transfer.Get("raw_data.contract.0.parameter.value.amount").String() + decimalQuant, err := decimal.NewFromString(rawAmount) + if err != nil { + log.Sugar.Errorf("[TRX][%s] 第%d条: 解析金额失败: %v", address, i, err) + continue + } + divisor := decimal.NewFromInt(1000000) + amount := math.MustParsePrecFloat64(decimalQuant.Div(divisor).InexactFloat64(), 2) + txID := transfer.Get("txID").String() + log.Sugar.Infof("[TRX][%s] 第%d条: txID=%s, rawAmount=%s, 解析金额=%.2f", address, i, txID, rawAmount, amount) + if amount <= 0 { + log.Sugar.Infof("[TRX][%s] 第%d条: 金额<=0, 跳过", address, i) + continue + } + + cacheKey := fmt.Sprintf("wallet:%s_%s_%v", address, "TRX", amount) + log.Sugar.Infof("[TRX][%s] 第%d条: 查询Redis匹配, cacheKey=%s", address, i, cacheKey) + tradeId, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "TRX", amount) if err != nil { panic(err) } if tradeId == "" { + log.Sugar.Infof("[TRX][%s] 第%d条: Redis未匹配到订单, 金额=%.2f, 跳过", address, i, amount) continue } + log.Sugar.Infof("[TRX][%s] 第%d条: Redis匹配到订单! tradeId=%s, 金额=%.2f", address, i, tradeId, amount) order, err := data.GetOrderInfoByTradeId(tradeId) if err != nil { panic(err) } - // 区块的确认时间必须在订单创建时间之后 + log.Sugar.Infof("[TRX][%s] 查到订单: tradeId=%s, orderId=%s, status=%d, amount=%.2f, actualAmount=%.2f", address, order.TradeId, order.OrderId, order.Status, order.Amount, order.ActualAmount) + createTime := order.CreatedAt.TimestampMilli() - if transfer.BlockTimestamp < createTime { + blockTimestamp := transfer.Get("block_timestamp").Int() + log.Sugar.Infof("[TRX][%s] 时间校验: blockTimestamp=%d, orderCreateTime=%d", address, blockTimestamp, createTime) + if blockTimestamp < createTime { + log.Sugar.Errorf("[TRX][%s] 区块时间早于订单创建时间,无法匹配! blockTimestamp=%d < createTime=%d", address, blockTimestamp, createTime) panic("Orders cannot actually be matched") } - // 到这一步就完全算是支付成功了 + + transferHash := transfer.Get("txID").String() + log.Sugar.Infof("[TRX][%s] 开始处理订单: tradeId=%s, hash=%s, amount=%.2f", address, tradeId, transferHash, amount) + req := &request.OrderProcessingRequest{ - Token: token, + ReceiveAddress: address, + Token: "TRX", TradeId: tradeId, Amount: amount, - BlockTransactionId: transfer.Hash, + BlockTransactionId: transferHash, } err = OrderProcessing(req) if err != nil { + log.Sugar.Errorf("[TRX][%s] OrderProcessing失败: tradeId=%s, err=%v", address, tradeId, err) panic(err) } - // 回调队列 + log.Sugar.Infof("[TRX][%s] OrderProcessing成功: tradeId=%s", address, tradeId) + 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)) - // 发送机器人消息 + log.Sugar.Infof("[TRX][%s] 回调队列已入队: tradeId=%s", address, tradeId) + msgTpl := ` -📢📢有新的交易支付成功! -
交易号:%s
-
订单号:%s
-
请求支付金额:%f cny
-
实际支付金额:%f usdt
-
钱包地址:%s
-
订单创建时间:%s
-
支付成功时间:%s
+🎉 收款成功通知 + +💰 金额信息 +├ 订单金额:%.2f %s +└ 实际到账:%.2f %s + +📋 订单信息 +├ 交易号:%s +├ 订单号:%s +└ 钱包地址:%s + +⏰ 时间信息 +├ 创建时间:%s +└ 支付时间:%s ` - msg := fmt.Sprintf(msgTpl, order.TradeId, order.OrderId, order.Amount, order.ActualAmount, order.Token, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString()) + msg := fmt.Sprintf(msgTpl, order.Amount, strings.ToUpper(order.Currency), order.ActualAmount, strings.ToUpper(order.Token), order.TradeId, order.OrderId, order.ReceiveAddress, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString()) + log.Sugar.Infof("[TRX][%s] 准备发送Telegram通知: tradeId=%s, orderId=%s", address, tradeId, order.OrderId) + telegram.SendToBot(msg) + } +} + +// checkTrc20Transfers 查询 TRC20 (USDT) 转账 +func checkTrc20Transfers(address string, wg *sync.WaitGroup) { + defer wg.Done() + defer func() { + if err := recover(); err != nil { + log.Sugar.Errorf("[TRC20][%s] panic recovered: %v", address, err) + } + }() + + client := http_client.GetHttpClient() + startTime := carbon.Now().AddHours(-24).TimestampMilli() + endTime := carbon.Now().TimestampMilli() + + url := fmt.Sprintf("https://api.trongrid.io/v1/accounts/%s/transactions/trc20", address) + log.Sugar.Debugf("checkTrc20Transfers URL: %s, from %d to %d", url, startTime, endTime) + + resp, err := client.R().SetQueryParams(map[string]string{ + "order_by": "block_timestamp,desc", + "limit": "100", + // "only_confirmed": "true", + "only_to": "true", + "min_timestamp": stdutil.ToString(startTime), + "max_timestamp": stdutil.ToString(endTime), + }).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url) + if err != nil { + panic(err) + } + if resp.StatusCode() != http.StatusOK { + panic(fmt.Sprintf("TRC20 API returned status %d", resp.StatusCode())) + } + + log.Sugar.Debugf("Raw request URL: %s", resp.Request.URL) + + success := gjson.GetBytes(resp.Body(), "success").Bool() + if !success { + panic("TRC20 API response indicates failure") + } + dataArray := gjson.GetBytes(resp.Body(), "data").Array() + log.Sugar.Infof("[TRC20][%s] API返回 %d 条交易记录", address, len(dataArray)) + if len(dataArray) == 0 { + log.Sugar.Infof("[TRC20][%s] 没有找到任何交易记录,跳过", address) + return + } + + for i, transfer := range dataArray { + // 只处理 USDT + tokenAddress := transfer.Get("token_info.address").String() + if tokenAddress != TRC20_USDT_ID { + log.Sugar.Debugf("[TRC20][%s] 第%d条: tokenAddress=%s, 非USDT, 跳过", address, i, tokenAddress) + continue + } + + to := transfer.Get("to").String() + if to != address { + log.Sugar.Debugf("[TRC20][%s] 第%d条: to=%s, 不匹配, 跳过", address, i, to) + continue + } + + // 解析金额: value / 10^decimals + valueStr := transfer.Get("value").String() + decimalQuant, err := decimal.NewFromString(valueStr) + if err != nil { + log.Sugar.Errorf("[TRC20][%s] 第%d条: 解析value失败: %v", address, i, err) + continue + } + tokenDecimals := transfer.Get("token_info.decimals").Int() + divisor := decimal.New(1, int32(tokenDecimals)) // 10^decimals + amount := math.MustParsePrecFloat64(decimalQuant.Div(divisor).InexactFloat64(), 2) + txID := transfer.Get("transaction_id").String() + log.Sugar.Infof("[TRC20][%s] 第%d条: txID=%s, value=%s, decimals=%d, 解析金额=%.2f", address, i, txID, valueStr, tokenDecimals, amount) + if amount <= 0 { + log.Sugar.Infof("[TRC20][%s] 第%d条: 金额<=0, 跳过", address, i) + continue + } + + cacheKey := fmt.Sprintf("wallet:%s_%s_%v", address, "USDT", amount) + log.Sugar.Infof("[TRC20][%s] 第%d条: 查询Redis匹配, cacheKey=%s", address, i, cacheKey) + tradeId, err := data.GetTradeIdByWalletAddressAndAmountAndToken(address, "USDT", amount) + if err != nil { + panic(err) + } + if tradeId == "" { + log.Sugar.Infof("[TRC20][%s] 第%d条: Redis未匹配到订单, 金额=%.2f, 跳过", address, i, amount) + continue + } + log.Sugar.Infof("[TRC20][%s] 第%d条: Redis匹配到订单! tradeId=%s, 金额=%.2f", address, i, tradeId, amount) + order, err := data.GetOrderInfoByTradeId(tradeId) + if err != nil { + panic(err) + } + log.Sugar.Infof("[TRC20][%s] 查到订单: tradeId=%s, orderId=%s, status=%d, amount=%.2f, actualAmount=%.2f", address, order.TradeId, order.OrderId, order.Status, order.Amount, order.ActualAmount) + + createTime := order.CreatedAt.TimestampMilli() + blockTimestamp := transfer.Get("block_timestamp").Int() + log.Sugar.Infof("[TRC20][%s] 时间校验: blockTimestamp=%d, orderCreateTime=%d", address, blockTimestamp, createTime) + if blockTimestamp < createTime { + log.Sugar.Errorf("[TRC20][%s] 区块时间早于订单创建时间,无法匹配! blockTimestamp=%d < createTime=%d", address, blockTimestamp, createTime) + panic("Orders cannot actually be matched") + } + + transferHash := transfer.Get("transaction_id").String() + log.Sugar.Infof("[TRC20][%s] 开始处理订单: tradeId=%s, hash=%s, amount=%.2f", address, tradeId, transferHash, amount) + + req := &request.OrderProcessingRequest{ + ReceiveAddress: address, + Token: "USDT", + TradeId: tradeId, + Amount: amount, + BlockTransactionId: transferHash, + } + err = OrderProcessing(req) + if err != nil { + log.Sugar.Errorf("[TRC20][%s] OrderProcessing失败: tradeId=%s, err=%v", address, tradeId, err) + panic(err) + } + log.Sugar.Infof("[TRC20][%s] OrderProcessing成功: tradeId=%s", address, tradeId) + + orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order) + orderNoticeMaxRetry := viper.GetInt("order_notice_max_retry") + mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(orderNoticeMaxRetry), + asynq.Retention(config.GetOrderExpirationTimeDuration()), + ) + log.Sugar.Infof("[TRC20][%s] 回调队列已入队: tradeId=%s", address, tradeId) + + msgTpl := ` +🎉 收款成功通知 + +💰 金额信息 +├ 订单金额:%.2f %s +└ 实际到账:%.2f %s + +📋 订单信息 +├ 交易号:%s +├ 订单号:%s +└ 钱包地址:%s + +⏰ 时间信息 +├ 创建时间:%s +└ 支付时间:%s +` + msg := fmt.Sprintf(msgTpl, order.Amount, strings.ToUpper(order.Currency), order.ActualAmount, strings.ToUpper(order.Token), order.TradeId, order.OrderId, order.ReceiveAddress, order.CreatedAt.ToDateTimeString(), carbon.Now().ToDateTimeString()) + log.Sugar.Infof("[TRC20][%s] 准备发送Telegram通知: tradeId=%s, orderId=%s", address, tradeId, order.OrderId) telegram.SendToBot(msg) } } diff --git a/src/mq/handle/callback_queue.go b/src/mq/handle/callback_queue.go index 230e815..ef2d35b 100644 --- a/src/mq/handle/callback_queue.go +++ b/src/mq/handle/callback_queue.go @@ -47,6 +47,7 @@ func OrderCallbackHandle(ctx context.Context, t *asynq.Task) error { OrderId: order.OrderId, Amount: order.Amount, ActualAmount: order.ActualAmount, + ReceiveAddress: order.ReceiveAddress, Token: order.Token, BlockTransactionId: order.BlockTransactionId, Status: mdb.StatusPaySuccess, diff --git a/src/mq/handle/order_expiration_queue.go b/src/mq/handle/order_expiration_queue.go index 7d57887..94feed7 100644 --- a/src/mq/handle/order_expiration_queue.go +++ b/src/mq/handle/order_expiration_queue.go @@ -31,7 +31,7 @@ func OrderExpirationHandle(ctx context.Context, t *asynq.Task) error { if err != nil { return err } - err = data.UnLockTransaction(orderInfo.Token, orderInfo.ActualAmount) + err = data.UnLockTransaction(orderInfo.ReceiveAddress, orderInfo.Token, orderInfo.ActualAmount) if err != nil { return err } diff --git a/src/task/listen.go b/src/task/listen.go index 46991eb..3eb96ca 100644 --- a/src/task/listen.go +++ b/src/task/listen.go @@ -1,12 +1,20 @@ package task -import "github.com/robfig/cron/v3" +import ( + "github.com/assimon/luuu/util/log" + "github.com/robfig/cron/v3" +) func Start() { + log.Sugar.Info("[task] Starting task scheduler...") c := cron.New() - // 汇率监听 - c.AddJob("@every 60s", UsdtRateJob{}) // trc20钱包监听 - c.AddJob("@every 5s", ListenTrc20Job{}) + _, err := c.AddJob("@every 5s", ListenTrc20Job{}) + if err != nil { + log.Sugar.Errorf("[task] Failed to add ListenTrc20Job: %v", err) + return + } + log.Sugar.Info("[task] ListenTrc20Job scheduled successfully (@every 5s)") c.Start() + log.Sugar.Info("[task] Task scheduler started") } diff --git a/src/task/listen_trc20_job.go b/src/task/listen_trc20_job.go index c4a180e..f35cc86 100644 --- a/src/task/listen_trc20_job.go +++ b/src/task/listen_trc20_job.go @@ -1,10 +1,11 @@ package task import ( + "sync" + "github.com/assimon/luuu/model/data" "github.com/assimon/luuu/model/service" "github.com/assimon/luuu/util/log" - "sync" ) type ListenTrc20Job struct { @@ -15,18 +16,24 @@ var gListenTrc20JobLock sync.Mutex func (r ListenTrc20Job) Run() { gListenTrc20JobLock.Lock() defer gListenTrc20JobLock.Unlock() + log.Sugar.Debug("[ListenTrc20Job] Job triggered") walletAddress, err := data.GetAvailableWalletAddress() if err != nil { - log.Sugar.Error(err) + log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err) return } if len(walletAddress) <= 0 { + log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses") return } + log.Sugar.Infof("[ListenTrc20Job] Found %d wallet addresses to monitor", len(walletAddress)) var wg sync.WaitGroup for _, address := range walletAddress { + log.Sugar.Infof("[ListenTrc20Job] Listening to address: %s", address.Address) + wg.Add(1) - go service.Trc20CallBack(address.Token, &wg) + go service.Trc20CallBack(address.Address, &wg) } wg.Wait() + log.Sugar.Debug("[ListenTrc20Job] Job completed") } diff --git a/src/task/usdt_rate_job.go b/src/task/usdt_rate_job.go deleted file mode 100644 index 90a8891..0000000 --- a/src/task/usdt_rate_job.go +++ /dev/null @@ -1,62 +0,0 @@ -package task - -import ( - "github.com/assimon/luuu/config" - "github.com/assimon/luuu/util/http_client" - "github.com/assimon/luuu/util/json" - "github.com/assimon/luuu/util/log" - "github.com/assimon/luuu/util/math" - "time" -) - -const UsdtRateApiUri = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail/chart" - -type UsdtRateJob struct { -} - -type UsdtRateResp struct { - Data Data `json:"data"` - Status Status `json:"status"` -} - -type Status struct { - Timestamp time.Time `json:"timestamp"` - ErrorCode string `json:"error_code"` - ErrorMessage string `json:"error_message"` - Elapsed string `json:"elapsed"` - CreditCount int `json:"credit_count"` -} - -type Data struct { - Points map[string]Points `json:"points"` -} - -type Points struct { - V []float64 `json:"v"` - C []float64 `json:"c"` -} - -func (r UsdtRateJob) Run() { - client := http_client.GetHttpClient() - resp, err := client.R().SetQueryString("id=825&range=1H&convertId=2787").SetHeader("Accept", "application/json").Get(UsdtRateApiUri) - if err != nil { - log.Sugar.Error("usdt rate get err:", err.Error()) - return - } - var usdtResp UsdtRateResp - err = json.Cjson.Unmarshal(resp.Body(), &usdtResp) - if err != nil { - log.Sugar.Error("Unmarshal usdt resp err:", err.Error()) - return - } - if usdtResp.Status.ErrorCode != "0" { - log.Sugar.Error("usdt resp err:", usdtResp.Status.ErrorMessage) - return - } - for _, points := range usdtResp.Data.Points { - if len(points.C) > 0 && points.C[0] > 0 { - config.UsdtRate = math.MustParsePrecFloat64(points.C[0], 2) - return - } - } -} diff --git a/src/telegram/handle.go b/src/telegram/handle.go index 8b4a3ef..3b5f008 100644 --- a/src/telegram/handle.go +++ b/src/telegram/handle.go @@ -45,8 +45,8 @@ func WalletList(c tb.Context) error { } var temp []tb.InlineButton btnInfo := tb.InlineButton{ - Unique: wallet.Token, - Text: fmt.Sprintf("%s[%s]", wallet.Token, status), + Unique: wallet.Address, + Text: fmt.Sprintf("%s[%s]", wallet.Address, status), Data: strutil.MustString(wallet.ID), } bots.Handle(&btnInfo, WalletInfo) @@ -93,7 +93,7 @@ func WalletInfo(c tb.Context) error { bots.Handle(&disableBtn, DisableWallet) bots.Handle(&delBtn, DelWallet) bots.Handle(&backBtn, WalletList) - return c.EditOrReply(tokenInfo.Token, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{ + return c.EditOrReply(tokenInfo.Address, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{ { enableBtn, disableBtn, diff --git a/src/telegram/telegram.go b/src/telegram/telegram.go index b29bf63..7410a55 100644 --- a/src/telegram/telegram.go +++ b/src/telegram/telegram.go @@ -46,14 +46,21 @@ func RegisterHandle() { // SendToBot 主动发送消息机器人消息 func SendToBot(msg string) { go func() { + if bots == nil { + log.Sugar.Error("[Telegram] bots实例为nil,无法发送消息") + return + } user := tb.User{ ID: config.TgManage, } + log.Sugar.Infof("[Telegram] 正在发送消息给用户ID=%d", config.TgManage) _, err := bots.Send(&user, msg, &tb.SendOptions{ ParseMode: tb.ModeHTML, }) if err != nil { - log.Sugar.Error(err) + log.Sugar.Errorf("[Telegram] 发送消息失败: userID=%d, err=%v", config.TgManage, err) + } else { + log.Sugar.Infof("[Telegram] 发送消息成功: userID=%d", config.TgManage) } }() }