1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-12 19:39:10 +08:00

完善直播聊天

This commit is contained in:
xiaochong0302 2020-07-04 19:48:58 +08:00
parent 51e88b53c0
commit 20b8ef8132
11 changed files with 234 additions and 106 deletions

View File

@ -14,6 +14,20 @@ class LiveController extends Controller
use ResponseTrait; use ResponseTrait;
/**
* @Get("/{id:[0-9]+}/chats", name="web.live.chats")
*/
public function chatsAction($id)
{
$service = new LiveService();
$chats = $service->getRecentChats($id);
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('chapter/live_chats');
$this->view->setVar('chats', $chats);
}
/** /**
* @Get("/{id:[0-9]+}/stats", name="web.live.stats") * @Get("/{id:[0-9]+}/stats", name="web.live.stats")
*/ */
@ -29,9 +43,9 @@ class LiveController extends Controller
} }
/** /**
* @Post("/{id:[0-9]+}/bind", name="web.live.bind") * @Post("/{id:[0-9]+}/user/bind", name="web.live.bind_user")
*/ */
public function bindAction($id) public function bindUserAction($id)
{ {
$service = new LiveService(); $service = new LiveService();
@ -41,15 +55,15 @@ class LiveController extends Controller
} }
/** /**
* @Post("/{id:[0-9]+}/message", name="web.live.message") * @Post("/{id:[0-9]+}/msg/send", name="web.live.send_msg")
*/ */
public function messageAction($id) public function sendMessageAction($id)
{ {
$service = new LiveService(); $service = new LiveService();
$service->sendMessage($id); $response = $service->sendMessage($id);
return $this->jsonSuccess(); return $this->jsonSuccess($response);
} }
} }

View File

@ -2,7 +2,7 @@
namespace App\Http\Web\Services; namespace App\Http\Web\Services;
use App\Repos\User as UserRepo; use App\Library\Cache\Backend\Redis as RedisCache;
use App\Services\Frontend\ChapterTrait; use App\Services\Frontend\ChapterTrait;
use GatewayClient\Gateway; use GatewayClient\Gateway;
@ -11,6 +11,25 @@ class Live extends Service
use ChapterTrait; use ChapterTrait;
public function getRecentChats($id)
{
$redis = $this->getRedis();
$key = $this->getRedisListKey($id);
$items = $redis->lRange($key, 0, 10);
$result = [];
if ($items) {
foreach (array_reverse($items) as $item) {
$result[] = json_decode($item, true);
}
}
return $result;
}
public function getStats($id) public function getStats($id)
{ {
$chapter = $this->checkChapterCache($id); $chapter = $this->checkChapterCache($id);
@ -23,14 +42,10 @@ class Live extends Service
$userCount = Gateway::getUidCountByGroup($groupName); $userCount = Gateway::getUidCountByGroup($groupName);
$guestCount = $clientCount - $userCount; $guestCount = $clientCount - $userCount;
$userIds = Gateway::getUidListByGroup($groupName);
$users = $this->handleUsers($userIds);
return [ return [
'client_count' => $clientCount,
'user_count' => $userCount, 'user_count' => $userCount,
'guest_count' => $guestCount, 'guest_count' => $guestCount,
'users' => $users,
]; ];
} }
@ -46,11 +61,23 @@ class Live extends Service
Gateway::$registerAddress = $this->getRegisterAddress(); Gateway::$registerAddress = $this->getRegisterAddress();
if ($user->id > 0) {
Gateway::bindUid($clientId, $user->id);
}
Gateway::joinGroup($clientId, $groupName); Gateway::joinGroup($clientId, $groupName);
if ($user->id > 0) {
Gateway::bindUid($clientId, $user->id);
$message = kg_json_encode([
'type' => 'new_user',
'user' => [
'id' => $user->id,
'name' => $user->name,
'vip' => $user->vip,
],
]);
Gateway::sendToGroup($groupName, $message, $clientId);
}
} }
public function sendMessage($id) public function sendMessage($id)
@ -59,52 +86,41 @@ class Live extends Service
$user = $this->getLoginUser(); $user = $this->getLoginUser();
$content = $this->request->getPost('content', ['trim', 'striptags']);
$content = kg_substr($content, 0, 150);
Gateway::$registerAddress = $this->getRegisterAddress(); Gateway::$registerAddress = $this->getRegisterAddress();
$groupName = $this->getGroupName($chapter->id); $groupName = $this->getGroupName($chapter->id);
$excludeClientId = Gateway::getClientIdByUid($user->id); $clientId = Gateway::getClientIdByUid($user->id);
$message = json_encode([ $message = [
'type' => 'show_message', 'type' => 'new_message',
'user' => [ 'user' => [
'id' => $user->id, 'id' => $user->id,
'name' => $user->name, 'name' => $user->name,
'avatar' => $user->avatar, 'vip' => $user->vip,
], ],
]); 'content' => $content,
Gateway::sendToGroup($groupName, $message, $excludeClientId);
}
protected function handleUsers($userIds)
{
if (!$userIds) return [];
$userRepo = new UserRepo();
$users = $userRepo->findByIds($userIds);
$baseUrl = kg_ci_base_url();
$result = [];
foreach ($users->toArray() as $key => $user) {
$user['avatar'] = $baseUrl . $user['avatar'];
$result[] = [
'id' => $user['id'],
'name' => $user['name'],
'vip' => $user['vip'],
'avatar' => $user['avatar'],
]; ];
$encodeMessage = kg_json_encode($message);
Gateway::sendToGroup($groupName, $encodeMessage, $clientId);
$redis = $this->getRedis();
$key = $this->getRedisListKey($id);
$redis->lPush($key, $encodeMessage);
$redis->lTrim($key, 0, 10);
return $message;
} }
return $result; protected function getGroupName($id)
}
protected function getGroupName($groupId)
{ {
return "live_{$groupId}"; return "live_{$id}";
} }
protected function getRegisterAddress() protected function getRegisterAddress()
@ -114,4 +130,19 @@ class Live extends Service
return $config->websocket->register_address; return $config->websocket->register_address;
} }
protected function getRedisListKey($id)
{
return "live_recent_chat:{$id}";
}
protected function getRedis()
{
/**
* @var RedisCache $cache
*/
$cache = $this->getDI()->get('cache');
return $cache->getRedis();
}
} }

View File

@ -0,0 +1,9 @@
{% for chat in chats %}
<div class="chat">
{% if chat.user.vip == 0 %}
<span class="vip-icon layui-icon layui-icon-diamond"></span>
{% endif %}
<span class="user">{{ chat.user.name }}</span>
<span class="content">{{ chat.content }}</span>
</div>
{% endfor %}

View File

@ -1,17 +1,6 @@
<div class="layui-card"> <div class="live-stats">
<div class="layui-card-header">在线成员</div>
<div class="layui-card-body live-stats">
<div class="stats"> <div class="stats">
用户:<span class="count">{{ stats.user_count }}</span> 登录:<span class="count">{{ stats.user_count }} 人</span>
游客:<span class="count">{{ stats.guest_count }}</span> 游客:<span class="count">{{ stats.guest_count }} 人</span>
</div>
<div class="live-user-list">
{% for user in stats.users %}
{% set vip_flag = user.vip ? '<span class="layui-badge">vip</span>' : '' %}
<div class="live-user-card">
<div class="name">{{ user.name }} {{ vip_flag }}</div>
</div>
{% endfor %}
</div>
</div> </div>
</div> </div>

View File

@ -2,13 +2,16 @@
{% block content %} {% block content %}
{% set course_url = url({'for':'web.course.show','id':chapter.course.id}) %}
{% set learning_url = url({'for':'web.chapter.learning','id':chapter.id}) %} {% set learning_url = url({'for':'web.chapter.learning','id':chapter.id}) %}
{% set stats_url = url({'for':'web.live.stats','id':chapter.id}) %} {% set live_chats_url = url({'for':'web.live.chats','id':chapter.id}) %}
{% set live_stats_url = url({'for':'web.live.stats','id':chapter.id}) %}
{% set send_msg_url = url({'for':'web.live.send_msg','id':chapter.id}) %}
{% set bind_user_url = url({'for':'web.live.bind_user','id':chapter.id}) %}
<div class="breadcrumb"> <div class="breadcrumb">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
<span><i class="layui-icon layui-icon-return"></i> <a href="{{ course_url }}">返回课程主页</a></span> <a><cite>{{ chapter.course.title }}</cite></a>
<a><cite>{{ chapter.title }}</cite></a>
</span> </span>
</div> </div>
@ -19,35 +22,35 @@
</div> </div>
</div> </div>
<div class="layout-sidebar"> <div class="layout-sidebar">
<div class="chat-container">
<div class="layui-tab layui-tab-brief user-tab"> <div class="layui-tab layui-tab-brief user-tab">
<ul class="layui-tab-title"> <ul class="layui-tab-title">
<li class="layui-this">讨论</li> <li class="layui-this">讨论</li>
<li>成员</li> <li>统计</li>
</ul> </ul>
<div class="layui-tab-content"> <div class="layui-tab-content">
<div class="layui-tab-item layui-show"> <div class="layui-tab-item layui-show">
<div class="live-msg-list"></div> <div class="chat-msg-list" id="chat-msg-list" data-url="{{ live_chats_url }}"></div>
<div class="live-msg-form"> <div class="chat-msg-form">
<form class="layui-form" method="post" action="{{ url({'for':'web.live.message'}) }}"> <form class="layui-form" method="post" action="{{ send_msg_url }}">
<input class="layui-input" type="text" name="content" placeholder="请输入内容..." lay-verify="required"> <input class="layui-input" type="text" name="content" maxlength="150" placeholder="快来和大家一起互动吧~" lay-verType="tips" lay-verify="required">
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button> <button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button>
</form> </form>
</div> </div>
</div> </div>
<div class="layui-tab-item" id="tab-stats" data-url="{{ stats_url }}"></div> <div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="layui-hide"> <div class="layui-hide">
<input type="hidden" name="user.id" value="{{ auth_user.id }}">
<input type="hidden" name="user.name" value="{{ auth_user.name }}">
<input type="hidden" name="user.avatar" value="{{ auth_user.avatar }}">
<input type="hidden" name="chapter.id" value="{{ chapter.id }}"> <input type="hidden" name="chapter.id" value="{{ chapter.id }}">
<input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}"> <input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}">
<input type="hidden" name="chapter.learning_url" value="{{ learning_url }}"> <input type="hidden" name="chapter.learning_url" value="{{ learning_url }}">
<input type="hidden" name="chapter.play_urls" value='{{ chapter.play_urls|json_encode }}'> <input type="hidden" name="chapter.play_urls" value='{{ chapter.play_urls|json_encode }}'>
<input type="hidden" name="bind_user_url" value='{{ bind_user_url }}'>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,7 +7,8 @@
<div class="breadcrumb"> <div class="breadcrumb">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
<span><i class="layui-icon layui-icon-return"></i> <a href="{{ course_url }}">返回课程主页</a></span> <a><cite>{{ chapter.course.title }}</cite></a>
<a><cite>{{ chapter.title }}</cite></a>
</span> </span>
</div> </div>
@ -22,9 +23,6 @@
</div> </div>
<div class="layui-hide"> <div class="layui-hide">
<input type="hidden" name="user.id" value="{{ auth_user.id }}">
<input type="hidden" name="user.name" value="{{ auth_user.name }}">
<input type="hidden" name="user.avatar" value="{{ auth_user.avatar }}">
<input type="hidden" name="chapter.id" value="{{ chapter.id }}"> <input type="hidden" name="chapter.id" value="{{ chapter.id }}">
<input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}"> <input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}">
<input type="hidden" name="chapter.learning_url" value="{{ learning_url }}"> <input type="hidden" name="chapter.learning_url" value="{{ learning_url }}">

View File

@ -23,7 +23,7 @@
<div class="search"> <div class="search">
<form class="layui-form" action="{{ url({'for':'web.search.list'}) }}"> <form class="layui-form" action="{{ url({'for':'web.search.list'}) }}">
<input class="layui-input" type="text" name="query" value="{{ request.get('query')|striptags }}" autocomplete="off" placeholder="请输入课程关键字..."> <input class="layui-input" type="text" name="query" maxlength="30" autocomplete="off" placeholder="请输入课程关键字..." value="{{ request.get('query')|striptags }}">
</form> </form>
</div> </div>

View File

@ -757,7 +757,60 @@
height: 428px; height: 428px;
} }
.chat-container {
padding: 10px 20px 20px 20px;
background-color: white;
}
.vip-icon {
color: orange;
}
.chat-msg-list {
height: 380px;
margin-bottom: 10px;
overflow-y: auto;
}
.chat-msg-list::-webkit-scrollbar {
width: 5px;
}
.chat-msg-list::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0, 0, 0, 0.1);
}
.chat-msg-list .chat, .chat-msg-list .chat-sys {
margin-bottom: 10px;
line-height: 25px;
word-break: break-all;
}
.chat-sys {
color: #999;
}
.chat-sys span, .chat span {
margin-right: 5px;
}
.chat .user {
color: orange;
}
.chat .content {
color: #666;
}
.chat-msg-form .layui-input {
height: 32px;
font-size: 12px;
color: #666;
}
.live-stats { .live-stats {
height: 420px;
color: #666; color: #666;
} }
@ -770,7 +823,8 @@
} }
.live-user-card { .live-user-card {
line-height: 30px; margin-bottom: 10px;
line-height: 25px;
} }
.chat-login-tips { .chat-login-tips {

View File

@ -1,9 +1,11 @@
layui.use(['jquery', 'helper'], function () { layui.use(['jquery', 'form', 'helper'], function () {
var $ = layui.jquery; var $ = layui.jquery;
var form = layui.form;
var helper = layui.helper; var helper = layui.helper;
var socket = new WebSocket(window.koogua.socketUrl); var socket = new WebSocket(window.koogua.socketUrl);
var bindUserUrl = $('input[name="bind_user_url"]').val();
var $chatContent = $('input[name=content]');
var $chatMsgList = $('#chat-msg-list'); var $chatMsgList = $('#chat-msg-list');
socket.onopen = function () { socket.onopen = function () {
@ -25,8 +27,10 @@ layui.use(['jquery', 'helper'], function () {
socket.send('pong...'); socket.send('pong...');
} else if (data.type === 'bind_user') { } else if (data.type === 'bind_user') {
bindUser(data.client_id); bindUser(data.client_id);
} else if (data.type === 'show_message') { } else if (data.type === 'new_message') {
showMessage(data.content); showNewMessage(data);
} else if (data.type === 'new_user') {
showLoginMessage(data);
} }
}; };
@ -36,38 +40,64 @@ layui.use(['jquery', 'helper'], function () {
url: data.form.action, url: data.form.action,
data: data.field, data: data.field,
success: function (res) { success: function (res) {
showMessage(res); showNewMessage(res);
$chatContent.val('');
} }
}); });
return false; return false;
}); });
loadRecentChats();
refreshLiveStats(); refreshLiveStats();
setInterval('refreshLiveStats()', 60000); setInterval(function () {
refreshLiveStats();
}, 300000);
function bindUser(clientId) { function bindUser(clientId) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/live/bind', url: bindUserUrl,
data: {client_id: clientId} data: {client_id: clientId}
}); });
} }
function showMessage(res) { function showNewMessage(res) {
var html = '<div class="chat">'; var html = '<div class="chat">';
html += '<span class="user">' + res.user.name + '</span>'; if (res.user.vip === 0) {
if (res.user.vip === 1) { html += '<span class="vip-icon layui-icon layui-icon-diamond"></span>';
html += '<span class="layui-badge">VIP</span>';
} }
html += '<span class="user">' + res.user.name + ':</span>';
html += '<span class="content">' + res.content + '</span>'; html += '<span class="content">' + res.content + '</span>';
html += '</div>'; html += '</div>';
$chatMsgList.append(html); $chatMsgList.append(html);
scrollToBottom();
}
function showLoginMessage(res) {
var html = '<div class="chat chat-sys">';
html += '<span>' + res.user.name + '</span>';
html += '<span>进入了直播间</span>';
html += '</div>';
$chatMsgList.append(html);
scrollToBottom();
}
function scrollToBottom() {
var $scrollTo = $chatMsgList.find('.chat:last');
$chatMsgList.scrollTop(
$scrollTo.offset().top - $chatMsgList.offset().top + $chatMsgList.scrollTop()
);
} }
function refreshLiveStats() { function refreshLiveStats() {
var $liveStats = $('#live-stats'); var $tabStats = $('#tab-stats');
helper.ajaxLoadHtml($liveStats.data('url'), $liveStats.attr('id')); helper.ajaxLoadHtml($tabStats.data('url'), $tabStats.attr('id'));
}
function loadRecentChats() {
helper.ajaxLoadHtml($chatMsgList.data('url'), $chatMsgList.attr('id'));
} }
}); });

View File

@ -6,9 +6,9 @@ layui.use(['jquery', 'helper'], function () {
var interval = null; var interval = null;
var intervalTime = 5000; var intervalTime = 5000;
var position = 0; var position = 0;
var userId = window.koogua.user.id;
var chapterId = $('input[name="chapter.id"]').val(); var chapterId = $('input[name="chapter.id"]').val();
var planId = $('input[name="chapter.plan_id"]').val(); var planId = $('input[name="chapter.plan_id"]').val();
var userId = $('input[name="user.id"]').val();
var learningUrl = $('input[name="chapter.learning_url"]').val(); var learningUrl = $('input[name="chapter.learning_url"]').val();
var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val()); var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val());
var requestId = helper.getRequestId(); var requestId = helper.getRequestId();

View File

@ -6,9 +6,9 @@ layui.use(['jquery', 'helper'], function () {
var interval = null; var interval = null;
var intervalTime = 5000; var intervalTime = 5000;
var position = 0; var position = 0;
var userId = window.koogua.user.id;
var chapterId = $('input[name="chapter.id"]').val(); var chapterId = $('input[name="chapter.id"]').val();
var planId = $('input[name="chapter.plan_id"]').val(); var planId = $('input[name="chapter.plan_id"]').val();
var userId = $('input[name="user.id"]').val();
var learningUrl = $('input[name="chapter.learning_url"]').val(); var learningUrl = $('input[name="chapter.learning_url"]').val();
var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val()); var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val());
var requestId = helper.getRequestId(); var requestId = helper.getRequestId();