Kaynağa Gözat

feat(front): 支付

Go 4 yıl önce
ebeveyn
işleme
d145ebb477
67 değiştirilmiş dosya ile 2034 ekleme ve 611 silme
  1. 24 10
      front/project/Constant.js
  2. 1 1
      front/project/admin/routes/course/package/page.js
  3. 180 6
      front/project/admin/routes/ready/article/page.js
  4. 0 11
      front/project/admin/routes/setting/service/page.js
  5. 2 6
      front/project/admin/routes/user/orderDetail/page.js
  6. 8 0
      front/project/admin/stores/ready.js
  7. 1 1
      front/project/h5/index.js
  8. 1 1
      front/project/h5/routes/page/invitation/page.js
  9. 1 0
      front/project/h5/routes/page/pay/index.less
  10. 140 44
      front/project/h5/routes/page/pay/page.js
  11. 11 9
      front/project/h5/stores/common.js
  12. 4 0
      front/project/h5/stores/my.js
  13. 4 4
      front/project/h5/stores/order.js
  14. 12 2
      front/project/h5/stores/user.js
  15. 8 6
      front/project/www/app.js
  16. 8 0
      front/project/www/app.less
  17. 3 3
      front/project/www/components/Card/index.js
  18. 2 0
      front/project/www/components/ListTable/index.less
  19. 8 3
      front/project/www/components/Login/index.js
  20. 4 0
      front/project/www/components/Module/index.less
  21. 334 58
      front/project/www/components/PayModal/index.js
  22. 107 36
      front/project/www/components/VipRenew/index.js
  23. 23 2
      front/project/www/components/VipRenew/index.less
  24. 2 0
      front/project/www/layouts/User/index.js
  25. 15 4
      front/project/www/routes/examination/main/page.js
  26. 7 3
      front/project/www/routes/exercise/main/page.js
  27. 33 37
      front/project/www/routes/my/course/page.js
  28. 27 5
      front/project/www/routes/my/main/page.js
  29. 6 21
      front/project/www/routes/my/order/page.js
  30. 0 1
      front/project/www/routes/my/tools/page.js
  31. 1 2
      front/project/www/routes/page/cart/index.js
  32. 228 90
      front/project/www/routes/page/cart/page.js
  33. 9 0
      front/project/www/routes/page/order/index.js
  34. 113 55
      front/project/www/routes/page/order/page.js
  35. 6 7
      front/project/www/stores/order.js
  36. 83 4
      front/project/www/stores/user.js
  37. 96 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticleCategory.java
  38. 51 16
      server/data/src/main/java/com/qxgmat/data/dao/entity/User.java
  39. 4 5
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrder.java
  40. 4 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/ReadyArticleCategoryMapper.xml
  41. 4 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMapper.xml
  42. 1 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserOrderMapper.xml
  43. 5 1
      server/data/src/main/resources/db/migration/V1__init_table.sql
  44. 1 1
      server/data/src/main/resources/mybatis-generator.xml
  45. 2 1
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java
  46. 22 4
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/ReadyController.java
  47. 15 18
      server/gateway-api/src/main/java/com/qxgmat/controller/api/AuthController.java
  48. 1 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  49. 95 41
      server/gateway-api/src/main/java/com/qxgmat/controller/api/OrderController.java
  50. 68 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ReadyArticleCategoryDto.java
  51. 8 8
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/request/ReadyArticleDto.java
  52. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CoursePackageExtendDto.java
  53. 63 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/UserOrderRecordExtendDto.java
  54. 3 33
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderDetailDto.java
  55. 9 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderRecordListDto.java
  56. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserStudyDayDto.java
  57. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/annotation/ChangeCheckout.java
  58. 19 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java
  59. 36 14
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  60. 14 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ToolsService.java
  61. 7 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java
  62. 38 2
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyArticleCategoryService.java
  63. 10 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyArticleService.java
  64. 2 2
      server/gateway-api/src/main/profile/dev/application-runtime.yml
  65. 5 11
      server/gateway-api/src/main/resources/application.yml
  66. 4 2
      server/tools/src/main/java/com/nuliji/tools/pay/Alipay.java
  67. 9 4
      server/tools/src/main/java/com/nuliji/tools/third/wechat/WechatClient.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;

+ 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>;
   }
 }

+ 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: [

+ 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);
   }

+ 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) {

+ 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

@@ -338,6 +338,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>

+ 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;

+ 8 - 3
front/project/www/components/Login/index.js

@@ -38,7 +38,12 @@ export default class Login extends Component {
     );
     Main.getContract('register')
       .then(result => {
-        this.setState({ contract: result });
+        this.setState({ registerContract: result });
+      });
+
+    Main.getContract('privacy')
+      .then(result => {
+        this.setState({ privacyContract: result });
       });
   }
 
@@ -199,7 +204,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 +249,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;
+  }
 }

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

@@ -1,37 +1,232 @@
 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 };
+    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 +235,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 +273,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">
+              商品: 购物车结账<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 +326,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 +369,7 @@ export class PayKBankModal extends Component {
       <Modal
         show={show}
         width={630}
-        title="购买千行课堂"
+        title="银行汇款"
         btnAlign="center"
         confirmText="好的,知道了"
         onConfirm={onConfirm}
@@ -183,3 +384,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>
+    );
+  }
+}

+ 107 - 36
front/project/www/components/VipRenew/index.js

@@ -1,20 +1,24 @@
 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 };
+    this.state = { tab: '2', pay: '', select: null };
     Main.getService('vip')
       .then(result => {
         result.package = result.package.map((row, index) => {
@@ -26,9 +30,19 @@ export default class extends Component {
       });
   }
 
+  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 +73,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 +83,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 +133,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>;
             })
           ) : (

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

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { asyncSMessage } from '@src/services/AsyncTools';
+// import { asyncSMessage } from '@src/services/AsyncTools';
 import { formatTreeData, formatPercent, formatDate } from '@src/services/Tools';
 import Panel, { WaitPanel, BuyPanel, SmallPanel, SmallWaitPanel, SmallBuyPanel } from '../../../components/Panel';
 import Tabs from '../../../components/Tabs';
@@ -183,7 +183,6 @@ export default class extends Page {
   // 开通模考或者机经
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refresh();
     });
   }
@@ -203,14 +202,26 @@ 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();
       });
   }
 

+ 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={() => {

+ 33 - 37
front/project/www/routes/my/course/page.js

@@ -131,7 +131,7 @@ export default class extends Page {
     });
   }
 
-  onAction() {}
+  onAction() { }
 
   onTabChange(tab) {
     const data = { tab };
@@ -245,7 +245,6 @@ export default class extends Page {
 
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refreshDetail(recordId);
     });
   }
@@ -367,7 +366,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];
@@ -713,8 +712,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%'}`;
         },
       },
       {
@@ -1667,20 +1665,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 ? (
@@ -1690,26 +1687,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 (

+ 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;
       });

+ 0 - 1
front/project/www/routes/my/tools/page.js

@@ -320,7 +320,6 @@ export default class extends Page {
 
   open(recordId) {
     Order.useRecord(recordId).then(() => {
-      asyncSMessage('开通成功');
       this.refresh();
     });
   }

+ 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={() => onChangeNumber(data.number + 1)} />,
+            <Icon className="down" type="caret-down" onClick={() => data.number === 1 && 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>
     );
   }

+ 6 - 7
front/project/www/stores/order.js

@@ -1,5 +1,4 @@
 import BaseStore from '@src/stores/base';
-// import * as querystring from 'querystring';
 
 export default class OrderStore extends BaseStore {
   allCheckout() {
@@ -10,16 +9,16 @@ 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() {
-    return this.apiPost('/order/pay/confirm');
+  confirmPay(courseId) {
+    return this.apiPost('/order/pay/confirm', { courseId });
   }
 
   speedPay({ productType, productId, service, param, number }) {

+ 83 - 4
front/project/www/stores/user.js

@@ -1,6 +1,63 @@
 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') {
+    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 +83,28 @@ 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 });
+    });
+  }
+
+  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 +120,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 +140,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) {

+ 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;
         }
 

+ 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>

+ 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>

+ 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" />

+ 5 - 1
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),
@@ -942,7 +945,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),

+ 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){

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

@@ -89,13 +89,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 +108,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 +129,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 +166,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 +212,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);

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

@@ -573,6 +573,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);
     }

+ 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;
+    }
 }

+ 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;
+    }
 }

+ 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());
+            }
+        }
+    }
 }

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

@@ -259,7 +259,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,8 +267,8 @@ 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)){
@@ -312,9 +312,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 +353,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 +369,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 +384,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 +404,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 +780,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 +803,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 +842,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 +908,7 @@ public class OrderFlowService {
                 .money(BigDecimal.valueOf(0))
                 .build();
         // 记录订单优惠
-        JSONObject promote = new JSONObject();
+        JSONArray promote = new JSONArray();
         order.setPromote(promote);
         // 记录订单类型
         JSONArray productTypes = new JSONArray();

+ 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)

+ 2 - 2
server/gateway-api/src/main/profile/dev/application-runtime.yml

@@ -100,12 +100,12 @@ third:
     questionTemplate: 123123
     courseTemplate: 1312
 
-  redirectUrl: http://www.qianxing.com/gateway/oauth
+  redirectUrl: http://test.qianxing.com/gateway/oauth
 
 pay:
   wechat:
     appId: wx65c7d378b4184bcc
-  notifyUrl: http://www.qianxing.com/gateway/pay
+  notifyUrl: http://test.duoshaojiaoyu.com/gateway/pay
 
 video:
   ffmpeg: ffmpeg

Dosya farkı çok büyük olduğundan ihmal edildi
+ 5 - 11
server/gateway-api/src/main/resources/application.yml


+ 4 - 2
server/tools/src/main/java/com/nuliji/tools/pay/Alipay.java

@@ -3,6 +3,7 @@ package com.nuliji.tools.pay;
 import com.alibaba.fastjson.JSON;
 import com.alipay.api.AlipayApiException;
 import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConstants;
 import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.AlipayTradeAppPayModel;
 import com.alipay.api.domain.AlipayTradePrecreateModel;
@@ -41,7 +42,8 @@ public class Alipay implements PaySource{
         this.appId = appId;
         this.appKey = appKey;
         this.appPublicKey = appPublicKey;
-        alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", appId, appKey, "json", "utf-8", appPublicKey, "RSA2");
+        logger.debug("alipay=>{},{}", appKey, appPublicKey);
+        alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", appId, appKey, "json", "utf-8", appPublicKey, AlipayConstants.SIGN_TYPE_RSA2);
     }
     @Override
     public ResultInfo notifyTrade(HttpServletRequest request) throws Exception{
@@ -188,7 +190,7 @@ public class Alipay implements PaySource{
         model.setOutTradeNo(payNo);
         model.setTimeoutExpress("30m");
         model.setTotalAmount(money.toString());
-        model.setProductCode("QUICK_MSECURITY_PAY");
+        model.setProductCode("FACE_TO_FACE_PAYMENT");
         request.setBizModel(model);
         request.setNotifyUrl(notifyUrl);
 

+ 9 - 4
server/tools/src/main/java/com/nuliji/tools/third/wechat/WechatClient.java

@@ -6,14 +6,13 @@ import com.nuliji.tools.third.OauthData;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
+import org.springframework.http.*;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.sql.Time;
 import java.util.*;
 
@@ -275,14 +274,20 @@ public class WechatClient {
             HttpHeaders headers = new HttpHeaders();
             MediaType type = MediaType.APPLICATION_JSON;
             headers.setContentType(type);
+            // 微信对英文发送utf8编码信息
+            headers.setAcceptLanguage(new ArrayList<Locale.LanguageRange>(){{add(new Locale.LanguageRange("en-US",0.9));}});
             RestTemplate restTemplate = new RestTemplate();
+
             String result ="";
             if (bodyParams == null){
-                result = restTemplate.getForObject(api+"?"+paramStr, String.class);
+                HttpEntity<String> baseEntity = new HttpEntity<>(null, headers);
+                ResponseEntity<String> response2 = restTemplate.exchange(api+"?"+paramStr, HttpMethod.GET, baseEntity, String.class);
+                result = response2.getBody();
             } else {
                 HttpEntity<String> formEntity = new HttpEntity<>(JSONObject.toJSONString(bodyParams), headers);
                 result = restTemplate.postForObject(api+ "?" + paramStr, formEntity, String.class);
             }
+            result = new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
             logger.info("微信返回结果:" + result);
             JSONObject a = JSONObject.parseObject(result);