- 流式调用增加可重试错误码检测与指数退避重试 - 业务字段写入改为前置追加,不覆盖已有值 - 请求体按模板元数据递归合并,补充数组/对象字段处理 - 异步任务查询支持请求体映射与占位符替换 - 升级 common 与 gmq 依赖版本,移除本地 replace
261 lines
7.1 KiB
Go
261 lines
7.1 KiB
Go
package utils
|
||
|
||
import "github.com/gogf/gf/v2/util/gconv"
|
||
|
||
// MergeNode 按模板节点的元数据把用户数据节点合并为模板可解析的节点:
|
||
// - 用户节点无 type 键 → 套上模板类型包装(标量取用户值,空则回落模板 value/defaultValue)
|
||
// - 用户节点有 type 键 → 递归合并子节点(子节点缺 type 时同样补模板类型,避免模板包装泄漏)
|
||
// - 对象:模板 attrs 缺省字段补进用户容器(保留模板默认值)
|
||
// - 数组:用户 enumValues/attrs 元素逐个与模板元素原型合并(模板未提供的槽位克隆原型)
|
||
func MergeNode(user, tmpl any) any {
|
||
tmplMap, ok := tmpl.(map[string]any)
|
||
if !ok {
|
||
return user
|
||
}
|
||
fieldType, _ := tmplMap["type"].(string)
|
||
|
||
// 用户节点已带 type:按用户类型递归合并子节点;标量叶子已完备,直接返回
|
||
if userMap, ok := user.(map[string]any); ok {
|
||
if ut, isTpl := userMap["type"]; isTpl {
|
||
switch gconv.String(ut) {
|
||
case TypeObject:
|
||
return mergeObject(userMap, tmplMap)
|
||
case TypeArray:
|
||
return mergeArray(userMap, tmplMap)
|
||
default:
|
||
return userMap
|
||
}
|
||
}
|
||
}
|
||
|
||
switch fieldType {
|
||
case TypeString, TypeBool, TypeNumber, TypeNumberInt, TypeNumberFloat:
|
||
return wrapScalarNode(tmplMap, user)
|
||
case TypeObject:
|
||
return mergeObject(user, tmplMap)
|
||
case TypeArray:
|
||
return mergeArray(user, tmplMap)
|
||
default:
|
||
return user
|
||
}
|
||
}
|
||
|
||
// wrapScalarNode 把用户标量值包装为 {type, value} 节点;用户值为空时回落模板 value/defaultValue
|
||
func wrapScalarNode(tmplMap map[string]any, user any) map[string]any {
|
||
val := user
|
||
if m, ok := user.(map[string]any); ok {
|
||
if v, has := m["value"]; has {
|
||
val = v
|
||
} else {
|
||
val = nil
|
||
}
|
||
}
|
||
node := map[string]any{"type": normalizeScalarType(gconv.String(tmplMap["type"]))}
|
||
if hasUsableValue(val) {
|
||
node["value"] = val
|
||
} else if v, has := tmplMap["value"]; has {
|
||
node["value"] = v
|
||
} else if d, has := tmplMap["defaultValue"]; has {
|
||
node["value"] = d
|
||
}
|
||
return node
|
||
}
|
||
|
||
// normalizeScalarType integer/float 统一为 number(resolveField 只分发 string/boolean/number)
|
||
func normalizeScalarType(t string) string {
|
||
if t == TypeNumberInt || t == TypeNumberFloat {
|
||
return TypeNumber
|
||
}
|
||
return t
|
||
}
|
||
|
||
// mergeObject 合并对象节点:模板 attrs 缺省字段补进用户容器(模板默认值保留)
|
||
func mergeObject(user any, tmplMap map[string]any) map[string]any {
|
||
tmplAttrs, _ := tmplMap["attrs"].(map[string]any)
|
||
node := map[string]any{"type": TypeObject}
|
||
if tmplAttrs == nil {
|
||
node["attrs"] = user
|
||
return node
|
||
}
|
||
var container map[string]any
|
||
switch u := user.(type) {
|
||
case map[string]any:
|
||
if a, has := u["attrs"]; has {
|
||
if am, ok := a.(map[string]any); ok {
|
||
container = copyMap(am)
|
||
} else {
|
||
container = map[string]any{}
|
||
}
|
||
} else if v, has := u["value"]; has {
|
||
if vm, ok := v.(map[string]any); ok {
|
||
container = copyMap(vm)
|
||
} else {
|
||
container = map[string]any{}
|
||
}
|
||
} else {
|
||
container = copyMap(u)
|
||
}
|
||
default:
|
||
container = map[string]any{}
|
||
}
|
||
for k, subTmpl := range tmplAttrs {
|
||
if _, has := container[k]; !has {
|
||
container[k] = DeepCopyNode(subTmpl)
|
||
} else {
|
||
container[k] = MergeNode(container[k], subTmpl)
|
||
}
|
||
}
|
||
node["attrs"] = container
|
||
return node
|
||
}
|
||
|
||
// mergeArray 合并数组节点:用户 enumValues/attrs 元素逐个与模板元素原型合并。
|
||
// 用户未填的槽位(sjson null 填充)用模板对应槽位原型补位,保留下标不塌缩。
|
||
func mergeArray(user any, tmplMap map[string]any) map[string]any {
|
||
proto := arrayElementTemplate(tmplMap)
|
||
tmplSlots, _ := tmplMap["enumValues"].([]any) // 模板各槽位原型,按下标一一对应
|
||
node := map[string]any{"type": TypeArray}
|
||
slotProto := func(i int) any {
|
||
if i < len(tmplSlots) {
|
||
return tmplSlots[i]
|
||
}
|
||
return proto
|
||
}
|
||
switch u := user.(type) {
|
||
case map[string]any:
|
||
if evs, ok := u["enumValues"].([]any); ok {
|
||
out := make([]any, len(evs))
|
||
for i, ev := range evs {
|
||
if ev == nil {
|
||
// 保留 null 槽位以维持下标;解析阶段 resolveArray 会丢弃空元素,
|
||
// 避免用模板原型填充 null 而物化出"幽灵"元素(用户未填的数组槽位)
|
||
out[i] = nil
|
||
continue
|
||
}
|
||
out[i] = mergeArrayElement(ev, slotProto(i))
|
||
}
|
||
node["enumValues"] = out
|
||
} else if a, ok := u["attrs"].([]any); ok {
|
||
out := make([]any, len(a))
|
||
for i, item := range a {
|
||
out[i] = mergeArrayElement(item, proto)
|
||
}
|
||
node["attrs"] = out
|
||
} else if v, has := u["value"]; has {
|
||
node["value"] = v
|
||
} else if len(u) > 0 {
|
||
node["attrs"] = []any{mergeArrayElement(u, proto)}
|
||
}
|
||
case []any:
|
||
if isSchemaElementList(u) {
|
||
out := make([]any, len(u))
|
||
for i, item := range u {
|
||
out[i] = mergeArrayElement(item, proto)
|
||
}
|
||
node["attrs"] = out
|
||
} else {
|
||
node["value"] = u
|
||
}
|
||
default:
|
||
node["value"] = user
|
||
}
|
||
return node
|
||
}
|
||
|
||
// mergeArrayElement 合并单个数组元素:元素缺 type 时按槽位原型补默认字段。
|
||
// slotProto 可为 nil(模板未提供槽位原型时)→ 原样返回用户元素
|
||
func mergeArrayElement(ev any, slotProto any) any {
|
||
if ev == nil {
|
||
return ev
|
||
}
|
||
proto, _ := slotProto.(map[string]any)
|
||
if proto == nil {
|
||
return ev
|
||
}
|
||
evMap, ok := ev.(map[string]any)
|
||
if !ok {
|
||
return ev
|
||
}
|
||
if _, isTpl := evMap["type"]; isTpl {
|
||
return ev
|
||
}
|
||
// 元素原型自身是包装节点(嵌套对象/数组)→ 按模板类型合并
|
||
if t, has := proto["type"].(string); has && t != "" {
|
||
return MergeNode(ev, proto)
|
||
}
|
||
protoAttrs, _ := proto["attrs"].(map[string]any)
|
||
var container map[string]any
|
||
if a, has := evMap["attrs"]; has {
|
||
if am, ok := a.(map[string]any); ok {
|
||
container = copyMap(am)
|
||
} else {
|
||
container = map[string]any{}
|
||
}
|
||
} else {
|
||
container = copyMap(evMap)
|
||
}
|
||
for k, subTmpl := range protoAttrs {
|
||
if _, has := container[k]; !has {
|
||
container[k] = DeepCopyNode(subTmpl)
|
||
} else {
|
||
container[k] = MergeNode(container[k], subTmpl)
|
||
}
|
||
}
|
||
return map[string]any{"attrs": container}
|
||
}
|
||
|
||
// isSchemaElementList 判断 []any 是否为 schema 元素列表(元素均为 map,且带 type 或 attrs 包装)
|
||
func isSchemaElementList(list []any) bool {
|
||
if len(list) == 0 {
|
||
return false
|
||
}
|
||
for _, item := range list {
|
||
m, ok := item.(map[string]any)
|
||
if !ok {
|
||
return false
|
||
}
|
||
if _, hasType := m["type"]; !hasType {
|
||
if _, hasAttrs := m["attrs"]; !hasAttrs {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// DeepCopyNode 深拷贝任意嵌套节点(模板节点整体拷贝进 src 时使用,避免共享引用被后续回填修改)
|
||
func DeepCopyNode(v any) any {
|
||
switch val := v.(type) {
|
||
case map[string]any:
|
||
out := make(map[string]any, len(val))
|
||
for k, sub := range val {
|
||
out[k] = DeepCopyNode(sub)
|
||
}
|
||
return out
|
||
case []any:
|
||
out := make([]any, len(val))
|
||
for i, sub := range val {
|
||
out[i] = DeepCopyNode(sub)
|
||
}
|
||
return out
|
||
default:
|
||
return v
|
||
}
|
||
}
|
||
|
||
// hasUsableValue 值是否可写入节点 value;0/false 视为有效值
|
||
func hasUsableValue(v any) bool {
|
||
switch val := v.(type) {
|
||
case nil:
|
||
return false
|
||
case string:
|
||
return val != ""
|
||
case []any:
|
||
return len(val) > 0
|
||
case map[string]any:
|
||
return len(val) > 0
|
||
default:
|
||
return true
|
||
}
|
||
}
|