如何设计业务异常码与国际化消息?
解读
面试官问“业务异常码与国际化消息”,并不是想听“用 try-catch 抛 Exception”这种入门级答案,而是考察候选人是否具备“可维护、可扩展、可交付海外客户”的工程化思维。国内一线互联网与外包项目普遍面临以下痛点:
- 异常码随意编号,前端、客户端、运维三方对不上,排查靠吼;
- 错误提示直接写死中文,海外用户看到“用户名不能为空”一脸懵;
- 异常穿透到前端,把 SQL 语句直接暴露,引发安全事故;
- 多项目复用时,每个团队重新定义一遍,造成“重复造轮子”。
因此,面试官期望你给出“编号规范 + 国际化翻译 + 统一渲染”的闭环方案,并能在 Laravel/Yii/ThinkPHP 等主流框架里落地。
知识点
- 异常码分层规范
采用“3 位业务域 + 3 位子模块 + 3 位错误序号”的 9 位数字,如 100200007,可读可排序,避免魔法数字。 - 国际化键命名
使用蛇形小写,带模块前缀,如auth.user_not_found,与 Laravel 的__()助手函数天然契合。 - 翻译文件组织
按语言代码分目录,resources/lang/{locale}/business.php,保证翻译外包团队可独立 PR。 - 异常基类设计
定义BusinessException extends Exception并强制传入$code、$messageKey、$replacements,屏蔽直接 new \Exception。 - 渲染管道
利用框架的ExceptionHandler,统一把 BusinessException 映射成{code, message, data}的 JSON,非业务异常走 500 兜底。 - 日志与监控
异常码进入 ELK/SkyWalking 索引字段,方便告警规则“code > 100200000 且 code < 100300000”直接@责任人。 - 自动化测试
单元测试断言getCode()与getMessageKey(),防止重构时把码改错;集成测试通过__()切换语言,保证 i18n 全覆盖。
答案
下面给出一套可直接搬进简历项目的“最小可运行”设计,以 Laravel 8+ 为例,其他框架思路同源。
- 定义异常码枚举(仅示范)
namespace App\Exceptions\Code;
final class UserCode
{
const NOT_FOUND = 100200001;
const PASSWORD_ERROR = 100200002;
const STATUS_BANNED = 100200003;
}
- 业务异常基类
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);
}
}
- 具体异常
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]
);
}
}
- 翻译文件
resources/lang/zh_CN/business.php
return [
'user.not_found' => '用户 :name 不存在',
];
resources/lang/en/business.php
return [
'user.not_found' => 'User :name not found',
];
- 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);
}
- 使用示例
$user = User::find($id);
if (! $user) {
throw new UserNotFoundException($username);
}
- 日志与监控埋点
在 Handler 的 report 方法里:
if ($e instanceof BusinessException) {
\Log::channel('business')->warning('biz_error', [
'code' => $e->getCode(),
'key' => $e->getMessageKey(),
'uri' => request()->getRequestUri(),
]);
}
配合阿里云 SLS 或 ELK,即可按异常码维度做 Dashboard。
拓展思考
-
如何支持“多项目复用”?
把异常码、基类、翻译文件抽成私有 Composer 包company/biz-exception,通过 GitLab CI 发版,各业务线只需composer require即可统一规范。 -
如何防止“异常码泄漏”给黑客?
对外只暴露 9 位码,详细堆栈写进日志并脱敏;同时给前端提供“错误映射表”,把敏感信息替换成“系统繁忙,请稍后再试”。 -
如何与 OpenAPI 文档联动?
在swagger-php注解里增加x-biz-codes字段,CI 阶段扫描异常码枚举,自动生成“错误码说明”章节,减少文档同步成本。 -
如何灰度切换语言?
在 Header 或 Query 里带?lang=th-TH,中间件解析后App::setLocale($lang),可实现“链接即语言”,方便海外运营灰度。 -
如何兼容传统 Bank/ERP 的“字母+数字”短码?
新增一层映射表:business_error_map(code, short_code, locale, message),对外 API 可返回短码P2001,对内仍用 9 位数字,兼顾老系统习惯。
掌握以上思路,你不仅能在面试中把“异常码与国际化”讲成体系化方案,还能让面试官感受到你对“高可维护、可交付海外”项目的真实经验。