// +---------------------------------------------------------------------- namespace App\Services\Api; use App\Models\ExamAccessLogModel; use App\Models\ExamAnswerModel; use App\Models\ExamAnswerTopicModel; use App\Models\ExamErrorModel; use App\Models\ExamPaperModel; use App\Models\ExamTopicModel; use App\Services\BaseService; use App\Services\ConfigService; use App\Services\RedisService; use Illuminate\Support\Facades\DB; /** * 试卷服务-服务类 * @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); // 访问次数统计 $sc = isset($params['sc']) ? $params['sc'] : 0; $type = isset($params['type']) ? $params['type'] : 0; $sceneType = isset($params['scene_type']) ? $params['scene_type'] : 0; if (empty($sc) && !in_array($sceneType, [3, 4, 6])) { ExamAccessLogModel::saveLog(date('Y-m-d'), $type, $sceneType); } if ($datas) { return $datas; } $userId = isset($params['user_id']) ? $params['user_id'] : 0; $query = $this->getQuery($params); $list = $query->with(['answer' => function ($query) use ($userId) { $query->where('user_id', $userId); }]) ->where('a.topic_count', '>', 0) ->select(['a.*', 'b.id as rid', 'b.score', 'b.accurate_count', 'b.user_id', 'b.answer_times', 'b.answer_count', 'b.is_submit']) ->groupBy('a.id') ->orderBy('a.sort', 'desc')->orderBy('a.id', 'desc') ->paginate($pageSize > 0 ? $pageSize : 9999999); $list = $list ? $list->toArray() : []; if ($list) { foreach ($list['data'] as &$item) { $item['score'] = !empty($item['score']) ? $item['score'] : 0; $item['is_submit'] = !empty($item['is_submit']) ? $item['is_submit'] : 0; $item['answer_count'] = !empty($item['answer_count']) ? $item['answer_count'] : 0; $item['accurate_count'] = !empty($item['accurate_count']) ? $item['accurate_count'] : 0; $item['date'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d') : ''; } } $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 * @param int $pageSize * @return array */ public function getPracticeList($params, $pageSize = 15) { $page = isset($params['page']) ? $params['page'] : 1; $cacheKey = "caches:paper:practice_list_{$page}_{$pageSize}:" . md5(json_encode($params)); $datas = RedisService::get($cacheKey); // 访问次数统计 $sc = isset($params['sc']) ? $params['sc'] : 0; $type = isset($params['type']) ? $params['type'] : 0; if (empty($sc)) { ExamAccessLogModel::saveLog(date('Y-m-d'), $type, 1); } if ($datas) { return $datas; } $userId = isset($params['user_id']) ? $params['user_id'] : 0; $query = $this->getQuery($params); $list = $query->with(['answer' => function ($query) use ($userId) { $query->where('user_id', $userId); }]) ->where('a.topic_count', '>', 0) ->select(['a.*', 'b.id as rid', 'b.score', 'b.accurate_count', 'b.user_id', 'b.answer_times', 'b.answer_count', 'b.is_submit']) ->groupBy('a.id') ->orderBy('a.sort', 'desc')->orderBy('a.id', 'desc') ->paginate($pageSize > 0 ? $pageSize : 9999999); $list = $list ? $list->toArray() : []; if ($list) { foreach ($list['data'] as &$item) { $item['score'] = !empty($item['score']) ? $item['score'] : 0; $item['is_submit'] = !empty($item['is_submit']) ? $item['is_submit'] : 0; $item['answer_count'] = !empty($item['answer_count']) ? $item['answer_count'] : 0; $item['accurate_count'] = !empty($item['accurate_count']) ? $item['accurate_count'] : 0; $item['date'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d') : ''; } } $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 = ['a.status' => 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['a.type'] = $type; } if ($sceneType > 0) { $where['a.scene_type'] = $sceneType; } if ($subjectId > 0) { $where['a.subject_id'] = $subjectId; } $userId = isset($params['user_id']) ? $params['user_id'] : 0; return $this->model->from('exam_papers as a') ->leftJoin('exam_answers as b', function ($join) use ($userId) { // 未完成交卷的答题数据 $join->on('b.paper_id', '=', 'a.id')->where(['b.user_id' => $userId, 'b.status' => 1, 'b.mark' => 1]); }) ->where($where) ->where(function ($query) use ($params) { $keyword = isset($params['keyword']) ? $params['keyword'] : ''; if ($keyword) { $query->where('a.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', 'answer_last_id']) ->first(); $data = $data ? $data->toArray() : []; if ($data) { RedisService::set($cacheKey, $data, rand(5, 10)); } return $data; } /** * 用户最近答题记录 * @param $userId * @param $paperId * @return array|mixed */ public function getLastAnswerLogId($userId, $paperId) { $cacheKey = "caches:paper:log_{$userId}_{$paperId}"; $id = RedisService::get($cacheKey); if ($id) { return $id; } $id = ExamAnswerModel::where(['user_id' => $userId, 'paper_id' => $paperId, 'status' => 1, 'mark' => 1]) ->where('create_time', '>=', strtotime(date('Y-m-d'))) ->value('id'); if ($id) { RedisService::set($cacheKey, $id, rand(5, 10)); } return $id; } /** * 获取详情 * @param $id * @return array|mixed */ public function getInfo($userId, $paperId, $params = []) { $lid = isset($params['lid']) ? intval($params['lid']) : 0; $tid = isset($params['tid']) ? intval($params['tid']) : 0; $rid = isset($params['rid']) ? intval($params['rid']) : 0; $type = isset($params['type']) ? intval($params['type']) : 1; $sc = isset($params['sc']) ? intval($params['sc']) : 0; $cacheKey = "caches:paper:info_{$userId}:p{$paperId}_t{$tid}_r{$rid}_l{$lid}_{$sc}"; $info = RedisService::get($cacheKey); if ($info) { return $info; } // 若进行答题 if ($rid <= 0) { if ($type != 1) { // 判断N分钟内是否有未交卷的答题 $lastAnswerInfo = $this->getLastAnswer($userId, $paperId, 0); $rid = isset($lastAnswerInfo['id']) ? $lastAnswerInfo['id'] : 0; } else { $rid = $this->getLastAnswerLogId($userId, $paperId); } } $where = ['a.id' => $paperId, 'a.status' => 1, 'a.mark' => 1]; $info = $this->model->from('exam_papers as a') ->leftJoin('exam_answers as b', function ($join) use ($rid, $userId) { $join->on('b.paper_id', '=', 'a.id')->where(['b.id' => $rid, 'b.user_id' => $userId, 'b.status' => 1]); }) ->leftJoin('exam_subjects as c','c.id','=','a.subject_id') ->where($where) ->select(['a.id as paper_id','c.answer_times as paper_answer_times', 'b.id as rid', 'b.score', 'b.accurate_count', 'b.is_submit', 'b.answer_count', 'b.answer_last_id', 'b.answer_times', 'a.name', 'a.type', 'a.scene_type', 'a.subject_id', 'a.score_total', 'a.topic_count', 'a.total_time', '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') : ''; $info['answer_count'] = isset($info['answer_count']) ? intval($info['answer_count']) : 0; $info['accurate_count'] = isset($info['accurate_count']) ? intval($info['accurate_count']) : 0; $info['score'] = isset($info['score']) ? floatval($info['score']) : 0; $info['rid'] = isset($info['rid']) ? intval($info['rid']) : 0; // 剩余时间 $isSubmit = isset($info['is_submit']) ? $info['is_submit'] : 0; $totalTime = isset($info['paper_answer_times']) && $info['paper_answer_times']? intval($info['paper_answer_times']) : 0; $totalTime = $totalTime > 0 ? $totalTime : ConfigService::make()->getConfigByCode('answer_total_time', 60); $totalTime = $totalTime? $totalTime * 60 : 3600; $info['answer_times'] = isset($info['answer_times']) ? intval($info['answer_times']) : 0; $info['remain_time'] = $totalTime > $info['answer_times'] ? $totalTime - $info['answer_times'] : 0; $info['remain_time'] = $isSubmit ? 0 : $info['remain_time']; $info['remain_time_text'] = $info['remain_time'] ? format_times($info['remain_time']) : '00:00'; $info['progress'] = $info['topic_count'] ? intval($info['answer_count'] / $info['topic_count'] * 100) : 0; $info['fee_num'] = (int)ConfigService::make()->getConfigByCode('fee_answer_num',10); // 当前题目 //$prefix = env('DB_PREFIX','_lev'); $model = ExamTopicModel::from('exam_topics as a') ->leftJoin('exam_answers_topics as b', function ($join) use ($rid, $userId) { // 是否有最近答题记录 $join->on('b.topic_id', '=', "a.id")->where("b.answer_log_id", '=', $rid)->where(['b.user_id' => $userId, 'b.status' => 1, "b.mark" => 1]); }) ->where(['a.paper_id' => $paperId, 'a.status' => 1, 'a.mark' => 1]) ->where(function ($query) use ($tid) { // 答题卡选择的题目,否则默认按题目排序返回第一题 if ($tid > 0) { $query->where('a.id', $tid); } }); $info['topic'] = $model->select(['a.*', 'b.id as answer_topic_id', 'b.answer_log_id', 'b.answer as submit_answer', 'b.answer_analysis as submit_answer_analysis', 'b.answer_type as submit_answer_type', 'b.score as submit_score', 'b.accurate']) ->orderBy('a.sort', 'desc') ->orderBy('a.id', 'asc') ->first(); $info['topic'] = $info['topic']? $info['topic']->toArray() : []; $topicId = isset($info['topic']['id'])? $info['topic']['id'] : 0; $sort = isset($info['topic']['sort'])? $info['topic']['sort'] : 0; if($info['topic'] && $topicId) { $info['topic']['accurate'] = isset($info['topic']['accurate'])?$info['topic']['accurate'] : -1; $info['topic']['submit_score'] = isset($info['topic']['submit_score'])?floatval($info['topic']['submit_score']) : 0; $info['topic']['topic_analysis'] = isset($info['topic']['topic_analysis'])?$info['topic']['topic_analysis'] : ''; $info['topic']['answer_analysis'] = isset($info['topic']['answer_analysis'])?$info['topic']['answer_analysis'] : ''; $info['topic']['topic_analysis'] = $info['topic']['topic_analysis']?$info['topic']['topic_analysis'] : $info['topic']['answer_analysis']; $info['topic']['submit_answer'] = isset($info['topic']['submit_answer'])?$info['topic']['submit_answer'] : ''; $info['topic']['submit_answer_type'] = isset($info['topic']['submit_answer_type'])?$info['topic']['submit_answer_type'] : 1; $info['topic']['submit_answers'] = ''; $info['topic']['correct_answers'] = []; if ($info['topic']['submit_answer_type'] == 3) { $info['topic']['submit_answer'] = get_image_url($info['topic']['submit_answer']); } else if ($info['topic']['submit_answer_type'] == 4) { $info['topic']['submit_answer'] = $info['topic']['submit_answer'] ? json_decode($info['topic']['submit_answer'], true) : []; } // else { // $info['topic']['submit_answer'] = format_content($info['topic']['submit_answer']); // } $info['topic']['correct_show_type'] = 2; // 文本 if (preg_match("/(\/images\/|\/temp\/)/", $info['topic']['correct_answer'])) { $info['topic']['correct_show_type'] = 1; // 图片 $info['topic']['correct_answer'] = get_image_url($info['topic']['correct_answer']); } $info['topic']['topic_analysis'] = format_content($info['topic']['topic_analysis']); // $info['topic']['topic_analysis'] = html_entity_decode($info['topic']['topic_analysis']); if ($info['topic']['show_type'] == 1) { $info['topic']['topic_name'] = format_content($info['topic']['topic_name']); } else if ($info['topic']['show_type'] == 2) { $info['topic']['topic_name'] = get_image_url($info['topic']['topic_name']); // 已经答题,返回答案 if ($rid > 0 || $info['topic']['submit_answer']) { //$info['topic']['topic_analysis'] = get_image_url($info['topic']['topic_analysis']); if (preg_match("/(images|temp)/", $info['topic']['answer_A'])) { $info['topic']['answer_A'] = get_image_url($info['topic']['answer_A']); } if (preg_match("/(images|temp)/", $info['topic']['answer_B'])) { $info['topic']['answer_B'] = get_image_url($info['topic']['answer_B']); } if (preg_match("/(images|temp)/", $info['topic']['answer_C'])) { $info['topic']['answer_C'] = get_image_url($info['topic']['answer_C']); } if (preg_match("/(images|temp)/", $info['topic']['answer_D'])) { $info['topic']['answer_D'] = get_image_url($info['topic']['answer_D']); } if (preg_match("/(images|temp)/", $info['topic']['answer_E'])) { $info['topic']['answer_E'] = get_image_url($info['topic']['answer_E']); } if (preg_match("/(images|temp)/", $info['topic']['answer_F'])) { $info['topic']['answer_F'] = get_image_url($info['topic']['answer_F']); } } } // 多选题 $topicType = isset($info['topic']['topic_type']) ? $info['topic']['topic_type'] : ''; if ($topicType == '多选题') { $info['topic']['submit_answers'] = $info['topic']['submit_answer'] ? explode(',', $info['topic']['submit_answer']) : ''; $info['topic']['correct_answer'] = str_replace([',',',','、'],['','',''], $info['topic']['correct_answer']); $info['topic']['correct_answers'] = $info['topic']['correct_answer'] ? str_split($info['topic']['correct_answer']) : []; }else if(in_array($topicType,['综合题','问答题'])){ $info['topic']['correct_answer'] = format_content($info['topic']['correct_answer']); } $info['topic']['answers'] = []; if ($info['topic']['answer_A']!=='') { $info['topic']['answers'][] = ['code' => 'A', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_A']):$info['topic']['answer_A']]; } if ($info['topic']['answer_B'] !== '') { $info['topic']['answers'][] = ['code' => 'B', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_B']):$info['topic']['answer_B']]; } if ($info['topic']['answer_C'] !=='') { $info['topic']['answers'][] = ['code' => 'C', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_C']):$info['topic']['answer_C']]; } if ($info['topic']['answer_D']!=='') { $info['topic']['answers'][] = ['code' => 'D', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_D']):$info['topic']['answer_D']]; } if ($info['topic']['answer_E']!=='') { $info['topic']['answers'][] = ['code' => 'E', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_E']):$info['topic']['answer_E']]; } if ($info['topic']['answer_F']!=='') { $info['topic']['answers'][] = ['code' => 'F', 'value' => in_array($topicType,['单选题','选择题','多选题'])?format_content($info['topic']['answer_F']):$info['topic']['answer_F']]; } // 上一题 $info['last'] = ExamTopicModel::where(['paper_id'=> $paperId,'status'=>1,'mark'=>1]) ->where('sort','>', $sort) ->select(['id','sort','topic_name']) ->orderBy('sort','asc') ->orderBy('id','asc') ->first(); $info['last'] = $info['last'] ? $info['last']->toArray() : ['id' => 0]; // 下一题 $info['next'] = ExamTopicModel::where(['paper_id'=> $paperId,'status'=>1,'mark'=>1]) ->where('sort','<', $sort) ->select(['id','sort','topic_name']) ->orderBy('sort','desc') ->orderBy('id','desc') ->first(); $info['next'] = $info['next'] ? $info['next']->toArray() : ['id' => 0]; } RedisService::set($cacheKey, $info, rand(20, 30)); } // $lockCacheKey = "caches:paper:read_{$userId}:p{$paperId}_lock"; // if($sc<=0 && !RedisService::get($lockCacheKey)){ // // 统计答题次数 // $this->model->where(['id'=>$paperId])->update(['answer_counts'=>DB::raw("answer_counts + 1"),'update_time'=>time()]); // RedisService::set($lockCacheKey, ['id'=>$paperId,'user_id'=>$userId], rand(3,5)); // } return $info; } /** * 获取随机试卷 * @param $userId 用户 * @param $type 试卷类型 * @param $sceneType 场景 * @param false $refresh * @return array|mixed */ public function getRandomPaper($userId, $type, $sceneType, $refresh = false) { $cacheKey = "caches:papers:random_{$userId}:{$type}_{$sceneType}"; $data = RedisService::get($cacheKey); if ($data && !$refresh) { return $data; } $data = $this->model->where(['type'=>$type,'scene_type'=>$sceneType,'status'=>1,'mark'=>1]) ->where('topic_count','>',0) ->orderByRaw('RAND()') ->first(); $data = $data ? $data->toArray() : []; if ($data) { $data['score'] = 0; $data['paper_id'] = $data['id']; $data['accurate_count'] = 0; $data['date'] = date('Y-m-d'); $data['answer_count'] = 0; $data['answer_times'] = 0; RedisService::set($cacheKey, $data, rand(10, 20)); } return $data; } /** * 纠错 * @param $userId * @param $params * @return bool */ public function submitError($userId, $params) { $id = isset($params['id']) ? $params['id'] : 0; $rid = isset($params['rid']) ? $params['rid'] : 0; $type = isset($params['type']) ? $params['type'] : 0; $moduleType = isset($params['module_type']) ? $params['module_type'] : 1; $description = isset($params['description']) ? $params['description'] : ''; if ($id <= 0 || $moduleType <= 0) { $this->error = '参数错误'; return false; } if ($type <= 0) { $this->error = '请选择错误类型'; return false; } if (empty($description)) { $this->error = '请输入错误描述'; return false; } $limitTime = ConfigService::make()->getConfigByCode('limit_exam_error_submit', 5); $cacheKey = "caches:paper:errors_{$userId}:{$id}_{$rid}"; if (RedisService::get($cacheKey) && $limitTime > 0) { $this->error = '您近期已提交过该题错误,请稍后再试~'; return false; } $paperInfo = $this->model->where(['id' => $id, 'mark' => 1])->first(); if (empty($paperInfo)) { $this->error = '试题不存在'; return false; } if ($limitTime > 0 && $logId = ExamErrorModel::where(['user_id' => $userId, 'rid' => $rid, 'paper_id' => $id, 'mark' => 1])->where('create_time', '>=', time() - $limitTime * 60)->value('id')) { $this->error = '您近期已提交过该题错误,请稍后再试~'; RedisService::set($cacheKey, ['id' => $logId, 'paper_id' => $id], $limitTime * 60); return false; } $data = [ 'user_id' => $userId, 'paper_id' => $id, 'rid' => $rid, 'type' => $type, 'module_type' => $moduleType, 'description' => $description, 'image_url' => isset($params['image_url']) && $params['image_url'] ? get_image_path($params['image_url']) : '', 'create_time' => time(), 'status' => 1, 'mark' => 1 ]; if (!$logId = ExamErrorModel::insertGetId($data)) { $this->error = '提交失败'; return false; } $this->error = '提交成功'; RedisService::set($cacheKey, $data, $limitTime * 60); return ['id' => $logId, 'paper_id' => $id]; } }