ソースを参照

feat(fronnt): 头像

Go 5 年 前
コミット
4bab3a2991
60 ファイル変更4697 行追加273 行削除
  1. 31 0
      front/package-lock.json
  2. 2 0
      front/package.json
  3. 7 1
      front/project/Constant.js
  4. 8 2
      front/project/h5/local.json
  5. 8 2
      front/project/h5/routes/page/home/page.js
  6. 2 2
      front/project/h5/stores/main.js
  7. 9 0
      front/project/h5/stores/my.js
  8. 40 29
      front/project/www/components/Examination/index.js
  9. 71 30
      front/project/www/components/Invite/index.js
  10. 298 32
      front/project/www/components/OtherModal/index.js
  11. 1 0
      front/project/www/components/OtherModal/index.less
  12. 37 16
      front/project/www/components/VipRenew/index.js
  13. 5 2
      front/project/www/layouts/User/index.js
  14. 7 0
      front/project/www/local.json
  15. 15 9
      front/project/www/routes/examination/main/page.js
  16. 30 15
      front/project/www/routes/exercise/main/page.js
  17. 2 1
      front/project/www/routes/my/answer/index.js
  18. 2 1
      front/project/www/routes/my/collect/index.js
  19. 2 1
      front/project/www/routes/my/course/index.js
  20. 2 1
      front/project/www/routes/my/data/index.js
  21. 2 1
      front/project/www/routes/my/error/index.js
  22. 2 1
      front/project/www/routes/my/main/index.js
  23. 5 0
      front/project/www/routes/my/main/index.less
  24. 155 53
      front/project/www/routes/my/main/page.js
  25. 2 1
      front/project/www/routes/my/message/index.js
  26. 2 1
      front/project/www/routes/my/note/index.js
  27. 2 1
      front/project/www/routes/my/order/index.js
  28. 2 1
      front/project/www/routes/my/report/index.js
  29. 2 1
      front/project/www/routes/my/tools/index.js
  30. 10 2
      front/project/www/stores/main.js
  31. 18 1
      front/project/www/stores/my.js
  32. 2 2
      front/src/components/FileUpload/index.js
  33. 3739 0
      front/src/static/qrious.js
  34. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrder.java
  35. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserOrderMapper.xml
  36. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionNoRelationMapper.xml
  37. 7 7
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCourseRecordRelationMapper.xml
  38. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRelationMapper.xml
  39. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserQuestionRelationMapper.xml
  40. 10 10
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml
  41. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserSentenceRecordRelationMapper.xml
  42. 1 1
      server/data/src/main/resources/application-data.yml
  43. 3 2
      server/data/src/main/resources/db/migration/V1__init_table.sql
  44. 1 1
      server/gateway-api/build.gradle
  45. 6 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CommonController.java
  46. 21 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  47. 1 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/OrderController.java
  48. 0 4
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/QuestionDto.java
  49. 36 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserMobileDto.java
  50. 20 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  51. 7 3
      server/gateway-api/src/main/java/com/qxgmat/help/AiHelp.java
  52. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java
  53. 2 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  54. 0 2
      server/gateway-api/src/main/java/com/qxgmat/util/shiro/ManagerRealm.java
  55. 0 2
      server/gateway-api/src/main/java/com/qxgmat/util/shiro/OauthRealm.java
  56. 0 5
      server/gateway-api/src/main/java/com/qxgmat/util/shiro/TokenRealm.java
  57. 2 5
      server/gateway-api/src/main/java/com/qxgmat/util/shiro/UserRealm.java
  58. 1 1
      server/gateway-api/src/main/profile/prod/application-runtime.yml
  59. 1 1
      server/gateway-api/src/main/profile/test/application-runtime.yml
  60. 11 6
      server/tools/build.gradle

+ 31 - 0
front/package-lock.json

@@ -3326,6 +3326,11 @@
         "gud": "^1.0.0"
       }
     },
+    "cropperjs": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npm.taobao.org/cropperjs/download/cropperjs-1.5.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcropperjs%2Fdownload%2Fcropperjs-1.5.5.tgz",
+      "integrity": "sha1-B9BMZCRKuIwb/DgqhI7DRu9Qt7k="
+    },
     "cross-spawn": {
       "version": "5.1.0",
       "resolved": "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz",
@@ -9229,6 +9234,32 @@
         "object-assign": "^4.1.0"
       }
     },
+    "react-copy-to-clipboard": {
+      "version": "5.0.1",
+      "resolved": "http://registry.npm.taobao.org/react-copy-to-clipboard/download/react-copy-to-clipboard-5.0.1.tgz",
+      "integrity": "sha1-jq4Qe7QAvnMTLtO2p7T7FWCQII4=",
+      "requires": {
+        "copy-to-clipboard": "^3",
+        "prop-types": "^15.5.8"
+      }
+    },
+    "react-cropper": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npm.taobao.org/react-cropper/download/react-cropper-1.3.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-cropper%2Fdownload%2Freact-cropper-1.3.0.tgz",
+      "integrity": "sha1-feDAmkQNYvcsDfXxBNStdZjMO/A=",
+      "requires": {
+        "babel-core": "7.0.0-bridge.0",
+        "cropperjs": "^1.5.5",
+        "prop-types": "^15.5.8"
+      },
+      "dependencies": {
+        "babel-core": {
+          "version": "7.0.0-bridge.0",
+          "resolved": "http://registry.npm.taobao.org/babel-core/download/babel-core-7.0.0-bridge.0.tgz",
+          "integrity": "sha1-laSS3dkPm06aSh2hTrM1uHtjTs4="
+        }
+      }
+    },
     "react-dom": {
       "version": "16.8.4",
       "resolved": "http://registry.npm.taobao.org/react-dom/download/react-dom-16.8.4.tgz",

+ 2 - 0
front/package.json

@@ -82,6 +82,8 @@
     "react": "^16.6.3",
     "react-addons-css-transition-group": "^15.6.2",
     "react-contextmenu": "^2.10",
+    "react-copy-to-clipboard": "^5.0.1",
+    "react-cropper": "^1.3.0",
     "react-dom": "^16.6.3",
     "react-fullscreen-crossbrowser": "^1.0.9",
     "react-quill": "^1.3.3",

+ 7 - 1
front/project/Constant.js

@@ -50,7 +50,13 @@ export const FeedbackStatus = [{ value: 0, label: '新增' }, { value: 1, label:
 
 export const FeedbackModule = [{ value: 'question', label: '题目' }, { value: 'data', label: '资料' }];
 
-export const PrepareStatus = [{ label: '学生-Domestic', value: 'student_domestic' }, { label: '学生-Overseas', value: 'student_overseas' }, { label: '在职-Domestic', value: 'worker_domestic' }, { label: '在职-Overseas', value: 'worker_overseas' }, { label: 'Gap Year', value: 'gap_year' }];
+export const PrepareStatus = [
+  { label: '学生-Domestic', value: 'student_domestic', short: '学生-D' },
+  { label: '学生-Overseas', value: 'student_overseas', short: '学生-O' },
+  { label: '在职-Domestic', value: 'worker_domestic', short: '在职-D' },
+  { label: '在职-Overseas', value: 'worker_overseas', short: '在职-O' },
+  { label: 'Gap Year', value: 'gap_year', short: 'Gap Year' },
+];
 
 export const PrepareExaminationTime = [{ label: '近1个月', value: 'one_month' }, { label: '近2个月', value: 'two_month' }, { label: '近3个月', value: 'three_month' }, { label: '半年内', value: 'six_month' }];
 

+ 8 - 2
front/project/h5/local.json

@@ -13,9 +13,15 @@
     ]
   },
   "test": {
-    "scripts": []
+    "scripts": [
+      "http://res.wx.qq.com/open/js/jweixin-1.2.0.js",
+      "/sha1.js"
+    ]
   },
   "production": {
-    "scripts": []
+    "scripts": [
+      "http://res.wx.qq.com/open/js/jweixin-1.2.0.js",
+      "/sha1.js"
+    ]
   }
 }

+ 8 - 2
front/project/h5/routes/page/home/page.js

@@ -1,7 +1,9 @@
 import React from 'react';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import { PcUrl } from '../../../../Constant';
 import { Main } from '../../../stores/main';
 
@@ -25,7 +27,11 @@ export default class extends Page {
           <div className="input">
             <div className="prefix">http://</div>
             <div className="value">{PcUrl.replace('http://', '')}/id/{info.inviteCode}</div>
-            <Assets name="copy" />
+            <CopyToClipboard
+              text={`${PcUrl}/id/${info.inviteCode}`}
+              onCopy={() => asyncSMessage('已复制')}>
+              <Assets name="copy" />
+            </CopyToClipboard>
           </div>
         </div>
         <div className="block-2">
@@ -110,7 +116,7 @@ export default class extends Page {
             </div>
           </div>
         </div>
-      </div>
+      </div >
     );
   }
 }

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

@@ -23,8 +23,8 @@ export default class MainStore extends BaseStore {
   /**
    * 获取广告列表
    */
-  getAd() {
-    return this.apiGet('/base/ad');
+  getAd(channel) {
+    return this.apiGet('/base/ad', { channel });
   }
 
   /**

+ 9 - 0
front/project/h5/stores/my.js

@@ -10,6 +10,15 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 绑定手机
+   * @param {*} area
+   * @param {*}
+   */
+  bindMobile(area, mobile) {
+    return this.apiPost('/my/mobile', { area, mobile });
+  }
+
+  /**
    * 实名认证: 正面
    * @param {*} file
    */

+ 40 - 29
front/project/www/components/Examination/index.js

@@ -1,12 +1,18 @@
 import React, { Component } from 'react';
 import './index.less';
 import { Icon } from 'antd';
+import { getMap, formatDate } from '@src/services/Tools';
 import { SpecialRadioGroup } from '../Radio';
 import Modal from '../Modal';
 import Button from '../Button';
 import TotalSort from '../TotalSort';
 import Date from '../Date';
 import Ratio from '../Ratio';
+import { My } from '../../stores/my';
+import { PrepareStatus, PrepareExaminationTime } from '../../../Constant';
+
+const PrepareStatusMap = getMap(PrepareStatus, 'value', 'short');
+const PrepareExaminationTimeMap = getMap(PrepareExaminationTime, 'value', 'label');
 
 export default class extends Component {
   constructor(props) {
@@ -32,7 +38,11 @@ export default class extends Component {
         cancelText: '修改',
       },
     };
-    this.state = { step: 0, data: { type: '1', time: '1' } };
+    this.state = { step: 0, data: {} };
+    My.getPrepare()
+      .then(result => {
+        this.setState({ data: result, first: !result.prepareStatus });
+      });
   }
 
   onChange(type, key) {
@@ -51,6 +61,15 @@ export default class extends Component {
     this.setState({ step: step + 1 });
   }
 
+  submitPrepare() {
+    const { data } = this.state;
+    My.editPrepare(data)
+      .then(result => {
+        this.setState({ result });
+        this.onNext();
+      });
+  }
+
   render() {
     const { step } = this.state;
     const { show, onClose } = this.props;
@@ -69,21 +88,15 @@ export default class extends Component {
 
   renderStep0() {
     const { data } = this.state;
-    const { type } = data;
+    const { prepareStatus } = data;
     return (
       <div className="step-0-layout">
         <SpecialRadioGroup
-          list={[
-            { label: '学生-Domestic', value: '1' },
-            { label: '学生-Overseas', value: '2' },
-            { label: '在职-Domestic', value: '3' },
-            { label: '在职-Overseas', value: '4' },
-            { label: 'Gap Year', value: '5' },
-          ]}
-          value={type}
+          list={PrepareStatus}
+          value={prepareStatus}
           width={190}
           space={10}
-          onChange={key => this.onChange('type', key)}
+          onChange={key => this.onChange('prepareStatus', key)}
         />
         <div className="action-layout">
           <div className="next" onClick={() => this.onNext()}>
@@ -97,20 +110,15 @@ export default class extends Component {
 
   renderStep1() {
     const { data } = this.state;
-    const { time } = data;
+    const { prepareExaminationTime } = data;
     return (
       <div className="step-1-layout">
         <SpecialRadioGroup
-          list={[
-            { label: '近1个月', value: '1' },
-            { label: '近2个月', value: '2' },
-            { label: '近3个月', value: '3' },
-            { label: '半年内', value: '4' },
-          ]}
-          value={time}
+          list={PrepareExaminationTime}
+          value={prepareExaminationTime}
           width={195}
           space={10}
-          onChange={key => this.onChange('time', key)}
+          onChange={key => this.onChange('prepareExaminationTime', key)}
         />
         <div className="action-layout">
           <div className="prev" onClick={() => this.onPrev()}>
@@ -153,7 +161,9 @@ export default class extends Component {
             <Icon type="left-circle" theme="filled" />
             上一题
           </div>
-          <Button size="lager" radius>
+          <Button size="lager" radius onClick={() => {
+            this.submitPrepare();
+          }}>
             提交
           </Button>
         </div>
@@ -162,26 +172,27 @@ export default class extends Component {
   }
 
   renderStep4() {
+    const { first, data } = this.state;
     return (
       <div className="step-4-layout">
-        <div className="tip">
+        {first && <div className="tip">
           <Icon type="check" />
           7天VIP权限已赠送至您的账户。
-        </div>
+        </div>}
         <Ratio
           text="身份"
-          subtext="在职(D)"
+          subtext={PrepareStatusMap[data.prepareStatus]}
           values={[
             { label: '学生-D; 20%', value: 10, color: '#41A6F3' },
-            { label: '学生- O; 20%', value: 20, color: '#3F86EA' },
+            { label: '学生-O; 20%', value: 20, color: '#3F86EA' },
             { label: '在职-D; 20%', value: 20, color: '#41A6F3' },
-            { label: '在职- O; 20%', value: 20, color: '#6DC0FF' },
+            { label: '在职-O; 20%', value: 20, color: '#6DC0FF' },
             { label: 'Gap Year; 20%', value: 20, color: '#9BD4FF' },
           ]}
         />
         <Ratio
           text="考试时间"
-          subtext="近1个月"
+          subtext={PrepareExaminationTimeMap[data.prepareExaminationTime]}
           values={[
             { label: '近1个月; 20%', value: 10, color: '#6865FD' },
             { label: '近2个月; 20%', value: 20, color: '#322EF2' },
@@ -191,7 +202,7 @@ export default class extends Component {
         />
         <Ratio
           text="目标成绩"
-          subtext="670分"
+          subtext={`${data.prepareGoal}分`}
           values={[
             { label: '600+; 20%', value: 10, color: '#2754E0' },
             { label: '650+; 20%', value: 20, color: '#3F86EA' },
@@ -201,7 +212,7 @@ export default class extends Component {
         />
         <Ratio
           text="出分日期"
-          subtext="2019-11"
+          subtext={`${formatDate(data.prepareScoreTime, 'YYYY-MM')}`}
           values={[
             { label: '近1个月; 20%', value: 10, color: '#6865FD' },
             { label: '近2个月; 20%', value: 20, color: '#322EF2' },

+ 71 - 30
front/project/www/components/Invite/index.js

@@ -1,44 +1,85 @@
-import React from 'react';
+import React, { Component } from 'react';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
 import './index.less';
 import Assets from '@src/components/Assets';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import { Icon } from 'antd';
 import Button from '../Button';
 import { Input } from '../Login';
+import { Main } from '../../stores/main';
+import { My } from '../../stores/my';
+import { PcUrl, H5Url } from '../../../Constant';
 
-function Invite() {
-  return (
-    <div className="invite-block">
-      <div className="title-block">
-        <b>方法一:</b> 将邀请链接发送给好友
+export default class Invite extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { data: this.props.data || {}, qr: Main.qrCode(`${H5Url}/id/${(this.props.data || {}).inviteCode}`) };
+  }
+
+  changeData(field, value) {
+    let { data } = this.state;
+    data = data || {};
+    data[field] = value;
+    this.setState({ data, error: null });
+  }
+
+  send() {
+    const { data } = this.state;
+    if (!data.emails) return;
+    My.inviteEmail(data.emails.replace(/;/g, ';').split(';'))
+      .then(() => {
+        data.emails = '';
+        this.setState({ success: true, data });
+      });
+  }
+
+  render() {
+    const { data } = this.props;
+    const { success, qr } = this.state;
+    return (
+      <div className="invite-block">
+        <div className="title-block">
+          <b>方法一:</b> 将邀请链接发送给好友
       </div>
-      <div className="input-layout">
-        <div className="input-block">
-          <div className="t1">http://</div>
-          <div className="t2">qianhanggmat.com/id123134341431</div>
+        <div className="input-layout">
+          <div className="input-block">
+            <div className="t1">http://</div>
+            <div className="t2">{PcUrl.replace('http://', '')}/id/{data.inviteCode}</div>
+          </div>
+          <CopyToClipboard
+            text={`${PcUrl}/id/${data.inviteCode}`}
+            onCopy={() => asyncSMessage('已复制')}>
+            <Button radius>复制</Button>
+          </CopyToClipboard>
         </div>
-        <Button radius>复制</Button>
+        <div className="title-block">
+          <b>方法二:</b> 微信扫码分享
       </div>
-      <div className="title-block">
-        <b>方法二:</b> 微信扫码分享
-      </div>
-      <div className="qrcode">
-        <Assets name="qrcode" />
-      </div>
-      <div className="title-block">
-        <b>方法三:</b> 向好友发送邀请邮件
+        <div className="qrcode">
+          <Assets src={qr} />
+        </div>
+        <div className="title-block">
+          <b>方法三:</b> 向好友发送邀请邮件
       </div>
-      <div className="input-layout">
-        <div className="input-block">
-          <Input placeholder="输入邮箱后,回车可添加多个" />
-          <div className="success">
-            <Icon type="check" />
-            邀请已发送
+        <div className="input-layout">
+          <div className="input-block">
+            <Input
+              placeholder="输入多个邮箱,用';'隔开"
+              value={this.state.data.emails || ''}
+              error={this.state.error}
+              onChange={e => {
+                this.changeData('emails', e.target.value);
+              }}
+            />
+            {success && <div className="success">
+              <Icon type="check" />邀请已发送
+          </div>}
           </div>
+          <Button radius onClick={() => {
+            this.send();
+          }}>发送</Button>
         </div>
-        <Button radius>复制</Button>
       </div>
-    </div>
-  );
+    );
+  }
 }
-Invite.propTypes = {};
-export default Invite;

+ 298 - 32
front/project/www/components/OtherModal/index.js

@@ -1,34 +1,120 @@
 import React, { Component } from 'react';
+import Cropper from 'react-cropper';
+import 'cropperjs/dist/cropper.css';
 import './index.less';
 import FileUpload from '@src/components/FileUpload';
 import Assets from '@src/components/Assets';
+import scale from '@src/services/Scale';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import { SelectInput, VerificationInput, Input } from '../Login';
 import { MobileArea } from '../../../Constant';
 import Invite from '../Invite';
 import Modal from '../Modal';
+import { Common } from '../../stores/common';
+import { User } from '../../stores/user';
+import { My } from '../../stores/my';
 
 export class BindPhone extends Component {
   constructor(props) {
     super(props);
-    this.state = { step: 0, data: {} };
+    this.props.data = this.props.data || {};
+    this.state = this.initState(this.props);
     this.stepProp = {
       0: {
         title: '绑定手机',
-        onConfirm: () => this.onNext(),
+        onConfirm: props.onConfirm,
       },
       1: {
         title: '绑定手机',
-        onConfirm: props.onConfirm,
+        onConfirm: () => {
+          this.submit();
+        },
         onCancel: props.onCancel,
         confirmText: '提交',
       },
     };
   }
 
+  initState(props) {
+    if (this.wait) return {};
+    const data = Object.assign({}, props.data);
+    if (!data.area) data.area = MobileArea[0].value;
+    return { step: props.data.mobile ? 0 : 1, data };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState(this.initState(nextProps));
+  }
+
   onNext() {
     this.setState({ step: 1 });
   }
 
+  changeData(field, value) {
+    let { data } = this.state;
+    data = data || {};
+    data[field] = value;
+    this.setState({ data, error: null });
+  }
+
+  validMobile() {
+    const { data } = this.state;
+    const { area, mobile } = data;
+    if (!area || !mobile) return;
+    this.validNumber += 1;
+    const number = this.validNumber;
+    User.validMobile(area, mobile)
+      .then(result => {
+        if (number !== this.validNumber) return Promise.resolve();
+        return result ? Promise.resolve() : Promise.reject(new Error('该手机已绑定其他账号,请更换手机号码'));
+      })
+      .catch(err => {
+        this.setState({ mobileError: err.message });
+      });
+  }
+
+  sendValid() {
+    const { data, error } = this.state;
+    const { area, mobile } = data;
+    if (!area || !mobile || error) return Promise.reject();
+    this.wait = true;
+    return Common.sendSms(area, mobile)
+      .then(result => {
+        this.wait = false;
+        if (result) {
+          asyncSMessage('发送成功');
+          this.setState({ error: '', validError: '' });
+        } else {
+          throw new Error('发送失败');
+        }
+      })
+      .catch(err => {
+        this.wait = false;
+        this.setState({ error: err.message });
+        throw err;
+      });
+  }
+
+  submit() {
+    const { data, error } = this.state;
+    console.log(data, error);
+    if (!data.mobile || !data.area || error) return;
+    const { area, mobile } = data;
+    this.wait = true;
+    My.bindMobile(area, mobile)
+      .then(() => {
+        asyncSMessage('操作成功');
+        this.setState({ step: 0 });
+        User.infoHandle(Object.assign(this.props.data, { area, mobile }));
+        this.props.onConfirm();
+        this.wait = false;
+      })
+      .catch(e => {
+        this.setState({ error: e.message });
+        this.wait = false;
+      });
+  }
+
   render() {
     const { show } = this.props;
     const { step } = this.state;
@@ -40,9 +126,10 @@ export class BindPhone extends Component {
   }
 
   renderStep0() {
+    const { data } = this.state;
     return (
       <div className="step-0-layout">
-        已绑定手机 13123123123123
+        已绑定手机 {data.mobile}
         <a onClick={() => this.onNext()}>修改</a>
       </div>
     );
@@ -58,14 +145,14 @@ export class BindPhone extends Component {
             selectValue={this.state.data.area}
             select={MobileArea}
             value={this.state.data.mobile}
-            error={this.state.mobileError}
+            error={this.state.error}
             onSelect={value => {
               this.changeData('area', value);
-              this.validMobile(true);
+              this.validMobile();
             }}
             onChange={e => {
               this.changeData('mobile', e.target.value);
-              this.validMobile(true);
+              this.validMobile();
             }}
           />
           <VerificationInput
@@ -88,25 +175,63 @@ export class BindPhone extends Component {
 export class BindEmail extends Component {
   constructor(props) {
     super(props);
-    this.state = { step: 0, data: {} };
+    this.props.data = this.props.data || {};
+    this.state = this.initState(this.props);
     this.stepProp = {
       0: {
-        title: '绑定手机',
-        onConfirm: () => this.onNext(),
+        title: '绑定邮箱',
+        onConfirm: props.onConfirm,
       },
       1: {
-        title: '绑定手机',
-        onConfirm: props.onConfirm,
+        title: '绑定邮箱',
+        onConfirm: () => {
+          this.submit();
+        },
         onCancel: props.onCancel,
         confirmText: '提交',
       },
     };
   }
 
+  initState(props) {
+    if (this.wait) return {};
+    return { step: props.data.email ? 0 : 1, data: Object.assign({}, props.data) };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState(this.initState(nextProps));
+  }
+
   onNext() {
     this.setState({ step: 1 });
   }
 
+  changeData(field, value) {
+    let { data } = this.state;
+    data = data || {};
+    data[field] = value;
+    this.setState({ data, error: null });
+  }
+
+  submit() {
+    const { data, error } = this.state;
+    if (!data.email || error) return;
+    const { email } = data;
+    this.wait = true;
+    My.bindEmail(email)
+      .then(() => {
+        asyncSMessage('操作成功');
+        this.setState({ step: 0 });
+        User.infoHandle(Object.assign(this.props.data, { email }));
+        this.props.onConfirm();
+        this.wait = false;
+      })
+      .catch(e => {
+        this.setState({ error: e.message });
+        this.wait = false;
+      });
+  }
+
   render() {
     const { show } = this.props;
     const { step } = this.state;
@@ -118,9 +243,10 @@ export class BindEmail extends Component {
   }
 
   renderStep0() {
+    const { data } = this.state;
     return (
       <div className="step-0-layout">
-        已绑定邮箱 13123123123123
+        已绑定邮箱 {data.email}
         <a onClick={() => this.onNext()}>修改</a>
       </div>
     );
@@ -133,11 +259,10 @@ export class BindEmail extends Component {
         <div className="input-layout">
           <Input
             placeholder="请输入邮箱"
-            value={this.state.data.mobile}
-            error={this.state.mobileError}
+            value={this.state.data.email}
+            error={this.state.error}
             onChange={e => {
-              this.changeData('mobile', e.target.value);
-              this.validMobile(true);
+              this.changeData('email', e.target.value);
             }}
           />
         </div>
@@ -149,11 +274,55 @@ export class BindEmail extends Component {
 export class EditInfo extends Component {
   constructor(props) {
     super(props);
-    this.state = { data: {} };
+    this.props.data = this.props.data || {};
+    this.state = this.initState(this.props);
+  }
+
+  initState(props) {
+    if (props.image && this.waitImage) {
+      const { state } = this;
+      Common.upload(props.image).then(result => {
+        const { data } = this.state;
+        data.avatar = result.url;
+        this.setState({ data, uploading: false });
+      }).catch((e) => {
+        this.setState({ imageError: e.message });
+      });
+      state.uploading = true;
+      this.waitImage = false;
+      return state;
+    }
+    return { data: Object.assign({}, props.data) };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState(this.initState(nextProps));
+  }
+
+  changeData(field, value) {
+    let { data } = this.state;
+    data = data || {};
+    data[field] = value;
+    this.setState({ data, error: null });
+  }
+
+  submit() {
+    const { data, error } = this.state;
+    if (!data.nickname || error) return;
+    const { nickname, avatar } = data;
+    My.editInfo({ nickname, avatar })
+      .then(() => {
+        asyncSMessage('操作成功');
+        User.infoHandle(Object.assign(this.props.data, { nickname, avatar }));
+        this.props.onConfirm();
+      })
+      .catch(e => {
+        this.setState({ error: e.message });
+      });
   }
 
   render() {
-    const { show, onCancel, onConfirm } = this.props;
+    const { show, onCancel, onSelectImage } = this.props;
     return (
       <Modal
         className="edit-info-modal"
@@ -162,7 +331,9 @@ export class EditInfo extends Component {
         title="修改资料"
         confirmText="保存"
         onCancel={onCancel}
-        onConfirm={onConfirm}
+        onConfirm={() => {
+          this.submit();
+        }}
       >
         <div className="edit-info-modal-wrapper">
           <div className="edit-info-modal-block">
@@ -170,11 +341,10 @@ export class EditInfo extends Component {
             <div className="input-layout">
               <Input
                 placeholder="2-20位,不可使用特殊字符。"
-                value={this.state.data.mobile}
-                error={this.state.mobileError}
+                value={this.state.data.nickname || ''}
+                error={this.state.error}
                 onChange={e => {
-                  this.changeData('mobile', e.target.value);
-                  this.validMobile(true);
+                  this.changeData('nickname', e.target.value);
                 }}
               />
             </div>
@@ -182,7 +352,15 @@ export class EditInfo extends Component {
           <div className="edit-info-modal-block">
             <div className="label">头像</div>
             <div className="input-layout">
-              <FileUpload />
+              <FileUpload
+                uploading={this.state.uploading}
+                value={this.state.data.avatar}
+                onUpload={({ file }) => {
+                  this.waitImage = true;
+                  onSelectImage(file);
+                  return Promise.reject();
+                }} />
+              {this.state.imageError || ''}
             </div>
           </div>
         </div>
@@ -227,11 +405,71 @@ export class RealAuth extends Component {
 export class EditAvatar extends Component {
   constructor(props) {
     super(props);
-    this.state = { data: {} };
+    this.state = this.initState(props);
+  }
+
+  initState(props) {
+    if (props.image) this.loadImage(props.image);
+    return { data: {} };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState(this.initState(nextProps));
+  }
+
+  loadImage(file) {
+    this.defaultZoomValue = 1;
+    if (window.FileReader) {
+      const reader = new FileReader();
+      reader.readAsDataURL(file);
+      // 渲染文件
+      reader.onload = (arg) => {
+        this.setState({ image: arg.target.result, fileName: file.name, zoomValue: 1 });
+      };
+    } else {
+      const img = new Image();
+      img.onload = function () {
+        const canvas = document.createElement('canvas');
+        canvas.height = img.height;
+        canvas.width = img.width;
+        const ctx = canvas.getContext('2d');
+        ctx.drawImage(img, 0, 0);
+        this.setState({ image: canvas.toDataURL() });
+      };
+      img.src = '1.gif';
+    }
+  }
+
+  computerZoom() {
+    const data = this.cropRef.getImageData();
+    this.defaultZoomValue = 200 / parseFloat(Math.max(data.naturalWidth, data.naturalHeight));
+  }
+
+  crop() {
+    // image in dataUrl
+    // console.log(this.cropRef.getCroppedCanvas().toDataURL())
+    // const canvas = this.cropRef.getCroppedCanvas();
+    // const avatar = scale(this.props.crop, canvas, 'png-src');
+    // let scaleCanvas = this.cropRef.getCroppedCanvas(this.props.crop)
+    // this.setState({ avatar });
+  }
+
+  select() {
+    const { fileName } = this.state;
+    const { onConfirm } = this.props;
+    const canvas = this.cropRef.getCroppedCanvas();
+    const scaleCanvas = scale(this.props.crop, canvas);
+    // let scaleCanvas = this.cropRef.getCroppedCanvas(this.props.crop)
+
+    scaleCanvas.toBlob((blob) => {
+      const file = new File([blob], fileName);
+      onConfirm(file);
+    });
   }
 
   render() {
-    const { show, onConfirm, onCancel } = this.props;
+    const { show, onCancel } = this.props;
+    const { image } = this.state;
     return (
       <Modal
         className="edit-avatar-modal"
@@ -239,16 +477,44 @@ export class EditAvatar extends Component {
         width={630}
         title="调整头像"
         confirmText="保存头像"
-        onConfirm={onConfirm}
+        onConfirm={() => {
+          this.select();
+        }}
         onCancel={onCancel}
       >
         <div className="edit-avatar-modal-wrapper">
           <div className="edit-avatar-o">
-            <Assets name="qrcode" />
+            <Cropper
+              ref={(ref) => { this.cropRef = ref; }}
+              src={image}
+              ready={() => {
+                this.computerZoom();
+              }}
+              style={{ height: 360, width: 360 }}
+              // Cropper.js options
+              aspectRatio={1}
+              viewMode={0}
+              // autoCropArea={0.8}
+              preview=".img-preview"
+              // dragMode='move'
+              guides={false}
+              movable={false}
+              rotatable={false}
+              scalable={false}
+              // zoom={(value) => {
+              //   console.log('zoom', value);
+              //   const zoomValue = value * this.defaultZoomValue / 50;
+              //   this.cropRef.zoomTo(zoomValue);
+              // }}
+              cropmove={() => {
+                this.crop();
+              }}
+              toggleDragModeOnDblclick={false}
+              cropBoxResizable />
           </div>
           <div className="edit-avatar-r">
             <div className="text">头像预览</div>
-            <Assets name="qrcode" />
+            <div className="img-preview" style={{ width: 100, height: 100, overflow: 'hidden' }} />
           </div>
         </div>
       </Modal>
@@ -263,12 +529,12 @@ export class InviteModal extends Component {
   }
 
   render() {
-    const { show, onClose } = this.props;
+    const { show, onClose, data } = this.props;
     return (
       <Modal className="invite-modal" show={show} width={630} title="邀请好友" onClose={onClose}>
         <div className="invite-modal-wrapper">
           <div className="tip">每邀请一位小伙伴加入“千行GMAT”, 您的VIP权限会延长7天,可累加 无上限!赶紧行动吧~</div>
-          <Invite />
+          <Invite data={data} />
         </div>
       </Modal>
     );

+ 1 - 0
front/project/www/components/OtherModal/index.less

@@ -120,6 +120,7 @@
       margin-bottom: 20px;
     }
 
+    .img-preview,
     .assets {
       border-radius: 50%;
       width: 100px;

+ 37 - 16
front/project/www/components/VipRenew/index.js

@@ -1,16 +1,36 @@
 import React, { Component } from 'react';
 import './index.less';
 import Assets from '@src/components/Assets';
+import { formatMoney } from '@src/services/Tools';
 import Modal from '../Modal';
 import Tabs from '../Tabs';
 import { SpecialRadioGroup } from '../Radio';
 import Invite from '../Invite';
 import Button from '../Button';
+import { Main } from '../../stores/main';
+import { Order } from '../../stores/order';
+import { ServiceParamMap } from '../../../Constant';
 
 export default class extends Component {
   constructor(props) {
     super(props);
-    this.state = { tab: '2', pay: 'alipay', select: '1', auth: true };
+    this.state = { tab: '2', pay: '', select: null, auth: true };
+    Main.getService('vip')
+      .then(result => {
+        result.package = result.package.map((row, index) => {
+          row.label = `${row.title}: ¥${formatMoney(row.price)}`;
+          row.value = ServiceParamMap.vip[index].value;
+          return row;
+        });
+        this.setState({ service: result });
+      });
+  }
+
+  select(key) {
+    Order.speedPay().then(result => {
+      this.setState({ order: result });
+    });
+    this.setState({ select: key });
   }
 
   render() {
@@ -34,23 +54,19 @@ export default class extends Component {
   }
 
   renderTab1() {
-    const { pay, select } = this.state;
+    const { pay, select, service = {}, order } = this.state;
     return (
       <div className="tab-1-layout">
         <div className="select-layout">
           <SpecialRadioGroup
-            list={[
-              { label: '1个月: ¥ 9999', value: '1' },
-              { label: '3个月: ¥ 9999', value: '2' },
-              { label: '3个月: ¥ 9999', value: '3' },
-            ]}
+            list={service.package || []}
             value={select}
             width={150}
             space={10}
-            onChange={key => this.setState({ select: key })}
+            onChange={key => this.select(key)}
           />
         </div>
-        <div className="pay-layout">
+        {order && <div className="pay-layout">
           <Tabs
             border
             size="small"
@@ -64,25 +80,27 @@ export default class extends Component {
             <Assets name="qrcode" />
           </div>
           <div className="t">请使用手机微信或支付宝扫码付款</div>
-          <div className="t">支付金额: ¥ 8888.88</div>
-        </div>
+          <div className="t">支付金额: ¥ {order.money}</div>
+        </div>}
       </div>
     );
   }
 
   renderTab2() {
-    const { auth } = this.state;
+    const { data, onReal, onPrepare } = this.props;
     return (
       <div className="tab-2-layout">
         <div className="list">
           <div className="item">
-            {auth && <span className="over">已完成</span>}
+            {data.bindReal && <span className="over">已完成</span>}
             <Assets className="icon" name="realname2" />
             <div className="t">
               <Assets name="gift" />
               6个月
             </div>
-            <Button size="small" radius disabled={auth}>
+            <Button size="small" radius disabled={data.bindReal} onClick={() => {
+              onReal();
+            }}>
               实名认证
             </Button>
           </div>
@@ -97,17 +115,20 @@ export default class extends Component {
             </Button>
           </div>
           <div className="item">
+            {data.bindPrepare && <span className="over">已完成</span>}
             <Assets className="icon" name="information2" />
             <div className="t">
               <Assets name="gift" />
               1个月
             </div>
-            <Button size="small" radius>
+            <Button size="small" radius disabled={data.bindPrepare} onClick={() => {
+              onPrepare();
+            }}>
               完善信息
             </Button>
           </div>
         </div>
-        {auth && <Invite />}
+        <Invite data={data} />
       </div>
     );
   }

+ 5 - 2
front/project/www/layouts/User/index.js

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
 import './index.less';
 
 function UserLayout(props) {
-  const { menu = [], active, right, center } = props;
+  const { menu = [], active, right, center, ads = [] } = props;
 
   return (
     <div className={`user-layout content ${right ? 'right' : ''}`}>
@@ -12,7 +12,7 @@ function UserLayout(props) {
           {menu.map(item => {
             return (
               <Link to={item.path} className={`item ${active === item.key ? 'active' : ''}`}>
-                {item.title}
+                {item.short}
               </Link>
             );
           })}
@@ -38,6 +38,9 @@ function UserLayout(props) {
           ) : (
             <div className="block-layout">{right}</div>
           )}
+          {ads.length > 0 && ads.map(item => {
+            return <div className="block-layout">{item}</div>;
+          })}
         </div>
       )}
     </div>

+ 7 - 0
front/project/www/local.json

@@ -2,6 +2,7 @@
   "development": {
     "scripts": [
       "masonry.pkgd.min.js",
+      "/qrious.js",
       "/ckeditor/ckeditor.js",
       "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
     ],
@@ -15,11 +16,17 @@
   },
   "test": {
     "scripts": [
+      "masonry.pkgd.min.js",
+      "/qrious.js",
+      "/ckeditor/ckeditor.js",
       "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
     ]
   },
   "production": {
     "scripts": [
+      "masonry.pkgd.min.js",
+      "/qrious.js",
+      "/ckeditor/ckeditor.js",
       "http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
     ]
   }

+ 15 - 9
front/project/www/routes/examination/main/page.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import { formatTreeData, formatPercent, formatDate } from '@src/services/Tools';
 import Panel, { WaitPanel, BuyPanel, SmallPanel, SmallWaitPanel, SmallBuyPanel } from '../../../components/Panel';
 import Tabs from '../../../components/Tabs';
@@ -188,14 +188,6 @@ export default class extends Page {
     });
   }
 
-  restart(item) {
-    asyncConfirm('提示', '是否重置', () => {
-      Question.restart(item.paper.id).then(() => {
-        this.refresh();
-      });
-    });
-  }
-
   examinationList(item) {
     User.needLogin().then(() => {
       linkTo(`/examination/list/${item.id}`);
@@ -208,6 +200,20 @@ export default class extends Page {
     });
   }
 
+  buyTextbook() {
+    User.needLogin()
+      .then(() => {
+
+      });
+  }
+
+  buyQxCat() {
+    User.needLogin()
+      .then(() => {
+
+      });
+  }
+
   renderView() {
     const { tab1, tab2, tabs } = this.state;
     const [subject] = tabs.filter(row => row.key === tab1);

+ 30 - 15
front/project/www/routes/exercise/main/page.js

@@ -210,7 +210,10 @@ export default class extends Page {
                 type="start"
                 tip="Start"
                 onClick={() => {
-                  Question.startLink('sentence', record);
+                  User.needLogin()
+                    .then(() => {
+                      Question.startLink('sentence', record);
+                    });
                 }}
               />
             )}
@@ -220,7 +223,10 @@ export default class extends Page {
                 type="continue"
                 tip="Continue"
                 onClick={() => {
-                  Question.continueLink('sentence', record);
+                  User.needLogin()
+                    .then(() => {
+                      Question.continueLink('sentence', record);
+                    });
                 }}
               />
             )}
@@ -229,7 +235,10 @@ export default class extends Page {
                 type="restart"
                 tip="Restart"
                 onClick={() => {
-                  this.restart(record);
+                  User.needLogin()
+                    .then(() => {
+                      this.restart(record);
+                    });
                 }}
               />
             )}
@@ -244,7 +253,10 @@ export default class extends Page {
         return (
           <div className="table-row p-t-1">
             {record.report && record.report.isFinish && <IconButton type="report" tip="Report" onClick={() => {
-              Question.reportLink(record);
+              User.needLogin()
+                .then(() => {
+                  Question.reportLink(record);
+                });
             }} />}
           </div>
         );
@@ -1094,17 +1106,20 @@ export default class extends Page {
                 data={struct}
                 col={col}
                 onClick={item => {
-                  if (item.type === 'paper') {
-                    if (item.progress === 0) {
-                      Question.startLink('exercise', item);
-                    } else if (item.progress === 100) {
-                      Question.startLink('exercise', item);
-                    } else {
-                      Question.continueLink('exercise', item);
-                    }
-                  } else {
-                    this.exerciseList(item);
-                  }
+                  User.needLogin()
+                    .then(() => {
+                      if (item.type === 'paper') {
+                        if (item.progress === 0) {
+                          Question.startLink('exercise', item);
+                        } else if (item.progress === 100) {
+                          Question.startLink('exercise', item);
+                        } else {
+                          Question.continueLink('exercise', item);
+                        }
+                      } else {
+                        this.exerciseList(item);
+                      }
+                    });
                 }}
               />
             );

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/answer',
   key: 'my-answer',
-  title: '问答',
+  title: '个人中心-问答',
+  short: '问答',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/collect',
   key: 'my-collect',
-  title: '收藏',
+  title: '个人中心-收藏',
+  short: '收藏',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/course',
   key: 'my-course',
-  title: '课程',
+  title: '个人中心-课程',
+  short: '课程',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/data',
   key: 'my-data',
-  title: '数据',
+  title: '个人中心-数据',
+  short: '数据',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/error',
   key: 'my-error',
-  title: '错题',
+  title: '个人中心-错题',
+  short: '错题',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/main',
   key: 'my-main',
-  title: '我的',
+  title: '个人中心-我的',
+  short: '我的',
   needLogin: true,
   component() {
     return import('./page');

+ 5 - 0
front/project/www/routes/my/main/index.less

@@ -45,6 +45,11 @@
       .right {
         cursor: pointer;
         float: right;
+        position: relative;
+
+        .cal {
+          position: absolute;
+        }
       }
     }
 

+ 155 - 53
front/project/www/routes/my/main/page.js

@@ -4,7 +4,10 @@ import './index.less';
 import { Tooltip, Icon } from 'antd';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { formatPercent, formatDate, formatSeconds } from '@src/services/Tools';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import UserLayout from '../../../layouts/User';
+import Tabs from '../../../components/Tabs';
 import Button from '../../../components/Button';
 import { Icon as GIcon } from '../../../components/Icon';
 import UserTable from '../../../components/UserTable';
@@ -12,6 +15,8 @@ import Examination from '../../../components/Examination';
 import menu from '../index';
 import { BindPhone, BindEmail, EditInfo, RealAuth, EditAvatar, InviteModal } from '../../../components/OtherModal';
 import VipRenew from '../../../components/VipRenew';
+import { My } from '../../../stores/my';
+import { Main } from '../../../stores/main';
 
 class LogItem extends Component {
   constructor(props) {
@@ -111,51 +116,98 @@ export default class extends Page {
     };
   }
 
+  initData() {
+    // 获取学习数据
+    My.getStudyTotal().then(total => {
+      this.setState({ total });
+    });
+    My.getStudyWeek(0).then(latest => {
+      const diff = latest.time - latest.avgTime;
+      const diffPercent = diff > 0 ? formatPercent(latest.time - latest.avgTime, latest.avgTime, true) : formatPercent(latest.avgTime - latest.time, latest.avgTime, true);
+      this.setState({ latest, diff, diffPercent });
+      My.getStudyWeek(1).then(last => {
+        const diffLast = latest.time - last.time;
+        const diffLastPercent = diffLast > 0 ? formatPercent(latest.time - last.time, last.time, true) : formatPercent(last.time - latest.time, last.time, true);
+        this.setState({ last, diffLast, diffLastPercent });
+      });
+    });
+    // 获取广告
+    Main.getAd('my-self')
+      .then(result => {
+        this.setState({ ads: result });
+      });
+    // 获取未读消息
+    My.message({ page: 1, size: 2, read: 0 })
+      .then(result => {
+        this.setState({ messages: result.list });
+      });
+  }
+
+  readAllMessage() {
+    My.readAllMessage()
+      .then(() => {
+        asyncSMessage('操作成功');
+      });
+  }
+
+  refreshDay(value) {
+    console.log(value);
+  }
+
   renderView() {
     const { config } = this.props;
     return (
       <UserLayout
         active={config.key}
         menu={menu}
-        center={[this.renderTotal(), this.renderLog(), this.renderTime()]}
+        center={[this.renderTop(), this.renderLog(), this.renderTime()]}
         right={[this.renderInfo(), this.renderMessage()]}
+        ads={(this.state.ads || []).map(row => {
+          return <a href={row.link} target="_blank"><Assets src={row.image} /></a>;
+        })}
       />
     );
   }
 
-  renderTotal() {
-    return <div className="total-layout">1</div>;
+  renderTop() {
+    return null; // <div className="total-layout">1</div>;
   }
 
   renderLog() {
     const { logList = [] } = this.state;
+    const { latest = {}, diff = 0, diffPercent = 0, diffLast = 0, diffLastPercent = 0, day, time } = this.state;
     return (
       <div className="log-layout">
         <div className="header">
           <div className="title">学习记录</div>
           <div className="right">
+            <span dangerouslySetInnerHTML={{ __html: `本周学习时间${formatSeconds(latest.time).replace(/([0-9]+)(m|min|h|hour|s)/g, '<b>$1</b>$2')}` }} />
             <span>
-              本周学习时间<b>23</b>Hour
-            </span>
-            <span>
-              同比上周<b>15</b>% <Assets name="up" />
+              同比上周<b>{diffLastPercent}</b>% <Assets name={diffLast > 0 ? 'up' : 'down'} />
             </span>
             <span>
-              同比全站<b>15</b>% <Assets name="down" />
+              同比全站<b>{diffPercent}</b>% <Assets name={diff > 0 ? 'up' : 'down'} />
             </span>
           </div>
         </div>
         <div className="action">
-          <Button size="small" radius>
-            今天
-          </Button>
-          <Button size="small" radius>
-            昨天
-          </Button>
-          <Button size="small" radius>
-            前天
-          </Button>
-          <Assets className="right" name="calendar" />
+          <Tabs
+            className="d-i-b"
+            type="tag"
+            width={54}
+            space={5}
+            value={day}
+            tabs={[{ title: '今天', key: 'today' }, { title: '昨天', key: 'yesterday' }, { title: '前天', key: 'before' }]}
+            onChange={(value) => {
+              this.refreshDay(value);
+            }}
+          />
+          <div className="right">
+            {day === 'other' && formatDate(time, 'YYYY-MM-DD')}
+            <Assets className="right" name="calendar" onClick={() => {
+              this.setState({ showCal: true });
+            }} />
+          </div>
         </div>
         {logList.map((log, index) => {
           return <LogItem key={index} data={log} />;
@@ -165,7 +217,7 @@ export default class extends Page {
   }
 
   renderTime() {
-    const { timeList = [] } = this.state;
+    const { timeList = [], total = {} } = this.state;
     return (
       <div className="time-layout">
         <div className="header">
@@ -176,7 +228,7 @@ export default class extends Page {
             </Tooltip>
           </div>
           <div className="right">
-            自 2019-05-26 ,您已在千行学习<b>23</b>天,累计 <b>32h</b> 30min
+            自 {total.createTIme && formatDate(total.createTime, 'YYYY-MM-DD')} ,您已在千行学习<b>{total.days}</b>天,累计<span dangerouslySetInnerHTML={{ __html: formatSeconds(total.time).replace(/([0-9]+)(m|min|h|hour|s)/g, '<b>$1</b>$2') }} />
           </div>
         </div>
         <div className="body">
@@ -206,84 +258,134 @@ export default class extends Page {
   }
 
   renderInfo() {
-    const { showExamination } = this.state;
+    const { info = {} } = this.props.user;
+    const { showExamination, showPhone, showEmail, showEdit, showReal, showAvatar, showInvite, showVip } = this.state;
     return (
       <div className="info-layout">
         <div className="body">
           <div className="info">
-            <Assets name="sun_blue" />
+            <Assets name="sun_blue" src={info.avatar} onClick={() => {
+              this.setState({ showEdit: true });
+            }} />
             <div className="detail">
-              <div className="name">怕死的胡萝卜 </div>
-              <div className="id">ID: 2392401 </div>
+              <div className="name" onClick={() => {
+                this.setState({ showEdit: true });
+              }}>{info.nickname} </div>
+              <div className="id">ID: {info.id} </div>
             </div>
           </div>
           <div className="auth">
             <span className="invite">
-              <Button radius size="small">
+              <Button radius size="small" onClick={() => {
+                this.setState({ showInvite: true });
+              }}>
                 邀请
               </Button>
             </span>
-            <Assets name="wechat" />
-            <Assets name="phone_1" />
-            <Assets name="realname" />
-            <Assets name="email" />
-            <Assets name="information" />
+            <Assets name="wechat" active={info.bindWechat} />
+            <Assets name="phone_1" active={info.bindMobile} onClick={() => {
+              this.setState({ showPhone: true });
+            }} />
+            <Assets name="realname" active={info.bindReal} onClick={() => {
+              this.setState({ showReal: true });
+            }} />
+            <Assets name="email" active={!!info.email} onClick={() => {
+              this.setState({ showEmail: true });
+            }} />
+            <Assets name="information" active={info.bindPrepare} onClick={() => {
+              this.setState({ showExamination: true });
+            }} />
           </div>
         </div>
-        <div className="footer">
+        {<div className="footer">
           <Assets className="m-r-5" name="VIP" />
-          <span className="date">2019-10-15到期</span>
-          <Link to="">续费</Link>
-        </div>
+          {info.vip && <span className="date">{formatDate(info.vip, 'YYYY-MM-DD')}到期</span>}
+          <a onClick={() => {
+            this.setState({ showVip: true });
+          }}>续费</a>
+        </div>}
         <Examination
           show={showExamination}
+          data={info}
           onConfirm={() => this.setState({ showExamination: false })}
           onCancel={() => this.setState({ showExamination: false })}
           onClose={() => this.setState({ showExamination: false })}
         />
         <BindPhone
+          show={showPhone}
+          data={info}
           onConfirm={() => this.setState({ showPhone: false })}
           onCancel={() => this.setState({ showPhone: false })}
         />
         <BindEmail
+          show={showEmail}
+          data={info}
           onConfirm={() => this.setState({ showEmail: false })}
           onCancel={() => this.setState({ showEmail: false })}
         />
         <EditInfo
-          onConfirm={() => this.setState({ showEdit: false })}
-          onCancel={() => this.setState({ showEdit: false })}
+          show={showEdit}
+          data={info}
+          image={this.state.avatarFile}
+          onSelectImage={(file) => this.setState({ showEdit: false, showAvatar: true, avatarImage: file })}
+          onConfirm={() => this.setState({ showEdit: false, avatarFile: null })}
+          onCancel={() => this.setState({ showEdit: false, avatarFile: null })}
+        />
+        <RealAuth
+          show={showReal}
+          data={info}
+          onConfirm={() => this.setState({ showReal: false })}
         />
-        <RealAuth onConfirm={() => this.setState({ showReal: false })} />
         <EditAvatar
-          onConfirm={() => this.setState({ showAvatar: false })}
-          onCancel={() => this.setState({ showAvatar: false })}
+          show={showAvatar}
+          data={info}
+          crop={{ width: 200, height: 200 }}
+          image={this.state.avatarImage}
+          onConfirm={(file) => this.setState({ showAvatar: false, showEdit: true, avatarFile: file, avatarImage: null })}
+          onCancel={() => this.setState({ showAvatar: false, showEdit: true, avatarImage: null })}
+        />
+        <InviteModal
+          show={showInvite}
+          data={info}
+          onClose={() => this.setState({ showInvite: false })}
+        />
+        <VipRenew
+          show={showVip}
+          data={info}
+          onReal={() => this.setState({ showVip: false, showReal: true })}
+          onPrepare={() => this.setState({ showVip: false, showExamination: true })}
+          onClose={() => this.setState({ showVip: false })}
         />
-        <InviteModal onClose={() => this.setState({ showInvite: false })} />
-        <VipRenew onClose={() => this.setState({ showVip: false })} />
       </div>
     );
   }
 
   renderMessage() {
-    return (
+    const { messages = [] } = this.state;
+    const number = (messages || []).length;
+    return (number > 0 &&
       <div className="message-layout">
         <div className="header">
-          <Assets name="all" />
+          <Assets name="all" onCancel={() => {
+            this.readAllMessage();
+          }} />
           全部已读
         </div>
         <div className="body">
-          <div className="item">
-            <div className="title dot">老师回答了您的提问</div>
-            <div className="date">2019-05-15 16:21:06</div>
-            <GIcon name="arrow-right-small" onClick={() => {}} />
-          </div>
-          <div className="item">
-            <div className="title dot">老师回答了您的提问</div>
-            <div className="date">2019-05-15 16:21:06</div>
-          </div>
+          {(messages || []).map(row => {
+            return <div className="item">
+              <div className="title dot">老师回答了您的提问</div>
+              <div className="date">{formatDate(row.createTime, 'YYYY-MM-DD HH:mm:ss')}</div>
+              {row.link && <GIcon name="arrow-right-small" onClick={() => {
+                openLink(row.link);
+              }} />}
+            </div>;
+          })}
         </div>
         <div className="footer">
-          <Button radius size="small">
+          <Button radius size="small" onClick={() => {
+            linkTo('/my/message');
+          }}>
             全部消息
           </Button>
         </div>

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/message',
   key: 'my-message',
-  title: '消息',
+  title: '个人中心-消息',
+  short: '消息',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/note',
   key: 'my-note',
-  title: '笔记',
+  title: '个人中心-笔记',
+  short: '笔记',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/order',
   key: 'my-order',
-  title: '订单',
+  title: '个人中心-订单',
+  short: '订单',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/report',
   key: 'my-report',
-  title: '报告',
+  title: '个人中心-报告',
+  short: '报告',
   needLogin: true,
   component() {
     return import('./page');

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

@@ -1,7 +1,8 @@
 export default {
   path: '/my/tools',
   key: 'my-tools',
-  title: '工具',
+  title: '个人中心-工具',
+  short: '工具',
   needLogin: true,
   component() {
     return import('./page');

+ 10 - 2
front/project/www/stores/main.js

@@ -1,6 +1,14 @@
 import BaseStore from '@src/stores/base';
 
 export default class MainStore extends BaseStore {
+  qrCode(url) {
+    const qr = new window.QRious({
+      value: url,
+      size: 200,
+    });
+    return qr.toDataURL();
+  }
+
   /**
    * 获取首页配置
    */
@@ -11,8 +19,8 @@ export default class MainStore extends BaseStore {
   /**
    * 获取广告列表
    */
-  getAd() {
-    return this.apiGet('/base/ad');
+  getAd(channel) {
+    return this.apiGet('/base/ad', { channel });
   }
 
   /**

+ 18 - 1
front/project/www/stores/my.js

@@ -10,6 +10,15 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 绑定手机
+   * @param {*} area
+   * @param {*}
+   */
+  bindMobile(area, mobile) {
+    return this.apiPost('/my/mobile', { area, mobile });
+  }
+
+  /**
    * 修改用户信息
    * @param {*} info  nickname avatar
    */
@@ -18,13 +27,21 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 发送邀请码到邮箱
+   * @param {*} emails
+   */
+  inviteEmail(emails) {
+    return this.apiPost('/my/invite/email', { emails });
+  }
+
+  /**
    * 用户站内信
    * @param {*} page
    * @param {*} size
    * @param {*} type
    * @param {*} read
    */
-  message(page, size, type, read) {
+  message({ page, size, type, read }) {
     return this.apiGet('/my/message', { page, size, type, read });
   }
 

+ 2 - 2
front/src/components/FileUpload/index.js

@@ -88,10 +88,10 @@ class FileUpload extends Component {
           onChange={e => this.onChange(e)}
         />
         {this.state.type === 'image' ? (
-          this.props.value && !this.state.uploading ? (
+          this.props.value && (!this.state.uploading && !this.props.uploading) ? (
             <img src={this.props.value} style={{ width: '100%', height: '100%' }} />
           ) : (
-            <Icon type={this.state.uploading ? 'loading' : 'upload'} />
+            <Icon type={this.state.uploading || this.props.uploading ? 'loading' : 'upload'} />
           )
         ) : (
           ''

ファイルの差分が大きいため隠しています
+ 3739 - 0
front/src/static/qrious.js


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

@@ -87,6 +87,12 @@ public class UserOrder implements Serializable {
     private Date payTime;
 
     /**
+     * 快速支付:不显示在后台
+     */
+    @Column(name = "`is_speed`")
+    private Integer isSpeed;
+
+    /**
      * 交易流水号
      */
     @Column(name = "`transaction_no`")
@@ -335,6 +341,24 @@ public class UserOrder implements Serializable {
     }
 
     /**
+     * 获取快速支付:不显示在后台
+     *
+     * @return is_speed - 快速支付:不显示在后台
+     */
+    public Integer getIsSpeed() {
+        return isSpeed;
+    }
+
+    /**
+     * 设置快速支付:不显示在后台
+     *
+     * @param isSpeed 快速支付:不显示在后台
+     */
+    public void setIsSpeed(Integer isSpeed) {
+        this.isSpeed = isSpeed;
+    }
+
+    /**
      * 获取交易流水号
      *
      * @return transaction_no - 交易流水号
@@ -372,6 +396,7 @@ public class UserOrder implements Serializable {
         sb.append(", payStatus=").append(payStatus);
         sb.append(", createTime=").append(createTime);
         sb.append(", payTime=").append(payTime);
+        sb.append(", isSpeed=").append(isSpeed);
         sb.append(", transactionNo=").append(transactionNo);
         sb.append("]");
         return sb.toString();
@@ -523,6 +548,16 @@ public class UserOrder implements Serializable {
         }
 
         /**
+         * 设置快速支付:不显示在后台
+         *
+         * @param isSpeed 快速支付:不显示在后台
+         */
+        public Builder isSpeed(Integer isSpeed) {
+            obj.setIsSpeed(isSpeed);
+            return this;
+        }
+
+        /**
          * 设置交易流水号
          *
          * @param transactionNo 交易流水号

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

@@ -19,6 +19,7 @@
     <result column="pay_status" jdbcType="INTEGER" property="payStatus" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="pay_time" jdbcType="TIMESTAMP" property="payTime" />
+    <result column="is_speed" jdbcType="INTEGER" property="isSpeed" />
     <result column="transaction_no" jdbcType="VARCHAR" property="transactionNo" />
   </resultMap>
   <sql id="Base_Column_List">
@@ -27,6 +28,6 @@
     -->
     `id`, `user_id`, `product_types`, `pay_method`, `money`, `origin_money`, `invoice_money`, 
     `promote`, `gift`, `ask_time`, `pay_id`, `pay_status`, `create_time`, `pay_time`, 
-    `transaction_no`
+    `is_speed`, `transaction_no`
   </sql>
 </mapper>

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

@@ -61,7 +61,7 @@
   <select id="searchStemFulltext" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />,
-    if(qn.`title` = #{keyword, jdbcType=VARCHAR}, 1, MATCH (q.`stem`) AGAINST (#{keyword, jdbcType=VARCHAR} IN NATURAL LANGUAGE MODE)) as `relation_score`,
+    if(qn.`title` = #{keyword, jdbcType=VARCHAR}, 1, MATCH (q.`description`) AGAINST (#{keyword, jdbcType=VARCHAR} IN NATURAL LANGUAGE MODE)) as `relation_score`,
     <if test="qxCatId != null">
       if(qn.module='examination', find_in_set(#{qxCatId}, qn.`module_struct`), 0) as `select`,
     </if>
@@ -84,7 +84,7 @@
       </foreach>
     </if>
     where
-    (qn.`title` = #{keyword, jdbcType=VARCHAR} or MATCH (q.`stem`) AGAINST (#{keyword, jdbcType=VARCHAR} IN NATURAL LANGUAGE MODE))
+    (qn.`title` = #{keyword, jdbcType=VARCHAR} or MATCH (q.`description`) AGAINST (#{keyword, jdbcType=VARCHAR} IN NATURAL LANGUAGE MODE))
     and q.id &gt; 0
     <if test="qxCatId != null">
       and `select` = 0

+ 7 - 7
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCourseRecordRelationMapper.xml

@@ -11,21 +11,21 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="user_time" jdbcType="INTEGER" property="user_time" />
+    <id column="user_time" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <resultMap id="studyModuleMap" type="com.qxgmat.data.relation.entity.UserModuleRecordStatRelation">
     <!--
       WARNING - @mbg.generated
     -->
     <id column="module" jdbcType="VARCHAR" property="module" />
-    <id column="user_time" jdbcType="INTEGER" property="user_time" />
+    <id column="user_time" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <resultMap id="rankMap" type="com.qxgmat.data.relation.entity.UserRankStatRelation">
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="rank" jdbcType="INTEGER" property="user_number" />
-    <id column="total" jdbcType="INTEGER" property="user_time" />
+    <id column="rank" jdbcType="INTEGER" property="userNumber" />
+    <id column="total" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <sql id="Id_Column_List">
     <!--
@@ -73,7 +73,7 @@
   -->
   <select id="statAvg" resultMap="studyMap">
     select
-    sum(ucr.`user_time`) / count(distance(ucr.`user_id`)) as `user_time`
+    sum(ucr.`user_time`) / count(distinct(ucr.`user_id`)) as `user_time`
     from `user_course_record` ucr
     where
     1
@@ -88,7 +88,7 @@
   <!--
     用户听课记录统计,分题型
   -->
-  <select id="statGroupType" resultMap="studyMap">
+  <select id="statGroupType" resultMap="studyModuleMap">
     select
     sum(ucr.`user_time`) as `user_time`, cc.`extend` as `module`
     from `user_course_record` ucr
@@ -101,7 +101,7 @@
     <if test="endTime != null">
       and ucr.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
-    group cc.`extend`
+    group by cc.`extend`
   </select>
 
   <!--

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRelationMapper.xml

@@ -19,7 +19,7 @@
     select
     <include refid="Id_Column_List" />
     from `user_order` uo
-    where 1
+    where uo.is_speed = 0
     <if test="userId != null">
       and uo.`user_id` = #{userId,jdbcType=VARCHAR}
     </if>

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

@@ -11,7 +11,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="user_time" jdbcType="INTEGER" property="user_time" />
+    <id column="user_time" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <sql id="Id_Column_List">
     <!--
@@ -190,7 +190,7 @@
   -->
   <select id="statAvg" resultMap="studyMap">
     select
-    sum(uq.`user_time`) /count(distance(uq.`user_id`)) as `user_time`
+    sum(uq.`user_time`) /count(distinct(uq.`user_id`)) as `user_time`
     from `user_question` uq
     where
     1

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

@@ -11,24 +11,24 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="user_number" jdbcType="INTEGER" property="user_number" />
-    <id column="user_correct" jdbcType="INTEGER" property="user_correct" />
+    <id column="user_number" jdbcType="INTEGER" property="userNumber" />
+    <id column="user_correct" jdbcType="INTEGER" property="userCorrect" />
   </resultMap>
   <resultMap id="studyMap" type="com.qxgmat.data.relation.entity.UserStudyStatRelation">
     <!--
       WARNING - @mbg.generated
     -->
     <id column="module" jdbcType="VARCHAR" property="module" />
-    <id column="user_number" jdbcType="INTEGER" property="user_number" />
-    <id column="user_time" jdbcType="INTEGER" property="user_time" />
-    <id column="user_correct" jdbcType="INTEGER" property="user_correct" />
+    <id column="user_number" jdbcType="INTEGER" property="userNumber" />
+    <id column="user_time" jdbcType="INTEGER" property="userTime" />
+    <id column="user_correct" jdbcType="INTEGER" property="userCorrect" />
   </resultMap>
   <resultMap id="rankMap" type="com.qxgmat.data.relation.entity.UserRankStatRelation">
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="rank" jdbcType="INTEGER" property="user_number" />
-    <id column="total" jdbcType="INTEGER" property="user_time" />
+    <id column="rank" jdbcType="INTEGER" property="userNumber" />
+    <id column="total" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <sql id="Id_Column_List">
     <!--
@@ -146,7 +146,7 @@
     from `user_report`
     where
     `user_id` = #{userId,jdbcType=VARCHAR}
-    group `paper_origin`
+    group by `paper_origin`
   </select>
 
   <!--
@@ -156,7 +156,7 @@
     select
     sum(ur.`user_number`) as `user_number`, sum(ur.`user_time`) as `user_time`, sum(ur.`user_correct`) as `user_correct`, ep.`question_type` as `module`
     from `user_report` ur
-      left join `exercise_paper` ep on ep.`id` = ur.`module_id`
+      left join `exercise_paper` ep on ep.`id` = ur.`origin_id`
     where
     ur.`user_id` = #{userId,jdbcType=VARCHAR}
     and ur.`paper_origin` = 'exercise'
@@ -167,7 +167,7 @@
     <if test="endTime != null">
       and ur.`update_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
-    group ep.`question_type`
+    group by ep.`question_type`
   </select>
 
   <!--

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

@@ -11,7 +11,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <id column="user_time" jdbcType="INTEGER" property="user_time" />
+    <id column="user_time" jdbcType="INTEGER" property="userTime" />
   </resultMap>
   <sql id="Id_Column_List">
     <!--
@@ -42,7 +42,7 @@
   -->
   <select id="statAvg" resultMap="studyMap">
     select
-    sum(usr.`user_time`) / count(distance(usr.`user_id`)) as `user_time`
+    sum(usr.`user_time`) / count(distinct(usr.`user_id`)) as `user_time`
     from `user_sentence_record` usr
     where
     1

+ 1 - 1
server/data/src/main/resources/application-data.yml

@@ -3,7 +3,7 @@ mybatis:
   # type-aliases扫描路径
   type-aliases-package: tk.mybatis.springboot.model
   # mapper xml实现扫描路径
-  mapper-locations: classpath:com/qxgmat/data/dao/**/*.xml,classpath:com/qxgmat/data/relation/**/*.xml
+  mapper-locations: classpath:com/qxgmat/data/dao/mapping/*.xml,classpath:com/qxgmat/data/relation/mapping/*.xml
   property:
     order: BEFORE
 

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

@@ -509,8 +509,8 @@ CREATE TABLE question_no (
   relation_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联题目数量',
   PRIMARY KEY (id),
   KEY question_id (question_id),
-  KEY title (title),
-  KEY module (module)
+  KEY module (module),
+  FULLTEXT KEY title (title)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='题目-编号';
 
 CREATE TABLE rank (
@@ -1177,6 +1177,7 @@ CREATE TABLE user_order (
   pay_status tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '支付状态',
   create_time datetime DEFAULT NULL,
   pay_time datetime DEFAULT NULL,
+  is_speed tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '快速支付,不显示在后台',
   transaction_no varchar(255) DEFAULT NULL COMMENT '交易流水号',
   PRIMARY KEY (id),
   KEY user_id (user_id,pay_status),

+ 1 - 1
server/gateway-api/build.gradle

@@ -23,7 +23,7 @@ dependencies {
     compileClasspath 'com.github.penggle:kaptcha:2.3.2'
 
     // https://mvnrepository.com/artifact/org.apache.poi/poi
-    compile group: 'org.apache.poi', name: 'poi', version: '3.17'
+    compileClasspath group: 'org.apache.poi', name: 'poi', version: '3.17'
 
 
 //    compile group: 'commons-lang', name: 'commons-lang', version:'2.6'

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

@@ -126,9 +126,9 @@ public class CommonController {
 //        return MessageHelp.success(uploadInfoDto);
 //    }
 
-    @RequestMapping(value = "/upload/image", produces = MediaType.IMAGE_JPEG_VALUE, method = RequestMethod.POST)
+    @RequestMapping(value = "/upload/image", method = RequestMethod.POST)
     @ApiOperation(value = "上传文件", notes = "上传至本地服务器", httpMethod = "POST")
-    public Response<String> uploadImage(@RequestParam("file") MultipartFile multipartFile)  {
+    public Response<JSONObject> uploadImage(@RequestParam("file") MultipartFile multipartFile)  {
         if (multipartFile.isEmpty()) {
             throw new ParameterException("上传文件为空");
         }
@@ -145,7 +145,10 @@ public class CommonController {
             File dest = new File(dir.getAbsolutePath() + File.separator+file);
 
             multipartFile.transferTo(dest);
-            return ResponseHelp.success(webUrl+file);
+            String url = webUrl+file;
+            JSONObject imageInfo = new JSONObject();
+            imageInfo.put("url", url);
+            return ResponseHelp.success(imageInfo);
         } catch (IOException e) {
             e.printStackTrace();
             return ResponseHelp.exception(new SystemException("图片上传失败"));

+ 21 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -222,6 +222,26 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/mobile", method = RequestMethod.POST)
+    @ApiOperation(value = "绑定手机", httpMethod = "POST")
+    public Response<Boolean> mobile(@RequestBody @Validated UserMobileDto dto, HttpSession session, HttpServletRequest request) {
+        User user = (User) shiroHelp.getLoginUser();
+        User in = usersService.get(user.getId());
+        if (in.getArea().equals(dto.getArea()) && in.getMobile().equals(dto.getMobile())) {
+            return ResponseHelp.success(true);
+        }
+        User other = usersService.getByMobile(dto.getArea(), dto.getMobile());
+        if (other != null){
+            throw new ParameterException("该手机已绑定其他账号,请更换手机号码");
+        }
+        usersService.edit(User.builder()
+                .id(user.getId())
+                .area(dto.getArea())
+                .mobile(dto.getMobile())
+                .build());
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/info", method = RequestMethod.POST)
     @ApiOperation(value = "修改用户信息", httpMethod = "POST")
     public Response<Boolean> info(@RequestBody @Validated UserInfoDto dto){
@@ -409,6 +429,7 @@ public class MyController {
 
         Setting setting = settingService.getByKey(SettingKey.PREPARE_INFO);
         JSONObject value = setting.getValue();
+        dto.setStat(value);
         return ResponseHelp.success(dto);
     }
 

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

@@ -122,7 +122,7 @@ public class OrderController {
     public Response<UserOrderPreDto> speedPay(@RequestBody @Validated RecordAddDto dto, HttpServletRequest request) throws Exception {
         User user = (User) shiroHelp.getLoginUser();
         UserOrderCheckout checkout = Transform.dtoToEntity(dto);
-        UserOrder order = orderFlowService.makeOrderWithProduct(user.getId(), checkout);
+        UserOrder order = orderFlowService.makeOrderWithSpeed(user.getId(), checkout);
 
         return ResponseHelp.success(detail(user.getId(), order, null));
     }

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

@@ -2,10 +2,6 @@ package com.qxgmat.dto.admin.request;
 
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.Question;
-import com.qxgmat.dto.admin.extend.QuestionNoExtendDto;
-import org.json.JSONObject;
-
-import javax.validation.constraints.NotEmpty;
 
 @Dto(entity = Question.class)
 public class QuestionDto {

+ 36 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserMobileDto.java

@@ -0,0 +1,36 @@
+package com.qxgmat.dto.request;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * Created by GaoJie on 2017/11/3.
+ */
+public class UserMobileDto {
+
+    private String area;
+    /**
+     * 手机号
+     */
+    @NotBlank(message = "手机号不能为空!")
+    @ApiModelProperty(value = "手机号", required = true)
+    private String mobile;
+
+
+    public String getMobile() {
+        return mobile;
+    }
+
+    public void setMobile(String mobile) {
+        this.mobile = mobile;
+    }
+
+    public String getArea() {
+        return area;
+    }
+
+    public void setArea(String area) {
+        this.area = area;
+    }
+}

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

@@ -13,6 +13,10 @@ public class MyDto extends UserDto {
 
     private String email;
 
+    private String area;
+
+    private String mobile;
+
     private String inviteCode;
 
     private Boolean bindWechat;
@@ -118,4 +122,20 @@ public class MyDto extends UserDto {
     public void setVip(Date vip) {
         this.vip = vip;
     }
+
+    public String getArea() {
+        return area;
+    }
+
+    public void setArea(String area) {
+        this.area = area;
+    }
+
+    public String getMobile() {
+        return mobile;
+    }
+
+    public void setMobile(String mobile) {
+        this.mobile = mobile;
+    }
 }

+ 7 - 3
server/gateway-api/src/main/java/com/qxgmat/help/AiHelp.java

@@ -32,11 +32,15 @@ public class AiHelp {
         // City类可用于IPDB格式的IPv4免费库,IPv4与IPv6的每周高级版、每日标准版、每日高级版、每日专业版、每日旗舰版
         if (path.startsWith(".")){
             // 相对路径,从resource中获取
-            logger.debug("{}, {}", path, this.getClass().getClassLoader().getResource(path));
+//            logger.debug("{}, {}", path, this.getClass().getClassLoader().getResource(path.replaceFirst(".","")));
             try {
-                cityDb = new City(this.getClass().getClassLoader().getResourceAsStream(path));
+                cityDb = new City(this.getClass().getClassLoader().getResourceAsStream(path.replaceFirst(".","")));
             }catch (Exception e){
-                logger.error(e.getLocalizedMessage());
+                try{
+                    cityDb = new City(this.getClass().getClassLoader().getResourceAsStream(path));
+                }catch (Exception ee){
+                    logger.error(ee.getLocalizedMessage());
+                }
             }
         }else{
             cityDb = new City(path);

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

@@ -322,7 +322,7 @@ public class MessageExtendService {
 
     private String replaceBody(String body, Map<String, String> params){
         for(String key :params.keySet()){
-            body = body.replaceAll("{"+key+"}", params.get(key));
+            body = body.replaceAll("\\{"+key+"}", params.getOrDefault(key, ""));
         }
         return body;
     }

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -940,9 +940,10 @@ public class OrderFlowService {
      * @return
      */
     @Transactional
-    public UserOrder makeOrderWithProduct(Integer userId, UserOrderCheckout checkout){
+    public UserOrder makeOrderWithSpeed(Integer userId, UserOrderCheckout checkout){
         List<UserOrderCheckout> list = new ArrayList<UserOrderCheckout>(){{add(checkout);}};
         UserOrder order = preOrderWithCheckout(userId, list);
+        order.setIsSpeed(1);
         order = userOrderService.add(order);
         checkout.setOrderId(order.getId());
         userOrderCheckoutService.add(checkout);

+ 0 - 2
server/gateway-api/src/main/java/com/qxgmat/util/shiro/ManagerRealm.java

@@ -4,7 +4,6 @@ import com.qxgmat.data.dao.entity.Manager;
 import com.qxgmat.data.dao.entity.ManagerRole;
 import com.qxgmat.service.inline.ManagerRoleService;
 import com.qxgmat.service.ManagerService;
-import org.apache.commons.lang.time.FastDateFormat;
 import org.apache.shiro.authc.*;
 import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.authz.SimpleAuthorizationInfo;
@@ -23,7 +22,6 @@ import java.util.List;
  * Created by GaoJie on 2017/11/3.
  */
 public class ManagerRealm extends AuthorizingRealm {
-    private static final FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
     private static final Logger logger = LoggerFactory.getLogger(ManagerRealm.class);
 
     @Autowired

+ 0 - 2
server/gateway-api/src/main/java/com/qxgmat/util/shiro/OauthRealm.java

@@ -2,7 +2,6 @@ package com.qxgmat.util.shiro;
 
 import com.qxgmat.data.dao.entity.User;
 import com.qxgmat.service.UsersService;
-import org.apache.commons.lang.time.FastDateFormat;
 import org.apache.shiro.authc.*;
 import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.realm.AuthorizingRealm;
@@ -14,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
  */
 
 public class OauthRealm extends AuthorizingRealm {
-    private static final FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
 
     @Autowired
     private UsersService usersService;

+ 0 - 5
server/gateway-api/src/main/java/com/qxgmat/util/shiro/TokenRealm.java

@@ -1,14 +1,10 @@
 package com.qxgmat.util.shiro;
 
-import com.nuliji.tools.shiro.inter.RealmAuthenticationToken;
 import com.qxgmat.data.dao.entity.User;
 import com.qxgmat.service.UsersService;
-import org.apache.commons.lang.time.FastDateFormat;
 import org.apache.shiro.authc.*;
-import org.apache.shiro.authc.credential.CredentialsMatcher;
 import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.authz.SimpleAuthorizationInfo;
-import org.apache.shiro.cache.CacheManager;
 import org.apache.shiro.realm.AuthorizingRealm;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.slf4j.Logger;
@@ -22,7 +18,6 @@ import java.util.Objects;
  * Created by GaoJie on 2017/11/3.
  */
 public class TokenRealm extends AuthorizingRealm {
-    private static final FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
     private static final Logger logger = LoggerFactory.getLogger(TokenRealm.class);
 
     @Autowired

+ 2 - 5
server/gateway-api/src/main/java/com/qxgmat/util/shiro/UserRealm.java

@@ -2,8 +2,6 @@ package com.qxgmat.util.shiro;
 
 import com.qxgmat.data.dao.entity.User;
 import com.qxgmat.service.UsersService;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.time.FastDateFormat;
 import org.apache.shiro.authc.*;
 import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.authz.SimpleAuthorizationInfo;
@@ -19,7 +17,6 @@ import java.util.ArrayList;
  * Created by GaoJie on 2017/11/3.
  */
 public class UserRealm extends AuthorizingRealm {
-    private static final FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
     private static final Logger logger = LoggerFactory.getLogger(UserRealm.class);
 
     @Autowired
@@ -49,10 +46,10 @@ public class UserRealm extends AuthorizingRealm {
         if (info.length<2){
             throw new UnknownAccountException("手机格式错误!");
         }
-        if (StringUtils.isBlank(info[0])) {
+        if (info[0]==null || info[0].isEmpty()) {
             throw new UnknownAccountException("国际码不能为空!");
         }
-        if (StringUtils.isBlank(info[1])) {
+        if (info[1]==null || info[1].isEmpty()) {
             throw new UnknownAccountException("手机不能为空!");
         }
         User user = usersService.getByMobile(info[0], info[1]);

+ 1 - 1
server/gateway-api/src/main/profile/prod/application-runtime.yml

@@ -81,7 +81,7 @@ spring:
         # 连接池最大阻塞等待时间(使用负值表示没有限制)
         max-wait: 10000
     # 连接超时时间(毫秒)
-    timeout: 0
+    timeout: 5000
 
 upload:
   local_path: ../upload/

+ 1 - 1
server/gateway-api/src/main/profile/test/application-runtime.yml

@@ -81,7 +81,7 @@ spring:
         # 连接池最大阻塞等待时间(使用负值表示没有限制)
         max-wait: 10000
     # 连接超时时间(毫秒)
-    timeout: 0
+    timeout: 5000
 
 upload:
   local_path: ../upload/

+ 11 - 6
server/tools/build.gradle

@@ -37,12 +37,17 @@ dependencies {
 //    compileClasspath group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.2.0'
 //    parentClasspath group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.2.0'
 
-    compile group: 'com.aliyun', name: 'aliyun-java-sdk-core', version:'3.3.1'
-    compile group: 'com.aliyun', name: 'aliyun-java-sdk-dysmsapi', version: '1.0.0'
-    compile group: 'com.aliyun.oss', name: 'aliyun-sdk-oss', version:'2.5.0'
-
-    implementation 'com.alipay.sdk:alipay-sdk-java:3.7.110.ALL'
-    compile 'com.thoughtworks.xstream:xstream:1.4.7'
+    compileClasspath group: 'com.aliyun', name: 'aliyun-java-sdk-core', version:'3.3.1'
+    parentClasspath group: 'com.aliyun', name: 'aliyun-java-sdk-core', version:'3.3.1'
+    compileClasspath group: 'com.aliyun', name: 'aliyun-java-sdk-dysmsapi', version: '1.0.0'
+    parentClasspath group: 'com.aliyun', name: 'aliyun-java-sdk-dysmsapi', version: '1.0.0'
+    compileClasspath group: 'com.aliyun.oss', name: 'aliyun-sdk-oss', version:'2.5.0'
+    parentClasspath group: 'com.aliyun.oss', name: 'aliyun-sdk-oss', version:'2.5.0'
+
+    compileClasspath 'com.alipay.sdk:alipay-sdk-java:3.7.110.ALL'
+    parentClasspath 'com.alipay.sdk:alipay-sdk-java:3.7.110.ALL'
+    compileClasspath 'com.thoughtworks.xstream:xstream:1.4.7'
+    parentClasspath 'com.thoughtworks.xstream:xstream:1.4.7'
 }
 
 apply plugin: 'java-library'