瀏覽代碼

feat(front): 弹框逻辑调整

Go 5 年之前
父節點
當前提交
6217558361
共有 30 個文件被更改,包括 1072 次插入342 次删除
  1. 32 1
      front/project/admin/routes/course/vsDetail/page.js
  2. 44 38
      front/project/www/components/Examination/index.js
  3. 10 9
      front/project/www/components/Login/index.js
  4. 277 42
      front/project/www/components/OtherModal/index.js
  5. 11 5
      front/project/www/components/PayModal/index.js
  6. 14 8
      front/project/www/components/VipRenew/index.js
  7. 7 6
      front/project/www/routes/examination/list/page.js
  8. 8 4
      front/project/www/routes/examination/main/page.js
  9. 17 47
      front/project/www/routes/my/course/page.js
  10. 50 159
      front/project/www/routes/my/tools/page.js
  11. 2 2
      front/project/www/routes/page/cart/page.js
  12. 2 2
      front/project/www/routes/room/main/page.js
  13. 10 2
      front/project/www/routes/textbook/list/page.js
  14. 4 0
      front/project/www/stores/main.js
  15. 7 2
      front/project/www/stores/my.js
  16. 6 0
      front/project/www/stores/user.js
  17. 1 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageMethod.java
  18. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserAskCourse.java
  19. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAskCourseMapper.xml
  20. 8 6
      server/data/src/main/resources/db/migration/V1__init_table.sql
  21. 40 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java
  22. 7 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java
  23. 11 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  24. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskCourseDto.java
  25. 39 7
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  26. 325 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExportService.java
  27. 7 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java
  28. 13 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  29. 18 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java
  30. 55 0
      server/gateway-api/src/main/java/com/qxgmat/task/ScheduledTask.java

+ 32 - 1
front/project/admin/routes/course/vsDetail/page.js

@@ -96,7 +96,8 @@ export default class extends Page {
   }
 
   renderVs() {
-    const { getFieldDecorator } = this.props.form;
+    const { getFieldDecorator, getFieldValue, setFieldsValue } = this.props.form;
+    const cover = getFieldValue('cover');
     return <Block>
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
@@ -155,6 +156,36 @@ export default class extends Page {
             <InputNumber placeholder='天' />,
           )}
         </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='简短描述'>
+          {getFieldDecorator('comment', {
+            rules: [
+              { required: true, message: '请输入描述' },
+            ],
+          })(
+            <Input placeholder='请输入简短描述' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程封面'>
+          {getFieldDecorator('cover', {
+            rules: [
+              { required: true, message: '上传图片' },
+            ],
+          })(
+            <Upload
+              listType="picture-card"
+              showUploadList={false}
+              beforeUpload={(file) => System.uploadImage(file).then((result) => {
+                setFieldsValue({ cover: result.url });
+                return Promise.reject();
+              })}
+            >
+              {cover ? <img src={cover} alt="avatar" /> : <div>
+                <Icon type={this.state.loading ? 'loading' : 'plus'} />
+                <div className="ant-upload-text">Upload</div>
+              </div>}
+            </Upload>,
+          )}
+        </Form.Item>
       </Form>
     </Block>;
   }

+ 44 - 38
front/project/www/components/Examination/index.js

@@ -49,42 +49,48 @@ export default class extends Component {
       },
     };
     this.state = { step: 0, data: { prepareGoal: 650 } };
-    My.getPrepare()
-      .then(result => {
-        const statusTotal = result.stat.status.reduce((p, n) => { return p + n.value; }, 0);
-        const goalTotal = result.stat.goal.reduce((p, n) => { return p + n.value; }, 0);
-        const examinationTimeTotal = result.stat.examinationTime.reduce((p, n) => { return p + n.value; }, 0);
-        const scoreTimeTotal = result.stat.scoreTime.reduce((p, n) => { return p + n.value; }, 0);
-        const stat = {
-          status: result.stat.status.map((row, index) => {
-            row.value = formatPercent(row.value, statusTotal);
-            row.label = `${PrepareStatusMap[row.key]}; ${row.value}%`;
-            row.color = this.statusColors[index];
-            return row;
-          }),
-          goal: result.stat.goal.map((row, index) => {
-            row.value = formatPercent(row.value, goalTotal);
-            row.label = `${row.key}+; ${row.value}%`;
-            row.color = this.goalColors[index];
-            return row;
-          }),
-          examinationTime: result.stat.status.map((row, index) => {
-            row.value = formatPercent(row.value, examinationTimeTotal);
-            row.label = `${PrepareExaminationTimeMap[row.key]}; ${row.value}%`;
-            row.color = this.examinationTimeColors[index];
-            return row;
-          }),
-          scoreTime: result.stat.scoreTime.map((row, index) => {
-            row.value = formatPercent(row.value, scoreTimeTotal);
-            row.label = `${PrepareScoreTimeMap[row.key]}; ${row.value}%`;
-            row.color = this.scoreTimeColors[index];
-            return row;
-          }),
-        };
-        result.prepareGoal = result.prepareGoal || 650;
-        result.prepareScoreTime = result.prepareScoreTime ? moment(result.prepareScoreTime) : null;
-        this.setState({ data: result, stat, first: !result.prepareStatus, step: !result.prepareStatus ? 0 : 4, info: result.info });
-      });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.show && !this.init) {
+      this.init = true;
+      My.getPrepare()
+        .then(result => {
+          const statusTotal = result.stat.status.reduce((p, n) => { return p + n.value; }, 0);
+          const goalTotal = result.stat.goal.reduce((p, n) => { return p + n.value; }, 0);
+          const examinationTimeTotal = result.stat.examinationTime.reduce((p, n) => { return p + n.value; }, 0);
+          const scoreTimeTotal = result.stat.scoreTime.reduce((p, n) => { return p + n.value; }, 0);
+          const stat = {
+            status: result.stat.status.map((row, index) => {
+              row.value = formatPercent(row.value, statusTotal);
+              row.label = `${PrepareStatusMap[row.key]}; ${row.value}%`;
+              row.color = this.statusColors[index];
+              return row;
+            }),
+            goal: result.stat.goal.map((row, index) => {
+              row.value = formatPercent(row.value, goalTotal);
+              row.label = `${row.key}+; ${row.value}%`;
+              row.color = this.goalColors[index];
+              return row;
+            }),
+            examinationTime: result.stat.status.map((row, index) => {
+              row.value = formatPercent(row.value, examinationTimeTotal);
+              row.label = `${PrepareExaminationTimeMap[row.key]}; ${row.value}%`;
+              row.color = this.examinationTimeColors[index];
+              return row;
+            }),
+            scoreTime: result.stat.scoreTime.map((row, index) => {
+              row.value = formatPercent(row.value, scoreTimeTotal);
+              row.label = `${PrepareScoreTimeMap[row.key]}; ${row.value}%`;
+              row.color = this.scoreTimeColors[index];
+              return row;
+            }),
+          };
+          result.prepareGoal = result.prepareGoal || 650;
+          result.prepareScoreTime = result.prepareScoreTime ? moment(result.prepareScoreTime) : null;
+          this.setState({ data: result, stat, first: !result.prepareStatus, step: !result.prepareStatus ? 0 : 4, info: result.info });
+        });
+    }
   }
 
   onChange(type, key) {
@@ -118,7 +124,7 @@ export default class extends Component {
   }
 
   render() {
-    const { step } = this.state;
+    const { step, info } = this.state;
     const { show } = this.props;
     return (
       <Modal
@@ -128,7 +134,7 @@ export default class extends Component {
         {...this.stepProp[step]}
         onClose={() => this.onClose()}
       >
-        <div className="examination-modal-wrapper">{this[`renderStep${step}`]()}</div>
+        {info && <div className="examination-modal-wrapper">{this[`renderStep${step}`]()}</div>}
       </Modal>
     );
   }

+ 10 - 9
front/project/www/components/Login/index.js

@@ -36,15 +36,16 @@ export default class Login extends Component {
       },
       false,
     );
-    Main.getContract('register')
-      .then(result => {
-        this.setState({ registerContract: result });
-      });
-
-    Main.getContract('privacy')
-      .then(result => {
-        this.setState({ privacyContract: result });
-      });
+    if (!props.user.login) {
+      Main.getContract('register')
+        .then(result => {
+          this.setState({ registerContract: result });
+        });
+      Main.getContract('privacy')
+        .then(result => {
+          this.setState({ privacyContract: result });
+        });
+    }
   }
 
   close() {

+ 277 - 42
front/project/www/components/OtherModal/index.js

@@ -2,19 +2,22 @@ import React, { Component } from 'react';
 import Cropper from 'react-cropper';
 import 'cropperjs/dist/cropper.css';
 import './index.less';
-import { Checkbox } from 'antd';
+import { Checkbox, Icon } from 'antd';
 import FileUpload from '@src/components/FileUpload';
 import Assets from '@src/components/Assets';
 import scale from '@src/services/Scale';
 import { asyncSMessage } from '@src/services/AsyncTools';
 import { SelectInput, VerificationInput, Input } from '../Login';
-import { MobileArea } from '../../../Constant';
+import { MobileArea, TextbookFeedbackTarget } from '../../../Constant';
 import Invite from '../Invite';
 import Modal from '../Modal';
 import { Common } from '../../stores/common';
 import { User } from '../../stores/user';
 import { My } from '../../stores/my';
 import Select from '../Select';
+import { formatDate, getMap } from '../../../../src/services/Tools';
+
+const TextbookFeedbackTargetMap = getMap(TextbookFeedbackTarget, 'value', 'label');
 
 export class BindPhone extends Component {
   constructor(props) {
@@ -425,7 +428,7 @@ export class EditAvatar extends Component {
       };
     } else {
       const img = new Image();
-      img.onload = function() {
+      img.onload = function () {
         const canvas = document.createElement('canvas');
         canvas.height = img.height;
         canvas.width = img.width;
@@ -543,8 +546,31 @@ export class InviteModal extends Component {
 
 // 模考选择下载
 export class DownloadModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { checkMap: {} };
+  }
+
+  onConfirm() {
+    const { onConfirm, data } = this.props;
+    if (onConfirm) onConfirm();
+    const { checkMap } = this.state;
+    Object.keys(checkMap).forEach(key => {
+      if (!checkMap[key]) return;
+      const link = data[`${key}`];
+      if (link) {
+        openLink(link);
+      }
+    });
+    this.setState({ checkList: [] });
+  }
+
   render() {
-    const { show, onConfirm, onCancel } = this.props;
+    const { show, data = {}, onCancel } = this.props;
+    const { checkMap = {} } = this.state;
+    const quantVersion = data.quantVersion || 0;
+    const irVersion = data.irVersion || 0;
+    const rcVersion = data.rcVersion || 0;
     return (
       <Modal
         className="download-modal"
@@ -552,27 +578,36 @@ export class DownloadModal extends Component {
         width={570}
         title="下载"
         confirmText="下载"
-        onConfirm={onConfirm}
+        onConfirm={() => this.onConfirm()}
         onCancel={onCancel}
       >
         <div className="download-modal-wrapper">
           <div className="t-2 t-s-18 m-b-1">请选择下载科目</div>
           <div className="m-b-1">
-            <div className="t-2 t-s-16">
-              <Checkbox />
-              <span className="m-l-5">数学</span>
-              <span className="t-8">(版本7 最后更新:2019-07-16 11:41:13)</span>
-            </div>
-            <div className="t-2 t-s-16">
-              <Checkbox />
-              <span className="m-l-5">数学</span>
-              <span className="t-8">(版本7 最后更新:2019-07-16 11:41:13)</span>
-            </div>
-            <div className="t-2 t-s-16">
-              <Checkbox />
+            {quantVersion > 0 && <div className="t-2 t-s-16">
+              <Checkbox checked={checkMap.quant} onChange={() => {
+                checkMap.quant = !checkMap.quant;
+                this.setState({ checkMap });
+              }} />
               <span className="m-l-5">数学</span>
-              <span className="t-8">(版本7 最后更新:2019-07-16 11:41:13)</span>
-            </div>
+              <span className="t-8">(版本{quantVersion} 最后更新:{formatDate(data.quantTime, 'YYYY-MM-DD HH:mm:ss')})</span>
+            </div>}
+            {irVersion > 0 && <div className="t-2 t-s-16">
+              <Checkbox checked={checkMap.ir} onChange={() => {
+                checkMap.ir = !checkMap.ir;
+                this.setState({ checkMap });
+              }} />
+              <span className="m-l-5">逻辑</span>
+              <span className="t-8">(版本{irVersion} 最后更新:{formatDate(data.irTime, 'YYYY-MM-DD HH:mm:ss')})</span>
+            </div>}
+            {rcVersion > 0 && <div className="t-2 t-s-16">
+              <Checkbox checked={checkMap.rc} onChange={() => {
+                checkMap.rc = !checkMap.rc;
+                this.setState({ checkMap });
+              }} />
+              <span className="m-l-5">阅读</span>
+              <span className="t-8">(版本{rcVersion} 最后更新:{formatDate(data.rcTime, 'YYYY-MM-DD HH:mm:ss')})</span>
+            </div>}
           </div>
         </div>
       </Modal>
@@ -583,7 +618,7 @@ export class DownloadModal extends Component {
 // 模考开通确认
 export class OpenConfirmModal extends Component {
   render() {
-    const { show, onConfirm, onCancel } = this.props;
+    const { show, onConfirm, onCancel, data = {} } = this.props;
     return (
       <Modal
         className="open-confirm-modal"
@@ -596,8 +631,8 @@ export class OpenConfirmModal extends Component {
         onCancel={onCancel}
       >
         <div className="open-confirm-modal-wrapper m-b-2">
-          <div className="t-2 t-s-18">您正在开通「千行CAT模考」。</div>
-          <div className="t-2 t-s-18">模考有效期至:2019-11-17</div>
+          <div className="t-2 t-s-18">您正在开通「{data.title}」。</div>
+          <div className="t-2 t-s-18">模考有效期至:{data.endTime && formatDate(data.endTime, 'YYYY-MM-DD')}</div>
         </div>
       </Modal>
     );
@@ -629,16 +664,33 @@ export class RestartConfirmModal extends Component {
   }
 }
 
-export class CheckErrorModal extends Component {
+export class FeedbackErrorDataModal extends Component {
   constructor(props) {
     super(props);
-    this.state = { data: { position: [] } };
+    this.state = { data: { position: ['', '', ''] } };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.defaultData) {
+      this.setState({ data: Object.assign({}, nextProps.defaultData, this.state.data) });
+    }
   }
 
   onConfirm() {
     const { onConfirm } = this.props;
-    if (onConfirm) onConfirm(this.state.data);
-    this.setState({ data: { position: [] } });
+    const { data } = this.state;
+    console.log(data, this.props);
+    if (!data.content || !data.originContent) return;
+    My.addFeedbackErrorData(
+      data.dataId,
+      data.title,
+      data.position.join(','),
+      data.originContent,
+      data.content,
+    ).then(() => {
+      if (onConfirm) onConfirm();
+      this.setState({ data: { position: ['', '', ''] } });
+    });
   }
 
   onCancel() {
@@ -719,16 +771,32 @@ export class CheckErrorModal extends Component {
   }
 }
 
-export class QuestionModal extends Component {
+export class AskCourseModal extends Component {
   constructor(props) {
     super(props);
-    this.state = { data: {} };
+    this.state = { data: { position: [] } };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.defaultData) {
+      this.setState({ data: Object.assign({}, nextProps.defaultData, this.state.data) });
+    }
   }
 
   onConfirm() {
-    const { onConfirm } = this.props;
-    if (onConfirm) onConfirm(this.state.data);
-    this.setState({ data: {} });
+    const { course, courseNo, onConfirm } = this.props;
+    const { data } = this.state;
+    if (!data.position || !data.originContent || !data.content) return;
+    My.addCourseAsk(
+      course.id,
+      courseNo.id,
+      data.position.join(','),
+      data.originContent,
+      data.content,
+    ).then(() => {
+      if (onConfirm) onConfirm();
+      this.setState({ data: { position: [] } });
+    });
   }
 
   onCancel() {
@@ -738,7 +806,7 @@ export class QuestionModal extends Component {
   }
 
   render() {
-    const { show, selectList } = this.props;
+    const { show, selectList, courseNo } = this.props;
     const { data } = this.state;
     return (
       <Modal
@@ -751,7 +819,7 @@ export class QuestionModal extends Component {
         onCancel={() => this.onCancel()}
       >
         <div className="t-2 m-b-1 t-s-16">
-          针对<span className="t-4">课时1</span>的 <Select theme="white" list={selectList} />
+          针对<span className="t-4">课时{courseNo.no}</span>的 <Select theme="white" list={selectList} />
           进行提问.
         </div>
         <div className="t-2 t-s-16">老师讲解的内容是:</div>
@@ -782,16 +850,30 @@ export class QuestionModal extends Component {
   }
 }
 
-export class NoteModal extends Component {
+export class CourseNoteModal extends Component {
   constructor(props) {
     super(props);
     this.state = { data: {} };
   }
 
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.defaultData) {
+      this.setState({ data: Object.assign({}, nextProps.defaultData, this.state.data) });
+    }
+  }
+
   onConfirm() {
-    const { onConfirm } = this.props;
-    if (onConfirm) onConfirm(this.state.data);
-    this.setState({ data: {} });
+    const { course, onConfirm } = this.props;
+    const { data } = this.state;
+    if (!data.content) return;
+    My.updateCourseNote(
+      course.id,
+      data.courseNoId,
+      data.content,
+    ).then(() => {
+      if (onConfirm) onConfirm();
+      this.setState({ data: {} });
+    });
   }
 
   onCancel() {
@@ -801,7 +883,7 @@ export class NoteModal extends Component {
   }
 
   render() {
-    const { show, selectList } = this.props;
+    const { show, course = {}, courseNos = [] } = this.props;
     const { data } = this.state;
     return (
       <Modal
@@ -813,16 +895,19 @@ export class NoteModal extends Component {
         onCancel={() => this.onCancel()}
       >
         <div className="t-2 m-b-1 t-s-16">
-          OG20 刷题 语文 SC
-          <Select theme="white" list={selectList} />
+          {course.title}
+          <Select theme="white" value={data.courseNoId} list={courseNos} onChange={(item) => {
+            data.courseNoId = item.id;
+            this.setState({ data });
+          }} />
         </div>
         <textarea
-          value={data.originContent}
+          value={data.content}
           className="b-c-1 w-10 p-10"
           rows={10}
           placeholder={'写下笔记,方便以后复习。'}
           onChange={e => {
-            data.originContent = e.target.value;
+            data.content = e.target.value;
             this.setState({ data });
           }}
         />
@@ -831,3 +916,153 @@ export class NoteModal extends Component {
     );
   }
 }
+
+export class TextbookFeedbackModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { data: {} };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.defaultData) {
+      this.setState({ data: Object.assign({}, nextProps.defaultData, this.state.data) });
+    }
+  }
+
+  onConfirm() {
+    const { onConfirm } = this.props;
+    const { data } = this.state;
+    if (!data.content) return;
+    if (data.target !== 'new' && !data.no) return;
+    My.addTextbookFeedback(data.questionSubject, data.target, data.no, data.content).then(() => {
+      if (onConfirm) onConfirm();
+      this.setState({ data: {} });
+    });
+  }
+
+  onCancel() {
+    const { onCancel } = this.props;
+    if (onCancel) onCancel();
+    this.setState({ data: {} });
+  }
+
+  render() {
+    const { show } = this.props;
+    const { data } = this.state;
+    return (
+      <Modal
+        show={show}
+        title="反馈"
+        width={630}
+        onConfirm={() => this.onConfirm()}
+        onCancel={() => this.onCancel()}
+      >
+        <div className="t-2 t-s-16 m-b-1">
+          机经类别: <Select
+            value={data.questionSubject}
+            theme="white"
+            list={[{ title: '数学机经', key: 'quant' }, { title: '逻辑机经', key: 'rc' }, { title: '阅读机经', key: 'ir' }]}
+            onChange={(value) => {
+              data.questionSubject = value;
+              this.setState({ data });
+            }}
+          />
+          反馈类型: <Select
+            value={data.target}
+            theme="white"
+            list={TextbookFeedbackTarget}
+            onChange={(value) => {
+              data.target = value;
+              this.setState({ data });
+            }}
+          />
+          <span hidden={data.target === 'new'}> 题号是 <input value={data.no} style={{ width: 80 }} className="m-l-1 b-c-1 t-c" onChange={e => {
+            data.no = e.target.value;
+            this.setState({ data });
+          }} />
+          </span>
+        </div>
+        <div className="t-2 t-s-16">{TextbookFeedbackTargetMap[data.target]}:</div>
+        <textarea
+          value={data.content}
+          className="b-c-1 w-10 p-10"
+          rows={10}
+          placeholder={TextbookFeedbackTargetMap[data.target]}
+          onChange={e => {
+            data.content = e.target.value;
+            this.setState({ data });
+          }}
+        />
+        <div className="b-b m-t-2" />
+      </Modal>
+    );
+  }
+}
+
+export class CommentModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { data: {} };
+  }
+
+  onConfirm() {
+    const { onConfirm } = this.props;
+    const { data } = this.state;
+    if (!data.content) return;
+    My.addComment(data.channel, data.position, data.content).then(() => {
+      if (onConfirm) onConfirm();
+      this.setState({ data: {} });
+    });
+  }
+
+  onCancel() {
+    const { onCancel } = this.props;
+    if (onCancel) onCancel();
+    this.setState({ data: {} });
+  }
+
+  render() {
+    const { show } = this.props;
+    const { data } = this.state;
+    return (
+      <Modal
+        show={show}
+        title="评价"
+        onConfirm={() => this.onConfirm()}
+        onCancel={() => this.onCancel()}
+      >
+        <textarea
+          value={data.content}
+          className="b-c-1 w-10 p-10"
+          rows={6}
+          placeholder="您的看法对我们来说很重要!"
+          onChange={e => {
+            data.content = e.target.value;
+            this.setState({ data });
+          }}
+        />
+        <div className="b-b m-t-2" />
+      </Modal>
+    );
+  }
+}
+
+export class FinishModal extends Component {
+  render() {
+    const { show, onConfirm } = this.props;
+    return (
+      <Modal
+        show={show}
+        title="提交成功"
+        confirmText="好的,知道了"
+        btnAlign="center"
+        onConfirm={() => onConfirm()}
+      >
+        <div className="t-2 t-s-18">
+          <Icon type="check" className="t-5 m-r-5" />
+          您的每一次反馈都是千行进步的动力。
+      </div>
+      </Modal>
+    );
+  }
+}

+ 11 - 5
front/project/www/components/PayModal/index.js

@@ -15,10 +15,16 @@ export class PayModal extends Component {
   constructor(props) {
     super(props);
     this.state = { show: true };
-    Main.getContract('course')
-      .then((result) => {
-        this.setState({ contract: result });
-      });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.show && !this.init) {
+      this.init = true;
+      Main.getContract('course')
+        .then((result) => {
+          this.setState({ contract: result });
+        });
+    }
   }
 
   changePay(key) {
@@ -300,7 +306,7 @@ export class PayMutilModal extends Component {
         <div className="pay-modal-wrapper">
           <div className="info-layout">
             <div className="desc">
-              商品: 购物车结账<br />
+              商品: {order.checkouts.map(row => row.title).join(' ')}<br />
               服务: 见<a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a><br />
               开通有效期: 见订单详情
               <br />

+ 14 - 8
front/project/www/components/VipRenew/index.js

@@ -19,15 +19,21 @@ export default class extends Component {
   constructor(props) {
     super(props);
     this.state = { tab: '2', pay: '', select: null };
-    Main.getService('vip')
-      .then(result => {
-        result.package = result.package.map((row, index) => {
-          row.label = `${row.title}: ¥${formatMoney(row.price)}`;
-          row.value = ServiceParamMap.vip[index].value;
-          return row;
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.show && !this.init) {
+      this.init = true;
+      Main.getService('vip')
+        .then(result => {
+          result.package = result.package.map((row, index) => {
+            row.label = `${row.title}: ¥${formatMoney(row.price)}`;
+            row.value = ServiceParamMap.vip[index].value;
+            return row;
+          });
+          this.setState({ service: result });
         });
-        this.setState({ service: result });
-      });
+    }
   }
 
   changeTab(key) {

+ 7 - 6
front/project/www/routes/examination/list/page.js

@@ -4,6 +4,7 @@ import './index.less';
 import Page from '@src/containers/Page';
 import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
 import { formatPercent, formatDate } from '@src/services/Tools';
+import { RestartConfirmModal } from '../../../components/OtherModal';
 import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
@@ -593,15 +594,13 @@ export default class extends Page {
   }
 
   resetCat() {
-    asyncConfirm('提示', '是否重置', () => {
-      Question.resetCat().then(() => {
-        this.refresh();
-      });
+    Question.resetCat().then(() => {
+      this.refresh();
     });
   }
 
   renderView() {
-    const { list, navs, search, examination = {} } = this.state;
+    const { list, navs, search, examination = {}, reset } = this.state;
     const { finish } = search;
     return (
       <div>
@@ -632,7 +631,7 @@ export default class extends Page {
                     size="small"
                     radius
                     onClick={() => {
-                      this.resetExamination();
+                      this.setState({ reset: true });
                     }}
                   >
                     Reset
@@ -671,6 +670,8 @@ export default class extends Page {
             columns={this.qxCatColumns}
           />
         </div>
+
+        <RestartConfirmModal show={reset} onConfirm={() => this.resetCat()} onCancel={() => this.setState({ reset: false })} />
       </div>
     );
   }

+ 8 - 4
front/project/www/routes/examination/main/page.js

@@ -4,6 +4,7 @@ import Page from '@src/containers/Page';
 // import { asyncSMessage } from '@src/services/AsyncTools';
 import { formatTreeData, formatPercent, formatDate } from '@src/services/Tools';
 import Panel, { WaitPanel, BuyPanel, SmallPanel, SmallWaitPanel, SmallBuyPanel } from '../../../components/Panel';
+import { OpenConfirmModal } from '../../../components/OtherModal';
 import Tabs from '../../../components/Tabs';
 import Module from '../../../components/Module';
 import Division from '../../../components/Division';
@@ -112,6 +113,7 @@ export default class extends Page {
           const day = parseInt((new Date().getTime() - new Date(row.startDate).getTime()) / 86400000, 10);
           row.desc = [`最近换库:${formatDate(row.startDate, 'YYYY-MM-DD')} ,已换库${day}天`, `最后更新:${formatDate(row.updateTime)}`];
         }
+        if (row.unUseRecord) User.formatCheckout(row.unUseRecord);
         return row;
       });
       this.setState({ textbookProgress: result });
@@ -148,7 +150,7 @@ export default class extends Page {
         row.pieValue = formatPercent(row.userNumber, row.paperNumber);
         row.pieText = formatPercent(row.userNumber, row.paperNumber, false);
         row.pieSubText = `共${row.paperNumber}套`;
-
+        if (row.unUseRecord) User.formatCheckout(row.unUseRecord);
         return row;
       });
       this.setState({ examinationProgress: result });
@@ -226,7 +228,7 @@ export default class extends Page {
   }
 
   renderView() {
-    const { tab1, tab2, tabs } = this.state;
+    const { tab1, tab2, tabs, record } = this.state;
     const [subject] = tabs.filter(row => row.key === tab1);
     const children = (subject && subject.children) ? subject.children : [];
     return (
@@ -250,6 +252,8 @@ export default class extends Page {
 
           {this.state.faqs && <QAList data={this.state.faqs} active={'faq'} tabs={[{ key: 'faq', name: 'FAQs' }]} />}
         </div>
+
+        <OpenConfirmModal data={record} onCancel={() => this.setState({ record: null })} onConfirm={() => this.open(record.id)} />
       </div>
     );
   }
@@ -267,7 +271,7 @@ export default class extends Page {
                   col="3"
                   data={struct}
                   onOpen={() => {
-                    this.open(struct.unUseRecord.id);
+                    this.setState({ record: struct.unUseRecord });
                   }}
                 />;
               }
@@ -311,7 +315,7 @@ export default class extends Page {
                 title={struct.title}
                 data={struct}
                 onOpen={() => {
-                  this.open(struct.unUseRecord.id);
+                  this.setState({ record: struct.unUseRecord });
                 }}
               />;
             }

+ 17 - 47
front/project/www/routes/my/course/page.js

@@ -19,6 +19,7 @@ import More from '../../../components/More';
 import Modal from '../../../components/Modal';
 import DatePlane from '../../../components/Date';
 import Note from '../../../components/Note';
+import { FinishModal, CommentModal } from '../../../components/OtherModal';
 import { My } from '../../../stores/my';
 import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
@@ -236,13 +237,6 @@ export default class extends Page {
     });
   }
 
-  submitComment() {
-    const { comment } = this.state;
-    My.addComment(comment.channel, comment.position, comment.content).then(() => {
-      this.setState({ showComment: false, showFinish: true, comment: {} });
-    });
-  }
-
   open(recordId) {
     Order.useRecord(recordId).then(() => {
       this.refreshDetail(recordId);
@@ -391,36 +385,6 @@ export default class extends Page {
           <div className="t-2 t-s-12">*听课频率≤2天/课时,作业完成度≥90%,课程有效期可延长7-10天。</div>
         </Modal>
         <Modal
-          show={showComment}
-          title="评价"
-          onConfirm={() => comment.content && this.submitComment()}
-          onCancel={() => this.setState({ showComment: false, comment: {} })}
-        >
-          <textarea
-            value={comment.content}
-            className="b-c-1 w-10 p-10"
-            rows={6}
-            placeholder="您的看法对我们来说很重要!"
-            onChange={e => {
-              comment.content = e.target.value;
-              this.setState({ comment });
-            }}
-          />
-          <div className="b-b m-t-2" />
-        </Modal>
-        <Modal
-          show={showFinish}
-          title="提交成功"
-          confirmText="好的,知道了"
-          btnAlign="center"
-          onConfirm={() => this.setState({ showFinish: false })}
-        >
-          <div className="t-2 t-s-18">
-            <Icon type="check" className="t-5 m-r-5" />
-            您的每一次反馈都是千行进步的动力。
-          </div>
-        </Modal>
-        <Modal
           show={showSuspend}
           title="申请停课"
           width={630}
@@ -554,6 +518,18 @@ export default class extends Page {
           </div>
           <div className="b-b m-t-2" />
         </Modal>
+
+        <CommentModal
+          show={showComment}
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div>
     );
   }
@@ -1167,9 +1143,7 @@ class CourseVs extends Component {
           render: (text, record) => {
             return record.noteList && record.noteList.length > 0 ? (
               <a onClick={() => this.props.onNote(record)}>查看</a>
-            ) : (
-              <span>查看</span>
-            );
+            ) : (<span>查看</span>);
           },
         },
         {
@@ -1178,9 +1152,7 @@ class CourseVs extends Component {
           render: (text, record) => {
             return record.supplyList && record.supplyList.length > 0 ? (
               <a onClick={() => this.props.onSupply(record)}>查看</a>
-            ) : (
-              <span>查看</span>
-            );
+            ) : (<span>查看</span>);
           },
         },
       ],
@@ -1677,8 +1649,7 @@ class TimeLineItem extends Component {
               <Button size="small" radius disabled>
                 提交
                 </Button>
-            </div>
-            );
+            </div>);
           case 'not':
             return data.cctalkName ? (
               <span>
@@ -1705,8 +1676,7 @@ class TimeLineItem extends Component {
               >
                 提交
                 </Button>
-            </div>
-            );
+            </div>);
           default:
             return (
               <span>

+ 50 - 159
front/project/www/routes/my/tools/page.js

@@ -1,6 +1,5 @@
 import React from 'react';
 import './index.less';
-import { Icon } from 'antd';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import { asyncSMessage } from '@src/services/AsyncTools';
@@ -13,7 +12,7 @@ import More from '../../../components/More';
 import Button from '../../../components/Button';
 import Switch from '../../../components/Switch';
 import TotalSort from '../../../components/TotalSort';
-import { RealAuth } from '../../../components/OtherModal';
+import { RealAuth, TextbookFeedbackModal, FinishModal, CommentModal, FeedbackErrorDataModal } from '../../../components/OtherModal';
 import Examination from '../../../components/Examination';
 import VipRenew from '../../../components/VipRenew';
 import Modal from '../../../components/Modal';
@@ -26,11 +25,9 @@ import { Textbook } from '../../../stores/textbook';
 import { DataType, ServiceKey, RecordSource, TextbookFeedbackTarget } from '../../../../Constant';
 import { Main } from '../../../stores/main';
 import { Question } from '../../../stores/question';
-import Select from '../../../components/Select';
 
 const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
 const RecordSourceMap = getMap(RecordSource, 'value', 'label');
-const TextbookFeedbackTargetMap = getMap(TextbookFeedbackTarget, 'value', 'tips');
 
 const dataHistoryColumns = [
   { title: '更新时间', key: 'time', width: 120 },
@@ -275,37 +272,6 @@ export default class extends Page {
     this.refreshQuery(data);
   }
 
-  submitComment() {
-    const { comment } = this.state;
-    if (!comment.content) return;
-    My.addComment(comment.channel, comment.position, comment.content).then(() => {
-      this.setState({ showComment: false, showFinish: true, comment: {} });
-    });
-  }
-
-  submitFeedbackError() {
-    const { feedbackError } = this.state;
-    if (!feedbackError.content || !feedbackError.originContent) return;
-    My.addFeedbackErrorData(
-      feedbackError.dataId,
-      feedbackError.title,
-      feedbackError.position.join(','),
-      feedbackError.originContent,
-      feedbackError.content,
-    ).then(() => {
-      this.setState({ showFinish: true, showFeedbackError: false, feedbackError: { position: ['', '', ''] } });
-    });
-  }
-
-  submitFeedback() {
-    const { feedback } = this.state;
-    if (!feedback.content) return;
-    if (feedback.target !== 'new' && !feedback.no) return;
-    My.addTextbookFeedback(feedback.questionSubject, feedback.target, feedback.no, feedback.content).then(() => {
-      this.setState({ showFinish: true, showFeedback: false, feedback: {} });
-    });
-  }
-
   subscribe(value) {
     My.subscribeData(value)
       .then(() => {
@@ -324,6 +290,32 @@ export default class extends Page {
     });
   }
 
+  buyTextbook() {
+    User.needLogin()
+      .then(() => {
+        return Order.speedPay({ productType: 'service', service: 'textbook' });
+      })
+      .then((order) => {
+        return User.needPay(order);
+      })
+      .then(() => {
+        this.refresh();
+      });
+  }
+
+  buyQxCat() {
+    User.needLogin()
+      .then(() => {
+        return Order.speedPay({ productType: 'service', service: 'qx_cat' });
+      })
+      .then((order) => {
+        return User.needPay(order);
+      })
+      .then(() => {
+        this.refresh();
+      });
+  }
+
   renderView() {
     const { config } = this.props;
     return <UserLayout active={config.key} menu={menu} center={this.renderDetail()} />;
@@ -398,134 +390,31 @@ export default class extends Page {
             maxHeight={maxHeight}
           />
         </Modal>
-        <Modal
+        <CommentModal
           show={showComment}
-          title="评价"
-          onConfirm={() => comment.content && this.submitComment()}
-          onCancel={() => this.setState({ showComment: false, comment: {} })}
-        >
-          <textarea
-            value={comment.content}
-            className="b-c-1 w-10 p-10"
-            rows={6}
-            placeholder="您的看法对我们来说很重要!"
-            onChange={e => {
-              comment.content = e.target.value;
-              this.setState({ comment });
-            }}
-          />
-          <div className="b-b m-t-2" />
-        </Modal>
-        <Modal
-          show={showFinish}
-          title="提交成功"
-          confirmText="好的,知道了"
-          btnAlign="center"
-          onConfirm={() => this.setState({ showFinish: false })}
-        >
-          <div className="t-2 t-s-18">
-            <Icon type="check" className="t-5 m-r-5" />
-            您的每一次反馈都是千行进步的动力。
-          </div>
-        </Modal>
-        <Modal
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FeedbackErrorDataModal
           show={showFeedbackError}
-          title="纠错"
-          btnType="link"
-          width={630}
-          onConfirm={() => this.submitFeedbackError()}
+          defaultData={feedbackError}
+          onConfirm={() => this.setState({ showFeedbackError: false, showFinish: true })}
           onCancel={() => this.setState({ showFeedbackError: false })}
-        >
-          <div className="t-2 m-b-1 t-s-16">
-            定位:
-            <input value={feedbackError.position[0]} className="t-c b-c-1 m-r-5" style={{ width: 56 }} onChange={e => {
-              feedbackError.position[0] = e.target.value;
-              this.setState({ feedbackError });
-            }} />
-            <span className="require">页</span>
-            <input value={feedbackError.position[1]} className="t-c b-c-1 m-r-5" style={{ width: 56 }} onChange={e => {
-              feedbackError.position[1] = e.target.value;
-              this.setState({ feedbackError });
-            }} />
-            <span className="require">行</span> , 题号
-            <input value={feedbackError.position[2]} className="t-c b-c-1" style={{ width: 56 }} onChange={e => {
-              feedbackError.position[2] = e.target.value;
-              this.setState({ feedbackError });
-            }} />
-          </div>
-          <div className="t-2 t-s-16">错误内容是:</div>
-          <textarea
-            value={feedbackError.originContent}
-            className="b-c-1 w-10 p-10"
-            rows={10}
-            placeholder={'可简单描述您发现的问题'}
-            onChange={e => {
-              feedbackError.originContent = e.target.value;
-              this.setState({ feedbackError });
-            }}
-          />
-          <div className="t-2 t-s-16">应该更改为:</div>
-          <textarea
-            value={feedbackError.content}
-            className="b-c-1 w-10 p-10"
-            rows={10}
-            placeholder={'提供您认为正确的内容即可'}
-            onChange={e => {
-              feedbackError.content = e.target.value;
-              this.setState({ feedbackError });
-            }}
-          />
-          <div className="b-b m-t-2" />
-        </Modal>
-        <Modal
+          onClose={() => this.setState({ showFeedbackError: false })}
+        />
+        <TextbookFeedbackModal
           show={showFeedback}
-          title="反馈"
-          width={630}
-          onConfirm={() => this.submitFeedback()}
+          defaultData={feedback}
+          onConfirm={() => this.setState({ showFeedback: false, showFinish: true })}
           onCancel={() => this.setState({ showFeedback: false })}
-        >
-          <div className="t-2 t-s-16 m-b-1">
-            机经类别:
-            <Select
-              value={feedback.questionSubject}
-              theme="white"
-              list={[{ title: '数学机经', key: 'quant' }, { title: '逻辑机经', key: 'rc' }, { title: '阅读机经', key: 'ir' }]}
-              onChange={(value) => {
-                feedback.questionSubject = value;
-                this.setState({ feedback });
-              }}
-            />
-            反馈类型:
-            <Select
-              value={feedback.target}
-              theme="white"
-              list={TextbookFeedbackTarget}
-              onChange={(value) => {
-                feedback.target = value;
-                this.setState({ feedback });
-              }}
-            />
-            <span hidden={feedback.target === 'new'}>
-              题号是
-              <input value={feedback.no} style={{ width: 80 }} className="m-l-1 b-c-1 t-c" onChange={e => {
-                feedback.no = e.target.value;
-                this.setState({ feedback });
-              }} />
-            </span>
-          </div>
-          <div className="t-2 t-s-16">{TextbookFeedbackTargetMap[feedback.target]}:</div>
-          <textarea
-            value={feedback.content}
-            className="b-c-1 w-10 p-10"
-            rows={10}
-            placeholder={TextbookFeedbackTargetMap[feedback.target]}
-            onChange={e => {
-              feedback.content = e.target.value;
-              this.setState({ feedback });
-            }}
-          />
-          <div className="b-b m-t-2" />
-        </Modal>
+          onClose={() => this.setState({ showFeedback: false })}
+        />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
         <Examination
           show={showExamination}
           data={info}
@@ -794,7 +683,9 @@ export default class extends Page {
               {formatDate(data.startTime, 'YYYY-MM-DD')} ~ {formatDate(data.expireTime, 'YYYY-MM-DD')}
             </div>
             <div className="desc">¥ {service && service.package && service.package[0].price}</div>
-            <Button radius size="lager" width={150}>
+            <Button radius size="lager" width={150} onClick={() => {
+              this.buyQxCat();
+            }}>
               立即购买
             </Button>
           </div>

+ 2 - 2
front/project/www/routes/page/cart/page.js

@@ -297,8 +297,8 @@ class OrderItem extends Component {
         <div style={{ width: 120 }} className="d-i-b t-8 t-s-12 p-r">
           {data.number > 0 && ['数量',
             <input value={data.number} style={{ width: 32 }} className="m-l-5 t-c" />,
-            <Icon className="up" type="caret-up" onClick={() => onChangeNumber(data.number + 1)} />,
-            <Icon className="down" type="caret-down" onClick={() => data.number === 1 && onChangeNumber(data.number - 1)} />]}
+            <Icon className="up" type="caret-up" onClick={() => data.number < data.maxNumber && onChangeNumber(data.number + 1)} />,
+            <Icon className="down" type="caret-down" onClick={() => data.number > 1 && data.number > data.minNumber && onChangeNumber(data.number - 1)} />]}
         </div>
         <div className="d-i-b t-7 t-s-16"> ¥ {data.money}</div>
       </div>

+ 2 - 2
front/project/www/routes/room/main/page.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { NoteModal } from '../../../components/OtherModal';
+import { CourseNoteModal } from '../../../components/OtherModal';
 
 export default class extends Page {
   initState() {
@@ -11,7 +11,7 @@ export default class extends Page {
   renderView() {
     return (
       <div>
-        <NoteModal selectList={[{ title: '123', key: '123' }]} show onConfirm={() => {}} onCancel={() => {}} />
+        <CourseNoteModal selectList={[{ title: '123', key: '123' }]} show onConfirm={() => { }} onCancel={() => { }} />
       </div>
     );
   }

+ 10 - 2
front/project/www/routes/textbook/list/page.js

@@ -8,6 +8,7 @@ import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
 import Button from '../../../components/Button';
+import { DownloadModal } from '../../../components/OtherModal';
 import { Question } from '../../../stores/question';
 import { Textbook } from '../../../stores/textbook';
 import Select from '../../../components/Select';
@@ -174,7 +175,7 @@ export default class extends Page {
   }
 
   renderView() {
-    const { list, search, info = {}, textbook = {} } = this.state;
+    const { list, search, info = {}, textbook = {}, showDownload } = this.state;
     const { finish } = search;
     return (
       <div>
@@ -194,7 +195,7 @@ export default class extends Page {
                 this.subscribe(!textbook.subscribe);
               }} /></span>}
               {!!info.latest && <Button radius onClick={() => {
-                this.download();
+                this.setState({ showDownload: true });
               }}>下载</Button>}
               {!info.latest && <Select value={info.year} theme="default" list={this.state.yearList || []} onChange={(item) => {
                 this.search({ year: item.key });
@@ -222,6 +223,13 @@ export default class extends Page {
             columns={this.columns}
           />
         </div>
+        <DownloadModal
+          show={showDownload}
+          data={textbook.latest}
+          onConfirm={() => this.setState({ showDownload: false })}
+          onCancel={() => this.setState({ showDownload: false })}
+          onClose={() => this.setState({ showDownload: false })}
+        />
       </div>
     );
   }

+ 4 - 0
front/project/www/stores/main.js

@@ -52,6 +52,10 @@ export default class MainStore extends BaseStore {
     return this.apiGet('/base/experience');
   }
 
+  getCourseIndex() {
+    return this.apiGet('/base/course_index');
+  }
+
   /**
    * 获取考分排行信息
    */

+ 7 - 2
front/project/www/stores/my.js

@@ -320,10 +320,11 @@ export default class MyStore extends BaseStore {
    * @param {*} courseId
    * @param {*} courseNoId
    * @param {*} position
+   * @param {*} originContent
    * @param {*} content
    */
-  addCourseAsk(courseId, courseNoId, position, content) {
-    return this.apiPost('/my/ask/course', { courseId, courseNoId, position, content });
+  addCourseAsk(courseId, courseNoId, position, originContent, content) {
+    return this.apiPost('/my/ask/course', { courseId, courseNoId, position, originContent, content });
   }
 
   /**
@@ -485,6 +486,10 @@ export default class MyStore extends BaseStore {
     return this.apiPost('/my/export/note/course', { setting });
   }
 
+  exportDetail(id) {
+    return this.apiGet('/my/export/detail', { id });
+  }
+
   /**
    * 关闭导出提示
    */

+ 6 - 0
front/project/www/stores/user.js

@@ -19,6 +19,8 @@ function formatTitle(record) {
     return (record.coursePackage || {}).title;
   }
   if (record.productType === 'course') {
+    record.minNumber = record.course.minNumber;
+    record.maxNumber = record.course.maxNumber;
     return (record.course || {}).title;
   }
   if (record.productType === 'data') {
@@ -92,6 +94,10 @@ export default class UserStore extends BaseStore {
     });
   }
 
+  formatCheckout(record) {
+    formatCheckout(record);
+  }
+
   formatOrder(order) {
     formatCheckout(order.checkouts);
   }

+ 1 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/MessageMethod.java

@@ -7,6 +7,7 @@ public enum MessageMethod {
     INSIDE("inside", "站内信"),
     EMAIL("email", "邮件"),
     WECHAT("wechat","微信通知"),
+    SMS("sms", "短信"),
     ;
 
     final static public String message = "消息方式";

+ 35 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/UserAskCourse.java

@@ -90,6 +90,12 @@ public class UserAskCourse implements Serializable {
     private Date updateTime;
 
     /**
+     * 老师讲解内容
+     */
+    @Column(name = "`origin_content`")
+    private String originContent;
+
+    /**
      * 提问
      */
     @Column(name = "`content`")
@@ -362,6 +368,24 @@ public class UserAskCourse implements Serializable {
     }
 
     /**
+     * 获取老师讲解内容
+     *
+     * @return origin_content - 老师讲解内容
+     */
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    /**
+     * 设置老师讲解内容
+     *
+     * @param originContent 老师讲解内容
+     */
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
+
+    /**
      * 获取提问
      *
      * @return content - 提问
@@ -418,6 +442,7 @@ public class UserAskCourse implements Serializable {
         sb.append(", sort=").append(sort);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
+        sb.append(", originContent=").append(originContent);
         sb.append(", content=").append(content);
         sb.append(", answer=").append(answer);
         sb.append("]");
@@ -590,6 +615,16 @@ public class UserAskCourse implements Serializable {
         }
 
         /**
+         * 设置老师讲解内容
+         *
+         * @param originContent 老师讲解内容
+         */
+        public Builder originContent(String originContent) {
+            obj.setOriginContent(originContent);
+            return this;
+        }
+
+        /**
          * 设置提问
          *
          * @param content 提问

+ 2 - 1
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAskCourseMapper.xml

@@ -25,6 +25,7 @@
     <!--
       WARNING - @mbg.generated
     -->
+    <result column="origin_content" jdbcType="LONGVARCHAR" property="originContent" />
     <result column="content" jdbcType="LONGVARCHAR" property="content" />
     <result column="answer" jdbcType="LONGVARCHAR" property="answer" />
   </resultMap>
@@ -40,6 +41,6 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `content`, `answer`
+    `origin_content`, `content`, `answer`
   </sql>
 </mapper>

+ 8 - 6
server/data/src/main/resources/db/migration/V1__init_table.sql

@@ -974,6 +974,7 @@ CREATE TABLE user_ask_course (
   course_no_id int(11) unsigned NOT NULL COMMENT '课时id',
   record_id int(11) unsigned NOT NULL COMMENT '记录id',
   position varchar(20) NOT NULL DEFAULT '' COMMENT '位置',
+  origin_content text COMMENT '老师讲解内容',
   content text COMMENT '提问',
   ask_time int(11) unsigned NOT NULL DEFAULT '0' COMMENT '提问优先回答时间',
   expire_time datetime DEFAULT NULL COMMENT '过期时间',
@@ -1110,12 +1111,13 @@ CREATE TABLE user_course_record (
   KEY user_id (user_id,course_id,record_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-课程-访问记录';
 
-CREATE TABLE use_export (
-  id` int(11) unsigned NOT NULL AUTO_INCREMENT,
-  user_id int(11) unsigned NOT NULL DEFAULT '0',
-  type varchar(20) NOT NULL DEFAULT '' COMMENT '导出类型',
-  setting text COMMENT '导出设置',
-  create_time datetime DEFAULT NULL,
+CREATE TABLE `user_export` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) unsigned NOT NULL DEFAULT '0',
+  `type` varchar(20) NOT NULL DEFAULT '' COMMENT '导出类型',
+  `setting` text COMMENT '导出设置',
+  `content` longtext,
+  `create_time` datetime DEFAULT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-导出-记录';
 

+ 40 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java

@@ -1,5 +1,6 @@
 package com.qxgmat.controller.api;
 
+import com.github.pagehelper.Page;
 import com.nuliji.tools.Response;
 import com.nuliji.tools.ResponseHelp;
 import com.nuliji.tools.Tools;
@@ -8,7 +9,11 @@ import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.qxgmat.data.constants.enums.ServiceKey;
+import com.qxgmat.data.dao.entity.TextbookLibrary;
 import com.qxgmat.data.dao.entity.User;
+import com.qxgmat.data.dao.entity.UserMessage;
+import com.qxgmat.data.dao.entity.UserOrderRecord;
+import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.MyDto;
 import com.qxgmat.help.AiHelp;
@@ -17,7 +22,11 @@ import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.help.SmsHelp;
 import com.qxgmat.service.UsersService;
 import com.qxgmat.service.UserServiceService;
+import com.qxgmat.service.extend.PreviewService;
+import com.qxgmat.service.inline.TextbookLibraryService;
 import com.qxgmat.service.inline.UserAbnormalService;
+import com.qxgmat.service.inline.UserMessageService;
+import com.qxgmat.service.inline.UserOrderRecordService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -29,7 +38,9 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import javax.validation.Validator;
+import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 
 /**
  * Created by GaoJie on 2017/10/31.
@@ -60,6 +71,20 @@ public class AuthController {
     @Autowired
     private UserAbnormalService userAbnormalService;
 
+    // 初始化用户信息
+
+    @Autowired
+    private TextbookLibraryService textbookLibraryService;
+
+    @Autowired
+    private UserMessageService userMessageService;
+
+    @Autowired
+    private UserOrderRecordService userOrderRecordService;
+
+    @Autowired
+    private PreviewService previewService;
+
 
     @RequestMapping(value = "/token", method = RequestMethod.POST)
     @ApiOperation(value = "验证token", httpMethod = "POST")
@@ -239,7 +264,22 @@ public class AuthController {
         if(!user.getPrepareStatus().isEmpty()){
             dto.setBindPrepare(true);
         }
+        // vip
         dto.setVip(userServiceService.timeService(user.getId(), ServiceKey.VIP));
+        // 最新机经
+        if (userServiceService.hasService(user.getId(), ServiceKey.TEXTBOOK)){
+            TextbookLibrary latest = textbookLibraryService.getLatest();
+            dto.setTextbook(latest.getUpdateTime());
+        }
+        // 未读消息
+        Page<UserMessage> messageList = userMessageService.list(1, 4, user.getId(), null, 0);
+        dto.setMessageNumber((int)messageList.getTotal());
+        dto.setMessages(messageList);
+        // 未完成作业
+        List<UserOrderRecord> recordList = userOrderRecordService.listWithCourse(1, 1000, null, null, true, false, null, null);
+        Collection recordIds = Transform.getIds(recordList, UserOrderRecord.class, "id");
+        List<UserPreviewPaperRelation> relationList = previewService.listByRecordId(user.getId(), recordIds, 2);
+        dto.setPreviewNumber(relationList.size());
         return dto;
     }
 }

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

@@ -150,6 +150,13 @@ public class BaseController {
         return ResponseHelp.success(rank);
     }
 
+    @RequestMapping(value = "/course_index", method = RequestMethod.GET)
+    @ApiOperation(value = "获取课程首页", notes = "获取课程首页", httpMethod = "GET")
+    public Response<JSONObject> courseIndex()  {
+        Setting entity = settingService.getByKey(SettingKey.COURSE_INDEX);
+        return ResponseHelp.success(entity.getValue());
+    }
+
     @RequestMapping(value = "/exercise/main", method = RequestMethod.GET)
     @ApiOperation(value = "所有练习头2层", httpMethod = "GET")
     public Response<List<ExerciseStruct>> exerciseMain(HttpSession session) {

+ 11 - 1
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -216,6 +216,9 @@ public class MyController {
     @Autowired
     private MessageExtendService messageExtendService;
 
+    @Autowired
+    private ExportService exportService;
+
     @RequestMapping(value = "/email", method = RequestMethod.POST)
     @ApiOperation(value = "绑定邮箱", httpMethod = "POST")
     public Response<Boolean> email(@RequestBody @Validated UserEmailDto dto, HttpSession session, HttpServletRequest request) {
@@ -1184,7 +1187,7 @@ public class MyController {
 
     @RequestMapping(value = "/note/course", method = RequestMethod.PUT)
     @ApiOperation(value = "更新课程笔记", notes = "更新课程笔记", httpMethod = "PUT")
-    public Response<Boolean> updateNoteCourse(@RequestBody @Validated UserNoteQuestionDto dto)  {
+    public Response<Boolean> updateNoteCourse(@RequestBody @Validated UserNoteCourseDto dto)  {
         UserNoteCourse entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
         entity.setUserId(user.getId());
@@ -1922,6 +1925,13 @@ public class MyController {
         return ResponseHelp.success(entity);
     }
 
+    @RequestMapping(value = "/export/note/course", method = RequestMethod.GET)
+    @ApiOperation(value = "导出详情", notes = "导出详情", httpMethod = "GET")
+    public Response<UserExport> exportDetail(int id)  {
+        UserExport entity = userExportService.get(id);
+        return ResponseHelp.success(entity);
+    }
+
     @RequestMapping(value = "/export/tips", method = RequestMethod.POST)
     @ApiOperation(value = "关闭提示", notes = "关闭提示", httpMethod = "POST")
     public Response<Boolean> exportTips()  {

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskCourseDto.java

@@ -11,6 +11,8 @@ public class UserAskCourseDto {
 
     private String position;
 
+    private String originContent;
+
     private String content;
 
     public Integer getCourseId() {
@@ -44,4 +46,12 @@ public class UserAskCourseDto {
     public void setContent(String content) {
         this.content = content;
     }
+
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
 }

+ 39 - 7
server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java

@@ -1,8 +1,10 @@
 package com.qxgmat.dto.response;
 
+import com.qxgmat.data.dao.entity.UserMessage;
 import io.swagger.annotations.ApiModelProperty;
 
 import java.util.Date;
+import java.util.List;
 
 
 /**
@@ -37,15 +39,13 @@ public class MyDto extends UserDto {
 
     private Date vip;
 
-    private int messageNum;
+    private Date textbook;
 
-    public int getMessageNum() {
-        return messageNum;
-    }
+    private List<UserMessage> messages;
 
-    public void setMessageNum(int messageNum) {
-        this.messageNum = messageNum;
-    }
+    private int messageNumber;
+
+    private int previewNumber;
 
     public String getAvatar() {
         return avatar;
@@ -158,4 +158,36 @@ public class MyDto extends UserDto {
     public void setInviteNumber(Integer inviteNumber) {
         this.inviteNumber = inviteNumber;
     }
+
+    public Date getTextbook() {
+        return textbook;
+    }
+
+    public void setTextbook(Date textbook) {
+        this.textbook = textbook;
+    }
+
+    public List<UserMessage> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<UserMessage> messages) {
+        this.messages = messages;
+    }
+
+    public int getMessageNumber() {
+        return messageNumber;
+    }
+
+    public void setMessageNumber(int messageNumber) {
+        this.messageNumber = messageNumber;
+    }
+
+    public int getPreviewNumber() {
+        return previewNumber;
+    }
+
+    public void setPreviewNumber(int previewNumber) {
+        this.previewNumber = previewNumber;
+    }
 }

+ 325 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/ExportService.java

@@ -0,0 +1,325 @@
+package com.qxgmat.service.extend;
+
+import com.nuliji.tools.Tools;
+import com.nuliji.tools.Transform;
+import com.nuliji.tools.exception.ParameterException;
+import com.qxgmat.data.constants.enums.QuestionSubject;
+import com.qxgmat.data.constants.enums.QuestionType;
+import com.qxgmat.data.constants.enums.module.VideoCourseType;
+import com.qxgmat.data.dao.entity.*;
+import com.qxgmat.data.relation.entity.UserRecordStatRelation;
+import com.qxgmat.service.inline.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ExportService {
+
+    @Autowired
+    private CourseService courseService;
+
+    @Autowired
+    private UserCourseService userCourseService;
+
+    @Autowired
+    private UserOrderRecordService userOrderRecordService;
+
+    @Autowired
+    private UserCourseRecordService userCourseRecordService;
+
+    @Resource
+    private UserCourseProgressService userCourseProgressService;
+
+    /**
+     * 计算vs课程有效期
+     * @param vsNumber
+     */
+    public int computeExpire(Integer vsNumber, Course course){
+        // day / 10
+        Integer expireDays = course.getExpirePreDays();
+
+        return Math.max(vsNumber / 10, 1) * expireDays;
+    }
+
+    /**
+     * 获取video课程有效期
+     * @param course
+     * @return
+     */
+    public int computeExpire(Course course){
+        return course.getUseExpireDays();
+    }
+
+    public void suspendCourse(Integer userId, Integer recordId){
+        UserOrderRecord record = userOrderRecordService.get(recordId);
+        if (record == null){
+            throw new ParameterException("记录不存在");
+        }
+        if (!record.getUserId().equals(userId)){
+            throw new ParameterException("记录不存在");
+        }
+        if (record.getIsUsed() == 0){
+            throw new ParameterException("课程还未开通");
+        }
+        if (record.getIsSuspend() > 0){
+            throw new ParameterException("已停课1次");
+        }
+        userOrderRecordService.edit(UserOrderRecord.builder()
+                .id(record.getId())
+                .isSuspend(1)
+                .suspendTime(new Date())
+                .build());
+    }
+
+    public void restoreCourse(Integer userId, Integer recordId){
+        UserOrderRecord record = userOrderRecordService.get(recordId);
+        if (record == null){
+            throw new ParameterException("记录不存在");
+        }
+        if (!record.getUserId().equals(userId)){
+            throw new ParameterException("记录不存在");
+        }
+        if (record.getIsUsed() == 0){
+            throw new ParameterException("课程还未开通");
+        }
+        if (record.getIsSuspend() == 0){
+            throw new ParameterException("无需恢复");
+        }
+        if (record.getRestoreTime() != null){
+            throw new ParameterException("已恢复");
+        }
+
+        userOrderRecordService.edit(UserOrderRecord.builder()
+                .id(record.getId())
+                .restoreTime(new Date())
+                .useEndTime(Tools.addDate(record.getUseEndTime(), (int)((new Date().getTime() - record.getSuspendTime().getTime()) / 86400000)))
+                .build());
+    }
+
+    public void awardCourse(Integer userId, Integer recordId, Integer day){
+        UserOrderRecord record = userOrderRecordService.get(recordId);
+        if (record == null){
+            throw new ParameterException("记录不存在");
+        }
+        if (!record.getUserId().equals(userId)){
+            throw new ParameterException("记录不存在");
+        }
+        if (record.getIsUsed() == 0){
+            throw new ParameterException("课程还未开通");
+        }
+        if (record.getCourseAward() > 0){
+            throw new ParameterException("已奖励");
+        }
+
+        userOrderRecordService.edit(UserOrderRecord.builder()
+                .id(record.getId())
+                .courseAward(day)
+                .useEndTime(Tools.addDate(record.getUseEndTime(), day))
+                .build());
+    }
+
+    /**
+     * 累计听课学习时间
+     * @param userId
+     * @return
+     */
+    public Integer studyTime(Integer userId, Date startTime, Date endTime){
+        UserRecordStatRelation record = userCourseRecordService.stat(userId, startTime != null? startTime.toString():null, endTime != null ? endTime.toString(): null);
+        return record != null ? record.getUserTime() : 0;
+    }
+
+    /**
+     * 全站平均听课时间
+     * @return
+     */
+    public Integer studyAvgTime(Date startTime, Date endTime){
+        UserRecordStatRelation record = userCourseRecordService.statAvg(startTime != null? startTime.toString():null, endTime != null ? endTime.toString(): null);
+        return record != null ? record.getUserTime() : 0;
+    }
+
+    public UserCourse userCourse(Integer userId, Integer courseId){
+        return userCourseService.getCourse(userId, courseId);
+    }
+
+    /**
+     * 根据题目类型获取课程信息
+     * @param questionType
+     * @return
+     */
+    public Integer questionRelationCourse(Integer userId, QuestionType questionType){
+        QuestionSubject subject = QuestionSubject.FromType(questionType);
+        // 只查询系统授课记录
+        List<Course> courseList = courseService.listByExtend(questionType.key, subject.key, VideoCourseType.SYSTEM.key);
+        if (courseList.size()==0)return null;
+        Collection ids = Transform.getIds(courseList, Course.class, "id");
+        List<UserCourse> userCourseList = userCourseService.listByCourse(userId, ids);
+        if (userCourseList.size() == 0) return null;
+        if (userCourseList.size() == 1) return userCourseList.get(0).getRecordId();
+        // 获取回答时间最小的记录
+        Collection recordIds = Transform.getIds(userCourseList, UserCourse.class, "recordId");
+        List<UserOrderRecord> userOrderRecordList =  userOrderRecordService.select(recordIds);
+        int min = 0;
+        Integer minId = null;
+        for(UserOrderRecord userOrderRecord : userOrderRecordList){
+            if (minId == null || min > userOrderRecord.getAskTime()){
+                min = userOrderRecord.getAskTime();
+                minId = userOrderRecord.getId();
+            }
+        }
+        return minId;
+    }
+
+    /**
+     * 获取已完成课时序号
+     * @param courseNoList
+     * @param progressList
+     * @return
+     */
+    public int computeCourseNoFinish(Collection<CourseNo> courseNoList, Collection<UserCourseProgress> progressList){
+        Map progressMap = Transform.getMap(progressList, UserCourseProgress.class, "courseNoId");
+
+        int min = 0;
+        for(CourseNo no : courseNoList){
+            UserCourseProgress progress = (UserCourseProgress)progressMap.get(no.getId());
+            if(progress == null) continue;
+            if (min != 0 && min > no.getNo()) continue;
+            if (progress.getProgress() > 50) {
+                min = no.getNo();
+            }
+        }
+        return min;
+    }
+
+    /**
+     * 获取当前课时
+     * @param courseNoList
+     * @param progressList
+     * @return
+     */
+    public int computeCourseNoCurrent(Collection<CourseNo> courseNoList, Collection<UserCourseProgress> progressList){
+        Map progressMap = Transform.getMap(progressList, UserCourseProgress.class, "courseNoId");
+
+        int min = 0;
+        for(CourseNo no : courseNoList){
+            // 如果进度不过半,按当前课时+下一课时
+            // 如果进度过半,下2次课时
+            UserCourseProgress progress = (UserCourseProgress)progressMap.get(no.getId());
+            if(progress == null) continue;
+            if (min != 0 && min > no.getNo()) continue;
+            min = no.getNo();
+        }
+        return min;
+    }
+
+
+    /**
+     * 计算用户该课程的总学习时长
+     * @param userCourseRecords
+     * @return
+     */
+    public int computeCourseTime(Collection<UserCourseRecord> userCourseRecords){
+        int time = 0;
+        for(UserCourseRecord userCourseRecord:userCourseRecords){
+            time += userCourseRecord.getUserTime();
+        }
+        return time;
+    }
+
+    /**
+     * 计算用户该课程的总学习天数
+     * @param  record
+     * @return
+     */
+    public int computeCourseDay(UserOrderRecord record){
+        if(record.getUseEndTime() == null){
+            return 0;
+        }
+        int suspend = 0;
+        if(record.getIsSuspend() > 0){
+            if(record.getRestoreTime() == null){
+                suspend = (int)(Tools.day(new Date()).getTime() - Tools.day(record.getSuspendTime()).getTime())/86400000;
+            }else{
+                suspend = (int)(Tools.day(record.getRestoreTime()).getTime() - Tools.day(record.getSuspendTime()).getTime())/86400000;
+            }
+        }
+        if (record.getUseEndTime().before(new Date())){
+            return (int)(Tools.day(record.getUseEndTime()).getTime() - Tools.day(record.getUseStartTime()).getTime())/86400000 - suspend;
+        }else{
+            return (int)(Tools.day(new Date()).getTime() - Tools.day(record.getUseStartTime()).getTime())/86400000 - suspend;
+        }
+//        Date min = null;
+//        Date max = null;
+//        for(UserCourseRecord userCourseRecord:userCourseRecords){
+//            if(min==null || min.after(userCourseRecord.getCreateTime())){
+//                min = userCourseRecord.getCreateTime();
+//            }
+//            if (max == null || max.before(userCourseRecord.getCreateTime())){
+//                max = userCourseRecord.getCreateTime();
+//            }
+//        }
+//        int suspend = 0;
+//        if (min == null) return 0;
+//        return (int)((Tools.day(max).getTime() - Tools.day(min).getTime())/86400000) - suspend;
+    }
+
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseId
+     * @param courseNoList
+     */
+    public void refreshNoResource(User user, Integer courseId, List<CourseNo> courseNoList){
+        if (user != null){
+            if (userCourseService.getCourse(user.getId(), courseId) == null){
+                for(CourseNo courseNo : courseNoList){
+                    courseNo.setResource(courseNo.getTrailResource());
+                }
+            }
+        }else{
+            for(CourseNo courseNo : courseNoList){
+                courseNo.setResource(courseNo.getTrailResource());
+            }
+        }
+    }
+
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseData
+     */
+    public void refreshDataResource(User user, CourseData courseData){
+        // 处理权限
+        if (user != null){
+            if (!userOrderRecordService.hasData(user.getId(), courseData.getId())){
+                courseData.setResource(courseData.getTrailResource());
+            }
+        }else{
+            courseData.setResource(courseData.getTrailResource());
+        }
+    }
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseDataList
+     */
+    public void refreshDataResource(User user, List<CourseData> courseDataList){
+        // 处理权限
+        if (user != null){
+            for(CourseData courseData : courseDataList){
+                if (!userOrderRecordService.hasData(user.getId(), courseData.getId())){
+                    courseData.setResource(courseData.getTrailResource());
+                }
+            }
+        }else{
+            for(CourseData courseData : courseDataList){
+                courseData.setResource(courseData.getTrailResource());
+            }
+        }
+    }
+}

+ 7 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java

@@ -60,6 +60,9 @@ public class MessageExtendService {
                 case WECHAT:
                     sendWechat(user.getWechatOpenidWechat(), category, params);
                     break;
+                case SMS:
+                    sendSms(user.getArea(), user.getMobile(), category, params);
+                    break;
                 default:
                     throw new ParameterException("消息发送方式错误");
             }
@@ -87,6 +90,10 @@ public class MessageExtendService {
         wechatHelp.sendMessage(openId, category, params);
     }
 
+    private void sendSms(String area, String mobile, MessageCategory category, Map<String, String> params){
+
+    }
+
     public void sendCustom(User user, MessageTemplate template, Map<String, String>params){
         params.put("nickname", user.getNickname());
         MessageMethod messageMethod = MessageMethod.ValueOf(template.getMessageMethod());

+ 13 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -116,6 +116,13 @@ public class OrderFlowService {
                     // 累加课时
                     checkout.setId(tmp.getId());
                     checkout.setNumber(tmp.getNumber() + checkout.getNumber());
+                    if (mainCourse.getMaxNumber() < checkout.getNumber()){
+                        checkout.setNumber(mainCourse.getMaxNumber());
+                    }
+                }else{
+                    if (mainCourse.getMinNumber() > checkout.getNumber()){
+                        checkout.setNumber(mainCourse.getMinNumber());
+                    }
                 }
                 checkout.setOriginMoney(mainCourse.getPrice().multiply(BigDecimal.valueOf(checkout.getNumber())));
                 int percent = toolsService.computeVsMoney(checkout);
@@ -272,6 +279,12 @@ public class OrderFlowService {
 
             // 判断是否是1v1课程
             if (mainCourse.getCourseModule().equals(CourseModule.VS.key)){
+                if (mainCourse.getMaxNumber() < checkout.getNumber()){
+                    throw new ParameterException("超过最大购买上限");
+                }
+                if (mainCourse.getMinNumber() > checkout.getNumber()){
+                    throw new ParameterException("超过最小购买下限");
+                }
                 checkout.setOriginMoney(mainCourse.getPrice().multiply(BigDecimal.valueOf(checkout.getNumber())));
                 int percent = toolsService.computeVsMoney(checkout);
                 checkout.setMoney(checkout.getOriginMoney().multiply(BigDecimal.valueOf(percent)).divide(BigDecimal.valueOf(100), BigDecimal.ROUND_HALF_UP));

+ 18 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java

@@ -71,6 +71,24 @@ public class PreviewService extends AbstractService {
     }
 
     /**
+     * 获取用户分组作业列表
+     * @param userId
+     * @param top
+     * @return
+     */
+    public List<UserPreviewPaperRelation> listByRecordId(Integer userId, Collection recordIds, Integer top){
+        List<UserPreviewPaperRelation> relationList = new ArrayList<>();
+        if(recordIds == null || recordIds.size() == 0) return relationList;
+        for(Object id : recordIds){
+            Integer recordId = (Integer)id;
+            List<UserPreviewPaperRelation> tmp = list(1, top, recordId, userId,  null, 0);
+
+            relationList.addAll(tmp);
+        }
+        return relationList;
+    }
+
+    /**
      * 返回用户的预习作业列表
      * @param recordId
      * @param userId

+ 55 - 0
server/gateway-api/src/main/java/com/qxgmat/task/ScheduledTask.java

@@ -322,4 +322,59 @@ public class ScheduledTask {
             courseExtendService.restoreCourse(record.getUserId(), record.getId());
         }
     }
+
+    // 每天判断模考一直没开通、距离开通有效期还剩30天
+    @Scheduled(cron="0 1 0 * * *")
+    public void catExpire() {
+        Date endTime = Tools.today();
+        Date startTime = Tools.addDate(endTime, 30);
+        List<UserOrderRecord> recordList = userOrderRecordService.allSuspendExpire(startTime.toString(), endTime.toString());
+        for(UserOrderRecord record : recordList){
+            courseExtendService.restoreCourse(record.getUserId(), record.getId());
+        }
+    }
+
+    // 每天判断模考已开通,使用有效期还剩10天
+    @Scheduled(cron="0 1 0 * * *")
+    public void catUseExpire() {
+        Date endTime = Tools.today();
+        Date startTime = Tools.addDate(endTime, 30);
+        List<UserOrderRecord> recordList = userOrderRecordService.allSuspendExpire(startTime.toString(), endTime.toString());
+        for(UserOrderRecord record : recordList){
+            courseExtendService.restoreCourse(record.getUserId(), record.getId());
+        }
+    }
+
+    // 每天判断机经一直没开通、距离开通有效期还剩30天。
+    @Scheduled(cron="0 1 0 * * *")
+    public void textbookExpire() {
+        Date endTime = Tools.today();
+        Date startTime = Tools.addDate(endTime, 30);
+        List<UserOrderRecord> recordList = userOrderRecordService.allSuspendExpire(startTime.toString(), endTime.toString());
+        for(UserOrderRecord record : recordList){
+            courseExtendService.restoreCourse(record.getUserId(), record.getId());
+        }
+    }
+
+    // 每天判断课程一直没开通、距离开通有效期还剩30天。
+    @Scheduled(cron="0 1 0 * * *")
+    public void courseExpire() {
+        Date endTime = Tools.today();
+        Date startTime = Tools.addDate(endTime, 30);
+        List<UserOrderRecord> recordList = userOrderRecordService.allSuspendExpire(startTime.toString(), endTime.toString());
+        for(UserOrderRecord record : recordList){
+            courseExtendService.restoreCourse(record.getUserId(), record.getId());
+        }
+    }
+
+    // 每天判断课程已开通,使用有效期还剩10天
+    @Scheduled(cron="0 1 0 * * *")
+    public void courseUseExpire() {
+        Date endTime = Tools.today();
+        Date startTime = Tools.addDate(endTime, 30);
+        List<UserOrderRecord> recordList = userOrderRecordService.allSuspendExpire(startTime.toString(), endTime.toString());
+        for(UserOrderRecord record : recordList){
+            courseExtendService.restoreCourse(record.getUserId(), record.getId());
+        }
+    }
 }