Question.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. <?php
  2. namespace app\admin\controller\exam;
  3. use addons\exam\enum\CommonStatus;
  4. use addons\exam\library\FrontService;
  5. use app\admin\model\exam\MaterialQuestionModel;
  6. use app\admin\model\exam\QuestionModel;
  7. use app\common\controller\Backend;
  8. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  9. use PhpOffice\PhpSpreadsheet\Reader\Csv;
  10. use PhpOffice\PhpSpreadsheet\Reader\Xls;
  11. use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
  12. use think\Db;
  13. use think\exception\PDOException;
  14. use think\exception\ValidateException;
  15. use think\Session;
  16. /**
  17. * 试题
  18. *
  19. * @icon fa fa-circle-o
  20. */
  21. class Question extends Backend
  22. {
  23. /**
  24. * QuestionModel模型对象
  25. *
  26. * @var \app\admin\model\exam\QuestionModel
  27. */
  28. protected $model = null;
  29. protected $noNeedRight = ['*'];
  30. protected $multiFields = 'status';
  31. public function _initialize()
  32. {
  33. parent::_initialize();
  34. $this->model = new \app\admin\model\exam\QuestionModel;
  35. $this->view->assign("kindList", $this->model->getKindList());
  36. $this->view->assign("difficultyList", $this->model->getDifficultyList());
  37. $this->view->assign("statusList", $this->model->getStatusList());
  38. }
  39. /**
  40. * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
  41. * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
  42. * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
  43. */
  44. /**
  45. * 查看
  46. */
  47. public function index()
  48. {
  49. //当前是否为关联查询
  50. $this->relationSearch = true;
  51. //设置过滤方法
  52. $this->request->filter(['strip_tags', 'trim']);
  53. if ($this->request->isAjax()) {
  54. //如果发送的来源是Selectpage,则转发到Selectpage
  55. if ($this->request->request('keyField')) {
  56. return $this->selectpage();
  57. }
  58. [$where, $sort, $order, $offset, $limit] = $this->buildparams();
  59. $list = $this->model
  60. ->with(['cate'])
  61. ->where($where)
  62. ->order($sort, $order)
  63. ->paginate($limit);
  64. foreach ($list as $row) {
  65. if (isset($row['cate'])) {
  66. $row->getRelation('cate')->visible(['name']);
  67. }
  68. }
  69. $count = $list->total();
  70. $items = $list->items();
  71. // 检测是否有重复题目
  72. foreach ($items as &$question) {
  73. $title = str_replace(' ', '', $question['title']);;
  74. $question['is_repeat'] = 0;
  75. foreach ($items as $item) {
  76. if ($item['id'] != $question['id']) {
  77. $title2 = str_replace(' ', '', $item['title']);
  78. if ($title == $title2) {
  79. // 标记重复
  80. $question['is_repeat'] = 1;
  81. break;
  82. }
  83. }
  84. }
  85. }
  86. $result = ["total" => $count, "rows" => $items];
  87. return json($result);
  88. }
  89. return $this->view->fetch();
  90. }
  91. /**
  92. * 添加
  93. */
  94. public function add()
  95. {
  96. if ($this->request->isPost()) {
  97. $params = $this->request->post("row/a");
  98. // if ($params['options_extend']) {
  99. // $params['options_extend'] = json_decode(urldecode($params['options_extend']), true);
  100. // }
  101. // dd($params);
  102. if ($params) {
  103. $params = $this->preExcludeFields($params);
  104. // 检查题目输入
  105. $this->checkTitle($params);
  106. // 检查答案输入
  107. $this->checkAnswer($params);
  108. // 处理选项图片链接域名
  109. $this->optionsImage($params);
  110. if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
  111. $params[$this->dataLimitField] = $this->auth->id;
  112. }
  113. $result = false;
  114. Db::startTrans();
  115. try {
  116. //是否采用模型验证
  117. if ($this->modelValidate) {
  118. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  119. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
  120. $this->model->validateFailException(true)->validate($validate);
  121. }
  122. $result = $this->model->allowField(true)->save($params);
  123. // 保存材料题父题目
  124. $this->saveMaterialParentQuestion($this->model);
  125. // 保存材料题子题目
  126. $this->saveMaterialQuestions($this->model, $params['material_questions'] ?? []);
  127. Db::commit();
  128. } catch (ValidateException $e) {
  129. Db::rollback();
  130. $this->error($e->getMessage());
  131. } catch (PDOException $e) {
  132. Db::rollback();
  133. $this->error($e->getMessage());
  134. } catch (Exception $e) {
  135. Db::rollback();
  136. $this->error($e->getMessage());
  137. }
  138. if ($result !== false) {
  139. $this->success();
  140. } else {
  141. $this->error(__('No rows were inserted'));
  142. }
  143. }
  144. $this->error(__('Parameter %s can not be empty', ''));
  145. }
  146. return $this->view->fetch();
  147. }
  148. /**
  149. * 编辑
  150. */
  151. public function edit($ids = null)
  152. {
  153. $row = $this->model->get($ids, [
  154. 'material_questions' => function ($query) {
  155. return $query->with([
  156. 'question' => function ($query) {
  157. return $query->with('cates');
  158. },
  159. ])->order('weigh');
  160. },
  161. ]);
  162. // dd($row->toArray());
  163. if (!$row) {
  164. $this->error(__('No Results were found'));
  165. }
  166. $adminIds = $this->getDataLimitAdminIds();
  167. if (is_array($adminIds)) {
  168. if (!in_array($row[$this->dataLimitField], $adminIds)) {
  169. $this->error(__('You have no permission'));
  170. }
  171. }
  172. if ($this->request->isPost()) {
  173. $params = $this->request->post("row/a");
  174. if ($params) {
  175. $params = $this->preExcludeFields($params);
  176. // 检查题目输入
  177. $this->checkTitle($params);
  178. // 检查答案输入
  179. $this->checkAnswer($params);
  180. // 处理选项图片链接域名
  181. $this->optionsImage($params);
  182. $result = false;
  183. Db::startTrans();
  184. try {
  185. //是否采用模型验证
  186. if ($this->modelValidate) {
  187. $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
  188. $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
  189. $row->validateFailException(true)->validate($validate);
  190. }
  191. $result = $row->allowField(true)->save($params);
  192. // 保存材料题父题目
  193. $this->saveMaterialParentQuestion($row);
  194. // 保存材料题子题目
  195. $this->saveMaterialQuestions($row, $params['material_questions'] ?? []);
  196. Db::commit();
  197. } catch (ValidateException $e) {
  198. Db::rollback();
  199. $this->error($e->getMessage());
  200. } catch (PDOException $e) {
  201. Db::rollback();
  202. $this->error($e->getMessage());
  203. } catch (Exception $e) {
  204. Db::rollback();
  205. $this->error($e->getMessage());
  206. }
  207. if ($result !== false) {
  208. $this->success();
  209. } else {
  210. $this->error(__('No rows were updated'));
  211. }
  212. }
  213. $this->error(__('Parameter %s can not be empty', ''));
  214. }
  215. $this->view->assign("row", $row);
  216. return $this->view->fetch();
  217. }
  218. /**
  219. * 选项图片页面
  220. *
  221. * @return string
  222. */
  223. public function image()
  224. {
  225. if ($this->request->isPost()) {
  226. $this->success($this->request->post());
  227. }
  228. return $this->view->fetch();
  229. }
  230. /**
  231. * 试题导入
  232. */
  233. public function importExcel()
  234. {
  235. if ($this->request->isPost()) {
  236. // parent::import();
  237. $cate = $this->request->param('cate');
  238. // $exam_type = $this->request->param('exam_type');
  239. if (!$cate) { // || !$exam_type
  240. $this->error('请先选择所属类型及考试分类再进行上传');
  241. }
  242. $file = $this->request->request('file');
  243. if (!$file) {
  244. $this->error(__('Parameter %s can not be empty', 'file'));
  245. }
  246. $filePath = ROOT_PATH . DS . 'public' . DS . $file;
  247. if (!is_file($filePath)) {
  248. $this->error(__('No results were found'));
  249. }
  250. //实例化reader
  251. $ext = pathinfo($filePath, PATHINFO_EXTENSION);
  252. if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
  253. $this->error(__('Unknown data format'));
  254. }
  255. if ($ext === 'csv') {
  256. $file = fopen($filePath, 'r');
  257. $filePath = tempnam(sys_get_temp_dir(), 'import_csv');
  258. $fp = fopen($filePath, "w");
  259. $n = 0;
  260. while ($line = fgets($file)) {
  261. $line = rtrim($line, "\n\r\0");
  262. $encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']);
  263. if ($encoding != 'utf-8') {
  264. $line = mb_convert_encoding($line, 'utf-8', $encoding);
  265. }
  266. if ($n == 0 || preg_match('/^".*"$/', $line)) {
  267. fwrite($fp, $line . "\n");
  268. } else {
  269. fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n");
  270. }
  271. $n++;
  272. }
  273. fclose($file) || fclose($fp);
  274. $reader = new Csv();
  275. } elseif ($ext === 'xls') {
  276. $reader = new Xls();
  277. } else {
  278. $reader = new Xlsx();
  279. }
  280. //加载文件
  281. $insert = [];
  282. try {
  283. if (!$PHPExcel = $reader->load($filePath)) {
  284. throw new \Exception(__('Unknown data format'));
  285. }
  286. $currentSheet = $PHPExcel->getSheet(0); //读取文件中的第一个工作表
  287. $allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号
  288. $allRow = $currentSheet->getHighestRow(); //取得一共有多少行
  289. $maxColumnNumber = Coordinate::columnIndexFromString($allColumn);
  290. $fields = [
  291. 'kind',
  292. 'title',
  293. 'explain',
  294. 'difficulty',
  295. 'answer',
  296. 'A',
  297. 'B',
  298. 'C',
  299. 'D',
  300. 'E',
  301. 'F',
  302. 'G',
  303. 'H',
  304. ];
  305. for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) {
  306. $values = [];
  307. for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
  308. $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue();
  309. $values[] = is_null($val) ? '' : $val;
  310. }
  311. // $temp = array_combine($fields, $values);
  312. $row = [];
  313. $options = [];
  314. // 选项及字段组合
  315. for ($i = 0; $i < min($maxColumnNumber, count($fields)); $i++) {
  316. $field = $fields[$i];
  317. $value = $values[$i];
  318. if ($i > 4) {
  319. if (!empty($value) || $value == '0') {
  320. $options[$field] = $value;
  321. }
  322. } else {
  323. $row[$field] = $value;
  324. }
  325. }
  326. // 过滤空行
  327. if (!$row['title']) {
  328. continue;
  329. }
  330. // 特殊字段处理
  331. foreach ($row as $key => $item) {
  332. if ($key == 'kind') {
  333. switch ($item) {
  334. case '判断':
  335. case '判断题':
  336. $item = 'JUDGE';
  337. break;
  338. case '单选':
  339. case '单选题':
  340. $item = 'SINGLE';
  341. break;
  342. case '多选':
  343. case '多选题':
  344. $item = 'MULTI';
  345. break;
  346. case '填空':
  347. case '填空题':
  348. $item = 'FILL';
  349. break;
  350. case '简答':
  351. case '简答题':
  352. $item = 'SHORT';
  353. break;
  354. }
  355. } else if ($key == 'difficulty') {
  356. switch ($item) {
  357. case '低':
  358. case '简单':
  359. $item = 'EASY';
  360. break;
  361. case '高':
  362. case '困难':
  363. $item = 'HARD';
  364. break;
  365. default:
  366. $item = 'GENERAL';
  367. break;
  368. }
  369. }
  370. $row[$key] = $item;
  371. }
  372. // 判断题特殊情况处理
  373. if ($row['kind'] == 'JUDGE') {
  374. switch ($row['answer']) {
  375. case '正确':
  376. case '对':
  377. $row['answer'] = 'A';
  378. break;
  379. case '错误':
  380. case '错':
  381. $row['answer'] = 'B';
  382. break;
  383. }
  384. $options = $options ? $options : ['A' => '正确', 'B' => '错误'];
  385. }
  386. // 答案特殊处理
  387. $row['answer'] = str_replace(' ', '', $row['answer']);
  388. $row['answer'] = str_replace(' ', '', $row['answer']);
  389. $row['answer'] = str_replace(',', ',', $row['answer']);
  390. if (!$row['answer'] || ($row['kind'] == 'MULTI' && !strpos($row['answer'], ','))) {
  391. throw new \Exception('题目【' . $row['title'] . '】答案格式有误');
  392. }
  393. // 填空题
  394. if ($row['kind'] == 'FILL') {
  395. $row['answer'] = explode('|||', $row['answer']);
  396. $fill_count = count(explode('______', $row['title'])) - 1;
  397. $answer_count = count($row['answer']);
  398. if ($fill_count != $answer_count) {
  399. throw new \Exception('题目【' . $row['title'] . '】填空位与答案数量不匹配');
  400. }
  401. $fill_answers = [];
  402. foreach ($row['answer'] as $item) {
  403. $fill_answers[] = ['answers' => explode(',', $item)];
  404. }
  405. $row['answer'] = json_encode($fill_answers, JSON_UNESCAPED_UNICODE);
  406. }
  407. // 简答题
  408. if ($row['kind'] == 'SHORT') {
  409. $row['answer'] = explode('|||', $row['answer']);
  410. if ($row['answer']) {
  411. $short_answers = [
  412. 'answer' => $row['answer'][0],
  413. 'config' => [],
  414. ];
  415. if (isset($row['answer'][1]) && $row['answer'][1]) {
  416. $keywords = explode(',', str_replace(',', ',', $row['answer'][1]));
  417. foreach ($keywords as $keyword) {
  418. $short_answers['config'][] = [
  419. 'answer' => $keyword,
  420. 'score' => 1,
  421. ];
  422. }
  423. }
  424. $row['answer'] = json_encode($short_answers, JSON_UNESCAPED_UNICODE);
  425. }
  426. }
  427. $row['options_json'] = json_encode($options, JSON_UNESCAPED_UNICODE);
  428. $row['cate_id'] = $cate;
  429. // $row['exam_type_id'] = $exam_type;
  430. $insert[] = $row;
  431. }
  432. Session::set('import_question', $insert);
  433. } catch (\Exception $exception) {
  434. $this->error($exception->getMessage());
  435. }
  436. if (!$insert) {
  437. $this->error(__('No rows were updated'));
  438. }
  439. $this->success('识别成功', '', ['count' => count($insert)]);
  440. }
  441. $this->error('错误的提交方式');
  442. }
  443. /**
  444. * 试题导入提交
  445. *
  446. * @return string|void
  447. */
  448. public function import()
  449. {
  450. if ($this->request->isPost()) {
  451. // 加载数据
  452. $insert = Session::pull('import_question');
  453. if (!$insert) {
  454. $this->error(__('没有可以导入的数据,请重新上传Excel文件'));
  455. }
  456. try {
  457. $this->model->saveAll($insert);
  458. } catch (PDOException $exception) {
  459. $msg = $exception->getMessage();
  460. if (preg_match("/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is", $msg, $matches)) {
  461. $msg = "导入失败,包含【{$matches[1]}】的记录已存在";
  462. };
  463. $this->error($msg);
  464. } catch (\Exception $exception) {
  465. $this->error($exception->getMessage());
  466. }
  467. $this->success();
  468. }
  469. return $this->view->fetch();
  470. }
  471. /**
  472. * 获取题库数量
  473. */
  474. public function getCount()
  475. {
  476. $cate_ids = $this->request->param('cate_ids');
  477. if (!$cate_ids) {
  478. $this->error('请先选择题库');
  479. }
  480. $this->success('', '', $this->model->getCount($cate_ids));
  481. }
  482. /**
  483. * 选择题目页面
  484. */
  485. public function select()
  486. {
  487. //当前是否为关联查询
  488. $this->relationSearch = true;
  489. //设置过滤方法
  490. $this->request->filter(['strip_tags', 'trim']);
  491. if ($this->request->isAjax()) {
  492. //如果发送的来源是Selectpage,则转发到Selectpage
  493. if ($this->request->request('keyField')) {
  494. return $this->selectpage();
  495. }
  496. [$where, $sort, $order, $offset, $limit] = $this->buildparams();
  497. $list = $this->model
  498. ->with(['cate'])
  499. ->where($where)
  500. ->where('status', CommonStatus::NORMAL)
  501. ->order($sort, $order)
  502. ->paginate($limit);
  503. foreach ($list as $row) {
  504. $row->getRelation('cate')->visible(['name']);
  505. }
  506. $rows = $list->items();
  507. foreach ($rows as &$row) {
  508. $row['title'] = strip_tags($row['title']);
  509. }
  510. $result = ["total" => $list->total(), "rows" => $rows];
  511. return json($result);
  512. }
  513. return $this->view->fetch();
  514. }
  515. public function test()
  516. {
  517. $this->success('', '', Session::get('import_question'));
  518. }
  519. /**
  520. * 检查答案输入
  521. *
  522. * @param $params
  523. */
  524. protected function checkAnswer(&$params)
  525. {
  526. $answer = $params['answer'] ?? '';
  527. $options = $params['options_json'] ?? [];
  528. if (in_array($params['kind'], ['MULTI', 'JUDGE', 'SINGLE'])) {
  529. if (!$answer) {
  530. $this->error('请设置题目答案');
  531. }
  532. if (!$options) {
  533. $this->error('请设置题目选项');
  534. }
  535. $options = json_decode($options, true);
  536. $option_keys = array_keys($options);
  537. // 多选题
  538. if (strpos($answer, ',') !== false) {
  539. $answer_arr = explode(',', trim($answer));
  540. foreach ($answer_arr as $item) {
  541. if (!in_array($item, $option_keys)) {
  542. $this->error('答案设置有误,答案选项不存在');
  543. }
  544. }
  545. } else {
  546. if (!in_array($answer, $option_keys)) {
  547. $this->error('答案设置有误,答案选项不存在');
  548. }
  549. }
  550. }
  551. // 后期拓展题型
  552. switch ($params['kind']) {
  553. // 填空题
  554. case 'FILL':
  555. if (!is_array($answer) || !count($answer)) {
  556. $this->error('未设置填空题答案,至少设置一个');
  557. }
  558. // 转json
  559. $params['answer'] = json_encode($params['answer'], JSON_UNESCAPED_UNICODE);
  560. break;
  561. // 简答题
  562. case 'SHORT':
  563. $answer = $params['short_answer'] ?? '';
  564. if (!$answer) {
  565. $this->error('请设置简答题标准答案');
  566. }
  567. $answer_config = $params['answer_config'] ?? [];
  568. if (!$answer_config) {
  569. $this->error('请设置简答题答案关键词');
  570. }
  571. foreach ($answer_config as &$item) {
  572. if (!$item['answer']) {
  573. $this->error('简答题答案关键词不能为空');
  574. }
  575. if (!is_numeric($item['score']) || $item['score'] < 0) {
  576. $this->error('简答题分数设置有误,必须为数字且不能小于0');
  577. }
  578. // $item['score'] = 0;
  579. }
  580. // 转json
  581. $params['answer'] = json_encode([
  582. 'answer' => $answer,
  583. 'config' => $answer_config,
  584. ], JSON_UNESCAPED_UNICODE);
  585. break;
  586. // 材料题
  587. case 'MATERIAL':
  588. // dd([$params, $answer, $options]);
  589. if ($params['is_material_child']) {
  590. $this->error('题型设置有误,材料题不能再设置为材料题的子题');
  591. }
  592. $params['is_material_child'] = 0;
  593. $params['material_question_id'] = 0;
  594. $params['material_questions'] = $params['material_questions'] ? json_decode($params['material_questions'], true) : [];
  595. if (!$params['material_questions']) {
  596. if ($params['status'] == CommonStatus::NORMAL) {
  597. $this->error('请设置材料题的子题(或者状态先设置隐藏)');
  598. }
  599. }
  600. // foreach ($params['material_questions'] as $material_question) {
  601. // if ($material_question['kind'] == 'MATERIAL') {
  602. // $this->error('材料题的子题不能为材料题');
  603. // }
  604. // }
  605. // 转json
  606. $params['answer'] = json_encode([
  607. 'questions' => $params['material_questions'],
  608. ], JSON_UNESCAPED_UNICODE);
  609. break;
  610. }
  611. if ($params['is_material_child']) {
  612. $params['material_question_id'] = $params['material_question_id'] ?? 0;
  613. if (!$params['material_question_id']) {
  614. $this->error('请设置材料题的父题');
  615. }
  616. if ($params['material_score'] <= 0) {
  617. $this->error('请设置材料题子题的分数');
  618. }
  619. }
  620. }
  621. /**
  622. * 保存材料题父题
  623. *
  624. * @param $question
  625. * @return void
  626. */
  627. protected function saveMaterialParentQuestion($question)
  628. {
  629. if (!$question || !$question['is_material_child'] || $question['kind'] == 'MATERIAL') {
  630. return;
  631. }
  632. if ($parentQuestion = QuestionModel::where('id', $question['material_question_id'])
  633. ->where('kind', 'MATERIAL')
  634. ->find()) {
  635. $answer = json_decode($parentQuestion->answer, true);
  636. if (!$answer) {
  637. $answer = [
  638. 'questions' => [
  639. [
  640. 'id' => $question['id'],
  641. 'score' => $question['material_score'],
  642. 'answer' => is_array($question['answer']) ? json_encode($question['answer'], JSON_UNESCAPED_UNICODE) : $question['answer'],
  643. 'answer_config' => null,
  644. ],
  645. ],
  646. ];
  647. } else {
  648. $has_child = false;
  649. foreach ($answer['questions'] as &$child_question) {
  650. if ($child_question['id'] == $question['id']) {
  651. $has_child = true;
  652. $child_question['score'] = $question['material_score'];
  653. $child_question['answer'] = is_array($question['answer']) ? json_encode($question['answer'], JSON_UNESCAPED_UNICODE) : $question['answer'];
  654. break;
  655. }
  656. }
  657. if (!$has_child) {
  658. $answer['questions'][] = [
  659. 'id' => $question['id'],
  660. 'score' => $question['material_score'],
  661. 'answer' => is_array($question['answer']) ? json_encode($question['answer'], JSON_UNESCAPED_UNICODE) : $question['answer'],
  662. 'answer_config' => null,
  663. ];
  664. }
  665. }
  666. // 材料题父题答案追加子题设置
  667. $parentQuestion->answer = json_encode($answer, JSON_UNESCAPED_UNICODE);
  668. $parentQuestion->save();
  669. if ($materialQuestion = MaterialQuestionModel::where('parent_question_id', $question['material_question_id'])
  670. ->where('question_id', $question['id'])
  671. ->find()) {
  672. $materialQuestion->score = $question['material_score'];
  673. $materialQuestion->answer = is_array($question['answer']) ? json_encode($question['answer'], JSON_UNESCAPED_UNICODE) : $question['answer'];
  674. $materialQuestion->save();
  675. } else {
  676. // 保存材料题子题关联
  677. MaterialQuestionModel::create([
  678. 'parent_question_id' => $question['material_question_id'],
  679. 'question_id' => $question['id'],
  680. 'score' => $question['material_score'],
  681. 'answer' => $question['answer'],
  682. 'weigh' => 0,
  683. ]);
  684. }
  685. } else {
  686. $this->error('未找到所属材料题');
  687. }
  688. }
  689. /**
  690. * 保存材料题子题
  691. *
  692. * @param $parentQuestion
  693. * @param $questions
  694. * @return void
  695. */
  696. protected function saveMaterialQuestions($parentQuestion, $questions)
  697. {
  698. if (!$parentQuestion || $parentQuestion['kind'] != 'MATERIAL' || !$questions) {
  699. return;
  700. }
  701. // 删除旧的子题
  702. MaterialQuestionModel::where('parent_question_id', $parentQuestion->id)->delete();
  703. // 保存新的子题
  704. $inserts = [];
  705. foreach ($questions as $key => $question) {
  706. $inserts[] = [
  707. 'parent_question_id' => $parentQuestion->id,
  708. 'question_id' => $question['id'],
  709. 'score' => $question['score'],
  710. 'answer' => is_array($question['answer']) ? json_encode($question['answer'], JSON_UNESCAPED_UNICODE) : $question['answer'],
  711. 'weigh' => $key,
  712. 'createtime' => time(),
  713. ];
  714. }
  715. (new MaterialQuestionModel())->saveAll($inserts);
  716. }
  717. /**
  718. * 处理选项图片链接域名
  719. *
  720. * @param $data
  721. */
  722. protected function optionsImage(&$data)
  723. {
  724. $options_img = $data['options_img'] ?? null;
  725. $options_img = json_decode($options_img, true);
  726. if ($options_img) {
  727. foreach ($options_img as &$item) {
  728. if (strpos($item['value'], '://') === false) {
  729. $item['value'] = cdnurl($item['value'], true);
  730. }
  731. }
  732. }
  733. $data['options_img'] = json_encode($options_img);
  734. }
  735. /**
  736. * 检查题目内容输入
  737. *
  738. * @param $params
  739. */
  740. protected function checkTitle(&$params)
  741. {
  742. if (in_array($params['kind'], ['MULTI', 'JUDGE', 'SINGLE'])) {
  743. if (!($params['title'] ?? '')) {
  744. $this->error('请输入题目内容');
  745. }
  746. } else if ($params['kind'] == 'FILL') {
  747. if (!($params['title_fill'] ?? '')) {
  748. $this->error('请输入填空题题目内容');
  749. }
  750. $params['title'] = $params['title_fill'];
  751. }
  752. // 替换题目图片CDN链接
  753. $params['title'] = FrontService::replaceImgUrl($params['title']);
  754. }
  755. }