199 lines
6.4 KiB
Go
199 lines
6.4 KiB
Go
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
|
||
}
|
||
}
|