本文代碼獲取方式:Github,走過路過,歡迎點贊和 star,本人會持續優化該項目html
系列文章傳送門git
在前面的文章中,咱們完成了微信回調服務器基本流程的的打通,但實際業務場景中,用戶給咱們發送的不單單是文本類型的消息,咱們給用戶回覆的也須要支持不一樣的類型。另外,微信對回調消息的響應時間作了規定,不能超過 5 秒,當咱們的業務須要對用戶消息處理較長時間再給予答覆時,得采用另外的手段來進行消息的回覆github
咱們查看 接收普通消息 文檔能夠得知,當用戶發送的消息爲圖片、語音、視頻等媒體類型的消息時,微信給咱們的網關的回調信息中,會有一個 mediaId
redis
微信會將用戶消息以 臨時素材
的形式存儲,並提供 API 讓開發者獲取該媒體文件。經過 獲取臨時素材 文檔能夠發現,除了 mediaId
,咱們還須要一個 access_token
用於請求該接口數據庫
關於 access_token
此文檔 已經作了很是詳細的說明,咱們須要注意如下的信息npm
綜合以上信息,咱們能夠固化出,微信網關調用微信 API 的流程json
咱們先來處理緩存,此處使用 redis 來實現,若沒有 redis,則可使用內存、本地文件或者數據庫的方式。集中實現方式的對比,可參考我以前的 文章api
import { Service } from 'egg';
// 緩存數據,若項目中無 redis,則修改方法,使用內存或數據庫實現
export default class extends Service {
public async get(key: string) {
const { ctx } = this;
return await ctx.app.redis.get(key);
}
public async set(key: string, value: string, expires = 60 * 60) {
const { ctx } = this;
await ctx.app.redis.set(key, value);
await ctx.app.redis.expire(key, expires);
}
public async expire(key: string, expires = 60 * 60) {
const { ctx } = this;
await ctx.app.redis.expire(key, expires);
}
public async delete(key: string) {
const { ctx } = this;
await ctx.app.redis.del(key);
}
}
複製代碼
再把咱們以前的 service/wechat 拆分一層,把 encode 和 decode 方法放入 service/wechat/adapter 中,而後建立 service/wechat/util 用於實現輔助方法,咱們將獲取當前有效的 access_token 的方法放入其中緩存
import { Service } from 'egg';
export default class extends Service {
public async getAccessToken() {
const { ctx } = this;
const config = ctx.app.config.wechat;
const cacheKey = `wechat_token_${config.appId}`;
let token = await ctx.service.utils.cache.get(cacheKey);
if (!token) {
const result = await ctx.app.curl(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appId}&secret=${config.secret}`,
{
dataType: 'json'
}
);
if (result.data.errcode) {
throw new Error('get mp access_token error');
}
token = result.data.access_token;
if (!token) {
throw new Error('get mp access_token error');
}
await ctx.service.utils.cache.set(cacheKey, token, 60 * 60);
}
return { token };
}
}
複製代碼
咱們把媒體文件處理的方法單獨出來,實現獲取媒體文件流的方法安全
import { Service } from 'egg';
export default class extends Service {
public async getMedia(mediaId) {
const { ctx } = this;
const { token } = await ctx.service.wechat.util.getAccessToken();
const result = await ctx.app.curl(
`https://api.weixin.qq.com/cgi-bin/media/get?access_token=${token}&media_id=${mediaId}`,
{
timeout: 30000
}
);
return result;
}
}
複製代碼
最後,在 controller 中調用獲取媒體文件的方法,便可獲得媒體文件的文件流,並根據業務需求進行處理,例如轉存到本地,或者自有文件服務器,以便於將來能夠被訪問到(微信只會存儲臨時媒體文件 3 天)
// 當用戶消息類型爲圖片、語音、視頻類型時,微信回調一個 mediaId
if (message.mediaId) {
// 獲取媒體文件流
const buffer = await ctx.service.wechat.media.getMedia(message.mediaId);
// 對媒體文件流作一些處理,例如轉存到本地
console.log(buffer.data);
}
複製代碼
另外,微信回調消息中,也有用戶的一些事件消息,詳情可見 接收事件推送,只須要在 decode 函數中把解析到的參數添加到咱們自定義的 message 對象中便可,在此不作贅述
在前面的文章中,咱們已經實現了被動回覆文本類型的消息。在 被動回覆用戶消息 文檔中,咱們能夠發現,當咱們須要回覆圖片、語音、視頻等類型的消息時,須要把咱們的媒體文件上傳到微信服務器並獲取一個 mediaId
,再組裝響應數據
上傳媒體文件的文檔參考 新增臨時素材
咱們先安裝一個 form 請求輔助包 formstream
npm i --save formstream
複製代碼
而後在媒體文件的 service 中增長上傳文件的方法
/** * 上傳媒體文件 * @param type 媒體類型,參考微信官方文檔 * @param buffer 媒體文件流 * @param filename 文件名 * @param contentType content-type */
public async uploadMedia(type, buffer, filename, contentType) {
const { ctx } = this;
const { token } = await ctx.service.wechat.util.getAccessToken();
const form = formstream();
form.buffer('media', buffer, filename, contentType);
const result = await ctx.app.curl(
`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`,
{
method: 'POST',
headers: form.headers(),
stream: form,
dataType: 'json',
timeout: 30000
}
);
return result;
}
複製代碼
接着,咱們須要完善一下 encode 方法,來支持組裝多種類型的響應消息
// 獲取原始回覆 xml
private compileReply(info) {
return ejs.compile(
[
'<xml>',
'<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>',
'<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>',
'<CreateTime><%=createTime%></CreateTime>',
'<MsgType><![CDATA[<%=msgType%>]]></MsgType>',
'<% if (msgType === "news") { %>',
'<ArticleCount><%=content.length%></ArticleCount>',
'<Articles>',
'<% content.forEach(function(item){ %>',
'<item>',
'<Title><![CDATA[<%-item.title%>]]></Title>',
'<Description><![CDATA[<%-item.description%>]]></Description>',
'<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic %>]]></PicUrl>',
'<Url><![CDATA[<%-item.url%>]]></Url>',
'</item>',
'<% }); %>',
'</Articles>',
'<% } else if (msgType === "music") { %>',
'<Music>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>',
'<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>',
'</Music>',
'<% } else if (msgType === "voice") { %>',
'<Voice>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Voice>',
'<% } else if (msgType === "image") { %>',
'<Image>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'</Image>',
'<% } else if (msgType === "video") { %>',
'<Video>',
'<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>',
'<Title><![CDATA[<%-content.title%>]]></Title>',
'<Description><![CDATA[<%-content.description%>]]></Description>',
'</Video>',
'<% } else { %>',
'<Content><![CDATA[<%-content%>]]></Content>',
'<% } %>',
'</xml>'
].join('')
)(info);
}
複製代碼
最後,在 controller 中根據須要響應用戶消息
例如,咱們實現用戶媒體類型消息的回覆,用戶發圖片、語音就原樣回覆過去
// 正常狀況下,服務端須要先將媒體文件經過 ctx.service.wechat.media.uploadMedia 方法上傳至微信服務器獲取 mediaId
// eg: 將用戶原始消息直接回復
if (message.msgType === 'text') {
ctx.body = await ctx.service.wechat.adapter.encodeMsg({
type: 'text',
content: message.content
});
} else {
ctx.body = await ctx.service.wechat.adapter.encodeMsg({
type: message.msgType,
content: {
mediaId: message.mediaId
}
});
}
複製代碼
若是是回覆圖文消息,則變成
// eg: 圖文消息回覆
ctx.body = await ctx.service.wechat.adapter.encodeMsg({
type: 'news',
content: [
{
title: '掘金社區',
description: '一塊兒來學習吧',
url: 'https://juejin.cn'
}
]
});
複製代碼
在有些場景下,咱們並非能夠及時響應用戶的消息,而是要通過必定時間的處理(超過 5 秒),才能給用戶響應,這個時候就須要主動給用戶發送消息
參考 官方文檔,咱們能夠在收到用戶消息的 48 小時內,調用客服消息接口給用戶主動發送消息,類型包括:文本、圖片、語音、視頻、圖文等
咱們新建一個 message 文件用於處理消息發送的邏輯,實現常見的客服消息發送方法
import { Service } from 'egg';
import { IMessage } from '../../interface/message';
export interface IMenuMsg {
headContent?: string;
tailContent?: string;
list: { id: number; content: string }[];
}
export default class extends Service {
// 調用微信客服消息 API
public async sendMsg(data: any) {
const { ctx } = this;
const { token } = await this.service.wechat.util.getAccessToken();
const result = await ctx.app.curl(
`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`,
{
method: 'POST',
contentType: 'json',
dataType: 'json',
data
}
);
return result;
}
// 文本消息
public async sendTextMsg(message: IMessage) {
await this.sendMsg({
touser: message.userId,
msgtype: 'text',
text: {
content: message.content
}
});
}
// 圖片消息
public async sendImageMsg(message: IMessage) {
await this.sendMsg({
touser: message.userId,
msgtype: 'image',
image: {
media_id: message.mediaId
}
});
}
// 語音消息
public async sendVoiceMsg(message: IMessage) {
await this.sendMsg({
touser: message.userId,
msgtype: 'voice',
voice: {
media_id: message.mediaId
}
});
}
// 視頻消息
public async sendVideoMsg(message: IMessage) {
await this.sendMsg({
touser: message.userId,
msgtype: 'video',
video: {
media_id: message.mediaId
}
});
}
// 圖文消息
public async sendNewsMsg(message: IMessage) {
await this.sendMsg({
touser: message.userId,
msgtype: 'news',
news: {
articles: message.articles
}
});
}
// 菜單消息
public async sendMenuMsg(message: IMessage, data: IMenuMsg) {
await this.sendMsg({
touser: message.userId,
msgtype: 'msgmenu',
msgmenu: {
head_content: data.headContent + '\n',
list: data.list,
tail_content: '\n' + data.tailContent
}
});
}
}
複製代碼
在 controller 中,先給予微信一個空值響應,間隔一段時間後,再發送消息給用戶
// 先給微信服務器一個響應
ctx.body = await ctx.service.wechat.adapter.encodeMsg('');
// 模擬一段時間的處理後,給用戶主動推送消息
setTimeout(() => {
ctx.service.wechat.message.sendTextMsg({
...message,
content: '哈嘍,這是五秒後的回覆'
});
}, 5000);
複製代碼
同時,爲了給用戶一個更好的體驗,咱們能夠利用 客服輸入狀態
這個 API,讓用戶在服務器處理的期間,能看到 對方正在輸入...
這樣的提示,而不是空蕩蕩的,覺得公衆號出問題了
// 客服輸入狀態
public async typing(message: IMessage, isTyping = true) {
const { ctx } = this;
const { token } = await this.service.wechat.util.getAccessToken();
const result = await ctx.app.curl(
`https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=${token}`,
{
method: 'POST',
contentType: 'json',
dataType: 'json',
data: {
touser: message.userId,
command: isTyping ? 'Typing' : 'CancelTyping'
}
}
);
return result;
}
複製代碼
// 先給微信服務器一個響應
ctx.body = await ctx.service.wechat.adapter.encodeMsg('');
ctx.service.wechat.message.typing(message, true);
// 模擬一段時間的處理後,給用戶主動推送消息
setTimeout(() => {
ctx.service.wechat.message.typing(message, false);
ctx.service.wechat.message.sendTextMsg({
...message,
content: '哈嘍,這是五秒後的回覆'
});
}, 5000);
複製代碼
更多的消息類型大同小異,你們可自行實現
至此,咱們的服務已經實現瞭解析多種類型的用戶消息,按需被動回覆不一樣類型的消息,而且在必要的時候能夠主動給用戶發送消息。咱們實現了 access_token 的通用處理方式,在將來調用微信提供的 API 時能夠直接複用
在下一篇內容裏,咱們將探索基於公衆號的網頁開發,敬請期待
本文代碼獲取方式:Github