lrf402788946 4 years ago
commit
766a1d54ca
47 changed files with 1667 additions and 0 deletions
  1. 27 0
      .autod.conf.js
  2. 1 0
      .eslintignore
  3. 3 0
      .eslintrc
  4. 24 0
      .github/PULL_REQUEST_TEMPLATE.md
  5. 10 0
      .gitignore
  6. 13 0
      .travis.yml
  7. 26 0
      README.md
  8. 29 0
      README.zh_CN.md
  9. 6 0
      app.js
  10. 5 0
      app/extend/application.js
  11. 35 0
      app/extend/context.js
  12. 21 0
      app/middleware/access-log.js
  13. 38 0
      app/middleware/error-handler.js
  14. 45 0
      app/middleware/error-mongo.js
  15. 21 0
      app/service/seq.js
  16. 21 0
      app/view/error.njk
  17. 15 0
      appveyor.yml
  18. 39 0
      config/config.default.js
  19. 32 0
      config/config.error.js
  20. 24 0
      config/plugin.js
  21. 76 0
      index.d.ts
  22. 9 0
      index.js
  23. 229 0
      lib/controller/crud-controller.js
  24. 69 0
      lib/controller/index.d.ts
  25. 5 0
      lib/controller/index.js
  26. 24 0
      lib/framework.js
  27. 1 0
      lib/model/README.md
  28. 10 0
      lib/model/meta-plugin.js
  29. 24 0
      lib/model/schema.js
  30. 9 0
      lib/model/seq.js
  31. 114 0
      lib/plugin/egg-multi-tenancy/app/extend/application.js
  32. 53 0
      lib/plugin/egg-multi-tenancy/app/extend/context.js
  33. 7 0
      lib/plugin/egg-multi-tenancy/config/config.default.js
  34. 6 0
      lib/plugin/egg-multi-tenancy/package.json
  35. 163 0
      lib/service/axios-service.js
  36. 126 0
      lib/service/crud-service.js
  37. 92 0
      lib/service/index.d.ts
  38. 7 0
      lib/service/index.js
  39. 48 0
      lib/service/naf-service.d.ts
  40. 25 0
      lib/service/naf-service.js
  41. 65 0
      package.json
  42. 12 0
      test/fixtures/example/app/controller/home.js
  43. 7 0
      test/fixtures/example/app/router.js
  44. 3 0
      test/fixtures/example/config/config.unittest.js
  45. 4 0
      test/fixtures/example/package.json
  46. 26 0
      test/framework.test.js
  47. 18 0
      test/uri.js

+ 27 - 0
.autod.conf.js

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

+ 1 - 0
.eslintignore

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

+ 3 - 0
.eslintrc

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

+ 24 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,24 @@
+<!--
+Thank you for your pull request. Please review below requirements.
+Bug fixes and new features should include tests and possibly benchmarks.
+Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md
+
+感谢您贡献代码。请确认下列 checklist 的完成情况。
+Bug 修复和新功能必须包含测试,必要时请附上性能测试。
+Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md
+-->
+
+##### Checklist
+<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
+
+- [ ] `npm test` passes
+- [ ] tests and/or benchmarks are included
+- [ ] documentation is changed or added
+- [ ] commit message follows commit guidelines
+
+##### Affected core subsystem(s)
+<!-- Provide affected core subsystem(s). -->
+
+
+##### Description of change
+<!-- Provide a description of the change below this comment. -->

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+logs/
+npm-debug.log
+node_modules/
+coverage/
+.idea/
+run/
+.DS_Store
+*.swp
+package-lock.json
+

+ 13 - 0
.travis.yml

@@ -0,0 +1,13 @@
+sudo: false
+language: node_js
+node_js:
+  - '8'
+  - '9'
+before_install:
+  - npm i npminstall -g
+install:
+  - npminstall
+script:
+  - npm run ci
+after_script:
+  - npminstall codecov && codecov

+ 26 - 0
README.md

@@ -0,0 +1,26 @@
+# naf-framework-mongoose-free
+
+free egg framework use mongoose
+
+## QuickStart
+
+```bash
+$ npm install
+$ npm test
+```
+
+publish your framework to npm, then change app's dependencies:
+
+```js
+// {app_root}/index.js
+require('naf-framework-mongoose').startCluster({
+  baseDir: __dirname,
+  // port: 7001, // default to 7001
+});
+
+```
+
+## Questions & Suggestions
+
+Please open an issue [here](https://github.com/eggjs/egg/issues).
+

+ 29 - 0
README.zh_CN.md

@@ -0,0 +1,29 @@
+# naf-framework-mongoose-free
+
+naf egg framework use mongoose
+
+## 快速入门
+
+```bash
+$ npm install
+$ npm test
+```
+
+在应用中声明使用框架:
+
+```js
+// {app_root}/index.js
+require('naf-framework-mongoose').startCluster({
+  baseDir: __dirname,
+  // port: 7001, // default to 7001
+});
+
+```
+
+## 提问交流
+
+请到 [egg issues](https://github.com/eggjs/egg/issues) 异步交流。
+
+## License
+
+[MIT](LICENSE)

+ 6 - 0
app.js

@@ -0,0 +1,6 @@
+'use strict';
+
+module.exports = app => {
+  // 处理请求中抛出的MongoError错误类型,按正常http请求响应业务错误消息
+  app.config.coreMiddleware.unshift('accessLog', 'errorHandler', 'errorMongo');
+};

+ 5 - 0
app/extend/application.js

@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+
+};

+ 35 - 0
app/extend/context.js

@@ -0,0 +1,35 @@
+'use strict';
+
+const is = require('is-type-of');
+const { isString } = require('util');
+const { ErrorCode } = require('naf-core').Error;
+
+// this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性
+module.exports = {
+  get requestparam() {
+    return { ...this.query, ...this.request.body };
+  },
+
+  // 返回JSON结果
+  json(errcode = 0, errmsg = 'ok', data = {}) {
+    if (is.object(errmsg)) {
+      data = errmsg;
+      errmsg = 'ok';
+    }
+    this.body = { errcode, errmsg, ...data };
+  },
+  success(message = 'ok', data = {}) {
+    this.json(0, message, data);
+  },
+  fail(errcode, errmsg, details) {
+    if (isString(errcode)) {
+      this.json(ErrorCode.BUSINESS, errcode, errmsg);
+    } else {
+      this.json(errcode, errmsg, { details });
+    }
+  },
+  ok(message, data) {
+    this.success(message, data);
+  },
+
+};

+ 21 - 0
app/middleware/access-log.js

@@ -0,0 +1,21 @@
+// app/middleware/gzip.js
+'use strict';
+
+module.exports = ({ enable = false, body = false }) => async function accessLog(ctx, next) {
+
+  if (enable) {
+    ctx.app.logger.debug(`[access-log] ${ctx.logger.paddingMessage} start...`);
+    if (body && ctx.method !== 'GET') {
+      ctx.app.logger.debug('[access-log] request body', ctx.request.body);
+    }
+  }
+
+  await next();
+
+  if (enable) {
+    ctx.app.logger.info(`[access-log] ${ctx.logger.paddingMessage} ${ctx.response.status} ${ctx.response.message}`);
+  }
+  if (body && ctx.acceptJSON) {
+    ctx.app.logger.debug('[access-log] response body', ctx.response && ctx.response.body);
+  }
+};

+ 38 - 0
app/middleware/error-handler.js

@@ -0,0 +1,38 @@
+'use strict';
+
+const { AssertionError } = require('assert');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+
+module.exports = options => {
+  return async function(ctx, next) {
+    try {
+      await next();
+    } catch (err) {
+      if (err instanceof BusinessError /* && ctx.acceptJSON */) {
+        // 业务错误
+        let res = { errcode: err.errcode, errmsg: err.errmsg };
+        if (options.details) {
+          res = { errcode: err.errcode, errmsg: err.errmsg, details: err.details };
+        }
+        ctx.body = res;
+        ctx.status = 200;
+        ctx.app.logger.warn(`[error-handler] BusinessError: ${err.errcode}, ${err.errmsg}`);
+        ctx.app.logger.debug(err);
+      } else if (err instanceof AssertionError /* && ctx.acceptJSON */) {
+        // Assert错误
+        const errcode = ErrorCode.BADPARAM;
+        let res = { errcode, errmsg: BusinessError.getErrorMsg(errcode) };
+        if (options.details) {
+          res = { errcode, errmsg: BusinessError.getErrorMsg(errcode), details: err.message };
+        }
+        ctx.body = res;
+        ctx.status = 200;
+        ctx.app.logger.warn(`[error-handler] AssertionError: ${err.message}`);
+        ctx.app.logger.debug(err);
+      } else {
+        throw err;
+      }
+    }
+  };
+};

+ 45 - 0
app/middleware/error-mongo.js

@@ -0,0 +1,45 @@
+'use strict';
+
+// const { MongoError } = require('mongodb-core');
+const { ValidationError } = require('mongoose').Error;
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+// mongodb错误处理
+const handleMongoError = (ctx, err, options) => {
+  let errcode = ErrorCode.DATABASE_FAULT;
+  if (err.code === 11000) {
+    errcode = ErrorCode.DATA_EXISTED;
+  } /* else if (err instanceof ValidationError && ctx.acceptJSON) {
+    // 数据库错误
+    errcode = ErrorCode.BADPARAM;
+  }*/
+
+
+  let res = { errcode, errmsg: BusinessError.getErrorMsg(errcode) };
+  if (options.details) {
+    // expose details
+    res = { errcode, errmsg: BusinessError.getErrorMsg(errcode), details: err.message };
+  }
+  ctx.body = res;
+  ctx.status = 200;
+  ctx.app.logger.warn(`[error-mongo] MongoError: ${err.code}, ${err.message}`);
+  ctx.app.logger.debug(err);
+};
+
+module.exports = (options = {}) => {
+  return async function(ctx, next) {
+    try {
+      await next();
+    } catch (err) {
+      if (err.name === 'MongoError' && ctx.acceptJSON) {
+        // 数据库错误
+        handleMongoError(ctx, err, options);
+      } else if (err instanceof ValidationError && ctx.acceptJSON) {
+        // 数据库错误
+        handleMongoError(ctx, err, options);
+      } else {
+        throw err;
+      }
+    }
+  };
+};

+ 21 - 0
app/service/seq.js

@@ -0,0 +1,21 @@
+'use strict';
+
+const assert = require('assert');
+const Service = require('egg').Service;
+
+
+class SequenceService extends Service {
+  async nextval(name) {
+    const { ctx } = this;
+    assert(ctx.model.Seq, 'Model Seq not found!');
+    assert(name, 'seq name must not empty!');
+
+    const _id = (ctx.tenant && `${ctx.tenant}_${name}`) || name;
+    const { value } = await ctx.model.Seq.findByIdAndUpdate(_id,
+      { $inc: { value: 1 } },
+      { new: true, upsert: true }).exec();
+    return value;
+  }
+}
+
+module.exports = SequenceService;

+ 21 - 0
app/view/error.njk

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+
+<head>
+  <title>抱歉,出错了</title>
+  <meta charset="utf-8"></meta>
+  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"></meta>
+  <link rel="stylesheet" type="text/css" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"></link>
+</head>
+
+<body>
+  <div class="weui-msg">
+    <div class="weui-msg__icon-area"><i class="weui-icon-warn weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">业务错误: {{errcode}}</h2>
+      <p class="weui-msg__desc">{{errmsg}}</p>
+    </div>
+  </div>
+</body>
+
+</html>

+ 15 - 0
appveyor.yml

@@ -0,0 +1,15 @@
+environment:
+  matrix:
+    - nodejs_version: '8'
+    - nodejs_version: '9'
+
+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

+ 39 - 0
config/config.default.js

@@ -0,0 +1,39 @@
+'use strict';
+
+const ErrorConfig = require('./config.error.js');
+
+module.exports = appInfo => {
+  const config = {};
+
+  /**
+   * some description
+   * @member Config#test
+   * @property {String} key - some description
+   */
+  config.test = {
+    key: appInfo.name + '_123456',
+  };
+
+  // 安全配置
+  config.security = {
+    csrf: {
+      // ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
+      enable: false,
+    },
+  };
+
+  config.onerror = ErrorConfig;
+
+  config.errorMongo = {
+    details: true,
+  };
+  config.errorHandler = {
+    details: true,
+  };
+  config.accessLog = {
+    enable: true,
+    body: false,
+  };
+
+  return config;
+};

+ 32 - 0
config/config.error.js

@@ -0,0 +1,32 @@
+'use strict';
+
+const { NafError, BusinessError } = require('naf-core').Error;
+
+module.exports = {
+  json(err, ctx) {
+    // json hander
+    if (err instanceof BusinessError) {
+      // 业务错误
+      ctx.body = { errcode: err.errcode, errmsg: err.errmsg };
+      ctx.status = 200;
+    } else if (err instanceof NafError) {
+      // 框架错误
+      ctx.body = { errcode: err.errcode, errmsg: err.errmsg };
+      ctx.status = 500;
+    } else if (err instanceof Error) {
+      // 其他错误
+      const res = { errcode: err.status || 500, errmsg: '系统错误', details: err.message };
+      if (err.status === 422) {
+        // for egg-validate
+        res.errmsg = '数据校验错误';
+        res.details = err.errors;
+      }
+      ctx.body = res;
+      ctx.status = 500;
+    } else {
+      // 未知错误
+      ctx.body = { errcode: 500, errmsg: '未知错误', details: err };
+      ctx.status = 500;
+    }
+  },
+};

+ 24 - 0
config/plugin.js

@@ -0,0 +1,24 @@
+'use strict';
+
+const path = require('path');
+
+// add you build-in plugin here, example:
+// exports.nunjucks = {
+//   enable: true,
+//   package: 'egg-view-nunjucks',
+// };
+
+exports.multiTenancy = {
+  enable: false,
+  path: path.join(__dirname, '../lib/plugin/egg-multi-tenancy'),
+};
+
+exports.mongoose = {
+  enable: true,
+  package: 'egg-mongoose',
+};
+
+exports.validate = {
+  enable: true,
+  package: 'egg-validate',
+};

+ 76 - 0
index.d.ts

@@ -0,0 +1,76 @@
+import * as EggApplication from 'egg';
+import * as mongoose from 'mongoose';
+
+declare module 'egg' {
+
+  type MongooseModels = {
+    [key: string]: mongoose.Model<any>
+  };
+
+  type MongooseSingleton = {
+    clients: Map<string, mongoose.Connection>,
+    get (id: string) : mongoose.Connection
+  };
+
+  type MongooseConfig = {
+    url: string,
+    options?: mongoose.ConnectionOptions
+  };
+
+  // extend app
+  interface Application {
+    mongooseDB: mongoose.Connection | MongooseSingleton;
+    mongoose: typeof mongoose;
+    model: MongooseModels;
+  }
+
+  // extend context
+  interface Context {
+    model: MongooseModels;
+
+    /**
+     * compose from ctx.query and ctx.request.body
+     */
+    requestparam: any;
+
+    /**
+     * 租户ID,用于多租户系统
+     */
+    tenant: string;
+    /**
+     * 当前用户ID
+     */
+    userid: string;
+    /**
+     * 当前用户角色
+     */
+    role: string;
+    /**
+     * 当前用户标签
+     */
+    tags: string;
+
+    // 返回JSON结果
+    json(errcode: number, errmsg: string, data: object);
+    success(message: string, data: object);
+    fail(errcode: number, errmsg: string, details: any);
+    /**
+     * same to success(message: string, data: object);
+     */
+    ok(message: string, data: object);
+  }
+
+  // extend your config
+  interface EggAppConfig {
+    mongoose: {
+      url?: string,
+      options?: mongoose.ConnectionOptions,
+      client?: MongooseConfig,
+      clients?: {
+        [key: string]: MongooseConfig
+      }
+    };
+  }
+
+}
+

+ 9 - 0
index.js

@@ -0,0 +1,9 @@
+'use strict';
+
+module.exports = require('./lib/framework.js');
+module.exports.service = require('./lib/service');
+module.exports.controller = require('./lib/controller');
+
+module.exports.Services = require('./lib/service');
+module.exports.Controllers = require('./lib/controller');
+module.exports.ErrorConfig = require('./config/config.error.js');

+ 229 - 0
lib/controller/crud-controller.js

@@ -0,0 +1,229 @@
+'use strict';
+/**
+ * 基于meta描述数据生成Controller类
+ * 按照描述信息将web请求中的数据提取出来,组织为service的调用参数
+ * {Controller名字}.json
+ * meta文件属性描述:
+ * service 字符串或者对象,字符串表示服务方法名,默认与action同名;
+ *         对象可包含name、func两个属性,func与使用字符串是含义相同,
+ *         name表示服务名
+ * parameters 请求参数描述,对象类型,可以包含属性为:'query'、 'params'、 'requestBody'、'options',
+ *            分别对应eggjs中的三个请求参数来源和可选参数类型
+ * options 可选参数,对象类型,可以指定排序字段和一些常量参数值,具体内容格式随意,在服务中解析
+ * params 路由参数,数组类型,从ctx.params中提取数据
+ * requestBody 请求数据,数组类型,从ctx.request.body中提取数据
+ * query 查询参数,数组类型'options',从ctx.query中提取数据
+ * 完整格式:
+  "action": {
+    "parameters": {
+      "params": ["field1", "field2",...], // 可选
+      "query": ["field1", "field2",...], // 可选
+      "requestBody": ["field1", "field2",...], // 可选
+      "options": { "ext1": "value1"}, // 可选
+    },
+    "service": "query", // 可选
+    "options": { // 可选
+      "sort": ["field1", "field2",...]
+    }
+  },
+ * 简单格式:
+ * 如果meta对象中没有parameters属性,则按简单格式处理,即整个meta的内容都作为parameters来处理
+  "action": {
+    "params": ["attr1", "attr2",...], // 可选
+    "query": ["attr1", "attr2",...], // 可选
+    "requestBody": ["attr1", "attr2",...], // 可选
+    "options": { "ext1": "value1"}, // 可选
+  },
+* meta实例:
+ {
+  "create": {
+    "requestBody": ["code","name","order"]
+  },
+  "delete": {
+    "query": { "id": "_id" }
+  },
+  "update": {
+    "query": ["_id"],
+    "requestBody": ["name","order"]
+  },
+  "list": {
+    "parameters": {},
+    "service": "query",
+    "options": {
+      "sort": ["order", "code"]
+    }
+  },
+  "fetch": {
+    "query": ["_id"]
+    "service": {
+      "name": "items",
+      "func": "query"
+    }
+  }
+}
+* 服务接口:
+* someService.someMethod(requestParams, requestBody, options)
+* 服务参数:
+* requestParams 请求参数,必须
+* requestBody 请求数据,可选
+* options 可选参数,可选
+*/
+const assert = require('assert');
+const _ = require('lodash');
+const is = require('is-type-of');
+const { trimData } = require('naf-core').Util;
+
+const key_reg = /^([!#]*)(.*)$/;
+
+const MapData = (data, names) => {
+  if (_.isArray(names)) {
+    // return names.reduce((p, c) => {
+    //   p[c] = data[c];
+    //   return p;
+    // }, {});
+    names = names.reduce((p, c) => {
+      // p[c] = c.charAt(0) === '!' ? c.substr(1) : c;
+      p[c] = key_reg.exec(c)[2];
+      return p;
+    }, {});
+  }
+  if (_.isObject(names)) {
+    // TODO: 参数映射方式,.eg { "id": "_id", "corp.id": "corpid", "src": "dest" }
+    return Object.entries(names).reduce((p, [ key, val ]) => {
+      // const required = key.charAt(0) === '!';
+      const [ , qualifier, _key ] = key_reg.exec(key);
+      key = _key;
+      const required = qualifier.includes('!');
+      const numeric = qualifier.includes('#');
+      let value = _.get(data, key);
+      if (required) {
+        // key = key.substr(1);
+        value = _.get(data, key);
+        assert(!_.isUndefined(value), `${key}不能为空`);
+      }
+      if (!_.isString(val)) {
+        val = key;
+      }
+      if (!_.isUndefined(value)) {
+        if (numeric) {
+          assert(_.isNumber(value) || (_.isString(value) && /^-?\d+$/.test(value)), `${key}必须为数字`);
+          value = Number(value);
+        }
+        p[val] = value;
+      }
+      return p;
+    }, {});
+  }
+  return data;
+};
+let MapOptions;
+const MapParameters = (ctx, opts) => {
+  const include = [ 'parameters', 'query', 'params', 'requestBody', 'options' ];
+  const _opts = trimData({ ...opts }, null, include);
+  const keys = Object.keys(_opts);
+  return keys.map(key => {
+    // 嵌套调用
+    if (key === 'parameters') return MapParameters(ctx, opts[key]);
+    if (key === 'options') return MapOptions(ctx, opts[key]);
+
+    let data = ctx[key];
+    if (key === 'requestBody') data = ctx.request.body;
+    const names = opts[key];
+    // if (_.isArray(names)) {
+    //   return names.reduce((p, c) => {
+    //     p[c] = data[c];
+    //     return p;
+    //   }, {});
+    // }
+    // return data;
+    return MapData(data, names);
+  }).reduce((p, c) => {
+    if (c) {
+      p = { ...c, ...p };
+    }
+    return p;
+  }, {});
+};
+const MapRequestBody = (ctx, opts) => {
+  // if (_.isArray(opts)) {
+  //   const data = ctx.request.body;
+  //   return opts.reduce((p, c) => {
+  //     p[c] = data[c];
+  //     return p;
+  //   }, {});
+  // }
+  // return MapParameters(ctx, opts) || {};
+
+  if (_.isObject(opts) && [ 'parameters', 'query', 'params', 'requestBody' ].some(p => Object.keys(opts).includes(p))) {
+    return MapParameters(ctx, opts) || {};
+  }
+  return MapData(ctx.request.body, opts);
+};
+MapOptions = (ctx, opts) => {
+  const exclude = [ 'parameters', 'query', 'params', 'requestBody' ];
+  const _opts = trimData({ ...opts }, exclude) || {};
+  const params = MapParameters(ctx, opts) || {};
+
+  if (!_.isUndefined(params.skip) && !_.isNumber(params.skip)) params.skip = Number(params.skip);
+  if (!_.isUndefined(params.limit) && !_.isNumber(params.limit)) params.limit = Number(params.limit);
+
+  return { ...params, ..._opts };
+};
+
+const CrudController = (cls, meta) => {
+  Object.keys(meta)
+    .forEach(key => {
+      // Do not override existing functions
+      if (!cls.prototype[key]) {
+        const { parameters, requestBody, options } = meta[key];
+        cls.prototype[key] = async function() {
+          const { ctx } = this;
+          const requestParams = MapParameters(ctx, parameters || meta[key]) || {};
+          const _requestBody = requestBody && MapRequestBody(ctx, requestBody);
+          const _options = options && MapOptions(ctx, options);
+          const serviceParams = [ requestParams ];
+          if (requestBody) {
+            serviceParams.push(_requestBody);
+          }
+          if (options) {
+            serviceParams.push(_options);
+          }
+
+          let { service } = meta[key];
+          // 修改service元数据的定义方式
+          // const funcName = (_.isObject(service) && service.action) || (_.isString(service) && service) || key;
+          // const provider = (_.isObject(service) && service.name && ctx.service[service.name]) || this.service;
+          if (_.isString(service)) { // TODO: 解析service为对象
+            const tokens = service.split('.');
+            service = { action: tokens.pop() };
+            if (tokens.length > 0) {
+              service.name = tokens;
+            }
+          }
+          const funcName = (_.isObject(service) && service.action) || key;
+          const provider = (_.isObject(service) && service.name && _.get(ctx.service, service.name)) || this.service;
+
+          const func = provider[funcName];
+          if (!is.asyncFunction(func)) {
+            throw new Error(`service not support function ${funcName}`);
+          }
+          const data = await func.call(provider, ...serviceParams);
+          let res = { data };
+          // 统计数据总数,处理分页
+          if (_options && _options.count) {
+            const funcName = _.isString(_options.count) ? _options.count : 'count';
+            const func = provider[funcName];
+            if (!is.asyncFunction(func)) {
+              throw new Error('not support count function');
+            }
+            const total = await func.call(provider, ...serviceParams);
+            res = { data, total };
+          }
+          this.ctx.ok(res);
+        };
+      }
+    });
+  return cls;
+};
+
+module.exports = CrudController;

+ 69 - 0
lib/controller/index.d.ts

@@ -0,0 +1,69 @@
+import * as EggApplication from 'egg';
+import * as mongoose from 'mongoose';
+
+declare module 'egg' {
+
+  type MongooseModels = {
+    [key: string]: mongoose.Model<any>
+  };
+
+  type MongooseSingleton = {
+    clients: Map<string, mongoose.Connection>,
+    get(id: string): mongoose.Connection
+  };
+
+  type MongooseConfig = {
+    url: string,
+    options?: mongoose.ConnectionOptions
+  };
+
+  // extend app
+  interface Application {
+    mongooseDB: mongoose.Connection | MongooseSingleton;
+    mongoose: typeof mongoose;
+    model: MongooseModels;
+    tenantModel(tenant: string): MongooseModels;
+  }
+
+  // extend context
+  interface Context {
+    model: MongooseModels;
+
+    /**
+     * compose from ctx.query and ctx.request.body
+     */
+    requestparam: any;
+
+    /**
+     * 租户ID,用于多租户系统
+     */
+    tenant: string;
+
+    // 返回JSON结果
+    json(errcode: number, errmsg: string, data: object);
+    success(message: string, data: object);
+    fail(errcode: number, errmsg: string, details: any);
+    /**
+     * same to success(message: string, data: object);
+     */
+    ok(message: string, data: object);
+  }
+
+  // extend your config
+  interface EggAppConfig {
+    mongoose: {
+      url?: string,
+      options?: mongoose.ConnectionOptions,
+      client?: MongooseConfig,
+      clients?: {
+        [key: string]: MongooseConfig
+      }
+    };
+  }
+
+}
+
+/**
+* wrapper crud metods for Controller class
+*/
+export function CrudController(cls: EggApplication.Controller, meta: any): EggApplication.Controller;

+ 5 - 0
lib/controller/index.js

@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  CrudController: require('./crud-controller')
+};

+ 24 - 0
lib/framework.js

@@ -0,0 +1,24 @@
+'use strict';
+
+const path = require('path');
+// const naf = require('naf-framework');
+const egg = require('egg');
+
+const EGG_PATH = Symbol.for('egg#eggPath');
+
+class Application extends egg.Application {
+  get [EGG_PATH]() {
+    return path.dirname(__dirname);
+  }
+}
+
+class Agent extends egg.Agent {
+  get [EGG_PATH]() {
+    return path.dirname(__dirname);
+  }
+}
+
+module.exports = Object.assign(egg, {
+  Application,
+  Agent,
+});

+ 1 - 0
lib/model/README.md

@@ -0,0 +1 @@
+###此文件夹定义一些公共类型的Model Schema

+ 10 - 0
lib/model/meta-plugin.js

@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = exports = function metaPlugin(schema/* , options*/) {
+  schema.add({
+    meta: {
+      state: { type: Number, default: 0 }, // 数据状态: 0-正常;1-标记删除
+      comment: String,
+    } });
+  schema.set('timestamps', { createdAt: 'meta.createdAt', updatedAt: 'meta.updatedAt' });
+};

+ 24 - 0
lib/model/schema.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+// 代码
+const codeSchema = new Schema({
+  code: { type: String, required: true, maxLength: 64 },
+  name: String,
+}, { _id: false });
+
+// 密码
+const secretSchema = new Schema({
+  // 加密类型:plain、hash、encrypt等
+  mech: { type: String, required: true, maxLength: 64, default: 'plain' },
+  // 密码值
+  secret: { type: String, required: true, maxLength: 128 },
+}, { _id: false, timestamps: true, select: false });
+
+
+module.exports = {
+  NullableString: len => ({ type: String, maxLength: len }),
+  RequiredString: len => ({ type: String, required: true, maxLength: len }),
+  CodeNamePair: codeSchema,
+  Secret: secretSchema,
+};

+ 9 - 0
lib/model/seq.js

@@ -0,0 +1,9 @@
+'use strict';
+const { RequiredString } = require('./schema');
+
+const SchemaDefine = {
+  _id: RequiredString(64),
+  value: Number,
+};
+
+module.exports = SchemaDefine;

+ 114 - 0
lib/plugin/egg-multi-tenancy/app/extend/application.js

@@ -0,0 +1,114 @@
+'use strict';
+
+const _ = require('lodash');
+const TENANT_MODELS = Symbol('Context#models@tenant');
+
+// 已废弃
+const _loadModel = (app, tenant) => {
+  app.logger.info(`[multi-tenancy] Load tenant models for ${tenant}`);
+  const model = {};
+  _.forEach(app.model, (val, key) => {
+    const modelName = `${val.modelName}@${tenant}`;
+    const collName = `_${tenant}.${val.collection.name}`;
+    const multiTenancy = val.schema.get('multi-tenancy');
+    if (multiTenancy) {
+      app.logger.debug(`[multi-tenancy] ${modelName} loaded`);
+      model[key] = val.db.model(modelName, val.schema, collName);
+    } else {
+      app.logger.debug(`[multi-tenancy] skip ${val.modelName}, schema not enable multi-tenancy.`);
+      model[key] = val;
+    }
+  });
+  return model;
+};
+
+const multiTenancyPlugin = (schema, options = {}) => {
+  const { defaultTenant = 'master' } = options;
+  schema.add({
+    _tenant: { type: String, default: defaultTenant, index: true },
+  });
+
+  schema.pre('save', async function() {
+    const tenant = schema.get('x-tenant');
+    if (tenant !== 'global') {
+      this._tenant = schema.get('x-tenant');
+    }
+  });
+
+  const querys = [ 'count', 'countDocuments', 'find', 'findOne', 'findOneAndRemove', 'findOneAndUpdate', 'remove', 'deleteOne', 'deleteMany', 'update', 'updateOne', 'updateMany' ];
+  querys.forEach(action => {
+    schema.pre(action, async function() {
+      const tenant = schema.get('x-tenant');
+      if (tenant !== 'global') {
+        this.where('_tenant').equals(schema.get('x-tenant'));
+      }
+    });
+  });
+};
+
+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;
+};
+
+// this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性
+module.exports = {
+
+  // 多租户系统中的model对象
+  tenantModel(tenant) {
+    const { defaultTenant: _defaultTenant } = this.config.multiTenancy || {};
+    if (tenant /* && tenant !== _defaultTenant*/) {
+      // 非默认租户,加载租户model
+      if (!this[TENANT_MODELS]) {
+        this[TENANT_MODELS] = [];
+      }
+      const models = this[TENANT_MODELS];
+      if (!models[tenant]) {
+        // 加载租户Model
+        models[tenant] = loadModel2(this, tenant);
+      }
+      return models[tenant];
+    }
+    // 默认租户返回原始model对象
+    return this.model;
+  },
+};

+ 53 - 0
lib/plugin/egg-multi-tenancy/app/extend/context.js

@@ -0,0 +1,53 @@
+'use strict';
+
+const TENANT = Symbol('Context#tenant');
+// const TENANT_MODEL = Symbol('Context#model@tenant');
+const TENANT_USERID = Symbol('Context#userid@tenant');
+const TENANT_ROLE = Symbol('Context#role@tenant');
+const TENANT_TAGS = Symbol('Context#tags@tenant');
+
+// this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性
+module.exports = {
+  // 多租户系统中的当前租户信息(multi-tenancy)
+  get tenant() {
+    const { defaultTenant } = this.app.config.multiTenancy || {};
+    if (!this[TENANT]) {
+      // 从 header或请求参数中获取,否则使用默认值。实际情况可能更复杂,需要从登录用户中获取该信息
+      this[TENANT] = this.query._tenant || this.get('x-tenant') || defaultTenant;
+    }
+    return this[TENANT];
+  },
+
+  set tenant(value) {
+    this[TENANT] = value;
+  },
+
+  // 多租户系统中的当前租户信息(multi-tenancy)
+  get model() {
+    const { defaultTenant: _defaultTenant } = this.app.config.multiTenancy || {};
+    if (this.tenant /* && this.tenant !== defaultTenant */) {
+      return this.app.tenantModel(this.tenant);
+    }
+    return this.app.model;
+  },
+
+  // 当前用户相关信息
+  get userid() {
+    if (!this[TENANT_USERID]) {
+      this[TENANT_USERID] = this.get('x-userid');
+    }
+    return this[TENANT_USERID];
+  },
+  get role() {
+    if (!this[TENANT_ROLE]) {
+      this[TENANT_ROLE] = this.get('x-role');
+    }
+    return this[TENANT_ROLE];
+  },
+  get tags() {
+    if (!this[TENANT_TAGS]) {
+      this[TENANT_TAGS] = this.get('x-tags');
+    }
+    return this[TENANT_TAGS];
+  },
+};

+ 7 - 0
lib/plugin/egg-multi-tenancy/config/config.default.js

@@ -0,0 +1,7 @@
+'use strict';
+
+const DEFAULT_TENANT = 'master';
+
+exports.multiTenancy = {
+  defaultTenant: DEFAULT_TENANT,
+};

+ 6 - 0
lib/plugin/egg-multi-tenancy/package.json

@@ -0,0 +1,6 @@
+{
+  "eggPlugin": {
+    "name": "multiTenancy",
+    "dependencies": [ "mongoose" ]
+  }
+}

+ 163 - 0
lib/service/axios-service.js

@@ -0,0 +1,163 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { trimData, isNullOrUndefined } = require('naf-core').Util;
+const { NafService } = require('./naf-service');
+const axios = require('axios');
+
+
+/**
+ * meta 格式
+ * {
+ *  "baseUrl": "可选",
+ *  "uri": "接口地址",
+ *  "method": "GET or POST",如果为空根据接口data参数推断
+ * }
+ *
+ * 接口参数定义
+ * api(query, data)
+ * query - 查询参数对象
+ * data - POST data
+ */
+
+class AxiosService extends NafService {
+  constructor(ctx, meta, { baseUrl = '' }) {
+    super(ctx);
+    assert(_.isObject(meta));
+
+    this.baseUrl = baseUrl;
+
+    _.forEach(meta, (val, key) => {
+      const { method, uri = key, baseUrl: _baseUrl } = val;
+      this[key] = async (query = {}, data, options) => {
+        if (_.isUndefined(options) && _.toLower(method) === 'get') {
+          // TODO: get 请求可以没有data参数,直接是query,options
+          options = data;
+          data = undefined;
+        }
+        if (_.isUndefined(options) && _.isUndefined(data) && _.toLower(method) === 'post') {
+          // TODO: post 请求可以只有一个data
+          if (AxiosService.isOpts(query)) {
+            options = query;
+          } else {
+            data = query;
+          }
+          query = undefined;
+        }
+        options = AxiosService.mergeOpts(query, data, options);
+        options = _.merge(trimData({ method, baseURL: _baseUrl }), options);
+        return await this.request(uri, options);
+      };
+    });
+  }
+
+  static isOpts(data) {
+    // TODO: 判断是否Options对象
+    return _.isObject(data) &&
+      (_.isString(data.baseURL) || _.isObject(data.params) || _.isObject(data.data));
+  }
+  // 替换uri中的参数变量
+  static mergeOpts(query, data, options) {
+    // TODO: 合并query、data和options
+    if (query && _.isUndefined(data) && _.isUndefined(options)) { // 只有一个参数,作为options或者query
+      options = AxiosService.isOpts(query) ? query : { };
+    }
+    options = options || {};
+    options.params = trimData(_.merge(options.params, query));
+    if (data) {
+      options.data = trimData(data);
+    }
+    return options;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/ig;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach(key => {
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  httpGet(uri, query = {}, options) {
+    options = AxiosService.mergeOpts(query, null, options);
+    options = _.merge(options, { method: 'get' });
+    return this.request(uri, options);
+  }
+
+  httpPost(uri, query = {}, data, options) {
+    options = AxiosService.mergeOpts(query, data, options);
+    options = _.merge(options, { method: 'post' });
+    return this.request(uri, options);
+  }
+
+  async request(uri, query, data, options) {
+    // TODO: 合并query和options
+    options = AxiosService.mergeOpts(query, data, options);
+    // TODO: 处理租户信息
+    if (!options.params._tenant) {
+      options.params._tenant = this.ctx.tenant; // 租户信息
+    }
+    const url = AxiosService.merge(uri, options.params);
+    try {
+      let res = await axios({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        baseURL: this.baseUrl, // 可以被options中的baseURL覆盖
+        responseType: 'json',
+        ...options,
+      });
+      if (res.status !== 200) {
+        throw new BusinessError(ErrorCode.NETWORK, `Http Code: ${res.status}`, res.data);
+      }
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        throw new BusinessError(errcode, errmsg, details);
+      }
+      res = _.omit(res, [ 'errcode', 'errmsg', 'details' ]);
+      const keys = Object.keys(res);
+      if (keys.length === 1 && keys.includes('data')) {
+        res = res.data;
+      }
+      return res;
+    } catch (err) {
+      if (err instanceof BusinessError) {
+        throw err;
+      }
+
+      let errmsg = '接口请求失败';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) {
+          errmsg += ': 用户认证失败';
+        } else if (status === 403) {
+          errmsg += ': 当前用户不允许执行该操作';
+        } else if (status >= 300) {
+          errmsg += `: 网络错误:HttpCode ${status}`;
+        }
+      }
+      this.ctx.logger.error(`[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - ${err.message}`);
+      if (err.response && err.response.data) {
+        this.ctx.logger.debug('[AxiosService]', err.response.data);
+      }
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, errmsg, err.message);
+    }
+  }
+
+}
+
+module.exports.AxiosService = AxiosService;

+ 126 - 0
lib/service/crud-service.js

@@ -0,0 +1,126 @@
+"use strict";
+
+const { isString, isArray, get } = require("lodash");
+const { isNullOrUndefined, trimData } = require("naf-core").Util;
+const assert = require("assert");
+const { ObjectId } = require("mongoose").Types;
+const { BusinessError, ErrorCode } = require("naf-core").Error;
+const { NafService } = require("./naf-service");
+
+class CrudService extends NafService {
+  async create(data) {
+    assert(data);
+    // TODO:保存数据
+    const res = await this.model.create(data);
+    return res;
+  }
+
+  async update(filter, update, { projection } = {}) {
+    assert(filter);
+    assert(update);
+    const { _id, id } = filter;
+    if (_id || id) filter = { _id: ObjectId(_id || id) };
+    // TODO:检查数据是否存在
+    const entity = await this.model.findOne(filter).exec();
+    if (isNullOrUndefined(entity))
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST);
+
+    // TODO: 修改数据
+    entity.set(trimData(update));
+    await entity.save();
+    return await this.model.findOne(filter, projection).exec();
+  }
+
+  async delete(filter) {
+    assert(filter);
+    const { _id, id } = filter;
+    if (_id || id) {
+      await this.model.findByIdAndDelete(_id || id).exec();
+    } else {
+      await this.model.deleteMany(filter).exec();
+    }
+    return "deleted";
+  }
+
+  async fetch(filter, { sort, desc, projection } = {}) {
+    assert(filter);
+    const { _id, id } = filter;
+    if (_id || id) filter = { _id: ObjectId(_id || id) };
+
+    // 处理排序
+    if (sort && isString(sort)) {
+      sort = { [sort]: desc ? -1 : 1 };
+    } else if (sort && isArray(sort)) {
+      sort = sort
+        .map((f) => ({ [f]: desc ? -1 : 1 }))
+        .reduce((p, c) => ({ ...p, ...c }), {});
+    }
+
+    return await this.model.findOne(filter, projection).exec();
+  }
+
+  async query(filter, { skip, limit, sort, desc, projection } = {}) {
+    // 处理排序
+    if (sort && isString(sort)) {
+      sort = { [sort]: desc ? -1 : 1 };
+    } else if (sort && isArray(sort)) {
+      sort = sort
+        .map((f) => ({ [f]: desc ? -1 : 1 }))
+        .reduce((p, c) => ({ ...p, ...c }), {});
+    }
+    filter = this.turnFilter(this.turnDateRangeQuery(filter));
+    const rs = await this.model
+      .find(trimData(filter), projection, { skip, limit, sort })
+      .exec();
+    return rs;
+  }
+
+  async count(filter) {
+    filter = this.turnFilter(this.turnDateRangeQuery(filter));
+    const res = await this.model.countDocuments(trimData(filter)).exec();
+    return res;
+  }
+
+  async queryAndCount(filter, options) {
+    filter = this.turnFilter(this.turnDateRangeQuery(filter));
+    const total = await this.count(filter);
+    if (total === 0) return { total, data: [] };
+    const rs = await this.query(filter, options);
+    return { total, data: rs };
+  }
+
+  turnFilter(filter) {
+    let str = /^%\S*%$/;
+    let keys = Object.keys(filter);
+    for (const key of keys) {
+      let res = key.match(str);
+      if (res) {
+        let newKey = key.slice(1, key.length - 1);
+        filter[newKey] = new RegExp(filter[key]);
+        delete filter[key];
+      }
+    }
+    return 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.CrudService = CrudService;

+ 92 - 0
lib/service/index.d.ts

@@ -0,0 +1,92 @@
+import * as EggApplication from 'egg';
+import * as mongoose from 'mongoose';
+
+declare module 'egg' {
+
+  type MongooseModels = {
+    [key: string]: mongoose.Model<any>
+  };
+
+  type MongooseSingleton = {
+    clients: Map<string, mongoose.Connection>,
+    get (id: string) : mongoose.Connection
+  };
+
+  type MongooseConfig = {
+    url: string,
+    options?: mongoose.ConnectionOptions
+  };
+
+  // extend app
+  interface Application {
+    mongooseDB: mongoose.Connection | MongooseSingleton;
+    mongoose: typeof mongoose;
+    model: MongooseModels;
+    tenantModel(tenant: string): MongooseModels;
+  }
+
+  // extend context
+  interface Context {
+    model: MongooseModels;
+
+    /**
+     * compose from ctx.query and ctx.request.body
+     */
+    requestparam: any;
+
+    /**
+     * 租户ID,用于多租户系统
+     */
+    tenant: string;
+    /**
+     * 当前用户ID
+     */
+    userid: string;
+    /**
+     * 当前用户角色
+     */
+    role: string;
+    /**
+     * 当前用户标签
+     */
+    tags: string;
+
+    // 返回JSON结果
+    json(errcode: number, errmsg: string, data: object);
+    success(message: string, data: object);
+    fail(errcode: number, errmsg: string, details: any);
+    /**
+     * same to success(message: string, data: object);
+     */
+    ok(message: string, data: object);
+  }
+
+  // extend your config
+  interface EggAppConfig {
+    mongoose: {
+      url?: string,
+      options?: mongoose.ConnectionOptions,
+      client?: MongooseConfig,
+      clients?: {
+        [key: string]: MongooseConfig
+      }
+    };
+  }
+
+}
+
+import { NafService }  from './naf-service';
+export { NafService } from './naf-service';
+
+  /**
+ * CrudService is extending from {@link NafService} ,
+ */
+export class CrudService extends NafService { }
+
+export class AxiosService extends EggApplication.Service { 
+  constructor(ctx: EggApplication.Context, meta: object, options: object = {});
+
+  async httpGet(uri: string, query: object, options);
+  async httpPost(uri: string, query: object = {}, data: object, options);
+  async request(uri: string, query: object, data: object, options);
+}

+ 7 - 0
lib/service/index.js

@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = {
+  ...require('./naf-service'),
+  ...require('./crud-service'),
+  ...require('./axios-service'),
+};

+ 48 - 0
lib/service/naf-service.d.ts

@@ -0,0 +1,48 @@
+import * as Egg from 'egg';
+import * as mongoose from 'mongoose';
+
+  /**
+   * NafService is a base service class that can be extended,
+   * it's extending from {@link Service},
+   */
+  export class NafService extends Egg.Service {
+    /**
+     * 构造函数
+     * @param ctx context对象 
+     * @param name service名称
+     */
+    // constructor(ctx: Context, name: String);
+
+    /**
+     * 租户ID,用于多租户系统
+     */
+    tenant: string;
+    /**
+     * 当前用户ID
+     */
+    userid: string;
+    /**
+     * 当前用户角色
+     */
+    role: string;
+    /**
+     * 当前用户标签
+     */
+    tags: string;
+
+    /**
+     * 服务名,用于默认序列名,可选
+     */
+    name: string;
+
+    /**
+     * 服务默认Model对象
+     */
+    model: mongoose.Model<any>;
+
+    /** 
+     * 生成Id,sequence名用service的name
+     */
+    nextId(seqName: string = null): Number;
+
+  }

+ 25 - 0
lib/service/naf-service.js

@@ -0,0 +1,25 @@
+'use strict';
+
+const assert = require('assert');
+const Service = require('egg').Service;
+
+class NafService extends Service {
+  constructor(ctx, name) {
+    super(ctx);
+    this.name = name;
+  }
+  get tenant() {
+    return this.ctx.tenant;
+  }
+  set tenant(value) {
+    this.ctx.tenant = value;
+  }
+  async nextId(seqName) {
+    assert(this.name);
+    const { seq } = this.ctx.service;
+    const value = await seq.nextVal(seqName || this.name);
+    return value;
+  }
+}
+
+module.exports.NafService = NafService;

+ 65 - 0
package.json

@@ -0,0 +1,65 @@
+{
+  "name": "naf-framework-mongoose-free",
+  "version": "0.0.1",
+  "description": "naf egg framework use mongoose - free version",
+  "dependencies": {
+    "axios": "^0.19.0",
+    "egg": "^2.23.0",
+    "egg-mongoose": "^3.2.0",
+    "egg-scripts": "^2.11.0",
+    "egg-validate": "^2.0.2",
+    "is-type-of": "^1.2.1",
+    "lodash": "^4.17.15",
+    "mongoose": "^5.7.3",
+    "naf-core": "^0.1.2",
+    "saslprep": "^1.0.3"
+  },
+  "devDependencies": {
+    "@types/lodash": "^4.14.142",
+    "autod": "^3.1.0",
+    "autod-egg": "^1.1.0",
+    "egg-bin": "^4.13.2",
+    "egg-ci": "^1.13.0",
+    "egg-mock": "^3.24.1",
+    "eslint": "^6.5.1",
+    "eslint-config-egg": "^7.5.1",
+    "eslint-config-naf": "^0.0.6",
+    "webstorm-disable-index": "^1.2.0"
+  },
+  "engines": {
+    "node": ">=8.9.0"
+  },
+  "scripts": {
+    "test": "npm run lint -- --fix && egg-bin pkgfiles && npm run test-local",
+    "test-local": "egg-bin test",
+    "cov": "egg-bin cov",
+    "lint": "eslint .",
+    "ci": "npm run lint && egg-bin pkgfiles --check && npm run cov",
+    "autod": "autod",
+    "pkgfiles": "egg-bin pkgfiles"
+  },
+  "ci": {
+    "version": "8, 9"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "keywords": [
+    "egg",
+    "egg-framework"
+  ],
+  "author": "dyg",
+  "files": [
+    "app",
+    "config",
+    "lib",
+    "index.js",
+    "index.d.ts",
+    "app.js"
+  ],
+  "license": "MIT",
+  "publishConfig": {
+    "registry": "https://registry.npmjs.org/"
+  }
+}

+ 12 - 0
test/fixtures/example/app/controller/home.js

@@ -0,0 +1,12 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+  async index() {
+    const data = await this.service.test.get(123);
+    this.ctx.body = data.name;
+  }
+}
+
+module.exports = HomeController;

+ 7 - 0
test/fixtures/example/app/router.js

@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = app => {
+  const { router, controller } = app;
+
+  router.get('/', controller.home.index);
+};

+ 3 - 0
test/fixtures/example/config/config.unittest.js

@@ -0,0 +1,3 @@
+'use strict';
+
+exports.keys = '123456';

+ 4 - 0
test/fixtures/example/package.json

@@ -0,0 +1,4 @@
+{
+  "name": "framework-example",
+  "version": "1.0.0"
+}

+ 26 - 0
test/framework.test.js

@@ -0,0 +1,26 @@
+'use strict';
+
+const mock = require('egg-mock');
+
+describe('test/framework.test.js', () => {
+  let app;
+  before(() => {
+    app = mock.app({
+      baseDir: 'example',
+      framework: true,
+    });
+    return app.ready();
+  });
+
+  after(() => app.close());
+
+  afterEach(mock.restore);
+
+  it('should GET /', () => {
+    return app.httpRequest()
+      .get('/')
+      .expect('framework-example_123456')
+      .expect(200);
+  });
+});
+

+ 18 - 0
test/uri.js

@@ -0,0 +1,18 @@
+'use strict';
+
+const URI = require('urijs');
+const qs = require('qs');
+
+const uri = '/weixin/api/fetch?id=123&corp=345';
+const parsed = URI.parse(uri);
+console.log(parsed);
+console.log(URI.parseQuery(parsed.query));
+console.log(URI.parseQuery(''));
+
+console.log(qs.parse(uri));
+const query = console.log(qs.stringify({}));
+if (query) {
+  console.log('true');
+} else {
+  console.log(false);
+}