feat: 新增业务字段路径读写工具

新增 TakeBusinessFields、WriteBusinessFields、SetByPath 与 GetByPath 等工具,支持按映射路径写入请求体与解析响应,并更新相关依赖。
This commit is contained in:
2026-08-18 10:11:09 +08:00
parent 55c797dd96
commit 76c55fbb73
41 changed files with 6011 additions and 56 deletions
+462
View File
@@ -0,0 +1,462 @@
package utils
import (
"fmt"
"model-gateway/model/dto"
"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 IsFlatMap(userParams) {
nested, err := 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 直接以自身为容器)
if attrs, ok := protoTmpl.Attrs.(map[string]interface{}); ok {
for i, elem := range userArr {
elemMap, isMap := elem.(map[string]interface{})
if !isMap {
continue
}
container, has := userObjectValue(elemMap, true)
if !has {
continue
}
for subKey, subTmpl := range attrs {
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
}
// 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
}