lrf 1 年之前
當前提交
4953cbc534
共有 65 個文件被更改,包括 4256 次插入0 次删除
  1. 11 0
      .editorconfig
  2. 17 0
      .eslintrc.json
  3. 2 0
      .gitattributes
  4. 16 0
      .gitignore
  5. 4 0
      .prettierrc.js
  6. 26 0
      .vscode/launch.json
  7. 10 0
      README.md
  8. 9 0
      README.zh-CN.md
  9. 2 0
      bootstrap.js
  10. 19 0
      ecosystem.config.js
  11. 6 0
      jest.config.js
  12. 69 0
      package.json
  13. 72 0
      src/config/config.default.ts
  14. 73 0
      src/config/config.local.ts
  15. 3 0
      src/config/config.prod.ts
  16. 7 0
      src/config/config.unittest.ts
  17. 66 0
      src/configuration.ts
  18. 72 0
      src/controller/goodsConfig.controller.ts
  19. 141 0
      src/controller/group.controller.ts
  20. 226 0
      src/controller/groupAfterSale.controller.ts
  21. 210 0
      src/controller/groupOrder.controller.ts
  22. 21 0
      src/controller/home.controller.ts
  23. 49 0
      src/controller/orderOthers.ts
  24. 128 0
      src/controller/pay.controller.ts
  25. 25 0
      src/controller/view.controller.ts
  26. 19 0
      src/entity/base/admin.ts
  27. 20 0
      src/entity/base/cashBack.ts
  28. 21 0
      src/entity/base/config.ts
  29. 62 0
      src/entity/base/goods.ts
  30. 32 0
      src/entity/base/goodsSpec.ts
  31. 43 0
      src/entity/base/shop.ts
  32. 22 0
      src/entity/base/shopInBill.ts
  33. 39 0
      src/entity/base/user.ts
  34. 32 0
      src/entity/group/goodsConfig.ts
  35. 29 0
      src/entity/group/group.ts
  36. 36 0
      src/entity/group/groupAfterSale.ts
  37. 43 0
      src/entity/group/groupOrder.ts
  38. 1 0
      src/interface.ts
  39. 88 0
      src/interface/goodsConfig.interface.ts
  40. 149 0
      src/interface/group.interface.ts
  41. 210 0
      src/interface/groupAfterSale.interface.ts
  42. 385 0
      src/interface/groupOrder.interface.ts
  43. 13 0
      src/interface/pay.interface.ts
  44. 12 0
      src/interface/view.interface.ts
  45. 33 0
      src/middleware/checkToken.middleware.ts
  46. 11 0
      src/service/goodsConfig.service.ts
  47. 254 0
      src/service/group.service.ts
  48. 141 0
      src/service/groupAfterSale.service.ts
  49. 388 0
      src/service/groupOrder.service.ts
  50. 72 0
      src/service/mq/mqConsumer.service.ts
  51. 71 0
      src/service/mq/mqSender.service.ts
  52. 74 0
      src/service/shopInBill.service.ts
  53. 70 0
      src/service/view.service.ts
  54. 44 0
      src/util/computed.ts
  55. 88 0
      src/util/kd100.ts
  56. 22 0
      src/util/pipeline.util.ts
  57. 177 0
      src/util/transactions.ts
  58. 9 0
      src/util/util.ts
  59. 93 0
      src/util/wxpay.ts
  60. 20 0
      test/controller/api.test.ts
  61. 21 0
      test/controller/home.test.ts
  62. 54 0
      test/controller/weather.test.ts
  63. 21 0
      tsconfig.json
  64. 1 0
      update.sh
  65. 52 0
      view/info.html

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+# 🎨 editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 17 - 0
.eslintrc.json

@@ -0,0 +1,17 @@
+{
+  "extends": "./node_modules/mwts/",
+  "ignorePatterns": [
+    "node_modules",
+    "dist",
+    "test",
+    "jest.config.js",
+    "typings"
+  ],
+  "env": {
+    "jest": true
+  },
+  "plugins": ["node", "prettier"],
+  "rules": {
+    "prettier/prettier": "off"
+  }
+}

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+src/config/** merge=ours
+ecosystem.config.js merge=ours

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+logs/
+npm-debug.log
+yarn-error.log
+node_modules/
+package-lock.json
+yarn.lock
+coverage/
+dist/
+.idea/
+run/
+.DS_Store
+*.sw*
+*.un~
+.tsbuildinfo
+.tsbuildinfo.*
+.history

+ 4 - 0
.prettierrc.js

@@ -0,0 +1,4 @@
+module.exports = {
+  ...require('mwts/.prettierrc.json'),
+  "printWidth": 200
+}

+ 26 - 0
.vscode/launch.json

@@ -0,0 +1,26 @@
+{
+  // 使用 IntelliSense 了解相关属性。
+  // 悬停以查看现有属性的描述。
+  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "调试团购",
+      "type": "node",
+      "request": "launch",
+      "cwd": "${workspaceRoot}",
+      "runtimeExecutable": "npm",
+      "windows": {
+        "runtimeExecutable": "npm.cmd"
+      },
+      "runtimeArgs": ["run", "dev"],
+      "env": {
+        "NODE_ENV": "local"
+      },
+      "console": "integratedTerminal",
+      "protocol": "auto",
+      "restart": true,
+      "autoAttachChildProcesses": true
+    }
+  ]
+}

+ 10 - 0
README.md

@@ -0,0 +1,10 @@
+# midway quick guide sample
+
+## Usage
+
+```bash
+$ npm i
+$ npm run dev
+$ npm run test
+```
+

+ 9 - 0
README.zh-CN.md

@@ -0,0 +1,9 @@
+# Midway 快速入门示例
+
+## 快速入门
+
+```bash
+$ npm i
+$ npm run dev
+$ npm run test
+```

+ 2 - 0
bootstrap.js

@@ -0,0 +1,2 @@
+const { Bootstrap } = require('@midwayjs/bootstrap');
+Bootstrap.run();

+ 19 - 0
ecosystem.config.js

@@ -0,0 +1,19 @@
+'use strict';
+// 服务设置
+const app = '商城-团购服务';
+module.exports = {
+  apps: [
+    {
+      name: app, // 应用名称
+      script: './bootstrap.js', // 实际启动脚本
+      out: `./logs/${app}.log`,
+      error: `./logs/${app}.err`,
+      watch: [ // 监控变化的目录,一旦变化,自动重启
+        'dist'
+      ],
+      env: {
+        NODE_ENV: 'production', // 环境参数,当前指定为生产环境
+      },
+    },
+  ],
+};

+ 6 - 0
jest.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
+  coveragePathIgnorePatterns: ['<rootDir>/test/'],
+};

+ 69 - 0
package.json

@@ -0,0 +1,69 @@
+{
+  "name": "midway-quick-start",
+  "version": "1.0.0",
+  "description": "",
+  "private": true,
+  "dependencies": {
+    "@midwayjs/axios": "^3.8.0",
+    "@midwayjs/bootstrap": "^3.0.0",
+    "@midwayjs/core": "^3.0.0",
+    "@midwayjs/decorator": "^3.0.0",
+    "@midwayjs/info": "^3.0.0",
+    "@midwayjs/jwt": "^3.8.0",
+    "@midwayjs/koa": "^3.0.0",
+    "@midwayjs/logger": "^2.14.0",
+    "@midwayjs/rabbitmq": "^3.8.0",
+    "@midwayjs/redis": "^3.8.0",
+    "@midwayjs/swagger": "^3.7.1",
+    "@midwayjs/typegoose": "^3.0.0",
+    "@midwayjs/validate": "^3.0.0",
+    "@typegoose/typegoose": "^9.0.0",
+    "@types/crypto-js": "^4.1.1",
+    "amqp-connection-manager": "^4.1.9",
+    "amqplib": "^0.10.3",
+    "crypto-js": "^4.1.1",
+    "decimal.js": "^10.4.2",
+    "free-midway-component": "^1.0.33",
+    "moment": "^2.29.4",
+    "mongoose": "^6.0.7",
+    "swagger-ui-dist": "^4.15.2"
+  },
+  "devDependencies": {
+    "@midwayjs/cli": "^2.0.0",
+    "@midwayjs/mock": "^3.0.0",
+    "@types/amqplib": "^0.10.0",
+    "@types/jest": "^29.2.0",
+    "@types/jsonwebtoken": "^8.5.9",
+    "@types/koa": "^2.13.4",
+    "@types/node": "14",
+    "cross-env": "^6.0.0",
+    "jest": "^29.2.2",
+    "mwts": "^1.0.5",
+    "nock": "^13.2.9",
+    "ts-jest": "^29.0.3",
+    "typescript": "~4.8.0"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  },
+  "scripts": {
+    "start": "NODE_ENV=production node ./bootstrap.js",
+    "dev": "cross-env NODE_ENV=local midway-bin dev --ts",
+    "test": "midway-bin test --ts",
+    "cov": "midway-bin cov --ts",
+    "lint": "mwts check",
+    "lint:fix": "mwts fix",
+    "ci": "npm run cov",
+    "build": "midway-bin build -c"
+  },
+  "midway-bin-clean": [
+    ".vscode/.tsbuildinfo",
+    "dist"
+  ],
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "anonymous",
+  "license": "MIT"
+}

+ 72 - 0
src/config/config.default.ts

@@ -0,0 +1,72 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+const suffix = '';
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1669597198171_1000',
+  koa: {
+    port: 12113,
+    globalPrefix: '/point/group/v1/api',
+  },
+  swagger: {
+    swaggerPath: '/point/group/v1/api/doc/api',
+  },
+  mongoose: {
+    dataSource: {
+      default: {
+        uri: 'mongodb://127.0.0.1:27017/point_shopping',
+        options: {
+          user: 'admin',
+          pass: 'admin',
+          authSource: 'admin',
+          useNewUrlParser: true,
+        },
+        entities: ['./entity'],
+      },
+    },
+  },
+  redis: {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: '123456',
+      db: 1,
+    },
+  },
+  redisKey: {
+    orderKeyPrefix: 'orderKey:',
+  },
+  redisTimeout: 600,
+
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+  },
+  axios: {
+    clients: {
+      wechat: {
+        baseURL: 'https://broadcast.waityou24.cn/wechat/api',
+      },
+      base: {
+        baseURL: 'http://127.0.0.1/point/v1/api',
+      },
+    },
+  },
+  rabbitmq: {
+    url: 'amqp://tehq:tehq@127.0.0.1/tehq',
+  },
+  mqConfig: {
+    normal: {
+      ex: `t_g_ex${suffix}`,
+      q: `t_g_q${suffix}`,
+      rk: `t_g_rk${suffix}`,
+    },
+    dead: {
+      ex: `d_g_ex${suffix}`,
+      q: `d_g_q${suffix}`,
+      rk: `d_g_rk${suffix}`,
+    },
+    timeout: 15, //min
+  },
+  wxPayConfig: 'tehqApp',
+  wxPayCallBack: '/point/group/v1/api/orderDeal/callback',
+} as MidwayConfig;

+ 73 - 0
src/config/config.local.ts

@@ -0,0 +1,73 @@
+import { MidwayConfig } from '@midwayjs/core';
+const ip = '120.48.146.1';
+const suffix = '_dev_local';
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1669597198171_1000',
+  koa: {
+    port: 12213,
+    globalPrefix: '/dev/point/group/v1/api',
+  },
+  swagger: {
+    swaggerPath: '/dev/point/group/v1/api/doc/api',
+  },
+  mongoose: {
+    dataSource: {
+      default: {
+        uri: `mongodb://${ip}:27017/point_shopping-dev`,
+        options: {
+          user: 'admin',
+          pass: 'admin',
+          authSource: 'admin',
+          useNewUrlParser: true,
+        },
+        entities: ['./entity'],
+      },
+    },
+  },
+  redis: {
+    client: {
+      port: 6379, // Redis port
+      host: ip, // Redis host
+      password: '123456',
+      db: 2,
+    },
+  },
+  redisKey: {
+    orderKeyPrefix: 'orderKey:',
+  },
+  redisTimeout: 600,
+
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+  },
+  axios: {
+    clients: {
+      wechat: {
+        baseURL: 'https://broadcast.waityou24.cn/wechat/api', // http://127.0.0.1:14001/wechat/api
+      },
+      base: {
+        baseURL: 'https://broadcast.waityou24.cn/dev/point/v1/api', // http://127.0.0.1:12211
+      },
+    },
+  },
+  rabbitmq: {
+    url: 'amqp://tehqDev:tehqDev@120.48.146.1/tehqDev',
+  },
+  mqConfig: {
+    normal: {
+      ex: `t_g_ex${suffix}`,
+      q: `t_g_q${suffix}`,
+      rk: `t_g_rk${suffix}`,
+    },
+    dead: {
+      ex: `d_g_ex${suffix}`,
+      q: `d_g_q${suffix}`,
+      rk: `d_g_rk${suffix}`,
+    },
+    timeout: 1, //min
+  },
+
+  wxPayConfig: 'pointApp',
+  wxPayCallBack: '/dev/point/group/v1/api/orderDeal/callback',
+} as MidwayConfig;

+ 3 - 0
src/config/config.prod.ts

@@ -0,0 +1,3 @@
+import { MidwayConfig } from '@midwayjs/core';
+// 使用 默认设置
+export default {} as MidwayConfig;

+ 7 - 0
src/config/config.unittest.ts

@@ -0,0 +1,7 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+export default {
+  koa: {
+    port: null,
+  },
+} as MidwayConfig;

+ 66 - 0
src/configuration.ts

@@ -0,0 +1,66 @@
+import { Configuration, App, Inject } from '@midwayjs/decorator';
+import * as koa from '@midwayjs/koa';
+import * as validate from '@midwayjs/validate';
+import * as info from '@midwayjs/info';
+import { join } from 'path';
+import * as FreeFrame from 'free-midway-component';
+// api文档
+import * as swagger from '@midwayjs/swagger';
+import * as redis from '@midwayjs/redis';
+import * as jwt from '@midwayjs/jwt';
+import { CheckTokenMiddleware } from './middleware/checkToken.middleware';
+import * as axios from '@midwayjs/axios';
+import { ILifeCycle, IMidwayContainer } from '@midwayjs/core';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import * as rabbitmq from '@midwayjs/rabbitmq';
+import { Context } from '@midwayjs/koa';
+
+const axiosResponse = response => {
+  if (response.status === 200) return response.data;
+  else {
+    console.log(JSON.stringify(response));
+    throw new ServiceError('请求失败', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+};
+const axiosError = error => {
+  return Promise.reject(error);
+};
+
+@Configuration({
+  imports: [
+    FreeFrame,
+    validate,
+    jwt,
+    axios,
+    swagger,
+    rabbitmq,
+    // {
+    //   component: swagger,
+    //   enabledEnvironment: ['local'],
+    // },
+    {
+      component: info,
+      enabledEnvironment: ['local'],
+    },
+    redis,
+  ],
+  importConfigs: [join(__dirname, './config')],
+})
+export class ContainerLifeCycle implements ILifeCycle {
+  @App()
+  app: koa.Application;
+
+  @Inject()
+  ctx: Context;
+
+  async onReady(container: IMidwayContainer) {
+    this.app.getMiddleware().insertFirst(CheckTokenMiddleware);
+    // base 添加头设置
+    const httpServiceFactory = await container.getAsync(axios.HttpServiceFactory);
+    const baseAxios = httpServiceFactory.get('base');
+    baseAxios.defaults.headers.common['project'] = 'group-service';
+    baseAxios.interceptors.response.use(axiosResponse, axiosError);
+    const wechatAxios = httpServiceFactory.get('wechat');
+    wechatAxios.interceptors.response.use(axiosResponse, axiosError);
+  }
+}

+ 72 - 0
src/controller/goodsConfig.controller.ts

@@ -0,0 +1,72 @@
+import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { BaseController } from 'free-midway-component';
+import { GoodsConfigService } from '../service/goodsConfig.service';
+import {
+  CreateDTO_goodsConfig,
+  CreateVO_goodsConfig,
+  FetchVO_goodsConfig,
+  QueryDTO_goodsConfig,
+  QueryVO_goodsConfig,
+  UpdateDTO_goodsConfig,
+  UpdateVO_goodsConfig,
+} from '../interface/goodsConfig.interface';
+import { ApiQuery, ApiResponse, ApiTags } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+@ApiTags(['商品设置'])
+@Controller('/goodsConfig')
+export class GoodsConfigController extends BaseController {
+  @Inject()
+  service: GoodsConfigService;
+
+  @Post('/')
+  @Validate()
+  @ApiResponse({ type: CreateVO_goodsConfig })
+  async create(@Body() data: CreateDTO_goodsConfig) {
+    const result = await this.service.create(data);
+    return result;
+  }
+  @Get('/')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_goodsConfig })
+  async query(@Query() filter: QueryDTO_goodsConfig, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const data = await this.service.query(filter, { skip, limit, sort: { 'meta.createdAt': 1 } });
+    const total = await this.service.count(filter);
+    return { data, total };
+  }
+
+  @Get('/:id')
+  @ApiResponse({ type: FetchVO_goodsConfig })
+  async fetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id, { populate: false });
+    const result = new FetchVO_goodsConfig(data);
+    return result;
+  }
+
+  @Post('/:id')
+  @Validate()
+  @ApiResponse({ type: UpdateVO_goodsConfig })
+  async update(@Param('id') id: string, @Body() body: UpdateDTO_goodsConfig) {
+    const result = await this.service.updateOne(id, body);
+    return result;
+  }
+
+  @Del('/:id')
+  @Validate()
+  async delete(@Param('id') id: string) {
+    await this.service.delete(id);
+    return 'ok';
+  }
+  async createMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async updateMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async deleteMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+}

+ 141 - 0
src/controller/group.controller.ts

@@ -0,0 +1,141 @@
+import { Body, Controller, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { BaseController, ServiceError, TransactionService } from 'free-midway-component';
+import { GroupService } from '../service/group.service';
+import { AqView, CreateDTO_group, CreateVO_group, FetchVO_group, QueryDTO_group, QueryVO_group, UpdateDTO_group, UpdateVO_group, UserListView } from '../interface/group.interface';
+import { ApiQuery, ApiResponse, ApiTags } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+import { GroupOrderService } from '../service/groupOrder.service';
+import { QueryDTO_groupOrder } from '../interface/groupOrder.interface';
+import { WxPayService } from '../util/wxpay';
+import { WxRefundData } from '../interface/pay.interface';
+import _ = require('lodash');
+import { randomStr } from '../util/util';
+@ApiTags(['团表'])
+@Controller('/group')
+export class GroupController extends BaseController {
+  @Inject()
+  service: GroupService;
+
+  @Inject()
+  orderService: GroupOrderService;
+
+  @Inject()
+  wxpayService: WxPayService;
+
+  @Post('/')
+  @Validate()
+  @ApiResponse({ type: CreateVO_group })
+  async create(@Body() data: CreateDTO_group) {
+    const result = await this.service.create(data);
+    // 暂不需要系统控制团状态
+    const status = _.get(data, 'status');
+    if (status === '0') await this.service.toSendMsg(result);
+    return result;
+  }
+  @Get('/')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_group })
+  async query(@Query() filter: QueryDTO_group, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const data = await this.service.query(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const total = await this.service.count(filter);
+    return { data, total };
+  }
+
+  @Get('/aq')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_group })
+  async aggQuery(@Query() filter: object) {
+    const po = _.pick(filter, ['skip', 'limit']);
+    const fo = _.omit(filter, ['skip', 'limit']);
+    const { data, total } = await this.service.aggQuery(fo, po);
+    const list = [];
+    for (const i of data) {
+      const d = new AqView(i);
+      list.push(d);
+    }
+    return { data: list, total };
+  }
+
+  @Get('/:id')
+  @ApiResponse({ type: FetchVO_group })
+  async fetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id, { populate: false });
+    const result = new FetchVO_group(data);
+    return result;
+  }
+
+  @Post('/:id')
+  @Validate()
+  @ApiResponse({ type: UpdateVO_group })
+  async update(@Param('id') id: string, @Body() body: UpdateDTO_group) {
+    // 需要检查这个团里有没有订单,如果有订单,需要将订单退款后再解散团
+    const { status } = body;
+    if (status === '-1') {
+      const filter = new QueryDTO_groupOrder();
+      filter.props = ['group', 'status'];
+      filter.group = id;
+      filter.status = '1';
+      const orderList = await this.orderService.query(filter);
+      // 退单
+      const errorList = [];
+      for (const o of orderList) {
+        try {
+          const rd = new WxRefundData();
+          rd.order_no = _.get(o, 'pay.pay_no');
+          rd.money = _.get(o, 'pay.pay_money');
+          rd.out_refund_no = `${_.get(o, 'pay.pay_no')}-r-${randomStr()}`;
+          rd.reason = '散团退款';
+          // 退款
+          await this.wxpayService.refund(rd);
+          // 修改订单
+          await this.orderService.updateOne(_.get(o, '_id'), { status: '-1' });
+        } catch (error) {
+          errorList.push(_.get(o, 'no'));
+        }
+      }
+      if (errorList.length > 0) throw new ServiceError(`散团失败! 订单:${errorList.join(';')} 未退款成功,请重新尝试`);
+    }
+    const result = await this.service.updateOne(id, body);
+    // 最后判断是否需要重新发送mq消息
+    const msg = await this.service.needMqMsg({ ...body, id });
+    if (msg) await this.service.toSendMsg({ id, ...msg });
+    if (status === '1' || status === '-1') this.service.toSendCustomerMsg(id);
+    return result;
+  }
+
+  @Validate()
+  async delete(@Param('id') id: string) {
+    await this.service.delete(id);
+    return 'ok';
+  }
+
+  @Get('/userView')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: UserListView })
+  async userView(@Query() filter: QueryDTO_group, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const { data, total } = await this.service.userListView(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const newData = [];
+    for (const i of data) {
+      const nd = new UserListView(i);
+      newData.push(nd);
+    }
+    return { data: newData, total };
+  }
+  async createMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async updateMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async deleteMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+}

+ 226 - 0
src/controller/groupAfterSale.controller.ts

@@ -0,0 +1,226 @@
+import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { BaseController, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { GroupAfterSaleService } from '../service/groupAfterSale.service';
+import {
+  AdminView,
+  CreateDTO_groupAfterSale,
+  CreateVO_groupAfterSale,
+  QueryDTO_groupAfterSale,
+  QueryVO_groupAfterSale,
+  UpdateDTO_groupAfterSale,
+  UpdateVO_groupAfterSale,
+  UserListView,
+} from '../interface/groupAfterSale.interface';
+import { ApiQuery, ApiResponse, ApiTags } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+import { TransactionService } from '../util/transactions';
+import _ = require('lodash');
+import moment = require('moment');
+import { WxPayService } from '../util/wxpay';
+import { GroupOrderService } from '../service/groupOrder.service';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { Config } from '../entity/base/config';
+import { ReturnModelType } from '@typegoose/typegoose';
+import * as computedUtil from '../util/computed';
+@ApiTags(['团购售后'])
+@Controller('/groupAfterSale')
+export class GroupAfterSaleController extends BaseController {
+  @Inject()
+  service: GroupAfterSaleService;
+  @Inject()
+  orderService: GroupOrderService;
+
+  @Inject()
+  wxpayService: WxPayService;
+
+  @InjectEntityModel(Config)
+  configModel: ReturnModelType<typeof Config>;
+
+  @Post('/')
+  @ApiResponse({ type: CreateVO_groupAfterSale })
+  async create(@Body() data: CreateDTO_groupAfterSale) {
+    const order = _.get(data, 'order');
+    if (!order) throw new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY);
+    const od = await this.orderService.fetch(order, { populate: false });
+    if (!od) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const has_afterSale = await this.service.findOne({ order, status: { $nin: ['!1', '!2', '!3', '!4', '!5'] } });
+    if (has_afterSale) throw new ServiceError('该订单已有正在处理中的售后申请,请勿重复申请', FrameworkErrorEnum.SERVICE_FAULT);
+    const customer = _.get(od, 'customer');
+    data.customer = customer;
+    if (!data.apply_time) data.apply_time = moment().format('YYYY-MM-DD HH:mm:ss');
+    const type = data.type;
+    if (type === '4' || type === '5') {
+      const realPay = _.get(od, 'pay.pay_money');
+      data.money = realPay;
+      data.desc = type === '4' ? '取消订单' : '拒收商品';
+    }
+    const result = await this.service.create(data);
+    return result;
+  }
+  @Get('/')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_groupAfterSale })
+  async query(@Query() filter: QueryDTO_groupAfterSale, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const data = await this.service.query(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const total = await this.service.count(filter);
+    return { data, total };
+  }
+
+  @Get('/:id')
+  @ApiResponse({ type: AdminView })
+  async fetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id, { populate: true, lean: true });
+    const nd = await this.service.addInfo(data);
+    const result = new AdminView(nd);
+    return result;
+  }
+
+  @Post('/:id')
+  @ApiResponse({ type: UpdateVO_groupAfterSale })
+  async update(@Param('id') id: string, @Body() body: UpdateDTO_groupAfterSale) {
+    const asd = await this.service.fetch(id);
+    if (!asd) throw new ServiceError('未找到售后数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const order_id = _.get(asd, 'order._id');
+    const tran = new TransactionService();
+    try {
+      const type = _.get(asd, 'type');
+      const status = _.get(body, 'status');
+      if (!status) {
+        const data = await this.service.updateOne(id, body);
+        return data;
+      }
+      let refundInfo;
+      const uStatus = _.get(body, 'status');
+      if (type === '1') {
+        // 仅退款,退优惠券,退款
+        // 如果不是处理中,则不进行退款
+        if (_.get(body, 'status') === '1') {
+          // 1.检验并组织退款信息
+          refundInfo = await this.service.toReturnMoney(asd, body, tran);
+          // 3.修改数据, 直接修改成退完款的状态.如果后面退款失败了.直接回滚了
+          body.status = '-1';
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+          this.service.refundOrder(order_id, tran);
+        } else if (_.get(body, 'status') === '!1') {
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+        }
+        // 4.修改订单状态
+      } else if (type === '2') {
+        // 退货(退库存),退款,退优惠券
+        // 做记录,但是不是结束状态,都不退
+        if (uStatus === '-2') {
+          // 1.检验并组织退款信息
+          refundInfo = await this.service.toReturnMoney(asd, body, tran);
+          // 2.修改订单状态
+          this.service.refundOrder(order_id, tran);
+          // 结束时间
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+        } else if (uStatus === '!2') {
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+        }
+      } else if (type === '3') {
+        // 换货,不需要退款
+        // 但需要检查买卖双方的快递是否签收,都签收的话,需要将状态改为结束
+        const uto = _.get(body, 'transport', {});
+        const eto = _.get(asd, 'transport', {});
+        // 有关快递的字段整合,将传来的和以前的放在一起.然后找是否签收
+        const to = { ...eto, ...uto };
+        const cr = _.get(to, 'customer_receive');
+        const sr = _.get(to, 'shop_receive');
+        if (cr && sr) {
+          body.status = '-3';
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+          this.service.refundOrder(order_id, tran);
+        } else if (uStatus === '!3') {
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+        }
+      } else if (type === '4') {
+        // 取消订单: 退钱,退货(退库存)----不过不需要有快递信息,没发货,退优惠券
+        // 没有商品,只有拆分的订单号,用这个去退
+        if (uStatus === '4') {
+          refundInfo = await this.service.returnOrder(asd, tran);
+          body.status = '-4';
+          this.service.refundOrder(order_id, tran);
+        }
+        body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+      } else if (type === '5') {
+        // 拒收: 退钱, 退货
+        // 如果状态不是完成,那就不退,只是数据修改
+        if (uStatus === '-5') {
+          refundInfo = await this.service.returnOrder(asd, tran);
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+          this.service.refundOrder(order_id, tran);
+        } else if (uStatus === '!5') {
+          body.end_time = moment().format('YYYY-MM-DD HH:mm:ss');
+        }
+      } else throw new ServiceError('未知的售后类型,无法处理', FrameworkErrorEnum.SERVICE_FAULT);
+      // 售后处理人的添加
+      if (body.status !== '0') {
+        const admin = this.ctx.admin;
+        body.deal_person = admin._id;
+      }
+      // 修改数据
+      tran.update('GroupAfterSale', { _id: id }, body);
+      await tran.run();
+      // 退钱
+      if (!refundInfo) return;
+      const res = await this.wxpayService.refund(refundInfo);
+      if (res.errcode && res.errcode !== 0) throw new ServiceError(res.errmsg, FrameworkErrorEnum.SERVICE_FAULT);
+    } catch (error) {
+      await tran.rollback();
+      console.log(error);
+      throw new ServiceError('售后处理失败', FrameworkErrorEnum.SERVICE_FAULT);
+    }
+  }
+
+  @Del('/:id')
+  @Validate()
+  async delete(@Param('id') id: string) {
+    await this.service.delete(id);
+    return 'ok';
+  }
+
+  @Get('/userView')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_groupAfterSale })
+  async userListView(@Query() filter: QueryDTO_groupAfterSale, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const data = await this.service.query(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const newData = [];
+    for (const i of data) {
+      const ad = await this.service.addInfo(i);
+      const nd = new UserListView(ad);
+      newData.push(nd);
+    }
+    const total = await this.service.count(filter);
+    return { data: newData, total };
+  }
+
+  @Get('/canRefund/:id')
+  async canRefund(@Param('id') id: string) {
+    const order = await this.orderService.fetch(id);
+    if (!order) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const config = await this.configModel.findOne({}, { reward_day: 1 });
+    const rd = _.get(config, 'reward_day', 14);
+    const min = computedUtil.multiply(rd, computedUtil.multiply(24, 60));
+    const buy_time = _.get(order, 'buy_time');
+    const m = moment().diff(buy_time, 'm');
+    const r = m < min;
+    return r;
+  }
+
+  async createMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async updateMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async deleteMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+}

+ 210 - 0
src/controller/groupOrder.controller.ts

@@ -0,0 +1,210 @@
+import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { BaseController, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { GroupOrderService } from '../service/groupOrder.service';
+import {
+  AdminListView,
+  AdminView,
+  CreateDTO_groupOrder,
+  CreateVO_groupOrder,
+  FetchVO_groupOrder,
+  QueryDTO_groupOrder,
+  QueryVO_groupOrder,
+  toOrderPageDTO,
+  UpdateDTO_groupOrder,
+  UpdateVO_groupOrder,
+  UserListView,
+  UserView,
+} from '../interface/groupOrder.interface';
+import { ApiQuery, ApiResponse, ApiTags } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+import { RedisService } from '@midwayjs/redis';
+import _ = require('lodash');
+import { TransactionService } from '../util/transactions';
+import { WxPayService } from '../util/wxpay';
+import { GroupService } from '../service/group.service';
+import { MqSender } from '../service/mq/mqSender.service';
+import { ShopInBillService } from '../service/shopInBill.service';
+@ApiTags(['团购订单'])
+@Controller('/groupOrder')
+export class GroupOrderController extends BaseController {
+  @Inject()
+  service: GroupOrderService;
+
+  @Inject()
+  redis: RedisService;
+
+  @Inject()
+  wxpayService: WxPayService;
+
+  @Inject()
+  groupService: GroupService;
+
+  @Inject()
+  mqSender: MqSender;
+
+  @Inject()
+  shopInBillService: ShopInBillService;
+
+  @Post('/checkCanBuy')
+  @Validate()
+  async checkCanBuy(@Body() data: toOrderPageDTO, makeCache = true) {
+    // 进入订单页前的检查
+    // 1.检查店铺是否运行
+    await this.service.checkCanBuy_shop(data);
+    // 2.检查商品是否可以购买
+    await this.service.checkCanBuy_goods(data);
+    // 3.检查规格是否可以购买
+    await this.service.checkCanBuy_goodsSpec(data);
+    // 4.检查团是否满足可以够买
+    const { result, msg } = await this.groupService.checkGroup(data.group);
+    if (!result) throw new ServiceError(msg, FrameworkErrorEnum.SERVICE_FAULT);
+    // 5.检查规格是否有团购设置
+    await this.service.checkCanBuy_specInGroupConfig(data);
+    // 检查完了,做缓存,将key返回回去
+    if (makeCache) {
+      const key = await this.service.makeCache(data);
+      return key;
+    }
+    return 'ok';
+  }
+
+  @Post('/toOrderPage')
+  async toOrderPage(@Body('key') key: string) {
+    const checkData = await this.service.getCache(key);
+    const { result, msg } = await this.groupService.checkGroup(checkData.group);
+    if (!result) throw new ServiceError(msg, FrameworkErrorEnum.SERVICE_FAULT);
+    const data = await this.service.getOrderPageData(checkData);
+    return data;
+  }
+
+  @Post('/')
+  // @Validate()
+  @ApiResponse({ type: CreateVO_groupOrder })
+  async create(@Body() data: CreateDTO_groupOrder) {
+    // 需要重写,不是简单创建了
+    const customer = _.get(this.ctx, 'user._id');
+    if (!customer) throw new ServiceError('缺少用户信息', FrameworkErrorEnum.NEED_PARAMS);
+    // 1.重走一遍商品检查
+    const checkData: toOrderPageDTO = {
+      shop: _.get(data, 'shop'),
+      goods: _.get(data, 'goods'),
+      goodsSpec: _.get(data, 'goodsSpec'),
+      num: _.get(data, 'num'),
+      group: _.get(data, 'group'),
+    };
+    await this.checkCanBuy(checkData, false);
+    // 2.组织订单数据
+    const createData = await this.service.toMakeOrderData(data, customer);
+    // 3.事务保存
+    const tran = new TransactionService();
+    try {
+      const id = tran.insert('GroupOrder', createData);
+      // 4.减库存
+      await this.service.dealGoods(createData, tran);
+      await tran.run();
+      // 5.创建死信
+      await this.mqSender.orderMsg({ order_id: id });
+      return id;
+    } catch (error) {
+      await tran.rollback();
+    }
+  }
+  @Get('/')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_groupOrder })
+  async query(@Query() filter: QueryDTO_groupOrder, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const data = await this.service.query(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const newData = [];
+    for (const i of data) {
+      const ni = await this.service.addInfo(i);
+      const nd = new AdminListView(ni);
+      newData.push(nd);
+    }
+    const total = await this.service.count(filter);
+    return { data: newData, total };
+  }
+
+  @Get('/userView')
+  @ApiQuery({
+    name: 'query',
+  })
+  @ApiResponse({ type: QueryVO_groupOrder })
+  async userView(@Query() filter: QueryDTO_groupOrder, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const { data, total } = await this.service.userListView(filter, { skip, limit, sort: { 'meta.createdAt': -1 } });
+    const newData = [];
+    for (const i of data) {
+      const ni = await this.service.addInfo(i);
+      const nd = new UserListView(ni);
+      newData.push(nd);
+    }
+    return { data: newData, total };
+  }
+
+  @Get('/userView/:id')
+  @ApiResponse({ type: FetchVO_groupOrder })
+  async userFetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id, { populate: true });
+    const result = new UserView(data);
+    return result;
+  }
+
+  @Get('/:id')
+  @ApiResponse({ type: AdminView })
+  async fetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id, { populate: true });
+    const ni = await this.service.addInfo(data);
+    const result = new AdminView(ni);
+    return result;
+  }
+
+  @Post('/:id')
+  @ApiResponse({ type: UpdateVO_groupOrder })
+  async update(@Param('id') id: string, @Body() body: UpdateDTO_groupOrder) {
+    const status = body.status;
+    const groupSuccess = await this.service.checkGroupSuccess(id);
+    if (!groupSuccess && parseInt(status) > 1) throw new ServiceError('该团未拼团成功,无法进行订单处理');
+    const oldData = await this.service.fetch(id);
+    let result;
+    // 如果没有改状态,那就直接修改内容
+    if (status === _.get(oldData, 'status')) result = await this.service.updateOne(id, body);
+    else {
+      // 如果改了状态,那就要看是哪种状态
+      if (status === '3') {
+        // 收货了,需要给团长记账了
+        const tran = new TransactionService();
+        try {
+          tran.update('GroupOrder', { _id: id }, body);
+          // 佣金金额; 店铺本单实际收入 =  订单实际支付价格*(1-抽成) - 佣金金额
+          const commission = await this.service.leaderCommission(id, tran);
+          await this.shopInBillService.createByOrder(id, commission, tran);
+          await tran.run();
+        } catch (error) {
+          await tran.rollback();
+          throw new ServiceError('收货发生错误', FrameworkErrorEnum.SERVICE_FAULT);
+        }
+      } else result = await this.service.updateOne(id, body);
+    }
+    return result;
+  }
+
+  @Del('/:id')
+  @Validate()
+  async delete(@Param('id') id: string) {
+    await this.service.delete(id);
+    return 'ok';
+  }
+
+  async createMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async updateMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async deleteMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+}

+ 21 - 0
src/controller/home.controller.ts

@@ -0,0 +1,21 @@
+import { App, Controller, Get, Inject } from '@midwayjs/decorator';
+import { WxPayService } from '../util/wxpay';
+import { Application } from '@midwayjs/koa';
+import { MqSender } from '../service/mq/mqSender.service';
+@Controller('/')
+export class HomeController {
+  @Inject()
+  wxpayService: WxPayService;
+
+  @App()
+  app: Application;
+
+  @Inject()
+  mqService: MqSender;
+
+  @Get('/')
+  async home(): Promise<string> {
+    await this.mqService.groupMsg({ order_id: '638fec34a4ec0bad05bc2c54' }, 5000);
+    return 'Hello Midwayjs!';
+  }
+}

+ 49 - 0
src/controller/orderOthers.ts

@@ -0,0 +1,49 @@
+import { Body, Controller, Inject, Post } from '@midwayjs/decorator';
+import { ApiTags } from '@midwayjs/swagger';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { GroupOrderService } from '../service/groupOrder.service';
+import { Kd100Service } from '../util/kd100';
+import _ = require('lodash');
+import { GroupAfterSaleService } from '../service/groupAfterSale.service';
+
+@ApiTags(['订单其他接口'])
+@Controller('/orderOthers')
+export class OrderOthersController {
+  @Inject()
+  kd100: Kd100Service;
+  @Inject()
+  orderService: GroupOrderService;
+
+  @Inject()
+  afterSaleService: GroupAfterSaleService;
+
+  @Post('/transport')
+  async transport(@Body('order') order: string) {
+    const od = await this.orderService.fetch(order, { populate: false, lean: true });
+    if (!od) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const transport = _.get(od, 'transport', []);
+    if (transport.length <= 0) return [];
+    const result = [];
+    for (const t of transport) {
+      const no = _.get(t, 'shop_transport_no');
+      const type = _.get(t, 'shop_transport_type');
+      const tr = await this.kd100.search({ no, type });
+      result.push(tr);
+    }
+    return result;
+  }
+
+  @Post('/afterSale/transport')
+  async afterSaleTransport(@Body('id') id: string) {
+    const asd = await this.afterSaleService.fetch(id);
+    if (!asd) throw new ServiceError('未找到售后数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const transport = _.get(asd, 'transport', {});
+    const ct = { no: _.get(transport, 'customer_transport_no'), type: _.get(transport, 'customer_transport_type') };
+    const st = { no: _.get(transport, 'shop_transport_no'), type: _.get(transport, 'shop_transport_type') };
+    const result = { customer: {}, shop: {} };
+    if (ct.no && ct.type) result.customer = await this.kd100.search(ct);
+    if (st.no && st.type) result.shop = await this.kd100.search(st);
+    return result;
+  }
+
+}

+ 128 - 0
src/controller/pay.controller.ts

@@ -0,0 +1,128 @@
+import { Body, Controller, Inject, Post } from '@midwayjs/decorator';
+import { ApiTags } from '@midwayjs/swagger';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { WxPayService } from '../util/wxpay';
+import _ = require('lodash');
+import { GroupOrderService } from '../service/groupOrder.service';
+import * as computedUtil from '../util/computed';
+import { PayData, WxRefundData } from '../interface/pay.interface';
+import { User } from '../entity/base/user';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { TransactionService } from '../util/transactions';
+import moment = require('moment');
+import { GroupService } from '../service/group.service';
+import { randomStr } from '../util/util';
+@ApiTags(['订单支付相关'])
+@Controller('/orderDeal')
+export class PayController {
+  @Inject()
+  wxpayService: WxPayService;
+
+  @Inject()
+  orderService: GroupOrderService;
+  @Inject()
+  groupService: GroupService;
+  @InjectEntityModel(User)
+  userModel: ReturnModelType<typeof User>;
+
+  @Post('/pay')
+  /**订单支付 */
+  async pay(@Body() body: object) {
+    const order_id = _.get(body, 'order_id');
+    if (!order_id) throw new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY);
+    const order = await this.orderService.fetch(order_id);
+    if (!order) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    // 检查团是否可以继续购买
+    const group = _.get(order, 'group._id');
+    const { result, msg } = await this.groupService.checkGroup(group);
+    if (!result) throw new ServiceError(msg, FrameworkErrorEnum.SERVICE_FAULT);
+    if (_.get(order, 'pay.result')) throw new ServiceError('订单已支付', FrameworkErrorEnum.SERVICE_FAULT);
+    let rePayTimes = 0;
+    const pay = _.get(order, 'pay', {});
+    let pay_no = _.get(pay, 'pay_no');
+    if (pay_no) {
+      const arr = pay_no.split('-');
+      const last = _.last(arr);
+      rePayTimes = computedUtil.plus(last, 1);
+      // 关闭前一个订单
+      await this.wxpayService.close(pay_no);
+    }
+    pay_no = `${_.get(order, 'no')}-${rePayTimes}`;
+    pay.pay_no = pay_no;
+    const customer = _.get(order, 'customer');
+    if (!customer) throw new ServiceError('订单信息缺少购买人信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const user = await this.userModel.findById(customer).lean();
+    const payData: PayData = { money: _.get(order, 'pay.pay_money'), desc: '团购', order_no: pay_no, openid: _.get(user, 'openid') };
+    await this.orderService.updateOne(_.get(order, '_id'), { pay });
+    let data = await this.wxpayService.create(payData);
+    data = this.wxpayService.preparToUniAppWxPay(data);
+    return data;
+  }
+
+  /**订单取消 */
+  @Post('/cancel')
+  async cancel(@Body() body: object) {
+    const order_id = _.get(body, 'order_id');
+    if (!order_id) throw new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY);
+    const order = await this.orderService.fetch(order_id);
+    if (!order) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    if (_.get(order, 'pay.result')) throw new ServiceError('该订单已支付完成', FrameworkErrorEnum.SERVICE_FAULT);
+    if (_.get(order, 'status') !== '0') throw new ServiceError('该订单不处于可以退单的状态', FrameworkErrorEnum.SERVICE_FAULT);
+    const tran = new TransactionService();
+    try {
+      // 加库存
+      await this.orderService.returnGoods(order, tran);
+      tran.update('GroupOrder', { _id: _.get(order, '_id') }, { status: '-1' });
+      // 改状态
+      await tran.run();
+    } catch (error) {
+      await tran.rollback();
+      throw new ServiceError('订单取消失败', FrameworkErrorEnum.SERVICE_FAULT);
+    }
+  }
+
+  /**支付回调 */
+  @Post('/callback')
+  async callback(@Body() body: object) {
+    const result = _.get(body, 'result');
+    if (!result) throw new ServiceError('缺少支付回调信息', FrameworkErrorEnum.NEED_BODY);
+    const { out_trade_no, payer } = result;
+    const openid = _.get(payer, 'openid');
+    const query = { 'pay.pay_no': new RegExp(`${out_trade_no}`), 'pay.openid': openid };
+    const order = await this.orderService.findOne(query);
+    if (!order) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const payData = _.get(order, 'pay', {});
+    // 支付结果全都存起来
+    payData.result = result;
+    payData.pay_time = moment().format('YYYY-MM-DD HH:mm:ss');
+    // 检查团状态
+    const { result: gr, msg } = await this.groupService.checkGroup(order.group);
+    if (!gr) {
+      // 不满足团条件.退款
+      const rd = new WxRefundData();
+      rd.money = _.get(order, 'pay.pay_money');
+      rd.order_no = _.get(order, 'pay.pay_no');
+      rd.out_refund_no = `${_.get(order, 'no')}-r-${randomStr()}`;
+      rd.reason = '团购未参与成功';
+      await this.wxpayService.refund(rd);
+      // 还原库存
+      const tran = new TransactionService();
+      this.orderService.returnGoods(order, tran);
+      tran.update('GroupOrder', { _id: _.get(order, '_id') }, { status: '-1' });
+      await tran.run();
+      throw new ServiceError(msg, FrameworkErrorEnum.SERVICE_FAULT);
+    }
+    const tran = new TransactionService();
+    try {
+      // 1.修改状态及支付结果
+      tran.update('GroupOrder', { _id: order._id }, { pay: payData, status: '1' });
+      // 2.加销量
+      await this.orderService.addSell(_.get(order, 'goods'), _.get(order, 'num'), tran);
+      await tran.run();
+    } catch (error) {
+      await tran.rollback();
+      throw new ServiceError('支付回调失败', FrameworkErrorEnum.SERVICE_FAULT);
+    }
+  }
+}

+ 25 - 0
src/controller/view.controller.ts

@@ -0,0 +1,25 @@
+import { Body, Controller, Inject, Post } from '@midwayjs/decorator';
+import { ApiBody, ApiOperation, ApiTags } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+import { GoodsDetailDTO } from '../interface/view.interface';
+import { GoodsConfigService } from '../service/goodsConfig.service';
+import { ViewService } from '../service/view.service';
+
+@ApiTags(['视图'])
+@Controller('/view')
+export class ViewController {
+  @Inject()
+  goodsConfig: GoodsConfigService;
+
+  @Inject()
+  service: ViewService;
+
+  @Post('/goodsDetail')
+  @ApiOperation({ description: '团购商品详情接口' })
+  @ApiBody({ type: GoodsDetailDTO })
+  @Validate()
+  async goodsDetail(@Body() body: GoodsDetailDTO) {
+    const viewData = await this.service.setGoodsDetailViewData(body);
+    return viewData;
+  }
+}

+ 19 - 0
src/entity/base/admin.ts

@@ -0,0 +1,19 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'admin' },
+})
+export class Admin extends BaseModel {
+  @prop({ required: false, index: true, zh: '账号' })
+  account: string;
+  @prop({ required: false, index: false, zh: '密码', select: false })
+  password: object;
+  @prop({ required: false, index: false, zh: '名称' })
+  name: string;
+  @prop({ required: false, index: true, zh: '角色', ref: 'Role' })
+  role: string;
+  @prop({ required: false, index: true, zh: '店铺', ref: 'Shop' })
+  shop: string;
+  @prop({ required: false, index: false, zh: '邮箱' })
+  email: string;
+}

+ 20 - 0
src/entity/base/cashBack.ts

@@ -0,0 +1,20 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+import { Decimal128 } from 'mongoose';
+@modelOptions({
+  schemaOptions: { collection: 'cashBack' },
+})
+export class CashBack extends BaseModel {
+  @prop({ required: false, index: true, zh: '推荐人' })
+  inviter: string;
+  @prop({ required: false, index: false, zh: '返现金额' })
+  money: Decimal128;
+  @prop({ required: false, index: false, zh: '返现时间' })
+  time: string;
+  @prop({ required: false, index: true, zh: '状态', remark: '字典:cashBack_status', default: '0' })
+  status: string;
+  @prop({ required: false, index: true, zh: '来源', remark: '字典:cashBack_source' })
+  source: string;
+  @prop({ required: false, index: true, zh: '来源id' })
+  source_id: string;
+}

+ 21 - 0
src/entity/base/config.ts

@@ -0,0 +1,21 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'config' },
+})
+export class Config extends BaseModel {
+  @prop({ required: false, index: false, zh: '系统名称' })
+  title: string;
+  @prop({ required: false, index: false, zh: '设置' })
+  config: object;
+  @prop({ required: false, index: false, zh: '用户协议', remark: '富文本' })
+  agree: string;
+  @prop({ required: false, index: false, zh: '底部加载数据结束的文字提示' })
+  bottom_title: string;
+  @prop({ required: false, index: false, zh: '底部菜单设置' })
+  bottom_menu: object;
+  @prop({ required: false, index: false, zh: '提现日期' })
+  reward_day: number;
+  @prop({ required: false, index: false, zh: '团长规则', remark: '富文本' })
+  leader_rule: string
+}

+ 62 - 0
src/entity/base/goods.ts

@@ -0,0 +1,62 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'goods' },
+})
+export class Goods extends BaseModel {
+  @prop({ required: false, index: true, zh: '店铺' })
+  shop: string;
+  @prop({ required: false, index: true, zh: '商品名称' })
+  name: string;
+  @prop({
+    required: false,
+    index: false,
+    zh: '简短简介',
+    remark: '长度在50以内',
+  })
+  shot_brief: string;
+  @prop({ required: false, index: false, zh: '发货时间' })
+  send_time: string;
+  @prop({ required: false, index: false, zh: '商品介绍' })
+  brief: string;
+  @prop({ required: false, index: false, zh: '商品图片' })
+  file: Array<any>;
+  @prop({ required: false, index: false, zh: '商品分类' })
+  tags: Array<any>;
+  @prop({ required: false, index: false, zh: '浏览量' })
+  view_num: number;
+  @prop({ required: false, index: false, zh: '销量' })
+  sell_num: number;
+  @prop({
+    required: false,
+    index: true,
+    zh: '商品状态',
+    remark: '字典:goods_status',
+    default: '0',
+  })
+  status: string;
+  @prop({
+    required: false,
+    index: false,
+    zh: '活动标签',
+    remark: '字典:act_tags',
+  })
+  act_tags: Array<any>;
+  @prop({ required: false, index: true, zh: '排序', default: '0' })
+  sort: number;
+  @prop({ required: false, index: false, zh: '来源' })
+  source: string;
+  @prop({ required: false, index: false, zh: '网址' })
+  url: string;
+  @prop({ required: false, index: true, zh: '是否返现', remark: '字典:use' })
+  is_cashBack: string;
+  @prop({ required: false, index: false, zh: '返现设置', remark: '返现设置' })
+  cb_config: object;
+  @prop({
+    required: false,
+    index: true,
+    zh: '商品编码',
+    remark: '随机生成,8位',
+  })
+  code: string;
+}

+ 32 - 0
src/entity/base/goodsSpec.ts

@@ -0,0 +1,32 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+import { Decimal128 } from 'mongoose';
+@modelOptions({
+  schemaOptions: { collection: 'goodsSpec' },
+})
+export class GoodsSpec extends BaseModel {
+  @prop({ required: false, index: true, zh: '商品源' })
+  goods: string;
+  @prop({ required: false, index: true, zh: '规格名称' })
+  name: string;
+  @prop({ required: false, index: false, zh: '实际销售价格' })
+  sell_money: Decimal128;
+  @prop({ required: false, index: false, zh: '划掉销售价格' })
+  flow_money: Decimal128;
+  @prop({ required: false, index: false, zh: '运费' })
+  freight: Decimal128;
+  @prop({ required: false, index: false, zh: '库存' })
+  num: number;
+  @prop({
+    required: false,
+    index: true,
+    zh: '状态',
+    remark: '字典:use',
+    default: '0',
+  })
+  status: string;
+  @prop({ required: false, index: false, zh: '图片' })
+  file: Array<any>;
+  @prop({ required: false, index: true, zh: '排序', default: '0' })
+  sort: number;
+}

+ 43 - 0
src/entity/base/shop.ts

@@ -0,0 +1,43 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'shop' },
+})
+export class Shop extends BaseModel {
+  @prop({ required: false, index: false, zh: '店铺logo' })
+  logo: Array<any>;
+  @prop({ required: false, index: true, zh: '商店名称' })
+  name: string;
+  @prop({
+    required: false,
+    index: true,
+    zh: '店铺编号',
+    remark: '自增,中间件处理',
+  })
+  code: string;
+  @prop({ required: false, index: true, zh: '店主' })
+  person: string;
+  @prop({ required: false, index: true, zh: '联系电话' })
+  phone: string;
+  @prop({ required: false, index: false, zh: '地址' })
+  address: string;
+  @prop({ required: false, index: false, zh: '证件照片' })
+  file: Array<any>;
+  @prop({
+    required: false,
+    index: true,
+    zh: '店铺状态',
+    remark: '字典:shop_status',
+  })
+  status: string;
+  @prop({ required: false, index: false, zh: '商品评分', default: '0' })
+  goods_score: number;
+  @prop({ required: false, index: false, zh: '发货评分', default: '0' })
+  send_score: number;
+  @prop({ required: false, index: false, zh: '服务评分', default: '0' })
+  service_score: number;
+  @prop({ required: false, index: false, zh: '抽成比例', default: 0 })
+  cut: number;
+  @prop({ required: false, index: false, zh: '二维码' })
+  qrcode: Array<any>;
+}

+ 22 - 0
src/entity/base/shopInBill.ts

@@ -0,0 +1,22 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+import { Decimal128 } from 'mongoose';
+@modelOptions({
+  schemaOptions: { collection: 'shopInBill' },
+})
+export class ShopInBill extends BaseModel {
+  @prop({ required: false, index: true, zh: '店铺', ref: 'Shop' })
+  shop: string;
+  @prop({ required: false, index: false, zh: '抽成比例' })
+  cut: number;
+  @prop({ required: false, index: false, zh: '总金额' })
+  total: Decimal128;
+  @prop({ required: false, index: false, zh: '净金额' })
+  receipts: Decimal128;
+  @prop({ required: false, index: false, zh: '流水时间' })
+  time: string;
+  @prop({ required: false, index: true, zh: '来源', remark: '字典:cashBack_source' })
+  source: string;
+  @prop({ required: false, index: true, zh: '来源id' })
+  source_id: string;
+}

+ 39 - 0
src/entity/base/user.ts

@@ -0,0 +1,39 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'user' },
+})
+export class User extends BaseModel {
+  @prop({ required: false, index: true, zh: '用户名' })
+  name: string;
+  @prop({ required: false, index: true, zh: '手机号' })
+  phone: string;
+  @prop({ required: false, index: true, zh: '电子邮箱' })
+  email: string;
+  @prop({ required: false, index: false, zh: '头像' })
+  icon: Array<any>;
+  @prop({ required: false, index: false, zh: '生日' })
+  birth: string;
+  @prop({ required: false, index: true, zh: '性别', remark: '字典:gender' })
+  gender: string;
+  @prop({ required: false, index: true, zh: '微信小程序' })
+  openid: string;
+  @prop({
+    required: false,
+    index: true,
+    zh: '状态',
+    remark: '字典:user_status',
+    default: '0',
+  })
+  status: string;
+  @prop({
+    required: false,
+    index: true,
+    zh: '是否是团长',
+    remark: '字典:is_use',
+    default: '1',
+  })
+  is_leader: string;
+  @prop({ required: false, index: true, zh: '密码', select: false })
+  password: object;
+}

+ 32 - 0
src/entity/group/goodsConfig.ts

@@ -0,0 +1,32 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+import { Decimal128 } from 'mongoose';
+@modelOptions({
+  schemaOptions: { collection: 'goodsConfig' },
+})
+export class GoodsConfig extends BaseModel {
+  @prop({ required: false, index: true, zh: '店铺id', ref: 'Shop' })
+  shop: string;
+  @prop({ required: false, index: true, zh: '商品id', ref: 'Goods' })
+  goods: string;
+  @prop({ required: false, index: true, zh: '规格id', ref: 'GoodsSpec' })
+  spec: string;
+  @prop({ required: false, index: false, zh: '团长价' })
+  leader_price: Decimal128;
+  @prop({ required: false, index: false, zh: '团购价' })
+  price: Decimal128;
+  @prop({
+    required: false,
+    index: false,
+    zh: '团长提成金额',
+    remark: '输入固定金额,不要百分比,只要最后的钱',
+  })
+  leader_get: Decimal128;
+  @prop({ required: false, index: false, zh: '运费' })
+  freight: Decimal128;
+
+  @prop({ required: false, index: false, zh: '购买限制,字典:buy_limit' })
+  buy_limit: string;
+  @prop({ required: false, index: false, zh: '购买数量界限' })
+  limit_num: number;
+}

+ 29 - 0
src/entity/group/group.ts

@@ -0,0 +1,29 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'group' },
+})
+export class Group extends BaseModel {
+  @prop({ required: true, index: true, zh: '商店id', ref: 'Shop' })
+  shop: string;
+  @prop({ required: true, index: true, zh: '商品id', ref: 'Goods' })
+  goods: string;
+  @prop({ required: false, index: true, zh: '团长', ref: 'User' })
+  leader: string;
+  @prop({ required: true, index: false, zh: '人数限制' })
+  person_limit: number;
+  @prop({ required: false, index: false, zh: '团购设置' })
+  group_config: Array<any>;
+  @prop({ required: false, index: false, zh: '开始时间' })
+  start_time: string;
+  @prop({ required: false, index: false, zh: '结束时间' })
+  end_time: string;
+  @prop({
+    required: false,
+    index: false,
+    zh: '团状态',
+    remark: '字典:group_status',
+    default: '2',
+  })
+  status: string;
+}

+ 36 - 0
src/entity/group/groupAfterSale.ts

@@ -0,0 +1,36 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+import { Decimal128 } from 'mongoose';
+@modelOptions({
+  schemaOptions: { collection: 'groupAfterSale' },
+})
+export class GroupAfterSale extends BaseModel {
+  @prop({ required: false, index: true, zh: '订单', ref: 'GroupOrder' })
+  order: string;
+  @prop({ required: false, index: true, zh: '订单', ref: 'Shop' })
+  shop: string;
+  @prop({ required: false, index: true, zh: '顾客', ref: 'User' })
+  customer: string;
+  @prop({ required: false, index: true, zh: '售后类型', remark: '字典:afterSale_type' })
+  type: string;
+  @prop({ required: false, index: false, zh: '申请理由', remark: '字典:afterSale_reason' })
+  reason: string;
+  @prop({ required: false, index: false, zh: '申请售后描述' })
+  desc: string;
+  @prop({ required: false, index: false, zh: '凭证图片' })
+  file: Array<any>;
+  @prop({ required: false, index: false, zh: '快递信息' })
+  transport: object;
+  @prop({ required: false, index: true, zh: '售后申请时间' })
+  apply_time: string;
+  @prop({ required: false, index: true, zh: '售后结束时间' })
+  end_time: string;
+  @prop({ required: false, index: false, zh: '售后状态', remark: '字典:afterSale_status', default: '0' })
+  status: string;
+  @prop({ required: false, index: false, zh: '退款金额' })
+  money: Decimal128;
+  @prop({ required: false, index: true, zh: '处理人', ref: 'Admin' })
+  deal_person: string;
+  @prop({ required: false, index: true, zh: '团长建议' })
+  leader_suggest: boolean;
+}

+ 43 - 0
src/entity/group/groupOrder.ts

@@ -0,0 +1,43 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'groupOrder' },
+})
+export class GroupOrder extends BaseModel {
+  @prop({ required: false, index: true, zh: '用户', ref: 'User' })
+  customer: string;
+  @prop({ required: false, index: true, zh: '地址' })
+  address: object;
+  @prop({ required: false, index: true, zh: '店铺', ref: 'Shop' })
+  shop: string;
+  @prop({ required: false, index: true, zh: '商品' })
+  goods: object;
+  @prop({ required: false, index: true, zh: '规格' })
+  goodsSpec: object;
+  @prop({ required: false, index: true, zh: '购买时间' })
+  buy_time: string;
+  @prop({ required: false, index: true, zh: '订单号' })
+  no: string;
+  @prop({ required: false, index: false, zh: '购买数量' })
+  num: number;
+  @prop({
+    required: false,
+    index: true,
+    zh: '订单状态',
+    remark: '字典:order_process',
+    default: '0',
+  })
+  status: string;
+  @prop({ required: false, index: false, zh: '支付数据' })
+  pay: object;
+  @prop({ required: false, index: true, zh: '团id', ref: 'Group' })
+  group: string;
+  @prop({ required: false, index: false, zh: '购买时的团购设置' })
+  config: object;
+  @prop({ required: false, index: false, zh: '备注' })
+  remarks: string;
+  @prop({ required: false, index: false, zh: '快递类型', remark: '字典:transport_type' })
+  transport_type: string;
+  @prop({ required: false, index: false, zh: '快递信息' })
+  transport: Array<any>;
+}

+ 1 - 0
src/interface.ts

@@ -0,0 +1 @@
+export * from './interface/goodsConfig.interface';

+ 88 - 0
src/interface/goodsConfig.interface.ts

@@ -0,0 +1,88 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+import { ApiProperty } from '@midwayjs/swagger';
+import _ = require('lodash');
+import { SearchBase } from 'free-midway-component';
+export class FetchVO_goodsConfig {
+  constructor(data: object) {
+    for (const key of Object.keys(this)) {
+      this[key] = _.get(data, key);
+    }
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: '店铺id' })
+  'shop': string = undefined;
+  @ApiProperty({ description: '商品id' })
+  'goods': string = undefined;
+  @ApiProperty({ description: '规格id' })
+  'spec': string = undefined;
+  @ApiProperty({ description: '团购价' })
+  'price': number = undefined;
+  @ApiProperty({ description: '团长提成金额' })
+  'leader_get': number = undefined;
+  @ApiProperty({ description: '运费' })
+  'freight': number = undefined;
+  @ApiProperty({ description: '购买限制' })
+  'buy_limit': string = undefined;
+  @ApiProperty({ description: '购买数量界限' })
+  'limit_num': number = undefined;
+}
+
+export class QueryDTO_goodsConfig extends SearchBase {
+  constructor() {
+    const like_prop = [];
+    const props = ['shop', 'goods', 'spec'];
+    const mapping = {};
+    super({ like_prop, props, mapping });
+  }
+  @ApiProperty({ description: 'shop' })
+  'shop': string = undefined;
+  @ApiProperty({ description: 'goods' })
+  'goods': string = undefined;
+  @ApiProperty({ description: 'spec' })
+  'spec': string = undefined;
+}
+
+export class QueryVO_goodsConfig extends FetchVO_goodsConfig {}
+
+export class CreateDTO_goodsConfig {
+  @ApiProperty({ description: 'shop', example: '6333d71d32c5f69745f9bd32' })
+  @Rule(RuleType['string']().empty())
+  'shop': string = undefined;
+  @ApiProperty({ description: 'goods', example: '635b89c042e87c7a2880b484' })
+  @Rule(RuleType['string']().empty())
+  'goods': string = undefined;
+  @ApiProperty({ description: 'spec', example: '635b89d742e87c7a2880b4bb' })
+  @Rule(RuleType['string']().empty())
+  'spec': string = undefined;
+  @ApiProperty({ description: 'leader_price', example: 0.04 })
+  @Rule(RuleType['number']().empty())
+  'leader_price': number = undefined;
+  @ApiProperty({ description: 'price', example: 0.08 })
+  @Rule(RuleType['number']().empty())
+  'price': number = undefined;
+  @ApiProperty({ description: 'leader_get', example: 0.02 })
+  @Rule(RuleType['number']().empty())
+  'leader_get': number = undefined;
+  @ApiProperty({ description: 'freight', example: 0.02 })
+  @Rule(RuleType['number']().empty())
+  'freight': number = undefined;
+
+  @ApiProperty({ description: '购买限制', example: '1' })
+  @Rule(RuleType['string']().empty())
+  'buy_limit': string = undefined;
+
+  @ApiProperty({ description: '购买数量界限', example: 2 })
+  @Rule(RuleType['number']().empty())
+  'limit_num': number = undefined;
+}
+
+export class CreateVO_goodsConfig extends FetchVO_goodsConfig {}
+
+export class UpdateDTO_goodsConfig extends CreateDTO_goodsConfig {
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty())
+  '_id': string = undefined;
+}
+
+export class UpdateVO_goodsConfig extends FetchVO_goodsConfig {}

+ 149 - 0
src/interface/group.interface.ts

@@ -0,0 +1,149 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+import { ApiProperty } from '@midwayjs/swagger';
+import _ = require('lodash');
+import { FrameworkErrorEnum, SearchBase, ServiceError } from 'free-midway-component';
+import { getDataFromTarget } from '../util/util';
+import * as computedUtil from '../util/computed';
+export class FetchVO_group {
+  constructor(data: object) {
+    for (const key of Object.keys(this)) {
+      this[key] = _.get(data, key);
+    }
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: '商店id' })
+  'shop': string = undefined;
+  @ApiProperty({ description: '商品id' })
+  'goods': string = undefined;
+  @ApiProperty({ description: '团长' })
+  'leader': string = undefined;
+  @ApiProperty({ description: '人数限制' })
+  'person_limit': number = undefined;
+  @ApiProperty({ description: '团购设置' })
+  'group_config': Array<any> = undefined;
+  @ApiProperty({ description: '开始时间' })
+  'start_time': string = undefined;
+  @ApiProperty({ description: '结束时间' })
+  'end_time': string = undefined;
+  @ApiProperty({ description: '团状态' })
+  'status': string = undefined;
+}
+
+export class QueryDTO_group extends SearchBase {
+  constructor() {
+    const like_prop = ['goods'];
+    const props = ['shop', 'goods', 'leader', 'status'];
+    const mapping = { goods: 'goods.name', leader: 'leader_id' };
+    super({ like_prop, props, mapping });
+  }
+  @ApiProperty({ description: '商店id' })
+  'shop': string = undefined;
+  @ApiProperty({ description: '商品名称' })
+  'goods': string = undefined;
+  @ApiProperty({ description: '团长' })
+  'leader': string = undefined;
+  @ApiProperty({ description: '状态' })
+  'status': string = undefined;
+}
+
+export class QueryVO_group extends FetchVO_group {}
+
+export class CreateDTO_group {
+  @ApiProperty({ description: '商店id' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少商店id', FrameworkErrorEnum.NEED_BODY)))
+  'shop': string = undefined;
+  @ApiProperty({ description: '商品id' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少商品id', FrameworkErrorEnum.NEED_BODY)))
+  'goods': string = undefined;
+  @ApiProperty({ description: '团长' })
+  @Rule(RuleType['string']().empty())
+  'leader': string = undefined;
+  @ApiProperty({ description: '人数限制' })
+  @Rule(RuleType['number']().required().error(new ServiceError('缺少人数限制', FrameworkErrorEnum.NEED_BODY)))
+  'person_limit': number = undefined;
+  @ApiProperty({ description: '团购设置' })
+  @Rule(RuleType['array']().empty())
+  'group_config': Array<any> = undefined;
+  @ApiProperty({ description: '开始时间' })
+  @Rule(RuleType['string']().empty())
+  'start_time': string = undefined;
+  @ApiProperty({ description: '结束时间' })
+  @Rule(RuleType['string']().empty())
+  'end_time': string = undefined;
+  @ApiProperty({ description: '团状态' })
+  @Rule(RuleType['string']().empty())
+  'status': string = undefined;
+}
+
+export class CreateVO_group extends FetchVO_group {}
+
+export class UpdateDTO_group {
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty())
+  '_id'?: string = undefined;
+  @ApiProperty({ description: '商店id' })
+  @Rule(RuleType['string']().empty())
+  'shop'?: string = undefined;
+  @ApiProperty({ description: '商品id' })
+  @Rule(RuleType['string']().empty())
+  'goods'?: string = undefined;
+  @ApiProperty({ description: '团长' })
+  @Rule(RuleType['string']().empty())
+  'leader'?: string = undefined;
+  @ApiProperty({ description: '人数限制' })
+  @Rule(RuleType['number']().empty())
+  'person_limit'?: number = undefined;
+  @ApiProperty({ description: '团购设置' })
+  @Rule(RuleType['array']().empty())
+  'group_config'?: Array<any> = undefined;
+  @ApiProperty({ description: '开始时间' })
+  @Rule(RuleType['string']().empty())
+  'start_time'?: string = undefined;
+  @ApiProperty({ description: '结束时间' })
+  @Rule(RuleType['string']().empty())
+  'end_time'?: string = undefined;
+  @ApiProperty({ description: '团状态' })
+  @Rule(RuleType['string']().empty())
+  'status'?: string = undefined;
+}
+
+export class UpdateVO_group extends FetchVO_group {}
+export class UserListView {
+  constructor(data) {
+    const dl = ['_id', 'can_group', 'end_time', 'start_time', 'person_limit', 'status'];
+    for (const i of dl) this[i] = _.get(data, i);
+    this.goods = getDataFromTarget(data, 'goods', ['_id', 'name', 'file']);
+    this.shop = getDataFromTarget(data, 'shop', ['_id', 'name']);
+    const gc = _.minBy(data.group_config, 'price');
+    const group_price = _.get(gc, 'price', 0);
+    this.group_price = computedUtil.toNumber(group_price);
+    const sell_price = _.get(gc, 'spec.sell_money', 0);
+    this.sell_price = computedUtil.toNumber(sell_price);
+  }
+  _id: string;
+  end_time: string;
+  start_time: string;
+  person_limit: number;
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  shop: {
+    _id: string;
+    name: string;
+  };
+  status: string;
+  can_group: boolean;
+  group_price: number;
+  sell_price: number;
+}
+
+export class AqView extends UserListView {
+  constructor(data) {
+    super(data);
+    this.leader_name = _.get(data, 'leader.name');
+  }
+  leader_name: string;
+}

+ 210 - 0
src/interface/groupAfterSale.interface.ts

@@ -0,0 +1,210 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+import { ApiProperty } from '@midwayjs/swagger';
+import _ = require('lodash');
+import { FrameworkErrorEnum, SearchBase, ServiceError } from 'free-midway-component';
+import { getDataFromTarget } from '../util/util';
+export class FetchVO_groupAfterSale {
+  constructor(data: object) {
+    for (const key of Object.keys(this)) {
+      this[key] = _.get(data, key);
+    }
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: '订单' })
+  'order': string = undefined;
+  @ApiProperty({ description: '顾客' })
+  'customer': string = undefined;
+  @ApiProperty({ description: '售后类型' })
+  'type': string = undefined;
+  @ApiProperty({ description: '申请理由' })
+  'reason': string = undefined;
+  @ApiProperty({ description: '申请售后描述' })
+  'desc': string = undefined;
+  @ApiProperty({ description: '凭证图片' })
+  'file': Array<any> = undefined;
+  @ApiProperty({ description: '快递信息' })
+  'transport': object = undefined;
+  @ApiProperty({ description: '售后申请时间' })
+  'apply_time': string = undefined;
+  @ApiProperty({ description: '售后结束时间' })
+  'end_time': string = undefined;
+  @ApiProperty({ description: '售后状态' })
+  'status': string = undefined;
+  @ApiProperty({ description: '退款金额' })
+  'money': number = undefined;
+  @ApiProperty({ description: '处理人' })
+  'deal_person': string = undefined;
+}
+
+export class QueryDTO_groupAfterSale extends SearchBase {
+  constructor() {
+    const like_prop = [];
+    const props = ['order', 'customer', 'type', 'apply_time', 'end_time', 'deal_person', 'status'];
+    super({ like_prop, props });
+  }
+  @ApiProperty({ description: '订单' })
+  'order': string = undefined;
+  @ApiProperty({ description: '顾客' })
+  'customer': string = undefined;
+  @ApiProperty({ description: '售后类型' })
+  'type': string = undefined;
+  @ApiProperty({ description: '售后申请时间' })
+  'apply_time': string = undefined;
+  @ApiProperty({ description: '售后结束时间' })
+  'end_time': string = undefined;
+  @ApiProperty({ description: '处理人' })
+  'deal_person': string = undefined;
+  @ApiProperty({ description: '状态' })
+  'status': string = undefined;
+}
+
+export class QueryVO_groupAfterSale extends FetchVO_groupAfterSale {}
+
+export class CreateDTO_groupAfterSale {
+  @ApiProperty({ description: '订单' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY)))
+  'order': string = undefined;
+  @ApiProperty({ description: '顾客' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY)))
+  'customer': string = undefined;
+  @ApiProperty({ description: '售后类型' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少订单信息', FrameworkErrorEnum.NEED_BODY)))
+  'type': string = undefined;
+  @ApiProperty({ description: '申请理由' })
+  @Rule(RuleType['string']().empty())
+  'reason': string = undefined;
+  @ApiProperty({ description: '申请售后描述' })
+  @Rule(RuleType['string']().empty())
+  'desc': string = undefined;
+  @ApiProperty({ description: '凭证图片' })
+  @Rule(RuleType['array']().empty())
+  'file': Array<any> = undefined;
+  @ApiProperty({ description: '快递信息' })
+  @Rule(RuleType['object']().empty())
+  'transport': object = undefined;
+  @ApiProperty({ description: '售后申请时间' })
+  @Rule(RuleType['string']().empty())
+  'apply_time': string = undefined;
+  @ApiProperty({ description: '售后结束时间' })
+  @Rule(RuleType['string']().empty())
+  'end_time': string = undefined;
+  @ApiProperty({ description: '售后状态' })
+  @Rule(RuleType['string']().empty())
+  'status': string = undefined;
+  @ApiProperty({ description: '退款金额' })
+  @Rule(RuleType['number']().empty())
+  'money': number = undefined;
+  @ApiProperty({ description: '处理人' })
+  @Rule(RuleType['string']().empty())
+  'deal_person': string = undefined;
+}
+
+export class CreateVO_groupAfterSale extends FetchVO_groupAfterSale {}
+
+export class UpdateDTO_groupAfterSale extends CreateDTO_groupAfterSale {
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty())
+  '_id': string = undefined;
+  @ApiProperty({ description: '团长建议' })
+  @Rule(RuleType['string']().empty())
+  leader_suggest: boolean = undefined;
+}
+
+export class UpdateVO_groupAfterSale extends FetchVO_groupAfterSale {}
+
+export class AdminView {
+  constructor(data) {
+    const dl = ['_id', 'apply_time', 'end_time', 'type', 'reason', 'desc', 'file', 'status', 'transport', 'money', 'leader_suggest'];
+    for (const i of dl) this[i] = _.get(data, i);
+    this.customer = getDataFromTarget(data, 'customer', ['_id', 'name', 'phone']);
+    this.address = getDataFromTarget(data, 'order.address', ['_id', 'name', 'phone', 'province', 'city', 'area', 'address']);
+    this.shop = getDataFromTarget(data, 'order.shop', ['_id', 'name']);
+    this.goods = getDataFromTarget(data, 'order.goods', ['_id', 'name', 'file']);
+    this.goodsSpec = getDataFromTarget(data, 'order.goodsSpec', ['_id', 'name', 'file']);
+    let price = 0;
+    // const is_leader = _.get(data, 'customer.is_leader', '1');
+    // if (is_leader === '0') price = _.get(data, 'order.config.leader_price');
+    // else
+    price = _.get(data, 'order.config.price');
+    this.goodsSpec.price = price;
+    this.goodsSpec.freight = _.get(data, 'order.config.freight');
+    this.goodsSpec.num = _.get(data, 'order.num');
+    this.pay = _.get(data, 'order.pay.pay_money');
+    this.deal_person = getDataFromTarget(data, 'deal_person', ['_id', 'name']);
+  }
+  customer: {
+    _id: string;
+    name: string;
+    phone: string;
+  };
+  address: {
+    _id: string;
+    name: string;
+    phone: string;
+    province: string;
+    city: string;
+    area: string;
+    address: string;
+  };
+  shop: {
+    _id: string;
+    name: string;
+  };
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  goodsSpec: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+    num: number;
+    price: number;
+    freight: number;
+  };
+  total_detail: Array<any>;
+  apply_time: string;
+  end_time: string;
+  type: string;
+  reason: string;
+  desc: string;
+  deal_person: string;
+  file: Array<any>;
+  status: string;
+  transport: object;
+  money: number;
+  pay: number; // 订单支付总额
+  leader_suggest: boolean;
+}
+
+export class UserListView {
+  constructor(data) {
+    const dl = ['_id', 'apply_time', 'type', 'status', 'money', 'leader_suggest'];
+    for (const i of dl) this[i] = _.get(data, i);
+    this.shop = getDataFromTarget(data, 'order.shop', ['_id', 'name']);
+    this.goods = getDataFromTarget(data, 'order.goods', ['_id', 'name', 'file']);
+    this.spec = getDataFromTarget(data, 'order.goodsSpec', ['_id', 'name', 'file']);
+  }
+  _id: string;
+  apply_time: string;
+  shop: {
+    _id: string;
+    name: string;
+  };
+  type: string;
+  status: string;
+  money: number;
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  spec: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  leader_suggest: boolean;
+}

+ 385 - 0
src/interface/groupOrder.interface.ts

@@ -0,0 +1,385 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+import { ApiProperty } from '@midwayjs/swagger';
+import _ = require('lodash');
+import * as computedUtil from '../util/computed';
+import { FrameworkErrorEnum, SearchBase, ServiceError } from 'free-midway-component';
+import { getDataFromTarget } from '../util/util';
+const getTotalDetail = (data, price) => {
+  const num = _.get(data, 'num', 0);
+  const freight = _.get(data, 'config.freight', 0);
+  const total_detail = [];
+  const goods_total = computedUtil.multiply(num, price);
+  total_detail.push({ key: 'goods_total', zh: '商品总价', money: goods_total });
+  const freight_total = computedUtil.multiply(num, freight);
+  total_detail.push({ key: 'freight_total', zh: '运费总价', money: freight_total });
+  return total_detail;
+};
+export class FetchVO_groupOrder {
+  constructor(data: object) {
+    let price = 0;
+    // const is_leader = _.get(data, 'customer.is_leader', '1');
+    // if (is_leader === '0') price = _.get(data, 'config.leader_price');
+    // else
+    price = _.get(data, 'config.price');
+    for (const key of Object.keys(this)) {
+      if (key === 'goodsSpec') {
+        const kd = _.get(data, key);
+        kd.price = price;
+        this[key] = kd;
+      } else if (key === 'total_detail') {
+        this.total_detail = getTotalDetail(data, price);
+      } else this[key] = _.get(data, key);
+    }
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: 'customer' })
+  'customer': string = undefined;
+  @ApiProperty({ description: 'address' })
+  'address': object = undefined;
+  @ApiProperty({ description: 'goods' })
+  'goods': object = undefined;
+  @ApiProperty({ description: 'goodsSpec' })
+  'goodsSpec': object = undefined;
+  @ApiProperty({ description: 'buy_time' })
+  'buy_time': string = undefined;
+  @ApiProperty({ description: 'no' })
+  'no': string = undefined;
+  @ApiProperty({ description: 'status' })
+  'status': string = undefined;
+  @ApiProperty({ description: 'pay' })
+  'pay': object = undefined;
+  @ApiProperty({ description: 'group' })
+  'group': string = undefined;
+  @ApiProperty({ description: 'config' })
+  'config': object = undefined;
+  @ApiProperty({ description: '备注' })
+  'remarks': string = undefined;
+  @ApiProperty({ description: '快递类型' })
+  'transport_type': string = undefined;
+  @ApiProperty({ description: '快递信息' })
+  'transport': Array<any> = undefined;
+  @ApiProperty({ description: '是否有售后' })
+  'is_afterSale' = false;
+  @ApiProperty({ description: '购买数量' })
+  'num': number;
+  @ApiProperty({ description: '明细' })
+  'total_detail': Array<any>;
+}
+
+export class QueryDTO_groupOrder extends SearchBase {
+  constructor() {
+    const like_prop = ['goods', 'customer', 'customer_name'];
+    const props = ['_id', 'customer', 'address', 'goods', 'goodsSpec', 'buy_time', 'no', 'status', 'group', 'customer_name'];
+    const mapping = { goods: 'goods.name', customer: 'customer_id', customer_name: 'customer.name', group: 'group' };
+    super({ like_prop, props, mapping });
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: '顾客id' })
+  'customer': string = undefined;
+  @ApiProperty({ description: '顾客姓名' })
+  customer_name: string = undefined;
+  @ApiProperty({ description: 'address' })
+  'address': object = undefined;
+  @ApiProperty({ description: 'goods' })
+  'goods': string = undefined;
+  @ApiProperty({ description: 'goodsSpec' })
+  'goodsSpec': object = undefined;
+  @ApiProperty({ description: 'buy_time' })
+  'buy_time': string = undefined;
+  @ApiProperty({ description: 'no' })
+  'no': string = undefined;
+  @ApiProperty({ description: 'status' })
+  'status': string = undefined;
+  @ApiProperty({ description: 'group' })
+  'group': string = undefined;
+}
+
+export class QueryVO_groupOrder extends FetchVO_groupOrder {}
+
+export class CreateDTO_groupOrder {
+  // @ApiProperty({ description: 'customer' })
+  // @Rule(RuleType['string']().empty())
+  // 'customer': string = undefined;
+
+  @ApiProperty({ description: 'address' })
+  @Rule(RuleType['object']().required().error(new ServiceError('缺少配送地址', FrameworkErrorEnum.NEED_BODY)))
+  'address': object = undefined;
+
+  @ApiProperty({ description: '店铺' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少店铺信息', FrameworkErrorEnum.NEED_BODY)))
+  'shop': string = undefined;
+
+  @ApiProperty({ description: 'goods' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少商品信息', FrameworkErrorEnum.NEED_BODY)))
+  'goods': string = undefined;
+
+  @ApiProperty({ description: 'goodsSpec' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少规格信息', FrameworkErrorEnum.NEED_BODY)))
+  'goodsSpec': string = undefined;
+
+  @ApiProperty({ description: '购买数量' })
+  @Rule(RuleType['number']().required().error(new ServiceError('缺少购买数量', FrameworkErrorEnum.NEED_BODY)))
+  'num': number = undefined;
+
+  @ApiProperty({ description: 'group' })
+  @Rule(RuleType['string']().required().error(new ServiceError('缺少团信息', FrameworkErrorEnum.NEED_BODY)))
+  'group': string = undefined;
+  @ApiProperty({ description: '备注' })
+  @Rule(RuleType['string']().empty())
+  'remarks': string = undefined;
+  // @ApiProperty({ description: 'buy_time' })
+  // @Rule(RuleType['string']().empty())
+  // 'buy_time': string = undefined;
+  // @ApiProperty({ description: 'no' })
+  // @Rule(RuleType['string']().empty())
+  // 'no': string = undefined;
+  // @ApiProperty({ description: 'status' })
+  // @Rule(RuleType['string']().empty())
+  // 'status': string = undefined;
+  // @ApiProperty({ description: 'pay' })
+  // @Rule(RuleType['object']().empty())
+  // 'pay': object = undefined;
+
+  // @ApiProperty({ description: 'config' })
+  // @Rule(RuleType['object']().empty())
+  // 'config': object = undefined;
+}
+
+export class CreateVO_groupOrder extends FetchVO_groupOrder {}
+
+export class UpdateDTO_groupOrder extends CreateDTO_groupOrder {
+  @ApiProperty({ description: '状态', example: '2' })
+  @Rule(RuleType.string().empty())
+  status: string;
+  @Rule(RuleType['string']().empty())
+  'transport_type': string = undefined;
+  @ApiProperty({ description: '快递信息' })
+  @Rule(RuleType['array']().empty())
+  'transport': Array<any> = undefined;
+}
+
+export class UpdateVO_groupOrder extends FetchVO_groupOrder {
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty())
+  '_id': string = undefined;
+}
+
+export class toOrderPageDTO {
+  @ApiProperty({ description: '店铺id', example: '6333d71d32c5f69745f9bd32' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少店铺信息', FrameworkErrorEnum.NEED_BODY)))
+  shop: string;
+  @ApiProperty({ description: '商品id', example: '635b89c042e87c7a2880b484' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少商品信息', FrameworkErrorEnum.NEED_BODY)))
+  goods: string;
+  @ApiProperty({ description: '规格id', example: '635b89d742e87c7a2880b4bb' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少规格信息', FrameworkErrorEnum.NEED_BODY)))
+  goodsSpec: string;
+  @ApiProperty({ description: '购买数量', example: 1 })
+  @Rule(RuleType.number().required().error(new ServiceError('缺少购买数量', FrameworkErrorEnum.NEED_BODY)))
+  num: number;
+  @ApiProperty({ description: '团id', example: '638574d85df0386470559163' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少团信息', FrameworkErrorEnum.NEED_BODY)))
+  group: string;
+}
+
+export class AdminListView {
+  constructor(data) {
+    this._id = _.get(data, '_id');
+    this.no = _.get(data, 'no');
+    this.customer = _.get(data, 'customer.name');
+    this.goods = _.get(data, 'goods.name');
+    this.spec = _.get(data, 'goodsSpec.name');
+    this.pay = _.get(data, 'pay.pay_money');
+    this.num = _.get(data, 'num', 0);
+    this.is_afterSale = _.get(data, 'is_afterSale', false);
+    this.buy_time = _.get(data, 'buy_time');
+    this.address = getDataFromTarget(data, 'address', ['name', 'phone', 'province', 'city', 'area', 'address', '_id']);
+    this.status = _.get(data, 'status');
+    this.group_status = _.get(data, 'group.status');
+  }
+  _id: string;
+  no: string;
+  customer: string;
+  goods: string;
+  spec: string;
+  pay: number;
+  num: number;
+  buy_time: string;
+  is_afterSale: boolean;
+  address: string;
+  status: string;
+  group_status: string;
+}
+export class AdminView {
+  constructor(data) {
+    const dl = ['_id', 'is_afterSale', 'buy_time', 'remarks', 'transport_type', 'transport', 'status'];
+    for (const i of dl) this[i] = _.get(data, i);
+    this.address = _.pick(_.get(data, 'address', {}), ['name', 'phone', 'province', 'city', 'area', 'address']);
+    this.shop = _.pick(_.get(data, 'shop', {}), ['_id', 'name']);
+    this.goods = _.pick(_.get(data, 'goods', {}), ['_id', 'name', 'file']);
+    let price = 0;
+    // const is_leader = _.get(data, 'customer.is_leader', '1');
+    // if (is_leader === '0') price = _.get(data, 'config.leader_price');
+    // else
+    price = _.get(data, 'config.price');
+    const goodsSpec = _.pick(_.get(data, 'goodsSpec', {}), ['_id', 'name', 'file']);
+    goodsSpec.price = price;
+    goodsSpec.num = _.get(data, 'num', 0);
+    this.goodsSpec = goodsSpec;
+    this.pay = _.pick(_.get(data, 'pay', {}), ['pay_money']);
+
+    this.group_status = _.get(data, 'group.status');
+    this.total_detail = getTotalDetail(data, price);
+  }
+  _id: string;
+  address: {
+    name: string;
+    phone: string;
+    province: string;
+    city: string;
+    area: string;
+    address: string;
+  };
+  shop: {
+    _id: string;
+    name: string;
+  };
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  goodsSpec: {
+    _id: string;
+    name: string;
+    price: string;
+    num: number;
+    file: Array<any>;
+  };
+  is_afterSale = false;
+  total_detail: Array<any>;
+  buy_time: string;
+  remarks: string;
+  transport_type: string;
+  transport: Array<any>;
+  pay: {
+    pay_money: number;
+  };
+  status: string;
+  group_status: string;
+}
+export class UserListView {
+  constructor(data) {
+    this._id = _.get(data, '_id');
+    const shop = _.get(data, 'shop', {});
+    const shopData = _.pick(shop, ['_id', 'name']);
+    this.shop = shopData;
+
+    const goods = _.get(data, 'goods', {});
+    const goodsData = _.pick(goods, ['_id', 'name', 'file']);
+    this.goods = goodsData;
+
+    const spec = _.get(data, 'goodsSpec', {});
+    const specData = _.pick(spec, ['_id', 'name', 'file']);
+    // const is_leader = _.get(data, 'customer.is_leader', '1');
+    // if (is_leader === '0') specData.price = _.get(data, 'config.leader_price');
+    // else
+    specData.price = _.get(data, 'config.price');
+    this.spec = specData;
+
+    this.pay = _.get(data, 'pay.pay_money', 0);
+
+    const customer = _.get(data, 'customer', {});
+    const customerData = _.pick(customer, ['_id', 'name']);
+    this.customer = customerData;
+    this.buy_time = _.get(data, 'buy_time', 0);
+    this.num = _.get(data, 'num');
+    this.status = _.get(data, 'status');
+    this.is_afterSale = _.get(data, 'is_afterSale', false);
+    this.is_rate = _.get(data, 'is_rate', false);
+  }
+  _id: string;
+  shop: {
+    _id: string;
+    name: string;
+  };
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  spec: {
+    _id: string;
+    name: string;
+    price: number;
+    file: Array<any>;
+  };
+  pay: number;
+  customer: {
+    _id: string;
+    name: string;
+  };
+  buy_time: string;
+  num: number;
+  status: string;
+  is_afterSale: boolean;
+  is_rate: boolean;
+}
+
+export class UserView {
+  constructor(data) {
+    this._id = _.get(data, '_id');
+    this.address = _.get(data, 'address');
+    this.status = _.get(data, 'status');
+    const shop = _.get(data, 'shop', {});
+    const shopData = _.pick(shop, ['_id', 'name']);
+    this.shop = shopData;
+
+    const goods = _.get(data, 'goods', {});
+    const goodsData = _.pick(goods, ['_id', 'name', 'file']);
+    this.goods = goodsData;
+
+    const spec = _.get(data, 'goodsSpec', {});
+    const specData = _.pick(spec, ['_id', 'name', 'file']);
+    let price = 0;
+    // const is_leader = _.get(data, 'customer.is_leader', '1');
+    // if (is_leader === '0') price = _.get(data, 'config.leader_price');
+    // else
+    price = _.get(data, 'config.price');
+    specData.price = price;
+    this.spec = specData;
+
+    this.num = _.get(data, 'num', 0);
+    this.pay = _.get(data, 'pay.pay_money', 0);
+    this.no = _.get(data, 'no');
+    this.buy_time = _.get(data, 'buy_time');
+    this.remarks = _.get(data, 'remarks');
+    this.total_detail = getTotalDetail(data, price);
+  }
+  _id: string;
+  address: object;
+  status: string;
+  shop: {
+    _id: string;
+    name: string;
+  };
+  goods: {
+    _id: string;
+    name: string;
+    file: Array<any>;
+  };
+  spec: {
+    _id: string;
+    name: string;
+    price: number;
+    file: Array<any>;
+  };
+  num: number;
+  pay: number;
+  no: string;
+  buy_time: string;
+  remarks: string;
+  total_detail: Array<any>;
+}

+ 13 - 0
src/interface/pay.interface.ts

@@ -0,0 +1,13 @@
+export class PayData {
+  money: number;
+  openid: string;
+  order_no: string;
+  desc: string;
+}
+/**微信退款数据 */
+export class WxRefundData {
+  order_no!: string;
+  out_refund_no: string;
+  money: number;
+  reason?: string;
+}

+ 12 - 0
src/interface/view.interface.ts

@@ -0,0 +1,12 @@
+import { ApiProperty } from '@midwayjs/swagger';
+import { Rule, RuleType } from '@midwayjs/validate';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+
+export class GoodsDetailDTO {
+  @ApiProperty({ description: '商品id' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少商品信息', FrameworkErrorEnum.NEED_QUERY)))
+  id: string;
+  @ApiProperty({ description: '团id' })
+  @Rule(RuleType.string().required().error(new ServiceError('缺少团信息', FrameworkErrorEnum.NEED_QUERY)))
+  group: string;
+}

+ 33 - 0
src/middleware/checkToken.middleware.ts

@@ -0,0 +1,33 @@
+import { IMiddleware } from '@midwayjs/core';
+import { Middleware, Inject } from '@midwayjs/decorator';
+import { NextFunction, Context } from '@midwayjs/koa';
+import _ = require('lodash');
+import { JwtService } from '@midwayjs/jwt';
+
+@Middleware()
+export class CheckTokenMiddleware
+  implements IMiddleware<Context, NextFunction>
+{
+  @Inject()
+  jwtService: JwtService;
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      const token = _.get(ctx.request, 'header.token');
+      if (token) {
+        const data = this.jwtService.decodeSync(token);
+        if (data) ctx.user = data;
+      }
+      // 添加管理员身份
+      const adminToken = _.get(ctx.request, 'header.admin-token');
+      if (adminToken) {
+        const data = this.jwtService.decodeSync(adminToken);
+        if (data) ctx.admin = data;
+      }
+      await next();
+    };
+  }
+
+  static getName(): string {
+    return 'checkToken';
+  }
+}

+ 11 - 0
src/service/goodsConfig.service.ts

@@ -0,0 +1,11 @@
+import { Provide } from '@midwayjs/decorator';
+import { GoodsConfig } from '../entity/group/goodsConfig';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from 'free-midway-component';
+type modelType = ReturnModelType<typeof GoodsConfig>;
+@Provide()
+export class GoodsConfigService extends BaseService<modelType> {
+  @InjectEntityModel(GoodsConfig)
+  model: modelType;
+}

+ 254 - 0
src/service/group.service.ts

@@ -0,0 +1,254 @@
+import { Inject, Provide } from '@midwayjs/decorator';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { Group } from '../entity/group/group';
+import moment = require('moment');
+import { GroupOrder } from '../entity/group/groupOrder';
+import _ = require('lodash');
+import { lookUp } from '../util/pipeline.util';
+import { MqSender } from './mq/mqSender.service';
+import { Admin } from '../entity/base/admin';
+import { HttpServiceFactory } from '@midwayjs/axios';
+import { PopulateOptions } from 'mongoose';
+import * as computedUtil from '../util/computed';
+
+type modelType = ReturnModelType<typeof Group>;
+@Provide()
+export class GroupService extends BaseService<modelType> {
+  @InjectEntityModel(Group)
+  model: modelType;
+  @InjectEntityModel(GroupOrder)
+  orderModel: ReturnModelType<typeof GroupOrder>;
+  @InjectEntityModel(Admin)
+  adminModel: ReturnModelType<typeof Admin>;
+  @Inject()
+  httpServiceFactory: HttpServiceFactory;
+  @Inject()
+  mqSender: MqSender;
+
+  async toSendCustomerMsg(id) {
+    const group = await this.model.findById(id);
+    const os = await this.orderModel.find({ group: id, status: _.get(group, 'status', '1') }, { customer: 1, goods: 1 }).lean();
+    const gn = _.get(_.head(os), 'goods.name');
+    const customer = os.map(i => i.customer);
+    const content = `您参加的团购,商品为: ${gn} 团已拼团${_.get(group, 'status') === '1' ? '成功' : '失败'}`;
+    const obj = { customer, source: '2', source_id: id, time: moment().format('YYYY-MM-DD HH:mm:ss'), content };
+    const axios = this.httpServiceFactory.get('base');
+    await axios.post('/notice', obj);
+  }
+
+  /**发送系统消息 */
+  async sendSystemMsg(id) {
+    const refs = this.getRefs();
+    const group = await this.model
+      .findById(id, { leader: 1, goods: 1 })
+      .populate(refs as PopulateOptions[])
+      .lean();
+    if (!group) return;
+    const leader = _.get(group, 'leader');
+    const customer = [];
+    if (leader) customer.push(leader);
+    else {
+      // 没有团长,那就是管理员的,咋发还没想好,也先直接把管理员查出来塞进去吧
+      const admin = await this.adminModel.findOne({ account: 'sadmin' });
+      if (admin) customer.push(_.get(admin, '_id'));
+    }
+    if (customer.length <= 0) return;
+    const obj = { customer, source: '2', source_id: id, time: moment().format('YYYY-MM-DD HH:mm:ss'), content: `由您主导的团购,商品: ${_.get(group, 'goods.name')} 已临近截止时间,请及时进行操作` };
+    const axios = this.httpServiceFactory.get('base');
+    await axios.post('/notice', obj);
+  }
+
+  /** mq检查是否关团 */
+  async autoCheck(params) {
+    const id = _.get(params, 'id');
+    const start_time = _.get(params, 'start_time');
+    const end_time = _.get(params, 'end_time');
+    if (!id) throw new ServiceError('缺少团信息', FrameworkErrorEnum.NEED_PARAMS);
+    const group = await this.model.findById(id).lean();
+    if (!group) throw new ServiceError('未找到团信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const ost = _.get(group, 'start_time');
+    const oet = _.get(group, 'end_time');
+    if (ost !== start_time || oet !== end_time) throw new ServiceError('时间已修改,此判断无效', FrameworkErrorEnum.SERVICE_FAULT);
+    const status = _.get(group, 'status');
+    // 团已结束,不需要处理
+    if (status === '1') return;
+    // 团已经散了,不需要处理
+    else if (status === '-1') return;
+    // 准备中,不需要处理
+    else if (status === '2') return;
+    return true;
+  }
+  /**判断是否需要mq信息 */
+  async needMqMsg(data) {
+    const id = _.get(data, 'id');
+    const start_time = _.get(data, 'start_time');
+    const end_time = _.get(data, 'end_time');
+    const status = _.get(data, 'status');
+    if (!(start_time && end_time)) return;
+    if (!id) return;
+    const group = await this.model.findById(id);
+    if (!group) throw new ServiceError('未找到团信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    // 没有团信息,那就看原数据是什么
+    if (!status) {
+      const sg = _.get(group, 'status');
+      // 如果原数据也不是开团中,那就不管了
+      if (sg !== '0') return;
+    }
+    // 状态不是开团中,不需要管
+    else if (status !== '0') return;
+    const ost = _.get(group, 'start_time');
+    const oet = _.get(group, 'end_time');
+    if (ost !== start_time || oet !== end_time) {
+      // 需要重新发送mq消息,之前的消息会被判断失败
+      const msg = { id, start_time, end_time };
+      return msg;
+    }
+  }
+  /**发送mq消息 */
+  async toSendMsg(data) {
+    const start_time = _.get(data, 'start_time');
+    const end_time = _.get(data, 'end_time');
+    if (!(start_time && end_time)) return;
+    // mq作用改变,从原来关闭团,变为提示,所以需要提前一些时间,暂定提前3分钟
+    const ms = moment(end_time).subtract(3, 'm').diff(moment());
+    const obj = _.pick(data, ['id', 'start_time', 'end_time']);
+    if (!obj.id) obj.id = _.get(data, '_id');
+    await this.mqSender.groupMsg(obj, ms);
+  }
+
+  /**
+   * 检查当前团是否满足可以参团
+   * @param id 团id
+   */
+  async checkGroup(id) {
+    const g = await this.model.findById(id);
+    const result = { result: true, msg: '' };
+    // 找这个团
+    if (!g) {
+      result.result = false;
+      result.msg = '未找到指定团';
+      return result;
+    }
+    const { status, start_time, end_time, person_limit } = g;
+    // 看这个团的状态
+    if (status !== '0') {
+      result.result = false;
+      if (status === '2') result.msg = '团正在准备中';
+      else if (status === '-1') result.msg = '已散团';
+      else if (status === '1') result.msg = '团购结束';
+      return result;
+    }
+    const r = moment().isBetween(start_time, end_time, null, '[]');
+    // 再看当前时间
+    if (!r) {
+      result.result = false;
+      result.msg = '当前时间不在该团开团时间内';
+      return result;
+    }
+    // 再看已经有几单了
+    const num = await this.orderModel.count({
+      group: id,
+      status: '1',
+    });
+    if (num >= person_limit) {
+      result.result = false;
+      result.msg = '团已满人';
+      return result;
+    }
+    return result;
+  }
+
+  async userListView(filter, pageOptions = {}) {
+    const dup = _.cloneDeep(filter.getFilter());
+    const refs = this.getRefs();
+    const pipeline = [];
+    for (const ref of refs) {
+      const path = _.get(ref, 'path');
+      const modelName = _.lowerFirst(_.get(ref, 'model.modelName'));
+      if (!path) continue;
+      const arr = lookUp(path, modelName);
+      if (path === 'leader') {
+        arr.pop();
+      }
+      pipeline.push(...arr);
+    }
+    pipeline.push({ $match: dup });
+    if (_.get(pageOptions, 'sort')) pipeline.push({ $sort: _.get(pageOptions, 'sort') });
+    const qp = _.cloneDeep(pipeline);
+    qp.push({ $addFields: { group: { $toString: '$_id' } } });
+    qp.push({
+      $lookup: {
+        from: 'groupOrder',
+        localField: 'group',
+        foreignField: 'group',
+        pipeline: [{ $match: { status: '1' } }, { $project: { _id: 1 } }],
+        as: 'orders',
+      },
+    });
+    qp.push({
+      $addFields: {
+        can_group: {
+          $cond: {
+            if: { $eq: ['$person_limit', { $size: '$orders' }] },
+            then: true,
+            else: false,
+          },
+        },
+      },
+    });
+    if (_.get(pageOptions, 'skip')) qp.push({ $skip: _.get(pageOptions, 'skip') });
+    if (_.get(pageOptions, 'limit')) qp.push({ $limit: _.get(pageOptions, 'limit') });
+    const data = await this.model.aggregate(qp);
+    const totalResult = await this.model.aggregate([...pipeline, { $count: 'total' }]);
+    const total = _.get(_.head(totalResult), 'total', 0);
+    return { data, total };
+  }
+
+  async aggQuery(filter, { skip, limit }) {
+    const { shop, shop_name, goods, goods_name, leader, leader_name, status, start_time, end_time } = filter;
+    const pipeline = [];
+    const step1: any = {};
+    if (shop) step1.shop = shop;
+    if (goods) step1.goods = goods;
+    if (leader) step1.leader = leader;
+    if (status) step1.status = status;
+    if (start_time) step1.start_time = { $gte: start_time };
+    if (end_time) step1.end_time = { $lte: end_time };
+    if (Object.keys(step1).length > 0) pipeline.push({ $match: step1 });
+    const step2: any = [];
+    const refs = this.getRefs();
+    for (const ref of refs) {
+      const path = _.get(ref, 'path');
+      const modelName = _.lowerFirst(_.get(ref, 'model.modelName'));
+      if (!path) continue;
+      const arr = lookUp(path, modelName);
+      step2.push(...arr);
+    }
+    pipeline.push(...step2);
+    const step3: any = {};
+    if (shop_name) step3['shop.name'] = new RegExp(shop_name);
+    if (goods_name) step3['goods.name'] = new RegExp(goods_name);
+    if (leader_name) step3['leader.name'] = new RegExp(leader_name);
+    if (Object.keys(step3).length > 0) pipeline.push({ $match: step3 });
+    const qp = _.cloneDeep(pipeline);
+    if (limit) {
+      qp.push({ $skip: parseInt(skip) });
+      qp.push({ $limit: parseInt(limit) });
+    }
+    const data = await this.model.aggregate(qp);
+    for (const i of data) {
+      const gc = _.minBy(i.group_config, 'price');
+      const group_price = _.get(gc, 'price', 0);
+      i.group_price = computedUtil.toNumber(group_price);
+      const sell_price = _.get(gc, 'spec.sell_money', 0);
+      i.sell_price = computedUtil.toNumber(sell_price);
+    }
+    const tp = _.cloneDeep(pipeline);
+    tp.push({ $count: 'total' });
+    const tr = await this.model.aggregate(tp);
+    const total = _.get(_.head(tr), 'total', 0);
+    return { data, total };
+  }
+}

+ 141 - 0
src/service/groupAfterSale.service.ts

@@ -0,0 +1,141 @@
+import { Inject, Provide } from '@midwayjs/decorator';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { GroupAfterSale } from '../entity/group/groupAfterSale';
+import { GroupOrder } from '../entity/group/groupOrder';
+import { GroupOrderService } from './groupOrder.service';
+import _ = require('lodash');
+import * as computedUtil from '../util/computed';
+import moment = require('moment');
+import { CashBack } from '../entity/base/cashBack';
+import { randomStr } from '../util/util';
+import { Shop } from '../entity/base/shop';
+import { ShopInBillService } from './shopInBill.service';
+type modelType = ReturnModelType<typeof GroupAfterSale>;
+@Provide()
+export class GroupAfterSaleService extends BaseService<modelType> {
+  @InjectEntityModel(GroupAfterSale)
+  model: modelType;
+
+  @InjectEntityModel(GroupOrder)
+  orderModel: ReturnModelType<typeof GroupOrder>;
+
+  @InjectEntityModel(Shop)
+  shopModel: ReturnModelType<typeof Shop>;
+
+  @Inject()
+  groupOrderService: GroupOrderService;
+  @InjectEntityModel(CashBack)
+  cashBackModel: ReturnModelType<typeof CashBack>;
+
+  @Inject()
+  shopInBillService: ShopInBillService;
+
+  async create(body: object): Promise<object> {
+    const order = _.get(body, 'order');
+    const type = _.get(body, 'type');
+    // 1.校验数据是否存在
+    const o = await this.groupOrderService.fetch(order);
+    if (!o) throw new ServiceError('未找到订单信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    // 2.再校验订单是否在处理售后
+    const hasData = await this.model.count({ order, status: { $nin: ['!1', '!2', '!3', '!4', '!5'] } });
+    if (hasData > 0) throw new ServiceError('该商品已有正在处理中的售后申请.请勿重复申请', FrameworkErrorEnum.SERVICE_FAULT);
+    if (type === '4' || type === '5') {
+      const moneyDetail = await this.groupOrderService.moneyDetail(o);
+      const realPay = _.get(moneyDetail, 'total');
+      const obj = { money: realPay, desc: type === '4' ? '取消订单' : '拒收商品' };
+      Object.assign(body, obj);
+    }
+    if (!_.get(body, 'shop')) Object.assign(body, { shop: _.get(o, 'shop._id', _.get(o, 'shop')) });
+    const data = await this.model.create(body);
+    return data;
+  }
+
+  refundOrder(id, tran) {
+    tran.update('GroupOrder', { _id: id }, { status: '-4' });
+  }
+  /**
+   * 整单退
+   * @param data 售后数据
+   * @param tran 事务实例
+   */
+  async returnOrder(data, tran) {
+    const order_id = _.get(data, 'order');
+    const order = await this.orderModel.findById(order_id);
+    if (!order) throw new ServiceError('未找到订单数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    let returnMoney = _.get(data, 'money');
+    if (!returnMoney || computedUtil.minus(_.get(data, 'money'), _.get(data, 'pay.pay_money')) < 0) returnMoney = _.get(data, 'pay.pay_money');
+    const order_no = _.get(order, 'pay.pay_no');
+    const str = randomStr();
+    const out_refund_no = `${order_no}-r-${str}`;
+    const refundInfo = { reason: _.get(data, 'desc', '购物退款'), money: returnMoney, order_no, out_refund_no };
+    return refundInfo;
+  }
+
+  /**
+   * 退钱
+   * @param data 售后数据
+   * @param body 要修改成的数据
+   * @param tran 事务实例
+   */
+  async toReturnMoney(data, body, tran) {
+    const order_id = _.get(data, 'order._id');
+    const order = await this.orderModel.findById(order_id);
+    if (!order) throw new ServiceError('未找到订单数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    // 组织退款信息
+    let returnMoney = _.get(body, 'money');
+    // 如果在要修改的数据中没有金额,那就直接去找原售后数据中的退款金额
+    if (!returnMoney) returnMoney = _.get(data, 'money');
+    // 再判断,退款金额是否超过支付金额
+    const real_pay = _.get(order, 'pay.pay_money');
+    // 退款金额超过实际支付金额,那就使用支付金额作为退款金额
+    if (computedUtil.minus(returnMoney, real_pay) > 0) returnMoney = real_pay;
+    const order_no = _.get(order, 'pay.pay_no');
+    const str = randomStr();
+    const out_refund_no = `${order_no}-r-${str}`;
+    const refundInfo = { reason: _.get(data, 'desc', '购物退款'), money: returnMoney, order_no, out_refund_no };
+    // 组织出账信息
+    // 先找入账记录,有入账,再出账,否则干扣了
+    // 记录退款金额
+    let refundMoney = _.cloneDeep(returnMoney);
+    const inBill = await this.cashBackModel.findOne({ source: '2', source_id: order._id });
+    if (inBill) {
+      const num = await this.cashBackModel.count({ source: '-2', source_id: data._id });
+      if (num <= 0) {
+        // 有入账信息,看入账信息中的佣金是否能抵消退款金额
+        const get = _.get(inBill, 'money');
+        // 团长需要出账的金额
+        let needRefundForBill = 0;
+        // 如果退款金额 比 团长入账的还多, 那就只能退团长入账的金额
+        if (computedUtil.minus(refundMoney, get) > 0) needRefundForBill = get;
+        // 反之,直接退
+        else needRefundForBill = refundMoney;
+        refundMoney = computedUtil.minus(refundMoney, get);
+        const cashBackData = {
+          inviter: _.get(inBill, 'inviter'),
+          money: needRefundForBill,
+          time: moment().format('YYYY-MM-DD HH:mm:ss'),
+          status: '-1',
+          source: '-2',
+          source_id: data._id,
+        };
+        tran.insert('CashBack', cashBackData);
+      }
+    }
+    if (refundMoney > 0) {
+      // 团长的佣金退完了,但是退款的钱仍没有退完,需要从店铺中再退
+      await this.shopInBillService.createByAfterSale(data, refundMoney, tran);
+    }
+    //
+    return refundInfo;
+  }
+
+  async addInfo(data) {
+    const shop = _.get(data, 'order.shop');
+    if (!shop) return data;
+    const sd = await this.shopModel.findById(shop).lean();
+    if (sd) data.order.shop = sd;
+    return data;
+  }
+}

+ 388 - 0
src/service/groupOrder.service.ts

@@ -0,0 +1,388 @@
+import { Inject, Provide } from '@midwayjs/decorator';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { GroupOrder } from '../entity/group/groupOrder';
+import { Shop } from '../entity/base/shop';
+import { toOrderPageDTO } from '../interface/groupOrder.interface';
+import { Goods } from '../entity/base/goods';
+import { GoodsSpec } from '../entity/base/goodsSpec';
+import { Group } from '../entity/group/group';
+import { GoodsConfig } from '../entity/group/goodsConfig';
+import _ = require('lodash');
+import moment = require('moment');
+import { RedisService } from '@midwayjs/redis';
+import * as computedUtil from '../util/computed';
+import { PopulateOptions, Types } from 'mongoose';
+import { CashBack } from '../entity/base/cashBack';
+import { User } from '../entity/base/user';
+import { HttpServiceFactory } from '@midwayjs/axios';
+import { randomStr } from '../util/util';
+import { GroupAfterSale } from '../entity/group/groupAfterSale';
+import { lookUp } from '../util/pipeline.util';
+import { GoodsConfigService } from './goodsConfig.service';
+const ObjectId = Types.ObjectId;
+type modelType = ReturnModelType<typeof GroupOrder>;
+@Provide()
+export class GroupOrderService extends BaseService<modelType> {
+  @Inject()
+  httpServiceFactory: HttpServiceFactory;
+  @InjectEntityModel(GroupOrder)
+  model: modelType;
+
+  @Inject()
+  goodsConfigService: GoodsConfigService;
+
+  @InjectEntityModel(Shop)
+  shopModel: ReturnModelType<typeof Shop>;
+
+  @InjectEntityModel(Goods)
+  goodsModel: ReturnModelType<typeof Goods>;
+  @InjectEntityModel(GoodsSpec)
+  goodsSpecModel: ReturnModelType<typeof GoodsSpec>;
+  @InjectEntityModel(Group)
+  groupModel: ReturnModelType<typeof Group>;
+  @InjectEntityModel(GoodsConfig)
+  goodsConfigModel: ReturnModelType<typeof GoodsConfig>;
+  @InjectEntityModel(CashBack)
+  cashBackModel: ReturnModelType<typeof CashBack>;
+  @InjectEntityModel(User)
+  userModel: ReturnModelType<typeof User>;
+  @InjectEntityModel(GroupAfterSale)
+  afterSaleModel: ReturnModelType<typeof GroupAfterSale>;
+
+  @Inject()
+  redis: RedisService;
+
+  async checkGroupSuccess(id) {
+    const data = await this.model.findById(id, { group: 1 });
+    const group = await this.groupModel.findById(_.get(data, 'group')).lean();
+    return _.get(group, 'status') === '1';
+  }
+
+  /**
+   * 给团长提成
+   * @param id 订单id
+   * @param tran 事务实例
+   */
+  async leaderCommission(id, tran) {
+    const order = await this.model.findById(id);
+    if (!order) throw new ServiceError('未找到订单数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const { num, config, group } = order;
+    const money = computedUtil.multiply(num, _.get(config, 'leader_get'));
+    const time = moment().format('YYYY-MM-DD HH:mm:ss');
+    const source = '2';
+    const source_id = id;
+    const gd = await this.groupModel.findById(group);
+    if (!gd) throw new ServiceError('未找到团信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const inviter = _.get(gd, 'leader');
+    if (!inviter) return;
+    const cashBackData = { inviter, money, time, source, source_id };
+    // 检查是否入账,不要重复入账
+    const cbn = await this.cashBackModel.count({ source, source_id });
+    if (cbn > 0) throw new ServiceError('改订单已经入账', FrameworkErrorEnum.SERVICE_FAULT);
+    // 0元不入账
+    if (money > 0) tran.insert('CashBack', cashBackData);
+    return money;
+  }
+
+  /**
+   * 使用事务存储数据
+   * @param data 数据
+   * @param tran 事务
+   */
+  tranCreate(data, tran) {
+    return tran.insert('goodsOrder', data);
+  }
+  /**
+   * 组织创建订单数据
+   * @param data 接口数据
+   * @param customer 用户id
+   */
+  async toMakeOrderData(data, customer) {
+    data.customer = customer;
+    const result = _.cloneDeep(data);
+    const goods = await this.goodsModel.findById(data.goods).lean();
+    if (!goods) throw new ServiceError('未找到指定商品,订单创建失败', FrameworkErrorEnum.SERVICE_FAULT);
+    result.goods = goods;
+    const goodsSpec = await this.goodsSpecModel.findById(data.goodsSpec).lean();
+    if (!goodsSpec) throw new ServiceError('未找到指定商品规格,订单创建失败', FrameworkErrorEnum.SERVICE_FAULT);
+    result.goodsSpec = goodsSpec;
+    // 找到当前团设置的商品规格
+    const gpd = await this.groupModel.findById(data.group).lean();
+    if (!gpd) throw new ServiceError('未找到团信息');
+    const goodsConfigs = _.get(gpd, 'group_config', []);
+    const goodsConfig = goodsConfigs.find(f => _.isEqual(_.get(f, 'spec._id'), data.goodsSpec));
+    result.config = goodsConfig;
+    result.buy_time = moment().format('YYYY-MM-DD HH:mm:ss');
+    result.no = this.getNo();
+    const moneyDetail = await this.moneyDetail(result);
+    const user = await this.userModel.findById(customer, { openid: 1 }).lean();
+    const pay_money = _.get(moneyDetail, 'total');
+    const pay = {
+      pay_money,
+      openid: _.get(user, 'openid'),
+    };
+    result.pay = pay;
+    return result;
+  }
+
+  /**
+   * 减库存
+   * @param data 订单数据
+   * @param tran 事务实例
+   */
+  async dealGoods(data, tran) {
+    const { goodsSpec, num } = data;
+    const newNum = computedUtil.minus(_.get(goodsSpec, 'num'), num);
+    tran.update('GoodsSpec', { _id: _.get(goodsSpec, '_id') }, { num: newNum });
+  }
+  /**
+   * 还原库存
+   * @param data 订单数据
+   * @param tran 事务实例
+   */
+  async returnGoods(data, tran) {
+    const { goodsSpec, num } = data;
+    const newNum = computedUtil.plus(_.get(goodsSpec, 'num'), num);
+    tran.update('GoodsSpec', { _id: _.get(goodsSpec, '_id') }, { num: newNum });
+  }
+  /**
+   * 加销量
+   * @param goods 商品id
+   * @param num 数量
+   * @param tran 事务实例
+   */
+  async addSell(goods: string, num: number, tran) {
+    const gd = await this.goodsModel.findById(goods, { sell_num: 1 }).lean();
+    if (!gd) throw new ServiceError('未找到商品信息', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const { sell_num } = gd;
+    const newSellNum = computedUtil.plus(sell_num, num);
+    tran.update('Goods', { _id: _.get(goods, '_id') }, { sell_num: newSellNum });
+  }
+
+  /**
+   * 组织成页面数据
+   ** 因为要新写页面,所以这个地方的数据变得简单点
+   ** 团购规定是只能买一种商品且是1个规格.
+   ** 所以只有数量 和 优惠有变化(优惠取决于是否可以使用优惠券,先不做TODO标记下)
+   ** 数据返回简单点
+   * @param data 缓存数据(检查是否可以购买的数据)
+   */
+  async getOrderPageData(data: toOrderPageDTO) {
+    const { shop, goods, goodsSpec, num, group } = data;
+    const sd = await this.shopModel.findById(shop, { name: 1 }).lean();
+    const gd = await this.goodsModel.findById(goods, { name: 1, file: 1 }).lean();
+    const gsd = await this.goodsSpecModel.findById(goodsSpec, { name: 1, freight: 1 }).lean();
+    Object.assign(gsd, { freight: computedUtil.toNumber(gsd.freight) });
+    const gpd = await this.groupModel.findById(group).lean();
+    const goodsConfigs = _.get(gpd, 'group_config', []);
+    const goodsConfig = goodsConfigs.find(f => _.isEqual(_.get(f, 'spec._id'), goodsSpec));
+    // 团购中,团长也按团员价格走
+    // const leader = _.get(this.ctx, 'user.is_leader', '1');
+    const customer = _.get(this.ctx, 'user._id');
+    let price = 0;
+    // if (leader === '0') price = _.get(goodsConfig, 'leader_price');
+    // else
+    price = _.get(goodsConfig, 'price');
+    const freight = _.get(goodsConfig, 'freight');
+    Object.assign(gsd, { price, freight });
+    // 获取默认地址
+    const axios = this.httpServiceFactory.get('base');
+    const result = await axios.get(`/address?customer=${customer}&is_default=1`);
+    const address = _.head(result.data);
+    // 计算total_detail
+    const total_detail = [];
+    const goods_total = computedUtil.multiply(num, price);
+    total_detail.push({ key: 'goods_total', zh: '商品总价', money: goods_total });
+    const freight_total = computedUtil.multiply(num, freight);
+    total_detail.push({ key: 'freight_total', zh: '运费总价', money: freight_total });
+    return { shop: sd, goods: gd, goodsSpec: gsd, group, num, address, total_detail };
+  }
+
+  /**
+   * 1.检查是否可以购买商品-检查店铺
+   * @param data 前端数据
+   */
+  async checkCanBuy_shop(data: toOrderPageDTO) {
+    const { shop } = data;
+    const num = await this.shopModel.count({ _id: shop, status: '1' });
+    if (!num) throw new ServiceError('该店铺不处于营业状态', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 2.检查是否可以购买商品-检查商品
+   * @param data 前端数据
+   */
+  async checkCanBuy_goods(data: toOrderPageDTO) {
+    const { goods } = data;
+    const num = await this.goodsModel.count({ _id: goods, status: '1' }); // , status: '0'
+    if (!num) throw new ServiceError('该商品已下架', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 3.检查是否可以购买商品-检查商品规格
+   * @param data 前端数据
+   */
+  async checkCanBuy_goodsSpec(data: toOrderPageDTO) {
+    const { goodsSpec } = data;
+    const gs = await this.goodsSpecModel.findById(goodsSpec).lean();
+    if (!gs) throw new ServiceError('未找到指定规格', FrameworkErrorEnum.SERVICE_FAULT);
+    const num = _.get(gs, 'num', 0);
+    const buy_num = _.get(data, 'num', 0);
+    if (num < buy_num) throw new ServiceError('规格库存不足', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 4.检查是否可以购买商品-检查团
+   * @param data 前端数据
+   */
+  async checkCanBuy_group(data: toOrderPageDTO) {
+    const { group } = data;
+    const g = await this.groupModel.findById(group);
+    // 找这个团
+    if (!g) throw new ServiceError('未找到指定团', FrameworkErrorEnum.SERVICE_FAULT);
+    const { status, start_time, end_time, person_limit } = g;
+    // 看这个团的状态
+    if (status !== '0') throw new ServiceError('团已结束', FrameworkErrorEnum.SERVICE_FAULT);
+    const r = moment().isBetween(start_time, end_time, null, '[]');
+    // 再看当前时间
+    if (!r) throw new ServiceError('当前时间不在该团开团时间内', FrameworkErrorEnum.SERVICE_FAULT);
+    // 再看已经有几单了
+    const num = await this.model.count({
+      group,
+      'pay.result': { $exists: true },
+    });
+    if (num > person_limit) throw new ServiceError('团已满人', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 5.检查指定规格是否在团购设置中存在
+   * @param data 前端数据
+   */
+  async checkCanBuy_specInGroupConfig(data: toOrderPageDTO) {
+    const { group, goods, goodsSpec, num } = data;
+    const refs = this.goodsConfigService.getRefs();
+    const gc = await this.goodsConfigModel
+      .find({ group, goods })
+      .populate(refs as PopulateOptions[])
+      .lean();
+    if (gc.length <= 0) throw new ServiceError('未找到商品参加团购设置', FrameworkErrorEnum.SERVICE_FAULT);
+    const config = gc.find(f => new ObjectId(_.get(f, 'spec._id')).toString() === goodsSpec);
+    if (!config) throw new ServiceError('未找到规格参加团购设置', FrameworkErrorEnum.SERVICE_FAULT);
+    // 检查是否有购买限制
+    const buy_limit = _.get(config, 'buy_limit', _.get(config, 'spec.buy_limit'));
+    // 没有购买限制或者购买限制为无限制,则不进行后续检查
+    if (!buy_limit || buy_limit === '0') return;
+    const limit_num = _.get(config, 'limit_num', _.get(config, 'spec.limit_num', 0));
+    if (buy_limit === '1') {
+      if (num < limit_num) throw new ServiceError(`该规格至少购买 ${limit_num} 份, 购买数量不足`, FrameworkErrorEnum.SERVICE_FAULT);
+    } else if (buy_limit === '2') {
+      if (num > limit_num) throw new ServiceError(`该规格至多购买 ${limit_num} 份, 购买数量超过上限`, FrameworkErrorEnum.SERVICE_FAULT);
+    } else if (buy_limit === '3') {
+      if (num !== limit_num) throw new ServiceError(`该规格只能购买 ${limit_num} 份`, FrameworkErrorEnum.SERVICE_FAULT);
+    }
+  }
+
+  /**
+   * 缓存进redis
+   * @param data 缓存数据
+   */
+  async makeCache(data) {
+    const str = randomStr();
+    const orderKey = this.app.getConfig('redisKey.orderKeyPrefix');
+    const timeout = this.app.getConfig('redisTimeout');
+    const key = `${orderKey}${str}`;
+    const value = JSON.stringify(data);
+    await this.redis.set(key, value, 'EX', timeout);
+    return str;
+  }
+  /**
+   * 获取缓存内容
+   * @param key redis的key
+   * @returns 缓存数据
+   */
+  async getCache(key) {
+    const orderKey = this.app.getConfig('redisKey.orderKeyPrefix');
+    key = `${orderKey}${key}`;
+    const data = await this.redis.get(key);
+    await this.redis.del(key);
+    if (data) return JSON.parse(data);
+  }
+
+  /** 获取订单号 */
+  getNo(): string {
+    const str = randomStr();
+    const no = `TEHQG${moment().format('YYYYMMDDHHmmss')}-${str}`;
+    return no;
+  }
+  /**
+   * 计算价格明细,因为只有一个商品,所以直接就是一个数
+   * @param data 含有规格及其团购设置的对象,购买数量
+   * @property {object} goodsSpec 商品规格
+   * @property {object} config 规格的团购设置
+   * @property {number} num 购买数量
+   */
+  async moneyDetail(data): Promise<object> {
+    const { goodsSpec, config, num, customer } = data;
+    const { _id } = goodsSpec;
+    const { spec = {} } = config;
+    if (!new Types.ObjectId(_id).equals(spec._id)) throw new ServiceError('价格明细处理报错:不是同一个规格', FrameworkErrorEnum.SERVICE_FAULT);
+    // 如果当前用户是团长身份,则以团长价格购买,反之以团员价格购买
+    // const leader = _.get(this.ctx, 'user.is_leader', '1');
+    // const user = await this.userModel.findById(customer);
+    // 团购中,团长也按照团员价格走
+    // const leader = _.get(user, 'is_leader', '1');
+    let price = 0;
+    // if (leader === '0') price = _.get(config, 'leader_price');
+    // else
+    price = _.get(config, 'price');
+    const gt = computedUtil.multiply(price, num);
+    const ft = computedUtil.multiply(_.get(config, 'freight'), num);
+    const obj = { price, gt, ft, num, total: computedUtil.plus(gt, ft) };
+    return obj;
+  }
+
+  /**
+   * 非id单查询
+   * @param query 查询条件
+   */
+  async findOne(query: object) {
+    const data = await this.model.findOne(query).lean();
+    return data;
+  }
+  /**
+   * 为数据填充内容:
+   ** is_afterSale:是否存在售后
+   ** is_rate:是否评价(暂时不加)
+   * @param data 数据
+   */
+  async addInfo(data) {
+    const asn = await this.afterSaleModel.count({ order: data._id, status: { $nin: ['-1', '!1', '-2', '!2', '-3', '!3', '-4', '!4', '-5', '!5'] } });
+    data.is_afterSale = asn > 0;
+
+    return data;
+  }
+
+  async userListView(filter, pageOptions = {}) {
+    const dup = _.cloneDeep(filter.getFilter());
+    const refs = this.getRefs();
+    const pipeline = [];
+    for (const ref of refs) {
+      const path = _.get(ref, 'path');
+      const modelName = _.lowerFirst(_.get(ref, 'model.modelName'));
+      if (!path) continue;
+      const arr = lookUp(path, modelName);
+      pipeline.push(...arr);
+    }
+    pipeline.push({ $match: dup });
+    if (_.get(pageOptions, 'sort')) pipeline.push({ $sort: _.get(pageOptions, 'sort') });
+    const qp = _.cloneDeep(pipeline);
+    if (_.get(pageOptions, 'skip')) qp.push({ $skip: _.get(pageOptions, 'skip') });
+    if (_.get(pageOptions, 'limit')) qp.push({ $limit: _.get(pageOptions, 'limit') });
+    const data = await this.model.aggregate(qp);
+    const totalResult = await this.model.aggregate([...pipeline, { $count: 'total' }]);
+    const total = _.get(_.head(totalResult), 'total', 0);
+    return { data, total };
+  }
+}

+ 72 - 0
src/service/mq/mqConsumer.service.ts

@@ -0,0 +1,72 @@
+import { Inject, App, Config, Provide, ScopeEnum, Scope, Init, Autoload } from '@midwayjs/decorator';
+import { Context, Application } from '@midwayjs/rabbitmq';
+import * as amqp from 'amqp-connection-manager';
+import { ConsumeMessage } from 'amqplib';
+import _ = require('lodash');
+import { PayController } from '../../controller/pay.controller';
+import { GroupService } from '../group.service';
+import { GroupController } from '../../controller/group.controller';
+@Autoload()
+@Provide()
+@Scope(ScopeEnum.Request, { allowDowngrade: true })
+export class MqConsumer {
+  @App()
+  app: Application;
+
+  @Inject()
+  ctx: Context;
+
+  @Config('rabbitmq.url')
+  mqUrl: object;
+
+  @Config('mqConfig.dead')
+  mqDeadConfig: object;
+  @Inject()
+  payController: PayController;
+
+  @Inject()
+  groupService: GroupService;
+
+  @Inject()
+  groupController: GroupController;
+
+  /**mq连接 */
+  conn;
+  /**订单频道 */
+  order_ch;
+  @Init()
+  async init() {
+    this.conn = await amqp.connect(this.mqUrl);
+    this.order_ch = this.conn.createChannel({
+      json: true,
+      setup: ch => {
+        return Promise.all([
+          ch.assertExchange(_.get(this.mqDeadConfig, 'ex'), 'direct', { durable: true }),
+          ch.assertQueue(_.get(this.mqDeadConfig, 'q'), { durable: true, exclusive: false }),
+          ch.bindQueue(_.get(this.mqDeadConfig, 'd'), _.get(this.mqDeadConfig, 'ex'), _.get(this.mqDeadConfig, 'rk')),
+          ch.consume(_.get(this.mqDeadConfig, 'q'), msg => this.dealOrder.call(this, msg), { noAck: true }),
+        ]);
+      },
+    });
+  }
+
+  /**
+   * 处理死信信息,订单过期
+   * @param data mq数据
+   */
+  async dealOrder(data: ConsumeMessage) {
+    const str = data.content.toString();
+    const obj = JSON.parse(str);
+    const type = _.get(obj, 'type');
+    const params = _.omit(obj, ['type']);
+    if (type === 'order') await this.payController.cancel(params);
+    else if (type === 'group') {
+      const id = _.get(params, 'id');
+      // 先查询该团是否满足开团的条件了.满足的可以直接走
+      const result = await this.groupService.autoCheck(params);
+      // 没有异常,结果不是布尔值,说明不需要处理/参数错误,不予处理
+      if (!_.isBoolean(result)) return;
+      else await this.groupService.sendSystemMsg(id);
+    }
+  }
+}

+ 71 - 0
src/service/mq/mqSender.service.ts

@@ -0,0 +1,71 @@
+import { App, Provide, Scope, ScopeEnum, Init, Autoload, Destroy, Config } from '@midwayjs/decorator';
+import * as amqp from 'amqp-connection-manager';
+import { Application } from '@midwayjs/koa';
+import _ = require('lodash');
+import * as computedUtil from '../../util/computed';
+@Autoload()
+@Provide()
+@Scope(ScopeEnum.Singleton)
+export class MqSender {
+  @App()
+  app: Application;
+
+  private connection: amqp.AmqpConnectionManager;
+
+  private channelWrapper;
+
+  @Config('rabbitmq.url')
+  mqUrl: object;
+
+  @Config('mqConfig.normal')
+  mqConfig: object;
+  @Config('mqConfig.dead')
+  mqDeadConfig: object;
+
+  @Config('mqConfig.timeout')
+  timeout: number;
+
+  @Init()
+  async connect() {
+    // 创建连接,你可以把配置放在 Config 中,然后注入进来
+    this.connection = await amqp.connect(this.mqUrl);
+    // 创建 channel
+    this.channelWrapper = this.connection.createChannel({
+      json: true,
+      setup: channel => {
+        return Promise.all([
+          // 绑定队列
+          channel.assertExchange(_.get(this.mqConfig, 'ex'), 'direct', { durable: true }),
+          channel.assertQueue(_.get(this.mqConfig, 'q'), {
+            durable: true,
+            exclusive: false,
+            deadLetterExchange: _.get(this.mqDeadConfig, 'ex'),
+            deadLetterRoutingKey: _.get(this.mqDeadConfig, 'rk'),
+          }),
+          channel.bindQueue(_.get(this.mqConfig, 'q'), _.get(this.mqConfig, 'ex')),
+        ]);
+      },
+    });
+  }
+
+  // 订单死信消息
+  async orderMsg(data: object) {
+    const q = _.get(this.mqConfig, 'q');
+    const min = this.timeout;
+    // 转换成毫秒
+    const ms = computedUtil.multiply(min, 60000);
+    return this.channelWrapper.sendToQueue(q, { ...data, type: 'order' }, { expiration: ms });
+  }
+
+  /**团死信消息 */
+  async groupMsg(data: object, ms: number) {
+    const q = _.get(this.mqConfig, 'q');
+    return this.channelWrapper.sendToQueue(q, { ...data, type: 'group' }, { expiration: ms });
+  }
+
+  @Destroy()
+  async close() {
+    await this.channelWrapper.close();
+    await this.connection.close();
+  }
+}

+ 74 - 0
src/service/shopInBill.service.ts

@@ -0,0 +1,74 @@
+import { Provide } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { ShopInBill } from '../entity/base/shopInBill';
+import _ = require('lodash');
+import { Shop } from '../entity/base/shop';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import moment = require('moment');
+import * as computedUtil from '../util/computed';
+import { GroupOrder } from '../entity/group/groupOrder';
+@Provide()
+export class ShopInBillService {
+  @InjectEntityModel(ShopInBill)
+  model: ReturnModelType<typeof ShopInBill>;
+  @InjectEntityModel(Shop)
+  shopModel: ReturnModelType<typeof Shop>;
+  @InjectEntityModel(GroupOrder)
+  orderModel: ReturnModelType<typeof GroupOrder>;
+
+  /**
+   * 收货创建店铺流水
+   * 店铺本单实际收入 =  订单实际支付价格*(1-抽成) - 佣金金额
+   * @param id 订单id
+   * @param commission 佣金金额
+   * @param tran 数据库事务
+   */
+  async createByOrder(id, commission, tran) {
+    const order = await this.orderModel.findById(id).lean();
+    if (!order) throw new ServiceError('未找到订单数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    const obj: any = { shop: _.get(order, 'shop') };
+    const shopData = await this.shopModel.findById(_.get(order, 'shop'), { cut: 1 });
+    if (!shopData) throw new ServiceError('未找到店铺数据', FrameworkErrorEnum.NOT_FOUND_DATA);
+    obj.cut = _.get(shopData, 'cut', 0);
+    const real_pay = _.get(order, 'pay.pay_money');
+    obj.total = real_pay;
+    let receipts = computedUtil.multiply(computedUtil.minus(1, computedUtil.divide(obj.cut, 100)), real_pay);
+    // 减佣金
+    receipts = computedUtil.minus(receipts, commission);
+    obj.receipts = receipts;
+    obj.time = moment().format('YYYY-MM-DD HH:mm:ss');
+    obj.source = '2';
+    obj.source_id = _.get(order, '_id');
+    const num = await this.model.count({ source: obj.source, source_id: obj.source_id });
+    if (num <= 0) tran.insert('ShopInBill', obj);
+  }
+
+  /**
+   * 退货创建店铺流水
+   * @param afterSale 售后数据
+   * @param returnMoney 还需退款金额
+   * @param tran 数据库事务
+   */
+  async createByAfterSale(afterSale, returnMoney, tran) {
+    if (returnMoney <= 0) return;
+    const query: any = { source: '2' };
+    if (_.isObject(_.get(afterSale, 'order'))) query.source_id = _.get(afterSale, 'order._id');
+    else if (_.isString(_.get(afterSale, 'order'))) query.source_id = _.get(afterSale, 'order');
+    const inBill = await this.model.findOne(query);
+    if (!inBill) return;
+    const q2 = { source: '-2', source_id: _.get(afterSale, '_id') };
+    const n2 = await this.model.count(q2);
+    if (n2 > 0) return;
+    const obj: any = {};
+    if (_.isObject(_.get(afterSale, 'shop'))) obj.shop = _.get(afterSale, 'shop._id');
+    else if (_.isString(_.get(afterSale, 'shop'))) obj.shop = _.get(afterSale, 'shop');
+    obj.time = moment().format('YYYY-MM-DD HH:mm:ss');
+    obj.source = '-2';
+    obj.source_id = _.get(afterSale, 'order');
+    const limit = _.get(inBill, 'receipts');
+    if (computedUtil.minus(limit, returnMoney) >= 0) obj.receipts = returnMoney;
+    else obj.receipts = limit;
+    tran.insert('ShopInBill', obj);
+  }
+}

+ 70 - 0
src/service/view.service.ts

@@ -0,0 +1,70 @@
+import { Inject, Provide } from '@midwayjs/decorator';
+import { HttpServiceFactory } from '@midwayjs/axios';
+import { GoodsConfigService } from './goodsConfig.service';
+import { Context } from '@midwayjs/koa';
+import _ = require('lodash');
+import { GroupService } from './group.service';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import moment = require('moment');
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { GoodsSpec } from '../entity/base/goodsSpec';
+@Provide()
+export class ViewService {
+  key = 'base';
+  @Inject()
+  ctx: Context;
+  @Inject()
+  httpServiceFactory: HttpServiceFactory;
+  @Inject()
+  goodsConfigService: GoodsConfigService;
+  @Inject()
+  groupService: GroupService;
+  @InjectEntityModel(GoodsSpec)
+  goodsSpecModel: ReturnModelType<typeof GoodsSpec>;
+
+  /**
+   * 整理视图所需数据
+   * @param id 商品id
+   * @param group 团id
+   */
+  async setGoodsDetailViewData({ id, group }) {
+    const is_leader = _.get(this.ctx, 'user.is_leader', '1');
+    // 把数据请求来,拿到这边再改
+    const axios = this.httpServiceFactory.get(this.key);
+    const result = await axios.post('/viewGoods/goodsDetail', { id });
+    // 正常查看商品详情的数据
+    const { data } = result;
+    if (!data) throw new ServiceError('未找到商品详情', FrameworkErrorEnum.NOT_FOUND_DATA);
+    // 需要处理下;1.不需要act
+    delete data.act;
+    // 2.检查团
+    // const { result: gr, msg } = await this.groupService.checkGroup(group);
+    // if (!gr) throw new ServiceError(msg, FrameworkErrorEnum.SERVICE_FAULT);
+    const groupData = await this.groupService.fetch(group, { populate: false, lean: true });
+    const group_config = _.get(groupData, 'group_config');
+    // 整理规格
+    const groupSpecs = [];
+    for (const gc of group_config) {
+      // 1.检查规格状态是否可以销售
+      const { spec, leader_price, price: number_price, freight, buy_limit, limit_num } = gc;
+      const num = await this.goodsSpecModel.count({ _id: spec._id, status: '0' });
+      if (num <= 0) continue;
+      // 限制购买默认继承了商品的规格设置.如果在商品设置处有设置过,则会使用商品设置处的设置,如果没有,就是用原设置
+      const obj = _.pick(spec, ['_id', 'file', 'name', 'num', 'buy_limit', 'limit_num']);
+      if (_.isObject(freight)) obj.freight = _.get(freight, '$numberDecimal');
+      else obj.freight = freight;
+      if (buy_limit) obj.buy_limit = buy_limit;
+      if (limit_num) obj.limit_num = limit_num;
+      obj.leader_price = leader_price;
+      obj.number_price = number_price;
+      // 不需要判断了,团购中,团长也按团员的价格走
+      // if (is_leader === '0') obj.price = leader_price;
+      // else
+      obj.price = number_price;
+      groupSpecs.push(obj);
+    }
+    data.specs = groupSpecs;
+    return data;
+  }
+}

+ 44 - 0
src/util/computed.ts

@@ -0,0 +1,44 @@
+import Decimal from 'decimal.js';
+import _ = require('lodash');
+
+export function toNumber(num) {
+  if (_.isObject(num)) {
+    num = JSON.parse(JSON.stringify(num));
+    if (_.isObject(num)) num = _.get(num, '$numberDecimal');
+  }
+  return new Decimal(num).toNumber();
+}
+const turnDecimal = n => {
+  if (_.isObject(n)) {
+    n = JSON.parse(JSON.stringify(n));
+    if(_.isObject(n)) n = _.get(n, '$numberDecimal');
+  }
+  return new Decimal(n);
+};
+
+export function plus(n1 = 0, n2 = 0) {
+  const number1 = turnDecimal(n1);
+  const number2 = turnDecimal(n2);
+  const result = number1.add(number2).toFixed(2, Decimal.ROUND_DOWN);
+  return toNumber(result);
+}
+
+export function minus(n1 = 0, n2 = 0) {
+  const number1 = turnDecimal(n1);
+  const number2 = turnDecimal(n2);
+  const result = number1.minus(number2).toFixed(2, Decimal.ROUND_DOWN);
+  return toNumber(result);
+}
+export function multiply(n1 = 0, n2 = 0) {
+  const number1 = turnDecimal(n1);
+  const number2 = turnDecimal(n2);
+  const result = number1.mul(number2).toFixed(2, Decimal.ROUND_DOWN);
+  return toNumber(result);
+}
+
+export function divide(n1 = 0, n2 = 0) {
+  const number1 = turnDecimal(n1);
+  const number2 = turnDecimal(n2);
+  const result = number1.div(number2).toFixed(2, Decimal.ROUND_DOWN);
+  return toNumber(result);
+}

+ 88 - 0
src/util/kd100.ts

@@ -0,0 +1,88 @@
+import { HttpService } from '@midwayjs/axios';
+import { Inject, Provide } from '@midwayjs/core';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import _ = require('lodash');
+import CryptoJS = require('crypto-js');
+
+@Provide()
+export class Kd100Service {
+  @Inject()
+  httpService: HttpService;
+  api = 'https://poll.kuaidi100.com/poll/query.do';
+  customer = '5EC65D1966B410C333013563B7156AEE';
+  key = 'jrwohIUD2299';
+  async search({ no, type }) {
+    const param = { com: type, num: no, resultv2: '4' };
+    const md5Str = `${JSON.stringify(param)}${this.key}${this.customer}`;
+    const sign = _.toUpper(this.getSign(md5Str));
+    const body = { customer: this.customer, sign, param: JSON.stringify(param) };
+    const res = await this.httpService.post(this.api, body, { headers: { 'content-type': 'application/x-www-form-urlencoded' } });
+    if (res.status === 200) {
+      const { ischeck, data, nu, status, returnCode, message } = res.data;
+      if (!returnCode) {
+        const list = this.resetData(data);
+        const is_check = this.isCheck(ischeck);
+        return { list, is_check, no: nu, status };
+      } else if (returnCode !== '200') throw new ServiceError(message, FrameworkErrorEnum.SERVICE_FAULT);
+    }
+  }
+
+  isCheck(value) {
+    return value === '0' ? '未签收' : '已签收';
+  }
+
+  resetData(list) {
+    const newList = list.map(i => {
+      const { ftime, context, statusCode } = i;
+      const obj = { time: ftime, context, statsu: this.getStatus(statusCode) };
+      return obj;
+    });
+    return newList;
+  }
+  getStatus(code) {
+    const arr = [
+      { code: '1', text: '快递揽件' },
+      { code: '101', text: '已经下快件单' },
+      { code: '102', text: '待快递公司揽收' },
+      { code: '103', text: '快递公司已经揽收' },
+      { code: '0', text: '快件在途中' },
+      { code: '1001', text: '快件到达收件人城市' },
+      { code: '1002', text: '快件处于运输过程中' },
+      { code: '1003', text: '快件发往到新的收件地址' },
+      { code: '5', text: '快件正在派件' },
+      { code: '501', text: '快件已经投递到快递柜或者快递驿站' },
+      { code: '3', text: '快件已签收' },
+      { code: '301', text: '收件人正常签收' },
+      { code: '302', text: '快件显示派件异常,但后续正常签收' },
+      { code: '303', text: '快件已被代签' },
+      { code: '304', text: '快件已从快递柜或者驿站取出签收' },
+      { code: '6', text: '快件正处于返回发货人的途中' },
+      { code: '4', text: '此快件单已退签' },
+      { code: '401', text: '此快件单已撤销' },
+      { code: '14', text: '收件人拒签快件' },
+      { code: '7', text: '快件转给其他快递公司邮寄' },
+      { code: '2', text: '快件存在疑难' },
+      { code: '201', text: '快件长时间派件后未签收' },
+      { code: '202', text: '快件长时间没有派件或签收' },
+      { code: '203', text: '收件人发起拒收快递,待发货方确认' },
+      { code: '204', text: '快件派件时遇到异常情况' },
+      { code: '205', text: '快件在快递柜或者驿站长时间未取' },
+      { code: '206', text: '无法联系到收件人' },
+      { code: '207', text: '超出快递公司的服务区范围' },
+      { code: '208', text: '快件滞留在网点,没有派送' },
+      { code: '209', text: '快件破损' },
+      { code: '8', text: '超出快件清关递公司的服务区范围' },
+      { code: '10', text: '快件等待清关' },
+      { code: '11', text: '快件正在清关流程中' },
+      { code: '12', text: '快件已完成清关流程' },
+      { code: '13', text: '货物在清关过程中出现异常' },
+      { code: '\\', text: '收件人拒签快件' },
+    ];
+    return _.get(arr[code], 'text');
+  }
+
+  getSign(data) {
+    const md5 = CryptoJS.MD5(data).toString();
+    return md5;
+  }
+}

+ 22 - 0
src/util/pipeline.util.ts

@@ -0,0 +1,22 @@
+import _ = require('lodash');
+export function lookUp(model, ref, others = { fField: '_id', need_add: true }) {
+  const pipeline = [];
+  let local_prop = model;
+  if (others.need_add) {
+    const oid_prop = `${model}_oid`;
+    const id_prop = `${model}_id`;
+    local_prop = oid_prop;
+    const addField = { $addFields: { [local_prop]: { $toObjectId: `$${model}` }, [id_prop]: `$${model}` } };
+    pipeline.push(addField);
+  }
+  const $lookup = {
+    from: ref,
+    localField: local_prop,
+    foreignField: others.fField,
+    as: model,
+  };
+  if (_.get(others, 'childrenPipeline')) Object.assign($lookup, { pipeline: _.get(others, 'childrenPipeline') });
+  pipeline.push({ $lookup });
+  pipeline.push({ $unwind: `$${model}` });
+  return pipeline;
+}

+ 177 - 0
src/util/transactions.ts

@@ -0,0 +1,177 @@
+import { Provide } from '@midwayjs/decorator';
+// eslint-disable-next-line node/no-extraneous-import
+import { getClass, getModelForClass } from '@typegoose/typegoose';
+import { AnyParamConstructor } from '@typegoose/typegoose/lib/types';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import { Types } from 'mongoose';
+import _ = require('lodash');
+/**状态码 */
+enum StatusCode {
+  /**待处理 */
+  PADDING = 'padding',
+  /**处理成功 */
+  SUCCESS = 'success',
+  // /**处理失败 */
+  // ERROR = 'error',
+  ROLLBACK = 'rollback',
+}
+/**操作码 */
+enum OperaCode {
+  /**创建 */
+  CREATE = 'create',
+  /**修改 */
+  UPDATE = 'update',
+  /**删除 */
+  DELETE = 'delete',
+}
+/**任务接口 */
+interface IMission {
+  /**表名 */
+  model: string;
+  /**操作 */
+  opera: OperaCode;
+  /**数据 */
+  data?: any;
+  /**任务状态 */
+  status: StatusCode;
+  /**任务操作前的数据(添加不需要) */
+  backUp?: any;
+  /**任务范围(添加不需要) */
+  query?: object;
+}
+
+@Provide()
+export class TransactionService {
+  mission: Array<IMission> = [];
+  /**
+   * 事务添加
+   * @param modelName 表名
+   * @param data 添加的数据
+   */
+  insert(modelName: string, data: object): Types.ObjectId {
+    const _id = new Types.ObjectId();
+    Object.assign(data, { _id });
+    const mission: IMission = {
+      model: modelName,
+      opera: OperaCode.CREATE,
+      data,
+      status: StatusCode.PADDING,
+    };
+    this.mission.push(mission);
+    return _id;
+  }
+
+  /**
+   * 事务修改
+   * @param modelName 表名
+   * @param query 修改的数据范围
+   * @param data 要修改为的数据
+   */
+  update(modelName: string, query: object, data: object) {
+    const mission: IMission = {
+      model: modelName,
+      opera: OperaCode.UPDATE,
+      data,
+      status: StatusCode.PADDING,
+      query,
+    };
+    this.mission.push(mission);
+  }
+  /**
+   * 事务删除
+   * @param modelName 表名
+   * @param query 删除范围
+   */
+  delete(modelName: string, query: object) {
+    const mission: IMission = {
+      model: modelName,
+      opera: OperaCode.DELETE,
+      status: StatusCode.PADDING,
+      query,
+    };
+    this.mission.push(mission);
+  }
+
+  async run() {
+    // 需要备份的操作先备份
+    await this.toRestoreData();
+    for (const m of this.mission) {
+      const { model: modelName, opera, status, data, query } = m;
+      // 判断当前任务状态
+      // 成功:下一步;
+      if (status === StatusCode.SUCCESS) continue;
+      try {
+        // 先找到model
+        const model = this.getModel(modelName);
+        // 再看opera,进行操作
+        if (opera === OperaCode.CREATE) {
+          await model.create(data);
+        } else if (opera === OperaCode.UPDATE) {
+          await model.updateMany(query, data);
+        } else if (opera === OperaCode.DELETE) {
+          await model.deleteMany(query);
+        }
+        m.status = StatusCode.SUCCESS;
+      } catch (error) {
+        // 发生错误后,当前步骤状态为padding,之前均为success.将success回滚
+        // 更改标记,中断程序
+        throw new ServiceError(error, FrameworkErrorEnum.SERVICE_FAULT);
+        break;
+      }
+    }
+  }
+
+  /**
+   * 针对每步的操作,进行数据备份,以便回滚还原
+   */
+  private async toRestoreData() {
+    for (const m of this.mission) {
+      const { model: modelName, opera, status, query } = m;
+      // 只进行未处理的备份,处理过的不再备份
+      if (status !== StatusCode.PADDING) continue;
+      // 数据备份;创建不需要备份,没有
+      if (opera === OperaCode.CREATE) continue;
+      const model = this.getModel(modelName);
+      const backUp = await model.find(query).lean();
+
+      m.backUp = backUp;
+    }
+  }
+  /**
+   * 回滚,将success的任务全部回滚
+   */
+  async rollback() {
+    const needRollback = this.mission.filter(f => f.status === StatusCode.SUCCESS);
+    for (const m of needRollback) {
+      const { model: modelName, opera, data, backUp } = m;
+      const model = this.getModel(modelName);
+      if (opera === OperaCode.CREATE) {
+        const _id = _.get(data, '_id');
+        await model.deleteOne({ _id });
+      } else if (opera === OperaCode.UPDATE) {
+        for (const bd of backUp) {
+          await model.updateOne({ _id: bd._id }, bd);
+        }
+      } else if (opera === OperaCode.DELETE) {
+        await model.insertMany(backUp);
+      }
+      m.status = StatusCode.ROLLBACK;
+    }
+  }
+
+  /**
+   * 找model
+   * @param modelName 表名
+   */
+  private getModel(modelName: string) {
+    let model;
+    try {
+      model = getModelForClass(getClass(modelName) as AnyParamConstructor<any>);
+    } catch (error) {
+      throw new ServiceError('事务:生成模型错误', FrameworkErrorEnum.SERVICE_FAULT);
+    }
+    if (!model) throw new ServiceError('事务:未找到模型', FrameworkErrorEnum.SERVICE_FAULT);
+
+    return model;
+  }
+}

+ 9 - 0
src/util/util.ts

@@ -0,0 +1,9 @@
+import _ = require('lodash');
+export function randomStr() {
+  return Math.random().toString(36).substr(2, 8);
+}
+export function getDataFromTarget(data: object, target: string, props: Array<string>) {
+  const obj = _.get(data, target);
+  if (!obj) return;
+  return _.pick(obj, props);
+}

+ 93 - 0
src/util/wxpay.ts

@@ -0,0 +1,93 @@
+import { Provide, Inject, App } from '@midwayjs/decorator';
+import { HttpServiceFactory } from '@midwayjs/axios';
+import { Application } from '@midwayjs/koa';
+import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
+import _ = require('lodash');
+import { WxRefundData } from '../interface/pay.interface';
+@Provide()
+export class WxPayService {
+  key = 'wechat';
+  @App()
+  app: Application;
+  @Inject()
+  httpServiceFactory: HttpServiceFactory;
+  /**
+   * 支付下单
+   */
+  async create(data) {
+    const { money, openid, order_no, desc } = data;
+    if (!money) throw new ServiceError('微信支付请求:缺少金额', FrameworkErrorEnum.NEED_BODY);
+    if (!openid) throw new ServiceError('微信支付请求:缺少支付用户信息', FrameworkErrorEnum.NEED_BODY);
+    if (!order_no) throw new ServiceError('微信支付请求:缺少订单号', FrameworkErrorEnum.NEED_BODY);
+    const config = this.app.getConfig('wxPayConfig');
+    const notice_url = this.app.getConfig('wxPayCallBack');
+    const wxOrderData = { config, money, openid, order_no, desc, notice_url };
+    const axios = await this.httpServiceFactory.get(this.key);
+    const result = await axios.post('/pay/payOrder', wxOrderData);
+    return _.get(result, 'data');
+    // if (result.status === 200) {
+    //   return _.get(result, 'data.data');
+    // } else throw new ServiceError('微信支付请求失败', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 关闭支付订单
+   */
+  async close(order_no) {
+    if (!order_no) throw new ServiceError('微信关闭订单请求:缺少订单号', FrameworkErrorEnum.NEED_BODY);
+    const config = this.app.getConfig('wxPayConfig');
+    const params = { config, order_no };
+    const axios = await this.httpServiceFactory.get(this.key);
+    const result = await axios.post('/pay/closeOrder', params);
+    return _.get(result, 'data');
+    // if (result.status === 200) {
+    //   return _.get(result, 'data.data');
+    // } else throw new ServiceError('微信关闭订单请求失败', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 查询支付单
+   */
+  async search(order_no) {
+    if (!order_no) throw new ServiceError('微信关闭订单请求:缺少订单号', FrameworkErrorEnum.NEED_BODY);
+    const config = this.app.getConfig('wxPayConfig');
+    const params = { config, order_no };
+    const axios = await this.httpServiceFactory.get(this.key);
+    const result = await axios.post('/pay/searchOrderByOrderNo', params);
+    return _.get(result, 'data');
+    // if (result.status === 200) {
+    //   return _.get(result, 'data.data');
+    // } else throw new ServiceError('微信关闭订单请求失败', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+
+  /**
+   * 支付单退款
+   */
+  async refund(data: WxRefundData) {
+    const { order_no, out_refund_no, money, reason } = data;
+    if (!order_no) throw new ServiceError('微信退款请求:缺少订单号', FrameworkErrorEnum.NEED_BODY);
+    if (!out_refund_no) throw new ServiceError('微信退款请求:缺少退款单号', FrameworkErrorEnum.NEED_BODY);
+    const config = this.app.getConfig('wxPayConfig');
+    const params = { config, order_no, out_refund_no, money, reason };
+    const axios = await this.httpServiceFactory.get(this.key);
+    const result = await axios.post('/pay/refundOrder', params);
+    return _.get(result, 'data');
+    // if (result.status === 200) {
+    //   return _.get(result, 'data.data');
+    // } else throw new ServiceError('微信退款请求失败', FrameworkErrorEnum.SERVICE_FAULT);
+  }
+  /**
+   * 组织uniapp的微信支付数据
+   * @param data 微信请求支付返回值
+   */
+  preparToUniAppWxPay(data: object): object {
+    const obj = {
+      nonceStr: _.get(data, 'nonceStr'),
+      package: `prepay_id=${_.get(data, 'prepay_id')}`,
+      signType: _.get(data, 'signType'),
+      timeStamp: _.get(data, 'timestamp'),
+      paySign: _.get(data, 'paySign'),
+    };
+    return obj;
+  }
+}

+ 20 - 0
test/controller/api.test.ts

@@ -0,0 +1,20 @@
+import { createApp, close, createHttpRequest } from '@midwayjs/mock';
+import { Framework } from '@midwayjs/koa';
+
+describe('test/controller/home.test.ts', () => {
+
+  it('should POST /api/get_user', async () => {
+    // create app
+    const app = await createApp<Framework>();
+
+    // make request
+    const result = await createHttpRequest(app).get('/api/get_user').query({ uid: 123 });
+
+    // use expect by jest
+    expect(result.status).toBe(200);
+    expect(result.body.message).toBe('OK');
+
+    // close app
+    await close(app);
+  });
+});

+ 21 - 0
test/controller/home.test.ts

@@ -0,0 +1,21 @@
+import { createApp, close, createHttpRequest } from '@midwayjs/mock';
+import { Framework } from '@midwayjs/koa';
+
+describe('test/controller/home.test.ts', () => {
+
+  it('should GET /', async () => {
+    // create app
+    const app = await createApp<Framework>();
+
+    // make request
+    const result = await createHttpRequest(app).get('/');
+
+    // use expect by jest
+    expect(result.status).toBe(200);
+    expect(result.text).toBe('Hello Midwayjs!');
+
+    // close app
+    await close(app);
+  });
+
+});

+ 54 - 0
test/controller/weather.test.ts

@@ -0,0 +1,54 @@
+import { createApp, close, createHttpRequest } from '@midwayjs/mock';
+import { Framework, Application } from '@midwayjs/koa';
+import * as nock from 'nock';
+
+describe('test/controller/weather.test.ts', () => {
+  let app: Application;
+  beforeAll(async () => {
+    // create app
+    app = await createApp<Framework>();
+    // 这里由于 github 测试环境无法顺畅的连通国内网络,使用 nock 这个库模拟了服务,实际测试不需要
+    nock('http://www.weather.com.cn')
+      .get('/data/sk/101010100.html')
+      .reply(200, {
+        weatherinfo: {
+          city: '北京',
+          cityid: '101010100',
+          temp: '27.9',
+          WD: '南风',
+          WS: '小于3级',
+          SD: '28%',
+          AP: '1002hPa',
+          njd: '暂无实况',
+          WSE: '<3',
+          time: '17:55',
+          sm: '2.1',
+          isRadar: '1',
+          Radar: 'JC_RADAR_AZ9010_JB',
+        },
+      });
+  });
+
+  afterAll(async () => {
+    // close app
+    await close(app);
+    nock.restore();
+  });
+
+  it('should test /weather with success request', async () => {
+    // make request
+    const result = await createHttpRequest(app)
+      .get('/weather')
+      .query({ cityId: 101010100 });
+
+    expect(result.status).toBe(200);
+    expect(result.text).toMatch(/北京/);
+  });
+
+  it('should test /weather with fail request', async () => {
+    const result = await createHttpRequest(app).get('/weather');
+
+    expect(result.status).toBe(200);
+    expect(result.text).toMatch(/weather data is empty/);
+  });
+});

+ 21 - 0
tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "compileOnSave": true,
+  "compilerOptions": {
+    "target": "es2018",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "inlineSourceMap": true,
+    "noImplicitThis": true,
+    "noUnusedLocals": false,
+    "stripInternal": true,
+    "skipLibCheck": true,
+    "pretty": true,
+    "declaration": true,
+    "forceConsistentCasingInFileNames": true,
+    "typeRoots": ["./typings", "./node_modules/@types"],
+    "outDir": "dist"
+  },
+  "exclude": ["dist", "node_modules", "test"]
+}

+ 1 - 0
update.sh

@@ -0,0 +1 @@
+git pull && npm run build && pm2 restart 32

+ 52 - 0
view/info.html

@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>天气预报</title>
+    <style>
+      .weather_bg {
+        background-color: #0d68bc; 
+        height: 150px;
+        color: #fff;
+        font-size: 12px;
+        line-height: 1em;
+        text-align: center;
+        padding: 10px;
+      }
+
+      .weather_bg label {
+        line-height: 1.5em;
+        text-align: center;
+        text-shadow: 1px 1px 1px #555;
+        background: #afdb00;
+        width: 100px;
+        display: inline-block;
+        margin-left: 10px;
+      }
+
+      .weather_bg .temp {
+        font-size: 32px;
+        margin-top: 5px;
+        padding-left: 14px;
+      }
+      .weather_bg sup {
+        font-size: 0.5em;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="weather_bg">
+      <div>
+        <p>
+          {{city}}({{WD}}{{WS}})
+        </p>
+        <p class="temp">{{temp}}<sup>℃</sup></p>
+        <p>
+          气压<label>{{AP}}</label>
+        </p>
+        <p>
+          湿度<label>{{SD}}</label>
+        </p>
+      </div>
+    </div>
+  </body>
+</html>