package payment import ( "context" "sort" "hfb_sys/backend/internal/logging" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/internal/modules/paymentconfig" "go.uber.org/zap" "gorm.io/gorm" ) // MohongOrderConfirmer 撞车订单支付确认接口,避免 payment 与 mohong 循环依赖。 type MohongOrderConfirmer interface { ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error) NotifyNewConversation(conversationID uint64) } type Repository struct { db *gorm.DB configRepo *paymentconfig.Repository orderRepo *order.Repository mohongRepo MohongOrderConfirmer logger *zap.Logger } type runtimePaymentConfig struct { ID uint64 Provider string MerchantID string PayWay string JSPayFlag string NotifyURL string JumpURL string Channel channelClient } const ( channelSourceCreate = "create" channelSourceQuery = "query" channelSourceNotify = "notify" channelSourceMock = "mock" ) var refundBizTypes = []string{ "cancel_refund", "admin_close_refund", "admin_refund", "checkout_refund", "deposit_refund", "rent_refund", "arbitration_refund", } func RefundBizTypes() []string { out := make([]string, len(refundBizTypes)) copy(out, refundBizTypes) return out } // NewRepository 创建支付仓库,注入支付配置仓库和订单仓库。 func NewRepository(db *gorm.DB, configRepo *paymentconfig.Repository, orderRepo *order.Repository, options ...RepositoryOption) *Repository { repo := &Repository{ db: db, configRepo: configRepo, orderRepo: orderRepo, logger: zap.NewNop(), } for _, option := range options { option(repo) } if repo.logger == nil { repo.logger = zap.NewNop() } return repo } type RepositoryOption func(*Repository) // WithLogger 为支付仓库注入结构化日志器。 func WithLogger(logger *zap.Logger) RepositoryOption { return func(repo *Repository) { if logger != nil { repo.logger = logger } } } // WithMohongRepo 注入撞车订单确认器。 func WithMohongRepo(repo MohongOrderConfirmer) RepositoryOption { return func(r *Repository) { r.mohongRepo = repo } } func (r *Repository) log() *zap.Logger { if r == nil || r.logger == nil { return zap.NewNop() } return r.logger } func paymentLogFields(ctx context.Context, fields ...zap.Field) []zap.Field { base := []zap.Field{zap.String("module", "payment")} if requestID := logging.RequestIDFromContext(ctx); requestID != "" { base = append(base, zap.String("request_id", requestID)) } return append(base, fields...) } func runtimeConfigFields(runtimeConfig *runtimePaymentConfig) []zap.Field { if runtimeConfig == nil { return nil } return []zap.Field{ zap.Uint64("payment_config_id", runtimeConfig.ID), zap.String("provider", runtimeConfig.Provider), zap.String("merchant_id", runtimeConfig.MerchantID), zap.String("pay_way", runtimeConfig.PayWay), } } func paymentOrderFields(payment *model.PaymentOrder) []zap.Field { if payment == nil { return nil } return []zap.Field{ zap.Uint64("payment_id", payment.ID), zap.Uint64("order_id", payment.OrderID), zap.String("order_no", payment.OrderNo), zap.String("third_order_id", payment.ThirdOrderID), zap.String("provider_order_id", payment.ProviderOrderID), zap.Int64("amount_cent", payment.AmountCent), zap.String("biz_type", payment.BizType), zap.String("status", payment.Status), } } func appendFields(groups ...[]zap.Field) []zap.Field { var out []zap.Field for _, group := range groups { out = append(out, group...) } return out } func stringMapKeys(value map[string]string) []string { keys := make([]string, 0, len(value)) for key := range value { keys = append(keys, key) } sort.Strings(keys) return keys } // isMockMode 判断当前运行时配置是否为模拟支付。 func (c runtimePaymentConfig) isMockMode() bool { return c.Provider == "mock" } // defaultRuntimeConfig 按支付方式获取默认启用配置,用于新支付单创建。 func (r *Repository) defaultRuntimeConfig(ctx context.Context, payWay string) (*runtimePaymentConfig, error) { if r.configRepo == nil { return nil, ErrPaymentUnavailable } dto, err := r.configRepo.FindDefaultByPayWay(ctx, payWay, true) if err != nil { return nil, err } return runtimeConfigFromDTO(dto), nil } // runtimeConfigForPayment 回溯支付单当时命中的配置,优先使用 payment_config_id。 func (r *Repository) runtimeConfigForPayment(ctx context.Context, payment *model.PaymentOrder) (*runtimePaymentConfig, error) { provider := firstNonEmpty(payment.Provider, "mock") merchantID := payment.MerchantID payWay := normalizePayWay(payment.PayWay) if r.configRepo != nil && payment.PaymentConfigID > 0 { dto, err := r.configRepo.FindRuntimeByID(ctx, payment.PaymentConfigID, true) if err == nil { return runtimeConfigFromDTO(dto), nil } if err != paymentconfig.ErrConfigNotFound { return nil, err } } if r.configRepo != nil && merchantID != "" { dto, err := r.configRepo.FindByProviderMerchantPayWay(ctx, provider, merchantID, payWay, true) if err == nil { return runtimeConfigFromDTO(dto), nil } if err != paymentconfig.ErrConfigNotFound { return nil, err } dto, err = r.configRepo.FindByProviderMerchant(ctx, provider, merchantID, true) if err == nil { return runtimeConfigFromDTO(dto), nil } if err != paymentconfig.ErrConfigNotFound { return nil, err } } if provider == "mock" { return &runtimePaymentConfig{ Provider: provider, MerchantID: merchantID, }, nil } if provider != "leshua" && provider != "lakala" && provider != "shuncheng" { return &runtimePaymentConfig{ Provider: provider, MerchantID: merchantID, }, nil } if r.configRepo == nil { return nil, ErrPaymentUnavailable } dto, err := r.configRepo.FindDefaultByProviderPayWay(ctx, provider, payWay, true) if err != nil { return nil, err } return runtimeConfigFromDTO(dto), nil } // runtimeConfigFromDTO 将后台支付配置 DTO 转为支付运行时配置。 func runtimeConfigFromDTO(dto *paymentconfig.ConfigDTO) *runtimePaymentConfig { provider := firstNonEmpty(dto.Provider, "mock") payWay := normalizePayWay(dto.PayWay) jsPayFlag := firstNonEmpty(dto.JSPayFlag, "2") client, err := buildChannelClient(dto) if err != nil { client = nil } return &runtimePaymentConfig{ ID: dto.ID, Provider: provider, MerchantID: dto.MerchantID, PayWay: payWay, JSPayFlag: jsPayFlag, NotifyURL: dto.NotifyURL, JumpURL: dto.JumpURL, Channel: client, } } // recordConfigUsage 记录成功支付使用的配置,失败只写日志不影响主流程。 func (r *Repository) recordConfigUsage(ctx context.Context, runtimeConfig *runtimePaymentConfig, payment *model.PaymentOrder) { if r.configRepo == nil || runtimeConfig == nil || payment == nil || runtimeConfig.ID == 0 { return } if (payment.BizType != "order_pay" && payment.BizType != "mohong_pay") || payment.Status != "paid" { return } if err := r.configRepo.RecordUsage(ctx, runtimeConfig.ID, payment.ID, runtimeConfig.Provider, runtimeConfig.MerchantID, payment.AmountCent, payment.BizType); err != nil { r.log().Warn("支付配置使用记录保存失败", paymentLogFields(ctx, zap.Uint64("payment_config_id", runtimeConfig.ID), zap.Uint64("payment_id", payment.ID), zap.Error(err), )...) } }