目前网站有两个用到实时消息推送的功能,源码:https://github.com/wuzhc/team
- 最新动态,实时显示用户的操作行为
- 消息推送,如重要消息通知,任务指派等等
考虑的问题
就可以实现对多个页面推送消息
实时动态
实时消息推送
客户端
// 初始化io对象
var socket = io('http://' + document.domain + ':2120');
// 当socket连接后发送登录请求
socket.on('connect', function () {
socket.emit('login', {
uid: "<?=Yii::$app->user->id?>",
companyID: "<?=Yii::$app->user->identity->fdCompanyID?>"
});
});
// 实时消息,当服务端推送来消息时触发
socket.on('new_msg', function (msg) {
if (msg.typeID == '<?= \common\config\Conf::MSG_HANDLE?>') {
var html = '<li>' +
'<a href="' + msg.url + '">' +
'<div class="pull-left">' +
'<img src="<?= $directoryAsset ?>/img/user2-160x160.jpg" class="img-circle" alt="User Image"/>' +
'</div>' +
'<h4 style="overflow:hidden;text-overflow:ellipsis;">' + msg.title + '</h4>' +
'<p style="overflow:hidden;text-overflow:ellipsis;">' + msg.content + '</p>' +
'</a>' +
'</li>';
$('#msg-handle').prepend(html);
var num = $('.msg-handle-num').eq(1).text() * 1;
$('.msg-handle-num').text(num + 1);
}
});
// 实时动态,当服务端推送来消息时触发
socket.on('update_dynamic', function (msg) {
var html = '<li>' +
'<i class="portrait">' +
'<img class="img-circle img-bordered-sm portrait-img" src="' + msg.portrait + '" alt="User Image"></i>' +
'<div class="timeline-item">' +
'<span class="time">' +
'<i class="fa fa-clock-o"></i>' + msg.date + '</span>' +
'<h3 class="timeline-header">' +
'<a href="' + msg.url + '">' + msg.operator + '</a>' +
'<span class="desc">' + msg.title + '</span>' +
'</h3>' +
'<div class="timeline-body">' + msg.content + '</div>' +
'</div>' +
'</li>';
$('#dynamic-list').prepend(html);
});
// 在线人数
socket.on('update_online_count', function (msg) {
$('#online-people').html(msg);
});
服务端
/**
* 消息推送
*/
public function actionStart()
{
// PHPSocketIO服务
$io = new SocketIO(2120);
// 客户端发起连接事件时,设置连接socket的各种事件回调
$io->on('connection', function ($socket) use ($io) {
/** @var Connection $redis */
$redis = Yii::$app->redis;
// 当客户端发来登录事件时触发
$socket->on('login', function ($data) use ($socket, $redis, $io) {
$uid = isset($data['uid']) ? $data['uid'] : null;
$companyID = isset($data['companyID']) ? $data['companyID'] : null;
// uid和companyID应该做下加密 by wuzhc 2018-01-28
if (empty($uid) || empty($companyID)) {
return ;
}
$this->companyID = $companyID;
// 已经登录过了
if (isset($socket->uid)) {
return;
}
$key = 'socketio:company:' . $companyID;
$field = 'uid:' . $uid;
// 用hash方便统计用户打开页面数量
if (!$redis->hget($key, $field)) {
$redis->hset($key, $field, 0);
}
// 同个用户打开新页面时加1
$redis->hincrby($key, $field, 1);
// 加入uid分组,方便对同个用户的所有打开页面推送消息
$socket->join('uid:'. $uid);
// 加入companyID,方便对整个公司的所有用户推送消息
$socket->join('company:'.$companyID);
$socket->uid = $uid;
$socket->companyID = $companyID;
// 整个公司在线人数更新
$io->to('company:'.$companyID)->emit('update_online_count', $redis->hlen($key));
});
// 当客户端断开连接是触发(一般是关闭网页或者跳转刷新导致)
$socket->on('disconnect', function () use ($socket, $redis, $io) {
if (!isset($socket->uid)) {
return;
}
$key = 'socketio:company:' . $socket->companyID;
$field = 'uid:' . $socket->uid;
$redis->hincrby($key, $field, -1);
if ($redis->hget($key, $field) <= 0) {
$redis->hdel($key, $field);
// 某某下线了,刷新整个公司的在线人数
$io->to('company:'.$socket->companyID)->emit('update_online_count', $redis->hlen($key));
}
});
});
// 开始进程时,监听2121端口,用户数据推送
$io->on('workerStart', function () use ($io) {
/** @var Connection $redis */
$redis = Yii::$app->redis;
$httpWorker = new Worker('http://0.0.0.0:2121');
// 当http客户端发来数据时触发
$httpWorker->onMessage = function ($conn, $data) use ($io, $redis) {
$_POST = $_POST ? $_POST : $_GET;
switch (@$_POST['action']) {
case 'message':
$to = 'uid:' . @$_POST['receiverID'];
$_POST['content'] = htmlspecialchars(@$_POST['content']);
$_POST['title'] = htmlspecialchars(@$_POST['title']);
// 有指定uid则向uid所在socket组发送数据
if ($to) {
$io->to($to)->emit('new_msg', $_POST);
}
$companyID = @$_POST['companyID'];
$key = 'socketio:company:' . $companyID;
$field = 'uid:' . $to;
// http接口返回,如果用户离线socket返回fail
if ($to && $redis->hget($key, $field)) {
return $conn->send('offline');
} else {
return $conn->send('ok');
}
break;
case 'dynamic':
$to = 'company:' . @$_POST['companyID'];
$_POST['content'] = htmlspecialchars(@$_POST['content']);
$_POST['title'] = htmlspecialchars(@$_POST['title']);
// 有指定uid则向uid所在socket组发送数据
if ($to) {
$io->to($to)->emit('update_dynamic', $_POST);
}
$companyID = @$_POST['companyID'];
$key = 'socketio:company:' . $companyID;
// http接口返回,如果用户离线socket返回fail
if ($to && $redis->hlen($key)) {
return $conn->send('offline');
} else {
return $conn->send('ok');
}
}
return $conn->send('fail');
};
// 执行监听
$httpWorker->listen();
});
// 运行所有的实例
global $argv;
array_shift($argv);
if (isset($argv[2]) && $argv[2] == 'daemon') {
$argv[2] = '-d';
}
Worker::runAll();
}
例子
/**
* 新建任务
* @param $projectID
* @param $categoryID
* @return string
* @throws ForbiddenHttpException
* @since 2018-01-26
*/
public function actionCreate($projectID, $categoryID)
{
$userID = Yii::$app->user->id;
if (!Yii::$app->user->can('createTask')) {
throw new ForbiddenHttpException(ResponseUtil::$msg[1]);
}
if ($data = Yii::$app->request->post()) {
if (empty($data['name'])) {
ResponseUtil::jsonCORS(['status' => Conf::FAILED, 'msg' => '任务标题不能为空']);
}
// 保存任务
$taskID = TaskService::factory()->save([
'name' => $data['name'],
'creatorID' => $userID,
'companyID' => $this->companyID,
'level' => $data['level'],
'categoryID' => $categoryID,
'projectID' => $projectID,
'content' => $data['content']
]);
if ($taskID) {
$url = Url::to(['task/view', 'taskID' => $taskID]);
$portrait = UserService::factory()->getUserPortrait($userID);
$username = UserService::factory()->getUserName($userID);
$title = '创建了新任务';
$content = $data['name'];
// 保存操作日志
LogService::factory()->saveHandleLog([
'objectID' => $taskID,
'companyID' => $this->companyID,
'operatorID' => $userID,
'objectType' => Conf::OBJECT_TASK,
'content' => $content,
'url' => $url,
'title' => $title,
'portrait' => $portrait,
'operator' => $username
]);
// 动态推送
MsgService::factory()->push('dynamic', [
'companyID' => $this->companyID,
'operatorID' => $userID,
'operator' => $username,
'portrait' => $portrait,
'title' => $title,
'content' => $content,
'url' => $url,
'date' => date('Y-m-d H:i:s'),
]);
ResponseUtil::jsonCORS(null, Conf::SUCCESS, '创建成功');
} else {
ResponseUtil::jsonCORS(null, Conf::FAILED, '创建失败');
}
} else {
return $this->render('create', [
'projectID' => $projectID,
'category' => TaskCategory::findOne(['id' => $categoryID]),
]);
}
}
调用保存操作日志底层接口
LogService::factory()->saveHandleLog($args)
参数说明
参数 |
说明 |
operatorID |
操作者ID,用于用户跳转链接 |
companyID |
公司ID,用于该公司下的动态 |
operator |
操作者姓名 |
portrait |
操作者头像 |
receiver |
接受者,可选 |
title |
动态标题,例如创建任务 |
content |
动态内容,例如创建任务的名称 |
date |
动态时间 |
url |
动态跳转链接 |
token |
验证 |
objectType |
对象类型,例如任务,文档等等 |
objectID |
对象ID,例如任务ID,文档ID等等 |
- companyID 用于查询该公司下的动态
- objectType和objectID组合可以查询某个对象的操作日志,例如一个任务被操作的日志
保存消息
参数 |
说明 |
senderID |
发送者ID,用于用户跳转链接 |
companyID |
公司ID,用于该公司下的动态 |
sender |
发送者姓名 |
portrait |
发送者头像 |
receiverID |
接受者ID |
title |
动态标题,例如创建任务 |
content |
动态内容,例如创建任务的名称 |
date |
动态时间 |
url |
动态跳转链接 |
typeID |
消息类型,1系统通知,2管理员通知,3操作通知 |
- receiverID和typeID和isRead组合可以查询用户未读消息
请求接口
MsgService::factory()->push($action, $args)
即时动态
参数 |
说明 |
operatorID |
操作者ID,用于用户跳转链接 |
companyID |
公司ID,用于给该公司的所有socket推送 |
operator |
操作者姓名 |
portrait |
操作者头像 |
receiver |
接受者,可选 |
title |
动态标题,例如创建任务 |
content |
动态内容,例如创建任务的名称 |
date |
动态时间 |
url |
动态跳转链接 |
即时消息
参数 |
说明 |
senderID |
发送者ID,用于用户跳转链接 |
sender |
发送者姓名 |
receiverID |
接受者ID,用于给接受者的所有socket推送消息 |
typeID |
消息类型,1系统通知,2管理员通知,3操作通知 |
portrait |
发送者头像 |
title |
消息标题 |
content |
消息内容 |
url |
消息跳转链接 |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。