package integration import ( "context" "encoding/json" "errors" integrationbiz "kra/internal/biz/integration" "strings" "time" "kra/internal/config" "kra/internal/integration/runtimeconfig" "gorm.io/gorm" ) type ConfigPO struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time Kind string `gorm:"size:32;not null;uniqueIndex:idx_integration_kind_provider"` Provider string `gorm:"size:64;not null;uniqueIndex:idx_integration_kind_provider"` Enabled bool Config string `gorm:"type:text;not null"` } func (ConfigPO) TableName() string { return "sys_integration_configs" } // ConfigRow is one integration configuration row without its PO: the parent // data package owns the storage and email upgrade paths, but the table shape // stays private here. type ConfigRow struct { Provider string Enabled bool Config string } // HasConfigTable reports whether the integration configuration table exists, // which callers use to tell "not migrated yet" from "no rows". func HasConfigTable(db *gorm.DB) bool { return db != nil && cleanSession(db).Migrator().HasTable(&ConfigPO{}) } // ListConfigs returns every row of a kind in insertion order. func ListConfigs(db *gorm.DB, kind string) ([]ConfigRow, error) { var rows []ConfigPO if err := cleanSession(db).Where("kind = ?", kind).Order("id ASC").Find(&rows).Error; err != nil { return nil, err } result := make([]ConfigRow, 0, len(rows)) for _, row := range rows { result = append(result, ConfigRow{Provider: row.Provider, Enabled: row.Enabled, Config: row.Config}) } return result, nil } // FindConfig returns one row and whether it exists. A missing row is not an // error because every caller treats it as "fall back to the legacy value". func FindConfig(db *gorm.DB, kind, provider string) (ConfigRow, bool, error) { var row ConfigPO err := cleanSession(db).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return ConfigRow{}, false, nil } if err != nil { return ConfigRow{}, false, err } return ConfigRow{Provider: row.Provider, Enabled: row.Enabled, Config: row.Config}, true, nil } // UpsertConfigs writes every given row of a kind in one transaction, inserting // the ones that do not exist yet and updating the rest in place. func UpsertConfigs(db *gorm.DB, kind string, rows []ConfigRow) error { return cleanSession(db).Transaction(func(tx *gorm.DB) error { for _, item := range rows { var current ConfigPO err := tx.Where("kind = ? AND provider = ?", kind, item.Provider).First(¤t).Error switch { case errors.Is(err, gorm.ErrRecordNotFound): err = tx.Create(&ConfigPO{Kind: kind, Provider: item.Provider, Enabled: item.Enabled, Config: item.Config}).Error case err == nil: err = tx.Model(¤t).Updates(map[string]any{"enabled": item.Enabled, "config": item.Config}).Error } if err != nil { return err } } return nil }) } // cleanSession drops conditions inherited from the caller's handle so a scoped // query cannot leak a WHERE clause into these statements. func cleanSession(db *gorm.DB) *gorm.DB { return db.Session(&gorm.Session{NewDB: true}) } type integrationConfigRepo struct{ data Provider } type paymentConfigReader struct{ data Provider } func NewIntegrationConfigRepo(data Provider) integrationbiz.IntegrationConfigRepo { return &integrationConfigRepo{data: data} } // NewPaymentConfigReader exposes only the raw payment configuration needed by // the payment data module. The ConfigPO and its table name stay private here. func NewPaymentConfigReader(data Provider) integrationbiz.PaymentConfigReader { return &paymentConfigReader{data: data} } func (r *paymentConfigReader) ReadPaymentConfig(ctx context.Context, provider string) (*integrationbiz.PaymentConfig, error) { if r == nil || r.data == nil || r.data.DB() == nil { return nil, errors.New("集成配置数据库未初始化") } provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "" { return nil, errors.New("支付渠道不能为空") } var row ConfigPO if err := r.data.DB().WithContext(ctx). Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindPayment, provider). First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, integrationbiz.ErrPaymentConfigNotFound } return nil, err } return &integrationbiz.PaymentConfig{Enabled: row.Enabled, Values: append(json.RawMessage(nil), []byte(row.Config)...)}, nil } func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*integrationbiz.IntegrationConfig, error) { var rows []ConfigPO if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil { return nil, err } result := make([]*integrationbiz.IntegrationConfig, 0, len(rows)) for _, row := range rows { result = append(result, integrationConfigFromPO(row)) } return result, nil } func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*integrationbiz.IntegrationConfig, error) { var row ConfigPO if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("集成配置不存在") } return nil, err } return integrationConfigFromPO(row), nil } func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *integrationbiz.IntegrationConfig) error { db := r.data.DB().WithContext(ctx) var row ConfigPO err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error values := integrationObject(config.Values) if errors.Is(err, gorm.ErrRecordNotFound) { mergeIntegrationSecrets(config.Kind, config.Provider, values, map[string]any{}) if config.Enabled { if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err } } encoded, _ := json.Marshal(values) if err := db.Create(&ConfigPO{Kind: config.Kind, Provider: config.Provider, Enabled: config.Enabled, Config: string(encoded)}).Error; err != nil { return err } r.publish(config.Kind, config.Provider, config.Enabled, encoded) return nil } if err != nil { return err } mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config))) if config.Enabled { if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err } } encoded, _ := json.Marshal(values) if err := db.Model(&row).Updates(map[string]any{"enabled": config.Enabled, "config": string(encoded)}).Error; err != nil { return err } r.publish(config.Kind, config.Provider, config.Enabled, encoded) return nil } func (r *integrationConfigRepo) DeleteIntegrationConfig(ctx context.Context, kind, provider string) error { if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).Delete(&ConfigPO{}).Error; err != nil { return err } if runtime := integrationRuntime(r.data); runtime != nil { runtime.Delete(kind, provider) } return nil } func (r *integrationConfigRepo) publish(kind, provider string, enabled bool, values []byte) { if runtime := integrationRuntime(r.data); runtime != nil { runtime.Set(runtimeconfig.Config{Kind: kind, Provider: provider, Enabled: enabled, Values: values}) } } func integrationRuntime(provider Provider) *runtimeconfig.Store { if provider != nil { return provider.IntegrationRuntime() } return nil } func integrationConfigFromPO(row ConfigPO) *integrationbiz.IntegrationConfig { values := integrationObject(json.RawMessage(row.Config)) maskIntegrationSecrets(row.Kind, row.Provider, values) encoded, _ := json.Marshal(values) return &integrationbiz.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded} } func integrationObject(raw json.RawMessage) map[string]any { values := map[string]any{} _ = json.Unmarshal(raw, &values) return values } func maskIntegrationSecrets(kind, provider string, values map[string]any) { secretFields := integrationSecretFields(kind, provider) for key, value := range values { if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) { if text, ok := value.(string); ok && text != "" { values[key] = config.MaskedSecret } continue } if nested, ok := value.(map[string]any); ok { maskIntegrationSecrets(kind, provider, nested) } if nested, ok := value.([]any); ok { for _, item := range nested { if child, ok := item.(map[string]any); ok { maskIntegrationSecrets(kind, provider, child) } } } } } func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) { secretFields := integrationSecretFields(kind, provider) for key, value := range values { if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) { if text, ok := value.(string); ok && text == config.MaskedSecret { if prior, exists := old[key]; exists { values[key] = prior } else { values[key] = "" } } continue } if nested, ok := value.(map[string]any); ok { if prior, ok := old[key].(map[string]any); ok { mergeIntegrationSecrets(kind, provider, nested, prior) } } if nested, ok := value.([]any); ok { if prior, ok := old[key].([]any); ok { mergeIntegrationSecretArrays(kind, provider, nested, prior) } } } } func mergeIntegrationSecretArrays(kind, provider string, values, old []any) { for i, item := range values { if nested, ok := item.(map[string]any); ok { if i < len(old) { if prior, ok := old[i].(map[string]any); ok { mergeIntegrationSecrets(kind, provider, nested, prior) } } } } } func integrationSecretFields(kind, provider string) map[string]bool { result := map[string]bool{} if definition, ok := integrationbiz.IntegrationDefinition(kind, provider); ok { for _, field := range definition.Fields { if field.Secret { result[field.Key] = true } } } return result }