diff --git a/backend/internal/model/payment.go b/backend/internal/model/payment.go index 3ffcefc..5192887 100644 --- a/backend/internal/model/payment.go +++ b/backend/internal/model/payment.go @@ -12,6 +12,7 @@ type PaymentOrder struct { OrderID uint64 `gorm:"not null;index" json:"order_id"` OrderNo string `gorm:"size:64;not null;index" json:"order_no"` UserID uint64 `gorm:"not null;index" json:"user_id"` + PaymentConfigID uint64 `gorm:"not null;default:0;index" json:"payment_config_id"` Provider string `gorm:"size:32;not null" json:"provider"` MerchantID string `gorm:"size:32;not null;default:''" json:"merchant_id"` ThirdOrderID string `gorm:"size:64;not null;uniqueIndex" json:"third_order_id"` diff --git a/backend/internal/model/payment_merchant_config.go b/backend/internal/model/payment_merchant_config.go index 2357837..fac1789 100644 --- a/backend/internal/model/payment_merchant_config.go +++ b/backend/internal/model/payment_merchant_config.go @@ -17,7 +17,7 @@ type PaymentMerchantConfig struct { NotifyKey string `gorm:"type:text" json:"notify_key"` // 加密存储,拉卡拉使用通知验签证书 PEM NotifyURL string `gorm:"size:512;not null;default:''" json:"notify_url"` JumpURL string `gorm:"size:512;not null;default:''" json:"jump_url"` - PayWay string `gorm:"size:32;not null;default:'ZFBZF'" json:"pay_way"` + PayWay string `gorm:"size:32;not null;default:'ZFBZF';index:idx_payment_merchant_configs_pay_way" json:"pay_way"` JSPayFlag string `gorm:"column:jspay_flag;size:8;not null;default:'2'" json:"jspay_flag"` SignType string `gorm:"size:32;not null;default:'MD5'" json:"sign_type"` ExtraConfig JSONMap `gorm:"type:json" json:"extra_config"` diff --git a/backend/internal/modules/payment/dto.go b/backend/internal/modules/payment/dto.go index 3cceb5a..8f91db4 100644 --- a/backend/internal/modules/payment/dto.go +++ b/backend/internal/modules/payment/dto.go @@ -16,6 +16,7 @@ type PaymentDTO struct { PaymentNo string `json:"payment_no"` OrderID uint64 `json:"order_id"` OrderNo string `json:"order_no"` + PaymentConfigID uint64 `json:"payment_config_id"` Provider string `json:"provider"` ThirdOrderID string `json:"third_order_id"` ProviderOrderID string `json:"provider_order_id"` @@ -75,6 +76,7 @@ type AdminPaymentDTO struct { OrderID uint64 `json:"order_id"` OrderNo string `json:"order_no"` UserID uint64 `json:"user_id"` + PaymentConfigID uint64 `json:"payment_config_id"` UserPhone string `json:"user_phone"` Provider string `json:"provider"` MerchantID string `json:"merchant_id"` diff --git a/backend/internal/modules/payment/payment_start.go b/backend/internal/modules/payment/payment_start.go index 6d5ad54..03c8886 100644 --- a/backend/internal/modules/payment/payment_start.go +++ b/backend/internal/modules/payment/payment_start.go @@ -11,8 +11,10 @@ import ( "time" ) +// Start 发起订单支付,并按请求支付方式选择对应默认渠道配置。 func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) { - defaultConfig, err := r.defaultRuntimeConfig(ctx) + req.PayWay = normalizePayWay(req.PayWay) + defaultConfig, err := r.defaultRuntimeConfig(ctx, req.PayWay) if err != nil { return nil, ErrPaymentUnavailable } @@ -103,6 +105,8 @@ func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, r dto := toDTO(*latest) return &dto, nil } + +// preparePayment 创建或复用订单支付单,并写入本次命中的支付配置 ID。 func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.RentalOrder, error) { var paymentID uint64 var orderRow model.RentalOrder @@ -130,6 +134,7 @@ func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID existing.PayWay = firstNonEmpty(req.PayWay, existing.PayWay, runtimeConfig.PayWay, "ZFBZF") existing.JSPayFlag = firstNonEmpty(req.JSPayFlag, existing.JSPayFlag, runtimeConfig.JSPayFlag, "2") existing.AmountCent = amountCent + existing.PaymentConfigID = firstNonZero(existing.PaymentConfigID, runtimeConfig.ID) existing.Provider = firstNonEmpty(existing.Provider, runtimeConfig.Provider) existing.MerchantID = firstNonEmpty(existing.MerchantID, runtimeConfig.MerchantID) if existing.Provider == "mock" && existing.ProviderOrderID == "" { @@ -165,6 +170,8 @@ func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID } return payment, &orderRow, nil } + +// canReuseOrderPayment 判断旧支付单是否还能复用,支付方式或配置不同则必须重新下单。 func canReuseOrderPayment(payment model.PaymentOrder, runtimeConfig runtimePaymentConfig) bool { if payment.Status == "paid" { return true @@ -178,11 +185,19 @@ func canReuseOrderPayment(payment model.PaymentOrder, runtimeConfig runtimePayme if payment.MerchantID != "" && runtimeConfig.MerchantID != "" && payment.MerchantID != runtimeConfig.MerchantID { return false } + if payment.PaymentConfigID != 0 && runtimeConfig.ID != 0 && payment.PaymentConfigID != runtimeConfig.ID { + return false + } + if payment.PayWay != "" && runtimeConfig.PayWay != "" && normalizePayWay(payment.PayWay) != normalizePayWay(runtimeConfig.PayWay) { + return false + } if payment.Status == "paying" && paymentCashierExpired(payment) { return false } return true } + +// paymentCashierExpired 判断渠道收银台是否已过期,过期后不再复用支付单。 func paymentCashierExpired(payment model.PaymentOrder) bool { if len(payment.RawRequest) == 0 { return false @@ -197,6 +212,8 @@ func paymentCashierExpired(payment model.PaymentOrder) bool { } return !timeutil.ShanghaiNow().Before(*deadline) } + +// newOrderPayment 创建新的订单支付单,并固化支付方式和支付配置 ID。 func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (model.PaymentOrder, error) { paymentNo, err := newPaymentNo() if err != nil { @@ -207,6 +224,7 @@ func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRe OrderID: row.ID, OrderNo: row.OrderNo, UserID: row.RenterID, + PaymentConfigID: runtimeConfig.ID, Provider: runtimeConfig.Provider, MerchantID: runtimeConfig.MerchantID, ThirdOrderID: paymentNo, @@ -223,4 +241,3 @@ func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRe } return payment, nil } - diff --git a/backend/internal/modules/payment/presenter.go b/backend/internal/modules/payment/presenter.go index 18dc6b0..32642a9 100644 --- a/backend/internal/modules/payment/presenter.go +++ b/backend/internal/modules/payment/presenter.go @@ -9,6 +9,7 @@ import ( "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/timeutil" "math" + "strings" "time" ) @@ -18,6 +19,7 @@ func toDTO(payment model.PaymentOrder) PaymentDTO { PaymentNo: payment.PaymentNo, OrderID: payment.OrderID, OrderNo: payment.OrderNo, + PaymentConfigID: payment.PaymentConfigID, Provider: payment.Provider, ThirdOrderID: payment.ThirdOrderID, ProviderOrderID: payment.ProviderOrderID, @@ -115,3 +117,25 @@ func firstNonEmpty(values ...string) string { } return "" } + +// firstNonZero 返回第一个非零 uint64,用于保留历史支付单已有配置 ID。 +func firstNonZero(values ...uint64) uint64 { + for _, value := range values { + if value != 0 { + return value + } + } + return 0 +} + +// normalizePayWay 将前端或渠道别名统一为系统内部支付方式编码。 +func normalizePayWay(value string) string { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "", "ZFBZF", "ALIPAY", "ALIPAYPAY": + return "ZFBZF" + case "WXZF", "WECHAT", "WECHATPAY": + return "WXZF" + default: + return strings.ToUpper(strings.TrimSpace(value)) + } +} diff --git a/backend/internal/modules/payment/query.go b/backend/internal/modules/payment/query.go index 90bf42d..9c8434a 100644 --- a/backend/internal/modules/payment/query.go +++ b/backend/internal/modules/payment/query.go @@ -104,6 +104,7 @@ func (row adminPaymentRow) toDTO() AdminPaymentDTO { OrderID: row.OrderID, OrderNo: row.OrderNo, UserID: row.UserID, + PaymentConfigID: row.PaymentConfigID, UserPhone: row.UserPhone, Provider: row.Provider, MerchantID: row.MerchantID, diff --git a/backend/internal/modules/payment/refund.go b/backend/internal/modules/payment/refund.go index e8f55b2..2c8b3c8 100644 --- a/backend/internal/modules/payment/refund.go +++ b/backend/internal/modules/payment/refund.go @@ -160,6 +160,8 @@ func (r *Repository) findOriginalPayment(ctx context.Context, orderID uint64) (m } return originalPayment, nil } + +// prepareRefundOrder 创建退款支付单,并继承原支付单回溯出的支付配置。 func (r *Repository) prepareRefundOrder(ctx context.Context, originalPayment model.PaymentOrder, runtimeConfig runtimePaymentConfig, refundAmountCent int64, bizType string) (*model.PaymentOrder, bool, error) { var paymentID uint64 existing := false @@ -191,6 +193,7 @@ func (r *Repository) prepareRefundOrder(ctx context.Context, originalPayment mod OrderID: originalPayment.OrderID, OrderNo: originalPayment.OrderNo, UserID: originalPayment.UserID, + PaymentConfigID: runtimeConfig.ID, Provider: runtimeConfig.Provider, MerchantID: runtimeConfig.MerchantID, ThirdOrderID: merchantRefundID, diff --git a/backend/internal/modules/payment/repository.go b/backend/internal/modules/payment/repository.go index c02cdb2..b8c5a9b 100644 --- a/backend/internal/modules/payment/repository.go +++ b/backend/internal/modules/payment/repository.go @@ -50,6 +50,7 @@ func RefundBizTypes() []string { return out } +// NewRepository 创建支付仓库,注入支付配置仓库和订单仓库。 func NewRepository(db *gorm.DB, configRepo *paymentconfig.Repository, orderRepo *order.Repository) *Repository { return &Repository{ db: db, @@ -57,24 +58,47 @@ func NewRepository(db *gorm.DB, configRepo *paymentconfig.Repository, orderRepo orderRepo: orderRepo, } } + +// isMockMode 判断当前运行时配置是否为模拟支付。 func (c runtimePaymentConfig) isMockMode() bool { return c.Provider == "mock" } -func (r *Repository) defaultRuntimeConfig(ctx context.Context) (*runtimePaymentConfig, error) { + +// defaultRuntimeConfig 按支付方式获取默认启用配置,用于新支付单创建。 +func (r *Repository) defaultRuntimeConfig(ctx context.Context, payWay string) (*runtimePaymentConfig, error) { if r.configRepo == nil { return nil, ErrPaymentUnavailable } - dto, err := r.configRepo.FindDefaultAny(ctx, true) + 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.FindByProviderMerchant(ctx, provider, merchantID, true) + 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 } @@ -84,7 +108,7 @@ func (r *Repository) runtimeConfigForPayment(ctx context.Context, payment *model } if provider != "leshua" { if provider == "lakala" && r.configRepo != nil { - dto, err := r.configRepo.FindDefaultByProvider(ctx, provider, true) + dto, err := r.configRepo.FindDefaultByProviderPayWay(ctx, provider, payWay, true) if err != nil { return nil, err } @@ -98,15 +122,17 @@ func (r *Repository) runtimeConfigForPayment(ctx context.Context, payment *model if r.configRepo == nil { return nil, ErrPaymentUnavailable } - dto, err := r.configRepo.FindDefaultByProvider(ctx, provider, true) + 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 := firstNonEmpty(dto.PayWay, "ZFBZF") + payWay := normalizePayWay(dto.PayWay) jsPayFlag := firstNonEmpty(dto.JSPayFlag, "2") client, err := buildChannelClient(dto) if err != nil { @@ -123,6 +149,8 @@ func runtimeConfigFromDTO(dto *paymentconfig.ConfigDTO) *runtimePaymentConfig { 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 diff --git a/backend/internal/modules/paymentconfig/backup.go b/backend/internal/modules/paymentconfig/backup.go index e20fbb9..3d8b66b 100644 --- a/backend/internal/modules/paymentconfig/backup.go +++ b/backend/internal/modules/paymentconfig/backup.go @@ -50,9 +50,7 @@ func (r *Repository) ExportBackup(ctx context.Context, actorID uint64, meta Audi return backup, nil } -// ImportBackup 导入支付配置备份,按 provider + merchant_id 更新或新增。 - -// ImportBackup 导入支付配置备份,按 provider + merchant_id 更新或新增。 +// ImportBackup 导入支付配置备份,按 provider + merchant_id + pay_way 更新或新增。 func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, actorID uint64, meta AuditMeta) (*ImportBackupResult, error) { if backup.Type != "payment_config_backup" || backup.Version <= 0 { return nil, ErrInvalidBackup @@ -61,21 +59,22 @@ func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, acto return nil, ErrEmptyBackup } - activeKey := backupActiveKey(backup.Configs) + activeKeys := backupActiveKeys(backup.Configs) result := &ImportBackupResult{Total: len(backup.Configs)} err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if activeKey != "" { - if err := deactivateOtherConfigs(tx, 0, actorID); err != nil { + for payWay := range activeKeys { + if err := deactivateOtherConfigs(tx, 0, actorID, payWay); err != nil { return err } } for _, cfg := range backup.Configs { - req := importCreateRequest(cfg, activeKey) + req := importCreateRequest(cfg, activeKeys) if err := r.validateCreateRequest(req); err != nil { return err } + payWay := firstNonEmpty(req.PayWay, "ZFBZF") encryptedSignKey, err := r.encryptor.Encrypt(req.SignKey) if err != nil { @@ -93,7 +92,7 @@ func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, acto "notify_key": encryptedNotifyKey, "notify_url": req.NotifyURL, "jump_url": req.JumpURL, - "pay_way": firstNonEmpty(req.PayWay, "ZFBZF"), + "pay_way": payWay, "jspay_flag": firstNonEmpty(req.JSPayFlag, "2"), "sign_type": firstNonEmpty(req.SignType, defaultSignType(req.Provider)), "extra_config": model.JSONMap(req.ExtraConfig), @@ -106,7 +105,7 @@ func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, acto var existing model.PaymentMerchantConfig err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where("provider = ? AND merchant_id = ?", req.Provider, req.MerchantID). + Where("provider = ? AND merchant_id = ? AND pay_way = ?", req.Provider, req.MerchantID, payWay). First(&existing).Error if err == nil { if err := tx.Model(&existing).Updates(updates).Error; err != nil { @@ -129,7 +128,7 @@ func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, acto NotifyKey: encryptedNotifyKey, NotifyURL: req.NotifyURL, JumpURL: req.JumpURL, - PayWay: firstNonEmpty(req.PayWay, "ZFBZF"), + PayWay: payWay, JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"), SignType: firstNonEmpty(req.SignType, defaultSignType(req.Provider)), ExtraConfig: model.JSONMap(req.ExtraConfig), @@ -158,23 +157,30 @@ func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, acto return result, nil } -// FindActiveByProvider 查询提供商的所有激活配置 - -func backupActiveKey(configs []ConfigDTO) string { +// backupActiveKeys 提取备份中每个支付方式的启用配置键。 +func backupActiveKeys(configs []ConfigDTO) map[string]string { + keys := make(map[string]string) for _, cfg := range configs { + payWay := firstNonEmpty(cfg.PayWay, "ZFBZF") + if _, exists := keys[payWay]; exists { + continue + } if cfg.Status == "active" || cfg.IsDefault { - return paymentConfigImportKey(cfg.Provider, cfg.MerchantID) + keys[payWay] = paymentConfigImportKey(cfg.Provider, cfg.MerchantID, payWay) } } - return "" + return keys } -func importCreateRequest(cfg ConfigDTO, activeKey string) CreateRequest { +// importCreateRequest 将备份 DTO 转为创建请求,并保留每个支付方式唯一启用配置。 +func importCreateRequest(cfg ConfigDTO, activeKeys map[string]string) CreateRequest { status := cfg.Status if status == "" { status = "disabled" } - isActive := activeKey != "" && paymentConfigImportKey(cfg.Provider, cfg.MerchantID) == activeKey + payWay := firstNonEmpty(cfg.PayWay, "ZFBZF") + activeKey := activeKeys[payWay] + isActive := activeKey != "" && paymentConfigImportKey(cfg.Provider, cfg.MerchantID, payWay) == activeKey if isActive { status = "active" } else if status == "active" { @@ -190,7 +196,7 @@ func importCreateRequest(cfg ConfigDTO, activeKey string) CreateRequest { NotifyKey: cfg.NotifyKey, NotifyURL: cfg.NotifyURL, JumpURL: cfg.JumpURL, - PayWay: cfg.PayWay, + PayWay: payWay, JSPayFlag: cfg.JSPayFlag, SignType: cfg.SignType, ExtraConfig: cfg.ExtraConfig, @@ -201,6 +207,7 @@ func importCreateRequest(cfg ConfigDTO, activeKey string) CreateRequest { } } -func paymentConfigImportKey(provider string, merchantID string) string { - return provider + ":" + merchantID +// paymentConfigImportKey 生成导入去重键,同商户可按支付方式拆分为多条配置。 +func paymentConfigImportKey(provider string, merchantID string, payWay string) string { + return provider + ":" + merchantID + ":" + payWay } diff --git a/backend/internal/modules/paymentconfig/dto.go b/backend/internal/modules/paymentconfig/dto.go index 0ba660b..f2f23b3 100644 --- a/backend/internal/modules/paymentconfig/dto.go +++ b/backend/internal/modules/paymentconfig/dto.go @@ -6,6 +6,7 @@ var ( ErrConfigNotFound = errors.New("payment config not found") ErrDuplicateDefault = errors.New("duplicate default config for provider") ErrInvalidProvider = errors.New("invalid payment provider") + ErrInvalidPayWay = errors.New("invalid pay_way") ErrInvalidStatus = errors.New("invalid status") ErrInvalidBackup = errors.New("invalid payment config backup") ErrEmptyBackup = errors.New("payment config backup is empty") @@ -95,6 +96,7 @@ type UpdateRequest struct { // ListQuery 列表查询参数 type ListQuery struct { Provider string `form:"provider"` + PayWay string `form:"pay_way"` Status string `form:"status"` Environment string `form:"environment"` Page int `form:"page"` diff --git a/backend/internal/modules/paymentconfig/handler.go b/backend/internal/modules/paymentconfig/handler.go index 657c0fe..fd48be5 100644 --- a/backend/internal/modules/paymentconfig/handler.go +++ b/backend/internal/modules/paymentconfig/handler.go @@ -24,6 +24,7 @@ func NewHandler(service *Service) *Handler { // @Summary 获取支付配置列表 // @Tags 管理后台-支付配置 // @Param provider query string false "支付服务商" +// @Param pay_way query string false "支付方式" // @Param status query string false "状态" // @Param environment query string false "环境" // @Param page query int false "页码" @@ -141,7 +142,7 @@ func (h *Handler) ImportBackup(c *gin.Context) { if err == ErrInvalidBackup || err == ErrEmptyBackup || err == ErrInvalidProvider || err == ErrNameRequired || err == ErrMerchantIDRequired || err == ErrGatewayURLRequired || err == ErrSignKeyRequired || err == ErrNotifyKeyRequired || err == ErrNotifyURLRequired || - err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrAppIDRequired || + err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrInvalidPayWay || err == ErrAppIDRequired || err == ErrSerialNoRequired || err == ErrTermNoRequired { response.BadRequest(c, err.Error()) return @@ -180,7 +181,7 @@ func (h *Handler) Create(c *gin.Context) { config, err := h.service.Create(c.Request.Context(), req, adminID, auditMeta(c)) if err == ErrNameRequired || err == ErrMerchantIDRequired || err == ErrGatewayURLRequired || err == ErrSignKeyRequired || err == ErrNotifyKeyRequired || err == ErrInvalidProvider || - err == ErrNotifyURLRequired || err == ErrInvalidSignType || err == ErrAppIDRequired || + err == ErrNotifyURLRequired || err == ErrInvalidSignType || err == ErrInvalidPayWay || err == ErrAppIDRequired || err == ErrInvalidStatus || err == ErrSerialNoRequired || err == ErrTermNoRequired { response.BadRequest(c, err.Error()) return @@ -224,7 +225,7 @@ func (h *Handler) Update(c *gin.Context) { response.NotFound(c, "配置不存在") return } - if err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrNotifyURLRequired { + if err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrInvalidPayWay || err == ErrNotifyURLRequired { response.BadRequest(c, err.Error()) return } diff --git a/backend/internal/modules/paymentconfig/mutation.go b/backend/internal/modules/paymentconfig/mutation.go index 41fbcff..db20572 100644 --- a/backend/internal/modules/paymentconfig/mutation.go +++ b/backend/internal/modules/paymentconfig/mutation.go @@ -37,9 +37,10 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, actorID uint if environment == "" { environment = "production" } + payWay := firstNonEmpty(req.PayWay, "ZFBZF") isDefault := req.IsDefault if status == "active" { - if err := deactivateOtherConfigs(tx, 0, actorID); err != nil { + if err := deactivateOtherConfigs(tx, 0, actorID, payWay); err != nil { return err } isDefault = true @@ -56,7 +57,7 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, actorID uint NotifyKey: encryptedNotifyKey, NotifyURL: req.NotifyURL, JumpURL: req.JumpURL, - PayWay: firstNonEmpty(req.PayWay, "ZFBZF"), + PayWay: payWay, JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"), SignType: firstNonEmpty(req.SignType, defaultSignType(req.Provider)), ExtraConfig: req.ExtraConfig, @@ -97,6 +98,9 @@ func (r *Repository) Update(ctx context.Context, id uint64, req UpdateRequest, a if req.SignType != nil && *req.SignType != "" && !isValidSignType(*req.SignType) { return nil, ErrInvalidSignType } + if req.PayWay != nil && !isValidPayWay(*req.PayWay) { + return nil, ErrInvalidPayWay + } if req.Status != nil && !isValidStatus(*req.Status) { return nil, ErrInvalidStatus } @@ -145,7 +149,7 @@ func (r *Repository) Update(ctx context.Context, id uint64, req UpdateRequest, a updates["jump_url"] = *req.JumpURL } if req.PayWay != nil { - updates["pay_way"] = *req.PayWay + updates["pay_way"] = firstNonEmpty(*req.PayWay, "ZFBZF") } if req.JSPayFlag != nil { updates["jspay_flag"] = *req.JSPayFlag @@ -174,8 +178,15 @@ func (r *Repository) Update(ctx context.Context, id uint64, req UpdateRequest, a if req.Status != nil { finalStatus = *req.Status } + finalPayWay := item.PayWay + if req.PayWay != nil && *req.PayWay != "" { + finalPayWay = *req.PayWay + } + if finalPayWay == "" { + finalPayWay = "ZFBZF" + } if finalStatus == "active" { - if err := deactivateOtherConfigs(tx, id, actorID); err != nil { + if err := deactivateOtherConfigs(tx, id, actorID, finalPayWay); err != nil { return err } updates["status"] = "active" @@ -225,8 +236,10 @@ func (r *Repository) Delete(ctx context.Context, id uint64, actorID uint64, meta } var usedCount int64 - if err := tx.Model(&model.PaymentOrder{}). - Where("provider = ? AND merchant_id = ?", item.Provider, item.MerchantID). + usedDB := tx.Model(&model.PaymentOrder{}). + Where("payment_config_id = ?", item.ID). + Or("(payment_config_id = ? OR payment_config_id IS NULL) AND provider = ? AND merchant_id = ? AND pay_way = ?", 0, item.Provider, item.MerchantID, item.PayWay) + if err := usedDB. Count(&usedCount).Error; err != nil { return err } @@ -246,12 +259,10 @@ func (r *Repository) Delete(ctx context.Context, id uint64, actorID uint64, meta }) } -// IncrementUsage 增加使用统计 - -// deactivateOtherConfigs 保证全局同一时间只有一个启用配置。 -func deactivateOtherConfigs(tx *gorm.DB, activeID uint64, actorID uint64) error { +// deactivateOtherConfigs 保证同一个支付方式同一时间只有一个启用配置。 +func deactivateOtherConfigs(tx *gorm.DB, activeID uint64, actorID uint64, payWay string) error { db := tx.Model(&model.PaymentMerchantConfig{}). - Where("status = ? OR is_default = ?", "active", true) + Where("pay_way = ? AND (status = ? OR is_default = ?)", payWay, "active", true) if activeID > 0 { db = db.Where("id != ?", activeID) } diff --git a/backend/internal/modules/paymentconfig/query.go b/backend/internal/modules/paymentconfig/query.go index d7bb6c5..0e2bede 100644 --- a/backend/internal/modules/paymentconfig/query.go +++ b/backend/internal/modules/paymentconfig/query.go @@ -20,6 +20,9 @@ func (r *Repository) List(ctx context.Context, query ListQuery) ([]ConfigDTO, in if query.Provider != "" { db = db.Where("provider = ?", query.Provider) } + if query.PayWay != "" { + db = db.Where("pay_way = ?", query.PayWay) + } if query.Status != "" { db = db.Where("status = ?", query.Status) } @@ -46,7 +49,7 @@ func (r *Repository) List(ctx context.Context, query ListQuery) ([]ConfigDTO, in } offset := (page - 1) * pageSize - if err := db.Order("is_default DESC, id DESC").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil { + if err := db.Order("pay_way ASC, is_default DESC, id DESC").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil { return nil, 0, err } @@ -62,8 +65,6 @@ func (r *Repository) List(ctx context.Context, query ListQuery) ([]ConfigDTO, in return dtos, total, nil } -// FindByID 根据 ID 查询配置 - // FindByID 根据 ID 查询配置 func (r *Repository) FindByID(ctx context.Context, id uint64, includeSecret bool, actorID uint64, meta AuditMeta) (*ConfigDTO, error) { var item model.PaymentMerchantConfig @@ -89,9 +90,23 @@ func (r *Repository) FindByID(ctx context.Context, id uint64, includeSecret bool return &dto, nil } -// FindDefault 查询默认配置 +// FindRuntimeByID 根据配置 ID 查询运行时配置,不写审计日志,供支付单回溯原配置使用。 +func (r *Repository) FindRuntimeByID(ctx context.Context, id uint64, includeSecret bool) (*ConfigDTO, error) { + var item model.PaymentMerchantConfig + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrConfigNotFound + } + return nil, err + } + dto, err := r.toDTO(item, includeSecret) + if err != nil { + return nil, err + } + return &dto, nil +} -// FindDefault 查询默认配置 +// FindDefault 查询指定服务商的默认启用配置,兼容旧调用路径。 func (r *Repository) FindDefault(ctx context.Context, provider string) (*model.PaymentMerchantConfig, error) { var item model.PaymentMerchantConfig if err := r.db.WithContext(ctx).Where("provider = ? AND is_default = ? AND status = ?", provider, true, "active").First(&item).Error; err != nil { @@ -103,12 +118,13 @@ func (r *Repository) FindDefault(ctx context.Context, provider string) (*model.P return &item, nil } -// FindDefaultAny 查询任意服务商的默认启用配置。 - -// FindDefaultAny 查询任意服务商的默认启用配置。 -func (r *Repository) FindDefaultAny(ctx context.Context, includeSecret bool) (*ConfigDTO, error) { +// FindDefaultByPayWay 查询指定支付方式的默认启用配置。 +func (r *Repository) FindDefaultByPayWay(ctx context.Context, payWay string, includeSecret bool) (*ConfigDTO, error) { var item model.PaymentMerchantConfig - if err := r.db.WithContext(ctx).Where("status = ?", "active").Order("is_default DESC, id DESC").First(&item).Error; err != nil { + if err := r.db.WithContext(ctx). + Where("pay_way = ? AND status = ?", payWay, "active"). + Order("is_default DESC, id DESC"). + First(&item).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNoActiveConfigFound } @@ -121,8 +137,6 @@ func (r *Repository) FindDefaultAny(ctx context.Context, includeSecret bool) (*C return &dto, nil } -// FindDefaultByProvider 查询指定服务商的默认启用配置。 - // FindDefaultByProvider 查询指定服务商的默认启用配置。 func (r *Repository) FindDefaultByProvider(ctx context.Context, provider string, includeSecret bool) (*ConfigDTO, error) { item, err := r.FindDefault(ctx, provider) @@ -136,9 +150,44 @@ func (r *Repository) FindDefaultByProvider(ctx context.Context, provider string, return &dto, nil } -// FindByProviderMerchant 根据服务商和商户号查配置,用于历史支付单继续使用原商户密钥。 +// FindDefaultByProviderPayWay 查询指定服务商和支付方式的默认启用配置。 +func (r *Repository) FindDefaultByProviderPayWay(ctx context.Context, provider string, payWay string, includeSecret bool) (*ConfigDTO, error) { + var item model.PaymentMerchantConfig + if err := r.db.WithContext(ctx). + Where("provider = ? AND pay_way = ? AND status = ?", provider, payWay, "active"). + Order("is_default DESC, id DESC"). + First(&item).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNoActiveConfigFound + } + return nil, err + } + dto, err := r.toDTO(item, includeSecret) + if err != nil { + return nil, err + } + return &dto, nil +} -// FindByProviderMerchant 根据服务商和商户号查配置,用于历史支付单继续使用原商户密钥。 +// FindByProviderMerchantPayWay 根据服务商、商户号和支付方式查配置,用于旧支付单回溯原商户密钥。 +func (r *Repository) FindByProviderMerchantPayWay(ctx context.Context, provider string, merchantID string, payWay string, includeSecret bool) (*ConfigDTO, error) { + var item model.PaymentMerchantConfig + if err := r.db.WithContext(ctx).Where("provider = ? AND merchant_id = ? AND pay_way = ?", provider, merchantID, payWay). + Order("status = 'active' DESC, is_default DESC, id DESC"). + First(&item).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrConfigNotFound + } + return nil, err + } + dto, err := r.toDTO(item, includeSecret) + if err != nil { + return nil, err + } + return &dto, nil +} + +// FindByProviderMerchant 根据服务商和商户号查配置,保留给历史数据没有支付方式时兜底使用。 func (r *Repository) FindByProviderMerchant(ctx context.Context, provider string, merchantID string, includeSecret bool) (*ConfigDTO, error) { var item model.PaymentMerchantConfig if err := r.db.WithContext(ctx).Where("provider = ? AND merchant_id = ?", provider, merchantID). @@ -156,8 +205,6 @@ func (r *Repository) FindByProviderMerchant(ctx context.Context, provider string return &dto, nil } -// ExportBackup 导出所有支付配置备份,包含解密后的密钥。 - // FindActiveByProvider 查询提供商的所有激活配置 func (r *Repository) FindActiveByProvider(ctx context.Context, provider string) ([]model.PaymentMerchantConfig, error) { var items []model.PaymentMerchantConfig diff --git a/backend/internal/modules/paymentconfig/repository_test.go b/backend/internal/modules/paymentconfig/repository_test.go index 9d25b68..a5b2de0 100644 --- a/backend/internal/modules/paymentconfig/repository_test.go +++ b/backend/internal/modules/paymentconfig/repository_test.go @@ -2,15 +2,17 @@ package paymentconfig import "testing" -func TestImportCreateRequestKeepsOnlyOneActiveConfig(t *testing.T) { +func TestImportCreateRequestKeepsOneActiveConfigPerPayWay(t *testing.T) { configs := []ConfigDTO{ - {Provider: "lakala", MerchantID: "M1", Status: "active", IsDefault: true}, - {Provider: "leshua", MerchantID: "M2", Status: "active", IsDefault: false}, + {Provider: "lakala", MerchantID: "M1", PayWay: "ZFBZF", Status: "active", IsDefault: true}, + {Provider: "leshua", MerchantID: "M2", PayWay: "ZFBZF", Status: "active", IsDefault: false}, + {Provider: "leshua", MerchantID: "M3", PayWay: "WXZF", Status: "active", IsDefault: true}, } - activeKey := backupActiveKey(configs) + activeKeys := backupActiveKeys(configs) - first := importCreateRequest(configs[0], activeKey) - second := importCreateRequest(configs[1], activeKey) + first := importCreateRequest(configs[0], activeKeys) + second := importCreateRequest(configs[1], activeKeys) + third := importCreateRequest(configs[2], activeKeys) if first.Status != "active" || !first.IsDefault { t.Fatalf("first config status/default = %s/%v, want active/true", first.Status, first.IsDefault) @@ -18,6 +20,9 @@ func TestImportCreateRequestKeepsOnlyOneActiveConfig(t *testing.T) { if second.Status != "disabled" || second.IsDefault { t.Fatalf("second config status/default = %s/%v, want disabled/false", second.Status, second.IsDefault) } + if third.Status != "active" || !third.IsDefault { + t.Fatalf("third config status/default = %s/%v, want active/true", third.Status, third.IsDefault) + } } func TestImportCreateRequestPreservesTestingConfig(t *testing.T) { @@ -27,7 +32,7 @@ func TestImportCreateRequestPreservesTestingConfig(t *testing.T) { Status: "testing", } - req := importCreateRequest(cfg, "") + req := importCreateRequest(cfg, nil) if req.Status != "testing" || req.IsDefault { t.Fatalf("status/default = %s/%v, want testing/false", req.Status, req.IsDefault) diff --git a/backend/internal/modules/paymentconfig/service.go b/backend/internal/modules/paymentconfig/service.go index 37447d2..900b951 100644 --- a/backend/internal/modules/paymentconfig/service.go +++ b/backend/internal/modules/paymentconfig/service.go @@ -64,7 +64,7 @@ func (s *Service) Delete(ctx context.Context, id uint64, actorID uint64, meta Au return s.repo.Delete(ctx, id, actorID, meta) } -// GetDefaultConfig 获取默认配置(用于支付模块调用) +// GetDefaultConfig 获取指定服务商的默认配置,保留给管理工具或兼容调用使用。 func (s *Service) GetDefaultConfig(ctx context.Context, provider string) (*ConfigDTO, error) { config, err := s.repo.FindDefault(ctx, provider) if err != nil { @@ -76,3 +76,8 @@ func (s *Service) GetDefaultConfig(ctx context.Context, provider string) (*Confi } return &dto, nil } + +// GetDefaultConfigByPayWay 获取指定支付方式的默认配置,供新支付链路按微信/支付宝选渠道。 +func (s *Service) GetDefaultConfigByPayWay(ctx context.Context, payWay string) (*ConfigDTO, error) { + return s.repo.FindDefaultByPayWay(ctx, payWay, true) +} diff --git a/backend/internal/modules/paymentconfig/validation.go b/backend/internal/modules/paymentconfig/validation.go index bb5d738..747bad4 100644 --- a/backend/internal/modules/paymentconfig/validation.go +++ b/backend/internal/modules/paymentconfig/validation.go @@ -17,6 +17,9 @@ func (r *Repository) validateCreateRequest(req CreateRequest) error { if req.SignType != "" && !isValidSignType(req.SignType) { return ErrInvalidSignType } + if req.PayWay != "" && !isValidPayWay(req.PayWay) { + return ErrInvalidPayWay + } if req.Status != "" && !isValidStatus(req.Status) { return ErrInvalidStatus } @@ -71,6 +74,11 @@ func isValidSignType(value string) bool { return value == "MD5" || value == "SHA256withRSA" } +// isValidPayWay 校验支付方式是否属于系统支持的微信或支付宝编码。 +func isValidPayWay(value string) bool { + return value == "ZFBZF" || value == "WXZF" +} + func isValidStatus(value string) bool { return value == "active" || value == "disabled" || value == "testing" } diff --git a/backend/migrations/000004_payment_config_pay_way.sql b/backend/migrations/000004_payment_config_pay_way.sql new file mode 100644 index 0000000..9922086 --- /dev/null +++ b/backend/migrations/000004_payment_config_pay_way.sql @@ -0,0 +1,80 @@ +-- +goose Up + +-- 支付单记录命中的支付配置 ID,后续查询、退款和通知验签优先回溯原配置。 +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_orders' AND COLUMN_NAME = 'payment_config_id') = 0, + 'ALTER TABLE payment_orders ADD COLUMN payment_config_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''命中的支付配置ID'' AFTER user_id', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 支付单按配置 ID 查询,用于回调、退款和后台排障。 +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_orders' AND INDEX_NAME = 'idx_payment_orders_payment_config_id') = 0, + 'ALTER TABLE payment_orders ADD KEY idx_payment_orders_payment_config_id (payment_config_id)', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 支付配置按支付方式筛选默认渠道,支持微信和支付宝分别启用。 +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_merchant_configs' AND INDEX_NAME = 'idx_payment_merchant_configs_pay_way') = 0, + 'ALTER TABLE payment_merchant_configs ADD KEY idx_payment_merchant_configs_pay_way (pay_way, status, is_default)', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 历史支付单按服务商、商户号和支付方式回填命中的配置 ID。 +UPDATE payment_orders p +SET payment_config_id = ( + SELECT c.id + FROM payment_merchant_configs c + WHERE c.provider = p.provider + AND c.merchant_id = p.merchant_id + AND c.pay_way = p.pay_way + ORDER BY c.status = 'active' DESC, c.is_default DESC, c.id DESC + LIMIT 1 +) +WHERE p.payment_config_id = 0 + AND EXISTS ( + SELECT 1 + FROM payment_merchant_configs c + WHERE c.provider = p.provider + AND c.merchant_id = p.merchant_id + AND c.pay_way = p.pay_way + ); + +-- +goose Down + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_merchant_configs' AND INDEX_NAME = 'idx_payment_merchant_configs_pay_way') > 0, + 'ALTER TABLE payment_merchant_configs DROP INDEX idx_payment_merchant_configs_pay_way', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_orders' AND INDEX_NAME = 'idx_payment_orders_payment_config_id') > 0, + 'ALTER TABLE payment_orders DROP INDEX idx_payment_orders_payment_config_id', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payment_orders' AND COLUMN_NAME = 'payment_config_id') > 0, + 'ALTER TABLE payment_orders DROP COLUMN payment_config_id', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 44cd6b9..0023a2f 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -47,6 +47,7 @@ declare module 'vue' { ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElStep: typeof import('element-plus/es')['ElStep'] ElSteps: typeof import('element-plus/es')['ElSteps'] diff --git a/frontend/src/features/admin/api/paymentConfig.ts b/frontend/src/features/admin/api/paymentConfig.ts index 048d5bc..5355485 100644 --- a/frontend/src/features/admin/api/paymentConfig.ts +++ b/frontend/src/features/admin/api/paymentConfig.ts @@ -80,6 +80,7 @@ export interface UpdatePaymentConfigRequest { export async function fetchPaymentConfigs(params?: { provider?: string + pay_way?: string status?: string environment?: string page?: number diff --git a/frontend/src/features/admin/components/PaymentConfigDialog.vue b/frontend/src/features/admin/components/PaymentConfigDialog.vue index a200d70..2c161fb 100644 --- a/frontend/src/features/admin/components/PaymentConfigDialog.vue +++ b/frontend/src/features/admin/components/PaymentConfigDialog.vue @@ -31,10 +31,49 @@ const advancedOpen = ref([]) const defaultGateways: Record = { leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi', - lakala: 'https://test.wsmsd.cn/sit', mock: '', } +const lakalaGateways: Record = { + production: 'https://s2.lakala.com', + sandbox: 'https://test.wsmsd.cn/sit', +} + +const defaultNotifyURLs: Record = { + leshua: 'https://hao858.com/api/payments/leshua/notify', + lakala: 'https://hao858.com/api/payments/lakala/notify', + mock: '', +} + +const defaultJumpURLs: Record = { + leshua: 'https://hao858.com/orders', + lakala: 'https://hao858.com/orders', + mock: '', +} + +const lakalaPayModeByPayWay: Record = { + ZFBZF: 'ALIPAY', + WXZF: 'WECHAT', +} + +// defaultEnvironment 返回新建配置时的默认环境。 +function defaultEnvironment(provider: string) { + return provider === 'mock' ? 'production' : 'production' +} + +// defaultGateway 根据服务商和环境生成默认网关地址。 +function defaultGateway(provider: string, environment = defaultEnvironment(provider)) { + if (provider === 'lakala') { + return lakalaGateways[environment] || lakalaGateways.production + } + return defaultGateways[provider] || '' +} + +// isDefaultLakalaGateway 判断当前网关是否仍是拉卡拉系统默认值。 +function isDefaultLakalaGateway(value?: string) { + return !value || Object.values(lakalaGateways).includes(value) +} + function defaultExtraConfig(provider: string) { if (provider === 'lakala') { return { @@ -42,30 +81,33 @@ function defaultExtraConfig(provider: string) { serial_no: '', term_no: '', lakala_cert: '', - pay_mode: 'AUTO', + pay_mode: 'ALIPAY', order_expire_minutes: 15, } } return {} } -function makeDefaultForm(provider = 'leshua'): CreatePaymentConfigRequest { +function makeDefaultForm( + provider = 'leshua', + environment = defaultEnvironment(provider) +): CreatePaymentConfigRequest { return { name: '', provider, merchant_id: '', - gateway_url: defaultGateways[provider] || '', + gateway_url: defaultGateway(provider, environment), sign_key: '', notify_key: '', - notify_url: '', - jump_url: '', + notify_url: defaultNotifyURLs[provider] || '', + jump_url: defaultJumpURLs[provider] || '', pay_way: 'ZFBZF', jspay_flag: '2', sign_type: provider === 'lakala' ? 'SHA256withRSA' : 'MD5', extra_config: defaultExtraConfig(provider), is_default: false, status: 'active', - environment: provider === 'lakala' ? 'sandbox' : 'production', + environment, business_tags: [], } } @@ -99,6 +141,15 @@ function normalizeExtraConfig(provider: string, extraConfig?: Record props.modelValue, val => { @@ -134,12 +185,25 @@ watch( function handleProviderChange(provider: string) { if (props.mode !== 'create') return const currentName = formData.value.name - const currentNotifyURL = formData.value.notify_url + const currentPayWay = formData.value.pay_way || 'ZFBZF' + const currentEnvironment = formData.value.environment || defaultEnvironment(provider) formData.value = { - ...makeDefaultForm(provider), + ...makeDefaultForm(provider, currentEnvironment), name: currentName, - notify_url: currentNotifyURL, + pay_way: currentPayWay, } + syncLakalaPayMode(currentPayWay) +} + +// handlePayWayChange 处理支付方式切换,并联动渠道扩展配置。 +function handlePayWayChange(payWay: string | number | boolean) { + syncLakalaPayMode(String(payWay)) +} + +// handleEnvironmentChange 处理环境切换,并同步拉卡拉默认网关。 +function handleEnvironmentChange(environment: string) { + if (!isLakala.value || !isDefaultLakalaGateway(formData.value.gateway_url)) return + formData.value.gateway_url = defaultGateway('lakala', environment) } async function handleViewSecrets() { @@ -206,7 +270,7 @@ function handleClose() {