asd123a20 3 years ago
parent
commit
b127ae69fb
69 changed files with 2929 additions and 0 deletions
  1. 30 0
      .autod.conf.js
  2. 1 0
      .eslintignore
  3. 11 0
      .eslintrc
  4. 12 0
      .gitignore
  5. 12 0
      .travis.yml
  6. 39 0
      README.zh-CN.md
  7. 14 0
      app/controller/admin.js
  8. 23 0
      app/controller/admin.json
  9. 14 0
      app/controller/content.js
  10. 23 0
      app/controller/content.json
  11. 14 0
      app/controller/hospital.js
  12. 23 0
      app/controller/hospital.json
  13. 23 0
      app/controller/log.js
  14. 26 0
      app/controller/login.js
  15. 19 0
      app/controller/order.js
  16. 23 0
      app/controller/order.json
  17. 14 0
      app/controller/pages.js
  18. 23 0
      app/controller/pages.json
  19. 14 0
      app/controller/specialist.js
  20. 23 0
      app/controller/specialist.json
  21. 14 0
      app/controller/subject.js
  22. 23 0
      app/controller/subject.json
  23. 14 0
      app/controller/user.js
  24. 23 0
      app/controller/user.json
  25. 156 0
      app/controller/weixin.js
  26. 27 0
      app/middleware/error_handler.js
  27. 12 0
      app/middleware/routerMethod.js
  28. 15 0
      app/middleware/routerMondel.js
  29. 34 0
      app/model/Log.js
  30. 13 0
      app/model/admin.js
  31. 15 0
      app/model/content.js
  32. 17 0
      app/model/hospital.js
  33. 25 0
      app/model/order.js
  34. 13 0
      app/model/pages.js
  35. 18 0
      app/model/specialist.js
  36. 15 0
      app/model/subject.js
  37. 14 0
      app/model/user.js
  38. 20 0
      app/public/weui/weui-prompt.css
  39. 136 0
      app/public/weui/weui-prompt.js
  40. 241 0
      app/public/weui/weui-util.js
  41. 99 0
      app/router.js
  42. 13 0
      app/service/admin.js
  43. 13 0
      app/service/content.js
  44. 13 0
      app/service/hospital.js
  45. 80 0
      app/service/log.js
  46. 111 0
      app/service/login.js
  47. 37 0
      app/service/order.js
  48. 13 0
      app/service/pages.js
  49. 13 0
      app/service/specialist.js
  50. 13 0
      app/service/subject.js
  51. 13 0
      app/service/user.js
  52. 224 0
      app/service/weixin.js
  53. 360 0
      app/service/wxpayv3.js
  54. 7 0
      app/util/constant.js
  55. 107 0
      app/view/pay.njk
  56. 29 0
      app/view/redirect.njk
  57. 86 0
      app/view/weixinbind.njk
  58. 103 0
      app/view/weixinlogin.njk
  59. 14 0
      appveyor.yml
  60. 112 0
      config/config.default.js
  61. 31 0
      config/config.error.js
  62. 38 0
      config/config.local.js
  63. 18 0
      config/config.prod.js
  64. 22 0
      config/plugin.js
  65. 17 0
      ecosystem.config.js
  66. 55 0
      package.json
  67. 9 0
      server.js
  68. 21 0
      test/app/controller/home.test.js
  69. 32 0
      test/http/dept.http

+ 30 - 0
.autod.conf.js

@@ -0,0 +1,30 @@
+'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',
+    'webstorm-disable-index',
+  ],
+  exclude: [
+    './test/fixtures',
+    './dist',
+  ],
+};
+

+ 1 - 0
.eslintignore

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

+ 11 - 0
.eslintrc

@@ -0,0 +1,11 @@
+{
+  "extends": "eslint-config-egg",
+  "parserOptions": {
+    "ecmaFeatures": {
+        "experimentalObjectRestSpread": true
+    }
+  },
+  "rules": {
+    "quotes": ["warn", "single"]
+  }
+}

+ 12 - 0
.gitignore

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

+ 12 - 0
.travis.yml

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

+ 39 - 0
README.zh-CN.md

@@ -0,0 +1,39 @@
+# gaf
+
+
+
+## 快速入门
+
+<!-- 在此次添加使用文档 -->
+
+如需进一步了解,参见 [egg 文档][egg]。
+
+### 本地开发
+
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### 部署
+
+```bash
+$ npm start
+$ npm stop
+```
+
+### 单元测试
+
+- [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。
+- 断言库非常推荐使用 [power-assert]。
+- 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。
+
+### 内置指令
+
+- 使用 `npm run lint` 来做代码风格检查。
+- 使用 `npm test` 来执行单元测试。
+- 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。
+
+
+[egg]: https://eggjs.org

+ 14 - 0
app/controller/admin.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./admin.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.admin;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/admin.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["userName", "openid", "password", "status"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "userName", "status", "password", "openid"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["userName", "openid"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 14 - 0
app/controller/content.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./content.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.content;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/content.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["title", "content", "column", "thumbnail", "slug"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "title", "content", "column", "thumbnail", "slug"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["column"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 14 - 0
app/controller/hospital.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./hospital.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.hospital;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/hospital.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["name", "code", "level", "content", "address", "thumbnail", "region"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "name", "code", "level", "content", "address", "thumbnail", "region"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["region", "name"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 23 - 0
app/controller/log.js

@@ -0,0 +1,23 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class LogController extends Controller {
+  async create() {
+    const { ctx } = this;
+    const res = await ctx.service.log.create(ctx.request.body);
+    ctx.body = res;
+  }
+  async delete() {
+    const { ctx } = this;
+    const res = await ctx.service.log.del(ctx.params);
+    ctx.body = res;
+  }
+  async query() {
+    const { ctx } = this;
+    const res = await ctx.service.log.query(ctx.query);
+    ctx.body = res;
+  }
+}
+
+module.exports = LogController;

+ 26 - 0
app/controller/login.js

@@ -0,0 +1,26 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class LoginController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.login;
+  }
+
+  async adminlogin() {
+    const { ctx } = this;
+    const { userName, password } = ctx.request.body;
+    const res = await this.service.adminlogin({ userName, password });
+    this.ctx.ok(res);
+  }
+  // POST 修改用户密码
+  async editPwa() {
+    const { ctx } = this;
+    await this.service.editPwa(ctx.request.body);
+    this.ctx.ok('updated');
+  }
+
+}
+
+module.exports = LoginController;

+ 19 - 0
app/controller/order.js

@@ -0,0 +1,19 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./order.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.order;
+  }
+  async updatestatus() {
+    const { out_trade_no } = this.ctx.query;
+    const res = await this.service.updatestatus({ out_trade_no });
+    this.ctx.ok(res);
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/order.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["name", "phone", "openid","time","code","hospitalId","hospitalName","subjectId","subjectName","specialistId","specialistName","money","status","remark"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "name", "phone", "openid","time","code","hospitalId","hospitalName","subjectId","subjectName","specialistId","specialistName","money","status","remark"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["name", "status", "openid"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 14 - 0
app/controller/pages.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./pages.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.pages;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/pages.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["title", "content", "code"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "title", "content", "code"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["code"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["code"]
+    }
+  }

+ 14 - 0
app/controller/specialist.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./specialist.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.specialist;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/specialist.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["name", "hospitalId","content", "code", "subjectId", "thumbnail", "subjectName", "ability", "money"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "name", "hospitalId", "content", "code", "subjectId", "thumbnail", "subjectName", "ability", "money"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["subjectId", "name"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 14 - 0
app/controller/subject.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./subject.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.subject;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/subject.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["name", "hospitalId","content", "code", "thumbnail"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "name", "hospitalId", "content", "code", "thumbnail"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["hospitalId", "name"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "params": ["_id"]
+    }
+  }

+ 14 - 0
app/controller/user.js

@@ -0,0 +1,14 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const meta = require('./user.json');
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class ItemsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.user;
+  }
+}
+
+module.exports = CrudController(ItemsController, meta);

+ 23 - 0
app/controller/user.json

@@ -0,0 +1,23 @@
+{
+    "create": {
+      "requestBody": ["name", "openid", "phone", "status"]
+    },
+    "delete": {
+      "params": ["_id"]
+    },
+    "update": {
+      "requestBody": ["_id", "name", "phone", "openid", "status"]
+    },
+    "query": {
+      "parameters": {
+        "query": ["openid"]
+      },
+      "options": {
+        "count": true,
+        "query": ["skip", "limit"]
+      }
+    },
+    "fetch": {
+      "query": ["_id", "openid"]
+    }
+  }

+ 156 - 0
app/controller/weixin.js

@@ -0,0 +1,156 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+const uuid = require('uuid');
+class LoginController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.login;
+    this.model = this.ctx.model.Admin;
+  }
+
+  // GET 通过微信号获得用户信息
+  async fetch() {
+    const { openid } = this.ctx.requestparam;
+    const res = await this.service.fetchByWeixin(openid);
+    this.ctx.ok({ userinfo: res });
+  }
+  // 获取openid
+  async auth() {
+    try {
+      const { redirect_uri, code } = this.ctx.query;
+      if (redirect_uri) {
+        await this.app.redis.set('redirect_uri', redirect_uri, 'EX', 600);
+      }
+      if (code) {
+        return await this.authBack({ code });
+      }
+      // TODO: 生成回调地址
+      const { wxapi, authUrl = this.ctx.path } = this.app.config;
+      // windos环境
+      // const host = this.ctx.header.referer.split('/')[2];
+      // linux环境
+      const host = this.ctx.header.host;
+      const backUrl = encodeURI(`${this.ctx.protocol}://${host}${authUrl}`);
+      const to_uri = `${wxapi.baseUrl}/api/auth?appid=${wxapi.appid}&response_type=code&redirect_uri=${backUrl}#wechat`;
+      this.ctx.redirect(to_uri);
+    } catch (error) {
+      console.log(error);
+    }
+  }
+  // GET 获取openid认证回调
+  async authBack({ code }) {
+    const { weixin } = this.ctx.service;
+    const val = await this.app.redis.get('redirect_uri');
+    const res = await weixin.fetch(code);
+    const openid = res.openid;
+    // TODO: 重定性到跳转页面
+    await this.ctx.render('redirect.njk', { openid, redirect_uri: val });
+  }
+
+  // POST 绑定用户微信号
+  async bind() {
+    try {
+      const { userName, code } = this.ctx.query;
+      if (userName) {
+        const userinfo = await this.model.findOne({ userName });
+        await this.app.redis.set('key', userinfo._id, 'EX', 600);
+      }
+      if (code) {
+        return await this.Back({ code });
+      }
+
+      // TODO: 生成回调地址
+      const { wxapi, authUrl = this.ctx.path } = this.app.config;
+      // windos环境
+      // const host = this.ctx.header.referer.split('/')[2];
+      // linux环境
+      const host = this.ctx.header.host;
+      const backUrl = encodeURI(`${this.ctx.protocol}://${host}${authUrl}`);
+      const to_uri = `${wxapi.baseUrl}/api/auth?appid=${wxapi.appid}&response_type=code&redirect_uri=${backUrl}#wechat`;
+
+      this.ctx.redirect(to_uri);
+    } catch (error) {
+      console.log(error);
+    }
+  }
+  // GET 绑定认证回调
+  async Back({ code }) {
+    const { weixin } = this.ctx.service;
+    const val = await this.app.redis.get('key');
+    const res = await weixin.fetch(code);
+    const openid = res.openid;
+    // TODO: 重定性到跳转页面
+    await this.ctx.render('weixinbind.njk', { openid, id: val });
+  }
+  // mp
+  async mqtt() {
+    const key = this.ctx.params.key;
+    // TODO: 发布扫码成功消息
+    const { mq } = this.ctx;
+    if (mq) {
+      await mq.topic('qrcode.topic', key, 'success', { durable: true });
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+    this.ctx.ok();
+  }
+  // POST 检查二维码
+  async check() {
+    const uuid = this.ctx.query;
+    const res = await this.service.checkQrcode(uuid);
+    this.ctx.ok(res);
+  }
+
+  // 获取uuid创建二维码
+  async getuuid() {
+    const out_trade_no = uuid.v1();
+    await this.app.redis.set(out_trade_no, 'ready', 'EX', 600);
+    this.ctx.ok({ uuid: out_trade_no });
+  }
+
+  // 二维码登录
+  async qrcodelogin() {
+    const { ctx } = this;
+    const { uuid } = ctx.params;
+    await this.service.qrcodelogin({ uuid });
+    // this.ctx.ok(res);
+  }
+  // get uuid换取token
+  async qrcodeToken() {
+    const { ctx } = this;
+    const { uuid } = ctx.query;
+    const res = await this.service.qrcodeToken(uuid);
+    this.ctx.ok({ data: JSON.parse(res) });
+  }
+
+  // 获取微信token
+  async gettoken() {
+    const { weixin } = this.ctx.service;
+    const res = await weixin.gettoken();
+    this.ctx.ok({ data: res });
+  }
+  // 预支付订单
+  async orderPay() {
+    const { openid, money, out_trade_no, hospitalName, subjectName, specialistName, _id } = this.ctx.request.body;
+    await this.ctx.service.weixin.orderPay({ openid, money, out_trade_no, description: `${hospitalName}-${subjectName}-${specialistName}`, _id });
+    this.ctx.ok();
+  }
+  // 支付接口
+  async pay() {
+    await this.ctx.service.weixin.pay();
+  }
+  // 关闭订单
+  async orderClose() {
+    const { out_trade_no } = this.ctx.query;
+    const res = await this.ctx.service.weixin.orderClose({ out_trade_no });
+    this.ctx.ok(res);
+  }
+  // 消息模板下发
+  async pushMould() {
+    const { out_trade_no, openid } = this.ctx.query;
+    await this.ctx.service.weixin.pushMould({ out_trade_no, openid });
+  }
+}
+
+module.exports = LoginController;

+ 27 - 0
app/middleware/error_handler.js

@@ -0,0 +1,27 @@
+'use strict';
+module.exports = () => {
+  return async function errorHandler(ctx, next) {
+    const { method } = ctx.request;
+    try {
+      await next();
+      const jsons = await ctx.service.log.init();
+      if (method !== 'GET' && jsons) {
+        jsons.result = '成功';
+        await ctx.service.log.create(jsons);
+      }
+    } catch (err) {
+      const jsons = await ctx.service.log.init();
+      if (method !== 'GET' && jsons) {
+        jsons.result = '失败';
+        await ctx.service.log.create(jsons);
+      }
+      const { message } = err;
+      const json = {
+        errcode: -1001,
+        errmsg: message,
+      };
+      ctx.body = json;
+      ctx.status = 400;
+    }
+  };
+};

+ 12 - 0
app/middleware/routerMethod.js

@@ -0,0 +1,12 @@
+/* eslint-disable no-dupe-keys */
+/* eslint-disable quote-props */
+'use strict';
+
+/** @type Egg.EggPlugin */
+module.exports = {
+  'create': '添加',
+  'update': '修改',
+  'delete': '删除',
+  'upload': '上传文件',
+  'login': '系统登录',
+};

+ 15 - 0
app/middleware/routerMondel.js

@@ -0,0 +1,15 @@
+/* eslint-disable quote-props */
+'use strict';
+
+/** @type Egg.EggPlugin */
+module.exports = {
+  'admin': '系统用户',
+  // 'user': '用户管理',
+  'pages': '单页管理',
+  'hospital': '医院管理',
+  'content': '内容管理',
+  'subject': '科室管理',
+  'specialist': '专家管理',
+  'order': '订单管理',
+  'power': '系统鉴权',
+};

+ 34 - 0
app/model/Log.js

@@ -0,0 +1,34 @@
+'use strict';
+module.exports = app => {
+  const { mongoose } = app;
+  const { Schema } = mongoose;
+  const LogSchema = new Schema({
+    // 模块
+    mondel: {
+      type: String,
+    },
+    // 类型
+    method: {
+      type: String,
+    },
+    // 结果
+    result: {
+      type: String,
+    },
+    // 数据
+    data: {
+      type: Object,
+    },
+    // 创建时间
+    createAt: {
+      type: String,
+    },
+    userName: {
+      type: String,
+    },
+    date: {
+      type: String,
+    },
+  });
+  return mongoose.model('Log', LogSchema);
+};

+ 13 - 0
app/model/admin.js

@@ -0,0 +1,13 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const SchemaDefine = {
+  openid: { type: String, required: false },
+  userName: { type: String, required: true },
+  password: { type: String, required: true },
+  status: { type: String, required: true },
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Admin', schema, 'admin');
+};

+ 15 - 0
app/model/content.js

@@ -0,0 +1,15 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  thumbnail: { type: String, required: true }, // 缩略图
+  title: { type: String, required: true }, // 标题
+  content: { type: String, required: true }, // 内容
+  column: { type: String, required: true }, // 绑定的栏目
+  slug: { type: String, required: true }, // 摘要
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Content', schema, 'content');
+};

+ 17 - 0
app/model/hospital.js

@@ -0,0 +1,17 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  thumbnail: { type: String, required: true }, // 缩略图
+  name: { type: String, required: true }, // 名称
+  code: { type: String, required: true }, // 编码
+  level: { type: String, required: true }, // 医院级别
+  content: { type: String, required: true }, // 内容
+  address: { type: String, required: true }, // 地址
+  region: { type: String, required: true }, // 区域编码
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Hospital', schema, 'hospital');
+};

+ 25 - 0
app/model/order.js

@@ -0,0 +1,25 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  name: { type: String, required: true }, // 姓名
+  phone: { type: String, required: true }, // 手机号
+  time: { type: String, required: true }, // 订单时间
+  openid: { type: String, required: true }, // openid
+  code: { type: String, required: true }, // 行政区编码
+  hospitalId: { type: String, required: true }, // 医院id
+  hospitalName: { type: String, required: true }, // 医院名称
+  subjectId: { type: String, required: true }, // 科室id
+  subjectName: { type: String, required: true }, // 科室名称
+  specialistId: { type: String, required: true }, // 专家id
+  specialistName: { type: String, required: true }, // 专家名称
+  money: { type: String, required: true }, // 金额
+  status: { type: String, required: false, default: '0' }, // 付款状态
+  remark: { type: String, required: false }, // 备注
+  out_trade_no: { type: String, required: true }, // 订单号
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Order', schema, 'order');
+};

+ 13 - 0
app/model/pages.js

@@ -0,0 +1,13 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  title: { type: String, required: true },
+  content: { type: String, required: true },
+  code: { type: String, required: true },
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Pages', schema, 'pages');
+};

+ 18 - 0
app/model/specialist.js

@@ -0,0 +1,18 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const SchemaDefine = {
+  thumbnail: { type: String, required: true },
+  code: { type: String, required: true },
+  hospitalId: { type: String, required: true },
+  subjectId: { type: String, required: true },
+  subjectName: { type: String, required: true },
+  name: { type: String, required: true },
+  ability: { type: String, required: true }, // 能力
+  content: { type: String, required: true },
+  money: { type: String, required: true },
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Specialist', schema, 'specialist');
+};

+ 15 - 0
app/model/subject.js

@@ -0,0 +1,15 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  thumbnail: { type: String, required: true },
+  name: { type: String, required: true },
+  hospitalId: { type: String, required: true },
+  content: { type: String, required: true },
+  code: { type: String, required: true },
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Subject', schema, 'subject');
+};

+ 14 - 0
app/model/user.js

@@ -0,0 +1,14 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+
+const SchemaDefine = {
+  openid: { type: String, required: true },
+  name: { type: String, required: true },
+  phone: { type: String, required: true },
+  status: { type: String, required: false, default: 0 },
+};
+const schema = new Schema(SchemaDefine);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('User', schema, 'user');
+};

+ 20 - 0
app/public/weui/weui-prompt.css

@@ -0,0 +1,20 @@
+.weui-prompt-box {
+    margin-top: 10px;
+}
+.weui-prompt-input {
+    padding: 4px 6px;
+    border: 1px solid #ccc;
+    box-sizing: border-box;
+    height: 2em;
+    width: 85%;
+}
+.weui-prompt-input.hasbtn {
+    width: 55% !important;
+}
+.weui-prompt-button  {
+	margin-left: 2%;
+	width: 28% !important;
+	padding: 0;
+	white-space: nowrap;
+	vertical-align: middle;
+}

+ 136 - 0
app/public/weui/weui-prompt.js

@@ -0,0 +1,136 @@
+// 输入验证码
+// options = {
+//   text: String,
+//   title: String,
+//   inputPhone: String 或  true
+//   placeholder: String 或者 {phone: String, verifycode: String}
+//   okText: String,
+//   cancelText: String,
+//   btnText: String,
+//   delay: Number,
+//   onOK: Function(text),
+//   onCancel: Function,
+//   onSend: Function,
+//   verifyImg: String //图片验证码url
+// }
+function promptVC(options) {
+  let countdown = options.delay || 60;
+  var settime = function(obj) {
+    if (countdown == 0) {
+      $(obj).removeAttr('disabled');
+      $(obj).removeClass('weui-btn_disabled');
+      $(obj).text('获取');
+      countdown = options.delay || 60;
+      return;
+    }
+    console.log('剩余(' + countdown + '秒)');
+    $(obj).attr('disabled', 'disabled');
+    $(obj).addClass('weui-btn_disabled');
+    $(obj).text('(' + countdown + '秒)');
+    countdown--;
+    setTimeout(function() {
+      settime(obj);
+    }, 1000);
+
+  };
+
+  if (options.placeholder && options.placeholder instanceof String) {
+    options.placeholder = {
+      verifycode: options.placeholder,
+    };
+  }
+
+  let content = '<p>' + options.text + '</p>';
+  if (options.inputPhone === true) {
+    content += '<div class="weui-prompt-box"><input placeholder="' + ((options.placeholder && options.placeholder.phone) || '请输入手机号') + '" class="weui-prompt-input weui-input" id="weui-prompt-phone"/></div>';
+  } else if (options.inputPhone && typeof options.inputPhone === 'string') {
+    content += '<div class="weui-prompt-box"><input value="' + options.inputPhone + '" class="weui-prompt-input weui-input" id="weui-prompt-phone" readonly="readonly"/></div>';
+  }
+  content += '<div class="weui-prompt-box"><input placeholder="' + ((options.placeholder && options.placeholder.verifycode) || '请输入验证码') + '" class="weui-prompt-input weui-input hasbtn" id="weui-prompt-verifycode"/>' +
+		'<button class="weui-prompt-button weui-btn weui-btn_mini weui-btn_default" id="weui-prompt-button">' + (options.btnText || '获取') + '</button></div>';
+  if (options.verifyImg) {
+    content += '<div class="weui-prompt-box"><input placeholder="' + ((options.placeholder && options.placeholder.verifyimg) || '图片验证码') + '" class="weui-prompt-input weui-input hasbtn" id="weui-prompt-verifyimg"/>' +
+			'<img class="weui-prompt-button weui-btn weui-btn_mini weui-btn_default" src="' + options.verifyImg + '"></img></div>';
+  }
+
+  const checkInput = function(input, message, regexp) {
+    if (input.val() === '' || input.val() === null) {
+      input.focus()[0].select();
+      weui.topTips(message || '请填写正确的字段');
+      return false;
+    }
+    if (regexp && regexp instanceof RegExp && !regexp.test(input.val())) {
+      input.focus()[0].select();
+      weui.topTips(message || '请填写正确的字段');
+      return false;
+    }
+    return true;
+  };
+
+  const dlgOpts = {
+    title: options.title,
+    content,
+    buttons: [{
+      label: options.cancelText || '取消',
+      type: 'default',
+      onClick() {
+        if (options.onCancel && (options.onCancel.call(dlg) == false)) { return false; }
+
+        countdown = 0;
+      },
+    }, {
+      label: options.okText || '确定',
+      type: 'primary',
+      onClick() {
+        const val = {};
+        let input;
+        // TODO: 检查手机号
+        // 134,135,136,137,138,139,147,150,151,152,157,158,159,178,182,183,184,187,188
+        const regex = /^(13[4-9]|147|15[0-27-9]|178|198|18[23478])\d{8}$/; /* /^1[3-8]\d{9}$/*/
+        if (options.inputPhone == true) {
+          if (!checkInput(input = $(dlg).find('#weui-prompt-phone'), '请输入有效的手机号', regex)) { return false; }
+          val.phone = input.val();
+        } else if (typeof options.inputPhone === 'string' && regex.test(options.inputPhone)) {
+          val.phone = options.inputPhone;
+        }
+        // TODO: 检查验证码
+        if (!checkInput(input = $(dlg).find('#weui-prompt-verifycode'), '请输入有效的短信验证码', /^\d{6}$/)) {
+          return false;
+        } val.verifyCode = input.val();
+
+        // TODO: 检查图片验证码
+        if (options.verifyImg) {
+          if (!checkInput(input = $(dlg).find('#weui-prompt-verifyimg'), '请输入有效的图片验证码', /^[A-z0-9]{4}$/)) { return false; }
+          val.verifyImg = input.val();
+        }
+        if (options.onOK && (options.onOK.call(dlg, val) == false)) { return false; }
+        countdown = 0;
+      },
+    }],
+  };
+
+  if (options.cancelText == 'disabled') {
+    dlgOpts.buttons.shift();
+  }
+
+  if (options.isAndroid != undefined) { dlgOpts.isAndroid = options.isAndroid; }
+  var dlg = weui.dialog(dlgOpts);
+
+  const btn = $(dlg).find('#weui-prompt-button');
+  btn.click(function() {
+    let phone,
+      input;
+    // TODO: 检查手机号
+    const regex = /^(13[4-9]|147|15[0-27-9]|178|198|18[23478])\d{8}$/; /* /^1[3-8]\d{9}$/*/
+    if (options.inputPhone == true) {
+      if (!checkInput(input = $(dlg).find('#weui-prompt-phone'), '请输入有效的手机号', regex)) {
+        return false;
+      } phone = input.val();
+    } else if (typeof options.inputPhone === 'string' && regex.test(options.inputPhone)) {
+      phone = options.inputPhone;
+    }
+
+    if (options.onSend && (options.onSend.call(dlg, phone) != false)) { settime(this); }
+  });
+
+}

+ 241 - 0
app/public/weui/weui-util.js

@@ -0,0 +1,241 @@
+let loading = null;
+
+function showToast(message, delay) {
+  weui.toast(message, delay || 3000);
+}
+function showLoading(message, delay) {
+  if (loading) return;
+
+  loading = weui.loading(message, {
+		    className: 'custom-classname',
+  });
+  if (delay) {
+    setTimeout(function() {
+			    hideLoading();
+    }, delay || 3000);
+  }
+}
+function hideLoading() {
+	    if (loading) loading.hide();
+	    loading = null;
+}
+function showAlert(message, title, onClick, retry) {
+  if ($('.weui-dialog').length > 0) {
+    retry = retry || 0;
+    if (retry && retry > 30) {
+      weui.topTips('不能同时显示多个对话框');
+      return;
+    }
+    setTimeout(function() {
+      showAlert(message, title, onClick, ++retry);
+    }, 100);
+  } else {
+    weui.alert(message, onClick, { title, isAndroid: false });
+  }
+}
+function showConfirm(message, title, callback) {
+  weui.confirm(message, function() {
+    if (callback && callback instanceof Function) { callback(true); }
+  }, function() {
+    if (callback && callback instanceof Function) { callback(false); }
+  }, { title, isAndroid: false });
+}
+function onConfirm(res) {
+  if (res) { alert('click yes'); } else { alert('click no'); }
+}
+function showCustom1(message, title) {
+  promptVC({
+    text: message,
+    title,
+    onOK(val) {
+      // val格式: {verifyCode: 'xxxxxx'}
+      console.log(val);
+      alert(JSON.stringify(val));
+    },
+    onSend() {
+      showToast('验证码已发送');
+    },
+    isAndroid: false,
+  });
+}
+function showCustom2(message, title) {
+  promptVC({
+    text: message,
+    title,
+    inputPhone: true,
+    onOK(val) {
+      // val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx'}
+      console.log(val);
+      //				if(!/1[3-8]\d{9}/.test(val.phone)){
+      //					weui.topTips('手机号无效');
+      //					return false;
+      //				}
+      alert(JSON.stringify(val));
+    },
+    onSend(val) {
+      if (/1[3-8]\d{9}/.test(val)) { showToast('验证码已发送'); } else {
+        weui.topTips('手机号无效');
+        return false;
+      }
+    },
+  });
+}
+function promptOrder(message, pkgName, pkgCode, okText, cancelText, onSubmit, onCancel) {
+  const imgUrl = 'verifyImage?timestamp=' + new Date().getTime();
+  promptVC({
+    text: message,
+    title: pkgName,
+    inputPhone: true,
+    okText: okText || '办理',
+    cancelText: cancelText || '关闭',
+    verifyImg: imgUrl,
+    isAndroid: false,
+    onOK(val) {
+      // val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx', verifyImg: 'xxxx'}
+      console.log(val);
+      if (onSubmit) { onSubmit(val.phone, val.verifyCode, val.verifyImg, pkgCode, pkgName); }
+    },
+    onCancel,
+    onSend(phone) {
+      requestVerify(phone);
+    },
+  });
+  $('img.weui-prompt-button').click(function() {
+    $(this).attr('src', 'verifyImage?timestamp=' + new Date().getTime());
+  });
+}
+
+function promptAction(message, title, okText, cancelText, onSubmit) {
+  const imgUrl = 'verifyImage?timestamp=' + new Date().getTime();
+  let opts = {};
+  if (message instanceof Object) {
+    opts = message;
+  } else {
+    opts.message = message;
+    opts.title = title;
+    opts.okText = okText;
+    opts.cancelText = cancelText;
+    opts.onSubmit = onSubmit;
+  }
+  if (opts.imgUrl == undefined) { opts.imgUrl = imgUrl; }
+  promptVC({
+    text: opts.message,
+    title: opts.title,
+    inputPhone: opts.phoneNo || true,
+    okText: opts.okText || '办理',
+    cancelText: opts.cancelText || '关闭',
+    verifyImg: opts.imgUrl,
+    isAndroid: false,
+    onOK(val) {
+      // val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx', verifyImg: 'xxxx'}
+      console.log(val);
+      if (opts.onSubmit && opts.onSubmit instanceof Function) {
+        opts.onSubmit(val.phone, val.verifyCode, val.verifyImg);
+      }
+    },
+    onCancel() {
+      if (opts.closable === false) { return false; }
+    },
+    onSend(phone) {
+      requestVerify(phone, opts.verifyCode);
+    },
+  });
+  $('img.weui-prompt-button').click(function() {
+    $(this).attr('src', 'verifyImage?timestamp=' + new Date().getTime());
+  });
+}
+
+
+function requestVerify(phone, opts) {
+  showLoading('请求发送中');
+  opts = opts || {};
+  const params = {};
+  params[opts.param || 'phoneNo'] = phone;
+  $.ajax({
+    type: 'post',
+    url: opts.url || 'verifyCode',
+    data: params,
+    dataType: 'json',
+    success(result) {
+      hideLoading();
+      if (result.status == 0) {
+        showToast('验证码已发送,请查收短信', 3000);
+      } else {
+        console.log(result);
+        weui.topTips(result.message);
+      }
+    },
+  }).always(function() {
+    hideLoading();
+  });
+}
+
+// 输入验证码
+// options = {
+//   message: String,
+//   title: String,
+//   phoneNo: String 或  true
+//   okText: String,
+//   cancelText: String,
+//   onResult: Function,
+//   verifyImg: Boolean,
+//   closable: Boolean,
+// }
+function showLogin(message, title, okText, cancelText, onResult) {
+  let opts = {};
+  if (message instanceof Object) {
+    opts = message;
+  } else {
+    opts.message = message;
+    opts.title = title;
+    opts.okText = okText;
+    opts.cancelText = cancelText;
+    opts.onResult = onResult;
+  }
+
+  if (!opts.onResult || !(opts.onResult instanceof Function)) {
+    showAlert('调用错误:必须指定onResult回调参数');
+    return;
+  }
+  const imgUrl = 'verifyImage?timestamp=' + new Date().getTime();
+  if (opts.verifyImg) { opts.imgUrl = imgUrl; }
+  promptVC({
+    text: opts.message,
+    title: opts.title,
+    inputPhone: opts.phoneNo || true,
+    okText: opts.okText || '登录',
+    cancelText: opts.cancelText || '关闭',
+    verifyImg: opts.imgUrl,
+    isAndroid: false,
+    onOK(val) {
+      // val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx', verifyImg: 'xxxx'}
+      console.log(val);
+      const param = {
+	              phoneNo: val.phone,
+	              verifyCode: val.verifyCode,
+	              verifyImg: val.verifyImg,
+	            };
+      showLoading();
+      $.post('login', param)
+        .then(function(result) {
+          opts.onResult(result);
+        }).fail(function(jqXHR, textStatus, errorThrown) {
+          let msg = '请求发送失败';
+          if (typeof jqXHR === 'string') { msg = jqXHR; }
+          weui.topTips(msg);
+        })
+        .always(function() {
+          hideLoading();
+        });
+    },
+    onCancel() {
+      if (opts.closable === false) { return false; }
+    },
+    onSend(phone) {
+      requestVerify(phone, opts.verifyCode);
+    },
+  });
+  $('img.weui-prompt-button').click(function() {
+    $(this).attr('src', 'verifyImage?timestamp=' + new Date().getTime());
+  });
+}

+ 99 - 0
app/router.js

@@ -0,0 +1,99 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+
+  // 开放接口
+  router.post('/api/power/login', controller.login.adminlogin); // 管理员帐号密码登录
+  router.get('/api/qrcodeToken', controller.weixin.qrcodeToken); // 获取公众号token
+  router.get('/api/getuuid', controller.weixin.getuuid); // 获取uuid
+  router.get('/api/qrcodelogin/:uuid', controller.weixin.qrcodelogin); // 二维码登录
+  router.get('/api/check', controller.weixin.check); // 查询二维码状态
+  router.get('/api/mqtt/:key', controller.weixin.mqtt); // 发送mq消息
+  // 支付相关
+  router.post('/api/weixin/orderPay', controller.weixin.orderPay); // 预支付交易单
+  router.get('/api/weixin/pay', controller.weixin.pay); // 发起支付接口
+  router.get('/api/weixin/orderClose', controller.weixin.orderClose); // 关闭订单
+  router.get('/api/weixin/pushMould', controller.weixin.pushMould); // 模板下发
+  // 微信登录相关接口
+  router.get('/api/weixin/getopenid', controller.weixin.auth); // 获取openid
+  router.get('/api/weixin/bind', controller.weixin.bind); // 管理员绑定微信
+  router.get('/api/weixin/fetch', controller.weixin.fetch);
+
+  // 日志接口
+  // router.post('/api/log/create', controller.log.create);
+  router.delete('/api/log/delete/:_id', controller.log.delete);
+  router.get('/api/log/query', controller.log.query);
+
+  // 管理员接口
+  router.post('/api/admin/create', controller.admin.create);
+  router.post('/api/admin/update', controller.admin.update);
+  router.delete('/api/admin/delete/:_id', controller.admin.delete);
+  router.get('/api/admin/query', controller.admin.query);
+  router.get('/api/admin/fetch/:_id', controller.admin.fetch);
+  router.post('/api/editPwa', controller.login.editPwa);
+
+  // 用户接口 开放
+  router.post('/api/user/create', controller.user.create);
+  router.post('/api/user/update', controller.user.update);
+  router.delete('/api/user/delete/:_id', controller.user.delete);
+  router.get('/api/user/query', controller.user.query);
+  router.get('/api/user/fetch/:_id', controller.user.fetch);
+
+  // 单页接口
+  router.post('/api/pages/create', controller.pages.create);
+  router.post('/api/pages/update', controller.pages.update);
+  router.delete('/api/pages/delete/:_id', controller.pages.delete);
+  router.get('/api/pages/query', controller.pages.query);
+  router.get('/api/pages/fetch/:code', controller.pages.fetch);
+
+  // 内容接口
+  router.post('/api/content/create', controller.content.create);
+  router.post('/api/content/update', controller.content.update);
+  router.delete('/api/content/delete/:_id', controller.content.delete);
+  router.get('/api/content/query', controller.content.query);
+  router.get('/api/content/fetch/:_id', controller.content.fetch);
+
+  // 医院接口
+  router.post('/api/hospital/create', controller.hospital.create);
+  router.post('/api/hospital/update', controller.hospital.update);
+  router.delete('/api/hospital/delete/:_id', controller.hospital.delete);
+  router.get('/api/hospital/query', controller.hospital.query);
+  router.get('/api/hospital/fetch/:_id', controller.hospital.fetch);
+
+  // 科室接口
+  router.post('/api/subject/create', controller.subject.create);
+  router.post('/api/subject/update', controller.subject.update);
+  router.delete('/api/subject/delete/:_id', controller.subject.delete);
+  router.get('/api/subject/query', controller.subject.query);
+  router.get('/api/subject/fetch/:_id', controller.subject.fetch);
+
+  // 专家接口
+  router.post('/api/specialist/create', controller.specialist.create);
+  router.post('/api/specialist/update', controller.specialist.update);
+  router.delete('/api/specialist/delete/:_id', controller.specialist.delete);
+  router.get('/api/specialist/query', controller.specialist.query);
+  router.get('/api/specialist/fetch/:_id', controller.specialist.fetch);
+
+  // 订单接口
+  router.post('/api/order/create', controller.order.create); // 开放
+  router.post('/api/order/update', controller.order.update);
+  router.delete('/api/order/delete/:_id', controller.order.delete);
+  router.get('/api/order/query', controller.order.query);
+  router.get('/api/order/fetch/:_id', controller.order.fetch);
+  router.get('/api/order/updatestatus', controller.order.updatestatus);
+
+  // TODO: 自动配置路由,将所有以‘Action’结尾的方法自动进行路由注册
+  Object.keys(app.controller).forEach(key => {
+    const c = app.controller[key];
+    Object.keys(c).forEach(a => {
+      if (a.endsWith('Action')) {
+        const p = a.substr(0, a.length - 6);
+        app.all(`/${key}${p === 'index' ? '' : ('/' + p)}`, `${key}.${a}`);
+      }
+    });
+  });
+};

+ 13 - 0
app/service/admin.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class AdminService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'admin');
+    this.model = this.ctx.model.Admin;
+  }
+
+}
+
+module.exports = AdminService;

+ 13 - 0
app/service/content.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class ContentService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'content');
+    this.model = this.ctx.model.Content;
+  }
+
+}
+
+module.exports = ContentService;

+ 13 - 0
app/service/hospital.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class HospitalService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'hospital');
+    this.model = this.ctx.model.Hospital;
+  }
+
+}
+
+module.exports = HospitalService;

+ 80 - 0
app/service/log.js

@@ -0,0 +1,80 @@
+'use strict';
+
+const Service = require('egg').Service;
+const assert = require('assert');
+const moment = require('moment');
+const routerMethod = require('../middleware/routerMethod');
+const routerMondel = require('../middleware/routerMondel');
+const jwt = require('jsonwebtoken');
+class LogService extends Service {
+  async create({ mondel, method, data, result, userName, date }) {
+    assert(mondel, '模块不存在');
+    assert(method, '类型不存在');
+    assert(data, '数据不存在');
+    assert(result, '结果不存在');
+    assert(date, '时间不存在');
+    const { Log: model } = this.ctx.model;
+    const createAt = moment().format('x');
+    try {
+      await model.create({ data, method, createAt, mondel, result, userName, date });
+      return { errmsg: '', errcode: 0 };
+    } catch (error) {
+      throw new Error({ errcode: -2001, errmsg: '添加失败' });
+    }
+  }
+  async del({ id }) {
+    assert(id, 'id不存在');
+    const { Log: model } = this.ctx.model;
+    try {
+      await model.findById(id).remove();
+      return { errmsg: '', errcode: 0 };
+    } catch (error) {
+      throw new Error({ errcode: -2001, errmsg: '删除失败' });
+    }
+  }
+  async query({ skip, limit, mondel, method, result }) {
+    const { Log: model } = this.ctx.model;
+    const filter = {};
+    if (mondel) filter.mondel = mondel;
+    if (method) filter.method = method;
+    if (result) filter.result = result;
+    try {
+      const total = await model.find({ ...filter });
+      let res;
+      if (skip && limit) {
+        res = await model.find({ ...filter }).skip(Number(skip) * Number(limit)).limit(Number(limit));
+      } else {
+        res = await model.find({ ...filter });
+      }
+      return { errmsg: '', errcode: 0, data: res, total: total.length };
+    } catch (error) {
+      console.log(error);
+      throw new Error({ errcode: -2001, errmsg: '查询失败' });
+    }
+  }
+  async init() {
+    const { ctx } = this;
+    const { url, method, body } = ctx.request;
+    let decode = {};
+    const date = moment().format('YYYY-MM-DD HH:mm:ss');
+    if (method !== 'GET') {
+      // token
+      const tokenstr = ctx.request.header.authorization;
+      if (tokenstr !== 'undefined' && tokenstr) {
+        const token = tokenstr.substring(7);
+        decode = jwt.verify(token, this.config.jwt.secret);
+      } else {
+        decode = body;
+      }
+      const str = url.split('/');
+      const methods = routerMethod[str[3]];
+      const mondel = routerMondel[str[2]];
+      if (methods && mondel) {
+        return { mondel, method: methods, data: body, userName: decode.userName, date };
+      }
+      return false;
+    }
+  }
+}
+
+module.exports = LogService;

+ 111 - 0
app/service/login.js

@@ -0,0 +1,111 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { NafService } = require('naf-framework-mongoose/lib/service');
+const jwt = require('jsonwebtoken');
+
+class JwtLoginService extends NafService {
+  constructor(ctx) {
+    super(ctx, 'naf_user_info');
+    this.model = this.ctx.model.Admin;
+    this.dept = this.ctx.service.dept;
+    this.tag = this.ctx.service.tag;
+  }
+
+  async adminlogin({ userName, password }) {
+    // TODO:参数检查和默认参数处理
+    assert(userName);
+    assert(password);
+    // TODO:检查useridh和mobile
+    const entity = await this.model.findOne({ userName });
+    if (entity == null) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '用户不存在');
+    }
+    if (!entity.password || entity.password !== password) {
+      throw new BusinessError(ErrorCode.BAD_PASSWORD);
+    }
+    const userinfo = _.pick(entity, [ 'userName', 'openid' ]);
+    const { secret, expiresIn = '1h', issuer = 'naf' } = this.config.jwt;
+    const token = await jwt.sign(userinfo, secret, { expiresIn, issuer, subject: `${userName}@${this.tenant}` });
+    return { userinfo, token };
+  }
+
+  // 修改密码
+  async editPwa({ userName, oldpass, newpass }) {
+    assert(userName, '用户名不能为空');
+    assert(oldpass, '原有密码不能为空');
+    assert(newpass, '新密码不能为空');
+
+    // 检查用户是否存在
+    const entity = await this.model.findOne({ userName });
+    if (!entity) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '用户不存在');
+    // 校验旧密码
+    if (!entity.password || entity.password !== oldpass) {
+      throw new BusinessError(ErrorCode.BAD_PASSWORD, '原密码错误');
+    }
+    if (entity.password) {
+      entity.password = newpass;
+    }
+    await entity.save();
+  }
+
+  // 二维码登录
+  async qrcodelogin({ uuid }) {
+    const { code } = this.ctx.query;
+    if (code) {
+      return await this.Back({ code, uuid });
+    }
+    // TODO: 生成回调地址
+    const { wxapi, authUrl = this.ctx.path } = this.app.config;
+    const backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${authUrl}`);
+    const to_uri = `${wxapi.baseUrl}/api/auth?appid=${wxapi.appid}&response_type=code&redirect_uri=${backUrl}#wechat`;
+    this.ctx.redirect(to_uri);
+  }
+  // GET 认证回调
+  async Back({ code, uuid }) {
+    const { weixin } = this.ctx.service;
+    const res = await weixin.fetch(code);
+    const openid = res.openid;
+    await this.ctx.render('weixinlogin.njk', { openid, uuid });
+  }
+  // 检查uuid是否过期与uuid状态
+  async checkQrcode({ uuid, openid }) {
+    // 验证二维码
+    const val = await this.app.redis.get(uuid);
+    if (!val) {
+      return { errcode: -1001, errmsg: '二维码已过期' };
+    }
+    if (val !== 'ready') {
+      return { errcode: -1001, errmsg: '二维码已失效' };
+    }
+    // 验证用户
+    const res = await this.ctx.service.admin.query({ openid });
+    if (res.length <= 0) {
+      return { errcode: -1001, errmsg: '微信用户不存在' };
+    }
+    const msg = JSON.stringify(await this.adminlogin(res[0]));
+    await this.app.redis.set(uuid, msg, 'EX', 600);
+    return;
+  }
+
+  // 使用二维码换取登录凭证
+  async qrcodeToken(uuid) {
+    assert(uuid, 'uuid不能为空');
+    const val = await this.app.redis.get(uuid);
+    if (!val) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    if (val === 'ready' || val === 'consumed') {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
+    }
+
+    // TODO: 修改二维码状态
+    await this.app.redis.set(uuid, 'consumed', 'EX', 600);
+
+    return val;
+  }
+}
+
+module.exports = JwtLoginService;

+ 37 - 0
app/service/order.js

@@ -0,0 +1,37 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const uuid = require('uuid');
+const moment = require('moment');
+const assert = require('assert');
+class OrderService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'order');
+    this.model = this.ctx.model.Order;
+  }
+  async create({ name, phone, openid, code, hospitalId, hospitalName, subjectId, subjectName, specialistId, specialistName, money, status, remark }) {
+    assert(name, '请填写完整信息');
+    assert(phone, '请填写完整信息');
+    assert(openid, '请填写完整信息');
+    assert(code, '请填写完整信息');
+    assert(hospitalId, '请填写完整信息');
+    assert(hospitalName, '请填写完整信息');
+    assert(subjectId, '请填写完整信息');
+    assert(subjectName, '请填写完整信息');
+    assert(specialistId, '请填写完整信息');
+    assert(specialistName, '请填写完整信息');
+    assert(money, '请填写完整信息');
+    const time = moment().format('YYYY-MM-DD HH:mm:ss');
+    let out_trade_no = uuid.v1();
+    out_trade_no = out_trade_no.replace(/-/g, '');
+    const res = await this.model.create({ name, phone, openid, time, code, hospitalId, hospitalName, subjectId, subjectName, specialistId, specialistName, money, status, remark, out_trade_no });
+    return res;
+  }
+  async updatestatus({ out_trade_no }) {
+    assert(out_trade_no, '缺少参数out_trade_no');
+    const res = await this.model.findOne({ out_trade_no }).updateOne({ status: '1' });
+    return res;
+  }
+}
+
+module.exports = OrderService;

+ 13 - 0
app/service/pages.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class PagesService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'page');
+    this.model = this.ctx.model.Pages;
+  }
+
+}
+
+module.exports = PagesService;

+ 13 - 0
app/service/specialist.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class SpecialistService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'specialist');
+    this.model = this.ctx.model.Specialist;
+  }
+
+}
+
+module.exports = SpecialistService;

+ 13 - 0
app/service/subject.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class SubjectService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'subject');
+    this.model = this.ctx.model.Subject;
+  }
+
+}
+
+module.exports = SubjectService;

+ 13 - 0
app/service/user.js

@@ -0,0 +1,13 @@
+'use strict';
+
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+class UserService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'user');
+    this.model = this.ctx.model.User;
+  }
+
+}
+
+module.exports = UserService;

+ 224 - 0
app/service/weixin.js

@@ -0,0 +1,224 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const Payment = require('./wxpayv3');
+const { readFileSync } = require('fs');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { AxiosService } = require('naf-framework-mongoose/lib/service');
+class WeixinAuthService extends AxiosService {
+  constructor(ctx) {
+    super(ctx, {}, _.get(ctx.app.config, 'wxapi'));
+  }
+
+  // 通过认证码获得用户信息
+  async fetch(code) {
+    // TODO:参数检查和默认参数处理
+    assert(code);
+    const res = await this.httpGet('/api/fetch', { code });
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch open by code fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信认证信息失败');
+    }
+    return res;
+  }
+  // 获取access_token
+  async gettoken() {
+    const { wxapi } = this.app.config;
+    const to_uri = `${wxapi.tokenUrl}?appid=${wxapi.appid}&secret=${wxapi.appsecret}&grant_type=client_credential`;
+    const { access_token } = await this.httpGet(to_uri);
+    return access_token;
+  }
+  // 下发模板消息
+  async pushMould({ openid, out_trade_no }) {
+    await this.userMould({ openid, out_trade_no });
+    await this.adminMould({ out_trade_no });
+  }
+  // 管理员消息下发
+  async adminMould({ out_trade_no }) {
+    const { wxapi } = this.app.config;
+    const access_token = await this.gettoken();
+    const to_uri = `${wxapi.mould}?access_token=${access_token}`;
+    const { Order: model } = this.ctx.model;
+    const { Admin: adminModel } = this.ctx.model;
+    const order = await model.findOne({ out_trade_no });
+    const data = {
+      first: {
+        value: '您有新的订单',
+        color: '#173177',
+      },
+      keyword1: {
+        value: order.name,
+        color: '#173177',
+      },
+      keyword2: {
+        value: order.phone,
+        color: '#173177',
+      },
+      keyword3: {
+        value: order.hospitalName,
+        color: '#173177',
+      },
+      keyword4: {
+        value: order.subjectName,
+        color: '#173177',
+      },
+      keyword5: {
+        value: order.time,
+        color: '#173177',
+      },
+      remark: {
+        value: order.remark,
+        color: '#173177',
+      },
+    };
+    const userList = await adminModel.find();
+    return Promise.all(
+      userList.filter(async e => {
+        if (e.openid && e.openid !== null) {
+          await this.ctx.curl(to_uri, {
+            method: 'POST',
+            dataType: 'json',
+            data: JSON.stringify({
+              touser: e.openid,
+              template_id: wxapi.adminTemplateId,
+              data,
+            }),
+          });
+        }
+      })
+    );
+  }
+  // 用户消息下发
+  async userMould({ openid, out_trade_no }) {
+    const { wxapi } = this.app.config;
+    const access_token = await this.gettoken();
+    const { Order: model } = this.ctx.model;
+    const order = await model.findOne({ out_trade_no });
+    if (access_token && order) {
+      const to_uri = `${wxapi.mould}?access_token=${access_token}`;
+      const data = {
+        touser: openid,
+        template_id: wxapi.userTemplateId,
+        url: '',
+        topcolor: '#FF0000',
+        data: {
+          first: {
+            value: '感谢您登陆佳泰医疗健康管理',
+            color: '#173177',
+          },
+          keyword1: {
+            value: order.subjectName,
+            color: '#173177',
+          },
+          keyword2: {
+            value: order.hospitalName,
+            color: '#173177',
+          },
+          keyword3: {
+            value: order.name,
+            color: '#173177',
+          },
+          keyword4: {
+            value: order.phone,
+            color: '#173177',
+          },
+          keyword5: {
+            value: order.time,
+            color: '#173177',
+          },
+          remark: {
+            value: '我们将会尽快处理您的预约请求。',
+            color: '#173177',
+          },
+        },
+      };
+      await this.ctx.curl(to_uri, {
+        method: 'POST',
+        dataType: 'json',
+        data: JSON.stringify(data),
+      });
+    }
+  }
+  // 预支付交易单 # description = 商品描述, out_trade_no = 自定义订单号
+  async orderPay({ openid, description, out_trade_no, money, _id }) {
+    const { wxapi, authUrl = this.ctx.path } = this.app.config;
+    const paymnet = new Payment({
+      appid: wxapi.appid,
+      mchid: wxapi.mchid,
+      private_key: readFileSync(wxapi.wxKey).toString(), // 或者直接复制证书文件内容
+      serial_no: wxapi.certid,
+      apiv3_private_key: wxapi.v3key,
+    });
+    const host = this.ctx.header.host;
+    const backUrl = encodeURI(`${this.ctx.protocol}://${host}${authUrl}`);
+    // jsapi 支付下单
+    const result = await paymnet.jsapi({
+      description,
+      out_trade_no,
+      notify_url: backUrl,
+      amount: {
+        total: money * 100,
+        currency: 'CNY',
+      },
+      payer: {
+        openid,
+      },
+    });
+    // 如果请求成功 redis存数据
+    if (result.status === 200) {
+      const { prepay_id } = JSON.parse(result.data);
+      const order = JSON.stringify({ openid, prepay_id, _id, out_trade_no });
+      await this.app.redis.set('orderPay', order, 'EX', 1200000);
+      return false;
+    }
+  }
+  // 跳转发起支付页面
+  async pay() {
+    const { wxapi } = this.app.config;
+    const orderPay = await this.app.redis.get('orderPay');
+    const { openid, prepay_id, out_trade_no } = JSON.parse(orderPay);
+    const appid = wxapi.appid;
+    // 商户密钥证书
+    const privateKey = readFileSync(wxapi.wxKey).toString();
+    // 32位随机字符串
+    const payment = new Payment({
+      appid: wxapi.appid,
+      mchid: wxapi.mchid,
+      private_key: readFileSync(wxapi.wxKey).toString(), // 或者直接复制证书文件内容
+      serial_no: wxapi.certid,
+      apiv3_private_key: wxapi.v3key,
+    });
+    const payNonceStr = payment.generate();
+    // 时间戳
+    const payTimestamp = new Date().getTime();
+    // 签名
+    const data = `${appid}\n${payTimestamp}\n${payNonceStr}\nprepay_id=${prepay_id}\n`;
+    const createSign = payment.rsaSign(data, privateKey);
+    const pay_uri = this.ctx.header.referer;
+    const redirect_uri = `${this.ctx.protocol}://${this.ctx.header.host}/yl-web/reserve`;
+    // TODO: 重定向到支付页面
+    await this.ctx.render('pay.njk', { openid, appid, prepay_id, payNonceStr, payTimestamp, createSign, redirect_uri, out_trade_no, pay_uri });
+  }
+  // 关闭订单
+  async orderClose({ out_trade_no }) {
+    const { wxapi } = this.app.config;
+    const { Order: model } = this.ctx.model;
+    // 32位随机字符串
+    const payment = new Payment({
+      appid: wxapi.appid,
+      mchid: wxapi.mchid,
+      private_key: readFileSync(wxapi.wxKey).toString(),
+      serial_no: wxapi.certid,
+      apiv3_private_key: wxapi.v3key,
+    });
+    const res = await payment.close({ out_trade_no });
+    if (res.status === 204) {
+      // 订单关闭成功  删除原订单
+      await model.findOne({ out_trade_no }).remove();
+    }
+    return res;
+  }
+}
+
+module.exports = WeixinAuthService;

+ 360 - 0
app/service/wxpayv3.js

@@ -0,0 +1,360 @@
+/* eslint-disable strict */
+const urllib = require('urllib');
+const { KJUR, hextob64 } = require('jsrsasign');
+const assert = require('assert');
+// const nodeAesGcm = require('node-aes-gcm');
+const crypto = require('crypto');
+const x509 = require('@peculiar/x509');
+class Payment {
+  constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
+    assert(appid, 'appid is required');
+    assert(mchid, 'mchid is required');
+    assert(private_key, 'private_key is required');
+    assert(serial_no, 'serial_no is required');
+    assert(apiv3_private_key, 'apiv3_private_key is required');
+
+    this.appid = appid;
+    this.mchid = mchid;
+    this.private_key = private_key;
+    this.serial_no = serial_no;
+    this.apiv3_private_key = apiv3_private_key;
+    this.notify_url = notify_url;
+
+    this.urls = {
+      jsapi: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
+          method: 'POST',
+          pathname: '/v3/pay/transactions/jsapi',
+        };
+      },
+      app: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/app',
+          method: 'POST',
+          pathname: '/v3/pay/transactions/app',
+        };
+      },
+      h5: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5',
+          method: 'POST',
+          pathname: '/v3/pay/transactions/h5',
+        };
+      },
+      native: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
+          method: 'POST',
+          pathname: '/v3/pay/transactions/native',
+        };
+      },
+      getTransactionsById: ({ pathParams }) => {
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
+          method: 'GET',
+          pathname: `/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
+        };
+      },
+      getTransactionsByOutTradeNo: ({ pathParams }) => {
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
+          method: 'GET',
+          pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
+        };
+      },
+      close: ({ pathParams }) => {
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
+          method: 'POST',
+          pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
+        };
+      },
+      refund: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
+          method: 'POST',
+          pathname: '/v3/refund/domestic/refunds',
+        };
+      },
+      getRefund: ({ pathParams }) => {
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
+          method: 'GET',
+          pathname: `/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
+        };
+      },
+      getCertificates: () => {
+        return {
+          url: 'https://api.mch.weixin.qq.com/v3/certificates',
+          method: 'GET',
+          pathname: '/v3/certificates',
+        };
+      },
+      tradebill: ({ queryParams }) => {
+        const { bill_date, bill_type, tar_type } = queryParams;
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/bill/tradebill?bill_date=${bill_date}${bill_type ? '&bill_type=' + bill_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
+          method: 'GET',
+          pathname: `/v3/bill/tradebill?bill_date=${bill_date}${bill_type ? '&bill_type=' + bill_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
+        };
+      },
+      fundflowbill: ({ queryParams }) => {
+        const { bill_date, account_type, tar_type } = queryParams;
+        return {
+          url: `https://api.mch.weixin.qq.com/v3/bill/fundflowbill?bill_date=${bill_date}${account_type ? '&account_type=' + account_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
+          method: 'GET',
+          pathname: `/v3/bill/fundflowbill?bill_date=${bill_date}${account_type ? '&account_type=' + account_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
+        };
+      },
+      downloadbill: ({ pathParams }) => {
+        const url = pathParams;
+        const index = url.indexOf('/v3');
+        const pathname = url.substr(index);
+        return {
+          url,
+          method: 'GET',
+          pathname,
+        };
+      },
+    };
+    this.decodeCertificates();
+  }
+  // 调用封装  制作签名
+  async run({ pathParams, queryParams, bodyParams, type }) {
+    assert(type, 'type is required');
+    const { url, method, pathname } = this.urls[type]({ pathParams, queryParams });
+    const timestamp = Math.floor(Date.now() / 1000);
+    const onece_str = this.generate();
+    const bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '';
+    const signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA');
+    const Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`;
+    const { status, data } = await urllib.request(url, {
+      method,
+      dataType: 'text',
+      data: method === 'GET' ? '' : bodyParams,
+      timeout: [ 10000, 15000 ],
+      headers: {
+        'Content-Type': 'application/json',
+        Accept: 'application/json',
+        Authorization,
+      },
+    });
+    return { status, data };
+  }
+
+  // jsapi统一下单
+  async jsapi(params) {
+    const bodyParams = {
+      ...params,
+      appid: this.appid,
+      mchid: this.mchid,
+      notify_url: params.notify_url || this.notify_url,
+    };
+    return await this.run({ bodyParams, type: 'jsapi' });
+  }
+
+  // app统一下单
+  async app(params) {
+    const bodyParams = {
+      ...params,
+      appid: this.appid,
+      mchid: this.mchid,
+      notify_url: params.notify_url || this.notify_url,
+    };
+    return await this.run({ bodyParams, type: 'app' });
+  }
+
+  // h5统一下单
+  async h5(params) {
+    const bodyParams = {
+      ...params,
+      appid: this.appid,
+      mchid: this.mchid,
+      notify_url: params.notify_url || this.notify_url,
+    };
+    return await this.run({ bodyParams, type: 'h5' });
+  }
+
+  // native统一下单
+  async native(params) {
+    const bodyParams = {
+      ...params,
+      appid: this.appid,
+      mchid: this.mchid,
+      notify_url: params.notify_url || this.notify_url,
+    };
+    return await this.run({ bodyParams, type: 'native' });
+  }
+
+  // 通过transaction_id查询订单
+  async getTransactionsById(params) {
+    return await this.run({ pathParams: params, type: 'getTransactionsById' });
+  }
+
+  // 通过out_trade_no查询订单
+  async getTransactionsByOutTradeNo(params) {
+    return await this.run({ pathParams: params, type: 'getTransactionsByOutTradeNo' });
+  }
+
+  // 关闭订单
+  async close(params) {
+    return await this.run({ pathParams: {
+      out_trade_no: params.out_trade_no,
+    }, bodyParams: {
+      mchid: this.mchid,
+    }, type: 'close' });
+  }
+
+  // 退款
+  async refund(params) {
+    const bodyParams = {
+      ...params,
+      notify_url: params.notify_url || this.notify_url,
+    };
+    return await this.run({ bodyParams, type: 'refund' });
+  }
+
+  // 查询单笔退款订单
+  async getRefund(params) {
+    return await this.run({ pathParams: params, type: 'getRefund' });
+  }
+
+  // 获取平台证书列表
+  async getCertificates() {
+    return await this.run({ type: 'getCertificates' });
+  }
+
+  // 解密证书列表 解出CERTIFICATE以及public key
+  async decodeCertificates() {
+    const result = await this.getCertificates();
+    if (result.status !== 200) {
+      throw new Error('获取证书列表失败');
+    }
+    const certificates = typeof result.data === 'string' ? JSON.parse(result.data).data : result.data.data;
+    for (const cert of certificates) {
+      const plaintext = this.decode(cert.encrypt_certificate);
+      cert.decrypt_certificate = plaintext.toString();
+      const beginIndex = cert.decrypt_certificate.indexOf('-\n');
+      const endIndex = cert.decrypt_certificate.indexOf('\n-');
+      const str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex);
+      const x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
+      const public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64');
+      cert.public_key = '-----BEGIN PUBLIC KEY-----\n' + public_key + '\n-----END PUBLIC KEY-----';
+    }
+    // eslint-disable-next-line no-return-assign
+    return this.certificates = certificates;
+  }
+
+  // 验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
+  async verifySign({ timestamp, nonce, serial, body, signature }, repeatVerify = true) {
+    const data = `${timestamp}\n${nonce}\n${typeof body === 'string' ? body : JSON.stringify(body)}\n`;
+    const verify = crypto.createVerify('RSA-SHA256');
+    verify.update(Buffer.from(data));
+    let verifySerialNoPass = false;
+    for (const cert of this.certificates) {
+      if (cert.serial_no === serial) {
+        verifySerialNoPass = true;
+        return verify.verify(cert.public_key, signature, 'base64');
+      }
+    }
+    if (!verifySerialNoPass && repeatVerify) {
+      await this.decodeCertificates();
+      return await this.verifySign({ timestamp, nonce, serial, body, signature }, false);
+    }
+    throw new Error('平台证书序列号不相符');
+
+  }
+
+
+  // 申请交易账单
+  async tradebill(params) {
+    return await this.run({ queryParams: params, type: 'tradebill' });
+  }
+
+  // 申请资金账单
+  async fundflowbill(params) {
+    return await this.run({ queryParams: params, type: 'fundflowbill' });
+  }
+
+  // 下载账单
+  async downloadbill(download_url) {
+    return await this.run({ pathParams: download_url, type: 'downloadbill' });
+  }
+
+
+  // 解密支付退款通知资源数据
+  decodeResource(resource) {
+    const { plaintext } = this.decode(resource);
+    return JSON.parse(plaintext.toString());
+  }
+
+  // 解密
+  decode(params) {
+    const AUTH_KEY_LENGTH = 16;
+    // ciphertext = 密文,associated_data = 填充内容, nonce = 位移
+    const { ciphertext, associated_data, nonce } = params;
+    // 密钥
+    const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
+    // 位移
+    const nonce_bytes = Buffer.from(nonce, 'utf8');
+    // 填充内容
+    const associated_data_bytes = Buffer.from(associated_data, 'utf8');
+    // 密文Buffer
+    const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
+    // 计算减去16位长度
+    const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
+    // upodata
+    const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
+    // tag
+    const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
+    const decipher = crypto.createDecipheriv(
+      'aes-256-gcm', key_bytes, nonce_bytes
+    );
+    decipher.setAuthTag(auth_tag_bytes);
+    decipher.setAAD(Buffer.from(associated_data_bytes));
+
+    const output = Buffer.concat([
+      decipher.update(cipherdata_bytes),
+      decipher.final(),
+    ]);
+    // output = Buffer对象
+    return output;
+    // return nodeAesGcm.decrypt(key_bytes, nonce_bytes, cipherdata_bytes, associated_data_bytes, auth_tag_bytes);
+  }
+
+  // 生成随机字符串
+  generate(length = 32) {
+    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let noceStr = '',
+      // eslint-disable-next-line prefer-const
+      maxPos = chars.length;
+    // eslint-disable-next-line no-bitwise
+    while (length--) noceStr += chars[Math.random() * maxPos | 0];
+    return noceStr;
+  }
+
+
+  /**
+   * rsa签名
+   * @param content 签名内容
+   * @param privateKey 私钥,PKCS#1
+   * @param hash hash算法,SHA256withRSA,SHA1withRSA
+   * @return 返回签名字符串,base64
+   */
+  rsaSign(content, privateKey, hash = 'SHA256withRSA') {
+    // 创建 Signature 对象
+    const signature = new KJUR.crypto.Signature({
+      alg: hash,
+      // !这里指定 私钥 pem!
+      prvkeypem: privateKey,
+    });
+    signature.updateString(content);
+    const signData = signature.sign();
+    // 将内容转成base64
+    return hextob64(signData);
+  }
+}
+
+module.exports = Payment;
+

+ 7 - 0
app/util/constant.js

@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = {
+  SEQ_TAG: 'naf_tag',
+  SEQ_DEPT: 'naf_dept',
+  SEQ_USER: 'naf_user',
+};

+ 107 - 0
app/view/pay.njk

@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <title>确认订单</title>
+  <meta charset="utf-8"></meta>
+  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"></meta>
+	<script type="text/javascript" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
+  <script type="text/javascript" src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.1.3/weui.min.js"></script>
+  <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-success weui-icon_msg" id="success"></i>
+			<i class="weui-icon-waiting weui-icon_msg" id="waiting"></i>
+		</div>
+		<div class="weui-msg__text-area">
+			<h2 class="weui-msg__title" id="success2">
+				微信支付成功
+			</h2>
+			<h2 class="weui-msg__title" id="waiting2">
+				微信支付中......
+			</h2>
+		</div>
+    <div class="weui-msg__opr-area">
+			<p class="weui-btn-area">
+				<a href="javascript:;" class="weui-btn weui-btn_default" id="btn">完成</a>
+			</p>
+		</div>
+  </div>
+  <script>
+    var openid = '{{openid}}';
+    var appid = '{{appid}}';
+    var prepay_id = '{{prepay_id}}'; 
+    var payTimestamp = '{{payTimestamp}}';
+    var payNonceStr = '{{payNonceStr}}';
+    var createSign = '{{createSign}}';
+    var redirect_uri = '{{redirect_uri}}';
+    var out_trade_no = '{{out_trade_no}}';
+    var pay_uri = '{{pay_uri}}'
+    {# 显示成功 #}
+    function showsuccess() {
+      $("#success").show()
+      $("#success2").show()
+      $("#waiting").hide()
+      $("#waiting2").hide()
+      $("#btn").show()
+    }
+    {# 显示加载中 #}
+    function showwaiting() {
+      $("#waiting").show()
+      $("#waiting2").show()
+      $("#success2").hide()
+      $("#success").hide()
+      $("#btn").hide()
+    }
+    {# 点击关闭 #}
+    $("#btn").click(function () {
+      wx.closeWindow();
+    })
+    {# 支付接口 #}
+    function onBridgeReady() {
+      WeixinJSBridge.invoke('getBrandWCPayRequest', {
+          "appId": appid,     //公众号ID,由商户传入     
+          "timeStamp":payTimestamp,     //时间戳,自1970年以来的秒数     
+          "nonceStr": payNonceStr,      //随机串     
+          "package": `prepay_id=${prepay_id}`,
+          "signType": "RSA",     //微信签名方式:     
+          "paySign": createSign //微信签名 
+      },
+      function(res) {
+          if (res.err_msg == "get_brand_wcpay_request:ok") {
+            showsuccess();
+            $.get('/api/weixin/pushMould', { out_trade_no, openid }) 
+            $.get('/api/order/updatestatus', { out_trade_no })
+          } else {
+            $.get('/api/weixin/orderClose', { out_trade_no })
+            .then(function(res) {
+              if (res.status == 204) {
+                window.location.replace(redirect_uri);
+              }
+            }).fail(function( jqXHR, textStatus, errorThrown ) {
+              window.location.replace(pay_uri);
+            });
+          }
+      });
+    }
+    if (typeof WeixinJSBridge == "undefined") {
+      if (document.addEventListener) {
+          document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
+      } else if (document.attachEvent) {
+          document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
+          document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
+      }
+    } else {
+      onBridgeReady();
+    }
+    $(function () {
+      showwaiting()
+    })
+  </script>
+</body>
+
+</html>

+ 29 - 0
app/view/redirect.njk

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+
+<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-waiting weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">正在跳转,请稍候</h2>
+      {# <p class="weui-msg__desc">{{message}}</p> #}
+    </div>
+  </div>
+  <script>
+    var openid = '{{openid | safe}}';
+    var redirect_uri = '{{redirect_uri}}';
+    window.onload = function() {
+      sessionStorage.setItem('openid', openid);
+      window.location.replace(redirect_uri);
+    }
+  </script>
+</body>
+
+</html>

+ 86 - 0
app/view/weixinbind.njk

@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+<title>微信登录</title>
+<meta charset="utf-8"></meta>
+<meta name="viewport"
+	content="width=device-width, initial-scale=1, user-scalable=0"></meta>
+	<script type="text/javascript" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.1.3/weui.min.js"></script>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
+	<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
+	<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"></link>
+</head>
+
+<body>
+	<div class="weui-msg" id="app">
+		<div class="weui-msg__icon-area">
+			<i class="weui-icon-success weui-icon_msg" v-if="view == 'success'"></i>
+			<i class="weui-icon-waiting weui-icon_msg" v-else></i>
+		</div>
+		<div class="weui-msg__text-area" v-if="view == 'success'">
+			<h2 class="weui-msg__title">
+				微信绑定成功
+			</h2>
+			<p class="weui-msg__desc">您已成功通过微信扫码绑定。</p>
+		</div>
+		<div class="weui-msg__text-area" v-if="view == 'login'">
+			<h2 class="weui-msg__title">
+				你确定要绑定吗?
+			</h2>
+		</div>
+		<div class="weui-msg__opr-area" v-if="view == 'login'">
+			<p class="weui-btn-area">
+				<a href="javascript:;" class="weui-btn weui-btn_primary" v-bind:class="{ 'weui-btn_disabled': loading }"
+					v-on:click="login">${ loading ? '正在绑定...' : '确定' }</a> 
+				<a href="javascript:;" class="weui-btn weui-btn_default" v-show="!loading" v-on:click="close">取消</a>
+			</p>
+		</div>
+	</div>
+	<script type="text/javascript" th:inline="javascript">
+    const openid = '{{ openid | safe }}'
+    const id = '{{ id | safe }}'
+		var app = new Vue({
+			delimiters: ['${', '}'],
+			el : '#app',
+			data : {
+				loading : false,
+				view: 'login',
+			},
+			methods : {
+				login : function() {
+					if(this.loading) return;
+					this.loading = true;
+					$.post('/api/admin/update', { _id: id, openid: openid })
+					.then(function(result) {
+						if (result.errcode == 0) {
+							$.get('/api/mqtt/bind').then(function(res) {
+								if (res.errcode == 0) {
+                  					app.view = 'success';
+								} else {
+								  return $.Deferred().reject(res.errmsg);
+								}
+							})
+						} else {
+							return $.Deferred().reject(result.errmsg); 
+						}
+					}).fail(function( jqXHR, textStatus, errorThrown ) {
+						var msg = "处理失败,请稍后重试!";
+						if(typeof jqXHR == "string") msg = jqXHR;
+						showAlert(msg, '绑定失败');
+						app.message = msg;
+					}).always(function(){
+						app.loading = false;
+					});
+				},
+				close: function() {
+					wx.closeWindow();
+				}
+			}
+		});
+	</script>
+
+</body>
+
+</html>

+ 103 - 0
app/view/weixinlogin.njk

@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+<title>微信登录</title>
+<meta charset="utf-8"></meta>
+<meta name="viewport"
+	content="width=device-width, initial-scale=1, user-scalable=0"></meta>
+	<script type="text/javascript" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.1.3/weui.min.js"></script>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
+	<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
+	<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"></link>
+</head>
+
+<body>
+	<div class="weui-msg" id="app">
+		<div class="weui-msg__icon-area">
+			<i class="weui-icon-warn weui-icon_msg" v-if="view == 'err'"></i>
+			<i class="weui-icon-success weui-icon_msg" v-if="view == 'success'"></i>
+			<i class="weui-icon-waiting weui-icon_msg" v-else></i> 
+		</div>
+		<div class="weui-msg__text-area" v-if="view == 'success'">
+			<h2 class="weui-msg__title">
+				微信登录成功
+			</h2>
+			<p class="weui-msg__desc">您已成功通过微信扫码登录。</p>
+		</div>
+		<div class="weui-msg__text-area" v-if="view == 'login'">
+			<h2 class="weui-msg__title">
+				你确定要登录吗?
+			</h2>
+		</div>
+		<div class="weui-msg__text-area" v-if="view == 'err'">
+			<h2 class="weui-msg__title">
+				登录失败
+			</h2>
+		</div>
+		<div class="weui-msg__opr-area" v-if="view == 'login'">
+			<p class="weui-btn-area">
+				<a href="javascript:;" class="weui-btn weui-btn_primary" v-bind:class="{ 'weui-btn_disabled': loading }"
+					v-on:click="login">${ loading ? '正在登录...' : '确定' }</a> 
+				<a href="javascript:;" class="weui-btn weui-btn_default" v-show="!loading" v-on:click="close">取消</a>
+			</p>
+		</div>
+	</div>
+	<script type="text/javascript" th:inline="javascript">
+    const openid = '{{ openid | safe }}'
+    const uuid = '{{ uuid | safe }}'
+		var app = new Vue({
+			delimiters: ['${', '}'],
+			el : '#app',
+			data : {
+				loading : false,
+				view: 'login',
+			},
+			methods : {
+				login : function() {
+					if(this.loading) return;
+					this.loading = true;
+					// 验证二维码是否失效 用户是否失效 生成token
+					$.get('/api/check', { uuid, openid })
+					.then(function(result) {
+						if (result.errcode !== 0) {
+							return $.Deferred().reject(result.errmsg); 
+						} else {
+							// 下发消息通知
+							$.get('/api/mqtt/login')
+							.then(function(res) {
+								if (res.errcode == 0) {
+									app.view = 'success';
+								} else {
+									return $.Deferred().reject(res.errmsg);
+								}
+							}).fail(function( jqXHR, textStatus, errorThrown ) {
+								var msg = "处理失败,请稍后重试!";
+								if(typeof jqXHR == "string") msg = jqXHR;
+								app.view = 'err';
+								app.message = msg;
+							}).always(function(){
+								app.loading = false;
+							});
+						}
+					}).fail(function( jqXHR, textStatus, errorThrown ) {
+						var msg = "处理失败,请稍后重试!";
+						if(typeof jqXHR == "string") msg = jqXHR;
+						app.view = 'err';
+						app.message = msg;
+						return false;
+					}).always(function(){
+						app.loading = false;
+					});
+			},
+				close: function() {
+					wx.closeWindow();
+				}
+			}
+		});
+	</script>
+
+</body>
+
+</html>

+ 14 - 0
appveyor.yml

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

+ 112 - 0
config/config.default.js

@@ -0,0 +1,112 @@
+'use strict';
+
+const ErrorConfig = require('./config.error.js');
+
+module.exports = appInfo => {
+  const config = exports = {};
+
+  // use for cookie sign key, should change to your own and keep security
+  config.keys = appInfo.name + '_1512517259953_9547';
+
+  // add your config here
+  config.middleware = [];
+
+  // 安全配置
+  config.security = {
+    csrf: {
+      // ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
+      enable: false,
+    },
+  };
+  config.wxapi = {
+    // 微信公众号APPID
+    appid: 'wx3ce04cdc39e157c7',
+    // 微信网关地址
+    baseUrl: 'http://wx.cc-lotus.info',
+    // 获取微信token地址
+    tokenUrl: 'https://api.weixin.qq.com/cgi-bin/token',
+    // 密钥
+    appsecret: 'ea08dc7bf4a1af94de364037d088d386',
+    // 模板消息请求地址
+    mould: 'https://api.weixin.qq.com/cgi-bin/message/template/send',
+    // 管理员消息模板(派单模板)
+    adminTemplateId: 'ZocrG3JPgbhEzgdJZwh4mkPUZBly9ebmVi1vbqCWrGw',
+    // 用户消息模板
+    userTemplateId: 'uBv5J9ZVroBO5hFf1pMhE-wgqHWsecLgqBRctfHzNog',
+    // 直连商户号
+    mchid: '1615207497',
+    // 证书序列号
+    certid: '40B12C961A549B4A5FE8E6953035011C0DCAEF97',
+    // 私钥开发
+    // wxKey: 'D:/wxcert/apiclient_key.pem',
+    // 发布
+    wxKey: '/home/medical/wxcert/apiclient_key.pem',
+    // 证书
+    // wxcert: 'D:/wxcert/apiclient_cert.pem',
+    v3key: '95a2d13030a711eca32bf1859288b55d',
+  };
+  config.jwt = {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: '2h',
+    issuer: 'platform',
+  };
+
+  config.onerror = ErrorConfig;
+
+  // server config
+  config.cluster = {
+    listen: {
+      port: 8001,
+    },
+  };
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27018/jzyl',
+    options: {
+      user: 'root',
+      pass: 'cms@cc-lotus',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+
+  // mq config
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'smart',
+      password: 'smart123',
+      vhost: 'smart',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: null,
+      db: 0,
+    },
+  };
+
+  config.view = {
+    defaultViewEngine: 'nunjucks',
+    mapping: {
+      '.njk': 'nunjucks',
+    },
+  };
+
+  // 加载 errorHandler 中间件
+  // config.middleware = [ 'errorHandler' ];
+  // 只对 /api 前缀的 url 路径生效
+  // config.errorHandler = {
+  //   match: '/api',
+  // };
+
+  return config;
+};

+ 31 - 0
config/config.error.js

@@ -0,0 +1,31 @@
+'use strict';
+
+const { NafError, BusinessError } = require('naf-core').Error;
+const { ValidationError } = require('mongoose').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 ValidationError) {
+      // 参数错误
+      ctx.body = { errcode: 400, errmsg: '参数错误', details: err.message };
+      ctx.status = 400;
+    } else if (err instanceof Error) {
+      // 其他错误
+      ctx.body = { errcode: 500, errmsg: '系统错误', details: err.message };
+      ctx.status = 500;
+    } else {
+      // 未知错误
+      ctx.body = { errcode: 500, errmsg: '未知错误', details: err };
+      ctx.status = 500;
+    }
+  },
+};

+ 38 - 0
config/config.local.js

@@ -0,0 +1,38 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27017/jzyl',
+    options: {},
+    // url: 'mongodb://192.168.1.170:27018/naf',
+  };
+
+  // mq config
+  config.amqp = {
+    client: {
+      // hostname: '127.0.0.1',
+      hostname: '192.168.0.45',
+    },
+  };
+
+  // redis config
+  // config.redis = {
+  //   client: {
+  //     host: '127.0.0.1', // Redis host
+  //     // host: '192.168.1.170', // Redis host
+  //   },
+  // };
+
+  config.logger = {
+    consoleLevel: 'DEBUG',
+  };
+
+  config.jwt = {
+    expiresIn: '1d',
+  };
+
+  return config;
+};

+ 18 - 0
config/config.prod.js

@@ -0,0 +1,18 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+
+  // mq config
+  config.amqp = {
+    client: {
+      hostname: '192.168.1.190',
+    },
+  };
+
+  config.logger = {
+    consoleLevel: 'INFO',
+  };
+
+  return config;
+};

+ 22 - 0
config/plugin.js

@@ -0,0 +1,22 @@
+'use strict';
+
+// had enabled by egg
+// exports.static = true;
+exports.multiTenancy = {
+  enable: true,
+};
+
+exports.amqp = {
+  enable: true,
+  package: 'egg-naf-amqp',
+};
+
+exports.redis = {
+  enable: true,
+  package: 'egg-redis',
+};
+
+exports.nunjucks = {
+  enable: true,
+  package: 'egg-view-nunjucks',
+};

+ 17 - 0
ecosystem.config.js

@@ -0,0 +1,17 @@
+'use strict';
+
+const app = 'yl-service';
+module.exports = {
+  apps: [{
+    name: app, // 应用名称
+    script: './server.js', // 实际启动脚本
+    out: `./logs/${app}.log`,
+    error: `./logs/${app}.err`,
+    watch: [ // 监控变化的目录,一旦变化,自动重启
+      'app', 'config',
+    ],
+    env: {
+      NODE_ENV: process.env.NODE_ENV || 'production', // 环境参数,当前指定为生产环境
+    },
+  }],
+};

+ 55 - 0
package.json

@@ -0,0 +1,55 @@
+{
+  "name": "naf-service",
+  "version": "1.0.0",
+  "description": "naf common service",
+  "private": true,
+  "egg": {
+    "framework": "naf-framework-mongoose"
+  },
+  "dependencies": {
+    "@peculiar/x509": "^1.4.1",
+    "egg-naf-amqp": "0.0.13",
+    "egg-redis": "^2.3.0",
+    "egg-view-nunjucks": "^2.3.0",
+    "jsonwebtoken": "^8.5.1",
+    "jsrsasign": "^10.4.1",
+    "lodash": "^4.17.11",
+    "naf-framework-mongoose": "^0.6.1",
+    "uuid": "^3.3.2"
+  },
+  "devDependencies": {
+    "autod": "^3.0.1",
+    "autod-egg": "^1.1.0",
+    "egg-bin": "^4.12.0",
+    "egg-ci": "^1.11.0",
+    "egg-mock": "^3.22.1",
+    "eslint": "^5.15.3",
+    "eslint-config-egg": "^7.2.0"
+  },
+  "engines": {
+    "node": ">=8.9.0"
+  },
+  "scripts": {
+    "start": "egg-scripts start --daemon --title=naf-user",
+    "stop": "egg-scripts stop  --title=naf-user",
+    "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",
+    "pm2": "pm2 start",
+    "restart": "pm2 restart naf-user"
+  },
+  "ci": {
+    "version": "8"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jilinjobs/naf-service"
+  },
+  "author": "dyg",
+  "license": "MIT"
+}

+ 9 - 0
server.js

@@ -0,0 +1,9 @@
+
+// eslint-disable-next-line strict
+const egg = require('egg');
+
+const workers = Number(process.argv[2] || require('os').cpus().length);
+egg.startCluster({
+  workers,
+  baseDir: __dirname,
+});

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

@@ -0,0 +1,21 @@
+'use strict';
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/controller/home.test.js', () => {
+
+  it('should assert', function* () {
+    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);
+  });
+});

+ 32 - 0
test/http/dept.http

@@ -0,0 +1,32 @@
+###
+# 创建部门
+POST http://localhost:7001/dept/create HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+  "parentid": 20000,
+  "name": "策划中心"
+}
+
+###
+# list
+GET http://localhost:7001/dept/list?recursive=1 HTTP/1.1
+Accept: application/json
+
+###
+# 删除
+GET http://localhost:7001/dept/delete?id=20003 HTTP/1.1
+#GET http://oa.chinahuian.cn/naf/code/mz/fetch?code=1 HTTP/1.1
+Accept: application/json
+
+###
+# 修改部门
+#POST http://localhost:7001/dept/update?id=20000 HTTP/1.1
+POST http://localhost:3000/smart/api/naf/dept/update?id=20000 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+  "name": "测试分部1"
+}