2026-06-17 23:13:08 +08:00

587 lines
24 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 wallet
import (
"context"
"encoding/json"
"errors"
"math"
"net/url"
"strconv"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
)
// ExternalRechargeConfig 保存 H5 外部充值需要公开给业务链路的运行配置。
type ExternalRechargeConfig struct {
USDTTRC20Enabled bool
USDTTRC20Address string
MifaPayNotifyURL string
MifaPayReturnURL string
}
// MifaPayCreateOrderRequest 是 wallet-service 传给 MiFaPay client 的已校验下单快照。
type MifaPayCreateOrderRequest struct {
OrderID string
TargetUserID int64
ProductName string
CoinAmount int64
AmountMinor int64
CurrencyCode string
CountryCode string
PayWay string
PayType string
NotifyURL string
ReturnURL string
ClientIP string
Language string
ProviderAmountMinor int64
PayerName string
PayerAccount string
PayerEmail string
}
type MifaPayCreateOrderResponse struct {
OrderID string
ProviderOrderID string
PayURL string
RawJSON string
}
type MifaPayQueryOrderRequest struct {
OrderID string
ProviderOrderID string
OrderCreatedAtMS int64
}
type MifaPayQueryOrderResponse struct {
OrderID string
ProviderOrderID string
Status string
Amount string
Currency string
PayType string
RawJSON string
}
type MifaPayNotifyData struct {
OrderID string
ProviderOrderID string
OrderStatus string
Amount string
Currency string
PayType string
RawJSON string
}
// MifaPayClient 隔离 MiFaPay RSA 签名、验签和 HTTP 协议细节service 只处理订单状态。
type MifaPayClient interface {
CreateOrder(ctx context.Context, req MifaPayCreateOrderRequest) (MifaPayCreateOrderResponse, error)
QueryOrder(ctx context.Context, req MifaPayQueryOrderRequest) (MifaPayQueryOrderResponse, error)
ParseNotification(notification ledger.MifaPayNotification) (MifaPayNotifyData, error)
}
// TronUSDTClient 校验用户提交的 TRC20 tx_hash 是否真实打到平台共享地址。
type TronUSDTClient interface {
VerifyUSDTTransfer(ctx context.Context, txHash string, toAddress string, amountMinor int64) (string, error)
}
func normalizeExternalRechargeConfig(config ExternalRechargeConfig) ExternalRechargeConfig {
config.USDTTRC20Address = strings.TrimSpace(config.USDTTRC20Address)
config.MifaPayNotifyURL = strings.TrimSpace(config.MifaPayNotifyURL)
config.MifaPayReturnURL = strings.TrimSpace(config.MifaPayReturnURL)
return config
}
// ListThirdPartyPaymentChannels 返回后台三方支付管理页需要的渠道、国家和方式树。
func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
query.AppCode = appcode.Normalize(query.AppCode)
if strings.TrimSpace(query.Status) != "" {
query.Status = ledger.NormalizeThirdPartyPaymentStatus(query.Status)
if query.Status == "" {
return nil, xerr.New(xerr.InvalidArgument, "status is invalid")
}
}
ctx = appcode.WithContext(ctx, query.AppCode)
return s.repository.ListThirdPartyPaymentChannels(ctx, query)
}
func (s *Service) SetThirdPartyPaymentMethodStatus(ctx context.Context, command ledger.ThirdPartyPaymentMethodStatusCommand) (ledger.ThirdPartyPaymentMethod, error) {
if command.MethodID <= 0 || command.OperatorUserID <= 0 {
return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.InvalidArgument, "payment method and operator are required")
}
if s.repository == nil {
return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.SetThirdPartyPaymentMethodStatus(ctx, command)
}
func (s *Service) UpdateThirdPartyPaymentRate(ctx context.Context, command ledger.ThirdPartyPaymentRateCommand) (ledger.ThirdPartyPaymentMethod, error) {
command.AppCode = appcode.Normalize(command.AppCode)
command.USDToCurrencyRate = strings.TrimSpace(command.USDToCurrencyRate)
if command.MethodID <= 0 || command.OperatorUserID <= 0 || !validNonNegativeDecimal(command.USDToCurrencyRate) {
return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.InvalidArgument, "payment rate command is invalid")
}
if s.repository == nil {
return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.UpdateThirdPartyPaymentRate(ctx, command)
}
// ListH5RechargeOptions 只返回目标用户当前可买商品和已配置汇率的支付方式。
func (s *Service) ListH5RechargeOptions(ctx context.Context, query ledger.H5RechargeOptionsQuery) (ledger.H5RechargeOptions, error) {
query.AppCode = appcode.Normalize(query.AppCode)
query.AudienceType = ledger.NormalizeRechargeAudienceType(query.AudienceType)
query.TargetCountryCode = strings.ToUpper(strings.TrimSpace(query.TargetCountryCode))
if query.TargetUserID <= 0 || query.TargetRegionID <= 0 || query.AudienceType == "" {
return ledger.H5RechargeOptions{}, xerr.New(xerr.InvalidArgument, "h5 recharge options query is incomplete")
}
if s.repository == nil {
return ledger.H5RechargeOptions{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
ctx = appcode.WithContext(ctx, query.AppCode)
options, err := s.repository.ListH5RechargeOptions(ctx, query)
if err != nil {
return ledger.H5RechargeOptions{}, err
}
if s.mifaPay == nil {
options.PaymentMethods = nil
}
options.USDTTRC20Enabled = s.externalRecharge.USDTTRC20Enabled && s.externalRecharge.USDTTRC20Address != ""
options.USDTTRC20Address = s.externalRecharge.USDTTRC20Address
return options, nil
}
// 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)
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) {
if !s.externalRecharge.USDTTRC20Enabled || s.externalRecharge.USDTTRC20Address == "" {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "usdt trc20 recharge is not configured")
}
return s.repository.CreateExternalRechargeOrder(ctx, command, product, nil, amountMicroToUSDMinor(product.AmountMicro), s.externalRecharge.USDTTRC20Address)
}
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 := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate)
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 normalizeMifaPayOrderError(err error) error {
if err == nil {
return nil
}
if _, ok := xerr.As(err); ok {
return err
}
// MiFaPay HTTP、JSON、RSA 验签和响应结构错误都是外部支付依赖不可用;不能把普通 Go error 透到 gRPC 边界,
// 否则 gateway 只能降级成 INTERNAL_ERROR前端拿不到“上游支付失败”的可重试语义。
return xerr.New(xerr.Unavailable, "mifapay order upstream response is invalid")
}
type mifaPayProviderPayloadError interface {
ProviderPayloadJSON() string
}
func mifaPayProviderPayloadJSON(err error) string {
var payloadErr mifaPayProviderPayloadError
if errors.As(err, &payloadErr) {
// MiFaPay 网关拒单的原始响应是和支付订单排障强相关的三方事实,落库后可直接给支付技术支持按 code/msg 查链路。
// 普通网络错误、JSON 解析错误或本地 RSA 错误没有三方响应体,这里保持空值,避免写入误导性的本地错误文本。
return strings.TrimSpace(payloadErr.ProviderPayloadJSON())
}
return ""
}
// SubmitH5RechargeTx 把用户提交的 tx_hash 交给 TRON 适配器校验,通过后立即入账。
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")
}
if s.tronUSDT == nil {
return s.repository.AttachExternalRechargeTx(ctx, command, "")
}
rawJSON, err := s.tronUSDT.VerifyUSDTTransfer(ctx, command.TxHash, order.ReceiveAddress, order.USDMinorAmount)
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, "", command.TxHash, rawJSON)
}
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
}
return order, nil
}
func (s *Service) refreshMifaPayRechargeOrder(ctx context.Context, order ledger.ExternalRechargeOrder) (ledger.ExternalRechargeOrder, error) {
if s.mifaPay == nil || order.ProviderCode != ledger.PaymentProviderMifaPay || order.Status == ledger.ExternalRechargeStatusCredited || order.Status == ledger.ExternalRechargeStatusFailed {
return order, nil
}
query, err := s.mifaPay.QueryOrder(ctx, MifaPayQueryOrderRequest{
OrderID: order.OrderID,
ProviderOrderID: order.ProviderOrderID,
OrderCreatedAtMS: order.CreatedAtMS,
})
if err != nil {
// H5 轮询不能因为 MiFaPay 查询短暂失败而中断;异步回调仍然是主链路,查询只是用户返回页的补偿刷新。
return order, err
}
providerOrderID := firstNonEmpty(query.ProviderOrderID, order.ProviderOrderID)
data := MifaPayNotifyData{
OrderID: firstNonEmpty(query.OrderID, order.OrderID),
ProviderOrderID: providerOrderID,
Amount: query.Amount,
Currency: query.Currency,
PayType: query.PayType,
RawJSON: query.RawJSON,
}
switch strings.TrimSpace(query.Status) {
case "1":
if !mifaPayQueryMatchesOrder(data, order) {
// 主动查询和回调使用同一笔本地订单快照校验金额、币种和支付类型;不匹配时拒绝入账,避免三方串单或通道异常。
return s.repository.MarkExternalRechargeOrderFailed(ctx, order.AppCode, order.OrderID, "mifapay query order data mismatch", query.RawJSON)
}
return s.repository.CreditExternalRechargeOrder(ctx, order.AppCode, order.OrderID, providerOrderID, "", query.RawJSON)
case "4":
return s.repository.MarkExternalRechargeOrderFailed(ctx, order.AppCode, order.OrderID, "mifapay order status 4", query.RawJSON)
case "0", "5", "":
return order, nil
default:
return order, nil
}
}
// HandleMifapayNotify 只在验签、订单号、币种和金额全部匹配后入账;重复回调返回 SUCCESS。
func (s *Service) HandleMifapayNotify(ctx context.Context, appCode string, notification ledger.MifaPayNotification) (ledger.ExternalRechargeOrder, bool, error) {
if s.mifaPay == nil {
return ledger.ExternalRechargeOrder{}, false, xerr.New(xerr.Unavailable, "mifapay client is not configured")
}
if s.repository == nil {
return ledger.ExternalRechargeOrder{}, false, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
data, err := s.mifaPay.ParseNotification(notification)
if err != nil {
return ledger.ExternalRechargeOrder{}, false, err
}
order, err := s.repository.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), data.OrderID)
if err != nil {
return ledger.ExternalRechargeOrder{}, false, err
}
if !strings.EqualFold(data.OrderStatus, "SUCCESS") {
order, err = s.repository.MarkExternalRechargeOrderFailed(ctx, order.AppCode, order.OrderID, "mifapay order status "+data.OrderStatus, data.RawJSON)
return order, true, err
}
if !mifaPayNotifyMatchesOrder(data, order) {
order, err = s.repository.MarkExternalRechargeOrderFailed(ctx, order.AppCode, order.OrderID, "mifapay callback amount or currency mismatch", data.RawJSON)
return order, false, err
}
credited, err := s.repository.CreditExternalRechargeOrder(ctx, order.AppCode, order.OrderID, data.ProviderOrderID, "", data.RawJSON)
return credited, err == nil, err
}
func normalizeCreateExternalRechargeOrderCommand(command ledger.CreateExternalRechargeOrderCommand) ledger.CreateExternalRechargeOrderCommand {
command.AppCode = appcode.Normalize(command.AppCode)
command.CommandID = strings.TrimSpace(command.CommandID)
command.TargetCountryCode = strings.ToUpper(strings.TrimSpace(command.TargetCountryCode))
command.AudienceType = ledger.NormalizeRechargeAudienceType(command.AudienceType)
command.ProviderCode = ledger.NormalizePaymentProvider(command.ProviderCode)
command.ReturnURL = strings.TrimSpace(command.ReturnURL)
command.NotifyURL = strings.TrimSpace(command.NotifyURL)
command.ClientIP = strings.TrimSpace(command.ClientIP)
command.Language = normalizeMifaPayLanguage(command.Language)
command.PayerName = strings.TrimSpace(command.PayerName)
command.PayerAccount = strings.TrimSpace(command.PayerAccount)
command.PayerEmail = strings.TrimSpace(command.PayerEmail)
return command
}
func normalizeMifaPayLanguage(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "en"
}
if comma := strings.IndexByte(value, ','); comma >= 0 {
value = value[:comma]
}
if semicolon := strings.IndexByte(value, ';'); semicolon >= 0 {
value = value[:semicolon]
}
value = strings.TrimSpace(value)
if !validMifaPayLanguageTag(value) {
return "en"
}
// wallet-service 是 MiFaPay 的最后一道协议边界;即使未来有非 gateway 调用方传入浏览器语言列表,
// 这里也只向三方支付发送单个 BCP47 风格标签,避免订单落库后被 MiFaPay 参数格式校验拒掉。
return value
}
func validMifaPayLanguageTag(value string) bool {
if value == "" {
return false
}
parts := strings.Split(value, "-")
if len(parts) == 0 || len(parts) > 4 || len(parts[0]) < 2 || len(parts[0]) > 3 || !allASCIILetters(parts[0]) {
return false
}
for _, part := range parts[1:] {
if len(part) < 2 || len(part) > 8 || !allASCIILettersOrDigits(part) {
return false
}
}
return true
}
func allASCIILetters(value string) bool {
for _, char := range value {
if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') {
return false
}
}
return value != ""
}
func allASCIILettersOrDigits(value string) bool {
for _, char := range value {
if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') {
return false
}
}
return value != ""
}
func validateCreateExternalRechargeOrderCommand(command ledger.CreateExternalRechargeOrderCommand) error {
if command.CommandID == "" || command.TargetUserID <= 0 || command.TargetRegionID <= 0 || command.ProductID <= 0 {
return xerr.New(xerr.InvalidArgument, "external recharge command is incomplete")
}
if command.AudienceType == "" || command.ProviderCode == "" {
return xerr.New(xerr.InvalidArgument, "external recharge command type is invalid")
}
if len(command.CommandID) > 128 {
return xerr.New(xerr.InvalidArgument, "command_id is too long")
}
return nil
}
func validateExternalRechargeProduct(product ledger.RechargeProduct, command ledger.CreateExternalRechargeOrderCommand) error {
if product.Status != ledger.RechargeProductStatusActive || !product.Enabled {
return xerr.New(xerr.Conflict, "recharge product is not active")
}
if product.Platform != ledger.RechargeProductPlatformWeb {
return xerr.New(xerr.InvalidArgument, "recharge product is not h5 product")
}
if product.AudienceType != command.AudienceType {
return xerr.New(xerr.Conflict, "recharge product audience does not match")
}
if !rechargeProductSupportsRegion(product, command.TargetRegionID) {
return xerr.New(xerr.Conflict, "recharge product is not available in user region")
}
return nil
}
func validateMifaPayMethodForOrder(method ledger.ThirdPartyPaymentMethod, command ledger.CreateExternalRechargeOrderCommand) error {
if method.ProviderCode != ledger.PaymentProviderMifaPay || method.Status != ledger.ThirdPartyPaymentStatusActive {
return xerr.New(xerr.Conflict, "mifapay method is not active")
}
if method.CountryCode != "GLOBAL" && !strings.EqualFold(method.CountryCode, command.TargetCountryCode) {
return xerr.New(xerr.Conflict, "mifapay method country does not match")
}
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate); err != nil {
return err
}
return nil
}
func calculateProviderAmountMinor(usdMinor int64, rate string) (int64, error) {
if usdMinor <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "usd amount is invalid")
}
value, err := strconv.ParseFloat(strings.TrimSpace(rate), 64)
if err != nil || value <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "payment exchange rate is invalid")
}
return int64(math.Round(float64(usdMinor) * value)), nil
}
func amountMicroToUSDMinor(amountMicro int64) int64 {
if amountMicro <= 0 {
return 0
}
return amountMicro / 10_000
}
func validNonNegativeDecimal(value string) bool {
parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
return err == nil && parsed >= 0
}
func mifaPayNotifyMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRechargeOrder) bool {
if !strings.EqualFold(data.Currency, order.CurrencyCode) {
return false
}
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
return false
}
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
return err == nil && amount == order.ProviderAmountMinor
}
func mifaPayQueryMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRechargeOrder) bool {
if data.OrderID != "" && !strings.EqualFold(data.OrderID, order.OrderID) {
return false
}
if data.Currency != "" && !strings.EqualFold(data.Currency, order.CurrencyCode) {
return false
}
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
return false
}
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
return err == nil && amount == order.ProviderAmountMinor
}
func mifaPayProviderReturnURL(returnURL string) string {
returnURL = strings.TrimSpace(returnURL)
if returnURL == "" {
return returnURL
}
parsed, err := url.Parse(returnURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return returnURL
}
// MiFaPay 生产网关会对 payment.resultRedirect 做严格格式校验,动态 query 会在下单阶段被拒。
// 订单 ID 已经通过创建订单响应返回给 H5 并落在钱包库,回跳页只能作为稳定入口,不能依赖三方回传本地查询参数。
parsed.RawQuery = ""
parsed.Fragment = ""
return parsed.String()
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}
func mustJSONText(value any) string {
body, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(body)
}