推荐使用 alibabacloud/sdk:执行 composer require alibabacloud/sdk,清除旧 SDK 残留,配置时区为 Asia/Shanghai,短信限流须用 Redis 原子 Lua 脚本,SmsService 应依赖注入并统一异常。

阿里云短信 SDK 在 ThinkPHP 8 中怎么装才不报 ClassNotFoundException
直接用 Composer 安装官方 aliyun-openapi-php-sdk 会失败,它不兼容 PSR-4 自动加载,且依赖已废弃的 aliyun-php-sdk-core。ThinkPHP 8 默认用 Composer 2+ 和 PHP 8.1+,必须换轻量方案。
推荐用阿里云新推的 alibabacloud/sdk,它支持现代 PHP,且自带自动发现机制:
- 执行
composer require alibabacloud/sdk,别加版本号(默认拉取最新稳定版) - 删掉旧项目里手动引入的
aliyun-php-sdk-dysmsapi目录或vendor/aliyun下所有残留 - 确保
config/app.php中'default_timezone' => 'Asia/Shanghai'已设好——时区错会导致签名失败,报InvalidTimeStamp.Expired
发短信前必须绕开的三个 Cache 层陷阱
防刷不是加个 Cache::has() 就完事。ThinkPHP 的缓存驱动、键名设计、TTL 策略三者稍不匹配,就会让限流形同虚设。
- 别用
cache('sms:138****1234')这种裸键——Redis 驱动下会拼成:sms:138****1234,冒号开头导致批量清理失效;统一用Cache::tag('sms')->set($key, $value, $ttl) - 本地文件缓存(
File驱动)在多机部署时完全失效,生产环境必须切到Redis或Memcached -
$ttl别硬写60,验证码有效期是 5 分钟,但防刷窗口应设为 60 秒 + 频次限制(比如 1 小时最多 5 次),否则用户填错两次就锁死一小时
SmsService 类里怎么组织发送逻辑才不和业务耦合
把阿里云 SDK 初始化、请求构造、异常映射全塞进控制器,后续改渠道(比如切腾讯云)就得全局搜 AlibabaCloud,非常危险。
立即学习“PHP免费学习笔记(深入)”;
建议拆出独立服务类,关键点在依赖注入和错误归一化:
- 构造函数只接收配置数组,不 new 实例:
public function __construct(array $config) { $this->client = AlibabaCloud::rpc()->regionId($config['region'])->timeout($config['timeout']); } - 所有阿里云返回的异常(如
ClientException、ServerException)统一 try/catch 后转成自定义SmsSendException,上层只处理这个 - 手机号参数必须走
filter_var($phone, FILTER_VALIDATE_INT)或正则/^1[3-9]\d{9}$/校验,SDK 不做输入过滤,传错格式会返回模糊的InvalidParameter.PhoneNumber
为什么 Redis 的 INCR + EXPIRE 组合在高并发下会漏限流
用 Cache::inc($key) 再调 Cache::expire($key, 3600) 看似合理,但两个操作非原子——中间若进程崩溃或超时,key 就永久存在,再也无法重发。
- 必须用 Lua 脚本封装:ThinkPHP 的 Redis 驱动支持
$redis->eval(...),脚本内完成INCR+EXPIRE原子操作 - 别依赖
Cache::get($key, 0)判断是否超限——如果 key 过期但没被及时回收(Redis 惰性删除),get返回 null,inc又从 0 开始计,等于绕过限制 - 实际阈值检查要放在
INCR后立刻比对,例如:if ($count > 5) throw new SmsRateLimitException();,不能先查再 incr
防刷的核心从来不在“加了多少层缓存”,而在于“每次请求是否都落到同一份原子状态上”。Redis 键设计、TTL 设置、异常分支的 cache 清理,漏掉任意一环,限流就只是心理安慰。











