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.
242 lines
10 KiB
242 lines
10 KiB
package comm
|
|
|
|
import "yunyan/lego/sys/mysql"
|
|
|
|
// 应用「业务功能配置」托管(登录 / 短信 / 邮件 / 支付…)。
|
|
//
|
|
// 与「基础设施/运行配置」([[app_service_config]],存 console 公共库、按微服务分区)不同:
|
|
// 这些是应用的**独立业务配置**(微信/Google 登录、短信、邮件、微信/支付宝/苹果/Google 支付),
|
|
// 存到**应用自己的业务库**(随部署,每个区域部署各一套),不进公共库。
|
|
//
|
|
// 数据模型:一行 = 某业务模块的一个字段(module + key → value)。
|
|
// 密钥字段(AppSecret、支付商户密钥/证书、SMTP 密码…)以 AES-256-GCM 密文落库(comm.Encrypt,
|
|
// 密钥 ${FIELD_ENCRYPT_KEY}),console 与业务服务必须配同一把 key;返回前端脱敏为 EncMask。
|
|
//
|
|
// 字段清单由 ModuleCatalog 数据驱动(哪些字段、类型、是否密钥、UI 分组),
|
|
// console 后台据此渲染表单、判定加密;业务服务据 SysKey 把库值覆盖回 GetSettings().Sys[key](后置)。
|
|
|
|
// AppModuleConfig 一行 = 应用业务库里某业务模块的一个配置项。存应用业务库(mysql/pg,随部署)。
|
|
type AppModuleConfig struct {
|
|
Id uint64 `gorm:"primaryKey;autoIncrement;column:id" json:"id"`
|
|
Module string `gorm:"column:module;size:32;uniqueIndex:uidx_modcfg" json:"module"` // 模块键 wechat_login/sms/wechatpay…
|
|
Key string `gorm:"column:key;size:64;uniqueIndex:uidx_modcfg" json:"key"` // 字段键 AppID/AppSecret…
|
|
Value string `gorm:"column:value;type:text" json:"value"` // 值(密钥字段存 AES-GCM 密文)
|
|
Encrypted bool `gorm:"column:encrypted" json:"encrypted"` // 是否密钥字段(前端脱敏、落库加密)
|
|
Createtime int64 `gorm:"column:createtime" json:"createtime"`
|
|
Updatetime int64 `gorm:"column:updatetime" json:"updatetime"`
|
|
}
|
|
|
|
func (AppModuleConfig) TableName() string { return TableAppModuleConfig }
|
|
|
|
// ModuleFieldOption select 类型字段的一个选项。
|
|
type ModuleFieldOption struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// ModuleField 一个模块的一个配置字段的元数据(驱动前端表单 + 后端密钥判定)。
|
|
type ModuleField struct {
|
|
Key string `json:"key"`
|
|
Label string `json:"label"`
|
|
Type string `json:"type"` // text|password|textarea|bool|select
|
|
Secret bool `json:"secret"`
|
|
Required bool `json:"required"`
|
|
Placeholder string `json:"placeholder,omitempty"`
|
|
Desc string `json:"desc,omitempty"`
|
|
Options []ModuleFieldOption `json:"options,omitempty"`
|
|
}
|
|
|
|
// ModuleDef 一个业务模块(如"微信登录")的定义。
|
|
type ModuleDef struct {
|
|
Module string `json:"module"` // 存库键
|
|
Name string `json:"name"` // 展示名
|
|
Group string `json:"group"` // UI 分组:登录 / 通知 / 支付
|
|
SysKey string `json:"sys_key"` // 业务服务 GetSettings().Sys[key](业务消费用)
|
|
Desc string `json:"desc,omitempty"`
|
|
Fields []ModuleField `json:"fields"`
|
|
}
|
|
|
|
// 分组名。
|
|
const (
|
|
ModuleGroupAuth = "登录"
|
|
ModuleGroupNotify = "通知"
|
|
ModuleGroupPay = "支付"
|
|
)
|
|
|
|
// ModuleCatalog 业务功能配置字段目录(字段依据现有 sys/* 各 options.go 的结构体,见调研)。
|
|
var ModuleCatalog = []ModuleDef{
|
|
{
|
|
Module: "wechat_login", Name: "微信登录", Group: ModuleGroupAuth, SysKey: "wechat_auth",
|
|
Desc: "微信开放平台 App/公众号登录",
|
|
Fields: []ModuleField{
|
|
{Key: "AppID", Label: "AppID", Type: "text", Required: true},
|
|
{Key: "AppSecret", Label: "AppSecret", Type: "password", Secret: true, Required: true},
|
|
},
|
|
},
|
|
{
|
|
Module: "google_login", Name: "Google 登录", Group: ModuleGroupAuth, SysKey: "google_auth",
|
|
Desc: "Firebase Admin SDK 校验 Google/Firebase 身份令牌",
|
|
Fields: []ModuleField{
|
|
{Key: "ApiKeyFile", Label: "Firebase 服务账号密钥", Type: "textarea", Secret: true, Required: true,
|
|
Desc: "直接粘贴服务账号 JSON 内容(推荐,配置全在库);或填服务器上的文件路径"},
|
|
},
|
|
},
|
|
{
|
|
Module: "sms", Name: "短信服务", Group: ModuleGroupNotify, SysKey: "sms",
|
|
Desc: "腾讯云短信",
|
|
Fields: []ModuleField{
|
|
{Key: "AppId", Label: "短信 SDK AppID", Type: "text", Required: true},
|
|
{Key: "SecretId", Label: "SecretId", Type: "text", Required: true},
|
|
{Key: "SecretKey", Label: "SecretKey", Type: "password", Secret: true, Required: true},
|
|
{Key: "SignName", Label: "短信签名", Type: "text", Required: true},
|
|
{Key: "Template1", Label: "国内模板 ID", Type: "text"},
|
|
{Key: "Template2", Label: "国际模板 ID", Type: "text"},
|
|
},
|
|
},
|
|
{
|
|
Module: "email", Name: "邮件服务", Group: ModuleGroupNotify, SysKey: "email",
|
|
Desc: "SMTP 发信(验证码等)",
|
|
Fields: []ModuleField{
|
|
{Key: "EmailType", Label: "邮箱类型", Type: "select", Required: true, Options: []ModuleFieldOption{
|
|
{Value: "0", Label: "QQ 邮箱"}, {Value: "1", Label: "腾讯企业邮箱"}, {Value: "2", Label: "Gmail"},
|
|
}},
|
|
{Key: "FromEmail", Label: "发件人邮箱", Type: "text", Required: true},
|
|
{Key: "FromName", Label: "发件人昵称", Type: "text"},
|
|
{Key: "Password", Label: "SMTP 密码/授权码", Type: "password", Secret: true, Required: true},
|
|
},
|
|
},
|
|
{
|
|
Module: "wechatpay", Name: "微信支付", Group: ModuleGroupPay, SysKey: "wechatpay",
|
|
Fields: []ModuleField{
|
|
{Key: "AppID", Label: "AppID", Type: "text", Required: true},
|
|
{Key: "MchID", Label: "商户号", Type: "text", Required: true},
|
|
{Key: "ApiV3Key", Label: "APIv3 密钥", Type: "password", Secret: true, Required: true},
|
|
{Key: "PrivateKeyPath", Label: "商户私钥", Type: "textarea", Secret: true, Required: true,
|
|
Desc: "直接粘贴商户私钥 PEM 内容(推荐,配置全在库);或填 .pem 文件路径"},
|
|
{Key: "CertSerialNo", Label: "证书序列号", Type: "text", Required: true},
|
|
},
|
|
},
|
|
{
|
|
Module: "alipay", Name: "支付宝", Group: ModuleGroupPay, SysKey: "alipay",
|
|
Fields: []ModuleField{
|
|
{Key: "AppID", Label: "应用 AppID", Type: "text", Required: true},
|
|
{Key: "PrivateKey", Label: "应用私钥", Type: "textarea", Secret: true, Required: true, Desc: "PKCS8 格式"},
|
|
{Key: "PublicKey", Label: "支付宝公钥", Type: "textarea", Required: true},
|
|
},
|
|
},
|
|
{
|
|
Module: "appleiap", Name: "苹果支付", Group: ModuleGroupPay, SysKey: "appleiap",
|
|
Desc: "App Store 内购/订阅校验",
|
|
Fields: []ModuleField{
|
|
{Key: "AppStoreBundleID", Label: "Bundle ID", Type: "text", Required: true},
|
|
{Key: "SharedSecret", Label: "共享密钥", Type: "password", Secret: true, Desc: "legacy verifyReceipt 用"},
|
|
{Key: "AppStoreIssuerID", Label: "Issuer ID", Type: "text"},
|
|
{Key: "AppStoreKeyID", Label: "Key ID", Type: "text"},
|
|
{Key: "AppStorePrivateKeyPEM", Label: "App Store API 密钥(.p8)", Type: "textarea", Secret: true,
|
|
Desc: "ECDSA P-256 私钥 PEM 内容"},
|
|
{Key: "UseSandbox", Label: "强制沙盒环境", Type: "bool"},
|
|
},
|
|
},
|
|
{
|
|
Module: "googleiap", Name: "Google 支付", Group: ModuleGroupPay, SysKey: "googleiap",
|
|
Desc: "Google Play 内购/订阅校验",
|
|
Fields: []ModuleField{
|
|
{Key: "ServiceAccountJSON", Label: "服务账号 JSON", Type: "textarea", Secret: true, Required: true,
|
|
Desc: "Google 服务账号 JSON 内容"},
|
|
{Key: "ServiceAccountJSONPath", Label: "服务账号 JSON 路径", Type: "text",
|
|
Desc: "可选:本地文件路径(内容优先)"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// FindModuleDef 按 module 键取模块定义(找不到返回 nil)。
|
|
func FindModuleDef(module string) *ModuleDef {
|
|
for i := range ModuleCatalog {
|
|
if ModuleCatalog[i].Module == module {
|
|
return &ModuleCatalog[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ModuleFieldIsSecret 判断某模块某字段是否为密钥字段(落库加密、前端脱敏)。
|
|
// 以目录定义为准;目录里没有的键回退到通用命名规则 IsSecretField,防止漏加密。
|
|
func ModuleFieldIsSecret(module, key string) bool {
|
|
if def := FindModuleDef(module); def != nil {
|
|
for _, f := range def.Fields {
|
|
if f.Key == key {
|
|
return f.Secret
|
|
}
|
|
}
|
|
}
|
|
return IsSecretField(key)
|
|
}
|
|
|
|
// LoadOrSeedModuleConfig 「库优先,无则读 yaml 回写」——业务服务(home)启动时用。
|
|
//
|
|
// db 已连上的**应用业务库**(mysql/pg,本服务自己的库;整表即属本应用,无需 app_name/region 过滤)。
|
|
// encKey ${FIELD_ENCRYPT_KEY},须与 console 一致;密钥字段解密/加密用。
|
|
// sys GetSettings().Sys(map 引用);对每个模块,库有则把库值覆盖回 Sys[SysKey] 再供随后 OnInit 用。
|
|
//
|
|
// 按模块粒度处理:某模块在库里有行 → 覆盖 Sys(改配置需重启才生效);库里无该模块 → 把当前 yaml 值 seed 进库
|
|
// (source 语义由存在与否表达;密钥字段加密落库),Sys 保持 yaml 原值不动。best-effort:seed 单条失败即返回错误由调用方降级。
|
|
func LoadOrSeedModuleConfig(db mysql.ISys, encKey string, sys map[string]map[string]interface{}) error {
|
|
if db == nil {
|
|
return nil
|
|
}
|
|
if err := db.CreateTable(TableAppModuleConfig, &AppModuleConfig{}); err != nil {
|
|
return err
|
|
}
|
|
rows := make([]*AppModuleConfig, 0)
|
|
if err := db.Find(TableAppModuleConfig, &rows, ""); err != nil {
|
|
return err
|
|
}
|
|
byModule := make(map[string][]*AppModuleConfig)
|
|
for _, r := range rows {
|
|
byModule[r.Module] = append(byModule[r.Module], r)
|
|
}
|
|
|
|
for i := range ModuleCatalog {
|
|
def := &ModuleCatalog[i]
|
|
if existing := byModule[def.Module]; len(existing) > 0 {
|
|
// 库有:覆盖 Sys[SysKey](密钥解密、按 yaml 原值类型还原)。
|
|
sec := sys[def.SysKey]
|
|
if sec == nil {
|
|
sec = map[string]interface{}{}
|
|
sys[def.SysKey] = sec
|
|
}
|
|
for _, r := range existing {
|
|
val := r.Value
|
|
if r.Encrypted && val != "" {
|
|
if plain, e := Decrypt(encKey, val); e == nil {
|
|
val = plain
|
|
}
|
|
}
|
|
sec[r.Key] = coerceVal(val, sec[r.Key])
|
|
}
|
|
continue
|
|
}
|
|
// 库无该模块:把当前 yaml 值 seed 进库(仅 seed yaml 里确有的字段;yaml 无此段则跳过)。
|
|
sec := sys[def.SysKey]
|
|
if sec == nil {
|
|
continue
|
|
}
|
|
for _, f := range def.Fields {
|
|
v, ok := sec[f.Key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
val := stringifyVal(v)
|
|
if f.Secret && val != "" {
|
|
if ct, e := Encrypt(encKey, val); e == nil {
|
|
val = ct
|
|
}
|
|
}
|
|
row := &AppModuleConfig{Module: def.Module, Key: f.Key, Value: val, Encrypted: f.Secret}
|
|
if e := db.Insert(TableAppModuleConfig, row); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|