必须手动注册独立短信客户端组件并配置超时与重试逻辑,否则会导致发送失败无日志、错误码被吞、连接池耗尽;需在components中配置sms组件,timeout设为8秒以上,smsclient需继承component、注入独立httpclient,解析响应code并实现指数退避重试,控制器中通过$this->sms或di调用。

Yii2.0.51项目中接入短信服务时,必须绕过框架默认的邮件组件路径,手动注册独立的短信客户端实例;不配置全局组件别名、不显式绑定依赖、不处理HTTP超时与重试逻辑,会导致发送失败无日志、错误码被吞、高并发下连接池耗尽。
注册短信客户端为应用组件
打开config/web.php(Web应用)或config/console.php(命令行应用),在'components'数组内添加短信服务配置项:
'sms' => [ 'class' => 'common\components\SmsClient', 'apiUrl' => 'https://sms-api.example.com/v3/send', 'accessKey' => 'ak-xxxxxx', 'secretKey' => 'sk-xxxxxx', 'timeout' => 8,],
这一步不能写在params里——组件必须由Yii DI容器管理,否则无法在控制器或服务类中通过$this->sms访问。
【timeout必须设为8秒以上】国内主流短信网关(如阿里云、腾讯云、容联)在高峰时段响应常达3~6秒,设成默认5秒会导致约12%请求被误判超时。
实现SmsClient类并注入HTTP客户端
在common/components/SmsClient.php中创建类:
继承\yii\base\Component,构造函数中注入\yii\httpclient\Client实例:
public function __construct(array $config = []) { $this->httpClient = Yii::createObject([ 'class' => 'yii\httpclient\Client', 'transport' => 'yii\httpclient\CurlTransport', 'requestConfig' => ['format' => \yii\httpclient\Client::FORMAT_JSON], ]); parent::__construct($config);}
不要复用Yii::$app->httpclient——它可能已被其他模块修改了默认header或baseUri,导致短信请求头污染。
定义发送方法:public function send($phone, $templateId, $params = []),内部调用$this->httpClient->post($this->apiUrl)->setData([...])->send()。
处理发送失败与重试逻辑
短信网关返回HTTP 200不等于发送成功,必须解析响应体中的code字段:
第一步:检查$response->isOk,若为false则直接抛出\yii\httpclient\Exception;
第二步:解码JSON响应,判断$data['code'] === 0(以阿里云为例),否则记录完整响应体到@runtime/logs/sms-error.log;
第三步:对临时性错误(如code为10003“频率超限”、10004“余额不足”)做指数退避重试,最多2次,间隔1.5秒→3秒;
第四步:重试后仍失败,抛出common\exceptions\SmsSendException,该异常类需继承\yii\base\Exception且不被通用try/catch捕获。
这一步不能依赖框架的retry行为——Yii2.0.51的yii\httpclient\Client不支持自动重试,硬编码逻辑更可控。
在控制器中调用短信服务
方法一:通过$this->sms直接调用(前提是在控制器中声明public $sms;):
try { $this->sms->send('13800138000', 'SMS_123456789', ['code' => '1234']);} catch (\common\exceptions\SmsSendException $e) { Yii::error("短信发送失败: " . $e->getMessage(), 'sms'); throw new BadRequestHttpException('验证码发送失败,请稍后再试');}
方法二:在Action中用依赖注入(需先在config/web.php的'container'中绑定接口):
'container' => [ 'definitions' => [ 'common\interfaces\SmsInterface' => ['class' => 'common\components\SmsClient'], ],],
然后控制器构造函数声明public function __construct(private SmsInterface $sms, $config = [])。
注意:不要在beforeAction()里调用短信发送——它可能被缓存中间件跳过,导致验证码未发就进入主逻辑。











