diff --git a/CHANGELOG.md b/CHANGELOG.md index 90eadbb7..1029575e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) + - 完善首页文章缓存的获取条件 - 完善热门专题的获取条件 - 优化课程章节列表逻辑 diff --git a/README.md b/README.md index 16c93aba..0a5626ac 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/Http/Admin/Controllers/StudentController.php b/app/Http/Admin/Controllers/StudentController.php index c8470500..b846002f 100644 --- a/app/Http/Admin/Controllers/StudentController.php +++ b/app/Http/Admin/Controllers/StudentController.php @@ -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); } /** diff --git a/app/Http/Admin/Controllers/UploadController.php b/app/Http/Admin/Controllers/UploadController.php index 57eda34b..268ec1a7 100644 --- a/app/Http/Admin/Controllers/UploadController.php +++ b/app/Http/Admin/Controllers/UploadController.php @@ -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") */ diff --git a/app/Http/Admin/Services/Course.php b/app/Http/Admin/Services/Course.php index 4c53af65..0cbf5d44 100644 --- a/app/Http/Admin/Services/Course.php +++ b/app/Http/Admin/Services/Course.php @@ -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), ]; diff --git a/app/Http/Admin/Services/Package.php b/app/Http/Admin/Services/Package.php index e8ce12c4..adf44462 100644 --- a/app/Http/Admin/Services/Package.php +++ b/app/Http/Admin/Services/Package.php @@ -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), ]; diff --git a/app/Http/Admin/Services/Student.php b/app/Http/Admin/Services/Student.php index 7c80e3bb..a89fcc4c 100644 --- a/app/Http/Admin/Services/Student.php +++ b/app/Http/Admin/Services/Student.php @@ -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(); diff --git a/app/Http/Admin/Services/Topic.php b/app/Http/Admin/Services/Topic.php index a299cb4c..5b336cb7 100644 --- a/app/Http/Admin/Services/Topic.php +++ b/app/Http/Admin/Services/Topic.php @@ -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), ]; diff --git a/app/Http/Admin/Views/student/add.volt b/app/Http/Admin/Views/student/add.volt index 8dd037f2..40dbcd56 100644 --- a/app/Http/Admin/Views/student/add.volt +++ b/app/Http/Admin/Views/student/add.volt @@ -2,22 +2,20 @@ {% block content %} - {% set course_id = course ? course.id : '' %} -
添加学员
- +
- +
- +
- +
@@ -37,6 +35,12 @@ {% endblock %} +{% block include_js %} + + {{ js_include('lib/xm-select.js') }} + +{% endblock %} + {% block inline_js %} + {% endblock %} \ No newline at end of file diff --git a/app/Http/Admin/Views/user/edit.volt b/app/Http/Admin/Views/user/edit.volt index 652e4e99..aa4cbac1 100644 --- a/app/Http/Admin/Views/user/edit.volt +++ b/app/Http/Admin/Views/user/edit.volt @@ -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' %}
@@ -46,6 +47,25 @@
{% endif %} +
+ +
+ + +
+
+
+
+ +
+ {% if user.vip_expiry_time > 0 %} + + {% else %} + + {% endif %} +
+
+
@@ -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') { diff --git a/app/Http/Home/Controllers/UploadController.php b/app/Http/Home/Controllers/UploadController.php index 1905951a..fc832cd6 100644 --- a/app/Http/Home/Controllers/UploadController.php +++ b/app/Http/Home/Controllers/UploadController.php @@ -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") */ diff --git a/app/Library/AppInfo.php b/app/Library/AppInfo.php index fed579d7..184af632 100644 --- a/app/Library/AppInfo.php +++ b/app/Library/AppInfo.php @@ -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) { diff --git a/app/Library/Paginator/Query.php b/app/Library/Paginator/Query.php index 9c34e892..abb4ab3f 100644 --- a/app/Library/Paginator/Query.php +++ b/app/Library/Paginator/Query.php @@ -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]); diff --git a/app/Models/Order.php b/app/Models/Order.php index 2a0e236d..8f89eb57 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -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(); + } + } diff --git a/app/Models/Refund.php b/app/Models/Refund.php index 2f68af7f..0b53e770 100644 --- a/app/Models/Refund.php +++ b/app/Models/Refund.php @@ -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(); + } + } diff --git a/app/Models/Trade.php b/app/Models/Trade.php index 27a6ec1f..3d1148e5 100644 --- a/app/Models/Trade.php +++ b/app/Models/Trade.php @@ -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(); + } + } diff --git a/app/Services/MyStorage.php b/app/Services/MyStorage.php index 157855e5..7c2c0d7c 100644 --- a/app/Services/MyStorage.php +++ b/app/Services/MyStorage.php @@ -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; + } + /** * 上传文件 * diff --git a/app/Services/Storage.php b/app/Services/Storage.php index 148cd0d3..6aa1eaee 100644 --- a/app/Services/Storage.php +++ b/app/Services/Storage.php @@ -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); } /** diff --git a/app/Validators/Account.php b/app/Validators/Account.php index 095bcdfa..af5bc1a4 100644 --- a/app/Validators/Account.php +++ b/app/Validators/Account.php @@ -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) { diff --git a/app/Validators/CourseUser.php b/app/Validators/CourseUser.php index 67eb19d6..baeeb819 100644 --- a/app/Validators/CourseUser.php +++ b/app/Validators/CourseUser.php @@ -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) diff --git a/app/Validators/Security.php b/app/Validators/Security.php index d6fddd04..8387fc49 100644 --- a/app/Validators/Security.php +++ b/app/Validators/Security.php @@ -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', + ]; + } + } diff --git a/public/static/admin/js/vditor.js b/public/static/admin/js/vditor.js index 054e8fed..0cdd31fd 100644 --- a/public/static/admin/js/vditor.js +++ b/public/static/admin/js/vditor.js @@ -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: { diff --git a/public/static/home/js/vditor.js b/public/static/home/js/vditor.js index 3126b7fb..6704f3a7 100644 --- a/public/static/home/js/vditor.js +++ b/public/static/home/js/vditor.js @@ -71,6 +71,7 @@ layui.use(['jquery'], function () { }, upload: { url: '/upload/content/img', + linkToImgUrl: '/upload/remote/img', max: 10 * 1024 * 1024, accept: 'image/*', headers: {