package console import ( "crypto/aes" "crypto/cipher" cryptorand "crypto/rand" "encoding/base64" "fmt" "io" "strings" "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 依赖服务配置域 (svcconfig) ============================ // // 移植自 deep_server modules/console/api_svcconfig.go,适配 yunyan-sas 的 postgres 封装与本地 struct。 // 数据存 console 主库(postgres.GetSys()),与 globalconfig/mcp/会议模板同库。 // 服务字段(fields)用 jsonb 列(serializer:json)存储;加密字段值 AES-256-GCM 加密落库, // 返回前端时脱敏为 "[encrypted]"。区域覆盖:默认值 + 各区域覆盖,缺失字段继承默认值。 // ============================== AES-256-GCM 字段加密 ============================== // svcEncrypt 用 AES-256-GCM 加密明文,返回 base64(nonce‖ciphertext)。空明文返回空串。 func svcEncrypt(key, plaintext string) (string, error) { if plaintext == "" { return "", nil } block, err := aes.NewCipher(normAESKey(key)) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(cryptorand.Reader, nonce); err != nil { return "", err } ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ct), nil } // svcDecrypt 解密 svcEncrypt 产出的密文,返回明文。供业务侧内部调用(不暴露给 console 前端)。 func svcDecrypt(key, ciphertext string) (string, error) { if ciphertext == "" { return "", nil } raw, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "", fmt.Errorf("base64 解码失败: %w", err) } block, err := aes.NewCipher(normAESKey(key)) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } ns := gcm.NonceSize() if len(raw) < ns { return "", fmt.Errorf("密文过短") } plain, err := gcm.Open(nil, raw[:ns], raw[ns:], nil) if err != nil { return "", fmt.Errorf("GCM 解密失败: %w", err) } return string(plain), nil } // normAESKey 把任意字符串规整为 32 字节 AES-256 密钥(不足补零,超出截断)。 func normAESKey(key string) []byte { k := make([]byte, 32) copy(k, []byte(key)) return k } // ============================== 脱敏工具 ============================== // maskSvcFields 把返回给前端的字段列表中 encrypted=true 的 def_value 替换为 "[encrypted]"。 func maskSvcFields(fields []SvcField) { for i := range fields { if fields[i].Encrypted && fields[i].DefValue != "" { fields[i].DefValue = "[encrypted]" } } } // encryptFieldsSlice 对字段列表中 encrypted=true 的字段做加密处理: // oldMap 不为 nil 时,空值/占位符保留 oldMap[key] 密文;oldMap 为 nil 时直接加密非空值。 func encryptFieldsSlice(key string, fields []SvcField, oldMap map[string]string) error { for i := range fields { if !fields[i].Encrypted { continue } if fields[i].DefValue == "" || fields[i].DefValue == "[encrypted]" { if oldMap != nil { fields[i].DefValue = oldMap[fields[i].Key] } else { fields[i].DefValue = "" } continue } ct, err := svcEncrypt(key, fields[i].DefValue) if err != nil { return err } fields[i].DefValue = ct } return nil } // ============================== 依赖服务配置 CRUD ============================== // getSvcConfigs 列出全部依赖服务,加密字段值脱敏。 func (this *serverComp) getSvcConfigs(c *gin.Context) { items := make([]*ThirdSvcConfig, 0) if err := postgres.Find(comm.TableThirdSvcConfig, &items, ""); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } for _, item := range items { maskSvcFields(item.Fields) } writeOK(c, gin.H{"items": items}) } // addSvcConfig 新增依赖服务。encrypted=true 的字段值加密存储。 func (this *serverComp) addSvcConfig(c *gin.Context) { var m ThirdSvcConfig if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if strings.TrimSpace(m.Id) == "" || strings.TrimSpace(m.Name) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 和 名称 必填") return } // 主键重复检查 exist := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableThirdSvcConfig, exist, "id=?", m.Id); err == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 已存在: "+m.Id) return } // 加密字段 if err := encryptFieldsSlice(this.options.EncryptKey, m.Fields, nil); err != nil { writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error()) return } now := time.Now().UnixMilli() m.Createtime = now m.Updatetime = now if err := postgres.Insert(comm.TableThirdSvcConfig, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindThirdSvc, "add", 0) maskSvcFields(m.Fields) writeOK(c, &m) } // updateSvcConfig 更新依赖服务。encrypted 字段:传 "" 或 "[encrypted]" 保留原密文,否则重新加密。 func (this *serverComp) updateSvcConfig(c *gin.Context) { var m ThirdSvcConfig if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if strings.TrimSpace(m.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 必填") return } // 读旧记录,取已存储的加密字段密文 old := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableThirdSvcConfig, old, "id=?", m.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+m.Id) return } oldMap := make(map[string]string, len(old.Fields)) for _, f := range old.Fields { oldMap[f.Key] = f.DefValue } if err := encryptFieldsSlice(this.options.EncryptKey, m.Fields, oldMap); err != nil { writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error()) return } m.Createtime = old.Createtime m.Updatetime = time.Now().UnixMilli() if err := postgres.Save(comm.TableThirdSvcConfig, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", 0) maskSvcFields(m.Fields) writeOK(c, &m) } // delSvcConfig 删除服务及其所有区域覆盖。 func (this *serverComp) delSvcConfig(c *gin.Context) { var req struct { Id string `json:"id"` } if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } if err := postgres.Delete(comm.TableThirdSvcConfig, "id=?", req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } _ = postgres.Delete(comm.TableThirdSvcRegionOverride, "svc_id=?", req.Id) broadcastConfigChanged(comm.ConfigKindThirdSvc, "delete", 0) writeOK(c, gin.H{"id": req.Id}) } // ============================== 区域字段覆盖 ============================== // svcRegionFieldView 是返回给前端的合并视图(字段定义 + 有效值 + 是否被覆盖)。 type svcRegionFieldView struct { Key string `json:"key"` Description string `json:"description"` Encrypted bool `json:"encrypted"` IsOverridden bool `json:"is_overridden"` Value string `json:"value"` // 加密字段显示 [encrypted] } // getSvcRegionOverride 返回某服务在指定区域的字段有效值(覆盖优先,否则默认值,加密字段脱敏)。 func (this *serverComp) getSvcRegionOverride(c *gin.Context) { var req struct { SvcId string `json:"svc_id"` Region int32 `json:"region"` } if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" || req.Region == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 和 region 必填") return } cfg := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil { writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId) return } ovr := &SvcRegionOverride{} _ = postgres.FindOne(comm.TableThirdSvcRegionOverride, ovr, "svc_id=? AND region=?", req.SvcId, req.Region) if ovr.Overrides == nil { ovr.Overrides = make(map[string]string) } views := make([]*svcRegionFieldView, 0, len(cfg.Fields)) for _, f := range cfg.Fields { val, overridden := ovr.Overrides[f.Key] if !overridden { val = f.DefValue } if f.Encrypted && val != "" { val = "[encrypted]" } views = append(views, &svcRegionFieldView{ Key: f.Key, Description: f.Description, Encrypted: f.Encrypted, IsOverridden: overridden, Value: val, }) } writeOK(c, gin.H{"svc_id": req.SvcId, "region": req.Region, "override_id": ovr.Id, "fields": views}) } // saveSvcRegionOverride 保存(upsert)某服务在指定区域的字段覆盖。 // overrides map 中加密字段:空值/占位符保留原密文,非空重新加密;非加密字段直接存。 // 空值表示继承(不写入 newOverrides,即删除该字段覆盖)。 func (this *serverComp) saveSvcRegionOverride(c *gin.Context) { var req struct { SvcId string `json:"svc_id"` Region int32 `json:"region"` Overrides map[string]string `json:"overrides"` } if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" || req.Region == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 和 region 必填") return } if req.Overrides == nil { req.Overrides = make(map[string]string) } // 读服务字段定义,确定哪些字段需要加密 cfg := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil { writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId) return } encFields := make(map[string]bool, len(cfg.Fields)) for _, f := range cfg.Fields { if f.Encrypted { encFields[f.Key] = true } } // 读旧覆盖,用于保留旧密文 old := &SvcRegionOverride{} _ = postgres.FindOne(comm.TableThirdSvcRegionOverride, old, "svc_id=? AND region=?", req.SvcId, req.Region) if old.Overrides == nil { old.Overrides = make(map[string]string) } key := this.options.EncryptKey newOverrides := make(map[string]string, len(req.Overrides)) for k, v := range req.Overrides { if encFields[k] { if v == "" || v == "[encrypted]" { // 加密字段留空/占位:保留旧密文(若有),否则视为继承(不写入) if oldVal, ok := old.Overrides[k]; ok { newOverrides[k] = oldVal } } else { ct, err := svcEncrypt(key, v) if err != nil { writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error()) return } newOverrides[k] = ct } } else { // 非加密字段:空值表示继承/删除覆盖,不写入 newOverrides if v != "" { newOverrides[k] = v } } } ovr := &SvcRegionOverride{ Id: old.Id, SvcId: req.SvcId, Region: req.Region, Overrides: newOverrides, Updatetime: time.Now().UnixMilli(), } var err error if ovr.Id == 0 { err = postgres.Insert(comm.TableThirdSvcRegionOverride, ovr) } else { err = postgres.Save(comm.TableThirdSvcRegionOverride, ovr) } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", req.Region) writeOK(c, gin.H{"ok": true, "override_id": ovr.Id}) } // syncSvcToRegions 把某服务的字段值(来自默认或指定区域覆盖)批量推送到多个目标区域。 // src_region=0 时使用字段 def_value;>0 时使用该区域的覆盖值。 func (this *serverComp) syncSvcToRegions(c *gin.Context) { var req struct { SvcId string `json:"svc_id"` SrcRegion int32 `json:"src_region"` TargetRegions []int32 `json:"target_regions"` } if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 必填") return } if len(req.TargetRegions) == 0 { writeOK(c, gin.H{"synced": 0}) return } // 确定同步源 overrides map var srcOverrides map[string]string if req.SrcRegion == 0 { cfg := &ThirdSvcConfig{} if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil { writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId) return } srcOverrides = make(map[string]string, len(cfg.Fields)) for _, f := range cfg.Fields { srcOverrides[f.Key] = f.DefValue } } else { src := &SvcRegionOverride{} if err := postgres.FindOne(comm.TableThirdSvcRegionOverride, src, "svc_id=? AND region=?", req.SvcId, req.SrcRegion); err != nil { writeErr(c, pb.ErrorCode_DBError, "源区域覆盖不存在") return } srcOverrides = src.Overrides } now := time.Now().UnixMilli() synced := 0 for _, tgt := range req.TargetRegions { old := &SvcRegionOverride{} _ = postgres.FindOne(comm.TableThirdSvcRegionOverride, old, "svc_id=? AND region=?", req.SvcId, tgt) ovr := &SvcRegionOverride{ Id: old.Id, SvcId: req.SvcId, Region: tgt, Overrides: srcOverrides, Updatetime: now, } var err error if ovr.Id == 0 { err = postgres.Insert(comm.TableThirdSvcRegionOverride, ovr) } else { err = postgres.Save(comm.TableThirdSvcRegionOverride, ovr) } if err == nil { synced++ broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", tgt) } } writeOK(c, gin.H{"synced": synced}) }