hucheng1997

一、微信公众号设计思路

二、微信公众号的分类

1、订阅号

1.简介 为媒体和个人提供一种新的信息传播方式,主要功能是在微信侧给用户传达资讯;(功能类似报纸杂志,提供新闻信息或娱乐趣事)

2.适用主要人群:个人、媒体。

3.群发次数 订阅号(认证用户、非认证用户)1天内可群发1条消息。

2、服务号

1.简介 为企业和组织提供更强大的业务服务与用户管理能力,主要偏向服务类交互(功能类似银行,12315,114等)

2.适用主要人群:企业、政府或其他组织。

3.群发次数 服务号1个月(按自然月)内可发送4条群发消息。

三、微信公众号的开发

1、验证服务器的合法性

①测试账号的使用

开发/开发者工具 => 开发者文档 => 开始开发/接口测试号申请 => 进入微信公众帐号测试号申请系统

填写URL和Token

②验证服务器地址的有效性

①将token、timestamp、nonce三个参数进行字典序排序

②将三个参数字符串拼接成一个字符串进行sha1加密

③开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

 ()=>{
  return async(req,res,next) =>{
  const {signature,echostr,timestamp,nonce}=rquery.query\
  //获取自己配置的token
  const {token}=config
  const sha1Str= sha1([timestamp,nonce,token].sort().join(""))
  if(req.mothod===\'GET\'){
  if(sha1Str===signature){
  res.send(echostr)
  }else{
  res.send("error")
  }
  }
  }
 }

2、获取access_token

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。

2小时需要更新一次,提前5分钟刷新

 /*
  用来获取access_token
  */
 getAccessToken() {
    const url = `${api.access_token}&appid=${appID}&secret=${appsecret}`
    return new Promise((resolve, reject) => {
        rp({method: "GET", url, json: true}).then(res => {
            /*
            access_token: \'23_E3GhXnvYKh6xxdPq0e5ECFgXI4DeU2FNNGbQUKRX1O-4a0KrVJGduEwnBgul730-9qEkxqvnE2KE9YPxUrNVIzvcnlBfTqapB2HQGw840fjoGXs3L7UN08GrqQDOGV2UlX3P01lqKo0uLtcuTGWiAHAFSI\',
            expires_in: 7200
              */
            res.expires_in = Date.now() + (res.expires_in - 300) * 1000
            resolve(res)
        }).catch(error => {
            reject("30:" + \'getAccessToken()方法出错:\' + error)
        })
    })
 
 }
 
 /*
 保存access_token
  */
 saveAccessToken(access_token) {
    return writeFileAsync(access_token, \'accessToken.txt\')
 }
 
 /*
 读取access_token
  */
 readAccessToken() {
    return readFileAsync(\'accessToken.txt\')
 
 }
 
 /*
 用来检测access_token是否有效
  */
 isValidAccessToken(data) {
    if (!data || !data.access_token || !data.expires_in) {
        //代表access_token无效
        return false
    }
    if (data.expires_in < Date.now()) {
        //代表access过期
        return false
    }
    return true
 }
 
 /*
 获取没有过期的access_token
  */
 fetchAccessToken() {
 
    //优化
    if (this.access_token && this.expires_in && this.isValidAccessToken(this)) {
 
        //说明之前保存过access_token,并且它是有效的, 直接使用
        return Promise.resolve({
            access_token: this.access_token,
            expires_in: this.expires_in
        })
    }
    return new Promise((resolve, reject) => {
        this.readAccessToken()
            .then(async res => {
                //本地有文件,需要判断是否过期
                if (this.isValidAccessToken(res)) {
                    //有效的
                    // resolve(res)
                    return Promise.resolve(res)
                } else {
                    const res = await this.getAccessToken()
                    await this.saveAccessToken(res)
                    //返回access_token
                    // resolve(res)
                    return Promise.resolve(res)
                }
            })
            .catch(async err => {
                const res = await
                    this.getAccessToken()
                await this.saveAccessToken(res)
                //返回access_token
                //resolve(res)
                return Promise.resolve(res)
            })
            .then(res => {
                //将access_token挂载到this上
                this.access_token = res.access_token;
                this.expires_in = res.expires_in;
 
                //返回res包装了一层promise对象(此对象为成功的状态)
                //是this.readAccessToken()最终的返回值
                resolve(res);
            })
    })
 }

3、自动回复消息

 else if (req.method === \'POST\') {
    if (sha1Str != signature) {
    //验证失败
    res.end("error")
    }
         
    const xmlData = await getUserDataAsync(req)
    //将xml解析为js
    const jsData = await parseXMLAsync(xmlData)
 
    const message = await formatMessage(jsData)
    const options = await reply(message)
    let replyMessage = template(options)
    res.send(replyMessage)
 
 }
 
 /*
  获取用户发送的消息
 */
 getUserDataAsync(req) {
    return new Promise((resolve, reject) => {
        let xmlData = "";
        req
            .on(\'data\', data => {
                //当流式数据传递过来的时候,会触发当前事件,会将数据注入到回调函数汇总
                //data为buffer类型
                data = data.toString()
                xmlData += data;
            })
            .on(\'end\', () => {
                //当数据接收完毕时,会触发当前函数
                resolve(xmlData)
            })
    })
 
 }  
 
 
 const {parseString} = require("xml2js")
 /*
  将xml转换为js
 */
 parseXMLAsync(xmlData) {
    return new Promise((resolve, reject) => {
        parseString(xmlData, {trim: true}, (err, data) => {
            if (!err) {
                resolve(data)
            } else {
                reject("parseXMLAsync有误:" + err)
            }
        })
    })
 }
 
 formatMessage(jsData) {
    return new Promise((resolve, reject) => {
        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]
                }
            }
        }
        resolve(message)
    })
 }
 
 [reply.js]
 const Theaters = require(\'../model/Theaters\');
 const Trailers = require(\'../model/Trailers\');
 const {url, qiniuImgUrl} = require(\'../config\')
 /*
  处理用户发送的消息类型和内容,决定返回不同的内容给用户
  */
 module.exports = message => {
 
    return new Promise(async (resolve, reject) => {
 
        let options = {
            toUserName: message.FromUserName,
            fromUserName: message.ToUserName,
            createTime: Date.now(),
            msgType: \'text\'
        }
        let content = \'您在说什么,我听不懂?\';
 
        if (message.MsgType === \'text\') {
            //用户发送文字信息
            if (message.Content === \'预告片\') {
                const data = await Trailers.find({}, {posterKey: 1, _id: 0})
                //将回复内容初始化为空数据
                content = []
                options.msgType = \'news\'
                content.push({
                    title: "最新热门电影预告片",
                    description: "点击查看最近热门预告片",
                    picUrl: `${qiniuImgUrl}${data[0].posterKey}`,
                    url: `${url}/trailer`
                })
            }
        } else if (message.MsgType === \'voice\') {
            options.msgType = \'voice\'
            options.mediaId = message.MediaId
            const text = message.Recognition.toString().replace(\'。\', "")
            console.log(text)
            const data = await Theaters.findOne({title: text}, {
                title: 1,
                summary: 1,
                doubanId: 1,
                image: 1,
                _id: 0
            })
            //将回复内容初始化为空数据
            content = []
            options.msgType = \'news\'
            content.push({
                title: data.title,
                description: data.summary,
                picUrl: data.image,
                url: `${url}/detail/${data.doubanId}`
            })
        } else if (message.MsgType === \'event\') {
            if (message.Event === \'subscribe\') {
                options.msgType = \'text\'
                content = \'欢迎您关注XXX电影公众号~ \n\' +
                    \'回复 预告片 查看最新热门电影预告片 \n\' +
                    \'回复 文本 搜索电影信息 \n\' +
                    \'回复 语音 搜索电影信息 \n\' +
                    \'也可以点击下面菜单按钮,来了解XXX电影公众号\'
            } else if (message.Event === \'unsubscribe\') {
                options.msgType = \'event\'
                content = \'取关了哈哈\'
            } else if (message.Event === \'CLICK\') {
                content = \'您可以按照以下提示来进行操作~ \n\' +
                    \'回复 预告片 查看最新热门电影预告片 \n\' +
                    \'回复 文本 搜索电影信息 \n\' +
                    \'回复 语音 搜索电影信息 \n\' +
                    \'也可以点击下面菜单按钮,来了解XXX电影公众号\'
            }
        }
        options.content = content
        resolve(options)
    })
 
 }

4、上传素材

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。

①上传临时素材

 uploadTemporaryMeterial(type, fileName) {
    //获取文件的绝对路径
    const filePath = resolve(__dirname, \'../media\', fileName)
    return new Promise(async (resolve1, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken()
            //定义请求的地址
            const url = `${api.temporary.upload}access_token=${data.access_token}&type=${type}`
 
            const formData = {
                media: createReadStream(fileName)
            }
            //以form表单的方式发送请求
            const result = rp({method: \'POST\', url, json: true, formData})
            //将数据返回给用户
            resolve(result)
        } catch (e) {
            reject(\'uploadTemporaryMaterial方法出了问题:\' + e);
        }
 
    })
 }

② 获取临时素材

 getTemporaryMeterial(type, mediaId, fileName) {
    //获取文件的绝对路径
    const filePath = resolve(__dirname, \'../media\', fileName);
    return new Promise(async (resolve1, reject) => {
        //获取access_token
        const data = await this.fetchAccessToken()
        //定义请求地址
        let url = `${api.temporary.get}access_token=${data.access_token}&media_id=${mediaId}`
        if (type === \'video\') {
            //视频文件只支持http协议
            url = url.replace(\'https://\', \'http://\');
            //发送请求
            const data = await rp({method: \'GET\', url, json: true});
            //返回出去
            resolve(data);
        } else {
            //其他类型文件
            request(url)
                .pipe(createWriteStream(filePath))
                .once(\'close\', resolve)   //当文件读取完毕时,可读流会自动关闭,一旦关闭触发close事件,从而调用resolve方法通知外部文件读取完毕了
        }
    })
 }

③上传永久素材

 uploadPermanentMaterial (type, material, body) {
 
    return new Promise(async (resolve, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken();
            //请求的配置对象
            let options = {
                method: \'POST\',
                json: true
            }
 
            if (type === \'news\') {
                //上传图文消息
                options.url = `${api.permanment.uploadNews}access_token=${data.access_token}`;
                options.body = material;
            } else if (type === \'pic\') {
                //上传图文消息中的图片
                options.url = `${api.permanment.uploadImg}access_token=${data.access_token}`;
                options.formData = {
                    media: createReadStream(join(__dirname, \'../media\', material))
                }
            } else {
                //其他媒体素材的上传
                options.url = `${api.permanment.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);
        }
 
    })
 }

④获取永久素材

  
 getPermanentMaterial (type, mediaId, fileName) {
    return new Promise(async (resolve, reject) => {
        try {
            //获取access_token
            const data = await this.fetchAccessToken();
            //定义请求地址
            const url = `${api.permanment.get}access_token=${data.access_token}`;
 
            const options = {method: \'POST\', url, json: true, body: {media_id: mediaId}};
            //发送请求
            if (type === \'news\' || \'video\') {
                const data = await rp(options);
                resolve(data);
            } else {
                request(options)
                    .pipe(createWriteStream(join(__dirname, \'../media\', fileName)))
                    .once(\'close\', resolve)
            }
        } catch (e) {
            reject(\'getPermanentMaterial方法出了问题:\' + e);
        }
 
    })
 
 }

5、菜单

 /*
  菜单栏的组成
 */
 const {url}=require(\'../config\')
 module.exports = {
    "button": [
        {
            "type":"view",
            "name":"预告片

分类:

技术点:

相关文章: