Tao Zhang 5 жил өмнө
commit
4b764e3830
4 өөрчлөгдсөн 1694 нэмэгдсэн , 0 устгасан
  1. 2 0
      .gitignore
  2. 386 0
      README.md
  3. 3 0
      go.mod
  4. 1303 0
      weixin.go

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+##
+.idea

+ 386 - 0
README.md

@@ -0,0 +1,386 @@
+# 微信公众平台库 – Go语言版本
+
+## 简介
+
+这是一个使用Go语言对微信公众平台的封装。参考了微信公众平台API文档
+
+## 入门
+
+### 安装
+
+通过执行下列语句就可以完成安装
+
+	go get app.yhyue.com/BP/wx_util
+
+### 注册微信公众平台
+
+注册微信公众平台,填写验证微信公众平台的Token
+
+### 示例
+
+```Go
+package main
+
+import (
+	"github.com/wizjin/weixin"
+	"net/http"
+)
+
+// 文本消息的处理函数
+func Echo(w weixin.ResponseWriter, r *weixin.Request) {
+	txt := r.Content			// 获取用户发送的消息
+	w.ReplyText(txt)			// 回复一条文本消息
+	w.PostText("Post:" + txt)	// 发送一条文本消息
+}
+
+// 关注事件的处理函数
+func Subscribe(w weixin.ResponseWriter, r *weixin.Request) {
+	w.ReplyText("欢迎关注") // 有新人关注,返回欢迎消息
+}
+
+func main() {
+	// my-token 验证微信公众平台的Token
+	// app-id, app-secret用于高级API调用。
+	// 如果仅使用接收/回复消息,则可以不填写,使用下面语句
+	// mux := weixin.New("my-token", "", "")
+	mux := weixin.New("my-token", "app-id", "app-secret")
+	// 设置AES密钥,如果不使用AES可以省略这行代码
+	mux.SetEncodingAESKey("encoding-AES-key")
+	// 注册文本消息的处理函数
+	mux.HandleFunc(weixin.MsgTypeText, Echo)
+	// 注册关注事件的处理函数
+	mux.HandleFunc(weixin.MsgTypeEventSubscribe, Subscribe)
+	http.Handle("/", mux) // 注册接收微信服务器数据的接口URI
+	http.ListenAndServe(":80", nil) // 启动接收微信数据服务器
+}
+```
+
+微信公众平台要求在收到消息后5秒内回复消息(Reply接口)
+如果时间操作很长,则可以使用Post接口发送消息
+如果只使用Post接口发送消息,则需要先调用ReplyOK来告知微信不用等待回复。
+
+### 处理函数
+
+处理函数的定义可以使用下面的形式
+
+```Go
+func Func(w weixin.ResponseWriter, r *weixin.Request) {
+	...
+}
+```
+
+可以注册的处理函数类型有以下几种
+
+- `weixin.MsgTypeText`				接收文本消息
+- `weixin.MsgTypeImage`				接收图片消息
+- `weixin.MsgTypeVoice`				接收语音消息
+- `weixin.MsgTypeVideo`				接收视频消息
+- `weixin.MsgTypeShortVideo`		接收小视频消息
+- `weixin.MsgTypeLocation`			接收地理位置消息
+- `weixin.MsgTypeLink`				接收链接消息
+- `weixin.MsgTypeEventSubscribe`	接收关注事件
+- `weixin.MsgTypeEventUnsubscribe`	接收取消关注事件
+- `weixin.MsgTypeEventScan`			接收扫描二维码事件
+- `weixin.MsgTypeEventView`			接收点击菜单跳转链接时的事件
+- `weixin.MsgTypeEventClick`		接收自定义菜单事件
+- `weixin.MsgTypeEventLocation`		接收上报地理位置事件
+- `weixin.MsgTypeEventTemplateSent` 接收模版消息发送结果
+
+### 发送被动响应消息
+
+需要发送被动响应消息,可通过`weixin.ResponseWriter`的下列方法完成
+
+- `ReplyOK()`								无同步消息回复
+- `ReplyText(text)`							回复文本消息
+- `ReplyImage(mediaId)`						回复图片消息
+- `ReplyVoice(mediaId)`						回复语音消息
+- `ReplyVideo(mediaId, title, description)`	回复视频消息
+- `ReplyMusic(music)`						回复音乐消息
+- `ReplyNews(articles)`						回复图文消息
+
+### 发送客服消息
+
+- `PostText(text)`							发送文本消息
+- `PostImage(mediaId)`						发送图片消息
+- `PostVoice(mediaId)`						发送语音消息
+- `PostVideo(mediaId, title, description)`	发送视频消息
+- `PostMusic(music)`						发送音乐消息
+- `PostNews(articles)`						发送图文消息
+
+### 发送模版消息
+
+如需要发送模版消息,需要先获取模版ID,之后再根据ID发送。
+
+```Go
+func GetTemplateId(wx *weixin.Weixin) {
+	tid, err := wx.AddTemplate("TM00015")
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(ips)	// 模版ID
+	}
+}
+```
+
+随后可以发送模版消息了。
+
+```Go
+func SendTemplateMessage(w weixin.ResponseWriter, r *weixin.Request) {
+	templateId := ...
+	url := ...
+	msgid, err := w.PostTemplateMessage(templateId, url,
+		weixin.TmplData{ "first": weixin.TmplItem{"Hello World!", "#173177"}})
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(msgid)
+	}
+}
+```
+
+在模版消息发送成功后,还会通过类型为`MsgTypeEventTemplateSent`的消息推送获得发送结果。
+
+- `TemplateSentStatusSuccess` 		发送成功
+- `TemplateSentStatusUserBlock`		发送失败,用户拒绝接收
+- `TemplateSentStatusSystemFailed`	发送失败,系统原因
+
+
+### 上传/下载多媒体文件
+
+使用如下函数可以用来上传多媒体文件:
+
+`UploadMediaFromFile(mediaType string, filepath string)`
+
+示例 (用一张本地图片来返回图片消息):
+
+```Go
+func ReciveMessage(w weixin.ResponseWriter, r *weixin.Request) {
+	// 上传本地文件并获取MediaID
+	mediaId, err := w.UploadMediaFromFile(weixin.MediaTypeImage, "/my-file-path")
+	if err != nil {
+		w.ReplyText("上传图片失败")
+	} else {
+		w.ReplyImage(mediaId)	// 利用获取的MediaId来返回图片消息
+	}
+}
+```
+
+使用如下函数可以用来下载多媒体文件:
+
+`DownloadMediaToFile(mediaId string, filepath string)`
+
+示例 (收到一条图片消息,然后保存图片到本地文件):
+
+```Go
+func ReciveImageMessage(w weixin.ResponseWriter, r *weixin.Request) {
+	// 下载文件并保存到本地
+	err := w.DownloadMediaToFile(r.MediaId, "/my-file-path")
+	if err != nil {
+		w.ReplyText("保存图片失败")
+	} else {
+		w.ReplyText("保存图片成功")
+	}
+}
+```
+
+### 获取微信服务器IP地址
+
+示例,获取微信服务器IP地址列表
+
+```Go
+func GetIpList(wx *weixin.Weixin) {
+	ips, err := wx.GetIpList()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(ips)	// Ip地址列表
+	}
+}
+```
+
+### 获取AccessToken
+
+示例,获取AccessToken
+
+```Go
+func GetAccessToken(wx *weixin.Weixin) {
+	a := wx.GetAccessToken
+	if time.Until(token.Expires).Seconds() > 0 {
+		fmt.Println(a.Token)	// AccessToken
+	} else {
+		fmt.Println("Timeout")	// 超时
+	}
+}
+```
+
+### 创建/换取二维码
+
+示例,创建临时二维码
+
+```Go
+func CreateQRScene(wx *weixin.Weixin) {
+	// 二维码ID - 1000
+	// 过期时间 - 1800秒
+	qr, err := wx.CreateQRScene(1000, 1800)
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		url := qr.ToURL() // 获取二维码的URL
+		fmt.Println(url)
+	}
+}
+```
+
+示例,创建永久二维码
+
+```Go
+func CreateQRScene(wx *weixin.Weixin) {
+	// 二维码ID - 1001
+	qr, err := wx.CreateQRLimitScene(1001)
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		url := qr.ToURL() // 获取二维码的URL
+		fmt.Println(url)
+	}
+}
+```
+
+### 长链接转短链接接口
+
+```Go
+func ShortURL(wx *weixin.Weixin) {
+	// 长链接转短链接
+	url, err := wx.ShortURL("http://mp.weixin.qq.com/wiki/10/165c9b15eddcfbd8699ac12b0bd89ae6.html")
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(url)
+	}
+}
+```
+
+### 自定义菜单
+
+示例,创建自定义菜单
+
+```Go
+func CreateMenu(wx *weixin.Weixin) {
+	menu := &weixin.Menu{make([]weixin.MenuButton, 2)}
+	menu.Buttons[0].Name = "我的菜单"
+	menu.Buttons[0].Type = weixin.MenuButtonTypeUrl
+	menu.Buttons[0].Url = "https://mp.weixin.qq.com"
+	menu.Buttons[1].Name = "我的子菜单"
+	menu.Buttons[1].SubButtons = make([]weixin.MenuButton, 1)
+	menu.Buttons[1].SubButtons[0].Name = "测试"
+	menu.Buttons[1].SubButtons[0].Type = weixin.MenuButtonTypeKey
+	menu.Buttons[1].SubButtons[0].Key = "MyKey001"
+	err := wx.CreateMenu(menu)
+	if err != nil {
+		fmt.Println(err)
+	}
+}
+```
+
+自定义菜单的类型有如下几种
+
+- `weixin.MenuButtonTypeKey`				点击推事件
+- `weixin.MenuButtonTypeUrl`				跳转URL
+- `weixin.MenuButtonTypeScancodePush`		扫码推事件
+- `weixin.MenuButtonTypeScancodeWaitmsg`	扫码推事件且弹出“消息接收中”提示框
+- `weixin.MenuButtonTypePicSysphoto`		弹出系统拍照发图
+- `weixin.MenuButtonTypePicPhotoOrAlbum`	弹出拍照或者相册发图
+- `weixin.MenuButtonTypePicWeixin`			弹出微信相册发图器
+- `weixin.MenuButtonTypeLocationSelect`		弹出地理位置选择器
+- `weixin.MenuButtonTypeMediaId`			下发消息(除文本消息)
+- `weixin.MenuButtonTypeViewLimited`		跳转图文消息URL
+
+示例,获取自定义菜单
+
+```Go
+func DeleteMenu(wx *weixin.Weixin) {
+	menu, err := wx.GetMenu()
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(menu)
+	}
+}
+```
+
+示例,删除自定义菜单
+
+```Go
+func DeleteMenu(wx *weixin.Weixin) {
+	err := wx.DeleteMenu()
+	if err != nil {
+		fmt.Println(err)
+	}
+}
+```
+
+### JSSDK签名
+
+示例,生成JSSDK签名
+```Go
+func SignJSSDK(wx *weixin.Weixin, url string) {
+	timestamp := time.Now().Unix()
+	noncestr := fmt.Sprintf("%d", c.randreader.Int())
+	sign, err := wx.JsSignature(url, timestamp, noncestr)
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(sign)
+	}
+}
+```
+
+### 重定向URL生成
+
+示例,生成重定向URL
+```Go
+func CreateRedirect(wx *weixin.Weixin, url string) {
+	redirect := wx.CreateRedirectURL(url, weixin.RedirectURLScopeBasic, "")
+}
+```
+
+URL的类型有如下几种:
+
+- `RedirectURLScopeBasic`					基本授权,仅用来获取OpenId或UnionId
+- `RedirectURLScopeUserInfo`				高级授权,可以用于获取用户基本信息,需要用户同意
+
+### 用户OpenId和UnionId获取
+
+示例,获取用户OpenId和UnionId
+```Go
+func GetUserId(wx *weixin.Weixin, code string) {
+	user, err := wx.GetUserAccessToken(code)
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(user.OpenId)
+		fmt.Println(user.UnionId)
+	}
+}
+```
+
+### 用户信息获取
+
+示例,获取用户信息
+```Go
+func GetUserInfo(wx *weixin.Weixin, openid string) {
+	user, err := wx.GetUserInfo(openid)
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println(user.Nickname)
+		fmt.Println(user.Sex)
+		fmt.Println(user.City)
+		fmt.Println(user.Country)
+		fmt.Println(user.Province)
+		fmt.Println(user.HeadImageUrl)
+		fmt.Println(user.SubscribeTime)
+		fmt.Println(user.Remark)
+	}
+}
+```

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module app.yhyue.com/BP/wx_util
+
+go 1.14

+ 1303 - 0
weixin.go

@@ -0,0 +1,1303 @@
+package wx_util
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha1"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/json"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strings"
+	"sync/atomic"
+	"time"
+)
+
+// nolint
+const (
+	// Event type
+	msgEvent = "event"
+
+	EventSubscribe    = "subscribe"
+	EventUnsubscribe  = "unsubscribe"
+	EventScan         = "SCAN"
+	EventView         = "VIEW"
+	EventClick        = "CLICK"
+	EventLocation     = "LOCATION"
+	EventTemplateSent = "TEMPLATESENDJOBFINISH"
+
+	// Message type
+	MsgTypeDefault           = ".*"
+	MsgTypeText              = "text"
+	MsgTypeImage             = "image"
+	MsgTypeVoice             = "voice"
+	MsgTypeVideo             = "video"
+	MsgTypeShortVideo        = "shortvideo"
+	MsgTypeLocation          = "location"
+	MsgTypeLink              = "link"
+	MsgTypeEvent             = msgEvent + ".*"
+	MsgTypeEventSubscribe    = msgEvent + "\\." + EventSubscribe
+	MsgTypeEventUnsubscribe  = msgEvent + "\\." + EventUnsubscribe
+	MsgTypeEventScan         = msgEvent + "\\." + EventScan
+	MsgTypeEventView         = msgEvent + "\\." + EventView
+	MsgTypeEventClick        = msgEvent + "\\." + EventClick
+	MsgTypeEventLocation     = msgEvent + "\\." + EventLocation
+	MsgTypeEventTemplateSent = msgEvent + "\\." + EventTemplateSent
+
+	// Media type
+	MediaTypeImage = "image"
+	MediaTypeVoice = "voice"
+	MediaTypeVideo = "video"
+	MediaTypeThumb = "thumb"
+	// Button type
+	MenuButtonTypeKey             = "click"
+	MenuButtonTypeUrl             = "view"
+	MenuButtonTypeScancodePush    = "scancode_push"
+	MenuButtonTypeScancodeWaitmsg = "scancode_waitmsg"
+	MenuButtonTypePicSysphoto     = "pic_sysphoto"
+	MenuButtonTypePicPhotoOrAlbum = "pic_photo_or_album"
+	MenuButtonTypePicWeixin       = "pic_weixin"
+	MenuButtonTypeLocationSelect  = "location_select"
+	MenuButtonTypeMediaId         = "media_id"
+	MenuButtonTypeViewLimited     = "view_limited"
+	MenuButtonTypeMiniProgram     = "miniprogram"
+	// Template Status
+	TemplateSentStatusSuccess      = "success"
+	TemplateSentStatusUserBlock    = "failed:user block"
+	TemplateSentStatusSystemFailed = "failed:system failed"
+	// Redirect Scope
+	RedirectURLScopeBasic    = "snsapi_base"
+	RedirectURLScopeUserInfo = "snsapi_userinfo"
+	// Weixin host URL
+	weixinHost               = "https://api.weixin.qq.com/cgi-bin"
+	weixinQRScene            = "https://api.weixin.qq.com/cgi-bin/qrcode"
+	weixinShowQRScene        = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
+	weixinMaterialURL        = "https://api.weixin.qq.com/cgi-bin/material"
+	weixinShortURL           = "https://api.weixin.qq.com/cgi-bin/shorturl"
+	weixinUserInfo           = "https://api.weixin.qq.com/cgi-bin/user/info"
+	weixinFileURL            = "http://file.api.weixin.qq.com/cgi-bin/media"
+	weixinTemplate           = "https://api.weixin.qq.com/cgi-bin/template"
+	weixinRedirectURL        = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
+	weixinUserAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
+	weixinJsApiTicketURL     = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
+	// Max retry count
+	retryMaxN = 3
+	// Reply format
+	replyText               = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
+	replyImage              = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
+	replyVoice              = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
+	replyVideo              = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
+	replyMusic              = "<xml>%s<MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><MusicUrl><![CDATA[%s]]></MusicUrl><HQMusicUrl><![CDATA[%s]]></HQMusicUrl><ThumbMediaId><![CDATA[%s]]></ThumbMediaId></Music></xml>"
+	replyNews               = "<xml>%s<MsgType><![CDATA[news]]></MsgType><ArticleCount>%d</ArticleCount><Articles>%s</Articles></xml>"
+	replyHeader             = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
+	replyArticle            = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
+	transferCustomerService = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
+
+	// Material request
+	requestMaterial = `{"type":"%s","offset":%d,"count":%d}`
+	// QR scene request
+	requestQRScene         = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
+	requestQRSceneStr      = `{"expire_seconds":%d,"action_name":"QR_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
+	requestQRLimitScene    = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
+	requestQRLimitSceneStr = `{"action_name":"QR_LIMIT_STR_SCENE","action_info":{"scene":{"scene_str":"%s"}}}`
+)
+
+// MessageHeader is the header of common message.
+type MessageHeader struct {
+	ToUserName   string
+	FromUserName string
+	CreateTime   int
+	MsgType      string
+	Encrypt      string
+}
+
+// Request is weixin event request.
+type Request struct {
+	MessageHeader
+	MsgId        int64 // nolint
+	Content      string
+	PicUrl       string // nolint
+	MediaId      string // nolint
+	Format       string
+	ThumbMediaId string  // nolint
+	LocationX    float32 `xml:"Location_X"`
+	LocationY    float32 `xml:"Location_Y"`
+	Scale        float32
+	Label        string
+	Title        string
+	Description  string
+	Url          string // nolint
+	Event        string
+	EventKey     string
+	Ticket       string
+	Latitude     float32
+	Longitude    float32
+	Precision    float32
+	Recognition  string
+	Status       string
+}
+
+// Music is the response of music message.
+type Music struct {
+	Title        string `json:"title"`
+	Description  string `json:"description"`
+	MusicUrl     string `json:"musicurl"`       // nolint
+	HQMusicUrl   string `json:"hqmusicurl"`     // nolint
+	ThumbMediaId string `json:"thumb_media_id"` // nolint
+}
+
+// Article is the response of news message.
+type Article struct {
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	PicUrl      string `json:"picurl"` // nolint
+	Url         string `json:"url"`    // nolint
+}
+
+// QRScene is the QR code.
+type QRScene struct {
+	Ticket        string `json:"ticket"`
+	ExpireSeconds int    `json:"expire_seconds"`
+	Url           string `json:"url,omitempty"` // nolint
+}
+
+// Menu is custom menu.
+type Menu struct {
+	Buttons []MenuButton `json:"button,omitempty"`
+}
+
+// MenuButton is the button of custom menu.
+type MenuButton struct {
+	Name       string       `json:"name"`
+	Type       string       `json:"type,omitempty"`
+	Key        string       `json:"key,omitempty"`
+	Url        string       `json:"url,omitempty"`      // nolint
+	MediaId    string       `json:"media_id,omitempty"` // nolint
+	SubButtons []MenuButton `json:"sub_button,omitempty"`
+	AppId      string       `json:"appid,omitempty"` // nolint
+	PagePath   string       `json:"pagepath,omitempty"`
+}
+
+// UserAccessToken access token for user.
+type UserAccessToken struct {
+	AccessToken   string `json:"access_token"`
+	RefreshToken  string `json:"refresh_token"`
+	ExpireSeconds int    `json:"expires_in"`
+	OpenId        string `json:"openid"` // nolint
+	Scope         string `json:"scope"`
+	UnionId       string `json:"unionid,omitempty"` // nolint
+}
+
+// UserInfo store user information.
+type UserInfo struct {
+	Subscribe     int    `json:"subscribe,omitempty"`
+	Language      string `json:"language,omitempty"`
+	OpenId        string `json:"openid,omitempty"`  // nolint
+	UnionId       string `json:"unionid,omitempty"` // nolint
+	Nickname      string `json:"nickname,omitempty"`
+	Sex           int    `json:"sex,omitempty"`
+	City          string `json:"city,omitempty"`
+	Country       string `json:"country,omitempty"`
+	Province      string `json:"province,omitempty"`
+	HeadImageUrl  string `json:"headimgurl,omitempty"` // nolint
+	SubscribeTime int64  `json:"subscribe_time,omitempty"`
+	Remark        string `json:"remark,omitempty"`
+	GroupId       int    `json:"groupid,omitempty"` // nolint
+}
+
+// Material data.
+type Material struct {
+	MediaId    string `json:"media_id,omitempty"` // nolint
+	Name       string `json:"name,omitempty"`
+	UpdateTime int64  `json:"update_time,omitempty"`
+	CreateTime int64  `json:"create_time,omitempty"`
+	Url        string `json:"url,omitempty"` // nolint
+	Content    struct {
+		NewsItem []struct {
+			Title            string `json:"title,omitempty"`
+			ThumbMediaId     string `json:"thumb_media_id,omitempty"` // nolint
+			ShowCoverPic     int    `json:"show_cover_pic,omitempty"`
+			Author           string `json:"author,omitempty"`
+			Digest           string `json:"digest,omitempty"`
+			Content          string `json:"content,omitempty"`
+			Url              string `json:"url,omitempty"`                // nolint
+			ContentSourceUrl string `json:"content_source_url,omitempty"` // nolint
+		} `json:"news_item,omitempty"`
+	} `json:"content,omitempty"`
+}
+
+// Materials is the list of material
+type Materials struct {
+	TotalCount int        `json:"total_count,omitempty"`
+	ItemCount  int        `json:"item_count,omitempty"`
+	Items      []Material `json:"item,omitempty"`
+}
+
+// TmplData for mini program
+type TmplData map[string]TmplItem
+
+// TmplItem for mini program
+type TmplItem struct {
+	Value string `json:"value,omitempty"`
+	Color string `json:"color,omitempty"`
+}
+
+// TmplMiniProgram for mini program
+type TmplMiniProgram struct {
+	AppId    string `json:"appid,omitempty"` // nolint
+	PagePath string `json:"pagepath,omitempty"`
+}
+
+// TmplMsg for mini program
+type TmplMsg struct {
+	ToUser      string           `json:"touser"`
+	TemplateId  string           `json:"template_id"`           // nolint
+	Url         string           `json:"url,omitempty"`         // nolint 若填写跳转小程序 则此为版本过低的替代跳转url
+	MiniProgram *TmplMiniProgram `json:"miniprogram,omitempty"` // 跳转小程序 选填
+	Data        TmplData         `json:"data,omitempty"`
+	Color       string           `json:"color,omitempty"` // 全局颜色
+}
+
+// ResponseWriter is used to output reply
+// nolint
+type ResponseWriter interface {
+	// Get weixin
+	GetWeixin() *Weixin
+	GetUserData() interface{}
+	// Reply message
+	replyMsg(msg string)
+	ReplyOK()
+	ReplyText(text string)
+	ReplyImage(mediaId string)
+	ReplyVoice(mediaId string)
+	ReplyVideo(mediaId string, title string, description string)
+	ReplyMusic(music *Music)
+	ReplyNews(articles []Article)
+	TransferCustomerService(serviceId string)
+	// Post message
+	PostText(text string) error
+	PostImage(mediaId string) error
+	PostVoice(mediaId string) error
+	PostVideo(mediaId string, title string, description string) error
+	PostMusic(music *Music) error
+	PostNews(articles []Article) error
+	PostTemplateMessage(templateid string, url string, data TmplData) (int32, error)
+	// Media operator
+	UploadMediaFromFile(mediaType string, filepath string) (string, error)
+	DownloadMediaToFile(mediaId string, filepath string) error
+	UploadMedia(mediaType string, filename string, reader io.Reader) (string, error)
+	DownloadMedia(mediaId string, writer io.Writer) error
+}
+
+type responseWriter struct {
+	wx           *Weixin
+	writer       http.ResponseWriter
+	toUserName   string
+	fromUserName string
+}
+
+type response struct {
+	ErrorCode    int    `json:"errcode,omitempty"`
+	ErrorMessage string `json:"errmsg,omitempty"`
+}
+
+// HandlerFunc is callback function handler
+type HandlerFunc func(ResponseWriter, *Request)
+
+type route struct {
+	regex   *regexp.Regexp
+	handler HandlerFunc
+}
+
+// AccessToken define weixin access token.
+type AccessToken struct {
+	Token   string
+	Expires time.Time
+}
+
+type jsAPITicket struct {
+	ticket  string
+	expires time.Time
+}
+
+// Weixin instance
+type Weixin struct {
+	token          string
+	routes         []*route
+	tokenChan      chan AccessToken
+	ticketChan     chan jsAPITicket
+	userData       interface{}
+	appID          string
+	appSecret      string
+	refreshToken   int32
+	encodingAESKey []byte
+}
+
+// ToURL convert qr scene to url.
+func (qr *QRScene) ToURL() string {
+	return (weixinShowQRScene + "?ticket=" + qr.Ticket)
+}
+
+// New create a Weixin instance.
+func New(token string, appid string, secret string) *Weixin {
+	wx := &Weixin{}
+	wx.token = token
+	wx.appID = appid
+	wx.appSecret = secret
+	wx.refreshToken = 0
+	wx.encodingAESKey = []byte{}
+	if len(appid) > 0 && len(secret) > 0 {
+		wx.tokenChan = make(chan AccessToken)
+		go wx.createAccessToken(wx.tokenChan, appid, secret)
+		wx.ticketChan = make(chan jsAPITicket)
+		go createJsAPITicket(wx.tokenChan, wx.ticketChan)
+	}
+	return wx
+}
+
+// NewWithUserData create data with userdata.
+func NewWithUserData(token string, appid string, secret string, userData interface{}) *Weixin {
+	wx := New(token, appid, secret)
+	wx.userData = userData
+	return wx
+}
+
+// SetEncodingAESKey set AES key
+func (wx *Weixin) SetEncodingAESKey(key string) error {
+	k, err := base64.StdEncoding.DecodeString(key + "=")
+	if err != nil {
+		return err
+	}
+	wx.encodingAESKey = k
+	return nil
+}
+
+// GetAppId retrun app id.
+func (wx *Weixin) GetAppId() string { // nolint
+	return wx.appID
+}
+
+// GetAppSecret return app secret.
+func (wx *Weixin) GetAppSecret() string {
+	return wx.appSecret
+}
+
+// RefreshAccessToken update access token.
+func (wx *Weixin) RefreshAccessToken() {
+	atomic.StoreInt32(&wx.refreshToken, 1)
+	<-wx.tokenChan
+}
+
+// GetAccessToken read access token.
+func (wx *Weixin) GetAccessToken() AccessToken {
+	for i := 0; i < retryMaxN; i++ {
+		token := <-wx.tokenChan
+		if time.Since(token.Expires).Seconds() < 0 {
+			return token
+		}
+	}
+	return AccessToken{}
+}
+
+// HandleFunc used to register request callback.
+func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
+	regex, err := regexp.Compile(pattern)
+	if err != nil {
+		panic(err)
+	}
+	route := &route{regex, handler}
+	wx.routes = append(wx.routes, route)
+}
+
+// PostText used to post text message.
+func (wx *Weixin) PostText(touser string, text string) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		Text    struct {
+			Content string `json:"content"`
+		} `json:"text"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "text"
+	msg.Text.Content = text
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// PostImage used to post image message.
+func (wx *Weixin) PostImage(touser string, mediaID string) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		Image   struct {
+			MediaID string `json:"media_id"`
+		} `json:"image"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "image"
+	msg.Image.MediaID = mediaID
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// PostVoice used to post voice message.
+func (wx *Weixin) PostVoice(touser string, mediaID string) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		Voice   struct {
+			MediaID string `json:"media_id"`
+		} `json:"voice"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "voice"
+	msg.Voice.MediaID = mediaID
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// PostVideo used to post video message.
+func (wx *Weixin) PostVideo(touser string, m string, t string, d string) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		Video   struct {
+			MediaID     string `json:"media_id"`
+			Title       string `json:"title"`
+			Description string `json:"description"`
+		} `json:"video"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "video"
+	msg.Video.MediaID = m
+	msg.Video.Title = t
+	msg.Video.Description = d
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// PostMusic used to post music message.
+func (wx *Weixin) PostMusic(touser string, music *Music) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		Music   *Music `json:"music"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "video"
+	msg.Music = music
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// PostNews used to post news message.
+func (wx *Weixin) PostNews(touser string, articles []Article) error {
+	var msg struct {
+		ToUser  string `json:"touser"`
+		MsgType string `json:"msgtype"`
+		News    struct {
+			Articles []Article `json:"articles"`
+		} `json:"news"`
+	}
+	msg.ToUser = touser
+	msg.MsgType = "news"
+	msg.News.Articles = articles
+	return postMessage(wx.tokenChan, &msg)
+}
+
+// UploadMediaFromFile used to upload media from local file.
+func (wx *Weixin) UploadMediaFromFile(mediaType string, fp string) (string, error) {
+	file, err := os.Open(fp)
+	if err != nil {
+		return "", err
+	}
+	defer file.Close()
+	return wx.UploadMedia(mediaType, filepath.Base(fp), file)
+}
+
+// DownloadMediaToFile used to download media and save to local file.
+func (wx *Weixin) DownloadMediaToFile(mediaID string, fp string) error {
+	file, err := os.Create(fp)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	return wx.DownloadMedia(mediaID, file)
+}
+
+// UploadMedia used to upload media with media.
+func (wx *Weixin) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
+	return uploadMedia(wx.tokenChan, mediaType, filename, reader)
+}
+
+// DownloadMedia used to download media with media.
+func (wx *Weixin) DownloadMedia(mediaID string, writer io.Writer) error {
+	return downloadMedia(wx.tokenChan, mediaID, writer)
+}
+
+// BatchGetMaterial used to batch get Material.
+func (wx *Weixin) BatchGetMaterial(materialType string, offset int, count int) (*Materials, error) {
+	reply, err := postRequest(weixinMaterialURL+"/batchget_material?access_token=", wx.tokenChan,
+		[]byte(fmt.Sprintf(requestMaterial, materialType, offset, count)))
+	if err != nil {
+		return nil, err
+	}
+	var materials Materials
+	if err := json.Unmarshal(reply, &materials); err != nil {
+		return nil, err
+	}
+	return &materials, nil
+}
+
+// GetIpList used to get ip list.
+func (wx *Weixin) GetIpList() ([]string, error) { // nolint
+	reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result struct {
+		IPList []string `json:"ip_list"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.IPList, nil
+}
+
+// CreateQRScene used to create QR scene.
+func (wx *Weixin) CreateQRScene(sceneID int, expires int) (*QRScene, error) {
+	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRScene, expires, sceneID)))
+	if err != nil {
+		return nil, err
+	}
+	var qr QRScene
+	if err := json.Unmarshal(reply, &qr); err != nil {
+		return nil, err
+	}
+	return &qr, nil
+}
+
+// CreateQRSceneByString used to create QR scene by str.
+func (wx *Weixin) CreateQRSceneByString(sceneStr string, expires int) (*QRScene, error) {
+	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRSceneStr, expires, sceneStr)))
+	if err != nil {
+		return nil, err
+	}
+	var qr QRScene
+	if err := json.Unmarshal(reply, &qr); err != nil {
+		return nil, err
+	}
+	return &qr, nil
+}
+
+// CreateQRLimitScene used to create QR limit scene.
+func (wx *Weixin) CreateQRLimitScene(sceneID int) (*QRScene, error) {
+	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitScene, sceneID)))
+	if err != nil {
+		return nil, err
+	}
+	var qr QRScene
+	if err := json.Unmarshal(reply, &qr); err != nil {
+		return nil, err
+	}
+	return &qr, nil
+}
+
+// CreateQRLimitSceneByString used to create QR limit scene by str.
+func (wx *Weixin) CreateQRLimitSceneByString(sceneStr string) (*QRScene, error) {
+	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRLimitSceneStr, sceneStr)))
+	if err != nil {
+		return nil, err
+	}
+	var qr QRScene
+	if err := json.Unmarshal(reply, &qr); err != nil {
+		return nil, err
+	}
+	return &qr, nil
+}
+
+// ShortURL used to convert long url to short url
+func (wx *Weixin) ShortURL(url string) (string, error) {
+	var request struct {
+		Action  string `json:"action"`
+		LongURL string `json:"long_url"`
+	}
+	request.Action = "long2short"
+	request.LongURL = url
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinShortURL+"?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var shortURL struct {
+		URL string `json:"short_url"`
+	}
+	if err := json.Unmarshal(reply, &shortURL); err != nil {
+		return "", err
+	}
+	return shortURL.URL, nil
+}
+
+// CreateMenu used to create custom menu.
+func (wx *Weixin) CreateMenu(menu *Menu) error {
+	data, err := marshal(menu)
+	if err != nil {
+		return err
+	}
+	_, err = postRequest(weixinHost+"/menu/create?access_token=", wx.tokenChan, data)
+	return err
+}
+
+// GetMenu used to get menu.
+func (wx *Weixin) GetMenu() (*Menu, error) {
+	reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result struct {
+		MenuCtx *Menu `json:"menu"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.MenuCtx, nil
+}
+
+// DeleteMenu used to delete menu.
+func (wx *Weixin) DeleteMenu() error {
+	_, err := sendGetRequest(weixinHost+"/menu/delete?access_token=", wx.tokenChan)
+	return err
+}
+
+// SetTemplateIndustry used to set template industry.
+func (wx *Weixin) SetTemplateIndustry(id1 string, id2 string) error {
+	var industry struct {
+		ID1 string `json:"industry_id1,omitempty"`
+		ID2 string `json:"industry_id2,omitempty"`
+	}
+	industry.ID1 = id1
+	industry.ID2 = id2
+	data, err := marshal(industry)
+	if err != nil {
+		return err
+	}
+	_, err = postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	return err
+}
+
+// AddTemplate used to add template.
+func (wx *Weixin) AddTemplate(shortid string) (string, error) {
+	var request struct {
+		Shortid string `json:"template_id_short,omitempty"`
+	}
+	request.Shortid = shortid
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var templateID struct {
+		ID string `json:"template_id,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &templateID); err != nil {
+		return "", err
+	}
+	return templateID.ID, nil
+}
+
+// PostTemplateMessage used to post template message.
+func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data TmplData) (int32, error) {
+	var msg struct {
+		ToUser     string   `json:"touser"`
+		TemplateID string   `json:"template_id"`
+		URL        string   `json:"url,omitempty"`
+		Data       TmplData `json:"data,omitempty"`
+	}
+	msg.ToUser = touser
+	msg.TemplateID = templateid
+	msg.URL = url
+	msg.Data = data
+	msgStr, err := marshal(msg)
+	if err != nil {
+		return 0, err
+	}
+	reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
+	if err != nil {
+		return 0, err
+	}
+	var resp struct {
+		MsgID int32 `json:"msgid,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &resp); err != nil {
+		return 0, err
+	}
+	return resp.MsgID, nil
+}
+
+// PostTemplateMessageMiniProgram 兼容模板消息跳转小程序
+func (wx *Weixin) PostTemplateMessageMiniProgram(msg *TmplMsg) (int64, error) {
+	msgStr, err := marshal(msg)
+	if err != nil {
+		return 0, err
+	}
+	reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
+	if err != nil {
+		return 0, err
+	}
+	var resp struct {
+		MsgID int64 `json:"msgid,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &resp); err != nil {
+		return 0, err
+	}
+	return resp.MsgID, nil
+}
+
+// CreateRedirectURL used to create redirect url
+func (wx *Weixin) CreateRedirectURL(urlStr string, scope string, state string) string {
+	return fmt.Sprintf(weixinRedirectURL, wx.appID, url.QueryEscape(urlStr), scope, state)
+}
+
+// GetUserAccessToken used to get open id
+func (wx *Weixin) GetUserAccessToken(code string) (*UserAccessToken, error) {
+	resp, err := http.Get(fmt.Sprintf(weixinUserAccessTokenURL, wx.appID, wx.appSecret, code))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	var res UserAccessToken
+	if err := json.Unmarshal(body, &res); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+// GetUserInfo used to get user info
+func (wx *Weixin) GetUserInfo(openid string) (*UserInfo, error) {
+	reply, err := sendGetRequest(fmt.Sprintf("%s?openid=%s&lang=zh_CN&access_token=", weixinUserInfo, openid), wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result UserInfo
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+// GetJsAPITicket used to get js api ticket.
+func (wx *Weixin) GetJsAPITicket() (string, error) {
+	for i := 0; i < retryMaxN; i++ {
+		ticket := <-wx.ticketChan
+		if time.Since(ticket.expires).Seconds() < 0 {
+			return ticket.ticket, nil
+		}
+	}
+	return "", errors.New("Get JsApi Ticket Timeout")
+}
+
+// JsSignature used to sign js url.
+func (wx *Weixin) JsSignature(url string, timestamp int64, noncestr string) (string, error) {
+	ticket, err := wx.GetJsAPITicket()
+	if err != nil {
+		return "", err
+	}
+	h := sha1.New()
+	h.Write([]byte(fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s", // nolint
+		ticket, noncestr, timestamp, url)))
+	return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
+
+// CreateHandlerFunc used to create handler function.
+func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		wx.ServeHTTP(w, r)
+	}
+}
+
+// ServeHTTP used to process weixin request and send response.
+func (wx *Weixin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if !checkSignature(wx.token, w, r) {
+		http.Error(w, "", http.StatusUnauthorized)
+		return
+	}
+	// Verify request
+	if r.Method == "GET" {
+		fmt.Fprintf(w, r.FormValue("echostr")) // nolint
+		return
+	}
+	// Process message
+	data, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		log.Println("Weixin receive message failed:", err)
+		http.Error(w, "", http.StatusBadRequest)
+	} else {
+		var msg Request
+		if err := xml.Unmarshal(data, &msg); err != nil {
+			log.Println("Weixin parse message failed:", err)
+			http.Error(w, "", http.StatusBadRequest)
+			return
+		}
+		if len(wx.encodingAESKey) > 0 && len(msg.Encrypt) > 0 {
+			// check encrypt
+			d, err := base64.StdEncoding.DecodeString(msg.Encrypt)
+			if err != nil {
+				log.Println("Weixin decode base64 message failed:", err)
+				http.Error(w, "", http.StatusBadRequest)
+				return
+			}
+			if len(d) <= 20 {
+				log.Println("Weixin invalid aes message:", err)
+				http.Error(w, "", http.StatusBadRequest)
+				return
+			}
+			// valid
+			strs := sort.StringSlice{wx.token, r.FormValue("timestamp"), r.FormValue("nonce"), msg.Encrypt}
+			sort.Strings(strs)
+			if fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(strs, "")))) != r.FormValue("msg_signature") {
+				log.Println("Weixin check message sign failed!")
+				http.Error(w, "", http.StatusBadRequest)
+				return
+			}
+			// decode
+			key := wx.encodingAESKey
+			b, err := aes.NewCipher(key)
+			if err != nil {
+				log.Println("Weixin create cipher failed:", err)
+				http.Error(w, "", http.StatusBadRequest)
+				return
+			}
+			bs := b.BlockSize()
+			bm := cipher.NewCBCDecrypter(b, key[:bs])
+			data = make([]byte, len(d))
+			bm.CryptBlocks(data, d)
+			data = fixPKCS7UnPadding(data)
+			len := binary.BigEndian.Uint32(data[16:20])
+			if err := xml.Unmarshal(data[20:(20+len)], &msg); err != nil {
+				log.Println("Weixin parse aes message failed:", err)
+				http.Error(w, "", http.StatusBadRequest)
+				return
+			}
+		}
+		wx.routeRequest(w, &msg)
+	}
+	return
+}
+
+func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
+	requestPath := r.MsgType
+	if requestPath == msgEvent {
+		requestPath += "." + r.Event
+	}
+	for _, route := range wx.routes {
+		if !route.regex.MatchString(requestPath) {
+			continue
+		}
+		writer := responseWriter{}
+		writer.wx = wx
+		writer.writer = w
+		writer.toUserName = r.FromUserName
+		writer.fromUserName = r.ToUserName
+		route.handler(writer, r)
+		return
+	}
+	http.Error(w, "", http.StatusNotFound)
+	return
+}
+
+func marshal(v interface{}) ([]byte, error) {
+	data, err := json.Marshal(v)
+	if err == nil {
+		data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
+		data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
+		data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
+	}
+	return data, err
+}
+
+func fixPKCS7UnPadding(data []byte) []byte {
+	length := len(data)
+	unpadding := int(data[length-1])
+	return data[:(length - unpadding)]
+}
+
+func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
+	r.ParseForm() // nolint
+	signature := r.FormValue("signature")
+	timestamp := r.FormValue("timestamp")
+	nonce := r.FormValue("nonce")
+	strs := sort.StringSlice{t, timestamp, nonce}
+	sort.Strings(strs)
+	var str string
+	for _, s := range strs {
+		str += s
+	}
+	h := sha1.New()
+	h.Write([]byte(str)) // nolint
+	return fmt.Sprintf("%x", h.Sum(nil)) == signature
+}
+
+func authAccessToken(appid string, secret string) (string, time.Duration) {
+	resp, err := http.Get(weixinHost + "/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret)
+	if err != nil {
+		log.Println("Get access token failed: ", err)
+	} else {
+		defer resp.Body.Close()
+		body, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			log.Println("Read access token failed: ", err)
+		} else {
+			var res struct {
+				AccessToken string `json:"access_token"`
+				ExpiresIn   int64  `json:"expires_in"`
+			}
+			if err := json.Unmarshal(body, &res); err != nil {
+				log.Println("Parse access token failed: ", err)
+			} else {
+				//log.Printf("AuthAccessToken token=%s expires_in=%d", res.AccessToken, res.ExpiresIn)
+				return res.AccessToken, time.Duration(res.ExpiresIn * 1000 * 1000 * 1000)
+			}
+		}
+	}
+	return "", 0
+}
+
+func getJsAPITicket(c chan AccessToken) (*jsAPITicket, error) {
+	reply, err := sendGetRequest(weixinJsApiTicketURL+"?type=jsapi&access_token=", c)
+	if err != nil {
+		return nil, err
+	}
+	var res struct {
+		Ticket    string `json:"ticket"`
+		ExpiresIn int64  `json:"expires_in"`
+	}
+	if err := json.Unmarshal(reply, &res); err != nil {
+		return nil, err
+	}
+	var ticket jsAPITicket
+	ticket.ticket = res.Ticket
+	ticket.expires = time.Now().Add(time.Duration(res.ExpiresIn * 1000 * 1000 * 1000))
+	return &ticket, nil
+
+}
+
+func (wx *Weixin) createAccessToken(c chan AccessToken, appid string, secret string) {
+	token := AccessToken{"", time.Now()}
+	c <- token
+	for {
+		swapped := atomic.CompareAndSwapInt32(&wx.refreshToken, 1, 0)
+		if swapped || time.Since(token.Expires).Seconds() >= 0 {
+			var expires time.Duration
+			token.Token, expires = authAccessToken(appid, secret)
+			token.Expires = time.Now().Add(expires)
+		}
+		c <- token
+	}
+}
+
+func createJsAPITicket(cin chan AccessToken, c chan jsAPITicket) {
+	ticket := jsAPITicket{"", time.Now()}
+	c <- ticket
+	for {
+		if time.Since(ticket.expires).Seconds() >= 0 {
+			t, err := getJsAPITicket(cin)
+			if err == nil {
+				ticket = *t
+			}
+		}
+		c <- ticket
+	}
+}
+
+func sendGetRequest(reqURL string, c chan AccessToken) ([]byte, error) {
+	for i := 0; i < retryMaxN; i++ {
+		token := <-c
+		if time.Since(token.Expires).Seconds() < 0 {
+			r, err := http.Get(reqURL + token.Token)
+			if err != nil {
+				return nil, err
+			}
+			defer r.Body.Close()
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return nil, err
+			}
+			var result response
+			if err := json.Unmarshal(reply, &result); err != nil {
+				return nil, err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return reply, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return nil, fmt.Errorf("WeiXin send get request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
+			}
+		}
+	}
+	return nil, errors.New("WeiXin post request too many times:" + reqURL)
+}
+
+func postRequest(reqURL string, c chan AccessToken, data []byte) ([]byte, error) {
+	for i := 0; i < retryMaxN; i++ {
+		token := <-c
+		if time.Since(token.Expires).Seconds() < 0 {
+			r, err := http.Post(reqURL+token.Token, "application/json; charset=utf-8", bytes.NewReader(data))
+			if err != nil {
+				return nil, err
+			}
+			defer r.Body.Close()
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return nil, err
+			}
+			var result response
+			if err := json.Unmarshal(reply, &result); err != nil {
+				return nil, err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return reply, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return nil, fmt.Errorf("WeiXin send post request reply[%d]: %s", result.ErrorCode, result.ErrorMessage)
+			}
+		}
+	}
+	return nil, errors.New("WeiXin post request too many times:" + reqURL)
+}
+
+func postMessage(c chan AccessToken, msg interface{}) error {
+	data, err := marshal(msg)
+	if err != nil {
+		return err
+	}
+	_, err = postRequest(weixinHost+"/message/custom/send?access_token=", c, data)
+	return err
+}
+
+// nolint: gocyclo
+func uploadMedia(c chan AccessToken, mediaType string, filename string, reader io.Reader) (string, error) {
+	reqURL := weixinFileURL + "/upload?type=" + mediaType + "&access_token="
+	for i := 0; i < retryMaxN; i++ {
+		token := <-c
+		if time.Since(token.Expires).Seconds() < 0 {
+			bodyBuf := &bytes.Buffer{}
+			bodyWriter := multipart.NewWriter(bodyBuf)
+			fileWriter, err := bodyWriter.CreateFormFile("filename", filename)
+			if err != nil {
+				return "", err
+			}
+			if _, err = io.Copy(fileWriter, reader); err != nil {
+				return "", err
+			}
+			contentType := bodyWriter.FormDataContentType()
+			bodyWriter.Close() // nolint
+			r, err := http.Post(reqURL+token.Token, contentType, bodyBuf)
+			if err != nil {
+				return "", err
+			}
+			defer r.Body.Close()
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return "", err
+			}
+			var result struct {
+				response
+				Type      string `json:"type"`
+				MediaID   string `json:"media_id"`
+				CreatedAt int64  `json:"created_at"`
+			}
+			err = json.Unmarshal(reply, &result)
+			if err != nil {
+				return "", err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return result.MediaID, nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return "", fmt.Errorf("WeiXin upload[%d]: %s", result.ErrorCode, result.ErrorMessage)
+			}
+		}
+	}
+	return "", errors.New("WeiXin upload media too many times")
+}
+
+func downloadMedia(c chan AccessToken, mediaID string, writer io.Writer) error {
+	reqURL := weixinFileURL + "/get?media_id=" + mediaID + "&access_token="
+	for i := 0; i < retryMaxN; i++ {
+		token := <-c
+		if time.Since(token.Expires).Seconds() < 0 {
+			r, err := http.Get(reqURL + token.Token)
+			if err != nil {
+				return err
+			}
+			defer r.Body.Close()
+			if r.Header.Get("Content-Type") != "text/plain" {
+				_, err = io.Copy(writer, r.Body)
+				return err
+			}
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return err
+			}
+			var result response
+			if err := json.Unmarshal(reply, &result); err != nil {
+				return err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return fmt.Errorf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage)
+			}
+		}
+	}
+	return errors.New("WeiXin download media too many times")
+}
+
+// Format reply message header.
+func (w responseWriter) replyHeader() string {
+	return fmt.Sprintf(replyHeader, w.toUserName, w.fromUserName, time.Now().Unix())
+}
+
+// Return weixin instance.
+func (w responseWriter) GetWeixin() *Weixin {
+	return w.wx
+}
+
+// Return user data.
+func (w responseWriter) GetUserData() interface{} {
+	return w.wx.userData
+}
+
+func (w responseWriter) replyMsg(msg string) {
+	w.writer.Write([]byte(msg))
+}
+
+// ReplyOK used to reply empty message.
+func (w responseWriter) ReplyOK() {
+	w.replyMsg("success")
+}
+
+// ReplyText used to reply text message.
+func (w responseWriter) ReplyText(text string) {
+	w.replyMsg(fmt.Sprintf(replyText, w.replyHeader(), text))
+}
+
+// ReplyImage used to reply image message.
+func (w responseWriter) ReplyImage(mediaID string) {
+	w.replyMsg(fmt.Sprintf(replyImage, w.replyHeader(), mediaID))
+}
+
+// ReplyVoice used to reply voice message.
+func (w responseWriter) ReplyVoice(mediaID string) {
+	w.replyMsg(fmt.Sprintf(replyVoice, w.replyHeader(), mediaID))
+}
+
+// ReplyVideo used to reply video message
+func (w responseWriter) ReplyVideo(mediaID string, title string, description string) {
+	w.replyMsg(fmt.Sprintf(replyVideo, w.replyHeader(), mediaID, title, description))
+}
+
+// ReplyMusic used to reply music message
+func (w responseWriter) ReplyMusic(m *Music) {
+	msg := fmt.Sprintf(replyMusic, w.replyHeader(), m.Title, m.Description, m.MusicUrl, m.HQMusicUrl, m.ThumbMediaId)
+	w.replyMsg(msg)
+}
+
+// ReplyNews used to reply news message (max 10 news)
+func (w responseWriter) ReplyNews(articles []Article) {
+	var ctx string
+	for _, article := range articles {
+		ctx += fmt.Sprintf(replyArticle, article.Title, article.Description, article.PicUrl, article.Url)
+	}
+	msg := fmt.Sprintf(replyNews, w.replyHeader(), len(articles), ctx)
+	w.replyMsg(msg)
+}
+
+// TransferCustomerService used to tTransfer customer service
+func (w responseWriter) TransferCustomerService(serviceID string) {
+	msg := fmt.Sprintf(transferCustomerService, serviceID, w.fromUserName, time.Now().Unix())
+	w.replyMsg(msg)
+}
+
+// PostText used to Post text message
+func (w responseWriter) PostText(text string) error {
+	return w.wx.PostText(w.toUserName, text)
+}
+
+// Post image message
+func (w responseWriter) PostImage(mediaID string) error {
+	return w.wx.PostImage(w.toUserName, mediaID)
+}
+
+// Post voice message
+func (w responseWriter) PostVoice(mediaID string) error {
+	return w.wx.PostVoice(w.toUserName, mediaID)
+}
+
+// Post video message
+func (w responseWriter) PostVideo(mediaID string, title string, desc string) error {
+	return w.wx.PostVideo(w.toUserName, mediaID, title, desc)
+}
+
+// Post music message
+func (w responseWriter) PostMusic(music *Music) error {
+	return w.wx.PostMusic(w.toUserName, music)
+}
+
+// Post news message
+func (w responseWriter) PostNews(articles []Article) error {
+	return w.wx.PostNews(w.toUserName, articles)
+}
+
+// Post template message
+func (w responseWriter) PostTemplateMessage(templateid string, url string, data TmplData) (int32, error) {
+	return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
+}
+
+// Upload media from local file
+func (w responseWriter) UploadMediaFromFile(mediaType string, filepath string) (string, error) {
+	return w.wx.UploadMediaFromFile(mediaType, filepath)
+}
+
+// Download media and save to local file
+func (w responseWriter) DownloadMediaToFile(mediaID string, filepath string) error {
+	return w.wx.DownloadMediaToFile(mediaID, filepath)
+}
+
+// Upload media with reader
+func (w responseWriter) UploadMedia(mediaType string, filename string, reader io.Reader) (string, error) {
+	return w.wx.UploadMedia(mediaType, filename, reader)
+}
+
+// Download media with writer
+func (w responseWriter) DownloadMedia(mediaID string, writer io.Writer) error {
+	return w.wx.DownloadMedia(mediaID, writer)
+}