订阅号开发(服务号需要企业认证,不适合个人开发)
1 注册微信公众号 1.注册网站
官网:https://mp.weixin.qq.com/
2.注册流程
1)打开官网,点击右上角的立即注册 2)选择订阅号注册 3)依次输入要求的信息,勾上我同意,点击注册 4)选择中国内地,点击确定 5)选择订阅号确定 6)主体类型选择个人,填写好信息点击继续 7)填写好信息点击完成
关键词回复:
开发者自行开发 接口测试号 位置: 微信官方文档->开始开发->接口测试号申请
打开WebStorm创建一个简单的服务器:
在工程文件夹下导包:npm install express
1 2 3 4 5 6 7 8 9 10 const express = require ('express' )const app = express ();app.use ((req,res,next )=> { }) app.listen (3000 ,()=> console .log ('服务器启动成功' ));
此时得到的是一个本地网址:http://localhost:3000 ,要将其转换为外部可访问网址才行 工具:将内网映射成外网 ngrok
指令: ngrok http 3000
观察Forwarding
后面即为外网网址
将网址加上/html
输入在浏览器(例如: http://xxx.ngrok.io/html ) (注意:有的浏览器可能无法访问,换个浏览器就好)
有Gitub账号可直接登录,若没有账号可以先注册再登录
重新打开ngrok输入ngrok http 3000 --authtoken 加上上图authtoken后面的内容
(例如: ngrok http 3000 --authtoken 23RthpB7QpLdHrBTivoitz4CzZ8_4a9AcRX3ZuKKBXWfoPSYe)
将最终得到的网址填入URL栏,Token随便填写即可(尽量复杂)
填好之后在服务器上进行测试:
导入sha1库:npm install sha1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 const express = require ('express' );const sha1 = require ('sha1' );const app = express ();const config = { token : '' , appID : '' , appsecret : '' } app.use ((req, res, next ) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const arr = [timestamp, nonce, token]; const arrSort = arr.sort (); console .log (arrSort) const str = arr.join ('' ); console .log (str); const sha1Str = sha1 (str); console .log (signature); console .log (sha1Str); if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } }) app.listen (3000 , () => console .log ('服务器启动成功' ));
模块化代码: 新建config文件夹,在该文件夹下新建index.js文件
index.js
1 2 3 4 5 6 module .exports = { token : '' , appID : '' , appsecret : '' }
新建wechat文件夹,在该文件下新建auth.js文件
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const config = require ("../config" );const sha1 = require ("sha1" );module .exports = () => { return (req, res, next ) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const arr = [timestamp, nonce, token]; const arrSort = arr.sort (); console .log (arrSort) const str = arr.join ('' ); console .log (str); const sha1Str = sha1 (str); console .log (signature); console .log (sha1Str); if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } }
主文件
1 2 3 4 5 6 7 8 9 10 const express = require ('express' );const auth = require ('./wechat/auth' )const app = express ();app.use (auth ()) app.listen (3000 , () => console .log ('服务器启动成功' ));
获取access_token 位置: 微信官方文档->开始开发->获取access_token
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
公众平台的API调用所需的access_token的使用及生成方式说明:
1、建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器可对外继续输出的老access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;
3、access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。
4、对于可能存在风险的调用,在开发者进行获取 access_token调用时进入风险调用确认流程,需要用户管理员确认后才可以成功获取。具体流程为:
开发者通过某IP发起调用->平台返回错误码[89503]并同时下发模板消息给公众号管理员->公众号管理员确认该IP可以调用->开发者使用该IP再次发起调用->调用成功。
如公众号管理员第一次拒绝该IP调用,用户在1个小时内将无法使用该IP再次发起调用,如公众号管理员多次拒绝该IP调用,该IP将可能长期无法发起调用。平台建议开发者在发起调用前主动与管理员沟通确认调用需求,或请求管理员开启IP白名单功能并将该IP加入IP白名单列表。
公众号和小程序均可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。**调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。**小程序无需配置IP白名单。
接口调用请求说明
1 https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
参数说明
参数 是否必须 说明 grant_type 是 获取access_token填写client_credential appid 是 第三方用户唯一凭证 secret 是 第三方用户唯一凭证密钥,即appsecret
返回说明
正常情况下,微信会返回下述JSON数据包给公众号:
{“access_token”:“ACCESS_TOKEN”,“expires_in”:7200}
参数说明
参数 说明 access_token 获取到的凭证 expires_in 凭证有效时间,单位:秒
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
{“errcode”:40013,“errmsg”:“invalid appid”}
返回码说明
返回码 说明 -1 系统繁忙,此时请开发者稍候再试 0 请求成功 40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性 40002 请确保grant_type字段值为client_credential 40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。 89503 此IP调用需要管理员确认,请联系管理员 89501 此IP正在等待管理员确认,请联系管理员 89506 24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用 89507 1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用
在wechat文件夹下新建accessToken.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 const rp = require ('request-promise-native' );const {writeFile, readFile} = require ('fs' );const {appID, appsecret} = require ('../config' );class Wechat { constructor ( ) { } getAccessToken ( ) { const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + appID + '&secret=' + appsecret; return new Promise ((resolve, reject ) => { rp ({method : 'GET' , url, json : true }) .then (res => { console .log (res); res.expires_in = Date .now () + (res.expires_in - 300 ) * 1000 ; resolve (res); }) .catch (err => { console .log (err); reject ('getAccessToken方法出了问题:' + err); }) }) } saveAccessToken (accessToken ) { accessToken = JSON .stringify (accessToken); return new Promise ((resolve, reject ) => { writeFile ('./accessToken.txt' , accessToken, err => { if (!err) { console .log ('文件保存成功' ); resolve (); } else { reject ('saveAccessToken方法出了问题:' + err); } }) }) } readAccessToken ( ) { return new Promise ((resolve, reject ) => { readFile ('./accessToken.txt' , (err, data ) => { if (!err) { console .log ('文件读取成功' ); data = JSON .parse (data); resolve (data); } else { reject ('readAccessToken方法出了问题:' + err); } }) }) } isValidAccessToken (data ) { if (!data && !data.access_token && !data.expires_in ) { return false ; } return data.expires_in > Date .now (); } fetchAccessToken ( ) { if (this .access_token && this .expires_in && this .isValidAccessToken (this )) { return Promise .resolve ({ access_token : this .access_token , expires_in : this .expires_in }) } return this .readAccessToken () .then (async res => { if (this .isValidAccessToken (res)) { return Promise .resolve (res); } else { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); } }) .catch (async err => { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); }) .then (res => { this .access_token = res.access_token ; this .expires_in = res.expires_in ; return Promise .resolve (res); }) } } const w = new Wechat ();w.fetchAccessToken ();
获取用户发送的消息 扫描测试号二维码关注测试公众号,发送一条消息进行测试
auto.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 const config = require ("../config" );const sha1 = require ("sha1" );module .exports = () => { return (req, res, next ) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } console .log (req.query ); } else { res.end ('error' ); } } }
运行app.js得到如下信息:
1 2 3 4 5 6 { signature: '', timestamp: '', nonce: '', openid: '' //用户的微信ID }
如果开发者服务器没有返回响应给微信服务器,微信服务器会发送三次请求过来 在if中最后加上res.end('');
即可
在项目下新建utils
工具包文件夹,在里面新建一个文件tool.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 module .exports = { getUserDataAsync (req ) { return new Promise ((resolve, reject ) => { let xmlData = '' ; req .on ('data' , data => { console .log (data); xmlData += data.toString (); }) .on ('end' , () => { resolve (xmlData); }) }) } }
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 const config = require ("../config" );const sha1 = require ("sha1" );const {getUserDataAsync} = require ('../utils/tool' );module .exports = () => { return async (req, res, next) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } const xmlData = await getUserDataAsync (req); console .log (xmlData); res.end ('' ); } else { res.end ('error' ); } } }
运行app.js得到如下信息:
1 2 3 4 5 6 7 <xml><ToUserName><![CDATA[gh_3b6b3cd4d8e8]]></ToUserName> //开发中id <FromUserName><![CDATA[okzHZ6Xb1dMfGx1fti6VV4en0aTU]]></FromUserName>//用户 openid <CreateTime>1643090286</CreateTime>//发送的时间戳 <MsgType><![CDATA[text]]></MsgType>//发送消息类型 <Content><![CDATA[123]]></Content>//内容 <MsgId>23523394398679914</MsgId>//消息id 微信服务器会默认保存3天用户发送的数据,通过次id三天内就可以找到此消息数据 </xml>
接下来我们要得到xml中的数据
tool.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const {parseString} = require ('xml2js' );module .exports = { getUserDataAsync (req ) { return new Promise ((resolve, reject ) => { let xmlData = '' ; req .on ('data' , data => { console .log (data); xmlData += data.toString (); }) .on ('end' , () => { resolve (xmlData); }) }) }, parseXMLAsync (xmlData ){ return new Promise ((resolve, reject ) => { parseString (xmlData,{trim :true },(err,data )=> { if (!err){ resolve (data); }else { reject ('parseXMLAsync方法出了问题:' +err); } }) }) } }
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const config = require ("../config" );const sha1 = require ("sha1" );const {getUserDataAsync,parseXMLAsync} = require ('../utils/tool' );module .exports = () => { return async (req, res, next) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } const xmlData = await getUserDataAsync (req); console .log (xmlData); const jsData = await parseXMLAsync (xmlData); console .log (jsData); res.end ('' ); } else { res.end ('error' ); } } }
运行app.js得到如下信息:
1 2 3 4 5 6 7 8 9 10 { xml: { ToUserName: [ 'gh_3b6b3cd4d8e8' ], FromUserName: [ 'okzHZ6Xb1dMfGx1fti6VV4en0aTU' ], CreateTime: [ '1643091294' ], MsgType: [ 'text' ], Content: [ '111' ], MsgId: [ '23523410572447438' ] } }
格式化得到的数据:
tool.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const {parseString} = require ('xml2js' );module .exports = { getUserDataAsync (req ) { return new Promise ((resolve, reject ) => { let xmlData = '' ; req .on ('data' , data => { console .log (data); xmlData += data.toString (); }) .on ('end' , () => { resolve (xmlData); }) }) }, parseXMLAsync (xmlData ) { return new Promise ((resolve, reject ) => { parseString (xmlData, {trim : true }, (err, data ) => { if (!err) { resolve (data); } else { reject ('parseXMLAsync方法出了问题:' + err); } }) }) }, formatMessage (jsData ) { let message = {}; jsData = jsData.xml ; if (typeof jsData === 'object' ) { for (let key in jsData) { let value = jsData[key]; if (Array .isArray (value) && value.length > 0 ) { message[key] = value[0 ]; } } } return message; } }
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 const config = require ("../config" );const sha1 = require ("sha1" );const {getUserDataAsync,parseXMLAsync,formatMessage} = require ('../utils/tool' );module .exports = () => { return async (req, res, next) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } const xmlData = await getUserDataAsync (req); console .log (xmlData); const jsData = await parseXMLAsync (xmlData); console .log (jsData); const message = formatMessage (jsData); console .log (message); res.end ('' ); } else { res.end ('error' ); } } }
运行app.js得到如下信息:
1 { CreateTime: '1643092243', MsgId: '23523424720943421' }
简单的自动回复 微信官方文档->基础消息能力->被动回复用户消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime 排重。
如果开发者希望增强安全性,可以在开发者中心处开启消息加密,这样,用户发给公众号的消息以及公众号被动回复用户消息都会继续加密,详见被动回复消息加解密说明。
假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明:
1、直接回复success(推荐方式) 2、直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)
一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:
1、开发者在5秒内未回复任何内容 2、开发者回复了异常数据,比如JSON数据等
另外,请注意,回复图片(不支持gif动图)等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。
各消息类型需要的XML数据包结构如下:
回复文本消息 1 2 3 4 5 6 7 <xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> </xml>
参数 是否必须 描述 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,文本为text Content 是 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)
回复图片消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 12345678</CreateTime > <MsgType > <![CDATA[image]]></MsgType > <Image > <MediaId > <![CDATA[media_id]]></MediaId > </Image > </xml >
参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,图片为image MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id。
回复语音消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 12345678</CreateTime > <MsgType > <![CDATA[voice]]></MsgType > <Voice > <MediaId > <![CDATA[media_id]]></MediaId > </Voice > </xml >
参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间戳 (整型) MsgType 是 消息类型,语音为voice MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id
回复视频消息 1 2 3 4 5 6 7 8 9 10 11 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 12345678</CreateTime > <MsgType > <![CDATA[video]]></MsgType > <Video > <MediaId > <![CDATA[media_id]]></MediaId > <Title > <![CDATA[title]]></Title > <Description > <![CDATA[description]]></Description > </Video > </xml >
参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,视频为video MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id Title 否 视频消息的标题 Description 否 视频消息的描述
回复音乐消息 1 2 3 4 5 6 7 8 9 10 11 12 13 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 12345678</CreateTime > <MsgType > <![CDATA[music]]></MsgType > <Music > <Title > <![CDATA[TITLE]]></Title > <Description > <![CDATA[DESCRIPTION]]></Description > <MusicUrl > <![CDATA[MUSIC_Url]]></MusicUrl > <HQMusicUrl > <![CDATA[HQ_MUSIC_Url]]></HQMusicUrl > <ThumbMediaId > <![CDATA[media_id]]></ThumbMediaId > </Music > </xml >
参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,音乐为music Title 否 音乐标题 Description 否 音乐描述 MusicURL 否 音乐链接 HQMusicUrl 否 高质量音乐链接,WIFI环境优先使用该链接播放音乐 ThumbMediaId 是 缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id
回复图文消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 12345678</CreateTime > <MsgType > <![CDATA[news]]></MsgType > <ArticleCount > 1</ArticleCount > <Articles > <item > <Title > <![CDATA[title1]]></Title > <Description > <![CDATA[description1]]></Description > <PicUrl > <![CDATA[picurl]]></PicUrl > <Url > <![CDATA[url]]></Url > </item > </Articles > </xml >
参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,图文为news ArticleCount 是 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息 Articles 是 图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数 Title 是 图文消息标题 Description 是 图文消息描述 PicUrl 是 图片链接,支持JPG、PNG格式,较好的效果为大图360 * 200,小图200 * 200 Url 是 点击图文消息跳转链接
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const config = require ("../config" );const sha1 = require ("sha1" );const {getUserDataAsync, parseXMLAsync, formatMessage} = require ('../utils/tool' );module .exports = () => { return async (req, res, next) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } const xmlData = await getUserDataAsync (req); const jsData = await parseXMLAsync (xmlData); const message = formatMessage (jsData); let content = '你在说什么?' ; if (message.MsgType ==='text' ){ if (message.Content ==='1' ){ content='你好!!' ; }else if (message.Content ==='2' ){ content='hello~~' ; }else if (message.Content .match ('爱' )){ content='我爱你' ; } } let replyMessage = '<xml><ToUserName><![CDATA[' +message.FromUserName +']]></ToUserName><FromUserName><![CDATA[' +message.ToUserName +']]></FromUserName><CreateTime>' +Date .now ()+'</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[' +content+']]></Content></xml>' ; res.send (replyMessage); } else { res.end ('error' ); } } }
运行app.js即可测试公众号
定义回复用户消息的模块文件 接收普通消息 当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
请注意:
关于重试的消息排重,推荐使用msgid排重。 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”。 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息-被动回复消息”接口向用户被动回复消息时,可以在
公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)。关于消息加解密的详细说明,请见“发送消息-被动回复消息加解密说明”。 各消息类型的推送XML数据包结构如下:
文本消息 1 2 3 4 5 6 7 8 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1348831860</CreateTime > <MsgType > <![CDATA[text]]></MsgType > <Content > <![CDATA[this is a test]]></Content > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,文本为text Content 文本消息内容 MsgId 消息id,64位整型
图片消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1348831860</CreateTime > <MsgType > <![CDATA[image]]></MsgType > <PicUrl > <![CDATA[this is a url]]></PicUrl > <MediaId > <![CDATA[media_id]]></MediaId > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,图片为image PicUrl 图片链接(由系统生成) MediaId 图片消息媒体id,可以调用获取临时素材接口拉取数据。 MsgId 消息id,64位整型
语音消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1357290913</CreateTime > <MsgType > <![CDATA[voice]]></MsgType > <MediaId > <![CDATA[media_id]]></MediaId > <Format > <![CDATA[Format]]></Format > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 语音为voice MediaId 语音消息媒体id,可以调用获取临时素材接口拉取数据。 Format 语音格式,如amr,speex等 MsgId 消息id,64位整型
请注意,开通语音识别后,用户每次发送语音给公众号时,微信会在推送的语音消息XML数据包中,增加一个Recognition字段(注:由于客户端缓存,开发者开启或者关闭语音识别功能,对新关注者立刻生效,对已关注用户需要24小时生效。开发者可以重新关注此帐号进行测试)。开启语音识别后的语音XML数据包如下:
1 2 3 4 5 6 7 8 9 10 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1357290913</CreateTime > <MsgType > <![CDATA[voice]]></MsgType > <MediaId > <![CDATA[media_id]]></MediaId > <Format > <![CDATA[Format]]></Format > <Recognition > <![CDATA[腾讯微信团队]]></Recognition > <MsgId > 1234567890123456</MsgId > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 语音为voice MediaId 语音消息媒体id,可以调用获取临时素材接口拉取该媒体 Format 语音格式:amr Recognition 语音识别结果,UTF8编码 MsgId 消息id,64位整型
视频消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1357290913</CreateTime > <MsgType > <![CDATA[video]]></MsgType > <MediaId > <![CDATA[media_id]]></MediaId > <ThumbMediaId > <![CDATA[thumb_media_id]]></ThumbMediaId > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 视频为video MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。 ThumbMediaId 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。 MsgId 消息id,64位整型
小视频消息 1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1357290913</CreateTime > <MsgType > <![CDATA[shortvideo]]></MsgType > <MediaId > <![CDATA[media_id]]></MediaId > <ThumbMediaId > <![CDATA[thumb_media_id]]></ThumbMediaId > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 小视频为shortvideo MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。 ThumbMediaId 视频消息缩略图的媒体id,可以调用获取临时素材接口拉取数据。 MsgId 消息id,64位整型
地理位置消息 1 2 3 4 5 6 7 8 9 10 11 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1351776360</CreateTime > <MsgType > <![CDATA[location]]></MsgType > <Location_X > 23.134521</Location_X > <Location_Y > 113.358803</Location_Y > <Scale > 20</Scale > <Label > <![CDATA[位置信息]]></Label > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,地理位置为location Location_X 地理位置纬度 Location_Y 地理位置经度 Scale 地图缩放大小 Label 地理位置信息 MsgId 消息id,64位整型
链接消息 1 2 3 4 5 6 7 8 9 10 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 1351776360</CreateTime > <MsgType > <![CDATA[link]]></MsgType > <Title > <![CDATA[公众平台官网链接]]></Title > <Description > <![CDATA[公众平台官网链接]]></Description > <Url > <![CDATA[url]]></Url > <MsgId > 1234567890123456</MsgId > </xml >
参数 描述 ToUserName 接收方微信号 FromUserName 发送方微信号,若为普通用户,则是一个OpenID CreateTime 消息创建时间 MsgType 消息类型,链接为link Title 消息标题 Description 消息描述 Url 消息链接 MsgId 消息id,64位整型
接收事件推送 在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:
关注/取消关注事件 用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。
微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。
关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。
假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。
推送XML数据包示例:
1 2 3 4 5 6 7 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[FromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[subscribe]]></Event > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,subscribe(订阅)、unsubscribe(取消订阅)
扫描带参数二维码事件 用户扫描带场景值二维码时,可能推送以下两种事件:
如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。 如果用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者。
用户未关注时,进行关注后的事件推送 推送XML数据包示例:
1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[FromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[subscribe]]></Event > <EventKey > <![CDATA[qrscene_123123]]></EventKey > <Ticket > <![CDATA[TICKET]]></Ticket > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,subscribe EventKey 事件KEY值,qrscene_为前缀,后面为二维码的参数值 Ticket 二维码的ticket,可用来换取二维码图片
用户已关注时的事件推送 推送XML数据包示例:
1 2 3 4 5 6 7 8 9 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[FromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[SCAN]]></Event > <EventKey > <![CDATA[SCENE_VALUE]]></EventKey > <Ticket > <![CDATA[TICKET]]></Ticket > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,SCAN EventKey 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id Ticket 二维码的ticket,可用来换取二维码图片
上报地理位置事件 用户同意上报地理位置后,每次进入公众号会话时,都会在进入时上报地理位置,或在进入会话后每5秒上报一次地理位置,公众号可以在公众平台网站中修改以上设置。上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。
推送XML数据包示例:
1 2 3 4 5 6 7 8 9 10 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[fromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[LOCATION]]></Event > <Latitude > 23.137466</Latitude > <Longitude > 113.352425</Longitude > <Precision > 119.385040</Precision > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,LOCATION Latitude 地理位置纬度 Longitude 地理位置经度 Precision 地理位置精度
自定义菜单事件 用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。
点击菜单拉取消息时的事件推送
推送XML数据包示例:
1 2 3 4 5 6 7 8 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[FromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[CLICK]]></Event > <EventKey > <![CDATA[EVENTKEY]]></EventKey > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,CLICK EventKey 事件KEY值,与自定义菜单接口中KEY值对应
点击菜单跳转链接时的事件推送
推送XML数据包示例:
1 2 3 4 5 6 7 8 <xml > <ToUserName > <![CDATA[toUser]]></ToUserName > <FromUserName > <![CDATA[FromUser]]></FromUserName > <CreateTime > 123456789</CreateTime > <MsgType > <![CDATA[event]]></MsgType > <Event > <![CDATA[VIEW]]></Event > <EventKey > <![CDATA[www.qq.com]]></EventKey > </xml >
参数说明:
参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,VIEW EventKey 事件KEY值,设置的跳转URL
在wechat文件夹下新建template.js,reply.js文件
template.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 module .exports = options => { let replayMessage = '<xml>' + '<ToUserName><![CDATA[' + options.toUserName + ']]></ToUserName>' + '<FromUserName><![CDATA[' + message.fromUserName + ']]></FromUserName>' + '<CreateTime>' + options.createTime + '</CreateTime>' + '<MsgType><![CDATA[' + options.msgType + ']]></MsgType>' ; if (options.msgType === 'text' ) { replyMessage += '<Content><![CDATA[' + options.content + ']]></Content>' ; } else if (options.msgType === 'image' ) { replyMessage += '<Image><MediaId><![CDATA[' + options.mediaID + ']]></MediaId></Image>' ; } else if (options.msgType === 'voice' ) { replyMessage += '<Voice><MediaId><![CDATA[' + options.mediaID + ']]></MediaId></Voice>' ; } else if (options.msgType === 'video' ) { replyMessage += '<Video>' + '<MediaId><![CDATA[' + options.mediaID + ']]></MediaId>' + '<Title><![CDATA[' + options.title + ']]></Title>' + '<Description><![CDATA[' + options.description + ']]></Description>' + '</Video>' ; } else if (options.msgType === 'music' ) { replyMessage += '<Music>' + '<Title><![CDATA[' + options.title + ']]></Title>' + '<Description><![CDATA[' + options.description + ']]></Description>' + '<MusicUrl><![CDATA[' + options.musicUrl + ']]></MusicUrl>' + '<HQMusicUrl><![CDATA[' + options.hpMusicUrl + ']]></HQMusicUrl>' + '<ThumbMediaId><![CDATA[' + options.mediaID + ']]></ThumbMediaId>' + '</Music>' ; } else if (options.msgType === 'news' ) { replyMessage += '<ArticleCount>' + options.content .length + '</ArticleCount>' + '<Articles>' ; options.content .forEach (item => { replyMessage += '<item>' + '<Title><![CDATA[' + item.title + ']]></Title>' + '<Description><![CDATA[' + item.description + ']]></Description>' + '<PicUrl><![CDATA[' + item.picUrl + ']]></PicUrl>' + '<Url><![CDATA[' + item.url + ']]></Url>' + '</item>' ; }) replayMessage += '</Articles>' ; } replayMessage += '</xml>' ; return replayMessage; }
reply.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 module .exports = message => { let options = { toUserName : message.FromUserName , fromUserName : message.ToUserName , createTime : Date .now (), magType : 'text' } let content = '你在说什么?' ; if (message.MsgType === 'text' ) { if (message.Content === '1' ) { content = '你好!!' ; } else if (message.Content === '2' ) { content = 'hello~~' ; } else if (message.Content .match ('爱' )) { content = '我爱你' ; } } else if (message.MsgType === 'image' ) { options.msgType = 'image' ; options.mediaID = message.MediaId ; console .log (message.PicUrl ); } else if (message.MsgType === 'voice' ) { options.msgType = 'voice' ; options.mediaID = message.MediaId ; console .log (message.Recognition ); } else if (message.MsgType === 'location' ) { content = '维度:' + message.Location_X + ' 经度:' + message.Location_Y + ' 缩放大小:' + message.Scale + ' 位置信息:' + message.Label ; console .log (message.PicUrl ); } else if (message.MsgType === 'event' ) { if (message.Event === 'subscribe' ) { content = "感谢您的订阅" ; if (message.EventKey ) { content = '用户扫描了带参数的二维码关注事件' ; } } else if (message.Event === 'unsubscribe' ) { console .log ('无情取关' ); } else if (message.Event === 'SCAN' ) { content = '用户已经关注,再次扫描了带参数的二维码关注事件' ; } else if (message.Event === 'LOCATION' ) { content = '维度:' + message.Latitude + ' 经度:' + message.Longitude + ' 精度:' + message.Precision ; } else if (message.Event === 'CLICK' ) { content = '你点击了按钮:' + message.EventKey ; } } options.content = content; return options; }
auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 const config = require ("../config" );const template = require ("./template" );const reply = require ("./reply" );const sha1 = require ("sha1" );const {getUserDataAsync, parseXMLAsync, formatMessage} = require ('../utils/tool' );module .exports = () => { return async (req, res, next) => { const {signature, echostr, timestamp, nonce} = req.query ; const {token} = config; const sha1str = sha1 ([timestamp, nonce, token].sort ().join ("" )); if (req.method === 'GET' ) { if (sha1Str === signature) { res.send (echostr); } else { res.end ('error' ); } } else if (req.method === 'POST' ) { if (sha1str !== signature) { res.end ('error' ); } const xmlData = await getUserDataAsync (req); const jsData = await parseXMLAsync (xmlData); const message = formatMessage (jsData); const options=reply (message); const replyMessage = template (options); res.send (replyMessage); } else { res.end ('error' ); } } }
注意:接收语音识别结果、获取用户地理位置需在测试号管理界面开启
实现自定义菜单 创建接口 请注意:
自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
自定义菜单接口可实现多种类型按钮,如下:
click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互; view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。 scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。 scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。 pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。 pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。 pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。 location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。 media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频 、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。 view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。 article_id:用户点击 article_id 类型按钮后,微信客户端将会以卡片形式,下发开发者在按钮中填写的图文消息 article_view_limited:类似 view_limited,但不使用 media_id 而使用 article_id
注意: 草稿接口灰度完成后,将不再支持图文信息类型的 media_id 和 view_limited,有需要的,请使用 article_id 和 article_view_limited 代替
请注意,3到8的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。9~12,是专门给第三方平台旗下未微信认证(具体而言,是资质认证未通过)的订阅号准备的事件类型,它们是没有事件推送的,能力相对受限,其他类型的公众号不必使用。
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
click和view的请求示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "button":[ { "type":"click", "name":"今日歌曲", "key":"V1001_TODAY_MUSIC" }, { "name":"菜单", "sub_button":[ { "type":"view", "name":"搜索", "url":"http://www.soso.com/" }, { "type":"miniprogram", "name":"wxa", "url":"http://mp.weixin.qq.com", "appid":"wx286b93c14bbf93aa", "pagepath":"pages/lunar/index" }, { "type":"click", "name":"赞一下我们", "key":"V1001_GOOD" }] }] }
其他新增按钮类型的请求示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 { "button": [ { "name": "扫码", "sub_button": [ { "type": "scancode_waitmsg", "name": "扫码带提示", "key": "rselfmenu_0_0", "sub_button": [ ] }, { "type": "scancode_push", "name": "扫码推事件", "key": "rselfmenu_0_1", "sub_button": [ ] } ] }, { "name": "发图", "sub_button": [ { "type": "pic_sysphoto", "name": "系统拍照发图", "key": "rselfmenu_1_0", "sub_button": [ ] }, { "type": "pic_photo_or_album", "name": "拍照或者相册发图", "key": "rselfmenu_1_1", "sub_button": [ ] }, { "type": "pic_weixin", "name": "微信相册发图", "key": "rselfmenu_1_2", "sub_button": [ ] } ] }, { "name": "发送位置", "type": "location_select", "key": "rselfmenu_2_0" }, { "type": "media_id", "name": "图片", "media_id": "MEDIA_ID1" }, { "type": "view_limited", "name": "图文消息", "media_id": "MEDIA_ID2" }, { "type": "article_id", "name": "发布后的图文消息", "article_id": "ARTICLE_ID1" }, { "type": "article_view_limited", "name": "发布后的图文消息", "article_id": "ARTICLE_ID2" } ] }
参数说明
参数 是否必须 说明 button 是 一级菜单数组,个数应为1~3个 sub_button 否 二级菜单数组,个数应为1~5个 type 是 菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型 name 是 菜单标题,不超过16个字节,子菜单不超过60个字节 key click等点击类型必须 菜单KEY值,用于消息接口推送,不超过128字节 url view、miniprogram类型必须 网页 链接,用户点击菜单可打开链接,不超过1024字节。 type为miniprogram时,不支持小程序的老版本客户端将打开本url。 media_id media_id类型和view_limited类型必须 调用新增永久素材接口返回的合法media_id appid miniprogram类型必须 小程序的appid(仅认证公众号可配置) pagepath miniprogram类型必须 小程序的页面路径 article_id article_id类型和article_view_limited类型必须 发布后获得的合法 article_id
返回结果
正确时的返回JSON数据包如下:
{“errcode”:0,“errmsg”:“ok”}
错误时的返回JSON数据包如下(示例为无效菜单名长度):
{“errcode”:40018,“errmsg”:“invalid button name size”}
删除接口 使用接口创建自定义菜单后,开发者还可使用接口删除当前使用的自定义菜单。另请注意,在个性化菜单时,调用此接口会删除默认菜单及全部个性化菜单。
请求说明
http请求方式:GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN
返回说明
对应创建接口,正确的Json返回结果:
{“errcode”:0,“errmsg”:“ok”}
代码:
在wechat文件夹下新建menu.js文件用来存储发送回去的数据:
menu.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 module .exports = { "button" : [ { "type" : "click" , "name" : "戳我呀~" , "key" : "CLICK" }, { "name" : "菜单二" , "sub_button" : [ { "type" : "view" , "name" : "百度搜索" , "url" : "http://www.baidu.com/" }, { "type" : "scancode_waitmsg" , "name" : "扫码带提示" , "key" : "扫码带提示" }, ] }, { "name" : "发图" , "sub_button" : [ { "type" : "pic_sysphoto" , "name" : "系统拍照发图" , "key" : "系统拍照发图" }, { "type" : "pic_photo_or_album" , "name" : "拍照或者相册发图" , "key" : "拍照或者相册发图" }, { "type" : "pic_weixin" , "name" : "微信相册发图" , "key" : "微信相册发图" }, { "name" : "发送位置" , "type" : "location_select" , "key" : "rselfmenu_2_0" } ] } ] }
因为创建菜单需要用到access_token值,所以我们在accessToken.js文件下增加俩个函数:创建菜单和删除菜单,并改名为wechat.js
wechat.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 const menu=require ('./menu' );const rp = require ('request-promise-native' );const {writeFile, readFile} = require ('fs' );const {appID, appsecret} = require ('../config' );class Wechat { constructor ( ) { } getAccessToken ( ) { const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + appID + '&secret=' + appsecret; return new Promise ((resolve, reject ) => { rp ({method : 'GET' , url, json : true }) .then (res => { console .log (res); res.expires_in = Date .now () + (res.expires_in - 300 ) * 1000 ; resolve (res); }) .catch (err => { console .log (err); reject ('getAccessToken方法出了问题:' + err); }) }) } saveAccessToken (accessToken ) { accessToken = JSON .stringify (accessToken); return new Promise ((resolve, reject ) => { writeFile ('./accessToken.txt' , accessToken, err => { if (!err) { console .log ('文件保存成功' ); resolve (); } else { reject ('saveAccessToken方法出了问题:' + err); } }) }) } readAccessToken ( ) { return new Promise ((resolve, reject ) => { readFile ('./accessToken.txt' , (err, data ) => { if (!err) { console .log ('文件读取成功' ); data = JSON .parse (data); resolve (data); } else { reject ('readAccessToken方法出了问题:' + err); } }) }) } isValidAccessToken (data ) { if (!data && !data.access_token && !data.expires_in ) { return false ; } return data.expires_in > Date .now (); } fetchAccessToken ( ) { if (this .access_token && this .expires_in && this .isValidAccessToken (this )) { return Promise .resolve ({ access_token : this .access_token , expires_in : this .expires_in }) } return this .readAccessToken () .then (async res => { if (this .isValidAccessToken (res)) { return Promise .resolve (res); } else { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); } }) .catch (async err => { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); }) .then (res => { this .access_token = res.access_token ; this .expires_in = res.expires_in ; return Promise .resolve (res); }) } createMenu (menu ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${data.access_token} ` ; const result = await rp ({method : 'POST' , url, json : true , body : menu}); resolve (result); } catch (e) { reject ('createMenu方法出了问题:' + e); } }) } deleteMenu ( ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${data.access_token} ` ; const result = await rp ({method : 'GET' , url, json : true }); resolve (result); } catch (e) { reject ('deleteMenu方法出了问题:' + e); } }) } } (async ()=>{ const w=new Wechat (); let result = await w.deleteMenu (); console .log (result); result = await w.createMenu (menu); console .log (result); })()
运行wechat.js得到如下结果:
{ errcode: 0, errmsg: ‘ok’ } { errcode: 0, errmsg: ‘ok’ }
网页开发 获取ticket 自己写页面: 在项目下新建一个views文件夹,在里面新建search.ejs文件
search.ejs
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>search</title> </head> <body> <h1>这是一个搜索页面</h1> </body> </html>
然后配置模板资源目录、配置模板引擎、渲染页面,将渲染好的页面通过render
方法返回给用户 app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const express = require ('express' );const auth = require ('./wechat/auth' );const app = express ();app.set ('views' ,'./views' ); app.set ('view engine' ,'ejs' ); app.get ('/search' ,(req,res )=> { res.render ('search' ); }) app.use (auth ()) app.listen (3000 , () => console .log ('服务器启动成功' ));
运行app.js,输入ngrok网址+/search
得到如下结果:
接下来我们使用微信给我们提供的工具:JS-SDK
官方文档
首先我们需要获得ticket(获取方式与access_token大同小异)
在utils文件夹下新建api.js用来存放api接口:
api.js
1 2 3 4 5 6 7 8 9 10 11 12 const prefix = 'https://api.weixin.qq.com/cgi-bin/' ;module .exports = { accessToken : `${prefix} token?grant_type=client_credential` , ticket : `${prefix} ticket/getticket?type=jsapi` , menu : { create : `${prefix} menu/create?` , delete : `${prefix} menu/delete?` } }
tool.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 const {parseString} = require ('xml2js' );const {writeFile, readFile} = require ('fs' );const {resolve}=require ('path' );module .exports = { getUserDataAsync (req ) { return new Promise ((resolve, reject ) => { let xmlData = '' ; req .on ('data' , data => { xmlData += data.toString (); }) .on ('end' , () => { resolve (xmlData); }) }) }, parseXMLAsync (xmlData ) { return new Promise ((resolve, reject ) => { parseString (xmlData, {trim : true }, (err, data ) => { if (!err) { resolve (data); } else { reject ('parseXMLAsync方法出了问题:' + err); } }) }) }, formatMessage (jsData ) { let message = {}; jsData = jsData.xml ; if (typeof jsData === 'object' ) { for (let key in jsData) { let value = jsData[key]; if (Array .isArray (value) && value.length > 0 ) { message[key] = value[0 ]; } } } return message; }, writeFileAsync (data, fileName ) { data = JSON .stringify (data); const filePath = resolve (__dirname,fileName); return new Promise ((resolve, reject ) => { writeFile (filePath, data, err => { if (!err) { console .log ('文件保存成功' ); resolve (); } else { reject ('writeFileAsync方法出了问题:' + err); } }) }) }, readFileAsync (fileName ){ const filePath = resolve (__dirname,fileName); return new Promise ((resolve, reject ) => { readFile (filePath, (err, data ) => { if (!err) { console .log ('文件读取成功' ); data = JSON .parse (data); resolve (data); } else { reject ('readFileAsync方法出了问题:' + err); } }) }) } }
wechat.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 const menu=require ('./menu' );const rp = require ('request-promise-native' );const api=require ('../utils/api' )const {writeFileAsync,readFileAsync}=require ('../utils/tool' );const {appID, appsecret} = require ('../config' );class Wechat { constructor ( ) { } getAccessToken ( ) { const url = `${api.accessToken} &appid=${appID} &secret=${appsecret} ` ; return new Promise ((resolve, reject ) => { rp ({method : 'GET' , url, json : true }) .then (res => { console .log (res); res.expires_in = Date .now () + (res.expires_in - 300 ) * 1000 ; resolve (res); }) .catch (err => { console .log (err); reject ('getAccessToken方法出了问题:' + err); }) }) } saveAccessToken (accessToken ) { return writeFileAsync (accessToken,'access_token.txt' ); } readAccessToken ( ) { return readFileAsync ('access_token.txt' ) } isValidAccessToken (data ) { if (!data && !data.access_token && !data.expires_in ) { return false ; } return data.expires_in > Date .now (); } fetchAccessToken ( ) { if (this .access_token && this .expires_in && this .isValidAccessToken (this )) { return Promise .resolve ({ access_token : this .access_token , expires_in : this .expires_in }) } return this .readAccessToken () .then (async res => { if (this .isValidAccessToken (res)) { return Promise .resolve (res); } else { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); } }) .catch (async err => { const res = await this .getAccessToken (); await this .saveAccessToken (res); return Promise .resolve (res); }) .then (res => { this .access_token = res.access_token ; this .expires_in = res.expires_in ; return Promise .resolve (res); }) } getTicket ( ) { return new Promise (async (resolve, reject) => { const data = await this .fetchAccessToken (); const url = `${api.ticket} &access_token=${data.access_token} ` ; rp ({method : 'GET' , url, json : true }) .then (res => { resolve ({ ticket :res.ticket , expires_in :Date .now () + (res.expires_in - 300 ) * 1000 }); }) .catch (err => { console .log (err); reject ('getTicket方法出了问题:' + err); }) }) } saveTicket (ticket ) { return writeFileAsync (ticket,'ticket.txt' ); } readTicket ( ) { return readFileAsync ('ticket.txt' ) } isValidTicket (data ) { if (!data && !data.ticket && !data.expires_in ) { return false ; } return data.expires_in > Date .now (); } fetchTicket ( ) { if (this .ticket && this .ticket_expires_in && this .isValidTicket (this )) { return Promise .resolve ({ ticket : this .ticket , expires_in : this .expires_in }) } return this .readTicket () .then (async res => { if (this .isValidTicket (res)) { return Promise .resolve (res); } else { const res = await this .getTicket (); await this .saveTicket (res); return Promise .resolve (res); } }) .catch (async err => { const res = await this .getTicket (); await this .saveTicket (res); return Promise .resolve (res); }) .then (res => { this .ticket = res.ticket ; this .ticket_expires_in = res.expires_in ; return Promise .resolve (res); }) } createMenu (menu ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `${api.menu.create} access_token=${data.access_token} ` ; const result = await rp ({method : 'POST' , url, json : true , body : menu}); resolve (result); } catch (e) { reject ('createMenu方法出了问题:' + e); } }) } deleteMenu ( ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `${api.menu.delete } access_token=${data.access_token} ` ; const result = await rp ({method : 'GET' , url, json : true }); resolve (result); } catch (e) { reject ('deleteMenu方法出了问题:' + e); } }) } } (async ()=>{ const w=new Wechat (); const data = await w.fetchTicket (); console .log (data); })()
将Wechat暴露出去:在最后添加module.exports = Wechat;
并把上面的测试代码注释掉 即:
在config文件夹下的index.js中添加url: 'ngrok地址'
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 const express = require ('express' );const sha1 = require ('sha1' );const {url} = require ('./config' );const auth = require ('./wechat/auth' );const Wechat = require ('./wechat/wechat' );const app = express ();app.set ('views' , './views' ); app.set ('view engine' , 'ejs' ); const wechatApi = new Wechat ();app.get ('/search' , async (req, res) => { const noncestr = Math .random ().split ('.' )[1 ]; const timestamp = Date .now (); const {ticket} = await wechatApi.fetchTicket (); const arr = [ `jsapi_ticket=${ticket} ` , `noncestr=${noncestr} ` , `timestamp=${timestamp} ` , `url=${url} /search` ] const str = arr.sort ().join ('&' ); console .log (str); const signature = sha1 (str); res.render ('search' , { signature, noncestr, timestamp }); }) app.use (auth ()) app.listen (3000 , () => console .log ('服务器启动成功' ));
修改search.ejs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> <title>search</title> </head> <body> <h1 id="search">语音识别查电影</h1> <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.2.0/zepto.min.js"></script> <script type="text/javascript"> /* 1.绑定域名 - 在接口测试号页面上填写js安全域名接口 不用写域名:"http://" 2.引入js文件 - http://res.wx.qq.com/open/js/jweixin-1.6.0.js 3.通过config接口注入权限验证配置 */ wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: 'wxd9cdd2f13c019b6d', // 必填,公众号的唯一标识 timestamp: '<%= timestamp %>', // 必填,生成签名的时间戳 nonceStr: '<%= noncestr %>', // 必填,生成签名的随机串 signature: '<%= signature %>',// 必填,签名 jsApiList: [ 'onMenuShareQQ', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'translateVoice' ] // 必填,需要使用的JS接口列表 }); //通过ready接口处理成功验证 wx.ready(function () { // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。 // 对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 //验证接口是否有权限 wx.checkJsApi({ jsApiList: ['onMenuShareQQ', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'translateVoice'], // 需要检测的JS接口列表,所有JS接口列表见附录2, success: function (res) { // 以键值对的形式返回,可用的api值true,不可用为false // 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"} console.log(res); }, fail: function (err) { } }); //设置标志位,是否在录音中 var isRecord = false; //语音识别功能 $('#search').tap(function () { if (!isRecord) { //开始录音 wx.startRecord(); isRecord = false; } else { //结束录音 wx.stopRecord({ success: function (res) { //结束录音后会自动上传录音到微信服务器中,微信服务器会返回一个id给开发者使用 var localId = res.localId; //将录音转化为文字 /*wx.translateVoice({ localId: localId, // 需要识别的音频的本地Id,由录音相关接口获得 isShowProgressTips: 1, // 默认为1,显示进度提示 success: function (res) { alert(res.translateResult); // 语音识别的结果 } });*/ isRecord = false; } }); } }) }); //通过error接口处理失败验证 wx.error(function (res) { // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); </script> </body> </html>
进入BootCDN官网 ,搜索zepto
,复制第一个script标签即可
分享接口:
1 2 3 4 5 6 7 8 9 10 11 12 wx.onMenuShareQQ({ title: '', // 分享标题 desc: '', // 分享描述 link: '', // 分享链接 imgUrl: '', // 分享图标 success: function () { // 用户确认分享后执行的回调函数 }, cancel: function () { // 用户取消分享后执行的回调函数 } });
优化项目 写了这么久的项目,是不是觉得项目十分的杂乱,这时候我们就需要对项目就行优化
因为auth,reply,template三个模块的作用都是拿到数据并做出反应给用户,所以我们可以把他们整合到一起
然后menu,wechat我们是用来实现自定义菜单功能的,我们今后可以将所有接口都放到wechat里面
在项目下新建文件夹reply,用来处理用户响应,反应给用户的一些模块,即将auth,reply,template三个模块放进去,将auth改名为index
修改app.js中的引入auth代码,修改为
1 2 const reply = require ('./reply' );
修改app.use(auth())
为app.use(reply())
提取app.js中的路由模块
在项目下新建router文件夹,新建index.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const express = require ('express' );const sha1 = require ('sha1' );const {url} = require ("../config" );const Wechat = require ("../wechat/wechat" );const reply = require ("../reply" );const Router = express.Router ;const router = new Router ();const wechatApi = new Wechat ();router.get ('/search' , async (req, res) => { const noncestr = Math .random ().split ('.' )[1 ]; const timestamp = Date .now (); const {ticket} = await wechatApi.fetchTicket (); const arr = [ `jsapi_ticket=${ticket} ` , `noncestr=${noncestr} ` , `timestamp=${timestamp} ` , `url=${url} /search` ] const str = arr.sort ().join ('&' ); console .log (str); const signature = sha1 (str); res.render ('search' , { signature, noncestr, timestamp }); }) router.use (reply ()) module .exports = router;
修改app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const express = require ('express' );const router = require ('./router' );const app = express ();app.set ('views' , './views' ); app.set ('view engine' , 'ejs' ); app.use (router); app.listen (3000 , () => console .log ('服务器启动成功' ));
爬取浏览器数据 使用puppeteer
库
github地址
api文档
下载: npm i puppeteer
puppeteer 在执行安装的过程中需要执行install.js,这里会下载Chromium,翻墙也下载失败,导致安装不成功,解决办法(使用淘宝镜像):
npm config set puppeteer_download_host=https://npm.taobao.org/mirrors npm i puppeteer
我们只需要爬取8条数据
在项目下新建文件夹server,在里面再新建一个文件夹crawler,在其中新建theaterCrawler.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 const puppeteer = require ('puppeteer' )const url = 'https://movie.douban.com/cinema/nowplaying/chongqing/' ;module .exports = async () => { const browser = await puppeteer.launch ({ headless : true }); const page = await browser.newPage (); await page.goto (url, { waitUntil : 'networkidle2' }); await timeout (); let result = await page.evaluate (() => { let result = []; const $list = $('#nowplaying>.mod-bd>.lists>.list-item' ); for (var i = 0 ; i < 8 ; i++) { const liDom = $list[i]; let title = $(liDom).data ('title' ); let rating = $(liDom).data ('score' ); let runtime = $(liDom).data ('duration' ); let directors = $(liDom).data ('director' ); let casts = $(liDom).data ('actors' ); let href = $(liDom).find ('.poster>a' ).attr ('href' ); let image = $(liDom).find ('.poster>a>img' ).attr ('src' ); let doubanId = $(liDom).data ('subject' ); result.push ({ title, rating, runtime, directors, casts, href, image, doubanId }) } return result; }) for (let i = 0 ; i < result.length ; i++) { let item = result[i]; let url = result[i].href ; await page.goto (url, { waitUntil : 'networkidle2' }); let itemResult = await page.evaluate (() => { let genre = []; const $genre = $('[property="v:genre"]' ); for (let j = 0 ; j < $genre.length ; j++) { genre.push ($genre[j].innerText ); } const summary = $('[property="v:summary"]' ).html ().replace (/\s+/g , '' ); const releaseDate = $('[property="v:initialReleaseDate"]' )[0 ].innerText ; return { genre, summary, releaseDate } }) item.genre = itemResult.genre ; item.summary = itemResult.summary ; item.releaseDate = itemResult.releaseDate ; } console .log (result); await browser.close (); } function timeout ( ) { return new Promise (resolve => setTimeout (resolve, 2000 )) }
在server文件夹下新建index.js文件
1 2 3 4 5 const theatersCrawler = require ('./crawler/theatersCrawler' );(async () => { await theatersCrawler (); })()
运行index.js得到如下数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 [ { title: '四海', rating: 5.6, runtime: '128分钟', directors: '韩寒', casts: '刘昊然 / 刘浩存 / 沈腾', href: 'https://movie.douban.com/subject/35337517/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2867433563.webp', doubanId: 35337517, genre: [ '喜剧', '动作', '爱情' ], summary: '在码头做摩托车特技表演顺便拉客的年轻人吴仁耀(刘昊然饰),他多年不见的浪荡父亲吴仁腾(沈腾饰);梦想大城市生活的餐馆服务员周欢颂和他浮夸真诚的哥哥周 欢歌。一支从没有赢过一场比赛的“不败传说”车队频频出战,一群可笑又可爱的小人物命运交织。阿耀和欢颂都立志活成自己亲人的反面,想彼此取暖,彼此独立,却又总不在一个频道 上。世事无常,他们不得不背井离乡,迎接一场未知旅途,阿耀的一身技能竟会用在一个自己都意想不到的场合,他的亲情友情和爱情最终又将会是如何……', releaseDate: '2022-02-01(中国大陆)' }, { title: '奇迹·笨小孩', rating: 7.4, runtime: '106分钟', directors: '文牧野', casts: '易烊千玺 / 田雨 / 陈哈琳', href: 'https://movie.douban.com/subject/35312437/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2842327103.webp', doubanId: 35312437, genre: [ '剧情' ], summary: '二十岁的景浩(易烊千玺饰)独自带着年幼的妹妹来到深圳生活,兄妹俩生活温馨却拮据。为了妹妹高昂的手术费,机缘巧合之下,景浩得到一个机会,本以为美好生活 即将来临,却不料遭遇重创。在时间和金钱的双重压力下,毫无退路的景浩决定孤注一掷,而他陷入困境的平凡人生,又能否燃起希望的火花?<br>电影《奇迹》是中宣部国家电影局20 21年重点电影项目,也是2021年重点建党百年献礼片,描述十八大以后新时代年轻人在深圳创业的影片。', releaseDate: '2022-02-01(中国大陆)' }, { title: '这个杀手不太冷静', rating: 6.9, runtime: '109分钟', directors: '邢文雄', casts: '马丽 / 魏翔 / 陈明昊', href: 'https://movie.douban.com/subject/35505100/?from=playing_poster', image: 'https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2814949620.webp', doubanId: 35505100, genre: [ '喜剧' ], summary: '毕生追求男主梦的魏成功(魏翔饰)终于得到了女明星米兰(马丽饰)的“赏识”,被邀请出演她的男一号“杀手卡尔”,他兴致勃勃诠释角色的同时,却没想到已经落入了 一场危机四伏的阴谋,但他依然借自己“精湛”的演技和绝佳的运气化险为夷,而残酷的真相也离他越来越近……', releaseDate: '2022-02-01(中国大陆)' }, { title: '长津湖之水门桥', rating: 7.2, runtime: '149分钟', directors: '徐克', casts: '吴京 / 易烊千玺 / 朱亚文', href: 'https://movie.douban.com/subject/35613853/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2846021991.webp', doubanId: 35613853, genre: [ '剧情', '历史', '战争' ], summary: '电影以抗美援朝战争第二次战役中的长津湖战役为背景,讲述了在结束了新兴里和下碣隅里的战斗之后,七连战士们又接到了更艰巨的任务……', releaseDate: '2022-02-01(中国大陆)' }, { title: '狙击手', rating: 7.7, runtime: '96分钟', directors: '张艺谋 张末', casts: '陈永胜 / 章宇 / 张译', href: 'https://movie.douban.com/subject/35215390/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2738601191.webp', doubanId: 35215390, genre: [ '剧情', '历史', '战争' ], summary: '影片根据抗美援朝战争“冷枪冷炮”运动中神枪手群体事迹改编。1952年冬至1953年初,中国人民志愿军与联合国军在朝鲜战场形成僵持,双方发起了低强度的密集狙击战 ,史称"冷枪冷炮运动"。在连长(张译饰)带领下的狙击五班战士枪法过人,成为敌军的心头大患,班长刘文武(章宇饰)更成为重点狙击对象。为重创狙击五班,敌方调配精英狙击小 队,配以最先进的武器装备,更迫使狙击五班战士大永(陈永胜饰)等人为救同伴进入其设好的险境之地。但正当敌军打响自己如意算盘之时,他们未料到,被他们当作诱饵的侦察兵亮 亮(刘奕铁饰)身上其实隐藏着更大的秘密......', releaseDate: '2022-02-01(中国大陆)' }, { title: '喜羊羊与灰太狼之筐出未来', rating: 0, runtime: '94分钟', directors: '黄伟明', casts: '祖晴 / 张琳 / 邓玉婷', href: 'https://movie.douban.com/subject/35608160/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2797468943.webp', doubanId: 35608160, genre: [ '喜剧', '动画', '运动' ], summary: '喜羊羊、灰太狼与一众小羊组成的守护者队进入篮球顶级赛事决赛,但却意外地败北,团队分崩离析。<br>虽然各散东西,但对篮球的热爱和对冠军的渴望让大家再次组 队,参加新一届大赛,然而这次的对手更强大,他们面临更大的挑战!', releaseDate: '2022-02-01(中国大陆)' }, { title: '熊出没·重返地球', rating: 0, runtime: '99分钟', directors: '林汇达', casts: '张秉君 / 张伟 / 谭笑', href: 'https://movie.douban.com/subject/35377026/?from=playing_poster', image: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2856825681.webp', doubanId: 35377026, genre: [ '喜剧', '动画', '儿童' ], summary: '有点懒又有点馋的熊二虽然总是各种失误犯错,内心却一直梦想成为一位英雄,以此获得大家特别是哥哥熊大的认可。<br>一块外星原核的坠落打破了狗熊岭的平静,熊 二意外的与外星原核合体,成为了拥有外星智慧能量的熊!随之而来的是“外星人”阿布的抢夺。阿布为了夺回原核,故意制造事端,让熊二众叛亲离。就在阿布将要成功的时候,一支神 秘高科技军团的攻击彻底打乱了计划,导致熊强组合甚至整个地球于巨大的危难之中。<br>阿布隐藏的身份以及来到地球的真正目的到底是什么?神秘的高科技军团背后有何故事?自暴 自弃的熊二能否振作起来实现他的英雄梦?他们能否挽救地球的危机?', releaseDate: '2022-02-01(中国大陆)' }, { title: '李茂扮太子', rating: 4.3, runtime: '100分钟', directors: '高可', casts: '马丽 / 常远 / 艾伦', href: 'https://movie.douban.com/subject/35444998/?from=playing_poster', image: 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2812626447.webp', doubanId: 35444998, genre: [ '喜剧', '古装' ], summary: '富家女杨家珍(马丽饰)与小捕快李茂(常远饰)成婚,虽夫妻恩爱,但始终得不到家珍父母的认可。李茂意外发现自己竟与当朝太子相貌相同,一个想进宫获得晋升, 一个想出宫获得自由,二人交换身份,却不知正一步步卷入尚书的阴谋里……', releaseDate: '2022-01-01(中国大陆)' } ]
保存数据到数据库 在项目下新建db文件夹,在其中新建index.js文件,需要下载mongoose包:npm install mongoose
安装mongoDB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const mongoose = require ('mongoose' );module .exports = new Promise ((resolve, reject ) => { mongoose.connect ('mongodb://localhost:27017/movie' , {useNewUrlParser : true }); mongoose.connection .once ('open' , err => { if (!err) { console .log ('数据库连接成功~~' ); resolve (); }else { reject ('数据库连接失败:' +err); } }) })
在项目下新建model文件夹,在其中新建Theaters.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const mongoose = require ('mongoose' );const Schema = mongoose.Schema ;const theatersSchema = new Schema ({ title : String , rating : Number , runtime : String , directors : String , casts : String , image : String , doubanId : { type : Number , unique : true }, genre : [String ], summary : String , releaseDate : String , posterKey : String , createTime : { type : Date , default : Date .now () } }) const Theaters = mongoose.model ('Theaters' ,theatersSchema);module .exports = Theaters ;
在server文件夹下新建save文件夹,并在其中新建saveTheaters.js文件用来保存实现保存操作的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const Theaters = require ('../../model/Theaters' );module .exports = async data => { for (var i = 0 ; i < data.length ; i++) { let item = data[i]; await Theaters .create ({ title : item.title , rating : item.rating , runtime : item.runtime , directors : item.directors , casts : item.casts , image : item.image , doubanId : item.doubanId , genre : item.genre , summary : item.summary , releaseDate : item.releaseDate , }) console .log ('数据保存成功' ); } }
在关闭浏览器后将数据返回出去:在theatersCrawler.js中关闭浏览器代码后添加return result;
修改server文件夹下面的index文件
1 2 3 4 5 6 7 8 9 10 11 const db = require ('../db' );const theatersCrawler = require ('./crawler/theatersCrawler' );const saveTheaters = require ('./save/saveTheaters' );(async () => { await db; const data = await theatersCrawler (); await saveTheaters (data); })()
在数据库中执行:db.theaters.find({})
查看数据保存情况
回复用户热门电影数据 修改reply下的reply.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const Theaters = require ('../model/Theaters' );const {url} = require ('../config' );if (message.Content === '热门' ) { const data = await Theaters .find ({}, {title : 1 , summary : 1 , image : 1 , doubanId : 1 , _id : 0 }); content = []; options.msgType = 'news' ; for (var i = 0 ; i < data.length ; i++) { let item = data[i]; content.push ({ title : item.title , description : item.summary , picUrl : item.image , url : `${url} /detail/${item.doubanId} ` }) } }
将reply下的index.js文件中的const options = reply(message);
改为
1 2 3 const options = await reply (message);
电影详情页面 router的index.js中,在搜索页面路由下添加电影详情页面路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const Theaters = require ('../model/Theaters' );router.get ('/detail/:id' , async (req, res) => { const {id} = req.params ; if (id) { const data = await Theaters .findOne ({doubanId : id}, {_id : 0 , __v : 0 , createTime : 0 , doubanId : 0 }); res.render ('detail' , {data}); } else { res.end ('error' ); } })
电影详情页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>movie</title> </head> <body> <div id="app"> <header> <a class="header_title" href="/movie">电影</a> <a class="header_search" href="/search">搜索</a> </header> <div class="page"> <h1 class="title"><%= data.title %></h1> <section class="info"> <div class="left"> <p class="rating"> <span>评分:</span> <strong><%= data.rating %></strong> <span class="ratingNum">98375人评价</span> </p> <p class="meta"> <%= data.runtime %> / <%= data.genre.forEach()(function(item){ %> <%= item %> / <% }) %> <%= data.directors %>(导演) / <%= data.casts %> / <%= data.releaseDate %> </p> </div> <div class="right"> <a href="javascript:"> <img src="<%= data.image %>" alt="<%= data.title %>"> </a> </div> </section> <section class="intro"> <h2><%= data.title %>的简介</h2> <div class="bd"> <p><%= data.summary %></p> </div> </section> </div> </div> </body> </html>
文本搜索电影 在匹配的最后加一个else判断,用来搜索用户输入指定电影信息
reply.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const rp = require ('request-promise-native' );else { const url = 'https://api.douban.com/v2/movie/search' ; const {subjects} = await rp ({method : 'GET' , url, json : true , qs : {q : message.Content , count : 8 }}); if (subjects && subjects.length ) { content = []; options.msgType = 'news' ; for (var i = 0 ; i < subjects.length ; i++) { let item = subjects[i]; content.push ({ title : item.title , description : `电影评分为:${item.rating.average} ` , picUrl : item.image .small , url : item.alt }) } } else { content = "暂时没有电影信息" ; } }
上传图片到七牛 七牛官网
注册并登录
点击控制台->资源管理下的存储空间->新建存储空间(填写信息后确认创建)
我们上传成功之后,只需复制外链使用即可
文档:文档中心->SDK&工具->官方SDK->Node.js 服务器
安装依赖: npm i qiniu -D
在server文件夹下新建qiniu文件夹,并在其中新建upload.js、index.js文件
upload.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const qiniu = require ('qiniu' );var accessKey = 'your access key' ;var secretKey = 'your secret key' var mac = new qiniu.auth .digest .Mac (accessKey, secretKey);var config = new qiniu.conf .Config ();config.zone = qiniu.zone .Zone_z2 ; var bucketManager = new qiniu.rs .BucketManager (mac, config);const bucket = '' ;module .exports = (resUrl, key ) => { return new Promise ((resolve, reject ) => { bucketManager.fetch (resUrl, bucket, key, function (err, respBody, respInfo ) { if (err) { console .log (err); reject ('上传图片方法出了问题' + err); } else { if (respInfo.statusCode == 200 ) { console .log ('文件上传成功' ); resolve (); } } }); }) }
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const Theaters = require ('../../model/Theaters' );const upload = require ('./upload' );const nanoid = require ('nanoid' );module .exports = async () => { const movies = await Theaters .find ({ $or : [ {posterKey : '' }, {posterKey : null }, {posterKey : {$exists : false }} ] }) for (let i = 0 ; i < movies.length ; i++) { let movie = movies[i]; let url = movie.image ; let key = `${nanoid(10 )} .jpg` ; await upload (url, key); movie.posterKey = key; await movie.save (); } }
修改server文件夹下的index.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const db = require ('../db' );const theatersCrawler = require ('./crawler/theatersCrawler' );const saveTheaters = require ('./save/saveTheaters' );const uploadToQiniu = require ('./qiniu' );(async () => { await db; await uploadToQiniu (); })()
运行server文件夹下的index.js文件
nanoid库:生成唯一key值 下载: npm i nanoid -D
修改reply.js回复热门下面的picUrl为:
picUrl: http://r6v3vcdm2.hn-bkt.clouddn.com/${item.posterKey}
,
爬取预告片电影数据 跟之前爬取热门电影一样,我们先在server文件夹下的crawler文件夹下新建trailersCrawler.js文件用来爬取预告片电影数据
trailersCrawler.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 const puppeteer = require ('puppeteer' )const url = 'https://movie.douban.com/coming' ;module .exports = async () => { const browser = await puppeteer.launch ({ headless : true }); const page = await browser.newPage (); await page.goto (url, { waitUntil : 'networkidle2' }); await timeout (); let result = await page.evaluate (() => { let result = []; const $trs = $('.coming_list>tbody>tr' ); for (var i = 0 ; i < $trs.length ; i++) { const trDom = $trs[i]; let num = parseInt ($(trDom).find ('td' ).last ().html ()); if (num > 1000 ) { let href = $(trDom).find ('a' ).attr ('href' ); result.push (href); } } return result; }) let moviesData = []; for (let i = 0 ; i < result.length ; i++) { let url = result[i]; await page.goto (url, { waitUntil : 'networkidle2' }); let itemResult = await page.evaluate (() => { const href = $('.related-pic-video' ).attr ('href' ); if (!href) { return false ; } let title = $('[property="v:itemreviewed"]' ).html (); let directors = $('[rel="v:directedBy"]' ).html (); let image = $('[rel="v:image"]' ).attr ('src' ); let doubanId = $('.lnk-sharing' ).attr ('share-id' ); let casts = []; let $star = $('[rel="v:starring"]' ); let length = $star.length > 3 ? 3 : $star.length ; for (var j = 0 ; j < length; j++) { casts.push ($star[j].innerText ); } let genre = []; const $genre = $('[property="v:genre"]' ); for (let j = 0 ; j < $genre.length ; j++) { genre.push ($genre[j].innerText ); } const summary = $('[property="v:summary"]' ).html ().replace (/\s+/g , '' ); const releaseDate = $('[property="v:initialReleaseDate"]' )[0 ].innerText ; const runTime = $('[property="v:runtime"]' ).html (); const cover = $('.related-pic-video' ).attr ('background-image' ); return { title, directors, casts, genre, image, summary, releaseDate, doubanId, runTime, href, cover } }) if (itemResult) { moviesData.push (itemResult); } } for (let i = 0 ; i < moviesData.length ; i++) { let item = moviesData[i]; let url = item.href ; await page.goto (url, { waitUntil : 'networkidle2' }); item.link = await page.evaluate (() => { return $('video>source' ).attr ('src' ); }) } console .log (moviesData); await browser.close (); return moviesData; } function timeout ( ) { return new Promise (resolve => setTimeout (resolve, 2000 )) }
修改server文件夹下的index.js文件并运行测试是否爬取到数据
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const db = require ('../db' );const theatersCrawler = require ('./crawler/theatersCrawler' );const trailersCrawler = require ('./crawler/trailersCrawler' );const saveTheaters = require ('./save/saveTheaters' );const uploadToQiniu = require ('./qiniu' );(async () => { const data = await trailersCrawler (); })()
我们同样在server文件夹下的save文件夹下新建saveTrailers.js用来将爬取到的数据保存到数据库
saveTrailers.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Trailers = require ('../../model/Trailers' );module .exports = async data => { for (var i = 0 ; i < data.length ; i++) { let item = data[i]; await Trailers .create ({ title : item.title , runtime : item.runtime , directors : item.directors , casts : item.casts , image : item.image , doubanId : item.doubanId , cover : item.cover , genre : item.genre , summary : item.summary , releaseDate : item.releaseDate , link : item.link , }) console .log ('数据保存成功' ); } }
Trailers.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const mongoose = require ('mongoose' );const Schema = mongoose.Schema ;const trailersSchema = new Schema ({ title : String , runtime : String , directors : String , casts : [String ], image : String , doubanId : { type : Number , unique : true }, genre : [String ], cover : String , summary : String , releaseDate : String , link :String , posterKey : String , coverKey : String , videoKey : String , createTime : { type : Date , default : Date .now () } }) const Trailers = mongoose.model ('Trailers' , trailersSchema);module .exports = Trailers ;
修改server文件夹下的index.js文件并运行测试保存数据是否成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const db = require ('../db' );const theatersCrawler = require ('./crawler/theatersCrawler' );const trailersCrawler = require ('./crawler/trailersCrawler' );const saveTheaters = require ('./save/saveTheaters' );const saveTrailers = require ('./save/saveTrailers' );const uploadToQiniu = require ('./qiniu' );(async () => { await db; const data = await trailersCrawler (); await saveTrailers (data); })()
打开数据库即可查看
重新定义上传七牛的方法 修改server文件夹下的qiniu文件夹下的index.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const upload = require ('./upload' );const {nanoid} = require ('nanoid' );module .exports = async (key, Model ) => { const movies = await Model .find ({ $or : [ {[key]: '' }, {[key]: null }, {[key]: {$exists : false }} ] }) for (let i = 0 ; i < movies.length ; i++) { let movie = movies[i]; let url = movie.image ; let filename = '.jpg' ; if (key === 'coverKey' ) { url = movie.cover ; } else if (key === 'videoKey' ) { url = movie.link ; filename = '.mp4' ; } filename = `${nanoid(10 )} ${filename} ` ; await upload (url, filename); movie[key] = filename; await movie.save (); } }
修改server文件夹下的index.js文件并运行测试上传七牛是否成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const db = require ('../db' );const theatersCrawler = require ('./crawler/theatersCrawler' );const trailersCrawler = require ('./crawler/trailersCrawler' );const saveTheaters = require ('./save/saveTheaters' );const saveTrailers = require ('./save/saveTrailers' );const uploadToQiniu = require ('./qiniu' );const Theaters = require ('../model/Theaters' );const Trailers = require ('../model/Trailers' );(async () => { await db; await uploadToQiniu ('coverKey' , Trailers ); await uploadToQiniu ('posterKey' , Trailers ); await uploadToQiniu ('videoKey' , Trailers ); })()
搭建预告片静态页面 可自行搭建movie.html
搭建预告片ejs页面 在router文件夹的index.js中添加电影预告片路由
1 2 3 4 5 6 7 8 9 const Trailers = require ('../model/Trailers' );router.get ('/movie' , async (req, res) => { const data = await Trailers .find ({}, {_id : 0 , __v : 0 ,cover :0 ,link :0 ,image :0 }); res.render ('movie' , {data}); })
修改movie.html为movie.ejs,同之前的search.ejs修改即可
预告片视频功能 可以使用一个叫DPlayer的库,可以去github 查看一下文档
bootcdn地址
我们在项目下新建media文件夹来存放素材
在utils文件夹中的api.js新增接口
1 2 3 4 5 6 7 8 9 10 temporary : { upload : `${prefix} media/upload?` , get : `${prefix} media/get?` }, permanent : { uploadNews : `${prefix} material/add_news?` , uploadImg : `${prefix} media/uploadimg?` , uploadOthers : `${prefix} material/add_material?` , get : `${prefix} material/get_material?` }
上传临时素材 官方文档
我们在wechat.js添加方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const {createReadStream}= require ('fs' );const {resolve}=require ('path' );uploadTemporaryMaterial (type, fileName ) { const filePath = resolve (__dirname, '../media' , fileName); return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `${api.temporary.upload} access_token=${data.access_token} ` ; const formData = { media : createReadStream (filePath) } const result = await rp ({method : 'POST' , url, json : true , formData}); resolve (result); } catch (e) { reject ('uploadTemporaryMaterial方法出了问题' + e); } }) }
获取临时素材 官方文档
wechat.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const request = require ('request' );const {createReadStream,createWriteStream}= require ('fs' );getTemporaryMaterial (type, mediaId, fileName ) { const filePath = resolve (__dirname, '../media' , fileName); return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); let url = `${api.temporary.get} access_token=${data.access_token} &media_id=${mediaId} ` ; if (type === 'video' ) { url = url.replace ('https://' , 'http://' ); const result = await rp ({method : 'GET' , url, json : true }); resolve (result); } else { request (url) .pipe (createWriteStream (filePath)) .once ('close' , resolve) } }catch (e) { reject ('getTemporaryMaterial方法出了问题' +e); } }) }
上传永久素材 官方文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const {resolve,join} = require ('path' );uploadPermanentMaterial (type, material, body ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); let options = { method : 'POST' , json : true } if (type === 'news' ) { options.url = `${api.permanent.uploadNews} access_token=${data.access_token} ` ; options.body = material; } else if (type === 'pic' ) { options.url = `${api.permanent.uploadImg} access_token=${data.access_token} ` ; options.formData = { media : createReadStream (join (__dirname, '../media' , material)) } } else { options.url = `${api.permanent.uploadOthers} access_token=${data.access_token} &type=${type} ` ; options.formData = { media : createReadStream (join (__dirname, '../media' , material)) } if (type === 'video' ) { options.body = body; } } const result = await rp (options); resolve (result) } catch (e) { reject ('uploadPermanentMaterial方法出了问题' + e); } }) }
获取永久素材 官方文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 getPermanentMaterial (type, mediaId, fileName ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); const url = `${api.permanent.get} access_token=${data.access_token} ` ; const options = {method : 'POST' , url, json : true ,body :{media_id :mediaId}}; if (type === 'video' ||'news' ) { const result = await rp (options); resolve (result); } else { request (options) .pipe (createWriteStream (join (__filename,'../media' ,fileName))) .once ('close' , resolve) } }catch (e) { reject ('getPermanentMaterial方法出了问题' +e); } }) }
封装上传素材函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 uploadMaterial (type, material, body, isPermanent = true ) { return new Promise (async (resolve, reject) => { try { const data = await this .fetchAccessToken (); let options = { method : 'POST' , json : true , formData : { media : createReadStream (join (__dirname, '../media' , material)) } } if (isPermanent) { if (type === 'news' ) { options.url = `${api.permanent.uploadNews} access_token=${data.access_token} ` ; options.body = material; options.formData = null ; } else if (type === 'pic' ) { options.url = `${api.permanent.uploadImg} access_token=${data.access_token} ` ; } else { options.url = `${api.permanent.uploadOthers} access_token=${data.access_token} &type=${type} ` ; if (type === 'video' ) { options.body = body; } } }else { options.url = `${api.temporary.upload} access_token=${data.access_token} ` ; } const result = await rp (options); resolve (result) } catch (e) { reject ('uploadPermanentMaterial方法出了问题' + e); } }) }