- parseModelError 按模型配置的 ErrorMessageMapping(schema 树)解析错误响应 - 成功判定: code 提取为空 / code 节点配置 defaultValue 且提取值等于它 → 成功 - 未配置 mapping 不识别错误(纯配置驱动), 删硬编码 ModelErrorResp/ModelError1Resp - DTO 加 errorMessageMapping 字段, update.sql 建 error_message_mapping JSONB 列 - 单测: parse_error_test.go 覆盖扁平/嵌套/数组/纯字符串路径/未配置/坏 JSON
195 lines
6.3 KiB
Go
195 lines
6.3 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"reflect"
|
||
|
||
modelUtils "model-gateway/service/utils"
|
||
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
)
|
||
|
||
// parseModelError 按模型配置的 ErrorMessageMapping 解析错误响应,返回错误码与错误消息(无错误均返回空串)。
|
||
// ErrorMessageMapping 为 schema 树(与请求模板同格式:type/value/label/isForm/required/fieldType/defaultValue/attrs),
|
||
// 解析时剔除包装字段取 code/message 的字段路径(复用 normalizeSchemaPath 归一 attrs/数组段),
|
||
// 经 GetByPath 从响应体提取。成功判定:
|
||
// - code 提取值为空(nil/空串/数字零)→ 成功
|
||
// - code 节点配置了 defaultValue 且提取值等于它 → 成功
|
||
// - 否则 → 错误(返回 code + message)
|
||
//
|
||
// 未配置 ErrorMessageMapping → 不识别错误,一律返回成功(纯配置驱动)。
|
||
// 解析失败返回 err,由调用方决定重试/终态。
|
||
func parseModelError(body []byte, mapping map[string]any) (code, msg string, err error) {
|
||
if len(mapping) == 0 {
|
||
return "", "", nil
|
||
}
|
||
var respObj map[string]any
|
||
if err = json.Unmarshal(body, &respObj); err != nil {
|
||
return "", "", err
|
||
}
|
||
codePath, msgPath, hasCodeDefault, codeDefault := collectErrorMapping(mapping)
|
||
codeVal := modelUtils.GetByPathValue(respObj, codePath)
|
||
msgVal := modelUtils.GetByPathValue(respObj, msgPath)
|
||
if codePath != "" {
|
||
if isEmptyErrorCode(codeVal) {
|
||
return "", "", nil
|
||
}
|
||
if hasCodeDefault && sameErrorValue(codeVal, codeDefault) {
|
||
return "", "", nil
|
||
}
|
||
return gconv.String(codeVal), gconv.String(msgVal), nil
|
||
}
|
||
// 未配置 code 路径:以 message 是否为空判定错误
|
||
if !isEmptyErrorCode(msgVal) {
|
||
return "", gconv.String(msgVal), nil
|
||
}
|
||
return "", "", nil
|
||
}
|
||
|
||
// collectErrorMapping 遍历 ErrorMessageMapping schema 树,提取 code/message 的字段路径与 code 节点的 defaultValue。
|
||
// 支持 schema 节点(含 type/attrs 等包装)与纯字符串路径两种形态;数组段经 normalizeSchemaPath 归一到 [*]。
|
||
// 返回路径为已归一字段路径;未配置返回空串。
|
||
func collectErrorMapping(mapping map[string]any) (codePath, msgPath string, hasCodeDefault bool, codeDefault any) {
|
||
var walk func(node map[string]any, prefix string)
|
||
walk = func(node map[string]any, prefix string) {
|
||
for key, val := range node {
|
||
if isErrorMetaKey(key) {
|
||
continue
|
||
}
|
||
m, ok := val.(map[string]any)
|
||
if !ok {
|
||
// 纯值形态:字段值直接是字段路径字符串
|
||
if s, ok := val.(string); ok {
|
||
path := normalizeSchemaPath(joinErrorFieldPath(prefix, s))
|
||
if key == "code" && codePath == "" {
|
||
codePath = path
|
||
} else if key == "message" && msgPath == "" {
|
||
msgPath = path
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
nodeType, _ := m["type"].(string)
|
||
hasDef := false
|
||
var def any
|
||
if d, has := m["defaultValue"]; has && d != nil {
|
||
hasDef, def = true, d
|
||
}
|
||
switch {
|
||
case nodeType == "object":
|
||
if attrs, ok := m["attrs"].(map[string]any); ok {
|
||
walk(attrs, joinErrorFieldPath(prefix, key))
|
||
} else if attrs, ok := m["attrs"].([]any); ok && len(attrs) > 0 {
|
||
if elm, ok := attrs[0].(map[string]any); ok {
|
||
walkErrorArrayElement(elm, joinErrorArrayPath(prefix, key), walk)
|
||
}
|
||
} else {
|
||
walk(m, joinErrorFieldPath(prefix, key))
|
||
}
|
||
case nodeType == "array":
|
||
walkErrorArrayContainer(m, joinErrorArrayPath(prefix, key), walk)
|
||
case nodeType == "":
|
||
// 无 type 键:纯容器(字段直接作为键),递归下钻
|
||
walk(m, joinErrorFieldPath(prefix, key))
|
||
default:
|
||
// 标量叶子字段
|
||
path := normalizeSchemaPath(joinErrorFieldPath(prefix, key))
|
||
if key == "code" && codePath == "" {
|
||
codePath, hasCodeDefault, codeDefault = path, hasDef, def
|
||
} else if key == "message" && msgPath == "" {
|
||
msgPath = path
|
||
}
|
||
}
|
||
}
|
||
}
|
||
walk(mapping, "")
|
||
return
|
||
}
|
||
|
||
// walkErrorArrayContainer 数组节点:子字段容器依次尝试 attrs([]any)/enumValues/value([]any)
|
||
func walkErrorArrayContainer(node map[string]any, prefix string, walk func(map[string]any, string)) {
|
||
for _, container := range []string{"attrs", "enumValues", "value"} {
|
||
items, ok := node[container].([]any)
|
||
if !ok || len(items) == 0 {
|
||
continue
|
||
}
|
||
if elm, ok := items[0].(map[string]any); ok {
|
||
walkErrorArrayElement(elm, prefix, walk)
|
||
}
|
||
return
|
||
}
|
||
}
|
||
|
||
// walkErrorArrayElement 数组元素:字段在其 attrs 下(对象元素)或直接作为键(纯元素)
|
||
func walkErrorArrayElement(elm map[string]any, prefix string, walk func(map[string]any, string)) {
|
||
if attrs, ok := elm["attrs"].(map[string]any); ok {
|
||
walk(attrs, prefix)
|
||
return
|
||
}
|
||
walk(elm, prefix)
|
||
}
|
||
|
||
// isErrorMetaKey 判断是否为 schema 节点元数据字段(非业务字段,剔除)
|
||
func isErrorMetaKey(key string) bool {
|
||
switch key {
|
||
case "type", "value", "label", "isForm", "required", "fieldType", "defaultValue", "description":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// joinErrorFieldPath 拼接字段路径(数组段 [0] 由 normalizeSchemaPath 归一为 [*])
|
||
func joinErrorFieldPath(prefix, key string) string {
|
||
if prefix == "" {
|
||
return key
|
||
}
|
||
return prefix + "." + key
|
||
}
|
||
|
||
// joinErrorArrayPath 数组字段路径:元素下标 [0] 由 normalizeSchemaPath 归一为 [*]
|
||
func joinErrorArrayPath(prefix, key string) string {
|
||
if prefix == "" {
|
||
return key + "[0]"
|
||
}
|
||
return prefix + "." + key + "[0]"
|
||
}
|
||
|
||
// isEmptyErrorCode 判断错误码是否为空(空=无错误):nil / 空串 / 布尔 false / 数字零值
|
||
func isEmptyErrorCode(v any) bool {
|
||
if v == nil {
|
||
return true
|
||
}
|
||
switch t := v.(type) {
|
||
case string:
|
||
return t == ""
|
||
case bool:
|
||
return !t
|
||
}
|
||
if isNumericType(v) {
|
||
return gconv.Float64(v) == 0
|
||
}
|
||
return false
|
||
}
|
||
|
||
// sameErrorValue 判断提取值是否等于配置的 defaultValue(数值/字符串跨类型兼容)
|
||
func sameErrorValue(a, b any) bool {
|
||
if isNumericType(a) && isNumericType(b) {
|
||
return gconv.Float64(a) == gconv.Float64(b)
|
||
}
|
||
return gconv.String(a) == gconv.String(b)
|
||
}
|
||
|
||
// isNumericType 判断是否为数值类型
|
||
func isNumericType(v any) bool {
|
||
if v == nil {
|
||
return false
|
||
}
|
||
switch reflect.TypeOf(v).Kind() {
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||
reflect.Float32, reflect.Float64:
|
||
return true
|
||
}
|
||
return false
|
||
}
|