Browse Source

直播服务

liuyu 4 years ago
parent
commit
45c80e84cd

+ 42 - 0
app/controller/.role.js

@@ -0,0 +1,42 @@
+module.exports = {
+  create: {
+    requestBody: [
+      'code',
+      '!role_name',
+      'url'
+    ]
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete'
+  },
+  update: {
+    params: ['!id'],
+    requestBody: [
+      'code',
+      '!role_name',
+      'url'
+    ]
+  },
+  show: {
+    parameters: {
+      params: ['!id']
+    },
+    service: 'fetch'
+  },
+  index: {
+    parameters: {
+      query: {
+        role_name:'%role_name%',
+        url:'code'
+      }
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true
+    }
+  },
+};

+ 35 - 0
app/controller/login.js

@@ -0,0 +1,35 @@
+'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 token() {
+    const res = await this.service.token(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+
+  async destroy() {
+    const res = await this.service.destroy(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;

+ 18 - 0
app/controller/role.js

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

+ 20 - 0
app/controller/user.js

@@ -12,6 +12,26 @@ class UserController extends Controller {
     this.service = this.ctx.service.user;
   }
 
+  async uppasswd() {
+    const res = await this.service.uppasswd(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+
+  async querymenus() {
+    const res = await this.service.querymenus(this.ctx.query);
+    this.ctx.ok({ data: res });
+  }
+
+  async updatebyuid() {
+    const res = await this.service.updatebyuid(this.ctx.params, this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+
+  async bind() {
+    const res = await this.service.bind(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', uid, qrcode } = 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, uid, qrcode });
+    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);
+  }
+
+  // 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, uid, qrcode } = JSON.parse(val);
+      console.log('redirect_uri-->' + redirect_uri);
+      const user = await this.ctx.service.user.findByOpenid(openid);
+      if (type === '0') {
+        // 通过openid取得用户信息
+        if (user) {
+          const token = await this.ctx.service.login.createJwt(user);
+          const to_uri = urljoin(redirect_uri, `?token=${token}`);
+          // TODO: 重定性页面
+          console.log('to_uri000-->' + to_uri);
+          this.ctx.redirect(to_uri);
+
+        } else {
+          console.log('rrr0000--->' + redirect_uri);
+          const touri = `${this.app.config.baseUrl}/platmobile/error`;
+          const to_uri = urljoin(touri, `?openid=${openid}`);
+          // TODO: 重定性页面
+          this.ctx.redirect(to_uri);
+        }
+      } else if (type === '1') {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&uid=${uid}&type=${type}&qrcode=${qrcode}`);
+        // TODO: 重定性页面
+        console.log('1111---?' + to_uri);
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+
+  // GET 用户授权内部测试接口
+  async authTest() {
+    const { redirect_uri, type, uid, qrcode, openid } = this.ctx.query;
+    this.ctx.logger.debug(`[auth-test] reditect_uri - ${redirect_uri}, openid - ${openid}`);
+    assert(redirect_uri, '回调地址不能为空');
+    assert(openid, 'openid不能为空');
+    if (openid) {
+      console.log('redirect_uri-->' + redirect_uri);
+      const user = await this.ctx.service.user.findByOpenid(openid);
+      if (type === '0') {
+        // 通过openid取得用户信息
+        if (user) {
+          const token = await this.ctx.service.login.createJwt(user);
+          const to_uri = urljoin(redirect_uri, `?token=${token}`);
+          // TODO: 重定性页面
+          console.log('to_uri000-->' + to_uri);
+          this.ctx.redirect(to_uri);
+
+        } else {
+          console.log('rrr0000--->' + redirect_uri);
+          const touri = `${this.app.config.baseUrl}/mobile/error`;
+          const to_uri = urljoin(touri, `?openid=${openid}`);
+          // TODO: 重定性页面
+          this.ctx.redirect(to_uri);
+        }
+      } else if (type === '1') {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&uid=${uid}&type=${type}&qrcode=${qrcode}`);
+        // TODO: 重定性页面
+        console.log('1111---?' + to_uri);
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+}
+
+module.exports = WeixinController;

+ 21 - 0
app/model/role.js

@@ -0,0 +1,21 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 权限表
+const RoleSchema = {
+  code: { type: String, required: false, maxLength: 200 }, // 权限CODE
+  role_name: { type: String, required: true, maxLength: 64 }, // 权限名称
+  url: { type: String, required: false }, // 权限路由
+};
+
+
+const schema = new Schema(RoleSchema, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ code: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Role', schema, 'role');
+};

+ 15 - 1
app/router.js

@@ -9,7 +9,9 @@ module.exports = app => {
   // 用户表设置路由
   router.resources('user', '/api/onlive/user', controller.user); // index、create、show、destroy
   router.post('user', '/api/onlive/user/update/:id', controller.user.update);
-
+  router.post('user', '/api/onlive/user/uppasswd', controller.user.uppasswd);
+  router.post('/api/onlive/user/updatebyuid/:id', controller.user.updatebyuid);
+  router.post('/api/onlive/user/bind', controller.user.bind);
 
   // 房间表设置路由
   router.resources('room', '/api/onlive/room', controller.room); // index、create、show、destroy
@@ -22,4 +24,16 @@ module.exports = app => {
   // 主播表设置路由
   router.resources('roomuser', '/api/onlive/roomuser', controller.roomuser); // index、create、show、destroy
   router.post('roomuser', '/api/onlive/roomuser/update/:id', controller.roomuser.update);
+
+  // 用户登录
+  router.post('/api/onlive/login', controller.login.login);
+  // 根据token取得用户信息
+  router.post('/api/onlive/token', controller.login.token);
+  // 用户退出登录
+  router.post('/api/onlive/logout', controller.login.destroy);
+
+  // 微信端访问地址
+  router.get('/api/onlive/wxchat', controller.weixin.auth); // 微信登录
+  // 微信端访问地址
+  router.get('/api/onlive/wxchattest', controller.weixin.authTest); // 微信登录测试
 };

+ 89 - 0
app/service/login.js

@@ -0,0 +1,89 @@
+'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');
+const uuid = require('uuid');
+
+class LoginService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'login');
+    this.model = this.ctx.model.User;
+    this.rmodel = this.ctx.model.Role;
+    this.umodel = this.ctx.model.Roomuser;
+  }
+
+  // 用户登录
+  async login(data) {
+    const { phone, passwd } = data;
+    // 根据用户输入的手机号查询其他用户表中是否存在相应数据
+    const user = await this.model.findOne({ phone });
+    // 如果用户不存在抛出异常
+    if (!user) {
+      throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+    }
+    const _user = await this.model.findOne({ phone }, '+passwd');
+    // 将用户输入的密码进行加密并与查询到的用户数据密码相比对
+    const pas = await this.createJwtPwd(passwd);
+    // 如果两个密码不一致抛出异常
+    if (pas !== _user.passwd.secret) {
+      throw new BusinessError(ErrorCode.BAD_PASSWORD);
+    }
+    if (_user.role === '4') {
+      throw new BusinessError(ErrorCode.ACCESS_DENIED);
+    }
+    // 取出用户的类型,根据用户类型返回相应信息
+    const state = uuid();
+    const key = `free:auth:state:${state}`;
+    const _menus = [];
+    for (const elm of user.menus) {
+      const _menu = await this.rmodel.findById({ _id: ObjectId(elm) });
+      if (_menu) {
+        _menus.push({ id: elm, role_name: _menu.role_name, url: _menu.url });
+      }
+    }
+    user.menus = JSON.stringify(_menus);
+    const token = await this.createJwt(user);
+    await this.app.redis.set(key, token, 'EX', 60 * 60 * 24);
+    return { key };
+  }
+
+  // 创建登录Token
+  async createJwtPwd(password) {
+    const { secret, expiresIn, issuer } = this.config.jwt;
+    const token = await jwt.sign(password, secret);
+    return token;
+  }
+
+  // 创建登录Token
+  async createJwt({ id, name, uid, phone, role, menus, remark, openid, deptname }) {
+    const { secret, expiresIn = '1d', issuer = role } = this.config.jwt;
+    const subject = phone;
+    const res = { uid: id, userid: uid, name, phone, role, menus, openid, remark, deptname };
+    const token = await jwt.sign(res, secret, { expiresIn, issuer, subject });
+    return token;
+  }
+
+  // 取得redis内token信息
+  async token({ key }) {
+    assert(key, 'key不能为空');
+    const token = await this.app.redis.get(key);
+    if (!token) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, 'token已经过期');
+    }
+    return { token };
+  }
+
+  // 删除操作
+  async destroy({ key }) {
+    const res = await this.app.redis.del(key);
+    console.log(res);
+    return res;
+  }
+
+
+}
+module.exports = LoginService;

+ 17 - 0
app/service/role.js

@@ -0,0 +1,17 @@
+'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 RoleService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'role');
+    this.model = this.ctx.model.Role;
+  }
+}
+
+module.exports = RoleService;

+ 125 - 0
app/service/user.js

@@ -6,6 +6,7 @@ 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 UserService extends CrudService {
   constructor(ctx) {
@@ -13,6 +14,130 @@ class UserService extends CrudService {
     this.model = this.ctx.model.User;
   }
 
+  // 重写创建方法
+  async create(data) {
+    const { name, phone, passwd } = data;
+    console.log(data);
+    assert(name && phone && passwd, '缺少部分信息项');
+    assert(/^\d{11}$/i.test(phone), 'phone无效');
+    const user = await this.model.findOne({ phone });
+    if (user) {
+      throw new BusinessError(ErrorCode.DATA_EXISTED);
+    }
+    const newdata = data;
+    const pas = await this.createJwtPwd(passwd);
+    newdata.passwd = { secret: pas };
+    const res = await this.model.create(newdata);
+    return res;
+  }
+
+  // 创建登录Token
+  async createJwtPwd(password) {
+    const { secret, expiresIn, issuer } = this.config.jwt;
+    const token = await jwt.sign(password, secret);
+    return token;
+  }
+
+  // 重写修改方法
+  async update({ id }, data) {
+    const { name, phone, passwd, openid, role, menus, remark, uid, deptname } = data;
+    const user = await this.model.findById(id, '+passwd');
+    if (name) {
+      user.name = name;
+    }
+    if (phone) {
+      user.phone = phone;
+    }
+    if (passwd) {
+      const newpasswd = await this.createJwtPwd(passwd);
+      user.passwd = { secret: newpasswd };
+    }
+    if (openid) {
+      user.openid = openid;
+    }
+    if (role) {
+      user.role = role;
+    }
+    if (menus) {
+      user.menus = menus;
+    }
+    if (uid) {
+      user.uid = uid;
+    }
+
+    if (deptname) {
+      user.deptname = deptname;
+    }
+    if (remark) {
+      user.remark = remark;
+    }
+    await user.save();
+  }
+
+  // 用户修改密码
+  async uppasswd(data) {
+    const { id, oldpasswd, newpasswd } = data;
+    assert(id && oldpasswd && newpasswd, '缺少部分信息项');
+    // 根据用户id查询其他用户表中是否存在相应数据
+    const user = await this.model.findById(id, '+passwd');
+    // 如果用户不存在抛出异常
+    if (!user) {
+      throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+    }
+    // 将用户输入的密码进行加密并与查询到的用户数据密码相比对
+    const _oldpasswd = await this.createJwtPwd(oldpasswd);
+    // 如果两个密码不一致抛出异常
+    if (_oldpasswd !== user.passwd.secret) {
+      throw new BusinessError(ErrorCode.BAD_PASSWORD);
+    }
+    const _newpasswd = await this.createJwtPwd(newpasswd);
+    user.passwd = { secret: _newpasswd };
+    await user.save();
+  }
+
+  async querymenus({ id }) {
+    const user = await this.model.findById(id);
+    if (!user) {
+      throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+    }
+    const _menus = [];
+    for (const elm of user.menus) {
+      const _menu = await this.rmodel.findById({ _id: ObjectId(elm) });
+      if (_menu) {
+        _menus.push({ id: elm, role_name: _menu.role_name, url: _menu.url });
+      }
+    }
+    return { id, menus: _menus };
+  }
+
+  // 按条件更新方法
+  async updatebyuid({ id }, data) {
+    const user = await this.model.findOne({ uid: id });
+    if (user) {
+      user.name = data.name;
+      user.deptname = data.deptname;
+    }
+    await user.save();
+  }
+
+  // 按条件更新方法
+  async bind(data) {
+    const user = await this.model.findById(data.uid);
+    if (!user) {
+      throw new BusinessError(ErrorCode.USER_NOT_EXIST);
+    }
+    if (user.openid) {
+      throw new BusinessError(ErrorCode.USER_EXIST);
+    }
+    user.openid = data.openid;
+    return await user.save();
+  }
+
+  async findByOpenid(openid) {
+    const user = await this.model.findOne({ openid });
+    return user;
+  }
+
 }
 
 module.exports = UserService;

+ 226 - 0
app/service/weixin.js

@@ -0,0 +1,226 @@
+'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;
+    let res = await this.httpGet('/api/fetch', { code });
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch open by code fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信认证信息失败');
+    }
+    const { openid } = res;
+
+    // TODO: 获得用户信息
+    res = await this.httpGet('/api.weixin.qq.com/cgi-bin/user/info?lang=zh_CN', { appid: wxapi.appid, openid });
+    // console.debug('res: ', res);
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    return res;
+  }
+
+  // 通过openid获得用户信息
+  async fetchUnionID(openid) {
+    // TODO:参数检查和默认参数处理
+    assert(openid);
+    const appid = 'wxdf3ed83c095be97a';
+    const grant_type = 'client_credential';
+    const secret = '748df7c2a75077a79ae0c971b1638244';
+    // TODO: 获得用户信息
+    const url = 'http://wx.cc-lotus.info/api.weixin.qq.com/cgi-bin/token?appid=' + appid + '&grant_type=' + grant_type + '&secret=' + secret;
+    const res = await this.ctx.curl(url, {
+      method: 'get',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+    });
+    // console.debug('res: ', res);
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    const token = res.access_token;
+    console.log(token);
+    const urlun = 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=' + token + '&openid=' + openid + '&lang=zh_CN';
+    const result = await this.ctx.curl(urlun, {
+      method: 'get',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+    });
+    // console.debug('res: ', res);
+    if (result.errcode && result.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    return result;
+  }
+
+  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 };
+  }
+
+  // 发送微信模板消息
+  async sendTemplateMsg(templateid, openid, first, keyword1, keyword2, remark, tourl) {
+    const url = this.ctx.app.config.sendDirMq + this.ctx.app.config.appid;
+    let _url = '';
+    if (tourl) {
+      _url = this.ctx.app.config.baseUrl + '/api/auth?state=1&redirect_uri=' + this.ctx.app.config.baseUrl + '/classinfo/&type=template&objid=' + tourl;
+    }
+    const requestData = { // 发送模板消息的数据
+      touser: openid,
+      template_id: templateid,
+      url: _url,
+      data: {
+        first: {
+          value: first,
+          color: '#173177',
+        },
+        keyword1: {
+          value: keyword1,
+          color: '#1d1d1d',
+        },
+        keyword2: {
+          value: keyword2,
+          color: '#1d1d1d',
+        },
+        remark: {
+          value: remark,
+          color: '#173177',
+        },
+      },
+    };
+    console.log('templateid---' + templateid);
+    console.log('openid---' + openid);
+    console.log('requestData---' + JSON.stringify(requestData));
+    await this.ctx.curl(url, {
+      method: 'post',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+      data: JSON.stringify(requestData),
+    });
+  }
+}
+
+module.exports = WeixinAuthService;