230 lines
11 KiB
Go
230 lines
11 KiB
Go
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 := calculateProviderAmountMinor(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 := calculateProviderAmountMinor(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:")
|
||
}
|