npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

koa2wechat

v1.1.1

Published

simple wechat backend based on koa2

Downloads

3

Readme

#koa2wechat

简单的微信公众平台接入中间件(koa2)

#####环境依赖 node v4.4.4+ npm v3.10.6 babel v6.14.0

#####实现功能

  • 根据配置自动接入公众号
  • 默认自动回复文本消息
  • 支持自定义规则,进行自动回复(包括文本,图片,音乐,小视频,视频,图文类型)

#####快速开始 1.安装

    npm install koa2wechat

2.引入模块(模块导出{Wechat,WeConnector,WeHandler,WeReply},下文详细介绍)

    import Koa from 'koa2'
    // 这两个是koa2中间件
    import {WeConnector,WeHandler} from './src/index'

3.引入配置文件

    import {weconfig} from './config'
    // 或者 import config from './config'
    // let weconfig = config.weconfig

配置文件示例:config.example.js

    let config = {
    	weconfig:{
    		local_token_path:__dirname + '/src/token.txt',
    		appid:"your app id",
    		secret:"your secret",
    		token:"your token for encrypt"
    	}
    }

    export const {weconfig} = config
    export default config

4.装载中间件、启动服务

    const app = new Koa()
    app
    // WeConnector可以使用 用户自定义的token 接入微信平台
    .use(WeConnector(weconfig.token))
    // 接入后,WeHandler 处理用户请求 这里的参数是根据业务逻辑编写的代码,具体看下面的介绍
    .use(WeHandler(null))
    app.listen(3000)

此时公众号会默认自动回复文本消息,若要自定义规则需要将上面的null替换为根据业务逻辑写出的handler 至此,之用到了4个模块中的2个模块,实际{WeConnector,WeHandler}这两个模块是koa2中间件 剩下的{Wechat,WeReply}则是用于操作微信后台的工具类

5.根据业务逻辑自定义handler,要特别注意的是,handler一定要返回一个Promise或者它是一个Promise 例如:/src/handler/defaultHandler.js

    import {WeReply} from '../index'
    // WeReply 是一个类,用于创建各类返回信息,生成对应的xml字符串
    const welcomeMsg = 'hello from koa2wechat'

    // 返回一个Promise对象
    let defaultHandler = (xml)=>{
      // 获取来源FromUserName,在回复信息中将其设置为目的地 `to`
      // 获取我们公众号的标识ToUserName,在回复信息中将其设置为发送地 `from`
      let {FromUserName,ToUserName} = xml

      // 这里可以插入需要执行的业务逻辑代码,比如判断消息来源是谁,他回复了什么类型的数据之类的


      // 实例化weReply
      let weReply = new WeReply()

      // 生成回复将会使用到的meta信息,包括 本机`from`, 目标`to`, 时间戳`ts`(时间戳非必选项)
      let meta = {from:ToUserName,to:FromUserName,ts:new Date().getTime()}
      // 回复文本类型的数据,要构造选项`type` 和 回复内容`content` 以及上面获取的 meta 信息
      // 更多类型的数据在下面会提供
      let textRpl = {
        meta:meta,
        type:"text",
        content:welcomeMsg
      }
      // 传入回复选项,生成相应的xml字符串
      let rpl = weReply.genXML(textRpl)
      // 把Promise 返回回去
      return Promise.resolve(rpl)
    }

    export default defaultHandler

推荐方式 随着业务逻辑的复杂度增加,可以考虑使用一个handler来分发各个事件的handler来实现响应。比如实现一个handler 例如:src/handler/handler.js 配合上文的defaultHandler

    import defaultHandler from './defaultHandler'

    /**
     * 示例 handler
     * @param  {obj} xml , 根据微信服务器推送的xml数据转化的Object,譬如,
     *      当接收到文本信息的时候,xml为:
     *        {
     *          ToUserName:"",
     *          FromUserName:"",
     *          CreateTime:12345678,
     *          MsgType:"text",
     *          Content:"this is a test",
     *          MsgId:1234567890123456
     *        }
     *      接收到图片信息的时候为:
     *        {
     *          ToUserName:"",
     *          FromUserName:"",
     *          CreateTime:12345678,
     *          MsgType:"image",
     *          PicUrl:"",
     *          MediaId:"",
     *          MsgId:1234567890123456
     *        }
     *  其他类型同理,具体信息参考 http://mp.weixin.qq.com/wiki/17/f298879f8fb29ab98b2f2971d42552fd.html
     * @return {Promise}     [一定记得返回Promise对象]
     */
    let handler = (xml)=>{
        let {MsgType} = xml
        if(!MsgType) return
        switch(MsgType){
            // 下面定义了客户端可能的几种请求(应该是全部包含了的)
            // 订阅的时候会触发'event'
            case 'event':
            // 客户端发送文字/图片/语音/视频/小视频/地址/链接时,MsgType的相应值,可以参考
            // http://mp.weixin.qq.com/wiki/17/f298879f8fb29ab98b2f2971d42552fd.html
            case 'text':
            case 'image':
            case 'voice':
            case 'video':
            case 'shortvideo':
            case 'location':
            case 'link':
            default:
            // 这里的defaultHandler是一个Promise对象
                return defaultHandler(xml)
        }
    }
    export {handler}
    export default handler

然后再将handler引入,将第4步中的null替换为handler即可 其实,到这里为止,我们一直没有用到Wechat对象,是因为,微信公众号有不同的类型 参考:官方文档 (未认证订阅号 微信认证订阅号 未认证服务号 微信认证服务号) 不同类型的号,有不同的权限,譬如对素材的管理,用户的管理,等等。 所有的操作都涉及到access_token,而Wechat类的核心就是要维护access_token, 现在的Wechat类,只简单实现了对access_token的维护, 因此有需要的开发者可以自行实现在这个基础之上,自己需要的功能,譬如素材上传,对象分组等等。

6.示例程序在这里:

#####结构说明:

koa2wechat
├── config.example.js(config示例)
├── lib
│   ├── api.js
│   ├── handler
│   │   ├── defaultHandler.js
│   │   └── handler.js
│   ├── index.js
│   ├── wechat
│   │   ├── apitest.js
│   │   ├── Exception.js
│   │   ├── Loader.js
│   │   ├── Reply.js
│   │   ├── WechatApi.js
│   │   ├── Wechat.js
│   │   ├── WeConnector.js
│   │   ├── WeHandler.js
│   │   └── WeReply.js
│   └── xml
│       ├── formatter.js
│       ├── parser.js
│       ├── templates
│       │   ├── imageTpl.js
│       │   ├── musicTpl.js
│       │   ├── newsItemTpl.js
│       │   ├── newsTpl.js
│       │   ├── textTpl.js
│       │   ├── videoTpl.js
│       │   └── voiceTpl.js
│       └── templates.js
├── package.json
├── readme.md
├── server.js(模拟server)
├── src
│   ├── api.js(api接口,暂时只添加了素材接口)
│   ├── handler
│   │   ├── defaultHandler.js
│   │   └── handler.js
│   ├── index.js(主文件)
│   ├── wechat
│   │   ├── Exception.js(异常类)
│   │   ├── token.txt(存储access_token)
│   │   ├── WechatApi.js(类,暂时只实现了素材管理)
│   │   ├── Wechat.js(基类,只负责access_token的维护)
│   │   ├── WeConnector.js(中间件,只负责接入微信server)
│   │   ├── WeHandler.js(中间件,负责业务逻辑)
│   │   └── WeReply.js(类,用于创建各类回复信息)
│   └── xml
│       ├── formatter.js(扁平化object)
│       ├── parser.js(解析xml至object)
│       ├── templates(回复模板)
│       │   ├── imageTpl.js
│       │   ├── musicTpl.js
│       │   ├── newsItemTpl.js
│       │   ├── newsTpl.js
│       │   ├── textTpl.js
│       │   ├── videoTpl.js
│       │   └── voiceTpl.js
│       └── templates.js(模板入口)
└── test(测试/未完成)
    ├── mocha.opts
    ├── ReplyBuilder.js
    └── templates.js

###更新 2016-09-20-------------------增加新类WechatApi(实现素材上传下载接口) 导出接口:

	/**
	 * [uploadTemp 用于上传临时素材]
	 * @param  {[string]} type      [媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)]
	 * @param  {[string]} mediaPath [文件全路径,例如‘../../test.jpg’]
	 * @return {[Promise]}           [返回一个网络请求Promise]
	 */
     uploadTemp(type,mediaPath)

	/**
	 * [downloadTemp 下载临时素材]
	 * @param  {[string]} mediaId   [参数]
	 * @param  {[string]} directory [存储素材目录]
	 * @return {[promise]}           [成功将resolve已下载的素材名]
	 */
	downloadTemp(mediaId,directory,isVideo=false)
    
	/**
	 * [uploadNews 上传永久素材--图文]
	 * @param  {[object]} articles 详细字段参考:http://mp.weixin.qq.com/wiki/10/10ea5a44870f53d79449290dfd43d006.html
	 * 例如  {
	 *        "title": TITLE,
	 *        "thumb_media_id": THUMB_MEDIA_ID,
	 *        "author": AUTHOR,
	 *        "digest": DIGEST,
	 *        "show_cover_pic": SHOW_COVER_PIC(0 / 1),
	 *        "content": CONTENT,
	 *        "content_source_url": CONTENT_SOURCE_URL
	 *      }
	 * @return {[Promise]}          [axios Post]
	 */
	uploadNews(articles)
    
	/**
	 * [uploadPerm 上传除了图文类型的其他永久素材]
	 * @param  {[string]} type      [媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)]
	 * @param  {[string]} mediaPath [目标文件路径]
	 * @param  {[object]} videoDesc [只有上传类型为‘video’才需要,例如:
	 * {
	 * 		"title":VIDEO_TITLE,
	 * 		"introduction":INTRODUCTION
	 * 	}					  						  
	 * @return {[Promise]}           [axios POST]
	 */
	uploadPerm(type,mediaPath,videoDesc)
    
	/**
	 * [downloadPerm 下载永久素材]
	 * @param  {[string]} type      [如果要获取图文,则填写(news),获取视频填写(video),获取图片(image)、语音(voice)和缩略图(thumb)]
	 * @param  {[string]} mediaId   [media_id]
	 * @param  {[存储在本地的目录]} directory [图文直接返回字符串,不会写入本地]
	 * @return {[Promise]}           [获取图文最终返回json,其他多媒体类型最终返回文件名]
	 */
	downloadPerm(type,mediaId,directory)
    
	/**
	 * [removePerm 删除永久素材]
	 * @param  {[string]} mediaId [media_id]
	 * @return {[Promise]}         [最终返回json{status:true,item:form},status代表是否删除成功。item为post的内容,如果不成功可以缓存下来]
	 */
	removePerm(mediaId)
    
	/**
	 * [updateNews 更新永久图文素材] 具体参数意义请参考:http://mp.weixin.qq.com/wiki/10/c7bad9a463db20ff8ccefeedeef51f9e.html
	 * @param  {[string]} mediaId [media_id]
	 * @param  {[int]} index   [index]
	 * @param  {[object]} article [article]
	 * @return {[type]}         [最终返回json{status:true,item:form},status代表是否删除成功。item为post的内容,如果不成功可以缓存下来]
	 */
	updateNews(mediaId,index,article)
    
	/**
	 * [getPermCount 获取永久素材类型和个数]
	 * @return {[Promise]} 最终返回json,形如
	 * {
	 *	  "voice_count":COUNT,
	 *	  "video_count":COUNT,
	 *	  "image_count":COUNT,
	 *	  "news_count":COUNT
	 * }
	 */
	getPermCount()
    
	/**
	 * [batchGetPerm 批量获取某类型的永久素材信息]
	 * @param  {[string]} type   [素材的类型,图片(image)、视频(video)、语音 (voice)、图文(news)]
	 * @param  {[int]} offset [从全部素材的该偏移位置开始返回,0表示从第一个素材 返回]
	 * @param  {[int]} count  [返回素材的数量,取值在1到20之间]
	 * @return {[Promise]}        [最终返回参数请参考:http://mp.weixin.qq.com/wiki/15/8386c11b7bc4cdd1499c572bfe2e95b3.html]
	 */
	batchGetPerm(type,offset,count)

使用方法:

// weconfig 和 新建Wechat对象的weconfig格式相同
let wechatapi = new WechatApi(weconfig)

// 上传临时素材-thumb
wechatapi
.uploadTemp('thumb',__dirname + '/../../test/logo.png')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})

// 上传临时素材 voice
// server error 猜想并不支持amr格式
wechatapi
.uploadTemp('voice',__dirname + '/../../material/voice.amr')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})

// 上传临时素材 video
wechatapi
.uploadTemp('video',__dirname + '/../../material/gf.mp4')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})


// 下载临时素材
// 视频只支持http协议
wechatapi
.downloadTemp(mediaId,__dirname + '/../../material',true)
.then(filename=>{
	console.log(`==>${filename} downloaded!`)
})
.catch(e=>console.log(e))

// 上传永久素材-news 注意 articles 长度<=8
let thumb_media_id = 'LnFqDNEdJXP8Mt8lfcrKcipoGRdbxawd5iF-4CoBjRk'
let articles = [{
       "title": 'logo',
       "thumb_media_id": thumb_media_id,
       "author": "徐涌盛",
       "digest": "digest",
       "show_cover_pic": 1,
       "content": "这里是内容1",
       "content_source_url": "https://github.com/chux0519"
    },
    {
       "title": 'logo2',
       "thumb_media_id": thumb_media_id,
       "author": "徐涌盛",
       "digest": "digest2",
       "show_cover_pic": 1,
       "content": "这里是内容2",
       "content_source_url": "https://github.com/chux0519"
    }]
wechatapi.uploadNews(articles)
.then(response=>{
	console.log(response.data)
})
.catch(e=>console.log(e))


// 上传永久素材-thumb
wechatapi
.uploadPerm('thumb',__dirname + '/../../test/logo.png')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})

// 上传永久素材-image
wechatapi
.uploadPerm('image',__dirname + '/../../test/logo.png')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})


// 上传永久素材-video
wechatapi
.uploadPerm('video',__dirname + '/../../material/gf.mp4',videoDesc)
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})

// 上传永久素材-voice
wechatapi
.uploadPerm('voice',__dirname + '/../../material/voice.amr')
.then(response=>console.log(response.data))
.catch(e=>{console.log(e)})


// 下载永久素材-articles
let mediaId = 'LnFqDNEdJXP8Mt8lfcrKcsQ7QsxUDgVa_23horR0Hi0'
wechatapi.downloadPerm('news',mediaId,'./')
.then(response=>{
	if(response.data)
		console.log(response.data)
	else
		console.log(response)
})
.catch(e=>console.log(e))

// 下载永久素材-video
let mediaId = 'LnFqDNEdJXP8Mt8lfcrKckzvFfJcIuHQNWv039vuqZA'
wechatapi.downloadPerm('video',mediaId,__dirname + '/../../material')
.then(response=>{
	if(response.data)
		console.log(response.data)
	else
		console.log(response)
})
.catch(e=>console.log(e))

// 下载永久素材-image
// 测试号测试时有问题,文件会莫名其妙大几百b
// curl和axios下载的结果相同,初步认为是微信服务器/或者是测试号的原因
let mediaId = 'LnFqDNEdJXP8Mt8lfcrKctnwaBVhh1Zq7VoRaBEOZN8'

wechatapi.downloadPerm('thumb',mediaId,__dirname + '/../../material')
.then(response=>{
	if(response.data)
		console.log(response.data)
	else
		console.log(response)
})
.catch(e=>console.log(e))


// 删除永久素材
wechatapi.removePerm("123456")
.then(msg=>console.log(msg))
.catch(e=>console.log(e))

// 更改永久图文消息 
let mediaId = 'LnFqDNEdJXP8Mt8lfcrKcsQ7QsxUDgVa_23horR0Hi0'
// 不能改变已经放上去图文素材的长度 index<length
let index = 0
let article = {
       "title": "这里是更新后的标题",
       "thumb_media_id": 'LnFqDNEdJXP8Mt8lfcrKcipoGRdbxawd5iF-4CoBjRk',
       "author": "徐涌盛",
       "digest": "",
       "show_cover_pic": 1,
       "content": "这里是内容",
       "content_source_url": "https://github.com/chux0519"
    }
wechatapi.updateNews(mediaId,index,article)
.then(msg=>console.log(msg))
.catch(e=>console.log(e))

// 获取永久素材条数 
{ voice_count: 0, video_count: 4, image_count: 5, news_count: 1 }
wechatapi.getPermCount()
.then(response=>console.log(response.data))
.catch(e=>console.log(e))

// 批量获取永久素材
wechatapi.batchGetPerm("image",0,5)
.then(response=>console.log(response.data))
.catch(e=>console.log(e))