├── README.md
├── aes_ecb.go
├── cache.go
├── code.go
├── crypto.go
├── example
└── main.go
├── go.mod
├── http.go
├── js.go
├── mp.go
├── open.go
├── options.go
├── other.go
├── pay.go
├── qy.go
├── reply.go
├── token.go
├── utils.go
├── utils_test.go
├── weapp.go
├── web.go
├── wechat.go
└── wechat_test.go
/README.md:
--------------------------------------------------------------------------------
1 | # Golang Wechat SDK
2 |
3 | 支持微信公众平台/微信开放平台/企业微信
4 |
5 | ## Documentation
6 |
7 | - [中文文档](https://docs.73zls.com/zlsgo/#/34b3462b-403a-4df2-9e69-45947a7bfceb)
--------------------------------------------------------------------------------
/aes_ecb.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "encoding/base64"
8 | "errors"
9 |
10 | "github.com/sohaha/zlsgo/zstring"
11 | )
12 |
13 | type ecb struct {
14 | b cipher.Block
15 | blockSize int
16 | }
17 |
18 | func newECB(b cipher.Block) *ecb {
19 | return &ecb{
20 | b: b,
21 | blockSize: b.BlockSize(),
22 | }
23 | }
24 |
25 | type ecbEncrypter ecb
26 |
27 | func newECBEncrypter(b cipher.Block) cipher.BlockMode {
28 | return (*ecbEncrypter)(newECB(b))
29 | }
30 |
31 | func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
32 |
33 | func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
34 | if len(src)%x.blockSize != 0 {
35 | panic("crypto/cipher: input not full blocks")
36 | }
37 | if len(dst) < len(src) {
38 | panic("crypto/cipher: output smaller than input")
39 | }
40 | for len(src) > 0 {
41 | x.b.Encrypt(dst, src[:x.blockSize])
42 | src = src[x.blockSize:]
43 | dst = dst[x.blockSize:]
44 | }
45 | }
46 |
47 | type ecbDecrypter ecb
48 |
49 | func newECBDecrypter(b cipher.Block) cipher.BlockMode {
50 | return (*ecbDecrypter)(newECB(b))
51 | }
52 |
53 | func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
54 |
55 | func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
56 | if len(src)%x.blockSize != 0 {
57 | panic("crypto/cipher: input not full blocks")
58 | }
59 | if len(dst) < len(src) {
60 | panic("crypto/cipher: output smaller than input")
61 | }
62 | for len(src) > 0 {
63 | x.b.Decrypt(dst, src[:x.blockSize])
64 | src = src[x.blockSize:]
65 | dst = dst[x.blockSize:]
66 | }
67 | }
68 |
69 | func aesECBEncrypt(plaintext, key string) (ciphertext []byte, err error) {
70 | text := zstring.String2Bytes(plaintext)
71 | text = pkcs5Padding(text, aes.BlockSize)
72 | if len(text)%aes.BlockSize != 0 {
73 | return nil, errors.New("plaintext is not a multiple of the block size")
74 | }
75 | aesKey := zstring.String2Bytes(key)
76 | block, err := aes.NewCipher(aesKey)
77 | if err != nil {
78 | return nil, err
79 | }
80 | cipher := make([]byte, len(text))
81 | newECBEncrypter(block).CryptBlocks(cipher, text)
82 | base64Msg := make([]byte, base64.StdEncoding.EncodedLen(len(cipher)))
83 | base64.StdEncoding.Encode(base64Msg, cipher)
84 |
85 | return base64Msg, nil
86 | }
87 |
88 | func aesECBDecrypt(ciphertext, key string) (plaintext []byte, err error) {
89 | text, _ := base64.StdEncoding.DecodeString(ciphertext)
90 | if len(text) < aes.BlockSize {
91 | return nil, errors.New("ciphertext too short")
92 | }
93 | if len(text)%aes.BlockSize != 0 {
94 | return nil, errors.New("ciphertext is not a multiple of the block size")
95 | }
96 |
97 | aesKey := zstring.String2Bytes(key)
98 | block, err := aes.NewCipher(aesKey)
99 | if err != nil {
100 | return nil, err
101 | }
102 | newECBDecrypter(block).CryptBlocks(text, text)
103 |
104 | plaintext = pkcs5UnPadding(text)
105 | return plaintext, nil
106 | }
107 |
108 | func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
109 | padding := blockSize - len(ciphertext)%blockSize
110 | padtext := bytes.Repeat([]byte{byte(padding)}, padding)
111 | return append(ciphertext, padtext...)
112 | }
113 |
114 | func pkcs5UnPadding(origData []byte) []byte {
115 | length := len(origData)
116 | unpadding := int(origData[length-1])
117 | return origData[:(length - unpadding)]
118 | }
119 |
--------------------------------------------------------------------------------
/cache.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | var (
9 | CacheFile = "wechat.json"
10 | CacheTime = time.Second * 60 * 10
11 | cacheFileOnce sync.Once
12 | )
13 |
--------------------------------------------------------------------------------
/code.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "strconv"
5 | "errors"
6 | )
7 |
8 | var errNoJSON = errors.New("no json")
9 | var errCode = map[int]string{
10 | -1: "系统繁忙",
11 | 0: "请求成功",
12 | 40001: "AppSecret 错误,或者 access_token 无效",
13 | 40002: "不合法的凭证类型",
14 | 40003: "不合法的OpenID",
15 | 40004: "不合法的媒体文件类型",
16 | 40005: "不合法的文件类型",
17 | 40006: "不合法的文件大小",
18 | 40007: "不合法的媒体文件id",
19 | 40008: "不合法的消息类型",
20 | 40009: "不合法的图片文件大小",
21 | 40010: "不合法的语音文件大小",
22 | 40011: "不合法的视频文件大小",
23 | 40012: "不合法的缩略图文件大小",
24 | 40013: "不合法的 APPID",
25 | 40014: "不合法的 access_token",
26 | 40015: "不合法的菜单类型",
27 | 40016: "不合法的按钮个数",
28 | 40017: "不合法的按钮类型",
29 | 40018: "不合法的按钮名字长度",
30 | 40019: "不合法的按钮 KEY 长度",
31 | 40020: "不合法的按钮 URL 长度",
32 | 40021: "不合法的菜单版本号",
33 | 40022: "不合法的子菜单级数",
34 | 40023: "不合法的子菜单按钮个数",
35 | 40024: "不合法的子菜单按钮类型",
36 | 40025: "不合法的子菜单按钮名字长度",
37 | 40026: "不合法的子菜单按钮 KEY 长度",
38 | 40027: "不合法的子菜单按钮 URL 长度",
39 | 40028: "不合法的自定义菜单使用用户",
40 | 40029: "不合法的 oauth_code",
41 | 40030: "不合法的 refresh_token",
42 | 40031: "不合法的 openid 列表",
43 | 40032: "不合法的 openid 列表长度/每次传入的 openid 列表个数不能超过50个",
44 | 40033: "不合法的请求字符,不能包含 \\uxxxx 格式的字符",
45 | 40035: "不合法的参数",
46 | 40038: "不合法的请求格式",
47 | 40039: "不合法的 URL 长度",
48 | 40050: "不合法的分组 id",
49 | 40051: "分组名字不合法",
50 | 40066: "不合法的 url",
51 | 40099: "该 code 已被核销",
52 | 40226: "高风险等级用户,登录拦截",
53 | 41001: "缺少 access_token 参数",
54 | 40125: "appsecret 无效",
55 | 45011: "频率限制,每个用户每分钟100次",
56 | 40130: "参数错误",
57 | 41002: "缺少 appid 参数",
58 | 41003: "缺少 refresh_token 参数",
59 | 41004: "缺少 secret 参数",
60 | 41005: "缺少多媒体文件数据",
61 | 41006: "缺少 media_id 参数",
62 | 41007: "缺少子菜单数据",
63 | 41008: "缺少 oauth code",
64 | 41009: "缺少 openid",
65 | 42001: "access_token 超时",
66 | 42002: "refresh_token 超时",
67 | 42003: "oauth_code 超时",
68 | 42005: "调用接口频率超过上限",
69 | 43001: "需要 GET 请求",
70 | 43002: "需要 POST 请求",
71 | 43003: "需要 HTTPS 请求",
72 | 43004: "需要接收者关注",
73 | 43005: "需要好友关系",
74 | 44001: "多媒体文件为空",
75 | 44002: "POST 的数据包为空",
76 | 44003: "图文消息内容为空",
77 | 44004: "文本消息内容为空",
78 | 45001: "多媒体文件大小超过限制",
79 | 45002: "消息内容超过限制",
80 | 45003: "标题字段超过限制",
81 | 45004: "描述字段超过限制",
82 | 45005: "链接字段超过限制",
83 | 45006: "图片链接字段超过限制",
84 | 45007: "语音播放时间超过限制",
85 | 45008: "图文消息超过限制",
86 | 45009: "接口调用超过限制",
87 | 45010: "创建菜单个数超过限制",
88 | 45015: "回复时间超过限制",
89 | 45016: "系统分组,不允许修改",
90 | 45017: "分组名字过长",
91 | 45018: "分组数量超过上限",
92 | 45024: "账号数量超过上限",
93 | 45157: "标签名非法,请注意不能和其他标签重名",
94 | 45158: "标签名长度超过30个字节",
95 | 45056: "创建的标签数过多,请注意不能超过100个",
96 | 45058: "不能修改0/1/2这三个系统默认保留的标签",
97 | 45057: "该标签下粉丝数超过10w,不允许直接删除",
98 | 45059: "有粉丝身上的标签数已经超过限制",
99 | 45159: "非法的 tag_id",
100 | 46001: "不存在媒体数据",
101 | 46002: "不存在的菜单版本",
102 | 46003: "不存在的菜单数据",
103 | 46004: "不存在的用户",
104 | 47001: "解析 JSON/XML 内容错误",
105 | 48001: "api功能未授权",
106 | 48002: "粉丝拒收消息",
107 | 48004: "api 接口被封禁",
108 | 48005: "api 禁止删除被自动回复和自定义菜单引用的素材",
109 | 48006: "api 禁止清零调用次数,因为清零次数达到上限",
110 | 48008: "没有该类型消息的发送权限",
111 | 49003: "传入的 openid 不属于此 AppID",
112 | 50001: "用户未授权该 api",
113 | 50002: "用户受限,可能是违规后接口被封禁",
114 | 50005: "用户未关注公众号",
115 | 61450: "系统错误",
116 | 61451: "参数错误",
117 | 61452: "无效客服账号",
118 | 61453: "账号已存在",
119 | 61454: "客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)",
120 | 61455: "客服账号名包含非法字符(英文+数字)",
121 | 61456: "客服账号个数超过限制(10个客服账号)",
122 | 61457: "无效头像文件类型",
123 | 61458: "客户正在被其他客服接待",
124 | 61459: "客服不在线",
125 | 61500: "日期格式错误",
126 | 61501: "日期范围错误",
127 | 7000000: "请求正常,无语义结果",
128 | 7000001: "缺失请求参数",
129 | 7000002: "signature 参数无效",
130 | 7000003: "地理位置相关配置 1 无效",
131 | 7000004: "地理位置相关配置 2 无效",
132 | 7000005: "请求地理位置信息失败",
133 | 7000006: "地理位置结果解析失败",
134 | 7000007: "内部初始化失败",
135 | 7000008: "非法 appid(获取密钥失败)",
136 | 7000009: "请求语义服务失败",
137 | 7000010: "非法 post 请求",
138 | 7000011: "post 请求 json 字段无效",
139 | 7000030: "查询 query 太短",
140 | 7000031: "查询 query 太长",
141 | 7000032: "城市、经纬度信息缺失",
142 | 7000033: "query 请求语义处理失败",
143 | 7000034: "获取天气信息失败",
144 | 7000035: "获取股票信息失败",
145 | 7000036: "utf8 编码转换失败",
146 | }
147 |
148 | func code2Str(code int) string {
149 | s, _ := errCode[code]
150 | return s
151 | }
152 |
153 | type httpError struct {
154 | Code int
155 | Msg string
156 | }
157 |
158 | func (e httpError) Error() string {
159 | return e.Msg
160 | }
161 |
162 | func (e httpError) message() (msg string) {
163 | msg = code2Str(e.Code)
164 | if msg == "" {
165 | msg = strconv.Itoa(e.Code) + " " + e.Msg
166 | }
167 | return
168 | }
169 |
170 | // ErrorMsg Get error information
171 | func ErrorMsg(err error) string {
172 | if err == nil {
173 | return ""
174 | }
175 | e, ok := err.(httpError)
176 | if ok {
177 | return e.message()
178 | }
179 | s := err.Error()
180 | if s == "" {
181 | return "未知错误"
182 | }
183 | return s
184 | }
185 |
186 | // ErrorCode Get error code
187 | func ErrorCode(err error) int {
188 | if err == nil {
189 | return -1
190 | }
191 | e, ok := err.(httpError)
192 | if ok {
193 | return e.Code
194 | }
195 | return -1
196 | }
197 |
198 |
--------------------------------------------------------------------------------
/crypto.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/sha1"
8 | "encoding/base64"
9 | "encoding/binary"
10 | "errors"
11 | "fmt"
12 | "io"
13 | "sort"
14 |
15 | "github.com/sohaha/zlsgo/zstring"
16 | )
17 |
18 | func sha1Signature(params ...string) string {
19 | sort.Strings(params)
20 | h := sha1.New()
21 | for _, s := range params {
22 | _, _ = io.WriteString(h, s)
23 | }
24 | return fmt.Sprintf("%x", h.Sum(nil))
25 | }
26 |
27 | func aesEncrypt(data string, key string, iv ...byte) ([]byte, error) {
28 | aesKey := encodingAESKey2AESKey(key)
29 | block, err := aes.NewCipher(aesKey)
30 | if err != nil {
31 | return nil, err
32 | }
33 | if len(iv) == 0 {
34 | iv = aesKey[:block.BlockSize()]
35 | }
36 | plainText := PKCS7Padding(zstring.String2Bytes(data), len(aesKey))
37 | blockMode := cipher.NewCBCEncrypter(block, iv)
38 | cipherText := make([]byte, len(plainText))
39 | blockMode.CryptBlocks(cipherText, plainText)
40 | base64Msg := make([]byte, base64.StdEncoding.EncodedLen(len(cipherText)))
41 | base64.StdEncoding.Encode(base64Msg, cipherText)
42 |
43 | return base64Msg, nil
44 | }
45 |
46 | func aesDecrypt(data string, key string, iv ...string) ([]byte,
47 | error) {
48 | aesKey := encodingAESKey2AESKey(key)
49 | cipherData, err := base64.StdEncoding.DecodeString(data)
50 | if err != nil {
51 | return nil, err
52 | }
53 | block, err := aes.NewCipher(aesKey)
54 | if err != nil {
55 | return nil, err
56 | }
57 | var ivRaw []byte
58 | plainText := make([]byte, len(cipherData))
59 | if len(iv) == 0 {
60 | ivRaw = aesKey[:block.BlockSize()]
61 | } else {
62 | ivRaw = zstring.String2Bytes(iv[0])
63 | }
64 | blockMode := cipher.NewCBCDecrypter(block, ivRaw)
65 | blockMode.CryptBlocks(plainText, cipherData)
66 |
67 | return PKCS7UnPadding(plainText, len(key)), nil
68 | }
69 |
70 | func encodingAESKey2AESKey(encodingKey string) []byte {
71 | data, _ := base64.StdEncoding.DecodeString(encodingKey + "=")
72 | return data
73 | }
74 |
75 | func PKCS7Padding(cipherText []byte, blockSize int) []byte {
76 | padding := blockSize - len(cipherText)%blockSize
77 | if padding == 0 {
78 | padding = blockSize
79 | }
80 | padText := bytes.Repeat([]byte{byte(padding)}, padding)
81 | return append(cipherText, padText...)
82 | }
83 |
84 | func PKCS7UnPadding(plainText []byte, blockSize int) []byte {
85 | l := len(plainText)
86 | unpadding := int(plainText[l-1])
87 | if unpadding < 0 || unpadding > blockSize {
88 | unpadding = 0
89 | }
90 | return plainText[:(l - unpadding)]
91 | }
92 |
93 | func parsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, error) {
94 | textLen := uint32(len(plaintext))
95 | if textLen < 20 {
96 | return nil, 0, nil, nil, errors.New("plain is to small 1")
97 | }
98 | random := plaintext[:16]
99 | msgLen := binary.BigEndian.Uint32(plaintext[16:20])
100 | if textLen < (20 + msgLen) {
101 | return nil, 0, nil, nil, errors.New("plain is to small 2")
102 | }
103 | msg := plaintext[20 : 20+msgLen]
104 | receiverId := plaintext[20+msgLen:]
105 | return random, msgLen, msg, receiverId, nil
106 | }
107 |
108 | func MarshalPlainText(replyMsg, receiverId, random string) string {
109 | var buffer bytes.Buffer
110 | buffer.WriteString(random)
111 | msgLenBuf := make([]byte, 4)
112 | binary.BigEndian.PutUint32(msgLenBuf, uint32(len(replyMsg)))
113 | buffer.Write(msgLenBuf)
114 | buffer.WriteString(replyMsg)
115 | buffer.WriteString(receiverId)
116 | return buffer.String()
117 | }
118 |
--------------------------------------------------------------------------------
/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/zlsgo/wechat"
5 | )
6 |
7 | // Wx 微信实例
8 | var (
9 | Wx *wechat.Engine
10 | WxOpen *wechat.Engine
11 | WxQy *wechat.Engine
12 | Weapp *wechat.Engine
13 | Pay *wechat.Pay
14 | )
15 |
16 | func main() {
17 | // 开启调试日志
18 | wechat.Debug()
19 |
20 | // 加载文件缓存数据
21 | _ = wechat.LoadCacheData("wechat.json")
22 |
23 | // 支持公众号 企业微信 开放平台 小程序 微信支付
24 | Wx = wechat.New(&wechat.Mp{
25 | AppID: "",
26 | AppSecret: "",
27 | Token: "",
28 | })
29 | WxOpen = wechat.New(&wechat.Open{
30 | AppID: "",
31 | AppSecret: "",
32 | EncodingAesKey: "",
33 | })
34 | WxQy = wechat.New(&wechat.Qy{
35 | CorpID: "",
36 | Secret: "",
37 | Token: "",
38 | EncodingAesKey: "",
39 | })
40 | Weapp = wechat.New(&wechat.Weapp{
41 | AppID: "",
42 | AppSecret: "",
43 | })
44 | Pay = wechat.NewPay(wechat.Pay{
45 | MchId: "",
46 | Key: "",
47 | CertPath: "",
48 | KeyPath: "",
49 | })
50 | }
51 |
52 | func SaveWxCacheData() (string, error) {
53 | // 保存缓存数据至文件
54 | return wechat.SaveCacheData("wechat.json")
55 | }
56 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/zlsgo/wechat
2 |
3 | go 1.11
4 |
5 |
--------------------------------------------------------------------------------
/http.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "github.com/sohaha/zlsgo/zhttp"
8 | "github.com/sohaha/zlsgo/zjson"
9 | "github.com/sohaha/zlsgo/ztype"
10 | )
11 |
12 | var http = zhttp.New()
13 |
14 | func init() {
15 | http.DisableChunke()
16 | }
17 |
18 | func (e *Engine) Http() *zhttp.Engine {
19 | return http
20 | }
21 |
22 | func (e *Engine) HttpAccessTokenGet(url string, v ...interface{}) (j *zjson.Res, err error) {
23 | token, err := e.GetAccessToken()
24 | if err != nil {
25 | return nil, err
26 | }
27 | j, err = httpResProcess(http.Get(url, append(transformSendData(v), zhttp.QueryParam{"access_token": token})...))
28 | if e.checkTokenExpiration(err) {
29 | return e.HttpAccessTokenGet(url, v...)
30 | }
31 | return
32 | }
33 |
34 | func (e *Engine) HttpAccessTokenPost(url string, v ...interface{}) (j *zjson.Res, err error) {
35 | var token string
36 | token, err = e.GetAccessToken()
37 | if err != nil {
38 | return
39 | }
40 | j, err = httpResProcess(http.Post(url, append(transformSendData(v), zhttp.QueryParam{"access_token": token})...))
41 | if e.checkTokenExpiration(err) {
42 | return e.HttpAccessTokenPost(url, v...)
43 | }
44 | return
45 | }
46 |
47 | func (e *Engine) HttpAccessTokenPostRaw(url string, v ...interface{}) (j []byte, err error) {
48 | token, err := e.GetAccessToken()
49 | if err != nil {
50 | return
51 | }
52 |
53 | b, err := httpProcess(http.Post(url, append(transformSendData(v), zhttp.QueryParam{"access_token": token})...))
54 |
55 | if err == errNoJSON {
56 | return b, nil
57 | }
58 |
59 | if e.checkTokenExpiration(err) {
60 | return e.HttpAccessTokenPostRaw(url, v...)
61 | }
62 |
63 | _, err = CheckResError(b)
64 | return nil, err
65 | }
66 |
67 | func httpResProcess(r *zhttp.Res, e error) (*zjson.Res, error) {
68 | b, err := httpProcess(r, e)
69 | if err != nil {
70 | return nil, err
71 | }
72 | return CheckResError(b)
73 | }
74 |
75 | func httpProcess(r *zhttp.Res, e error) ([]byte, error) {
76 | if e != nil {
77 | return nil, httpError{Code: -2, Msg: "网络请求失败"}
78 | }
79 | if r.StatusCode() != 200 {
80 | return nil, httpError{Code: -2, Msg: "接口请求失败: " + r.Response().Status}
81 | }
82 | bytes := r.Bytes()
83 | ctype := r.Response().Header.Get("Content-Type")
84 | if strings.Contains(ctype, "image/") {
85 | e = errNoJSON
86 | }
87 | return bytes, e
88 | }
89 |
90 | func httpPayProcess(r *zhttp.Res, e error) (ztype.Map, error) {
91 | b, err := httpProcess(r, e)
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | x, err := ParseXML2Map(b)
97 | if err == nil {
98 | code := x.Get("return_code").String()
99 | if code == "SUCCESS" {
100 | resultCode := x.Get("result_code").String()
101 | if resultCode != "" && resultCode != "FAIL" {
102 | return x, nil
103 | }
104 | }
105 | codeDes := x.Get("err_code_des")
106 | msg := "未知错误"
107 | if !codeDes.Exists() {
108 | returnMsg := x.Get("return_msg")
109 | if returnMsg.Exists() {
110 | msg = returnMsg.String()
111 | }
112 | } else {
113 | msg = codeDes.String()
114 | }
115 | if strings.Contains(msg, "无效,请检查需要验收的case") {
116 | msg = "沙盒只支持指定金额, 如: 101 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_13"
117 | }
118 | return ztype.Map{}, errors.New(msg)
119 | }
120 |
121 | return x, err
122 | }
123 |
124 | func (e *Engine) checkTokenExpiration(err error) bool {
125 | if err != nil && ErrorCode(err) == 42001 {
126 | _, _ = e.cache.Delete(cacheToken)
127 | return true
128 | }
129 | return false
130 | }
131 |
--------------------------------------------------------------------------------
/js.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/sohaha/zlsgo/zhttp"
8 | "github.com/sohaha/zlsgo/zjson"
9 | "github.com/sohaha/zlsgo/zstring"
10 | )
11 |
12 | type (
13 | JsSign struct {
14 | AppID string `json:"appid"`
15 | Timestamp int64 `json:"timestamp"`
16 | NonceStr string `json:"nonce_str"`
17 | Signature string `json:"signature"`
18 | }
19 | )
20 |
21 | func (e *Engine) GetJsSign(url string) (JsSign, error) {
22 | jsapiTicket, err := e.GetJsapiTicket()
23 | if err != nil {
24 | return JsSign{}, err
25 | }
26 |
27 | timestamp := time.Now().Unix()
28 | noncestr := zstring.Rand(16)
29 | signature := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", jsapiTicket, noncestr, timestamp, url)
30 | signature = sha1Signature(signature)
31 |
32 | return JsSign{
33 | AppID: e.GetAppID(),
34 | NonceStr: noncestr,
35 | Timestamp: timestamp,
36 | Signature: signature,
37 | }, nil
38 | }
39 |
40 | func (e *Engine) SetJsapiTicket(ticket string, expiresIn uint) error {
41 | e.cache.Set(cacheJsapiTicket, ticket, expiresIn-60)
42 | return nil
43 | }
44 |
45 | func (e *Engine) GetJsapiTicket() (string, error) {
46 | data, err := e.cache.MustGet(cacheJsapiTicket, func(set func(data interface{}, lifeSpan time.Duration, interval ...bool)) (err error) {
47 | var res *zhttp.Res
48 | res, err = e.config.getJsapiTicket()
49 | if err != nil {
50 | return
51 | }
52 | var json *zjson.Res
53 | json, err = CheckResError(res.Bytes())
54 | if err != nil {
55 | return
56 | }
57 | ticket := json.Get("ticket").String()
58 | if ticket == "" {
59 | return
60 | }
61 | set(ticket, time.Duration(json.Get("expires_in").Int()-200)*time.Second)
62 | return
63 | })
64 | if err != nil {
65 | return "", err
66 | }
67 | return data.(string), nil
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/mp.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/sohaha/zlsgo/zhttp"
7 | )
8 |
9 | type (
10 | Mp struct {
11 | // 公众号 ID
12 | AppID string
13 | // 公众号密钥
14 | AppSecret string
15 | EncodingAesKey string
16 | Token string
17 | engine *Engine
18 | }
19 | )
20 |
21 | var _ Cfg = new(Mp)
22 |
23 | func (m *Mp) setEngine(engine *Engine) {
24 | m.engine = engine
25 | }
26 |
27 | func (m *Mp) getEngine() *Engine {
28 | return m.engine
29 | }
30 |
31 | func (m *Mp) GetAppID() string {
32 | return m.AppID
33 | }
34 |
35 | func (m *Mp) GetSecret() string {
36 | return m.AppSecret
37 | }
38 |
39 | func (m *Mp) GetToken() string {
40 | return m.Token
41 | }
42 |
43 | func (m *Mp) GetEncodingAesKey() string {
44 | return m.EncodingAesKey
45 | }
46 |
47 | func (m *Mp) getAccessToken() (data []byte, err error) {
48 | res, err := http.Post(fmt.Sprintf(
49 | "%s/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
50 | APIURL, m.AppID, m.AppSecret))
51 | if err != nil {
52 | return
53 | }
54 | data = res.Bytes()
55 | return
56 | }
57 |
58 | func (m *Mp) getJsapiTicket() (data *zhttp.Res, err error) {
59 | var token string
60 | token, err = m.engine.GetAccessToken()
61 | if err != nil {
62 | return nil, err
63 | }
64 | return http.Post(fmt.Sprintf(
65 | "%s/cgi-bin/ticket/getticket?&type=jsapi&access_token=%s",
66 | APIURL, token))
67 | }
68 |
--------------------------------------------------------------------------------
/open.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/sohaha/zlsgo/zhttp"
9 | "github.com/sohaha/zlsgo/zjson"
10 | "github.com/sohaha/zlsgo/znet"
11 | "github.com/sohaha/zlsgo/zstring"
12 | "github.com/sohaha/zlsgo/ztype"
13 | )
14 |
15 | type (
16 | Open struct {
17 | AppID string
18 | AppSecret string
19 | EncodingAesKey string
20 | engine *Engine
21 | refreshToken string
22 | authorizerAppID string
23 | Token string
24 | }
25 | )
26 |
27 | var (
28 | ErrOpenJumpAuthorization = errors.New(
29 | "need to jump to the authorization page")
30 | )
31 | var _ Cfg = new(Open)
32 |
33 | func (o *Open) setEngine(engine *Engine) {
34 | o.engine = engine
35 | }
36 |
37 | func (o *Open) getEngine() *Engine {
38 | return o.engine
39 | }
40 |
41 | func (o *Open) GetSecret() string {
42 | return o.AppSecret
43 | }
44 | func (o *Open) GetToken() string {
45 | return o.Token
46 | }
47 |
48 | func (o *Open) GetEncodingAesKey() string {
49 | return o.EncodingAesKey
50 | }
51 |
52 | func (o *Open) checkEngine() (*Engine, error) {
53 | if o.engine == nil {
54 | return nil, errors.New(`please use wechat.New(&wechat.Open{})`)
55 | }
56 | return o.engine, nil
57 | }
58 |
59 | func (o *Open) GetComponentTicket() (string, error) {
60 | if _, err := o.checkEngine(); err != nil {
61 | return "", err
62 | }
63 | data, err := o.engine.cache.GetString(cacheComponentVerifyTicket)
64 | if err != nil {
65 | return "", errors.New("have not received wechat push information")
66 | }
67 |
68 | return data, nil
69 | }
70 |
71 | func (o *Open) GetComponentAccessToken() (string, error) {
72 | if _, err := o.checkEngine(); err != nil {
73 | return "", err
74 | }
75 | data, err := o.engine.cache.MustGet("component_access_token", func(set func(data interface{}, lifeSpan time.Duration, interval ...bool)) (err error) {
76 | var ticket string
77 | ticket, err = o.GetComponentTicket()
78 | if err != nil {
79 | return
80 | }
81 | post := zhttp.Param{
82 | "component_appid": o.AppID,
83 | "component_appsecret": o.AppSecret,
84 | "component_verify_ticket": ticket,
85 | }
86 | res, err := http.Post(fmt.Sprintf(
87 | "%s/cgi-bin/component/api_component_token", APIURL), zhttp.BodyJSON(post))
88 | if err != nil {
89 | return
90 | }
91 | var json *zjson.Res
92 | json, err = CheckResError(res.Bytes())
93 | if err != nil {
94 | return
95 | }
96 | componentAppsecret := json.Get("component_access_token").String()
97 | if componentAppsecret == "" {
98 | return errors.New("failed to parse component access token")
99 | }
100 | set(componentAppsecret, time.Duration(json.Get("expires_in").Int()-200)*time.Second)
101 |
102 | return
103 | })
104 | if err != nil {
105 | return "", err
106 | }
107 | return data.(string), nil
108 |
109 | }
110 |
111 | func (o *Open) getPreAuthCode() (string, error) {
112 | e, err := o.checkEngine()
113 | if err != nil {
114 | return "", err
115 | }
116 | var data interface{}
117 | data, err = e.cache.MustGet("pre_auth_code",
118 | func(set func(data interface{}, lifeSpan time.Duration, interval ...bool)) (err error) {
119 | var ticket string
120 | ticket, err = o.GetComponentAccessToken()
121 | if err != nil {
122 | return
123 | }
124 | url := fmt.Sprintf("%s/cgi-bin/component/api_create_preauthcode?component_access_token=%s", APIURL, ticket)
125 | var res *zhttp.Res
126 | post, _ := zjson.Set("{}", "component_appid", o.AppID)
127 | res, err = http.Post(url, post)
128 | if err != nil {
129 | return
130 | }
131 | var json *zjson.Res
132 | json, err = CheckResError(res.Bytes())
133 | if err != nil {
134 | return
135 | }
136 | authCode := json.Get("pre_auth_code").String()
137 | set(authCode, time.Duration(json.Get("expires_in").Int()-200)*time.Second)
138 |
139 | return
140 | })
141 |
142 | if err != nil {
143 | return "", err
144 | }
145 |
146 | return data.(string), nil
147 |
148 | }
149 |
150 | func (e *Engine) GetConfig() Cfg {
151 | return e.config
152 | }
153 |
154 | // ComponentVerifyTicket 解析微信开放平台 Ticket
155 | func (e *Engine) ComponentVerifyTicket(raw string) (
156 | string, error) {
157 | if !e.IsOpen() {
158 | return "", errors.New("only supports open")
159 | }
160 | config, ok := e.config.(*Open)
161 | if !ok {
162 | return "", errors.New("only supports open")
163 | }
164 | data, _ := ParseXML2Map(zstring.String2Bytes(raw))
165 | encrypt := data.Get("Encrypt").String()
166 | if encrypt == "" {
167 | return "", errors.New("illegal data")
168 | }
169 |
170 | var (
171 | err error
172 | cipherText []byte
173 | )
174 | cipherText, _ = aesDecrypt(encrypt, config.EncodingAesKey)
175 | appidOffset := len(cipherText) - len(zstring.String2Bytes(config.AppID))
176 | if appid := string(cipherText[appidOffset:]); appid != config.AppID {
177 | return "", errors.New("appid mismatch")
178 | }
179 | cipherText = cipherText[20:appidOffset]
180 | var ticketData ztype.Map
181 | ticketData, err = ParseXML2Map(cipherText)
182 | if err != nil {
183 | return "", err
184 | }
185 | ticket := ticketData.Get("ComponentVerifyTicket").String()
186 | log.Debug("收到 Ticket:", ticket)
187 | e.cache.Set(cacheComponentVerifyTicket, ticket, 0)
188 | return ticket, nil
189 | }
190 |
191 | func (o *Open) GetAppID() string {
192 | return o.AppID
193 | }
194 |
195 | func (e *Engine) ComponentApiQueryAuth(c *znet.Context, authCode string) (s string,
196 | redirect string, err error) {
197 | if !e.IsOpen() {
198 | return "", "", errors.New("only supports open")
199 | }
200 |
201 | var redirectUri string
202 | if len(e.redirectDomain) > 0 {
203 | redirect = e.redirectDomain + c.Request.URL.String()
204 | } else {
205 | redirect = c.Host(true)
206 | }
207 |
208 | config := e.config.(*Open)
209 | if authCode == "" {
210 | return e.getAuthUri(config, redirect)
211 | }
212 | componentAccessToken, err := config.GetComponentAccessToken()
213 | if err != nil {
214 | return "", "", err
215 | }
216 | res, err := http.Post(fmt.Sprintf(
217 | "%s/cgi-bin/component/api_query_auth?component_access_token=%s", APIURL,
218 | componentAccessToken), zhttp.BodyJSON(
219 | map[string]string{
220 | "component_appid": e.GetAppID(),
221 | "authorization_code": authCode,
222 | }))
223 | if err != nil {
224 | return "", "", err
225 | }
226 | json, err := CheckResError(res.Bytes())
227 | if err != nil {
228 | return e.getAuthUri(config, redirectUri)
229 | }
230 | return json.String(), "", nil
231 | }
232 |
233 | func (e *Engine) getAuthUri(config *Open, redirectUri string) (string, string, error) {
234 | preAuthCode, err := config.getPreAuthCode()
235 | if err != nil {
236 | return "", "", err
237 | }
238 | url := fmt.Sprintf("https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s", e.GetAppID(), preAuthCode, redirectUri)
239 | return "", url, ErrOpenJumpAuthorization
240 | }
241 |
242 | func (o *Open) getAccessToken() (data []byte, err error) {
243 | if o.refreshToken == "" {
244 | err = errors.New("please authorize it through the ComponentApiQueryAuth method")
245 | return
246 | }
247 | var componentAccessToken string
248 | componentAccessToken, err = o.GetComponentAccessToken()
249 | if err != nil {
250 | return
251 | }
252 | res, err := http.Post(fmt.Sprintf(
253 | "%s/cgi-bin/component/api_authorizer_token?component_access_token=%s", APIURL, componentAccessToken), zhttp.BodyJSON(zhttp.Param{
254 | "component_appid": o.AppID,
255 | "authorizer_appid": o.authorizerAppID,
256 | "authorizer_refresh_token": o.refreshToken,
257 | }))
258 | if err != nil {
259 | return
260 | }
261 | var json *zjson.Res
262 | json, err = CheckResError(res.Bytes())
263 | if err != nil {
264 | return
265 | }
266 | refreshToken := json.Get("authorizer_refresh_token").String()
267 | if refreshToken == "" {
268 | err = errors.New("failed to parse api authorizer token")
269 | return
270 | }
271 | o.refreshToken = refreshToken
272 |
273 | return res.Bytes(), nil
274 | }
275 |
276 | func (o *Open) SetAuthorizerAccessToken(authorizerAppID, accessToken,
277 | refreshToken string, expiresIn uint) {
278 | o.refreshToken = refreshToken
279 | o.authorizerAppID = authorizerAppID
280 | o.engine.cache.Set(cacheToken, accessToken, expiresIn)
281 | }
282 |
283 | func (o *Open) getJsapiTicket() (data *zhttp.Res, err error) {
284 | var token string
285 | token, err = o.engine.GetAccessToken()
286 | if err != nil {
287 | return nil, err
288 | }
289 | return http.Post(fmt.Sprintf(
290 | "%s/cgi-bin/ticket/getticket?&type=jsapi&access_token=%s",
291 | APIURL, token))
292 | }
293 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | type WechatOptions struct {
8 | e *Engine
9 | }
10 |
11 | type Option func(e *Engine)
12 |
13 | func (e *Engine) SetOptions(opts ...Option) {
14 | for _, opt := range opts {
15 | opt(e)
16 | }
17 | }
18 |
19 | func WithRedirectDomain(domain string) Option {
20 | return func(e *Engine) {
21 | e.redirectDomain = strings.TrimRight(domain, "/")
22 | }
23 | }
24 |
25 | func WithToggleAgentID(agentID string) Option {
26 | return func(e *Engine) {
27 | conf, ok := e.config.(*Qy)
28 | if ok {
29 | conf.AgentID = agentID
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/other.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "github.com/sohaha/zlsgo/zhttp"
5 | "github.com/sohaha/zlsgo/zjson"
6 | "github.com/sohaha/zlsgo/zstring"
7 | )
8 |
9 | // GetAuthUserInfo 获取用户信息
10 | // 企业微信需要使用 user_ticket 代替 openid
11 | func (e *Engine) GetAuthUserInfo(openid, authAccessToken string) (json *zjson.Res, err error) {
12 | u := zstring.Buffer(6)
13 | u.WriteString(e.apiURL)
14 | switch true {
15 | case e.IsQy():
16 | u.WriteString("/cgi-bin/user/getuserdetail?access_token=")
17 | u.WriteString(authAccessToken)
18 | return httpResProcess(http.Post(u.String(), zhttp.BodyJSON(map[string]interface{}{"user_ticket": openid})))
19 | default:
20 | u.WriteString("/sns/userinfo?access_token=")
21 | u.WriteString(authAccessToken)
22 | u.WriteString("&openid=")
23 | u.WriteString(openid)
24 | return httpResProcess(http.Get(u.String()))
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/pay.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "strconv"
6 | "time"
7 |
8 | "github.com/sohaha/zlsgo/zfile"
9 | "github.com/sohaha/zlsgo/zhttp"
10 | "github.com/sohaha/zlsgo/zstring"
11 | "github.com/sohaha/zlsgo/ztype"
12 | )
13 |
14 | type Pay struct {
15 | MchId string // 商户ID
16 | Key string // V2密钥
17 | CertPath string // 证书路径
18 | KeyPath string // 证书路径
19 | prikey string // 私钥内容
20 | sandbox bool // 开启支付沙盒
21 | sandboxKey string
22 | http *zhttp.Engine
23 | }
24 |
25 | type OrderCondition struct {
26 | OutTradeNo string `json:"out_trade_no,omitempty"`
27 | TransactionId string `json:"transaction_id,omitempty"`
28 | }
29 |
30 | type PayOrder struct {
31 | Appid string `json:"appid,omitempty"`
32 | DeviceInfo string `json:"device_info,omitempty"`
33 | NonceStr string `json:"nonce_str,omitempty"`
34 | SignType string `json:"sign_type,omitempty"`
35 | Body string `json:"body,omitempty"`
36 | OutTradeNo string `json:"out_trade_no,omitempty"`
37 | FeeType string `json:"fee_type,omitempty"`
38 | totalFee uint
39 | spbillCreateIp string
40 | TradeType string `json:"trade_type,omitempty"`
41 | openid string
42 | }
43 | type PayOrderOption func(*PayOrder)
44 |
45 | func (p PayOrder) GetOutTradeNo() string {
46 | return p.OutTradeNo
47 | }
48 |
49 | func (p PayOrder) build() ztype.Map {
50 | m := ztype.ToMap(p)
51 | m["openid"] = p.openid
52 | m["total_fee"] = p.totalFee
53 | m["spbill_create_ip"] = p.spbillCreateIp
54 |
55 | return m
56 | }
57 |
58 | // NewPayOrder 支付订单
59 | func NewPayOrder(openid string, totalFee uint, ip string, body string, opts ...PayOrderOption) PayOrder {
60 | outTradeNo := zstring.Md5(openid)[0:12] + strconv.Itoa(int(time.Now().Unix())) + zstring.Rand(10)
61 | p := PayOrder{
62 | DeviceInfo: "WEB",
63 | NonceStr: zstring.Rand(16),
64 | SignType: "MD5",
65 | FeeType: "CNY",
66 | TradeType: "JSAPI",
67 | openid: openid,
68 | OutTradeNo: outTradeNo,
69 | totalFee: totalFee,
70 | Body: body,
71 | spbillCreateIp: ip,
72 | }
73 | for _, opt := range opts {
74 | opt(&p)
75 | }
76 | return p
77 | }
78 |
79 | type RefundOrder struct {
80 | Appid string `json:"appid,omitempty"`
81 | DeviceInfo string `json:"device_info,omitempty"`
82 | NonceStr string `json:"nonce_str,omitempty"`
83 | SignType string `json:"sign_type,omitempty"`
84 | OutRefundNo string `json:"out_refund_no,omitempty"`
85 | RefundFeeType string `json:"refund_fee_type,omitempty"`
86 | totalFee uint
87 | refundFee uint
88 | OutTradeNo string `json:"out_trade_no,omitempty"`
89 | TransactionId string `json:"transaction_id,omitempty"`
90 | }
91 |
92 | type RefundOrderOption func(*RefundOrder)
93 |
94 | func (p RefundOrder) GetOutRefundNo() string {
95 | return p.OutRefundNo
96 | }
97 |
98 | func (p RefundOrder) build() map[string]interface{} {
99 | m := ztype.ToMap(p)
100 | m["total_fee"] = p.totalFee
101 | m["refund_fee"] = p.refundFee
102 |
103 | return m
104 | }
105 |
106 | // NewRefundOrder 退款订单
107 | func NewRefundOrder(totalFee, refundFee uint, condition OrderCondition, opts ...RefundOrderOption) RefundOrder {
108 | m := condition.OutTradeNo
109 | if len(m) == 0 {
110 | m = condition.TransactionId
111 | }
112 | OutRefundNo := zstring.Md5(m)[0:12] + strconv.Itoa(int(time.Now().Unix())) + zstring.Rand(10)
113 | p := RefundOrder{
114 | DeviceInfo: "WEB",
115 | NonceStr: zstring.Rand(16),
116 | SignType: "MD5",
117 | RefundFeeType: "CNY",
118 | OutRefundNo: OutRefundNo,
119 | OutTradeNo: condition.OutTradeNo,
120 | TransactionId: condition.TransactionId,
121 | totalFee: totalFee,
122 | refundFee: refundFee,
123 | }
124 | for _, opt := range opts {
125 | opt(&p)
126 | }
127 | return p
128 | }
129 |
130 | // NewPay 创建支付
131 | func NewPay(p Pay) *Pay {
132 | p.http = zhttp.New()
133 | if len(p.CertPath) > 0 && len(p.KeyPath) > 0 {
134 | p.http.TlsCertificate(zhttp.Certificate{
135 | CertFile: p.CertPath,
136 | KeyFile: p.KeyPath,
137 | })
138 | }
139 | return &p
140 | }
141 |
142 | func (p *Pay) Sandbox(enable bool) *Pay {
143 | p.sandbox = enable
144 | return p
145 | }
146 |
147 | func (p *Pay) GetSandboxSignkey() (string, error) {
148 | data := map[string]interface{}{
149 | "mch_id": p.MchId,
150 | "key": p.Key,
151 | "sign_type": "MD5",
152 | "nonce_str": zstring.Rand(16),
153 | }
154 |
155 | data["sign"] = signParam(sortParam(data, p.Key), "MD5", "")
156 |
157 | sMap := make(ztype.Map, len(data))
158 | for k, val := range data {
159 | sMap[k] = ztype.ToString(val)
160 | }
161 |
162 | xml, _ := FormatMap2XML(sMap)
163 | res, err := http.Post("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", xml)
164 | if err != nil {
165 | return "", err
166 | }
167 |
168 | xmlData, err := ParseXML2Map(res.Bytes())
169 | if err != nil {
170 | return "", err
171 | }
172 |
173 | key := xmlData.Get("sandbox_signkey").String()
174 | if key == "" {
175 | return "", errors.New("获取沙盒 Key 失败")
176 | }
177 | return key, nil
178 | }
179 |
180 | func (p *Pay) prikeyText() string {
181 | if len(p.prikey) == 0 {
182 | if prikey, err := zfile.ReadFile(p.KeyPath); err == nil {
183 | p.prikey = zstring.Bytes2String(prikey)
184 | }
185 | }
186 | return p.prikey
187 | }
188 |
189 | func (p *Pay) getKey() string {
190 | if !p.sandbox {
191 | return p.Key
192 | }
193 | if len(p.sandboxKey) == 0 {
194 | p.sandboxKey, _ = p.GetSandboxSignkey()
195 | }
196 | return p.sandboxKey
197 | }
198 |
199 | type Order struct {
200 | TransactionID string
201 | OutTradeNo string
202 | }
203 |
204 | // Orderquery 订单查询
205 | func (p *Pay) Orderquery(o Order) (ztype.Map, error) {
206 | if len(o.OutTradeNo) == 0 && len(o.TransactionID) == 0 {
207 | return nil, errors.New("out_trade_no、transaction_id 至少填一个")
208 | }
209 |
210 | data := map[string]interface{}{
211 | "mch_id": p.MchId,
212 | "appid": "wx591bf582cee71574",
213 | "nonce_str": zstring.Rand(32),
214 | "sign_type": "MD5",
215 | "transaction_id": o.TransactionID,
216 | "out_trade_no": o.OutTradeNo,
217 | }
218 |
219 | data["sign"] = signParam(sortParam(data, p.getKey()), "MD5", "")
220 | url := "https://api.mch.weixin.qq.com/pay/orderquery"
221 | if p.sandbox {
222 | url = "https://api.mch.weixin.qq.com/sandboxnew/pay/orderquery"
223 | }
224 |
225 | sMap := make(ztype.Map, len(data))
226 | for k, val := range data {
227 | sMap[k] = ztype.ToString(val)
228 | }
229 |
230 | xml, err := FormatMap2XML(sMap)
231 | if err != nil {
232 | return nil, err
233 | }
234 |
235 | return httpPayProcess(p.http.Post(url, xml))
236 | }
237 |
238 | // UnifiedOrder 统一下单
239 | func (p *Pay) UnifiedOrder(appid string, order PayOrder, notifyUrl string) (prepayID string, err error) {
240 | url := "https://api.mch.weixin.qq.com/pay/unifiedorder"
241 | if p.sandbox {
242 | url = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder"
243 | }
244 |
245 | data := order.build()
246 | data["notify_url"] = notifyUrl
247 | data["mch_id"] = p.MchId
248 | data["appid"] = appid
249 | data["sign"] = signParam(sortParam(data, p.getKey()), "MD5", "")
250 |
251 | sMap := make(ztype.Map, len(data))
252 | for k := range data {
253 | sMap[k] = data.Get(k).String()
254 | }
255 |
256 | xml, err := FormatMap2XML(sMap)
257 | if err != nil {
258 | return "", err
259 | }
260 |
261 | xmlData, err := httpPayProcess(p.http.Post(url, xml))
262 | if err != nil {
263 | return "", err
264 | }
265 | return xmlData.Get("prepay_id").String(), nil
266 | }
267 |
268 | // Refund 申请退款
269 | func (p *Pay) Refund(appid string, order RefundOrder, notifyUrl string) (refundID string, err error) {
270 | url := "https://api.mch.weixin.qq.com/secapi/pay/refund"
271 | if p.sandbox {
272 | url = "https://api.mch.weixin.qq.com/sandboxnew/pay/refund"
273 | }
274 |
275 | data := order.build()
276 | data["notify_url"] = notifyUrl
277 | data["mch_id"] = p.MchId
278 | data["appid"] = appid
279 | data["sign"] = signParam(sortParam(data, p.getKey()), "MD5", "")
280 |
281 | sMap := make(ztype.Map, len(data))
282 | for k, val := range data {
283 | sMap[k] = ztype.ToString(val)
284 | }
285 |
286 | xml, err := FormatMap2XML(sMap)
287 | if err != nil {
288 | return "", err
289 | }
290 |
291 | xmlData, err := httpPayProcess(p.http.Post(url, xml))
292 | if err != nil {
293 | return "", err
294 | }
295 |
296 | return xmlData.Get("refund_id").String(), nil
297 | }
298 |
299 | // JsSign 微信页面支付签名
300 | func (p *Pay) JsSign(appid, prepayID string) map[string]interface{} {
301 | data := map[string]interface{}{
302 | "signType": "MD5",
303 | "timeStamp": strconv.Itoa(int(time.Now().Unix())),
304 | "nonceStr": zstring.Rand(16),
305 | "package": "prepay_id=" + prepayID,
306 | "appId": appid,
307 | }
308 |
309 | key := p.prikeyText()
310 | data["paySign"] = signParam(sortParam(data, p.Key), "MD5", key)
311 | return data
312 | }
313 |
314 | type NotifyType uint
315 |
316 | const (
317 | UnknownNotify NotifyType = iota
318 | PayNotify
319 | RefundNotify
320 | )
321 |
322 | type NotifyResult struct {
323 | Type NotifyType
324 | Data ztype.Map
325 | Response []byte
326 | }
327 |
328 | // Notify 支付/退款通知
329 | func (p *Pay) Notify(raw string) (result *NotifyResult, err error) {
330 | result = &NotifyResult{
331 | Type: UnknownNotify,
332 | }
333 |
334 | defer func() {
335 | if err != nil {
336 | result.Response = []byte(``)
337 | } else {
338 | result.Response = []byte(``)
339 | }
340 | }()
341 |
342 | var data ztype.Map
343 | data, err = ParseXML2Map(zstring.String2Bytes(raw))
344 | if err != nil {
345 | return
346 | }
347 |
348 | info := data.Get("req_info").String()
349 | if info != "" {
350 | result.Type = RefundNotify
351 | var plain []byte
352 | plain, err = aesECBDecrypt(info, zstring.Md5(p.getKey()))
353 | if err != nil {
354 | return
355 | }
356 |
357 | if d, err := ParseXML2Map(plain); err == nil {
358 | for k := range d {
359 | data[k] = d[k]
360 | }
361 | delete(data, "req_info")
362 | }
363 | } else {
364 | result.Type = PayNotify
365 | }
366 |
367 | result.Data = data
368 |
369 | if returnCode, ok := data["return_code"]; ok && returnCode == "SUCCESS" {
370 | signData := make(map[string]interface{}, len(data))
371 | resultSign := ""
372 | signType := ""
373 | for key := range data {
374 | if key == "sign" {
375 | resultSign = data.Get(key).String()
376 | continue
377 | }
378 | if key == "sign_type" {
379 | signType = data.Get(key).String()
380 | }
381 | signData[key] = data[key]
382 | }
383 | if len(signType) > 0 {
384 | sign := signParam(sortParam(signData, p.getKey()), signType, "")
385 | if resultSign != sign {
386 | err = errors.New("非法支付结果通用通知")
387 | return
388 | }
389 | }
390 | }
391 |
392 | return
393 | }
394 |
--------------------------------------------------------------------------------
/qy.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/sohaha/zlsgo/zhttp"
7 | )
8 |
9 | type (
10 | Qy struct {
11 | AgentID string
12 | Secret string
13 | EncodingAesKey string
14 | Token string
15 | CorpID string
16 | engine *Engine
17 | }
18 | )
19 |
20 | var _ Cfg = new(Qy)
21 |
22 | func (q *Qy) setEngine(engine *Engine) {
23 | q.engine = engine
24 | }
25 |
26 | func (q *Qy) getEngine() *Engine {
27 | return q.engine
28 | }
29 |
30 | func (q *Qy) GetAppID() string {
31 | return q.CorpID
32 | }
33 |
34 | func (q *Qy) GetSecret() string {
35 | return q.Secret
36 | }
37 |
38 | func (q *Qy) GetToken() string {
39 | return q.Token
40 | }
41 |
42 | func (q *Qy) GetEncodingAesKey() string {
43 | return q.EncodingAesKey
44 | }
45 |
46 | func (q *Qy) getAccessToken() (data []byte, err error) {
47 | var res *zhttp.Res
48 | res, err = http.Get(fmt.Sprintf(
49 | "%s/cgi-bin/gettoken?corpid=%s&corpsecret=%s", QyAPIURL, q.CorpID,
50 | q.Secret))
51 | if err != nil {
52 | return
53 | }
54 | data = res.Bytes()
55 | return
56 | }
57 |
58 | func (q *Qy) getJsapiTicket() (data *zhttp.Res, err error) {
59 | var token string
60 | token, err = q.engine.GetAccessToken()
61 | if err != nil {
62 | return nil, err
63 | }
64 | return http.Post(fmt.Sprintf(
65 | "%s/cgi-bin/get_jsapi_ticket?access_token=%s",
66 | QyAPIURL, token))
67 | }
68 |
--------------------------------------------------------------------------------
/reply.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/sohaha/zlsgo/zstring"
10 | "github.com/sohaha/zlsgo/ztype"
11 | )
12 |
13 | type (
14 | ReplyNews []struct {
15 | PicUrl string
16 | Title string
17 | Description string
18 | Url string
19 | }
20 |
21 | Reply struct {
22 | openid string
23 | }
24 | CDATA struct {
25 | Value string `xml:",cdata"`
26 | }
27 | ReplySt struct {
28 | Content string `xml:"Content"`
29 | CreateTime uint64
30 | FromUserName string
31 | MsgId int
32 | MsgType string
33 | Event string
34 | ToUserName string
35 |
36 | MediaId string
37 | // image
38 | PicUrl string
39 |
40 | // voice
41 | Format string
42 | Recognition string
43 |
44 | // video or shortvideo
45 | ThumbMediaId string
46 |
47 | // location
48 | LocationX string `xml:"Location_X"`
49 | LocationY string `xml:"Location_Y"`
50 | Longitude string `xml:"Longitude"`
51 | Latitude string `xml:"Latitude"`
52 |
53 | Scale string
54 | Label string
55 |
56 | // link
57 | Title string
58 | Description string
59 | Url string
60 |
61 | // Qy
62 | AgentID string `xml:"AgentID"`
63 | isEncrypt bool
64 | receiverID string
65 | received *ReceivedSt
66 | }
67 | )
68 |
69 | type (
70 | ReceivedSt struct {
71 | echostr string
72 | data *ReplySt
73 | encrypt string
74 | isEncrypt bool
75 | signature string
76 | timestamp string
77 | nonce string
78 | bodyData []byte
79 | token string
80 | encodingAesKey string
81 | msgSignature string
82 | }
83 | )
84 |
85 | func (e *Engine) Reply(querys map[string]string,
86 | bodyData []byte) (received *ReceivedSt,
87 | err error) {
88 | received = &ReceivedSt{}
89 | received.echostr, _ = querys["echostr"]
90 | received.signature, _ = querys["signature"]
91 | received.timestamp, _ = querys["timestamp"]
92 | received.nonce, _ = querys["nonce"]
93 | received.encrypt, _ = querys["encrypt"]
94 | received.msgSignature, _ = querys["msg_signature"]
95 | received.token = e.GetToken()
96 | received.encodingAesKey = e.GetEncodingAesKey()
97 | received.isEncrypt = received.msgSignature != ""
98 | received.bodyData = bodyData
99 | return
100 | }
101 |
102 | func (r *ReceivedSt) Valid() (validMsg string, err error) {
103 | if r.isEncrypt {
104 | if r.msgSignature != sha1Signature(r.token, r.timestamp, r.nonce, r.echostr) {
105 | err = errors.New("decryption failed")
106 | return
107 | }
108 | var plaintext []byte
109 | plaintext, err = aesDecrypt(r.echostr, r.encodingAesKey)
110 | if err != nil {
111 | return
112 | }
113 | _, _, plaintext, _, err = parsePlainText(plaintext)
114 | if err != nil {
115 | return
116 | }
117 | validMsg = zstring.Bytes2String(plaintext)
118 | } else {
119 | if r.signature != sha1Signature(r.token, r.timestamp, r.nonce) {
120 | err = errors.New("decryption failed")
121 | return
122 | }
123 | validMsg = r.echostr
124 | }
125 | return
126 | }
127 |
128 | func (r *ReceivedSt) Data() (data *ReplySt, err error) {
129 | if r.data != nil {
130 | return r.data, nil
131 | }
132 | if r.isEncrypt {
133 | var arr ztype.Map
134 | arr, err = ParseXML2Map(r.bodyData)
135 | if err != nil {
136 | return
137 | }
138 | var plaintext []byte
139 | plaintext, err = aesDecrypt(arr.Get("Encrypt").String(), r.encodingAesKey)
140 | if err != nil {
141 | return
142 | }
143 | var receiverID []byte
144 | _, _, plaintext, receiverID, err = parsePlainText(plaintext)
145 | if err != nil {
146 | return
147 | }
148 |
149 | log.Debug(zstring.Bytes2String(plaintext))
150 | err = xml.Unmarshal(plaintext, &data)
151 | if err == nil {
152 | data.received = r
153 | data.isEncrypt = true
154 | data.receiverID = zstring.Bytes2String(receiverID)
155 | }
156 | } else {
157 | // log.Debug(zstring.Bytes2String(r.bodyData))
158 | err = xml.Unmarshal(r.bodyData, &data)
159 | }
160 | return
161 | }
162 |
163 | func (t *ReplySt) ReplyCustom(fn func(r *ReplySt) (xml string)) string {
164 | reply := t.encrypt(fn(t))
165 | return reply
166 | }
167 |
168 | func (t *ReplySt) encrypt(content string) string {
169 | var err error
170 | if t.isEncrypt {
171 | data := ztype.Map{}
172 | var encrypt []byte
173 | encrypt, err = aesEncrypt(MarshalPlainText(content, t.receiverID,
174 | zstring.Rand(16)),
175 | t.received.encodingAesKey)
176 | if err != nil {
177 | return ""
178 | }
179 | signature := sha1Signature(t.received.token, zstring.Bytes2String(encrypt), t.received.timestamp, t.received.nonce)
180 | data["Encrypt"] = zstring.Bytes2String(encrypt)
181 | data["MsgSignature"] = signature
182 | data["TimeStamp"] = t.received.timestamp
183 | data["Nonce"] = t.received.nonce
184 | reply, _ := FormatMap2XML(data)
185 | return reply
186 | }
187 | return content
188 | }
189 |
190 | func (t *ReplySt) ReplyText(content ...string) (reply string) {
191 | if len(content) == 0 {
192 | return "success"
193 | }
194 | data := ztype.Map{
195 | "Content": content[0],
196 | "CreateTime": strconv.FormatInt(time.Now().Unix(), 10),
197 | "ToUserName": t.FromUserName,
198 | "FromUserName": t.ToUserName,
199 | "MsgType": "text",
200 | }
201 | reply, _ = FormatMap2XML(data)
202 | reply = t.encrypt(reply)
203 | return
204 | }
205 |
206 | func (t *ReplySt) ReplyNews(items ReplyNews) (reply string) {
207 | if len(items) == 0 {
208 | return "success"
209 | }
210 | data := ztype.Map{
211 | "CreateTime": strconv.FormatInt(time.Now().Unix(), 10),
212 | "ToUserName": t.FromUserName,
213 | "FromUserName": t.ToUserName,
214 | "MsgType": "news",
215 | "ArticleCount": len(items),
216 | }
217 | articles := make(ztype.Maps, len(items))
218 | for i := range items {
219 | articles = append(articles, ztype.Map{
220 | "Title": items[i].Title,
221 | "Description": items[i].Description,
222 | "PicUrl": items[i].PicUrl,
223 | "Url": items[i].Url,
224 | })
225 | }
226 | data["Articles"] = articles
227 | reply, _ = FormatMap2XML(data)
228 | reply = t.encrypt(reply)
229 | return
230 | }
231 |
--------------------------------------------------------------------------------
/token.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "net/url"
6 | "time"
7 |
8 | "github.com/sohaha/zlsgo/zjson"
9 | "github.com/sohaha/zlsgo/znet"
10 | "github.com/sohaha/zlsgo/zstring"
11 | )
12 |
13 | // AccessToken accessToken
14 | type AccessToken struct {
15 | AccessToken string `json:"access_token"`
16 | ExpiresIn int `json:"expires_in"`
17 | }
18 |
19 | type ScopeType string
20 |
21 | const (
22 | ScopeBase ScopeType = "snsapi_base"
23 | ScopeUserinfo ScopeType = "snsapi_userinfo"
24 | // ScopePrivateinfo 企业微信需要使用这个才能拿到用户的基本信息
25 | ScopePrivateinfo ScopeType = "snsapi_privateinfo"
26 | )
27 |
28 | func (e *Engine) GetAccessTokenExpiresInCountdown() float64 {
29 | data, err := e.cache.GetT(cacheToken)
30 | if err != nil {
31 | return 0
32 | }
33 | return data.RemainingLife().Seconds()
34 | }
35 |
36 | // 设置 AccessToken
37 | func (e *Engine) SetAccessToken(accessToken string, expiresIn uint) error {
38 | e.cache.Set(cacheToken, accessToken, expiresIn-60)
39 | return nil
40 | }
41 |
42 | // 获取 AccessToken
43 | func (e *Engine) GetAccessToken() (string, error) {
44 | data, err := e.cache.MustGet(cacheToken, func(set func(data interface{},
45 | lifeSpan time.Duration, interval ...bool)) (err error) {
46 | var res []byte
47 | res, err = e.config.getAccessToken()
48 | if err != nil {
49 | return
50 | }
51 | var json *zjson.Res
52 | json, err = CheckResError(res)
53 | if err != nil {
54 | return err
55 | }
56 | accessToken := json.Get("access_token").String()
57 | if accessToken == "" {
58 | accessToken = json.Get("authorizer_access_token").String()
59 | }
60 | if accessToken == "" {
61 | return errors.New("access_token parsing failed")
62 | }
63 | set(accessToken, time.Duration(json.Get("expires_in").Int()-200)*time.Second)
64 | return nil
65 | })
66 |
67 | if err != nil {
68 | return "", err
69 | }
70 |
71 | return data.(string), nil
72 | }
73 |
74 | // Auth 用户授权
75 | func (e *Engine) Auth(c *znet.Context, state string, scope ScopeType) (*zjson.Res, bool, error) {
76 | code := e.authCode(c, state, scope, "", "")
77 | if len(code) == 0 {
78 | return nil, false, nil
79 | }
80 | json, err := e.GetAuthInfo(code)
81 | if err != nil {
82 | if httpErr, ok := err.(httpError); ok {
83 | switch httpErr.Code {
84 | case 41008, 40029, 40163:
85 | if len(e.authCode(c, state, scope, "", code)) == 0 {
86 | return nil, false, nil
87 | }
88 | }
89 | }
90 | }
91 | return json, true, err
92 | }
93 |
94 | func (e *Engine) authCode(c *znet.Context, state string, scope ScopeType, uri, oldCode string) string {
95 | code, _ := c.GetQuery("code")
96 | if len(code) > 0 && code != oldCode {
97 | return code
98 | }
99 |
100 | if len(uri) == 0 {
101 | if len(e.redirectDomain) > 0 {
102 | uri = e.redirectDomain + c.Request.URL.String()
103 | } else {
104 | uri = c.Host(true)
105 | }
106 | }
107 |
108 | c.Redirect(e.getOauthRedirect(paramFilter(uri), state, scope))
109 | c.Abort()
110 | return ""
111 | }
112 |
113 | func (e *Engine) getOauthRedirect(callback string, state string, scope ScopeType) string {
114 | if len(scope) == 0 {
115 | scope = "snsapi_userinfo"
116 | }
117 | u := zstring.Buffer(10)
118 | if e.IsQy() {
119 | u.WriteString("https://open.weixin.qq.com/connect/oauth2/authorize?appid=")
120 | u.WriteString(e.GetAppID())
121 | u.WriteString("&agentid=")
122 | conf := e.config.(*Qy)
123 | u.WriteString(conf.AgentID)
124 | } else {
125 | u.WriteString(openURL)
126 | u.WriteString("/connect/oauth2/authorize?appid=")
127 | u.WriteString(e.GetAppID())
128 | }
129 |
130 | u.WriteString("&redirect_uri=")
131 | u.WriteString(url.QueryEscape(callback))
132 | u.WriteString("&response_type=code&scope=")
133 | u.WriteString(string(scope))
134 | u.WriteString("&state=")
135 | u.WriteString(state)
136 | u.WriteString("#wechat_redirect")
137 |
138 | return u.String()
139 | }
140 |
141 | func (e *Engine) GetAuthInfo(authCode string) (*zjson.Res, error) {
142 | u := zstring.Buffer(3)
143 | u.WriteString(e.apiURL)
144 |
145 | appid := e.config.GetAppID()
146 | switch true {
147 | case e.IsWeapp():
148 | return (e.config.(*Weapp)).GetSessionKey(authCode, "authorization_code")
149 | case e.IsQy():
150 | u.WriteString("/cgi-bin/user/getuserinfo?access_token=")
151 | token, err := e.GetAccessToken()
152 | if err != nil {
153 | return nil, err
154 | }
155 | u.WriteString(token)
156 | u.WriteString("&code=")
157 | u.WriteString(authCode)
158 | json, err := httpResProcess(http.Post(u.String()))
159 | if err == nil {
160 | openid := json.Get("OpenId").String()
161 | j, err := zjson.Set(json.String(), "openid", openid)
162 | if err == nil {
163 | accessToken, _ := e.GetAccessToken()
164 | j, _ = zjson.Set(j, "access_token", accessToken)
165 | njson := zjson.Parse(j)
166 | return njson, nil
167 | }
168 | }
169 | return json, err
170 | case e.IsOpen():
171 | return nil, errors.New("not support")
172 | default:
173 | u.WriteString("/sns/oauth2/")
174 | u.WriteString("access_token?appid=")
175 | u.WriteString(appid)
176 | u.WriteString("&secret=")
177 | u.WriteString(e.config.GetSecret())
178 | u.WriteString("&code=")
179 | u.WriteString(authCode)
180 | u.WriteString("&grant_type=authorization_code")
181 | }
182 |
183 | return httpResProcess(http.Post(u.String()))
184 |
185 | }
186 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha1"
6 | "encoding/hex"
7 | "encoding/xml"
8 | "errors"
9 | "fmt"
10 | "io"
11 | netHttp "net/http"
12 | "net/url"
13 | "sort"
14 | "strconv"
15 | "strings"
16 |
17 | "github.com/sohaha/zlsgo/zhttp"
18 | "github.com/sohaha/zlsgo/zjson"
19 | "github.com/sohaha/zlsgo/zstring"
20 | "github.com/sohaha/zlsgo/ztype"
21 | "github.com/sohaha/zlsgo/zutil"
22 | )
23 |
24 | type (
25 | // SendData Send Data
26 | SendData ztype.Map
27 | request struct {
28 | request *netHttp.Request
29 | rawData []byte
30 | }
31 | )
32 |
33 | func transformSendData(v []interface{}) []interface{} {
34 | for i := range v {
35 | switch val := v[i].(type) {
36 | case string:
37 | v[i] = val
38 | case map[string]string, SendData, map[string]interface{}, ztype.Map:
39 | v[i] = zhttp.BodyJSON(val)
40 | }
41 | }
42 | return v
43 | }
44 |
45 | func recurveFormatMap2XML(buf io.Writer, m ztype.Map) {
46 | for k := range m {
47 | _, _ = io.WriteString(buf, fmt.Sprintf("<%s>", k))
48 | v := m.Get(k)
49 | switch val := v.Value().(type) {
50 | case ztype.Maps:
51 | io.WriteString(buf, "- ")
52 | for _, vs := range val {
53 | recurveFormatMap2XML(buf, vs)
54 | }
55 | io.WriteString(buf, "
")
56 | case ztype.Map:
57 | recurveFormatMap2XML(buf, val)
58 | default:
59 | if err := xml.EscapeText(buf, zstring.String2Bytes(m.Get(k).String())); err != nil {
60 | return
61 | }
62 | }
63 | _, _ = io.WriteString(buf, fmt.Sprintf("%s>", k))
64 | }
65 |
66 | }
67 |
68 | func FormatMap2XML(m ztype.Map) (string, error) {
69 | buf := zutil.GetBuff()
70 | defer zutil.PutBuff(buf)
71 | if _, err := io.WriteString(buf, ""); err != nil {
72 | return "", err
73 | }
74 | for k, v := range m {
75 | if _, err := io.WriteString(buf, fmt.Sprintf("<%s>", k)); err != nil {
76 | return "", err
77 | }
78 | switch val := v.(type) {
79 | case ztype.Map:
80 | recurveFormatMap2XML(buf, val)
81 | case ztype.Maps:
82 | io.WriteString(buf, "- ")
83 | for _, vs := range val {
84 | recurveFormatMap2XML(buf, vs)
85 | }
86 | io.WriteString(buf, "
")
87 | default:
88 | if err := xml.EscapeText(buf, zstring.String2Bytes(ztype.ToString(val))); err != nil {
89 | return "", err
90 | }
91 | }
92 | _, _ = io.WriteString(buf, fmt.Sprintf("%s>", k))
93 | }
94 | _, _ = io.WriteString(buf, "")
95 | return buf.String(), nil
96 | }
97 |
98 | // ParseXML2Map parse xml to map
99 | func ParseXML2Map(b []byte) (m ztype.Map, err error) {
100 | if len(b) == 0 {
101 | return nil, errors.New("xml data is empty")
102 | }
103 | var (
104 | d = xml.NewDecoder(bytes.NewReader(b))
105 | depth = 0
106 | tk xml.Token
107 | key string
108 | buf bytes.Buffer
109 | )
110 | m = ztype.Map{}
111 | for {
112 | tk, err = d.Token()
113 | if err != nil {
114 | if err == io.EOF {
115 | err = nil
116 | return
117 | }
118 | return
119 | }
120 | switch v := tk.(type) {
121 | case xml.StartElement:
122 | depth++
123 | switch depth {
124 | case 2:
125 | key = v.Name.Local
126 | buf.Reset()
127 | case 3:
128 | if err = d.Skip(); err != nil {
129 | return
130 | }
131 | depth--
132 | key = "" // key == "" indicates that the node with depth==2 has children
133 | }
134 | case xml.CharData:
135 | if depth == 2 && key != "" {
136 | buf.Write(v)
137 | }
138 | case xml.EndElement:
139 | if depth == 2 && key != "" {
140 | m[key] = buf.String()
141 | }
142 | depth--
143 | }
144 | }
145 | }
146 |
147 | // CheckResError CheckResError
148 | func CheckResError(v []byte) (*zjson.Res, error) {
149 | data := zjson.ParseBytes(v)
150 | code := data.Get("errcode").Int()
151 | if code != 0 {
152 | errmsg := data.Get("errmsg").String()
153 | if errmsg == "" {
154 | return &zjson.Res{}, httpError{Code: code, Msg: "errcode: " + strconv.Itoa(code)}
155 | }
156 | return &zjson.Res{}, httpError{Code: code, Msg: errmsg}
157 | }
158 | return data, nil
159 | }
160 |
161 | func paramFilter(uri string) string {
162 | if u, err := url.Parse(uri); err == nil {
163 | querys := u.Query()
164 | for k := range querys {
165 | if k == "code" || k == "state" || k == "scope" {
166 | delete(querys, k)
167 | }
168 | }
169 | u.RawQuery = querys.Encode()
170 | uri = u.String()
171 | }
172 | return uri
173 | }
174 |
175 | func sortParam(v map[string]interface{}, key string) string {
176 | l := len(v)
177 | keys := make([]string, 0, l)
178 | for k := range v {
179 | keys = append(keys, k)
180 | }
181 | sort.Strings(keys)
182 | b := zstring.Buffer(l * 3)
183 | for i := range keys {
184 | k := keys[i]
185 | s := ztype.ToString(v[k])
186 | if len(s) == 0 {
187 | continue
188 | }
189 | if i > 0 {
190 | b.WriteString("&")
191 | }
192 | b.WriteString(k)
193 | b.WriteString("=")
194 | b.WriteString(s)
195 | }
196 | return b.String() + "&key=" + key
197 | }
198 |
199 | func signParam(v string, signType, key string) string {
200 | switch strings.ToUpper(signType) {
201 | case "SHA1":
202 | b := sha1.Sum(zstring.String2Bytes(v))
203 | return hex.EncodeToString(b[:])
204 | default:
205 | // MD5
206 | return strings.ToUpper(zstring.Md5(v))
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/utils_test.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/sohaha/zlsgo"
7 | )
8 |
9 | func TestUtilsParamFlter(t *testing.T) {
10 | tt := zlsgo.NewTest(t)
11 | for r, w := range map[string]string{
12 | "https://api.weixin.qq.com/?code=1&code=2&test=3": "https://api.weixin.qq.com/?test=3",
13 | } {
14 | tt.Equal(w, paramFilter(r))
15 | }
16 | }
17 |
18 | func TestUtilsKsortParam(t *testing.T) {
19 | tt := zlsgo.NewTest(t)
20 | for r, w := range map[string]map[string]interface{}{
21 | "a=1&b=dd&er=6&z=222&key=999": {
22 | "a": 1,
23 | "b": "dd",
24 | "z": 222,
25 | "er": 6,
26 | },
27 | } {
28 | param := sortParam(w, "999")
29 | tt.Equal(r, param)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/weapp.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/sha1"
7 | "encoding/base64"
8 | "fmt"
9 |
10 | "github.com/sohaha/zlsgo/zhttp"
11 | "github.com/sohaha/zlsgo/zjson"
12 | "github.com/sohaha/zlsgo/zstring"
13 | )
14 |
15 | type (
16 | Weapp struct {
17 | AppID string
18 | AppSecret string
19 | EncodingAesKey string
20 | Token string
21 | engine *Engine
22 | }
23 | )
24 |
25 | var _ Cfg = new(Weapp)
26 |
27 | func (m *Weapp) setEngine(engine *Engine) {
28 | m.engine = engine
29 | }
30 |
31 | func (m *Weapp) getEngine() *Engine {
32 | return m.engine
33 | }
34 |
35 | func (m *Weapp) GetAppID() string {
36 | return m.AppID
37 | }
38 |
39 | func (m *Weapp) GetSecret() string {
40 | return m.AppSecret
41 | }
42 |
43 | func (m *Weapp) GetToken() string {
44 | return m.Token
45 | }
46 |
47 | func (m *Weapp) GetEncodingAesKey() string {
48 | return m.EncodingAesKey
49 | }
50 |
51 | func (m *Weapp) getAccessToken() (data []byte, err error) {
52 | res, err := http.Post(fmt.Sprintf(
53 | "%s/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
54 | APIURL, m.AppID, m.AppSecret))
55 | if err != nil {
56 | return
57 | }
58 | data = res.Bytes()
59 | return
60 | }
61 |
62 | func (m *Weapp) getJsapiTicket() (data *zhttp.Res, err error) {
63 | var token string
64 | token, err = m.engine.GetAccessToken()
65 | if err != nil {
66 | return nil, err
67 | }
68 | return http.Post(fmt.Sprintf(
69 | "%s/cgi-bin/ticket/getticket?&type=jsapi&access_token=%s",
70 | APIURL, token))
71 | }
72 |
73 | func (m *Weapp) GetSessionKey(code, grantType string) (data *zjson.Res, err error) {
74 | u := zstring.Buffer(9)
75 | u.WriteString(APIURL)
76 | u.WriteString("/sns/jscode2session?appid=")
77 | u.WriteString(m.AppID)
78 | u.WriteString("&secret=")
79 | u.WriteString(m.AppSecret)
80 | u.WriteString("&js_code=")
81 | u.WriteString(code)
82 | u.WriteString("&grant_type=")
83 | u.WriteString(grantType)
84 | return httpResProcess(http.Get(u.String()))
85 | }
86 |
87 | func (m *Weapp) Decrypt(seesionKey, iv, encryptedData string) (string, error) {
88 | byts := make([][]byte, 0, 3)
89 | for _, v := range []string{seesionKey, iv, encryptedData} {
90 | b, err := base64.StdEncoding.DecodeString(v)
91 | if err != nil {
92 | return "", err
93 | }
94 | byts = append(byts, b)
95 | }
96 | aesKey := byts[0]
97 | ivRaw := byts[1]
98 | cipherData := byts[2]
99 | block, _ := aes.NewCipher(aesKey)
100 | blockSize := block.BlockSize()
101 | blockMode := cipher.NewCBCDecrypter(block, ivRaw)
102 | plaintext := make([]byte, len(cipherData))
103 | blockMode.CryptBlocks(plaintext, cipherData)
104 | plaintext = PKCS7UnPadding(plaintext, blockSize)
105 | return zstring.Bytes2String(plaintext), nil
106 | }
107 |
108 | func (m *Weapp) Verify(seesionKey, rawData, signature string) bool {
109 | h := sha1.New()
110 | h.Write(zstring.String2Bytes(rawData))
111 | h.Write(zstring.String2Bytes(seesionKey))
112 | sign := fmt.Sprintf("%x", h.Sum(nil))
113 | return sign == signature
114 | }
115 |
--------------------------------------------------------------------------------
/web.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "github.com/sohaha/zlsgo/znet"
8 | "github.com/sohaha/zlsgo/ztype"
9 | )
10 |
11 | type RouterOption struct {
12 | Prefix string
13 | JsapiTicketCallback func(*znet.Context, ztype.Map, error)
14 | }
15 |
16 | func (w *Engine) Router(r *znet.Engine, opt RouterOption) {
17 | opt.Prefix = strings.TrimRight(opt.Prefix, "/")
18 |
19 | r.GET(opt.Prefix+"/js_ticket", func(c *znet.Context) {
20 | opt.JsapiTicketCallback(getJsapiTicket(w, c))
21 | })
22 | }
23 |
24 | func getJsapiTicket(wx *Engine, c *znet.Context) (*znet.Context, ztype.Map, error) {
25 | jsapiTicket, err := wx.GetJsapiTicket()
26 | if err != nil {
27 | return c, nil, errors.New(ErrorMsg(err))
28 | }
29 | url := c.Host(true)
30 | jsSign, err := wx.GetJsSign(url)
31 | if err != nil {
32 | return c, nil, errors.New(ErrorMsg(err))
33 | }
34 |
35 | return c, map[string]interface{}{
36 | "jsapiTicket": jsapiTicket,
37 | "jsSign": jsSign,
38 | "url": url,
39 | }, nil
40 | }
41 |
--------------------------------------------------------------------------------
/wechat.go:
--------------------------------------------------------------------------------
1 | package wechat
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "os"
7 | "strings"
8 | "time"
9 |
10 | "github.com/sohaha/zlsgo/zcache"
11 | "github.com/sohaha/zlsgo/zfile"
12 | "github.com/sohaha/zlsgo/zhttp"
13 | "github.com/sohaha/zlsgo/zjson"
14 | "github.com/sohaha/zlsgo/zlog"
15 | "github.com/sohaha/zlsgo/zstring"
16 | )
17 |
18 | type (
19 | // Cfg 微信配置
20 | Cfg interface {
21 | GetAppID() string
22 | GetToken() string
23 | GetEncodingAesKey() string
24 | getEngine() *Engine
25 | setEngine(*Engine)
26 | GetSecret() string
27 | getAccessToken() (data []byte, err error)
28 | getJsapiTicket() (data *zhttp.Res, err error)
29 | }
30 |
31 | Engine struct {
32 | config Cfg
33 | cache *zcache.Table
34 | action string
35 | apiURL string
36 | redirectDomain string
37 | }
38 | )
39 |
40 | const (
41 | APIURL = "https://api.weixin.qq.com"
42 | QyAPIURL = "https://qyapi.weixin.qq.com"
43 | openURL = "https://open.weixin.qq.com"
44 | cachePrtfix = "go_wechat_"
45 | cacheToken = "Token"
46 | cacheJsapiTicket = "JsapiTicket"
47 | cacheComponentVerifyTicket = "componentVerifyTicket"
48 | )
49 |
50 | var (
51 | log = zlog.New("[Wx] ")
52 | apps = map[string]string{}
53 | cacheData []byte
54 | )
55 |
56 | func init() {
57 | log.ResetFlags(zlog.BitLevel | zlog.BitTime)
58 | Debug(false)
59 | }
60 |
61 | // Debug 调试模式
62 | func Debug(disable ...bool) {
63 | state := true
64 | if len(disable) > 0 {
65 | state = disable[0]
66 | }
67 | if state {
68 | log.SetLogLevel(zlog.LogDump)
69 | } else {
70 | log.SetLogLevel(zlog.LogWarn)
71 | }
72 |
73 | }
74 |
75 | // LoadCacheData 加载缓存文件
76 | func LoadCacheData(path string) (err error) {
77 | var f os.FileInfo
78 | path = zfile.RealPath(path)
79 | f, err = os.Stat(path)
80 | if err != nil || f.IsDir() {
81 | return errors.New("file does not exist")
82 | }
83 | var data []byte
84 | var now = time.Now().Unix()
85 | data, _ = ioutil.ReadFile(path)
86 | cacheData = data
87 | zjson.ParseBytes(data).ForEach(func(key, value *zjson.Res) bool {
88 | k := strings.Split(key.String(), "|")
89 | if len(k) < 2 || (k[0] == "" || k[1] == "") {
90 | return true
91 | }
92 | cacheName := cachePrtfix + k[1] + k[0]
93 | cache := zcache.New(cacheName)
94 | apps[k[0]] = k[1]
95 | value.ForEach(func(key, value *zjson.Res) bool {
96 | cachekey := key.String()
97 | log.Debug("载入缓存", cacheName, cachekey)
98 | switch cachekey {
99 | default:
100 | var t uint = 0
101 | lifespan := isSetCache(value, now)
102 | if lifespan == 0 {
103 | return true
104 | }
105 | t = uint(lifespan)
106 | cache.Set(cachekey, value.Get("content").String(), t)
107 | }
108 | return true
109 | })
110 | return true
111 | })
112 | return nil
113 | }
114 |
115 | func isSetCache(value *zjson.Res, now int64) (diffTime int) {
116 | saveTime := value.Get("SaveTime").Int()
117 | outTime := value.Get("OutTime").Int()
118 | diffTime = outTime - (int(now) - saveTime)
119 | return
120 | }
121 |
122 | // SaveCacheData 保存缓存数据
123 | func SaveCacheData(path string) (json string, err error) {
124 | var file *os.File
125 | json = "{}"
126 | path = zfile.RealPath(path)
127 | if zfile.FileExist(path) {
128 | file, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
129 | content, err := ioutil.ReadAll(file)
130 | if err == nil && zjson.ValidBytes(content) {
131 | json = zstring.Bytes2String(content)
132 | }
133 | } else {
134 | file, err = os.Create(path)
135 | }
136 | if err != nil {
137 | return
138 | }
139 | defer file.Close()
140 | now := time.Now().Unix()
141 | for k, v := range apps {
142 | log.Debug("SaveCacheData: ", cachePrtfix+v+k)
143 | cache := zcache.New(cachePrtfix + v + k)
144 | cache.ForEachRaw(func(key string, value *zcache.Item) bool {
145 | title := k + "\\|" + v
146 | log.Debug(title, key)
147 | if str, ok := value.Data().(string); ok {
148 | json, _ = zjson.Set(json, title+"."+key+".content", str)
149 | } else {
150 | json, _ = zjson.Set(json, title+"."+key, value.Data())
151 | }
152 | json, _ = zjson.Set(json, title+"."+key+".SaveTime", now)
153 | json, _ = zjson.Set(json, title+"."+key+".OutTime", value.RemainingLife().Seconds())
154 |
155 | return true
156 | })
157 | }
158 | saveData := zstring.String2Bytes(json)
159 | if len(saveData) == 2 && len(cacheData) > 2 {
160 | return
161 | }
162 | _, err = file.Write(saveData)
163 |
164 | return
165 | }
166 |
167 | // New 初始一个实例
168 | func New(c Cfg) *Engine {
169 | cacheFileOnce.Do(func() {
170 | isAutoCache := len(CacheFile) > 0
171 | if isAutoCache {
172 | _ = LoadCacheData(CacheFile)
173 | go func() {
174 | for {
175 | time.Sleep(CacheTime)
176 | if len(CacheFile) > 0 {
177 | _, _ = SaveCacheData(CacheFile)
178 | }
179 | }
180 | }()
181 | }
182 | })
183 |
184 | appid, action, apiURL := c.GetAppID(), "", APIURL
185 | switch c.(type) {
186 | case *Open:
187 | action = "open"
188 | case *Qy:
189 | action = "qy"
190 | apiURL = QyAPIURL
191 | case *Mp:
192 | action = "mp"
193 | case *Weapp:
194 | action = "weapp"
195 | }
196 | engine := &Engine{
197 | cache: zcache.New(cachePrtfix + action + appid),
198 | config: c,
199 | action: action,
200 | apiURL: apiURL,
201 | }
202 | c.setEngine(engine)
203 | apps[appid] = action
204 | return engine
205 | }
206 |
207 | // GetAction 获取实例
208 | func (e *Engine) GetAction() Cfg {
209 | return e.config
210 | }
211 |
212 | // GetAppID 获取 Appid
213 | func (e *Engine) GetAppID() string {
214 | return e.config.GetAppID()
215 | }
216 |
217 | // GetSecret 获取密钥
218 | func (e *Engine) GetSecret() string {
219 | return e.config.GetSecret()
220 | }
221 |
222 | // IsMp 是否公众号
223 | func (e *Engine) IsMp() bool {
224 | return e.action == "mp"
225 | }
226 |
227 | // IsQy 是否企业微信
228 | func (e *Engine) IsQy() bool {
229 | return e.action == "qy"
230 | }
231 |
232 | // IsOpen 是否开放平台
233 | func (e *Engine) IsOpen() bool {
234 | return e.action == "open"
235 | }
236 |
237 | // IsWeapp 是否小程序
238 | func (e *Engine) IsWeapp() bool {
239 | return e.action == "weapp"
240 | }
241 |
242 | // GetToken 获取 Token
243 | func (e *Engine) GetToken() string {
244 | return e.config.GetToken()
245 | }
246 |
247 | // GetEncodingAesKey 获取 Aes Key
248 | func (e *Engine) GetEncodingAesKey() string {
249 | return e.config.GetEncodingAesKey()
250 | }
251 |
--------------------------------------------------------------------------------
/wechat_test.go:
--------------------------------------------------------------------------------
1 | package wechat_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/zlsgo/wechat"
7 | )
8 |
9 | func TestWechat(t *testing.T) {
10 | wx := wechat.New(&wechat.Mp{
11 | AppID: "wx9d1fcb71007a71b0",
12 | AppSecret: "c4132441ded3301bda2d2373609959e1",
13 | })
14 | t.Log(wx.GetAccessToken())
15 | }
16 |
17 | func TestApi(t *testing.T) {
18 |
19 | wx := wechat.New(&wechat.Mp{
20 | AppID: "wx9d1fcb71007a71b0",
21 | AppSecret: "c4132441ded3301bda2d2373609959e1",
22 | })
23 |
24 | res, err := wx.HttpAccessTokenPost("https://api.weixin.qq.com/cgi-bin/shorturl", map[string]string{
25 | "action": "long2short",
26 | "long_url": "https://api.weixin.qq.com",
27 | })
28 | if err != nil {
29 | t.Fatal(wechat.ErrorMsg(err))
30 | }
31 |
32 | t.Log(res.String())
33 |
34 | res, err = wx.HttpAccessTokenPost("https://api.weixin.qq.com/cgi-bin/shorturl", map[string]string{
35 | "action": "long2short",
36 | })
37 | if err == nil {
38 | t.Fail()
39 |
40 | }
41 | t.Log(wechat.ErrorMsg(err))
42 | t.Log(res.String())
43 | }
44 |
--------------------------------------------------------------------------------