Explorar el Código

feat(h5): 交互

Go hace 5 años
padre
commit
90775dae07
Se han modificado 76 ficheros con 2350 adiciones y 258 borrados
  1. 24 2
      front/project/admin/routes/course/detail/page.js
  2. 12 10
      front/project/h5/components/Block/index.js
  3. 1 1
      front/project/h5/routes/page/identity/index.js
  4. 1 1
      front/project/h5/routes/page/invitation/index.js
  5. 1 1
      front/project/h5/routes/page/message/index.js
  6. 16 0
      front/project/h5/routes/page/message/index.less
  7. 88 23
      front/project/h5/routes/page/message/page.js
  8. 1 1
      front/project/h5/routes/page/open/index.js
  9. 50 7
      front/project/h5/routes/page/open/page.js
  10. 1 1
      front/project/h5/routes/page/study/index.js
  11. 2 2
      front/project/h5/routes/product/bought/index.js
  12. 1 1
      front/project/h5/routes/product/courseDetail/index.js
  13. 34 11
      front/project/h5/routes/product/courseDetail/page.js
  14. 42 14
      front/project/h5/routes/product/coursePackage/page.js
  15. 1 1
      front/project/h5/routes/product/courseVideo/index.js
  16. 14 2
      front/project/h5/routes/product/courseVideo/index.less
  17. 162 35
      front/project/h5/routes/product/courseVideo/page.js
  18. 52 2
      front/project/h5/routes/product/courseVs/page.js
  19. 14 2
      front/project/h5/routes/product/data/index.less
  20. 77 8
      front/project/h5/routes/product/data/page.js
  21. 2 2
      front/project/h5/routes/product/dataDetail/index.js
  22. 36 18
      front/project/h5/routes/product/dataDetail/page.js
  23. 1 1
      front/project/h5/routes/product/dataHistory/index.js
  24. 15 1
      front/project/h5/routes/product/dataHistory/index.less
  25. 86 17
      front/project/h5/routes/product/dataHistory/page.js
  26. 3 4
      front/project/h5/routes/product/index.js
  27. 1 1
      front/project/h5/routes/product/main/page.js
  28. 1 1
      front/project/h5/routes/product/serviceDetail/index.js
  29. 1 1
      front/project/h5/routes/textbook/detail/index.js
  30. 1 1
      front/project/h5/routes/textbook/detail/page.js
  31. 1 1
      front/project/h5/routes/textbook/main/index.js
  32. 1 2
      front/project/h5/routes/textbook/main/page.js
  33. 12 0
      front/project/h5/stores/course.js
  34. 18 2
      front/project/h5/stores/my.js
  35. 2 2
      front/project/h5/stores/textbook.js
  36. 221 0
      front/src/services/ListData.js
  37. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/UserAbnormalMapper.java
  38. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/Course.java
  39. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/ExaminationStruct.java
  40. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/TextbookLibrary.java
  41. 121 16
      server/data/src/main/java/com/qxgmat/data/dao/entity/User.java
  42. 230 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserAbnormal.java
  43. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrderRecord.java
  44. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseMapper.xml
  45. 3 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/ExaminationStructMapper.xml
  46. 3 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/TextbookLibraryMapper.xml
  47. 21 0
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAbnormalMapper.xml
  48. 6 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserMapper.xml
  49. 3 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserOrderRecordMapper.xml
  50. 17 0
      server/data/src/main/java/com/qxgmat/data/relation/CourseDataHistoryRelationMapper.java
  51. 21 0
      server/data/src/main/java/com/qxgmat/data/relation/CourseDataRelationMapper.java
  52. 36 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataHistoryRelationMapper.xml
  53. 39 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataRelationMapper.xml
  54. 7 1
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java
  55. 81 34
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  56. 49 6
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  57. 1 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/SentenceController.java
  58. 1 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java
  59. 27 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseDataExtendDto.java
  60. 59 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java
  61. 23 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/RecordOpenDto.java
  62. 91 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDataHistoryInfoDto.java
  63. 113 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/CoursePackageDetailDto.java
  64. 113 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/CoursePackageListDto.java
  65. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/ManagerService.java
  66. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExaminationService.java
  67. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ExerciseService.java
  68. 24 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseDataHistoryService.java
  69. 26 2
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseDataService.java
  70. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseExperienceService.java
  71. 28 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CoursePackageService.java
  72. 54 2
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java
  73. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ManagerRoleService.java
  74. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/TextbookQuestionService.java
  75. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserAskCourseService.java
  76. 1 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserAskQuestionService.java

+ 24 - 2
front/project/admin/routes/course/detail/page.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
 import moment from 'moment';
-import { Form, Input, Button, Row, Col, InputNumber, Upload, DatePicker, Checkbox } from 'antd';
+import { Form, Input, Button, Row, Col, InputNumber, Upload, DatePicker, Checkbox, Icon } from 'antd';
 import './index.less';
 import Editor from '@src/components/Editor';
 import Page from '@src/containers/Page';
@@ -260,7 +260,8 @@ export default class extends Page {
 
   renderVideo() {
     const { exercise, data } = this.state;
-    const { getFieldDecorator } = this.props.form;
+    const { getFieldDecorator, getFieldValue, setFieldsValue } = this.props.form;
+    const cover = getFieldValue('cover');
     return <Block>
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
@@ -319,6 +320,27 @@ export default class extends Page {
             <Input placeholder='请输入教师' />,
           )}
         </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程封面'>
+          {getFieldDecorator('cover', {
+            rules: [
+              { required: true, message: '上传图片' },
+            ],
+          })(
+            <Upload
+              listType="picture-card"
+              showUploadList={false}
+              beforeUpload={(file) => System.uploadImage(file).then((result) => {
+                setFieldsValue({ cover: result.url });
+                return Promise.reject();
+              })}
+            >
+              {cover ? <img src={cover} alt="avatar" /> : <div>
+                <Icon type={this.state.loading ? 'loading' : 'plus'} />
+                <div className="ant-upload-text">Upload</div>
+              </div>}
+            </Upload>,
+          )}
+        </Form.Item>
       </Form>
     </Block>;
   }

+ 12 - 10
front/project/h5/components/Block/index.js

@@ -12,22 +12,22 @@ const CrowdMap = getMap(CrowdList, 'value', 'label');
 
 export class Block extends Component {
   render() {
-    const { className = '', children } = this.props;
-    return <div className={`g-block ${className}`}>{children}</div>;
+    const { className = '', children, onClick } = this.props;
+    return <div className={`g-block ${className}`} onClick={() => onClick && onClick()}>{children}</div>;
   }
 }
 export class TopBlock extends Component {
   render() {
-    const { className = '', theme = 'default', children } = this.props;
-    return <div className={`g-top-block ${className} ${theme}`}>{children}</div>;
+    const { className = '', theme = 'default', children, onClick } = this.props;
+    return <div className={`g-top-block ${className} ${theme}`} onClick={() => onClick && onClick()}>{children}</div>;
   }
 }
 
 export class TagBlock extends Component {
   render() {
-    const { className = '', theme = 'default', tag, children } = this.props;
+    const { className = '', theme = 'default', tag, children, onClick } = this.props;
     return (
-      <div className={`g-tag-block ${className} ${theme}`}>
+      <div className={`g-tag-block ${className} ${theme}`} onClick={() => onClick && onClick()}>
         <div className="g-tag-block-tag">{tag}</div>
         {children}
       </div>
@@ -81,17 +81,19 @@ export class CourseBlock extends Component {
   }
 }
 
-export class CourseCoBlock extends Component {
+export class CoursePackageBlock extends Component {
   render() {
     const { theme, data } = this.props;
     return (
-      <TagBlock className="course-co-block" theme={theme} tag={''} onClick={() => {
-        linkTo(`/product/course/detail/${data.id}`);
+      <TagBlock className="course-co-block" theme={theme} tag={data.tag} onClick={() => {
+        linkTo(`/product/course/package/${data.id}`);
       }}>
         <div className="title">{data.title}</div>
         <div className="info">
           <div className="teacher">
-            授课老师<span>{data.teacher}</span>
+            授课老师{(data.courses || []).map(row => {
+            return <span>{row.teacher}</span>;
+          })}
           </div>
           {CrowdMap[data.crowd] && <Tag size="small">适合{CrowdMap[data.crowd]}</Tag>}
         </div>

+ 1 - 1
front/project/h5/routes/page/identity/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/identity',
   key: 'identity',
   title: '身份认证',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

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

@@ -2,7 +2,7 @@ export default {
   path: '/invitation',
   key: 'invitation',
   title: '邀请好友',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 1 - 1
front/project/h5/routes/page/message/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/message',
   key: 'message',
   title: '站内信',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 16 - 0
front/project/h5/routes/page/message/index.less

@@ -13,6 +13,22 @@
   }
 
   .list {
+    height: 100%;
+
+    .am-list-body {
+      // padding: 15px;
+    }
+
+    .am-list-body::before {
+      background: none;
+    }
+
+    .am-list-body::after {
+      background: none;
+    }
+  }
+
+  .list {
     .item {
       display: flex;
       padding: 20px 15px;

+ 88 - 23
front/project/h5/routes/page/message/page.js

@@ -1,16 +1,44 @@
 import React from 'react';
-import { Badge } from 'antd-mobile';
+import { Badge, ListView } from 'antd-mobile';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import { formatDate } from '@src/services/Tools';
+import ListData from '@src/services/ListData';
 import { My } from '../../../stores/my';
 
 export default class extends Page {
+  initState() {
+    return {
+      listMap: {},
+    };
+  }
+
   initData() {
-    My.message(this.state.search)
-      .then(result => {
-        this.setTableData(result.list, result.total);
+    return this.initListKeys(['message'])
+      .then(() => {
+        return this.getList('message', 1);
+      });
+  }
+
+  initListKeys(keys) {
+    const { listMap } = this.state;
+    keys.forEach((key) => {
+      listMap[key] = new ListData();
+    });
+    this.setState({ listMap });
+    return Promise.resolve();
+  }
+
+  getList(key, page) {
+    My.message(Object.assign({ page }, this.state.search))
+      .then((result) => {
+        const { listMap } = this.state;
+        if (page === 1) { // todo 是否重新读取第一页为刷新所有数据
+          listMap[key] = new ListData();
+        }
+        listMap[key].get(page, result, this.state.search.size);
+        this.setState({ listMap });
       });
   }
 
@@ -22,7 +50,6 @@ export default class extends Page {
   }
 
   renderView() {
-    const { list = [] } = this.state;
     return (
       <div>
         <div className="all" onClick={() => {
@@ -31,25 +58,63 @@ export default class extends Page {
           <Assets name="clean" />
           全部已读
         </div>
-        <div className="list">
-          {list.map(row => {
-            return <div className="item">
-              <div className="detail">
-                <div className="title">
-                  {row.title}
-                  <Badge dot />
-                </div>
-                <div className="desc">{row.description}</div>
-                {row.link && <a href={row.link}>查看详情</a>}
-              </div>
-              <div className="date-time">
-                <div className="date">{formatDate(row.createTime, 'YYYY-MM-DD')}</div>
-                <div className="time">{formatDate(row.createTime, 'HH:mm:ss')}</div>
-              </div>
-            </div>;
-          })}
-        </div>
+        {this.renderList()}
       </div>
     );
   }
+
+  renderRow(rowData) {
+    return <div className="item">
+      <div className="detail">
+        <div className="title">
+          {rowData.title}
+          {rowData.isRead > 0 && <Badge dot />}
+        </div>
+        <div className="desc">{rowData.description}</div>
+        {rowData.link && <a href={rowData.link}>查看详情</a>}
+      </div>
+      <div className="date-time">
+        <div className="date">{formatDate(rowData.createTime, 'YYYY-MM-DD')}</div>
+        <div className="time">{formatDate(rowData.createTime, 'HH:mm:ss')}</div>
+      </div>
+    </div>;
+  }
+
+  renderList() {
+    const { listMap } = this.state;
+    const { message = {} } = listMap;
+    const { dataSource = {}, bottom, loading, finish, maxSectionId = 1, total } = message;
+    if (total === 0) return this.renderEmpty();
+    return <ListView
+      className="list"
+      ref={el => { this.lv = el; }}
+      dataSource={dataSource}
+      renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
+        {loading ? '加载中...' : (bottom ? '没有更多了' : '')}
+      </div>)}
+      renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
+      style={{
+        height: this.state.height,
+        overflow: 'auto',
+      }}
+      pageSize={this.state.search.size}
+      scrollRenderAheadDistance={500}
+      onEndReached={() => {
+        if (loading) return;
+        if (bottom) {
+          if (!finish) {
+            message.finish = true;
+            // this.setState({ time: new Date() })
+          }
+          return;
+        }
+        this.getList('message', maxSectionId + 1);
+      }}
+      onEndReachedThreshold={10}
+    />;
+  }
+
+  renderEmpty() {
+    return <div />;
+  }
 }

+ 1 - 1
front/project/h5/routes/page/open/index.js

@@ -3,7 +3,7 @@ export default {
   matchPath: '/open/:id',
   key: 'open',
   title: '开通',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 50 - 7
front/project/h5/routes/page/open/page.js

@@ -2,32 +2,75 @@ import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { getMap, formatDate } from '@src/services/Tools';
 import Checkbox from '../../../components/CheckBox';
 import Button from '../../../components/Button';
 import { My } from '../../../stores/my';
+import { ServiceKey } from '../../../../Constant';
+
+const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
 
 export default class extends Page {
   initData() {
     const { id } = this.params;
-    My.getOrderRecord(id);
+    My.getOrderRecord(id)
+      .then(result => {
+        this.setState(result);
+      });
   }
 
   submit() {
-
+    My.useRecord(this.params.id)
+      .then();
   }
 
   renderView() {
+    const { productType } = this.state;
+    if (productType === 'service') {
+      return this.renderService();
+    }
+    return this.renderCourse();
+  }
+
+  renderCourse() {
+    const { course = {}, endTime } = this.state;
+    if (!endTime) return null;
     return (
       <div>
         <div className="icon">
           <Assets name="img3" />
         </div>
-        <div className="title">您正在开通“机经服务”</div>
-        <div className="tip">有效期至:2019-12-03</div>
-        <div className="check">
-          <Checkbox />
-          <span>邮箱订阅机经</span>
+        <div className="title">您正在开通“{course.data}”</div>
+        <div className="tip">有效期至:{formatDate(endTime, 'YYYY-MM-DD')}</div>
+        <Button block radius onClick={() => {
+          this.submit();
+        }}>
+          立即开通
+        </Button>
+        <div className="no" onClick={() => {
+          goBack();
+        }}>暂不开通</div>
+      </div>
+    );
+  }
+
+  renderService() {
+    const { service, endTime } = this.state;
+    if (!endTime) return null;
+    return (
+      <div>
+        <div className="icon">
+          <Assets name="img3" />
         </div>
+        <div className="title">您正在开通“{ServiceKeyMap[service]}”</div>
+        <div className="tip">有效期至:{formatDate(endTime, 'YYYY-MM-DD')}</div>
+        {service === 'textbook' && <div className="check">
+          <Checkbox checked={!!this.state.checked} onClick={() => {
+            this.setState({ checked: !this.state.checked });
+          }} />
+          <span>邮箱订阅机经</span>
+        </div>}
+
         <Button block radius onClick={() => {
           this.submit();
         }}>

+ 1 - 1
front/project/h5/routes/page/study/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/study',
   key: 'study',
   title: '数据',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 2 - 2
front/project/h5/routes/product/bought/index.js

@@ -1,8 +1,8 @@
 export default {
   path: '/product/data',
   key: 'product-data',
-  title: '全部资料',
-  needLogin: false,
+  title: '已购买',
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 1 - 1
front/project/h5/routes/product/courseDetail/index.js

@@ -1,7 +1,7 @@
 export default {
   path: '/product/course/detail/:id',
   key: 'product-course-detail',
-  title: '在线课程',
+  title: '课程详情',
   needLogin: false,
   component() {
     return import('./page');

+ 34 - 11
front/project/h5/routes/product/courseDetail/page.js

@@ -4,33 +4,56 @@ import { Tabs } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Money from '../../../components/Money';
 import Button from '../../../components/Button';
+import { Course } from '../../../stores/course';
+import { UserUrl } from '../../../../Constant';
 
 export default class extends Page {
-  init() {}
+  initState() {
+    return { tab: 'teacherContent' };
+  }
+
+  initData() {
+    const { id } = this.params;
+    Course.get(id).then(result => {
+      this.setState({ data: result });
+    });
+  }
+
+  buy() {
+
+  }
 
   renderView() {
+    const { data = {}, tab } = this.state;
     return (
       <div>
-        <div className="b-g" style={{ backgroundImage: 'url(/assets/p_bg.png)' }}>
-          <div className="title">OG20整合刷题-语法SC</div>
+        <div className="b-g" style={{ backgroundImage: `url(${data.cover})` }}>
+          <div className="title">{data.title}</div>
         </div>
-        <div className="tip">访问WWW.XXXXX,试听该课程</div>
+        <div className="tip">访问{UserUrl}/course/detail/{data.id},试听该课程</div>
         <div className="detail">
           <Tabs
+            page={tab}
             tabs={[
-              { title: '资料介绍', key: 'd' },
-              { title: '作者介绍', key: 'a' },
-              { title: '获取方式', key: 'h' },
-              { title: 'FAQs', key: 'f' },
-              { title: '用户评价', key: 'u' },
+              { title: '老师资质', key: 'teacherContent' },
+              { title: '基本参数', key: 'baseContent' },
+              { title: '授课重点', key: 'pointContent' },
+              { title: 'FAQs', key: 'faq' },
+              { title: '用户评价', key: 'comment' },
             ]}
+            onChange={(value) => {
+              this.setState({ tab: value.key });
+            }}
           />
+          <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
         </div>
         <div className="fixed">
           <div className="fee">
-            总额: <Money value="1200" size="lager" />
+            总额: <Money value={data.price} size="lager" />
           </div>
-          <Button width={110} className="f-r" radius>
+          <Button width={110} className="f-r" radius onClick={() => {
+            this.buy();
+          }}>
             立即购买
           </Button>
         </div>

+ 42 - 14
front/project/h5/routes/product/coursePackage/page.js

@@ -1,35 +1,53 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
+import { getMap } from '@src/services/Tools';
 import Money from '../../../components/Money';
 import Button from '../../../components/Button';
 import Tag from '../../../components/Tag';
-import Avatar from '../../../components/Avatar';
+// import Avatar from '../../../components/Avatar';
+import { Course } from '../../../stores/course';
+import { ServiceKey, ServiceParamMap } from '../../../../Constant';
 
 export default class extends Page {
-  init() {}
+  init() { }
+
+  initData() {
+    const { id } = this.params;
+    Course.getPackage(id).then(result => {
+      result.originPrice = result.courses.reduce((x, y) => x + y, 0);
+      this.setState({ data: result });
+    });
+  }
+
+  buy() {
+
+  }
 
   renderView() {
+    const { data = {} } = this.state;
     return (
       <div>
-        <div className="title">OG16/17/18/19语法千行OG16/17/18/19语法千 行</div>
+        <div className="title">{data.title}</div>
         <div className="tags">
-          <Tag size="small">适合新手</Tag>
+          {data.isNovice > 0 && <Tag size="small">适合新手</Tag>}
         </div>
         <div className="detail-title">授课老师</div>
         <div className="name">
-          <Avatar name="scan1" />
-          <span>李奕都(DUKB24)</span>
+          {/* <Avatar name="scan1" /> */}
+          {(data.courses || []).map(row => {
+            return <span>{row.teacher}</span>;
+          })}
         </div>
         <div className="detail-title">课程介绍</div>
         <div className="desc">
-          本商品仅限购买者本人使用,不可商用和传播。 若在购买过程中 遇到问题,请联系千行小助手协助解决。
+          {data.description}
         </div>
         <div className="detail-title">包含课程</div>
         <div className="detail-tags">
-          <Tag theme="border">OG20阅读刷题(7课时)</Tag>
-          <Tag theme="border">OOG20逻辑刷题(8课时)</Tag>
-          <Tag theme="border">OG20语法SC刷题(10 课时)</Tag>
+          {(data.courses || []).map(row => {
+            return <Tag theme="border">{row.title}({row.noNumber}课时)</Tag>;
+          })}
         </div>
         <div className="detail-title">配套服务</div>
         <div className="detail-tags">
@@ -38,14 +56,24 @@ export default class extends Page {
         </div>
         <div className="detail-title">赠送服务</div>
         <div className="detail-tags">
-          <Tag theme="border">机经对折券×1</Tag>
-          <Tag theme="border">VIP 权限×6个月</Tag>
+          {data.gift && ServiceKey.map(row => {
+            if (!data.gift[row.value]) return null;
+            const list = ServiceParamMap[row.value];
+            if (list) {
+              const map = getMap(list, 'value', 'label');
+              return <Tag theme="border">{row.label}×{map[data.gift[row.value]]}</Tag>;
+            }
+            return <Tag theme="border">{row.label}×{data.gift[row.value]}</Tag>;
+          })}
         </div>
         <div className="fixed">
           <div className="fee">
-            总额: <Money value="1200" size="lager" />
+            原价: <Money value={data.originPrice} size="lager" />
+            套餐价: <Money value={data.price} size="lager" />
           </div>
-          <Button width={110} className="f-r" radius>
+          <Button width={110} className="f-r" radius onClick={() => {
+            this.buy();
+          }}>
             立即购买
           </Button>
         </div>

+ 1 - 1
front/project/h5/routes/product/courseVideo/index.js

@@ -1,6 +1,6 @@
 export default {
   path: '/product/course/video',
-  key: 'product-course',
+  key: 'product-course-video',
   title: '在线课程',
   needLogin: false,
   component() {

+ 14 - 2
front/project/h5/routes/product/courseVideo/index.less

@@ -39,8 +39,20 @@
   }
 
   .list {
-    padding: 15px;
-    padding-top: 59px;
+    padding-top: 46px;
+    height: 100%;
+
+    .am-list-body {
+      padding: 15px;
+    }
+
+    .am-list-body::before {
+      background: none;
+    }
+
+    .am-list-body::after {
+      background: none;
+    }
   }
 
   .am-drawer {

+ 162 - 35
front/project/h5/routes/product/courseVideo/page.js

@@ -1,15 +1,95 @@
 import React from 'react';
 import './index.less';
-import { Drawer, Tabs } from 'antd-mobile';
+import { Drawer, Tabs, ListView } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
-import { CourseCoBlock } from '../../../components/Block';
+import { formatTreeData, getMap } from '@src/services/Tools';
+import ListData from '@src/services/ListData';
+import { CourseBlock, CoursePackageBlock } from '../../../components/Block';
 import { SpecialRadioGroup } from '../../../components/Radio';
 import Button from '../../../components/Button';
 import Checkbox from '../../../components/CheckBox';
+import { Course } from '../../../stores/course';
+import { Main } from '../../../stores/main';
 
 export default class extends Page {
-  init() {}
+  initState() {
+    return {
+      listMap: {},
+    };
+  }
+
+  init() {
+    Main.dataStruct().then((list) => {
+      list = list.map(row => {
+        row.title = `${row.titleZh} ${row.titleEn}`;
+        row.label = row.title;
+        row.key = row.id;
+        return row;
+      });
+      const structMap = getMap(list, 'id');
+      const videoTree = formatTreeData(list, 'id', 'title', 'parentId');
+      const packageTree = formatTreeData(list.filter(row => row.level === 1), 'id', 'title', 'parentId');
+      this.setState({ videoTree, packageTree, structMap });
+    });
+
+    const { search } = this.state;
+    if (!search.order) search.order = 'saleNumber';
+    this.setState({ search, tab: 'video' });
+  }
+
+  initData() {
+    return this.initListKeys(['video', 'package'])
+      .then(() => {
+        return this.getList('video', 1);
+      });
+  }
+
+  initListKeys(keys) {
+    const { listMap } = this.state;
+    keys.forEach((key) => {
+      listMap[key] = new ListData();
+    });
+    this.setState({ listMap });
+    return Promise.resolve();
+  }
+
+  getList(key, page) {
+    this.setState({ tab: key });
+    let handler;
+    switch (key) {
+      case 'video':
+        handler = Course.listVideo(Object.assign({ page }, this.state.search));
+        break;
+      case 'package':
+        handler = Course.listPackage(Object.assign({ page }, this.state.search))
+          .then(result => {
+            const { structMap } = this.state;
+            result.list = result.list.map(row => {
+              row.tag = (structMap[row.structId] || {}).title;
+              return row;
+            });
+            return result;
+          });
+        break;
+      default:
+        return;
+    }
+    handler.then((result) => {
+      const { listMap } = this.state;
+      if (page === 1) { // todo 是否重新读取第一页为刷新所有数据
+        listMap[key] = new ListData();
+      }
+      listMap[key].get(page, result, this.state.search.size);
+      this.setState({ listMap, tab: key });
+    });
+  }
+
+  changeStruct(value) {
+    const { search = {} } = this.state;
+    search.structId = value;
+    this.setState({ search });
+  }
 
   renderView() {
     const { filter } = this.state;
@@ -22,56 +102,103 @@ export default class extends Page {
         onOpenChange={isOpen => this.setState({ filter: isOpen })}
       >
         <div className="top">
-          <Tabs tabs={[{ title: '单次', key: 'd' }, { title: '套餐', key: 'a' }]} />
+          <Tabs onChange={(value) => {
+            this.changeStruct(null);
+            this.getList(value.key, 1);
+          }} tabs={[{ title: '单次', key: 'video' }, { title: '套餐', key: 'package' }]} />
           <div className="right" onClick={() => this.setState({ filter: true })}>
             筛选
             <Assets name="screen" />
           </div>
         </div>
-        <div className="list">
-          <CourseCoBlock theme="not" />
-          <CourseCoBlock />
-          <CourseCoBlock theme="end" />
-          <CourseCoBlock />
-          <CourseCoBlock />
-          <CourseCoBlock />
-          <CourseCoBlock />
-        </div>
+        {this.renderList()}
       </Drawer>
     );
   }
 
+  renderRow(rowData) {
+    const { tab } = this.state;
+    switch (tab) {
+      case 'video':
+        return <CourseBlock data={rowData} />;
+      case 'package':
+        return <CoursePackageBlock data={rowData} />;
+      default:
+        return <div />;
+    }
+  }
+
+  renderList() {
+    const { listMap, tab } = this.state;
+    const data = listMap[tab];
+    if (!data) return <div />;
+    const { dataSource = {}, bottom, loading, finish, maxSectionId = 1, total } = data;
+    if (total === 0) return this.renderEmpty();
+    return <ListView
+      className="list"
+      ref={el => { this.lv = el; }}
+      dataSource={dataSource}
+      renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
+        {loading ? '加载中...' : (bottom ? '没有更多了' : '')}
+      </div>)}
+      renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
+      style={{
+        height: this.state.height,
+        overflow: 'auto',
+      }}
+      pageSize={this.state.search.size}
+      scrollRenderAheadDistance={500}
+      onEndReached={() => {
+        if (loading) return;
+        if (bottom) {
+          if (!finish) {
+            data.finish = true;
+            // this.setState({ time: new Date() })
+          }
+          return;
+        }
+        this.getList(tab, maxSectionId + 1);
+      }}
+      onEndReachedThreshold={10}
+    />;
+  }
+
+  renderEmpty() {
+    return <div />;
+  }
+
   renderFilter() {
+    const { search, videoTree = [], packageTree = [], tab } = this.state;
     return (
       <div className="filter">
         <div className="body">
           <div className="sub">
             <div className="title">筛选学科</div>
-            <div className="item">
-              <div className="label left">长难句</div>
-              <div className="value right">
-                <Checkbox />
-              </div>
-            </div>
-            <div className="item">
-              <div className="label">语文 Verbal</div>
-              <div className="value">
-                <SpecialRadioGroup
-                  list={[
-                    { key: '1', label: '语法 SC' },
-                    { key: '2', label: '阅读 RC' },
-                    { key: '3', label: '逻辑 CR' },
-                  ]}
-                />
-              </div>
-            </div>
-            <div className="item">
-              <div className="label">数学 Quant</div>
-            </div>
+            {(tab === 'package' ? packageTree : videoTree).map((row) => {
+              return <div className="item">
+                <div className={row.children.length > 0 ? 'label' : 'label left'}>{row.title}</div>
+                {row.children.length > 0 && <div className="value">
+                  <SpecialRadioGroup
+                    list={row.children}
+                    value={search.structId}
+                    onChange={(value) => {
+                      this.changeStruct(value);
+                    }}
+                  />
+                </div>}
+                {row.children.length === 0 && <div className="value right">
+                  <Checkbox checked={search.structId === row.id} onClick={() => {
+                    this.changeStruct(row.id);
+                  }} />
+                </div>}
+              </div>;
+            })}
           </div>
         </div>
         <div className="footer">
-          <Button radius width={90}>
+          <Button radius width={90} onClick={() => {
+            this.getList(tab, 1);
+          }}>
             确定
           </Button>
         </div>

+ 52 - 2
front/project/h5/routes/product/courseVs/page.js

@@ -1,12 +1,62 @@
 import React from 'react';
 import './index.less';
+import { Tabs } from 'antd-mobile';
 import Page from '@src/containers/Page';
+import Money from '../../../components/Money';
+import Button from '../../../components/Button';
+import { Course } from '../../../stores/course';
 
 export default class extends Page {
-  init() {
+  initState() {
+    return { tab: 'serviceContent' };
+  }
+
+  initData() {
+    const { id } = this.params;
+    Course.get(id).then(result => {
+      this.setState({ data: result });
+    });
+  }
+
+  buy() {
+
   }
 
   renderView() {
-    return <div />;
+    const { data = {}, tab } = this.state;
+    return (
+      <div>
+        <div className="b-g" style={{ backgroundImage: `url(${data.cover})` }}>
+          <div className="title">{data.title}</div>
+        </div>
+        {/* <div className="tip">访问{UserUrl}/course/detail/{data.id},试听该课程</div> */}
+        <div className="detail">
+          <Tabs
+            page={tab}
+            tabs={[
+              { title: '服务介绍', key: 'serviceContent' },
+              { title: '适合人群呢', key: 'crowdContent' },
+              { title: '授课流程', key: 'processContent' },
+              { title: 'FAQs', key: 'faq' },
+              { title: '用户评价', key: 'comment' },
+            ]}
+            onChange={(value) => {
+              this.setState({ tab: value.key });
+            }}
+          />
+          <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
+        </div>
+        <div className="fixed">
+          <div className="fee">
+            总额: <Money value={data.price} size="lager" />
+          </div>
+          <Button width={110} className="f-r" radius onClick={() => {
+            this.buy();
+          }}>
+            立即购买
+          </Button>
+        </div>
+      </div>
+    );
   }
 }

+ 14 - 2
front/project/h5/routes/product/data/index.less

@@ -34,8 +34,20 @@
   }
 
   .list {
-    padding: 15px;
-    padding-top: 59px;
+    padding-top: 46px;
+    height: 100%;
+
+    .am-list-body {
+      padding: 15px;
+    }
+
+    .am-list-body::before {
+      background: none;
+    }
+
+    .am-list-body::after {
+      background: none;
+    }
   }
 
   .am-drawer {

+ 77 - 8
front/project/h5/routes/product/data/page.js

@@ -1,9 +1,10 @@
 import React from 'react';
 import './index.less';
-import { Drawer } from 'antd-mobile';
+import { Drawer, ListView } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import { formatTreeData } from '@src/services/Tools';
+import ListData from '@src/services/ListData';
 import { DataBlock } from '../../../components/Block';
 import { SpecialRadioGroup } from '../../../components/Radio';
 import Switch from '../../../components/Switch';
@@ -14,6 +15,12 @@ import { Main } from '../../../stores/main';
 import { DataType } from '../../../../Constant';
 
 export default class extends Page {
+  initState() {
+    return {
+      listMap: {},
+    };
+  }
+
   init() {
     Main.dataStruct().then((list) => {
       const structTree = formatTreeData(list.map(row => {
@@ -31,9 +38,31 @@ export default class extends Page {
   }
 
   initData() {
-    Course.listData(this.state.search).then(result => {
-      this.setTableData(result.list, result.total);
+    return this.initListKeys(['data'])
+      .then(() => {
+        return this.getList('data', 1);
+      });
+  }
+
+  initListKeys(keys) {
+    const { listMap = {} } = this.state;
+    keys.forEach((key) => {
+      listMap[key] = new ListData();
     });
+    this.setState({ listMap });
+    return Promise.resolve();
+  }
+
+  getList(key, page) {
+    Course.listData(Object.assign({ page }, this.state.search))
+      .then((result) => {
+        const { listMap = {} } = this.state;
+        if (page === 1) { // todo 是否重新读取第一页为刷新所有数据
+          listMap[key] = new ListData();
+        }
+        listMap[key].get(page, result, this.state.search.size);
+        this.setState({ listMap });
+      });
   }
 
   changeOrder(order, direction) {
@@ -73,7 +102,7 @@ export default class extends Page {
   }
 
   renderView() {
-    const { filter, list = [], search = {} } = this.state;
+    const { filter, search = {} } = this.state;
     return (<Drawer
       style={{ minHeight: document.documentElement.clientHeight }}
       position="right"
@@ -93,13 +122,53 @@ export default class extends Page {
             <Assets name="screen" />
         </div>
       </div>
-      <div className="list">
-        {list.map(row => <DataBlock data={row} />)}
-      </div>
+      {this.renderList()}
     </Drawer>
     );
   }
 
+  renderRow(rowData) {
+    return <DataBlock data={rowData} />;
+  }
+
+  renderList() {
+    const { listMap } = this.state;
+    const { data = {} } = listMap;
+    const { dataSource = {}, bottom, loading, finish, maxSectionId = 1, total } = data;
+    if (total === 0) return this.renderEmpty();
+    return <ListView
+      className="list"
+      ref={el => { this.lv = el; }}
+      dataSource={dataSource}
+      renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
+        {loading ? '加载中...' : (bottom ? '没有更多了' : '')}
+      </div>)}
+      renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
+      style={{
+        height: this.state.height,
+        overflow: 'auto',
+      }}
+      pageSize={this.state.search.size}
+      scrollRenderAheadDistance={500}
+      onEndReached={() => {
+        if (loading) return;
+        if (bottom) {
+          if (!finish) {
+            data.finish = true;
+            // this.setState({ time: new Date() })
+          }
+          return;
+        }
+        this.getList('data', maxSectionId + 1);
+      }}
+      onEndReachedThreshold={10}
+    />;
+  }
+
+  renderEmpty() {
+    return <div />;
+  }
+
   renderFilter() {
     const { search, structTree = [] } = this.state;
     return (
@@ -159,7 +228,7 @@ export default class extends Page {
         </div>
         <div className="footer">
           <Button radius width={90} onClick={() => {
-            this.refresh();
+            this.getList('data', 1);
           }}>
             确定
           </Button>

+ 2 - 2
front/project/h5/routes/product/dataDetail/index.js

@@ -1,8 +1,8 @@
 export default {
   path: '/product/data/detail/:id',
   key: 'product-data-detail',
-  title: '已购买',
-  needLogin: false,
+  title: '资料详情',
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 36 - 18
front/project/h5/routes/product/dataDetail/page.js

@@ -3,12 +3,20 @@ import './index.less';
 import { Tabs } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { formatDate, getMap } from '@src/services/Tools';
 import Money from '../../../components/Money';
 import Button from '../../../components/Button';
 import Tag from '../../../components/Tag';
 import { Course } from '../../../stores/course';
+import { DataType } from '../../../../Constant';
+
+const DataTypeMap = getMap(DataType, 'value', 'label');
 
 export default class extends Page {
+  initState() {
+    return { tab: 'content' };
+  }
+
   initData() {
     const { id } = this.state;
     Course.getData(id)
@@ -17,40 +25,50 @@ export default class extends Page {
       });
   }
 
+  buy() {
+
+  }
+
   renderView() {
+    const { data = {}, tab } = this.state;
     return (
       <div>
         <div className="detail">
-          <Assets className="info-img" name="d_b" />
+          <Assets className="info-img" name="d_b" src={data.cover} />
           <div className="info">
-            <div className="title">OG16/17/18/19语法千行OG16/ 17/18/19语法千行</div>
+            <div className="title">{data.title}</div>
             <div className="tags">
-              <Tag size="small">原创</Tag>
-              <Tag size="small">适合新手</Tag>
+              {data.isOriginal && <Tag size="small">原创</Tag>}
+              {data.isNovice && <Tag size="small">适合新手</Tag>}
             </div>
-            <div className="data">页数: 30页</div>
-            <div className="data">格式: PDF</div>
-            <div className="data">2019-06-20 10:21 更新</div>
-            <Money value="200" size="lager" theme="sell" />
+            <div className="data">页数: {data.page}页</div>
+            <div className="data">格式: {DataTypeMap[data.dataType]}</div>
+            <div className="data">{formatDate(data.updateTime, 'YYYY-MM-DD HH:mm')} 更新</div>
+            <Money value={data.price} size="lager" theme="sell" />
           </div>
         </div>
-        <div className="desc">
-          对“忽略有效考点”的解析进行补充,同时讲解OG不够精准的解析,帮助考生明确重点、避开误区,节省大量的时间和精力,刷OG必备。
-        </div>
+        <div className="desc" dangerouslySetInnerHTML={{ __html: data.description }} />
         <Tabs
+          page={tab}
           tabs={[
-            { title: '资料介绍', key: 'd' },
-            { title: '作者介绍', key: 'a' },
-            { title: '获取方式', key: 'h' },
-            { title: 'FAQs', key: 'f' },
-            { title: '用户评价', key: 'u' },
+            { title: '资料介绍', key: 'content' },
+            { title: '作者介绍', key: 'author_content' },
+            { title: '获取方式', key: 'method_content' },
+            { title: 'FAQs', key: 'faq' },
+            { title: '用户评价', key: 'comment' },
           ]}
+          onChange={(value) => {
+            this.setState({ tab: value.key });
+          }}
         />
+        <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
         <div className="fixed">
           <div className="fee">
-            总额: <Money value="1200" size="lager" />
+            总额: <Money value={data.price} size="lager" />
           </div>
-          <Button width={110} className="f-r" radius>
+          <Button width={110} className="f-r" radius onClick={() => {
+            this.buy();
+          }}>
             立即购买
           </Button>
         </div>

+ 1 - 1
front/project/h5/routes/product/dataHistory/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/product/data/history',
   key: 'product-data-history',
   title: '资料更新',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 15 - 1
front/project/h5/routes/product/dataHistory/index.less

@@ -1,7 +1,21 @@
 @charset "utf-8";
 
 #product-data-history {
-  padding: 0 15px;
+  .list {
+    height: 100%;
+
+    .am-list-body {
+      padding: 15px;
+    }
+
+    .am-list-body::before {
+      background: none;
+    }
+
+    .am-list-body::after {
+      background: none;
+    }
+  }
 
   .list {
     .item {

+ 86 - 17
front/project/h5/routes/product/dataHistory/page.js

@@ -1,8 +1,12 @@
 import React, { Component } from 'react';
+import { ListView } from 'antd-mobile';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
-import Tag from '../../../components/Tag';
+import ListData from '@src/services/ListData';
+// import Tag from '../../../components/Tag';
+import { My } from '../../../stores/my';
+import { formatDate } from '../../../../../src/services/Tools';
 
 class Item extends Component {
   constructor(props) {
@@ -11,21 +15,18 @@ class Item extends Component {
   }
 
   render() {
+    const { data } = this.props;
     const { show } = this.state;
     return (
       <div className="item">
         <div className="title">
-          OG19 语法千行<span className="date">2019-08-11</span>
+          {data.data.title}<span className="date">{formatDate(data.time, 'YYYY-MM-DD')}</span>
         </div>
         <div className="tip">
-          <Tag className="new" size="small">
-            NEW
-          </Tag>
-          变更点:版本2
+          变更点:{data.version}
         </div>
         <div hidden={!show} className="desc">
-          删除“the number of wolf population 意思重复”删除“the number of wolf population 意思重复”删除“the number of
-          wolf population 意思重复”
+          位置 {data.position}原内容 {data.originContent} 更改为 {data.content}
         </div>
         <Assets
           className="drop"
@@ -38,23 +39,91 @@ class Item extends Component {
 }
 
 export default class extends Page {
-  init() { }
+  initState() {
+    return {
+      listMap: {},
+    };
+  }
+
+  initData() {
+    My.listData(Object.assign({ order: 'isSubscribe', direction: 'desc' }, this.state.search)).then(result => {
+      this.setTableData(result.list.filter(row => row.isSubscribe), result.total);
+    });
+    return this.initListKeys(['history'])
+      .then(() => {
+        return this.getList('history', 1);
+      });
+  }
+
+  initListKeys(keys) {
+    const { listMap } = this.state;
+    keys.forEach((key) => {
+      listMap[key] = new ListData();
+    });
+    this.setState({ listMap });
+    return Promise.resolve();
+  }
+
+  getList(key, page) {
+    My.dataHistory(Object.assign({ page }, this.state.search))
+      .then((result) => {
+        const { listMap } = this.state;
+        if (page === 1) { // todo 是否重新读取第一页为刷新所有数据
+          listMap[key] = new ListData();
+        }
+        listMap[key].get(page, result, this.state.search.size);
+        this.setState({ listMap });
+      });
+  }
 
   renderView() {
-    const { list = [] } = this.state;
-    return <div>{list.length > 0 ? this.renderList() : this.renderEmpty()}</div>;
+    return this.renderList();
+  }
+
+  renderRow(rowData) {
+    return <Item data={rowData} />;
   }
 
   renderList() {
-    return (
-      <div className="list">
-        <Item />
-        <Item />
-      </div>
-    );
+    const { listMap } = this.state;
+    const { history = {} } = listMap;
+    const { dataSource = {}, bottom, loading, finish, maxSectionId = 1, total } = history;
+    if (total === 0) return this.renderEmpty();
+    return <ListView
+      className="list"
+      ref={el => { this.lv = el; }}
+      dataSource={dataSource}
+      renderFooter={() => (<div style={{ padding: 30, textAlign: 'center' }}>
+        {loading ? '加载中...' : (bottom ? '没有更多了' : '')}
+      </div>)}
+      renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
+      style={{
+        height: this.state.height,
+        overflow: 'auto',
+      }}
+      pageSize={this.state.search.size}
+      scrollRenderAheadDistance={500}
+      onEndReached={() => {
+        if (loading) return;
+        if (bottom) {
+          if (!finish) {
+            history.finish = true;
+            // this.setState({ time: new Date() })
+          }
+          return;
+        }
+        this.getList('history', maxSectionId + 1);
+      }}
+      onEndReachedThreshold={10}
+    />;
   }
 
   renderEmpty() {
+    const { list = [] } = this.state;
+    return list.length === 0 ? this.renderNoData() : <div className="empty">资料还没有更新记录</div>;
+  }
+
+  renderNoData() {
     return <div className="empty">还未购买或订阅资料</div>;
   }
 }

+ 3 - 4
front/project/h5/routes/product/index.js

@@ -2,12 +2,11 @@ import serviceDetail from './serviceDetail';
 import data from './data';
 import dataDetail from './dataDetail';
 import dataHistory from './dataHistory';
-import list from './list';
-import course from './course';
+import courseVideo from './courseVideo';
 import courseDetail from './courseDetail';
 import coursePackage from './coursePackage';
-import courseSingle from './courseSingle';
+import courseVs from './courseVs';
 import bought from './bought';
 import main from './main';
 
-export default [list, course, coursePackage, courseSingle, courseDetail, serviceDetail, data, dataDetail, dataHistory, bought, main];
+export default [courseVideo, coursePackage, courseVs, courseDetail, serviceDetail, data, dataDetail, dataHistory, bought, main];

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

@@ -18,7 +18,7 @@ export default class extends Page {
 
   initData() {
     Promise.all(ServiceKey.map(service => {
-      return Main.getService(service).then(result => {
+      return Main.getService(service.value).then(result => {
         this.setState({ [service]: result });
       });
     }));

+ 1 - 1
front/project/h5/routes/product/serviceDetail/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/product/service/:service',
   key: 'product-service-detail',
   title: '服务详情',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

+ 1 - 1
front/project/h5/routes/textbook/detail/index.js

@@ -2,7 +2,7 @@ export default {
   path: '/textbook/detail/:subject',
   key: 'textbook-detail',
   title: '机经详情',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

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

@@ -217,7 +217,7 @@ export default class extends Page {
         </div>
         <div className="footer">
           <Button radius width={90} onClick={() => {
-            this.refresh();
+            this.search({ page: 1 });
           }}>
             确定
           </Button>

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

@@ -2,7 +2,7 @@ export default {
   path: '/textbook',
   key: 'textbook',
   title: '机经主页',
-  needLogin: false,
+  needLogin: true,
   component() {
     return import('./page');
   },

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

@@ -33,7 +33,7 @@ export default class extends Page {
 
   refreshTab(tab) {
     this.setState({ tab });
-    Textbook.listHistory(tab.key).then(result => {
+    Textbook.allHistory(tab.key).then(result => {
       this.setState({ list: result });
     });
   }
@@ -50,7 +50,6 @@ export default class extends Page {
         <div>{hasService ? this.renderList() : this.renderEmpty()}</div>
         <div className="fixed">
           <Button block disabled={!hasService} size="lager" onClick={() => {
-            console.log(1);
             linkTo('/textbook/detail');
           }}>
             查阅机经

+ 12 - 0
front/project/h5/stores/course.js

@@ -12,13 +12,25 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/video/list', params);
   }
 
+  get(courseId) {
+    return this.apiGet('/course/detail', { courseId });
+  }
+
   listPackage(params) {
     return this.apiGet('/course/package/list', params);
   }
 
+  getPackage(packageId) {
+    return this.apiGet('/course/package/detail', { packageId });
+  }
+
   listData(params) {
     return this.apiGet('/course/data/list', params);
   }
+
+  getData(dataId) {
+    return this.apiGet('/course/data/detail', { dataId });
+  }
 }
 
 export const Course = new CourseStore({ key: 'course' });

+ 18 - 2
front/project/h5/stores/my.js

@@ -291,8 +291,24 @@ export default class MyStore extends BaseStore {
    * 开通服务、课程等
    * @param {*} id
    */
-  useRecord(id) {
-    return this.apiGet('/my/record/use', { id });
+  useRecord(id, isSubscribe) {
+    return this.apiPost('/my/record/use', { id, isSubscribe });
+  }
+
+  /**
+   * 获取数据订阅更新
+   * @param {*} param0
+   */
+  dataHistory({ page, size }) {
+    return this.apiGet('/my/data/history', { page, size });
+  }
+
+  /**
+   * 获取购买的资料列表
+   * @param {*} param0
+   */
+  listData({ page, size, structId, dataType, order, direction }) {
+    return this.apiGet('/my/data/list', { page, size, structId, dataType, order, direction });
   }
 }
 

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

@@ -12,8 +12,8 @@ export default class TextbookStore extends BaseStore {
     return this.apiGet('/textbook/year', { year });
   }
 
-  listHistory(subject) {
-    return this.apiGet('/textbook/history/list', { subject });
+  allHistory(subject) {
+    return this.apiGet('/textbook/history/all', { subject });
   }
 
   listTopic({ page, size, latest, qualitys, isOld, order, direction }) {

+ 221 - 0
front/src/services/ListData.js

@@ -0,0 +1,221 @@
+import { ListView } from 'antd-mobile';
+
+const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
+const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
+export const NewDataSource = function () {
+  return new ListView.DataSource({
+    getRowData,
+    getSectionHeaderData: getSectionData,
+    rowHasChanged: (row1, row2) => { return row1 !== row2; },
+    sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
+  });
+};
+export const ListData = function (field = 'id') {
+  this.field = field;
+  this.maxSectionId = 0;
+  this.maxSectionId = 0;
+  this.loading = false;
+  this.bottom = false;
+  this.top = false;
+  this.dataSource = NewDataSource();
+  this.dataBlob = {};
+  this.sectionIDs = [];
+  this.rowIDs = [];
+  this.count = -1;
+  this.total = 0;
+  this.dataSource = NewDataSource().cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+};
+
+ListData.prototype.refresh = function () {
+  this.sectionIDs = [];
+  this.rowIDs = [];
+  this.dataBlob = {};
+  this.maxSectionId = 0;
+  this.maxSectionId = 0;
+  this.loading = false;
+  this.bottom = false;
+  this.top = false;
+  this.count = 0;
+  this.total = 0;
+  this.dataSource = NewDataSource().cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+};
+
+ListData.prototype.append = function (sectionId, data, preNumber) {
+  const dataNum = data.length;
+  const ids = [];
+  for (let i = 0; i < dataNum; i += 1) {
+    const item = data[i];
+    const id = `${sectionId}:${item[this.field]}`;
+    this.dataBlob[id] = item;
+    ids.push(id);
+  }
+  const index = this.sectionIDs.indexOf(sectionId);
+  if (index >= 0) {
+    this.rowIDs[index] = ids.concat(this.rowIDs[index]);
+  } else {
+    this.sectionIDs = [sectionId].concat(this.sectionIDs);
+    this.rowIDs.unshift(ids);
+  }
+  this.rowIDs = [].concat(this.rowIDs);
+  this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  this.minSectionId = sectionId;
+  // console.log('append', data, preNumber)
+  if (dataNum < preNumber) {
+    this.top = true;
+    if (this.sectionIDs.length === 1) this.bottom = true;
+  }
+  this.count += dataNum;
+  this.loading = false;
+};
+ListData.prototype.push = function (sectionId, data, preNumber) {
+  // console.log('push', sectionId, data, preNumber)
+  const dataNum = data.length;
+  const ids = [];
+  for (let i = 0; i < dataNum; i += 1) {
+    const item = data[i];
+    const id = `${sectionId}:${item[this.field]}`;
+    this.dataBlob[id] = item;
+    ids.push(id);
+  }
+  const index = this.sectionIDs.indexOf(sectionId);
+  if (index >= 0) {
+    this.rowIDs[index] = this.rowIDs[index].concat(ids);
+  } else {
+    this.sectionIDs.push(sectionId);
+    this.rowIDs.push(ids);
+    this.sectionIDs = [].concat(this.sectionIDs);
+  }
+  this.rowIDs = [].concat(this.rowIDs);
+
+  this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  this.maxSectionId = sectionId;
+  if (dataNum < preNumber) {
+    this.bottom = true;
+    if (this.sectionIDs.length === 1) this.top = true;
+  }
+  this.count += dataNum;
+  this.loading = false;
+};
+ListData.prototype.pop = function () {
+  console.log('pop', this.sectionIDs.length);
+  if (this.sectionIDs.length === 0) return;
+  this.sectionIDs.pop();
+  this.sectionIDs = [].concat(this.sectionIDs);
+  const ids = this.rowIDs.pop();
+  ids.forEach((row) => { delete this.dataBlob[row]; });
+  this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  this.maxSectionId = this.sectionIDs[this.sectionIDs.length - 1];
+  this.bottom = false;
+  this.count -= ids.length;
+};
+ListData.prototype.shift = function () {
+  console.log('shift', this.sectionIDs.length);
+  if (this.sectionIDs.length === 0) return;
+  this.sectionIDs.pop();
+  this.sectionIDs = [].concat(this.sectionIDs);
+  const ids = this.rowIDs.pop();
+  ids.forEach((row) => { delete this.dataBlob[row]; });
+  this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  ([this.minSectionId] = this.sectionIDs);
+  this.top = false;
+  this.count -= ids.length;
+};
+ListData.prototype.get = function (sectionId, value, preNumber) {
+  this.total = value.total;
+  return this.push(sectionId, value.list, preNumber);
+};
+ListData.prototype.load = function () {
+  this.loading = true;
+};
+ListData.prototype.delete = function (sectionId, rowId) {
+  const index = this.sectionIDs.indexOf(sectionId);
+  if (index === -1) return;
+  this.rowIDs = [].concat(this.rowIDs);
+  this.rowIDs[index] = this.rowIDs[index].filter((row) => row !== rowId);
+  delete this.dataBlob[rowId];
+  this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  this.count -= 1;
+};
+ListData.prototype.update = function (sectionId, item) {
+  const index = this.sectionIDs.indexOf(sectionId);
+  if (index === -1) return;
+  const id = `${sectionId}:${item[this.field]}`;
+  const tmp = {};
+  Object.assign(tmp, item);
+  this.dataBlob[id] = tmp;
+  this.refresh();
+};
+ListData.prototype.addTop = function (value, preNumber) {
+  // console.log(value)
+  if (!value || value.length === 0) return [];
+  const sectionIds = [];
+  if (this.sectionIDs.length > 0) {
+    const index = 0;
+    const sectionId = this.sectionIDs[index];
+    const rowNumber = this.rowIDs[index].length;
+    if (rowNumber < preNumber) {
+      value.splice(0, preNumber - rowNumber).forEach((row) => {
+        const id = `${sectionId}:${row[this.field]}`;
+        this.rowIDs[index].push(id);
+
+        this.dataBlob[id] = row;
+        this.count += 1;
+      });
+      sectionIds.push(sectionId);
+    }
+  }
+  if (value.length > 0) {
+    const ids = value.map(row => {
+      return row[this.field];
+    });
+    const maxSectionId = Math.max.apply(null, ids);
+    // console.log(ids, maxSectionId)
+    sectionIds.push(maxSectionId);
+    this.append(maxSectionId, value, preNumber);
+  } else {
+    this.rowIDs = [].concat(this.rowIDs);
+    this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  }
+  return sectionIds;
+};
+ListData.prototype.addBottom = function (value, preNumber) {
+  // console.log(value)
+  if (!value || value.length === 0) return [];
+  const sectionIds = [];
+  if (this.sectionIDs.length > 0) {
+    const index = this.sectionIDs.length - 1;
+    const sectionId = this.sectionIDs[index];
+    const rowNumber = this.rowIDs[index].length;
+    if (rowNumber < preNumber) {
+      value.splice(0, preNumber - rowNumber).forEach((row) => {
+        const id = `${sectionId}:${row[this.field]}`;
+        this.rowIDs[index].push(id);
+
+        this.dataBlob[id] = row;
+        this.count += 1;
+      });
+      sectionIds.push(sectionId);
+    }
+  }
+  if (value.length > 0) {
+    const ids = value.map(row => {
+      return row[this.field];
+    });
+    const minSectionId = Math.min.apply(null, ids);
+    // console.log(ids, minSectionId)
+    sectionIds.push(minSectionId);
+    this.push(minSectionId, value, preNumber);
+  } else {
+    this.rowIDs = [].concat(this.rowIDs);
+    this.dataSource = this.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+  }
+  return sectionIds;
+};
+ListData.prototype.refresh = function () {
+  this.dataSource = NewDataSource().cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs);
+};
+ListData.prototype.rowId = function (sectionId, item) {
+  return `${sectionId}:${item[this.field]}`;
+};
+
+export default ListData;

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/UserAbnormalMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.UserAbnormal;
+
+public interface UserAbnormalMapper extends Mapper<UserAbnormal> {
+}

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

@@ -31,6 +31,12 @@ public class Course implements Serializable {
     private String courseModule;
 
     /**
+     * 课时数
+     */
+    @Column(name = "`no_number`")
+    private Integer noNumber;
+
+    /**
      * 1vs1课程类型
      */
     @Column(name = "`vs_type`")
@@ -281,6 +287,24 @@ public class Course implements Serializable {
     }
 
     /**
+     * 获取课时数
+     *
+     * @return no_number - 课时数
+     */
+    public Integer getNoNumber() {
+        return noNumber;
+    }
+
+    /**
+     * 设置课时数
+     *
+     * @param noNumber 课时数
+     */
+    public void setNoNumber(Integer noNumber) {
+        this.noNumber = noNumber;
+    }
+
+    /**
      * 获取1vs1课程类型
      *
      * @return vs_type - 1vs1课程类型
@@ -840,6 +864,7 @@ public class Course implements Serializable {
         sb.append(", structId=").append(structId);
         sb.append(", parentStructId=").append(parentStructId);
         sb.append(", courseModule=").append(courseModule);
+        sb.append(", noNumber=").append(noNumber);
         sb.append(", vsType=").append(vsType);
         sb.append(", videoType=").append(videoType);
         sb.append(", extend=").append(extend);
@@ -925,6 +950,16 @@ public class Course implements Serializable {
         }
 
         /**
+         * 设置课时数
+         *
+         * @param noNumber 课时数
+         */
+        public Builder noNumber(Integer noNumber) {
+            obj.setNoNumber(noNumber);
+            return this;
+        }
+
+        /**
          * 设置1vs1课程类型
          *
          * @param vsType 1vs1课程类型

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

@@ -47,6 +47,12 @@ public class ExaminationStruct implements Serializable {
     private Integer isAdapt;
 
     /**
+     * 提问状态
+     */
+    @Column(name = "`question_status`")
+    private Integer questionStatus;
+
+    /**
      * 对应服务
      */
     @Column(name = "`extend`")
@@ -183,6 +189,24 @@ public class ExaminationStruct implements Serializable {
     }
 
     /**
+     * 获取提问状态
+     *
+     * @return question_status - 提问状态
+     */
+    public Integer getQuestionStatus() {
+        return questionStatus;
+    }
+
+    /**
+     * 设置提问状态
+     *
+     * @param questionStatus 提问状态
+     */
+    public void setQuestionStatus(Integer questionStatus) {
+        this.questionStatus = questionStatus;
+    }
+
+    /**
      * 获取对应服务
      *
      * @return extend - 对应服务
@@ -231,6 +255,7 @@ public class ExaminationStruct implements Serializable {
         sb.append(", order=").append(order);
         sb.append(", level=").append(level);
         sb.append(", isAdapt=").append(isAdapt);
+        sb.append(", questionStatus=").append(questionStatus);
         sb.append(", extend=").append(extend);
         sb.append(", description=").append(description);
         sb.append("]");
@@ -317,6 +342,16 @@ public class ExaminationStruct implements Serializable {
         }
 
         /**
+         * 设置提问状态
+         *
+         * @param questionStatus 提问状态
+         */
+        public Builder questionStatus(Integer questionStatus) {
+            obj.setQuestionStatus(questionStatus);
+            return this;
+        }
+
+        /**
          * 设置对应服务
          *
          * @param extend 对应服务

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

@@ -83,6 +83,12 @@ public class TextbookLibrary implements Serializable {
     @Column(name = "`history_number`")
     private Integer historyNumber;
 
+    /**
+     * 提问状态
+     */
+    @Column(name = "`question_status`")
+    private Integer questionStatus;
+
     @Column(name = "`create_time`")
     private Date createTime;
 
@@ -322,6 +328,24 @@ public class TextbookLibrary implements Serializable {
     }
 
     /**
+     * 获取提问状态
+     *
+     * @return question_status - 提问状态
+     */
+    public Integer getQuestionStatus() {
+        return questionStatus;
+    }
+
+    /**
+     * 设置提问状态
+     *
+     * @param questionStatus 提问状态
+     */
+    public void setQuestionStatus(Integer questionStatus) {
+        this.questionStatus = questionStatus;
+    }
+
+    /**
      * @return create_time
      */
     public Date getCreateTime() {
@@ -368,6 +392,7 @@ public class TextbookLibrary implements Serializable {
         sb.append(", rcVersion=").append(rcVersion);
         sb.append(", rcTime=").append(rcTime);
         sb.append(", historyNumber=").append(historyNumber);
+        sb.append(", questionStatus=").append(questionStatus);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
         sb.append("]");
@@ -514,6 +539,16 @@ public class TextbookLibrary implements Serializable {
         }
 
         /**
+         * 设置提问状态
+         *
+         * @param questionStatus 提问状态
+         */
+        public Builder questionStatus(Integer questionStatus) {
+            obj.setQuestionStatus(questionStatus);
+            return this;
+        }
+
+        /**
          * @param createTime
          */
         public Builder createTime(Date createTime) {

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

@@ -190,14 +190,32 @@ public class User implements Serializable {
     private Integer inviteNumber;
 
     /**
-     * 是否订阅最新机经
+     * 注册ip
      */
-    @Column(name = "`is_subscribe_textbook`")
-    private Integer isSubscribeTextbook;
+    @Column(name = "`register_ip`")
+    private String registerIp;
+
+    /**
+     * 是否冻结
+     */
+    @Column(name = "`is_frozen`")
+    private Integer isFrozen;
 
     @Column(name = "`create_time`")
     private Date createTime;
 
+    /**
+     * 资料订阅
+     */
+    @Column(name = "`data_email_subscribe`")
+    private Integer dataEmailSubscribe;
+
+    /**
+     * 机经邮箱订阅
+     */
+    @Column(name = "`textbook_email_subscribe`")
+    private Integer textbookEmailSubscribe;
+
     private static final long serialVersionUID = 1L;
 
     /**
@@ -751,21 +769,39 @@ public class User implements Serializable {
     }
 
     /**
-     * 获取是否订阅最新机经
+     * 获取注册ip
      *
-     * @return is_subscribe_textbook - 是否订阅最新机经
+     * @return register_ip - 注册ip
      */
-    public Integer getIsSubscribeTextbook() {
-        return isSubscribeTextbook;
+    public String getRegisterIp() {
+        return registerIp;
     }
 
     /**
-     * 设置是否订阅最新机经
+     * 设置注册ip
      *
-     * @param isSubscribeTextbook 是否订阅最新机经
+     * @param registerIp 注册ip
      */
-    public void setIsSubscribeTextbook(Integer isSubscribeTextbook) {
-        this.isSubscribeTextbook = isSubscribeTextbook;
+    public void setRegisterIp(String registerIp) {
+        this.registerIp = registerIp;
+    }
+
+    /**
+     * 获取是否冻结
+     *
+     * @return is_frozen - 是否冻结
+     */
+    public Integer getIsFrozen() {
+        return isFrozen;
+    }
+
+    /**
+     * 设置是否冻结
+     *
+     * @param isFrozen 是否冻结
+     */
+    public void setIsFrozen(Integer isFrozen) {
+        this.isFrozen = isFrozen;
     }
 
     /**
@@ -782,6 +818,42 @@ public class User implements Serializable {
         this.createTime = createTime;
     }
 
+    /**
+     * 获取资料订阅
+     *
+     * @return data_email_subscribe - 资料订阅
+     */
+    public Integer getDataEmailSubscribe() {
+        return dataEmailSubscribe;
+    }
+
+    /**
+     * 设置资料订阅
+     *
+     * @param dataEmailSubscribe 资料订阅
+     */
+    public void setDataEmailSubscribe(Integer dataEmailSubscribe) {
+        this.dataEmailSubscribe = dataEmailSubscribe;
+    }
+
+    /**
+     * 获取机经邮箱订阅
+     *
+     * @return textbook_email_subscribe - 机经邮箱订阅
+     */
+    public Integer getTextbookEmailSubscribe() {
+        return textbookEmailSubscribe;
+    }
+
+    /**
+     * 设置机经邮箱订阅
+     *
+     * @param textbookEmailSubscribe 机经邮箱订阅
+     */
+    public void setTextbookEmailSubscribe(Integer textbookEmailSubscribe) {
+        this.textbookEmailSubscribe = textbookEmailSubscribe;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -819,8 +891,11 @@ public class User implements Serializable {
         sb.append(", inviteCode=").append(inviteCode);
         sb.append(", totalMoney=").append(totalMoney);
         sb.append(", inviteNumber=").append(inviteNumber);
-        sb.append(", isSubscribeTextbook=").append(isSubscribeTextbook);
+        sb.append(", registerIp=").append(registerIp);
+        sb.append(", isFrozen=").append(isFrozen);
         sb.append(", createTime=").append(createTime);
+        sb.append(", dataEmailSubscribe=").append(dataEmailSubscribe);
+        sb.append(", textbookEmailSubscribe=").append(textbookEmailSubscribe);
         sb.append("]");
         return sb.toString();
     }
@@ -1143,12 +1218,22 @@ public class User implements Serializable {
         }
 
         /**
-         * 设置是否订阅最新机经
+         * 设置注册ip
+         *
+         * @param registerIp 注册ip
+         */
+        public Builder registerIp(String registerIp) {
+            obj.setRegisterIp(registerIp);
+            return this;
+        }
+
+        /**
+         * 设置是否冻结
          *
-         * @param isSubscribeTextbook 是否订阅最新机经
+         * @param isFrozen 是否冻结
          */
-        public Builder isSubscribeTextbook(Integer isSubscribeTextbook) {
-            obj.setIsSubscribeTextbook(isSubscribeTextbook);
+        public Builder isFrozen(Integer isFrozen) {
+            obj.setIsFrozen(isFrozen);
             return this;
         }
 
@@ -1160,6 +1245,26 @@ public class User implements Serializable {
             return this;
         }
 
+        /**
+         * 设置资料订阅
+         *
+         * @param dataEmailSubscribe 资料订阅
+         */
+        public Builder dataEmailSubscribe(Integer dataEmailSubscribe) {
+            obj.setDataEmailSubscribe(dataEmailSubscribe);
+            return this;
+        }
+
+        /**
+         * 设置机经邮箱订阅
+         *
+         * @param textbookEmailSubscribe 机经邮箱订阅
+         */
+        public Builder textbookEmailSubscribe(Integer textbookEmailSubscribe) {
+            obj.setTextbookEmailSubscribe(textbookEmailSubscribe);
+            return this;
+        }
+
         public User build() {
             return this.obj;
         }

+ 230 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/UserAbnormal.java

@@ -0,0 +1,230 @@
+package com.qxgmat.data.dao.entity;
+
+import java.io.Serializable;
+import java.util.Date;
+import javax.persistence.*;
+
+@Table(name = "user_abnormal")
+public class UserAbnormal implements Serializable {
+    @Id
+    @Column(name = "`id`")
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Integer id;
+
+    /**
+     * 用户id
+     */
+    @Column(name = "`user_id`")
+    private Integer userId;
+
+    /**
+     * 是否已警告
+     */
+    @Column(name = "`is_alert`")
+    private Integer isAlert;
+
+    /**
+     * 登录ip
+     */
+    @Column(name = "`login_ip`")
+    private String loginIp;
+
+    /**
+     * 已累计警告
+     */
+    @Column(name = "`total_alert`")
+    private Integer totalAlert;
+
+    @Column(name = "`create_time`")
+    private Date createTime;
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * @return id
+     */
+    public Integer getId() {
+        return id;
+    }
+
+    /**
+     * @param id
+     */
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * 获取用户id
+     *
+     * @return user_id - 用户id
+     */
+    public Integer getUserId() {
+        return userId;
+    }
+
+    /**
+     * 设置用户id
+     *
+     * @param userId 用户id
+     */
+    public void setUserId(Integer userId) {
+        this.userId = userId;
+    }
+
+    /**
+     * 获取是否已警告
+     *
+     * @return is_alert - 是否已警告
+     */
+    public Integer getIsAlert() {
+        return isAlert;
+    }
+
+    /**
+     * 设置是否已警告
+     *
+     * @param isAlert 是否已警告
+     */
+    public void setIsAlert(Integer isAlert) {
+        this.isAlert = isAlert;
+    }
+
+    /**
+     * 获取登录ip
+     *
+     * @return login_ip - 登录ip
+     */
+    public String getLoginIp() {
+        return loginIp;
+    }
+
+    /**
+     * 设置登录ip
+     *
+     * @param loginIp 登录ip
+     */
+    public void setLoginIp(String loginIp) {
+        this.loginIp = loginIp;
+    }
+
+    /**
+     * 获取已累计警告
+     *
+     * @return total_alert - 已累计警告
+     */
+    public Integer getTotalAlert() {
+        return totalAlert;
+    }
+
+    /**
+     * 设置已累计警告
+     *
+     * @param totalAlert 已累计警告
+     */
+    public void setTotalAlert(Integer totalAlert) {
+        this.totalAlert = totalAlert;
+    }
+
+    /**
+     * @return create_time
+     */
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    /**
+     * @param createTime
+     */
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", userId=").append(userId);
+        sb.append(", isAlert=").append(isAlert);
+        sb.append(", loginIp=").append(loginIp);
+        sb.append(", totalAlert=").append(totalAlert);
+        sb.append(", createTime=").append(createTime);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    public static UserAbnormal.Builder builder() {
+        return new UserAbnormal.Builder();
+    }
+
+    public static class Builder {
+        private UserAbnormal obj;
+
+        public Builder() {
+            this.obj = new UserAbnormal();
+        }
+
+        /**
+         * @param id
+         */
+        public Builder id(Integer id) {
+            obj.setId(id);
+            return this;
+        }
+
+        /**
+         * 设置用户id
+         *
+         * @param userId 用户id
+         */
+        public Builder userId(Integer userId) {
+            obj.setUserId(userId);
+            return this;
+        }
+
+        /**
+         * 设置是否已警告
+         *
+         * @param isAlert 是否已警告
+         */
+        public Builder isAlert(Integer isAlert) {
+            obj.setIsAlert(isAlert);
+            return this;
+        }
+
+        /**
+         * 设置登录ip
+         *
+         * @param loginIp 登录ip
+         */
+        public Builder loginIp(String loginIp) {
+            obj.setLoginIp(loginIp);
+            return this;
+        }
+
+        /**
+         * 设置已累计警告
+         *
+         * @param totalAlert 已累计警告
+         */
+        public Builder totalAlert(Integer totalAlert) {
+            obj.setTotalAlert(totalAlert);
+            return this;
+        }
+
+        /**
+         * @param createTime
+         */
+        public Builder createTime(Date createTime) {
+            obj.setCreateTime(createTime);
+            return this;
+        }
+
+        public UserAbnormal build() {
+            return this.obj;
+        }
+    }
+}

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

@@ -78,6 +78,12 @@ public class UserOrderRecord implements Serializable {
     private Integer askTime;
 
     /**
+     * 订阅:千行、资料等
+     */
+    @Column(name = "`is_subscribe`")
+    private Integer isSubscribe;
+
+    /**
      * 开通开始时间
      */
     @Column(name = "`start_time`")
@@ -331,6 +337,24 @@ public class UserOrderRecord implements Serializable {
     }
 
     /**
+     * 获取订阅:千行、资料等
+     *
+     * @return is_subscribe - 订阅:千行、资料等
+     */
+    public Integer getIsSubscribe() {
+        return isSubscribe;
+    }
+
+    /**
+     * 设置订阅:千行、资料等
+     *
+     * @param isSubscribe 订阅:千行、资料等
+     */
+    public void setIsSubscribe(Integer isSubscribe) {
+        this.isSubscribe = isSubscribe;
+    }
+
+    /**
      * 获取开通开始时间
      *
      * @return start_time - 开通开始时间
@@ -470,6 +494,7 @@ public class UserOrderRecord implements Serializable {
         sb.append(", teacherId=").append(teacherId);
         sb.append(", vsNumber=").append(vsNumber);
         sb.append(", askTime=").append(askTime);
+        sb.append(", isSubscribe=").append(isSubscribe);
         sb.append(", startTime=").append(startTime);
         sb.append(", endTime=").append(endTime);
         sb.append(", useStartTime=").append(useStartTime);
@@ -611,6 +636,16 @@ public class UserOrderRecord implements Serializable {
         }
 
         /**
+         * 设置订阅:千行、资料等
+         *
+         * @param isSubscribe 订阅:千行、资料等
+         */
+        public Builder isSubscribe(Integer isSubscribe) {
+            obj.setIsSubscribe(isSubscribe);
+            return this;
+        }
+
+        /**
          * 设置开通开始时间
          *
          * @param startTime 开通开始时间

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

@@ -9,6 +9,7 @@
     <result column="struct_id" jdbcType="INTEGER" property="structId" />
     <result column="parent_struct_id" jdbcType="INTEGER" property="parentStructId" />
     <result column="course_module" jdbcType="VARCHAR" property="courseModule" />
+    <result column="no_number" jdbcType="INTEGER" property="noNumber" />
     <result column="vs_type" jdbcType="VARCHAR" property="vsType" />
     <result column="video_type" jdbcType="VARCHAR" property="videoType" />
     <result column="extend" jdbcType="VARCHAR" property="extend" />
@@ -50,7 +51,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `struct_id`, `parent_struct_id`, `course_module`, `vs_type`, `video_type`, 
+    `id`, `struct_id`, `parent_struct_id`, `course_module`, `no_number`, `vs_type`, `video_type`, 
     `extend`, `title`, `comment`, `crowd`, `price`, `teacher`, `cover`, `min_number`, 
     `max_number`, `expire_days`, `expire_time`, `use_expire_time`, `wechat_avatar`, `trail_number`, 
     `sale_number`, `package_sale_number`, `create_time`, `update_time`

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

@@ -12,6 +12,7 @@
     <result column="order" jdbcType="INTEGER" property="order" />
     <result column="level" jdbcType="INTEGER" property="level" />
     <result column="is_adapt" jdbcType="INTEGER" property="isAdapt" />
+    <result column="question_status" jdbcType="INTEGER" property="questionStatus" />
     <result column="extend" jdbcType="VARCHAR" property="extend" />
   </resultMap>
   <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.qxgmat.data.dao.entity.ExaminationStruct">
@@ -24,7 +25,8 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `title_zh`, `title_en`, `parent_id`, `order`, `level`, `is_adapt`, `extend`
+    `id`, `title_zh`, `title_en`, `parent_id`, `order`, `level`, `is_adapt`, `question_status`, 
+    `extend`
   </sql>
   <sql id="Blob_Column_List">
     <!--

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

@@ -18,6 +18,7 @@
     <result column="rc_version" jdbcType="INTEGER" property="rcVersion" />
     <result column="rc_time" jdbcType="TIMESTAMP" property="rcTime" />
     <result column="history_number" jdbcType="INTEGER" property="historyNumber" />
+    <result column="question_status" jdbcType="INTEGER" property="questionStatus" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
     <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
   </resultMap>
@@ -26,6 +27,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `start_date`, `end_date`, `quant`, `quant_version`, `quant_time`, `ir`, `ir_version`, 
-    `ir_time`, `rc`, `rc_version`, `rc_time`, `history_number`, `create_time`, `update_time`
+    `ir_time`, `rc`, `rc_version`, `rc_time`, `history_number`, `question_status`, `create_time`, 
+    `update_time`
   </sql>
 </mapper>

+ 21 - 0
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAbnormalMapper.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qxgmat.data.dao.UserAbnormalMapper">
+  <resultMap id="BaseResultMap" type="com.qxgmat.data.dao.entity.UserAbnormal">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <id column="id" jdbcType="INTEGER" property="id" />
+    <result column="user_id" jdbcType="INTEGER" property="userId" />
+    <result column="is_alert" jdbcType="INTEGER" property="isAlert" />
+    <result column="login_ip" jdbcType="VARCHAR" property="loginIp" />
+    <result column="total_alert" jdbcType="INTEGER" property="totalAlert" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    `id`, `user_id`, `is_alert`, `login_ip`, `total_alert`, `create_time`
+  </sql>
+</mapper>

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

@@ -36,8 +36,11 @@
     <result column="invite_code" jdbcType="VARCHAR" property="inviteCode" />
     <result column="total_money" jdbcType="DECIMAL" property="totalMoney" />
     <result column="invite_number" jdbcType="INTEGER" property="inviteNumber" />
-    <result column="is_subscribe_textbook" jdbcType="INTEGER" property="isSubscribeTextbook" />
+    <result column="register_ip" jdbcType="VARCHAR" property="registerIp" />
+    <result column="is_frozen" jdbcType="INTEGER" property="isFrozen" />
     <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="data_email_subscribe" jdbcType="INTEGER" property="dataEmailSubscribe" />
+    <result column="textbook_email_subscribe" jdbcType="INTEGER" property="textbookEmailSubscribe" />
   </resultMap>
   <sql id="Base_Column_List">
     <!--
@@ -48,7 +51,7 @@
     `wechat_expire_time`, `real_time`, `real_name`, `real_address`, `real_identity`, 
     `real_photo_front`, `real_photo_back`, `real_status`, `prepare_time`, `prepare_status`, 
     `prepare_goal`, `prepare_examination_time`, `prepare_score_time`, `latest_exercise`, 
-    `latest_error`, `origin_id`, `invite_code`, `total_money`, `invite_number`, `is_subscribe_textbook`, 
-    `create_time`
+    `latest_error`, `origin_id`, `invite_code`, `total_money`, `invite_number`, `register_ip`, 
+    `is_frozen`, `create_time`, `data_email_subscribe`, `textbook_email_subscribe`
   </sql>
 </mapper>

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

@@ -17,6 +17,7 @@
     <result column="teacher_id" jdbcType="INTEGER" property="teacherId" />
     <result column="vs_number" jdbcType="INTEGER" property="vsNumber" />
     <result column="ask_time" jdbcType="INTEGER" property="askTime" />
+    <result column="is_subscribe" jdbcType="INTEGER" property="isSubscribe" />
     <result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
     <result column="end_time" jdbcType="TIMESTAMP" property="endTime" />
     <result column="use_start_time" jdbcType="TIMESTAMP" property="useStartTime" />
@@ -30,7 +31,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `user_id`, `order_id`, `parent_record_id`, `product_type`, `product_id`, `service`, 
-    `param`, `source`, `teacher_id`, `vs_number`, `ask_time`, `start_time`, `end_time`, 
-    `use_start_time`, `use_end_time`, `is_used`, `is_stop`, `create_time`
+    `param`, `source`, `teacher_id`, `vs_number`, `ask_time`, `is_subscribe`, `start_time`, 
+    `end_time`, `use_start_time`, `use_end_time`, `is_used`, `is_stop`, `create_time`
   </sql>
 </mapper>

+ 17 - 0
server/data/src/main/java/com/qxgmat/data/relation/CourseDataHistoryRelationMapper.java

@@ -0,0 +1,17 @@
+package com.qxgmat.data.relation;
+
+import com.qxgmat.data.dao.entity.CourseDataHistory;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Created by gaojie on 2017/11/9.
+ */
+public interface CourseDataHistoryRelationMapper {
+
+    List<CourseDataHistory> listByUser(
+            @Param("dataId") Integer dataId,
+            @Param("userId") Integer userId
+    );
+}

+ 21 - 0
server/data/src/main/java/com/qxgmat/data/relation/CourseDataRelationMapper.java

@@ -0,0 +1,21 @@
+package com.qxgmat.data.relation;
+
+import com.qxgmat.data.dao.entity.CourseData;
+import com.qxgmat.data.relation.entity.CourseStudentNumberRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Created by gaojie on 2017/11/9.
+ */
+public interface CourseDataRelationMapper {
+
+    List<CourseData> listByUser(
+            @Param("userId") Integer userId,
+            @Param("structId") Integer structId,
+            @Param("dataType") String dataType,
+            String order,
+            String direction
+    );
+}

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

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qxgmat.data.relation.CourseDataHistoryRelationMapper">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.CourseDataHistory">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <id column="id" jdbcType="INTEGER" property="id" />
+  </resultMap>
+
+  <sql id="Id_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    cd.`id`
+  </sql>
+  <!--
+    获取用户购买的资料更新记录
+  -->
+  <select id="listByUser" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />
+    from `course_data_history` cd
+    left join `user_order_record` uo on `uo`.`product_type`='data' and `uo`.`product_id` = cd.`data_id`
+    <if test="userId != null">
+      and `uo`.userId = #{userId,jdbcType=VARCHAR}
+    </if>
+    <!--and `uo`.create_time &lt; cd.create_time-->
+    where
+    `uo`.`id` > 0
+    <if test="dataId != null">
+      and `cd`.dataId = #{dataId,jdbcType=VARCHAR}
+    </if>
+
+  </select>
+</mapper>

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

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qxgmat.data.relation.CourseDataRelationMapper">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.CourseData">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <id column="id" jdbcType="INTEGER" property="id" />
+  </resultMap>
+
+  <sql id="Id_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    cd.`id`
+  </sql>
+  <!--
+    获取用户购买的资料记录
+  -->
+  <select id="listByUser" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />
+    from `course_data` cd
+    left join `user_order_record` uo on `uo`.`product_type`='data' and `uo`.`product_id` = cd.`id`
+    <if test="userId != null">
+      and `uo`.userId = #{userId,jdbcType=VARCHAR}
+    </if>
+    where
+    `uo`.`id` > 0
+    <if test="structId != null">
+      and (cd.`struct_id` = #{structId,jdbcType=VARCHAR} or cd.`parent_struct_id` = #{structId,jdbcType=VARCHAR})
+    </if>
+    <if test="dataType != null">
+      and cd.`data_type` = #{dataType,jdbcType=VARCHAR}
+    </if>
+
+    order by ${order} ${direction}
+  </select>
+</mapper>

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

@@ -375,6 +375,9 @@ public class CourseController {
         // 查询课时
         CourseNo entity = Transform.dtoToEntity(dto);
         entity = courseNoService.addNo(entity);
+        // 统计课时数
+        Course course = courseService.get(dto.getCourseId());
+        courseService.edit(Course.builder().id(course.getId()).noNumber(course.getNoNumber() + 1).build());
         managerLogService.log(request);
         return ResponseHelp.success(Transform.convert(entity, CourseNo.class));
     }
@@ -392,8 +395,11 @@ public class CourseController {
     @RequestMapping(value = "/no/delete", method = RequestMethod.DELETE)
     @ApiOperation(value = "删除课时", httpMethod = "DELETE")
     public Response<Boolean> deleteNo(@RequestParam int id, HttpServletRequest request) {
-        // 调整课时顺序
+        CourseNo in = courseNoService.get(id);
         courseNoService.deleteNo(id);
+        // 统计课时数
+        Course course = courseService.get(in.getCourseId());
+        courseService.edit(Course.builder().id(course.getId()).noNumber(course.getNoNumber() - 1).build());
         managerLogService.log(request);
         return ResponseHelp.success(true);
     }

+ 81 - 34
server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java

@@ -9,6 +9,7 @@ import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
+import com.qxgmat.dto.extend.CourseExtendDto;
 import com.qxgmat.dto.extend.UserPreviewPaperExtendDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
@@ -72,40 +73,74 @@ public class CourseController {
         return ResponseHelp.success(pr);
     }
 
-//    @RequestMapping(value = "/video/list", method = RequestMethod.GET)
-//    @ApiOperation(value = "在线课程列表", httpMethod = "GET")
-//    public Response<PageMessage<CourseListDto>> listVideo(
-//            @RequestParam(required = false, defaultValue = "1") int page,
-//            @RequestParam(required = false, defaultValue = "100") int size,
-//            @RequestParam(required = true) String subject,
-//            @RequestParam(required = false) String[] qualitys,
-//            @RequestParam(required = false) Boolean isOld,
-//            @RequestParam(required = false, defaultValue = "id") String order,
-//            @RequestParam(required = false, defaultValue = "desc") String direction,
-//            HttpSession session) {
-//
-//        Page<Course> p = courseService.list(page, size, library.getId(), QuestionSubject.ValueOf(subject), qualitys, isOld, order, DirectionStatus.ValueOf(direction));
-//
-//        List<CourseListDto> pr = Transform.convert(p, CourseListDto.class);
-//        return ResponseHelp.success(pr, page, size, p.getTotal());
-//    }
-
-//    @RequestMapping(value = "/package/list", method = RequestMethod.GET)
-//    @ApiOperation(value = "套餐列表", httpMethod = "GET")
-//    public Response<PageMessage<CourseListDto>> listPackage(
-//            @RequestParam(required = false, defaultValue = "1") int page,
-//            @RequestParam(required = false, defaultValue = "100") int size,
-//            @RequestParam(required = true) String subject,
-//            @RequestParam(required = false) String[] qualitys,
-//            @RequestParam(required = false) Boolean isOld,
-//            @RequestParam(required = false, defaultValue = "id") String order,
-//            @RequestParam(required = false, defaultValue = "desc") String direction,
-//            HttpSession session) {
-//
-//        Page<Course> p = courseService.list(page, size, library.getId(), QuestionSubject.ValueOf(subject), qualitys, isOld, order, DirectionStatus.ValueOf(direction));
-//
-//        return ResponseHelp.success(p, page, size, p.getTotal());
-//    }
+    @RequestMapping(value = "/video/list", method = RequestMethod.GET)
+    @ApiOperation(value = "在线课程列表", httpMethod = "GET")
+    public Response<PageMessage<CourseListDto>> listVideo(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) Integer structId,
+            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "desc") String direction,
+            HttpSession session) {
+
+        Page<Course> p = courseService.list(page, size, CourseModule.VIDEO, structId, order, DirectionStatus.ValueOf(direction));
+
+        List<CourseListDto> pr = Transform.convert(p, CourseListDto.class);
+        return ResponseHelp.success(pr, page, size, p.getTotal());
+    }
+
+    @RequestMapping(value = "/detail", method = RequestMethod.GET)
+    @ApiOperation(value = "课程详情", httpMethod = "GET")
+    public Response<Course> detail(
+            @RequestParam(required = true) Integer courseId
+    ) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        Course course = courseService.get(courseId);
+
+        return ResponseHelp.success(course);
+    }
+
+    @RequestMapping(value = "/package/list", method = RequestMethod.GET)
+    @ApiOperation(value = "套餐列表", httpMethod = "GET")
+    public Response<PageMessage<CoursePackageListDto>> listPackage(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) Integer structId,
+            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "desc") String direction,
+            HttpSession session) {
+
+        Page<CoursePackage> p = coursePackageService.list(page, size, structId, order, DirectionStatus.ValueOf(direction));
+
+        List<CoursePackageListDto> pr = Transform.convert(p, CoursePackageListDto.class);
+
+        Map<Integer, Integer[]> courseIdsMap = new HashMap<>();
+        for(CoursePackage cp : p){
+            courseIdsMap.put(cp.getId(), cp.getCourseIds());
+        }
+        Map courseMap = courseService.groupByMap(courseIdsMap);
+        Transform.combine(pr, courseMap, CoursePackageListDto.class, "id", "courses", CourseExtendDto.class);
+
+        return ResponseHelp.success(pr, page, size, p.getTotal());
+    }
+
+    @RequestMapping(value = "/package/detail", method = RequestMethod.GET)
+    @ApiOperation(value = "套餐详情", httpMethod = "GET")
+    public Response<CoursePackageDetailDto> detailPackage(
+            @RequestParam(required = true) Integer packageId
+    ) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        CoursePackage coursePackage = coursePackageService.get(packageId);
+        CoursePackageDetailDto dto = Transform.convert(coursePackage, CoursePackageDetailDto.class);
+
+        List<Course> courseList = courseService.select(coursePackage.getCourseIds());
+
+        dto.setCourses(Transform.convert(courseList, CourseExtendDto.class));
+
+        return ResponseHelp.success(dto);
+    }
 
     @RequestMapping(value = "/data/list", method = RequestMethod.GET)
     @ApiOperation(value = "资料列表", httpMethod = "GET")
@@ -126,6 +161,18 @@ public class CourseController {
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 
+    @RequestMapping(value = "/data/detail", method = RequestMethod.GET)
+    @ApiOperation(value = "资料详情", httpMethod = "GET")
+    public Response<CourseData> detailData(
+            @RequestParam(required = true) Integer dataId
+    ) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        CourseData courseData = courseDataService.get(dataId);
+
+        return ResponseHelp.success(courseData);
+    }
+
     @RequestMapping(value = "/progress", method = RequestMethod.GET)
     @ApiOperation(value = "获取课程进度", notes = "获取所有课程及状态进度", httpMethod = "GET")
     public Response<List<UserCourseDetailDto>> progress()  {

+ 49 - 6
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -13,6 +13,7 @@ import com.qxgmat.data.constants.enums.module.PaperModule;
 import com.qxgmat.data.constants.enums.module.PaperOrigin;
 import com.qxgmat.data.constants.enums.module.QuestionModule;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
+import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.inline.UserQuestionStat;
 import com.qxgmat.data.relation.entity.*;
@@ -97,6 +98,12 @@ public class MyController {
     private TextbookQuestionService textbookQuestionService;
 
     @Autowired
+    private CourseDataService courseDataService;
+
+    @Autowired
+    private CourseDataHistoryService courseDataHistoryService;
+
+    @Autowired
     private UsersService usersService;
 
     @Autowired
@@ -988,22 +995,58 @@ public class MyController {
         if (!record.getUserId().equals(user.getId())){
             throw new ParameterException("记录不存在");
         }
+        // todo
 
         return ResponseHelp.success(record);
     }
 
-    @RequestMapping(value = "/record/use", method = RequestMethod.GET)
-    @ApiOperation(value = "开通", notes = "开通", httpMethod = "GET")
-    public Response<UserOrderRecord> useRecord(
-            @RequestParam(required = true) Integer id
-    )  {
+    @RequestMapping(value = "/record/use", method = RequestMethod.POST)
+    @ApiOperation(value = "开通", notes = "开通", httpMethod = "POST")
+    public Response<UserOrderRecord> useRecord(@RequestBody @Validated RecordOpenDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
-        UserOrderRecord record = userOrderRecordService.get(id);
+        UserOrderRecord record = userOrderRecordService.get(dto.getRecordId());
         if (!record.getUserId().equals(user.getId())){
             throw new ParameterException("记录不存在");
         }
 
+        // todo
 
         return ResponseHelp.success(record);
     }
+
+    @RequestMapping(value = "/data/history", method = RequestMethod.GET)
+    @ApiOperation(value = "资料更新记录", httpMethod = "GET")
+    public Response<PageMessage<CourseDataHistoryInfoDto>> listDataHistory(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) Integer dataId,
+            HttpSession session) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        Page<CourseDataHistory> p = courseDataHistoryService.listByUser(page, size, dataId, user.getId());
+        List<CourseDataHistoryInfoDto> pr = Transform.convert(p, CourseDataHistoryInfoDto.class);
+
+        // 绑定资料
+        Collection dataIds = Transform.getIds(p, CourseDataHistory.class, "dataId");
+        List<CourseData> dataList = courseDataService.select(dataIds);
+        Transform.combine(pr, dataList, CourseDataHistoryInfoDto.class, "dataId", "data", CourseData.class, "id", CourseDataExtendDto.class);
+        return ResponseHelp.success(pr, page, size, p.getTotal());
+    }
+
+    @RequestMapping(value = "/data/list", method = RequestMethod.GET)
+    @ApiOperation(value = "购买的资料记录", httpMethod = "GET")
+    public Response<PageMessage<CourseData>> listData(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) Integer structId,
+            @RequestParam(required = false) String dataType,
+            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "desc") String direction,
+            HttpSession session) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        Page<CourseData> p = courseDataService.listByUser(page, size, user.getId(), structId, DataType.ValueOf(dataType),order, DirectionStatus.ValueOf(direction));
+
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
 }

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

@@ -149,7 +149,7 @@ public class SentenceController
     }
 
     @RequestMapping(value = "/article/detail", method = RequestMethod.GET)
-    @ApiOperation(value = "长难句文章详情", httpMethod = "PUT")
+    @ApiOperation(value = "长难句文章详情", httpMethod = "GET")
     public Response<UserSentenceArticleDetailDto> detailArticle(
             @RequestParam(required = true) Integer articleId
     ) {

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

@@ -102,7 +102,7 @@ public class TextbookController
         return ResponseHelp.success(libraryList);
     }
 
-    @RequestMapping(value = "/history/list", method = RequestMethod.GET)
+    @RequestMapping(value = "/history/all", method = RequestMethod.GET)
     @ApiOperation(value = "机经更新记录", httpMethod = "GET")
     public Response<List<TextbookLibraryHistory>> listHistory(
             @RequestParam(required = true) String subject,

+ 27 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseDataExtendDto.java

@@ -0,0 +1,27 @@
+package com.qxgmat.dto.extend;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CourseData;
+
+@Dto(entity = CourseData.class)
+public class CourseDataExtendDto {
+    private Integer id;
+
+    private String title;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+}

+ 59 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java

@@ -0,0 +1,59 @@
+package com.qxgmat.dto.extend;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.Course;
+
+import java.math.BigDecimal;
+
+@Dto(entity = Course.class)
+public class CourseExtendDto {
+    private Integer id;
+
+    private String title;
+
+    private String teacher;
+
+    private BigDecimal price;
+
+    private Integer noNumber;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public Integer getNoNumber() {
+        return noNumber;
+    }
+
+    public void setNoNumber(Integer noNumber) {
+        this.noNumber = noNumber;
+    }
+
+    public String getTeacher() {
+        return teacher;
+    }
+
+    public void setTeacher(String teacher) {
+        this.teacher = teacher;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+}

+ 23 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/RecordOpenDto.java

@@ -0,0 +1,23 @@
+package com.qxgmat.dto.request;
+
+public class RecordOpenDto {
+    private Integer recordId;
+
+    private Integer isSubscribe;
+
+    public Integer getRecordId() {
+        return recordId;
+    }
+
+    public void setRecordId(Integer recordId) {
+        this.recordId = recordId;
+    }
+
+    public Integer getIsSubscribe() {
+        return isSubscribe;
+    }
+
+    public void setIsSubscribe(Integer isSubscribe) {
+        this.isSubscribe = isSubscribe;
+    }
+}

+ 91 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDataHistoryInfoDto.java

@@ -0,0 +1,91 @@
+package com.qxgmat.dto.response;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CourseDataHistory;
+import com.qxgmat.dto.extend.CourseDataExtendDto;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Dto(entity = CourseDataHistory.class)
+public class CourseDataHistoryInfoDto {
+    private Integer id;
+
+    private Integer dataId;
+
+    private CourseDataExtendDto data;
+
+    private String version;
+
+    private Date time;
+
+    private String position;
+
+    private String originContent;
+
+    private String content;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getDataId() {
+        return dataId;
+    }
+
+    public void setDataId(Integer dataId) {
+        this.dataId = dataId;
+    }
+
+    public CourseDataExtendDto getData() {
+        return data;
+    }
+
+    public void setData(CourseDataExtendDto data) {
+        this.data = data;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public Date getTime() {
+        return time;
+    }
+
+    public void setTime(Date time) {
+        this.time = time;
+    }
+
+    public String getPosition() {
+        return position;
+    }
+
+    public void setPosition(String position) {
+        this.position = position;
+    }
+
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 113 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/CoursePackageDetailDto.java

@@ -0,0 +1,113 @@
+package com.qxgmat.dto.response;
+
+import com.alibaba.fastjson.JSONObject;
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CoursePackage;
+import com.qxgmat.dto.extend.CourseExtendDto;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Date;
+
+@Dto(entity = CoursePackage.class)
+public class CoursePackageDetailDto {
+    private Integer id;
+
+    private Integer structId;
+
+    private String title;
+
+    private BigDecimal price;
+
+    private Integer[] courseIds;
+
+    private Collection<CourseExtendDto> courses;
+
+    private JSONObject gift;
+
+    private Integer saleNumber;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public Integer[] getCourseIds() {
+        return courseIds;
+    }
+
+    public void setCourseIds(Integer[] courseIds) {
+        this.courseIds = courseIds;
+    }
+
+    public JSONObject getGift() {
+        return gift;
+    }
+
+    public void setGift(JSONObject gift) {
+        this.gift = gift;
+    }
+
+    public Integer getSaleNumber() {
+        return saleNumber;
+    }
+
+    public void setSaleNumber(Integer saleNumber) {
+        this.saleNumber = saleNumber;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public Collection<CourseExtendDto> getCourses() {
+        return courses;
+    }
+
+    public void setCourses(Collection<CourseExtendDto> courses) {
+        this.courses = courses;
+    }
+}

+ 113 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/CoursePackageListDto.java

@@ -0,0 +1,113 @@
+package com.qxgmat.dto.response;
+
+import com.alibaba.fastjson.JSONObject;
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CoursePackage;
+import com.qxgmat.dto.extend.CourseExtendDto;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Date;
+
+@Dto(entity = CoursePackage.class)
+public class CoursePackageListDto {
+    private Integer id;
+
+    private Integer structId;
+
+    private String title;
+
+    private BigDecimal price;
+
+    private Integer[] courseIds;
+
+    private Collection<CourseExtendDto> courses;
+
+    private JSONObject gift;
+
+    private Integer saleNumber;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public BigDecimal getPrice() {
+        return price;
+    }
+
+    public void setPrice(BigDecimal price) {
+        this.price = price;
+    }
+
+    public Integer[] getCourseIds() {
+        return courseIds;
+    }
+
+    public void setCourseIds(Integer[] courseIds) {
+        this.courseIds = courseIds;
+    }
+
+    public JSONObject getGift() {
+        return gift;
+    }
+
+    public void setGift(JSONObject gift) {
+        this.gift = gift;
+    }
+
+    public Integer getSaleNumber() {
+        return saleNumber;
+    }
+
+    public void setSaleNumber(Integer saleNumber) {
+        this.saleNumber = saleNumber;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public Collection<CourseExtendDto> getCourses() {
+        return courses;
+    }
+
+    public void setCourses(Collection<CourseExtendDto> courses) {
+        this.courses = courses;
+    }
+}

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

@@ -91,7 +91,7 @@ public class ManagerService extends AbstractService {
     public Page<Manager> select(int page, int pageSize, String order, DirectionStatus direction){
         Example example = new Example(Manager.class);
 
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction != null){
             switch(direction){
                 case ASC:

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

@@ -157,7 +157,7 @@ public class ExaminationService extends AbstractService {
      * @return
      */
     public Page<QuestionNoRelation> listAdmin(int page, int pageSize, String questionType, Number structId, Number questionNo, Number paperId, String place, String difficult, String order, DirectionStatus direction){
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
         }

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

@@ -82,7 +82,7 @@ public class ExerciseService extends AbstractService {
      * @return
      */
     public Page<QuestionNoRelation> listAdmin(int page, int pageSize, String questionType, Number structId, Number questionNo, Number paperId, String place, String difficult, String startTime, String endTime, String order, DirectionStatus direction){
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
         }

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

@@ -2,6 +2,7 @@ package com.qxgmat.service.inline;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.nuliji.tools.mybatis.Example;
@@ -9,6 +10,7 @@ import com.qxgmat.data.dao.CourseDataHistoryMapper;
 import com.qxgmat.data.dao.CourseDataMapper;
 import com.qxgmat.data.dao.entity.CourseData;
 import com.qxgmat.data.dao.entity.CourseDataHistory;
+import com.qxgmat.data.relation.CourseDataHistoryRelationMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -24,6 +26,28 @@ public class CourseDataHistoryService extends AbstractService {
     @Resource
     private CourseDataHistoryMapper courseDataHistoryMapper;
 
+    @Resource
+    private CourseDataHistoryRelationMapper courseDataHistoryRelationMapper;
+
+    /**
+     * 获取用户订阅
+     * @param page
+     * @param size
+     * @param userId
+     * @return
+     */
+    public Page<CourseDataHistory> listByUser(int page, int size, Integer dataId, Integer userId){
+        Page<CourseDataHistory> p = page(()->{
+            courseDataHistoryRelationMapper.listByUser(dataId, userId);
+        }, page, size);
+
+        Collection ids = Transform.getIds(p, CourseDataHistory.class, "id");
+
+        // 获取详细数据
+        List<CourseDataHistory> list = select(ids);
+        Transform.replace(p, list, CourseDataHistory.class, "id");
+        return p;
+    }
     /**
      * 根据资料获取更新记录
      * @param page

+ 26 - 2
server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseDataService.java

@@ -2,6 +2,7 @@ package com.qxgmat.service.inline;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.nuliji.tools.mybatis.Example;
@@ -11,6 +12,7 @@ import com.qxgmat.data.dao.CourseDataMapper;
 import com.qxgmat.data.dao.CourseMapper;
 import com.qxgmat.data.dao.entity.Course;
 import com.qxgmat.data.dao.entity.CourseData;
+import com.qxgmat.data.relation.CourseDataRelationMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -26,6 +28,9 @@ public class CourseDataService extends AbstractService {
     @Resource
     private CourseDataMapper courseDataMapper;
 
+    @Resource
+    private CourseDataRelationMapper courseDataRelationMapper;
+
     public Page<CourseData> listAdmin(int page, int size, Integer structId, DataType dataType, String order, DirectionStatus direction){
         Example example = new Example(CourseData.class);
         if(structId != null){
@@ -41,7 +46,7 @@ public class CourseDataService extends AbstractService {
                     .andEqualTo("dataType", dataType.key)
             );
         }
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         switch(direction){
             case ASC:
                 example.orderBy(order).asc();
@@ -80,7 +85,7 @@ public class CourseDataService extends AbstractService {
                             .andEqualTo("dataType", dataType.key)
             );
         }
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         switch(direction){
             case ASC:
                 example.orderBy(order).asc();
@@ -91,6 +96,25 @@ public class CourseDataService extends AbstractService {
         }
         return select(courseDataMapper, example, page, size);
     }
+    /**
+     * 获取用户购买记录
+     * @param page
+     * @param size
+     * @param userId
+     * @return
+     */
+    public Page<CourseData> listByUser(int page, int size, Integer userId, Integer structId, DataType dataType, String order, DirectionStatus direction){
+        Page<CourseData> p = page(()->{
+            courseDataRelationMapper.listByUser(userId, structId, dataType != null ? dataType.key : null, order != null ? order : "id", direction != null ? direction.key : "desc");
+        }, page, size);
+
+        Collection ids = Transform.getIds(p, CourseData.class, "id");
+
+        // 获取详细数据
+        List<CourseData> list = select(ids);
+        Transform.replace(p, list, CourseData.class, "id");
+        return p;
+    }
 
     public CourseData add(CourseData courseData){
         int result = insert(courseDataMapper, courseData);

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

@@ -41,7 +41,7 @@ public class CourseExperienceService extends AbstractService {
                     .orLike("content", "%"+keyword+"%")
             );
         }
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         switch(direction){
             case ASC:
                 example.orderBy(order).asc();

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

@@ -4,8 +4,12 @@ import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
+import com.nuliji.tools.mybatis.Example;
+import com.qxgmat.data.constants.enums.module.CourseModule;
+import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.CourseNoMapper;
 import com.qxgmat.data.dao.CoursePackageMapper;
+import com.qxgmat.data.dao.entity.Course;
 import com.qxgmat.data.dao.entity.CourseNo;
 import com.qxgmat.data.dao.entity.CoursePackage;
 import org.slf4j.Logger;
@@ -23,6 +27,30 @@ public class CoursePackageService extends AbstractService {
     @Resource
     private CoursePackageMapper coursePackageMapper;
 
+    public Page<CoursePackage> list(int page, int size, Integer structId, String order, DirectionStatus direction){
+        Example example = new Example(Course.class);
+        if(structId != null){
+            example.and(
+                    example.createCriteria()
+                            .orEqualTo("structId", structId)
+            );
+        }
+        if(order == null || order.isEmpty()) order = "id";
+        if (direction != null){
+            switch(direction){
+                case ASC:
+                    example.orderBy(order).asc();
+                    break;
+                case DESC:
+                default:
+                    example.orderBy(order).desc();
+            }
+        } else {
+            example.orderBy(order).desc();
+        }
+        return select(coursePackageMapper, example, page, size);
+    }
+
     public CoursePackage add(CoursePackage coursePackage){
         int result = insert(coursePackageMapper, coursePackage);
         coursePackage = one(coursePackageMapper, coursePackage.getId());

+ 54 - 2
server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java

@@ -2,10 +2,12 @@ package com.qxgmat.service.inline;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.nuliji.tools.mybatis.Example;
 import com.qxgmat.data.constants.enums.module.CourseModule;
+import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.CourseMapper;
 import com.qxgmat.data.dao.entity.Course;
 import org.slf4j.Logger;
@@ -13,8 +15,8 @@ import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Service
 public class CourseService extends AbstractService {
@@ -53,6 +55,56 @@ public class CourseService extends AbstractService {
         return select(courseMapper, example, page, size);
     }
 
+    public Page<Course> list(int page, int size, CourseModule module, Integer structId, String order, DirectionStatus direction){
+        Example example = new Example(Course.class);
+        if(module != null){
+            example.and(
+                    example.createCriteria()
+                            .andEqualTo("courseModule", module.key)
+            );
+        }
+        if(structId != null){
+            example.and(
+                    example.createCriteria()
+                            .orEqualTo("structId", structId)
+                            .orEqualTo("parentStructId", structId)
+            );
+        }
+        if(order == null || order.isEmpty()) order = "id";
+        if (direction != null){
+            switch(direction){
+                case ASC:
+                    example.orderBy(order).asc();
+                    break;
+                case DESC:
+                default:
+                    example.orderBy(order).desc();
+            }
+        } else {
+            example.orderBy(order).desc();
+        }
+        return select(courseMapper, example, page, size);
+    }
+
+    public Map<Integer, List<Course>> groupByMap(Map<Integer, Integer[]> courseIdsMap){
+        Map<Integer, List<Course>> relationMap = new HashMap<>();
+        List<Integer> ids = new ArrayList<>();
+        for(Integer[] courseIds : courseIdsMap.values()){
+            ids.addAll(Arrays.stream(courseIds).collect(Collectors.toList()));
+        }
+        List<Course> courseList = select(ids);
+        Map courseMap = Transform.getMap(courseList, Course.class, "id");
+        for(Integer k: courseIdsMap.keySet()){
+            List<Course> l = new ArrayList<>();
+            for (Integer courseId : courseIdsMap.get(k)){
+                l.add((Course)courseMap.get(courseId));
+            }
+            relationMap.put(k, l);
+        }
+
+        return relationMap;
+    }
+
     public List<Course> all(CourseModule module){
         Example example = new Example(Course.class);
         example.and(

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

@@ -66,7 +66,7 @@ public class ManagerRoleService extends AbstractService {
 
     public Page<ManagerRole> select(int page, int pageSize, String order, DirectionStatus direction){
         Example example = new Example(ManagerRole.class);
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         switch(direction){
             case ASC:
                 example.orderBy(order).asc();

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

@@ -51,7 +51,7 @@ public class TextbookQuestionService extends AbstractService {
      * @return
      */
     public Page<TextbookQuestion> listAdmin(int page, int pageSize, String questionType, Number paperId, Integer questionNoId, String order, DirectionStatus direction){
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
         }

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

@@ -38,7 +38,7 @@ public class UserAskCourseService extends AbstractService {
 
     public Page<UserAskCourse> listWithCourse(int page, int size, Integer structId, Number courseId, AskStatus status, Integer showStatus, Integer userId, MoneyRange moneyRange, String order, DirectionStatus direction){
         Integer statusIndex = status != null ? status.index : null;
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
         }

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

@@ -39,7 +39,7 @@ public class UserAskQuestionService extends AbstractService {
         Integer statusIndex = status != null ? status.index : null;
         Integer max = moneyRange != null ? moneyRange.max == Integer.MAX_VALUE ? null : moneyRange.max : null;
         Integer min = moneyRange != null ? moneyRange.min : null;
-        if(order.isEmpty()) order = "id";
+        if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
         }