mirror of
https://gitee.com/koogua/course-tencent-cloud.git
synced 2025-06-24 20:06:09 +08:00
Merge branch 'develop'
This commit is contained in:
commit
4b9bd42d91
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,7 +6,6 @@
|
||||
/config/xs.user.ini
|
||||
/config/alipay/*.crt
|
||||
/config/wxpay/*.pem
|
||||
/db/migrations/schema.php
|
||||
/public/robots.txt
|
||||
/public/sitemap.xml
|
||||
/public/h5
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,3 +1,18 @@
|
||||
### [v1.2.3](https://gitee.com/koogua/course-tencent-cloud/releases/v1.2.3)(2021-01-03)
|
||||
|
||||
#### 增加
|
||||
|
||||
- 多人使用同一帐号防范机制
|
||||
- 首页缓存刷新工具
|
||||
- 课程综合评分
|
||||
- 课程推荐
|
||||
|
||||
#### 修复
|
||||
|
||||
- phinx-migration-generator 无符号问题
|
||||
- online表并发写入重复记录问题
|
||||
- 计划任务生成sitemap.xml失败
|
||||
|
||||
### [v1.2.2](https://gitee.com/koogua/course-tencent-cloud/releases/v1.2.2)(2020-12-24)
|
||||
|
||||
#### 增加
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
酷瓜云课堂,依托腾讯云基础服务架构,采用C扩展框架Phalcon开发,GPL-2.0开源协议,致力开源网课系统,开源网校系统,开源在线教育系统。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 系统功能
|
||||
|
118
app/Caches/IndexFeaturedCourseList.php
Normal file
118
app/Caches/IndexFeaturedCourseList.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Caches;
|
||||
|
||||
use App\Models\Category as CategoryModel;
|
||||
use App\Models\Course as CourseModel;
|
||||
use App\Services\Category as CategoryService;
|
||||
use Phalcon\Mvc\Model\Resultset;
|
||||
use Phalcon\Mvc\Model\ResultsetInterface;
|
||||
|
||||
/**
|
||||
* 推荐课程
|
||||
*/
|
||||
class IndexFeaturedCourseList extends Cache
|
||||
{
|
||||
|
||||
protected $lifetime = 1 * 86400;
|
||||
|
||||
public function getLifetime()
|
||||
{
|
||||
return $this->lifetime;
|
||||
}
|
||||
|
||||
public function getKey($id = null)
|
||||
{
|
||||
return 'index_featured_course_list';
|
||||
}
|
||||
|
||||
public function getContent($id = null)
|
||||
{
|
||||
$categoryLimit = 5;
|
||||
|
||||
$courseLimit = 8;
|
||||
|
||||
$categories = $this->findCategories($categoryLimit);
|
||||
|
||||
if ($categories->count() == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
|
||||
$item = [];
|
||||
|
||||
$item['category'] = [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
];
|
||||
|
||||
$item['courses'] = [];
|
||||
|
||||
$courses = $this->findCategoryCourses($category->id, $courseLimit);
|
||||
|
||||
if ($courses->count() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$categoryCourses = [];
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$categoryCourses[] = [
|
||||
'id' => $course->id,
|
||||
'title' => $course->title,
|
||||
'cover' => $course->cover,
|
||||
'market_price' => $course->market_price,
|
||||
'vip_price' => $course->vip_price,
|
||||
'model' => $course->model,
|
||||
'level' => $course->level,
|
||||
'user_count' => $course->user_count,
|
||||
'lesson_count' => $course->lesson_count,
|
||||
];
|
||||
}
|
||||
|
||||
$item['courses'] = $categoryCourses;
|
||||
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @return ResultsetInterface|Resultset|CategoryModel[]
|
||||
*/
|
||||
protected function findCategories($limit = 5)
|
||||
{
|
||||
return CategoryModel::query()
|
||||
->where('type = :type:', ['type' => CategoryModel::TYPE_COURSE])
|
||||
->andWhere('level = 1 AND published = 1')
|
||||
->orderBy('priority ASC')
|
||||
->limit($limit)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $categoryId
|
||||
* @param int $limit
|
||||
* @return ResultsetInterface|Resultset|CourseModel[]
|
||||
*/
|
||||
protected function findCategoryCourses($categoryId, $limit = 8)
|
||||
{
|
||||
$categoryService = new CategoryService();
|
||||
|
||||
$categoryIds = $categoryService->getChildCategoryIds($categoryId);
|
||||
|
||||
return CourseModel::query()
|
||||
->inWhere('category_id', $categoryIds)
|
||||
->andWhere('published = 1')
|
||||
->andWhere('featured = 1')
|
||||
->orderBy('id DESC')
|
||||
->limit($limit)
|
||||
->execute();
|
||||
}
|
||||
|
||||
}
|
70
app/Caches/IndexSimpleFeaturedCourseList.php
Normal file
70
app/Caches/IndexSimpleFeaturedCourseList.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Caches;
|
||||
|
||||
use App\Models\Course as CourseModel;
|
||||
use Phalcon\Mvc\Model\Resultset;
|
||||
use Phalcon\Mvc\Model\ResultsetInterface;
|
||||
|
||||
/**
|
||||
* 简版推荐课程
|
||||
*/
|
||||
class IndexSimpleFeaturedCourseList extends Cache
|
||||
{
|
||||
|
||||
protected $lifetime = 1 * 86400;
|
||||
|
||||
public function getLifetime()
|
||||
{
|
||||
return $this->lifetime;
|
||||
}
|
||||
|
||||
public function getKey($id = null)
|
||||
{
|
||||
return 'index_simple_featured_course_list';
|
||||
}
|
||||
|
||||
public function getContent($id = null)
|
||||
{
|
||||
$limit = 8;
|
||||
|
||||
$courses = $this->findCourses($limit);
|
||||
|
||||
if ($courses->count() == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$result[] = [
|
||||
'id' => $course->id,
|
||||
'title' => $course->title,
|
||||
'cover' => $course->cover,
|
||||
'market_price' => $course->market_price,
|
||||
'vip_price' => $course->vip_price,
|
||||
'model' => $course->model,
|
||||
'level' => $course->level,
|
||||
'user_count' => $course->user_count,
|
||||
'lesson_count' => $course->lesson_count,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @return ResultsetInterface|Resultset|CourseModel[]
|
||||
*/
|
||||
protected function findCourses($limit = 8)
|
||||
{
|
||||
return CourseModel::query()
|
||||
->where('published = 1')
|
||||
->andWhere('featured = 1')
|
||||
->orderBy('id DESC')
|
||||
->limit($limit)
|
||||
->execute();
|
||||
}
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Tasks;
|
||||
|
||||
class CleanSessionTask extends Task
|
||||
{
|
||||
|
||||
public function mainAction()
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$redis->select($config->path('session.db'));
|
||||
|
||||
$keys = $this->querySessionKeys(10000);
|
||||
|
||||
if (count($keys) == 0) return;
|
||||
|
||||
$lifetime = $config->path('session.lifetime');
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$ttl = $redis->ttl($key);
|
||||
$content = $redis->get($key);
|
||||
if (empty($content) && $ttl < $lifetime * 0.5) {
|
||||
$redis->del($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找待清理会话
|
||||
*
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
protected function querySessionKeys($limit)
|
||||
{
|
||||
$cache = $this->getCache();
|
||||
|
||||
return $cache->queryKeys('_PHCR', $limit);
|
||||
}
|
||||
|
||||
}
|
@ -2,14 +2,9 @@
|
||||
|
||||
namespace App\Console\Tasks;
|
||||
|
||||
use App\Caches\IndexFreeCourseList as IndexFreeCourseListCache;
|
||||
use App\Caches\IndexNewCourseList as IndexNewCourseListCache;
|
||||
use App\Caches\IndexSimpleFreeCourseList as IndexSimpleFreeCourseListCache;
|
||||
use App\Caches\IndexSimpleNewCourseList as IndexSimpleNewCourseListCache;
|
||||
use App\Caches\IndexSimpleVipCourseList as IndexSimpleVipCourseListCache;
|
||||
use App\Caches\IndexVipCourseList as IndexVipCourseListCache;
|
||||
use App\Http\Admin\Services\Setting as SettingService;
|
||||
use App\Library\Utils\Password as PasswordUtil;
|
||||
use App\Services\Utils\IndexCourseCache as IndexCourseCacheUtil;
|
||||
use App\Validators\Account as AccountValidator;
|
||||
|
||||
class MaintainTask extends Task
|
||||
@ -25,39 +20,9 @@ class MaintainTask extends Task
|
||||
{
|
||||
$section = $params[0] ?? null;
|
||||
|
||||
$site = $this->getSettings('site');
|
||||
$util = new IndexCourseCacheUtil();
|
||||
|
||||
$type = $site['index_tpl_type'] ?: 'full';
|
||||
|
||||
if (!$section || $section == 'new_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexNewCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleNewCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$section || $section == 'free_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexFreeCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleFreeCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$section || $section == 'vip_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexVipCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleVipCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
$util->rebuild($section);
|
||||
|
||||
echo 'rebuild index course cache success' . PHP_EOL;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class SitemapTask extends Task
|
||||
|
||||
$this->sitemap = new Sitemap();
|
||||
|
||||
$filename = public_path('sitemap.xml');
|
||||
$filename = tmp_path('sitemap.xml');
|
||||
|
||||
$this->addIndex();
|
||||
$this->addCourses();
|
||||
|
44
app/Console/Tasks/SyncCourseScoreTask.php
Normal file
44
app/Console/Tasks/SyncCourseScoreTask.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Tasks;
|
||||
|
||||
use App\Repos\Course as CourseRepo;
|
||||
use App\Services\CourseStat as CourseStatService;
|
||||
use App\Services\Sync\CourseScore as CourseScoreSync;
|
||||
|
||||
class SyncCourseScoreTask extends Task
|
||||
{
|
||||
|
||||
public function mainAction()
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$key = $this->getSyncKey();
|
||||
|
||||
$courseIds = $redis->sRandMember($key, 1000);
|
||||
|
||||
if (!$courseIds) return;
|
||||
|
||||
$courseRepo = new CourseRepo();
|
||||
|
||||
$courses = $courseRepo->findByIds($courseIds);
|
||||
|
||||
if ($courses->count() == 0) return;
|
||||
|
||||
$statService = new CourseStatService();
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$statService->updateScore($course->id);
|
||||
}
|
||||
|
||||
$redis->sRem($key, ...$courseIds);
|
||||
}
|
||||
|
||||
protected function getSyncKey()
|
||||
{
|
||||
$sync = new CourseScoreSync();
|
||||
|
||||
return $sync->getSyncKey();
|
||||
}
|
||||
|
||||
}
|
@ -42,13 +42,9 @@ class UpgradeTask extends Task
|
||||
*/
|
||||
public function resetAnnotationAction()
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$dbIndex = $config->path('annotation.db');
|
||||
$statsKey = $config->path('annotation.statsKey');
|
||||
|
||||
$redis->select($dbIndex);
|
||||
$statsKey = '_ANNOTATION_';
|
||||
|
||||
$keys = $redis->sMembers($statsKey);
|
||||
|
||||
@ -70,13 +66,9 @@ class UpgradeTask extends Task
|
||||
*/
|
||||
public function resetMetadataAction()
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$dbIndex = $config->path('metadata.db');
|
||||
$statsKey = $config->path('metadata.statsKey');
|
||||
|
||||
$redis->select($dbIndex);
|
||||
$statsKey = '_METADATA_';
|
||||
|
||||
$keys = $redis->sMembers($statsKey);
|
||||
|
||||
|
30
app/Http/Admin/Controllers/UtilController.php
Normal file
30
app/Http/Admin/Controllers/UtilController.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Admin\Controllers;
|
||||
|
||||
use App\Http\Admin\Services\Util as UtilService;
|
||||
|
||||
/**
|
||||
* @RoutePrefix("/admin/util")
|
||||
*/
|
||||
class UtilController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* @Route("/index/cache", name="admin.util.index_cache")
|
||||
*/
|
||||
public function indexCacheAction()
|
||||
{
|
||||
$service = new UtilService();
|
||||
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
$service->handleIndexCache();
|
||||
|
||||
return $this->jsonSuccess(['msg' => '更新缓存成功']);
|
||||
}
|
||||
|
||||
$this->view->pick('util/index_cache');
|
||||
}
|
||||
|
||||
}
|
@ -14,6 +14,7 @@ class AuthNode extends Service
|
||||
$nodes[] = $this->getFinanceNodes();
|
||||
$nodes[] = $this->getUserNodes();
|
||||
$nodes[] = $this->getSettingNodes();
|
||||
$nodes[] = $this->getUtilNodes();
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
@ -768,4 +769,27 @@ class AuthNode extends Service
|
||||
];
|
||||
}
|
||||
|
||||
protected function getUtilNodes()
|
||||
{
|
||||
return [
|
||||
'id' => '6',
|
||||
'title' => '实用工具',
|
||||
'children' => [
|
||||
[
|
||||
'id' => '6-1',
|
||||
'title' => '常用工具',
|
||||
'type' => 'menu',
|
||||
'children' => [
|
||||
[
|
||||
'id' => '6-1-1',
|
||||
'title' => '首页缓存',
|
||||
'type' => 'menu',
|
||||
'route' => 'admin.util.index_cache',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -172,6 +172,10 @@ class Course extends Service
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($post['featured'])) {
|
||||
$data['featured'] = $validator->checkFeatureStatus($post['featured']);
|
||||
}
|
||||
|
||||
if (isset($post['published'])) {
|
||||
$data['published'] = $validator->checkPublishStatus($post['published']);
|
||||
if ($post['published'] == 1) {
|
||||
|
40
app/Http/Admin/Services/Util.php
Normal file
40
app/Http/Admin/Services/Util.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Admin\Services;
|
||||
|
||||
use App\Caches\IndexSlideList as IndexSlideListCache;
|
||||
use App\Services\Utils\IndexCourseCache as IndexCourseCacheUtil;
|
||||
|
||||
class Util extends Service
|
||||
{
|
||||
|
||||
public function handleIndexCache()
|
||||
{
|
||||
$items = $this->request->getPost('items');
|
||||
|
||||
if ($items['slide'] == 1) {
|
||||
$cache = new IndexSlideListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
|
||||
$util = new IndexCourseCacheUtil();
|
||||
|
||||
if ($items['featured_course'] == 1) {
|
||||
$util->rebuild('featured_course');
|
||||
}
|
||||
|
||||
if ($items['new_course'] == 1) {
|
||||
$util->rebuild('new_course');
|
||||
}
|
||||
|
||||
if ($items['free_course'] == 1) {
|
||||
$util->rebuild('free_course');
|
||||
}
|
||||
|
||||
if ($items['vip_course'] == 1) {
|
||||
$util->rebuild('vip_course');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -59,6 +59,7 @@
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col width="10%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
@ -67,6 +68,7 @@
|
||||
<th>课时数</th>
|
||||
<th>用户数</th>
|
||||
<th>价格</th>
|
||||
<th>推荐</th>
|
||||
<th>发布</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@ -101,6 +103,7 @@
|
||||
<p>市场:{{ '¥%0.2f'|format(item.market_price) }}</p>
|
||||
<p>会员:{{ '¥%0.2f'|format(item.vip_price) }}</p>
|
||||
</td>
|
||||
<td><input type="checkbox" name="featured" value="1" lay-skin="switch" lay-text="是|否" lay-filter="featured" data-url="{{ update_url }}" {% if item.featured == 1 %}checked="checked"{% endif %}></td>
|
||||
<td><input type="checkbox" name="published" value="1" lay-skin="switch" lay-text="是|否" lay-filter="published" data-url="{{ update_url }}" {% if item.published == 1 %}checked="checked"{% endif %}></td>
|
||||
<td class="center">
|
||||
<div class="layui-dropdown">
|
||||
@ -129,4 +132,45 @@
|
||||
|
||||
{{ partial('partials/pager') }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block inline_js %}
|
||||
|
||||
<script>
|
||||
|
||||
layui.define(['jquery', 'form', 'layer'], function () {
|
||||
|
||||
var $ = layui.jquery;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
form.on('switch(featured)', function (data) {
|
||||
var checked = $(this).is(':checked');
|
||||
var featured = checked ? 1 : 0;
|
||||
var url = $(this).data('url');
|
||||
var tips = featured === 1 ? '确定要推荐?' : '确定要取消推荐?';
|
||||
layer.confirm(tips, function () {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: {featured: featured},
|
||||
success: function (res) {
|
||||
layer.msg(res.msg, {icon: 1});
|
||||
},
|
||||
error: function (xhr) {
|
||||
var json = JSON.parse(xhr.responseText);
|
||||
layer.msg(json.msg, {icon: 2});
|
||||
data.elem.checked = !checked;
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
}, function () {
|
||||
data.elem.checked = !checked;
|
||||
form.render();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -54,6 +54,13 @@
|
||||
<input type="radio" name="free" value="0" title="否">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">推荐</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="featured" value="1" title="是">
|
||||
<input type="radio" name="featured" value="0" title="否">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">发布</label>
|
||||
<div class="layui-input-block">
|
||||
|
89
app/Http/Admin/Views/util/index_cache.volt
Normal file
89
app/Http/Admin/Views/util/index_cache.volt
Normal file
@ -0,0 +1,89 @@
|
||||
{% extends 'templates/main.volt' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.util.index_cache'}) }}">
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>首页缓存</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">轮播图</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="items[slide]" value="1" title="是">
|
||||
<input type="radio" name="items[slide]" value="0" title="否" checked="checked">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">推荐课程</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="items[featured_course]" value="1" title="是">
|
||||
<input type="radio" name="items[featured_course]" value="0" title="否" checked="checked">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新上课程</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="items[new_course]" value="1" title="是">
|
||||
<input type="radio" name="items[new_course]" value="0" title="否" checked="checked">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">免费课程</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="items[free_course]" value="1" title="是">
|
||||
<input type="radio" name="items[free_course]" value="0" title="否" checked="checked">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">会员课程</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="items[vip_course]" value="1" title="是">
|
||||
<input type="radio" name="items[vip_course]" value="0" title="否" checked="checked">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"></label>
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit="true" lay-filter="go">刷新</button>
|
||||
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block inline_js %}
|
||||
|
||||
<script>
|
||||
|
||||
layui.use(['jquery', 'form', 'layer'], function () {
|
||||
|
||||
var $ = layui.jquery;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
form.on('submit(back_verify)', function (data) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: data.form.action,
|
||||
data: data.field,
|
||||
success: function (res) {
|
||||
if (res.code === 0) {
|
||||
$('#back-verify-btn').remove();
|
||||
$('#back-verify-tips').removeClass('layui-hide');
|
||||
}
|
||||
layer.msg(res.msg, {icon: 1});
|
||||
},
|
||||
error: function (xhr) {
|
||||
var json = JSON.parse(xhr.responseText);
|
||||
layer.msg(json.msg, {icon: 2});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Api\Controllers;
|
||||
|
||||
use App\Caches\IndexSimpleFeaturedCourseList;
|
||||
use App\Caches\IndexSimpleFreeCourseList;
|
||||
use App\Caches\IndexSimpleNewCourseList;
|
||||
use App\Caches\IndexSimpleVipCourseList;
|
||||
@ -25,6 +26,18 @@ class IndexController extends Controller
|
||||
return $this->jsonSuccess(['slides' => $slides]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Get("/courses/featured", name="api.index.featured_courses")
|
||||
*/
|
||||
public function featuredCoursesAction()
|
||||
{
|
||||
$cache = new IndexSimpleFeaturedCourseList();
|
||||
|
||||
$courses = $cache->get();
|
||||
|
||||
return $this->jsonSuccess(['courses' => $courses]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Get("/courses/new", name="api.index.new_courses")
|
||||
*/
|
||||
|
@ -47,6 +47,7 @@ class IndexController extends Controller
|
||||
$this->view->pick('index/full');
|
||||
$this->view->setVar('lives', $service->getLives());
|
||||
$this->view->setVar('slides', $service->getSlides());
|
||||
$this->view->setVar('featured_courses', $service->getFeaturedCourses());
|
||||
$this->view->setVar('new_courses', $service->getNewCourses());
|
||||
$this->view->setVar('free_courses', $service->getFreeCourses());
|
||||
$this->view->setVar('vip_courses', $service->getVipCourses());
|
||||
@ -59,6 +60,7 @@ class IndexController extends Controller
|
||||
$this->view->pick('index/simple');
|
||||
$this->view->setVar('lives', $service->getLives());
|
||||
$this->view->setVar('slides', $service->getSlides());
|
||||
$this->view->setVar('featured_courses', $service->getSimpleFeaturedCourses());
|
||||
$this->view->setVar('new_courses', $service->getSimpleNewCourses());
|
||||
$this->view->setVar('free_courses', $service->getSimpleFreeCourses());
|
||||
$this->view->setVar('vip_courses', $service->getSimpleVipCourses());
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Http\Home\Services;
|
||||
|
||||
use App\Caches\IndexFeaturedCourseList;
|
||||
use App\Caches\IndexFreeCourseList;
|
||||
use App\Caches\IndexLiveList;
|
||||
use App\Caches\IndexNewCourseList;
|
||||
use App\Caches\IndexSimpleFeaturedCourseList;
|
||||
use App\Caches\IndexSimpleFreeCourseList;
|
||||
use App\Caches\IndexSimpleNewCourseList;
|
||||
use App\Caches\IndexSimpleVipCourseList;
|
||||
@ -61,6 +63,15 @@ class Index extends Service
|
||||
return $cache->get();
|
||||
}
|
||||
|
||||
public function getFeaturedCourses()
|
||||
{
|
||||
$cache = new IndexFeaturedCourseList();
|
||||
|
||||
$courses = $cache->get();
|
||||
|
||||
return $this->handleCategoryCourses($courses);
|
||||
}
|
||||
|
||||
public function getNewCourses()
|
||||
{
|
||||
$cache = new IndexNewCourseList();
|
||||
@ -95,6 +106,13 @@ class Index extends Service
|
||||
return $cache->get();
|
||||
}
|
||||
|
||||
public function getSimpleFeaturedCourses()
|
||||
{
|
||||
$cache = new IndexSimpleFeaturedCourseList();
|
||||
|
||||
return $cache->get();
|
||||
}
|
||||
|
||||
public function getSimpleFreeCourses()
|
||||
{
|
||||
$cache = new IndexSimpleFreeCourseList();
|
||||
|
@ -45,6 +45,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="index-wrap wrap">
|
||||
<div class="header">推荐课程</div>
|
||||
<div class="content">
|
||||
{{ category_courses(featured_courses) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="index-wrap wrap">
|
||||
<div class="header">新上课程</div>
|
||||
<div class="content">
|
||||
|
@ -30,6 +30,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="index-wrap wrap">
|
||||
<div class="header">推荐课程</div>
|
||||
<div class="content">
|
||||
{{ show_courses(featured_courses) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="index-wrap wrap">
|
||||
<div class="header">新上课程</div>
|
||||
<div class="content">
|
||||
|
@ -11,7 +11,7 @@ class AppInfo
|
||||
|
||||
protected $link = 'https://gitee.com/koogua';
|
||||
|
||||
protected $version = '1.2.2';
|
||||
protected $version = '1.2.3';
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
|
79
app/Library/Utils/Lock.php
Normal file
79
app/Library/Utils/Lock.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Library\Utils;
|
||||
|
||||
use App\Library\Cache\Backend\Redis as RedisCache;
|
||||
use Phalcon\Di;
|
||||
use Phalcon\Text;
|
||||
|
||||
class Lock
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $itemId
|
||||
* @param int $expire
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function addLock($itemId, $expire = 600)
|
||||
{
|
||||
if (!$itemId || $expire <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var RedisCache $cache
|
||||
*/
|
||||
$cache = Di::getDefault()->getShared('cache');
|
||||
|
||||
$redis = $cache->getRedis();
|
||||
|
||||
$lockId = Text::random(Text::RANDOM_ALNUM, 16);
|
||||
|
||||
$keyName = self::getLockKey($itemId);
|
||||
|
||||
$result = $redis->set($keyName, $lockId, ['nx', 'ex' => $expire]);
|
||||
|
||||
return $result ? $lockId : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $itemId
|
||||
* @param string $lockId
|
||||
* @return bool
|
||||
*/
|
||||
public static function releaseLock($itemId, $lockId)
|
||||
{
|
||||
if (!$itemId || !$lockId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var RedisCache $cache
|
||||
*/
|
||||
$cache = Di::getDefault()->getShared('cache');
|
||||
|
||||
$redis = $cache->getRedis();
|
||||
|
||||
$keyName = self::getLockKey($itemId);
|
||||
|
||||
$redis->watch($keyName);
|
||||
|
||||
/**
|
||||
* 监听key防止被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
|
||||
*/
|
||||
if ($lockId == $redis->get($keyName)) {
|
||||
$redis->multi()->del($keyName)->exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
$redis->unwatch();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getLockKey($itemId)
|
||||
{
|
||||
return sprintf('kg_lock:%s', $itemId);
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Library\Utils\Lock as LockUtil;
|
||||
use App\Models\Online as OnlineModel;
|
||||
use App\Models\User as UserModel;
|
||||
use App\Repos\Online as OnlineRepo;
|
||||
@ -15,7 +16,13 @@ class User extends Listener
|
||||
|
||||
public function online(Event $event, $source, UserModel $user)
|
||||
{
|
||||
$itemId = "user:{$user->id}";
|
||||
|
||||
$lockId = LockUtil::addLock($itemId);
|
||||
|
||||
$now = time();
|
||||
$clientType = $this->getClientType();
|
||||
$clientIp = $this->getClientIp();
|
||||
|
||||
if ($now - $user->active_time > 600) {
|
||||
|
||||
@ -25,28 +32,46 @@ class User extends Listener
|
||||
|
||||
$onlineRepo = new OnlineRepo();
|
||||
|
||||
$online = $onlineRepo->findByUserDate($user->id, date('Y-m-d'));
|
||||
$records = $onlineRepo->findByUserDate($user->id, date('Y-m-d'));
|
||||
|
||||
if ($online) {
|
||||
if ($records->count() > 0) {
|
||||
|
||||
$online->active_time = $now;
|
||||
$online->client_type = $this->getClientType();
|
||||
$online->client_ip = $this->getClientIp();
|
||||
$online = null;
|
||||
|
||||
$online->update();
|
||||
foreach ($records as $record) {
|
||||
if ($record->client_type == $clientType && $record->client_ip == $clientIp) {
|
||||
$online = $record;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($online) {
|
||||
$online->active_time = $now;
|
||||
$online->update();
|
||||
} else {
|
||||
$this->createOnline($user->id, $clientType, $clientIp);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$online = new OnlineModel();
|
||||
|
||||
$online->user_id = $user->id;
|
||||
$online->active_time = $now;
|
||||
$online->client_type = $this->getClientType();
|
||||
$online->client_ip = $this->getClientIp();
|
||||
|
||||
$online->create();
|
||||
$this->createOnline($user->id, $clientType, $clientIp);
|
||||
}
|
||||
}
|
||||
|
||||
LockUtil::releaseLock($itemId, $lockId);
|
||||
}
|
||||
|
||||
protected function createOnline($userId, $clientType, $clientIp)
|
||||
{
|
||||
$online = new OnlineModel();
|
||||
|
||||
$online->user_id = $userId;
|
||||
$online->client_type = $clientType;
|
||||
$online->client_ip = $clientIp;
|
||||
$online->active_time = time();
|
||||
|
||||
$online->create();
|
||||
|
||||
return $online;
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Caches\MaxCourseId as MaxCourseIdCache;
|
||||
use App\Services\Sync\CourseIndex as CourseIndexSync;
|
||||
use App\Services\Sync\CourseScore as CourseScoreSync;
|
||||
use Phalcon\Mvc\Model\Behavior\SoftDelete;
|
||||
use Phalcon\Text;
|
||||
|
||||
@ -165,6 +166,13 @@ class Course extends Model
|
||||
*/
|
||||
public $attrs;
|
||||
|
||||
/**
|
||||
* 推荐标识
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $featured;
|
||||
|
||||
/**
|
||||
* 发布标识
|
||||
*
|
||||
@ -302,6 +310,9 @@ class Course extends Model
|
||||
if (time() - $this->update_time > 3 * 3600) {
|
||||
$sync = new CourseIndexSync();
|
||||
$sync->addItem($this->id);
|
||||
|
||||
$sync = new CourseScoreSync();
|
||||
$sync->addItem($this->id);
|
||||
}
|
||||
|
||||
if (Text::startsWith($this->cover, 'http')) {
|
||||
@ -377,6 +388,7 @@ class Course extends Model
|
||||
'rating' => '好评',
|
||||
'latest' => '最新',
|
||||
'popular' => '最热',
|
||||
'featured' => '推荐',
|
||||
'free' => '免费',
|
||||
];
|
||||
}
|
||||
|
@ -19,13 +19,6 @@ class Online extends Model
|
||||
*/
|
||||
public $user_id;
|
||||
|
||||
/**
|
||||
* 计划编号
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $date;
|
||||
|
||||
/**
|
||||
* 客户端类型
|
||||
*
|
||||
|
93
app/Models/UserSession.php
Normal file
93
app/Models/UserSession.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Phalcon\Mvc\Model\Behavior\SoftDelete;
|
||||
|
||||
class UserSession extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* 主键编号
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $user_id;
|
||||
|
||||
/**
|
||||
* 会话编号
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $session_id;
|
||||
|
||||
/**
|
||||
* 终端类型
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $client_type;
|
||||
|
||||
/**
|
||||
* 终端IP
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $client_ip;
|
||||
|
||||
/**
|
||||
* 删除标识
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $deleted;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $create_time;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $update_time;
|
||||
|
||||
public function getSource(): string
|
||||
{
|
||||
return 'kg_user_session';
|
||||
}
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$this->addBehavior(
|
||||
new SoftDelete([
|
||||
'field' => 'deleted',
|
||||
'value' => 1,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function beforeCreate()
|
||||
{
|
||||
$this->create_time = time();
|
||||
}
|
||||
|
||||
public function beforeUpdate()
|
||||
{
|
||||
$this->update_time = time();
|
||||
}
|
||||
|
||||
}
|
93
app/Models/UserToken.php
Normal file
93
app/Models/UserToken.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Phalcon\Mvc\Model\Behavior\SoftDelete;
|
||||
|
||||
class UserToken extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* 主键编号
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $user_id;
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* 终端类型
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $client_type;
|
||||
|
||||
/**
|
||||
* 终端IP
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $client_ip;
|
||||
|
||||
/**
|
||||
* 删除标识
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $deleted;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $create_time;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $update_time;
|
||||
|
||||
public function getSource(): string
|
||||
{
|
||||
return 'kg_user_token';
|
||||
}
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$this->addBehavior(
|
||||
new SoftDelete([
|
||||
'field' => 'deleted',
|
||||
'value' => 1,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function beforeCreate()
|
||||
{
|
||||
$this->create_time = time();
|
||||
}
|
||||
|
||||
public function beforeUpdate()
|
||||
{
|
||||
$this->update_time = time();
|
||||
}
|
||||
|
||||
}
|
@ -23,13 +23,14 @@ class Annotation extends Provider
|
||||
if ($config->get('env') == ENV_DEV) {
|
||||
$annotations = new MemoryAnnotations();
|
||||
} else {
|
||||
$statsKey = '_ANNOTATION_';
|
||||
$annotations = new RedisAnnotations([
|
||||
'host' => $config->path('redis.host'),
|
||||
'port' => $config->path('redis.port'),
|
||||
'auth' => $config->path('redis.auth'),
|
||||
'index' => $config->path('annotation.db'),
|
||||
'lifetime' => $config->path('annotation.lifetime'),
|
||||
'statsKey' => $config->path('annotation.statsKey'),
|
||||
'lifetime' => $config->path('annotation.lifetime') ?: 30 * 86400,
|
||||
'prefix' => $statsKey . ':',
|
||||
'statsKey' => $statsKey,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -23,13 +23,14 @@ class MetaData extends Provider
|
||||
if ($config->get('env') == ENV_DEV) {
|
||||
$metaData = new MemoryMetaData();
|
||||
} else {
|
||||
$statsKey = '_METADATA_';
|
||||
$metaData = new RedisMetaData([
|
||||
'host' => $config->path('redis.host'),
|
||||
'port' => $config->path('redis.port'),
|
||||
'auth' => $config->path('redis.auth'),
|
||||
'index' => $config->path('metadata.db'),
|
||||
'statsKey' => $config->path('metadata.statsKey'),
|
||||
'lifetime' => $config->path('metadata.lifetime'),
|
||||
'lifetime' => $config->path('metadata.lifetime') ?: 30 * 86400,
|
||||
'prefix' => $statsKey . ':',
|
||||
'statsKey' => $statsKey,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,8 @@ class Session extends Provider
|
||||
'host' => $config->path('redis.host'),
|
||||
'port' => $config->path('redis.port'),
|
||||
'auth' => $config->path('redis.auth'),
|
||||
'index' => $config->path('session.db'),
|
||||
'lifetime' => $config->path('session.lifetime'),
|
||||
'lifetime' => $config->path('session.lifetime') ?: 24 * 3600,
|
||||
'prefix' => '_SESSION_:',
|
||||
]);
|
||||
|
||||
$session->start();
|
||||
|
@ -67,6 +67,10 @@ class Course extends Repository
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($where['featured'])) {
|
||||
$builder->andWhere('featured = :featured:', ['featured' => $where['featured']]);
|
||||
}
|
||||
|
||||
if (isset($where['published'])) {
|
||||
$builder->andWhere('published = :published:', ['published' => $where['published']]);
|
||||
}
|
||||
@ -77,6 +81,8 @@ class Course extends Repository
|
||||
|
||||
if ($sort == 'free') {
|
||||
$builder->andWhere('market_price = 0');
|
||||
} elseif ($sort == 'featured') {
|
||||
$builder->andWhere('featured = 1');
|
||||
} elseif ($sort == 'vip_discount') {
|
||||
$builder->andWhere('vip_price < market_price');
|
||||
$builder->andWhere('vip_price > 0');
|
||||
|
@ -3,7 +3,8 @@
|
||||
namespace App\Repos;
|
||||
|
||||
use App\Models\Online as OnlineModel;
|
||||
use Phalcon\Mvc\Model;
|
||||
use Phalcon\Mvc\Model\Resultset;
|
||||
use Phalcon\Mvc\Model\ResultsetInterface;
|
||||
|
||||
class Online extends Repository
|
||||
{
|
||||
@ -11,20 +12,18 @@ class Online extends Repository
|
||||
/**
|
||||
* @param int $userId
|
||||
* @param string $activeDate
|
||||
* @return OnlineModel|Model|bool
|
||||
* @return ResultsetInterface|Resultset|OnlineModel[]
|
||||
*/
|
||||
public function findByUserDate($userId, $activeDate)
|
||||
{
|
||||
$activeTime = strtotime($activeDate);
|
||||
$startTime = strtotime($activeDate);
|
||||
|
||||
return OnlineModel::findFirst([
|
||||
'conditions' => 'user_id = ?1 AND active_time BETWEEN ?2 AND ?3',
|
||||
'bind' => [
|
||||
1 => $userId,
|
||||
2 => $activeTime,
|
||||
3 => $activeTime + 86400,
|
||||
],
|
||||
]);
|
||||
$endTime = $startTime + 86400;
|
||||
|
||||
return OnlineModel::query()
|
||||
->where('user_id = :user_id:', ['user_id' => $userId])
|
||||
->betweenWhere('active_time', $startTime, $endTime)
|
||||
->execute();
|
||||
}
|
||||
|
||||
}
|
24
app/Repos/UserSession.php
Normal file
24
app/Repos/UserSession.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repos;
|
||||
|
||||
use App\Models\UserSession as UserSessionModel;
|
||||
use Phalcon\Mvc\Model\Resultset;
|
||||
use Phalcon\Mvc\Model\ResultsetInterface;
|
||||
|
||||
class UserSession extends Repository
|
||||
{
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
* @return ResultsetInterface|Resultset|UserSessionModel[]
|
||||
*/
|
||||
public function findByUserId($userId)
|
||||
{
|
||||
return UserSessionModel::query()
|
||||
->where('user_id = :user_id:', ['user_id' => $userId])
|
||||
->andWhere('deleted = 0')
|
||||
->execute();
|
||||
}
|
||||
|
||||
}
|
24
app/Repos/UserToken.php
Normal file
24
app/Repos/UserToken.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repos;
|
||||
|
||||
use App\Models\UserToken as UserTokenModel;
|
||||
use Phalcon\Mvc\Model\Resultset;
|
||||
use Phalcon\Mvc\Model\ResultsetInterface;
|
||||
|
||||
class UserToken extends Repository
|
||||
{
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
* @return ResultsetInterface|Resultset|UserTokenModel[]
|
||||
*/
|
||||
public function findByUserId($userId)
|
||||
{
|
||||
return UserTokenModel::query()
|
||||
->where('user_id = :user_id:', ['user_id' => $userId])
|
||||
->andWhere('deleted = 0')
|
||||
->execute();
|
||||
}
|
||||
|
||||
}
|
@ -3,70 +3,114 @@
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\User as UserModel;
|
||||
use App\Models\UserToken as UserTokenModel;
|
||||
use App\Repos\UserToken as UserTokenRepo;
|
||||
use App\Services\Auth as AuthService;
|
||||
use Lcobucci\JWT\Builder as JwtBuilder;
|
||||
use Lcobucci\JWT\Parser as JwtParser;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256 as JwtSingerSha256;
|
||||
use Lcobucci\JWT\Signer\Key as JwtSingerKey;
|
||||
use Lcobucci\JWT\ValidationData as JwtValidationData;
|
||||
use App\Traits\Client as ClientTrait;
|
||||
|
||||
class Api extends AuthService
|
||||
{
|
||||
|
||||
use ClientTrait;
|
||||
|
||||
public function saveAuthInfo(UserModel $user)
|
||||
{
|
||||
$builder = new JwtBuilder();
|
||||
$token = $this->generateToken($user->id);
|
||||
|
||||
$this->logoutOtherClients($user->id);
|
||||
|
||||
$this->createUserToken($user->id, $token);
|
||||
|
||||
$cache = $this->getCache();
|
||||
|
||||
$key = $this->getTokenCacheKey($token);
|
||||
|
||||
$authInfo = [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
];
|
||||
|
||||
$config = $this->getConfig();
|
||||
|
||||
$expireTime = time() + $config->path('jwt.lifetime');
|
||||
$lifetime = $config->path('token.lifetime') ?: 7 * 86400;
|
||||
|
||||
$builder->expiresAt($expireTime);
|
||||
$builder->withClaim('user_id', $user->id);
|
||||
$builder->withClaim('user_name', $user->name);
|
||||
$cache->save($key, $authInfo, $lifetime);
|
||||
|
||||
$singer = new JwtSingerSha256();
|
||||
|
||||
$key = new JwtSingerKey($config->path('jwt.key'));
|
||||
|
||||
$token = $builder->getToken($singer, $key);
|
||||
|
||||
return $token->__toString();
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function clearAuthInfo()
|
||||
{
|
||||
$token = $this->request->getHeader('X-Token');
|
||||
|
||||
if (empty($token)) return null;
|
||||
|
||||
$cache = $this->getCache();
|
||||
|
||||
$key = $this->getTokenCacheKey($token);
|
||||
|
||||
$cache->delete($key);
|
||||
}
|
||||
|
||||
public function getAuthInfo()
|
||||
{
|
||||
$authToken = $this->request->getHeader('X-Token');
|
||||
$token = $this->request->getHeader('X-Token');
|
||||
|
||||
if (!$authToken) return null;
|
||||
if (empty($token)) return null;
|
||||
|
||||
$config = $this->getConfig();
|
||||
$cache = $this->getCache();
|
||||
|
||||
$parser = new JWTParser();
|
||||
$key = $this->getTokenCacheKey($token);
|
||||
|
||||
$token = $parser->parse($authToken);
|
||||
$authInfo = $cache->get($key);
|
||||
|
||||
$data = new JWTValidationData(time(), $config->path('jwt.leeway'));
|
||||
return $authInfo ?: null;
|
||||
}
|
||||
|
||||
if (!$token->validate($data)) {
|
||||
return null;
|
||||
protected function createUserToken($userId, $token)
|
||||
{
|
||||
$userToken = new UserTokenModel();
|
||||
|
||||
$userToken->user_id = $userId;
|
||||
$userToken->token = $token;
|
||||
$userToken->client_type = $this->getClientType();
|
||||
$userToken->client_ip = $this->getClientIp();
|
||||
|
||||
$userToken->create();
|
||||
}
|
||||
|
||||
protected function logoutOtherClients($userId)
|
||||
{
|
||||
$repo = new UserTokenRepo();
|
||||
|
||||
$records = $repo->findByUserId($userId);
|
||||
|
||||
$cache = $this->getCache();
|
||||
|
||||
$clientType = $this->getClientType();
|
||||
|
||||
if ($records->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$singer = new JwtSingerSha256();
|
||||
|
||||
if (!$token->verify($singer, $config->path('jwt.key'))) {
|
||||
return null;
|
||||
foreach ($records as $record) {
|
||||
if ($record->client_type == $clientType) {
|
||||
$record->deleted = 1;
|
||||
$record->update();
|
||||
$key = $this->getTokenCacheKey($record->token);
|
||||
$cache->delete($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $token->getClaim('user_id'),
|
||||
'name' => $token->getClaim('user_name'),
|
||||
];
|
||||
protected function generateToken($userId)
|
||||
{
|
||||
return md5(uniqid() . time() . $userId);
|
||||
}
|
||||
|
||||
protected function getTokenCacheKey($token)
|
||||
{
|
||||
return "_PHCR_TOKEN_:{$token}";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,24 @@
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\User as UserModel;
|
||||
use App\Models\UserSession as UserSessionModel;
|
||||
use App\Repos\UserSession as UserSessionRepo;
|
||||
use App\Services\Auth as AuthService;
|
||||
use App\Traits\Client as ClientTrait;
|
||||
|
||||
class Home extends AuthService
|
||||
{
|
||||
|
||||
use ClientTrait;
|
||||
|
||||
public function saveAuthInfo(UserModel $user)
|
||||
{
|
||||
$sessionId = $this->session->getId();
|
||||
|
||||
$this->logoutOtherClients($user->id);
|
||||
|
||||
$this->createUserSession($user->id, $sessionId);
|
||||
|
||||
$authKey = $this->getAuthKey();
|
||||
|
||||
$authInfo = [
|
||||
@ -41,4 +52,41 @@ class Home extends AuthService
|
||||
return 'home_auth_info';
|
||||
}
|
||||
|
||||
protected function createUserSession($userId, $sessionId)
|
||||
{
|
||||
$userSession = new UserSessionModel();
|
||||
|
||||
$userSession->user_id = $userId;
|
||||
$userSession->session_id = $sessionId;
|
||||
$userSession->client_type = $this->getClientType();
|
||||
$userSession->client_ip = $this->getClientIp();
|
||||
|
||||
$userSession->create();
|
||||
}
|
||||
|
||||
protected function logoutOtherClients($userId)
|
||||
{
|
||||
$cache = $this->getCache();
|
||||
|
||||
$repo = new UserSessionRepo();
|
||||
|
||||
$records = $repo->findByUserId($userId);
|
||||
|
||||
if ($records->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
$record->deleted = 1;
|
||||
$record->update();
|
||||
$key = $this->getSessionCacheKey($record->session_id);
|
||||
$cache->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSessionCacheKey($sessionId)
|
||||
{
|
||||
return "_PHCR_SESSION_:{$sessionId}";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Course as CourseModel;
|
||||
use App\Repos\Course as CourseRepo;
|
||||
use App\Repos\CourseRating as CourseRatingRepo;
|
||||
|
||||
@ -58,9 +59,19 @@ class CourseStat extends Service
|
||||
|
||||
public function updateScore($courseId)
|
||||
{
|
||||
/**
|
||||
* @todo 计算综合评分
|
||||
*/
|
||||
$courseRepo = new CourseRepo();
|
||||
|
||||
$course = $courseRepo->findById($courseId);
|
||||
|
||||
if ($course->market_price == 0) {
|
||||
$score = $this->calculateFreeCourseScore($course);
|
||||
} else {
|
||||
$score = $this->calculateChargeCourseScore($course);
|
||||
}
|
||||
|
||||
$course->score = $score;
|
||||
|
||||
$course->update();
|
||||
}
|
||||
|
||||
public function updateReadAttrs($courseId)
|
||||
@ -181,4 +192,72 @@ class CourseStat extends Service
|
||||
$course->update(['attrs' => $attrs]);
|
||||
}
|
||||
|
||||
protected function calculateFreeCourseScore(CourseModel $course)
|
||||
{
|
||||
$weight = [
|
||||
'factor1' => 0.1,
|
||||
'factor2' => 0.25,
|
||||
'factor3' => 0.2,
|
||||
'factor4' => 0.1,
|
||||
'factor5' => 0.25,
|
||||
'factor6' => 0.1,
|
||||
];
|
||||
|
||||
return $this->calculateCourseScore($course, $weight);
|
||||
}
|
||||
|
||||
protected function calculateChargeCourseScore(CourseModel $course)
|
||||
{
|
||||
$weight = [
|
||||
'factor1' => 0.1,
|
||||
'factor2' => 0.3,
|
||||
'factor3' => 0.15,
|
||||
'factor4' => 0.15,
|
||||
'factor5' => 0.2,
|
||||
'factor6' => 0.1,
|
||||
];
|
||||
|
||||
return $this->calculateCourseScore($course, $weight);
|
||||
}
|
||||
|
||||
protected function calculateCourseScore(CourseModel $course, $weight)
|
||||
{
|
||||
$items = [
|
||||
'factor1' => 0.0,
|
||||
'factor2' => 0.0,
|
||||
'factor3' => 0.0,
|
||||
'factor4' => 0.0,
|
||||
'factor5' => 0.0,
|
||||
'factor6' => 0.0,
|
||||
];
|
||||
|
||||
$items['factor1'] = ($course->featured == 1 ? 1 : 0) * 10 * $weight['factor1'];
|
||||
|
||||
if ($course->user_count > 0) {
|
||||
$items['factor2'] = log($course->user_count) * $weight['factor2'];
|
||||
}
|
||||
|
||||
if ($course->favorite_count > 0) {
|
||||
$items['factor3'] = log($course->favorite_count) * $weight['factor3'];
|
||||
}
|
||||
|
||||
if ($course->consult_count > 0) {
|
||||
$items['factor4'] = log($course->consult_count) * $weight['factor4'];
|
||||
}
|
||||
|
||||
if ($course->review_count > 0 && $course->rating > 0) {
|
||||
$items['factor5'] = log($course->review_count * $course->rating) * $weight['factor5'];
|
||||
}
|
||||
|
||||
$sumCount = $course->lesson_count + $course->package_count + $course->resource_count;
|
||||
|
||||
if ($sumCount > 0) {
|
||||
$items['factor6'] = log($sumCount) * $weight['factor6'];
|
||||
}
|
||||
|
||||
$score = array_sum($items) / log(time() - $course->create_time);
|
||||
|
||||
return round($score, 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
33
app/Services/Sync/CourseScore.php
Normal file
33
app/Services/Sync/CourseScore.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Sync;
|
||||
|
||||
use App\Services\Service;
|
||||
|
||||
class CourseScore extends Service
|
||||
{
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $lifetime = 86400;
|
||||
|
||||
public function addItem($courseId)
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
$key = $this->getSyncKey();
|
||||
|
||||
$redis->sAdd($key, $courseId);
|
||||
|
||||
if ($redis->sCard($key) == 1) {
|
||||
$redis->expire($key, $this->lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSyncKey()
|
||||
{
|
||||
return 'sync_course_score';
|
||||
}
|
||||
|
||||
}
|
65
app/Services/Utils/IndexCourseCache.php
Normal file
65
app/Services/Utils/IndexCourseCache.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Utils;
|
||||
|
||||
use App\Caches\IndexFeaturedCourseList as IndexFeaturedCourseListCache;
|
||||
use App\Caches\IndexFreeCourseList as IndexFreeCourseListCache;
|
||||
use App\Caches\IndexNewCourseList as IndexNewCourseListCache;
|
||||
use App\Caches\IndexSimpleFeaturedCourseList as IndexSimpleFeaturedCourseListCache;
|
||||
use App\Caches\IndexSimpleFreeCourseList as IndexSimpleFreeCourseListCache;
|
||||
use App\Caches\IndexSimpleNewCourseList as IndexSimpleNewCourseListCache;
|
||||
use App\Caches\IndexSimpleVipCourseList as IndexSimpleVipCourseListCache;
|
||||
use App\Caches\IndexVipCourseList as IndexVipCourseListCache;
|
||||
use App\Services\Service;
|
||||
|
||||
class IndexCourseCache extends Service
|
||||
{
|
||||
|
||||
public function rebuild($section = null)
|
||||
{
|
||||
$site = $this->getSettings('site');
|
||||
|
||||
$type = $site['index_tpl_type'] ?: 'full';
|
||||
|
||||
if (!$section || $section == 'featured_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexFeaturedCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleFeaturedCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$section || $section == 'new_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexNewCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleNewCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$section || $section == 'free_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexFreeCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleFreeCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$section || $section == 'vip_course') {
|
||||
if ($type == 'full') {
|
||||
$cache = new IndexVipCourseListCache();
|
||||
$cache->rebuild();
|
||||
} else {
|
||||
$cache = new IndexSimpleVipCourseListCache();
|
||||
$cache->rebuild();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,14 @@ trait Client
|
||||
*/
|
||||
$request = Di::getDefault()->get('request');
|
||||
|
||||
$platform = $request->getHeader('X-Platform');
|
||||
|
||||
$types = array_flip(ClientModel::types());
|
||||
|
||||
if (!empty($platform) && isset($types[$platform])) {
|
||||
return $types[$platform];
|
||||
}
|
||||
|
||||
$userAgent = $request->getServer('HTTP_USER_AGENT');
|
||||
|
||||
$result = new BrowserParser($userAgent);
|
||||
|
@ -212,6 +212,15 @@ class Course extends Validator
|
||||
return $expiry;
|
||||
}
|
||||
|
||||
public function checkFeatureStatus($status)
|
||||
{
|
||||
if (!in_array($status, [0, 1])) {
|
||||
throw new BadRequestException('course.invalid_feature_status');
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function checkPublishStatus($status)
|
||||
{
|
||||
if (!in_array($status, [0, 1])) {
|
||||
|
@ -24,7 +24,7 @@
|
||||
"overtrue/wechat": "^4.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"odan/phinx-migrations-generator": "^5.1",
|
||||
"odan/phinx-migrations-generator": "^5.3",
|
||||
"phalcon/ide-stubs": "^3.4"
|
||||
},
|
||||
"repositories": {
|
||||
|
677
composer.lock
generated
677
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "60ff0e1868be7414a1b31d397ced7fbd",
|
||||
"content-hash": "09a618cffed2c4cfb593c0a791c19b3f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aferrandini/phpqrcode",
|
||||
@ -15,16 +15,16 @@
|
||||
"reference": "3c1c0454d43710ab5bbe19a51ad4cb41c22e3d46"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aferrandini/PHPQRCode/zipball/3c1c0454d43710ab5bbe19a51ad4cb41c22e3d46",
|
||||
"reference": "3c1c0454d43710ab5bbe19a51ad4cb41c22e3d46",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aferrandini/PHPQRCode/zipball/3c1c0454d43710ab5bbe19a51ad4cb41c22e3d46",
|
||||
"reference": "3c1c0454d43710ab5bbe19a51ad4cb41c22e3d46",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
@ -46,16 +46,16 @@
|
||||
"homepage": "http://www.ferrandini.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHPQRCode porting and changed for PHP 5.3 compatibility",
|
||||
"homepage": "https://github.com/aferrandini/PHPQRCode",
|
||||
"keywords": [
|
||||
"barcode",
|
||||
"php",
|
||||
"qrcode"
|
||||
],
|
||||
"abandoned": "endroid/qr-code",
|
||||
"time": "2013-07-08T09:39:08+00:00"
|
||||
],
|
||||
"description": "PHPQRCode porting and changed for PHP 5.3 compatibility",
|
||||
"homepage": "https://github.com/aferrandini/PHPQRCode",
|
||||
"keywords": [
|
||||
"barcode",
|
||||
"php",
|
||||
"qrcode"
|
||||
],
|
||||
"abandoned": "endroid/qr-code",
|
||||
"time": "2013-07-08T09:39:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "cakephp/core",
|
||||
@ -334,16 +334,16 @@
|
||||
"email": "guilhermeblanco@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Schmitt",
|
||||
"email": "schmittjoh@gmail.com"
|
||||
}
|
||||
{
|
||||
"name": "Johannes Schmitt",
|
||||
"email": "schmittjoh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
|
||||
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
|
||||
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
|
||||
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
|
||||
"keywords": [
|
||||
"annotations",
|
||||
"docblock",
|
||||
@ -418,16 +418,16 @@
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
|
||||
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"doctrine/lexer": "^1.0.1",
|
||||
"php": ">= 5.5"
|
||||
},
|
||||
@ -1066,16 +1066,16 @@
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
|
||||
"keywords": [
|
||||
"cron",
|
||||
@ -1242,16 +1242,16 @@
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
|
||||
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": "^7"
|
||||
},
|
||||
"require-dev": {
|
||||
@ -1399,15 +1399,15 @@
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phalcon Team",
|
||||
"email": "team@phalconphp.com",
|
||||
"homepage": "https://phalconphp.com/en/team"
|
||||
},
|
||||
{
|
||||
"name": "Contributors",
|
||||
"homepage": "https://github.com/phalcon/incubator/graphs/contributors"
|
||||
}
|
||||
{
|
||||
"name": "Phalcon Team",
|
||||
"email": "team@phalconphp.com",
|
||||
"homepage": "https://phalconphp.com/en/team"
|
||||
},
|
||||
{
|
||||
"name": "Contributors",
|
||||
"homepage": "https://github.com/phalcon/incubator/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Adapters, prototypes or functionality that can be potentially incorporated to the C-framework.",
|
||||
"homepage": "https://phalconphp.com",
|
||||
@ -1488,16 +1488,16 @@
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
|
||||
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
@ -1707,16 +1707,16 @@
|
||||
"reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
@ -1738,14 +1738,14 @@
|
||||
}
|
||||
],
|
||||
"description": "Common interfaces for simple caching",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"caching",
|
||||
"psr",
|
||||
"psr-16",
|
||||
"simple-cache"
|
||||
],
|
||||
"time": "2017-10-23T01:57:42+00:00"
|
||||
"keywords": [
|
||||
"cache",
|
||||
"caching",
|
||||
"psr",
|
||||
"psr-16",
|
||||
"simple-cache"
|
||||
],
|
||||
"time": "2017-10-23T01:57:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "qcloud/cos-sdk-v5",
|
||||
@ -2026,15 +2026,15 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Corbyn"
|
||||
},
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Corbyn"
|
||||
},
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
}
|
||||
],
|
||||
"description": "Swiftmailer, free feature-rich PHP mailer",
|
||||
"homepage": "https://swiftmailer.symfony.com",
|
||||
"keywords": [
|
||||
@ -2241,16 +2241,16 @@
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/config/zipball/cf63f0613a6c6918e96db39c07a43b01e19a0773",
|
||||
"reference": "cf63f0613a6c6918e96db39c07a43b01e19a0773",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"reference": "cf63f0613a6c6918e96db39c07a43b01e19a0773",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1",
|
||||
"symfony/filesystem": "^4.4|^5.0",
|
||||
@ -2403,15 +2403,15 @@
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-06T13:18:39+00:00"
|
||||
"time": "2020-07-06T13:18:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@ -2433,16 +2433,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
@ -2473,16 +2473,16 @@
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-09-07T11:33:47+00:00"
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v4.4.17",
|
||||
@ -2503,16 +2503,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.3",
|
||||
"symfony/event-dispatcher-contracts": "^1.1"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<3.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/event-dispatcher-implementation": "1.0",
|
||||
"symfony/event-dispatcher-implementation": "1.1"
|
||||
"require": {
|
||||
"php": ">=7.1.3",
|
||||
"symfony/event-dispatcher-contracts": "^1.1"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<3.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/event-dispatcher-implementation": "1.0",
|
||||
"symfony/event-dispatcher-implementation": "1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "~1.0",
|
||||
@ -2541,16 +2541,16 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony EventDispatcher Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"funding": [
|
||||
@ -2589,23 +2589,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/event-dispatcher": "",
|
||||
"symfony/event-dispatcher-implementation": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1.3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/event-dispatcher": "",
|
||||
"symfony/event-dispatcher-implementation": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\EventDispatcher\\": ""
|
||||
@ -2615,16 +2615,16 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to dispatching event",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
@ -2651,16 +2651,16 @@
|
||||
],
|
||||
"time": "2020-07-06T13:19:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v5.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "6e4320f06d5f2cce0d96530162491f4465179157"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v5.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "6e4320f06d5f2cce0d96530162491f4465179157"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157",
|
||||
"reference": "6e4320f06d5f2cce0d96530162491f4465179157",
|
||||
"shasum": "",
|
||||
@ -2711,16 +2711,16 @@
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-30T20:35:19+00:00"
|
||||
},
|
||||
"time": "2020-05-30T20:35:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v5.2.0",
|
||||
@ -2741,16 +2741,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1",
|
||||
"symfony/polyfill-mbstring": "~1.1",
|
||||
"symfony/polyfill-php80": "^1.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"predis/predis": "~1.0",
|
||||
"symfony/cache": "^4.4|^5.0",
|
||||
"symfony/expression-language": "^4.4|^5.0",
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1",
|
||||
"symfony/polyfill-mbstring": "~1.1",
|
||||
"symfony/polyfill-php80": "^1.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"predis/predis": "~1.0",
|
||||
"symfony/cache": "^4.4|^5.0",
|
||||
"symfony/expression-language": "^4.4|^5.0",
|
||||
"symfony/mime": "^4.4|^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
@ -3112,15 +3112,15 @@
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-14T12:35:20+00:00"
|
||||
"time": "2020-07-14T12:35:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
@ -3142,16 +3142,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.20-dev"
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.20-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@ -3439,15 +3439,15 @@
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-14T12:35:20+00:00"
|
||||
"time": "2020-07-14T12:35:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
@ -3469,16 +3469,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.20-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.20-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
@ -3520,10 +3520,10 @@
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
@ -3643,17 +3643,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"psr/container": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/service-implementation": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2-dev"
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"psr/container": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/service-implementation": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.2-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
@ -3690,10 +3690,10 @@
|
||||
"standards"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
@ -3793,16 +3793,16 @@
|
||||
"reference": "a3b3054262e48776e8014d5e385a8932b0102f29"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TencentCloud/tencentcloud-sdk-php/zipball/a3b3054262e48776e8014d5e385a8932b0102f29",
|
||||
"reference": "a3b3054262e48776e8014d5e385a8932b0102f29",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TencentCloud/tencentcloud-sdk-php/zipball/a3b3054262e48776e8014d5e385a8932b0102f29",
|
||||
"reference": "a3b3054262e48776e8014d5e385a8932b0102f29",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
@ -4192,40 +4192,41 @@
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "odan/phinx-migrations-generator",
|
||||
"version": "5.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/odan/phinx-migrations-generator.git",
|
||||
"reference": "f3cb7cc6bc7eb22e85f34f229b6d476e96d99c73"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/odan/phinx-migrations-generator/zipball/f3cb7cc6bc7eb22e85f34f229b6d476e96d99c73",
|
||||
"reference": "f3cb7cc6bc7eb22e85f34f229b6d476e96d99c73",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "odan/phinx-migrations-generator",
|
||||
"version": "5.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/odan/phinx-migrations-generator.git",
|
||||
"reference": "2d3620f8251838b53717f7a43a348de31e9d451c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/odan/phinx-migrations-generator/zipball/2d3620f8251838b53717f7a43a348de31e9d451c",
|
||||
"reference": "2d3620f8251838b53717f7a43a348de31e9d451c",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"php": "^7.2",
|
||||
"riimu/kit-phpencoder": "^2.4",
|
||||
"robmorgan/phinx": "^0.12",
|
||||
"symfony/console": "^2.8 || ^3.0 || ^4.0 || ^5.0"
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"php": "^7.2",
|
||||
"riimu/kit-phpencoder": "^2.4",
|
||||
"robmorgan/phinx": "^0.12",
|
||||
"symfony/console": "^2.8 || ^3.0 || ^4.0 || ^5.0",
|
||||
"symfony/polyfill-php73": "^1.18"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"overtrue/phplint": "^1.1",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"overtrue/phplint": "^1.1 || ^2.0",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
},
|
||||
"bin": [
|
||||
"./bin/phinx-migrations"
|
||||
@ -4240,18 +4241,22 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Migration generator for Phinx",
|
||||
"homepage": "https://github.com/odan/phinx-migrations-generator",
|
||||
"keywords": [
|
||||
"database",
|
||||
"generator",
|
||||
"migration",
|
||||
"migrations",
|
||||
"mysql",
|
||||
"phinx"
|
||||
],
|
||||
"time": "2020-06-15T19:36:35+00:00"
|
||||
"description": "Migration generator for Phinx",
|
||||
"homepage": "https://github.com/odan/phinx-migrations-generator",
|
||||
"keywords": [
|
||||
"database",
|
||||
"generator",
|
||||
"migration",
|
||||
"migrations",
|
||||
"mysql",
|
||||
"phinx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/odan/phinx-migrations-generator/issues",
|
||||
"source": "https://github.com/odan/phinx-migrations-generator/tree/5.3.2"
|
||||
},
|
||||
"time": "2020-12-30T23:59:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phalcon/ide-stubs",
|
||||
"version": "v3.4.3",
|
||||
@ -4262,15 +4267,15 @@
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phalcon/ide-stubs/zipball/65144f2b0fad32b182ccb062b1efc1b4edea5d44",
|
||||
"reference": "65144f2b0fad32b182ccb062b1efc1b4edea5d44",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
"url": "https://api.github.com/repos/phalcon/ide-stubs/zipball/65144f2b0fad32b182ccb062b1efc1b4edea5d44",
|
||||
"reference": "65144f2b0fad32b182ccb062b1efc1b4edea5d44",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
@ -4287,16 +4292,16 @@
|
||||
"homepage": "https://phalconphp.com/en/team"
|
||||
},
|
||||
{
|
||||
"name": "Contributors",
|
||||
"homepage": "https://github.com/phalcon/ide-stubs/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "The most complete Phalcon Framework IDE stubs library which enables autocompletion in modern IDEs.",
|
||||
"homepage": "https://phalconphp.com",
|
||||
"keywords": [
|
||||
"Devtools",
|
||||
"Eclipse",
|
||||
"autocomplete",
|
||||
"name": "Contributors",
|
||||
"homepage": "https://github.com/phalcon/ide-stubs/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "The most complete Phalcon Framework IDE stubs library which enables autocompletion in modern IDEs.",
|
||||
"homepage": "https://phalconphp.com",
|
||||
"keywords": [
|
||||
"Devtools",
|
||||
"Eclipse",
|
||||
"autocomplete",
|
||||
"ide",
|
||||
"netbeans",
|
||||
"phalcon",
|
||||
|
@ -83,70 +83,30 @@ $config['redis']['port'] = 6379;
|
||||
$config['redis']['auth'] = '1qaz2wsx3edc';
|
||||
|
||||
/**
|
||||
* redis库编号
|
||||
*/
|
||||
$config['cache']['db'] = 0;
|
||||
|
||||
/**
|
||||
* 有效期(秒)
|
||||
* 缓存有效期(秒)
|
||||
*/
|
||||
$config['cache']['lifetime'] = 24 * 3600;
|
||||
|
||||
/**
|
||||
* redis库编号
|
||||
*/
|
||||
$config['session']['db'] = 1;
|
||||
|
||||
/**
|
||||
* 有效期(秒)
|
||||
* 会话有效期(秒)
|
||||
*/
|
||||
$config['session']['lifetime'] = 24 * 3600;
|
||||
|
||||
/**
|
||||
* redis库编号
|
||||
* 令牌有效期(秒)
|
||||
*/
|
||||
$config['metadata']['db'] = 2;
|
||||
$config['token']['lifetime'] = 7 * 86400;
|
||||
|
||||
/**
|
||||
* 有效期(秒)
|
||||
* 元数据有效期(秒)
|
||||
*/
|
||||
$config['metadata']['lifetime'] = 7 * 86400;
|
||||
|
||||
/**
|
||||
* statsKey
|
||||
*/
|
||||
$config['metadata']['statsKey'] = '_METADATA_';
|
||||
|
||||
/**
|
||||
* redis库编号
|
||||
*/
|
||||
$config['annotation']['db'] = 2;
|
||||
|
||||
/**
|
||||
* 有效期(秒)
|
||||
* 注解有效期(秒)
|
||||
*/
|
||||
$config['annotation']['lifetime'] = 7 * 86400;
|
||||
|
||||
/**
|
||||
* statsKey
|
||||
*/
|
||||
$config['annotation']['statsKey'] = '_ANNOTATION_';
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
$config['jwt']['key'] = 'fu6ckEc8pv8k5K7m';
|
||||
|
||||
/**
|
||||
* 有效期(秒)
|
||||
*/
|
||||
$config['jwt']['lifetime'] = 7 * 86400;
|
||||
|
||||
/**
|
||||
* 回旋时间(秒)
|
||||
*/
|
||||
$config['jwt']['leeway'] = 30;
|
||||
|
||||
/**
|
||||
* 允许跨域
|
||||
*/
|
||||
|
@ -114,6 +114,7 @@ $error['course.invalid_vip_price'] = '无效的会员价格(范围:0-10000
|
||||
$error['course.invalid_compare_price'] = '无效的比较定价(会员价格高于市场价格)';
|
||||
$error['course.invalid_study_expiry'] = '无效的学习期限';
|
||||
$error['course.invalid_refund_expiry'] = '无效的退款期限';
|
||||
$error['course.invalid_feature_status'] = '无效的推荐状态';
|
||||
$error['course.invalid_publish_status'] = '无效的发布状态';
|
||||
$error['course.pub_chapter_not_found'] = '尚未发现已发布的课时';
|
||||
$error['course.pub_chapter_not_enough'] = '已发布的课时太少(小于30%)';
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
{
|
||||
|
||||
public function change()
|
||||
{
|
||||
$this->table('kg_online', [
|
||||
@ -18,6 +19,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
->addColumn('id', 'integer', [
|
||||
'null' => false,
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'identity' => 'enable',
|
||||
'comment' => '主键编号',
|
||||
])
|
||||
@ -25,6 +27,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '用户编号',
|
||||
'after' => 'id',
|
||||
])
|
||||
@ -32,6 +35,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '1',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '终端类型',
|
||||
'after' => 'user_id',
|
||||
])
|
||||
@ -48,6 +52,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '活跃时间',
|
||||
'after' => 'client_ip',
|
||||
])
|
||||
@ -55,6 +60,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'active_time',
|
||||
])
|
||||
@ -62,6 +68,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'create_time',
|
||||
])
|
||||
@ -90,7 +97,7 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'comment' => '',
|
||||
'comment' => '主键编号',
|
||||
'row_format' => 'DYNAMIC',
|
||||
])
|
||||
->changeColumn('channel_sn', 'string', [
|
||||
@ -104,4 +111,5 @@ class CreateOnlineTable extends Phinx\Migration\AbstractMigration
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
{
|
||||
|
||||
public function change()
|
||||
{
|
||||
$this->table('kg_connect', [
|
||||
@ -18,6 +19,7 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
->addColumn('id', 'integer', [
|
||||
'null' => false,
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'identity' => 'enable',
|
||||
'comment' => '主键编号',
|
||||
])
|
||||
@ -25,13 +27,14 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '用户编号',
|
||||
'after' => 'id',
|
||||
])
|
||||
->addColumn('open_id', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 50,
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '开放ID',
|
||||
@ -59,6 +62,7 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '提供方',
|
||||
'after' => 'open_avatar',
|
||||
])
|
||||
@ -66,6 +70,7 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '删除标识',
|
||||
'after' => 'provider',
|
||||
])
|
||||
@ -73,6 +78,7 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'deleted',
|
||||
])
|
||||
@ -80,6 +86,7 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'create_time',
|
||||
])
|
||||
@ -89,4 +96,5 @@ class CreateConnectTable extends Phinx\Migration\AbstractMigration
|
||||
])
|
||||
->create();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
{
|
||||
|
||||
public function change()
|
||||
{
|
||||
$this->table('kg_consult')
|
||||
@ -11,6 +12,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '回复者编号',
|
||||
'after' => 'owner_id',
|
||||
])
|
||||
@ -19,7 +21,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
->addColumn('union_id', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 50,
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => 'union_id',
|
||||
@ -46,6 +48,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
->addColumn('id', 'integer', [
|
||||
'null' => false,
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'identity' => 'enable',
|
||||
'comment' => '主键编号',
|
||||
])
|
||||
@ -53,13 +56,14 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '用户编号',
|
||||
'after' => 'id',
|
||||
])
|
||||
->addColumn('open_id', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 50,
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '开放ID',
|
||||
@ -69,6 +73,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '删除标识',
|
||||
'after' => 'open_id',
|
||||
])
|
||||
@ -76,6 +81,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'deleted',
|
||||
])
|
||||
@ -83,6 +89,7 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'create_time',
|
||||
])
|
||||
@ -96,4 +103,5 @@ class Schema202012121830 extends Phinx\Migration\AbstractMigration
|
||||
])
|
||||
->create();
|
||||
}
|
||||
|
||||
}
|
||||
|
22
db/migrations/20201227081614_schema_202012271615.php
Normal file
22
db/migrations/20201227081614_schema_202012271615.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class Schema202012271615 extends Phinx\Migration\AbstractMigration
|
||||
{
|
||||
|
||||
public function change()
|
||||
{
|
||||
$this->table('kg_course')
|
||||
->addColumn('featured', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '推荐标识',
|
||||
'after' => 'attrs',
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
}
|
170
db/migrations/20210102041941_schema_202101021220.php
Normal file
170
db/migrations/20210102041941_schema_202101021220.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class Schema202101021220 extends Phinx\Migration\AbstractMigration
|
||||
{
|
||||
|
||||
public function change()
|
||||
{
|
||||
$this->table('kg_user_session', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'comment' => '',
|
||||
'row_format' => 'DYNAMIC',
|
||||
])
|
||||
->addColumn('id', 'integer', [
|
||||
'null' => false,
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'identity' => 'enable',
|
||||
'comment' => '主键编号',
|
||||
])
|
||||
->addColumn('user_id', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '用户编号',
|
||||
'after' => 'id',
|
||||
])
|
||||
->addColumn('session_id', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '会话编号',
|
||||
'after' => 'user_id',
|
||||
])
|
||||
->addColumn('client_type', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '终端类型',
|
||||
'after' => 'session_id',
|
||||
])
|
||||
->addColumn('client_ip', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '终端IP',
|
||||
'after' => 'client_type',
|
||||
])
|
||||
->addColumn('deleted', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '删除标识',
|
||||
'after' => 'client_ip',
|
||||
])
|
||||
->addColumn('create_time', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'deleted',
|
||||
])
|
||||
->addColumn('update_time', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'create_time',
|
||||
])
|
||||
->addIndex(['user_id'], [
|
||||
'name' => 'user_id',
|
||||
'unique' => false,
|
||||
])
|
||||
->create();
|
||||
$this->table('kg_user_token', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'comment' => '',
|
||||
'row_format' => 'DYNAMIC',
|
||||
])
|
||||
->addColumn('id', 'integer', [
|
||||
'null' => false,
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'identity' => 'enable',
|
||||
'comment' => '主键编号',
|
||||
])
|
||||
->addColumn('user_id', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '用户编号',
|
||||
'after' => 'id',
|
||||
])
|
||||
->addColumn('token', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '身份令牌',
|
||||
'after' => 'user_id',
|
||||
])
|
||||
->addColumn('client_type', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '终端类型',
|
||||
'after' => 'token',
|
||||
])
|
||||
->addColumn('client_ip', 'string', [
|
||||
'null' => false,
|
||||
'default' => '',
|
||||
'limit' => 64,
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
'comment' => '终端IP',
|
||||
'after' => 'client_type',
|
||||
])
|
||||
->addColumn('deleted', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '删除标识',
|
||||
'after' => 'client_ip',
|
||||
])
|
||||
->addColumn('create_time', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '创建时间',
|
||||
'after' => 'deleted',
|
||||
])
|
||||
->addColumn('update_time', 'integer', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => MysqlAdapter::INT_REGULAR,
|
||||
'signed' => false,
|
||||
'comment' => '更新时间',
|
||||
'after' => 'create_time',
|
||||
])
|
||||
->addIndex(['user_id'], [
|
||||
'name' => 'user_id',
|
||||
'unique' => false,
|
||||
])
|
||||
->create();
|
||||
}
|
||||
|
||||
}
|
16169
db/migrations/schema.php
Normal file
16169
db/migrations/schema.php
Normal file
File diff suppressed because it is too large
Load Diff
10331
public/sitemap.xml
10331
public/sitemap.xml
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,9 @@ $scheduler->php($script, $bin, ['--task' => 'sync_group_index', '--action' => 'm
|
||||
$scheduler->php($script, $bin, ['--task' => 'sync_user_index', '--action' => 'main'])
|
||||
->hourly(23);
|
||||
|
||||
$scheduler->php($script, $bin, ['--task' => 'sync_course_score', '--action' => 'main'])
|
||||
->hourly(29);
|
||||
|
||||
$scheduler->php($script, $bin, ['--task' => 'clean_log', '--action' => 'main'])
|
||||
->daily(3, 3);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user