- 流式调用增加可重试错误码检测与指数退避重试 - 业务字段写入改为前置追加,不覆盖已有值 - 请求体按模板元数据递归合并,补充数组/对象字段处理 - 异步任务查询支持请求体映射与占位符替换 - 升级 common 与 gmq 依赖版本,移除本地 replace
504 lines
15 KiB
Go
504 lines
15 KiB
Go
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
|
||
}
|