package paymentconfig import ( "encoding/json" "errors" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB encryptor Encryptor } type AuditMeta = auditlog.Meta func NewRepository(db *gorm.DB, encryptor Encryptor) *Repository { return &Repository{ db: db, encryptor: encryptor, } } // List 获取配置列表 func (r *Repository) List(query ListQuery) ([]ConfigDTO, int64, error) { var items []model.PaymentMerchantConfig var total int64 db := r.db.Model(&model.PaymentMerchantConfig{}) // 过滤条件 if query.Provider != "" { db = db.Where("provider = ?", query.Provider) } if query.Status != "" { db = db.Where("status = ?", query.Status) } if query.Environment != "" { db = db.Where("environment = ?", query.Environment) } // 获取总数 if err := db.Count(&total).Error; err != nil { return nil, 0, err } // 分页 page := query.Page if page < 1 { page = 1 } pageSize := query.PageSize if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize if err := db.Order("is_default DESC, id DESC").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil { return nil, 0, err } dtos := make([]ConfigDTO, 0, len(items)) for _, item := range items { dto, err := r.toDTO(item, false) if err != nil { return nil, 0, err } dtos = append(dtos, dto) } return dtos, total, nil } // FindByID 根据 ID 查询配置 func (r *Repository) FindByID(id uint64, includeSecret bool) (*ConfigDTO, error) { var item model.PaymentMerchantConfig if err := r.db.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 查询默认配置 func (r *Repository) FindDefault(provider string) (*model.PaymentMerchantConfig, error) { var item model.PaymentMerchantConfig if err := r.db.Where("provider = ? AND is_default = ? AND status = ?", provider, true, "active").First(&item).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNoActiveConfigFound } return nil, err } return &item, nil } // FindDefaultAny 查询任意服务商的默认启用配置。 func (r *Repository) FindDefaultAny(includeSecret bool) (*ConfigDTO, error) { var item model.PaymentMerchantConfig if err := r.db.Where("status = ?", "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 } // FindDefaultByProvider 查询指定服务商的默认启用配置。 func (r *Repository) FindDefaultByProvider(provider string, includeSecret bool) (*ConfigDTO, error) { item, err := r.FindDefault(provider) if err != nil { return nil, err } dto, err := r.toDTO(*item, includeSecret) if err != nil { return nil, err } return &dto, nil } // FindByProviderMerchant 根据服务商和商户号查配置,用于历史支付单继续使用原商户密钥。 func (r *Repository) FindByProviderMerchant(provider string, merchantID string, includeSecret bool) (*ConfigDTO, error) { var item model.PaymentMerchantConfig if err := r.db.Where("provider = ? AND merchant_id = ?", provider, merchantID). 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 } // FindActiveByProvider 查询提供商的所有激活配置 func (r *Repository) FindActiveByProvider(provider string) ([]model.PaymentMerchantConfig, error) { var items []model.PaymentMerchantConfig if err := r.db.Where("provider = ? AND status = ?", provider, "active").Order("is_default DESC, id DESC").Find(&items).Error; err != nil { return nil, err } return items, nil } // Create 创建配置 func (r *Repository) Create(req CreateRequest, actorID uint64, meta AuditMeta) (*ConfigDTO, error) { // 验证必填字段 if err := r.validateCreateRequest(req); err != nil { return nil, err } var dto ConfigDTO err := r.db.Transaction(func(tx *gorm.DB) error { // 如果设置为默认,先取消同 provider 的其他默认配置 if req.IsDefault { if err := tx.Model(&model.PaymentMerchantConfig{}). Where("provider = ? AND is_default = ?", req.Provider, true). Update("is_default", false).Error; err != nil { return err } } // 加密密钥 encryptedSignKey, err := r.encryptor.Encrypt(req.SignKey) if err != nil { return ErrEncryptionFailed } encryptedNotifyKey, err := r.encryptor.Encrypt(req.NotifyKey) if err != nil { return ErrEncryptionFailed } status := req.Status if status == "" { status = "active" } environment := req.Environment if environment == "" { environment = "production" } item := model.PaymentMerchantConfig{ Name: req.Name, Provider: req.Provider, MerchantID: req.MerchantID, GatewayURL: req.GatewayURL, SignKey: encryptedSignKey, NotifyKey: encryptedNotifyKey, NotifyURL: req.NotifyURL, JumpURL: req.JumpURL, PayWay: firstNonEmpty(req.PayWay, "ZFBZF"), JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"), SignType: firstNonEmpty(req.SignType, "MD5"), ExtraConfig: req.ExtraConfig, IsDefault: req.IsDefault, Status: status, Environment: environment, BusinessTags: req.BusinessTags, CreatedBy: &actorID, UpdatedBy: &actorID, } if err := tx.Create(&item).Error; err != nil { return err } // 记录审计日志 if err := appendAuditLog(tx, actorID, "payment_config.create", item.ID, meta, map[string]any{ "name": item.Name, "provider": item.Provider, }); err != nil { return err } dto, err = r.toDTO(item, false) return err }) if err != nil { return nil, err } return &dto, nil } // Update 更新配置 func (r *Repository) Update(id uint64, req UpdateRequest, actorID uint64, meta AuditMeta) (*ConfigDTO, error) { if req.SignType != nil && *req.SignType != "" && *req.SignType != "MD5" { return nil, ErrInvalidSignType } if req.NotifyURL != nil && *req.NotifyURL == "" { return nil, ErrNotifyURLRequired } var dto ConfigDTO err := r.db.Transaction(func(tx *gorm.DB) error { var item model.PaymentMerchantConfig if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).First(&item).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrConfigNotFound } return err } // 如果设置为默认,先取消同 provider 的其他默认配置 if req.IsDefault != nil && *req.IsDefault && !item.IsDefault { if err := tx.Model(&model.PaymentMerchantConfig{}). Where("provider = ? AND is_default = ? AND id != ?", item.Provider, true, id). Update("is_default", false).Error; err != nil { return err } } updates := make(map[string]any) if req.Name != nil { updates["name"] = *req.Name } if req.MerchantID != nil { updates["merchant_id"] = *req.MerchantID } if req.GatewayURL != nil { updates["gateway_url"] = *req.GatewayURL } if req.SignKey != nil { encrypted, err := r.encryptor.Encrypt(*req.SignKey) if err != nil { return ErrEncryptionFailed } updates["sign_key"] = encrypted } if req.NotifyKey != nil { encrypted, err := r.encryptor.Encrypt(*req.NotifyKey) if err != nil { return ErrEncryptionFailed } updates["notify_key"] = encrypted } if req.NotifyURL != nil { updates["notify_url"] = *req.NotifyURL } if req.JumpURL != nil { updates["jump_url"] = *req.JumpURL } if req.PayWay != nil { updates["pay_way"] = *req.PayWay } if req.JSPayFlag != nil { updates["jspay_flag"] = *req.JSPayFlag } if req.SignType != nil { updates["sign_type"] = *req.SignType } if req.ExtraConfig != nil { updates["extra_config"] = req.ExtraConfig } if req.IsDefault != nil { updates["is_default"] = *req.IsDefault } if req.Status != nil { updates["status"] = *req.Status } if req.Environment != nil { updates["environment"] = *req.Environment } if req.BusinessTags != nil { updates["business_tags"] = req.BusinessTags } updates["updated_by"] = actorID if err := tx.Model(&item).Updates(updates).Error; err != nil { return err } // 记录审计日志 if err := appendAuditLog(tx, actorID, "payment_config.update", item.ID, meta, map[string]any{ "updates": updates, }); err != nil { return err } // 重新查询 if err := tx.Where("id = ?", id).First(&item).Error; err != nil { return err } var dtoErr error dto, dtoErr = r.toDTO(item, false) return dtoErr }) if err != nil { return nil, err } return &dto, nil } // Delete 删除配置 func (r *Repository) Delete(id uint64, actorID uint64, meta AuditMeta) error { return r.db.Transaction(func(tx *gorm.DB) error { var item model.PaymentMerchantConfig if err := tx.Where("id = ?", id).First(&item).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrConfigNotFound } return err } var usedCount int64 if err := tx.Model(&model.PaymentOrder{}). Where("provider = ? AND merchant_id = ?", item.Provider, item.MerchantID). Count(&usedCount).Error; err != nil { return err } if usedCount > 0 { return ErrCannotDeleteInUse } if err := tx.Delete(&item).Error; err != nil { return err } // 记录审计日志 return appendAuditLog(tx, actorID, "payment_config.delete", item.ID, meta, map[string]any{ "name": item.Name, "provider": item.Provider, }) }) } // IncrementUsage 增加使用统计 func (r *Repository) IncrementUsage(id uint64, amountCent int64) error { now := time.Now() return r.db.Model(&model.PaymentMerchantConfig{}).Where("id = ?", id).Updates(map[string]any{ "total_transactions": gorm.Expr("total_transactions + ?", 1), "total_amount_cent": gorm.Expr("total_amount_cent + ?", amountCent), "last_used_at": now, }).Error } // RecordUsage 记录支付配置命中情况,同一个支付单只记录一次。 func (r *Repository) RecordUsage(configID uint64, paymentOrderID uint64, provider string, merchantID string, amountCent int64, bizType string) error { if configID == 0 || paymentOrderID == 0 { return nil } return r.db.Transaction(func(tx *gorm.DB) error { var existing model.PaymentConfigUsageLog err := tx.Where("payment_order_id = ?", paymentOrderID).First(&existing).Error if err == nil { return nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } log := model.PaymentConfigUsageLog{ ConfigID: configID, PaymentOrderID: paymentOrderID, Provider: provider, MerchantID: merchantID, AmountCent: amountCent, BizType: bizType, } if err := tx.Create(&log).Error; err != nil { return err } now := time.Now() return tx.Model(&model.PaymentMerchantConfig{}).Where("id = ?", configID).Updates(map[string]any{ "total_transactions": gorm.Expr("total_transactions + ?", 1), "total_amount_cent": gorm.Expr("total_amount_cent + ?", amountCent), "last_used_at": now, }).Error }) } // toDTO 转换为 DTO func (r *Repository) toDTO(item model.PaymentMerchantConfig, includeSecret bool) (ConfigDTO, error) { dto := ConfigDTO{ ID: item.ID, Name: item.Name, Provider: item.Provider, MerchantID: item.MerchantID, GatewayURL: item.GatewayURL, NotifyURL: item.NotifyURL, JumpURL: item.JumpURL, PayWay: item.PayWay, JSPayFlag: item.JSPayFlag, SignType: item.SignType, ExtraConfig: item.ExtraConfig, IsDefault: item.IsDefault, Status: item.Status, Environment: item.Environment, BusinessTags: item.BusinessTags, TotalTransactions: item.TotalTransactions, TotalAmountCent: item.TotalAmountCent, CreatedBy: item.CreatedBy, UpdatedBy: item.UpdatedBy, CreatedAt: item.CreatedAt.Format(time.RFC3339), UpdatedAt: item.UpdatedAt.Format(time.RFC3339), } if item.LastUsedAt != nil { s := item.LastUsedAt.Format(time.RFC3339) dto.LastUsedAt = &s } // 只有明确要求时才解密并返回密钥 if includeSecret { signKey, err := r.encryptor.Decrypt(item.SignKey) if err != nil { return ConfigDTO{}, ErrDecryptionFailed } notifyKey, err := r.encryptor.Decrypt(item.NotifyKey) if err != nil { return ConfigDTO{}, ErrDecryptionFailed } dto.SignKey = signKey dto.NotifyKey = notifyKey } return dto, nil } // validateCreateRequest 验证创建请求 func (r *Repository) validateCreateRequest(req CreateRequest) error { if req.Name == "" { return ErrNameRequired } if req.Provider == "" { return ErrInvalidProvider } if req.MerchantID == "" { return ErrMerchantIDRequired } if req.SignType != "" && req.SignType != "MD5" { return ErrInvalidSignType } // leshua 特定验证 if req.Provider == "leshua" { if req.GatewayURL == "" { return ErrGatewayURLRequired } if req.SignKey == "" { return ErrSignKeyRequired } if req.NotifyKey == "" { return ErrNotifyKeyRequired } if req.NotifyURL == "" { return ErrNotifyURLRequired } } return nil } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error { detailJSON, err := json.Marshal(detail) if err != nil { return err } log := model.AuditLog{ ActorType: "admin", ActorID: actorID, Action: action, BizType: "payment_config", BizID: &bizID, IP: meta.IP, UserAgent: meta.UserAgent, Detail: detailJSON, } return tx.Create(&log).Error }