package admin import ( "encoding/json" "fmt" "github.com/go-nunu/nunu-layout-advanced/internal/service" "github.com/tidwall/gjson" "go.uber.org/zap" "strconv" "strings" ) // --- 1. 集中化字段路径配置 --- // FieldPathConfig 定义了提取一个特定字段所需的所有信息 type FieldPathConfig struct { // Paths 是一个优先级列表,解析器会从前到后尝试这些路径 Paths []string // FieldType 指示字段的预期类型,用于特殊处理(如'array_object', 'array_string', 'bool') FieldType string } // apiFieldMappings 是驱动整个解析逻辑的核心配置 // Key: API名称的关键字 (e.g., "web", "tcp", "allowAndDeny") // Value: 一个映射,定义了该API类型下需要提取的各个字段及其查找路径 var apiFieldMappings = map[string]map[string]FieldPathConfig{ "web": { "Comment": {Paths: []string{"comment", "data.comment", "desc"}}, "Port": {Paths: []string{"port", "data.port"}}, "Domain": {Paths: []string{"domain", "data.domain", "host"}}, "IsHttps": {Paths: []string{"isHttps", "data.isHttps"}}, "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, "BackendList": {Paths: []string{"backendList", "data.backendList", "backends"}, FieldType: "array_object"}, }, "tcp": { "Comment": {Paths: []string{"comment", "data.comment", "desc"}}, "Port": {Paths: []string{"port", "data.port"}}, "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, "AddrBackendList": {Paths: []string{"addrBackendList", "data.addrBackendList"}, FieldType: "array_string"}, }, "udp": { // UDP 和 TCP 结构类似 "Comment": {Paths: []string{"comment", "data.comment", "desc"}}, "Port": {Paths: []string{"port", "data.port"}}, "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, "AddrBackendList": {Paths: []string{"addrBackendList", "data.addrBackendList"}, FieldType: "array_string"}, }, "globalLimit": { "Comment": {Paths: []string{"comment", "data.comment", "desc"}}, "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, }, "allowAndDeny": { "AllowAndDenyIps": {Paths: []string{"ip", "ips"}, FieldType: "array_string"}, "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, }, "ccIpList": { "AllowAndDenyIps": {Paths: []string{"ip", "ips","newIp"}}, // 精确指定 ccIpList 只查找 "ip" "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"}, }, // "分配网关组" 的日志通常不包含用户层面的业务数据,所以这里不定义 } // --- 2. 清洗后的统一数据结构 --- type CleanedExtraData struct { Port string Domain string Comment string IsHttps int RuleID []int64 AddrBackendList []string CustomHost []string AllowAndDenyIps string } // --- 3. 服务实现 --- type WafLogDataCleanService interface { ParseWafLogExtraData(extraDataBytes json.RawMessage, apiName string) CleanedExtraData FormatBackendList(backendList interface{}) string } func NewWafLogDataCleanService( service *service.Service, ) WafLogDataCleanService { return &wafLogDataCleanService{ Service: service, } } type wafLogDataCleanService struct { *service.Service } // ParseWafLogExtraData 使用配置驱动的 gjson 解析,兼具灵活性和可维护性 func (s *wafLogDataCleanService) ParseWafLogExtraData(extraDataBytes json.RawMessage, apiName string) CleanedExtraData { var cleaned CleanedExtraData if len(extraDataBytes) == 0 || !gjson.Valid(string(extraDataBytes)) { if len(extraDataBytes) > 0 { s.Logger.Warn("ExtraData 不是有效的JSON", zap.String("raw_data", string(extraDataBytes))) } return cleaned } jsonStr := string(extraDataBytes) // 根据 apiName 找到对应的字段映射配置 var fieldConfig map[string]FieldPathConfig for keyword, config := range apiFieldMappings { if strings.Contains(strings.ToLower(apiName), keyword) { fieldConfig = config break } } // 如果没有找到配置,直接返回空结构 if fieldConfig == nil { return cleaned } // 通用、循环地提取字段 for fieldName, config := range fieldConfig { s.extractField(jsonStr, fieldName, config, &cleaned) } return cleaned } // extractField 是一个通用的字段提取辅助函数 func (s *wafLogDataCleanService) extractField(jsonStr, fieldName string, config FieldPathConfig, cleaned *CleanedExtraData) { // 找到第一个有效的路径和其结果 var validPathResult gjson.Result for _, path := range config.Paths { result := gjson.Get(jsonStr, path) if result.Exists() { validPathResult = result break } } if !validPathResult.Exists() { return // 如果所有路径都找不到,直接返回 } // 根据字段名称和类型将结果赋值给 CleanedExtraData switch fieldName { case "Comment": cleaned.Comment = validPathResult.String() case "Port": cleaned.Port = validPathResult.String() case "Domain": cleaned.Domain = validPathResult.String() case "IsHttps": cleaned.IsHttps = int(validPathResult.Int()) case "RuleID": if validPathResult.IsArray() { validPathResult.ForEach(func(_, value gjson.Result) bool { cleaned.RuleID = append(cleaned.RuleID, value.Int()) return true }) } else { cleaned.RuleID = append(cleaned.RuleID, validPathResult.Int()) } case "BackendList": // 特殊处理对象数组 if validPathResult.IsArray() { validPathResult.ForEach(func(_, value gjson.Result) bool { if value.IsObject() { addr := gjson.Get(value.Raw, "addr").String() customHost := gjson.Get(value.Raw, "customHost").String() isHttps := gjson.Get(value.Raw, "isHttps").Int() if isHttps == 1 { addr = "https://" + addr } else { addr = "http://" + addr } cleaned.AddrBackendList = append(cleaned.AddrBackendList, addr) cleaned.CustomHost = append(cleaned.CustomHost, customHost) } return true }) } case "AddrBackendList": // 处理字符串数组 if validPathResult.IsArray() { validPathResult.ForEach(func(_, value gjson.Result) bool { cleaned.AddrBackendList = append(cleaned.AddrBackendList, value.String()) return true }) } case "AllowAndDenyIps": if validPathResult.IsArray() { var ips []string validPathResult.ForEach(func(_, value gjson.Result) bool { ips = append(ips, value.String()) return true }) cleaned.AllowAndDenyIps = strings.Join(ips, ", ") } else { cleaned.AllowAndDenyIps = validPathResult.String() } } } // FormatBackendList 格式化后端地址列表(已简化) func (s *wafLogDataCleanService) FormatBackendList(backendList interface{}) string { if backendList == nil { return "" } switch v := backendList.(type) { case []string: return strings.Join(v, ", ") case []int64: if len(v) == 0 { return "" } var strList []string for _, id := range v { strList = append(strList, strconv.FormatInt(id, 10)) } return strings.Join(strList, ", ") case string: return v default: // 其他类型直接转为字符串,作为最后的兼容手段 return fmt.Sprintf("%v", v) } }