lrf402788946 4 jaren geleden
commit
48fc3df0a4
84 gewijzigde bestanden met toevoegingen van 4030 en 0 verwijderingen
  1. 29 0
      .autod.conf.js
  2. 1 0
      .eslintignore
  3. 3 0
      .eslintrc
  4. 42 0
      .github/workflows/nodejs.yml
  5. 14 0
      .gitignore
  6. 12 0
      .travis.yml
  7. 3 0
      .vscode/settings.json
  8. 33 0
      README.md
  9. 19 0
      app.js
  10. 38 0
      app/controller/.adv.js
  11. 60 0
      app/controller/.article.js
  12. 57 0
      app/controller/.doctor.js
  13. 62 0
      app/controller/.nurse.js
  14. 47 0
      app/controller/.user.js
  15. 13 0
      app/controller/adv.js
  16. 13 0
      app/controller/article.js
  17. 40 0
      app/controller/chat.js
  18. 34 0
      app/controller/doctor.js
  19. 40 0
      app/controller/doctormoney.js
  20. 60 0
      app/controller/group.js
  21. 40 0
      app/controller/groupchat.js
  22. 40 0
      app/controller/grouppatients.js
  23. 16 0
      app/controller/home.js
  24. 25 0
      app/controller/login.js
  25. 27 0
      app/controller/nurse.js
  26. 78 0
      app/controller/patient.js
  27. 49 0
      app/controller/patientemrs.js
  28. 70 0
      app/controller/qrcode.js
  29. 29 0
      app/controller/room.js
  30. 23 0
      app/controller/user.js
  31. 142 0
      app/controller/weixin.js
  32. 67 0
      app/controller/wxpay.js
  33. 24 0
      app/model/adv.js
  34. 27 0
      app/model/article.js
  35. 28 0
      app/model/chat.js
  36. 31 0
      app/model/doctor.js
  37. 24 0
      app/model/doctormoney.js
  38. 29 0
      app/model/group.js
  39. 28 0
      app/model/groupchat.js
  40. 29 0
      app/model/nurse.js
  41. 39 0
      app/model/patient.js
  42. 20 0
      app/model/patientdocs.js
  43. 21 0
      app/model/room.js
  44. 24 0
      app/model/user.js
  45. 29 0
      app/public/demo.html
  46. BIN
      app/public/qrcode_for_wx0a4d3e220354c906.jpg
  47. BIN
      app/public/qrcode_for_wxd2e28415cb866c0b.jpg
  48. 22 0
      app/public/subscribe.html
  49. 20 0
      app/public/weui/weui-prompt.css
  50. 145 0
      app/public/weui/weui-prompt.js
  51. 255 0
      app/public/weui/weui-util.js
  52. 82 0
      app/router.js
  53. 15 0
      app/service/adv.js
  54. 15 0
      app/service/article.js
  55. 145 0
      app/service/chat.js
  56. 59 0
      app/service/doctor.js
  57. 103 0
      app/service/doctormoney.js
  58. 118 0
      app/service/group.js
  59. 120 0
      app/service/groupchat.js
  60. 68 0
      app/service/login.js
  61. 44 0
      app/service/nurse.js
  62. 219 0
      app/service/patient.js
  63. 53 0
      app/service/rabbitmq.js
  64. 55 0
      app/service/room.js
  65. 57 0
      app/service/wechat.js
  66. 144 0
      app/service/weixin.js
  67. 102 0
      app/service/wxpay.js
  68. 21 0
      app/view/error.njk
  69. 21 0
      app/view/info.njk
  70. 88 0
      app/view/login.njk
  71. 34 0
      app/view/redirect.njk
  72. 28 0
      app/view/register.njk
  73. 24 0
      app/view/subscribe.njk
  74. 14 0
      appveyor.yml
  75. 134 0
      config/config.default.js
  76. 61 0
      config/config.local.js
  77. 45 0
      config/config.prod.js
  78. 7 0
      config/config.secret.js
  79. 20 0
      config/plugin.js
  80. 17 0
      ecosystem.config.js
  81. 5 0
      jsconfig.json
  82. 62 0
      package.json
  83. 9 0
      server.js
  84. 20 0
      test/app/controller/home.test.js

+ 29 - 0
.autod.conf.js

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

+ 1 - 0
.eslintignore

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

+ 3 - 0
.eslintrc

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

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

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

+ 14 - 0
.gitignore

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

+ 12 - 0
.travis.yml

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

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "eggHelper.serverPort": 35684
+}

+ 33 - 0
README.md

@@ -0,0 +1,33 @@
+# service-visit
+
+
+
+## QuickStart
+
+<!-- add docs here for user -->
+
+see [egg docs][egg] for more detail.
+
+### Development
+
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### Deploy
+
+```bash
+$ npm start
+$ npm stop
+```
+
+### npm scripts
+
+- Use `npm run lint` to check code style.
+- Use `npm test` to run unit test.
+- Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail.
+
+
+[egg]: https://eggjs.org

+ 19 - 0
app.js

@@ -0,0 +1,19 @@
+'use strict';
+class AppBootHook {
+  constructor(app) {
+    this.app = app;
+  }
+
+  async didReady() {
+    // 应用已经启动完毕
+    // const ctx = await this.app.createAnonymousContext();
+    // 企业入驻申请消息接收事件
+    // await ctx.service.rabbitmq.receiveQueueMsg('chat');
+
+  }
+
+  async serverDidReady() {
+    // 应用已经启动完毕
+  }
+}
+module.exports = AppBootHook;

+ 38 - 0
app/controller/.adv.js

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

+ 60 - 0
app/controller/.article.js

@@ -0,0 +1,60 @@
+module.exports = {
+  create: {
+    requestBody: [
+      "title",
+      "user",
+      "user_id",
+      "origin",
+      "img_url",
+      "brief",
+      "content",
+      "file_url",
+      "create_time",
+    ],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: [
+      "title",
+      "user",
+      "user_id",
+      "origin",
+      "img_url",
+      "brief",
+      "content",
+      "file_url",
+      "create_time",
+    ],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        title: "title",
+        user: "user",
+        user_id: "user_id",
+        "create_time@start": "create_time@start",
+        "create_time@end": "create_time@end",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+};

+ 57 - 0
app/controller/.doctor.js

@@ -0,0 +1,57 @@
+module.exports = {
+  create: {
+    requestBody: [
+      'name',
+      '!mobile',
+      '!passwd',
+      'hosname',
+      'deptname',
+      'title',
+      'post',
+      'content',
+      'openid',
+      'remark'
+    ]
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete'
+  },
+  update: {
+    params: ['!id'],
+    requestBody: [
+      'name',
+      '!mobile',
+      'passwd',
+      'hosname',
+      'deptname',
+      'title',
+      'post',
+      'content',
+      'openid',
+      'remark'
+    ]
+  },
+  show: {
+    parameters: {
+      params: ['!id']
+    },
+    service: 'fetch'
+  },
+  index: {
+    parameters: {
+      query: {
+        name: 'name',
+        mobile: 'mobile',
+        openid: 'openid'
+      }
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true
+    }
+  },
+};

+ 62 - 0
app/controller/.nurse.js

@@ -0,0 +1,62 @@
+module.exports = {
+  create: {
+    query: ['doctorid', 'doctorname'],
+    requestBody: [
+      'name',
+      '!mobile',
+      '!passwd',
+      'hosname',
+      'deptname',
+      'doctorid',
+      'doctorname',
+      'title',
+      'post',
+      'content',
+      'openid',
+      'remark'
+    ]
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete'
+  },
+  update: {
+    params: ['!id'],
+    requestBody: [
+      'name',
+      '!mobile',
+      '!passwd',
+      'hosname',
+      'deptname',
+      'doctorid',
+      'doctorname',
+      'title',
+      'post',
+      'content',
+      'openid',
+      'remark'
+    ]
+  },
+  show: {
+    parameters: {
+      params: ['!id']
+    },
+    service: 'fetch'
+  },
+  index: {
+    parameters: {
+      query: {
+        name: 'name',
+        mobile: 'mobile',
+        openid: 'openid'
+      }
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true
+    }
+  },
+};

+ 47 - 0
app/controller/.user.js

@@ -0,0 +1,47 @@
+module.exports = {
+  create: {
+    requestBody: [
+      'name',
+      '!mobile',
+      '!passwd',
+      'openid',
+      'remark'
+    ]
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete'
+  },
+  update: {
+    params: ['!id'],
+    requestBody: [
+      'name',
+      'mobile',
+      'passwd',
+      'openid',
+      'remark'
+    ]
+  },
+  show: {
+    parameters: {
+      params: ['!id']
+    },
+    service: 'fetch'
+  },
+  index: {
+    parameters: {
+      query: {
+        name: 'name',
+        mobile: 'mobile',
+        openid: 'openid'
+      }
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true
+    }
+  },
+};

+ 13 - 0
app/controller/adv.js

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

+ 13 - 0
app/controller/article.js

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

+ 40 - 0
app/controller/chat.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 私聊管理
+class ChatController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.chat;
+  }
+
+  // 查询列表
+  async index() {
+    let { skip, limit, ...info } = this.ctx.query;
+    if (skip && !_.isNumber(skip)) skip = Number(skip);
+    if (limit && !_.isNumber(limit)) limit = Number(limit);
+    const data = await this.service.query(info, { skip, limit });
+    this.ctx.ok({ ...data });
+  }
+
+  // POST
+  // 添加群
+  async create() {
+    // 如果参数校验未通过,将会抛出一个 status = 422 的异常
+    const res = await this.service.create(this.ctx.request.body);
+    this.ctx.ok({ msg: 'created', data: res });
+  }
+
+  // DELETE /{id}
+  // 删除群信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = ChatController;

+ 34 - 0
app/controller/doctor.js

@@ -0,0 +1,34 @@
+'use strict';
+
+const _ = require('lodash');
+const meta = require('./.doctor.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 医生管理
+class DoctorController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.doctor;
+  }
+
+  async update() {
+    await this.service.update(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ msg: 'accepted' });
+  }
+
+  // 根据openid 取得医生信息
+  async findopenid() {
+    return await this.service.findByOpenid(this.ctx.query);
+  }
+
+  // 更新openid
+  async updateopenid() {
+    await this.service.updateopenid(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ msg: 'accepted' });
+  }
+
+}
+
+module.exports = CrudController(DoctorController, meta);

+ 40 - 0
app/controller/doctormoney.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 打赏管理
+class DoctorMoneyController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.doctormoney;
+  }
+
+  // 查询列表
+  async index() {
+    let { skip, limit, ...info } = this.ctx.query;
+    if (skip && !_.isNumber(skip)) skip = Number(skip);
+    if (limit && !_.isNumber(limit)) limit = Number(limit);
+    const data = await this.service.query(info, { skip, limit });
+    this.ctx.ok({ ...data });
+  }
+
+  // POST
+  // 添加群
+  async create() {
+    // 如果参数校验未通过,将会抛出一个 status = 422 的异常
+    const res = await this.service.create(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ msg: 'created', data: res });
+  }
+
+  // DELETE /{id}
+  // 删除群信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = DoctorMoneyController;

+ 60 - 0
app/controller/group.js

@@ -0,0 +1,60 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 群管理
+class GroupController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.group;
+  }
+
+  // 查询列表
+  async index() {
+    let { skip, limit, ...info } = this.ctx.query;
+    if (skip && !_.isNumber(skip)) skip = Number(skip);
+    if (limit && !_.isNumber(limit)) limit = Number(limit);
+    const res = await this.service.query(info, { skip, limit });
+    this.ctx.ok({ ...res });
+  }
+
+  // POST
+  // 添加群
+  async create() {
+    // 如果参数校验未通过,将会抛出一个 status = 422 的异常
+    const res = await this.service.create(this.ctx.request.body);
+    this.ctx.ok({ msg: 'created', data: res });
+  }
+
+  // GET /{id}
+  // 获得群详情
+  async show() {
+    const res = await this.service.fetch(this.ctx.params);
+    this.ctx.ok({ data: res });
+  }
+
+  // GET /{id}/info
+  // POST /{id}/info
+  // 获得基本信息,修改基本信息
+  async info() {
+    if (this.ctx.request.method === 'POST') {
+      await this.service.updateInfo(this.ctx.params, this.ctx.request.body);
+      this.ctx.ok({ msg: 'accepted' });
+    } else {
+      const res = await this.service.fetch(this.ctx.params, { patients: '+patients' });
+      this.ctx.ok({ data: res && res.emrs });
+    }
+  }
+
+  // DELETE /{id}
+  // 删除群信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = GroupController;

+ 40 - 0
app/controller/groupchat.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 群管理
+class GroupChatController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.groupchat;
+  }
+
+  // 查询列表
+  async index() {
+    let { skip, limit, ...info } = this.ctx.query;
+    if (skip && !_.isNumber(skip)) skip = Number(skip);
+    if (limit && !_.isNumber(limit)) limit = Number(limit);
+    const data = await this.service.query(info, { skip, limit });
+    this.ctx.ok({ ...data });
+  }
+
+  // POST
+  // 添加群
+  async create() {
+    // 如果参数校验未通过,将会抛出一个 status = 422 的异常
+    const res = await this.service.create(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ msg: 'created', data: res });
+  }
+
+  // DELETE /{id}
+  // 删除群信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = GroupChatController;

+ 40 - 0
app/controller/grouppatients.js

@@ -0,0 +1,40 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+// 群患者
+class GroupPatientsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.group;
+  }
+
+  // GET LIST
+  async index() {
+    const data = await this.service.fetchPatient(this.ctx.params);
+    this.ctx.ok({ data });
+  }
+
+  // GET /{id}
+  async fetch() {
+    const { groupid } = this.ctx.params;
+    const data = await this.service.fetchPatient({ id: groupid });
+    this.ctx.ok({ data });
+  }
+
+  // POST /{id}
+  async update() {
+    const { groupid, id } = this.ctx.params;
+    await this.service.updatePatient({ groupid, id }, this.ctx.request.body);
+    this.ctx.ok({ msg: 'accepted' });
+  }
+
+  // DELETE /{id}
+  async destroy() {
+    const { groupid, id } = this.ctx.params;
+    await this.service.deletePatient({ groupid, id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = GroupPatientsController;

+ 16 - 0
app/controller/home.js

@@ -0,0 +1,16 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+  async index() {
+    const { ctx } = this;
+    const a = { id: '1', name: '123' };
+    const icon = 'jjjj';
+    Object.assign(a, { icon });
+    console.log(a);
+    ctx.body = 'hi, egg';
+  }
+}
+
+module.exports = HomeController;

+ 25 - 0
app/controller/login.js

@@ -0,0 +1,25 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 登录管理
+class LoginController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.login;
+  }
+
+  async login() {
+    const res = await this.service.login(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+
+  async wxlogin() {
+    const res = await this.service.wxlogin(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+}
+
+module.exports = LoginController;

+ 27 - 0
app/controller/nurse.js

@@ -0,0 +1,27 @@
+'use strict';
+
+const _ = require('lodash');
+const meta = require('./.nurse.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 护士管理
+class NurseController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.nurse;
+  }
+
+  async update() {
+    await this.service.update(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ msg: 'accepted' });
+  }
+
+  async login() {
+    const res = await this.service.login(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+}
+
+module.exports = CrudController(NurseController, meta);

+ 78 - 0
app/controller/patient.js

@@ -0,0 +1,78 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 病人管理
+class PatientController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.patient;
+  }
+
+  // 查询列表
+  async index() {
+    let { skip, limit, ...info } = this.ctx.query;
+    if (skip && !_.isNumber(skip)) skip = Number(skip);
+    if (limit && !_.isNumber(limit)) limit = Number(limit);
+    const res = await this.service.query(info, { skip, limit });
+    this.ctx.ok(res);
+  }
+
+  // POST
+  // 注册患者
+  async create() {
+    // 如果参数校验未通过,将会抛出一个 status = 422 的异常
+    const res = await this.service.create(this.ctx.request.body);
+    this.ctx.ok({ msg: 'created', data: res });
+  }
+
+  // GET /{id}
+  // 获得患者详情
+  async show() {
+    const res = await this.service.fetch(this.ctx.params);
+    this.ctx.ok({ data: res });
+  }
+
+  // GET /{id}
+  // 获得患者所有关注的医生列表
+  async doctors() {
+    const res = await this.service.doctors(this.ctx.params);
+    this.ctx.ok({ data: res });
+  }
+
+  // 查询患者所有群信息
+  async groups() {
+    const res = await this.service.groups(this.ctx.params);
+    this.ctx.ok({ data: res });
+  }
+
+  // GET /{id}/info
+  // POST /{id}/info
+  // 获得基本信息,修改基本信息
+  async info() {
+    if (this.ctx.request.method === 'POST') {
+      await this.service.updateInfo(this.ctx.params, this.ctx.request.body);
+      this.ctx.ok({ msg: 'accepted' });
+    } else {
+      const res = await this.service.fetch(this.ctx.params);
+      this.ctx.ok({ data: res && res.emrs });
+    }
+  }
+
+  // DELETE /{id}
+  // 删除患者信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+
+  // 根据openid 取得医生信息
+  async findopenid() {
+    return await this.service.findByOpenid(this.ctx.query);
+  }
+}
+
+module.exports = PatientController;

+ 49 - 0
app/controller/patientemrs.js

@@ -0,0 +1,49 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+// 患者病历
+class PatientEmrsController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.patient;
+  }
+
+  // GET LIST
+  async index() {
+    const { patientid } = this.ctx.params;
+    const res = await this.service.fetch({ id: patientid }, { emrs: '+emrs' });
+    this.ctx.ok({ data: res.emrs });
+  }
+
+  // POST
+  async create() {
+    const { patientid } = this.ctx.params;
+    await this.service.createEmr({ patientid }, this.ctx.request.body);
+    this.ctx.ok({ msg: 'created' });
+  }
+
+  // GET /{id}
+  async fetch() {
+    const { patientid, emrid } = this.ctx.params;
+    const res = await this.service.fetchEmr({ id: patientid, emrid });
+    this.ctx.ok({ data: res });
+  }
+
+  // POST /{id}
+  async update() {
+    const { patientid, id } = this.ctx.params;
+    await this.service.updateEmr({ patientid, id }, this.ctx.request.body);
+    this.ctx.ok({ msg: 'accepted' });
+  }
+
+  // DELETE /{id}
+  async destroy() {
+    const { patientid, id } = this.ctx.params;
+    console.log(patientid);
+    await this.service.deleteEmr({ patientid, id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = PatientEmrsController;

+ 70 - 0
app/controller/qrcode.js

@@ -0,0 +1,70 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+/**
+ * 微信扫码登录
+ */
+class QrcodeController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.weixin;
+  }
+
+  // POST 生成二维码
+  async create() {
+    const res = await this.service.createQrcode();
+    this.ctx.ok({ data: res });
+  }
+
+  // POST 生成群二维码
+  async createQrcode() {
+    console.log(this.ctx.query);
+    const res = await this.service.createQrcodeGroup(this.ctx.query);
+    this.ctx.ok({ data: res });
+  }
+
+  // POST 检查二维码
+  async check() {
+    const { qrcode } = this.ctx.params;
+    const res = await this.service.checkQrcode(qrcode);
+    this.ctx.ok(res);
+  }
+
+  // POST 微信扫码登录
+  async login() {
+    const { token } = this.ctx.requestparam;
+    const { qrcode } = this.ctx.params;
+    await this.service.scanQrcode({ qrcode, token });
+    this.ctx.ok();
+  }
+
+  // GET 微信扫码确认页面
+  async scan() {
+    // TODO: 获得微信认证token
+    const token = this.ctx.query.token || this.ctx.cookies.get('wxtoken');
+    if (!token) {
+      this.ctx.logger.debug('【originalUrl】', this.ctx.originalUrl);
+      // TODO: 跳转到授权地址
+      const { baseUrl } = this.app.config;
+      const { authUrl = this.ctx.path } = this.app.config;
+      const backUrl = encodeURI(`${baseUrl}${this.ctx.originalUrl}`);
+      const to_uri = `${authUrl}?response_type=token&redirect_uri=${backUrl}#wechat`;
+
+      this.ctx.redirect(to_uri);
+      return;
+    }
+
+    await this.ctx.render('login.njk', { message: '扫码登录确认', token });
+  }
+
+  // POST 换取微信认证token
+  async token() {
+    const { qrcode } = this.ctx.params;
+    const res = await this.service.qrcodeLogin(qrcode);
+    this.ctx.ok(res);
+  }
+}
+
+module.exports = QrcodeController;

+ 29 - 0
app/controller/room.js

@@ -0,0 +1,29 @@
+'use strict';
+
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+// 房间管理
+class RoomController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.room;
+  }
+
+  // 查询列表
+  async index() {
+    const data = await this.service.query(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+
+  // DELETE /{id}
+  // 删除房间信息
+  async destroy() {
+    const { id } = this.ctx.params;
+    await this.service.delete({ id });
+    this.ctx.ok({ msg: 'deleted' });
+  }
+}
+
+module.exports = RoomController;

+ 23 - 0
app/controller/user.js

@@ -0,0 +1,23 @@
+'use strict';
+
+const _ = require('lodash');
+const meta = require('./.user.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 管理
+class UserController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.user;
+  }
+
+  async login() {
+    const res = await this.service.login(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+
+}
+
+module.exports = CrudController(UserController, meta);

+ 142 - 0
app/controller/weixin.js

@@ -0,0 +1,142 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const uuid = require('uuid');
+const urljoin = require('url-join');
+const Controller = require('egg').Controller;
+
+/**
+ * 微信认证,获得openid和用户信息,生成微信Jwt
+ */
+class WeixinController extends Controller {
+  /**
+   * 认证流程
+   * 1. 缓存原始请求地址,生成state和认证回调地址
+   * 2. 通过wxapi认证,获得认证code
+   * 3. 通过code获得openid,通过openid,查询绑定用户,创建jwt
+   * 4. jwt写入redis,返回认证code
+   * 5. 通过code获取jwt
+   */
+  // GET 请求认证
+  // response_type:
+  //       code - url带上code参数重定向到原始地址
+  //       store - 默认,认证结果写入sessionStore,然后重定向回请求页面(要求请求页面和认证服务在同一域名下)
+  //       token - url带上token参数重定向到原始地址
+  async auth() {
+    const { redirect_uri, code, test, type, response_type = 'store', groupid, doctorid } = this.ctx.query;
+    if (test) {
+      return await this.authTest();
+    }
+    if (code) {
+      return await this.authBack(this.ctx.query);
+    }
+
+    this.ctx.logger.debug(`[auth] reditect_uri - ${redirect_uri}`);
+    // assert(redirect_uri, '回调地址不能为空');
+
+    // TODO: 保存原始请求地址
+    // const { config } = this.app;
+    // console.log('ctx.host: ', this.ctx.host);
+    // console.log('config.hostHeaders: ', config.hostHeaders);
+    // TODO: 保存原始请求地址
+    const state = uuid();
+    const key = `visit:auth:state:${state}`;
+    const val = JSON.stringify({ redirect_uri, type, groupid, doctorid });
+    await this.app.redis.set(key, val, 'EX', 600);
+
+    // TODO: 生成回调地址
+    const { wxapi, authUrl = this.ctx.path } = this.app.config;
+    const backUrl = encodeURI(`${this.app.config.baseUrl}${this.config.authUrl}?state=${state}`);
+    // const to_uri = `${wxapi.baseUrl}/api/auth?appid=${wxapi.appid}&response_type=code&redirect_uri=${backUrl}&connect_redirect=1#wechat`;
+    // console.log('url-->' + to_uri);
+    // this.ctx.redirect(to_uri);
+    const to_uri = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxapi.appid}&response_type=code&scope=snsapi_base&redirect_uri=${backUrl}&state=${state}&connect_redirect=1#wechat_redirect`;
+    console.log('url-->' + to_uri);
+    this.ctx.redirect(to_uri);
+  }
+
+  // GET 认证回调
+  async authBack({ code, state }) {
+    // const { code, state, type, redirecturi } = this.ctx.query;
+    console.log(this.ctx.query);
+    this.ctx.logger.debug(`[auth-back] code - ${code}, state - ${state}`);
+    assert(code, 'code不能为空');
+    assert(state, 'state不能为空');
+    console.log('code-->' + code);
+
+    const { weixin } = this.ctx.service;
+    let openid;
+    try {
+      ({ openid } = await weixin.fetch(code));
+    } catch (err) {
+      await this.ctx.render('error.njk', { title: err.message, message: err.details });
+      return;
+    }
+    console.log('openid--->' + openid);
+    if (openid) {
+      const key = `visit:auth:state:${state}`;
+      const val = await this.app.redis.get(key);
+      const { redirect_uri, type, groupid, doctorid } = JSON.parse(val);
+      console.log('type-->' + type);
+      if (type === 'group') {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&groupid=${groupid}`);
+        // TODO: 重定性页面
+        console.log('to_uri222-->' + to_uri);
+        this.ctx.redirect(to_uri);
+      } else if (type === 'login') {
+        console.log('to_uri333-->' + redirect_uri);
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+        console.log('to_uri333-->' + to_uri);
+        // TODO: 重定性页面
+        this.ctx.redirect(to_uri);
+      } else if (type === 'doctor') {
+        console.log('to_uridoctor-->' + redirect_uri);
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&doctorid=${doctorid}`);
+        // TODO: 重定性页面
+        this.ctx.redirect(to_uri);
+      } else {
+        console.log('rrr--->' + redirect_uri);
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+
+        // TODO: 重定性页面
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+
+  // GET 用户授权内部测试接口
+  async authTest() {
+    const { redirect_uri, type, groupid, doctorid } = this.ctx.query;
+    const openid = '1234567';
+    this.ctx.logger.debug(`[auth-test] reditect_uri - ${redirect_uri}, openid - ${openid}`);
+    assert(redirect_uri, '回调地址不能为空');
+    assert(openid, 'openid不能为空');
+
+    if (type === 'group') {
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}&groupid=${groupid}`);
+      // TODO: 重定性页面
+      console.log('to_uri222-->' + to_uri);
+      this.ctx.redirect(to_uri);
+    } else if (type === 'login') {
+      console.log('to_uri333-->' + redirect_uri);
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+      console.log('to_uri333-->' + to_uri);
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    } else if (type === 'doctor') {
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}&doctorid=${doctorid}`);
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    } else {
+      console.log('rrr--->' + redirect_uri);
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    }
+  }
+}
+
+module.exports = WeixinController;

+ 67 - 0
app/controller/wxpay.js

@@ -0,0 +1,67 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const Controller = require('egg').Controller;
+
+/**
+ * 微信支付
+ */
+class WxPayController extends Controller {
+  /**
+   * 微信支付取得sign等参数
+   */
+  async wxpaysign() {
+    const { orderno, money, content, openid } = this.ctx.request.body;
+    assert(orderno, 'orderno不能为空');
+    assert(openid, 'openid不能为空');
+    assert(money, 'money不能为空');
+    const { wxapi } = this.app.config;
+    const mch_id = wxapi.mchid;
+    const nonce_str = await this.service.wxpay.createNonceStr();
+    const timestamp = await this.service.wxpay.createTimeStamp();
+    const body = content;
+    const out_trade_no = orderno;
+    const total_fee = await this.service.wxpay.getmoney(money);
+    const spbill_create_ip = this.ctx.ip;
+    const notify_url = wxapi.wxurl;
+    const trade_type = 'JSAPI';
+    const sign = await this.service.wxpay.paysignjsapi(wxapi.appid, body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, trade_type, wxapi.mchkey, openid);
+    console.log('sign==', sign);
+    let formData = '<xml>';
+    formData += '<appid>' + wxapi.appid + '</appid>'; // appid
+    formData += '<body><![CDATA[' + body + ']]></body>';
+    formData += '<mch_id>' + mch_id + '</mch_id>'; // 商户号
+    formData += '<nonce_str>' + nonce_str + '</nonce_str>'; // 随机字符串,不长于32位。
+    formData += '<notify_url>' + notify_url + '</notify_url>';
+    formData += '<out_trade_no>' + out_trade_no + '</out_trade_no>';
+    formData += '<spbill_create_ip>' + spbill_create_ip + '</spbill_create_ip>';
+    formData += '<total_fee>' + total_fee + '</total_fee>';
+    formData += '<trade_type>' + trade_type + '</trade_type>';
+    formData += '<sign>' + sign + '</sign>';
+    formData += '<openid>' + openid + '</openid>';
+    formData += '</xml>';
+    console.log('formData===', formData);
+
+    const payurl = wxapi.payurl;
+    const paydata = await this.ctx.curl(payurl, {
+      method: 'post',
+      data: formData,
+    });
+    const prepay_id = await this.service.wxpay.getXMLNodeValue(paydata.data.toString('UTF-8'));
+    console.log('解析后的prepay_id==', prepay_id);
+    // 将预支付订单和其他信息一起签名后返回给前端
+    const finalsign = await this.service.wxpay.paysignjsapifinal(wxapi.appid, mch_id, prepay_id, nonce_str, timestamp, wxapi.mchkey);
+    const res = { appId: wxapi.appid, partnerId: wxapi.mchid, prepayId: prepay_id, nonceStr: nonce_str, timeStamp: timestamp, package: 'prepay_id=' + prepay_id, sign: finalsign };
+    this.ctx.ok({ data: res });
+  }
+
+  async wxpaysignback() {
+    const { ctx } = this;
+    console.log();
+    ctx.body = 'hi, egg';
+  }
+
+}
+
+module.exports = WxPayController;

+ 24 - 0
app/model/adv.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+// 广告表
+const adv = {
+  pos: { type: String }, // 位置:index=>首页;doctor=>医生介绍
+  image: { type: String }, // 图片
+  url: { type: String }, // 跳转位置
+  doctor_id: { type: String }, // 医生id,
+  remark: { type: String, maxLength: 200 },
+  create_time: {
+    type: String,
+    default: moment().format('YYYY-MM-DD HH:mm:ss'),
+  },
+};
+const schema = new Schema(adv, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ pos: 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Adv', schema, 'adv');
+};

+ 27 - 0
app/model/article.js

@@ -0,0 +1,27 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+// 文章表
+const article = {
+  title: { type: String }, // 标题
+  user: { type: String }, // 医生
+  user_id: { type: String }, // 医生id
+  origin: { type: String }, // 来源
+  img_url: { type: String }, // 图片
+  brief: { type: String }, // 简介
+  content: { type: String }, // 内容
+  file_url: { type: String }, // 附件
+  remark: { type: String, maxLength: 200 },
+  create_time: { type: String, default: moment().format('YYYY-MM-DD HH:mm:ss') },
+};
+const schema = new Schema(article, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ title: 1 });
+schema.index({ user: 1 });
+schema.index({ user_id: 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Article', schema, 'article');
+};

+ 28 - 0
app/model/chat.js

@@ -0,0 +1,28 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const moment = require('moment');
+
+// 群聊信息表
+const ChatSchema = {
+  type: { type: String, required: false, maxLength: 200 }, // 类别0、医生1、患者
+  sendid: { type: String, required: false, maxLength: 200 }, // 发送人ID
+  sendname: { type: String, required: false, maxLength: 200 }, // 发送人姓名
+  receiveid: { type: String, required: false, maxLength: 200 }, // 收件人ID
+  receivename: { type: String, required: false, maxLength: 200 }, // 收件人姓名
+  sendtime: { type: String, required: false, default: moment().format('YYYY-MM-DD HH:mm:ss') }, // 发送时间
+  contenttype: { type: String, required: false, maxLength: 20 }, // 类别0、图片1、音频、2、视频、3、文字
+  audiotime: { type: String, required: false, maxLength: 100 }, // 语音时长
+  content: { type: String, required: false }, // 内容
+  status: { type: String, required: false }, // 状态0、未读1、已读
+};
+
+const schema = new Schema(ChatSchema, { toJSON: { virtuals: true } });
+schema.index({ sendid: 1 });
+schema.index({ receiveid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Chat', schema, 'chat');
+};

+ 31 - 0
app/model/doctor.js

@@ -0,0 +1,31 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+
+// 医生表
+const DoctorSchema = {
+  name: { type: String, required: false, maxLength: 200 }, // 名称
+  icon: { type: String, required: false, maxLength: 500 }, // 头像
+  mobile: { type: String, required: true, maxLength: 64 }, // 手机
+  passwd: { type: Secret, select: false }, // 注册密码
+  hosname: { type: String, required: false, maxLength: 200 }, // 医院名称
+  deptname: { type: String, required: false, maxLength: 200 }, // 科室名称
+  title: { type: String, required: false, maxLength: 200 }, // 职称
+  post: { type: String, required: false, maxLength: 200 }, // 职务
+  content: { type: String, required: false }, // 简介
+  openid: { type: String, required: false }, // 微信openid
+  money: { type: String, required: false, maxLength: 200 }, // 总金额
+  balance: { type: String, required: false, maxLength: 200 }, // 剩余金额
+  remark: { type: String, required: false }, // 备注
+};
+
+
+const schema = new Schema(DoctorSchema, { toJSON: { virtuals: true } });
+schema.index({ openid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Doctor', schema, 'doctor');
+};

+ 24 - 0
app/model/doctormoney.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 打赏信息表
+const DoctormoneySchema = {
+  doctorid: { type: String, required: true, maxLength: 200 }, // 医生id
+  doctorname: { type: String, required: true, maxLength: 500 }, // 医生名称
+  patientid: { type: String, required: true, maxLength: 200 }, // 患者ID
+  patientname: { type: String, required: true, maxLength: 200 }, // 患者名称
+  sendtime: { type: String, required: false }, // 发送时间
+  money: { type: String, required: false, maxLength: 200 }, // 打赏金额
+  orderno: { type: String, required: false, maxLength: 200 }, // 订单号
+  content: { type: String, required: false }, // 内容
+};
+
+const schema = new Schema(DoctormoneySchema, { toJSON: { virtuals: true } });
+schema.index({ doctorid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Doctormoney', schema, 'doctormoney');
+};

+ 29 - 0
app/model/group.js

@@ -0,0 +1,29 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 群内成员信息
+const Patientinfo = new Schema({
+  patientid: { type: String, required: false, maxLength: 20 }, // 病人ID
+  url: { type: String, required: false }, // 头像url
+  name: { type: String, required: false }, // 病人名称
+  openid: { type: String, required: false }, // 微信openid
+});
+Patientinfo.index({ patientid: 1 });
+
+// 群信息表
+const GroupSchema = {
+  doctorid: { type: String, required: true, maxLength: 500 }, // 医生id列表
+  name: { type: String, required: false, maxLength: 200 }, // 群名称
+  content: { type: String, required: false }, // 群简介
+  patients: { type: [ Patientinfo ], select: false }, // 群里人员信息
+};
+
+const schema = new Schema(GroupSchema, { toJSON: { virtuals: true } });
+schema.index({ doctorid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Group', schema, 'group');
+};

+ 28 - 0
app/model/groupchat.js

@@ -0,0 +1,28 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 群聊信息表
+const GroupchatSchema = {
+  doctorid: { type: String, required: true, maxLength: 500 }, // 医生id
+  doctorname: { type: String, required: true, maxLength: 500 }, // 医生名称
+  groupid: { type: String, required: true, maxLength: 50 }, // 群id
+  groupname: { type: String, required: true, maxLength: 50 }, // 群名称
+  type: { type: String, required: false, maxLength: 200 }, // 类别0、医生1、患者
+  sendid: { type: String, required: false, maxLength: 200 }, // 发送人ID
+  sendname: { type: String, required: false, maxLength: 200 }, // 发送人姓名
+  sendtime: { type: String, required: false }, // 发送时间
+  contenttype: { type: String, required: false, maxLength: 20 }, // 类别0、图片1、音频、2、视频、3、文字
+  content: { type: String, required: false }, // 内容
+  audiotime: { type: String, required: false, maxLength: 100 }, // 语音时长
+};
+
+const schema = new Schema(GroupchatSchema, { toJSON: { virtuals: true } });
+schema.index({ groupid: 1 });
+schema.index({ sendid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Groupchat', schema, 'group_chat');
+};

+ 29 - 0
app/model/nurse.js

@@ -0,0 +1,29 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+
+// 护士表
+const NurseSchema = {
+  name: { type: String, required: false, maxLength: 200 }, // 名称
+  mobile: { type: String, required: true, maxLength: 64 }, // 手机
+  passwd: { type: Secret, select: false }, // 注册密码
+  hosname: { type: String, required: false, maxLength: 200 }, // 医院名称
+  deptname: { type: String, required: false, maxLength: 200 }, // 科室名称
+  doctorid: { type: String, required: true, maxLength: 64 }, // 医生id
+  doctorname: { type: String, required: true, maxLength: 200 }, // 医生名称
+  title: { type: String, required: false, maxLength: 200 }, // 职称
+  post: { type: String, required: false, maxLength: 200 }, // 职务
+  content: { type: String, required: false }, // 简介
+  openid: { type: String, required: false }, // 微信openid
+  remark: { type: String, required: false }, // 备注
+};
+
+const schema = new Schema(NurseSchema, { toJSON: { virtuals: true } });
+schema.index({ openid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Nurse', schema, 'nurse');
+};

+ 39 - 0
app/model/patient.js

@@ -0,0 +1,39 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 病历信息
+const Emrinfo = new Schema({
+  indate: { type: String, required: false, maxLength: 20 }, // 入院时间
+  outdate: { type: String, required: false, maxLength: 20 }, // 出院时间
+  doctorid: { type: String, required: false }, // 医生ID
+  doctorname: { type: String, required: false, maxLength: 200 }, // 医生名称
+  title: { type: String, required: false }, // 标题
+  content: { type: String, required: false }, // 病历内容
+});
+Emrinfo.index({ mobile: 1 });
+
+// 患者信息表
+const PatientSchema = {
+  name: { type: String, required: false, maxLength: 200 }, // 名称
+  icon: { type: String, required: false, maxLength: 500 }, // 头像
+  cardno: { type: String, required: false, maxLength: 200 }, // 就诊卡号
+  gender: { type: String, required: false, maxLength: 200 }, // 性别
+  birthday: { type: String, required: false }, // 生日
+  address: { type: String, required: false, maxLength: 500 }, // 地址
+  tel: { type: String, required: false, maxLength: 200 }, // 电话
+  urgentname: { type: String, required: false, maxLength: 200 }, // 紧急联系人
+  urgenttel: { type: String, required: false, maxLength: 200 }, // 紧急联系电话
+  content: { type: String, required: false }, // 简介
+  openid: { type: String, required: false }, // 微信openid
+  emrs: { type: [ Emrinfo ], select: true }, // 病人病历信息
+};
+
+const schema = new Schema(PatientSchema, { toJSON: { virtuals: true } });
+schema.index({ openid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Patient', schema, 'patient');
+};

+ 20 - 0
app/model/patientdocs.js

@@ -0,0 +1,20 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 患者入驻医生信息
+const PatientdocsSchema = {
+  patientid: { type: String, required: true, maxLength: 64 }, // 患者ID
+  patientname: { type: String, required: true, maxLength: 128 }, // 患者名称
+  doctorid: { type: String, required: true, maxLength: 64 }, // 医生ID
+  doctorname: { type: String, required: false, maxLength: 128 }, // 医生名称
+};
+const schema = new Schema(PatientdocsSchema, { toJSON: { virtuals: true } });
+schema.index({ patientid: 1 });
+schema.index({ doctorid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('PatientDoctor', schema, 'patient_doctor');
+};

+ 21 - 0
app/model/room.js

@@ -0,0 +1,21 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 群聊信息表
+const RoomSchema = {
+  patientid: { type: String, required: false, maxLength: 200 }, // 患者ID
+  patientname: { type: String, required: false, maxLength: 200 }, // 患者姓名
+  doctorid: { type: String, required: false, maxLength: 200 }, // 医生ID
+  doctorname: { type: String, required: false, maxLength: 200 }, // 医生姓名
+};
+
+const schema = new Schema(RoomSchema, { toJSON: { virtuals: true } });
+schema.index({ patientid: 1 });
+schema.index({ doctorid: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Room', schema, 'room');
+};

+ 24 - 0
app/model/user.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+
+// 用户表
+const UserSchema = {
+  name: { type: String, required: false, maxLength: 200 }, // 名称
+  mobile: { type: String, required: true, maxLength: 64 }, // 手机
+  passwd: { type: Secret, select: false }, // 注册密码
+  openid: { type: String, required: false }, // 微信openid
+  remark: { type: String, required: false }, // 备注
+};
+
+
+const schema = new Schema(UserSchema, { toJSON: { virtuals: true } });
+schema.index({ openid: 1 });
+schema.index({ mobile: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('User', schema, 'user');
+};

+ 29 - 0
app/public/demo.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+
+<head>
+  <title>Demo页面</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>
+  <script type="text/javascript" src="http://cdnjs.gtimg.com/cdnjs/libs/zepto/1.1.4/zepto.min.js"></script>
+</head>
+
+<body>
+  <div class="weui-msg">
+    <div class="weui-msg__icon-area"><i class="weui-icon-info weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">用户信息</h2>
+      <p class="weui-msg__desc"></p>
+    </div>
+  </div>
+  <script>
+    $(function() {
+      var user = sessionStorage.getItem('nickname');
+      console.log('session user:', user);
+      $('.weui-msg__desc').text(user);
+    });
+  </script>
+</body>
+
+</html>

BIN
app/public/qrcode_for_wx0a4d3e220354c906.jpg


BIN
app/public/qrcode_for_wxd2e28415cb866c0b.jpg


+ 22 - 0
app/public/subscribe.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+
+<head>
+  <title>未关注公众号</title>
+  <meta charset="utf-8"></meta>
+  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"></meta>
+  <link rel="stylesheet" type="text/css" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"></link>
+</head>
+
+<body>
+  <div class="weui-msg">
+    <div class="weui-msg__icon-area"><i class="weui-icon-info weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">未关注公众号</h2>
+      <p class="weui-msg__desc">请识别二维码关注公众号</p>
+      <img src="qrcode_for_gh_cc6425d359a6_344.jpg" width="70%">
+    </div>
+  </div>
+</body>
+
+</html>

+ 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;
+}

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

@@ -0,0 +1,145 @@
+//输入验证码
+// 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) {
+	var 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;
+		} else {
+			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
+		}
+	}
+
+	var 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>'
+	}
+
+	var 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;
+	}
+
+	var dlgOpts = {
+		title: options.title,
+		content: content,
+		buttons: [{
+			label: options.cancelText || '取消',
+			type: 'default',
+			onClick: function () {
+				if (options.onCancel && (options.onCancel.call(dlg) == false))
+					return false;
+
+				countdown = 0;
+			}
+		}, {
+			label: options.okText || '确定',
+			type: 'primary',
+			onClick: function () {
+				var val = {};
+				var input;
+				//TODO: 检查手机号
+				//134,135,136,137,138,139,147,150,151,152,157,158,159,178,182,183,184,187,188
+				var 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;
+					else
+						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;
+				} else
+					val.verifyCode = input.val();
+
+				//TODO: 检查图片验证码
+				if (options.verifyImg) {
+					if (!checkInput(input = $(dlg).find("#weui-prompt-verifyimg"), '请输入有效的图片验证码', /^[A-z0-9]{4}$/))
+						return false;
+					else
+						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);
+
+	var btn = $(dlg).find('#weui-prompt-button');
+	btn.click(function () {
+		var phone, input;
+		//TODO: 检查手机号
+		var 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;
+			} else
+				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);
+	});
+
+}

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

@@ -0,0 +1,255 @@
+	var 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: 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: title, isAndroid: false });
+	}
+	function onConfirm(res){
+		if(res)
+			alert('click yes');
+		else
+			alert('click no');
+	}
+	function showCustom1(message,title){
+		promptVC({
+			text: message,
+			title: title,
+			onOK: function(val){
+				//val格式: {verifyCode: 'xxxxxx'}
+				console.log(val)
+				alert(JSON.stringify(val))
+			},
+			onSend: function(){
+				showToast("验证码已发送")
+			},
+			isAndroid: false 	
+		});
+	}
+	function showCustom2(message,title){
+		promptVC({
+			text: message,
+			title: title,
+			inputPhone: true,
+			onOK: function(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: function(val){
+				if(/1[3-8]\d{9}/.test(val))
+					showToast("验证码已发送")
+				else{
+					weui.topTips('手机号无效');
+					return false;
+				}
+			}
+		});
+	}
+	function promptOrder(message, pkgName, pkgCode, okText, cancelText, onSubmit, onCancel){
+		var imgUrl = 'verifyImage?timestamp=' + new Date().getTime();
+		promptVC({
+			text: message,
+			title: pkgName,
+			inputPhone: true,
+			okText: okText || "办理",
+			cancelText: cancelText || "关闭",
+			verifyImg: imgUrl,
+			isAndroid: false,
+			onOK: function(val){
+				//val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx', verifyImg: 'xxxx'}
+				console.log(val)
+				if(onSubmit)
+					onSubmit(val.phone, val.verifyCode, val.verifyImg, pkgCode, pkgName);
+			},
+			onCancel: onCancel,
+			onSend: function(phone){
+				requestVerify(phone);
+			}
+		});
+		$('img.weui-prompt-button').click(function(){
+			$(this).attr('src','verifyImage?timestamp=' + new Date().getTime());
+		});
+	}
+
+	function promptAction(message, title, okText, cancelText, onSubmit){
+		var imgUrl = 'verifyImage?timestamp=' + new Date().getTime();
+		var 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: function(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: function() {
+				if(opts.closable === false)
+					return false;
+			},
+			onSend: function(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 || {};
+		var params = {};
+		params[opts.param || 'phoneNo'] = phone;
+		$.ajax({
+			type : "post",
+			url : opts.url || 'verifyCode',
+			data : params,
+			dataType : 'json',
+			success : function(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){
+		var 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;
+		}
+		var 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: function(val){
+				//val格式: {phone: '13xxxxxxxx', verifyCode: 'xxxxxx', verifyImg: 'xxxx'}
+				console.log(val)
+                var 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 ) {
+                    var msg = "请求发送失败";
+                    if(typeof jqXHR == "string")
+                        msg = jqXHR;
+                    weui.topTips(msg);
+                }).always(function(){
+                    hideLoading();
+                });
+			},
+			onCancel: function() {
+				if(opts.closable === false)
+					return false;
+			},
+			onSend: function(phone){
+				requestVerify(phone, opts.verifyCode);
+			}
+		});
+		$('img.weui-prompt-button').click(function(){
+			$(this).attr('src','verifyImage?timestamp=' + new Date().getTime());
+		});
+	}
+

+ 82 - 0
app/router.js

@@ -0,0 +1,82 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  router.get('/', controller.home.index);
+  router.post('/api/visit/login', controller.login.login); // 用户登录
+  router.post('/api/visit/wxlogin', controller.login.wxlogin); // 微信用户登录
+  router.resources('user', '/api/visit/user', controller.user); // index、create、show、destroy
+
+  router.resources('doctor', '/api/visit/doctor', controller.doctor); // index、create、show、destroy
+  router.post('doctor', '/api/visit/doctor/:id', controller.doctor.update);
+  router.get('doctor', '/api/visit/doctor/findopenid', controller.doctor.findopenid);
+  router.post('doctor', '/api/visit/doctor/:id/bind', controller.doctor.updateopenid);
+
+  router.resources('nurse', '/api/visit/nurse', controller.nurse); // index、create、show、destroy
+  router.post('nurse', '/api/visit/nurse/:id', controller.nurse.update);
+  router.post('nurse', '/api/visit/nurse/login', controller.nurse.login);
+  router.resources('group', '/api/visit/group', controller.group); // index、create、show、destroy
+  router.get('/api/visit/group/:id/info', controller.group.info);
+  router.post('/api/visit/group/:id/info', controller.group.info);
+  // 群里患者接口
+  router.resources('grouppatients', '/api/visit/grouppatients', controller.grouppatients); // index、create、show
+  router.get('/api/visit/grouppatients/:groupid/patients', controller.grouppatients.index); // index
+  router.get('/api/visit/grouppatients/:groupid/patients/fetch', controller.grouppatients.fetch); // index
+  router.post('/api/visit/grouppatients/:groupid/update/:id', controller.grouppatients.update); // update
+  router.post('/api/visit/grouppatients/:groupid/destroy/:id', controller.grouppatients.destroy); // destroy
+
+  router.resources('patient', '/api/visit/patient', controller.patient); // index、create、show、destroy
+  router.get('/api/visit/patient/:id/info', controller.patient.info);
+  router.get('/api/visit/patient/:id/doctors', controller.patient.doctors);
+  router.get('/api/visit/patient/:id/groups', controller.patient.groups);
+  router.post('/api/visit/patient/:id/info', controller.patient.info);
+  router.get('patient', '/api/visit/patient/findopenid', controller.patient.findopenid);
+  // 患者病历接口
+  router.resources('patientemrs', '/api/visit/patientemrs', controller.patientemrs); // index、create、show
+  router.get('/api/visit/patientemrs/:patientid/emrs', controller.patientemrs.index); // index
+  router.get('/api/visit/patientemrs/:patientid/emrs/fetch/:emrid', controller.patientemrs.fetch); // index
+  router.post('/api/visit/patientemrs/:patientid/create', controller.patientemrs.create); // update
+  router.post('/api/visit/patientemrs/:patientid/update/:id', controller.patientemrs.update); // update
+  router.delete('/api/visit/patientemrs/:patientid/destroy/:id', controller.patientemrs.destroy); // destroy
+
+  router.get('/api/visit/rooms', controller.room.index); // index
+  router.get('/api/visit/rooms/destroy/:id', controller.room.destroy); // destroy
+  router.get('/api/visit/chat', controller.chat.index); // index
+  router.post('/api/visit/chat/create', controller.chat.create); // update
+  router.delete('/api/visit/chat/destroy/:id', controller.chat.destroy); // destroy
+
+  router.get('/api/visit/groupchat', controller.groupchat.index); // index
+  router.post('/api/visit/groupchat/create/:groupid', controller.groupchat.create); // update
+  router.delete('/api/visit/groupchat/destroy/:id', controller.groupchat.destroy); // destroy
+
+
+  // 微信认证,生成包含微信用户信息的token写入cookie
+  router.get('/api/visit/auth', controller.weixin.auth);
+  router.post('/api/visit/wxpaysign', controller.wxpay.wxpaysign); // 取得支付签名
+  router.post('/api/visit/wxpayback', controller.wxpay.wxpaysignback); // 取得支付签名
+  // 打赏管理
+  router.get('/api/visit/doctormoney', controller.doctormoney.index); // index
+  router.post('/api/visit/doctormoney/create/:doctorid', controller.doctormoney.create); // create
+  router.delete('/api/visit/doctormoney/destroy/:id', controller.doctormoney.destroy); // destroy
+
+
+  // 微信扫码登录,生成包含微信用户信息的token,存入redis
+  router.post('/api/visit/qrcode/create', controller.qrcode.create); // 创建二维码
+  router.post('/api/visit/qrcode/creategroup', controller.qrcode.createQrcode); // 创建群二维码
+
+  router.post('/api/visit/qrcode/:qrcode/check', controller.qrcode.check); // 检查二维码状态
+  router.get('/api/visit/qrcode/:qrcode/scan', controller.qrcode.scan); // 扫码确认页面
+  router.post('/api/visit/qrcode/:qrcode/login', controller.qrcode.login); // 扫码登录确认
+  router.post('/api/visit/qrcode/:qrcode/token', controller.qrcode.token); // 换取微信认证码
+
+  // 广告
+  router.resources('adv', '/api/visit/adv', controller.adv); // index、create、show、destroy
+  router.post('/api/visit/adv/update/:id', controller.adv.update);
+
+  // 医疗科普文章
+  router.resources('article', '/api/visit/article', controller.article); // index、create、show、destroy
+  router.post('/api/visit/article/update/:id', controller.article.update);
+};

+ 15 - 0
app/service/adv.js

@@ -0,0 +1,15 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 广告
+class AdvService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'adv');
+    this.model = this.ctx.model.Adv;
+  }
+}
+
+module.exports = AdvService;

+ 15 - 0
app/service/article.js

@@ -0,0 +1,15 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 文章
+class ArticleService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'article');
+    this.model = this.ctx.model.Article;
+  }
+}
+
+module.exports = ArticleService;

+ 145 - 0
app/service/chat.js

@@ -0,0 +1,145 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class ChatService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'chat');
+    this.model = this.ctx.model.Chat;
+    this.cDoctor = this.ctx.model.Doctor;
+    this.cPatient = this.ctx.model.Patient;
+    this.cRoom = this.ctx.model.Room;
+  }
+
+  async query({ sendid, receiveid, type }, { skip, limit }) {
+    assert(sendid, 'sendid不能为空');
+    assert(receiveid, 'receiveid不能为空');
+    const chatsall = await this.model.find({ $or: [{ sendid, receiveid }, { sendid: receiveid, receiveid: sendid }] });
+    const chats = await this.model.find({ $or: [{ sendid, receiveid }, { sendid: receiveid, receiveid: sendid }] }).sort({ sendtime: -1 }).limit(limit)
+      .skip(skip);
+    const newchats = [];
+    for (const el of chats) {
+      let icon;
+      if (el.type === '1') {
+        const patient = await this.cPatient.findById(el.sendid);
+        if (patient) icon = patient.icon;
+      }
+      if (el.type === '0') {
+        const doctor = await this.cDoctor.findById(el.sendid);
+        if (doctor) icon = doctor.icon;
+      }
+      const { id, type, sendid, sendname, receiveid, receivename, sendtime, contenttype, content, status, audiotime } = el;
+      newchats.push({ id, type, sendid, sendname, receiveid, receivename, sendtime, contenttype, content, status, icon, audiotime });
+    }
+    const result = { total: chatsall.length, data: newchats };
+    return result;
+  }
+
+  // 创建群信息
+  async create(data) {
+    const { sendid, receiveid, type, content, audiotime } = data;
+    assert(sendid, '发送人不能为空');
+    assert(type, '类别不能为空');
+    assert(content, '发送内容不能为空');
+    assert(receiveid, '收件人不能为空');
+
+    // 通过群id取得群内医生信息
+    let sendname;
+    let receivename;
+    let receiveopenid;
+    let patientid;
+    let patientname;
+    let doctorid;
+    let doctorname;
+    let icon;
+    if (type === '0') {
+      const doctor = await this.cDoctor.findById(sendid);
+      if (!doctor) {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '医生不存在');
+      }
+      sendname = doctor.name;
+      const patient = await this.cPatient.findById(receiveid);
+      if (!patient) {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+      }
+      receivename = patient.name;
+      receiveopenid = patient.openid;
+      doctorid = sendid;
+      patientid = receiveid;
+      doctorname = sendname;
+      patientname = receivename;
+      icon = doctor.icon;
+    } else {
+      const patient = await this.cPatient.findById(sendid);
+      if (!patient) {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+      }
+      sendname = patient.name;
+      const doctor = await this.cDoctor.findById(receiveid);
+      if (!doctor) {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '医生不存在');
+      }
+      receivename = doctor.name;
+      receiveopenid = doctor.openid;
+      patientid = sendid;
+      doctorid = receiveid;
+      doctorname = receivename;
+      patientname = sendname;
+      icon = patient.icon;
+    }
+    // TODO: 检查是否已经注册
+    const newdata = { type, sendid, audiotime, sendname, receiveid, receivename, content, contenttype: data.contenttype };
+    const entity = await this.model.create(newdata);
+
+    // 发送成功的时候创建房间,首先判断是已经创建房间
+    let room = await this.cRoom.findOne({ patientid, doctorid });
+    if (!room) {
+      const newroom = { patientid, patientname, doctorid, doctorname };
+      room = await this.cRoom.create(newroom);
+    }
+
+    // 调用mq发送消息 只有在医生的情况下才会触发群消息
+    const exchange = 'chat';
+    const routeKey = room.id;
+    const contentMq = sendname + '给' + receivename + '发布了一条消息请及时查收。';
+    const parm = {
+      durable: true,
+      headers: {
+        sendid,
+        receiveid,
+        type,
+        icon,
+        sendname,
+        audiotime,
+        receivename,
+        createtime: new Date().toLocaleDateString(),
+        content,
+        contenttype: data.contenttype,
+        remark: contentMq,
+      },
+    };
+    const { mq } = this.ctx;
+    if (mq) {
+      try {
+        await mq.topic(exchange, routeKey, content, parm);
+      } catch (error) {
+        this.ctx.logger.error(error);
+      }
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+    // 微信提醒
+    await this.service.wechat.sendTemplateMsg(this.ctx.app.config.REVIEW_TEMPLATE_ID, receiveopenid, contentMq, sendname, new Date().toLocaleDateString(), '私聊', '请及时查看');
+    return await this.fetch({ id: entity.id });
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    return 'deleted';
+  }
+}
+
+module.exports = ChatService;

+ 59 - 0
app/service/doctor.js

@@ -0,0 +1,59 @@
+'use strict';
+
+
+const assert = require('assert');
+const _ = require('lodash');
+const { ObjectId } = require('mongoose').Types;
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class DoctorService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'doctor');
+    this.model = this.ctx.model.Doctor;
+  }
+
+  async create(data) {
+    const { name, mobile, passwd } = data;
+    assert(name && mobile && passwd, '缺少部分信息项');
+    const newdata = data;
+    newdata.passwd = { secret: passwd };
+    return await this.model.create(newdata);
+  }
+
+  async update({ id }, data) {
+    console.log(data.id);
+    const doctor = await this.model.findById(id);
+    console.log(doctor);
+    doctor.name = data.name;
+    doctor.mobile = data.mobile;
+    doctor.hosname = data.hosname;
+    doctor.deptname = data.deptname;
+    doctor.title = data.title;
+    doctor.post = data.post;
+    doctor.content = data.content;
+    doctor.remark = data.remark;
+    doctor.openid = data.openid;
+    if (data.icon) {
+      doctor.icon = data.icon;
+    }
+    if (data.passwd) {
+      doctor.passwd = { secret: data.passwd };
+    }
+    return await doctor.save();
+  }
+
+  // 根据openid 取得医生信息
+  async findByOpenid(data) {
+    return await this.model.findOne({ openid: data.openid });
+  }
+
+  async updateopenid({ id }, data) {
+    console.log(data.id);
+    const doctor = await this.model.findById(id);
+    doctor.openid = data.openid;
+    return await doctor.save();
+  }
+}
+
+module.exports = DoctorService;

+ 103 - 0
app/service/doctormoney.js

@@ -0,0 +1,103 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class DoctorMoneyService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'doctormoney');
+    this.model = this.ctx.model.Doctormoney;
+    this.cDoctor = this.ctx.model.Doctor;
+    this.cPatient = this.ctx.model.Patient;
+  }
+
+  async query({ doctorid }, { skip, limit }) {
+    assert(doctorid, 'doctorid不能为空');
+    const moneyall = await this.model.find({ doctorid });
+    const moneys = await this.model.find({ doctorid }).sort({ sendtime: -1 }).limit(limit)
+      .skip(skip);
+    const newmoneys = [];
+    for (const el of moneys) {
+      let icon;
+      const { id, doctorid, doctorname, patientid, patientname, sendtime, money, content, orderno } = el;
+      const patient = await this.cPatient.findById(patientid);
+      if (patient) {
+        icon = patient.icon;
+      }
+      newmoneys.push({ id, doctorid, doctorname, patientid, patientname, sendtime, money, content, orderno, icon });
+    }
+    const result = { total: moneyall.length, data: newmoneys };
+    return result;
+  }
+
+  // 创建打赏记录
+  async create({ doctorid }, data) {
+    const { patientid, money, content, orderno } = data;
+    assert(patientid, '发送人不能为空');
+    assert(money, '金额不能为空');
+    assert(doctorid, 'doctorid不能为空');
+
+    // 通过群id取得群内医生信息
+    const doctor = await this.cDoctor.findById(doctorid);
+    if (!doctor) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '医生不存在');
+    }
+    const patient = await this.cPatient.findById(patientid);
+    if (!patient) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+    }
+    // TODO: 建立打赏信息
+    const newdata = { doctorid, doctorname: doctor.name, patientid, patientname: patient.name, sendtime: new Date().toLocaleString(), money, content, orderno };
+    const entity = await this.model.create(newdata);
+    if (entity) {
+      // 将打赏金额加入到医生信息中
+      doctor.money = await this.plus(doctor.money, money);
+      doctor.balance = await this.plus(doctor.balance, money);
+      await doctor.save();
+    }
+    // 当患者给医生打赏时提醒医生
+    await this.service.wechat.sendTemplateMsg(this.ctx.app.config.REVIEW_TEMPLATE_ID, doctor.openid, content, patient.name, new Date().toLocaleDateString(), '礼物', '请及时查看');
+    return await this.fetch({ id: entity.id });
+  }
+
+  async num(a) {
+    if (a !== null && a.toString() !== '') {
+      const r = /^-?(0|[1-9]+\d*|[1-9]+\d*\.\d+|0\.\d+)$/;
+      if (r.test(a.toString())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  async plus(a, b) {
+    if (!a) a = 0;
+    if (!this.num(a) || !this.num(b)) {
+      return null;
+    }
+    let c,
+      d,
+      m;
+    try {
+      c = a.toString().split('.')[1].length;
+    } catch (e) {
+      c = 0;
+    }
+    try {
+      d = b.toString().split('.')[1].length;
+    } catch (e) {
+      d = 0;
+    }
+    m = Math.pow(10, Math.max(c, d));
+    return (a * m + b * m) / m;
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    return 'deleted';
+  }
+}
+
+module.exports = DoctorMoneyService;

+ 118 - 0
app/service/group.js

@@ -0,0 +1,118 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class GroupService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'group');
+    this.model = this.ctx.model.Group;
+    this.gcModel = this.ctx.model.Groupchat;
+  }
+
+  async query({ doctorid, name }, { skip, limit }) {
+    assert(doctorid, 'doctorid不能为空');
+    let info = { doctorid };
+    if (name) {
+      info = { doctorid, name };
+    }
+    const groupall = await this.model.find(info);
+    const groups = await this.model.find(info).limit(limit).skip(skip);
+    const newgroups = [];
+    for (const el of groups) {
+      const groupchat = await this.gcModel.findOne({ groupid: el.id }).sort({ sendtime: -1 });
+      let group;
+      if (groupchat) {
+        el.content = groupchat.content;
+        el.sendtime = groupchat.sendtime;
+        group = el;
+      } else {
+        group = el;
+      }
+      newgroups.push(group);
+    }
+    const result = { total: groupall.length, data: newgroups };
+    return result;
+  }
+
+  // 创建群信息
+  async create(data) {
+    const { name, doctorid } = data;
+    assert(name, '群名称不能为空');
+    assert(doctorid, '缺少医生信息项');
+
+    // TODO: 检查是否已经注册
+    const newdata = { ...data };
+    const entity = await this.model.create(newdata);
+    return await this.fetch({ id: entity.id });
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    return 'deleted';
+  }
+
+  async updateInfo({ id }, data) {
+    // TODO: 检查数据是否存在
+    const entity = await this.model.findById(id);
+    if (!entity) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '群信息不存在');
+    }
+    entity.doctorid = data.doctorid;
+    entity.name = data.name;
+    entity.content = data.content;
+    return await entity.save();
+  }
+
+  // 群删除患者信息
+  async deletePatient({ groupid, id }) {
+    assert(id, 'id不能为空');
+
+    // TODO: 检查数据是否存在
+    let group;
+    if (groupid) {
+      group = await this.model.findById(groupid, '+patients');
+    }
+    if (!group) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '群信息不存在');
+    }
+    // TODO: 保存数据
+    group.patients.id(id).remove();
+    return await group.save();
+  }
+
+  // 修改患者病历信息
+  async updatePatient({ groupid, id }, data) {
+    assert(id, 'id不能为空');
+
+    // TODO: 检查数据是否存在
+    let group;
+    if (groupid) {
+      group = await this.model.findById(groupid, '+patients');
+    }
+    if (!group) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '群信息不存在');
+    }
+
+    // TODO: 保存数据
+    const patient = group.patients.id(id);
+    const { patientid, url, name, openid } = data;
+    patient.patientid = patientid;
+    patient.url = url;
+    patient.name = name;
+    patient.openid = openid;
+    return await group.save();
+  }
+
+  // 取得患者病历信息
+  async fetchPatient({ groupid }) {
+    assert(groupid, '群id不能为空');
+    // TODO: 检查数据是否存在
+    return await this.model.findById(groupid, '+patients');
+  }
+
+}
+
+module.exports = GroupService;

+ 120 - 0
app/service/groupchat.js

@@ -0,0 +1,120 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class GroupChatService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'groupchat');
+    this.model = this.ctx.model.Groupchat;
+    this.cGroup = this.ctx.model.Group;
+    this.cDoctor = this.ctx.model.Doctor;
+    this.cPatient = this.ctx.model.Patient;
+  }
+
+  async query({ groupid }, { skip, limit }) {
+    assert(groupid, 'groupid不能为空');
+    const groupall = await this.model.find({ groupid });
+    const groups = await this.model.find({ groupid }).sort({ sendtime: -1 }).limit(limit)
+      .skip(skip);
+    const newgroups = [];
+    for (const el of groups) {
+      let icon;
+      const { id, doctorid, doctorname, groupid, groupname, type, sendid, sendname, sendtime, contenttype, content, audiotime } = el;
+      if (el.type === '1') {
+        const patient = await this.cPatient.findById(el.sendid);
+        if (patient) {
+          icon = patient.icon;
+        }
+      }
+      if (el.type === '0') {
+        const doctor = await this.cDoctor.findById(el.sendid);
+        if (doctor) icon = doctor.icon;
+      }
+      const group = { id, doctorid, doctorname, groupid, groupname, type, sendid, sendname, sendtime, contenttype, content, icon, audiotime };
+      newgroups.push(group);
+    }
+    const result = { total: groupall.length, data: newgroups };
+    return result;
+  }
+
+  // 创建群信息
+  async create({ groupid }, data) {
+    const { sendid, type, content, audiotime } = data;
+    assert(sendid, '发送人不能为空');
+    assert(type, '类别不能为空');
+    assert(content, '发送内容不能为空');
+    assert(groupid, '缺少群信息项');
+
+    // 通过群id取得群内医生信息
+    const group = await this.cGroup.findById(groupid, '+patients');
+    if (!group) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '群不存在');
+    }
+    const doctor = await this.cDoctor.findById(group.doctorid);
+    if (!doctor) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '医生不存在');
+    }
+    let sendname;
+    let icon;
+    if (type === '0') {
+      sendname = doctor.name;
+      icon = doctor.icon;
+    } else {
+      console.log(sendid);
+      const patient = await this.cPatient.findById(sendid);
+      console.log(patient);
+      if (!patient) {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+      }
+      sendname = patient.name;
+      icon = patient.icon;
+    }
+    // TODO: 检查是否已经注册
+    const newdata = { doctorid: group.doctorid, audiotime, doctorname: doctor.name, groupid, groupname: group.name, type, sendid, sendname, sendtime: new Date().toLocaleString(), content, contenttype: data.contenttype };
+    const entity = await this.model.create(newdata);
+
+    // 调用mq发送消息 只有在医生的情况下才会触发群消息
+    const exchange = 'group_chat';
+    const routeKey = groupid;
+    const contentMq = doctor.name + '医生的' + group.name + '群内发布了一条消息请及时查收。';
+    const parm = {
+      durable: true,
+      headers: {
+        sendid,
+        groupid,
+        icon,
+        type,
+        audiotime,
+        sendname,
+        createtime: new Date().toLocaleDateString(),
+        content,
+        contenttype: data.contenttype,
+        remark: contentMq,
+      },
+    };
+    const { mq } = this.ctx;
+    if (mq) {
+      await mq.fanout(exchange, routeKey, content, parm);
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+    // 当医生发送群消息时微信提醒所有群内患者
+    const self = this;
+    if (type === '0') {
+      for (const elem of group.patients) {
+        await self.service.wechat.sendTemplateMsg(self.ctx.app.config.REVIEW_TEMPLATE_ID, elem.openid, contentMq, doctor.name, new Date().toLocaleDateString(), '群聊', '请及时查看');
+      }
+    }
+    return await this.fetch({ id: entity.id });
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    return 'deleted';
+  }
+}
+
+module.exports = GroupChatService;

+ 68 - 0
app/service/login.js

@@ -0,0 +1,68 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { ObjectId } = require('mongoose').Types;
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const jwt = require('jsonwebtoken');
+
+class LoginService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'login');
+    this.nModel = this.ctx.model.Nurse;
+    this.uModel = this.ctx.model.User;
+    this.dModel = this.ctx.model.Doctor;
+    this.pModel = this.ctx.model.Patient;
+  }
+
+  async login(data) {
+    const { type, mobile, passwd } = data;
+    assert(type, 'type不能为空');
+    assert(mobile, 'mobile不能为空');
+    assert(/^\d{11}$/i.test(mobile), 'mobile无效');
+    assert(passwd, 'passwd不能为空');
+    let res;
+    if (type === 'admin') {
+      res = await this.uModel.findOne({ mobile }, '+passwd');
+    } else {
+      res = await this.nModel.findOne({ mobile }, '+passwd');
+    }
+    if (!res) {
+      throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+    }
+    // 验证密码
+    console.log(res.passwd.secret);
+    console.log(passwd);
+    if (res.passwd.secret !== passwd) {
+      throw new BusinessError(ErrorCode.BAD_PASSWORD);
+    }
+    return await this.createJwt(type, res);
+  }
+
+  // 创建登录Token
+  async createJwt(type, { _id, name, mobile, openid, doctorid, doctorname }) {
+    const { secret, expiresIn = '1d', issuer = type } = this.config.jwt;
+    const subject = mobile;
+    const token = await jwt.sign({ userid: _id.toString(), name, openid, type, doctorid, doctorname }, secret, { expiresIn, issuer, subject });
+    return token;
+  }
+
+  async wxlogin(data) {
+    const { openid } = data;
+    assert(openid, 'openid不能为空');
+    let res = await this.dModel.findOne({ openid });
+    let newdata = {};
+    if (!res) {
+      res = await this.pModel.findOne({ openid });
+      if (!res) {
+        throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+      }
+      newdata = { id: res.id, name: res.name, openid: res.openid, type: 'patient' };
+    } else {
+      newdata = { id: res.id, name: res.name, openid: res.openid, type: 'doctor' };
+    }
+    return await newdata;
+  }
+}
+module.exports = LoginService;

+ 44 - 0
app/service/nurse.js

@@ -0,0 +1,44 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { ObjectId } = require('mongoose').Types;
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class NurseService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'nurse');
+    this.model = this.ctx.model.Nurse;
+  }
+
+  async create(data) {
+    const { name, mobile, passwd } = data;
+    assert(name && mobile && passwd, '缺少部分信息项');
+    const newdata = data;
+    newdata.passwd = { secret: passwd };
+    return await this.model.create(newdata);
+  }
+
+  async update({ id }, data) {
+    console.log(data.id);
+    const nurse = await this.model.findById(id);
+    console.log(nurse);
+    nurse.name = data.name;
+    nurse.mobile = data.mobile;
+    nurse.hosname = data.hosname;
+    nurse.deptname = data.deptname;
+    nurse.title = data.title;
+    nurse.post = data.post;
+    nurse.content = data.content;
+    nurse.remark = data.remark;
+    nurse.openid = data.openid;
+    nurse.doctorid = data.doctorid;
+    nurse.doctroname = data.doctroname;
+    if (data.passwd) {
+      nurse.passwd = { secret: data.passwd };
+    }
+    return await nurse.save();
+  }
+}
+module.exports = NurseService;

+ 219 - 0
app/service/patient.js

@@ -0,0 +1,219 @@
+'use strict';
+
+
+const assert = require('assert');
+const _ = require('lodash');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class PatientService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'patient');
+    this.model = this.ctx.model.Patient;
+    this.pDocs = this.ctx.model.Patientdocs;
+    this.pGroups = this.ctx.model.Group;
+    this.pDoctor = this.ctx.model.Doctor;
+    this.chatModel = this.ctx.model.Chat;
+  }
+
+  async query({ doctorid }, { skip, limit }) {
+    assert(doctorid, 'doctorid不能为空');
+    const pdocall = await this.pDocs.find({ doctorid });
+    const pdoc = await this.pDocs.find({ doctorid }).limit(limit).skip(skip);
+    const patients = [];
+    for (const el of pdoc) {
+      const patient = await this.model.findById(el.patientid);
+      patients.push(patient);
+    }
+    const result = { total: pdocall.length, data: patients };
+    return result;
+  }
+
+  // 创建患者信息
+  async create(data) {
+    const { name, tel, openid, groupid } = data;
+    assert(name, '患者名称不能为空');
+    assert(tel, '患者电话不能为空');
+    assert(openid, '微信openid不能为空');
+    assert(groupid, '缺少群信息项');
+
+    const group = await this.pGroups.findById(groupid, '+patients');
+    if (!group) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '群不存在');
+    }
+    const doctor = await this.pDoctor.findById(group.doctorid);
+    // TODO: 检查是否已经注册
+    let entity = await this.model.findOne({ openid });
+    if (entity) {
+      console.log(group.patients);
+      if (group.patients.length === 0) {
+        const patient = { patientid: entity.id, url: entity.url, name: entity.name, openid: entity.openid };
+        // const newpatient = group.patients.create(patient);
+        // console.log('newpatient:', newpatient);
+        group.patients.push(patient);
+        await group.save();
+      } else {
+        const groupPatient = group.patients.filter(fil => fil.patientid === entity.id);
+        if (groupPatient) {
+          throw new BusinessError(ErrorCode.DATA_EXIST, '已经注册,无需重复注册');
+        } else {
+          const patient = { patientid: entity.id, url: entity.url, name: entity.name, openid: entity.openid };
+          // const newpatient = group.patients.create(patient);
+          // console.log('newpatient:', newpatient);
+          group.patients.push(patient);
+          await group.save();
+        }
+      }
+      const pdoc = await this.pDocs.findOne({ doctorid: doctor.id });
+      if (!pdoc) {
+        await this.pDocs.create({ patientid: entity.id, patientname: entity.name, doctorid: doctor.id, doctorname: doctor.name });
+      }
+    } else {
+      entity = await this.model.create(data);
+      await this.pDocs.create({ patientid: entity.id, patientname: name, doctorid: doctor.id, doctorname: doctor.name });
+      const patient = { patientid: entity.id, url: entity.url, name: entity.name, openid: entity.openid };
+      group.patients.push(patient);
+      await group.save();
+    }
+    return await this.fetch({ id: entity.id });
+  }
+
+  // 查询患者所属医生列表信息
+  async doctors({ id }) {
+    assert(id, '患者id不能为空');
+    const doctors = await this.pDocs.find({ patientid: id });
+    const data = [];
+    for (const elm of doctors) {
+
+      let contenttype = '';
+      let content = '';
+      let sendtime = '';
+      const chats = await this.chatModel.find({ $or: [{ sendid: elm.doctorid, receiveid: elm.patientid }, { sendid: elm.patientid, receiveid: elm.doctorid }] }).sort({ sendtime: -1 }).limit(1)
+        .skip(0);
+      if (chats && chats.length > 0) {
+        contenttype = chats[0].contenttype;
+        content = chats[0].content;
+        sendtime = chats[0].sendtime;
+      }
+      const newdata = { patientid: elm.patientid, patientname: elm.patientname, doctorid: elm.doctorid, doctorname: elm.doctorname, contenttype, content, sendtime };
+      data.push(newdata);
+    }
+    return data;
+  }
+
+  // 查询患者所属医生列表信息
+  async groups({ id }) {
+    assert(id, '患者id不能为空');
+    const groups = await this.pGroups.find({ 'patients.patientid': id }, '+patients');
+    return groups;
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    await this.pDocs.deleteMany({ patientid: id });
+    return 'deleted';
+  }
+
+  async updateInfo({ id }, data) {
+    // TODO: 检查数据是否存在
+    const entity = await this.model.findById(id);
+    if (!entity) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+    }
+    entity.name = data.name;
+    entity.cardno = data.cardno;
+    entity.gender = data.gender;
+    entity.birthday = data.birthday;
+    entity.address = data.address;
+    entity.tel = data.tel;
+    entity.urgentname = data.urgentname;
+    entity.urgenttel = data.urgenttel;
+    entity.content = data.content;
+    entity.openid = data.openid;
+    entity.icon = data.icon;
+    return await entity.save();
+  }
+
+  // 用户创建患者病历信息
+  async createEmr({ patientid }, data) {
+    const { indate, outdate, title, content, doctorid, doctorname } = data;
+    assert(patientid, '患者ID不能为空');
+    assert(title, '标题不能为空');
+
+    // TODO: 检查数据是否存在
+    const patient = await this.model.findById(patientid, '+emrs');
+    if (!patient) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+    }
+
+    // 准备数据
+    const newData = { indate, outdate, title, content, doctorid, doctorname };
+    // TODO: 保存数据
+    const newemr = patient.emrs.create(newData);
+    console.log('newemr:', newemr);
+    patient.emrs.push(newemr);
+    await patient.save();
+    return newemr;
+  }
+
+  // 删除患者病历信息
+  async deleteEmr({ patientid, id }) {
+    assert(id, 'id不能为空');
+
+    // TODO: 检查数据是否存在
+    let patient;
+    if (patientid) {
+      patient = await this.model.findById(patientid, '+emrs');
+    }
+    if (!patient) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+    }
+    // TODO: 保存数据
+    patient.emrs.id(id).remove();
+    return await patient.save();
+  }
+
+  // 修改患者病历信息
+  async updateEmr({ patientid, id }, data) {
+    assert(id, 'id不能为空');
+
+    // TODO: 检查数据是否存在
+    let patient;
+    if (patientid) {
+      patient = await this.model.findById(patientid, '+emrs');
+    }
+    if (!patient) {
+      throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '患者信息不存在');
+    }
+
+    // TODO: 保存数据
+    const emr = patient.emrs.id(id);
+    const { indate, outdate, title, content, doctorid, doctorname } = data;
+    emr.indate = indate;
+    emr.outdate = outdate;
+    emr.title = title;
+    emr.content = content;
+    emr.doctorid = doctorid;
+    emr.doctorname = doctorname;
+    return await patient.save();
+  }
+
+  // 取得患者病历信息
+  async fetchEmr({ patientid, emrid }) {
+    assert(patientid, '患者id不能为空');
+    assert(emrid, '病历id不能为空');
+    // TODO: 检查数据是否存在
+    const patient = await this.model.findById(patientid, '+emrs');
+    const emr = patient.emrs.id(emrid);
+    return emr;
+  }
+
+  // 根据openid 取得医生信息
+  async findByOpenid(data) {
+    return await this.model.findOne({ openid: data.openid });
+  }
+
+
+}
+
+module.exports = PatientService;

+ 53 - 0
app/service/rabbitmq.js

@@ -0,0 +1,53 @@
+'use strict';
+const Service = require('egg').Service;
+
+class RabbitmqService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+    this.exType = 'topic';
+    this.durable = true;
+  }
+
+  // 发送消息
+  async sendQueueMsg(ex, routeKey, msg, parm) {
+    const { mq } = this.ctx;
+    if (mq) {
+      await mq.topic(ex, routeKey, msg, parm);
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+  // 接收消息
+  async receiveQueueMsg(ex) {
+    const self = this;
+    const { mq } = self.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      try {
+        await ch.assertExchange(ex, self.exType, { durable: self.durable });
+        const q = await ch.assertQueue('', { exclusive: true });
+        console.log('==q=', q);
+        // 队列绑定 exchange
+        await ch.bindQueue(q.queue, ex, '*');
+        ch.consume(q.queue, msg => {
+          console.log('收到消息: ', msg);
+          const result = msg.content.toString();
+          const headers = msg.properties.headers;
+          // 插入待办事项到数据库中。
+          console.log(result);
+          console.log(headers);
+        }, { noAck: true });
+      } catch (e) {
+        console.log('==e==', e);
+        await ch.close();
+      }
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+}
+
+module.exports = RabbitmqService;

+ 55 - 0
app/service/room.js

@@ -0,0 +1,55 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const { ObjectId } = require('mongoose').Types;
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class RoomService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'room');
+    this.model = this.ctx.model.Room;
+    this.pmodel = this.ctx.model.Patient;
+    this.chatModel = this.ctx.model.Chat;
+  }
+
+  async query(data) {
+    const { patientid, doctorid } = data;
+    let res;
+    if (patientid) {
+      res = await this.model.find({ patientid });
+    }
+    if (doctorid) {
+      const res_ = await this.model.find({ doctorid });
+      const data = [];
+      for (const elm of res_) {
+        const patient = await this.pmodel.findById(elm.patientid);
+        let icon = '';
+        if (patient) {
+          icon = patient.icon;
+        }
+        let contenttype = '';
+        let content = '';
+        let sendtime = '';
+        const chats = await this.chatModel.find({ $or: [{ sendid: elm.doctorid, receiveid: elm.patientid }, { sendid: elm.patientid, receiveid: elm.doctorid }] }).sort({ sendtime: -1 }).limit(1)
+          .skip(0);
+        if (chats && chats.length > 0) {
+          contenttype = chats[0].contenttype;
+          content = chats[0].content;
+          sendtime = chats[0].sendtime;
+        }
+        const newdata = { patientid: elm.patientid, patientname: elm.patientname, icon, contenttype, content, sendtime, doctorid: elm.doctorid, doctorname: elm.doctorname };
+        data.push(newdata);
+      }
+      res = data;
+    }
+    return res;
+  }
+
+  async delete({ id }) {
+    await this.model.findByIdAndDelete(id);
+    return 'deleted';
+  }
+}
+module.exports = RoomService;

+ 57 - 0
app/service/wechat.js

@@ -0,0 +1,57 @@
+'use strict';
+const Service = require('egg').Service;
+
+class WechatService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+    this.appid = 'wxdf3ed83c095be97a';
+  }
+
+  // 发送微信模板消息
+  async sendTemplateMsg(templateid, openid, first, keyword1, keyword2, keyword3, remark) {
+    const url = this.ctx.app.config.sendDirMq + this.ctx.app.config.appid;
+    const requestData = { // 发送模板消息的数据
+      touser: openid,
+      template_id: templateid,
+      url: '',
+      data: {
+        first: {
+          value: first,
+          color: '#173177',
+        },
+        keyword1: {
+          value: keyword1,
+          color: '#1d1d1d',
+        },
+        keyword2: {
+          value: keyword2,
+          color: '#1d1d1d',
+        },
+        keyword3: {
+          value: keyword3,
+          color: '#1d1d1d',
+        },
+        remark: {
+          value: remark,
+          color: '#173177',
+        },
+      },
+    };
+    console.log('templateid---' + templateid);
+    console.log('openid---' + openid);
+    console.log('requestData---' + JSON.stringify(requestData));
+    console.log('url---' + url);
+    await this.ctx.curl(url, {
+      method: 'post',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+      data: JSON.stringify(requestData),
+    });
+  }
+
+}
+
+module.exports = WechatService;

+ 144 - 0
app/service/weixin.js

@@ -0,0 +1,144 @@
+'use strict';
+
+const assert = require('assert');
+const uuid = require('uuid');
+const _ = require('lodash');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const jwt = require('jsonwebtoken');
+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 { wxapi } = this.app.config;
+    const reqUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token';
+    const params = {
+      appid: wxapi.appid,
+      secret: wxapi.appSecret,
+      code,
+      grant_type: 'authorization_code',
+    };
+    const res = await this.httpGet(reqUrl, params);
+    console.log(res);
+    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, '获得微信认证信息失败');
+    }
+    // const { openid } = res;
+    return res;
+  }
+
+  async createJwt({ openid, nickname, subscribe }) {
+    const { secret, expiresIn = '1d', issuer = 'weixin' } = this.config.jwt;
+    const subject = openid;
+    const userinfo = { nickname, subscribe };
+    const token = await jwt.sign(userinfo, secret, { expiresIn, issuer, subject });
+    return token;
+  }
+  /**
+   * 创建二维码
+   * 随机生成二维码,并保存在Redis中,状态初始为pending
+   * 状态描述:
+   * pending - 等待扫码
+   * consumed - 使用二维码登录完成
+   * scand:token - Jwt登录凭证
+   */
+  async createQrcode() {
+    const qrcode = uuid();
+    const key = `visit:qrcode:group:${qrcode}`;
+    await this.app.redis.set(key, 'pending', 'EX', 600);
+    return qrcode;
+  }
+
+  /**
+   * 创建二维码
+   * 生成群二维码
+   * 状态描述:
+   * pending - 等待扫码
+   * consumed - 使用二维码登录完成
+   * scand:token - Jwt登录凭证
+   */
+  async createQrcodeGroup({ groupid }) {
+    const { authUrl = this.ctx.path } = this.app.config;
+    let backUrl;
+    if (authUrl.startsWith('http')) {
+      backUrl = encodeURI(`${authUrl}?state=${groupid}`);
+    } else {
+      backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${authUrl}?state=${groupid}`);
+    }
+    console.log(backUrl);
+    return backUrl;
+  }
+
+  /**
+   * 扫码登录确认
+   */
+  async scanQrcode({ qrcode, token }) {
+    assert(qrcode, 'qrcode不能为空');
+    assert(token, 'token不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const status = await this.app.redis.get(key);
+    if (!status) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    if (status !== 'pending') {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
+    }
+
+    // 验证Token
+    const { secret } = this.config.jwt;
+    const decoded = jwt.verify(token, secret, { issuer: 'weixin' });
+    this.ctx.logger.debug(`[weixin] qrcode login - ${decoded}`);
+
+    // TODO: 修改二维码状态,登录凭证保存到redis
+    await this.app.redis.set(key, `scaned:${token}`, 'EX', 600);
+
+    // TODO: 发布扫码成功消息
+    const { mq } = this.ctx;
+    const ex = 'qrcode.login';
+    if (mq) {
+      await mq.topic(ex, qrcode, 'scaned', { durable: true });
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+  // 使用二维码换取登录凭证
+  async qrcodeLogin(qrcode) {
+    assert(qrcode, 'qrcode不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const val = await this.app.redis.get(key);
+    if (!val) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    const [ status, token ] = val.split(':', 2);
+    if (status !== 'scaned' || !token) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
+    }
+
+    // TODO: 修改二维码状态
+    await this.app.redis.set(key, 'consumed', 'EX', 600);
+
+    return { token };
+  }
+
+  // 检查二维码状态
+  async checkQrcode(qrcode) {
+    assert(qrcode, 'qrcode不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const val = await this.app.redis.get(key);
+    if (!val) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    const [ status ] = val.split(':', 2);
+    return { status };
+  }
+}
+
+module.exports = WeixinAuthService;

+ 102 - 0
app/service/wxpay.js

@@ -0,0 +1,102 @@
+'use strict';
+
+const xmlreader = require('xmlreader');
+const fs = require('fs');
+const Service = require('egg').Service;
+
+class WxPayService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+    this.abc = 'wxdf3ed83c095be97a';
+  }
+
+  // 把金额转为分
+  async getmoney(money) {
+    return parseFloat(money) * 100;
+  }
+
+  // 随机字符串产生函数
+  async createNonceStr() {
+    return Math.random().toString(36).substr(2, 15);
+  }
+
+  // 时间戳产生函数
+  async createTimeStamp() {
+    return parseInt(new Date().getTime() / 1000) + '';
+  }
+
+  // 签名加密算法
+  async paysignjsapi(appid, body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, trade_type, mchkey, openid) {
+    const ret = {
+      appid,
+      mch_id,
+      nonce_str,
+      body,
+      notify_url,
+      out_trade_no,
+      spbill_create_ip,
+      total_fee,
+      trade_type,
+      openid,
+    };
+    let string = await this.raw(ret);
+    const key = mchkey;
+    string = string + '&key=' + key;
+    const crypto = require('crypto');
+    return crypto.createHash('md5').update(string, 'utf8').digest('hex')
+      .toUpperCase();
+  }
+  // 签名加密算法,第二次的签名
+  async paysignjsapifinal(appid, mch_id, prepayid, noncestr, timestamp, mchkey) {
+    const ret = {
+      appId: appid,
+      timeStamp: timestamp,
+      nonceStr: noncestr,
+      package: 'prepay_id=' + prepayid,
+      signType: 'MD5',
+    };
+    let string = await this.raw(ret);
+    const key = mchkey;
+    string = string + '&key=' + key;
+    const crypto = require('crypto');
+    return crypto.createHash('md5').update(string, 'utf8').digest('hex')
+      .toUpperCase();
+  }
+  async getXMLNodeValue(xml) {
+    // var tmp = xml.split("<"+node_name+">");
+    // console.log('tmp',tmp);
+    // var _tmp = tmp[1].split("</"+node_name+">");
+    // console.log('_tmp',_tmp);
+    // return _tmp[0];
+    const parseObj = await new Promise(function(resolve) {
+      xmlreader.read(xml, function(errors, response) {
+        if (errors !== null) {
+          console.log(errors);
+          return;
+        }
+        const prepay_id = response.xml.prepay_id.text();
+        resolve(prepay_id);
+      });
+    });
+    return parseObj;
+  }
+
+  async raw(args) {
+    let keys = Object.keys(args);
+    keys = keys.sort();
+    const newArgs = {};
+    keys.forEach(function(key) {
+      newArgs[key] = args[key];
+    });
+    let string = '';
+    for (const k in newArgs) {
+      string += '&' + k + '=' + newArgs[k];
+    }
+    string = string.substr(1);
+    return string;
+  }
+
+}
+
+module.exports = WxPayService;

+ 21 - 0
app/view/error.njk

@@ -0,0 +1,21 @@
+<!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-warn weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">{{ title | d("服务器错误")}}</h2>
+      <p class="weui-msg__desc">{{message}}</p>
+    </div>
+  </div>
+</body>
+
+</html>

+ 21 - 0
app/view/info.njk

@@ -0,0 +1,21 @@
+<!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-info weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">{{ title | d("信息提醒")}}</h2>
+      <p class="weui-msg__desc">{{message}}</p>
+    </div>
+  </div>
+</body>
+
+</html>

+ 88 - 0
app/view/login.njk

@@ -0,0 +1,88 @@
+<!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://cdnjs.gtimg.com/cdnjs/libs/jquery/2.1.1/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 type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
+	<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"></link>
+	<script type="text/javascript" src="../../public/weui/weui-prompt.js"></script>
+	<script type="text/javascript" src="../../public/weui/weui-util.js"></script>
+	<link rel="stylesheet" href="../../public/weui/weui-prompt.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>
+			<p class="weui-msg__desc">${message}</p>
+		</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">
+    var token = '{{token | safe}}';
+		var app = new Vue({
+			delimiters: ['${', '}'],
+			el : '#app',
+			data : {
+				loading : false,
+				view: 'login',
+				message: '{{ message| safe }}',
+			},
+			methods : {
+				login : function() {
+					if(this.loading) return;
+					this.loading = true;
+					$.post('login', { token: token })
+					.then(function(result) {
+						console.log(result);
+						if (result.errcode == 0) {
+							showAlert('您已成功通过微信扫码登录双困生培训管理平台。', '微信登录成功', function(){
+								app.view = 'success';
+							});
+						} 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>

+ 34 - 0
app/view/redirect.njk

@@ -0,0 +1,34 @@
+<!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">{{ title | d("正在跳转,请稍候")}}</h2>
+      <p class="weui-msg__desc">{{message}}</p>
+    </div>
+  </div>
+  <script>
+    var redirect_uri = '{{redirect_uri}}';
+    var wxtoken = '{{wxtoken | safe}}';
+    var openid = '{{openid | safe}}';
+    var nickname = '{{nickname | safe}}';
+
+    window.onload = function() {
+      sessionStorage.setItem('wxtoken', wxtoken);
+      sessionStorage.setItem('openid', openid);
+      sessionStorage.setItem('nickname', nickname);
+      window.location.replace(redirect_uri);
+    }
+  </script>
+</body>
+
+</html>

+ 28 - 0
app/view/register.njk

@@ -0,0 +1,28 @@
+<!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>
+	<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
+</head>
+
+<body>
+  <div class="weui-msg">
+    <div class="weui-msg__icon-area"><i class="weui-icon-info weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">尚未完成注册</h2>
+      <p class="weui-msg__desc">{{ message | d("请先完成用户注册流程,再重新扫码登录。")}}</p>
+    </div>
+		<div class="weui-msg__opr-area" v-if="view == 'login'">
+			<p class="weui-btn-area">
+				<a href="/weixin/ui/center/index.html" class="weui-btn weui-btn_primary">前往用户注册</a> 
+				<a href="javascript: wx.closeWindow();" class="weui-btn weui-btn_default">取消</a>
+			</p>
+		</div>
+  </div>
+</body>
+
+</html>

+ 24 - 0
app/view/subscribe.njk

@@ -0,0 +1,24 @@
+<!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-info weui-icon_msg"></i></div>
+    <div class="weui-msg__text-area">
+      <h2 class="weui-msg__title">未关注公众号</h2>
+      <p class="weui-msg__desc">请识别二维码关注公众号</p>
+      {% if appid %}
+      <img src="public/qrcode_for_{{appid}}.jpg" width="70%">
+      {% endif %}
+    </div>
+  </div>
+</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

+ 134 - 0
config/config.default.js

@@ -0,0 +1,134 @@
+/* eslint valid-jsdoc: "off" */
+
+'use strict';
+
+const { jwt } = require('./config.secret');
+/**
+ * @param {Egg.EggAppInfo} appInfo app info
+ */
+module.exports = appInfo => {
+  /**
+   * built-in config
+   * @type {Egg.EggAppConfig}
+   **/
+  const config = exports = {};
+
+  // use for cookie sign key, should change to your own and keep security
+  config.keys = appInfo.name + '_1576744686394_2727';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+
+  config.proxy = true;
+  config.hostHeaders = 'x-forwarded-host';
+
+  config.wxapi = {
+    appid: 'wxdf3ed83c095be97a', // 微信公众号APPID
+    appSecret: '748df7c2a75077a79ae0c971b1638244',
+    baseUrl: 'http://wx.cc-lotus.info', // 微信网关地址
+    mchid: '1505364491', // 商户ID
+    mchkey: '1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9', // 商户key
+    wxurl: 'http://broadcast.waityou24.cn/api/visit/wxpayback',
+    payurl: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+  };
+
+  // 服务器发布路径
+  config.baseUrl = 'http://broadcast.waityou24.cn';
+  // 认证回调地址
+  config.authUrl = '/api/visit/auth';
+  // 回调地址
+  config.redirect_uri = `${config.baseUrl}/weixin`;
+  config.redirect_uri_doctor = `${config.baseUrl}/patient/manage`;
+  config.redirect_uri_patient = `${config.baseUrl}/doctor/manage`;
+
+
+  config.errorMongo = {
+    details: true,
+  };
+  config.errorHanler = {
+    details: true,
+  };
+
+  // add your config here
+  config.cluster = {
+    listen: {
+      port: 8888,
+    },
+  };
+
+  // base路径
+  config.sendDirMq = 'http://wx.cc-lotus.info/api.weixin.qq.com/cgi-bin/message/template/send?appid=';
+  // appID
+  config.appid = 'wxdf3ed83c095be97a';
+  config.REVIEW_TEMPLATE_ID = 'BI4h0AQpdctm74I7-7PyHAspSMX2oJOTJVQsgrppOag';
+
+  config.multipart = {
+    fileSize: '50mb', // 文件大小
+    mode: 'file', // 文件模式
+    whitelist: [ '.xlsx' ], // 文件类型白名单
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'visit',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27017/visit',
+    options: {
+      // user: 'admin',
+      // pass: 'admin',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+    },
+  };
+
+  // 安全配置
+  config.security = {
+    csrf: {
+      // ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
+      enable: false,
+    },
+  };
+  // // JWT config
+  config.jwt = {
+    ...jwt,
+    expiresIn: '1d',
+    issuer: 'jobs',
+  };
+
+  config.view = {
+    defaultViewEngine: 'nunjucks',
+    mapping: {
+      '.njk': 'nunjucks',
+    },
+  };
+
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 61 - 0
config/config.local.js

@@ -0,0 +1,61 @@
+'use strict';
+
+module.exports = () => {
+  const config = (exports = {});
+
+  config.logger = {
+    level: 'DEBUG',
+    consoleLevel: 'DEBUG',
+  };
+
+  config.wxapi = {
+    appid: 'wxdf3ed83c095be97a', // 微信公众号APPID
+    appSecret: 'ceb7f0986c76f227ae3049934c313f22',
+    baseUrl: 'http://wx.cc-lotus.info', // 微信网关地址
+    mchid: '1505364491', // 商户ID
+    mchkey: '1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9', // 商户key
+    wxurl: 'http://broadcast.waityou24.cn/api/visit/wxpayback',
+    payurl: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+  };
+
+  // 服务器发布路径
+  config.baseUrl = 'http://broadcast.waityou24.cn';
+  // 认证回调地址
+  config.authUrl = '/api/visit/auth';
+
+  // mq config
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'visit',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+
+  config.mongoose = {
+    url: 'mongodb://localhost:27017/visit',
+    options: {
+      // user: 'admin',
+      // pass: 'admin',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+      // useUnifiedTopology: true,
+    },
+  };
+
+  return config;
+};

+ 45 - 0
config/config.prod.js

@@ -0,0 +1,45 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+
+  config.logger = {
+    // level: 'DEBUG',
+    // consoleLevel: 'DEBUG',
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'visit',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27017/visit',
+    options: {
+      user: 'admin',
+      pass: 'admin',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+
+  return config;
+};

+ 7 - 0
config/config.secret.js

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

+ 20 - 0
config/plugin.js

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

+ 5 - 0
jsconfig.json

@@ -0,0 +1,5 @@
+{
+  "include": [
+    "**/*"
+  ]
+}

+ 62 - 0
package.json

@@ -0,0 +1,62 @@
+{
+  "name": "service-visit",
+  "version": "1.0.0",
+  "description": "student info service",
+  "private": true,
+  "egg": {
+    "framework": "naf-framework-mongoose"
+  },
+  "dependencies": {
+    "egg": "^2.23.0",
+    "egg-naf-amqp": "0.0.13",
+    "egg-redis": "^2.4.0",
+    "egg-scripts": "^2.11.0",
+    "egg-view-nunjucks": "^2.2.0",
+    "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.15",
+    "naf-framework-mongoose": "^0.6.11",
+    "raml2html": "^6.1.0",
+    "silly-datetime": "^0.1.2",
+    "url-join": "^4.0.1",
+    "uuid": "^3.3.3",
+    "xmlreader": "^0.2.3"
+  },
+  "devDependencies": {
+    "autod": "^3.1.0",
+    "autod-egg": "^1.1.0",
+    "egg-bin": "^4.13.1",
+    "egg-ci": "^1.13.0",
+    "egg-mock": "^3.23.1",
+    "eslint": "^6.1.0",
+    "eslint-config-egg": "^7.4.1",
+    "eslint-config-egg-naf": "0.0.3",
+    "webstorm-disable-index": "^1.2.0"
+  },
+  "engines": {
+    "node": ">=8.9.0"
+  },
+  "scripts": {
+    "start": "egg-scripts start --daemon --title=egg-service-student",
+    "stop": "egg-scripts stop --title=egg-service-student",
+    "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 service-student",
+    "raml": "raml2html -i app/controller/api.raml -o app/public/api.html"
+  },
+  "ci": {
+    "version": "8"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "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,
+});

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

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