news 2026/9/24 3:55:44

AI陪伴机器人统一响应与全局异常-ApiResponse三段式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI陪伴机器人统一响应与全局异常-ApiResponse三段式

06-统一响应与全局异常-ApiResponse三段式

黒漂技术佬 · AI 伙伴(AI-Partner)「数据接口部署与二次开发」系列 06

上一篇的 19 个接口返回格式全都长一个样:{"code":0,"message":"success","data":...}。这套"三段式"是怎么实现的?报错时 HTTP 状态码和业务码怎么配合?为什么业务代码里几乎看不到 try-catch?这篇把 AI 伙伴的 common 包三个类拆干净——总共不到一百行代码,却是整个接口层体验的压舱石。

一、ApiResponse:三段式统一响应

// 项目源码(common/ApiResponse.java)@Data@NoArgsConstructor@AllArgsConstructorpublicclassApiResponse<T>{privateintcode;// 业务码:0 成功,非 0 失败privateStringmessage;// 提示信息privateTdata;// 业务数据publicstatic<T>ApiResponse<T>ok(Tdata){returnnewApiResponse<>(0,"success",data);}publicstatic<T>ApiResponse<T>ok(){returnnewApiResponse<>(0,"success",null);}publicstatic<T>ApiResponse<T>fail(intcode,Stringmessage){returnnewApiResponse<>(code,message,null);}publicstatic<T>ApiResponse<T>fail(Stringmessage){returnnewApiResponse<>(500,message,null);}}

三个字段各司其职:code给程序判断(0 成功、非 0 失败),message给人看(错误提示或固定 “success”),data装业务数据(成功时装实体,失败时为 null)。四个静态工厂覆盖了所有出口语义:

静态工厂codemessagedata使用场景
ok(data)0success业务数据查询/创建成功
ok()0successnull无返回值的操作
fail(code, msg)自定义错误信息null明确知道错误码的失败
fail(msg)500错误信息null不细分码的失败(单参默认 500)

二、code=0 vs HTTP 200:两派的取舍

业界对"成功怎么表达"有两派。一派信 HTTP 语义:200 就是成功,404/500 各表其义,无需业务码。另一派(国内后端主流,微信/支付宝开放平台都是)是本项目的做法:HTTP 状态码只表达传输层结果,业务结果放进 body 的 code 字段

本项目的组合更微妙:两层都在用。业务成功时 HTTP 200 + code=0;业务异常时 HTTP 状态码也会变(见下节,BusinessException 返回 HTTP 400)。也就是说它没有走"永远 200、错误全靠 code"的极端派,而是让 HTTP 状态承担粗分类(4xx 客户端的锅、5xx 服务器的锅),业务码承担细分类。

维度纯 HTTP 派纯业务码派(永远 200)本项目混合派
网关/监控识别错误天然支持(按状态码告警)失效,需解析 body部分支持
前端统一处理要枚举各种状态码只判 code状态码粗判 + code 细判
中间件友好度(重试/熔断)较好

取舍本身没有标准答案,重要的是全项目一致——这恰恰是三段式包装最大的价值:前端只需要写一次拦截器,判断 code 是否为 0,非 0 弹 message,齐活。

三、BusinessException:把业务错误变成数据

// 项目源码(common/BusinessException.java)@GetterpublicclassBusinessExceptionextendsRuntimeException{privatefinalintcode;publicBusinessException(Stringmessage){super(message);this.code=500;}publicBusinessException(intcode,Stringmessage){super(message);this.code=code;}}

注意它继承的是RuntimeException(非受检异常)——业务代码抛它不用层层声明 throws。两个构造函数语义分明:单参抛"我不关心错误码"的通用错误(code 默认 500),双参抛"这个错误值得一个专属码"的精确错误。项目里的实际用法,比如对话服务里:

// 项目源码(ChatService 内,节选)thrownewBusinessException(400,"大模型 API Key 未配置…");thrownewBusinessException("AI 服务暂时不可用,请稍后再试");

抛出之后业务代码就撒手不管了——接下来的活全是全局异常处理器的。

四、GlobalExceptionHandler:三类拦截,全项目兜底

// 项目源码(common/GlobalExceptionHandler.java)@Slf4j@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(BusinessException.class)publicResponseEntity<ApiResponse<Void>>handleBusiness(BusinessExceptione){returnResponseEntity.badRequest().body(ApiResponse.fail(e.getCode(),e.getMessage()));}@ExceptionHandler(MethodArgumentNotValidException.class)publicResponseEntity<ApiResponse<Void>>handleValidation(MethodArgumentNotValidExceptione){FieldErrorfieldError=e.getBindingResult().getFieldError();Stringmessage=fieldError==null?"参数校验失败":fieldError.getDefaultMessage();returnResponseEntity.badRequest().body(ApiResponse.fail(400,message));}@ExceptionHandler(Exception.class)publicResponseEntity<ApiResponse<Void>>handleOther(Exceptione){log.error("系统异常",e);returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.fail(500,"系统繁忙,请稍后再试"));}}

@RestControllerAdvice让它成为所有 Controller 的"常驻保安",三类拦截各有讲究:

**第一类:BusinessException → HTTP 400 + 业务码透传。**业务员主动抛的错(设备不存在、提醒创建失败),code 从异常里取出来原样进 body。注意 HTTP 状态固定给 400——这类错误的共同点是"请求本身有问题",客户端改改参数就能重试。

第二类:MethodArgumentNotValidException → HTTP 400 + code 400。DTO 上@NotNull/@NotBlank校验失败时由@Valid触发。消息取首个FieldError 的 defaultMessage(比如"userId 不能为空"),一个字段一个字段地修,体验友好;一个 FieldError 都没有就退回兜底文案"参数校验失败"。

**第三类:Exception 兆底 → HTTP 500 + 固定文案。**任何没被预料到的异常,统一返回"系统繁忙,请稍后再试",同时log.error("系统异常", e)把完整堆栈打进日志。这里有个教科书级的细节:对外文案模糊、对内日志详尽。错误堆栈里的类名、SQL 片段绝不能透给前端(信息泄露风险),但日志里必须留全,否则排查问题两眼一抹黑。

五、为什么参数校验错误要用 400

有同学会问:反正前端只看 code,HTTP 状态码随便给不行吗?行,但浪费了。HTTP 4xx 和 5xx 的分工是整个互联网基础设施的共识:

  • 4xx = 客户端的错:网关不会告警、重试也没用(参数不变结果不变)、前端该提示用户改输入;
  • 5xx = 服务器的错:监控系统该告警、运维该介入、客户端重试可能恢复。

参数校验失败 100% 是客户端请求的问题,标 400 后,APM 工具、Nginx 日志分析、前端 axios 拦截器都能自动把它归入"用户输入问题"处理,而不是误报"服务挂了"。一行状态码,省掉一整层沟通成本。

六、异常交给全局处理:业务代码零 try-catch

这套机制的最终红利是:Controller 和 Service 里几乎看不到 try-catch。对比一下两种写法:

// 示意:没有全局处理器的世界,每个接口都要这样包@PostMapping("/api/chat")publicApiResponse<ChatResult>chat(@Valid@RequestBodyChatRequestreq){try{returnApiResponse.ok(chatService.chat(...));}catch(BusinessExceptione){returnApiResponse.fail(e.getCode(),e.getMessage());}catch(Exceptione){log.error("chat error",e);returnApiResponse.fail(500,"系统繁忙,请稍后再试");}}

19 个接口 × 每个都写一遍 = 维护灾难。而 AI 伙伴的实际 Controller 长这样:

// 项目源码(ChatController,节选)@PostMappingpublicApiResponse<ChatService.ChatResult>chat(@Valid@RequestBodyChatRequestrequest){returnApiResponse.ok(chatService.chat(request.getUserId(),request.getMessage(),request.getSessionType(),Boolean.TRUE.equals(request.getNeedTts())));}

干净得像伪代码。校验失败、业务错误、意外异常各走各的拦截通道,横切关注点(异常处理)被彻底从业务代码里剥离——这就是 AOP 思想在异常处理上的落地。

七、当前缺少的异常类型与补齐建议

全局处理器三类拦截能兜住大局,但有两类异常目前会掉进"Exception 兜底",体验打折:

  1. IllegalArgumentException:Service 里throw new IllegalArgumentException("openId 不能为空")这类参数问题,现在会被 500 兜底返回"系统繁忙"——明明是客户端的错却报成服务器故障。补一个 Handler 返回 400 即可。
  2. 鉴权异常:项目无鉴权体系,未来引入 Spring Security 或登录拦截器后,AccessDeniedException/401 场景必须有专属处理,否则未登录用户会看到"系统繁忙"而不是"请先登录"。

补齐示例(示意):

// 示意:建议新增的两个 Handler@ExceptionHandler(IllegalArgumentException.class)publicResponseEntity<ApiResponse<Void>>handleIllegalArgument(IllegalArgumentExceptione){returnResponseEntity.badRequest().body(ApiResponse.fail(400,e.getMessage()));}

八、"异常 → HTTP 状态 → 业务码 → 前端处理"对照表

异常来源HTTP 状态业务码 codemessage前端建议处理
业务成功2000success渲染 data
BusinessException(双参)400自定义(如 400)具体业务提示toast 展示 message
BusinessException(单参)400500具体业务提示toast 展示 message
@Valid 校验失败400400首个字段的校验文案高亮对应表单项
未捕获异常500500“系统繁忙,请稍后再试”通用错误页,引导重试
(建议补)IllegalArgumentException400400参数问题提示按输入错误处理
(建议补)鉴权异常401/403401/403请登录/无权限跳登录页

九、合规提醒

异常处理是隐私泄露的常见暗门。二次开发时守住三条:兜底异常的对外文案保持模糊(堆栈、SQL、表结构一律不外泄);log.error的日志里如果含用户对话、健康数值等敏感数据,要按公司日志规范脱敏并限制留存期;健康类业务错误(如"心率数据格式错误")的 message 措辞避免下诊断结论——系统只报数据问题,医疗判断永远留给专业医生


小结ApiResponse三段式 +BusinessException+ 三类全局拦截,不到一百行代码撑起了 19 个接口的统一出口。HTTP 状态码管粗分类、业务码管细分类、业务代码零 try-catch——这就是"小项目也有工程尊严"的样子。至此,从表设计到接口出口的整条数据链路你都过了一遍,接下来无论是把系统部署上线还是动手二次开发,心里都有底了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 3:55:02

别再把 AI 当高级搜索引擎:用好 WorkBuddy 的十条心法

大多数人用 AI 的方式&#xff0c;是把一个本来可以做项目经理的助手&#xff0c;当成了一个会打字的实习生。一个让人不安的事实 我观察过很多人第一次用 AI 助手的场景。通常是这样的&#xff1a; 打开对话框&#xff0c;敲下一句帮我写一份季度运营报告&#xff0c;回车&…

作者头像 李华
网站建设 2026/9/24 3:34:21

CEF+WebRTC+NVENC:Web端云渲染低延迟高画质方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 3:28:17

Claude Code:住在终端里的AI智能体,从安装到实战全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 3:24:27

大一学计算机的第一天

我是重庆某二本的计算机学生 所有人都说计算机不好走 不过为了给小时候的自己圆梦我还是义无反顾的报了计算机。 学习计算机的顺序应该是什么呢&#xff1f;其实我不知道&#xff0c;跟着课上走一步看一步吧。现在在学c&#xff0c;下一步是python&#xff0c;希望大一上学期可…

作者头像 李华
网站建设 2026/9/24 3:22:23

Unity UGUI中的Canvas重建机制

在 Unity UGUI 中,Canvas 是整个 UI 系统的核心。很多 UI 性能问题,例如界面频繁卡顿、批次突然增加、Canvas.BuildBatch 占用 CPU、UI 动画导致大量 CPU 消耗,本质上都可能与 Canvas 的重建机制有关。 很多开发者知道修改 UI 属性会触发 Canvas 重建,但真正的问题是:什么…

作者头像 李华