Browse Source

feat(front): 个人中心-工具

Go 5 years ago
parent
commit
280137bcb1
38 changed files with 1102 additions and 330 deletions
  1. 5 0
      front/project/Constant.js
  2. 66 0
      front/project/admin/routes/show/deploy/page.js
  3. 8 0
      front/project/admin/stores/system.js
  4. 0 1
      front/project/h5/routes/textbook/main/page.js
  5. 6 10
      front/project/www/components/Examination/index.js
  6. 2 2
      front/project/www/local.json
  7. 99 75
      front/project/www/routes/my/data/page.js
  8. 10 7
      front/project/www/routes/my/main/page.js
  9. 52 17
      front/project/www/routes/my/message/page.js
  10. 377 113
      front/project/www/routes/my/tools/page.js
  11. 28 4
      front/project/www/stores/my.js
  12. 3 1
      server/data/src/main/java/com/qxgmat/data/constants/enums/QuestionSubject.java
  13. 2 1
      server/data/src/main/java/com/qxgmat/data/constants/enums/SettingKey.java
  14. 26 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/TextbookSubject.java
  15. 26 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/CourseData.java
  16. 105 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/TextbookLibrary.java
  17. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseDataMapper.xml
  18. 6 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/TextbookLibraryMapper.xml
  19. 3 3
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataHistoryRelationMapper.xml
  20. 3 3
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataRelationMapper.xml
  21. 7 2
      server/data/src/main/resources/db/migration/V1__init_table.sql
  22. 17 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/SettingController.java
  23. 7 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/TextbookController.java
  24. 48 8
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  25. 1 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  26. 3 2
      server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java
  27. 13 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/DataSubscribeDto.java
  28. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  29. 20 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserDataDto.java
  30. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserExaminationInfoDto.java
  31. 13 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserPrepareDetailDto.java
  32. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserTextbookInfoDto.java
  33. 47 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserVipInfoDto.java
  34. 0 2
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExaminationService.java
  35. 11 70
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  36. 45 5
      server/gateway-api/src/main/java/com/qxgmat/service/extend/TextbookService.java
  37. 10 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserTextbookEnrollService.java
  38. 1 0
      server/gateway-api/src/main/java/com/qxgmat/task/AsyncTask.java

+ 5 - 0
front/project/Constant.js

@@ -143,6 +143,11 @@ export const MessageCustomStatus = [
   { label: '发送完', value: 2 },
 ];
 
+export const MessageType = [
+  { label: '系统消息', value: 'system' },
+  { label: '动态消息', value: 'feed' },
+];
+
 export const FaqChannel = [
   { label: 'GetReady', value: 'getready' },
   { label: '模考', value: 'examination' },

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

@@ -22,6 +22,9 @@ export default class extends Page {
     if (tab === 'sentence') {
       return this.refreshSentence();
     }
+    if (tab === 'prepare') {
+      return this.refreshPrepare();
+    }
     return Promise.reject();
   }
 
@@ -33,6 +36,14 @@ export default class extends Page {
     });
   }
 
+  refreshPrepare() {
+    return System.getPrepareInfo().then(result => {
+      this.setState({ prepare: result || {} });
+      const { form } = this.props;
+      form.setFieldsValue(flattenObject(result, 'prepare'));
+    });
+  }
+
   changeMapValue(field, second, index, key, value) {
     const data = this.state[field] || {};
     data[second] = data[second] || [];
@@ -53,6 +64,9 @@ export default class extends Page {
     if (tab === 'sentence') {
       handler = this.submitSentence();
     }
+    if (tab === 'prepare') {
+      handler = this.submitPrepare();
+    }
     handler.then(() => {
       asyncSMessage('保存成功');
     });
@@ -77,6 +91,25 @@ export default class extends Page {
     });
   }
 
+  submitPrepare() {
+    const { form } = this.props;
+    return new Promise((resolve, reject) => {
+      form.validateFields(['prepare'], (err, values) => {
+        if (err) {
+          reject(err);
+        }
+        const data = values.prepare;
+        return System.setPrepareInfo(data)
+          .then(() => {
+            resolve();
+          }).catch((e) => {
+            form.setFields(formatFormError(data, e.result));
+            reject(e);
+          });
+      });
+    });
+  }
+
   renderSentence() {
     const { getFieldDecorator } = this.props.form;
     return <Form>
@@ -113,6 +146,36 @@ export default class extends Page {
     </Form>;
   }
 
+  renderPrepare() {
+    const { getFieldDecorator } = this.props.form;
+    return <Form>
+      <Row>
+        <Form.Item labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} label='访问链接'>
+          {getFieldDecorator('prepare.link', {
+            rules: [
+              { required: true, message: '输入访问链接' },
+            ],
+          })(
+            <Input placeholder='请输入访问链接' onChange={(e) => {
+              this.changeValue('prepare', 'link', e.target.value);
+            }} style={{ width: '200px' }} />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} label='访问标题'>
+          {getFieldDecorator('prepare.title', {
+            rules: [
+              { required: true, message: '输入访问标题' },
+            ],
+          })(
+            <Input placeholder='请输入访问标题' onChange={(value) => {
+              this.changeValue('prepare', 'title', value);
+            }} style={{ width: '200px' }} />,
+          )}
+        </Form.Item>
+      </Row>
+    </Form>;
+  }
+
   renderView() {
     const { tab } = this.state;
     return <Block><Tabs activeKey={tab} onChange={(value) => {
@@ -122,6 +185,9 @@ export default class extends Page {
       <Tabs.TabPane tab="长难句购买" key="sentence">
         {this.renderSentence()}
       </Tabs.TabPane>
+      <Tabs.TabPane tab="备考时间" key="prepare">
+        {this.renderPrepare()}
+      </Tabs.TabPane>
     </Tabs>
       <Row type="flex" justify="center">
         <Col>

+ 8 - 0
front/project/admin/stores/system.js

@@ -165,6 +165,14 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/tips', params);
   }
 
+  getPrepareInfo() {
+    return this.apiGet('/setting/prepare_info');
+  }
+
+  setPrepareInfo(params) {
+    return this.apiPut('/setting/prepare_info', params);
+  }
+
   getSentenceInfo() {
     return this.apiGet('/setting/sentence_info');
   }

+ 0 - 1
front/project/h5/routes/textbook/main/page.js

@@ -25,7 +25,6 @@ export default class extends Page {
     Textbook.getInfo()
       .then(result => {
         result.day = parseInt((new Date().getTime() - new Date(result.latest.startDate).getTime()) / 86400000, 10);
-        result.hasService = true;
         this.setState(result);
       });
     this.refreshTab(this.state.tab);

+ 6 - 10
front/project/www/components/Examination/index.js

@@ -10,7 +10,6 @@ import TotalSort from '../TotalSort';
 import Date from '../Date';
 import Ratio from '../Ratio';
 import { My } from '../../stores/my';
-import { Main } from '../../stores/main';
 import { PrepareStatus, PrepareExaminationTime, PrepareScoreTime } from '../../../Constant';
 
 const PrepareStatusMap = getMap(PrepareStatus, 'value', 'short');
@@ -84,11 +83,8 @@ export default class extends Component {
         };
         result.prepareGoal = result.prepareGoal || 650;
         result.prepareScoreTime = result.prepareScoreTime ? moment(result.prepareScoreTime) : null;
-        this.setState({ data: result, stat, first: !result.prepareStatus, step: !result.prepareStatus ? 0 : 4 });
+        this.setState({ data: result, stat, first: !result.prepareStatus, step: !result.prepareStatus ? 0 : 4, info: result.info });
       });
-    Main.getContract('register').then(() => {
-      this.setState({ link: { url: 'www', title: '了解出分时间信息>' } });
-    });
   }
 
   onChange(type, key) {
@@ -150,7 +146,7 @@ export default class extends Component {
           onChange={key => this.onChange('prepareStatus', key)}
         />
         <div className="action-layout">
-          <div className="next" onClick={() => this.onNext()}>
+          <div className="next" onClick={() => prepareStatus && this.onNext()}>
             下一题
             <Icon type="right-circle" theme="filled" />
           </div>
@@ -176,7 +172,7 @@ export default class extends Component {
             <Icon type="left-circle" theme="filled" />
             上一题
           </div>
-          <div className="next" onClick={() => this.onNext()}>
+          <div className="next" onClick={() => prepareExaminationTime && this.onNext()}>
             下一题
             <Icon type="right-circle" theme="filled" />
           </div>
@@ -201,7 +197,7 @@ export default class extends Component {
             <Icon type="left-circle" theme="filled" />
             上一题
           </div>
-          <div className="next" onClick={() => this.onNext()}>
+          <div className="next" onClick={() => prepareGoal && this.onNext()}>
             下一题
             <Icon type="right-circle" theme="filled" />
           </div>
@@ -211,11 +207,11 @@ export default class extends Component {
   }
 
   renderStep3() {
-    const { data, link, step } = this.state;
+    const { data, info, step } = this.state;
     const { prepareScoreTime } = data;
     return (
       <div className="step-3-layout">
-        {link && <a href={link.url} className="a-l" target='_blank'>{link.title}</a>}
+        {info && <a href={info.link} className="a-l" target='_blank'>{info.title}</a>}
         <Date
           show={step === 3}
           theme="filled"

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

@@ -8,7 +8,7 @@
     ],
     "proxy": [
       {
-        "target": "http://qianxing.nuliji.com",
+        "target": "http://127.0.0.1:8888",
         "from": "/api",
         "to": "/api"
       }
@@ -30,4 +30,4 @@
       "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
     ]
   }
-}
+}

+ 99 - 75
front/project/www/routes/my/data/page.js

@@ -11,8 +11,12 @@ import UserAction from '../../../components/UserAction';
 import Select from '../../../components/Select';
 import menu from '../index';
 import Tabs from '../../../components/Tabs';
-// import { QuestionDifficult } from '../../../../Constant';
+import { My } from '../../../stores/my';
+import { QuestionDifficult } from '../../../../Constant';
+import { getMap, formatPercent, formatSeconds } from '../../../../../src/services/Tools';
 
+const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
+console.log(QuestionDifficultMap);
 const columns = [
   {
     key: 'title',
@@ -24,11 +28,11 @@ const columns = [
   {
     key: 'progress',
     title: '进度',
-    render() {
+    render(text, record) {
       return (
         <div className="v">
-          <div className="t">50%</div>
-          <div className="d">已做20道</div>
+          <div className="t">{formatPercent(record.userQuestion, record.questionNumber, false)}</div>
+          <div className="d">已做{record.userQuestion}道</div>
         </div>
       );
     },
@@ -36,11 +40,11 @@ const columns = [
   {
     key: 'ratio',
     title: '正确率',
-    render() {
+    render(text, record) {
       return (
         <div className="v">
-          <div className="t">100%</div>
-          <div className="d">899/899</div>
+          <div className="t">{formatPercent(record.userCorrect, record.userNumber, false)}</div>
+          <div className="d">{record.userCorrect}/{record.userNumber}</div>
         </div>
       );
     },
@@ -49,6 +53,9 @@ const columns = [
     key: 'time',
     title: '平均用时',
     help: '',
+    render(text, record) {
+      return formatSeconds(record.userTime / record.userNumber);
+    },
   },
 ];
 
@@ -121,7 +128,7 @@ function barOption1(avgTotal, avgCorrect, avgIncorrent) {
           label: {
             show: true,
             position: 'top',
-            formatter: `{a|${avgTotal}}`,
+            formatter: `{a|${formatSeconds(avgTotal)}}`,
             rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
           },
         },
@@ -132,7 +139,7 @@ function barOption1(avgTotal, avgCorrect, avgIncorrent) {
           label: {
             show: true,
             position: 'top',
-            formatter: `{a|${avgCorrect}}`,
+            formatter: `{a|${formatSeconds(avgCorrect)}}`,
             rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
           },
         },
@@ -143,7 +150,7 @@ function barOption1(avgTotal, avgCorrect, avgIncorrent) {
           label: {
             show: true,
             position: 'top',
-            formatter: `{a|${avgIncorrent}}`,
+            formatter: `{a|${formatSeconds(avgIncorrent)}}`,
             rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
           },
         },
@@ -289,39 +296,60 @@ export default class extends Page {
   initState() {
     return {
       filterMap: {},
-      data: [
-        { title: '语法SC', time: '1min20s' },
-        { title: '逻辑CR', time: '1min20s' },
-        { title: '阅读RC', time: '1min20s' },
-      ],
       selectList: [],
-      tab1: '1',
-      tab2: '1',
-      tab3: '1',
+      tab: 'exercise',
+      questionType: '',
+      info: 'base',
     };
   }
 
-  onTabChange(tab) {
-    this.setState({ tab1: tab });
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    if (data.order) {
+      data.sortMap = { [data.order]: data.direction };
+    }
+    data.filterMap = this.state.search;
+    const startTime = null;
+    const endTime = null;
+    switch (data.timerange) {
+      case 'all':
+        break;
+      case 'week':
+        break;
+      case 'month':
+        break;
+      case 'month3':
+        break;
+      case 'today':
+      default:
+    }
+    My.getData(data.tab, data.subject, '', startTime, endTime).then(result => {
+      console.log(result);
+      this.data = result;
+      this.setState({ list: Object.values(result) });
+      this.onQuestionTypeChange();
+    });
   }
 
-  onTabChange1(tab) {
-    this.setState({ tab2: tab });
+  onTabChange(tab) {
+    const data = { tab };
+    this.refreshQuery(data);
   }
 
-  onTabChange2(tab) {
-    this.setState({ tab3: tab });
+  onQuestionTypeChange(questionType) {
+    const data = this.data[questionType];
+    this.setState({ questionType, data });
   }
 
-  onFilter(value) {
-    this.setState({ filterMap: value });
+  onInfoChange(info) {
+    this.setState({ info });
   }
 
-  onDataChange(page) {
-    this.setState({ page, allChecked: false, selectList: [] });
+  onFilter(value) {
+    this.search(value);
   }
 
-  onAction() {}
+  onAction() { }
 
   onSelect(selectList) {
     this.setState({ selectList });
@@ -333,7 +361,7 @@ export default class extends Page {
   }
 
   renderTable() {
-    const { tab1, tab2, tab3, filterMap = {}, data } = this.state;
+    const { tab, questionType, info, questionTypeSelect, filterMap = {}, list = [] } = this.state;
     return (
       <div className="table-layout">
         <Tabs
@@ -343,48 +371,42 @@ export default class extends Page {
           size="small"
           space={2.5}
           width={100}
-          active={tab1}
-          tabs={[{ key: '1', title: '练习' }, { key: '2', title: '模考' }]}
+          active={tab}
+          tabs={[{ key: 'exercise', title: '练习' }, { key: 'examination', title: '模考' }]}
           onChange={key => this.onTabChange(key)}
         />
         <UserAction
-          search
-          selectList={[
-            {
-              label: '123',
-              children: [
-                {
-                  key: 'one',
-                  default: '1',
-                  select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
-                },
-                {
-                  key: 'two',
-                  be: 'one',
-                  placeholder: '全部',
-                  selectMap: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
-                },
-              ],
-            },
-            {
-              label: '123',
-              right: true,
-              select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
-            },
-          ]}
+          selectList={[{
+            children: [
+              {
+                key: 'one',
+                select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+              },
+              {
+                key: 'two',
+                be: 'one',
+                placeholder: '全部',
+                selectMap: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+              },
+            ],
+          }, {
+            right: true,
+            key: 'timerange',
+            select: [{ title: '今天', key: 'today' }, { title: '近一周', key: 'week' }, { title: '近一个月', key: 'month' }, { title: '近三个月', key: 'month3' }, { title: '全部', key: 'all' }],
+          }]}
           filterMap={filterMap}
           onFilter={value => this.onFilter(value)}
         />
         <div className="title">整体情况</div>
-        <UserTable size="small" columns={columns} data={data} />
+        <UserTable size="small" columns={columns} data={list} />
         <div className="title">
           单项分析
           <Select
             size="small"
             theme="default"
-            value={tab2}
-            list={[{ key: '1', title: '全部' }, { key: '2', title: '语法' }]}
-            onChange={({ key }) => this.onTabChange1(key)}
+            value={questionType}
+            list={questionTypeSelect}
+            onChange={({ key }) => this.onQuestionTypeChange(key)}
           />
         </div>
         <Tabs
@@ -393,69 +415,71 @@ export default class extends Page {
           theme="theme"
           size="small"
           width={80}
-          active={tab3}
-          tabs={[{ key: '1', title: '基本情况' }, { key: '2', title: '难度分析' }, { key: '3', title: '考点分析' }]}
-          onChange={key => this.onTabChange2(key)}
+          active={info}
+          tabs={[{ key: 'base', title: '基本情况' }, { key: 'difficult', title: '难度分析' }, { key: 'place', title: '考点分析' }]}
+          onChange={key => this.onInfoChange(key)}
         />
-        {this[`renderTab${tab3}`]()}
+        {this[`renderTab${info}`]()}
       </div>
     );
   }
 
-  renderTab1() {
+  renderTabbase() {
+    const { data = {} } = this.state;
     return (
       <div className="tab-1-layout">
         <div className="block">
           <div className="chart">
-            <PieChart height={110} width={110} option={pieOption1(10, '123', '321')} />
+            <PieChart height={110} width={110} option={pieOption1(formatPercent(data.userQuestion, data.questionNumber), formatPercent(data.userQuestion, data.questionNumber, false), `全站${formatPercent(data.totalCorrect, data.totalNumber, false)}`)} />
           </div>
           <div className="value">
-            <div className="total">共200题</div>
+            <div className="total">共{data.questionNumber}题</div>
             <div className="item">
               <div className="t">已做</div>
               <div className="v">
-                <b>265</b>题
+                <b>{data.userQuestion}</b>题
               </div>
             </div>
             <div className="item">
               <div className="t">剩余</div>
               <div className="v">
-                <b>8</b>题
+                <b>{data.questionNumber - data.userQuestion}</b>题
               </div>
             </div>
             <div className="item">
               <div className="t">正确率</div>
               <div className="v">
-                <b>50%</b>
+                <b>{formatPercent(data.userCorrect, data.userNumber, false)}</b>
               </div>
             </div>
             <div className="item">
               <div className="t">全站</div>
               <div className="v">
-                <b>23%</b>
+                <b>{formatPercent(data.totalCorrect, data.totalNumber, false)}</b>
               </div>
             </div>
           </div>
         </div>
         <div className="block">
-          <BarChart height={300} option={barOption1(10, 20, 30)} />
+          <BarChart height={300} option={barOption1(data.userNumber ? data.userTime / data.userNumber : 0, data.userCorrect ? data.correctTime / data.userCorrect : 0, data.userNumber - data.userCorrect ? data.incorrectTime / (data.userNumber - data.userCorrect) : 0)} />
         </div>
       </div>
     );
   }
 
-  renderTab2() {
+  renderTabdifficult() {
+    const { data = {} } = this.state;
     return (
       <div className="tab-2-layout">
         <BarChart
           height={350}
-          option={barOption2('平均正确率80%', '正确率', [['easy', 10], ['Medium', 30], ['Hard', 40]])}
+          option={barOption2(`平均正确率${formatPercent(data.userCorrect, data.userNumber, false)}`, '正确率', [['easy', 10], ['Medium', 30], ['Hard', 40]])}
         />
       </div>
     );
   }
 
-  renderTab3() {
+  renderTabplace() {
     return (
       <div className="tab-3-layout">
         <BarChart

+ 10 - 7
front/project/www/routes/my/main/page.js

@@ -1,12 +1,13 @@
 import React, { Component } from 'react';
 import { Link } from 'react-router-dom';
 import './index.less';
-import { Tooltip, Icon, Calendar } from 'antd';
+import { Tooltip, Icon } from 'antd';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import { formatPercent, formatDate, formatSeconds, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
 import moment from 'moment';
+import Date from '../../../components/Date';
 import UserLayout from '../../../layouts/User';
 import Tabs from '../../../components/Tabs';
 import Button from '../../../components/Button';
@@ -335,7 +336,7 @@ export default class extends Page {
           return {
             type: QuestionTypeMap[row.extend],
             title: `课时${row.no}: ${row.title}`,
-            time: formatDate(row.createTime, 'YYYY-MM-DD HH:mm:ss'),
+            time: formatDate(row.createTime, 'YYYY-MM-DD\n HH:mm:ss'),
           };
         }),
       });
@@ -414,7 +415,6 @@ export default class extends Page {
             }}
           />
           <div className="right">
-            {day === 'other' && formatDate(time, 'YYYY-MM-DD')}
             <Assets
               className="right"
               name="calendar"
@@ -422,12 +422,15 @@ export default class extends Page {
                 this.setState({ showCal: true });
               }}
             />
+            {day === 'other' && <span className="right">{formatDate(time, 'YYYY-MM-DD')}</span>}
             {showCal && (
-              <Calendar
-                className="cal"
-                fullscreen={false}
+              <Date
+                show
+                hideInput
+                theme="filled"
+                value={moment(time)}
                 disabledDate={date => date.unix() <= moment.unix()}
-                onSelect={date => {
+                onChange={date => {
                   this.refreshDay(date);
                 }}
               />

+ 52 - 17
front/project/www/routes/my/message/page.js

@@ -1,36 +1,68 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import UserLayout from '../../../layouts/User';
 import menu from '../index';
 import UserTable from '../../../components/UserTable';
 import UserAction from '../../../components/UserAction';
 import Tabs from '../../../components/Tabs';
+import { My } from '../../../stores/my';
+import { MessageType } from '../../../../Constant';
+import { getMap, formatDate } from '../../../../../src/services/Tools';
+
+const MessageTypeMap = getMap(MessageType, 'value', 'label');
 
 const columns = [{ title: '消息', key: 'content' }, { title: '类型', key: 'type' }, { title: '发送时间', key: 'date' }];
 
 export default class extends Page {
   initState() {
     return {
-      tab: '1',
+      tab: '',
       filterMap: {},
-      data: [
-        { content: '您的会员即将到期,请及时续费', type: '动态消息', date: '2019-07-12 \n 11:38:51' },
-        {
-          content: '请尽快完成作业 \n 消息详情消息详情消息详情消息详情 ',
-          type: '系统消息',
-          date: '2019-07-12 \n 11:38:51',
-        },
-      ],
     };
   }
 
-  onFilter(filterMap) {
-    this.setState({ filterMap });
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    if (data.order) {
+      data.sortMap = { [data.order]: data.direction };
+    }
+    data.filterMap = this.state.search;
+    data.messageTypeSelect = MessageType.map(row => {
+      row.title = row.label;
+      row.key = row.value;
+      return row;
+    });
+    data.messageTypeSelect.unshift({
+      title: '消息类型',
+      key: '',
+    });
+    this.setState(data);
+    My.message(Object.assign({ read: data.tab === 'unread' ? 0 : null }, this.state.search)).then(result => {
+      result.list = result.list.map(row => {
+        row.type = MessageTypeMap[row.type];
+        row.date = formatDate(row.createTime, 'YYYY-MM-DD\n HH:mm:ss');
+
+        return row;
+      });
+      this.setState(result);
+    });
+  }
+
+  onFilter(value) {
+    this.search(value);
   }
 
   onTabChange(tab) {
-    this.setState({ tab });
+    const data = { tab };
+    this.refreshQuery(data);
+  }
+
+  readAllMessage() {
+    My.readAllMessage().then(() => {
+      asyncSMessage('操作成功');
+    });
   }
 
   renderView() {
@@ -39,7 +71,7 @@ export default class extends Page {
   }
 
   renderTable() {
-    const { tab, data, filterMap } = this.state;
+    const { tab, list, messageTypeSelect, filterMap } = this.state;
     return (
       <div className="table-layout">
         <UserAction
@@ -51,20 +83,23 @@ export default class extends Page {
               space={5}
               width={54}
               active={tab}
-              tabs={[{ key: '1', title: '全部' }, { key: '2', title: '未读' }]}
+              tabs={[{ key: '', title: '全部' }, { key: 'unread', title: '未读' }]}
               onChange={key => this.onTabChange(key)}
             />
           }
-          right={<span>全部已读</span>}
+          right={<span style={{ cursor: 'pointer' }} onClick={() => {
+            this.readAllMessage();
+          }}>全部已读</span>}
           selectList={[
             {
-              select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+              select: messageTypeSelect,
+              key: 'messageType',
             },
           ]}
           filterMap={filterMap}
           onFilter={value => this.onFilter(value)}
         />
-        <UserTable size="small" columns={columns} data={data} />
+        <UserTable size="small" columns={columns} data={list} />
       </div>
     );
   }

+ 377 - 113
front/project/www/routes/my/tools/page.js

@@ -3,6 +3,8 @@ import './index.less';
 import { Icon } from 'antd';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { asyncSMessage } from '@src/services/AsyncTools';
+import { formatDate } from '@src/services/Tools';
 import UserLayout from '../../../layouts/User';
 import UserAction from '../../../components/UserAction';
 import menu from '../index';
@@ -13,39 +15,254 @@ import Switch from '../../../components/Switch';
 import TotalSort from '../../../components/TotalSort';
 import Modal from '../../../components/Modal';
 import UserTable from '../../../components/UserTable';
+import { My } from '../../../stores/my';
+import { User } from '../../../stores/user';
+import { Order } from '../../../stores/order';
+import { Textbook } from '../../../stores/textbook';
+import { DataType } from '../../../../Constant';
+import { Main } from '../../../stores/main';
+import { Question } from '../../../stores/question';
 
-const updateColumns = [
-  { title: '更新时间', key: '1', width: 120 },
-  { title: '位置', key: '2', width: 120 },
-  { title: '原内容', key: '3', width: 120 },
-  { title: '更改为', key: '4', width: 120 },
-  { title: '更新至', key: '5', width: 90 },
+const dataHistoryColumns = [
+  { title: '更新时间', key: 'time', width: 120 },
+  { title: '位置', key: 'position', width: 120 },
+  { title: '原内容', key: 'originContent', width: 120 },
+  { title: '更改为', key: 'content', width: 120 },
+  { title: '更新至', key: 'version', width: 90 },
+];
+
+const textbookHistoryColumns = [
+  { title: '更新时间', key: 'createTime', width: 120 },
+  { title: '版本', key: 'version', width: 90 },
+  { title: '内容', key: 'content', width: 360 },
+];
+
+const openColumns = [
+  { title: '商品名称', key: 'title', width: 240 },
+  { title: '开通期限', key: 'endTime', width: 240 },
+  { title: '操作', key: 'handler', width: 90 },
 ];
 
 export default class extends Page {
   initState() {
     return {
-      tab: '1',
+      tab: 'data',
       sortMap: {},
       filterMap: {},
-      data: [
-        { num: '30', version: '7', title: 'OG16/17/18/19语法千行', date: '2019-08-31  09:26:13' },
-        { num: '30', version: '7', title: 'OG16/17/18/19语法千行', date: '2019-08-31  09:26:13' },
-        { num: '30', version: '7', title: 'OG16/17/18/19语法千行', date: '2019-08-31  09:26:13' },
-      ],
     };
   }
 
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    if (data.order) {
+      data.sortMap = { [data.order]: data.direction };
+    }
+    data.filterMap = this.state.search;
+    this.setState(data);
+
+    const { tab } = this.state;
+    switch (tab) {
+      case 'textbook':
+        this.refreshTextbook();
+        break;
+      case 'examination':
+        this.refreshExamination();
+        break;
+      case 'vip':
+        this.refreshVip();
+        break;
+      case 'cal':
+        break;
+      case 'data':
+      default:
+        this.refreshData();
+        break;
+    }
+  }
+
+  refreshTextbook() {
+    Main.getService('textbook').then(result => {
+      this.setState({ service: result });
+    });
+    Textbook.getInfo()
+      .then(result => {
+        const { latest } = result;
+        result.day = parseInt((new Date().getTime() - new Date(result.latest.startDate).getTime()) / 86400000, 10);
+
+        result.expireDay = result.expireTime && parseInt((new Date().getTime() - new Date(result.expireTime).getTime()) / 86400000, 10);
+
+        const list = [];
+
+        list.push({ subject: 'quant', number: latest.quantNumber, time: latest.quantTime, version: latest.quantVersion });
+        list.push({ subject: 'rc', number: latest.rcNumber, time: latest.rcTime, version: latest.rcVersion });
+        list.push({ subject: 'ir', number: latest.irNumber, time: latest.irTime, version: latest.irVersion });
+        this.setState({ data: result, list });
+      });
+  }
+
+  textbookHistory({ page, size, subject }) {
+    Textbook.listHistory({ subject })
+      .then(result => {
+        this.setState({
+          updateList: result.map(row => {
+            row.version = row[`${subject}Version`];
+            row.content = row[`${subject}Content`];
+            row.createTime = formatDate(row.createTime, 'YYYY-MM-DD\nHH:mm:ss');
+            return row;
+          }),
+          updateTotal: result.length,
+          updatePage: page,
+          updateData: { page, size, subject, columns: textbookHistoryColumns, type: 'textbook' },
+        });
+      });
+  }
+
+  refreshExamination() {
+    Main.getService('qx_cat').then(result => {
+      this.setState({ service: result });
+    });
+    Question.getExaminationInfo(result => {
+      result.expireDay = result.expireTime && parseInt((new Date().getTime() - new Date(result.expireTime).getTime()) / 86400000, 10);
+      this.setState({ data: result });
+    });
+  }
+
+  refreshVip() {
+    Main.getService('vip').then(result => {
+      this.setState({ service: result });
+    });
+  }
+
+  recordList({ page, size, service, isUse }) {
+    Order.listRecord({ page, size, productType: 'service', service, isUse })
+      .then(result => {
+        this.setState({
+          updateList: result.map(row => {
+            row.handler = <a onClick={() => {
+              this.open(row.id);
+            }}>立即开通</a>;
+            row.endTime = `${formatDate(row.endTime, 'YYYY-MM-DD')} 前`;
+            return row;
+          }),
+          updateTotal: result.length,
+          updatePage: page,
+          updateData: { page, size, service, isUse, columns: isUse ? [] : openColumns, type: 'record' },
+        });
+      });
+  }
+
+  refreshData() {
+    const dataTypeSelect = DataType.map(row => {
+      row.title = row.label;
+      row.key = row.value;
+      return row;
+    });
+    dataTypeSelect.unshift({
+      title: '全部',
+      key: '',
+    });
+    this.setState({ dataTypeSelect });
+    Main.dataStruct().then(result => {
+      const structs = result.filter(row => row.level === 1).map(row => {
+        row.title = `${row.titleZh}${row.titleEn}`;
+        row.key = `${row.id}`;
+        return row;
+      });
+      structs.unshift({
+        title: '全部',
+        key: '',
+      });
+      this.setState({
+        structs,
+      });
+    });
+    My.listData(Object.assign({}, this.state.search))
+      .then(result => {
+        result = {
+          list: [{
+            title: '123123',
+            latestTime: '',
+            id: 1,
+            number: 10,
+            version: '1231',
+          }],
+        };
+        this.setState({
+          list: result.list.map(row => {
+            row.time = formatDate(row.time, 'YYYY-MM-DD HH:mm:ss');
+            return row;
+          }),
+          total: result.total,
+        });
+      });
+  }
+
+  dataHistory({ dataId, page, size }) {
+    My.listDataHistory({ page, size, dataId })
+      .then(result => {
+        result.list = result.list.map(row => {
+          row.time = formatDate(row.time, 'YYYY-MM-DD\nHH:mm:ss');
+          return row;
+        });
+        this.setState({ updateList: result.list, updateTotal: result.total, updatePage: page, updateData: { page, size, dataId, columns: dataHistoryColumns, type: 'data' } });
+      });
+  }
+
   onFilter(value) {
-    this.setState({ filterMap: value });
+    this.search(value);
   }
 
   onSort(value) {
-    this.setState({ sortMap: value });
+    const keys = Object.keys(value);
+    this.search({ order: keys.length ? keys[0] : null, direction: keys.length ? value[keys[0]] : null });
   }
 
   onTabChange(tab) {
-    this.setState({ tab });
+    const data = { tab };
+    this.refreshQuery(data);
+  }
+
+  submitComment() {
+    const { comment } = this.state;
+    My.addComment(comment.channel, comment.position, comment.content)
+      .then(() => {
+        this.setState({ showComment: false, showFinish: true, comment: {} });
+      });
+  }
+
+  submitFeedbackError() {
+    const { feedbackError } = this.state;
+    My.addFeedbackErrorData(feedbackError.dataId, feedbackError.title, feedbackError.position, feedbackError.originContent, feedbackError.content)
+      .then(() => {
+        this.setState({ showFinish: true, showFeedbackError: false, feedbackError: {} });
+      });
+  }
+
+  submitFeedback() {
+    const { feedback } = this.state;
+    My.addTextbookFeedback('', feedback.target, feedback.content)
+      .then(() => {
+        this.setState({ showFinish: true, showFeedback: false, feedbackError: {} });
+      });
+  }
+
+  subscribe(value) {
+    My.subscribeData(value)
+      .then(() => {
+        const { info } = this.props.user;
+        info.dataEmailSubscribe = value;
+        User.infoHandle(info);
+      })
+      .catch(err => {
+        asyncSMessage(err.message, 'warn');
+      });
+  }
+
+  open(recordId) {
+    Order.useRecord(recordId).then(() => {
+      asyncSMessage('开通成功');
+      this.refresh();
+    });
   }
 
   renderView() {
@@ -54,7 +271,7 @@ export default class extends Page {
   }
 
   renderDetail() {
-    const { tab } = this.state;
+    const { tab, comment = {}, showComment, showFinish, showUpdate, updateList, updateTotal, updateData = {} } = this.state;
     return (
       <div className="table-layout">
         <Tabs
@@ -66,49 +283,42 @@ export default class extends Page {
           width={100}
           active={tab}
           tabs={[
-            { key: '1', title: '资料' },
-            { key: '2', title: '机经' },
-            { key: '3', title: '模考' },
-            { key: '4', title: 'VIP' },
-            { key: '5', title: '考分计算器' },
+            { key: 'data', title: '资料' },
+            { key: 'textbook', title: '机经' },
+            { key: 'examination', title: '模考' },
+            { key: 'vip', title: 'VIP' },
+            { key: 'cal', title: '考分计算器' },
           ]}
           onChange={key => this.onTabChange(key)}
         />
         {this[`renderTab${tab}`]()}
-        <Modal maskClosable close={false} body={false} width={630} onClose={() => {}}>
+        <Modal show={showUpdate} maskClosable close={false} body={false} width={630} onClose={() => this.setState({ showUpdate: false, updateList: [] })} >
           <UserTable
             size="small"
-            columns={updateColumns}
-            data={[
-              {
-                1: '2019-07-12 11:38:51',
-                2: 'P30 第 20 行',
-                3: 'the number of wolf population',
-                4: '删除',
-                5: '版本3',
-              },
-              {
-                1: '2019-07-12 11:38:51',
-                2: 'P30 第 20 行',
-                3: 'the number of wolf population',
-                4: '删除',
-                5: '版本3',
-              },
-              {
-                1: '2019-07-12 11:38:51',
-                2: 'P30 第 20 行',
-                3: 'the number of wolf population',
-                4: '删除',
-                5: '版本3',
-              },
-            ]}
+            columns={updateData.columns}
+            data={updateList}
+            current={updateData.page}
+            onChangePage={(page) => {
+              updateData.page = page;
+              if (updateData.type === 'data') {
+                this.dataHistory(updateData);
+              } else if (updateData.type === 'textbook') {
+                this.textbookHistory(updateData);
+              } else if (updateData.type === 'record') {
+                this.recordList(updateData);
+              }
+            }}
+            total={updateTotal}
           />
         </Modal>
-        <Modal title="评价" onConfirm={() => {}} onCancel={() => {}}>
-          <textarea className="b-c-1 w-10 p-10" rows={6} placeholder="您的看法对我们来说很重要!" />
+        <Modal show={showComment} title="评价" onConfirm={() => comment.content && this.submitComment()} onCancel={() => this.setState({ showComment: false, comment: {} })}>
+          <textarea value={comment.content} className="b-c-1 w-10 p-10" rows={6} placeholder="您的看法对我们来说很重要!" onChange={(e) => {
+            comment.content = e.target.value;
+            this.setState({ comment });
+          }} />
           <div className="b-b m-t-2" />
         </Modal>
-        <Modal title="提交成功" confirmText="好的,知道了" btnAlign="center" onConfirm={() => {}}>
+        <Modal show={showFinish} title="提交成功" confirmText="好的,知道了" btnAlign="center" onConfirm={() => this.setState({ showFinish: false })} >
           <div className="t-2 t-s-18">
             <Icon type="check" className="t-5 m-r-5" />
             您的每一次反馈都是千行进步的动力。
@@ -118,53 +328,71 @@ export default class extends Page {
     );
   }
 
-  renderTab1() {
-    const { data = [], filterMap = {}, sortMap = {} } = this.state;
+  renderTabdata() {
+    const { list = [], filterMap = {}, sortMap = {}, structs, dataTypeSelect } = this.state;
+    const { info } = this.props.user;
     return (
       <div className="tab-1-layout">
         <UserAction
-          selectList={[
-            {
-              label: '学科',
-              key: '1',
-              select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
-            },
-            {
-              label: '资料形式',
-              key: '2',
-              select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
-            },
-          ]}
-          sortList={[{ right: true, label: '销量', key: '1' }, { right: true, label: '更新时间', key: '2' }]}
+          selectList={[{
+            label: '学科',
+            key: 'structId',
+            select: structs,
+          },
+          {
+            label: '资料形式',
+            key: 'dataType',
+            select: dataTypeSelect,
+          }]}
+          sortList={[{ right: true, label: '销量', key: 'sale_number' }, { right: true, label: '更新时间', key: 'latest_time' }]}
           sortMap={sortMap}
           filterMap={filterMap}
           onFilter={value => this.onFilter(value)}
           onSort={value => this.onSort(value)}
           right={
             <div className="email">
-              邮箱订阅 <Switch />
+              邮箱订阅 <Switch checked={info.dataEmailSubscribe} onChange={() => {
+              this.subscribe(!info.dataEmailSubscribe);
+            }} />
             </div>
           }
         />
         <div className="data-layout">
-          {data.map(item => {
+          {list.map(item => {
             return (
               <div className="data-item">
-                <Assets name="sun_blue" />
+                <Assets name="sun_blue" src={item.cover} />
                 <div className="fixed">
                   <div className="btns">
-                    <Button size="small" radius>
+                    <Button size="small" radius onClick={() => {
+                      openLink(item.resource);
+                    }}>
                       阅读
                     </Button>
-                    <div className="white">下载</div>
+                    <div className="white" onClick={() => {
+                      openLink(item.resource);
+                    }}>下载</div>
                   </div>
                 </div>
                 <div className="title">
                   <span>版本{item.version}</span>
                   {item.title}
                 </div>
-                <div className="date">{item.date}</div>
-                <More menu={[{ label: '纠错', key: '1' }, { label: '评价', key: '2' }, { label: '更新', key: '3' }]} />
+                <div className="date">{formatDate(item.latestTime, 'YYYY-MM-DD HH:mm:ss')}</div>
+                <More
+                  menu={[{ label: '纠错', key: 'feedback' }, { label: '评价', key: 'comment' }, { label: '更新', key: 'update' }]}
+                  onClick={(value) => {
+                    const { key } = value;
+                    if (key === 'comment') {
+                      this.setState({ showComment: true, comment: { channel: 'course_data', position: item.id } });
+                    } else if (key === 'update') {
+                      this.setState({ showUpdate: true });
+                      this.dataHistory({ dataId: item.id, page: 1, size: 10 });
+                    } else if (key === 'feedback') {
+                      this.setState({ showFeedbackError: true, feedbackError: { dataId: item.id, title: item.title } });
+                    }
+                  }}
+                />
               </div>
             );
           })}
@@ -173,23 +401,27 @@ export default class extends Page {
     );
   }
 
-  renderTab2() {
-    const { data = [] } = this.state;
+  renderTabtextbook() {
+    const { data = {}, list = [], service } = this.state;
+    const { latest = {}, day } = data;
     return (
       <div className="tab-2-layout">
         <UserAction
           left={
             <div className="total-log">
               <span>最新换库</span>
-              <span>2019-07-22</span>
+              <span>{latest.startDate ? formatDate(latest.startDate, 'YYYY-MM-DD') : ''}</span>
               <span>
-                已换库<b>10</b>天
+                已换库<b>{day}</b>天
               </span>
             </div>
           }
+          right={!data.hasService && data.unUseRecord && <div className="email" onClick={() => {
+            this.recordList({ page: 1, size: 10, service: 'textbook', isUse: false });
+          }}>待开通</div>}
         />
-        <div className="data-layout">
-          {data.map(item => {
+        {data.hasService && <div className="data-layout">
+          {list.map(item => {
             return (
               <div className="data-item">
                 <Assets name="sun_blue" />
@@ -197,92 +429,124 @@ export default class extends Page {
                   已更新至<b>{item.num}</b>题
                 </div>
                 <div className="date">{item.date}</div>
-                <More menu={[{ label: '更新', key: '1' }, { label: '反馈', key: '2' }, { label: '评价', key: '3' }]} />
+                <More
+                  menu={[{ label: '更新', key: 'update' }, { label: '反馈', key: 'feedback' }, { label: '评价', key: 'comment' }]}
+                  onClick={(value) => {
+                    const { key } = value;
+                    if (key === 'comment') {
+                      this.setState({ showComment: true, comment: { channel: 'library' } });
+                    } else if (key === 'update') {
+                      this.setState({ showUpdate: true });
+                      this.textbookHistory({ page: 1, size: 100, subject: item.subject });
+                    } else if (key === 'feedback') {
+                      this.setState({ showFeedback: true });
+                    }
+                  }}
+                />
               </div>
             );
           })}
-        </div>
-        <div className="tip-layout">
+        </div>}
+        {!data.hasService && !data.unUseRecord && <div className="tip-layout">
           <div className="t1">还未购买本月机经</div>
-          <div className="desc">¥ 888 / 月</div>
+          <div className="desc">¥ {service && service.package && service.package[0].price}</div>
           <Button radius size="lager" width={150}>
             立即购买
           </Button>
-        </div>
-        <div className="tip-layout">
-          <div className="t2">请于2019-11-20前开通</div>
-          <Button radius size="lager" width={150}>
+        </div>}
+        {data.hasService && <div className="tip-layout">
+          <div className="t1">使用中</div>
+          <div className="t2">距离到期还有 {data.expireDay} 天</div>
+        </div>}
+        {!data.hasService && data.unUseRecord && <div className="tip-layout">
+          <div className="t2">请于{formatDate(data.unUseRecord.endTime, 'YYYY-MM-DD')}前开通</div>
+          <Button radius size="lager" width={150} onClick={() => {
+            this.open(data.unUseRecord.id);
+          }}>
             立即开通
           </Button>
-        </div>
+        </div>}
       </div>
     );
   }
 
-  renderTab3() {
+  renderTabexamination() {
+    const { data = {}, service } = this.state;
     return (
       <div className="tab-3-layout">
-        <UserAction right={<div className="email">待开通</div>} />
-        <div className="tip-layout">
+        <UserAction right={data.unUseRecord && <div className="email" onClick={() => {
+          this.recordList({ page: 1, size: 10, service: 'qx_cat', isUse: false });
+        }}>待开通</div>} />
+        {!data.hasService && !data.unUseRecord && !data.expireTime && <div className="tip-layout">
           <div className="t1">未购买</div>
-          <div className="desc">¥ 888 / 月</div>
+          <div className="desc">¥ {service && service.package && service.package[0].price}</div>
           <Button radius size="lager" width={150}>
             立即购买
           </Button>
-        </div>
-        <div className="tip-layout">
-          <div className="t2">请于2019-11-20前开通</div>
-          <Button radius size="lager" width={150}>
+        </div>}
+        {!data.hasService && data.unUseRecord && <div className="tip-layout">
+          <div className="t2">请于{formatDate(data.unUseRecord.endTime, 'YYYY-MM-DD')}前开通</div>
+          <Button radius size="lager" width={150} onClick={() => {
+            this.open(data.unUseRecord.id);
+          }}>
             立即开通
           </Button>
-        </div>
-        <div className="tip-layout">
+        </div>}
+        {data.hasService && <div className="tip-layout">
           <div className="t1">使用中</div>
-          <div className="t2">距离到期还有 10 天</div>
-        </div>
-        <div className="tip-layout">
+          <div className="t2">距离到期还有 {data.expireDay} 天</div>
+        </div>}
+        {!data.hasService && !data.unUseRecord && data.expireTime && <div className="tip-layout">
           <div className="t3">已过期</div>
-          <div className="date">2019-05-11 ~ 2019-09-11</div>
-          <div className="desc">¥ 800/3个月</div>
+          <div className="date">{formatDate(data.startTime, 'YYYY-MM-DD')} ~ {formatDate(data.expireTime, 'YYYY-MM-DD')}</div>
+          <div className="desc">¥ {service && service.package && service.package[0].price}</div>
           <Button radius size="lager" width={150}>
             立即购买
           </Button>
-        </div>
+        </div>}
       </div>
     );
   }
 
-  renderTab4() {
+  renderTabvip() {
+    const { data } = this.state;
     return (
       <div className="tab-4-layout">
-        <div className="tip-layout">
+        {!data.hasService && !data.unUseRecord && !data.expireTime && <div className="tip-layout">
           <div className="t2">未购买</div>
           <Button radius size="lager" width={150}>
             立即购买
           </Button>
-        </div>
-        <div className="tip-layout">
+        </div>}
+        {data.hasService && <div className="tip-layout">
           <div className="t1">使用中</div>
-          <div className="desc">2019-05-20 到期</div>
+          <div className="desc">{formatDate(data.expireTime, 'YYYY-MM-DD')} 到期</div>
           <Button radius size="lager" width={150}>
             续费
           </Button>
-        </div>
-        <div className="tip-layout">
+        </div>}
+        {!data.hasService && !data.unUseRecord && data.expireTime && <div className="tip-layout">
           <div className="t1">已过期</div>
-          <div className="desc">2019-05-11 ~ 2019-09-11</div>
+          <div className="desc">{formatDate(data.startTime, 'YYYY-MM-DD')} ~ {formatDate(data.expireTime, 'YYYY-MM-DD')}</div>
           <Button radius size="lager" width={150}>
             立即购买
           </Button>
-        </div>
+        </div>}
       </div>
     );
   }
 
-  renderTab5() {
+  renderTabcal() {
+    const { data = {} } = this.state;
     return (
       <div className="tab-5-layout">
-        <TotalSort />
+        <TotalSort
+          value={data.value || 650}
+          onChange={(value) => {
+            data.value = value;
+            this.setState({ data });
+          }}
+        />
       </div>
     );
   }

+ 28 - 4
front/project/www/stores/my.js

@@ -38,11 +38,11 @@ export default class MyStore extends BaseStore {
    * 用户站内信
    * @param {*} page
    * @param {*} size
-   * @param {*} type
+   * @param {*} messageType
    * @param {*} read
    */
-  message({ page, size, type, read }) {
-    return this.apiGet('/my/message', { page, size, type, read });
+  message({ page, size, messageType, read }) {
+    return this.apiGet('/my/message', { page, size, messageType, read });
   }
 
   /**
@@ -343,7 +343,7 @@ export default class MyStore extends BaseStore {
    * @param {*} originContent
    * @param {*} content
    */
-  addErrorData(dataId, title, position, originContent, content) {
+  addFeedbackErrorData(dataId, title, position, originContent, content) {
     return this.apiPost('/my/feedback/error/question', { dataId, title, position, originContent, content });
   }
 
@@ -378,6 +378,30 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 资料全局订阅开关
+   * @param {*} subscribe
+   */
+  subscribeData(subscribe) {
+    return this.apiPost('/my/data/subscribe', { subscribe });
+  }
+
+  /**
+   * 获取资料更新列表
+   * @param {*}} param0
+   */
+  listDataHistory({ page, size, dataId }) {
+    return this.apiGet('/my/data/history', { page, size, dataId });
+  }
+
+  /**
+   * 获取购买资料列表
+   * @param {*} param0
+   */
+  listData({ page, size, structId, dataType, order, direction }) {
+    return this.apiGet('/my/data/list', { page, size, structId, dataType, order, direction });
+  }
+
+  /**
    * 购买的课程列表
    * @param {*} param0
    */

+ 3 - 1
server/data/src/main/java/com/qxgmat/data/constants/enums/QuestionSubject.java

@@ -7,7 +7,9 @@ public enum QuestionSubject {
     VERBAL("verbal", "语文"),
     QUANT("quant", "数学"),
     IR("ir", "综合推理"),
-    AWA("awa", "作文");
+    AWA("awa", "作文"),
+
+    ;
 
     final static public String message = "考试结构:学科";
 

+ 2 - 1
server/data/src/main/java/com/qxgmat/data/constants/enums/SettingKey.java

@@ -14,7 +14,8 @@ public enum SettingKey {
     EXERCISE_PAPER_AUTO("exercise_paper_auto"), // 自动组卷设置
     EXAMINATION_TIME("examination_time"), // 考试时间
     FILTER_TIME("filter_time"), // 题目剔除时间
-    PREPARE_INFO("prepare_info"), // 备考统计信息
+    PREPARE_INFO("prepare_info"), // 备考信息
+    PREPARE_STAT("prepare_stat"), // 备考统计信息
 
     EXERCISE_PAPER_STATUS("exercise_paper_status"), // 练习自动组卷状态:process进度,finish时间
     SENTENCE_PAPER_STATUS("sentence_paper_status"), // 长难句自动组卷状态:process进度,finish时间

+ 26 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/TextbookSubject.java

@@ -0,0 +1,26 @@
+package com.qxgmat.data.constants.enums;
+
+/**
+ * Created by gaojie on 2017/11/19.
+ */
+public enum TextbookSubject {
+    QUANT("quant", "数学"),
+    IR("ir", "综合推理"),
+    RC("rc", "阅读"),
+
+    ;
+
+    final static public String message = "机经学科:学科";
+
+    public String key;
+    public String title;
+    private TextbookSubject(String key, String title){
+        this.key = key;
+        this.title = title;
+    }
+
+    public static TextbookSubject ValueOf(String name){
+        if (name == null) return null;
+        return TextbookSubject.valueOf(name.toUpperCase());
+    }
+}

+ 26 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/CourseData.java

@@ -129,6 +129,9 @@ public class CourseData implements Serializable {
     @Column(name = "`latest_time`")
     private Date latestTime;
 
+    @Column(name = "`version`")
+    private String version;
+
     /**
      * 摘要介绍
      */
@@ -536,6 +539,20 @@ public class CourseData implements Serializable {
     }
 
     /**
+     * @return version
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * @param version
+     */
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    /**
      * 获取摘要介绍
      *
      * @return description - 摘要介绍
@@ -635,6 +652,7 @@ public class CourseData implements Serializable {
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
         sb.append(", latestTime=").append(latestTime);
+        sb.append(", version=").append(version);
         sb.append(", description=").append(description);
         sb.append(", content=").append(content);
         sb.append(", authorContent=").append(authorContent);
@@ -867,6 +885,14 @@ public class CourseData implements Serializable {
         }
 
         /**
+         * @param version
+         */
+        public Builder version(String version) {
+            obj.setVersion(version);
+            return this;
+        }
+
+        /**
          * 设置摘要介绍
          *
          * @param description 摘要介绍

+ 105 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/TextbookLibrary.java

@@ -42,6 +42,12 @@ public class TextbookLibrary implements Serializable {
     private Date quantTime;
 
     /**
+     * 数学题目数
+     */
+    @Column(name = "`quant_number`")
+    private Integer quantNumber;
+
+    /**
      * 综合逻辑
      */
     @Column(name = "`ir`")
@@ -60,6 +66,12 @@ public class TextbookLibrary implements Serializable {
     private Date irTime;
 
     /**
+     * 综合逻辑题目数
+     */
+    @Column(name = "`ir_number`")
+    private Integer irNumber;
+
+    /**
      * 阅读
      */
     @Column(name = "`rc`")
@@ -78,6 +90,12 @@ public class TextbookLibrary implements Serializable {
     private Date rcTime;
 
     /**
+     * 阅读题目数
+     */
+    @Column(name = "`rc_number`")
+    private Integer rcNumber;
+
+    /**
      * 更新次数
      */
     @Column(name = "`history_number`")
@@ -202,6 +220,24 @@ public class TextbookLibrary implements Serializable {
     }
 
     /**
+     * 获取数学题目数
+     *
+     * @return quant_number - 数学题目数
+     */
+    public Integer getQuantNumber() {
+        return quantNumber;
+    }
+
+    /**
+     * 设置数学题目数
+     *
+     * @param quantNumber 数学题目数
+     */
+    public void setQuantNumber(Integer quantNumber) {
+        this.quantNumber = quantNumber;
+    }
+
+    /**
      * 获取综合逻辑
      *
      * @return ir - 综合逻辑
@@ -256,6 +292,24 @@ public class TextbookLibrary implements Serializable {
     }
 
     /**
+     * 获取综合逻辑题目数
+     *
+     * @return ir_number - 综合逻辑题目数
+     */
+    public Integer getIrNumber() {
+        return irNumber;
+    }
+
+    /**
+     * 设置综合逻辑题目数
+     *
+     * @param irNumber 综合逻辑题目数
+     */
+    public void setIrNumber(Integer irNumber) {
+        this.irNumber = irNumber;
+    }
+
+    /**
      * 获取阅读
      *
      * @return rc - 阅读
@@ -310,6 +364,24 @@ public class TextbookLibrary implements Serializable {
     }
 
     /**
+     * 获取阅读题目数
+     *
+     * @return rc_number - 阅读题目数
+     */
+    public Integer getRcNumber() {
+        return rcNumber;
+    }
+
+    /**
+     * 设置阅读题目数
+     *
+     * @param rcNumber 阅读题目数
+     */
+    public void setRcNumber(Integer rcNumber) {
+        this.rcNumber = rcNumber;
+    }
+
+    /**
      * 获取更新次数
      *
      * @return history_number - 更新次数
@@ -385,12 +457,15 @@ public class TextbookLibrary implements Serializable {
         sb.append(", quant=").append(quant);
         sb.append(", quantVersion=").append(quantVersion);
         sb.append(", quantTime=").append(quantTime);
+        sb.append(", quantNumber=").append(quantNumber);
         sb.append(", ir=").append(ir);
         sb.append(", irVersion=").append(irVersion);
         sb.append(", irTime=").append(irTime);
+        sb.append(", irNumber=").append(irNumber);
         sb.append(", rc=").append(rc);
         sb.append(", rcVersion=").append(rcVersion);
         sb.append(", rcTime=").append(rcTime);
+        sb.append(", rcNumber=").append(rcNumber);
         sb.append(", historyNumber=").append(historyNumber);
         sb.append(", questionStatus=").append(questionStatus);
         sb.append(", createTime=").append(createTime);
@@ -449,6 +524,16 @@ public class TextbookLibrary implements Serializable {
         }
 
         /**
+         * 设置数学题目数
+         *
+         * @param quantNumber 数学题目数
+         */
+        public Builder quantNumber(Integer quantNumber) {
+            obj.setQuantNumber(quantNumber);
+            return this;
+        }
+
+        /**
          * 设置数学时间
          *
          * @param quantTime 数学时间
@@ -479,6 +564,16 @@ public class TextbookLibrary implements Serializable {
         }
 
         /**
+         * 设置综合逻辑题目数
+         *
+         * @param irNumber 综合逻辑题目数
+         */
+        public Builder irNumber(Integer irNumber) {
+            obj.setIrNumber(irNumber);
+            return this;
+        }
+
+        /**
          * 设置综合逻辑时间
          *
          * @param irTime 综合逻辑时间
@@ -509,6 +604,16 @@ public class TextbookLibrary implements Serializable {
         }
 
         /**
+         * 设置阅读题目数
+         *
+         * @param rcNumber 阅读题目数
+         */
+        public Builder rcNumber(Integer rcNumber) {
+            obj.setRcNumber(rcNumber);
+            return this;
+        }
+
+        /**
          * 设置阅读时间
          *
          * @param rcTime 阅读时间

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

@@ -27,6 +27,7 @@
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
     <result column="latest_time" jdbcType="TIMESTAMP" property="latestTime" />
+    <result column="version" jdbcType="VARCHAR" property="version" />
   </resultMap>
   <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.qxgmat.data.dao.entity.CourseData">
     <!--
@@ -44,7 +45,7 @@
     `id`, `title`, `comment`, `struct_id`, `parent_struct_id`, `is_sentence`, `data_type`, 
     `is_novice`, `is_original`, `price`, `pages`, `link`, `cover`, `resource`, `trail_resource`, 
     `trail_start`, `trail_end`, `view_number`, `sale_number`, `create_time`, `update_time`, 
-    `latest_time`
+    `latest_time`, `version`
   </sql>
   <sql id="Blob_Column_List">
     <!--

+ 6 - 3
server/data/src/main/java/com/qxgmat/data/dao/mapping/TextbookLibraryMapper.xml

@@ -11,12 +11,15 @@
     <result column="quant" jdbcType="VARCHAR" property="quant" />
     <result column="quant_version" jdbcType="INTEGER" property="quantVersion" />
     <result column="quant_time" jdbcType="TIMESTAMP" property="quantTime" />
+    <result column="quant_number" jdbcType="INTEGER" property="quantNumber" />
     <result column="ir" jdbcType="VARCHAR" property="ir" />
     <result column="ir_version" jdbcType="INTEGER" property="irVersion" />
     <result column="ir_time" jdbcType="TIMESTAMP" property="irTime" />
+    <result column="ir_number" jdbcType="INTEGER" property="irNumber" />
     <result column="rc" jdbcType="VARCHAR" property="rc" />
     <result column="rc_version" jdbcType="INTEGER" property="rcVersion" />
     <result column="rc_time" jdbcType="TIMESTAMP" property="rcTime" />
+    <result column="rc_number" jdbcType="INTEGER" property="rcNumber" />
     <result column="history_number" jdbcType="INTEGER" property="historyNumber" />
     <result column="question_status" jdbcType="INTEGER" property="questionStatus" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
@@ -26,8 +29,8 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `start_date`, `end_date`, `quant`, `quant_version`, `quant_time`, `ir`, `ir_version`, 
-    `ir_time`, `rc`, `rc_version`, `rc_time`, `history_number`, `question_status`, `create_time`, 
-    `update_time`
+    `id`, `start_date`, `end_date`, `quant`, `quant_version`, `quant_time`, `quant_number`, 
+    `ir`, `ir_version`, `ir_time`, `ir_number`, `rc`, `rc_version`, `rc_time`, `rc_number`, 
+    `history_number`, `question_status`, `create_time`, `update_time`
   </sql>
 </mapper>

+ 3 - 3
server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataHistoryRelationMapper.xml

@@ -23,13 +23,13 @@
     <include refid="Id_Column_List" />
     from `course_data_history` cdh
     <if test="userId != null">
-    left join `user_order_record` uo on `uo`.`product_type`='data' and `uo`.`product_id` = cdh.`data_id`
-      and `uo`.userId = #{userId,jdbcType=VARCHAR}
+    left join `user_order_record` uor on `uor`.`product_type`='data' and `uor`.`product_id` = cdh.`data_id`
+      and `uor`.user_id = #{userId,jdbcType=VARCHAR}
     </if>
     <!--and `uo`.create_time &lt; cd.create_time-->
     where 1
     <if test="userId != null">
-    `uo`.`id` > 0
+    `uor`.`id` > 0
     </if>
     <if test="dataId != null">
       and `cdh`.dataId = #{dataId,jdbcType=VARCHAR}

+ 3 - 3
server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataRelationMapper.xml

@@ -32,12 +32,12 @@
     select
     <include refid="Id_Column_List" />
     from `course_data` cd
-    left join `user_order_record` uo on `uo`.`product_type`='data' and `uo`.`product_id` = cd.`id`
+    left join `user_order_record` uor on `uor`.`product_type`='data' and `uor`.`product_id` = cd.`id`
     <if test="userId != null">
-      and `uo`.userId = #{userId,jdbcType=VARCHAR}
+      and `uor`.user_id = #{userId,jdbcType=VARCHAR}
     </if>
     where
-    `uo`.`id` > 0
+    `uor`.`id` > 0
     <if test="structId != null">
       and (cd.`struct_id` = #{structId,jdbcType=VARCHAR} or cd.`parent_struct_id` = #{structId,jdbcType=VARCHAR})
     </if>

+ 7 - 2
server/data/src/main/resources/db/migration/V1__init_table.sql

@@ -115,6 +115,7 @@ CREATE TABLE course_data (
   create_time datetime DEFAULT NULL,
   update_time datetime DEFAULT NULL,
   latest_time datetime DEFAULT NULL,
+  version varchar(20) DEFAULT NULL,
   PRIMARY KEY (id),
   KEY data_type (data_type,struct_id),
   KEY struct_id (struct_id)
@@ -702,13 +703,13 @@ INSERT INTO setting (id, `key`, value)
 VALUES
 	(1,'index','{}'),
 	(2,'sentence','{}'),
-	(3,'message_template','{}'),
+	(3,'prepare_info','{}'),
 	(4,'place','{}'),
 	(5,'exercise_time','{}'),
 	(6,'exercise_paper_auto','{}'),
 	(7,'examination_time','{\"verbal\":{\"number\":\"36\",\"time\":\"3900\"},\"ir\":{\"number\":\"12\",\"time\":\"1800\"},\"awa\":{\"number\":\"1\",\"time\":\"1800\"},\"quant\":{\"number\":\"31\",\"time\":\"3720\"}}'),
 	(8,'filter_time','{}'),
-	(9,'prepare_info','{}'),
+	(9,'prepare_stat','{}'),
 	(10,'tips','{}'),
 	(11,'exercise_paper_status','{}'),
 	(12,'sentence_paper_status','{}'),
@@ -732,12 +733,15 @@ CREATE TABLE textbook_library (
   quant varchar(255) NOT NULL DEFAULT '' COMMENT '数学',
   quant_version int(11) unsigned NOT NULL DEFAULT '0' COMMENT '数学版本',
   quant_time datetime DEFAULT NULL COMMENT '数学时间',
+  quant_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '数学题目数',
   ir varchar(255) NOT NULL DEFAULT '' COMMENT '综合逻辑',
   ir_version int(11) unsigned NOT NULL DEFAULT '0' COMMENT '综合逻辑版本',
   ir_time datetime DEFAULT NULL COMMENT '综合逻辑时间',
+  ir_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '综合逻辑题目数',
   rc varchar(255) NOT NULL DEFAULT '' COMMENT '阅读',
   rc_version int(10) unsigned NOT NULL DEFAULT '0' COMMENT '阅读版本',
   rc_time datetime DEFAULT NULL COMMENT '阅读时间',
+  rc_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '阅读题目数',
   history_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新次数',
   question_status tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '提问状态',
   create_time datetime DEFAULT NULL,
@@ -1370,6 +1374,7 @@ CREATE TABLE user_textbook_feedback (
   id int(11) unsigned NOT NULL AUTO_INCREMENT,
   user_id int(11) unsigned NOT NULL COMMENT '用户id',
   topic_id int(11) unsigned NOT NULL COMMENT '机经问题',
+  question_subject varchar(20) NOT NULL COMMENT '学科',
   library_id int(11) unsigned NOT NULL COMMENT '换库表',
   target varchar(20) NOT NULL DEFAULT '' COMMENT '反馈类型',
   content text COMMENT '正确内容',

+ 17 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/SettingController.java

@@ -347,6 +347,23 @@ public class SettingController {
         return ResponseHelp.success(entity.getValue());
     }
 
+    @RequestMapping(value = "/prepare_info", method = RequestMethod.PUT)
+    @ApiOperation(value = "修改备考信息", httpMethod = "PUT")
+    private Response<Boolean> editPrepareInfo(@RequestBody @Validated JSONObject dto){
+        Setting entity = settingService.getByKey(SettingKey.PREPARE_INFO);
+        entity.setValue(dto);
+        settingService.edit(entity);
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/prepare_info", method = RequestMethod.GET)
+    @ApiOperation(value = "获取备考信息", httpMethod = "GET")
+    private Response<JSONObject> getPrepareInfo(){
+        Setting entity = settingService.getByKey(SettingKey.PREPARE_INFO);
+
+        return ResponseHelp.success(entity.getValue());
+    }
+
     @RequestMapping(value = "/experience_info", method = RequestMethod.PUT)
     @ApiOperation(value = "修改心经信息", httpMethod = "PUT")
     private Response<Boolean> editExperienceInfo(@RequestBody @Validated JSONObject dto){

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

@@ -3,6 +3,8 @@ package com.qxgmat.controller.admin;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.*;
+import com.qxgmat.data.constants.enums.QuestionSubject;
+import com.qxgmat.data.constants.enums.TextbookSubject;
 import com.qxgmat.data.constants.enums.TopicQuality;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.entity.*;
@@ -216,14 +218,17 @@ public class TextbookController {
     public Response<TextbookTopic> addTopic(@RequestBody @Validated TextbookTopic dto, HttpServletRequest request) {
         TextbookTopic entity = Transform.convert(dto, TextbookTopic.class);
         entity = textbookTopicService.add(entity);
+        textbookService.updateLibraryNo(entity.getLibraryId(), entity.getQuestionSubject());
         managerLogService.log(request);
         return ResponseHelp.success(Transform.convert(entity, TextbookTopic.class));
     }
+
     @RequestMapping(value = "/topic/edit", method = RequestMethod.PUT)
     @ApiOperation(value = "编辑机经题目", httpMethod = "PUT")
     public Response<Boolean> editTopic(@RequestBody @Validated TextbookTopic dto, HttpServletRequest request) {
         TextbookTopic entity = Transform.convert(dto, TextbookTopic.class);
         entity = textbookTopicService.edit(entity);
+        textbookService.updateLibraryNo(entity.getLibraryId(), entity.getQuestionSubject());
         managerLogService.log(request);
         return ResponseHelp.success(true);
     }
@@ -231,7 +236,9 @@ public class TextbookController {
     @RequestMapping(value = "/topic/delete", method = RequestMethod.DELETE)
     @ApiOperation(value = "删除机经题目", httpMethod = "DELETE")
     public Response<Boolean> deleteTopic(@RequestParam int id, HttpServletRequest request) {
+        TextbookTopic entity = textbookTopicService.get(id);
         textbookTopicService.delete(id);
+        textbookService.updateLibraryNo(entity.getLibraryId(), entity.getQuestionSubject());
         managerLogService.log(request);
         return ResponseHelp.success(true);
     }

+ 48 - 8
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -4,12 +4,10 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.github.pagehelper.Page;
 import com.nuliji.tools.*;
+import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
-import com.qxgmat.data.constants.enums.MessageType;
-import com.qxgmat.data.constants.enums.QuestionSubject;
-import com.qxgmat.data.constants.enums.QuestionType;
-import com.qxgmat.data.constants.enums.SettingKey;
+import com.qxgmat.data.constants.enums.*;
 import com.qxgmat.data.constants.enums.module.*;
 import com.qxgmat.data.constants.enums.status.AskStatus;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
@@ -358,6 +356,25 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/vip/info", method = RequestMethod.GET)
+    @ApiOperation(value = "vip信息", httpMethod = "GET")
+    public Response<UserVipInfoDto> info(HttpSession session) {
+        User user = (User) shiroHelp.getLoginUser();
+        UserVipInfoDto dto = new UserVipInfoDto();
+
+        if (user != null){
+            UserService userService = userServiceService.getService(user.getId(), ServiceKey.VIP);
+            dto.setHasService(userService != null);
+            UserOrderRecord record = userOrderRecordService.getUnUseService(user.getId(), ServiceKey.VIP);
+            dto.setUnUseRecord(Transform.convert(record, UserServiceRecordExtendDto.class));
+
+            dto.setStartTime(userService!=null ? userService.getStartTime() : null);
+            dto.setExpireTime(userService != null ? userService.getExpireTime() : null);
+        }
+
+        return ResponseHelp.success(dto);
+    }
+
     @RequestMapping(value = "/message", method = RequestMethod.GET)
     @ApiOperation(value = "用户站内信", notes = "用户消息列表", httpMethod = "GET")
     public Response<PageMessage<UserMessage>> message(
@@ -427,9 +444,13 @@ public class MyController {
         User entity = usersService.get(user.getId());
         UserPrepareDetailDto dto = Transform.convert(entity, UserPrepareDetailDto.class);
 
-        Setting setting = settingService.getByKey(SettingKey.PREPARE_INFO);
-        JSONObject value = setting.getValue();
-        dto.setStat(value);
+        Setting settingStat = settingService.getByKey(SettingKey.PREPARE_STAT);
+        JSONObject valueStat = settingStat.getValue();
+        dto.setStat(valueStat);
+
+        Setting settingInfo = settingService.getByKey(SettingKey.PREPARE_INFO);
+        JSONObject valueInfo = settingInfo.getValue();
+        dto.setInfo(valueInfo);
         return ResponseHelp.success(dto);
     }
 
@@ -689,6 +710,8 @@ public class MyController {
             Integer incorrectTime = 0;
 
             List<QuestionNo> list = relationList.stream().filter((row)->row.getQuestion().getQuestionType().equals(questionType)).collect(Collectors.toList());
+            dto.setQuestionNumber(list.size());
+
             PaperStat stat = questionNoService.statPaper(list);
             dto.setTotalCorrect(stat.getTotalCorrect());
             dto.setTotalNumber(stat.getTotalNumber());
@@ -696,8 +719,10 @@ public class MyController {
 
             Collection questionNoIds = Transform.getIds(list, QuestionNo.class, "id");
             List<UserQuestion> userQuestionList = userQuestionService.listByQuestionWithTime(user.getId(), QuestionModule.BASE, questionNoIds, startTime, endTime);
-            UserQuestionStat userQuestionStat = userQuestionService.statQuestion(userQuestionList);
+            Map userQuestionMap = Transform.getMap(userQuestionList, UserQuestion.class, "questionNoId");
+            dto.setUserQuestion(userQuestionMap.size());
 
+            UserQuestionStat userQuestionStat = userQuestionService.statQuestion(userQuestionList);
             dto.setUserCorrect(userQuestionStat.getUserCorrect());
             dto.setUserNumber(userQuestionStat.getUserNumber());
             dto.setUserTime(userQuestionStat.getUserTime());
@@ -1467,6 +1492,21 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+
+    @RequestMapping(value = "/data/subscribe", method = RequestMethod.POST)
+    @ApiOperation(value = "资料订阅", notes = "资料订阅", httpMethod = "POST")
+    public Response<Boolean> addComment(@RequestBody @Validated DataSubscribeDto dto)  {
+        User user = (User) shiroHelp.getLoginUser();
+        if (user == null){
+            throw new AuthException("请先登录");
+        }
+        usersService.edit(User.builder()
+                .id(user.getId())
+                .dataEmailSubscribe(dto.getSubscribe() ? 1 : 0)
+                .build());
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/data/history", method = RequestMethod.GET)
     @ApiOperation(value = "资料更新记录", httpMethod = "GET")
     public Response<PageMessage<CourseDataHistoryInfoDto>> listDataHistory(

+ 1 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java

@@ -426,6 +426,7 @@ public class QuestionController {
             dto.setUnUseRecord(Transform.convert(record, UserServiceRecordExtendDto.class));
 
             dto.setReset(userService != null && userService.getIsReset() > 0);
+            dto.setStartTime(userService!=null ? userService.getStartTime() : null);
             dto.setExpireTime(userService != null ? userService.getExpireTime() : null);
 
             dto.setCanReset(examinationService.isFinishCat(user.getId()));

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

@@ -209,12 +209,13 @@ public class TextbookController
         dto.setLatest(latest);
         dto.setHasService(false);
         if (user != null){
-            UserService userService = userServiceService.getService(user.getId(), ServiceKey.TEXTBOOK);
-            dto.setHasService(userService != null);
+            UserService userService = userServiceService.getServiceBase(user.getId(), ServiceKey.TEXTBOOK);
+            dto.setHasService(userService != null && userService.getExpireTime().after(new Date()));
             UserOrderRecord record = userOrderRecordService.getUnUseService(user.getId(), ServiceKey.TEXTBOOK);
             dto.setUnUseRecord(Transform.convert(record, UserServiceRecordExtendDto.class));
 
             dto.setSubscribe(userService != null && userService.getIsSubscribe() > 0);
+            dto.setStartTime(userService != null ? userService.getStartTime() : null);
             dto.setExpireTime(userService != null ? userService.getExpireTime() : null);
         }
         if (!dto.getHasService()){

+ 13 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/DataSubscribeDto.java

@@ -0,0 +1,13 @@
+package com.qxgmat.dto.request;
+
+public class DataSubscribeDto {
+    private Boolean subscribe;
+
+    public Boolean getSubscribe() {
+        return subscribe;
+    }
+
+    public void setSubscribe(Boolean subscribe) {
+        this.subscribe = subscribe;
+    }
+}

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

@@ -31,6 +31,8 @@ public class MyDto extends UserDto {
 
     private Integer latestExercise;
 
+    private Integer dataEmailSubscribe;
+
     private Date vip;
 
     private int messageNum;
@@ -138,4 +140,12 @@ public class MyDto extends UserDto {
     public void setMobile(String mobile) {
         this.mobile = mobile;
     }
+
+    public Integer getDataEmailSubscribe() {
+        return dataEmailSubscribe;
+    }
+
+    public void setDataEmailSubscribe(Integer dataEmailSubscribe) {
+        this.dataEmailSubscribe = dataEmailSubscribe;
+    }
 }

+ 20 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserDataDto.java

@@ -3,12 +3,16 @@ package com.qxgmat.dto.response;
 import com.alibaba.fastjson.JSONArray;
 
 public class UserDataDto {
+    private Integer questionNumber;
+
     private Integer totalNumber;
 
     private Integer totalTime;
 
     private Integer totalCorrect;
 
+    private Integer userQuestion;
+
     private Integer userNumber;
 
     private Integer userCorrect;
@@ -102,4 +106,20 @@ public class UserDataDto {
     public void setPlace(JSONArray place) {
         this.place = place;
     }
+
+    public Integer getQuestionNumber() {
+        return questionNumber;
+    }
+
+    public void setQuestionNumber(Integer questionNumber) {
+        this.questionNumber = questionNumber;
+    }
+
+    public Integer getUserQuestion() {
+        return userQuestion;
+    }
+
+    public void setUserQuestion(Integer userQuestion) {
+        this.userQuestion = userQuestion;
+    }
 }

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

@@ -7,6 +7,8 @@ import java.util.Date;
 public class UserExaminationInfoDto {
     private Date expireTime;
 
+    private Date startTime;
+
     private Boolean hasService;
 
     private UserServiceRecordExtendDto unUseRecord;
@@ -54,4 +56,12 @@ public class UserExaminationInfoDto {
     public void setExpireTime(Date expireTime) {
         this.expireTime = expireTime;
     }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
 }

+ 13 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserPrepareDetailDto.java

@@ -33,6 +33,11 @@ public class UserPrepareDetailDto {
      */
     private JSONObject stat;
 
+    /**
+     * 备考信息:{link:{url,title}}
+     */
+    private JSONObject info;
+
 
     public String getPrepareStatus() {
         return prepareStatus;
@@ -73,4 +78,12 @@ public class UserPrepareDetailDto {
     public void setStat(JSONObject stat) {
         this.stat = stat;
     }
+
+    public JSONObject getInfo() {
+        return info;
+    }
+
+    public void setInfo(JSONObject info) {
+        this.info = info;
+    }
 }

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

@@ -8,6 +8,8 @@ import java.util.Date;
 public class UserTextbookInfoDto {
     private Date expireTime;
 
+    private Date startTime;
+
     private TextbookLibrary latest;
 
     private TextbookLibrary second;
@@ -65,4 +67,12 @@ public class UserTextbookInfoDto {
     public void setExpireTime(Date expireTime) {
         this.expireTime = expireTime;
     }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
 }

+ 47 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserVipInfoDto.java

@@ -0,0 +1,47 @@
+package com.qxgmat.dto.response;
+
+import com.qxgmat.dto.extend.UserServiceRecordExtendDto;
+
+import java.util.Date;
+
+public class UserVipInfoDto {
+    private Date expireTime;
+
+    private Date startTime;
+
+    private Boolean hasService;
+
+    private UserServiceRecordExtendDto unUseRecord;
+
+    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 getExpireTime() {
+        return expireTime;
+    }
+
+    public void setExpireTime(Date expireTime) {
+        this.expireTime = expireTime;
+    }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
+}

+ 0 - 2
server/gateway-api/src/main/java/com/qxgmat/service/extend/ExaminationService.java

@@ -217,8 +217,6 @@ public class ExaminationService extends AbstractService {
         return new PageResult<>(list, p.getTotal());
     }
 
-
-
     /**
      * cat模考是否已经完成
      * @param userId

+ 11 - 70
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -475,6 +475,7 @@ public class OrderFlowService {
         initRecordCallback.put(ProductType.DATA, ((order, record)->{
             // 资料无需开启,也没有有效期
             record.setIsUsed(1);
+            record.setIsSubscribe(1);
             Date time = new Date();
             record.setUseTime(time);
 
@@ -623,44 +624,15 @@ public class OrderFlowService {
         }));
 
         stopRecordCallback.put(ProductType.COURSE, (record->{
-            record.setIsUsed(1);
-            Date time = new Date();
-            record.setUseTime(time);
-
-            Course course = courseService.get(record.getProductId());
-            Integer expireDay = 0;
-            if (course.getCourseModule().equals(CourseModule.VS.key)){
-                // 根据课时数进行计算
-                expireDay = courseExtendService.computeExpire(record.getNumber(), course);
-            }else{
-                // 根据设置进行计算
+            record.setIsStop(1);
+            if (record.getIsUsed() == 0) return record;
+            if (record.getUseEndTime().before(new Date())) return record;
 
-            }
-            Date startTime = time;
-            Date endTime = Tools.addDate(startTime, expireDay);
             UserCourse userCourse = userCourseService.getCourseBase(record.getUserId(), record.getProductId());
-            if(userCourse == null){
-                userCourse = UserCourse.builder()
-                        .userId(record.getUserId())
-                        .recordId(record.getId())
-                        .startTime(startTime)
-                        .expireTime(endTime)
-                        .build();
-                userCourse = userCourseService.add(userCourse);
-            }else{
-                if (userCourse.getExpireTime().before(time)){
-                    // 已到期 - 续期
-                    userCourse.setStartTime(startTime);
-                    userCourse.setExpireTime(endTime);
-                    userCourse.setRecordId(record.getId());
-                }else{
-                    // 未到期 - 报错
-                    throw new ParameterException("已开通当前课程");
-                }
+            if(userCourse != null){
+                userCourse.setExpireTime(new Date());
                 userCourse = userCourseService.edit(userCourse);
             }
-            record.setUseStartTime(startTime);
-            record.setUseEndTime(endTime);
             return record;
         }));
         stopRecordCallback.put(ProductType.COURSE_PACKAGE, (record->{
@@ -673,48 +645,17 @@ public class OrderFlowService {
             return record;
         }));
         stopRecordCallback.put(ProductType.SERVICE, (record->{
-            record.setIsUsed(1);
-            Date time = new Date();
-            record.setUseTime(time);
+            record.setIsStop(1);
+            if (record.getIsUsed() == 0) return record;
+            if (record.getUseEndTime().before(new Date())) return record;
 
             ServiceKey serviceKey = ServiceKey.ValueOf(record.getService());
-            Integer expireDay = serviceKey.useExpireDay;
-            if(serviceKey == ServiceKey.VIP){
-                ServiceVipKey vipKey = ServiceVipKey.ValueOf(record.getParam());
-                expireDay = vipKey.useExpireDay;
-            }
 
-            Date startTime = time;
-            Date endTime = Tools.addDate(startTime, expireDay);
             UserService userService = userServiceService.getServiceBase(record.getUserId(), serviceKey);
-            if (userService == null){
-                userService = UserService.builder()
-                        .userId(record.getUserId())
-                        .isSubscribe(record.getIsSubscribe())
-                        .startTime(startTime)
-                        .expireTime(endTime)
-                        .build();
-                userService = userServiceService.add(userService);
-            }else{
-                if (userService.getExpireTime().before(time)){
-                    // 已到期 - 续期
-                    userService.setStartTime(startTime);
-                    userService.setExpireTime(endTime);
-                }else{
-                    if (serviceKey == ServiceKey.QX_CAT){
-                        // 未到期 - 报错
-                        throw new ParameterException("已开通当前服务");
-                    }
-                    // 未到期 - 延长有效期
-                    startTime = userService.getExpireTime();
-                    endTime = Tools.addDate(userService.getExpireTime(), expireDay);
-                    userService.setExpireTime(endTime);
-                }
-                userService.setIsSubscribe(record.getIsSubscribe());
+            if (userService != null){
+                userService.setExpireTime(new Date());
                 userService = userServiceService.edit(userService);
             }
-            record.setUseStartTime(startTime);
-            record.setUseEndTime(endTime);
             return record;
         }));
     }

+ 45 - 5
server/gateway-api/src/main/java/com/qxgmat/service/extend/TextbookService.java

@@ -1,13 +1,11 @@
 package com.qxgmat.service.extend;
 
 import com.nuliji.tools.Tools;
+import com.qxgmat.data.constants.enums.TextbookSubject;
 import com.qxgmat.data.constants.enums.logic.SentenceLogic;
 import com.qxgmat.data.constants.enums.logic.TextbookLogic;
 import com.qxgmat.data.constants.enums.module.QuestionModule;
-import com.qxgmat.data.dao.entity.Question;
-import com.qxgmat.data.dao.entity.TextbookLibrary;
-import com.qxgmat.data.dao.entity.TextbookPaper;
-import com.qxgmat.data.dao.entity.TextbookQuestion;
+import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.TextbookQuestionRelation;
 import com.qxgmat.service.inline.*;
 import org.springframework.beans.factory.annotation.Value;
@@ -45,6 +43,9 @@ public class TextbookService {
     @Resource
     private UserTextbookEnrollService userTextbookEnrollService;
 
+    @Resource
+    private TextbookTopicService textbookTopicService;
+
     /**
      * 报名
      * @param userId
@@ -53,7 +54,14 @@ public class TextbookService {
     @Transactional
     public void enroll(Integer userId, Date date){
         date = Tools.month(date);
-
+        UserTextbookEnroll in = userTextbookEnrollService.get(userId, date.toString());
+        if (in != null){
+            return;
+        }
+        userTextbookEnrollService.add(UserTextbookEnroll.builder()
+                .userId(userId)
+                .month(date)
+                .build());
     }
 
     /**
@@ -128,7 +136,39 @@ public class TextbookService {
         return textbookQuestion;
     }
 
+    /**
+     * 更新换库表题目数
+     * @param libraryId
+     * @param subject
+     */
+    public void updateLibraryNo(Integer libraryId, String subject){
+        TextbookTopic last = textbookTopicService.lastByLibrary(libraryId,subject);
+        TextbookLibrary library = textbookLibraryService.get(last.getLibraryId());
+        TextbookLibrary tmp = TextbookLibrary.builder()
+                .id(library.getId())
+                .build();
+        switch(TextbookSubject.ValueOf(subject)){
+            case QUANT:
+                if (!last.getNo().equals(library.getQuantNumber())){
+                    tmp.setQuantNumber(last.getNo());
+                    textbookLibraryService.edit(tmp);
+                }
+                break;
+            case IR:
+                if (!last.getNo().equals(library.getIrNumber())){
+                    tmp.setIrNumber(last.getNo());
+                    textbookLibraryService.edit(tmp);
+                }
+                break;
+            case RC:
+                if (!last.getNo().equals(library.getRcNumber())){
+                    tmp.setRcNumber(last.getNo());
+                    textbookLibraryService.edit(tmp);
+                }
+                break;
 
+        }
+    }
 
     private void addQuestionToPaper(TextbookLibrary library, TextbookQuestion question, TextbookLogic logic){
         String prefixTitle = generatePrefixTitle(library);

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

@@ -46,6 +46,16 @@ public class UserTextbookEnrollService extends AbstractService {
         return userTextbookEnrollRelationMapper.groupByMonth(startTime, endTime);
     }
 
+    public UserTextbookEnroll get(Integer userId, String month){
+        Example example = new Example(UserTextbookEnroll.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("month", month)
+        );
+        return one(userTextbookEnrollMapper, example);
+    }
+
     public UserTextbookEnroll add(UserTextbookEnroll ad){
         int result = insert(userTextbookEnrollMapper, ad);
         ad = one(userTextbookEnrollMapper, ad.getId());

+ 1 - 0
server/gateway-api/src/main/java/com/qxgmat/task/AsyncTask.java

@@ -283,6 +283,7 @@ public class AsyncTask {
             Collection userIds = Transform.getIds(userOrderRecordList, UserOrderRecord.class, "userId");
             List<User> userList = usersService.select(userIds);
             for(User user : userList){
+                if (user.getDataEmailSubscribe() == 0) continue;
                 messageExtendService.sendDataUpdate(user, courseData, history);
             }
         }while(userOrderRecordList.size() >= size);