566 lines
19 KiB
Go
566 lines
19 KiB
Go
package hostorg
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/integration/userclient"
|
||
"hyapp-admin-server/internal/integration/walletclient"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
)
|
||
|
||
const (
|
||
coinSellerStatusActive = "active"
|
||
coinSellerMerchantAssetType = "COIN_SELLER_COIN"
|
||
coinSellerStockTypePurchase = "usdt_purchase"
|
||
coinSellerStockTypeCompensate = "coin_compensation"
|
||
paidCurrencyUSDT = "USDT"
|
||
sortByDiamond = "diamond"
|
||
sortByMerchantBalance = "merchant_balance"
|
||
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||
bdLeaderPositionAliasMaxRunes = 64
|
||
)
|
||
|
||
type Service struct {
|
||
userClient userclient.Client
|
||
walletClient walletclient.Client
|
||
reader *Reader
|
||
}
|
||
|
||
func NewService(userClient userclient.Client, walletClient walletclient.Client, reader *Reader) *Service {
|
||
return &Service{userClient: userClient, walletClient: walletClient, reader: reader}
|
||
}
|
||
|
||
func (s *Service) ListBDLeaders(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) {
|
||
query = normalizeListQuery(query)
|
||
return s.reader.ListBDProfiles(ctx, query, "bd_leader")
|
||
}
|
||
|
||
func (s *Service) ListBDs(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) {
|
||
query = normalizeListQuery(query)
|
||
return s.reader.ListBDProfiles(ctx, query, "bd")
|
||
}
|
||
|
||
func (s *Service) ListAgencies(ctx context.Context, query listQuery) ([]*userclient.Agency, int64, error) {
|
||
query = normalizeListQuery(query)
|
||
return s.reader.ListAgencies(ctx, query)
|
||
}
|
||
|
||
func (s *Service) ListHosts(ctx context.Context, query listQuery) ([]*userclient.HostProfile, int64, error) {
|
||
query = normalizeListQuery(query)
|
||
return s.reader.ListHostProfiles(ctx, query)
|
||
}
|
||
|
||
func (s *Service) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinSellerListItem, int64, error) {
|
||
query = normalizeListQuery(query)
|
||
return s.reader.ListCoinSellers(ctx, query)
|
||
}
|
||
|
||
func (s *Service) GetCoinSellerSalaryRates(ctx context.Context, regionID int64) ([]CoinSellerSalaryRateTier, error) {
|
||
return s.reader.ListCoinSellerSalaryRates(ctx, regionID)
|
||
}
|
||
|
||
func (s *Service) ReplaceCoinSellerSalaryRates(ctx context.Context, actorID int64, regionID int64, req replaceCoinSellerSalaryRatesRequest) ([]CoinSellerSalaryRateTier, error) {
|
||
tiers := make([]CoinSellerSalaryRateTier, 0, len(req.Tiers))
|
||
for index, item := range req.Tiers {
|
||
minUSDMinor, err := salaryRateUSDMinor(item.MinUSDMinor, item.MinUSD)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("min_usd is invalid")
|
||
}
|
||
maxUSDMinor, err := salaryRateUSDMinor(item.MaxUSDMinor, item.MaxUSD)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("max_usd is invalid")
|
||
}
|
||
enabled := item.Status == "active"
|
||
if item.Enabled != nil {
|
||
enabled = *item.Enabled
|
||
}
|
||
status := strings.ToLower(strings.TrimSpace(item.Status))
|
||
if status == "" {
|
||
if enabled {
|
||
status = "active"
|
||
} else {
|
||
status = "disabled"
|
||
}
|
||
}
|
||
if status != "active" && status != "disabled" {
|
||
return nil, fmt.Errorf("status is invalid")
|
||
}
|
||
if minUSDMinor <= 0 || maxUSDMinor < minUSDMinor {
|
||
return nil, fmt.Errorf("usd range is invalid")
|
||
}
|
||
if item.CoinPerUSD <= 0 || item.CoinPerUSD%100 != 0 {
|
||
return nil, fmt.Errorf("coin_per_usd must be a positive multiple of 100")
|
||
}
|
||
sortOrder := item.SortOrder
|
||
if sortOrder == 0 {
|
||
sortOrder = (index + 1) * 10
|
||
}
|
||
tiers = append(tiers, CoinSellerSalaryRateTier{
|
||
RegionID: regionID,
|
||
MinUSDMinor: minUSDMinor,
|
||
MaxUSDMinor: maxUSDMinor,
|
||
MinUSD: formatUSDMinor(minUSDMinor),
|
||
MaxUSD: formatUSDMinor(maxUSDMinor),
|
||
CoinPerUSD: item.CoinPerUSD,
|
||
Status: status,
|
||
Enabled: status == "active",
|
||
SortOrder: sortOrder,
|
||
})
|
||
}
|
||
if err := validateCoinSellerSalaryRateTiers(tiers); err != nil {
|
||
return nil, err
|
||
}
|
||
return s.reader.ReplaceCoinSellerSalaryRates(ctx, regionID, actorID, tiers)
|
||
}
|
||
|
||
func (s *Service) CreateBDLeader(ctx context.Context, actorID int64, requestID string, req createBDLeaderRequest) (*userclient.BDProfile, error) {
|
||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// BD Leader 区域由目标用户当前 users.region_id 派生;后台不再传区域,避免创建角色时顺手迁移用户国家/区域。
|
||
profile, err := s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
TargetUserID: targetUserID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 职位别名只影响 Flutter H5 配置里 admin 入口的展示名,不参与 user-service 的身份创建事务,避免把后台展示字段扩散到 App 身份主协议。
|
||
if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
|
||
return nil, err
|
||
}
|
||
profile.PositionAlias = positionAlias
|
||
return profile, nil
|
||
}
|
||
|
||
func (s *Service) UpdateBDLeaderPositionAlias(ctx context.Context, actorID int64, targetUserID int64, req bdLeaderPositionAliasRequest) (*userclient.BDProfile, error) {
|
||
if targetUserID <= 0 {
|
||
return nil, fmt.Errorf("target_user_id is required")
|
||
}
|
||
positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
profile, err := s.reader.GetBDLeader(ctx, targetUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 行内编辑只修改后台维护的展示别名;先确认 BD Leader 身份存在,再写覆盖表,避免产生无法在列表里追踪的孤立别名。
|
||
if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
|
||
return nil, err
|
||
}
|
||
profile.PositionAlias = positionAlias
|
||
return profile, nil
|
||
}
|
||
|
||
func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string, req createBDRequest) (*userclient.BDProfile, error) {
|
||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parentLeaderUserID := int64(0)
|
||
if req.ParentLeaderUserID < 0 {
|
||
return nil, fmt.Errorf("parent_leader_user_id must not be negative")
|
||
}
|
||
if req.ParentLeaderUserID > 0 {
|
||
// 父级 Leader 是可选关系;传入短 ID 时才解析成内部 user_id,未传时保留 0 表示独立 BD。
|
||
parentLeaderUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentLeaderUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return s.userClient.CreateBD(ctx, userclient.CreateBDRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
TargetUserID: targetUserID,
|
||
ParentLeaderUserID: parentLeaderUserID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func (s *Service) SetBDStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, role string, req bdStatusRequest) (*userclient.BDProfile, error) {
|
||
return s.userClient.SetBDStatus(ctx, userclient.SetBDStatusRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
TargetUserID: targetUserID,
|
||
Role: role,
|
||
Status: strings.TrimSpace(req.Status),
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func (s *Service) DeleteBDLeader(ctx context.Context, targetUserID int64) (*userclient.BDProfile, error) {
|
||
if targetUserID <= 0 {
|
||
return nil, fmt.Errorf("target_user_id is required")
|
||
}
|
||
return s.reader.DeleteBDLeader(ctx, targetUserID)
|
||
}
|
||
|
||
func (s *Service) CreateCoinSeller(ctx context.Context, actorID int64, requestID string, req createCoinSellerRequest) (*userclient.CoinSellerProfile, error) {
|
||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
profile, err := s.userClient.CreateCoinSeller(ctx, userclient.CreateCoinSellerRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
TargetUserID: targetUserID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
contact := firstNonBlank(req.Contact, req.Reason)
|
||
if contact != "" {
|
||
if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, contact); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
func (s *Service) SetCoinSellerStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, req coinSellerStatusRequest) (*userclient.CoinSellerProfile, error) {
|
||
profile, err := s.userClient.SetCoinSellerStatus(ctx, userclient.SetCoinSellerStatusRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
TargetUserID: targetUserID,
|
||
Status: strings.TrimSpace(req.Status),
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if req.Contact != nil {
|
||
if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, *req.Contact); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockCreditRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
|
||
if s.walletClient == nil {
|
||
return nil, fmt.Errorf("wallet client is not configured")
|
||
}
|
||
if sellerUserID <= 0 || actorID <= 0 {
|
||
return nil, fmt.Errorf("seller_user_id and operator_user_id are required")
|
||
}
|
||
commandID := strings.TrimSpace(req.CommandID)
|
||
stockType := strings.ToLower(strings.TrimSpace(req.Type))
|
||
evidenceRef := strings.TrimSpace(req.EvidenceRef)
|
||
reason := strings.TrimSpace(req.Reason)
|
||
if commandID == "" || req.CoinAmount <= 0 {
|
||
return nil, fmt.Errorf("command_id and coin_amount are required")
|
||
}
|
||
|
||
paidCurrencyCode := ""
|
||
paidAmountMicro := int64(0)
|
||
paymentRef := strings.TrimSpace(req.PaymentRef)
|
||
switch stockType {
|
||
case coinSellerStockTypePurchase:
|
||
paidCurrencyCode = paidCurrencyUSDT
|
||
parsed, err := parseUSDTAmountMicro(req.RechargeAmount)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
paidAmountMicro = parsed
|
||
case coinSellerStockTypeCompensate:
|
||
if strings.TrimSpace(req.RechargeAmount) != "" || paymentRef != "" {
|
||
return nil, fmt.Errorf("coin compensation must not include recharge_amount or payment_ref")
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("stock type is invalid")
|
||
}
|
||
|
||
profile, err := s.userClient.GetCoinSellerProfile(ctx, userclient.GetCoinSellerProfileRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
UserID: sellerUserID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if profile == nil || profile.Status != coinSellerStatusActive || profile.MerchantAssetType != coinSellerMerchantAssetType {
|
||
return nil, fmt.Errorf("target user is not an active coin seller")
|
||
}
|
||
seller, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
UserID: sellerUserID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if seller == nil || seller.CountryID <= 0 || seller.RegionID <= 0 {
|
||
return nil, fmt.Errorf("coin seller country and region are required")
|
||
}
|
||
|
||
return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
|
||
CommandId: commandID,
|
||
SellerUserId: sellerUserID,
|
||
SellerCountryId: seller.CountryID,
|
||
SellerRegionId: seller.RegionID,
|
||
StockType: stockType,
|
||
CoinAmount: req.CoinAmount,
|
||
PaidCurrencyCode: paidCurrencyCode,
|
||
PaidAmountMicro: paidAmountMicro,
|
||
PaymentRef: paymentRef,
|
||
EvidenceRef: evidenceRef,
|
||
OperatorUserId: actorID,
|
||
Reason: reason,
|
||
AppCode: appctx.FromContext(ctx),
|
||
})
|
||
}
|
||
|
||
func (s *Service) CreateAgency(ctx context.Context, actorID int64, requestID string, req createAgencyRequest) (*userclient.CreateAgencyResult, error) {
|
||
ownerUserID, err := s.resolveDisplayUserID(ctx, requestID, req.OwnerUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parentBDUserID := int64(0)
|
||
if req.ParentBDUserID < 0 {
|
||
return nil, fmt.Errorf("parent_bd_user_id must not be negative")
|
||
}
|
||
if req.ParentBDUserID > 0 {
|
||
// 父级 BD 是可选关系;传入短 ID 时才解析成内部 user_id,未传时保留 0 表示独立 Agency。
|
||
parentBDUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentBDUserID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
joinEnabled := false
|
||
if req.JoinEnabled != nil {
|
||
joinEnabled = *req.JoinEnabled
|
||
}
|
||
return s.userClient.CreateAgency(ctx, userclient.CreateAgencyRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
OwnerUserID: ownerUserID,
|
||
ParentBDUserID: parentBDUserID,
|
||
Name: strings.TrimSpace(req.Name),
|
||
JoinEnabled: joinEnabled,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func (s *Service) CloseAgency(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyCloseRequest) (*userclient.Agency, error) {
|
||
return s.userClient.CloseAgency(ctx, userclient.CloseAgencyRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
AgencyID: agencyID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func (s *Service) DeleteAgency(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyDeleteRequest) (*userclient.Agency, error) {
|
||
return s.userClient.DeleteAgency(ctx, userclient.DeleteAgencyRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
AgencyID: agencyID,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func (s *Service) resolveDisplayUserID(ctx context.Context, requestID string, displayUserID int64) (int64, error) {
|
||
if displayUserID <= 0 {
|
||
return 0, fmt.Errorf("display_user_id is required")
|
||
}
|
||
identity, err := s.userClient.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
DisplayUserID: strconv.FormatInt(displayUserID, 10),
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if identity == nil || identity.UserID <= 0 {
|
||
return 0, fmt.Errorf("display_user_id %d not found", displayUserID)
|
||
}
|
||
return identity.UserID, nil
|
||
}
|
||
|
||
func normalizeBDLeaderPositionAlias(value string) (string, error) {
|
||
positionAlias := strings.TrimSpace(value)
|
||
if len([]rune(positionAlias)) > bdLeaderPositionAliasMaxRunes {
|
||
return "", fmt.Errorf("position_alias must not exceed %d characters", bdLeaderPositionAliasMaxRunes)
|
||
}
|
||
return positionAlias, nil
|
||
}
|
||
|
||
func (s *Service) SetAgencyJoinEnabled(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyJoinEnabledRequest) (*userclient.Agency, error) {
|
||
joinEnabled := false
|
||
if req.JoinEnabled != nil {
|
||
joinEnabled = *req.JoinEnabled
|
||
}
|
||
return s.userClient.SetAgencyJoinEnabled(ctx, userclient.SetAgencyJoinEnabledRequest{
|
||
RequestID: requestID,
|
||
Caller: "hyapp-admin-server",
|
||
CommandID: strings.TrimSpace(req.CommandID),
|
||
AdminUserID: actorID,
|
||
AgencyID: agencyID,
|
||
JoinEnabled: joinEnabled,
|
||
Reason: strings.TrimSpace(req.Reason),
|
||
})
|
||
}
|
||
|
||
func parseUSDTAmountMicro(raw string) (int64, error) {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return 0, fmt.Errorf("recharge_amount is required for usdt purchase")
|
||
}
|
||
parts := strings.Split(raw, ".")
|
||
if len(parts) > 2 || parts[0] == "" {
|
||
return 0, fmt.Errorf("recharge_amount is invalid")
|
||
}
|
||
whole := parts[0]
|
||
frac := ""
|
||
if len(parts) == 2 {
|
||
frac = parts[1]
|
||
if frac == "" || len(frac) > 6 {
|
||
return 0, fmt.Errorf("recharge_amount supports up to 6 decimals")
|
||
}
|
||
}
|
||
digits := whole + frac + strings.Repeat("0", 6-len(frac))
|
||
var amount int64
|
||
const maxInt64 = int64(^uint64(0) >> 1)
|
||
for _, char := range digits {
|
||
if char < '0' || char > '9' {
|
||
return 0, fmt.Errorf("recharge_amount is invalid")
|
||
}
|
||
next := int64(char - '0')
|
||
if amount > (maxInt64-next)/10 {
|
||
return 0, fmt.Errorf("recharge_amount is too large")
|
||
}
|
||
amount = amount*10 + next
|
||
}
|
||
if amount <= 0 {
|
||
return 0, fmt.Errorf("recharge_amount must be positive")
|
||
}
|
||
return amount, nil
|
||
}
|
||
|
||
func salaryRateUSDMinor(minor int64, raw string) (int64, error) {
|
||
if minor > 0 {
|
||
return minor, nil
|
||
}
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return 0, fmt.Errorf("usd amount is required")
|
||
}
|
||
parts := strings.Split(raw, ".")
|
||
if len(parts) > 2 || parts[0] == "" {
|
||
return 0, fmt.Errorf("usd amount is invalid")
|
||
}
|
||
digits := parts[0]
|
||
fraction := ""
|
||
if len(parts) == 2 {
|
||
fraction = parts[1]
|
||
if len(fraction) > 2 {
|
||
return 0, fmt.Errorf("usd amount supports up to 2 decimals")
|
||
}
|
||
}
|
||
digits += fraction + strings.Repeat("0", 2-len(fraction))
|
||
var amount int64
|
||
const maxInt64 = int64(^uint64(0) >> 1)
|
||
for _, char := range digits {
|
||
if char < '0' || char > '9' {
|
||
return 0, fmt.Errorf("usd amount is invalid")
|
||
}
|
||
next := int64(char - '0')
|
||
if amount > (maxInt64-next)/10 {
|
||
return 0, fmt.Errorf("usd amount is too large")
|
||
}
|
||
amount = amount*10 + next
|
||
}
|
||
return amount, nil
|
||
}
|
||
|
||
func validateCoinSellerSalaryRateTiers(tiers []CoinSellerSalaryRateTier) error {
|
||
active := make([]CoinSellerSalaryRateTier, 0, len(tiers))
|
||
for _, tier := range tiers {
|
||
if tier.Status == "active" {
|
||
active = append(active, tier)
|
||
}
|
||
}
|
||
for i := 0; i < len(active); i++ {
|
||
for j := i + 1; j < len(active); j++ {
|
||
if active[i].MinUSDMinor <= active[j].MaxUSDMinor && active[j].MinUSDMinor <= active[i].MaxUSDMinor {
|
||
return fmt.Errorf("active usd ranges must not overlap")
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func formatUSDMinor(value int64) string {
|
||
sign := ""
|
||
if value < 0 {
|
||
sign = "-"
|
||
value = -value
|
||
}
|
||
return fmt.Sprintf("%s%d.%02d", sign, value/100, value%100)
|
||
}
|
||
|
||
func normalizeListQuery(query listQuery) listQuery {
|
||
if query.Page < 1 {
|
||
query.Page = 1
|
||
}
|
||
if query.PageSize < 1 {
|
||
query.PageSize = 20
|
||
}
|
||
if query.PageSize > 100 {
|
||
query.PageSize = 100
|
||
}
|
||
query.Keyword = strings.TrimSpace(query.Keyword)
|
||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||
query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
|
||
switch query.SortBy {
|
||
case sortByDiamond, sortByMerchantBalance, sortByTotalRechargeUSDT:
|
||
default:
|
||
query.SortBy = ""
|
||
query.SortDirection = ""
|
||
return query
|
||
}
|
||
query.SortDirection = strings.ToLower(strings.TrimSpace(query.SortDirection))
|
||
if query.SortDirection != "asc" {
|
||
query.SortDirection = "desc"
|
||
}
|
||
return query
|
||
}
|
||
|
||
func firstNonBlank(values ...string) string {
|
||
for _, value := range values {
|
||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||
return trimmed
|
||
}
|
||
}
|
||
return ""
|
||
}
|