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)
}
}()
}