// Package systemconfig 提供受控系统配置的简单写事务脚本。 package systemconfig import ( "context" "crypto/sha256" "encoding/hex" stderrors "errors" "time" "gorm.io/gorm" "gorm.io/gorm/clause" configinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // ChangeAudit 是系统配置更新交给统一审计 Port 的事实。 type ChangeAudit struct { OperatorID uint OperationType string Description string ConfigKey string BeforeData map[string]any AfterData map[string]any RequestID string CorrelationID string } // AuditWriter 由 tech-global-audit 提供事务内实现。 type AuditWriter interface { WriteConfigChange(ctx context.Context, tx *gorm.DB, audit ChangeAudit) error } // UnavailableAuditWriter 是统一审计尚未注入时的失败关闭策略。 type UnavailableAuditWriter struct{} // WriteConfigChange 拒绝在缺少统一审计时修改敏感配置。 func (UnavailableAuditWriter) WriteConfigChange(_ context.Context, _ *gorm.DB, _ ChangeAudit) error { return stderrors.New("统一审计接缝尚未配置") } // UpdateService 执行单 Key 校验、事务更新、审计和提交后缓存失效。 type UpdateService struct { db *gorm.DB registry *configinfra.Registry cache configinfra.Cache audit AuditWriter alerts configinfra.AlertSink now func() time.Time } // NewUpdateService 创建系统配置更新事务脚本。 func NewUpdateService( db *gorm.DB, registry *configinfra.Registry, cache configinfra.Cache, audit AuditWriter, alerts configinfra.AlertSink, now func() time.Time, ) *UpdateService { if audit == nil { audit = UnavailableAuditWriter{} } if now == nil { now = time.Now } return &UpdateService{db: db, registry: registry, cache: cache, audit: audit, alerts: alerts, now: now} } // Execute 更新一个已注册且非只读的配置 Key。 func (s *UpdateService) Execute(ctx context.Context, key string, request dto.UpdateSystemConfigRequest) (*dto.SystemConfigItem, error) { if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeSuperAdmin { return nil, errors.New(errors.CodeForbidden) } operatorID := middleware.GetUserIDFromContext(ctx) if operatorID == 0 || key == "" { return nil, errors.New(errors.CodeInvalidParam) } definition, registered := s.registry.Get(key) if !registered { return nil, errors.New(errors.CodeInvalidParam, "系统配置 Key 未注册") } if definition.Readonly { return nil, errors.New(errors.CodeInvalidStatus, "系统配置为只读,不能更新") } if err := configinfra.ValidateValue(definition, request.Value); err != nil { return nil, errors.New(errors.CodeInvalidParam, "系统配置值不符合注册规则") } now := s.now().UTC() var saved model.SystemConfig err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 同一 Key 的首次创建和后续更新都由 PostgreSQL 事务级咨询锁串行裁决。 if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", key).Error; err != nil { return err } var existing model.SystemConfig findErr := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("config_key = ?", key).First(&existing).Error beforeValue := definition.DefaultValue if findErr == nil { beforeValue = existing.ConfigValue } else if findErr != gorm.ErrRecordNotFound { return findErr } if findErr == gorm.ErrRecordNotFound { existing = model.SystemConfig{ ConfigKey: key, ConfigValue: request.Value, ValueType: definition.ValueType, Module: definition.Module, Description: definition.Description, IsReadonly: definition.Readonly, IsSensitive: definition.Sensitive, Creator: operatorID, Updater: operatorID, CreatedAt: now, UpdatedAt: now, } if err := tx.Create(&existing).Error; err != nil { return err } } else { if err := tx.Model(&model.SystemConfig{}).Where("id = ?", existing.ID).Updates(map[string]any{ "config_value": request.Value, "value_type": definition.ValueType, "module": definition.Module, "description": definition.Description, "is_readonly": definition.Readonly, "is_sensitive": definition.Sensitive, "updater": operatorID, "updated_at": now, }).Error; err != nil { return err } existing.ConfigValue = request.Value existing.Updater = operatorID existing.UpdatedAt = now } requestID := "" if value := middleware.GetRequestIDFromContext(ctx); value != nil { requestID = *value } if err := s.audit.WriteConfigChange(ctx, tx, ChangeAudit{ OperatorID: operatorID, OperationType: "system_config_update", Description: "更新受控系统配置", ConfigKey: key, BeforeData: map[string]any{"config_key": key, "value": auditValue(definition, beforeValue)}, AfterData: map[string]any{"config_key": key, "value": auditValue(definition, request.Value)}, RequestID: requestID, CorrelationID: requestID, }); err != nil { return err } saved = existing return nil }) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新系统配置失败") } s.registry.Remember(key, request.Value) if s.cache != nil { if err := s.cache.Delete(ctx, constants.RedisSystemConfigKey(key)); err != nil { if s.alerts != nil { s.alerts.Warn(ctx, "SYSTEM_CONFIG_CACHE_INVALIDATE_FAILED", "system_config", key, "系统配置缓存失效失败,数据库事实已提交") } } } value := saved.ConfigValue if definition.Sensitive && value != "" { value = "[已配置]" } updatedAt := saved.UpdatedAt return &dto.SystemConfigItem{ ConfigKey: key, Value: value, ValueType: definition.ValueType, Module: definition.Module, Description: definition.Description, Readonly: definition.Readonly, Sensitive: definition.Sensitive, Registered: true, Control: definition.Control, EnumValues: definition.EnumValues, Min: definition.Min, Max: definition.Max, UpdatedAt: &updatedAt, }, nil } func auditValue(definition configinfra.Definition, value string) string { if !definition.Sensitive { return value } sum := sha256.Sum256([]byte(value)) return "[敏感值 sha256:" + hex.EncodeToString(sum[:8]) + "]" }