Files
model-gateway/service/parse_error.go
T
19904408334 923644db2b refactor: 模型错误解析改为 ErrorMessageMapping 配置驱动
- parseModelError 按模型配置的 ErrorMessageMapping(schema 树)解析错误响应
- 成功判定: code 提取为空 / code 节点配置 defaultValue 且提取值等于它 → 成功
- 未配置 mapping 不识别错误(纯配置驱动), 删硬编码 ModelErrorResp/ModelError1Resp
- DTO 加 errorMessageMapping 字段, update.sql 建 error_message_mapping JSONB 列
- 单测: parse_error_test.go 覆盖扁平/嵌套/数组/纯字符串路径/未配置/坏 JSON
2026-08-29 17:52:23 +08:00

195 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}