一、背景
之前公司在做了一个关于现场互动的产品,以微信小程序为用户端,电视盒子APP(安卓应用)为显示端,产品主打的内容为:用户在小程序端发弹幕和霸屏信息,然后服务端把用户发送的内容推送到安卓端(电视盒子APP)显示。
推送能力我们选择了第三方SDK,极光推送。在应用的过程中,有一个难点,因为是第三方推送能力,我们无法保证推送一定成功,尽管极光返回结果状态显示为推送成功,但在实际情况中也可能是推送失败,所以唯一成功的标识是电视盒子APP收到了推送,因此,针对这个问题,思考了很久,最终采取了一个相对有效的方案解决这个问题。
下面记录整个思路。
二、Laravel集成极光推送(参考:https://github.com/jpush/jpush-api-php-client)
1. 在项目根目录下,找到composer.json,添加jpush(极光推送包)依赖:
"require": {
"jpush/jpush": "*"
}
2. 打开dos系统(linux不用),跳转至项目根目录,执行composer命令安装:
composer install
三、完成极光推送基础代码
1. 创建极光推送类

2. 完成基础代码
<?php
namespace App\Jpush;
use JPush\Client as JPush;
use App\Models\DeviceJpush;
use App\Models\JpushAppConfig;
class JpushSend
{
/**
* 作用:根据安卓设备注册极光Id推送消息
* 参数:
* $appId —— 极光应用码(存在数据库中的极光应用配置对应的ID)
* $deviceId —— 设备码, 安卓设备能够通过设备的编码传参到极光注册一个唯一register_id,这个关系可以保存在数据库中
* $date —— 推送数据
* 返回:推送结果
*/
public function send($appId, $deviceId, $data)
{
$registerId = DeviceJpush::where(['device_id' => $deviceId, 'app_id' => $appId])->value('register_id');
$jpushConfig = JpushAppConfig::find($appId);
$client = new JPush($jpushConfig->app_key, $jpushConfig->master_secret, null);
return $client->push()
->setPlatform('android')
->addRegistrationId($registerId)
->message('jpush', array('extras' => $data))
->options(array('time_to_live' => 10))
->send();
}
}
3. 控制器中应用推送
<?php
namespace App\Http\Controllers\Home;
use App\Models\Device; // 引入设备数据库操作模型
use App\Jpush\JpushSend; // 引入我们刚才写好的极光推送基础类
use App\Models\DeviceJpush; // 引入保存设备注册极光推送的register_id数据库操作模型
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
Class TestJpush extends Controller
{
protected $jpush;
public function __construct()
{
$this->jpush = new JpushSend();
}
/*
* 保存安卓设备注册极光的register_id到数据库
* 参数:
* app_id :极光应用配置存在我们服务器数据库中的数据ID
* device_code:安卓设备码
* register_id:安卓设备调用极光接口注册的ID值(register_id),这个让java工程师自己去找接口解决
*/
public function deviceRegister(Request $request)
{
$data = $request->validate([
'app_id' => 'required',
'device_code' => 'required',
'register_id' => 'required'
]);
// 1. 根据设备码(device_code)获取设备ID(device_id)
$device_id = Device::where('device_code', $data['device_code'])->find(['id']);
// 2. 联合设备ID(device_id)和极光应用ID(app_id),保存设备在极光注册的register_id
DeviceJpush::insert([
'app_id' => $data['app_id'],
'device_id' => $device_id,
'register_id' => $data['register_id']
]);
return responseOk();
}
/*
* 极光推送数据
* 参数:
* app_id :极光应用配置保存在数据库中的ID值
* device_id:设备保存在数据库中的ID
* data :要推送的数据,数组
*/
public function push(Request $request)
{
$data = $request->validate([
'app_id' => 'required',
'device_id' => 'required',
'data' => 'required'
]);
$this->jpush->send($data['app_code'], $data['device_id'], $data['data']);
return responseOk();
}
}
3. 上面提到的数据库设计如下:
# 设备数据表(device)
id、device_code、device_name ……
# 极光应用数据表(jpush_app_config)
id、app_key、master_secret、package_name ……
# 设备极光注册信息(device_jpush),注意,register_id是一个字符串
id、device_id、app_id、register_id ……
四、解决推送不成功的方法
经过以上三个步骤,我们绝大多数推送应该都可以实现,但我们最前面也说到,推送实际的过程是在极光的服务器上完成,我们只是调用了极光的接口把数据传了过去而已,至于是否推送成功,我们不能完全保证,因此,需要做特殊情况处理,保证每一条数据都能推送成功,因此,以 “安卓端接收到推送” 为成功的唯一依据,采取以下措施:
1. 创建一张推送记录表:
# 推送记录表,包含推送的数据和推送状态,push_record
# 这里data数据保存推送数据数组转换成的json字符串
id, device_id, app_id, data, push_status
2. 执行推送时,采取事件操作(Laravel事件系统参考:https://laravel-china.org/docs/laravel/5.5/events/1318)
(1)注册推送事件:打开 /app/Providers/EventServiceProvider.php,添加事件:
protected $listen = [
'App\Events\JpushMsg' => [
'App\Listeners\SendJpushMsg',
],
];
(2)进入dos系统,跳转到项目根目录,执行命令生成事件文件:
php artisan event:generate
(3)完成Events:打开生成的事件文件,/app/Events/JpushMsg.php,添加代码:
<?php
namespace App\Events;
use App\Models\PushRecord;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class JpushMsg
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $pushRecord;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(PushRecord $pushRecord)
{
$this->pushRecord = $pushRecord;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
(3)完成Listeners:执行推送事件后,将推送记录保存在推送记录数据库中,且push_status推送状态为0,然后再将推送的数据延迟1分钟传入制作好的队列进行检查push_status是否改变为1,如果仍然为0,则为推送失败,重新推送;同时,在缓存中记录推送次数,队列中判断,如果超过10次仍然推送失败,则放弃重新推送当前的数据。
<?php
namespace App\Listeners;
use Cache;
use Carbon\Carbon;
use App\Events\JpushMsg;
use App\Jpush\JpushSend;
use App\Jobs\CheckPushMsg;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs;
class SendJpushMsg implements ShouldQueue // 这里继承ShouldQueue接口用于将该事件传入队列中执行
{
use DispatchesJobs;
public $jpushSend;
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
$this->jpushSend = new JpushSend();
}
/**
* Handle the event.
*
* @param JpushBarrage $event
* @return void
*/
public function handle(JpushMsg $event)
{
// 1. 获取推送数据
$pushRecord = $event->pushRecord;
// 2. 组装推送数据,将记录ID加入到数据中
$data = array_merge(['record_id'=>$pushRecord->id], json_decode($pushRecord->data, true));
// 3. 执行推送
$this->jpushSend->send($pushRecord->app_id, $pushRecord->device_id, $data);
// 4. 设置已推送的次数
$num = Cache::get('record'.$pushRecord->id);
$num = empty($num) ? 1 : ($num + 1);
Cache::put('record'.$pushRecord->id, $num, 60);
// 5. 执行延迟1分钟的队列操作,1分钟后在队列中判断当前数据是否已成功推送
CheckPushMsg::dispatch($pushRecord->id)->delay(Carbon::now()->addMinutes(1));
}
}
(4)创建队列(Laravel队列系统参考:https://laravel-china.org/docs/laravel/5.5/queues/1324)
- 当前测试采用database驱动,实际上线最好改成redis比较好。使用数据库驱动需要先创建两张表,一张用于存储待上传的队列数据(jobs),一张表用于存储队列操作失败的数据(failed_jobs):
# 创建jobs
php artisan queue:table
# 创建failed_jobs
php artisan queue:failed-table
# 执行创建数据表命令
php artisan migrate
- 生成检查推送的队列任务类
php artisan make:job CheckPushMsg
- 完成任务类代码操作
<?php
namespace App\Jobs;
use Cache;
use App\Events\JpushMsg;
use App\Models\PushRecord;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class CheckPushMsg implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $recordId;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($recordId)
{
$this->recordId = $recordId;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// 1. 根据id获取缓存的已推送次数
$num = Cache::get('record'.$this->recordId);
// 2. 判断缓存的推送次数,大于10,则不再继续执行推送
if($num > 10) return;
// 3. 获取已推送的信息记录
$pushRecord = PushRecord::find($this->recordId);
// 4. 判断推送状态,如果推送状态从0修改为1,则不再推送
if($pushRecord->push_status === 1) return;
// 5. 重新执行推送事件
event(new JpushMsg($pushRecord));
}
}
五、完善第四步措施的控制器代码完善
<?php
namespace App\Http\Controllers\Home;
use App\Models\Device; // 引入设备数据库操作模型
use App\Events\JpushMsg; // 引入极光推送消息事件类
use App\Models\PushRecord; // 引入推送记录数据库操作模型
use App\Models\DeviceJpush; // 引入保存设备注册极光推送的register_id数据库操作模型
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
Class TestJpush extends Controller
{
/*
* 保存安卓设备注册极光的register_id到数据库
* 参数:
* app_id :极光应用配置存在我们服务器数据库中的数据ID
* device_code:安卓设备码
* register_id:安卓设备调用极光接口注册的ID值(register_id),这个让java工程师自己去找接口解决
*/
public function deviceRegister(Request $request)
{
$data = $request->validate([
'app_id' => 'required',
'device_code' => 'required',
'register_id' => 'required'
]);
// 1. 根据设备码(device_code)获取设备ID(device_id)
$device_id = Device::where('device_code', $data['device_code'])->find(['id']);
// 2. 联合设备ID(device_id)和极光应用ID(app_id),保存设备在极光注册的register_id
DeviceJpush::insert([
'app_id' => $data['app_id'],
'device_id' => $device_id,
'register_id' => $data['register_id']
]);
return responseOk();
}
/*
* 调用事件完成极光推送
* 参数:
* app_id :极光应用配置保存在数据库中的ID值
* device_id:设备保存在数据库中的ID
* data :要推送的数据,json数组
*/
public function push(Request $request)
{
$data = $request->validate([
'app_id' => 'required',
'device_id' => 'required',
'data' => 'required'
]);
// 1. 保存推送记录
$pushRecord = PushRecord::insert([
'app_id' => $data['app_id'],
'device_id' => $data['device_id'],
'data' => $data['data']
]);
// 2. 调用推送事件
event(new JpushMsg($pushRecord));
return responseOk();
}
/*
* 设备接收到推送后调用接口改变推送记录状态
* 参数:record_id —— 推送记录ID
*/
public function updatePushStatus(Request $rquest)
{
$data = $request->validate([
'record_id' => 'required'
]);
PushRecord::where('id', $data['record_id'])->update(['push_status'=>1]);
return responseOk();
}
}
总结以上,思路主要是以下4个步骤:
- 保存每一条推送所需数据以及推送状态初始值 0 到数据库;
- 执行第一次推送后,增加一个延迟任务检查该条推送是否成功;
- 安卓端接收到推送则调用接口,更改当前推送记录状态为1;
- 延迟任务再检查当前推送是否推送成功,如果失败,则重新推送,如果成功,则结束。
需要注意:
- 失败的推送不能无限次循环推送,如果推送达到一定值的时候,就应该停止这个任务,否则失败任务如果产生堆积,一直执行,数据量增大后,会占用大量正常运行的带宽,不合理;
- 第一次执行后,到底延迟多久才检查推送是否成功?这个时间需要按照自己把握,我这里是给的1分钟延迟。
- 本篇文章中的代码是根据实际项目思考抽离出来的,没有在项目中跑过,所以如果直接复制到项目运行报错是有可能的,如果有误,欢迎留言告知,我及时更改,谢谢!
完!祝好运!
3934

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



