From d91d1f045628734202ef85f829463c34a1968ff9 Mon Sep 17 00:00:00 2001 From: jay Date: Fri, 29 Jul 2022 00:48:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20wechat=20service=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BC=A0appid=E5=92=8Csecret,=E9=80=82?= =?UTF-8?q?=E5=90=88=E5=A4=9A=E5=85=AC=E4=BC=97=E5=8F=B7=E5=A4=9A=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E6=83=85=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + lib/miniprogram.service.ts | 7 +- lib/wechat.service.ts | 201 ++++++++++++++++++++------------ tests/e2e/cache-adapter.spec.ts | 4 +- tests/e2e/jsapi.spec.ts | 20 ++-- 5 files changed, 145 insertions(+), 88 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2346284..5d6b612 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "eslint.enable": true, "cSpell.words": [ "commitlint", + "errcode", "jsapi", "jssdk", "nestjs", diff --git a/lib/miniprogram.service.ts b/lib/miniprogram.service.ts index 680870f..a46e23f 100644 --- a/lib/miniprogram.service.ts +++ b/lib/miniprogram.service.ts @@ -22,8 +22,11 @@ export class MiniProgramService { * @link https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html */ public async code2Session (code: string, appId?: string, secret?: string): Promise { - appId = appId || this.options?.appId; - secret = secret || this.options?.secret; + if (!appId || !secret) { + appId = this.options?.appId; + secret = this.options?.secret; + } + if (!appId || !secret) { throw new Error(`${MiniProgramService.name}': No appId or secret.`); } else { diff --git a/lib/wechat.service.ts b/lib/wechat.service.ts index 099a4ce..08c6f6b 100644 --- a/lib/wechat.service.ts +++ b/lib/wechat.service.ts @@ -21,14 +21,31 @@ import { WePayService } from './wepay.service'; @Injectable() export class WeChatService { + /** + * key_access_token + * @static + * @memberof WeChatService + */ public static KEY_ACCESS_TOKEN = 'key_access_token'; + /** + * key_ticket + * @static + * @memberof WeChatService + */ public static KEY_TICKET = 'key_ticket'; protected _cacheAdapter: ICache = new MapCache(); + /** + * MiniProgram Service Namespace + * + * @type {MiniProgramService} + * @memberof WeChatService + */ public mp: MiniProgramService; + /** - * WePay Service + * WePay Service Namespace * @type {WePayService} * @memberof WeChatService */ @@ -51,9 +68,19 @@ export class WeChatService { } } + /** + * + * @deprecated + * @memberof WeChatService + */ public get config () { return this.options; } + + /** + * @deprecated + * @memberof WeChatService + */ public set config (options: WeChatModuleOptions) { this.options = options; } @@ -69,48 +96,40 @@ export class WeChatService { * 错误返回 * {"errcode":40013,"errmsg":"invalid appid"} * + * @param _appId + * @param _secret * @tutorial https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html * @returns */ - public async getAccountAccessToken (): Promise { - return new Promise((resolve, reject) => { - if (!this.options.appId || !this.options.secret) { - return reject(new Error(`${WeChatService.name}: No appId or secret.`)); + public async getAccountAccessToken (_appId?: string, _secret?: string): Promise { + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${secret}`; + const res = await axios.get(url); + const ret = res && res.data; + if (ret.access_token) { + // eslint-disable-next-line camelcase + ret.expires_in += (Date.now() / 1000 - 120); + if (this.cacheAdapter) { + this.cacheAdapter.set(`${WeChatService.KEY_ACCESS_TOKEN}_${appId}`, ret, 7100); } - const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.options.appId}&secret=${this.options.secret}`; - axios.get(url).then((res) => { - const ret = res && res.data; - - if ((ret as AccountAccessTokenResult).access_token) { - // 正确返回 - // eslint-disable-next-line camelcase - (ret as AccountAccessTokenResult).expires_in += (Date.now() / 1000 - 120); - - this.cacheAdapter.set(WeChatService.KEY_ACCESS_TOKEN, ret, 7100); - } - resolve(ret); - }).catch((err) => { - reject(err); - }); - }); + } + return ret; } /** + * 读取access token的逻辑封装 * - * 实例读取access token的逻辑封装 - * + * @param _appId + * @param _secret * @returns */ - private async getToken (): Promise { + private async getToken (_appId?: string, _secret?: string): Promise { let accessToken; - - // get token from cache - const cache = await this.cacheAdapter.get(WeChatService.KEY_ACCESS_TOKEN); + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const cache = await this.cacheAdapter.get(`${WeChatService.KEY_ACCESS_TOKEN}_${appId}`); if (!this.checkAccessToken(cache)) { - // expire, request a new one. - const ret = await this.getAccountAccessToken(); - if (!(ret instanceof Error) && ret.access_token) { - // got + const ret = await this.getAccountAccessToken(appId, secret); + if (ret && ret.access_token) { accessToken = ret.access_token; } } else { @@ -119,13 +138,22 @@ export class WeChatService { return accessToken; } - private async getTicket (): Promise { - let ticket; - const cache = await this.cacheAdapter.get(WeChatService.KEY_TICKET); + /** + * + * 读取JS-SDK Ticket逻辑封装 + * + * @param _appId + * @param _secret + * @returns + */ + private async getTicket (_appId?: string, _secret?: string): Promise { + let ticket = ''; + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const cache = await this.cacheAdapter.get(`${WeChatService.KEY_TICKET}_${appId}`); if (!this.checkTicket(cache)) { // expire, request a new ticket - const ret = await this.getJSApiTicket(); - if (!(ret instanceof Error) && ret.errcode === 0) { + const ret = await this.getJSApiTicket(appId, secret); + if (ret && ret.errcode === 0) { ticket = ret.ticket; } } else { @@ -143,56 +171,74 @@ export class WeChatService { * 错误返回 * * @tutorial https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62 - * @param accessToken + * @param _appId + * @param _secret * @returns */ - public async getJSApiTicket (): Promise { - const accessToken = await this.getToken(); + public async getJSApiTicket (_appId?: string, _secret?: string): Promise { + + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const accessToken = await this.getToken(appId, secret); + if (!accessToken) { // finally, there was no access token. - return new Error(`${WeChatService.name}: No access token of official account.`); + throw new Error(`${WeChatService.name}: No access token of official account.`); } - try { - const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`; - const ret = await axios.get(url); - if (ret.data.errcode === 0) { - // eslint-disable-next-line camelcase - (ret.data as TicketResult).expires_in += (Date.now() / 1000 - 120); - this.cacheAdapter.set(WeChatService.KEY_TICKET, ret.data, 7100); + + const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`; + const ret = await axios.get(url); + if (ret.data.errcode === 0) { + // eslint-disable-next-line camelcase + ret.data.expires_in += (Date.now() / 1000 - 120); + if (this.cacheAdapter) { + this.cacheAdapter.set(`${WeChatService.KEY_TICKET}_${appId}`, ret.data, 7100); } - return ret.data; - } catch (error) { - return (error as Error); } + return ret.data; } /** * - * 对URL进行签名 + * 对URL进行权限签名 + * sign a url * + * @param {String} url url for signature + * @throws {Error} + * @link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62 + */ + public async jssdkSignature (url: string): Promise; + /** + * + * 对URL进行权限签名 * sign a url * - * @param url url for signature - * @returns + * @param {String} url + * @param {String} appId + * @param {String} secret + * @throws {Error} + * @link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62 */ - public async jssdkSignature (url: string): Promise { + public async jssdkSignature (url: string, appId: string, secret:string): Promise; + public async jssdkSignature (url: string, _appId?: string, _secret?: string): Promise { if (!url) { - return new Error(`${WeChatService.name}: JS-SDK signature must provide url param.`); + throw new Error(`${WeChatService.name}: JS-SDK signature must provide url param.`); } - const ticket = await this.getTicket(); + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const ticket = await this.getTicket(appId, secret); if (!ticket) { // finally, there waw no ticket. - return new Error(`${WeChatService.name}: JS-SDK could NOT get a ticket.`); + throw new Error(`${WeChatService.name}: JS-SDK could NOT get a ticket.`); } + const timestamp = Math.floor(Date.now() / 1000); const nonceStr = createNonceStr(16); const signStr = 'jsapi_ticket=' + ticket + '&noncestr=' + nonceStr + '×tamp=' + timestamp + '&url=' + url; const signature = createHash('sha1').update(signStr).digest('hex'); return { - appId: this.options.appId, + appId, nonceStr, timestamp, signature, @@ -221,6 +267,19 @@ export class WeChatService { return ticket && ticket.expires_in > (Date.now() / 1000); } + private chooseAppIdAndSecret (appId?: string, secret?: string): { appId: string, secret: string} { + let ret; + if (!appId || !secret) { + ret = { appId: this.options?.appId, secret: this.options?.secret }; + } else { + ret = { appId, secret }; + } + if (!ret.appId || !ret.secret) { + throw new Error(`${WeChatService.name}: No appId or secret.`); + } + return ret; + } + /** * * 通过code换取网页授权access_token @@ -247,23 +306,17 @@ export class WeChatService { * * {"errcode":40013,"errmsg":"iinvalid appid, rid: 61c82e61-2e62fb72-467cb9ec"} * - * @param code - * @param code + * @param {String} code + * @param {String} appId + * @param {String} secret * @returns * @tutorial https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#1 */ - public async getAccessTokenByCode (code: string): Promise { - if (!this.options.appId || !this.options.secret) { - return new Error(`${WeChatService.name}': No appId or secret.`); - } else { - const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.options.appId}&secret=${this.options.secret}&code=${code}&grant_type=authorization_code`; - try { - const ret = await axios.get(url); - return ret.data; - } catch (error) { - return (error as Error); - } - } + public async getAccessTokenByCode (code: string, _appId?: string, _secret?: string): Promise { + const { appId, secret } = this.chooseAppIdAndSecret(_appId, _secret); + const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${secret}&code=${code}&grant_type=authorization_code`; + const ret = await axios.get(url); + return ret.data; } /** @@ -274,8 +327,8 @@ export class WeChatService { * @returns * @tutorial https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#5 */ - public async sendTemplateMessage (message: TemplateMessage): Promise { - const token = await this.getToken(); + public async sendTemplateMessage (message: TemplateMessage, appId?: string, secret?: string): Promise { + const token = await this.getToken(appId, secret); const url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`; try { const ret = await axios.post(url, message); diff --git a/tests/e2e/cache-adapter.spec.ts b/tests/e2e/cache-adapter.spec.ts index 56fc664..ca73476 100644 --- a/tests/e2e/cache-adapter.spec.ts +++ b/tests/e2e/cache-adapter.spec.ts @@ -24,8 +24,8 @@ describe('Test module register', () => { jest.spyOn(service.cacheAdapter, 'set'); jest.spyOn(service.cacheAdapter, 'get'); - expect(service.cacheAdapter.remove(WeChatService.KEY_TICKET)).toBeTruthy(); - expect(service.cacheAdapter.remove(WeChatService.KEY_ACCESS_TOKEN)).toBeTruthy(); + expect(service.cacheAdapter.remove(`${WeChatService.KEY_TICKET}_${service.config.appId}`)).toBeTruthy(); + expect(service.cacheAdapter.remove(`${WeChatService.KEY_ACCESS_TOKEN}_${service.config.appId}`)).toBeTruthy(); // to sign a url and use the ticket in cache let sign = await service.jssdkSignature(process.env.TEST_JSSDK_URL || '').catch(err => err); diff --git a/tests/e2e/jsapi.spec.ts b/tests/e2e/jsapi.spec.ts index ea63f4e..fcf6694 100644 --- a/tests/e2e/jsapi.spec.ts +++ b/tests/e2e/jsapi.spec.ts @@ -38,7 +38,7 @@ describe('jsapi', () => { // eslint-disable-next-line camelcase expires_in: Date.now() / 1000 + 10000, }; - service.cacheAdapter.set(WeChatService.KEY_ACCESS_TOKEN, incorrectToken); + service.cacheAdapter.set(`${WeChatService.KEY_ACCESS_TOKEN}_${service.config.appId}`, incorrectToken); const ret = await service.getJSApiTicket(); expect(ret).toHaveProperty('errcode', 40001); }); @@ -88,9 +88,9 @@ describe('jsapi', () => { const ticket = retTicket.ticket; // cache must be the same - expect(accessToken).toEqual((await service.cacheAdapter.get(WeChatService.KEY_ACCESS_TOKEN)).access_token); + expect(accessToken).toEqual((await service.cacheAdapter.get(`${WeChatService.KEY_ACCESS_TOKEN}_${process.env.TEST_APPID}`)).access_token); expect(service.cacheAdapter.get).toBeCalledTimes(2); - expect(ticket).toEqual((await service.cacheAdapter.get(WeChatService.KEY_TICKET)).ticket); + expect(ticket).toEqual((await service.cacheAdapter.get(`${WeChatService.KEY_TICKET}_${process.env.TEST_APPID}`)).ticket); expect(service.cacheAdapter.get).toBeCalledTimes(3); // to sign a url and use the ticket in cache @@ -106,20 +106,20 @@ describe('jsapi', () => { expect(sign.signature).toBeTruthy(); // throw error when no url - expect(service.jssdkSignature('')).resolves.toThrow(); + expect(service.jssdkSignature('')).rejects.toThrowError(new Error(`${WeChatService.name}: JS-SDK signature must provide url param.`)); // request an access token and a ticket expect(axios.get).toBeCalledTimes(2); // make the ticket and the token expire - const ticketInCache = await service.cacheAdapter.get(WeChatService.KEY_TICKET); + const ticketInCache = await service.cacheAdapter.get(`${WeChatService.KEY_TICKET}_${process.env.TEST_APPID}`); // eslint-disable-next-line camelcase ticketInCache.expires_in -= 10800; - service.cacheAdapter.set(WeChatService.KEY_TICKET, ticketInCache); - const tokenInCache = await service.cacheAdapter.get(WeChatService.KEY_ACCESS_TOKEN); + service.cacheAdapter.set(`${WeChatService.KEY_TICKET}_${process.env.TEST_APPID}`, ticketInCache); + const tokenInCache = await service.cacheAdapter.get(`${WeChatService.KEY_ACCESS_TOKEN}_${process.env.TEST_APPID}`); // eslint-disable-next-line camelcase tokenInCache.expires_in -= 10800; - service.cacheAdapter.set(WeChatService.KEY_ACCESS_TOKEN, tokenInCache); + service.cacheAdapter.set(`${WeChatService.KEY_ACCESS_TOKEN}_${process.env.TEST_APPID}`, tokenInCache); // now they are expired @@ -144,10 +144,10 @@ describe('jsapi', () => { // eslint-disable-next-line camelcase ticketInCache.expires_in += 10800; - service.cacheAdapter.set(WeChatService.KEY_TICKET, ticketInCache); + service.cacheAdapter.set(`${WeChatService.KEY_TICKET}_${process.env.TEST_APPID}`, ticketInCache); // eslint-disable-next-line camelcase tokenInCache.expires_in += 10800; - service.cacheAdapter.set(WeChatService.KEY_ACCESS_TOKEN, tokenInCache); + service.cacheAdapter.set(`${WeChatService.KEY_ACCESS_TOKEN}_${process.env.TEST_APPID}`, tokenInCache); // now token and ticket are valid const message: TemplateMessage = {