package grpc import ( "context" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // ListThirdPartyPaymentChannels 返回三方支付渠道、国家和方式树,后台用它直接渲染二级展开列表。 func (s *Server) ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) channels, err := s.svc.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{ AppCode: req.GetAppCode(), ProviderCode: req.GetProviderCode(), Status: req.GetStatus(), IncludeDisabledMethods: req.GetIncludeDisabledMethods(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListThirdPartyPaymentChannelsResponse{Channels: make([]*walletv1.ThirdPartyPaymentChannel, 0, len(channels))} for _, channel := range channels { resp.Channels = append(resp.Channels, thirdPartyPaymentChannelToProto(channel)) } return resp, nil } // SetThirdPartyPaymentMethodStatus 开关某个国家下的具体支付方式;渠道本身不被连带关闭。 func (s *Server) SetThirdPartyPaymentMethodStatus(ctx context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) method, err := s.svc.SetThirdPartyPaymentMethodStatus(ctx, ledger.ThirdPartyPaymentMethodStatusCommand{ AppCode: req.GetAppCode(), MethodID: req.GetMethodId(), Enabled: req.GetEnabled(), OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ThirdPartyPaymentMethodResponse{Method: thirdPartyPaymentMethodToProto(method)}, nil } // UpdateThirdPartyPaymentRate 保存 USD 到国家本币汇率;H5 只展示已配置正汇率的国家方式。 func (s *Server) UpdateThirdPartyPaymentRate(ctx context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) method, err := s.svc.UpdateThirdPartyPaymentRate(ctx, ledger.ThirdPartyPaymentRateCommand{ AppCode: req.GetAppCode(), MethodID: req.GetMethodId(), USDToCurrencyRate: req.GetUsdToCurrencyRate(), OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ThirdPartyPaymentMethodResponse{Method: thirdPartyPaymentMethodToProto(method)}, nil } // SyncThirdPartyPaymentMethods 从支付商同步当前商户实际开通的支付方式;写库仍由 wallet-service 统一负责。 func (s *Server) SyncThirdPartyPaymentMethods(ctx context.Context, req *walletv1.SyncThirdPartyPaymentMethodsRequest) (*walletv1.SyncThirdPartyPaymentMethodsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) result, err := s.svc.SyncThirdPartyPaymentMethods(ctx, ledger.ThirdPartyPaymentMethodSyncCommand{ AppCode: req.GetAppCode(), ProviderCode: req.GetProviderCode(), OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.SyncThirdPartyPaymentMethodsResponse{ ProviderCode: result.ProviderCode, ScannedCountryCount: int32(result.ScannedCountryCount), FetchedCountryCount: int32(result.FetchedCountryCount), CreatedCount: int32(result.CreatedCount), UpdatedCount: int32(result.UpdatedCount), SkippedCount: int32(result.SkippedCount), Issues: make([]*walletv1.ThirdPartyPaymentMethodSyncIssue, 0, len(result.Issues)), } for _, issue := range result.Issues { resp.Issues = append(resp.Issues, thirdPartyPaymentMethodSyncIssueToProto(issue)) } return resp, nil } // ListH5RechargeOptions 返回 H5 当前账号的商品和支付方式,普通用户和币商由 audience_type 分流。 func (s *Server) ListH5RechargeOptions(ctx context.Context, req *walletv1.H5RechargeOptionsRequest) (*walletv1.H5RechargeOptionsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) options, err := s.svc.ListH5RechargeOptions(ctx, ledger.H5RechargeOptionsQuery{ AppCode: req.GetAppCode(), TargetUserID: req.GetTargetUserId(), TargetRegionID: req.GetTargetRegionId(), TargetCountryCode: req.GetTargetCountryCode(), AudienceType: req.GetAudienceType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.H5RechargeOptionsResponse{ Products: make([]*walletv1.RechargeProduct, 0, len(options.Products)), PaymentMethods: make([]*walletv1.ThirdPartyPaymentMethod, 0, len(options.PaymentMethods)), UsdtTrc20Enabled: options.USDTTRC20Enabled, UsdtTrc20Address: options.USDTTRC20Address, } for _, product := range options.Products { resp.Products = append(resp.Products, rechargeProductToProto(product)) } for _, method := range options.PaymentMethods { resp.PaymentMethods = append(resp.PaymentMethods, thirdPartyPaymentMethodToProto(method)) } return resp, nil } // CreateH5RechargeOrder 创建外部充值订单;MiFaPay 只返回 pay_url,USDT 只返回共享收款地址。 func (s *Server) CreateH5RechargeOrder(ctx context.Context, req *walletv1.CreateH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.CreateH5RechargeOrder(ctx, ledger.CreateExternalRechargeOrderCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), TargetRegionID: req.GetTargetRegionId(), TargetCountryID: req.GetTargetCountryId(), TargetCountryCode: req.GetTargetCountryCode(), AudienceType: req.GetAudienceType(), ProductID: req.GetProductId(), ProviderCode: req.GetProviderCode(), PaymentMethodID: req.GetPaymentMethodId(), ReturnURL: req.GetReturnUrl(), NotifyURL: req.GetNotifyUrl(), ClientIP: req.GetClientIp(), Language: req.GetLanguage(), PayerName: req.GetPayerName(), PayerAccount: req.GetPayerAccount(), PayerEmail: req.GetPayerEmail(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // CreateTemporaryRechargeOrder 创建运营临时支付链接;支付成功只标记 paid,不触发钱包入账。 func (s *Server) CreateTemporaryRechargeOrder(ctx context.Context, req *walletv1.CreateTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.CreateTemporaryRechargeOrder(ctx, ledger.CreateTemporaryExternalRechargeOrderCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), CoinAmount: req.GetCoinAmount(), USDMinorAmount: req.GetUsdMinorAmount(), ProviderCode: req.GetProviderCode(), PaymentMethodID: req.GetPaymentMethodId(), ReturnURL: req.GetReturnUrl(), NotifyURL: req.GetNotifyUrl(), ClientIP: req.GetClientIp(), Language: req.GetLanguage(), PayerName: req.GetPayerName(), PayerAccount: req.GetPayerAccount(), PayerEmail: req.GetPayerEmail(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // GetTemporaryRechargeOrder 给运营页验证临时链接是否已支付,并顺带触发三方查单刷新状态。 func (s *Server) GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.GetTemporaryRechargeOrder(ctx, req.GetAppCode(), req.GetOrderId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // ListTemporaryRechargeOrders 返回后台临时支付链接列表;这里只读订单快照,不批量访问上游支付商。 func (s *Server) ListTemporaryRechargeOrders(ctx context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) orders, total, err := s.svc.ListTemporaryRechargeOrders(ctx, ledger.ListTemporaryExternalRechargeOrdersQuery{ AppCode: req.GetAppCode(), Status: req.GetStatus(), ProviderCode: req.GetProviderCode(), Keyword: req.GetKeyword(), Page: req.GetPage(), PageSize: req.GetPageSize(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListTemporaryRechargeOrdersResponse{Orders: make([]*walletv1.ExternalRechargeOrder, 0, len(orders)), Total: total} for _, order := range orders { resp.Orders = append(resp.Orders, externalRechargeOrderToProto(order)) } return resp, nil } // SubmitH5RechargeTx 接收 USDT tx_hash;链上校验和幂等入账仍在 wallet-service 事务里完成。 func (s *Server) SubmitH5RechargeTx(ctx context.Context, req *walletv1.SubmitH5RechargeTxRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.SubmitH5RechargeTx(ctx, ledger.SubmitExternalRechargeTxCommand{ AppCode: req.GetAppCode(), OrderID: req.GetOrderId(), TargetUserID: req.GetTargetUserId(), TxHash: req.GetTxHash(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // GetH5RechargeOrder 给 H5 轮询订单状态;target_user_id 用于避免无 token 场景越权看单。 func (s *Server) GetH5RechargeOrder(ctx context.Context, req *walletv1.GetH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, err := s.svc.GetH5RechargeOrder(ctx, req.GetAppCode(), req.GetOrderId(), req.GetTargetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.H5RechargeOrderResponse{Order: externalRechargeOrderToProto(order)}, nil } // VerifyCoinSellerRechargeReceipt 只读校验币商充值凭证,不创建外部订单,也不触发钱包入账。 func (s *Server) VerifyCoinSellerRechargeReceipt(ctx context.Context, req *walletv1.VerifyCoinSellerRechargeReceiptRequest) (*walletv1.VerifyCoinSellerRechargeReceiptResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.VerifyCoinSellerRechargeReceipt(ctx, ledger.VerifyCoinSellerRechargeReceiptCommand{ RequestID: req.GetRequestId(), AppCode: req.GetAppCode(), ProviderCode: req.GetProviderCode(), ExternalOrderNo: req.GetExternalOrderNo(), Chain: req.GetChain(), USDMinorAmount: req.GetUsdMinorAmount(), OrderDateMS: req.GetOrderDateMs(), ProviderCountryCode: req.GetProviderCountryCode(), ProviderCurrencyCode: req.GetProviderCurrencyCode(), ProviderPayType: req.GetProviderPayType(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return coinSellerRechargeReceiptVerificationToProto(receipt), nil } // HandleMifapayNotify 验签并处理 MiFaPay 回调;成功或重复成功由 HTTP 层按 response_text 返回大写 SUCCESS。 func (s *Server) HandleMifapayNotify(ctx context.Context, req *walletv1.HandleMifapayNotifyRequest) (*walletv1.HandleMifapayNotifyResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, accepted, err := s.svc.HandleMifapayNotify(ctx, req.GetAppCode(), ledger.MifaPayNotification{ MerAccount: req.GetMerAccount(), Data: req.GetData(), Sign: req.GetSign(), }) if err != nil { return nil, xerr.ToGRPCError(err) } responseText := "FAIL" if accepted { responseText = "SUCCESS" } return &walletv1.HandleMifapayNotifyResponse{Accepted: accepted, ResponseText: responseText, OrderId: order.OrderID}, nil } // HandleV5PayNotify 验签并处理 V5Pay 回调;成功或重复成功由 HTTP 层按 response_text 返回小写 success。 func (s *Server) HandleV5PayNotify(ctx context.Context, req *walletv1.HandleV5PayNotifyRequest) (*walletv1.HandleV5PayNotifyResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) order, accepted, err := s.svc.HandleV5PayNotify(ctx, req.GetAppCode(), ledger.V5PayNotification{ Fields: req.GetFields(), RawJSON: req.GetRawJson(), }) if err != nil { return nil, xerr.ToGRPCError(err) } responseText := "fail" if accepted { responseText = "success" } return &walletv1.HandleV5PayNotifyResponse{Accepted: accepted, ResponseText: responseText, OrderId: order.OrderID}, nil } func coinSellerRechargeReceiptVerificationToProto(receipt ledger.CoinSellerRechargeReceiptVerification) *walletv1.VerifyCoinSellerRechargeReceiptResponse { return &walletv1.VerifyCoinSellerRechargeReceiptResponse{ Verified: receipt.Verified, ProviderCode: receipt.ProviderCode, ExternalOrderNo: receipt.ExternalOrderNo, ProviderOrderId: receipt.ProviderOrderID, Status: receipt.Status, CurrencyCode: receipt.CurrencyCode, ProviderAmountMinor: receipt.ProviderAmountMinor, RawJson: receipt.RawJSON, Chain: receipt.Chain, ReceiveAddress: receipt.ReceiveAddress, VerifiedAtMs: receipt.VerifiedAtMS, FailureReason: receipt.FailureReason, } } func thirdPartyPaymentChannelToProto(channel ledger.ThirdPartyPaymentChannel) *walletv1.ThirdPartyPaymentChannel { resp := &walletv1.ThirdPartyPaymentChannel{ AppCode: channel.AppCode, ProviderCode: channel.ProviderCode, ProviderName: channel.ProviderName, Status: channel.Status, SortOrder: channel.SortOrder, Methods: make([]*walletv1.ThirdPartyPaymentMethod, 0, len(channel.Methods)), } for _, method := range channel.Methods { resp.Methods = append(resp.Methods, thirdPartyPaymentMethodToProto(method)) } return resp } func thirdPartyPaymentMethodSyncIssueToProto(issue ledger.ThirdPartyPaymentMethodSyncIssue) *walletv1.ThirdPartyPaymentMethodSyncIssue { return &walletv1.ThirdPartyPaymentMethodSyncIssue{ CountryCode: issue.CountryCode, CurrencyCode: issue.CurrencyCode, Code: issue.Code, Message: issue.Message, } } func thirdPartyPaymentMethodToProto(method ledger.ThirdPartyPaymentMethod) *walletv1.ThirdPartyPaymentMethod { return &walletv1.ThirdPartyPaymentMethod{ MethodId: method.MethodID, AppCode: method.AppCode, ProviderCode: method.ProviderCode, ProviderName: method.ProviderName, CountryCode: method.CountryCode, CountryName: method.CountryName, CurrencyCode: method.CurrencyCode, PayWay: method.PayWay, PayType: method.PayType, MethodName: method.MethodName, LogoUrl: method.LogoURL, Status: method.Status, UsdToCurrencyRate: method.USDToCurrencyRate, SortOrder: method.SortOrder, CreatedAtMs: method.CreatedAtMS, UpdatedAtMs: method.UpdatedAtMS, } } func externalRechargeOrderToProto(order ledger.ExternalRechargeOrder) *walletv1.ExternalRechargeOrder { return &walletv1.ExternalRechargeOrder{ OrderId: order.OrderID, AppCode: order.AppCode, TargetUserId: order.TargetUserID, TargetRegionId: order.TargetRegionID, TargetCountryId: order.TargetCountryID, TargetCountryCode: order.TargetCountryCode, AudienceType: order.AudienceType, ProductId: order.ProductID, ProductName: order.ProductName, CoinAmount: order.CoinAmount, UsdMinorAmount: order.USDMinorAmount, ProviderCode: order.ProviderCode, PaymentMethodId: order.PaymentMethodID, CountryCode: order.CountryCode, CurrencyCode: order.CurrencyCode, ProviderAmountMinor: order.ProviderAmountMinor, PayWay: order.PayWay, PayType: order.PayType, PayUrl: order.PayURL, ProviderOrderId: order.ProviderOrderID, TxHash: order.TxHash, ReceiveAddress: order.ReceiveAddress, Status: order.Status, FailureReason: order.FailureReason, TransactionId: order.TransactionID, CreatedAtMs: order.CreatedAtMS, UpdatedAtMs: order.UpdatedAtMS, IdempotentReplay: order.IdempotentReplay, } }