1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-18 18:16:55 +08:00

Compare commits

..

No commits in common. "master" and "v1.7.7" have entirely different histories.

50 changed files with 368 additions and 331 deletions

View File

@ -1,18 +1,3 @@
### [v1.7.8](https://gitee.com/koogua/course-tencent-cloud/releases/v1.7.8)(2025-06-20)
- 移除ThrottleLimit
- 增加CloseLiveTask
- 增加搜索页图片alt属性striptags过滤
- 后台增加返回顶部快捷方式
- 前台fixbar增加联系电话
- 优化安装脚本
- 优化课时列表直播提示
- 优化后台返回链接
- 优化统计分析代码位置
- 直播回调后更新课时缓存
- 后台清空头像->上传头像
- sitemap.xml直接写入网站根目录
### [v1.7.7](https://gitee.com/koogua/course-tencent-cloud/releases/v1.7.7)(2025-04-20) ### [v1.7.7](https://gitee.com/koogua/course-tencent-cloud/releases/v1.7.7)(2025-04-20)
- 优化索引管理工具 - 优化索引管理工具

View File

@ -18,13 +18,6 @@ abstract class Migration
abstract public function run(); abstract public function run();
protected function saveSettings(array $settings)
{
foreach ($settings as $setting) {
$this->saveSetting($setting);
}
}
protected function saveSetting(array $setting) protected function saveSetting(array $setting)
{ {
$settingRepo = new SettingRepo(); $settingRepo = new SettingRepo();

View File

@ -1,72 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2024 深圳市酷瓜软件有限公司
* @license https://opensource.org/licenses/GPL-2.0
* @link https://www.koogua.com
*/
namespace App\Console\Tasks;
use App\Caches\CourseChapterList as CourseChapterListCache;
use App\Models\Chapter as ChapterModel;
use App\Models\ChapterLive as ChapterLiveModel;
use App\Repos\Chapter as ChapterRepo;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class CloseLiveTask extends Task
{
public function mainAction()
{
$chapterLives = $this->findChapterLives();
echo sprintf('pending lives: %s', $chapterLives->count()) . PHP_EOL;
if ($chapterLives->count() == 0) return;
echo '------ start close live task ------' . PHP_EOL;
foreach ($chapterLives as $chapterLive) {
$chapterLive->status = ChapterLiveModel::STATUS_INACTIVE;
$chapterLive->update();
$chapterRepo = new ChapterRepo();
$chapter = $chapterRepo->findById($chapterLive->chapter_id);
$attrs = $chapter->attrs;
$attrs['stream']['status'] = ChapterModel::SS_INACTIVE;
$chapter->attrs = $attrs;
$chapter->update();
$cache = new CourseChapterListCache();
$cache->rebuild($chapterLive->course_id);
}
echo '------ end close live task ------' . PHP_EOL;
}
/**
* 查找待关闭直播
*
* @param int $limit
* @return ResultsetInterface|Resultset|ChapterLiveModel[]
*/
protected function findChapterLives(int $limit = 100)
{
$status = ChapterLiveModel::STATUS_ACTIVE;
$endTime = time() - 3600;
return ChapterLiveModel::query()
->where('status = :status:', ['status' => $status])
->andWhere('end_time < :end_time:', ['end_time' => $endTime])
->limit($limit)
->execute();
}
}

View File

@ -37,7 +37,7 @@ class SitemapTask extends Task
$this->sitemap = new Sitemap(); $this->sitemap = new Sitemap();
$filename = public_path('sitemap.xml'); $filename = tmp_path('sitemap.xml');
echo '------ start sitemap task ------' . PHP_EOL; echo '------ start sitemap task ------' . PHP_EOL;

View File

@ -13,12 +13,12 @@ use GuzzleHttp\Client;
class SyncAppInfoTask extends Task class SyncAppInfoTask extends Task
{ {
const API_BASE_URL = 'https://www.koogua.com/api';
public function mainAction() public function mainAction()
{ {
echo '------ start sync app info ------' . PHP_EOL; echo '------ start sync app info ------' . PHP_EOL;
$url = 'https://www.koogua.com/api/instance/collect';
$site = $this->getSettings('site'); $site = $this->getSettings('site');
$serverHost = parse_url($site['url'], PHP_URL_HOST); $serverHost = parse_url($site['url'], PHP_URL_HOST);
@ -38,8 +38,6 @@ class SyncAppInfoTask extends Task
$client = new Client(); $client = new Client();
$url = sprintf('%s/instance/collect', self::API_BASE_URL);
$client->request('POST', $url, ['form_params' => $params]); $client->request('POST', $url, ['form_params' => $params]);
echo '------ end sync app info ------' . PHP_EOL; echo '------ end sync app info ------' . PHP_EOL;

View File

@ -71,6 +71,28 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]); return $this->jsonSuccess(['data' => $data]);
} }
/**
* @Post("/avatar/img", name="admin.upload.avatar_img")
*/
public function uploadAvatarImageAction()
{
$service = new StorageService();
$file = $service->uploadAvatarImage();
if (!$file) {
return $this->jsonError(['msg' => '上传文件失败']);
}
$data = [
'id' => $file->id,
'name' => $file->name,
'url' => $service->getImageUrl($file->path),
];
return $this->jsonSuccess(['data' => $data]);
}
/** /**
* @Post("/content/img", name="admin.upload.content_img") * @Post("/content/img", name="admin.upload.content_img")
*/ */

View File

@ -251,16 +251,14 @@ class ChapterContent extends Service
$content = $validator->checkContent($post['content']); $content = $validator->checkContent($post['content']);
$read->content = $content; $read->update(['content' => $content]);
$read->update();
$attrs = $chapter->attrs; $attrs = $chapter->attrs;
$attrs['word_count'] = WordUtil::getWordCount($content); $attrs['word_count'] = WordUtil::getWordCount($content);
$attrs['duration'] = WordUtil::getWordDuration($content); $attrs['duration'] = WordUtil::getWordDuration($content);
$chapter->attrs = $attrs;
$chapter->update(); $chapter->update(['attrs' => $attrs]);
$this->updateCourseReadAttrs($read->course_id); $this->updateCourseReadAttrs($read->course_id);
} }

View File

@ -9,7 +9,7 @@
<div class="kg-nav-left"> <div class="kg-nav-left">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
{% if parent.id > 0 %} {% if parent.id > 0 %}
<a href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a> <a class="kg-back" href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a>
<a><cite>{{ parent.name }}</cite></a> <a><cite>{{ parent.name }}</cite></a>
{% endif %} {% endif %}
<a><cite>分类管理</cite></a> <a><cite>分类管理</cite></a>

View File

@ -88,7 +88,7 @@
layer.open({ layer.open({
type: 2, type: 2,
title: '推流测试', title: '推流测试',
area: ['720px', '540px'], area: ['720px', '500px'],
content: [url, 'no'] content: [url, 'no']
}); });
}); });

View File

@ -43,7 +43,7 @@
<div class="kg-nav"> <div class="kg-nav">
<div class="kg-nav-left"> <div class="kg-nav-left">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
<a href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a> <a class="kg-back" href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a>
<a><cite>{{ course.title }}</cite></a> <a><cite>{{ course.title }}</cite></a>
<a><cite>{{ chapter.title }}</cite></a> <a><cite>{{ chapter.title }}</cite></a>
<a><cite>课时管理</cite></a> <a><cite>课时管理</cite></a>

View File

@ -9,7 +9,7 @@
<div class="kg-nav"> <div class="kg-nav">
<div class="kg-nav-left"> <div class="kg-nav-left">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
<a href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a> <a class="kg-back" href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a>
<a><cite>{{ course.title }}</cite></a> <a><cite>{{ course.title }}</cite></a>
<a><cite>章节管理</cite></a> <a><cite>章节管理</cite></a>
</span> </span>

View File

@ -25,7 +25,9 @@
<div class="kg-nav-left"> <div class="kg-nav-left">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
{% if parent.id > 0 %} {% if parent.id > 0 %}
<a href="{{ back_url }}"><i class="layui-icon layui-icon-return"></i>返回</a> <a class="kg-back" href="{{ back_url }}">
<i class="layui-icon layui-icon-return"></i> 返回
</a>
<a><cite>{{ parent.name }}</cite></a> <a><cite>{{ parent.name }}</cite></a>
{% endif %} {% endif %}
<a><cite>导航管理</cite></a> <a><cite>导航管理</cite></a>

View File

@ -20,7 +20,6 @@
{{ js_include('lib/layui/layui.js') }} {{ js_include('lib/layui/layui.js') }}
{{ js_include('admin/js/common.js') }} {{ js_include('admin/js/common.js') }}
{{ js_include('admin/js/fixbar.js') }}
{% block include_js %}{% endblock %} {% block include_js %}{% endblock %}
{% block inline_js %}{% endblock %} {% block inline_js %}{% endblock %}

View File

@ -21,11 +21,12 @@
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label" style="padding-top:30px;">头像</label> <label class="layui-form-label" style="padding-top:30px;">头像</label>
<div class="layui-input-inline" style="width:80px;"> <div class="layui-input-inline" style="width:80px;">
<img id="img-avatar" class="kg-avatar" src="{{ user.avatar }}"> <img id="avatar" class="kg-avatar" src="{{ user.avatar }}">
<input type="hidden" name="avatar" value="{{ user.avatar }}"> <input type="hidden" name="avatar" value="{{ user.avatar }}">
<input type="hidden" name="default_avatar" value="{{ default_avatar }}">
</div> </div>
<div class="layui-input-inline" style="padding-top:25px;"> <div class="layui-input-inline" style="padding-top:25px;">
<button id="change-avatar" class="layui-btn layui-btn-sm" type="button">更换</button> <button id="clear-avatar" class="layui-btn layui-btn-sm" type="button">清空</button>
</div> </div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
@ -152,12 +153,6 @@
{% endblock %} {% endblock %}
{% block include_js %}
{{ js_include('admin/js/avatar.upload.js') }}
{% endblock %}
{% block inline_js %} {% block inline_js %}
<script> <script>
@ -168,6 +163,12 @@
var form = layui.form; var form = layui.form;
var laydate = layui.laydate; var laydate = layui.laydate;
$('#clear-avatar').on('click', function () {
var defaultAvatar = $('input[name=default_avatar]').val();
$('input[name=avatar]').val(defaultAvatar);
$('#avatar').attr('src', defaultAvatar);
});
laydate.render({ laydate.render({
elem: 'input[name=vip_expiry_time]', elem: 'input[name=vip_expiry_time]',
type: 'datetime' type: 'datetime'

View File

@ -30,6 +30,8 @@ class Controller extends \Phalcon\Mvc\Controller
$this->setCors(); $this->setCors();
} }
$this->checkRateLimit();
return true; return true;
} }

View File

@ -120,16 +120,19 @@ class ConnectController extends Controller
$service = new ConnectService(); $service = new ConnectService();
$openUser = $service->getOpenUserInfo($code, $state, $provider); $openUser = $service->getOpenUserInfo($code, $state, $provider);
$connect = $service->getConnectRelation($openUser['id'], $openUser['provider']); $connect = $service->getConnectRelation($openUser['id'], $openUser['provider']);
if ($this->authUser->id > 0 && $openUser) { if ($this->authUser->id > 0) {
$service->bindUser($openUser); if ($openUser) {
return $this->response->redirect(['for' => 'home.uc.account']); $service->bindUser($openUser);
} return $this->response->redirect(['for' => 'home.uc.account']);
}
if ($this->authUser->id == 0 && $connect) { } else {
$service->authConnectLogin($connect); if ($connect) {
return $this->response->redirect(['for' => 'home.index']); $service->authConnectLogin($connect);
return $this->response->redirect(['for' => 'home.index']);
}
} }
$this->seo->prependTitle('绑定帐号'); $this->seo->prependTitle('绑定帐号');

View File

@ -77,6 +77,8 @@ class Controller extends \Phalcon\Mvc\Controller
$this->checkCsrfToken(); $this->checkCsrfToken();
} }
$this->checkRateLimit();
return true; return true;
} }

View File

@ -37,6 +37,8 @@ class LayerController extends \Phalcon\Mvc\Controller
$this->checkCsrfToken(); $this->checkCsrfToken();
} }
$this->checkRateLimit();
return true; return true;
} }

View File

@ -36,6 +36,6 @@
</form> </form>
{% else %} {% else %}
<div class="register-close-tips"> <div class="register-close-tips">
<i class="layui-icon layui-icon-lock"></i> 邮箱注册已关闭 <i class="layui-icon layui-icon-tips"></i> 邮箱注册已关闭
</div> </div>
{% endif %} {% endif %}

View File

@ -36,6 +36,6 @@
</form> </form>
{% else %} {% else %}
<div class="register-close-tips"> <div class="register-close-tips">
<i class="layui-icon layui-icon-lock"></i> 手机注册已关闭 <i class="layui-icon layui-icon-tips"></i> 手机注册已关闭
</div> </div>
{% endif %} {% endif %}

View File

@ -98,11 +98,9 @@
{% if lesson.attrs.stream.status == 'active' %} {% if lesson.attrs.stream.status == 'active' %}
<span class="flag flag-active">直播中</span> <span class="flag flag-active">直播中</span>
{% elseif lesson.attrs.start_time > time() %} {% elseif lesson.attrs.start_time > time() %}
<span class="flag flag-scheduled">倒计时</span> <span class="flag flag-pending">倒计时</span>
{% elseif lesson.attrs.end_time < time() %} {% elseif lesson.attrs.end_time < time() %}
<span class="flag flag-ended">已结束</span> <span class="flag flag-ended">已结束</span>
{% elseif lesson.attrs.stream.status == 'inactive' %}
<span class="flag flag-inactive">未推流</span>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
@ -110,7 +108,7 @@
{% if lesson.attrs.start_time < time() and lesson.attrs.end_time > time() %} {% if lesson.attrs.start_time < time() and lesson.attrs.end_time > time() %}
<span class="flag flag-active">授课中</span> <span class="flag flag-active">授课中</span>
{% elseif lesson.attrs.start_time > time() %} {% elseif lesson.attrs.start_time > time() %}
<span class="flag flag-scheduled">未开始</span> <span class="flag flag-pending">未开始</span>
{% elseif lesson.attrs.end_time < time() %} {% elseif lesson.attrs.end_time < time() %}
<span class="flag flag-ended">已结束</span> <span class="flag flag-ended">已结束</span>
{% endif %} {% endif %}

View File

@ -31,10 +31,17 @@
{% endblock %} {% endblock %}
{% block include_js %} {% block inline_js %}
{{ js_include('lib/clipboard.min.js') }} <script>
{{ js_include('home/js/help.show.js') }} layui.use(['jquery', 'helper'], function () {
{{ js_include('home/js/copy.js') }} var $ = layui.jquery;
var helper = layui.helper;
var $courseList = $('#course-list');
if ($courseList.length > 0) {
helper.ajaxLoadHtml($courseList.data('url'), $courseList.attr('id'));
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -30,10 +30,17 @@
{% endblock %} {% endblock %}
{% block include_js %} {% block inline_js %}
{{ js_include('lib/clipboard.min.js') }} <script>
{{ js_include('home/js/page.show.js') }} layui.use(['jquery', 'helper'], function () {
{{ js_include('home/js/copy.js') }} var $ = layui.jquery;
var helper = layui.helper;
var $courseList = $('#course-list');
if ($courseList.length > 0) {
helper.ajaxLoadHtml($courseList.data('url'), $courseList.attr('id'));
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -19,7 +19,7 @@
{% if item.cover %} {% if item.cover %}
<div class="cover"> <div class="cover">
<a href="{{ article_url }}" target="_blank"> <a href="{{ article_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title|striptags }}"> <img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -6,7 +6,7 @@
<div class="search-course-card"> <div class="search-course-card">
<div class="cover"> <div class="cover">
<a href="{{ course_url }}" target="_blank"> <a href="{{ course_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title|striptags }}"> <img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a> </a>
</div> </div>
<div class="info"> <div class="info">

View File

@ -19,7 +19,7 @@
{% if item.cover %} {% if item.cover %}
<div class="cover"> <div class="cover">
<a href="{{ question_url }}" target="_blank"> <a href="{{ question_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title|striptags }}"> <img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,9 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN-Hans"> <html lang="zh-CN-Hans">
<head> <head>
{% if site_info.analytics_enabled == 1 %}
{{ site_info.analytics_script }}
{% endif %}
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <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">
@ -41,5 +38,12 @@
{% block include_js %}{% endblock %} {% block include_js %}{% endblock %}
{% block inline_js %}{% endblock %} {% block inline_js %}{% endblock %}
{% if site_info.analytics_enabled == 1 %}
<div class="layui-hide">
{{ site_info.analytics_script }}
</div>
{% endif %}
</body> </body>
</html> </html>

View File

@ -79,6 +79,7 @@
{% block include_js %} {% block include_js %}
{{ js_include('home/js/user.avatar.upload.js') }}
{{ js_include('home/js/user.console.profile.js') }} {{ js_include('home/js/user.console.profile.js') }}
{% endblock %} {% endblock %}

View File

@ -16,7 +16,7 @@ class AppInfo
protected $link = 'https://www.koogua.com'; protected $link = 'https://www.koogua.com';
protected $version = '1.7.8'; protected $version = '1.7.7';
public function __get($name) public function __get($name)
{ {

View File

@ -19,7 +19,6 @@ class CourseUser extends Model
const SOURCE_MANUAL = 4; // 分配 const SOURCE_MANUAL = 4; // 分配
const SOURCE_POINT_REDEEM = 5; // 积分兑换 const SOURCE_POINT_REDEEM = 5; // 积分兑换
const SOURCE_LUCKY_REDEEM = 6; // 抽奖兑换 const SOURCE_LUCKY_REDEEM = 6; // 抽奖兑换
const SOURCE_TEACHER = 7; // 教师
const SOURCE_TRIAL = 10; // 试听 const SOURCE_TRIAL = 10; // 试听
/** /**
@ -138,7 +137,6 @@ class CourseUser extends Model
self::SOURCE_TRIAL => '试听', self::SOURCE_TRIAL => '试听',
self::SOURCE_VIP => '畅学', self::SOURCE_VIP => '畅学',
self::SOURCE_MANUAL => '分配', self::SOURCE_MANUAL => '分配',
self::SOURCE_TEACHER => '教师',
self::SOURCE_POINT_REDEEM => '积分兑换', self::SOURCE_POINT_REDEEM => '积分兑换',
self::SOURCE_LUCKY_REDEEM => '抽奖兑换', self::SOURCE_LUCKY_REDEEM => '抽奖兑换',
]; ];

View File

@ -7,7 +7,7 @@
namespace App\Services; namespace App\Services;
use App\Caches\CourseChapterList as CourseChapterListCache; use App\Caches\CourseChapterList as CatalogCache;
use App\Models\Chapter as ChapterModel; use App\Models\Chapter as ChapterModel;
use App\Models\ChapterLive as ChapterLiveModel; use App\Models\ChapterLive as ChapterLiveModel;
use App\Repos\Chapter as ChapterRepo; use App\Repos\Chapter as ChapterRepo;
@ -175,7 +175,7 @@ class LiveNotify extends Service
protected function rebuildCatalogCache(ChapterModel $chapter) protected function rebuildCatalogCache(ChapterModel $chapter)
{ {
$cache = new CourseChapterListCache(); $cache = new CatalogCache();
$cache->rebuild($chapter->course_id); $cache->rebuild($chapter->course_id);
} }

View File

@ -46,11 +46,7 @@ trait CourseUserTrait
$this->joinedCourse = true; $this->joinedCourse = true;
} }
if ($course->teacher_id == $user->id) { if ($course->market_price == 0) {
$this->ownedCourse = true;
} elseif ($course->market_price == 0) {
$this->ownedCourse = true; $this->ownedCourse = true;
@ -100,7 +96,6 @@ trait CourseUserTrait
case CourseUserModel::SOURCE_FREE: case CourseUserModel::SOURCE_FREE:
case CourseUserModel::SOURCE_TRIAL: case CourseUserModel::SOURCE_TRIAL:
case CourseUserModel::SOURCE_VIP: case CourseUserModel::SOURCE_VIP:
case CourseUserModel::SOURCE_TEACHER:
$this->createCourseUser($course, $user, $expiryTime, $sourceType); $this->createCourseUser($course, $user, $expiryTime, $sourceType);
$this->deleteCourseUser($relation); $this->deleteCourseUser($relation);
break; break;
@ -174,8 +169,6 @@ trait CourseUserTrait
$result = true; $result = true;
} elseif ($course->vip_price == 0 && $user->vip == 1) { } elseif ($course->vip_price == 0 && $user->vip == 1) {
$result = true; $result = true;
} elseif($course->teacher_id == $user->id) {
$result = true;
} }
return $result; return $result;
@ -183,10 +176,6 @@ trait CourseUserTrait
protected function getFreeSourceType(CourseModel $course, UserModel $user) protected function getFreeSourceType(CourseModel $course, UserModel $user)
{ {
if ($course->teacher_id == $user->id) {
return CourseUserModel::SOURCE_TEACHER;
}
$sourceType = CourseUserModel::SOURCE_FREE; $sourceType = CourseUserModel::SOURCE_FREE;
if ($course->market_price > 0) { if ($course->market_price > 0) {

View File

@ -121,6 +121,7 @@ class OrderConfirm extends LogicService
'lesson_count' => $course->lesson_count, 'lesson_count' => $course->lesson_count,
'study_expiry' => $course->study_expiry, 'study_expiry' => $course->study_expiry,
'refund_expiry' => $course->refund_expiry, 'refund_expiry' => $course->refund_expiry,
'origin_price' => $course->origin_price,
'market_price' => $course->market_price, 'market_price' => $course->market_price,
'vip_price' => $course->vip_price, 'vip_price' => $course->vip_price,
]; ];

65
app/Services/Throttle.php Normal file
View File

@ -0,0 +1,65 @@
<?php
/**
* @copyright Copyright (c) 2021 深圳市酷瓜软件有限公司
* @license https://opensource.org/licenses/GPL-2.0
* @link https://www.koogua.com
*/
namespace App\Services;
class Throttle extends Service
{
public function checkRateLimit()
{
$config = $this->getConfig();
if (!$config->path('throttle.enabled')) {
return true;
}
$cache = $this->getCache();
$sign = $this->getRequestSignature();
$cacheKey = $this->getCacheKey($sign);
if ($cache->ttl($cacheKey) < 1) {
$cache->save($cacheKey, 0, $config->path('throttle.lifetime'));
}
$rateLimit = $cache->get($cacheKey);
if ($rateLimit >= $config->path('throttle.rate_limit')) {
return false;
}
$cache->increment($cacheKey, 1);
return true;
}
protected function getRequestSignature()
{
$authUser = $this->getAuthUser();
if (!empty($authUser['id'])) {
return md5($authUser['id']);
}
$httpHost = $this->request->getHttpHost();
$clientAddress = $this->request->getClientAddress();
if ($httpHost && $clientAddress) {
return md5($httpHost . '|' . $clientAddress);
}
throw new \RuntimeException('Unable to generate request signature');
}
protected function getCacheKey($sign)
{
return "throttle:{$sign}";
}
}

View File

@ -28,6 +28,13 @@ trait Security
$validator->checkHttpReferer(); $validator->checkHttpReferer();
} }
public function checkRateLimit()
{
$validator = new SecurityValidator();
$validator->checkRateLimit();
}
public function isNotSafeRequest() public function isNotSafeRequest()
{ {
/** /**

View File

@ -11,7 +11,10 @@ use App\Exceptions\BadRequest as BadRequestException;
use App\Models\Order as OrderModel; use App\Models\Order as OrderModel;
use App\Models\Refund as RefundModel; use App\Models\Refund as RefundModel;
use App\Models\Trade as TradeModel; use App\Models\Trade as TradeModel;
use App\Repos\Course as CourseRepo;
use App\Repos\Order as OrderRepo; use App\Repos\Order as OrderRepo;
use App\Repos\Package as PackageRepo;
use App\Repos\Vip as VipRepo;
class Order extends Validator class Order extends Validator
{ {
@ -47,36 +50,54 @@ class Order extends Validator
return $order; return $order;
} }
public function checkItemType($type) public function checkItemType($itemType)
{ {
$types = OrderModel::itemTypes(); $list = OrderModel::itemTypes();
if (!array_key_exists($type, $types)) { if (!array_key_exists($itemType, $list)) {
throw new BadRequestException('order.invalid_item_type'); throw new BadRequestException('order.invalid_item_type');
} }
return $type; return $itemType;
} }
public function checkCourse($id) public function checkCourse($itemId)
{ {
$validator = new Course(); $courseRepo = new CourseRepo();
return $validator->checkCourse($id); $course = $courseRepo->findById($itemId);
if (!$course || $course->published == 0) {
throw new BadRequestException('order.item_not_found');
}
return $course;
} }
public function checkPackage($id) public function checkPackage($itemId)
{ {
$validator = new Package(); $packageRepo = new PackageRepo();
return $validator->checkPackage($id); $package = $packageRepo->findById($itemId);
if (!$package || $package->published == 0) {
throw new BadRequestException('order.item_not_found');
}
return $package;
} }
public function checkVip($id) public function checkVip($itemId)
{ {
$validator = new Vip(); $vipRepo = new VipRepo();
return $validator->checkVip($id); $vip = $vipRepo->findById($itemId);
if (!$vip || $vip->deleted == 1) {
throw new BadRequestException('order.item_not_found');
}
return $vip;
} }
public function checkAmount($amount) public function checkAmount($amount)
@ -84,7 +105,7 @@ class Order extends Validator
$value = $this->filter->sanitize($amount, ['trim', 'float']); $value = $this->filter->sanitize($amount, ['trim', 'float']);
if ($value < 0.01 || $value > 100000) { if ($value < 0.01 || $value > 100000) {
throw new BadRequestException('order.invalid_amount'); throw new BadRequestException('order.invalid_pay_amount');
} }
return $value; return $value;
@ -127,7 +148,7 @@ class Order extends Validator
]; ];
if (!in_array($order->item_type, $types)) { if (!in_array($order->item_type, $types)) {
throw new BadRequestException('order.refund_not_supported'); throw new BadRequestException('order.refund_item_unsupported');
} }
$orderRepo = new OrderRepo(); $orderRepo = new OrderRepo();
@ -146,7 +167,7 @@ class Order extends Validator
]; ];
if ($refund && in_array($refund->status, $scopes)) { if ($refund && in_array($refund->status, $scopes)) {
throw new BadRequestException('order.refund_request_existed'); throw new BadRequestException('order.refund_apply_existed');
} }
} }

View File

@ -8,7 +8,9 @@
namespace App\Validators; namespace App\Validators;
use App\Exceptions\BadRequest as BadRequestException; use App\Exceptions\BadRequest as BadRequestException;
use App\Exceptions\ServiceUnavailable as ServiceUnavailableException;
use App\Library\CsrfToken as CsrfTokenService; use App\Library\CsrfToken as CsrfTokenService;
use App\Services\Throttle as ThrottleService;
class Security extends Validator class Security extends Validator
{ {
@ -51,6 +53,17 @@ class Security extends Validator
} }
} }
public function checkRateLimit()
{
$service = new ThrottleService();
$result = $service->checkRateLimit();
if (!$result) {
throw new ServiceUnavailableException('security.too_many_requests');
}
}
protected function getCsrfWhitelist() protected function getCsrfWhitelist()
{ {
return []; return [];

View File

@ -91,7 +91,7 @@ class Trade extends Validator
]; ];
if ($refund && in_array($refund->status, $scopes)) { if ($refund && in_array($refund->status, $scopes)) {
throw new BadRequestException('trade.refund_request_existed'); throw new BadRequestException('trade.refund_apply_existed');
} }
} }

View File

@ -147,6 +147,21 @@ $config['cors']['allow_headers'] = '*';
*/ */
$config['cors']['allow_methods'] = ['GET', 'POST', 'OPTIONS']; $config['cors']['allow_methods'] = ['GET', 'POST', 'OPTIONS'];
/**
* 限流开启
*/
$config['throttle']['enabled'] = true;
/**
* 有效期(秒)
*/
$config['throttle']['lifetime'] = 60;
/**
* 限流频率
*/
$config['throttle']['rate_limit'] = 60;
/** /**
* 客户端ping服务端间隔 * 客户端ping服务端间隔
*/ */

View File

@ -22,6 +22,7 @@ $error['sys.unknown_error'] = '未知错误';
/** /**
* 安全相关 * 安全相关
*/ */
$error['security.too_many_requests'] = '请求过于频繁';
$error['security.invalid_csrf_token'] = '无效的CSRF令牌'; $error['security.invalid_csrf_token'] = '无效的CSRF令牌';
$error['security.invalid_http_referer'] = '无效请求来源'; $error['security.invalid_http_referer'] = '无效请求来源';
@ -363,15 +364,17 @@ $error['slide.invalid_publish_status'] = '无效的发布状态';
* 订单相关 * 订单相关
*/ */
$error['order.not_found'] = '订单不存在'; $error['order.not_found'] = '订单不存在';
$error['order.invalid_amount'] = '无效的支付金额';
$error['order.invalid_status'] = '无效的状态类型'; $error['order.invalid_status'] = '无效的状态类型';
$error['order.item_not_found'] = '商品不存在';
$error['order.trade_expired'] = '交易已过期';
$error['order.is_delivering'] = '已经下过单了,正在准备发货中'; $error['order.is_delivering'] = '已经下过单了,正在准备发货中';
$error['order.has_bought_course'] = '已经购买过该课程'; $error['order.has_bought_course'] = '已经购买过该课程';
$error['order.has_bought_package'] = '已经购买过该套餐'; $error['order.has_bought_package'] = '已经购买过该套餐';
$error['order.cancel_not_allowed'] = '当前不允许取消订单'; $error['order.cancel_not_allowed'] = '当前不允许取消订单';
$error['order.close_not_allowed'] = '当前不允许关闭订单';
$error['order.refund_not_allowed'] = '当前不允许申请退款'; $error['order.refund_not_allowed'] = '当前不允许申请退款';
$error['order.refund_not_supported'] = '该品类不支持退款'; $error['order.refund_item_unsupported'] = '该品类不支持退款';
$error['order.refund_request_existed'] = '退款申请已经存在'; $error['order.refund_apply_existed'] = '退款申请已经存在';
/** /**
* 交易相关 * 交易相关
@ -382,7 +385,7 @@ $error['trade.invalid_channel'] = '无效的平台类型';
$error['trade.invalid_status'] = '无效的状态类型'; $error['trade.invalid_status'] = '无效的状态类型';
$error['trade.close_not_allowed'] = '当前不允许关闭交易'; $error['trade.close_not_allowed'] = '当前不允许关闭交易';
$error['trade.refund_not_allowed'] = '当前不允许交易退款'; $error['trade.refund_not_allowed'] = '当前不允许交易退款';
$error['trade.refund_request_existed'] = '退款申请已经存在,请等待处理结果'; $error['trade.refund_apply_existed'] = '退款申请已经存在,请等待处理结果';
/** /**
* 退款相关 * 退款相关

View File

@ -294,6 +294,21 @@ final class V20210403184518 extends AbstractMigration
protected function initSettingData() protected function initSettingData()
{ {
$rows = [ $rows = [
[
'section' => 'captcha',
'item_key' => 'enabled',
'item_value' => '0',
],
[
'section' => 'captcha',
'item_key' => 'app_id',
'item_value' => '',
],
[
'section' => 'captcha',
'item_key' => 'secret_key',
'item_value' => '',
],
[ [
'section' => 'live.push', 'section' => 'live.push',
'item_key' => 'domain', 'item_key' => 'domain',
@ -377,7 +392,7 @@ final class V20210403184518 extends AbstractMigration
[ [
'section' => 'mail', 'section' => 'mail',
'item_key' => 'smtp_host', 'item_key' => 'smtp_host',
'item_value' => '', 'item_value' => 'smtp.163.com',
], ],
[ [
'section' => 'mail', 'section' => 'mail',
@ -397,22 +412,22 @@ final class V20210403184518 extends AbstractMigration
[ [
'section' => 'mail', 'section' => 'mail',
'item_key' => 'smtp_username', 'item_key' => 'smtp_username',
'item_value' => '', 'item_value' => 'xxx@163.com',
], ],
[ [
'section' => 'mail', 'section' => 'mail',
'item_key' => 'smtp_password', 'item_key' => 'smtp_password',
'item_value' => '', 'item_value' => 'xxx',
], ],
[ [
'section' => 'mail', 'section' => 'mail',
'item_key' => 'smtp_from_email', 'item_key' => 'smtp_from_email',
'item_value' => '', 'item_value' => 'xxx@163.com',
], ],
[ [
'section' => 'mail', 'section' => 'mail',
'item_key' => 'smtp_from_name', 'item_key' => 'smtp_from_name',
'item_value' => '', 'item_value' => 'XXX有限公司',
], ],
[ [
'section' => 'pay.alipay', 'section' => 'pay.alipay',
@ -602,18 +617,18 @@ final class V20210403184518 extends AbstractMigration
[ [
'section' => 'sms', 'section' => 'sms',
'item_key' => 'signature', 'item_key' => 'signature',
'item_value' => '', 'item_value' => '酷瓜云课堂',
], ],
[ [
'section' => 'sms', 'section' => 'sms',
'item_key' => 'template', 'item_key' => 'template',
'item_value' => json_encode([ 'item_value' => json_encode([
'verify' => ['enabled' => 1, 'id' => 0], 'verify' => ['enabled' => 1, 'id' => ''],
'order_finish' => ['enabled' => 0, 'id' => 0], 'order_finish' => ['enabled' => 1, 'id' => ''],
'refund_finish' => ['enabled' => 0, 'id' => 0], 'refund_finish' => ['enabled' => 1, 'id' => ''],
'live_begin' => ['enabled' => 0, 'id' => 0], 'live_begin' => ['enabled' => 1, 'id' => ''],
'consult_reply' => ['enabled' => 0, 'id' => 0], 'consult_reply' => ['enabled' => 1, 'id' => ''],
'goods_deliver' => ['enabled' => 0, 'id' => 0], 'goods_deliver' => ['enabled' => 1, 'id' => ''],
]), ]),
], ],
[ [
@ -820,12 +835,12 @@ final class V20210403184518 extends AbstractMigration
'section' => 'wechat.oa', 'section' => 'wechat.oa',
'item_key' => 'notice_template', 'item_key' => 'notice_template',
'item_value' => json_encode([ 'item_value' => json_encode([
'account_login' => ['enabled' => 0, 'id' => 0], 'account_login' => ['enabled' => 1, 'id' => ''],
'order_finish' => ['enabled' => 0, 'id' => 0], 'order_finish' => ['enabled' => 1, 'id' => ''],
'refund_finish' => ['enabled' => 0, 'id' => 0], 'refund_finish' => ['enabled' => 1, 'id' => ''],
'goods_deliver' => ['enabled' => 0, 'id' => 0], 'goods_deliver' => ['enabled' => 1, 'id' => ''],
'consult_reply' => ['enabled' => 0, 'id' => 0], 'consult_reply' => ['enabled' => 1, 'id' => ''],
'live_begin' => ['enabled' => 0, 'id' => 0], 'live_begin' => ['enabled' => 1, 'id' => ''],
]), ]),
], ],
[ [

View File

@ -9,9 +9,28 @@ layui.use(['jquery', 'layer', 'upload'], function () {
url: '/admin/upload/avatar/img', url: '/admin/upload/avatar/img',
accept: 'images', accept: 'images',
acceptMime: 'image/*', acceptMime: 'image/*',
size: 512,
auto: false,
before: function () { before: function () {
layer.load(); layer.load();
}, },
choose: function (obj) {
var flag = true;
obj.preview(function (index, file, result) {
var img = new Image();
img.src = result;
img.onload = function () {
if (img.width < 1000 && img.height < 1000) {
obj.upload(index, file);
} else {
flag = false;
layer.msg("图片尺寸必须小于 1000 * 1000");
return false;
}
};
return flag;
});
},
done: function (res, index, upload) { done: function (res, index, upload) {
$('#img-avatar').attr('src', res.data.url); $('#img-avatar').attr('src', res.data.url);
$('input[name=avatar]').val(res.data.url); $('input[name=avatar]').val(res.data.url);

View File

@ -1,7 +0,0 @@
layui.use(['util'], function () {
var util = layui.util;
util.fixbar();
});

View File

@ -1096,7 +1096,7 @@
color: orange; color: orange;
} }
.lesson-item .flag-scheduled { .lesson-item .flag-pending {
border: 1px solid green; border: 1px solid green;
color: green; color: green;
} }
@ -1106,11 +1106,6 @@
color: red; color: red;
} }
.lesson-item .flag-inactive {
border: 1px solid gray;
color: gray;
}
.lesson-item .flag-ended { .lesson-item .flag-ended {
border: 1px solid gray; border: 1px solid gray;
color: gray; color: gray;

View File

@ -1,4 +1,4 @@
layui.use(['jquery', 'util'], function () { layui.use(['jquery', 'helper', 'util'], function () {
var $ = layui.jquery; var $ = layui.jquery;
var util = layui.util; var util = layui.util;
@ -47,19 +47,6 @@ layui.use(['jquery', 'util'], function () {
}); });
} }
var showPhoneCode = function () {
var content = '<div class="layui-font-32 layui-font-red layui-padding-5">';
content += '<i class="iconfont icon-phone layui-padding-1 layui-font-28"></i>' + window.contact.phone;
content += '</div>';
layer.open({
type: 1,
title: false,
closeBtn: 0,
shadeClose: true,
content: content,
});
}
var bars = []; var bars = [];
if (window.contact.wechat) { if (window.contact.wechat) {
@ -76,13 +63,6 @@ layui.use(['jquery', 'util'], function () {
}); });
} }
if (window.contact.phone) {
bars.push({
type: 'phone',
content: '<i class="iconfont icon-phone layui-font-30"></i>',
});
}
util.fixbar({ util.fixbar({
bars: bars, bars: bars,
click: function (type) { click: function (type) {
@ -90,30 +70,24 @@ layui.use(['jquery', 'util'], function () {
showWechatCode(); showWechatCode();
} else if (type === 'qq') { } else if (type === 'qq') {
showQQCode(); showQQCode();
} else if (type === 'phone') {
showPhoneCode();
} }
} }
}); });
$('.contact > .wechat').on('click', function () { $('.icon-wechat').on('click', function () {
showWechatCode(); showWechatCode();
}); });
$('.contact > .qq').on('click', function () { $('.icon-qq').on('click', function () {
showQQCode(); showQQCode();
}); });
$('.contact > .toutiao').on('click', function () { $('.icon-toutiao').on('click', function () {
showTouTiaoCode(); showTouTiaoCode();
}); });
$('.contact > .douyin').on('click', function () { $('.icon-douyin').on('click', function () {
showDouYinCode(); showDouYinCode();
}); });
$('.contact > .phone').on('click', function () {
showPhoneCode();
});
}); });

View File

@ -1,12 +0,0 @@
layui.use(['jquery', 'helper'], function () {
var $ = layui.jquery;
var helper = layui.helper;
var $courseList = $('#course-list');
if ($courseList.length > 0) {
helper.ajaxLoadHtml($courseList.data('url'), $courseList.attr('id'));
}
});

View File

@ -1,12 +0,0 @@
layui.use(['jquery', 'helper'], function () {
var $ = layui.jquery;
var helper = layui.helper;
var $courseList = $('#course-list');
if ($courseList.length > 0) {
helper.ajaxLoadHtml($courseList.data('url'), $courseList.attr('id'));
}
});

View File

@ -0,0 +1,44 @@
layui.use(['jquery', 'layer', 'upload'], function () {
var $ = layui.jquery;
var layer = layui.layer;
var upload = layui.upload;
upload.render({
elem: '#change-avatar',
url: '/upload/avatar/img',
accept: 'images',
acceptMime: 'image/*',
size: 512,
auto: false,
before: function () {
layer.load();
},
choose: function (obj) {
var flag = true;
obj.preview(function (index, file, result) {
var img = new Image();
img.src = result;
img.onload = function () {
if (img.width < 1000 && img.height < 1000) {
obj.upload(index, file);
} else {
flag = false;
layer.msg("图片尺寸必须小于 1000 * 1000");
return false;
}
};
return flag;
});
},
done: function (res, index, upload) {
$('#img-avatar').attr('src', res.data.url);
$('input[name=avatar]').val(res.data.url);
layer.closeAll('loading');
},
error: function (index, upload) {
layer.msg('上传文件失败', {icon: 2});
}
});
});

View File

@ -1,47 +1,7 @@
layui.use(['jquery', 'upload', 'layer', 'layarea'], function () { layui.use(['layarea'], function () {
var $ = layui.jquery;
var layer = layui.layer;
var upload = layui.upload;
var layarea = layui.layarea; var layarea = layui.layarea;
upload.render({
elem: '#change-avatar',
url: '/upload/avatar/img',
accept: 'images',
acceptMime: 'image/*',
size: 512,
auto: false,
before: function () {
layer.load();
},
choose: function (obj) {
var flag = true;
obj.preview(function (index, file, result) {
var img = new Image();
img.src = result;
img.onload = function () {
if (img.width < 1000 && img.height < 1000) {
obj.upload(index, file);
} else {
flag = false;
layer.msg("图片尺寸必须小于 1000 * 1000");
return false;
}
};
return flag;
});
},
done: function (res) {
$('#img-avatar').attr('src', res.data.url);
$('input[name=avatar]').val(res.data.url);
layer.closeAll('loading');
},
error: function () {
layer.msg('上传文件失败', {icon: 2});
}
});
layarea.render({ layarea.render({
elem: '#area-picker', elem: '#area-picker',
change: function (res) { change: function (res) {

View File

@ -63,9 +63,6 @@ $scheduler->php($script, $bin, ['--task' => 'sync_article_score', '--action' =>
$scheduler->php($script, $bin, ['--task' => 'sync_question_score', '--action' => 'main']) $scheduler->php($script, $bin, ['--task' => 'sync_question_score', '--action' => 'main'])
->hourly(29); ->hourly(29);
$scheduler->php($script, $bin, ['--task' => 'close_live', '--action' => 'main'])
->hourly(31);
$scheduler->php($script, $bin, ['--task' => 'clean_log', '--action' => 'main']) $scheduler->php($script, $bin, ['--task' => 'clean_log', '--action' => 'main'])
->daily(3, 3); ->daily(3, 3);