diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..53c083b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/config/config.php +/vendor \ No newline at end of file diff --git a/.phalcon/.gitkeep b/.phalcon/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 9f7e125c..00000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# course-tencent-cloud - -#### Description -{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) \ No newline at end of file diff --git a/app/Caches/Cache.php b/app/Caches/Cache.php new file mode 100644 index 00000000..ed592a0f --- /dev/null +++ b/app/Caches/Cache.php @@ -0,0 +1,75 @@ +cache = $this->getDI()->get('cache'); + } + + /** + * 获取缓存内容 + * + * @param mixed $params + * @return mixed + */ + public function get($params = null) + { + $key = $this->getKey($params); + $content = $this->cache->get($key); + $lifetime = $this->getLifetime(); + + if (!$content) { + $content = $this->getContent($params); + $this->cache->save($key, $content, $lifetime); + $content = $this->cache->get($key); + } + + return $content; + } + + /** + * 删除缓存内容 + * + * @param mixed $params + */ + public function delete($params = null) + { + $key = $this->getKey($params); + $this->cache->delete($key); + } + + /** + * 获取缓存有效期 + * + * @return integer + */ + abstract protected function getLifetime(); + + /** + * 获取键值 + * + * @param mixed $params + * @return string + */ + abstract protected function getKey($params = null); + + /** + * 获取原始内容 + * + * @param mixed $params + * @return mixed + */ + abstract protected function getContent($params = null); + +} diff --git a/app/Caches/Category.php b/app/Caches/Category.php new file mode 100644 index 00000000..440b38d0 --- /dev/null +++ b/app/Caches/Category.php @@ -0,0 +1,57 @@ +getById($id); + + if (!$result) { + throw new ModelNotFoundException('category.not_found'); + } + + return $result; + } + + public function get($id) + { + $cacheOptions = [ + 'key' => $this->getKey($id), + 'lifetime' => $this->getLifetime(), + ]; + + $result = CategoryModel::query() + ->where('id = :id:', ['id' => $id]) + ->cache($cacheOptions) + ->execute() + ->getFirst(); + + return $result; + } + + public function delete($id) + { + $key = $this->getKey($id); + + $this->modelsCache->delete($key); + } + + public function getKey($id) + { + return "category:{$id}"; + } + + public function getLifetime() + { + return $this->lifetime; + } + +} diff --git a/app/Caches/Config.php b/app/Caches/Config.php new file mode 100644 index 00000000..d5c788ac --- /dev/null +++ b/app/Caches/Config.php @@ -0,0 +1,70 @@ +get(); + + if (!$items) return; + + $result = new \stdClass(); + + foreach ($items as $item) { + if ($item->section == $section) { + $result->{$item->item_key} = $item->item_value; + } + } + + return $result; + } + + /** + * 获取某个配置项的值 + * + * @param string $section + * @param string $key + * @return string|null + */ + public function getItemValue($section, $key) + { + $config = $this->getSectionConfig($section); + + $result = $config->{$key} ?? null; + + return $result; + } + + protected function getLifetime() + { + return $this->lifetime; + } + + protected function getKey($params = null) + { + return 'site_config'; + } + + protected function getContent($params = null) + { + $configRepo = new ConfigRepo(); + + $items = $configRepo->findAll(); + + return $items; + } + +} diff --git a/app/Caches/Course.php b/app/Caches/Course.php new file mode 100644 index 00000000..d02bfa98 --- /dev/null +++ b/app/Caches/Course.php @@ -0,0 +1,57 @@ +getById($id); + + if (!$result) { + throw new ModelNotFoundException('course.not_found'); + } + + return $result; + } + + public function get($id) + { + $cacheOptions = [ + 'key' => $this->getKey($id), + 'lifetime' => $this->getLifetime(), + ]; + + $result = CourseModel::query() + ->where('id = :id:', ['id' => $id]) + ->cache($cacheOptions) + ->execute() + ->getFirst(); + + return $result; + } + + public function delete($id) + { + $key = $this->getKey($id); + + $this->modelsCache->delete($key); + } + + public function getKey($id) + { + return "course:{$id}"; + } + + public function getLifetime() + { + return $this->lifetime; + } + +} diff --git a/app/Caches/Nav.php b/app/Caches/Nav.php new file mode 100644 index 00000000..1754b36f --- /dev/null +++ b/app/Caches/Nav.php @@ -0,0 +1,53 @@ +get(); + + if (!$items) return; + + $result = new \stdClass(); + + foreach ($items as $item) { + if ($item->position == 'top') { + $result->{$item->item_key} = $item->item_value; + } + } + + return $result; + } + + public function getBottomNav() + { + + } + + protected function getLifetime() + { + return $this->lifetime; + } + + protected function getKey($params = null) + { + return 'nav'; + } + + protected function getContent($params = null) + { + $navRepo = new NavRepo(); + + $items = $navRepo->findAll(); + + return $items; + } + +} diff --git a/app/Caches/User.php b/app/Caches/User.php new file mode 100644 index 00000000..5878f544 --- /dev/null +++ b/app/Caches/User.php @@ -0,0 +1,57 @@ +getById($id); + + if (!$result) { + throw new ModelNotFoundException('user.not_found'); + } + + return $result; + } + + public function get($id) + { + $cacheOptions = [ + 'key' => $this->getKey($id), + 'lifetime' => $this->getLifetime(), + ]; + + $result = UserModel::query() + ->where('id = :id:', ['id' => $id]) + ->cache($cacheOptions) + ->execute() + ->getFirst(); + + return $result; + } + + public function delete($id) + { + $key = $this->getKey($id); + + $this->modelsCache->delete($key); + } + + public function getKey($id) + { + return "user:{$id}"; + } + + public function getLifetime() + { + return $this->lifetime; + } + +} diff --git a/app/Console/Tasks/CleanLogTask.php b/app/Console/Tasks/CleanLogTask.php new file mode 100644 index 00000000..651980bf --- /dev/null +++ b/app/Console/Tasks/CleanLogTask.php @@ -0,0 +1,149 @@ +cleanCommonLog(); + $this->cleanConsoleLog(); + $this->cleanSqlLog(); + $this->cleanListenerLog(); + $this->cleanCaptchaLog(); + $this->cleanMailerLog(); + $this->cleanSmserLog(); + $this->cleanVodLog(); + $this->cleanStorageLog(); + $this->cleanAlipayLog(); + $this->cleanWxpayLog(); + $this->cleanRefundLog(); + } + + /** + * 清理通用日志 + */ + protected function cleanCommonLog() + { + $this->cleanLog('common', 7); + } + + /** + * 清理Console日志 + */ + protected function cleanConsoleLog() + { + $this->cleanLog('console', 7); + } + + /** + * 清理SQL日志 + */ + protected function cleanSqlLog() + { + $this->cleanLog('sql', 3); + } + + /** + * 清理监听者日志 + */ + protected function cleanListenerLog() + { + $this->cleanLog('listener', 7); + } + + /** + * 清理验证码服务日志 + */ + protected function cleanCaptchaLog() + { + $this->cleanLog('captcha', 7); + } + + /** + * 清理点播服务日志 + */ + protected function cleanVodLog() + { + $this->cleanLog('vod', 7); + } + + /** + * 清理存储服务日志 + */ + protected function cleanStorageLog() + { + $this->cleanLog('storage', 7); + } + + /** + * 清理短信服务日志 + */ + protected function cleanSmserLog() + { + $this->cleanLog('smser', 7); + } + + /** + * 清理邮件服务日志 + */ + protected function cleanMailerLog() + { + $this->cleanLog('mailer', 7); + } + + /** + * 清理阿里支付服务日志 + */ + protected function cleanAlipayLog() + { + $this->cleanLog('alipay', 30); + } + + /** + * 清理微信支付服务日志 + */ + protected function cleanWxpayLog() + { + $this->cleanLog('wxpay', 30); + } + + /** + * 清理退款日志 + */ + protected function cleanRefundLog() + { + $this->cleanLog('refund', 30); + } + + /** + * 清理日志文件 + * + * @param string $prefix + * @param integer $keepDays 保留天数 + * @return mixed + */ + protected function cleanLog($prefix, $keepDays) + { + $files = glob(log_path() . "/{$prefix}-*.log"); + + if (!$files) return false; + + foreach ($files as $file) { + $date = substr($file, -14, 10); + $today = date('Y-m-d'); + if (strtotime($today) - strtotime($date) >= $keepDays * 86400) { + $deleted = unlink($file); + if ($deleted) { + echo "Delete {$file} success" . PHP_EOL; + } else { + echo "Delete {$file} failed" . PHP_EOL; + } + } + } + } + +} diff --git a/app/Console/Tasks/CloseOrderTask.php b/app/Console/Tasks/CloseOrderTask.php new file mode 100644 index 00000000..711a2b4b --- /dev/null +++ b/app/Console/Tasks/CloseOrderTask.php @@ -0,0 +1,46 @@ +findOrders(); + + if ($orders->count() == 0) { + return; + } + + foreach ($orders as $order) { + $order->status = OrderModel::STATUS_CLOSED; + $order->update(); + } + } + + /** + * 查找待关闭订单 + * + * @param integer $limit + * @return \Phalcon\Mvc\Model\ResultsetInterface + */ + protected function findOrders($limit = 1000) + { + $status = OrderModel::STATUS_PENDING; + + $createdAt = time() - 12 * 3600; + + $orders = OrderModel::query() + ->where('status = :status:', ['status' => $status]) + ->andWhere('created_at < :created_at:', ['created_at' => $createdAt]) + ->limit($limit) + ->execute(); + + return $orders; + } + +} diff --git a/app/Console/Tasks/CloseTradeTask.php b/app/Console/Tasks/CloseTradeTask.php new file mode 100644 index 00000000..f87cda90 --- /dev/null +++ b/app/Console/Tasks/CloseTradeTask.php @@ -0,0 +1,94 @@ +findTrades(); + + if ($trades->count() == 0) { + return; + } + + foreach ($trades as $trade) { + if ($trade->channel == TradeModel::CHANNEL_ALIPAY) { + $this->closeAlipayTrade($trade); + } elseif ($trade->channel == TradeModel::CHANNEL_WXPAY) { + $this->closeWxpayTrade($trade); + } + } + } + + /** + * 关闭支付宝交易 + * + * @param TradeModel $trade + */ + protected function closeAlipayTrade($trade) + { + $service = new AlipayService(); + + $alyOrder = $service->findOrder($trade->sn); + + if ($alyOrder) { + if ($alyOrder->trade_status == 'WAIT_BUYER_PAY') { + $service->closeOrder($trade->sn); + } + } + + $trade->status = TradeModel::STATUS_CLOSED; + + $trade->update(); + } + + /** + * 关闭微信交易 + * + * @param TradeModel $trade + */ + protected function closeWxpayTrade($trade) + { + $service = new WxpayService(); + + $wxOrder = $service->findOrder($trade->sn); + + if ($wxOrder) { + if ($wxOrder->trade_state == 'NOTPAY') { + $service->closeOrder($trade->sn); + } + } + + $trade->status = TradeModel::STATUS_CLOSED; + + $trade->update(); + } + + /** + * 查找待关闭交易 + * + * @param integer $limit + * @return \Phalcon\Mvc\Model\ResultsetInterface + */ + protected function findTrades($limit = 5) + { + $status = TradeModel::STATUS_PENDING; + + $createdAt = time() - 15 * 60; + + $trades = TradeModel::query() + ->where('status = :status:', ['status' => $status]) + ->andWhere('created_at < :created_at:', ['created_at' => $createdAt]) + ->limit($limit) + ->execute(); + + return $trades; + } + +} diff --git a/app/Console/Tasks/CourseCountTask.php b/app/Console/Tasks/CourseCountTask.php new file mode 100644 index 00000000..f522cdc3 --- /dev/null +++ b/app/Console/Tasks/CourseCountTask.php @@ -0,0 +1,44 @@ +findAll(['level' => 2, 'deleted' => 0]); + + foreach ($subCategories as $category) { + + $courseCount = $repo->countCourses($category->id); + $category->course_count = $courseCount; + $category->update(); + + $parentId = $category->parent_id; + + if (isset($mapping[$parentId])) { + $mapping[$parentId] += $courseCount; + } else { + $mapping[$parentId] = $courseCount; + } + } + + $topCategories = $repo->findAll(['level' => 1, 'deleted' => 0]); + + foreach ($topCategories as $category) { + if (isset($mapping[$category->id])) { + $category->course_count = $mapping[$category->id]; + $category->update(); + } + } + } + +} diff --git a/app/Console/Tasks/ImageSyncTask.php b/app/Console/Tasks/ImageSyncTask.php new file mode 100644 index 00000000..8aac31b0 --- /dev/null +++ b/app/Console/Tasks/ImageSyncTask.php @@ -0,0 +1,50 @@ +where('id = 42') + ->execute(); + + $storage = new Storage(); + + foreach ($courses as $course) { + $cover = $course->cover; + if (Text::startsWith($cover, '//')) { + $cover = 'http:' . $cover; + } + $url = str_replace('-240-135', '', $cover); + + $fileName = parse_url($url, PHP_URL_PATH); + $filePath = tmp_path() . $fileName; + $content = file_get_contents($url); + file_put_contents($filePath, $content); + $keyName = $this->getKeyName($filePath); + $remoteUrl = $storage->putFile($keyName, $filePath); + if ($remoteUrl) { + $course->cover = $keyName; + $course->update(); + echo "upload cover of course {$course->id} success" . PHP_EOL; + } else { + echo "upload cover of course {$course->id} failed" . PHP_EOL; + } + } + } + + protected function getKeyName($filePath) + { + $ext = pathinfo($filePath, PATHINFO_EXTENSION); + return '/img/cover/' . date('YmdHis') . rand(1000, 9999) . '.' . $ext; + } + +} diff --git a/app/Console/Tasks/LearningTask.php b/app/Console/Tasks/LearningTask.php new file mode 100644 index 00000000..ce874343 --- /dev/null +++ b/app/Console/Tasks/LearningTask.php @@ -0,0 +1,139 @@ +cache = $this->getDI()->get('cache'); + + $keys = $this->cache->queryKeys('learning:'); + + if (empty($keys)) return; + + $keys = array_slice($keys, 0, 500); + + $prefix = $this->cache->getPrefix(); + + foreach ($keys as $key) { + /** + * 去掉前缀,避免重复加前缀导致找不到缓存 + */ + if ($prefix) { + $key = str_replace($prefix, '', $key); + } + $this->handleLearning($key); + } + } + + protected function handleLearning($key) + { + $content = $this->cache->get($key); + + if (empty($content->user_id)) { + return false; + } + + if (!empty($content->client_ip)) { + $region = kg_ip2region($content->client_ip); + $content->country = $region->country; + $content->province = $region->province; + $content->city = $region->city; + } + + $learningRepo = new LearningRepo(); + + $learning = $learningRepo->findByRequestId($content->request_id); + + if (!$learning) { + $learning = new LearningModel(); + $data = kg_object_array($content); + $learning->create($data); + } else { + $learning->duration += $content->duration; + $learning->update(); + } + + $this->updateChapterUser($content->chapter_id, $content->user_id, $content->duration, $content->position); + $this->updateCourseUser($content->course_id, $content->user_id, $content->duration); + + $this->cache->delete($key); + } + + protected function updateChapterUser($chapterId, $userId, $duration = 0, $position = 0) + { + $chapterUserRepo = new ChapterUserRepo(); + + $chapterUser = $chapterUserRepo->findChapterUser($chapterId, $userId); + + if (!$chapterUser) return false; + + $chapterRepo = new ChapterRepo(); + + $chapter = $chapterRepo->findById($chapterId); + + if (!$chapter) return false; + + $chapter->duration = $chapter->attrs['duration'] ?: 0; + + $chapterUser->duration += $duration; + $chapterUser->position = floor($position); + + /** + * 观看时长超过视频时长80%标记完成学习 + */ + if ($chapterUser->duration > $chapter->duration * 0.8) { + if ($chapterUser->finished == 0) { + $chapterUser->finished = 1; + $this->updateCourseProgress($chapterUser->course_id, $chapterUser->user_id); + } + } + + $chapterUser->update(); + } + + protected function updateCourseUser($courseId, $userId, $duration) + { + $courseUserRepo = new CourseUserRepo(); + + $courseUser = $courseUserRepo->findCourseUser($courseId, $userId); + + if ($courseUser) { + $courseUser->duration += $duration; + $courseUser->update(); + } + } + + protected function updateCourseProgress($courseId, $userId) + { + $courseUserRepo = new CourseUserRepo(); + + $courseUser = $courseUserRepo->findCourseUser($courseId, $userId); + + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($courseId); + + if ($courseUser) { + $count = $courseUserRepo->countFinishedChapters($courseId, $userId); + $courseUser->progress = intval(100 * $count / $course->lesson_count); + $courseUser->update(); + } + } + +} diff --git a/app/Console/Tasks/MainTask.php b/app/Console/Tasks/MainTask.php new file mode 100644 index 00000000..6523cfe8 --- /dev/null +++ b/app/Console/Tasks/MainTask.php @@ -0,0 +1,24 @@ +duration = 123; + + echo $chapter->duration; + } + +} diff --git a/app/Console/Tasks/MaintainTask.php b/app/Console/Tasks/MaintainTask.php new file mode 100644 index 00000000..ec641854 --- /dev/null +++ b/app/Console/Tasks/MaintainTask.php @@ -0,0 +1,37 @@ +getLogger('refund'); + + $tasks = $this->findTasks(); + + if ($tasks->count() == 0) { + return; + } + + $tradeRepo = new TradeRepo(); + $orderRepo = new OrderRepo(); + $refundRepo = new RefundRepo(); + + foreach ($tasks as $task) { + + $refund = $refundRepo->findBySn($task->item_info['refund']['sn']); + $trade = $tradeRepo->findBySn($task->item_info['refund']['trade_sn']); + $order = $orderRepo->findBySn($task->item_info['refund']['order_sn']); + + try { + + $this->db->begin(); + + $this->handleTradeRefund($trade, $refund); + $this->handleOrderRefund($order); + + $refund->status = RefundModel::STATUS_FINISHED; + + if ($refund->update() === false) { + throw new \RuntimeException('Update Refund Status Failed'); + } + + $trade->status = TradeModel::STATUS_REFUNDED; + + if ($trade->update() === false) { + throw new \RuntimeException('Update Trade Status Failed'); + } + + $order->status = OrderModel::STATUS_REFUNDED; + + if ($order->update() === false) { + throw new \RuntimeException('Update Order Status Failed'); + } + + $task->status = TaskModel::STATUS_FINISHED; + + if ($task->update() === false) { + throw new \RuntimeException('Update Task Status Failed'); + } + + $this->db->commit(); + + } catch (\Exception $e) { + + $this->db->rollback(); + + $task->try_count += 1; + + if ($task->try_count > self::TRY_COUNT) { + $task->status = TaskModel::STATUS_FAILED; + $refund->status = RefundModel::STATUS_FAILED; + $refund->update(); + } + + $task->update(); + + $logger->info('Refund Task Exception ' . kg_json_encode([ + 'message' => $e->getMessage(), + 'task' => $task->toArray(), + ])); + } + } + } + + /** + * 处理交易退款 + * + * @param TradeModel $trade + * @param RefundModel $refund + */ + protected function handleTradeRefund(TradeModel $trade, RefundModel $refund) + { + $response = false; + + if ($trade->channel == TradeModel::CHANNEL_ALIPAY) { + $alipay = new AlipayService(); + $response = $alipay->refundOrder([ + 'out_trade_no' => $trade->sn, + 'out_request_no' => $refund->sn, + 'refund_amount' => $refund->amount, + ]); + } elseif ($trade->channel == TradeModel::CHANNEL_WXPAY) { + $wxpay = new WxpayService(); + $response = $wxpay->refundOrder([ + 'out_trade_no' => $trade->sn, + 'out_refund_no' => $refund->sn, + 'total_fee' => 100 * $trade->order_amount, + 'refund_fee' => 100 * $refund->amount, + ]); + } + + if (!$response) { + throw new \RuntimeException('Payment Refund Failed'); + } + } + + /** + * 处理订单退款 + * + * @param OrderModel $order + */ + protected function handleOrderRefund(OrderModel $order) + { + switch ($order->item_type) { + case OrderModel::TYPE_COURSE: + $this->handleCourseOrderRefund($order); + break; + case OrderModel::TYPE_PACKAGE: + $this->handlePackageOrderRefund($order); + break; + case OrderModel::TYPE_REWARD: + $this->handleRewardOrderRefund($order); + break; + case OrderModel::TYPE_VIP: + $this->handleVipOrderRefund($order); + break; + case OrderModel::TYPE_TEST: + $this->handleTestOrderRefund($order); + break; + } + } + + /** + * 处理课程订单退款 + * + * @param OrderModel $order + */ + protected function handleCourseOrderRefund(OrderModel $order) + { + $courseUserRepo = new CourseUserRepo(); + + $courseUser = $courseUserRepo->findCourseStudent($order->item_id, $order->user_id); + + if ($courseUser) { + $courseUser->deleted = 1; + if ($courseUser->update() === false) { + throw new \RuntimeException('Delete Course User Failed'); + } + } + } + + /** + * 处理套餐订单退款 + * + * @param OrderModel $order + */ + protected function handlePackageOrderRefund(OrderModel $order) + { + $courseUserRepo = new CourseUserRepo(); + + foreach ($order->item_info['courses'] as $course) { + $courseUser = $courseUserRepo->findCourseStudent($course['id'], $order->user_id); + if ($courseUser) { + $courseUser->deleted = 1; + if ($courseUser->update() === false) { + throw new \RuntimeException('Delete Course User Failed'); + } + } + } + } + + /** + * 处理会员订单退款 + * + * @param OrderModel $order + */ + protected function handleVipOrderRefund(OrderModel $order) + { + $userRepo = new UserRepo(); + + $user = $userRepo->findById($order->user_id); + + $baseTime = $user->vip_expiry; + + switch ($order->item_info['vip']['duration']) { + case 'one_month': + $user->vip_expiry = strtotime('-1 months', $baseTime); + break; + case 'three_month': + $user->vip_expiry = strtotime('-3 months', $baseTime); + break; + case 'six_month': + $user->vip_expiry = strtotime('-6 months', $baseTime); + break; + case 'twelve_month': + $user->vip_expiry = strtotime('-12 months', $baseTime); + break; + } + + if ($user->vip_expiry < time()) { + $user->vip = 0; + } + + if ($user->update() === false) { + throw new \RuntimeException('Update User Vip Failed'); + } + } + + /** + * 处理打赏订单退款 + * + * @param OrderModel $order + */ + protected function handleRewardOrderRefund(OrderModel $order) + { + + } + + /** + * 处理测试订单退款 + * + * @param OrderModel $order + */ + protected function handleTestOrderRefund(OrderModel $order) + { + + } + + /** + * 查找退款任务 + * + * @param integer $limit + * @return \Phalcon\Mvc\Model\ResultsetInterface + */ + protected function findTasks($limit = 5) + { + $itemType = TaskModel::TYPE_REFUND; + $status = TaskModel::STATUS_PENDING; + $tryCount = self::TRY_COUNT; + + $tasks = TaskModel::query() + ->where('item_type = :item_type:', ['item_type' => $itemType]) + ->andWhere('status = :status:', ['status' => $status]) + ->andWhere('try_count < :try_count:', ['try_count' => $tryCount]) + ->orderBy('priority ASC,try_count DESC') + ->limit($limit) + ->execute(); + + return $tasks; + } + +} diff --git a/app/Console/Tasks/SpiderTask.php b/app/Console/Tasks/SpiderTask.php new file mode 100644 index 00000000..ac1f67ee --- /dev/null +++ b/app/Console/Tasks/SpiderTask.php @@ -0,0 +1,282 @@ +getCategoryId($category); + + if (empty($categoryId)) { + throw new \Exception('invalid category'); + } + + $url = "http://www.imooc.com/course/list?c={$category}&page={$page}"; + + $data = QueryList::get($url)->rules([ + 'link' => ['a.course-card', 'href'], + 'title' => ['h3.course-card-name', 'text'], + 'cover' => ['img.course-banner', 'data-original'], + 'summary' => ['p.course-card-desc', 'text'], + 'level' => ['.course-card-info>span:even', 'text'], + 'user_count' => ['.course-card-info>span:odd', 'text'], + ])->query()->getData(); + + if ($data->count() == 0) { + return false; + } + + foreach ($data->all() as $item) { + $course = [ + 'id' => substr($item['link'], 7), + 'category_id' => $categoryId, + 'title' => $item['title'], + 'cover' => $item['cover'], + 'summary' => $item['summary'], + 'user_count' => $item['user_count'], + 'level' => $this->getLevel($item['level']), + ]; + $model = new CourseModel(); + $model->save($course); + } + + echo sprintf("saved: %d course", $data->count()); + } + + public function courseAction() + { + $courses = CourseModel::query() + ->where('1 = 1') + ->andWhere('id > :id:', ['id' => 1128]) + ->orderBy('id asc') + ->execute(); + + $baseUrl = 'http://www.imooc.com/learn'; + + $instance = QueryList::getInstance(); + + foreach ($courses as $course) { + $url = $baseUrl . '/' . $course->id; + $ql = $instance->get($url); + $result = $this->handleCourseInfo($course, $ql); + if (!$result) { + continue; + } + $this->handleCourseChapters($course, $ql); + echo "finished course " . $course->id . PHP_EOL; + sleep(1); + } + } + + public function teacherAction() + { + $courses = CourseModel::query() + ->where('1 = 1') + ->groupBy('user_id') + ->execute(); + + foreach ($courses as $course) { + $this->handleTeacherInfo($course->user_id); + echo "finished teacher: {$course->user_id}" . PHP_EOL; + sleep(1); + } + } + + public function userAction() + { + $users = UserModel::query() + ->where('1 = 1') + ->andWhere('name = :name:', ['name' => '']) + ->execute(); + + foreach ($users as $user) { + $this->handleUserInfo($user->id); + echo "finished user: {$user->id}" . PHP_EOL; + sleep(1); + } + } + + protected function handleUserInfo($id) + { + $url = 'http://www.imooc.com/u/'. $id; + + $ql = QueryList::getInstance()->get($url); + + $data = []; + + $data['id'] = $id; + $data['avatar'] = $ql->find('.user-pic-bg>img')->attr('src'); + $data['name'] = $ql->find('h3.user-name>span')->text(); + $data['about'] = $ql->find('p.user-desc')->text(); + + $user = new UserModel(); + + $user->save($data); + } + + protected function handleTeacherInfo($id) + { + $url = 'http://www.imooc.com/t/'. $id; + + $ql = QueryList::getInstance()->get($url); + + $data = []; + + $data['id'] = $id; + $data['avatar'] = $ql->find('img.tea-header')->attr('src'); + $data['name'] = $ql->find('p.tea-nickname')->text(); + $data['title'] = $ql->find('p.tea-professional')->text(); + $data['about'] = $ql->find('p.tea-desc')->text(); + + $user = new UserModel(); + + $user->create($data); + } + + protected function handleCourseInfo(CourseModel $course, QueryList $ql) + { + $data = []; + + $data['user_id'] = $ql->find('img.js-usercard-dialog')->attr('data-userid'); + $data['description'] = $ql->find('.course-description')->text(); + $data['duration'] = $ql->find('.static-item:eq(1)>.meta-value')->text(); + $data['score'] = $ql->find('.score-btn>.meta-value')->text(); + + if (empty($data['user_id'])) { + return false; + } + + $data['duration'] = $this->getCourseDuration($data['duration']); + + return $course->update($data); + } + + protected function handleCourseChapters(CourseModel $course, QueryList $ql) + { + $topChapters = $ql->rules([ + 'title' => ['.chapter>h3', 'text'], + 'sub_chapter_html' => ['.chapter>.video', 'html'], + ])->query()->getData(); + + if ($topChapters->count() == 0) { + return false; + } + + foreach ($topChapters->all() as $item) { + $data = [ + 'course_id' => $course->id, + 'title' => $item['title'], + ]; + + // create top chapter + $chapter = new ChapterModel(); + $chapter->create($data); + + // create sub chapter + if (!empty($item['sub_chapter_html'])) { + $this->handleSubChapters($chapter, $item['sub_chapter_html']); + } + } + } + + protected function handleSubChapters(ChapterModel $topChapter, $subChapterHtml) + { + $ql = QueryList::html($subChapterHtml); + + $chapters = $ql->find('li')->texts(); + + if ($chapters->count() == 0) { + return false; + } + + foreach ($chapters->all() as $item) { + preg_match('/(\d{1,}-\d{1,})\s{1,}(.*?)\((.*?)\)/s', $item, $matches); + if (!isset($matches[3]) || empty($matches[3])) { + continue; + } + $data = [ + 'course_id' => $topChapter->course_id, + 'parent_id' => $topChapter->id, + 'title' => $matches[2], + 'duration' => $this->getChapterDuration($matches[3]), + ]; + $model = new ChapterModel(); + $model->create($data); + } + } + + protected function getCourseDuration($duration) + { + $hours = 0; + $minutes = 0; + + if (preg_match('/(.*?)小时(.*?)分/s', $duration, $matches)) { + $hours = trim($matches[1]); + $minutes = trim($matches[2]); + } elseif (preg_match('/(.*?)分/s', $duration, $matches)) { + $minutes = trim($matches[1]); + } + + return 3600 * $hours + 60 * $minutes; + } + + protected function getChapterDuration($duration) + { + if (strpos($duration, ':') === false) { + return 0; + } + + list($minutes, $seconds) = explode(':', trim($duration)); + + return 60 * $minutes + $seconds; + } + + protected function getLevel($type) + { + $mapping = [ + '入门' => CourseModel::LEVEL_ENTRY, + '初级' => CourseModel::LEVEL_JUNIOR, + '中级' => CourseModel::LEVEL_MIDDLE, + '高级' => CourseModel::LEVEL_SENIOR, + ]; + + return $mapping[$type] ?? CourseModel::LEVEL_ENTRY; + } + + protected function getCategoryId($type) + { + $mapping = [ + 'html' => 1, 'javascript' => 2, 'vuejs' => 10, 'reactjs' => 19, + 'angular' => 18, 'nodejs' => 16, 'jquery' => 15, + 'bootstrap' => 17, 'sassless' => 21, 'webapp' => 22, 'fetool' => 23, + 'html5' => 13, 'css3' => 14, + 'php' => 24, 'java' => 25, 'python' => 26, 'c' => 27, 'cplusplus' => 28, 'ruby' => 29, 'go' => 30, 'csharp' => 31, + 'android' => 32, 'ios' => 33, + 'mysql' => 36, 'mongodb' => 37, 'redis' => 38, 'oracle' => 39, 'pgsql' => 40, + 'cloudcomputing' => 42, 'bigdata' => 43, + 'unity3d' => 34, 'cocos2dx' => 35, + 'dxdh' => 46, 'uitool' => 47, 'uijc' => 48, + ]; + + return $mapping[$type] ?? 0; + } + +} diff --git a/app/Console/Tasks/Task.php b/app/Console/Tasks/Task.php new file mode 100644 index 00000000..dc875cd1 --- /dev/null +++ b/app/Console/Tasks/Task.php @@ -0,0 +1,17 @@ +getInstance($channel); + } + +} diff --git a/app/Console/Tasks/UnlockUserTask.php b/app/Console/Tasks/UnlockUserTask.php new file mode 100644 index 00000000..e1319a0a --- /dev/null +++ b/app/Console/Tasks/UnlockUserTask.php @@ -0,0 +1,45 @@ +findUsers(); + + if ($users->count() == 0) { + return; + } + + foreach ($users as $user) { + $user->locked = 0; + $user->locked_expiry = 0; + $user->update(); + } + } + + /** + * 查找待解锁用户 + * + * @param integer $limit + * @return \Phalcon\Mvc\Model\ResultsetInterface + */ + protected function findUsers($limit = 1000) + { + $time = time() - 6 * 3600; + + $users = UserModel::query() + ->where('locked = 1') + ->andWhere('locked_expiry < :time:', ['time' => $time]) + ->limit($limit) + ->execute(); + + return $users; + } + +} diff --git a/app/Console/Tasks/VodEventTask.php b/app/Console/Tasks/VodEventTask.php new file mode 100644 index 00000000..b0514f60 --- /dev/null +++ b/app/Console/Tasks/VodEventTask.php @@ -0,0 +1,157 @@ +pullEvents(); + + if (!$events) { + return; + } + + $handles = []; + + $count = 0; + + foreach ($events as $event) { + $handles[] = $event['EventHandle']; + if ($event['EventType'] == 'NewFileUpload') { + $this->handleNewFileUploadEvent($event); + } elseif ($event['EventType'] == 'ProcedureStateChanged') { + $this->handleProcedureStateChangedEvent($event); + } + $count++; + if ($count >= 12) { + break; + } + } + + $this->confirmEvents($handles); + } + + protected function handleNewFileUploadEvent($event) + { + $fileId = $event['FileUploadEvent']['FileId']; + $format = $event['FileUploadEvent']['MediaBasicInfo']['Type']; + $duration = $event['FileUploadEvent']['MetaData']['Duration']; + + $chapterRepo = new ChapterRepo(); + + $chapter = $chapterRepo->findByFileId($fileId); + + if (!$chapter) { + return; + } + + $vodService = new VodService(); + + if ($this->isAudioFile($format)) { + $vodService->createTransAudioTask($fileId); + } else { + $vodService->createTransVideoTask($fileId); + } + + /** + * @var \stdClass $attrs + */ + $attrs = $chapter->attrs; + $attrs->file_status = ChapterModel::FS_TRANSLATING; + $attrs->duration = $duration; + $chapter->update(['attrs' => $attrs]); + + $this->updateCourseDuration($chapter->course_id); + } + + protected function handleProcedureStateChangedEvent($event) + { + $fileId = $event['ProcedureStateChangeEvent']['FileId']; + $processResult = $event['ProcedureStateChangeEvent']['MediaProcessResultSet']; + + $chapterRepo = new ChapterRepo(); + + $chapter = $chapterRepo->findByFileId($fileId); + + if (!$chapter) { + return; + } + + $failCount = $successCount = 0; + + foreach ($processResult as $item) { + if ($item['Type'] == 'Transcode') { + if ($item['TranscodeTask']['Status'] == 'SUCCESS') { + $successCount++; + } elseif ($item['TranscodeTask']['Status'] == 'FAIL') { + $failCount++; + } + } + } + + $status = ChapterModel::FS_TRANSLATING; + + /** + * 当有一个成功标记为成功 + */ + if ($successCount > 0) { + $status = ChapterModel::FS_TRANSLATED; + } elseif ($failCount > 0) { + $status = ChapterModel::FS_FAILED; + } + + if ($status == ChapterModel::FS_TRANSLATING) { + return; + } + + /** + * @var \stdClass $attrs + */ + $attrs = $chapter->attrs; + $attrs->file_status = $status; + $chapter->update(['attrs' => $attrs]); + } + + protected function pullEvents() + { + $vodService = new VodService(); + + $result = $vodService->pullEvents(); + + return $result; + } + + protected function confirmEvents($handles) + { + $vodService = new VodService(); + + $result = $vodService->confirmEvents($handles); + + return $result; + } + + protected function isAudioFile($format) + { + $formats = ['mp3', 'm4a', 'wav', 'flac', 'ogg']; + + $result = in_array(strtolower($format), $formats); + + return $result; + } + + protected function updateCourseDuration($courseId) + { + $courseStats = new CourseStatsService(); + + $courseStats->updateVodDuration($courseId); + } + +} diff --git a/app/Exceptions/BadRequest.php b/app/Exceptions/BadRequest.php new file mode 100644 index 00000000..cf863209 --- /dev/null +++ b/app/Exceptions/BadRequest.php @@ -0,0 +1,8 @@ +getAudits(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/{id}/show", name="admin.audit.show") + */ + public function showAction($id) + { + $auditService = new AuditService(); + + $audit = $auditService->getAudit($id); + + $this->view->setVar('audit', $audit); + } + +} diff --git a/app/Http/Admin/Controllers/CategoryController.php b/app/Http/Admin/Controllers/CategoryController.php new file mode 100644 index 00000000..8e0482dc --- /dev/null +++ b/app/Http/Admin/Controllers/CategoryController.php @@ -0,0 +1,140 @@ +request->get('parent_id', 'int', 0); + + $service = new CategoryService(); + + $parent = $service->getParentCategory($parentId); + $categories = $service->getChildCategories($parentId); + + $this->view->setVar('parent', $parent); + $this->view->setVar('categories', $categories); + } + + /** + * @Get("/add", name="admin.category.add") + */ + public function addAction() + { + $parentId = $this->request->get('parent_id', 'int', 0); + + $service = new CategoryService(); + + $topCategories = $service->getTopCategories(); + + $this->view->setVar('parent_id', $parentId); + $this->view->setVar('top_categories', $topCategories); + } + + /** + * @Post("/create", name="admin.category.create") + */ + public function createAction() + { + $service = new CategoryService(); + + $category = $service->createCategory(); + + $location = $this->url->get( + ['for' => 'admin.category.list'], + ['parent_id' => $category->parent_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '创建分类成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.category.edit") + */ + public function editAction($id) + { + $service = new CategoryService(); + + $category = $service->getCategory($id); + + $this->view->setVar('category', $category); + } + + /** + * @Post("/{id}/update", name="admin.category.update") + */ + public function updateAction($id) + { + $service = new CategoryService(); + + $category = $service->getCategory($id); + + $service->updateCategory($id); + + $location = $this->url->get( + ['for' => 'admin.category.list'], + ['parent_id' => $category->parent_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '更新分类成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.category.delete") + */ + public function deleteAction($id) + { + $service = new CategoryService(); + + $service->deleteCategory($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除分类成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.category.restore") + */ + public function restoreAction($id) + { + $service = new CategoryService(); + + $service->restoreCategory($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原分类成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/ChapterController.php b/app/Http/Admin/Controllers/ChapterController.php new file mode 100644 index 00000000..1279cb1f --- /dev/null +++ b/app/Http/Admin/Controllers/ChapterController.php @@ -0,0 +1,215 @@ +getChapter($id); + $course = $courseService->getCourse($chapter->course_id); + $lessons = $chapterService->getLessons($chapter->id); + + $this->view->setVar('lessons', $lessons); + $this->view->setVar('chapter', $chapter); + $this->view->setVar('course', $course); + } + + /** + * @Get("/add", name="admin.chapter.add") + */ + public function addAction() + { + $courseId = $this->request->getQuery('course_id'); + $parentId = $this->request->getQuery('parent_id'); + $type = $this->request->getQuery('type'); + + $chapterService = new ChapterService(); + + $course = $chapterService->getCourse($courseId); + $courseChapters = $chapterService->getCourseChapters($courseId); + + $this->view->setVar('course', $course); + $this->view->setVar('parent_id', $parentId); + $this->view->setVar('course_chapters', $courseChapters); + + if ($type == 'chapter') { + $this->view->pick('chapter/add_chapter'); + } else { + $this->view->pick('chapter/add_lesson'); + } + } + + /** + * @Post("/create", name="admin.chapter.create") + */ + public function createAction() + { + $service = new ChapterService(); + + $chapter = $service->createChapter(); + + $location = $this->url->get([ + 'for' => 'admin.course.chapters', + 'id' => $chapter->course_id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建章节成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.chapter.edit") + */ + public function editAction($id) + { + $contentService = new ChapterContentService(); + $chapterService = new ChapterService(); + $configService = new ConfigService(); + + $chapter = $chapterService->getChapter($id); + $course = $chapterService->getCourse($chapter->course_id); + $storage = $configService->getSectionConfig('storage'); + + switch ($course->model) { + case CourseModel::MODEL_VOD: + $vod = $contentService->getChapterVod($chapter->id); + $translatedFiles = $contentService->getTranslatedFiles($vod->file_id); + $this->view->setVar('vod', $vod); + $this->view->setVar('translated_files', $translatedFiles); + break; + case CourseModel::MODEL_LIVE: + $live = $contentService->getChapterLive($chapter->id); + $this->view->setVar('live', $live); + break; + case CourseModel::MODEL_ARTICLE: + $article = $contentService->getChapterArticle($chapter->id); + $this->view->setVar('article', $article); + break; + } + + $this->view->setVar('storage', $storage); + $this->view->setVar('chapter', $chapter); + $this->view->setVar('course', $course); + + if ($chapter->parent_id > 0) { + $this->view->pick('chapter/edit_lesson'); + } else { + $this->view->pick('chapter/edit_chapter'); + } + } + + /** + * @Post("/{id}/update", name="admin.chapter.update") + */ + public function updateAction($id) + { + $service = new ChapterService(); + + $chapter = $service->updateChapter($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, + 'msg' => '更新章节成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.chapter.delete") + */ + public function deleteAction($id) + { + $service = new ChapterService(); + + $service->deleteChapter($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除章节成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.chapter.restore") + */ + public function restoreAction($id) + { + $service = new ChapterService(); + + $service->restoreChapter($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除章节成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/content", name="admin.chapter.content") + */ + public function contentAction($id) + { + $contentService = new ChapterContentService(); + + $contentService->updateChapterContent($id); + + $chapterService = new ChapterService(); + + $chapter = $chapterService->getChapter($id); + + $location = $this->url->get([ + 'for' => 'admin.chapter.lessons', + 'id' => $chapter->parent_id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '更新课时内容成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/ConfigController.php b/app/Http/Admin/Controllers/ConfigController.php new file mode 100644 index 00000000..91d338ab --- /dev/null +++ b/app/Http/Admin/Controllers/ConfigController.php @@ -0,0 +1,275 @@ +request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateSectionConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $website = $service->getSectionConfig($section); + + $this->view->setVar('website', $website); + } + } + + /** + * @Route("/secret", name="admin.config.secret") + */ + public function secretAction() + { + $section = 'secret'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateStorageConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $secret = $service->getSectionConfig($section); + + $this->view->setVar('secret', $secret); + } + } + + /** + * @Route("/storage", name="admin.config.storage") + */ + public function storageAction() + { + $section = 'storage'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateStorageConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $storage = $service->getSectionConfig($section); + + $this->view->setVar('storage', $storage); + } + } + + /** + * @Route("/vod", name="admin.config.vod") + */ + public function vodAction() + { + $section = 'vod'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateVodConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $vod = $service->getSectionConfig($section); + + $this->view->setVar('vod', $vod); + } + } + + /** + * @Route("/live", name="admin.config.live") + */ + public function liveAction() + { + $section = 'live'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateLiveConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $live = $service->getSectionConfig($section); + + $ptt = json_decode($live->pull_trans_template); + + $this->view->setVar('live', $live); + $this->view->setVar('ptt', $ptt); + } + } + + /** + * @Route("/payment", name="admin.config.payment") + */ + public function paymentAction() + { + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $section = $this->request->getPost('section'); + $data = $this->request->getPost(); + + $service->updateSectionConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $alipay = $service->getSectionConfig('payment.alipay'); + $wxpay = $service->getSectionConfig('payment.wxpay'); + + $this->view->setVar('alipay', $alipay); + $this->view->setVar('wxpay', $wxpay); + } + } + + /** + * @Route("/smser", name="admin.config.smser") + */ + public function smserAction() + { + $section = 'smser'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateSmserConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $smser = $service->getSectionConfig($section); + + $template = json_decode($smser->template); + + $this->view->setVar('smser', $smser); + $this->view->setVar('template', $template); + } + } + + /** + * @Route("/mailer", name="admin.config.mailer") + */ + public function mailerAction() + { + $section = 'mailer'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateSectionConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $mailer = $service->getSectionConfig($section); + + $this->view->setVar('mailer', $mailer); + } + } + + /** + * @Route("/captcha", name="admin.config.captcha") + */ + public function captchaAction() + { + $section = 'captcha'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateSectionConfig($section, $data); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '更新配置成功', + ]; + + return $this->ajaxSuccess($content); + + } else { + + $captcha = $service->getSectionConfig($section); + + $this->view->setVar('captcha', $captcha); + } + } + + /** + * @Route("/vip", name="admin.config.vip") + */ + public function vipAction() + { + $section = 'vip'; + + $service = new ConfigService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $service->updateSectionConfig($section, $data); + + return $this->ajaxSuccess(['msg' => '更新配置成功']); + + } else { + + $vip = $service->getSectionConfig($section); + + $this->view->setVar('vip', $vip); + } + } + +} diff --git a/app/Http/Admin/Controllers/Controller.php b/app/Http/Admin/Controllers/Controller.php new file mode 100644 index 00000000..72a5e1dc --- /dev/null +++ b/app/Http/Admin/Controllers/Controller.php @@ -0,0 +1,113 @@ +notSafeRequest()) { + if (!$this->checkHttpReferer() || !$this->checkCsrfToken()) { + $dispatcher->forward([ + 'controller' => 'public', + 'action' => 'csrf', + ]); + return false; + } + } + + $this->authUser = $this->getDI()->get('auth')->getAuthUser(); + + if (!$this->authUser) { + $dispatcher->forward([ + 'controller' => 'public', + 'action' => 'auth', + ]); + return false; + } + + $controller = $dispatcher->getControllerName(); + + $route = $this->router->getMatchedRoute(); + + /** + * 管理员忽略权限检查 + */ + if ($this->authUser->admin) { + return true; + } + + /** + * 特例白名单 + */ + $whitelist = [ + 'controller' => [ + 'public', 'index', 'storage', 'vod', 'test', + 'xm_course', + ], + 'route' => [ + 'admin.package.guiding', + ], + ]; + + /** + * 特定控制器忽略权限检查 + */ + if (in_array($controller, $whitelist['controller'])) { + return true; + } + + /** + * 特定路由忽略权限检查 + */ + if (in_array($route->getName(), $whitelist['route'])) { + return true; + } + + /** + * 执行路由权限检查 + */ + if (!in_array($route->getName(), $this->authUser->routes)) { + $dispatcher->forward([ + 'controller' => 'public', + 'action' => 'forbidden', + ]); + return false; + } + + return true; + } + + public function initialize() + { + $this->view->setVar('auth_user', $this->authUser); + } + + public function afterExecuteRoute(Dispatcher $dispatcher) + { + if ($this->request->isPost()) { + + $audit = new AuditModel(); + + $audit->user_id = $this->authUser->id; + $audit->user_name = $this->authUser->name; + $audit->user_ip = $this->request->getClientAddress(); + $audit->req_route = $this->router->getMatchedRoute()->getName(); + $audit->req_path = $this->request->getServer('REQUEST_URI'); + $audit->req_data = $this->request->getPost(); + + $audit->create(); + } + } + +} diff --git a/app/Http/Admin/Controllers/CourseController.php b/app/Http/Admin/Controllers/CourseController.php new file mode 100644 index 00000000..705512dd --- /dev/null +++ b/app/Http/Admin/Controllers/CourseController.php @@ -0,0 +1,147 @@ +getXmCategories(0); + + $this->view->setVar('xm_categories', $xmCategories); + } + + /** + * @Get("/list", name="admin.course.list") + */ + public function listAction() + { + $courseService = new CourseService(); + + $pager = $courseService->getCourses(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.course.add") + */ + public function addAction() + { + + } + + /** + * @Post("/create", name="admin.course.create") + */ + public function createAction() + { + $courseService = new CourseService(); + + $course = $courseService->createCourse(); + + $location = $this->url->get([ + 'for' => 'admin.course.edit', + 'id' => $course->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建课程成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.course.edit") + */ + public function editAction($id) + { + $courseService = new CourseService(); + + $course = $courseService->getCourse($id); + $xmTeachers = $courseService->getXmTeachers($id); + $xmCategories = $courseService->getXmCategories($id); + $xmCourses = $courseService->getXmCourses($id); + + $this->view->setVar('course', $course); + $this->view->setVar('xm_teachers', $xmTeachers); + $this->view->setVar('xm_categories', $xmCategories); + $this->view->setVar('xm_courses', $xmCourses); + } + + /** + * @Post("/{id}/update", name="admin.course.update") + */ + public function updateAction($id) + { + $courseService = new CourseService(); + + $courseService->updateCourse($id); + + $content = ['msg' => '更新课程成功']; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.course.delete") + */ + public function deleteAction($id) + { + $courseService = new CourseService(); + + $courseService->deleteCourse($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除课程成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.course.restore") + */ + public function restoreAction($id) + { + $courseService = new CourseService(); + + $courseService->restoreCourse($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原课程成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/chapters", name="admin.course.chapters") + */ + public function chaptersAction($id) + { + $courseService = new CourseService(); + + $course = $courseService->getCourse($id); + $chapters = $courseService->getChapters($id); + + $this->view->setVar('course', $course); + $this->view->setVar('chapters', $chapters); + } + +} diff --git a/app/Http/Admin/Controllers/IndexController.php b/app/Http/Admin/Controllers/IndexController.php new file mode 100644 index 00000000..3e642706 --- /dev/null +++ b/app/Http/Admin/Controllers/IndexController.php @@ -0,0 +1,55 @@ +getTopMenus(); + $leftMenus = $authMenu->getLeftMenus(); + + $this->view->setRenderLevel(View::LEVEL_ACTION_VIEW); + + $this->view->setVar('top_menus', $topMenus); + $this->view->setVar('left_menus', $leftMenus); + } + + /** + * @Get("/main", name="admin.main") + */ + public function mainAction() + { + /* + $service = new \App\Services\Order(); + $course = \App\Models\Course::findFirstById(1152); + $service->createCourseOrder($course); + */ + + /* + $service = new \App\Services\Order(); + $package = \App\Models\Package::findFirstById(5); + $service->createPackageOrder($package); + */ + + $refund = new \App\Services\Refund(); + $order = \App\Models\Order::findFirstById(131); + $amount = $refund->getRefundAmount($order); + + dd($amount); + + } + +} diff --git a/app/Http/Admin/Controllers/NavController.php b/app/Http/Admin/Controllers/NavController.php new file mode 100644 index 00000000..9fbac3d3 --- /dev/null +++ b/app/Http/Admin/Controllers/NavController.php @@ -0,0 +1,140 @@ +request->get('parent_id', 'int', 0); + + $navService = new NavService(); + + $parent = $navService->getParentNav($parentId); + $navs = $navService->getChildNavs($parentId); + + $this->view->setVar('parent', $parent); + $this->view->setVar('navs', $navs); + } + + /** + * @Get("/add", name="admin.nav.add") + */ + public function addAction() + { + $parentId = $this->request->get('parent_id', 'int', 0); + + $navService = new NavService(); + + $topNavs = $navService->getTopNavs(); + + $this->view->setVar('parent_id', $parentId); + $this->view->setVar('top_navs', $topNavs); + } + + /** + * @Post("/create", name="admin.nav.create") + */ + public function createAction() + { + $navService = new NavService(); + + $nav = $navService->createNav(); + + $location = $this->url->get( + ['for' => 'admin.nav.list'], + ['parent_id' => $nav->parent_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '创建导航成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.nav.edit") + */ + public function editAction($id) + { + $navService = new NavService(); + + $nav = $navService->getNav($id); + + $this->view->setVar('nav', $nav); + } + + /** + * @Post("/{id}/update", name="admin.nav.update") + */ + public function updateAction($id) + { + $navService = new NavService(); + + $nav = $navService->getNav($id); + + $navService->updateNav($id); + + $location = $this->url->get( + ['for' => 'admin.nav.list'], + ['parent_id' => $nav->parent_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '更新导航成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.nav.delete") + */ + public function deleteAction($id) + { + $navService = new NavService(); + + $navService->deleteNav($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除导航成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.nav.restore") + */ + public function restoreAction($id) + { + $navService = new NavService(); + + $navService->restoreNav($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原导航成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/OrderController.php b/app/Http/Admin/Controllers/OrderController.php new file mode 100644 index 00000000..d5be7bb9 --- /dev/null +++ b/app/Http/Admin/Controllers/OrderController.php @@ -0,0 +1,91 @@ +getOrders(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/{id}/show", name="admin.order.show") + */ + public function showAction($id) + { + $orderService = new OrderService(); + + $order = $orderService->getOrder($id); + $trades = $orderService->getTrades($order->sn); + $refunds = $orderService->getRefunds($order->sn); + $user = $orderService->getUser($order->user_id); + + $this->view->setVar('order', $order); + $this->view->setVar('trades', $trades); + $this->view->setVar('refunds', $refunds); + $this->view->setVar('user', $user); + } + + /** + * @Post("/{id}/close", name="admin.order.close") + */ + public function closeAction($id) + { + $orderService = new OrderService(); + + $orderService->closeOrder($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '关闭订单成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/refund", name="admin.order.refund") + */ + public function refundAction() + { + $tradeId = $this->request->getPost('trade_id', 'int'); + + $orderService = new OrderService; + + $orderService->refundTrade($tradeId); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '订单退款成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/PackageController.php b/app/Http/Admin/Controllers/PackageController.php new file mode 100644 index 00000000..84041618 --- /dev/null +++ b/app/Http/Admin/Controllers/PackageController.php @@ -0,0 +1,133 @@ +request->getQuery('xm_course_ids'); + + $packageService = new PackageService(); + + $courses = $packageService->getGuidingCourses($xmCourseIds); + $guidingPrice = $packageService->getGuidingPrice($courses); + + $this->view->setVar('courses', $courses); + $this->view->setVar('guiding_price', $guidingPrice); + } + + /** + * @Get("/list", name="admin.package.list") + */ + public function listAction() + { + $packageService = new PackageService(); + + $pager = $packageService->getPackages(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.package.add") + */ + public function addAction() + { + + } + + /** + * @Post("/create", name="admin.package.create") + */ + public function createAction() + { + $packageService = new PackageService(); + + $package = $packageService->createPackage(); + + $location = $this->url->get([ + 'for' => 'admin.package.edit', + 'id' => $package->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建套餐成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.package.edit") + */ + public function editAction($id) + { + $packageService = new PackageService(); + + $package = $packageService->getPackage($id); + $xmCourses = $packageService->getXmCourses($id); + + $this->view->setVar('package', $package); + $this->view->setVar('xm_courses', $xmCourses); + } + + /** + * @Post("/{id}/update", name="admin.package.update") + */ + public function updateAction($id) + { + $packageService = new PackageService(); + + $packageService->updatePackage($id); + + $content = ['msg' => '更新套餐成功']; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.package.delete") + */ + public function deleteAction($id) + { + $packageService = new PackageService(); + + $packageService->deletePackage($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除套餐成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.package.restore") + */ + public function restoreAction($id) + { + $packageService = new PackageService(); + + $packageService->restorePackage($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原套餐成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/PageController.php b/app/Http/Admin/Controllers/PageController.php new file mode 100644 index 00000000..047a0dc7 --- /dev/null +++ b/app/Http/Admin/Controllers/PageController.php @@ -0,0 +1,121 @@ +getPages(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.page.add") + */ + public function addAction() + { + + } + + /** + * @Post("/create", name="admin.page.create") + */ + public function createAction() + { + $service = new PageService(); + + $service->createPage(); + + $location = $this->url->get(['for' => 'admin.page.list']); + + $content = [ + 'location' => $location, + 'msg' => '创建单页成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.page.edit") + */ + public function editAction($id) + { + $service = new PageService; + + $page = $service->getPage($id); + + $this->view->setVar('page', $page); + } + + /** + * @Post("/{id}/update", name="admin.page.update") + */ + public function updateAction($id) + { + $service = new PageService(); + + $service->updatePage($id); + + $location = $this->url->get(['for' => 'admin.page.list']); + + $content = [ + 'location' => $location, + 'msg' => '更新单页成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.page.delete") + */ + public function deleteAction($id) + { + $service = new PageService(); + + $service->deletePage($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除单页成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.page.restore") + */ + public function restoreAction($id) + { + $service = new PageService(); + + $service->restorePage($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原单页成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/PublicController.php b/app/Http/Admin/Controllers/PublicController.php new file mode 100644 index 00000000..4f6793ec --- /dev/null +++ b/app/Http/Admin/Controllers/PublicController.php @@ -0,0 +1,63 @@ +request->isAjax()) { + return $this->ajaxError(['msg' => '会话已过期,请重新登录']); + } + + $this->response->redirect(['for' => 'admin.login']); + } + + /** + * @Route("/csrf", name="admin.csrf") + */ + public function csrfAction() + { + if ($this->request->isAjax()) { + return $this->ajaxError(['msg' => 'CSRF令牌验证失败']); + } + + $this->view->pick('public/csrf'); + } + + /** + * @Route("/forbidden", name="admin.forbidden") + */ + public function forbiddenAction() + { + if ($this->request->isAjax()) { + return $this->ajaxError(['msg' => '无相关操作权限']); + } + + $this->view->pick('public/forbidden'); + } + + /** + * @Route("/ip2region", name="admin.ip2region") + */ + public function ip2regionAction() + { + $ip = $this->request->getQuery('ip', 'trim'); + + $region = kg_ip2region($ip); + + $this->view->setVar('region', $region); + } + +} diff --git a/app/Http/Admin/Controllers/RefundController.php b/app/Http/Admin/Controllers/RefundController.php new file mode 100644 index 00000000..ab15c55f --- /dev/null +++ b/app/Http/Admin/Controllers/RefundController.php @@ -0,0 +1,70 @@ +getRefunds(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/{id}/show", name="admin.refund.show") + */ + public function showAction($id) + { + $refundService = new RefundService(); + + $refund = $refundService->getRefund($id); + $order = $refundService->getOrder($refund->order_sn); + $trade = $refundService->getTrade($refund->trade_sn); + $user = $refundService->getUser($trade->user_id); + + $this->view->setVar('refund', $refund); + $this->view->setVar('order', $order); + $this->view->setVar('trade', $trade); + $this->view->setVar('user', $user); + } + + /** + * @Post("/{id}/review", name="admin.refund.review") + */ + public function reviewAction($id) + { + $refundService = new RefundService; + + $refundService->reviewRefund($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '审核退款成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/ReviewController.php b/app/Http/Admin/Controllers/ReviewController.php new file mode 100644 index 00000000..aa1a5a79 --- /dev/null +++ b/app/Http/Admin/Controllers/ReviewController.php @@ -0,0 +1,108 @@ +getReviews(); + + $courseId = $this->request->getQuery('course_id', 'int', 0); + + $course = null; + + if ($courseId > 0) { + $course = $service->getCourse($courseId); + } + + $this->view->setVar('pager', $pager); + $this->view->setVar('course', $course); + } + + /** + * @Get("/{id}/edit", name="admin.review.edit") + */ + public function editAction($id) + { + $service = new ReviewService(); + + $review = $service->getReview($id); + + $this->view->setVar('review', $review); + } + + /** + * @Post("/{id}/update", name="admin.review.update") + */ + public function updateAction($id) + { + $service = new ReviewService(); + + $service->updateReview($id); + + $content = [ + 'msg' => '更新评价成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.review.delete") + */ + public function deleteAction($id) + { + $service = new ReviewService(); + + $service->deleteReview($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除评价成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.review.restore") + */ + public function restoreAction($id) + { + $service = new ReviewService(); + + $service->restoreReview($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原评价成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/RoleController.php b/app/Http/Admin/Controllers/RoleController.php new file mode 100644 index 00000000..31462d35 --- /dev/null +++ b/app/Http/Admin/Controllers/RoleController.php @@ -0,0 +1,132 @@ +getRoles(); + + $this->view->setVar('roles', $roles); + } + + /** + * @Get("/add", name="admin.role.add") + */ + public function addAction() + { + + } + + /** + * @Post("/create", name="admin.role.create") + */ + public function createAction() + { + $roleService = new RoleService(); + + $role = $roleService->createRole(); + + $location = $this->url->get([ + 'for' => 'admin.role.edit', + 'id' => $role->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建角色成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.role.edit") + */ + public function editAction($id) + { + $roleService = new RoleService(); + + $role = $roleService->getRole($id); + + //dd($role->routes); + + $adminNode = new AuthNode(); + + $nodes = $adminNode->getAllNodes(); + + $this->view->setVar('role', $role); + $this->view->setVar('nodes', kg_array_object($nodes)); + } + + /** + * @Post("/{id}/update", name="admin.role.update") + */ + public function updateAction($id) + { + $roleService = new RoleService(); + + $roleService->updateRole($id); + + $location = $this->url->get(['for' => 'admin.role.list']); + + $content = [ + 'location' => $location, + 'msg' => '更新角色成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.role.delete") + */ + public function deleteAction($id) + { + $roleService = new RoleService(); + + $roleService->deleteRole($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除角色成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.role.restore") + */ + public function restoreAction($id) + { + $roleService = new RoleService(); + + $roleService->restoreRole($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原角色成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/SessionController.php b/app/Http/Admin/Controllers/SessionController.php new file mode 100644 index 00000000..efbe80e0 --- /dev/null +++ b/app/Http/Admin/Controllers/SessionController.php @@ -0,0 +1,64 @@ +request->isPost()) { + + if (!$this->checkHttpReferer() || !$this->checkCsrfToken()) { + $this->dispatcher->forward([ + 'controller' => 'public', + 'action' => 'forbidden', + ]); + return false; + } + + $sessionService = new SessionService(); + + $sessionService->login(); + + $location = $this->url->get(['for' => 'admin.index']); + + return $this->ajaxSuccess(['location' => $location]); + } + + $configService = new ConfigService(); + + $captcha = $configService->getSectionConfig('captcha'); + + $this->view->pick('public/login'); + + $this->view->setVar('captcha', $captcha); + } + + /** + * @Get("/logout", name="admin.logout") + */ + public function logoutAction() + { + $service = new SessionService(); + + $service->logout(); + + $this->response->redirect(['for' => 'admin.login']); + } + +} diff --git a/app/Http/Admin/Controllers/SlideController.php b/app/Http/Admin/Controllers/SlideController.php new file mode 100644 index 00000000..754d2db3 --- /dev/null +++ b/app/Http/Admin/Controllers/SlideController.php @@ -0,0 +1,124 @@ +getSlides(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.slide.add") + */ + public function addAction() + { + + } + + /** + * @Post("/create", name="admin.slide.create") + */ + public function createAction() + { + $service = new SlideService(); + + $slide = $service->createSlide(); + + $location = $this->url->get([ + 'for' => 'admin.slide.edit', + 'id' => $slide->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建轮播成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.slide.edit") + */ + public function editAction($id) + { + $service = new SlideService(); + + $slide = $service->getSlide($id); + + $this->view->setVar('slide', $slide); + } + + /** + * @Post("/{id}/update", name="admin.slide.update") + */ + public function updateAction($id) + { + $service = new SlideService(); + + $service->updateSlide($id); + + $location = $this->url->get(['for' => 'admin.slide.list']); + + $content = [ + 'location' => $location, + 'msg' => '更新轮播成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/delete", name="admin.slide.delete") + */ + public function deleteAction($id) + { + $service = new SlideService(); + + $service->deleteSlide($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除轮播成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/restore", name="admin.slide.restore") + */ + public function restoreAction($id) + { + $service = new SlideService(); + + $service->restoreSlide($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原轮播成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/StorageController.php b/app/Http/Admin/Controllers/StorageController.php new file mode 100644 index 00000000..48294581 --- /dev/null +++ b/app/Http/Admin/Controllers/StorageController.php @@ -0,0 +1,47 @@ +uploadCoverImage(); + + $url = $storageService->getCiImageUrl($key); + + if ($url) { + return $this->ajaxSuccess(['data' => ['src' => $url, 'title' => '']]); + } else { + return $this->ajaxError(['msg' => '上传文件失败']); + } + } + + /** + * @Post("/content/img/upload", name="admin.content.img.upload") + */ + public function uploadContentImageAction() + { + $storageService = new StorageService(); + + $url = $storageService->uploadContentImage(); + + if ($url) { + return $this->ajaxSuccess(['data' => ['src' => $url, 'title' => '']]); + } else { + return $this->ajaxError(['msg' => '上传文件失败']); + } + } + +} diff --git a/app/Http/Admin/Controllers/StudentController.php b/app/Http/Admin/Controllers/StudentController.php new file mode 100644 index 00000000..6ceb4e22 --- /dev/null +++ b/app/Http/Admin/Controllers/StudentController.php @@ -0,0 +1,121 @@ +request->getQuery('course_id', 'int', ''); + + $service = new CourseStudentService(); + + $pager = $service->getCourseStudents(); + + $this->view->setVar('pager', $pager); + $this->view->setVar('course_id', $courseId); + } + + /** + * @Get("/add", name="admin.student.add") + */ + public function addAction() + { + $courseId = $this->request->getQuery('course_id', 'int', ''); + + $this->view->setVar('course_id', $courseId); + } + + /** + * @Post("/create", name="admin.student.create") + */ + public function createAction() + { + $service = new CourseStudentService(); + + $student = $service->createCourseStudent(); + + $location = $this->url->get( + ['for' => 'admin.student.list'], + ['course_id' => $student->course_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '添加学员成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/edit", name="admin.student.edit") + */ + public function editAction() + { + $courseId = $this->request->getQuery('course_id', 'int'); + $userId = $this->request->getQuery('user_id', 'int'); + + $service = new CourseStudentService(); + + $courseStudent = $service->getCourseStudent($courseId, $userId); + $course = $service->getCourse($courseId); + $student = $service->getStudent($userId); + + $this->view->setVar('course_student', $courseStudent); + $this->view->setVar('course', $course); + $this->view->setVar('student', $student); + } + + /** + * @Post("/update", name="admin.student.update") + */ + public function updateAction() + { + $service = new CourseStudentService(); + + $student = $service->updateCourseStudent(); + + $location = $this->url->get( + ['for' => 'admin.student.list'], + ['course_id' => $student->course_id] + ); + + $content = [ + 'location' => $location, + 'msg' => '更新学员成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/learning", name="admin.student.learning") + */ + public function learningAction() + { + $service = new CourseStudentService(); + + $pager = $service->getCourseLearnings(); + + $this->view->setVar('pager', $pager); + } + +} diff --git a/app/Http/Admin/Controllers/TestController.php b/app/Http/Admin/Controllers/TestController.php new file mode 100644 index 00000000..92e376e8 --- /dev/null +++ b/app/Http/Admin/Controllers/TestController.php @@ -0,0 +1,201 @@ +uploadTestFile(); + + if ($result) { + return $this->ajaxSuccess(['msg' => '上传文件成功,请到控制台确认']); + } else { + return $this->ajaxError(['msg' => '上传文件失败,请检查相关配置']); + } + } + + /** + * @Post("/vod", name="admin.test.vod") + */ + public function vodTestAction() + { + $vodService = new VodService(); + + $result = $vodService->test(); + + if ($result) { + return $this->ajaxSuccess(['msg' => '接口返回成功']); + } else { + return $this->ajaxError(['msg' => '接口返回失败,请检查相关配置']); + } + } + + /** + * @Get("/live/push", name="admin.test.live.push") + */ + public function livePushTestAction() + { + $liveService = new LiveService(); + + $pushUrl = $liveService->getPushUrl('test'); + + $obs = new \stdClass(); + + $position = strrpos($pushUrl, '/'); + $obs->fms_url = substr($pushUrl, 0, $position + 1); + $obs->stream_code = substr($pushUrl, $position + 1); + + $this->view->pick('config/live_push_test'); + $this->view->setVar('push_url', $pushUrl); + $this->view->setVar('obs', $obs); + } + + /** + * @Get("/live/pull", name="admin.test.live.pull") + */ + public function livePullTestAction() + { + $liveService = new LiveService(); + + $m3u8PullUrls = $liveService->getPullUrls('test', 'm3u8'); + $flvPullUrls = $liveService->getPullUrls('test', 'flv'); + + $this->view->setRenderLevel(View::LEVEL_ACTION_VIEW); + $this->view->pick('public/live_player'); + $this->view->setVar('m3u8_pull_urls', $m3u8PullUrls); + $this->view->setVar('flv_pull_urls', $flvPullUrls); + } + + /** + * @Post("/smser", name="admin.test.smser") + */ + public function smserTestAction() + { + $phone = $this->request->getPost('phone'); + + $smserService = new SmserService(); + + $response = $smserService->sendTestMessage($phone); + + if ($response) { + return $this->ajaxSuccess(['msg' => '发送短信成功,请到收件箱确认']); + } else { + return $this->ajaxError(['msg' => '发送短信失败,请查看短信日志']); + } + } + + /** + * @Post("/mailer", name="admin.test.mailer") + */ + public function mailerTestAction() + { + $email = $this->request->getPost('email'); + + $mailerService = new MailerService(); + + $result = $mailerService->sendTestMail($email); + + if ($result) { + return $this->ajaxSuccess(['msg' => '发送邮件成功,请到收件箱确认']); + } else { + return $this->ajaxError(['msg' => '发送邮件失败,请检查配置']); + } + } + + /** + * @Post("/captcha", name="admin.test.captcha") + */ + public function captchaTestAction() + { + $post = $this->request->getPost(); + + $captchaService = new CaptchaService(); + + $result = $captchaService->verify($post['ticket'], $post['rand']); + + if ($result) { + + $configService = new ConfigService(); + + $configService->updateSectionConfig('captcha', ['enabled' => 1]); + + return $this->ajaxSuccess(['msg' => '后台验证成功']); + + } else { + return $this->ajaxError(['msg' => '后台验证失败']); + } + } + + /** + * @Get("/alipay", name="admin.test.alipay") + */ + public function alipayTestAction() + { + $alipayTestService = new AlipayTestService(); + + $this->db->begin(); + + $order = $alipayTestService->createTestOrder(); + $trade = $alipayTestService->createTestTrade($order); + $qrcode = $alipayTestService->getTestQrCode($trade); + + if ($order->id > 0 && $trade->id > 0 && $qrcode) { + $this->db->commit(); + } else { + $this->db->rollback(); + } + + $this->view->pick('config/alipay_test'); + $this->view->setVar('trade', $trade); + $this->view->setVar('qrcode', $qrcode); + } + + /** + * @Post("/alipay/status", name="admin.test.alipay.status") + */ + public function alipayTestStatusAction() + { + $sn = $this->request->getPost('sn'); + + $alipayTestService = new AlipayTestService(); + + $status = $alipayTestService->getTestStatus($sn); + + return $this->ajaxSuccess(['status' => $status]); + } + + /** + * @Post("/alipay/cancel", name="admin.test.alipay.cancel") + */ + public function alipayTestCancelAction() + { + $sn = $this->request->getPost('sn'); + + $alipayTestService = new AlipayTestService(); + + $alipayTestService->cancelTestOrder($sn); + + return $this->ajaxSuccess(['msg' => '取消订单成功']); + } + +} diff --git a/app/Http/Admin/Controllers/TradeController.php b/app/Http/Admin/Controllers/TradeController.php new file mode 100644 index 00000000..f5869b41 --- /dev/null +++ b/app/Http/Admin/Controllers/TradeController.php @@ -0,0 +1,89 @@ +getTrades(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/{id}/show", name="admin.trade.show") + */ + public function showAction($id) + { + $tradeService = new TradeService(); + + $trade = $tradeService->getTrade($id); + $refunds = $tradeService->getRefunds($trade->sn); + $order = $tradeService->getOrder($trade->order_sn); + $user = $tradeService->getUser($trade->user_id); + + $this->view->setVar('refunds', $refunds); + $this->view->setVar('trade', $trade); + $this->view->setVar('order', $order); + $this->view->setVar('user', $user); + } + + /** + * @Post("/{id}/close", name="admin.trade.close") + */ + public function closeAction($id) + { + $tradeService = new TradeService(); + + $tradeService->closeTrade($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '关闭交易成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Post("/{id}/refund", name="admin.trade.refund") + */ + public function refundAction($id) + { + $tradeService = new TradeService(); + + $tradeService->refundTrade($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '申请退款成功,请到退款管理中审核确认', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/UserController.php b/app/Http/Admin/Controllers/UserController.php new file mode 100644 index 00000000..b60b9bea --- /dev/null +++ b/app/Http/Admin/Controllers/UserController.php @@ -0,0 +1,109 @@ +getRoles(); + + $this->view->setVar('roles', $roles); + } + + /** + * @Get("/list", name="admin.user.list") + */ + public function listAction() + { + $userService = new UserService(); + + $pager = $userService->getUsers(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/{id}/show", name="admin.user.show") + */ + public function showAction($id) + { + + } + + /** + * @Get("/add", name="admin.user.add") + */ + public function addAction() + { + $userService = new UserService(); + + $roles = $userService->getRoles(); + + $this->view->setVar('roles', $roles); + } + + /** + * @Post("/create", name="admin.user.create") + */ + public function createAction() + { + $userService = new UserService(); + + $userService->createUser(); + + $location = $this->url->get(['for' => 'admin.user.list']); + + $content = [ + 'location' => $location, + 'msg' => '新增用户成功', + ]; + + return $this->ajaxSuccess($content); + } + + /** + * @Get("/{id}/edit", name="admin.user.edit") + */ + public function editAction($id) + { + $userService = new UserService(); + + $user = $userService->getUser($id); + $roles = $userService->getRoles(); + + $this->view->setVar('user', $user); + $this->view->setVar('roles', $roles); + } + + /** + * @Post("/{id}/update", name="admin.user.update") + */ + public function updateAction($id) + { + $userService = new UserService(); + + $userService->updateUser($id); + + $location = $this->url->get(['for' => 'admin.user.list']); + + $content = [ + 'location' => $location, + 'msg' => '更新用户成功', + ]; + + return $this->ajaxSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/VodController.php b/app/Http/Admin/Controllers/VodController.php new file mode 100644 index 00000000..f84d7745 --- /dev/null +++ b/app/Http/Admin/Controllers/VodController.php @@ -0,0 +1,68 @@ +getUploadSignature(); + + return $this->ajaxSuccess(['signature' => $signature]); + } + + /** + * @Get("/player", name="admin.vod.player") + */ + public function playerAction() + { + $courseId = $this->request->getQuery('course_id'); + $chapterId = $this->request->getQuery('chapter_id'); + $playUrl = $this->request->getQuery('play_url'); + + $this->view->setRenderLevel(View::LEVEL_ACTION_VIEW); + + $this->view->pick('public/vod_player'); + + $this->view->setVar('course_id', $courseId); + $this->view->setVar('chapter_id', $chapterId); + $this->view->setVar('play_url', urldecode($playUrl)); + } + + /** + * @Get("/learning", name="admin.vod.learning") + */ + public function learningAction() + { + $query = $this->request->getQuery(); + + $learning = new LearningModel(); + + $learning->user_id = $this->authUser->id; + $learning->request_id = $query['request_id']; + $learning->course_id = $query['course_id']; + $learning->chapter_id = $query['chapter_id']; + $learning->position = $query['position']; + + $learningService = new LearningService(); + + $learningService->save($learning, $query['timeout']); + + return $this->ajaxSuccess(); + } + +} diff --git a/app/Http/Admin/Controllers/XmCourseController.php b/app/Http/Admin/Controllers/XmCourseController.php new file mode 100644 index 00000000..d470bb0e --- /dev/null +++ b/app/Http/Admin/Controllers/XmCourseController.php @@ -0,0 +1,43 @@ +getAllCourses(); + + return $this->ajaxSuccess([ + 'count' => $pager->total_items, + 'data' => $pager->items, + ]); + } + + /** + * @Get("/paid", name="admin.xm.course.paid") + */ + public function paidAction() + { + $xmCourseService = new XmCourseService(); + + $pager = $xmCourseService->getPaidCourses(); + + return $this->ajaxSuccess([ + 'count' => $pager->total_items, + 'data' => $pager->items, + ]); + } + +} diff --git a/app/Http/Admin/Module.php b/app/Http/Admin/Module.php new file mode 100644 index 00000000..9ae9f52e --- /dev/null +++ b/app/Http/Admin/Module.php @@ -0,0 +1,35 @@ +set('view', function () { + $view = new View(); + $view->setViewsDir(__DIR__ . '/Views'); + $view->registerEngines([ + '.volt' => 'volt', + ]); + return $view; + }); + + $di->setShared('auth', function () { + $authUser = new AuthUser(); + return $authUser; + }); + } + +} diff --git a/app/Http/Admin/Services/AlipayTest.php b/app/Http/Admin/Services/AlipayTest.php new file mode 100644 index 00000000..c61ea9bf --- /dev/null +++ b/app/Http/Admin/Services/AlipayTest.php @@ -0,0 +1,61 @@ + $trade->sn, + 'total_amount' => $trade->amount, + 'subject' => $trade->subject, + ]; + + $alipayService = new AlipayService(); + $qrcode = $alipayService->getQrCode($outOrder); + $result = $qrcode ?: false; + + return $result; + } + + /** + * 取消测试订单 + * + * @param string $sn + */ + public function cancelTestOrder($sn) + { + $tradeRepo = new TradeRepo(); + $trade = $tradeRepo->findBySn($sn); + + $orderRepo = new OrderRepo(); + $order = $orderRepo->findBySn($trade->order_sn); + + $alipayService = new AlipayService(); + $response = $alipayService->cancelOrder($trade->sn); + + if ($response) { + $trade->status = TradeModel::STATUS_CLOSED; + $trade->update(); + if ($order->status != OrderModel::STATUS_PENDING) { + $order->status = OrderModel::STATUS_PENDING; + $order->update(); + } + } + } + +} diff --git a/app/Http/Admin/Services/Audit.php b/app/Http/Admin/Services/Audit.php new file mode 100644 index 00000000..f62e5365 --- /dev/null +++ b/app/Http/Admin/Services/Audit.php @@ -0,0 +1,37 @@ +getParams(); + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $auditRepo = new AuditRepo(); + + $pager = $auditRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + + public function getAudit($id) + { + $auditRepo = new AuditRepo(); + + $audit = $auditRepo->findById($id); + + return $audit; + } + +} diff --git a/app/Http/Admin/Services/AuthMenu.php b/app/Http/Admin/Services/AuthMenu.php new file mode 100644 index 00000000..7f23b0b4 --- /dev/null +++ b/app/Http/Admin/Services/AuthMenu.php @@ -0,0 +1,122 @@ +authUser = $this->getAuthUser(); + $this->nodes = $this->getAllNodes(); + $this->setOwnedLevelIds(); + } + + public function getTopMenus() + { + $menus = []; + + foreach ($this->nodes as $node) { + if ($this->authUser->admin || in_array($node['id'], $this->owned1stLevelIds)) { + $menus[] = [ + 'id' => $node['id'], + 'label' => $node['label'], + ]; + } + } + + return kg_array_object($menus); + } + + public function getLeftMenus() + { + $menus = []; + + foreach ($this->nodes as $key => $level) { + foreach ($level['child'] as $key2 => $level2) { + foreach ($level2['child'] as $key3 => $level3) { + $hasRight = $this->authUser->admin || in_array($level3['id'], $this->owned3rdLevelIds); + if ($level3['type'] == 'menu' && $hasRight) { + $menus[$key]['id'] = $level['id']; + $menus[$key]['label'] = $level['label']; + $menus[$key]['child'][$key2]['id'] = $level2['id']; + $menus[$key]['child'][$key2]['label'] = $level2['label']; + $menus[$key]['child'][$key2]['child'][$key3] = [ + 'id' => $level3['id'], + 'label' => $level3['label'], + 'url' => $this->url->get(['for' => $level3['route']]), + ]; + } + } + } + } + + return kg_array_object($menus); + } + + protected function setOwnedLevelIds() + { + $routeIdMapping = $this->getRouteIdMapping(); + + if (!$routeIdMapping) return; + + $owned1stLevelIds = []; + $owned2ndLevelIds = []; + $owned3rdLevelIds = []; + + foreach ($routeIdMapping as $key => $value) { + $ids = explode('-', $value); + if (in_array($key, $this->authUser->routes)) { + $owned1stLevelIds[] = $ids[0]; + $owned2ndLevelIds[] = $ids[0] . '-' . $ids[1]; + $owned3rdLevelIds[] = $value; + } + } + + $this->owned1stLevelIds = array_unique($owned1stLevelIds); + $this->owned2ndLevelIds = array_unique($owned2ndLevelIds); + $this->owned3rdLevelIds = array_unique($owned3rdLevelIds); + } + + protected function getRouteIdMapping() + { + $mapping = []; + + foreach ($this->nodes as $level) { + foreach ($level['child'] as $level2) { + foreach ($level2['child'] as $level3) { + if ($level3['type'] == 'menu') { + $mapping[$level3['route']] = $level3['id']; + } + } + } + } + + return $mapping; + } + + protected function getAllNodes() + { + $authNode = new AuthNode(); + + return $authNode->getAllNodes(); + } + + protected function getAuthUser() + { + $auth = $this->getDI()->get('auth'); + + return $auth->getAuthUser(); + } + +} diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php new file mode 100644 index 00000000..800d3b6d --- /dev/null +++ b/app/Http/Admin/Services/AuthNode.php @@ -0,0 +1,582 @@ +getContentNodes(); + $nodes[] = $this->getOperationNodes(); + $nodes[] = $this->getFinanceNodes(); + $nodes[] = $this->getUserNodes(); + $nodes[] = $this->getConfigNodes(); + + return $nodes; + } + + protected function getContentNodes() + { + $nodes = [ + 'id' => '1', + 'label' => '内容管理', + 'child' => [ + [ + 'id' => '1-1', + 'label' => '课程管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '1-1-1', + 'label' => '课程列表', + 'type' => 'menu', + 'route' => 'admin.course.list', + ], + [ + 'id' => '1-1-2', + 'label' => '搜索课程', + 'type' => 'menu', + 'route' => 'admin.course.search', + ], + [ + 'id' => '1-1-3', + 'label' => '添加课程', + 'type' => 'menu', + 'route' => 'admin.course.add', + ], + [ + 'id' => '1-1-4', + 'label' => '编辑课程', + 'type' => 'button', + 'route' => 'admin.course.edit', + ], + [ + 'id' => '1-1-5', + 'label' => '删除课程', + 'type' => 'button', + 'route' => 'admin.course.edit', + ], + ], + ], + [ + 'id' => '1-2', + 'label' => '分类管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '1-2-1', + 'label' => '分类列表', + 'type' => 'menu', + 'route' => 'admin.category.list', + ], + [ + 'id' => '1-2-2', + 'label' => '添加分类', + 'type' => 'menu', + 'route' => 'admin.category.add', + ], + [ + 'id' => '1-2-3', + 'label' => '编辑分类', + 'type' => 'button', + 'route' => 'admin.category.edit', + ], + [ + 'id' => '1-2-4', + 'label' => '删除分类', + 'type' => 'button', + 'route' => 'admin.category.delete', + ], + ], + ], + [ + 'id' => '1-3', + 'label' => '套餐管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '1-3-1', + 'label' => '套餐列表', + 'type' => 'menu', + 'route' => 'admin.package.list', + ], + [ + 'id' => '1-3-2', + 'label' => '添加套餐', + 'type' => 'menu', + 'route' => 'admin.package.add', + ], + [ + 'id' => '1-3-3', + 'label' => '编辑套餐', + 'type' => 'button', + 'route' => 'admin.package.edit', + ], + [ + 'id' => '1-3-4', + 'label' => '删除套餐', + 'type' => 'button', + 'route' => 'admin.package.delete', + ], + ], + ], + ], + ]; + + return $nodes; + } + + protected function getOperationNodes() + { + $nodes = [ + 'id' => '2', + 'label' => '运营管理', + 'child' => [ + [ + 'id' => '2-5', + 'label' => '学员管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '2-5-1', + 'label' => '学员列表', + 'type' => 'menu', + 'route' => 'admin.student.list', + ], + [ + 'id' => '2-5-2', + 'label' => '搜索学员', + 'type' => 'menu', + 'route' => 'admin.student.search', + ], + [ + 'id' => '2-5-3', + 'label' => '添加学员', + 'type' => 'menu', + 'route' => 'admin.student.add', + ], + [ + 'id' => '2-5-4', + 'label' => '编辑学员', + 'type' => 'button', + 'route' => 'admin.student.edit', + ], + ], + ], + [ + 'id' => '2-1', + 'label' => '评价管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '2-1-1', + 'label' => '评价列表', + 'type' => 'menu', + 'route' => 'admin.review.list', + ], + [ + 'id' => '2-1-2', + 'label' => '搜索评价', + 'type' => 'menu', + 'route' => 'admin.review.search', + ], + [ + 'id' => '2-1-3', + 'label' => '编辑评价', + 'type' => 'button', + 'route' => 'admin.review.edit', + ], + [ + 'id' => '2-1-4', + 'label' => '删除评价', + 'type' => 'button', + 'route' => 'admin.review.delete', + ], + ], + ], + [ + 'id' => '2-3', + 'label' => '轮播管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '2-3-1', + 'label' => '轮播列表', + 'type' => 'menu', + 'route' => 'admin.slide.list', + ], + [ + 'id' => '2-3-2', + 'label' => '添加轮播', + 'type' => 'menu', + 'route' => 'admin.slide.add', + ], + [ + 'id' => '2-3-3', + 'label' => '编辑轮播', + 'type' => 'button', + 'route' => 'admin.slide.edit', + ], + [ + 'id' => '2-3-4', + 'label' => '删除轮播', + 'type' => 'button', + 'route' => 'admin.slide.delete', + ], + ], + ], + [ + 'id' => '2-4', + 'label' => '单页管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '2-4-1', + 'label' => '单页列表', + 'type' => 'menu', + 'route' => 'admin.page.list', + ], + [ + 'id' => '2-4-2', + 'label' => '添加单页', + 'type' => 'menu', + 'route' => 'admin.page.add', + ], + [ + 'id' => '2-4-3', + 'label' => '编辑单页', + 'type' => 'button', + 'route' => 'admin.page.edit', + ], + [ + 'id' => '2-4-4', + 'label' => '删除单页', + 'type' => 'button', + 'route' => 'admin.page.delete', + ], + ], + ], + [ + 'id' => '2-6', + 'label' => '导航管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '2-6-1', + 'label' => '导航列表', + 'type' => 'menu', + 'route' => 'admin.nav.list', + ], + [ + 'id' => '2-6-2', + 'label' => '添加导航', + 'type' => 'menu', + 'route' => 'admin.nav.add', + ], + [ + 'id' => '2-6-3', + 'label' => '编辑导航', + 'type' => 'button', + 'route' => 'admin.nav.edit', + ], + [ + 'id' => '2-6-4', + 'label' => '删除导航', + 'type' => 'button', + 'route' => 'admin.nav.delete', + ], + ], + ], + ], + ]; + + return $nodes; + } + + protected function getFinanceNodes() + { + $nodes = [ + 'id' => '3', + 'label' => '财务管理', + 'child' => [ + [ + 'id' => '3-1', + 'label' => '订单管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '3-1-1', + 'label' => '订单列表', + 'type' => 'menu', + 'route' => 'admin.order.list', + ], + [ + 'id' => '3-1-2', + 'label' => '搜索订单', + 'type' => 'menu', + 'route' => 'admin.order.search', + ], + [ + 'id' => '3-1-3', + 'label' => '订单详情', + 'type' => 'button', + 'route' => 'admin.order.show', + ], + [ + 'id' => '3-1-4', + 'label' => '关闭订单', + 'type' => 'button', + 'route' => 'admin.order.close', + ], + ], + ], + [ + 'id' => '3-2', + 'label' => '交易管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '3-2-1', + 'label' => '交易记录', + 'type' => 'menu', + 'route' => 'admin.trade.list', + ], + [ + 'id' => '3-2-2', + 'label' => '搜索交易', + 'type' => 'menu', + 'route' => 'admin.trade.search', + ], + [ + 'id' => '3-2-3', + 'label' => '关闭交易', + 'type' => 'button', + 'route' => 'admin.trade.close', + ], + [ + 'id' => '3-2-4', + 'label' => '交易退款', + 'type' => 'button', + 'route' => 'admin.trade.refund', + ], + ], + ], + [ + 'id' => '3-3', + 'label' => '退款管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '3-3-1', + 'label' => '退款列表', + 'type' => 'menu', + 'route' => 'admin.refund.list', + ], + [ + 'id' => '3-3-2', + 'label' => '搜索退款', + 'type' => 'menu', + 'route' => 'admin.refund.search', + ], + [ + 'id' => '3-3-3', + 'label' => '退款详情', + 'type' => 'button', + 'route' => 'admin.refund.show', + ], + [ + 'id' => '3-3-4', + 'label' => '审核退款', + 'type' => 'button', + 'route' => 'admin.refund.review', + ], + ], + ], + ], + ]; + + return $nodes; + } + + protected function getUserNodes() + { + $nodes = [ + 'id' => '4', + 'label' => '用户管理', + 'child' => [ + [ + 'id' => '4-1', + 'label' => '用户管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '4-1-1', + 'label' => '用户列表', + 'type' => 'menu', + 'route' => 'admin.user.list', + ], + [ + 'id' => '4-1-2', + 'label' => '搜索用户', + 'type' => 'menu', + 'route' => 'admin.user.search', + ], + [ + 'id' => '4-1-3', + 'label' => '添加用户', + 'type' => 'menu', + 'route' => 'admin.user.add', + ], + [ + 'id' => '4-1-4', + 'label' => '编辑用户', + 'type' => 'button', + 'route' => 'admin.user.edit', + ] + ], + ], + [ + 'id' => '4-2', + 'label' => '角色管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '4-2-1', + 'label' => '角色列表', + 'type' => 'menu', + 'route' => 'admin.role.list', + ], + [ + 'id' => '4-2-2', + 'label' => '添加角色', + 'type' => 'menu', + 'route' => 'admin.role.add', + ], + [ + 'id' => '4-2-3', + 'label' => '编辑角色', + 'type' => 'button', + 'route' => 'admin.role.edit', + ], + [ + 'id' => '4-2-4', + 'label' => '删除角色', + 'type' => 'button', + 'route' => 'admin.role.delete', + ] + ], + ], + [ + 'id' => '4-3', + 'label' => '操作记录', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '4-3-1', + 'label' => '记录列表', + 'type' => 'menu', + 'route' => 'admin.audit.list', + ], + [ + 'id' => '4-3-2', + 'label' => '搜索记录', + 'type' => 'menu', + 'route' => 'admin.audit.search', + ], + [ + 'id' => '4-3-3', + 'label' => '浏览记录', + 'type' => 'button', + 'route' => 'admin.audit.show', + ], + ], + ], + ], + ]; + + return $nodes; + } + + protected function getConfigNodes() + { + $nodes = [ + 'id' => '5', + 'label' => '系统配置', + 'child' => [ + [ + 'id' => '5-1', + 'label' => '配置管理', + 'type' => 'menu', + 'child' => [ + [ + 'id' => '5-1-1', + 'label' => '网站设置', + 'type' => 'menu', + 'route' => 'admin.config.website', + ], + [ + 'id' => '5-1-2', + 'label' => '密钥设置', + 'type' => 'menu', + 'route' => 'admin.config.secret', + ], + [ + 'id' => '5-1-3', + 'label' => '存储设置', + 'type' => 'menu', + 'route' => 'admin.config.storage', + ], + [ + 'id' => '5-1-4', + 'label' => '点播设置', + 'type' => 'menu', + 'route' => 'admin.config.vod', + ], + [ + 'id' => '5-1-5', + 'label' => '直播设置', + 'type' => 'menu', + 'route' => 'admin.config.live', + ], + [ + 'id' => '5-1-6', + 'label' => '短信设置', + 'type' => 'menu', + 'route' => 'admin.config.smser', + ], + [ + 'id' => '5-1-7', + 'label' => '邮件设置', + 'type' => 'menu', + 'route' => 'admin.config.mailer', + ], + [ + 'id' => '5-1-8', + 'label' => '验证码设置', + 'type' => 'menu', + 'route' => 'admin.config.captcha', + ], + [ + 'id' => '5-1-9', + 'label' => '支付设置', + 'type' => 'menu', + 'route' => 'admin.config.payment', + ], + [ + 'id' => '5-1-10', + 'label' => '会员设置', + 'type' => 'menu', + 'route' => 'admin.config.vip', + ] + ], + ], + ], + ]; + + return $nodes; + } + +} diff --git a/app/Http/Admin/Services/AuthUser.php b/app/Http/Admin/Services/AuthUser.php new file mode 100644 index 00000000..7bd1b3b8 --- /dev/null +++ b/app/Http/Admin/Services/AuthUser.php @@ -0,0 +1,91 @@ +getAuthUser(); + + if ($authUser->admin) return true; + + if (in_array($route, $authUser->routes)) return true; + + return false; + } + + /** + * 写入会话 + * + * @param UserModel $user + */ + public function setAuthUser(UserModel $user) + { + $role = RoleModel::findFirstById($user->admin_role); + + if ($role->id == RoleModel::ROLE_ADMIN) { + $admin = 1; + $routes = []; + } else { + $admin = 0; + $routes = $role->routes; + } + + $authKey = $this->getAuthKey(); + + $authUser = new \stdClass(); + + $authUser->id = $user->id; + $authUser->name = $user->name; + $authUser->avatar = $user->avatar; + $authUser->admin = $admin; + $authUser->routes = $routes; + + $this->session->set($authKey, $authUser); + } + + /** + * 清除会话 + */ + public function removeAuthUser() + { + $authKey = $this->getAuthKey(); + + $this->session->remove($authKey); + } + + /** + * 读取会话 + * + * @return mixed + */ + public function getAuthUser() + { + $authKey = $this->getAuthKey(); + + return $this->session->get($authKey); + } + + /** + * 获取会话键值 + * + * @return string + */ + public function getAuthKey() + { + return 'admin'; + } + +} diff --git a/app/Http/Admin/Services/Category.php b/app/Http/Admin/Services/Category.php new file mode 100644 index 00000000..814c8561 --- /dev/null +++ b/app/Http/Admin/Services/Category.php @@ -0,0 +1,163 @@ +findOrFail($id); + + return $category; + } + + public function getParentCategory($id) + { + if ($id > 0) { + $parent = CategoryModel::findFirst($id); + } else { + $parent = new CategoryModel(); + $parent->id = 0; + $parent->level = 0; + } + + return $parent; + } + + public function getTopCategories() + { + $categoryRepo = new CategoryRepo(); + + $categories = $categoryRepo->findAll([ + 'parent_id' => 0, + 'deleted' => 0, + ]); + + return $categories; + } + + public function getChildCategories($parentId) + { + $deleted = $this->request->getQuery('deleted', 'int', 0); + + $categoryRepo = new CategoryRepo(); + + $categories = $categoryRepo->findAll([ + 'parent_id' => $parentId, + 'deleted' => $deleted, + ]); + + return $categories; + } + + public function createCategory() + { + $post = $this->request->getPost(); + + $validator = new CategoryValidator(); + + $data = [ + 'parent_id' => 0, + 'published' => 1, + ]; + + $parent = null; + + if ($post['parent_id'] > 0) { + $parent = $validator->checkParent($post['parent_id']); + $data['parent_id'] = $parent->id; + } + + $data['name'] = $validator->checkName($post['name']); + $data['priority'] = $validator->checkPriority($post['priority']); + $data['published'] = $validator->checkPublishStatus($post['published']); + + $category = new CategoryModel(); + + $category->create($data); + + if ($parent) { + $category->path = $parent->path . $category->id . ','; + $category->level = $parent->level + 1; + } else { + $category->path = ',' . $category->id . ','; + $category->level = 1; + } + + $category->update(); + + return $category; + } + + public function updateCategory($id) + { + $category = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new CategoryValidator(); + + $data = []; + + if (isset($post['name'])) { + $data['name'] = $validator->checkName($post['name']); + } + + if (isset($post['priority'])) { + $data['priority'] = $validator->checkPriority($post['priority']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $category->update($data); + + return $category; + } + + public function deleteCategory($id) + { + $category = $this->findOrFail($id); + + if ($category->deleted == 1) { + return false; + } + + $category->deleted = 1; + + $category->update(); + + return $category; + } + + public function restoreCategory($id) + { + $category = $this->findOrFail($id); + + if ($category->deleted == 0) { + return false; + } + + $category->deleted = 0; + + $category->update(); + + return $category; + } + + protected function findOrFail($id) + { + $validator = new CategoryValidator(); + + $result = $validator->checkCategory($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/Chapter.php b/app/Http/Admin/Services/Chapter.php new file mode 100644 index 00000000..5aee88a0 --- /dev/null +++ b/app/Http/Admin/Services/Chapter.php @@ -0,0 +1,248 @@ +findById($courseId); + + return $result; + } + + public function getCourseChapters($courseId) + { + $chapterRepo = new ChapterRepo(); + + $result = $chapterRepo->findAll([ + 'course_id' => $courseId, + 'parent_id' => 0, + 'deleted' => 0, + ]); + + return $result; + } + + public function getLessons($parentId) + { + $deleted = $this->request->getQuery('deleted', 'int', 0); + + $chapterRepo = new ChapterRepo(); + + $result = $chapterRepo->findAll([ + 'parent_id' => $parentId, + 'deleted' => $deleted, + ]); + + return $result; + } + + public function getChapter($id) + { + $chapter = $this->findOrFail($id); + + return $chapter; + } + + public function createChapter() + { + $post = $this->request->getPost(); + + $validator = new ChapterValidator(); + + $data = []; + + $data['course_id'] = $validator->checkCourseId($post['course_id']); + $data['title'] = $validator->checkTitle($post['title']); + $data['summary'] = $validator->checkSummary($post['summary']); + $data['free'] = $validator->checkFreeStatus($post['free']); + + $chapterRepo = new ChapterRepo(); + + if (isset($post['parent_id'])) { + $data['parent_id'] = $validator->checkParentId($post['parent_id']); + $data['priority'] = $chapterRepo->maxLessonPriority($post['parent_id']); + } else { + $data['priority'] = $chapterRepo->maxChapterPriority($post['course_id']); + } + + $data['priority'] += 1; + + $chapter = new ChapterModel(); + + $chapter->create($data); + + $this->updateChapterStats($chapter); + $this->updateCourseStats($chapter); + + return $chapter; + } + + public function updateChapter($id) + { + $chapter = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new ChapterValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['summary'])) { + $data['summary'] = $validator->checkSummary($post['summary']); + } + + if (isset($post['priority'])) { + $data['priority'] = $validator->checkPriority($post['priority']); + } + + if (isset($post['free'])) { + $data['free'] = $validator->checkFreeStatus($post['free']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + if ($post['published'] == 1) { + $validator->checkPublishAbility($chapter); + } + } + + $chapter->update($data); + + $this->updateChapterStats($chapter); + $this->updateCourseStats($chapter); + + return $chapter; + } + + public function deleteChapter($id) + { + $chapter = $this->findOrFail($id); + + if ($chapter->deleted == 1) { + return false; + } + + $chapter->deleted = 1; + + $chapter->update(); + + if ($chapter->parent_id == 0) { + $this->deleteChildChapters($chapter->id); + } + + return $chapter; + } + + public function restoreChapter($id) + { + $chapter = $this->findOrFail($id); + + if ($chapter->deleted == 0) { + return false; + } + + $chapter->deleted = 0; + + $chapter->update(); + + if ($chapter->parent_id == 0) { + $this->restoreChildChapters($chapter->id); + } + + $this->updateChapterStats($chapter); + $this->updateCourseStats($chapter); + + return $chapter; + } + + protected function deleteChildChapters($parentId) + { + $chapterRepo = new ChapterRepo(); + + $chapters = $chapterRepo->findAll(['parent_id' => $parentId]); + + if ($chapters->count() == 0) { + return; + } + + foreach ($chapters as $chapter) { + $chapter->deleted = 1; + $chapter->update(); + } + } + + protected function restoreChildChapters($parentId) + { + $chapterRepo = new ChapterRepo(); + + $chapters = $chapterRepo->findAll(['parent_id' => $parentId]); + + if ($chapters->count() == 0) { + return; + } + + foreach ($chapters as $chapter) { + $chapter->deleted = 0; + $chapter->update(); + } + } + + protected function updateChapterStats($chapter) + { + $chapterRepo = new ChapterRepo(); + + if ($chapter->parent_id > 0) { + $chapter = $chapterRepo->findById($chapter->parent_id); + } + + $lessonCount = $chapterRepo->countLessons($chapter->id); + $chapter->lesson_count = $lessonCount; + $chapter->update(); + + } + + protected function updateCourseStats($chapter) + { + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($chapter->course_id); + + $courseStats = new CourseStatsService(); + + $courseStats->updateLessonCount($course->id); + + if ($course->model == CourseModel::MODEL_VOD) { + $courseStats->updateVodDuration($course->id); + } elseif ($course->model == CourseModel::MODEL_LIVE) { + $courseStats->updateLiveDateRange($course->id); + } elseif ($course->model == CourseModel::MODEL_ARTICLE) { + $courseStats->updateArticleWordCount($course->id); + } + } + + protected function findOrFail($id) + { + $validator = new ChapterValidator(); + + $result = $validator->checkChapter($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/ChapterContent.php b/app/Http/Admin/Services/ChapterContent.php new file mode 100644 index 00000000..a91328a8 --- /dev/null +++ b/app/Http/Admin/Services/ChapterContent.php @@ -0,0 +1,178 @@ +findChapterVod($chapterId); + + return $result; + } + + public function getChapterLive($chapterId) + { + $chapterRepo = new ChapterRepo(); + + $result = $chapterRepo->findChapterLive($chapterId); + + return $result; + } + + public function getChapterArticle($chapterId) + { + $chapterRepo = new ChapterRepo(); + + $result = $chapterRepo->findChapterArticle($chapterId); + + return $result; + } + + public function getTranslatedFiles($fileId) + { + if (!$fileId) return; + + $vodService = new VodService(); + + $mediaInfo = $vodService->getMediaInfo($fileId); + + if (!$mediaInfo) return; + + $result = []; + + $files = $mediaInfo['MediaInfoSet'][0]['TranscodeInfo']['TranscodeSet']; + + foreach ($files as $file) { + + if ($file['Definition'] == 0) { + continue; + } + + $result[] = [ + 'play_url' => $vodService->getPlayUrl($file['Url']), + 'width' => $file['Width'], + 'height' => $file['Height'], + 'definition' => $file['Definition'], + 'duration' => kg_play_duration($file['Duration']), + 'format' => pathinfo($file['Url'], PATHINFO_EXTENSION), + 'size' => sprintf('%0.2f', $file['Size'] / 1024 / 1024), + 'bit_rate' => intval($file['Bitrate'] / 1024), + ]; + } + + return kg_array_object($result); + } + + public function updateChapterContent($chapterId) + { + $chapterRepo = new ChapterRepo(); + $chapter = $chapterRepo->findById($chapterId); + + $courseRepo = new CourseRepo(); + $course = $courseRepo->findById($chapter->course_id); + + switch ($course->model) { + case CourseModel::MODEL_VOD: + $this->updateChapterVod($chapter); + break; + case CourseModel::MODEL_LIVE: + $this->updateChapterLive($chapter); + break; + case CourseModel::MODEL_ARTICLE: + $this->updateChapterArticle($chapter); + break; + } + } + + protected function updateChapterVod($chapter) + { + $post = $this->request->getPost(); + + $validator = new ChapterVodValidator(); + + $fileId = $validator->checkFileId($post['file_id']); + + $chapterRepo = new ChapterRepo(); + + $vod = $chapterRepo->findChapterVod($chapter->id); + + if ($fileId == $vod->file_id) { + return; + } + + $vod->update(['file_id' => $fileId]); + + $attrs = $chapter->attrs; + $attrs->file_id = $fileId; + $attrs->file_status = ChapterModel::FS_UPLOADED; + $chapter->update(['attrs' => $attrs]); + } + + protected function updateChapterLive($chapter) + { + $post = $this->request->getPost(); + + $chapterRepo = new ChapterRepo(); + + $live = $chapterRepo->findChapterLive($chapter->id); + + $validator = new ChapterLiveValidator(); + + $data = []; + + $data['start_time'] = $validator->checkStartTime($post['start_time']); + $data['end_time'] = $validator->checkEndTime($post['end_time']); + + $validator->checkTimeRange($post['start_time'], $post['end_time']); + + $live->update($data); + + $attrs = $chapter->attrs; + $attrs->start_time = $data['start_time']; + $attrs->end_time = $data['end_time']; + $chapter->update(['attrs' => $attrs]); + + $courseStats = new CourseStatsService(); + $courseStats->updateLiveDateRange($chapter->course_id); + } + + protected function updateChapterArticle($chapter) + { + $post = $this->request->getPost(); + + $chapterRepo = new ChapterRepo(); + + $article = $chapterRepo->findChapterArticle($chapter->id); + + $validator = new ChapterArticleValidator(); + + $data = []; + + $data['content'] = $validator->checkContent($post['content']); + + $article->update($data); + + $attrs = $chapter->attrs; + $attrs->word_count = WordUtil::getWordCount($article->content); + $chapter->update(['attrs' => $attrs]); + + $courseStats = new CourseStatsService(); + $courseStats->updateArticleWordCount($chapter->course_id); + } + +} diff --git a/app/Http/Admin/Services/Config.php b/app/Http/Admin/Services/Config.php new file mode 100644 index 00000000..c79ccbdf --- /dev/null +++ b/app/Http/Admin/Services/Config.php @@ -0,0 +1,116 @@ +findBySection($section); + + $result = new \stdClass(); + + if ($items->count() > 0) { + foreach ($items as $item) { + $result->{$item->item_key} = $item->item_value; + } + } + + return $result; + } + + public function updateSectionConfig($section, $config) + { + $configRepo = new ConfigRepo(); + + foreach ($config as $key => $value) { + $item = $configRepo->findItem($section, $key); + if ($item) { + $item->item_value = trim($value); + $item->update(); + } + } + + $configCache = new ConfigCache(); + + $configCache->delete(); + } + + public function updateStorageConfig($section, $config) + { + $protocol = ['http://', 'https://']; + + if (isset($config['bucket_domain'])) { + $config['bucket_domain'] = str_replace($protocol, '', $config['bucket_domain']); + } + + if (isset($config['ci_domain'])) { + $config['ci_domain'] = str_replace($protocol, '', $config['ci_domain']); + } + + $this->updateSectionConfig($section, $config); + } + + public function updateVodConfig($section, $config) + { + $this->updateSectionConfig($section, $config); + } + + public function updateLiveConfig($section, $config) + { + $protocol = ['http://', 'https://']; + + if (isset($config['push_domain'])) { + $config['push_domain'] = str_replace($protocol, '', $config['push_domain']); + } + + if (isset($config['pull_domain'])) { + $config['pull_domain'] = str_replace($protocol, '', $config['pull_domain']); + } + + if (isset($config['ptt'])) { + + $ptt = $config['ptt']; + $keys = array_keys($ptt['id']); + $myPtt = []; + + foreach ($keys as $key) { + $myPtt[$key] = [ + 'id' => $ptt['id'][$key], + 'bit_rate' => $ptt['bit_rate'][$key], + 'summary' => $ptt['summary'][$key], + 'height' => $ptt['height'][$key], + ]; + } + + $config['pull_trans_template'] = kg_json_encode($myPtt); + } + + $this->updateSectionConfig($section, $config); + } + + public function updateSmserConfig($section, $config) + { + $template = $config['template']; + $keys = array_keys($template['id']); + $myTemplate = []; + + foreach ($keys as $key) { + $myTemplate[$key] = [ + 'id' => $template['id'][$key], + 'content' => $template['content'][$key], + ]; + } + + $config['template'] = kg_json_encode($myTemplate); + + $this->updateSectionConfig($section, $config); + } + +} diff --git a/app/Http/Admin/Services/Course.php b/app/Http/Admin/Services/Course.php new file mode 100644 index 00000000..ec28f24f --- /dev/null +++ b/app/Http/Admin/Services/Course.php @@ -0,0 +1,485 @@ +getParams(); + + if (isset($params['xm_category_ids'])) { + $params['id'] = $this->getCategoryCourseIds($params['xm_category_ids']); + } + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $courseRepo = new CourseRepo(); + + $pager = $courseRepo->paginate($params, $sort, $page, $limit); + + return $this->handleCourses($pager); + } + + public function getCourse($id) + { + $course = $this->findOrFail($id); + + return $course; + } + + public function createCourse() + { + $post = $this->request->getPost(); + + $validator = new CourseValidator(); + + $data = []; + + $data['model'] = $validator->checkModel($post['model']); + $data['title'] = $validator->checkTitle($post['title']); + $data['published'] = 0; + + $course = new CourseModel(); + + $course->create($data); + + return $course; + } + + public function updateCourse($id) + { + $course = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new CourseValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['cover'])) { + $data['cover'] = $validator->checkCover($post['cover']); + } + + if (isset($post['keywords'])) { + $data['keywords'] = $validator->checkKeywords($post['keywords']); + } + + if (isset($post['summary'])) { + $data['summary'] = $validator->checkSummary($post['summary']); + } + + if (isset($post['details'])) { + $data['details'] = $validator->checkDetails($post['details']); + } + + if (isset($post['level'])) { + $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['market_price'] = $validator->checkMarketPrice($post['market_price']); + $data['vip_price'] = $validator->checkVipPrice($post['vip_price']); + $data['expiry'] = $validator->checkExpiry($post['expiry']); + } + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + if ($post['published'] == 1) { + $validator->checkPublishAbility($course); + } + } + + if (isset($post['xm_category_ids'])) { + $this->saveCategories($course, $post['xm_category_ids']); + } + + if (isset($post['xm_teacher_ids'])) { + $this->saveTeachers($course, $post['xm_teacher_ids']); + } + + if (isset($post['xm_course_ids'])) { + $this->saveRelatedCourses($course, $post['xm_course_ids']); + } + + $course->update($data); + + return $course; + } + + public function deleteCourse($id) + { + $course = $this->findOrFail($id); + + if ($course->deleted == 1) { + return false; + } + + $course->deleted = 1; + + $course->update(); + + return $course; + } + + public function restoreCourse($id) + { + $course = $this->findOrFail($id); + + if ($course->deleted == 0) { + return false; + } + + $course->deleted = 0; + + $course->update(); + + return $course; + } + + public function getXmCategories($id) + { + $categoryRepo = new CategoryRepo(); + + $allCategories = $categoryRepo->findAll(['deleted' => 0]); + + if ($allCategories->count() == 0) { + return []; + } + + $courseCategoryIds = []; + + if ($id > 0) { + $courseRepo = new CourseRepo(); + $courseCategories = $courseRepo->findCategories($id); + if ($courseCategories->count() > 0) { + foreach ($courseCategories as $category) { + $courseCategoryIds[] = $category->id; + } + } + } + + $list = []; + + foreach ($allCategories as $category) { + if ($category->level == 1) { + $list[$category->id] = [ + 'name' => $category->name, + 'value' => $category->id, + 'children' => [], + ]; + } + } + + foreach ($allCategories as $category) { + $selected = in_array($category->id, $courseCategoryIds); + $parentId = $category->parent_id; + if ($category->level == 2) { + $list[$parentId]['children'][] = [ + 'id' => $category->id, + 'name' => $category->name, + 'selected' => $selected, + ]; + } + } + + return array_values($list); + } + + public function getXmTeachers($id) + { + $userRepo = new UserRepo(); + + $allTeachers = $userRepo->findTeachers(); + + if ($allTeachers->count() == 0) { + return []; + } + + $courseTeacherIds = []; + + if ($id > 0) { + $courseRepo = new CourseRepo(); + $courseTeachers = $courseRepo->findTeachers($id); + if ($courseTeachers->count() > 0) { + foreach ($courseTeachers as $teacher) { + $courseTeacherIds[] = $teacher->id; + } + } + } + + $list = []; + + foreach ($allTeachers as $teacher) { + $selected = in_array($teacher->id, $courseTeacherIds); + $list[] = [ + 'id' => $teacher->id, + 'name' => $teacher->name, + 'selected' => $selected, + ]; + } + + return $list; + } + + public function getXmCourses($id) + { + $courseRepo = new CourseRepo(); + + $courses = $courseRepo->findRelatedCourses($id); + + $list = []; + + if ($courses->count() > 0) { + foreach ($courses as $course) { + $list[] = [ + 'id' => $course->id, + 'title' => $course->title, + 'selected' => true, + ]; + } + } + + return $list; + } + + public function getChapters($id) + { + $course = $this->findOrFail($id); + + $deleted = $this->request->getQuery('deleted', 'int', 0); + + $chapterRepo = new ChapterRepo(); + + $chapters = $chapterRepo->findAll([ + 'parent_id' => 0, + 'course_id' => $course->id, + 'deleted' => $deleted, + ]); + + return $chapters; + } + + protected function findOrFail($id) + { + $validator = new CourseValidator(); + + $result = $validator->checkCourse($id); + + return $result; + } + + protected function saveTeachers($course, $teacherIds) + { + $courseRepo = new CourseRepo(); + + $courseTeachers = $courseRepo->findTeachers($course->id); + + $originTeacherIds = []; + + if ($courseTeachers->count() > 0) { + foreach ($courseTeachers as $teacher) { + $originTeacherIds[] = $teacher->id; + } + } + + $newTeacherIds = explode(',', $teacherIds); + $addedTeacherIds = array_diff($newTeacherIds, $originTeacherIds); + + if ($addedTeacherIds) { + foreach ($addedTeacherIds as $teacherId) { + $courseTeacher = new CourseUserModel(); + $courseTeacher->create([ + 'course_id' => $course->id, + 'user_id' => $teacherId, + 'role_type' => CourseUserModel::ROLE_TEACHER, + 'source_type' => CourseUserModel::SOURCE_IMPORT, + 'expire_time' => strtotime('+10 years'), + ]); + } + } + + $deletedTeacherIds = array_diff($originTeacherIds, $newTeacherIds); + + if ($deletedTeacherIds) { + $courseUserRepo = new CourseUserRepo(); + foreach ($deletedTeacherIds as $teacherId) { + $courseTeacher = $courseUserRepo->findCourseTeacher($course->id, $teacherId); + if ($courseTeacher) { + $courseTeacher->delete(); + } + } + } + } + + protected function saveCategories($course, $categoryIds) + { + $courseRepo = new CourseRepo(); + + $courseCategories = $courseRepo->findCategories($course->id); + + $originCategoryIds = []; + + if ($courseCategories->count() > 0) { + foreach ($courseCategories as $category) { + $originCategoryIds[] = $category->id; + } + } + + $newCategoryIds = explode(',', $categoryIds); + $addedCategoryIds = array_diff($newCategoryIds, $originCategoryIds); + + if ($addedCategoryIds) { + foreach ($addedCategoryIds as $categoryId) { + $courseCategory = new CourseCategoryModel(); + $courseCategory->create([ + 'course_id' => $course->id, + 'category_id' => $categoryId, + ]); + } + } + + $deletedCategoryIds = array_diff($originCategoryIds, $newCategoryIds); + + if ($deletedCategoryIds) { + $courseCategoryRepo = new CourseCategoryRepo(); + foreach ($deletedCategoryIds as $categoryId) { + $courseCategory = $courseCategoryRepo->findCourseCategory($course->id, $categoryId); + if ($courseCategory) { + $courseCategory->delete(); + } + } + } + } + + protected function saveRelatedCourses($course, $courseIds) + { + $courseRepo = new CourseRepo(); + + $relatedCourses = $courseRepo->findRelatedCourses($course->id); + + $originRelatedIds = []; + + if ($relatedCourses->count() > 0) { + foreach ($relatedCourses as $relatedCourse) { + $originRelatedIds[] = $relatedCourse->id; + } + } + + $newRelatedIds = explode(',', $courseIds); + $addedRelatedIds = array_diff($newRelatedIds, $originRelatedIds); + + $courseRelatedRepo = new CourseRelatedRepo(); + + /** + * 双向关联 + */ + if ($addedRelatedIds) { + foreach ($addedRelatedIds as $relatedId) { + if ($relatedId != $course->id) { + $record = $courseRelatedRepo->findCourseRelated($course->id, $relatedId); + if (!$record) { + $courseRelated = new CourseRelatedModel(); + $courseRelated->create([ + 'course_id' => $course->id, + 'related_id' => $relatedId, + ]); + } + $record = $courseRelatedRepo->findCourseRelated($relatedId, $course->id); + if (!$record) { + $courseRelated = new CourseRelatedModel(); + $courseRelated->create([ + 'course_id' => $relatedId, + 'related_id' => $course->id, + ]); + } + } + } + } + + $deletedRelatedIds = array_diff($originRelatedIds, $newRelatedIds); + + /** + * 单向删除 + */ + if ($deletedRelatedIds) { + $courseRelatedRepo = new CourseRelatedRepo(); + foreach ($deletedRelatedIds as $relatedId) { + $courseRelated = $courseRelatedRepo->findCourseRelated($course->id, $relatedId); + if ($courseRelated) { + $courseRelated->delete(); + } + } + } + } + + protected function getCategoryCourseIds($categoryIds) + { + if (empty($categoryIds)) return []; + + $courseCategoryRepo = new CourseCategoryRepo(); + + $categoryIds = explode(',', $categoryIds); + + $relations = $courseCategoryRepo->findByCategoryIds($categoryIds); + + $result = []; + + if ($relations->count() > 0) { + foreach ($relations as $relation) { + $result[] = $relation->course_id; + } + } + + return $result; + } + + protected function handleCourses($pager) + { + if ($pager->total_items > 0) { + + $transformer = new CourseListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleCourses($pipeA); + $pipeC = $transformer->handleCategories($pipeB); + $pipeD = $transformer->arrayToObject($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/CourseStudent.php b/app/Http/Admin/Services/CourseStudent.php new file mode 100644 index 00000000..84c7160e --- /dev/null +++ b/app/Http/Admin/Services/CourseStudent.php @@ -0,0 +1,180 @@ +findById($courseId); + } + + public function getStudent($userId) + { + $repo = new UserRepo(); + + return $repo->findById($userId); + } + + public function getCourseStudents() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['role_type'] = CourseUserModel::ROLE_STUDENT; + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $courseUserRepo = new CourseUserRepo(); + + $pager = $courseUserRepo->paginate($params, $sort, $page, $limit); + + return $this->handleCourseStudents($pager); + } + + public function getCourseLearnings() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $learningRepo = new LearningRepo(); + + $pager = $learningRepo->paginate($params, $sort, $page, $limit); + + return $this->handleCourseLearnings($pager); + } + + public function getCourseStudent($courseId, $userId) + { + $result = $this->findOrFail($courseId, $userId); + + return $result; + } + + public function createCourseStudent() + { + $post = $this->request->getPost(); + + $validator = new CourseUserValidator(); + + $data = [ + 'role_type' => CourseUserModel::ROLE_STUDENT, + 'source_type' => CourseUserModel::SOURCE_IMPORT, + ]; + + $data['course_id'] = $validator->checkCourseId($post['course_id']); + $data['user_id'] = $validator->checkUserId($post['user_id']); + $data['expire_time'] = $validator->checkExpireTime($post['expire_time']); + + $validator->checkIfJoined($post['course_id'], $post['user_id']); + + $courseUser = new CourseUserModel(); + + $courseUser->create($data); + + $this->updateStudentCount($data['course_id']); + + return $courseUser; + } + + public function updateCourseStudent() + { + $post = $this->request->getPost(); + + $courseStudent = $this->findOrFail($post['course_id'], $post['user_id']); + + $validator = new CourseUserValidator(); + + $data = []; + + if (isset($post['expire_time'])) { + $data['expire_time'] = $validator->checkExpireTime($post['expire_time']); + } + + if (isset($post['locked'])) { + $data['locked'] = $validator->checkLockStatus($post['locked']); + } + + $courseStudent->update($data); + + return $courseStudent; + } + + protected function updateStudentCount($courseId) + { + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($courseId); + + $updater = new CourseStatsUpdater(); + + $updater->updateStudentCount($course); + } + + protected function findOrFail($courseId, $userId) + { + $validator = new CourseUserValidator(); + + $result = $validator->checkCourseStudent($courseId, $userId); + + return $result; + } + + protected function handleCourseStudents($pager) + { + if ($pager->total_items > 0) { + + $transformer = new CourseUserListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleCourses($pipeA); + $pipeC = $transformer->handleUsers($pipeB); + $pipeD = $transformer->arrayToObject($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + + protected function handleCourseLearnings($pager) + { + if ($pager->total_items > 0) { + + $transformer = new LearningListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleCourses($pipeA); + $pipeC = $transformer->handleChapters($pipeB); + $pipeD = $transformer->handleUsers($pipeC); + $pipeE = $transformer->arrayToObject($pipeD); + + $pager->items = $pipeE; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/Nav.php b/app/Http/Admin/Services/Nav.php new file mode 100644 index 00000000..5d3c16d8 --- /dev/null +++ b/app/Http/Admin/Services/Nav.php @@ -0,0 +1,179 @@ +findOrFail($id); + + return $nav; + } + + public function getParentNav($id) + { + if ($id > 0) { + $parent = NavModel::findFirst($id); + } else { + $parent = new NavModel(); + $parent->id = 0; + $parent->level = 0; + } + + return $parent; + } + + public function getTopNavs() + { + $navRepo = new NavRepo(); + + $navs = $navRepo->findAll([ + 'parent_id' => 0, + 'position' => 'top', + 'deleted' => 0, + ]); + + return $navs; + } + + public function getChildNavs($parentId) + { + $deleted = $this->request->getQuery('deleted', 'int', 0); + + $navRepo = new NavRepo(); + + $navs = $navRepo->findAll([ + 'parent_id' => $parentId, + 'deleted' => $deleted, + ]); + + return $navs; + } + + public function createNav() + { + $post = $this->request->getPost(); + + $validator = new NavValidator(); + + $data = [ + 'parent_id' => 0, + 'published' => 1, + ]; + + $parent = null; + + if ($post['parent_id'] > 0) { + $parent = $validator->checkParent($post['parent_id']); + $data['parent_id'] = $parent->id; + } + + $data['name'] = $validator->checkName($post['name']); + $data['priority'] = $validator->checkPriority($post['priority']); + $data['url'] = $validator->checkUrl($post['url']); + $data['target'] = $validator->checkTarget($post['target']); + $data['position'] = $validator->checkPosition($post['position']); + $data['published'] = $validator->checkPublishStatus($post['published']); + + $nav = new NavModel(); + + $nav->create($data); + + if ($parent) { + $nav->path = $parent->path . $nav->id . ','; + $nav->level = $parent->level + 1; + } else { + $nav->path = ',' . $nav->id . ','; + $nav->level = 1; + } + + $nav->update(); + + return $nav; + } + + public function updateNav($id) + { + $nav = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new NavValidator(); + + $data = []; + + if (isset($post['name'])) { + $data['name'] = $validator->checkName($post['name']); + } + + if (isset($post['position'])) { + $data['position'] = $validator->checkPosition($post['position']); + } + + if (isset($post['url'])) { + $data['url'] = $validator->checkUrl($post['url']); + } + + if (isset($post['target'])) { + $data['target'] = $validator->checkTarget($post['target']); + } + + if (isset($post['priority'])) { + $data['priority'] = $validator->checkPriority($post['priority']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $nav->update($data); + + return $nav; + } + + public function deleteNav($id) + { + $nav = $this->findOrFail($id); + + if ($nav->deleted == 1) { + return false; + } + + $nav->deleted = 1; + + $nav->update(); + + return $nav; + } + + public function restoreNav($id) + { + $nav = $this->findOrFail($id); + + if ($nav->deleted == 0) { + return false; + } + + $nav->deleted = 0; + + $nav->update(); + + return $nav; + } + + protected function findOrFail($id) + { + $validator = new NavValidator(); + + $result = $validator->checkNav($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/Order.php b/app/Http/Admin/Services/Order.php new file mode 100644 index 00000000..78e065eb --- /dev/null +++ b/app/Http/Admin/Services/Order.php @@ -0,0 +1,103 @@ +getParams(); + $sort = $pageQuery->getSort(); + $page = $pageQuery->getPage(); + $limit = $pageQuery->getLimit(); + + $orderRepo = new OrderRepo(); + + $pager = $orderRepo->paginate($params, $sort, $page, $limit); + + return $this->handleOrders($pager); + } + + public function getTrades($sn) + { + $orderRepo = new OrderRepo(); + + $trades = $orderRepo->findTrades($sn); + + return $trades; + } + + public function getRefunds($sn) + { + $orderRepo = new OrderRepo(); + + $trades = $orderRepo->findRefunds($sn); + + return $trades; + } + + public function getUser($userId) + { + $userRepo = new UserRepo(); + + $user = $userRepo->findById($userId); + + return $user; + } + + public function getOrder($id) + { + $order = $this->findOrFail($id); + + return $order; + } + + public function closeOrder($id) + { + $order = $this->findOrFail($id); + + if ($order->status == OrderModel::STATUS_PENDING) { + $order->status = OrderModel::STATUS_CLOSED; + $order->update(); + } + + return $order; + } + + protected function findOrFail($id) + { + $validator = new OrderValidator(); + + $result = $validator->checkOrder($id); + + return $result; + } + + protected function handleOrders($pager) + { + if ($pager->total_items > 0) { + + $transformer = new OrderListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleItems($pipeA); + $pipeC = $transformer->handleUsers($pipeB); + $pipeD = $transformer->arrayToObject($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/Package.php b/app/Http/Admin/Services/Package.php new file mode 100644 index 00000000..f9a05388 --- /dev/null +++ b/app/Http/Admin/Services/Package.php @@ -0,0 +1,246 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $pageRepo = new PackageRepo(); + + $pager = $pageRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + + public function getPackage($id) + { + $package = $this->findOrFail($id); + + return $package; + } + + public function createPackage() + { + $post = $this->request->getPost(); + + $validator = new PackageValidator(); + + $data = []; + + $data['title'] = $validator->checkTitle($post['title']); + $data['summary'] = $validator->checkSummary($post['summary']); + + $package = new PackageModel(); + + $package->create($data); + + return $package; + } + + public function updatePackage($id) + { + $package = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new PackageValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['summary'])) { + $data['summary'] = $validator->checkSummary($post['summary']); + } + + 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['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + if (isset($post['xm_course_ids'])) { + $this->saveCourses($package, $post['xm_course_ids']); + } + + $package->update($data); + + $this->updateCourseCount($package); + + return $package; + } + + public function deletePackage($id) + { + $package = $this->findOrFail($id); + + if ($package->deleted == 1) { + return false; + } + + $package->deleted = 1; + + $package->update(); + + return $package; + } + + public function restorePackage($id) + { + $package = $this->findOrFail($id); + + if ($package->deleted == 0) { + return false; + } + + $package->deleted = 0; + + $package->update(); + + return $package; + } + + public function getGuidingCourses($courseIds) + { + if (!$courseIds) return []; + + $courseRepo = new CourseRepo(); + + $ids = explode(',', $courseIds); + + $courses = $courseRepo->findByIds($ids); + + return $courses; + } + + public function getGuidingPrice($courses) + { + $totalMarketPrice = $totalVipPrice = 0; + + if ($courses) { + foreach ($courses as $course) { + $totalMarketPrice += $course->market_price; + $totalVipPrice += $course->vip_price; + } + } + + $sgtMarketPrice = sprintf('%0.2f', intval($totalMarketPrice * 0.9)); + $sgtVipPrice = sprintf('%0.2f', intval($totalVipPrice * 0.8)); + + $price = new \stdClass(); + $price->market_price = $sgtMarketPrice; + $price->vip_price = $sgtVipPrice; + + return $price; + } + + public function getXmCourses($id) + { + $packageRepo = new PackageRepo(); + + $courses = $packageRepo->findCourses($id); + + $list = []; + + if ($courses->count() > 0) { + foreach ($courses as $course) { + $list[] = [ + 'id' => $course->id, + 'title' => $course->title, + 'selected' => true, + ]; + } + } + + return $list; + } + + protected function saveCourses($package, $courseIds) + { + $packageRepo = new PackageRepo(); + + $courses = $packageRepo->findCourses($package->id); + + $originCourseIds = []; + + if ($courses->count() > 0) { + foreach ($courses as $course) { + $originCourseIds[] = $course->id; + } + } + + $newCourseIds = explode(',', $courseIds); + $addedCourseIds = array_diff($newCourseIds, $originCourseIds); + + if ($addedCourseIds) { + foreach ($addedCourseIds as $courseId) { + $coursePackage = new CoursePackageModel(); + $coursePackage->create([ + 'course_id' => $courseId, + 'package_id' => $package->id, + ]); + } + } + + $deletedCourseIds = array_diff($originCourseIds, $newCourseIds); + + if ($deletedCourseIds) { + $coursePackageRepo = new CoursePackageRepo(); + foreach ($deletedCourseIds as $courseId) { + $coursePackage = $coursePackageRepo->findCoursePackage($courseId, $package->id); + if ($coursePackage) { + $coursePackage->delete(); + } + } + } + } + + protected function updateCourseCount($package) + { + $packageRepo = new PackageRepo(); + + $courseCount = $packageRepo->countCourses($package->id); + + $package->course_count = $courseCount; + + $package->update(); + } + + protected function findOrFail($id) + { + $validator = new PackageValidator(); + + $result = $validator->checkPackage($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/Page.php b/app/Http/Admin/Services/Page.php new file mode 100644 index 00000000..023a863c --- /dev/null +++ b/app/Http/Admin/Services/Page.php @@ -0,0 +1,124 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $pageRepo = new PageRepo(); + + $pager = $pageRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + + public function getPage($id) + { + $page = $this->findOrFail($id); + + return $page; + } + + public function createPage() + { + $post = $this->request->getPost(); + + $validator = new PageValidator(); + + $data = []; + + $data['title'] = $validator->checkTitle($post['title']); + $data['content'] = $validator->checkContent($post['content']); + $data['published'] = $validator->checkPublishStatus($post['published']); + + $page = new PageModel(); + + $page->create($data); + + return $page; + } + + public function updatePage($id) + { + $page = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new PageValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['content'])) { + $data['content'] = $validator->checkContent($post['content']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $page->update($data); + + return $page; + } + + public function deletePage($id) + { + $page = $this->findOrFail($id); + + if ($page->deleted == 1) { + return false; + } + + $page->deleted = 1; + + $page->update(); + + return $page; + } + + public function restorePage($id) + { + $page = $this->findOrFail($id); + + if ($page->deleted == 0) { + return false; + } + + $page->deleted = 0; + + $page->update(); + + return $page; + } + + protected function findOrFail($id) + { + $validator = new PageValidator(); + + $result = $validator->checkPage($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/PaymentTest.php b/app/Http/Admin/Services/PaymentTest.php new file mode 100644 index 00000000..388ef269 --- /dev/null +++ b/app/Http/Admin/Services/PaymentTest.php @@ -0,0 +1,82 @@ +getDI()->get('auth')->getAuthUser(); + + $order = new OrderModel(); + + $order->subject = '测试 - 支付测试0.01元'; + $order->amount = 0.01; + $order->user_id = $authUser->id; + $order->item_type = OrderModel::TYPE_TEST; + $order->create(); + + return $order; + } + + /** + * 创建交易 + * + * @param OrderModel $order + * @return TradeModel $trade + */ + public function createTestTrade($order) + { + $trade = new TradeModel(); + + $trade->user_id = $order->user_id; + $trade->order_sn = $order->sn; + $trade->subject = $order->subject; + $trade->amount = $order->amount; + $trade->channel = TradeModel::CHANNEL_ALIPAY; + $trade->create(); + + return $trade; + } + + /** + * 获取订单状态 + * + * @param string $sn + * @return string + */ + public function getTestStatus($sn) + { + $tradeRepo = new TradeRepo(); + + $trade = $tradeRepo->findBySn($sn); + + return $trade->status; + } + + /** + * 获取测试二维码 + * + * @param TradeModel $trade + * @return mixed + */ + abstract public function getTestQrCode($trade); + + /** + * 取消测试订单 + * + * @param string $sn + */ + abstract public function cancelTestOrder($sn); + +} diff --git a/app/Http/Admin/Services/Refund.php b/app/Http/Admin/Services/Refund.php new file mode 100644 index 00000000..a4af36f5 --- /dev/null +++ b/app/Http/Admin/Services/Refund.php @@ -0,0 +1,122 @@ +getParams(); + $sort = $pageQuery->getSort(); + $page = $pageQuery->getPage(); + $limit = $pageQuery->getLimit(); + + $refundRepo = new RefundRepo(); + + $pager = $refundRepo->paginate($params, $sort, $page, $limit); + + return $this->handleRefunds($pager); + } + + public function getRefund($id) + { + $refund = $this->findOrFail($id); + + return $refund; + } + + public function getTrade($sn) + { + $tradeRepo = new TradeRepo(); + + $trade = $tradeRepo->findBySn($sn); + + return $trade; + } + + public function getOrder($sn) + { + $orderRepo = new OrderRepo(); + + $order = $orderRepo->findBySn($sn); + + return $order; + } + + public function getUser($id) + { + $userRepo = new UserRepo(); + + $user = $userRepo->findById($id); + + return $user; + } + + public function reviewRefund($id) + { + $refund = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new RefundValidator(); + + $data = []; + + $validator->checkIfAllowReview($refund); + + $data['status'] = $validator->checkReviewStatus($post['status']); + $data['review_note'] = $validator->checkReviewNote($post['review_note']); + + $refund->update($data); + + $task = new TaskModel(); + + $task->item_id = $refund->id; + $task->item_type = TaskModel::TYPE_REFUND; + $task->item_info = ['refund' => $refund->toArray()]; + $task->priority = TaskModel::PRIORITY_HIGH; + $task->status = TaskModel::STATUS_PENDING; + + $task->create(); + + return $refund; + } + + protected function findOrFail($id) + { + $validator = new RefundValidator(); + + $result = $validator->checkRefund($id); + + return $result; + } + + protected function handleRefunds($pager) + { + if ($pager->total_items > 0) { + + $transformer = new RefundListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleUsers($pipeA); + $pipeC = $transformer->arrayToObject($pipeB); + + $pager->items = $pipeC; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/Review.php b/app/Http/Admin/Services/Review.php new file mode 100644 index 00000000..88594afe --- /dev/null +++ b/app/Http/Admin/Services/Review.php @@ -0,0 +1,140 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $reviewRepo = new ReviewRepo(); + + $pager = $reviewRepo->paginate($params, $sort, $page, $limit); + + return $this->handleReviews($pager); + } + + public function getCourse($courseId) + { + $courseRepo = new CourseRepo(); + + $result = $courseRepo->findById($courseId); + + return $result; + } + + public function getReview($id) + { + $result = $this->findOrFail($id); + + return $result; + } + + public function updateReview($id) + { + $review = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new ReviewValidator(); + + $data = []; + + if (isset($post['content'])) { + $data['content'] = $validator->checkContent($post['content']); + } + + if (isset($post['rating'])) { + $data['rating'] = $validator->checkRating($post['rating']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $review->update($data); + + return $review; + } + + public function deleteReview($id) + { + $review = $this->findOrFail($id); + + if ($review->deleted == 1) return false; + + $review->deleted = 1; + + $review->update(); + + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($review->course_id); + + $course->review_count -= 1; + + $course->update(); + } + + public function restoreReview($id) + { + $review = $this->findOrFail($id); + + if ($review->deleted == 0) return false; + + $review->deleted = 0; + + $review->update(); + + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($review->course_id); + + $course->review_count += 1; + + $course->update(); + } + + protected function findOrFail($id) + { + $validator = new ReviewValidator(); + + $result = $validator->checkReview($id); + + return $result; + } + + protected function handleReviews($pager) + { + if ($pager->total_items > 0) { + + $transformer = new ReviewListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleCourses($pipeA); + $pipeC = $transformer->handleUsers($pipeB); + $pipeD = $transformer->arrayToObject($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/Role.php b/app/Http/Admin/Services/Role.php new file mode 100644 index 00000000..ced78b5a --- /dev/null +++ b/app/Http/Admin/Services/Role.php @@ -0,0 +1,166 @@ +request->getQuery('deleted', 'int', 0); + + $roleRepo = new RoleRepo(); + + $roles = $roleRepo->findAll(['deleted' => $deleted]); + + return $roles; + } + + public function getRole($id) + { + $role = $this->findOrFail($id); + + return $role; + } + + public function createRole() + { + $post = $this->request->getPost(); + + $validator = new RoleValidator(); + + $data = []; + + $data['name'] = $validator->checkName($post['name']); + $data['summary'] = $validator->checkSummary($post['summary']); + $data['type'] = RoleModel::TYPE_CUSTOM; + + $role = new RoleModel(); + + $role->create($data); + + return $role; + } + + public function updateRole($id) + { + $role = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new RoleValidator(); + + $data = []; + + $data['name'] = $validator->checkName($post['name']); + $data['summary'] = $validator->checkSummary($post['summary']); + $data['routes'] = $validator->checkRoutes($post['routes']); + $data['routes'] = $this->handleRoutes($data['routes']); + + $role->update($data); + + return $role; + } + + public function deleteRole($id) + { + $role = $this->findOrFail($id); + + if ($role->deleted == 1) { + return false; + } + + if ($role->type == RoleModel::TYPE_SYSTEM) { + return false; + } + + $role->deleted = 1; + + $role->update(); + + return $role; + } + + public function restoreRole($id) + { + $role = $this->findOrFail($id); + + if ($role->deleted == 0) { + return false; + } + + $role->deleted = 0; + + $role->update(); + + return $role; + } + + protected function findOrFail($id) + { + $validator = new RoleValidator(); + + $result = $validator->checkRole($id); + + return $result; + } + + /** + * 处理路由权限(补充关联权限) + * + * 新增操作 => 补充列表权限 + * 修改操作 => 补充列表权限 + * 删除操作 => 补充还原权限 + * 课程操作 => 补充章节权限 + * 搜索操作 => 补充列表权限 + * + * @param array $routes + * @return array + */ + protected function handleRoutes($routes) + { + if (!$routes) return []; + + $list = []; + + foreach ($routes as $route) { + $list [] = $route; + if (strpos($route, '.add')) { + $list[] = str_replace('.add', '.create', $route); + $list[] = str_replace('.add', '.list', $route); + } elseif (strpos($route, '.edit')) { + $list[] = str_replace('.edit', '.update', $route); + $list[] = str_replace('.edit', '.list', $route); + } elseif (strpos($route, '.delete')) { + $list[] = str_replace('.delete', '.restore', $route); + } elseif (strpos($route, '.search')) { + $list[] = str_replace('.search', '.list', $route); + } + } + + if (in_array('admin.course.list', $routes)) { + $list[] = 'admin.course.chapters'; + $list[] = 'admin.chapter.sections'; + } + + if (array_intersect(['admin.course.add', 'admin.course.edit'], $routes)) { + $list[] = 'admin.chapter.add'; + $list[] = 'admin.chapter.edit'; + $list[] = 'admin.chapter.content'; + } + + if (in_array('admin.course.delete', $routes)) { + $list[] = 'admin.chapter.delete'; + $list[] = 'admin.chapter.restore'; + } + + $result = array_values(array_unique($list)); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/Service.php b/app/Http/Admin/Services/Service.php new file mode 100644 index 00000000..11c6b75a --- /dev/null +++ b/app/Http/Admin/Services/Service.php @@ -0,0 +1,8 @@ +auth = $this->getDI()->get('auth'); + } + + public function login() + { + $post = $this->request->getPost(); + + $validator = new UserValidator(); + + $user = $validator->checkLoginAccount($post['account']); + + $validator->checkLoginPassword($user, $post['password']); + + $validator->checkAdminLogin($user); + + $config = new Config(); + + $captcha = $config->getSectionConfig('captcha'); + + /** + * 验证码是一次性的,放到最后检查,减少第三方调用 + */ + if ($captcha->enabled) { + $validator->checkCaptchaCode($post['ticket'], $post['rand']); + } + + $this->auth->setAuthUser($user); + } + + public function logout() + { + $this->auth->removeAuthUser(); + } + +} diff --git a/app/Http/Admin/Services/Slide.php b/app/Http/Admin/Services/Slide.php new file mode 100644 index 00000000..f7dd7840 --- /dev/null +++ b/app/Http/Admin/Services/Slide.php @@ -0,0 +1,161 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $slideRepo = new SlideRepo(); + + $pager = $slideRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + + public function getSlide($id) + { + $slide = $this->findOrFail($id); + + return $slide; + } + + public function createSlide() + { + $post = $this->request->getPost(); + + $validator = new SlideValidator(); + + $data['title'] = $validator->checkTitle($post['title']); + $data['target'] = $validator->checkTarget($post['target']); + + if ($post['target'] == SlideModel::TARGET_COURSE) { + $course = $validator->checkCourse($post['content']); + $data['content'] = $course->id; + $data['cover'] = $course->cover; + $data['summary'] = $course->summary; + } elseif ($post['target'] == SlideModel::TARGET_PAGE) { + $page = $validator->checkPage($post['content']); + $data['content'] = $page->id; + } elseif ($post['target'] == SlideModel::TARGET_LINK) { + $data['content'] = $validator->checkLink($post['content']); + } + + $data['start_time'] = strtotime(date('Y-m-d')); + $data['end_time'] = strtotime('+15 days', $data['start_time']); + $data['priority'] = 10; + $data['published'] = 0; + + $slide = new SlideModel(); + + $slide->create($data); + + return $slide; + } + + public function updateSlide($id) + { + $slide = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new SlideValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['summary'])) { + $data['summary'] = $validator->checkSummary($post['summary']); + } + + if (isset($post['cover'])) { + $data['cover'] = $validator->checkCover($post['cover']); + } + + if (isset($post['content'])) { + if ($slide->target == SlideModel::TARGET_COURSE) { + $course = $validator->checkCourse($post['content']); + $data['content'] = $course->id; + } elseif ($slide->target == SlideModel::TARGET_PAGE) { + $page = $validator->checkPage($post['content']); + $data['content'] = $page->id; + } elseif ($slide->target == SlideModel::TARGET_LINK) { + $data['content'] = $validator->checkLink($post['content']); + } + } + + if (isset($post['priority'])) { + $data['priority'] = $validator->checkPriority($post['priority']); + } + + if (isset($post['start_time']) || isset($post['end_time'])) { + $data['start_time'] = $validator->checkStartTime($post['start_time']); + $data['end_time'] = $validator->checkEndTime($post['end_time']); + $validator->checkTimeRange($post['start_time'], $post['end_time']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $slide->update($data); + + return $slide; + } + + public function deleteSlide($id) + { + $slide = $this->findOrFail($id); + + if ($slide->deleted == 1) return false; + + $slide->deleted = 1; + + $slide->update(); + + return $slide; + } + + public function restoreSlide($id) + { + $slide = $this->findOrFail($id); + + if ($slide->deleted == 0) return false; + + $slide->deleted = 0; + + $slide->update(); + + return $slide; + } + + protected function findOrFail($id) + { + $validator = new SlideValidator(); + + $result = $validator->checkSlide($id); + + return $result; + } + +} diff --git a/app/Http/Admin/Services/Trade.php b/app/Http/Admin/Services/Trade.php new file mode 100644 index 00000000..1b88d152 --- /dev/null +++ b/app/Http/Admin/Services/Trade.php @@ -0,0 +1,130 @@ +getParams(); + $sort = $pageQuery->getSort(); + $page = $pageQuery->getPage(); + $limit = $pageQuery->getLimit(); + + $tradeRepo = new TradeRepo(); + + $pager = $tradeRepo->paginate($params, $sort, $page, $limit); + + return $this->handleTrades($pager); + } + + public function getTrade($id) + { + $tradeRepo = new TradeRepo(); + + $trade = $tradeRepo->findById($id); + + return $trade; + } + + public function getOrder($sn) + { + $orderRepo = new OrderRepo(); + + $order = $orderRepo->findBySn($sn); + + return $order; + } + + public function getRefunds($sn) + { + $tradeRepo = new TradeRepo(); + + $refunds = $tradeRepo->findRefunds($sn); + + return $refunds; + } + + public function getUser($id) + { + $userRepo = new UserRepo(); + + $user = $userRepo->findById($id); + + return $user; + } + + public function closeTrade($id) + { + $trade = $this->findOrFail($id); + + $validator = new TradeValidator(); + + $validator->checkIfAllowClose($trade); + + $trade->status = TradeModel::STATUS_CLOSED; + $trade->update(); + + return $trade; + } + + public function refundTrade($id) + { + $trade = $this->findOrFail($id); + + $validator = new TradeValidator(); + + $validator->checkIfAllowRefund($trade); + + $refund = new RefundModel(); + + $refund->subject = $trade->subject; + $refund->amount = $trade->amount; + $refund->user_id = $trade->user_id; + $refund->order_sn = $trade->order_sn; + $refund->trade_sn = $trade->sn; + $refund->apply_reason = '后台人工申请退款'; + + $refund->create(); + + return $trade; + } + + protected function findOrFail($id) + { + $validator = new TradeValidator(); + + $result = $validator->checkTrade($id); + + return $result; + } + + protected function handleTrades($pager) + { + if ($pager->total_items > 0) { + + $transformer = new TradeListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleUsers($pipeA); + $pipeC = $transformer->arrayToObject($pipeB); + + $pager->items = $pipeC; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/User.php b/app/Http/Admin/Services/User.php new file mode 100644 index 00000000..d11ca290 --- /dev/null +++ b/app/Http/Admin/Services/User.php @@ -0,0 +1,181 @@ +findAll(['deleted' => 0]); + + return $roles; + } + + public function getUsers() + { + $pageQuery = new PaginateQuery(); + + $params = $pageQuery->getParams(); + $sort = $pageQuery->getSort(); + $page = $pageQuery->getPage(); + $limit = $pageQuery->getLimit(); + + $userRepo = new UserRepo(); + + $pager = $userRepo->paginate($params, $sort, $page, $limit); + + $result = $this->handleUsers($pager); + + return $result; + } + + public function getUser($id) + { + $user = $this->findOrFail($id); + + return $user; + } + + public function createUser() + { + $post = $this->request->getPost(); + + $validator = new UserValidator(); + + $name = $validator->checkName($post['name']); + $password = $validator->checkPassword($post['password']); + $eduRole = $validator->checkEduRole($post['edu_role']); + $adminRole = $validator->checkAdminRole($post['admin_role']); + + $validator->checkIfNameTaken($name); + + $data = []; + + $data['name'] = $name; + $data['salt'] = PasswordUtil::salt(); + $data['password'] = PasswordUtil::hash($password, $data['salt']); + $data['edu_role'] = $eduRole; + $data['admin_role'] = $adminRole; + + $user = new UserModel(); + + $user->create($data); + + if ($user->admin_role > 0) { + $this->updateAdminUserCount($user->admin_role); + } + + return $user; + } + + public function updateUser($id) + { + $user = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new UserValidator(); + + $data = []; + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['about'])) { + $data['about'] = $validator->checkAbout($post['about']); + } + + if (isset($post['edu_role'])) { + $data['edu_role'] = $validator->checkEduRole($post['edu_role']); + } + + if (isset($post['admin_role'])) { + $data['admin_role'] = $validator->checkAdminRole($post['admin_role']); + } + + if (isset($post['locked'])) { + $data['locked'] = $validator->checkLockStatus($post['locked']); + } + + if (isset($post['locked_expiry'])) { + $data['locked_expiry'] = $validator->checkLockExpiry($post['locked_expiry']); + if ($data['locked_expiry'] < time()) { + $data['locked'] = 0; + } + } + + $oldAdminRole = $user->admin_role; + + $user->update($data); + + if ($oldAdminRole > 0) { + $this->updateAdminUserCount($oldAdminRole); + } + + if ($user->admin_role > 0) { + $this->updateAdminUserCount($user->admin_role); + } + + return $user; + } + + protected function findOrFail($id) + { + $validator = new UserValidator(); + + $result = $validator->checkUser($id); + + return $result; + } + + protected function updateAdminUserCount($roleId) + { + if (!$roleId) { + return false; + } + + $roleRepo = new RoleRepo(); + + $role = $roleRepo->findById($roleId); + + if (!$role) { + return false; + } + + $userCount = $roleRepo->countUsers($roleId); + + $role->user_count = $userCount; + + $role->update(); + } + + protected function handleUsers($pager) + { + if ($pager->total_items > 0) { + + $transformer = new UserListTransformer(); + + $pipeA = $pager->items->toArray(); + $pipeB = $transformer->handleAdminRoles($pipeA); + $pipeC = $transformer->handleEduRoles($pipeB); + $pipeD = $transformer->arrayToObject($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/WxpayTest.php b/app/Http/Admin/Services/WxpayTest.php new file mode 100644 index 00000000..eb71c586 --- /dev/null +++ b/app/Http/Admin/Services/WxpayTest.php @@ -0,0 +1,41 @@ + $trade->sn, + 'total_fee' => 100 * $trade->amount, + 'body' => $trade->subject, + ]; + + $wxpayService = new WxpayService(); + $qrcode = $wxpayService->qrcode($outOrder); + $result = $qrcode ?: false; + + return $result; + } + + /** + * 取消测试订单 + * + * @param string $sn + */ + public function cancelTestOrder($sn) + { + + } + +} diff --git a/app/Http/Admin/Services/XmCourse.php b/app/Http/Admin/Services/XmCourse.php new file mode 100644 index 00000000..dcc20efa --- /dev/null +++ b/app/Http/Admin/Services/XmCourse.php @@ -0,0 +1,50 @@ +getParams(); + + $params['deleted'] = 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $courseRepo = new CourseRepo(); + + $pager = $courseRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + + public function getPaidCourses() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['free'] = 0; + $params['deleted'] = 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $courseRepo = new CourseRepo(); + + $pager = $courseRepo->paginate($params, $sort, $page, $limit); + + return $pager; + } + +} diff --git a/app/Http/Admin/Views/audit/list.volt b/app/Http/Admin/Views/audit/list.volt new file mode 100644 index 00000000..f4e8277b --- /dev/null +++ b/app/Http/Admin/Views/audit/list.volt @@ -0,0 +1,76 @@ +
用户编号 | +用户名称 | +用户IP | +请求路由 | +请求路径 | +请求时间 | +请求内容 | +
---|---|---|---|---|---|---|
{{ item.user_id }} | +{{ item.user_name }} | +{{ item.user_ip }} | +{{ item.req_route }} | +{{ item.req_path }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | ++ + | +
编号 | +名称 | +层级 | +课程数 | +排序 | +发布 | +操作 | +|
---|---|---|---|---|---|---|---|
{{ item.id }} | + {% if item.level < 2 %} +{{ item.name }} | + {% else %} +{{ item.name }} | + {% endif %} +{{ item.level }} | +{{ item.course_count }} | ++ | + | + + | +
格式 | +时长 | +分辨率 | +码率 | +大小 | +操作 | +
---|---|---|---|---|---|
{{ item.format }} | +{{ item.duration }} | +{{ item.width }} x {{ item.height }} | +{{ item.bit_rate }}kbps | +{{ item.size }}M | ++ 预览 + 复制 + | +
编号 | +名称 | +字数 | +排序 | +免费 | +发布 | +操作 | +
---|---|---|---|---|---|---|
{{ item.id }} | ++ {{ item.title }} + 课 + | +{{ item.attrs.word_count }} | ++ | + | + | + + | +
编号 | +名称 | +直播时间 | +排序 | +免费 | +发布 | +操作 | +
---|---|---|---|---|---|---|
{{ item.id }} | ++ {{ item.title }} + 课 + | +
+ {% if item.attrs.start_time > 0 %}
+ 开始:{{ date('Y-m-d H:i',item.attrs.start_time) }} +结束:{{ date('Y-m-d H:i',item.attrs.end_time) }} + {% else %} + N/A + {% endif %} + |
+ + | + | + | + + | +
编号 | +名称 | +视频状态 | +视频时长 | +排序 | +免费 | +发布 | +操作 | +
---|---|---|---|---|---|---|---|
{{ item.id }} | ++ {{ item.title }} + 课 + | +{{ file_status(item.attrs.file_status) }} | +{{ item.attrs.duration|play_duration }} | ++ | + | + | + + | +
编号 | +名称 | +课时数 | +排序 | +操作 | +
---|---|---|---|---|
{{ item.id }} | ++ {{ item.title }} + 章 + | ++ + {{ item.lesson_count }} + + | ++ | + + | +
课程 | +课时数 | +学员数 | +有效期 | +价格 | +发布 | +操作 | +
---|---|---|---|---|---|---|
+ 标题:{{ item.title }} {{ model_info(item.model) }} +分类:{{ category_info(item.categories) }} + |
+ + + {{ item.lesson_count }} + + | ++ + {{ item.student_count }} + + | +{{ expiry_info(item.expiry) }} | +
+ 市场:¥{{ item.market_price }} +会员:¥{{ item.vip_price }} + |
+ + | + + | +
编号 | +名称 | +层级 | +位置 | +目标 | +排序 | +发布 | +操作 | +|
---|---|---|---|---|---|---|---|---|
{{ item.id }} | + {% if item.position == 'top' and item.level < 2 %} +{{ item.name }} | + {% else %} +{{ item.name }} | + {% endif %} +{{ item.level }} | +{{ position_info(item.position) }} | +{{ target_info(item.target) }} | ++ | + | + + | +
商品信息 | +买家信息 | +订单金额 | +订单状态 | +创建时间 | +操作 | +
---|---|---|---|---|---|
+ 商品:{{ item.subject }} {{ item_type(item.item_type) }} +单号:{{ item.sn }} + |
+
+ 名称:{{ item.user.name }} +编号:{{ item.user.id }} + |
+ ¥{{ item.amount }} | +{{ order_status(item.status) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | ++ 详情 + | +
课程名称:{{ course['title'] }}
+市场价格:¥{{ course['market_price'] }}
+有效期限:{{ date('Y-m-d', course['expire_time']) }}
+课程名称:{{ course['title'] }}
+市场价格:¥{{ course['market_price'] }}
+有效期限:{{ date('Y-m-d', course['expire_time']) }}
+课程名称:{{ course['title'] }}
+赞赏金额:¥{{ order.amount }}
+商品名称:{{ order.subject }}
+商品价格:¥{{ order.amount }}
+商品名称:{{ order.subject }}
+商品价格:¥{{ order.amount }}
++ 订单编号:{{ order.sn }} + | +订单金额 | +订单类型 | +订单状态 | +创建时间 | +
{{ item_info(order) }} | +¥{{ order.amount }} | +{{ item_type(order.item_type) }} | +{{ order_status(order.status) }} | +{{ date('Y-m-d H:i:s',order.created_at) }} | +
退款序号 | +退款金额 | +退款原因 | +退款状态 | +创建时间 | +
---|---|---|---|---|
{{ item.sn }} | +¥{{ item.amount }} | +{{ substr(item.apply_reason,0,15) }} | +{{ refund_status(item) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | +
交易号 | +交易金额 | +交易平台 | +交易状态 | +创建时间 | +
---|---|---|---|---|
{{ item.sn }} | +¥{{ item.amount }} | +{{ channel_type(item.channel) }} | +{{ trade_status(item.status) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | +
编号 | +用户名 | +邮箱 | +手机号 | +注册时间 | +
---|---|---|---|---|
{{ user.id }} | +{{ user.name }} | +{{ user.email }} | +{{ user.phone }} | +{{ date('Y-m-d H:i:s',user.created_at) }} | +
标题 | +课时数 | +有效期 | +价格 | +
---|---|---|---|
{{ item.title }} | +{{ item.lesson_count }} | +{{ expiry_info(item.expiry) }} | +
+ 市场价:¥{{ item.market_price }} +会员价:¥{{ item.vip_price }} + |
+
编号 | +标题 | +课程数 | +市场价 | +会员价 | +发布 | +操作 | +
---|---|---|---|---|---|---|
{{ item.id }} | +{{ item.title }} | +{{ item.course_count }} | +¥{{ item.market_price }} | +¥{{ item.vip_price }} | ++ | + + | +
编号 | +标题 | +创建时间 | +更新时间 | +发布 | +操作 | +
---|---|---|---|---|---|
{{ item.id }} | +{{ item.title }} | +{{ date('Y-m-d H:i',item.created_at) }} | +{{ date('Y-m-d H:i',item.updated_at) }} | ++ | ++ + | +
国家 | +省份 | +城市 | +运营商 | +
---|---|---|---|
{{ region.country }} | +{{ region.province }} | +{{ region.city }} | +{{ region.isp }} | +
商品信息 | +买家信息 | +退款金额 | +退款状态 | +创建时间 | +操作 | +
---|---|---|---|---|---|
+ 商品:{{ item.subject }} +单号:{{ item.order_sn }} + |
+
+ 名称:{{ item.user.name }} +编号:{{ item.user.id }} + |
+ ¥{{ item.amount }} | +{{ refund_status(item) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | ++ 详情 + | +
退款序号 | +退款金额 | +退款原因 | +退款状态 | +创建时间 | +
---|---|---|---|---|
{{ refund.sn }} | +¥{{ refund.amount }} | +{{ substr(refund.apply_reason,0,15) }} | +{{ refund_status(refund) }} | +{{ date('Y-m-d H:i:s',refund.created_at) }} | +
交易序号 | +交易金额 | +交易平台 | +交易状态 | +创建时间 | +
---|---|---|---|---|
{{ trade.sn }} | +¥{{ trade.amount }} | +{{ channel_type(trade.channel) }} | +{{ trade_status(trade.status) }} | +{{ date('Y-m-d H:i:s',trade.created_at) }} | +
订单序号 | +商品名称 | +订单金额 | +订单状态 | +创建时间 | +
---|---|---|---|---|
{{ order.sn }} | +{{ order.subject }} | +¥{{ order.amount }} | +{{ order_status(order.status) }} | +{{ date('Y-m-d H:i:s',order.created_at) }} | +
编号 | +用户名 | +邮箱 | +手机号 | +注册时间 | +
---|---|---|---|---|
{{ user.id }} | +{{ user.name }} | +{{ user.email }} | +{{ user.phone }} | +{{ date('Y-m-d H:i:s',user.created_at) }} | +
+ | 內容 | +时间 | +发布 | +操作 | +
---|---|---|---|---|
+ 评分: +课程:{{ item.course.title }} +用户:{{ item.user.name }} + |
+ {{ item.content }} | +{{ date('Y-m-d', item.created_at) }} | ++ | + + | +
编号 | +名称 | +类型 | +成员数 | +操作 | +
---|---|---|---|---|
{{ item.id }} | +{{ item.name }} | +{{ type_info(item.type) }} | ++ + {{ item.user_count }} + + | ++ + | +
编号 | +标题 | +目标类型 | +有效期限 | +排序 | +发布 | +操作 | +
---|---|---|---|---|---|---|
{{ item.id }} | +{{ item.title }} | +{{ target_info(item.target) }} | +
+ 开始:{{ date('Y-m-d H:i',item.start_time) }} +结束:{{ date('Y-m-d H:i',item.end_time) }} + |
+ + | + | + + | +
课时信息 | +学习时长 | +客户端类型 | +客户端地址 | +创建时间 | +
---|---|---|---|---|
+ 课程:{{ item.course.title }} +章节:{{ item.chapter.title }} + |
+ {{ item.duration|play_duration }} | +{{ client_type(item.client_type) }} | +{{ item.client_ip }} | +{{ date('Y-m-d H:i',item.created_at) }} | +
基本信息 | +学习情况 | +加入时间 | +过期时间 | +锁定 | +操作 | +
---|---|---|---|---|---|
+ 课程:{{ item.course.title }} +学员:{{ item.user.name }} + |
+
+
+
+
+
+
+ 时长:{{ item.duration|total_duration }} + |
+ {{ date('Y-m-d',item.created_at) }} | +{{ date('Y-m-d',item.expire_time) }} | ++ | + + | +
商品信息 | +买家信息 | +交易金额 | +交易平台 | +交易状态 | +创建时间 | +操作 | +
---|---|---|---|---|---|---|
+ 商品:{{ item.subject }} +单号:{{ item.order_sn }} + |
+
+ 名称:{{ item.user.name }} +编号:{{ item.user.id }} + |
+ ¥{{ item.amount }} | +{{ channel_type(item.channel) }} | +{{ trade_status(item.status) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | ++ 详情 + | +
交易序号 | +交易金额 | +交易平台 | +交易状态 | +创建时间 | +
---|---|---|---|---|
{{ trade.sn }} | +¥{{ trade.amount }} | +{{ channel_type(trade.channel) }} | +{{ trade_status(trade.status) }} | +{{ date('Y-m-d H:i:s',trade.created_at) }} | +
退款序号 | +退款金额 | +退款原因 | +退款状态 | +创建时间 | +
---|---|---|---|---|
{{ item.sn }} | +¥{{ item.amount }} | +{{ substr(item.apply_reason,0,15) }} | +{{ refund_status(item) }} | +{{ date('Y-m-d H:i:s',item.created_at) }} | +
订单序号 | +商品名称 | +订单金额 | +订单状态 | +创建时间 | +
---|---|---|---|---|
{{ order.sn }} | +{{ order.subject }} | +¥{{ order.amount }} | +{{ order_status(order.status) }} | +{{ date('Y-m-d H:i:s',order.created_at) }} | +
编号 | +用户名 | +邮箱 | +手机号 | +注册时间 | +
---|---|---|---|---|
{{ user.id }} | +{{ user.name }} | +{{ user.email }} | +{{ user.phone }} | +{{ date('Y-m-d H:i:s',user.created_at) }} | +
编号 | +用户 | +角色 | +注册日期 | +状态 | +操作 | +
---|---|---|---|---|---|
{{ item.id }} | +{{ item.name }}{{ vip_info(item) }} | +{{ role_info(item) }} | +{{ date('Y-m-d',item.created_at) }} | +{{ status_info(item) }} | +
+
+
+
+
|
+
+ 官网chplayer.com,版本号:1.0
+以下仅列出部分功能,全部功能请至官网《手册》查看
++ + + + + + + 更多弹幕动作 +
+ +单独监听功能:
+
+ 播放状态:暂停
+ 无跳转时间
+ 当前音量:0.8
+ 是否全屏:否
+ 还未结束
+ 视频地址正常
+ 当前播放时间(秒):0
+
+ * $order = [
+ * 'out_trade_no' => '1514027114',
+ * 'refund_amount' => '0.01',
+ * ];
+ *
+ *
+ * @param array $order
+ * @return bool|mixed
+ */
+ public function refundOrder($order)
+ {
+ try {
+
+ $result = $this->gateway->refund($order);
+
+ return $result;
+
+ } catch (\Exception $e) {
+
+ Log::error('Alipay Refund Order Exception', [
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取二维码内容
+ *
+ * @param array $order
+ * @return bool|string
+ */
+ public function getQrCode($order)
+ {
+ try {
+
+ $response = $this->gateway->scan($order);
+
+ $result = $response->qr_code ?? false;
+
+ return $result;
+
+ } catch (\Exception $e) {
+
+ Log::error('Alipay Qrcode Exception', [
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+ }
+
+ /**
+ * 处理异步通知
+ **/
+ public function handleNotify()
+ {
+ try {
+
+ $data = $this->gateway->verify();
+
+ Log::debug('Alipay Verify Data', $data->all());
+
+ } catch (\Exception $e) {
+
+ Log::error('Alipay Verify Exception', [
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+
+ if ($data->trade_status != 'TRADE_SUCCESS') {
+ return false;
+ }
+
+ if ($data->app_id != $this->config->app_id) {
+ return false;
+ }
+
+ $tradeRepo = new TradeRepo();
+
+ $trade = $tradeRepo->findBySn($data->out_trade_no);
+
+ if (!$trade) return false;
+
+ if ($data->total_amount != $trade->amount) {
+ return false;
+ }
+
+ if ($trade->status != TradeModel::STATUS_PENDING) {
+ return false;
+ }
+
+ $trade->channel_sn = $data->trade_no;
+
+ $this->eventsManager->fire('payment:afterPay', $this, $trade);
+
+ return $this->gateway->success();
+ }
+
+ /**
+ * 获取 Alipay Gateway
+ *
+ * @return \Yansongda\Pay\Gateways\Alipay
+ */
+ public function getGateway()
+ {
+ $config = [
+ 'app_id' => $this->config->app_id,
+ 'ali_public_key' => $this->config->public_key,
+ 'private_key' => $this->config->private_key,
+ 'notify_url' => $this->config->notify_url,
+ 'log' => [
+ 'file' => log_path('alipay.log'),
+ 'level' => 'debug',
+ 'type' => 'daily',
+ 'max_file' => 30,
+ ],
+ 'mode' => 'dev',
+ ];
+
+ $gateway = Pay::alipay($config);
+
+ return $gateway;
+ }
+
+}
diff --git a/app/Services/Captcha.php b/app/Services/Captcha.php
new file mode 100644
index 00000000..3ec838f7
--- /dev/null
+++ b/app/Services/Captcha.php
@@ -0,0 +1,108 @@
+config = $this->getSectionConfig('captcha');
+ $this->logger = $this->getLogger('captcha');
+ $this->client = $this->getCaptchaClient();
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param string $ticket
+ * @param string $rand
+ * @return bool
+ */
+ function verify($ticket, $rand)
+ {
+ $appId = $this->config->app_id;
+ $secretKey = $this->config->secret_key;
+ $userIp = $this->request->getClientAddress();
+ $captchaType = 9;
+
+ try {
+
+ $request = new DescribeCaptchaResultRequest();
+
+ $params = json_encode([
+ 'Ticket' => $ticket,
+ 'Randstr' => $rand,
+ 'UserIp' => $userIp,
+ 'CaptchaType' => (int)$captchaType,
+ 'CaptchaAppId' => (int)$appId,
+ 'AppSecretKey' => $secretKey,
+ ]);
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Describe Captcha Result Request ' . $params);
+
+ $response = $this->client->DescribeCaptchaResult($request);
+
+ $this->logger->debug('Describe Captcha Result Response ' . $response->toJsonString());
+
+ $result = json_decode($response->toJsonString(), true);
+
+ return $result['CaptchaCode'] == 1 ? true : false;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Describe Captcha Result Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取CaptchaClient
+ *
+ * @return CaptchaClient
+ */
+ public function getCaptchaClient()
+ {
+ $secret = $this->getSectionConfig('secret');
+
+ $secretId = $secret->secret_id;
+ $secretKey = $secret->secret_key;
+
+ $region = $this->config->region ?? 'ap-guangzhou';
+
+ $credential = new Credential($secretId, $secretKey);
+
+ $httpProfile = new HttpProfile();
+
+ $httpProfile->setEndpoint(self::END_POINT);
+
+ $clientProfile = new ClientProfile();
+
+ $clientProfile->setHttpProfile($httpProfile);
+
+ $client = new CaptchaClient($credential, $region, $clientProfile);
+
+ return $client;
+ }
+
+}
diff --git a/app/Services/Category.php b/app/Services/Category.php
new file mode 100644
index 00000000..b65f6f19
--- /dev/null
+++ b/app/Services/Category.php
@@ -0,0 +1,82 @@
+findByCategoryIds($categoryIds);
+
+ $result = [];
+
+ if ($relations->count() > 0) {
+ foreach ($relations as $relation) {
+ $result[] = $relation->course_id;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 通过单个分类(顶级|子级)查找课程号
+ *
+ * @param integer $categoryId
+ * @return array
+ */
+ public function getCourseIdsBySingleCategory($categoryId)
+ {
+ $categoryRepo = new CategoryRepo();
+
+ $category = $categoryRepo->findById($categoryId);
+
+ $childCategoryIds = [];
+
+ if ($category->level == 1) {
+ $childCategories = $categoryRepo->findChildCategories($categoryId);
+ if ($childCategories->count() > 0) {
+ foreach ($childCategories as $category) {
+ $childCategoryIds[] = $category->id;
+ }
+ }
+ } else {
+ $childCategoryIds[] = $categoryId;
+ }
+
+ if (empty($childCategoryIds)) {
+ return [];
+ }
+
+ $courseCategoryRepo = new CourseCategoryRepo();
+
+ $relations = $courseCategoryRepo->findByCategoryIds($childCategoryIds);
+
+ $result = [];
+
+ if ($relations->count() > 0) {
+ foreach ($relations as $relation) {
+ $result[] = $relation->course_id;
+ }
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Services/CourseStats.php b/app/Services/CourseStats.php
new file mode 100644
index 00000000..380b991d
--- /dev/null
+++ b/app/Services/CourseStats.php
@@ -0,0 +1,133 @@
+findById($courseId);
+
+ $lessonCount = $courseRepo->countLessons($courseId);
+
+ $course->lesson_count = $lessonCount;
+
+ $course->update();
+ }
+
+ public function updateStudentCount($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ $studentCount = $courseRepo->countStudents($courseId);
+
+ $course->student_count = $studentCount;
+
+ $course->update();
+ }
+
+ public function updateArticleWordCount($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ $lessons = $courseRepo->findLessons($courseId);
+
+ if ($lessons->count() == 0) {
+ return;
+ }
+
+ $wordCount = 0;
+
+ foreach ($lessons as $lesson) {
+ if (isset($lesson->attrs->word_count)) {
+ $wordCount += $lesson->attrs->word_count;
+ }
+ }
+
+ if ($wordCount == 0) {
+ return;
+ }
+
+ /**
+ * @var \stdClass $attrs
+ */
+ $attrs = $course->attrs;
+ $attrs->word_count = $wordCount;
+ $course->update(['attrs' => $attrs]);
+ }
+
+ public function updateLiveDateRange($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ $lessons = $courseRepo->findChapters($course->id);
+
+ if ($lessons->count() == 0) {
+ return;
+ }
+
+ $scopes = [];
+
+ foreach ($lessons as $lesson) {
+ if (isset($lesson->attrs->start_time)) {
+ $scopes[] = $lesson->attrs->start_time;
+ }
+ }
+
+ if (!$scopes) {
+ return;
+ }
+
+ /**
+ * @var \stdClass $attrs
+ */
+ $attrs = $course->attrs;
+ $attrs->start_date = date('Y-m-d', min($scopes));
+ $attrs->end_date = date('Y-m-d', max($scopes));
+ $course->update(['attrs' => $attrs]);
+ }
+
+ public function updateVodDuration($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ $lessons = $courseRepo->findLessons($course->id);
+
+ if ($lessons->count() == 0) {
+ return;
+ }
+
+ $duration = 0;
+
+ foreach ($lessons as $lesson) {
+ if (isset($lesson->attrs->duration)) {
+ $duration += $lesson->attrs->duration;
+ }
+ }
+
+ if ($duration == 0) {
+ return;
+ }
+
+ /**
+ * @var \stdClass $attrs
+ */
+ $attrs = $course->attrs;
+ $attrs->duration = $duration;
+ $course->update(['attrs' => $attrs]);
+ }
+
+}
diff --git a/app/Services/Learning.php b/app/Services/Learning.php
new file mode 100644
index 00000000..dfdffdbd
--- /dev/null
+++ b/app/Services/Learning.php
@@ -0,0 +1,68 @@
+cache = $this->getDI()->get('cache');
+ }
+
+ public function save(LearningModel $learning, $timeout = 10)
+ {
+ // 秒和毫秒判断
+ if ($timeout > 1000) {
+ $timeout = intval($timeout / 1000);
+ }
+
+ $key = $this->getKey($learning->request_id);
+
+ $item = $this->cache->get($key);
+
+ $clientIp = $this->getClientIp();
+ $clientType = $this->getClientType();
+
+ $content = [
+ 'request_id' => $learning->request_id,
+ 'course_id' => $learning->course_id,
+ 'chapter_id' => $learning->chapter_id,
+ 'user_id' => $learning->user_id,
+ 'position' => $learning->position,
+ 'client_ip' => $clientIp,
+ 'client_type' => $clientType,
+ ];
+
+ if (!$item) {
+
+ $content['duration'] = $timeout;
+
+ $this->cache->save($key, $content, $this->lifetime);
+
+ } else {
+
+ $content['duration'] = $item->duration + $timeout;
+
+ $this->cache->save($key, $content, $this->lifetime);
+ }
+ }
+
+ public function getKey($requestId)
+ {
+ return "learning:{$requestId}";
+ }
+
+}
diff --git a/app/Services/Live.php b/app/Services/Live.php
new file mode 100644
index 00000000..9c1b2aed
--- /dev/null
+++ b/app/Services/Live.php
@@ -0,0 +1,103 @@
+config = $this->getSectionConfig('live');
+ }
+
+ /**
+ * 获取推流地址
+ *
+ * @param string $streamName
+ * @return string
+ */
+ function getPushUrl($streamName)
+ {
+ $authEnabled = $this->config->push_auth_enabled;
+ $authKey = $this->config->push_auth_key;
+ $expireTime = $this->config->push_auth_delta + time();
+ $domain = $this->config->push_domain;
+ $appName = 'live';
+
+ $authParams = $this->getAuthParams($streamName, $authKey, $expireTime);
+
+ $pushUrl = "rtmp://{$domain}/{$appName}/{$streamName}";
+ $pushUrl .= $authEnabled ? "?{$authParams}" : '';
+
+ return $pushUrl;
+ }
+
+ /**
+ * 获取拉流地址
+ *
+ * @param string $streamName
+ * @param string $format
+ * @return mixed
+ */
+ public function getPullUrls($streamName, $format)
+ {
+ $extension = ($format == 'hls') ? 'm3u8' : $format;
+
+ $extensions = ['flv', 'm3u8'];
+
+ if (!in_array($extension, $extensions)) return;
+
+ $appName = 'live';
+
+ $protocol = $this->config->pull_protocol;
+ $domain = $this->config->pull_domain;
+ $authEnabled = $this->config->pull_auth_enabled;
+ $transEnabled = $this->config->pull_trans_enabled;
+ $authKey = $this->config->pull_auth_key;
+ $expireTime = $this->config->pull_auth_delta + time();
+
+ $urls = [];
+
+ if ($transEnabled) {
+ foreach (['fd', 'sd', 'hd', 'od'] as $rateName) {
+ $realStreamName = ($rateName == 'od') ? $streamName : "{$streamName}_{$rateName}";
+ $authParams = $this->getAuthParams($realStreamName, $authKey, $expireTime);
+ $url = "{$protocol}://{$domain}/{$appName}/{$realStreamName}.{$extension}";
+ $url .= $authEnabled ? "?{$authParams}" : '';
+ $urls[$rateName] = $url;
+ }
+ } else {
+ $authParams = $this->getAuthParams($streamName, $authKey, $expireTime);
+ $url = "{$protocol}://{$domain}/{$appName}/{$streamName}.{$extension}";
+ $url .= $authEnabled ? "?{$authParams}" : '';
+ $urls['od'] = $url;
+ }
+
+ return $urls;
+ }
+
+ /**
+ * 获取鉴权参数
+ *
+ * @param string $streamName
+ * @param string $authKey
+ * @param integer $expireTime
+ * @return string
+ */
+ protected function getAuthParams($streamName, $authKey, $expireTime)
+ {
+ $txTime = strtoupper(base_convert($expireTime, 10, 16));
+
+ $txSecret = md5($authKey . $streamName . $txTime);
+
+ $authParams = http_build_query([
+ 'txSecret' => $txSecret,
+ 'txTime' => $txTime
+ ]);
+
+ return $authParams;
+ }
+
+}
diff --git a/app/Services/Mailer.php b/app/Services/Mailer.php
new file mode 100644
index 00000000..3f1b8139
--- /dev/null
+++ b/app/Services/Mailer.php
@@ -0,0 +1,66 @@
+manager = $this->getManager();
+ }
+
+ /**
+ * 发送测试邮件
+ *
+ * @param string $email
+ * @return mixed
+ */
+ public function sendTestMail($email)
+ {
+ $message = $this->manager->createMessage();
+
+ $result = $message->to($email)
+ ->subject('这是一封测试邮件')
+ ->content('这是一封测试邮件')
+ ->send();
+
+ return $result;
+ }
+
+ /**
+ * 获取Manager
+ */
+ protected function getManager()
+ {
+ $opt = $this->getSectionConfig('mailer');
+
+ $config = [
+ 'driver' => 'smtp',
+ 'host' => $opt->smtp_host,
+ 'port' => $opt->smtp_port,
+ 'from' => [
+ 'email' => $opt->smtp_from_email,
+ 'name' => $opt->smtp_from_name,
+ ],
+ ];
+
+ if ($opt->smtp_encryption) {
+ $config['encryption'] = $opt->smtp_encryption;
+ }
+
+ if ($opt->smtp_authentication) {
+ $config['username'] = $opt->smtp_username;
+ $config['password'] = $opt->smtp_password;
+ }
+
+ $manager = new MailerManager($config);
+
+ return $manager;
+ }
+
+}
diff --git a/app/Services/Order.php b/app/Services/Order.php
new file mode 100644
index 00000000..98c3ecb9
--- /dev/null
+++ b/app/Services/Order.php
@@ -0,0 +1,179 @@
+getDI()->get('auth')->getAuthUser();
+
+ $expiry = $course->expiry;
+ $expireTime = strtotime("+{$expiry} days");
+
+ $itemInfo = [
+ 'course' => [
+ 'id' => $course->id,
+ 'title' => $course->title,
+ 'cover' => $course->cover,
+ 'expiry' => $course->expiry,
+ 'market_price' => $course->market_price,
+ 'vip_price' => $course->vip_price,
+ 'expire_time' => $expireTime,
+ ]
+ ];
+
+ $amount = $authUser->vip ? $course->vip_price : $course->market_price;
+
+ $order = new OrderModel();
+
+ $order->user_id = $authUser->id;
+ $order->item_id = $course->id;
+ $order->item_type = OrderModel::TYPE_COURSE;
+ $order->item_info = $itemInfo;
+ $order->amount = $amount;
+ $order->subject = "课程 - {$course->title}";
+ $order->create();
+
+ return $order;
+ }
+
+ /**
+ * 创建套餐订单
+ *
+ * @param PackageModel $package
+ * @return OrderModel $order
+ */
+ public function createPackageOrder(PackageModel $package)
+ {
+ $authUser = $this->getDI()->get('auth')->getAuthUser();
+
+ $packageRepo = new PackageRepo();
+
+ $courses = $packageRepo->findCourses($package->id);
+
+ $itemInfo = [];
+
+ $itemInfo['package'] = [
+ 'id' => $package->id,
+ 'title' => $package->title,
+ 'market_price' => $package->market_price,
+ 'vip_price' => $package->vip_price,
+ ];
+
+ foreach ($courses as $course) {
+ $expiry = $course->expiry;
+ $expireTime = strtotime("+{$expiry} days");
+ $itemInfo['courses'][] = [
+ 'id' => $course->id,
+ 'title' => $course->title,
+ 'cover' => $course->cover,
+ 'expiry' => $expiry,
+ 'market_price' => $course->market_price,
+ 'vip_price' => $course->vip_price,
+ 'expire_time' => $expireTime,
+ ];
+ }
+
+ $amount = $authUser->vip ? $package->vip_price : $package->market_price;
+
+ $order = new OrderModel();
+
+ $order->user_id = $authUser->id;
+ $order->item_id = $package->id;
+ $order->item_type = OrderModel::TYPE_PACKAGE;
+ $order->item_info = $itemInfo;
+ $order->amount = $amount;
+ $order->subject = "套餐 - {$package->title}";
+ $order->create();
+
+ return $order;
+ }
+
+ /**
+ * 创建赞赏订单
+ *
+ * @param CourseModel $course
+ * @param float $amount
+ * @return OrderModel $order
+ */
+ public function createRewardOrder(CourseModel $course, $amount)
+ {
+ $authUser = $this->getDI()->get('auth')->getAuthUser();
+
+ $itemInfo = [
+ 'course' => [
+ 'id' => $course->id,
+ 'title' => $course->title,
+ 'cover' => $course->cover,
+ ]
+ ];
+
+ $order = new OrderModel();
+
+ $order->user_id = $authUser->id;
+ $order->item_id = $course->id;
+ $order->item_type = OrderModel::TYPE_REWARD;
+ $order->item_info = $itemInfo;
+ $order->amount = $amount;
+ $order->subject = "赞赏 - {$course->title}";
+ $order->create();
+
+ return $order;
+ }
+
+ /**
+ * 创建会员服务订单
+ *
+ * @param string $duration
+ * @return OrderModel
+ */
+ public function createVipOrder($duration)
+ {
+ $authUser = $this->getDI()->get('auth')->getAuthUser();
+
+ $vipInfo = new VipInfo();
+
+ $vipItem = $vipInfo->getItem($duration);
+
+ $itemInfo = [
+ 'vip' => [
+ 'duration' => $vipItem['duration'],
+ 'label' => $vipItem['label'],
+ 'price' => $vipItem['price'],
+ ]
+ ];
+
+ $order = new OrderModel();
+
+ $order->user_id = $authUser->id;
+ $order->item_type = OrderModel::TYPE_VIP;
+ $order->item_info = $itemInfo;
+ $order->amount = $vipItem['price'];
+ $order->subject = "会员 - 会员服务({$vipItem['label']})";
+ $order->create();
+
+ return $order;
+ }
+
+ /**
+ * 获取订单来源
+ */
+ protected function getSource()
+ {
+
+ }
+
+}
diff --git a/app/Services/Refund.php b/app/Services/Refund.php
new file mode 100644
index 00000000..b32ff5eb
--- /dev/null
+++ b/app/Services/Refund.php
@@ -0,0 +1,128 @@
+status != OrderModel::STATUS_FINISHED) {
+ //return $amount;
+ }
+
+ if ($order->item_type == OrderModel::TYPE_COURSE) {
+ $amount = $this->getCourseRefundAmount($order);
+ } elseif ($order->item_type == OrderModel::TYPE_PACKAGE) {
+ $amount = $this->getPackageRefundAmount($order);
+ }
+
+ return $amount;
+ }
+
+ protected function getCourseRefundAmount(OrderModel $order)
+ {
+ $course = $order->item_info->course;
+
+ $courseId = $order->item_id;
+ $userId = $order->user_id;
+ $amount = $order->amount;
+ $expireTime = $course->expire_time;
+
+ $refundAmount = 0.00;
+
+ if ($expireTime > time()) {
+ $percent = $this->getCourseRefundPercent($courseId, $userId);
+ $refundAmount = $amount * $percent;
+ }
+
+ return $refundAmount;
+ }
+
+ protected function getPackageRefundAmount(OrderModel $order)
+ {
+ $userId = $order->user_id;
+ $courses = $order->item_info->courses;
+ $amount = $order->amount;
+
+ $totalMarketPrice = 0.00;
+
+ foreach ($courses as $course) {
+ $totalMarketPrice += $course->market_price;
+ }
+
+ $totalRefundAmount = 0.00;
+
+ /**
+ * 按照占比方式计算退款
+ */
+ foreach ($courses as $course) {
+ if ($course->expire_time > time()) {
+ $pricePercent = round($course->market_price / $totalMarketPrice, 4);
+ $refundPercent = $this->getCourseRefundPercent($course->id, $userId);
+ $refundAmount = round($amount * $pricePercent * $refundPercent, 2);
+ $totalRefundAmount += $refundAmount;
+ }
+ }
+
+ return $totalRefundAmount;
+ }
+
+ protected function getCourseRefundPercent($courseId, $userId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $userLessons = $courseRepo->findUserLessons($courseId, $userId);
+
+ if ($userLessons->count() == 0) {
+ return 1.00;
+ }
+
+ $course = $courseRepo->findById($courseId);
+ $lessons = $courseRepo->findLessons($courseId);
+
+ $durationMapping = [];
+
+ foreach ($lessons as $lesson) {
+ $durationMapping[$lesson->id] = $lesson->attrs->duration ?? null;
+ }
+
+ $totalCount = $course->lesson_count;
+ $finishCount = 0;
+
+ /**
+ * 消费规则
+ * 1.点播观看时间大于时长30%
+ * 2.直播观看时间超过10分钟
+ * 3.图文浏览即消费
+ */
+ foreach ($userLessons as $learning) {
+ $chapterId = $learning->chapter_id;
+ $duration = $durationMapping[$chapterId] ?? null;
+ if ($course->model == CourseModel::MODEL_VOD) {
+ if ($duration && $learning->duration > 0.3 * $duration) {
+ $finishCount++;
+ }
+ } elseif ($course->model == CourseModel::MODEL_LIVE) {
+ if ($learning->duration > 600) {
+ $finishCount++;
+ }
+ } elseif ($course->model == CourseModel::MODEL_LIVE) {
+ $finishCount++;
+ }
+ }
+
+ $refundCount = $totalCount - $finishCount;
+
+ $percent = round($refundCount / $totalCount, 4);
+
+ return $percent;
+ }
+
+}
diff --git a/app/Services/Service.php b/app/Services/Service.php
new file mode 100644
index 00000000..0bee4d07
--- /dev/null
+++ b/app/Services/Service.php
@@ -0,0 +1,41 @@
+getInstance($channel);
+ }
+
+ /**
+ * 获取某组配置项
+ *
+ * @param string $section
+ * @return \stdClass
+ */
+ public function getSectionConfig($section)
+ {
+ $configCache = new ConfigCache();
+
+ $result = $configCache->getSectionConfig($section);
+
+ return $result;
+ }
+
+}
diff --git a/app/Services/Smser.php b/app/Services/Smser.php
new file mode 100644
index 00000000..ba9a4860
--- /dev/null
+++ b/app/Services/Smser.php
@@ -0,0 +1,110 @@
+config = $this->getSectionConfig('smser');
+ $this->logger = $this->getLogger('smser');
+ }
+
+ public function register()
+ {
+
+ }
+
+ public function resetPassword()
+ {
+
+ }
+
+ public function buyCourse()
+ {
+
+ }
+
+ public function buyMember()
+ {
+
+ }
+
+ /**
+ * 发送测试短信
+ *
+ * @param string $phone
+ * @return bool
+ */
+ public function sendTestMessage($phone)
+ {
+ $sender = $this->createSingleSender();
+ $templateId = $this->getTemplateId('register');
+ $signature = $this->getSignature();
+
+ $params = [888888, 5];
+
+ try {
+
+ $response = $sender->sendWithParam('86', $phone, $templateId, $params, $signature);
+
+ $this->logger->debug('Send Test Message Response ' . $response);
+
+ $content = json_decode($response, true);
+
+ return $content['result'] == 0 ? true : false;
+
+ } catch (\Exception $e) {
+
+ $this->logger->error('Send Test Message Exception ' . kg_json_encode([
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]));
+
+ return false;
+ }
+ }
+
+ protected function createSingleSender()
+ {
+ $sender = new SmsSingleSender($this->config->app_id, $this->config->app_key);
+
+ return $sender;
+ }
+
+ protected function createMultiSender()
+ {
+ $sender = new SmsMultiSender($this->config->app_id, $this->config->app_key);
+
+ return $sender;
+ }
+
+ protected function getRandNumber()
+ {
+ $result = rand(100, 999) . rand(100, 999);
+
+ return $result;
+ }
+
+ protected function getTemplateId($code)
+ {
+ $template = json_decode($this->config->template);
+
+ $templateId = $template->{$code}->id ?? null;
+
+ return $templateId;
+ }
+
+ protected function getSignature()
+ {
+ return $this->config->signature;
+ }
+
+}
diff --git a/app/Services/Storage.php b/app/Services/Storage.php
new file mode 100644
index 00000000..8945cc5a
--- /dev/null
+++ b/app/Services/Storage.php
@@ -0,0 +1,281 @@
+config = $this->getSectionConfig('storage');
+ $this->logger = $this->getLogger('storage');
+ $this->client = $this->getCosClient();
+ }
+
+ /**
+ * 上传测试文件
+ *
+ * @return bool
+ */
+ public function uploadTestFile()
+ {
+ $key = 'hello_world.txt';
+ $value = 'hello world';
+
+ $result = $this->putString($key, $value);
+
+ return $result;
+ }
+
+ /**
+ * 上传封面图片
+ *
+ * @return mixed
+ */
+ public function uploadCoverImage()
+ {
+ $result = $this->uploadImage('/img/cover/');
+
+ return $result;
+ }
+
+ /**
+ * 上传内容图片
+ *
+ * @return mixed
+ */
+ public function uploadContentImage()
+ {
+ $path = $this->uploadImage('/img/content/');
+
+ if (!$path) return false;
+
+ $contentImage = new ContentImageModel();
+
+ $contentImage->path = $path;
+
+ $contentImage->create();
+
+ $result = $this->url->get([
+ 'for' => 'home.content.img',
+ 'id' => $contentImage->id,
+ ]);
+
+ return $result;
+ }
+
+ /**
+ * 上传头像图片
+ *
+ * @return mixed
+ */
+ public function uploadAvatarImage()
+ {
+ $result = $this->uploadImage('/img/avatar/');
+
+ return $result;
+ }
+
+ /**
+ * 上传图片
+ *
+ * @param string $prefix
+ * @return mixed
+ */
+ public function uploadImage($prefix = '')
+ {
+ $paths = [];
+
+ if ($this->request->hasFiles(true)) {
+
+ $files = $this->request->getUploadedFiles(true);
+
+ foreach ($files as $file) {
+ $extension = $this->getFileExtension($file->getName());
+ $keyName = $this->generateFileName($extension, $prefix);
+ $path = $this->putFile($keyName, $file->getTempName());
+ if ($path) {
+ $paths[] = $path;
+ }
+ }
+ }
+
+ $result = !empty($paths[0]) ? $paths[0] : false;
+
+ return $result;
+ }
+
+ /**
+ * 上传字符内容
+ *
+ * @param string $key
+ * @param string $body
+ * @return mixed string|bool
+ */
+ public function putString($key, $body)
+ {
+ $bucket = $this->config->bucket_name;
+
+ try {
+
+ $response = $this->client->upload($bucket, $key, $body);
+
+ $result = $response['Location'] ? $key : false;
+
+ return $result;
+
+ } catch (\Exception $e) {
+
+ $this->logger->error('Put String Exception ' . kg_json_encode([
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 上传文件
+ *
+ * @param string $key
+ * @param string $fileName
+ * @return mixed string|bool
+ */
+ public function putFile($key, $fileName)
+ {
+ $bucket = $this->config->bucket_name;
+
+ try {
+
+ $body = fopen($fileName, 'rb');
+
+ $response = $this->client->upload($bucket, $key, $body);
+
+ $result = $response['Location'] ? $key : false;
+
+ return $result;
+
+ } catch (\Exception $e) {
+
+ $this->logger->error('Put File Exception ' . kg_json_encode([
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取存储桶文件URL
+ *
+ * @param string $key
+ * @return string
+ */
+ public function getBucketFileUrl($key)
+ {
+ $result = $this->getBucketBaseUrl() . $key;
+
+ return $result;
+ }
+
+ /**
+ * 获取数据万象图片URL
+ * @param string $key
+ * @param integer $width
+ * @param integer $height
+ * @return string
+ */
+ public function getCiImageUrl($key, $width = 0, $height = 0)
+ {
+ $result = $this->getCiBaseUrl() . $key;
+
+ return $result;
+ }
+
+ /**
+ * 获取存储桶根URL
+ *
+ * @return string
+ */
+ public function getBucketBaseUrl()
+ {
+ $protocol = $this->config->bucket_protocol;
+ $domain = $this->config->bucket_domain;
+ $result = $protocol . '://' . $domain;
+
+ return $result;
+ }
+
+ /**
+ * 获取数据万象根URL
+ *
+ * @return string
+ */
+ public function getCiBaseUrl()
+ {
+ $protocol = $this->config->ci_protocol;
+ $domain = $this->config->ci_domain;
+ $result = $protocol . '://' . $domain;
+
+ return $result;
+ }
+
+ /**
+ * 生成文件存储名
+ *
+ * @param string $extension
+ * @param string $prefix
+ * @return string
+ */
+ protected function generateFileName($extension = '', $prefix = '')
+ {
+ $randName = date('YmdHis') . rand(1000, 9999);
+
+ $result = $prefix . $randName . '.' . $extension;
+
+ return $result;
+ }
+
+ /**
+ * 获取文件扩展名
+ *
+ * @param $fileName
+ * @return string
+ */
+ protected function getFileExtension($fileName)
+ {
+ $result = pathinfo($fileName, PATHINFO_EXTENSION);
+
+ return strtolower($result);
+ }
+
+ /**
+ * 获取 CosClient
+ *
+ * @return CosClient
+ */
+ public function getCosClient()
+ {
+ $secret = $this->getSectionConfig('secret');
+
+ $client = new CosClient([
+ 'region' => $this->config->bucket_region,
+ 'schema' => 'https',
+ 'credentials' => [
+ 'secretId' => $secret->secret_id,
+ 'secretKey' => $secret->secret_key,
+ ]]);
+
+ return $client;
+ }
+
+}
diff --git a/app/Services/Trade.php b/app/Services/Trade.php
new file mode 100644
index 00000000..2c4e526b
--- /dev/null
+++ b/app/Services/Trade.php
@@ -0,0 +1,85 @@
+findBySn($sn);
+
+ $trade = new TradeModel();
+
+ $trade->user_id = $order->user_id;
+ $trade->order_sn = $order->sn;
+ $trade->subject = $order->subject;
+ $trade->amount = $order->amount;
+ $trade->channel = $channel;
+
+ $trade->create();
+
+ return $trade;
+ }
+
+ /**
+ * 获取交易二维码
+ *
+ * @param $sn
+ * @param $channel
+ * @return bool|string|null
+ */
+ public function getQrCode($sn, $channel)
+ {
+ $trade = $this->createTrade($sn, $channel);
+
+ $code = null;
+
+ if ($channel == TradeModel::CHANNEL_ALIPAY) {
+ $alipay = new Alipay();
+ $code = $alipay->getQrCode([
+ 'out_trade_no' => $trade->sn,
+ 'total_amount' => $trade->amount,
+ 'subject' => $trade->subject,
+ ]);
+ } elseif ($channel == TradeModel::CHANNEL_WXPAY) {
+ $wxpay = new Wxpay();
+ $code = $wxpay->getQrCode([
+ 'out_trade_no' => $trade->sn,
+ 'total_fee' => 100 * $trade->amount,
+ 'body' => $trade->subject,
+ ]);
+ }
+
+ return $code;
+ }
+
+ /**
+ * 获取交易状态
+ *
+ * @param string $sn
+ * @return string
+ */
+ public function getStatus($sn)
+ {
+ $tradeRepo = new TradeRepo();
+
+ $trade = $tradeRepo->findBySn($sn);
+
+ return $trade->status;
+ }
+
+}
diff --git a/app/Services/VipInfo.php b/app/Services/VipInfo.php
new file mode 100644
index 00000000..6fd54899
--- /dev/null
+++ b/app/Services/VipInfo.php
@@ -0,0 +1,67 @@
+config = $this->getSectionConfig('vip');
+ }
+
+ /**
+ * 获取条目
+ *
+ * @param string $duration
+ * @return array
+ */
+ public function getItem($duration)
+ {
+ $items = $this->getItems();
+
+ foreach ($items as $item) {
+ if ($item['duration'] == $duration) {
+ return $item;
+ }
+ }
+
+ return $items[0];
+ }
+
+ /**
+ * 获取条目列表
+ *
+ * @return array
+ */
+ public function getItems()
+ {
+ $items = [
+ [
+ 'duration' => 'one_month',
+ 'label' => '1个月',
+ 'price' => $this->config->one_month,
+ ],
+ [
+ 'duration' => 'three_month',
+ 'label' => '3个月',
+ 'price' => $this->config->three_month,
+ ],
+ [
+ 'duration' => 'six_month',
+ 'label' => '6个月',
+ 'price' => $this->config->six_month,
+ ],
+ [
+ 'duration' => 'twelve_month',
+ 'label' => '12个月',
+ 'price' => $this->config->twelve_month,
+ ],
+ ];
+
+ return $items;
+ }
+
+}
diff --git a/app/Services/Vod.php b/app/Services/Vod.php
new file mode 100644
index 00000000..76834772
--- /dev/null
+++ b/app/Services/Vod.php
@@ -0,0 +1,595 @@
+config = $this->getSectionConfig('vod');
+ $this->logger = $this->getLogger('vod');
+ $this->client = $this->getVodClient();
+ }
+
+ /**
+ * 配置测试
+ *
+ * @return bool
+ */
+ public function test()
+ {
+ try {
+
+ $request = new DescribeAudioTrackTemplatesRequest();
+
+ $params = '{}';
+
+ $request->fromJsonString($params);
+
+ $response = $this->client->DescribeAudioTrackTemplates($request);
+
+ $this->logger->debug('Describe Audio Track Templates Response ' . $response->toJsonString());
+
+ $result = $response->TotalCount > 0 ? true : false;
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Describe Audio Track Templates Exception ', kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取上传签名
+ *
+ * @return string
+ */
+ public function getUploadSignature()
+ {
+ $secret = $this->getSectionConfig('secret');
+
+ $secretId = $secret->secret_id;
+ $secretKey = $secret->secret_key;
+
+ $params = [
+ 'secretId' => $secretId,
+ 'currentTimeStamp' => time(),
+ 'expireTime' => time() + 86400,
+ 'random' => rand(1000, 9999),
+ ];
+
+ $original = http_build_query($params);
+ $hash = hash_hmac('SHA1', $original, $secretKey, true);
+ $signature = base64_encode($hash . $original);
+
+ return $signature;
+ }
+
+ /**
+ * 获取播放地址
+ *
+ * @param string $playUrl
+ * @return string
+ */
+ public function getPlayUrl($playUrl)
+ {
+ if ($this->config->key_anti_enabled == 0) {
+ return $playUrl;
+ }
+
+ $key = $this->config->key_anti_key;
+ $expiry = $this->config->key_anti_expiry ?: 10800;
+
+ $path = parse_url($playUrl, PHP_URL_PATH);
+ $pos = strrpos($path, '/');
+ $fileName = substr($path, $pos + 1);
+ $dirName = str_replace($fileName, '', $path);
+
+ $expiredTime = base_convert(time() + $expiry, 10, 16); // 过期时间(十六进制)
+ $tryTime = 0; // 试看时间,0不限制
+ $ipLimit = 0; // ip数量限制,0不限制
+ $random = rand(100000, 999999); // 随机数
+
+ /**
+ * 腾讯坑爹的参数类型和文档,先凑合吧
+ * 不限制试看 => 必须exper=0(不能设置为空)
+ * 不限制IP => 必须rlimit为空(不能设置为0),暂不可用
+ */
+ $myTryTime = $tryTime >= 0 ? $tryTime : 0;
+ $myIpLimit = $ipLimit > 0 ? $ipLimit : '';
+ $sign = $key . $dirName . $expiredTime . $myTryTime . $myIpLimit . $random; // 签名串
+
+ $query = [];
+
+ $query['t'] = $expiredTime;
+
+ if ($tryTime >= 0) {
+ $query['exper'] = $tryTime;
+ }
+
+ if ($ipLimit > 0) {
+ $query['rlimit'] = $ipLimit;
+ }
+
+ $query['us'] = $random;
+ $query['sign'] = md5($sign);
+
+ $result = $playUrl . '?' . http_build_query($query);
+
+ return $result;
+ }
+
+ /**
+ * 拉取事件
+ *
+ * @return bool|array
+ */
+ public function pullEvents()
+ {
+ try {
+
+ $request = new PullEventsRequest();
+
+ $params = '{}';
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Pull Events Request ' . $params);
+
+ $response = $this->client->PullEvents($request);
+
+ $this->logger->debug('Pull Events Response ' . $response->toJsonString());
+
+ $result = json_decode($response->toJsonString(), true);
+
+ return $result['EventSet'] ?? [];
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Pull Events Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 确认事件
+ *
+ * @param array $eventHandles
+ * @return bool|mixed
+ */
+ public function confirmEvents($eventHandles)
+ {
+ try {
+
+ $request = new ConfirmEventsRequest();
+
+ $params = json_encode(['EventHandles' => $eventHandles]);
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Confirm Events Request ' . $params);
+
+ $response = $this->client->ConfirmEvents($request);
+
+ $this->logger->debug('Confirm Events Response ' . $response->toJsonString());
+
+ $result = json_decode($response->toJsonString(), true);
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Confirm Events Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取媒体信息
+ *
+ * @param string $fileId
+ * @return array|bool
+ */
+ public function getMediaInfo($fileId)
+ {
+ try {
+
+ $request = new DescribeMediaInfosRequest();
+
+ $fileIds = [$fileId];
+
+ $params = json_encode(['FileIds' => $fileIds]);
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Describe Media Info Request ' . $params);
+
+ $response = $this->client->DescribeMediaInfos($request);
+
+ $this->logger->debug('Describe Media Info Response ' . $response->toJsonString());
+
+ $result = json_decode($response->toJsonString(), true);
+
+ if (!isset($result['MediaInfoSet'][0]['MetaData'])) {
+ return false;
+ }
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Describe Media Info Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取任务信息
+ *
+ * @param string $taskId
+ * @return array|bool
+ */
+ public function getTaskInfo($taskId)
+ {
+ try {
+
+ $request = new DescribeTaskDetailRequest();
+
+ $params = json_encode(['TaskId' => $taskId]);
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Describe Task Detail Request ' . $params);
+
+ $response = $this->client->DescribeTaskDetail($request);
+
+ $this->logger->debug('Describe Task Detail Response ' . $response->toJsonString());
+
+ $result = json_decode($response->toJsonString(), true);
+
+ if (!isset($result['TaskType'])) {
+ return false;
+ }
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Describe Task Detail Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 创建视频转码任务
+ *
+ * @param string $fileId
+ * @return string|bool
+ */
+ public function createTransVideoTask($fileId)
+ {
+ $originVideoInfo = $this->getOriginVideoInfo($fileId);
+
+ if (!$originVideoInfo) return false;
+
+ $videoTransTemplates = $this->getVideoTransTemplates();
+
+ $watermarkTemplate = $this->getWatermarkTemplate();
+
+ $transCodeTaskSet = [];
+
+ foreach ($videoTransTemplates as $key => $template) {
+ if ($originVideoInfo['width'] >= $template['width'] ||
+ $originVideoInfo['bit_rate'] >= 1000 * $template['bit_rate']
+ ) {
+ $item = ['Definition' => $key];
+ if ($watermarkTemplate) {
+ $item['WatermarkSet'][] = ['Definition' => $watermarkTemplate];
+ }
+ $transCodeTaskSet[] = $item;
+ }
+ }
+
+ /**
+ * 无匹配转码模板,取第一项转码
+ */
+ if (empty($transCodeTaskSet)) {
+ $keys = array_keys($videoTransTemplates);
+ $item = ['Definition' => $keys[0]];
+ if ($watermarkTemplate) {
+ $item['WatermarkSet'][] = ['Definition' => $watermarkTemplate];
+ }
+ $transCodeTaskSet[] = $item;
+ }
+
+ $params = json_encode([
+ 'FileId' => $fileId,
+ 'MediaProcessTask' => [
+ 'TranscodeTaskSet' => $transCodeTaskSet,
+ ],
+ ]);
+
+ try {
+
+ $request = new ProcessMediaRequest();
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Process Media Request ' . $params);
+
+ $response = $this->client->ProcessMedia($request);
+
+ $this->logger->debug('Process Media Response ' . $response->toJsonString());
+
+ $result = $response->TaskId ?: false;
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Process Media Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 创建音频转码任务
+ *
+ * @param string $fileId
+ * @return string|bool
+ */
+ public function createTransAudioTask($fileId)
+ {
+ $originAudioInfo = $this->getOriginAudioInfo($fileId);
+
+ if (!$originAudioInfo) return false;
+
+ $audioTransTemplates = $this->getAudioTransTemplates();
+
+ $transCodeTaskSet = [];
+
+ foreach ($audioTransTemplates as $key => $template) {
+ if ($originAudioInfo['bit_rate'] >= 1000 * $template['bit_rate']) {
+ $item = ['Definition' => $key];
+ $transCodeTaskSet[] = $item;
+ }
+ }
+
+ /**
+ * 无匹配转码模板,取第一项转码
+ */
+ if (empty($transCodeTaskSet)) {
+ $keys = array_keys($audioTransTemplates);
+ $item = ['Definition' => $keys[0]];
+ $transCodeTaskSet[] = $item;
+ }
+
+ $params = json_encode([
+ 'FileId' => $fileId,
+ 'MediaProcessTask' => [
+ 'TranscodeTaskSet' => $transCodeTaskSet,
+ ],
+ ]);
+
+ try {
+
+ $request = new ProcessMediaRequest();
+
+ $request->fromJsonString($params);
+
+ $this->logger->debug('Process Media Request ' . $params);
+
+ $response = $this->client->ProcessMedia($request);
+
+ $this->logger->debug('Process Media Response ' . $response->toJsonString());
+
+ $result = $response->TaskId ?: false;
+
+ return $result;
+
+ } catch (TencentCloudSDKException $e) {
+
+ $this->logger->error('Process Media Exception ' . kg_json_encode([
+ 'code' => $e->getErrorCode(),
+ 'message' => $e->getMessage(),
+ 'requestId' => $e->getRequestId(),
+ ]));
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取原始视频信息
+ *
+ * @param string $fileId
+ * @return array|bool
+ */
+ public function getOriginVideoInfo($fileId)
+ {
+ $response = $this->getMediaInfo($fileId);
+
+ if (!$response) return false;
+
+ $metaData = $response['MediaInfoSet'][0]['MetaData'];
+
+ $result = [
+ 'bit_rate' => $metaData['Bitrate'],
+ 'size' => $metaData['Size'],
+ 'width' => $metaData['Width'],
+ 'height' => $metaData['Height'],
+ 'duration' => $metaData['Duration'],
+ ];
+
+ return $result;
+ }
+
+ /**
+ * 获取原始音频信息
+ *
+ * @param string $fileId
+ * @return array|bool
+ */
+ public function getOriginAudioInfo($fileId)
+ {
+ $response = $this->getMediaInfo($fileId);
+
+ if (!$response) return false;
+
+ $metaData = $response['MediaInfoSet'][0]['MetaData'];
+
+ $result = [
+ 'bit_rate' => $metaData['Bitrate'],
+ 'size' => $metaData['Size'],
+ 'width' => $metaData['Width'],
+ 'height' => $metaData['Height'],
+ 'duration' => $metaData['Duration'],
+ ];
+
+ return $result;
+ }
+
+ /**
+ * 获取水印模板
+ *
+ * @return mixed
+ */
+ public function getWatermarkTemplate()
+ {
+ $result = null;
+
+ if ($this->config->watermark_enabled && $this->config->watermark_template > 0) {
+ $result = (int)$this->config->watermark_template;
+ }
+
+ return $result;
+ }
+
+ /***
+ * 获取视频转码模板
+ *
+ * @return array
+ */
+ public function getVideoTransTemplates()
+ {
+ $hls = [
+ 210 => ['width' => 480, 'bit_rate' => 256, 'frame_rate' => 24],
+ 220 => ['width' => 640, 'bit_rate' => 512, 'frame_rate' => 24],
+ 230 => ['width' => 1280, 'bit_rate' => 1024, 'frame_rate' => 25],
+ ];
+
+ $mp4 = [
+ 10 => ['width' => 480, 'bit_rate' => 256, 'frame_rate' => 24],
+ 20 => ['width' => 640, 'bit_rate' => 512, 'frame_rate' => 24],
+ 30 => ['width' => 1280, 'bit_rate' => 1024, 'frame_rate' => 25],
+ ];
+
+ $format = $this->config->video_format;
+
+ $result = $format == 'hls' ? $hls : $mp4;
+
+ return $result;
+ }
+
+ /**
+ * 获取音频转码模板
+ *
+ * @return array
+ */
+ public function getAudioTransTemplates()
+ {
+ $m4a = [
+ 1110 => ['bit_rate' => 48, 'sample_rate' => 44100],
+ 1120 => ['bit_rate' => 96, 'sample_rate' => 44100],
+ ];
+
+ $mp3 = [
+ 1010 => ['bit_rate' => 128, 'sample_rate' => 44100],
+ ];
+
+ $result = $this->config->audio_format == 'm4a' ? $m4a : $mp3;
+
+ return $result;
+ }
+
+ /**
+ * 获取VodClient
+ *
+ * @return VodClient
+ */
+ public function getVodClient()
+ {
+ $secret = $this->getSectionConfig('secret');
+
+ $secretId = $secret->secret_id;
+ $secretKey = $secret->secret_key;
+
+ $region = $this->config->storage_type == 'fixed' ? $this->config->storage_region : '';
+
+ $credential = new Credential($secretId, $secretKey);
+
+ $httpProfile = new HttpProfile();
+
+ $httpProfile->setEndpoint(self::END_POINT);
+
+ $clientProfile = new ClientProfile();
+
+ $clientProfile->setHttpProfile($httpProfile);
+
+ $client = new VodClient($credential, $region, $clientProfile);
+
+ return $client;
+ }
+
+}
diff --git a/app/Services/Wxpay.php b/app/Services/Wxpay.php
new file mode 100644
index 00000000..5e4b39f9
--- /dev/null
+++ b/app/Services/Wxpay.php
@@ -0,0 +1,127 @@
+config = $this->getSectionConfig('payment.wxpay');
+ $this->gateway = $this->getGateway();
+ }
+
+ /**
+ * 获取二维码内容
+ *
+ * @param array $order
+ * @return bool|string
+ */
+ public function qrcode($order)
+ {
+ try {
+
+ $response = $this->gateway->scan($order);
+
+ $result = $response->code_url ?? false;
+
+ return $result;
+
+ } catch (\Exception $e) {
+
+ Log::error('Wxpay Scan Error', [
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+ }
+
+ /**
+ * 处理异步通知
+ */
+ public function notify()
+ {
+ try {
+
+ $data = $this->gateway->verify();
+
+ Log::debug('Wxpay Verify Data', $data->all());
+
+ } catch (\Exception $e) {
+
+ Log::error('Wxpay Verify Error', [
+ 'code' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+
+ if ($data->result_code != 'SUCCESS') {
+ return false;
+ }
+
+ if ($data->mch_id != $this->config->mch_id) {
+ return false;
+ }
+
+ $tradeRepo = new TradeRepo();
+
+ $trade = $tradeRepo->findBySn($data->out_trade_no);
+
+ if (!$trade) {
+ return false;
+ }
+
+ if ($data->total_fee != 100 * $trade->amount) {
+ return false;
+ }
+
+ if ($trade->status != TradeModel::STATUS_PENDING) {
+ return false;
+ }
+
+ $trade->channel_sn = $data->transaction_id;
+
+ $this->eventsManager->fire('payment:afterPay', $this, $trade);
+
+ return $this->gateway->success();
+ }
+
+ /**
+ * 获取 Wxpay Gateway
+ *
+ * @return \Yansongda\Pay\Gateways\Wxpay
+ */
+ public function getGateway()
+ {
+ $config = [
+ 'app_id' => $this->config->app_id,
+ 'mch_id' => $this->config->mch_id,
+ 'key' => $this->config->key,
+ 'notify_url' => $this->config->notify_url,
+ 'log' => [
+ 'file' => log_path('wxpay.log'),
+ 'level' => 'debug',
+ 'type' => 'daily',
+ 'max_file' => 30,
+ ],
+ 'mode' => 'dev',
+ ];
+
+ $gateway = Pay::wxpay($config);
+
+ return $gateway;
+ }
+
+}
diff --git a/app/Traits/Ajax.php b/app/Traits/Ajax.php
new file mode 100644
index 00000000..a86fab82
--- /dev/null
+++ b/app/Traits/Ajax.php
@@ -0,0 +1,38 @@
+response->setStatusCode(200);
+ $this->response->setJsonContent($content);
+
+ return $this->response;
+ }
+
+ public function ajaxError($content = [])
+ {
+ $content['code'] = $content['code'] ?? 1;
+ $content['msg'] = $content['msg'] ?? $this->getErrorMessage($content['code']);
+
+ $this->response->setJsonContent($content);
+
+ return $this->response;
+ }
+
+ public function getErrorMessage($code)
+ {
+ $errors = require config_path() . '/errors.php';
+
+ $message = $errors[$code] ?? $code;
+
+ return $message;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Traits/Client.php b/app/Traits/Client.php
new file mode 100644
index 00000000..44a802e9
--- /dev/null
+++ b/app/Traits/Client.php
@@ -0,0 +1,30 @@
+request->getClientAddress();
+ }
+
+ public function getClientType()
+ {
+ $userAgent = $this->request->getServer('HTTP_USER_AGENT');
+
+ $result = new BrowserParser($userAgent);
+
+ $clientType = 'desktop';
+
+ if ($result->isMobile()) {
+ $clientType = 'mobile';
+ }
+
+ return $clientType;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Traits/Security.php b/app/Traits/Security.php
new file mode 100644
index 00000000..248656ea
--- /dev/null
+++ b/app/Traits/Security.php
@@ -0,0 +1,38 @@
+request->getHeader('X-Csrf-Token-Key');
+ $tokenValue = $this->request->getHeader('X-Csrf-Token-Value');
+ $checkToken = $this->security->checkToken($tokenKey, $tokenValue);
+
+ return $checkToken;
+ }
+
+ public function checkHttpReferer()
+ {
+ $httpHost = parse_url($this->request->getHttpReferer(), PHP_URL_HOST);
+
+ $checkHost = $httpHost == $this->request->getHttpHost();
+
+ return $checkHost;
+ }
+
+ public function notSafeRequest()
+ {
+ $method = $this->request->getMethod();
+
+ $whitelist = ['post', 'put', 'patch', 'delete'];
+
+ $result = in_array(strtolower($method), $whitelist);
+
+ return $result;
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/Transformers/ChapterList.php b/app/Transformers/ChapterList.php
new file mode 100644
index 00000000..81a5c5cb
--- /dev/null
+++ b/app/Transformers/ChapterList.php
@@ -0,0 +1,51 @@
+ $chapter) {
+ $chapters[$key]['finished'] = isset($status[$chapter['id']]) ? $status[$chapter['id']] : 0;
+ }
+
+ return $chapters;
+ }
+
+ public function handleTree($chapters)
+ {
+ $list = [];
+
+ foreach ($chapters as $chapter) {
+ if ($chapter['parent_id'] == 0) {
+ $list[$chapter['id']] = $chapter;
+ $list[$chapter['id']]['child'] = [];
+ } else {
+ $list[$chapter['parent_id']]['child'][] = $chapter;
+ }
+ }
+
+ usort($list, function($a, $b) {
+ return $a['priority'] > $b['priority'];
+ });
+
+ foreach ($list as $key => $value) {
+ usort($list[$key]['child'], function($a, $b) {
+ return $a['priority'] > $b['priority'];
+ });
+ }
+
+ return $list;
+ }
+
+}
diff --git a/app/Transformers/ChapterUserList.php b/app/Transformers/ChapterUserList.php
new file mode 100644
index 00000000..a87e59ce
--- /dev/null
+++ b/app/Transformers/ChapterUserList.php
@@ -0,0 +1,67 @@
+getChapters($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['course'] = $courses[$value['chapter_id']];
+ }
+
+ return $relations;
+ }
+
+ public function handleUsers($relations)
+ {
+ $users = $this->getUsers($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['user'] = $users[$value['user_id']];
+ }
+
+ return $relations;
+ }
+
+ protected function getChapters($relations)
+ {
+ $ids = kg_array_column($relations, 'chapter_id');
+
+ $courseRepo = new ChapterRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($relations)
+ {
+ $ids = kg_array_column($relations, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/CommentList.php b/app/Transformers/CommentList.php
new file mode 100644
index 00000000..8ba10f4f
--- /dev/null
+++ b/app/Transformers/CommentList.php
@@ -0,0 +1,100 @@
+getCourses($comments);
+
+ foreach ($comments as $key => $comment) {
+ $comments[$key]['course'] = $courses[$comment['course_id']];
+ }
+
+ return $comments;
+ }
+
+ public function handleChapters($comments)
+ {
+ $chapters = $this->getChapters($comments);
+
+ foreach ($comments as $key => $comment) {
+ $comments[$key]['chapter'] = $chapters[$comment['chapter_id']];
+ }
+
+ return $comments;
+ }
+
+ public function handleUsers($comments)
+ {
+ $users = $this->getUsers($comments);
+
+ foreach ($comments as $key => $comment) {
+ $comments[$key]['user'] = $users[$comment['user_id']];
+ $comments[$key]['to_user'] = $comment['to_user_id'] > 0 ? $users[$comment['to_user_id']] : [];
+ }
+
+ return $comments;
+ }
+
+ protected function getCourses($comments)
+ {
+ $ids = kg_array_column($comments, 'course_id');
+
+ $courseRepo = new CourseRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getChapters($comments)
+ {
+ $ids = kg_array_column($comments, 'chapter_id');
+
+ $chapterRepo = new ChapterRepo();
+
+ $chapters = $chapterRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($chapters as $chapter) {
+ $result[$chapter['id']] = $chapter;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($comments)
+ {
+ $userIds = kg_array_column($comments, 'user_id');
+ $toUserIds = kg_array_column($comments, 'to_user_id');
+
+ $ids = array_merge($userIds, $toUserIds);
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/CourseFavoriteList.php b/app/Transformers/CourseFavoriteList.php
new file mode 100644
index 00000000..3833a7da
--- /dev/null
+++ b/app/Transformers/CourseFavoriteList.php
@@ -0,0 +1,67 @@
+getCourses($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['course'] = $courses[$value['course_id']];
+ }
+
+ return $relations;
+ }
+
+ public function handleUsers($relations)
+ {
+ $users = $this->getUsers($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['user'] = $users[$value['user_id']];
+ }
+
+ return $relations;
+ }
+
+ protected function getCourses($relations)
+ {
+ $ids = kg_array_column($relations, 'course_id');
+
+ $courseRepo = new CourseRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($relations)
+ {
+ $ids = kg_array_column($relations, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/CourseList.php b/app/Transformers/CourseList.php
new file mode 100644
index 00000000..801f565a
--- /dev/null
+++ b/app/Transformers/CourseList.php
@@ -0,0 +1,92 @@
+getCategories($courses);
+
+ foreach ($courses as $key => $course) {
+ $courses[$key]['categories'] = $categories[$course['id']] ?? [];
+ }
+
+ return $courses;
+ }
+
+ public function handleUsers($courses)
+ {
+ $users = $this->getUsers($courses);
+
+ foreach ($courses as $key => $course) {
+ $courses[$key]['user'] = $users[$course['user_id']];
+ }
+
+ return $courses;
+ }
+
+ public function handleCourses($courses)
+ {
+ foreach ($courses as $key => $course) {
+ unset($courses[$key]['details']);
+ }
+
+ return $courses;
+ }
+
+ protected function getCategories($courses)
+ {
+ $categoryRepo = new CategoryRepo();
+
+ $categories = $categoryRepo->findAll();
+
+ $mapping = [];
+
+ foreach ($categories as $category) {
+ $mapping[$category->id] = [
+ 'id' => $category->id,
+ 'name' => $category->name,
+ ];
+ }
+
+ $courseIds = kg_array_column($courses, 'id');
+
+ $courseCategoryRepo = new CourseCategoryRepo();
+
+ $relations = $courseCategoryRepo->findByCourseIds($courseIds);
+
+ $result = [];
+
+ foreach ($relations as $relation) {
+ $categoryId = $relation->category_id;
+ $courseId = $relation->course_id;
+ $result[$courseId][] = $mapping[$categoryId] ?? [];
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($courses)
+ {
+ $ids = kg_array_column($courses, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/CourseUserList.php b/app/Transformers/CourseUserList.php
new file mode 100644
index 00000000..d1edc302
--- /dev/null
+++ b/app/Transformers/CourseUserList.php
@@ -0,0 +1,67 @@
+getCourses($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['course'] = $courses[$value['course_id']];
+ }
+
+ return $relations;
+ }
+
+ public function handleUsers($relations)
+ {
+ $users = $this->getUsers($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['user'] = $users[$value['user_id']];
+ }
+
+ return $relations;
+ }
+
+ protected function getCourses($relations)
+ {
+ $ids = kg_array_column($relations, 'course_id');
+
+ $courseRepo = new CourseRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title', 'cover'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($relations)
+ {
+ $ids = kg_array_column($relations, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/LearningList.php b/app/Transformers/LearningList.php
new file mode 100644
index 00000000..74caa99a
--- /dev/null
+++ b/app/Transformers/LearningList.php
@@ -0,0 +1,96 @@
+getCourses($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['course'] = $courses[$value['course_id']];
+ }
+
+ return $relations;
+ }
+
+ public function handleChapters($relations)
+ {
+ $chapters = $this->getChapters($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['chapter'] = $chapters[$value['chapter_id']];
+ }
+
+ return $relations;
+ }
+
+ public function handleUsers($relations)
+ {
+ $users = $this->getUsers($relations);
+
+ foreach ($relations as $key => $value) {
+ $relations[$key]['user'] = $users[$value['user_id']];
+ }
+
+ return $relations;
+ }
+
+ protected function getCourses($relations)
+ {
+ $ids = kg_array_column($relations, 'course_id');
+
+ $courseRepo = new CourseRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title', 'cover'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getChapters($relations)
+ {
+ $ids = kg_array_column($relations, 'chapter_id');
+
+ $chapterRepo = new ChapterRepo();
+
+ $chapters = $chapterRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($chapters as $chapter) {
+ $result[$chapter['id']] = $chapter;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($relations)
+ {
+ $ids = kg_array_column($relations, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/OrderList.php b/app/Transformers/OrderList.php
new file mode 100644
index 00000000..8ab4fdd3
--- /dev/null
+++ b/app/Transformers/OrderList.php
@@ -0,0 +1,99 @@
+imgBaseUrl = kg_img_base_url();
+ }
+
+ public function handleItems($orders)
+ {
+ $itemInfo = [];
+
+ foreach ($orders as $key => $order) {
+ switch ($order['item_type']) {
+ case OrderModel::TYPE_COURSE:
+ $itemInfo = $this->handleCourseInfo($order['item_info']);
+ break;
+ case OrderModel::TYPE_PACKAGE:
+ $itemInfo = $this->handlePackageInfo($order['item_info']);
+ break;
+ case OrderModel::TYPE_REWARD:
+ $itemInfo = $this->handleRewardInfo($order['item_info']);
+ break;
+ }
+ $orders[$key]['item_info'] = $itemInfo;
+ }
+
+ return $orders;
+ }
+
+ protected function handleCourseInfo($itemInfo)
+ {
+ if (!empty($itemInfo) && is_string($itemInfo)) {
+ $itemInfo = json_decode($itemInfo, true);
+ $itemInfo['course']['cover'] = $this->imgBaseUrl . $itemInfo['course']['cover'];
+ }
+
+ return $itemInfo;
+ }
+
+ protected function handlePackageInfo($itemInfo)
+ {
+ if (!empty($itemInfo) && is_string($itemInfo)) {
+ $itemInfo = json_decode($itemInfo, true);
+ foreach ($itemInfo['courses'] as $key => $course) {
+ $itemInfo['courses'][$key]['cover'] = $this->imgBaseUrl . $course['cover'];
+ }
+ }
+
+ return $itemInfo;
+ }
+
+ protected function handleRewardInfo($itemInfo)
+ {
+ if (!empty($itemInfo) && is_string($itemInfo)) {
+ $itemInfo = json_decode($itemInfo, true);
+ $itemInfo['course']['cover'] = $this->imgBaseUrl . $itemInfo['course']['cover'];
+ }
+
+ return $itemInfo;
+ }
+
+ public function handleUsers($orders)
+ {
+ $users = $this->getUsers($orders);
+
+ foreach ($orders as $key => $order) {
+ $orders[$key]['user'] = $users[$order['user_id']];
+ }
+
+ return $orders;
+ }
+
+ protected function getUsers($orders)
+ {
+ $ids = kg_array_column($orders, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/RefundList.php b/app/Transformers/RefundList.php
new file mode 100644
index 00000000..4d7909e2
--- /dev/null
+++ b/app/Transformers/RefundList.php
@@ -0,0 +1,38 @@
+getUsers($refunds);
+
+ foreach ($refunds as $key => $refund) {
+ $refunds[$key]['user'] = $users[$refund['user_id']];
+ }
+
+ return $refunds;
+ }
+
+ protected function getUsers($refunds)
+ {
+ $ids = kg_array_column($refunds, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/ReviewList.php b/app/Transformers/ReviewList.php
new file mode 100644
index 00000000..7c397448
--- /dev/null
+++ b/app/Transformers/ReviewList.php
@@ -0,0 +1,67 @@
+getCourses($reviews);
+
+ foreach ($reviews as $key => $review) {
+ $reviews[$key]['course'] = $courses[$review['course_id']];
+ }
+
+ return $reviews;
+ }
+
+ public function handleUsers($reviews)
+ {
+ $users = $this->getUsers($reviews);
+
+ foreach ($reviews as $key => $review) {
+ $reviews[$key]['user'] = $users[$review['user_id']];
+ }
+
+ return $reviews;
+ }
+
+ protected function getCourses($reviews)
+ {
+ $ids = kg_array_column($reviews, 'course_id');
+
+ $courseRepo = new CourseRepo();
+
+ $courses = $courseRepo->findByIds($ids, ['id', 'title'])->toArray();
+
+ $result = [];
+
+ foreach ($courses as $course) {
+ $result[$course['id']] = $course;
+ }
+
+ return $result;
+ }
+
+ protected function getUsers($reviews)
+ {
+ $ids = kg_array_column($reviews, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/TradeList.php b/app/Transformers/TradeList.php
new file mode 100644
index 00000000..20aa6910
--- /dev/null
+++ b/app/Transformers/TradeList.php
@@ -0,0 +1,38 @@
+getUsers($trades);
+
+ foreach ($trades as $key => $trade) {
+ $trades[$key]['user'] = $users[$trade['user_id']];
+ }
+
+ return $trades;
+ }
+
+ protected function getUsers($trades)
+ {
+ $ids = kg_array_column($trades, 'user_id');
+
+ $userRepo = new UserRepo();
+
+ $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar'])->toArray();
+
+ $result = [];
+
+ foreach ($users as $user) {
+ $result[$user['id']] = $user;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/app/Transformers/Transformer.php b/app/Transformers/Transformer.php
new file mode 100644
index 00000000..3aef33b3
--- /dev/null
+++ b/app/Transformers/Transformer.php
@@ -0,0 +1,17 @@
+getAdminRoles($users);
+
+ foreach ($users as $key => $user) {
+ $users[$key]['admin_role'] = $roles[$user['admin_role']] ?? ['id' => 0, 'name' => 'N/A'];
+ }
+
+ return $users;
+ }
+
+ public function handleEduRoles($users)
+ {
+ $roles = $this->getEduRoles($users);
+
+ foreach ($users as $key => $user) {
+ $users[$key]['edu_role'] = $roles[$user['edu_role']] ?? ['id' => 0, 'name' => 'N/A'];
+ }
+
+ return $users;
+ }
+
+ private function getAdminRoles($users)
+ {
+ $ids = kg_array_column($users, 'admin_role');
+
+ $roleRepo = new RoleRepo();
+
+ $roles = $roleRepo->findByIds($ids, ['id', 'name'])->toArray();
+
+ $result = [];
+
+ foreach ($roles as $role) {
+ $result[$role['id']] = $role;
+ }
+
+ return $result;
+ }
+
+ private function getEduRoles()
+ {
+ $result = [
+ UserModel::EDU_ROLE_STUDENT => [
+ 'id' => UserModel::EDU_ROLE_STUDENT,
+ 'name' => '学员',
+ ],
+ UserModel::EDU_ROLE_TEACHER => [
+ 'id' => UserModel::EDU_ROLE_TEACHER,
+ 'name' => '讲师',
+ ],
+ ];
+
+ return $result;
+ }
+
+}
diff --git a/app/Validators/Category.php b/app/Validators/Category.php
new file mode 100644
index 00000000..484cf041
--- /dev/null
+++ b/app/Validators/Category.php
@@ -0,0 +1,82 @@
+findById($id);
+
+ if (!$category) {
+ throw new NotFoundException('category.not_found');
+ }
+
+ return $category;
+ }
+
+ public function checkParent($parentId)
+ {
+ $categoryRepo = new CategoryRepo();
+
+ $category = $categoryRepo->findById($parentId);
+
+ if (!$category || $category->deleted == 1) {
+ throw new BadRequestException('category.parent_not_found');
+ }
+
+ return $category;
+ }
+
+ public function checkName($name)
+ {
+ $value = $this->filter->sanitize($name, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('category.name_too_short');
+ }
+
+ if ($length > 30) {
+ throw new BadRequestException('category.name_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkPriority($priority)
+ {
+ $value = $this->filter->sanitize($priority, ['trim', 'int']);
+
+ if ($value < 1 || $value > 255) {
+ throw new BadRequestException('category.invalid_priority');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('category.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Chapter.php b/app/Validators/Chapter.php
new file mode 100644
index 00000000..263b20dd
--- /dev/null
+++ b/app/Validators/Chapter.php
@@ -0,0 +1,179 @@
+findById($id);
+
+ if (!$chapter) {
+ throw new NotFoundException('chapter.not_found');
+ }
+
+ return $chapter;
+ }
+
+ public function checkCourseId($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ if (!$course) {
+ throw new BadRequestException('chapter.invalid_course_id');
+ }
+
+ return $course->id;
+ }
+
+ public function checkParentId($parentId)
+ {
+ $chapterRepo = new ChapterRepo();
+
+ $chapter = $chapterRepo->findById($parentId);
+
+ if (!$chapter) {
+ throw new BadRequestException('chapter.invalid_parent_id');
+ }
+
+ return $chapter->id;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('chapter.title_too_short');
+ }
+
+ if ($length > 50) {
+ throw new BadRequestException('chapter.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkSummary($summary)
+ {
+ $value = $this->filter->sanitize($summary, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkPriority($priority)
+ {
+ $value = $this->filter->sanitize($priority, ['trim', 'int']);
+
+ if ($value < 1 || $value > 255) {
+ throw new BadRequestException('chapter.invalid_priority');
+ }
+
+ return $value;
+ }
+
+ public function checkFreeStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($status, [0, 1])) {
+ throw new BadRequestException('chapter.invalid_free_status');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('course.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishAbility($chapter)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($chapter->course_id);
+
+ if ($course->model == CourseModel::MODEL_VOD) {
+ if ($chapter->attrs['upload'] == 0) {
+ throw new BadRequestException('chapter.vod_not_uploaded');
+ }
+ if ($chapter->attrs['translate'] != 'finished') {
+ throw new BadRequestException('chapter.vod_not_translated');
+ }
+ } elseif ($course->model == CourseModel::MODEL_LIVE) {
+ if ($chapter->attrs['start_time'] == 0) {
+ throw new BadRequestException('chapter.live_time_empty');
+ }
+ } elseif ($course->model == CourseModel::MODEL_ARTICLE) {
+ if ($chapter->attrs['word_count'] == 0) {
+ throw new BadRequestException('chapter.article_content_empty');
+ }
+ }
+ }
+
+ public function checkViewPrivilege($user, $chapter, $course)
+ {
+ if ($chapter->parent_id == 0) {
+ return false;
+ }
+
+ if ($course->deleted == 1) {
+ return false;
+ }
+
+ if ($chapter->published == 0) {
+ return false;
+ }
+
+ if ($chapter->free == 1) {
+ return true;
+ }
+
+ if ($course->price == 0) {
+ return true;
+ }
+
+ if ($user->id == 0) {
+ return false;
+ }
+
+ $courseUserRepo = new CourseUserRepo();
+
+ $courseUser = $courseUserRepo->findCourseUser($user->id, $course->id);
+
+ if (!$courseUser) {
+ return false;
+ }
+
+ if ($courseUser->expire_at < time()) {
+ return false;
+ }
+ }
+
+}
diff --git a/app/Validators/ChapterArticle.php b/app/Validators/ChapterArticle.php
new file mode 100644
index 00000000..108be9ce
--- /dev/null
+++ b/app/Validators/ChapterArticle.php
@@ -0,0 +1,26 @@
+filter->sanitize($content, ['trim']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 10) {
+ throw new BadRequestException('chapter_article.content_too_short');
+ }
+
+ if ($length > 65535) {
+ throw new BadRequestException('chapter_article.content_too_long');
+ }
+
+ return $value;
+ }
+}
diff --git a/app/Validators/ChapterAudio.php b/app/Validators/ChapterAudio.php
new file mode 100644
index 00000000..02e0c158
--- /dev/null
+++ b/app/Validators/ChapterAudio.php
@@ -0,0 +1,22 @@
+filter->sanitize($fileId, ['trim', 'string']);
+
+ if (!CommonValidator::intNumber($value)) {
+ throw new BadRequestException('chapter_audio.invalid_file_id');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/ChapterLive.php b/app/Validators/ChapterLive.php
new file mode 100644
index 00000000..5ee226c1
--- /dev/null
+++ b/app/Validators/ChapterLive.php
@@ -0,0 +1,51 @@
+= $endTimeStamp) {
+ throw new BadRequestException('chapter_live.start_gt_end');
+ }
+
+ if ($endTimeStamp - $startTimeStamp > 3 * 3600) {
+ throw new BadRequestException('chapter_live.time_too_long');
+ }
+ }
+
+}
diff --git a/app/Validators/ChapterVod.php b/app/Validators/ChapterVod.php
new file mode 100644
index 00000000..becf93b9
--- /dev/null
+++ b/app/Validators/ChapterVod.php
@@ -0,0 +1,22 @@
+filter->sanitize($fileId, ['trim', 'string']);
+
+ if (!CommonValidator::intNumber($value)) {
+ throw new BadRequestException('chapter_vod.invalid_file_id');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Course.php b/app/Validators/Course.php
new file mode 100644
index 00000000..b3e20b8a
--- /dev/null
+++ b/app/Validators/Course.php
@@ -0,0 +1,178 @@
+findById($id);
+
+ if (!$course) {
+ throw new NotFoundException('course.not_found');
+ }
+
+ return $course;
+ }
+
+ public function checkModel($model)
+ {
+ $value = $this->filter->sanitize($model, ['trim', 'string']);
+
+ $scopes = CourseModel::models();
+
+ if (!isset($scopes[$value])) {
+ throw new BadRequestException('course.invalid_model');
+ }
+
+ return $value;
+ }
+
+ public function checkCover($cover)
+ {
+ $value = $this->filter->sanitize($cover, ['trim', 'string']);
+
+ if (!CommonValidator::url($value)) {
+ throw new BadRequestException('course.invalid_cover');
+ }
+
+ $result = parse_url($value, PHP_URL_PATH);
+
+ return $result;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 5) {
+ throw new BadRequestException('course.title_too_short');
+ }
+
+ if ($length > 50) {
+ throw new BadRequestException('course.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkDetails($details)
+ {
+ $value = $this->filter->sanitize($details, ['trim']);
+
+ return $value;
+ }
+
+ public function checkSummary($summary)
+ {
+ $value = $this->filter->sanitize($summary, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkKeywords($keywords)
+ {
+ $value = $this->filter->sanitize($keywords, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkMarketPrice($price)
+ {
+ $value = $this->filter->sanitize($price, ['trim', 'float']);
+
+ if ($value < 0 || $value > 10000) {
+ throw new BadRequestException('course.invalid_market_price');
+ }
+
+ return $value;
+ }
+
+ public function checkVipPrice($price)
+ {
+ $value = $this->filter->sanitize($price, ['trim', 'float']);
+
+ if ($value < 0 || $value > 10000) {
+ throw new BadRequestException('course.invalid_vip_price');
+ }
+
+ return $value;
+ }
+
+ public function checkExpiry($expiry)
+ {
+ $value = $this->filter->sanitize($expiry, ['trim', 'int']);
+
+ if ($value < 1 || $value > 3 * 365) {
+ throw new BadRequestException('course.invalid_expiry');
+ }
+
+ return $value;
+ }
+
+ public function checkLevel($level)
+ {
+ $value = $this->filter->sanitize($level, ['trim', 'string']);
+
+ $scopes = CourseModel::levels();
+
+ if (!isset($scopes[$value])) {
+ throw new BadRequestException('course.invalid_level');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('course.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishAbility($course)
+ {
+ $courseRepo = new CourseRepo();
+
+ $chapters = $courseRepo->findChapters($course->id);
+
+ $totalCount = $chapters->count();
+
+ if ($totalCount < 1) {
+ throw new BadRequestException('course.pub_chapter_not_found');
+ }
+
+ $publishedCount = 0;
+
+ foreach ($chapters as $chapter) {
+ if ($chapter->parent_id > 0 && $chapter->published == 1) {
+ $publishedCount++;
+ }
+ }
+
+ if ($publishedCount < $totalCount / 3) {
+ throw new BadRequestException('course.pub_chapter_too_few');
+ }
+ }
+
+}
diff --git a/app/Validators/CourseQuery.php b/app/Validators/CourseQuery.php
new file mode 100644
index 00000000..09c6e6cd
--- /dev/null
+++ b/app/Validators/CourseQuery.php
@@ -0,0 +1,128 @@
+filter->sanitize($courseId, ['trim', 'int']);
+
+ if ($value > 0) {
+ return $value;
+ }
+
+ return false;
+ }
+
+ public function checkUserId($userId)
+ {
+ $value = $this->filter->sanitize($userId, ['trim', 'int']);
+
+ if ($value > 0) {
+ return $value;
+ }
+
+ return false;
+ }
+
+ public function checkCategoryId($categoryId)
+ {
+ $value = $this->filter->sanitize($categoryId, ['trim', 'int']);
+
+ if ($value <= 0) {
+ return false;
+ }
+
+ $categoryRepo = new CategoryRepo();
+
+ $category = $categoryRepo->findById($value);
+
+ if (!$category) {
+ return false;
+ }
+
+ return $category->id;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ if (!empty($value)) {
+ return $value;
+ }
+
+ return false;
+ }
+
+ public function checkLevel($level)
+ {
+ $value = $this->filter->sanitize($level, ['trim', 'int']);
+
+ $scopes = [
+ CourseModel::LEVEL_ENTRY,
+ CourseModel::LEVEL_JUNIOR,
+ CourseModel::LEVEL_MIDDLE,
+ CourseModel::LEVEL_SENIOR,
+ ];
+
+ if (in_array($value, $scopes)) {
+ return $value;
+ }
+
+ return false;
+ }
+
+ public function checkPrice($price)
+ {
+ $value = $this->filter->sanitize($price, ['trim', 'float']);
+
+ if ($value < 0) {
+ throw new BadRequestException('无效的价格');
+ }
+
+ return $value;
+ }
+
+ public function checkStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ $scopes = [
+ CourseModel::LEVEL_ENTRY,
+ CourseModel::LEVEL_JUNIOR,
+ CourseModel::LEVEL_MIDDLE,
+ CourseModel::LEVEL_SENIOR,
+ ];
+
+ if (in_array($value, $scopes)) {
+ return $value;
+ }
+
+ return false;
+ }
+
+ public function checkSort($sort)
+ {
+ switch ($sort) {
+ case 'rating':
+ $orderBy = 'rating DESC';
+ break;
+ case 'score':
+ $orderBy = 'score DESC';
+ break;
+ default:
+ $orderBy = 'id DESC';
+ break;
+ }
+
+ return $orderBy;
+ }
+
+}
diff --git a/app/Validators/CourseUser.php b/app/Validators/CourseUser.php
new file mode 100644
index 00000000..e7901faf
--- /dev/null
+++ b/app/Validators/CourseUser.php
@@ -0,0 +1,96 @@
+findCourseStudent($courseId, $userId);
+
+ if (!$courseUser) {
+ throw new BadRequestException('course_student.not_found');
+ }
+
+ return $courseUser;
+ }
+
+ public function checkCourseId($courseId)
+ {
+ $value = $this->filter->sanitize($courseId, ['trim', 'int']);
+
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($value);
+
+ if (!$course) {
+ throw new BadRequestException('course_student.course_not_found');
+ }
+
+ return $course->id;
+ }
+
+ public function checkUserId($userId)
+ {
+ $value = $this->filter->sanitize($userId, ['trim', 'int']);
+
+ $userRepo = new UserRepo();
+
+ $user = $userRepo->findById($value);
+
+ if (!$user) {
+ throw new BadRequestException('course_student.user_not_found');
+ }
+
+ return $user->id;
+ }
+
+ public function checkExpireTime($expireTime)
+ {
+ $value = $this->filter->sanitize($expireTime, ['trim', 'string']);
+
+ if (!CommonValidator::date($value, 'Y-m-d H:i:s')) {
+ throw new BadRequestException('course_student.invalid_expire_time');
+ }
+
+ return strtotime($value);
+ }
+
+ public function checkLockStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('course_student.invalid_lock_status');
+ }
+
+ return $value;
+ }
+
+ public function checkIfJoined($courseId, $userId)
+ {
+ $repo = new CourseUserRepo();
+
+ $courseUser = $repo->findCourseStudent($courseId, $userId);
+
+ if ($courseUser) {
+ throw new BadRequestException('course_student.user_has_joined');
+ }
+ }
+
+}
diff --git a/app/Validators/Nav.php b/app/Validators/Nav.php
new file mode 100644
index 00000000..2c579a90
--- /dev/null
+++ b/app/Validators/Nav.php
@@ -0,0 +1,125 @@
+findById($id);
+
+ if (!$nav) {
+ throw new NotFoundException('nav.not_found');
+ }
+
+ return $nav;
+ }
+
+ public function checkParent($parentId)
+ {
+ $navRepo = new NavRepo();
+
+ $nav = $navRepo->findById($parentId);
+
+ if (!$nav || $nav->deleted == 1) {
+ throw new BadRequestException('nav.parent_not_found');
+ }
+
+ return $nav;
+ }
+
+ public function checkName($name)
+ {
+ $value = $this->filter->sanitize($name, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('nav.name_too_short');
+ }
+
+ if ($length > 30) {
+ throw new BadRequestException('nav.name_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkPriority($priority)
+ {
+ $value = $this->filter->sanitize($priority, ['trim', 'int']);
+
+ if ($value < 1 || $value > 255) {
+ throw new BadRequestException('nav.invalid_priority');
+ }
+
+ return $value;
+ }
+
+ public function checkUrl($url)
+ {
+ $value = $this->filter->sanitize($url, ['trim']);
+
+ $stageA = Text::startsWith($value, '/');
+ $stageB = CommonValidator::url($value);
+
+ if (!$stageA && !$stageB) {
+ throw new BadRequestException('nav.invalid_url');
+ }
+
+ return $value;
+ }
+
+ public function checkTarget($target)
+ {
+ $value = $this->filter->sanitize($target, ['trim']);
+
+ $scopes = NavModel::targets();
+
+ if (!isset($scopes[$value])) {
+ throw new BadRequestException('nav.invalid_target');
+ }
+
+ return $value;
+ }
+
+ public function checkPosition($position)
+ {
+ $value = $this->filter->sanitize($position, ['trim']);
+
+ $scopes = NavModel::positions();
+
+ if (!isset($scopes[$value])) {
+ throw new BadRequestException('nav.invalid_position');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('nav.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Order.php b/app/Validators/Order.php
new file mode 100644
index 00000000..55ca0aca
--- /dev/null
+++ b/app/Validators/Order.php
@@ -0,0 +1,163 @@
+findById($id);
+
+ if (!$order) {
+ throw new NotFoundException('order.not_found');
+ }
+
+ return $order;
+ }
+
+ public function checkItemId($id)
+ {
+ $value = $this->filter->sanitize($id, ['trim', 'int']);
+
+ return $value;
+ }
+
+ public function checkItemType($type)
+ {
+ $scopes = [
+ OrderModel::ITEM_TYPE_COURSE,
+ OrderModel::ITEM_TYPE_PACKAGE,
+ OrderModel::ITEM_TYPE_REWARD,
+ ];
+
+ if (!in_array($type, $scopes)) {
+ throw new BadRequestException('order.invalid_item_type');
+ }
+
+ return $type;
+ }
+
+ public function checkAmount($amount)
+ {
+ $value = $this->filter->sanitize($amount, ['trim', 'float']);
+
+ if ($value < 0.01) {
+ throw new BadRequestException('order.invalid_pay_amount');
+ }
+
+ return $value;
+ }
+
+ public function checkStatus($status)
+ {
+ $scopes = [
+ OrderModel::STATUS_PENDING,
+ OrderModel::STATUS_FINISHED,
+ OrderModel::STATUS_CLOSED,
+ OrderModel::STATUS_REFUND,
+ ];
+
+ if (!in_array($status, $scopes)) {
+ throw new BadRequestException('order.invalid_status');
+ }
+
+ return $status;
+ }
+
+ public function checkPayChannel($channel)
+ {
+ $scopes = [
+ OrderModel::PAY_CHANNEL_ALIPAY,
+ OrderModel::PAY_CHANNEL_WXPAY,
+ ];
+
+ if (!in_array($channel, $scopes)) {
+ throw new BadRequestException('order.invalid_pay_channel');
+ }
+
+ return $channel;
+ }
+
+ public function checkDailyLimit($userId)
+ {
+ $orderRepo = new OrderRepo();
+
+ $count = $orderRepo->countUserTodayOrders($userId);
+
+ if ($count > 50) {
+ throw new BadRequestException('order.reach_daily_limit');
+ }
+ }
+
+ public function checkIfAllowPay($order)
+ {
+ if (time() - $order->created_at > 3600) {
+
+ if ($order->status == OrderModel::STATUS_PENDING) {
+
+ $order->status = OrderModel::STATUS_CLOSED;
+
+ $order->update();
+ }
+
+ throw new BadRequestException('order.trade_expired');
+ }
+
+ if ($order->status != OrderModel::STATUS_PENDING) {
+ throw new BadRequestException('order.invalid_status_action');
+ }
+ }
+
+ public function checkIfAllowCancel($order)
+ {
+ if ($order->status != OrderModel::STATUS_PENDING) {
+ throw new BadRequestException('order.invalid_status_action');
+ }
+ }
+
+ public function checkIfBoughtCourse($userId, $courseId)
+ {
+ $courseUserRepo = new CourseUserRepo();
+
+ $record = $courseUserRepo->find($userId, $courseId);
+
+ if ($record) {
+
+ $conditionA = $record->expire_time == 0;
+ $conditionB = $record->expire_time > time();
+
+ if ($conditionA || $conditionB) {
+ throw new BadRequestException('order.has_bought_course');
+ }
+ }
+ }
+
+ public function checkIfBoughtPackage($userId, $packageId)
+ {
+ $orderRepo = new OrderRepo();
+
+ $itemType = OrderModel::ITEM_TYPE_PACKAGE;
+
+ $order = $orderRepo->findSuccessUserOrder($userId, $packageId, $itemType);
+
+ if ($order) {
+ throw new BadRequestException('order.has_bought_package');
+ }
+ }
+
+}
diff --git a/app/Validators/Package.php b/app/Validators/Package.php
new file mode 100644
index 00000000..84e37c0e
--- /dev/null
+++ b/app/Validators/Package.php
@@ -0,0 +1,87 @@
+findById($id);
+
+ if (!$package) {
+ throw new NotFoundException('package.not_found');
+ }
+
+ return $package;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('package.title_too_short');
+ }
+
+ if ($length > 50) {
+ throw new BadRequestException('package.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkSummary($summary)
+ {
+ $value = $this->filter->sanitize($summary, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkMarketPrice($price)
+ {
+ $value = $this->filter->sanitize($price, ['trim', 'float']);
+
+ if ($value < 0.01 || $value > 10000) {
+ throw new BadRequestException('package.invalid_market_price');
+ }
+
+ return $value;
+ }
+
+ public function checkVipPrice($price)
+ {
+ $value = $this->filter->sanitize($price, ['trim', 'float']);
+
+ if ($value < 0.01 || $value > 10000) {
+ throw new BadRequestException('package.invalid_vip_price');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('package.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Page.php b/app/Validators/Page.php
new file mode 100644
index 00000000..cee177ca
--- /dev/null
+++ b/app/Validators/Page.php
@@ -0,0 +1,75 @@
+findById($id);
+
+ if (!$page) {
+ throw new NotFoundException('page.not_found');
+ }
+
+ return $page;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('page.title_too_short');
+ }
+
+ if ($length > 50) {
+ throw new BadRequestException('page.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkContent($content)
+ {
+ $value = $this->filter->sanitize($content, ['trim']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 10) {
+ throw new BadRequestException('page.content_too_short');
+ }
+
+ if ($length > 65535) {
+ throw new BadRequestException('page.content_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('page.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Refund.php b/app/Validators/Refund.php
new file mode 100644
index 00000000..97288793
--- /dev/null
+++ b/app/Validators/Refund.php
@@ -0,0 +1,56 @@
+findById($id);
+
+ if (!$trade) {
+ throw new NotFoundException('refund.not_found');
+ }
+
+ return $trade;
+ }
+
+ public function checkIfAllowReview($refund)
+ {
+ if ($refund->status != RefundModel::STATUS_PENDING) {
+ throw new BadRequestException('refund.review_not_allowed');
+ }
+ }
+
+ public function checkReviewStatus($status)
+ {
+ $scopes = [RefundModel::STATUS_APPROVED, RefundModel::STATUS_REFUSED];
+
+ if (!in_array($status, $scopes)) {
+ throw new BadRequestException('refund.invalid_review_status');
+ }
+
+ return $status;
+ }
+
+ public function checkReviewNote($note)
+ {
+ $value = $this->filter->sanitize($note, ['trim', 'string']);
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Review.php b/app/Validators/Review.php
new file mode 100644
index 00000000..8da6ceae
--- /dev/null
+++ b/app/Validators/Review.php
@@ -0,0 +1,83 @@
+findById($id);
+
+ if (!$review) {
+ throw new NotFoundException('review.not_found');
+ }
+
+ return $review;
+ }
+
+ public function checkCourseId($courseId)
+ {
+ $courseRepo = new CourseRepo();
+
+ $course = $courseRepo->findById($courseId);
+
+ if (!$course) {
+ throw new BadRequestException('review.course_not_found');
+ }
+
+ return $courseId;
+ }
+
+ public function checkContent($content)
+ {
+ $value = $this->filter->sanitize($content, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 5) {
+ throw new BadRequestException('review.content_too_short');
+ }
+
+ if ($length > 255) {
+ throw new BadRequestException('review.content_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkRating($rating)
+ {
+ $value = $this->filter->sanitize($rating, ['trim', 'int']);
+
+ if (!in_array($value, [1, 2, 3, 4, 5])) {
+ throw new BadRequestException('review.invalid_rating');
+ }
+
+ return $value;
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('review.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Role.php b/app/Validators/Role.php
new file mode 100644
index 00000000..f7d31261
--- /dev/null
+++ b/app/Validators/Role.php
@@ -0,0 +1,63 @@
+findById($id);
+
+ if (!$role) {
+ throw new NotFoundException('role.not_found');
+ }
+
+ return $role;
+ }
+
+ public function checkName($name)
+ {
+ $value = $this->filter->sanitize($name, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('role.name_too_short');
+ }
+
+ if ($length > 30) {
+ throw new BadRequestException('role.name_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkSummary($summary)
+ {
+ $value = $this->filter->sanitize($summary, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkRoutes($routes)
+ {
+ if (empty($routes)) {
+ throw new BadRequestException('role.routes_required');
+ }
+
+ return array_values($routes);
+ }
+
+}
diff --git a/app/Validators/Slide.php b/app/Validators/Slide.php
new file mode 100644
index 00000000..1649cc10
--- /dev/null
+++ b/app/Validators/Slide.php
@@ -0,0 +1,170 @@
+findById($id);
+
+ if (!$slide) {
+ throw new NotFoundException('slide.not_found');
+ }
+
+ return $slide;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('slide.title_too_short');
+ }
+
+ if ($length > 30) {
+ throw new BadRequestException('slide.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkSummary($summary)
+ {
+ $value = $this->filter->sanitize($summary, ['trim', 'string']);
+
+ return $value;
+ }
+
+ public function checkCover($cover)
+ {
+ $value = $this->filter->sanitize($cover, ['trim', 'string']);
+
+ if (!CommonValidator::url($value)) {
+ throw new BadRequestException('slide.invalid_cover');
+ }
+
+ $result = parse_url($value, PHP_URL_PATH);
+
+ return $result;
+ }
+
+ public function checkTarget($target)
+ {
+ $targets = array_keys(SlideModel::targets());
+
+ if (!in_array($target, $targets)) {
+ throw new BadRequestException('slide.invalid_target');
+ }
+
+ return $target;
+ }
+
+ public function checkPriority($priority)
+ {
+ $value = $this->filter->sanitize($priority, ['trim', 'int']);
+
+ if ($value < 1 || $value > 255) {
+ throw new BadRequestException('slide.invalid_priority');
+ }
+
+ return $value;
+ }
+
+ public function checkStartTime($startTime)
+ {
+ if (!CommonValidator::date($startTime, 'Y-m-d H:i:s')) {
+ throw new BadRequestException('slide.invalid_start_time');
+ }
+
+ return strtotime($startTime);
+ }
+
+ public function checkEndTime($endTime)
+ {
+ if (!CommonValidator::date($endTime, 'Y-m-d H:i:s')) {
+ throw new BadRequestException('slide.invalid_end_time');
+ }
+
+ return strtotime($endTime);
+ }
+
+ public function checkTimeRange($startTime, $endTime)
+ {
+ if (strtotime($startTime) >= strtotime($endTime)) {
+ throw new BadRequestException('slide.invalid_time_range');
+ }
+ }
+
+ public function checkPublishStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('slide.invalid_publish_status');
+ }
+
+ return $value;
+ }
+
+ public function checkCourse($courseId)
+ {
+ $course = CourseModel::findFirstById($courseId);
+
+ if (!$course || $course->deleted == 1) {
+ throw new BadRequestException('slide.course_not_found');
+ }
+
+ if ($course->published == 0) {
+ throw new BadRequestException('slide.course_not_published');
+ }
+
+ return $course;
+ }
+
+ public function checkPage($pageId)
+ {
+ $page = PageModel::findFirstById($pageId);
+
+ if (!$page || $page->deleted == 1) {
+ throw new BadRequestException('slide.page_not_found');
+ }
+
+ if ($page->published == 0) {
+ throw new BadRequestException('slide.page_not_published');
+ }
+
+ return $page;
+ }
+
+ public function checkLink($link)
+ {
+ $value = $this->filter->sanitize($link, ['trim', 'string']);
+
+ if (!CommonValidator::url($value)) {
+ throw new BadRequestException('slide.invalid_link');
+ }
+
+ return $value;
+ }
+
+}
diff --git a/app/Validators/Trade.php b/app/Validators/Trade.php
new file mode 100644
index 00000000..16806377
--- /dev/null
+++ b/app/Validators/Trade.php
@@ -0,0 +1,56 @@
+findById($id);
+
+ if (!$trade) {
+ throw new NotFoundException('trade.not_found');
+ }
+
+ return $trade;
+ }
+
+ public function checkIfAllowClose($trade)
+ {
+ if ($trade->status != TradeModel::STATUS_PENDING) {
+ throw new BadRequestException('trade.close_not_allowed');
+ }
+ }
+
+ public function checkIfAllowRefund($trade)
+ {
+ if ($trade->status != TradeModel::STATUS_FINISHED) {
+ throw new BadRequestException('trade.refund_not_allowed');
+ }
+
+ $tradeRepo = new TradeRepo();
+
+ $refund = $tradeRepo->findLatestRefund($trade->sn);
+
+ $scopes = [RefundModel::STATUS_PENDING, RefundModel::STATUS_APPROVED];
+
+ if ($refund && in_array($refund->status, $scopes)) {
+ throw new BadRequestException('trade.refund_existed');
+ }
+ }
+
+}
diff --git a/app/Validators/User.php b/app/Validators/User.php
new file mode 100644
index 00000000..cd492f36
--- /dev/null
+++ b/app/Validators/User.php
@@ -0,0 +1,273 @@
+findById($id);
+
+ if (!$user) {
+ throw new NotFoundException('user.not_found');
+ }
+
+ return $user;
+ }
+
+ public function checkPhone($phone)
+ {
+ $value = $this->filter->sanitize($phone, ['trim', 'string']);
+
+ if (!CommonValidator::phone($value)) {
+ throw new BadRequestException('account.invalid_phone');
+ }
+
+ return $value;
+ }
+
+ public function checkEmail($email)
+ {
+ $value = $this->filter->sanitize($email, ['trim', 'string']);
+
+ if (!CommonValidator::email($value)) {
+ throw new BadRequestException('account.invalid_email');
+ }
+
+ return $value;
+ }
+
+ public function checkPassword($password)
+ {
+ $value = $this->filter->sanitize($password, ['trim', 'string']);
+
+ if (!CommonValidator::password($value)) {
+ throw new BadRequestException('account.invalid_password');
+ }
+
+ return $value;
+ }
+
+ public function checkName($name)
+ {
+ $value = $this->filter->sanitize($name, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length < 2) {
+ throw new BadRequestException('user.name_too_short');
+ }
+
+ if ($length > 15) {
+ throw new BadRequestException('user.name_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkTitle($title)
+ {
+ $value = $this->filter->sanitize($title, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length > 30) {
+ throw new BadRequestException('role.title_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkAbout($about)
+ {
+ $value = $this->filter->sanitize($about, ['trim', 'string']);
+
+ $length = kg_strlen($value);
+
+ if ($length > 255) {
+ throw new BadRequestException('user.about_too_long');
+ }
+
+ return $value;
+ }
+
+ public function checkEduRole($role)
+ {
+ $value = $this->filter->sanitize($role, ['trim', 'int']);
+
+ $roleIds = [UserModel::EDU_ROLE_STUDENT, UserModel::EDU_ROLE_TEACHER];
+
+ if (!in_array($value, $roleIds)) {
+ throw new BadRequestException('user.invalid_edu_role');
+ }
+
+ return $value;
+ }
+
+ public function checkAdminRole($role)
+ {
+ $value = $this->filter->sanitize($role, ['trim', 'int']);
+
+ if (!$value) return 0;
+
+ $roleRepo = new RoleRepo();
+
+ $role = $roleRepo->findById($value);
+
+ if (!$role || $role->deleted == 1) {
+ throw new BadRequestException('user.invalid_admin_role');
+ }
+
+ return $role->id;
+ }
+
+ public function checkLockStatus($status)
+ {
+ $value = $this->filter->sanitize($status, ['trim', 'int']);
+
+ if (!in_array($value, [0, 1])) {
+ throw new BadRequestException('user.invalid_lock_status');
+ }
+
+ return $value;
+ }
+
+ public function checkLockExpiry($expiry)
+ {
+ if (!CommonValidator::date($expiry, 'Y-m-d H:i:s')) {
+ throw new BadRequestException('user.invalid_locked_expiry');
+ }
+
+ return strtotime($expiry);
+ }
+
+ public function checkIfPhoneTaken($phone)
+ {
+ $accountRepo = new AccountRepo();
+
+ $account = $accountRepo->findByPhone($phone);
+
+ if ($account) {
+ throw new BadRequestException('account.phone_taken');
+ }
+ }
+
+ public function checkIfEmailTaken($email)
+ {
+ $accountRepo = new AccountRepo();
+
+ $account = $accountRepo->findByEmail($email);
+
+ if ($account) {
+ throw new BadRequestException('account.email_taken');
+ }
+ }
+
+ public function checkIfNameTaken($name)
+ {
+ $userRepo = new UserRepo();
+
+ $user = $userRepo->findByName($name);
+
+ if ($user) {
+ throw new BadRequestException('user.name_taken');
+ }
+ }
+
+ public function checkVerifyCode($key, $code)
+ {
+ if (!VerificationUtil::checkCode($key, $code)) {
+ throw new BadRequestException('user.invalid_verify_code');
+ }
+ }
+
+ public function checkCaptchaCode($ticket, $rand)
+ {
+ $captchaService = new CaptchaService();
+
+ $result = $captchaService->verify($ticket, $rand);
+
+ if (!$result) {
+ throw new BadRequestException('user.invalid_captcha_code');
+ }
+ }
+
+ public function checkOriginPassword($user, $password)
+ {
+ $hash = PasswordUtil::hash($password, $user->salt);
+
+ if ($hash != $user->password) {
+ throw new BadRequestException('user.origin_password_incorrect');
+ }
+ }
+
+ public function checkConfirmPassword($newPassword, $confirmPassword)
+ {
+ if ($newPassword != $confirmPassword) {
+ throw new BadRequestException('user.confirm_password_incorrect');
+ }
+ }
+
+ public function checkAdminLogin($user)
+ {
+ if ($user->admin_role == 0) {
+ throw new ForbiddenException('user.admin_not_authorized');
+ }
+ }
+
+ public function checkLoginAccount($account)
+ {
+ $userRepo = new UserRepo();
+
+ $user = $userRepo->findByAccount($account);
+
+ if (!$user) {
+ throw new BadRequestException('user.login_account_incorrect');
+ }
+
+ return $user;
+ }
+
+ public function checkLoginPassword($user, $password)
+ {
+ $hash = PasswordUtil::hash($password, $user->salt);
+
+ if ($hash != $user->password) {
+ throw new BadRequestException('user.login_password_incorrect');
+ }
+
+ if ($user->locked == 1) {
+ throw new ForbiddenException('user.login_locked');
+ }
+
+ }
+
+ public function checkIfCanEditUser($user)
+ {
+ $auth = $this->getDI()->get('auth');
+
+ $authUser = $auth->getAuthUser();
+
+ if ($authUser->id) {
+ }
+ }
+
+}
diff --git a/app/Validators/Validator.php b/app/Validators/Validator.php
new file mode 100644
index 00000000..4cdc58e0
--- /dev/null
+++ b/app/Validators/Validator.php
@@ -0,0 +1,37 @@
+logger = $this->getLogger();
+
+ set_error_handler([$this, 'handleError']);
+
+ set_exception_handler([$this, 'handleException']);
+ }
+
+ public function handleError($no, $str, $file, $line)
+ {
+ $content = compact('no', 'str', 'file', 'line');
+
+ $this->logger->error('Console Error ' . kg_json_encode($content));
+ }
+
+ public function handleException($e)
+ {
+ $content = [
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'message' => $e->getMessage(),
+ ];
+
+ $this->logger->error('Console Exception ' . kg_json_encode($content));
+ }
+
+ protected function getLogger()
+ {
+ $logger = new AppLogger();
+
+ return $logger->getInstance('console');
+ }
+
+}
diff --git a/bootstrap/ConsoleKernel.php b/bootstrap/ConsoleKernel.php
new file mode 100644
index 00000000..892ae050
--- /dev/null
+++ b/bootstrap/ConsoleKernel.php
@@ -0,0 +1,92 @@
+di = new \Phalcon\Di\FactoryDefault\Cli();
+ $this->app = new \Phalcon\Cli\Console();
+ $this->loader = new \Phalcon\Loader();
+
+ $this->initAppEnv();
+ $this->initAppConfigs();
+ $this->initAppSettings();
+ $this->registerLoaders();
+ $this->registerServices();
+ $this->registerErrorHandler();
+ }
+
+ public function handle()
+ {
+ $this->app->setDI($this->di);
+
+ $options = getopt('', ['task:', 'action:']);
+
+ if (!empty($options['task']) && !empty($options['action'])) {
+
+ $this->app->handle($options);
+
+ } else {
+
+ $options = [];
+
+ foreach ($_SERVER['argv'] as $k => $arg) {
+ if ($k == 1) {
+ $options['task'] = $arg;
+ } elseif ($k == 2) {
+ $options['action'] = $arg;
+ } elseif ($k >= 3) {
+ $options['params'][] = $arg;
+ }
+ }
+
+ $this->app->handle($options);
+
+ echo PHP_EOL . PHP_EOL;
+ }
+ }
+
+ protected function registerLoaders()
+ {
+ $this->loader->registerNamespaces([
+ 'App' => app_path(),
+ 'Bootstrap' => bootstrap_path(),
+ ]);
+
+ $this->loader->registerFiles([
+ vendor_path('autoload.php'),
+ app_path('Library/Helper.php'),
+ ]);
+
+ $this->loader->register();
+ }
+
+ protected function registerServices()
+ {
+ $providers = [
+ \App\Providers\Cache::class,
+ \App\Providers\Config::class,
+ \App\Providers\Crypt::class,
+ \App\Providers\Database::class,
+ \App\Providers\EventsManager::class,
+ \App\Providers\Logger::class,
+ \App\Providers\MetaData::class,
+ \App\Providers\Redis::class,
+ \App\Providers\CliDispatcher::class,
+ ];
+
+ foreach ($providers as $provider) {
+ $service = new $provider($this->di);
+ $service->register();
+ }
+ }
+
+ protected function registerErrorHandler()
+ {
+ return new ConsoleErrorHandler();
+ }
+
+}
\ No newline at end of file
diff --git a/bootstrap/Helper.php b/bootstrap/Helper.php
new file mode 100644
index 00000000..6fca6bf2
--- /dev/null
+++ b/bootstrap/Helper.php
@@ -0,0 +1,139 @@
+logger = $this->getDI()->get('logger');
+
+ set_error_handler([$this, 'handleError']);
+
+ set_exception_handler([$this, 'handleException']);
+ }
+
+ public function handleError($no, $str, $file, $line)
+ {
+ $content = compact('no', 'str', 'file', 'line');
+
+ $error = json_encode($content);
+
+ $this->logger->log($error);
+ }
+
+ public function handleException($e)
+ {
+ $this->setStatusCode($e);
+
+ if ($this->router->getModuleName() == 'api') {
+ $this->apiError($e);
+ } else if ($this->isAjax()) {
+ $this->ajaxError($e);
+ } else {
+ $this->pageError($e);
+ }
+ }
+
+ protected function setStatusCode($e)
+ {
+ if ($e instanceof BadRequestException) {
+ $this->response->setStatusCode(400);
+ } else if ($e instanceof UnauthorizedException) {
+ $this->response->setStatusCode(401);
+ } else if ($e instanceof ForbiddenException) {
+ $this->response->setStatusCode(403);
+ } else if ($e instanceof NotFoundException) {
+ $this->response->setStatusCode(404);
+ } else {
+ $this->response->setStatusCode(500);
+ $this->report($e);
+ }
+ }
+
+ protected function report($e)
+ {
+ $content = sprintf('%s(%d): %s', $e->getFile(), $e->getLine(), $e->getMessage());
+
+ $this->logger->error($content);
+ }
+
+ protected function apiError($e)
+ {
+ $content = $this->translate($e->getMessage());
+
+ $this->response->setJsonContent($content);
+ $this->response->send();
+ }
+
+ protected function ajaxError($e)
+ {
+ $content = $this->translate($e->getMessage());
+
+ $this->response->setJsonContent($content);
+ $this->response->send();
+ }
+
+ protected function pageError($e)
+ {
+ $content = $this->translate($e->getMessage());
+
+ $this->flash->error($content);
+
+ $this->response->redirect([
+ 'for' => 'error.' . $this->response->getStatusCode()
+ ])->send();
+ }
+
+ protected function translate($code)
+ {
+ $errors = require config_path() . '/errors.php';
+
+ $content = [
+ 'code' => $code,
+ 'msg' => $errors[$code] ?? $code,
+ ];
+
+ return $content;
+ }
+
+ protected function isAjax()
+ {
+ if ($this->request->isAjax()) {
+ return true;
+ }
+
+ $contentType = $this->request->getContentType();
+
+ if (Text::startsWith($contentType, 'application/json')) {
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/bootstrap/HttpKernel.php b/bootstrap/HttpKernel.php
new file mode 100644
index 00000000..54164a0e
--- /dev/null
+++ b/bootstrap/HttpKernel.php
@@ -0,0 +1,95 @@
+di = new \Phalcon\Di\FactoryDefault();
+ $this->app = new \Phalcon\Mvc\Application();
+ $this->loader = new \Phalcon\Loader();
+
+ $this->initAppEnv();
+ $this->initAppConfigs();
+ $this->initAppSettings();
+ $this->registerLoaders();
+ $this->registerServices();
+ $this->registerModules();
+ $this->registerErrorHandler();
+ }
+
+ public function handle()
+ {
+ $this->app->setDI($this->di);
+ $this->app->handle()->send();
+ }
+
+ protected function registerLoaders()
+ {
+ $this->loader->registerNamespaces([
+ 'App' => app_path(),
+ 'Bootstrap' => bootstrap_path(),
+ ]);
+
+ $this->loader->registerFiles([
+ vendor_path('autoload.php'),
+ app_path('Library/Helper.php'),
+ ]);
+
+ $this->loader->register();
+ }
+
+ protected function registerServices()
+ {
+ $providers = [
+ \App\Providers\Annotation::class,
+ \App\Providers\Cache::class,
+ \App\Providers\Cookie::class,
+ \App\Providers\Config::class,
+ \App\Providers\Crypt::class,
+ \App\Providers\Database::class,
+ \App\Providers\EventsManager::class,
+ \App\Providers\Logger::class,
+ \App\Providers\MetaData::class,
+ \App\Providers\Redis::class,
+ \App\Providers\Router::class,
+ \App\Providers\Security::class,
+ \App\Providers\Session::class,
+ \App\Providers\Url::class,
+ \App\Providers\Volt::class,
+ ];
+
+ foreach ($providers as $provider) {
+ $service = new $provider($this->di);
+ $service->register();
+ }
+ }
+
+ protected function registerModules()
+ {
+ $modules = [
+ 'api' => [
+ 'className' => 'App\Http\Api\Module',
+ 'path' => app_path('Http/Api/Module.php'),
+ ],
+ 'admin' => [
+ 'className' => 'App\Http\Admin\Module',
+ 'path' => app_path('Http/Admin/Module.php'),
+ ],
+ 'home' => [
+ 'className' => 'App\Http\Home\Module',
+ 'path' => app_path('Http/Home/Module.php'),
+ ],
+ ];
+
+ $this->app->registerModules($modules);
+ }
+
+ protected function registerErrorHandler()
+ {
+ return new HttpErrorHandler();
+ }
+
+}
\ No newline at end of file
diff --git a/bootstrap/Kernel.php b/bootstrap/Kernel.php
new file mode 100644
index 00000000..ca52573a
--- /dev/null
+++ b/bootstrap/Kernel.php
@@ -0,0 +1,57 @@
+app;
+ }
+
+ public function getDI()
+ {
+ return $this->di;
+ }
+
+ protected function initAppEnv()
+ {
+ require __DIR__ . '/Helper.php';
+ }
+
+ protected function initAppConfigs()
+ {
+ $this->configs = require config_path() . '/config.php';
+ }
+
+ protected function initAppSettings()
+ {
+ ini_set('date.timezone', $this->configs['timezone']);
+
+ if ($this->configs['env'] == ENV_DEV) {
+ ini_set('display_errors', 1);
+ error_reporting(E_ALL);
+ } else {
+ ini_set('display_errors', 0);
+ error_reporting(0);
+ }
+ }
+
+ abstract public function handle();
+
+ abstract protected function registerLoaders();
+
+ abstract protected function registerServices();
+
+ abstract protected function registerErrorHandler();
+
+}
\ No newline at end of file
diff --git a/common b/common
new file mode 100644
index 00000000..bcbc138c
--- /dev/null
+++ b/common
@@ -0,0 +1 @@
+[Sun, 17 Nov 19 16:45:15 +0800][ERROR] fuck you off
diff --git a/composer.json b/composer.json
new file mode 100644
index 00000000..33da8f8b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,24 @@
+{
+ "require": {
+ "phalcon/incubator": "^3.4",
+ "guzzlehttp/guzzle": "^6.3",
+ "swiftmailer/swiftmailer": "^6.0",
+ "peppeocchi/php-cron-scheduler": "^2.4",
+ "yansongda/pay": "^2.8",
+ "xiaochong0302/ip2region": "dev-master",
+ "tencentcloud/tencentcloud-sdk-php": "3.*",
+ "qcloudsms/qcloudsms_php": "0.1.*",
+ "qcloud/cos-sdk-v5": "2.*",
+ "workerman/gateway-worker": "^3.0.12",
+ "whichbrowser/parser": "^2.0",
+ "hightman/xunsearch": "^1.4.14"
+ },
+ "require-dev": {
+ },
+ "repositories": {
+ "packagist": {
+ "type": "composer",
+ "url": "https://mirrors.aliyun.com/composer"
+ }
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 00000000..a042edc2
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2021 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "0f1bd4f098897d13a5f8452989407860",
+ "packages": [
+ {
+ "name": "doctrine/lexer",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/lexer.git",
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "phpstan/phpstan": "^0.11.8",
+ "phpunit/phpunit": "^8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "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",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "lexer",
+ "parser",
+ "php"
+ ],
+ "time": "2019-07-30T19:33:28+00:00"
+ },
+ {
+ "name": "egulias/email-validator",
+ "version": "2.1.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/egulias/EmailValidator.git",
+ "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
+ },
+ "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": {
+ "doctrine/lexer": "^1.0.1",
+ "php": ">= 5.5"
+ },
+ "require-dev": {
+ "dominicsayers/isemail": "dev-master",
+ "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
+ "satooshi/php-coveralls": "^1.0.1",
+ "symfony/phpunit-bridge": "^4.4@dev"
+ },
+ "suggest": {
+ "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Egulias\\EmailValidator\\": "EmailValidator"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eduardo Gulias Davis"
+ }
+ ],
+ "description": "A library for validating emails against several RFCs",
+ "homepage": "https://github.com/egulias/EmailValidator",
+ "keywords": [
+ "email",
+ "emailvalidation",
+ "emailvalidator",
+ "validation",
+ "validator"
+ ],
+ "time": "2019-08-13T17:33:27+00:00"
+ },
+ {
+ "name": "guzzlehttp/command",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/command.git",
+ "reference": "2aaa2521a8f8269d6f5dfc13fe2af12c76921034"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/command/zipball/2aaa2521a8f8269d6f5dfc13fe2af12c76921034",
+ "reference": "2aaa2521a8f8269d6f5dfc13fe2af12c76921034",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.2",
+ "guzzlehttp/promises": "~1.3",
+ "guzzlehttp/psr7": "~1.0",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0|~5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Command\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ }
+ ],
+ "description": "Provides the foundation for building command-based web service clients",
+ "time": "2016-11-24T13:34:15+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "6.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/promises": "^1.0",
+ "guzzlehttp/psr7": "^1.4",
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+ "psr/log": "^1.0"
+ },
+ "suggest": {
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.3-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "rest",
+ "web service"
+ ],
+ "time": "2018-04-22T15:46:56+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle-services",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle-services.git",
+ "reference": "9e3abf20161cbf662d616cbb995f2811771759f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle-services/zipball/9e3abf20161cbf662d616cbb995f2811771759f7",
+ "reference": "9e3abf20161cbf662d616cbb995f2811771759f7",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/command": "~1.0",
+ "guzzlehttp/guzzle": "^6.2",
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "suggest": {
+ "gimler/guzzle-description-loader": "^0.0.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Command\\Guzzle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "Stefano Kowalke",
+ "email": "blueduck@mail.org",
+ "homepage": "https://github.com/konafets"
+ }
+ ],
+ "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.",
+ "time": "2017-10-06T14:32:02+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "v1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "time": "2016-12-20T10:07:11+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "1.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "psr/http-message": "~1.0",
+ "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-zlib": "*",
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+ },
+ "suggest": {
+ "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Schultze",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "time": "2019-07-01T23:21:34+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/68545165e19249013afd1d6f7485aecff07a2d22",
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.2",
+ "psr/log": "^1.0.1"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "elasticsearch/elasticsearch": "^6.0",
+ "graylog2/gelf-php": "^1.4.2",
+ "jakub-onderka/php-parallel-lint": "^0.9",
+ "php-amqplib/php-amqplib": "~2.4",
+ "php-console/php-console": "^3.1.3",
+ "phpspec/prophecy": "^1.6.1",
+ "phpunit/phpunit": "^8.3",
+ "predis/predis": "^1.1",
+ "rollbar/rollbar": "^1.3",
+ "ruflin/elastica": ">=0.90 <3.0",
+ "swiftmailer/swiftmailer": "^5.3|^6.0"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "php-console/php-console": "Allow sending log messages to Google Chrome",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "http://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "time": "2019-08-30T09:56:44+00:00"
+ },
+ {
+ "name": "mtdowling/cron-expression",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mtdowling/cron-expression.git",
+ "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
+ "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0|~5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "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",
+ "schedule"
+ ],
+ "time": "2017-01-23T04:29:33+00:00"
+ },
+ {
+ "name": "peppeocchi/php-cron-scheduler",
+ "version": "v2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/peppeocchi/php-cron-scheduler.git",
+ "reference": "1b18892fdd4f9c913107fda1544ac402eb7ec6e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/peppeocchi/php-cron-scheduler/zipball/1b18892fdd4f9c913107fda1544ac402eb7ec6e5",
+ "reference": "1b18892fdd4f9c913107fda1544ac402eb7ec6e5",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "mtdowling/cron-expression": "~1.0",
+ "php": ">=5.5.9"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5.7",
+ "satooshi/php-coveralls": "^1.0",
+ "swiftmailer/swiftmailer": "~5.4 || ^6.0"
+ },
+ "suggest": {
+ "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GO\\": "src/GO/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Giuseppe Occhipinti",
+ "email": "peppeocchi@gmail.com"
+ },
+ {
+ "name": "Carsten Windler",
+ "email": "carsten@carstenwindler.de",
+ "homepage": "http://carstenwindler.de",
+ "role": "Contributor"
+ }
+ ],
+ "description": "PHP Cron Job Scheduler",
+ "keywords": [
+ "cron job",
+ "scheduler"
+ ],
+ "time": "2018-10-25T21:33:38+00:00"
+ },
+ {
+ "name": "phalcon/incubator",
+ "version": "v3.4.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phalcon/incubator.git",
+ "reference": "4883d9009a9d651308bfc201a0e9440c0ff692e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phalcon/incubator/zipball/4883d9009a9d651308bfc201a0e9440c0ff692e2",
+ "reference": "4883d9009a9d651308bfc201a0e9440c0ff692e2",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-phalcon": "^3.3",
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "codeception/aerospike-module": "^1.0",
+ "codeception/codeception": "^2.5",
+ "codeception/mockery-module": "0.2.2",
+ "codeception/specify": "^0.4",
+ "codeception/verify": "^0.3",
+ "doctrine/instantiator": "1.0.5",
+ "phalcon/dd": "^1.1",
+ "phpdocumentor/reflection-docblock": "2.0.4",
+ "phpunit/phpunit": "^4.8",
+ "squizlabs/php_codesniffer": "^2.9",
+ "vlucas/phpdotenv": "^2.4"
+ },
+ "suggest": {
+ "duncan3dc/fork-helper": "To use extended class to access the beanstalk queue service",
+ "ext-aerospike": "*",
+ "phalcon/ide-stubs": "Phalcon IDE Stubs",
+ "sergeyklay/aerospike-php-stubs": "The most complete Aerospike PHP stubs which allows autocomplete in modern IDEs",
+ "swiftmailer/swiftmailer": "~5.2",
+ "twig/twig": "~1.35|~2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phalcon\\": "Library/Phalcon/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "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"
+ }
+ ],
+ "description": "Adapters, prototypes or functionality that can be potentially incorporated to the C-framework.",
+ "homepage": "https://phalconphp.com",
+ "keywords": [
+ "framework",
+ "incubator",
+ "phalcon"
+ ],
+ "time": "2019-09-16T13:54:24+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+ },
+ "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": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "time": "2016-08-06T20:24:11+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2018-11-20T15:27:04+00:00"
+ },
+ {
+ "name": "qcloud/cos-sdk-v5",
+ "version": "v2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tencentyun/cos-php-sdk-v5.git",
+ "reference": "a8ac2dc1f58ddb36e5d702d19f9d7cb8d6f1dc5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tencentyun/cos-php-sdk-v5/zipball/a8ac2dc1f58ddb36e5d702d19f9d7cb8d6f1dc5e",
+ "reference": "a8ac2dc1f58ddb36e5d702d19f9d7cb8d6f1dc5e",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/guzzle": "~6.3",
+ "guzzlehttp/guzzle-services": "~1.1",
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Qcloud\\Cos\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "yaozongyou",
+ "email": "yaozongyou@vip.qq.com"
+ },
+ {
+ "name": "lewzylu",
+ "email": "327874225@qq.com"
+ }
+ ],
+ "description": "PHP SDK for QCloud COS",
+ "keywords": [
+ "cos",
+ "php",
+ "qcloud"
+ ],
+ "time": "2019-09-25T12:28:54+00:00"
+ },
+ {
+ "name": "qcloudsms/qcloudsms_php",
+ "version": "v0.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/qcloudsms/qcloudsms_php.git",
+ "reference": "48822045772d343b93c3d505d8a187cd51153c5a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/qcloudsms/qcloudsms_php/zipball/48822045772d343b93c3d505d8a187cd51153c5a",
+ "reference": "48822045772d343b93c3d505d8a187cd51153c5a",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require-dev": {
+ "sami/sami": "dev-master"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Qcloud\\Sms\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "qcloud sms php sdk",
+ "keywords": [
+ "php",
+ "qcloud",
+ "sdk",
+ "sms"
+ ],
+ "time": "2018-09-19T07:19:17+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "swiftmailer/swiftmailer",
+ "version": "v6.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/swiftmailer/swiftmailer.git",
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "egulias/email-validator": "~2.0",
+ "php": ">=7.0.0",
+ "symfony/polyfill-iconv": "^1.0",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "~0.9.1",
+ "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
+ },
+ "suggest": {
+ "ext-intl": "Needed to support internationalized email addresses",
+ "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.2-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "lib/swift_required.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Chris Corbyn"
+ },
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ }
+ ],
+ "description": "Swiftmailer, free feature-rich PHP mailer",
+ "homepage": "https://swiftmailer.symfony.com",
+ "keywords": [
+ "email",
+ "mail",
+ "mailer"
+ ],
+ "time": "2019-04-21T09:21:45+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "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",
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/http-foundation": "^3.4|^4.0",
+ "symfony/service-contracts": "^1.1",
+ "symfony/stopwatch": "~3.4|~4.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony EventDispatcher Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "psr/event-dispatcher": "",
+ "symfony/event-dispatcher-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "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": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-06-20T06:46:26+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/mime": "^4.3",
+ "symfony/polyfill-mbstring": "~1.1"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0",
+ "symfony/expression-language": "~3.4|~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony HttpFoundation Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/mime",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mime.git",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/987a05df1c6ac259b34008b932551353f4f408df",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10",
+ "symfony/dependency-injection": "~3.4|^4.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mime\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A library to manipulate MIME messages",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mime",
+ "mime-type"
+ ],
+ "time": "2019-08-22T08:16:11+00:00"
+ },
+ {
+ "name": "symfony/polyfill-iconv",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-iconv.git",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-iconv": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Iconv\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Iconv extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "iconv",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php72": "^1.9"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php72",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php72\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "tencentcloud/tencentcloud-sdk-php",
+ "version": "3.0.92",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/TencentCloud/tencentcloud-sdk-php.git",
+ "reference": "71164956c234368c65c00e321e96f6dbd0f8d9c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/TencentCloud/tencentcloud-sdk-php/zipball/71164956c234368c65c00e321e96f6dbd0f8d9c0",
+ "reference": "71164956c234368c65c00e321e96f6dbd0f8d9c0",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.3",
+ "guzzlehttp/psr7": "^1.4",
+ "php": ">=5.6.33"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/QcloudApi/QcloudApi.php"
+ ],
+ "psr-4": {
+ "TencentCloud\\": "./src/TencentCloud"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "coolli",
+ "email": "tencentcloudapi@tencent.com",
+ "homepage": "https://cloud.tencent.com/document/sdk/PHP",
+ "role": "Developer"
+ }
+ ],
+ "description": "TencentCloudApi php sdk",
+ "homepage": "https://github.com/TencentCloud/tencentcloud-sdk-php",
+ "time": "2019-09-27T09:23:30+00:00"
+ },
+ {
+ "name": "whichbrowser/parser",
+ "version": "v2.0.37",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WhichBrowser/Parser-PHP.git",
+ "reference": "9c6ad8eadc23294b1c66d92876c11f13c5d4cf48"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WhichBrowser/Parser-PHP/zipball/9c6ad8eadc23294b1c66d92876c11f13c5d4cf48",
+ "reference": "9c6ad8eadc23294b1c66d92876c11f13c5d4cf48",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "psr/cache": "^1.0"
+ },
+ "require-dev": {
+ "icomefromthenet/reverse-regex": "0.0.6.3",
+ "phpunit/php-code-coverage": "^2.2|^3.0",
+ "phpunit/phpunit": "^4.0|^5.0",
+ "satooshi/php-coveralls": "^1.0",
+ "squizlabs/php_codesniffer": "2.5.*",
+ "symfony/yaml": ">=2.8"
+ },
+ "suggest": {
+ "cache/array-adapter": "Allows testing of the caching functionality"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "WhichBrowser\\": [
+ "src/",
+ "tests/src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Niels Leenheer",
+ "email": "niels@leenheer.nl",
+ "role": "Developer"
+ }
+ ],
+ "description": "Useragent sniffing library for PHP",
+ "homepage": "http://whichbrowser.net",
+ "keywords": [
+ "browser",
+ "sniffing",
+ "ua",
+ "useragent"
+ ],
+ "time": "2018-10-02T09:26:41+00:00"
+ },
+ {
+ "name": "workerman/gateway-worker",
+ "version": "v3.0.13",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/GatewayWorker.git",
+ "reference": "38b44c95f21cd340b5a9cff3987ddb2abb9a2a38"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/38b44c95f21cd340b5a9cff3987ddb2abb9a2a38",
+ "reference": "38b44c95f21cd340b5a9cff3987ddb2abb9a2a38",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "workerman/workerman": ">=3.1.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GatewayWorker\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "homepage": "http://www.workerman.net",
+ "keywords": [
+ "communication",
+ "distributed"
+ ],
+ "time": "2019-07-02T11:55:24+00:00"
+ },
+ {
+ "name": "workerman/workerman",
+ "version": "v3.5.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/Workerman.git",
+ "reference": "488f108f9e446f31bac4d689bb9f9fe3705862cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/Workerman/zipball/488f108f9e446f31bac4d689bb9f9fe3705862cf",
+ "reference": "488f108f9e446f31bac4d689bb9f9fe3705862cf",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "suggest": {
+ "ext-event": "For better performance. "
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Workerman\\": "./"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "walkor",
+ "email": "walkor@workerman.net",
+ "homepage": "http://www.workerman.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
+ "homepage": "http://www.workerman.net",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "time": "2019-09-06T03:42:47+00:00"
+ },
+ {
+ "name": "xiaochong0302/ip2region",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/xiaochong0302/ip2region.git",
+ "reference": "3cb3c50fa9e2c49115e40252f6b651437712a4e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/xiaochong0302/ip2region/zipball/3cb3c50fa9e2c49115e40252f6b651437712a4e9",
+ "reference": "3cb3c50fa9e2c49115e40252f6b651437712a4e9",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Koogua\\Ip2Region\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "xiaochong0302",
+ "email": "xiaochong0302@gmail.com"
+ }
+ ],
+ "description": "ip2region扩展包",
+ "keywords": [
+ "Ip2Region"
+ ],
+ "time": "2019-08-18T14:57:02+00:00"
+ },
+ {
+ "name": "yansongda/pay",
+ "version": "v2.8.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yansongda/pay.git",
+ "reference": "841999b65f97466ed1b405c52400c0c73aeaa3b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yansongda/pay/zipball/841999b65f97466ed1b405c52400c0c73aeaa3b5",
+ "reference": "841999b65f97466ed1b405c52400c0c73aeaa3b5",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-openssl": "*",
+ "ext-simplexml": "*",
+ "php": ">=7.1.3",
+ "symfony/event-dispatcher": "^4.0",
+ "symfony/http-foundation": "^4.0",
+ "yansongda/supports": "^2.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.2",
+ "phpunit/phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Yansongda\\Pay\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "yansongda",
+ "email": "me@yansongda.cn"
+ }
+ ],
+ "description": "专注 Alipay 和 WeChat 的支付扩展包",
+ "keywords": [
+ "alipay",
+ "pay",
+ "wechat"
+ ],
+ "time": "2019-09-21T15:05:57+00:00"
+ },
+ {
+ "name": "yansongda/supports",
+ "version": "v2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yansongda/supports.git",
+ "reference": "d4742562cf0453d127dc064334ad6dc3f86d247d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yansongda/supports/zipball/d4742562cf0453d127dc064334ad6dc3f86d247d",
+ "reference": "d4742562cf0453d127dc064334ad6dc3f86d247d",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.2",
+ "monolog/monolog": "^1.23 || ^2.0",
+ "php": ">=7.1.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.15",
+ "phpunit/phpunit": "^7.5",
+ "predis/predis": "^1.1"
+ },
+ "suggest": {
+ "predis/predis": "Allows to use throttle feature"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Yansongda\\Supports\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "yansongda",
+ "email": "me@yansongda.cn"
+ }
+ ],
+ "description": "common components",
+ "keywords": [
+ "Guzzle",
+ "array",
+ "collection",
+ "config",
+ "http",
+ "support",
+ "throttle"
+ ],
+ "time": "2019-09-21T14:56:18+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "xiaochong0302/ip2region": 20
+ },
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/config/config.default.php b/config/config.default.php
new file mode 100644
index 00000000..7daab3cb
--- /dev/null
+++ b/config/config.default.php
@@ -0,0 +1,45 @@
+ '/', // 必须以"/"结尾
+ 'static' => '/static/', // 必须以"/"结尾
+];
+
+$config['db'] = [
+ 'adapter' => 'Mysql',
+ 'host' => 'localhost',
+ 'username' => '',
+ 'password' => '',
+ 'dbname' => '',
+ 'charset' => 'utf8',
+];
+
+$config['redis'] = [
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'persistent' => false,
+ 'auth' => '',
+ 'index' => 0,
+ 'lifetime' => 86400,
+];
+
+$config['session'] = [
+ 'lifetime' => 7200,
+];
+
+$config['log'] = [
+ 'level' => Phalcon\Logger::INFO,
+];
+
+return $config;
diff --git a/config/crossdomain.xml b/config/crossdomain.xml
new file mode 100644
index 00000000..a4251da5
--- /dev/null
+++ b/config/crossdomain.xml
@@ -0,0 +1,6 @@
+
+
+t |