直接在控制器中手动返回数组结构会导致维护困难和前后端协议不一致,应通过自定义Response类统一响应格式,并结合异常处理器和中间件确保所有出口(正常返回、异常、验证失败等)均遵循同一JSON结构。

为什么直接在控制器里 return ['code'=>0,'data'=>[]] 不行
因为每个接口都手写数组结构,后期改字段名(比如 code 改成 status)要全局搜、全替换,漏一个就崩;更麻烦的是错误处理逻辑分散——throw new ValidateException() 和 throw new HttpException(404) 返回格式不一致,前端得写三套解析逻辑。
ThinkPHP 自带的 json() 方法只是包装了 json_encode(),不介入业务状态码和结构规范。真正要统一,得从响应出口拦截——也就是替换掉默认的 Response 构建方式。
- 别在每个
index()里手动return json(['code'=>0]),这是反模式 - 不要继承
Controller再加个success()方法,那只是把重复代码换个地方写 - 核心是让所有 JSON 响应都走同一个构造器,包括异常抛出后的自动响应
如何用 Response 类封装统一结构(ThinkPHP 6.1+)
ThinkPHP 的 Response 类支持自定义输出内容,关键在重写 output() 方法,但更稳妥的做法是:不改框架源码,而是在中间件或事件中接管响应体。
推荐方案:新建 app/common/Response.php,继承 think\Response,覆盖 init() 和 send(),但实际只需重载 output() —— 因为 json() 最终调用的就是它:
立即学习“PHP免费学习笔记(深入)”;
namespace app\common;
use think\Response;
class Response extends \think\Response
{
protected function output($data): string
{
$result = [
'code' => 0,
'msg' => 'ok',
'data' => $data,
'time' => time(),
];
// 如果$data本身已是标准结构(如异常时传入的数组),不二次包装
if (is_array($data) && isset($data['code']) && isset($data['msg'])) {
$result = $data;
}
return json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
然后在 app/middleware/ResponseFormat.php 中强制使用它:
- 在
handle()末尾判断响应类型:if ($response instanceof \think\response\Json) - 用
new \app\common\Response($response->getData())替换原响应 - 注意:不能直接
$response = new ...赋值,要return新实例
异常响应怎么也走统一格式
默认情况下,ValidateException 或 HttpException 抛出后,ThinkPHP 会走 think\exception\Handle,返回的是 HTML 页面或原始 JSON,跟你的 API 格式对不上。
解决办法不是重写整个异常处理器,而是利用 render() 方法做类型识别:
public function render(Request $request, Throwable $e): Response
{
if ($request->isAjax() || $request->header('accept') === 'application/json') {
$code = $e instanceof HttpException ? $e->getStatusCode() : 500;
$msg = $e->getMessage();
$data = [];
if ($e instanceof ValidateException) {
$msg = $e->getError();
$data = $e->getRuleMsg(); // 或留空
}
return json([
'code' => $code >= 400 ? $code : 1,
'msg' => $msg,
'data' => $data,
'time' => time(),
]);
}
return parent::render($request, $e);
}
- 别漏掉
$request->isAjax()判断,否则 POST 表单提交也会被当成 API 响应 -
ValidateException的getError()返回的是第一条错误,如果要全部,得用$e->getErrors()并转成字符串或数组 - HTTP 状态码 200 是安全的,但前端通常只看
code字段,所以异常时设code为非 0 更可靠
前端拿不到 code?检查 Content-Type 和 CORS
即使后端结构完全正确,前端 fetch().then(res => res.json()) 拿到的还是原始数据,说明响应头没设对——ThinkPHP 默认 JSON 响应的 Content-Type 是 application/json,但某些 Nginx 配置或 CDN 会把它改成 text/plain,导致浏览器不解析 JSON。
验证方法:用 curl 看响应头:curl -I http://api.xxx.com/user,确认有 Content-Type: application/json。
- 如果用了 Nginx,检查有没有
add_header Content-Type "text/plain";这类覆盖配置 - CORS 中间件若手动设置了
Access-Control-Allow-Headers,别漏掉Content-Type - 测试时用 Postman 直接看 raw body,比浏览器 Network 面板更准——有时候浏览器缓存了旧响应头
统一格式这事,最难的不是写代码,是让所有出口(正常返回、redirect、异常、验证失败、模型 save 失败)都经过同一层包装。少一个分支,前端就得多写一种 case。



















