weixin.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. 'use strict';
  2. const assert = require('assert');
  3. const uuid = require('uuid');
  4. const _ = require('lodash');
  5. const { BusinessError, ErrorCode } = require('naf-core').Error;
  6. const jwt = require('jsonwebtoken');
  7. const { AxiosService } = require('naf-framework-mongoose/lib/service');
  8. class WeixinAuthService extends AxiosService {
  9. constructor(ctx) {
  10. super(ctx, {}, _.get(ctx.app.config, 'wxapi'));
  11. }
  12. // 通过认证码获得用户信息
  13. async fetch(code) {
  14. // TODO:参数检查和默认参数处理
  15. assert(code);
  16. const { wxapi } = this.app.config;
  17. const res = await this.httpGet('http://api.jilinjobswx.cn/api/fetch', { code });
  18. console.log(res);
  19. if (res.errcode && res.errcode !== 0) {
  20. this.ctx.logger.error(`[WeixinAuthService] fetch open by code fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
  21. throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信认证信息失败');
  22. }
  23. this.ctx.logger.info(`auth=>${JSON.stringify(res)}`);
  24. // const reqUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token';
  25. // const params = {
  26. // appid: wxapi.appid,
  27. // secret: wxapi.appSecret,
  28. // code,
  29. // grant_type: 'authorization_code',
  30. // };
  31. // const res = await this.httpGet(reqUrl, params);
  32. // if (res.errcode && res.errcode !== 0) {
  33. // this.ctx.logger.error(`[WeixinAuthService] fetch open by code fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
  34. // throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信认证信息失败');
  35. // }
  36. // const { openid } = res;
  37. return res;
  38. }
  39. // 通过openid获得用户信息
  40. async fetchUnionID(openid) {
  41. // TODO:参数检查和默认参数处理
  42. assert(openid);
  43. const appid = 'wxdf3ed83c095be97a';
  44. const grant_type = 'client_credential';
  45. const secret = '748df7c2a75077a79ae0c971b1638244';
  46. // TODO: 获得用户信息
  47. const url = 'http://wx.cc-lotus.info/api.weixin.qq.com/cgi-bin/token?appid=' + appid + '&grant_type=' + grant_type + '&secret=' + secret;
  48. const res = await this.ctx.curl(url, {
  49. method: 'get',
  50. headers: {
  51. 'content-type': 'application/json',
  52. },
  53. dataType: 'json',
  54. });
  55. // console.debug('res: ', res);
  56. if (res.errcode && res.errcode !== 0) {
  57. this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
  58. throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
  59. }
  60. const token = res.access_token;
  61. const urlun = 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=' + token + '&openid=' + openid + '&lang=zh_CN';
  62. const result = await this.ctx.curl(urlun, {
  63. method: 'get',
  64. headers: {
  65. 'content-type': 'application/json',
  66. },
  67. dataType: 'json',
  68. });
  69. // console.debug('res: ', res);
  70. if (result.errcode && result.errcode !== 0) {
  71. this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
  72. throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
  73. }
  74. return result;
  75. }
  76. async createJwt({ openid, nickname, subscribe }) {
  77. const { secret, expiresIn = '1d', issuer = 'weixin' } = this.config.jwt;
  78. const subject = openid;
  79. const userinfo = { nickname, subscribe };
  80. const token = await jwt.sign(userinfo, secret, { expiresIn, issuer, subject });
  81. return token;
  82. }
  83. /**
  84. * 创建二维码
  85. * 随机生成二维码,并保存在Redis中,状态初始为pending
  86. * 状态描述:
  87. * pending - 等待扫码
  88. * consumed - 使用二维码登录完成
  89. * scand:token - Jwt登录凭证
  90. */
  91. async createQrcode() {
  92. const qrcode = uuid();
  93. const key = `visit:qrcode:group:${qrcode}`;
  94. await this.app.redis.set(key, 'pending', 'EX', 600);
  95. return qrcode;
  96. }
  97. /**
  98. * 创建二维码
  99. * 生成群二维码
  100. * 状态描述:
  101. * pending - 等待扫码
  102. * consumed - 使用二维码登录完成
  103. * scand:token - Jwt登录凭证
  104. */
  105. async createQrcodeGroup({ groupid }) {
  106. const { authUrl = this.ctx.path } = this.app.config;
  107. let backUrl;
  108. if (authUrl.startsWith('http')) {
  109. backUrl = encodeURI(`${authUrl}?state=${groupid}`);
  110. } else {
  111. backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${authUrl}?state=${groupid}`);
  112. }
  113. return backUrl;
  114. }
  115. /**
  116. * 扫码登录确认
  117. */
  118. async scanQrcode({ qrcode, token }) {
  119. assert(qrcode, 'qrcode不能为空');
  120. assert(token, 'token不能为空');
  121. const key = `smart:qrcode:login:${qrcode}`;
  122. const status = await this.app.redis.get(key);
  123. if (!status) {
  124. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  125. }
  126. if (status !== 'pending') {
  127. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
  128. }
  129. // 验证Token
  130. const { secret } = this.config.jwt;
  131. const decoded = jwt.verify(token, secret, { issuer: 'weixin' });
  132. this.ctx.logger.debug(`[weixin] qrcode login - ${decoded}`);
  133. // TODO: 修改二维码状态,登录凭证保存到redis
  134. await this.app.redis.set(key, `scaned:${token}`, 'EX', 600);
  135. // TODO: 发布扫码成功消息
  136. const { mq } = this.ctx;
  137. const ex = 'qrcode.login';
  138. if (mq) {
  139. await mq.topic(ex, qrcode, 'scaned', { durable: true });
  140. } else {
  141. this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
  142. }
  143. }
  144. // 使用二维码换取登录凭证
  145. async qrcodeLogin(qrcode) {
  146. assert(qrcode, 'qrcode不能为空');
  147. const key = `smart:qrcode:login:${qrcode}`;
  148. const val = await this.app.redis.get(key);
  149. if (!val) {
  150. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  151. }
  152. const [status, token] = val.split(':', 2);
  153. if (status !== 'scaned' || !token) {
  154. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
  155. }
  156. // TODO: 修改二维码状态
  157. await this.app.redis.set(key, 'consumed', 'EX', 600);
  158. return { token };
  159. }
  160. // 检查二维码状态
  161. async checkQrcode(qrcode) {
  162. assert(qrcode, 'qrcode不能为空');
  163. const key = `smart:qrcode:login:${qrcode}`;
  164. const val = await this.app.redis.get(key);
  165. if (!val) {
  166. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  167. }
  168. const [status] = val.split(':', 2);
  169. return { status };
  170. }
  171. // 发送微信模板消息
  172. async sendTemplateMsg(templateid, openid, first, keyword1, keyword2, remark, tourl) {
  173. const { wxapi } = this.ctx.app.config;
  174. const url = wxapi.sendDirMq + wxapi.appid;
  175. let _url = '';
  176. if (tourl) {
  177. // 通过openid取得用户基本信息
  178. const user = await this.ctx.model.User.findOne({ openid });
  179. const newdata = { openid, name: user.name, title: first, detail: keyword1, date: keyword2, remark, status: '0' };
  180. const res = await this.ctx.model.Message.create(newdata);
  181. // 需要回执的时候进行保存处理
  182. _url = this.ctx.app.config.baseUrl + '/api/train/auth?state=1&redirect_uri=' + this.ctx.app.config.baseUrl + '/classinfo/&type=9&objid=' + tourl + '&msgid=' + res.id;
  183. }
  184. const requestData = {
  185. // 发送模板消息的数据
  186. touser: openid,
  187. template_id: templateid,
  188. url: _url,
  189. data: {
  190. first: {
  191. value: first,
  192. color: '#173177',
  193. },
  194. keyword1: {
  195. value: keyword1,
  196. color: '#1d1d1d',
  197. },
  198. keyword2: {
  199. value: keyword2,
  200. color: '#1d1d1d',
  201. },
  202. remark: {
  203. value: remark,
  204. color: '#173177',
  205. },
  206. },
  207. };
  208. try {
  209. await this.ctx.curl(url, {
  210. method: 'post',
  211. headers: {
  212. 'content-type': 'application/json',
  213. },
  214. dataType: 'json',
  215. data: JSON.stringify(requestData),
  216. });
  217. } catch (error) {
  218. console.error(error);
  219. console.error('微信发送消息失败');
  220. }
  221. }
  222. // 发送微信模板消息自定义消息
  223. async sendTemplateDesign(templateid, openid, first, keyword1, keyword2, remark, tourl) {
  224. const { wxapi } = this.ctx.app.config;
  225. const url = wxapi.sendDirMq + wxapi.appid;
  226. if (process.env.NODE_ENV === 'development') openid = 'ocPqjswkUejZHq2ANriNrFFC7A3I';
  227. const requestData = {
  228. // 发送模板消息的数据
  229. touser: openid,
  230. template_id: templateid,
  231. url: tourl,
  232. data: {
  233. first: {
  234. value: first,
  235. color: '#173177',
  236. },
  237. keyword1: {
  238. value: keyword1,
  239. color: '#1d1d1d',
  240. },
  241. keyword2: {
  242. value: keyword2,
  243. color: '#1d1d1d',
  244. },
  245. remark: {
  246. value: remark,
  247. color: '#173177',
  248. },
  249. },
  250. };
  251. try {
  252. const res = await this.ctx.curl(url, {
  253. method: 'post',
  254. headers: {
  255. 'content-type': 'application/json',
  256. },
  257. dataType: 'json',
  258. data: JSON.stringify(requestData),
  259. });
  260. } catch (error) {
  261. console.error(error);
  262. console.error('微信发送消息失败');
  263. }
  264. }
  265. // 小程序登录
  266. async appAuth(js_code) {
  267. const { wxapp } = this.app.config;
  268. if (!wxapp) return;
  269. let url = 'https://api.weixin.qq.com/sns/jscode2session';
  270. let query = `?js_code=${js_code}`;
  271. const keys = Object.keys(wxapp);
  272. for (const key of keys) {
  273. query = `${query}&${key}=${wxapp[key]}`;
  274. }
  275. url = `${url}${query}`;
  276. const res = await this.ctx.curl(url, {
  277. method: 'get',
  278. headers: {
  279. 'content-type': 'application/json',
  280. },
  281. dataType: 'json',
  282. });
  283. const { openid } = res.data;
  284. if (!openid) throw new BusinessError(ErrorCode.BUSINESS, '未获取到openid', '未获取到openid');
  285. return openid;
  286. }
  287. }
  288. module.exports = WeixinAuthService;