diff --git a/server/internal/handler/config.go b/server/internal/handler/config.go
index bb2209d..0daa12d 100644
--- a/server/internal/handler/config.go
+++ b/server/internal/handler/config.go
@@ -231,6 +231,11 @@ func (h *ConfigHandler) Add(c *gin.Context) {
}
}
+ if err := h.parseService.ValidateConfig(&config); err != nil {
+ c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: err.Error()})
+ return
+ }
+
if _, err := dao.Engine.Insert(&config); err != nil {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 500, Msg: "保存失败"})
return
@@ -264,6 +269,15 @@ func (h *ConfigHandler) Update(c *gin.Context) {
return
}
+ // 校验新内容语法,避免保存后客户端解析失败
+ newConfig := config
+ newConfig.Contents = req.Contents
+ newConfig.Version = req.Version
+ if err := h.parseService.ValidateConfig(&newConfig); err != nil {
+ c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: err.Error()})
+ return
+ }
+
// save history
h.saveHistory(&config, userInfo.Name, "update")
diff --git a/server/internal/handler/config_api.go b/server/internal/handler/config_api.go
index ec53d4b..74ad5c0 100644
--- a/server/internal/handler/config_api.go
+++ b/server/internal/handler/config_api.go
@@ -108,8 +108,12 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0)",
req.Env, version, *app.GroupId).Find(&globalConfigs)
for _, cfg := range globalConfigs {
+ kvMap, err := h.parseService.ParseConfigToKVMap(&cfg)
+ if err != nil {
+ logger.SugaredLogger.Errorf("[FetchAllConfigs] parse config %d (%s) error: %v", cfg.Id, cfg.Name, err)
+ continue
+ }
configIds = append(configIds, cfg.Id)
- kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k, v := range kvMap {
result[k] = v
}
@@ -121,8 +125,12 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ?",
req.Env, version, app.Id).Find(&appConfigs)
for _, cfg := range appConfigs {
+ kvMap, err := h.parseService.ParseConfigToKVMap(&cfg)
+ if err != nil {
+ logger.SugaredLogger.Errorf("[FetchAllConfigs] parse config %d (%s) error: %v", cfg.Id, cfg.Name, err)
+ continue
+ }
configIds = append(configIds, cfg.Id)
- kvMap := h.parseService.ParseConfigToKVMap(&cfg)
for k, v := range kvMap {
result[k] = v
}
@@ -197,7 +205,11 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ? AND updated_at <= ?",
req.Env, version, *app.GroupId, lastUpdateTime, upperBound).Find(&deletedGlobalConfigs)
for _, cfg := range deletedGlobalConfigs {
- kvMap := h.parseService.ParseConfigToKVMap(&cfg)
+ kvMap, err := h.parseService.ParseConfigToKVMap(&cfg)
+ if err != nil {
+ logger.SugaredLogger.Errorf("[FetchChangedConfigs] parse deleted config %d (%s) error: %v", cfg.Id, cfg.Name, err)
+ continue
+ }
for k := range kvMap {
deletedKeys = append(deletedKeys, k)
}
@@ -207,7 +219,11 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 0 AND app_id = ? AND updated_at > ? AND updated_at <= ?",
req.Env, version, app.Id, lastUpdateTime, upperBound).Find(&deletedAppConfigs)
for _, cfg := range deletedAppConfigs {
- kvMap := h.parseService.ParseConfigToKVMap(&cfg)
+ kvMap, err := h.parseService.ParseConfigToKVMap(&cfg)
+ if err != nil {
+ logger.SugaredLogger.Errorf("[FetchChangedConfigs] parse deleted config %d (%s) error: %v", cfg.Id, cfg.Name, err)
+ continue
+ }
for k := range kvMap {
deletedKeys = append(deletedKeys, k)
}
@@ -222,8 +238,9 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND group_id = ? AND (app_id is null OR app_id = 0) AND updated_at > ? AND updated_at <= ?",
req.Env, version, *app.GroupId, lastUpdateTime, upperBound).Find(&globalConfigs)
for _, cfg := range globalConfigs {
- configIds = append(configIds, cfg.Id)
- h.addChangedConfigs(result, &cfg, req.FetchAll)
+ if h.addChangedConfigs(result, &cfg, req.FetchAll) {
+ configIds = append(configIds, cfg.Id)
+ }
}
}
@@ -232,8 +249,9 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
dao.Engine.Where("env = ? AND version = ? AND enabled = 1 AND app_id = ? AND updated_at > ? AND updated_at <= ?",
req.Env, version, app.Id, lastUpdateTime, upperBound).Find(&appConfigs1)
for _, cfg := range appConfigs1 {
- configIds = append(configIds, cfg.Id)
- h.addChangedConfigs(result, &cfg, req.FetchAll)
+ if h.addChangedConfigs(result, &cfg, req.FetchAll) {
+ configIds = append(configIds, cfg.Id)
+ }
}
result["latest.fetch.timestamp"] = strconv.FormatInt(upperBound.UnixMilli(), 10)
@@ -266,11 +284,16 @@ func (h *ConfigApiHandler) FetchChangedConfigs(c *gin.Context) {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: result})
}
-func (h *ConfigApiHandler) addChangedConfigs(result map[string]string, config *model.AppConfig, fetchAll bool) {
- kvMap := h.parseService.ParseConfigToKVMap(config)
+func (h *ConfigApiHandler) addChangedConfigs(result map[string]string, config *model.AppConfig, fetchAll bool) bool {
+ kvMap, err := h.parseService.ParseConfigToKVMap(config)
+ if err != nil {
+ logger.SugaredLogger.Errorf("[FetchChangedConfigs] parse config %d (%s) error: %v", config.Id, config.Name, err)
+ return false
+ }
for k, v := range kvMap {
result[k] = fmt.Sprintf("%v", v)
}
+ return true
}
func (h *ConfigApiHandler) recordClientInstances(ip string, configIds []int, appName, env, fetchType string) {
diff --git a/server/internal/service/config_parse.go b/server/internal/service/config_parse.go
index b645485..e4983db 100644
--- a/server/internal/service/config_parse.go
+++ b/server/internal/service/config_parse.go
@@ -2,7 +2,10 @@ package service
import (
"encoding/json"
+ "encoding/xml"
+ "errors"
"fmt"
+ "io"
"kenaito-conf/server/internal/model"
"strings"
@@ -15,29 +18,44 @@ func NewConfigParseService() *ConfigParseService {
return &ConfigParseService{}
}
-// ParseConfigToKVMap parses config content based on type
-func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) map[string]interface{} {
+// ParseConfigToKVMap 解析配置内容,语法错误时返回 error 而不是静默返回空结果。
+func (s *ConfigParseService) ParseConfigToKVMap(config *model.AppConfig) (map[string]interface{}, error) {
result := make(map[string]interface{})
switch config.Type {
case 1: // file
- if strings.HasSuffix(strings.ToLower(config.Name), ".xml") {
- s.parseXML(result, config.Contents)
- } else if strings.HasSuffix(strings.ToLower(config.Name), ".yml") ||
- strings.HasSuffix(strings.ToLower(config.Name), ".yaml") {
- s.parseYAML(result, config.Contents)
- } else {
+ name := strings.ToLower(config.Name)
+ switch {
+ case strings.HasSuffix(name, ".xml"):
+ if err := s.parseXML(result, config.Contents); err != nil {
+ return nil, fmt.Errorf("解析 XML 配置 %q 失败: %w", config.Name, err)
+ }
+ case strings.HasSuffix(name, ".yml"), strings.HasSuffix(name, ".yaml"):
+ if err := s.parseYAML(result, config.Contents); err != nil {
+ return nil, fmt.Errorf("解析 YAML 配置 %q 失败: %w", config.Name, err)
+ }
+ default:
s.parseProperties(result, config.Contents)
}
case 2: // key-value
result[config.Name] = config.Contents
case 3: // JSON
- json.Unmarshal([]byte(config.Contents), &result)
+ if err := json.Unmarshal([]byte(config.Contents), &result); err != nil {
+ return nil, fmt.Errorf("解析 JSON 配置 %q 失败: %w", config.Name, err)
+ }
+ default:
+ return nil, fmt.Errorf("不支持的配置类型: %d", config.Type)
}
// resolve placeholders
s.resolvePlaceholders(result)
- return result
+ return result, nil
+}
+
+// ValidateConfig 在保存配置前校验语法。
+func (s *ConfigParseService) ValidateConfig(config *model.AppConfig) error {
+ _, err := s.ParseConfigToKVMap(config)
+ return err
}
func (s *ConfigParseService) parseProperties(result map[string]interface{}, content string) {
@@ -59,49 +77,71 @@ func (s *ConfigParseService) parseProperties(result map[string]interface{}, cont
}
}
-func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) {
- // simple XML parsing for value format
- // This is a simplified version - for production use a proper XML parser
- lines := strings.Split(content, "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if !strings.Contains(line, "value 结构,
+// 支持嵌套标签、CDATA 和实体转义,语法错误会返回 error。
+func (s *ConfigParseService) parseXML(result map[string]interface{}, content string) error {
+ decoder := xml.NewDecoder(strings.NewReader(content))
- // extract value
- valStart := strings.Index(line, ">")
- if valStart < 0 {
- continue
+ var (
+ currentKey string
+ currentValue strings.Builder
+ depth int
+ )
+
+ for {
+ token, err := decoder.Token()
+ if errors.Is(err, io.EOF) {
+ break
}
- valEnd := strings.Index(line, "")
- if valEnd < 0 {
- continue
+ if err != nil {
+ return err
}
- value := strings.TrimSpace(line[valStart+1 : valEnd])
- if key != "" && value != "" {
- result[key] = value
+
+ switch t := token.(type) {
+ case xml.StartElement:
+ if depth == 0 && t.Name.Local == "entry" {
+ currentKey = ""
+ for _, attr := range t.Attr {
+ if attr.Name.Local == "key" {
+ currentKey = attr.Value
+ break
+ }
+ }
+ currentValue.Reset()
+ depth = 1
+ } else if depth > 0 {
+ depth++
+ }
+ case xml.CharData:
+ if depth > 0 {
+ currentValue.Write(t)
+ }
+ case xml.EndElement:
+ if depth > 0 {
+ depth--
+ if depth == 0 {
+ value := strings.TrimSpace(currentValue.String())
+ if currentKey != "" && value != "" {
+ result[currentKey] = value
+ }
+ }
+ }
}
}
+
+ if depth != 0 {
+ return errors.New("XML 标签未闭合")
+ }
+ return nil
}
-func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) {
+func (s *ConfigParseService) parseYAML(result map[string]interface{}, content string) error {
var data map[string]interface{}
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
- return
+ return err
}
s.flattenYAML("", result, data)
+ return nil
}
func (s *ConfigParseService) flattenYAML(prefix string, result map[string]interface{}, data map[string]interface{}) {
@@ -134,7 +174,7 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
result := value
maxIterations := 100
processedPlaceholders := make(map[string]bool)
-
+
for i := 0; i < maxIterations; i++ {
start := strings.Index(result, "${")
if start < 0 {
@@ -173,7 +213,7 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
// 标记已处理
processedPlaceholders[refKey] = true
-
+
result = result[:start] + replacement + result[end+1:]
}
return result
diff --git a/server/internal/service/config_parse_test.go b/server/internal/service/config_parse_test.go
new file mode 100644
index 0000000..ce74a2e
--- /dev/null
+++ b/server/internal/service/config_parse_test.go
@@ -0,0 +1,88 @@
+package service
+
+import (
+ "fmt"
+ "testing"
+
+ "kenaito-conf/server/internal/model"
+)
+
+func TestParseConfigToKVMap(t *testing.T) {
+ svc := NewConfigParseService()
+
+ tests := []struct {
+ name string
+ config model.AppConfig
+ wantKey string
+ wantVal string
+ wantErr bool
+ }{
+ {
+ name: "properties with placeholder",
+ config: model.AppConfig{Type: 1, Name: "app.properties", Contents: "a=1\nb=${a}-2\n"},
+ wantKey: "b",
+ wantVal: "1-2",
+ },
+ {
+ name: "json",
+ config: model.AppConfig{Type: 3, Name: "app.json", Contents: `{"a":"1","b":"2"}`},
+ wantKey: "a",
+ wantVal: "1",
+ },
+ {
+ name: "invalid json",
+ config: model.AppConfig{Type: 3, Name: "app.json", Contents: `{"a":`},
+ wantErr: true,
+ },
+ {
+ name: "yaml",
+ config: model.AppConfig{Type: 1, Name: "app.yaml", Contents: "a:\n b: 1\n"},
+ wantKey: "a.b",
+ wantVal: "1",
+ },
+ {
+ name: "invalid yaml",
+ config: model.AppConfig{Type: 1, Name: "app.yaml", Contents: "a: [1,\n"},
+ wantErr: true,
+ },
+ {
+ name: "xml with escaped value",
+ config: model.AppConfig{Type: 1, Name: "app.xml", Contents: `1 & 2`},
+ wantKey: "a",
+ wantVal: "1 & 2",
+ },
+ {
+ name: "invalid xml",
+ config: model.AppConfig{Type: 1, Name: "app.xml", Contents: `1`},
+ wantErr: true,
+ },
+ {
+ name: "key-value",
+ config: model.AppConfig{Type: 2, Name: "a", Contents: "1"},
+ wantKey: "a",
+ wantVal: "1",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := svc.ParseConfigToKVMap(&tt.config)
+ if tt.wantErr {
+ if err == nil {
+ t.Fatalf("expected an error, got nil")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ value, ok := got[tt.wantKey]
+ if !ok {
+ t.Fatalf("key %q not found in %v", tt.wantKey, got)
+ }
+ if fmt.Sprint(value) != tt.wantVal {
+ t.Fatalf("value of %q = %v, want %q", tt.wantKey, value, tt.wantVal)
+ }
+ })
+ }
+}