package wallet import ( "context" "errors" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" "hyapp/services/wallet-service/internal/service/wallet/ports" "strings" ) type mifaPayProviderPayloadError interface { ProviderPayloadJSON() string } // CreateH5RechargeOrder 创建 H5 外部充值订单;只有回调或链上校验成功后才会入账。 func (s *Service) CreateH5RechargeOrder(ctx context.Context, command ledger.CreateExternalRechargeOrderCommand) (ledger.ExternalRechargeOrder, error) { command = normalizeCreateExternalRechargeOrderCommand(command) if err := validateCreateExternalRechargeOrderCommand(command); err != nil { return ledger.ExternalRechargeOrder{}, err } if s.repository == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, command.AppCode) product, err := s.repository.GetRechargeProduct(ctx, command.AppCode, command.ProductID) if err != nil { return ledger.ExternalRechargeOrder{}, err } if err := validateExternalRechargeProduct(product, command); err != nil { return ledger.ExternalRechargeOrder{}, err } switch command.ProviderCode { case ledger.PaymentProviderUSDTTRC20: return s.createUSDTRechargeOrder(ctx, command, product) case ledger.PaymentProviderMifaPay: return s.createMifaPayRechargeOrder(ctx, command, product) case ledger.PaymentProviderV5Pay: return s.createV5PayRechargeOrder(ctx, command, product) default: return ledger.ExternalRechargeOrder{}, xerr.New(xerr.InvalidArgument, "payment provider is invalid") } } func (s *Service) createUSDTRechargeOrder(ctx context.Context, command ledger.CreateExternalRechargeOrderCommand, product ledger.RechargeProduct) (ledger.ExternalRechargeOrder, error) { receiveAddress := s.externalRecharge.usdtTRC20AddressForApp(command.AppCode) if !s.externalRecharge.USDTTRC20Enabled || receiveAddress == "" { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "usdt trc20 recharge is not configured") } return s.repository.CreateExternalRechargeOrder(ctx, command, product, nil, amountMicroToUSDMinor(product.AmountMicro), receiveAddress) } func (s *Service) createMifaPayRechargeOrder(ctx context.Context, command ledger.CreateExternalRechargeOrderCommand, product ledger.RechargeProduct) (ledger.ExternalRechargeOrder, error) { if s.mifaPay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured") } method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err } if err := validateMifaPayMethodForOrder(method, command); err != nil { return ledger.ExternalRechargeOrder{}, err } providerAmountMinor, err := calculateMifaPayProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode) if err != nil { return ledger.ExternalRechargeOrder{}, err } order, err := s.repository.CreateExternalRechargeOrder(ctx, command, product, &method, providerAmountMinor, "") if err != nil || order.IdempotentReplay { return order, err } notifyURL := firstNonEmpty(command.NotifyURL, s.externalRecharge.MifaPayNotifyURL) returnURL := firstNonEmpty(command.ReturnURL, s.externalRecharge.MifaPayReturnURL) if notifyURL == "" { _, _ = s.repository.MarkExternalRechargeOrderFailed(ctx, command.AppCode, order.OrderID, "mifapay notify_url is not configured", "") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay notify_url is not configured") } resp, err := s.mifaPay.CreateOrder(ctx, MifaPayCreateOrderRequest{ OrderID: order.OrderID, TargetUserID: order.TargetUserID, ProductName: order.ProductName, CoinAmount: order.CoinAmount, AmountMinor: order.USDMinorAmount, CurrencyCode: order.CurrencyCode, CountryCode: order.CountryCode, PayWay: order.PayWay, PayType: order.PayType, NotifyURL: notifyURL, ReturnURL: mifaPayProviderReturnURL(returnURL), ClientIP: command.ClientIP, Language: command.Language, ProviderAmountMinor: providerAmountMinor, PayerName: command.PayerName, PayerAccount: command.PayerAccount, PayerEmail: command.PayerEmail, }) if err != nil { _, _ = s.repository.MarkExternalRechargeOrderFailed(ctx, command.AppCode, order.OrderID, err.Error(), mifaPayProviderPayloadJSON(err)) return ledger.ExternalRechargeOrder{}, normalizeMifaPayOrderError(err) } return s.repository.MarkExternalRechargeOrderRedirected(ctx, command.AppCode, order.OrderID, resp.ProviderOrderID, resp.PayURL, resp.RawJSON) } func (s *Service) createV5PayRechargeOrder(ctx context.Context, command ledger.CreateExternalRechargeOrderCommand, product ledger.RechargeProduct) (ledger.ExternalRechargeOrder, error) { if s.v5Pay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured") } method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err } if err := validateV5PayMethodForOrder(method, command); err != nil { return ledger.ExternalRechargeOrder{}, err } providerAmountMinor, err := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode) if err != nil { return ledger.ExternalRechargeOrder{}, err } order, err := s.repository.CreateExternalRechargeOrder(ctx, command, product, &method, providerAmountMinor, "") if err != nil || order.IdempotentReplay { return order, err } notifyURL := firstNonEmpty(command.NotifyURL, s.externalRecharge.V5PayNotifyURL) returnURL := firstNonEmpty(command.ReturnURL, s.externalRecharge.V5PayReturnURL) if notifyURL == "" { _, _ = s.repository.MarkExternalRechargeOrderFailed(ctx, command.AppCode, order.OrderID, "v5pay notify_url is not configured", "") return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay notify_url is not configured") } resp, err := s.v5Pay.CreateOrder(ctx, V5PayCreateOrderRequest{ OrderID: order.OrderID, TargetUserID: order.TargetUserID, ProductName: order.ProductName, CoinAmount: order.CoinAmount, AmountMinor: order.USDMinorAmount, CurrencyCode: order.CurrencyCode, CountryCode: order.CountryCode, ProductType: order.PayType, NotifyURL: notifyURL, ReturnURL: v5PayProviderReturnURL(returnURL), Language: command.Language, ProviderAmountMinor: providerAmountMinor, PayerName: command.PayerName, PayerAccount: command.PayerAccount, PayerEmail: command.PayerEmail, }) if err != nil { _, _ = s.repository.MarkExternalRechargeOrderFailed(ctx, command.AppCode, order.OrderID, err.Error(), providerPayloadJSON(err)) return ledger.ExternalRechargeOrder{}, normalizeExternalProviderOrderError(err, "v5pay") } return s.repository.MarkExternalRechargeOrderRedirected(ctx, command.AppCode, order.OrderID, resp.ProviderOrderID, resp.PayURL, resp.RawJSON) } func normalizeMifaPayOrderError(err error) error { return normalizeExternalProviderOrderError(err, "mifapay") } func mifaPayProviderPayloadJSON(err error) string { return providerPayloadJSON(err) } func normalizeExternalProviderOrderError(err error, provider string) error { if err == nil { return nil } if _, ok := xerr.As(err); ok { return err } return xerr.New(xerr.Unavailable, provider+" order upstream response is invalid") } func providerPayloadJSON(err error) string { var payloadErr mifaPayProviderPayloadError if errors.As(err, &payloadErr) { return strings.TrimSpace(payloadErr.ProviderPayloadJSON()) } return "" } // SubmitH5RechargeTx 校验用户提交的 TRON 链上哈希或 Binance 站内转账号,通过后立即入账。 func (s *Service) SubmitH5RechargeTx(ctx context.Context, command ledger.SubmitExternalRechargeTxCommand) (ledger.ExternalRechargeOrder, error) { command.AppCode = appcode.Normalize(command.AppCode) command.OrderID = strings.TrimSpace(command.OrderID) command.TxHash = strings.TrimSpace(command.TxHash) if command.OrderID == "" || command.TargetUserID <= 0 || command.TxHash == "" { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.InvalidArgument, "usdt tx command is incomplete") } if s.repository == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, command.AppCode) order, err := s.repository.GetExternalRechargeOrder(ctx, command.AppCode, command.OrderID) if err != nil { return ledger.ExternalRechargeOrder{}, err } if order.TargetUserID != command.TargetUserID { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.PermissionDenied, "external recharge order does not belong to user") } if order.ProviderCode != ledger.PaymentProviderUSDTTRC20 || order.Status != ledger.ExternalRechargeStatusPending { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Conflict, "external recharge order is not pending usdt order") } providerOrderID, rawJSON, err := s.verifyH5USDTTransfer(ctx, command, order) if err != nil { // 校验失败仍保留用户提交的编号和上游快照,便于后续重试与财务复核;这一步不会改变账务。 _, _ = s.repository.AttachExternalRechargeTx(ctx, command, rawJSON) return ledger.ExternalRechargeOrder{}, err } if _, err := s.repository.AttachExternalRechargeTx(ctx, command, rawJSON); err != nil { return ledger.ExternalRechargeOrder{}, err } return s.repository.CreditExternalRechargeOrder(ctx, command.AppCode, command.OrderID, providerOrderID, command.TxHash, rawJSON) } // verifyH5USDTTransfer 只根据凭证形态选择事实源:64 位十六进制哈希查 TRON,其他编号查当前 App 的 Binance 入金记录。 // Binance 路径不信任付款方截图;FindUSDTDeposit 会同时核对币种、网络、收款地址、金额和成功状态。 func (s *Service) verifyH5USDTTransfer(ctx context.Context, command ledger.SubmitExternalRechargeTxCommand, order ledger.ExternalRechargeOrder) (string, string, error) { if isLikelyChainTransactionHash(command.TxHash) { if s.tronUSDT == nil { return "", "", xerr.New(xerr.Unavailable, "tron grid client is not configured") } rawJSON, err := s.tronUSDT.VerifyUSDTTransfer(ctx, command.TxHash, order.ReceiveAddress, order.USDMinorAmount) return "", rawJSON, err } if s.binance == nil { return "", "", xerr.New(xerr.Unavailable, "binance client is not configured") } record, err := s.binance.FindUSDTDeposit(ctx, ports.BinanceDepositLookupRequest{ AppCode: command.AppCode, ExternalID: command.TxHash, Coin: ledger.PaidCurrencyUSDT, Network: "TRX", ToAddress: order.ReceiveAddress, AmountMinor: order.USDMinorAmount, }) if err != nil { return "", "", err } return record.TxID, strings.TrimSpace(record.RawJSON), nil } func (s *Service) GetH5RechargeOrder(ctx context.Context, appCode string, orderID string, targetUserID int64) (ledger.ExternalRechargeOrder, error) { if s.repository == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.Normalize(appCode)) order, err := s.repository.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), strings.TrimSpace(orderID)) if err != nil { return ledger.ExternalRechargeOrder{}, err } if targetUserID > 0 && order.TargetUserID != targetUserID { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.PermissionDenied, "external recharge order does not belong to user") } if refreshed, err := s.refreshMifaPayRechargeOrder(ctx, order); err == nil { order = refreshed } if refreshed, err := s.refreshV5PayRechargeOrder(ctx, order); err == nil { order = refreshed } return order, nil }