实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s

This commit is contained in:
2026-07-23 17:52:48 +09:00
parent f7c42252c0
commit 17782d5f8e
76 changed files with 5246 additions and 158 deletions

View File

@@ -0,0 +1,149 @@
package systemconfig
import (
"context"
stderrors "errors"
"sort"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AlertSink 接收配置读取和缓存故障的中文安全告警。
type AlertSink interface {
Warn(ctx context.Context, code, component, safeID, summary string)
}
// LogAlertSink 使用应用日志承接安全告警。
type LogAlertSink struct {
logger *zap.Logger
}
// NewLogAlertSink 创建系统配置日志告警 Adapter。
func NewLogAlertSink(logger *zap.Logger) *LogAlertSink {
if logger == nil {
logger = zap.NewNop()
}
return &LogAlertSink{logger: logger}
}
// Warn 记录不包含配置值的中文安全告警。
func (s *LogAlertSink) Warn(_ context.Context, code, component, safeID, summary string) {
s.logger.Warn(summary, zap.String("error_code", code), zap.String("component", component), zap.String("safe_id", safeID))
}
// Reader 提供以 PostgreSQL 为唯一事实来源的缓存读取能力。
type Reader struct {
db *gorm.DB
registry *Registry
cache Cache
alerts AlertSink
}
func (r *Reader) warn(ctx context.Context, code, key, summary string) {
if r.alerts != nil {
r.alerts.Warn(ctx, code, "system_config", key, summary)
}
}
// NewReader 创建系统配置读取器。
func NewReader(db *gorm.DB, registry *Registry, cache Cache, alerts AlertSink) *Reader {
return &Reader{db: db, registry: registry, cache: cache, alerts: alerts}
}
// Get 读取单个已注册配置Redis 故障时回退 PostgreSQL。
func (r *Reader) Get(ctx context.Context, key string) (string, error) {
definition, registered := r.registry.Get(key)
if !registered {
return "", stderrors.New("系统配置 Key 未注册")
}
cacheKey := constants.RedisSystemConfigKey(key)
if r.cache != nil {
if value, err := r.cache.Get(ctx, cacheKey); err == nil {
if ValidateValue(definition, value) == nil {
r.registry.Remember(key, value)
return value, nil
}
r.warn(ctx, "SYSTEM_CONFIG_CACHE_INVALID", key, "系统配置缓存值非法,已回退 PostgreSQL")
} else if err != redis.Nil {
r.warn(ctx, "SYSTEM_CONFIG_CACHE_READ_FAILED", key, "系统配置缓存读取失败,已回退 PostgreSQL")
}
}
var record model.SystemConfig
err := r.db.WithContext(ctx).Where("config_key = ?", key).First(&record).Error
if err == gorm.ErrRecordNotFound {
return definition.DefaultValue, nil
}
if err != nil {
return "", err
}
value := record.ConfigValue
if ValidateValue(definition, value) != nil {
value = r.registry.LastValidatedOrDefault(definition)
r.warn(ctx, "SYSTEM_CONFIG_DATABASE_VALUE_INVALID", key, "数据库配置值非法,已使用最近验证值或安全默认值")
} else {
r.registry.Remember(key, value)
}
if r.cache != nil {
if err := r.cache.Set(ctx, cacheKey, value, constants.SystemConfigCacheTTL); err != nil {
r.warn(ctx, "SYSTEM_CONFIG_CACHE_WRITE_FAILED", key, "系统配置缓存回填失败")
}
}
return value, nil
}
// ListItem 是查询层组装 DTO 所需的稳定投影。
type ListItem struct {
Definition Definition
Record *model.SystemConfig
Value string
Registered bool
}
// List 合并代码注册表和数据库遗留记录;未注册记录强制只读。
func (r *Reader) List(ctx context.Context, module string) ([]ListItem, error) {
var records []model.SystemConfig
query := r.db.WithContext(ctx).Order("config_key ASC")
if module != "" {
query = query.Where("module = ?", module)
}
if err := query.Find(&records).Error; err != nil {
return nil, err
}
byKey := make(map[string]*model.SystemConfig, len(records))
for index := range records {
byKey[records[index].ConfigKey] = &records[index]
}
items := make([]ListItem, 0, len(records)+len(r.registry.List()))
for _, definition := range r.registry.List() {
if module != "" && definition.Module != module {
continue
}
record := byKey[definition.Key]
value := definition.DefaultValue
if record != nil {
value = record.ConfigValue
if ValidateValue(definition, value) != nil {
value = r.registry.LastValidatedOrDefault(definition)
r.warn(ctx, "SYSTEM_CONFIG_DATABASE_VALUE_INVALID", definition.Key, "数据库配置值非法,列表已使用安全值")
} else {
r.registry.Remember(definition.Key, value)
}
delete(byKey, definition.Key)
}
items = append(items, ListItem{Definition: definition, Record: record, Value: value, Registered: true})
}
for _, record := range byKey {
definition := Definition{
Key: record.ConfigKey, Module: record.Module, ValueType: record.ValueType,
Description: record.Description, Readonly: true, Sensitive: record.IsSensitive, Control: "readonly",
}
items = append(items, ListItem{Definition: definition, Record: record, Value: record.ConfigValue, Registered: false})
}
sort.Slice(items, func(i, j int) bool { return items[i].Definition.Key < items[j].Definition.Key })
return items, nil
}