feat(admin): expose app-scoped USDT addresses

This commit is contained in:
zhx 2026-07-15 10:39:13 +08:00
parent 892e237be5
commit 51b1ee7481
4 changed files with 150 additions and 0 deletions

View File

@ -101,6 +101,15 @@ func (h *Handler) GetCoinSellerRechargeExchangeRate(c *gin.Context) {
response.OK(c, item) response.OK(c, item)
} }
func (h *Handler) GetUSDTAddresses(c *gin.Context) {
item, err := h.service.GetUSDTAddresses(shared.ActorFromContext(c), c.Param("app_code"))
if err != nil {
writeServiceError(c, err, "获取 USDT 收款地址失败")
return
}
response.OK(c, item)
}
func (h *Handler) ReplaceCoinSellerRechargeExchangeRate(c *gin.Context) { func (h *Handler) ReplaceCoinSellerRechargeExchangeRate(c *gin.Context) {
var req coinSellerRechargeExchangeRatePolicyRequest var req coinSellerRechargeExchangeRatePolicyRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -17,6 +17,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.POST("/admin/finance/orders/coin-seller-recharges", middleware.RequirePermission(permissionCoinSellerRechargeCreate), h.CreateCoinSellerRechargeOrder) protected.POST("/admin/finance/orders/coin-seller-recharges", middleware.RequirePermission(permissionCoinSellerRechargeCreate), h.CreateCoinSellerRechargeOrder)
protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/verify", middleware.RequirePermission(permissionCoinSellerRechargeVerify), h.VerifyCoinSellerRechargeOrder) protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/verify", middleware.RequirePermission(permissionCoinSellerRechargeVerify), h.VerifyCoinSellerRechargeOrder)
protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/grant", middleware.RequirePermission(permissionCoinSellerRechargeGrant), h.GrantCoinSellerRechargeOrder) protected.POST("/admin/finance/orders/coin-seller-recharges/:order_id/grant", middleware.RequirePermission(permissionCoinSellerRechargeGrant), h.GrantCoinSellerRechargeOrder)
protected.GET("/admin/finance/usdt-addresses/:app_code", middleware.RequirePermission(permissionCoinSellerRechargeView), h.GetUSDTAddresses)
protected.GET("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.GetCoinSellerRechargeExchangeRate) protected.GET("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.GetCoinSellerRechargeExchangeRate)
protected.PUT("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.ReplaceCoinSellerRechargeExchangeRate) protected.PUT("/admin/finance/coin-seller-recharge-exchange-rates/:app_code", middleware.RequirePermission(permissionCoinSellerExchangeRate), h.ReplaceCoinSellerRechargeExchangeRate)
} }

View File

@ -0,0 +1,112 @@
package financeorder
import (
"errors"
"fmt"
"sort"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/modules/shared"
)
const financeUSDTAddressGroup = "finance-usdt-address"
var financeUSDTChainOrder = map[string]int{
"TRON": 0,
"BSC": 1,
}
type financeUSDTAddressDTO struct {
Chain string `json:"chain"`
Address string `json:"address"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type financeUSDTAddressesDTO struct {
AppCode string `json:"appCode"`
Items []financeUSDTAddressDTO `json:"items"`
}
// GetUSDTAddresses 只读取当前 App 自己持有的配置行,不继承空 app_code 的历史基线。
// 收款地址一旦跨 App 回退,运营会拿到另一账套的地址并让链上订单校验必然失败。
func (s *Service) GetUSDTAddresses(actor shared.Actor, appCode string) (*financeUSDTAddressesDTO, error) {
if s == nil || s.store == nil {
return nil, errors.New("admin store is not configured")
}
if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionCoinSellerRechargeView) {
return nil, errCoinSellerRechargeForbidden
}
if strings.TrimSpace(appCode) == "" {
return nil, errInvalidCoinSellerRechargeOrderInput
}
appCode = appctx.Normalize(appCode)
// App 范围是后台用户的数据边界;即使调用方绕过页面手写 app_code也不能读取未授权账套地址。
access, err := s.store.AppAccessForUser(actor.UserID)
if err != nil {
return nil, err
}
if !access.Allows(appCode) {
return nil, errCoinSellerRechargeForbidden
}
configs, err := s.store.ListOwnedAppConfigs(appCode, financeUSDTAddressGroup)
if err != nil {
return nil, err
}
items, err := financeUSDTAddressesFromConfigs(configs)
if err != nil {
return nil, err
}
return &financeUSDTAddressesDTO{AppCode: appCode, Items: items}, nil
}
func financeUSDTAddressesFromConfigs(configs []model.AppConfig) ([]financeUSDTAddressDTO, error) {
items := make([]financeUSDTAddressDTO, 0, len(configs))
for _, config := range configs {
chain := strings.ToUpper(strings.TrimSpace(config.Key))
if _, supported := financeUSDTChainOrder[chain]; !supported {
// 同组未来若新增其他链,旧版本必须安全忽略,不能把未知地址误标成 TRON/BSC。
continue
}
address := strings.TrimSpace(config.Value)
if address == "" {
continue
}
if !validFinanceUSDTAddress(chain, address) {
return nil, fmt.Errorf("%s USDT 收款地址配置不正确", chain)
}
items = append(items, financeUSDTAddressDTO{Chain: chain, Address: address, UpdatedAtMS: config.UpdatedAtMS})
}
sort.Slice(items, func(i, j int) bool {
return financeUSDTChainOrder[items[i].Chain] < financeUSDTChainOrder[items[j].Chain]
})
return items, nil
}
func validFinanceUSDTAddress(chain string, address string) bool {
switch chain {
case "TRON":
if len(address) != 34 || address[0] != 'T' {
return false
}
for _, char := range address {
if !strings.ContainsRune("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", char) {
return false
}
}
return true
case "BSC":
if len(address) != 42 || !strings.HasPrefix(address, "0x") {
return false
}
for _, char := range address[2:] {
if !strings.ContainsRune("0123456789abcdefABCDEF", char) {
return false
}
}
return true
default:
return false
}
}

View File

@ -0,0 +1,28 @@
package financeorder
import (
"testing"
"hyapp-admin-server/internal/model"
)
func TestFinanceUSDTAddressesFromConfigsKeepsSupportedValidChainsInBusinessOrder(t *testing.T) {
items, err := financeUSDTAddressesFromConfigs([]model.AppConfig{
{Key: "BSC", Value: "0x55d398326f99059fF775485246999027B3197955", UpdatedAtMS: 200},
{Key: "future-chain", Value: "not-exposed", UpdatedAtMS: 300},
{Key: "TRON", Value: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", UpdatedAtMS: 100},
})
if err != nil {
t.Fatalf("normalize USDT address configs failed: %v", err)
}
if len(items) != 2 || items[0].Chain != "TRON" || items[1].Chain != "BSC" {
t.Fatalf("USDT addresses must expose only supported chains in business order: %+v", items)
}
}
func TestFinanceUSDTAddressesFromConfigsRejectsMalformedStoredAddress(t *testing.T) {
_, err := financeUSDTAddressesFromConfigs([]model.AppConfig{{Key: "TRON", Value: "wrong-app-address"}})
if err == nil {
t.Fatal("malformed SQL address must fail closed instead of being copied into a recharge order")
}
}