├── 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("", 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("", 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 | --------------------------------------------------------------------------------