You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

176 lines
6.9 KiB

package comm
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"yunyan/lego/sys/postgres"
)
// 应用×服务的「基础设施/运行配置」托管。
//
// 目标:业务服务启动时——
// 1. 先用本地 yaml/env 连上 console 共享库(postgres);
// 2. 若库中没有本(应用,区域,服务)的配置,则把当前文件里的基础设施配置(DSN/Redis/NATS/COS…) seed 进库(source=file),
// 这样 console「应用的服务配置」页立刻能看到默认值;
// 3. 若库中已有,则以库为准,覆盖内存里的 Sys 配置(后台改过的值生效),再继续初始化 mysql/redis/cos 等。
//
// 注意:postgres 自身是 bootstrap(要先连它才能读配置),故它不纳入托管;其覆盖也不会在本次启动生效。
// 密钥字段(DSN/密码/secret/token)以 AES-256-GCM 密文落库(comm.Encrypt,密钥 ${FIELD_ENCRYPT_KEY}),
// console 与业务服务必须配同一把 key。
// svcRuntimeSections 纳入托管的 Sys 子系统(基础设施/运行配置)。postgres/log/lgid 等 bootstrap 不含。
var svcRuntimeSections = []string{"mysql", "redis", "nats", "cos", "email", "translate"}
// AppServiceConfig 一行 = 某应用×区域×服务 的一个配置项(扁平化点号无关,直接存 section+key)。
// 存 console 共享库(postgres.GetSys()),与 global_config 同库;console 前端读写、业务服务启动 seed/读取。
type AppServiceConfig struct {
Id uint64 `gorm:"primaryKey;autoIncrement;column:id" json:"id"`
AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_svcconf" json:"app_name"` // 应用名(app_registry.app_name)
Region string `gorm:"column:region;size:32;uniqueIndex:uidx_svcconf" json:"region"` // 区域(app_registry.region),可空
Service string `gorm:"column:service;size:32;uniqueIndex:uidx_svcconf" json:"service"` // 服务类型 api/gateway/home…
Section string `gorm:"column:section;size:32;uniqueIndex:uidx_svcconf" json:"section"` // 配置分区 mysql/redis/nats/cos…
Key string `gorm:"column:key;size:64;uniqueIndex:uidx_svcconf" json:"key"` // 配置键 Dsn/Addr/Password…
Value string `gorm:"column:value;type:text" json:"value"` // 值(密钥字段存 AES-GCM 密文;非标量存 JSON)
Encrypted bool `gorm:"column:encrypted" json:"encrypted"` // 是否密钥字段(前端脱敏、落库加密)
Source string `gorm:"column:source;size:16" json:"source"` // file=启动 seed 自文件 / db=后台改过
Description string `gorm:"column:description;size:255" json:"description"`
Sort int32 `gorm:"column:sort" json:"sort"`
Createtime int64 `gorm:"column:createtime" json:"createtime"`
Updatetime int64 `gorm:"column:updatetime" json:"updatetime"`
}
func (AppServiceConfig) TableName() string { return TableAppServiceConfig }
// IsSecretField 判断某配置键是否为需加密/脱敏的密钥字段(DSN 含库密码,一并算密钥)。
func IsSecretField(key string) bool {
k := strings.ToLower(key)
return strings.Contains(k, "password") || strings.Contains(k, "pwd") ||
strings.Contains(k, "secret") || strings.Contains(k, "token") ||
strings.Contains(k, "privatekey") || k == "dsn"
}
// stringifyVal 把任意配置值转为可落库的字符串:标量用 fmt,容器(list/map)用 JSON。
func stringifyVal(v interface{}) string {
switch v.(type) {
case string:
return v.(string)
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return fmt.Sprint(v)
default:
if b, err := json.Marshal(v); err == nil {
return string(b)
}
return fmt.Sprint(v)
}
}
// coerceVal 把库里的字符串按内存中原值 orig 的类型还原,供覆盖 Sys map 用。orig 为 nil 时按字符串处理。
func coerceVal(s string, orig interface{}) interface{} {
if orig == nil {
// 库里有、文件里没有的键:尝试当 JSON,否则原样字符串。
var any interface{}
if json.Unmarshal([]byte(s), &any) == nil {
switch any.(type) {
case []interface{}, map[string]interface{}:
return any
}
}
return s
}
switch orig.(type) {
case string:
return s
case bool:
if b, err := strconv.ParseBool(s); err == nil {
return b
}
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
return int(n)
}
case float32, float64:
if f, err := strconv.ParseFloat(s, 64); err == nil {
return f
}
default:
// list/map:反序列化回同结构(失败则保留原值)。
nv := reflect.New(reflect.TypeOf(orig)).Interface()
if json.Unmarshal([]byte(s), nv) == nil {
return reflect.ValueOf(nv).Elem().Interface()
}
}
return orig
}
// LoadOrSeedServiceConfig 「库优先,无则读文件回写」。返回 seeded=true 表示本次是首次 seed。
//
// encKey ${FIELD_ENCRYPT_KEY},与 console 一致;用于密钥字段加解密。
// app 应用名(空则直接跳过,返回 false,nil——未接入托管的服务不产生孤儿数据)。
// region 区域(可空)。service 服务类型(如 "api")。
// sys GetSettings().Sys(map 引用),有覆盖时就地改写其内容,供随后 OnInit 使用。
func LoadOrSeedServiceConfig(encKey, app, region, service string, sys map[string]map[string]interface{}) (seeded bool, err error) {
if strings.TrimSpace(app) == "" {
return false, nil
}
if err = postgres.CreateTable(TableAppServiceConfig, &AppServiceConfig{}); err != nil {
return false, err
}
rows := make([]*AppServiceConfig, 0)
if err = postgres.Find(TableAppServiceConfig, &rows,
"app_name=? AND region=? AND service=?", app, region, service); err != nil {
return false, err
}
// 库已有:以库为准,覆盖内存 Sys(postgres 分区跳过——它是 bootstrap,本次已连)。
if len(rows) > 0 {
for _, r := range rows {
if r.Section == "postgres" {
continue
}
sec := sys[r.Section]
if sec == nil {
sec = map[string]interface{}{}
sys[r.Section] = sec
}
val := r.Value
if r.Encrypted {
if plain, e := Decrypt(encKey, r.Value); e == nil {
val = plain
}
}
sec[r.Key] = coerceVal(val, sec[r.Key])
}
return false, nil
}
// 库为空:把当前文件里的基础设施配置 seed 进库(source=file)。
for _, section := range svcRuntimeSections {
kv := sys[section]
if kv == nil {
continue
}
var sort int32
for k, v := range kv {
enc := IsSecretField(k)
val := stringifyVal(v)
if enc && val != "" {
if ct, e := Encrypt(encKey, val); e == nil {
val = ct
}
}
row := &AppServiceConfig{
AppName: app, Region: region, Service: service,
Section: section, Key: k, Value: val, Encrypted: enc, Source: "file", Sort: sort,
}
if e := postgres.Insert(TableAppServiceConfig, row); e != nil {
return false, e
}
sort++
}
}
return true, nil
}