Skip to content

统一的业务错误码设计 #33

Description

@mengnankkkk

总体设计
系统里拆成两层错误响应:
业务接口响应 Business API Result
给前端 / 管理后台 / 普通 HTTP API 使用

AI 工具响应 Tool Result
给 Agent / LLM / MCP Tool / 内部工具调用使用

共同依赖:
ErrorCode
BusinessException
全局异常处理
也就是:

flowchart TD
    A["Service / Domain Logic"] --> B["throw BusinessException(ErrorCode)"]
    B --> C["ErrorCode enum"]

    C --> D["Business API Result"]
    C --> E["AI Tool Result"]

    D --> F["前端根据 code 做 i18n / UI 展示"]
    E --> G["AI 根据 code / action 判断下一步"]
Loading
  1. 公共错误码层
    错误码必须是全系统唯一、稳定、可被前端和 AI 共同理解的东西。
    ErrorCode
    @Getter
    @requiredargsconstructor
    public enum ErrorCode {

    SUCCESS(200, "success"),

    PARAM_INVALID(100001, "请求参数不合法"),
    PARAM_REQUIRED(100002, "必填参数不能为空"),
    PAGE_INVALID(100003, "分页参数不合法"),
    SORT_ORDER_INVALID(100004, "排序方向不合法"),

    DATASOURCE_NOT_FOUND(200001, "数据源不存在"),

    TABLE_SEMANTIC_NOT_FOUND(310001, "表语义元数据不存在"),
    TABLE_HIDDEN(310002, "表已隐藏"),
    TABLE_PHYSICAL_MISSING(310003, "物理表不存在或已下线"),

    COLUMN_SEMANTIC_NOT_FOUND(320001, "列语义元数据不存在"),
    COLUMN_PHYSICAL_MISSING(320002, "物理列不存在或已下线"),

    RELATION_NOT_FOUND(330001, "逻辑关系不存在"),
    RELATION_TYPE_INVALID(330002, "关系类型不合法"),

    SCHEMA_READ_FAILED(510001, "读取数据库结构失败"),
    SQL_ONLY_SELECT_ALLOWED(500002, "只允许执行 SELECT 查询"),

    SYSTEM_ERROR(900001, "系统异常");

    private final int code;
    private final String defaultMessage;
    }
    BusinessException
    @Getter
    public class BusinessException extends RuntimeException {

    private final ErrorCode errorCode;
    private final Object detail;

    public BusinessException(ErrorCode errorCode) {
    this(errorCode, errorCode.getDefaultMessage(), null);
    }

    public BusinessException(ErrorCode errorCode, String message) {
    this(errorCode, message, null);
    }

    public BusinessException(ErrorCode errorCode, String message, Object detail) {
    super(message);
    this.errorCode = errorCode;
    this.detail = detail;
    }

    public int getCode() {
    return errorCode.getCode();
    }
    }
    这里 detail 很关键。业务接口可以不返回它,AI 工具层可以返回它。

  2. 业务接口响应设计
    业务 API 继续用现在的 Result,保持前端兼容。
    返回格式
    {
    "code": 310003,
    "message": "物理表不存在或已下线,请重新同步表结构后再试。",
    "data": null
    }
    Java 结构
    @DaTa
    public class Result implements Serializable {

    private Integer code;
    private String message;
    private T data;

    public static Result success(T data) {
    return new Result<>(200, "success", data);
    }

    public static Result error(ErrorCode errorCode) {
    return new Result<>(errorCode.getCode(), errorCode.getDefaultMessage());
    }

    public static Result error(ErrorCode errorCode, String message) {
    return new Result<>(errorCode.getCode(), message);
    }
    }
    业务接口层关注的是:
    code:前端判断错误类型
    message:默认展示文案
    data:业务数据
    业务 API 不需要暴露太多 AI 行为信息,比如 suggestAction,否则前端模型会被污染。

  3. AI 工具响应设计
    AI 工具层用另一个模型,不直接复用 Result。
    返回格式
    {
    "success": false,
    "code": 310003,
    "message": "物理表不存在或已下线,请重新同步表结构后再试。",
    "action": "SYNC_TABLE_SCHEMA",
    "data": {
    "datasourceId": 1,
    "tableName": "orders"
    }
    }
    Java 结构
    @builder
    public record ToolResult(
    boolean success,
    Integer code,
    String message,
    String action,
    T data) {

    public static ToolResult success(T data) {
    return ToolResult.builder()
    .success(true)
    .code(ErrorCode.SUCCESS.getCode())
    .message(ErrorCode.SUCCESS.getDefaultMessage())
    .data(data)
    .build();
    }

    public static ToolResult failed(
    ErrorCode errorCode,
    String message,
    String action,
    T data) {
    return ToolResult.builder()
    .success(false)
    .code(errorCode.getCode())
    .message(message)
    .action(action)
    .data(data)
    .build();
    }
    }
    AI 工具层比业务接口多两个字段:
    success 让模型一眼知道工具是否成功
    action 告诉模型建议下一步做什么
    比如:
    SYNC_TABLE_SCHEMA
    CHECK_DATASOURCE
    ASK_USER_CONFIRMATION
    RETRY_LATER
    CONTACT_ADMIN

  4. Tool Action 设计
    建议加一个枚举,避免字符串乱飞。
    @Getter
    @requiredargsconstructor
    public enum ToolAction {

    NONE("NONE"),
    SYNC_TABLE_SCHEMA("SYNC_TABLE_SCHEMA"),
    CHECK_DATASOURCE("CHECK_DATASOURCE"),
    CHECK_TABLE_NAME("CHECK_TABLE_NAME"),
    ASK_USER_CONFIRMATION("ASK_USER_CONFIRMATION"),
    RETRY_LATER("RETRY_LATER"),
    CONTACT_ADMIN("CONTACT_ADMIN");

    private final String code;
    }
    AI 看到 action=SYNC_TABLE_SCHEMA,就知道应该说:
    这个表的物理结构已经不存在或下线了,需要先同步表结构后再查询。

而不是胡乱猜。
5. 业务异常到 AI 工具异常的转换
核心思路:
Service 抛 BusinessException

普通 Controller:GlobalExceptionHandler 转成 Result

AI Tool:ToolExceptionMapper 转成 ToolResult
ToolExceptionMapper
@component
public class ToolExceptionMapper {

public ToolResult<Object> toToolResult(Exception exception) {
    if (exception instanceof BusinessException businessException) {
        ErrorCode errorCode = businessException.getErrorCode();
        return ToolResult.failed(
                errorCode,
                businessException.getMessage(),
                resolveAction(errorCode),
                businessException.getDetail());
    }

    return ToolResult.failed(
            ErrorCode.SYSTEM_ERROR,
            ErrorCode.SYSTEM_ERROR.getDefaultMessage(),
            ToolAction.CONTACT_ADMIN.getCode(),
            null);
}

private String resolveAction(ErrorCode errorCode) {
    return switch (errorCode) {
        case TABLE_PHYSICAL_MISSING -> ToolAction.SYNC_TABLE_SCHEMA.getCode();
        case DATASOURCE_NOT_FOUND -> ToolAction.CHECK_DATASOURCE.getCode();
        case TABLE_SEMANTIC_NOT_FOUND -> ToolAction.SYNC_TABLE_SCHEMA.getCode();
        case COLUMN_SEMANTIC_NOT_FOUND -> ToolAction.SYNC_TABLE_SCHEMA.getCode();
        case SQL_ONLY_SELECT_ALLOWED -> ToolAction.CHECK_TABLE_NAME.getCode();
        default -> ToolAction.NONE.getCode();
    };
}

}
6. getTableSchema 示例
业务层不要关心自己是被前端调,还是被 AI 调。它只抛统一异常。
if (Boolean.FALSE.equals(tableInfo.getPhysicalStatus())) {
throw new BusinessException(
ErrorCode.TABLE_PHYSICAL_MISSING,
"物理表不存在或已下线,请重新同步表结构后再试。",
Map.of(
"datasourceId", datasourceId,
"tableName", tableName
));
}
前端接口看到
{
"code": 310003,
"message": "物理表不存在或已下线,请重新同步表结构后再试。",
"data": null
}
AI 工具看到
{
"success": false,
"code": 310003,
"message": "物理表不存在或已下线,请重新同步表结构后再试。",
"action": "SYNC_TABLE_SCHEMA",
"data": {
"datasourceId": 1,
"tableName": "orders"
}
}
同一个错误,不同出口。漂亮,稳。
7. 推荐包结构
io.github.malonetalk.common
Result.java

io.github.malonetalk.exception
ErrorCode.java
BusinessException.java
GlobalExceptionHandler.java

io.github.malonetalk.agent.tool
ToolResult.java
ToolAction.java
ToolExceptionMapper.java
如果想更清楚,也可以放:
io.github.malonetalk.agent.tools.result
8. 落地顺序
我建议按这个顺序改:
新增 ErrorCode
新增 BusinessException
改造 Result 支持 ErrorCode
改造 GlobalExceptionHandler
新增 ToolResult
新增 ToolAction
新增 ToolExceptionMapper
先改 getTableSchema 这条链路
再逐步替换其他 IllegalArgumentException

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions