// +---------------------------------------------------------------------- namespace App\Services\Api; use App\Models\ExamAccessLogModel; use App\Models\ExamAnswerModel; use App\Models\ExamAnswerTopicModel; use App\Models\ExamPaperModel; use App\Models\ExamTopicModel; use App\Models\MemberAnswerRankModel; use App\Services\BaseService; use App\Services\ConfigService; use App\Services\DeepSeekService; use App\Services\RedisService; use Illuminate\Support\Facades\DB; /** * 答题服务-服务类 * @author laravel开发员 * @since 2020/11/11 * @package App\Services\Api */ class ExamService extends BaseService { // 静态对象 protected static $instance = null; /** * 构造函数 * @author laravel开发员 * @since 2020/11/11 */ public function __construct() { $this->model = new ExamAnswerModel(); } /** * 静态入口 */ 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:exams: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_ids', '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(30, 60)); } 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; $userId = isset($params['user_id']) ? $params['user_id'] : 0; $subjectId = isset($params['subject_id']) ? $params['subject_id'] : 0; if ($userId > 0) { $where['a.user_id'] = $userId; } 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 $params * @param int $pageSize * @return array|mixed */ public function getHistoryList($params, $pageSize = 10) { $page = isset($params['page']) ? $params['page'] : 1; $userId = isset($params['user_id']) ? $params['user_id'] : 0; $cacheKey = "caches:exams:{$userId}_history_{$page}:" . 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.scene_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(10, 20)); } return $datas; } /** * 错题记录 * @param $params * @param int $pageSize * @return array|mixed */ public function getErrorList($params, $pageSize = 10) { $page = isset($params['page']) ? $params['page'] : 1; $userId = isset($params['user_id']) ? $params['user_id'] : 0; $cacheKey = "caches:exams:{$userId}_errors_{$page}:" . md5(json_encode($params)); $datas = RedisService::get($cacheKey); if ($datas) { return $datas; } $prefix = env('DB_PREFIX','lev_'); $query = $this->getQuery($params); $list = $query->with(['error'])->whereRaw("{$prefix}a.accurate_count < {$prefix}a.answer_count")->where('b.id','>',0) ->select(['a.id', 'a.user_id', 'a.paper_id', 'a.score', 'a.accurate_count', 'a.answer_count', 'b.name', 'b.type', 'b.scene_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['error_count'] = $item['answer_count'] - $item['accurate_count']; $item['create_time'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d H.i.s') : ''; $item['error_id'] = isset($item['error']) && isset($item['error']['topic_id'])? $item['error']['topic_id'] : 0; } } $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(10, 20)); } return $datas; } /** * 每日一练目录数据 * @param $userId 用户ID * @param $params * @param $pageSize * @return array|mixed */ public function getPracticeList($userId, $params, $pageSize = 10) { $page = isset($params['page']) ? $params['page'] : 1; $type = isset($params['type']) ? $params['type'] : 1; $sc = isset($params['sc']) ? $params['sc'] : 0; $cacheKey = "caches:exam:{$userId}_practice:{$page}_" . md5(json_encode($params)); $datas = RedisService::get($cacheKey); // 每日一练访问次数统计 if (empty($sc)) { ExamAccessLogModel::saveLog(date('Y-m-d'), $type, 1); } if ($datas) { return $datas; } $list = $this->model->from('exam_answers as a') ->leftJoin('exam_papers as b', 'b.id', '=', 'a.paper_id') ->where('b.topic_count', '>', 0) ->where(['b.scene_type' => 1, 'b.status' => 1, 'a.status' => 1, 'b.mark' => 1, 'a.mark' => 1]) ->where(function ($query) use ($params) { $type = isset($params['type']) && $params['type'] ? intval($params['type']) : 1; if ($type > 0) { $query->where('b.type', $type); } }) ->select(['a.*', 'b.name', 'b.scene_type', 'b.type', 'b.score_total', 'b.topic_count']) ->orderBy('a.create_time', 'desc') ->paginate($pageSize > 0 ? $pageSize : 9999999); $list = $list ? $list->toArray() : []; if ($list) { foreach ($list['data'] as &$item) { $item['date'] = $item['create_time'] ? datetime($item['create_time'], 'Y-m-d') : ''; } } // 今日是否练习过 $rows = isset($list['data']) ? $list['data'] : []; $first = isset($rows[0]) ? $rows[0] : []; $firstTime = isset($first['date']) ? $first['date'] : 0; if ($page == 1 && strtotime($firstTime) < strtotime(date('Y-m-d'))) { $type = isset($params['type']) && $params['type'] ? intval($params['type']) : 1; $data = PaperService::make()->getRandomPaper($userId, $type, 1); if ($data) { $rows = array_merge([$data], $rows); } } $datas = [ 'pageSize' => $pageSize, 'total' => isset($list['total']) ? $list['total'] : 0, 'list' => $rows ]; if ($rows) { RedisService::set($cacheKey, $datas, rand(10, 20)); } return $datas; } /** * 答题排行榜 * @param $type 1-日,2-周(7天),3-月 * @param int $num * @return array|mixed */ public function getRankByType($type, $num = 0) { $num = $num ? $num : ConfigService::make()->getConfigByCode('rank_num', 50); $cacheKey = "caches:exams:ranks:{$type}_{$num}"; $datas = RedisService::get($cacheKey); if ($datas) { return $datas; } $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')); } }) ->select(['a.id', 'a.user_id', 'b.avatar', 'b.nickname', 'a.answer_time', 'a.answer_count', DB::raw("sum({$prefix}a.answer_time) as answer_times"), DB::raw("sum({$prefix}a.answer_count) as count")]) ->groupBy('a.user_id') ->orderByRaw("sum({$prefix}a.answer_time) desc") ->take($num) ->get(); $datas = $datas ? $datas->toArray() : []; if ($datas) { foreach ($datas as &$item) { $item['avatar'] = $item['avatar'] ? get_image_url($item['avatar']) : ''; $item['answer_hour'] = $item['answer_times'] > 3600 ? intval($item['answer_times'] / 3600) . 'h' : ($item['answer_times'] > 60 ? intval($item['answer_times'] / 60) . 'm' : $item['answer_times'] . 's'); } RedisService::set($cacheKey, $datas, rand(20, 30)); } return $datas; } /** * 获取答题卡列表题目数据 * @param $userId 用户 * @param $paperId * @param int $rid * @return array|mixed */ public function getCardList($userId, $paperId, $rid = 0) { $cacheKey = "caches:exams:{$userId}_cardList:{$paperId}_{$rid}"; $datas = RedisService::get($cacheKey); if ($datas) { return $datas; } $datas = ExamTopicModel::from('exam_topics as a') ->leftJoin('exam_answers_topics as b', function ($join) use ($userId, $rid) { $join->on('b.topic_id', '=', 'a.id')->where(['b.user_id' => $userId, 'b.answer_log_id' => $rid, 'b.status' => 1, 'b.mark' => 1]); }) ->where(['a.paper_id' => $paperId, 'a.status' => 1, 'a.mark' => 1]) ->select(['a.id', 'a.topic_name', 'a.correct_answer', 'b.answer', 'a.score', 'a.paper_id', 'a.topic_type', 'b.answer_log_id as rid', 'b.answer as submit_answer', 'b.accurate', 'b.score as submit_score']) ->orderBy('a.sort', 'desc') ->orderBy('a.id', 'asc') ->get(); $datas = $datas ? $datas->toArray() : []; $list = []; if ($datas) { foreach ($datas as &$item) { $item['rid'] = !empty($item['rid']) && $item['rid'] ? $item['rid'] : $rid; $item['submit_answer'] = isset($item['submit_answer']) ? $item['submit_answer'] : ''; $item['accurate'] = isset($item['accurate']) ? $item['accurate'] : -1; $item['submit_score'] = isset($item['submit_score']) ? $item['submit_score'] : 0; if ($item['topic_type']) { $list[$item['topic_type']][] = $item; } } RedisService::set($cacheKey, $list, rand(5, 10)); } return $list; } /** * 重新答题,清除答题记录数据 * @param $id * @return bool */ public function reset($id, $userId = 0) { $log = $this->model->where(['id' => $id, 'mark' => 1])->first(); if (empty($log)) { return true; } $updateData = ['score' => 0, 'accurate_count' => 0, 'answer_count' => 0, 'answer_times' => 0, 'answer_last_id' => 0, 'is_submit' => 0, 'status' => 1]; $this->model->where(['id' => $id, 'mark' => 1])->update($updateData); ExamAnswerTopicModel::where(['answer_log_id' => $id, 'mark' => 1])->update(['status' => 2, 'update_time' => time()]); RedisService::keyDel("caches:exams:{$userId}*"); return true; } /** * 答题 * @param $userId * @param $params * @return array|false */ public function answer($userId, $params) { $paperId = isset($params['id']) ? $params['id'] : 0; $rid = isset($params['rid']) ? $params['rid'] : 0; $tid = isset($params['tid']) ? $params['tid'] : 0; $isSubmit = isset($params['is_submit']) ? $params['is_submit'] : 1; $answer = isset($params['answer']) && $params['answer']? str_replace("\n",'\n', $params['answer']) : ''; $answerImage = isset($params['answer_image']) ? $params['answer_image'] : ''; $answerType = isset($params['answer_type']) && $params['answer_type'] ? $params['answer_type'] : 2; $remainTime = isset($params['remain_time']) && $params['remain_time'] ? $params['remain_time'] : 0; if ($isSubmit <= 0 && $answerType == 2 && empty($answer)) { $this->error = '请先提交答案'; return false; } if ($isSubmit <= 0 && $answerType == 1 && empty($answerImage)) { $this->error = '请先上传图片答案'; return false; } $cacheKey = "caches:answers:{$userId}_{$paperId}:{$tid}_{$rid}"; if (RedisService::get($cacheKey . '_lock')) { $this->error = '请不要频繁提交'; return false; } RedisService::set($cacheKey . '_lock', $params, rand(2, 3)); // 试卷数据 $paperInfo = ExamPaperModel::where(['id' => $paperId, 'status' => 1, 'mark' => 1]) ->first(); $sceneType = isset($paperInfo['scene_type']) && $paperInfo['scene_type'] ? $paperInfo['scene_type'] : 1; if (empty($paperInfo)) { RedisService::clear($cacheKey . '_lock'); $this->error = '试题数据错误,请返回刷新重试'; return false; } // 题目数据 $topicInfo = ExamTopicModel::where(['id' => $tid, 'paper_id' => $paperId, 'status' => 1, 'mark' => 1])->first(); $topicType = isset($topicInfo['topic_type']) ? trim($topicInfo['topic_type']) : ''; $topicScore = isset($topicInfo['score']) ? intval($topicInfo['score']) : 0; $topicName = isset($topicInfo['topic_name']) && $topicInfo['topic_name']? str_replace("\n",'\n', $topicInfo['topic_name']) : ''; $topicShowType = isset($topicInfo['show_type']) ? $topicInfo['show_type'] : 1; $correctAnswer = isset($topicInfo['correct_answer']) ? $topicInfo['correct_answer'] : ''; if (empty($topicInfo) || empty($topicType) || empty($topicName)) { RedisService::clear($cacheKey . '_lock'); $this->error = '题库已更新,请返回刷新重试'; return false; } // 答题记录 $submit = 0; $answerTimes = 0; $answerCount = 0; if ($rid) { $answerInfo = ExamAnswerModel::where(['id' => $rid, 'user_id' => $userId, 'status' => 1, 'mark' => 1])->first(); $submit = isset($answerInfo['is_submit']) ? $answerInfo['is_submit'] : 0; $answerCount = isset($answerInfo['answer_count']) ? $answerInfo['answer_count'] : 0; $answerTimes = isset($answerInfo['answer_times']) ? $answerInfo['answer_times'] : 0; if (empty($answerInfo)) { $rid = 0; } if ($submit) { RedisService::clear($cacheKey . '_lock'); $this->error = '您已交卷'; return false; } } // 验证答案内容类型和数据 // 每日一练 if ($sceneType == 1 && $rid <= 0) { // 今日记录 $answerInfo = ExamAnswerModel::where(['paper_id' => $paperId, 'status' => 1, 'mark' => 1])->where('create_time', '>=', strtotime(date('Y-m-d')))->first(); $rid = isset($answerInfo['id']) ? $answerInfo['id'] : 0; $submit = isset($answerInfo['is_submit']) ? $answerInfo['is_submit'] : 0; $answerCount = isset($answerInfo['answer_count']) ? $answerInfo['answer_count'] : 0; } // 是否已交卷 if ($submit == 1) { RedisService::clear($cacheKey . '_lock'); $this->error = '您已交卷'; return false; } // 直接交卷 $totalTime = ConfigService::make()->getConfigByCode('answer_total_time', 1800); $answerTime = $totalTime > $remainTime ? $totalTime - $remainTime : $totalTime; $newAnswerTime = $answerTime > $answerTimes ? $answerTime - $answerTimes : 0; if ($isSubmit == 1 && empty($answer) && empty($answerImage)) { // 是否提交过 if ($answerCount <= 0) { RedisService::clear($cacheKey . '_lock'); $this->error = '您未提交过答案,请先答题再交卷'; return false; } $this->model->where(['id' => $rid])->update(['is_submit' => 1, 'update_time' => time()]); $this->error = '交卷成功'; RedisService::keyDel("caches:exams:{$userId}*"); RedisService::clear($cacheKey . '_lock'); return ['rid' => $rid, 'paper_id' => $paperId]; } // 该题是否已提交答案 $logId = 0; if ($rid > 0) { $answerTopic = ExamAnswerTopicModel::where(['answer_log_id' => $rid, 'user_id' => $userId, 'topic_id' => $tid, 'mark' => 1])->first(); $accurate = isset($answerTopic['accurate']) ? $answerTopic['accurate'] : -1; $logId = isset($answerTopic['id']) ? $answerTopic['id'] : 0; $status = isset($answerTopic['status']) ? $answerTopic['status'] : 0; if ($answerTopic && $accurate >= 0 && $status == 1) { RedisService::clear($cacheKey . '_lock'); $this->error = '该题答案已提交'; return false; } } /* TODO 验证答案 */ $submitType = 1; $topicData = [ 'user_id' => $userId, 'topic_id' => $tid, 'answer' => $answerType == 1 ? get_image_path($answerImage) : $answer, 'score' => 0, 'accurate' => 0, 'create_time' => time(), 'status' => 1, 'mark' => 1, ]; if (in_array($topicType, ['选择题', '单选题', '多选题', '填空题'])) { $topicData['answer_analysis'] = isset($topicInfo['answer_analysis']) ? $topicInfo['answer_analysis'] : ''; if ($answer == $correctAnswer) { $topicData['accurate'] = 1; $topicData['score'] = $topicScore; } else { $topicData['accurate'] = 0; } // }else if (in_array($topicType, ['简答题','计算题','阅读理解'])){ } else { // 图片答案AI验证 if ($answerType == 1) { if (empty($answerImage)) { $this->error = '请上传图片答案~'; return false; } $submitType = 3; $apiData = [ 'answer' => DeepSeekService::make()->getImageTopicData($answerImage), 'score' => $topicScore, 'topic' => $topicShowType == 2 && $topicName ? DeepSeekService::make()->getImageTopicData($topicName) : $topicName, ]; //dump($apiData); $result = DeepSeekService::make()->apiRequest($apiData); $score = isset($result['score']) ? $result['score'] : 0; $analysis = isset($result['analysis']) ? $result['analysis'] : ''; if ($score > 0) { $topicData['accurate'] = 1; $topicData['score'] = $score; $topicData['answer_analysis'] = $analysis; } else { $topicData['accurate'] = 0; $topicData['answer_analysis'] = $apiData['answer']; } } else { $submitType = 2; $apiData = [ 'answer' => $answer, 'score' => $topicScore, 'topic' => $topicShowType == 2 && $topicName ? DeepSeekService::make()->getImageTopicData($topicName) : $topicName, ]; $result = DeepSeekService::make()->apiRequest($apiData); $score = isset($result['score']) ? $result['score'] : 0; $analysis = isset($result['analysis']) ? $result['analysis'] : ''; if ($score > 0) { $topicData['accurate'] = 1; $topicData['score'] = $score; $topicData['answer_analysis'] = $analysis; } else { $topicData['accurate'] = 0; } } } DB::beginTransaction(); if ($rid) { $log = [ 'answer_times' => $answerTime, 'answer_last_at' => date('Y-m-d H:i:s'), 'answer_count' => DB::raw("answer_count + 1"), 'answer_last_id' => $tid, 'is_submit' => $isSubmit == 1 ? 1 : 0, 'update_time' => time() ]; // 对题数 if ($topicData['accurate'] == 1) { $log['accurate_count'] = DB::raw("accurate_count + 1"); $log['score'] = DB::raw("score + {$topicData['score']}"); } if (!$this->model->where(['id' => $rid])->update($log)) { DB::rollBack(); RedisService::clear($cacheKey . '_lock'); $this->error = '答题失败,请刷新后重新提交'; return false; } } else { $log = [ 'user_id' => $userId, 'paper_id' => $paperId, 'score' => $topicData['score'], 'answer_count' => 1, 'answer_times' => $answerTime, 'answer_last_at' => date('Y-m-d H:i:s'), 'answer_last_id' => $tid, 'is_submit' => $isSubmit, 'create_time' => time() ]; if (!$rid = $this->model->insertGetId($log)) { DB::rollBack(); RedisService::clear($cacheKey . '_lock'); $this->error = '答题失败,请刷新后重新提交'; return false; } } // 答题题目数据 $topicData['answer_type'] = $submitType; $topicData['answer_log_id'] = $rid; if ($logId) { $topicData['update_time'] = $submitType; ExamAnswerTopicModel::where(['id' => $logId])->update($topicData); } else if (!ExamAnswerTopicModel::insert($topicData)) { DB::rollBack(); RedisService::clear($cacheKey . '_lock'); $this->error = '答题失败,请刷新后重新提交'; return false; } DB::commit(); // 答题时间统计 if ($newAnswerTime > 0) { $id = MemberAnswerRankModel::where(['user_id' => $userId, 'date' => date('Y-m-d'), 'mark' => 1])->value('id'); if ($id) { MemberAnswerRankModel::where(['id' => $id])->update(['answer_time' => DB::raw("answer_time + {$newAnswerTime}"), 'answer_count' => DB::raw("answer_count + 1"), 'update_time' => time()]); } else { MemberAnswerRankModel::insert(['user_id' => $userId, 'answer_time' => $answerTime, 'answer_count' => 1, 'date' => date('Y-m-d'), 'create_time' => time()]); } } $this->error = '答题成功'; RedisService::clear($cacheKey . '_lock'); RedisService::clear("caches:exams:info_{$rid}"); RedisService::keyDel("caches:exams:ranks*"); RedisService::keyDel("caches:exams:{$userId}*"); RedisService::keyDel("caches:paper:list*"); RedisService::keyDel("caches:paper:info_{$userId}:p{$paperId}*"); return ['paper_id' => $paperId, 'rid' => $rid, 'tid' => $tid, 'accurate' => $topicData['accurate'], 'score' => $topicData['score']]; } /** * 是否有答题记录数据 * @param $rid * @param $tid * @return array|mixed */ public function getAnswerCacheTopic($rid, $tid) { $cacheKey = "caches:answers:topic_{$rid}_{$tid}"; $data = RedisService::get($cacheKey); if ($data) { return $data; } $data = ExamAnswerTopicModel::where(['rid' => $rid, 'topic_id' => $tid, 'mark' => 1])->value('id'); if ($data) { RedisService::set($cacheKey, $data, rand(5, 10)); } return $data; } /** * 答题分数结果详情 * @param $rid * @return array|mixed */ public function getInfo($rid) { $cacheKey = "caches:exams:info_{$rid}"; $data = RedisService::get($cacheKey); if ($data) { return $data; } $data = $this->model->from('exam_answers as a') ->leftJoin('exam_papers as b', 'b.id', '=', 'a.paper_id') ->where(['a.id' => $rid, 'a.status' => 1, 'a.mark' => 1]) ->select(['a.*', 'b.topic_count', 'b.scene_type']) ->first(); $data = $data ? $data->toArray() : []; if ($data) { $data['accurate_rate'] = round($data['accurate_count'] / $data['topic_count'] * 100, 2); $data['answer_times_text'] = $data['answer_times'] ? format_times($data['answer_times']) : '00:00'; RedisService::set($cacheKey, $data, rand(5, 10)); } return $data; } }