瀏覽代碼

feat(front): 报告详情

Go 4 年之前
父節點
當前提交
f475f6c7ad
共有 32 個文件被更改,包括 3403 次插入2816 次删除
  1. 2345 2323
      front/package-lock.json
  2. 11 7
      front/project/Constant.js
  3. 18 10
      front/project/h5/routes/page/bind/page.js
  4. 1 1
      front/project/h5/routes/page/login/page.js
  5. 2 2
      front/project/h5/routes/page/study/page.js
  6. 36 20
      front/project/www/components/Login/index.js
  7. 7 6
      front/project/www/components/Panel/index.js
  8. 8 0
      front/project/www/routes/examination/main/page.js
  9. 4 0
      front/project/www/routes/exercise/main/page.js
  10. 5 6
      front/project/www/routes/paper/process/sentence/index.js
  11. 3 3
      front/project/www/routes/paper/question/page.js
  12. 41 2
      front/project/www/routes/paper/report/index.less
  13. 607 366
      front/project/www/routes/paper/report/page.js
  14. 3 3
      front/project/www/routes/question/detail/page.js
  15. 3 2
      front/project/www/stores/question.js
  16. 2 2
      front/project/www/stores/user.js
  17. 8 3
      front/src/services/Tools.js
  18. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserQuestion.java
  19. 3 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserQuestionMapper.xml
  20. 5 0
      server/data/src/main/java/com/qxgmat/data/relation/UserQuestionRelationMapper.java
  21. 16 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserQuestionRelationMapper.xml
  22. 3 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java
  23. 15 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  24. 77 2
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  25. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/UserPaperBaseExtendDto.java
  26. 0 34
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  27. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCollectQuestionInfoDto.java
  28. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserQuestionErrorInfoDto.java
  29. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserReportDetailDto.java
  30. 27 2
      server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java
  31. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/UsersService.java
  32. 75 15
      server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java

文件差異過大導致無法顯示
+ 2345 - 2323
front/package-lock.json


文件差異過大導致無法顯示
+ 11 - 7
front/project/Constant.js


+ 18 - 10
front/project/h5/routes/page/bind/page.js

@@ -16,7 +16,9 @@ export default class extends Page {
     };
   }
 
-  init() { }
+  init() {
+    this.validNumber = 0;
+  }
 
   changeData(field, value) {
     let { data } = this.state;
@@ -25,21 +27,24 @@ export default class extends Page {
     this.setState({ data });
   }
 
-  validMobile() {
+  validMobile(login) {
     const { data } = this.state;
     const { area, mobile } = data;
-    if (area === '' || mobile === '') return;
+    if (!area || !mobile) return;
+    this.validNumber += 1;
+    const number = this.validNumber;
     User.validWechat(area, mobile)
       .then(result => {
         if (result) {
           this.setState({ mobileError: '' });
           return User.validMobile(area, mobile)
             .then(r => {
+              if (number !== this.validNumber) return;
               this.setState({ needEmail: r });
             });
         }
         this.setState({ needEmail: false });
-        return Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
+        return login ? Promise.resolve() : Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
       })
       .catch(err => {
         this.setState({ mobileError: err.message });
@@ -49,7 +54,7 @@ export default class extends Page {
   sendValid() {
     const { data, mobileError } = this.state;
     const { area, mobile } = data;
-    if (area === '' || mobile === '' || mobileError !== '') return Promise.reject();
+    if (!area || !mobile || mobileError) return Promise.reject();
     return Common.sendSms(area, mobile)
       .then((result) => {
         if (result) {
@@ -68,9 +73,9 @@ export default class extends Page {
   submit() {
     const { data, needEmail, mobileError, validError } = this.state;
     const { area, mobile, mobileVerifyCode, email } = data;
-    if (mobileError !== '' || validError !== '') return;
-    if (area === '' || mobile === '' || mobileVerifyCode === '') return;
-    if (needEmail && email === '') return;
+    if (mobileError || validError) return;
+    if (!area || !mobile || !mobileVerifyCode) return;
+    if (needEmail && !email) return;
     User.bind(area, mobile, mobileVerifyCode).then(() => {
       let handler = null;
       if (needEmail) {
@@ -79,7 +84,7 @@ export default class extends Page {
         handler = Promise.resolve();
       }
       handler.then(() => {
-        linkTo('/identity');
+        linkTo('/');
       });
     })
       .catch(err => {
@@ -97,12 +102,15 @@ export default class extends Page {
       <div>
         <SelectInput placeholder="请输入手机号" selectValue={this.state.data.area} select={MobileArea} value={this.state.data.mobile} error={this.state.mobileError} onSelect={(value) => {
           this.changeData('area', value);
+          this.validMobile();
         }} onChange={(e) => {
           this.changeData('mobile', e.target.value);
-          this.validMobile(e.target.value);
+          this.validMobile();
         }} />
         <VerificationInput placeholder="请输入验证码" value={this.state.data.mobileVerifyCode} error={this.state.validError} onSend={() => {
           return this.sendValid();
+        }} onChange={(e) => {
+          this.changeData('mobileVerifyCode', e.target.value);
         }} />
         {needEmail && <Input placeholder="请输入邮箱" value={this.state.data.email} onChange={(e) => {
           this.changeData('email', e.target.value);

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

@@ -9,7 +9,7 @@ export default class extends Page {
     const { code } = this.props.core.query;
     if (code) {
       User.loginWechat(code).then((info) => {
-        if (info.mobile) {
+        if (info.bindMobile) {
           replaceLink('/');
         } else {
           replaceLink('/bind');

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

@@ -33,14 +33,14 @@ export default class extends Page {
             自{formatDate(total.createTime, 'YYYY-MM-DD')},您已在千行学习<span>{total.days}</span>天
           </div>
           <div className="text">
-            累积<span dangerouslySetInnerHTML={{ __html: formatSeconds(total.time).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+            累积<span dangerouslySetInnerHTML={{ __html: formatSeconds(total.time).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
           </div>
         </div>
         <div className="title">本周数据</div>
         <div className="t-c">
           <div className="item">
             <div className="text">学习时间</div>
-            <div className="value" dangerouslySetInnerHTML={{ __html: formatSeconds(latest.time).replace(/([0-9]+)([msh])/g, '<span>$1</span>$2') }} />
+            <div className="value" dangerouslySetInnerHTML={{ __html: formatSeconds(latest.time).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span>$1</span>$2') }} />
           </div>
           <div className="item">
             <div className="text">同比上周</div>

+ 36 - 20
front/project/www/components/Login/index.js

@@ -19,13 +19,18 @@ const BIND_WX_ERROR = 'BIND_WX_ERROR';
 export default class Login extends Component {
   constructor(props) {
     super(props);
+    this.validNumber = 0;
     this.state = { type: LOGIN_WX, data: { area: MobileArea[0].value } };
     window.addEventListener(
       'message',
       event => {
         if (typeof event.data === 'string' && event.data.indexOf('code:') === 0) {
           const code = event.data.split(':')[1];
-          console.log(code);
+          if (this.state.type === LOGIN_WX) {
+            this.scanLogin(code);
+          } else if (this.state.type === BIND_WX) {
+            this.scanBind(code);
+          }
         }
       },
       false,
@@ -39,18 +44,18 @@ export default class Login extends Component {
   login() {
     const { data, needEmail, mobileError, validError } = this.state;
     const { area, mobile, mobileVerifyCode, email } = data;
-    if (mobileError !== '' || validError !== '') return;
-    if (area === '' || mobile === '' || mobileVerifyCode === '') return;
-    if (needEmail && email === '') return;
+    if (mobileError || validError) return;
+    if (!area || !mobile || !mobileVerifyCode) return;
+    if (needEmail && !email) return;
     User.login(area, mobile, mobileVerifyCode)
-      .then(() => {
+      .then((result) => {
         let handler = null;
         if (needEmail) {
           handler = My.bindEmail(email);
         } else {
           handler = Promise.resolve();
         }
-        handler.then(result => {
+        handler.then(() => {
           if (result.bindWechat) {
             this.close();
           } else {
@@ -70,9 +75,9 @@ export default class Login extends Component {
   bind() {
     const { data, needEmail, mobileError, validError } = this.state;
     const { area, mobile, mobileVerifyCode, email } = data;
-    if (mobileError !== '' || validError !== '') return;
-    if (area === '' || mobile === '' || mobileVerifyCode === '') return;
-    if (needEmail && email === '') return;
+    if (mobileError || validError) return;
+    if (!area || !mobile || !mobileVerifyCode) return;
+    if (needEmail && !email) return;
     User.bind(area, mobile, mobileVerifyCode)
       .then(() => {
         let handler = null;
@@ -94,8 +99,8 @@ export default class Login extends Component {
       });
   }
 
-  scanLogin() {
-    User.loginWechat('').then(result => {
+  scanLogin(code) {
+    User.loginWechat(code).then(result => {
       if (result.bindMobile) {
         this.close();
       } else {
@@ -104,8 +109,8 @@ export default class Login extends Component {
     });
   }
 
-  scanBind() {
-    User.loginWechat('')
+  scanBind(code) {
+    User.loginWechat(code)
       .then(() => {
         this.close();
       })
@@ -121,20 +126,23 @@ export default class Login extends Component {
     this.setState({ data });
   }
 
-  validMobile() {
+  validMobile(login) {
     const { data } = this.state;
     const { area, mobile } = data;
-    if (area === '' || mobile === '') return;
+    if (!area || !mobile) return;
+    this.validNumber += 1;
+    const number = this.validNumber;
     User.validWechat(area, mobile)
       .then(result => {
         if (result) {
           this.setState({ mobileError: '' });
           return User.validMobile(area, mobile).then(r => {
+            if (number !== this.validNumber) return;
             this.setState({ needEmail: r });
           });
         }
         this.setState({ needEmail: false });
-        return Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
+        return login ? Promise.resolve() : Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
       })
       .catch(err => {
         this.setState({ mobileError: err.message });
@@ -144,7 +152,7 @@ export default class Login extends Component {
   sendValid() {
     const { data, mobileError } = this.state;
     const { area, mobile } = data;
-    if (area === '' || mobile === '' || mobileError !== '') return Promise.reject();
+    if (!area || !mobile || mobileError) return Promise.reject();
     return Common.sendSms(area, mobile)
       .then(result => {
         if (result) {
@@ -164,7 +172,7 @@ export default class Login extends Component {
     const { type } = this.state;
     const { user } = this.props;
     return (
-      <Modal wrapClassName={`login-modal ${type}`} visible={user.needLogin} footer={null} closable={false} width={470}>
+      <Modal ref={(ref) => { this.modalR = ref; }} wrapClassName={`login-modal ${type}`} visible={user.needLogin} footer={null} closable={false} width={470}>
         {this.renderBody(type)}
         <GIcon
           name="close"
@@ -209,10 +217,11 @@ export default class Login extends Component {
           error={this.state.mobileError}
           onSelect={value => {
             this.changeData('area', value);
+            this.validMobile(true);
           }}
           onChange={e => {
             this.changeData('mobile', e.target.value);
-            this.validMobile(e.target.value);
+            this.validMobile(true);
           }}
         />
         <VerificationInput
@@ -222,6 +231,9 @@ export default class Login extends Component {
           onSend={() => {
             return this.sendValid();
           }}
+          onChange={e => {
+            this.changeData('mobileVerifyCode', e.target.value);
+          }}
         />
         {needEmail && (
           <Input
@@ -295,10 +307,11 @@ export default class Login extends Component {
           error={this.state.mobileError}
           onSelect={value => {
             this.changeData('area', value);
+            this.validMobile(false);
           }}
           onChange={e => {
             this.changeData('mobile', e.target.value);
-            this.validMobile(e.target.value);
+            this.validMobile(false);
           }}
         />
         <VerificationInput
@@ -308,6 +321,9 @@ export default class Login extends Component {
           onSend={() => {
             return this.sendValid();
           }}
+          onChange={e => {
+            this.changeData('mobileVerifyCode', e.target.value);
+          }}
         />
         {needEmail && (
           <Input

+ 7 - 6
front/project/www/components/Panel/index.js

@@ -8,17 +8,18 @@ import Module from '../Module';
 import ProgressButton from '../ProgressButton';
 import Button from '../Button';
 
-function makePie(value) {
+function makePie(value, text, subtext) {
   return {
     title: {
-      text: '30%',
+      text,
       textAlign: 'center',
       textVerticalAlign: 'middle',
-      subtext: '全站 60%',
+      subtext,
       top: '28%',
       left: '48%',
     },
-    color: [value < 50 ? '#f19057' : '#6966fb', '#f7f7f7'],
+    // value < 50 ? '#f19057' :
+    color: ['#6966fb', '#f7f7f7'],
     series: [
       {
         type: 'pie',
@@ -45,7 +46,7 @@ export default function Panel(props) {
       </div>
       <div className="body">
         <div className="chart-info">
-          <PieChart option={makePie(23)} width={110} height={110} />
+          <PieChart option={makePie(data.pieValue, data.pieText, data.pieSubText)} width={110} height={110} />
           <div className="info">
             {(data.info || []).map(row => {
               return (
@@ -179,7 +180,7 @@ export function SmallPanel(props) {
       </div>
       <div className="body" onClick={() => onClick && onClick()}>
         <div className="chart-info">
-          <PieChart option={makePie(33)} width={110} height={110} />
+          <PieChart option={makePie(data.pieValue, data.pieText, data.pieSubText)} width={110} height={110} />
           <div className="info">
             {(data.info || []).map(row => {
               return (

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

@@ -103,6 +103,10 @@ export default class extends Page {
           return r;
         });
 
+        row.pieValue = formatPercent(row.userNumber, row.questionNumber);
+        row.pieText = formatPercent(row.userNumber, row.questionNumber, false);
+        row.pieSubText = `共${row.questionNumber}`;
+
         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)}`];
@@ -136,6 +140,10 @@ export default class extends Page {
             unit: '套',
           },
         ];
+        row.pieValue = formatPercent(row.userNumber, row.questionNumber);
+        row.pieText = formatPercent(row.userNumber, row.questionNumber, false);
+        row.pieSubText = `共${row.questionNumber}套`;
+
         return row;
       });
       this.setState({ examinationProgress: result });

+ 4 - 0
front/project/www/routes/exercise/main/page.js

@@ -494,6 +494,10 @@ export default class extends Page {
         row.progress = formatPercent(row.questionNumber - row.userNumber || 0, row.questionNumber);
         row.totalCorrect = formatPercent(row.stat.totalCorrect, row.stat.totalNumber, false);
 
+        row.pieValue = formatPercent(row.userNumber, row.questionNumber);
+        row.pieText = formatPercent(row.userNumber, row.questionNumber, false);
+        row.pieSubText = `共${row.questionNumber}题`;
+
         row.children = row.children.map(r => {
           r.title = r.title || r.titleZh;
           r.progress = formatPercent(r.userNumber, r.questionNumber);

+ 5 - 6
front/project/www/routes/paper/process/sentence/index.js

@@ -154,7 +154,7 @@ export default class extends Component {
             用时:
             <span
               dangerouslySetInnerHTML={{
-                __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2'),
+                __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2'),
               }}
             />
             {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
@@ -164,7 +164,7 @@ export default class extends Component {
             <span
               dangerouslySetInnerHTML={{
                 __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(
-                  /([0-9]+)([msh])/g,
+                  /([0-9]+)(m|min|h|hour|s)/g,
                   '<span class="s">$1</span>$2',
                 ),
               }}
@@ -316,10 +316,9 @@ export default class extends Component {
           >
             {showAnswer ? '显示答案' : '关闭答案'}
           </Switch>
-        ) : (
-          <div className="title">
-            <Icon name="question" />
-            请分别找出句子中的主语,谓语和宾语,并做出逻辑关系判断。
+        ) : (<div className="title">
+          <Icon name="question" />
+          请分别找出句子中的主语,谓语和宾语,并做出逻辑关系判断。
           </div>
         )}
 

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

@@ -251,11 +251,11 @@ export default class extends Page {
       </div>
       <div className="right" hidden={question.questionType === 'awa'}>
         <span className="b" hidden={!userQuestion.id}>
-          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
           {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
         </span>
         <span className="b">
-          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
           {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
         </span>
         <span className="b">
@@ -446,7 +446,7 @@ export default class extends Page {
         {showAnswer && <div className='detail'>
           <div className='info'>
             <span className="b">
-              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
               {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
             </span>
             <span className="b">

+ 41 - 2
front/project/www/routes/paper/report/index.less

@@ -104,6 +104,35 @@
       background: #E3E5E9;
       display: inline-block;
     }
+
+  }
+
+  .list {
+    .detail {
+
+      .block {
+        width: 160px;
+      }
+
+      .t1 {
+        min-height: 30px;
+      }
+
+      .t4 {
+        height: 95px;
+        line-height: 95px;
+        color: #686872;
+        font-size: 24px;
+      }
+
+      .line {
+        height: 80px;
+        line-height: 80px;
+        width: 1px;
+        background: #E3E5E9;
+        display: inline-block;
+      }
+    }
   }
 
   .detail-1 {
@@ -163,8 +192,7 @@
   }
 }
 
-.exercise,
-.sentence {
+.exercise {
   border-top: 20px solid #7775CA;
   background: #fff;
 
@@ -175,6 +203,17 @@
   }
 }
 
+.sentence {
+  border-top: 20px solid #435C96;
+  background: #fff;
+
+  .body {
+    .title {
+      border-left: 10px solid #435C96;
+    }
+  }
+}
+
 .examination {
   border-top: 20px solid #8D909C;
   background: #fff;

文件差異過大導致無法顯示
+ 607 - 366
front/project/www/routes/paper/report/page.js


+ 3 - 3
front/project/www/routes/question/detail/page.js

@@ -237,11 +237,11 @@ export default class extends Page {
       </div>
       <div className="right" hidden={question.questionType === 'awa'}>
         <span className="b" hidden={!userQuestion.id}>
-          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
           {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
         </span>
         <span className="b">
-          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
           {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
         </span>
         <span className="b">
@@ -432,7 +432,7 @@ export default class extends Page {
         {showAnswer && <div className='detail'>
           <div className='info'>
             <span className="b">
-              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
               {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
             </span>
             <span className="b">

+ 3 - 2
front/project/www/stores/question.js

@@ -94,9 +94,10 @@ export default class QuestionStore extends BaseStore {
    * 通过记录及序号获取基础信息
    * @param {*} userReportId
    * @param {*} no
+   * @param {*} stage
    */
-  getDetailByNo(userReportId, no) {
-    return this.apiGet('/question/base', { userReportId, no });
+  getDetailByNo(userReportId, no, stage) {
+    return this.apiGet('/question/base', { userReportId, no, stage });
   }
 
   /**

+ 2 - 2
front/project/www/stores/user.js

@@ -147,8 +147,8 @@ export default class UserStore extends BaseStore {
   /**
    * 查询手机对应账号
    */
-  validMobile(mobile) {
-    return this.apiGet('/auth/valid/mobile', { mobile });
+  validMobile(area, mobile) {
+    return this.apiGet('/auth/valid/mobile', { area, mobile });
   }
 
   /**

+ 8 - 3
front/src/services/Tools.js

@@ -280,15 +280,20 @@ export function formatDate(time, format = 'YYYY-MM-DD HH:mm:ss') {
   return format;
 }
 
-export function formatSeconds(seconds) {
+export function formatMinute(seconds, number = true) {
+  const time = parseInt(seconds || 0, 10);
+  return number ? parseInt(time / 60, 10) : `${parseInt(time / 60, 10)}min`;
+}
+
+export function formatSeconds(seconds, rand = false) {
   const time = parseInt(seconds || 0, 10);
   if (time < 60) {
     return `${time}s`;
   }
   if (time >= 60 && time < 3600) {
-    return `${parseInt(time / 60, 10)}m${formatSeconds(time % 60)}`;
+    return `${parseInt(time / 60, 10)}min${rand ? '' : formatSeconds(time % 60)}`;
   }
-  return `${parseInt(time / 3600, 10)}h${formatSecond(time % 3600)}`;
+  return `${parseInt(time / 3600, 10)}hour${rand ? '' : formatSecond(time % 3600)}hour`;
 }
 
 export function formatPercent(child, mother, number = true) {

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

@@ -55,6 +55,12 @@ public class UserQuestion implements Serializable {
     private Integer no;
 
     /**
+     * 子阶段
+     */
+    @Column(name = "`stage`")
+    private String stage;
+
+    /**
      * 子阶段序号
      */
     @Column(name = "`stage_no`")
@@ -242,6 +248,24 @@ public class UserQuestion implements Serializable {
     }
 
     /**
+     * 获取子阶段
+     *
+     * @return stage - 子阶段
+     */
+    public String getStage() {
+        return stage;
+    }
+
+    /**
+     * 设置子阶段
+     *
+     * @param stage 子阶段
+     */
+    public void setStage(String stage) {
+        this.stage = stage;
+    }
+
+    /**
      * 获取子阶段序号
      *
      * @return stage_no - 子阶段序号
@@ -395,6 +419,7 @@ public class UserQuestion implements Serializable {
         sb.append(", questionId=").append(questionId);
         sb.append(", questionNoId=").append(questionNoId);
         sb.append(", no=").append(no);
+        sb.append(", stage=").append(stage);
         sb.append(", stageNo=").append(stageNo);
         sb.append(", time=").append(time);
         sb.append(", userTime=").append(userTime);
@@ -497,6 +522,16 @@ public class UserQuestion implements Serializable {
         }
 
         /**
+         * 设置子阶段
+         *
+         * @param stage 子阶段
+         */
+        public Builder stage(String stage) {
+            obj.setStage(stage);
+            return this;
+        }
+
+        /**
          * 设置子阶段序号
          *
          * @param stageNo 子阶段序号

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

@@ -13,6 +13,7 @@
     <result column="question_id" jdbcType="INTEGER" property="questionId" />
     <result column="question_no_id" jdbcType="INTEGER" property="questionNoId" />
     <result column="no" jdbcType="INTEGER" property="no" />
+    <result column="stage" jdbcType="VARCHAR" property="stage" />
     <result column="stage_no" jdbcType="INTEGER" property="stageNo" />
     <result column="time" jdbcType="INTEGER" property="time" />
     <result column="user_time" jdbcType="INTEGER" property="userTime" />
@@ -27,7 +28,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `user_id`, `report_id`, `question_module`, `question_type`, `question_id`, 
-    `question_no_id`, `no`, `stage_no`, `time`, `user_time`, `user_answer`, `is_correct`, 
-    `setting`, `detail`, `create_time`
+    `question_no_id`, `no`, `stage`, `stage_no`, `time`, `user_time`, `user_answer`, 
+    `is_correct`, `setting`, `detail`, `create_time`
   </sql>
 </mapper>

+ 5 - 0
server/data/src/main/java/com/qxgmat/data/relation/UserQuestionRelationMapper.java

@@ -4,6 +4,7 @@ import com.qxgmat.data.dao.entity.UserQuestion;
 import com.qxgmat.data.relation.entity.UserRecordStatRelation;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -33,6 +34,10 @@ public interface UserQuestionRelationMapper {
             String direction
     );
 
+    List<UserQuestion> listLast(
+            @Param("questionIds") Collection questionIds
+    );
+
     List<UserRecordStatRelation> stat(
             @Param("userId") Integer userId,
             @Param("startTime") String startTime,

+ 16 - 0
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserQuestionRelationMapper.xml

@@ -131,6 +131,22 @@
   </select>
 
   <!--
+    用户最后一次
+    https://blog.csdn.net/t_1007/article/details/52369261
+  -->
+  <select id="listLast" resultMap="IdMap">
+    select
+    <!--SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`-->
+    max(uq.`id`) as `id`
+    from `user_question` uq
+    where uq.`question_id` IN
+    <foreach collection="questionIds" item="item" index="index" open="(" close=")" separator=",">
+      #{item}
+    </foreach>
+    group by uq.`question_id`
+  </select>
+
+  <!--
     用户做题记录统计
   -->
   <select id="stat" resultMap="studyMap">

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

@@ -97,9 +97,9 @@ public class AuthController {
     @RequestMapping(value = "/login", method = RequestMethod.POST)
     @ApiOperation(value = "登录/注册", httpMethod = "POST")
     public Response<MyDto> login(@RequestBody @Validated UserLoginDto userLoginDto, HttpSession session, HttpServletRequest request) {
-        if (!smsHelp.verifyCode(userLoginDto.getArea(), userLoginDto.getMobile(), userLoginDto.getMobileVerifyCode(), session)) {
-            throw new ParameterException("手机验证码错误!");
-        }
+//        if (!smsHelp.verifyCode(userLoginDto.getArea(), userLoginDto.getMobile(), userLoginDto.getMobileVerifyCode(), session)) {
+//            throw new ParameterException("手机验证码错误!");
+//        }
         try {
             String ip = Tools.getClientIp(request);
             User user = usersService.register(userLoginDto.getArea(), userLoginDto.getMobile(), userLoginDto.getInviteCode(), null, ip, aiHelp.parseIp(ip));

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

@@ -875,6 +875,11 @@ public class MyController {
         Map<Object, UserQuestionStat> stats = userQuestionService.statQuestionMap(userQuestionList);
         Transform.combine(pr, stats, UserCollectQuestionInfoDto.class, "questionId", "stat");
 
+        // 最近做题
+        List<UserQuestion> lastList = userQuestionService.listWithLast(questionIds);
+        Map lastMap = Transform.getMap(lastList, UserQuestion.class, "id", "createTime");
+        Transform.combine(pr, lastMap, UserQuestionErrorInfoDto.class, "questionId", "latestTime");
+
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 
@@ -933,7 +938,16 @@ public class MyController {
         List<TextbookQuestion> textbookQuestionList = textbookQuestionService.select(textbookQuestionNoIds);
         Transform.combine(textbookPr, textbookQuestionList, UserQuestionErrorInfoDto.class, "questionNoId", "questionNo", TextbookQuestion.class, "id", QuestionNoExtendDto.class);
 
-        // 最近做题,及做题统计
+        // 绑定题目统计
+        List<UserQuestion> userQuestionList = userQuestionService.listByQuestion(user.getId(), questionIds);
+        Map<Object, UserQuestionStat> stats = userQuestionService.statQuestionMap(userQuestionList);
+        Transform.combine(pr, stats, UserQuestionErrorInfoDto.class, "questionId", "stat");
+
+        // 最近做题
+        List<UserQuestion> lastList = userQuestionService.listWithLast(questionIds);
+        Map lastMap = Transform.getMap(lastList, UserQuestion.class, "id", "createTime");
+        Transform.combine(pr, lastMap, UserQuestionErrorInfoDto.class, "questionId", "latestTime");
+
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 

+ 77 - 2
server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java

@@ -3,6 +3,7 @@ package com.qxgmat.controller.api;
 
 import com.alibaba.fastjson.JSONObject;
 import com.nuliji.tools.*;
+import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.constants.enums.QuestionType;
 import com.qxgmat.data.constants.enums.ServiceKey;
@@ -503,13 +504,17 @@ public class QuestionController {
     @ApiOperation(value = "获取题目详情", notes = "根据题目序号获取题目", httpMethod = "GET")
     public Response<UserQuestionBaseDto> base(
             @RequestParam(required = true) Integer userReportId,
-            @RequestParam(required = true) Integer no
+            @RequestParam(required = true) Integer no,
+            @RequestParam(required = false) String stage
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         if (no == null || no == 0){
             no = 1;
         }
-        UserQuestion userQuestion = userQuestionService.getByReportAndNo(user.getId(), userReportId, no);
+        UserQuestion userQuestion = userQuestionService.getByReportAndNo(user.getId(), userReportId, no, stage);
         UserQuestionBaseDto dto = Transform.convert(userQuestion, UserQuestionBaseDto.class);
 
         return ResponseHelp.success(dto);
@@ -606,6 +611,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.EXERCISE, paperId);
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
@@ -618,6 +626,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.EXAMINATION, paperId);
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
@@ -630,6 +641,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserReportRelation report = questionFlowService.baseReport(user.getId(), userReportId);
 
         UserReportBaseDto userReportDto = Transform.convert(report, UserReportBaseDto.class);
@@ -642,6 +656,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserReportRelation report = questionFlowService.baseReport(user.getId(), userReportId);
 
         UserReportDetailDto userReportDto = Transform.convert(report, UserReportDetailDto.class);
@@ -659,6 +676,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         List<UserQuestion> userQuestionList = questionFlowService.listByReport(user.getId(), userReportId);
         List<UserQuestionExtendDto> userQuestionDtos = Transform.convert(userQuestionList, UserQuestionExtendDto.class);
 
@@ -681,6 +701,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 模考", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startExamination(@RequestBody @Validated ExaminationStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         // 判断是否是cat模考,并且有模考权限
         ExaminationPaper examinationPaper = examinationPaperService.get(dto.getPaperId());
         if(examinationService.isCat(examinationPaper)){
@@ -701,6 +724,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 练习", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startExercise(@RequestBody @Validated ExerciseStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         setting.put("disorder", dto.getDisorder());
         UserReport report = questionFlowService.start(user.getId(), PaperOrigin.EXERCISE, dto.getPaperId(), setting);
@@ -712,6 +738,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 预习作业", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startPreview(@RequestBody @Validated PreviewStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         setting.put("disorder", dto.getDisorder());
         UserReportRelation report = questionFlowService.start(user.getId(), PaperOrigin.PREVIEW, dto.getPaperId(), setting);
@@ -727,6 +756,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.TEXTBOOK, paperId);
 
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
@@ -738,6 +770,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 机经", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startTextbook(@RequestBody @Validated TextbookStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         setting.put("disorder", dto.getDisorder());
         UserReportRelation report = questionFlowService.start(user.getId(), PaperOrigin.TEXTBOOK, dto.getPaperId(), setting);
@@ -753,6 +788,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.SENTENCE, paperId);
 
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
@@ -764,6 +802,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 长难句", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startSentence(@RequestBody @Validated SentenceStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         UserReportRelation report = questionFlowService.start(user.getId(), PaperOrigin.SENTENCE, dto.getPaperId(), setting);
 
@@ -778,6 +819,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = userPaperService.get(paperId);
         if (!paper.getPaperOrigin().equals(PaperOrigin.ERROR.key)){
             throw new ParameterException("试卷不存在");
@@ -794,6 +838,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 错题组卷", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startError(@RequestBody @Validated PreviewStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         setting.put("disorder", dto.getDisorder());
         UserReportRelation report = questionFlowService.start(user.getId(), PaperOrigin.ERROR, dto.getPaperId(), setting);
@@ -809,6 +856,9 @@ public class QuestionController {
             @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserPaper paper = userPaperService.get(paperId);
         if (!paper.getPaperOrigin().equals(PaperOrigin.COLLECT.key)){
             throw new ParameterException("试卷不存在");
@@ -825,6 +875,9 @@ public class QuestionController {
     @ApiOperation(value = "开始: 收藏组卷", notes = "提交考试设置", httpMethod = "POST")
     public Response<UserReportBaseDto> startCollect(@RequestBody @Validated PreviewStartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         JSONObject setting = new JSONObject();
         setting.put("disorder", dto.getDisorder());
         UserReportRelation report = questionFlowService.start(user.getId(), PaperOrigin.COLLECT, dto.getPaperId(), setting);
@@ -838,6 +891,9 @@ public class QuestionController {
     @ApiOperation(value = "继续做题", notes = "获取报告信息", httpMethod = "POST")
     public Response<UserReportBaseDto> continueReport(@RequestBody @Validated ReportContinueDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserReportRelation report = questionFlowService.continueReport(user.getId(), dto.getUserReportId());
 
         UserReportBaseDto userReportBaseDto = Transform.convert(report, UserReportBaseDto.class);
@@ -849,6 +905,9 @@ public class QuestionController {
     @ApiOperation(value = "获取下一题", notes = "获取下一题", httpMethod = "POST")
     public Response<UserQuestionBaseDto> next(@RequestBody @Validated ReportNextDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserQuestion userQuestion = questionFlowService.next(user.getId(), dto.getUserReportId());
         if (userQuestion == null) {
             throw new ParameterException("finish");
@@ -874,6 +933,9 @@ public class QuestionController {
     @ApiOperation(value = "提交题目答案", notes = "提交题目", httpMethod = "POST")
     public Response<Boolean> submit(@RequestBody @Validated QuestionSubmitDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
         UserQuestion userQuestion = userQuestionService.get(dto.getUserQuestionId());
         if (userQuestion == null){
             throw new ParameterException("做题不存在");
@@ -889,6 +951,9 @@ public class QuestionController {
     @ApiOperation(value = "完成考试", notes = "完成考试", httpMethod = "POST")
     public Response<Boolean> finish(@RequestBody @Validated ReportFinishDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
 
         Boolean result = questionFlowService.finish(user.getId(), dto.getUserReportId());
         return ResponseHelp.success(result);
@@ -898,6 +963,10 @@ public class QuestionController {
     @ApiOperation(value = "本阶段完成", notes = "结束当前阶段: 达到阶段时间时调用,然后继续调用next", httpMethod = "POST")
     public Response<Boolean> stage(@RequestBody @Validated ReportStageDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
+
         Boolean result = questionFlowService.stage(user.getId(), dto.getUserReportId());
         return ResponseHelp.success(result);
     }
@@ -906,6 +975,9 @@ public class QuestionController {
     @ApiOperation(value = "重置考试", notes = "重置考试", httpMethod = "POST")
     public Response<Boolean> restart(@RequestBody @Validated PaperRestartDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
 
         questionFlowService.restart(dto.getUserPaperId(), user.getId());
         return ResponseHelp.success(true);
@@ -915,6 +987,9 @@ public class QuestionController {
     @ApiOperation(value = "重置整套模拟卷", notes = "重置考试", httpMethod = "POST")
     public Response<Boolean> resetCat()  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null) {
+            throw new AuthException("请先登录");
+        }
 
         UserService userService = userServiceService.getServiceBase(user.getId(), ServiceKey.QX_CAT);
         if (userService == null){

+ 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 String title;
+
     private Integer paperNo;
 
     private Integer questionNumber;
@@ -58,4 +60,12 @@ public class UserPaperBaseExtendDto {
     public void setPaperNo(Integer paperNo) {
         this.paperNo = paperNo;
     }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
 }

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

@@ -7,18 +7,12 @@ import io.swagger.annotations.ApiModelProperty;
  * Created by GaoJie on 2017/11/1.
  */
 public class MyDto extends UserDto {
-    private Integer id;
-
-    private String nickname;
-
     private String avatar;
 
     private String email;
 
     private String inviteCode;
 
-    private Integer qxCat;
-
     private Boolean bindWechat;
 
     private Boolean bindReal;
@@ -33,7 +27,6 @@ public class MyDto extends UserDto {
 
     private Boolean vip;
 
-    @ApiModelProperty(value = "未读消息数", required = true)
     private int messageNum;
 
     public int getMessageNum() {
@@ -44,16 +37,6 @@ public class MyDto extends UserDto {
         this.messageNum = messageNum;
     }
 
-    @Override
-    public String getNickname() {
-        return nickname;
-    }
-
-    @Override
-    public void setNickname(String nickname) {
-        this.nickname = nickname;
-    }
-
     public String getAvatar() {
         return avatar;
     }
@@ -133,21 +116,4 @@ public class MyDto extends UserDto {
     public void setVip(Boolean vip) {
         this.vip = vip;
     }
-
-    @Override
-    public Integer getId() {
-        return id;
-    }
-
-    public void setId(Integer id) {
-        this.id = id;
-    }
-
-    public Integer getQxCat() {
-        return qxCat;
-    }
-
-    public void setQxCat(Integer qxCat) {
-        this.qxCat = qxCat;
-    }
 }

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

@@ -24,6 +24,8 @@ public class UserCollectQuestionInfoDto {
 
     private Date createTime;
 
+    private Date latestTime;
+
     private UserQuestionStat stat;
 
     public Integer getId() {
@@ -89,4 +91,12 @@ public class UserCollectQuestionInfoDto {
     public void setQuestionNoId(Integer questionNoId) {
         this.questionNoId = questionNoId;
     }
+
+    public Date getLatestTime() {
+        return latestTime;
+    }
+
+    public void setLatestTime(Date latestTime) {
+        this.latestTime = latestTime;
+    }
 }

+ 11 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserQuestionErrorInfoDto.java

@@ -2,6 +2,7 @@ package com.qxgmat.dto.response;
 
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserQuestion;
+import com.qxgmat.data.inline.UserQuestionStat;
 import com.qxgmat.dto.extend.QuestionExtendDto;
 import com.qxgmat.dto.extend.QuestionNoExtendDto;
 
@@ -25,6 +26,8 @@ public class UserQuestionErrorInfoDto {
 
     private Date latestTime;
 
+    private UserQuestionStat stat;
+
     public Integer getId() {
         return id;
     }
@@ -88,4 +91,12 @@ public class UserQuestionErrorInfoDto {
     public void setLatestTime(Date latestTime) {
         this.latestTime = latestTime;
     }
+
+    public UserQuestionStat getStat() {
+        return stat;
+    }
+
+    public void setStat(UserQuestionStat stat) {
+        this.stat = stat;
+    }
 }

+ 11 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserReportDetailDto.java

@@ -3,6 +3,7 @@ package com.qxgmat.dto.response;
 import com.alibaba.fastjson.JSONObject;
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserReport;
+import com.qxgmat.dto.extend.UserExtendDto;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
 import com.qxgmat.dto.extend.UserQuestionExtendDto;
 
@@ -13,6 +14,8 @@ import java.util.List;
 public class UserReportDetailDto {
     private Integer id;
 
+    private UserExtendDto user;
+
     private Integer paperId;
 
     private UserPaperBaseExtendDto paper;
@@ -170,4 +173,12 @@ public class UserReportDetailDto {
     public void setFinishTime(Date finishTime) {
         this.finishTime = finishTime;
     }
+
+    public UserExtendDto getUser() {
+        return user;
+    }
+
+    public void setUser(UserExtendDto user) {
+        this.user = user;
+    }
 }

+ 27 - 2
server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java

@@ -101,6 +101,19 @@ public class UserQuestionService extends AbstractService {
     }
 
     /**
+     * 查找userQuestion最后一次做题记录
+     * @param questionIds
+     */
+    public List<UserQuestion> listWithLast(Collection questionIds){
+        if(questionIds == null || questionIds.size() == 0) return new ArrayList<>();
+        List<UserQuestion> list = userQuestionRelationMapper.listLast(questionIds);
+
+        Collection reportIds = Transform.getIds(list, UserQuestion.class, "id");
+        Transform.replace(list, select(reportIds), UserQuestion.class, "id");
+        return list;
+    }
+
+    /**
      * 根据用户题目获取该题总统计
      * @param userQuestionList
      * @return
@@ -226,14 +239,25 @@ public class UserQuestionService extends AbstractService {
      * @param no
      * @return
      */
-    public UserQuestion getByReportAndNo(Integer userId, Integer userReportId, Integer no){
+    public UserQuestion getByReportAndNo(Integer userId, Integer userReportId, Integer no, String stage){
         Example example = new Example(UserQuestion.class);
         example.and(
                 example.createCriteria()
                         .andEqualTo("userId", userId)
                         .andEqualTo("reportId", userReportId)
-                        .andEqualTo("no", no)
         );
+        if (stage == null){
+            example.and(
+                    example.createCriteria()
+                    .andEqualTo("no", no)
+            );
+        }else{
+            example.and(
+                    example.createCriteria()
+                    .andEqualTo("stage", stage)
+                    .andEqualTo("stageNo", no)
+            );
+        }
         return one(userQuestionMapper, example);
     }
 
@@ -253,6 +277,7 @@ public class UserQuestionService extends AbstractService {
                 // 添加题目序号
                 .no(lastQuestion != null ? lastQuestion.getNo() + 1: 1)
                 // 子阶段序号:外部切换stage重置
+                .stage(lastQuestion != null ? lastQuestion.getStage(): "")
                 .stageNo(lastQuestion != null ? lastQuestion.getStageNo() + 1: 1)
                 .build();
         return question;

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

@@ -197,7 +197,7 @@ public class UsersService extends AbstractService {
             }
         }else{
             // 注册,并且绑定邀请者
-            user = User.builder().mobile(mobile).build();
+            user = User.builder().area(area).mobile(mobile).build();
             n = true;
             if (inviteCode != null && !inviteCode.isEmpty()){
                 User origin = getByInviteCode(inviteCode);

+ 75 - 15
server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java

@@ -784,6 +784,7 @@ public class QuestionFlowService {
             setting.put("stage", stage);
             // 重置stage数
             question.setStageNo(1);
+            question.setStage(stage);
         }
         // 获取本阶段完成数,剩余题目数
         Integer subnumber =  number.getIntValue(stage);
@@ -1509,19 +1510,22 @@ public class QuestionFlowService {
         JSONObject subjectMap = new JSONObject();
         JSONObject typeMap = new JSONObject();
         JSONObject tempMap = new JSONObject();
-
+        JSONObject difficult = null;
+        JSONObject place = null;
         for (UserQuestion userQuestion : questionList){
             QuestionNoRelation relation = relationMap.get(userQuestion.getQuestionNoId());
             QuestionType questionType = QuestionType.ValueOf(relation.getQuestion().getQuestionType());
             QuestionSubject questionSubject = QuestionSubject.FromType(questionType);
             JSONObject type = typeMap.getJSONObject(questionType.key);
-            JSONObject temp = tempMap.getJSONObject(questionSubject.key);
+            JSONObject tempType = tempMap.getJSONObject(questionType.key);
             JSONObject subject = subjectMap.getJSONObject(questionSubject.key);
+            JSONObject tempSubject = tempMap.getJSONObject(questionSubject.key);
 
             // 归类
             if (type == null || type.isEmpty()){
                 type = new JSONObject();
                 type.put("key", questionType.key);
+                subject.put("pace", new JSONArray());
                 JSONObject initTypeInfo = new JSONObject();
                 // 初始化题型基础信息
                 initTypeInfo.put("userNumber", 0);
@@ -1532,10 +1536,11 @@ public class QuestionFlowService {
                 initTypeInfo.put("diffCorrect", 0f);
                 initTypeInfo.put("diffIncorrect", 0f);
                 type.put("info", initTypeInfo);
-                temp = new JSONObject();
-                temp.put("place", new JSONObject());
-                temp.put("difficult", new JSONObject());
-                tempMap.put(questionSubject.key, temp);
+                typeMap.put(questionType.key, type);
+                tempType = new JSONObject();
+                tempType.put("place", new JSONObject());
+                tempType.put("difficult", new JSONObject());
+                tempMap.put(questionType.key, tempType);
             }
             if (subject == null || subject.isEmpty()){
                 subject = new JSONObject();
@@ -1548,33 +1553,59 @@ public class QuestionFlowService {
                 initSubjectInfo.put("time", subjectBase.getIntValue("time"));
                 initSubjectInfo.put("userNumber", 0);
                 initSubjectInfo.put("userTime", 0);
+                initSubjectInfo.put("userCorrect", 0);
+                initSubjectInfo.put("correctTime", 0);
+                initSubjectInfo.put("incorrectTime", 0);
+                initSubjectInfo.put("diffCorrect", 0f);
+                initSubjectInfo.put("diffIncorrect", 0f);
                 initSubjectInfo.put("difficultScore", 0);
                 subject.put("info", initSubjectInfo);
                 subjectMap.put(questionSubject.key, subject);
+                tempSubject = new JSONObject();
+                tempSubject.put("place", new JSONObject());
+                tempSubject.put("difficult", new JSONObject());
+                tempMap.put(questionSubject.key, tempType);
             }
-            JSONArray pace = subject.getJSONArray("pace");
-            JSONObject placeMap = temp.getJSONObject("place");
-            JSONObject difficultMap = temp.getJSONObject("difficult");
+            JSONArray paceType = type.getJSONArray("pace");
+            JSONObject placeTypeMap = tempType.getJSONObject("place");
+            JSONObject difficultTypeMap = tempType.getJSONObject("difficult");
             JSONObject typeInfo = type.getJSONObject("info");
-            JSONObject subjectInfo = type.getJSONObject("info");
+            JSONArray paceSubject = subject.getJSONArray("pace");
+            JSONObject placeSubjectMap = tempType.getJSONObject("place");
+            JSONObject difficultSubjectMap = tempType.getJSONObject("difficult");
+            JSONObject subjectInfo = subject.getJSONObject("info");
 
             // 每题用时
             JSONObject one = new JSONObject();
             one.put("time", userQuestion.getTime());
             one.put("userTime", userQuestion.getUserTime());
             one.put("no", userQuestion.getNo());
-            pace.add(one);
+            paceSubject.add(one);
+            paceType.add(one);
 
             // 考点用时,以及正确度
             String placeKey = relation.getQuestion().getPlace();
-            JSONObject place = placeMap.getJSONObject(placeKey);
+            place = placeTypeMap.getJSONObject(placeKey);
             if (place == null){
                 place = new JSONObject();
                 place.put("key", placeKey);
                 place.put("userNumber", 1);
                 place.put("userCorrect", userQuestion.getIsCorrect());
                 place.put("userTime", userQuestion.getUserTime());
-                placeMap.put(placeKey, place);
+                placeTypeMap.put(placeKey, place);
+            }else{
+                place.put("userNumber", place.getInteger("userNumber") + 1);
+                place.put("userCorrect", place.getInteger("userCorrect") + userQuestion.getIsCorrect());
+                place.put("userTime", place.getInteger("userTime") + userQuestion.getUserTime());
+            }
+            place = placeSubjectMap.getJSONObject(placeKey);
+            if (place == null){
+                place = new JSONObject();
+                place.put("key", placeKey);
+                place.put("userNumber", 1);
+                place.put("userCorrect", userQuestion.getIsCorrect());
+                place.put("userTime", userQuestion.getUserTime());
+                placeSubjectMap.put(placeKey, place);
             }else{
                 place.put("userNumber", place.getInteger("userNumber") + 1);
                 place.put("userCorrect", place.getInteger("userCorrect") + userQuestion.getIsCorrect());
@@ -1583,7 +1614,7 @@ public class QuestionFlowService {
 
             // 难度正确度
             String difficultKey = relation.getQuestion().getDifficult();
-            JSONObject difficult = difficultMap.getJSONObject(difficultKey);
+            difficult = difficultTypeMap.getJSONObject(difficultKey);
             if (difficult == null){
                 difficult = new JSONObject();
                 difficult.put("key", difficultKey);
@@ -1591,7 +1622,22 @@ public class QuestionFlowService {
                 difficult.put("userCorrect", userQuestion.getIsCorrect());
                 difficult.put("totalNumber", relation.getTotalNumber());
                 difficult.put("totalCorrect", relation.getTotalCorrect());
-                difficultMap.put(difficultKey, difficult);
+                difficultTypeMap.put(difficultKey, difficult);
+            }else{
+                difficult.put("userNumber", difficult.getInteger("userNumber") + 1);
+                difficult.put("userCorrect", difficult.getInteger("userCorrect") + userQuestion.getIsCorrect());
+                difficult.put("totalNumber", difficult.getInteger("totalNumber") + relation.getTotalNumber());
+                difficult.put("totalCorrect", difficult.getInteger("totalCorrect") + relation.getTotalCorrect());
+            }
+            difficult = difficultSubjectMap.getJSONObject(difficultKey);
+            if (difficult == null){
+                difficult = new JSONObject();
+                difficult.put("key", difficultKey);
+                difficult.put("userNumber", 1);
+                difficult.put("userCorrect", userQuestion.getIsCorrect());
+                difficult.put("totalNumber", relation.getTotalNumber());
+                difficult.put("totalCorrect", relation.getTotalCorrect());
+                difficultSubjectMap.put(difficultKey, difficult);
             }else{
                 difficult.put("userNumber", difficult.getInteger("userNumber") + 1);
                 difficult.put("userCorrect", difficult.getInteger("userCorrect") + userQuestion.getIsCorrect());
@@ -1611,13 +1657,21 @@ public class QuestionFlowService {
 
             subjectInfo.put("userNumber", subjectInfo.getIntValue("userNumber")+1);
             subjectInfo.put("userTime", subjectInfo.getIntValue("userTime")+userQuestion.getUserTime());
+            subjectInfo.put("userCorrect", subjectInfo.getIntValue("userCorrect")+userQuestion.getIsCorrect());
+            if (userQuestion.getIsCorrect() > 0){
+                subjectInfo.put("correctTime", subjectInfo.getIntValue("correctTime")+userQuestion.getUserTime());
+            }else{
+                subjectInfo.put("incorrectTime", subjectInfo.getIntValue("incorrectTime")+userQuestion.getUserTime());
+            }
 
             // 题型难度分计算
             QuestionDifficult questionDifficult = QuestionDifficult.ValueOf(difficultKey);
             if (userQuestion.getIsCorrect() > 0){
                 typeInfo.put("diffCorrect", typeInfo.getFloatValue("diffCorrect") + toolsService.diffScore(relation.getTotalNumber(), relation.getTotalCorrect(), questionDifficult));
+                subjectInfo.put("diffCorrect", subjectInfo.getFloatValue("diffCorrect") + toolsService.diffScore(relation.getTotalNumber(), relation.getTotalCorrect(), questionDifficult));
             }else{
                 typeInfo.put("diffIncorrect", typeInfo.getFloatValue("diffIncorrect") + toolsService.diffScore(relation.getTotalNumber(), relation.getTotalCorrect(), questionDifficult));
+                subjectInfo.put("diffIncorrect", subjectInfo.getFloatValue("diffIncorrect") + toolsService.diffScore(relation.getTotalNumber(), relation.getTotalCorrect(), questionDifficult));
             }
 
             // 获取2级难度得分
@@ -1632,6 +1686,12 @@ public class QuestionFlowService {
             typeInfo.put("avgDiffCorrect", toolsService.avgDiffScore(typeInfo.getFloat("diffCorrect"), typeInfo.getIntValue("userCorrect")));
             typeInfo.put("avgDiffIncorrect", toolsService.avgDiffScore(typeInfo.getFloat("diffIncorrect"), typeInfo.getIntValue("userNumber") - typeInfo.getIntValue("userCorrect")));
         }
+        for (String key : subjectMap.keySet()) {
+            JSONObject subject = subjectMap.getJSONObject(key);
+            JSONObject subjectInfo = subject.getJSONObject("info");
+            subjectInfo.put("avgDiffCorrect", toolsService.avgDiffScore(subjectInfo.getFloat("diffCorrect"), subjectInfo.getIntValue("userCorrect")));
+            subjectInfo.put("avgDiffIncorrect", toolsService.avgDiffScore(subjectInfo.getFloat("diffIncorrect"), subjectInfo.getIntValue("userNumber") - subjectInfo.getIntValue("userCorrect")));
+        }
 
         // 学科得分计算
         JSONObject quantSubject = subjectMap.getJSONObject(QuestionSubject.QUANT.key);