model = new \app\admin\model\app\exam\WarehouseQuestion; $this->warehouseGroupModel = new \app\admin\model\app\exam\WarehouseGroup; } /** * 校验题目数据 * @param string $type 题型 * @param array $option 选项 * @param mixed $answer 答案 */ protected function validateQuestionData($type, $option, $answer) { switch ($type) { case 'fillblank': if (empty($answer) || !is_array($answer)) { $this->error('填空题答案为空,请填写至少一个填空答案'); } $hasValidBlank = false; foreach ($answer as $blankAnswers) { if (is_array($blankAnswers) && !empty($blankAnswers)) { $hasValidBlank = true; break; } } if (!$hasValidBlank) { $this->error('填空题答案为空,请填写至少一个填空答案'); } break; case 'indefinite': if (empty($option) || !is_array($option) || count($option) < 2) { $this->error('不定项选择题选项不足,请添加至少 2 个选项'); } if (empty($answer) || !is_array($answer)) { $this->error('不定项选择题答案为空,请选择正确答案'); } break; case 'essay': if (empty($answer) || !is_string($answer)) { $this->error('问答题参考答案为空,请填写参考答案'); } break; } } /** * 将请求中的答案统一转换为便于校验的结构 * @param string $type 题型 * @param mixed $answer 请求中的答案 * @return mixed */ protected function normalizeAnswerFromRequest($type, $answer) { if (in_array($type, ['multiple', 'indefinite', 'fillblank'])) { return is_array($answer) ? $answer : []; } return $answer; } /** * 将选项格式化为数据库存储格式 * @param string $type 题型 * @param mixed $option 选项 * @return string */ protected function formatOptionForStorage($type, $option) { return json_encode(is_array($option) ? $option : []); } /** * 将答案格式化为数据库存储格式 * @param string $type 题型 * @param mixed $answer 答案 * @return string */ protected function formatAnswerForStorage($type, $answer) { switch ($type) { case 'multiple': case 'indefinite': return is_array($answer) ? implode(',', $answer) : (string)$answer; case 'fillblank': return is_array($answer) ? json_encode($answer) : (string)$answer; case 'essay': return (string)$answer; case 'single': case 'judge': default: return (string)$answer; } } /** * 将数据库中的答案格式化为接口返回格式 * @param string $type 题型 * @param mixed $answer 数据库中的答案 * @return mixed */ protected function formatAnswerForDetail($type, $answer) { switch ($type) { case 'multiple': case 'indefinite': $answer = explode(',', $answer); foreach ($answer as $index => $option) { $answer[$index] = intval($option); } return $answer; case 'fillblank': $answer = json_decode($answer, true); return is_array($answer) ? $answer : []; case 'essay': return $answer; case 'single': case 'judge': default: return intval($answer); } } /** * 列表 */ public function index() { //当前是否为关联查询 $this->relationSearch = true; //设置过滤方法 $this->request->filter(['strip_tags', 'trim']); //如果发送的来源是Selectpage,则转发到Selectpage if ($this->request->request('keyField')) { return $this->selectpage(); } list($where, $sort, $order, $offset, $limit, $page, $alias, $bind,$originWhere) = $this->buildparams(); $list = $this->model ->with(['group']) ->where($where) ->order($sort, $order) ->paginate($limit); foreach ($list as $row) { $row->visible(['id','question','type','degree','option','createtime','group']); $row->question = strip_tags($row->question); } $result = array("total" => $list->total(), "rows" => $list->items(),'attr'=>[ 'typeList' => $this->model->getTypeList(), 'degreeList' => $this->model->getDegreeList() ]); return $this->success("获取成功",$result); } /** * 导出题库 * @return void */ public function export() { $ids = input('ids/a', []); $this->request->filter(['strip_tags', 'trim']); list($where, $sort, $order, $offset, $limit, $page, $alias, $bind, $originWhere) = $this->buildparams(); $query = $this->model->where('uniacid', UNIACID); if (!empty($ids)) { $query->where('id', 'in', $ids); } $query->where($where); $questions = $query->order('id', 'asc')->select(); if (empty($questions)) { $this->error('没有可导出的题目'); } $spreadsheet = $this->buildExportSpreadsheet($questions); $filename = '题库导出_' . date('Y-m-d_H-i') . '.xlsx'; if (ob_get_level() > 0) { ob_clean(); } header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment;filename="' . $filename . '"'); header('Cache-Control: max-age=0'); $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); $writer->save('php://output'); } /** * 构建导出用 Spreadsheet * @param mixed $questions * @return Spreadsheet */ protected function buildExportSpreadsheet($questions) { $typeList = $this->model->getTypeList(); $typeOrder = ['single', 'multiple', 'judge', 'indefinite', 'fillblank', 'essay']; $headers = [ 'single' => ['题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '选项G', '选项H', '选项I', '选项J', '正确答案', '解析'], 'multiple' => ['题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '选项G', '选项H', '选项I', '选项J', '正确答案', '解析'], 'indefinite' => ['题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '选项G', '选项H', '选项I', '选项J', '正确答案', '解析'], 'judge' => ['题目', '正确/错误', '解析'], 'fillblank' => ['题目', '答案(空格用|分隔,备选用,分隔)', '解析'], 'essay' => ['题目', '参考答案', '解析'], ]; $spreadsheet = new Spreadsheet(); $sheetIndex = 0; $typeSheets = []; foreach ($typeOrder as $type) { if ($sheetIndex === 0) { $sheet = $spreadsheet->getActiveSheet(); } else { $sheet = new \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet($spreadsheet, $typeList[$type] ?? $type); $spreadsheet->addSheet($sheet); } $sheet->setTitle($typeList[$type] ?? $type); foreach ($headers[$type] as $colIndex => $title) { $sheet->setCellValueExplicitByColumnAndRow($colIndex + 1, 1, $title, DataType::TYPE_STRING); } $typeSheets[$type] = ['sheet' => $sheet, 'row' => 2]; $sheetIndex++; } foreach ($questions as $question) { $type = $question['type']; if (!isset($typeSheets[$type])) { continue; } $rowData = $this->buildExportRow($question); foreach ($rowData as $colIndex => $value) { $typeSheets[$type]['sheet']->setCellValueExplicitByColumnAndRow($colIndex + 1, $typeSheets[$type]['row'], $value, DataType::TYPE_STRING); } $typeSheets[$type]['row']++; } $spreadsheet->setActiveSheetIndex(0); return $spreadsheet; } /** * 构建单行导出数据 * @param mixed $question * @return array */ protected function buildExportRow($question) { $type = $question['type']; $questionText = $this->plainText($question['question']); $analysis = $this->plainText($question['analysis']); switch ($type) { case 'single': case 'multiple': case 'indefinite': $option = is_array($question['option']) ? $question['option'] : json_decode($question['option'], true); if (!is_array($option)) { $option = []; } $row = [$questionText]; for ($i = 0; $i < 10; $i++) { $row[] = isset($option[$i]) ? $this->plainText($option[$i]) : ''; } $row[] = $this->answerToLetters($type, $question['answer']); $row[] = $analysis; return $row; case 'judge': $answer = (string)$question['answer'] === '0' ? '正确' : '错误'; return [$questionText, $answer, $analysis]; case 'fillblank': $answer = json_decode($question['answer'], true); if (!is_array($answer)) { $answer = []; } $parts = []; foreach ($answer as $blank) { $parts[] = is_array($blank) ? implode(',', $blank) : (string)$blank; } return [$questionText, implode('|', $parts), $analysis]; case 'essay': return [$questionText, $this->plainText($question['answer']), $analysis]; default: return [$questionText]; } } /** * 将答案索引转换为字母 * @param string $type * @param mixed $answer * @return string */ protected function answerToLetters($type, $answer) { if (in_array($type, ['multiple', 'indefinite'])) { $indices = explode(',', (string)$answer); } else { $indices = [(int)$answer]; } $letters = ''; foreach ($indices as $index) { $index = (int)$index; if ($index >= 0 && $index < 26) { $letters .= chr(65 + $index); } } return $letters; } /** * 富文本转纯文本 * @param mixed $content * @return string */ protected function plainText($content) { if (is_null($content)) { return ''; } return html_entity_decode(strip_tags((string)$content), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * 添加 * * @return string * @throws \think\Exception */ public function add() { if (false === $this->request->isPost()) { return $this->view->fetch(); } $params = $this->request->post('row/a'); if (empty($params)) { $this->error(__('Parameter %s can not be empty', '')); } $params = $this->preExcludeFields($params); if ($this->dataLimit && $this->dataLimitFieldAutoFill) { $params[$this->dataLimitField] = $this->auth->id; } $params['uniacid'] = UNIACID; $params['option'] = is_array($params['option']) ? $params['option'] : []; $params['answer'] = $this->normalizeAnswerFromRequest($params['type'], $params['answer']); $this->validateQuestionData($params['type'], $params['option'], $params['answer']); $params['option'] = $this->formatOptionForStorage($params['type'], $params['option']); $params['answer'] = $this->formatAnswerForStorage($params['type'], $params['answer']); $result = false; Db::startTrans(); try { //是否采用模型验证 if ($this->modelValidate) { $name = str_replace("\\model\\", "\\validate\\", get_class($this->model)); $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate; $this->model->validateFailException()->validate($validate); } $result = $this->model->allowField(true)->save($params); Db::commit(); } catch (ValidateException|PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($result === false) { $this->error(__('No rows were inserted')); } $this->success("提交成功"); } /** * 编辑 * * @param $ids * @return string * @throws DbException * @throws \think\Exception */ public function edit($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error(__('No Results were found')); } $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) { $this->error(__('You have no permission')); } $params = $this->request->post('row/a'); if (empty($params)) { $this->error(__('Parameter %s can not be empty', '')); } $params = $this->preExcludeFields($params); $params['option'] = is_array($params['option']) ? $params['option'] : []; $params['answer'] = $this->normalizeAnswerFromRequest($params['type'], $params['answer']); $this->validateQuestionData($params['type'], $params['option'], $params['answer']); $params['option'] = $this->formatOptionForStorage($params['type'], $params['option']); $params['answer'] = $this->formatAnswerForStorage($params['type'], $params['answer']); $params['createtime'] = time(); $result = false; Db::startTrans(); try { //是否采用模型验证 if ($this->modelValidate) { $name = str_replace("\\model\\", "\\validate\\", get_class($this->model)); $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate; $row->validateFailException()->validate($validate); } $result = $row->allowField(true)->save($params); Db::commit(); } catch (ValidateException|PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if (false === $result) { $this->error(__('No rows were updated')); } $this->success("提交成功"); } /** * 获取详情 * @param $id * @return mixed */ public function detail($id){ $row = $this->model->get($id); if (!$row) { $this->error(__('No Results were found')); } $row['answer'] = $this->formatAnswerForDetail($row['type'], $row['answer']); return $this->success("获取成功",$row); } /** * 复制题目 * @return void */ public function copy($ids=null){ if (false === $this->request->isPost()) { $this->error(__("Invalid parameters")); } $ids = $ids ?: $this->request->post("ids"); if (empty($ids)) { $this->error(__('Parameter %s can not be empty', 'ids')); } $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { $this->model->where($this->dataLimitField, 'in', $adminIds); } $list = $this->model->where($pk, 'in', $ids)->select(); $count = 0; Db::startTrans(); try { foreach ($list as $item) { $item = $item->toArray(); unset($item['id']); $item['createtime'] = time(); $item['option'] = $this->formatOptionForStorage($item['type'], $item['option']); $item['answer'] = $this->formatAnswerForStorage($item['type'], $item['answer']); $this->model->insert($item); $count++; } Db::commit(); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($count) { $this->success("操作成功"); } $this->error(__('未操作任何数据')); } /** * 转移分组 * @return void */ public function move($ids=null,$groupId = 0){ if (false === $this->request->isPost()) { $this->error(__("Invalid parameters")); } $ids = $ids ?: $this->request->post("ids"); if (empty($ids)) { $this->error(__('Parameter %s can not be empty', 'ids')); } $groupId = $groupId ?: $this->request->post("group_id"); $pk = $this->model->getPk(); $adminIds = $this->getDataLimitAdminIds(); if (is_array($adminIds)) { $this->model->where($this->dataLimitField, 'in', $adminIds); } $list = $this->model->where($pk, 'in', $ids)->select(); $count = 0; Db::startTrans(); try { foreach ($list as $item) { $row = [ 'group_id'=>$groupId ]; $this->model->where([ 'id'=>$item['id'] ])->update($row); $count++; } Db::commit(); } catch (PDOException|Exception $e) { Db::rollback(); $this->error($e->getMessage()); } if ($count) { $this->success("操作成功"); } $this->error(__('未操作任何数据')); } /** * 获取题目数量 * @remark 通过分组 ids 数组获取题目不同类型对应的数量 * @return void */ /** * 获取题目数量 * @remark 通过分组 ids 数组获取题目不同类型对应的数量 * @return void */ public function groupGetQuestionTypeTotal(){ $groupIds = input('ids/a'); if(!$groupIds){ $groupIds = input('group_ids/a'); } $list = $this->model->where([ 'group_id'=>['in',$groupIds] ])->field([ 'type','COUNT(*) as count' ])->group('type')->select(); $this->success("获取成功",$list); } /** * 随机返回题目 * @remark 通过分组 ids 和 不同类型指定的数量随机返回题目 * @return void */ public function getTypeRandomQuestion(){ $groupIds = input('group_ids/a'); $type_num = input('type_num/a'); $questions = []; foreach ($type_num as $type => $num){ if($num==0){ continue; } $temp = $this->model->where([ 'group_id'=>['in',$groupIds], 'type'=>$type, ])->limit($num)->orderRaw('RAND()')->select(); $questions = array_merge($questions,$temp); } $this->success("获取成功",$questions); } }