1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-28 13:21:37 +08:00

完善直播聊天

This commit is contained in:
xiaochong0302 2020-07-05 19:23:41 +08:00
parent 20b8ef8132
commit 401ec0793a
17 changed files with 326 additions and 119 deletions

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Web\Controllers;
use App\Services\Pay\Alipay as AlipayService;
use App\Traits\Response as ResponseTrait;
class AlipayController extends \Phalcon\Mvc\Controller
{
use ResponseTrait;
/**
* @Post("/alipay/notify", name="web.alipay.notify")
*/
public function notifyAction()
{
$alipayService = new AlipayService();
$response = $alipayService->notify();
if (!$response) exit;
$response->send();
exit;
}
}

View File

@ -14,6 +14,20 @@ class LiveController extends Controller
use ResponseTrait; use ResponseTrait;
/**
* @Get("/{id:[0-9]+}/preview", name="web.live.preview")
*/
public function previewAction($id)
{
$service = new LiveService();
$stats = $service->getStats($id);
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('chapter/live_stats');
$this->view->setVar('stats', $stats);
}
/** /**
* @Get("/{id:[0-9]+}/chats", name="web.live.chats") * @Get("/{id:[0-9]+}/chats", name="web.live.chats")
*/ */

View File

@ -4,6 +4,8 @@ namespace App\Http\Web\Controllers;
use App\Library\CsrfToken as CsrfTokenService; use App\Library\CsrfToken as CsrfTokenService;
use App\Models\ContentImage as ContentImageModel; use App\Models\ContentImage as ContentImageModel;
use App\Services\Pay\Alipay as AlipayService;
use App\Services\Pay\Wxpay as WxpayService;
use App\Services\Storage as StorageService; use App\Services\Storage as StorageService;
use App\Traits\Response as ResponseTrait; use App\Traits\Response as ResponseTrait;
use App\Traits\Security as SecurityTrait; use App\Traits\Security as SecurityTrait;
@ -67,4 +69,44 @@ class PublicController extends \Phalcon\Mvc\Controller
return $this->jsonSuccess(['token' => $token]); return $this->jsonSuccess(['token' => $token]);
} }
/**
* @Post("/alipay/notify", name="web.alipay_notify")
*/
public function alipayNotifyAction()
{
$alipayService = new AlipayService();
$response = $alipayService->notify();
if (!$response) exit;
$response->send();
exit;
}
/**
* @Post("/wxpay/notify", name="web.wxpay_notify")
*/
public function wxpayNotifyAction()
{
$wxpayService = new WxpayService();
$response = $wxpayService->notify();
if (!$response) exit;
$response->send();
exit;
}
/**
* @Post("/live/notify", name="web.live_notify")
*/
public function liveNotifyAction()
{
}
} }

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Web\Controllers;
use App\Services\Pay\Wxpay as WxpayService;
use App\Traits\Response as ResponseTrait;
class WxpayController extends \Phalcon\Mvc\Controller
{
use ResponseTrait;
/**
* @Post("/wxpay/notify", name="web.wxpay.notify")
*/
public function notifyAction()
{
$wxpayService = new WxpayService();
$response = $wxpayService->notify();
if (!$response) exit;
$response->send();
exit;
}
}

View File

@ -17,7 +17,9 @@ class Live extends Service
$key = $this->getRedisListKey($id); $key = $this->getRedisListKey($id);
$items = $redis->lRange($key, 0, 10); $redis->expire($key, 3 * 3600);
$items = $redis->lRange($key, 0, 15);
$result = []; $result = [];
@ -88,7 +90,7 @@ class Live extends Service
$content = $this->request->getPost('content', ['trim', 'striptags']); $content = $this->request->getPost('content', ['trim', 'striptags']);
$content = kg_substr($content, 0, 150); $content = kg_substr($content, 0, 80);
Gateway::$registerAddress = $this->getRegisterAddress(); Gateway::$registerAddress = $this->getRegisterAddress();
@ -111,9 +113,14 @@ class Live extends Service
Gateway::sendToGroup($groupName, $encodeMessage, $clientId); Gateway::sendToGroup($groupName, $encodeMessage, $clientId);
$redis = $this->getRedis(); $redis = $this->getRedis();
$key = $this->getRedisListKey($id); $key = $this->getRedisListKey($id);
$redis->lPush($key, $encodeMessage); $redis->lPush($key, $encodeMessage);
$redis->lTrim($key, 0, 10);
if ($redis->lLen($key) % 20 == 0) {
$redis->lTrim($key, 0, 15);
}
return $message; return $message;
} }

View File

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

View File

@ -31,12 +31,16 @@
<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="chat-msg-list" id="chat-msg-list" data-url="{{ live_chats_url }}"></div> <div class="chat-msg-list" id="chat-msg-list" data-url="{{ live_chats_url }}"></div>
<div class="chat-msg-form"> {% if auth_user.id > 0 %}
<form class="layui-form" method="post" action="{{ send_msg_url }}"> <div class="chat-msg-form">
<input class="layui-input" type="text" name="content" maxlength="150" placeholder="快来和大家一起互动吧~" lay-verType="tips" lay-verify="required"> <form class="layui-form" method="post" action="{{ send_msg_url }}">
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button> <input class="layui-input" type="text" name="content" maxlength="80" placeholder="快来和大家一起互动吧~" lay-verType="tips" lay-verify="required">
</form> <button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button>
</div> </form>
</div>
{% else %}
<div class="chat-login-tips">登录后才可以发言哦~</div>
{% endif %}
</div> </div>
<div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div> <div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div>
</div> </div>

View File

@ -68,19 +68,8 @@
{% endblock %} {% endblock %}
{% block inline_js %} {% block include_js %}
<script> {{ js_include('web/js/index.js') }}
layui.use(['carousel', 'flow'], function () {
var carousel = layui.carousel;
var flow = layui.flow;
carousel.render({
elem: '#carousel',
width: '100%',
height: '270px'
});
flow.lazyimg();
});
</script>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,7 @@
<html lang="zh-CN-Hans"> <html lang="zh-CN-Hans">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="keywords" content="{{ site_seo.getKeywords() }}"> <meta name="keywords" content="{{ site_seo.getKeywords() }}">
<meta name="description" content="{{ site_seo.getDescription() }}"> <meta name="description" content="{{ site_seo.getDescription() }}">

View File

@ -2,6 +2,7 @@
<html lang="zh-CN-Hans"> <html lang="zh-CN-Hans">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="csrf-token" content="{{ csrfToken.getToken() }}"> <meta name="csrf-token" content="{{ csrfToken.getToken() }}">
{{ icon_link('favicon.ico') }} {{ icon_link('favicon.ico') }}

View File

@ -34,15 +34,21 @@ class CsrfToken
public function checkToken($token) public function checkToken($token)
{ {
if (!$token) return false;
$text = $this->crypt->decryptBase64($token); $text = $this->crypt->decryptBase64($token);
list($time, $fixed, $random) = explode($this->delimiter, $text); $params = explode($this->delimiter, $text);
if ($time != intval($time) || $fixed != $this->fixed || strlen($random) != 8) { if (!isset($params[0]) || !isset($params[1]) || !isset($params[2])) {
return false; return false;
} }
if (time() - $time > $this->lifetime) { if ($params[0] != intval($params[0]) || $params[1] != $this->fixed || strlen($params[2]) != 8) {
return false;
}
if (time() - $params[0] > $this->lifetime) {
return false; return false;
} }

View File

@ -130,11 +130,6 @@ class ChapterInfo extends FrontendService
$playUrls = $service->getPlayUrls($chapter->id); $playUrls = $service->getPlayUrls($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [ return [
'id' => $chapter->id, 'id' => $chapter->id,
'title' => $chapter->title, 'title' => $chapter->title,
@ -150,22 +145,36 @@ class ChapterInfo extends FrontendService
protected function formatChapterLive(ChapterModel $chapter) protected function formatChapterLive(ChapterModel $chapter)
{ {
$liveService = new LiveService(); $service = new LiveService();
$playUrls = $liveService->getPullUrls("chapter_{$chapter->id}"); $streamName = $this->getLiveStreamName($chapter->id);
/** $chapterRepo = new ChapterRepo();
* @var array $attrs
*/ $live = $chapterRepo->findChapterLive($chapter->id);
$attrs = $chapter->attrs;
$playUrls = [];
if ($live->start_time - time() > 1800) {
$status = 'pending';
} elseif (time() - $live->end_time > 1800) {
$status = 'finished';
} else {
$status = $service->getStreamState($streamName);
}
if ($status == 'active') {
$playUrls = $service->getPullUrls($streamName);
}
return [ return [
'id' => $chapter->id, 'id' => $chapter->id,
'title' => $chapter->title, 'title' => $chapter->title,
'summary' => $chapter->summary, 'summary' => $chapter->summary,
'model' => $chapter->model, 'model' => $chapter->model,
'start_time' => $attrs['start_time'], 'status' => $status,
'end_time' => $attrs['end_time'], 'start_time' => $live->start_time,
'end_time' => $live->end_time,
'play_urls' => $playUrls, 'play_urls' => $playUrls,
'user_count' => $chapter->user_count, 'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count, 'agree_count' => $chapter->agree_count,
@ -180,11 +189,6 @@ class ChapterInfo extends FrontendService
$read = $chapterRepo->findChapterRead($chapter->id); $read = $chapterRepo->findChapterRead($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [ return [
'id' => $chapter->id, 'id' => $chapter->id,
'title' => $chapter->title, 'title' => $chapter->title,
@ -259,4 +263,9 @@ class ChapterInfo extends FrontendService
$this->eventsManager->fire('chapterCounter:incrUserCount', $this, $chapter); $this->eventsManager->fire('chapterCounter:incrUserCount', $this, $chapter);
} }
protected function getLiveStreamName($id)
{
return "chapter_{$id}";
}
} }

View File

@ -2,32 +2,191 @@
namespace App\Services; namespace App\Services;
use Phalcon\Logger\Adapter\File as FileLogger;
use TencentCloud\Common\Credential;
use TencentCloud\Common\Exception\TencentCloudSDKException;
use TencentCloud\Common\Profile\ClientProfile;
use TencentCloud\Common\Profile\HttpProfile;
use TencentCloud\Live\V20180801\LiveClient;
use TencentCloud\Live\V20180801\Models\DescribeLiveStreamStateRequest;
use TencentCloud\Live\V20180801\Models\ForbidLiveStreamRequest;
use TencentCloud\Live\V20180801\Models\ResumeLiveStreamRequest;
class Live extends Service class Live extends Service
{ {
const END_POINT = 'live.tencentcloudapi.com';
/** /**
* @var array * @var array
*/ */
protected $settings; protected $settings;
/**
* @var LiveClient
*/
protected $client;
/**
* @var FileLogger
*/
protected $logger;
public function __construct() public function __construct()
{ {
$this->settings = $this->getSectionSettings('live'); $this->settings = $this->getSectionSettings('live');
$this->logger = $this->getLogger('live');
$this->client = $this->getLiveClient();
}
/**
* 获取流的状态
*
* @param string $streamName
* @param string $appName
* @return string|bool
*/
public function getStreamState($streamName, $appName = 'live')
{
try {
$request = new DescribeLiveStreamStateRequest();
$params = json_encode([
'DomainName' => $this->settings['push_domain'],
'AppName' => $appName ?: 'live',
'StreamName' => $streamName,
]);
$request->fromJsonString($params);
$this->logger->debug('Describe Live Stream State Request ' . $params);
$response = $this->client->DescribeLiveStreamState($request);
$this->logger->debug('Describe Live Stream State Response ' . $response->toJsonString());
$result = $response->StreamState;
} catch (TencentCloudSDKException $e) {
$this->logger->error('Describe Live Stream State Exception ' . kg_json_encode([
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
'requestId' => $e->getRequestId(),
]));
$result = false;
}
return $result;
}
/**
* 禁推直播推流
*
* @param string $streamName
* @param string $appName
* @param string $reason
* @return array|bool
*/
public function forbidStream($streamName, $appName = 'live', $reason = '')
{
try {
$request = new ForbidLiveStreamRequest();
$params = json_encode([
'DomainName' => $this->settings['push_domain'],
'AppName' => $appName ?: 'live',
'StreamName' => $streamName,
'Reason' => $reason,
]);
$request->fromJsonString($params);
$this->logger->debug('Forbid Live Stream Request ' . $params);
$response = $this->client->ForbidLiveStream($request);
$this->logger->debug('Forbid Live Stream Response ' . $response->toJsonString());
$result = json_decode($response->toJsonString(), true);
} catch (TencentCloudSDKException $e) {
$this->logger->error('Forbid Live Stream Exception ' . kg_json_encode([
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
'requestId' => $e->getRequestId(),
]));
$result = false;
}
return $result;
}
/**
* 恢复直播推流
*
* @param string $streamName
* @param string $appName
* @return array|bool
*/
public function resumeStream($streamName, $appName = 'live')
{
try {
$request = new ResumeLiveStreamRequest();
$params = json_encode([
'DomainName' => $this->settings['push_domain'],
'AppName' => $appName ?: 'live',
'StreamName' => $streamName,
]);
$request->fromJsonString($params);
$this->logger->debug('Resume Live Stream Request ' . $params);
$response = $this->client->ResumeLiveStream($request);
$this->logger->debug('Resume Live Stream Response ' . $response->toJsonString());
$result = json_decode($response->toJsonString(), true);
} catch (TencentCloudSDKException $e) {
$this->logger->error('Resume Live Stream Exception ' . kg_json_encode([
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
'requestId' => $e->getRequestId(),
]));
$result = false;
}
return $result;
} }
/** /**
* 获取推流地址 * 获取推流地址
* *
* @param string $streamName * @param string $streamName
* @param string $appName
* @return string * @return string
*/ */
function getPushUrl($streamName) function getPushUrl($streamName, $appName = 'live')
{ {
$appName = $appName ?: 'live';
$authEnabled = $this->settings['push_auth_enabled']; $authEnabled = $this->settings['push_auth_enabled'];
$authKey = $this->settings['push_auth_key']; $authKey = $this->settings['push_auth_key'];
$expireTime = $this->settings['push_auth_delta'] + time(); $expireTime = $this->settings['push_auth_delta'] + time();
$domain = $this->settings['push_domain']; $domain = $this->settings['push_domain'];
$appName = 'live';
$authParams = $this->getAuthParams($streamName, $authKey, $expireTime); $authParams = $this->getAuthParams($streamName, $authKey, $expireTime);
@ -46,6 +205,8 @@ class Live extends Service
*/ */
public function getPullUrls($streamName, $appName = 'live') public function getPullUrls($streamName, $appName = 'live')
{ {
$appName = $appName ?: 'live';
$protocol = $this->settings['pull_protocol']; $protocol = $this->settings['pull_protocol'];
$domain = $this->settings['pull_domain']; $domain = $this->settings['pull_domain'];
$authEnabled = $this->settings['pull_auth_enabled']; $authEnabled = $this->settings['pull_auth_enabled'];
@ -116,4 +277,25 @@ class Live extends Service
]); ]);
} }
protected function getLiveClient()
{
$secret = $this->getSectionSettings('secret');
$secretId = $secret['secret_id'];
$secretKey = $secret['secret_key'];
$region = '';
$credential = new Credential($secretId, $secretKey);
$httpProfile = new HttpProfile();
$httpProfile->setEndpoint(self::END_POINT);
$clientProfile = new ClientProfile();
$clientProfile->setHttpProfile($httpProfile);
return new LiveClient($credential, $region, $clientProfile);
}
} }

View File

@ -123,12 +123,12 @@ $config['throttle']['lifetime'] = 60;
$config['throttle']['rate_limit'] = 60; $config['throttle']['rate_limit'] = 60;
/** /**
* 客户端连接地址 * 客户端连接地址外部可访问的ip或域名
*/ */
$config['websocket']['url'] = 'ws://127.0.0.1:8282'; $config['websocket']['url'] = 'ws://127.0.0.1:8282';
/** /**
* gateway和worker注册地址 * gateway和worker注册地址(内部访问)
*/ */
$config['websocket']['register_address'] = '127.0.0.1:1238'; $config['websocket']['register_address'] = '127.0.0.1:1238';

View File

@ -183,6 +183,7 @@
.index-course-list .course-card .info { .index-course-list .course-card .info {
border: 1px solid #eee; border: 1px solid #eee;
padding-bottom: 18px;
} }
.index-carousel { .index-carousel {
@ -803,6 +804,14 @@
color: #666; color: #666;
} }
.chat-login-tips {
border-top: 1px solid #f2f2f2;
padding-top: 10px;
text-align: center;
font-size: 12px;
color: #999;
}
.chat-msg-form .layui-input { .chat-msg-form .layui-input {
height: 32px; height: 32px;
font-size: 12px; font-size: 12px;
@ -822,18 +831,6 @@
margin-right: 10px; margin-right: 10px;
} }
.live-user-card {
margin-bottom: 10px;
line-height: 25px;
}
.chat-login-tips {
padding-top: 20px;
text-align: center;
font-size: 12px;
color: #999;
}
.chapter-bg { .chapter-bg {
background-color: #f2f2f2; background-color: #f2f2f2;
} }

View File

@ -0,0 +1,13 @@
layui.use(['carousel', 'flow'], function () {
var carousel = layui.carousel;
var flow = layui.flow;
carousel.render({
elem: '#carousel',
width: '100%',
height: '270px'
});
flow.lazyimg();
});

View File

@ -65,7 +65,7 @@ layui.use(['jquery', 'form', 'helper'], function () {
function showNewMessage(res) { function showNewMessage(res) {
var html = '<div class="chat">'; var html = '<div class="chat">';
if (res.user.vip === 0) { if (res.user.vip === 1) {
html += '<span class="vip-icon layui-icon layui-icon-diamond"></span>'; html += '<span class="vip-icon layui-icon layui-icon-diamond"></span>';
} }
html += '<span class="user">' + res.user.name + ':</span>'; html += '<span class="user">' + res.user.name + ':</span>';