From cb0cacdcc58442b00c795b978a816ce0259ea9f4 Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 23 Jul 2026 11:27:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mysql/social_requirements_query.go | 5 +- .../internal/client/trongrid/client.go | 88 ++++++++++++++++--- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/services/statistics-service/internal/storage/mysql/social_requirements_query.go b/services/statistics-service/internal/storage/mysql/social_requirements_query.go index 4a18406a..14e578a9 100644 --- a/services/statistics-service/internal/storage/mysql/social_requirements_query.go +++ b/services/statistics-service/internal/storage/mysql/social_requirements_query.go @@ -1312,7 +1312,8 @@ func socialRequirementMetricDefinitions(section string) []MetricDefinition { default: return []MetricDefinition{ {Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"}, - {Metric: "registered_users", Definition: "UserRegistered 服务端事实去重人数。"}, + // registered_users 保留历史字段兼容,业务展示统一为“新增用户”;资格和幂等边界由 UserRegistered 生产、消费链路保证。 + {Metric: "registered_users", Definition: "注册成功的新用户人数:仅统计资料完成后由 user-service 发布的 UserRegistered 服务端事实,按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复。"}, {Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"}, } } @@ -1421,7 +1422,7 @@ func socialRequirementColumns(section string) []SocialRequirementColumn { default: return []SocialRequirementColumn{ {Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"}, - {Key: "registered_users", Label: "注册成功用户", Type: "count"}, + {Key: "registered_users", Label: "新增用户", Type: "count", Tooltip: "注册成功的新用户人数:资料完成后产生 UserRegistered 服务端事实并按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复"}, {Key: "new_host_users", Label: "新主播用户", Type: "count"}, {Key: "new_normal_users", Label: "新普通用户", Type: "count"}, {Key: "registration_success_rate", Label: "注册成功率", Type: "ratio"}, diff --git a/services/wallet-service/internal/client/trongrid/client.go b/services/wallet-service/internal/client/trongrid/client.go index 00a9082a..8891cbc8 100644 --- a/services/wallet-service/internal/client/trongrid/client.go +++ b/services/wallet-service/internal/client/trongrid/client.go @@ -1,9 +1,13 @@ package trongrid import ( + "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "io" + "math/big" "net/http" "net/url" "strconv" @@ -29,10 +33,11 @@ func New(cfg config.TronGridConfig) *Client { timeout = 10 * time.Second } return &Client{ - httpClient: &http.Client{Timeout: timeout}, - apiBaseURL: strings.TrimRight(strings.TrimSpace(cfg.APIBaseURL), "/"), - apiKey: strings.TrimSpace(cfg.APIKey), - contractAddress: strings.ToLower(strings.TrimSpace(cfg.USDTContractAddress)), + httpClient: &http.Client{Timeout: timeout}, + apiBaseURL: strings.TrimRight(strings.TrimSpace(cfg.APIBaseURL), "/"), + apiKey: strings.TrimSpace(cfg.APIKey), + // Base58Check 区分大小写;保存原始形式,比较时再统一解码为账户 Hex。 + contractAddress: strings.TrimSpace(cfg.USDTContractAddress), } } @@ -42,10 +47,13 @@ func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddres return "", xerr.New(xerr.Unavailable, "tron grid client is not configured") } txHash = strings.TrimSpace(txHash) - toAddress = strings.ToLower(strings.TrimSpace(toAddress)) if txHash == "" || toAddress == "" || amountMinor <= 0 { return "", xerr.New(xerr.InvalidArgument, "usdt transfer verification request is incomplete") } + expectedAddress, ok := normalizeTronAddress(toAddress) + if !ok { + return "", xerr.New(xerr.InvalidArgument, "tron receive address is invalid") + } // H5 商品金额以 USD cent 保存,USDT 使用 6 位小数;1 cent = 0.01 USDT = 10,000 atomic units。 requiredAtomicAmount := amountMinor * 10_000 events, rawJSON, err := c.listTransferEvents(ctx, txHash) @@ -59,7 +67,10 @@ func (c *Client) VerifyUSDTTransfer(ctx context.Context, txHash string, toAddres if !c.contractMatches(event.ContractAddress) { continue } - if strings.ToLower(strings.TrimSpace(event.Result.To)) != toAddress { + // TronGrid 的 event result.to 可能返回 0x/41 Hex,而后台保存的是 Base58Check。 + // 两端先归一为去除 TRON 网络前缀的 20-byte Hex,避免同一地址因编码形式不同被误判。 + eventAddress, validAddress := normalizeTronAddress(event.Result.To) + if !validAddress || eventAddress != expectedAddress { continue } value, err := strconv.ParseInt(strings.TrimSpace(event.Result.Value), 10, 64) @@ -102,15 +113,66 @@ func (c *Client) listTransferEvents(ctx context.Context, txHash string) ([]trans } func (c *Client) contractMatches(value string) bool { - value = strings.ToLower(strings.TrimSpace(value)) - if value == "" || c.contractAddress == "" { - return false + configured, configuredOK := normalizeTronAddress(c.contractAddress) + actual, actualOK := normalizeTronAddress(value) + return configuredOK && actualOK && configured == actual +} + +const tronBase58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +// normalizeTronAddress 接受后台常用的 Base58Check,以及 TronGrid 可能返回的 +// 0x + 20-byte、41 + 20-byte 或裸 20-byte Hex,统一输出账户主体的 40 位小写 Hex。 +// Base58 输入必须通过网络前缀和双 SHA-256 checksum 校验,避免把格式正确但已损坏的地址用于放币判断。 +func normalizeTronAddress(value string) (string, bool) { + value = strings.TrimSpace(value) + if value == "" { + return "", false } - if value == c.contractAddress { - return true + hexValue := strings.TrimPrefix(strings.ToLower(value), "0x") + if len(hexValue) == 42 && strings.HasPrefix(hexValue, "41") { + hexValue = hexValue[2:] } - // TronGrid 的不同接口可能返回 base58 或 hex 合约地址;这里保留主网 USDT hex 兼容,避免官方返回格式差异导致误拒。 - return c.contractAddress == "tr7nhqjekqxgtci8q8zyk4pyl8otszgjlj6t" && value == "a614f803b6fd780986a42c78ec9c7f77e6ded13c" + if len(hexValue) == 40 { + if _, err := hex.DecodeString(hexValue); err == nil { + return hexValue, true + } + } + payload, ok := decodeTronBase58Check(value) + if !ok || len(payload) != 21 || payload[0] != 0x41 { + return "", false + } + return hex.EncodeToString(payload[1:]), true +} + +func decodeTronBase58Check(value string) ([]byte, bool) { + number := new(big.Int) + base := big.NewInt(58) + for _, char := range value { + index := strings.IndexRune(tronBase58Alphabet, char) + if index < 0 { + return nil, false + } + number.Mul(number, base) + number.Add(number, big.NewInt(int64(index))) + } + decoded := number.Bytes() + leadingZeroes := 0 + for leadingZeroes < len(value) && value[leadingZeroes] == '1' { + leadingZeroes++ + } + if leadingZeroes > 0 { + decoded = append(make([]byte, leadingZeroes), decoded...) + } + if len(decoded) < 5 { + return nil, false + } + payload, checksum := decoded[:len(decoded)-4], decoded[len(decoded)-4:] + firstHash := sha256.Sum256(payload) + secondHash := sha256.Sum256(firstHash[:]) + if !bytes.Equal(checksum, secondHash[:4]) { + return nil, false + } + return payload, true } type transferEvent struct {