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
This commit is contained in:
@@ -78,3 +78,6 @@ order_expiration_time=10
|
|||||||
order_notice_max_retry=0
|
order_notice_max_retry=0
|
||||||
#强制汇率(设置此参数后每笔交易将按照此汇率计算,例如:6.4)
|
#强制汇率(设置此参数后每笔交易将按照此汇率计算,例如:6.4)
|
||||||
forced_usdt_rate=
|
forced_usdt_rate=
|
||||||
|
# 汇率接口url
|
||||||
|
api_rate_url=
|
||||||
|
tron_grid_api_key=
|
||||||
|
|||||||
+65
-10
@@ -2,22 +2,28 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/viper"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/assimon/luuu/util/http_client"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AppDebug bool
|
AppDebug bool
|
||||||
MysqlDns string
|
MysqlDns string
|
||||||
RuntimePath string
|
RuntimePath string
|
||||||
LogSavePath string
|
LogSavePath string
|
||||||
StaticPath string
|
StaticPath string
|
||||||
TgBotToken string
|
TgBotToken string
|
||||||
TgProxy string
|
TgProxy string
|
||||||
TgManage int64
|
TgManage int64
|
||||||
UsdtRate float64
|
UsdtRate float64
|
||||||
|
RateApiUrl string
|
||||||
|
TRON_GRID_API_KEY string
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -52,6 +58,9 @@ func Init() {
|
|||||||
TgBotToken = viper.GetString("tg_bot_token")
|
TgBotToken = viper.GetString("tg_bot_token")
|
||||||
TgProxy = viper.GetString("tg_proxy")
|
TgProxy = viper.GetString("tg_proxy")
|
||||||
TgManage = viper.GetInt64("tg_manage")
|
TgManage = viper.GetInt64("tg_manage")
|
||||||
|
|
||||||
|
GetRateApiUrl()
|
||||||
|
TRON_GRID_API_KEY = viper.GetString("tron_grid_api_key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAppVersion() string {
|
func GetAppVersion() string {
|
||||||
@@ -74,6 +83,52 @@ func GetApiAuthToken() string {
|
|||||||
return viper.GetString("api_auth_token")
|
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 {
|
func GetUsdtRate() float64 {
|
||||||
forcedUsdtRate := viper.GetFloat64("forced_usdt_rate")
|
forcedUsdtRate := viper.GetFloat64("forced_usdt_rate")
|
||||||
if forcedUsdtRate > 0 {
|
if forcedUsdtRate > 0 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -17,9 +17,11 @@ require (
|
|||||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
|
github.com/shengdoushi/base58 v1.0.0
|
||||||
github.com/shopspring/decimal v1.3.1
|
github.com/shopspring/decimal v1.3.1
|
||||||
github.com/spf13/cobra v1.2.1
|
github.com/spf13/cobra v1.2.1
|
||||||
github.com/spf13/viper v1.9.0
|
github.com/spf13/viper v1.9.0
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
go.uber.org/zap v1.17.0
|
go.uber.org/zap v1.17.0
|
||||||
gopkg.in/telebot.v3 v3.0.0
|
gopkg.in/telebot.v3 v3.0.0
|
||||||
gorm.io/driver/mysql v1.5.1
|
gorm.io/driver/mysql v1.5.1
|
||||||
@@ -62,6 +64,8 @@ require (
|
|||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.2.0 // 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/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
|
|||||||
+10
@@ -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 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
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/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 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
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=
|
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/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 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
|||||||
@@ -8,5 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func openDB(dsn string, cfg *gorm.Config) (*gorm.DB, error) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package data
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/dao"
|
"github.com/assimon/luuu/model/dao"
|
||||||
"github.com/assimon/luuu/model/mdb"
|
"github.com/assimon/luuu/model/mdb"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/assimon/luuu/model/request"
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%v" // 钱包_待支付金额 : 交易号
|
CacheWalletAddressWithAmountToTradeIdKey = "wallet:%s_%s_%v" // 钱包_币种_待支付金额 : 交易号
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetOrderInfoByOrderId 通过客户订单号查询订单
|
// GetOrderInfoByOrderId 通过客户订单号查询订单
|
||||||
@@ -78,10 +81,10 @@ func UpdateOrderIsExpirationById(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTradeIdByWalletAddressAndAmount 通过钱包地址,支付金额获取交易号
|
// GetTradeIdByWalletAddressAndAmountAndToken 通过钱包地址、币种、支付金额获取交易号
|
||||||
func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, error) {
|
func GetTradeIdByWalletAddressAndAmountAndToken(address string, token string, amount float64) (string, error) {
|
||||||
ctx := context.Background()
|
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()
|
result, err := dao.Rdb.Get(ctx, cacheKey).Result()
|
||||||
if err == redis.Nil {
|
if err == redis.Nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -93,17 +96,18 @@ func GetTradeIdByWalletAddressAndAmount(token string, amount float64) (string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LockTransaction 锁定交易
|
// 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()
|
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()
|
err := dao.Rdb.Set(ctx, cacheKey, tradeId, expirationTime).Err()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnLockTransaction 解锁交易
|
// UnLockTransaction 解锁交易
|
||||||
func UnLockTransaction(token string, amount float64) error {
|
func UnLockTransaction(address string, token string, amount float64) error {
|
||||||
ctx := context.Background()
|
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()
|
err := dao.Rdb.Del(ctx, cacheKey).Err()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AddWalletAddress 创建钱包
|
// AddWalletAddress 创建钱包
|
||||||
func AddWalletAddress(token string) (*mdb.WalletAddress, error) {
|
func AddWalletAddress(address string) (*mdb.WalletAddress, error) {
|
||||||
exist, err := GetWalletAddressByToken(token)
|
exist, err := GetWalletAddressByToken(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -16,17 +16,17 @@ func AddWalletAddress(token string) (*mdb.WalletAddress, error) {
|
|||||||
return nil, constant.WalletAddressAlreadyExists
|
return nil, constant.WalletAddressAlreadyExists
|
||||||
}
|
}
|
||||||
walletAddress := &mdb.WalletAddress{
|
walletAddress := &mdb.WalletAddress{
|
||||||
Token: token,
|
Address: address,
|
||||||
Status: mdb.TokenStatusEnable,
|
Status: mdb.TokenStatusEnable,
|
||||||
}
|
}
|
||||||
err = dao.Mdb.Create(walletAddress).Error
|
err = dao.Mdb.Create(walletAddress).Error
|
||||||
return walletAddress, err
|
return walletAddress, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWalletAddressByToken 通过钱包地址获取token
|
// GetWalletAddressByToken 通过钱包地址获取address
|
||||||
func GetWalletAddressByToken(token string) (*mdb.WalletAddress, error) {
|
func GetWalletAddressByToken(address string) (*mdb.WalletAddress, error) {
|
||||||
walletAddress := new(mdb.WalletAddress)
|
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
|
return walletAddress, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ type Orders struct {
|
|||||||
OrderId string `gorm:"column:order_id" json:"order_id"` // 客户交易id
|
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
|
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位小数
|
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位小数
|
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:已过期
|
Status int `gorm:"column:status;default:1" json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"` // 异步回调地址
|
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"` // 异步回调地址
|
||||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"` // 同步回调地址
|
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"` // 同步回调地址
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const (
|
|||||||
|
|
||||||
// WalletAddress 钱包表
|
// WalletAddress 钱包表
|
||||||
type WalletAddress struct {
|
type WalletAddress struct {
|
||||||
Token string `gorm:"index:wallet_address_token_index;column:token" json:"token"` // 钱包token
|
Address string `gorm:"index:wallet_address_index;column:address" json:"address"` // 钱包地址
|
||||||
Status int64 `gorm:"column:status;default:1" json:"status"` // 1:启用 2:禁用
|
Status int64 `gorm:"column:status;default:1" json:"status"` // 1:启用 2:禁用
|
||||||
BaseModel
|
BaseModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import "github.com/gookit/validate"
|
|||||||
// CreateTransactionRequest 创建交易请求
|
// CreateTransactionRequest 创建交易请求
|
||||||
type CreateTransactionRequest struct {
|
type CreateTransactionRequest struct {
|
||||||
OrderId string `json:"order_id" validate:"required|maxLen:32"`
|
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"`
|
Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"`
|
||||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||||
Signature string `json:"signature" validate:"required"`
|
Signature string `json:"signature" validate:"required"`
|
||||||
@@ -14,6 +16,8 @@ type CreateTransactionRequest struct {
|
|||||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||||
return validate.MS{
|
return validate.MS{
|
||||||
"OrderId": "订单号",
|
"OrderId": "订单号",
|
||||||
|
"Currency": "货币",
|
||||||
|
"Token": "币种",
|
||||||
"Amount": "支付金额",
|
"Amount": "支付金额",
|
||||||
"NotifyUrl": "异步回调网址",
|
"NotifyUrl": "异步回调网址",
|
||||||
"Signature": "签名",
|
"Signature": "签名",
|
||||||
@@ -22,6 +26,8 @@ func (r CreateTransactionRequest) Translates() map[string]string {
|
|||||||
|
|
||||||
// OrderProcessingRequest 订单处理
|
// OrderProcessingRequest 订单处理
|
||||||
type OrderProcessingRequest struct {
|
type OrderProcessingRequest struct {
|
||||||
|
ReceiveAddress string
|
||||||
|
Currency string
|
||||||
Token string
|
Token string
|
||||||
Amount float64
|
Amount float64
|
||||||
TradeId string
|
TradeId string
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ type CreateTransactionResponse struct {
|
|||||||
TradeId string `json:"trade_id"` // epusdt订单号
|
TradeId string `json:"trade_id"` // epusdt订单号
|
||||||
OrderId string `json:"order_id"` // 客户交易id
|
OrderId string `json:"order_id"` // 客户交易id
|
||||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||||
|
Currency string `json:"currency"` // 订单货币类型 CNY USD......
|
||||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
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"` // 过期时间 时间戳
|
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||||
PaymentUrl string `json:"payment_url"` // 收银台地址
|
PaymentUrl string `json:"payment_url"` // 收银台地址
|
||||||
}
|
}
|
||||||
@@ -17,7 +19,8 @@ type OrderNotifyResponse struct {
|
|||||||
OrderId string `json:"order_id"` // 客户交易id
|
OrderId string `json:"order_id"` // 客户交易id
|
||||||
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
Amount float64 `json:"amount"` // 订单金额,保留4位小数
|
||||||
ActualAmount float64 `json:"actual_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
|
BlockTransactionId string `json:"block_transaction_id"` // 区块id
|
||||||
Signature string `json:"signature"` // 签名
|
Signature string `json:"signature"` // 签名
|
||||||
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package response
|
|||||||
type CheckoutCounterResponse struct {
|
type CheckoutCounterResponse struct {
|
||||||
TradeId string `json:"trade_id"` // epusdt订单号
|
TradeId string `json:"trade_id"` // epusdt订单号
|
||||||
ActualAmount float64 `json:"actual_amount"` // 订单实际需要支付的金额,保留4位小数
|
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"` // 过期时间 时间戳
|
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||||
RedirectUrl string `json:"redirect_url"`
|
RedirectUrl string `json:"redirect_url"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,10 +36,14 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
gCreateTransactionLock.Lock()
|
gCreateTransactionLock.Lock()
|
||||||
defer gCreateTransactionLock.Unlock()
|
defer gCreateTransactionLock.Unlock()
|
||||||
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
|
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)
|
decimalPayAmount := decimal.NewFromFloat(payAmount)
|
||||||
decimalRate := decimal.NewFromFloat(config.GetUsdtRate())
|
decimalRate := decimal.NewFromFloat(coinRateBaseCurrency)
|
||||||
decimalUsdt := decimalPayAmount.Div(decimalRate)
|
decimalUsdt := decimalPayAmount.Mul(decimalRate)
|
||||||
// cny 是否可以满足最低支付金额
|
// cny 是否可以满足最低支付金额
|
||||||
if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 {
|
if decimalPayAmount.Cmp(decimal.NewFromFloat(CnyMinimumPaymentAmount)) == -1 {
|
||||||
return nil, constant.PayAmountErr
|
return nil, constant.PayAmountErr
|
||||||
@@ -64,23 +69,25 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
return nil, constant.NotAvailableWalletAddress
|
return nil, constant.NotAvailableWalletAddress
|
||||||
}
|
}
|
||||||
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2)
|
amount := math.MustParsePrecFloat64(decimalUsdt.InexactFloat64(), 2)
|
||||||
availableToken, availableAmount, err := CalculateAvailableWalletAndAmount(amount, walletAddress)
|
availableAddress, availableAmount, err := CalculateAvailableWalletAndAmount(amount, req.Token, walletAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if availableToken == "" {
|
if availableAddress == "" {
|
||||||
return nil, constant.NotAvailableAmountErr
|
return nil, constant.NotAvailableAmountErr
|
||||||
}
|
}
|
||||||
tx := dao.Mdb.Begin()
|
tx := dao.Mdb.Begin()
|
||||||
order := &mdb.Orders{
|
order := &mdb.Orders{
|
||||||
TradeId: GenerateCode(),
|
TradeId: GenerateCode(),
|
||||||
OrderId: req.OrderId,
|
OrderId: req.OrderId,
|
||||||
Amount: req.Amount,
|
Amount: req.Amount,
|
||||||
ActualAmount: availableAmount,
|
Currency: req.Currency,
|
||||||
Token: availableToken,
|
ActualAmount: availableAmount,
|
||||||
Status: mdb.StatusWaitPay,
|
ReceiveAddress: availableAddress,
|
||||||
NotifyUrl: req.NotifyUrl,
|
Token: req.Token,
|
||||||
RedirectUrl: req.RedirectUrl,
|
Status: mdb.StatusWaitPay,
|
||||||
|
NotifyUrl: req.NotifyUrl,
|
||||||
|
RedirectUrl: req.RedirectUrl,
|
||||||
}
|
}
|
||||||
err = data.CreateOrderWithTransaction(tx, order)
|
err = data.CreateOrderWithTransaction(tx, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,7 +95,7 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -104,7 +111,9 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
|||||||
TradeId: order.TradeId,
|
TradeId: order.TradeId,
|
||||||
OrderId: order.OrderId,
|
OrderId: order.OrderId,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
|
Currency: order.Currency,
|
||||||
ActualAmount: order.ActualAmount,
|
ActualAmount: order.ActualAmount,
|
||||||
|
ReceiveAddress: order.ReceiveAddress,
|
||||||
Token: order.Token,
|
Token: order.Token,
|
||||||
ExpirationTime: ExpirationTime,
|
ExpirationTime: ExpirationTime,
|
||||||
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
PaymentUrl: fmt.Sprintf("%s/pay/checkout-counter/%s", config.GetAppUri(), order.TradeId),
|
||||||
@@ -130,7 +139,7 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 解锁交易
|
// 解锁交易
|
||||||
err = data.UnLockTransaction(req.Token, req.Amount)
|
err = data.UnLockTransaction(req.ReceiveAddress, req.Token, req.Amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@@ -140,40 +149,40 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CalculateAvailableWalletAndAmount 计算可用钱包地址和金额
|
// CalculateAvailableWalletAndAmount 计算可用钱包地址和金额
|
||||||
func CalculateAvailableWalletAndAmount(amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
func CalculateAvailableWalletAndAmount(amount float64, token string, walletAddress []mdb.WalletAddress) (string, float64, error) {
|
||||||
availableToken := ""
|
availableAddress := ""
|
||||||
availableAmount := amount
|
availableAmount := amount
|
||||||
calculateAvailableWalletFunc := func(amount float64) (string, error) {
|
calculateAvailableWalletFunc := func(amount float64) (string, error) {
|
||||||
availableWallet := ""
|
availableWallet := ""
|
||||||
for _, address := range walletAddress {
|
for _, addr := range walletAddress {
|
||||||
token := address.Token
|
walletAddr := addr.Address
|
||||||
result, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
|
result, err := data.GetTradeIdByWalletAddressAndAmountAndToken(walletAddr, token, amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if result == "" {
|
if result == "" {
|
||||||
availableWallet = token
|
availableWallet = walletAddr
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return availableWallet, nil
|
return availableWallet, nil
|
||||||
}
|
}
|
||||||
for i := 0; i < IncrementalMaximumNumber; i++ {
|
for i := 0; i < IncrementalMaximumNumber; i++ {
|
||||||
token, err := calculateAvailableWalletFunc(availableAmount)
|
wallet, err := calculateAvailableWalletFunc(availableAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
// 拿不到可用钱包就累加金额
|
// 拿不到可用钱包就累加金额
|
||||||
if token == "" {
|
if wallet == "" {
|
||||||
decimalOldAmount := decimal.NewFromFloat(availableAmount)
|
decimalOldAmount := decimal.NewFromFloat(availableAmount)
|
||||||
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
|
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
|
||||||
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
|
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
availableToken = token
|
availableAddress = wallet
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return availableToken, availableAmount, nil
|
return availableAddress, availableAmount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateCode 订单号生成
|
// GenerateCode 订单号生成
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
|
|||||||
resp := &response.CheckoutCounterResponse{
|
resp := &response.CheckoutCounterResponse{
|
||||||
TradeId: orderInfo.TradeId,
|
TradeId: orderInfo.TradeId,
|
||||||
ActualAmount: orderInfo.ActualAmount,
|
ActualAmount: orderInfo.ActualAmount,
|
||||||
|
ReceiveAddress: orderInfo.ReceiveAddress,
|
||||||
Token: orderInfo.Token,
|
Token: orderInfo.Token,
|
||||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||||
RedirectUrl: orderInfo.RedirectUrl,
|
RedirectUrl: orderInfo.RedirectUrl,
|
||||||
|
|||||||
@@ -1,161 +1,347 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/config"
|
tron "github.com/assimon/luuu/crypto"
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
|
"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/data"
|
||||||
"github.com/assimon/luuu/model/request"
|
"github.com/assimon/luuu/model/request"
|
||||||
"github.com/assimon/luuu/mq"
|
"github.com/assimon/luuu/mq"
|
||||||
"github.com/assimon/luuu/mq/handle"
|
"github.com/assimon/luuu/mq/handle"
|
||||||
"github.com/assimon/luuu/telegram"
|
"github.com/assimon/luuu/telegram"
|
||||||
"github.com/assimon/luuu/util/http_client"
|
"github.com/assimon/luuu/util/http_client"
|
||||||
"github.com/assimon/luuu/util/json"
|
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/assimon/luuu/util/log"
|
||||||
|
"github.com/assimon/luuu/util/math"
|
||||||
"github.com/dromara/carbon/v2"
|
"github.com/dromara/carbon/v2"
|
||||||
"github.com/gookit/goutil/stdutil"
|
"github.com/gookit/goutil/stdutil"
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const UsdtTrc20ApiUri = "https://apilist.tronscanapi.com/api/transfer/trc20"
|
const TRC20_USDT_ID = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trc20CallBack trc20回调
|
// Trc20CallBack trc20回调
|
||||||
func Trc20CallBack(token string, wg *sync.WaitGroup) {
|
func Trc20CallBack(address string, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Sugar.Error(err)
|
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()
|
client := http_client.GetHttpClient()
|
||||||
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
startTime := carbon.Now().AddHours(-24).TimestampMilli()
|
||||||
endTime := carbon.Now().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{
|
resp, err := client.R().SetQueryParams(map[string]string{
|
||||||
"sort": "-timestamp",
|
"order_by": "block_timestamp,desc",
|
||||||
"limit": "50",
|
"limit": "100",
|
||||||
"start": "0",
|
// "only_confirmed": "true",
|
||||||
"direction": "2",
|
"only_to": "true",
|
||||||
"db_version": "1",
|
"min_timestamp": stdutil.ToString(startTime),
|
||||||
"trc20Id": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
"max_timestamp": stdutil.ToString(endTime),
|
||||||
"address": token,
|
}).SetHeader("TRON-PRO-API-KEY", config.TRON_GRID_API_KEY).Get(url)
|
||||||
"start_timestamp": stdutil.ToString(startTime),
|
|
||||||
"end_timestamp": stdutil.ToString(endTime),
|
|
||||||
}).Get(UsdtTrc20ApiUri)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode() != http.StatusOK {
|
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)
|
log.Sugar.Debugf("Raw request URL: %s", resp.Request.URL)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
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
|
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
|
continue
|
||||||
}
|
}
|
||||||
decimalQuant, err := decimal.NewFromString(transfer.Amount)
|
|
||||||
if err != nil {
|
contractRet := transfer.Get("ret.0.contractRet").String()
|
||||||
panic(err)
|
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()
|
toAddressHex := transfer.Get("raw_data.contract.0.parameter.value.to_address").String()
|
||||||
tradeId, err := data.GetTradeIdByWalletAddressAndAmount(token, amount)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if tradeId == "" {
|
if tradeId == "" {
|
||||||
|
log.Sugar.Infof("[TRX][%s] 第%d条: Redis未匹配到订单, 金额=%.2f, 跳过", address, i, amount)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log.Sugar.Infof("[TRX][%s] 第%d条: Redis匹配到订单! tradeId=%s, 金额=%.2f", address, i, tradeId, amount)
|
||||||
order, err := data.GetOrderInfoByTradeId(tradeId)
|
order, err := data.GetOrderInfoByTradeId(tradeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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()
|
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")
|
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{
|
req := &request.OrderProcessingRequest{
|
||||||
Token: token,
|
ReceiveAddress: address,
|
||||||
|
Token: "TRX",
|
||||||
TradeId: tradeId,
|
TradeId: tradeId,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
BlockTransactionId: transfer.Hash,
|
BlockTransactionId: transferHash,
|
||||||
}
|
}
|
||||||
err = OrderProcessing(req)
|
err = OrderProcessing(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("[TRX][%s] OrderProcessing失败: tradeId=%s, err=%v", address, tradeId, err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
// 回调队列
|
log.Sugar.Infof("[TRX][%s] OrderProcessing成功: tradeId=%s", address, tradeId)
|
||||||
|
|
||||||
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
|
orderCallbackQueue, _ := handle.NewOrderCallbackQueue(order)
|
||||||
orderNoticeMaxRetry := viper.GetInt("order_notice_max_retry")
|
orderNoticeMaxRetry := viper.GetInt("order_notice_max_retry")
|
||||||
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(orderNoticeMaxRetry),
|
mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(orderNoticeMaxRetry),
|
||||||
asynq.Retention(config.GetOrderExpirationTimeDuration()),
|
asynq.Retention(config.GetOrderExpirationTimeDuration()),
|
||||||
)
|
)
|
||||||
// mq.MClient.Enqueue(orderCallbackQueue, asynq.MaxRetry(5))
|
log.Sugar.Infof("[TRX][%s] 回调队列已入队: tradeId=%s", address, tradeId)
|
||||||
// 发送机器人消息
|
|
||||||
msgTpl := `
|
msgTpl := `
|
||||||
<b>📢📢有新的交易支付成功!</b>
|
🎉 <b>收款成功通知</b>
|
||||||
<pre>交易号:%s</pre>
|
|
||||||
<pre>订单号:%s</pre>
|
💰 <b>金额信息</b>
|
||||||
<pre>请求支付金额:%f cny</pre>
|
├ 订单金额:<code>%.2f %s</code>
|
||||||
<pre>实际支付金额:%f usdt</pre>
|
└ 实际到账:<code>%.2f %s</code>
|
||||||
<pre>钱包地址:%s</pre>
|
|
||||||
<pre>订单创建时间:%s</pre>
|
📋 <b>订单信息</b>
|
||||||
<pre>支付成功时间:%s</pre>
|
├ 交易号:<code>%s</code>
|
||||||
|
├ 订单号:<code>%s</code>
|
||||||
|
└ 钱包地址:<code>%s</code>
|
||||||
|
|
||||||
|
⏰ <b>时间信息</b>
|
||||||
|
├ 创建时间:%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 := `
|
||||||
|
🎉 <b>收款成功通知</b>
|
||||||
|
|
||||||
|
💰 <b>金额信息</b>
|
||||||
|
├ 订单金额:<code>%.2f %s</code>
|
||||||
|
└ 实际到账:<code>%.2f %s</code>
|
||||||
|
|
||||||
|
📋 <b>订单信息</b>
|
||||||
|
├ 交易号:<code>%s</code>
|
||||||
|
├ 订单号:<code>%s</code>
|
||||||
|
└ 钱包地址:<code>%s</code>
|
||||||
|
|
||||||
|
⏰ <b>时间信息</b>
|
||||||
|
├ 创建时间:%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)
|
telegram.SendToBot(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func OrderCallbackHandle(ctx context.Context, t *asynq.Task) error {
|
|||||||
OrderId: order.OrderId,
|
OrderId: order.OrderId,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
ActualAmount: order.ActualAmount,
|
ActualAmount: order.ActualAmount,
|
||||||
|
ReceiveAddress: order.ReceiveAddress,
|
||||||
Token: order.Token,
|
Token: order.Token,
|
||||||
BlockTransactionId: order.BlockTransactionId,
|
BlockTransactionId: order.BlockTransactionId,
|
||||||
Status: mdb.StatusPaySuccess,
|
Status: mdb.StatusPaySuccess,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func OrderExpirationHandle(ctx context.Context, t *asynq.Task) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = data.UnLockTransaction(orderInfo.Token, orderInfo.ActualAmount)
|
err = data.UnLockTransaction(orderInfo.ReceiveAddress, orderInfo.Token, orderInfo.ActualAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -1,12 +1,20 @@
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
import "github.com/robfig/cron/v3"
|
import (
|
||||||
|
"github.com/assimon/luuu/util/log"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
func Start() {
|
func Start() {
|
||||||
|
log.Sugar.Info("[task] Starting task scheduler...")
|
||||||
c := cron.New()
|
c := cron.New()
|
||||||
// 汇率监听
|
|
||||||
c.AddJob("@every 60s", UsdtRateJob{})
|
|
||||||
// trc20钱包监听
|
// 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()
|
c.Start()
|
||||||
|
log.Sugar.Info("[task] Task scheduler started")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/assimon/luuu/model/data"
|
"github.com/assimon/luuu/model/data"
|
||||||
"github.com/assimon/luuu/model/service"
|
"github.com/assimon/luuu/model/service"
|
||||||
"github.com/assimon/luuu/util/log"
|
"github.com/assimon/luuu/util/log"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListenTrc20Job struct {
|
type ListenTrc20Job struct {
|
||||||
@@ -15,18 +16,24 @@ var gListenTrc20JobLock sync.Mutex
|
|||||||
func (r ListenTrc20Job) Run() {
|
func (r ListenTrc20Job) Run() {
|
||||||
gListenTrc20JobLock.Lock()
|
gListenTrc20JobLock.Lock()
|
||||||
defer gListenTrc20JobLock.Unlock()
|
defer gListenTrc20JobLock.Unlock()
|
||||||
|
log.Sugar.Debug("[ListenTrc20Job] Job triggered")
|
||||||
walletAddress, err := data.GetAvailableWalletAddress()
|
walletAddress, err := data.GetAvailableWalletAddress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Sugar.Error(err)
|
log.Sugar.Errorf("[ListenTrc20Job] Failed to get wallet addresses: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(walletAddress) <= 0 {
|
if len(walletAddress) <= 0 {
|
||||||
|
log.Sugar.Debug("[ListenTrc20Job] No available wallet addresses")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Sugar.Infof("[ListenTrc20Job] Found %d wallet addresses to monitor", len(walletAddress))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, address := range walletAddress {
|
for _, address := range walletAddress {
|
||||||
|
log.Sugar.Infof("[ListenTrc20Job] Listening to address: %s", address.Address)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go service.Trc20CallBack(address.Token, &wg)
|
go service.Trc20CallBack(address.Address, &wg)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
log.Sugar.Debug("[ListenTrc20Job] Job completed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,8 +45,8 @@ func WalletList(c tb.Context) error {
|
|||||||
}
|
}
|
||||||
var temp []tb.InlineButton
|
var temp []tb.InlineButton
|
||||||
btnInfo := tb.InlineButton{
|
btnInfo := tb.InlineButton{
|
||||||
Unique: wallet.Token,
|
Unique: wallet.Address,
|
||||||
Text: fmt.Sprintf("%s[%s]", wallet.Token, status),
|
Text: fmt.Sprintf("%s[%s]", wallet.Address, status),
|
||||||
Data: strutil.MustString(wallet.ID),
|
Data: strutil.MustString(wallet.ID),
|
||||||
}
|
}
|
||||||
bots.Handle(&btnInfo, WalletInfo)
|
bots.Handle(&btnInfo, WalletInfo)
|
||||||
@@ -93,7 +93,7 @@ func WalletInfo(c tb.Context) error {
|
|||||||
bots.Handle(&disableBtn, DisableWallet)
|
bots.Handle(&disableBtn, DisableWallet)
|
||||||
bots.Handle(&delBtn, DelWallet)
|
bots.Handle(&delBtn, DelWallet)
|
||||||
bots.Handle(&backBtn, WalletList)
|
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,
|
enableBtn,
|
||||||
disableBtn,
|
disableBtn,
|
||||||
|
|||||||
@@ -46,14 +46,21 @@ func RegisterHandle() {
|
|||||||
// SendToBot 主动发送消息机器人消息
|
// SendToBot 主动发送消息机器人消息
|
||||||
func SendToBot(msg string) {
|
func SendToBot(msg string) {
|
||||||
go func() {
|
go func() {
|
||||||
|
if bots == nil {
|
||||||
|
log.Sugar.Error("[Telegram] bots实例为nil,无法发送消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
user := tb.User{
|
user := tb.User{
|
||||||
ID: config.TgManage,
|
ID: config.TgManage,
|
||||||
}
|
}
|
||||||
|
log.Sugar.Infof("[Telegram] 正在发送消息给用户ID=%d", config.TgManage)
|
||||||
_, err := bots.Send(&user, msg, &tb.SendOptions{
|
_, err := bots.Send(&user, msg, &tb.SendOptions{
|
||||||
ParseMode: tb.ModeHTML,
|
ParseMode: tb.ModeHTML,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user