- 流式调用增加可重试错误码检测与指数退避重试 - 业务字段写入改为前置追加,不覆盖已有值 - 请求体按模板元数据递归合并,补充数组/对象字段处理 - 异步任务查询支持请求体映射与占位符替换 - 升级 common 与 gmq 依赖版本,移除本地 replace
629 lines
20 KiB
Go
629 lines
20 KiB
Go
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=#)
|
||
// - 多级选择器 → 递归:中间级选择器定位/新建容器元素并继续下钻,叶子选择器写值
|
||
// - 值本身是数组 → 叶子选择器逐值追加;普通数组段/点号路径前置追加(业务值在前,原值保留在后,不覆盖)
|
||
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] = prependToArray(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) {
|
||
existing, ok := parent[key]
|
||
if !ok {
|
||
parent[key] = value
|
||
return
|
||
}
|
||
parent[key] = prependValue(value, existing)
|
||
}
|
||
|
||
// prependValue 把 value 前置到 existing 前(业务值在前,原值保留在后,不覆盖):
|
||
// - existing 是数组 → value(或其元素)前插到数组头部
|
||
// - value 是数组(existing 为标量)→ value 各元素在前,existing 作为末位元素
|
||
// - 其余标量 → 字符串拼接,业务值在前
|
||
func prependValue(value, existing any) any {
|
||
if arr, isArr := existing.([]any); isArr {
|
||
return prependToArray(arr, value)
|
||
}
|
||
if vals, isArr := value.([]any); isArr {
|
||
out := make([]any, 0, len(vals)+1)
|
||
out = append(out, vals...)
|
||
out = append(out, existing)
|
||
return out
|
||
}
|
||
return gconv.String(value) + gconv.String(existing)
|
||
}
|
||
|
||
// prependToArray 把 value(或其元素)插到数组头部,原元素依次后移
|
||
func prependToArray(arr []any, value any) []any {
|
||
if vals, isArr := value.([]any); isArr {
|
||
out := make([]any, 0, len(vals)+len(arr))
|
||
out = append(out, vals...)
|
||
out = append(out, arr...)
|
||
return out
|
||
}
|
||
out := make([]any, 0, len(arr)+1)
|
||
out = append(out, value)
|
||
out = append(out, arr...)
|
||
return out
|
||
}
|
||
|
||
// 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
|
||
}
|