Browse Source

Merge branch 'master' of http://git.derkj.com:9095/waibao/NN2025081602

# Conflicts:
#	app/Models/VideoCoursesModel.php
罗永浩 5 tháng trước cách đây
mục cha
commit
df58ab12d3
33 tập tin đã thay đổi với 1239 bổ sung218 xóa
  1. 1 1
      .env
  2. 4 4
      addons/admin/src/views/exam/index2-1.vue
  3. 4 4
      addons/admin/src/views/exam/index2-5.vue
  4. 2 1
      addons/admin/src/views/vip/vip.vue
  5. 19 0
      app/Helpers/common.php
  6. 73 0
      app/Http/Controllers/Api/v1/CourseController.php
  7. 19 18
      app/Http/Controllers/Api/v1/ExamController.php
  8. 4 7
      app/Http/Controllers/Api/v1/IndexController.php
  9. 22 17
      app/Http/Controllers/Api/v1/LoginController.php
  10. 10 6
      app/Http/Controllers/Api/v1/MemberController.php
  11. 18 30
      app/Http/Controllers/Api/v1/PaperController.php
  12. 44 0
      app/Models/ExamAccessLogModel.php
  13. 5 0
      app/Models/InstitutionModel.php
  14. 24 0
      app/Models/MemberAnswerRankModel.php
  15. 19 0
      app/Models/VideoCategoryModel.php
  16. 59 0
      app/Models/VideoCoursesModel.php
  17. 87 0
      app/Models/VideoCoursesModel_BACKUP_1590.php
  18. 3 13
      app/Models/DepositModel.php
  19. 24 0
      app/Models/VideoCoursesModel_LOCAL_1590.php
  20. 84 0
      app/Models/VideoCoursesModel_REMOTE_1590.php
  21. 24 0
      app/Models/VideoLearnLogModel.php
  22. 19 0
      app/Models/VideoModel.php
  23. 10 0
      app/Models/VipModel.php
  24. 2 6
      app/Services/Api/AccountService.php
  25. 240 0
      app/Services/Api/CourseService.php
  26. 33 73
      app/Services/Api/ExamService.php
  27. 77 30
      app/Services/Api/MemberService.php
  28. 268 0
      app/Services/Api/PaperService.php
  29. 3 0
      app/Services/Common/VideoService.php
  30. 0 1
      app/Services/MpService.php
  31. 27 5
      app/Services/PaymentService.php
  32. 1 0
      resources/lang/zh-cn/api.php
  33. 10 2
      routes/api.php

+ 1 - 1
.env

@@ -39,7 +39,7 @@ SESSION_LIFETIME=120
 
 REDIS_PREFIX=null
 REDIS_HOST=127.0.0.1
-REDIS_PASSWORD=
+REDIS_PASSWORD=derkj&6688
 REDIS_PORT=16379
 REDIS_DB=1
 

+ 4 - 4
addons/admin/src/views/exam/index2-1.vue

@@ -12,10 +12,10 @@ export default {
     data() {
         return {
             permissionMap: {
-                delete: "sys:duikou1:delete",
-                edit: "sys:duikou1:edit",
-                add: "sys:duikou1:add",
-                index: "sys:duikou1:index",
+                delete: "exam:duikou:daily:delete",
+                edit: "exam:duikou:daily:edit",
+                add: "exam:duikou:daily:add",
+                index: "exam:duikou:daily:index",
             }
         }
     },

+ 4 - 4
addons/admin/src/views/exam/index2-5.vue

@@ -12,10 +12,10 @@ export default {
     data() {
         return {
             permissionMap: {
-                delete: "sys:duikou5:delete",
-                edit: "sys:duikou5:edit",
-                add: "sys:duikou5:add",
-                index: "sys:duikou5:index",
+                delete: "exam:duikou:review:delete",
+                edit: "exam:duikou:review:edit",
+                add: "exam:duikou:review:add",
+                index: "exam:duikou:review:index",
             }
         }
     },

+ 2 - 1
addons/admin/src/views/vip/vip.vue

@@ -71,11 +71,12 @@
                             </el-tag>
                         </template>
                     </el-table-column>
-                    <el-table-column prop="remark" label="备注" min-width="60" align="center" fixed="left" />
                     <el-table-column label="操作" width="160" fixed="right">
                         <template slot-scope="{ row }">
                             <el-button v-if="permission.includes(permissionMap['edit'])" type="text" size="mini"
                                 @click="openForm(row)">编辑</el-button>
+                            <el-button v-if="permission.includes(permissionMap['delete'])" type="text" size="mini"
+                                @click="remove(row)">删除</el-button>
                         </template>
                     </el-table-column>
                 </template>

+ 19 - 0
app/Helpers/common.php

@@ -2337,6 +2337,25 @@ if (!function_exists('api_decrypt')) {
     }
 }
 
+if (!function_exists('crypt_answer')) {
+    /**
+     * 编码答案
+     * @param $id
+     * @param $answer
+     * @param string $key
+     * @return false|string
+     */
+    function crypt_answer($id, $answer, $key = '')
+    {
+        if (empty($id) || empty($answer)) {
+            return false;
+        }
+
+        $key = $key ? $key : env('APP_SIGN_KEY', '');
+        return md5($id.'-'. $answer.'-'.$key);
+    }
+}
+
 if (!function_exists('format_message')) {
     /**
      * 格式化消息内容

+ 73 - 0
app/Http/Controllers/Api/v1/CourseController.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Http\Controllers\Api\v1;
+
+use App\Http\Controllers\Api\webApp;
+use App\Services\Api\CourseService;
+use App\Services\Api\PaperService;
+
+/**
+ * 视频课管理
+ * @package App\Http\Controllers\Api
+ */
+class CourseController extends webApp
+{
+
+    /**
+     * 主页列表
+     * @return array
+     */
+    public function index()
+    {
+        try {
+            $params = request()->all();
+            $datas = CourseService::make()->getListByCate($params);
+            return message(1010, true, $datas);
+        } catch (\Exception $exception) {
+            return message(1009, false, $exception->getMessage());
+        }
+    }
+
+    /**
+     * 列表
+     * @return array
+     */
+    public function list()
+    {
+        try {
+            $params = request()->all();
+            $groupId = isset($params['gid']) ? $params['gid'] : 0;
+            if (empty($groupId)) {
+                return message(1036, false);
+            }
+            $pageSize = isset($params['pageSize']) ? $params['pageSize'] : 10;
+            $datas = CourseService::make()->getListByGroup($groupId, $params, $pageSize);
+            return message(1010, true, $datas);
+        } catch (\Exception $exception) {
+            return message(1009, false, $exception->getMessage());
+        }
+    }
+
+    /**
+     * 详情
+     */
+    public function info()
+    {
+        try {
+            $params = request()->all();
+            $id = isset($params['id']) ? intval($params['id']) : 0;
+            if (empty($id)) {
+                return message(1036, false);
+            }
+
+            if ($info = CourseService::make()->getInfo($this->userId, $id)) {
+                return message(1010, true, $info);
+            } else {
+                return message(1009, false);
+            }
+        } catch (\Exception $exception) {
+            return message(1009, false, $exception->getMessage());
+        }
+    }
+
+}

+ 19 - 18
app/Http/Controllers/Api/v1/ExamController.php

@@ -19,7 +19,7 @@ class ExamController extends webApp
      */
     public function index()
     {
-        $params =request()->post();
+        $params = request()->post();
         $pageSize = request()->post('pageSize', 15);
         $datas = ExamService::make()->getDataList($params, $pageSize);
         return message(1010, true, $datas);
@@ -31,10 +31,14 @@ class ExamController extends webApp
      */
     public function history()
     {
-        $params =request()->post();
-        $pageSize = request()->post('pageSize', 15);
-        $datas = ExamService::make()->getHistoryList($params, $pageSize);
-        return message(1010, true, $datas);
+        try {
+            $params = request()->post();
+            $pageSize = request()->post('pageSize', 15);
+            $datas = ExamService::make()->getHistoryList($params, $pageSize);
+            return message(1010, true, $datas);
+        } catch (\Exception $exception) {
+            return message(1009, false);
+        }
     }
 
     /**
@@ -43,34 +47,31 @@ class ExamController extends webApp
     public function info()
     {
         $params = request()->all();
-        $id = isset($params['id'])? intval($params['id']) : 0;
-        if(empty($id)){
+        $id = isset($params['id']) ? intval($params['id']) : 0;
+        if (empty($id)) {
             return message(1036, false);
         }
 
-        if($info = ArticleService::make()->getInfo($id)){
+        if ($info = ArticleService::make()->getInfo($id)) {
             return message(1010, true, $info);
-        }else{
+        } else {
             return message(1009, false);
         }
     }
 
 
     /**
-     * 单页数据
+     * 排行榜
      */
-    public function page()
+    public function ranks()
     {
         $params = request()->all();
-        $type = isset($params['type'])? intval($params['type']) : 0;
-        if(empty($type)){
+        $type = isset($params['type']) ? intval($params['type']) : 0;
+        if (empty($type)) {
             return message(1031, false);
         }
 
-        if($info = ArticleService::make()->getInfoByType($type)){
-            return message(1010, true, $info);
-        }else{
-            return message(1009, false);
-        }
+        $datas = ExamService::make()->getRankByType($type);
+        return message(1010, true, $datas);
     }
 }

+ 4 - 7
app/Http/Controllers/Api/v1/IndexController.php

@@ -3,13 +3,9 @@
 namespace App\Http\Controllers\Api\v1;
 
 use App\Http\Controllers\Api\webApp;
-use App\Services\Api\AccountService;
-use App\Services\Api\MemberService;
-use App\Services\Api\OrderService;
+use App\Services\Api\CourseService;
 use App\Services\Common\NoticeService;
-use App\Services\Common\VideoService;
 use App\Services\ConfigService;
-use App\Services\MpService;
 use App\Services\RedisService;
 
 /**
@@ -37,6 +33,7 @@ class IndexController extends webApp
                     'app_version' => ConfigService::make()->getConfigByCode('app_version'),
                     'wxpay_open' => ConfigService::make()->getConfigByCode('wxpay_open', 1),
                     'vip_buy_desc' => format_content(ConfigService::make()->getConfigByCode('vip_buy_desc', '')),
+                    'video_vip_tips' => format_content(ConfigService::make()->getConfigByCode('video_vip_tips', '')),
                     'kfUrl' => ConfigService::make()->getConfigByCode('wechat_kf_url', ''),
                 ];
                 RedisService::set($cacheKey, $config, 3600);
@@ -62,8 +59,8 @@ class IndexController extends webApp
             'notices' => NoticeService::make()->getRecommandList(),
             // 线上课程
             'courses' => [
-                VideoService::make()->getListByType(1),
-                VideoService::make()->getListByType(2),
+                CourseService::make()->getListByType(1),
+                CourseService::make()->getListByType(2),
             ],
         ];
         return showJson(1010, true, $data);

+ 22 - 17
app/Http/Controllers/Api/v1/LoginController.php

@@ -32,7 +32,7 @@ class LoginController extends webApp
             return showJson($params, false);
         }
 
-        if(!$result = MemberService::make()->login($params)){
+        if (!$result = MemberService::make()->login($params)) {
             return showJson(MemberService::make()->getError(), false);
         }
 
@@ -46,18 +46,23 @@ class LoginController extends webApp
      */
     public function mpAuth(MemberValidator $validator)
     {
-        $params = request()->all();
-        $params = $validator->check($params, 'mp');
-        if (!is_array($params)) {
-            return showJson($params, false);
-        }
 
-        $code = isset($params['code'])? $params['code'] : '';
-        if($result = MemberService::make()->mpAuth($code, $params)){
-            return showJson(MemberService::make()->getError(), true, $result);
+        try {
+            $params = request()->all();
+
+            $params = $validator->check($params, 'mp');
+            if (!is_array($params)) {
+                return showJson($params, false);
+            }
+
+            $code = isset($params['code']) ? $params['code'] : '';
+            if ($result = MemberService::make()->mpAuth($code, $params)) {
+                return showJson(MemberService::make()->getError(), true, $result);
+            }
+        } catch (\Exception $exception) {
+            $error = env('APP_DEBUG') ? ['data' => $exception->getTrace(), 'err' => $exception->getMessage()] : [];
+            return showJson(1046, false, $error);
         }
-
-        return showJson(MemberService::make()->getError(), false);
     }
 
     /**
@@ -73,11 +78,11 @@ class LoginController extends webApp
             return showJson($params, false);
         }
 
-        if(!MemberService::make()->forget($params)){
+        if (!MemberService::make()->forget($params)) {
             $error = MemberService::make()->getError();
-            return showJson($error,false,'',$error==1040?405:0);
-        }else{
-            return showJson(MemberService::make()->getError(),true);
+            return showJson($error, false, '', $error == 1040 ? 405 : 0);
+        } else {
+            return showJson(MemberService::make()->getError(), true);
         }
     }
 
@@ -105,7 +110,7 @@ class LoginController extends webApp
             case 'deposit': // 退保
             case 'reset_password': // 忘记密码
             case 'login':
-                if (!MemberService::make()->checkExists('mobile',$mobile)) {
+                if (!MemberService::make()->checkExists('mobile', $mobile)) {
                     return showJson(2001, false);
                 }
                 break;
@@ -124,7 +129,7 @@ class LoginController extends webApp
      */
     public function logout()
     {
-        RedisService::clear("auths:info:".$this->userId);
+        RedisService::clear("auths:info:" . $this->userId);
         return showJson(1003, true);
     }
 }

+ 10 - 6
app/Http/Controllers/Api/v1/MemberController.php

@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api\v1;
 
 use App\Http\Controllers\Api\webApp;
 use App\Services\Api\MemberService;
-use App\Services\MpService;
 use Illuminate\Http\Request;
 
 /**
@@ -112,11 +111,16 @@ class MemberController extends webApp
      */
     public function vipBuy()
     {
-        $vipId = request()->post('id');
-        if ($result = MemberService::make()->vipBuy($this->userId, $vipId)) {
-            return showJson(MemberService::make()->getError(), true, $result);
-        } else {
-            return showJson(MemberService::make()->getError(), false);
+        try {
+            $params = request()->all();
+            if ($result = MemberService::make()->vipBuy($this->userId, $params)) {
+                return showJson(MemberService::make()->getError(), true, $result);
+            } else {
+                return showJson(MemberService::make()->getError(), false);
+            }
+        } catch (\Exception $exception){
+            $error = env('APP_DEBUG')? ['data'=>$exception->getTrace(),'err'=>$exception->getMessage()] : [];
+            return showJson(1046, false, $error);
         }
     }
 

+ 18 - 30
app/Http/Controllers/Api/v1/PaperController.php

@@ -3,7 +3,7 @@
 namespace App\Http\Controllers\Api\v1;
 
 use App\Http\Controllers\Api\webApp;
-use App\Services\Api\ArticleService;
+use App\Services\Api\PaperService;
 
 /**
  * 试卷管理
@@ -20,7 +20,7 @@ class PaperController extends webApp
     {
         $params =request()->post();
         $pageSize = request()->post('pageSize', 15);
-        $datas = Exa::make()->getDataList($params, $pageSize);
+        $datas = PaperService::make()->getDataList($params, $pageSize);
         return message(1010, true, $datas);
     }
 
@@ -29,35 +29,23 @@ class PaperController extends webApp
      */
     public function info()
     {
-        $params = request()->all();
-        $id = isset($params['id'])? intval($params['id']) : 0;
-        if(empty($id)){
-            return message(1036, false);
-        }
-
-        if($info = ArticleService::make()->getInfo($id)){
-            return message(1010, true, $info);
-        }else{
-            return message(1009, false);
+        try {
+            $params = request()->all();
+            $id = isset($params['id'])? intval($params['id']) : 0;
+            $tid = isset($params['tid'])? intval($params['tid']) : 0;
+            $rid = isset($params['rid'])? intval($params['rid']) : 0;
+            if(empty($id)){
+                return message(1036, false);
+            }
+
+            if($info = PaperService::make()->getInfo($this->userId, $id, $tid, $rid)){
+                return message(1010, true, $info);
+            }else{
+                return message(1009, false);
+            }
+        }catch (\Exception $exception){
+            return message(1009, false, $exception->getMessage());
         }
     }
 
-
-    /**
-     * 单页数据
-     */
-    public function page()
-    {
-        $params = request()->all();
-        $type = isset($params['type'])? intval($params['type']) : 0;
-        if(empty($type)){
-            return message(1031, false);
-        }
-
-        if($info = ArticleService::make()->getInfoByType($type)){
-            return message(1010, true, $info);
-        }else{
-            return message(1009, false);
-        }
-    }
 }

+ 44 - 0
app/Models/ExamAccessLogModel.php

@@ -6,7 +6,51 @@
  * @package App\Models
  */
 namespace App\Models;
+use App\Services\RedisService;
+use Illuminate\Support\Facades\DB;
+
 class ExamAccessLogModel extends BaseModel
 {
     protected $table = 'exam_access_logs';
+
+    /**
+     * 模块访问日志
+     * @param $date 日期
+     * @param int $type 模块
+     * @param int $scene 场景
+     */
+    public static function saveLog($date, $type=1, $scene=1, $userId=0)
+    {
+        if($type <= 0){
+            return false;
+        }
+
+        $cacheKey = "caches:access:{$date}:{$type}_{$scene}";
+        if(RedisService::get($cacheKey."_lock:{$userId}")){
+            return false;
+        }
+
+        RedisService::set($cacheKey."_lock:{$userId}", date('Y-m-d H:i:s'), rand(2, 3));
+        $checkId = RedisService::get($cacheKey);
+        if(!$checkId){
+            $checkId = self::where(['date'=> $date,'type'=>$type,'mark'=>1])
+                ->value('id');
+        }
+
+        if($checkId){
+            self::where(['id'=> $checkId])->update(["scene_count{$scene}"=>DB::raw("scene_count{$scene} + 1"),'update_time'=>time()]);
+        }else{
+            $id = self::insertGetId([
+                'date'=> $date,
+                'type'=> $type,
+                "scene_count{$scene}"=> 1,
+                'create_time'=>time(),
+                'update_time'=>time(),
+                'status'=>1,
+            ]);
+            if($id){
+                RedisService::set($cacheKey, $id, rand(300, 600));
+            }
+        }
+    }
 }

+ 5 - 0
app/Models/InstitutionModel.php

@@ -47,4 +47,9 @@ class InstitutionModel extends BaseModel
 
         return $info;
     }
+
+    public function getThumbAttribute($value)
+    {
+
+    }
 }

+ 24 - 0
app/Models/MemberAnswerRankModel.php

@@ -0,0 +1,24 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Models;
+
+/**
+ * 会员答题排名-模型
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Models
+ */
+class MemberAnswerRankModel extends BaseModel
+{
+    // 设置数据表
+    protected $table = 'member_answer_ranks';
+}

+ 19 - 0
app/Models/VideoCategoryModel.php

@@ -22,4 +22,23 @@ class VideoCategoryModel extends BaseModel
     // 设置数据表
     protected $table = 'videos_categorys';
 
+    public function getIconAttribute($value)
+    {
+        return $value? get_image_url($value) : '';
+    }
+
+    public function setIconAttribute($value)
+    {
+        return $value? get_image_path($value) : '';
+    }
+
+    /**
+     * 课程列表
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function courses(){
+        return $this->hasMany(VideoModel::class, 'category_id','id')
+            ->where(['status'=>1,'mark'=>1])->orderBy('sort','desc');
+    }
+
 }

+ 59 - 0
app/Models/VideoCoursesModel.php

@@ -21,4 +21,63 @@ class VideoCoursesModel extends BaseModel
 {
     // 设置数据表
     protected $table = 'videos_courses';
+
+    public function getPosterAttribute($value)
+    {
+        return $value ? get_image_url($value) : '';
+    }
+
+    public function setPosterAttribute($value)
+    {
+        return $value ? get_image_path($value) : '';
+    }
+
+    public function getFeeAttribute($value)
+    {
+        return $value ? floatval($value) : 0.00;
+    }
+
+    /**
+     * 课程集
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function collection()
+    {
+        return $this->hasOne(VideoModel::class, 'id', 'video_id')
+            ->with(['category'])
+            ->where(['status' => 1, 'mark' => 1])
+            ->select(['id', 'video_name', 'category_id', 'type', 'poster', 'description', 'status']);
+    }
+
+    /**
+     * 是否有效购买单集VIP
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function vip()
+    {
+        return $this->hasOne(VideoOrderModel::class, 'goods_id', 'id')
+            ->where('expired_at', '>', date('Y-m-d H:i:s'))
+            ->where(['status' => 2, 'mark' => 1])
+            ->select(['id', 'order_no', 'goods_id', 'total', 'expired_at', 'status']);
+    }
+
+    /**
+     * 所有课程
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function courses()
+    {
+        return $this->hasMany(VideoCoursesModel::class, 'video_id', 'video_id')
+            ->where(['status' => 1, 'mark' => 1]);
+    }
+
+    /**
+     * 学习记录
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function learns()
+    {
+        return $this->hasMany(VideoLearnLogModel::class, 'video_id', 'video_id')
+            ->where(['status' => 1, 'mark' => 1]);
+    }
 }

+ 87 - 0
app/Models/VideoCoursesModel_BACKUP_1590.php

@@ -0,0 +1,87 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Models;
+
+/**
+ * -模型
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Models
+ */
+class VideoCoursesModel extends BaseModel
+{
+    // 设置数据表
+    protected $table = 'videos_courses';
+<<<<<<< HEAD
+=======
+
+    public function getPosterAttribute($value)
+    {
+        return $value? get_image_url($value) : '';
+    }
+
+    public function setPosterAttribute($value)
+    {
+        return $value? get_image_path($value) : '';
+    }
+
+    public function getFeeAttribute($value)
+    {
+        return $value? floatval($value) : 0.00;
+    }
+
+    /**
+     * 课程集
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function collection()
+    {
+        return $this->hasOne(VideoModel::class, 'id', 'video_id')
+            ->with(['category'])
+            ->where(['status'=>1,'mark'=>1])
+            ->select(['id', 'video_name', 'category_id','type','poster','description', 'status']);
+    }
+
+    /**
+     * 是否有效购买单集VIP
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function vip()
+    {
+        return $this->hasOne(VideoOrderModel::class, 'goods_id', 'id')
+            ->where('expired_at','>', date('Y-m-d H:i:s'))
+            ->where(['status'=>2,'mark'=>1])
+            ->select(['id', 'order_no', 'goods_id','total','expired_at', 'status']);
+    }
+
+    /**
+     * 所有课程
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function courses()
+    {
+        return $this->hasMany(VideoCoursesModel::class, 'video_id', 'video_id')
+            ->where(['status'=>1,'mark'=>1]);
+    }
+
+    /**
+     * 学习记录
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function learns()
+    {
+        return $this->hasMany(VideoLearnLogModel::class, 'video_id', 'video_id')
+            ->where(['status'=>1,'mark'=>1]);
+    }
+
+>>>>>>> d64ac277db90f4649e277b9e40eb3d141ae8c846
+}

+ 3 - 13
app/Models/DepositModel.php

@@ -12,24 +12,14 @@
 namespace App\Models;
 
 /**
- * 保证金-模型
+ * -模型
  * @author laravel开发员
  * @since 2020/11/11
- * Class CityModel
  * @package App\Models
  */
-class DepositModel extends BaseModel
+class VideoCoursesModel extends BaseModel
 {
     // 设置数据表
-    protected $table = 'deposit_orders';
+    protected $table = 'videos_courses';
 
-    /**
-     * 用户
-     * @return \Illuminate\Database\Eloquent\Relations\HasOne
-     */
-    public function user()
-    {
-        return $this->hasOne(MemberModel::class, 'id','user_id')
-            ->select(['id','mobile','nickname','status']);
-    }
 }

+ 24 - 0
app/Models/VideoCoursesModel_LOCAL_1590.php

@@ -0,0 +1,24 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Models;
+
+/**
+ * -模型
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Models
+ */
+class VideoCoursesModel extends BaseModel
+{
+    // 设置数据表
+    protected $table = 'videos_courses';
+}

+ 84 - 0
app/Models/VideoCoursesModel_REMOTE_1590.php

@@ -0,0 +1,84 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Models;
+
+/**
+ * -模型
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Models
+ */
+class VideoCoursesModel extends BaseModel
+{
+    // 设置数据表
+    protected $table = 'videos_courses';
+
+    public function getPosterAttribute($value)
+    {
+        return $value? get_image_url($value) : '';
+    }
+
+    public function setPosterAttribute($value)
+    {
+        return $value? get_image_path($value) : '';
+    }
+
+    public function getFeeAttribute($value)
+    {
+        return $value? floatval($value) : 0.00;
+    }
+
+    /**
+     * 课程集
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function collection()
+    {
+        return $this->hasOne(VideoModel::class, 'id', 'video_id')
+            ->with(['category'])
+            ->where(['status'=>1,'mark'=>1])
+            ->select(['id', 'video_name', 'category_id','type','poster','description', 'status']);
+    }
+
+    /**
+     * 是否有效购买单集VIP
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function vip()
+    {
+        return $this->hasOne(VideoOrderModel::class, 'goods_id', 'id')
+            ->where('expired_at','>', date('Y-m-d H:i:s'))
+            ->where(['status'=>2,'mark'=>1])
+            ->select(['id', 'order_no', 'goods_id','total','expired_at', 'status']);
+    }
+
+    /**
+     * 所有课程
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function courses()
+    {
+        return $this->hasMany(VideoCoursesModel::class, 'video_id', 'video_id')
+            ->where(['status'=>1,'mark'=>1]);
+    }
+
+    /**
+     * 学习记录
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function learns()
+    {
+        return $this->hasMany(VideoLearnLogModel::class, 'video_id', 'video_id')
+            ->where(['status'=>1,'mark'=>1]);
+    }
+
+}

+ 24 - 0
app/Models/VideoLearnLogModel.php

@@ -0,0 +1,24 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Models;
+
+/**
+ * 视频课学习记录-模型
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Models
+ */
+class VideoLearnLogModel extends BaseModel
+{
+    // 设置数据表
+    protected $table = 'videos_learn_logs';
+}

+ 19 - 0
app/Models/VideoModel.php

@@ -22,4 +22,23 @@ class VideoModel extends BaseModel
     // 设置数据表
     protected $table = 'videos';
 
+    public function getPosterAttribute($value)
+    {
+        return $value? get_image_url($value) : '';
+    }
+
+    public function setPosterAttribute($value)
+    {
+        return $value? get_image_path($value) : '';
+    }
+
+    /**
+     * 课程分类
+     * @return \Illuminate\Database\Eloquent\Relations\HasOne
+     */
+    public function category()
+    {
+        return $this->hasOne(VideoCategoryModel::class, 'id', 'category_id')
+            ->select(['id', 'name', 'pid','icon']);
+    }
 }

+ 10 - 0
app/Models/VipModel.php

@@ -22,6 +22,16 @@ class VipModel extends BaseModel
     // 设置数据表
     protected $table = 'member_vips';
 
+    public function getOriginalPriceAttribute($value)
+    {
+        return $value? floatval($value) : 0;
+    }
+
+    public function getPriceAttribute($value)
+    {
+        return $value? floatval($value) : 0;
+    }
+
     /**
      * 用户
      * @return \Illuminate\Database\Eloquent\Relations\HasOne

+ 2 - 6
app/Services/Api/AccountService.php

@@ -65,15 +65,11 @@ class AccountService extends BaseService
             $accountTypes = config('payment.accountTypes');
             foreach($list['data'] as &$item){
                 $item['create_time'] = $item['create_time']? datetime($item['create_time'],'Y-m-d H:i:s') : '';
-                $item['time_text'] = $item['create_time']? datetime($item['create_time'],'Y年m月d日') : '';
+                $item['time_text'] = $item['create_time']? datetime($item['create_time'],'Y-m-d H:i') : '';
                 $type = isset($item['type'])? intval($item['type']) : 0;
                 $item['type_text'] = isset($item['remark'])? trim($item['remark']) : '';
                 if(empty($item['type_text'])){
-                    $item['type_text'] = isset($accountTypes[$type])? $accountTypes[$type] : '收支明细';
-                }
-                $item['change_type'] = 1;
-                if(in_array($type,[3,4])){
-                    $item['change_type'] = 2;
+                    $item['type_text'] = isset($accountTypes[$type])? $accountTypes[$type] : '支付订单';
                 }
             }
         }

+ 240 - 0
app/Services/Api/CourseService.php

@@ -0,0 +1,240 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Services\Api;
+
+use App\Models\ExamAccessLogModel;
+use App\Models\VideoCategoryModel;
+use App\Models\VideoCoursesModel;
+use App\Models\VideoModel;
+use App\Services\BaseService;
+use App\Services\ConfigService;
+use App\Services\RedisService;
+
+/**
+ * 视频课服务-服务类
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Services\Api
+ */
+class CourseService extends BaseService
+{
+    // 静态对象
+    protected static $instance = null;
+
+    /**
+     * 构造函数
+     * @author laravel开发员
+     * @since 2020/11/11
+     */
+    public function __construct()
+    {
+        $this->model = new VideoCoursesModel();
+    }
+
+    /**
+     * 静态入口
+     */
+    public static function make()
+    {
+        if (!self::$instance) {
+            self::$instance = new static();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * 获取
+     * @param $type
+     * @param int $num
+     * @return array|mixed
+     */
+    public function getListByType($type, $num = 0)
+    {
+        $num = $num? $num : \App\Services\ConfigService::make()->getConfigByCode('show_course_num', 4);
+        $cacheKey = "caches:videos:type_list_{$type}_{$num}";
+        $datas = RedisService::get($cacheKey);
+        if($datas){
+            return $datas;
+        }
+
+        $datas = VideoModel::where(['type'=>$type,'status'=>1,'mark'=>1])
+            ->select(['id','video_name','poster','type','description','is_recommend','status'])
+            ->orderBy('is_recommend','asc')
+            ->orderBy('create_time','desc')
+            ->limit($num)
+            ->get();
+        $datas = $datas? $datas->toArray() : [];
+        if($datas){
+            foreach ($datas as &$item){
+                $item['poster'] = $item['poster'] ? get_image_url($item['poster']) : '';
+            }
+
+            RedisService::set($cacheKey, $datas, rand(3600, 7200));
+        }
+
+        return $datas;
+    }
+
+    /**
+     * 获取分类下视频课
+     * @param int
+     * @return array|mixed
+     */
+    public function getListByCate($params)
+    {
+        $type = isset($params['type'])? $params['type'] : 0;
+        $sc = isset($params['sc'])? $params['sc'] : 0;
+        $cacheKey = "caches:videos:list_by_cate:{$type}_".md5(json_encode($params));
+        $datas = RedisService::get($cacheKey);
+
+        // 视频课访问次数统计
+        if(empty($sc)){
+            ExamAccessLogModel::saveLog(date('Y-m-d'), $type, 20);
+        }
+        if($datas){
+            return $datas;
+        }
+
+        //
+        $datas = VideoCategoryModel::with(['courses'=>function($query) use($params){
+                $kw = isset($params['keyword'])? trim($params['keyword']) : '';
+                if($kw){
+                    $query->where(function($query) use($kw){
+                        $query->where('video_name',"like","%{$kw}%")
+                            ->orwhere('description',"like","%{$kw}%");
+                    });
+                }
+
+                $type = isset($params['type'])? intval($params['type']) : 0;
+                if($type>0){
+                    $query->where('type',$type);
+                }
+            }])->distinct()
+            ->from('videos_categorys as a')
+            ->where(['a.status'=>1,'a.mark'=>1])
+            ->select(['a.id','a.name','a.sort','a.icon'])
+            ->orderBy('a.sort','desc')
+            ->get();
+        $datas = $datas? $datas->toArray() : [];
+        if($datas){
+            foreach ($datas as &$item){
+                $item['icon'] = $item['icon']? get_image_url($item['icon']) : '';
+            }
+            RedisService::set($cacheKey, $datas, rand(300,600));
+        }
+
+        return $datas;
+    }
+
+    /**
+     * 获取视频集下课程列表
+     * @param int
+     * @return array|mixed
+     */
+    public function getListByGroup($groupId,$params,$pageSize)
+    {
+        $page = isset($params['page'])? $params['page'] : 1;
+        $type = isset($params['type'])? $params['type'] : 0;
+        $cid = isset($params['cid'])? $params['cid'] : 0;
+        $sc = isset($params['sc'])? $params['sc'] : 0;
+        $cacheKey = "caches:videos:list_by_group:{$groupId}_{$page}_{$pageSize}_{$type}".md5(json_encode($params));
+        $datas = RedisService::get($cacheKey);
+        // 视频课访问次数统计
+        if(empty($sc)){
+            ExamAccessLogModel::saveLog(date('Y-m-d'), $type, 20);
+        }
+
+        if($datas){
+            return $datas;
+        }
+
+        $list = $this->model->leftJoin('videos as b','b.id','=','videos_courses.video_id')
+            ->with(['vip'])
+            ->where(['videos_courses.video_id'=> $groupId,'videos_courses.status'=>1,'videos_courses.mark'=>1,'b.status'=>1,'b.mark'=>1])
+            ->where(function($query) use($params){
+                $kw = isset($params['keyword'])? trim($params['keyword']) : '';
+                if($kw){
+                    $query->where('videos_courses.course_name',"like","%{$kw}%")
+                        ->orwhere('videos_courses.description',"like","%{$kw}%");
+                }
+            })
+            ->where(function($query) use($cid){
+                if($cid){
+                    $query->whereNotIn('videos_courses.id', [$cid]);
+                }
+            })
+            ->select(['videos_courses.id','videos_courses.video_id','videos_courses.course_name','videos_courses.course_url','videos_courses.fee','videos_courses.poster','videos_courses.description','videos_courses.sort'])
+            ->withCount(['courses','learns'])
+            ->orderBy('videos_courses.sort','desc')
+            ->orderBy('videos_courses.id','asc')
+            ->paginate($pageSize > 0 ? $pageSize : 9999999);
+        $list = $list? $list->toArray() :[];
+        if($list){
+            foreach ($list['data'] as &$item){
+
+            }
+        }
+        $rows = isset($list['data'])? $list['data'] : [];
+        $datas = [
+            'pageSize'=> $pageSize,
+            'total'=> isset($list['total'])? $list['total'] : 0,
+            'list'=> $rows
+        ];
+        if($rows){
+            RedisService::set($cacheKey, $datas, rand(300,600));
+        }
+
+        return $datas;
+    }
+
+    /**
+     * 视频课详情
+     * @param $userId 用户
+     * @param $id 课程ID
+     * @return array|mixed
+     */
+    public function getInfo($userId, $id)
+    {
+        $cacheKey = "caches:videos:info_{$userId}_{$id}";
+        $data = RedisService::get($cacheKey, $cacheKey);
+        if($data){
+            return $data;
+        }
+
+        $data = $this->model->from('videos_courses as videos_courses')
+            ->leftJoin('videos as b','b.id','=','videos_courses.video_id')
+            /*->leftJoin('videos_learn_logs as c',function($join) use($userId){
+                $join->on('c.course_id','=','videos_courses.id')
+                    ->where(['c.user_id'=>$userId,'c.status'=>1,'c.mark'=>1]);
+            })*/
+            ->with(['vip'])
+            ->withCount(['courses','learns'])
+            ->where(['videos_courses.id'=>$id,'videos_courses.status'=>1,'videos_courses.mark'=>1])
+            ->select(['videos_courses.id','videos_courses.video_id','videos_courses.course_name','videos_courses.course_url','videos_courses.fee','videos_courses.poster','videos_courses.description','videos_courses.sort','b.type'])
+            ->first();
+        $data = $data? $data->toArray() : [];
+        if($data){
+            // 验证付费视频是否有播放权限
+            $fee = isset($data['fee'])? $data['fee'] : 0;
+            $buyVipData = isset($data['vip'])? $data['vip'] : [];
+            $data['buy_vip'] = $buyVipData? 1 : 0;
+            $data['can_play'] = $buyVipData || $fee<=0? 1 : 0;
+            $data['preview_time'] = ConfigService::make()->getConfigByCode('course_play_preview_time', 3);
+
+            // 播放
+            RedisService::set($cacheKey, $data, rand(5, 10));
+        }
+
+        $this->model->where(['id'=> $id])->increment('views', 1);
+        return $data;
+    }
+}

+ 33 - 73
app/Services/Api/ExamService.php

@@ -12,9 +12,11 @@
 namespace App\Services\Api;
 
 use App\Models\ExamAnswerModel;
+use App\Models\MemberAnswerRankModel;
 use App\Services\BaseService;
 use App\Services\ConfigService;
 use App\Services\RedisService;
+use Illuminate\Support\Facades\DB;
 
 /**
  * 答题服务-服务类
@@ -157,99 +159,57 @@ class ExamService extends BaseService
             'list'=> $rows
         ];
         if($rows){
-            RedisService::set($cacheKey, $datas, rand(300, 600));
+            RedisService::set($cacheKey, $datas, rand(10, 20));
         }
 
         return $datas;
     }
 
     /**
-     * 获取文章详情
-     * @param $id
+     * 答题排行榜
+     * @param $type 1-日,2-周(7天),3-月
+     * @param int $num
      * @return array|mixed
      */
-    public function getInfo($id)
+    public function getRankByType($type, $num = 0)
     {
-        $cacheKey = "caches:articles:info_{$id}";
-        $info = RedisService::get($cacheKey);
-        if($info){
-            return $info;
-        }
-
-        $info = $this->model->where(['id'=> $id,'status'=>1,'mark'=>1])
-            ->select(['id','title','type','cover','view_num','author','description','create_time','type','content'])
-            ->first();
-        $info = $info? $info->toArray() : [];
-        if($info){
-            $info['create_time'] = $info['create_time']? datetime($info['create_time'],'Y-m-d') : '';
-            $info['cover'] = get_image_url($info['cover']);
-            $info['content'] = get_format_content($info['content']);
-            $this->model->where(['id'=> $id])->increment('view_num',1);
-            $info['view_num'] += intval($info['view_num']);
-            RedisService::set($cacheKey, $info, rand(5,10));
-        }
-
-        return $info;
-    }
-
-    /**
-     * 获取分类文章推荐
-     * @param int $cateId 推荐分类ID
-     * @param int $type 类别:3-普通文章,4-客服回复
-     * @return array|mixed
-     */
-    public function getCustomRecommend($cateId=0, $type=4)
-    {
-        $cacheKey = "caches:articles:list_{$cateId}";
+        $num = $num? $num : ConfigService::make()->getConfigByCode('rank_num', 10);
+        $cacheKey = "caches:exams:ranks:{$type}_{$num}";
         $datas = RedisService::get($cacheKey);
         if($datas){
             return $datas;
         }
 
-        $limitNum = ConfigService::make()->getConfigByCode('custom_recommend_num', 6);
-        $limitNum = $limitNum? $limitNum : 6;
-        $datas = ArticleCateModel::where(function($query) use($cateId){
-                if($cateId){
-                    $query->where('cate_id', $cateId);
+        $prefix = env('DB_PREFIX','lev_');
+        $datas = MemberAnswerRankModel::from('member_answer_ranks as a')
+            ->leftJoin('member as b','b.id','=','a.user_id')
+            ->where(['a.status'=>1,'a.mark'=>1])
+            ->where(function($query) use($type){
+                if($type==1){
+                    // 日
+                    $query->where('a.date', date('Y-m-d'));
+                }else if($type == 2){
+                    // 周
+                    $query->where('a.date','>=', date('Y-m-d', time() - 7 * 86400));
+                }else if($type == 3){
+                    // 月
+                    $query->where('a.date','>=', date('Y-m-01'));
                 }
-            })->where(['type'=>$type,'status'=>1,'mark'=>1])
-            ->select(['id','cate_id','title','description','sort','type','status'])
-            ->limit($limitNum)
-            ->orderBy('sort','desc')
-            ->orderBy('create_time','desc')
+            })
+            ->select(['a.id','a.user_id','b.avatar','b.nickname','a.answer_time','a.answer_count',DB::raw("ROUND(sum({$prefix}a.answer_time)/3600,0) as answer_hour"),DB::raw("sum({$prefix}a.answer_count) as count")])
+            ->groupBy('a.user_id')
+            ->orderByRaw("sum({$prefix}a.answer_time)/3600 desc")
+            ->take($num)
             ->get();
         $datas = $datas? $datas->toArray() : [];
         if($datas){
-            RedisService::set($cacheKey, $datas, rand(300,600));
-        }
-
-        return $datas;
-    }
-
-    /**
-     * 获取文章推荐分类
-     * @param int $type 1-普通文章分类
-     * @return array|mixed
-     */
-    public function getCateList($type=2)
-    {
-        $cacheKey = "caches:articles:cateList_{$type}";
-        $datas = RedisService::get($cacheKey);
-        if($datas){
-            return $datas;
-        }
+            foreach ($datas as &$item){
+                $item['avatar'] = $item['avatar']? get_image_url($item['avatar']) : '';
+            }
 
-        $limitNum = ConfigService::make()->getConfigByCode('custom_cate_num', 6);
-        $limitNum = $limitNum? $limitNum : 6;
-        $datas = ArticleCateModel::where(['type'=> $type,'status'=>1,'mark'=>1])
-            ->select(['cate_id','name','sort','type'])
-            ->limit($limitNum)
-            ->get();
-        $datas = $datas? $datas->toArray() : [];
-        if($datas){
-            RedisService::set($cacheKey, $datas, rand(300,600));
+            RedisService::set($cacheKey, $datas, rand(20, 30));
         }
-
         return $datas;
     }
+
 }

+ 77 - 30
app/Services/Api/MemberService.php

@@ -15,6 +15,8 @@ use App\Helpers\Jwt;
 use App\Models\ActionLogModel;
 use App\Models\MemberModel;
 use App\Models\OrderModel;
+use App\Models\VideoCoursesModel;
+use App\Models\VideoOrderModel;
 use App\Models\VipModel;
 use App\Services\BaseService;
 use App\Services\ConfigService;
@@ -353,13 +355,25 @@ class MemberService extends BaseService
                 $isZsbVip = 0;
             }
 
+            $isVideoVip = isset($info['is_video_vip']) ? $info['is_video_vip'] : 0;
+            $videoVipExpired = isset($info['video_vip_expired']) && !empty($info['video_vip_expired']) ? $info['video_vip_expired'] : '';
+            if ($isVideoVip==1 && $videoVipExpired && $videoVipExpired > date('Y-m-d H:i:s')) {
+                $isVideoVip = 1;
+            } else {
+                $isVideoVip = 0;
+            }
+
             $info['is_vip'] = $isVip;
             $info['is_zg_vip'] = $isZgVip;
             $info['is_zsb_vip'] = $isZsbVip;
             $info['vip_expired'] = $vipExpired;
             $info['zg_vip_expired'] = $zgVipExpired;
+            $info['zg_vip_day'] = max(0, intval((strtotime($zgVipExpired) - time())/86400));
             $info['zsb_vip_expired'] = $zsbVipExpired;
-            $info['video_vip_expired'] = isset($info['video_vip_expired']) && !empty($info['video_vip_expired']) ? $info['video_vip_expired'] : '';
+            $info['zsb_vip_day'] = max(0, intval((strtotime($zsbVipExpired) - time())/86400));
+            $info['is_video_vip'] = $isVideoVip;
+            $info['video_vip_expired'] = $videoVipExpired;
+            $info['video_vip_day'] = max(0, intval((strtotime($videoVipExpired) - time())/86400));
             RedisService::set($cacheKey, $info, rand(5, 10));
         }
 
@@ -549,7 +563,7 @@ class MemberService extends BaseService
         }
 
         $datas = VipModel::where(['type' => $type, 'status' => 1, 'mark' => 1])
-            ->select(['id', 'name', 'type', 'price', 'day', 'remark', 'status'])
+            ->select(['id', 'name', 'type', 'price','original_price', 'day', 'remark', 'status'])
             ->orderBy('id', 'asc')
             ->get();
         $datas = $datas ? $datas->toArray() : [];
@@ -566,8 +580,10 @@ class MemberService extends BaseService
      * @param $vipId
      * @return array|false
      */
-    public function vipBuy($userId, $vipId)
+    public function vipBuy($userId, $params)
     {
+        $vipId = isset($params['id'])? $params['id'] : 0; // VIP
+        $cId = isset($params['cid'])? $params['cid'] : 0; // 课程ID
         $cacheKey = "caches:members:vipBuy:{$userId}_{$vipId}";
         if (RedisService::get($cacheKey . '_lock')) {
             $this->error = '请不要频繁提交~';
@@ -617,56 +633,85 @@ class MemberService extends BaseService
         }
 
         // 验证是否
+        $goodsId = 0;
+        $scene = 'vip';
         $expiredTime = time();
-        $vipLimitOpen = ConfigService::make()->getConfigByCode('vip_limit_more', 0);
-        if($vipLimitOpen){
-            // 已开通职高VIP无法再开通
-            if($vipType == 1 && ($isZgVip==1 && $zgVipExpired >= date('Y-m-d H:i:s'))){
-                $this->error = '抱歉,您的职高VIP未到期,到期后再尝试~';
+        if($vipId == 5){
+            // 单节视频验证是否已付费过
+            if(VideoOrderModel::where(['goods_id'=> $vipId,'status'=>2,'mark'=>1])->where('expired_at','>', date('Y-m-d H:i:s'))->value('id')){
+                $this->error = '抱歉,您已购买过该节视频课,请刷新后尝试观看~';
                 return false;
             }
 
-            // 已开通专升本VIP无法再开通
-            if($vipType == 2 && ($isZsbVip==1 && $zsbVipExpired >= date('Y-m-d H:i:s'))){
-                $this->error = '抱歉,您的专升本VIP未到期,到期后再尝试~';
+            // 视频数据
+            $goodsId = $cId;
+            $scene = 'course';
+            $courseInfo = VideoCoursesModel::where(['id'=>$goodsId,'status'=>1,'mark'=>1])->select(['id','fee','course_name','status'])->first();
+            $fee = isset($courseInfo['fee'])?$courseInfo['fee'] : 0;
+            if(empty($courseInfo)){
+                $this->error = '抱歉,您购买的课程已下架,请刷新后重试~';
                 return false;
             }
 
-            // 已开通全部视频VIP无法再开通
-            if($vipType == 3 && ($isVideoVip && $videoVipExpired >= date('Y-m-d H:i:s'))){
-                $this->error = '抱歉,您的已开通全部视频VIP,请到期后再尝试~';
+            if($fee<=0){
+                $this->error = '抱歉,您购买的课程为免费课程,请刷新后直接观看~';
                 return false;
             }
-        }else{
-            if($vipType == 1 && $isZgVip==1 && $zgVipExpired >= date('Y-m-d H:i:s')){
-                $expiredTime = strtotime($zgVipExpired);
-            }
-
-            if($vipType == 2 && $isZsbVip==1 && $zsbVipExpired >= date('Y-m-d H:i:s')){
-                $expiredTime = strtotime($zsbVipExpired);
-            }
-
-            if($vipType == 3 && $isVideoVip==1 && $videoVipExpired >= date('Y-m-d H:i:s')){
-                $expiredTime = strtotime($videoVipExpired);
+        }else {
+            // VIP 验证
+            $goodsId = $vipId;
+            $vipLimitOpen = ConfigService::make()->getConfigByCode('vip_limit_more', 0);
+            if($vipLimitOpen){
+                // 已开通职高VIP无法再开通
+                if($vipType == 1 && ($isZgVip==1 && $zgVipExpired >= date('Y-m-d H:i:s'))){
+                    $this->error = '抱歉,您的职高VIP未到期,到期后再尝试~';
+                    return false;
+                }
+
+                // 已开通专升本VIP无法再开通
+                if($vipType == 2 && ($isZsbVip==1 && $zsbVipExpired >= date('Y-m-d H:i:s'))){
+                    $this->error = '抱歉,您的专升本VIP未到期,到期后再尝试~';
+                    return false;
+                }
+
+                // 已开通全部视频VIP无法再开通
+                if($vipType == 3 && ($isVideoVip && $videoVipExpired >= date('Y-m-d H:i:s'))){
+                    $this->error = '抱歉,您的已开通全部视频VIP,请到期后再尝试~';
+                    return false;
+                }
+            }else{
+                if($vipType == 1 && $isZgVip==1 && $zgVipExpired >= date('Y-m-d H:i:s')){
+                    $expiredTime = strtotime($zgVipExpired);
+                }
+
+                if($vipType == 2 && $isZsbVip==1 && $zsbVipExpired >= date('Y-m-d H:i:s')){
+                    $expiredTime = strtotime($zsbVipExpired);
+                }
+
+                if($vipType == 3 && $isVideoVip==1 && $videoVipExpired >= date('Y-m-d H:i:s')){
+                    $expiredTime = strtotime($videoVipExpired);
+                }
             }
         }
 
         // 创建订单
         $orderNo = get_order_num('VP');
+        $remark = $vipType==3? "购买{$vipName}" : "购买{$vipName}VIP";
         $order = [
             'order_no'=> $orderNo,
             'user_id'=> $userId,
-            'goods_id'=> $vipId,
+            'goods_id'=> $goodsId,
             'total'=> $price,
             'expired_at'=> date('Y-m-d H:i:s', $expiredTime + $day * 86400),
             'create_time'=> time(),
-            'remark'=> "购买{$vipName}VIP",
+            'remark'=> $remark,
             'status'=>1,
             'mark'=>1
         ];
 
         DB::beginTransaction();
-        if(!$orderId = OrderModel::insertGetId($order)){
+        $model = $vipId==5? new VideoOrderModel : new OrderModel;
+        if(!$orderId = $model::insertGetId($order)){
             $this->error = '创建VIP订单失败';
             return false;
         }
@@ -676,12 +721,12 @@ class MemberService extends BaseService
             'type'=> 1,
             'order_no'=> $orderNo,
             'pay_money'=> $price,
-            'body'=> "购买{$vipName}VIP",
+            'body'=> $remark,
             'openid'=> $openid
         ];
 
         // 调起支付
-        $payment = PaymentService::make()->minPay($info, $payOrder,'vip');
+        $payment = PaymentService::make()->minPay($info, $payOrder,$scene);
         if(empty($payment)){
             DB::rollBack();
             RedisService::clear($cacheKey.'_lock');
@@ -693,6 +738,8 @@ class MemberService extends BaseService
         DB::commit();
         $this->error = '创建VIP订单成功,请前往支付~';
         RedisService::clear($cacheKey.'_lock');
+        RedisService::keyDel("caches:videos:list_by_group*");
+        RedisService::clear("caches:videos:info_{$userId}_{$cId}");
         return [
             'order_id'=> $orderId,
             'payment'=> $payment,

+ 268 - 0
app/Services/Api/PaperService.php

@@ -0,0 +1,268 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Services\Api;
+
+use App\Models\ExamAnswerModel;
+use App\Models\ExamPaperModel;
+use App\Models\ExamTopicModel;
+use App\Services\BaseService;
+use App\Services\ConfigService;
+use App\Services\RedisService;
+
+/**
+ * 试卷服务-服务类
+ * @author laravel开发员
+ * @since 2020/11/11
+ * @package App\Services\Api
+ */
+class PaperService extends BaseService
+{
+    // 静态对象
+    protected static $instance = null;
+
+    /**
+     * 构造函数
+     * @author laravel开发员
+     * @since 2020/11/11
+     */
+    public function __construct()
+    {
+        $this->model = new ExamPaperModel();
+    }
+
+    /**
+     * 静态入口
+     */
+    public static function make()
+    {
+        if (!self::$instance) {
+            self::$instance = new static();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * @param $params
+     * @param int $pageSize
+     * @return array
+     */
+    public function getDataList($params, $pageSize = 15)
+    {
+        $page = isset($params['page'])? $params['page'] : 1;
+        $cacheKey = "caches:paper:list_{$page}_{$pageSize}:".md5(json_encode($params));
+        $datas = RedisService::get($cacheKey);
+        if($datas){
+            return $datas;
+        }
+
+        $query = $this->getQuery($params);
+        $list = $query->select(['a.id','a.user_id','a.paper_id','a.score','a.accurate_count','b.name','b.type','b.topic_count','b.score_total','b.is_charge','a.create_time','a.answer_times','a.status'])
+            ->orderBy('a.create_time','desc')
+            ->paginate($pageSize > 0 ? $pageSize : 9999999);
+        $list = $list? $list->toArray() :[];
+        if($list){
+            foreach($list['data'] as &$item){
+                $item['create_time'] = $item['create_time']? datetime($item['create_time'],'Y-m-d H.i.s') : '';
+            }
+        }
+
+        $rows = isset($list['data'])? $list['data'] : [];
+        $datas = [
+            'pageSize'=> $pageSize,
+            'total'=> isset($list['total'])? $list['total'] : 0,
+            'list'=> $rows
+        ];
+        if($rows){
+            RedisService::set($cacheKey, $datas, rand(300, 600));
+        }
+
+        return $datas;
+    }
+
+    /**
+     * 查询
+     * @param $params
+     * @return mixed
+     */
+    public function getQuery($params)
+    {
+        $where = ['b.status'=>1,'b.mark'=>1,'a.mark' => 1];
+        $status = isset($params['status'])? $params['status'] : 0;
+        $type = isset($params['type'])? $params['type'] : 0;
+        $sceneType = isset($params['scene_type'])? $params['scene_type'] : 0;
+        $subjectId = isset($params['subject_id'])? $params['subject_id'] : 0;
+        if($status>0){
+            $where['a.status'] = $status;
+        }
+        if($type>0){
+            $where['b.type'] = $type;
+        }
+
+        if($sceneType>0){
+            $where['b.scene_type'] = $sceneType;
+        }
+
+        if($subjectId>0){
+            $where['b.subject_id'] = $subjectId;
+        }
+
+        return $this->model->from('exam_answers as a')
+            ->leftJoin('exam_papers as b','b.id','=','a.paper_id')
+            ->where($where)
+            ->where(function ($query) use($params){
+                $keyword = isset($params['keyword'])? $params['keyword'] : '';
+                if($keyword){
+                    $query->where('b.name','like',"%{$keyword}%");
+                }
+            });
+    }
+
+    /**
+     * 最近是否答过该题
+     * @param $userId 用户ID
+     * @param $paperId 试卷ID
+     * @param int $submit 是否已交卷,1-是,0-否
+     * @return array|mixed
+     */
+    public function getLastAnswer($userId, $paperId, $submit=0)
+    {
+        $cacheKey = "caches:paper:answer_last_{$userId}:{$paperId}_{$submit}";
+        $data = RedisService::get($cacheKey);
+        if($data){
+            return $data;
+        }
+
+        $lastTime = ConfigService::make()->getConfigByCode('submit_paper_time', 30);
+        $lastTime = $lastTime>=1 && $lastTime <= 150? $lastTime : 30;
+        $data = ExamAnswerModel::where(['user_id'=>$userId,'is_submit'=> $submit,'paper_id'=> $paperId,'status'=>1,'mark'=>1])
+            ->where(function($query) use($lastTime, $submit){
+                if($submit<=0){
+                    // 未交卷
+                    $query->where('is_submit', 0)->orWhere('answer_last_at','>=', time() - $lastTime * 60);
+                }else{
+                    $query->where('is_submit', $submit)->orWhere('answer_last_at','<', time() - $lastTime * 60);
+                }
+            })
+            ->select(['id','paper_id','score','answer_last_at'])
+            ->first();
+        $data = $data? $data->toArray() : [];
+        if($data){
+            RedisService::set($cacheKey, $data, rand(5, 10));
+        }
+        return $data;
+    }
+
+    /**
+     * 获取详情
+     * @param $id
+     * @return array|mixed
+     */
+    public function getInfo($userId, $paperId, $tid=0, $rid=0)
+    {
+        $cacheKey = "caches:paper:info_{$userId}:p{$paperId}_t{$tid}_r{$rid}";
+        $info = RedisService::get($cacheKey);
+        if($info){
+            return $info;
+        }
+
+        // 若进行答题
+        if($rid<=0){
+            // 判断N分钟内是否有未交卷的答题
+            $lastAnswerInfo = $this->getLastAnswer($userId, $paperId, 0);
+            $rid = isset($lastAnswerInfo['id'])? $lastAnswerInfo['id'] : 0;
+        }
+
+        $where = ['a.id'=> $paperId,'a.status'=>1,'a.mark'=>1];
+        $info = $this->model->from('exam_papers as a')
+            ->leftJoin('exam_answers as b','b.paper_id','=','a.id')
+            ->where($where)
+            ->where(function($query) use($rid){
+                if($rid>0){
+                    $query->where(['b.id'=>$rid]);
+                }
+            })
+            ->select(['a.id as paper_id','b.id as rid','b.score','b.accurate_count','b.answer_times','a.name','a.type','a.scene_type','a.subject_id','a.score_total','a.topic_count','a.is_charge','a.create_time','a.status'])
+            ->first();
+        $info = $info? $info->toArray() : [];
+        if($info){
+            $info['create_time'] = $info['create_time']? datetime($info['create_time'],'Y-m-d') : '';
+
+            // 当前题目
+            //$prefix = env('DB_PREFIX','_lev');
+            $info['topic'] = ExamTopicModel::from('exam_topics as a')
+                ->leftJoin('exam_answers_topics as b', function($join) use($rid){
+                    // 是否有最近答题记录
+                    $join->on('b.topic_id','=',"a.id")->where("b.answer_log_id",'=', $rid);
+                })
+                ->where(['a.paper_id'=> $paperId,'a.status'=>1,'a.mark'=>1])
+                ->where(function($query) use($tid){
+                    // 答题卡选择的题目,否则默认按题目排序返回第一题
+                    if($tid>0){
+                      $query->where('a.id', $tid);
+                    }
+                })
+                ->select(['a.*','b.id as answer_topic_id','b.answer_log_id','b.answer as submit_answer','b.answer_type as submit_answer_type','b.score as submit_score','b.accurate'])
+                ->orderBy('a.sort','desc')
+                ->orderBy('a.id','asc')
+                ->first();
+            if($info['topic']) {
+                if($info['topic']['show_type'] == 1){
+                    $info['topic']['topic_name'] = format_content($info['topic']['topic_name']);
+                    if($rid<=0){
+                        $info['topic']['correct_answer'] = crypt_answer($info['topic']['id'], $info['topic']['correct_answer']);
+                        $info['topic']['topic_analysis'] = '';
+                    }
+                }else if($info['topic']['show_type'] == 2){
+                    $info['topic']['topic_name'] = get_image_url($info['topic']['topic_name']);
+
+                    // 已经答题,返回答案
+                    if($rid>0){
+                        $info['topic']['topic_analysis'] = get_image_url($info['topic']['topic_analysis']);
+                        if(preg_match("/images/", $info['topic']['correct_answer'])){
+                            $info['topic']['correct_answer'] = get_image_url($info['topic']['correct_answer']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_A'])){
+                            $info['topic']['answer_A'] = get_image_url($info['topic']['answer_A']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_B'])){
+                            $info['topic']['answer_B'] = get_image_url($info['topic']['answer_B']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_C'])){
+                            $info['topic']['answer_C'] = get_image_url($info['topic']['answer_C']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_D'])){
+                            $info['topic']['answer_D'] = get_image_url($info['topic']['answer_D']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_E'])){
+                            $info['topic']['answer_E'] = get_image_url($info['topic']['answer_E']);
+                        }
+
+                        if(preg_match("/images/", $info['topic']['answer_F'])){
+                            $info['topic']['answer_F'] = get_image_url($info['topic']['answer_F']);
+                        }
+                    }
+                }
+
+            }
+
+            RedisService::set($cacheKey, $info, rand(10, 20));
+        }
+
+        return $info;
+    }
+
+}

+ 3 - 0
app/Services/Common/VideoService.php

@@ -152,6 +152,8 @@ class VideoService extends BaseService
 
         return $datas ? $datas->toArray() : [];
     }
+<<<<<<< HEAD
+=======
 
     /**
      * 获取
@@ -197,4 +199,5 @@ class VideoService extends BaseService
         $this->model->where('mark', 0)->where('update_time', '<=', time() - 7 * 86400)->delete();
         return parent::delete();
     }
+>>>>>>> 9fe3ebdb5f6152e2be13b11c08901349803b4db7
 }

+ 0 - 1
app/Services/MpService.php

@@ -313,7 +313,6 @@ class MpService extends BaseService
 
             $url = sprintf($this->apiUrls['getServiceToken'], $this->mpAppid, $this->mpAppSecret);
             $result = httpRequest($url,'', 'get','',5);
-            var_dump($result);
             $this->saveLog($cacheKey.'kfTokens:request', ['url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]);
             $token = isset($result['access_token'])? $result['access_token'] : '';
             if(empty($result) || empty($token)){

+ 27 - 5
app/Services/PaymentService.php

@@ -111,7 +111,9 @@ class PaymentService extends BaseService
             if ($wxpayPublicCert) {
                 $payConfig['wechat']['default']['mch_public_cert_path'] = $wxpayPublicCert;
             }
-            $payConfig['wechat']['default']['notify_url'] = url('/api/notify/' . $scene . '/10');
+            //$payConfig['wechat']['default']['notify_url'] = url('/api/notify/' . $scene . '/10');
+            $payConfig['wechat']['default']['notify_url'] = url(env('APP_URL').'api/notify/' . $scene . '/10');
+
             $this->config = $payConfig;
 
             return Pay::wechat($payConfig);
@@ -465,6 +467,7 @@ class PaymentService extends BaseService
             }
 
             /* TODO 订单验证和状态处理 */
+            $orderInfo = [];
             // VIP购买
             if ($scene == 'vip') {
                 $orderInfo = OrderModel::with(['vip'])->where(['order_no' => $orderNo, 'mark' => 1])
@@ -493,7 +496,7 @@ class PaymentService extends BaseService
                 }
 
             } // 视频单集购买
-            if ($scene == 'video') {
+            if ($scene == 'course') {
                 $orderInfo = VideoOrderModel::where(['order_no' => $orderNo, 'mark' => 1])
                     ->select(['id as order_id', 'user_id', 'goods_id', 'expired_at', 'order_no', 'total as pay_money', 'pay_at as pay_time', 'remark', 'status'])
                     ->first();
@@ -601,10 +604,29 @@ class PaymentService extends BaseService
                     }
 
                     break;
+                case 'course':
+                    $price = isset($orderInfo['pay_money']) ? $orderInfo['pay_money'] : 0;
+                    $remark = isset($orderInfo['remark']) && $orderInfo['remark'] ? $orderInfo['remark'] : '购买VIP会员';
+                    // 账单记录
+                    $log = [
+                        'user_id' => $orderUserId,
+                        'source_order_no' => $orderNo,
+                        'type' => 2,
+                        'money' => $price,
+                        'date' => date('Y-m-d H:i:s'),
+                        'create_time' => time(),
+                        'remark' => $remark,
+                        'status' => 1,
+                        'mark' => 1
+                    ];
+                    RedisService::set("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}_log", ['order' => $orderInfo, 'log' => $log, 'notify' => $data], 600);
+                    if (!AccountLogModel::insertGetId($log)) {
+                        $this->error = 2635;
+                        return false;
+                    }
+                    break;
                 default:
-                    DB::rollBack();
-                    $this->error = 2631;
-                    return false;
+                    break;
             }
 
             $this->error = 2638;

+ 1 - 0
resources/lang/zh-cn/api.php

@@ -40,6 +40,7 @@ return [
     '1042' => '授权登录错误',
     '1043' => '操作失败,请返回重试~',
     '1045' => '操作失败,该账户已被冻结,请联系客服~',
+    '1046' => '服务器错误',
 
     // 账户
     '2011'=>"授权登录失败,用户账号被冻结",

+ 10 - 2
routes/api.php

@@ -71,10 +71,18 @@ Route::prefix('v1')->middleware('web.login')->group(function() {
     // 答题
     Route::post('/exam/index', [\App\Http\Controllers\Api\v1\ExamController::class, 'index']);
     Route::post('/exam/history', [\App\Http\Controllers\Api\v1\ExamController::class, 'history']);
+    Route::post('/exam/ranks', [\App\Http\Controllers\Api\v1\ExamController::class, 'ranks']);
 
     // 试卷
-    Route::post('/paper/index', [\App\Http\Controllers\Api\v1\MessageController::class, 'index']);
-    Route::post('/paper/subject', [\App\Http\Controllers\Api\v1\MessageController::class, 'subject']);
+    Route::post('/paper/index', [\App\Http\Controllers\Api\v1\PaperController::class, 'index']);
+    Route::post('/paper/info', [\App\Http\Controllers\Api\v1\PaperController::class, 'info']);
+    Route::post('/paper/subject', [\App\Http\Controllers\Api\v1\PaperController::class, 'subject']);
+
+    // 视频课
+    Route::post('/course/index', [\App\Http\Controllers\Api\v1\CourseController::class, 'index']);
+    Route::post('/course/list', [\App\Http\Controllers\Api\v1\CourseController::class, 'list']);
+    Route::post('/course/info', [\App\Http\Controllers\Api\v1\CourseController::class, 'info']);
+
 
 });