199 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package financeorder
import (
"errors"
"fmt"
"sort"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/modules/shared"
"gorm.io/gorm"
)
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"`
}
type financeUSDTAddressUpsertRequest struct {
Address string `json:"address"`
}
type financeUSDTAddressUpsertDTO struct {
AppCode string `json:"appCode"`
Chain string `json:"chain"`
Address string `json:"address"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
// 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
}
// UpsertUSDTAddress 把收款地址写入现有 app-scoped 配置表;唯一键确保更新是单行 upsert
// 不新增地址历史副本,也不会覆盖其他 App。生产查单会在每次校验前重新读取该行。
func (s *Service) UpsertUSDTAddress(actor shared.Actor, appCode string, chain string, req financeUSDTAddressUpsertRequest) (*financeUSDTAddressUpsertDTO, error) {
if s == nil || s.store == nil {
return nil, errors.New("admin store is not configured")
}
if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionUSDTAddressUpdate) {
return nil, errCoinSellerRechargeForbidden
}
if strings.TrimSpace(appCode) == "" {
return nil, errInvalidCoinSellerRechargeOrderInput
}
appCode = appctx.Normalize(appCode)
chain = strings.ToUpper(strings.TrimSpace(chain))
address := strings.TrimSpace(req.Address)
if _, supported := financeUSDTChainOrder[chain]; !supported {
return nil, errors.New("USDT 链不正确")
}
if !validFinanceUSDTAddress(chain, address) {
return nil, errors.New("USDT 收款地址不正确")
}
access, err := s.store.AppAccessForUser(actor.UserID)
if err != nil {
return nil, err
}
if !access.Allows(appCode) {
return nil, errCoinSellerRechargeForbidden
}
if err := s.store.UpsertScopedAppConfigs(appCode, []model.AppConfig{{
Group: financeUSDTAddressGroup,
Key: chain,
Value: address,
Description: "财务 USDT 收款地址",
}}); err != nil {
return nil, err
}
item, err := s.store.GetOwnedAppConfig(appCode, financeUSDTAddressGroup, chain)
if err != nil {
return nil, err
}
return &financeUSDTAddressUpsertDTO{AppCode: appCode, Chain: chain, Address: item.Value, UpdatedAtMS: item.UpdatedAtMS}, nil
}
// financeUSDTReceiveAddress 把订单校验链映射到当前 SQL 地址。Off-chain 虽没有公链 hash
// 但 Binance Deposit History 仍记录实际 TRX 收款地址,因此必须复用 TRON 配置C2C Pay 则没有地址。
func (s *Service) financeUSDTReceiveAddress(appCode string, receiptChain string) (string, error) {
addressChain := ""
switch strings.ToUpper(strings.TrimSpace(receiptChain)) {
case "TRON":
addressChain = "TRON"
case "BSC":
addressChain = "BSC"
case "OFFCHAIN":
addressChain = "TRON"
case "", "C2C":
return "", nil
default:
return "", errors.New("USDT 链不正确")
}
item, err := s.store.GetOwnedAppConfig(appctx.Normalize(appCode), financeUSDTAddressGroup, addressChain)
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", fmt.Errorf("当前 APP 未配置 %s USDT 收款地址", addressChain)
}
if err != nil {
return "", err
}
address := strings.TrimSpace(item.Value)
if !validFinanceUSDTAddress(addressChain, address) {
return "", fmt.Errorf("%s USDT 收款地址配置不正确", addressChain)
}
return address, 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
}
}