package integration import ( "context" "encoding/json" "errors" integrationbiz "kra/internal/biz/integration" "strings" "time" "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" } 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) { 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] || likelyIntegrationSecret(key) { if text, ok := value.(string); ok && text != "" { values[key] = "******" } continue } if nested, ok := value.(map[string]any); ok { maskIntegrationSecrets(kind, provider, nested) } } } func mergeIntegrationSecrets(kind, provider string, values, old map[string]any) { secretFields := integrationSecretFields(kind, provider) for key, value := range values { if secretFields[key] || likelyIntegrationSecret(key) { if text, ok := value.(string); ok && text == "******" { if prior, exists := old[key]; exists { values[key] = prior } } continue } if nested, ok := value.(map[string]any); ok { if prior, ok := old[key].(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 } func likelyIntegrationSecret(key string) bool { normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) if strings.Contains(normalized, "secret") || strings.Contains(normalized, "private") || strings.Contains(normalized, "password") || strings.Contains(normalized, "credential") { return true } for _, item := range []string{"key", "token", "access_token", "api_key", "mch_key", "client_key", "certificate", "cert", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} { if normalized == item || strings.HasSuffix(normalized, "_"+item) { return true } } return false }