Sfoglia il codice sorgente

feat(server): 题目录入

Go 6 anni fa
parent
commit
2c385baf7b
60 ha cambiato i file con 2424 aggiunte e 437 eliminazioni
  1. 23 4
      front/.eslintrc
  2. 57 0
      front/project/Constant.js
  3. 80 0
      front/project/admin/components/Association/index.js
  4. 0 0
      front/project/admin/components/Association/index.less
  5. 77 0
      front/project/admin/components/QuestionNoList/index.js
  6. 0 0
      front/project/admin/components/QuestionNoList/index.less
  7. 78 0
      front/project/admin/components/SimilarQuestionNo/index.js
  8. 0 0
      front/project/admin/components/SimilarQuestionNo/index.less
  9. 1 1
      front/project/admin/routes/setting/place/page.js
  10. 1 1
      front/project/admin/routes/setting/time/page.js
  11. 18 3
      front/project/admin/routes/subject/exercise/page.js
  12. 2 1
      front/project/admin/routes/subject/index.js
  13. 1 1
      front/project/admin/routes/subject/preview/page.js
  14. 31 44
      front/project/admin/routes/subject/previewDetail/page.js
  15. 2 1
      front/project/admin/routes/subject/question/index.js
  16. 66 1
      front/project/admin/routes/subject/question/index.less
  17. 799 1
      front/project/admin/routes/subject/question/page.js
  18. 1 2
      front/project/admin/routes/subject/sentence/page.js
  19. 20 1
      front/project/admin/routes/subject/sentenceQuestion/index.less
  20. 152 67
      front/project/admin/routes/subject/sentenceQuestion/page.js
  21. 15 0
      front/project/admin/routes/subject/textbookQuestion/index.js
  22. 3 0
      front/project/admin/routes/subject/textbookQuestion/index.less
  23. 151 0
      front/project/admin/routes/subject/textbookQuestion/page.js
  24. 2 3
      front/project/admin/routes/system/manager/list/page.js
  25. 1 1
      front/project/admin/routes/user/ask/page.js
  26. 1 1
      front/project/admin/routes/user/askDetail/page.js
  27. 11 5
      front/project/admin/routes/user/detail/page.js
  28. 1 1
      front/project/admin/routes/user/list/page.js
  29. 12 4
      front/project/admin/stores/question.js
  30. 4 0
      front/project/admin/stores/user.js
  31. 89 77
      front/src/components/Editor/index.js
  32. 4 6
      front/src/containers/Admin.js
  33. 2 2
      front/src/containers/App.js
  34. 1 0
      front/src/layouts/FormLayout/index.js
  35. 0 22
      front/src/services/Constant.js
  36. 1 1
      front/src/services/Tools.js
  37. 1 1
      server/data/src/main/java/com/qxgmat/data/constants/enums/user/PrepareExaminationTime.java
  38. 51 77
      server/data/src/main/java/com/qxgmat/data/dao/entity/Question.java
  39. 6 7
      server/data/src/main/java/com/qxgmat/data/dao/mapping/QuestionMapper.xml
  40. 25 0
      server/data/src/main/java/com/qxgmat/data/inline/UserToken.java
  41. 1 1
      server/data/src/main/resources/mybatis-generator.xml
  42. 4 4
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/PreviewController.java
  43. 62 30
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/QuestionController.java
  44. 8 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java
  45. 12 4
      server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java
  46. 15 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java
  47. 19 9
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/QuestionExtendDto.java
  48. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/QuestionNoExtendDto.java
  49. 31 21
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionDto.java
  50. 69 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionNoDto.java
  51. 57 8
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionNoSearchDto.java
  52. 22 1
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/ExerciseQuestionListDto.java
  53. 8 8
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/HomeworkPreviewDetailDto.java
  54. 130 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/QuestionDetailDto.java
  55. 38 0
      server/gateway-api/src/main/java/com/qxgmat/service/UsersService.java
  56. 18 7
      server/gateway-api/src/main/java/com/qxgmat/service/inline/QuestionNoService.java
  57. 3 0
      server/gateway-api/src/main/resources/application.yml
  58. 111 0
      server/tools/src/main/java/com/nuliji/tools/CipherHelp.java
  59. 11 3
      server/tools/src/main/java/com/nuliji/tools/Tools.java
  60. 5 5
      server/tools/src/main/java/com/nuliji/tools/mybatis/handler/NativeJsonHandler.java

+ 23 - 4
front/.eslintrc

@@ -1,11 +1,30 @@
 {
   "parser": "babel-eslint",
-  "extends": ["standard-react", "airbnb-base"],
-  "plugins": ["react", "import"],
+  "extends": [
+    "standard-react",
+    "airbnb-base"
+  ],
+  "plugins": [
+    "react",
+    "import"
+  ],
   "settings": {
     "import/resolver": {
       "alias": {
-        "map": [["@src", "./src"], ["@project", "./project/admin"], ["@components", "./components"]]
+        "map": [
+          [
+            "@src",
+            "./src"
+          ],
+          [
+            "@project",
+            "./project/admin"
+          ],
+          [
+            "@components",
+            "./components"
+          ]
+        ]
       }
     }
   },
@@ -54,4 +73,4 @@
     "global-require": "off",
     "import/no-extraneous-dependencies": "off"
   }
-}
+}

+ 57 - 0
front/project/Constant.js

@@ -0,0 +1,57 @@
+export const UserUrl = 'http://www.baidu.com/';
+
+export const QuestionDifficult = [{ label: 'easy', value: 'easy' }, { label: 'medium', value: 'medium' }, { label: 'hard', value: 'hard' }];
+
+export const QuestionType = [{ label: 'SC/语法', value: 'sc' }, { label: 'RC/阅读', value: 'rc' }, { label: 'CR/逻辑', value: 'cr' }, { label: 'PS/数学', value: 'ps' }, { label: 'DS/数学', value: 'ds' }, { label: 'IR/综合推理', value: 'ir' }, { label: 'AWA/作文', value: 'awa' }];
+
+export const MoneyRange = [{ label: '0', value: 0 }, { label: '1-1000', value: 1 }, { label: '1000-5000', value: 2 }, { label: '5000-10000', value: 3 }, { label: '10000以上', value: 4 }];
+
+export const AskTarget = [{ label: '题目', value: 'question' }, { label: '官方', value: 'official' }, { label: '千行解析', value: 'qx' }, { label: '题源联想', value: 'association' }];
+
+export const PreviewStatus = [{ label: '全部', value: 0 }, { label: '未开始', value: 1 }, { label: '进行中', value: 2 }, { label: '已结束', value: 3 }];
+
+export const ServiceKey = [{ label: 'VIP', value: 'vip' }, { label: '机经', value: 'textbook' }, { label: '千行CAT', value: 'qx_cat' }];
+
+export const SwitchSelect = [{ value: 0, label: '否' }, { value: 1, label: '是' }];
+
+export const AskStatus = [{ value: 0, label: '新增' }, { value: 1, label: '已回答' }, { value: 2, label: '忽略' }];
+
+export const PrepareStatus = [{ label: '学生-Domestic', value: 'student_domestic' }, { label: '学生-Overseas', value: 'student_overseas' }, { label: '在职-Domestic', value: 'worker_domestic' }, { label: '在职-Overseas', value: 'worker_overseas' }, { label: 'Gap Year', value: 'gap_year' }];
+
+export const PrepareExaminationTime = [{ label: '近1个月', value: 'one_month' }, { label: '近2个月', value: 'two_month' }, { label: '近3个月', value: 'three_month' }, { label: '半年内', value: 'six_month' }];
+
+export const PayModule = [{ label: '服务', value: 'service' }, { label: '课程', value: 'class' }, { label: '资料', value: 'data' }];
+
+export const QuestionStyleType = [{ label: '单选', value: 'single' }, { label: '横纵向单选', value: 'double' }, { label: '题目内选择', value: 'inline' }];
+
+export const QuestionRadioDirection = [{ label: '横向', value: 'landscape' }, { label: '纵向', value: 'portrait' }];
+
+export const SentenceOption = [{ label: '平行', value: 'parallel' }, { label: '修饰', value: 'embellish' }, { label: '转折', value: 'transition' }, { label: '比较', value: 'compare' }, { label: '因果', value: 'cause' }, { label: '递进', value: 'progressively' }];
+
+// const content = {
+//   steps: [{
+//     title: '',
+//     stem: '',
+//   }],
+//   questions: [{
+//     description: '', // type!=inline
+//     answer: [],
+//     select: [],
+//     direction: '', // type=double
+//   }],
+//   table: {
+//     row: 3,
+//     col: 3,
+//     header: [],
+//     data: [[], []],
+//   },
+//   type: '',
+//   number: '',
+//   typeset: 'one|two',
+
+//   // sentence
+//   subject: [],
+//   predicate: [],
+//   object: [],
+//   options: [],
+// };

+ 80 - 0
front/project/admin/components/Association/index.js

@@ -0,0 +1,80 @@
+import React, { Component } from 'react';
+import { Form, Modal, Alert } from 'antd';
+import './index.less';
+import Select from '@src/components/Select';
+import QuestionNoList from '../QuestionNoList';
+
+class Association extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { ids: this.props.ids, nos: this.props.nos, show: !!props.modal, loading: false, err: '' };
+    this.questionNos = [];
+  }
+
+  onConfirm() {
+    this.props.form.validateFields((err, fieldsValue) => {
+      if (err) {
+        return;
+      }
+      if (this.props.onConfirm && this.props.modal) {
+        this.setState({ loading: true });
+        this.props
+          .onConfirm(fieldsValue)
+          .then(() => {
+            this.setState({ loading: false });
+            this.onCancel();
+          })
+          .catch(e => {
+            this.setState({ loading: false, err: e.message });
+          });
+      } else {
+        this.onCancel();
+      }
+    });
+  }
+
+  onCancel() {
+    if (this.props.modal) this.setState({ show: false });
+    if (this.props.onCancel) this.props.onCancel();
+  }
+
+  renderForm() {
+    const { getFieldDecorator, setFieldsValue } = this.props.form;
+    const { field = 'questionNoIds' } = this.props;
+    return <Form>
+      <Form.Item>
+        {getFieldDecorator(field)(
+          <Select mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入题目id, 逗号分隔' tokenSeparators={[',', ',']} onChange={(values) => {
+            this.setState({ nos: values });
+          }} />,
+        )}
+      </Form.Item>
+      <QuestionNoList module={this.props.module} loading={false} ids={this.state.ids} nos={this.state.nos} onChange={(questionNos) => {
+        this.questionNos = questionNos;
+        getFieldDecorator(field);
+        const nos = questionNos.map(row => row.no);
+        setFieldsValue({ [field]: nos });
+      }} />
+    </Form>;
+  }
+
+  render() {
+    const { modal, title, confirmText = '确定', cancelText = '取消' } = this.props;
+    const { show, loading, err } = this.state;
+    return modal ? (
+      <Modal
+        title={title}
+        visible={show}
+        okText={confirmText}
+        cancelText={cancelText}
+        confirmLoading={loading}
+        onOk={() => this.onConfirm()}
+        onCancel={() => this.onCancel()}
+      >
+        {err && <Alert type="error" showIcon message={err} closable onClose={() => this.setState({ err: '' })} />}
+        {this.renderForm()}
+      </Modal>
+    ) : (<div>{this.renderForm()}</div>);
+  }
+}
+export default Form.create()(Association);

+ 0 - 0
front/project/admin/components/Association/index.less


+ 77 - 0
front/project/admin/components/QuestionNoList/index.js

@@ -0,0 +1,77 @@
+import React, { Component } from 'react';
+import { Icon, List, Avatar, Typography } from 'antd';
+import './index.less';
+import DragList from '@src/components/DragList';
+import { getMap } from '@src/services/Tools';
+import { Question } from '../../stores/question';
+
+export default class QuestionNoList extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { loading: 0 };
+    this.searchQuestion(this.props.ids, 'ids');
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.props.ids !== nextProps.ids) {
+      this.searchQuestion(nextProps.ids, 'ids');
+    } else if (this.props.nos !== nextProps.nos) {
+      this.searchQuestion(nextProps.nos, 'nos');
+    }
+  }
+
+  deleteQuestion(index) {
+    const { questionNos, loading } = this.state;
+    questionNos.splice(index, 1);
+    this.setState({ questionNos, loading: loading + 1 });
+    this.props.onChange(questionNos);
+  }
+
+  orderQuestion(oldIndex, newIndex) {
+    const { questionNos, loading } = this.state;
+    const tmp = questionNos[oldIndex];
+    questionNos[oldIndex] = questionNos[newIndex];
+    questionNos[newIndex] = tmp;
+    console.log(questionNos);
+    this.setState({ questionNos, loading: loading + 1 });
+    this.props.onChange(questionNos);
+  }
+
+  searchQuestion(values, field = 'nos') {
+    // console.log('search', values, field);
+    if (!values || values.length === 0) {
+      this.setState({ questionNos: [] });
+      return;
+    }
+    // 查找练习题目
+    Question.listNo({ [field]: values, module: this.props.module }).then(result => {
+      const { loading } = this.state;
+      const map = getMap(result, 'no');
+      const questionNos = values.map(no => map[no]).filter(row => row);
+      this.setState({ questionNos, loading: loading + 1 });
+      this.props.onChange(questionNos);
+    });
+  }
+
+  render() {
+    return <DragList
+      key={this.state.loading}
+      loading={this.props.loading}
+      dataSource={this.state.questionNos || []}
+      handle={'.icon'}
+      onMove={(oldIndex, newIndex) => {
+        this.orderQuestion(oldIndex, newIndex);
+      }}
+      renderItem={(item, index) => (
+        <List.Item actions={[<Icon type='delete' onClick={() => {
+          this.deleteQuestion(index);
+        }} />, <Icon type='bars' className='icon' />]}>
+          <List.Item.Meta
+            avatar={<Avatar alt={index + 1} size='small' >{index + 1}</Avatar>}
+            title={item.no}
+            description={<Typography.Text ellipsis disabled>{item.description}</Typography.Text>}
+          />
+        </List.Item>
+      )} />;
+  }
+}

+ 0 - 0
front/project/admin/components/QuestionNoList/index.less


+ 78 - 0
front/project/admin/components/SimilarQuestionNo/index.js

@@ -0,0 +1,78 @@
+import React, { Component } from 'react';
+import { Modal, Row, Col, Checkbox, Alert } from 'antd';
+import './index.less';
+
+class SimilarQuestionNo extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { show: !!props.modal, loading: false };
+    this.questionNos = [];
+  }
+
+  onConfirm() {
+    const { questionNo } = this.state;
+    if (!questionNo) {
+      this.setState({ err: '请选择需要加载的题目' });
+      return;
+    }
+    if (this.props.onConfirm && this.props.modal) {
+      this.setState({ loading: true });
+      this.props
+        .onConfirm(questionNo)
+        .then(() => {
+          this.setState({ loading: false });
+          this.onCancel();
+        })
+        .catch(e => {
+          this.setState({ loading: false, err: e.message });
+        });
+    } else {
+      this.onCancel();
+    }
+  }
+
+  onCancel() {
+    if (this.props.modal) this.setState({ show: false });
+    if (this.props.onCancel) this.props.onCancel();
+  }
+
+  renderForm() {
+    const { questionNos = [] } = this.props;
+    const { questionNo } = this.state;
+    return <div>
+      {questionNos.map(row => {
+        return <Row style={{ width: '100%' }}>
+          <Col span={1}><Checkbox checked={questionNo && row.id === questionNo.id} onChange={(value) => {
+            this.setState({ questionNo: value ? row : null });
+          }} /></Col>
+          <Col span={4} offset={1}>
+            {row.no}
+          </Col>
+          <Col span={16} offset={1}>
+            {row.question.description}
+          </Col>
+        </Row>;
+      })}
+    </div>;
+  }
+
+  render() {
+    const { modal, title, confirmText = '确定', cancelText = '取消' } = this.props;
+    const { show, loading, err } = this.state;
+    return modal ? (
+      <Modal
+        title={title}
+        visible={show}
+        okText={confirmText}
+        cancelText={cancelText}
+        confirmLoading={loading}
+        onOk={() => this.onConfirm()}
+        onCancel={() => this.onCancel()}
+      >
+        {err && <Alert type="error" showIcon message={err} closable onClose={() => this.setState({ err: '' })} />}
+        {this.renderForm()}
+      </Modal>
+    ) : (<div>{this.renderForm()}</div>);
+  }
+}
+export default SimilarQuestionNo;

+ 0 - 0
front/project/admin/components/SimilarQuestionNo/index.less


+ 1 - 1
front/project/admin/routes/setting/place/page.js

@@ -5,7 +5,7 @@ import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import { formatFormError } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
-import { QuestionType } from '@src/services/Constant';
+import { QuestionType } from '../../../../Constant';
 import { System } from '../../../stores/system';
 
 export default class extends Page {

+ 1 - 1
front/project/admin/routes/setting/time/page.js

@@ -7,7 +7,7 @@ import EditTableCell from '@src/components/EditTableCell';
 import { getMap, flattenObject } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
 import TableLayout from '@src/layouts/TableLayout';
-import { QuestionDifficult } from '@src/services/Constant';
+import { QuestionDifficult } from '../../../../Constant';
 import { System } from '../../../stores/system';
 import { Examination } from '../../../stores/examination';
 import { Exercise } from '../../../stores/exercise';

+ 18 - 3
front/project/admin/routes/subject/exercise/page.js

@@ -7,13 +7,14 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { QuestionType, QuestionDifficult } from '@src/services/Constant';
 import { getMap, formatTreeData, bindSearch, formatDate } from '@src/services/Tools';
-import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
+import { asyncSMessage, asyncDelConfirm, asyncGet } from '@src/services/AsyncTools';
+import { QuestionType, QuestionDifficult } from '../../../../Constant';
 import { Exercise } from '../../../stores/exercise';
 import { System } from '../../../stores/system';
 import { Question } from '../../../stores/question';
 import { Slient } from '../../../stores/slient';
+// import Association from '../../../components/Association';
 
 const QuestionTypeMap = getMap(QuestionType, 'value', 'label');
 
@@ -164,6 +165,19 @@ export default class extends Page {
           {(
             <Link to={`/subject/question/${record.question_id}`}>编辑</Link>
           )}
+          {(
+            <a onClick={() => {
+              // this.setState({ detail: { title: '题源联想', ids: record.question.associationContent, field: 'associationContent', module: 'exercise', modal: true, show: true } });
+              asyncGet(() => import('../../../components/Association'),
+                { title: '题源联想', ids: record.question.associationContent, field: 'associationContent', module: 'exercise', modal: true },
+                (data) => {
+                  data.id = record.questionId;
+                  Question.edit(data).then(() => {
+                    asyncSMessage('修改成功!');
+                  });
+                });
+            }}>编辑</a>
+          )}
         </div>;
       },
     }];
@@ -228,7 +242,7 @@ export default class extends Page {
       data.endTime = data.time[1] || '';
     }
     Exercise.listQuestion(data).then(result => {
-      this.setTableData(result.list, result.total);
+      this.setTableData([{ question: {}, questionNo: { moduleStruct: [0] }, paper: {} }], result.total || 1);
     });
   }
 
@@ -278,6 +292,7 @@ export default class extends Page {
         onSelect={(keys, rows) => this.tableSelect(keys, rows)}
         selectedKeys={this.state.selectedKeys}
       />
+      {/* {this.state.detail && <Association {...this.state.detail} />} */}
     </Block>;
   }
 }

+ 2 - 1
front/project/admin/routes/subject/index.js

@@ -2,10 +2,11 @@ import question from './question';
 import exercise from './exercise';
 import examination from './examination';
 import textbook from './textbook';
+import textbookQuestion from './textbookQuestion';
 import sentence from './sentence';
 import preview from './preview';
 import previewDetail from './previewDetail';
 import sentenceArticle from './sentenceArticle';
 import sentenceQuestion from './sentenceQuestion';
 
-export default [question, exercise, examination, textbook, sentence, preview, previewDetail, sentenceArticle, sentenceQuestion];
+export default [question, exercise, examination, textbook, textbookQuestion, sentence, preview, previewDetail, sentenceArticle, sentenceQuestion];

+ 1 - 1
front/project/admin/routes/subject/preview/page.js

@@ -7,9 +7,9 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { PreviewStatus } from '@src/services/Constant';
 import { getMap, formatDate } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
+import { PreviewStatus } from '../../../../Constant';
 import { Exercise } from '../../../stores/exercise';
 import { Preview } from '../../../stores/preview';
 import { System } from '../../../stores/system';

+ 31 - 44
front/project/admin/routes/subject/previewDetail/page.js

@@ -1,13 +1,13 @@
 import React from 'react';
-import { Form, Input, Button, Row, Col, DatePicker, List, Icon, Typography, Avatar } from 'antd';
+import { Form, Input, Button, Row, Col, DatePicker } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import Select from '@src/components/Select';
-import DragList from '@src/components/DragList';
 // import FileUpload from '@src/components/FileUpload';
 import { formatFormError, generateSearch, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
+import QuestionNoList from '../../../components/QuestionNoList';
 import { Preview } from '../../../stores/preview';
 import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
@@ -40,12 +40,11 @@ export default class extends Page {
     if (id) {
       handler = Preview.get({ id });
     } else {
-      handler = Promise.resolve({ questionNos: [] });
+      handler = Promise.resolve({ questionNoIds: [] });
     }
     handler
       .then(result => {
-        const { questionNos } = result;
-        result.questionNos = result.questionNos.map(row => row.no);
+        const { questionNoIds } = result;
         form.setFieldsValue(result);
         generateSearch('userIds', { mode: 'multiple' }, this, (search) => {
           return User.list(search);
@@ -55,26 +54,10 @@ export default class extends Page {
             value: row.id,
           };
         }, result.userIds || [], null);
-        this.setState({ questionNos });
+        this.setState({ questionNoIds });
       });
   }
 
-  deleteQuestion(index) {
-    const { questionNos } = this.state;
-    questionNos.splice(index, 1);
-    this.setState({ questionNos });
-    this.props.form.setFieldsValue({ questionNos: questionNos.map(row => row.no) });
-  }
-
-  orderQuestion(oldIndex, newIndex) {
-    const { questionNos } = this.state;
-    const tmp = questionNos[oldIndex];
-    questionNos[oldIndex] = questionNos[newIndex];
-    questionNos[newIndex] = tmp;
-    this.setState({ questionNos });
-    this.props.form.setFieldsValue({ questionNos: questionNos.map(row => row.no) });
-  }
-
   submit() {
     const { form } = this.props;
     form.validateFields((err) => {
@@ -96,13 +79,29 @@ export default class extends Page {
     });
   }
 
-  searchQuestion(values) {
+  deleteQuestion(index) {
+    const { questionNos } = this.state;
+    questionNos.splice(index, 1);
+    this.setState({ questionNos });
+    this.props.form.setFieldsValue({ questionNos: questionNos.map(row => row.no) });
+  }
+
+  orderQuestion(oldIndex, newIndex) {
+    const { questionNos } = this.state;
+    const tmp = questionNos[oldIndex];
+    questionNos[oldIndex] = questionNos[newIndex];
+    questionNos[newIndex] = tmp;
+    this.setState({ questionNos });
+    this.props.form.setFieldsValue({ questionNos: questionNos.map(row => row.no) });
+  }
+
+  searchQuestion(values, field = 'nos') {
     if (values.length === 0) {
       this.setState({ questionNos: [] });
       return;
     }
     // 查找练习题目
-    Question.listNo({ no: values, module: 'exercise' }).then(result => {
+    Question.listNo({ [field]: values, module: 'exercise' }).then(result => {
       const map = getMap(result, 'no');
       const questionNos = values.map(no => map[no]).filter(row => row);
       this.setState({ questionNos });
@@ -158,7 +157,7 @@ export default class extends Page {
             ],
           })(
             <Select mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入题目id, 逗号分隔' tokenSeparators={[',', ',']} onChange={(values) => {
-              this.searchQuestion(values);
+              this.setState({ questionNos: values });
             }} />,
           )}
         </Form.Item>
@@ -167,27 +166,15 @@ export default class extends Page {
   }
 
   renderQuestionList() {
+    const { getFieldDecorator, setFieldsValue } = this.props.form;
     return <Block>
       <h1>题目预览</h1>
-      <DragList
-        loading={this.props.core.loading}
-        dataSource={this.state.questionNos || []}
-        handle={'.icon'}
-        onMove={(oldIndex, newIndex) => {
-          this.orderQuestion(oldIndex, newIndex);
-        }}
-        renderItem={(item, index) => (
-          <List.Item actions={[<Icon type='delete' onClick={() => {
-            this.deleteQuestion(index);
-          }} />, <Icon type='bars' className='icon' />]}>
-            <List.Item.Meta
-              avatar={<Avatar alt={index + 1} size='small' >{index + 1}</Avatar>}
-              title={item.no}
-              description={<Typography.Text ellipsis disabled>{item.stem}</Typography.Text>}
-            />
-          </List.Item>
-        )}
-      /></Block>;
+      <QuestionNoList module='exercise' loading={false} ids={this.state.questionNoIds} nos={this.state.questionNos} onChange={(questionNos) => {
+        this.questionNos = questionNos;
+        getFieldDecorator('questionNos');
+        setFieldsValue({ questionNos: questionNos.map(row => row.no) });
+      }} />
+    </Block>;
   }
 
   renderView() {

+ 2 - 1
front/project/admin/routes/subject/question/index.js

@@ -2,7 +2,8 @@ import module from '../../module';
 import group from '../group';
 
 export default {
-  path: '/subject/question/:id',
+  path: '/subject/question',
+  matchPath: '/subject/question/:id?',
   key: 'subject-question',
   title: '题目录入',
   needLogin: true,

File diff suppressed because it is too large
+ 66 - 1
front/project/admin/routes/subject/question/index.less


+ 799 - 1
front/project/admin/routes/subject/question/page.js

@@ -1,10 +1,808 @@
 import React from 'react';
+import { Tabs, Form, Tag, InputNumber, Radio, Row, Col, Checkbox, Icon, Input, Button, List, Cascader } from 'antd';
 import './index.less';
+import DragList from '@src/components/DragList';
+import Editor from '@src/components/Editor';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+import Select from '@src/components/Select';
+import { getMap, formatFormError, formatTreeData } from '@src/services/Tools';
+import { asyncSMessage, asyncGet } from '@src/services/AsyncTools';
+import { QuestionType, QuestionDifficult, QuestionStyleType, QuestionRadioDirection } from '../../../../Constant';
+import QuestionNoList from '../../../components/QuestionNoList';
+import { System } from '../../../stores/system';
+import { Question } from '../../../stores/question';
+import { Examination } from '../../../stores/examination';
+import { Exercise } from '../../../stores/exercise';
+
+const QuestionStyleTypeMap = getMap(QuestionStyleType, 'value', 'label');
 
 export default class extends Page {
+  constructor(props) {
+    super(props);
+    this.placeList = [];
+    this.placeSetting = null;
+    this.associationContent = [];
+    this.uuid = [];
+  }
+
+  init() {
+    Promise.all([
+      Exercise.allStruct().then(result => {
+        return { value: 'exercise', key: 'exercise', label: '练习', title: '练习', children: formatTreeData(result.map(row => { row.title = `${row.titleZh}/${row.titleEn}`; return row; }), 'id', 'title', 'parentId') };
+      }),
+      Examination.allStruct().then(result => {
+        return { value: 'examination', key: 'examination', label: '模考', title: '模考', children: formatTreeData(result.map(row => { row.title = `${row.titleZh}/${row.titleEn}`; return row; }), 'id', 'title', 'parentId') };
+      }),
+    ]).then(result => {
+      console.log(result);
+      this.setState({ moduleStructData: result });
+    });
+  }
+
+  initData() {
+    const { id } = this.params;
+    const { form } = this.props;
+    let handler;
+    if (id) {
+      handler = Question.get({ id }).then(result => {
+        result.content = result.content || { questions: [], steps: [] };
+        result.keyword = result.keyword || [];
+        return result;
+      });
+    } else {
+      handler = Promise.resolve({ content: { number: 1, type: 'single', typeset: 'one', questions: [], steps: [] } });
+    }
+    handler.then(result => {
+      this.uuid[0] = -1;
+      result.content.questions.forEach((row, index) => {
+        const keys = [];
+        this.uuid[index] = 0;
+        if (row.select && row.select.length > 0) {
+          for (; this.uuid[index] < row.select.length; this.uuid[index] += 1) {
+            keys.push(this.uuid[index]);
+            form.getFieldDecorator(`content.questions[${index}].select[${this.uuid}]`);
+            form.getFieldDecorator(`content.questions[${index}].answer[${this.uuid}]`);
+          }
+          const answerMap = {};
+          row.answer.forEach(r => {
+            answerMap[r] = true;
+          });
+          row.answer = row.select.map(r => answerMap[r] || false);
+        } else if (row.answer && row.answer.length > 0) {
+          for (; this.uuid[index] < row.answer.length; this.uuid[index] += 1) {
+            keys.push(this.uuid);
+            form.getFieldDecorator(`content.questions[${index}].answer[${this.uuid}]`);
+          }
+        }
+        form.getFieldDecorator(`content.questions[${index}].keys`);
+        row.keys = keys;
+        return row;
+      });
+      result.content.steps.forEach((row, index) => {
+        form.getFieldDecorator(`content.steps[${index}].title`);
+        form.getFieldDecorator(`content.steps[${index}].stem`);
+      });
+
+      form.getFieldDecorator('content.step');
+      form.getFieldDecorator('content.number');
+      form.setFieldsValue(result);
+      return result;
+    })
+      .then((result) => {
+        this.setState({ associationContentIds: result.associationContent || [] });
+        console.log(result.questionNoIds);
+        if (result.questionNoIds && result.questionNoIds.length > 0) {
+          return Question.listNo({ ids: result.questionNoIds }).then(list => {
+            this.setState({ questionNos: list });
+          });
+        }
+        return null;
+      });
+  }
+
+  refreshPlace(type) {
+    let handler = null;
+    if (this.placeSetting) {
+      handler = Promise.resolve(this.placeSetting);
+    } else {
+      handler = System.getPlace();
+    }
+    handler.then(result => {
+      this.placeSetting = result;
+      this.placeList = result[type] || [];
+    });
+  }
+
+  addNo() {
+    const { form } = this.props;
+    form.validateFields(['moduleStruct', 'questionNo'], (err) => {
+      if (!err) {
+        const data = form.getFieldsValue(['id', 'moduleStruct', 'questionNo']);
+        data.moduleStruct = data.moduleStruct.map(row => row);
+        data.module = data.moduleStruct.shift();
+        data.questionId = data.id || 0;
+        delete data.id;
+        data.no = data.questionNo;
+        delete data.questionNo;
+        // todo 合并struct+no:-连接
+        Question.addNo(data).then((result) => {
+          const { questionNos = [] } = this.state;
+          questionNos.push(result);
+          this.setState({ questionNos });
+          form.setFieldsValue({ moduleStruct: [], questionNo: '' });
+          asyncSMessage('保存成功');
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  removeNo(noId) {
+    let { questionNos } = this.state;
+    questionNos = questionNos.filter(row => row.id !== noId);
+    Question.delNo({ id: noId }).then(() => {
+      this.setState({ questionNos });
+    });
+  }
+
+  changeType(type) {
+    const { getFieldValue, setFieldsValue, getFieldDecorator } = this.props.form;
+    const number = getFieldValue('content.number');
+    // const keys = [];
+    this.uuid = [];
+    for (let index = 0; index < Number(number); index += 1) {
+      this.uuid[index] = 0;
+      switch (type) {
+        case 'double':
+          getFieldDecorator(`content.questions[${index}].direction`);
+          setFieldsValue({ [`content.questions[${index}].direction`]: 'landscape' });
+          break;
+        default:
+      }
+    }
+  }
+
+  removeQuestion(index, k) {
+    const { form } = this.props;
+    const keys = form.getFieldValue(`content.questions[${index}].keys`);
+    if (keys.length === 1) {
+      return;
+    }
+    form.setFieldsValue({
+      [`content.questions[${index}].keys`]: keys.filter(key => key !== k),
+    });
+  }
+
+  addQuestion(index) {
+    const { form } = this.props;
+    const keys = form.getFieldValue(`content.questions[${index}].keys`) || [];
+    this.uuid[index] += 1;
+    const nextKeys = keys.concat(this.uuid[index]);
+    form.setFieldsValue({
+      [`content.questions[${index}].keys`]: nextKeys,
+    });
+  }
+
+  orderQuestion(index, oldIndex, newIndex) {
+    const { form } = this.props;
+    const keys = form.getFieldValue(`content.questions[${index}].keys`) || [];
+    const tmp = keys[oldIndex];
+    keys[oldIndex] = keys[newIndex];
+    keys[newIndex] = tmp;
+    form.setFieldsValue({
+      [`content.questions[${index}].keys`]: keys,
+    });
+  }
+
+  changeQuestion(index, k, value) {
+    const { form } = this.props;
+    let answer = form.getFieldValue(`content.questions[${index}].answer`) || [];
+    answer = answer.map(() => !value);
+    answer[k] = !!value;
+    form.setFieldsValue({ [`content.questions[${index}].answer`]: answer });
+  }
+
+  changeDouble(index, k, o, value) {
+    const { form } = this.props;
+    const direction = form.getFieldValue(`content.questions[${index}].direction`);
+    let answer = form.getFieldValue(`content.questions[${index}].answer`) || [];
+    switch (direction) {
+      case 'landscape':
+        answer[k] = answer[k].map(() => !value);
+        if (o >= 0) {
+          answer[k][o] = !!value;
+        }
+        break;
+      case 'portrait':
+        answer = answer.map((row) => { row[o] = !value; return row; });
+        if (o >= 0) {
+          answer[k][o] = !!value;
+        }
+        break;
+      default:
+    }
+    form.setFieldsValue({ [`content.questions[${index}].answer`]: answer });
+  }
+
+  submit() {
+    const { form } = this.props;
+    const fields = ['type', 'place', 'difficult', 'content', 'stem', 'keyword', 'questionNoIds', 'officialContent', 'qxContent', 'associationContent'];
+    form.validateFields(fields, (err) => {
+      if (!err) {
+        const data = form.getFieldsValue(fields);
+        let handler;
+        data.associationContent = this.state.associationContent.map(row => row.id);
+        data.description = data.stem.replace(/<[^>]+>/g, '');
+        data.content.questions = data.content.questions.map(row => {
+          row.answer = row.answer.filter(r => r);
+          row.select = row.select.filter(r => r);
+          return row;
+        });
+        if (data.id) {
+          handler = Question.add(data);
+        } else {
+          handler = Question.edit(data);
+        }
+        handler.then(() => {
+          asyncSMessage('保存成功');
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  searchStem() {
+    const { form } = this.props;
+    const content = form.getFieldValue('stem').replace(/<[^>]+>/g, '');
+    Question.searchStem({ content })
+      .then(result => {
+        if (result.list.length > 0) {
+          asyncGet(() => import('../../../components/SimilarQuestionNo'),
+            { title: '找到可匹配题目', questionNos: result.list, modal: true },
+            (questionNo) => {
+              this.inited = false;
+              linkTo(`/subject/question/${questionNo.questionId}`);
+              return Promise.resolve();
+            });
+        } else {
+          asyncSMessage('无可匹配题目');
+        }
+      });
+  }
+
+  renderBase() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题干信息</h1>
+      <Form>
+        {getFieldDecorator('id')(<input hidden />)}
+        {getFieldDecorator('stem', {
+        })(
+          <Editor modules={{
+            toolbar: {
+              container: [
+                ['image', 'table', 'select'],
+                [{ header: '1' }, { header: '2' }],
+                ['bold', 'underline', 'blockquote'],
+                [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
+              ],
+              handlers: {
+                table: (quill) => {
+                  const range = quill.getSelection(true);
+                  quill.insertText(range.index, '#table#');
+                },
+                select: (quill) => {
+                  const range = quill.getSelection(true);
+                  quill.insertText(range.index, '#select#');
+                },
+              },
+            },
+          }} placeholder='请输入内容' onUpload={(file) => System.uploadImage(file)} />,
+        )}
+        <Button style={{ marginBottom: '10px', marginTop: '10px' }} onClick={() => {
+          this.searchStem();
+        }}>查询相似</Button>
+        <Row>
+          <Col span={12}>
+            <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='递进层次'>
+              {getFieldDecorator('content.step', {
+                normalize: (value, preValue) => {
+                  if (value === undefined || value === '' || value === null) return preValue;
+                  if (Math.abs(value - preValue) > 1) return preValue;
+                  return value;
+                },
+              })(
+                <InputNumber defaultValue={0} min={1} max={10} placeholder='输入数量' />,
+              )}
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='折叠表格'>
+              <Col span={1}>行</Col>
+              <Col span={9} offset={1}>
+                {getFieldDecorator('content.table.row')(
+                  <InputNumber placeholder='行' defaultValue={0} precision={0} min={0} />,
+                )}
+              </Col>
+              <Col span={1} offset={1}>列</Col>
+              <Col span={9} offset={1}>
+                {getFieldDecorator('content.table.col')(
+                  <InputNumber placeholder='列' defaultValue={0} precision={0} min={0} />,
+                )}
+              </Col>
+            </Form.Item>
+          </Col>
+        </Row>
+        {this.renderTable()}
+      </Form>
+    </Block>;
+  }
+
+  renderTable() {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    const row = Number(getFieldValue('content.table.row'));
+    const col = Number(getFieldValue('content.table.col'));
+    const table = [];
+    if (row === 0 || col === 0) return table;
+    const span = 100 / col;
+
+    const colums = [];
+    for (let i = 0; i < col; i += 1) {
+      colums.push(<div style={{ width: `${span}%` }} className='table-col'>{getFieldDecorator(`content.table.header[${i}]`)(<Input size='small' />)}</div>);
+    }
+    table.push(<Row style={{ width: '100%' }} className='table-header' gutter={10}>{colums}</Row>);
+    for (let index = 0; index < row; index += 1) {
+      const cols = [];
+      for (let i = 0; i < col; i += 1) {
+        cols.push(<div style={{ width: `${span}%` }} className='table-col'>{getFieldDecorator(`content.table.data[${index}][${i}]`)(<Input size='small' />)}</div>);
+      }
+      table.push(<Row style={{ width: '100%' }} gutter={10}>{cols}</Row>);
+    }
+    return table;
+  }
+
+  renderStep() {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    const number = getFieldValue('content.step');
+    const result = [];
+    for (let index = 0; index < Number(number); index += 1) {
+      result.push(<Block flex>
+        <h1>递进层次 {index + 1}</h1>
+        <Form>
+          <Form.Item>
+            {getFieldDecorator(`content.steps[${index}].title`, {
+              rules: [
+                { required: true, message: '请输入标题' },
+              ],
+            })(
+              <Input placeholder='请输入标题' />,
+            )}
+          </Form.Item>
+          <Form.Item>
+            {getFieldDecorator(`content.steps[${index}].stem`, {
+            })(
+              <Editor placeholder='请输入内容' />,
+            )}
+          </Form.Item>
+        </Form>
+      </Block>);
+    }
+    return result;
+  }
+
+  renderIdentity() {
+    const { questionNos = [] } = this.state;
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题目身份</h1>
+      <Form>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 17 }} label='请输入题目id'>
+          {getFieldDecorator('questionNoIds', {
+            rules: [{
+              required: true, message: '添加关联题目ID',
+            }],
+          })(<input hidden />)}
+          {questionNos.map((no, index) => {
+            return <Tag key={index} closable onClose={() => {
+              this.removeNo(no.id);
+            }}>
+              {no.no}
+            </Tag>;
+          })}
+          <Row>
+            <Col span={14}>
+              <Form.Item>
+                {getFieldDecorator('moduleStruct', {
+                  rules: [{
+                    required: true, message: '选择题目编号关系',
+                  }],
+                })(<Cascader fieldNames={{ label: 'title', value: 'value', children: 'children' }} onClick={(value) => {
+                  this.setState({ moduleStruct: value });
+                }} placeholder='选择' options={this.state.moduleStructData} />)}
+              </Form.Item>
+            </Col>
+            <Col span={4} offset={1}>
+              <Form.Item>
+                {getFieldDecorator('questionNo', {
+                  rules: [{
+                    required: true, message: '输入编号',
+                  }],
+                })(<Input placeholder='题目id' onClick={(value) => {
+                  this.setState({ questionNo: value });
+                }} />)}
+              </Form.Item>
+            </Col>
+            <Col span={4} offset={1}>
+              <Button size='small' onClick={() => {
+                this.addNo();
+              }}><Icon type='plus' /></Button>
+            </Col>
+          </Row>
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label={'关键词'}>
+          {getFieldDecorator('keyword')(
+            <Select mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入多个关键词, 逗号分隔' tokenSeparators={[',', ',']} />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderAttr() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题目属性</h1>
+      <Form>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='题型'>
+          {getFieldDecorator('type', {
+            rules: [
+              { required: true, message: '请选择题型' },
+            ],
+          })(
+            <Select select={QuestionType} placeholder='请选择题型' onChange={(v) => {
+              this.refreshPlace(v);
+            }} />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='考点'>
+          {getFieldDecorator('place', {
+            rules: [
+              { required: true, message: '请选择考点' },
+            ],
+          })(
+            <Select select={this.placeList} placeholder='请选择考点' onChange={(v) => {
+              this.refreshPart(v);
+            }} />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='难度'>
+          {getFieldDecorator('difficult', {
+            rules: [
+              { required: true, message: '请选择难度' },
+            ],
+          })(
+            <Select select={QuestionDifficult} placeholder='请选择难度' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderStyle() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题目样式</h1>
+      <Form>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='选项类型'>
+          {getFieldDecorator('content.type', {
+            rules: [
+              { required: true, message: '请选择类型' },
+            ],
+          })(
+            <Select select={QuestionStyleType} placeholder='请选择类型' onChange={(type) => {
+              this.changeType(type);
+            }} />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='题目数量'>
+          {getFieldDecorator('content.number', {
+            normalize: (value, preValue) => {
+              if (value === undefined || value === '' || value === null) return preValue;
+              if (Math.abs(value - preValue) > 1) return preValue;
+              return value;
+            },
+          })(
+            <InputNumber defaultValue={1} min={1} max={10} placeholder='请输入数量' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='排版方式'>
+          {getFieldDecorator('content.typeset')(
+            <Radio.Group defaultValue='one'>
+              <Radio value='one'>单排</Radio>
+              <Radio value='two'>双排</Radio>
+            </Radio.Group>,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderSelect() {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    const number = getFieldValue('content.number');
+    const type = getFieldValue('content.type');
+    const result = [];
+    let handler = null;
+    switch (type) {
+      case 'single':
+        handler = (index) => this.renderSelectSingle(index);
+        break;
+      case 'double':
+        handler = (index) => this.renderSelectDouble(index);
+        break;
+      case 'inline':
+        handler = (index) => this.renderSelectInline(index);
+        break;
+      default:
+    }
+    for (let index = 0; index < Number(number); index += 1) {
+      result.push(<Block flex className={type}>
+        <h1>选项信息({QuestionStyleTypeMap[type]})</h1>
+        <Form>
+          {type !== 'inline' && (
+            <Form.Item>
+              {getFieldDecorator(`content.questions[${index}].description`, {
+              })(
+                <Editor placeholder='选项问题说明' />,
+              )}
+            </Form.Item>
+          )}
+          {handler(index)}
+        </Form>
+      </Block>);
+    }
+    return result;
+  }
+
+  renderSelectSingle(index) {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    getFieldDecorator(`content.questions[${index}].keys`);
+    const keys = getFieldValue(`content.questions[${index}].keys`) || [];
+    return [
+      <DragList
+        loading={false}
+        dataSource={keys || []}
+        handle={'.icon'}
+        onMove={(oldIndex, newIndex) => {
+          this.order(index, oldIndex, newIndex);
+        }}
+        renderItem={(k) => (
+          <List.Item actions={[<Icon type='bars' className='icon' />]}>
+            <Row key={k} style={{ width: '100%' }}>
+              <Col span={1}>
+                {getFieldDecorator(`content.questions[${index}].answer[${k}]`, {
+                  valuePropName: 'checked',
+                })(
+                  <Checkbox onChange={(value) => {
+                    this.changeQuestion(index, k, value);
+                  }} />,
+                )}
+              </Col>
+              <Col span={23}>
+                <Form.Item
+                  key={k}
+                  hidden
+                >
+                  {getFieldDecorator(`content.questions[${index}].select[${k}]`, {
+                    rules: [{
+                      required: true,
+                      whitespace: true,
+                      message: '请填写选项信息',
+                    }],
+                  })(
+                    <Input />,
+                  )}
+                  {keys.length > 1 ? (
+                    <Icon
+                      type='minus-circle-o'
+                      disabled={keys.length === 1}
+                      onClick={() => this.removeQuestion(index, k)}
+                    />
+                  ) : null}
+                </Form.Item>
+              </Col>
+            </Row>
+          </List.Item>
+        )}
+      />,
+      <Form.Item>
+        <Button type='dashed' onClick={() => this.addQuestion(index)} >
+          <Icon type='plus' /> 新增
+      </Button>
+      </Form.Item>,
+    ];
+  }
+
+  renderSelectDouble(index) {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    getFieldDecorator(`content.questions[${index}].keys`);
+    const keys = getFieldValue(`content.questions[${index}].keys`) || [];
+    return [
+      <Form.Item className='no-validate' labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='单选方向'>
+        {getFieldDecorator(`content.questions[${index}].direction`)(
+          <Select select={QuestionRadioDirection} placeholder='请选择方向' onChange={(value) => {
+            keys.forEach((k) => this.changeDouble(index, k, -1, value));
+          }} />,
+        )}
+      </Form.Item>,
+      <List.Item actions={[<Icon type='bars' style={{ display: 'none' }} className='icon' />]}><Row style={{ width: '100%' }}>
+        <Col span={4}>
+          {getFieldDecorator(`content.questions[${index}].first`)(
+            <Input placeholder='选项一' />,
+          )}
+        </Col>
+        <Col span={4} offset={1}>
+          {getFieldDecorator(`content.questions[${index}].second`)(
+            <Input placeholder='选项二' />,
+          )}
+        </Col>
+      </Row></List.Item>,
+      <DragList
+        loading={false}
+        dataSource={keys || []}
+        handle={'.icon'}
+        onMove={(oldIndex, newIndex) => {
+          this.orderQuestion(index, oldIndex, newIndex);
+        }}
+        renderItem={(k) => (
+          <List.Item actions={[<Icon type='bars' className='icon' />]}>
+            <Row key={k} style={{ width: '100%' }}>
+              <Col span={4}>
+                {getFieldDecorator(`content.questions[${index}].answer[${k}][0]`, {
+                  valuePropName: 'checked',
+                })(
+                  <Checkbox onChange={(value) => {
+                    this.changeDouble(index, k, 0, value);
+                  }} />,
+                )}
+              </Col>
+              <Col span={4} offset={1}>
+                {getFieldDecorator(`content.questions[${index}].answer[${k}][1]`, {
+                  valuePropName: 'checked',
+                })(
+                  <Checkbox onChange={(value) => {
+                    this.changeDouble(index, k, 1, value);
+                  }} />,
+                )}
+              </Col>
+              <Col span={14} offset={1}>
+                <Form.Item
+                  key={k}
+                  hidden
+                >
+                  {getFieldDecorator(`content.questions[${index}].select[${k}]`, {
+                    rules: [{
+                      required: true,
+                      whitespace: true,
+                      message: '请填写选项信息',
+                    }],
+                  })(
+                    <Input />,
+                  )}
+                  {keys.length > 1 ? (
+                    <Icon
+                      type='minus-circle-o'
+                      disabled={keys.length === 1}
+                      onClick={() => this.removeQuestion(index, k)}
+                    />
+                  ) : null}
+                </Form.Item>
+              </Col>
+            </Row>
+          </List.Item>
+        )}
+      />,
+      <Form.Item>
+        <Button type='dashed' onClick={() => this.addQuestion(index)} >
+          <Icon type='plus' /> 新增
+      </Button>
+      </Form.Item>,
+    ];
+  }
+
+  renderSelectInline(index) {
+    return this.renderSelectSingle(index);
+  }
+
+  renderOffical() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <Form>
+        <Form.Item label='官方解析'>
+          {getFieldDecorator('officalContent', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderQX() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <Form>
+        <Form.Item label='千行解析'>
+          {getFieldDecorator('qxContent', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderAssociation() {
+    const { getFieldDecorator, setFieldsValue } = this.props.form;
+    return <Block flex>
+      <h1>题源联想</h1>
+      <Form>
+        <Form.Item>
+          {getFieldDecorator('associationContent')(
+            <Select mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入题目id, 逗号分隔' tokenSeparators={[',', ',']} onChange={(values) => {
+              this.setState({ associationContent: values });
+            }} />,
+          )}
+        </Form.Item>
+        <QuestionNoList loading={false} ids={this.state.associationContentIds} nos={this.state.associationContent} onChange={(questionNos) => {
+          this.associationContent = questionNos;
+          getFieldDecorator('associationContent');
+          setFieldsValue({ associationContent: questionNos.map(row => row.no) });
+        }} />
+      </Form>
+    </Block>;
+  }
+
+  renderTab() {
+    return <Tabs activeKey='base' onChange={(tab) => {
+      switch (tab) {
+        case 'sentence':
+          linkTo('/subject/sentence/question');
+          break;
+        case 'textbook':
+          linkTo('/subject/textbook/question');
+          break;
+        default:
+      }
+    }}>
+      <Tabs.TabPane key='base' tab='考试题型' />
+      <Tabs.TabPane key='sentence' tab='长难句' />
+      <Tabs.TabPane key='textbook' tab='数学机经' />
+    </Tabs>;
+  }
+
   renderView() {
-    return <Block flex />;
+    return <div flex >
+      {this.renderTab()}
+      {this.renderBase()}
+      {this.renderStep()}
+      {this.renderIdentity()}
+      {this.renderAttr()}
+      {this.renderStyle()}
+      {this.renderSelect()}
+      {this.renderOffical()}
+      {this.renderQX()}
+      {this.renderAssociation()}
+      <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>
+    </div>;
   }
 }

+ 1 - 2
front/project/admin/routes/subject/sentence/page.js

@@ -8,7 +8,6 @@ import EditTableCell from '@src/components/EditTableCell';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-// import { PreviewStatus } from '@src/services/Constant';
 import { getMap } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
 import { Sentence } from '../../../stores/sentence';
@@ -253,7 +252,7 @@ export default class extends Page {
           list={this.state.detail.chapters}
           pagination={false}
         />
-        <Button type='link' onClick={() => {
+        <Button onClick={() => {
           const { detail } = this.state;
           detail.chapters.push({});
           this.setState({ detail });

+ 20 - 1
front/project/admin/routes/subject/sentenceQuestion/index.less

@@ -1,3 +1,22 @@
 @charset "utf-8";
 
-#subject-sentence-question {}
+#subject-sentence-question {
+  .answer {
+    width: 100%;
+    // border: 1px solid #d9d9d9;
+    padding: 10px;
+
+    tr {}
+
+    tr td:first-child {
+      width: 20%;
+      text-align: center;
+      font-weight: bold;
+      padding-bottom: 25px;
+    }
+  }
+
+  .ant-checkbox-group-item {
+    margin-right: 50px;
+  }
+}

+ 152 - 67
front/project/admin/routes/subject/sentenceQuestion/page.js

@@ -1,18 +1,32 @@
 import React from 'react';
-import { Form, Input, Button, Row, Col, DatePicker, List, Icon } from 'antd';
+import { Form, Input, InputNumber, Tabs, Switch, Checkbox, Row, Col, Button } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+import Editor from '@src/components/Editor';
 import Select from '@src/components/Select';
 // import FileUpload from '@src/components/FileUpload';
-import { formatFormError, generateSearch } from '@src/services/Tools';
+import { formatFormError } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
+import { SentenceOption } from '../../../../Constant';
 import { Preview } from '../../../stores/preview';
 import { Exercise } from '../../../stores/exercise';
-import { User } from '../../../stores/user';
+import { Sentence } from '../../../stores/sentence';
 import config from './index';
 
 export default class extends Page {
+  constructor(props) {
+    super(props);
+
+    const { id } = this.params;
+
+    if (id) {
+      config.title = '编辑长难句题目';
+    } else {
+      config.title = '添加长难句题目';
+    }
+  }
+
   init() {
     Exercise.allStruct().then(result => {
       result = result.filter(row => row.level === 2).map(row => { row.title = `${row.titleZh}/${row.titleEn}`; row.value = row.id; return row; });
@@ -25,23 +39,13 @@ export default class extends Page {
     const { form } = this.props;
     let handler;
     if (id) {
-      config.title = '编辑预习作业';
       handler = Preview.get({ id });
     } else {
-      config.title = '添加预习作业';
-      handler = Promise.resolve({});
+      handler = Promise.resolve({ no: 1 });
     }
     handler
       .then(result => {
         form.setFieldsValue(result);
-        generateSearch('userIds', { mode: 'multiple' }, this, (search) => {
-          return User.list(search);
-        }, (row) => {
-          return {
-            title: `${row.nickname}(${row.mobile})`,
-            value: row.id,
-          };
-        }, result.userIds || [], null);
       });
   }
 
@@ -50,11 +54,12 @@ export default class extends Page {
     form.validateFields((err) => {
       if (!err) {
         const data = form.getFieldsValue();
+        data.isTrail = data.isTrail ? 1 : 0;
         let handler;
         if (data.id) {
-          handler = Preview.edit(data);
+          handler = Sentence.editQuestion(data);
         } else {
-          handler = Preview.add(data);
+          handler = Sentence.addQuestion(data);
         }
         handler.then(() => {
           asyncSMessage('保存成功');
@@ -65,87 +70,167 @@ export default class extends Page {
     });
   }
 
-  searchQuestion(values) {
-    console.log(values);
-  }
-
-  renderBase() {
+  renderTitle() {
     const { getFieldDecorator } = this.props.form;
     return <Block>
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='选择课程'>
-          {getFieldDecorator('category', {
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='题目序号'>
+          {getFieldDecorator('no', {
             rules: [
-              { required: true, message: '请选择课程' },
+              { required: true, message: '请输入序号' },
+              // {
+              //   validator: (rule, value, callback) => {
+              //     if (this.partList.indexOf(value) >= 0) callback('该part已被使用');
+              //     else callback();
+              //     callback();
+              //   },
+              // },
             ],
           })(
-            <Select select={this.state.exercise} placeholder='请选择课程' />,
+            <InputNumber min={1} precision={0} formatter={(v) => parseInt(v, 10) || 1} />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='起止时间'>
-          {getFieldDecorator('time', {
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='名称'>
+          {getFieldDecorator('title', {
             rules: [
-              { required: true, message: '请输入起止时间' },
+              { required: true, message: '请输入名称' },
             ],
           })(
-            <DatePicker.RangePicker />,
+            <Input placeholder='请输入' />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='作业标题'>
-          {getFieldDecorator('title', {
-            rules: [
-              { required: true, message: '请输入作业标题' },
-            ],
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='开放试用'>
+          {getFieldDecorator('isTrail', {
+            valuePropName: 'checked',
           })(
-            <Input placeholder='请输入作业标题' />,
+            <Switch checkedChildren='on' unCheckedChildren='off' />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='指定做题人'>
-          {getFieldDecorator('userIds', {
-            rules: [
-              { required: true, message: '请指定做题人' },
-            ],
+      </Form>
+    </Block>;
+  }
+
+  renderBase() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题干信息</h1>
+      <Form>
+        {getFieldDecorator('id')(<input hidden />)}
+        <Form.Item>
+          {getFieldDecorator('stem', {
           })(
-            <Select {...this.state.userIds} placeholder='请指定做题人' />,
+            <Editor placeholder='请输入内容' />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='选择作业题'>
-          {getFieldDecorator('questionIds', {
-            rules: [
-              { required: true, message: '请选择作业题' },
-            ],
+      </Form>
+    </Block>;
+  }
+
+  renderAnswer() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>题目答案</h1>
+      <table boarder cellSpacing className='answer'>
+        <tr>
+          <td>主语</td>
+          <td>
+            <Form.Item>
+              {getFieldDecorator('content.subject')(
+                <Select mode='tags' maxTagCount={200} notFoundContent={null} tokenSeparators={[',', ',']} />,
+              )}
+            </Form.Item>
+          </td>
+        </tr>
+        <tr>
+          <td>谓语</td>
+          <td><Form.Item>
+            {getFieldDecorator('content.predicate')(
+              <Select mode='tags' maxTagCount={200} notFoundContent={null} tokenSeparators={[',', ',']} />,
+            )}
+          </Form.Item></td>
+        </tr>
+        <tr>
+          <td>宾语</td>
+          <td><Form.Item>
+            {getFieldDecorator('content.object')(
+              <Select mode='tags' maxTagCount={200} notFoundContent={null} tokenSeparators={[',', ',']} />,
+            )}
+          </Form.Item></td>
+        </tr>
+      </table>
+    </Block>;
+  }
+
+  renderOption() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <h1>选项信息(长难句)</h1>
+      <Form>
+        <Form.Item>
+          {getFieldDecorator('content.options')(
+            <Checkbox.Group options={SentenceOption} />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderQX() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <Form>
+        <Form.Item label='千行解析'>
+          {getFieldDecorator('qxContent', {
           })(
-            <Select select={[]} mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入题目id, 逗号分隔' tokenSeparators={[',', ',']} onSelect={(values) => {
-              this.searchQuestion(values);
-            }} />,
+            <Editor placeholder='输入内容' />,
           )}
         </Form.Item>
       </Form>
     </Block>;
   }
 
-  renderQuestionList() {
-    return <List
-      header={<h1>题目预览</h1>}
-      loading={this.props.core.loading}
-      itemLayout="horizontal"
-      dataSource={this.state.questionList || [{}]}
-      renderItem={item => (
-        <List.Item actions={[<Button type='link' onClick={() => {
-          console.log(item);
-        }}><Icon type='delete' /></Button>, <a>more</a>]}>
-          123123
-        </List.Item>
-      )}
-    />;
+  renderChinese() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block flex>
+      <Form>
+        <Form.Item label='中文解析'>
+          {getFieldDecorator('chinese', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderTab() {
+    return <Tabs activeKey='sentence' onChange={(tab) => {
+      switch (tab) {
+        case 'textbook':
+          linkTo('/subject/textbook/question');
+          break;
+        case 'base':
+          linkTo('/subject/question');
+          break;
+        default:
+      }
+    }}>
+      <Tabs.TabPane key='base' tab='考试题型' />
+      <Tabs.TabPane key='sentence' tab='长难句' />
+      <Tabs.TabPane key='textbook' tab='数学机经' />
+    </Tabs>;
   }
 
   renderView() {
-    return <Block flex>
+    return <div flex >
+      {this.renderTab()}
+      {this.renderTitle()}
       {this.renderBase()}
-      {this.renderQuestionList()}
-
+      {this.renderAnswer()}
+      {this.renderOption()}
+      {this.renderQX()}
+      {this.renderChinese()}
       <Row type="flex" justify="center">
         <Col>
           <Button type="primary" onClick={() => {
@@ -153,6 +238,6 @@ export default class extends Page {
           }}>保存</Button>
         </Col>
       </Row>
-    </Block>;
+    </div>;
   }
 }

+ 15 - 0
front/project/admin/routes/subject/textbookQuestion/index.js

@@ -0,0 +1,15 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/subject/textbook/question/:id?',
+  key: 'subject-textbook-question',
+  title: '新建机经题目',
+  needLogin: true,
+  module,
+  group,
+  showKey: 'subject-textbook',
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/subject/textbookQuestion/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#subject-textbook-question {}

+ 151 - 0
front/project/admin/routes/subject/textbookQuestion/page.js

@@ -0,0 +1,151 @@
+import React from 'react';
+import { Form, Input, Tabs, DatePicker } from 'antd';
+import './index.less';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+import Select from '@src/components/Select';
+// import FileUpload from '@src/components/FileUpload';
+import { formatFormError, generateSearch } from '@src/services/Tools';
+import { asyncSMessage } from '@src/services/AsyncTools';
+import { Preview } from '../../../stores/preview';
+import { Exercise } from '../../../stores/exercise';
+import { User } from '../../../stores/user';
+import config from './index';
+
+export default class extends Page {
+  init() {
+    Exercise.allStruct().then(result => {
+      result = result.filter(row => row.level === 2).map(row => { row.title = `${row.titleZh}/${row.titleEn}`; row.value = row.id; return row; });
+      this.setState({ exercise: result });
+    });
+  }
+
+  initData() {
+    const { id } = this.params;
+    const { form } = this.props;
+    let handler;
+    if (id) {
+      config.title = '编辑预习作业';
+      handler = Preview.get({ id });
+    } else {
+      config.title = '添加预习作业';
+      handler = Promise.resolve({});
+    }
+    handler
+      .then(result => {
+        form.setFieldsValue(result);
+        generateSearch('userIds', { mode: 'multiple' }, this, (search) => {
+          return User.list(search);
+        }, (row) => {
+          return {
+            title: `${row.nickname}(${row.mobile})`,
+            value: row.id,
+          };
+        }, result.userIds || [], null);
+      });
+  }
+
+  submit() {
+    const { form } = this.props;
+    form.validateFields((err) => {
+      if (!err) {
+        const data = form.getFieldsValue();
+        let handler;
+        if (data.id) {
+          handler = Preview.edit(data);
+        } else {
+          handler = Preview.add(data);
+        }
+        handler.then(() => {
+          asyncSMessage('保存成功');
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  searchQuestion(values) {
+    console.log(values);
+  }
+
+  renderBase() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block>
+      <Form>
+        {getFieldDecorator('id')(<input hidden />)}
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='选择课程'>
+          {getFieldDecorator('category', {
+            rules: [
+              { required: true, message: '请选择课程' },
+            ],
+          })(
+            <Select select={this.state.exercise} placeholder='请选择课程' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='起止时间'>
+          {getFieldDecorator('time', {
+            rules: [
+              { required: true, message: '请输入起止时间' },
+            ],
+          })(
+            <DatePicker.RangePicker />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='作业标题'>
+          {getFieldDecorator('title', {
+            rules: [
+              { required: true, message: '请输入作业标题' },
+            ],
+          })(
+            <Input placeholder='请输入作业标题' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='指定做题人'>
+          {getFieldDecorator('userIds', {
+            rules: [
+              { required: true, message: '请指定做题人' },
+            ],
+          })(
+            <Select {...this.state.userIds} placeholder='请指定做题人' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='选择作业题'>
+          {getFieldDecorator('questionIds', {
+            rules: [
+              { required: true, message: '请选择作业题' },
+            ],
+          })(
+            <Select select={[]} mode='tags' maxTagCount={200} notFoundContent={null} placeholder='输入题目id, 逗号分隔' tokenSeparators={[',', ',']} onSelect={(values) => {
+              this.searchQuestion(values);
+            }} />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderTab() {
+    return <Tabs activeKey='textbook' onChange={(tab) => {
+      switch (tab) {
+        case 'sentence':
+          linkTo('/subject/sentence/question');
+          break;
+        case 'base':
+          linkTo('/subject/question');
+          break;
+        default:
+      }
+    }}>
+      <Tabs.TabPane key='base' tab='考试题型' />
+      <Tabs.TabPane key='sentence' tab='长难句' />
+      <Tabs.TabPane key='textbook' tab='数学机经' />
+    </Tabs>;
+  }
+
+  renderView() {
+    return <div flex >
+      {this.renderTab()}
+    </div>;
+  }
+}

+ 2 - 3
front/project/admin/routes/system/manager/list/page.js

@@ -1,5 +1,4 @@
 import React from 'react';
-import { Button } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import TableLayout from '@src/layouts/TableLayout';
@@ -49,9 +48,9 @@ export default class extends Page {
         render: (text, record) => {
           return <div className="table-button">
             {(
-              <Button type='link' onClick={() => {
+              <a onClick={() => {
                 this.editAction(record);
-              }} >编辑</Button>
+              }} >编辑</a>
             )}
           </div>;
         },

+ 1 - 1
front/project/admin/routes/user/ask/page.js

@@ -7,7 +7,7 @@ import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
 import { getMap, bindSearch, formatDate } from '@src/services/Tools';
-import { QuestionType, AskStatus, MoneyRange, SwitchSelect, AskTarget } from '@src/services/Constant';
+import { QuestionType, AskStatus, MoneyRange, SwitchSelect, AskTarget } from '../../../../Constant';
 import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
 

+ 1 - 1
front/project/admin/routes/user/askDetail/page.js

@@ -7,8 +7,8 @@ import Block from '@src/components/Block';
 import DragList from '@src/components/DragList';
 // import FileUpload from '@src/components/FileUpload';
 import { formatFormError, formatDate, getMap } from '@src/services/Tools';
-import { AskTarget, QuestionType } from '@src/services/Constant';
 import { asyncSMessage } from '@src/services/AsyncTools';
+import { AskTarget, QuestionType } from '../../../../Constant';
 import { User } from '../../../stores/user';
 
 const QuestionTypeMap = getMap(QuestionType, 'value', 'label');

+ 11 - 5
front/project/admin/routes/user/detail/page.js

@@ -1,11 +1,11 @@
 import React from 'react';
-import { Form, Row, Col, Avatar } from 'antd';
+import { Form, Row, Col, Avatar, Button } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import TableLayout from '@src/layouts/TableLayout';
-import { PrepareStatus, PrepareExaminationTime, ServiceKey, PayModule } from '@src/services/Constant';
 import { formatDate, getMap, formatMoney } from '@src/services/Tools';
+import { UserUrl, PrepareStatus, PrepareExaminationTime, ServiceKey, PayModule } from '../../../../Constant';
 import { User } from '../../../stores/user';
 import { Exercise } from '../../../stores/exercise';
 
@@ -83,7 +83,7 @@ export default class extends Page {
   }
 
   renderBase() {
-    const { data } = this.state;
+    const { data = {} } = this.state;
     return <Block>
       <h1>用户基本信息</h1>
       <Form>
@@ -183,7 +183,7 @@ export default class extends Page {
   }
 
   renderService() {
-    const { data } = this.state;
+    const { data = {} } = this.state;
     return <Block>
       <h1>服务开通</h1>
       <div className="group">
@@ -224,7 +224,13 @@ export default class extends Page {
       </div>
       <div className="group">
         <h2>学习数据</h2>
-
+        <Button onClick={() => {
+          User.token({ id: this.params.id || 1 })
+            .then(token => {
+              const w = window.open('about:blank');
+              w.location.href = `${UserUrl}/my/data?token=${token}`;
+            });
+        }}>查看</Button>
       </div>
     </Block>;
   }

+ 1 - 1
front/project/admin/routes/user/list/page.js

@@ -6,8 +6,8 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 // import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { SwitchSelect, ServiceKey } from '@src/services/Constant';
 import { getMap, formatMoney } from '@src/services/Tools';
+import { SwitchSelect, ServiceKey } from '../../../../Constant';
 import { User } from '../../../stores/user';
 
 const SwitchSelectMap = getMap(SwitchSelect, 'value', 'label');

+ 12 - 4
front/project/admin/stores/question.js

@@ -2,17 +2,25 @@ import BaseStore from '@src/stores/base';
 
 export default class QuestionStore extends BaseStore {
   searchNo(params) {
-    return this.apiGet('/question/search/no', params);
+    return this.apiPost('/question/search/no', params);
   }
 
   searchStem(params) {
-    return this.apiGet('/question/search/stem', params);
+    return this.apiPost('/question/search/stem', params);
   }
 
   listNo(params) {
     return this.apiPost('/question/list/no', params);
   }
 
+  addNo(params) {
+    return this.apiPost('/question/add/no', params);
+  }
+
+  delNo(params) {
+    return this.apiDel('/question/delete/no', params);
+  }
+
   add(params) {
     return this.apiPost('/question/add', params);
   }
@@ -21,8 +29,8 @@ export default class QuestionStore extends BaseStore {
     return this.apiPut('/question/edit', params);
   }
 
-  del(params) {
-    return this.apiDel('/question/delete', params);
+  get(params) {
+    return this.apiGet('/question/detail', params);
   }
 }
 

+ 4 - 0
front/project/admin/stores/user.js

@@ -5,6 +5,10 @@ export default class UserStore extends BaseStore {
     return this.apiGet('/user/list', params);
   }
 
+  token(params) {
+    return this.apiGet('/user/token', params);
+  }
+
   get(params) {
     return this.apiPost('/user/detail', params);
   }

+ 89 - 77
front/src/components/Editor/index.js

@@ -3,67 +3,61 @@ import ReactQuill from 'react-quill';
 import 'react-quill/dist/quill.snow.css';
 import { uuid } from '../../services/Tools';
 
+const modules = {
+  toolbar: {
+    container: [
+      [{ header: '1' }, { header: '2' }],
+      [
+        'bold',
+        'underline',
+        'blockquote',
+        // { color: ['red', 'green', 'blue', 'orange', 'violet', '#d0d1d2', 'black'] },
+      ],
+      [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
+      // ['image'],
+      // ['link', 'video'],
+      // ['clean'],
+    ],
+    handlers: {},
+  },
+  clipboard: {
+    // toggle to add extra line breaks when pasting HTML:
+    matchVisual: false,
+  },
+};
+
+const formats = [
+  'header',
+  'font',
+  'size',
+  'bold',
+  'italic',
+  'underline',
+  'strike',
+  'blockquote',
+  'color',
+  'list',
+  'bullet',
+  'indent',
+  'link',
+  'image',
+  // 'video',
+];
 class Editor extends React.Component {
   constructor(props) {
     super(props);
-    this.state = { theme: 'snow' };
-    this.modules = {
-      toolbar: {
-        container: [
-          [{ header: '1' }, { header: '2' }],
-          [
-            'bold',
-            'underline',
-            'blockquote',
-            // { color: ['red', 'green', 'blue', 'orange', 'violet', '#d0d1d2', 'black'] },
-          ],
-          [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
-          // ['image'],
-          // ['link', 'video'],
-          // ['clean'],
-        ],
-        handlers: {
-          image: () => {
-            const self = this;
-            this.container = this.quillRef.editingArea;
-            const quill = this.quillRef.getEditor();
-            let fileInput = this.container.querySelector('input.ql-image[type=file]'); // fileInput
-
-            if (fileInput == null) {
-              fileInput = document.createElement('input');
-              fileInput.setAttribute('type', 'file');
-              fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
-              fileInput.classList.add('ql-image');
-              fileInput.addEventListener('change', () => {
-                if (fileInput.files != null && fileInput.files[0] != null) {
-                  // getSelection 选择当前光标位置咯 然后在下一个range.index用它自带的embed媒介插入方式插入你已经存储在阿里上的图片了
-                  const range = quill.getSelection(true);
-                  const file = fileInput.files[0];
-                  const suffix = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
-                  const name = uuid();
-                  self.props
-                    .onUpload(file, self.props.path, self.progress, `${self.props.path}/${name}${suffix}`)
-                    .then(data => {
-                      self.setState({ uploading: false, load: self.state.load + 1 });
-                      quill.insertEmbed(range.index, 'image', data.url);
-                      quill.setSelection(range.index + 1);
-                    })
-                    .catch(err => {
-                      self.setState({ uploading: false, load: self.state.load + 1 });
-                      if (self.props.onError) self.props.onError(err);
-                    });
-                }
-              });
-            }
-            fileInput.click();
-          },
-        },
-      },
-      clipboard: {
-        // toggle to add extra line breaks when pasting HTML:
-        matchVisual: false,
-      },
-    };
+    this.quillRef = null;
+    this.state = {};
+    this.modules = Object.assign({}, modules, props.modules);
+    // console.log(this.modules);
+    this.modules.toolbar.handlers.image = () => this.image();
+    Object.keys(this.modules.toolbar.handlers).forEach(key => {
+      const handler = this.modules.toolbar.handlers[key];
+      this.modules.toolbar.handlers[key] = () => {
+        handler(this.quillRef.getEditor());
+      };
+    });
+    this.formats = Object.assign({}, formats, props.formats);
   }
 
   progress(data) {
@@ -73,6 +67,41 @@ class Editor extends React.Component {
     };
   }
 
+  image() {
+    const self = this;
+    this.container = this.quillRef.editingArea;
+    const quill = this.quillRef.getEditor();
+    let fileInput = this.container.querySelector('input.ql-image[type=file]'); // fileInput
+
+    if (fileInput == null) {
+      fileInput = document.createElement('input');
+      fileInput.setAttribute('type', 'file');
+      fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
+      fileInput.classList.add('ql-image');
+      fileInput.addEventListener('change', () => {
+        if (fileInput.files != null && fileInput.files[0] != null) {
+          // getSelection 选择当前光标位置咯 然后在下一个range.index用它自带的embed媒介插入方式插入你已经存储在阿里上的图片了
+          const range = quill.getSelection(true);
+          const file = fileInput.files[0];
+          const suffix = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+          const name = uuid();
+          self.props
+            .onUpload(file, self.props.path, self.progress, `${self.props.path}/${name}${suffix}`)
+            .then(data => {
+              self.setState({ uploading: false, load: self.state.load + 1 });
+              quill.insertEmbed(range.index, 'image', data.url || data);
+              quill.setSelection(range.index + 1);
+            })
+            .catch(err => {
+              self.setState({ uploading: false, load: self.state.load + 1 });
+              if (self.props.onError) self.props.onError(err);
+            });
+        }
+      });
+    }
+    fileInput.click();
+  }
+
   render() {
     return (
       <div>
@@ -82,11 +111,11 @@ class Editor extends React.Component {
           }}
           defaultValue={this.props.value}
           style={this.props.style}
-          theme={'snow'}
+          theme={this.props.theme || 'snow'}
           onChange={(content, delta, source, editor) => this.props.onChange && this.props.onChange(content, delta, source, editor)}
           // value={new Delta(this.props.value)}
           modules={this.modules}
-          formats={Editor.formats}
+          formats={this.formats}
           placeholder={this.props.placeholder}
         />
       </div>
@@ -94,22 +123,5 @@ class Editor extends React.Component {
   }
 }
 
-Editor.formats = [
-  'header',
-  'font',
-  'size',
-  'bold',
-  'italic',
-  'underline',
-  'strike',
-  'blockquote',
-  'color',
-  'list',
-  'bullet',
-  'indent',
-  'link',
-  // 'image',
-  // 'video',
-];
 
 export default Editor;

+ 4 - 6
front/src/containers/Admin.js

@@ -203,12 +203,10 @@ export default class extends Component {
                           );
                         })}
                       </SubMenu>
-                    ) : (
-                      <Menu.Item key={group.key} path={group.path}>
-                        <Icon type={group.icon} />
-                        <span>{group.name}</span>
-                      </Menu.Item>
-                    );
+                    ) : (<Menu.Item key={group.key} path={group.path}>
+                      <Icon type={group.icon} />
+                      <span>{group.name}</span>
+                    </Menu.Item>);
                     return view;
                   })}
                 </Menu>

+ 2 - 2
front/src/containers/App.js

@@ -64,9 +64,9 @@ export default class App extends Component {
           {routes.map(route => {
             return (
               <Route
-                exact={!(route.path.indexOf(':') >= 0)}
+                exact={!((route.matchPath || route.path).indexOf(':') >= 0)}
                 key={route.key}
-                path={route.path}
+                path={route.matchPath || route.path}
                 render={props => {
                   if (project.loginAuth && !project.loginAuth(route, store.getState())) {
                     return <Redirect to={project.loginPath || '/login'} />;

+ 1 - 0
front/src/layouts/FormLayout/index.js

@@ -85,6 +85,7 @@ class FormLayout extends Component {
         return item.render(item, this.props.data, this.props.form);
       case 'textarea':
         return <TextArea {...item} autosize className={item.class} placeholder={item.placeholder} disabled={!!item.disabled} />;
+      case 'password':
       case 'input':
         return (
           <Input

+ 0 - 22
front/src/services/Constant.js

@@ -11,25 +11,3 @@ export const STORE_LOADING = '@LOADING';
 export const STORE_LOADED = '@LOADED';
 
 export const FORM_LAYOUT = '@src/layouts/FormLayout';
-
-export const QuestionDifficult = [{ label: 'easy', value: 'easy' }, { label: 'medium', value: 'medium' }, { label: 'hard', value: 'hard' }];
-
-export const QuestionType = [{ label: 'SC/语法', value: 'sc' }, { label: 'RC/阅读', value: 'rc' }, { label: 'CR/逻辑', value: 'cr' }, { label: 'PS/数学', value: 'ps' }, { label: 'DS/数学', value: 'ds' }, { label: 'IR/综合推理', value: 'ir' }, { label: 'AWA/作文', value: 'awa' }];
-
-export const MoneyRange = [{ label: '0', value: 0 }, { label: '1-1000', value: 1 }, { label: '1000-5000', value: 2 }, { label: '5000-10000', value: 3 }, { label: '10000以上', value: 4 }];
-
-export const AskTarget = [{ label: '题目', value: 'question' }, { label: '官方', value: 'official' }, { label: '千行解析', value: 'qx' }, { label: '题源联想', value: 'association' }];
-
-export const PreviewStatus = [{ label: '全部', value: 0 }, { label: '未开始', value: 1 }, { label: '进行中', value: 2 }, { label: '已结束', value: 3 }];
-
-export const ServiceKey = [{ label: 'VIP', value: 'vip' }, { label: '机经', value: 'textbook' }, { label: '千行CAT', value: 'qx_cat' }];
-
-export const SwitchSelect = [{ value: 0, label: '否' }, { value: 1, label: '是' }];
-
-export const AskStatus = [{ value: 0, label: '新增' }, { value: 1, label: '已回答' }, { value: 2, label: '忽略' }];
-
-export const PrepareStatus = [{ label: '学生-Domestic', value: 'student_domestic' }, { label: '学生-Overseas', value: 'student_overseas' }, { label: '在职-Domestic', value: 'worker_domestic' }, { label: '在职-Overseas', value: 'worker_overseas' }, { label: 'Gap Year', value: 'gap_year' }];
-
-export const PrepareExaminationTime = [{ label: '近1个月', value: 'one_month' }, { label: '近2个月', value: 'two_month' }, { label: '近3个月', value: 'three_month' }, { label: '半年内', value: 'six_month' }];
-
-export const PayModule = [{ label: '服务', value: 'service' }, { label: '课程', value: 'class' }, { label: '资料', value: 'data' }];

+ 1 - 1
front/src/services/Tools.js

@@ -342,7 +342,7 @@ export function bindSearch(targetList, field, Component, listFunc, render, def,
         // for fetch callback order
         return;
       }
-      targetList[index].select = result.list.map(row => {
+      targetList[index].select = (result.list || result || []).map(row => {
         return render(row);
       });
       Component.setState({ fetching: false });

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/constants/enums/user/PrepareExaminationTime.java

@@ -12,7 +12,7 @@ public enum PrepareExaminationTime {
     }
 
     public static PrepareExaminationTime ValueOf(String name){
-        if (name == "") return null;
+        if (name == null || name.isEmpty()) return null;
         return PrepareExaminationTime.valueOf(name.toUpperCase());
     }
 }

+ 51 - 77
server/data/src/main/java/com/qxgmat/data/dao/entity/Question.java

@@ -37,16 +37,10 @@ public class Question implements Serializable {
     private String difficult;
 
     /**
-     * 样式:json
+     * 简介:自动从题干提取
      */
-    @Column(name = "`style`")
-    private JSONObject style;
-
-    /**
-     * 系统答案:json
-     */
-    @Column(name = "`answer`")
-    private JSONObject answer;
+    @Column(name = "`description`")
+    private String description;
 
     /**
      * 内容:json
@@ -67,6 +61,12 @@ public class Question implements Serializable {
     private Date associationTime;
 
     /**
+     * 题源联想:json
+     */
+    @Column(name = "`association_content`")
+    private Integer[] associationContent;
+
+    /**
      * 总作答时间
      */
     @Column(name = "`total_time`")
@@ -102,9 +102,6 @@ public class Question implements Serializable {
     @Column(name = "`official_content`")
     private String officialContent;
 
-    @Column(name = "`association_content`")
-    private String associationContent;
-
     private static final long serialVersionUID = 1L;
 
     /**
@@ -194,39 +191,21 @@ public class Question implements Serializable {
     }
 
     /**
-     * 获取样式:json
-     *
-     * @return style - 样式:json
-     */
-    public JSONObject getStyle() {
-        return style;
-    }
-
-    /**
-     * 设置样式:json
+     * 获取简介:自动从题干提取
      *
-     * @param style 样式:json
+     * @return description - 简介:自动从题干提取
      */
-    public void setStyle(JSONObject style) {
-        this.style = style;
+    public String getDescription() {
+        return description;
     }
 
     /**
-     * 获取系统答案:json
+     * 设置简介:自动从题干提取
      *
-     * @return answer - 系统答案:json
+     * @param description 简介:自动从题干提取
      */
-    public JSONObject getAnswer() {
-        return answer;
-    }
-
-    /**
-     * 设置系统答案:json
-     *
-     * @param answer 系统答案:json
-     */
-    public void setAnswer(JSONObject answer) {
-        this.answer = answer;
+    public void setDescription(String description) {
+        this.description = description;
     }
 
     /**
@@ -304,6 +283,24 @@ public class Question implements Serializable {
     }
 
     /**
+     * 获取题源联想:json
+     *
+     * @return association_content - 题源联想:json
+     */
+    public Integer[] getAssociationContent() {
+        return associationContent;
+    }
+
+    /**
+     * 设置题源联想:json
+     *
+     * @param associationContent 题源联想:json
+     */
+    public void setAssociationContent(Integer[] associationContent) {
+        this.associationContent = associationContent;
+    }
+
+    /**
      * 获取总作答时间
      *
      * @return total_time - 总作答时间
@@ -431,20 +428,6 @@ public class Question implements Serializable {
         this.officialContent = officialContent;
     }
 
-    /**
-     * @return association_content
-     */
-    public String getAssociationContent() {
-        return associationContent;
-    }
-
-    /**
-     * @param associationContent
-     */
-    public void setAssociationContent(String associationContent) {
-        this.associationContent = associationContent;
-    }
-
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -456,13 +439,13 @@ public class Question implements Serializable {
         sb.append(", type=").append(type);
         sb.append(", place=").append(place);
         sb.append(", difficult=").append(difficult);
-        sb.append(", style=").append(style);
-        sb.append(", answer=").append(answer);
+        sb.append(", description=").append(description);
         sb.append(", content=").append(content);
         sb.append(", questionTime=").append(questionTime);
         sb.append(", qxTime=").append(qxTime);
         sb.append(", officialTime=").append(officialTime);
         sb.append(", associationTime=").append(associationTime);
+        sb.append(", associationContent=").append(associationContent);
         sb.append(", totalTime=").append(totalTime);
         sb.append(", totalNumber=").append(totalNumber);
         sb.append(", totalCorrect=").append(totalCorrect);
@@ -471,7 +454,6 @@ public class Question implements Serializable {
         sb.append(", stem=").append(stem);
         sb.append(", qxContent=").append(qxContent);
         sb.append(", officialContent=").append(officialContent);
-        sb.append(", associationContent=").append(associationContent);
         sb.append("]");
         return sb.toString();
     }
@@ -536,22 +518,12 @@ public class Question implements Serializable {
         }
 
         /**
-         * 设置样式:json
+         * 设置简介:自动从题干提取
          *
-         * @param style 样式:json
+         * @param description 简介:自动从题干提取
          */
-        public Builder style(JSONObject style) {
-            obj.setStyle(style);
-            return this;
-        }
-
-        /**
-         * 设置系统答案:json
-         *
-         * @param answer 系统答案:json
-         */
-        public Builder answer(JSONObject answer) {
-            obj.setAnswer(answer);
+        public Builder description(String description) {
+            obj.setDescription(description);
             return this;
         }
 
@@ -598,6 +570,16 @@ public class Question implements Serializable {
         }
 
         /**
+         * 设置题源联想:json
+         *
+         * @param associationContent 题源联想:json
+         */
+        public Builder associationContent(Integer[] associationContent) {
+            obj.setAssociationContent(associationContent);
+            return this;
+        }
+
+        /**
          * 设置总作答时间
          *
          * @param totalTime 总作答时间
@@ -669,14 +651,6 @@ public class Question implements Serializable {
             return this;
         }
 
-        /**
-         * @param associationContent
-         */
-        public Builder associationContent(String associationContent) {
-            obj.setAssociationContent(associationContent);
-            return this;
-        }
-
         public Question build() {
             return this.obj;
         }

+ 6 - 7
server/data/src/main/java/com/qxgmat/data/dao/mapping/QuestionMapper.xml

@@ -10,13 +10,13 @@
     <result column="type" jdbcType="VARCHAR" property="type" />
     <result column="place" jdbcType="VARCHAR" property="place" />
     <result column="difficult" jdbcType="VARCHAR" property="difficult" />
-    <result column="style" jdbcType="VARCHAR" property="style" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
-    <result column="answer" jdbcType="VARCHAR" property="answer" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
+    <result column="description" jdbcType="VARCHAR" property="description" />
     <result column="content" jdbcType="VARCHAR" property="content" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
     <result column="question_time" jdbcType="TIMESTAMP" property="questionTime" />
     <result column="qx_time" jdbcType="TIMESTAMP" property="qxTime" />
     <result column="official_time" jdbcType="TIMESTAMP" property="officialTime" />
     <result column="association_time" jdbcType="TIMESTAMP" property="associationTime" />
+    <result column="association_content" jdbcType="VARCHAR" property="associationContent" typeHandler="com.nuliji.tools.mybatis.handler.IntegerArrayWithJsonHandler" />
     <result column="total_time" jdbcType="INTEGER" property="totalTime" />
     <result column="total_number" jdbcType="INTEGER" property="totalNumber" />
     <result column="total_correct" jdbcType="INTEGER" property="totalCorrect" />
@@ -30,20 +30,19 @@
     <result column="stem" jdbcType="LONGVARCHAR" property="stem" />
     <result column="qx_content" jdbcType="LONGVARCHAR" property="qxContent" />
     <result column="official_content" jdbcType="LONGVARCHAR" property="officialContent" />
-    <result column="association_content" jdbcType="LONGVARCHAR" property="associationContent" />
   </resultMap>
   <sql id="Base_Column_List">
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `keyword`, `type`, `place`, `difficult`, `style`, `answer`, `content`, `question_time`, 
-    `qx_time`, `official_time`, `association_time`, `total_time`, `total_number`, `total_correct`, 
-    `create_time`, `update_time`
+    `id`, `keyword`, `type`, `place`, `difficult`, `description`, `content`, `question_time`, 
+    `qx_time`, `official_time`, `association_time`, `association_content`, `total_time`, 
+    `total_number`, `total_correct`, `create_time`, `update_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--
       WARNING - @mbg.generated
     -->
-    `stem`, `qx_content`, `official_content`, `association_content`
+    `stem`, `qx_content`, `official_content`
   </sql>
 </mapper>

+ 25 - 0
server/data/src/main/java/com/qxgmat/data/inline/UserToken.java

@@ -0,0 +1,25 @@
+package com.qxgmat.data.inline;
+
+import java.util.Date;
+
+public class UserToken {
+    private Integer id;
+
+    private Date expire;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Date getExpire() {
+        return expire;
+    }
+
+    public void setExpire(Date expire) {
+        this.expire = expire;
+    }
+}

+ 1 - 1
server/data/src/main/resources/mybatis-generator.xml

@@ -128,8 +128,8 @@
             <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
             <columnOverride column="keyword" javaType="String[]" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.StringArrayHandler"/>
             <columnOverride column="answer" javaType="com.alibaba.fastjson.JSONObject" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler"/>
-            <columnOverride column="style" javaType="com.alibaba.fastjson.JSONObject" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler"/>
             <columnOverride column="content" javaType="com.alibaba.fastjson.JSONObject" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler"/>
+            <columnOverride column="association_content" javaType="Integer[]" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.IntegerArrayWithJsonHandler"/>
         </table>
         <table schema="qianxing" tableName="question_no" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" delimitAllColumns="true">
             <generatedKey column="id" sqlStatement="Mysql" identity="true"/>

+ 4 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/admin/PreviewController.java

@@ -70,10 +70,10 @@ public class PreviewController {
         HomeworkPreview entity = homeworkPreviewService.get(id);
         HomeworkPreviewDetailDto dto = Transform.convert(entity, HomeworkPreviewDetailDto.class);
 
-        List<QuestionNoRelation> questionNos = questionNoService.listByIds(entity.getQuestionNoIds());
-        List<QuestionNoExtendDto> questionNoExtendDtos = Transform.convert(questionNos, QuestionNoExtendDto.class);
-
-        dto.setQuestionNos(questionNoExtendDtos);
+//        List<QuestionNoRelation> questionNos = questionNoService.listByIds(entity.getQuestionNoIds());
+//        List<QuestionNoExtendDto> questionNoExtendDtos = Transform.convert(questionNos, QuestionNoExtendDto.class);
+//
+//        dto.setQuestionNos(questionNoExtendDtos);
 
         return ResponseHelp.success(dto);
     }

+ 62 - 30
server/gateway-api/src/main/java/com/qxgmat/controller/admin/QuestionController.java

@@ -1,14 +1,16 @@
 package com.qxgmat.controller.admin;
 
 
-import com.nuliji.tools.Response;
-import com.nuliji.tools.ResponseHelp;
-import com.nuliji.tools.Transform;
+import com.nuliji.tools.*;
+import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.dao.entity.Question;
+import com.qxgmat.data.dao.entity.QuestionNo;
 import com.qxgmat.data.relation.entity.QuestionNoRelation;
 import com.qxgmat.dto.admin.extend.QuestionNoExtendDto;
 import com.qxgmat.dto.admin.request.QuestionDto;
+import com.qxgmat.dto.admin.request.QuestionNoDto;
 import com.qxgmat.dto.admin.request.QuestionNoSearchDto;
+import com.qxgmat.dto.admin.response.QuestionDetailDto;
 import com.qxgmat.service.ExercisePaperService;
 import com.qxgmat.service.inline.ManagerLogService;
 import com.qxgmat.service.inline.QuestionNoService;
@@ -21,6 +23,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
 import java.util.List;
 
 @RestController("AdminQuestionController")
@@ -51,20 +54,50 @@ public class QuestionController {
         return ResponseHelp.success(entity);
     }
 
+
     @RequestMapping(value = "/edit", method = RequestMethod.PUT)
     @ApiOperation(value = "修改题目", httpMethod = "PUT")
     public Response<Boolean> edit(@RequestBody @Validated QuestionDto dto, HttpServletRequest request) {
-        Question entity = Transform.dtoToEntity(dto);
+        // 更新编号绑定
+        managerLogService.log(request);
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/detail", method = RequestMethod.GET)
+    @ApiOperation(value = "获取题目", httpMethod = "GET")
+    public Response<QuestionDetailDto> detail(int id, HttpServletRequest request) {
+        Question entity = questionService.get(id);
+        QuestionDetailDto dto = Transform.convert(entity, QuestionDetailDto.class);
+
+        List<QuestionNo> questionNoList = questionNoService.listByQuestion(entity.getId());
+        Integer[] ids = new Integer[questionNoList.size()];
+        for (int index = 0; index < questionNoList.size(); index++) {
+            ids[index] = questionNoList.get(index).getId();
+        }
+        dto.setQuestionNoIds(ids);
+
+//        List<QuestionNo> questionNoList = questionNoService.listByQuestion(entity.getId());
+//        List<QuestionNoExtendDto> questionNos = Transform.convert(questionNoList, QuestionNoExtendDto.class);
+//        dto.setQuestionNos(questionNos);
+
+        return ResponseHelp.success(dto);
+    }
+
+    @RequestMapping(value = "/add/no", method = RequestMethod.POST)
+    @ApiOperation(value = "添加编号", httpMethod = "POST")
+    public Response<QuestionNo> addNo(@RequestBody @Validated QuestionNoDto dto, HttpServletRequest request) {
+        QuestionNo entity = Transform.dtoToEntity(dto);
         // 添加
 //        entity.setModule(QuestionModule.EXERCISE.key);
-//        entity = exercisePaperService.editQuestion(entity);
+        entity = questionNoService.add(entity);
         managerLogService.log(request);
-        return ResponseHelp.success(true);
+        return ResponseHelp.success(entity);
     }
 
-    @RequestMapping(value = "/delete", method = RequestMethod.DELETE)
-    @ApiOperation(value = "删除题目", httpMethod = "DELETE")
+    @RequestMapping(value = "/delete/no", method = RequestMethod.DELETE)
+    @ApiOperation(value = "删除题目编号", httpMethod = "DELETE")
     public Response<Boolean> delete(int id, HttpServletRequest request) {
+
 //        Question entity = Transform.dtoToEntity(dto);
         // 添加
 //        entity.setModule(QuestionModule.EXERCISE.key);
@@ -74,37 +107,36 @@ public class QuestionController {
     }
 
     @RequestMapping(value = "/list/no", method = RequestMethod.POST)
-    @ApiOperation(value = "题目编号列表:通过编号", httpMethod = "POST")
+    @ApiOperation(value = "题目编号列表:通过id/编号", httpMethod = "POST")
     public Response<List<QuestionNoExtendDto>> listNo(@RequestBody @Validated QuestionNoSearchDto dto, HttpServletRequest request) {
-        List<QuestionNoRelation> questionNos = questionNoService.listByNos(dto.getNo(), dto.getModule());
+        List<QuestionNoRelation> questionNos;
+        if (dto.getIds() != null && dto.getIds().length > 0){
+           questionNos = questionNoService.listByIds(dto.getIds());
+        }else if(dto.getNos() != null && dto.getNos().length > 0){
+            questionNos = questionNoService.listByNos(dto.getNos(), dto.getModule());
+        }else{
+            throw new ParameterException("需要id或编号");
+        }
         List<QuestionNoExtendDto> questionNoExtendDtos = Transform.convert(questionNos, QuestionNoExtendDto.class);
 
         return ResponseHelp.success(questionNoExtendDtos);
     }
 
-    @RequestMapping(value = "/search/stem", method = RequestMethod.GET)
-    @ApiOperation(value = "搜索题目编号列表:题干搜索", httpMethod = "GET")
-    public Response<List<QuestionNoExtendDto>> searchStem(
-            @RequestParam(required = false, defaultValue = "1") int page,
-            @RequestParam(required = false, defaultValue = "100") int size,
-            @RequestParam(required = false, name="stem") String stem,
-            HttpServletRequest request) {
-        List<QuestionNoRelation> questionNos = questionNoService.searchStem(page, size, stem);
-        List<QuestionNoExtendDto> questionNoExtendDtos = Transform.convert(questionNos, QuestionNoExtendDto.class);
+    @RequestMapping(value = "/search/stem", method = RequestMethod.POST)
+    @ApiOperation(value = "搜索题目编号列表:题干搜索", httpMethod = "POST")
+    public Response<PageMessage<QuestionNoExtendDto>> searchStem(@RequestBody @Validated QuestionNoSearchDto dto, HttpServletRequest request) {
+        PageResult<QuestionNoRelation> p = questionNoService.searchStem(dto.getPage(), dto.getSize(), dto.getContent());
+        List<QuestionNoExtendDto> pr = Transform.convert(p, QuestionNoExtendDto.class);
 
-        return ResponseHelp.success(questionNoExtendDtos);
+        return ResponseHelp.success(pr, dto.getPage(), dto.getSize(), p.getTotal());
     }
 
-    @RequestMapping(value = "/search/no", method = RequestMethod.GET)
-    @ApiOperation(value = "搜索题目编号列表:题干搜索", httpMethod = "GET")
-    public Response<List<QuestionNoExtendDto>> searchNo(
-            @RequestParam(required = false, defaultValue = "1") int page,
-            @RequestParam(required = false, defaultValue = "100") int size,
-            @RequestParam(required = false, name="no") String no,
-            HttpServletRequest request) {
-        List<QuestionNoRelation> questionNos = questionNoService.searchNo(page, size, no);
-        List<QuestionNoExtendDto> questionNoExtendDtos = Transform.convert(questionNos, QuestionNoExtendDto.class);
+    @RequestMapping(value = "/search/no", method = RequestMethod.POST)
+    @ApiOperation(value = "搜索题目编号列表:题目编号搜索", httpMethod = "POST")
+    public Response<PageMessage<QuestionNoExtendDto>> searchNo(@RequestBody @Validated QuestionNoSearchDto dto, HttpServletRequest request) {
+        PageResult<QuestionNoRelation> p = questionNoService.searchNo(dto.getPage(), dto.getSize(), dto.getNo());
+        List<QuestionNoExtendDto> pr = Transform.convert(p, QuestionNoExtendDto.class);
 
-        return ResponseHelp.success(questionNoExtendDtos);
+        return ResponseHelp.success(pr, dto.getPage(), dto.getSize(), p.getTotal());
     }
 }

+ 8 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java

@@ -71,6 +71,14 @@ public class UserController {
 //        managerLogService.log(request);
 //        return ResponseHelp.success(userService.delete(id));
 //    }
+    @RequestMapping(value = "/token", method = RequestMethod.GET)
+    @ApiOperation(value = "获取用户token", httpMethod = "GET")
+    public Response<String> token(@RequestParam int id, HttpSession session) {
+        User entity = usersService.get(id);
+        String token = usersService.getTokenByUser(entity);
+
+        return ResponseHelp.success(token);
+    }
 
     @RequestMapping(value = "/detail", method = RequestMethod.GET)
     @ApiOperation(value = "获取用户", httpMethod = "GET")

+ 12 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java

@@ -52,11 +52,19 @@ public class AuthController {
 
     @RequestMapping(value = "/token", method = RequestMethod.POST)
     @ApiOperation(value = "验证token", httpMethod = "POST")
-    public Response<MyDto> token(HttpSession session, HttpServletRequest request) {
-        User user = shiroHelp.getLoginUser();
-        if (user == null) {
-            throw new AuthException("未登录");
+    public Response<MyDto> token(@RequestHeader("token") String token, HttpSession session, HttpServletRequest request) {
+        User user;
+        if (token == null || token.isEmpty()){
+            user = shiroHelp.getLoginUser();
+            if (user == null) {
+                throw new AuthException("未登录");
+            }
+        }else{
+            user = usersService.getUserByToken(token);
+            // 用该token登录
+            shiroHelp.getSession().login(shiroHelp.user(user.getMobile(), ""));
         }
+
         User entity = usersService.get(user.getId());
         return ResponseHelp.success(Transform.convert(entity, MyDto.class));
     }

+ 15 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java

@@ -16,7 +16,9 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpSession;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/api/base")
@@ -52,6 +54,19 @@ public class BaseController {
         return ResponseHelp.success(adList);
     }
 
+    @RequestMapping(value = "/tips", method = RequestMethod.GET)
+    @ApiOperation(value = "获取提示信息", notes = "获取提示信息", httpMethod = "GET")
+    public Response<Map<String, String>> tips(@RequestParam(required = true) String[] position)  {
+        Setting entity = settingService.getByKey(SettingKey.TIPS);
+        JSONObject value = entity.getValue();
+
+        Map<String, String> map = new HashMap<>();
+        for (String p: position) {
+            map.put(p, value.getString(p));
+        }
+        return ResponseHelp.success(map);
+    }
+
     @RequestMapping(value = "/score", method = RequestMethod.GET)
     @ApiOperation(value = "考分计算", notes = "获取考分排行信息", httpMethod = "GET")
     public Response<Rank> score(

+ 19 - 9
server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/QuestionExtendDto.java

@@ -8,12 +8,14 @@ public class QuestionExtendDto {
 
     private Integer id;
 
-    private String stem;
+    private String description;
 
     private String type;
     private String difficult;
     private String place;
 
+    private Integer[] associationContent;
+
     public Integer getId() {
         return id;
     }
@@ -22,14 +24,6 @@ public class QuestionExtendDto {
         this.id = id;
     }
 
-    public String getStem() {
-        return stem;
-    }
-
-    public void setStem(String stem) {
-        this.stem = stem;
-    }
-
     public String getType() {
         return type;
     }
@@ -53,4 +47,20 @@ public class QuestionExtendDto {
     public void setPlace(String place) {
         this.place = place;
     }
+
+    public Integer[] getAssociationContent() {
+        return associationContent;
+    }
+
+    public void setAssociationContent(Integer[] associationContent) {
+        this.associationContent = associationContent;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
 }

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/QuestionNoExtendDto.java

@@ -27,6 +27,8 @@ public class QuestionNoExtendDto {
     @NotEmpty(message = "对应模块层级!")
     private Integer[] moduleStruct;
 
+    private Integer questionId;
+
     private QuestionExtendDto question;
 
     public String getModule() {
@@ -68,4 +70,12 @@ public class QuestionNoExtendDto {
     public void setId(Integer id) {
         this.id = id;
     }
+
+    public Integer getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Integer questionId) {
+        this.questionId = questionId;
+    }
 }

+ 31 - 21
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionDto.java

@@ -14,6 +14,8 @@ public class QuestionDto {
 
     private String[] keyword;
 
+    private String stem;
+
     @NotEmpty(message = "题型不能为空!")
     private String type;
 
@@ -23,15 +25,15 @@ public class QuestionDto {
     @NotEmpty(message = "难度不能为空!")
     private String difficult;
 
-    private QuestionNoExtendDto questionIds;
+    private Integer[] questionNoIds;
 
     private String qxContent;
 
     private String officialContent;
 
-    private String associationContent;
+    private Integer[] associationContent;
 
-    private JSONObject answer;
+    private JSONObject content;
 
     private JSONObject style;
 
@@ -43,14 +45,6 @@ public class QuestionDto {
         this.id = id;
     }
 
-    public QuestionNoExtendDto getQuestionIds() {
-        return questionIds;
-    }
-
-    public void setQuestionIds(QuestionNoExtendDto questionIds) {
-        this.questionIds = questionIds;
-    }
-
     public String getType() {
         return type;
     }
@@ -99,27 +93,43 @@ public class QuestionDto {
         this.officialContent = officialContent;
     }
 
-    public String getAssociationContent() {
+    public JSONObject getStyle() {
+        return style;
+    }
+
+    public void setStyle(JSONObject style) {
+        this.style = style;
+    }
+
+    public Integer[] getQuestionNoIds() {
+        return questionNoIds;
+    }
+
+    public void setQuestionNoIds(Integer[] questionNoIds) {
+        this.questionNoIds = questionNoIds;
+    }
+
+    public Integer[] getAssociationContent() {
         return associationContent;
     }
 
-    public void setAssociationContent(String associationContent) {
+    public void setAssociationContent(Integer[] associationContent) {
         this.associationContent = associationContent;
     }
 
-    public JSONObject getAnswer() {
-        return answer;
+    public JSONObject getContent() {
+        return content;
     }
 
-    public void setAnswer(JSONObject answer) {
-        this.answer = answer;
+    public void setContent(JSONObject content) {
+        this.content = content;
     }
 
-    public JSONObject getStyle() {
-        return style;
+    public String getStem() {
+        return stem;
     }
 
-    public void setStyle(JSONObject style) {
-        this.style = style;
+    public void setStem(String stem) {
+        this.stem = stem;
     }
 }

+ 69 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionNoDto.java

@@ -0,0 +1,69 @@
+package com.qxgmat.dto.admin.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.QuestionNo;
+
+@Dto(entity = QuestionNo.class)
+public class QuestionNoDto {
+    private Integer id;
+
+    /**
+     * 题目id
+     */
+    private Integer questionId;
+
+    /**
+     * 模块:examination, exercise,sentence
+     */
+    private String module;
+
+    /**
+     * 人工id
+     */
+    private String no;
+
+    /**
+     * 对应模块结构信息,逗号分隔
+     */
+    private Integer[] moduleStruct;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Integer questionId) {
+        this.questionId = questionId;
+    }
+
+    public String getModule() {
+        return module;
+    }
+
+    public void setModule(String module) {
+        this.module = module;
+    }
+
+    public String getNo() {
+        return no;
+    }
+
+    public void setNo(String no) {
+        this.no = no;
+    }
+
+    public Integer[] getModuleStruct() {
+        return moduleStruct;
+    }
+
+    public void setModuleStruct(Integer[] moduleStruct) {
+        this.moduleStruct = moduleStruct;
+    }
+}

+ 57 - 8
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionNoSearchDto.java

@@ -8,18 +8,19 @@ import org.json.JSONObject;
 import javax.validation.constraints.NotEmpty;
 
 public class QuestionNoSearchDto {
+    private int page = 1;
 
-    private String[] no;
+    private int size = 20;
 
-    private String module;
+    private String no;
 
-    public String[] getNo() {
-        return no;
-    }
+    private String[] nos;
 
-    public void setNo(String[] no) {
-        this.no = no;
-    }
+    private Integer[] ids;
+
+    private String module;
+
+    private String content;
 
     public String getModule() {
         return module;
@@ -28,4 +29,52 @@ public class QuestionNoSearchDto {
     public void setModule(String module) {
         this.module = module;
     }
+
+    public String[] getNos() {
+        return nos;
+    }
+
+    public void setNos(String[] nos) {
+        this.nos = nos;
+    }
+
+    public Integer[] getIds() {
+        return ids;
+    }
+
+    public void setIds(Integer[] ids) {
+        this.ids = ids;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public int getPage() {
+        return page;
+    }
+
+    public void setPage(int page) {
+        this.page = page;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public void setSize(int size) {
+        this.size = size;
+    }
+
+    public String getNo() {
+        return no;
+    }
+
+    public void setNo(String no) {
+        this.no = no;
+    }
 }

+ 22 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/ExerciseQuestionListDto.java

@@ -1,21 +1,26 @@
 package com.qxgmat.dto.admin.response;
 
 import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.ExercisePaperQuestion;
 import com.qxgmat.data.dao.entity.QuestionNo;
 import com.qxgmat.dto.admin.extend.ExercisePaperExtendDto;
 import com.qxgmat.dto.admin.extend.QuestionExtendDto;
 import com.qxgmat.dto.admin.extend.QuestionNoExtendDto;
 
 
-@Dto(entity = QuestionNo.class)
+@Dto(entity = ExercisePaperQuestion.class)
 public class ExerciseQuestionListDto {
 
     private Integer id;
 
     private ExercisePaperExtendDto paper;
 
+    private Integer questionId;
+
     private QuestionExtendDto question;
 
+    private Integer questionNoId;
+
     private QuestionNoExtendDto questionNo;
 
     public Integer getId() {
@@ -49,4 +54,20 @@ public class ExerciseQuestionListDto {
     public void setQuestionNo(QuestionNoExtendDto questionNo) {
         this.questionNo = questionNo;
     }
+
+    public Integer getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Integer questionId) {
+        this.questionId = questionId;
+    }
+
+    public Integer getQuestionNoId() {
+        return questionNoId;
+    }
+
+    public void setQuestionNoId(Integer questionNoId) {
+        this.questionNoId = questionNoId;
+    }
 }

+ 8 - 8
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/HomeworkPreviewDetailDto.java

@@ -26,7 +26,7 @@ public class HomeworkPreviewDetailDto {
 
     private Integer finish;
 
-    private Collection<QuestionNoExtendDto> questionNos;
+//    private Collection<QuestionNoExtendDto> questionNos;
 
     public Integer getId() {
         return id;
@@ -92,11 +92,11 @@ public class HomeworkPreviewDetailDto {
         this.questionNoIds = questionNoIds;
     }
 
-    public Collection<QuestionNoExtendDto> getQuestionNos() {
-        return questionNos;
-    }
-
-    public void setQuestionNos(Collection<QuestionNoExtendDto> questionNos) {
-        this.questionNos = questionNos;
-    }
+//    public Collection<QuestionNoExtendDto> getQuestionNos() {
+//        return questionNos;
+//    }
+//
+//    public void setQuestionNos(Collection<QuestionNoExtendDto> questionNos) {
+//        this.questionNos = questionNos;
+//    }
 }

+ 130 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/QuestionDetailDto.java

@@ -0,0 +1,130 @@
+package com.qxgmat.dto.admin.response;
+
+import com.alibaba.fastjson.JSONObject;
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.Question;
+import com.qxgmat.dto.admin.extend.QuestionNoExtendDto;
+
+import java.util.List;
+
+@Dto(entity = Question.class)
+public class QuestionDetailDto {
+    private Integer id;
+
+    private String stem;
+
+    private String[] keyword;
+
+    private String type;
+
+    private String place;
+
+    private String difficult;
+
+//    private List<QuestionNoExtendDto> questionNos;
+    private Integer[] questionNoIds;
+
+    private JSONObject content;
+
+    private String officialContent;
+
+    private String qxContent;
+
+    private Integer[] associationContent;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getStem() {
+        return stem;
+    }
+
+    public void setStem(String stem) {
+        this.stem = stem;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getPlace() {
+        return place;
+    }
+
+    public void setPlace(String place) {
+        this.place = place;
+    }
+
+    public String getDifficult() {
+        return difficult;
+    }
+
+    public void setDifficult(String difficult) {
+        this.difficult = difficult;
+    }
+
+//    public List<QuestionNoExtendDto> getQuestionNos() {
+//        return questionNos;
+//    }
+//
+//    public void setQuestionNos(List<QuestionNoExtendDto> questionNos) {
+//        this.questionNos = questionNos;
+//    }
+
+    public JSONObject getContent() {
+        return content;
+    }
+
+    public void setContent(JSONObject content) {
+        this.content = content;
+    }
+
+    public String[] getKeyword() {
+        return keyword;
+    }
+
+    public void setKeyword(String[] keyword) {
+        this.keyword = keyword;
+    }
+
+    public String getOfficialContent() {
+        return officialContent;
+    }
+
+    public void setOfficialContent(String officialContent) {
+        this.officialContent = officialContent;
+    }
+
+    public String getQxContent() {
+        return qxContent;
+    }
+
+    public void setQxContent(String qxContent) {
+        this.qxContent = qxContent;
+    }
+
+    public Integer[] getAssociationContent() {
+        return associationContent;
+    }
+
+    public void setAssociationContent(Integer[] associationContent) {
+        this.associationContent = associationContent;
+    }
+
+    public Integer[] getQuestionNoIds() {
+        return questionNoIds;
+    }
+
+    public void setQuestionNoIds(Integer[] questionNoIds) {
+        this.questionNoIds = questionNoIds;
+    }
+}

+ 38 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UsersService.java

@@ -2,21 +2,26 @@ package com.qxgmat.service;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.CipherHelp;
 import com.nuliji.tools.Tools;
 import com.nuliji.tools.Transform;
+import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.nuliji.tools.mybatis.Example;
+import com.nuliji.tools.mybatis.handler.NativeJsonHandler;
 import com.nuliji.tools.third.OauthData;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.UserMapper;
 import com.qxgmat.data.dao.entity.User;
 import com.qxgmat.data.dao.entity.UserMessage;
+import com.qxgmat.data.inline.UserToken;
 import com.qxgmat.help.WechatHelp;
 import com.qxgmat.service.inline.UserClassService;
 import com.qxgmat.service.inline.UserMessageService;
 import com.qxgmat.service.inline.UserPayService;
 import com.qxgmat.service.inline.UserServiceService;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -36,6 +41,10 @@ public class UsersService extends AbstractService {
     final public String PLATORM_WECHAT_PC = "wechat_openid_pc";
     final public String PLATORM_WECHAT = "wechat_unionid";
 
+    @Value("${self.secret}")
+    private String secret;
+    private NativeJsonHandler<UserToken> tokenHandler = new NativeJsonHandler<UserToken>(UserToken.class);
+
     @Resource
     private UserMapper userMapper;
 
@@ -55,6 +64,35 @@ public class UsersService extends AbstractService {
     private UserServiceService userServiceService;
 
     /**
+     * 生成有效期token
+     * @param user
+     * @return
+     */
+    public String getTokenByUser(User user){
+        UserToken ut = new UserToken();
+        ut.setId(user.getId());
+        Date expire = new Date(new Date().getTime() + 86400);
+        ut.setExpire(expire);
+        String info = tokenHandler.toJson(ut);
+        return CipherHelp.encrypt(info, CipherHelp.DES, secret);
+    }
+
+    /**
+     * 解析有效期token
+     * @param token
+     * @return
+     */
+    public User getUserByToken(String token){
+        String info = CipherHelp.decrypt(token, CipherHelp.DES, secret);
+        UserToken ut = tokenHandler.toObject(info);
+        Date expire = ut.getExpire();
+        if (expire.before(new Date())){
+            throw new AuthException("token错误");
+        }
+        return get(ut.getId());
+    }
+
+    /**
      * 绑定第三方账号信息
      * @param user 当前登录用户
      * @param code

+ 18 - 7
server/gateway-api/src/main/java/com/qxgmat/service/inline/QuestionNoService.java

@@ -2,6 +2,7 @@ package com.qxgmat.service.inline;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.PageResult;
 import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
@@ -42,15 +43,16 @@ public class QuestionNoService extends AbstractService {
      * @param stem
      * @return
      */
-    public List<QuestionNoRelation> searchStem(int page, int size, String stem){
+    public PageResult<QuestionNoRelation> searchStem(int page, int size, String stem){
+        // todo 连表查询
         Example example = new Example(QuestionNo.class);
         example.and(
                 example.createCriteria()
-                        .andEqualTo("stem", stem)
+                        .andEqualTo("no", stem)
 
         );
-        List<QuestionNo> p = page(()->select(questionNoMapper, example), page, size);
-        return relation(p);
+        Page<QuestionNo> p = page(()->select(questionNoMapper, example), page, size);
+        return new PageResult<>(relation(p), p.getTotal());
     }
 
     /**
@@ -60,15 +62,15 @@ public class QuestionNoService extends AbstractService {
      * @param no
      * @return
      */
-    public List<QuestionNoRelation> searchNo(int page, int size, String no){
+    public PageResult<QuestionNoRelation> searchNo(int page, int size, String no){
         Example example = new Example(QuestionNo.class);
         example.and(
                 example.createCriteria()
                         .andLike("no", no)
         );
         example.orderBy("id").asc();
-        List<QuestionNo> p = page(()->select(questionNoMapper, example), page, size);
-        return relation(p);
+        Page<QuestionNo> p = page(()->select(questionNoMapper, example), page, size);
+        return new PageResult<>(relation(p), p.getTotal());
     }
 
     /**
@@ -92,6 +94,15 @@ public class QuestionNoService extends AbstractService {
         return relation(p);
     }
 
+    public List<QuestionNo> listByQuestion(Number questionId){
+        Example example = new Example(QuestionNo.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("questionId", questionId)
+        );
+        return select(questionNoMapper, example);
+    }
+
     public List<QuestionNoRelation> listByIds(Number[] ids){
         List<QuestionNo> p = select(questionNoMapper, ids);
         return relation(p);

+ 3 - 0
server/gateway-api/src/main/resources/application.yml

@@ -50,3 +50,6 @@ third:
     native:
       appId: 123123
       appSecret: 123123
+
+self:
+  secret: qianxing-duoshaojiaoyu

+ 111 - 0
server/tools/src/main/java/com/nuliji/tools/CipherHelp.java

@@ -0,0 +1,111 @@
+package com.nuliji.tools;
+
+
+import java.io.IOException;
+import java.security.Key;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+
+public class CipherHelp {
+
+    public final static String AES = "AES";
+    public final static String DES = "DES";
+    public final static String DES3 = "3DESC";
+
+    /**
+     * encrypt base on DES or 3DES or AES.
+     *
+     * @param target
+     *            The target (which is used to encrypt)
+     * @param algorithm
+     *            The algorithm can be DES or DESede or AES
+     * @param key
+     *            The key (which is key to handle this encrypt)
+     * @return
+     */
+    public static String encrypt(String target, String algorithm, String key) {
+        try {
+
+            byte[] targetToByte = target.getBytes("UTF-8");
+            Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(Cipher.ENCRYPT_MODE, getKey(key, algorithm));
+            byte[] result = cipher.doFinal(targetToByte);
+            // System.out.println("base64:" +encryptByBase64(result));
+            return Tools.encodeBase64(result); // 需要BASE64包装一下,否则会抛出异常
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+
+    }
+
+    /**
+     * decrypt base on DES or 3DES or AES.
+     *
+     * @param target
+     *            The target (which is used to decrypt)
+     * @param algorithm
+     *            The algorithm can be DES or DESede or AES
+     * @param key
+     *            The key (which is key to handle this decrypt)
+     * @return
+     */
+    public static String decrypt(String target, String algorithm, String key) {
+
+        try {
+
+            byte[] targetToByte = Tools.decodeBase64(target); // 需要BASE64包装一下,否则会抛出异常
+            Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(Cipher.DECRYPT_MODE, getKey(key, algorithm));
+            byte[] result = cipher.doFinal(targetToByte);
+            // System.out.println("base:" + new String(result,"UTF-8"));
+            return new String(result, "UTF-8");
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static Key getKey(String key, String algorithm) {
+        try {
+            KeyGenerator generator = KeyGenerator.getInstance(algorithm);
+            generator.init(new SecureRandom(key.getBytes()));
+            return generator.generateKey();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+
+    }
+
+    public static void main(String[] args) {
+        String target = "测试";
+        // String target = "test";
+        // DES
+        String encrypt = encrypt(target, "DES", "im a key");
+        System.out.println("加密后:" + encrypt);
+        String decrypt = decrypt(encrypt, "DES", "im a key");
+        System.out.println("解密后:" + decrypt);
+        // 3DES
+        /*
+         * String encrypt = encrypt(target, "DESede", "im a key");
+         * System.out.println("加密后:" + encrypt); String decrypt =
+         * decrypt(encrypt, "DESede", "im a key"); System.out.println("解密后:" +
+         * decrypt);
+         */
+        // AES
+        /*
+         * String encrypt = encrypt(target, "AES", "im a key");
+         * System.out.println("加密后:" + encrypt); String decrypt =
+         * decrypt(encrypt, "AES", "im a key"); System.out.println("解密后:" +
+         * decrypt);
+         */
+    }
+
+}
+

+ 11 - 3
server/tools/src/main/java/com/nuliji/tools/Tools.java

@@ -49,7 +49,7 @@ public class Tools {
         return sb.toString();
     }
 
-   /**
+    /**
      * 取得指定月份的最大天数
      *
      * @param date 月份,格式yyyy-MM
@@ -136,6 +136,14 @@ public class Tools {
     }
 
     /**
+     * @param str
+     * @return
+     */
+    public static byte[] decodeBase64(final String str) {
+        return Base64.decodeBase64(str.getBytes());
+    }
+
+    /**
      * 二进制数据编码为BASE64字符串
      *
      * @param bytes
@@ -197,7 +205,7 @@ public class Tools {
 
     public static String joinIds(Collection ids) {
         List<String> list = new ArrayList<>();
-//        logger.debug("join:{}", ids);
+    //        logger.debug("join:{}", ids);
         if(ids == null || ids.size() == 0) return "";
         for (Object id : ids) {
             list.add(id.toString());
@@ -207,7 +215,7 @@ public class Tools {
 
     public static Collection<Long> splitIds(String ids){
         List<Long> l = new ArrayList<>();
-//        logger.debug("split:{}", ids);
+    //        logger.debug("split:{}", ids);
         if(ids == null || ids.length() == 0) return l;
         String[] list = ids.split(",");
         for(String s:list){

+ 5 - 5
server/tools/src/main/java/com/nuliji/tools/mybatis/handler/NativeJsonHandler.java

@@ -34,20 +34,20 @@ public class NativeJsonHandler<T> extends BaseTypeHandler<T> {
 
     @Override
     public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
-        return this.toObject(rs.getString(columnName), clazz);
+        return this.toObject(rs.getString(columnName));
     }
 
     @Override
     public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
-        return this.toObject(rs.getString(columnIndex), clazz);
+        return this.toObject(rs.getString(columnIndex));
     }
 
     @Override
     public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
-        return this.toObject(cs.getString(columnIndex), clazz);
+        return this.toObject(cs.getString(columnIndex));
     }
 
-    private String toJson(T object) {
+    public String toJson(T object) {
         try {
             return mapper.writeValueAsString(object);
         } catch (Exception e) {
@@ -55,7 +55,7 @@ public class NativeJsonHandler<T> extends BaseTypeHandler<T> {
         }
     }
 
-    private T toObject(String content, Class<?> clazz) {
+    public T toObject(String content) {
         if (content != null && !content.isEmpty()) {
             try {
                 return (T) mapper.readValue(content, clazz);