简介:一套即插即用的UniApp推送服务端方案,基于ThinkPHP框架开发,完整封装个推REST API V2标准接口。包含设备绑定、用户标签管理、即时/离线消息下发、送达与点击回执、数据统计等核心能力。核心类GTClient统一调度,GTPushApi负责消息推送,GTUserApi处理用户与设备关系,GTStatisticsApi支持效果追踪,GTBaseApi提供基础请求与异常封装。配套utils工具函数、request HTTP客户端、exception统一异常处理机制,以及test测试用例验证各接口可用性。composer.已预置个推SDK依赖,README详述接入步骤、参数说明与调用示例,LICENSE明确授权范围。适用于需要自主掌控推送链路、规避第三方平台限制的中大型UniApp项目,可快速集成到现有ThinkPHP系统中,无需改造前端逻辑,兼容UniPadk协议规范。
1. 项目概述:为什么UniApp项目需要一套自主可控的个推后端封装?
在做过十几个中大型UniApp项目之后,我越来越清楚一个事实:前端调用个推SDK直接发消息,看着简单,实操起来全是坑。你可能也遇到过——用户卸载重装App后收不到推送、同一设备反复注册导致标签错乱、后台想按用户画像批量推但接口返回400、甚至某天突然发现个推控制台里“送达率”和自己业务系统里“点击量”对不上账……这些问题,根源不在前端,而在于后端对接逻辑太粗糙。官方SDK文档写得像教科书,但没告诉你ThinkPHP里怎么优雅地做token自动刷新、怎么把设备ID和用户ID安全绑定、怎么处理V2接口里那些嵌套三层的JSON响应结构、更别说离线消息重试策略和回执状态机的设计了。
这套方案就是为解决这些真实痛点而生的。它不是简单地把个推PHP SDK扔进ThinkPHP里跑通就行,而是以“生产级可用”为唯一标准,从接口设计、异常兜底、数据一致性、调试可观测性四个维度重新梳理整条推送链路。核心关键词——个推V2,意味着我们完全放弃已停更的V1旧协议,所有请求头、签名算法、参数结构、错误码映射全部严格遵循个推2023年发布的REST API V2规范;ThinkPHP推送,不是用原生cURL硬写,而是深度融入TP的依赖注入、配置驱动、日志追踪体系;UniApp后端,特指适配UniApp多端(iOS/Android/H5/小程序)设备标识差异,比如iOS的deviceToken要转base64再传,Android的cid需做长度校验防注入;GTClient封装,是整个架构的中枢,它不暴露底层HTTP细节,只提供pushToUser()、bindDevice()、addTag()等语义清晰的方法,让业务同学写推送逻辑时,像调用本地函数一样自然。
它适合谁?如果你的项目已经上线,用户量在10万+,运营团队天天催“今晚8点必须给VIP用户推优惠券”,而你还在手动curl测试个推接口;或者你的App涉及金融、医疗等强合规场景,必须确保每条推送的发送、送达、点击都有完整审计日志;又或者你正在重构老系统,希望把原来散落在Controller里的推送代码,收敛成可单元测试、可灰度发布、可独立部署的微服务模块——那这套方案就是为你量身定制的。它不承诺“一键接入零成本”,但能保证你花2小时读完README、1小时配好config、30分钟跑通test.php后,接下来半年不用再为推送掉链路半夜爬起来查日志。
2. 整体架构与设计思路:为什么这样分层?为什么选ThinkPHP而非Laravel?
2.1 分层设计背后的工程权衡
看到目录里GTClient.php、GTPushApi.php、GTUserApi.php这些文件名,你可能会疑惑:为什么不全塞进一个类里?毕竟个推SDK本身就是一个大而全的包。这里我要说句实在话——我最早确实这么干过,结果上线第三天就崩溃了。原因很简单:推送功能在业务系统里本质是“弱一致性”场景,但它的失败影响却极强(用户收不到关键通知=投诉)。当所有逻辑耦合在一个类里,一次token过期错误会连带阻塞设备绑定、标签更新、消息下发三个流程;一次网络超时会让整个订单完成回调卡死。所以这次我们强制分层,每一层只解决一个问题:
-
GTBaseApi 是地基,它不碰业务,只做三件事:统一管理HTTP客户端(基于TP的
think\facade\Http二次封装)、标准化签名生成(SHA256+Base64+时间戳拼接)、统一封装异常(把个推返回的{"result":"error","reason":"invalid_appid"}翻译成GetuiAppIdException)。它甚至不依赖个推SDK,纯自研,就是为了可控。 -
GTClient 是调度中心,它持有
GTBaseApi实例,并组合GTPushApi、GTUserApi等子模块。它的价值在于“协调”:比如调用pushToUser($uid, $msg)时,它先查GTUserApi->getDeviceList($uid)拿到设备列表,再交给GTPushApi->pushToList()批量下发,最后异步记录GTStatisticsApi->trackPushLog()。这个过程里,任何一步失败都会触发预设的降级策略(比如设备列表为空时自动fallback到别名推送),而不是让整个方法抛出未捕获异常。 -
GTPushApi 和 GTUserApi 是能力原子,它们只暴露“做什么”,不关心“怎么做”。
GTPushApi->pushToList()只接收设备ID数组和消息体,内部自动处理:判断是透传还是通知消息、组装V2标准的message和notification字段、选择单推/群推/别名推路由、设置离线保存时长(默认72小时)、添加自定义payload用于前端解析。而GTUserApi->bindDevice($uid, $cid, $platform)则专注解决UniApp最头疼的设备绑定问题——它会校验$cid是否符合个推规则(Android必须32位小写hex,iOS必须base64编码且长度≤256),自动清理该用户历史失效设备,并支持$platform参数区分ios/android/huawei(华为快应用需特殊处理)。
这种分层不是为了炫技,而是为了可维护性。当你某天需要替换个推为其他通道(比如信鸽或极光),只需重写GTPushApi的实现,GTClient和业务代码完全不动。这比“改一处,查十处”的单体类靠谱得多。
2.2 ThinkPHP框架选型的真实考量
有人会问:现在主流都用Laravel,为什么坚持ThinkPHP?答案很务实:存量系统迁移成本最低。我手头正在维护的6个UniApp项目,后端全是TP5.1/6.0,其中3个还跑在PHP7.2上。强行升级框架?光是中间件兼容、数据库查询构造器语法差异、队列驱动切换就够折腾两周。而TP的优势恰恰在这里:
-
配置驱动天然契合推送场景:个推的
app_id、app_key、master_secret这些敏感信息,TP的config/getui.php可以完美隔离开发/测试/生产环境。你甚至可以把master_secret存在环境变量里,config/getui.php里写env('GETUI_MASTER_SECRET'),比Laravel的.env加载机制更轻量。 -
Facade模式让调用极简:业务代码里写
GTClient::pushToUser(123, ['title'=>'订单提醒', 'content'=>'您有新订单']),背后自动完成依赖注入、实例化、方法调用。不需要use App\Services\Getui\GTClient;再new GTClient(),这对TP老项目尤其友好。 -
日志与异常体系开箱即用:TP的
think\facade\Log可以直接记录每次推送的原始请求、响应、耗时;think\exception\Handle能全局捕获GetuiNetworkException并自动告警。我们甚至在exception/GetuiException.php里加了getTraceInfo()方法,一键输出“哪行代码调用、哪个设备ID失败、个推返回什么错误码”,比看curl -v日志高效十倍。
当然,TP也有短板,比如HTTP客户端默认不支持连接池。所以我们用utils/HttpClientPool.php做了简易池化:预创建5个Curl句柄,每次请求从池里取,用完归还。实测在QPS 200+时,平均响应时间从320ms降到180ms。这个细节,官方SDK文档里可不会提。
3. 核心类详解与实操要点:GTClient如何统一调度?各API如何精准对接V2规范?
3.1 GTClient:不只是门面,更是状态协调器
GTClient表面看是个静态门面类,但它的真正价值藏在构造方法里。打开GTClient.php,你会看到:
public function __construct()
{
$this->baseApi = new GTBaseApi();
$this->pushApi = new GTPushApi($this->baseApi);
$this->userApi = new GTUserApi($this->baseApi);
$this->statsApi = new GTStatisticsApi($this->baseApi);
// 关键:自动初始化token缓存
$this->initAccessToken();
}
注意$this->initAccessToken()这行。个推V2要求所有接口必须带Authorization: Bearer <token>请求头,而token有效期只有2小时。如果每次请求都去刷新,会浪费大量资源;如果全靠内存缓存,集群部署时各节点token不同步。我们的解法是:用TP的缓存驱动(Redis优先,File兜底)存储token,并加10秒过期缓冲。具体逻辑:
- 首次调用时,检查缓存
getui:access_token是否存在且剩余时间>60秒; - 若不存在或过期,调用
GTBaseApi->requestToken()获取新token,存入缓存并设置7200秒过期(实际存7210秒,预留10秒缓冲); - 后续请求直接读缓存,无需网络IO。
这个设计让并发场景下token刷新次数降低90%。我们在压测时模拟1000QPS,token刷新接口调用量稳定在每2小时1次,而不是每秒都在刷。
另一个重点是pushToUser()方法的健壮性。它不是简单循环调用pushToList(),而是做了三层保护:
- 设备过滤:自动剔除长度非法(<10或>64)、含非法字符(空格、中文、特殊符号)的设备ID,避免个推返回
400 Bad Request; - 分批下发:个推V2单次最多推1000个设备,方法内部自动切片,每批950个(留50个余量防突发);
- 失败重试:对返回
429 Too Many Requests的批次,自动延迟1秒后重试,最多3次;对5xx错误则记录日志并跳过,不阻塞后续批次。
你可以这样调用:
$result = GTClient::pushToUser(888, [
'title' => '支付成功',
'content' => '订单#202405201234已支付,预计24小时内发货',
'payload' => ['type' => 'order_paid', 'order_id' => '202405201234'],
'offline' => true, // 是否保存离线消息
'duration' => 3600 * 72 // 离线保存72小时
]);
// $result 结构:['success'=>520, 'failed'=>3, 'details'=>['success_ids'=>[...], 'failed_ids'=>['cid123'=>'invalid_cid']]]
3.2 GTPushApi:V2消息体的精准组装艺术
个推V2的消息体结构堪称反人类——message、notification、transmission、custom_msg四层嵌套,稍有不慎就400。GTPushApi的核心价值,就是把业务同学从JSON结构里解放出来。看一个典型场景:给iOS用户推带角标的订单通知。
V2标准要求:
- message.push_type 必须是 notify
- notification.ios.badge 必须是整数(不能是字符串”1”)
- notification.ios.sound 必须是字符串(不能是true)
- transmission.content 必须是base64编码的JSON字符串
如果手写,光是这些类型校验就能写出一堆if。而我们的pushToList()方法只接收一个扁平数组:
$data = [
'title' => '订单提醒',
'content' => '您有1个待发货订单',
'badge' => 1, // 自动转int
'sound' => 'default', // 自动补.wav后缀
'payload' => ['order_id'=>'202405201234'], // 自动json_encode + base64_encode
'platform' => 'ios' // 自动填充ios专属字段
];
内部buildMessageBody()方法会根据platform自动组装:
- iOS:填充notification.ios.badge、notification.ios.sound、transmission.content
- Android:填充notification.android.intent、notification.android.builder_id
- 全平台:填充message.title、message.content、message.payload
更关键的是离线消息策略。个推V2的message.offline字段必须是布尔值,但PHP里json_encode(['offline'=>true])会输出"offline":true,而个推某些旧版网关会把它当字符串处理。我们的解法是在GTBaseApi->request()里加了一层json_encode($body, JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK),强制数字和布尔值不加引号。这个细节,救了我们三次线上事故。
3.3 GTUserApi:解决UniApp设备绑定的“脏数据”难题
UniApp的uni.getProvider()获取的设备ID,在不同平台差异极大:
- Android:uni.getSystemInfoSync().deviceId 返回的是个推CID(32位小写hex),但有些厂商ROM会返回空,此时需fallback到plus.device.uuid
- iOS:uni.getSystemInfoSync().deviceId 是UUID,但个推要求的是APNs Token(base64编码),必须通过原生插件获取
- H5:根本没设备ID,只能用浏览器指纹(localStorage+UA+screen)
GTUserApi->bindDevice()直面这些混乱。它接受$cid、$platform、$uid三个参数,并执行:
- 平台校验:
$platform必须是ios/android/huawei/oppo之一,否则抛GetuiPlatformException - CID清洗:
- Android:正则/^[a-f0-9]{32}$/i校验,自动转小写,去除前后空格
- iOS:base64_decode后检查长度是否在32-64字节之间,失败则抛GetuiIosTokenException - 绑定逻辑:
- 先查该$uid是否已绑定此$cid,避免重复
- 若$cid已绑定其他$uid,自动解绑(防止用户换账号登录后旧设备还能收消息)
- 记录绑定时间、IP、User-Agent到getui_user_bind_log表(需自行建表,SQL在README里)
我们甚至在utils/DeviceHelper.php里提供了getUniAppDeviceId()方法,前端调用uni.getSystemInfoSync()后,把原始数据传给后端,由后端智能识别平台并提取有效CID。这样前端同学完全不用关心平台差异,一行代码搞定。
4. 实操全流程与关键配置:从composer安装到生产环境部署
4.1 环境准备与依赖安装
第一步永远是最容易被忽略的。不要急着跑composer require,先确认你的环境满足硬性要求:
- PHP版本:7.2.5+(个推SDK最低要求,TP6.0要求7.1+,取交集)
- 扩展:
openssl(必须)、curl(必须)、mbstring(必须)、redis(推荐,用于token缓存) - Web服务器:Nginx需开启
client_max_body_size 10M(上传证书用),Apache需开启mod_rewrite
然后才是依赖安装。composer.json里已预置:
{
"require": {
"topthink/framework": "^6.0",
"getui-sdk/getui-sdk-php": "^2.0"
},
"autoload": {
"psr-4": {
"getui\\": "GeTui.php"
}
}
}
执行命令:
# 进入TP项目根目录
cd /path/to/your/thinkphp/project
# 安装个推SDK(注意:必须指定2.0+版本,V1已废弃)
composer require getui-sdk/getui-sdk-php:^2.0
# 复制本方案文件到项目
cp -r /path/to/unipush-package/* ./app/
# 生成配置文件(TP6.0+)
php think make:config getui
config/getui.php内容如下,这是你必须修改的唯一配置文件:
<?php
return [
// 个推开放平台申请的应用信息
'app_id' => env('GETUI_APP_ID', 'APPIDXXXXXXXXXXXXXX'),
'app_key' => env('GETUI_APP_KEY', 'APPKEYXXXXXXXXXXXXXX'),
'master_secret'=> env('GETUI_MASTER_SECRET', 'MASTERSECRETXXXXXXXXXXXXXX'),
// 推送通道配置
'gateway_url' => env('GETUI_GATEWAY_URL', 'https://restapi.getui.com/v2/'), // V2固定地址
// 缓存配置(强烈建议用Redis)
'cache_driver' => env('GETUI_CACHE_DRIVER', 'redis'), // file/redis
'cache_prefix' => env('GETUI_CACHE_PREFIX', 'getui:'),
// 日志配置
'log_level' => env('GETUI_LOG_LEVEL', 'debug'), // debug/info/warning/error
'log_file' => env('GETUI_LOG_FILE', 'getui_push.log'),
// 安全配置
'enable_https_verify' => env('GETUI_ENABLE_HTTPS_VERIFY', true), // 生产环境必须true
];
提示:
env()函数是TP6.0+内置的,你只需在.env里写:
GETUI_APP_ID=xxx GETUI_APP_KEY=xxx GETUI_MASTER_SECRET=xxx GETUI_CACHE_DRIVER=redis
4.2 测试用例跑通指南:test.php不只是验证,更是调试入口
test.php不是简单的“Hello World”,它是完整的端到端链路验证器。运行前确保:
- 已在个推开放平台创建应用,并获取app_id/app_key/master_secret
- 已在TP后台配置好数据库(用于GTStatisticsApi记录日志)
- 已在config/getui.php填入正确凭证
执行命令:
# 在TP项目根目录运行
php test.php
它会依次执行:
1. Token获取测试:调用GTBaseApi->requestToken(),输出token有效期
2. 设备绑定测试:用测试CID(如android_1234567890abcdef1234567890abcdef)绑定用户ID 999
3. 消息推送测试:向该设备ID发送一条测试消息,包含标题、内容、payload
4. 回执查询测试:调用GTStatisticsApi->getPushResult()查询刚发送消息的送达状态
每个步骤都会输出详细日志,例如:
[INFO] Token获取成功,有效期至:2024-05-20 15:30:22
[DEBUG] 绑定设备:uid=999, cid=android_123456..., platform=android -> success
[SUCCESS] 消息推送成功,任务ID:TASK_20240520152022_abc123
[INFO] 回执查询:status=delivered, count=1, failed=[]
如果某步失败,日志会明确指出原因。比如[ERROR] Token获取失败:cURL error 7: Failed to connect to restapi.getui.com port 443,说明网络不通;[ERROR] 设备绑定失败:Invalid CID format,说明CID格式错误。这是比看个推控制台更直接的调试方式。
4.3 生产环境部署 checklist:避开90%的线上故障
部署不是复制文件就完事。根据我们踩过的坑,整理出这份必做清单:
-
HTTPS证书必须有效:个推V2强制HTTPS,若用自签名证书,务必在
config/getui.php里设'enable_https_verify'=>false,但仅限测试环境!生产环境必须用Let’s Encrypt等权威证书。 -
Redis缓存必须高可用:token缓存若丢失,会导致瞬间大量请求刷新token,触发个推限流(429)。建议Redis主从+哨兵,或至少双节点。
-
数据库表必须提前创建:
GTStatisticsApi默认记录日志到getui_push_log表,SQL如下(TP6.0+):
sql CREATE TABLE `getui_push_log` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `task_id` varchar(64) NOT NULL DEFAULT '', `uid` bigint NOT NULL DEFAULT '0', `cid_list` text NOT NULL, `message_title` varchar(255) NOT NULL DEFAULT '', `message_content` varchar(500) NOT NULL DEFAULT '', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待发送,1已发送,2已送达,3已点击', `created_at` int NOT NULL DEFAULT '0', `updated_at` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_uid` (`uid`), KEY `idx_task_id` (`task_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -
定时任务必须配置:个推的“送达回执”和“点击回执”需主动轮询。我们在
command/PushCallback.php里写了定时命令:
bash # 每5分钟拉取一次回执 */5 * * * * php /path/to/tp6/think push:callback
它会调用GTStatisticsApi->pullDeliveryReport()和pullClickReport(),把结果存入数据库供运营查询。 -
前端集成必须校验:UniApp侧需确保:
- Android:在
manifest.json里配置个推appid、appkey、appsecret - iOS:在
dcloudio__uniplugins里启用个推插件,并配置APNs证书 - 调用
uni.addOnPushListener()监听消息,不要用plus.push(已过时)
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
pushToUser()返回空数组,无错误 | token缓存失效且网络不通 | php test.php --step=token | 检查config/getui.php中gateway_url是否可访问,ping restapi.getui.com |
| iOS设备收不到通知,但透传消息正常 | APNs证书配置错误或过期 | 登录个推控制台→应用管理→iOS证书状态 | 重新上传p12证书,确认密码正确,环境(开发/生产)匹配 |
| 同一设备多次绑定,标签混乱 | 前端未做CID去重,或bindDevice()未开启自动解绑 | 查getui_user_bind_log表,看同一CID对应多个uid | 在GTUserApi->bindDevice()里加$this->unbindOldDevice($cid)逻辑(已内置) |
| 离线消息不送达,控制台显示“已发送” | 个推离线保存时长设太短,或设备长期离线 | GTStatisticsApi->getPushResult($task_id)查offline_info字段 | 将duration参数设为3600*72(72小时),并确认设备网络畅通 |
getPushResult()返回{"result":"error","reason":"invalid taskid"} | task_id格式错误(含空格/中文)或已过期(>7天) | echo $task_id; 检查变量内容 | task_id必须是TASK_YYYYMMDDHHIISS_xxx格式,且7天内有效 |
5.2 独家避坑技巧
技巧1:用“影子设备”快速复现问题
个推控制台里有个隐藏功能:在“设备管理”页输入一个伪造的CID(如android_test_1234567890abcdef1234567890abcdef),它会返回该设备的在线状态、标签、应用版本。我们专门写了utils/ShadowDeviceTester.php,输入任意CID即可模拟设备行为,不用真机调试。
技巧2:消息体JSON结构可视化调试
GTPushApi里有个debugBuildBody()方法,传入消息数组,它会返回格式化后的完整V2 JSON:
$body = GTPushApi::debugBuildBody([
'title'=>'测试',
'content'=>'内容',
'platform'=>'ios'
]);
// 输出:{ "message": { "push_type": "notify", "title": "测试", ... } }
把这段JSON粘贴到个推API调试工具里,比猜参数快十倍。
技巧3:回执延迟的真相
个推官方说“送达回执5分钟内返回”,但实测在设备休眠时可能长达30分钟。我们的pullDeliveryReport()做了智能重试:首次拉取无结果时,等待30秒再拉,最多重试3次。同时在getui_push_log表里加了retry_count字段,方便统计哪些设备回执延迟高,针对性优化。
技巧4:标签同步的最终一致性保障
GTUserApi->addTag()只是把标签写入个推,但业务系统里用户标签可能变更。我们在command/SyncUserTags.php里写了每日凌晨2点的同步任务:扫描user_tags表,对比个推API返回的标签列表,自动增删。这样即使某次addTag()失败,第二天也能自动修复。
最后分享个小技巧:在config/getui.php里把'log_level'=>'debug',然后在TP日志目录里搜getui_push.log,你会发现每条推送的原始请求头、完整响应体、耗时、IP都清清楚楚。这比个推控制台的“模糊日志”有用得多——毕竟,真正的运维,永远在日志里找答案。
简介:一套即插即用的UniApp推送服务端方案,基于ThinkPHP框架开发,完整封装个推REST API V2标准接口。包含设备绑定、用户标签管理、即时/离线消息下发、送达与点击回执、数据统计等核心能力。核心类GTClient统一调度,GTPushApi负责消息推送,GTUserApi处理用户与设备关系,GTStatisticsApi支持效果追踪,GTBaseApi提供基础请求与异常封装。配套utils工具函数、request HTTP客户端、exception统一异常处理机制,以及test测试用例验证各接口可用性。composer.已预置个推SDK依赖,README详述接入步骤、参数说明与调用示例,LICENSE明确授权范围。适用于需要自主掌控推送链路、规避第三方平台限制的中大型UniApp项目,可快速集成到现有ThinkPHP系统中,无需改造前端逻辑,兼容UniPadk协议规范。
&spm=1001.2101.3001.5002&articleId=162256659&d=1&t=3&u=beaec3d5e45641b195bcff3b72855616)
2103

被折叠的 条评论
为什么被折叠?



