From a95aa655cd84a972f4bfd110b796cc4afbed393e Mon Sep 17 00:00:00 2001 From: koogua Date: Sat, 20 Mar 2021 17:44:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=AF=BE=E7=A8=8B=EF=BC=8C=E5=A5=97?= =?UTF-8?q?=E9=A4=90=E9=BB=98=E8=AE=A4=E5=B0=81=E9=9D=A2=E4=BA=92=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../static/admin/img/default/course_cover.png | Bin 9298 -> 5693 bytes .../admin/img/default/package_cover.png | Bin 5693 -> 9298 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/static/admin/img/default/course_cover.png b/public/static/admin/img/default/course_cover.png index 39ce024eb1756fd64e06af84157385dc817bf974..f1c21d35feee2a2f34341b3d949d39a42ed1b7db 100644 GIT binary patch literal 5693 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 literal 9298 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 diff --git a/public/static/admin/img/default/package_cover.png b/public/static/admin/img/default/package_cover.png index f1c21d35feee2a2f34341b3d949d39a42ed1b7db..39ce024eb1756fd64e06af84157385dc817bf974 100644 GIT binary patch literal 9298 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 literal 5693 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 From 6c86f914cce9dc946d186cc4d49c11b9648e47a2 Mon Sep 17 00:00:00 2001 From: koogua Date: Fri, 26 Mar 2021 15:15:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9D=A2=E6=8E=88?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=EF=BC=8C=E9=87=8D=E6=9E=84=E7=BE=A4=E7=BB=84?= =?UTF-8?q?=E6=88=90=E5=91=98=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +++ README.md | 14 +-- app/Builders/ImGroupList.php | 11 +++ app/Console/Tasks/DeliverTask.php | 11 ++- app/Console/Tasks/UpgradeTask.php | 2 +- .../Admin/Controllers/ChapterController.php | 19 +++- .../Admin/Controllers/ImGroupController.php | 16 ++++ .../Controllers/ImGroupUserController.php | 32 +++++++ app/Http/Admin/Services/AuthNode.php | 12 +++ app/Http/Admin/Services/Chapter.php | 18 ++-- app/Http/Admin/Services/ChapterContent.php | 86 +++++++++++++------ app/Http/Admin/Services/Course.php | 54 +++++++++--- app/Http/Admin/Services/ImGroup.php | 69 +++++++++++++-- app/Http/Admin/Services/ImGroupUser.php | 55 ++++++++++++ app/Http/Admin/Services/Package.php | 16 +++- app/Http/Admin/Services/User.php | 4 +- app/Http/Admin/Views/chapter/edit_lesson.volt | 4 + .../Views/chapter/edit_lesson_offline.volt | 24 ++++++ app/Http/Admin/Views/chapter/lessons.volt | 2 + .../Admin/Views/chapter/lessons_offline.volt | 64 ++++++++++++++ app/Http/Admin/Views/course/add.volt | 3 +- app/Http/Admin/Views/course/edit.volt | 38 ++++---- app/Http/Admin/Views/course/edit_offline.volt | 33 +++++++ app/Http/Admin/Views/course/edit_sale.volt | 54 +++++------- app/Http/Admin/Views/course/expiry_macro.volt | 31 ------- app/Http/Admin/Views/course/list.volt | 27 +----- app/Http/Admin/Views/im/group/list.volt | 45 ++++------ app/Http/Admin/Views/im/group/users.volt | 70 +++++++++++++++ app/Http/Admin/Views/macros/course.volt | 27 ++++++ app/Http/Admin/Views/macros/group.volt | 11 +++ app/Http/Admin/Views/macros/user.volt | 34 ++++++++ app/Http/Admin/Views/order/list.volt | 10 +-- app/Http/Admin/Views/order/macro.volt | 33 +++++-- app/Http/Admin/Views/user/list.volt | 60 ++++--------- .../Home/Controllers/ImGroupController.php | 40 +++++++++ .../Controllers/ImGroupManageController.php | 73 ---------------- .../Controllers/ImGroupUserController.php | 32 +++++++ app/Http/Home/Services/ImGroup.php | 53 +----------- app/Http/Home/Services/ImGroupTrait.php | 2 + app/Http/Home/Services/ImGroupUser.php | 58 +++++++++++++ app/Http/Home/Views/course/show_catalog.volt | 23 +++++ app/Http/Home/Views/course/show_meta.volt | 61 ++++++++++--- app/Http/Home/Views/flash_sale/index.volt | 13 +++ .../Views/im/group/{manage => }/edit.volt | 0 .../Home/Views/im/group/manage/users.volt | 32 ------- .../Home/Views/im/group/manage_users.volt | 55 ++++++++++++ app/Http/Home/Views/im/group/users.volt | 14 +-- app/Http/Home/Views/macros/course.volt | 16 ++-- app/Http/Home/Views/macros/order.volt | 49 +++++++---- app/Http/Home/Views/order/confirm.volt | 61 +++++++++---- app/Http/Home/Views/order/info.volt | 8 +- app/Http/Home/Views/package/courses.volt | 46 +++++----- app/Http/Home/Views/user/console/friends.volt | 15 ++-- .../Views/user/console/groups_joined.volt | 17 ++-- .../Home/Views/user/console/groups_owned.volt | 16 ++-- app/Library/AppInfo.php | 2 +- app/Models/Chapter.php | 19 +++- app/Models/ChapterOffline.php | 72 ++++++++++++++++ app/Models/Course.php | 40 +++++++-- app/Models/ImGroup.php | 2 +- app/Models/ImUser.php | 2 +- app/Repos/Chapter.php | 13 +++ app/Repos/Course.php | 12 ++- app/Services/CourseStat.php | 35 ++------ app/Services/Logic/Course/ChapterList.php | 8 +- app/Services/Logic/Order/OrderConfirm.php | 1 + app/Services/Logic/Order/OrderCreate.php | 2 + app/Services/Logic/Order/OrderInfo.php | 40 +++++++-- app/Validators/Chapter.php | 4 + app/Validators/ChapterLive.php | 8 -- app/Validators/ChapterOffline.php | 36 ++++++++ app/Validators/Course.php | 8 +- app/Validators/CourseOffline.php | 60 +++++++++++++ app/Validators/ImGroupUser.php | 9 ++ config/errors.php | 24 +++++- db/migrations/20210324064239.php | 86 +++++++++++++++++++ public/static/admin/css/common.css | 10 +++ public/static/home/css/common.css | 34 ++++++-- public/static/home/js/flashsale.js | 2 +- public/static/home/js/user.console.js | 4 +- 80 files changed, 1623 insertions(+), 593 deletions(-) create mode 100644 app/Http/Admin/Controllers/ImGroupUserController.php create mode 100644 app/Http/Admin/Services/ImGroupUser.php create mode 100644 app/Http/Admin/Views/chapter/edit_lesson_offline.volt create mode 100644 app/Http/Admin/Views/chapter/lessons_offline.volt create mode 100644 app/Http/Admin/Views/course/edit_offline.volt delete mode 100644 app/Http/Admin/Views/course/expiry_macro.volt create mode 100644 app/Http/Admin/Views/im/group/users.volt create mode 100644 app/Http/Admin/Views/macros/course.volt create mode 100644 app/Http/Admin/Views/macros/group.volt create mode 100644 app/Http/Admin/Views/macros/user.volt delete mode 100644 app/Http/Home/Controllers/ImGroupManageController.php create mode 100644 app/Http/Home/Controllers/ImGroupUserController.php create mode 100644 app/Http/Home/Services/ImGroupUser.php rename app/Http/Home/Views/im/group/{manage => }/edit.volt (100%) delete mode 100644 app/Http/Home/Views/im/group/manage/users.volt create mode 100644 app/Http/Home/Views/im/group/manage_users.volt create mode 100644 app/Models/ChapterOffline.php create mode 100644 app/Validators/ChapterOffline.php create mode 100644 app/Validators/CourseOffline.php create mode 100644 db/migrations/20210324064239.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd4f0bf..cb5bebab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### [v1.3.0](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.0)(2021-03-26) + +### 更新 + +- 课程增加面授模型 +- 重构前台群组成员管理 +- 后台增加群组成员管理 +- 重构订单存储商品详情数据结构 +- 调整用户和群组列表等UI + ### [v1.2.9](https://gitee.com/koogua/course-tencent-cloud/releases/v1.2.9)(2021-03-22) ### 更新 diff --git a/README.md b/README.md index b8019549..acae1239 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ ### 系统功能 -实现了点播、直播、专栏、会员、微聊等,是一个完整的产品,具体功能我也不想写一大堆,自己体验吧! +实现了点播、直播、专栏、面授、会员、群组、积分商城、秒杀等,全功能无阉割,100%真开源在线教育解决方案。具体功能我也不想写一大堆,自己体验吧! 友情提示: -- 系统配置低(1核 1G 1M 跑多个容器),切莫压测 -- 课程数据来源于网络(无实质内容),切莫购买 +- 演示系统配置低(1Core,1G,1M 跑多个容器)切莫压测 +- 课程数据来源于网络(无实质内容)切莫购买 - 管理后台已禁止数据提交,私密配置已过滤 桌面端演示: @@ -27,13 +27,13 @@ - [前台演示](https://ctc.koogua.com) - [后台演示](https://ctc.koogua.com/admin) -演示帐号:100015@163.com / 123456 (前后台通用) +演示账号:100015@163.com / 123456 (前后台通用) 移动端演示: ![移动端二维码](https://images.gitee.com/uploads/images/2020/1127/093203_265221a2_23592.png) -演示帐号:13507083515 / 123456 +演示账号:13507083515 / 123456 支付流程演示: @@ -42,10 +42,10 @@ - [数据库与中间件的基础必修课(0.02元)](https://ctc.koogua.com/order/confirm?item_id=80&item_type=2) Tips: 测试支付请用手机号注册一个新账户,以便接收订单通知,以及避免课程无法购买 - + 即时通讯演示: -请使用以下两个帐号在不同终端或者浏览器登录,打开微聊界面 +请使用以下两个账号在不同终端或者浏览器登录,打开微聊界面 - 帐号A:100015@163.com / 123456 - 帐号B:100065@163.com / 123456 diff --git a/app/Builders/ImGroupList.php b/app/Builders/ImGroupList.php index a21b9648..f0bdd8d9 100644 --- a/app/Builders/ImGroupList.php +++ b/app/Builders/ImGroupList.php @@ -8,6 +8,17 @@ use App\Repos\User as UserRepo; class ImGroupList extends Builder { + public function handleGroups(array $groups) + { + $baseUrl = kg_cos_url(); + + foreach ($groups as $key => $group) { + $groups[$key]['avatar'] = $baseUrl . $group['avatar']; + } + + return $groups; + } + public function handleCourses(array $groups) { $courses = $this->getCourses($groups); diff --git a/app/Console/Tasks/DeliverTask.php b/app/Console/Tasks/DeliverTask.php index f37a12d0..7c57b30d 100644 --- a/app/Console/Tasks/DeliverTask.php +++ b/app/Console/Tasks/DeliverTask.php @@ -2,6 +2,7 @@ namespace App\Console\Tasks; +use App\Models\Course as CourseModel; use App\Models\CourseUser as CourseUserModel; use App\Models\ImGroupUser as ImGroupUserModel; use App\Models\Order as OrderModel; @@ -105,13 +106,19 @@ class DeliverTask extends Task protected function handleCourseOrder(OrderModel $order) { - $itemInfo = $order->item_info; + $course = $order->item_info['course']; + + if ($course['model'] == CourseModel::MODEL_OFFLINE) { + $expiryTime = strtotime($course['attrs']['end_date']); + } else { + $expiryTime = $course['study_expiry_time']; + } $courseUser = new CourseUserModel(); $courseUser->user_id = $order->owner_id; $courseUser->course_id = $order->item_id; - $courseUser->expiry_time = $itemInfo['course']['study_expiry_time']; + $courseUser->expiry_time = $expiryTime; $courseUser->role_type = CourseUserModel::ROLE_STUDENT; $courseUser->source_type = CourseUserModel::SOURCE_CHARGE; diff --git a/app/Console/Tasks/UpgradeTask.php b/app/Console/Tasks/UpgradeTask.php index 2667186d..521289f1 100644 --- a/app/Console/Tasks/UpgradeTask.php +++ b/app/Console/Tasks/UpgradeTask.php @@ -80,7 +80,7 @@ class UpgradeTask extends Task $redis->del($statsKey); } - echo "end reset metadata..." . PHP_EOL; + echo "------ end reset metadata ------" . PHP_EOL; } /** diff --git a/app/Http/Admin/Controllers/ChapterController.php b/app/Http/Admin/Controllers/ChapterController.php index 1c81ebbc..f1a40e5d 100644 --- a/app/Http/Admin/Controllers/ChapterController.php +++ b/app/Http/Admin/Controllers/ChapterController.php @@ -79,10 +79,17 @@ class ChapterController extends Controller $chapter = $chapterService->createChapter(); - $location = $this->url->get([ - 'for' => 'admin.course.chapters', - 'id' => $chapter->course_id, - ]); + if ($chapter->parent_id > 0) { + $location = $this->url->get([ + 'for' => 'admin.chapter.lessons', + 'id' => $chapter->parent_id, + ]); + } else { + $location = $this->url->get([ + 'for' => 'admin.course.chapters', + 'id' => $chapter->course_id, + ]); + } $content = [ 'location' => $location, @@ -131,6 +138,10 @@ class ChapterController extends Controller $read = $contentService->getChapterRead($chapter->id); $this->view->setVar('read', $read); break; + case CourseModel::MODEL_OFFLINE: + $offline = $contentService->getChapterOffline($chapter->id); + $this->view->setVar('offline', $offline); + break; } } diff --git a/app/Http/Admin/Controllers/ImGroupController.php b/app/Http/Admin/Controllers/ImGroupController.php index e5659778..fc715e73 100644 --- a/app/Http/Admin/Controllers/ImGroupController.php +++ b/app/Http/Admin/Controllers/ImGroupController.php @@ -10,6 +10,22 @@ use App\Http\Admin\Services\ImGroup as ImGroupService; class ImGroupController extends Controller { + /** + * @Get("/{id:[0-9]+}/users", name="admin.im_group.users") + */ + public function usersAction($id) + { + $service = new ImGroupService(); + + $group = $service->getGroup($id); + $pager = $service->getGroupUsers($id); + + $this->view->pick('im/group/users'); + + $this->view->setVar('group', $group); + $this->view->setVar('pager', $pager); + } + /** * @Get("/list", name="admin.im_group.list") */ diff --git a/app/Http/Admin/Controllers/ImGroupUserController.php b/app/Http/Admin/Controllers/ImGroupUserController.php new file mode 100644 index 00000000..f5de83f1 --- /dev/null +++ b/app/Http/Admin/Controllers/ImGroupUserController.php @@ -0,0 +1,32 @@ +deleteGroupUser(); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除成员成功', + ]; + + return $this->jsonSuccess($content); + } + +} diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 236ae11b..646c0dec 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -381,6 +381,18 @@ class AuthNode extends Service 'type' => 'button', 'route' => 'admin.im_group.delete', ], + [ + 'id' => '2-4-6', + 'title' => '群员列表', + 'type' => 'button', + 'route' => 'admin.im_group.users', + ], + [ + 'id' => '2-4-7', + 'title' => '删除群员', + 'type' => 'button', + 'route' => 'admin.im_group_user.delete', + ], ], ], [ diff --git a/app/Http/Admin/Services/Chapter.php b/app/Http/Admin/Services/Chapter.php index 5a387d95..4d9b4efa 100644 --- a/app/Http/Admin/Services/Chapter.php +++ b/app/Http/Admin/Services/Chapter.php @@ -7,6 +7,7 @@ use App\Caches\Chapter as ChapterCache; use App\Caches\CourseChapterList as CatalogCache; use App\Models\Chapter as ChapterModel; use App\Models\ChapterLive as ChapterLiveModel; +use App\Models\ChapterOffline as ChapterOfflineModel; use App\Models\ChapterRead as ChapterReadModel; use App\Models\ChapterVod as ChapterVodModel; use App\Models\Course as CourseModel; @@ -25,9 +26,7 @@ class Chapter extends Service $resources = $resourceRepo->findByChapterId($id); - if ($resources->count() == 0) { - return []; - } + if ($resources->count() == 0) return []; $builder = new ResourceListBuilder(); @@ -118,6 +117,10 @@ class Chapter extends Service $chapterRead = new ChapterReadModel(); $attrs = $chapterRead->create($data); break; + case CourseModel::MODEL_OFFLINE: + $chapterOffline = new ChapterOfflineModel(); + $attrs = $chapterOffline->create($data); + break; } if ($attrs === false) { @@ -137,10 +140,11 @@ class Chapter extends Service $this->db->rollback(); - $logger = $this->getLogger(); + $logger = $this->getLogger('http'); $logger->error('Create Chapter Error ' . kg_json_encode([ - 'code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), 'message' => $e->getMessage(), ])); @@ -176,7 +180,7 @@ class Chapter extends Service if (isset($post['published'])) { $data['published'] = $validator->checkPublishStatus($post['published']); - if ($chapter->published == 0 && $post['published'] == 1) { + if ($post['published'] == 1) { $validator->checkPublishAbility($chapter); } } @@ -259,6 +263,8 @@ class Chapter extends Service $courseStats->updateLiveAttrs($course->id); } elseif ($course->model == CourseModel::MODEL_READ) { $courseStats->updateReadAttrs($course->id); + } elseif ($course->model == CourseModel::MODEL_OFFLINE) { + $courseStats->updateOfflineAttrs($course->id); } } diff --git a/app/Http/Admin/Services/ChapterContent.php b/app/Http/Admin/Services/ChapterContent.php index 09fccf37..a4bb6235 100644 --- a/app/Http/Admin/Services/ChapterContent.php +++ b/app/Http/Admin/Services/ChapterContent.php @@ -12,6 +12,7 @@ use App\Services\ChapterVod as ChapterVodService; use App\Services\CourseStat as CourseStatService; use App\Services\Vod as VodService; use App\Validators\ChapterLive as ChapterLiveValidator; +use App\Validators\ChapterOffline as ChapterOfflineValidator; use App\Validators\ChapterRead as ChapterReadValidator; use App\Validators\ChapterVod as ChapterVodValidator; @@ -39,6 +40,13 @@ class ChapterContent extends Service return $chapterRepo->findChapterRead($chapterId); } + public function getChapterOffline($chapterId) + { + $chapterRepo = new ChapterRepo(); + + return $chapterRepo->findChapterOffline($chapterId); + } + public function getPlayUrls($chapterId) { $service = new ChapterVodService(); @@ -64,6 +72,9 @@ class ChapterContent extends Service case CourseModel::MODEL_READ: $this->updateChapterRead($chapter); break; + case CourseModel::MODEL_OFFLINE: + $this->updateChapterOffline($chapter); + break; } $this->rebuildCatalogCache($chapter); @@ -84,9 +95,7 @@ class ChapterContent extends Service /** * 无新文件上传 */ - if ($fileId == $vod->file_id) { - return; - } + if ($fileId == $vod->file_id) return; /** * 删除旧文件 @@ -95,21 +104,17 @@ class ChapterContent extends Service $this->deleteVodFile($vod->file_id); } - $vod->update([ - 'file_id' => $fileId, - 'file_transcode' => '', - ]); + $vod->file_id = $fileId; + $vod->file_transcode = []; + + $vod->update(); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; - $attrs['duration'] = 0; - $attrs['file']['status'] = ChapterModel::FS_UPLOADED; + $chapter->attrs = $attrs; - $chapter->update(['attrs' => $attrs]); + $chapter->update(); $this->updateCourseVodAttrs($vod->course_id); } @@ -129,20 +134,17 @@ class ChapterContent extends Service $validator->checkTimeRange($startTime, $endTime); - $live->update([ - 'start_time' => $startTime, - 'end_time' => $endTime, - ]); + $live->start_time = $startTime; + $live->end_time = $endTime; + + $live->update(); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; - $attrs['start_time'] = $startTime; $attrs['end_time'] = $endTime; + $chapter->attrs = $attrs; - $chapter->update(['attrs' => $attrs]); + $chapter->update(); $this->updateCourseLiveAttrs($live->course_id); } @@ -161,9 +163,6 @@ class ChapterContent extends Service $read->update(['content' => $content]); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; $attrs['word_count'] = WordUtil::getWordCount($content); @@ -174,6 +173,36 @@ class ChapterContent extends Service $this->updateCourseReadAttrs($read->course_id); } + protected function updateChapterOffline(ChapterModel $chapter) + { + $post = $this->request->getPost(); + + $chapterRepo = new ChapterRepo(); + + $offline = $chapterRepo->findChapterOffline($chapter->id); + + $validator = new ChapterOfflineValidator(); + + $startTime = $validator->checkStartTime($post['start_time']); + $endTime = $validator->checkEndTime($post['end_time']); + + $validator->checkTimeRange($startTime, $endTime); + + $offline->start_time = $startTime; + $offline->end_time = $endTime; + + $offline->update(); + + $attrs = $chapter->attrs; + $attrs['start_time'] = $startTime; + $attrs['end_time'] = $endTime; + $chapter->attrs = $attrs; + + $chapter->update(); + + $this->updateCourseOfflineAttrs($offline->course_id); + } + protected function updateCourseVodAttrs($courseId) { $statService = new CourseStatService(); @@ -195,6 +224,13 @@ class ChapterContent extends Service $statService->updateReadAttrs($courseId); } + protected function updateCourseOfflineAttrs($courseId) + { + $statService = new CourseStatService(); + + $statService->updateOfflineAttrs($courseId); + } + protected function deleteVodFile($fileId) { $vodService = new VodService(); diff --git a/app/Http/Admin/Services/Course.php b/app/Http/Admin/Services/Course.php index 7ee7204d..12c858f4 100644 --- a/app/Http/Admin/Services/Course.php +++ b/app/Http/Admin/Services/Course.php @@ -26,6 +26,7 @@ use App\Repos\ImGroup as ImGroupRepo; use App\Repos\User as UserRepo; use App\Services\Sync\CourseIndex as CourseIndexSync; use App\Validators\Course as CourseValidator; +use App\Validators\CourseOffline as CourseOfflineValidator; class Course extends Service { @@ -114,7 +115,7 @@ class Course extends Service $this->db->rollback(); - $logger = $this->getLogger(); + $logger = $this->getLogger('http'); $logger->error('Create Course Error ' . kg_json_encode([ 'code' => $e->getCode(), @@ -159,17 +160,24 @@ class Course extends Service $data['level'] = $validator->checkLevel($post['level']); } - if (isset($post['price_mode'])) { - if ($post['price_mode'] == 'free') { - $data['market_price'] = 0; - $data['vip_price'] = 0; - } else { - $data['origin_price'] = $validator->checkOriginPrice($post['origin_price']); - $data['market_price'] = $validator->checkMarketPrice($post['market_price']); - $data['vip_price'] = $validator->checkVipPrice($post['vip_price']); - $data['study_expiry'] = $validator->checkStudyExpiry($post['study_expiry']); - $data['refund_expiry'] = $validator->checkRefundExpiry($post['refund_expiry']); - } + if (isset($post['study_expiry'])) { + $data['study_expiry'] = $validator->checkStudyExpiry($post['study_expiry']); + } + + if (isset($post['refund_expiry'])) { + $data['refund_expiry'] = $validator->checkRefundExpiry($post['refund_expiry']); + } + + if (isset($post['origin_price'])) { + $data['origin_price'] = $validator->checkOriginPrice($post['origin_price']); + } + + if (isset($post['market_price'])) { + $data['market_price'] = $validator->checkMarketPrice($post['market_price']); + } + + if (isset($post['vip_price'])) { + $data['vip_price'] = $validator->checkVipPrice($post['vip_price']); } if (isset($post['featured'])) { @@ -195,6 +203,28 @@ class Course extends Service $this->saveRelatedCourses($course, $post['xm_course_ids']); } + if ($course->model == CourseModel::MODEL_OFFLINE) { + + $validator = new CourseOfflineValidator(); + + $data['study_expiry'] = 0; + $data['refund_expiry'] = 0; + + if (isset($post['attrs']['start_date']) && isset($post['attrs']['end_date'])) { + $data['attrs']['start_date'] = $validator->checkStartDate($post['attrs']['start_date']); + $data['attrs']['end_date'] = $validator->checkEndDate($post['attrs']['end_date']); + $validator->checkDateRange($data['attrs']['start_date'], $data['attrs']['end_date']); + } + + if (isset($post['attrs']['user_limit'])) { + $data['attrs']['user_limit'] = $validator->checkUserLimit($post['attrs']['user_limit']); + } + + if (isset($post['attrs']['location'])) { + $data['attrs']['location'] = $validator->checkLocation($post['attrs']['location']); + } + } + $course->update($data); $this->updateImGroup($course); diff --git a/app/Http/Admin/Services/ImGroup.php b/app/Http/Admin/Services/ImGroup.php index 9f4af997..8cad2a93 100644 --- a/app/Http/Admin/Services/ImGroup.php +++ b/app/Http/Admin/Services/ImGroup.php @@ -3,13 +3,15 @@ namespace App\Http\Admin\Services; use App\Builders\ImGroupList as ImGroupListBuilder; +use App\Builders\ImGroupUserList as ImGroupUserListBuilder; use App\Library\Paginator\Query as PagerQuery; use App\Models\ImGroup as ImGroupModel; use App\Models\ImGroupUser as ImGroupUserModel; -use App\Models\User as UserModel; +use App\Models\ImUser as ImUserModel; use App\Repos\ImGroup as ImGroupRepo; use App\Repos\ImGroupUser as ImGroupUserRepo; use App\Validators\ImGroup as ImGroupValidator; +use App\Validators\ImGroupUser as ImGroupUserValidator; class ImGroup extends Service { @@ -89,9 +91,10 @@ class ImGroup extends Service } if (isset($post['owner_id'])) { - $owner = $validator->checkGroupOwner($post['owner_id']); - $data['owner_id'] = $owner->id; - $this->handleGroupOwner($group, $owner); + $validator = new ImGroupUserValidator(); + $user = $validator->checkUser($post['owner_id']); + $data['owner_id'] = $user->id; + $this->handleGroupOwner($group, $user); } $group->update($data); @@ -121,7 +124,42 @@ class ImGroup extends Service return $group; } - protected function handleGroupOwner(ImGroupModel $group, UserModel $user) + public function getGroupUsers($id) + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['group_id'] = $id; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $groupUserRepo = new ImGroupUserRepo(); + + $pager = $groupUserRepo->paginate($params, $sort, $page, $limit); + + return $this->handleGroupUsers($pager); + } + + protected function handleGroupUsers($pager) + { + if ($pager->total_items == 0) { + return $pager; + } + + $builder = new ImGroupUserListBuilder(); + + $stepA = $pager->items->toArray(); + $stepB = $builder->handleUsers($stepA); + + $pager->items = $stepB; + + return $pager; + } + + protected function handleGroupOwner(ImGroupModel $group, ImUserModel $user) { $repo = new ImGroupUserRepo(); @@ -130,21 +168,40 @@ class ImGroup extends Service if ($groupUser) return; $groupUser = new ImGroupUserModel(); + $groupUser->group_id = $group->id; $groupUser->user_id = $user->id; + $groupUser->create(); + $this->incrGroupUserCount($group); + + $this->incrUserGroupCount($user); + } + + protected function incrGroupUserCount(ImGroupModel $group) + { $group->user_count += 1; + $group->update(); } + protected function incrUserGroupCount(ImUserModel $user) + { + $user->group_count += 1; + + $user->update(); + } + protected function handleGroups($pager) { if ($pager->total_items > 0) { $builder = new ImGroupListBuilder(); - $pipeA = $pager->items->toArray(); + $items = $pager->items->toArray(); + + $pipeA = $builder->handleGroups($items); $pipeB = $builder->handleUsers($pipeA); $pipeC = $builder->objects($pipeB); diff --git a/app/Http/Admin/Services/ImGroupUser.php b/app/Http/Admin/Services/ImGroupUser.php new file mode 100644 index 00000000..22305ece --- /dev/null +++ b/app/Http/Admin/Services/ImGroupUser.php @@ -0,0 +1,55 @@ +request->getQuery('group_id', 'int', 0); + $userId = $this->request->getQuery('user_id', 'int', 0); + + $validator = new ImGroupUserValidator(); + + $group = $validator->checkGroup($groupId); + $user = $validator->checkUser($userId); + + $validator->checkIfAllowDelete($groupId, $userId); + + $groupUser = $this->findOrFail($groupId, $userId); + + $groupUser->delete(); + + $this->decrGroupUserCount($group); + $this->decrUserGroupCount($user); + } + + protected function decrGroupUserCount(ImGroupModel $group) + { + if ($group->user_count > 0) { + $group->user_count -= 1; + $group->update(); + } + } + + protected function decrUserGroupCount(ImUserModel $user) + { + if ($user->group_count > 0) { + $user->group_count -= 1; + $user->update(); + } + } + + protected function findOrFail($groupId, $userId) + { + $validator = new ImGroupUserValidator(); + + return $validator->checkGroupUser($groupId, $userId); + } + +} diff --git a/app/Http/Admin/Services/Package.php b/app/Http/Admin/Services/Package.php index f89a13ce..07171e7b 100644 --- a/app/Http/Admin/Services/Package.php +++ b/app/Http/Admin/Services/Package.php @@ -6,6 +6,7 @@ use App\Caches\CoursePackageList as CoursePackageListCache; use App\Caches\Package as PackageCache; use App\Caches\PackageCourseList as PackageCourseListCache; use App\Library\Paginator\Query as PagerQuery; +use App\Models\Course as CourseModel; use App\Models\CoursePackage as CoursePackageModel; use App\Models\Package as PackageModel; use App\Repos\Course as CourseRepo; @@ -32,7 +33,20 @@ class Package extends Service $courseRepo = new CourseRepo(); - $items = $courseRepo->findAll(['free' => 0, 'published' => 1]); + /** + * 面授课程不参与套餐计划,因为无法进行退款计算 + */ + $model = [ + CourseModel::MODEL_VOD, + CourseModel::MODEL_LIVE, + CourseModel::MODEL_READ, + ]; + + $items = $courseRepo->findAll([ + 'model' => $model, + 'free' => 0, + 'published' => 1, + ]); if ($items->count() == 0) return []; diff --git a/app/Http/Admin/Services/User.php b/app/Http/Admin/Services/User.php index 539fcb32..7ebf83c9 100644 --- a/app/Http/Admin/Services/User.php +++ b/app/Http/Admin/Services/User.php @@ -267,7 +267,9 @@ class User extends Service $builder = new UserListBuilder(); - $pipeA = $pager->items->toArray(); + $items = $pager->items->toArray(); + + $pipeA = $builder->handleUsers($items); $pipeB = $builder->handleAdminRoles($pipeA); $pipeC = $builder->handleEduRoles($pipeB); $pipeD = $builder->objects($pipeC); diff --git a/app/Http/Admin/Views/chapter/edit_lesson.volt b/app/Http/Admin/Views/chapter/edit_lesson.volt index 4f0c7dc6..9f4e98ab 100644 --- a/app/Http/Admin/Views/chapter/edit_lesson.volt +++ b/app/Http/Admin/Views/chapter/edit_lesson.volt @@ -9,6 +9,8 @@ 直播信息 {% elseif model == '3' %} 图文信息 + {% elseif model == '4' %} + 面授信息 {% endif %} {%- endmacro %} @@ -33,6 +35,8 @@ {{ partial('chapter/edit_lesson_live') }} {% elseif course.model == 3 %} {{ partial('chapter/edit_lesson_read') }} + {% elseif course.model == 4 %} + {{ partial('chapter/edit_lesson_offline') }} {% endif %}
diff --git a/app/Http/Admin/Views/chapter/edit_lesson_offline.volt b/app/Http/Admin/Views/chapter/edit_lesson_offline.volt new file mode 100644 index 00000000..9dfb9ce4 --- /dev/null +++ b/app/Http/Admin/Views/chapter/edit_lesson_offline.volt @@ -0,0 +1,24 @@ +{% set offline.start_time = offline.start_time > 0 ? date('Y-m-d H:i:s',offline.start_time) : '' %} +{% set offline.end_time = offline.end_time > 0 ? date('Y-m-d H:i:s',offline.end_time) : '' %} + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/app/Http/Admin/Views/chapter/lessons.volt b/app/Http/Admin/Views/chapter/lessons.volt index 8ec5ab29..f4d22162 100644 --- a/app/Http/Admin/Views/chapter/lessons.volt +++ b/app/Http/Admin/Views/chapter/lessons.volt @@ -27,6 +27,8 @@ {{ partial('chapter/lessons_live') }} {% elseif course.model == 3 %} {{ partial('chapter/lessons_read') }} + {% elseif course.model == 4 %} + {{ partial('chapter/lessons_offline') }} {% endif %} {% endblock %} diff --git a/app/Http/Admin/Views/chapter/lessons_offline.volt b/app/Http/Admin/Views/chapter/lessons_offline.volt new file mode 100644 index 00000000..d7d899af --- /dev/null +++ b/app/Http/Admin/Views/chapter/lessons_offline.volt @@ -0,0 +1,64 @@ +{%- macro offline_time_info(attrs) %} + {% if attrs['start_time'] > 0 %} +

开始:{{ date('Y-m-d H:i',attrs['start_time']) }}

+

结束:{{ date('Y-m-d H:i',attrs['end_time']) }}

+ {% else %} + N/A + {% endif %} +{%- endmacro %} + + + + + + + + + + + + + + + + + + + + + + + + + {% for item in lessons %} + {% set edit_url = url({'for':'admin.chapter.edit','id':item.id}) %} + {% set update_url = url({'for':'admin.chapter.update','id':item.id}) %} + {% set delete_url = url({'for':'admin.chapter.delete','id':item.id}) %} + {% set restore_url = url({'for':'admin.chapter.restore','id':item.id}) %} + + + + + + + + + + {% endfor %} + +
编号名称时间排序免费发布操作
{{ item.id }} + {{ item.title }} + + {{ offline_time_info(item.attrs) }} +
+ + +
+
\ No newline at end of file diff --git a/app/Http/Admin/Views/course/add.volt b/app/Http/Admin/Views/course/add.volt index f457c85c..e0f00957 100644 --- a/app/Http/Admin/Views/course/add.volt +++ b/app/Http/Admin/Views/course/add.volt @@ -50,7 +50,8 @@ var modelTips = { '1': '通过音视频呈现课程内容,内容可视化,有图像有声音,适合大部分场景', '2': '通过直播呈现课程内容,交互性强,适合需要交互反馈、情绪表达的场景', - '3': '通过图文呈现课程内容,简单直接,适合撰写文档、书籍、教程的场景' + '3': '通过图文呈现课程内容,简单直接,适合撰写文档、书籍、教程的场景', + '4': '面对面讲授课程内容,传统教学,适合有条件开展线下教学的场景', }; var modelTipsBlock = $('#model-tips'); diff --git a/app/Http/Admin/Views/course/edit.volt b/app/Http/Admin/Views/course/edit.volt index 2fac4899..4104faa6 100644 --- a/app/Http/Admin/Views/course/edit.volt +++ b/app/Http/Admin/Views/course/edit.volt @@ -9,6 +9,9 @@
  • 基本信息
  • + {% if course.model == 4 %} +
  • 面授信息
  • + {% endif %}
  • 课程介绍
  • 营销设置
  • 相关课程
  • @@ -17,6 +20,11 @@
    {{ partial('course/edit_basic') }}
    + {% if course.model == 4 %} +
    + {{ partial('course/edit_offline') }} +
    + {% endif %}
    {{ partial('course/edit_desc') }}
    @@ -53,23 +61,16 @@ xmSelect.render({ el: '#xm-category-ids', name: 'xm_category_ids', + filterable: true, max: 5, - prop: { - name: 'name', - value: 'id' - }, data: {{ xm_categories|json_encode }} }); xmSelect.render({ el: '#xm-teacher-ids', name: 'xm_teacher_ids', - paging: true, + filterable: true, max: 5, - prop: { - name: 'name', - value: 'id' - }, data: {{ xm_teachers|json_encode }} }); @@ -86,19 +87,20 @@