288 lines
9.7 KiB
Go
288 lines
9.7 KiB
Go
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) {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|