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.
 
 
 
 
 
 

229 lines
7.8 KiB

package console
import (
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// ============================ 应用业务功能配置 (modulecfg) ============================
//
// 「应用服务配置」页(admin appconfig.vue)下半部分的后端:读写**应用业务库**里的 app_module_config 表
// (登录/短信/邮件/支付…)。与基础设施配置(svcconf, 存 console 公共库)分属两个配置域。
//
// 业务库连接按部署 appId 从 registry 动态取(与 analyze/重置设备码同机制);
// 密钥字段(是否密钥以 comm.ModuleCatalog 为准)AES-GCM 加密落库、返回前端脱敏为 comm.EncMask。
// moduleCfgCatalog 返回业务功能配置的字段目录(前端据此渲染各模块表单、分组)。与选中应用无关。
func (this *serverComp) moduleCfgCatalog(c *gin.Context) {
writeOK(c, gin.H{"modules": comm.ModuleCatalog})
}
// moduleCfgList 列出某部署业务库里的配置项;module 为空则返回全部(用于标记哪些模块已配置)。密钥脱敏。
func (this *serverComp) moduleCfgList(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Module string `json:"module"`
}
_ = c.ShouldBindJSON(&req)
if req.AppId == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 必填")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := ensureModuleConfigTable(db); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
rows, err := listModuleConfig(db, strings.TrimSpace(req.Module))
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
for _, m := range rows {
if m.Encrypted && m.Value != "" {
m.Value = comm.EncMask
}
}
writeOK(c, gin.H{"items": rows})
}
// moduleCfgSave 保存某模块的配置到应用业务库:逐字段 upsert。
// 是否密钥以目录为准(不信前端);密钥字段回传占位/空则保留原密文,否则加密新明文;目录外的键忽略。
func (this *serverComp) moduleCfgSave(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Module string `json:"module"`
Items []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"items"`
}
if err := c.ShouldBindJSON(&req); err != nil {
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
return
}
req.Module = strings.TrimSpace(req.Module)
if req.AppId == 0 || req.Module == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 与 module 必填")
return
}
def := comm.FindModuleDef(req.Module)
if def == nil {
writeErr(c, pb.ErrorCode_ReqParameterError, "未知模块: "+req.Module)
return
}
// 目录内合法字段集,过滤前端多传的键。
allowed := make(map[string]bool, len(def.Fields))
for _, f := range def.Fields {
allowed[f.Key] = true
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := ensureModuleConfigTable(db); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
encKey := this.options.EncryptKey
now := time.Now().Unix()
saved := 0
for _, it := range req.Items {
key := strings.TrimSpace(it.Key)
if key == "" || !allowed[key] {
continue
}
old, e := getModuleConfigItem(db, req.Module, key)
if e != nil {
writeErr(c, pb.ErrorCode_DBError, e.Error())
return
}
secret := comm.ModuleFieldIsSecret(req.Module, key)
value := it.Value
if secret {
// 占位/空 → 保留原密文;否则加密新明文。
if value == "" || value == comm.EncMask {
if old != nil {
value = old.Value
} else {
value = ""
}
} else {
ct, ee := comm.Encrypt(encKey, value)
if ee != nil {
writeErr(c, pb.ErrorCode_SystemError, "加密失败: "+ee.Error())
return
}
value = ct
}
}
row := &comm.AppModuleConfig{
Module: req.Module, Key: key, Value: value, Encrypted: secret, Updatetime: now,
}
if old != nil {
row.Id, row.Createtime = old.Id, old.Createtime
} else {
row.Createtime = now
}
if e := upsertModuleConfigItem(db, row); e != nil {
writeErr(c, pb.ErrorCode_DBError, e.Error())
return
}
saved++
}
// 通知该部署热重载并取回执——让用户当场知道配置是否真的生效,
// 而不是"保存成功却还在用旧凭据"(那正是"后台显示已配置、发信却用着旧 key"的来源)。
acks := this.notifyModuleConfigChanged(req.AppId)
writeOK(c, gin.H{"saved": saved, "acks": acks})
}
// moduleCfgReset 把某模块的配置还原为**目标业务服务**配置文件(yaml)里的初始值。
//
// 重置动作必须由业务服务自己执行——yaml 只存在于它的容器里,且里面是明文密钥,
// 不该为了重置而把它们回传给 console(那等于开一个"凭签名导出所有应用密钥"的接口)。
// console 只负责发起、并把它的回执转给前端。
func (this *serverComp) moduleCfgReset(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Module string `json:"module"`
}
_ = c.ShouldBindJSON(&req)
req.Module = strings.TrimSpace(req.Module)
if req.AppId == 0 || req.Module == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 与 module 必填")
return
}
// "*" = 整库还原为配置文件初始值;否则必须是已知模块。
if req.Module != comm.ModuleResetAll && comm.FindModuleDef(req.Module) == nil {
writeErr(c, pb.ErrorCode_ReqParameterError, "未知模块: "+req.Module)
return
}
dep, err := this.module.model.getApp(req.AppId)
if err != nil || dep == nil {
writeErr(c, pb.ErrorCode_DBError, "取部署注册项失败")
return
}
report, err := callModuleConfigReset(dep.BaseUrl, this.options.EncryptKey, req.Module)
if err != nil {
// 重置未发生(业务侧要么没改动、要么已回报失败),如实把原因给前端,不做任何掩饰。
writeErr(c, pb.ErrorCode_SystemError, "重置失败: "+err.Error())
return
}
writeOK(c, gin.H{"module": req.Module, "acks": report.Acks})
}
// notifyModuleConfigChanged 调该部署的重载接口并返回回执(数组形式,前端按实例展示)。
// 任何失败都返回空数组:前端据此如实提示「未收到生效回执,需重启」,绝不静默当作成功。
func (this *serverComp) notifyModuleConfigChanged(appId uint32) []comm.ConfigApplyAck {
dep, err := this.module.model.getApp(appId)
if err != nil || dep == nil {
log.Warn("console: 取注册项失败,跳过热重载通知(该部署需重启才生效)",
log.Field{Key: "app_id", Value: appId})
return nil
}
report, err := callModuleConfigReload(dep.BaseUrl, this.options.EncryptKey)
if err != nil {
log.Warn("console: 通知部署热重载失败(该部署需重启才生效)",
log.Field{Key: "app", Value: dep.Name}, log.Field{Key: "err", Value: err.Error()})
return nil
}
return report.Acks
}
// moduleCfgDel 清空某模块在业务库里的全部配置。
func (this *serverComp) moduleCfgDel(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Module string `json:"module"`
}
_ = c.ShouldBindJSON(&req)
req.Module = strings.TrimSpace(req.Module)
if req.AppId == 0 || req.Module == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 与 module 必填")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := delModuleConfig(db, req.Module); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
acks := this.notifyModuleConfigChanged(req.AppId)
writeOK(c, gin.H{"deleted": req.Module, "acks": acks})
}