feat: 新增业务字段路径读写工具
新增 TakeBusinessFields、WriteBusinessFields、SetByPath 与 GetByPath 等工具,支持按映射路径写入请求体与解析响应,并更新相关依赖。
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user