Browse Source

Merge branch 'master' of www.gitinn.com:zaixianjiaoyu/sourcecode

KaysonCui 6 years ago
parent
commit
8b47e09878
100 changed files with 3163 additions and 1222 deletions
  1. 3 1
      front/project/Constant.js
  2. 9 0
      front/project/admin/routes/course/detail/page.js
  3. 2 2
      front/project/admin/routes/interaction/index.js
  4. 23 21
      front/project/admin/routes/setting/struct/page.js
  5. 0 1
      front/project/admin/routes/show/deploy/page.js
  6. 2 2
      front/project/h5/routes/page/login/page.js
  7. 2 2
      front/project/h5/stores/common.js
  8. 123 102
      front/project/www/components/Card/index.js
  9. 7 0
      front/project/www/components/Card/index.less
  10. 4 1
      front/project/www/components/Header/index.js
  11. 4 4
      front/project/www/components/Login/index.js
  12. 16 2
      front/project/www/components/Login/index.less
  13. 17 14
      front/project/www/components/Panel/index.js
  14. 1 1
      front/project/www/local.json
  15. 10 0
      front/project/www/routes/examination/list/index.js
  16. 59 0
      front/project/www/routes/examination/list/index.less
  17. 286 0
      front/project/www/routes/examination/list/page.js
  18. 266 596
      front/project/www/routes/examination/main/page.js
  19. 1 1
      front/project/www/routes/exercise/list/index.js
  20. 18 2
      front/project/www/routes/exercise/list/page.js
  21. 2 0
      front/project/www/routes/exercise/main/index.less
  22. 189 43
      front/project/www/routes/exercise/main/page.js
  23. 1 1
      front/project/www/routes/page/home/page.js
  24. 28 35
      front/project/www/routes/paper/process/base/index.js
  25. 51 10
      front/project/www/routes/paper/process/page.js
  26. 5 5
      front/project/www/routes/paper/question/page.js
  27. 2 1
      front/project/www/routes/preview/index.js
  28. 1 1
      front/project/www/routes/preview/list/index.less
  29. 2 1
      front/project/www/routes/textbook/index.js
  30. 10 0
      front/project/www/routes/textbook/list/index.js
  31. 59 0
      front/project/www/routes/textbook/list/index.less
  32. 286 0
      front/project/www/routes/textbook/list/page.js
  33. 194 186
      front/project/www/static/login.html
  34. 48 4
      front/project/www/stores/course.js
  35. 41 2
      front/project/www/stores/my.js
  36. 32 4
      front/project/www/stores/order.js
  37. 11 12
      front/project/www/stores/question.js
  38. 14 0
      front/project/www/stores/textbook.js
  39. 20 0
      front/src/services/Tools.js
  40. 5 1
      server/data/src/main/java/com/qxgmat/data/constants/enums/ServiceKey.java
  41. 3 2
      server/data/src/main/java/com/qxgmat/data/constants/enums/module/PaperOrigin.java
  42. 17 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/module/QuestionNoModule.java
  43. 18 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/module/VideoCourseType.java
  44. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/Course.java
  45. 70 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/User.java
  46. 12 12
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserNoteCourse.java
  47. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserPaper.java
  48. 4 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseMapper.xml
  49. 5 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMapper.xml
  50. 2 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserNoteCourseMapper.xml
  51. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserPaperMapper.xml
  52. 1 0
      server/data/src/main/java/com/qxgmat/data/relation/ExaminationPaperRelationMapper.java
  53. 4 0
      server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java
  54. 15 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/ExaminationPaperRelationMapper.xml
  55. 12 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/ExercisePaperRelationMapper.xml
  56. 43 13
      server/data/src/main/java/com/qxgmat/data/relation/mapping/PreviewAssignRelationMapper.xml
  57. 12 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/TextbookPaperRelationMapper.xml
  58. 4 4
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRecordRelationMapper.xml
  59. 25 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml
  60. 10 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java
  61. 19 4
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  62. 80 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  63. 71 12
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  64. 11 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java
  65. 2 1
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/AdDto.java
  66. 22 3
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ExaminationStructDto.java
  67. 30 3
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ExerciseStructDto.java
  68. 2 1
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ManagerRoleDto.java
  69. 2 1
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/UserAskCourseDto.java
  70. 2 1
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/UserAskQuestionDto.java
  71. 40 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java
  72. 69 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseTeacherExtendDto.java
  73. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/UserPaperBaseExtendDto.java
  74. 47 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskCourseDto.java
  75. 1 1
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskDto.java
  76. 37 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserNoteCourseDto.java
  77. 37 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserTextbookFeedbackDto.java
  78. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  79. 9 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseDetailDto.java
  80. 50 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseProgressDto.java
  81. 30 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserExaminationGroupDto.java
  82. 30 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserExaminationPaperDto.java
  83. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserQuestionDetailDto.java
  84. 72 36
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserTextbookGroupDto.java
  85. 26 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserNoteCourseService.java
  86. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/UserNoteQuestionService.java
  87. 21 4
      server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java
  88. 1 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java
  89. 1 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserServiceService.java
  90. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/annotation/InitPaper.java
  91. 42 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java
  92. 13 3
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExaminationService.java
  93. 28 5
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  94. 38 11
      server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java
  95. 60 6
      server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java
  96. 7 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CoursePackageService.java
  97. 16 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java
  98. 3 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/TextbookLibraryService.java
  99. 29 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseService.java
  100. 0 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java

+ 3 - 1
front/project/Constant.js

@@ -1,8 +1,10 @@
 export const UserUrl = 'http://www.baidu.com';
 
+export const WechatUserAppId = 'wx324965bb6800f9b9';
+
 export const H5Url = 'http://127.0.0.1:3000';
 
-export const WechatAppId = 'wxbee75af2ece94ed7';
+export const WechatH5AppId = 'wxbee75af2ece94ed7';
 
 export const QuestionDifficult = [{ label: 'easy', value: 'easy' }, { label: 'medium', value: 'medium' }, { label: 'hard', value: 'hard' }];
 

+ 9 - 0
front/project/admin/routes/course/detail/page.js

@@ -468,6 +468,15 @@ export default class extends Page {
             <Input placeholder='请输入课程名称' />,
           )}
         </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问权限延迟'>
+          {getFieldDecorator('askExtendDays', {
+            rules: [
+              { required: true, message: '请输入扩展天数' },
+            ],
+          })(
+            <InputNumber placeholder='天' />,
+          )}
+        </Form.Item>
       </Form>
     </Block>;
   }

+ 2 - 2
front/project/admin/routes/interaction/index.js

@@ -1,8 +1,8 @@
 
 import askQuestion from './askQuestion';
 import askQuestionDetail from './askQuestionDetail';
-import faqConsult from './faqConsult';
+import faq from './faq';
 import comment from './comment';
 import feedback from './feedback';
 
-export default [askQuestion, askQuestionDetail, faqConsult, comment, feedback];
+export default [askQuestion, askQuestionDetail, faq, comment, feedback];

+ 23 - 21
front/project/admin/routes/setting/struct/page.js

@@ -53,13 +53,11 @@ export default class extends Page {
       name: '英文名称',
       type: 'input',
       placeholder: '请输入英文名称',
-      required: true,
     }, {
       key: 'description',
       name: '描述',
       type: 'textarea',
       placeholder: '请输入描述',
-      required: true,
     }, {
       key: 'isSentence',
       type: 'hidden',
@@ -81,13 +79,11 @@ export default class extends Page {
       name: '英文名称',
       type: 'input',
       placeholder: '请输入英文名称',
-      required: true,
     }, {
       key: 'description',
       name: '描述',
       type: 'textarea',
       placeholder: '请输入描述',
-      required: true,
     }, {
       key: 'isSentence',
       type: 'hidden',
@@ -134,13 +130,11 @@ export default class extends Page {
       name: '英文名称',
       type: 'input',
       placeholder: '请输入英文名称',
-      required: true,
     }, {
       key: 'description',
       name: '描述',
       type: 'textarea',
       placeholder: '请输入描述',
-      required: true,
     }, {
       key: 'isAdapt',
       type: 'hidden',
@@ -160,13 +154,11 @@ export default class extends Page {
       name: '英文名称',
       type: 'input',
       placeholder: '请输入英文名称',
-      required: true,
     }, {
       key: 'description',
       name: '描述',
       type: 'textarea',
       placeholder: '请输入描述',
-      required: true,
     }, {
       key: 'isAdapt',
       type: 'hidden',
@@ -220,14 +212,14 @@ export default class extends Page {
         examinationMap: getMap(list, 'id'),
         examinationStruct: formatTreeData(list.map(row => {
           if (row.level !== 2) return row;
-          // row = Object.assign({}, row);
-          // row.title = <div className='node'>{row.title}<Button className='after-node' size='small' type={row.questionStatus > 0 ? 'primary' : 'ghost'} onClick={(e) => {
-          //   e.preventDefault();
-          //   row.payStatus = row.payStatus > 0 ? 0 : 1;
-          //   Examination.editStruct(row).then(() => {
-          //     this.refresh();
-          //   });
-          // }}>{row.payStatus > 0 ? [<Icon type="pay-circle" />, <span>付费</span>] : [<Icon type="eye" />, <span>免费</span>]}</Button></div>;
+          row = Object.assign({}, row);
+          row.title = <div className='node'>{row.title}<Button className='after-node' size='small' type={row.questionStatus > 0 ? 'primary' : 'ghost'} onClick={(e) => {
+            e.preventDefault();
+            row.questionStatus = row.questionStatus > 0 ? 0 : 1;
+            Examination.editStruct(row).then(() => {
+              this.refresh();
+            });
+          }}>{row.questionStatus > 0 ? [<Icon type='pause' />, <span>提问中</span>] : [<Icon type="caret-right" />, <span>提问关闭</span>]}</Button></div>;
           return row;
         }), 'id', 'title', 'parentId'),
       });
@@ -249,7 +241,11 @@ export default class extends Page {
         asyncSMessage('不允许添加该节点的子节点', 'warn');
         return;
       }
-      dep = {};
+      dep = {
+        isSentence: node.isSentence,
+        isCourse: node.isCourse,
+        isData: node.isData,
+      };
     } else {
       itemList = this.examinationItemList;
       if (selectedKeys.length > 0) {
@@ -264,8 +260,9 @@ export default class extends Page {
       };
     }
 
-    asyncForm('新增', itemList, Object.assign({ parentId: `${node.id}`, parentName: node.title }, dep), data => {
+    asyncForm('新增', itemList, Object.assign({ parentId: node.id, parentName: node.title }, dep), data => {
       let handler;
+      data.parentId = Number(data.parentId);
       if (tab === 'exercise') {
         handler = Exercise.addStruct(data);
       } else {
@@ -293,6 +290,9 @@ export default class extends Page {
         return;
       }
       dep = {
+        isSentence: node.isSentence,
+        isCourse: node.isCourse,
+        isData: node.isData,
         parentName: this.state.exerciseMap[node.parentId].title,
       };
     } else {
@@ -310,8 +310,9 @@ export default class extends Page {
       };
     }
 
-    asyncForm('新增', itemList, Object.assign({ parentId: `${node.parentId}`, parentName: node.title }, dep), data => {
+    asyncForm('新增', itemList, Object.assign({ parentId: node.parentId, parentName: node.title }, dep), data => {
       let handler;
+      data.parentId = Number(data.parentId);
       if (tab === 'exercise') {
         handler = Exercise.addStruct(data);
       } else {
@@ -331,7 +332,7 @@ export default class extends Page {
     if (tab === 'exercise') {
       itemList = this.exerciseItemList;
       ([node] = exerciseList.filter(row => row.id === Number(selectedKeys[0])).map(row => {
-        row.parentId = `${row.parentId}`;
+        // row.parentId = `${row.parentId}`;
         return row;
       }));
       if (node.level <= 2) {
@@ -345,7 +346,7 @@ export default class extends Page {
     } else {
       itemList = this.examinationItemList;
       ([node] = examinationList.filter(row => row.id === Number(selectedKeys[0])).map(row => {
-        row.parentId = `${row.parentId}`;
+        // row.parentId = `${row.parentId}`;
         return row;
       }));
       if (node.level <= 1) {
@@ -361,6 +362,7 @@ export default class extends Page {
 
     asyncForm('编辑', itemList, node, data => {
       let handler;
+      data.parentId = Number(data.parentId);
       if (tab === 'exercise') {
         handler = Exercise.editStruct(data);
       } else {

+ 0 - 1
front/project/admin/routes/show/deploy/page.js

@@ -11,7 +11,6 @@ import { System } from '../../../stores/system';
 export default class extends Page {
   constructor(props) {
     super(props);
-    this.state.tab = 'qx_cat';
     this.vipList = ServiceParamMap.vip;
   }
 

+ 2 - 2
front/project/h5/routes/page/login/page.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { WechatAppId, H5Url } from '../../../../Constant';
+import { WechatH5AppId, H5Url } from '../../../../Constant';
 import { User } from '../../../stores/user';
 
 export default class extends Page {
@@ -16,7 +16,7 @@ export default class extends Page {
         }
       });
     } else {
-      const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${WechatAppId}&redirect_uri=${encodeURIComponent(
+      const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${WechatH5AppId}&redirect_uri=${encodeURIComponent(
         `${H5Url}/login`,
       )}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`;
       window.location.href = url;

+ 2 - 2
front/project/h5/stores/common.js

@@ -1,6 +1,6 @@
 import BaseStore from '@src/stores/base';
 import { generateUUID } from '@src/services/Tools';
-import { WechatAppId } from '../../Constant';
+import { WechatH5AppId } from '../../Constant';
 
 export default class CommonStore extends BaseStore {
   /**
@@ -40,7 +40,7 @@ export default class CommonStore extends BaseStore {
       a.push('checkJsApi');
       wx.config({
         debug: false, // 是否打开调试模式,调用的api会被alert出来,在pc端也能看到log信息
-        appid: WechatAppId, // 必填,微信公众号的唯一标识
+        appid: WechatH5AppId, // 必填,微信公众号的唯一标识
         timestamp: time, // 必填,生成签名的时间戳
         nonceStr: nonce, // 必填,生成签名的随机串
         signature: sha1(p), // 必填,用于验证的签名

+ 123 - 102
front/project/www/components/Card/index.js

@@ -3,6 +3,7 @@ import './index.less';
 import { Link } from 'react-router-dom';
 import { Checkbox } from 'antd';
 import Assets from '@src/components/Assets';
+import { formatDate } from '@src/services/Tools';
 import Module from '../Module';
 import Progress from '../Progress';
 import IconButton from '../IconButton';
@@ -52,42 +53,41 @@ export default class Card extends Component {
             <div>好棒!</div>
             <div>近期的作业都完成啦</div>
           </div>
-        ) : (
-          <div className="list">
-            {process.previews.map(item => {
-              return (
-                <div className="item">
-                  <div className="top">
-                    <div className="date">{item.time}</div>
-                    <div className="action">
-                      {!item.repport.id && (
-                        <IconButton
-                          type="start"
-                          tip="Start"
-                          onClick={() => previewAction && previewAction('start', item)}
-                        />
-                      )}
-                      {item.repport.id && (
-                        <IconButton
-                          type="continue"
-                          onClick={() => previewAction && previewAction('continue', item)}
-                          tip="Continue"
-                        />
-                      )}
-                      {item.repport.id && (
-                        <IconButton
-                          type="restart"
-                          onClick={() => previewAction && previewAction('restart', item)}
-                          tip="Restart"
-                        />
-                      )}
-                    </div>
+        ) : (<div className="list">
+          {process.previews.map(item => {
+            return (
+              <div className="item">
+                <div className="top">
+                  <div className="date">{item.time}</div>
+                  <div className="action">
+                    {!item.repport.id && (
+                      <IconButton
+                        type="start"
+                        tip="Start"
+                        onClick={() => previewAction && previewAction('start', item)}
+                      />
+                    )}
+                    {item.repport.id && (
+                      <IconButton
+                        type="continue"
+                        onClick={() => previewAction && previewAction('continue', item)}
+                        tip="Continue"
+                      />
+                    )}
+                    {item.repport.id && (
+                      <IconButton
+                        type="restart"
+                        onClick={() => previewAction && previewAction('restart', item)}
+                        tip="Restart"
+                      />
+                    )}
                   </div>
-                  <Progress progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0} />
                 </div>
-              );
-            })}
-          </div>
+                <Progress progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0} />
+              </div>
+            );
+          })}
+        </div>
         )}
       </div>
     );
@@ -124,11 +124,16 @@ export default class Card extends Component {
 
 export class Card1 extends Component {
   getEndBody() {
+    const { data, onPreview } = this.props;
+    const { useStartTime, useEndTime } = data;
     return (
       <div className="body">
         <div className="text">
           <div className="t-1">课程已经结束啦</div>
         </div>
+        <div className="bottom">
+          有效期: {useStartTime && formatDate(useStartTime, 'YYYY-MM-DD')}至{useEndTime && formatDate(useEndTime, 'YYYY-MM-DD')} <a onClick={() => onPreview && onPreview()}>全部作业></a>
+        </div>
       </div>
     );
   }
@@ -145,36 +150,55 @@ export class Card1 extends Component {
   }
 
   getOpenBody() {
-    const { data } = this.props;
-    const { qrCode } = data;
-    return !qrCode ? (
-      <div className="body">
-        <div className="text">
-          <Checkbox />
-          <span>
-            我已阅读并同意<Link to="">《千行课程协议》</Link>
-          </span>
-        </div>
-        <div className="btn">
-          <Button size="lager" radius>
-            开通作业
-          </Button>
-        </div>
-      </div>
-    ) : (
-      <div className="body">
-        <div className="t-1">请尽快与老师预约上课时间</div>
-        <div className="t-2">请于 2019-07-25 前开通课程</div>
-        <div className="qr-code">
-          <Assets name="qrcode" />
-        </div>
-      </div>
-    );
+    const { checked } = this.state;
+    const { data, onOpen } = this.props;
+    const { teacher, endTime, courseModule } = data;
+    switch (courseModule) {
+      case 'video':
+        return <div className="body">
+          <div className="text">
+            <span>
+              请于{endTime && formatDate(endTime, 'YYYY-MM-DD')}前开通课程
+            </span>
+          </div>
+          <div className="btn">
+            <Button size="lager" radius onClick={() => onOpen && onOpen()}>
+              开通作业
+            </Button>
+          </div>
+        </div>;
+      case 'online':
+        return <div className="body">
+          <div className="text">
+            <Checkbox checked={checked} onChange={() => {
+              this.setState({ checked: !checked });
+            }} />
+            <span>
+              我已阅读并同意<Link to="">《千行课程协议》</Link>
+            </span>
+          </div>
+          <div className="btn">
+            <Button size="lager" radius onClick={() => onOpen && onOpen()}>
+              开通作业
+              </Button>
+          </div>
+        </div>;
+      case 'vs':
+        return <div className="body">
+          <div className="t-1">请尽快与老师预约上课时间</div>
+          <div className="t-2">请于 {endTime && formatDate(endTime, 'YYYY-MM-DD')} 前开通课程</div>
+          <div className="qr-code">
+            {teacher && <Assets name="qrcode" src={teacher.qr} />}
+          </div>
+        </div>;
+      default:
+        return <div />;
+    }
   }
 
   getIngBody() {
-    const { data, previewAction } = this.props;
-    const { list = [] } = data;
+    const { data, list = [], previewAction, onPreview } = this.props;
+    const { useStartTime, useEndTime } = data;
     return (
       <div className="body">
         {list.length > 0 && <div className="title">近期待完成</div>}
@@ -183,55 +207,53 @@ export class Card1 extends Component {
             <div className="t-1">好棒!</div>
             <div className="t-2">近期的作业都完成啦</div>
           </div>
-        ) : (
-          <div className="list">
-            {list.map(item => {
-              return (
-                <div className="item">
-                  <div className="top">
-                    <div className="title">{item.title}</div>
-                  </div>
-                  <div className="detail">
-                    <Progress size="small" progress={item.progress} />
-                    <div className="action">
-                      {item.status === 'start' && (
-                        <IconButton
-                          type="start"
-                          tip="Start"
-                          onClick={() => previewAction && previewAction('start', item)}
-                        />
-                      )}
-                      {item.status === 'continue' && (
-                        <IconButton
-                          type="continue"
-                          onClick={() => previewAction && previewAction('continue', item)}
-                          tip="Continue"
-                        />
-                      )}
-                      {item.status === 'restart' && (
-                        <IconButton
-                          type="restart"
-                          onClick={() => previewAction && previewAction('restart', item)}
-                          tip="Restart"
-                        />
-                      )}
-                    </div>
+        ) : (<div className="list">
+          {list.map(item => {
+            return (
+              <div className="item">
+                <div className="top">
+                  <div className="title">{item.title}</div>
+                </div>
+                <div className="detail">
+                  <Progress size="small" progress={item.progress} />
+                  <div className="action">
+                    {item.progress === 0 && (
+                      <IconButton
+                        type="start"
+                        tip="Start"
+                        onClick={() => previewAction && previewAction('start', item)}
+                      />
+                    )}
+                    {item.progress > 0 && (
+                      <IconButton
+                        type="continue"
+                        onClick={() => previewAction && previewAction('continue', item)}
+                        tip="Continue"
+                      />
+                    )}
+                    {item.progress > 0 && (
+                      <IconButton
+                        type="restart"
+                        onClick={() => previewAction && previewAction('restart', item)}
+                        tip="Restart"
+                      />
+                    )}
                   </div>
                 </div>
-              );
-            })}
-          </div>
+              </div>
+            );
+          })}
+        </div>
         )}
         <div className="bottom">
-          有效期: 2019-08-20至2020-01-01 <Link to="">全部作业></Link>
+          有效期: {useStartTime && formatDate(useStartTime, 'YYYY-MM-DD')}至{useEndTime && formatDate(useEndTime, 'YYYY-MM-DD')} <a onClick={() => onPreview && onPreview()}>全部作业></a>
         </div>
       </div>
     );
   }
 
   getBody() {
-    const { data } = this.props;
-    const { status } = data;
+    const { status } = this.props;
     if (status === 'end') return this.getEndBody();
     if (status === 'stop') return this.getStopBody();
     if (status === 'open') return this.getOpenBody();
@@ -240,8 +262,7 @@ export class Card1 extends Component {
   }
 
   render() {
-    const { style, data, title, tag } = this.props;
-    const { status } = data;
+    const { style, title, tag, status } = this.props;
     return (
       <Module style={style} className={`card1 ${status}`}>
         <div className="header">

+ 7 - 0
front/project/www/components/Card/index.less

@@ -295,6 +295,13 @@
       color: #5E677B;
     }
   }
+
+  .bottom {
+    color: #8897A8;
+    font-size: 12px;
+    position: absolute;
+    bottom: 20px;
+  }
 }
 
 .module.card1.stop {

+ 4 - 1
front/project/www/components/Header/index.js

@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
 import Assets from '@src/components/Assets';
 import Button from '../Button';
 import './index.less';
+import { User } from '../../stores/user';
 
 function Header(props) {
   const { tabs = [], active } = props;
@@ -24,7 +25,9 @@ function Header(props) {
           </div>
         </div>
         <div className="right">
-          <Button>登录</Button>
+          <Button onClick={() => {
+            User.needLogin();
+          }}>登录</Button>
         </div>
       </div>
     </div>

+ 4 - 4
front/project/www/components/Login/index.js

@@ -8,7 +8,7 @@ import { Button as GButton } from '../Button';
 import { User } from '../../stores/user';
 import { My } from '../../stores/my';
 import { Common } from '../../stores/common';
-import { MobileArea } from '../../../Constant';
+import { MobileArea, WechatUserAppId } from '../../../Constant';
 
 const LOGIN_PHONE = 'LOGIN_PHONE';
 const LOGIN_WX = 'LOGIN_WX';
@@ -19,7 +19,7 @@ const BIND_WX_ERROR = 'BIND_WX_ERROR';
 export default class Login extends Component {
   constructor(props) {
     super(props);
-    this.state = { type: LOGIN_WX };
+    this.state = { type: LOGIN_WX, data: { area: MobileArea[0].value } };
     window.addEventListener(
       'message',
       event => {
@@ -261,7 +261,7 @@ export default class Login extends Component {
       <div className="body">
         <div className="title">微信扫码登录</div>
         <div className="qr-code">
-          <iframe frameBorder="0" src="/login.html" width="300" height="300" />
+          <iframe frameBorder="0" src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`} width="300" height="300" />
           <div className="text">请使用微信扫描二维码登录</div>
         </div>
         <Tooltip overlayClassName="gray" placement="left" title="手机号登录">
@@ -341,7 +341,7 @@ export default class Login extends Component {
           手机号注册成功!为更好的使用服务,建议您绑定微信号。
         </div>
         <div className="qr-code">
-          <Assets name="qrcode" />
+          <iframe frameBorder="0" src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`} width="300" height="300" />
           <div className="text">请使用微信扫描二维码登录</div>
           <div
             className="jump"

+ 16 - 2
front/project/www/components/Login/index.less

@@ -97,6 +97,7 @@
       }
 
       .g-input-left {
+        position: relative;
         cursor: pointer;
 
         .g-input-left-select {
@@ -136,6 +137,19 @@
       height: 25px;
       line-height: 25px;
     }
+
+    .select-list {
+      top: 36px;
+      width: 61px;
+      border: 1px solid #EAEDF2;
+      max-height: 200px;
+      overflow-y: auto;
+      list-style: none;
+      margin: 0;
+      padding: 5px 10px;
+      position: absolute;
+      background: #fff;
+    }
   }
 }
 
@@ -149,8 +163,8 @@
 
 .LOGIN_WX .body {
   .qr-code {
-    padding-top: 60px;
-    padding-bottom: 120px;
+    padding-top: 20px;
+    padding-bottom: 10px;
   }
 }
 

+ 17 - 14
front/project/www/components/Panel/index.js

@@ -2,6 +2,7 @@ import React from 'react';
 import { Tooltip } from 'antd';
 import './index.less';
 import Assets from '@src/components/Assets';
+import { formatDate } from '@src/services/Tools';
 import Module from '../Module';
 import ProgressButton from '../ProgressButton';
 import Button from '../Button';
@@ -62,7 +63,7 @@ export default function Panel(props) {
   );
 }
 export function WaitPanel(props) {
-  const { style, message, data = {}, col = 3, title, onClick } = props;
+  const { style, message, data = {}, col = 3, title, onClick, onOpen } = props;
   return (
     <Module style={style} className="panel wait-panel">
       <div className="header">
@@ -113,7 +114,7 @@ export function WaitPanel(props) {
           })}
         </div>
 
-        <Button size="lager" radius>
+        <Button size="lager" radius onClick={() => onOpen && onOpen()}>
           立即开通
         </Button>
       </div>
@@ -121,7 +122,7 @@ export function WaitPanel(props) {
   );
 }
 export function BuyPanel(props) {
-  const { style, message, title } = props;
+  const { style, message, title, onBuy } = props;
   return (
     <Module style={style} className="panel buy-panel">
       <div className="header">
@@ -135,7 +136,7 @@ export function BuyPanel(props) {
       <div className="body">
         <Assets name="banner" />
         <div className="text">您还未购买本月机经</div>
-        <Button radius size="small" width={80}>
+        <Button radius size="small" width={80} onClick={() => onBuy && onBuy()}>
           立即购买
         </Button>
       </div>
@@ -144,14 +145,15 @@ export function BuyPanel(props) {
 }
 
 export function SmallPanel(props) {
-  const { style, title, lock, data = {} } = props;
+  const { style, title, lock, data = {}, onClick } = props;
+  const { useEndTime, needService } = data;
   return (
     <Module style={style} className="panel small-panel">
       <div className="header">
         <span>{title}</span>
         {lock && <Assets name="lock" />}
       </div>
-      <div className="body">
+      <div className="body" onClick={() => onClick && onClick()}>
         <div className="chart-info">
           <div className="chart" />
           <div className="info">
@@ -166,7 +168,7 @@ export function SmallPanel(props) {
                 </div>
               );
             })}
-            <div className="date">有效期至:2019-11-13</div>
+            {needService && <div className="date">有效期至:{useEndTime && formatDate(useEndTime, 'YYYY-MM-DD')}</div>}
           </div>
         </div>
       </div>
@@ -175,17 +177,18 @@ export function SmallPanel(props) {
 }
 
 export function SmallWaitPanel(props) {
-  const { style, title } = props;
+  const { style, title, lock, data, onOpen } = props;
+  const { endTime } = data;
   return (
     <Module style={style} className="panel small-wait-panel">
       <div className="header">
         <span>{title}</span>
-        <Assets name="lock" />
+        {lock && <Assets name="lock" />}
       </div>
       <div className="body">
-        <div className="title">请于20190-07-05前开通</div>
+        <div className="title">请于{endTime && formatDate(endTime, 'YYYY-MM-DD')}前开通</div>
         <div className="btn">
-          <Button size="lager" width={120} radius>
+          <Button size="lager" width={120} radius onClick={() => onOpen && onOpen()}>
             立即开通
           </Button>
         </div>
@@ -195,16 +198,16 @@ export function SmallWaitPanel(props) {
 }
 
 export function SmallBuyPanel(props) {
-  const { style, title } = props;
+  const { style, title, lock, onBuy } = props;
   return (
     <Module style={style} className="panel small-buy-panel">
       <div className="header">
         <span>{title}</span>
-        <Assets name="lock" />
+        {lock && <Assets name="lock" />}
       </div>
       <div className="body">
         <Assets name="banner_1" />
-        <Button radius size="small" width={80}>
+        <Button radius size="small" width={80} onClick={() => onBuy && onBuy()}>
           立即购买
         </Button>
       </div>

+ 1 - 1
front/project/www/local.json

@@ -7,7 +7,7 @@
     ],
     "proxy": [
       {
-        "target": "http://qianxing.nuliji.com",
+        "target": "http://127.0.0.1:8888",
         "from": "/api",
         "to": "/api"
       }

+ 10 - 0
front/project/www/routes/examination/list/index.js

@@ -0,0 +1,10 @@
+export default {
+  path: '/examination/list/:id',
+  key: 'examination-list',
+  title: '模考列表',
+  needLogin: false,
+  tab: 'examination',
+  component() {
+    return import('./page');
+  },
+};

+ 59 - 0
front/project/www/routes/examination/list/index.less

@@ -0,0 +1,59 @@
+@charset "utf-8";
+
+#examination-list {
+  .code-module {
+    padding: 80px 250px;
+    text-align: center;
+
+    .title {
+      font-size: 18px;
+      margin-bottom: 24px;
+    }
+
+    .input-block {
+      margin-bottom: 24px;
+
+      .input {
+        width: 350px;
+
+        input {
+          border-top-left-radius: 22px;
+          border-bottom-left-radius: 22px;
+        }
+      }
+
+      .button {
+        width: 150px;
+        border-top-right-radius: 22px;
+        border-bottom-right-radius: 22px;
+      }
+    }
+
+    .tip {
+      .left {
+        float: left;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+
+  .work-body {
+    .work-nav {
+      margin-bottom: 20px;
+
+      .left {
+        display: inline-block;
+        padding-left: 5px;
+        font-size: 16px;
+        font-weight: 600;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+}

+ 286 - 0
front/project/www/routes/examination/list/page.js

@@ -0,0 +1,286 @@
+import React from 'react';
+import './index.less';
+import Page from '@src/containers/Page';
+import { asyncConfirm } from '@src/services/AsyncTools';
+import { formatPercent, formatSeconds, formatDate } from '@src/services/Tools';
+import Tabs from '../../../components/Tabs';
+import Module from '../../../components/Module';
+import ListTable from '../../../components/ListTable';
+import ProgressText from '../../../components/ProgressText';
+import IconButton from '../../../components/IconButton';
+import { Main } from '../../../stores/main';
+import { Question } from '../../../stores/question';
+import { QuestionDifficult } from '../../../../Constant';
+
+const LOGIC_NO = 'no';
+const LOGIC_PLACE = 'place';
+const LOGIC_DIFFICULT = 'difficult';
+const LOGIC_ERROR = 'error';
+
+export default class extends Page {
+  initState() {
+    this.columns = [
+      {
+        title: '练习册',
+        width: 250,
+        align: 'left',
+        render: (record) => {
+          let progress = 0;
+          if (record.report) {
+            progress = formatPercent(record.report.userNumber, record.report.questionNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16">{record.title}</div>
+              <div>
+                <ProgressText progress={progress} size="small" />
+              </div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '正确率',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let correct = '--';
+          if (record.report) {
+            correct = formatPercent(record.report.userCorrect, record.report.userNumber, false);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{correct}</div>
+              <div className="f-s-12">全站{formatPercent(record.stat.totalCorrect, record.stat.totalNumber, false)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '全站用时',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let time = '--';
+          if (record.paper) {
+            time = formatSeconds(record.paper.report.userTime / record.paper.report.userNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{time}</div>
+              <div className="f-s-12">全站{formatSeconds(record.stat.totalTime / record.stat.totalNumber)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '最近做题',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          if (!record.report) return null;
+          return (
+            <div className="table-row">
+              <div>{formatDate(record.report.updateTime, 'YYYY-MM-DD')}</div>
+              <div>{formatDate(record.report.updateTime, 'HH:mm')}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: 180,
+        align: 'left',
+        render: (record) => {
+          return (
+            <div className="table-row p-t-1">
+              {!record.report && <IconButton type="start" tip="Start" onClick={() => {
+                Question.startLink('exercise', record);
+              }} />}
+              {(record.report && !record.report.isFinish) && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
+                Question.continueLink('exercise', record);
+              }} />}
+              <IconButton type="restart" tip="Restart" onClick={() => {
+                this.restart(record);
+              }} />
+            </div>
+          );
+        },
+      },
+      {
+        title: '报告',
+        width: 30,
+        align: 'right',
+        render: (record) => {
+          if (!record.report || !record.report.isFinish) return null;
+          return (
+            <div className="table-row p-t-1">
+              <IconButton type="report" tip="Report" onClick={() => {
+                Question.reportLink(record);
+              }} />
+            </div>
+          );
+        },
+      },
+    ];
+    this.placeList = [];
+    this.inited = false;
+    return {
+      logic: LOGIC_NO,
+      logicExtend: '',
+      logics: [{
+        key: LOGIC_NO,
+        title: '按顺序练习',
+      }, {
+        key: LOGIC_PLACE,
+        title: '按考点练习',
+      }, {
+        key: LOGIC_DIFFICULT,
+        title: '按难度练习',
+      }, {
+        key: LOGIC_ERROR,
+        title: '按易错度练习',
+      }],
+    };
+  }
+
+  init() {
+    const { id } = this.params;
+    Main.getExerciseParent(id).then(result => {
+      const navs = result;
+      this.inited = true;
+      this.setState({ navs });
+    });
+  }
+
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    this.setState(data);
+    this.refreshData();
+  }
+
+  refreshData(newLogic) {
+    const { logic } = this.state;
+    let handler = null;
+    switch (newLogic || logic) {
+      case LOGIC_PLACE:
+        handler = this.refreshPlace();
+        break;
+      case LOGIC_DIFFICULT:
+        handler = this.refreshDifficult();
+        break;
+      default:
+        handler = Promise.resolve();
+    }
+    handler.then(() => {
+      this.refreshExercise();
+    });
+  }
+
+  refreshPlace() {
+    const { id } = this.params;
+    let handler;
+    if (this.placeList.length > 0) {
+      this.setState({ logicExtends: this.placeList });
+      handler = Promise.resolve();
+    } else {
+      handler = Question.getExercisePlace(id).then(result => {
+        this.placeList = result.map(row => {
+          return {
+            name: row,
+            key: row,
+          };
+        });
+        this.setState({ logicExtends: this.placeList });
+      });
+    }
+    return handler.then(() => {
+      let { logicExtend } = this.state;
+      if (logicExtend === '') {
+        logicExtend = this.placeList[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshDifficult() {
+    let { logicExtend } = this.state;
+    this.setState({
+      logicExtends: QuestionDifficult.map(difficult => {
+        difficult.name = difficult.label;
+        difficult.key = difficult.value;
+        return difficult;
+      }),
+    });
+    return Promise.resolve().then(() => {
+      if (logicExtend === '') {
+        logicExtend = QuestionDifficult[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshExercise() {
+    const { logic, logicExtend } = this.state;
+    Question.getExerciseList(Object.assign({ structId: this.params.id, logic, logicExtend }, this.state.search))
+      .then((result) => {
+        this.setState({ list: result.list, total: result.total });
+      });
+  }
+
+  onChangeTab(key, value) {
+    const { logic } = this.state;
+    const data = {};
+    if (key === 'logicExtend') {
+      data.logic = logic;
+      data.logicExtend = value;
+    } else {
+      data.logic = value;
+    }
+    // this.refreshData(tab);
+    this.refreshQuery(data);
+  }
+
+  restart(item) {
+    asyncConfirm('提示', '是否重置', () => {
+      Question.restart(item.paper.id).then(() => {
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    return (
+      <div>
+        <div className="content">
+          <Module className="m-t-2">
+            <Tabs
+              active={logic}
+              border
+              width="180px"
+              space="0"
+              tabs={logics}
+              onChange={(key) => {
+                this.onChangeTab('logic', key);
+              }}
+            />
+            {logicExtends.length > 0 && <Tabs
+              active={logicExtend}
+              type="text"
+              tabs={logicExtends}
+              onChange={(key) => {
+                this.onChangeTab('logicExtend', key);
+              }}
+            />}
+          </Module>
+
+          <ListTable
+            data={list}
+            columns={this.columns}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 266 - 596
front/project/www/routes/examination/main/page.js

@@ -1,463 +1,293 @@
 import React from 'react';
 import './index.less';
-import { Link } from 'react-router-dom';
-import { Tooltip } from 'antd';
 import Page from '@src/containers/Page';
-import { asyncConfirm } from '@src/services/AsyncTools';
-import { formatTreeData, getMap } from '@src/services/Tools';
-import Continue from '../../../components/Continue';
-import Step from '../../../components/Step';
-import List from '../../../components/List';
+import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
+import { formatTreeData, formatSeconds, formatPercent, formatDate } from '@src/services/Tools';
+import Panel, { WaitPanel, BuyPanel, SmallPanel, SmallWaitPanel, SmallBuyPanel } from '../../../components/Panel';
 import Tabs from '../../../components/Tabs';
 import Module from '../../../components/Module';
-import Input from '../../../components/Input';
-import Button from '../../../components/Button';
 import Division from '../../../components/Division';
-import Card from '../../../components/Card';
-import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
 import { Main } from '../../../stores/main';
-import { My } from '../../../stores/my';
-import { Sentence } from '../../../stores/sentence';
+// import { My } from '../../../stores/my';
 import { Question } from '../../../stores/question';
-import { Course } from '../../../stores/course';
-import { User } from '../../../stores/user';
+import { Textbook } from '../../../stores/textbook';
+// import { User } from '../../../stores/user';
+// import { CourseModuleShow, CourseModule } from '../../../../Constant';
+import { Order } from '../../../stores/order';
 
-const SENTENCE = 'sentence';
-const PREVIEW = 'preview';
-const PREVIEW_CLASS = 'PREVIEW_CLASS';
-const PREVIEW_LIST = 'PREVIEW_LIST';
-
-const exerciseColumns = [
-  {
-    title: '练习册',
-    width: 250,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16">{item.title}</div>
-          <div>
-            <ProgressText
-              progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0}
-              size="small"
-            />
-          </div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '正确率',
-    width: 150,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">{item.stat.totalCorrect / item.stat.totalNumber}</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '全站用时',
-    width: 150,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">全站{item.stat.totalTime / item.stat.totalNumber}s</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '最近做题',
-    width: 150,
-    align: 'left',
-    render: () => {
-      return (
-        <div className="table-row">
-          <div>2019-04-28</div>
-          <div>07:30</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '操作',
-    width: 180,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row p-t-1">
-          {!item.repport.id && (
-            <IconButton type="start" tip="Start" onClick={() => this.previewAction('start', item)} />
-          )}
-          {item.repport.id && (
-            <IconButton
-              className="m-r-2"
-              type="continue"
-              tip="Continue"
-              onClick={() => this.previewAction('continue', item)}
-            />
-          )}
-          {item.repport.id && (
-            <IconButton type="restart" tip="Restart" onClick={() => this.previewAction('restart', item)} />
-          )}
-        </div>
-      );
-    },
-  },
-  {
-    title: '报告',
-    width: 30,
-    align: 'right',
-    render: item => {
-      return (
-        <div className="table-row p-t-1">
-          {item.report.userNumber === item.report.questionNumber && <IconButton type="report" tip="Report" />}
-        </div>
-      );
-    },
-  },
-];
+const TEXTBOOK = 'textbook';
 
 export default class extends Page {
   constructor(props) {
     super(props);
-    this.sentenceColums = [
-      {
-        title: '练习册',
-        width: 250,
-        align: 'left',
-        render: row => {
-          return (
-            <div className="table-row">
-              <div className="night f-s-16">{row.title}</div>
-              <div>
-                <ProgressText progress={row.process} size="small" />
-              </div>
+    this.examinationColumns = [{
+      title: '练习册',
+      width: 250,
+      align: 'left',
+      render: item => {
+        return (
+          <div className="table-row">
+            <div className="night f-s-16">{item.title}</div>
+            <div>
+              <ProgressText
+                progress={item.report.id ? formatPercent(item.repport.userNumber, item.report.questionNumber) : 0}
+                size="small"
+              />
             </div>
-          );
-        },
+          </div>
+        );
       },
-      {
-        title: '正确率',
-        width: 150,
-        align: 'left',
-        render: () => {
-          return (
-            <div className="table-row">
-              <div className="night f-s-16 f-w-b">--</div>
-              <div className="f-s-12">全站55%</div>
-            </div>
-          );
-        },
+    },
+    {
+      title: '正确率',
+      width: 150,
+      align: 'left',
+      render: item => {
+        return (
+          <div className="table-row">
+            <div className="night f-s-16 f-w-b">--</div>
+            <div className="f-s-12">{formatPercent(item.stat.totalCorrect, item.stat.totalNumber, false)}</div>
+          </div>
+        );
       },
-      {
-        title: '全站用时',
-        width: 150,
-        align: 'left',
-        render: () => {
-          return (
-            <div className="table-row">
-              <div className="night f-s-16 f-w-b">55%</div>
-              <div className="f-s-12">全站56s</div>
-            </div>
-          );
-        },
+    },
+    {
+      title: '全站用时',
+      width: 150,
+      align: 'left',
+      render: item => {
+        return (
+          <div className="table-row">
+            <div className="night f-s-16 f-w-b">--</div>
+            <div className="f-s-12">全站{formatSeconds(item.stat.totalTime / item.stat.totalNumber)}</div>
+          </div>
+        );
       },
-      {
-        title: '最近做题',
-        width: 150,
-        align: 'left',
-        render: () => {
-          return (
-            <div className="table-row">
-              <div>2019-04-28</div>
-              <div>07:30</div>
-            </div>
-          );
-        },
+    },
+    {
+      title: '最近做题',
+      width: 150,
+      align: 'left',
+      render: () => {
+        return (
+          <div className="table-row">
+            <div>2019-04-28</div>
+            <div>07:30</div>
+          </div>
+        );
       },
-      {
-        title: '操作',
-        width: 180,
-        align: 'left',
-        render: () => {
-          return (
-            <div className="table-row p-t-1">
-              <IconButton className="m-r-2" type="continue" tip="Continue" />
-              <IconButton type="restart" tip="Restart" />
-            </div>
-          );
-        },
+    },
+    {
+      title: '操作',
+      width: 180,
+      align: 'left',
+      render: item => {
+        return (
+          <div className="table-row p-t-1">
+            {!item.report && <IconButton type="start" tip="Start" onClick={() => Question.startLink('preview', item)} />}
+            {item.report.id && !item.report.isFinish && (
+              <IconButton
+                className="m-r-2"
+                type="continue"
+                tip="Continue"
+                onClick={() => Question.continueLink('preview', item)}
+              />
+            )}
+            {item.report.id && <IconButton type="restart" tip="Restart" onClick={() => this.restart('preview', item)} />}
+          </div>
+        );
       },
-      {
-        title: '报告',
-        width: 30,
-        align: 'right',
-        render: () => {
-          return (
-            <div className="table-row p-t-1">
-              <IconButton type="report" tip="Report" />
-            </div>
-          );
-        },
+    },
+    {
+      title: '报告',
+      width: 30,
+      align: 'right',
+      render: item => {
+        return (
+          <div className="table-row p-t-1">
+            {item.report.isFinish && <IconButton type="report" tip="Report" onClick={() => Question.reportLink(item)} />}
+          </div>
+        );
       },
-    ];
+    }];
   }
 
   initState() {
-    this.code = null;
-    this.columns = exerciseColumns;
-    this.exerciseProcess = {};
+    this.examinationProgress = {};
+    this.textbookProgress = {};
     this.inited = false;
     return {
-      tab1: SENTENCE,
+      tab1: '',
       tab2: '',
-      previewType: PREVIEW_CLASS,
       tabs: [],
-      allClass: [],
-      classProcess: {},
     };
   }
 
   init() {
-    Main.getExercise().then(result => {
-      const list = result.map(row => {
+    Main.getExamination().then(result => {
+      const list = result.filter(row => row.level === 1).map(row => {
         row.title = `${row.titleZh}${row.titleEn}`;
         row.key = row.extend;
         return row;
       });
       const tabs = formatTreeData(list, 'id', 'title', 'parentId');
-      tabs.push({ key: PREVIEW, name: '预习作业' });
-      const map = getMap(tabs, 'key');
-      this.setState({ tabs, map });
+      tabs.push({ key: TEXTBOOK, name: '数学机经' });
+      this.setState({
+        tabs,
+      });
       this.inited = true;
       this.refreshData();
     });
   }
 
   initData() {
-    const { info = {} } = this.props.user;
-    if (info.latestExercise) {
-      // 获取最后一次做题记录
-      Question.baseReport(info.latestExercise).then(result => {
-        this.setState({ latest: result });
-      });
+    const data = Object.assign(this.state, this.state.search);
+    if (!data.tab1) {
+      data.tab1 = TEXTBOOK;
     }
+    this.setState(data);
     if (this.inited) this.refreshData();
   }
 
-  refreshData() {
+  refreshData(tab) {
     const { tab1 } = this.state;
-    switch (tab1) {
-      case SENTENCE:
-        this.refreshSentence();
-        break;
-      case PREVIEW:
-        this.refreshPreview();
+    switch (tab || tab1) {
+      case TEXTBOOK:
+        this.refreshTextbook();
         break;
       default:
-        this.refreshExercise();
+        this.refreshExamination(tab || tab1);
     }
   }
 
-  refreshSentence() {
-    const { sentence, articleMap, paperList } = this.state;
-    if (!sentence) {
-      Sentence.getInfo().then(result => {
-        const chapters = [];
-        const map = {};
-        let index = 0;
-        let exerciseChapter = null;
-        if (!result.code) {
-          chapters.push(`${index}」试用`);
-        }
-        index += 1;
-        result.chapters.forEach(row => {
-          map[row.value] = row;
-          chapters.push(`「${index}」${row.short}`);
-          index += 1;
-          if (row.exercise) exerciseChapter = row;
-        });
-        this.setState({ sentence: result, chapters, chapterMap: map, exerciseChapter });
-      });
-    }
-
-    if (!articleMap) {
-      Sentence.listArticle().then(result => {
-        const map = {};
-        result.forEach(article => {
-          if (!map[article.chapter]) {
-            map[article.chapter] = [];
-          }
-          map[article.chapter].push(article);
+  refreshTextbook() {
+    Textbook.progress().then(result => {
+      // const exerciseProgress = getMap(r, 'id');
+      result = result.map(row => {
+        row.info = [
+          {
+            title: '已做',
+            number: row.userNumber || '-',
+            unit: '题',
+          },
+          {
+            title: '剩余',
+            number: row.userNumber ? row.questionNumber - row.userNumber : '-',
+            unit: '题',
+          },
+          {
+            title: '正确率',
+            number: row.userNumber ? formatPercent(row.userStat.userCorrect, row.userStat.userNumber, false) : '-%',
+            unit: '',
+          },
+          {
+            title: '全站',
+            number: row.userNumber ? formatPercent(row.stat.totalCorrect, row.stat.totalNumber, false) : '-%',
+            unit: '题',
+          },
+        ];
+
+        row.progress = formatPercent(row.questionNumber - row.userNumber || 0, row.questionNumber);
+        row.totalCorrect = formatPercent(row.stat.totalCorrect, row.stat.totalNumber, false);
+
+        row.children = row.children.map(r => {
+          r.title = r.title || r.titleZh;
+          r.progress = formatPercent(r.userNumber, r.questionNumber);
+          return r;
         });
-        this.setState({ articleMap: map });
-      });
-    }
 
-    if (!paperList) {
-      Sentence.listPaper().then(result => {
-        this.setState({ paperList: result, paperFilterList: result });
+        if (row.isLatest) {
+          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)}`];
+        }
+        return row;
       });
-    }
-  }
-
-  refreshPreview() {
-    const { previewType } = this.state;
-    switch (previewType) {
-      case PREVIEW_LIST:
-        this.refreshListPreview();
-        break;
-      case PREVIEW_CLASS:
-      default:
-        this.refreshClassProcess();
-        break;
-    }
-  }
-
-  refreshClassProcess() {
-    Course.classProcess().then(result => {
-      const classProcess = {};
-      for (let i = 0; i < result.length; i += 1) {
-        const item = result[i];
-        classProcess[item.category].push(item);
-      }
-      this.setState({ classProcess });
+      this.setState({ textbookProgress: result });
     });
   }
 
-  refreshListPreview() {
-    Question.listPreview().then(result => {
-      this.setState({ previews: result });
-    });
-  }
-
-  refreshExercise() {
-    const { map, tab1 } = this.state;
-    let { tab2 } = this.state;
-    if (!map) {
+  refreshExamination(tab) {
+    const { tabs, tab1 } = this.state;
+    if (!tabs) {
       // 等待数据加载
       return;
     }
-    if (tab1 === '') {
-      return;
-    }
-    const subject = map[tab1];
-    if (tab2 === '') {
-      tab2 = subject.children[0].key;
-      this.onChangeTab(2, tab2);
-      return;
-    }
-    const type = map[tab2];
-    Main.getExerciseChildren(type.id, true).then(result => {
-      const exerciseChild = result;
-      this.setState({ exerciseChild });
-    });
-    Question.getExerciseProcess(type.id).then(r => {
-      const exerciseProcess = getMap(r, 'id');
-      this.setState({ exerciseProcess });
+    const [subject] = tabs.filter(row => row.key === tab || row.key === tab1);
+    Question.getExaminationProgress(subject.id).then(result => {
+      // const exerciseProgress = getMap(r, 'id');
+      result = result.map(row => {
+        row.title = `${row.titleZh}${row.titleEn}`;
+        row.info = [
+          {
+            title: '已做',
+            number: row.userNumber || '-',
+            unit: '套',
+          },
+          {
+            title: '剩余',
+            number: row.userNumber ? row.questionNumber - row.userNumber : '-',
+            unit: '套',
+          },
+        ];
+        return row;
+      });
+      this.setState({ examinationProgress: result });
     });
   }
 
-  onChangePreviewType(type) {
-    this.setState({ previewType: type });
-    this.refreshPreview();
+  onChangeTab(level, tab) {
+    const { tab1 } = this.state;
+    const data = {};
+    if (level > 1) {
+      data.tab1 = tab1;
+      data.tab2 = tab;
+    } else {
+      data.tab1 = tab;
+    }
+    // this.refreshData(tab);
+    this.refreshQuery(data);
   }
 
-  onChangeTab(level, tab) {
-    const state = {};
-    state[`tab${level}`] = tab;
-    this.setState(state);
-    this.refresh();
+  onTextbook() {
+    const { tab1, tab2, struct } = this.state;
+    const data = {
+      tab1, tab2, struct,
+    };
+    this.refreshQuery(data);
   }
 
-  previewAction(type, item) {
-    switch (type) {
-      case 'start':
-        this.start('preview', item);
-        break;
-      case 'restart':
-        this.restart(item);
-        break;
-      case 'continue':
-        this.continue('preview', item);
-        break;
-      default:
-        break;
-    }
+  // 开通模考或者机经
+  open(recordId) {
+    Order.useRecord(recordId).then(() => {
+      asyncSMessage('开通成功');
+      this.refresh();
+    });
   }
 
   restart(item) {
     asyncConfirm('提示', '是否重置', () => {
-      Question.restart(item.report.id).then(() => {
+      Question.restart(item.paper.id).then(() => {
         this.refresh();
       });
     });
   }
 
-  start(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}`);
+  examinationList(item) {
+    linkTo(`/examination/list/${item.id}`);
   }
 
-  continue(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}?r=${item.report.id}`);
-  }
-
-  activeSentence() {
-    Sentence.active(this.code).then(() => {
-      // 重新获取长难句信息
-      this.clearSentenceTrail();
-      this.setState({ sentence: null, articleMap: null, paperList: null });
-      this.refresh();
-    });
-  }
-
-  trailSentence() {
-    this.setState({ sentenceInput: false });
-    User.sentenceTrail();
-  }
-
-  sentenceRead(article) {
-    linkTo(`/sentence/read?chapter=${article.chapter}&part=${article.part}`);
-  }
-
-  sentenceFilter() {
-    const { paperList } = this.state;
-    const list = paperList.filter(row => {
-      return !!row;
-    });
-    this.setState({ paperFilterList: list });
-  }
-
-  clearExercise() {
-    My.clearLatestExercise();
-    this.setState({ latest: null });
+  textbookList(item) {
+    linkTo(`/textbook/list/${item.id}`);
   }
 
   renderView() {
-    const { tab1 = {}, tab2 = {}, tabs, map = {}, latest } = this.state;
-    const children = (map[tab1] || {}).children || [];
+    const { tab1, tab2, tabs } = this.state;
+    const [subject] = tabs.filter(row => row.key === tab1);
+    const children = (subject && subject.children) ? subject.children : [];
     return (
       <div>
-        {latest && (
-          <Continue
-            data={latest}
-            onClose={() => {
-              this.clearExercise();
-            }}
-            onContinue={() => { }}
-            onRestart={() => { }}
-            onNext={() => { }}
-          />
-        )}
         <div className="content">
           <Module className="m-t-2">
             <Tabs
@@ -468,245 +298,85 @@ export default class extends Page {
                 this.onChangeTab(1, key);
               }}
             />
-            {children.length > 1 && <Tabs active={tab2} tabs={children} onChange={key => this.onChangeTab(2, key)} />}
+            {children && children.length > 1 && (
+              <Tabs active={tab2} tabs={children} onChange={key => this.onChangeTab(2, key)} />
+            )}
           </Module>
-          {tab1 !== SENTENCE && tab1 !== PREVIEW && this.renderExercise()}
-          {tab1 === SENTENCE && this.renderSentence()}
-          {tab1 === PREVIEW && this.renderPreview()}
+          {tab1 !== TEXTBOOK && this.renderExamination()}
+          {tab1 === TEXTBOOK && this.renderTextbook()}
         </div>
       </div>
     );
   }
 
-  renderPreview() {
-    const { previewType } = this.state;
-    switch (previewType) {
-      case PREVIEW_CLASS:
-        return this.renderPreviewClass();
-      case PREVIEW_LIST:
-        return this.renderPreviewList();
-      default:
-        return <div />;
-    }
-  }
-
-  renderPreviewClass() {
-    const { allClass, classProcess } = this.state;
+  renderTextbook() {
+    const { textbookProgress = [] } = this.state;
     return (
-      <div className="work-body">
-        <div className="work-nav">
-          <div className="left">完成情况</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_LIST)}>
-            全部作业 >
-          </div>
-        </div>
-        <Division col="3">
-          {allClass.map(item => {
-            return <Card data={item} process={classProcess[item.id]} previewAction={this.previewAction} />;
+      <div>
+        <Division col={2}>
+          {(textbookProgress || []).map(struct => {
+            if (struct.needService && !struct.hasService) {
+              if (struct.unUseRecord) {
+                return <WaitPanel
+                  title={struct.isLatest ? '最新' : '往期'}
+                  col="3"
+                  data={struct}
+                />;
+              }
+              return <BuyPanel
+                title={struct.isLatest ? '最新' : '往期'}
+                onBuy={() => {
+                  this.buyTextbook();
+                }}
+              />;
+            }
+            return <Panel
+              title={struct.isLatest ? '最新' : '往期'}
+              col="3"
+              data={struct}
+              onClick={(item) => {
+                this.textbookList(item);
+              }}
+            />;
           })}
         </Division>
       </div>
     );
   }
 
-  renderPreviewList() {
-    const { previews } = this.state;
-    return (
-      <div className="work-body">
-        <div className="work-nav">
-          <div className="left">全部作业</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_CLASS)}>
-            我的课程 >
-          </div>
-        </div>
-        <ListTable
-          filters={[
-            {
-              type: 'radio',
-              checked: 'today',
-              list: [{ key: 'today', title: '今日需完成' }, { key: 'tomorrow', title: '明日需完成' }],
-            },
-            {
-              type: 'radio',
-              checked: 'unfinish',
-              list: [{ key: 'unfinish', title: '未完成' }, { key: 'finish', title: '已完成' }],
-            },
-            { type: 'select', checked: 'all', list: [{ key: 'all', title: '全部' }] },
-          ]}
-          rightAction={
-            <div>
-              有效期至:2019-11-13{' '}
-              <Tooltip overlayClassName="gray" placement="top" title="全部模考做完才可重置">
-                <a>
-                  <Button size="small" disabled radius>
-                    Reset
-                  </Button>
-                </a>
-              </Tooltip>
-            </div>
-          }
-          data={previews}
-          columns={this.columns}
-        />
-      </div>
-    );
-  }
-
-  renderSentence() {
-    const { sentence = {}, sentenceInput } = this.state;
-    const { sentenceTrail } = this.props.user;
-    if (sentenceInput !== true && (sentence.code || sentenceTrail)) {
-      return this.renderSentenceArticle();
-    }
-    return this.renderInputCode();
-  }
-
-  renderSentenceArticle() {
-    const {
-      sentence = {},
-      chapters,
-      chapter,
-      exerciseChapter = {},
-      chapterMap = {},
-      articleMap = {},
-      paperFilterList = [],
-      paperList = [],
-      paperChecked,
-    } = this.state;
-    const { sentenceTrail } = this.props.user;
-    let maxStep = 0;
-    if (sentenceTrail) {
-      // 试用只能访问第一step
-      maxStep = 1;
-      // 查找练习章节
-    }
-    const chapterInfo = chapterMap[chapter] || {};
-    let isExercise = false;
-    if (chapterInfo && chapterInfo.exercise) {
-      isExercise = true;
-    }
+  renderExamination() {
+    const { examinationProgress = [] } = this.state;
     return (
       <div>
-        {sentence.code && <div className="sentence-code">CODE: {sentence.code}</div>}
-        {sentenceTrail && (
-          <div className="sentence-code">
-            CODE: <Link to="">去获取</Link>
-            <a
-              onClick={() => {
-                this.setState({ sentenceInput: true });
+        <Division col={3} type="compact">
+          {(examinationProgress || []).map(struct => {
+            if (struct.hasService) {
+              return <SmallPanel
+                title={struct.title}
+                data={struct}
+                onClick={() => {
+                  this.examinationList(struct);
+                }}
+              />;
+            } if (struct.unUseRecord) {
+              return <SmallWaitPanel
+                title={struct.title}
+                data={struct}
+                onOpen={() => {
+                  this.open(struct.unUseRecord);
+                }}
+              />;
+            }
+            return <SmallBuyPanel
+              title={struct.title}
+              data={struct}
+              onBuy={() => {
+                this.buyQxCat();
               }}
-            >
-              输入
-            </a>
-          </div>
-        )}
-        <Module>
-          <Step
-            list={chapters}
-            step={chapter}
-            onClick={step => {
-              this.setState({ chapter: step });
-            }}
-            message="请购买后访问"
-            maxStep={maxStep}
-          />
-        </Module>
-        {/* 正常文章 */}
-        {sentence.code && !isExercise && (
-          <List
-            title={`Chapter${chapter}`}
-            subTitle={chapterInfo.title}
-            list={articleMap[chapter]}
-            onClick={part => {
-              this.sentenceRead(part);
-            }}
-          />
-        )}
-        {/* 正常练习 */}
-        {sentence.code && isExercise && (
-          <ListTable
-            title={`Chapter${chapter}`}
-            subTitle={chapterInfo.title}
-            filters={[
-              {
-                type: 'radio',
-                checked: paperChecked,
-                list: [{ key: 0, title: '未完成' }, { key: 1, title: '已完成' }],
-                onChange: item => {
-                  console.log(item);
-                  this.sentenceFilter(item);
-                },
-              },
-            ]}
-            data={paperFilterList}
-            columns={this.sentenceColums}
-          />
-        )}
-        {/* 试读文章 */}
-        {sentenceTrail && (
-          <List
-            list={[]}
-            onClick={part => {
-              this.sentenceRead(part);
-            }}
-          />
-        )}
-        {/* 试练 */}
-        {sentenceTrail && (
-          <ListTable
-            title={`Chapter${exerciseChapter.value}`}
-            subTitle={exerciseChapter.title}
-            data={paperList}
-            columns={this.sentenceColums}
-          />
-        )}
+            />;
+          })}
+        </Division>
       </div>
     );
   }
-
-  renderInputCode() {
-    return (
-      <Module className="code-module">
-        <div className="title">输入《千行GMAT长难句》专属 Code,解锁在线练习功能。</div>
-        <div className="input-block">
-          <Input
-            size="lager"
-            placeholder="请输入CODE"
-            onChange={value => {
-              this.code = value;
-            }}
-          />
-          <Button
-            size="lager"
-            onClick={() => {
-              this.activeSentence();
-            }}
-          >
-            解锁
-          </Button>
-        </div>
-        <div className="tip">
-          <Link to="/" className="left link">
-            什么是CODE?
-          </Link>
-          <span>没有 CODE?</span>
-          <Link to="/" className="link">
-            去获取 >>
-          </Link>
-          <a
-            onClick={() => {
-              this.trailSentence();
-            }}
-            className="right link"
-          >
-            试用 >>
-          </a>
-        </div>
-      </Module>
-    );
-  }
-
-  renderExercise() {
-    return <div />;
-  }
 }

+ 1 - 1
front/project/www/routes/exercise/list/index.js

@@ -1,6 +1,6 @@
 export default {
   path: '/exercise/list/:id',
-  key: 'exercise',
+  key: 'exercise-list',
   title: '练习列表',
   needLogin: false,
   tab: 'exercise',

+ 18 - 2
front/project/www/routes/exercise/list/page.js

@@ -250,7 +250,8 @@ export default class extends Page {
   }
 
   renderView() {
-    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    const { logic, logicExtend, logics = [], logicExtends = [], list, search } = this.state;
+    const { finish } = search;
     return (
       <div>
         <div className="content">
@@ -274,8 +275,23 @@ export default class extends Page {
               }}
             />}
           </Module>
-
           <ListTable
+            filters={[
+              {
+                type: 'radio',
+                checked: finish,
+                list: [{ key: 0, title: '未完成' }, { key: 1, title: '已完成' }],
+                onChange: item => {
+                  if (item.key === 0) {
+                    this.search({ finish: 0 });
+                  } else if (item.key === 1) {
+                    this.search({ finish: 1 });
+                  } else {
+                    this.search({ finish: null });
+                  }
+                },
+              },
+            ]}
             data={list}
             columns={this.columns}
           />

+ 2 - 0
front/project/www/routes/exercise/main/index.less

@@ -24,6 +24,8 @@
 
       .button {
         width: 150px;
+        height: 44px;
+        line-height: 28px;
         border-top-right-radius: 22px;
         border-bottom-right-radius: 22px;
       }

+ 189 - 43
front/project/www/routes/exercise/main/page.js

@@ -3,8 +3,8 @@ import './index.less';
 import { Modal } from 'antd';
 import { Link } from 'react-router-dom';
 import Page from '@src/containers/Page';
-import { asyncConfirm } from '@src/services/AsyncTools';
-import { formatTreeData, formatSeconds, formatDate, formatPercent } from '@src/services/Tools';
+import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
+import { formatTreeData, formatSeconds, formatDate, formatPercent, getMap } from '@src/services/Tools';
 import Continue from '../../../components/Continue';
 import Step from '../../../components/Step';
 import Panel from '../../../components/Panel';
@@ -15,7 +15,7 @@ import Input from '../../../components/Input';
 import Button from '../../../components/Button';
 import AnswerButton from '../../../components/AnswerButton';
 import Division from '../../../components/Division';
-import Card from '../../../components/Card';
+import { Card1 } from '../../../components/Card';
 import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
@@ -25,13 +25,16 @@ import { Sentence } from '../../../stores/sentence';
 import { Question } from '../../../stores/question';
 import { Course } from '../../../stores/course';
 import { User } from '../../../stores/user';
-import { CourseModuleShow } from '../../../../Constant';
+import { CourseModuleShow, CourseModule } from '../../../../Constant';
+import { Order } from '../../../stores/order';
 
 const SENTENCE = 'sentence';
 const PREVIEW = 'preview';
 const PREVIEW_COURSE = 'PREVIEW_COURSE';
 const PREVIEW_LIST = 'PREVIEW_LIST';
 
+const CourseModuleMap = getMap(CourseModule, 'value', 'label');
+
 const exerciseColumns = [
   {
     title: '练习册',
@@ -286,18 +289,21 @@ export default class extends Page {
       const tabs = formatTreeData(list, 'id', 'title', 'parentId');
       // 课程顶级分类
       const courseStructs = result.filter(row => row.isCourse && row.level === 1);
+      courseStructs.unshift({ key: '', name: '全部' });
       tabs.push({ key: PREVIEW, name: '预习作业' });
-      this.setState({ tabs, courseStructs });
+      this.courseStructMap = getMap(courseStructs, 'id', 'title');
+      this.setState({
+        tabs,
+        courseStructs,
+        courseTabs: CourseModuleShow.map(row => {
+          row.title = row.label;
+          row.key = row.value;
+          return row;
+        }),
+      });
       this.inited = true;
       this.refreshData();
     });
-    this.setState({
-      courseTabs: CourseModuleShow.map(row => {
-        row.title = row.label;
-        row.key = row.value;
-        return row;
-      }),
-    });
   }
 
   initData() {
@@ -309,6 +315,13 @@ export default class extends Page {
       });
     }
     const data = Object.assign(this.state, this.state.search);
+    if (!data.tab1) {
+      data.tab1 = SENTENCE;
+    }
+    if (data.recordId) {
+      // 作业列表
+      data.previewType = PREVIEW_LIST;
+    }
     this.setState(data);
     if (this.inited) this.refreshData();
   }
@@ -420,7 +433,7 @@ export default class extends Page {
   }
 
   refreshCourseProcess() {
-    const { courseTabs, structId } = this.state;
+    const { courseTabs, courseStructs, struct } = this.state;
     let { tab2 } = this.state;
     let tab;
     if (tab2 === '') {
@@ -430,19 +443,25 @@ export default class extends Page {
     } else {
       ([tab] = courseTabs.filter(row => row.key === tab2));
     }
-    Course.progress(tab.courseModules, structId).then(result => {
-      const courseProgress = {};
-      for (let i = 0; i < result.length; i += 1) {
-        const item = result[i];
-        courseProgress[item.category].push(item);
-      }
-      this.setState({ courseProgress });
+    const [courseStruct] = courseStructs.filter(row => row.key === struct);
+    Course.progress(tab.value, courseStruct ? courseStruct.id : null).then(result => {
+      const courseMap = {};
+      const now = new Date().getTime();
+      courseMap.open = result.filter(row => !row.isUsed);
+      courseMap.end = result.filter(row => row.isUsed && (!row.isStop || (row.isStop && row.restoreTime)) && new Date(row.useEndTime).getTime() < now);
+      // todo 排序:sc,rc,cr
+      courseMap.process = result.filter(row => row.isUsed && (row.isStop || row.restoreTime) && new Date(row.useEndTime).getTime() >= now);
+      this.setState({ courseMap });
     });
   }
 
   refreshListPreview() {
-    Question.listPreview().then(result => {
-      this.setState({ previews: result });
+    const { recordId, endTime, finish } = this.state;
+    Course.listPreview({ recordId, endTime, finish }).then(result => {
+      this.setState({ previews: result.list });
+    });
+    Course.record(recordId).then(result => {
+      this.setState({ record: result });
     });
   }
 
@@ -499,11 +518,6 @@ export default class extends Page {
     });
   }
 
-  onChangePreviewType(type) {
-    this.setState({ previewType: type });
-    this.refreshPreview();
-  }
-
   onChangeTab(level, tab) {
     const { tab1 } = this.state;
     const data = {};
@@ -517,6 +531,30 @@ export default class extends Page {
     this.refreshQuery(data);
   }
 
+  onChangeCourse(struct) {
+    const { tab1, tab2 } = this.state;
+    const data = {
+      tab1, tab2, struct,
+    };
+    this.refreshQuery(data);
+  }
+
+  onPreviewCourse() {
+    const { tab1, tab2, struct } = this.state;
+    const data = {
+      tab1, tab2, struct,
+    };
+    this.refreshQuery(data);
+  }
+
+  onPreviewList(recordId) {
+    const { tab1, tab2, struct } = this.state;
+    const data = {
+      tab1, tab2, struct, recordId,
+    };
+    this.refreshQuery(data);
+  }
+
   previewAction(type, item) {
     switch (type) {
       case 'start':
@@ -533,6 +571,14 @@ export default class extends Page {
     }
   }
 
+  // 开通课程
+  open(recordId) {
+    Order.useRecord(recordId).then(() => {
+      asyncSMessage('开通成功');
+      this.refresh();
+    });
+  }
+
   restart(item) {
     asyncConfirm('提示', '是否重置', () => {
       Question.restart(item.paper.id).then(() => {
@@ -591,7 +637,7 @@ export default class extends Page {
   renderView() {
     const { tab1, tab2, tabs, latest, sentenceModel, previewType, courseTabs = [] } = this.state;
     const [subject] = tabs.filter(row => row.key === tab1);
-    const children = subject ? subject.children : (tab1 === 'preview' && previewType === 'PREVIEW_COURSE' ? courseTabs : []);
+    const children = (subject && subject.children) ? subject.children : (tab1 === 'preview' && previewType === PREVIEW_COURSE ? courseTabs : []);
     return (
       <div>
         {latest && (
@@ -624,7 +670,6 @@ export default class extends Page {
             {children && children.length > 1 && (
               <Tabs active={tab2} tabs={children} onChange={key => this.onChangeTab(2, key)} />
             )}
-            {}
           </Module>
           {tab1 !== SENTENCE && tab1 !== PREVIEW && this.renderExercise()}
           {tab1 === SENTENCE && this.renderSentence()}
@@ -648,18 +693,74 @@ export default class extends Page {
   }
 
   renderPreviewCourse() {
-    const { allCourse, courseProgress } = this.state;
+    const { courseStructs, struct, tab2, courseTabs, courseMap = {} } = this.state;
     return (
       <div className="work-body">
+        <div className="work-nav" hidden={courseTabs && courseTabs.length > 0 && tab2 !== courseTabs[0].key}>
+          <Tabs
+            type="tag"
+            active={struct || ''}
+            space={5}
+            tabs={courseStructs}
+            onChange={key => {
+              this.onChangeCourse(key);
+            }}
+          />
+        </div>
         <div className="work-nav">
-          <div className="left">完成情况</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_LIST)}>
-            全部作业 >
-          </div>
+          <div className="left">学习中</div>
+        </div>
+        <Division col="3">
+          {(courseMap.process || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status={row.isStop && !row.isSuspend ? 'stop' : 'ing'}
+              list={row.papers.map(r => {
+                let progress = 0;
+                if (r.report) {
+                  progress = formatPercent(r.report.userNumber, r.report.questionNumber);
+                }
+                r.progress = progress;
+                return r;
+              })}
+              data={row}
+              onPreview={() => {
+                this.onPreviewList(row.id);
+              }}
+            />;
+          })}
+        </Division>
+        <div className="work-nav">
+          <div className="left">待开通</div>
+        </div>
+        <Division col="3">
+          {(courseMap.wait || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status='open'
+              data={row}
+              onOpen={() => {
+                this.open(row.id);
+              }}
+            />;
+          })}
+        </Division>
+        <div className="work-nav">
+          <div className="left">已结束</div>
         </div>
         <Division col="3">
-          {allCourse.map(item => {
-            return <Card data={item} process={courseProgress[item.id]} previewAction={this.previewAction} />;
+          {(courseMap.end || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status='end'
+              data={row}
+              onPreview={() => {
+                this.onPreviewList(row.id);
+              }}
+            />;
           })}
         </Division>
       </div>
@@ -667,12 +768,23 @@ export default class extends Page {
   }
 
   renderPreviewList() {
-    const { previews } = this.state;
+    const { previews = [], record = {}, search = {} } = this.state;
+    const { finish, endTime } = search;
+    let finishTime = '';
+    if (endTime) {
+      const endTimeD = new Date(endTime);
+      const now = new Date();
+      if (now.getTime() + 86400000 > endTimeD.getTime()) {
+        finishTime = 'today';
+      } else {
+        finishTime = 'tomorrow';
+      }
+    }
     return (
       <div className="work-body">
         <div className="work-nav">
-          <div className="left">全部作业</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_COURSE)}>
+          <div className="left">{`${(record.course || {}).title || ''}${record.vsNo > 0 ? `V${record.vsNo}` : ''}${record.number > 0 ? `(${record.number}课时)` : ''}`}全部作业</div>
+          <div className="right theme c-p" onClick={() => this.onPreviewCourse()}>
             我的课程 >
           </div>
         </div>
@@ -680,13 +792,47 @@ export default class extends Page {
           filters={[
             {
               type: 'radio',
-              checked: 'today',
+              checked: finishTime,
               list: [{ key: 'today', title: '今日需完成' }, { key: 'tomorrow', title: '明日需完成' }],
+              onChange: (item) => {
+                if (item.key === finishTime) {
+                  this.search({ endTime: null });
+                } else if (item.key === 'today') {
+                  const a = new Date();
+                  a.setDate(a.getDate() + 1);
+                  a.setHours(0);
+                  a.setMinutes(0);
+                  a.setMilliseconds(0);
+                  a.setSeconds(0);
+                  this.search({ endTime: formatDate(a, 'YYYY-MM-DD') });
+                } else if (item.key === 'tomorrow') {
+                  const a = new Date();
+                  a.setDate(a.getDate() + 2);
+                  a.setHours(0);
+                  a.setMinutes(0);
+                  a.setMilliseconds(0);
+                  a.setSeconds(0);
+                  this.search({ endTime: formatDate(a, 'YYYY-MM-DD') });
+                } else {
+                  this.search({ endTime: null });
+                }
+              },
             },
             {
               type: 'radio',
-              checked: 'unfinish',
-              list: [{ key: 'unfinish', title: '未完成' }, { key: 'finish', title: '已完成' }],
+              checked: finish,
+              list: [{ key: '0', title: '未完成' }, { key: '1', title: '已完成' }],
+              onChange: (item) => {
+                if (item.key === finish) {
+                  this.search({ finish: null });
+                } else if (item.key === '0') {
+                  this.search({ finish: '0' });
+                } else if (item.key === '1') {
+                  this.search({ finish: '1' });
+                } else {
+                  this.search({ finish: null });
+                }
+              },
             },
           ]}
           data={previews}
@@ -912,7 +1058,7 @@ export default class extends Page {
     const { exerciseProgress = [] } = this.state;
     return (
       <div>
-        <Division col="2">
+        <Division col={2}>
           {(exerciseProgress || []).map(struct => {
             const [first] = struct.children;
             let col = 3;

+ 1 - 1
front/project/www/routes/page/home/page.js

@@ -39,7 +39,7 @@ export default class extends Page {
 
   test() {
     User.needLogin().then(() => {
-      console.log('loginCB test');
+      linkTo('/my');
     });
   }
 

+ 28 - 35
front/project/www/routes/paper/process/base/index.js

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
 import './index.less';
 import { Checkbox, Icon as AntDIcon } from 'antd';
 import Assets from '@src/components/Assets';
-import { formatSeconds, formatSecond, getMap } from '@src/services/Tools';
+import { formatSeconds, formatMinuteSecond, getMap } from '@src/services/Tools';
 import Icon from '../../../../components/Icon';
 import Button from '../../../../components/Button';
 import Navigation from '../../../../components/Navigation';
@@ -224,8 +224,8 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
+              {showTime && !stageTime && singleTime && `Time cost ${formatMinuteSecond(singleTime)}`}
             </div>
             <div
               className="block"
@@ -263,14 +263,14 @@ export default class extends Component {
 
   renderExaminationStart() {
     // const { paper, userQuestion, singleTime, stageTime, flow } = this.props;
-    // const { showTime, showNo } = this.state;
-    const { paper, flow } = this.props;
+    const { showTime } = this.state;
+    const { paper, flow, startTime } = this.props;
     return (
       <div className="layout">
         <div className="fixed" />
         <div className="layout-header">
           <div className="title">{paper.title}</div>
-          {/* <div className="right">
+          <div className="right">
             <div
               className="block"
               onClick={() => {
@@ -278,10 +278,9 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && startTime && `Time left ${formatMinuteSecond(startTime)}`}
             </div>
-            <div
+            {/* <div
               className="block"
               onClick={() => {
                 this.setState({ showNo: !showNo });
@@ -289,8 +288,8 @@ export default class extends Component {
             >
               <Assets name="subjectnumber_icon" />
               {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
-            </div>
-          </div> */}
+            </div> */}
+          </div>
         </div>
         <div className={'layout-body'}>{paper.isAdapt > 1 ? this.renderExaminationStartCAT() : this.renderExaminationStartDefault()}</div>
         <div className="layout-footer">
@@ -311,8 +310,8 @@ export default class extends Component {
   }
 
   renderExerciseStart() {
-    const { disorder } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder } = setting;
     return (
       <div className="start">
         <div className="bg" />
@@ -335,7 +334,7 @@ export default class extends Component {
             </div>
           </div>
           {paper.times > 0 && <div className="tip">
-            <Checkbox className="m-r-1" checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} />
+            <Checkbox className="m-r-1" checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} />
             题目选项乱序显示
           </div>}
           <div className="submit">
@@ -349,8 +348,8 @@ export default class extends Component {
   }
 
   renderExaminationStartCAT() {
-    const { disorder, order, orderIndex } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder, order, orderIndex } = setting;
     return (
       <div className="exercise-start default">
         <div className="title">Section Ordering</div>
@@ -368,7 +367,7 @@ export default class extends Component {
           {ExaminationOrder.map((row, index) => {
             return <div className="block-item">
               <div className="block-title" onClick={() => {
-                this.setState({ order: row.value, orderIndex: index });
+                flow.setSetting({ order: row.value, orderIndex: index });
               }}>
                 <div className="block-title-border">
                   {orderIndex === index && <AntDIcon type="check" />}
@@ -383,7 +382,7 @@ export default class extends Component {
         </div>
         <div className="bottom">
           {paper.times > 0 && <div className="text">
-            <Checkbox checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} /> 题目选项乱序显示
+            <Checkbox checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} /> 题目选项乱序显示
           </div>}
           <div className="text">
             Click{' '}
@@ -399,8 +398,8 @@ export default class extends Component {
   }
 
   renderExaminationStartDefault() {
-    const { disorder, order, orderIndex } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder, order, orderIndex } = setting;
     return (
       <div className="exercise-start cat">
         <div className="title">Section Ordering</div>
@@ -420,7 +419,7 @@ export default class extends Component {
                       // 选中
                       order[i] = r.value;
                     }
-                    this.setState({ order });
+                    flow.setSetting({ order });
                   }}>
                     <Checkbox checked={orderIndex === index ? order.indexOf(r.value) >= 0 : false} /> {r.label}{' '}
                   </div>;
@@ -431,7 +430,7 @@ export default class extends Component {
         </div>
         <div className="bottom">
           {paper.times > 0 && <div className="text">
-            <Checkbox checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} /> 题目选项乱序显示
+            <Checkbox checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} /> 题目选项乱序显示
           </div>}
           <div className="text">
             Click{' '}
@@ -448,8 +447,8 @@ export default class extends Component {
   }
 
   renderRelax() {
-    const { paper, userQuestion, singleTime, stageTime, flow } = this.props;
-    const { showTime, showNo } = this.state;
+    const { paper, stageTime, flow } = this.props;
+    const { showTime } = this.state;
     return (
       <div className="layout">
         <div className="layout-header">
@@ -462,10 +461,10 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
+              {/* {showTime && singleTime && `Time cost ${formatMinuteSecond(singleTime)}`} */}
             </div>
-            <div
+            {/* <div
               className="block"
               onClick={() => {
                 this.setState({ showNo: !showNo });
@@ -473,7 +472,7 @@ export default class extends Component {
             >
               <Assets name="subjectnumber_icon" />
               {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
-            </div>
+            </div> */}
           </div>
         </div>
         <div className={'layout-body'}>
@@ -481,13 +480,7 @@ export default class extends Component {
             <div className="title">
               Optional Break <Icon name="question" />
             </div>
-            <div className="time">
-              <div className="block">0</div>
-              <div className="block">1</div>
-              <div className="div">:</div>
-              <div className="block">2</div>
-              <div className="block">3</div>
-            </div>
+            <div className="time" dangerouslySetInnerHTML={{ __html: formatMinuteSecond(stageTime).split(':').map(row => row.replace(/([0-9])/g, '<div class="block">$1</div>')).join('<div class="div">:</div>') }} />
           </div>
         </div>
         <div className="layout-footer">

+ 51 - 10
front/project/www/routes/paper/process/page.js

@@ -6,6 +6,7 @@ import { randomList, sortListWithOrder, resortListWithOrder } from '@src/service
 import { Question } from '../../../stores/question';
 import Base from './base';
 import Sentence from './sentence';
+import { ExaminationOrder } from '../../../../Constant';
 import { Main } from '../../../stores/main';
 import { My } from '../../../stores/my';
 
@@ -21,6 +22,9 @@ export default class extends Page {
     this.stageNumber = 0;
     this.stageProcess = { number: 0, time: 0 };
     this.relaxProcess = { time: 0 };
+
+    this.startInterval = null;
+    this.startTime = 0;
   }
 
   initState() {
@@ -66,6 +70,10 @@ export default class extends Page {
         });
       } else {
         this.setState({ scene: 'start' });
+        // 模考cat1分钟自动开始
+        if (paper.isAdapt > 1) {
+          this.startWaitTime();
+        }
       }
       handler.catch(() => {
         goBack();
@@ -73,6 +81,11 @@ export default class extends Page {
     });
   }
 
+  setSetting(newSetting) {
+    const { setting } = this.state;
+    this.setState({ setting: Object.assign(setting, newSetting) });
+  }
+
   start(setting) {
     const { type, id } = this.params;
     return Question.start(type, id, setting).then(report => {
@@ -175,14 +188,12 @@ export default class extends Page {
     });
   }
 
+  // 主动进入下一阶段
   stage() {
     const { report } = this.state;
     return Question.stage(report.id)
       .then(() => {
-        return this.next();
-      })
-      .then(() => {
-        this.stageQuestionTime();
+        return this.relaxStage();
       });
   }
 
@@ -213,18 +224,48 @@ export default class extends Page {
     }
   }
 
-  stageQuestionTime(initTime) {
+  stageQuestionTime(initTime, stop) {
     if (this.stageInterval) {
       clearInterval(this.stageInterval);
       this.stageInterval = null;
       this.stageTime = initTime;
     }
-    this.stageInterval = setInterval(() => {
-      this.stageTime += 1;
-      if (this.stageTime >= this.stageProcess.number) {
-        this.nextStage();
+    if (!stop) {
+      this.stageInterval = setInterval(() => {
+        this.stageTime += 1;
+        if (this.stageTime >= this.stageProcess.time) {
+          const { scene } = this.state;
+          if (scene === 'relax') {
+            // 进入下一阶段,获取下一题
+            this.next();
+          } else {
+            // 提交当前阶段
+            this.stage();
+          }
+        }
+        this.setState({ stageTime: this.targetProcess.time - this.stageTime });
+      }, 1000);
+    }
+  }
+
+  startWaitTime() {
+    if (this.startInterval) {
+      clearInterval(this.startInterval);
+      this.startInterval = null;
+    }
+    this.startInterval = setInterval(() => {
+      this.startTime += 1;
+      // 1分钟等待: 自动提交第一选择
+      const { scene } = this.state;
+      if (scene !== 'start') {
+        clearInterval(this.startInterval);
+        this.startInterval = null;
+      } else if (this.startTime >= 60) {
+        clearInterval(this.startInterval);
+        this.startInterval = null;
+        this.start(Object.assign({ order: ExaminationOrder[0].value }, this.state.setting));
       }
-      this.setState({ stageTime: this.targetProcess.time - this.stageTime });
+      this.setState({ startTime: 60 - this.startTime });
     }, 1000);
   }
 

+ 5 - 5
front/project/www/routes/paper/question/page.js

@@ -231,7 +231,7 @@ export default class extends Page {
   }
 
   renderExerciseHeader() {
-    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [] } = this.state;
+    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [], question = {} } = this.state;
     return <div className="layout-header">
       <div className="left">
         <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
@@ -249,7 +249,7 @@ export default class extends Page {
           </div>}
         </div>
       </div>
-      <div className="right">
+      <div className="right" hidden={question.questionType === 'awa'}>
         <span className="b">
           用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
           {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
@@ -284,13 +284,13 @@ export default class extends Page {
         </div>
         <div className="center">
           <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
-          <AnswerButton className="item" onClick={() => {
-            if (questionStatus) {
+          {questionStatus >= 0 && <AnswerButton className="item" onClick={() => {
+            if (questionStatus > 0) {
               this.setState({ askModal: true });
             } else {
               this.setState({ askFailModal: true });
             }
-          }}>提问</AnswerButton>
+          }}>提问</AnswerButton>}
           <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
         </div>
         <div className="right">

+ 2 - 1
front/project/www/routes/preview/index.js

@@ -1,2 +1,3 @@
+import list from './list';
 
-export default [];
+export default [list];

+ 1 - 1
front/project/www/routes/preview/list/index.less

@@ -1,6 +1,6 @@
 @charset "utf-8";
 
-#exercise-list {
+#preview-list {
   .code-module {
     padding: 80px 250px;
     text-align: center;

+ 2 - 1
front/project/www/routes/textbook/index.js

@@ -1,2 +1,3 @@
+import list from './list';
 
-export default [];
+export default [list];

+ 10 - 0
front/project/www/routes/textbook/list/index.js

@@ -0,0 +1,10 @@
+export default {
+  path: '/textbook/list/:id',
+  key: 'textbook',
+  title: '数学机经',
+  needLogin: false,
+  tab: 'examination',
+  component() {
+    return import('./page');
+  },
+};

+ 59 - 0
front/project/www/routes/textbook/list/index.less

@@ -0,0 +1,59 @@
+@charset "utf-8";
+
+#textbook-list {
+  .code-module {
+    padding: 80px 250px;
+    text-align: center;
+
+    .title {
+      font-size: 18px;
+      margin-bottom: 24px;
+    }
+
+    .input-block {
+      margin-bottom: 24px;
+
+      .input {
+        width: 350px;
+
+        input {
+          border-top-left-radius: 22px;
+          border-bottom-left-radius: 22px;
+        }
+      }
+
+      .button {
+        width: 150px;
+        border-top-right-radius: 22px;
+        border-bottom-right-radius: 22px;
+      }
+    }
+
+    .tip {
+      .left {
+        float: left;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+
+  .work-body {
+    .work-nav {
+      margin-bottom: 20px;
+
+      .left {
+        display: inline-block;
+        padding-left: 5px;
+        font-size: 16px;
+        font-weight: 600;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+}

+ 286 - 0
front/project/www/routes/textbook/list/page.js

@@ -0,0 +1,286 @@
+import React from 'react';
+import './index.less';
+import Page from '@src/containers/Page';
+import { asyncConfirm } from '@src/services/AsyncTools';
+import { formatPercent, formatSeconds, formatDate } from '@src/services/Tools';
+import Tabs from '../../../components/Tabs';
+import Module from '../../../components/Module';
+import ListTable from '../../../components/ListTable';
+import ProgressText from '../../../components/ProgressText';
+import IconButton from '../../../components/IconButton';
+import { Main } from '../../../stores/main';
+import { Question } from '../../../stores/question';
+import { QuestionDifficult } from '../../../../Constant';
+
+const LOGIC_NO = 'no';
+const LOGIC_PLACE = 'place';
+const LOGIC_DIFFICULT = 'difficult';
+const LOGIC_ERROR = 'error';
+
+export default class extends Page {
+  initState() {
+    this.columns = [
+      {
+        title: '练习册',
+        width: 250,
+        align: 'left',
+        render: (record) => {
+          let progress = 0;
+          if (record.report) {
+            progress = formatPercent(record.report.userNumber, record.report.questionNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16">{record.title}</div>
+              <div>
+                <ProgressText progress={progress} size="small" />
+              </div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '正确率',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let correct = '--';
+          if (record.report) {
+            correct = formatPercent(record.report.userCorrect, record.report.userNumber, false);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{correct}</div>
+              <div className="f-s-12">全站{formatPercent(record.stat.totalCorrect, record.stat.totalNumber, false)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '全站用时',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let time = '--';
+          if (record.paper) {
+            time = formatSeconds(record.paper.report.userTime / record.paper.report.userNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{time}</div>
+              <div className="f-s-12">全站{formatSeconds(record.stat.totalTime / record.stat.totalNumber)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '最近做题',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          if (!record.report) return null;
+          return (
+            <div className="table-row">
+              <div>{formatDate(record.report.updateTime, 'YYYY-MM-DD')}</div>
+              <div>{formatDate(record.report.updateTime, 'HH:mm')}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: 180,
+        align: 'left',
+        render: (record) => {
+          return (
+            <div className="table-row p-t-1">
+              {!record.report && <IconButton type="start" tip="Start" onClick={() => {
+                Question.startLink('exercise', record);
+              }} />}
+              {(record.report && !record.report.isFinish) && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
+                Question.continueLink('exercise', record);
+              }} />}
+              <IconButton type="restart" tip="Restart" onClick={() => {
+                this.restart(record);
+              }} />
+            </div>
+          );
+        },
+      },
+      {
+        title: '报告',
+        width: 30,
+        align: 'right',
+        render: (record) => {
+          if (!record.report || !record.report.isFinish) return null;
+          return (
+            <div className="table-row p-t-1">
+              <IconButton type="report" tip="Report" onClick={() => {
+                Question.reportLink(record);
+              }} />
+            </div>
+          );
+        },
+      },
+    ];
+    this.placeList = [];
+    this.inited = false;
+    return {
+      logic: LOGIC_NO,
+      logicExtend: '',
+      logics: [{
+        key: LOGIC_NO,
+        title: '按顺序练习',
+      }, {
+        key: LOGIC_PLACE,
+        title: '按考点练习',
+      }, {
+        key: LOGIC_DIFFICULT,
+        title: '按难度练习',
+      }, {
+        key: LOGIC_ERROR,
+        title: '按易错度练习',
+      }],
+    };
+  }
+
+  init() {
+    const { id } = this.params;
+    Main.getExerciseParent(id).then(result => {
+      const navs = result;
+      this.inited = true;
+      this.setState({ navs });
+    });
+  }
+
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    this.setState(data);
+    this.refreshData();
+  }
+
+  refreshData(newLogic) {
+    const { logic } = this.state;
+    let handler = null;
+    switch (newLogic || logic) {
+      case LOGIC_PLACE:
+        handler = this.refreshPlace();
+        break;
+      case LOGIC_DIFFICULT:
+        handler = this.refreshDifficult();
+        break;
+      default:
+        handler = Promise.resolve();
+    }
+    handler.then(() => {
+      this.refreshExercise();
+    });
+  }
+
+  refreshPlace() {
+    const { id } = this.params;
+    let handler;
+    if (this.placeList.length > 0) {
+      this.setState({ logicExtends: this.placeList });
+      handler = Promise.resolve();
+    } else {
+      handler = Question.getExercisePlace(id).then(result => {
+        this.placeList = result.map(row => {
+          return {
+            name: row,
+            key: row,
+          };
+        });
+        this.setState({ logicExtends: this.placeList });
+      });
+    }
+    return handler.then(() => {
+      let { logicExtend } = this.state;
+      if (logicExtend === '') {
+        logicExtend = this.placeList[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshDifficult() {
+    let { logicExtend } = this.state;
+    this.setState({
+      logicExtends: QuestionDifficult.map(difficult => {
+        difficult.name = difficult.label;
+        difficult.key = difficult.value;
+        return difficult;
+      }),
+    });
+    return Promise.resolve().then(() => {
+      if (logicExtend === '') {
+        logicExtend = QuestionDifficult[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshExercise() {
+    const { logic, logicExtend } = this.state;
+    Question.getExerciseList(Object.assign({ structId: this.params.id, logic, logicExtend }, this.state.search))
+      .then((result) => {
+        this.setState({ list: result.list, total: result.total });
+      });
+  }
+
+  onChangeTab(key, value) {
+    const { logic } = this.state;
+    const data = {};
+    if (key === 'logicExtend') {
+      data.logic = logic;
+      data.logicExtend = value;
+    } else {
+      data.logic = value;
+    }
+    // this.refreshData(tab);
+    this.refreshQuery(data);
+  }
+
+  restart(item) {
+    asyncConfirm('提示', '是否重置', () => {
+      Question.restart(item.paper.id).then(() => {
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    return (
+      <div>
+        <div className="content">
+          <Module className="m-t-2">
+            <Tabs
+              active={logic}
+              border
+              width="180px"
+              space="0"
+              tabs={logics}
+              onChange={(key) => {
+                this.onChangeTab('logic', key);
+              }}
+            />
+            {logicExtends.length > 0 && <Tabs
+              active={logicExtend}
+              type="text"
+              tabs={logicExtends}
+              onChange={(key) => {
+                this.onChangeTab('logicExtend', key);
+              }}
+            />}
+          </Module>
+
+          <ListTable
+            data={list}
+            columns={this.columns}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 194 - 186
front/project/www/static/login.html

@@ -1,210 +1,218 @@
 <!DOCTYPE html>
 <html lang="zh">
-  <head>
-    <meta charset="utf-8" />
-    <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
-  </head>
-  <style type="text/css">
-    body {
-      margin: 0;
-      overflow: hidden;
-      width: 300px;
-    }
-    #root {
-      overflow: hidden;
-      margin-top: -45px;
-      height: 350px;
-    }
-    .loading-bg {
-      background: #fff;
-      position: fixed;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      z-index: 1;
-      opacity: 0.9;
-    }
-    .page-loading-warp {
-      padding: 130px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      position: absolute;
-      top: 0;
-      left: 0;
-      position: fixed;
-      z-index: 2;
-    }
-    .ant-spin {
-      -webkit-box-sizing: border-box;
-      box-sizing: border-box;
-      margin: 0;
-      padding: 0;
-      color: rgba(0, 0, 0, 0.65);
-      font-size: 14px;
-      font-variant: tabular-nums;
-      line-height: 1.5;
-      list-style: none;
-      -webkit-font-feature-settings: 'tnum';
-      font-feature-settings: 'tnum';
-      position: absolute;
-      display: none;
-      color: #1890ff;
-      text-align: center;
-      vertical-align: middle;
-      opacity: 0;
-      -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-        -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-    }
 
-    .ant-spin-spinning {
-      position: static;
-      display: inline-block;
-      opacity: 1;
-    }
+<head>
+  <meta charset="utf-8" />
+  <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
+</head>
+<style type="text/css">
+  body {
+    margin: 0;
+    overflow: hidden;
+    width: 300px;
+  }
 
-    .ant-spin-dot {
-      position: relative;
-      display: inline-block;
-      font-size: 20px;
-      width: 20px;
-      height: 20px;
-    }
+  #root {
+    overflow: hidden;
+    margin-top: -45px;
+    height: 350px;
+  }
 
-    .ant-spin-dot-item {
-      position: absolute;
-      display: block;
-      width: 9px;
-      height: 9px;
-      background-color: #1890ff;
-      border-radius: 100%;
-      -webkit-transform: scale(0.75);
-      -ms-transform: scale(0.75);
-      transform: scale(0.75);
-      -webkit-transform-origin: 50% 50%;
-      -ms-transform-origin: 50% 50%;
-      transform-origin: 50% 50%;
-      opacity: 0.3;
-      -webkit-animation: antSpinMove 1s infinite linear alternate;
-      animation: antSpinMove 1s infinite linear alternate;
-    }
+  .loading-bg {
+    background: #fff;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    opacity: 0.9;
+  }
 
-    .ant-spin-dot-item:nth-child(1) {
-      top: 0;
-      left: 0;
-    }
+  .page-loading-warp {
+    padding: 130px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    top: 0;
+    left: 0;
+    position: fixed;
+    z-index: 2;
+  }
 
-    .ant-spin-dot-item:nth-child(2) {
-      top: 0;
-      right: 0;
-      -webkit-animation-delay: 0.4s;
-      animation-delay: 0.4s;
-    }
+  .ant-spin {
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    color: rgba(0, 0, 0, 0.65);
+    font-size: 14px;
+    font-variant: tabular-nums;
+    line-height: 1.5;
+    list-style: none;
+    -webkit-font-feature-settings: 'tnum';
+    font-feature-settings: 'tnum';
+    position: absolute;
+    display: none;
+    color: #1890ff;
+    text-align: center;
+    vertical-align: middle;
+    opacity: 0;
+    -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
+      -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+  }
 
-    .ant-spin-dot-item:nth-child(3) {
-      right: 0;
-      bottom: 0;
-      -webkit-animation-delay: 0.8s;
-      animation-delay: 0.8s;
-    }
+  .ant-spin-spinning {
+    position: static;
+    display: inline-block;
+    opacity: 1;
+  }
 
-    .ant-spin-dot-item:nth-child(4) {
-      bottom: 0;
-      left: 0;
-      -webkit-animation-delay: 1.2s;
-      animation-delay: 1.2s;
-    }
+  .ant-spin-dot {
+    position: relative;
+    display: inline-block;
+    font-size: 20px;
+    width: 20px;
+    height: 20px;
+  }
 
-    .ant-spin-dot-spin {
-      -webkit-transform: rotate(45deg);
-      -ms-transform: rotate(45deg);
-      transform: rotate(45deg);
-      -webkit-animation: antRotate 1.2s infinite linear;
-      animation: antRotate 1.2s infinite linear;
-    }
+  .ant-spin-dot-item {
+    position: absolute;
+    display: block;
+    width: 9px;
+    height: 9px;
+    background-color: #1890ff;
+    border-radius: 100%;
+    -webkit-transform: scale(0.75);
+    -ms-transform: scale(0.75);
+    transform: scale(0.75);
+    -webkit-transform-origin: 50% 50%;
+    -ms-transform-origin: 50% 50%;
+    transform-origin: 50% 50%;
+    opacity: 0.3;
+    -webkit-animation: antSpinMove 1s infinite linear alternate;
+    animation: antSpinMove 1s infinite linear alternate;
+  }
 
-    .ant-spin-lg .ant-spin-dot {
-      font-size: 32px;
-      width: 32px;
-      height: 32px;
-    }
+  .ant-spin-dot-item:nth-child(1) {
+    top: 0;
+    left: 0;
+  }
 
-    .ant-spin-lg .ant-spin-dot i {
-      width: 14px;
-      height: 14px;
-    }
+  .ant-spin-dot-item:nth-child(2) {
+    top: 0;
+    right: 0;
+    -webkit-animation-delay: 0.4s;
+    animation-delay: 0.4s;
+  }
+
+  .ant-spin-dot-item:nth-child(3) {
+    right: 0;
+    bottom: 0;
+    -webkit-animation-delay: 0.8s;
+    animation-delay: 0.8s;
+  }
+
+  .ant-spin-dot-item:nth-child(4) {
+    bottom: 0;
+    left: 0;
+    -webkit-animation-delay: 1.2s;
+    animation-delay: 1.2s;
+  }
+
+  .ant-spin-dot-spin {
+    -webkit-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+    -webkit-animation: antRotate 1.2s infinite linear;
+    animation: antRotate 1.2s infinite linear;
+  }
+
+  .ant-spin-lg .ant-spin-dot {
+    font-size: 32px;
+    width: 32px;
+    height: 32px;
+  }
+
+  .ant-spin-lg .ant-spin-dot i {
+    width: 14px;
+    height: 14px;
+  }
 
-    @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
-      .ant-spin-blur {
-        background: #fff;
-        opacity: 0.5;
-      }
+  @media all and (-ms-high-contrast: none),
+  (-ms-high-contrast: active) {
+    .ant-spin-blur {
+      background: #fff;
+      opacity: 0.5;
     }
+  }
 
-    @-webkit-keyframes antSpinMove {
-      to {
-        opacity: 1;
-      }
+  @-webkit-keyframes antSpinMove {
+    to {
+      opacity: 1;
     }
+  }
 
-    @keyframes antSpinMove {
-      to {
-        opacity: 1;
-      }
+  @keyframes antSpinMove {
+    to {
+      opacity: 1;
     }
+  }
 
-    @-webkit-keyframes antRotate {
-      to {
-        -webkit-transform: rotate(405deg);
-        transform: rotate(405deg);
-      }
+  @-webkit-keyframes antRotate {
+    to {
+      -webkit-transform: rotate(405deg);
+      transform: rotate(405deg);
     }
+  }
 
-    @keyframes antRotate {
-      to {
-        -webkit-transform: rotate(405deg);
-        transform: rotate(405deg);
-      }
+  @keyframes antRotate {
+    to {
+      -webkit-transform: rotate(405deg);
+      transform: rotate(405deg);
     }
-  </style>
-  <body>
-    <div id="root"></div>
-    <div id="loading">
-      <div class="loading-bg"></div>
-      <div class="page-loading-warp">
-        <div class="ant-spin ant-spin-lg ant-spin-spinning">
-          <span class="ant-spin-dot ant-spin-dot-spin">
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-          </span>
-        </div>
+  }
+</style>
+
+<body>
+  <div id="root"></div>
+  <div id="loading">
+    <div class="loading-bg"></div>
+    <div class="page-loading-warp">
+      <div class="ant-spin ant-spin-lg ant-spin-spinning">
+        <span class="ant-spin-dot ant-spin-dot-spin">
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+        </span>
       </div>
     </div>
-  </body>
-  <script>
-    function getQuery(name) {
-      var reg = new RegExp(`(^|\\?|&)${name}=([^&]*)(&|$)`);
-      var r = window.location.href.substr(1).match(reg);
-      if (r != null) return unescape(r[2]);
-      return null;
-    }
-    var code = getQuery('code');
-    if (code) {
-      window.parent.postMessage('code:' + code, '*');
-    } else {
-      document.getElementById('loading').style.display = 'none';
-    }
-    new WxLogin({
-      id: 'root',
-      appid: 'wx8fa48dfc3752f0ce',
-      scope: 'snsapi_login',
-      redirect_uri: encodeURIComponent('http://www.scigou.com'),
-    });
-  </script>
-</html>
+  </div>
+</body>
+<script>
+  function getQuery(name) {
+    var reg = new RegExp(`(^|\\?|&)${name}=([^&]*)(&|$)`);
+    var r = window.location.href.substr(1).match(reg);
+    if (r != null) return unescape(r[2]);
+    return null;
+  }
+  var code = getQuery('code');
+  if (code) {
+    window.parent.postMessage('code:' + code, '*');
+  } else {
+    document.getElementById('loading').style.display = 'none';
+  }
+  new WxLogin({
+    id: 'root',
+    appid: getQuery('appid'),
+    scope: 'snsapi_login',
+    redirect_uri: getQuery('redirectUri'),
+  });
+</script>
+
+</html>

+ 48 - 4
front/project/www/stores/course.js

@@ -2,18 +2,62 @@ import BaseStore from '@src/stores/base';
 
 export default class CourseStore extends BaseStore {
   /**
+   * 所有vs课程
+   */
+  allVs() {
+    return this.apiGet('/course/vs');
+  }
+
+  listVideo(params) {
+    return this.apiGet('/course/video/list', params);
+  }
+
+  get(courseId) {
+    return this.apiGet('/course/simple', { courseId });
+  }
+
+  listPackage(params) {
+    return this.apiGet('/course/package/list', params);
+  }
+
+  getPackage(packageId) {
+    return this.apiGet('/course/package/detail', { packageId });
+  }
+
+  listData(params) {
+    return this.apiGet('/course/data/list', params);
+  }
+
+  getData(dataId) {
+    return this.apiGet('/course/data/detail', { dataId });
+  }
+
+  listExperience({ page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction }) {
+    return this.apiGet('/course/experience/list', { page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction });
+  }
+
+  /**
    * 获取课程进度
    */
-  progress(courseModules, structId) {
-    return this.apiGet('/course/progress', { courseModules, structId });
+  progress(courseModule, structId, courseId) {
+    return this.apiGet('/course/progress', { courseModule, structId, courseId });
   }
 
+
   /**
    * 获取预习作业列表
    * @param {*} param0
    */
-  listPreview({ page, size, isFinish, endTime }) {
-    return this.apiGet('/course/preview', { page, size, isFinish, endTime });
+  listPreview({ page, size, recordId, finish, endTime }) {
+    return this.apiGet('/course/preview/list', { page, size, recordId, endTime, times: finish });
+  }
+
+  /**
+   * 课程记录信息
+   * @param {*} recordId
+   */
+  record(recordId) {
+    return this.apiGet('/course/record', { recordId });
   }
 }
 

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

@@ -84,6 +84,14 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 获取每周学习记录
+   * @param {*} week 0本周,1上周
+   */
+  getStudyWeek(week) {
+    return this.apiGet('/my/study/week', { week });
+  }
+
+  /**
    * 获取总学习记录
    */
   getStudyTotal() {
@@ -170,7 +178,7 @@ export default class MyStore extends BaseStore {
   }
 
   /**
-   * 更新笔记
+   * 更新题目笔记
    * @param {*} questionModule
    * @param {*} questionNoId
    * @param {*} content
@@ -184,6 +192,16 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 更新课程笔记
+   * @param {*} courseId
+   * @param {*} courseNoId
+   * @param {*} content
+   */
+  updateCourseNote(courseId, courseNoId, content) {
+    return this.apiPut('/my/note/course', { courseId, courseNoId, content });
+  }
+
+  /**
    * 获取笔记列表
    * @param {*} questionModule
    * @param {*} questionType
@@ -214,7 +232,7 @@ export default class MyStore extends BaseStore {
   }
 
   /**
-   * 添加提问
+   * 添加题目提问
    * @param {*} target
    * @param {*} questionModule
    * @param {*} questionNoId
@@ -225,6 +243,17 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 添加课程提问
+   * @param {*} courseId
+   * @param {*} courseNoId
+   * @param {*} position
+   * @param {*} content
+   */
+  addCourseAsk(courseId, courseNoId, position, content) {
+    return this.apiPost('/my/ask/course', { courseId, courseNoId, position, content });
+  }
+
+  /**
    * 添加题目勘误
    * @param {*} moduleId
    * @param {*} title
@@ -247,6 +276,16 @@ export default class MyStore extends BaseStore {
   addErrorData(moduleId, title, position, originContent, content) {
     return this.apiPost('/my/feedback/error/question', { moduleId, title, position, originContent, content });
   }
+
+  /**
+   * 添加机经反馈
+   * @param {*} topicId
+   * @param {*} target
+   * @param {*} content
+   */
+  addTextbookFeedback(topicId, target, content) {
+    return this.apiPost('/my/feedback/textbook', { topicId, target, content });
+  }
 }
 
 export const My = new MyStore({ key: 'my' });

+ 32 - 4
front/project/www/stores/order.js

@@ -6,8 +6,8 @@ export default class OrderStore extends BaseStore {
     return this.apiGet('/order/checkout/all');
   }
 
-  addCheckout(productType, productId, service, param) {
-    return this.apiPost('/order/checkout/add', { productType, productId, service, param });
+  addCheckout({ productType, productId, service, param, number }) {
+    return this.apiPost('/order/checkout/add', { productType, productId, service, param, number });
   }
 
   removeCheckout(checkoutId) {
@@ -18,8 +18,8 @@ export default class OrderStore extends BaseStore {
     return this.apiPost('/order/pay/confirm');
   }
 
-  speedPay(productType, productId, service, param) {
-    return this.apiPost('/order/pay/speed', { productType, productId, service, param });
+  speedPay({ productType, productId, service, param, number }) {
+    return this.apiPost('/order/pay/speed', { productType, productId, service, param, number });
   }
 
   wechatQr(orderId) {
@@ -33,6 +33,34 @@ export default class OrderStore extends BaseStore {
   alipayQr(orderId) {
     return this.apiPost('/order/alipay/qr', { orderId });
   }
+
+  query(orderId) {
+    return this.apiGet('/order/pay/query', { orderId });
+  }
+
+  /**
+   * 获取所有已购记录
+   * @param {*} param0
+   */
+  listRecord({ page, size }) {
+    return this.apiGet('/my/record/list', { page, size });
+  }
+
+  /**
+   * 获取订单记录
+   * @param {*} id
+   */
+  getRecord(id) {
+    return this.apiGet('/my/record/detail', { id });
+  }
+
+  /**
+   * 开通服务、课程等
+   * @param {*} id
+   */
+  useRecord(id, isSubscribe) {
+    return this.apiPost('/my/record/use', { id, isSubscribe });
+  }
 }
 
 export const Order = new OrderStore({ key: 'order' });

+ 11 - 12
front/project/www/stores/question.js

@@ -34,6 +34,14 @@ export default class QuestionStore extends BaseStore {
   }
 
   /**
+   * 模考进度
+   * @param {*} structId
+   */
+  getExaminationProgress(structId) {
+    return this.apiGet('/question/examination/progress', { structId });
+  }
+
+  /**
    * 练习组卷
    * @param {*} page
    * @param {*} size
@@ -43,16 +51,7 @@ export default class QuestionStore extends BaseStore {
    * @param {*} finish: true完成,false未完成
    */
   getExerciseList({ page, size, structId, logic, logicExtend, finish }) {
-    return this.apiGet('/question/exercise/list', { page, size, structId, logic, logicExtend, times: finish ? 1 : null });
-  }
-
-  /**
-   * 模考进度
-   * @param {*} page
-   * @param {*} size
-   */
-  getExaminationProgress(page, size) {
-    return this.apiGet('/question/examination/progress', { page, size });
+    return this.apiGet('/question/exercise/list', { page, size, structId, logic, logicExtend, times: finish });
   }
 
   /**
@@ -60,8 +59,8 @@ export default class QuestionStore extends BaseStore {
    * @param {*} page
    * @param {*} size
    */
-  getExaminationList(page, size) {
-    return this.apiGet('/question/examination/list', { page, size });
+  getExaminationList(page, size, structId, finish) {
+    return this.apiGet('/question/examination/list', { page, size, structId, times: finish });
   }
 
   /**

+ 14 - 0
front/project/www/stores/textbook.js

@@ -8,6 +8,20 @@ export default class TextbookStore extends BaseStore {
     return this.apiGet('/textbook/info');
   }
 
+  /**
+   * 获取机经进度
+   */
+  progress() {
+    return this.apiGet('/textbook/progress');
+  }
+
+  /**
+   * 机经组卷列表
+   */
+  listPaper(page, size, latest, logic, finish) {
+    return this.apiGet('/textbook/paper', { page, size, latest: !!latest, logic, times: finish });
+  }
+
   listYear(year) {
     return this.apiGet('/textbook/year', { year });
   }

+ 20 - 0
front/src/services/Tools.js

@@ -232,6 +232,26 @@ export function formatSecond(value) {
   return `${hourTime}:${minuteTime}:${secondTime}`;
 }
 
+export function formatMinuteSecond(value) {
+  let secondTime = parseInt(value || 0, 10); // 秒
+  let minuteTime = 0;
+  if (secondTime > 60) {
+    minuteTime = parseInt(secondTime / 60, 10);
+    secondTime = parseInt(secondTime % 60, 10);
+  }
+  if (minuteTime >= 10) {
+    minuteTime = `${minuteTime}`;
+  } else {
+    minuteTime = `0${minuteTime}`;
+  }
+  if (secondTime >= 10) {
+    secondTime = `${secondTime}`;
+  } else {
+    secondTime = `0${secondTime}`;
+  }
+  return `${minuteTime}:${secondTime}`;
+}
+
 export function formatFormError(data, err, prefix = '') {
   const r = {};
   Object.keys(err).forEach(field => {

+ 5 - 1
server/data/src/main/java/com/qxgmat/data/constants/enums/ServiceKey.java

@@ -22,6 +22,10 @@ public enum ServiceKey {
 
     public static ServiceKey ValueOf(String name){
         if (name == null || name.isEmpty()) return null;
-        return ServiceKey.valueOf(name.toUpperCase());
+        try{
+            return ServiceKey.valueOf(name.toUpperCase());
+        }catch (Exception e){
+            return null;
+        }
     }
 }

+ 3 - 2
server/data/src/main/java/com/qxgmat/data/constants/enums/module/PaperOrigin.java

@@ -4,11 +4,12 @@ package com.qxgmat.data.constants.enums.module;
 public enum PaperOrigin {
     EXERCISE("exercise"),
     EXAMINATION("examination"),
+    TEXTBOOK("textbook"),
+    SENTENCE("sentence"),
+
     COLLECT("collect"),
     ERROR("error"),
     PREVIEW("preview"),
-    TEXTBOOK("textbook"),
-    SENTENCE("sentence"),
     ;
     public String key;
     private PaperOrigin(String key){

+ 17 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/module/QuestionNoModule.java

@@ -0,0 +1,17 @@
+package com.qxgmat.data.constants.enums.module;
+
+public enum QuestionNoModule {
+    EXERCISE("exercise"),
+    EXAMINATION("examination"),
+    ;
+    public String key;
+    private QuestionNoModule(String key){
+        this.key = key;
+    }
+
+    public static QuestionNoModule ValueOf(String name){
+        if (name == null) return null;
+        return QuestionNoModule.valueOf(name.toUpperCase());
+    }
+
+}

+ 18 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/module/VideoCourseType.java

@@ -0,0 +1,18 @@
+package com.qxgmat.data.constants.enums.module;
+
+public enum VideoCourseType {
+    BASE("base"),
+    THINKING("thinking"),
+    SYSTEM("system"),
+
+    ;
+    public String key;
+    private VideoCourseType(String key){
+        this.key = key;
+    }
+
+    public static VideoCourseType ValueOf(String name){
+        if (name == null) return null;
+        return VideoCourseType.valueOf(name.toUpperCase());
+    }
+}

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

@@ -115,6 +115,12 @@ public class Course implements Serializable {
     private Integer expireDays;
 
     /**
+     * 提问扩展天数
+     */
+    @Column(name = "`ask_extend_days`")
+    private Integer askExtendDays;
+
+    /**
      * 使用有效时长
      */
     @Column(name = "`use_expire_time`")
@@ -539,6 +545,24 @@ public class Course implements Serializable {
     }
 
     /**
+     * 获取提问扩展天数
+     *
+     * @return ask_extend_days - 提问扩展天数
+     */
+    public Integer getAskExtendDays() {
+        return askExtendDays;
+    }
+
+    /**
+     * 设置提问扩展天数
+     *
+     * @param askExtendDays 提问扩展天数
+     */
+    public void setAskExtendDays(Integer askExtendDays) {
+        this.askExtendDays = askExtendDays;
+    }
+
+    /**
      * 获取使用有效时长
      *
      * @return use_expire_time - 使用有效时长
@@ -878,6 +902,7 @@ public class Course implements Serializable {
         sb.append(", maxNumber=").append(maxNumber);
         sb.append(", expirePreDays=").append(expirePreDays);
         sb.append(", expireDays=").append(expireDays);
+        sb.append(", askExtendDays=").append(askExtendDays);
         sb.append(", useExpireTime=").append(useExpireTime);
         sb.append(", wechatAvatar=").append(wechatAvatar);
         sb.append(", trailNumber=").append(trailNumber);
@@ -1110,6 +1135,16 @@ public class Course implements Serializable {
         }
 
         /**
+         * 设置提问扩展天数
+         *
+         * @param askExtendDays 提问扩展天数
+         */
+        public Builder askExtendDays(Integer askExtendDays) {
+            obj.setAskExtendDays(askExtendDays);
+            return this;
+        }
+
+        /**
          * 设置使用有效时长
          *
          * @param useExpireTime 使用有效时长

+ 70 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/User.java

@@ -190,6 +190,18 @@ public class User implements Serializable {
     private Integer inviteNumber;
 
     /**
+     * 半价机经券
+     */
+    @Column(name = "`textbook_half`")
+    private Integer textbookHalf;
+
+    /**
+     * 千行cat次数
+     */
+    @Column(name = "`qx_cat`")
+    private Integer qxCat;
+
+    /**
      * 注册ip
      */
     @Column(name = "`register_ip`")
@@ -793,6 +805,42 @@ public class User implements Serializable {
     }
 
     /**
+     * 获取半价机经券
+     *
+     * @return textbook_half - 半价机经券
+     */
+    public Integer getTextbookHalf() {
+        return textbookHalf;
+    }
+
+    /**
+     * 设置半价机经券
+     *
+     * @param textbookHalf 半价机经券
+     */
+    public void setTextbookHalf(Integer textbookHalf) {
+        this.textbookHalf = textbookHalf;
+    }
+
+    /**
+     * 获取千行cat次数
+     *
+     * @return qx_cat - 千行cat次数
+     */
+    public Integer getQxCat() {
+        return qxCat;
+    }
+
+    /**
+     * 设置千行cat次数
+     *
+     * @param qxCat 千行cat次数
+     */
+    public void setQxCat(Integer qxCat) {
+        this.qxCat = qxCat;
+    }
+
+    /**
      * 获取注册ip
      *
      * @return register_ip - 注册ip
@@ -987,6 +1035,8 @@ public class User implements Serializable {
         sb.append(", inviteCode=").append(inviteCode);
         sb.append(", totalMoney=").append(totalMoney);
         sb.append(", inviteNumber=").append(inviteNumber);
+        sb.append(", textbookHalf=").append(textbookHalf);
+        sb.append(", qxCat=").append(qxCat);
         sb.append(", registerIp=").append(registerIp);
         sb.append(", registerCity=").append(registerCity);
         sb.append(", latestLoginIp=").append(latestLoginIp);
@@ -1318,6 +1368,26 @@ public class User implements Serializable {
         }
 
         /**
+         * 设置半价机经券
+         *
+         * @param textbookHalf 半价机经券
+         */
+        public Builder textbookHalf(Integer textbookHalf) {
+            obj.setTextbookHalf(textbookHalf);
+            return this;
+        }
+
+        /**
+         * 设置千行cat次数
+         *
+         * @param qxCat 千行cat次数
+         */
+        public Builder qxCat(Integer qxCat) {
+            obj.setQxCat(qxCat);
+            return this;
+        }
+
+        /**
          * 设置注册ip
          *
          * @param registerIp 注册ip

+ 12 - 12
server/data/src/main/java/com/qxgmat/data/dao/entity/UserNoteCourse.java

@@ -35,8 +35,8 @@ public class UserNoteCourse implements Serializable {
     @Column(name = "`update_time`")
     private Date updateTime;
 
-    @Column(name = "`conntent`")
-    private String conntent;
+    @Column(name = "`content`")
+    private String content;
 
     private static final long serialVersionUID = 1L;
 
@@ -137,17 +137,17 @@ public class UserNoteCourse implements Serializable {
     }
 
     /**
-     * @return conntent
+     * @return content
      */
-    public String getConntent() {
-        return conntent;
+    public String getContent() {
+        return content;
     }
 
     /**
-     * @param conntent
+     * @param content
      */
-    public void setConntent(String conntent) {
-        this.conntent = conntent;
+    public void setContent(String content) {
+        this.content = content;
     }
 
     @Override
@@ -162,7 +162,7 @@ public class UserNoteCourse implements Serializable {
         sb.append(", courseNoId=").append(courseNoId);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
-        sb.append(", conntent=").append(conntent);
+        sb.append(", content=").append(content);
         sb.append("]");
         return sb.toString();
     }
@@ -233,10 +233,10 @@ public class UserNoteCourse implements Serializable {
         }
 
         /**
-         * @param conntent
+         * @param content
          */
-        public Builder conntent(String conntent) {
-            obj.setConntent(conntent);
+        public Builder content(String content) {
+            obj.setContent(content);
             return this;
         }
 

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

@@ -54,6 +54,12 @@ public class UserPaper implements Serializable {
     private Integer recordId;
 
     /**
+     * 相同试卷不同编号:对应千行cat次数
+     */
+    @Column(name = "`paper_no`")
+    private Integer paperNo;
+
+    /**
      * 题目编号id列表:json
      */
     @Column(name = "`question_no_ids`")
@@ -256,6 +262,24 @@ public class UserPaper implements Serializable {
     }
 
     /**
+     * 获取相同试卷不同编号:对应千行cat次数
+     *
+     * @return paper_no - 相同试卷不同编号:对应千行cat次数
+     */
+    public Integer getPaperNo() {
+        return paperNo;
+    }
+
+    /**
+     * 设置相同试卷不同编号:对应千行cat次数
+     *
+     * @param paperNo 相同试卷不同编号:对应千行cat次数
+     */
+    public void setPaperNo(Integer paperNo) {
+        this.paperNo = paperNo;
+    }
+
+    /**
      * 获取题目编号id列表:json
      *
      * @return question_no_ids - 题目编号id列表:json
@@ -449,6 +473,7 @@ public class UserPaper implements Serializable {
         sb.append(", isAdapt=").append(isAdapt);
         sb.append(", originId=").append(originId);
         sb.append(", recordId=").append(recordId);
+        sb.append(", paperNo=").append(paperNo);
         sb.append(", questionNoIds=").append(questionNoIds);
         sb.append(", questionNumber=").append(questionNumber);
         sb.append(", times=").append(times);
@@ -553,6 +578,16 @@ public class UserPaper implements Serializable {
         }
 
         /**
+         * 设置相同试卷不同编号:对应千行cat次数
+         *
+         * @param paperNo 相同试卷不同编号:对应千行cat次数
+         */
+        public Builder paperNo(Integer paperNo) {
+            obj.setPaperNo(paperNo);
+            return this;
+        }
+
+        /**
          * 设置题目编号id列表:json
          *
          * @param questionNoIds 题目编号id列表:json

+ 4 - 2
server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseMapper.xml

@@ -23,6 +23,7 @@
     <result column="max_number" jdbcType="INTEGER" property="maxNumber" />
     <result column="expire_pre_days" jdbcType="INTEGER" property="expirePreDays" />
     <result column="expire_days" jdbcType="INTEGER" property="expireDays" />
+    <result column="ask_extend_days" jdbcType="INTEGER" property="askExtendDays" />
     <result column="use_expire_time" jdbcType="INTEGER" property="useExpireTime" />
     <result column="wechat_avatar" jdbcType="VARCHAR" property="wechatAvatar" />
     <result column="trail_number" jdbcType="INTEGER" property="trailNumber" />
@@ -53,8 +54,9 @@
     -->
     `id`, `struct_id`, `parent_struct_id`, `course_module`, `no_number`, `vs_type`, `video_type`, 
     `extend`, `title`, `comment`, `crowd`, `price`, `teacher`, `cover`, `min_number`, 
-    `max_number`, `expire_pre_days`, `expire_days`, `use_expire_time`, `wechat_avatar`, 
-    `trail_number`, `sale_number`, `package_sale_number`, `create_time`, `update_time`
+    `max_number`, `expire_pre_days`, `expire_days`, `ask_extend_days`, `use_expire_time`, 
+    `wechat_avatar`, `trail_number`, `sale_number`, `package_sale_number`, `create_time`, 
+    `update_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--

+ 5 - 3
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMapper.xml

@@ -36,6 +36,8 @@
     <result column="invite_code" jdbcType="VARCHAR" property="inviteCode" />
     <result column="total_money" jdbcType="DECIMAL" property="totalMoney" />
     <result column="invite_number" jdbcType="INTEGER" property="inviteNumber" />
+    <result column="textbook_half" jdbcType="INTEGER" property="textbookHalf" />
+    <result column="qx_cat" jdbcType="INTEGER" property="qxCat" />
     <result column="register_ip" jdbcType="VARCHAR" property="registerIp" />
     <result column="register_city" jdbcType="VARCHAR" property="registerCity" />
     <result column="latest_login_ip" jdbcType="VARCHAR" property="latestLoginIp" />
@@ -55,8 +57,8 @@
     `wechat_expire_time`, `real_time`, `real_name`, `real_address`, `real_identity`, 
     `real_photo_front`, `real_photo_back`, `real_status`, `prepare_time`, `prepare_status`, 
     `prepare_goal`, `prepare_examination_time`, `prepare_score_time`, `latest_exercise`, 
-    `latest_error`, `origin_id`, `invite_code`, `total_money`, `invite_number`, `register_ip`, 
-    `register_city`, `latest_login_ip`, `latest_login_time`, `is_frozen`, `create_time`, 
-    `data_email_subscribe`, `textbook_email_subscribe`, `total_alert`
+    `latest_error`, `origin_id`, `invite_code`, `total_money`, `invite_number`, `textbook_half`, 
+    `qx_cat`, `register_ip`, `register_city`, `latest_login_ip`, `latest_login_time`, 
+    `is_frozen`, `create_time`, `data_email_subscribe`, `textbook_email_subscribe`, `total_alert`
   </sql>
 </mapper>

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

@@ -16,7 +16,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <result column="conntent" jdbcType="LONGVARCHAR" property="conntent" />
+    <result column="content" jdbcType="LONGVARCHAR" property="content" />
   </resultMap>
   <sql id="Base_Column_List">
     <!--
@@ -28,6 +28,6 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `conntent`
+    `content`
   </sql>
 </mapper>

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

@@ -13,6 +13,7 @@
     <result column="is_adapt" jdbcType="INTEGER" property="isAdapt" />
     <result column="origin_id" jdbcType="INTEGER" property="originId" />
     <result column="record_id" jdbcType="INTEGER" property="recordId" />
+    <result column="paper_no" jdbcType="INTEGER" property="paperNo" />
     <result column="question_no_ids" jdbcType="VARCHAR" property="questionNoIds" typeHandler="com.nuliji.tools.mybatis.handler.IntegerArrayWithJsonHandler" />
     <result column="question_number" jdbcType="INTEGER" property="questionNumber" />
     <result column="times" jdbcType="INTEGER" property="times" />
@@ -29,7 +30,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `user_id`, `title`, `paper_module`, `paper_origin`, `is_adapt`, `origin_id`, 
-    `record_id`, `question_no_ids`, `question_number`, `times`, `time`, `latest_time`, 
+    `record_id`, `paper_no`, `question_no_ids`, `question_number`, `times`, `time`, `latest_time`, 
     `total_time`, `total_number`, `total_correct`, `delete_time`, `is_reset`
   </sql>
 </mapper>

+ 1 - 0
server/data/src/main/java/com/qxgmat/data/relation/ExaminationPaperRelationMapper.java

@@ -13,6 +13,7 @@ public interface ExaminationPaperRelationMapper {
     List<ExercisePaper> listWithUser(
             @Param("structId") Number structId,
             @Param("userId") Number userId,
+            @Param("qxCatNo") Integer qxCatNo,
             @Param("times") Integer times
     );
 }

+ 4 - 0
server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java

@@ -31,6 +31,10 @@ public interface UserReportRelationMapper {
             @Param("paperIds") Collection paperIds
     );
 
+    List<UserReport> listLastNoReset(
+            @Param("paperIds") Collection paperIds
+    );
+
     List<UserReportLimitRelation> statLimit(
             @Param("module") String module,
             @Param("moduleId") Integer moduleId

+ 15 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/ExaminationPaperRelationMapper.xml

@@ -25,8 +25,16 @@
     left join `user_paper` up on ep.`id` = up.`origin_id`
       and up.`paper_origin` = 'examination'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
+      <if test="qxCatNo != null">
+        and up.`qx_cat_no` = #{qxCatNo,jdbcType=VARCHAR}
+      </if>
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -34,7 +42,12 @@
       and (ep.`struct_three` = #{structId,jdbcType=VARCHAR} or ep.`struct_four` = #{structId,jdbcType=VARCHAR})
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
   </select>
 </mapper>

+ 12 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/ExercisePaperRelationMapper.xml

@@ -38,7 +38,12 @@
       and up.`paper_origin` = 'exercise'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -46,7 +51,12 @@
       and (ep.`struct_three` = #{structId,jdbcType=VARCHAR} or ep.`struct_four` = #{structId,jdbcType=VARCHAR})
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
     <if test="logic != null">
       and ep.`logic` = #{logic,jdbcType=VARCHAR}

+ 43 - 13
server/data/src/main/java/com/qxgmat/data/relation/mapping/PreviewAssignRelationMapper.xml

@@ -11,7 +11,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    ep.`id`
+    pa.`id`
   </sql>
 
   <!--
@@ -22,11 +22,16 @@
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-    left join `user_paper` up on ep.`id` = up.`origin_id`
+    left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -34,23 +39,33 @@
       and pa.`course_id` = #{courseId,jdbcType=VARCHAR}
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
   </select>
 
   <!--
-   1v1课程作业列表
+   小班课程作业列表
   -->
   <select id="listByAppointment" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-      left join `user_paper` up on ep.`id` = up.`origin_id`
+      left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -64,23 +79,33 @@
       </foreach>
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
   </select>
 
   <!--
-   小班课程作业列表
+   1v1课程作业列表
   -->
-  <select id="listByAppointment" resultMap="IdMap">
+  <select id="listByTime" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-      left join `user_paper` up on ep.`id` = up.`origin_id`
+      left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -91,7 +116,12 @@
       and pa.`course_time` = #{timeId,jdbcType=VARCHAR}
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
   </select>
 </mapper>

+ 12 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/TextbookPaperRelationMapper.xml

@@ -26,7 +26,12 @@
       and up.`paper_origin` = 'textbook'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
-        and up.`times` >= #{times,jdbcType=VARCHAR}
+        <if test="times == 0">
+          and up.`times` = 0
+        </if>
+        <if test="times > 0">
+          and up.`times` >= #{times,jdbcType=VARCHAR}
+        </if>
       </if>
     </if>
     where 1
@@ -34,7 +39,12 @@
       and tp.`library_id` = #{libraryId,jdbcType=VARCHAR}
     </if>
     <if test="userId != null">
-      and up.`id` &gt; 0
+      <if test="times == 0">
+        and (up.`id` &gt; 0 or up.`id` is null)
+      </if>
+      <if test="times > 0">
+        and up.`id` &gt; 0
+      </if>
     </if>
   </select>
 </mapper>

+ 4 - 4
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRecordRelationMapper.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.qxgmat.data.relation.UserOrderRecordRelationMapper">
-  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.UserNoteQuestion">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.UserOrderRecord">
     <!--
       WARNING - @mbg.generated
     -->
@@ -18,7 +18,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    up.`id`
+    uor.`id`
   </sql>
 
   <!--
@@ -26,7 +26,7 @@
   -->
   <select id="groupByTime" resultMap="NumberMap">
     select
-    count(cs.`id`) as `number`, cs.`no` as `id`
+    count(uor.`id`) as `number`, uor.`no` as `id`
     from `user_order_record` uor
     where
     uor.`no` IN
@@ -44,7 +44,7 @@
     from `user_order_record` uor
     left join `course` c on c.`id` = uor.`product_id` and uor.`product_type` = 'course'
     <if test="courseModules != null">
-      c.`course_module` IN
+      and c.`course_module` IN
       <foreach collection="courseModules" item="item" index="index" open="(" close=")" separator=",">
         #{item, jdbcType=VARCHAR}
       </foreach>

+ 25 - 0
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml

@@ -97,6 +97,31 @@
   </select>
 
   <!--
+    用户最后一次
+    https://blog.csdn.net/t_1007/article/details/52369261
+  -->
+  <select id="listLastNoReset" resultMap="IdMap">
+    select
+    SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`
+    from `user_report` ur
+    left join `user_paper` up on up.`id` = ur.`paper_id`
+    where
+    up.`id` &gt; 0
+    and ur.`paper_id` IN
+    <foreach collection="paperIds" item="item" index="index" open="(" close=")" separator=",">
+      #{item}
+    </foreach>
+
+    <!--<if test="startTime != null">-->
+    <!--and up.`createTime` &gt; #{startTime,jdbcType=VARCHAR}-->
+    <!--</if>-->
+    <!--<if test="endTime != null">-->
+    <!--and up.`createTime` &lt; #{endTime,jdbcType=VARCHAR}-->
+    <!--</if>-->
+    group by ur.`paper_id`
+  </select>
+
+  <!--
     用户限时完成的统计记录
   -->
   <select id="statLimit" resultMap="limitMap">

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java

@@ -22,6 +22,7 @@ import com.qxgmat.service.ManagerService;
 import com.qxgmat.service.UsersService;
 import com.qxgmat.service.extend.CourseExtendService;
 import com.qxgmat.service.extend.OrderFlowService;
+import com.qxgmat.service.extend.ToolsService;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,6 +32,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import javax.xml.transform.TransformerFactory;
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -93,6 +95,9 @@ public class CourseController {
     @Autowired
     private OrderFlowService orderFlowService;
 
+    @Autowired
+    private ToolsService toolsService;
+
 
     @RequestMapping(value = "/add", method = RequestMethod.POST)
     @ApiOperation(value = "添加课程", httpMethod = "POST")
@@ -528,6 +533,11 @@ public class CourseController {
         entity.setUseTime(new Date());
         entity.setStartTime(courseTime.getStartTime());
         entity.setEndTime(courseTime.getEndTime());
+
+        // 默认回答时间
+        Integer ask = toolsService.getAskTime(BigDecimal.valueOf(0));
+        entity.setAskTime(ask);
+
         entity = userOrderRecordService.addStudent(entity);
         managerLogService.log(request);
         return ResponseHelp.success(Transform.convert(entity, UserOrderRecord.class));

+ 19 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java

@@ -13,6 +13,7 @@ import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.dto.extend.CourseExtendDto;
+import com.qxgmat.dto.extend.CourseTeacherExtendDto;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
@@ -123,11 +124,12 @@ public class CourseController {
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = false) Integer structId,
+            @RequestParam(required = false) Boolean isSpecial,
             @RequestParam(required = false, defaultValue = "id") String order,
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session) {
 
-        Page<CoursePackage> p = coursePackageService.list(page, size, structId, order, DirectionStatus.ValueOf(direction));
+        Page<CoursePackage> p = coursePackageService.list(page, size, structId, isSpecial, order, DirectionStatus.ValueOf(direction));
 
         List<CoursePackageListDto> pr = Transform.convert(p, CoursePackageListDto.class);
 
@@ -245,10 +247,14 @@ public class CourseController {
             @RequestParam(required = false) Integer courseId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null){
+            throw new ParameterException("登录后查看作业");
+        }
         List<UserOrderRecord> userOrderRecordList;
         CourseModule module = CourseModule.ValueOf(courseModule);
-        if (module == CourseModule.VIDEO){
-            // 视频课程包含:小班课程
+
+        if (module == CourseModule.ONLINE){
+            // 在线课程包含:视频课程、小班课程
             userOrderRecordList = userOrderRecordService.listWithStudyAdmin(1, 1000, new String[]{CourseModule.VIDEO.key, CourseModule.ONLINE.key}, structId, courseId, user.getId(), null,null, null);
         } else if (module == CourseModule.VS){
             // 1v1课程:只有系统授课有作业
@@ -268,7 +274,13 @@ public class CourseController {
         List<UserOrderRecord> processList = userOrderRecordList.stream().filter(row-> row.getIsUsed() > 0 && row.getUseEndTime().after(now)).collect(Collectors.toList());
         Collection processIds = Transform.getIds(processList, UserOrderRecord.class, "id");
         Map<Object, Collection<UserPreviewPaperRelation>> previewMap = previewService.groupByCourseId(user.getId(), processIds, 2);
-        Transform.combine(dtos, previewMap, UserCourseProgressDto.class, "productId", "previews", UserPaperBaseExtendDto.class);
+        Transform.combine(dtos, previewMap, UserCourseProgressDto.class, "productId", "papers", UserPaperBaseExtendDto.class);
+
+        // 绑定老师
+        Collection teacherIds = Transform.getIds(userOrderRecordList, UserOrderRecord.class, "teacherId");
+        List<CourseTeacher> teacherList = courseTeacherService.select(teacherIds);
+        Transform.combine(dtos, teacherList, UserCourseProgressDto.class, "teacherId", "teacher", CourseTeacher.class, "id", CourseTeacherExtendDto.class);
+
 
         return ResponseHelp.success(dtos);
     }
@@ -283,6 +295,9 @@ public class CourseController {
             @RequestParam(required = false) Integer times
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null){
+            throw new ParameterException("登录后查看作业");
+        }
 
         List<UserPreviewPaperRelation> p = previewService.list(page, size, recordId, user.getId(), endTime, times);
         List<UserExercisePaperDto> pr = Transform.convert(p, UserExercisePaperDto.class);

+ 80 - 3
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -101,6 +101,9 @@ public class MyController {
     private TextbookQuestionService textbookQuestionService;
 
     @Autowired
+    private TextbookTopicService textbookTopicService;
+
+    @Autowired
     private CourseDataService courseDataService;
 
     @Autowired
@@ -128,12 +131,21 @@ public class MyController {
     private UserNoteQuestionService userNoteQuestionService;
 
     @Autowired
+    private UserNoteCourseService userNoteCourseService;
+
+    @Autowired
     private UserAskQuestionService userAskQuestionService;
 
     @Autowired
+    private UserAskCourseService userAskCourseService;
+
+    @Autowired
     private UserFeedbackErrorService userFeedbackErrorService;
 
     @Autowired
+    private UserTextbookFeedbackService userTextbookFeedbackService;
+
+    @Autowired
     private UserQuestionService userQuestionService;
 
     @Autowired
@@ -782,7 +794,7 @@ public class MyController {
     }
 
     @RequestMapping(value = "/note/question", method = RequestMethod.PUT)
-    @ApiOperation(value = "更新笔记", notes = "更新笔记", httpMethod = "PUT")
+    @ApiOperation(value = "更新题目笔记", notes = "更新题目笔记", httpMethod = "PUT")
     public Response<Boolean> updateNoteQuestion(@RequestBody @Validated UserNoteQuestionDto dto)  {
         UserNoteQuestion entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
@@ -853,6 +865,17 @@ public class MyController {
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 
+    @RequestMapping(value = "/note/course", method = RequestMethod.PUT)
+    @ApiOperation(value = "更新课程笔记", notes = "更新课程笔记", httpMethod = "PUT")
+    public Response<Boolean> updateNoteCourse(@RequestBody @Validated UserNoteQuestionDto dto)  {
+        UserNoteCourse entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        entity.setUserId(user.getId());
+        userNoteCourseService.update(entity);
+
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/report/list", method = RequestMethod.GET)
     @ApiOperation(value = "获取报告列表", notes = "获取报告列表", httpMethod = "GET")
     public Response<PageMessage<UserPaperDto>> listReport(
@@ -881,36 +904,76 @@ public class MyController {
     }
 
     @RequestMapping(value = "/ask/question", method = RequestMethod.POST)
-    @ApiOperation(value = "添加提问", notes = "添加提问", httpMethod = "POST")
-    public Response<Boolean> addAskQuestion(@RequestBody @Validated UserAskDto dto)  {
+    @ApiOperation(value = "添加题目提问", notes = "添加题目提问", httpMethod = "POST")
+    public Response<Boolean> addAskQuestion(@RequestBody @Validated UserAskQuestionDto dto)  {
         UserAskQuestion entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
         entity.setUserId(user.getId());
+        Question question;
         switch (QuestionModule.ValueOf(dto.getQuestionModule())){
             case BASE:
                 entity.setQuestionModule(QuestionModule.BASE.key);
                 QuestionNo questionNo = questionNoService.get(dto.getQuestionNoId());
                 entity.setQuestionId(questionNo.getQuestionId());
                 entity.setQuestionNoId(questionNo.getId());
+
+                question = questionService.get(questionNo.getQuestionId());
                 break;
             case SENTENCE:
                 entity.setQuestionModule(QuestionModule.SENTENCE.key);
                 SentenceQuestion sentenceQuestion = sentenceQuestionService.get(dto.getQuestionNoId());
                 entity.setQuestionId(sentenceQuestion.getQuestionId());
                 entity.setQuestionNoId(sentenceQuestion.getId());
+
+                question = questionService.get(sentenceQuestion.getQuestionId());
                 break;
             case TEXTBOOK:
                 entity.setQuestionModule(QuestionModule.SENTENCE.key);
                 TextbookQuestion textbookQuestion = textbookQuestionService.get(dto.getQuestionNoId());
                 entity.setQuestionId(textbookQuestion.getQuestionId());
                 entity.setQuestionNoId(textbookQuestion.getId());
+
+                question = questionService.get(textbookQuestion.getQuestionId());
                 break;
+            default:
+                throw new ParameterException("题目模块错误");
+        }
+
+        UserQuestion userQuestion = userQuestionService.get(dto.getUserQuestionId());
+        UserReport userReport = userReportService.get(userQuestion.getReportId());
+        PaperOrigin origin = PaperOrigin.ValueOf(userReport.getPaperOrigin());
+        Integer recordId = questionFlowService.questionRelationCourse(user.getId(), origin == PaperOrigin.PREVIEW ? userReport.getOriginId() : null, QuestionType.ValueOf(question.getQuestionType()));
+
+        if (recordId != null){
+            // 绑定提问权限
+            entity.setRecordId(recordId);
+        }else{
+            // todo 判断题目是否有提问权限
         }
         userAskQuestionService.add(entity);
 
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/ask/course", method = RequestMethod.POST)
+    @ApiOperation(value = "添加课程提问", notes = "添加课程提问", httpMethod = "POST")
+    public Response<Boolean> addAskCourse(@RequestBody @Validated UserAskCourseDto dto)  {
+        UserAskCourse entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        entity.setUserId(user.getId());
+
+        UserCourse userCourse = courseExtendService.userCourse(user.getId(), dto.getCourseId());
+        if (userCourse != null){
+            // 绑定提问权限
+            entity.setRecordId(userCourse.getRecordId());
+        }else{
+            throw new ParameterException("课程需开通后才能提问");
+        }
+        userAskCourseService.add(entity);
+
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/feedback/error/question", method = RequestMethod.POST)
     @ApiOperation(value = "添加题目勘误", notes = "添加勘误", httpMethod = "POST")
     public Response<Boolean> addFeedbackErrorQuestion(@RequestBody @Validated UserFeedbackErrorDto dto)  {
@@ -937,6 +1000,20 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/feedback/textbook", method = RequestMethod.POST)
+    @ApiOperation(value = "添加机经反馈", notes = "添加机经反馈", httpMethod = "POST")
+    public Response<Boolean> addFeedbackTextbook(@RequestBody @Validated UserTextbookFeedbackDto dto)  {
+        UserTextbookFeedback entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        TextbookTopic topic = textbookTopicService.get(dto.getTopicId());
+        entity.setUserId(user.getId());
+        entity.setStatus(0);
+        entity.setLibraryId(topic.getLibraryId());
+        userTextbookFeedbackService.add(entity);
+
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/data/history", method = RequestMethod.GET)
     @ApiOperation(value = "资料更新记录", httpMethod = "GET")
     public Response<PageMessage<CourseDataHistoryInfoDto>> listDataHistory(

+ 71 - 12
server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java

@@ -4,11 +4,10 @@ package com.qxgmat.controller.api;
 import com.alibaba.fastjson.JSONObject;
 import com.nuliji.tools.*;
 import com.nuliji.tools.exception.ParameterException;
+import com.qxgmat.data.constants.enums.QuestionType;
 import com.qxgmat.data.constants.enums.ServiceKey;
 import com.qxgmat.data.constants.enums.logic.ExerciseLogic;
-import com.qxgmat.data.constants.enums.module.PaperOrigin;
-import com.qxgmat.data.constants.enums.module.QuestionModule;
-import com.qxgmat.data.constants.enums.module.StructModule;
+import com.qxgmat.data.constants.enums.module.*;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.inline.UserQuestionStat;
 import com.qxgmat.data.relation.entity.QuestionNoRelation;
@@ -18,10 +17,7 @@ import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.service.*;
-import com.qxgmat.service.extend.ExaminationService;
-import com.qxgmat.service.extend.ExerciseService;
-import com.qxgmat.service.extend.PreviewService;
-import com.qxgmat.service.extend.QuestionFlowService;
+import com.qxgmat.service.extend.*;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -81,6 +77,9 @@ public class QuestionController {
     private TextbookPaperService textbookPaperService;
 
     @Autowired
+    private TextbookLibraryService textbookLibraryService;
+
+    @Autowired
     private UserQuestionService userQuestionService;
 
     @Autowired
@@ -113,6 +112,8 @@ public class QuestionController {
     @Autowired
     private QuestionFlowService questionFlowService;
 
+    @Autowired
+    private CourseExtendService courseExtendService;
 
     @RequestMapping(value = "/exercise/progress", method = RequestMethod.GET)
     @ApiOperation(value = "练习进度", httpMethod = "GET")
@@ -283,7 +284,7 @@ public class QuestionController {
             Transform.combine(pr, paperList, UserExercisePaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
             // 绑定userPaperId,用于关联report
             Map userPaperMap = Transform.getMap(paperList, UserPaper.class, "originId", "id");
-            Transform.combine(pr, userPaperMap, UserExercisePaperDto.class, "id", "paperId");
+            Transform.combine(pr, userPaperMap, UserExercisePaperDto.class, "id", "userPaperId");
 
             // 获取最后一次作业结果
             Collection paperIds = Transform.getIds(paperList, UserPaper.class, "id");
@@ -307,6 +308,7 @@ public class QuestionController {
         for(ExaminationStruct struct : two){
             UserExaminationGroupDto dto = Transform.convert(struct, UserExaminationGroupDto.class);
             ServiceKey serviceKey = ServiceKey.ValueOf(struct.getExtend());
+            dto.setNeedService(serviceKey != null);
             dto.setHasService(true);
             // 获取第三层节点
             // 以下属的paper作为children
@@ -340,6 +342,7 @@ public class QuestionController {
                     dto.setUserNumber(userNumber);
                 }
             }
+            p.add(dto);
         }
 
         return ResponseHelp.success(p);
@@ -354,23 +357,48 @@ public class QuestionController {
             @RequestParam(required = false)  Integer times,
             HttpSession session) {
         User user = (User) shiroHelp.getLoginUser();
-        PageResult<ExaminationPaper> p = examinationService.list(page, size, structId, user != null ? user.getId():null, times);
+        ExaminationStruct struct = examinationStructService.get(structId);
+        ServiceKey serviceKey = ServiceKey.ValueOf(struct.getExtend());
+        PageResult<ExaminationPaper> p = examinationService.list(page, size, structId, user != null ? user.getId():null, serviceKey == ServiceKey.QX_CAT && user != null ? user.getQxCat() : null, times);
 
         List<UserExaminationPaperDto> pr = Transform.convert(p, UserExaminationPaperDto.class);
 
         if (user != null){
             // 获取做题记录
             Collection ids = Transform.getIds(p, ExaminationPaper.class, "id");
-            List<UserPaper> paperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.EXAMINATION, ids, null);
+            List<UserPaper> paperList;
+            if (serviceKey == ServiceKey.QX_CAT){
+                // cat模考,根据情况获取对应试题列表
+                paperList = userPaperService.listWithCat(user.getId(), ids, user.getQxCat());
+            }else{
+                paperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.EXAMINATION, ids, null);
+            }
             Transform.combine(pr, paperList, UserExaminationPaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
             // 绑定userPaperId,用于关联report
             Map userPaperMap = Transform.getMap(paperList, UserPaper.class, "originId", "id");
-            Transform.combine(pr, userPaperMap, UserExaminationPaperDto.class, "id", "paperId");
+            Transform.combine(pr, userPaperMap, UserExaminationPaperDto.class, "id", "userPaperId");
 
-            // 获取最后一次作业结果
+            // 获取最后一次结果
             Collection paperIds = Transform.getIds(paperList, UserPaper.class, "id");
             List<UserReport> reportList = userReportService.listWithLater(paperIds);
             Transform.combine(pr, reportList, UserExaminationPaperDto.class, "id", "report", UserReport.class, "paperId", UserReportExtendDto.class);
+
+            if (serviceKey == ServiceKey.QX_CAT && user.getQxCat() > 0){
+                // 获取上一遍模考成绩
+                UserService userService = userServiceService.getService(user.getId(), serviceKey);
+                if (userService.getIsReset() > 0){
+                    List<UserPaper> prevPaperList = userPaperService.listWithCat(user.getId(), ids, user.getQxCat() - 1);
+                    Transform.combine(pr, prevPaperList, UserExaminationPaperDto.class, "id", "prevPaper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
+                    // 绑定userPaperId,用于关联report
+                    Map prevUserPaperMap = Transform.getMap(prevPaperList, UserPaper.class, "originId", "id");
+                    Transform.combine(pr, prevUserPaperMap, UserExaminationPaperDto.class, "id", "prevUserPaperId");
+
+                    // 获取最后一次结果
+                    Collection prevPaperIds = Transform.getIds(prevPaperList, UserPaper.class, "id");
+                    List<UserReport> prevReportList = userReportService.listWithLaterNoReset(prevPaperIds);
+                    Transform.combine(pr, prevReportList, UserExaminationPaperDto.class, "id", "prevReport", UserReport.class, "paperId", UserReportExtendDto.class);
+                }
+            }
         }
 
         return ResponseHelp.success(pr, page, size, p.getTotal());
@@ -440,6 +468,37 @@ public class QuestionController {
                 dto.setQuestionNo(Transform.convert(textbookQuestion, QuestionNoExtendDto.class));
                 break;
         }
+        // 获取提问权限
+        PaperOrigin origin= PaperOrigin.ValueOf(userReport.getPaperOrigin());
+        Integer recordId = questionFlowService.questionRelationCourse(user.getId(), origin == PaperOrigin.PREVIEW ? userReport.getOriginId() : null, QuestionType.ValueOf(question.getQuestionType()));
+        if (recordId != null){
+            dto.setQuestionStatus(1);
+        }else{
+            // 获取基本当前权限
+            switch(origin){
+                case EXAMINATION:
+                    ExaminationPaper examinationPaper = examinationPaperService.get(userReport.getOriginId());
+                    ExaminationStruct examinationStruct = examinationStructService.get(examinationPaper.getStructThree());
+                    dto.setQuestionStatus(examinationStruct.getQuestionStatus());
+                    break;
+                case EXERCISE:
+                    ExercisePaper exercisePaper = exercisePaperService.get(userReport.getOriginId());
+                    ExerciseStruct exerciseStruct = exerciseStructService.get(exercisePaper.getStructFour());
+                    dto.setQuestionStatus(exerciseStruct.getQuestionStatus());
+                    break;
+                case TEXTBOOK:
+                    TextbookQuestion textbookQuestion = textbookQuestionService.get(userQuestion.getQuestionNoId());
+                    TextbookLibrary textbookLibrary = textbookLibraryService.get(textbookQuestion.getLibraryId());
+                    dto.setQuestionStatus(textbookLibrary.getQuestionStatus());
+                    break;
+                case PREVIEW:
+                    // 上面questionRelationCourse包括了,不会执行到
+                default:
+                    // 自由组卷,不提供提问显示
+                    dto.setQuestionStatus(-1);
+            }
+        }
+
 
         return ResponseHelp.success(dto);
     }

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

@@ -89,7 +89,7 @@ public class TextbookController
     private TextbookTopicService textbookTopicService;
 
     @RequestMapping(value = "/progress", method = RequestMethod.GET)
-    @ApiOperation(value = "练习进度", httpMethod = "GET")
+    @ApiOperation(value = "机经进度", httpMethod = "GET")
     public Response<List<UserTextbookGroupDto>> progress(HttpSession session) {
         User user = (User) shiroHelp.getLoginUser();
 
@@ -99,6 +99,9 @@ public class TextbookController
 
         for(TextbookLibrary library : new ArrayList<TextbookLibrary>(2){{add(latest);add(second);}}){
             UserTextbookGroupDto dto = Transform.convert(library, UserTextbookGroupDto.class);
+            dto.setIsLatest(library.getEndDate() == null ? 1 : 0);
+            dto.setNeedService(library.getEndDate() == null);
+            dto.setHasService(true);
             // 获取第三层所有题目,并获取题目统计
             List<TextbookQuestion> list = textbookQuestionService.listByLibrary(library.getId());
             List<TextbookQuestionRelation> relations = textbookQuestionService.relation(list);
@@ -106,6 +109,12 @@ public class TextbookController
             dto.setQuestionNumber(list.size());
             Map<Object, UserQuestionStat> userQuestionStatMap = null;
             if(user != null){
+                if (dto.getNeedService()){
+                    dto.setHasService(userServiceService.hasService(user.getId(), ServiceKey.TEXTBOOK));
+                    // 服务, 判断对应服务状态
+                    UserOrderRecord record = userOrderRecordService.getUnUseService(user.getId(), ServiceKey.TEXTBOOK);
+                    dto.setUnUseRecord(Transform.convert(record, UserServiceRecordExtendDto.class));
+                }
                 Collection questionNoIds = Transform.getIds(list, QuestionNo.class, "id");
                 List<UserQuestion> userQuestionList = userQuestionService.listByQuestionNo(user.getId(), questionNoIds);
                 userQuestionStatMap = userQuestionService.statQuestionNoMap(userQuestionList);
@@ -134,6 +143,7 @@ public class TextbookController
             for(TextbookLogic logic : TextbookLogic.all()){
                 UserTextbookGroupExtendDto extendDto = new UserTextbookGroupExtendDto();
                 extendDto.setLogic(logic.key);
+                extendDto.setTitle(logic.key.toUpperCase());
                 List<TextbookQuestionRelation> childQuestionList = relations.stream().filter((q)-> logic.contain(QuestionType.ValueOf(q.getQuestion().getQuestionType()))).collect(Collectors.toList());
                 extendDto.setQuestionNumber(childQuestionList.size());
                 if (user != null){

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/AdDto.java

@@ -5,6 +5,7 @@ import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.Ad;
 
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 import java.util.Date;
 
 @Dto(entity = Ad.class)
@@ -15,7 +16,7 @@ public class AdDto {
     @NotEmpty(message = "广告名称不能为空!")
     private String title;
 
-    @NotEmpty(message = "位置不能为空!")
+    @NotNull(message = "位置不能为空!")
     private Integer position;
 
     private Date startTime;

+ 22 - 3
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ExaminationStructDto.java

@@ -4,6 +4,7 @@ import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.ExaminationStruct;
 
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 
 @Dto(entity= ExaminationStruct.class)
 public class ExaminationStructDto {
@@ -12,15 +13,17 @@ public class ExaminationStructDto {
     @NotEmpty(message = "中文名称不能为空!")
     private String titleZh;
 
-    @NotEmpty(message = "英文名称不能为空!")
     private String titleEn;
 
-    @NotEmpty(message = "描述不能为空!")
     private String description;
 
-    @NotEmpty(message = "父级不能为空!")
+    @NotNull(message = "父级不能为空!")
     private Integer parentId;
 
+    private Integer questionStatus;
+
+    private Integer isAdapt;
+
     public Integer getId() {
         return id;
     }
@@ -60,4 +63,20 @@ public class ExaminationStructDto {
     public void setTitleEn(String titleEn) {
         this.titleEn = titleEn;
     }
+
+    public Integer getIsAdapt() {
+        return isAdapt;
+    }
+
+    public void setIsAdapt(Integer isAdapt) {
+        this.isAdapt = isAdapt;
+    }
+
+    public Integer getQuestionStatus() {
+        return questionStatus;
+    }
+
+    public void setQuestionStatus(Integer questionStatus) {
+        this.questionStatus = questionStatus;
+    }
 }

+ 30 - 3
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ExerciseStructDto.java

@@ -13,18 +13,21 @@ public class ExerciseStructDto {
     @NotEmpty(message = "中文名称不能为空!")
     private String titleZh;
 
-    @NotEmpty(message = "英文名称不能为空!")
     private String titleEn;
 
-    @NotEmpty(message = "描述不能为空!")
     private String description;
 
     @NotNull(message = "父级不能为空!")
-
     private Integer parentId;
 
     private Integer questionStatus;
 
+    private Integer isSentence;
+
+    private Integer isCourse;
+
+    private Integer isData;
+
     public Integer getId() {
         return id;
     }
@@ -71,4 +74,28 @@ public class ExerciseStructDto {
     public void setTitleZh(String titleZh) {
         this.titleZh = titleZh;
     }
+
+    public Integer getIsSentence() {
+        return isSentence;
+    }
+
+    public void setIsSentence(Integer isSentence) {
+        this.isSentence = isSentence;
+    }
+
+    public Integer getIsCourse() {
+        return isCourse;
+    }
+
+    public void setIsCourse(Integer isCourse) {
+        this.isCourse = isCourse;
+    }
+
+    public Integer getIsData() {
+        return isData;
+    }
+
+    public void setIsData(Integer isData) {
+        this.isData = isData;
+    }
 }

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ManagerRoleDto.java

@@ -5,6 +5,7 @@ import com.qxgmat.data.dao.entity.Manager;
 import com.qxgmat.data.dao.entity.ManagerRole;
 
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 
 @Dto(entity = ManagerRole.class)
 public class ManagerRoleDto {
@@ -14,7 +15,7 @@ public class ManagerRoleDto {
     @NotEmpty(message = "角色名称不能为空!")
     private String title;
 
-    @NotEmpty(message = "权限不能为空!")
+    @NotNull(message = "权限不能为空!")
     private String[] permissionList;
 
     public Integer getId() {

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/UserAskCourseDto.java

@@ -4,11 +4,12 @@ import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserAskCourse;
 
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 
 @Dto(entity = UserAskCourse.class)
 public class UserAskCourseDto {
 
-    @NotEmpty(message = "提问id不允许为空!")
+    @NotNull(message = "提问id不允许为空!")
     private Integer id;
 
     @NotEmpty(message = "回答不允许为空")

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/UserAskQuestionDto.java

@@ -4,11 +4,12 @@ import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserAskQuestion;
 
 import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 
 @Dto(entity = UserAskQuestion.class)
 public class UserAskQuestionDto {
 
-    @NotEmpty(message = "提问id不允许为空!")
+    @NotNull(message = "提问id不允许为空!")
     private Integer id;
 
     @NotEmpty(message = "回答不允许为空")

+ 40 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java

@@ -9,10 +9,18 @@ import java.math.BigDecimal;
 public class CourseExtendDto {
     private Integer id;
 
+    private Integer structId;
+
+    private Integer parentStructId;
+
+    private String courseModule;
+
     private String title;
 
     private String teacher;
 
+    private String wechatAvatar;
+
     private BigDecimal price;
 
     private Integer noNumber;
@@ -56,4 +64,36 @@ public class CourseExtendDto {
     public void setPrice(BigDecimal price) {
         this.price = price;
     }
+
+    public String getWechatAvatar() {
+        return wechatAvatar;
+    }
+
+    public void setWechatAvatar(String wechatAvatar) {
+        this.wechatAvatar = wechatAvatar;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+
+    public Integer getParentStructId() {
+        return parentStructId;
+    }
+
+    public void setParentStructId(Integer parentStructId) {
+        this.parentStructId = parentStructId;
+    }
+
+    public String getCourseModule() {
+        return courseModule;
+    }
+
+    public void setCourseModule(String courseModule) {
+        this.courseModule = courseModule;
+    }
 }

+ 69 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseTeacherExtendDto.java

@@ -0,0 +1,69 @@
+package com.qxgmat.dto.extend;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CourseTeacher;
+
+import java.math.BigDecimal;
+
+@Dto(entity = CourseTeacher.class)
+public class CourseTeacherExtendDto {
+    private Integer id;
+
+    private Integer courseId;
+
+    private String realname;
+
+    private String avatar;
+
+    private String wechat;
+
+    private String qr;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public String getRealname() {
+        return realname;
+    }
+
+    public void setRealname(String realname) {
+        this.realname = realname;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+
+    public String getWechat() {
+        return wechat;
+    }
+
+    public void setWechat(String wechat) {
+        this.wechat = wechat;
+    }
+
+    public String getQr() {
+        return qr;
+    }
+
+    public void setQr(String qr) {
+        this.qr = qr;
+    }
+}

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

@@ -13,6 +13,8 @@ public class UserPaperBaseExtendDto {
 
     private Integer times;
 
+    private Integer paperNo;
+
     private Integer questionNumber;
 
     private Date latestTime;
@@ -48,4 +50,12 @@ public class UserPaperBaseExtendDto {
     public void setLatestTime(Date latestTime) {
         this.latestTime = latestTime;
     }
+
+    public Integer getPaperNo() {
+        return paperNo;
+    }
+
+    public void setPaperNo(Integer paperNo) {
+        this.paperNo = paperNo;
+    }
 }

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

@@ -0,0 +1,47 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.UserAskCourse;
+
+@Dto(entity = UserAskCourse.class)
+public class UserAskCourseDto {
+    private Integer courseId;
+
+    private Integer courseNoId;
+
+    private String position;
+
+    private String content;
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public Integer getCourseNoId() {
+        return courseNoId;
+    }
+
+    public void setCourseNoId(Integer courseNoId) {
+        this.courseNoId = courseNoId;
+    }
+
+    public String getPosition() {
+        return position;
+    }
+
+    public void setPosition(String position) {
+        this.position = position;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 1 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskDto.java

@@ -5,7 +5,7 @@ import com.qxgmat.data.dao.entity.UserAskQuestion;
 import com.qxgmat.data.dao.entity.UserNoteQuestion;
 
 @Dto(entity = UserAskQuestion.class)
-public class UserAskDto {
+public class UserAskQuestionDto {
     private Integer userQuestionId;
 
     private String target;

+ 37 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserNoteCourseDto.java

@@ -0,0 +1,37 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.UserNoteCourse;
+
+@Dto(entity = UserNoteCourse.class)
+public class UserNoteCourseDto {
+    private Integer courseId;
+
+    private Integer courseNoId;
+
+    private String content;
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public Integer getCourseNoId() {
+        return courseNoId;
+    }
+
+    public void setCourseNoId(Integer courseNoId) {
+        this.courseNoId = courseNoId;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 37 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserTextbookFeedbackDto.java

@@ -0,0 +1,37 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.UserTextbookFeedback;
+
+@Dto(entity = UserTextbookFeedback.class)
+public class UserTextbookFeedbackDto {
+    private Integer topicId;
+
+    private String target;
+
+    private String content;
+
+    public Integer getTopicId() {
+        return topicId;
+    }
+
+    public void setTopicId(Integer topicId) {
+        this.topicId = topicId;
+    }
+
+    public String getTarget() {
+        return target;
+    }
+
+    public void setTarget(String target) {
+        this.target = target;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java

@@ -17,6 +17,8 @@ public class MyDto extends UserDto {
 
     private String inviteCode;
 
+    private Integer qxCat;
+
     private Boolean bindWechat;
 
     private Boolean bindReal;
@@ -140,4 +142,12 @@ public class MyDto extends UserDto {
     public void setId(Integer id) {
         this.id = id;
     }
+
+    public Integer getQxCat() {
+        return qxCat;
+    }
+
+    public void setQxCat(Integer qxCat) {
+        this.qxCat = qxCat;
+    }
 }

+ 9 - 9
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseDetailDto.java

@@ -24,7 +24,7 @@ public class UserCourseDetailDto {
 
     private Date useEndTime;
 
-    private Integer isUse;
+    private Integer isUsed;
 
     private Integer isStop;
 
@@ -100,14 +100,6 @@ public class UserCourseDetailDto {
         this.useEndTime = useEndTime;
     }
 
-    public Integer getIsUse() {
-        return isUse;
-    }
-
-    public void setIsUse(Integer isUse) {
-        this.isUse = isUse;
-    }
-
     public Integer getIsStop() {
         return isStop;
     }
@@ -139,4 +131,12 @@ public class UserCourseDetailDto {
     public void setRestoreTime(Date restoreTime) {
         this.restoreTime = restoreTime;
     }
+
+    public Integer getIsUsed() {
+        return isUsed;
+    }
+
+    public void setIsUsed(Integer isUsed) {
+        this.isUsed = isUsed;
+    }
 }

+ 50 - 9
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseProgressDto.java

@@ -3,6 +3,7 @@ package com.qxgmat.dto.response;
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserOrderRecord;
 import com.qxgmat.dto.extend.CourseExtendDto;
+import com.qxgmat.dto.extend.CourseTeacherExtendDto;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
 
 import java.util.Date;
@@ -16,6 +17,14 @@ public class UserCourseProgressDto {
 
     private CourseExtendDto course;
 
+    private Integer number;
+
+    private Integer vsNo;
+
+    private Integer teacherId;
+
+    private CourseTeacherExtendDto teacher;
+
     private Date startTime;
 
     private Date endTime;
@@ -24,7 +33,7 @@ public class UserCourseProgressDto {
 
     private Date useEndTime;
 
-    private Integer isUse;
+    private Integer isUsed;
 
     private Integer isStop;
 
@@ -100,14 +109,6 @@ public class UserCourseProgressDto {
         this.useEndTime = useEndTime;
     }
 
-    public Integer getIsUse() {
-        return isUse;
-    }
-
-    public void setIsUse(Integer isUse) {
-        this.isUse = isUse;
-    }
-
     public Integer getIsStop() {
         return isStop;
     }
@@ -139,4 +140,44 @@ public class UserCourseProgressDto {
     public void setRestoreTime(Date restoreTime) {
         this.restoreTime = restoreTime;
     }
+
+    public Integer getNumber() {
+        return number;
+    }
+
+    public void setNumber(Integer number) {
+        this.number = number;
+    }
+
+    public Integer getVsNo() {
+        return vsNo;
+    }
+
+    public void setVsNo(Integer vsNo) {
+        this.vsNo = vsNo;
+    }
+
+    public CourseTeacherExtendDto getTeacher() {
+        return teacher;
+    }
+
+    public void setTeacher(CourseTeacherExtendDto teacher) {
+        this.teacher = teacher;
+    }
+
+    public Integer getTeacherId() {
+        return teacherId;
+    }
+
+    public void setTeacherId(Integer teacherId) {
+        this.teacherId = teacherId;
+    }
+
+    public Integer getIsUsed() {
+        return isUsed;
+    }
+
+    public void setIsUsed(Integer isUsed) {
+        this.isUsed = isUsed;
+    }
 }

+ 30 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserExaminationGroupDto.java

@@ -8,12 +8,18 @@ public class UserExaminationGroupDto {
     private String titleEn;
     private String title;
 
+    private String description;
+
+    private Integer isAdapt;
+
     private Integer paperNumber;
 
     private Integer userNumber;
 
     private Integer minTimes;
 
+    private Boolean needService;
+
     private Boolean hasService;
 
     private UserServiceRecordExtendDto unUseRecord;
@@ -89,4 +95,28 @@ public class UserExaminationGroupDto {
     public void setUnUseRecord(UserServiceRecordExtendDto unUseRecord) {
         this.unUseRecord = unUseRecord;
     }
+
+    public Integer getIsAdapt() {
+        return isAdapt;
+    }
+
+    public void setIsAdapt(Integer isAdapt) {
+        this.isAdapt = isAdapt;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Boolean getNeedService() {
+        return needService;
+    }
+
+    public void setNeedService(Boolean needService) {
+        this.needService = needService;
+    }
 }

+ 30 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserExaminationPaperDto.java

@@ -20,6 +20,12 @@ public class UserExaminationPaperDto {
 
     private Integer userPaperId;
 
+    private UserPaperBaseExtendDto prevPaper;
+
+    private UserReportExtendDto prevReport;
+
+    private Integer prevUserPaperId;
+
     public PaperStat getStat() {
         return stat;
     }
@@ -67,4 +73,28 @@ public class UserExaminationPaperDto {
     public void setTitle(Integer title) {
         this.title = title;
     }
+
+    public UserPaperBaseExtendDto getPrevPaper() {
+        return prevPaper;
+    }
+
+    public void setPrevPaper(UserPaperBaseExtendDto prevPaper) {
+        this.prevPaper = prevPaper;
+    }
+
+    public UserReportExtendDto getPrevReport() {
+        return prevReport;
+    }
+
+    public void setPrevReport(UserReportExtendDto prevReport) {
+        this.prevReport = prevReport;
+    }
+
+    public Integer getPrevUserPaperId() {
+        return prevUserPaperId;
+    }
+
+    public void setPrevUserPaperId(Integer prevUserPaperId) {
+        this.prevUserPaperId = prevUserPaperId;
+    }
 }

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserQuestionDetailDto.java

@@ -45,6 +45,8 @@ public class UserQuestionDetailDto {
 
     private List<QuestionBaseExtendDto> associations;
 
+    private Integer questionStatus;
+
     public Integer getId() {
         return id;
     }
@@ -188,4 +190,12 @@ public class UserQuestionDetailDto {
     public void setStageNo(Integer stageNo) {
         this.stageNo = stageNo;
     }
+
+    public Integer getQuestionStatus() {
+        return questionStatus;
+    }
+
+    public void setQuestionStatus(Integer questionStatus) {
+        this.questionStatus = questionStatus;
+    }
 }

+ 72 - 36
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserTextbookGroupDto.java

@@ -1,20 +1,32 @@
 package com.qxgmat.dto.response;
 
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.TextbookLibrary;
 import com.qxgmat.data.inline.PaperStat;
 import com.qxgmat.data.inline.UserQuestionStat;
+import com.qxgmat.dto.extend.UserServiceRecordExtendDto;
 import com.qxgmat.dto.extend.UserTextbookGroupExtendDto;
 
+import java.util.Date;
 import java.util.List;
+
+@Dto(entity = TextbookLibrary.class)
 public class UserTextbookGroupDto {
     private Integer id;
 
-    private String titleZh;
+    private Date startDate;
+
+    private Date endDate;
+
+    private Date updateTime;
 
-    private String titleEn;
+    private Integer isLatest;
 
-    private String description;
+    private Boolean needService;
 
-    private String extend;
+    private Boolean hasService;
+
+    private UserServiceRecordExtendDto unUseRecord;
 
     private Integer questionNumber;
 
@@ -36,30 +48,6 @@ public class UserTextbookGroupDto {
         this.id = id;
     }
 
-    public String getTitleZh() {
-        return titleZh;
-    }
-
-    public void setTitleZh(String titleZh) {
-        this.titleZh = titleZh;
-    }
-
-    public String getTitleEn() {
-        return titleEn;
-    }
-
-    public void setTitleEn(String titleEn) {
-        this.titleEn = titleEn;
-    }
-
-    public String getExtend() {
-        return extend;
-    }
-
-    public void setExtend(String extend) {
-        this.extend = extend;
-    }
-
     public Integer getQuestionNumber() {
         return questionNumber;
     }
@@ -92,14 +80,6 @@ public class UserTextbookGroupDto {
         this.userStat = userStat;
     }
 
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
     public Integer getMinTimes() {
         return minTimes;
     }
@@ -115,4 +95,60 @@ public class UserTextbookGroupDto {
     public void setChildren(List<UserTextbookGroupExtendDto> children) {
         this.children = children;
     }
+
+    public Boolean getNeedService() {
+        return needService;
+    }
+
+    public void setNeedService(Boolean needService) {
+        this.needService = needService;
+    }
+
+    public Boolean getHasService() {
+        return hasService;
+    }
+
+    public void setHasService(Boolean hasService) {
+        this.hasService = hasService;
+    }
+
+    public UserServiceRecordExtendDto getUnUseRecord() {
+        return unUseRecord;
+    }
+
+    public void setUnUseRecord(UserServiceRecordExtendDto unUseRecord) {
+        this.unUseRecord = unUseRecord;
+    }
+
+    public Date getStartDate() {
+        return startDate;
+    }
+
+    public void setStartDate(Date startDate) {
+        this.startDate = startDate;
+    }
+
+    public Date getEndDate() {
+        return endDate;
+    }
+
+    public void setEndDate(Date endDate) {
+        this.endDate = endDate;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public Integer getIsLatest() {
+        return isLatest;
+    }
+
+    public void setIsLatest(Integer isLatest) {
+        this.isLatest = isLatest;
+    }
 }

+ 26 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UserNoteCourseService.java

@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -32,6 +33,31 @@ public class UserNoteCourseService extends AbstractService {
     @Resource
     private UserNoteCourseMapper userNoteCourseMapper;
 
+    /**
+     * 更新用户笔记:没有则添加
+     * @param note
+     * @return
+     */
+    @Transactional
+    public UserNoteCourse update(UserNoteCourse note){
+        Example example = new Example(UserNoteCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", note.getUserId())
+                        .andEqualTo("courseId", note.getCourseId())
+                        .andEqualTo("courseNoId", note.getCourseNoId())
+        );
+        UserNoteCourse in = one(userNoteCourseMapper, example);
+        Date now = new Date();
+        if(in == null){
+            // 按实际更新更改更新时间
+            return add(note);
+        }else{
+            note.setId(in.getId());
+            return edit(note);
+        }
+    }
+
     public UserNoteCourse add(UserNoteCourse message){
         int result = insert(userNoteCourseMapper, message);
         message = one(userNoteCourseMapper, message.getId());

+ 1 - 1
server/gateway-api/src/main/java/com/qxgmat/service/UserNoteQuestionService.java

@@ -111,7 +111,7 @@ public class UserNoteQuestionService extends AbstractService {
     @Transactional
     public UserNoteQuestion update(UserNoteQuestion note){
 
-        Example example = new Example(UserCollectQuestion.class);
+        Example example = new Example(UserNoteQuestion.class);
         example.and(
                 example.createCriteria()
                         .andEqualTo("userId", note.getUserId())

+ 21 - 4
server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java

@@ -27,10 +27,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 @Service
 public class UserPaperService extends AbstractService {
@@ -79,6 +76,7 @@ public class UserPaperService extends AbstractService {
      * @return
      */
     public List<UserPaper> listWithOrigin(Integer userId, PaperOrigin origin, Collection ids, Integer recordId){
+        if (ids == null || ids.size() == 0) return new ArrayList<>();
         Example example = new Example(UserPaper.class);
         example.and(
                 example.createCriteria()
@@ -96,6 +94,25 @@ public class UserPaperService extends AbstractService {
     }
 
     /**
+     * 获取用户cat做题记录
+     * @param userId
+     * @param ids
+     * @param no
+     * @return
+     */
+    public List<UserPaper> listWithCat(Integer userId, Collection ids, Integer no){
+        Example example = new Example(UserPaper.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("paperOrigin", PaperOrigin.EXAMINATION.key)
+                        .andIn("originId", ids)
+                        .andEqualTo("qxCatNo", no)
+        );
+        return select(userPaperMapper, example);
+    }
+
+    /**
      * 获取用户做题组卷
      * @param userId
      * @param origin

+ 1 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java

@@ -73,6 +73,7 @@ public class UserQuestionService extends AbstractService {
     }
 
     public List<UserQuestion> listByQuestion(Integer userId, Collection questionIds){
+        if (questionIds == null || questionIds.size() == 0) return new ArrayList<>();
         Example example = new Example(UserQuestion.class);
         example.and(
                 example.createCriteria()

+ 1 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UserServiceService.java

@@ -33,6 +33,7 @@ public class UserServiceService extends AbstractService {
      * @return
      */
     public boolean hasService(Integer userId, ServiceKey key){
+        if (key == null) return false;
         Example example = new Example(UserService.class);
         example.and(
                 example.createCriteria()

+ 1 - 1
server/gateway-api/src/main/java/com/qxgmat/service/annotation/InitPaper.java

@@ -8,5 +8,5 @@ import com.qxgmat.data.dao.entity.UserPaper;
  */
 @FunctionalInterface
 public interface InitPaper {
-    void callback(UserPaper userPaper, Integer originId);
+    UserPaper callback(UserPaper userPaper, Integer originId);
 }

+ 42 - 1
server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java

@@ -1,17 +1,23 @@
 package com.qxgmat.service.extend;
 
 import com.nuliji.tools.Tools;
+import com.nuliji.tools.Transform;
+import com.qxgmat.data.constants.enums.QuestionSubject;
+import com.qxgmat.data.constants.enums.QuestionType;
 import com.qxgmat.data.constants.enums.module.ProductType;
+import com.qxgmat.data.constants.enums.module.VideoCourseType;
 import com.qxgmat.data.dao.entity.Course;
+import com.qxgmat.data.dao.entity.UserCourse;
 import com.qxgmat.data.dao.entity.UserOrderRecord;
 import com.qxgmat.data.relation.entity.UserRecordStatRelation;
 import com.qxgmat.service.inline.CourseService;
 import com.qxgmat.service.inline.UserCourseRecordService;
+import com.qxgmat.service.inline.UserCourseService;
 import com.qxgmat.service.inline.UserOrderRecordService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Date;
+import java.util.*;
 
 @Service
 public class CourseExtendService {
@@ -20,6 +26,9 @@ public class CourseExtendService {
     private CourseService courseService;
 
     @Autowired
+    private UserCourseService userCourseService;
+
+    @Autowired
     private UserOrderRecordService userOrderRecordService;
 
     @Autowired
@@ -71,4 +80,36 @@ public class CourseExtendService {
         UserRecordStatRelation record = userCourseRecordService.statAvg(startTime.toString(), endTime.toString());
         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;
+    }
 }

+ 13 - 3
server/gateway-api/src/main/java/com/qxgmat/service/extend/ExaminationService.java

@@ -21,6 +21,7 @@ import com.qxgmat.data.relation.UserReportRelationMapper;
 import com.qxgmat.data.relation.entity.QuestionDifficultRelation;
 import com.qxgmat.data.relation.entity.QuestionNoRelation;
 import com.qxgmat.service.UserPaperService;
+import com.qxgmat.service.UsersService;
 import com.qxgmat.service.inline.*;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -60,6 +61,9 @@ public class ExaminationService extends AbstractService {
     private UserReportService userReportService;
 
     @Resource
+    private UsersService usersService;
+
+    @Resource
     private ExaminationPaperRelationMapper examinationPaperRelationMapper;
 
     /**
@@ -96,6 +100,7 @@ public class ExaminationService extends AbstractService {
     public ExaminationStruct addPaper(ExaminationStruct entity){
         entity = examinationStructService.add(entity);
         if (entity.getLevel() == 3){
+            // 添加2份paper
             ExaminationPaper paper = examinationPaperService.add(ExaminationPaper.builder()
                     .isAdapt(entity.getIsAdapt())
                     .structTwo(entity.getParentId())
@@ -109,7 +114,7 @@ public class ExaminationService extends AbstractService {
 
     @Transactional
     public ExaminationStruct editPaper(ExaminationStruct entity){
-        entity = examinationStructService.add(entity);
+        entity = examinationStructService.edit(entity);
         if (entity.getLevel() == 3){
             ExaminationPaper paper = examinationPaperService.getByThree(entity.getId());
             if(paper == null){
@@ -188,9 +193,9 @@ public class ExaminationService extends AbstractService {
      * @param times
      * @return
      */
-    public PageResult<ExaminationPaper> list(int page, int size, Number structId, Number userId, Integer times){
+    public PageResult<ExaminationPaper> list(int page, int size, Number structId, Number userId, Integer qxCatNo, Integer times){
         Page<ExaminationPaper> p = page(()->{
-            examinationPaperRelationMapper.listWithUser(structId, userId, times);
+            examinationPaperRelationMapper.listWithUser(structId, userId, qxCatNo, times);
         },page, size);
 
         Collection ids = Transform.getIds(p, ExaminationPaper.class, "id");
@@ -200,6 +205,8 @@ public class ExaminationService extends AbstractService {
         return new PageResult<>(list, p.getTotal());
     }
 
+
+
     /**
      * cat模考是否已经完成
      * @param userId
@@ -246,6 +253,9 @@ public class ExaminationService extends AbstractService {
                 }
             }
         }
+        // 增加用户cat计数
+        User user = usersService.get(userId);
+        usersService.edit(User.builder().id(userId).qxCat(user.getQxCat() + 1).build());
         return userPaperService.reset(paperIds, userId);
     }
 

+ 28 - 5
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -229,8 +229,15 @@ public class OrderFlowService {
                 }
             }
             BigDecimal money = BigDecimal.valueOf(toolsService.getServicePrice(checkout.getService(), checkout.getParam()));
-            checkout.setMoney(money);
             checkout.setOriginMoney(money);
+            ServiceKey serviceKey = ServiceKey.ValueOf(checkout.getService());
+            if (serviceKey == ServiceKey.TEXTBOOK){
+                // 是否存在半价机经券
+                User user = usersService.get(checkout.getId());
+                checkout.setMoney(user.getTextbookHalf() > 0 ? money.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_UP):money);
+            }else{
+                checkout.setMoney(money);
+            }
             return checkout;
         }));
 
@@ -252,6 +259,15 @@ public class OrderFlowService {
             BigDecimal money = order.getMoney();
             BigDecimal originMoney = order.getOriginMoney();
             for(UserOrderCheckout checkout:userOrderCheckoutList){
+                ServiceKey serviceKey = ServiceKey.ValueOf(checkout.getService());
+
+                if (serviceKey == ServiceKey.TEXTBOOK){
+                    // 是否存在半价机经券
+                    if (!checkout.getMoney().equals(checkout.getOriginMoney())){
+                        JSONObject promote = order.getPromote();
+                        promote.put("textbook", "half");
+                    }
+                }
                 money = money.add(checkout.getMoney());
                 originMoney = originMoney.add(checkout.getMoney());
             }
@@ -296,9 +312,8 @@ public class OrderFlowService {
                 money = money.add(promoteVideoMoney);
 
                 // 添加视频优惠记录
-                JSONObject promoteVideo = new JSONObject();
                 JSONObject promote = order.getPromote();
-                promote.put("video", promoteVideo);
+                promote.put("video", percent);
             }else{
                 courseMoney = courseMoney.add(videoMoney);
                 money = money.add(videoMoney);
@@ -370,6 +385,14 @@ public class OrderFlowService {
                 Date endTime = Tools.addMonth(startTime, serviceKey.expireDay);
                 record.setStartTime(startTime);
                 record.setEndTime(endTime);
+                // 如果使用半价优惠券,则扣除
+                if (serviceKey == ServiceKey.TEXTBOOK){
+                    User user = usersService.get(record.getUserId());
+                    if (user.getTextbookHalf() == 0) {
+                        // todo 支付存在问题
+                    }
+                    usersService.edit(User.builder().id(user.getId()).textbookHalf(user.getTextbookHalf() - 1).build());
+                }
             }
             return record;
         }));
@@ -390,7 +413,7 @@ public class OrderFlowService {
             }
             Date startTime = time;
             Date endTime = Tools.addDate(startTime, expireDay);
-            UserCourse userCourse = userCourseService.getCourse(record.getUserId(), record.getProductId());
+            UserCourse userCourse = userCourseService.getCourseBase(record.getUserId(), record.getProductId());
             if(userCourse == null){
                 userCourse = UserCourse.builder()
                         .userId(record.getUserId())
@@ -499,7 +522,7 @@ public class OrderFlowService {
             }
             Date startTime = time;
             Date endTime = Tools.addDate(startTime, expireDay);
-            UserCourse userCourse = userCourseService.getCourse(record.getUserId(), record.getProductId());
+            UserCourse userCourse = userCourseService.getCourseBase(record.getUserId(), record.getProductId());
             if(userCourse == null){
                 userCourse = UserCourse.builder()
                         .userId(record.getUserId())

+ 38 - 11
server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java

@@ -1,26 +1,17 @@
 package com.qxgmat.service.extend;
 
-import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
-import com.nuliji.tools.PageResult;
+import com.nuliji.tools.Tools;
 import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
-import com.nuliji.tools.exception.SystemException;
-import com.nuliji.tools.mybatis.Example;
-import com.qxgmat.data.constants.enums.QuestionType;
 import com.qxgmat.data.constants.enums.module.*;
-import com.qxgmat.data.constants.enums.status.PreviewStatus;
-import com.qxgmat.data.dao.PreviewPaperMapper;
 import com.qxgmat.data.dao.entity.*;
-import com.qxgmat.data.relation.UserPaperRelationMapper;
-import com.qxgmat.data.relation.UserReportRelationMapper;
 import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.service.UserPaperService;
 import com.qxgmat.service.inline.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.*;
@@ -180,12 +171,48 @@ public class PreviewService extends AbstractService {
                 return timeRecord.getId();
             case VIDEO:
                 // 获取当前该课程记录
-                UserCourse record = userCourseService.getCourse(userId, assign.getCourseId());
+                UserCourse record = userCourseService.getCourseBase(userId, assign.getCourseId());
                 return record != null ? record.getRecordId() : 0;
             default:
                 return 0;
         }
+    }
 
+    /**
+     * 获取预习作业关联的提问权限记录
+     * @param assignId
+     * @return
+     */
+    public Integer questionCourse(Integer userId, Integer assignId){
+        PreviewAssign assign = previewAssignService.get(assignId);
+        if (assign == null){
+            throw new ParameterException("记录不存在");
+        }
+        Course course = courseService.get(assign.getCourseId());
+        CourseModule courseModule = CourseModule.ValueOf(course.getCourseModule());
+        switch(courseModule){
+            case VS:
+                // 默认没有?
+                return 0;
+//                UserCourseAppointment appointment = userCourseAppointmentService.get(assign.getCourseAppointment());
+//                return appointment.getRecordId();
+            case ONLINE:
+                UserOrderRecord timeRecord = userOrderRecordService.getByUserAndTime(userId, assign.getCourseId(), assign.getCourseTime());
+                // 有效期延长到n天
+                if (Tools.addDate(timeRecord.getUseEndTime(), course.getAskExtendDays()).before(new Date())){
+                    return 0;
+                }
+                return timeRecord.getId();
+            case VIDEO:
+                // 只有系统授课允许
+                VideoCourseType videoCourseType = VideoCourseType.ValueOf(course.getVideoType());
+                if (videoCourseType != VideoCourseType.SYSTEM) return 0;
+                // 获取当前该课程记录
+                UserCourse record = userCourseService.getCourseBase(userId, assign.getCourseId());
+                return record != null ? record.getRecordId() : 0;
+            default:
+                return 0;
+        }
     }
 
     private void replaceTitle(List<PreviewAssign> previewAssignList){

+ 60 - 6
server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java

@@ -31,6 +31,9 @@ public class QuestionFlowService {
     private ExaminationPaperService examinationPaperService;
 
     @Resource
+    private ExaminationStructService examinationStructService;
+
+    @Resource
     private QuestionNoService questionNoService;
 
     @Resource
@@ -79,6 +82,9 @@ public class QuestionFlowService {
     private ExaminationService examinationService;
 
     @Resource
+    private CourseExtendService courseExtendService;
+
+    @Resource
     private ToolsService toolsService;
 
     // 自由组卷初始化试卷callback
@@ -114,18 +120,21 @@ public class QuestionFlowService {
             List<QuestionNoRelation> relationList = questionNoService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new QuestionNoRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
         makePaperCallback.put(QuestionModule.SENTENCE, (userPaper, id)->{
             // 获取考题时间
             List<SentenceQuestionRelation> relationList = sentenceQuestionService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new SentenceQuestionRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
         makePaperCallback.put(QuestionModule.TEXTBOOK, (userPaper, id)->{
             // 获取考题时间
             List<TextbookQuestionRelation> relationList = textbookQuestionService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new TextbookQuestionRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
 
         initOriginPaperCallback.put(PaperOrigin.PREVIEW, (userPaper, id)->{
@@ -152,7 +161,7 @@ public class QuestionFlowService {
             // 根据papermodule得到考试时长
             QuestionModule module = QuestionModule.WithPaper(PaperModule.ValueOf(paper.getPaperModule()));
             InitPaper callback = makePaperCallback.get(module);
-            callback.callback(userPaper, id);
+            return callback.callback(userPaper, id);
         });
 
         initPaperCallback.put(PaperModule.EXERCISE, (userPaper, id)->{
@@ -163,6 +172,7 @@ public class QuestionFlowService {
             List<QuestionNoRelation> relationList = questionNoService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new QuestionNoRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
         initPaperCallback.put(PaperModule.SENTENCE, (userPaper, id)->{
             SentencePaper paper = sentencePaperService.get(id);
@@ -173,6 +183,7 @@ public class QuestionFlowService {
             List<SentenceQuestionRelation> relationList = sentenceQuestionService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new SentenceQuestionRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
         initPaperCallback.put(PaperModule.TEXTBOOK, (userPaper, id)->{
             TextbookPaper paper = textbookPaperService.get(id);
@@ -183,11 +194,20 @@ public class QuestionFlowService {
             List<TextbookQuestionRelation> relationList = textbookQuestionService.listWithRelationByIds(userPaper.getQuestionNoIds());
             Integer time = toolsService.computerTime(relationList.toArray(new TextbookQuestionRelation[0]));
             userPaper.setTime(time);
+            return userPaper;
         });
         initPaperCallback.put(PaperModule.EXAMINATION, (userPaper, id)->{
             ExaminationPaper paper = examinationPaperService.get(id);
             userPaper.setTitle(paper.getTitle());
             userPaper.setIsAdapt(paper.getIsAdapt());
+            ExaminationPaper examinationPaper = examinationPaperService.get(userPaper.getOriginId());
+            ExaminationStruct examinationStruct = examinationStructService.get(examinationPaper.getStructThree());
+            ServiceKey serviceKey = ServiceKey.ValueOf(examinationStruct.getExtend());
+            if (serviceKey == ServiceKey.QX_CAT) {
+                User user = usersService.get(userPaper.getUserId());
+                userPaper.setPaperNo(user.getQxCat());
+            }
+            return userPaper;
         });
 
         initReportCallback.put(PaperModule.EXERCISE, (report, paper)->{
@@ -227,7 +247,23 @@ public class QuestionFlowService {
         resetPaperCallback.put(PaperOrigin.PREVIEW, (userPaper, id)->{
             // 重新初始化
             InitPaper callback = initOriginPaperCallback.get(PaperOrigin.PREVIEW);
-            callback.callback(userPaper, id);
+            return callback.callback(userPaper, id);
+        });
+        resetPaperCallback.put(PaperOrigin.EXAMINATION, (userPaper, id)->{
+            // 如果是cat模考
+            ExaminationPaper examinationPaper = examinationPaperService.get(userPaper.getOriginId());
+            ExaminationStruct examinationStruct = examinationStructService.get(examinationPaper.getStructThree());
+            ServiceKey serviceKey = ServiceKey.ValueOf(examinationStruct.getExtend());
+            if (serviceKey == ServiceKey.QX_CAT){
+                User user = usersService.get(userPaper.getUserId());
+
+                // 创建新的paper
+                userPaper = userPaperService.newByPaper(userPaper.getUserId(), PaperOrigin.ValueOf(userPaper.getPaperOrigin()), PaperModule.ValueOf(userPaper.getPaperModule()), userPaper.getOriginId());
+                userPaper.setIsAdapt(userPaper.getIsAdapt());
+                userPaper.setTitle(userPaper.getTitle());
+                userPaper.setPaperNo(user.getQxCat());
+            }
+            return userPaper;
         });
 
         nextCallback.put(PaperModule.EXERCISE, (question, report, lastQuestion)->{
@@ -392,7 +428,7 @@ public class QuestionFlowService {
         paper.setQuestionNoIds(questionNoIds.toArray(new Integer[0]));
 
         InitPaper callback = makePaperCallback.get(module);
-        callback.callback(paper, 0);
+        paper = callback.callback(paper, 0);
         paper = userPaperService.add(paper);
 
         // 绑定关系,用于下次处理过滤
@@ -418,7 +454,7 @@ public class QuestionFlowService {
             if (callback == null) {
                 callback  = initPaperCallback.get(module);
             }
-            callback.callback(paper, paperId);
+            paper = callback.callback(paper, paperId);
             paper = userPaperService.add(paper);
         }
         return paper;
@@ -443,9 +479,13 @@ public class QuestionFlowService {
             // 是否对重置有特殊处理
             InitPaper callback = resetPaperCallback.get(origin);
             if (callback != null){
-                callback.callback(paper, paperId);
+                paper = callback.callback(paper, paperId);
+            }
+            if (paper.getId() != null){
+                userPaperService.edit(paper);
+            }else{
+                paper = userPaperService.add(paper);
             }
-            userPaperService.edit(paper);
         }else if(setting == null){
             // 对于长难句这种没有设置开始页的,读取最后一次
             userReport = userReportService.getLastByPaper(userId, paper.getId());
@@ -680,6 +720,19 @@ public class QuestionFlowService {
     }
 
     /**
+     * 获取提问权限记录
+     * @param questionType
+     * @return
+     */
+    public Integer questionRelationCourse(Integer userId, Integer assignId, QuestionType questionType){
+        if (assignId != null){
+            Integer assignRecordId = previewService.questionCourse(userId, assignId);
+            if (assignRecordId != null) return assignRecordId;
+        }
+        return courseExtendService.questionRelationCourse(userId, questionType);
+    }
+
+    /**
      * 累计考试学习时间
      * @param userId
      * @return
@@ -762,6 +815,7 @@ public class QuestionFlowService {
         number.put(stage, subnumber + 1);
         return true;
     }
+
     /**
      * 语文出题计算
      * @param setting

+ 7 - 1
server/gateway-api/src/main/java/com/qxgmat/service/inline/CoursePackageService.java

@@ -33,7 +33,7 @@ public class CoursePackageService extends AbstractService {
     @Resource
     private CoursePackageRelationMapper coursePackageRelationMapper;
 
-    public Page<CoursePackage> list(int page, int size, Integer structId, String order, DirectionStatus direction){
+    public Page<CoursePackage> list(int page, int size, Integer structId, Boolean isSpecial, String order, DirectionStatus direction){
         Example example = new Example(Course.class);
         if(structId != null){
             example.and(
@@ -41,6 +41,12 @@ public class CoursePackageService extends AbstractService {
                             .orEqualTo("structId", structId)
             );
         }
+        if (isSpecial != null){
+            example.and(
+                    example.createCriteria()
+                            .orEqualTo("isSpecial", isSpecial ? 1: 0)
+            );
+        }
         if(order == null || order.isEmpty()) order = "id";
         if (direction != null){
             switch(direction){

+ 16 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java

@@ -118,6 +118,22 @@ public class CourseService extends AbstractService {
         return select(courseMapper, example);
     }
 
+    public List<Course> listByExtend(String questionType, String questionSubject, String videoType){
+        Example example = new Example(Course.class);
+        example.and(
+                example.createCriteria()
+                        .orEqualTo("extend", questionType)
+                        .orEqualTo("extend", questionSubject)
+        );
+        if (videoType != null){
+            example.and(
+                    example.createCriteria()
+                        .andEqualTo("videoType", videoType)
+            );
+        }
+        return select(courseMapper, example);
+    }
+
     /**
      * 累加购买记录,访问记录
      * @param sale

+ 3 - 1
server/gateway-api/src/main/java/com/qxgmat/service/inline/TextbookLibraryService.java

@@ -57,7 +57,9 @@ public class TextbookLibraryService extends AbstractService {
     public TextbookLibrary getSecond(){
         Example example = new Example(TextbookLibrary.class);
         example.orderBy("id").desc();
-        return one(textbookLibraryMapper, example);
+
+        List<TextbookLibrary> list = select(textbookLibraryMapper, example, 2, 1);
+        return list.get(0);
     }
 
     /**

+ 29 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseService.java

@@ -27,16 +27,45 @@ public class UserCourseService extends AbstractService {
      * @param courseId
      * @return
      */
+    public UserCourse getCourseBase(Integer userId, Integer courseId){
+        Example example = new Example(UserCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("courseId", courseId)
+        );
+        return one(userCourseMapper, example);
+    }
+    /**
+     * 获取单个课程有效记录
+     * @param userId
+     * @param courseId
+     * @return
+     */
     public UserCourse getCourse(Integer userId, Integer courseId){
         Example example = new Example(UserCourse.class);
         example.and(
                 example.createCriteria()
                         .andEqualTo("userId", userId)
                         .andEqualTo("courseId", courseId)
+                        .andGreaterThanOrEqualTo("startTime", new Date())
+                        .andLessThan("expireTime", new Date())
         );
         return one(userCourseMapper, example);
     }
 
+    public List<UserCourse> listByCourse(Integer userId, Collection ids){
+        Example example = new Example(UserCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andIn("courseId", ids)
+                        .andGreaterThanOrEqualTo("startTime", new Date())
+                        .andLessThan("expireTime", new Date())
+        );
+        return select(userCourseMapper, example);
+    }
+
     /**
      * 合并用户信息,将old转移至new
      * @param oldUserId

+ 0 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java


Some files were not shown because too many files changed in this diff