package service import ( "context" "encoding/json" "fmt" "os" "strconv" "strings" "sync" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "github.com/hashicorp/consul/api" ) var ( configCache = make(map[string]interface{}) // 动态配置缓存 configMu sync.RWMutex // 读写锁 consulClient *api.Client // Consul客户端 startOnce sync.Once // 确保只启动一次监听 ) // InitConsulWatcher 初始化Consul配置监听 func InitConsulWatcher(ctx context.Context) error { consulAddr := g.Cfg().MustGet(ctx, "consul.address").String() if consulAddr == "" { glog.Warning(ctx, "Consul未配置,使用本地config.yml") return nil } config := api.DefaultConfig() config.Address = consulAddr client, err := api.NewClient(config) if err != nil { glog.Errorf(ctx, "Consul客户端初始化失败: %v", err) return err } consulClient = client startOnce.Do(func() { go watchAllConfigsV2(ctx) glog.Info(ctx, "Consul配置监听已启动") }) return nil } // watchAllConfigsV2 监听Consul前缀下所有配置 func watchAllConfigsV2(ctx context.Context) { const prefix = "customerService/" kv := consulClient.KV() var lastIndex uint64 for { pairs, meta, err := kv.List(prefix, &api.QueryOptions{ WaitIndex: lastIndex, WaitTime: 5 * time.Minute, }) if err != nil { glog.Errorf(ctx, "Consul查询失败: %v", err) time.Sleep(5 * time.Second) continue } if meta.LastIndex != lastIndex { lastIndex = meta.LastIndex updateConfigCacheDiff(ctx, pairs) } } } // updateConfigCacheDiff 增量更新配置缓存 func updateConfigCacheDiff(ctx context.Context, pairs api.KVPairs) { configMu.Lock() defer configMu.Unlock() for _, pair := range pairs { key := strings.TrimPrefix(pair.Key, "customerService/") key = strings.ReplaceAll(key, "/", ".") newVal := parseValue(pair.Value) oldVal, exists := configCache[key] if !exists || oldVal != newVal { logConfigChange(ctx, key, gconv.String(oldVal), gconv.String(newVal)) configCache[key] = newVal } } } // parseValue 自动推断配置值类型 func parseValue(value []byte) interface{} { str := string(value) if i, err := strconv.Atoi(str); err == nil { return i } if b, err := strconv.ParseBool(str); err == nil { return b } var arr []interface{} if err := json.Unmarshal(value, &arr); err == nil { return arr } return str } // GetConfigInt 读取int配置 func GetConfigInt(ctx context.Context, key string) int { configMu.RLock() val, ok := configCache[key] configMu.RUnlock() if ok { return gconv.Int(val) } return g.Cfg().MustGet(ctx, key).Int() } // GetConfigString 读取string配置 func GetConfigString(ctx context.Context, key string) string { configMu.RLock() val, ok := configCache[key] configMu.RUnlock() if ok { return gconv.String(val) } return g.Cfg().MustGet(ctx, key).String() } // GetConfigBool 读取bool配置 func GetConfigBool(ctx context.Context, key string) bool { configMu.RLock() val, ok := configCache[key] configMu.RUnlock() if ok { return gconv.Bool(val) } return g.Cfg().MustGet(ctx, key).Bool() } // GetConfigStringSlice 读取字符串数组配置 func GetConfigStringSlice(ctx context.Context, key string) []string { configMu.RLock() val, ok := configCache[key] configMu.RUnlock() if ok { if arr, ok := val.([]interface{}); ok { result := make([]string, len(arr)) for i, v := range arr { result[i] = gconv.String(v) } return result } } return g.Cfg().MustGet(ctx, key).Strings() } // GetInstanceConfigStringSlice 读取实例级字符串数组配置(支持实例级负载隔离) // 优先级:实例专用配置 > 全局Consul配置 > config.yml func GetInstanceConfigStringSlice(ctx context.Context, key string) []string { // 获取实例ID(环境变量优先,hostname备用) instanceID := os.Getenv("INSTANCE_ID") if instanceID == "" { hostname, err := os.Hostname() if err == nil { instanceID = hostname } } // 如果有实例ID,先查找实例专用配置 if instanceID != "" { instanceKey := fmt.Sprintf("instance.%s.%s", instanceID, key) configMu.RLock() val, ok := configCache[instanceKey] configMu.RUnlock() if ok { if arr, ok := val.([]interface{}); ok { result := make([]string, len(arr)) for i, v := range arr { result[i] = gconv.String(v) } glog.Debugf(ctx, "🎯 使用实例专用配置: %s = %v", instanceKey, result) return result } } } // 未找到实例配置,fallback到全局配置 return GetConfigStringSlice(ctx, key) } // logConfigChange 记录配置变更 func logConfigChange(ctx context.Context, key, oldVal, newVal string) { glog.Infof(ctx, "📝 配置变更: %s = %s → %s", key, oldVal, newVal) }