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
+605
View File
@@ -0,0 +1,605 @@
package utils
import (
"fmt"
"sort"
"strings"
"github.com/gogf/gf/v2/util/gconv"
)
// ============================================================
// 业务字段读写:TakeBusinessFields 把 businessParams 按映射解析为写入路径,
// WriteBusinessFields 按路径写入最终请求体(路径语法见 SetByPath)。
// ============================================================
// TakeBusinessFields 把业务参数(businessParams)按映射解析为写入路径:
// - 调用方按业务字段名(RequestBusinessFieldMapping 的 key)传值,这里是独立的 businessParams map
// 不再与模板字段混在 requestParams 中
// - 业务字段名未配置映射 → 返回错误(不静默忽略)
// - 解包 {type,value} 包裹格式为原始值
// - 跳过空值(空串/空数组),避免写入请求体污染
//
// 返回 map[映射路径]原始值,构建完成后由 WriteBusinessFields 按路径写入请求体。
func TakeBusinessFields(businessParams map[string]any, bizMapping map[string]string) (map[string]any, error) {
if len(businessParams) == 0 {
return nil, nil
}
keyToPath := make(map[string]string, len(bizMapping))
for key, path := range bizMapping {
if key != "" && path != "" {
keyToPath[key] = path
}
}
bizValues := make(map[string]any)
for key, raw := range businessParams {
path, isBiz := keyToPath[key]
if !isBiz {
return nil, fmt.Errorf("业务字段 [%s] 未配置映射(RequestBusinessFieldMapping 中不存在该业务字段名)", key)
}
v := unwrapBizValue(raw)
if isEmptyBizValue(v) {
continue
}
bizValues[path] = v
}
return bizValues, nil
}
// unwrapBizValue 解包模板包裹格式 {type, value},返回原始值;非包裹格式原样返回
func unwrapBizValue(v any) any {
if m, ok := v.(map[string]any); ok {
if _, hasType := m["type"]; hasType {
if val, hasVal := m["value"]; hasVal {
return val
}
}
}
return v
}
// isEmptyBizValue 判断业务字段值是否为空(空值不写入请求体)
func isEmptyBizValue(v any) bool {
switch t := v.(type) {
case nil:
return true
case string:
return t == ""
case []any:
return len(t) == 0
case map[string]any:
return len(t) == 0
}
return false
}
// ============================================================
// 统一字段路径语法(读/写共用,见 NormalizeFieldPath):
//
// a.b.c 普通点号路径
// a[*].b [*] 表示数组段
// a[*].b[*]?k=v&t=# 单层选择器:在数组中按 k==v 匹配元素,值/读取目标为 t
// a[*]?k=v&b[*]?k2=v2&t=# 多级选择器:选择器体内可再嵌 [*]?选择器,级数不限。
// 每级 k=v 既是匹配条件(命中已存在元素时),
// 也是新建元素时写入该元素的字段(如 role=user 直接落为 role 字段);
// 只有带 t=# 的那级是叶子目标(写值/读值的位置)。
//
// SetByPath(写,构建请求体)与 GetByPath(读,解析响应)共用 parsePath;
// 读方向语义:数组段非末段取第 0 个元素继续下钻,[*] 为末段返回整个数组,选择器定位匹配元素;
// 写方向语义:数组段非末段作用于最后一个元素,末段追加,选择器 upsert(命中更新/未命中新建),
// 多值([]any)仅在叶子选择器展开为多个独立元素(多个参考图/视频等)。
// ============================================================
// SetByPath 按业务字段映射路径把值写入请求结构(请求侧构建)。
// 路径语法与 BuildSchemaMapping 输出一致(干净形态,无需 attrs 剔除)。
//
// 写入语义:
// - 目标字段已存在且是数组 → 追加
// - 目标字段已存在且非数组 → 覆盖(普通叶子路径)
// - 目标字段不存在 → 新建
// - 数组段/选择器段目标字段不是数组(如字符串 content)→ 返回错误,不覆盖已有值
// - 中间路径遇到非对象字段 → 返回错误
// - 数组段无选择器且非末尾 → 作用于最后一个元素(追加语义),数组为空则补一个空元素
// - 数组段带选择器 → 命中则更新目标字段,未命中则按选择器字段构造新元素追加;
// 选择器段即使未标 [*] 也按数组处理(如 input.media?type=first_frame&url=#
// - 多级选择器 → 递归:中间级选择器定位/新建容器元素并继续下钻,叶子选择器写值
// - 值本身是数组 → 叶子选择器逐值追加;普通数组段/点号路径按 appendValues 追加
func SetByPath(root map[string]any, path string, value any) error {
steps := parsePath(NormalizeFieldPath(path))
if len(steps) == 0 {
return nil
}
return setBySteps(root, steps, value)
}
// setBySteps 按步骤序列写入;选择器步骤(可能带嵌套)递归处理,非选择器步骤逐层下钻
func setBySteps(cur map[string]any, steps []step, value any) error {
first := steps[0]
last := len(steps) == 1
if first.sel != nil {
// 选择器段:目标字段按数组处理(upsert),路径段未标 [*] 也按数组匹配
arr, err := existingArray(cur, first.key)
if err != nil {
return err
}
newArr, err := upsertStep(arr, first, value)
if err != nil {
return err
}
cur[first.key] = newArr
return nil
}
if !first.isArray {
if last {
setLeaf(cur, first.key, value)
return nil
}
next, err := ensureMap(cur, first.key)
if err != nil {
return err
}
return setBySteps(next, steps[1:], value)
}
// 数组段(无选择器)
arr, err := existingArray(cur, first.key)
if err != nil {
return err
}
if last {
cur[first.key] = appendValues(arr, value)
return nil
}
// 无选择器数组段:作用于最后一个元素(追加语义)
if len(arr) == 0 {
arr = append(arr, map[string]any{})
cur[first.key] = arr
}
lastElem, ok := arr[len(arr)-1].(map[string]any)
if !ok {
return fmt.Errorf("字段 [%s] 数组元素不是对象,无法继续下钻(当前类型 %T)", first.key, arr[len(arr)-1])
}
return setBySteps(lastElem, steps[1:], value)
}
// WriteBusinessFields 把业务字段值写入最终请求体。
// bizValues 的键为映射路径(如 input.media?type=reference_video&url=#),值由调用方按路径传入。
// 按字典序升序写入:父路径是子路径的前缀(短者靠前),保证容器先写、子路径再 upsert,
// 避免子路径先建出的结构被父路径整体覆盖(如 messages 容器与 messages[*].content[*] 内嵌目标并存)。
// 任一路径写入失败(如数组段目标不是数组)→ 返回错误,由调用方拒绝本次请求。
func WriteBusinessFields(out map[string]any, bizValues map[string]any) error {
paths := make([]string, 0, len(bizValues))
for path := range bizValues {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
if err := SetByPath(out, path, bizValues[path]); err != nil {
return fmt.Errorf("业务字段写入失败 [%s]: %w", path, err)
}
}
return nil
}
// GetByPath 按字段路径读取响应值(与 SetByPath 同一套路径语法,读方向语义):
// - 普通段:逐层进入对象取字段
// - 数组段 [*]:非末段取数组第 0 个元素继续下钻;[*] 为末段返回整个数组
// - 选择器段 ?k=v&t=#:定位 k==v 的元素,返回该元素 t 字段的值;多级选择器递归下钻
//
// 未命中(路径缺失 / 中间类型不符)返回 (nil, nil),不视为错误;语法错误返回 error。
func GetByPath(root map[string]any, path string) (any, error) {
steps := parsePath(NormalizeFieldPath(path))
if len(steps) == 0 {
return nil, nil
}
return getBySteps(root, steps)
}
// getBySteps 按步骤序列读取;选择器步骤(可能带嵌套)递归处理
func getBySteps(cur any, steps []step) (any, error) {
if len(steps) == 0 {
return nil, nil
}
first := steps[0]
rest := steps[1:]
m, ok := cur.(map[string]any)
if !ok {
return nil, nil
}
// 选择器段:定位匹配元素,返回叶子目标或递归嵌套下钻
if first.sel != nil {
arr, ok := m[first.key].([]any)
if !ok {
return nil, nil
}
for _, e := range arr {
em, ok := e.(map[string]any)
if !ok {
continue
}
if matchFilters(em, first.sel) {
return getSelValue(em, first.sel, rest)
}
}
return nil, nil
}
v, ok := m[first.key]
if !ok {
return nil, nil
}
if first.isArray {
arr, ok := v.([]any)
if !ok {
return nil, nil
}
if len(rest) == 0 {
return arr, nil
}
if len(arr) == 0 {
return nil, nil
}
return getBySteps(arr[0], rest)
}
if len(rest) == 0 {
return v, nil
}
return getBySteps(v, rest)
}
// getSelValue 选择器命中元素后取值:有嵌套路径则递归下钻,否则取叶子目标字段
// target 可为点号路径,如 image_url.url=#,按 parseSteps 拆级下钻)
func getSelValue(em map[string]any, sel *selNode, rest []step) (any, error) {
if len(sel.nested) > 0 {
return getBySteps(em, append(sel.nested, rest...))
}
if sel.target != "" {
return getBySteps(em, append(parseSteps(sel.target), rest...))
}
return getBySteps(em, rest)
}
// GetByPathValue 读取路径值,未命中或出错返回 nil(免去调用方处理双返回值)
func GetByPathValue(root map[string]any, path string) any {
v, err := GetByPath(root, path)
if err != nil {
return nil
}
return v
}
// GetByPathAll 按字段路径读取响应值(与 GetByPath 同一套语法),返回路径下**所有**命中值。
// 与 GetByPath 的区别:GetByPath 命中即返回第一个匹配;GetByPathAll 遍历数组段/选择器段的全部
// 元素并展开收集。适用于通配路径(messages[*]...[*]...)取全部匹配值(如收集所有图片 url)。
// 无命中返回 nil。
func GetByPathAll(root map[string]any, path string) []any {
steps := parsePath(NormalizeFieldPath(path))
if len(steps) == 0 {
return nil
}
return getAllBySteps(root, steps)
}
// getAllBySteps 按步骤序列收集全部匹配值;数组段/选择器段遍历所有元素展开,普通段单值包裹返回
func getAllBySteps(cur any, steps []step) []any {
if len(steps) == 0 {
return nil
}
first := steps[0]
rest := steps[1:]
m, ok := cur.(map[string]any)
if !ok {
return nil
}
// 选择器段:遍历命中元素收集
if first.sel != nil {
arr, ok := m[first.key].([]any)
if !ok {
return nil
}
var out []any
for _, e := range arr {
em, ok := e.(map[string]any)
if !ok {
continue
}
if matchFilters(em, first.sel) {
out = append(out, getSelValueAll(em, first.sel, rest)...)
}
}
return out
}
v, ok := m[first.key]
if !ok {
return nil
}
if first.isArray {
arr, ok := v.([]any)
if !ok {
return nil
}
if len(rest) == 0 {
return arr
}
var out []any
for _, e := range arr {
out = append(out, getAllBySteps(e, rest)...)
}
return out
}
if len(rest) == 0 {
return []any{v}
}
return getAllBySteps(v, rest)
}
// getSelValueAll 选择器命中元素后收集:有嵌套路径递归下钻,否则取叶子目标字段(全部)
func getSelValueAll(em map[string]any, sel *selNode, rest []step) []any {
if len(sel.nested) > 0 {
return getAllBySteps(em, append(sel.nested, rest...))
}
if sel.target != "" {
return getAllBySteps(em, append(parseSteps(sel.target), rest...))
}
return getAllBySteps(em, rest)
}
// step 路径段;sel 非空表示该段带选择器(按数组处理)
type step struct {
key string
isArray bool
sel *selNode
}
// selNode 选择器:
// - filters:k=v 匹配条件,新建元素时也作为字段写入
// - target:叶子目标字段(k=#),值/读取目标;target 为空且 nested 非空时为中间级选择器
// - nested:下钻子路径(多级嵌套选择器,级数不限)
type selNode struct {
filters [][2]string
target string
nested []step
}
// parsePath 解析路径为步骤序列。选择器体挂到最后一个步骤上;选择器体中的嵌套 [*]?选择器
// 递归解析为 nested(级数不限)。
func parsePath(p string) []step {
base, suffix := p, ""
if i := strings.Index(p, "?"); i >= 0 {
base, suffix = p[:i], p[i+1:]
}
steps := parseSteps(base)
if suffix != "" {
parseSelector(suffix, &steps)
}
return steps
}
// parseSteps 解析点号分隔的普通步骤(含 [*] 数组段)
func parseSteps(s string) []step {
var steps []step
for _, raw := range strings.Split(s, ".") {
raw = strings.TrimSpace(raw)
if raw == "" {
continue
}
st := step{}
if strings.HasSuffix(raw, "[*]") {
st.key = strings.TrimSuffix(raw, "[*]")
st.isArray = true
} else {
st.key = raw
}
steps = append(steps, st)
}
return steps
}
// parseSelector 解析选择器体(? 之后的内容)并挂到最后一个步骤上。
// 元素用顶层 & 分隔(? 之后的 & 属于嵌套选择器);k=v 为过滤/写入对,k=# 为叶子目标,
// 含 [*] 或路径的块为嵌套下钻子路径(递归 parsePath)。
func parseSelector(selStr string, steps *[]step) {
if len(*steps) == 0 {
return
}
sel := &selNode{}
var nested []step
for _, el := range splitTopLevel(selStr) {
if isPair(el) {
k, v, _ := strings.Cut(el, "=")
if v == "#" {
sel.target = k
} else {
sel.filters = append(sel.filters, [2]string{k, v})
}
continue
}
// 嵌套路径(含自己的选择器):级数不限,递归解析
nested = append(nested, parsePath(el)...)
}
if len(sel.filters) == 0 && sel.target == "" && len(nested) == 0 {
return
}
last := &(*steps)[len(*steps)-1]
if last.sel == nil {
last.sel = sel
}
if len(nested) > 0 {
last.sel.nested = nested
}
}
// splitTopLevel 按顶层 & 拆分选择器体;? 之后的 & 属于嵌套选择器,不在此层拆分
func splitTopLevel(s string) []string {
var elems []string
var cur strings.Builder
inNested := false
for _, ch := range s {
if ch == '?' {
inNested = true
}
if ch == '&' && !inNested {
elems = append(elems, cur.String())
cur.Reset()
continue
}
cur.WriteRune(ch)
}
if cur.Len() > 0 {
elems = append(elems, cur.String())
}
return elems
}
// isPair 判断元素是否为 k=v 对:= 出现在任何 [ ? 之前则是 pair,否则为嵌套路径。
// 目标字段 k 本身可以是点号路径(image_url.url=#),故 . 不参与判别。
func isPair(el string) bool {
for i := 0; i < len(el); i++ {
switch el[i] {
case '=':
return true
case '[', '?':
return false
}
}
return false
}
// existingArray 返回数组字段的当前数组:
// - 字段不存在 → 空数组(允许按追加语义新建)
// - 字段是数组 → 原样
// - 字段是其他类型(如字符串 content)→ 返回错误,调用方拒绝写入,不覆盖已有值
func existingArray(cur map[string]any, key string) ([]any, error) {
v, ok := cur[key]
if !ok {
return []any{}, nil
}
if arr, ok := v.([]any); ok {
return arr, nil
}
return nil, fmt.Errorf("字段 [%s] 不是数组,无法按数组路径写入(当前类型 %T)", key, v)
}
// upsertStep 选择器 upsert,返回追加后的数组:
// - 叶子选择器(有 target)且值为数组 → 每个值追加一个独立元素
// - 命中(所有 filters 匹配)→ 把值写入现有元素(叶子写 target,中间级递归 nested
// - 未命中 → 按选择器字段构造新元素并追加
//
// 返回新切片(append 可能重新分配底层数组),调用方需用返回值覆盖写回。
func upsertStep(arr []any, st step, value any) ([]any, error) {
sel := st.sel
// 叶子选择器:多值逐个展开为独立元素(多个参考图/视频等)
if sel.target != "" {
if vals, ok := value.([]any); ok && len(vals) > 0 {
for _, v := range vals {
elem, err := buildStepElement(sel, v)
if err != nil {
return nil, err
}
arr = append(arr, elem)
}
return arr, nil
}
}
for _, e := range arr {
m, ok := e.(map[string]any)
if !ok {
continue
}
if matchFilters(m, sel) {
if err := writeStepValue(m, sel, value); err != nil {
return nil, err
}
return arr, nil
}
}
elem, err := buildStepElement(sel, value)
if err != nil {
return nil, err
}
return append(arr, elem), nil
}
// matchFilters 判断元素是否匹配选择器全部过滤条件;无过滤条件时命中第一个元素
func matchFilters(m map[string]any, sel *selNode) bool {
if len(sel.filters) == 0 {
return true
}
for _, f := range sel.filters {
if gconv.String(m[f[0]]) != f[1] {
return false
}
}
return true
}
// writeStepValue 把值写入已定位元素:中间级递归 nested 下钻,叶子写 target 字段
func writeStepValue(m map[string]any, sel *selNode, value any) error {
if len(sel.nested) > 0 {
return setBySteps(m, sel.nested, value)
}
if sel.target != "" {
return setLeafPath(m, sel.target, value)
}
return nil
}
// buildStepElement 按选择器构造新元素:{filterKey: filterVal, ...} + 叶子写 target / 中间级递归 nested
func buildStepElement(sel *selNode, value any) (map[string]any, error) {
elem := make(map[string]any, len(sel.filters)+1)
for _, f := range sel.filters {
elem[f[0]] = f[1]
}
if err := writeStepValue(elem, sel, value); err != nil {
return nil, err
}
return elem, nil
}
// setLeafPath 在对象内按点号路径写入值(叶子用 setLeaf 语义)
func setLeafPath(m map[string]any, path string, value any) error {
cur := m
segs := strings.Split(path, ".")
for i, k := range segs {
if i == len(segs)-1 {
setLeaf(cur, k, value)
return nil
}
next, err := ensureMap(cur, k)
if err != nil {
return err
}
cur = next
}
return nil
}
// setLeaf 叶子写入:目标已是数组 → 追加;非数组/不存在 → 覆盖/新建
func setLeaf(parent map[string]any, key string, value any) {
if existing, ok := parent[key]; ok {
if arr, isArr := existing.([]any); isArr {
parent[key] = appendValues(arr, value)
return
}
}
parent[key] = value
}
// appendValues 追加值到数组;value 为数组时逐个追加
func appendValues(arr []any, value any) []any {
if vals, ok := value.([]any); ok {
return append(arr, vals...)
}
return append(arr, value)
}
// ensureMap 确保键对应 map,不存在则新建;已存在但非对象 → 返回错误
func ensureMap(parent map[string]any, key string) (map[string]any, error) {
if v, ok := parent[key]; ok {
if m, isMap := v.(map[string]any); isMap {
return m, nil
}
return nil, fmt.Errorf("字段 [%s] 不是对象,无法按路径写入(当前类型 %T)", key, v)
}
m := map[string]any{}
parent[key] = m
return m, nil
}
+43
View File
@@ -0,0 +1,43 @@
package utils
import (
"encoding/json"
"fmt"
"github.com/tidwall/sjson"
)
// IsFlatMap 递归判断 map 是否扁平化
func IsFlatMap(m map[string]interface{}) bool {
for _, v := range m {
switch val := v.(type) {
case map[string]interface{}:
return false
case []interface{}:
for _, item := range val {
if _, ok := item.(map[string]interface{}); ok {
return false
}
}
}
}
return true
}
// UnFlatBySjson 将扁平路径映射还原为嵌套 JSON
func UnFlatBySjson(flatMap map[string]interface{}) (map[string]interface{}, error) {
raw := "{}"
for path, val := range flatMap {
var err error
raw, err = sjson.Set(raw, path, val)
if err != nil {
return nil, fmt.Errorf("sjson set path %s failed: %w", path, err)
}
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(raw), &result); err != nil {
return nil, fmt.Errorf("parse final json failed: %w", err)
}
return result, nil
}
+58
View File
@@ -0,0 +1,58 @@
package utils
import (
"context"
"sync"
"sync/atomic"
"github.com/gogf/gf/v2/os/grpool"
)
var (
defaultPool atomic.Pointer[grpool.Pool]
once sync.Once
wg sync.WaitGroup
)
const DefaultWorkerNum = 100
// Init 初始化全局协程池,首次调用生效,后续调用忽略。
func Init(workerNum int) {
once.Do(func() {
if workerNum <= 0 {
workerNum = DefaultWorkerNum
}
defaultPool.Store(grpool.New(workerNum))
})
}
// Submit 提交异步任务,上下文透传至 grpool。
// Submit 也可在 Init 前调用(自动 Init),但 Shutdown 后返回 ErrPoolClosed。
func Submit(ctx context.Context, task func(ctx context.Context)) error {
p := defaultPool.Load()
if p == nil {
Init(DefaultWorkerNum)
p = defaultPool.Load()
}
wg.Add(1)
err := p.AddWithRecover(ctx, func(ctx context.Context) {
defer wg.Done()
task(ctx)
}, nil)
if err != nil {
wg.Done()
return err
}
return nil
}
// Shutdown 优雅关闭:停止新任务,等待全部已完成/排队任务完成。
func Shutdown() {
p := defaultPool.Swap(nil)
if p == nil {
return
}
wg.Wait()
p.Close()
}
+573
View File
@@ -0,0 +1,573 @@
package utils
import (
"regexp"
"strings"
"github.com/gogf/gf/v2/util/gconv"
)
var (
// 匹配 [数字]
regNumIndex = regexp.MustCompile(`\[\d+\]`)
// 匹配 .attrs
regAttrs = regexp.MustCompile(`\.attrs`)
)
// NormalizeFieldPath 归一化字段路径到统一语法([*] 数组段):
// - 移除模板残留 .attrs
// - [数字] 下标 → [*]choices[0] → choices[*]
// - 兼容 gjson 风格 .# / .数字 下标 → [*]choices.#、choices.0 → choices[*]
//
// 统一语法见 business_fields.go 的 SetByPath / GetByPath
//
// a.b.c 普通点号路径
// a[*].b [*] 表示数组段
// a[*].b[*]?k=v&t=# 选择器:数组元素按 k==v 定位,值/读取目标为 t
// a[*]?k=v&b[*]?k2=v2&t=# 多级选择器:级数不限,中间级定位容器元素,叶子写值
//
// 正则归一(.attrs / [数字] / .#)作用于整个路径(含多级选择器中的数组段);
// 纯数字段(gjson 下标)归一只作用于首个 ? 之前的 base 路径。
//
// 示例:
//
// usage.attrs.total_tokens → usage.total_tokens
// choices.attrs[0].attrs.message.attrs.content → choices[*].message.content
// choices.#.message.content → choices[*].message.content
// choices.0.message.content → choices[*].message.content
func NormalizeFieldPath(path string) string {
s := regAttrs.ReplaceAllString(path, "")
s = regNumIndex.ReplaceAllString(s, "[*]")
s = strings.ReplaceAll(s, ".#", "[*]")
base, suffix := s, ""
if i := strings.Index(s, "?"); i >= 0 {
base, suffix = s[:i], s[i:]
}
// 逐段把纯数字段(gjson 下标)归一为 [*]:附着到前一段字段(choices.0 → choices[*]),
// 避免误伤数字开头的字段名;选择器体用 # 作目标、不用数字段下标,故只归一 base
segs := strings.Split(base, ".")
var out []string
for _, seg := range segs {
if seg == "" {
continue
}
if isAllDigits(seg) {
if len(out) > 0 {
out[len(out)-1] += "[*]"
} else {
out = append(out, "[*]")
}
continue
}
out = append(out, seg)
}
return strings.Join(out, ".") + suffix
}
// isAllDigits 判断字符串是否全部为数字字符
func isAllDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// CleanFieldPath 清理字段路径(等价于 NormalizeFieldPath,保留旧名兼容)
func CleanFieldPath(path string) string {
return NormalizeFieldPath(path)
}
// CleanMapFieldPath 清理字段路径(Map
func CleanMapFieldPath(m map[string]string) map[string]string {
if m == nil {
return nil
}
newMap := make(map[string]string, len(m))
for k, _ := range m {
newMap[k] = CleanFieldPath(k)
}
return newMap
}
// ParseConfigTemplate 解析配置模板生成简化请求结构
//
// 输入: config 模板(含 type/value/defaultValue/attrs/enumValues 等元数据字段)
// 输出: 简化后的请求结构体
//
// 规则:
// - 标量字段(string/number/boolean: value 非零则用 value,为空则跳过(不再取 defaultValue
// - 对象字段(object: 递归处理 attrs
// - 数组字段(array: 遍历 enumValues,每个 enumValue 独立判断是否产出元素
// - 数组展开: enumValue 内某叶子字段 value 为数组时,按数组元素展开为多个项
func ParseConfigTemplate(cfg map[string]interface{}) map[string]interface{} {
var flattenJSON map[string]interface{}
flatMap := IsFlatMap(cfg)
if flatMap {
var err error
flattenJSON, err = UnFlatBySjson(cfg)
if err != nil {
return nil
}
} else {
flattenJSON = cfg
}
result := make(map[string]interface{})
for key, val := range flattenJSON {
field, ok := val.(map[string]interface{})
if !ok {
continue
}
if v := resolveField(field); v != nil {
result[key] = v
}
}
return result
}
// resolveField 按 type 分发解析
func resolveField(field map[string]interface{}) interface{} {
fieldType, _ := field["type"].(string)
switch fieldType {
case TypeString, TypeBool, TypeNumber:
return resolveScalar(field)
case TypeObject:
return resolveObject(field)
case TypeArray:
return resolveArray(field)
}
return nil
}
// resolveScalar 解析标量字段: value 非空则用 value,否则回落 defaultValue
// (模板只声明结构、值由业务字段给出时,defaultValue 生效)
func resolveScalar(field map[string]interface{}) interface{} {
if v, has := field["value"]; has && v != nil {
switch vv := v.(type) {
case string:
if vv != "" {
return vv
}
case float64:
if vv != 0 {
return vv
}
case bool:
return vv
default:
return vv
}
}
if d, has := field["defaultValue"]; has && d != nil {
switch dv := d.(type) {
case string:
if dv != "" {
return dv
}
case float64:
if dv != 0 {
return dv
}
case bool:
return dv
default:
return dv
}
}
return nil
}
// resolveObject 解析对象字段,递归处理 attrs
//
// 特殊处理「参数定义」结构:当 attrs 含 default 字段时,说明该对象是一个
// 参数定义(含 default/description/min/max/type/enum/required 等元数据),
// 此时只提取 default 的值作为该参数的值,其余元数据字段忽略。
func resolveObject(field map[string]interface{}) interface{} {
attrs, ok := field["attrs"].(map[string]interface{})
if !ok {
return nil
}
// 参数定义:只提取 default 值,跳过元数据
if defaultField, hasDefault := attrs["default"]; hasDefault {
if df, ok := defaultField.(map[string]interface{}); ok {
return extractRawValueKeepZero(df)
}
return nil
}
// 普通对象:递归处理所有 attrs
result := make(map[string]interface{})
for key, val := range attrs {
subField, ok := val.(map[string]interface{})
if !ok {
result[key] = val // 纯值字段原样保留
continue
}
if subType, _ := subField["type"].(string); subType == "" {
result[key] = val // 无 type 键的纯对象原样保留
continue
}
if v := resolveField(subField); v != nil {
result[key] = v
}
}
if len(result) == 0 {
return nil
}
return result
}
// resolveArray 解析数组字段,遍历 enumValues 或 attrs 生成元素列表
func resolveArray(field map[string]interface{}) []interface{} {
// 实际数据在 valueschema-editor 数据存放处),直接返回
if v, has := field["value"]; has {
if arr, ok := v.([]interface{}); ok && len(arr) > 0 {
return arr
}
}
enumValues, ok := field["enumValues"].([]interface{})
if ok {
var result []interface{}
for _, ev := range enumValues {
evMap, ok := ev.(map[string]interface{})
if !ok {
continue
}
items := resolveEnumObject(evMap)
result = append(result, items...)
}
if len(result) > 0 {
return result
}
}
// enumValues 取不到或为空时,尝试从 attrs(数组)中取
attrs, ok := field["attrs"].([]interface{})
if !ok {
return nil
}
var result []interface{}
for _, item := range attrs {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
if v := resolveField(itemMap); v != nil {
result = append(result, v)
}
}
return result
}
// resolveEnumObject 解析 enumValue 对象,支持数组展开
func resolveEnumObject(ev map[string]interface{}) []interface{} {
attrs, ok := ev["attrs"].(map[string]interface{})
if !ok {
return nil
}
// 将 enumValue 级别的 value 注入 attrs.type.value(如果 type.value 为空)
if evVal, has := ev["value"]; has && evVal != nil {
if s, ok := evVal.(string); ok && s != "" {
if typeField, has := attrs["type"]; has {
if typeMap, ok := typeField.(map[string]interface{}); ok {
if existing, has := typeMap["value"]; !has || existing == nil || existing == "" {
typeMap["value"] = s
}
}
}
}
}
return resolveAttrs(attrs)
}
// resolveAttrs 递归解析 attrs map,支持字段级数组展开
func resolveAttrs(attrs map[string]interface{}) []interface{} {
currentItems := []map[string]interface{}{{}}
hasValue := false
for key, val := range attrs {
subField, isMap := val.(map[string]interface{})
var subType string
if isMap {
subType, _ = subField["type"].(string)
}
var nextItems []map[string]interface{}
// 非包裹字段(纯值/纯对象,无 type 键):原样保留,数组值仍参与展开
if !isMap || subType == "" {
raw := val
if raw == nil {
nextItems = currentItems
currentItems = nextItems
continue
}
hasValue = true
if arr, ok := raw.([]interface{}); ok && len(arr) > 0 {
for _, item := range currentItems {
for _, elem := range arr {
cp := copyMap(item)
cp[key] = elem
nextItems = append(nextItems, cp)
}
}
} else {
for _, item := range currentItems {
cp := copyMap(item)
cp[key] = raw
nextItems = append(nextItems, cp)
}
}
currentItems = nextItems
continue
}
switch subType {
case TypeString, TypeBool, TypeNumber:
raw := extractRawValue(subField)
if raw == nil {
nextItems = currentItems
continue
}
hasValue = true
if arr, ok := raw.([]interface{}); ok && len(arr) > 0 {
for _, item := range currentItems {
for _, elem := range arr {
cp := copyMap(item)
cp[key] = elem
nextItems = append(nextItems, cp)
}
}
} else {
for _, item := range currentItems {
cp := copyMap(item)
cp[key] = raw
nextItems = append(nextItems, cp)
}
}
case TypeObject:
subAttrs, ok := subField["attrs"].(map[string]interface{})
if !ok {
nextItems = currentItems
continue
}
subItems := resolveAttrs(subAttrs)
if len(subItems) == 0 {
nextItems = currentItems
continue
}
hasValue = true
for _, item := range currentItems {
for _, subI := range subItems {
cp := copyMap(item)
cp[key] = subI
nextItems = append(nextItems, cp)
}
}
case TypeArray:
items := resolveArray(subField)
if len(items) == 0 {
nextItems = currentItems
continue
}
hasValue = true
for _, item := range currentItems {
cp := copyMap(item)
cp[key] = items
nextItems = append(nextItems, cp)
}
default:
nextItems = currentItems
}
currentItems = nextItems
}
if !hasValue {
return nil
}
result := make([]interface{}, len(currentItems))
for i, item := range currentItems {
result[i] = item
}
return result
}
// extractRawValue 提取原始值(保留数组值供上层展开)
func extractRawValue(field map[string]interface{}) interface{} {
if v, has := field["value"]; has && v != nil {
switch vv := v.(type) {
case string:
if vv != "" {
return vv
}
case float64:
if vv != 0 {
return vv
}
case bool:
return vv
case []interface{}:
if len(vv) > 0 {
return vv
}
default:
return vv
}
}
return nil
}
// extractRawValueKeepZero 同 extractRawValue,但不过滤零值
// 在参数定义场景下,default 可能是 false/0/"",需要保留
func extractRawValueKeepZero(field map[string]interface{}) interface{} {
if v, has := field["value"]; has && v != nil {
switch vv := v.(type) {
case string:
return vv
case float64:
return vv
case bool:
return vv
case []interface{}:
if len(vv) > 0 {
return vv
}
return vv
default:
return vv
}
}
if dv, has := field["defaultValue"]; has && dv != nil {
switch dvv := dv.(type) {
case string:
if field["type"] == TypeBool {
if dvv == "true" {
return true
}
if dvv == "false" {
return false
}
}
return dvv
case float64:
return dvv
case bool:
return dvv
case []interface{}:
if len(dvv) > 0 {
return dvv
}
return dvv
default:
return dvv
}
}
return nil
}
// copyMap 浅拷贝 map
func copyMap(src map[string]interface{}) map[string]interface{} {
dst := make(map[string]interface{}, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
// CoerceBodyTypes 按模板声明的 type 递归归一请求体字段值类型:
// - string → gconv.Stringnumber → gconv.Float64boolean → gconv.Bool
// - object → 按模板 attrs 递归子字段;array → 按元素模板逐个递归
// - 模板未声明的字段(业务字段写入且超出模板的部分)保持原样
//
// 用于构建请求体后统一修正:模板字段 value 与业务字段写入的值都可能携带与声明
// 类型不一致的 Go 类型(如 number 字段 value 为字符串 "0.7"),在此统一转成模型
// API 期望的 JSON 类型。仅做类型归一,不增删字段。
func CoerceBodyTypes(out map[string]interface{}, templateParams map[string]interface{}) map[string]interface{} {
if len(templateParams) == 0 {
return out
}
for key, raw := range out {
if tmplNode, has := templateParams[key]; has {
out[key] = coerceNode(raw, tmplNode)
}
}
return out
}
// coerceNode 按单个模板节点归一值类型
func coerceNode(value interface{}, tmplNode interface{}) interface{} {
tmplMap, ok := tmplNode.(map[string]interface{})
if !ok {
return value
}
fieldType, _ := tmplMap["type"].(string)
switch fieldType {
case TypeString:
return gconv.String(value)
case TypeNumber, TypeNumberInt, TypeNumberFloat:
return gconv.Float64(value)
case TypeBool:
return gconv.Bool(value)
case TypeObject:
sub, ok := value.(map[string]interface{})
if !ok {
return value
}
if attrs, ok := tmplMap["attrs"].(map[string]interface{}); ok {
return coerceObject(sub, attrs)
}
return value
case TypeArray:
arr, ok := value.([]interface{})
if !ok {
return value
}
proto := arrayElementTemplate(tmplMap)
if proto == nil {
return value
}
out := make([]interface{}, len(arr))
for i, elem := range arr {
out[i] = coerceNode(elem, proto)
}
return out
default:
return value
}
}
// coerceObject 按对象模板 attrs 归一对象子字段类型
func coerceObject(sub, attrs map[string]interface{}) map[string]interface{} {
for key, raw := range sub {
if tmplNode, has := attrs[key]; has {
sub[key] = coerceNode(raw, tmplNode)
}
}
return sub
}
// arrayElementTemplate 从数组模板节点提取元素模板(attrs 优先,其次 enumValues)。
// 与 arrayElementPrototype 语义一致,但直接工作在原始模板 map 上,供类型归一使用。
func arrayElementTemplate(field map[string]interface{}) map[string]interface{} {
if attrs, ok := field["attrs"].([]interface{}); ok && len(attrs) > 0 {
if m, ok := attrs[0].(map[string]interface{}); ok {
return m
}
}
if evs, ok := field["enumValues"].([]interface{}); ok && len(evs) > 0 {
if m, ok := evs[0].(map[string]interface{}); ok {
return m
}
}
return nil
}
+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
}