Browse Source

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

# Conflicts:
#	front/project/www/routes/room/main/page.js
KaysonCui 5 years ago
parent
commit
c58abb792a
100 changed files with 3217 additions and 1042 deletions
  1. 24 10
      front/project/Constant.js
  2. 1 1
      front/project/admin/routes/course/package/page.js
  3. 32 1
      front/project/admin/routes/course/vsDetail/page.js
  4. 180 6
      front/project/admin/routes/ready/article/page.js
  5. 77 51
      front/project/admin/routes/setting/index/page.js
  6. 0 11
      front/project/admin/routes/setting/service/page.js
  7. 1 1
      front/project/admin/routes/student/askCourseDetail/page.js
  8. 2 6
      front/project/admin/routes/user/orderDetail/page.js
  9. 8 0
      front/project/admin/stores/ready.js
  10. 8 0
      front/project/admin/stores/system.js
  11. 1 1
      front/project/h5/index.js
  12. 1 1
      front/project/h5/routes/page/invitation/page.js
  13. 1 0
      front/project/h5/routes/page/pay/index.less
  14. 140 44
      front/project/h5/routes/page/pay/page.js
  15. 11 9
      front/project/h5/stores/common.js
  16. 7 0
      front/project/h5/stores/main.js
  17. 4 0
      front/project/h5/stores/my.js
  18. 4 4
      front/project/h5/stores/order.js
  19. 12 2
      front/project/h5/stores/user.js
  20. 8 6
      front/project/www/app.js
  21. 8 0
      front/project/www/app.less
  22. 3 3
      front/project/www/components/Card/index.js
  23. 44 38
      front/project/www/components/Examination/index.js
  24. 2 0
      front/project/www/components/ListTable/index.less
  25. 12 6
      front/project/www/components/Login/index.js
  26. 4 0
      front/project/www/components/Module/index.less
  27. 281 42
      front/project/www/components/OtherModal/index.js
  28. 340 58
      front/project/www/components/PayModal/index.js
  29. 121 44
      front/project/www/components/VipRenew/index.js
  30. 23 2
      front/project/www/components/VipRenew/index.less
  31. 2 0
      front/project/www/layouts/User/index.js
  32. 7 6
      front/project/www/routes/examination/list/page.js
  33. 23 8
      front/project/www/routes/examination/main/page.js
  34. 7 3
      front/project/www/routes/exercise/main/page.js
  35. 2 2
      front/project/www/routes/my/answer/page.js
  36. 48 82
      front/project/www/routes/my/course/page.js
  37. 27 5
      front/project/www/routes/my/main/page.js
  38. 6 21
      front/project/www/routes/my/order/page.js
  39. 50 160
      front/project/www/routes/my/tools/page.js
  40. 1 2
      front/project/www/routes/page/cart/index.js
  41. 228 90
      front/project/www/routes/page/cart/page.js
  42. 9 0
      front/project/www/routes/page/order/index.js
  43. 113 55
      front/project/www/routes/page/order/page.js
  44. 10 2
      front/project/www/routes/textbook/list/page.js
  45. 4 0
      front/project/www/stores/course.js
  46. 11 0
      front/project/www/stores/main.js
  47. 29 4
      front/project/www/stores/my.js
  48. 25 10
      front/project/www/stores/order.js
  49. 88 3
      front/project/www/stores/user.js
  50. 2 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageCategory.java
  51. 1 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageMethod.java
  52. 1 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/SettingKey.java
  53. 96 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticleCategory.java
  54. 51 16
      server/data/src/main/java/com/qxgmat/data/dao/entity/User.java
  55. 39 4
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserAskCourse.java
  56. 52 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserExport.java
  57. 42 7
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserMessage.java
  58. 4 5
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrder.java
  59. 4 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/ReadyArticleCategoryMapper.xml
  60. 3 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAskCourseMapper.xml
  61. 14 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserExportMapper.xml
  62. 4 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMapper.xml
  63. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMessageMapper.xml
  64. 1 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserOrderMapper.xml
  65. 20 8
      server/data/src/main/resources/db/migration/V1__init_table.sql
  66. 1 1
      server/data/src/main/resources/mybatis-generator.xml
  67. 2 1
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java
  68. 22 4
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/ReadyController.java
  69. 17 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/SettingController.java
  70. 55 18
      server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java
  71. 14 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java
  72. 21 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  73. 70 23
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  74. 95 41
      server/gateway-api/src/main/java/com/qxgmat/controller/api/OrderController.java
  75. 68 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ReadyArticleCategoryDto.java
  76. 8 8
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ReadyArticleDto.java
  77. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CoursePackageExtendDto.java
  78. 63 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/UserOrderRecordExtendDto.java
  79. 13 3
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskCourseDto.java
  80. 13 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCourseNoIdsDto.java
  81. 0 10
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserExportDto.java
  82. 1 1
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserQuestionIdsDto.java
  83. 39 7
      server/gateway-api/src/main/java/com/qxgmat/dto/response/MyDto.java
  84. 3 33
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderDetailDto.java
  85. 9 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderRecordListDto.java
  86. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserStudyDayDto.java
  87. 47 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserNoteCourseService.java
  88. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/UserNoteQuestionService.java
  89. 14 14
      server/gateway-api/src/main/java/com/qxgmat/service/UsersService.java
  90. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/annotation/ChangeCheckout.java
  91. 19 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java
  92. 59 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExportService.java
  93. 19 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java
  94. 49 14
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  95. 18 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java
  96. 14 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ToolsService.java
  97. 7 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java
  98. 38 2
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyArticleCategoryService.java
  99. 10 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyArticleService.java
  100. 0 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserAskCourseService.java

+ 24 - 10
front/project/Constant.js

@@ -20,10 +20,10 @@ export const MoneyRange = [{ label: '0', value: 0 }, { label: '1-1000', value: 1
 
 export const AskTarget = [{ label: '题目', value: 'question', title: '题目', key: 'question' }, { label: '官方解析', value: 'official', title: '官方解析', key: 'official' }, { label: '千行解析', value: 'qx', title: '千行解析', key: 'qx' }, { label: '题源联想', value: 'association', title: '题源联想', key: 'association' }, { label: '相关问答', value: 'qa', title: '相关问答', key: 'qa' }];
 
-export const ServiceKey = [{ label: 'VIP', value: 'vip' }, { label: '机经', value: 'textbook' }, { label: '千行CAT', value: 'qx_cat' }];
+export const ServiceKey = [{ label: 'VIP', value: 'vip' }, { label: '机经', value: 'textbook', expireDays: 180, useExpireDays: 30 }, { label: '千行CAT', value: 'qx_cat', expireDays: 180, useExpireDays: 180 }];
 
 export const ServiceParamMap = {
-  vip: [{ label: '1个月', value: 'month1' }, { label: '3个月', value: 'month3' }, { label: '6个月', value: 'month6' }],
+  vip: [{ label: '30天', value: 'month1', expireDays: 0, useExpireDays: 30 }, { label: '90天', value: 'month3', expireDays: 0, useExpireDays: 90 }, { label: '180天', value: 'month6', expireDays: 0, useExpireDays: 180 }],
 };
 
 export const AskModule = [{ label: '练习', value: 'exercise' }, { label: '模考', value: 'examination' }];
@@ -94,7 +94,7 @@ export const CourseModule = [{ label: '视频课程', value: 'video' }, { label:
 
 export const CourseModuleShow = [{ label: '在线课程', value: 'online', courseModules: ['video', 'online'] }, { label: '1V1私教', value: 'vs', courseModules: ['vs'] }];
 
-export const CourseVsType = [{ label: '新手辅导', value: 'novice' }, { label: '诊断辅导', value: 'coach' }, { label: '系统授课', value: 'system' }, { label: '答疑课', value: 'answer' }];
+export const CourseVsType = [{ label: '新手辅导', value: 'novice', tips: '适合未参加过实战的考生' }, { label: '诊断辅导', value: 'coach', tips: '适合参加过实战的考生' }, { label: '系统授课', value: 'system' }, { label: '答疑课', value: 'answer' }];
 
 export const CourseVideoType = [{ label: '基础刷题', value: 'base' }, { label: '系统授课', value: 'system' }, { label: '思维提升', value: 'thinking' }];
 
@@ -165,7 +165,7 @@ export const FaqChannel = [
 ];
 
 export const CommentChannel = [
-  { label: '网站首页', value: 'main', type: 'manual' },
+  // { label: '网站首页', value: 'main', type: 'manual' },
   { label: '换库机经', value: 'library' },
   { label: '课堂-课程', value: 'course' },
   { label: '首页', value: 'index', parent: 'course', type: 'manual' },
@@ -194,33 +194,47 @@ export const AdPlace = [
 export const OrderInfoMap = {
   service: {
     qx_cat: {
-      service: '共包含6套CAT模考,在有效期内可重置一次,每套模考可做两次。',
+      label: '千行-CAT模考(6套)',
+      description: '共包含6套CAT模考,在有效期内可重置一次,每套模考可做两次。',
       refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
       copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
-      result: '您已成功购买“千行-CAT模考”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。\n您可至“个人中心- 工具”开通。',
+      result: '您已成功购买“千行-CAT模考”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。',
+      tips: '您可至“个人中心- 工具”开通。',
     },
     vip: {
-      result: '您已成功购买“VIP({expireDays}天)”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。\nVIP有效期至:{endTime}',
+      label: 'VIP',
+      description: '自由组卷、加强版报告、导出笔记、专享解析、换库提醒等权益。',
+      refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
+      copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
+      result: '您已成功购买“VIP({useExpireDays}天)”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。\nVIP有效期至:{endTime}',
+      tips: '您可至“个人中心- 工具”开通。',
     },
     textbook: {
-      service: '数学机经+阅读机经+逻辑机经,可在线查阅、在线练习或下载至本地,同时自动更新至预留邮箱。',
+      label: '千行机经',
+      description: '数学机经+阅读机经+逻辑机经,可在线查阅、在线练习或下载至本地,同时自动更新至预留邮箱。',
       refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
       copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
-      result: '您已成功购买“机经”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。\n您可至“个人中心-工具”开通。',
+      result: '您已成功购买“机经”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。',
+      tips: '您可至“个人中心-工具”开通。',
     },
   },
   data: {
     refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
     copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
-    result: '您已成功购买“千行-CAT模考”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。\n您可至“个人中心- 工具”开通。',
+    result: '您已成功购买“{title}”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。',
+    tips: '您可至“个人中心-工具”开通。',
   },
   course: {
     refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
     copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
+    result: '您已成功购买“{title}”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。',
+    tips: '您可至“个人中心-课程”进行开通。',
   },
   course_package: {
     refund_policy: '本商品为虚拟产品,购买成功后不支持退款。',
     copyright_notes: '本商品仅限购买者本人使用,不可商用和传播。',
+    result: '您已成功购买“{title}”服务,\n确认邮件已发送至您的邮箱:{email},请注意查收。',
+    tips: '您可至“个人中心-课程”进行开通。',
   },
 };
 

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

@@ -119,7 +119,7 @@ export default class extends Page {
       this.exerciseMap = getMap(list, 'id', 'title');
       this.setState({ exercise: formatTreeData(list, 'id', 'title', 'parentId') });
     });
-    Course.list().then((result) => {
+    Course.list({ excludeVs: true, excludeOnline: true }).then((result) => {
       this.itemList[5].select = result.list.map(row => {
         row.value = row.id;
         return row;

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

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

+ 180 - 6
front/project/admin/routes/ready/article/page.js

@@ -1,13 +1,15 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
-import { Button } from 'antd';
+import { Button, Modal, Checkbox, Row, Col, Switch } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+// import DragList from '@src/components/DragList';
 import FilterLayout from '@src/layouts/FilterLayout';
+import TreeLayout from '@src/layouts/TreeLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { formatDate, getMap } from '@src/services/Tools';
+import { formatDate, getMap, formatTreeData } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
 // import { ArticleChannel } from '../../../../Constant';
 import { Ready } from '../../../stores/ready';
@@ -16,8 +18,45 @@ import { Ready } from '../../../stores/ready';
 
 export default class extends Page {
   init() {
+    this.categoryTitleMap = {};
     this.categoryMap = {};
     this.categoryList = [];
+    this.categoryTree = [];
+    this.categoryColumns = [{
+      dataIndex: 'parentId',
+      title: '一级标题',
+      render: (text, record) => {
+        if (text) {
+          return this.categoryMap[text];
+        }
+        return record.title;
+      },
+    }, {
+      dataIndex: 'title',
+      title: '二级标题',
+      render: (text, record) => {
+        if (record.parentId) {
+          return record.title;
+        }
+        return '-';
+      },
+    }, {
+      dataIndex: 'isData',
+      title: '资料节点',
+      render: (text, record) => {
+        return record.parentId > 0 && <Checkbox onChange={(e) => {
+          this.changeCategoryData(record.id, e.target.checked);
+        }} checked={!!text} />;
+      },
+    }, {
+      dataIndex: 'isOfficial',
+      title: '官方资料',
+      render: (text, record) => {
+        return record.parentId > 0 && record.isData > 0 && <Checkbox onChange={(e) => {
+          this.changeCategoryOfficial(record.id, e.target.checked);
+        }} checked={!!text} />;
+      },
+    }];
     this.filterForm = [{
       key: 'parentCategoryId',
       type: 'select',
@@ -45,18 +84,21 @@ export default class extends Page {
       render: (item) => {
         return <Link to='/ready/article/detail'><Button>{item.name}</Button></Link>;
       },
+    }, {
+      key: 'category',
+      name: '目录',
     }];
     this.columns = [{
       title: '一级标题',
       dataIndex: 'parentCategoryId',
       render: (text) => {
-        return this.categoryMap[text];
+        return this.categoryTitleMap[text];
       },
     }, {
       title: '二级标题',
       dataIndex: 'categoryId',
       render: (text) => {
-        return this.categoryMap[text];
+        return this.categoryTitleMap[text];
       },
     }, {
       title: '文章标题',
@@ -79,15 +121,27 @@ export default class extends Page {
         </div>;
       },
     }];
+    this.refreshCategory();
+  }
+
+  refreshCategory() {
     Ready.allCategory().then(result => {
+      this.categoryTitleMap = getMap(result, 'id', 'title');
       this.categoryList = result.map(row => {
         row.value = row.id;
+        row.title = <Row style={{ width: 400 }}>
+          <Col span={11}>{row.title}</Col>
+          <Col span={5}>{row.parentId > 0 && <Switch checked={row.isData} checkedChildren='资料节点' unCheckedChildren='基本节点' onChange={() => this.changeCategoryData(row.id, !row.isData)} />}</Col>
+          <Col span={5}>{row.parentId > 0 && row.isData > 0 && <Switch checked={row.isOfficial} checkedChildren='官方资料' unCheckedChildren='非官方资料' onChange={() => this.changeCategoryOfficial(row.id, !row.isOfficial)} />}</Col>
+        </Row>;
         return row;
       });
-      this.categoryMap = getMap(result, 'id', 'title');
+      this.categoryTree = formatTreeData(result, 'id', 'title', 'parentId');
+      this.categoryMap = getMap(result, 'id');
       this.filterForm[0].select = this.categoryList.filter(row => row.parentId === 0);
-      this.onChangeSearch(this.filterForm, this, this.state.search.parentCategoryId);
+      this.changeSearch(this.filterForm, this, this.state.search.parentCategoryId);
       this.initData();
+      this.setState({ categoryList: this.categoryList, categoryTree: this.categoryTree });
     });
   }
 
@@ -118,6 +172,63 @@ export default class extends Page {
     });
   }
 
+  changeCategoryData(id, checked) {
+    const category = this.categoryMap[id];
+    if (category.isData) {
+      if (checked) return;
+    } else if (!checked) return;
+    Ready.editCategory({ id, isData: checked ? 1 : 0 })
+      .then(() => {
+        this.refreshCategory();
+      });
+  }
+
+  changeCategoryOfficial(id, checked) {
+    const category = this.categoryMap[id];
+    if (category.isData) {
+      if (checked) return;
+    } else if (!checked) return;
+    Ready.editCategory({ id, isOfficial: checked ? 1 : 0 })
+      .then(() => {
+        this.refreshCategory();
+      });
+  }
+
+  changeCategoryOrder(from, to) {
+    if (from.id === to.id) return;
+    if (from.parentId !== to.parentId) {
+      asyncSMessage('只允许同层排序', 'warn');
+      return;
+    }
+    let parent = [];
+    if (to.parentId === 0) {
+      parent = this.categoryTree;
+    } else {
+      parent = this.categoryMap[to.parentId].children;
+    }
+    let oldIndex = -1;
+    let newIndex = -1;
+    parent.forEach((row, i) => {
+      if (row.id === from.id) oldIndex = i;
+      if (row.id === to.id) newIndex = i;
+    });
+    const others = parent.map(row => row.id);
+    const tmp = others.splice(oldIndex, 1);
+    if (newIndex === parent.length) {
+      others.push(tmp[0]);
+    } else {
+      others.splice(newIndex, 0, tmp[0]);
+    }
+    Ready.editCategory({ id: from.id, index: newIndex === parent.length ? parent.length : newIndex })
+      .then(() => {
+        this.refreshCategory();
+      });
+  }
+
+  categoryAction() {
+    this.open({});
+  }
+
   renderView() {
     return <Block flex>
       <FilterLayout
@@ -141,6 +252,69 @@ export default class extends Page {
         onSelect={(keys, rows) => this.tableSelect(keys, rows)}
         selectedKeys={this.state.selectedKeys}
       />
+      {this.state.detail && <Modal visible closable title='目录结构' footer={null} onCancel={() => {
+        this.close(false, 'detail');
+      }} onOk={() => {
+        this.close(true, 'detail');
+      }}>
+
+        <TreeLayout
+          autoExpandParent
+          defaultExpandAll
+          itemList={this.state.categoryTree}
+          selectable={false}
+          draggable
+          // onDragStart={({ event, node }) => console.log('start', event, node.props)}
+          // onDragOver={({ event, node }) => console.log('over', event, node.props)}
+          // onDragLeave={({ event, node }) => console.log('leave', event, node.kepropsy)}
+          // onDragEnd={({ event, node }) => console.log('end', event, node.props)}
+          // onDragEnter={({ event, node, expandedKeys }) => console.log('enter', event, node.props, expandedKeys)}
+          onDrop={({ event, node, dragNode, dragNodesKeys }) => {
+            console.log('drop', event, node.props, dragNode.props, dragNodesKeys);
+            this.changeCategoryOrder(dragNode.props, node.props);
+          }
+          }
+        />
+        {/* <TableLayout
+          rowKey={'id'}
+          columns={this.categoryColumns}
+          list={this.state.categoryList}
+          pagination={false}
+        /> */}
+
+        {/* <DragList
+          loading={this.props.core.loading}
+          dataSource={this.state.categoryTree || []}
+          handle={'.icon'}
+          onMove={(oldIndex, newIndex) => {
+            this.orderQuestion(oldIndex, newIndex);
+          }}
+          renderItem={(item) => (
+            <List.Item actions={[<Icon type='bars' className='icon' />]}>
+              <Row style={{ width: '100%' }}>
+                <Col span={11}>标题: {item.title}</Col>
+              </Row>
+              <Row style={{ width: '100%' }}>
+                <DragList
+                  loading={false}
+                  dataSource={item.children}
+                  handle={`.icon${item.id}`}
+                  onMove={(oldIndex, newIndex) => {
+                    this.orderQuestion(oldIndex, newIndex);
+                  }}
+                  renderItem={(row) => (
+                    <List.Item actions={[<Icon type='bars' className={`.icon${item.id}`} />]}>
+                      <Row style={{ width: '100%' }}>
+                        <Col span={11}>标题: {row.title}</Col>
+                        <Col span={5}><Switch checked={row.isData} checkedChildren='资料节点' unCheckedChildren='基本节点' onChange={() => this.changeCategoryData(row.id, !row.isData)} /></Col>
+                        <Col span={5}>{row.isData > 0 && <Switch checked={row.isOfficial} checkedChildren='官方资料' unCheckedChildren='非官方资料' onChange={() => this.changeCategoryOfficial(row.id, !row.isOfficial)} />}</Col>
+                      </Row>
+                    </List.Item>
+                  )} />
+              </Row>
+            </List.Item>
+          )} /> */}
+      </Modal>}
     </Block>;
   }
 }

+ 77 - 51
front/project/admin/routes/setting/index/page.js

@@ -12,41 +12,67 @@ export default class extends Page {
   initData() {
     System.getIndex().then(result => {
       const { form } = this.props;
-      form.setFieldsValue(flattenObject(result));
-      this.setState({ load: true, data: result });
+      form.setFieldsValue(flattenObject(result, 'index'));
+      this.setState({ load: true, index: result });
+    });
+    System.getBase().then(result => {
+      const { form } = this.props;
+      form.setFieldsValue(flattenObject(result, 'base'));
+      this.setState({ load: true, base: result });
     });
   }
 
   addLength(field, info) {
-    let { data } = this.state;
-    data = data || {};
-    data[field] = data[field] || [];
-    data[field].push(info);
-    this.setState({ data });
+    let { index } = this.state;
+    index = index || {};
+    index[field] = index[field] || [];
+    index[field].push(info);
+    this.setState({ index });
   }
 
   deleteLength(field, start, length) {
-    let { data } = this.state;
-    data = data || {};
-    data[field] = data[field] || [];
-    data[field].splice(start, length);
-    this.setState({ data });
+    let { index } = this.state;
+    index = index || {};
+    index[field] = index[field] || [];
+    index[field].splice(start, length);
+    this.setState({ index });
   }
 
   submit() {
+    this.submitIndex();
+    this.submitBase();
+  }
+
+  submitIndex() {
+    const { form } = this.props;
+    form.validateFields(['index'], (err) => {
+      if (!err) {
+        const { index } = form.getFieldsValue();
+        index.class = Object.keys(index.class || {}).map((key) => index.class[key]);
+        index.activity = Object.keys(index.activity || {}).map((key) => index.activity[key]);
+        index.evaluation = Object.keys(index.evaluation || {}).map((key) => index.evaluation[key]);
+        System.setIndex(index)
+          .then(() => {
+            this.setState({ index });
+            asyncSMessage('保存成功');
+          }).catch((e) => {
+            form.setFields(formatFormError(index, e.result));
+          });
+      }
+    });
+  }
+
+  submitBase() {
     const { form } = this.props;
-    form.validateFields((err) => {
+    form.validateFields(['base'], (err) => {
       if (!err) {
-        const data = form.getFieldsValue();
-        data.class = Object.keys(data.class || {}).map((key) => data.class[key]);
-        data.activity = Object.keys(data.activity || {}).map((key) => data.activity[key]);
-        data.evaluation = Object.keys(data.evaluation || {}).map((key) => data.evaluation[key]);
-        System.setIndex(data)
+        const { base } = form.getFieldsValue();
+        System.setBase(base)
           .then(() => {
-            this.setState(data);
+            this.setState({ base });
             asyncSMessage('保存成功');
           }).catch((e) => {
-            form.setFields(formatFormError(data, e.result));
+            form.setFields(formatFormError(base, e.result));
           });
       }
     });
@@ -58,7 +84,7 @@ export default class extends Page {
       <h1>备考攻略</h1>
       <Form>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='自学-从零开始'>
-          {getFieldDecorator('prepare.first', {
+          {getFieldDecorator('index.prepare.first', {
             rules: [
               { required: false, message: '请输入跳转地址' },
             ],
@@ -67,7 +93,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='自学-继续练习'>
-          {getFieldDecorator('prepare.continue', {
+          {getFieldDecorator('index.prepare.continue', {
             rules: [
               { required: false, message: '请输入跳转地址' },
             ],
@@ -76,7 +102,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程-初学'>
-          {getFieldDecorator('prepare.classJunior', {
+          {getFieldDecorator('index.prepare.classJunior', {
             rules: [
               { required: false, message: '请输入跳转地址' },
             ],
@@ -85,7 +111,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程-中级'>
-          {getFieldDecorator('prepare.classMiddle', {
+          {getFieldDecorator('index.prepare.classMiddle', {
             rules: [
               { required: false, message: '请输入跳转地址' },
             ],
@@ -94,7 +120,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程-高级'>
-          {getFieldDecorator('prepare.classSenior', {
+          {getFieldDecorator('index.prepare.classSenior', {
             rules: [
               { required: false, message: '请输入跳转地址' },
             ],
@@ -112,7 +138,7 @@ export default class extends Page {
       <h1>用户数据</h1>
       <Form>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='线下用户量'>
-          {getFieldDecorator('user.numberOffline', {
+          {getFieldDecorator('index.user.numberOffline', {
             rules: [
               { required: false, message: '' },
             ],
@@ -122,7 +148,7 @@ export default class extends Page {
         </Form.Item>
 
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='700+学员数'>
-          {getFieldDecorator('user.number700', {
+          {getFieldDecorator('index.user.number700', {
             rules: [
               { required: false, message: '' },
             ],
@@ -132,7 +158,7 @@ export default class extends Page {
         </Form.Item>
 
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='学员平均分'>
-          {getFieldDecorator('user.numberScore', {
+          {getFieldDecorator('index.user.numberScore', {
             rules: [
               { required: false, message: '' },
             ],
@@ -153,7 +179,7 @@ export default class extends Page {
       <Form>
         <Row>
           {course.map((row, index) => {
-            const image = getFieldValue(`course.${index}.image`) || null;
+            const image = getFieldValue(`index.course.${index}.image`) || null;
             return <Col span={7} offset={index % 3 ? 1 : 0}><Card>
               <Button className="delete-button" size="small" onClick={() => {
                 this.deleteLength('course', index, 1);
@@ -161,7 +187,7 @@ export default class extends Page {
                 <Icon type="delete" />
               </Button>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='课程名称'>
-                {getFieldDecorator(`course.${index}.title`, {
+                {getFieldDecorator(`index.course.${index}.title`, {
                   rules: [
                     { required: true, message: '输入课程名称' },
                   ],
@@ -171,7 +197,7 @@ export default class extends Page {
                 )}
               </Form.Item>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='跳转链接'>
-                {getFieldDecorator(`course.${index}.link`, {
+                {getFieldDecorator(`index.course.${index}.link`, {
                   rules: [
                     { required: true, message: '输入跳转链接' },
                   ],
@@ -181,7 +207,7 @@ export default class extends Page {
                 )}
               </Form.Item>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='背景图片'>
-                {getFieldDecorator(`course.${index}.image`, {
+                {getFieldDecorator(`index.course.${index}.image`, {
                   rules: [
                     { required: true, message: '上传图片' },
                   ],
@@ -190,7 +216,7 @@ export default class extends Page {
                     listType="picture-card"
                     showUploadList={false}
                     beforeUpload={(file) => System.uploadImage(file).then((result) => {
-                      setFieldsValue({ [`course.${index}.image`]: result.url });
+                      setFieldsValue({ [`index.course.${index}.image`]: result.url });
                       return Promise.reject();
                     })}
                   >
@@ -224,7 +250,7 @@ export default class extends Page {
       <Form>
         <Row>
           {activity.map((row, index) => {
-            const image = getFieldValue(`activity.${index}.image`) || null;
+            const image = getFieldValue(`index.activity.${index}.image`) || null;
             return <Col span={7} offset={index % 3 ? 1 : 0}><Card>
               <Button className="delete-button" size="small" onClick={() => {
                 this.deleteLength('activity', index, 1);
@@ -232,7 +258,7 @@ export default class extends Page {
                 <Icon type="delete" />
               </Button>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='跳转链接'>
-                {getFieldDecorator(`activity.${index}.link`, {
+                {getFieldDecorator(`index.activity.${index}.link`, {
                   rules: [
                     { required: true, message: '输入跳转链接' },
                   ],
@@ -242,7 +268,7 @@ export default class extends Page {
                 )}
               </Form.Item>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='活动图片'>
-                {getFieldDecorator(`activity.${index}.image`, {
+                {getFieldDecorator(`index.activity.${index}.image`, {
                   rules: [
                     { required: true, message: '上传图片' },
                   ],
@@ -251,7 +277,7 @@ export default class extends Page {
                     listType="picture-card"
                     showUploadList={false}
                     beforeUpload={(file) => System.uploadImage(file).then((result) => {
-                      setFieldsValue({ [`claactivityss.${index}.image`]: result.url });
+                      setFieldsValue({ [`index.activity.${index}.image`]: result.url });
                       return Promise.reject();
                     })}
                   >
@@ -285,7 +311,7 @@ export default class extends Page {
       <Form>
         <Row>
           {evaluation.map((row, index) => {
-            const avatar = getFieldValue(`evaluation.${index}.avatar`) || null;
+            const avatar = getFieldValue(`index.evaluation.${index}.avatar`) || null;
             return <Col span={7} offset={index % 3 ? 1 : 0}><Card>
               <Button className="delete-button" size="small" onClick={() => {
                 this.deleteLength('evaluation', index, 1);
@@ -293,7 +319,7 @@ export default class extends Page {
                 <Icon type="delete" />
               </Button>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='学员昵称'>
-                {getFieldDecorator(`evaluation.${index}.nickname`, {
+                {getFieldDecorator(`index.evaluation.${index}.nickname`, {
                   rules: [
                     { required: true, message: '输入学员昵称' },
                   ],
@@ -303,7 +329,7 @@ export default class extends Page {
                 )}
               </Form.Item>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='学员头像'>
-                {getFieldDecorator(`evaluation.${index}.avatar`, {
+                {getFieldDecorator(`index.evaluation.${index}.avatar`, {
                   rules: [
                     { required: true, message: '上传图片' },
                   ],
@@ -312,7 +338,7 @@ export default class extends Page {
                     listType="picture-card"
                     showUploadList={false}
                     beforeUpload={(file) => System.uploadImage(file).then((result) => {
-                      setFieldsValue({ [`evaluation.${index}.avatar`]: result.url });
+                      setFieldsValue({ [`index.evaluation.${index}.avatar`]: result.url });
                       return Promise.reject();
                     })}
                   >
@@ -324,7 +350,7 @@ export default class extends Page {
                 )}
               </Form.Item>
               <Form.Item labelCol={{ span: 7 }} wrapperCol={{ span: 15 }} label='评价内容'>
-                {getFieldDecorator(`evaluation.${index}.content`, {
+                {getFieldDecorator(`index.evaluation.${index}.content`, {
                   rules: [
                     { required: true, message: '输入评价内容' },
                   ],
@@ -349,13 +375,13 @@ export default class extends Page {
 
   renderContact() {
     const { getFieldDecorator, setFieldsValue, getFieldValue } = this.props.form;
-    const wechatImage = getFieldValue('contact.wechatImage');
-    const weiboImage = getFieldValue('contacct.weiboImage');
+    const wechatImage = getFieldValue('base.contact.wechatImage');
+    const weiboImage = getFieldValue('base.contacct.weiboImage');
     return <Block>
       <Form>
         <h1>联系方式</h1>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='电话'>
-          {getFieldDecorator('contact.phone', {
+          {getFieldDecorator('base.contact.phone', {
             rules: [
               { required: false, message: '请输入电话' },
             ],
@@ -364,7 +390,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='邮箱'>
-          {getFieldDecorator('contact.email', {
+          {getFieldDecorator('base.contact.email', {
             rules: [
               { required: false, message: '请输入邮箱' },
             ],
@@ -373,7 +399,7 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='微信号'>
-          {getFieldDecorator('contact.wechat', {
+          {getFieldDecorator('base.contact.wechat', {
             rules: [
               { required: false, message: '请输入微信号' },
             ],
@@ -383,13 +409,13 @@ export default class extends Page {
         </Form.Item>
 
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='公众号二维码'>
-          {getFieldDecorator('contact.wechatImage')(
+          {getFieldDecorator('base.contact.wechatImage')(
             <Upload
               listType="picture-card"
               showUploadList={false}
               beforeUpload={(file) => {
                 System.uploadImage(file).then((result) => {
-                  setFieldsValue({ 'contact.wechatImage': result.url });
+                  setFieldsValue({ 'base.contact.wechatImage': result.url });
                   return Promise.reject();
                 });
               }
@@ -403,12 +429,12 @@ export default class extends Page {
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='微博二维码'>
-          {getFieldDecorator('contact.weiboImage')(
+          {getFieldDecorator('base.contact.weiboImage')(
             <Upload
               listType="picture-card"
               showUploadList={false}
               beforeUpload={(file) => System.uploadImage(file).then((result) => {
-                setFieldsValue({ 'contact.weiboImage': result.url });
+                setFieldsValue({ 'base.contact.weiboImage': result.url });
                 return Promise.reject();
               })}
             >

+ 0 - 11
front/project/admin/routes/setting/service/page.js

@@ -139,17 +139,6 @@ export default class extends Page {
             }} style={{ width: '200px' }} />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} label='有效期说明'>
-          {getFieldDecorator('qx_cat.package[0].expire_info', {
-            rules: [
-              { required: true, message: '输入千行Cat有效期说明' },
-            ],
-          })(
-            <Input placeholder='请输入千行Cat有效期说明' onChange={(e) => {
-              this.changeMapValue('qx_cat', 'package', 0, 'expire_info', e.target.value);
-            }} style={{ width: '200px' }} />,
-          )}
-        </Form.Item>
         <Form.Item labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} label='退款政策'>
           {getFieldDecorator('qx_cat.package[0].refund_policy', {
             rules: [

+ 1 - 1
front/project/admin/routes/student/askCourseDetail/page.js

@@ -127,7 +127,7 @@ export default class extends Page {
           {formatDate(createTime)}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问位置'>
-          {`P${courseNo.no}:${position}`}
+          {`P${courseNo.no}:${position}-${position + 5}min`}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问内容'>
           {originContent}

+ 2 - 6
front/project/admin/routes/user/orderDetail/page.js

@@ -13,9 +13,6 @@ const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
 const ServiceParamRelation = getMap(Object.keys(ServiceParamMap).map(key => {
   return { map: getMap(ServiceParamMap[key], 'value', 'label'), key };
 }), 'key', 'map');
-const promoteInfoMap = {
-  'textbook-half': '半价机经券',
-};
 export default class extends Page {
   initData() {
     const { id } = this.params;
@@ -81,15 +78,14 @@ export default class extends Page {
         <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='原价'>
           {formatMoney(data.originMoney)}
         </Form.Item>
-        {Object.keys(data.promote || {}).map(key => {
-          const info = data.promote[key];
+        {(data.promote || []).map(info => {
           return <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='优惠金额'>
             <Row>
               <Col span={12} >
                 -{formatMoney(info.originMoney - info.money)}
               </Col>
               <Col span={12}>
-                {info.message || promoteInfoMap[`${key}-${info.key}`]}
+                {info.message}
               </Col>
             </Row>
           </Form.Item>;

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

@@ -15,6 +15,14 @@ export default class ReadyStore extends BaseStore {
     return this.apiGet('/ready/category/all', params);
   }
 
+  editCategory(params) {
+    return this.apiPut('/ready/category/edit', params);
+  }
+
+  deleteCategory(params) {
+    return this.apiDel('/ready/category/delete', params);
+  }
+
   listArticle(params) {
     return this.apiGet('/ready/article/list', params);
   }

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

@@ -29,6 +29,14 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/index', params);
   }
 
+  getBase() {
+    return this.apiGet('/setting/base');
+  }
+
+  setBase(params) {
+    return this.apiPut('/setting/base', params);
+  }
+
   getPlace() {
     return this.apiGet('/setting/place');
   }

+ 1 - 1
front/project/h5/index.js

@@ -2,7 +2,7 @@ export default {
   mode: () => import('./app'),
   apiToken: 'token',
   loginAuth(route, { user }) {
-    if (route.needLogin && !user.login) return true;
+    if (route.needLogin && !user.login) return false;
     return true;
   },
 };

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

@@ -23,7 +23,7 @@ export default class extends Page {
       .then((wx) => {
         const { info } = this.props.user;
         wx.onMenuShareAppMessage({
-          title: '', // Share title
+          title: '千行GMAT', // Share title
           desc: '', // Share description
           link: `${H5Url}/id/${info.inviteCode}`, // Share Link,this link domain name and path must be the same as the current page which corresponding to JS secured domain name as Official account
           imgUrl: '', // Share Icon

+ 1 - 0
front/project/h5/routes/page/pay/index.less

@@ -31,6 +31,7 @@
   .desc {
     color: #303036FF;
     margin-bottom: 30px;
+    white-space: pre-line;
   }
 
   .agree {

+ 140 - 44
front/project/h5/routes/page/pay/page.js

@@ -3,55 +3,60 @@ import './index.less';
 import Page from '@src/containers/Page';
 import { Checkbox } from 'antd';
 // import Assets from '@src/components/Assets';
-import { getMap } from '@src/services/Tools';
+import { getMap, formatDate } from '@src/services/Tools';
 import Button from '../../../components/Button';
 import Money from '../../../components/Money';
+import Icon from '../../../components/Icon';
 import { Order } from '../../../stores/order';
+import { My } from '../../../stores/my';
 import { Main } from '../../../stores/main';
-import { ServiceKey, OrderInfoMap } from '../../../../Constant';
-
-const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
+import { ServiceParamMap, OrderInfoMap } from '../../../../Constant';
+import { Common } from '../../../stores/common';
 
+const ServiceParamRelation = getMap(Object.keys(ServiceParamMap).map(key => {
+  return {
+    map: getMap(ServiceParamMap[key].map((row, index) => {
+      row.index = index;
+      return row;
+    }), 'value', 'index'),
+    key,
+  };
+}), 'key', 'map');
+function formatTitle(record) {
+  if (record.productType === 'course_package') {
+    return (record.coursePackage || {}).title;
+  }
+  if (record.productType === 'course') {
+    return (record.course || {}).title;
+  }
+  if (record.productType === 'data') {
+    return (record.data || {}).title;
+  }
+  if (record.productType === 'service') {
+    return record.info.label || ((record.serviceInfo || {}).title);
+  }
+  return '';
+}
+function formatCheckout(checkouts) {
+  checkouts.forEach(checkout => {
+    checkout.info = OrderInfoMap[checkout.productType];
+    if (checkout.productType === 'service') {
+      const index = (ServiceParamRelation[checkout.service] && ServiceParamRelation[checkout.service][checkout.param]) || 0;
+      checkout.info = Object.assign({}, checkout.info[checkout.service], checkout.serviceInfo.package[index]);
+    }
+    checkout.title = formatTitle(checkout);
+  });
+}
 export default class extends Page {
   initState() {
-    return {};
+    return { show: true, showEnd: false };
   }
 
   initData() {
     const { id } = this.params;
     Order.getOrder(id).then(order => {
-      order.packageMap = {};
-      (order.packages || []).forEach(row => {
-        order.packageMap[row.id] = row;
-      });
-      order.courseMap = {};
-      (order.courses || []).forEach(row => {
-        order.courseMap[row.id] = row;
-      });
-      order.dataMap = {};
-      (order.datas || []).forEach(row => {
-        order.dataMap[row.id] = row;
-      });
-      order.checkouts.forEach(checkout => {
-        checkout.info = OrderInfoMap[checkout.productType];
-        switch (checkout.productType) {
-          case 'service':
-            checkout.title = ServiceKeyMap[checkout.service];
-            checkout.info = checkout.info[checkout.service];
-            break;
-          case 'data':
-            checkout.title = order.dataMap[checkout.productId].title;
-            break;
-          case 'course_package':
-            checkout.title = order.packageMap[checkout.productId].title;
-            break;
-          case 'course':
-            checkout.title = order.courseMap[checkout.productId].title;
-            break;
-          default:
-        }
-      });
-      const [checkout] = order.checkouts.filter(row => row.parentId === 0);
+      formatCheckout(order.checkouts);
+      const [checkout] = order.checkouts;
       this.setState({ order, checkout });
     });
     Main.getContract('course').then(result => {
@@ -61,10 +66,87 @@ export default class extends Page {
 
   pay() {
     const { id } = this.params;
-    Order.wechatJs(id).then(() => { });
+    if (this.paying) return;
+    this.paying = true;
+    this.setState({ paying: true });
+    Order.wechatJs(id).then((info) => {
+      return Common.readyWechatBridge().then(() => {
+        WeixinJSBridge.invoke(
+          'getBrandWCPayRequest', info,
+          (res) => {
+            if (res.err_msg === 'get_brand_wcpay_request:ok') {
+              // 使用以上方式判断前端返回,微信团队郑重提示:
+              // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
+              this.queryPay(true);
+            } else if (res.err_msg === 'get_brand_wcpay_request:cancel' || res.err_msg === 'get_brand_wcpay_request:fail') {
+              // 支付失败
+              this.paying = false;
+              this.setState({ paying: false });
+            }
+            console.log(res);
+          },
+        );
+      });
+    });
+  }
+
+  queryPay(force) {
+    const { order } = this.state;
+    if (this.time) {
+      clearTimeout(this.time);
+    }
+    if (force) {
+      this.times = 0;
+    } else {
+      this.times = (this.times || 0) + 1;
+    }
+    this.time = setTimeout(() => {
+      Order.query(order.id)
+        .then(result => {
+          if (result) {
+            // 支付成功
+            this.paySuccess();
+          } else if (order) {
+            this.queryPay();
+          } else {
+            this.setState({ select: null, pay: null, order: null, info: null });
+          }
+        });
+    }, 1000);
+  }
+
+  paySuccess() {
+    const { order } = this.state;
+    const { info } = this.props.user;
+    Order.getOrder(order.id).then(result => {
+      // 确保开通用的是record记录id
+      const [checkout] = result.checkouts;
+      formatCheckout(result.checkouts);
+      checkout.info.result = checkout.info.result.replace('{email}', info.email).replace('{useExpireDays}', checkout.useExpireDays).replace('{title}', checkout.title);
+      if (checkout.service === 'vip') {
+        // 查询最后有效期
+        My.getVipInfo().then(vip => {
+          checkout.info.result = checkout.info.result.replace('{endTime}', formatDate(vip.expireTime, 'YYYY-MM-DD'));
+          this.setState({ show: false, showEnd: true, order: result, checkout });
+        });
+      } else {
+        this.setState({ show: false, showEnd: true, order: result, checkout });
+      }
+    });
   }
 
   renderView() {
+    const { show, showEnd } = this.state;
+    if (show) {
+      return this.renderPay();
+    }
+    if (showEnd) {
+      return this.renderEnd();
+    }
+    return null;
+  }
+
+  renderPay() {
     const { order = {}, contract = {}, checkout = {} } = this.state;
     const { info = {}, productType } = checkout;
     let content = '';
@@ -88,9 +170,9 @@ export default class extends Page {
         </div>
         {content}
         {info.refund_policy && [< div className="title">退款政策</div>,
-          <div className="desc">本产品为虚拟产品,购买成功后不支持退款。</div>]}
+          <div className="desc">{info.refund_policy}</div>]}
         {info.copyright_notes && [<div className="title">版权说明</div>,
-          <div className="desc">本商品仅限购买者本人使用,不可商用和传播。</div>]}
+          <div className="desc">{info.copyright_notes}</div>]}
         {order.productTypes && order.productTypes.indexOf('course') > 0 && <div className="agree">
           <Checkbox checked />
           我已阅读并同意 <a onClick={() => this.setState({ showContract: true })}>{contract.title}</a>
@@ -104,6 +186,7 @@ export default class extends Page {
             width={110}
             className="f-r"
             radius
+            disabled={this.state.paying}
             onClick={() => {
               this.pay();
             }}
@@ -140,7 +223,7 @@ export default class extends Page {
           {order.checkouts.map(row => {
             if (row.parentId === 0) return null;
             return <div className="info-item">
-              {row.title} <span className="f-r">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '永久'} 使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
+              {row.title} <span className="f-r">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'} 使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
             </div>;
           })}
         </div>
@@ -149,15 +232,14 @@ export default class extends Page {
   }
 
   renderSingle() {
-    const { checkout } = this.state;
-    console.log(checkout);
+    const { checkout = {} } = this.state;
     return (
       <div className="info single">
         <div className="info-block">
           {checkout.title} <Money className="f-r" value={checkout.money} />
         </div>
         <div className="info-block">
-          开通有效期 <span className="f-r">{checkout.expireDays ? `${checkout.expireDays}天` : '永久'}</span>
+          开通有效期 <span className="f-r">{checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}</span>
         </div>
         <div className="info-block">
           使用有效期 <span className="f-r">{checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
@@ -165,4 +247,18 @@ export default class extends Page {
       </div>
     );
   }
+
+  renderEnd() {
+    const { checkout = {} } = this.state;
+    const { info = {} } = checkout;
+    return (
+      <div className="finish">
+        <div className="icon">
+          <Icon type="check-circle" />
+        </div>
+        <div className="title">支付成功!</div>
+        <div className="desc">{info.result}</div>
+      </div>
+    );
+  }
 }

+ 11 - 9
front/project/h5/stores/common.js

@@ -20,15 +20,17 @@ export default class CommonStore extends BaseStore {
     return this.apiForm('/common/upload/image', { file });
   }
 
-  readyWechatBridge(callback) {
-    if (typeof WeixinJSBridge === 'object' && typeof WeixinJSBridge.invoke === 'function') {
-      callback();
-    } else if (document.addEventListener) {
-      document.addEventListener('WeixinJSBridgeReady', callback, false);
-    } else if (document.attachEvent) {
-      document.attachEvent('WeixinJSBridgeReady', callback);
-      document.attachEvent('onWeixinJSBridgeReady', callback);
-    }
+  readyWechatBridge() {
+    return new Promise((resolve) => {
+      if (typeof WeixinJSBridge === 'object' && typeof WeixinJSBridge.invoke === 'function') {
+        resolve();
+      } else if (document.addEventListener) {
+        document.addEventListener('WeixinJSBridgeReady', resolve, false);
+      } else if (document.attachEvent) {
+        document.attachEvent('WeixinJSBridgeReady', resolve);
+        document.attachEvent('onWeixinJSBridgeReady', resolve);
+      }
+    });
   }
 
   readyWechat(url, list) {

+ 7 - 0
front/project/h5/stores/main.js

@@ -21,6 +21,13 @@ export default class MainStore extends BaseStore {
   }
 
   /**
+   * 获取基础配置
+   */
+  getBase() {
+    return this.apiGet('/base/base');
+  }
+
+  /**
    * 获取广告列表
    */
   getAd(channel) {

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

@@ -50,6 +50,10 @@ export default class MyStore extends BaseStore {
     return this.apiPost('/my/invite/email', { emails });
   }
 
+  getVipInfo() {
+    return this.apiGet('/my/vip/info');
+  }
+
   /**
    * 用户站内信
    * @param {*} page

+ 4 - 4
front/project/h5/stores/order.js

@@ -10,12 +10,12 @@ export default class OrderStore extends BaseStore {
     return this.apiPost('/order/checkout/add', { productType, productId, service, param, number });
   }
 
-  changeCheckout(checkoutId, number) {
-    return this.apiDelete('/order/checkout/number', { checkoutId, number });
+  changeCheckout(id, number) {
+    return this.apiPut('/order/checkout/number', { id, number });
   }
 
-  removeCheckout(checkoutId) {
-    return this.apiDelete('/order/checkout/delete', { checkoutId });
+  removeCheckout(id) {
+    return this.apiDel('/order/checkout/delete', { id });
   }
 
   confirmPay() {

+ 12 - 2
front/project/h5/stores/user.js

@@ -10,9 +10,19 @@ export default class UserStore extends BaseStore {
     return { login: false };
   }
 
+  initAfter() {
+    if (this.state.login) {
+      this.refreshToken().then(() => {
+        if (this.adminLogin) {
+          window.location.href = window.location.href.replace(`token=${this.adminLogin}`, '').replace('&&', '&');
+        }
+      });
+    }
+  }
+
   infoHandle(result) {
-    this.setToken(result.token);
-    this.setState({ login: true, needLogin: false, info: result, username: result.username });
+    if (result.token) this.setToken(result.token);
+    this.setState({ login: result.id, needLogin: false, info: result, username: result.username });
   }
 
   originInviteCode(inviteCode) {

+ 8 - 6
front/project/www/app.js

@@ -4,6 +4,7 @@ import zhCN from 'antd/lib/locale-provider/zh_CN';
 import './app.less';
 import Header from './components/Header';
 import Login from './components/Login';
+import { PayModal } from './components/PayModal';
 
 export default class extends Component {
   constructor(props) {
@@ -20,13 +21,14 @@ export default class extends Component {
           <div id="full-page">
             {children}
             <Login {...this.props} />
+            <PayModal {...this.props} />
           </div>
-        ) : (
-          <div className={`${config.tab || ''}`} id="page">
-            <Header tabs={project.tabs} active={config.tab} {...this.props} />
-            {children}
-            <Login {...this.props} />
-          </div>
+        ) : (<div className={`${config.tab || ''}`} id="page">
+          <Header tabs={project.tabs} active={config.tab} {...this.props} />
+          {children}
+          <Login {...this.props} />
+          <PayModal {...this.props} />
+        </div>
         )}
       </LocaleProvider>
     );

+ 8 - 0
front/project/www/app.less

@@ -354,6 +354,14 @@
   line-height: 16px;
 }
 
+.ws-pl {
+  white-space: pre-line;
+}
+
+.ws-p {
+  white-space: pre;
+}
+
 input,
 textarea {
   outline: none;

+ 3 - 3
front/project/www/components/Card/index.js

@@ -151,7 +151,7 @@ export class Card1 extends Component {
 
   getOpenBody() {
     const { checked } = this.state;
-    const { data, onOpen } = this.props;
+    const { data, onOpen, contract } = this.props;
     const { teacher, endTime, courseModule } = data;
     switch (courseModule) {
       case 'video':
@@ -174,11 +174,11 @@ export class Card1 extends Component {
               this.setState({ checked: !checked });
             }} />
             <span>
-              我已阅读并同意<Link to="">《千行课程协议》</Link>
+              我已阅读并同意<a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a>
             </span>
           </div>
           <div className="btn">
-            <Button size="lager" radius onClick={() => onOpen && onOpen()}>
+            <Button size="lager" disabled={!checked} radius onClick={() => checked && onOpen && onOpen()}>
               开通作业
               </Button>
           </div>

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

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

+ 2 - 0
front/project/www/components/ListTable/index.less

@@ -1,6 +1,8 @@
 @import '../../app.less';
 
 .module.list-table {
+  overflow: visible;
+
   .header {
     font-size: 18px;
     height: 56px;

+ 12 - 6
front/project/www/components/Login/index.js

@@ -36,10 +36,16 @@ export default class Login extends Component {
       },
       false,
     );
-    Main.getContract('register')
-      .then(result => {
-        this.setState({ contract: result });
-      });
+    if (!props.user.login) {
+      Main.getContract('register')
+        .then(result => {
+          this.setState({ registerContract: result });
+        });
+      Main.getContract('privacy')
+        .then(result => {
+          this.setState({ privacyContract: result });
+        });
+    }
   }
 
   close() {
@@ -199,7 +205,7 @@ export default class Login extends Component {
   }
 
   renderLoginPhone() {
-    const { needEmail, contract = {} } = this.state;
+    const { needEmail, registerContract = {}, privacyContract } = this.state;
     return (
       <div className="body">
         <div className="title">手机号登录</div>
@@ -244,7 +250,7 @@ export default class Login extends Component {
         )}
         {needEmail && (<div>
           <RadioItem checked theme="white" className="m-r-5" />
-          我已经阅读并同意 <a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a> 与 <a href={`/contract/${contract.key}`} target="_blank">隐私政策</a>.
+          我已经阅读并同意 <a href={`/contract/${registerContract.key}`} target="_blank">{registerContract.title}</a> 与 <a href={`/contract/${privacyContract.key}`} target="_blank">{privacyContract.title}</a>.
         </div>)}
         <Button
           type="primary"

+ 4 - 0
front/project/www/components/Module/index.less

@@ -6,4 +6,8 @@
   margin-bottom: 24px;
   box-shadow: 0 5px 6px 0 rgba(9, 9, 9, 0.05);
   overflow: hidden;
+
+  >.filter {
+    border-radius: 12px;
+  }
 }

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

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

+ 340 - 58
front/project/www/components/PayModal/index.js

@@ -1,37 +1,238 @@
 import React, { Component } from 'react';
 import './index.less';
-import { Link } from 'react-router-dom';
 import { Checkbox, Icon } from 'antd';
 import Assets from '@src/components/Assets';
+import { formatDate } from '@src/services/Tools';
 import Tabs from '../Tabs';
 import Modal from '../Modal';
 
+import { Order } from '../../stores/order';
+import { Main } from '../../stores/main';
+import { My } from '../../stores/my';
+import { User } from '../../stores/user';
+
+export class PayModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { show: true };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.show && !this.init) {
+      this.init = true;
+      Main.getContract('course')
+        .then((result) => {
+          this.setState({ contract: result });
+        });
+    }
+  }
+
+  changePay(key) {
+    const { order } = this.props.user;
+    if (!order) return;
+    this.setState({ info: {}, pay: key });
+    let handler = null;
+    switch (key) {
+      case 'wechatpay':
+        handler = Order.wechatQr(order.id)
+          .then((result) => {
+            this.setState({ info: result });
+          });
+        break;
+      case 'alipay':
+        handler = Order.alipayQr(order.id)
+          .then((result) => {
+            this.setState({ info: result });
+          });
+        break;
+      default:
+        return;
+    }
+    handler.then(() => {
+      this.queryPay();
+    });
+  }
+
+  queryPay(force) {
+    const { order, needPay } = this.props.user;
+    if (this.time) {
+      clearTimeout(this.time);
+    }
+    if (force) {
+      this.times = 0;
+    } else {
+      this.times = (this.times || 0) + 1;
+    }
+    this.time = setTimeout(() => {
+      Order.query(order.id)
+        .then(result => {
+          if (result) {
+            // 支付成功
+            this.paySuccess();
+          } else if (needPay) {
+            this.queryPay();
+          } else {
+            this.setState({ select: null, pay: null, order: null, info: null });
+          }
+        });
+    }, 1000);
+  }
+
+  paySuccess() {
+    const { order } = this.props.user;
+    const { info } = this.props.user;
+    Order.getOrder(order.id).then(result => {
+      User.formatOrder(result);
+      // 确保开通用的是record记录id
+      const [checkout] = result.checkouts;
+      checkout.info.result = checkout.info.result.replace('{email}', info.email).replace('{useExpireDays}', checkout.useExpireDays).replace('{title}', checkout.title);
+      if (checkout.service === 'vip') {
+        // 查询最后有效期
+        My.getVipInfo().then(vip => {
+          checkout.info.result = checkout.info.result.replace('{endTime}', formatDate(vip.expireTime, 'YYYY-MM-DD'));
+          this.setState({ show: false, showVipEnd: true, order: result, checkout });
+        });
+      } else if (order.checkouts.length === 1 && checkout.productType === 'data') {
+        this.setState({ show: false, showDataEnd: true, order: result, checkout });
+      } else if (order.checkouts.length === 1) {
+        this.setState({ show: false, showEnd: true, order: result, checkout });
+      } else {
+        User.closePay();
+      }
+    });
+  }
+
+  confirm() {
+    const { pay } = this.state;
+    if (pay === 'bank') {
+      this.setState({ showBank: true, show: false });
+    } else {
+      //
+    }
+  }
+
+  open() {
+    const { checkout } = this.state;
+    Order.useRecord(checkout.id)
+      .then(() => {
+        User.closePay();
+        this.setState({ show: true, pay: null });
+      });
+  }
+
+  read() {
+    User.closePay();
+    linkTo('/my/data');
+  }
+
+  close() {
+    const { showEnd, showBank } = this.state;
+    User.closePay(showEnd || showBank ? null : new Error('支付失败'));
+    this.setState({ show: true, pay: null, showEnd: false, showBank: false });
+  }
+
+  render() {
+    const { needPay, order } = this.props.user;
+    const { show, showBank, showEnd, showDataEnd, showVipEnd, contract } = this.state;
+    if (!needPay) return [];
+    return [
+      showBank && <PayKBankModal
+        show
+        order={order}
+        checkout={order.checkouts[0]}
+        onConfirm={() => this.close()}
+      />,
+      showEnd && <PayMEndModal
+        show
+        order={order}
+        checkout={order.checkouts[0]}
+        onCancel={() => this.close()}
+        onClose={() => this.close()}
+        onConfirm={() => this.open()}
+      />,
+      showDataEnd && <PayMDataEndModal
+        show
+        order={order}
+        checkout={order.checkouts[0]}
+        onCancel={() => this.close()}
+        onClose={() => this.close()}
+        onConfirm={() => this.read()}
+      />,
+      showVipEnd && <PayMVipEndModal
+        show
+        order={order}
+        checkout={order.checkouts[0]}
+        onConfirm={() => this.close()}
+      />,
+      show && order.checkouts.length > 1 && <PayMutilModal
+        show
+        contract={contract}
+        order={order}
+        onChangePay={(key) => this.changePay(key)}
+        onCancel={() => this.close()}
+        onClose={() => this.close()}
+        onConfirm={() => this.confirm()}
+      />,
+      show && order.checkouts.length === 1 && <PayMModal
+        show
+        contract={contract}
+        order={order}
+        checkout={order.checkouts[0]}
+        productType={order.productTypes[0]}
+        onChangePay={(key) => this.changePay(key)}
+        onCancel={() => this.close()}
+        onClose={() => this.close()}
+        onConfirm={() => this.confirm()}
+      />,
+    ];
+  }
+}
+
 export class PayMModal extends Component {
   constructor(props) {
     super(props);
-    this.state = { pay: 'alipay' };
+    const payList = [{ key: 'alipay', title: '支付宝' }, { key: 'wechatpay', title: '微信' }];
+    if (props.productType === 'course') {
+      payList.push({ key: 'bank', title: '银行转账' });
+    }
+    this.state = { pay: 'alipay', payList, checked: true, showChecked: props.productType === 'course' };
+    setTimeout(() => {
+      props.onChangePay('alipay');
+    }, 100);
   }
 
   render() {
-    const { show, desc, onConfirm, onCancel } = this.props;
-    const { pay } = this.state;
+    const { show, checkout, onClose, onConfirm, onChangePay, order, contract = {}, productType } = this.props;
+    const { info } = checkout;
+    const { pay, payList, checked, showChecked } = this.state;
     return (
       <Modal
         className="pay-modal"
         show={show}
         width={760}
-        title="购买 千行-CAT 模考"
-        confirmText="支付成功"
-        cancelText="稍后开通"
-        onConfirm={onConfirm}
-        onCancel={onCancel}
+        title={`购买${checkout.title}`}
+        confirmText={pay === 'bank' ? '确认' : '支付成功'}
+        onConfirm={pay === 'bank' ? onConfirm : null}
+        // cancelText="稍后开通"
+        // onCancel={onCancel}
+        onClose={onClose}
       >
         <div className="pay-modal-wrapper">
           <div className="info-layout">
-            <div className="desc">{desc}</div>
+            <div className="desc">
+              商品: {productType === 'data' ? info.title : checkout.title}<br />
+              服务: {productType === 'data' ? checkout.title : info.description}<br />
+              开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}
+              <br />
+              使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}
+              <br />
+              退款政策: {info.refund_policy}
+              <br />
+              版权说明: {info.copyright_notes}
+            </div>
             <div className="money">
               <div className="t-2">应付金额:</div>
-              <div className="t-1 f-w-b t-s-24">¥ 8888.88</div>
+              <div className="t-1 f-w-b t-s-24">¥ {order.money}</div>
             </div>
           </div>
           <div className="pay-layout">
@@ -40,16 +241,29 @@ export class PayMModal extends Component {
               size="small"
               active={pay}
               width={80}
-              tabs={[{ key: 'alipay', title: '支付宝' }, { key: 'wechatpay', title: '微信' }]}
+              tabs={payList}
               render={item => <Assets name={item.key} />}
-              onChange={key => this.setState({ pay: key })}
+              onChange={key => {
+                this.setState({ pay: key });
+                onChangePay(key);
+              }}
             />
-            <div className="pay">
+            <div hidden={pay === 'bank'} className="pay">
               <div className="qrcode">
-                <Assets name="qrcode" />
+                <Assets name="qrcode" src={checked ? '' : '模糊'} />
               </div>
               <div className="t">请使用手机微信或支付宝扫码付款</div>
-              <div className="t">支付金额: ¥ 300</div>
+              <div className="t">支付金额: ¥ {order.money}</div>
+            </div>
+            <div hidden={pay !== 'bank'} className="bank">
+              <div className="t">汇款银行:中国工商银行上海市浦东支行</div>
+              <div className="t">汇款账号:6100 0000 0000 000</div>
+            </div>
+            <div className="agree" hidden={!showChecked}>
+              <Checkbox className="m-r-1" checked={checked} onClick={() => {
+                this.setState({ showChecked: checked, checked: !checked });
+              }} />
+              我已阅读并同意<a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a>
             </div>
           </div>
 
@@ -65,57 +279,45 @@ export class PayMModal extends Component {
   }
 }
 
-export class PayMEndModal extends Component {
-  render() {
-    const { show, onConfirm, onCancel } = this.props;
-    return (
-      <Modal
-        show={show}
-        width={630}
-        title="付款成功"
-        confirmText="立即开通"
-        cancelText="稍后开通"
-        onConfirm={onConfirm}
-        onCancel={onCancel}
-      >
-        <div className="t-2">
-          <Icon className="t-5 m-r-5" type="check" />
-          您已成功购买「千行-CAT模考(6套)」服务。
-        </div>
-        <div className="t-2">确认邮件已发送至您的邮箱:XXXXX,请注意查收。</div>
-        <div style={{ bottom: 10, left: 0 }} className="p-a t-3 t-s-14">
-          *您可至“我的-工具”开通。
-        </div>
-      </Modal>
-    );
-  }
-}
-
-export class PayKModal extends Component {
+export class PayMutilModal extends Component {
   constructor(props) {
     super(props);
-    this.state = { pay: 'alipay' };
+    this.state = { pay: 'alipay', checked: true, showChecked: true };
+
+    setTimeout(() => {
+      props.onChangePay('alipay');
+    }, 100);
   }
 
   render() {
-    const { show, desc, onConfirm, onCancel } = this.props;
-    const { pay } = this.state;
+    const { show, onConfirm, onClose, onChangePay, order, contract = {} } = this.props;
+    const { pay, checked, showChecked } = this.state;
     return (
       <Modal
         className="pay-modal"
         show={show}
         width={760}
-        title="购买 千行课堂"
+        title={'购物车结账'}
         confirmText={pay === 'bank' ? '确认' : '支付成功'}
-        onConfirm={onConfirm}
-        onCancel={onCancel}
+        onConfirm={pay === 'bank' ? onConfirm : null}
+        // onCancel={onCancel}
+        onClose={onClose}
       >
         <div className="pay-modal-wrapper">
           <div className="info-layout">
-            <div className="desc">{desc}</div>
+            <div className="desc">
+              商品: {order.checkouts.map(row => row.title).join(' ')}<br />
+              服务: 见<a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a><br />
+              开通有效期: 见订单详情
+              <br />
+              使用有效期: 见订单详情
+              <br />
+              退款政策: 本商品为虚拟产品,购买成功后不支持退款。<br />
+              版权说明: 本商品为虚拟产品,购买成功后不支持退款。
+            </div>
             <div className="money">
               <div className="t-2">应付金额:</div>
-              <div className="t-1 f-w-b t-s-24">¥ 8888.88</div>
+              <div className="t-1 f-w-b t-s-24">¥ {order.money}</div>
             </div>
           </div>
           <div className="pay-layout">
@@ -130,22 +332,27 @@ export class PayKModal extends Component {
                 { key: 'bank', title: '银行转账' },
               ]}
               render={item => <Assets name={item.key} />}
-              onChange={key => this.setState({ pay: key })}
+              onChange={key => {
+                this.setState({ pay: key });
+                onChangePay(key);
+              }}
             />
             <div hidden={pay === 'bank'} className="pay">
               <div className="qrcode">
-                <Assets name="qrcode" />
+                <Assets name="qrcode" src={checked ? '' : '模糊'} />
               </div>
               <div className="t">请使用手机微信或支付宝扫码付款</div>
-              <div className="t">支付金额: ¥ 300</div>
+              <div className="t">支付金额: ¥ {order.money}</div>
             </div>
             <div hidden={pay !== 'bank'} className="bank">
               <div className="t">汇款银行:中国工商银行上海市浦东支行</div>
               <div className="t">汇款账号:6100 0000 0000 000</div>
             </div>
-            <div className="agree">
-              <Checkbox className="m-r-1" />
-              我已阅读并同意<Link to="">千行课程购买协议</Link>
+            <div className="agree" hidden={!showChecked}>
+              <Checkbox className="m-r-1" checked={checked} onClick={() => {
+                this.setState({ showChecked: checked, checked: !checked });
+              }} />
+              我已阅读并同意<a href={`/contract/${contract.key}`} target="_blank">{contract.title}</a>
             </div>
           </div>
 
@@ -168,7 +375,7 @@ export class PayKBankModal extends Component {
       <Modal
         show={show}
         width={630}
-        title="购买千行课堂"
+        title="银行汇款"
         btnAlign="center"
         confirmText="好的,知道了"
         onConfirm={onConfirm}
@@ -183,3 +390,78 @@ export class PayKBankModal extends Component {
     );
   }
 }
+
+export class PayMEndModal extends Component {
+  render() {
+    const { show, onConfirm, onCancel, checkout = {} } = this.props;
+    const { info } = checkout;
+    return (
+      <Modal
+        show={show}
+        width={630}
+        title="付款成功"
+        confirmText="立即开通"
+        cancelText="稍后开通"
+        onConfirm={onConfirm}
+        onCancel={onCancel}
+      >
+        <div className="t-2 ws-pl">
+          <Icon className="t-5 m-r-5" type="check" />{info.result}
+        </div>
+        {info.tips && <div style={{ bottom: 10, left: 0 }} className="p-a t-3 t-s-14">
+          *{info.tips}
+        </div>}
+      </Modal>
+    );
+  }
+}
+
+export class PayMDataEndModal extends Component {
+  render() {
+    const { show, onConfirm, onCancel, checkout = {} } = this.props;
+    const { info } = checkout;
+    return (
+      <Modal
+        show={show}
+        width={630}
+        title="付款成功"
+        confirmText="立即查看"
+        cancelText="暂不查看"
+        onConfirm={onConfirm}
+        onCancel={onCancel}
+      >
+        <div className="t-2 ws-pl">
+          <Icon className="t-5 m-r-5" type="check" />{info.result}
+        </div>
+        {info.tips && <div style={{ bottom: 10, left: 0 }} className="p-a t-3 t-s-14">
+          *{info.tips}
+        </div>}
+      </Modal>
+    );
+  }
+}
+
+export class PayMVipEndModal extends Component {
+  render() {
+    const { show, onConfirm, checkout = {} } = this.props;
+    const { info } = checkout;
+    return (
+      <Modal
+        show={show}
+        width={630}
+        title="付款成功"
+        confirmText="知道了"
+        // cancelText="稍后开通"
+        onConfirm={onConfirm}
+      // onCancel={onCancel}
+      >
+        <div className="t-2 ws-pl">
+          <Icon className="t-5 m-r-5" type="check" />{info.result}
+        </div>
+        {info.tips && <div style={{ bottom: 10, left: 0 }} className="p-a t-3 t-s-14">
+          *{info.tips}
+        </div>}
+      </Modal>
+    );
+  }
+}

+ 121 - 44
front/project/www/components/VipRenew/index.js

@@ -1,34 +1,54 @@
 import React, { Component } from 'react';
 import './index.less';
 import Assets from '@src/components/Assets';
-import { formatMoney } from '@src/services/Tools';
+import { formatMoney, formatDate } 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 { PayMVipEndModal } from '../PayModal';
 import { Main } from '../../stores/main';
 import { Order } from '../../stores/order';
+import { My } from '../../stores/my';
+import { User } from '../../stores/user';
+
 import { ServiceParamMap } from '../../../Constant';
 
 export default class extends Component {
   constructor(props) {
     super(props);
-    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.state = { tab: '2', pay: '', select: null };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.show && !this.init) {
+      this.init = true;
+      Main.getService('vip')
+        .then(result => {
+          result.package = result.package.map((row, index) => {
+            row.label = `${row.title}: ¥${formatMoney(row.price)}`;
+            row.value = ServiceParamMap.vip[index].value;
+            return row;
+          });
+          this.setState({ service: result });
         });
-        this.setState({ service: result });
-      });
+    }
+  }
+
+  changeTab(key) {
+    if (key === '1') {
+      // 自动选择第一个
+      this.select(ServiceParamMap.vip[0].value);
+    }
+    this.setState({ tab: key });
   }
 
   select(key) {
     Order.speedPay({ productType: 'service', service: 'vip', param: key }).then(result => {
-      this.setState({ order: result });
+      User.formatOrder(result);
+      const [checkout] = result.checkouts;
+      this.setState({ order: result, checkout });
       this.changePay('alipay');
     });
     this.setState({ select: key });
@@ -59,7 +79,8 @@ export default class extends Component {
   }
 
   queryPay() {
-    const { order, show } = this.state;
+    const { onClose, show } = this.props;
+    const { order } = this.state;
     if (this.time) {
       clearTimeout(this.time);
     }
@@ -68,21 +89,49 @@ export default class extends Component {
         .then(result => {
           if (result) {
             // 支付成功
-            this.setState();
+            this.paySuccess();
+            onClose();
           } else if (show) {
             this.queryPay();
           } else {
-            this.setState({ select: null, pay: null, order: null, info: null });
+            this.setState({ tab: '2', select: null, pay: null, order: {}, checkout: {} });
           }
         });
     }, 1000);
   }
 
+  paySuccess() {
+    const { order } = this.state;
+    const { data } = this.props;
+    Order.getOrder(order.id).then(result => {
+      User.formatOrder(result);
+      // 确保开通用的是record记录id
+      const [checkout] = result.checkouts;
+      checkout.info.result = checkout.info.result.replace('{email}', data.email).replace('{useExpireDays}', checkout.useExpireDays).replace('{title}', checkout.title);
+      if (checkout.service === 'vip') {
+        // 查询最后有效期
+        My.getVipInfo().then(vip => {
+          checkout.info.result = checkout.info.result.replace('{endTime}', formatDate(vip.expireTime, 'YYYY-MM-DD'));
+          this.setState({ show: false, showEnd: true, order: result, checkout });
+        });
+      } else {
+        this.setState({ show: false, showEnd: true, order: result, checkout });
+      }
+    });
+  }
+
+  close() {
+    const { onClose } = this.props;
+    this.setState({ tab: '2', showEnd: false, select: null, pay: null, order: {}, checkout: {} });
+    onClose();
+  }
+
   render() {
-    const { show, onClose } = this.props;
+    const { show } = this.props;
+    const { order, checkout, showEnd } = this.state;
     const { tab } = this.state;
-    return (
-      <Modal className="vip-renew-modal" show={show} width={630} title="VIP续期" onClose={onClose}>
+    return [
+      <Modal className="vip-renew-modal" show={show && !showEnd} width={630} title="VIP续期" onClose={() => this.close()}>
         <div className="vip-renew-wrapper">
           <Tabs
             border
@@ -90,42 +139,70 @@ export default class extends Component {
             active={tab}
             width={80}
             tabs={[{ key: '1', title: '购买' }, { key: '2', title: '免费领取' }]}
-            onChange={key => this.setState({ tab: key })}
+            onChange={key => this.changeTab(key)}
           />
           {this[`renderTab${tab}`]()}
         </div>
-      </Modal>
-    );
+      </Modal>,
+      showEnd && <PayMVipEndModal show={showEnd} order={order} checkout={checkout} onConfirm={() => this.close()} />,
+    ];
   }
 
   renderTab1() {
-    const { pay, select, service = {}, order } = this.state;
+    const { pay, select, service = {}, order = {}, checkout = {} } = this.state;
+    const { info = {} } = checkout;
     return (
       <div className="tab-1-layout">
-        <div className="select-layout">
-          <SpecialRadioGroup
-            list={service.package || []}
-            value={select}
-            width={150}
-            space={10}
-            onChange={key => this.select(key)}
-          />
-        </div>
-        <div className="pay-layout">
-          <Tabs
-            border
-            size="small"
-            active={pay}
-            width={80}
-            tabs={[{ key: 'alipay', title: '支付宝' }, { key: 'wechatpay', title: '微信' }]}
-            render={item => <Assets name={item.key} />}
-            onChange={key => this.changePay(key)}
-          />
-          <div className="qrcode">
-            <Assets name="qrcode" />
+        <div className="pay-modal-wrapper">
+          <div className="select-layout">
+            <SpecialRadioGroup
+              list={service.package || []}
+              value={select}
+              width={100}
+              space={10}
+              onChange={key => this.select(key)}
+            />
+            <div className="info-layout">
+              <div className="desc">
+                服务: {info.description}
+                <br />
+                开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}
+                <br />
+                使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}
+                <br />
+                退款政策: {info.refund_policy}
+                <br />
+                版权说明: {info.copyright_notes}
+              </div>
+              <div className="money">
+                <div className="t-2">应付金额:</div>
+                <div className="t-1 f-w-b t-s-24">¥ {order.money}</div>
+              </div>
+            </div>
+          </div>
+          <div className="pay-layout">
+            <Tabs
+              border
+              size="small"
+              active={pay}
+              width={80}
+              tabs={[{ key: 'alipay', title: '支付宝' }, { key: 'wechatpay', title: '微信' }]}
+              render={item => <Assets name={item.key} />}
+              onChange={key => this.changePay(key)}
+            />
+            <div className="qrcode">
+              <Assets name="qrcode" />
+            </div>
+            <div className="t">请使用手机微信或支付宝扫码付款</div>
+            {order && <div className="t">支付金额: ¥ {order.money}</div>}
+          </div>
+
+          <div style={{ bottom: 20, left: 0 }} className="p-a t-3 t-s-14">
+            *若在购买过程中遇到问题
+          </div>
+          <div style={{ bottom: 0, left: 0 }} className="p-a t-3 t-s-14">
+            请联系千行小助手:0193191safad 协助解决。
           </div>
-          <div className="t">请使用手机微信或支付宝扫码付款</div>
-          {order && <div className="t">支付金额: ¥ {order.money}</div>}
         </div>
       </div>
     );

+ 23 - 2
front/project/www/components/VipRenew/index.less

@@ -7,15 +7,36 @@
     .tab-1-layout {
       display: flex;
 
+      .pay-modal-wrapper {
+        display: flex;
+      }
+
       .select-layout {
-        flex: 1;
-        padding-top: 30px;
+        width: 375px;
+        padding-top: 14px;
 
         .g-special-radio-wrapper {
           margin-bottom: 15px;
+
+          .g-special-radio {
+            font-size: 12px;
+            padding: 10px;
+            white-space: pre;
+          }
+        }
+      }
+
+
+      .info-layout {
+        flex: 1;
+
+        .desc {
+          line-height: 27px;
+          margin-bottom: 12px;
         }
       }
 
+
       .pay-layout {
         width: 200px;
         padding-left: 10px;

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

@@ -22,6 +22,7 @@ function UserLayout(props) {
         <div className="center-layout">
           {center.length > 0 ? (
             center.map(item => {
+              if (!item) return null;
               return <div className="block-layout">{item}</div>;
             })
           ) : (
@@ -33,6 +34,7 @@ function UserLayout(props) {
         <div className="right-layout">
           {right.length > 0 ? (
             right.map(item => {
+              if (!item) return null;
               return <div className="block-layout">{item}</div>;
             })
           ) : (

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

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

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

@@ -1,9 +1,10 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { 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 { OpenConfirmModal } from '../../../components/OtherModal';
 import Tabs from '../../../components/Tabs';
 import Module from '../../../components/Module';
 import Division from '../../../components/Division';
@@ -112,6 +113,7 @@ export default class extends Page {
           const day = parseInt((new Date().getTime() - new Date(row.startDate).getTime()) / 86400000, 10);
           row.desc = [`最近换库:${formatDate(row.startDate, 'YYYY-MM-DD')} ,已换库${day}天`, `最后更新:${formatDate(row.updateTime)}`];
         }
+        if (row.unUseRecord) User.formatCheckout(row.unUseRecord);
         return row;
       });
       this.setState({ textbookProgress: result });
@@ -148,7 +150,7 @@ export default class extends Page {
         row.pieValue = formatPercent(row.userNumber, row.paperNumber);
         row.pieText = formatPercent(row.userNumber, row.paperNumber, false);
         row.pieSubText = `共${row.paperNumber}套`;
-
+        if (row.unUseRecord) User.formatCheckout(row.unUseRecord);
         return row;
       });
       this.setState({ examinationProgress: result });
@@ -183,7 +185,6 @@ export default class extends Page {
   // 开通模考或者机经
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refresh();
     });
   }
@@ -203,19 +204,31 @@ export default class extends Page {
   buyTextbook() {
     User.needLogin()
       .then(() => {
-
+        return Order.speedPay({ productType: 'service', service: 'textbook' });
+      })
+      .then((order) => {
+        return User.needPay(order);
+      })
+      .then(() => {
+        this.refresh();
       });
   }
 
   buyQxCat() {
     User.needLogin()
       .then(() => {
-
+        return Order.speedPay({ productType: 'service', service: 'qx_cat' });
+      })
+      .then((order) => {
+        return User.needPay(order);
+      })
+      .then(() => {
+        this.refresh();
       });
   }
 
   renderView() {
-    const { tab1, tab2, tabs } = this.state;
+    const { tab1, tab2, tabs, record } = this.state;
     const [subject] = tabs.filter(row => row.key === tab1);
     const children = (subject && subject.children) ? subject.children : [];
     return (
@@ -239,6 +252,8 @@ export default class extends Page {
 
           {this.state.faqs && <QAList data={this.state.faqs} active={'faq'} tabs={[{ key: 'faq', name: 'FAQs' }]} />}
         </div>
+
+        <OpenConfirmModal data={record} onCancel={() => this.setState({ record: null })} onConfirm={() => this.open(record.id)} />
       </div>
     );
   }
@@ -256,7 +271,7 @@ export default class extends Page {
                   col="3"
                   data={struct}
                   onOpen={() => {
-                    this.open(struct.unUseRecord.id);
+                    this.setState({ record: struct.unUseRecord });
                   }}
                 />;
               }
@@ -300,7 +315,7 @@ export default class extends Page {
                 title={struct.title}
                 data={struct}
                 onOpen={() => {
-                  this.open(struct.unUseRecord.id);
+                  this.setState({ record: struct.unUseRecord });
                 }}
               />;
             }

+ 7 - 3
front/project/www/routes/exercise/main/page.js

@@ -3,7 +3,7 @@ import './index.less';
 import { Modal } from 'antd';
 import { Link } from 'react-router-dom';
 import Page from '@src/containers/Page';
-import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
+import { asyncConfirm } from '@src/services/AsyncTools';
 import { formatTreeData, formatSeconds, formatDate, formatPercent, getMap } from '@src/services/Tools';
 import Continue from '../../../components/Continue';
 import Step from '../../../components/Step';
@@ -305,6 +305,10 @@ export default class extends Page {
       this.inited = true;
       this.refreshData();
     });
+    Main.getContract('course')
+      .then(result => {
+        this.setState({ contract: result });
+      });
   }
 
   initData() {
@@ -593,7 +597,6 @@ export default class extends Page {
   // 开通课程
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refresh();
     });
   }
@@ -723,7 +726,7 @@ export default class extends Page {
   }
 
   renderPreviewCourse() {
-    const { courseStructs, struct, tab2, courseTabs, courseMap = {} } = this.state;
+    const { courseStructs, struct, tab2, courseTabs, courseMap = {}, contract = {} } = this.state;
     return (
       <div className="work-body">
         <div className="work-nav" hidden={courseTabs && courseTabs.length > 0 && tab2 !== courseTabs[0].key}>
@@ -772,6 +775,7 @@ export default class extends Page {
             return <Card1
               title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
               tag={CourseModuleMap[row.course.courseModule]}
+              contract={contract}
               status='open'
               data={row}
               onOpen={() => {

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

@@ -158,7 +158,7 @@ export default class extends Page {
     this.initData();
   }
 
-  onAction() {}
+  onAction() { }
 
   onSelect(selectList) {
     this.setState({ selectList });
@@ -295,7 +295,7 @@ export default class extends Page {
                 )}
                 {item.answerStatus > 0 && (
                   <div className="desc">
-                    <OpenText>{item.answer}</OpenText>
+                    <OpenText>{item.answerStatus === 2 ? '与题目内容无关,老师无法作出回答,敬请谅解。' : item.answer}</OpenText>
                   </div>
                 )}
               </div>

+ 48 - 82
front/project/www/routes/my/course/page.js

@@ -19,6 +19,7 @@ import More from '../../../components/More';
 import Modal from '../../../components/Modal';
 import DatePlane from '../../../components/Date';
 import Note from '../../../components/Note';
+import { FinishModal, CommentModal } from '../../../components/OtherModal';
 import { My } from '../../../stores/my';
 import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
@@ -131,7 +132,7 @@ export default class extends Page {
     });
   }
 
-  onAction() {}
+  onAction() { }
 
   onTabChange(tab) {
     const data = { tab };
@@ -236,16 +237,8 @@ export default class extends Page {
     });
   }
 
-  submitComment() {
-    const { comment } = this.state;
-    My.addComment(comment.channel, comment.position, comment.content).then(() => {
-      this.setState({ showComment: false, showFinish: true, comment: {} });
-    });
-  }
-
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refreshDetail(recordId);
     });
   }
@@ -367,7 +360,7 @@ export default class extends Page {
           <DatePlane
             hideInput
             show={showTime}
-            onChange={() => {}}
+            onChange={() => { }}
             disabledDate={current => {
               const date = current.format('YYYY-MM-DD');
               return data.stopTimeMap[date];
@@ -392,36 +385,6 @@ export default class extends Page {
           <div className="t-2 t-s-12">*听课频率≤2天/课时,作业完成度≥90%,课程有效期可延长7-10天。</div>
         </Modal>
         <Modal
-          show={showComment}
-          title="评价"
-          onConfirm={() => comment.content && this.submitComment()}
-          onCancel={() => this.setState({ showComment: false, comment: {} })}
-        >
-          <textarea
-            value={comment.content}
-            className="b-c-1 w-10 p-10"
-            rows={6}
-            placeholder="您的看法对我们来说很重要!"
-            onChange={e => {
-              comment.content = e.target.value;
-              this.setState({ comment });
-            }}
-          />
-          <div className="b-b m-t-2" />
-        </Modal>
-        <Modal
-          show={showFinish}
-          title="提交成功"
-          confirmText="好的,知道了"
-          btnAlign="center"
-          onConfirm={() => this.setState({ showFinish: false })}
-        >
-          <div className="t-2 t-s-18">
-            <Icon type="check" className="t-5 m-r-5" />
-            您的每一次反馈都是千行进步的动力。
-          </div>
-        </Modal>
-        <Modal
           show={showSuspend}
           title="申请停课"
           width={630}
@@ -555,6 +518,18 @@ export default class extends Page {
           </div>
           <div className="b-b m-t-2" />
         </Modal>
+
+        <CommentModal
+          show={showComment}
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div>
     );
   }
@@ -713,8 +688,7 @@ class CourseOnline extends Component {
         render: (text, record) => {
           const { paper = {} } = record;
           return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${
-            paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'
-          }`;
+            paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'}`;
         },
       },
       {
@@ -1169,9 +1143,7 @@ class CourseVs extends Component {
           render: (text, record) => {
             return record.noteList && record.noteList.length > 0 ? (
               <a onClick={() => this.props.onNote(record)}>查看</a>
-            ) : (
-              <span>查看</span>
-            );
+            ) : (<span>查看</span>);
           },
         },
         {
@@ -1180,9 +1152,7 @@ class CourseVs extends Component {
           render: (text, record) => {
             return record.supplyList && record.supplyList.length > 0 ? (
               <a onClick={() => this.props.onSupply(record)}>查看</a>
-            ) : (
-              <span>查看</span>
-            );
+            ) : (<span>查看</span>);
           },
         },
       ],
@@ -1667,21 +1637,19 @@ class TimeLineItem extends Component {
                   CC talk使用手册
                 </a>
               </span>
-            ) : (
-              <div>
-                <input
-                  style={{ width: 200 }}
-                  className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
-                  placeholder="请输入CCtalk用户名查看授课频道"
-                  onChange={e => {
-                    this.setState({ cctalkName: e.target.value });
-                  }}
-                />
-                <Button size="small" radius disabled>
-                  提交
+            ) : (<div>
+              <input
+                style={{ width: 200 }}
+                className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
+                placeholder="请输入CCtalk用户名查看授课频道"
+                onChange={e => {
+                  this.setState({ cctalkName: e.target.value });
+                }}
+              />
+              <Button size="small" radius disabled>
+                提交
                 </Button>
-              </div>
-            );
+            </div>);
           case 'not':
             return data.cctalkName ? (
               <span>
@@ -1690,27 +1658,25 @@ class TimeLineItem extends Component {
                   CC talk使用手册
                 </a>
               </span>
-            ) : (
-              <div>
-                <input
-                  style={{ width: 200 }}
-                  className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
-                  placeholder="请输入CCtalk用户名查看授课频道"
-                  onChange={e => {
-                    this.setState({ cctalkName: e.target.value });
-                  }}
-                />
-                <Button
-                  size="small"
-                  radius
-                  onClick={() => {
-                    if (this.state.cctalkName) setCCTalkName(appointment, this.state.cctalkName);
-                  }}
-                >
-                  提交
+            ) : (<div>
+              <input
+                style={{ width: 200 }}
+                className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
+                placeholder="请输入CCtalk用户名查看授课频道"
+                onChange={e => {
+                  this.setState({ cctalkName: e.target.value });
+                }}
+              />
+              <Button
+                size="small"
+                radius
+                onClick={() => {
+                  if (this.state.cctalkName) setCCTalkName(appointment, this.state.cctalkName);
+                }}
+              >
+                提交
                 </Button>
-              </div>
-            );
+            </div>);
           default:
             return (
               <span>

+ 27 - 5
front/project/www/routes/my/main/page.js

@@ -128,7 +128,7 @@ class LogItem extends Component {
           data={detail}
         />
         <div className="t-r">
-          <Link to="/course">继续学习></Link>
+          {data.isCourse ? <Link to="/my/course">{'继续学习>'}</Link> : <Link to="/course">{'去购买'}</Link>}
         </div>
       </div>
     );
@@ -366,7 +366,7 @@ export default class extends Page {
         active={config.key}
         menu={menu}
         center={[this.renderTop(), this.renderLog(), this.renderTime()]}
-        right={[this.renderInfo(), this.renderMessage()]}
+        right={[this.renderInfo(), this.renderVip(), this.renderMessage()]}
         ads={(this.state.ads || []).map(row => {
           return (
             <a href={row.link} target="_blank">
@@ -379,7 +379,8 @@ export default class extends Page {
   }
 
   renderTop() {
-    return null; // <div className="total-layout">1</div>;
+    const { info } = this.props.user;
+    return !info.bindPrepare && <div className="total-layout" onClick={() => this.setState({ showExamination: true })}><Assets /></div>;
   }
 
   renderLog() {
@@ -577,8 +578,9 @@ export default class extends Page {
               }}
             />
           </div>
+          {!info.bindReal && <div className="t-3 t-s-12 m-t-1">完成实名认证送6个月VIP <a onClick={() => this.setState({ showReal: true })}>去完成</a></div>}
         </div>
-        {
+        {info.vip &&
           <div className="footer">
             <Assets className="m-r-5" name="VIP" />
             {info.vip && <span className="date">{formatDate(info.vip, 'YYYY-MM-DD')}到期</span>}
@@ -639,6 +641,26 @@ export default class extends Page {
     );
   }
 
+  renderVip() {
+    const { info } = this.props.user;
+    return !info.vip && <div className="info-layout">
+      <div className="body">
+        开通<Assets className="m-r-5" name="VIP" />解锁海量权限
+      </div>
+      <div className="footer">
+        <Button
+          radius
+          size="small"
+          onClick={() => {
+            this.setState({ showVip: true });
+          }}
+        >
+          立即开通
+            </Button>
+      </div>
+    </div>;
+  }
+
   renderMessage() {
     const { messages = [] } = this.state;
     const number = (messages || []).length;
@@ -658,7 +680,7 @@ export default class extends Page {
             {(messages || []).map(row => {
               return (
                 <div className="item">
-                  <div className="title dot">老师回答了您的提问</div>
+                  <div className="title dot">{row.title}</div>
                   <div className="date">{formatDate(row.createTime, 'YYYY-MM-DD HH:mm:ss')}</div>
                   {row.link && (
                     <GIcon

+ 6 - 21
front/project/www/routes/my/order/page.js

@@ -11,26 +11,10 @@ import Modal from '../../../components/Modal';
 import More from '../../../components/More';
 import IconButton from '../../../components/IconButton';
 import { Order } from '../../../stores/order';
-import { RecordSource, ServiceKey, InvoiceType } from '../../../../Constant';
+import { RecordSource, InvoiceType } from '../../../../Constant';
+import { User } from '../../../stores/user';
 
 const RecordSourceMap = getMap(RecordSource, 'value', 'label');
-const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
-
-function formatTitle(record) {
-  if (record.productType === 'course-package') {
-    return (record.coursePackage || {}).title;
-  }
-  if (record.productType === 'course') {
-    return (record.course || {}).title;
-  }
-  if (record.productType === 'data') {
-    return (record.data || {}).title;
-  }
-  if (record.productType === 'service') {
-    return `${ServiceKeyMap[record.service]}`;
-  }
-  return '';
-}
 
 export default class extends Page {
   constructor(props) {
@@ -68,7 +52,7 @@ export default class extends Page {
                 });
                 break;
               case 'detail':
-                openLink(`/order/detail/${record.id}`);
+                openLink(`/order/${record.id}`);
                 break;
               default:
             }
@@ -78,7 +62,7 @@ export default class extends Page {
             content.push(
               <div className="flex-layout m-b-5">
                 <div className="flex-block">
-                  {formatTitle(record.checkouts[0])}
+                  {record.checkouts[0].title}
                   <br />等{record.checkouts.length}个商品
                 </div>
                 {actionList.length > 0 && <More menu={actionList} onClick={onAction}>
@@ -90,7 +74,7 @@ export default class extends Page {
             content = record.checkouts.map((row, index) => {
               return (
                 <div className="flex-layout m-b-5">
-                  <div className="flex-block">{formatTitle(row)}</div>
+                  <div className="flex-block">{row.title}</div>
                   {index === 0 && actionList.length > 0 && (
                     <More menu={actionList} onClick={onAction}>
                       <IconButton type="more" />
@@ -150,6 +134,7 @@ export default class extends Page {
     Order.list(this.state.search).then(result => {
       result.list = result.list.map(row => {
         row.checkouts = row.checkouts || [];
+        User.formatOrder(row);
         row.createTime = formatDate(row.createTime, 'YYYY-MM-DD HH:mm:ss');
         return row;
       });

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

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

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

@@ -2,8 +2,7 @@ export default {
   path: '/cart',
   key: 'cart',
   title: '购物车',
-  needLogin: false,
-  tab: 'main',
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 228 - 90
front/project/www/routes/page/cart/page.js

@@ -3,59 +3,167 @@ import './index.less';
 import { Icon, Radio } from 'antd';
 import Assets from '@src/components/Assets';
 import Page from '@src/containers/Page';
+import { asyncSMessage } from '@src/services/AsyncTools';
 import CheckboxItem from '../../../components/CheckboxItem';
 import Button from '../../../components/Button';
+import Modal from '../../../components/Modal';
+import UserTable from '../../../components/UserTable';
+import { Order } from '../../../stores/order';
+import { Course } from '../../../stores/course';
+import { User } from '../../../stores/user';
+import { getMap } from '../../../../../src/services/Tools';
+import { CourseVsType } from '../../../../Constant';
+
+const CourseVsTypeMap = getMap(CourseVsType, 'value', 'tips');
 
 export default class extends Page {
+  initState() {
+    return { courses: [] };
+  }
+
+  init() {
+    Course.allVs()
+      .then(result => {
+        // 赠送选择选择
+        this.setState({ courses: result.filter(row => ['novice', 'coach'].indexOf(row.vsType) >= 0) });
+      });
+  }
+
+  initData() {
+    Order.getOrder(34)
+      .then((order) => {
+        User.needPay(order);
+      });
+    Order.allCheckout()
+      .then(result => {
+        User.formatOrder(result);
+        this.setState({ order: result, list: result.checkouts });
+      });
+  }
+
+  onAll(checked) {
+    const selectList = [];
+    if (checked) {
+      const { list = [] } = this.state;
+      list.forEach(item => {
+        if (selectList.indexOf(item.key) >= 0) return;
+        selectList.push(item.key);
+      });
+    }
+    this.setState({ selectList, allChecked: checked });
+  }
+
+  onSelect(key, checked) {
+    const { selectList = [] } = this.state;
+    if (checked) {
+      selectList.push(key);
+    } else {
+      selectList.splice(selectList.indexOf(key), 1);
+    }
+    this.setState({ selectList });
+  }
+
+  onDelete(list) {
+    Promise.all(list.map(row => {
+      return Order.removeCheckout(row);
+    }))
+      .then(() => {
+        this.refresh();
+      });
+  }
+
+  onChangeNumber(id, number) {
+    Order.changeCheckout(id, number)
+      .then(() => {
+        this.refresh();
+      });
+  }
+
+  pay() {
+    const { courseId, list, order } = this.state;
+    if (list.length === 0) return;
+    if (order.gift && order.gift.filter(row => row.key === 'vs').length > 0 && !courseId) {
+      // 请选择课程
+      asyncSMessage('请选择赠送的课程', 'warn');
+      return;
+    }
+    Order.confirmPay(courseId)
+      .then(result => {
+        return User.needPay(result)
+          .then(() => {
+            linkTo(`/order/${result.id}`);
+          })
+          .catch(() => {
+            this.refresh();
+          });
+      });
+  }
+
   renderView() {
-    const { _list = [{}, {}, {}] } = this.state;
+    const { courses = [], order = {}, list = [], allChecked, selectList = [], courseId } = this.state;
     return (
       <div style={{ paddingTop: 50 }}>
         <div className="content">
           <div className="t-1 m-b-2 f-w-b t-s-24">购物车</div>
         </div>
-        <div className="content">
-          <div className="m-b-1 t-8">您看中的课程已构成套餐,结算有优惠</div>
-        </div>
         <div className="list content">
-          {_list.map(item => {
-            return <OrderItem data={item} />;
+          {(list || []).map(item => {
+            return [
+              item.productType === 'course_package' && <div className="content">
+                <div className="m-b-1 t-8">您看中的课程已构成套餐,结算有优惠</div>
+              </div>,
+              <OrderItem
+                data={item}
+                checked={selectList.indexOf(item.id) >= 0}
+                onDelete={() => this.onDelete([item.id])}
+                onCheck={(checked) => this.onSelect(item.id, checked)}
+                onChangeNumber={(number) => this.onChangeNumber(item.id, number)}
+              />,
+            ];
           })}
         </div>
         <div className="gift-list content">
-          {_list.map(item => {
-            return <GiftItem data={item} />;
+          {(order.gift || []).map(item => {
+            return <GiftItem
+              data={item}
+              courses={courses}
+              courseId={courseId}
+              onSelectCourse={(id) => item.key === 'vs' && this.setState({ courseId: id })}
+            />;
           })}
         </div>
         <div className="footer">
           <div className="content">
             <div className="d-i-b m-t-1 m-r-2">
-              <CheckboxItem className="v-a-m m-r-5" theme="white" checked />
+              <CheckboxItem className="v-a-m m-r-5" theme="white" checked={allChecked} onClick={() => this.onAll(!allChecked)} />
               全选
             </div>
             <div style={{ marginRight: 50 }} className="d-i-b m-t-1">
-              <Button size="small" radius disabled={false}>
+              <Button size="small" radius disabled={selectList.length === 0} onClick={() => this.onDelete(selectList)}>
                 删除
               </Button>
             </div>
-            <div style={{ marginTop: 15 }} className="d-i-b t-9 t-s-12 m-r-5">
-              优惠活动:
-            </div>
-            <div style={{ marginTop: 15 }} className="d-i-b t-s-12">
-              <div>单项课程 2门9折,3门88折,4门及以上85折。</div>
-              <div>1VS1私教 满30课时享95折优惠。</div>
-            </div>
+            {order.promote && order.promote.length > 0 && [
+              <div style={{ marginTop: 15 }} className="d-i-b t-9 t-s-12 m-r-5">
+                优惠活动:
+            </div>,
+              <div style={{ marginTop: 15 }} className="d-i-b t-s-12">
+                {order.promote.map(row => {
+                  return <div>{row.message}</div>;
+                })}
+              </div>,
+            ]}
             <div className="f-r">
               <div className="d-i-b m-r-1 m-t-1">
                 <div className="t-1 t-s-16 f-w-b">
-                  实付<span className="m-l-5 t-7 t-s-20"> ¥ 15000</span>
+                  实付<span className="m-l-5 t-7 t-s-20"> ¥ {order.money || 0}</span>
                 </div>
                 <div className="t-1">
-                  原价<span className="m-l-5 t-8 t-d-l-t"15200</span>
-                  <span className="m-l-5  t-8">(优惠活动-¥200)</span>
+                  原价<span className="m-l-5 t-8 t-d-l-t" {order.originMoney || 0}</span>
+                  <span hidden={!((order.originMoney || 0) - (order.money || 0))} className="m-l-5  t-8">(优惠活动-¥{(order.originMoney || 0) - (order.money || 0)}</span>
                 </div>
               </div>
-              <Button className="submit">立即付款</Button>
+              <Button className="submit" onClick={() => this.pay()}>立即付款</Button>
             </div>
           </div>
         </div>
@@ -66,54 +174,57 @@ export default class extends Page {
 
 class GiftItem extends Component {
   render() {
+    const { data } = this.props;
     return (
       <div className="gift-item">
         {this.renderInfo()}
-        {this.renderDetail()}
+        {data.key === 'vs' && this.renderVs()}
       </div>
     );
   }
 
   renderInfo() {
+    const { data } = this.props;
+    let message = '';
+    if (data.money) {
+      message = `实付金额满${data.money}元赠送`;
+    } else if (data.from === 'vsNumber') {
+      message = `1VS1课程满${data.number}节赠送`;
+    }
     return (
       <div className="gift-item-info">
         <Assets name="gift2" className="gift" width={20} height={20} />
-        <div style={{ width: 360 }} className="d-i-b t-1 t-s-16">
-          OG 20 套餐
+        <div style={{ width: 350 }} className="d-i-b t-1 t-s-16">
+          {data.message}
         </div>
         <div style={{ width: 400 }} className="d-i-b t-8 t-s-12">
-          实付金额满20000元赠送
+          {message}
         </div>
       </div>
     );
   }
 
-  renderDetail() {
+  renderVs() {
+    const { courses, courseId, onSelectCourse } = this.props;
     return (
       <div className="gift-item-detail l-h-20">
         <div className="select">
-          <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
+          <div style={{ width: 350 }} className="d-i-b t-1 t-s-12">
             <span className="d-i-b f-w-b m-r-5">请选择</span>
           </div>
           <div className="d-i-b t-1 t-s-12">可至「个人中心-课程」预约辅导时间。</div>
         </div>
         <div className="select-list m-b-5 l-h-16">
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">
-                <Radio className="m-r-5" /> OG 20 语法 SC
-              </span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">适合未参加过实战的考生</div>
-          </div>
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">
-                <Radio className="m-r-5" /> OG 20 语法 SC
-              </span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">适合未参加过实战的考生</div>
-          </div>
+          {courses.map(course => {
+            return <div>
+              <div style={{ width: 350 }} className="d-i-b t-1 t-s-12">
+                <span className="d-i-b m-r-5">
+                  <Radio className="m-r-5" checked={courseId === course.id} onChange={() => onSelectCourse(course.id)} /> {course.title}
+                </span>
+              </div>
+              <div className="d-i-b t-8 t-s-12">{CourseVsTypeMap[course.vsType]}</div>
+            </div>;
+          })}
         </div>
       </div>
     );
@@ -123,45 +234,84 @@ class GiftItem extends Component {
 class OrderItem extends Component {
   constructor(props) {
     super(props);
-    this.state = { open: false };
+    this.state = { open: false, showList: false };
+    this.columns = [{
+      key: 'label',
+      title: '服务',
+    }, {
+      key: 'expireDays',
+      title: '开通有效期',
+      render: (text, checkout) => {
+        return checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效';
+      },
+    }, {
+      key: 'useExpireDays',
+      title: '使用有效期',
+      render: (text, checkout) => {
+        return checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久';
+      },
+    }];
   }
 
   render() {
+    const { data } = this.props;
+    const { showList } = this.state;
     return (
       <div className="order-item">
         {this.renderInfo()}
-        {this.renderDetail()}
+        {(data.children || data.gift) && this.renderDetail()}
+
+        <Modal
+          show={showList}
+          maskClosable
+          close={false}
+          body={false}
+          width={630}
+          onClose={() => this.setState({ showList: false })}
+        >
+          <UserTable
+            size="small"
+            theme="top"
+            columns={this.columns}
+            data={data.gift}
+          />
+        </Modal>
       </div>
     );
   }
 
   renderInfo() {
+    const { data, onCheck, onDelete, onChangeNumber, checked } = this.props;
+    const checkout = data;
     return (
       <div className="order-item-info">
-        <CheckboxItem theme="white" className="select" />
-        <Icon className="close" type="close-circle" theme="filled" />
-        <div style={{ width: 360 }} className="d-i-b t-1 t-s-16">
-          OG 20 套餐
+        <CheckboxItem theme="white" className="select" checked={checked} onClick={() => onCheck(!checked)} />
+        <Icon className="close" type="close-circle" theme="filled" onClick={() => onDelete()} />
+        <div style={{ width: 350 }} className="d-i-b t-1 t-s-16">
+          {data.title}
         </div>
-        <div style={{ width: 400 }} className="d-i-b t-8 t-s-12">
-          实付金额满20000元赠送
+        <div style={{ width: 430 }} className="d-i-b t-8 t-s-12">
+          <span className="m-r-2">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}</span>
+          <span className="m-l-2">使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
         </div>
         <div style={{ width: 120 }} className="d-i-b t-8 t-s-12 p-r">
-          数量
-          <input style={{ width: 32 }} className="m-l-5 t-c" />
-          <Icon className="up" type="caret-up" />
-          <Icon className="down" type="caret-down" />
+          {data.number > 0 && ['数量',
+            <input value={data.number} style={{ width: 32 }} className="m-l-5 t-c" />,
+            <Icon className="up" type="caret-up" onClick={() => data.number < data.maxNumber && onChangeNumber(data.number + 1)} />,
+            <Icon className="down" type="caret-down" onClick={() => data.number > 1 && data.number > data.minNumber && onChangeNumber(data.number - 1)} />]}
         </div>
-        <div className="d-i-b t-7 t-s-16"> ¥ 15000</div>
+        <div className="d-i-b t-7 t-s-16"> ¥ {data.money}</div>
       </div>
     );
   }
 
   renderDetail() {
+    const { data = {} } = this.props;
+    const { children = [], gift } = data;
     const { open } = this.state;
     return (
       <div className="order-item-detail l-h-20">
-        <div className="contain">
+        <div hidden={!children || children.length === 0} className="contain">
           <div style={{ width: 880 }} className="d-i-b t-1 t-s-12">
             <span className="d-i-b f-w-b m-r-5">
               包含
@@ -179,49 +329,37 @@ class OrderItem extends Component {
               />
             </span>
             <span hidden={open} style={{ width: 300 }} className="d-i-b nowrap">
-              OG 20 语法 SC +OG 20 语法 SC +OG 20 语法 SC +OG 20 语法 SC +
+              {(children || []).map(checkout => checkout.title).join(' + ')}
             </span>
           </div>
-          <div className="d-i-b t-1 t-s-12 t-d-l-t"> ¥ 123022</div>
+          <div className="d-i-b t-1 t-s-12 t-d-l-t"> ¥ {data.originMoney}</div>
         </div>
         <div hidden={!open} className="contain-list m-b-5 l-h-16">
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
+          {(children || []).map(checkout => {
+            return <div>
+              <div style={{ width: 350 }} className="d-i-b t-1 t-s-12">
+                <span className="d-i-b m-r-5">{checkout.title}</span>
+              </div>
+              <div className="d-i-b t-8 t-s-12">
+                <span className="m-r-2">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}</span>
+                <span className="m-l-2">使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
+              </div>
+            </div>;
+          })}
         </div>
-        <div className="service">
+        {gift && <div className="service">
           <div className="d-i-b t-1 t-s-12">
             <span className="d-i-b f-w-b m-r-5">赠送服务</span>
             <span className="d-i-b">
-              机经券×1+VIP×3 月+模考×1
-              <Icon className="m-l-5 close" type="exclamation-circle" theme="filled" />
+              {gift.map(row => {
+                return `${row.label}x${row.param ? row.param.label : row.number}`;
+              }).join(' ')}
+              <Icon className="m-l-5 close" type="exclamation-circle" theme="filled" onClick={() => {
+                this.setState({ showList: true });
+              }} />
             </span>
           </div>
-        </div>
+        </div>}
       </div>
     );
   }

+ 9 - 0
front/project/www/routes/page/order/index.js

@@ -0,0 +1,9 @@
+export default {
+  path: '/order/:id',
+  key: 'order',
+  title: '订单详情',
+  needLogin: true,
+  component() {
+    return import('./page');
+  },
+};

+ 113 - 55
front/project/www/routes/page/order/page.js

@@ -3,25 +3,41 @@ import './index.less';
 import { Icon } from 'antd';
 import Assets from '@src/components/Assets';
 import Page from '@src/containers/Page';
+import Modal from '../../../components/Modal';
+import UserTable from '../../../components/UserTable';
+import IconButton from '../../../components/IconButton';
+import { Order } from '../../../stores/order';
+import { User } from '../../../stores/user';
+import { formatDate } from '../../../../../src/services/Tools';
+// import { ServiceKey, ServiceParamMap, OrderInfoMap } from '../../../../Constant';
 
 export default class extends Page {
+  initData() {
+    const { id } = this.params;
+    Order.getOrder(id)
+      .then(result => {
+        User.formatOrder(result);
+        this.setState({ order: result, list: result.checkouts });
+      });
+  }
+
   renderView() {
-    const { _list = [{}, {}, {}] } = this.state;
+    const { order = {}, list = [] } = this.state;
     return (
       <div style={{ paddingTop: 40 }}>
         <div className="content">
           <div className="t-1 m-b-2 f-w-b t-s-24">订单详情</div>
         </div>
         <div className="content">
-          <div className="m-b-1 t-8 m-b-2">订单编号 34310431-0514-05265</div>
+          <div className="m-b-1 t-8 m-b-2">订单编号 {order.id}</div>
         </div>
         <div className="list content">
-          {_list.map(item => {
+          {(list || []).map(item => {
             return <OrderItem data={item} />;
           })}
         </div>
         <div className="gift-list content">
-          {_list.map(item => {
+          {(order.gift || []).map(item => {
             return <GiftItem data={item} />;
           })}
         </div>
@@ -29,15 +45,15 @@ export default class extends Page {
           <div className="t-1 t-s-18 f-w-b title">订单金额</div>
           <div className="t-2 t-s-12 item">
             <span>应付金额</span>
-            <span>¥ 21200.0</span>
+            <span>¥ {order.originMoney}</span>
           </div>
-          <div className="t-2 t-s-12 item">
+          {order.originMoney - order.money > 0 && <div className="t-2 t-s-12 item">
             <span>优惠金额</span>
-            <span>¥ 21200.0</span>
-          </div>
+            <span>¥ {order.originMoney - order.money}</span>
+          </div>}
           <div className="t-2 t-s-12 item">
             <span>实付金额</span>
-            <span>¥ 21200.0</span>
+            <span>¥ {order.money}</span>
           </div>
         </div>
         <div className="content block">
@@ -45,13 +61,13 @@ export default class extends Page {
           <div className="t-2 t-s-12 item">
             <span>支付方式</span>
             <span>
-              <Assets name="alipay" />
+              {order.payMethod && <Assets name={order.payMethod === 'wechat' ? 'wechatpay' : order.payMethod} />}
               {/* alipay wechatpay bank */}
             </span>
           </div>
           <div className="t-2 t-s-12 item">
             <span>付款时间</span>
-            <span>2019-03-23 10:00:02</span>
+            <span>{order.payTime && formatDate(order.payTime, 'YYYY-MM-DD HH:mm:ss')}</span>
           </div>
         </div>
       </div>
@@ -61,14 +77,21 @@ export default class extends Page {
 
 class GiftItem extends Component {
   render() {
+    const { data } = this.props;
+    let message = '';
+    if (data.money) {
+      message = `实付金额满${data.money}元赠送`;
+    } else if (data.from === 'vsNumber') {
+      message = `1VS1课程满${data.number}节赠送`;
+    }
     return (
       <div className="gift-item">
         <div className="d-i-b t-2 t-s-12 m-l-2">
           <Assets className="m-r-5" width={20} height={20} name="gift2" />
-          16小时极速问答权限
+          {data.message}
         </div>
         <div style={{ marginRight: 80 }} className="d-i-b t-2 t-s-12 f-r">
-          实付金额满20000元赠送
+          {message}
         </div>
       </div>
     );
@@ -78,41 +101,88 @@ class GiftItem extends Component {
 class OrderItem extends Component {
   constructor(props) {
     super(props);
-    this.state = { open: false };
+    this.state = { open: false, showList: false };
+    this.columns = [{
+      key: 'label',
+      title: '服务',
+    }, {
+      key: 'expireDays',
+      title: '开通有效期',
+      render: (text, checkout) => {
+        return checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效';
+      },
+    }, {
+      key: 'useExpireDays',
+      title: '使用有效期',
+      render: (text, checkout) => {
+        return checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久';
+      },
+    }];
   }
 
   render() {
+    const { data = {} } = this.props;
+    const { showList } = this.state;
     return (
       <div className="order-item">
         {this.renderInfo()}
-        {this.renderDetail()}
+        {(data.children || data.gift) && this.renderDetail()}
+
+        <Modal
+          show={showList}
+          maskClosable
+          close={false}
+          body={false}
+          width={630}
+          onClose={() => this.setState({ showList: false })}
+        >
+          <UserTable
+            size="small"
+            theme="top"
+            columns={this.columns}
+            data={data.gift}
+          />
+        </Modal>
       </div>
     );
   }
 
   renderInfo() {
+    const { data } = this.props;
+    const checkout = data;
     return (
       <div className="order-item-info">
         <div style={{ width: 360 }} className="d-i-b t-1 t-s-16">
-          OG 20 套餐
+          {data.title}
+        </div>
+        <div style={{ width: 530 }} hidden={checkout.productType === 'data'} className="d-i-b t-8 t-s-12">
+          <span className="m-r-2">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}</span>
+          <span className="m-l-2">使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
         </div>
-        <div style={{ width: 530 }} className="d-i-b t-8 t-s-12">
-          实付金额满20000元赠送
+        <div style={{ width: 530 }} hidden={checkout.productType !== 'data'} className="d-i-b t-8 t-s-12">
+          <IconButton
+            type="download"
+            onClick={() => {
+              openLink(checkout.data.resource);
+            }}
+          />
         </div>
         <div style={{ width: 120 }} className="d-i-b t-s-12 p-r">
-          <span className="t-8 m-r-2">数量</span>
-          <span className="t-1">1</span>
+          {data.number > 0 && <span className="t-8 m-r-2">数量</span>}
+          {data.number > 0 && <span className="t-1">{data.number}</span>}
         </div>
-        <div className="d-i-b t-7 t-s-16"> ¥ 15000</div>
+        <div className="d-i-b t-7 t-s-16"> ¥ {data.money}</div>
       </div>
     );
   }
 
   renderDetail() {
+    const { data = {} } = this.props;
+    const { children = [], gift } = data;
     const { open } = this.state;
     return (
       <div className="order-item-detail l-h-20">
-        <div className="contain">
+        <div hidden={!children || children.length === 0} className="contain">
           <div style={{ width: 1010 }} className="d-i-b t-1 t-s-12">
             <span className="d-i-b f-w-b m-r-5">
               包含
@@ -130,49 +200,37 @@ class OrderItem extends Component {
               />
             </span>
             <span hidden={open} style={{ width: 300 }} className="d-i-b nowrap">
-              OG 20 语法 SC +OG 20 语法 SC +OG 20 语法 SC +OG 20 语法 SC +
+              {(children || []).map(checkout => checkout.title).join(' + ')}
             </span>
           </div>
-          <div className="d-i-b t-1 t-s-12 t-d-l-t"> ¥ 123022</div>
+          <div className="d-i-b t-1 t-s-12 t-d-l-t"> ¥ {data.originMoney}</div>
         </div>
         <div hidden={!open} className="contain-list m-b-5 l-h-16">
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
-          <div>
-            <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
-              <span className="d-i-b m-r-5">OG 20 语法 SC</span>
-            </div>
-            <div className="d-i-b t-8 t-s-12">
-              <span className="m-r-2">开通有效期: 3个月</span>
-              <span className="m-l-2">使用有效期: 3个月</span>
-            </div>
-          </div>
+          {(children || []).map(checkout => {
+            return <div>
+              <div style={{ width: 360 }} className="d-i-b t-1 t-s-12">
+                <span className="d-i-b m-r-5">{checkout.title}</span>
+              </div>
+              <div className="d-i-b t-8 t-s-12">
+                <span className="m-r-2">开通有效期: {checkout.expireDays ? `${checkout.expireDays}天` : '付款后立即生效'}</span>
+                <span className="m-l-2">使用有效期: {checkout.useExpireDays ? `${checkout.useExpireDays}天` : '永久'}</span>
+              </div>
+            </div>;
+          })}
         </div>
-        <div className="service">
+        {gift && <div className="service">
           <div className="d-i-b t-1 t-s-12">
             <span className="d-i-b f-w-b m-r-5">赠送服务</span>
             <span className="d-i-b">
-              机经券×1+VIP×3 月+模考×1
-              <Icon className="m-l-5 close" type="exclamation-circle" theme="filled" />
+              {gift.map(row => {
+                return `${row.label}x${row.param ? row.param.label : row.number}`;
+              }).join(' ')}
+              <Icon className="m-l-5 close" type="exclamation-circle" theme="filled" onClick={() => {
+                this.setState({ showList: true });
+              }} />
             </span>
           </div>
-        </div>
+        </div>}
       </div>
     );
   }

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

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

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

@@ -16,6 +16,10 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/simple', { courseId });
   }
 
+  listAsk({ keyword, courseId, courseNoId, position, order, direction }) {
+    return this.apiGet('/course/ask/list', { keyword, courseId, courseNoId, position, order, direction });
+  }
+
   noProgress(courseId, courseNoId, progress, time, currentCourseNoId) {
     return this.apiPut('/course/no/progress', { courseId, courseNoId, progress, time, currentCourseNoId });
   }

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

@@ -17,6 +17,13 @@ export default class MainStore extends BaseStore {
   }
 
   /**
+   * 获取基础配置
+   */
+  getBase() {
+    return this.apiGet('/base/base');
+  }
+
+  /**
    * 获取广告列表
    */
   getAd(channel) {
@@ -52,6 +59,10 @@ export default class MainStore extends BaseStore {
     return this.apiGet('/base/experience');
   }
 
+  getCourseIndex() {
+    return this.apiGet('/base/course_index');
+  }
+
   /**
    * 获取考分排行信息
    */

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

@@ -257,6 +257,18 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 获取笔记列表
+   * @param {*} page
+   * @param {*} size
+   * @param {*} startTime
+   * @param {*} endTime
+   * @param {*} order
+   */
+  listQuestionNote({ keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order }) {
+    return this.apiGet('/my/note/question/list', { keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order });
+  }
+
+  /**
    * 更新课程笔记
    * @param {*} courseId
    * @param {*} courseNoId
@@ -266,6 +278,10 @@ export default class MyStore extends BaseStore {
     return this.apiPut('/my/note/course', { courseId, courseNoId, content });
   }
 
+  clearCourseNote(courseNoIds) {
+    return this.apiPost('/my/note/course/clear', { courseNoIds });
+  }
+
   /**
    * 获取笔记列表
    * @param {*} page
@@ -274,8 +290,8 @@ export default class MyStore extends BaseStore {
    * @param {*} endTime
    * @param {*} order
    */
-  listQuestionNote({ keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order }) {
-    return this.apiGet('/my/note/question/list', { keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order });
+  listCourseNote({ keyword, courseId, page, size, order, direction }) {
+    return this.apiGet('/my/note/course/list', { keyword, courseId, page, size, order, direction });
   }
 
   /**
@@ -320,10 +336,15 @@ export default class MyStore extends BaseStore {
    * @param {*} courseId
    * @param {*} courseNoId
    * @param {*} position
+   * @param {*} originContent
    * @param {*} content
    */
-  addCourseAsk(courseId, courseNoId, position, content) {
-    return this.apiPost('/my/ask/course', { courseId, courseNoId, position, content });
+  addCourseAsk(courseId, courseNoId, position, originContent, content) {
+    return this.apiPost('/my/ask/course', { courseId, courseNoId, position, originContent, content });
+  }
+
+  listCourseAsk({ keyword, courseId, courseNoId, order, direction }) {
+    return this.apiGet('/my/ask/course/list', { keyword, courseId, courseNoId, order, direction });
   }
 
   /**
@@ -485,6 +506,10 @@ export default class MyStore extends BaseStore {
     return this.apiPost('/my/export/note/course', { setting });
   }
 
+  exportDetail(id) {
+    return this.apiGet('/my/export/detail', { id });
+  }
+
   /**
    * 关闭导出提示
    */

+ 25 - 10
front/project/www/stores/order.js

@@ -1,25 +1,40 @@
 import BaseStore from '@src/stores/base';
-// import * as querystring from 'querystring';
 
 export default class OrderStore extends BaseStore {
   allCheckout() {
-    return this.apiGet('/order/checkout/all');
+    return this.apiGet('/order/checkout/all')
+      .then(result => {
+        this.setState({ number: result.checkouts.length });
+        return result;
+      });
   }
 
   addCheckout({ productType, productId, service, param, number }) {
-    return this.apiPost('/order/checkout/add', { productType, productId, service, param, number });
+    return this.apiPost('/order/checkout/add', { productType, productId, service, param, number })
+      .then(result => {
+        this.setState({ number: result });
+        return result;
+      });
   }
 
-  changeCheckout(checkoutId, number) {
-    return this.apiDelete('/order/checkout/number', { checkoutId, number });
+  changeCheckout(id, number) {
+    return this.apiPut('/order/checkout/number', { id, number })
+      .then(result => {
+        this.setState({ number: result });
+        return result;
+      });
   }
 
-  removeCheckout(checkoutId) {
-    return this.apiDelete('/order/checkout/delete', { checkoutId });
+  removeCheckout(id) {
+    return this.apiDel('/order/checkout/delete', { id })
+      .then(result => {
+        this.setState({ number: result });
+        return result;
+      });
   }
 
-  confirmPay() {
-    return this.apiPost('/order/pay/confirm');
+  confirmPay(courseId) {
+    return this.apiPost('/order/pay/confirm', { courseId });
   }
 
   speedPay({ productType, productId, service, param, number }) {
@@ -79,4 +94,4 @@ export default class OrderStore extends BaseStore {
   }
 }
 
-export const Order = new OrderStore({ key: 'order' });
+export const Order = new OrderStore({ key: 'order', local: true });

+ 88 - 3
front/project/www/stores/user.js

@@ -1,6 +1,65 @@
 import BaseStore from '@src/stores/base';
 import * as querystring from 'querystring';
+import { getMap } from '@src/services/Tools';
+// import * as querystring from 'querystring';
 
+import { ServiceParamMap, OrderInfoMap, ServiceKey } from '../../Constant';
+
+const ServiceParamRelation = getMap(Object.keys(ServiceParamMap).map(key => {
+  return {
+    map: getMap(ServiceParamMap[key].map((row, index) => {
+      row.index = index;
+      return row;
+    }), 'value', 'index'),
+    key,
+  };
+}), 'key', 'map');
+function formatTitle(record) {
+  if (record.productType === 'course_package') {
+    return (record.coursePackage || {}).title;
+  }
+  if (record.productType === 'course') {
+    record.minNumber = record.course.minNumber;
+    record.maxNumber = record.course.maxNumber;
+    return (record.course || {}).title;
+  }
+  if (record.productType === 'data') {
+    return (record.data || {}).title;
+  }
+  if (record.productType === 'service') {
+    return record.info.label || ((record.serviceInfo || {}).title);
+  }
+  return '';
+}
+function formatGift(record) {
+  let gift = null;
+  if (record.productType === 'course_package') {
+    ({ gift } = record.coursePackage || {});
+  }
+  if (!gift) return null;
+  return ServiceKey.map(row => {
+    if (!gift[row.value]) return null;
+    const list = ServiceParamMap[row.value];
+    if (list) {
+      const map = getMap(list, 'value');
+      return Object.assign({ param: map[gift[row.value]] }, map[gift[row.value]], row);
+    }
+    return Object.assign({ number: gift[row.value] }, row);
+  }).filter(row => row);
+}
+function formatCheckout(checkouts) {
+  checkouts.forEach(checkout => {
+    checkout.key = checkout.id;
+    checkout.info = OrderInfoMap[checkout.productType];
+    if (checkout.productType === 'service') {
+      const index = (ServiceParamRelation[checkout.service] && ServiceParamRelation[checkout.service][checkout.param]) || 0;
+      checkout.info = Object.assign({}, checkout.info[checkout.service], checkout.serviceInfo.package[index]);
+    }
+    checkout.title = formatTitle(checkout);
+    checkout.gift = formatGift(checkout);
+    if (checkout.children) formatCheckout(checkout.children);
+  });
+}
 export default class UserStore extends BaseStore {
   constructor(props) {
     super(props);
@@ -26,6 +85,32 @@ export default class UserStore extends BaseStore {
     }
   }
 
+  needPay(order) {
+    return new Promise((resolve, reject) => {
+      this.successCB = resolve;
+      this.failCB = reject;
+      formatCheckout(order.checkouts);
+      this.setState({ needPay: true, order });
+    });
+  }
+
+  formatCheckout(record) {
+    formatCheckout(record);
+  }
+
+  formatOrder(order) {
+    formatCheckout(order.checkouts);
+  }
+
+  closePay(err) {
+    this.setState({ needPay: false });
+    if (err) {
+      if (this.failCB) this.failCB();
+    } else if (this.successCB) this.successCB();
+    this.successCB = null;
+    this.failCB = null;
+  }
+
   needLogin() {
     if (this.state.login) {
       return Promise.resolve();
@@ -41,8 +126,8 @@ export default class UserStore extends BaseStore {
     this.setState({ needLogin: !!err });
     if (err) {
       if (this.failCB) this.failCB();
-    } else if (this.loginCB) this.loginCB();
-    this.loginCB = null;
+    } else if (this.successCB) this.successCB();
+    this.successCB = null;
     this.failCB = null;
   }
 
@@ -61,7 +146,7 @@ export default class UserStore extends BaseStore {
 
   infoHandle(result, auto = true) {
     if (result.token) this.setToken(result.token);
-    this.setState({ login: true, needLogin: !auto, info: result, username: result.username });
+    this.setState({ login: result.id, needLogin: !auto, info: result, username: result.username });
   }
 
   originInviteCode(inviteCode) {

+ 2 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/MessageCategory.java

@@ -17,6 +17,8 @@ public enum MessageCategory {
 
     INVITED("invited", "邀请好友注册"),
     EMAIL_CHANGE("email_change", "邮箱变更"),
+    EMAIL_UNNBIND("email_unbind", "邮箱解绑"),
+    EMAIL_BIND("email_bind", "邮箱绑定"),
 
     CUSTOM("custom", "自定义消息")
     ;

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

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

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

@@ -32,6 +32,7 @@ public enum SettingKey {
     WECHAT_INFO("wechat_info"), // 微信公众号信息
     READY_READ("ready_read"), // 推荐阅读设置
 
+    BASE("base"), // 基础设置
     TIPS("tips"); // 页面提示信息
 
     final static public String message = "设置key";

+ 96 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticleCategory.java

@@ -23,6 +23,21 @@ public class ReadyArticleCategory implements Serializable {
     @Column(name = "`parent_id`")
     private Integer parentId;
 
+    /**
+     * 是否资料节点
+     */
+    @Column(name = "`is_data`")
+    private Integer isData;
+
+    /**
+     * 是否官方资料
+     */
+    @Column(name = "`is_official`")
+    private Integer isOfficial;
+
+    @Column(name = "`sort`")
+    private Integer sort;
+
     @Column(name = "`create_time`")
     private Date createTime;
 
@@ -82,6 +97,56 @@ public class ReadyArticleCategory implements Serializable {
     }
 
     /**
+     * 获取是否资料节点
+     *
+     * @return is_data - 是否资料节点
+     */
+    public Integer getIsData() {
+        return isData;
+    }
+
+    /**
+     * 设置是否资料节点
+     *
+     * @param isData 是否资料节点
+     */
+    public void setIsData(Integer isData) {
+        this.isData = isData;
+    }
+
+    /**
+     * 获取是否官方资料
+     *
+     * @return is_official - 是否官方资料
+     */
+    public Integer getIsOfficial() {
+        return isOfficial;
+    }
+
+    /**
+     * 设置是否官方资料
+     *
+     * @param isOfficial 是否官方资料
+     */
+    public void setIsOfficial(Integer isOfficial) {
+        this.isOfficial = isOfficial;
+    }
+
+    /**
+     * @return sort
+     */
+    public Integer getSort() {
+        return sort;
+    }
+
+    /**
+     * @param sort
+     */
+    public void setSort(Integer sort) {
+        this.sort = sort;
+    }
+
+    /**
      * @return create_time
      */
     public Date getCreateTime() {
@@ -118,6 +183,9 @@ public class ReadyArticleCategory implements Serializable {
         sb.append(", id=").append(id);
         sb.append(", title=").append(title);
         sb.append(", parentId=").append(parentId);
+        sb.append(", isData=").append(isData);
+        sb.append(", isOfficial=").append(isOfficial);
+        sb.append(", sort=").append(sort);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
         sb.append("]");
@@ -164,6 +232,34 @@ public class ReadyArticleCategory implements Serializable {
         }
 
         /**
+         * 设置是否资料节点
+         *
+         * @param isData 是否资料节点
+         */
+        public Builder isData(Integer isData) {
+            obj.setIsData(isData);
+            return this;
+        }
+
+        /**
+         * 设置是否官方资料
+         *
+         * @param isOfficial 是否官方资料
+         */
+        public Builder isOfficial(Integer isOfficial) {
+            obj.setIsOfficial(isOfficial);
+            return this;
+        }
+
+        /**
+         * @param sort
+         */
+        public Builder sort(Integer sort) {
+            obj.setSort(sort);
+            return this;
+        }
+
+        /**
          * @param createTime
          */
         public Builder createTime(Date createTime) {

+ 51 - 16
server/data/src/main/java/com/qxgmat/data/dao/entity/User.java

@@ -271,16 +271,22 @@ public class User implements Serializable {
     private Integer exportQuestionNoteNumber;
 
     /**
+     * 导出提示:0展示,1关闭
+     */
+    @Column(name = "`export_tips`")
+    private Integer exportTips;
+
+    /**
      * 导出课时笔记次数
      */
     @Column(name = "`export_course_note_number`")
     private Integer exportCourseNoteNumber;
 
     /**
-     * 导出提示:0展示,1关闭
+     * 是否购买过课程
      */
-    @Column(name = "`export_tips`")
-    private Integer exportTips;
+    @Column(name = "`is_course`")
+    private Integer isCourse;
 
     private static final long serialVersionUID = 1L;
 
@@ -1083,6 +1089,24 @@ public class User implements Serializable {
     }
 
     /**
+     * 获取导出提示:0展示,1关闭
+     *
+     * @return export_tips - 导出提示:0展示,1关闭
+     */
+    public Integer getExportTips() {
+        return exportTips;
+    }
+
+    /**
+     * 设置导出提示:0展示,1关闭
+     *
+     * @param exportTips 导出提示:0展示,1关闭
+     */
+    public void setExportTips(Integer exportTips) {
+        this.exportTips = exportTips;
+    }
+
+    /**
      * 获取导出课时笔记次数
      *
      * @return export_course_note_number - 导出课时笔记次数
@@ -1101,21 +1125,21 @@ public class User implements Serializable {
     }
 
     /**
-     * 获取导出提示:0展示,1关闭
+     * 获取是否购买过课程
      *
-     * @return export_tips - 导出提示:0展示,1关闭
+     * @return is_course - 是否购买过课程
      */
-    public Integer getExportTips() {
-        return exportTips;
+    public Integer getIsCourse() {
+        return isCourse;
     }
 
     /**
-     * 设置导出提示:0展示,1关闭
+     * 设置是否购买过课程
      *
-     * @param exportTips 导出提示:0展示,1关闭
+     * @param isCourse 是否购买过课程
      */
-    public void setExportTips(Integer exportTips) {
-        this.exportTips = exportTips;
+    public void setIsCourse(Integer isCourse) {
+        this.isCourse = isCourse;
     }
 
     @Override
@@ -1169,8 +1193,9 @@ public class User implements Serializable {
         sb.append(", totalAlert=").append(totalAlert);
         sb.append(", exportQuestionErrorNumber=").append(exportQuestionErrorNumber);
         sb.append(", exportQuestionNoteNumber=").append(exportQuestionNoteNumber);
-        sb.append(", exportCourseNoteNumber=").append(exportCourseNoteNumber);
         sb.append(", exportTips=").append(exportTips);
+        sb.append(", exportCourseNoteNumber=").append(exportCourseNoteNumber);
+        sb.append(", isCourse=").append(isCourse);
         sb.append("]");
         return sb.toString();
     }
@@ -1631,6 +1656,16 @@ public class User implements Serializable {
         }
 
         /**
+         * 设置导出提示:0展示,1关闭
+         *
+         * @param exportTips 导出提示:0展示,1关闭
+         */
+        public Builder exportTips(Integer exportTips) {
+            obj.setExportTips(exportTips);
+            return this;
+        }
+
+        /**
          * 设置导出课时笔记次数
          *
          * @param exportCourseNoteNumber 导出课时笔记次数
@@ -1641,12 +1676,12 @@ public class User implements Serializable {
         }
 
         /**
-         * 设置导出提示:0展示,1关闭
+         * 设置是否购买过课程
          *
-         * @param exportTips 导出提示:0展示,1关闭
+         * @param isCourse 是否购买过课程
          */
-        public Builder exportTips(Integer exportTips) {
-            obj.setExportTips(exportTips);
+        public Builder isCourse(Integer isCourse) {
+            obj.setIsCourse(isCourse);
             return this;
         }
 

+ 39 - 4
server/data/src/main/java/com/qxgmat/data/dao/entity/UserAskCourse.java

@@ -39,7 +39,7 @@ public class UserAskCourse implements Serializable {
      * 位置
      */
     @Column(name = "`position`")
-    private String position;
+    private Integer position;
 
     /**
      * 提问优先回答时间
@@ -90,6 +90,12 @@ public class UserAskCourse implements Serializable {
     private Date updateTime;
 
     /**
+     * 老师讲解内容
+     */
+    @Column(name = "`origin_content`")
+    private String originContent;
+
+    /**
      * 提问
      */
     @Column(name = "`content`")
@@ -194,7 +200,7 @@ public class UserAskCourse implements Serializable {
      *
      * @return position - 位置
      */
-    public String getPosition() {
+    public Integer getPosition() {
         return position;
     }
 
@@ -203,7 +209,7 @@ public class UserAskCourse implements Serializable {
      *
      * @param position 位置
      */
-    public void setPosition(String position) {
+    public void setPosition(Integer position) {
         this.position = position;
     }
 
@@ -362,6 +368,24 @@ public class UserAskCourse implements Serializable {
     }
 
     /**
+     * 获取老师讲解内容
+     *
+     * @return origin_content - 老师讲解内容
+     */
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    /**
+     * 设置老师讲解内容
+     *
+     * @param originContent 老师讲解内容
+     */
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
+
+    /**
      * 获取提问
      *
      * @return content - 提问
@@ -418,6 +442,7 @@ public class UserAskCourse implements Serializable {
         sb.append(", sort=").append(sort);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
+        sb.append(", originContent=").append(originContent);
         sb.append(", content=").append(content);
         sb.append(", answer=").append(answer);
         sb.append("]");
@@ -488,7 +513,7 @@ public class UserAskCourse implements Serializable {
          *
          * @param position 位置
          */
-        public Builder position(String position) {
+        public Builder position(Integer position) {
             obj.setPosition(position);
             return this;
         }
@@ -590,6 +615,16 @@ public class UserAskCourse implements Serializable {
         }
 
         /**
+         * 设置老师讲解内容
+         *
+         * @param originContent 老师讲解内容
+         */
+        public Builder originContent(String originContent) {
+            obj.setOriginContent(originContent);
+            return this;
+        }
+
+        /**
          * 设置提问
          *
          * @param content 提问

+ 52 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/UserExport.java

@@ -15,6 +15,9 @@ public class UserExport implements Serializable {
     @Column(name = "`user_id`")
     private Integer userId;
 
+    @Column(name = "`no`")
+    private Integer no;
+
     /**
      * 导出类型
      */
@@ -30,6 +33,9 @@ public class UserExport implements Serializable {
     @Column(name = "`create_time`")
     private Date createTime;
 
+    @Column(name = "`content`")
+    private String content;
+
     private static final long serialVersionUID = 1L;
 
     /**
@@ -61,6 +67,20 @@ public class UserExport implements Serializable {
     }
 
     /**
+     * @return no
+     */
+    public Integer getNo() {
+        return no;
+    }
+
+    /**
+     * @param no
+     */
+    public void setNo(Integer no) {
+        this.no = no;
+    }
+
+    /**
      * 获取导出类型
      *
      * @return type - 导出类型
@@ -110,6 +130,20 @@ public class UserExport implements Serializable {
         this.createTime = createTime;
     }
 
+    /**
+     * @return content
+     */
+    public String getContent() {
+        return content;
+    }
+
+    /**
+     * @param content
+     */
+    public void setContent(String content) {
+        this.content = content;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -118,9 +152,11 @@ public class UserExport implements Serializable {
         sb.append("Hash = ").append(hashCode());
         sb.append(", id=").append(id);
         sb.append(", userId=").append(userId);
+        sb.append(", no=").append(no);
         sb.append(", type=").append(type);
         sb.append(", setting=").append(setting);
         sb.append(", createTime=").append(createTime);
+        sb.append(", content=").append(content);
         sb.append("]");
         return sb.toString();
     }
@@ -153,6 +189,14 @@ public class UserExport implements Serializable {
         }
 
         /**
+         * @param no
+         */
+        public Builder no(Integer no) {
+            obj.setNo(no);
+            return this;
+        }
+
+        /**
          * 设置导出类型
          *
          * @param type 导出类型
@@ -180,6 +224,14 @@ public class UserExport implements Serializable {
             return this;
         }
 
+        /**
+         * @param content
+         */
+        public Builder content(String content) {
+            obj.setContent(content);
+            return this;
+        }
+
         public UserExport build() {
             return this.obj;
         }

+ 42 - 7
server/data/src/main/java/com/qxgmat/data/dao/entity/UserMessage.java

@@ -18,12 +18,18 @@ public class UserMessage implements Serializable {
     private Integer userId;
 
     /**
-     * 消息类型
+     * 消息
      */
     @Column(name = "`type`")
     private String type;
 
     /**
+     * 消息类型
+     */
+    @Column(name = "`message_category`")
+    private String messageCategory;
+
+    /**
      * 标题
      */
     @Column(name = "`title`")
@@ -85,24 +91,42 @@ public class UserMessage implements Serializable {
     }
 
     /**
-     * 获取消息类型
+     * 获取消息
      *
-     * @return type - 消息类型
+     * @return type - 消息
      */
     public String getType() {
         return type;
     }
 
     /**
-     * 设置消息类型
+     * 设置消息
      *
-     * @param type 消息类型
+     * @param type 消息
      */
     public void setType(String type) {
         this.type = type;
     }
 
     /**
+     * 获取消息类型
+     *
+     * @return message_category - 消息类型
+     */
+    public String getMessageCategory() {
+        return messageCategory;
+    }
+
+    /**
+     * 设置消息类型
+     *
+     * @param messageCategory 消息类型
+     */
+    public void setMessageCategory(String messageCategory) {
+        this.messageCategory = messageCategory;
+    }
+
+    /**
      * 获取标题
      *
      * @return title - 标题
@@ -197,6 +221,7 @@ public class UserMessage implements Serializable {
         sb.append(", id=").append(id);
         sb.append(", userId=").append(userId);
         sb.append(", type=").append(type);
+        sb.append(", messageCategory=").append(messageCategory);
         sb.append(", title=").append(title);
         sb.append(", link=").append(link);
         sb.append(", isRead=").append(isRead);
@@ -236,9 +261,9 @@ public class UserMessage implements Serializable {
         }
 
         /**
-         * 设置消息类型
+         * 设置消息
          *
-         * @param type 消息类型
+         * @param type 消息
          */
         public Builder type(String type) {
             obj.setType(type);
@@ -246,6 +271,16 @@ public class UserMessage implements Serializable {
         }
 
         /**
+         * 设置消息类型
+         *
+         * @param messageCategory 消息类型
+         */
+        public Builder messageCategory(String messageCategory) {
+            obj.setMessageCategory(messageCategory);
+            return this;
+        }
+
+        /**
          * 设置标题
          *
          * @param title 标题

+ 4 - 5
server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrder.java

@@ -1,7 +1,6 @@
 package com.qxgmat.data.dao.entity;
 
 import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.util.Date;
@@ -54,7 +53,7 @@ public class UserOrder implements Serializable {
      * 优惠信息
      */
     @Column(name = "`promote`")
-    private JSONObject promote;
+    private JSONArray promote;
 
     /**
      * 赠品信息
@@ -227,7 +226,7 @@ public class UserOrder implements Serializable {
      *
      * @return promote - 优惠信息
      */
-    public JSONObject getPromote() {
+    public JSONArray getPromote() {
         return promote;
     }
 
@@ -236,7 +235,7 @@ public class UserOrder implements Serializable {
      *
      * @param promote 优惠信息
      */
-    public void setPromote(JSONObject promote) {
+    public void setPromote(JSONArray promote) {
         this.promote = promote;
     }
 
@@ -486,7 +485,7 @@ public class UserOrder implements Serializable {
          *
          * @param promote 优惠信息
          */
-        public Builder promote(JSONObject promote) {
+        public Builder promote(JSONArray promote) {
             obj.setPromote(promote);
             return this;
         }

+ 4 - 1
server/data/src/main/java/com/qxgmat/data/dao/mapping/ReadyArticleCategoryMapper.xml

@@ -8,6 +8,9 @@
     <id column="id" jdbcType="INTEGER" property="id" />
     <result column="title" jdbcType="VARCHAR" property="title" />
     <result column="parent_id" jdbcType="INTEGER" property="parentId" />
+    <result column="is_data" jdbcType="INTEGER" property="isData" />
+    <result column="is_official" jdbcType="INTEGER" property="isOfficial" />
+    <result column="sort" jdbcType="INTEGER" property="sort" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
   </resultMap>
@@ -15,6 +18,6 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `title`, `parent_id`, `create_time`, `update_time`
+    `id`, `title`, `parent_id`, `is_data`, `is_official`, `sort`, `create_time`, `update_time`
   </sql>
 </mapper>

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

@@ -10,7 +10,7 @@
     <result column="course_id" jdbcType="INTEGER" property="courseId" />
     <result column="course_no_id" jdbcType="INTEGER" property="courseNoId" />
     <result column="record_id" jdbcType="INTEGER" property="recordId" />
-    <result column="position" jdbcType="VARCHAR" property="position" />
+    <result column="position" jdbcType="INTEGER" property="position" />
     <result column="ask_time" jdbcType="INTEGER" property="askTime" />
     <result column="expire_time" jdbcType="TIMESTAMP" property="expireTime" />
     <result column="answer_status" jdbcType="INTEGER" property="answerStatus" />
@@ -25,6 +25,7 @@
     <!--
       WARNING - @mbg.generated
     -->
+    <result column="origin_content" jdbcType="LONGVARCHAR" property="originContent" />
     <result column="content" jdbcType="LONGVARCHAR" property="content" />
     <result column="answer" jdbcType="LONGVARCHAR" property="answer" />
   </resultMap>
@@ -40,6 +41,6 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `content`, `answer`
+    `origin_content`, `content`, `answer`
   </sql>
 </mapper>

+ 14 - 1
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserExportMapper.xml

@@ -7,14 +7,27 @@
     -->
     <id column="id" jdbcType="INTEGER" property="id" />
     <result column="user_id" jdbcType="INTEGER" property="userId" />
+    <result column="no" jdbcType="INTEGER" property="no" />
     <result column="type" jdbcType="VARCHAR" property="type" />
     <result column="setting" jdbcType="VARCHAR" property="setting" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
   </resultMap>
+  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.qxgmat.data.dao.entity.UserExport">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <result column="content" jdbcType="LONGVARCHAR" property="content" />
+  </resultMap>
   <sql id="Base_Column_List">
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `user_id`, `type`, `setting`, `create_time`
+    `id`, `user_id`, `no`, `type`, `setting`, `create_time`
+  </sql>
+  <sql id="Blob_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    `content`
   </sql>
 </mapper>

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

@@ -50,8 +50,9 @@
     <result column="total_alert" jdbcType="INTEGER" property="totalAlert" />
     <result column="export_question_error_number" jdbcType="INTEGER" property="exportQuestionErrorNumber" />
     <result column="export_question_note_number" jdbcType="INTEGER" property="exportQuestionNoteNumber" />
-    <result column="export_course_note_number" jdbcType="INTEGER" property="exportCourseNoteNumber" />
     <result column="export_tips" jdbcType="INTEGER" property="exportTips" />
+    <result column="export_course_note_number" jdbcType="INTEGER" property="exportCourseNoteNumber" />
+    <result column="is_course" jdbcType="INTEGER" property="isCourse" />
   </resultMap>
   <sql id="Base_Column_List">
     <!--
@@ -65,7 +66,7 @@
     `latest_error`, `latest_collect`, `origin_id`, `invite_code`, `total_money`, `invite_number`, 
     `textbook_half`, `qx_cat`, `register_ip`, `register_city`, `latest_login_ip`, `latest_login_time`, 
     `is_frozen`, `create_time`, `data_email_subscribe`, `textbook_email_subscribe`, `total_alert`, 
-    `export_question_error_number`, `export_question_note_number`, `export_course_note_number`, 
-    `export_tips`
+    `export_question_error_number`, `export_question_note_number`, `export_tips`, `export_course_note_number`, 
+    `is_course`
   </sql>
 </mapper>

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

@@ -8,6 +8,7 @@
     <id column="id" jdbcType="INTEGER" property="id" />
     <result column="user_id" jdbcType="INTEGER" property="userId" />
     <result column="type" jdbcType="VARCHAR" property="type" />
+    <result column="message_category" jdbcType="VARCHAR" property="messageCategory" />
     <result column="title" jdbcType="VARCHAR" property="title" />
     <result column="link" jdbcType="VARCHAR" property="link" />
     <result column="is_read" jdbcType="INTEGER" property="isRead" />
@@ -23,7 +24,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `user_id`, `type`, `title`, `link`, `is_read`, `create_time`
+    `id`, `user_id`, `type`, `message_category`, `title`, `link`, `is_read`, `create_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--

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

@@ -12,7 +12,7 @@
     <result column="money" jdbcType="DECIMAL" property="money" />
     <result column="origin_money" jdbcType="DECIMAL" property="originMoney" />
     <result column="invoice_money" jdbcType="DECIMAL" property="invoiceMoney" />
-    <result column="promote" jdbcType="VARCHAR" property="promote" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
+    <result column="promote" jdbcType="VARCHAR" property="promote" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler" />
     <result column="gift" jdbcType="VARCHAR" property="gift" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler" />
     <result column="ask_time" jdbcType="INTEGER" property="askTime" />
     <result column="pay_id" jdbcType="BIGINT" property="payId" />

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

@@ -546,6 +546,9 @@ CREATE TABLE ready_article_category (
   id int(11) unsigned NOT NULL AUTO_INCREMENT,
   title varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
   parent_id int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级id',
+  is_data tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否资料节点',
+  is_official tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否官方资料',
+  sort int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序:从大到小',
   create_time datetime DEFAULT NULL,
   update_time datetime DEFAULT NULL,
   PRIMARY KEY (id),
@@ -727,7 +730,8 @@ VALUES
 	(21,'experience_info','{}'),
 	(22,'sentence_info','{}'),
 	(23,'wechat_info','{}'),
-	(24,'ready_read','{}');
+	(24,'ready_read','{}'),
+	(25,'base','{}');
 
 CREATE TABLE textbook_library (
   id int(11) unsigned NOT NULL AUTO_INCREMENT,
@@ -942,7 +946,8 @@ CREATE TABLE user (
   export_question_error_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '导出错题次数',
   export_question_note_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '导出题目笔记次数',
   export_tips tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '导出提示:0展示,1关闭',
-  export_course_note_number int(11) unsigned NOT NULL COMMENT '导出课时笔记次数',
+  export_course_note_number int(11) unsigned NOT NULL DEFAULT '0' COMMENT '导出课时笔记次数',
+  is_course tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否购买课程',
   PRIMARY KEY (id),
   KEY mobile (mobile),
   KEY wechat_unionid (wechat_unionid),
@@ -969,7 +974,8 @@ CREATE TABLE user_ask_course (
   course_id int(11) unsigned NOT NULL COMMENT '课程id',
   course_no_id int(11) unsigned NOT NULL COMMENT '课时id',
   record_id int(11) unsigned NOT NULL COMMENT '记录id',
-  position varchar(20) NOT NULL DEFAULT '' COMMENT '位置',
+  position int(11) unsigned NOT NULL DEFAULT '0' COMMENT '位置',
+  origin_content text COMMENT '老师讲解内容',
   content text COMMENT '提问',
   ask_time int(11) unsigned NOT NULL DEFAULT '0' COMMENT '提问优先回答时间',
   expire_time datetime DEFAULT NULL COMMENT '过期时间',
@@ -1079,7 +1085,9 @@ CREATE TABLE user_course_appointment_comment (
   name varchar(255) DEFAULT NULL,
   create_time datetime DEFAULT NULL,
   delete_time datetime DEFAULT NULL,
-  PRIMARY KEY (`id`)
+  PRIMARY KEY (`id`),
+  KEY record_id (record_id),
+  KEY appointment_id (appointment_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-预约-聊天';
 
 CREATE TABLE user_course_progress (
@@ -1106,13 +1114,16 @@ CREATE TABLE user_course_record (
   KEY user_id (user_id,course_id,record_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-课程-访问记录';
 
-CREATE TABLE use_export (
-  id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+CREATE TABLE user_export (
+  id int(11) unsigned NOT NULL AUTO_INCREMENT,
   user_id int(11) unsigned NOT NULL DEFAULT '0',
+  no int(11) unsigned NOT NULL DEFAULT '0',
   type varchar(20) NOT NULL DEFAULT '' COMMENT '导出类型',
   setting text COMMENT '导出设置',
+  content longtext,
   create_time datetime DEFAULT NULL,
-  PRIMARY KEY (`id`)
+  PRIMARY KEY (id),
+  KEY user_id (user_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-导出-记录';
 
 CREATE TABLE user_feedback_error (
@@ -1157,7 +1168,8 @@ CREATE TABLE user_invoice (
 CREATE TABLE user_message (
   id int(11) unsigned NOT NULL AUTO_INCREMENT,
   user_id int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
-  type varchar(50) NOT NULL DEFAULT '' COMMENT '消息类型',
+  type varchar(50) NOT NULL DEFAULT '' COMMENT '消息',
+  message_category varchar(50) NOT NULL DEFAULT '' COMMENT '消息类型',
   title varchar(50) NOT NULL DEFAULT '' COMMENT '标题',
   content text COMMENT '内容',
   link varchar(255) NOT NULL DEFAULT '' COMMENT '链接',

+ 1 - 1
server/data/src/main/resources/mybatis-generator.xml

@@ -174,7 +174,7 @@
         <table schema="qianxing" tableName="user_order" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" delimitAllColumns="true">
             <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
             <columnOverride column="product_types" javaType="com.alibaba.fastjson.JSONArray" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler"/>
-            <columnOverride column="promote" javaType="com.alibaba.fastjson.JSONObject" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler"/>
+            <columnOverride column="promote" javaType="com.alibaba.fastjson.JSONArray" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler"/>
             <columnOverride column="gift" javaType="com.alibaba.fastjson.JSONArray" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler"/>
         </table>
         <table schema="qianxing" tableName="user_export" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" delimitAllColumns="true">

+ 2 - 1
server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java

@@ -153,6 +153,7 @@ public class CourseController {
             @RequestParam(required = false) String courseModule,
             @RequestParam(required = false) Integer structId,
             @RequestParam(required = false) Boolean excludeVs,
+            @RequestParam(required = false) Boolean excludeOnline,
             @RequestParam(required = false, defaultValue = "id") String order,
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session) {
@@ -160,7 +161,7 @@ public class CourseController {
         if (ids != null && ids.length > 0){
             p = courseService.select(ids);
         }else{
-            p = courseService.listAdmin(page, size, keyword, CourseModule.ValueOf(courseModule), structId, excludeVs, order, DirectionStatus.ValueOf(direction));
+            p = courseService.listAdmin(page, size, keyword, CourseModule.ValueOf(courseModule), structId, excludeVs, excludeOnline, order, DirectionStatus.ValueOf(direction));
         }
         List<CourseListDto> pr = Transform.convert(p, CourseListDto.class);
 

+ 22 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/admin/ReadyController.java

@@ -7,10 +7,7 @@ import com.nuliji.tools.ResponseHelp;
 import com.nuliji.tools.Transform;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.entity.*;
-import com.qxgmat.dto.admin.request.ReadyArticleDto;
-import com.qxgmat.dto.admin.request.ReadyDataDto;
-import com.qxgmat.dto.admin.request.ReadyReadDto;
-import com.qxgmat.dto.admin.request.ReadyRoomDto;
+import com.qxgmat.dto.admin.request.*;
 import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.Api;
@@ -58,6 +55,27 @@ public class ReadyController {
         return ResponseHelp.success(p);
     }
 
+    @RequestMapping(value = "/category/edit", method = RequestMethod.PUT)
+    @ApiOperation(value = "修改分类信息", httpMethod = "PUT")
+    public Response<Boolean> editCategory(@RequestBody @Validated ReadyArticleCategoryDto dto, HttpSession session) {
+        ReadyArticleCategory entity = Transform.dtoToEntity(dto);
+        if (dto.getIndex() != null){
+            // 排序操作
+            readyArticleCategoryService.changeOrder(dto.getId(), dto.getIndex());
+        }
+        readyArticleCategoryService.edit(entity);
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/category/delete", method = RequestMethod.DELETE)
+    @ApiOperation(value = "删除分类信息", httpMethod = "DELETE")
+    public Response<Boolean> deleteCategory(int id, HttpSession session) {
+        ReadyArticleCategory category = readyArticleCategoryService.get(id);
+        readyArticleService.deleteByCategory(category.getId());
+        readyArticleCategoryService.delete(id);
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/article/add", method = RequestMethod.POST)
     @ApiOperation(value = "添加文章", httpMethod = "POST")
     private Response<Boolean> addArticle(@RequestBody @Validated ReadyArticleDto dto){

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

@@ -114,6 +114,23 @@ public class SettingController {
         return ResponseHelp.success(entity.getValue());
     }
 
+    @RequestMapping(value = "/base", method = RequestMethod.PUT)
+    @ApiOperation(value = "修改基础配置", httpMethod = "PUT")
+    private Response<Boolean> editBase(@RequestBody @Validated JSONObject dto){
+        Setting entity = settingService.getByKey(SettingKey.BASE);
+        entity.setValue(dto);
+        settingService.edit(entity);
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/base", method = RequestMethod.GET)
+    @ApiOperation(value = "获取基础配置", httpMethod = "GET")
+    private Response<JSONObject> getBase(){
+        Setting entity = settingService.getByKey(SettingKey.BASE);
+        logger.debug("{}", entity);
+        return ResponseHelp.success(entity.getValue());
+    }
+
     @RequestMapping(value = "/place", method = RequestMethod.PUT)
     @ApiOperation(value = "修改考点设置", httpMethod = "PUT")
     private Response<Boolean> editPlace(@RequestBody @Validated JSONObject dto){

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

@@ -1,5 +1,6 @@
 package com.qxgmat.controller.api;
 
+import com.github.pagehelper.Page;
 import com.nuliji.tools.Response;
 import com.nuliji.tools.ResponseHelp;
 import com.nuliji.tools.Tools;
@@ -8,7 +9,11 @@ import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.qxgmat.data.constants.enums.ServiceKey;
+import com.qxgmat.data.dao.entity.TextbookLibrary;
 import com.qxgmat.data.dao.entity.User;
+import com.qxgmat.data.dao.entity.UserMessage;
+import com.qxgmat.data.dao.entity.UserOrderRecord;
+import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.MyDto;
 import com.qxgmat.help.AiHelp;
@@ -17,7 +22,11 @@ import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.help.SmsHelp;
 import com.qxgmat.service.UsersService;
 import com.qxgmat.service.UserServiceService;
+import com.qxgmat.service.extend.PreviewService;
+import com.qxgmat.service.inline.TextbookLibraryService;
 import com.qxgmat.service.inline.UserAbnormalService;
+import com.qxgmat.service.inline.UserMessageService;
+import com.qxgmat.service.inline.UserOrderRecordService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -29,7 +38,9 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import javax.validation.Validator;
+import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 
 /**
  * Created by GaoJie on 2017/10/31.
@@ -60,6 +71,20 @@ public class AuthController {
     @Autowired
     private UserAbnormalService userAbnormalService;
 
+    // 初始化用户信息
+
+    @Autowired
+    private TextbookLibraryService textbookLibraryService;
+
+    @Autowired
+    private UserMessageService userMessageService;
+
+    @Autowired
+    private UserOrderRecordService userOrderRecordService;
+
+    @Autowired
+    private PreviewService previewService;
+
 
     @RequestMapping(value = "/token", method = RequestMethod.POST)
     @ApiOperation(value = "验证token", httpMethod = "POST")
@@ -89,13 +114,14 @@ public class AuthController {
 //        }
         try {
             String ip = Tools.getClientIp(request);
-            User user = usersService.register(userLoginDto.getArea(), userLoginDto.getMobile(), userLoginDto.getInviteCode(), userLoginDto.getEmail(), null, ip, aiHelp.parseIp(ip));
+            usersService.register(userLoginDto.getArea(), userLoginDto.getMobile(), userLoginDto.getInviteCode(), userLoginDto.getEmail(), null, ip, aiHelp.parseIp(ip));
         }catch (ParameterException e){
             // 忽略已注册信息
         }
         shiroHelp.getSession().login(shiroHelp.user(userLoginDto.getArea()+":"+userLoginDto.getMobile(), ""));
 
-        User entity = shiroHelp.getLoginUser();
+        User user = shiroHelp.getLoginUser();
+        User entity = usersService.get(user.getId());
         MyDto dto = processUser(entity, request);
         return ResponseHelp.success(dto);
     }
@@ -107,13 +133,13 @@ public class AuthController {
             @RequestParam(required = false, defaultValue = "") String code,
             HttpSession session, HttpServletRequest request) {
         User user = (User) shiroHelp.getLoginUser();
-        if (user!=null){
-            // 已登录用户,绑定
-            user = usersService.Oauth(user, code, "wechat_pc", true);
-        }else{
-            shiroHelp.getSession().login(shiroHelp.oauth(code, "wechat_pc", true));
-            user = shiroHelp.getLoginUser();
+        user = usersService.Oauth(user, code, "wechat_pc", true);
+        if (user.getId() != null && user.getId() > 0){
+            user = usersService.get(user.getId());
+            shiroHelp.getSession().login(shiroHelp.user(user.getArea()+":"+user.getMobile(), ""));
         }
+        user = shiroHelp.getLoginUser();
+
         MyDto dto = processUser(user, request);
         return ResponseHelp.success(dto);
     }
@@ -128,12 +154,10 @@ public class AuthController {
             @RequestParam(required = false, defaultValue = "") boolean userInfo,
             HttpSession session, HttpServletRequest request) {
         User user = (User) shiroHelp.getLoginUser();
-        if (user!=null){
-            // 第二次获取userInfo的,重新登录
-            shiroHelp.getSession().login(shiroHelp.oauth(code, "wechat_native", userInfo));
-        }else{
-            shiroHelp.getSession().login(shiroHelp.oauth(code, "wechat_native", userInfo));
-            user = shiroHelp.getLoginUser();
+        user = usersService.Oauth(user, code, "wechat_native", userInfo);
+        if (user.getId() != null && user.getId() > 0){
+            user = usersService.get(user.getId());
+            shiroHelp.getSession().login(shiroHelp.user(user.getArea()+":"+user.getMobile(), ""));
         }
         MyDto dto = processUser(user, request);
         return ResponseHelp.success(dto);
@@ -167,7 +191,8 @@ public class AuthController {
         }
         shiroHelp.getSession().login(shiroHelp.user(userValidMobileDto.getArea()+":"+userValidMobileDto.getMobile(), ""));
 
-        User entity = shiroHelp.getLoginUser();
+        User user = shiroHelp.getLoginUser();
+        User entity = usersService.get(user.getId());
         MyDto dto = processUser(entity, request);
         return ResponseHelp.success(dto);
     }
@@ -212,9 +237,6 @@ public class AuthController {
     }
 
     private MyDto processUser(User user, HttpServletRequest request){
-        if (user.getId() != null){
-            user = usersService.get(user.getId());
-        }
         MyDto dto = Transform.convert(user, MyDto.class);
         if (user.getId() == null || user.getId() == 0) return dto;
         String ip = Tools.getClientIp(request);
@@ -242,7 +264,22 @@ public class AuthController {
         if(!user.getPrepareStatus().isEmpty()){
             dto.setBindPrepare(true);
         }
+        // vip
         dto.setVip(userServiceService.timeService(user.getId(), ServiceKey.VIP));
+        // 最新机经
+        if (userServiceService.hasService(user.getId(), ServiceKey.TEXTBOOK)){
+            TextbookLibrary latest = textbookLibraryService.getLatest();
+            dto.setTextbook(latest.getUpdateTime());
+        }
+        // 未读消息
+        Page<UserMessage> messageList = userMessageService.list(1, 4, user.getId(), null, 0);
+        dto.setMessageNumber((int)messageList.getTotal());
+        dto.setMessages(messageList);
+        // 未完成作业
+        List<UserOrderRecord> recordList = userOrderRecordService.listWithCourse(1, 1000, null, null, true, false, null, null);
+        Collection recordIds = Transform.getIds(recordList, UserOrderRecord.class, "id");
+        List<UserPreviewPaperRelation> relationList = previewService.listByRecordId(user.getId(), recordIds, 2);
+        dto.setPreviewNumber(relationList.size());
         return dto;
     }
 }

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

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

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

@@ -94,6 +94,9 @@ public class CourseController {
     private UserCourseRecordService userCourseRecordService;
 
     @Autowired
+    private UserAskCourseService userAskCourseService;
+
+    @Autowired
     private UserOrderRecordService userOrderRecordService;
 
     @Autowired
@@ -157,6 +160,24 @@ public class CourseController {
         return ResponseHelp.success(dto);
     }
 
+    @RequestMapping(value = "/ask/list", method = RequestMethod.GET)
+    @ApiOperation(value = "精选问答", httpMethod = "GET")
+    public Response<PageMessage<UserAskCourse>> listAsk(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer courseId,
+            @RequestParam(required = false) Integer courseNoId,
+            @RequestParam(required = false) Integer position,
+            @RequestParam(required = false) String order, // create_time, answer_time
+            @RequestParam(required = false) String direction
+    ) {
+        User user = (User) shiroHelp.getLoginUser();
+        Page<UserAskCourse> pr = userAskCourseService.listByCourse(page, size, keyword, courseId, courseNoId, position, true, order, DirectionStatus.ValueOf(direction));
+
+        return ResponseHelp.success(pr, page, size, pr.getTotal());
+    }
+
     @RequestMapping(value = "/simple", method = RequestMethod.GET)
     @ApiOperation(value = "课程基本信息", httpMethod = "GET")
     public Response<Course> simple(

+ 70 - 23
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -12,7 +12,6 @@ import com.qxgmat.data.constants.enums.module.*;
 import com.qxgmat.data.constants.enums.status.AskStatus;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.constants.enums.user.DataType;
-import com.qxgmat.data.constants.enums.user.ExportType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.inline.PaperStat;
 import com.qxgmat.data.inline.UserQuestionStat;
@@ -216,15 +215,19 @@ public class MyController {
     @Autowired
     private MessageExtendService messageExtendService;
 
+    @Autowired
+    private ExportService exportService;
+
     @RequestMapping(value = "/email", method = RequestMethod.POST)
     @ApiOperation(value = "绑定邮箱", httpMethod = "POST")
     public Response<Boolean> email(@RequestBody @Validated UserEmailDto dto, HttpSession session, HttpServletRequest request) {
         User user = (User) shiroHelp.getLoginUser();
+        User in = usersService.get(user.getId());
         usersService.edit(User.builder()
                 .id(user.getId())
                 .email(dto.getEmail())
                 .build());
-        messageExtendService.sendEmailChange(user);
+        messageExtendService.sendEmailChange(user, in.getEmail());
         return ResponseHelp.success(true);
     }
 
@@ -573,6 +576,7 @@ public class MyController {
         dto.setCourseNumber(courseNoIds.size());
         dto.setCourseList(courseResultList);
         dto.setCourseExceed(courseRank);
+        dto.setCourse(user.getIsCourse()!=null && user.getIsCourse() > 0);
 
         return ResponseHelp.success(dto);
     }
@@ -870,7 +874,7 @@ public class MyController {
 
     @RequestMapping(value = "/collect/question/clear", method = RequestMethod.DELETE)
     @ApiOperation(value = "移除题目收藏", notes = "移除题目收藏", httpMethod = "DELETE")
-    public Response<Boolean> deleteQuestionCollect(@RequestBody @Validated UserQuestionIdsDto dto)  {
+    public Response<Boolean> clearQuestionCollect(@RequestBody @Validated UserQuestionNoIdsDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
         List<QuestionNo> questionNoList = questionNoService.select(dto.getQuestionNoIds());
         for(QuestionNo questionNo : questionNoList){
@@ -1073,7 +1077,7 @@ public class MyController {
 
     @RequestMapping(value = "/error/clear", method = RequestMethod.POST)
     @ApiOperation(value = "错题移除", notes = "错题移除", httpMethod = "POST")
-    public Response<Boolean> clearError(@RequestBody @Validated UserQuestionIdsDto dto)  {
+    public Response<Boolean> clearError(@RequestBody @Validated UserQuestionNoIdsDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
 
         List<QuestionNo> questionNoList = questionNoService.select(dto.getQuestionNoIds());
@@ -1123,7 +1127,7 @@ public class MyController {
 
     @RequestMapping(value = "/note/question/clear", method = RequestMethod.POST)
     @ApiOperation(value = "笔记移除", notes = "笔记移除", httpMethod = "POST")
-    public Response<Boolean> clearNoteQuestion(@RequestBody @Validated UserQuestionIdsDto dto)  {
+    public Response<Boolean> clearNoteQuestion(@RequestBody @Validated UserQuestionNoIdsDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
         List<QuestionNo> questionNoList = questionNoService.select(dto.getQuestionNoIds());
         for(QuestionNo questionNo : questionNoList){
@@ -1183,7 +1187,7 @@ public class MyController {
 
     @RequestMapping(value = "/note/course", method = RequestMethod.PUT)
     @ApiOperation(value = "更新课程笔记", notes = "更新课程笔记", httpMethod = "PUT")
-    public Response<Boolean> updateNoteCourse(@RequestBody @Validated UserNoteQuestionDto dto)  {
+    public Response<Boolean> updateNoteCourse(@RequestBody @Validated UserNoteCourseDto dto)  {
         UserNoteCourse entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
         entity.setUserId(user.getId());
@@ -1192,6 +1196,33 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/note/course/clear", method = RequestMethod.POST)
+    @ApiOperation(value = "笔记移除", notes = "笔记移除", httpMethod = "POST")
+    public Response<Boolean> clearNoteCourse(@RequestBody @Validated UserCourseNoIdsDto dto)  {
+        User user = (User) shiroHelp.getLoginUser();
+        List<CourseNo> courseNoList = courseNoService.select(dto.getCourseNoIds());
+        for(CourseNo courseNo : courseNoList){
+            userNoteCourseService.deleteNote(user.getId(), courseNo.getId());
+        }
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/note/course/list", method = RequestMethod.GET)
+    @ApiOperation(value = "获取课程笔记列表", notes = "获取笔记列表", httpMethod = "GET")
+    public Response<PageMessage<UserNoteCourse>> listNoteCourse(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer courseId,
+            @RequestParam(required = false) String order, // update_time, no
+            @RequestParam(required = false) String direction,
+            HttpSession session)  {
+        User user = (User) shiroHelp.getLoginUser();
+        Page<UserNoteCourse> p = userNoteCourseService.listByCourse(page, size, keyword, user.getId(), courseId, order, DirectionStatus.ValueOf(direction));
+
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/report/list", method = RequestMethod.GET)
     @ApiOperation(value = "获取报告列表", notes = "获取报告列表", httpMethod = "GET")
     public Response<PageMessage<UserPaperDto>> listReport(
@@ -1401,6 +1432,24 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
+    @RequestMapping(value = "/ask/course/list", method = RequestMethod.GET)
+    @ApiOperation(value = "获取课程提问列表", notes = "获取课程提问列表", httpMethod = "GET")
+    public Response<PageMessage<UserAskCourse>> listAskCourse(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer courseId,
+            @RequestParam(required = false) Integer courseNoId,
+            @RequestParam(required = false) Integer askStatus,
+            @RequestParam(required = false) String order, // create_time, answer_time
+            @RequestParam(required = false) String direction,
+            HttpSession session)  {
+        User user = (User) shiroHelp.getLoginUser();
+        Page<UserAskCourse> p = userAskCourseService.listByUser(page, size, keyword, user.getId(), courseId, courseNoId, AskStatus.ValueOf(askStatus), order, DirectionStatus.ValueOf(direction));
+
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/feedback/error/question", method = RequestMethod.POST)
     @ApiOperation(value = "添加题目勘误", notes = "添加勘误", httpMethod = "POST")
     public Response<Boolean> addFeedbackErrorQuestion(@RequestBody @Validated UserFeedbackErrorQuestionDto dto)  {
@@ -1890,34 +1939,32 @@ public class MyController {
 
     @RequestMapping(value = "/export/question", method = RequestMethod.POST)
     @ApiOperation(value = "导出题目", notes = "导出题目", httpMethod = "POST")
-    public Response<UserExport> exportQuestion(@RequestBody @Validated UserExportDto dto)  {
+    public Response<Integer> exportQuestion(@RequestBody @Validated UserExportDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
-        UserExport entity = Transform.dtoToEntity(dto);
-        entity.setUserId(user.getId());
-        entity.setType(ExportType.QUESTION.key);
-        entity = userExportService.add(entity);
-        return ResponseHelp.success(entity);
+        UserExport entity = exportService.addQuestion(user.getId(), JSONObject.parseObject(JSONObject.toJSONString(dto.getSetting())));
+        return ResponseHelp.success(entity.getId());
     }
 
     @RequestMapping(value = "/export/note/question", method = RequestMethod.POST)
     @ApiOperation(value = "导出题目笔记", notes = "导出题目笔记", httpMethod = "POST")
-    public Response<UserExport> exportNoteQuestion(@RequestBody @Validated UserExportDto dto)  {
+    public Response<Integer> exportNoteQuestion(@RequestBody @Validated UserExportDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
-        UserExport entity = Transform.dtoToEntity(dto);
-        entity.setUserId(user.getId());
-        entity.setType(ExportType.NOTE_QUESTION.key);
-        entity = userExportService.add(entity);
-        return ResponseHelp.success(entity);
+        UserExport entity = exportService.addQuestionNote(user.getId(), JSONObject.parseObject(JSONObject.toJSONString(dto.getSetting())));
+        return ResponseHelp.success(entity.getId());
     }
 
     @RequestMapping(value = "/export/note/course", method = RequestMethod.POST)
     @ApiOperation(value = "导出课程笔记", notes = "导出课程笔记", httpMethod = "POST")
-    public Response<UserExport> exportNoteCourse(@RequestBody @Validated UserExportDto dto)  {
+    public Response<Integer> exportNoteCourse(@RequestBody @Validated UserExportDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
-        UserExport entity = Transform.dtoToEntity(dto);
-        entity.setUserId(user.getId());
-        entity.setType(ExportType.NOTE_COURSE.key);
-        entity = userExportService.add(entity);
+        UserExport entity = exportService.addCourseNote(user.getId(), JSONObject.parseObject(JSONObject.toJSONString(dto.getSetting())));
+        return ResponseHelp.success(entity.getId());
+    }
+
+    @RequestMapping(value = "/export/detail", method = RequestMethod.GET)
+    @ApiOperation(value = "导出详情", notes = "导出详情", httpMethod = "GET")
+    public Response<UserExport> exportDetail(int id)  {
+        UserExport entity = userExportService.get(id);
         return ResponseHelp.success(entity);
     }
 

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

@@ -25,11 +25,13 @@ import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.UserOrderDetailDto;
 import com.qxgmat.dto.response.UserOrderRecordListDto;
 import com.qxgmat.help.ShiroHelp;
+import com.qxgmat.service.extend.CourseExtendService;
 import com.qxgmat.service.extend.OrderFlowService;
 import com.qxgmat.service.extend.TradeService;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import org.apache.tomcat.websocket.TransformationResult;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.validation.annotation.Validated;
@@ -80,6 +82,9 @@ public class OrderController {
     private CourseDataService courseDataService;
 
     @Autowired
+    private CourseExtendService courseExtendService;
+
+    @Autowired
     private UserInvoiceService userInvoiceService;
 
     @RequestMapping(value = "/checkout/all", method = RequestMethod.GET)
@@ -102,20 +107,21 @@ public class OrderController {
         return ResponseHelp.success(number);
     }
 
-    @RequestMapping(value = "/checkout/number", method = RequestMethod.POST)
-    @ApiOperation(value = "修改购物车", notes = "修改购物车", httpMethod = "POST")
+    @RequestMapping(value = "/checkout/number", method = RequestMethod.PUT)
+    @ApiOperation(value = "修改购物车", notes = "修改购物车", httpMethod = "PUT")
     public Response<Integer> changeCheckout(@RequestBody @Validated RecordChangeDto dto, HttpServletRequest request)  {
         User user = (User) shiroHelp.getLoginUser();
-        UserOrderCheckout checkout = Transform.dtoToEntity(dto);
+        UserOrderCheckout checkout = userOrderCheckoutService.get(dto.getId());
+        checkout.setNumber(dto.getNumber());
         int number = orderFlowService.changeCheckout(user.getId(), checkout);
         return ResponseHelp.success(number);
     }
 
     @RequestMapping(value = "/checkout/delete", method = RequestMethod.DELETE)
     @ApiOperation(value = "删除购物车", notes = "删除购物车", httpMethod = "DELETE")
-    public Response<Integer> deleteCheckout(@RequestParam int checkoutId, HttpServletRequest request) throws Exception {
+    public Response<Integer> deleteCheckout(@RequestParam int id, HttpServletRequest request) throws Exception {
         User user = (User) shiroHelp.getLoginUser();
-        int number = orderFlowService.removeCheckout(checkoutId, user.getId());
+        int number = orderFlowService.removeCheckout(id, user.getId());
         return ResponseHelp.success(number);
     }
 
@@ -134,7 +140,6 @@ public class OrderController {
         User user = (User) shiroHelp.getLoginUser();
         UserOrderCheckout checkout = Transform.dtoToEntity(dto);
         UserOrder order = orderFlowService.makeOrderWithSpeed(user.getId(), checkout);
-        orderFlowService.payed(order.getId(), user.getId(), 123123123L, new Date(), PayMethod.WECHAT, "ceshi");
         return ResponseHelp.success(detail(user.getId(), order, null));
     }
 
@@ -149,7 +154,7 @@ public class OrderController {
         if (!order.getUserId().equals(user.getId())){
             throw new ParameterException("订单不存在");
         }
-        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), order.getMoney(), PayChannel.WECHAT_QR, request);
+        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), BigDecimal.valueOf(0.1), PayChannel.WECHAT_QR, request);
         return ResponseHelp.success(data);
     }
 
@@ -164,7 +169,7 @@ public class OrderController {
         if (!order.getUserId().equals(user.getId())){
             throw new ParameterException("订单不存在");
         }
-        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), order.getMoney(), PayChannel.WECHAT_JS, request);
+        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), BigDecimal.valueOf(0.1), PayChannel.WECHAT_JS, request);
         return ResponseHelp.success(data);
     }
 
@@ -179,14 +184,14 @@ public class OrderController {
         if (!order.getUserId().equals(user.getId())){
             throw new ParameterException("订单不存在");
         }
-        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), order.getMoney(), PayChannel.ALIPAY_QR, request);
+        PayResponseData data = tradeService.pay(user.getId(), "千行GMAT", "千行服务购买", PayModule.ORDER, order.getId(), BigDecimal.valueOf(0.1), PayChannel.ALIPAY_QR, request);
         return ResponseHelp.success(data);
     }
 
     @RequestMapping(value = "/pay/query", method = RequestMethod.GET)
     @ApiOperation(value = "支付结果查询", notes = "支付结果查询", httpMethod = "GET")
     public Response<Boolean> response(
-            @RequestParam(required = true, name="id") Long orderId
+            @RequestParam(required = true) Long orderId
     ) {
         User user = (User) shiroHelp.getLoginUser();
         UserOrder order = userOrderService.get(orderId);
@@ -218,10 +223,8 @@ public class OrderController {
         List<UserOrderDetailDto> pr = Transform.convert(p, UserOrderDetailDto.class);
         Collection orderIds = Transform.getIds(p, UserOrder.class, "id");
 
-        List<UserOrderRecord> recordList = userOrderRecordService.allByUser(user.getId(), orderIds).stream().filter(row->row.getParentId()==0).collect(Collectors.toList());
-        List<UserOrderRecordListDto> records = Transform.convert(recordList, UserOrderRecordListDto.class);
-        Map<Object, List<UserOrderRecordListDto>> recordMap = Transform.getMapList(records, UserOrderRecordListDto.class, "orderId");
-        Transform.combine(pr, recordMap, UserOrderDetailDto.class, "id", "checkouts");
+        List<UserOrderRecord> recordList = userOrderRecordService.allByUser(user.getId(), orderIds);
+        List<UserOrderRecordExtendDto> records = Transform.convert(recordList, UserOrderRecordExtendDto.class);
 
         // 绑定服务
         Map<Object, JSONObject> serviceList = new HashMap<Object, JSONObject>(){{
@@ -232,20 +235,29 @@ public class OrderController {
             Setting qxCatSetting = settingService.getByKey(SettingKey.SERVICE_QX_CAT);
             put(ServiceKey.QX_CAT.key, qxCatSetting.getValue());
         }};
-        List<UserOrderRecordListDto> prService = records.stream().filter((row)-> row.getProductType().equals(ProductType.SERVICE.key)).collect(Collectors.toList());
-        Transform.combine(prService, serviceList, UserOrderRecordListDto.class, "service", "serviceInfo");
+        List<UserOrderRecordExtendDto> prService = records.stream().filter((row)-> row.getProductType().equals(ProductType.SERVICE.key)).collect(Collectors.toList());
+        Transform.combine(prService, serviceList, UserOrderRecordExtendDto.class, "service", "serviceInfo");
 
         // 绑定课程
-        List<UserOrderRecordListDto> prCourse = records.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE.key)).collect(Collectors.toList());
-        Collection courseIds = Transform.getIds(prCourse, UserOrderRecordListDto.class, "productId");
+        List<UserOrderRecordExtendDto> prCourse = records.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE.key)).collect(Collectors.toList());
+        Collection courseIds = Transform.getIds(prCourse, UserOrderRecordExtendDto.class, "productId");
         List<Course> courseList = courseService.select(courseIds);
-        Transform.combine(prCourse, courseList, UserOrderRecordListDto.class, "productId", "course", Course.class, "id", CourseExtendDto.class);
+        Transform.combine(prCourse, courseList, UserOrderRecordExtendDto.class, "productId", "course", Course.class, "id", CourseExtendDto.class);
 
         // 绑定资料
-        List<UserOrderRecordListDto> prData = records.stream().filter((row)-> row.getProductType().equals(ProductType.DATA.key)).collect(Collectors.toList());
-        Collection dataIds = Transform.getIds(prData, UserOrderRecordListDto.class, "productId");
+        List<UserOrderRecordExtendDto> prData = records.stream().filter((row)-> row.getProductType().equals(ProductType.DATA.key)).collect(Collectors.toList());
+        Collection dataIds = Transform.getIds(prData, UserOrderRecordExtendDto.class, "productId");
         List<CourseData> dataList = courseDataService.select(dataIds);
-        Transform.combine(prData, dataList, UserOrderRecordListDto.class, "productId", "data", CourseData.class, "id", CourseDataExtendDto.class);
+        Transform.combine(prData, dataList, UserOrderRecordExtendDto.class, "productId", "data", CourseData.class, "id", CourseDataExtendDto.class);
+
+        // 绑定套餐
+        List<UserOrderRecordExtendDto> prPackage = records.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE_PACKAGE.key)).collect(Collectors.toList());
+        Collection packageIds = Transform.getIds(prPackage, UserOrderRecordExtendDto.class, "productId");
+        List<CoursePackage> packageList = coursePackageService.select(packageIds);
+        Transform.combine(prPackage, packageList, UserOrderRecordExtendDto.class, "productId", "coursePackage", CoursePackage.class, "id", CoursePackageExtendDto.class);
+
+        Map<Object, List<UserOrderRecordExtendDto>> recordMap = Transform.getMapList(group(records), UserOrderRecordExtendDto.class, "orderId");
+        Transform.combine(pr, recordMap, UserOrderDetailDto.class, "id", "checkouts");
 
         // 绑定发票
         List<UserInvoice> userInvoiceList = userInvoiceService.listByOrder(user.getId(), orderIds);
@@ -270,11 +282,19 @@ public class OrderController {
     )  {
         User user = (User) shiroHelp.getLoginUser();
         UserOrder order = userOrderService.get(id);
+        System.out.println(user.getId());
         if (!order.getUserId().equals(user.getId())){
             throw new ParameterException("记录不存在");
         }
+        List<UserOrderRecordExtendDto> dtos;
+
         List<UserOrderRecord> list = userOrderRecordService.allByUser(user.getId(), id);
-        List<UserOrderRecordExtendDto> dtos = Transform.convert(list, UserOrderRecordExtendDto.class);
+        if (list.size() == 0){
+            List<UserOrderCheckout> checkouts = userOrderCheckoutService.allByUser(user.getId(), id);
+            dtos = Transform.convert(checkouts, UserOrderRecordExtendDto.class);
+        }else{
+            dtos = Transform.convert(list, UserOrderRecordExtendDto.class);
+        }
 
         return ResponseHelp.success(detail(user.getId(), order, dtos));
     }
@@ -323,6 +343,12 @@ public class OrderController {
         List<CourseData> dataList = courseDataService.select(dataIds);
         Transform.combine(prData, dataList, UserOrderRecordListDto.class, "productId", "data", CourseData.class, "id", CourseDataExtendDto.class);
 
+        // 绑定套餐
+        List<UserOrderRecordListDto> prPackage = pr.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE_PACKAGE.key)).collect(Collectors.toList());
+        Collection packageIds = Transform.getIds(prPackage, UserOrderRecordListDto.class, "productId");
+        List<CoursePackage> packageList = coursePackageService.select(packageIds);
+        Transform.combine(prPackage, packageList, UserOrderRecordListDto.class, "productId", "coursePackage", CoursePackage.class, "id", CoursePackageExtendDto.class);
+
 
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
@@ -371,43 +397,71 @@ public class OrderController {
     }
 
     /**
+     * 根据parentId分组
+     * @param records
+     */
+    public List<UserOrderRecordExtendDto> group(List<UserOrderRecordExtendDto> records){
+        List<UserOrderRecordExtendDto> parents = records.stream().filter((row)-> row.getParentId() == 0).collect(Collectors.toList());
+        Map recordMap = Transform.getMapList(records, UserOrderRecordExtendDto.class, "parentId");
+        Transform.combine(parents, recordMap, UserOrderRecordExtendDto.class, "id", "children");
+        return parents;
+    }
+
+    /**
      * 统一处理订单以及完整购物车返回信息
      * @param userId
      * @param order
-     * @param list
+     * @param records
      * @return
      */
-    public UserOrderDetailDto detail(Integer userId, UserOrder order, List<UserOrderRecordExtendDto> list)  {
+    public UserOrderDetailDto detail(Integer userId, UserOrder order, List<UserOrderRecordExtendDto> records)  {
         UserOrderDetailDto dto = Transform.convert(order, UserOrderDetailDto.class);
-        if (list == null){
-            list = Transform.convert(userOrderCheckoutService.allByUser(userId, order.getId()), UserOrderRecordExtendDto.class);
+        if (records == null){
+            records = Transform.convert(userOrderCheckoutService.allByUser(userId, order.getId()), UserOrderRecordExtendDto.class);
         }
-        dto.setCheckouts(list);
 
-        // 获取所有课程信息
-        List<UserOrderRecordExtendDto> courseCheckout = list.stream().filter((checkout)-> checkout.getProductType().equals(ProductType.COURSE.key)).collect(Collectors.toList());
-        Collection courseIds = Transform.getIds(courseCheckout, UserOrderRecordExtendDto.class, "productId");
+        // 绑定服务
+        Map<Object, JSONObject> serviceList = new HashMap<Object, JSONObject>(){{
+            Setting vipSetting = settingService.getByKey(SettingKey.SERVICE_VIP);
+            put(ServiceKey.VIP.key, vipSetting.getValue());
+            Setting textbookSetting = settingService.getByKey(SettingKey.SERVICE_TEXTBOOK);
+            put(ServiceKey.TEXTBOOK.key, textbookSetting.getValue());
+            Setting qxCatSetting = settingService.getByKey(SettingKey.SERVICE_QX_CAT);
+            put(ServiceKey.QX_CAT.key, qxCatSetting.getValue());
+        }};
+        List<UserOrderRecordExtendDto> prService = records.stream().filter((row)-> row.getProductType().equals(ProductType.SERVICE.key)).collect(Collectors.toList());
+        Transform.combine(prService, serviceList, UserOrderRecordExtendDto.class, "service", "serviceInfo");
+
+        // 绑定课程
+        List<UserOrderRecordExtendDto> prCourse = records.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE.key)).collect(Collectors.toList());
+        Collection courseIds = Transform.getIds(prCourse, UserOrderRecordExtendDto.class, "productId");
         List<Course> courseList = courseService.select(courseIds);
-        dto.setCourses(Transform.convert(courseList, CourseExtendDto.class));
+        Transform.combine(prCourse, courseList, UserOrderRecordExtendDto.class, "productId", "course", Course.class, "id", CourseExtendDto.class);
 
-        // 获取所有资料信息
-        List<UserOrderRecordExtendDto> dataCheckout = list.stream().filter((checkout)-> checkout.getProductType().equals(ProductType.DATA.key)).collect(Collectors.toList());
-        Collection dataIds = Transform.getIds(dataCheckout, UserOrderRecordExtendDto.class, "productId");
+        // 绑定资料
+        List<UserOrderRecordExtendDto> prData = records.stream().filter((row)-> row.getProductType().equals(ProductType.DATA.key)).collect(Collectors.toList());
+        Collection dataIds = Transform.getIds(prData, UserOrderRecordExtendDto.class, "productId");
         List<CourseData> dataList = courseDataService.select(dataIds);
-        dto.setDatas(Transform.convert(dataList, CourseDataExtendDto.class));
+        if (order.getPayStatus()==null||order.getPayStatus()==0){
+            courseExtendService.refreshDataResource(null, dataList);
+        }
+        Transform.combine(prData, dataList, UserOrderRecordExtendDto.class, "productId", "data", CourseData.class, "id", CourseDataExtendDto.class);
 
-        // 获取所有套餐信息
-        List<UserOrderRecordExtendDto> packageCheckout = list.stream().filter((checkout)-> checkout.getProductType().equals(ProductType.COURSE_PACKAGE.key)).collect(Collectors.toList());
-        Collection packageIds = Transform.getIds(packageCheckout, UserOrderRecordExtendDto.class, "productId");
+        // 绑定套餐
+        List<UserOrderRecordExtendDto> prPackage = records.stream().filter((row)-> row.getProductType().equals(ProductType.COURSE_PACKAGE.key)).collect(Collectors.toList());
+        Collection packageIds = Transform.getIds(prPackage, UserOrderRecordExtendDto.class, "productId");
         List<CoursePackage> packageList = coursePackageService.select(packageIds);
-        dto.setPackages(Transform.convert(packageList, CoursePackageExtendDto.class));
+        Transform.combine(prPackage, packageList, UserOrderRecordExtendDto.class, "productId", "coursePackage", CoursePackage.class, "id", CoursePackageExtendDto.class);
+
+        dto.setCheckouts(group(records));
 
         // 发票
         UserInvoice invoice = userInvoiceService.getByOrder(userId, order.getId());
         dto.setHasInvoice(invoice != null);
-        if (courseCheckout.size() > 0){
+        if (prCourse.size() > 0){
             dto.setCanInvoice(true);
         }
+
         return dto;
     }
 }

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

@@ -0,0 +1,68 @@
+package com.qxgmat.dto.admin.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.ReadyArticleCategory;
+
+
+@Dto(entity = ReadyArticleCategory.class)
+public class ReadyArticleCategoryDto {
+    private Integer id;
+
+    private Integer parentId;
+
+    private String title;
+
+    private Integer isData;
+
+    private Integer isOfficial;
+
+    private Integer index;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getParentId() {
+        return parentId;
+    }
+
+    public void setParentId(Integer parentId) {
+        this.parentId = parentId;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public Integer getIsData() {
+        return isData;
+    }
+
+    public void setIsData(Integer isData) {
+        this.isData = isData;
+    }
+
+    public Integer getIsOfficial() {
+        return isOfficial;
+    }
+
+    public void setIsOfficial(Integer isOfficial) {
+        this.isOfficial = isOfficial;
+    }
+
+    public Integer getIndex() {
+        return index;
+    }
+
+    public void setIndex(Integer index) {
+        this.index = index;
+    }
+}

+ 8 - 8
server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ReadyArticleDto.java

@@ -52,14 +52,6 @@ public class ReadyArticleDto {
         this.parentCategoryId = parentCategoryId;
     }
 
-    public String getParentCategory() {
-        return parentCategory;
-    }
-
-    public void setParentCategory(String parentCategory) {
-        this.parentCategory = parentCategory;
-    }
-
     public Integer getCategoryId() {
         return categoryId;
     }
@@ -75,4 +67,12 @@ public class ReadyArticleDto {
     public void setCategory(String category) {
         this.category = category;
     }
+
+    public String getParentCategory() {
+        return parentCategory;
+    }
+
+    public void setParentCategory(String parentCategory) {
+        this.parentCategory = parentCategory;
+    }
 }

+ 11 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CoursePackageExtendDto.java

@@ -1,5 +1,6 @@
 package com.qxgmat.dto.extend;
 
+import com.alibaba.fastjson.JSONObject;
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.CoursePackage;
 
@@ -15,6 +16,8 @@ public class CoursePackageExtendDto {
 
     private BigDecimal price;
 
+    private JSONObject gift;
+
     public Integer getId() {
         return id;
     }
@@ -46,4 +49,12 @@ public class CoursePackageExtendDto {
     public void setCourseIds(Integer[] courseIds) {
         this.courseIds = courseIds;
     }
+
+    public JSONObject getGift() {
+        return gift;
+    }
+
+    public void setGift(JSONObject gift) {
+        this.gift = gift;
+    }
 }

+ 63 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/UserOrderRecordExtendDto.java

@@ -1,11 +1,16 @@
 package com.qxgmat.dto.extend;
 
 
+import com.alibaba.fastjson.JSONObject;
+
 import java.math.BigDecimal;
+import java.util.List;
 
 public class UserOrderRecordExtendDto {
     private Integer id;
 
+    private Integer orderId;
+
     private Integer parentId;
 
     private String productType;
@@ -26,6 +31,16 @@ public class UserOrderRecordExtendDto {
 
     private Integer useExpireDays;
 
+    private CourseExtendDto course;
+
+    private CourseDataExtendDto data;
+
+    private CoursePackageExtendDto coursePackage;
+
+    private JSONObject serviceInfo;
+
+    private List<UserOrderRecordExtendDto> children;
+
     public String getProductType() {
         return productType;
     }
@@ -113,4 +128,52 @@ public class UserOrderRecordExtendDto {
     public void setParentId(Integer parentId) {
         this.parentId = parentId;
     }
+
+    public CourseExtendDto getCourse() {
+        return course;
+    }
+
+    public void setCourse(CourseExtendDto course) {
+        this.course = course;
+    }
+
+    public CourseDataExtendDto getData() {
+        return data;
+    }
+
+    public void setData(CourseDataExtendDto data) {
+        this.data = data;
+    }
+
+    public CoursePackageExtendDto getCoursePackage() {
+        return coursePackage;
+    }
+
+    public void setCoursePackage(CoursePackageExtendDto coursePackage) {
+        this.coursePackage = coursePackage;
+    }
+
+    public JSONObject getServiceInfo() {
+        return serviceInfo;
+    }
+
+    public void setServiceInfo(JSONObject serviceInfo) {
+        this.serviceInfo = serviceInfo;
+    }
+
+    public List<UserOrderRecordExtendDto> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<UserOrderRecordExtendDto> children) {
+        this.children = children;
+    }
+
+    public Integer getOrderId() {
+        return orderId;
+    }
+
+    public void setOrderId(Integer orderId) {
+        this.orderId = orderId;
+    }
 }

+ 13 - 3
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserAskCourseDto.java

@@ -9,7 +9,9 @@ public class UserAskCourseDto {
 
     private Integer courseNoId;
 
-    private String position;
+    private Integer position;
+
+    private String originContent;
 
     private String content;
 
@@ -29,11 +31,11 @@ public class UserAskCourseDto {
         this.courseNoId = courseNoId;
     }
 
-    public String getPosition() {
+    public Integer getPosition() {
         return position;
     }
 
-    public void setPosition(String position) {
+    public void setPosition(Integer position) {
         this.position = position;
     }
 
@@ -44,4 +46,12 @@ public class UserAskCourseDto {
     public void setContent(String content) {
         this.content = content;
     }
+
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
 }

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

@@ -0,0 +1,13 @@
+package com.qxgmat.dto.request;
+
+public class UserCourseNoIdsDto {
+    private Integer[] courseNoIds;
+
+    public Integer[] getCourseNoIds() {
+        return courseNoIds;
+    }
+
+    public void setCourseNoIds(Integer[] courseNoIds) {
+        this.courseNoIds = courseNoIds;
+    }
+}

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

@@ -7,8 +7,6 @@ import com.qxgmat.data.dao.entity.UserExport;
 public class UserExportDto {
     private Object setting;
 
-    private String type;
-
     public Object getSetting() {
         return setting;
     }
@@ -16,12 +14,4 @@ public class UserExportDto {
     public void setSetting(Object setting) {
         this.setting = setting;
     }
-
-    public String getType() {
-        return type;
-    }
-
-    public void setType(String type) {
-        this.type = type;
-    }
 }

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

@@ -1,6 +1,6 @@
 package com.qxgmat.dto.request;
 
-public class UserQuestionIdsDto {
+public class UserQuestionNoIdsDto {
     private Integer[] questionNoIds;
 
     public Integer[] getQuestionNoIds() {

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

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

+ 3 - 33
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderDetailDto.java

@@ -25,7 +25,7 @@ public class UserOrderDetailDto {
 
     private BigDecimal originMoney;
 
-    private JSONObject promote;
+    private JSONArray promote;
 
     private JSONArray gift;
 
@@ -39,12 +39,6 @@ public class UserOrderDetailDto {
 
     private List<UserOrderRecordExtendDto> checkouts;
 
-    private List<CourseExtendDto> courses;
-
-    private List<CourseDataExtendDto> datas;
-
-    private List<CoursePackageExtendDto> packages;
-
     public BigDecimal getMoney() {
         return money;
     }
@@ -61,11 +55,11 @@ public class UserOrderDetailDto {
         this.originMoney = originMoney;
     }
 
-    public JSONObject getPromote() {
+    public JSONArray getPromote() {
         return promote;
     }
 
-    public void setPromote(JSONObject promote) {
+    public void setPromote(JSONArray promote) {
         this.promote = promote;
     }
 
@@ -77,30 +71,6 @@ public class UserOrderDetailDto {
         this.checkouts = checkouts;
     }
 
-    public List<CourseExtendDto> getCourses() {
-        return courses;
-    }
-
-    public void setCourses(List<CourseExtendDto> courses) {
-        this.courses = courses;
-    }
-
-    public List<CourseDataExtendDto> getDatas() {
-        return datas;
-    }
-
-    public void setDatas(List<CourseDataExtendDto> datas) {
-        this.datas = datas;
-    }
-
-    public List<CoursePackageExtendDto> getPackages() {
-        return packages;
-    }
-
-    public void setPackages(List<CoursePackageExtendDto> packages) {
-        this.packages = packages;
-    }
-
     public JSONArray getGift() {
         return gift;
     }

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

@@ -29,7 +29,7 @@ public class UserOrderRecordListDto {
 
     private CoursePackageExtendDto coursePackage;
 
-    private List<UserOrderRecordExtendDto> records;
+    private List<UserOrderRecordExtendDto> children;
 
     private String service;
 
@@ -259,14 +259,6 @@ public class UserOrderRecordListDto {
         this.coursePackage = coursePackage;
     }
 
-    public List<UserOrderRecordExtendDto> getRecords() {
-        return records;
-    }
-
-    public void setRecords(List<UserOrderRecordExtendDto> records) {
-        this.records = records;
-    }
-
     public Integer getExpireDays() {
         return expireDays;
     }
@@ -282,4 +274,12 @@ public class UserOrderRecordListDto {
     public void setUseExpireDays(Integer useExpireDays) {
         this.useExpireDays = useExpireDays;
     }
+
+    public List<UserOrderRecordExtendDto> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<UserOrderRecordExtendDto> children) {
+        this.children = children;
+    }
 }

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

@@ -29,6 +29,8 @@ public class UserStudyDayDto {
 
     private Integer courseNumber;
 
+    private Boolean isCourse;
+
     private UserRankStatRelation courseExceed;
 
     private List<UserCourseResultExtendDto> courseList;
@@ -128,4 +130,12 @@ public class UserStudyDayDto {
     public void setCourseList(List<UserCourseResultExtendDto> courseList) {
         this.courseList = courseList;
     }
+
+    public Boolean getCourse() {
+        return isCourse;
+    }
+
+    public void setCourse(Boolean course) {
+        isCourse = course;
+    }
 }

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

@@ -5,6 +5,8 @@ import com.nuliji.tools.AbstractService;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.nuliji.tools.mybatis.Example;
+import com.qxgmat.data.constants.enums.status.AskStatus;
+import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.UserNoteCourseMapper;
 import com.qxgmat.data.dao.entity.UserNoteCourse;
 import org.slf4j.Logger;
@@ -22,6 +24,30 @@ public class UserNoteCourseService extends AbstractService {
     @Resource
     private UserNoteCourseMapper userNoteCourseMapper;
 
+    public Page<UserNoteCourse> listByCourse(int page, int size, String keyword, Integer userId, Integer courseId, String order, DirectionStatus direction){
+        Example example = new Example(UserNoteCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("courseId", courseId)
+        );
+        if (keyword != null)
+            example.and(
+                    example.createCriteria()
+                            .orLike("content", "%"+keyword+"%")
+            );
+        if(order == null || order.isEmpty()) order = "id";
+        switch(direction){
+            case ASC:
+                example.orderBy(order).asc();
+                break;
+            case DESC:
+            default:
+                example.orderBy(order).desc();
+        }
+        return select(userNoteCourseMapper, example, page, size);
+    }
+
     public List<UserNoteCourse> listByCourse(Number courseId){
         Example example = new Example(UserNoteCourse.class);
         example.and(
@@ -85,6 +111,27 @@ public class UserNoteCourseService extends AbstractService {
         return relationMap;
     }
 
+    /**
+     * 删除笔记
+     * @param userId
+     * @param courseNoId
+     * @return
+     */
+    @Transactional
+    public boolean deleteNote(Integer userId, Integer courseNoId){
+        Example example = new Example(UserNoteCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("courseNoId", courseNoId)
+        );
+        UserNoteCourse in = one(userNoteCourseMapper, example);
+        if (in == null){
+            return true;
+        }
+        return delete(in.getId());
+    }
+
     public UserNoteCourse add(UserNoteCourse message){
         int result = insert(userNoteCourseMapper, message);
         message = one(userNoteCourseMapper, message.getId());

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

@@ -187,7 +187,7 @@ public class UserNoteQuestionService extends AbstractService {
     }
 
     /**
-     * 取消收藏题目编号
+     * 删除笔记
      * @param userId
      * @param questionId
      * @return

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

@@ -147,30 +147,30 @@ public class UsersService extends AbstractService {
                 throw new ParameterException("该微信账户已绑定其他手机号,您可直接使用微信登录");
             }
         }
-        openUser = User.builder()
-                .id(openUser != null ? openUser.getId() : user != null ? user.getId() : null)
+        User mm = User.builder()
+                .id(openUser != null ? openUser.getId() : null)
                 .build();
-        if (user ==null || user.getAvatar() == null || user.getAvatar().isEmpty()) openUser.setAvatar(data.getAvatar());
-        if (user == null || user.getNickname() == null|| user.getNickname().isEmpty() )openUser.setNickname(data.getNickName());
+        if (openUser ==null || openUser.getAvatar() == null || openUser.getAvatar().isEmpty()) mm.setAvatar(data.getAvatar());
+        if (openUser == null || openUser.getNickname() == null|| openUser.getNickname().isEmpty() )mm.setNickname(data.getNickName());
         switch(platform){
             case "wechat_pc":
-                openUser.setWechatOpenidPc(data.getOpenId());
-                openUser.setWechatUnionid(data.getUnionId());
+                mm.setWechatOpenidPc(data.getOpenId());
+                mm.setWechatUnionid(data.getUnionId());
                 break;
             case "wechat_native":
-                openUser.setWechatOpenidWechat(data.getOpenId());
-                openUser.setWechatUnionid(data.getUnionId());
-                openUser.setWechatAccessToken(data.getAccessToken());
-                openUser.setWechatRefreshToken(data.getRefreshToken());
-                openUser.setWechatExpireTime(data.getExpiresTime());
+                mm.setWechatOpenidWechat(data.getOpenId());
+                mm.setWechatUnionid(data.getUnionId());
+                mm.setWechatAccessToken(data.getAccessToken());
+                mm.setWechatRefreshToken(data.getRefreshToken());
+                mm.setWechatExpireTime(data.getExpiresTime());
                 break;
         }
-        if (openUser.getId() != null){
+        if (mm.getId() != null){
             // 直接更新数据
-            edit(openUser);
+            edit(mm);
         }
 
-        return openUser;
+        return mm;
     }
 
     /**

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

@@ -6,5 +6,5 @@ import java.util.List;
 
 
 public interface ChangeCheckout {
-    UserOrderCheckout callback(UserOrderCheckout checkout);
+    UserOrderCheckout callback(UserOrderCheckout originCheckout, UserOrderCheckout checkout);
 }

+ 19 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java

@@ -301,4 +301,23 @@ public class CourseExtendService {
             courseData.setResource(courseData.getTrailResource());
         }
     }
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseDataList
+     */
+    public void refreshDataResource(User user, List<CourseData> courseDataList){
+        // 处理权限
+        if (user != null){
+            for(CourseData courseData : courseDataList){
+                if (!userOrderRecordService.hasData(user.getId(), courseData.getId())){
+                    courseData.setResource(courseData.getTrailResource());
+                }
+            }
+        }else{
+            for(CourseData courseData : courseDataList){
+                courseData.setResource(courseData.getTrailResource());
+            }
+        }
+    }
 }

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

@@ -0,0 +1,59 @@
+package com.qxgmat.service.extend;
+
+import com.alibaba.fastjson.JSONObject;
+import com.qxgmat.data.constants.enums.user.ExportType;
+import com.qxgmat.data.dao.entity.UserExport;
+import com.qxgmat.service.UserNoteCourseService;
+import com.qxgmat.service.UserNoteQuestionService;
+import com.qxgmat.service.inline.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+@Service
+public class ExportService {
+
+    @Autowired
+    private UserNoteQuestionService userNoteQuestionService;
+
+    @Autowired
+    private UserNoteCourseService userNoteCourseService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Autowired
+    private QuestionNoService questionNoService;
+
+    @Resource
+    private UserExportService userExportService;
+
+    public UserExport addQuestion(Integer userId, JSONObject setting){
+        UserExport export = UserExport.builder()
+                .userId(userId)
+                .type(ExportType.QUESTION.key)
+                .setting(setting)
+                .build();
+        return userExportService.add(export);
+    }
+
+    public UserExport addQuestionNote(Integer userId, JSONObject setting){
+        UserExport export = UserExport.builder()
+                .userId(userId)
+                .type(ExportType.NOTE_QUESTION.key)
+                .setting(setting)
+                .build();
+        return userExportService.add(export);
+    }
+
+    public UserExport addCourseNote(Integer userId, JSONObject setting){
+        UserExport export = UserExport.builder()
+                .userId(userId)
+                .type(ExportType.NOTE_COURSE.key)
+                .setting(setting)
+                .build();
+
+        return userExportService.add(export);
+    }
+}

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

@@ -60,6 +60,9 @@ public class MessageExtendService {
                 case WECHAT:
                     sendWechat(user.getWechatOpenidWechat(), category, params);
                     break;
+                case SMS:
+                    sendSms(user.getArea(), user.getMobile(), category, params);
+                    break;
                 default:
                     throw new ParameterException("消息发送方式错误");
             }
@@ -87,6 +90,10 @@ public class MessageExtendService {
         wechatHelp.sendMessage(openId, category, params);
     }
 
+    private void sendSms(String area, String mobile, MessageCategory category, Map<String, String> params){
+
+    }
+
     public void sendCustom(User user, MessageTemplate template, Map<String, String>params){
         params.put("nickname", user.getNickname());
         MessageMethod messageMethod = MessageMethod.ValueOf(template.getMessageMethod());
@@ -310,10 +317,21 @@ public class MessageExtendService {
     }
 
     /**
+     * 邮箱绑定
+     * @param user
+     */
+    public void sendEmailBind(User user){
+        Map<String, String> map = new HashMap<>();
+        map.put("nickname", user.getNickname());
+        map.put("email", user.getEmail());
+        send(user, MessageCategory.EMAIL_CHANGE, map);
+    }
+
+    /**
      * 邮箱变更
      * @param user
      */
-    public void sendEmailChange(User user){
+    public void sendEmailChange(User user, String email){
         Map<String, String> map = new HashMap<>();
         map.put("nickname", user.getNickname());
         map.put("email", user.getEmail());

+ 49 - 14
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -116,6 +116,13 @@ public class OrderFlowService {
                     // 累加课时
                     checkout.setId(tmp.getId());
                     checkout.setNumber(tmp.getNumber() + checkout.getNumber());
+                    if (mainCourse.getMaxNumber() < checkout.getNumber()){
+                        checkout.setNumber(mainCourse.getMaxNumber());
+                    }
+                }else{
+                    if (mainCourse.getMinNumber() > checkout.getNumber()){
+                        checkout.setNumber(mainCourse.getMinNumber());
+                    }
                 }
                 checkout.setOriginMoney(mainCourse.getPrice().multiply(BigDecimal.valueOf(checkout.getNumber())));
                 int percent = toolsService.computeVsMoney(checkout);
@@ -259,7 +266,7 @@ public class OrderFlowService {
 
             if (serviceKey == ServiceKey.TEXTBOOK){
                 // 是否存在半价机经券
-                User user = usersService.get(checkout.getId());
+                User user = usersService.get(checkout.getUserId());
                 checkout.setMoney(user.getTextbookHalf() > 0 ? money.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_UP):money);
             }else{
                 checkout.setMoney(money);
@@ -267,11 +274,17 @@ public class OrderFlowService {
             return checkout;
         }));
 
-        changeCheckoutCallback.put(ProductType.COURSE, (checkout->{
-            Course mainCourse = courseService.get(checkout.getProductId());
+        changeCheckoutCallback.put(ProductType.COURSE, ((originCheckout, checkout)->{
+            Course mainCourse = courseService.get(originCheckout.getProductId());
 
             // 判断是否是1v1课程
             if (mainCourse.getCourseModule().equals(CourseModule.VS.key)){
+                if (mainCourse.getMaxNumber() < checkout.getNumber()){
+                    throw new ParameterException("超过最大购买上限");
+                }
+                if (mainCourse.getMinNumber() > checkout.getNumber()){
+                    throw new ParameterException("超过最小购买下限");
+                }
                 checkout.setOriginMoney(mainCourse.getPrice().multiply(BigDecimal.valueOf(checkout.getNumber())));
                 int percent = toolsService.computeVsMoney(checkout);
                 checkout.setMoney(checkout.getOriginMoney().multiply(BigDecimal.valueOf(percent)).divide(BigDecimal.valueOf(100), BigDecimal.ROUND_HALF_UP));
@@ -312,9 +325,10 @@ public class OrderFlowService {
                         JSONObject info = new JSONObject();
                         info.put("originMoney", originMoney);
                         info.put("money", money);
-                        info.put("key", "half");
-                        JSONObject promote = order.getPromote();
-                        promote.put("textbook", info);
+                        info.put("key", "textbook-half");
+                        info.put("message", "半价机经券");
+                        JSONArray promote = order.getPromote();
+                        promote.add(info);
                     }
                 }
                 money = money.add(checkout.getMoney());
@@ -352,8 +366,10 @@ public class OrderFlowService {
             // 视频课程优惠
             List<UserOrderCheckout> videoCheckout = courseCheckout.stream().filter((checkout)-> courseMap.get(checkout.getProductId()).getCourseModule().equals(CourseModule.VIDEO.key)).collect(Collectors.toList());
             BigDecimal videoMoney = BigDecimal.valueOf(0);
+            BigDecimal videoOriginMoney = BigDecimal.valueOf(0);
             for(UserOrderCheckout checkout : videoCheckout){
                 videoMoney = videoMoney.add(checkout.getMoney());
+                videoOriginMoney = videoOriginMoney.add(checkout.getOriginMoney());
             }
             int percent = toolsService.computeVideoMoney(courseCheckout);
             if (percent < 100){
@@ -366,13 +382,14 @@ public class OrderFlowService {
                 info.put("originMoney", videoMoney);
                 info.put("money", promoteVideoMoney);
                 info.put("message", toolsService.videoMoneyMessage());
-                JSONObject promote = order.getPromote();
-                promote.put("video", info);
+                info.put("key", "video");
+                JSONArray promote = order.getPromote();
+                promote.add(info);
             }else{
                 courseMoney = courseMoney.add(videoMoney);
                 money = money.add(videoMoney);
             }
-            originMoney = originMoney.add(videoMoney);
+            originMoney = originMoney.add(videoOriginMoney);
 
             BigDecimal vsMoney = BigDecimal.valueOf(0);
             // 1v1课程优惠: 添加时计算
@@ -380,8 +397,19 @@ public class OrderFlowService {
             for(UserOrderCheckout checkout: vsCheckout){
                 vsMoney  = vsMoney.add(checkout.getMoney());
                 money = money.add(checkout.getMoney());
-                originMoney = originMoney.add(checkout.getMoney());
+                originMoney = originMoney.add(checkout.getOriginMoney());
                 vsNumber += checkout.getNumber();
+
+                if(!money.equals(originMoney)){
+                    // 添加1v1优惠记录
+                    JSONObject info = new JSONObject();
+                    info.put("originMoney", originMoney);
+                    info.put("money", money);
+                    info.put("message", toolsService.vsMoneyMessage());
+                    info.put("key", "vs");
+                    JSONArray promote = order.getPromote();
+                    promote.add(info);
+                }
             }
 
             // 套餐费用: 添加时计算
@@ -389,7 +417,7 @@ public class OrderFlowService {
             for(UserOrderCheckout checkout : packageCheckout){
                 courseMoney = courseMoney.add(checkout.getMoney());
                 money = money.add(checkout.getMoney());
-                originMoney = originMoney.add(checkout.getMoney());
+                originMoney = originMoney.add(checkout.getOriginMoney());
                 // 套餐优惠记录 - 不参与总价,单独展示逻辑
             }
 
@@ -765,6 +793,8 @@ public class OrderFlowService {
                         UserOrderRecord record = UserOrderRecord.builder()
                                 .orderId(userOrder.getId())
                                 .userId(userOrder.getUserId())
+                                // 赠品不出现在订单详细列表中
+                                .parentId(Integer.MAX_VALUE)
                                 .productType(ProductType.COURSE.key)
                                 .productId(courseId)
                                 .number(info.getIntValue("number"))
@@ -786,6 +816,10 @@ public class OrderFlowService {
 
         // 发送通知
         User user = usersService.get(userId);
+        if (user.getIsCourse() == 0 && userOrder.getProductTypes().contains(ProductType.COURSE.key)){
+            // 设定标志
+            usersService.edit(User.builder().id(userOrder.getUserId()).isCourse(1).build());
+        }
         messageExtendService.sendPayed(user, userOrder);
         return true;
     }
@@ -821,11 +855,12 @@ public class OrderFlowService {
      */
     @Transactional
     public int changeCheckout(Integer userId, UserOrderCheckout checkout){
-        ChangeCheckout callback = changeCheckoutCallback.get(ProductType.ValueOf(checkout.getProductType()));
+        UserOrderCheckout in = userOrderCheckoutService.get(checkout.getId());
+        ChangeCheckout callback = changeCheckoutCallback.get(ProductType.ValueOf(in.getProductType()));
         if (callback == null){
             throw new ParameterException("无法修改记录");
         }
-        checkout = callback.callback(checkout);
+        checkout = callback.callback(in, checkout);
         if (checkout == null){
             // 无需操作
         }else{
@@ -886,7 +921,7 @@ public class OrderFlowService {
                 .money(BigDecimal.valueOf(0))
                 .build();
         // 记录订单优惠
-        JSONObject promote = new JSONObject();
+        JSONArray promote = new JSONArray();
         order.setPromote(promote);
         // 记录订单类型
         JSONArray productTypes = new JSONArray();

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

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

+ 14 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/ToolsService.java

@@ -416,6 +416,17 @@ public class ToolsService {
     }
 
     /**
+     * 获取1v1课程优惠文字说明:{vs: {text:}}
+     * @return
+     */
+    public String vsMoneyMessage(){
+        Setting setting = settingService.getByKey(SettingKey.PROMOTE);
+        JSONObject value = setting.getValue();
+        JSONObject video = value.getJSONObject("vs");
+        return video.getString("text");
+    }
+
+    /**
      * 计算1v1课程优惠:{vs_list: [{"number":32,"percent":2}]},单个课程计算
      * @param checkout
      * @return
@@ -476,6 +487,7 @@ public class ToolsService {
         JSONObject value = setting.getValue();
 
         JSONArray settings = value.getJSONArray("ask_time");
+        if (settings == null) return null;
         int max = 0;
         int maxIndex = -1;
         for(int i = 0; i < settings.size(); i++){
@@ -505,6 +517,7 @@ public class ToolsService {
         JSONObject value = setting.getValue();
 
         JSONArray settings = value.getJSONArray("vs_video_money");
+        if (settings == null) return null;
         int max = 0;
         int maxIndex = -1;
         for(int i = 0; i < settings.size(); i++){
@@ -534,6 +547,7 @@ public class ToolsService {
         JSONObject value = setting.getValue();
 
         JSONArray settings = value.getJSONArray("vs_vs_number");
+        if (settings == null) return null;
         int max = 0;
         int maxIndex = -1;
         for(int i = 0; i < settings.size(); i++){

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

@@ -29,7 +29,7 @@ public class CourseService extends AbstractService {
     @Resource
     private CourseRelationMapper courseRelationMapper;
 
-    public Page<Course> listAdmin(int page, int size, String keyword, CourseModule module, Integer structId, Boolean excludeVs, String order, DirectionStatus direction){
+    public Page<Course> listAdmin(int page, int size, String keyword, CourseModule module, Integer structId, Boolean excludeVs, Boolean excludeOnline, String order, DirectionStatus direction){
         Example example = new Example(Course.class);
         if (keyword != null) {
             example.and(
@@ -56,6 +56,12 @@ public class CourseService extends AbstractService {
                             .andNotEqualTo("courseModule", CourseModule.VS.key)
             );
         }
+        if (excludeVs != null) {
+            example.and(
+                    example.createCriteria()
+                            .andNotEqualTo("courseModule", CourseModule.ONLINE.key)
+            );
+        }
         if(order == null || order.isEmpty()) order = "id";
         switch(direction){
             case ASC:

+ 38 - 2
server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyArticleCategoryService.java

@@ -27,8 +27,8 @@ public class ReadyArticleCategoryService extends AbstractService {
 
     public List<ReadyArticleCategory> all(){
         Example example = new Example(ReadyArticleCategory.class);
-        example.setOrderByClause("id asc");
-        return select(readyArticleCategoryMapper);
+        example.setOrderByClause("parent_id asc, sort desc, id asc");
+        return select(readyArticleCategoryMapper, example);
     }
 
     public ReadyArticleCategory getCategory(String title, Integer parentId){
@@ -41,6 +41,16 @@ public class ReadyArticleCategoryService extends AbstractService {
         return one(readyArticleCategoryMapper, example);
     }
 
+    public List<ReadyArticleCategory> listByParentId( Integer parentId){
+        Example example = new Example(ReadyArticleCategory.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("parentId", parentId)
+        );
+        example.setOrderByClause("parent_id asc, sort desc, id asc");
+        return select(readyArticleCategoryMapper, example);
+    }
+
     public ReadyArticleCategory addCategory(String title, Integer parentId){
         ReadyArticleCategory in = getCategory(title, parentId);
         if (in == null){
@@ -52,6 +62,32 @@ public class ReadyArticleCategoryService extends AbstractService {
         return in;
     }
 
+    public void changeOrder(Integer id, Integer index){
+        ReadyArticleCategory in = get(id);
+        List<ReadyArticleCategory> list = listByParentId(in.getParentId());
+        int max = list.size()+1;
+        int i = -1;
+        for(ReadyArticleCategory category : list){
+            i += 1;
+            max = max - 1;
+            if (i==index){
+                continue;
+            }
+            if (category.getId().equals(in.getId())){
+                continue;
+            }
+            int tmp = max;
+            edit(ReadyArticleCategory.builder()
+                    .id(category.getId())
+                    .sort(tmp)
+                    .build());
+        }
+        edit(ReadyArticleCategory.builder()
+                .id(in.getId())
+                .sort(list.size()-index)
+                .build());
+    }
+
     public ReadyArticleCategory add(ReadyArticleCategory entity){
         int result = insert(readyArticleCategoryMapper, entity);
         entity = one(readyArticleCategoryMapper, entity.getId());

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

@@ -25,6 +25,16 @@ public class ReadyArticleService extends AbstractService {
     @Resource
     private ReadyArticleMapper readyArticleMapper;
 
+    public void deleteByCategory(Integer categoryId){
+        Example example = new Example(ReadyArticle.class);
+        example.and(
+                example.createCriteria()
+                        .orEqualTo("categoryId", categoryId)
+                        .orEqualTo("parentCategoryId", categoryId)
+        );
+        delete(readyArticleMapper, example);
+    }
+
     public Page<ReadyArticle> listAdmin(int page, int size, Integer parentCategoryId, Integer categoryId, String order, DirectionStatus direction){
         Example example = new Example(ReadyArticle.class);
         if (parentCategoryId != null)

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


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