hyapp-server/services/wallet-service/internal/service/wallet/external_recharge_temporary.go

230 lines
11 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"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
"strings"
)
const temporaryProviderMemberID int64 = 1
// CreateTemporaryRechargeOrder 创建运营临时收款订单;它只校验支付方式和金额,不读取充值商品。
func (s *Service) CreateTemporaryRechargeOrder(ctx context.Context, command ledger.CreateTemporaryExternalRechargeOrderCommand) (ledger.ExternalRechargeOrder, error) {
command = normalizeCreateTemporaryExternalRechargeOrderCommand(command)
if err := validateCreateTemporaryExternalRechargeOrderCommand(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)
switch command.ProviderCode {
case ledger.PaymentProviderMifaPay:
return s.createTemporaryMifaPayRechargeOrder(ctx, command)
case ledger.PaymentProviderV5Pay:
return s.createTemporaryV5PayRechargeOrder(ctx, command)
default:
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.InvalidArgument, "payment provider is invalid")
}
}
func (s *Service) createTemporaryMifaPayRechargeOrder(ctx context.Context, command ledger.CreateTemporaryExternalRechargeOrderCommand) (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 := validateMifaPayMethodForTemporaryOrder(method, command); err != nil {
return ledger.ExternalRechargeOrder{}, err
}
// 临时链接没有商品档位,三方实付金额只能由运营输入的 USD 金额和支付方式汇率即时换算;订单快照会保存换算结果。
providerAmountMinor, err := calculateMifaPayProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode)
if err != nil {
return ledger.ExternalRechargeOrder{}, err
}
// 先写 pending 订单再调三方,保证三方回调先于 HTTP 响应返回时也能按 order_id 找到本地订单。
order, err := s.repository.CreateTemporaryExternalRechargeOrder(ctx, command, method, providerAmountMinor)
if err != nil || order.IdempotentReplay {
return order, err
}
// notify_url 是支付状态闭环的必要条件;缺配置时把订单打失败,避免运营拿到一个无法回调核销的链接。
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: temporaryProviderMemberID,
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) createTemporaryV5PayRechargeOrder(ctx context.Context, command ledger.CreateTemporaryExternalRechargeOrderCommand) (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 := validateV5PayMethodForTemporaryOrder(method, command); err != nil {
return ledger.ExternalRechargeOrder{}, err
}
// V5Pay 同样以支付方式汇率换算本币金额;临时订单不读取 wallet_recharge_products避免误套正式商品受众。
providerAmountMinor, err := calculateProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode)
if err != nil {
return ledger.ExternalRechargeOrder{}, err
}
// 本地订单先落库,三方创建失败再转 failed这样人工排查时能看到每次临时建链尝试。
order, err := s.repository.CreateTemporaryExternalRechargeOrder(ctx, command, 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: temporaryProviderMemberID,
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 (s *Service) GetTemporaryRechargeOrder(ctx context.Context, appCode string, orderID string) (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 !isTemporaryExternalRechargeOrder(order) {
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "temporary recharge order not found")
}
// 单笔查询被 H5 验证框和 admin 页面共用;这里主动刷新三方状态,但刷新失败不盖掉本地快照,避免短暂上游错误让后台看不到订单。
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
}
func (s *Service) ListTemporaryRechargeOrders(ctx context.Context, query ledger.ListTemporaryExternalRechargeOrdersQuery) ([]ledger.ExternalRechargeOrder, int64, error) {
if s.repository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
query.AppCode = appcode.Normalize(query.AppCode)
query.ProviderCode = ledger.NormalizePaymentProvider(query.ProviderCode)
query.Status = strings.TrimSpace(query.Status)
query.Keyword = strings.TrimSpace(query.Keyword)
ctx = appcode.WithContext(ctx, query.AppCode)
return s.repository.ListTemporaryExternalRechargeOrders(ctx, query)
}
func normalizeCreateTemporaryExternalRechargeOrderCommand(command ledger.CreateTemporaryExternalRechargeOrderCommand) ledger.CreateTemporaryExternalRechargeOrderCommand {
command.AppCode = appcode.Normalize(command.AppCode)
command.CommandID = strings.TrimSpace(command.CommandID)
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 validateCreateTemporaryExternalRechargeOrderCommand(command ledger.CreateTemporaryExternalRechargeOrderCommand) error {
if command.CommandID == "" || command.USDMinorAmount <= 0 || command.PaymentMethodID <= 0 {
return xerr.New(xerr.InvalidArgument, "temporary recharge command is incomplete")
}
// 临时支付链接只记录三方收款事实不会触发用户金币入账负数没有业务含义0 用于表达“不发币”。
if command.CoinAmount < 0 {
return xerr.New(xerr.InvalidArgument, "temporary recharge coin amount is invalid")
}
if command.ProviderCode == "" {
return xerr.New(xerr.InvalidArgument, "temporary recharge provider is invalid")
}
if len(command.CommandID) > 128 {
return xerr.New(xerr.InvalidArgument, "command_id is too long")
}
return nil
}
func validateMifaPayMethodForTemporaryOrder(method ledger.ThirdPartyPaymentMethod, command ledger.CreateTemporaryExternalRechargeOrderCommand) error {
if method.ProviderCode != ledger.PaymentProviderMifaPay || method.Status != ledger.ThirdPartyPaymentStatusActive || method.MethodID != command.PaymentMethodID {
return xerr.New(xerr.Conflict, "mifapay method is not active")
}
if _, err := calculateMifaPayProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
}
func validateV5PayMethodForTemporaryOrder(method ledger.ThirdPartyPaymentMethod, command ledger.CreateTemporaryExternalRechargeOrderCommand) error {
if method.ProviderCode != ledger.PaymentProviderV5Pay || method.Status != ledger.ThirdPartyPaymentStatusActive || method.MethodID != command.PaymentMethodID {
return xerr.New(xerr.Conflict, "v5pay method is not active")
}
if strings.TrimSpace(method.PayType) == "" {
return xerr.New(xerr.InvalidArgument, "v5pay product type is required")
}
if _, err := calculateProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
}
func isTemporaryExternalRechargeOrder(order ledger.ExternalRechargeOrder) bool {
return order.AudienceType == ledger.RechargeAudienceTemporary || strings.HasPrefix(order.CommandID, "h5_temporary_recharge:")
}