1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-27 21:10:24 +08:00

v.1.4.7 beta1

This commit is contained in:
koogua 2021-10-27 18:16:40 +08:00
parent 7d2b9c87b7
commit 4628a272c8
26 changed files with 365 additions and 63 deletions

View File

@ -1,5 +1,17 @@
### [v1.4.6](https://gitee.com/koogua/course-tencent-cloud/releases/v1.4.6)(2021-10-18)
- 更新README.md
- 优化分页查询参数过滤
- 优化分页查询参数过滤
- 优化后台学员添加和搜索
- 优化后台学员课程过期管理
- 增加后台会员特权过期管理
- 增加编辑器内站外图片自动保存到本地
- 增加CSRF放行白名单
- 完善订单|交易|退款序号
### [v1.4.6](https://gitee.com/koogua/course-tencent-cloud/releases/v1.4.6)(2021-10-18)
- 完善首页文章缓存的获取条件
- 完善热门专题的获取条件
- 优化课程章节列表逻辑

View File

@ -1,6 +1,6 @@
## 酷瓜云课堂
![酷瓜云网课GPL协议开源](https://images.gitee.com/uploads/images/2021/0520/091646_a94e4e7c_23592.png)
![酷瓜云网课GPL协议开源](https://portal-1255691183.file.myqcloud.com/img/content/60e7aea40966f.png)
### 项目介绍
@ -31,13 +31,13 @@
H5手机端演示
![H5二维码](https://images.gitee.com/uploads/images/2021/1011/091358_05e79898_23592.png)
![H5二维码](https://portal-1255691183.file.myqcloud.com/img/content/616fc238895b7.png)
演示账号13507083515 / 123456
微信公众号演示:
![公众号二维码](https://images.gitee.com/uploads/images/2021/1011/090813_3b88ecc3_23592.jpeg)
![公众号二维码](https://portal-1255691183.file.myqcloud.com/img/content/616f998270eca.png)
演示账号13507083515 / 123456

View File

@ -24,7 +24,10 @@ class StudentController extends Controller
$sourceTypes = $studentService->getSourceTypes();
$xmCourses = $studentService->getXmCourses('all');
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('xm_courses', $xmCourses);
}
/**
@ -53,17 +56,11 @@ class StudentController extends Controller
*/
public function addAction()
{
$courseId = $this->request->getQuery('course_id', 'int', 0);
$studentService = new StudentService();
$course = null;
$xmCourses = $studentService->getXmCourses('charge');
if ($courseId > 0) {
$course = $studentService->getCourse($courseId);
}
$this->view->setVar('course', $course);
$this->view->setVar('xm_courses', $xmCourses);
}
/**

View File

@ -9,6 +9,7 @@ namespace App\Http\Admin\Controllers;
use App\Services\MyStorage as StorageService;
use App\Services\Vod as VodService;
use App\Validators\Validator as AppValidator;
/**
* @RoutePrefix("/admin/upload")
@ -16,6 +17,15 @@ use App\Services\Vod as VodService;
class UploadController extends Controller
{
public function initialize()
{
$authUser = $this->getAuthUser();
$validator = new AppValidator();
$validator->checkAuthUser($authUser->id);
}
/**
* @Post("/icon/img", name="admin.upload.icon_img")
*/
@ -100,6 +110,34 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/remote/img", name="admin.upload.remote_img")
*/
public function uploadRemoteImageAction()
{
$originalUrl = $this->request->getPost('url', ['trim', 'string']);
$service = new StorageService();
$file = $service->uploadRemoteImage($originalUrl);
$newUrl = $originalUrl;
if ($file) {
$newUrl = $service->getImageUrl($file->path);
}
/**
* 编辑器要求返回的数据结构
*/
$data = [
'url' => $newUrl,
'originalURL' => $originalUrl,
];
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/default/img", name="admin.upload.default_img")
*/

View File

@ -389,7 +389,10 @@ class Course extends Service
}
}
$items = $courseRepo->findAll(['published' => 1]);
$items = $courseRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];
@ -397,7 +400,7 @@ class Course extends Service
foreach ($items as $item) {
$result[] = [
'name' => sprintf('%s(¥%0.2f', $item->title, $item->market_price),
'name' => sprintf('%s - %s(¥%0.2f', $item->id, $item->title, $item->market_price),
'value' => $item->id,
'selected' => in_array($item->id, $courseIds),
];

View File

@ -51,6 +51,7 @@ class Package extends Service
'model' => $model,
'free' => 0,
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];
@ -59,7 +60,7 @@ class Package extends Service
foreach ($items as $item) {
$result[] = [
'name' => sprintf('%s(¥%0.2f', $item->title, $item->market_price),
'name' => sprintf('%s - %s(¥%0.2f', $item->id, $item->title, $item->market_price),
'value' => $item->id,
'selected' => in_array($item->id, $courseIds),
];

View File

@ -24,6 +24,38 @@ use App\Validators\CourseUser as CourseUserValidator;
class Student extends Service
{
public function getXmCourses($scope = 'all')
{
$courseRepo = new CourseRepo();
$where = [
'published' => 1,
'deleted' => 0,
];
/**
* 过滤付费课程
*/
if ($scope == 'charge') {
$where['free'] = 0;
}
$items = $courseRepo->findAll($where);
if ($items->count() == 0) return [];
$result = [];
foreach ($items as $item) {
$result[] = [
'name' => sprintf('%s - %s¥%0.2f', $item->id, $item->title, $item->market_price),
'value' => $item->id,
];
}
return $result;
}
public function getSourceTypes()
{
return CourseUserModel::sourceTypes();
@ -51,6 +83,18 @@ class Student extends Service
$params['role_type'] = CourseUserModel::ROLE_STUDENT;
$validator = new CourseUserValidator();
if (!empty($params['xm_course_id'])) {
$course = $validator->checkCourse($params['xm_course_id']);
$params['course_id'] = $course->id;
}
if (!empty($params['xm_user_id'])) {
$user = $validator->checkUser($params['xm_user_id']);
$params['user_id'] = $user->id;
}
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
@ -95,15 +139,15 @@ class Student extends Service
'source_type' => CourseUserModel::SOURCE_IMPORT,
];
$course = $validator->checkCourse($post['course_id']);
$user = $validator->checkUser($post['user_id']);
$course = $validator->checkCourse($post['xm_course_id']);
$user = $validator->checkUser($post['xm_user_id']);
$expiryTime = $validator->checkExpiryTime($post['expiry_time']);
$data['course_id'] = $course->id;
$data['user_id'] = $user->id;
$data['expiry_time'] = $expiryTime;
$validator->checkIfImported($post['course_id'], $post['user_id']);
$validator->checkIfImported($course->id, $user->id);
$courseUser = new CourseUserModel();

View File

@ -35,7 +35,10 @@ class Topic extends Service
$courseRepo = new CourseRepo();
$items = $courseRepo->findAll(['published' => 1]);
$items = $courseRepo->findAll([
'published' => 1,
'deleted' => 0,
]);
if ($items->count() == 0) return [];
@ -43,7 +46,7 @@ class Topic extends Service
foreach ($items as $item) {
$result[] = [
'name' => sprintf('%s(¥%0.2f', $item->title, $item->market_price),
'name' => sprintf('%s - %s(¥%0.2f', $item->id, $item->title, $item->market_price),
'value' => $item->id,
'selected' => in_array($item->id, $courseIds),
];

View File

@ -2,22 +2,20 @@
{% block content %}
{% set course_id = course ? course.id : '' %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.student.create'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>添加学员</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">课程编号</label>
<label class="layui-form-label">所属课程</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="course_id" value="{{ course_id }}" lay-verify="required">
<div id="xm-course-id"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户号</label>
<label class="layui-form-label">用户号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="user_id" lay-verify="required">
<input class="layui-input" type="text" name="xm_user_id" placeholder="用户编号 / 手机号码 / 邮箱地址" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
@ -37,6 +35,12 @@
{% endblock %}
{% block include_js %}
{{ js_include('lib/xm-select.js') }}
{% endblock %}
{% block inline_js %}
<script>
@ -45,6 +49,17 @@
var laydate = layui.laydate;
xmSelect.render({
el: '#xm-course-id',
name: 'xm_course_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});
laydate.render({
elem: 'input[name=expiry_time]',
type: 'datetime'

View File

@ -2,7 +2,7 @@
{% block content %}
{% set expiry_editable = relation.source_type in [1,3] %}
{% set expiry_editable = relation.source_type in [2,4,5,6] %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.student.update'}) }}">
<fieldset class="layui-elem-field layui-field-title">

View File

@ -47,6 +47,7 @@
<col>
<col>
<col>
<col>
<col width="12%">
</colgroup>
<thead>
@ -54,6 +55,7 @@
<th>基本信息</th>
<th>学习情况</th>
<th>来源类型</th>
<th>加入日期</th>
<th>有效期限</th>
<th>操作</th>
</tr>
@ -74,12 +76,13 @@
<p>时长:{{ item.duration|duration }}</p>
</td>
<td>{{ source_type_info(item.source_type) }}</td>
<td>{{ date('Y-m-d',item.create_time) }}</td>
<td>
{% if item.source_type in [1,3] %}
N/A
{% else %}
<p>开始:{{ date('Y-m-d H:i',item.create_time) }}</p>
<p>结束:{{ date('Y-m-d H:i',item.expiry_time) }}</p>
<p>开始:{{ date('Y-m-d',item.create_time) }}</p>
<p>结束:{{ date('Y-m-d',item.expiry_time) }}</p>
{% endif %}
</td>
<td class="center">

View File

@ -2,22 +2,20 @@
{% block content %}
{% set course_id = request.get('course_id', 'int', '') %}
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.student.list'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>搜索学员</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">课程编号</label>
<label class="layui-form-label">所属课程</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="course_id" value="{{ course_id }}" placeholder="课程编号精确匹配">
<div id="xm-course-id"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户号</label>
<label class="layui-form-label">用户号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="user_id" placeholder="用户编号精确匹配">
<input class="layui-input" type="text" name="xm_user_id" placeholder="用户编号 / 手机号码 / 邮箱地址">
</div>
</div>
<div class="layui-form-item">
@ -37,4 +35,29 @@
</div>
</form>
{% endblock %}
{% block include_js %}
{{ js_include('lib/xm-select.js') }}
{% endblock %}
{% block inline_js %}
<script>
xmSelect.render({
el: '#xm-course-id',
name: 'xm_course_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});
</script>
{% endblock %}

View File

@ -3,6 +3,7 @@
{% block content %}
{% set lock_expiry_display = user.locked == 1 ? 'display:block': 'display:none' %}
{% set vip_expiry_display = user.vip == 1 ? 'display:block': 'display:none' %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.user.update','id':user.id}) }}">
<fieldset class="layui-elem-field layui-field-title">
@ -46,6 +47,25 @@
</div>
</div>
{% endif %}
<div class="layui-form-item">
<label class="layui-form-label">会员特权</label>
<div class="layui-input-block">
<input type="radio" name="vip" value="1" title="是" lay-filter="vip" {% if user.vip == 1 %}checked="checked"{% endif %}>
<input type="radio" name="vip" value="0" title="否" lay-filter="vip" {% if user.vip == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div id="vip-expiry-block" style="{{ vip_expiry_display }}">
<div class="layui-form-item">
<label class="layui-form-label">会员期限</label>
<div class="layui-input-block">
{% if user.vip_expiry_time > 0 %}
<input class="layui-input" type="text" name="vip_expiry_time" autocomplete="off" value="{{ date('Y-m-d H:i:s',user.vip_expiry_time) }}">
{% else %}
<input class="layui-input" type="text" name="vip_expiry_time" autocomplete="off">
{% endif %}
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">锁定帐号</label>
<div class="layui-input-block">
@ -121,11 +141,25 @@
var form = layui.form;
var laydate = layui.laydate;
laydate.render({
elem: 'input[name=vip_expiry_time]',
type: 'datetime'
});
laydate.render({
elem: 'input[name=lock_expiry_time]',
type: 'datetime'
});
form.on('radio(vip)', function (data) {
var block = $('#vip-expiry-block');
if (data.value === '1') {
block.show();
} else {
block.hide();
}
});
form.on('radio(locked)', function (data) {
var block = $('#lock-expiry-block');
if (data.value === '1') {

View File

@ -46,27 +46,6 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/cover/img", name="home.upload.cover_img")
*/
public function uploadCoverImageAction()
{
$service = new StorageService();
$file = $service->uploadCoverImage();
if (!$file) {
return $this->jsonError(['msg' => '上传文件失败']);
}
$data = [
'src' => $service->getImageUrl($file->path),
'title' => $file->name,
];
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/content/img", name="home.upload.content_img")
*/
@ -88,6 +67,34 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/remote/img", name="home.upload.remote_img")
*/
public function uploadRemoteImageAction()
{
$originalUrl = $this->request->getPost('url', ['trim', 'string']);
$service = new StorageService();
$file = $service->uploadRemoteImage($originalUrl);
$newUrl = $originalUrl;
if ($file) {
$newUrl = $service->getImageUrl($file->path);
}
/**
* 编辑器要求返回的数据结构
*/
$data = [
'url' => $newUrl,
'originalURL' => $originalUrl,
];
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/im/img", name="home.upload.im_img")
*/

View File

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

View File

@ -60,7 +60,7 @@ class Query
$params = $this->request->getQuery();
if ($params) {
foreach ($params as $key => $value) {
foreach ($params as $key => &$value) {
$value = $this->filter->sanitize($value, ['trim', 'string']);
if ($whitelist && !in_array($value, $whitelist)) {
unset($params[$key]);

View File

@ -173,7 +173,7 @@ class Order extends Model
public function beforeCreate()
{
$this->sn = date('YmdHis') . rand(1000, 9999);
$this->sn = $this->getOrderSn();
$this->create_time = time();
}
@ -247,4 +247,18 @@ class Order extends Model
];
}
protected function getOrderSn()
{
$sn = date('YmdHis') . rand(1000, 9999);
$order = self::findFirst([
'conditions' => 'sn = :sn:',
'bind' => ['sn' => $sn],
]);
if (!$order) return $sn;
$this->getOrderSn();
}
}

View File

@ -134,7 +134,7 @@ class Refund extends Model
public function beforeCreate()
{
$this->sn = date('YmdHis') . rand(1000, 9999);
$this->sn = $this->getRefundSn();
$this->create_time = time();
}
@ -171,4 +171,18 @@ class Refund extends Model
];
}
protected function getRefundSn()
{
$sn = date('YmdHis') . rand(1000, 9999);
$order = self::findFirst([
'conditions' => 'sn = :sn:',
'bind' => ['sn' => $sn],
]);
if (!$order) return $sn;
$this->getRefundSn();
}
}

View File

@ -131,7 +131,7 @@ class Trade extends Model
public function beforeCreate()
{
$this->sn = date('YmdHis') . rand(1000, 9999);
$this->sn = $this->getTradeSn();
$this->create_time = time();
}
@ -174,4 +174,18 @@ class Trade extends Model
];
}
protected function getTradeSn()
{
$sn = date('YmdHis') . rand(1000, 9999);
$order = self::findFirst([
'conditions' => 'sn = :sn:',
'bind' => ['sn' => $sn],
]);
if (!$order) return $sn;
$this->getTradeSn();
}
}

View File

@ -181,12 +181,66 @@ class MyStorage extends Storage
/**
* 上传im文件
*
* @return UploadModel|bool
*/
public function uploadImFile()
{
return $this->upload('/im/file/', self::MIME_FILE, UploadModel::TYPE_IM_FILE);
}
/**
* @param string $url
*
* @return UploadModel|bool
*/
public function uploadRemoteImage($url)
{
$path = parse_url($url, PHP_URL_PATH);
$extension = pathinfo($path, PATHINFO_EXTENSION);
$originalName = pathinfo($path, PATHINFO_BASENAME);
$fileName = $this->generateFileName($extension);
$filePath = tmp_path($fileName);
$contents = file_get_contents($url);
if (file_put_contents($filePath, $contents) === false) {
return false;
}
$keyName = "/img/content/{$fileName}";
$uploadPath = $this->putFile($keyName, $filePath);
if (!$uploadPath) return false;
$md5 = md5_file($filePath);
$uploadRepo = new UploadRepo();
$upload = $uploadRepo->findByMd5($md5);
if ($upload == false) {
$upload = new UploadModel();
$upload->name = $originalName;
$upload->mime = mime_content_type($filePath);
$upload->size = filesize($filePath);
$upload->type = UploadModel::TYPE_CONTENT_IMG;
$upload->path = $uploadPath;
$upload->md5 = $md5;
$upload->create();
}
unlink($filePath);
return $upload;
}
/**
* 上传文件
*

View File

@ -255,7 +255,9 @@ class Storage extends Service
{
$name = uniqid();
return sprintf('%s%s.%s', $prefix, $name, $extension);
$dot = $extension ? '.' : '';
return sprintf('%s%s%s%s', $prefix, $name, $dot, $extension);
}
/**

View File

@ -29,6 +29,8 @@ class Account extends Validator
$account = $accountRepo->findByEmail($name);
} elseif (CommonValidator::phone($name)) {
$account = $accountRepo->findByPhone($name);
} elseif (CommonValidator::intNumber($name)) {
$account = $accountRepo->findById($name);
}
if (!$account) {

View File

@ -48,11 +48,15 @@ class CourseUser extends Validator
return $validator->checkCourse($id);
}
public function checkUser($id)
public function checkUser($name)
{
$validator = new Account();
$account = $validator->checkAccount($name);
$validator = new User();
return $validator->checkUser($id);
return $validator->checkUser($account->id);
}
public function checkExpiryTime($expiryTime)

View File

@ -17,6 +17,12 @@ class Security extends Validator
public function checkCsrfToken()
{
$route = $this->router->getMatchedRoute();
if (in_array($route->getName(), $this->getCsrfWhitelist())) {
return;
}
$token = $this->request->getHeader('X-Csrf-Token');
$service = new CsrfTokenService();
@ -50,4 +56,14 @@ class Security extends Validator
}
}
protected function getCsrfWhitelist()
{
return [
'admin.upload.content_img',
'admin.upload.remote_img',
'home.upload.content_img',
'home.upload.remote_img',
];
}
}

View File

@ -71,6 +71,7 @@ layui.use(['jquery'], function () {
},
upload: {
url: '/admin/upload/content/img',
linkToImgUrl: '/admin/upload/remote/img',
max: 10 * 1024 * 1024,
accept: 'image/*',
headers: {

View File

@ -71,6 +71,7 @@ layui.use(['jquery'], function () {
},
upload: {
url: '/upload/content/img',
linkToImgUrl: '/upload/remote/img',
max: 10 * 1024 * 1024,
accept: 'image/*',
headers: {