package utils import ( "fmt" "model-gateway/model/dto" "gitea.redpowerfuture.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gutil" ) // 数据类型常量 const ( TypeString = "string" TypeBool = "boolean" TypeNumber = "number" TypeNumberInt = "integer" TypeNumberFloat = "float" TypeNull = "null" TypeObject = "object" TypeArray = "array" ) // CheckParams 校验用户入参并回填默认值: // 用户只传 key/value,约束参数(type/required/constraint)全部取模板定义。 // 模板定义必填的字段,用户未传或传空值都报错;用户值为空时用模板 defaultValue 回填。 // 严格模式:未知字段报错。 func CheckParams(userParams map[string]interface{}, templateParams map[string]interface{}) error { return checkParams(userParams, templateParams, true, true) } // CheckBody 校验构建完成的请求体(ParseConfigTemplate + WriteBusinessFields 之后): // 业务字段按映射写入的路径可能超出模板声明,未知字段不报错;默认值已在构建期处理,不做回填。 // 仍按模板约束校验必填/长度/范围。 func CheckBody(body map[string]interface{}, templateParams map[string]interface{}) error { return checkParams(body, templateParams, false, true) } // checkParams 按模板校验请求结构。strictUnknown:未知字段是否报错;backfill:空值是否回填 defaultValue。 func checkParams(userParams map[string]interface{}, templateParams map[string]interface{}, strictUnknown, backfill bool) error { // 兼容扁平路径入参:还原为嵌套结构 orig := userParams if utils.IsFlatMap(userParams) { nested, err := utils.UnFlatBySjson(userParams) if err != nil { return fmt.Errorf("无法解析用户参数: %w", err) } orig = nested } // 顶层未知字段检查 if strictUnknown { for key := range orig { if _, has := templateParams[key]; !has { return fmt.Errorf("非法字段: %s 模板中不存在该字段", key) } } } for key, tmplNode := range templateParams { if err := validateNode(orig, key, tmplNode, key, strictUnknown, backfill); err != nil { return err } } return nil } // validateNode 按模板节点校验用户值,空值回填 defaultValue。 // parent 为用户原始结构(模板格式 {type,value/attrs} 或纯值),key 为字段名;回填写回 parent[key]。 func validateNode(parent map[string]interface{}, key string, tmplNode interface{}, path string, strictUnknown, backfill bool) error { raw, hasRaw := parent[key] tmplMap, ok := tmplNode.(map[string]interface{}) if !ok { return nil // 模板节点不是对象,无约束可校验 } var tmpl dto.Template if err := gconv.Struct(tmplMap, &tmpl); err != nil { return fmt.Errorf("字段 [%s] 模板解析错误: %w", path, err) } label := tmpl.Label if label == "" { label = path } switch tmpl.Type { case TypeObject: userMap, hasUser := userObjectValue(raw, hasRaw) if !hasUser { if tmpl.Required { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", label) } return nil // 未传对象且非必填:跳过 } attrs, ok := tmpl.Attrs.(map[string]interface{}) if !ok { return nil } // 未知子字段检查 if strictUnknown { for k := range userMap { if _, has := attrs[k]; !has { return fmt.Errorf("非法字段: %s 模板中不存在该字段", path+"."+k) } } } // 递归子字段(即使对象未传,子字段必填校验仍生效) for subKey, subTmpl := range attrs { if err := validateNode(userMap, subKey, subTmpl, path+"."+subKey, strictUnknown, backfill); err != nil { return err } } return nil case TypeArray: // 枚举项:逐项校验请求 enumValue.attrs 子字段并回填默认值 if err := validateEnumValues(raw, hasRaw, path, strictUnknown, backfill); err != nil { return err } userArr, hasUser := userArrayValue(raw, hasRaw) if !hasUser || len(userArr) == 0 { // 先回填 defaultValue(必填字段也可由默认值兜底),回填后重新判空 if backfill { backfillDefault(parent, key, raw, hasRaw, &tmpl) } if tmpl.Required && isValueEmptyByType(&tmpl) { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", label) } return nil } // 数组数量约束:上限取 Constraint.UploadTotalMaxCount 或各 UploadRule.MaxCount 之和(schema 构建时配置) if limit := maxArrayCount(&tmpl); limit > 0 && len(userArr) > limit { return fmt.Errorf("字段 [%s] 数量 %d 超过限制 %d", label, len(userArr), limit) } proto := arrayElementPrototype(&tmpl) if proto == nil { return nil } var protoTmpl dto.Template if err := gconv.Struct(proto, &protoTmpl); err != nil { return nil } switch protoTmpl.Type { case TypeObject: // 对象元素:以元素 attrs 为容器递归校验子字段(模板对象节点 {type:object,attrs:{...}} 的 // 子字段藏在 attrs 下;纯对象 map 直接以自身为容器) for i, elem := range userArr { elemMap, isMap := elem.(map[string]interface{}) if !isMap { continue } container, has := userObjectValue(elemMap, true) if !has { continue } // 元素带 type 键(schema 包裹):已由 validateEnumValues 按其自身 attrs 校验,跳过, // 避免用枚举首原型(可能是必填字段模板)误报其他槽位元素缺失 if _, isWrapped := elemMap["type"]; isWrapped { continue } // 纯对象元素(解析后数组下标可能塌缩):按 attrs 键集结构匹配槽位原型,避免恒用首原型误报必填 subAttrs := matchArraySlotProto(&tmpl, container) if subAttrs == nil { continue } for subKey, subTmpl := range subAttrs { subPath := fmt.Sprintf("%s[%d].%s", path, i, subKey) if err := validateNode(container, subKey, subTmpl, subPath, strictUnknown, backfill); err != nil { return err } } } default: // 标量元素:逐元素校验(数组内元素不参与整体必填) for i, elem := range userArr { pt := protoTmpl pt.Value = elem pt.Required = false if err := checkScalar(&pt, fmt.Sprintf("%s[%d]", path, i)); err != nil { return err } } } return nil default: // 标量类型:先回填 defaultValue(必填字段也可由默认值兜底),回填后重新判空,再校验必填/约束 tmpl.Value = userScalarValue(raw, hasRaw) if isValueEmptyByType(&tmpl) { if backfill { backfillDefault(parent, key, raw, hasRaw, &tmpl) tmpl.Value = tmpl.DefaultValue } if isValueEmptyByType(&tmpl) { if tmpl.Required { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", label) } return nil } } return checkScalar(&tmpl, label) } } // checkScalar 校验标量值:必填 + 约束 func checkScalar(tmpl *dto.Template, label string) error { if isValueEmptyByType(tmpl) { if tmpl.Required { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", label) } return nil } switch tmpl.Type { case TypeString: return checkStringTmpl(tmpl) case TypeNumber: return checkNumberTmpl(tmpl) case TypeBool: return checkBoolTmpl(tmpl) case TypeNull: return nil default: return fmt.Errorf("字段 [%s] 不支持的模板类型: %s", label, tmpl.Type) } } // userObjectValue 从原始请求节点提取对象值(模板格式取 attrs/value,纯值直接返回 map) func userObjectValue(raw interface{}, hasRaw bool) (map[string]interface{}, bool) { if !hasRaw || raw == nil { return nil, false } if m, ok := raw.(map[string]interface{}); ok { if _, isTpl := m["type"]; isTpl { if v, has := m["attrs"]; has { if sub, ok := v.(map[string]interface{}); ok { return sub, true } } if v, has := m["value"]; has { if sub, ok := v.(map[string]interface{}); ok { return sub, true } } return nil, false } return m, true } return nil, false } // userArrayValue 从原始请求节点提取数组值 func userArrayValue(raw interface{}, hasRaw bool) ([]interface{}, bool) { if !hasRaw || raw == nil { return nil, false } if arr, ok := raw.([]interface{}); ok { return arr, true } if m, ok := raw.(map[string]interface{}); ok { if _, isTpl := m["type"]; isTpl { if v, has := m["value"]; has { if sub, ok := v.([]interface{}); ok { return sub, true } } v1, has1 := m["attrs"] v2, has2 := m["enumValues"] if has1 || has2 { sub1, ok1 := v1.([]interface{}) sub2, ok2 := v2.([]interface{}) if ok1 { if ok2 { return sub2, true } return sub1, true } if ok2 { return sub2, true } } } } return nil, false } // userScalarValue 从原始请求节点提取标量值 func userScalarValue(raw interface{}, hasRaw bool) interface{} { if !hasRaw { return nil } if m, ok := raw.(map[string]interface{}); ok { if _, isTpl := m["type"]; isTpl { return m["value"] } } return raw } // backfillDefault 空值回填 defaultValue: // 模板格式节点写 value 键;纯值直接覆盖;字段缺失则补一个模板格式节点供下游产出默认值。 func backfillDefault(parent map[string]interface{}, key string, raw interface{}, hasRaw bool, tmpl *dto.Template) { if tmpl.DefaultValue == nil { return } if m, ok := raw.(map[string]interface{}); ok { if _, isTpl := m["type"]; isTpl { m["value"] = tmpl.DefaultValue return } } if hasRaw { parent[key] = tmpl.DefaultValue return } parent[key] = map[string]interface{}{ "type": tmpl.Type, "value": tmpl.DefaultValue, } } // validateEnumValues 校验数组枚举项:逐项取请求 enumValue.attrs 作为字段容器, // 递归校验每个子字段(必填/约束)并回填空值的 defaultValue。与旧 checkArrayTmpl 行为对齐。 func validateEnumValues(raw interface{}, hasRaw bool, path string, strictUnknown, backfill bool) error { if !hasRaw { return nil } rawMap, ok := raw.(map[string]interface{}) if !ok { return nil } evs, ok := rawMap["enumValues"].([]interface{}) if !ok { return nil } for i, ev := range evs { evMap, ok := ev.(map[string]interface{}) if !ok { continue } attrs, ok := evMap["attrs"].(map[string]interface{}) if !ok { continue } for subKey, subTmpl := range attrs { subPath := fmt.Sprintf("%s.enumValues[%d].%s", path, i, subKey) if err := validateNode(attrs, subKey, subTmpl, subPath, strictUnknown, backfill); err != nil { return err } } } return nil } // maxArrayCount 取数组字段的数量上限:UploadTotalMaxCount 优先,其次各 UploadRule.MaxCount 之和;未配置返回 0 func maxArrayCount(tmpl *dto.Template) int { if tmpl.Constraint.UploadTotalMaxCount > 0 { return tmpl.Constraint.UploadTotalMaxCount } total := 0 for _, rule := range tmpl.Constraint.UploadRules { total += rule.MaxCount } return total } // arrayElementPrototype 取数组元素模板原型(attrs 优先,其次 enumValues) func arrayElementPrototype(tmpl *dto.Template) map[string]interface{} { if attrs, ok := tmpl.Attrs.([]interface{}); ok && len(attrs) > 0 { if m, ok := attrs[0].(map[string]interface{}); ok { return m } } if len(tmpl.EnumValues) > 0 { if m, ok := tmpl.EnumValues[0].(map[string]interface{}); ok { return m } } return nil } // matchArraySlotProto 按元素 attrs 键集与各槽位原型 attrs 键集的重合度匹配最合适的槽位原型。 // 解析后数组下标可能塌缩(resolveArray 丢弃空元素),不能按 index 对齐,故用结构匹配。 // 键集完全无重合时返回 nil(跳过该校验,避免用错误原型误报必填)。 func matchArraySlotProto(tmpl *dto.Template, container map[string]interface{}) map[string]interface{} { var best map[string]interface{} bestCount := -1 for _, ev := range tmpl.EnumValues { evMap, ok := ev.(map[string]interface{}) if !ok { continue } attrs, ok := evMap["attrs"].(map[string]interface{}) if !ok { continue } count := 0 for k := range container { if _, has := attrs[k]; has { count++ } } if count > bestCount { bestCount = count best = attrs } } if bestCount <= 0 { return nil } return best } // isValueEmptyByType 按 tmpl.Type 判断是否为"业务空值" func isValueEmptyByType(tmpl *dto.Template) bool { switch tmpl.Type { case TypeString: return g.IsEmpty(gconv.String(tmpl.Value)) case TypeNumber: return g.IsEmpty(gconv.Float64(tmpl.Value)) case TypeBool: return tmpl.Value == nil case TypeObject: return g.IsEmpty(gconv.Map(tmpl.Value)) case TypeArray: return g.IsEmpty(gconv.SliceAny(tmpl.Value)) case TypeNull: return true default: return true } } // checkStringTmpl 字符串类型校验 func checkStringTmpl(tmpl *dto.Template) error { val := gconv.String(tmpl.Value) if tmpl.Required && gutil.IsEmpty(val) { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", tmpl.Label) } ct := tmpl.Constraint if gutil.IsEmpty(ct) { return nil } if tmpl.FieldType == "string" || tmpl.FieldType == "textarea" { if ct.MinLength > 0 && len(val) < ct.MinLength { return fmt.Errorf("字段 [%s] 长度应大于等于 %d,当前长度 %d", tmpl.Label, ct.MinLength, len(val)) } if ct.MaxLength > 0 && len(val) > ct.MaxLength { return fmt.Errorf("字段 [%s] 长度应小于等于 %d,当前长度 %d", tmpl.Label, ct.MaxLength, len(val)) } } return nil } // checkNumberTmpl 数字类型校验 func checkNumberTmpl(tmpl *dto.Template) error { ct := tmpl.Constraint if gutil.IsEmpty(ct) { return nil } switch ct.NumberType { case TypeNumberInt: val := gconv.Int(tmpl.Value) if tmpl.Required && gutil.IsEmpty(val) { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", tmpl.Label) } minVal := gconv.Int(ct.Min) maxVal := gconv.Int(ct.Max) if !g.IsEmpty(minVal) && val < minVal { return fmt.Errorf("字段 [%s] 值 %d 不应小于 最小值 %d", tmpl.Label, val, minVal) } if !g.IsEmpty(maxVal) && val > maxVal { return fmt.Errorf("字段 [%s] 值 %d 不应大于 最大值 %d", tmpl.Label, val, maxVal) } case TypeNumberFloat: val := gconv.Float64(tmpl.Value) if tmpl.Required && gutil.IsEmpty(val) { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", tmpl.Label) } minVal := gconv.Float64(ct.Min) maxVal := gconv.Float64(ct.Max) if !g.IsEmpty(minVal) && val < minVal { return fmt.Errorf("字段 [%s] 值 %.2f 不应小于 最小值 %.2f", tmpl.Label, val, minVal) } if !g.IsEmpty(maxVal) && val > maxVal { return fmt.Errorf("字段 [%s] 值 %.2f 不应大于 最大值 %.2f", tmpl.Label, val, maxVal) } default: return fmt.Errorf("字段 [%s] 数字类型 [%s] 错误,仅支持 int/float", tmpl.Label, ct.NumberType) } return nil } // checkBoolTmpl 布尔类型校验 func checkBoolTmpl(tmpl *dto.Template) error { val := gconv.Bool(tmpl.Value) if tmpl.Required && gutil.IsEmpty(val) { return fmt.Errorf("字段 [%s] 为必填项,但未提供有效值", tmpl.Label) } return nil }