chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
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"
|
||||
}
|
||||
|
||||
// resolveOrderRuntimeConfig 根据订单所属分销站点配置或全局默认配置解析运行时支付配置。
|
||||
func (r *Repository) resolveOrderRuntimeConfig(ctx context.Context, tx *gorm.DB, siteID *uint64, payWay string) (*runtimePaymentConfig, error) {
|
||||
if siteID != nil && *siteID > 0 {
|
||||
var configID uint64
|
||||
queryDB := tx
|
||||
if queryDB == nil {
|
||||
queryDB = r.db
|
||||
}
|
||||
err := queryDB.WithContext(ctx).Table("distribution_site_payment_channels dspc").
|
||||
Joins("JOIN payment_merchant_configs pmc ON pmc.id = dspc.config_id").
|
||||
Where("dspc.site_id = ? AND dspc.status = 'active' AND pmc.status = 'active' AND (pmc.pay_way = ? OR pmc.pay_way = '')", *siteID, payWay).
|
||||
Order("dspc.priority ASC, dspc.id ASC").
|
||||
Limit(1).
|
||||
Pluck("pmc.id", &configID).Error
|
||||
if err == nil && configID > 0 && r.configRepo != nil {
|
||||
dto, err := r.configRepo.FindRuntimeByID(ctx, configID, true)
|
||||
if err == nil {
|
||||
return runtimeConfigFromDTO(dto), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.defaultRuntimeConfig(ctx, payWay)
|
||||
}
|
||||
|
||||
// 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),
|
||||
)...)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user