lrf402788946 3 anni fa
commit
8a82f28a71
50 ha cambiato i file con 1512 aggiunte e 0 eliminazioni
  1. 29 0
      .autod.conf.js
  2. 1 0
      .eslintignore
  3. 3 0
      .eslintrc
  4. 42 0
      .github/workflows/nodejs.yml
  5. 14 0
      .gitignore
  6. 12 0
      .travis.yml
  7. 63 0
      README.md
  8. 15 0
      app.js
  9. 46 0
      app/controller/dining/.arrange.js
  10. 53 0
      app/controller/dining/.menu.js
  11. 40 0
      app/controller/dining/.order.js
  12. 13 0
      app/controller/dining/arrange.js
  13. 13 0
      app/controller/dining/menu.js
  14. 13 0
      app/controller/dining/order.js
  15. 17 0
      app/controller/home.js
  16. 59 0
      app/controller/system/.admin.js
  17. 38 0
      app/controller/system/.tenant.js
  18. 13 0
      app/controller/system/admin.js
  19. 13 0
      app/controller/system/tenant.js
  20. 14 0
      app/middleware/tenant-check.js
  21. 20 0
      app/model/dining/arrange.js
  22. 26 0
      app/model/dining/menu.js
  23. 28 0
      app/model/dining/order.js
  24. 23 0
      app/model/system/admin-menu.js
  25. 28 0
      app/model/system/admin.js
  26. 24 0
      app/model/system/opera.js
  27. 23 0
      app/model/system/tenant.js
  28. 13 0
      app/router.js
  29. 12 0
      app/router/dining/arrange.js
  30. 11 0
      app/router/dining/menu.js
  31. 12 0
      app/router/dining/order.js
  32. 12 0
      app/router/system/admin.js
  33. 12 0
      app/router/system/tenant.js
  34. 25 0
      app/service/dining/arrange.js
  35. 15 0
      app/service/dining/menu.js
  36. 24 0
      app/service/dining/order.js
  37. 54 0
      app/service/install.js
  38. 38 0
      app/service/system/admin.js
  39. 15 0
      app/service/system/tenant.js
  40. 96 0
      app/service/util/http-util.js
  41. 66 0
      app/service/util/rabbitMq.js
  42. 115 0
      app/service/util/spm.js
  43. 144 0
      app/service/util/util.js
  44. 14 0
      appveyor.yml
  45. 55 0
      config/config.default.js
  46. 12 0
      config/config.prod.js
  47. 7 0
      config/config.secret.js
  48. 6 0
      config/plugin.js
  49. 51 0
      package.json
  50. 20 0
      test/app/controller/home.test.js

+ 29 - 0
.autod.conf.js

@@ -0,0 +1,29 @@
+'use strict';
+
+module.exports = {
+  write: true,
+  prefix: '^',
+  plugin: 'autod-egg',
+  test: [
+    'test',
+    'benchmark',
+  ],
+  dep: [
+    'egg',
+    'egg-scripts',
+  ],
+  devdep: [
+    'egg-ci',
+    'egg-bin',
+    'egg-mock',
+    'autod',
+    'autod-egg',
+    'eslint',
+    'eslint-config-egg',
+  ],
+  exclude: [
+    './test/fixtures',
+    './dist',
+  ],
+};
+

+ 1 - 0
.eslintignore

@@ -0,0 +1 @@
+coverage

+ 3 - 0
.eslintrc

@@ -0,0 +1,3 @@
+{
+  "extends": "eslint-config-egg"
+}

+ 42 - 0
.github/workflows/nodejs.yml

@@ -0,0 +1,42 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: Node.js CI
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: '0 2 * * *'
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        node-version: [10]
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+    - name: Checkout Git Source
+      uses: actions/checkout@v2
+
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+
+    - name: Install Dependencies
+      run: npm i -g npminstall && npminstall
+
+    - name: Continuous Integration
+      run: npm run ci
+
+    - name: Code Coverage
+      uses: codecov/codecov-action@v1
+      with:
+        token: ${{ secrets.CODECOV_TOKEN }}

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+logs/
+npm-debug.log
+yarn-error.log
+node_modules/
+package-lock.json
+yarn.lock
+coverage/
+.idea/
+run/
+.DS_Store
+*.sw*
+*.un~
+typings/
+.nyc_output/

+ 12 - 0
.travis.yml

@@ -0,0 +1,12 @@
+
+language: node_js
+node_js:
+  - '10'
+before_install:
+  - npm i npminstall -g
+install:
+  - npminstall
+script:
+  - npm run ci
+after_script:
+  - npminstall codecov && codecov

+ 63 - 0
README.md

@@ -0,0 +1,63 @@
+# service
+
+食堂服务端
+
+## 多租户,model下如果是多层级,需要修改框架的application.js
+> naf-framework-mongoose => lib => plugin => egg-multi-tenancy => app => extend => application.js
+```
+const loadModel2 = (app, tenant) => {
+  app.logger.info(`[multi-tenancy] Load tenant models for ${tenant}`);
+  let model = {};
+  const setModel = (val,key) =>{
+    const obj = {};
+    const modelName = `${val.modelName}@${tenant}`;
+    const collName = `${val.collection.name}`;
+    const multiTenancy = val.schema.get('multi-tenancy');
+    if (multiTenancy) {
+      app.logger.debug(`[multi-tenancy] ${modelName} loaded`);
+      const schema = val.schema.clone();
+      schema.set('x-tenant', tenant);
+      schema.plugin(multiTenancyPlugin, app.config.multiTenancy);
+      obj[key] = val.db.model(modelName, schema, collName);
+    } else {
+      app.logger.debug(`[multi-tenancy] skip ${val.modelName}, schema not enable multi-tenancy.`);
+      obj[key] = val;
+    }
+    return obj;
+  }
+
+  _.forEach(app.model, (val, key) => {
+    const nval = getAllModel(val);
+    // 文件夹分层级后,val不再只是Function,也有可能是Object,添加个方法,获取所有model
+    if(!_.isArray(nval)) {
+      let mod = {};
+      mod = setModel(nval,key);
+      model = {...model,...mod};
+    }
+    else {
+      let mods = {};
+      mods[key] = {};
+      for (const model of nval) {
+        const nkey = _.upperFirst(model.modelName);
+        let mod = setModel(model,nkey);
+        mods[key] = {...mods[key],...mod};
+      }
+      model = {...model,...mods};
+    }
+
+  });
+  return model;
+};
+
+const getAllModel = data => {
+  if(_.isFunction(data)) return data;
+  const keys = Object.keys(data);
+  const arr = [];
+  for (const key of keys) {
+    const model = data[key];
+    if(_.isFunction(model)) arr.push(model)
+    else arr.push(...getAllModel(model))
+  }
+  return arr;
+}
+```

+ 15 - 0
app.js

@@ -0,0 +1,15 @@
+'use strict';
+
+class AppBootHook {
+  constructor(app) {
+    this.app = app;
+  }
+  // 应用已启动阶段
+  async didReady() {
+    // 初始化数据
+    const ctx = await this.app.createAnonymousContext();
+    await ctx.service.install.index();
+  }
+}
+
+module.exports = AppBootHook;

+ 46 - 0
app/controller/dining/.arrange.js

@@ -0,0 +1,46 @@
+module.exports = {
+  create: {
+    requestBody: ["date", "arrange"],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ["date", "arrange"],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        arrange: "arrange",
+        "date@start": "date@start",
+        "date@end": "date@end",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+  getByDate:{
+    parameters: {
+      query: {
+        date: "date",
+      },
+    },
+    service: "getByDate",
+  }
+};

+ 53 - 0
app/controller/dining/.menu.js

@@ -0,0 +1,53 @@
+module.exports = {
+  create: {
+    requestBody: [
+      "name",
+      "params",
+      "order",
+      "img",
+      "content",
+      "calorie",
+      "is_use",
+    ],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: [
+      "name",
+      "params",
+      "order",
+      "img",
+      "content",
+      "calorie",
+      "is_use",
+    ],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        name: "name",
+        is_use: "is_use",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+};

+ 40 - 0
app/controller/dining/.order.js

@@ -0,0 +1,40 @@
+module.exports = {
+  create: {
+    requestBody: ['user_id','date','breakfast','lunch','dinner'],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ['user_id','date','breakfast','lunch','dinner'],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        "create_time@start": "create_time@start",
+        "create_time@end": "create_time@end",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+  useMeal:{
+    params: ["!id"],
+  }
+};

+ 13 - 0
app/controller/dining/arrange.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.arrange.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 安排
+class ArrangeController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.dining.arrange;
+  }
+}
+module.exports = CrudController(ArrangeController, meta);

+ 13 - 0
app/controller/dining/menu.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.menu.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 菜单
+class MenuController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.dining.menu;
+  }
+}
+module.exports = CrudController(MenuController, meta);

+ 13 - 0
app/controller/dining/order.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.order.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 点餐
+class OrderController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.dining.order;
+  }
+}
+module.exports = CrudController(OrderController, meta);

+ 17 - 0
app/controller/home.js

@@ -0,0 +1,17 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+  async index() {
+    const { ctx } = this;
+    ctx.body = 'hi, egg';
+  }
+  // 项目初始化
+  async install() {
+    await this.ctx.service.install.index(this.ctx.request.body);
+    this.ctx.body = 'ok';
+  }
+}
+
+module.exports = HomeController;

+ 59 - 0
app/controller/system/.admin.js

@@ -0,0 +1,59 @@
+module.exports = {
+  create: {
+    requestBody: [
+      "name",
+      "login_id",
+      "password",
+      "menu",
+      "is_root",
+      "openid",
+      "remark",
+    ],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: [
+      "name",
+      "login_id",
+      "password",
+      "menu",
+      "is_root",
+      "openid",
+      "remark",
+    ],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        name: "name",
+        login_id: "login_id",
+        openid: "openid",
+        is_root: "is_root",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+  login: {
+    requestBody: ["login_id", "password"],
+    service: "login",
+  },
+};

+ 38 - 0
app/controller/system/.tenant.js

@@ -0,0 +1,38 @@
+module.exports = {
+  create: {
+    requestBody: ["_tenant", "name", "is_use", "remark"],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ["_tenant", "name", "is_use", "remark"],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        _tenant: "_tenant",
+        name: "name",
+        is_use: "is_use",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+};

+ 13 - 0
app/controller/system/admin.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.admin.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 管理员
+class AdminController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.system.admin;
+  }
+}
+module.exports = CrudController(AdminController, meta);

+ 13 - 0
app/controller/system/tenant.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.tenant.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 站点
+class TenantController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.system.tenant;
+  }
+}
+module.exports = CrudController(TenantController, meta);

+ 14 - 0
app/middleware/tenant-check.js

@@ -0,0 +1,14 @@
+'use strict';
+const _ = require('lodash');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+module.exports = options => {
+  return async function tenantCheck(ctx, next) {
+    const request = ctx.request;
+    if (request.method !== 'GET') {
+      const tenant = _.get(request, 'header.x-tenant');
+      // 该中间只能通过master的权限进行增删改
+      if (tenant !== 'master') throw new BusinessError(ErrorCode.ACCESS_DENIED, '您没有访问的权限!');
+    }
+    await next();
+  };
+};

+ 20 - 0
app/model/dining/arrange.js

@@ -0,0 +1,20 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 安排表
+const arrange = {
+  date: { type: String, required: true }, // 日期
+  arrange: { type: Object }, // 安排
+  remark: { type: String },
+};
+const schema = new Schema(arrange, { 'multi-tenancy': true, toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ date: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Arrange', schema, 'arrange');
+};

+ 26 - 0
app/model/dining/menu.js

@@ -0,0 +1,26 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 菜单表
+const menu = {
+  name: { type: String }, // 菜名
+  params: { type: Array }, // 参数列表
+  order: { type: Number, default: 0 }, // 已点数量
+  img: { type: Array }, // 图片
+  content: { type: String }, // 本品介绍
+  calorie: { type: Number }, // 卡路里,单位:大卡 reserve
+  is_use: { type: Boolean, default: true }, // 可以点餐
+  remark: { type: String },
+};
+const schema = new Schema(menu, { 'multi-tenancy': true, toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ name: 1 });
+schema.index({ is_use: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Menu', schema, 'menu');
+};

+ 28 - 0
app/model/dining/order.js

@@ -0,0 +1,28 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+const meal = new Schema({
+  list: { type: Array }, // 点餐内容
+  reserve: { type: Number, default: 0 }, // 卡路里,热量
+  is_use: { type: Boolean, default: false }, // 是否使用
+});
+// 订餐表
+const order = {
+  user_id: { type: String, required: true }, // 用户openid
+  date: { type: String, required: true }, // 日期
+  breakfast: { type: meal }, // 早餐
+  lunch: { type: meal }, // 午餐
+  dinner: { type: meal }, // 晚餐
+  remark: { type: String },
+};
+const schema = new Schema(order, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ date: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Order', schema, 'order');
+};

+ 23 - 0
app/model/system/admin-menu.js

@@ -0,0 +1,23 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 系统菜单表
+const adminMenu = {
+  name: { type: String }, // 菜单名
+  path: { type: String }, // 路由
+  is_use: { type: Boolean, default: true }, // 是否启用
+  level: { type: Number, default: 1 }, // 等级
+  parent_id: { type: ObjectId }, // 父级id
+  sort: { type: Number, default: 0 }, // 排序:降序排列
+  remark: { type: String },
+};
+const schema = new Schema(adminMenu, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('AdminMenu', schema, 'adminMenu');
+};

+ 28 - 0
app/model/system/admin.js

@@ -0,0 +1,28 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+const { ObjectId } = require('mongoose').Types;
+// 系统管理员表
+const admin = {
+  name: { type: String, default: '管理员' }, // 昵称
+  login_id: { type: String }, // 登陆用户名
+  password: { type: Secret, select: false }, // 密码
+  menu: { type: Array }, // 菜单(管理员默认全有),如果里面不为空,或者有内容了,就按内容换菜单
+  is_root: { type: Boolean, default: false }, // 为之后用,这个管理员是不是拥有最高权限:可不可以更改菜单或者其他操作
+  openid: { type: String }, // 微信openid
+  remark: { type: String },
+};
+const schema = new Schema(admin, { 'multi-tenancy': true, toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ name: 1 });
+schema.index({ login_id: 1 });
+schema.index({ is_root: 1 });
+schema.index({ openid: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Admin', schema, 'admin');
+};

+ 24 - 0
app/model/system/opera.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 操作记录表
+const opera = {
+  user_id: { type: ObjectId, required: true }, // 操作人
+  target: { type: ObjectId, required: true }, // 目标id
+  target_model: { type: String, required: true }, // 目标model
+  method: { type: String, required: true }, // 执行函数, 之后自定义列表去换成人话
+  remark: { type: String },
+  // 操作时间转过来
+};
+const schema = new Schema(opera, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ user_id: 1 });
+schema.index({ target: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Opera', schema, 'opera');
+};

+ 23 - 0
app/model/system/tenant.js

@@ -0,0 +1,23 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 站点表
+const tenant = {
+  _tenant: { type: String, require: true }, // 站点
+  name: { type: String }, // 站点名称
+  is_use: { type: Boolean, default: true }, // 是否使用
+  remark: { type: String },
+};
+const schema = new Schema(tenant, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ _tenant: 1 });
+schema.index({ name: 1 });
+schema.index({ is_use: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Tenant', schema, 'tenant');
+};

+ 13 - 0
app/router.js

@@ -0,0 +1,13 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  router.get('/', controller.home.index);
+  require('./router/system/admin')(app); // 管理员
+  require('./router/system/tenant')(app); // 站点管理
+  require('./router/dining/menu')(app); // 菜单管理
+  require('./router/dining/arrange')(app); // 安排管理
+};

+ 12 - 0
app/router/dining/arrange.js

@@ -0,0 +1,12 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller, config } = app;
+  const profix = `/api${config.appName ? `/${config.appName}` : ''}`;
+  const index = 'dining';
+  const target = 'arrange';
+  router.get(target, `${profix}/${index}/${target}/getByDate`, controller[index][target].getByDate);
+  router.resources(target, `${profix}/${index}/${target}`, controller[index][target]); // index、create、show、destroy
+  router.post(target, `${profix}/${index}/${target}/update/:id`, controller[index][target].update);
+};

+ 11 - 0
app/router/dining/menu.js

@@ -0,0 +1,11 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller, config } = app;
+  const profix = `/api${config.appName ? `/${config.appName}` : ''}`;
+  const index = 'dining';
+  const target = 'menu';
+  router.resources(target, `${profix}/${index}/${target}`, controller[index][target]); // index、create、show、destroy
+  router.post(target, `${profix}/${index}/${target}/update/:id`, controller[index][target].update);
+};

+ 12 - 0
app/router/dining/order.js

@@ -0,0 +1,12 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller, config } = app;
+  const profix = `/api${config.appName ? `/${config.appName}` : ''}`;
+  const index = 'dining';
+  const target = 'order';
+  router.post(target, `${profix}/${index}/${target}/useMeal/:id`, controller[index][target].useMeal);
+  router.resources(target, `${profix}/${index}/${target}`, controller[index][target]); // index、create、show、destroy
+  router.post(target, `${profix}/${index}/${target}/update/:id`, controller[index][target].update);
+};

+ 12 - 0
app/router/system/admin.js

@@ -0,0 +1,12 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller, config } = app;
+  const profix = `/api${config.appName ? `/${config.appName}` : ''}`;
+  const index = 'system';
+  const target = 'admin';
+  router.post(target, `${profix}/${index}/${target}/login`, controller[index][target].login);
+  router.resources(target, `${profix}/${index}/${target}`, controller[index][target]); // index、create、show、destroy
+  router.post(target, `${profix}/${index}/${target}/update/:id`, controller[index][target].update);
+};

+ 12 - 0
app/router/system/tenant.js

@@ -0,0 +1,12 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller, config } = app;
+  const profix = `/api${config.appName ? `/${config.appName}` : ''}`;
+  const index = 'system';
+  const target = 'tenant';
+  const tc = app.middleware.tenantCheck();
+  router.resources(target, `${profix}/${index}/${target}`, tc, controller[index][target]); // index、create、show、destroy
+  router.post(target, `${profix}/${index}/${target}/update/:id`, tc, controller[index][target].update);
+};

+ 25 - 0
app/service/dining/arrange.js

@@ -0,0 +1,25 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 安排
+class ArrangeService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'arrange');
+    this.model = this.ctx.model.Dining.Arrange;
+  }
+
+  /**
+   * 获取某天的安排
+   * @param {Object} query 参数
+   * @property date 指定日期
+   */
+  async getByDate({ date }) {
+    const res = await this.model.findOne({ date });
+    return res;
+  }
+}
+
+module.exports = ArrangeService;

+ 15 - 0
app/service/dining/menu.js

@@ -0,0 +1,15 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 菜单
+class MenuService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'menu');
+    this.model = this.ctx.model.Dining.Menu;
+  }
+}
+
+module.exports = MenuService;

+ 24 - 0
app/service/dining/order.js

@@ -0,0 +1,24 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 点餐
+class OrderService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'order');
+    this.model = this.ctx.model.Dining.Order;
+  }
+
+  /**
+   * 扫码领餐
+   * @param {Object} query 参数
+   * @property id 早/中/晚餐的数据id,子文档id
+   */
+  async useMeal({ id }) {
+    console.log(id);
+  }
+}
+
+module.exports = OrderService;

+ 54 - 0
app/service/install.js

@@ -0,0 +1,54 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 初始化项目相关
+class InstallService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'install');
+    this.admin = this.ctx.model.System.Admin;
+    this.tenant = this.ctx.model.System.Tenant;
+  }
+  /**
+   * 初始化
+   */
+  async index() {
+    // TODO,初始化管理员之类的
+    this.adminInit();
+    this.tenantInit();
+  }
+
+  /**
+   * 管理员初始化,生成最高权限----root管理员,_tenant:分站标识,master是全站最高
+   */
+  async adminInit() {
+    const has = await this.admin.count({ _tenant: 'master', is_root: true });
+    if (has > 0) return;
+    const data = {
+      name: '总管理员',
+      login_id: 'admin',
+      password: {
+        secret: 'free1977',
+      },
+      is_root: true,
+    };
+    await this.admin.create(data);
+  }
+
+  /**
+   * 站点列表初始化
+   */
+  async tenantInit() {
+    const has = await this.tenant.count({ _tenant: 'master' });
+    if (has > 0) return;
+    const data = {
+      _tenant: 'master',
+      name: '总管理站',
+    };
+    await this.tenant.create(data);
+  }
+}
+
+module.exports = InstallService;

+ 38 - 0
app/service/system/admin.js

@@ -0,0 +1,38 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+const jwt = require('jsonwebtoken');
+
+// 管理员
+class AdminService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'admin');
+    this.model = this.ctx.model.System.Admin;
+  }
+
+  /**
+   * 管理员登陆
+   * @param {Object} body
+   * @property login_id 登陆用户名
+   * @property password 密码
+   */
+  async login({ login_id, password }) {
+    const user = await this.model.findOne({ login_id }, '+password');
+    if (!user) { throw new BusinessError(ErrorCode.USER_NOT_EXIST, '管理员不存在'); }
+    const { secret } = user.password;
+    if (secret !== password) { throw new BusinessError(ErrorCode.BAD_PASSWORD, '密码错误'); }
+    const data = _.omit(JSON.parse(JSON.stringify(user)), [
+      'meta',
+      'passwd',
+      '__v',
+      'password',
+    ]);
+    const { secret: secrets } = this.config.jwt;
+    const token = jwt.sign(data, secrets);
+    return token;
+  }
+}
+
+module.exports = AdminService;

+ 15 - 0
app/service/system/tenant.js

@@ -0,0 +1,15 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 站点
+class TenantService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'tenant');
+    this.model = this.ctx.model.System.Tenant;
+  }
+}
+
+module.exports = TenantService;

+ 96 - 0
app/service/util/http-util.js

@@ -0,0 +1,96 @@
+'use strict';
+const { AxiosService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { isNullOrUndefined } = require('naf-core').Util;
+const _ = require('lodash');
+
+//
+class HttpUtilService extends AxiosService {
+  constructor(ctx) {
+    super(ctx, {}, {});
+  }
+
+
+  // 替换uri中的参数变量
+  merge(uri, query = {}) {
+    const keys = Object.keys(query);
+    const arr = [];
+    for (const k of keys) {
+      arr.push(`${k}=${query[k]}`);
+    }
+    if (arr.length > 0) {
+      uri = `${uri}?${arr.join('&')}`;
+    }
+    return uri;
+  }
+
+  /**
+   * curl-get请求
+   * @param {String} uri 接口地址
+   * @param {String} project config中的项目key
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cget(uri, project, query, options) {
+    return this.toRequest(uri, project, null, query, options);
+  }
+
+  /**
+   * curl-post请求
+   * @param {String} uri 接口地址
+   * @param {String} project config中的项目key
+   * @param {Object} data post的body
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cpost(uri, project, data = {}, query, options) {
+    return this.toRequest(uri, project, data, query, options);
+  }
+
+  async toRequest(uri, project, data, query, options) {
+    const prefix = _.get(this.ctx.app.config.project, project);
+    if (!prefix) {
+      throw new BusinessError(
+        ErrorCode.SERVICE_FAULT,
+        `未设置用户权限项目的关联:config.project.${project} is undefined`
+      );
+    }
+    query = _.pickBy(
+      query,
+      val => val !== '' && val !== 'undefined' && val !== 'null'
+    );
+    if (!uri) console.error('uri不能为空');
+    if (_.isObject(query) && _.isObject(options)) {
+      const params = query.params ? query.params : query;
+      options = { ...options, params };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    // 是否多租户模式,需要改变headers
+    const headers = { 'content-type': 'application/json' };
+    const url = this.merge(`${prefix}${uri}`, options.params);
+    let res = await this.ctx.curl(url, {
+      method: isNullOrUndefined(data) ? 'get' : 'post',
+      url,
+      data,
+      dataType: 'json',
+      headers,
+      ...options,
+    });
+    if (res.status === 200) {
+      res = res.data || {};
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return { errcode, errmsg };
+      }
+      return res.data;
+    }
+    const { status } = res;
+    console.warn(`[${uri}] fail: ${status}-${res.data.message} `);
+  }
+}
+
+module.exports = HttpUtilService;

+ 66 - 0
app/service/util/rabbitMq.js

@@ -0,0 +1,66 @@
+'use strict';
+
+const Service = require('egg').Service;
+
+class RabbitmqService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+    this.exType = 'topic';
+    this.durable = true;
+  }
+
+  // 接收消息
+  async receiveQueueMsg(ex) {
+    this.ctx.logger.info('调用mq的' + ex);
+    const self = this;
+    const { mq } = self.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      await ch.assertExchange(ex, 'topic', { durable: true });
+      const q = await ch.assertQueue('', { exclusive: true });
+      await ch.bindQueue(q.queue, ex, '*');
+      await ch.consume(q.queue, msg => this.logMessage(msg, this), { noAck: true });
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+  async logMessage(msg) {
+    const result = msg.content.toString();
+    const headers = msg.properties.headers;
+  }
+
+  // mission队列处理
+  async mission() {
+    const { mq } = this.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      const queue = 'mission/market';
+      try {
+        // 创建队列:在没有队列的情况,直接获取会导致程序无法启动
+        await ch.assertQueue(queue, { durable: false });
+        await ch.consume(queue, msg => this.dealMission(msg), { noAck: true });
+      } catch (error) {
+        this.ctx.logger.error('未找到订阅的队列');
+      }
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+  // 执行任务
+  async dealMission(bdata) {
+    if (!bdata) this.ctx.logger.error('mission队列中信息不存在');
+    let data = bdata.content.toString();
+    try {
+      data = JSON.parse(data);
+    } catch (error) {
+      this.ctx.logger.error('数据不是object');
+    }
+    const { service, method, ...others } = data;
+    if (service && method) this.ctx.service[service][method](others);
+
+  }
+}
+
+module.exports = RabbitmqService;

+ 115 - 0
app/service/util/spm.js

@@ -0,0 +1,115 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 只属于这个项目的工具service
+class SpmService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'spm');
+    this.org = this.ctx.model.Organization;
+    this.personal = this.ctx.model.Personal;
+  }
+
+  /**
+   * 入口,根据method,分配到函数中进行处理,返回结果
+   * @param {Object} {method} method:在这个service的函数
+   * @param {*} body 条件
+   */
+  async index({ method }, body) {
+    return await this[method](body);
+  }
+
+  /**
+   * 获取用户信息,个人(专家)/企业都有可能
+   * @param {Object} condition 条件
+   */
+  async getAllUser(condition) {
+    const { ids } = condition;
+    const projection = 'name phone';
+    // 直接先用ids在企业表中查询
+    let arr = [];
+    const org = await this.org.find({ _id: ids }, projection);
+    arr = arr.concat(org);
+    if (org.length !== ids.length) {
+      // 还有个人用户,再查
+      const personal = await this.personal.find({ _id: ids }, projection);
+      arr = arr.concat(personal);
+    }
+    return arr;
+  }
+
+  /**
+   * 根据条件查找用户
+   * @param {Object} condition 查询条件
+   */
+  async getUser(condition) {
+    let res;
+    // 先查是不是企业
+    const org = await this.org.findOne(condition);
+    if (org) res = org;
+    else {
+      const personal = await this.personal.findOne(condition);
+      if (personal) res = personal;
+    }
+    return res;
+  }
+
+  /**
+   * 直接操作model做什么
+   * @param {Object} condition 条件
+   * @property {String} model 表
+   * @property {String} method mongoose的函数
+   * @property {Object} query 查询条件
+   * @property {Object} body 添加/修改数据
+   * @property {String/Object} projection 查询使用,选取指定字段
+   */
+  async useModel(condition = {}) {
+    const { method, query = {}, body = {}, projection } = condition;
+    let { model } = condition;
+    model = _.capitalize(model);
+    assert(model, '缺少指定表');
+    assert(method, '缺少使用函数');
+    // 区分method的情况
+    let res;
+    // 添加/批量添加
+    if (method === 'create' || method === 'insertMany') {
+      res = await this.ctx.model[model][method](body);
+    }
+    // 修改(updateOne)/批量修改(updateMany)
+    if (method.includes('update')) {
+      res = await this.ctx.model[model][method](query, body);
+    }
+    // 删除(deleteOne)/批量删除(deleteMany)
+    if (method.includes('delete')) {
+      res = await this.ctx.model[model][method](query);
+    }
+    // 查询(find,findById,findOne),反正都是只要query
+    if (method.includes('find')) {
+      res = await this.ctx.model[model][method](query, projection);
+    }
+    // 再下来,就是聚合了,聚合就算了吧.这个还跨项目写就没啥必要了
+    return res;
+  }
+
+  /**
+   * 调用service
+   * @param {Object} condition 条件
+   * @property {String} model 表
+   * @property {String} method mongoose的函数
+   */
+  async useService(condition) {
+    const { service, method, body } = condition;
+    assert(service, '缺少指定表');
+    assert(method, '缺少使用函数');
+    const arr = service.split('.');
+    let serve = this.ctx.service;
+    for (const i of arr) {
+      serve = serve[i];
+    }
+    return await serve[method](body);
+  }
+}
+
+module.exports = SpmService;

+ 144 - 0
app/service/util/util.js

@@ -0,0 +1,144 @@
+'use strict';
+const _ = require('lodash');
+const moment = require('moment');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { ObjectId } = require('mongoose').Types;
+const fs = require('fs');
+class UtilService extends CrudService {
+  constructor(ctx) {
+    super(ctx);
+    this.mq = this.ctx.mq;
+  }
+  async utilMethod(query, body) {
+    const Path = require('path');
+    const Excel = require('exceljs');
+
+    const { data } = await this.ctx.service.users.expert.query();
+    const root_path = 'E:\\exportFile\\';
+    const file_type = '';
+    if (!fs.existsSync(`${root_path}${file_type}`)) {
+      // 如果不存在文件夹,就创建
+      fs.mkdirSync(`${root_path}${file_type}`);
+    }
+    const workbook = new Excel.Workbook();
+    let sheet;
+    sheet = workbook.addWorksheet('sheet');
+    const meta = this.getHeader();
+    const head = meta.map(i => i.label);
+    // sheet.addRows(head);
+    const rows = [];
+    rows.push(head);
+    for (let i = 0; i < data.length; i++) {
+      const e = data[i];
+      const row = [];
+      let imgid;
+      for (const obj of meta) {
+        const { key } = obj;
+        if (key !== 'img_path') {
+          row.push(e[key] || '');
+        } else if (e.img_path) {
+          try {
+            const suffix = Path.extname(e.img_path).substring(1);
+            // 先请求图片buffer,然后addImage存起来
+            const res = await this.ctx.curl(`http://broadcast.waityou24.cn${e.img_path}`);
+            if (res.status === 200) {
+              const buffer = res.data;
+              imgid = workbook.addImage({
+                buffer,
+                extension: suffix,
+              });
+            }
+          } catch (error) {
+            console.log(`${e.name}图片下载失败`);
+          }
+
+        }
+      }
+      rows.push(row);
+      if (imgid || imgid === 0) {
+        sheet.addImage(imgid, {
+          tl: { col: 15.2, row: i + 1 + 0.2 },
+          br: { col: 16, row: i + 1 + 1 },
+          editAs: 'oneCell',
+        });
+      }
+    }
+    sheet.addRows(rows);
+    const filepath = `${root_path}专家导出.xlsx`;
+    await workbook.xlsx.writeFile(filepath);
+  }
+
+  getHeader() {
+    return [
+      { key: 'name', label: '用户姓名' },
+      { key: 'phone', label: '联系电话' },
+      { key: 'education', label: '最高学历' },
+      { key: 'school', label: '毕业学校' },
+      { key: 'birthDate', label: '出生日期' },
+      { key: 'email', label: '电子邮箱' },
+      { key: 'qqwx', label: 'QQ/微信' },
+      { key: 'company', label: '工作单位' },
+      { key: 'zwzc', label: '职务职称' },
+      { key: 'expertise', label: '擅长领域' },
+      { key: 'workexperience', label: '工作经历' },
+      { key: 'scientific', label: '科研综述' },
+      { key: 'undertakingproject', label: '承担项目' },
+      { key: 'scienceaward', label: '科技奖励' },
+      { key: 'social', label: '社会任职' },
+      { key: 'img_path', label: '用户头像' },
+    ];
+  }
+
+  dealQuery(query) {
+    return this.turnFilter(this.turnDateRangeQuery(query));
+  }
+
+  /**
+   * 将查询条件中模糊查询的标识转换成对应object
+   * @param {Object} filter 查询条件
+   */
+  turnFilter(filter) {
+    const str = /^%\S*%$/;
+    const keys = Object.keys(filter);
+    for (const key of keys) {
+      const res = key.match(str);
+      if (res) {
+        const newKey = key.slice(1, key.length - 1);
+        filter[newKey] = new RegExp(filter[key]);
+        delete filter[key];
+      }
+    }
+    return filter;
+  }
+  /**
+   * 将时间转换成对应查询Object
+   * @param {Object} filter 查询条件
+   */
+  turnDateRangeQuery(filter) {
+    const keys = Object.keys(filter);
+    for (const k of keys) {
+      if (k.includes('@')) {
+        const karr = k.split('@');
+        //  因为是针对处理范围日期,所以必须只有,开始时间和结束时间
+        if (karr.length === 2) {
+          const type = karr[1];
+          if (type === 'start') {
+            filter[karr[0]] = {
+              ..._.get(filter, karr[0], {}),
+              $gte: filter[k],
+            };
+          } else {
+            filter[karr[0]] = {
+              ..._.get(filter, karr[0], {}),
+              $lte: filter[k],
+            };
+          }
+          delete filter[k];
+        }
+      }
+    }
+    return filter;
+  }
+
+}
+module.exports = UtilService;

+ 14 - 0
appveyor.yml

@@ -0,0 +1,14 @@
+environment:
+  matrix:
+    - nodejs_version: '10'
+
+install:
+  - ps: Install-Product node $env:nodejs_version
+  - npm i npminstall && node_modules\.bin\npminstall
+
+test_script:
+  - node --version
+  - npm --version
+  - npm run test
+
+build: off

+ 55 - 0
config/config.default.js

@@ -0,0 +1,55 @@
+/* eslint valid-jsdoc: "off" */
+
+'use strict';
+const { jwt } = require('./config.secret');
+
+/**
+ * @param {Egg.EggAppInfo} appInfo app info
+ */
+module.exports = appInfo => {
+  /**
+   * built-in config
+   * @type {Egg.EggAppConfig}
+   **/
+  const config = exports = {};
+
+  // use for cookie sign key, should change to your own and keep security
+  config.keys = appInfo.name + '_1623739063308_1869';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+
+  config.cluster = {
+    listen: {
+      port: 9901,
+    },
+  };
+  config.appName = 'st'; // 路由的前缀会引用这个变量,组成接口地址
+  config.jwt = {
+    ...jwt,
+    expiresIn: '1d',
+    issuer: 'shitang',
+  };
+  config.dbName = 'shitang';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      // user: 'admin',
+      // pass: '111111',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+    },
+  };
+  config.wx = {};
+
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 12 - 0
config/config.prod.js

@@ -0,0 +1,12 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+
+  config.logger = {
+    level: 'INFO',
+    consoleLevel: 'INFO',
+  };
+
+  return config;
+};

+ 7 - 0
config/config.secret.js

@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = {
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+  },
+};

+ 6 - 0
config/plugin.js

@@ -0,0 +1,6 @@
+'use strict';
+
+/** @type Egg.EggPlugin */
+exports.multiTenancy = {
+  enable: true,
+};

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "service",
+  "version": "1.0.0",
+  "description": "食堂服务端",
+  "private": true,
+  "egg": {
+    "framework": "naf-framework-mongoose"
+  },
+  "dependencies": {
+    "egg": "^2.15.1",
+    "egg-scripts": "^2.11.0",
+    "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.1",
+    "naf-framework-mongoose": "^0.6.11",
+    "string-random": "^0.1.3"
+  },
+  "devDependencies": {
+    "autod": "^3.0.1",
+    "autod-egg": "^1.1.0",
+    "egg-bin": "^4.11.0",
+    "egg-ci": "^1.11.0",
+    "egg-mock": "^3.21.0",
+    "eslint": "^5.13.0",
+    "eslint-config-egg": "^7.1.0"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  },
+  "scripts": {
+    "start": "egg-scripts start --daemon --title=egg-server-service",
+    "stop": "egg-scripts stop --title=egg-server-service",
+    "dev": "egg-bin dev",
+    "debug": "egg-bin debug",
+    "test": "npm run lint -- --fix && npm run test-local",
+    "test-local": "egg-bin test",
+    "cov": "egg-bin cov",
+    "lint": "eslint .",
+    "ci": "npm run lint && npm run cov",
+    "autod": "autod"
+  },
+  "ci": {
+    "version": "10"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "lrf",
+  "license": "MIT"
+}

+ 20 - 0
test/app/controller/home.test.js

@@ -0,0 +1,20 @@
+'use strict';
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/controller/home.test.js', () => {
+  it('should assert', () => {
+    const pkg = require('../../../package.json');
+    assert(app.config.keys.startsWith(pkg.name));
+
+    // const ctx = app.mockContext({});
+    // yield ctx.service.xx();
+  });
+
+  it('should GET /', () => {
+    return app.httpRequest()
+      .get('/')
+      .expect('hi, egg')
+      .expect(200);
+  });
+});