瀏覽代碼

feat(service): 个人中心信息

Go 5 年之前
父節點
當前提交
840a4caff5
共有 58 個文件被更改,包括 2878 次插入292 次删除
  1. 142 1
      front/project/admin/routes/user/order/page.js
  2. 106 0
      front/project/admin/routes/user/orderDetail/page.js
  3. 4 5
      front/project/admin/routes/user/recordAll/page.js
  4. 1 1
      front/project/admin/routes/user/recordBuy/page.js
  5. 4 0
      front/project/admin/stores/user.js
  6. 10 0
      front/project/www/routes/question/detail/index.js
  7. 472 0
      front/project/www/routes/question/detail/index.less
  8. 648 0
      front/project/www/routes/question/detail/page.js
  9. 3 0
      front/project/www/routes/question/index.js
  10. 4 0
      front/project/www/stores/course.js
  11. 75 16
      front/project/www/stores/my.js
  12. 8 0
      front/project/www/stores/question.js
  13. 0 35
      server/data/src/main/java/com/qxgmat/data/dao/entity/Faq.java
  14. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserOrder.java
  15. 2 3
      server/data/src/main/java/com/qxgmat/data/dao/mapping/FaqMapper.xml
  16. 3 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserOrderMapper.xml
  17. 9 0
      server/data/src/main/java/com/qxgmat/data/relation/CourseExperienceRelationMapper.java
  18. 5 0
      server/data/src/main/java/com/qxgmat/data/relation/QuestionRelationMapper.java
  19. 25 0
      server/data/src/main/java/com/qxgmat/data/relation/UserOrderRelationMapper.java
  20. 3 3
      server/data/src/main/java/com/qxgmat/data/relation/UserPaperRelationMapper.java
  21. 2 8
      server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java
  22. 5 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseDataHistoryRelationMapper.xml
  23. 16 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseExperienceRelationMapper.xml
  24. 9 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionRelationMapper.xml
  25. 89 29
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskQuestionRelationMapper.xml
  26. 88 22
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCollectQuestionRelationMapper.xml
  27. 2 1
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCourseRecordRelationMapper.xml
  28. 73 19
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserNoteQuestionRelationMapper.xml
  29. 38 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRelationMapper.xml
  30. 105 32
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserPaperRelationMapper.xml
  31. 97 23
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserQuestionRelationMapper.xml
  32. 12 31
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml
  33. 1 0
      server/data/src/main/resources/mybatis-generator.xml
  34. 40 4
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java
  35. 17 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  36. 96 34
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  37. 67 0
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  38. 37 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/CoursePackageExtendDto.java
  39. 74 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/extend/UserOrderRecordExtendDto.java
  40. 214 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/UserOrderDetailDto.java
  41. 37 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/CommentDto.java
  42. 37 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/FaqDto.java
  43. 17 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCollectExperienceDto.java
  44. 1 1
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCollectDto.java
  45. 1 1
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCollectQuestionDto.java
  46. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderPreDto.java
  47. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserQuestionErrorInfoDto.java
  48. 5 2
      server/gateway-api/src/main/java/com/qxgmat/service/UserCollectExperienceService.java
  49. 5 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserCollectQuestionService.java
  50. 5 5
      server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java
  51. 25 3
      server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java
  52. 2 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java
  53. 20 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/ToolsService.java
  54. 17 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseExperienceService.java
  55. 9 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/QuestionService.java
  56. 10 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java
  57. 25 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderService.java
  58. 0 9
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserReportService.java

+ 142 - 1
front/project/admin/routes/user/order/page.js

@@ -1,10 +1,151 @@
 import React from 'react';
+import { Link } from 'react-router-dom';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+import FilterLayout from '@src/layouts/FilterLayout';
+// import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+import { getMap, formatDate, formatMoney, bindSearch } from '@src/services/Tools';
+import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
+import { ProductTypeMain, RecordBuySource } from '../../../../Constant';
+import { User } from '../../../stores/user';
 
+const ProductTypeMainMap = getMap(ProductTypeMain, 'value', 'label');
+const RecordBuySourceMap = getMap(RecordBuySource, 'value', 'label');
 export default class extends Page {
+  init() {
+    this.itemList = [{
+      key: 'id',
+      type: 'hidden',
+    }, {
+      key: 'transactionNo',
+      type: 'input',
+      name: '请输入支付流水号',
+    }];
+    this.filterF = null;
+    this.filterForm = [{
+      key: 'userId',
+      type: 'select',
+      allowClear: true,
+      name: '用户',
+      select: [],
+      number: true,
+      placeholder: '请输入',
+    }, {
+      key: 'productType',
+      type: 'select',
+      allowClear: true,
+      name: '种类',
+      select: ProductTypeMain,
+    }, {
+      key: 'payMethod',
+      type: 'select',
+      allowClear: true,
+      name: '购买方式',
+      select: RecordBuySource,
+    }, {
+      key: 'orderId',
+      type: 'input',
+      allowClear: true,
+      name: '订单id',
+    }];
+    this.columns = [{
+      title: '订单号',
+      dataIndex: 'id',
+    }, {
+      title: '下单时间',
+      dataIndex: 'createTime',
+      render: (text) => {
+        return text ? formatDate(text) : '';
+      },
+    }, {
+      title: '用户姓名',
+      dataIndex: 'user.nickname',
+    }, {
+      title: '种类',
+      dataIndex: 'productTypes',
+      render: (text) => {
+        return (text || []).map(row => `${ProductTypeMainMap[row]}`).join(', ');
+      },
+    }, {
+      title: '支付金额',
+      dataIndex: 'money',
+      render: (text, record) => {
+        return `${formatMoney(text)}${text !== record.originMoney ? `(${formatMoney(record.originMoney)})` : ''}`;
+      },
+    }, {
+      title: '支付方式',
+      dataIndex: 'payMethod',
+      render: (text) => {
+        return RecordBuySourceMap[text] || '';
+      },
+    }, {
+      title: '支付时间',
+      dataIndex: 'payTime',
+      render: (text) => {
+        return text ? formatDate(text) : '';
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          <Link to={`/user/order/detail/${record.id}`}>查看</Link>
+          {record.payStatus === 0 && (
+            <a onClick={() => {
+              this.finishActionn(record);
+            }}>确认收款</a>
+          )}
+        </div>;
+      },
+    }];
+    bindSearch(this.filterForm, 'userId', this, (search) => {
+      return User.list(search);
+    }, (row) => {
+      return {
+        title: `${row.nickname}(${row.mobile})`,
+        value: row.id,
+      };
+    }, this.state.search.userId ? Number(this.state.search.userId) : null, null);
+  }
+
+  initData() {
+    User.listOrder(Object.assign({}, this.state.search)).then(result => {
+      this.setTableData(result.list, result.total);
+    });
+  }
+
+  finishAction(row) {
+    asyncForm('确认收款', this.itemList, row, data => {
+      return User.finishOrder(data).then(() => {
+        asyncSMessage('支付成功!');
+        this.refresh();
+      });
+    }).then(component => {
+      this.formF = component;
+    });
+  }
+
   renderView() {
-    return <Block flex />;
+    return <Block flex>
+      <FilterLayout
+        show
+        ref={(ref) => { this.filterF = ref; }}
+        itemList={this.filterForm}
+        data={this.state.search}
+        onChange={data => {
+          this.search(data);
+        }} />
+      <TableLayout
+        columns={this.tableSort(this.columns)}
+        list={this.state.list}
+        pagination={this.state.page}
+        loading={this.props.core.loading}
+        onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
+        onSelect={(keys, rows) => this.tableSelect(keys, rows)}
+        selectedKeys={this.state.selectedKeys}
+      />
+    </Block>;
   }
 }

+ 106 - 0
front/project/admin/routes/user/orderDetail/page.js

@@ -1,30 +1,136 @@
 import React from 'react';
 import './index.less';
+import { Form, Row, Col } from 'antd';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+import { getMap, formatDate, formatMoney } from '@src/services/Tools';
+import { RecordBuySource, ServiceKey, ServiceParamMap } from '../../../../Constant';
+import { User } from '../../../stores/user';
 
+const RecordBuySourceMap = getMap(RecordBuySource, 'value', 'label');
+
+const ServiceKeyMap = getMap(ServiceKey, 'value', 'label');
+const ServiceParamRelation = getMap(Object.keys(ServiceParamMap).map(key => {
+  return { map: getMap(ServiceParamMap[key], 'value', 'label'), key };
+}), 'key', 'map');
+const promoteInfoMap = {
+  'textbook-half': '半价机经券',
+};
 export default class extends Page {
+  initData() {
+    const { id } = this.params;
+    let handler;
+    if (id) {
+      handler = User.getOrder({ id });
+    } else {
+      handler = Promise.resolve({});
+    }
+    handler
+      .then(result => {
+        const courseMap = getMap(result.courses, 'id');
+        const dataMap = getMap(result.datas, 'id');
+        const packageMap = getMap(result.packages, 'id');
+        this.setState({ courseMap, dataMap, packageMap, data: result });
+      });
+  }
+
   renderProduct() {
+    const { data = {}, courseMap, dataMap, packageMap } = this.state;
+    const { checkouts = [] } = data;
     return <Block>
       <h1>包含商品</h1>
+      <Form>
+        {(checkouts || []).map(row => {
+          let title = '';
+          switch (row.productType) {
+            case 'course':
+              ({ title } = (courseMap[row.productId] || {}));
+              break;
+            case 'data':
+              ({ title } = (dataMap[row.productId] || {}));
+              break;
+            case 'package':
+              ({ title } = (packageMap[row.productId] || {}));
+              break;
+            case 'service':
+              title = `服务:${ServiceKeyMap[row.service]}${row.param ? (ServiceParamRelation[row.service] || {})[row.param] || '' : ''}`;
+              break;
+            default:
+              break;
+          }
+          return <Row>
+            <Col span={10} offset={2}>
+              {title}{row.number > 1 ? `X ${row.number}` : ''}
+            </Col>
+            <Col span={12}>
+              {`${formatMoney(row.money)}${row.money !== row.originMoney ? `(${formatMoney(row.originMoney)})` : ''}`}
+            </Col>
+          </Row>;
+        })}
+      </Form>
     </Block>;
   }
 
   renderMoney() {
+    const { data = {} } = this.state;
     return <Block>
       <h1>订单金额</h1>
+      <Form>
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='原价'>
+          {formatMoney(data.originMoney)}
+        </Form.Item>
+        {Object.keys(data.promote || {}).map(key => {
+          const info = data.promote[key];
+          return <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='优惠金额'>
+            <Row>
+              <Col span={12} >
+                -{formatMoney(info.originMoney - info.money)}
+              </Col>
+              <Col span={12}>
+                {info.message || promoteInfoMap[`${key}-${info.key}`]}
+              </Col>
+            </Row>
+          </Form.Item>;
+        })}
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='总支付'>
+          {formatMoney(data.money)}
+        </Form.Item>
+      </Form>
     </Block>;
   }
 
   renderGift() {
+    const { data = {} } = this.state;
     return <Block>
       <h1>赠送服务</h1>
+      <Form>
+        {(data.gift || []).map(row => {
+          return <Form.Item >
+            {row.message}
+          </Form.Item>;
+        })}
+      </Form>
     </Block>;
   }
 
   renderTime() {
+    const { data = {} } = this.state;
     return <Block>
       <h1>订单时间</h1>
+      <Form>
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='提交时间'>
+          {data.createTime ? formatDate(data.createTime) : ''}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='支付时间'>
+          {data.payTime ? formatDate(data.payTime) : ''}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='支付方式'>
+          {RecordBuySourceMap[data.payMethod] || ''}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 3 }} wrapperCol={{ span: 16 }} label='付款流水号'>
+          {data.transactionNo}
+        </Form.Item>
+      </Form>
     </Block>;
   }
 

+ 4 - 5
front/project/admin/routes/user/recordAll/page.js

@@ -185,7 +185,7 @@ export default class extends Page {
           return (record.data || {}).title;
         }
         if (record.productType === 'service') {
-          return `${ServiceKeyMap[text]}${(ServiceParamRelation[record.service] || {})[text] || ''}`;
+          return `${ServiceKeyMap[text]}${record.param ? (ServiceParamRelation[record.service] || {})[record.param] || '' : ''}`;
         }
         return '';
       },
@@ -193,7 +193,7 @@ export default class extends Page {
       title: '开通时间',
       dataIndex: 'time',
       render: (text, record) => {
-        return `${record.startTime ? formatDate(record.startTime) : ''} - ${record.endTime ? formatDate(record.endTime) : ''}`;
+        return `${record.startTime ? formatDate(record.startTime) : ''} - ${record.endTime ? formatDate(record.endTime) : ''} `;
       },
     }, {
       title: '开通方式',
@@ -219,13 +219,12 @@ export default class extends Page {
           )}
         </div>;
       },
-    },
-    ];
+    }];
     bindSearch(this.filterForm, 'userId', this, (search) => {
       return User.list(search);
     }, (row) => {
       return {
-        title: `${row.nickname}(${row.mobile})`,
+        title: `${row.nickname} (${row.mobile})`,
         value: row.id,
       };
     }, this.state.search.userId ? Number(this.state.search.userId) : null, null);

+ 1 - 1
front/project/admin/routes/user/recordBuy/page.js

@@ -92,7 +92,7 @@ export default class extends Page {
           return (record.data || {}).title;
         }
         if (record.productType === 'service') {
-          return `${ServiceKeyMap[text]}${(ServiceParamRelation[record.service] || {})[text] || ''}`;
+          return `${ServiceKeyMap[text]}${record.param ? (ServiceParamRelation[record.service] || {})[record.param] || '' : ''}`;
         }
         return '';
       },

+ 4 - 0
front/project/admin/stores/user.js

@@ -125,6 +125,10 @@ export default class UserStore extends BaseStore {
     return this.apiGet('/user/order/list', params);
   }
 
+  finishOrder(params) {
+    return this.apiPut('/user/order/finish', params);
+  }
+
   stopRecord(params) {
     return this.apiPut('/user/record/stop', params);
   }

+ 10 - 0
front/project/www/routes/question/detail/index.js

@@ -0,0 +1,10 @@
+export default {
+  path: '/paper/question/:id',
+  key: 'paper-question',
+  title: '查看结果',
+  needLogin: true,
+  hideHeader: true,
+  component() {
+    return import('./page');
+  },
+};

+ 472 - 0
front/project/www/routes/question/detail/index.less

@@ -0,0 +1,472 @@
+@charset "utf-8";
+
+#paper-question {
+  height: 100%;
+
+  .base {
+    height: 100%;
+
+    .layout {
+      background: #fff;
+      height: 100%;
+      display: flex;
+      flex-direction: row;
+
+      .layout-header {
+        height: 60px;
+        line-height: 60px;
+        margin: 0 50px;
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
+        text-align: center;
+        z-index: 80;
+
+        .left {
+          position: absolute;
+
+          .no {
+            font-size: 20px;
+            display: inline-block;
+            color: #303036;
+            font-size: 20px;
+            margin-right: 25px;
+          }
+
+          .title {
+            color: #A7A7B7;
+            display: inline-block;
+            font-size: 20px;
+
+            img {
+              margin-top: -3px;
+              margin-right: 5px;
+            }
+          }
+        }
+
+
+        .menu-wrap {
+          position: absolute;
+          right: 0;
+          text-align: left;
+          padding: 0 10px;
+          white-space: nowrap;
+
+          .menu-content {
+            position: absolute;
+            background: #fff;
+            text-align: left;
+            top: 50px;
+            right: 10px;
+            border: 1px solid #EAEDF2;
+            padding: 10px 20px;
+            min-width: 150px;
+
+            p {
+              line-height: 30px;
+              height: 30px;
+              text-align: left;
+              margin: 0;
+            }
+          }
+        }
+
+        .center {
+          position: absolute;
+          right: 50%;
+          transform: translateX(100%);
+
+          .icon {
+            margin-left: 20px;
+          }
+        }
+
+        .right {
+          position: absolute;
+          right: 0;
+
+          .b {
+            margin-left: 30px;
+
+            .s {
+              color: #4299FF;
+            }
+          }
+
+          .icon {
+            margin-left: 10px;
+          }
+        }
+      }
+    }
+
+    .layout-footer {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 60px;
+      line-height: 60px;
+      box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
+
+      .left {
+        width: 30%;
+        display: inline-block;
+        padding-left: 50px;
+      }
+
+      .right {
+        width: 30%;
+        display: inline-block;
+        text-align: right;
+        padding-right: 50px;
+
+        .icon {
+          margin-left: 10px;
+        }
+      }
+
+      .center {
+        width: 40%;
+        display: inline-block;
+        text-align: center;
+
+        .item {
+          margin: 0 10px;
+        }
+      }
+    }
+
+    .layout-body {
+      background: #fff;
+      flex: 1;
+      overflow: hidden;
+      margin: 60px 0;
+
+      .layout-content {
+        height: 100%;
+        position: relative;
+
+        .one {
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+        }
+
+        .two {
+          flex: 1;
+          display: flex;
+          flex-direction: row;
+          overflow: hidden;
+          height: 100%;
+        }
+
+        .block {
+          flex: 1;
+        }
+
+        .block-content,
+        .block-answer,
+        .block-awa {
+          padding: 30px 60px;
+          color: #303036;
+          height: 100%;
+          overflow: hidden;
+          overflow-y: auto;
+        }
+
+        .block-content {
+          h2 {
+            padding: 65px 0 20px 0px;
+            font-size: 20px;
+            color: #303036;
+          }
+        }
+
+        .block-awa {
+          background: #EFF3F7;
+
+          h2 {
+            font-size: 20px;
+            color: #303036;
+            margin-top: 37px;
+            margin-bottom: 23px;
+          }
+
+          .detail {
+            .info {
+              font-weight: bold;
+              font-size: 18px;
+              color: #303036;
+
+              span.b {
+                margin-right: 80px;
+
+                .s {
+                  color: #4299FF;
+                }
+              }
+            }
+          }
+
+          .content-awa {
+            padding-top: 50px;
+            color: #686872;
+            font-size: 16px;
+          }
+
+          .show-awa {
+            font-size: 12px;
+            width: 100%;
+            height: 100%;
+            margin: 50% 0;
+            text-align: center;
+            line-height: 20px;
+            color: #A7A7B7;
+          }
+        }
+
+        .block-analysis {
+          background: #EFF3F7;
+          padding: 25px 25px 0 20px;
+          display: flex;
+          flex-direction: column;
+
+          .block-answer {
+            padding: 38px 50px;
+          }
+
+          .block {
+            background: #fff;
+          }
+
+          .detail {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+
+            .detail-block {
+              margin-top: 5px;
+              flex: 1;
+              padding: 30px 50px;
+              overflow: hidden;
+              overflow-y: auto;
+              font-size: 16px;
+              color: #686872;
+            }
+
+            .answer-block {
+              margin-bottom: 5px;
+            }
+          }
+
+          .other {
+            flex: 1;
+            background: #fff;
+            padding: 30px 50px;
+            overflow: hidden;
+            overflow-y: auto;
+            font-size: 16px;
+            color: #686872;
+
+            .other-answer {
+              margin-bottom: 30px;
+            }
+          }
+        }
+
+        .two-analysis {
+          position: absolute;
+          height: 100%;
+          top: 0;
+          left: 0;
+          width: 50%;
+          transition: all 0.3s;
+          transform: translateX(200%);
+        }
+
+        .two-analysis.show {
+          transform: translateX(100%);
+        }
+
+        .fixed-analysis {
+          height: 110px;
+          line-height: 20px;
+          position: absolute;
+          width: 35px;
+          padding: 5px;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          border: 1px solid #E7E7E7;
+          background: #fff;
+          z-index: 9;
+          color: #787883;
+          cursor: pointer;
+          text-align: center;
+        }
+      }
+    }
+
+    .modal {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+
+      >.mask {
+        background: #000;
+        opacity: .2;
+        width: 100%;
+        height: 100%;
+      }
+
+      .body {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        background: #fff;
+        width: 630px;
+        color: #686872;
+        padding: 20px 30px;
+
+        .title {
+          color: #303036;
+          font-size: 20px;
+          font-weight: 600;
+        }
+
+        .desc {
+          color: #686872;
+          font-size: 16px;
+          padding: 20px 0;
+
+          .select-inline {
+            margin-bottom: 15px;
+
+            .select {
+              display: inline-block;
+            }
+          }
+
+          .label {
+            margin-bottom: 5px;
+          }
+        }
+
+        .textarea {
+          width: 570px;
+          height: 80px;
+          background: rgba(247, 247, 247, 1);
+          margin-bottom: 15px;
+          border: none;
+          padding: 5px 10px;
+        }
+
+        .textarea::placeholder {
+          color: #A7A7B7;
+        }
+
+        .bottom {
+          border-top: 1px solid #E1E1E1;
+          padding-top: 10px;
+          text-align: right;
+        }
+      }
+    }
+
+    .modal.ask-ok,
+    .modal.error-ok {
+      .body {
+        .content {
+          width: 100%;
+          padding-top: 20px;
+          padding-bottom: 40px;
+          color: #686872;
+          overflow: hidden;
+
+          .left {
+            float: left;
+            width: 360px;
+            font-size: 18px;
+
+            a {
+              padding-top: 30px;
+              display: inline-block;
+              font-size: 14px;
+            }
+          }
+
+          .right {
+            float: right;
+            text-align: right;
+            font-size: 12px;
+          }
+        }
+
+        .confirm {
+          text-align: center;
+          padding-bottom: 10px;
+
+          .answer-button.lager {
+            font-size: 16px;
+          }
+        }
+      }
+    }
+
+    .modal.note {
+      .body {
+        width: 720px;
+
+        .content {
+          padding-top: 20px;
+
+          .tabs {
+            display: inline-block;
+            width: 170px;
+            vertical-align: top;
+            margin-left: -30px;
+            margin-right: 30px;
+
+            .tab {
+              padding: 5px 0px 5px 40px;
+              line-height: 20px;
+              color: #686872;
+              margin-bottom: 30px;
+              cursor: pointer;
+              transition: all 0.3s;
+              border-top-right-radius: 25px;
+              border-bottom-right-radius: 25px;
+
+              .date {
+                font-size: 12px;
+              }
+            }
+
+            .tab.active,
+            .tab:hover {
+              color: #fff;
+              background: #4299FF;
+            }
+          }
+
+          .input {
+            display: inline-block;
+
+            .textarea {
+              width: 490px;
+              height: 350px;
+              margin-bottom: 20px;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 648 - 0
front/project/www/routes/question/detail/page.js

@@ -0,0 +1,648 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Carousel, Tooltip } from 'antd';
+import { Link } from 'react-router-dom';
+import Fullscreen from 'react-fullscreen-crossbrowser';
+import './index.less';
+import Page from '@src/containers/Page';
+import { formatSeconds, formatPercent, formatDate, sortListWithOrder } from '@src/services/Tools';
+import Assets from '@src/components/Assets';
+import Navigation from '../../../components/Navigation';
+import Tabs from '../../../components/Tabs';
+import Icon from '../../../components/Icon';
+import Switch from '../../../components/Switch';
+import Select from '../../../components/Select';
+import AnswerSelect from '../../../components/AnswerSelect';
+import AnswerList from '../../../components/AnswerList';
+import AnswerButton from '../../../components/AnswerButton';
+import AnswerTable from '../../../components/AnswerTable';
+import OtherAnswer from '../../../components/OtherAnswer';
+import { AskTarget } from '../../../../Constant';
+import { Question } from '../../../stores/question';
+import { My } from '../../../stores/my';
+import Sentence from '../../paper/process/sentence';
+
+export default class extends Page {
+  initState() {
+    return {
+      step: 0,
+      hideAnalysis: true,
+      analysisTab: 'official',
+      showAnswer: false,
+      noteField: AskTarget[0].key,
+      showIds: false,
+
+      // question: {
+      //   content: {
+      //     typeset: 'one',
+      //   },
+      //   // questionType: 'awa',
+      //   answer: {
+      //     subject: [[{ text: 'like', uuid: 'hKyz' }]],
+      //     options: ['parallel'],
+      //   },
+      //   stem: "<p><span uuid='kBJe'>I</span> <span uuid='hKyz'>like</span> <span uuid='fQXh'>book</span></p>",
+      // },
+      // userQuestion: {
+      //   userAnswer: {
+      //     subject: [{ text: 'I', uuid: 'kBJe' }],
+      //     options: ['compare'],
+      //   },
+      //   no: 2,
+      // },
+      // paper: {
+      //   title: '长难句练习',
+      //   questionNumber: 20,
+      // },
+      // report: {
+      //   paperModule: 'sentence',
+      // },
+
+    };
+  }
+
+  initData() {
+    const { id } = this.params;
+    Question.getDetailById(id).then(userQuestion => {
+      const { question, questionNos, paper, note, report, setting } = userQuestion;
+      let { questionNo } = userQuestion;
+      if (!questionNo) ([questionNo] = questionNos);
+      if (!question.answer) question.answer = { questions: [] };
+      if (!question.answerDistributed) question.answerDistributed = { questions: [] };
+      if (!userQuestion.userAnswer) userQuestion.userAnswer = { questions: [] };
+      if ((report.setting || {}).disorder) {
+        const { content } = question;
+        // 还原做题顺序
+        content.questions.forEach((q, i) => {
+          q.select = sortListWithOrder(question.select, setting.questions[i]);
+        });
+        question.answer.questions.forEach((q, i) => {
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
+          });
+        });
+        question.answerDistributed.questions.forEach((q, i) => {
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
+          });
+        });
+        userQuestion.userAnswer.questions.forEach((q, i) => {
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
+          });
+        });
+      }
+      this.setState({ userQuestion, question, questionNo, note, paper, questionNos });
+    });
+  }
+
+  prevQuestion() {
+    const { userQuestion } = this.state;
+    if (userQuestion.no === 1) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no - 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  nextQuestion() {
+    const { userQuestion } = this.state;
+    if (userQuestion.questionNumber === userQuestion.no) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no + 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  submitAsk() {
+    const { question = {}, questionNo = {}, paper = {}, ask = {} } = this.state;
+    if (ask.originContent === '' || ask.content === '' || ask.target === '') return;
+    My.addQuestionAsk(paper.id, ask.target, question.questionModule, questionNo.id, ask.originContent, ask.content).then(() => {
+      this.setState({ askModal: false, askOkModal: true });
+    }).catch(err => {
+      this.setState({ askError: err.message });
+    });
+  }
+
+  submitFeedbackError() {
+    const { feedback = {}, question = {}, questionNo = {} } = this.state;
+    if (feedback.originContent === '' || feedback.content === '' || feedback.target === '') return;
+    My.addFeedbackErrorQuestion(question.questionModule, questionNo.id, questionNo.title, feedback.target, feedback.originContent, feedback.content).then(() => {
+      this.setState({ feedbackModal: false, feedbackOkModal: true });
+    }).catch(err => {
+      this.setState({ feedbackError: err.message });
+    });
+  }
+
+  submitNote(close) {
+    const { question = {}, questionNo = {}, note = {} } = this.state;
+    My.updateQuestionNote(question.questionModule, questionNo.id, note).then(() => {
+      if (close) this.setState({ noteModal: false });
+    }).catch(err => {
+      this.setState({ noteError: err.message });
+    });
+  }
+
+  toggleFullscreen() {
+    const { isFullscreenEnabled } = this.state;
+    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
+  }
+
+  toggleCollect() {
+    const { userQuestion = {}, question = {}, questionNo = {} } = this.state;
+    if (!userQuestion.collect) {
+      My.addQuestionCollect(question.questionModule, questionNo.id).then(() => {
+        userQuestion.collect = true;
+        this.setState({ userQuestion });
+      });
+    } else {
+      My.delQuestionCollect(question.questionModule, questionNo.id).then(() => {
+        userQuestion.collect = false;
+        this.setState({ userQuestion });
+      });
+    }
+  }
+
+  formatStem(text) {
+    if (!text) return '';
+    const { showAnswer, question = { content: {} }, userQuestion } = this.state;
+    const { table = {}, questions = [] } = question.content;
+    text = text.replace(/#select#/g, "<span class='#select#' />");
+    text = text.replace(/#table#/g, "<span class='#table#' />");
+    setTimeout(() => {
+      const selectList = document.getElementsByClassName('#select#');
+      const tableList = document.getElementsByClassName('#table#');
+      for (let i = 0; i < selectList.length; i += 1) {
+        if (!questions[i]) break;
+        ReactDOM.render(
+          <AnswerSelect
+            list={questions[i].select}
+            type={'single'}
+            selected={(userQuestion.userAnswer || { questions: [] }).questions[i]}
+            answer={(question.answer || { questions: [] }).questions[i]}
+            fix
+            show={showAnswer} />,
+          selectList[i],
+        );
+      }
+      if (table.row && table.col && table.header) {
+        const columns = table.header.map((title, index) => {
+          return { title, key: index };
+        });
+        for (let i = 0; i < tableList.length; i += 1) {
+          ReactDOM.render(<AnswerTable list={columns} columns={columns} data={table.data} />, tableList[i]);
+        }
+      }
+    }, 1);
+    return text;
+  }
+
+  renderView() {
+    return (
+      <Fullscreen
+        enabled={this.state.isFullscreenEnabled}
+        onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
+      >
+        {this.renderDetail()}
+      </Fullscreen>
+    );
+  }
+
+  renderDetail() {
+    const { report = {} } = this.state;
+    switch (report.paperModule) {
+      case 'sentence':
+        return <Sentence {...this.state} flow={this} scene='answer' mode='question' />;
+      default:
+        return <div className='base'>{this.renderBase()}</div>;
+    }
+  }
+
+  renderHeader() {
+    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [], question = {} } = this.state;
+    return <div className="layout-header">
+      <div className="left">
+        <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
+        <div className="title"><Assets name='book' />{paper.title}</div>
+      </div>
+      <div className="center">
+        <div className="menu-wrap">
+          ID:{questionNo.title}
+          {questionNos && questionNos.length > 0 && <Icon name="more" onClick={() => {
+            this.setState({ showIds: true });
+          }} />}
+          {showIds && <div className='menu-content'>
+            <p>题源汇总</p>
+            {(questionNos || []).map((row) => <p>ID:{row.title}</p>)}
+          </div>}
+        </div>
+      </div>
+      <div className="right" hidden={question.questionType === 'awa'}>
+        <span className="b" hidden={!userQuestion.id}>
+          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
+        </span>
+        <span className="b">
+          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+          {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
+        </span>
+        <span className="b">
+          <span className="s">{formatPercent(questionNo.totalCorrect, questionNo.totalNumber)}</span>%
+        </span>
+        <Icon name="question" />
+        <Icon name="star" active={userQuestion.collect} onClick={() => this.toggleCollect()} />
+      </div>
+    </div>;
+  }
+
+  renderBase() {
+    const { questionStatus, userQuestion = {}, showIds } = this.state;
+    return <div className="layout" onClick={() => {
+      if (showIds) this.setState({ showIds: false });
+    }}>
+      {this.renderHeader()}
+      <div className="layout-body">{this.renderBody()}</div>
+      <div className="layout-footer">
+        <div className="left">
+          <Tooltip overlayClassName='gray' placement='top' title='全屏'>
+            <a>
+              <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
+            </a>
+          </Tooltip>
+        </div>
+        <div className="center">
+          <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
+          {questionStatus >= 0 && <AnswerButton className="item" onClick={() => {
+            if (questionStatus > 0) {
+              this.setState({ askModal: true });
+            } else {
+              this.setState({ askFailModal: true });
+            }
+          }}>提问</AnswerButton>}
+          <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
+        </div>
+        <div className="right">
+          {userQuestion.no !== 1 && <Icon name="prev" onClick={() => this.prevQuestion()} />}
+          {userQuestion.questionNumber !== userQuestion.no && <Icon name="next" onClick={() => this.nextQuestion()} />}
+        </div>
+      </div>
+      {this.state.askModal && this.renderAsk()}
+      {this.state.askOkModal && this.renderAskOk()}
+      {this.state.askFailModal && this.renderAskFail()}
+      {this.state.feedbackModal && this.renderFeedbackError()}
+      {this.state.feedbackOkModal && this.renderFeedbackErrorOk()}
+      {this.state.noteModal && this.renderNote()}
+    </div>;
+  }
+
+  renderBody() {
+    const { question = { content: {} } } = this.state;
+    const { typeset = 'one' } = question.content;
+    const { hideAnalysis } = this.state;
+    const show = typeset === 'one' ? true : !hideAnalysis;
+    return (
+      <div className="layout-content">
+        <div className='two'>
+          {this.renderContent()}
+          {question.questionType !== 'awa' && this.renderAnswer()}
+          {question.questionType === 'awa' && this.renderAWA()}
+        </div>
+        {question.questionType !== 'awa' && this.renderAnalysis()}
+        {typeset === 'two' && question.questionType !== 'awa' && (
+          <div className="fixed-analysis" onClick={() => this.setState({ hideAnalysis: !hideAnalysis })}>
+            {show ? '收起解析 >' : '查看解析 <'}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  renderAnalysis() {
+    const { question = { content: {} }, analysisTab } = this.state;
+    const { typeset = 'one' } = question.content;
+    const { hideAnalysis } = this.state;
+    const show = typeset === 'one' ? true : !hideAnalysis;
+    return (
+      <div className={`block block-analysis two-analysis ${show ? 'show' : ''}`}>
+        <Tabs
+          type="division"
+          active={analysisTab}
+          space={2}
+          tabs={[
+            { key: 'official', name: '官方解析' },
+            { key: 'qx', name: '千行解析' },
+            { key: 'association', name: '题源联想' },
+            { key: 'qa', name: '相关回答' },
+          ]}
+          onChange={(key) => {
+            this.setState({ analysisTab: key });
+          }}
+        />
+        <div className="detail">
+          {typeset === 'two' && this.renderAnswer()}
+          {this.renderText()}
+        </div>
+      </div>
+    );
+  }
+
+  renderText() {
+    const { analysisTab, question = {}, userQuestion = {} } = this.state;
+    const { asks = [], associations = [] } = userQuestion;
+    let content;
+    switch (analysisTab) {
+      case 'official':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.officialContent }} />;
+        break;
+      case 'qx':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
+        break;
+      case 'association':
+        content = <div className="detail-block">
+          <Carousel>
+            {associations.map(association => {
+              return <div className="text-block" dangerouslySetInnerHTML={{ __html: association.stem }} />;
+            })}
+          </Carousel>
+        </div>;
+        break;
+      case 'qa':
+        content = <div className="detail-block answer-block">
+          {asks.map((ask, index) => {
+            return <OtherAnswer key={index} data={ask} />;
+          })}
+        </div>;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+
+  renderAnswer() {
+    const { question = { content: {} }, showAnswer, userQuestion = {} } = this.state;
+    const { questions = [], type, typeset = 'one' } = question.content;
+    return <div className="block block-answer">
+      {typeset === 'two' ? <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+      {questions.map((item, index) => {
+        return (
+          <div>
+            <div className="text m-b-2">{item.description}</div>
+            <AnswerList
+              show={showAnswer}
+              selected={(userQuestion.userAnswer || { questions: [] }).questions[index]}
+              answer={(question.answer || { questions: [] }).questions[index]}
+              distributed={(question.answerDistributed || { questions: [] }).questions[index]}
+              list={item.select}
+              type={type}
+              first={item.first}
+              second={item.second}
+              direction={item.direction}
+            />
+          </div>
+        );
+      })}
+    </div>;
+  }
+
+  renderContent() {
+    const { question = { content: {} }, showAnswer, step } = this.state;
+    const { typeset = 'one' } = question.content;
+    const { steps = [] } = question.content;
+    return (
+      <div className="block block-content">
+        {typeset === 'one' && question.questionType !== 'awa' ? <Switch checked={showAnswer} onChange={(value) => {
+          this.setState({ showAnswer: value });
+        }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+        {question.questionType === 'awa' && <h2>Analytical Writing Assessment</h2>}
+        {steps.length > 0 && <Navigation theme='detail' list={question.content.steps} active={step} onChange={(v) => this.setState({ step: v })} />}
+        <div className="text" style={{ height: 2000 }} dangerouslySetInnerHTML={{ __html: this.formatStem(steps.length > 0 ? steps[step].stem : question.stem) }} />
+      </div>
+    );
+  }
+
+  renderAWA() {
+    const { showAnswer, userQuestion = { detail: {}, userAnswer: {} } } = this.state;
+    return <div className="block block-awa">
+      <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch>
+      <div className="body">
+        <h2>Your Response</h2>
+        {showAnswer && <div className='detail'>
+          <div className='info'>
+            <span className="b">
+              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+              {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
+            </span>
+            <span className="b">
+              单词数:<span className="s">{Number((userQuestion.detail || {}).words || 0)}</span>词
+            </span>
+          </div>
+          <div className='content-awa' dangerouslySetInnerHTML={{ __html: userQuestion.userAnswer.awa || '' }} />
+        </div>}
+        {!showAnswer && <div className='show-awa'>选择「显示答案」查看自己的作文</div>}
+      </div>
+    </div>;
+  }
+
+  renderAsk() {
+    const { ask = {} } = this.state;
+    return (
+      <div className="modal ask">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="desc">
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={ask.target} list={AskTarget} onChange={(item) => {
+              ask.target = item.value;
+              this.setState({ ask });
+            }} />进行提问</div>
+            <div className="label">有疑问的具体内容是:</div>
+            <textarea className="textarea" value={ask.originContent} placeholder="请复制粘贴有疑问的内容。" onChange={(e) => {
+              ask.originContent = e.target.value;
+              this.setState({ ask });
+            }} />
+            <div className="label">针对以上内容的问题是:</div>
+            <textarea className="textarea" value={ask.content} placeholder="提问频率高的问题会被优先回答哦。" onChange={(e) => {
+              ask.content = e.target.value;
+              this.setState({ ask });
+            }} />
+          </div>
+          <div className="bottom">
+            <AnswerButton theme="cancel" size="lager" onClick={() => this.setState({ askModal: false })}>
+              取消
+            </AnswerButton>
+            <AnswerButton size="lager" onClick={() => this.submitAsk()}>提交</AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderAskOk() {
+    return (
+      <div className="modal ask-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="content">
+            <div className="left">
+              <div className="text">已提交成功!</div>
+              <div className="text">关注公众号,老师回答后会立即收到通知。</div>
+              <div className="text">我们也会通过站内信的方式通知你。</div>
+              <div className="small">成为学员享受极速答疑特权。<Link>了解更多</Link></div>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askOkModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderAskFail() {
+    return (
+      <div className="modal ask-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="content">
+            <div className="left">
+              <div className="text">提问功能正在维护中。</div>
+              <div className="text">可先查阅“相关问答” 或 成为学员享受极速 答疑特权。</div>
+              <Link to="/">了解更多></Link>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askFailModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderFeedbackError() {
+    const { feedback = {} } = this.state;
+    return (
+      <div className="modal error">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">纠错</div>
+          <div className="desc">
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={feedback.target} list={AskTarget} onChange={(item) => {
+              feedback.target = item.value;
+              this.setState({ feedback });
+            }} />进行提问</div>
+            <div className="label">错误内容是:</div>
+            <textarea className="textarea" value={feedback.originContent} placeholder="你可以适当扩大复制范围以使我们准确定位,感谢。" />
+            <div className="label">应该改为:</div>
+            <textarea className="textarea" placeholder="只需提供正确内容即可" />
+          </div>
+          <div className="bottom">
+            <AnswerButton theme="cancel" size="lager" onClick={() => {
+              this.setState({ feedbackModal: false });
+            }}>
+              取消
+            </AnswerButton>
+            <AnswerButton size="lager" onClick={() => {
+              this.submitFeedbackError();
+            }}>提交</AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderFeedbackErrorOk() {
+    return (
+      <div className="modal error-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">纠错</div>
+          <div className="content">
+            <div className="left">
+              <div className="text"><Assets name='right' svg />已提交成功!</div>
+              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
+              <div className="text">您也可以关注公众号及时获取结果。</div>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ feedbackOkModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderNote() {
+    const { noteField, note = {} } = this.state;
+    return (
+      <div className="modal note">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">笔记</div>
+          <div className="content">
+            <div className="tabs">
+              {AskTarget.map(item => {
+                return (
+                  <div className={`tab ${noteField === item.key ? 'active' : ''}`} onClick={() => {
+                    this.setState({ noteField: item.key });
+                  }}>
+                    <div className="text">{item.label}</div>
+                    <div className="date">{note[`${item.key}Time`] ? formatDate(note[`${item.key}Time`]) : ''}</div>
+                  </div>
+                );
+              })}
+            </div>
+            <div className="input">
+              <textarea className="textarea" value={note[`${noteField}Content`] || ''} placeholder="记下笔记,方便以后复习" onChange={(e) => {
+                note[`${noteField}Time`] = new Date();
+                note[`${noteField}Content`] = e.target.value;
+                this.setState({ note });
+              }} />
+              <div className="bottom">
+                <AnswerButton theme="cancel" size="lager" onClick={() => {
+                  this.setState({ noteModal: false });
+                }}>
+                  取消
+                </AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote();
+                }}>编辑</AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote(true);
+                }}>保存</AnswerButton>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}

+ 3 - 0
front/project/www/routes/question/index.js

@@ -0,0 +1,3 @@
+import detail from './detail';
+
+export default [detail];

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

@@ -32,6 +32,10 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/data/detail', { dataId });
   }
 
+  historyData(dataId) {
+    return this.apiGet('/course/data/history', { dataId });
+  }
+
   listExperience({ page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction }) {
     return this.apiGet('/course/experience/list', { page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction });
   }

+ 75 - 16
front/project/www/stores/my.js

@@ -99,7 +99,38 @@ export default class MyStore extends BaseStore {
   }
 
   /**
-   * 添加收藏
+   * 添加心经收藏
+   * @param {*} experienceId
+   */
+  addExperienceCollect(experienceId) {
+    return this.apiPut('/my/collect/experience/add', { experienceId });
+  }
+
+  /**
+   * 删除收藏
+   * @param {*} experienceId
+   */
+  delExperienceCollect(experienceId) {
+    return this.apiDel('/my/collect/experience/delete', { experienceId });
+  }
+
+  /**
+   * 获取收藏心经列表
+   * @param {*} questionModule
+   * @param {*} questionType
+   * @param {*} page
+   * @param {*} size
+   * @param {*} startTime
+   * @param {*} endTime
+   * @param {*} order
+   * @param {*} direction
+   */
+  listExperienceCollect({ page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/collect/question/list', { page, size, startTime, endTime, order, direction });
+  }
+
+  /**
+   * 添加题目收藏
    * @param {*} questionModule
    * @param {*} questionNoId
    */
@@ -108,7 +139,7 @@ export default class MyStore extends BaseStore {
   }
 
   /**
-   * 删除收藏
+   * 删除题目收藏
    * @param {*} questionModule
    * @param {*} questionNoId
    */
@@ -128,8 +159,6 @@ export default class MyStore extends BaseStore {
 
   /**
    * 获取收藏题目列表
-   * @param {*} questionModule
-   * @param {*} questionType
    * @param {*} page
    * @param {*} size
    * @param {*} startTime
@@ -137,8 +166,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  listQuestionCollect({ questionModule, questionType, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/collect/question/list', { questionModule, questionType, page, size, startTime, endTime, order, direction });
+  listQuestionCollect({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/collect/question/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -147,8 +176,8 @@ export default class MyStore extends BaseStore {
    * @param {*} page
    * @param {*} size
    */
-  listError({ questionModule, page, size }) {
-    return this.apiGet('/my/error/list', { questionModule, page, size });
+  listError({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/error/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -178,6 +207,18 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 获取学习数据
+   * @param {*} module
+   * @param {*} subject
+   * @param {*} structIds
+   * @param {*} startTime
+   * @param {*} endTime
+   */
+  getData(module, subject, structIds, startTime, endTime) {
+    return this.apiGet('/my/data', { module, subject, structIds, startTime, endTime });
+  }
+
+  /**
    * 更新题目笔记
    * @param {*} questionModule
    * @param {*} questionNoId
@@ -203,8 +244,6 @@ export default class MyStore extends BaseStore {
 
   /**
    * 获取笔记列表
-   * @param {*} questionModule
-   * @param {*} questionType
    * @param {*} page
    * @param {*} size
    * @param {*} startTime
@@ -212,8 +251,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  questionNoteList({ questionModule, questionType, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/note/question/list', { questionModule, questionType, page, size, startTime, endTime, order, direction });
+  questionNoteList({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/note/question/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -227,8 +266,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  reportList(origin, structId, page, size, startTime, endTime, order, direction) {
-    return this.apiGet('/my/report/list', { origin, structId, page, size, startTime, endTime, order, direction });
+  reportList({ module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/report/list', { module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -251,8 +290,8 @@ export default class MyStore extends BaseStore {
     return this.apiDel('/my/ask/question/delete', { id });
   }
 
-  listQuestionAsk({ page, size }) {
-    return this.apiGet('/my/ask/question/list', { page, size });
+  listQuestionAsk({ module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/ask/question/list', { module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -300,6 +339,26 @@ export default class MyStore extends BaseStore {
   addTextbookFeedback(topicId, target, content) {
     return this.apiPost('/my/feedback/textbook', { topicId, target, content });
   }
+
+  /**
+   * 添加Faq
+   * @param {*} channel
+   * @param {*} position
+   * @param {*} content
+   */
+  addFaq(channel, position, content) {
+    return this.apiPost('/my/faq', { channel, position, content });
+  }
+
+  /**
+   * 添加评论
+   * @param {*} channel
+   * @param {*} position
+   * @param {*} content
+   */
+  addComment(channel, position, content) {
+    return this.apiPost('/my/comment', { channel, position, content });
+  }
 }
 
 export const My = new MyStore({ key: 'my' });

+ 8 - 0
front/project/www/stores/question.js

@@ -22,6 +22,14 @@ export default class QuestionStore extends BaseStore {
   }
 
   /**
+   * 通过题目Id获取详情
+   * @param {*} questionId
+   */
+  getInfoById(questionId) {
+    return this.apiGet('/question/info', { questionId });
+  }
+
+  /**
    * 练习进度
    * @param {*} structId
    */

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

@@ -24,12 +24,6 @@ public class Faq implements Serializable {
     private String email;
 
     /**
-     * 提问者电话
-     */
-    @Column(name = "`phone`")
-    private String phone;
-
-    /**
      * 回复站内信
      */
     @Column(name = "`message`")
@@ -151,24 +145,6 @@ public class Faq implements Serializable {
     }
 
     /**
-     * 获取提问者电话
-     *
-     * @return phone - 提问者电话
-     */
-    public String getPhone() {
-        return phone;
-    }
-
-    /**
-     * 设置提问者电话
-     *
-     * @param phone 提问者电话
-     */
-    public void setPhone(String phone) {
-        this.phone = phone;
-    }
-
-    /**
      * 获取回复站内信
      *
      * @return message - 回复站内信
@@ -389,7 +365,6 @@ public class Faq implements Serializable {
         sb.append(", id=").append(id);
         sb.append(", userId=").append(userId);
         sb.append(", email=").append(email);
-        sb.append(", phone=").append(phone);
         sb.append(", message=").append(message);
         sb.append(", channel=").append(channel);
         sb.append(", position=").append(position);
@@ -446,16 +421,6 @@ public class Faq implements Serializable {
         }
 
         /**
-         * 设置提问者电话
-         *
-         * @param phone 提问者电话
-         */
-        public Builder phone(String phone) {
-            obj.setPhone(phone);
-            return this;
-        }
-
-        /**
          * 设置回复站内信
          *
          * @param message 回复站内信

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

@@ -57,6 +57,12 @@ public class UserOrder implements Serializable {
     private JSONObject promote;
 
     /**
+     * 赠品信息
+     */
+    @Column(name = "`gift`")
+    private JSONArray gift;
+
+    /**
      * 回复时长
      */
     @Column(name = "`ask_time`")
@@ -229,6 +235,24 @@ public class UserOrder implements Serializable {
     }
 
     /**
+     * 获取赠品信息
+     *
+     * @return gift - 赠品信息
+     */
+    public JSONArray getGift() {
+        return gift;
+    }
+
+    /**
+     * 设置赠品信息
+     *
+     * @param gift 赠品信息
+     */
+    public void setGift(JSONArray gift) {
+        this.gift = gift;
+    }
+
+    /**
      * 获取回复时长
      *
      * @return ask_time - 回复时长
@@ -342,6 +366,7 @@ public class UserOrder implements Serializable {
         sb.append(", originMoney=").append(originMoney);
         sb.append(", invoiceMoney=").append(invoiceMoney);
         sb.append(", promote=").append(promote);
+        sb.append(", gift=").append(gift);
         sb.append(", askTime=").append(askTime);
         sb.append(", payId=").append(payId);
         sb.append(", payStatus=").append(payStatus);
@@ -442,6 +467,16 @@ public class UserOrder implements Serializable {
         }
 
         /**
+         * 设置赠品信息
+         *
+         * @param gift 赠品信息
+         */
+        public Builder gift(JSONArray gift) {
+            obj.setGift(gift);
+            return this;
+        }
+
+        /**
          * 设置回复时长
          *
          * @param askTime 回复时长

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

@@ -8,7 +8,6 @@
     <id column="id" jdbcType="INTEGER" property="id" />
     <result column="user_id" jdbcType="INTEGER" property="userId" />
     <result column="email" jdbcType="VARCHAR" property="email" />
-    <result column="phone" jdbcType="VARCHAR" property="phone" />
     <result column="message" jdbcType="INTEGER" property="message" />
     <result column="channel" jdbcType="VARCHAR" property="channel" />
     <result column="position" jdbcType="VARCHAR" property="position" />
@@ -31,8 +30,8 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `user_id`, `email`, `phone`, `message`, `channel`, `position`, `manager_id`, 
-    `is_show`, `is_special`, `answer_status`, `answer_time`, `is_system`, `create_time`
+    `id`, `user_id`, `email`, `message`, `channel`, `position`, `manager_id`, `is_show`, 
+    `is_special`, `answer_status`, `answer_time`, `is_system`, `create_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--

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

@@ -13,6 +13,7 @@
     <result column="origin_money" jdbcType="DECIMAL" property="originMoney" />
     <result column="invoice_money" jdbcType="DECIMAL" property="invoiceMoney" />
     <result column="promote" jdbcType="VARCHAR" property="promote" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler" />
+    <result column="gift" jdbcType="VARCHAR" property="gift" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler" />
     <result column="ask_time" jdbcType="INTEGER" property="askTime" />
     <result column="pay_id" jdbcType="BIGINT" property="payId" />
     <result column="pay_status" jdbcType="INTEGER" property="payStatus" />
@@ -25,6 +26,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `user_id`, `product_types`, `pay_method`, `money`, `origin_money`, `invoice_money`, 
-    `promote`, `ask_time`, `pay_id`, `pay_status`, `create_time`, `pay_time`, `transaction_no`
+    `promote`, `gift`, `ask_time`, `pay_id`, `pay_status`, `create_time`, `pay_time`, 
+    `transaction_no`
   </sql>
 </mapper>

+ 9 - 0
server/data/src/main/java/com/qxgmat/data/relation/CourseExperienceRelationMapper.java

@@ -1,5 +1,6 @@
 package com.qxgmat.data.relation;
 
+import com.qxgmat.data.dao.entity.CourseExperience;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -14,4 +15,12 @@ public interface CourseExperienceRelationMapper {
             @Param("view") int view,
             @Param("collect") int collect
     );
+
+    List<CourseExperience> listWithUser(
+            @Param("userId") Number userId,
+            @Param("startTime") String startTime,
+            @Param("endTime") String endTime,
+            String order,
+            String direction
+    );
 }

+ 5 - 0
server/data/src/main/java/com/qxgmat/data/relation/QuestionRelationMapper.java

@@ -16,4 +16,9 @@ public interface QuestionRelationMapper {
             @Param("time") Integer time,
             @Param("correct") Integer correct
     );
+
+    void accumulationCollect(
+            @Param("id") Number questionId,
+            @Param("collect") int collect
+    );
 }

+ 25 - 0
server/data/src/main/java/com/qxgmat/data/relation/UserOrderRelationMapper.java

@@ -0,0 +1,25 @@
+package com.qxgmat.data.relation;
+
+import com.qxgmat.data.dao.entity.UserReport;
+import com.qxgmat.data.relation.entity.UserRankStatRelation;
+import com.qxgmat.data.relation.entity.UserReportLimitRelation;
+import com.qxgmat.data.relation.entity.UserStudyStatRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Created by gaojie on 2017/11/9.
+ */
+public interface UserOrderRelationMapper {
+
+    List<UserReport> listAdmin(
+            @Param("userId") Integer userId,
+            @Param("productType") String productType,
+            @Param("payMethod") String payMethod,
+            @Param("orderId") Integer orderId,
+            String order,
+            String direction
+    );
+}

+ 3 - 3
server/data/src/main/java/com/qxgmat/data/relation/UserPaperRelationMapper.java

@@ -19,7 +19,8 @@ public interface UserPaperRelationMapper {
             @Param("time") Integer time,
             @Param("correct") Integer correct,
             @Param("times") Integer times,
-            @Param("latestTime") Date latestTime
+            @Param("latestTime") Date latestTime,
+            @Param("latestReportId") Integer latestReportId
     );
 
     List<UserPaper> list(
@@ -35,7 +36,7 @@ public interface UserPaperRelationMapper {
             @Param("userId") Number userId,
             @Param("questionTypes") String[] questionTypes,
             @Param("structIds") Integer[] structIds,
-            @Param("courseModule") String courseModule,
+            @Param("courseModules") String[] courseModules,
             @Param("startTime") String startTime,
             @Param("endTime") String endTime,
             String order,
@@ -44,7 +45,6 @@ public interface UserPaperRelationMapper {
 
     List<UserPaper> listExamination(
             @Param("userId") Number userId,
-            @Param("questionTypes") String[] questionTypes,
             @Param("structIds") Integer[] structIds,
             @Param("libraryId") Integer libraryId,
             @Param("year") String year,

+ 2 - 8
server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java

@@ -36,8 +36,8 @@ public interface UserReportRelationMapper {
     );
 
     List<UserReportLimitRelation> statLimit(
-            @Param("module") String module,
-            @Param("moduleId") Integer moduleId
+            @Param("paperOrigin") String paperOrigin,
+            @Param("originId") Integer originId
     );
 
     List<UserStudyStatRelation> statGroupModule(
@@ -50,12 +50,6 @@ public interface UserReportRelationMapper {
             @Param("endTime") String endTime
     );
 
-    List<UserStudyStatRelation> statGroupPreviewType(
-            @Param("userId") Integer userId,
-            @Param("startTime") String startTime,
-            @Param("endTime") String endTime
-    );
-
     List<UserRankStatRelation> rankExerciseByTime(
         @Param("userId") Integer userId,
         @Param("startTime") String startTime,

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

@@ -14,6 +14,7 @@
     -->
     cd.`id`
   </sql>
+
   <!--
     获取用户购买的资料更新记录
   -->
@@ -21,13 +22,15 @@
     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">
+    left join `user_order_record` uo on `uo`.`product_type`='data' and `uo`.`product_id` = cd.`data_id`
       and `uo`.userId = #{userId,jdbcType=VARCHAR}
     </if>
     <!--and `uo`.create_time &lt; cd.create_time-->
-    where
+    where 1
+    <if test="userId != null">
     `uo`.`id` > 0
+    </if>
     <if test="dataId != null">
       and `cd`.dataId = #{dataId,jdbcType=VARCHAR}
     </if>

+ 16 - 0
server/data/src/main/java/com/qxgmat/data/relation/mapping/CourseExperienceRelationMapper.xml

@@ -25,4 +25,20 @@
     WHERE `id` = #{id, jdbcType=VARCHAR}
   </update>
 
+  <select id="listWithUser" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />, uce.`create_time` as `collect_time`
+    from `course_experience` ce
+    left join `user_collect_experience` uce on uce.`experience_id` = ce.`id` and uce.`user_id` = #{userId,jdbcType=INTEGER}
+    where
+    uce.`id` > 0
+    <if test="startTime != null">
+      and ce.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and ce.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
+    </if>
+    order by ${order} ${direction}
+  </select>
+
 </mapper>

+ 9 - 0
server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionRelationMapper.xml

@@ -24,4 +24,13 @@
     </trim>
     WHERE `id` = #{id, jdbcType=VARCHAR}
   </update>
+
+  <!--累加做题记录-->
+  <update id="accumulationCollect">
+    UPDATE `question`
+    <trim prefix="set" suffixOverrides=",">
+      `collect_number`=`collect_number`+#{collect, jdbcType=INTEGER},
+    </trim>
+    WHERE `id` = #{id, jdbcType=VARCHAR}
+  </update>
 </mapper>

+ 89 - 29
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskQuestionRelationMapper.xml

@@ -11,7 +11,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    ua.`id`
+    uaq.`id`
   </sql>
 
   <!--修改问答排序-->
@@ -26,17 +26,41 @@
   <select id="listExercise" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    from `user_ask_question` uaq
+    left join `question` q on q.`id` = uaq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'sentence')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'exercise'
+      and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    left join `sentence_question` sq on sq.`question_id` = q.`id`
+      and (q.`question_module` = 'sentence')
+    where
+    q.`id` > 0 and uaq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or sq.`id` > 0)
+    </if>
+    <if test="answerStatus != null">
+      and uaq.`answer_status` = #{answerStatus,jdbcType=INTEGER}
+    </if>
+    <if test="startTime != null">
+      and uaq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uaq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>
@@ -44,17 +68,53 @@
   <select id="listExamination" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    from `user_ask_question` uaq
+    left join `question` q on q.`id` = uaq.`question_id`
+      and (q.`question_module` = 'base' or q.`question_module` = 'textbook')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'examination'
+      and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    left join `textbook_question` tq on tq.`question_id` = q.`id`
+    and (q.`question_module` = 'textbook')
+    <if test="libraryId != null">
+      and tq.`library_id` = #{libraryId,jdbcType=VARCHAR}
+    </if>
+    <if test="year != null">
+      and tq.`year` = #{year,jdbcType=VARCHAR}
+    </if>
+    where
+    q.`id` > 0 and uaq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or tq.`id` > 0)
+    </if>
+    <if test="library != null">
+      and (tq.`id` > 0)
+    </if>
+    <if test="year != null">
+      and (tq.`id` > 0)
+    </if>
+    <if test="answerStatus != null">
+      and uaq.`answer_status` = #{answerStatus,jdbcType=INTEGER}
+    </if>
+    <if test="startTime != null">
+      and uaq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uaq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>
@@ -63,10 +123,10 @@
   <select id="listWithUser" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    from `user_ask_question` ua
+    from `user_ask_question` uaq
     left join `user` u on u.`id` = ua.`user_id`
       <if test="userId != null">
-        and ua.`user_id` = #{userId,jdbcType=VARCHAR}
+        and uaq.`user_id` = #{userId,jdbcType=VARCHAR}
       </if>
       <if test="max != null">
         and u.`total_money` &lt; ${max}
@@ -78,32 +138,32 @@
     where
     u.`id` &gt; 0
     <if test="questionNoId != null">
-      and ua.`question_no_id` = #{questionNoId,jdbcType=VARCHAR}
+      and uaq.`question_no_id` = #{questionNoId,jdbcType=VARCHAR}
     </if>
     <if test="target != null">
-      and ua.`target` = #{target,jdbcType=VARCHAR}
+      and uaq.`target` = #{target,jdbcType=VARCHAR}
     </if>
     <if test="answerStatus != null">
-      and ua.`answer_status` = #{answerStatus,jdbcType=INTEGER}
+      and uaq.`answer_status` = #{answerStatus,jdbcType=INTEGER}
     </if>
     <if test="showStatus != null">
-      and ua.`show_status` = #{showStatus,jdbcType=INTEGER}
+      and uaq.`show_status` = #{showStatus,jdbcType=INTEGER}
     </if>
     <if test="askModule != null">
-      and ua.`ask_module` =#{askModule,jdbcType=VARCHAR}
+      and uaq.`ask_module` =#{askModule,jdbcType=VARCHAR}
     </if>
     <if test="questionModule != null">
-      and ua.`question_module` =#{questionModule,jdbcType=VARCHAR}
+      and uaq.`question_module` =#{questionModule,jdbcType=VARCHAR}
     </if>
     <if test="questionType != null">
       and q.`question_type` =#{questionType,jdbcType=VARCHAR}
     </if>
     <if test="hasRecord != null">
       <if test="hasRecord">
-        and ua.`record_id` &gt; 0
+        and uaq.`record_id` &gt; 0
       </if>
       <if test="!hasRecord">
-        and ua.`record_id` = 0
+        and uaq.`record_id` = 0
       </if>
     </if>
     order by ${order} ${direction}

+ 88 - 22
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCollectQuestionRelationMapper.xml

@@ -15,38 +15,104 @@
   </sql>
 
   <select id="listExercise" resultMap="IdMap">
-    select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    select max(ucq.`id`) as `id`,
+    max(uq.`create_time`) as `latest_time`,
+    sum(uq.`user_correct`) / sum(uq.`user_number`) as `correct`,
+    sum(uq.`user_time`) / sum(uq.`user_number`) as `time`,
+    uq.`question_type` as `question_type`,
+    if (qn.`title` = null, sq.`title`, qn.`title`) as `title`,
+    from `user_collect_question` ucq
+    left join `user_question` uq on uq.`question_id` = ucq.`question_id` and uq.`user_id`=#{userId,jdbcType=VARCHAR}
+    left join `question` q on q.`id` = uq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'sentence')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'exercise'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    left join `sentence_question` sq on sq.`question_id` = q.`id`
+      and (q.`question_module` = 'sentence')
+    where
+    q.`id` > 0
+    <if test="structIds != null">
+      and qn.`id` > 0
     </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or sq.`id` > 0)
+    </if>
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
+    </if>
+    group by ucq.`question_module`, ucq.`question_no_id`
     order by ${order} ${direction}
   </select>
 
   <select id="listExamination" resultMap="IdMap">
-    select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    select max(ucq.`id`) as `id`,
+    max(uq.`create_time`) as `latest_time`,
+    sum(uq.`user_correct`) / sum(uq.`user_number`) as `correct`,
+    sum(uq.`user_time`) / sum(uq.`user_number`) as `time`,
+    uq.`question_type` as `question_type`,
+    if (qn.`title` = null, tq.`title`, qn.`title`) as `title`,
+    from `user_collect_question` ucq
+    left join `user_question` uq on uq.`question_id` = ucq.`question_id` and uq.`user_id`=#{userId,jdbcType=VARCHAR}
+    left join `question` q on q.`id` = uq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'textbook')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
+    </if>
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'examination'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
+    </if>
+    left join `textbook_question` tq on tq.`question_id` = q.`id`
+      and (q.`question_module` = 'textbook')
+      <if test="libraryId != null">
+        and tq.`library_id` = #{libraryId,jdbcType=VARCHAR}
+      </if>
+      <if test="year != null">
+        and tq.`year` = #{year,jdbcType=VARCHAR}
+      </if>
+    where
+    q.`id` > 0 and uq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or tq.`id` > 0)
+    </if>
+    <if test="library != null">
+      and (tq.`id` > 0)
+    </if>
+    <if test="year != null">
+      and (tq.`id` > 0)
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
+    group by ucq.`question_module`, ucq.`question_no_id`
     order by ${order} ${direction}
   </select>
 

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

@@ -40,7 +40,8 @@
   -->
   <select id="listLast" resultMap="IdMap">
     select
-    SUBSTRING_INDEX(GROUP_CONCAT(ucr.`id` ORDER BY ucr.`create_time` desc),',',1) as `id`
+    <!--SUBSTRING_INDEX(GROUP_CONCAT(ucr.`id` ORDER BY ucr.`create_time` desc),',',1) as `id`-->
+    max(ucr.`id`) as `id`
     from `user_course_record` ucr
     where
     ucr.`record_id` IN

+ 73 - 19
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserNoteQuestionRelationMapper.xml

@@ -11,23 +11,44 @@
     <!--
       WARNING - @mbg.generated
     -->
-    up.`id`
+    unq.`id`
   </sql>
 
   <select id="listExercise" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    from `user_note_question` unq
+    left join `question` q on q.`id` = unq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'sentence')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'exercise'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    left join `sentence_question` sq on sq.`question_id` = q.`id`
+    and (q.`question_module` = 'sentence')
+    where
+    q.`id` > 0 and unq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or sq.`id` > 0)
+    </if>
+    <if test="startTime != null">
+      and unq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and unq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>
@@ -35,17 +56,50 @@
   <select id="listExamination" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    from `user_note_question` unq
+    left join `question` q on q.`id` = unq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'textbook')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
+    </if>
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'examination'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
+    </if>
+    left join `textbook_question` tq on tq.`question_id` = q.`id`
+    and (q.`question_module` = 'textbook')
+    <if test="libraryId != null">
+      and tq.`library_id` = #{libraryId,jdbcType=VARCHAR}
+    </if>
+    <if test="year != null">
+      and tq.`year` = #{year,jdbcType=VARCHAR}
+    </if>
+    where
+    q.`id` > 0 and unq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or tq.`id` > 0)
+    </if>
+    <if test="library != null">
+      and (tq.`id` > 0)
+    </if>
+    <if test="year != null">
+      and (tq.`id` > 0)
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    <if test="startTime != null">
+      and unq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    <if test="endTime != null">
+      and unq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>

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

@@ -0,0 +1,38 @@
+<?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.UserOrderRelationMapper">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.UserOrder">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <id column="id" jdbcType="INTEGER" property="id" />
+  </resultMap>
+
+  <sql id="Id_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    uo.`id`
+  </sql>
+
+  <select id="listAdmin" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />
+    from `user_order` uo
+    where 1
+    <if test="userId != null">
+      and uo.`user_id` = #{userId,jdbcType=VARCHAR}
+    </if>
+    <if test="productType != null">
+      and find_in_set(#{productType,jdbcType=VARCHAR}, replace(replace(trim(TRAILING ']' from trim(LEADING '[' from uo.`product_types`)), '"', ''), ' ', ''))
+    </if>
+    <if test="payMethod != null">
+      and uo.`pay_method` = #{payMethod,jdbcType=VARCHAR}
+    </if>
+    <if test="orderId != null">
+      and uo.`id` = #{orderId,jdbcType=VARCHAR}
+    </if>
+    order by ${order} ${direction}
+  </select>
+
+</mapper>

+ 105 - 32
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserPaperRelationMapper.xml

@@ -23,60 +23,133 @@
       `total_correct`=`total_correct`+#{correct, jdbcType=INTEGER},
       `times` = `times`+#{times, jdbcType=INTEGER},
       `latest_time` = #{latestTime, jdbcType=DATE},
+      `latest_report_id` = #{latestReportId, jdbcType=INTEGER},
     </trim>
     WHERE `id` = #{id, jdbcType=VARCHAR}
   </update>
 
   <select id="list" resultMap="IdMap">
     select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    up.`id`, ur.`user_correct` / up.`question_number` as `correct`, up.`user_time` / up.`question_number` as `time`
+    from `user_paper` up
+    left join `user_report` ur on ur.`id`= up.`latest_report_id`
+    where up.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="paperOrigin != null">
+    and up.`paper_origin` = #{paperOrigin,jdbcType=VARCHAR}
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>
 
   <select id="listExercise" resultMap="IdMap">
     select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    up.`id`, ur.`user_correct` / up.`question_number` as `correct`, up.`user_time` / up.`question_number` as `time`
+    from `user_paper` up
+    left join `user_report` ur on ur.`id`= up.`latest_report_id`
+    left join `exercise_paper` ep on up.`paper_origin` = 'exercise' and ep.`id` = up.`origin_id`
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        ep.`question_type` = #{item}
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and (ep.`struct_four` in
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=",">
+        #{item}
+      </foreach>
+      or ep.`struct_three` in
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=",">
+        #{item}
+      </foreach>
+      )
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    left join `sentence_paper` sp on up.`paper_origin` = 'sentence' and sp.`id` = up.`origin_id`
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        'sentence' = #{item}
+      </foreach>
+    </if>
+    left join `preview_assign` pa on up.`paper_origin` = 'preview' and pa.`id` = up.`origin_id`
+    <if test="courseModules != null">
+      and pa.`course_module` in
+      <foreach collection="courseModules" item="item" index="index" open="(" close=")" separator=",">
+        #{item}
+      </foreach>
+    </if>
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        pa.`question_type` = #{item}
+      </foreach>
+    </if>
+    where up.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and ep.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (ep.`id` > 0 or sp.`id` > 0 or pa.`id` > 0)
+    </if>
+    <if test="courseModules != null">
+      and (pa.`id` > 0)
+    </if>
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>
 
   <select id="listExamination" resultMap="IdMap">
     select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
-    </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
-    </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    up.`id`, ur.`user_correct` / up.`question_number` as `correct`, up.`user_time` / up.`question_number` as `time`
+    from `user_paper` up
+    left join `user_report` ur on ur.`id`= up.`latest_report_id`
+    left join `examination_paper` ep on up.`paper_origin` = 'examination' and ep.`id` = up.`origin_id`
+    <if test="structIds != null">
+      and (ep.`struct_two` in
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=",">
+        #{item}
+      </foreach>
+       or ep.`struct_three` in
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=",">
+        #{item}
+      </foreach>
+      )
+    </if>
+    left join `textbook_paper` tp on up.`paper_origin` = 'textbook' and tp.`id` = up.`origin_id`
+    <if test="libraryId != null">
+      and tp.`library_id` = #{libraryId,jdbcType=VARCHAR}
+    </if>
+    <if test="year != null">
+      and tp.`year` = #{year,jdbcType=VARCHAR}
+    </if>
+    where up.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and ep.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (ep.`id` > 0 or tp.`id` > 0)
+    </if>
+    <if test="library != null">
+      and (tp.`id` > 0)
+    </if>
+    <if test="year != null">
+      and (tp.`id` > 0)
+    </if>
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
     order by ${order} ${direction}
   </select>

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

@@ -17,42 +17,116 @@
     <!--
       WARNING - @mbg.generated
     -->
-    ur.`id`
+    uq.`id`
   </sql>
 
   <select id="listExerciseError" resultMap="IdMap">
-    select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    select max(uq.`id`) as `id`,
+    max(uq.`create_time`) as `latest_time`,
+    sum(uq.`user_correct`) / sum(uq.`user_number`) as `correct`,
+    sum(uq.`user_time`) / sum(uq.`user_number`) as `time`,
+    uq.`question_type` as `question_type`,
+    if (qn.`title` = null, sq.`title`, qn.`title`) as `title`,
+    from `user_question` uq
+    left join `question` q on q.`id` = uq.`question_id`
+      and (q.`question_module` = 'base' or q.`question_module` = 'sentence')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
+    </if>
+    left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'exercise'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    left join `sentence_question` sq on sq.`question_id` = q.`id`
+      and (q.`question_module` = 'sentence')
+    left join `user_paper_question` upq on  on upq.`user_id` = #{userId,jdbcType=VARCHAR}
+      and upq.`question_id` = q.`id`
+      and upq.`question_module` = uq.`question_module`
+      and upq.`question_no_id` = uq.`question_no_id`
+      and upq.`question_origin` = 'remove_error'
+    where
+    q.`id` > 0 and upq.`id` = null and uq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    <if test="structIds == null">
+      and (qn.`id` > 0 or sq.`id` > 0)
     </if>
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
+    </if>
+    group by uq.`question_module`, uq.`question_no_id`
     order by ${order} ${direction}
   </select>
 
   <select id="listExaminationError" resultMap="IdMap">
-    select
-    <include refid="Id_Column_List" />
-    from `preview_paper` pp
-    left join `course` c on c.`id` = pp.`course_id`
-    <if test="structId != null">
-      and (c.`struct_id` = #{structId,jdbcType=VARCHAR} or c.`parent_struct_id` = #{structId, jdbcType=VARCHAR} )
+    select max(uq.`id`) as `id`,
+    max(uq.`create_time`) as `latest_time`,
+    sum(uq.`user_correct`) / sum(uq.`user_number`) as `correct`,
+    sum(uq.`user_time`) / sum(uq.`user_number`) as `time`,
+    uq.`question_type` as `question_type`,
+    if (qn.`title` = null, tq.`title`, qn.`title`) as `title`,
+    from `user_question` uq
+    left join `question` q on q.`id` = uq.`question_id`
+    and (q.`question_module` = 'base' or q.`question_module` = 'textbook')
+    <if test="questionTypes != null">
+      and
+      <foreach collection="questionTypes" item="item" index="index" open="(" close=")" separator=" or ">
+        q.`question_type` = #{item}
+      </foreach>
+    </if>
+      left join `question_no` qn on qn.`question_id` = q.`id` and qn.`module` = 'examination'
+    and (q.`question_module` = 'base')
+    <if test="structIds != null">
+      and
+      <foreach collection="structIds" item="item" index="index" open="(" close=")" separator=" or ">
+        find_in_set(#{item}, qn.`module_struct`)
+      </foreach>
+    </if>
+    left join `textbook_question` tq on tq.`question_id` = q.`id`
+      and (q.`question_module` = 'textbook')
+      <if test="libraryId != null">
+        and tq.`library_id` = #{libraryId,jdbcType=VARCHAR}
+      </if>
+      <if test="year != null">
+        and tq.`year` = #{year,jdbcType=VARCHAR}
+      </if>
+    left join `user_paper_question` upq on upq.`user_id` = #{userId,jdbcType=VARCHAR}
+      and upq.`question_id` = q.`id`
+      and upq.`question_module` = uq.`question_module`
+      and upq.`question_no_id` = uq.`question_no_id`
+      and upq.`question_origin` = 'remove_error'
+    where
+    q.`id` > 0 and upq.`id` = null and uq.`user_id` = #{userId,jdbcType=VARCHAR}
+    <if test="structIds != null">
+      and qn.`id` > 0
+    </if>
+    <if test="structIds == null">
+      and (qn.`id` > 0 or tq.`id` > 0)
+    </if>
+    <if test="library != null">
+      and (tq.`id` > 0)
     </if>
-    where c.`id` > 0
-    <if test="courseModule != null">
-      and pp.`course_module` = #{courseModule,jdbcType=VARCHAR}
+    <if test="year != null">
+      and (tq.`id` > 0)
     </if>
-    <if test="questionType != null">
-      and pp.`question_type` = #{questionType,jdbcType=VARCHAR}
+    <if test="startTime != null">
+      and uq.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+    </if>
+    <if test="endTime != null">
+      and uq.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
+    group by uq.`question_module`, uq.`question_no_id`
     order by ${order} ${direction}
   </select>
 

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

@@ -77,7 +77,8 @@
   -->
   <select id="listLast" resultMap="IdMap">
     select
-    SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`
+    <!--SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`-->
+    max(ur.`id`) as `id`
     from `user_report` ur
     left join `user_paper` up on up.`id` = ur.`paper_id` and up.`is_reset` = 0
     where
@@ -102,7 +103,8 @@
   -->
   <select id="listLastNoReset" resultMap="IdMap">
     select
-    SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`
+    <!--SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`-->
+    max(ur.`id`) as `id`
     from `user_report` ur
     left join `user_paper` up on up.`id` = ur.`paper_id`
     where
@@ -131,8 +133,8 @@
     where
       `user_time` &lt; `time`
     and `user_number` = `question_number`
-    and `paper_origin` = #{module,jdbcType=VARCHAR}
-    and `module_id` = #{module_id,jdbcType=INTEGER}
+    and `paper_origin` = #{paper_origin,jdbcType=VARCHAR}
+    and `origin_id` = #{origin_id,jdbcType=INTEGER}
   </select>
 
   <!--
@@ -169,27 +171,6 @@
   </select>
 
   <!--
-    用户预习作业模块记录统计,分题型
-  -->
-  <select id="statGroupPreviewType" resultMap="studyMap">
-    select
-    sum(ur.`user_number`) as `user_number`, sum(ur.`user_time`) as `user_time`, sum(ur.`user_correct`) as `user_correct`, ep.`question_type` as `module`
-    from `user_report` ur
-    left join `preview_paper` pp on pp.`id` = ur.`module_id`
-    where
-    ur.`user_id` = #{userId,jdbcType=VARCHAR}
-    and ur.`paper_origin` = 'preview'
-    and pp.`id` &gt; 0
-    <if test="startTime != null">
-      and ur.`update_time` &gt; #{startTime,jdbcType=VARCHAR}
-    </if>
-    <if test="endTime != null">
-      and ur.`update_time` &lt; #{endTime,jdbcType=VARCHAR}
-    </if>
-    group pp.`question_type`
-  </select>
-
-  <!--
     用户练习记录排名
   -->
   <select id="rankExerciseByTime" resultMap="studyMap">
@@ -200,7 +181,7 @@
     select sum(ur.`user_time`) as `user_time`, ur.`user_id` as `user_id`
     from `user_report` ur
     where
-
+      ur.`paper_module` = 'exercise'
     <if test="startTime != null">
       and ur.`update_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
@@ -220,17 +201,17 @@
     from (
     select aaa.`user_id` as `user_id`, (@rank:=@rank+1) as `rank`
     from (
-    select sum(ucr.`user_time`) as `user_time`, ucr.`user_id` as `user_id`
-    from `user_course_record` ucr
+    select sum(ur.`user_time`) as `user_time`, ur.`user_id` as `user_id`
+    from `user_report` ur
     where
-    1
+      ur.`paper_module` = 'examination'
     <if test="startTime != null">
-      and ucr.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
+      and ur.`create_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
     <if test="endTime != null">
       and ur.`create_time` &lt; #{endTime,jdbcType=VARCHAR}
     </if>
-    group by ucr.`user_id`
+    group by ur.`user_id`
     ) as aaa, (select @rank:=0) as bbb  order by `user_time` desc
     ) as ccc where ccc.`user_id` = #{userId,jdbcType=VARCHAR}
   </select>

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

@@ -173,6 +173,7 @@
             <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
             <columnOverride column="product_types" javaType="com.alibaba.fastjson.JSONArray" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler"/>
             <columnOverride column="promote" javaType="com.alibaba.fastjson.JSONObject" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonObjectHandler"/>
+            <columnOverride column="gift" javaType="com.alibaba.fastjson.JSONArray" jdbcType="VARCHAR" typeHandler="com.nuliji.tools.mybatis.handler.JsonArrayHandler"/>
         </table>
     </context>
 </generatorConfiguration>

+ 40 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java

@@ -69,6 +69,9 @@ public class UserController {
     private CourseDataService courseDataService;
 
     @Autowired
+    private CoursePackageService coursePackageService;
+
+    @Autowired
     private QuestionService questionService;
 
     @Autowired
@@ -123,6 +126,9 @@ public class UserController {
     private UserOrderRecordService userOrderRecordService;
 
     @Autowired
+    private UserOrderCheckoutService userOrderCheckoutService;
+
+    @Autowired
     private QuestionFlowService questionFlowService;
 
     @Autowired
@@ -282,10 +288,37 @@ public class UserController {
 
     @RequestMapping(value = "/order/detail", method = RequestMethod.GET)
     @ApiOperation(value = "用户订单列表", httpMethod = "GET")
-    public Response<UserOrder> getOrder(@RequestParam int id,
+    public Response<UserOrderDetailDto> getOrder(@RequestParam int id,
             HttpSession session) {
         UserOrder entity = userOrderService.get(id);
-        return ResponseHelp.success(entity);
+        UserOrderDetailDto dto = Transform.convert(entity, UserOrderDetailDto.class);
+
+        List<UserOrderRecordExtendDto> list;
+        if(entity.getPayStatus() == 0){
+            List<UserOrderCheckout> checkoutList = userOrderCheckoutService.allByUser(entity.getUserId(), entity.getId());
+            list = Transform.convert(checkoutList, UserOrderRecordExtendDto.class);
+        }else{
+            List<UserOrderRecord> recordList = userOrderRecordService.allByUser(entity.getUserId(), entity.getId());
+            list = Transform.convert(recordList, UserOrderRecordExtendDto.class);
+        }
+        dto.setCheckouts(list);
+
+        List<UserOrderRecordExtendDto> courseCheckout = list.stream().filter((row)->row.getProductType().equals(ProductType.COURSE.key)).collect(Collectors.toList());
+        Collection courseIds = Transform.getIds(courseCheckout, UserOrderRecordExtendDto.class, "productId");
+        List<Course> courseList = courseService.select(courseIds);
+        dto.setCourses(Transform.convert(courseList, CourseExtendDto.class));
+
+        List<UserOrderRecordExtendDto> dataCheckout = list.stream().filter((row)->row.getProductType().equals(ProductType.DATA.key)).collect(Collectors.toList());
+        Collection dataIds = Transform.getIds(dataCheckout, UserOrderRecordExtendDto.class, "productId");
+        List<CourseData> dataList = courseDataService.select(dataIds);
+        dto.setDatas(Transform.convert(dataList, CourseDataExtendDto.class));
+
+        List<UserOrderRecordExtendDto> packageCheckout = list.stream().filter((row)->row.getProductType().equals(ProductType.COURSE_PACKAGE.key)).collect(Collectors.toList());
+        Collection packageIds = Transform.getIds(packageCheckout, UserOrderRecordExtendDto.class, "productId");
+        List<CoursePackage> packageList = coursePackageService.select(packageIds);
+        dto.setPackages(Transform.convert(packageList, CoursePackageExtendDto.class));
+
+        return ResponseHelp.success(dto);
     }
 
     @RequestMapping(value = "/order/finish", method = RequestMethod.PUT)
@@ -302,14 +335,17 @@ public class UserController {
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) String productType,
+            @RequestParam(required = false) String payMethod,
+            @RequestParam(required = false) Integer orderId,
             @RequestParam(required = false) String order,
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session) {
-        Page<UserOrder> p = userOrderService.select(page, size);
+        Page<UserOrder> p = userOrderService.listAdmin(page, size, userId, productType, payMethod, orderId, order, DirectionStatus.ValueOf(direction));
         List<UserOrderListDto> pr = Transform.convert(p, UserOrderListDto.class);
 
         // 绑定用户
-        Collection userIds = Transform.getIds(p, UserOrderRecord.class, "userId");
+        Collection userIds = Transform.getIds(p, UserOrder.class, "userId");
         List<User> userList = usersService.select(userIds);
         Transform.combine(pr, userList, UserOrderListDto.class, "userId", "user", User.class, "id", UserExtendDto.class);
 

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

@@ -43,6 +43,9 @@ public class CourseController {
     private CourseDataService courseDataService;
 
     @Autowired
+    private CourseDataHistoryService courseDataHistoryService;
+
+    @Autowired
     private CoursePackageService coursePackageService;
 
     @Autowired
@@ -191,6 +194,20 @@ public class CourseController {
         return ResponseHelp.success(courseData);
     }
 
+    @RequestMapping(value = "/data/history", method = RequestMethod.GET)
+    @ApiOperation(value = "资料详情", httpMethod = "GET")
+    public Response<PageMessage<CourseDataHistory>> historyData(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = true) Integer dataId
+    ) {
+        User user = (User) shiroHelp.getLoginUser();
+
+        Page<CourseDataHistory> p = courseDataHistoryService.listByData(page, size, dataId);
+
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/teacher/list", method = RequestMethod.GET)
     @ApiOperation(value = "资料列表", httpMethod = "GET")
     public Response<List<CourseTeacher>> listTeacher(

+ 96 - 34
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -19,6 +19,9 @@ import com.qxgmat.data.inline.UserQuestionStat;
 import com.qxgmat.data.relation.entity.*;
 import com.qxgmat.dto.extend.*;
 import com.qxgmat.dto.request.*;
+import com.qxgmat.dto.request.CommentDto;
+import com.qxgmat.dto.request.FaqDto;
+import com.qxgmat.dto.request.UserCollectQuestionDto;
 import com.qxgmat.dto.request.UserNoteQuestionDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.AiHelp;
@@ -110,9 +113,18 @@ public class MyController {
     private CourseDataService courseDataService;
 
     @Autowired
+    private CourseExperienceService courseExperienceService;
+
+    @Autowired
     private CourseDataHistoryService courseDataHistoryService;
 
     @Autowired
+    private FaqService faqService;
+
+    @Autowired
+    private CommentService commentService;
+
+    @Autowired
     private UsersService usersService;
 
     @Autowired
@@ -131,6 +143,9 @@ public class MyController {
     private UserCollectQuestionService userCollectQuestionService;
 
     @Autowired
+    private UserCollectExperienceService userCollectExperienceService;
+
+    @Autowired
     private UserNoteQuestionService userNoteQuestionService;
 
     @Autowired
@@ -708,9 +723,45 @@ public class MyController {
         return ResponseHelp.success(dtoMap);
     }
 
+    @RequestMapping(value = "/collect/experience/add", method = RequestMethod.PUT)
+    @ApiOperation(value = "添加心经收藏", notes = "添加心经收藏", httpMethod = "PUT")
+    public Response<Boolean> addExperienceCollect(@RequestBody @Validated UserCollectExperienceDto dto)  {
+        UserCollectExperience entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        entity.setUserId(user.getId());
+        userCollectExperienceService.addExperience(entity);
+
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/collect/experience/delete", method = RequestMethod.DELETE)
+    @ApiOperation(value = "移除心经收藏", notes = "移除心经收藏", httpMethod = "DELETE")
+    public Response<Boolean> deleteExperienceCollect(Integer experienceId)  {
+        User user = (User) shiroHelp.getLoginUser();
+        Boolean result = userCollectExperienceService.deleteExperience(user.getId(), experienceId);
+
+        return ResponseHelp.success(result);
+    }
+
+    @RequestMapping(value = "/collect/experience/list", method = RequestMethod.GET)
+    @ApiOperation(value = "获取收藏题目列表", notes = "获取收藏题目列表", httpMethod = "GET")
+    public Response<PageMessage<CourseExperience>> listExperienceCollect(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime,
+            @RequestParam(required = false, defaultValue = "id") String order, // collect_time, update_time
+            @RequestParam(required = false, defaultValue = "desc") String direction,
+            HttpSession session)  {
+        User user = (User) shiroHelp.getLoginUser();
+        Page<CourseExperience> p = courseExperienceService.listWithUser(page, size, user.getId(), startTime, endTime, order, DirectionStatus.ValueOf(direction));
+
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/collect/question/add", method = RequestMethod.PUT)
-    @ApiOperation(value = "添加收藏", notes = "添加收藏", httpMethod = "PUT")
-    public Response<Boolean> addQuestionCollect(@RequestBody @Validated UserCollectDto dto)  {
+    @ApiOperation(value = "添加题目收藏", notes = "添加题目收藏", httpMethod = "PUT")
+    public Response<Boolean> addQuestionCollect(@RequestBody @Validated UserCollectQuestionDto dto)  {
         UserCollectQuestion entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
         switch (QuestionModule.ValueOf(dto.getQuestionModule())){
@@ -740,7 +791,7 @@ public class MyController {
     }
 
     @RequestMapping(value = "/collect/question/delete", method = RequestMethod.DELETE)
-    @ApiOperation(value = "移除收藏", notes = "移除收藏", httpMethod = "DELETE")
+    @ApiOperation(value = "移除题目收藏", notes = "移除题目收藏", httpMethod = "DELETE")
     public Response<Boolean> deleteQuestionCollect(String questionModule, Integer questionNoId)  {
         User user = (User) shiroHelp.getLoginUser();
         Boolean result = userCollectQuestionService.deleteQuestion(user.getId(), QuestionModule.ValueOf(questionModule), questionNoId);
@@ -749,7 +800,7 @@ public class MyController {
     }
 
     @RequestMapping(value = "/collect/question/bind", method = RequestMethod.POST)
-    @ApiOperation(value = "收藏组卷", notes = "收藏组卷", httpMethod = "POST")
+    @ApiOperation(value = "收藏题目组卷", notes = "收藏题目组卷", httpMethod = "POST")
     public Response<UserPaper> bindQuestionCollect(@RequestBody @Validated UserCustomBindDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
 
@@ -766,7 +817,7 @@ public class MyController {
 
     @RequestMapping(value = "/collect/question/list", method = RequestMethod.GET)
     @ApiOperation(value = "获取收藏题目列表", notes = "获取收藏题目列表", httpMethod = "GET")
-    public Response<PageMessage<UserCollectQuestionDto>> listQuestionCollect(
+    public Response<PageMessage<UserCollectQuestionInfoDto>> listQuestionCollect(
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = false) String module,
@@ -776,7 +827,7 @@ public class MyController {
             @RequestParam(required = false) String endTime,
             @RequestParam(required = false) Boolean latest,
             @RequestParam(required = false) String year,
-            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "id") String order, // title, time, correct, question_type, latest_time
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session)  {
         User user = (User) shiroHelp.getLoginUser();
@@ -797,32 +848,32 @@ public class MyController {
         }else{
             throw new ParameterException("参数逻辑错误");
         }
-        List<UserCollectQuestionDto> pr = Transform.convert(p, UserCollectQuestionDto.class);
+        List<UserCollectQuestionInfoDto> pr = Transform.convert(p, UserCollectQuestionInfoDto.class);
 
         // 获取题目信息
-        Collection questionIds = Transform.getIds(pr, UserCollectQuestionDto.class, "questionId");
+        Collection questionIds = Transform.getIds(pr, UserCollectQuestionInfoDto.class, "questionId");
         List<Question> questionList = questionService.select(questionIds);
-        Transform.combine(pr, questionList, UserCollectQuestionDto.class, "questionId", "question", Question.class, "id", QuestionExtendDto.class);
+        Transform.combine(pr, questionList, UserCollectQuestionInfoDto.class, "questionId", "question", Question.class, "id", QuestionExtendDto.class);
 
-        List<UserCollectQuestionDto> basePr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.BASE.key)).collect(Collectors.toList());
-        Collection baseQuestionNoIds = Transform.getIds(basePr, UserCollectQuestionDto.class, "questionNoId");
+        List<UserCollectQuestionInfoDto> basePr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.BASE.key)).collect(Collectors.toList());
+        Collection baseQuestionNoIds = Transform.getIds(basePr, UserCollectQuestionInfoDto.class, "questionNoId");
         List<QuestionNo> baseQuestionNoList = questionNoService.select(baseQuestionNoIds);
-        Transform.combine(basePr, baseQuestionNoList, UserCollectQuestionDto.class, "questionNoId", "questionNo", QuestionNo.class, "id", QuestionNoExtendDto.class);
+        Transform.combine(basePr, baseQuestionNoList, UserCollectQuestionInfoDto.class, "questionNoId", "questionNo", QuestionNo.class, "id", QuestionNoExtendDto.class);
 
-        List<UserCollectQuestionDto> sentencePr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.SENTENCE.key)).collect(Collectors.toList());
-        Collection sentenceQuestionNoIds = Transform.getIds(sentencePr, UserCollectQuestionDto.class, "questionNoId");
+        List<UserCollectQuestionInfoDto> sentencePr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.SENTENCE.key)).collect(Collectors.toList());
+        Collection sentenceQuestionNoIds = Transform.getIds(sentencePr, UserCollectQuestionInfoDto.class, "questionNoId");
         List<SentenceQuestion> sentenceQuestionList = sentenceQuestionService.select(sentenceQuestionNoIds);
-        Transform.combine(sentencePr, sentenceQuestionList, UserCollectQuestionDto.class, "questionNoId", "questionNo", SentenceQuestion.class, "id", QuestionNoExtendDto.class);
+        Transform.combine(sentencePr, sentenceQuestionList, UserCollectQuestionInfoDto.class, "questionNoId", "questionNo", SentenceQuestion.class, "id", QuestionNoExtendDto.class);
 
-        List<UserCollectQuestionDto> textbookPr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.TEXTBOOK.key)).collect(Collectors.toList());
-        Collection textbookQuestionNoIds = Transform.getIds(textbookPr, UserCollectQuestionDto.class, "questionNoId");
+        List<UserCollectQuestionInfoDto> textbookPr = pr.stream().filter((row)->row.getQuestionModule().equals(QuestionModule.TEXTBOOK.key)).collect(Collectors.toList());
+        Collection textbookQuestionNoIds = Transform.getIds(textbookPr, UserCollectQuestionInfoDto.class, "questionNoId");
         List<TextbookQuestion> textbookQuestionList = textbookQuestionService.select(textbookQuestionNoIds);
-        Transform.combine(textbookPr, textbookQuestionList, UserCollectQuestionDto.class, "questionNoId", "questionNo", TextbookQuestion.class, "id", QuestionNoExtendDto.class);
+        Transform.combine(textbookPr, textbookQuestionList, UserCollectQuestionInfoDto.class, "questionNoId", "questionNo", TextbookQuestion.class, "id", QuestionNoExtendDto.class);
 
         // 绑定题目统计
         List<UserQuestion> userQuestionList = userQuestionService.listByQuestion(user.getId(), questionIds);
         Map<Object, UserQuestionStat> stats = userQuestionService.statQuestionMap(userQuestionList);
-        Transform.combine(pr, stats, UserCollectQuestionDto.class, "questionId", "stat");
+        Transform.combine(pr, stats, UserCollectQuestionInfoDto.class, "questionId", "stat");
 
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
@@ -839,7 +890,7 @@ public class MyController {
             @RequestParam(required = false) String endTime,
             @RequestParam(required = false) Boolean latest,
             @RequestParam(required = false) String year,
-            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "id") String order, // title, time, correct, question_type, latest_time
             @RequestParam(required = false, defaultValue = "desc") String direction
     )  {
         User user = (User) shiroHelp.getLoginUser();
@@ -882,6 +933,7 @@ public class MyController {
         List<TextbookQuestion> textbookQuestionList = textbookQuestionService.select(textbookQuestionNoIds);
         Transform.combine(textbookPr, textbookQuestionList, UserQuestionErrorInfoDto.class, "questionNoId", "questionNo", TextbookQuestion.class, "id", QuestionNoExtendDto.class);
 
+        // 最近做题,及做题统计
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 
@@ -971,7 +1023,7 @@ public class MyController {
             @RequestParam(required = false) String endTime,
             @RequestParam(required = false) Boolean latest,
             @RequestParam(required = false) String year,
-            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "id") String order, // update_time
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session)  {
         User user = (User) shiroHelp.getLoginUser();
@@ -1041,8 +1093,8 @@ public class MyController {
             @RequestParam(required = false) String endTime,
             @RequestParam(required = false) Boolean latest,
             @RequestParam(required = false) String year,
-            @RequestParam(required = false) String courseModule,
-            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false) String[] courseModules,
+            @RequestParam(required = false, defaultValue = "id") String order, // title, latest_time,correct,time
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session)  {
         User user = (User) shiroHelp.getLoginUser();
@@ -1052,7 +1104,7 @@ public class MyController {
         if (questionNoModule != null && (paperOrigin == PaperOrigin.COLLECT || paperOrigin == PaperOrigin.ERROR)){
             p = userPaperService.list(page, size, user.getId(), paperOrigin, startTime, endTime, order, DirectionStatus.ValueOf(direction));
         }else if(questionNoModule == QuestionNoModule.EXERCISE){
-            p = userPaperService.listExercise(page, size, user.getId(), questionTypes, structIds, courseModule, startTime, endTime, order, DirectionStatus.ValueOf(direction));
+            p = userPaperService.listExercise(page, size, user.getId(), questionTypes, structIds, courseModules, startTime, endTime, order, DirectionStatus.ValueOf(direction));
         }else if (questionNoModule == QuestionNoModule.EXAMINATION){
             Integer libraryId = null;
             if (latest != null){
@@ -1063,7 +1115,7 @@ public class MyController {
                     year = null;
                 }
             }
-            p = userPaperService.listExamination(page, size, user.getId(), questionTypes, structIds, libraryId, year, order, startTime, endTime, DirectionStatus.ValueOf(direction));
+            p = userPaperService.listExamination(page, size, user.getId(), structIds, libraryId, year, order, startTime, endTime, DirectionStatus.ValueOf(direction));
         }else{
             throw new ParameterException("参数逻辑错误");
         }
@@ -1195,7 +1247,7 @@ public class MyController {
             @RequestParam(required = false) Integer askStatus,
             @RequestParam(required = false) Boolean latest,
             @RequestParam(required = false) String year,
-            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "id") String order, // create_time, answer_time
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session)  {
         User user = (User) shiroHelp.getLoginUser();
@@ -1320,16 +1372,26 @@ public class MyController {
         return ResponseHelp.success(true);
     }
 
-    @RequestMapping(value = "/feedback/textbook", method = RequestMethod.POST)
-    @ApiOperation(value = "添加机经反馈", notes = "添加机经反馈", httpMethod = "POST")
-    public Response<Boolean> addFeedbackTextbook(@RequestBody @Validated UserTextbookFeedbackDto dto)  {
-        UserTextbookFeedback entity = Transform.dtoToEntity(dto);
+    @RequestMapping(value = "/faq", method = RequestMethod.POST)
+    @ApiOperation(value = "添加faq", notes = "添加faq", httpMethod = "POST")
+    public Response<Boolean> addFaq(@RequestBody @Validated FaqDto dto)  {
+        Faq entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
-        TextbookTopic topic = textbookTopicService.get(dto.getTopicId());
         entity.setUserId(user.getId());
-        entity.setStatus(0);
-        entity.setLibraryId(topic.getLibraryId());
-        userTextbookFeedbackService.add(entity);
+        entity.setMessage(1);
+        entity.setEmail(user.getEmail());
+        faqService.add(entity);
+
+        return ResponseHelp.success(true);
+    }
+
+    @RequestMapping(value = "/comment", method = RequestMethod.POST)
+    @ApiOperation(value = "添加评论", notes = "添加评论", httpMethod = "POST")
+    public Response<Boolean> addComment(@RequestBody @Validated CommentDto dto)  {
+        Comment entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        entity.setUserId(user.getId());
+        commentService.add(entity);
 
         return ResponseHelp.success(true);
     }

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

@@ -115,6 +115,71 @@ public class QuestionController {
     @Autowired
     private CourseExtendService courseExtendService;
 
+    @RequestMapping(value = "/info", method = RequestMethod.GET)
+    @ApiOperation(value = "获取题目信息", notes = "获取题目信息", httpMethod = "GET")
+    public Response<UserQuestionDetailDto> info(
+            @RequestParam(required = true) Integer questionId,
+            @RequestParam(required = true) Integer questionNoId
+    )  {
+        User user = (User) shiroHelp.getLoginUser();
+        UserQuestion userQuestion = UserQuestion.builder()
+                .questionId(questionId)
+                .questionNoId(questionNoId)
+                .questionModule(QuestionModule.BASE.key)
+                .build();
+
+        UserQuestionDetailDto dto = Transform.convert(userQuestion, UserQuestionDetailDto.class);
+
+        Question question = questionService.get(userQuestion.getQuestionId());
+        dto.setQuestion(Transform.convert(question, QuestionDetailExtendDto.class));
+
+        UserCollectQuestion collect = userCollectQuestionService.getByUserAndQuestion(user.getId(), userQuestion.getQuestionId());
+        dto.setCollect(collect != null);
+
+        UserNoteQuestion userNoteQuestion = userNoteQuestionService.getByUserAndQuestion(user.getId(), userQuestion.getQuestionId());
+        dto.setNote(Transform.convert(userNoteQuestion, UserNoteQuestionExtendDto.class));
+
+        List<UserAskQuestion> userAskQuestionList = userAskQuestionService.listByQuestion(userQuestion.getQuestionId(), true);
+        dto.setAsks(Transform.convert(userAskQuestionList, UserAskQuestionExtendDto.class));
+
+        if (question.getAssociationContent() != null){
+            List<QuestionNoRelation> associations = questionNoService.listWithRelationByIds(question.getAssociationContent());
+            Collection questions = Transform.getIds(associations, QuestionNoRelation.class, "question");
+            dto.setAssociations(Transform.convert(questions, QuestionBaseExtendDto.class));
+        }
+
+        List<QuestionNo> questionNoList = questionNoService.listByQuestion(userQuestion.getQuestionId());
+        dto.setQuestionNos(Transform.convert(questionNoList, QuestionNoExtendDto.class));
+        QuestionNo questionNo = questionNoService.get(userQuestion.getQuestionNoId());
+        dto.setQuestionNo(Transform.convert(questionNo, QuestionNoExtendDto.class));
+
+        // 获取提问权限
+        Integer recordId = questionFlowService.questionRelationCourse(user.getId(), null, QuestionType.ValueOf(question.getQuestionType()));
+        if (recordId != null){
+            dto.setQuestionStatus(1);
+        }else{
+            Integer id = questionNo.getModuleStruct()[questionNo.getModuleStruct().length - 1];
+            QuestionNoModule questionNoModule = QuestionNoModule.ValueOf(questionNo.getModule());
+            // 获取基本当前权限
+            switch(questionNoModule){
+                case EXAMINATION:
+                    ExaminationStruct examinationStruct = examinationStructService.get(id);
+                    dto.setQuestionStatus(examinationStruct.getQuestionStatus());
+                    break;
+                case EXERCISE:
+                    ExerciseStruct exerciseStruct = exerciseStructService.get(id);
+                    dto.setQuestionStatus(exerciseStruct.getQuestionStatus());
+                    break;
+                default:
+                    // 自由组卷,不提供提问显示
+                    dto.setQuestionStatus(-1);
+            }
+        }
+
+
+        return ResponseHelp.success(dto);
+    }
+
     @RequestMapping(value = "/exercise/progress", method = RequestMethod.GET)
     @ApiOperation(value = "练习进度", httpMethod = "GET")
     public Response<List<UserExerciseGroupDto>> exerciseProgress(
@@ -488,6 +553,8 @@ public class QuestionController {
             case BASE:
                 List<QuestionNo> questionNoList = questionNoService.listByQuestion(userQuestion.getQuestionId());
                 dto.setQuestionNos(Transform.convert(questionNoList, QuestionNoExtendDto.class));
+                QuestionNo questionNo = questionNoService.get(userQuestion.getQuestionNoId());
+                dto.setQuestionNo(Transform.convert(questionNo, QuestionNoExtendDto.class));
                 break;
             case SENTENCE:
                 SentenceQuestion sentenceQuestion = sentenceQuestionService.get(userQuestion.getQuestionNoId());

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

@@ -0,0 +1,37 @@
+package com.qxgmat.dto.admin.extend;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CoursePackage;
+
+@Dto(entity = CoursePackage.class)
+public class CoursePackageExtendDto {
+    private Integer id;
+
+    private Integer structId;
+
+    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;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+}

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

@@ -0,0 +1,74 @@
+package com.qxgmat.dto.admin.extend;
+
+
+public class UserOrderRecordExtendDto {
+    private String productType;
+
+    private String productId;
+
+    private String service;
+
+    private String param;
+
+    private String number;
+
+    private String originMoney;
+
+    private String money;
+
+    public String getProductType() {
+        return productType;
+    }
+
+    public void setProductType(String productType) {
+        this.productType = productType;
+    }
+
+    public String getProductId() {
+        return productId;
+    }
+
+    public void setProductId(String productId) {
+        this.productId = productId;
+    }
+
+    public String getService() {
+        return service;
+    }
+
+    public void setService(String service) {
+        this.service = service;
+    }
+
+    public String getParam() {
+        return param;
+    }
+
+    public void setParam(String param) {
+        this.param = param;
+    }
+
+    public String getNumber() {
+        return number;
+    }
+
+    public void setNumber(String number) {
+        this.number = number;
+    }
+
+    public String getOriginMoney() {
+        return originMoney;
+    }
+
+    public void setOriginMoney(String originMoney) {
+        this.originMoney = originMoney;
+    }
+
+    public String getMoney() {
+        return money;
+    }
+
+    public void setMoney(String money) {
+        this.money = money;
+    }
+}

+ 214 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/UserOrderDetailDto.java

@@ -0,0 +1,214 @@
+package com.qxgmat.dto.admin.response;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.UserOrder;
+import com.qxgmat.dto.admin.extend.*;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Dto(entity = UserOrder.class)
+public class UserOrderDetailDto {
+    private Integer id;
+
+    private Integer userId;
+
+    private UserExtendDto user;
+
+    private JSONArray productTypes;
+
+    private String payMethod;
+
+    private BigDecimal money;
+
+    private BigDecimal originMoney;
+
+    private BigDecimal invoiceMoney;
+
+    private JSONObject promote;
+
+    private JSONArray gift;
+
+    private Integer askTime;
+
+    private Long payId;
+
+    private Integer payStatus;
+
+    private Date createTime;
+
+    private Date payTime;
+
+    private String transactionNo;
+
+    private List<UserOrderRecordExtendDto> checkouts;
+
+    private List<CourseExtendDto> courses;
+
+    private List<CourseDataExtendDto> datas;
+
+    private List<CoursePackageExtendDto> packages;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Integer userId) {
+        this.userId = userId;
+    }
+
+    public UserExtendDto getUser() {
+        return user;
+    }
+
+    public void setUser(UserExtendDto user) {
+        this.user = user;
+    }
+
+    public JSONArray getProductTypes() {
+        return productTypes;
+    }
+
+    public void setProductTypes(JSONArray productTypes) {
+        this.productTypes = productTypes;
+    }
+
+    public String getPayMethod() {
+        return payMethod;
+    }
+
+    public void setPayMethod(String payMethod) {
+        this.payMethod = payMethod;
+    }
+
+    public BigDecimal getMoney() {
+        return money;
+    }
+
+    public void setMoney(BigDecimal money) {
+        this.money = money;
+    }
+
+    public BigDecimal getOriginMoney() {
+        return originMoney;
+    }
+
+    public void setOriginMoney(BigDecimal originMoney) {
+        this.originMoney = originMoney;
+    }
+
+    public BigDecimal getInvoiceMoney() {
+        return invoiceMoney;
+    }
+
+    public void setInvoiceMoney(BigDecimal invoiceMoney) {
+        this.invoiceMoney = invoiceMoney;
+    }
+
+    public JSONObject getPromote() {
+        return promote;
+    }
+
+    public void setPromote(JSONObject promote) {
+        this.promote = promote;
+    }
+
+    public Integer getAskTime() {
+        return askTime;
+    }
+
+    public void setAskTime(Integer askTime) {
+        this.askTime = askTime;
+    }
+
+    public Long getPayId() {
+        return payId;
+    }
+
+    public void setPayId(Long payId) {
+        this.payId = payId;
+    }
+
+    public Integer getPayStatus() {
+        return payStatus;
+    }
+
+    public void setPayStatus(Integer payStatus) {
+        this.payStatus = payStatus;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    public Date getPayTime() {
+        return payTime;
+    }
+
+    public void setPayTime(Date payTime) {
+        this.payTime = payTime;
+    }
+
+    public String getTransactionNo() {
+        return transactionNo;
+    }
+
+    public void setTransactionNo(String transactionNo) {
+        this.transactionNo = transactionNo;
+    }
+
+    public List<UserOrderRecordExtendDto> getCheckouts() {
+        return checkouts;
+    }
+
+    public void setCheckouts(List<UserOrderRecordExtendDto> checkouts) {
+        this.checkouts = checkouts;
+    }
+
+    public List<CourseExtendDto> getCourses() {
+        return courses;
+    }
+
+    public void setCourses(List<CourseExtendDto> courses) {
+        this.courses = courses;
+    }
+
+    public List<CourseDataExtendDto> getDatas() {
+        return datas;
+    }
+
+    public void setDatas(List<CourseDataExtendDto> datas) {
+        this.datas = datas;
+    }
+
+    public List<CoursePackageExtendDto> getPackages() {
+        return packages;
+    }
+
+    public void setPackages(List<CoursePackageExtendDto> packages) {
+        this.packages = packages;
+    }
+
+    public JSONArray getGift() {
+        return gift;
+    }
+
+    public void setGift(JSONArray gift) {
+        this.gift = gift;
+    }
+}

+ 37 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/CommentDto.java

@@ -0,0 +1,37 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.Comment;
+
+@Dto(entity = Comment.class)
+public class CommentDto {
+    private String channel;
+
+    private String position;
+
+    private String content;
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public String getChannel() {
+        return channel;
+    }
+
+    public void setChannel(String channel) {
+        this.channel = channel;
+    }
+
+    public String getPosition() {
+        return position;
+    }
+
+    public void setPosition(String position) {
+        this.position = position;
+    }
+}

+ 37 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/FaqDto.java

@@ -0,0 +1,37 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.Faq;
+
+@Dto(entity = Faq.class)
+public class FaqDto {
+    private String channel;
+
+    private String position;
+
+    private String content;
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public String getChannel() {
+        return channel;
+    }
+
+    public void setChannel(String channel) {
+        this.channel = channel;
+    }
+
+    public String getPosition() {
+        return position;
+    }
+
+    public void setPosition(String position) {
+        this.position = position;
+    }
+}

+ 17 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCollectExperienceDto.java

@@ -0,0 +1,17 @@
+package com.qxgmat.dto.request;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.UserCollectExperience;
+
+@Dto(entity = UserCollectExperience.class)
+public class UserCollectExperienceDto {
+    private Integer experienceId;
+
+    public Integer getExperienceId() {
+        return experienceId;
+    }
+
+    public void setExperienceId(Integer experienceId) {
+        this.experienceId = experienceId;
+    }
+}

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

@@ -4,7 +4,7 @@ import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserCollectQuestion;
 
 @Dto(entity = UserCollectQuestion.class)
-public class UserCollectDto {
+public class UserCollectQuestionDto {
 
     private String questionModule;
 

+ 1 - 1
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCollectQuestionDto.java

@@ -9,7 +9,7 @@ import com.qxgmat.dto.extend.QuestionNoExtendDto;
 import java.util.Date;
 
 @Dto(entity = UserCollectQuestion.class)
-public class UserCollectQuestionDto {
+public class UserCollectQuestionInfoDto {
     private Integer id;
 
     private String questionModule;

+ 11 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserOrderPreDto.java

@@ -1,5 +1,6 @@
 package com.qxgmat.dto.response;
 
+import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.qxgmat.data.dao.entity.UserOrderCheckout;
 import com.qxgmat.dto.extend.CourseDataExtendDto;
@@ -16,6 +17,8 @@ public class UserOrderPreDto {
 
     private JSONObject promote;
 
+    private JSONArray gift;
+
     private List<UserOrderCheckout> checkouts;
 
     private List<CourseExtendDto> courses;
@@ -79,4 +82,12 @@ public class UserOrderPreDto {
     public void setPackages(List<CoursePackageExtendDto> packages) {
         this.packages = packages;
     }
+
+    public JSONArray getGift() {
+        return gift;
+    }
+
+    public void setGift(JSONArray gift) {
+        this.gift = gift;
+    }
 }

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

@@ -23,6 +23,8 @@ public class UserQuestionErrorInfoDto {
 
     private Date createTime;
 
+    private Date latestTime;
+
     public Integer getId() {
         return id;
     }
@@ -78,4 +80,12 @@ public class UserQuestionErrorInfoDto {
     public void setQuestionModule(String questionModule) {
         this.questionModule = questionModule;
     }
+
+    public Date getLatestTime() {
+        return latestTime;
+    }
+
+    public void setLatestTime(Date latestTime) {
+        this.latestTime = latestTime;
+    }
 }

+ 5 - 2
server/gateway-api/src/main/java/com/qxgmat/service/UserCollectExperienceService.java

@@ -16,6 +16,8 @@ import com.qxgmat.data.dao.entity.UserCollectExperience;
 import com.qxgmat.data.dao.entity.UserCollectQuestion;
 import com.qxgmat.data.relation.UserCollectQuestionRelationMapper;
 import com.qxgmat.data.relation.entity.UserCollectQuestionRelation;
+import com.qxgmat.service.inline.CourseExperienceService;
+import com.qxgmat.service.inline.QuestionService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -33,8 +35,7 @@ public class UserCollectExperienceService extends AbstractService {
     private UserCollectExperienceMapper userCollectExperienceMapper;
 
     @Resource
-    private UserCollectQuestionRelationMapper userCollectQuestionRelationMapper;
-
+    private CourseExperienceService courseExperienceService;
 
     /**
      * 查询用户收藏情况
@@ -87,6 +88,7 @@ public class UserCollectExperienceService extends AbstractService {
             return in;
         }
         int result = insert(userCollectExperienceMapper, entity);
+        courseExperienceService.accumulation(entity.getExpericenceId(), 0, 1);
         return entity;
     }
 
@@ -109,6 +111,7 @@ public class UserCollectExperienceService extends AbstractService {
             return true;
         }
         int result = delete(userCollectExperienceMapper, example);
+        courseExperienceService.accumulation(in.getExpericenceId(), 0, -1);
         return result > 0;
     }
 

+ 5 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UserCollectQuestionService.java

@@ -32,6 +32,9 @@ public class UserCollectQuestionService extends AbstractService {
     private static final Logger logger = LoggerFactory.getLogger(UserCollectQuestionService.class);
 
     @Resource
+    private QuestionService questionService;
+
+    @Resource
     private UserCollectQuestionMapper userCollectQuestionMapper;
 
     @Resource
@@ -141,6 +144,7 @@ public class UserCollectQuestionService extends AbstractService {
             return in;
         }
         int result = insert(userCollectQuestionMapper, entity);
+        questionService.accumulationCollect(entity, 1);
         return entity;
     }
 
@@ -164,6 +168,7 @@ public class UserCollectQuestionService extends AbstractService {
             return true;
         }
         int result = delete(userCollectQuestionMapper, example);
+        questionService.accumulationCollect(in, -1);
         return result > 0;
     }
 

+ 5 - 5
server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java

@@ -92,7 +92,7 @@ public class UserPaperService extends AbstractService {
      * @param direction
      * @return
      */
-    public Page<UserPaper> listExercise(int page, int size, Integer userId, String[] questionTypes, Integer[] structIds, String courseModule, String startTime, String endTime, String order, DirectionStatus direction){
+    public Page<UserPaper> listExercise(int page, int size, Integer userId, String[] questionTypes, Integer[] structIds, String[] courseModules, String startTime, String endTime, String order, DirectionStatus direction){
         if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
@@ -100,7 +100,7 @@ public class UserPaperService extends AbstractService {
         String finalOrder = order;
         DirectionStatus finalDirection = direction;
         Page<UserPaper> p = page(()->{
-            userPaperRelationMapper.listExercise(userId, questionTypes, structIds, courseModule, startTime, endTime, finalOrder, finalDirection.key);
+            userPaperRelationMapper.listExercise(userId, questionTypes, structIds, courseModules, startTime, endTime, finalOrder, finalDirection.key);
         }, page, size);
 
         Collection ids = Transform.getIds(p, UserPaper.class, "id");
@@ -122,7 +122,7 @@ public class UserPaperService extends AbstractService {
      * @param direction
      * @return
      */
-    public Page<UserPaper> listExamination(int page, int size, Integer userId, String[] questionTypes, Integer[] structIds, Integer libraryId, String year, String startTime, String endTime, String order, DirectionStatus direction){
+    public Page<UserPaper> listExamination(int page, int size, Integer userId, Integer[] structIds, Integer libraryId, String year, String startTime, String endTime, String order, DirectionStatus direction){
         if(order == null || order.isEmpty()) order = "id";
         if (direction == null){
             direction = DirectionStatus.DESC;
@@ -130,7 +130,7 @@ public class UserPaperService extends AbstractService {
         String finalOrder = order;
         DirectionStatus finalDirection = direction;
         Page<UserPaper> p = page(()->{
-            userPaperRelationMapper.listExamination(userId, questionTypes, structIds, libraryId, year, startTime, endTime, finalOrder, finalDirection.key);
+            userPaperRelationMapper.listExamination(userId, structIds, libraryId, year, startTime, endTime, finalOrder, finalDirection.key);
         }, page, size);
 
         Collection ids = Transform.getIds(p, UserPaper.class, "id");
@@ -308,7 +308,7 @@ public class UserPaperService extends AbstractService {
      * @param report
      */
     public void accumulation(UserReport report){
-        userPaperRelationMapper.accumulation(report.getPaperId(), report.getUserNumber(), report.getUserTime(), report.getUserCorrect(), 1, report.getFinishTime());
+        userPaperRelationMapper.accumulation(report.getPaperId(), report.getUserNumber(), report.getUserTime(), report.getUserCorrect(), 1, report.getFinishTime(), report.getId());
     }
 
     /**

+ 25 - 3
server/gateway-api/src/main/java/com/qxgmat/service/extend/OrderFlowService.java

@@ -264,8 +264,12 @@ public class OrderFlowService {
                 if (serviceKey == ServiceKey.TEXTBOOK){
                     // 是否存在半价机经券
                     if (!checkout.getMoney().equals(checkout.getOriginMoney())){
+                        JSONObject info = new JSONObject();
+                        info.put("originMoney", originMoney);
+                        info.put("money", money);
+                        info.put("key", "half");
                         JSONObject promote = order.getPromote();
-                        promote.put("textbook", "half");
+                        promote.put("textbook", info);
                     }
                 }
                 money = money.add(checkout.getMoney());
@@ -312,8 +316,12 @@ public class OrderFlowService {
                 money = money.add(promoteVideoMoney);
 
                 // 添加视频优惠记录
+                JSONObject info = new JSONObject();
+                info.put("originMoney", videoMoney);
+                info.put("money", promoteVideoMoney);
+                info.put("message", toolsService.videoMoneyMessage());
                 JSONObject promote = order.getPromote();
-                promote.put("video", percent);
+                promote.put("video", info);
             }else{
                 courseMoney = courseMoney.add(videoMoney);
                 money = money.add(videoMoney);
@@ -334,14 +342,25 @@ public class OrderFlowService {
                 courseMoney = courseMoney.add(checkout.getMoney());
                 money = money.add(checkout.getMoney());
                 originMoney = originMoney.add(checkout.getMoney());
+                // 套餐优惠记录 - 不参与总价,单独展示逻辑
             }
 
             // 满金额处理
             Integer ask = toolsService.getAskTime(courseMoney);
-            order.setAskTime(ask);
+            order.setAskTime(ask * 3600);
+
+            JSONArray gift = order.getGift();
+            if (ask> 0){
+                JSONObject info = new JSONObject();
+                info.put("key", "ask");
+                info.put("message", toolsService.askTimeMessage().replace("{}", ask.toString()));
+                gift.add(info);
+            }
 
             order.setMoney(money);
             order.setOriginMoney(originMoney);
+            // 课程金额进行开票处理
+            order.setInvoiceMoney(courseMoney);
         }));
 
         initRecordCallback.put(ProductType.COURSE, ((order, record) -> {
@@ -734,6 +753,9 @@ public class OrderFlowService {
         // 记录订单类型
         JSONArray productTypes = new JSONArray();
         order.setProductTypes(productTypes);
+        // 记录赠品信息
+        JSONArray gift = new JSONArray();
+        order.setGift(gift);
 
         InitOrder callback;
 

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

@@ -417,6 +417,8 @@ public class QuestionFlowService {
         if (questionNoIds.size() == 0){
             throw new ParameterException("题目数为空");
         }
+        // todo 题目数不能超过50
+        // todo 不同类型不能一起组卷
         QuestionOrigin questionOrigin = QuestionOrigin.WithPaper(origin);
         if (filterTimes > 0){
             // 过滤重复多次的题目

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

@@ -387,6 +387,17 @@ public class ToolsService {
     }
 
     /**
+     * 获取视频课程优惠文字说明:{video: {text:}}
+     * @return
+     */
+    public String videoMoneyMessage(){
+        Setting setting = settingService.getByKey(SettingKey.PROMOTE);
+        JSONObject value = setting.getValue();
+        JSONObject video = value.getJSONObject("video");
+        return video.getString("text");
+    }
+
+    /**
      * 计算视频课程优惠:{video_list: [{"number":32,"percent":2}]}
      * @param list
      * @return
@@ -447,6 +458,14 @@ public class ToolsService {
     }
 
     /**
+     * 获取计算回复时长:{video: {text:}}
+     * @return
+     */
+    public String askTimeMessage(){
+        return "{}小时内问答快速回复";
+    }
+
+    /**
      * 计算回复时长:{ask_time: [{"money":32,"hour":2}]}
      * 单位:秒
      * @param totalMoney 课程总金额
@@ -471,7 +490,7 @@ public class ToolsService {
         }
         if (maxIndex >= 0){
             JSONObject o = settings.getJSONObject(maxIndex);
-            return o.getIntValue("hour") * 3600;
+            return o.getIntValue("hour");
         }
         return 0;
     }

+ 17 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseExperienceService.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;
@@ -32,6 +33,22 @@ public class CourseExperienceService extends AbstractService {
     @Resource
     private CourseExperienceRelationMapper courseExperienceRelationMapper;
 
+    public Page<CourseExperience> listWithUser(int page, int size, Integer userId, String startTime, String endTime, String order, DirectionStatus direction){
+        if(order == null || order.isEmpty()) order = "id";
+        if (direction == null){
+            direction = DirectionStatus.DESC;
+        }
+        String finalOrder = order;
+        DirectionStatus finalDirection = direction;
+        Page<CourseExperience> p = page(
+                ()-> courseExperienceRelationMapper.listWithUser(userId, startTime, endTime, finalOrder, finalDirection.key)
+                , page, size);
+
+        Collection ids = Transform.getIds(p, CourseExperience.class, "id");
+        Transform.replace(p, select(ids), CourseExperience.class, "id");
+        return p;
+    }
+
     public Page<CourseExperience> listAdmin(int page, int size, Integer userId, String keyword, String prepareStatus, ExperienceScoreRange experienceScore, ExperienceDayRange experienceDay, String experiencePercent, String order, DirectionStatus direction){
         Example example = new Example(CourseExperience.class);
         if(userId != null){

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

@@ -7,6 +7,7 @@ import com.nuliji.tools.exception.ParameterException;
 import com.nuliji.tools.exception.SystemException;
 import com.qxgmat.data.dao.QuestionMapper;
 import com.qxgmat.data.dao.entity.Question;
+import com.qxgmat.data.dao.entity.UserCollectQuestion;
 import com.qxgmat.data.dao.entity.UserQuestion;
 import com.qxgmat.data.inline.PaperStat;
 import com.qxgmat.data.relation.QuestionRelationMapper;
@@ -82,6 +83,14 @@ public class QuestionService extends AbstractService {
         questionRelationMapper.accumulation(question.getQuestionId(), 1, question.getUserTime(), question.getIsCorrect());
     }
 
+    /**
+     * 累加做题记录到questionNo
+     * @param question
+     */
+    public void accumulationCollect(UserCollectQuestion question, int collect){
+        questionRelationMapper.accumulationCollect(question.getQuestionId(), collect);
+    }
+
     public Question add(Question question){
         // 按实际更新更改更新时间
         if (question.getQxContent() != null && !question.getQxContent().isEmpty()){

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

@@ -33,6 +33,16 @@ public class UserOrderRecordService extends AbstractService {
     @Resource
     private UserOrderRecordRelationMapper userOrderRecordRelationMapper;
 
+    public List<UserOrderRecord> allByUser(Integer userId, Integer orderId){
+        Example example = new Example(UserOrderRecord.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("orderId", orderId)
+        );
+        return select(userOrderRecordMapper, example);
+    }
+
     public Page<UserOrderRecord> listAdminByOnline(int page, int size, Integer courseId, Integer timeId){
         Example example = new Example(UserOrderRecord.class);
         if(courseId != null){

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

@@ -2,12 +2,15 @@ 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.ProductType;
+import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.dao.UserOrderMapper;
 import com.qxgmat.data.dao.entity.UserOrder;
+import com.qxgmat.data.relation.UserOrderRelationMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -23,6 +26,28 @@ public class UserOrderService extends AbstractService {
     @Resource
     private UserOrderMapper userOrderMapper;
 
+    @Resource
+    private UserOrderRelationMapper userOrderRelationMapper;
+
+
+    public Page<UserOrder> listAdmin(int page, int size, Integer userId, String productType, String payMethod, Integer orderId, String order, DirectionStatus direction){
+        if(order == null || order.isEmpty()) order = "id";
+        if (direction == null){
+            direction = DirectionStatus.DESC;
+        }
+        String finalOrder = order;
+        DirectionStatus finalDirection = direction;
+        Page<UserOrder> p = page(()->{
+            userOrderRelationMapper.listAdmin(userId, productType, payMethod, orderId, finalOrder, finalDirection.key);
+        }, page, size);
+
+        Collection ids = Transform.getIds(p, UserOrder.class, "id");
+
+        // 获取详细数据
+        List<UserOrder> list = select(ids);
+        Transform.replace(p, list, UserOrder.class, "id");
+        return p;
+    }
     /**
      * 合并用户信息,将old转移至new
      * @param oldUserId

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

@@ -144,15 +144,6 @@ public class UserReportService extends AbstractService {
     }
 
     /**
-     * 按report统计预习模块不同题型用户的做题记录
-     * @param userId
-     * @return
-     */
-    public List<UserStudyStatRelation> statGroupPreviewType(Integer userId, String startTime, String endTime){
-        return userReportRelationMapper.statGroupPreviewType(userId, startTime, endTime);
-    }
-
-    /**
      * 计算用户在指定时间内的练习排行
      * @param userId
      * @param startTime