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}) }