结论:用策略模式封装推送SDK是为解决厂商接口差异导致的维护痛点。通过抽象PushStrategy接口及各厂商实现类,统一send方法,将密钥、参数转换、异常处理等差异收口在策略内部,业务代码只需调用$pusher->send(),切换厂商或适配升级只需改对应策略类,避免全局搜索和if-else泥潭。

直接说结论:用策略模式封装不同推送 SDK,不是为了“设计漂亮”,而是为了解决真实痛点——比如极光升级 v4 接口后 JPush\Client 报 Class not found,而个推还在用 getui/igetui v3;又或者某天要切到阿里云推送,却不敢动原有 jpush()->send() 调用链。
为什么不能直接在业务里 new JPush\Client() 或 new \Getui\IGtPush()?
硬编码 SDK 实例会带来三个实际问题:
- 测试难:无法 mock 推送行为,每次跑单元测试都真发消息
- 切换慢:换厂商时得全局搜
->push()、->send(),漏改一处就丢通知 - 参数散:极光用
setAudience('all'),个推用setAudience('all')但含义不同(个推的all不含离线设备),阿里云却要用Target => 'ALL'——这些差异全堆在 Controller 里,越写越像 if-else 泥潭
定义统一推送接口与具体策略类
核心是抽象出一个 PushStrategy 接口,只暴露业务真正关心的操作:
interface PushStrategy
{
public function send(string $title, string $body, array $targets): array;
}
每个厂商对应一个实现类,例如极光:
立即学习“PHP免费学习笔记(深入)”;
class JPushStrategy implements PushStrategy
{
private $client;
public function __construct(string $appKey, string $masterSecret)
{
$this->client = new \JPush\Client($appKey, $masterSecret);
}
public function send(string $title, string $body, array $targets): array
{
// 极光要求 targets 是 alias/tag 数组,不是字符串
$audience = empty($targets) ? 'all' : ['alias' => $targets];
try {
$result = $this->client->push()
->setPlatform('all')
->setAudience($audience)
->setNotification(\JPush\Model\Notification::alert($body))
->send();
return ['success' => true, 'msg_id' => $result['msg_id'] ?? null];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
}
个推同理,但注意它的 setAudience() 接收的是 \IGtAudience 对象,且必须显式构造:
- 别直接传
'all'字符串,会报Call to a member function setAppId() on string - 用
\IGtAudience::all()才是正确姿势 - 个推返回结构是对象,不是数组,取
$result->data->msgId,不是$result['msg_id']
上下文类如何安全地选策略?
避免用 if ($vendor === 'jpush') { new JPushStrategy(...) } 这种硬分支。推荐两种方式:
- 配置驱动:在
config/push.php里定义默认厂商和密钥,运行时根据配置 new 对应策略 - 工厂方法:写一个
PushStrategyFactory::make(string $vendor),内部用 switch,但把所有 new 都收口到这一处
关键点:
- 工厂类不依赖任何 SDK,只返回
PushStrategy接口,方便单元测试时注入 mock - 如果某厂商 SDK 加载失败(比如
class_exists('JPush\Client')为 false),工厂应抛出明确异常,而不是静默 fallback 到其他厂商 - 别在工厂里做参数转换,比如把
['user_123']自动包装成['alias' => ['user_123']]——这属于策略内部逻辑,各厂商处理方式不同
最容易被忽略的兼容性细节
真实项目里,这些点常导致线上静默失败:
- 个推的
setPushType('NOTICE')和极光的setNotification()行为不等价:个推设为 NOTICE 后,iOS 可能不走 APNs,而极光的 notification 默认走系统通知通道 - 阿里云推送的
DeviceType必须显式指定'ANDROID'/'IOS'/'ALL',填'all'小写会直接 400 - 所有 SDK 的 cURL 超时默认值都偏短(通常 5~10 秒),高并发推送时容易触发
cURL error 28: Operation timed out,必须在策略构造时统一设置curl_setopt(CURLOPT_TIMEOUT, 30)级别 - 极光 v4 SDK 的
setAudience()不再接受字符串'all',必须用\JPush\Model\Audience::all(),否则静默忽略
策略模式的价值不在“用了设计模式”,而在于把上述所有差异锁死在几个策略类里,让业务代码永远只跟 $pusher->send($title, $body, $ids) 打交道。一旦某个厂商接口变更,你只需要改一个文件,而不是 grep 全项目。



















