Files
hfb_sys/backend/internal/modules/payment/repository.go
T

251 lines
6.8 KiB
Go

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"
)
type Repository struct {
db *gorm.DB
configRepo *paymentconfig.Repository
orderRepo *order.Repository
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
}
}
}
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.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("payment config usage record failed", paymentLogFields(ctx,
zap.Uint64("payment_config_id", runtimeConfig.ID),
zap.Uint64("payment_id", payment.ID),
zap.Error(err),
)...)
}
}