Browse Source

init init

ls 11 months ago
parent
commit
e1d0aa5eb4
11 changed files with 1123 additions and 0 deletions
  1. 13 0
      cache.go
  2. 339 0
      client.go
  3. 81 0
      const.go
  4. 5 0
      go.mod
  5. 92 0
      go.sum
  6. 1 0
      pay.go
  7. 33 0
      request.go
  8. 13 0
      response.go
  9. 41 0
      server.go
  10. 63 0
      sign.go
  11. 442 0
      structure.go

+ 13 - 0
cache.go

@@ -0,0 +1,13 @@
+package wechat
+
+func keyC(appid string) string {
+	return "wechat:client:" + appid
+}
+
+func keyToken(appid string) string {
+	return "wechat:client:token:" + appid
+}
+
+func keyTicket(appid string) string {
+	return "wechat:client:ticket:" + appid
+}

+ 339 - 0
client.go

@@ -0,0 +1,339 @@
+package wechat
+
+import (
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"time"
+
+	"git.chuangxin1.com/myth/sacred/cache"
+	"git.chuangxin1.com/myth/sacred/hash"
+)
+
+// NewClient new client
+func NewClient(appID, appSecret, token, encodingAESKey string) *Client {
+	key := keyC(appID)
+	if v, ok := cache.Load(key); ok {
+		return v.(*Client)
+	}
+	c := &Client{AppID: appID, AppSecret: appSecret, Token: token, EncodingAESKey: encodingAESKey}
+	cache.Store(key, c)
+	return c
+}
+
+// getToken get token
+func (wc *Client) getToken() (token string, err error) {
+	key := keyToken(wc.AppID)
+	if wc.UseCacheToken {
+		var ct ClientToken
+		s := ``
+		s, err = cache.RedisGet(key)
+		if err != nil {
+			return
+		}
+		err = json.Unmarshal([]byte(s), &ct)
+		if err != nil {
+			return
+		}
+		token = ct.AccessToken
+		return
+	}
+	now := time.Now().Unix()
+	if wc.LastTokenTime > 0 {
+		if now-wc.LastTokenTime < TokenExpires {
+			token = wc.AccessToken
+			return
+		}
+	}
+
+	token, err = wc.RefreshToken(now)
+	return
+}
+
+// RefreshToken refresh token, now current timestamp
+func (wc *Client) RefreshToken(now int64) (token string, err error) {
+	key := keyToken(wc.AppID)
+	uri := BaseURL + "/cgi-bin/token?"
+
+	args := url.Values{}
+	args.Add("grant_type", "client_credential")
+	args.Add("appid", wc.AppID)
+	args.Add("secret", wc.AppSecret)
+	uri += args.Encode()
+
+	var res ResponseToken
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+	err = res.Check()
+	if err != nil {
+		return
+	}
+	wc.LastTokenTime = now
+	wc.AccessToken = res.AccessToken
+
+	cache.Store(keyC(wc.AppID), wc)
+	ct := ClientToken{AppID: wc.AppID, AccessToken: wc.AccessToken, LastTokenTime: now}
+	bs, _ := json.Marshal(ct)
+	cache.RedisSet(key, string(bs), 0)
+
+	token = res.AccessToken
+	return
+}
+
+func (wc *Client) getTicket(now int64) (ticket string, err error) {
+	key := keyTicket(wc.AppID)
+	if wc.UseCacheToken {
+		var ct ClientTicket
+		s := ``
+		s, err = cache.RedisGet(key)
+		if err != nil {
+			return
+		}
+		err = json.Unmarshal([]byte(s), &ct)
+		if err != nil {
+			return
+		}
+		ticket = ct.Ticket
+		return
+	}
+	if wc.LastTicketTime > 0 {
+		if now-wc.LastTicketTime < TicketExpires {
+			ticket = wc.Ticket
+			return
+		}
+	}
+
+	ticket, err = wc.RefreshTicket(now)
+	return
+}
+
+// RefreshTicket refresh tocket, now current timestamp
+func (wc *Client) RefreshTicket(now int64) (ticket string, err error) {
+	key := keyTicket(wc.AppID)
+	args := url.Values{}
+	args.Add("access_token", wc.AccessToken)
+	args.Add("type", "jsapi")
+
+	uri := BaseURL + "/cgi-bin/ticket/getticket?"
+	uri += args.Encode()
+
+	var res ResponseTicket
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+	err = res.Check()
+	if err != nil {
+		return
+	}
+
+	wc.LastTicketTime = now
+	wc.Ticket = res.Ticket
+	cache.Store(keyC(wc.AppID), wc)
+
+	ct := ClientTicket{AppID: wc.AppID, LastTicketTime: now, Ticket: res.Ticket}
+	bs, _ := json.Marshal(ct)
+	cache.RedisSet(key, string(bs), 0)
+
+	ticket = res.Ticket
+	return
+}
+
+// Info client info
+func (wc Client) Info() (c *Client, ct ClientToken, ct1 ClientTicket, err error) {
+	key := keyC(wc.AppID)
+	if v, ok := cache.Load(key); ok {
+		c = v.(*Client)
+	}
+	s := ``
+	s, err = cache.RedisGet(keyToken(wc.AppID))
+	if err != nil {
+		return
+	}
+	err = json.Unmarshal([]byte(s), &ct)
+	if err != nil {
+		return
+	}
+	s, err = cache.RedisGet(keyTicket(wc.AppID))
+	if err != nil {
+		return
+	}
+	err = json.Unmarshal([]byte(s), &ct1)
+	if err != nil {
+		return
+	}
+	return
+}
+
+// GetUserInfo user info
+func (wc Client) GetUserInfo(openid string) (res UserInfo, err error) {
+	uri := BaseURL + "/cgi-bin/user/info?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+	args := url.Values{}
+	args.Add("access_token", wc.AccessToken)
+	args.Add("openid", openid)
+	args.Add("lang", "zh_CN")
+
+	uri += args.Encode()
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}
+
+// GetUserList user list
+func (wc Client) GetUserList(nextOpenID string) (res UserList, err error) {
+	uri := BaseURL + "/cgi-bin/user/get?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+	args := url.Values{}
+	args.Add("access_token", wc.AccessToken)
+	args.Add("next_openid", nextOpenID)
+
+	uri += args.Encode()
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}
+
+// GetMaterial 永久资料
+func (wc Client) GetMaterial(mtype string, offset, count int) (res Material, err error) {
+	uri := BaseURL + "/cgi-bin/material/batchget_material?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+
+	args := url.Values{}
+	data := make(map[string]interface{})
+
+	args.Add("access_token", wc.AccessToken)
+
+	data["type"] = mtype
+	data["offset"] = offset
+	data["count"] = count
+	uri += args.Encode()
+
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}
+
+// GetMenu 查询自定义菜单
+func (wc Client) GetMenu() (res Menu, err error) {
+	uri := BaseURL + "/cgi-bin/get_current_selfmenu_info?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+
+	args := url.Values{}
+	args.Add("access_token", wc.AccessToken)
+
+	uri += args.Encode()
+	_, err = getJSON(uri, &res)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}
+
+// CreateMenu 创建自定义菜单
+func (wc Client) CreateMenu(menu FormMenu) (err error) {
+	uri := BaseURL + "/cgi-bin/menu/create?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+
+	args := url.Values{}
+
+	args.Add("access_token", wc.AccessToken)
+	uri += args.Encode()
+
+	var res ResponseMsg
+	_, err = postJSON(&res, uri, menu)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}
+
+// GetSignPackage JS 签名
+//   uri      当前 URL
+//   nonceStr 随机字符串
+func (wc Client) GetSignPackage(uri, nonceStr string) (sign SignPackage, err error) {
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+
+	var (
+		s    string
+		unix int64
+	)
+	unix = time.Now().Unix()
+	if wc.Ticket, err = wc.getTicket(unix); err != nil {
+		return
+	}
+
+	s = `jsapi_ticket=` + wc.Ticket
+	s += `&noncestr=` + nonceStr
+	s += `&timestamp=` + fmt.Sprintf("%d", unix)
+	s += `&url=` + uri
+
+	hs, _ := hash.SHA1([]byte(s))
+
+	sign.AppID = wc.AppID
+	sign.NonceStr = nonceStr
+	sign.Signature = hex.EncodeToString(hs)
+	sign.Timestamp = unix
+	sign.Ticket = wc.Ticket
+	sign.URL = uri
+
+	return
+}
+
+// SendTemplateMessage send template message
+// POST /cgi-bin/message/template/send?access_token=ACCESS_TOKEN
+func (wc Client) SendTemplateMessage(template TemplateMessage) (res TemplateResponse, err error) {
+	uri := BaseURL + "/cgi-bin/message/template/send?"
+
+	if wc.AccessToken, err = wc.getToken(); err != nil {
+		return
+	}
+	args := url.Values{}
+	args.Add("access_token", wc.AccessToken)
+
+	uri += args.Encode()
+
+	_, err = postJSON(&res, uri, template)
+	if err != nil {
+		return
+	}
+
+	err = res.Check()
+	return
+}

+ 81 - 0
const.go

@@ -0,0 +1,81 @@
+package wechat
+
+const (
+	// TokenExpires token expires time 1 hours
+	TokenExpires = 90 * 60
+	// TicketExpires ticket expires time 1 hours
+	TicketExpires = 60 * 60
+
+	// BaseURL api host
+	BaseURL = `https://api.weixin.qq.com`
+	// OpenURL open host
+	OpenURL = `https://open.weixin.qq.com`
+
+	// PayHost pay base host
+	PayHost = `https://api.mch.weixin.qq.com`
+
+	// ErrResOk response ok
+	ErrResOk = 0
+
+	// PayURLUnifiedOrder pay 付款
+	PayURLUnifiedOrder = `/pay/unifiedorder`
+	// PayURLPayRefund pay refund 退款
+	PayURLPayRefund = `/secapi/pay/refund`
+	// PayURLPapPay 委托代扣申请扣款
+	PayURLPapPay = `/pay/pappayapply`
+	// PayURLPapayEntrust H5 纯签约
+	PayURLPapayEntrust = `/papay/h5entrustweb`
+
+	// PayTradeTypeJS JSAPI 公众号支付
+	PayTradeTypeJS = `JSAPI`
+	// PayTradeTypeNative NATIVE 扫码支付
+	PayTradeTypeNative = `NATIVE`
+	// PayTradeTypeAPP APP APP支付
+	PayTradeTypeAPP = `APP`
+
+	// MsgText text message
+	MsgText = `text`
+	// MsgImage image message
+	MsgImage = `image`
+	// MsgVoice voice message
+	MsgVoice = `voice`
+	// MsgVideo video message
+	MsgVideo = `video`
+	// MsgShortVideo shortvideo message
+	MsgShortVideo = `shortvideo`
+	// MsgLocation location message
+	MsgLocation = `location`
+	// MsgLink link message
+	MsgLink = `link`
+
+	// MsgNews news
+	MsgNews = `news`
+	// MsgMusic music
+	MsgMusic = `music`
+
+	// MsgEvent event
+	MsgEvent = `event`
+
+	// subscribe
+	// subscribe 事件 EventKey 不为空的, 为扫描带参数二维码事件关注
+	//
+
+	// MsgEventSubscribe subscribe 订阅
+	MsgEventSubscribe = `subscribe`
+	// MsgEventUnSubscribe unsubscribe 取消订阅
+	MsgEventUnSubscribe = `unsubscribe`
+
+	// MsgEventSCAN 已关注用户扫码事件
+	MsgEventSCAN = `SCAN`
+
+	// MsgEventLOCATION 上报地理位置事件
+	MsgEventLOCATION = `LOCATION`
+	// MsgEventCLICK 自定义菜单点击事件
+	MsgEventCLICK = `CLICK`
+
+	// MsgEventVIEW 点击自定义菜单跳转链接时的事件
+	MsgEventVIEW = `VIEW`
+
+	// MsgEventTEMPLATESENDJOBFINISH 模版消息发送任务结果事件
+	MsgEventTEMPLATESENDJOBFINISH = `TEMPLATESENDJOBFINISH`
+)

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module git.chuangxin1.com/myth/wechat
+
+go 1.16
+
+require git.chuangxin1.com/myth/sacred v0.0.0-20210709010131-da4903ddbad6 // indirect

+ 92 - 0
go.sum

@@ -0,0 +1,92 @@
+git.chuangxin1.com/myth/sacred v0.0.0-20210709010131-da4903ddbad6 h1:LfqB0+exwyixUe6jd2SniTwDtRpk95Hr6SaR2VH15fQ=
+git.chuangxin1.com/myth/sacred v0.0.0-20210709010131-da4903ddbad6/go.mod h1:sPnQaTU08Ec18cz7vtk9S+/zwOD03z71kaXu6fWP27U=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-redis/redis/v8 v8.11.0/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
+github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 1 - 0
pay.go

@@ -0,0 +1 @@
+package wechat

+ 33 - 0
request.go

@@ -0,0 +1,33 @@
+package wechat
+
+import (
+	"bytes"
+	"encoding/json"
+
+	sh "git.chuangxin1.com/myth/sacred/xhttp"
+)
+
+// getJSON GET response JSON
+func getJSON(uri string, v interface{}) (msg sh.Message, err error) {
+	opt := sh.DefaultRequestOption()
+
+	opt.AcceptEncodingGZIP()
+	msg, err = sh.GetJSON(v, uri, opt)
+	//fmt.Println(string(msg.Body), err)
+	return
+}
+
+// postJSON POST json data and response JSON
+func postJSON(v interface{}, uri string, data interface{}) (msg sh.Message, err error) {
+	bs, _ := json.Marshal(data)
+	opt := sh.DefaultRequestOption()
+
+	opt.AcceptEncodingGZIP()
+	opt.SetContentTypeJSON()
+	msg, err = sh.Post(uri, bytes.NewReader(bs), opt)
+	if err != nil {
+		return
+	}
+	err = msg.JSON(v)
+	return
+}

+ 13 - 0
response.go

@@ -0,0 +1,13 @@
+package wechat
+
+import (
+	"errors"
+)
+
+func (r ResponseMsg) Check() (err error) {
+	if r.ErrCode == ErrResOk {
+		return
+	}
+	err = errors.New(r.ErrMsg)
+	return
+}

+ 41 - 0
server.go

@@ -0,0 +1,41 @@
+package wechat
+
+import (
+	"git.chuangxin1.com/myth/sacred/cache"
+)
+
+// Server wechat
+type Server struct {
+	AppID          string `json:"appid"`
+	AppSecret      string `json:"appsecret"`
+	Token          string `json:"token"`
+	EncodingAESKey string `json:"encodingaeskey"`
+}
+
+func keyServer(appid string) string {
+	return "wechat:server:" + appid
+}
+
+// NewServer new Server
+func NewServer(appID, appSecret, token, encodingAESKey string) *Server {
+	key := keyServer(appID)
+	if v, ok := cache.Load(key); ok {
+		return v.(*Server)
+	}
+	s := &Server{AppID: appID, AppSecret: appSecret, Token: token, EncodingAESKey: encodingAESKey}
+	cache.Store(key, s)
+	return s
+}
+
+// Echo notify echo GET
+func (ws Server) Echo(s FormSignature) (data string, err error) {
+	if ok := makeSignature(ws.Token, s.Signature, s.TimeStamp, s.Nonce); ok {
+		data = s.Echostr
+	}
+	return
+}
+
+// Validate 验证请求合法性
+func (ws Server) Validate(signature, timestamp, nonce string) bool {
+	return makeSignature(ws.Token, signature, timestamp, nonce)
+}

+ 63 - 0
sign.go

@@ -0,0 +1,63 @@
+package wechat
+
+import (
+	"encoding/hex"
+	"fmt"
+	"math/rand"
+	"sort"
+	"strings"
+	"time"
+
+	"git.chuangxin1.com/myth/sacred/hash"
+)
+
+// Sign 微信计算签名的函数
+func Sign(req map[string]interface{}, key string) (sign string) {
+	keys := make([]string, 0)
+
+	for k := range req {
+		keys = append(keys, k)
+	}
+
+	sort.Strings(keys)
+
+	var s string
+	for _, k := range keys {
+		value := fmt.Sprintf("%v", req[k])
+		if value != "" {
+			s = s + k + "=" + value + "&"
+		}
+	}
+
+	if key != "" {
+		s = s + "key=" + key
+	}
+
+	hs, _ := hash.MD5([]byte(s))
+	sign = strings.ToUpper(hex.EncodeToString(hs))
+
+	return
+}
+
+// Random 指定长度的随机字符串(字母或数字)
+func Random(n int) string {
+	str := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+	bytes := []byte(str)
+	result := []byte{}
+
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
+	n1 := len(bytes)
+	for i := 0; i < n; i++ {
+		result = append(result, bytes[r.Intn(n1)])
+	}
+	return string(result)
+}
+
+// makeSignature echo 签名验证
+func makeSignature(token, signature, timestamp, nonce string) bool {
+	sl := []string{token, timestamp, nonce}
+	sort.Strings(sl)
+
+	hs, _ := hash.SHA1([]byte(strings.Join(sl, "")))
+	return strings.Compare(signature, hex.EncodeToString(hs)) == 0
+}

+ 442 - 0
structure.go

@@ -0,0 +1,442 @@
+package wechat
+
+import (
+	"encoding/xml"
+)
+
+// Client wechat
+type Client struct {
+	UseCacheToken bool
+
+	AppID          string `json:"appid"`
+	AppSecret      string `json:"appsecret"`
+	Token          string `json:"token"`
+	EncodingAESKey string `json:"encodingaeskey"`
+
+	AccessToken string
+	Ticket      string
+
+	//TokenFromCache bool
+
+	LastTokenTime  int64
+	LastTicketTime int64
+}
+
+// ClientToken wechat client token
+type ClientToken struct {
+	AppID string `json:"appid"`
+
+	AccessToken   string `json:"access_token"`
+	LastTokenTime int64  `json:"last_token_time"`
+}
+
+// ClientTicket wechat client ticket
+type ClientTicket struct {
+	AppID string `json:"appid"`
+
+	Ticket         string `json:"ticket"`
+	LastTicketTime int64  `json:"last_ticket_time"`
+}
+
+// MiniClient wechat mini
+type MiniClient struct {
+	AppID     string `json:"appid"`
+	AppSecret string `json:"appsecret"`
+
+	AccessToken   string
+	LastTokenTime int64
+}
+
+// ResponseMsg response
+type ResponseMsg struct {
+	ErrCode int    `json:"errcode,omitempty"`
+	ErrMsg  string `json:"errmsg,omitempty"`
+}
+
+// ResponseToken token
+type ResponseToken struct {
+	ResponseMsg
+	AccessToken string `json:"access_token"`
+	ExpiresIn   int    `json:"expires_in"`
+}
+
+// ResponseTicket ticket
+type ResponseTicket struct {
+	ResponseMsg
+	Ticket    string `json:"ticket"`
+	ExpiresIn int    `json:"expires_in"`
+}
+
+// FormSignature signature
+type FormSignature struct {
+	TimeStamp string `form:"timestamp" json:"timestamp"`
+	Nonce     string `form:"nonce" json:"nonce"`
+	Signature string `form:"signature" json:"signature"`
+	Echostr   string `form:"echostr" json:"echostr"`
+}
+
+// FormAuthorize get code
+type FormAuthorize struct {
+	Code  string `form:"code"`
+	State string `form:"state"`
+	URL   string `form:"url"`
+}
+
+// FormOpenID openid
+type FormOpenID struct {
+	OpenID string `form:"openid"`
+}
+
+// FormCode code
+type FormCode struct {
+	Code string `form:"code"`
+}
+
+// FormSignPackage js sign package
+type FormSignPackage struct {
+	URL string `form:"url"`
+}
+
+// FormURLState redirect url and state
+type FormURLState struct {
+	RedirectURL string `form:"url" json:"url" xml:"url"`
+	State       string `form:"state" json:"state" xml:"state"`
+}
+
+// Message message
+type Message struct {
+	XMLName      xml.Name `xml:"xml"`
+	ToUserName   string   `xml:"ToUserName" json:"ToUserName"`
+	FromUserName string   `xml:"FromUserName" json:"FromUserName"`
+	CreateTime   int64    `xml:"CreateTime" json:"CreateTime"`
+	MsgType      string   `xml:"MsgType" json:"MsgType"` // text/image/voice/video/shortvideo/location/link
+
+	// 文本 Text Text message
+	Text string `xml:"Content,omitempty" json:"Content,omitempty"`
+
+	// 图片 PicURL / MediaID image messgae
+	PicURL string `xml:"PicUrl,omitempty" json:"PicUrl,omitempty"`
+	// MediaID 媒体
+	MediaID string `xml:"MediaId,omitempty" json:"MediaId,omitempty"`
+	// 语音 MediaID / Format voice message
+	Format string `xml:"Format,omitempty" json:"Format,omitempty"`
+	// 视频/小视频 ThumbMediaID / MediaID video/shortvideo message
+	ThumbMediaID string `xml:"ThumbMediaId,omitempty" json:"ThumbMediaId,omitempty"`
+
+	// 地理位置 location message
+	LocationX string `xml:"Location_X,omitempty" json:"Location_X,omitempty"` // 纬度
+	LocationY string `xml:"Location_Y,omitempty" json:"Location_Y,omitempty"` // 经度
+	Scale     string `xml:"Scale,omitempty" json:"Scale,omitempty"`           // 地图缩放大小
+	Label     string `xml:"Label,omitempty" json:"Label,omitempty"`           // 地理位置信息
+
+	// 链接消息 link
+	Title       string `xml:"Title,omitempty" json:"Title,omitempty"`             // 消息标题
+	Description string `xml:"Description,omitempty" json:"Description,omitempty"` // 消息描述
+	URL         string `xml:"Url,omitempty" json:"Url,omitempty"`                 // 消息链接
+
+	Event string `xml:"Event,omitempty" json:"Event,omitempty"` // 事件 subscribe(订阅)、unsubscribe(取消订阅)、CLICK(自定义菜单事件)、SCAN
+
+	// 扫描带参数二维码事件
+	EventKey string `xml:"EventKey,omitempty" json:"EventKey,omitempty"` // 事件KEY 值,扫码未关注的 qrscene_ 为前缀(subscribe 事件, 已关注的推送 SCAN 事件),后面为二维码的参数值, 自定义菜单事件与自定义菜单接口中 KEY 值对应
+	Ticket   string `xml:"Ticket,omitempty" json:"Ticket,omitempty"`     // 二维码的ticket,可用来换取二维码图片
+
+	// 地理位置 location message
+	Latitude  string `xml:"Latitude,omitempty" json:"Latitude,omitempty"`   // 纬度
+	Longitude string `xml:"Longitude,omitempty" json:"Longitude,omitempty"` // 经度
+	Precision string `xml:"Precision,omitempty" json:"Precision,omitempty"` // 地理位置精度
+
+	MsgID  int64  `xml:"MsgID,omitempty" json:"MsgID,omitempty"`   // 消息id
+	Status string `xml:"Status,omitempty" json:"Status,omitempty"` // 消息发送状态
+}
+
+type menuSubButtonList struct {
+	Type string `json:"type"`
+	Name string `json:"name"`
+	Key  string `json:"key"`
+	URL  string `json:"url"`
+}
+
+type menuSubButton struct {
+	List []menuSubButtonList `json:"list"`
+}
+
+type menuButton struct {
+	Type      string        `json:"type"`
+	Name      string        `json:"name"`
+	Key       string        `json:"key"`
+	SubButton menuSubButton `json:"sub_button"`
+}
+
+type menuInfo struct {
+	Button []menuButton `json:"button"`
+}
+
+// Menu menu
+type Menu struct {
+	ResponseMsg
+	IsOpen int64    `json:"is_menu_open"`
+	Info   menuInfo `json:"selfmenu_info"`
+}
+
+// FormMenuButton menu button
+type FormMenuButton struct {
+	Type      string            `json:"type,omitempty"`
+	Name      string            `json:"name"`
+	Key       string            `json:"key,omitempty"`
+	URL       string            `json:"url,omitempty"`
+	AppID     string            `json:"appid,omitempty"`
+	Pagepath  string            `json:"pagepath,omitempty"`
+	MediaID   string            `json:"media_id,omitempty"`
+	SubButton []*FormMenuButton `json:"sub_button,omitempty"`
+}
+
+// FormMenu menu create
+type FormMenu struct {
+	Button []*FormMenuButton `json:"button"`
+}
+
+// ArticleItem article item
+type ArticleItem struct {
+	Title       string `xml:"Title" json:"Title"`
+	Description string `xml:"Description" json:"Description"`
+	PicURL      string `xml:"PicUrl" json:"PicUrl"`
+	URL         string `xml:"Url" json:"Url"`
+}
+
+type articleItems struct {
+	Items []ArticleItem `xml:"item" json:"item"`
+}
+
+// ReplyMessage reply message
+type ReplyMessage struct {
+	XMLName      xml.Name `xml:"xml"`
+	ToUserName   string   `xml:"ToUserName" json:"ToUserName"`
+	FromUserName string   `xml:"FromUserName" json:"FromUserName"`
+	CreateTime   int64    `xml:"CreateTime" json:"CreateTime"`
+	MsgType      string   `xml:"MsgType" json:"MsgType"` // text/image/voice/video/music/news
+
+	// 文本 Content Text message
+	Content string `xml:"Content,omitempty" json:"Content,omitempty"`
+
+	// image
+	ArticleCount int           `xml:"ArticleCount,omitempty" json:"ArticleCount,omitempty"`
+	Articles     *articleItems `xml:"Articles,omitempty" json:"Articles,omitempty"`
+}
+
+// EventTemplateReply event reply
+type EventTemplateReply struct {
+	XMLName      xml.Name `xml:"xml"`
+	ToUserName   string   `xml:"ToUserName"`
+	FromUserName string   `xml:"FromUserName"`
+	CreateTime   string   `xml:"CreateTime"`
+	MsgType      string   `xml:"MsgType"`
+	Event        string   `xml:"Event"`
+	MsgID        string   `xml:"MsgID"`
+	Status       string   `xml:"Status"`
+}
+
+// SignPackage sign package
+type SignPackage struct {
+	AppID     string `json:"appId"`
+	NonceStr  string `json:"nonceStr"`
+	Timestamp int64  `json:"timestamp"`
+	Signature string `json:"signature"`
+	Ticket    string `json:"ticket"`
+	URL       string `json:"url"`
+}
+
+// FormMaterial Material
+type FormMaterial struct {
+	Type   string `form:"type"`
+	Offset int    `form:"offset"`
+	Count  int    `form:"count"`
+}
+
+// UserInfo userinfo
+type UserInfo struct {
+	ResponseMsg
+	SubScribe      int    `json:"subscribe"`
+	OpenID         string `json:"openid"`
+	NickName       string `json:"nickname"`
+	Sex            int    `json:"sex"`
+	Language       string `json:"language"`
+	City           string `json:"city"`
+	Province       string `json:"province"`
+	Country        string `json:"country"`
+	HeadImgURL     string `json:"headimgurl"`
+	SubscribeTime  int    `json:"subscribe_time"`
+	UnionID        string `json:"unionid"`
+	Remark         string `json:"remark"`
+	GroupID        int    `json:"groupid"`
+	TagidList      []int  `json:"tagid_list"`
+	SubscribeScene string `json:"subscribe_scene"`
+	QrScene        int    `json:"qr_scene"`
+	QrSceneStr     string `json:"qr_scene_str"`
+}
+
+// FormNextOpenID next openid
+type FormNextOpenID struct {
+	Next string `form:"next_openid"`
+}
+
+// List openid list
+type List struct {
+	OpenID []string `json:"openid"`
+}
+
+// UserList user list
+type UserList struct {
+	ResponseMsg
+	Total      int    `json:"total"`
+	Count      int    `json:"count"`
+	Data       List   `json:"data"`
+	NextOpenID string `json:"next_openid"`
+}
+
+// MaterialNewsItem 图片素材
+type MaterialNewsItem struct {
+	Title              string `json:"title"`
+	Author             string `json:"author"`
+	Digest             string `json:"digest"`
+	Content            string `json:"content"`
+	ContentSourceURL   string `json:"content_source_url"`
+	ThumbMediaID       string `json:"thumb_media_id"`
+	ShowCoverPic       int    `json:"show_cover_pic"`
+	URL                string `json:"url"`
+	ThumbURL           string `json:"thumb_url"`
+	NeedOpenComment    int    `json:"need_open_comment"`
+	OnlyFansCanComment int    `json:"only_fans_can_comment"`
+}
+
+// MaterialContent 素材内容
+type MaterialContent struct {
+	NewsItem   []*MaterialNewsItem `json:"news_item,omitempty"`
+	CreateTime int                 `json:"create_time,omitempty"`
+	UpdateTime int                 `json:"update_time,omitempty"`
+}
+
+// MaterialItem 素材信息
+type MaterialItem struct {
+	MediaID    string           `json:"media_id"`
+	Name       string           `json:"name,omitempty"`
+	UpdateTime int              `json:"update_time"`
+	URL        string           `json:"url,omitempty"`
+	Content    *MaterialContent `json:"content,omitempty"`
+}
+
+// Material 素材
+type Material struct {
+	ResponseMsg
+	TotalCount int            `json:"total_count"`
+	ItemCount  int            `json:"item_count"`
+	Item       []MaterialItem `json:"item"`
+}
+
+// MiniProgramPage mini
+type MiniProgramPage struct {
+	AppID    string `json:"appid,omitempty"`
+	PagePath string `json:"pagepath,omitempty"`
+}
+
+// ValueColor value color
+type ValueColor struct {
+	Value string `json:"value"`
+	Color string `json:"color,omitempty"`
+}
+
+// TemplateResponse 模版消息返回
+type TemplateResponse struct {
+	ResponseMsg
+	MessageID int64 `json:"msgid"`
+}
+
+// TemplateData data
+type TemplateData struct {
+	First     *ValueColor `json:"first"`
+	Keyword1  *ValueColor `json:"keyword1,omitempty"`
+	Keyword2  *ValueColor `json:"keyword2,omitempty"`
+	Keyword3  *ValueColor `json:"keyword3,omitempty"`
+	Keyword4  *ValueColor `json:"keyword4,omitempty"`
+	Keyword5  *ValueColor `json:"keyword5,omitempty"`
+	Keyword6  *ValueColor `json:"keyword6,omitempty"`
+	Keyword7  *ValueColor `json:"keyword7,omitempty"`
+	Keyword8  *ValueColor `json:"keyword8,omitempty"`
+	Keyword9  *ValueColor `json:"keyword9,omitempty"`
+	Keyword10 *ValueColor `json:"keyword10,omitempty"`
+	Name      *ValueColor `json:"name,omitempty"`
+	ExpDate   *ValueColor `json:"expDate,omitempty"`
+	Remark    *ValueColor `json:"remark,omitempty"`
+}
+
+// TemplateMessage template message
+type TemplateMessage struct {
+	ToUser      string           `json:"touser"`
+	TemplateID  string           `json:"template_id"`
+	URL         string           `json:"url"`
+	MiniProgram *MiniProgramPage `json:"miniprogram,omitempty"`
+	Data        TemplateData     `json:"data"`
+}
+
+// MiniTemplateMessage mini template message
+type MiniTemplateMessage struct {
+	ToUser     string       `json:"touser"`
+	TemplateID string       `json:"template_id"`
+	Page       string       `json:"page"`
+	FormID     string       `json:"form_id"`
+	Data       TemplateData `json:"data"`
+}
+
+// WeappTemplateMessage mini message
+type WeappTemplateMessage struct {
+	TemplateID string       `json:"template_id"`
+	Page       string       `json:"page"`
+	FormID     string       `json:"form_id"`
+	Data       TemplateData `json:"data"`
+	Emphasis   string       `json:"emphasis_keyword"`
+}
+
+// MpTemplateMessage wechat public message
+type MpTemplateMessage struct {
+	AppID      string          `json:"appid"`
+	TemplateID string          `json:"template_id"`
+	URL        string          `json:"url"`
+	Mini       MiniProgramPage `json:"miniprogram"`
+	Data       TemplateData    `json:"data"`
+}
+
+// MiniUniformMessage mini uniform send
+type MiniUniformMessage struct {
+	ToUser string                `json:"touser"`
+	WeApp  *WeappTemplateMessage `json:"weapp_template_msg,omitempty"`
+	MP     *MpTemplateMessage    `json:"mp_template_msg,omitempty"`
+}
+
+// FormPayNotify notify
+type FormPayNotify struct {
+	XMLName  xml.Name `xml:"xml" json:"_,omitempty"`
+	AppID    string   `form:"appid" xml:"appid"`
+	Attach   string   `form:"attach" xml:"attach"`
+	BankType string   `form:"bank_type" xml:"bank_type"`
+	CashFee  int      `form:"cash_fee" xml:"cash_fee"`
+	FeeType  string   `form:"fee_type" xml:"fee_type"`
+
+	MchID       string `form:"mch_id" xml:"mch_id"`
+	IsSubscribe string `form:"is_subscribe" xml:"is_subscribe"`
+	NonceStr    string `form:"nonce_str" xml:"nonce_str"`
+	OpenID      string `form:"openid" xml:"openid"`
+	OutTradeNo  string `form:"out_trade_no" xml:"out_trade_no"`
+
+	ResultCode string `form:"result_code" xml:"result_code"`
+	ReturnMsg  string `form:"return_msg" xml:"return_msg"`
+	ReturnCode string `form:"return_code" xml:"return_code"`
+	ErrCodeDes string `form:"err_code_des" xml:"err_code_des"`
+	ErrCode    string `form:"err_code" xml:"err_code"`
+
+	Sign     string `form:"sign" xml:"sign"`
+	TimeEnd  string `form:"time_end" xml:"time_end"`
+	TotalFee int    `form:"total_fee" xml:"total_fee"`
+
+	TradeType     string `form:"trade_type" xml:"trade_type"`
+	TransactionID string `form:"transaction_id" xml:"transaction_id"`
+	ContractID    string `form:"contract_id" xml:"contract_id"`
+}