package paymentconfig import ( "context" "database/sql/driver" "errors" "time" "hfb_sys/backend/internal/model" "gorm.io/gorm" ) // List 获取配置列表 func (r *Repository) List(ctx context.Context, query ListQuery) ([]ConfigDTO, int64, error) { var items []model.PaymentMerchantConfig var total int64 db := r.db.WithContext(ctx).Model(&model.PaymentMerchantConfig{}) // 过滤条件 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) } 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("pay_way ASC, 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) } if err := r.applySuccessfulPaymentStats(ctx, dtos); err != nil { return nil, 0, err } return dtos, total, nil } // FindByID 根据 ID 查询配置 func (r *Repository) FindByID(ctx context.Context, id uint64, includeSecret bool, actorID uint64, meta AuditMeta) (*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 } statDTOs := []ConfigDTO{dto} if err := r.applySuccessfulPaymentStats(ctx, statDTOs); err != nil { return nil, err } dto = statDTOs[0] if includeSecret { if err := appendAuditLog(r.db.WithContext(ctx), actorID, "payment_config.view_secret", item.ID, meta, map[string]any{ "name": item.Name, "provider": item.Provider, "merchant_id": item.MerchantID, }); err != nil { return nil, err } } return &dto, nil } type paymentConfigSuccessStat struct { ConfigID uint64 TotalTransactions int64 TotalAmountCent int64 LastPaidAt nullableTime } // applySuccessfulPaymentStats 使用真实成功支付流水覆盖配置统计,避免仅打开二维码也被算作成交。 func (r *Repository) applySuccessfulPaymentStats(ctx context.Context, dtos []ConfigDTO) error { if len(dtos) == 0 { return nil } ids := make([]uint64, 0, len(dtos)) indexByID := make(map[uint64]int, len(dtos)) for idx, dto := range dtos { ids = append(ids, dto.ID) indexByID[dto.ID] = idx dtos[idx].TotalTransactions = 0 dtos[idx].TotalAmountCent = 0 dtos[idx].LastUsedAt = nil } var rows []paymentConfigSuccessStat if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}). Select("payment_config_id AS config_id, COUNT(*) AS total_transactions, COALESCE(SUM(amount_cent), 0) AS total_amount_cent, MAX(paid_at) AS last_paid_at"). Where("payment_config_id IN ? AND biz_type = ? AND status = ?", ids, "order_pay", "paid"). Group("payment_config_id"). Scan(&rows).Error; err != nil { return err } for _, row := range rows { idx, ok := indexByID[row.ConfigID] if !ok { continue } dtos[idx].TotalTransactions = row.TotalTransactions dtos[idx].TotalAmountCent = row.TotalAmountCent if row.LastPaidAt.Time != nil { value := row.LastPaidAt.Time.Format(time.RFC3339) dtos[idx].LastUsedAt = &value } } return nil } type nullableTime struct { Time *time.Time } // Value 实现 driver.Valuer,让 GORM 能把 nullableTime 当作普通扫描字段处理。 func (nt nullableTime) Value() (driver.Value, error) { if nt.Time == nil { return nil, nil } return *nt.Time, nil } // Scan 兼容 MySQL 的 time.Time 和 SQLite 聚合函数返回的字符串时间。 func (nt *nullableTime) Scan(value any) error { switch v := value.(type) { case nil: nt.Time = nil case time.Time: nt.Time = &v case []byte: return nt.scanString(string(v)) case string: return nt.scanString(v) default: nt.Time = nil } return nil } func (nt *nullableTime) scanString(value string) error { if value == "" { nt.Time = nil return nil } for _, layout := range []string{ time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05-07:00", "2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05", } { parsed, err := time.Parse(layout, value) if err == nil { nt.Time = &parsed return nil } } nt.Time = nil return nil } // 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 查询指定服务商的默认启用配置,兼容旧调用路径。 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 { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNoActiveConfigFound } return nil, err } return &item, nil } // 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("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 } return nil, err } dto, err := r.toDTO(item, includeSecret) if err != nil { return nil, err } return &dto, nil } // FindDefaultByProvider 查询指定服务商的默认启用配置。 func (r *Repository) FindDefaultByProvider(ctx context.Context, provider string, includeSecret bool) (*ConfigDTO, error) { item, err := r.FindDefault(ctx, provider) if err != nil { return nil, err } dto, err := r.toDTO(*item, includeSecret) if err != nil { return nil, err } return &dto, nil } // 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 } // 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). 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(ctx context.Context, provider string) ([]model.PaymentMerchantConfig, error) { var items []model.PaymentMerchantConfig if err := r.db.WithContext(ctx).Where("provider = ? AND status = ?", provider, "active").Order("is_default DESC, id DESC").Find(&items).Error; err != nil { return nil, err } return items, nil } // Create 创建配置