首页 新闻 会员 周边 捐助

node.js开发微信公众号之图文回复

0
悬赏园豆:15 [已解决问题] 解决于 2018-05-16 22:03

问题截图:

服务端打印的消息截图:

问题说明:文字回复时是正常的,输入文本,回复文本。输入2,需要回复图文。问题是,其他文本信息能够正常显示出来,但图文不可以,求大神解答。

总共分为 8 个功能块

程序主入口:app.js

代码展示:

"use strict"
var koa = require('koa');
var config = require('./configs/config');
//引入中间件
var middleware = require('./service/middleware');
var weixin = require('./service/eventback');
var app = new koa();
app.use(middleware(config.wechat,weixin.replay));
app.listen(80);
console.log('监听:80');

中间件:middleware.js

代码展示:

var sha1 = require('sha1');
var Wechat = require('./wechat');
var getRawBody = require('raw-body');
var util = require('../utils/util')
var bodyparse = require('body-parser');
var path = require('path');
module.exports = function(opts,handler){
//实例化wechat对象
var wechat = new Wechat(opts);
return function *(next){
//接受数据源--不确定是谁发过来的
//提取signature、timestamp、nonce、echostr字段
//判断字段均提取成功?
//token赋值为基本配置中的信息
//token、timestamp、nonce字典排序得到字符串list
//hash算法加密list得到hashcode
//如果hashcode==signature则数据源为微信后台提供的
//把echostr返回给微信后台,供微信后台认证token
//继续其他服务
console.log("123456this.query654321:",this.query);
var token = opts.Token;
var signature = this.query.signature;
var nonce = this.query.nonce;
var timestamp = this.query.timestamp;
var echostr = this.query.echostr;

var str = [token,timestamp,nonce].sort().join('');
var sha = sha1(str);
var that = this;

if(this.method === 'GET'){
if(sha !== signature){
this.body = 'wrong';
return false;
}
}else if(this.method === 'POST'){
if(sha !== signature){
this.body = 'wrong';
return false;
}
//微信客户端传过来的数据data
var data = yield getRawBody(this.req,{
length:this.length,
limit:'1mb',
encoding:this.charset
})

//console.log("data:",data);
//将data解析成xml格式
var content = yield util.parseXMLAsync(data);
//console.log("content:",content,"typeof:",typeof content);
var message = util.formatMessage(content.xml);
this.weixin = message; //挂载消息
yield handler(this,next); //转到业务层逻辑
//console.log(this.body);
//content,message
wechat.replay(this); //真正回复
console.log('this对象为:',this);
}

}
}

 

wechat功能模块:wechat.js

代码展示:

'use strict'
var prefix = 'https://api.weixin.qq.com/cgi-bin/';
var Promise = require('bluebird');
var request = Promise.promisify(require('request'));
var fs = require('fs');
var util = require('../utils/util');
var _ = require('lodash');
var api = {
access_token : prefix+'token?grant_type=client_credential',
//临时素材
temporary : {
upload : prefix + "media/upload?"
},
//永久素材
permanent : {
//https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=ACCESS_TOKEN
//新增其他类型永久素材
upload : prefix + 'material/add_material',
//新增永久图文素材
uploadNews : prefix + 'material/add_news',
//上传图文消息内的图片获取URL
uploadNewsPic : prefix + 'media/uploadimg'
}
}

function Wechat(opts){

this.readAccessToken = opts.getAccessTokenFromFile;
this.saveAccessTokenToFile = opts.saveAccessTokenToFile;
this.AppID = opts.AppID;
this.AppSecret = opts.AppSecret;
this.Token = opts.Token;
this.backAccessToken();

}

 

/**
作用:检查票据是否在有效期内
参数:data
参数格式:{"access_token":"**","expires_in":**}
返回:true、false*/
Wechat.prototype.isValidAccessToken = function(data){
if(!data||!data['access_token']||!data['expires_in']){
return false;
}
//拿到票据
var access_token = data.access_token;
//拿到存储时间
var expires_in = data.expires_in;
//拿到当前时间
var now = (new Date().getTime());
//判断票据是否过期
if(now < expires_in){
return true;
}else{
return false;
}
}

/**
作用:更新票据
返回:JSON格式的data数据*/
Wechat.prototype.updateAccessToken = function(){
var url = api.access_token + '&appid=' + this.AppID + '&secret=' + this.AppSecret;
return new Promise(function(resolve,reject){
console.log('获取票据地址的地址为:',url);
request({url : url,json : true})
.then(function(response){
var data = response.body;
var nowTime = (new Date().getTime());
var expires_in = nowTime + (data['expires_in']-20)*1000;
data.expires_in= expires_in;
resolve(data);
}).catch(function(err){
reject(err);
})
})

}

/**
作用:获得票据
返回:JSON格式的data数据*/
Wechat.prototype.backAccessToken = function(){
var that = this;
/*if(this.readAccessToken().access_token && this.readAccessToken().expires_in){
if(that.isValidAccessToken(this.readAccessToken())){
console.log("验证成功,直接返回access_token:",this.readAccessToken());
return Promise.resolve(this.readAccessToken());
}
}*/
return that.readAccessToken().then(function(data){
try{
data = JSON.parse(data);
console.log("从文件获取票据成功!票据为:"+data);
}catch(e){
//失败时跟新票据
console.log("从文件获取票据失败!");
return that.updateAccessToken();
}
//合法性检查-是否在有效期内
if(that.isValidAccessToken(data)){
console.log("票据在有效期内!");
return Promise.resolve(data);
}else{
console.log("票据不在有效期内!");
return that.updateAccessToken();
}
}).then(function(data){
console.log('把access_token:',data,'保存到文件里面');
that.saveAccessTokenToFile(data);
return Promise.resolve(data);
})
}

/**
作用:回复信息
*/
Wechat.prototype.replay = function(that){
var content = that.body;
var message = that.weixin;
//console.log('wechat里的message:',message,'\nwechat里的content:',content);
var xml = util.tpl(content,message);
that.status = 200;
that.type = 'application/xml';
that.body = xml;
}

/**
作用:上传临时素材
参数:type 上传素材的类型
material 如果是图文则是数组
如果是图片或者视频则是字符串,表示路径
*/
Wechat.prototype.uploadMaterial = function(type,material,permanent){
//构建表单
var that = this;
var form = {};
var uploadUrl = api.temporary.upload;
if(permanent){
uploadUrl = api.permanent.upload;
_.extend(form,permanent);
}
if(type === 'pic'){
uploadUrl = api.permanent.uploadNewsPic;
}
if(type === 'news'){
uploadUrl = api.permanent.uploadNewsPic;
form = material;
}else{
form.media = fs.createReadStream(material);
}
//console.log('that.backAccessToken:',that.backAccessToken());
//console.log('uploadMaterial--that',that);
return new Promise(function(resolve,reject){
that
.backAccessToken()
.then(function(data){
console.log('uploadMaterial里的data:',data);
var url = uploadUrl + 'access_token=' + data.access_token;
if(!permanent){
url += '&type=' + type;
}else{
form.access_token = data.access_token;
}
var options = {
method : 'POST',
url : url,
json : true
}
if(type === 'news'){
options.body = form;
}else{
options.formData = form;
}
console.log('uploadMaterial--url',url);
request({method : 'POST',url : url,formData : form,json : true})
.then(function(response){
var _data = response.body;
console.log('wechat里的_data',_data);
if(_data){
resolve(_data);
}else{
throw new Error('临时素材上传失败');
}
}).catch(function(err){
reject(err);
})
});
})
}

module.exports = Wechat;

工具模块:util.js

代码展示:

"use strict"
var fs = require('fs');
var Promise = require('bluebird');
//用来解析xml
var xml2js = require('xml2js');
var tpl = require('../service/tpl');
/**
作用:读取txt文件里面的票据信息
*/
exports.readFileAsync = function(fpath,encoding){
return new Promise(function(resolve,reject){
fs.readFile(fpath,encoding,function(error,content){
if(error){
console.log('读取失败!');
reject(error);
}else{
console.log('读取成功!','error:',error,'content:',content);
resolve(content);
}
})
})
}

/**
作用:将票据写入txt文件里面
*/
exports.writeFileAsync = function(fpath,content){
return new Promise(function(resolve,reject){
fs.writeFile(fpath,content,function(error){
if(error){
reject(error);
}else{
resolve();
}
})
})
}

/**
作用:解析xml数据
*/
exports.parseXMLAsync = function(xml){
return new Promise(function(resolve,reject){
xml2js.parseString(xml,{trim:true},function(err,content){
if(err){
reject(err);
}else{
resolve(content);
}
});
})
}


function formatMessage(result) {
//声明空对象message
var message = {};
//对result类型进行判断
if (typeof result === 'object') {
//如果是object类型 通过Object.keys()方法拿到result所有的key 并存入keys变量中
var keys = Object.keys(result);
//对keys进行循环遍历
for (var i = 0; i < keys.length; i++) {
//拿到每个key对应的value值
var item = result[keys[i]];
//拿到key
var key = keys[i];
//判断item是否为数组或者长度是否为0
if (!(item instanceof Array) || item.length === 0) {
//如果item不是数组或者长度为0 则跳过继续向下解析
continue;
}
//如果长度为1
if (item.length === 1) {
//拿到value值存入val变量
var val = item[0];
//判断val是否为对象
if (typeof val === 'object') {
//如果val为对象 则进一步进行遍历
message[key] = formatMessage(val);
} else {
//如果不是对象 就把值赋给当前的key放入message里 并去除收尾空格
message[key] = (val || '').trim();
}
}
//如果item的长度既不是0也不是1 则说明它是一个数组
else {
//把message的key设置为空数组
message[key] = [];
//对数组进行遍历
for (var j = 0, k = item.length; j < k; j++) {
message[key].push(formatMessage(item[j]));
}
}
}
}
return message;
}

/**
参数:xml数据
作用:格式化xml数据
*/
exports.formatMessage = function(xml) {
return new Promise(function(resolve, reject) {
xml2js.parseString(xml, { trim: true }, function(err, content) {
if (err) {
reject(err);
} else {
resolve(content);
};
});
});
};

/**
参数:
content:
message:解析后的xml数据
作用:
*/
exports.tpl = function(content,message){
//临时存储
var info = {};
//默认类型
var type = 'text';
var fromUserName = message.FromUserName;
var toUserName = message.ToUserName;

console.log("util.tpl的content:",content);

if(content instanceof Array){
type = 'news';
}

info.content = content;
info.createTime = new Date().getTime();
info.msgType = content.type||type;
info.toUserName = fromUserName;
info.fromUserName = toUserName;
//console.log("info:",info);
//tpl.test();
console.log("tpl.compiled:",tpl.compiled(info));
return tpl.compiled(info);
}

 

exports.formatMessage = formatMessage;

事件回复模块:eventback.js

代码展示:

var config = require('../configs/config');
var wechat = require('./wechat');
//初始化
var wechat = new wechat(config.wechat);

exports.replay = function* (that,next){
var message = that.weixin;
console.log(message);
if(message.MsgType === 'event'){
if(message.Event === 'subscribe'){
console.log('扫描二维码关注:\n回复1: 反馈文字回复\n回复2:反馈图文信息,');
that.body = '扫描二维码关注:\n回复1: 反馈文字回复\n回复2:反馈图文信息\n回复3:反馈临时素材\n回复4:反馈永久素材';
}else if(message.Event === 'unsubscribe'){
console.log('无情取消关注!');
}
}else if(message.MsgType === 'text'){
var content = message.Content;
if(content === '2'){
that.body = [
{
title:'金刚.骷髅岛',
description:'qwe',
picUrl:'https://mmbiz.qpic.cn/mmbiz_jpg/DoUeXzU5V6EvTDqDxrVKniaJZbgia05NlsqtQFw89De4xVm19cS5pcV090BqdZtp82fmg0podXYHt9pKGDNrIu9A/0?wx_fmt=jpeg',
url:'http://www.baidu.com'},
{
title : '老王改变世界',
description : '假的',
picUrl : 'https://mmbiz.qpic.cn/mmbiz_jpg/DoUeXzU5V6EvTDqDxrVKniaJZbgia05NlsqtQFw89De4xVm19cS5pcV090BqdZtp82fmg0podXYHt9pKGDNrIu9A/0?wx_fmt=jpeg',
url : 'http://www.baidu.com'}]

}else if(content === '3'){
var data = yield wechat.uploadMaterial('image',__dirname + '/2.jpg');
console.log("eventback里的data:",data);
that.body = {
type : 'image',
mediaId : data.media_id
}
}else if(content === '4'){
var data = yield wechat.uploadMaterial('image',__dirname + '/2.jpg',{type : 'image'});
that.body = {
type : 'image',
mediaId : data.media_id
}
}else if(content === '5'){
var data = yield wechat.uploadMaterial('image',__dirname + '/2.jpg',{type : 'video',description : '{"title" : "a nice place","introduction" : "think it so easy"}'});
that.body = {
type : 'video',
title : '回复视频内容',
description : '打个篮球玩玩',
mediaId : data.media_id
}
}else{
that.body = '您说的:' + content + '太复杂了';
}
}else if(message.MsgType === 'image'){
that.body = { mediaId : message.MediaId};
}else{
console.log("不是订阅或者关注");
/*that.body = message.Content;*/
}
// that.body.MsgType=message.msgType;
//return that;
yield next;
}

配置信息模块:config.js

代码展示:

'use strict'

/**
模块名:
作用:

*/

var util = require('../utils/util');

var path = require('path');

var wechat_file = path.join(__dirname,'../txt/access_token.txt');

var config = {
wechat:{
AppID : 'wx89daec921e55820c',
AppSecret : 'bf2a91d928fdd2ca0aacbf97143055ea',
Token : 'tomandjerry',
getAccessTokenFromFile : function(){
return util.readFileAsync(wechat_file,'utf-8');
},
saveAccessTokenToFile : function(data){
data = JSON.stringify(data);
return util.writeFileAsync(wechat_file,data);
}
}
}

module.exports = config;

 

xml解析功能模块展示:tpl.js

代码展示:

'use strict'
 
var ejs = require('ejs');
var heredoc = require('heredoc');
var tpl = heredoc(function(content){/*
  <xml>
    <ToUserName><![CDATA[<%= toUserName %>]]></ToUserName>
    <FromUserName><![CDATA[<%= fromUserName %>]]></FromUserName>
    <CreateTime><%= createTime%></CreateTime>
    <MsgType><![CDATA[<%= msgType %>]]></MsgType>
    <% if(msgType ==='text') { %>
      <Content><![CDATA[<%= content%>]]></Content> 
<% }else if(msgType ==='image'){%>
<Image>
<MediaId>< ![CDATA[<%=content.mediaId%>] ]></MediaId>
</Image>
<% }else if(msgType === 'voice' ) {%>
<Voice>
<MediaId>< ![CDATA[<%=content.mediaId%>] ]></MediaId>
</Voice>
<% }else if(msgType === 'video' ) {%>
<Video>
<MediaId>< ![CDATA[<%=content.mediaId%>] ]></MediaId>
<Title>< ![CDATA[<%=content.title%>] ]></Title>
<Description>< ![CDATA[<%=content.description%>] ]></Description>
</Video>
<% }else if(msgType === 'music' ) {%>
<Music>
<Title>< ![CDATA[<%=content.title%>] ]></Title>
<Description>< ![CDATA[<%=content.description%>] ]></Description>
<MusicUrl>< ![CDATA[<%=content.musicUrl%>] ]></MusicUrl>
<HQMusicUrl>< ![CDATA[<%=content.hqmusicUrl%>] ]></HQMusicUrl>
<ThumbMediaId>< ![CDATA[<%=content.thumbMediaId%>] ]></ThumbMediaId>
</Music>
<% }else 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%>] ]></PicUrl>
<Url>< ![CDATA[<%=item.url%>] ]></Url>
</item>
<%})%>
</Articles>
<% } %>
  </xml>
*/}); 
var compiled = ejs.compile(tpl);
exports = module.exports = {
  compiled:compiled
};

 

 

 

 

 

 

 

甜珊贝奇的主页 甜珊贝奇 | 初学一级 | 园豆:118
提问于:2018-04-28 15:39

建议改进一下排版,给代码加上高亮

dudu 6年前

@dudu: 嗯嗯,改好了

魅影独秀 6年前
< >
分享
最佳答案
0

挂了这么多天还是没有人来解决,不过最后还是在慕课网平台找到了答案。

其实是微信官方文档存在的缺陷,注意< !DATA[**]>尖括号和!存在的空格,去掉就可以了。

甜珊贝奇 | 初学一级 |园豆:118 | 2018-05-16 22:01
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册