flash-express / php-push-sdk
FlashExpress Push Message SDK - 支持多场景 push 消息发送、站内信、设备 token 获取,通过 JSON-RPC 调用 ard-api / by_rpc
Requires
- php: >=7.2
- ext-json: *
- guzzlehttp/guzzle: ^6.5 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^8.0 || ^9.0 || ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
FlashExpress 推送消息 PHP SDK,支持单条/批量推送、站内信发送、设备 Token 获取,通过 JSON-RPC 调用 ard-api / by_rpc 接口。
环境要求
- PHP >= 7.2
- ext-curl
- ext-json
- ext-openssl
安装
composer require flash-express/php-push-sdk:^1.4.1
各版本新增 / 调整的功能见 CHANGELOG.md
快速开始
初始化
<?php
use FlashExpress\Push\Client;
$client = new Client([
// 必填: ard-api SVC JSON-RPC 入口地址 (例如 /svc/call)
'ard_api_endpoint' => 'http://ard-api.svc/svc/call',
// 可选: by_rpc 地址 (用于获取待审批数量等)
'by_rpc_endpoint' => 'http://by-rpc.svc',
// 可选: 签名开关, 默认 false (关闭)
// 关闭时不校验 app_id / app_key, 也不生成签名
// 开启时 app_id 与 app_key 必填, 缺任一则初始化直接报错
'signature_enabled' => true,
// 条件必填: push.* 接口签名鉴权凭据 (签名开关开启时必填)
'app_id' => 'your_app_id',
'app_key' => 'your_app_key',
// 可选: 语言环境, 默认 zh-CN
'locale' => 'zh-CN',
// 可选: HTTP 超时 (秒), 默认 5
'timeout' => 5,
// 可选: RPC 失败重试次数, 默认 0 (不重试)
'retry_times' => 0,
// 可选: 重试间隔 (毫秒), 默认 200
'retry_delay' => 200,
// 可选: 性能追踪开关, 默认 false (关闭时不采集任何指标)
'trace_enabled' => false,
]);
API 接口
1. 单条推送 (按工号/客户 ID)
向单个指定用户发送 push 消息,SDK 自动获取设备 Token。
$response = $client->pushToStaff([
'src' => Client::SRC_BACKYARD, // 来源系统: c | ka | kit | backyard | osm
'staff_info_id' => '100001', // 工号或客户 ID
'message_title' => '您有新的审批待处理',
'message_content' => '请及时前往审批中心处理',
'message_scheme' => 'app://approval/detail', // 可选: 点击跳转 Scheme
'message_priority' => 0, // 可选: 0 普通 | 1 优先
'silence' => 0, // 可选: 0 有声音 | 1 静默推送
'thumbnail' => 'https://xxx.com/icon.png', // 可选: 图片推送 (iOS 富推送 / Android 图文)
]);
if ($response->isSuccess()) {
echo $response->getMessage();
} else {
echo $response->getMessage();
}
来源系统常量:
| 常量 | 值 | 说明 |
|---|---|---|
Client::SRC_C | 'c' | C端用户 |
Client::SRC_KA | 'ka' | KA客户 |
Client::SRC_KIT | 'kit' | KIT (快递员手持客户端) |
Client::SRC_BACKYARD | 'backyard' | OA (员工端) |
Client::SRC_OSM | 'osm' | OSM (外协员工管理客户端) |
Client::SRC_PAY | 'flashpay' | FlashPay (支付客户端) |
2. 批量推送 (按工号/客户 ID 列表)
一次性向最多 100 个用户推送消息。外层 message_* 字段作为公共参数,list 内每个 item 可单独覆盖(内层优先,缺失字段自动回落到外层)。
SDK 内部自动检测"统一内容"模式:当 list 中每个 item 只含 staff_info_id 和 src 两个字段时,说明消息内容一致,会按 os + src + device_type 分组合并 device_token 为数组,一条队列消息最多携带 100 个 token,大幅减少 MQ 写入量。
情形一: 统一内容 (推荐, 消息内容一致时使用)
$response = $client->pushToStaffs([
// 外层公共参数: 所有用户共享同一消息内容
'message_title' => '团队通知',
'message_content' => '今日例会时间调整为 15:00',
'message_scheme' => 'app://meeting',
'message_priority' => 0,
'src' => Client::SRC_BACKYARD
// 每个 item 只需 staff_info_id + src, 消息内容全部取自外层
// SDK 会自动合并同 os+src+device_type 的设备 token, 一条消息最多 100 个
'list' => [
['staff_info_id' => '100001'],
['staff_info_id' => '100002'],
['staff_info_id' => '100003'],
// ... 最多 100 个
],
]);
if ($response->isSuccess()) {
$data = $response->getData();
echo "总计: {$data['total']}, 成功: {$data['success']}, 失败: {$data['fail']}";
}
情形二: 非统一内容 (每个用户消息不同)
$response = $client->pushToStaffs([
// 外层公共参数 (被 list 内层 item 覆盖, 内层缺失的字段从此处取)
'message_title' => '团队通知',
'message_content' => '今日例会时间调整为 15:00',
'message_scheme' => 'app://meeting',
'message_priority' => 0,
// 每个 item 可携带额外字段覆盖外层, 消息内容各不相同
'list' => [
['staff_info_id' => '100001', 'src' => Client::SRC_BACKYARD],
['staff_info_id' => '100002', 'src' => Client::SRC_BACKYARD],
[
'staff_info_id' => '100003',
'src' => Client::SRC_KA,
'message_title' => '单独标题', // 仅覆盖此条的 title, 其他继续取外层
'message_content' => '单独内容',
],
],
]);
if ($response->isSuccess()) {
$data = $response->getData();
echo "总计: {$data['total']}, 成功: {$data['success']}, 失败: {$data['fail']}";
}
3. 直接设备 Token 推送
跳过设备 Token 查询,直接推送。适用于调用方已有设备 Token 的场景。
SDK 内部自动检测"统一内容"模式:当 list 中每个 item 只含 os、device_token、device_type、src(或其子集)时,说明消息内容一致,会按 os + src + device_type 分组合并 device_token 为数组,一条队列消息最多携带 100 个 token。
情形一: 统一内容 (推荐, 消息内容一致时使用)
$response = $client->pushToDevices([
// 外层公共参数: 所有设备共享同一消息内容
'message_title' => '新订单来了',
'message_content' => '点击查看详情',
'message_scheme' => 'app://order',
'message_priority' => 0,
'src' => Client::SRC_OSM
// 每个 item 只需 os/device_token/device_type/src, 消息内容全部取自外层
// SDK 会自动合并同 os+src+device_type 的设备 token, 一条消息最多 100 个
'list' => [
['os' => 'android', 'device_token' => 'token1', 'device_type' => Client::DEVICE_TYPE_GOOGLE],
['os' => 'android', 'device_token' => 'token2', 'device_type' => Client::DEVICE_TYPE_HUAWEI],
['os' => 'ios', 'device_token' => 'token3', 'device_type' => Client::DEVICE_TYPE_APPLE],
// ... 最多 100 个
],
]);
if ($response->isSuccess()) {
$data = $response->getData();
echo "总计: {$data['total']}, 成功: {$data['success']}, 失败: {$data['fail']}";
}
情形二: 非统一内容 (每个设备消息不同)
$response = $client->pushToDevices([
// 外层公共参数 (被 list 内层 item 覆盖, 内层缺失的字段从此处取)
'message_priority' => 0,
// 每个 item 可携带额外字段覆盖外层, 消息内容各不相同
'list' => [
[
'os' => 'android',
'device_token' => 'APA91b...',
'device_type' => Client::DEVICE_TYPE_GOOGLE,
'staff_info_id' => '100001',
'src' => Client::SRC_OSM,
'message_title' => '新订单来了',
'message_content' => '点击查看详情',
'message_scheme' => 'app://order',
'badge' => 0,
],
[
'os' => 'ios',
'device_token' => 'abc123...',
'device_type' => Client::DEVICE_TYPE_APPLE,
'staff_info_id' => '100002',
'src' => Client::SRC_KIT,
'message_title' => '配送提醒',
'message_content' => '您有新的配送任务',
'message_scheme' => 'app://delivery',
],
],
]);
if ($response->isSuccess()) {
$data = $response->getData();
echo "总计: {$data['total']}, 成功: {$data['success']}, 失败: {$data['fail']}";
}
设备类型常量:
| 常量 | 值 |
|---|---|
Client::DEVICE_TYPE_GOOGLE | 1 |
Client::DEVICE_TYPE_HUAWEI | 2 |
响应格式
所有方法统一返回 FlashExpress\Push\Contracts\ResponseInterface 对象:
$response->isSuccess(); // code === 1
$response->isFail(); // code === 0
$response->isError(); // code === -1
$response->getCode(); // 1=成功 | 0=业务失败 | -1=系统/参数错误
$response->getMessage(); // 消息文本
$response->getData(); // 业务数据 (mixed)
$response->getRaw(); // 原始 JSON-RPC 响应 (string)
$response->toArray(); // 转数组 ['code'=>1, 'msg'=>'ok', 'data'=>...]
异常处理
SDK 不会主动抛出异常,所有错误都通过 Response::error() / Response::fail() 返回。
如需捕获底层异常,可 catch 以下类型:
| 异常类 | 触发场景 |
|---|---|
FlashExpress\Push\Exceptions\ValidationException | 配置项缺失 / 参数校验失败 |
FlashExpress\Push\Exceptions\RpcException | JSON-RPC 调用异常 |
FlashExpress\Push\Exceptions\PushException | SDK 通用异常 (基类) |
use FlashExpress\Push\Exceptions\RpcException;
use FlashExpress\Push\Exceptions\ValidationException;
try {
$response = $client->pushToStaff([...]);
} catch (ValidationException $e) {
echo '参数错误: ' . $e->getMessage();
echo '详细: ' . json_encode($e->getErrors());
} catch (RpcException $e) {
echo 'RPC 调用失败: ' . $e->getMessage();
}
鉴权配置
ard-api 的 push.* 开头的 JSON-RPC 接口已强制开启签名鉴权,由 SignatureAuthMiddleware 在 SVC Server 层统一拦截校验。调用方需在配置中开启签名开关并传入凭据,SDK 会自动完成签名计算并附加到每个请求。
范围说明:仅对 JSON-RPC 方法名以
push.开头的接口生效(如push.sendToMQ、push.getDeviceToken、push.sendInternalMessage),其他 SVC 方法(如 by_rpc 的get_panding_count)不受影响,无需签名。
签名开关
由配置项 signature_enabled 控制,默认关闭:
signature_enabled | app_id / app_key | 行为 |
|---|---|---|
false(默认) | 不校验 | 不生成签名,请求不带任何签名 Header |
true | 两个都不为空 | 生成签名并附加到请求 |
true | 任一为空 | 初始化即抛出 ValidationException,不会继续发请求 |
开启签名但凭据缺失时会在
new Client([...])阶段直接报错,错误信息与getErrors()会指明缺失的字段,避免带着无效配置发起注定失败的请求。
也可以在运行时切换(用法同 setDebug(),支持链式调用):
$client->setSignatureEnabled(true); // 开启; 凭据缺失会抛 ValidationException 且不改变当前状态
$client->setSignatureEnabled(false); // 关闭; 后续请求不再带签名头
申请凭据
在 ard-api 侧为调用方分配一对 app_id / app_key,服务端通过以下方式之一验证:
- 推荐(生产):环境变量
PUSH_SDK_APPKEY_{APP_ID}(app_id 转大写) - 开发调试:
SignatureAuthMiddleware::APP_SECRETS常量数组
完整调用示例
use FlashExpress\Push\Client;
$client = new Client([
'ard_api_endpoint' => 'http://ard-api.svc/svc/call',
'signature_enabled' => true, // 开启签名
'app_id' => 'your_app_id', // 开启签名时必填
'app_key' => 'your_app_key', // 开启签名时必填
'locale' => 'zh-CN',
'timeout' => 5,
]);
$response = $client->pushToStaff([
'src' => Client::SRC_BACKYARD,
'staff_info_id' => '100001',
'message_title' => '您有新的审批待处理',
'message_content' => '请及时前往审批中心处理',
]);
if ($response->isSuccess()) {
echo "推送成功\n";
} else {
echo "推送失败: " . $response->getMessage() . "\n";
}
SDK 自动附加的 HTTP Header
所有对 push.* 方法的调用在 签名开关开启时 会自动附带以下 Header,调用方无需手动设置;开关关闭时不附带任何签名 Header:
| Header | 说明 |
|---|---|
X-App-Id | 应用 ID |
X-Timestamp | 秒级时间戳,服务端 ±5 分钟 有效期 |
X-Nonce | 32 位随机十六进制字符串 |
X-Signature | 签名值 |
签名算法(与服务端 SignatureAuthMiddleware 一致)
stringToSign = X-Timestamp + X-Nonce + json_encode(params, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)
X-Signature = hash_hmac('sha256', stringToSign, app_key)
注意:签名仅对 JSON-RPC 请求体的
params字段计算,不包含jsonrpc/id/method等其他顶层字段。
常见鉴权失败
| 服务端返回 | 原因 | 排查方向 |
|---|---|---|
缺少必要 Header | 签名开关未开启,或开启了但未传 app_id / app_key | 开启 signature_enabled 并确认凭据非空(开启时凭据缺失会在 SDK 初始化阶段直接报错) |
无效的 App ID | app_id 在服务端未注册 | 检查 PUSH_SDK_APPKEY_{APP_ID} 环境变量或常量 |
时间戳已过期 | 服务器时间与客户端差超过 5 分钟 | 检查服务器/客户端 NTP 同步 |
签名不匹配 | 签名计算逻辑不一致 | 确认 SDK 版本与服务端 SignatureAuthMiddleware 一致 |
调试技巧
开启 debug 模式可在日志中看到每次请求的完整 Header、Payload 和响应:
$client->setDebug(true);
// 或直接在 config 里传 'debug' => true
$response = $client->pushToStaff([...]);
// 最近一次请求的完整调试上下文
$debug = $client->getDebugInfo();
echo json_encode($debug, JSON_PRETTY_PRINT);
// 包含: endpoint / method / payload / headers / http_code / curl_error / response / duration_ms
性能追踪
debug 解决「请求发生了什么」,性能追踪解决「时间花在哪里」。通过配置项 trace_enabled(默认关闭)或运行时 setTraceEnabled() 开启,开启后每次业务调用会记录一条链路:root span(业务方法)+ 每次 RPC 尝试 + 本地构建阶段。
$client = new Client([
'ard_api_endpoint' => 'http://ard-api.svc/svc/call',
'trace_enabled' => true, // 默认 false
]);
// 或运行时开启 (用法同 setDebug, 支持链式调用)
$client->setTraceEnabled(true)->pushToStaffs([...]);
$trace = $client->getTraceInfo();
getTraceInfo() 返回结构:
[
'total_ms' => 12.34, // 本次调用总耗时
'spans' => [ // 按开始顺序, depth 表示层级
['name' => 'pushToStaffs', 'depth' => 0, 'duration_ms' => 12.34, 'meta' => []],
['name' => 'rpc:push.batchGetDeviceToken', 'depth' => 1, 'duration_ms' => 9.80,
'meta' => ['method' => 'push.batchGetDeviceToken', 'attempt' => 1, 'status' => 'ok']],
['name' => 'build_messages', 'depth' => 1, 'duration_ms' => 0.07, 'meta' => []],
['name' => 'rpc:push.sendToMQ', 'depth' => 1, 'duration_ms' => 2.40,
'meta' => ['method' => 'push.sendToMQ', 'attempt' => 1, 'status' => 'ok']],
],
'summary' => [ // 按 span 名称聚合, 便于看重复 RPC 的累积耗时
'rpc:push.getPendingCount' => ['count' => 3, 'total_ms' => 90.0, 'max_ms' => 40.0],
],
]
要点:
- 未开启时不创建 Tracer,所有打点直接跳过,不产生采集开销
- 每次 RPC 尝试单独一个 span:
meta.attempt区分第几次尝试,meta.status为ok/error,meta.retryable标记是否可重试 —— 能分辨「一次慢请求」还是「多次重试累积慢」 - root span 开在业务方法层,所以能看到「卡在哪一环」,而不是只有单条 RPC
- 若已通过
setLogger()注入 logger,trace 开启时每个 span 还会写一条[PushSDK][trace]的结构化 JSON 日志(级别info),便于接入日志聚合分析;未注入 logger 时只保留内存结果 getTraceInfo()只保留最近一次调用,未开启追踪时返回null
请求超时设置
默认超时时间在初始化时通过 timeout 配置项设定(秒,默认 5)。如需在运行时临时调整,可调用 setTimeout(),对后续所有 RPC 调用立即生效。
// 初始化时设定默认超时
$client = new Client([
'ard_api_endpoint' => 'http://ard-api.svc/svc/call',
'signature_enabled' => true,
'app_id' => 'your_app_id',
'app_key' => 'your_app_key',
'timeout' => 5, // 默认 5 秒
]);
// 批量推送可能耗时较长, 临时调大超时
$client->setTimeout(30);
$response = $client->pushToStaffs([...]);
// 复位为默认值
$client->setTimeout(5);
// 支持链式调用
$response = $client->setTimeout(30)->pushToStaffs([...]);
架构说明
┌──────────────┐ JSON-RPC ┌──────────────┐
│ Push SDK │ ───────────▶ │ ard-api │
│ (Client) │ sendToMQ │ PushController
└──────┬───────┘ getDeviceToken └──────────────┘
│
│ JSON-RPC
▼
┌──────────────┐
│ by_rpc │
│ (待审批数) │
└──────────────┘
SDK 通过 push.* 方法名调用 ard-api SVC 接口,通过 get_panding_count 调用 by_rpc。
License
MIT