Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
完成运营商实名回调、业务事件观测序列与受控配置装配,同时恢复 UR43 已交付的 packages[].remove 字段及旧响应兼容,统一更新 OpenSpec、OpenAPI 和交付文档。 Constraint: 七月测试环境里程碑不新增或运行自动化测试 Rejected: 以必填 operation_type 替换 packages[].remove | 会破坏已交付前端契约 Confidence: high Scope-risk: broad Directive: 后续修改系列套餐管理接口必须保持 packages[].remove 和 ShopSeriesGrantResponse 兼容 Tested: go run ./cmd/gendocs;go build -buildvcs=false ./...;openspec validate complete-july-iteration-test-release --strict;git diff --check Not-tested: 按本 Change 约定未运行 go test,真实运营商与 Gateway 联调延期
169 lines
5.9 KiB
Go
169 lines
5.9 KiB
Go
// Package systemconfig 提供受控系统配置的简单写事务脚本。
|
|
package systemconfig
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"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 可选接收系统配置事务内审计事实。
|
|
type AuditWriter interface {
|
|
WriteConfigChange(ctx context.Context, tx *gorm.DB, audit ChangeAudit) error
|
|
}
|
|
|
|
// 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 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 s.audit != nil {
|
|
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]) + "]"
|
|
}
|