node.js 做爲服務器node
微信公衆號(訂閱號)git
給我的、企業、組織 提供業務服務和用戶管理能力的全新服務平臺web
訂閱號 與 服務號 開發模式 是同樣的express
註冊npm
接口: 網址 url小程序
無需申請公衆帳號便可在測試帳號中測試全部高級接口數組
1、買域名服務器
2、使得 開發服務器地址 萬維網能訪問微信
開啓 node.js 寫好的服務器併發
雙擊啓動 ngrok 客戶端(ngrok 內網傳透)
ngrok http 3000
2. 驗證消息來自於服務器(只有驗證成功了,才能繼續開發。若是步驟無誤,多提交幾回,總會成功)
// 1. 定義本身的 測試號及接口 配置信息
const myWeChat= {
token: 'atguiguHTML0920',
appID: 'wxc8e92f7aada0fbca0',
appsecret: 'b4054e90b7a8sdasdasd0af50bf7fc3e87'
};
app.use((request, response, next)=>{
// 2. 將 timestamp、nonce、token 三個參數組合成 數組,按照字典序進行排序
const {token} = myWeChat;
const {signature, echostr, timestamp, nonce} = req.query;
const sortArr = [timestamp, nonce, token].sort();
// 3. 將排序後的參數拼接成一個新的字符串,進行 sha1 加密,獲得的就是 signature
const sha1Str = sha1(sortArr.join(""));
// 4. 將 加密後的簽名 和 微信發送來的 signature 進行對比
if(sha1Str === signature){
// 同樣,說明消息來自於服務器,返回 echostr 給微信服務器
response.end(echostr);
}else{
// 不同,說明消息不來自於微信服務器,返回 error 錯誤
response.end('error');
};
next();
});
/* 此時,再次點擊提交,顯示 "配置成功" */
/* 用手機掃描 測試帳號的二維碼 關注,併發送任意消息____能夠看到服務器接受到的數據*/
3. 驗證服務器有效性 的模塊化 源代碼
config/index.js
module.exports = { token: 'FinnKou', appID: 'wxba59db235745154d22cd32d', appsecret: '62a454d75919545d2f276680fcb6148d77b2e31' };
wechat/handleRequest.js
const sha1 = require('sha1'); const {token} = require('../config'); module.exports = ()=>{ return (request, response, next)=>{ const {signature, echostr, timestamp, nonce} = request.query; const sha1Str = sha1([timestamp, nonce, token].sort().join("")); if(sha1Str === signature){ response.end(echostr); }else{ response.end("error"); }; next(); }; };
index.js
const express = require('express'); const handleRequest = require('./handleRequest'); const app = express(); app.use(express.urlencoded({extended: true})); app.use(handleRequest()); app.listen( 3000, err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!') );
1. 被動回覆用戶消息789(此時,有兩個公衆號,一個本人的,一個測試的)
驗證服務器 有效性實際上是 get 請求
用戶發送的消息是 post 請求
if(request.method === 'GET'){ // 非用戶發送的消息
if(sha1Str === signature){
response.end(echostr);
}else{
response.end('error');
};
}else if(request.method === 'POST'){ // 多是用戶發送的消息
if(sha1Str !== signature)response.end('error');
// 接收用戶信息
const userXMLData = await getUserXMLData(request);
// 將 xml 數據轉化爲 js 對象____xml2js
}else{
response.end('error');
};
// 當服務器沒有返回響應時,微信服務器會發送 3 次請求到開發者服務器
// 每次請求都會佔用使用接口的使用次數
// 爲了保證後續開發有次數,返回一個響應 resquest.end('');
/*
若是node 服務器 發送一個不正確的 xml 消息,會在用戶微信報錯
*/
實例源代碼:
index.js
const express = require('express'); const handleRequest = require('./handleRequest'); const app = express(); app.use(express.urlencoded({extended: true})); app.use(handleRequest()); // 使用中間件的方式 激活自定義模塊 app.listen( 3000, err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!') );
config/index.js
module.exports = { // 微信公衆號 配置信息 token: 'FinnKou', appID: 'wxba554569dbd1127d22cd32d', appsecret: '62ad759915d251f276680fc153b618d77b2e31' };
handleRequest/index.js
const sha1 = require('sha1'); // sha1 加密庫 const {token} = require('../config'); const {getUserXMLData} = require('../utils/getUserDataAsync'); const {XML2JSON, JSON2Obj} = require("../utils/tools"); module.exports = ()=>{ return async (request, response, next)=>{ const {signature, echostr, timestamp, nonce} = request.query; const sha1Str = sha1([timestamp, nonce, token].sort().join("")); if(request.method === 'GET'){ // 服務器發過來的消息 if(sha1Str === signature){ response.end(echostr); }else{ response.end("error"); }; }else if(request.method === 'POST'){ if(sha1Str !== signature){ // 非微信用戶發過來的消息 response.end("error"); }; // 用戶微信客戶端 發過來的消息 /**** <xml> <ToUserName><![CDATA[gh_d9bf45407d2a]]></ToUserName> <FromUserName><![CDATA[oSX3Z1aufrhsCwuEKXbVRfqOC1Wo]]></FromUserName> <CreateTime>1545741162</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[1]]></Content> <MsgId>6638907739305821698</MsgId> </xml> ****/ const XMLData = await getUserXMLData(request); const JSONData = XML2JSON(XMLData); const objData = JSON2Obj(JSONData); /* { ToUserName: 'gh_d9bf45407d2a', FromUserName: 'oSX3Z1aufrhsCwuEKXbVRfqOC1Wo', CreateTime: '1545744183', MsgType: 'text', Content: '1', MsgId: '6638920714402022972' } */ if(objData.Content === '1'){ response.send(`<xml> <ToUserName><![CDATA[${objData.FromUserName}]]></ToUserName> <FromUserName><![CDATA[${objData.ToUserName}]]></FromUserName> <CreateTime>${Date.now()}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[聖誕節快樂]]></Content> </xml>` ); }else{ response.send( `<xml> <ToUserName><![CDATA[${objData.FromUserName}]]></ToUserName> <FromUserName><![CDATA[${objData.ToUserName}]]></FromUserName> <CreateTime>${Date.now()}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[hello world!]]></Content> </xml>` ); }; }else{ response.end("error"); }; }; };
utils/tools.js
const {parseString} = require('xml2js'); // xml2js 是第三方庫,XML 字符串 轉 JSON 對象 module.exports = { XML2JSON(originXML){ // XML 字符串 轉 JSON 對象 let JSONData = null; parseString(originXML, {"trim": true}, (err, result)=>{ if(!err){ JSONData = result; }; }); return JSONData; }, JSON2Obj({xml}){ // JSON 對象 轉成 普通對象 let Obj = {}; for (let attr in xml){ const value = xml[attr]; Obj[attr] = value[0]; }; return Obj; } };
utils/getUserDataAsync.js
module.exports = { getUserXMLData(request){ // 獲取用戶微信客戶端 發送到 服務器的 XML 字符串 return new Promise((resolve, reject)=>{ let resultStr = ''; request.on("data", browser_info=>{ // 流方式 接受 resultStr += browser_info.toString(); }).on("end", ()=>{ resolve(resultStr); }); }); } };
源代碼:
index.js
const express = require('express'); const handleRequest = require('./handleRequest'); const app = express(); app.use(express.urlencoded({extended: true})); app.use(handleRequest()); app.listen( 3000, err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!') );
config/index.js
module.exports = { token: 'FinnKou', appID: 'wxba59d182bd7d2245cd32d', appsecret: '62ad7594595d2f276680f454cb618d77b2e31' };
handleRequest/index.js
const sha1 = require('sha1'); const {token} = require('../config'); const {getUserXMLData} = require('../utils/getUserDataAsync'); const {XML2JSON, JSON2Obj} = require('../utils/tools'); const {autoReply} = require('../autoReply'); module.exports = ()=>{ return async (request, response, next)=>{ const {signature, echostr, timestamp, nonce} = request.query; const sha1Str = sha1([timestamp, nonce, token].sort().join("")); if(request.method === 'GET'){ // 服務器發過來的消息 if(sha1Str === signature){ response.end(echostr); }else{ response.end("error"); }; }else if(request.method === 'POST'){ if(sha1Str !== signature){ // 非微信用戶發過來的消息 response.end("error"); }; // 用戶發過來的消息 const XMLData = await getUserXMLData(request); const JSONData = XML2JSON(XMLData); const objData = JSON2Obj(JSONData); console.log(objData); response.send(autoReply(objData)); // 封裝 自動回覆用戶消息 }else{ response.end("error"); }; }; };
utils/getUserDataAsync.js
module.exports = { getUserXMLData(request){ return new Promise((resolve, reject)=>{ let resultStr = ''; request.on("data", browser_info=>{ resultStr += browser_info.toString(); }).on("end", ()=>{ resolve(resultStr); }); }); } }; /**** 用戶發過來的消息 <xml> <ToUserName><![CDATA[gh_d9bf45407d2a]]></ToUserName> <FromUserName><![CDATA[oSX3Z1aufrhsCwuEKXbVRfqOC1Wo]]></FromUserName> <CreateTime>1545741162</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[1]]></Content> <MsgId>6638907739305821698</MsgId> </xml> ****/
utils/tools.js
const {parseString} = require('xml2js'); module.exports = { XML2JSON(originXML){ let JSONData = null; parseString(originXML, {"trim": true}, (err, result)=>{ if(!err){ JSONData = result; }; }); return JSONData; }, JSON2Obj({xml}){ let Obj = {}; for (let attr in xml){ const value = xml[attr]; Obj[attr] = value[0]; }; return Obj; } }; // 用戶微信客戶端 發過來的消息 /**** { ToUserName: 'gh_d9bf45407d2a', FromUserName: 'oSX3Z1aufrhsCwuEKXbVRfqOC1Wo', CreateTime: '1545744183', MsgType: 'text', Content: '1', MsgId: '6638920714402022972' } ****/
autoReply/index.js
const {getReplyInfo} = require('./getReplyInfo'); module.exports = { autoReply(objData){ const options = getReplyInfo(objData); let replyXML = `<xml> <ToUserName><![CDATA[${options.toUserName}]]></ToUserName> <FromUserName><![CDATA[${options.fromUserName}]]></FromUserName> <CreateTime>${options.createTime}</CreateTime> <MsgType><![CDATA[${options.msgType}]]></MsgType> `; switch (options.msgType) { case 'text':{ replyXML += `<Content><![CDATA[${options.content}]]></Content>`; }break; case 'image':{ replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`; replyXML += `<Image><MediaId><![CDATA[${options.image}]]></MediaId></Image>`; }break; case 'voice':{ replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`; replyXML += `<Voice><MediaId><![CDATA[${options.voice}]]></MediaId></Voice>`; }break; case 'video':{ replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`; replyXML += `<Video> <MediaId><![CDATA[${options.video}]]></MediaId> <Title><![CDATA[${options.videoTitle}]]></Title> <Description><![CDATA[${options.videoDescription}]]></Description> </Video>`; }break; case 'music':{ replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`; replyXML += `<Music> <Title><![CDATA[${options.musicTitle}]]></Title> <Description><![CDATA[${options.musicDescription}]]></Description> <MusicUrl><![CDATA[${options.musicUrl}]]></MusicUrl> <HQMusicUrl><![CDATA[${options.hqMusicUrl}]]></HQMusicUrl> <ThumbMediaId><![CDATA[${options.musicMediaId}]]></ThumbMediaId> </Music>`; }break; case 'news':{ replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`; replyXML += `<ArticleCount>${options.content.length}</ArticleCount><Articles>`; options.content.forEach(item => { replyXML += `<item> <Title><![CDATA[${item.title}]]></Title> <Description><![CDATA[${item.description}]]></Description> <PicUrl><![CDATA[${item.picUrl}]]></PicUrl> <Url><![CDATA[${item.url}]]></Url> </item>`; }); replyXML += `</Articles>`; }break; }; replyXML += '</xml>'; return replyXML; } };
autoReply/getReplyInfo.js
module.exports = { getReplyInfo(objData){ let options = { toUserName: objData.FromUserName, fromUserName: objData.ToUserName, createTime: Date.now(), msgType: objData.MsgType, /**** 文本 ****/ content: objData.Content, /**** 圖片 ****/ image: objData.MediaId, // MediaId /**** 語音 ****/ voice: objData.MediaId, // MediaId recognition: objData.Recognition, // MediaId /**** 視頻 ****/ video: objData.MediaId, // MediaId videoTitle: objData.Title, videoDescription: objData.Description, /**** 音樂 ****/ musicTitle: objData.Title, musicDescription:objData.Description, musicUrl: objData.MusicUrl, hqMusicUrl: objData.HQMusicUrl, musicMediaId: objData.MediaId, // MediaId /**** 地理位置 ****/ location_X: objData.Location_X, location_Y: objData.Location_Y, scale: objData.Scale, label: objData.Label, /**** +位置 ****/ latitude: objData.Latitude, longitude: objData.Longitude, precision: objData.Precision, /**** 菜單 CLICK ****/ eventKey: objData.EventKey }; switch (options.msgType) { case 'text':{ // 文本 switch (objData.Content) { case 'news':{ options.msgType = 'news'; options.content = [{ title: '微信公衆號開發', description: '這裏有最新的公衆號教程', picUrl: 'https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2136740882,3271518133&fm=58&bpow=630&bpoh=630', url: 'http://www.atguigu.com' }]; }break; case '1':{ options.content = '我會對你 1 心 1 意~'; }break; case '2':{ options.content = '其實你一點都不 2 ~'; }break; case '3':{ options.content = '可能也會 3 心 二 意~'; }break; }; }break; case 'voice':{ // 語音 options.msgType = 'text'; options.content = `語音識別結果爲:${options.recognition}`; }break; case 'location':{ // + 位置 options.msgType = 'text'; options.content = ` 地理位置緯度:${options.location_X} 地理位置經度:${options.location_Y} 地圖縮放大小: ${options.scale} 地理位置信息: ${options.label}`; }break; case 'event':{ // 事件推送 switch (objData.Event) { case 'subscribe':{ // 訂閱 options.msgType = 'text'; options.content = '歡迎訂閱 FinnKou 的內容!' if (objData.EventKey) { //掃描帶參數二維碼關注的。 通常應用在活動上 options.content = '掃描帶參數二維碼關注, 歡迎您關注公衆號~'; } }break; case 'unsubscribe':{ // 取消訂閱 console.log('無情取關~'); }break; case 'LOCATION':{ // 用戶上報地理位置 // 當用戶關注公衆號時,它會問你是否容許上報地理位置,若是容許纔會觸發當前事件 options.msgType = 'text'; options.content = ` 地理位置緯度:${options.latitude} 地理位置經度:${options.longitude} 位置信息: ${options.precision}`; }break; case 'CLICK':{ // 用戶點擊菜單按鈕 options.msgType = 'text'; options.content = `菜單 key:${options.eventKey}`; }break; }; }break; case 'link':{ // 連接消息 }break; case 'image':{ // 圖片 }break; case 'video':{ // 視頻 }break; case 'shortvideo':{ // 小視頻 }break; case 'music':{ // 音樂 }break; case 'news':{ // 圖文 }break; }; return options; } };