如何设计业务异常码与国际化消息?

解读

面试官问“业务异常码与国际化消息”,并不是想听“用 try-catch 抛 Exception”这种入门级答案,而是考察候选人是否具备“可维护、可扩展、可交付海外客户”的工程化思维。国内一线互联网与外包项目普遍面临以下痛点:

  1. 异常码随意编号,前端、客户端、运维三方对不上,排查靠吼;
  2. 错误提示直接写死中文,海外用户看到“用户名不能为空”一脸懵;
  3. 异常穿透到前端,把 SQL 语句直接暴露,引发安全事故;
  4. 多项目复用时,每个团队重新定义一遍,造成“重复造轮子”。

因此,面试官期望你给出“编号规范 + 国际化翻译 + 统一渲染”的闭环方案,并能在 Laravel/Yii/ThinkPHP 等主流框架里落地。

知识点

  1. 异常码分层规范
    采用“3 位业务域 + 3 位子模块 + 3 位错误序号”的 9 位数字,如 100200007,可读可排序,避免魔法数字。
  2. 国际化键命名
    使用蛇形小写,带模块前缀,如 auth.user_not_found,与 Laravel 的 __() 助手函数天然契合。
  3. 翻译文件组织
    按语言代码分目录,resources/lang/{locale}/business.php,保证翻译外包团队可独立 PR。
  4. 异常基类设计
    定义 BusinessException extends Exception 并强制传入 $code、$messageKey、$replacements,屏蔽直接 new \Exception。
  5. 渲染管道
    利用框架的 ExceptionHandler,统一把 BusinessException 映射成 {code, message, data} 的 JSON,非业务异常走 500 兜底。
  6. 日志与监控
    异常码进入 ELK/SkyWalking 索引字段,方便告警规则“code > 100200000 且 code < 100300000”直接@责任人。
  7. 自动化测试
    单元测试断言 getCode()getMessageKey(),防止重构时把码改错;集成测试通过 __() 切换语言,保证 i18n 全覆盖。

答案

下面给出一套可直接搬进简历项目的“最小可运行”设计,以 Laravel 8+ 为例,其他框架思路同源。

  1. 定义异常码枚举(仅示范)
namespace App\Exceptions\Code;
final class UserCode
{
    const NOT_FOUND     = 100200001;
    const PASSWORD_ERROR = 100200002;
    const STATUS_BANNED = 100200003;
}
  1. 业务异常基类
namespace App\Exceptions;
use Illuminate\Support\Facades\Lang;
class BusinessException extends \Exception
{
    protected string $messageKey;
    protected array  $replacements;

    public function __construct(int $code, string $messageKey, array $replacements = [], \Throwable $previous = null)
    {
        $this->messageKey   = $messageKey;
        $this->replacements = $replacements;
        // 翻译在渲染阶段再做,构造函数只记录 key
        parent::__construct('', $code, $previous);
    }

    public function getMessageKey(): string
    {
        return $this->messageKey;
    }

    public function getReplacements(): array
    {
        return $this->replacements;
    }

    // 供 Handler 统一调用
    public function translate(): string
    {
        return Lang::get($this->messageKey, $this->replacements);
    }
}
  1. 具体异常
namespace App\Exceptions\User;
use App\Exceptions\BusinessException;
use App\Exceptions\Code\UserCode;
class UserNotFoundException extends BusinessException
{
    public function __construct(string $username)
    {
        parent::__construct(
            UserCode::NOT_FOUND,
            'user.not_found',
            ['name' => $username]
        );
    }
}
  1. 翻译文件
    resources/lang/zh_CN/business.php
return [
    'user.not_found' => '用户 :name 不存在',
];

resources/lang/en/business.php

return [
    'user.not_found' => 'User :name not found',
];
  1. Handler 统一渲染
public function render($request, \Throwable $e)
{
    if ($e instanceof BusinessException) {
        return response()->json([
            'code'    => $e->getCode(),
            'message' => $e->translate(),   // 自动根据当前 locale 翻译
            'data'    => null,
        ], 200);  // 业务异常 HTTP 状态仍为 200,前端根据 code 弹窗
    }

    return parent::render($request, $e);
}
  1. 使用示例
$user = User::find($id);
if (! $user) {
    throw new UserNotFoundException($username);
}
  1. 日志与监控埋点
    在 Handler 的 report 方法里:
if ($e instanceof BusinessException) {
    \Log::channel('business')->warning('biz_error', [
        'code' => $e->getCode(),
        'key'  => $e->getMessageKey(),
        'uri'  => request()->getRequestUri(),
    ]);
}

配合阿里云 SLS 或 ELK,即可按异常码维度做 Dashboard。

拓展思考

  1. 如何支持“多项目复用”?
    把异常码、基类、翻译文件抽成私有 Composer 包 company/biz-exception,通过 GitLab CI 发版,各业务线只需 composer require 即可统一规范。

  2. 如何防止“异常码泄漏”给黑客?
    对外只暴露 9 位码,详细堆栈写进日志并脱敏;同时给前端提供“错误映射表”,把敏感信息替换成“系统繁忙,请稍后再试”。

  3. 如何与 OpenAPI 文档联动?
    swagger-php 注解里增加 x-biz-codes 字段,CI 阶段扫描异常码枚举,自动生成“错误码说明”章节,减少文档同步成本。

  4. 如何灰度切换语言?
    在 Header 或 Query 里带 ?lang=th-TH,中间件解析后 App::setLocale($lang),可实现“链接即语言”,方便海外运营灰度。

  5. 如何兼容传统 Bank/ERP 的“字母+数字”短码?
    新增一层映射表:business_error_map(code, short_code, locale, message),对外 API 可返回短码 P2001,对内仍用 9 位数字,兼顾老系统习惯。

掌握以上思路,你不仅能在面试中把“异常码与国际化”讲成体系化方案,还能让面试官感受到你对“高可维护、可交付海外”项目的真实经验。