1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-10 10:40:03 +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;
/**
* @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")
*/
@ -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();
@ -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->sendMessage($id);
$response = $service->sendMessage($id);
return $this->jsonSuccess();
return $this->jsonSuccess($response);
}
}

View File

@ -2,7 +2,7 @@
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 GatewayClient\Gateway;
@ -11,6 +11,25 @@ class Live extends Service
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)
{
$chapter = $this->checkChapterCache($id);
@ -23,14 +42,10 @@ class Live extends Service
$userCount = Gateway::getUidCountByGroup($groupName);
$guestCount = $clientCount - $userCount;
$userIds = Gateway::getUidListByGroup($groupName);
$users = $this->handleUsers($userIds);
return [
'client_count' => $clientCount,
'user_count' => $userCount,
'guest_count' => $guestCount,
'users' => $users,
];
}
@ -46,11 +61,23 @@ class Live extends Service
Gateway::$registerAddress = $this->getRegisterAddress();
if ($user->id > 0) {
Gateway::bindUid($clientId, $user->id);
}
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)
@ -59,52 +86,41 @@ class Live extends Service
$user = $this->getLoginUser();
$content = $this->request->getPost('content', ['trim', 'striptags']);
$content = kg_substr($content, 0, 150);
Gateway::$registerAddress = $this->getRegisterAddress();
$groupName = $this->getGroupName($chapter->id);
$excludeClientId = Gateway::getClientIdByUid($user->id);
$clientId = Gateway::getClientIdByUid($user->id);
$message = json_encode([
'type' => 'show_message',
$message = [
'type' => 'new_message',
'user' => [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar,
'vip' => $user->vip,
],
]);
'content' => $content,
];
Gateway::sendToGroup($groupName, $message, $excludeClientId);
$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;
}
protected function handleUsers($userIds)
protected function getGroupName($id)
{
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'],
];
}
return $result;
}
protected function getGroupName($groupId)
{
return "live_{$groupId}";
return "live_{$id}";
}
protected function getRegisterAddress()
@ -114,4 +130,19 @@ class Live extends Service
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="layui-card-header">在线成员</div>
<div class="layui-card-body live-stats">
<div class="stats">
用户:<span class="count">{{ stats.user_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 class="live-stats">
<div class="stats">
登录:<span class="count">{{ stats.user_count }} 人</span>
游客:<span class="count">{{ stats.guest_count }} 人</span>
</div>
</div>
</div>

View File

@ -2,13 +2,16 @@
{% 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 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">
<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>
</div>
@ -19,35 +22,35 @@
</div>
</div>
<div class="layout-sidebar">
<div class="layui-tab layui-tab-brief user-tab">
<ul class="layui-tab-title">
<li class="layui-this">讨论</li>
<li>成员</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="live-msg-list"></div>
<div class="live-msg-form">
<form class="layui-form" method="post" action="{{ url({'for':'web.live.message'}) }}">
<input class="layui-input" type="text" name="content" placeholder="请输入内容..." lay-verify="required">
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button>
</form>
<div class="chat-container">
<div class="layui-tab layui-tab-brief user-tab">
<ul class="layui-tab-title">
<li class="layui-this">讨论</li>
<li>统计</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="chat-msg-list" id="chat-msg-list" data-url="{{ live_chats_url }}"></div>
<div class="chat-msg-form">
<form class="layui-form" method="post" action="{{ send_msg_url }}">
<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>
</form>
</div>
</div>
<div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div>
</div>
<div class="layui-tab-item" id="tab-stats" data-url="{{ stats_url }}"></div>
</div>
</div>
</div>
</div>
<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.plan_id" value="{{ chapter.me.plan_id }}">
<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="bind_user_url" value='{{ bind_user_url }}'>
</div>
{% endblock %}

View File

@ -7,7 +7,8 @@
<div class="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>
</div>
@ -22,9 +23,6 @@
</div>
<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.plan_id" value="{{ chapter.me.plan_id }}">
<input type="hidden" name="chapter.learning_url" value="{{ learning_url }}">

View File

@ -23,7 +23,7 @@
<div class="search">
<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>
</div>

View File

@ -757,7 +757,60 @@
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 {
height: 420px;
color: #666;
}
@ -770,7 +823,8 @@
}
.live-user-card {
line-height: 30px;
margin-bottom: 10px;
line-height: 25px;
}
.chat-login-tips {

View File

@ -1,9 +1,11 @@
layui.use(['jquery', 'helper'], function () {
layui.use(['jquery', 'form', 'helper'], function () {
var $ = layui.jquery;
var form = layui.form;
var helper = layui.helper;
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');
socket.onopen = function () {
@ -25,8 +27,10 @@ layui.use(['jquery', 'helper'], function () {
socket.send('pong...');
} else if (data.type === 'bind_user') {
bindUser(data.client_id);
} else if (data.type === 'show_message') {
showMessage(data.content);
} else if (data.type === 'new_message') {
showNewMessage(data);
} else if (data.type === 'new_user') {
showLoginMessage(data);
}
};
@ -36,38 +40,64 @@ layui.use(['jquery', 'helper'], function () {
url: data.form.action,
data: data.field,
success: function (res) {
showMessage(res);
showNewMessage(res);
$chatContent.val('');
}
});
return false;
});
loadRecentChats();
refreshLiveStats();
setInterval('refreshLiveStats()', 60000);
setInterval(function () {
refreshLiveStats();
}, 300000);
function bindUser(clientId) {
$.ajax({
type: 'POST',
url: '/live/bind',
url: bindUserUrl,
data: {client_id: clientId}
});
}
function showMessage(res) {
function showNewMessage(res) {
var html = '<div class="chat">';
html += '<span class="user">' + res.user.name + '</span>';
if (res.user.vip === 1) {
html += '<span class="layui-badge">VIP</span>';
if (res.user.vip === 0) {
html += '<span class="vip-icon layui-icon layui-icon-diamond"></span>';
}
html += '<span class="user">' + res.user.name + ':</span>';
html += '<span class="content">' + res.content + '</span>';
html += '</div>';
$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() {
var $liveStats = $('#live-stats');
helper.ajaxLoadHtml($liveStats.data('url'), $liveStats.attr('id'));
var $tabStats = $('#tab-stats');
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 intervalTime = 5000;
var position = 0;
var userId = window.koogua.user.id;
var chapterId = $('input[name="chapter.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 playUrls = JSON.parse($('input[name="chapter.play_urls"]').val());
var requestId = helper.getRequestId();

View File

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