├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── http_client.go ├── http_client_test.go ├── mch.go ├── mch_api ├── constant.go └── structs.go ├── mch_api_v3 ├── base.go ├── constant.go ├── other.go ├── partner_applyment.go ├── partner_base.go └── profit_sharing.go ├── mch_req.go ├── mch_req_v3.go ├── mch_test.go ├── mch_v3_cert.go ├── mch_v3_test.go ├── mp.go ├── mp_api ├── account.go ├── basic_information.go ├── constant.go ├── custom_menus.go ├── guide.go ├── media.go ├── message.go ├── mini_program.go ├── oa_web_apps.go ├── ocr.go ├── structs.go └── user.go ├── mp_req.go ├── mp_test.go ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .idea/ 14 | 15 | *.swp 16 | 17 | mch_v3_test.go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 jf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat 2 | 3 | wechat weixin sdk,支持微信应用和商户。 4 | 5 | ## 设计目标 6 | 在概念清晰的基础上追求更少的编码、更开放、灵活的结构。 7 | 8 | 本库不是在微信官方API基础上进一步封装,造出一个新的框架级的重体量SDK。而是努力成为微信官方文档的Golang版快速工具箱。 9 | 10 | 努力让开发者在学习微信官方文档后,不再有新的学习曲线(另学一套)! 11 | 12 | 所以本库目标是:极致、简单!不创另行发明新理念、不另行创造新架构! 13 | 14 | ## 概述 15 | 根据微信的文档,微信的业务有两个不同的领域: 16 | - 应用类账号下的Api 17 | - 商户类账号下的Api 18 | 19 | 遵守**高内聚,低耦合**的设计哲学。 20 | 21 | 所有同类型的请求,都能复用,封装聚合。其它零散的功能都封装成独立函数,按需调用。 22 | 23 | 所有的接口、请求数据、请求结果数据,都是开放的;可以使用我提供的数据,也能自行设计。 24 | 25 | ## 安装 26 | go get github.com/blusewang/wx 27 | 28 | # 应用账号API 29 | `订阅号`、`服务号`、`小程序`、`App` 30 | - [x] 支持连接不同的地区的微信服务器 31 | - [x] 支持一行代码从被动消息的 http.Request 中安全取出消息成`MessageData`。内部实现了识别并解密消息、校验请求的`Query`数据。 32 | - [x] 支持自动填充`Query`中的`access_token`数据。 33 | - [x] 链式调用,让不同需求的业务能一气和成! 34 | 35 | ## 时效性凭证安置方式约定 36 | `access_token`、`js_sdk_ticket` 这类需要每7200秒刷新一次的,放到`crontab`中。 37 | 38 | 对此不满的,完全可以在使用本库的基础上,采用自己熟悉的方式、甚至自己设计方案来替代`crontab`。 39 | 40 | ## 程序设计 41 | ### 对象设计 42 | 一个基础账号对象`MpAccount`,它有三个行为: 43 | - 为微信H5的网址签名 `UrlSign(url string)` 44 | - 读取被动消息通知 `ReadMessage(req *http.Request)` 45 | - 主动发出请求 `NewMpReq(path mp_api.MpApi) *mpReq` 46 | 47 | ### 数据设计 48 | - 常量:[constant.go](https://github.com/blusewang/wx/blob/master/mp_api/constant.go) 49 | - 基础信息:[basic_information.go](https://github.com/blusewang/wx/blob/master/mp_api/basic_information.go) 50 | - 自定义菜单:[custom_menus.go](https://github.com/wx/wechat/blob/master/mp_api/custom_menus.go) 51 | - 消息:[message.go](https://github.com/blusewang/wx/blob/master/mp_api/message.go) 52 | - 媒体文件上传:[media.go](https://github.com/blusewang/wx/blob/master/mp_api/media.go) 53 | - 微信网页开发:[oa_web_apps.go](https://github.com/blusewang/wx/blob/master/mp_api/oa_web_apps.go) 54 | - 用户管理:[user.go](https://github.com/blusewang/wx/blob/master/mp_api/user.go) 55 | - 账号管理:[account.go](https://github.com/blusewang/wx/blob/master/mp_api/account.go) 56 | - 对话能力:[guide.go](https://github.com/blusewang/wx/blob/master/mp_api/guide.go) 57 | - 小程序:[mini_program.go](https://github.com/blusewang/wx/blob/master/mp_api/mini_program.go) 58 | 59 | ### 说明 60 | 只实现了很有限的数据。若需要使用本库自带的数据结构之外的API。完全可以参考本库的数据结构写法,自行另起书写(注意不同业务的tag名称不同)。 61 | 并能得到一样的兼容体验! 62 | 63 | ## 举例 64 | ```go 65 | a := wx.MpAccount{ 66 | AppId: "your_app_id", 67 | AccessToken: "38_XtyPcVUODHd8q3TNYPVGAZ2WNRx_nW4gnclObbv78tsEa1Y_bwdkLALDMEb4372wYqcC_CanjU9O0Zw4MqHiqxrIukk_G4ElAUxyv_ASOb0V2y8647cbxbYU-G8CbtnPdLNub8NrqtUVrSTnWAPaAGALPE", 68 | // ... 69 | ServerHost: mp_api.ServerHostShangHai, // 选择离自己最近的服务主机 70 | } 71 | 72 | // 一个简单的只带access_token的GET API 73 | var list mp_api.MessageCustomServiceKfListRes 74 | if err := a.NewMpReq(mp_api.MessageCustomServiceKfList).Bind(&list).Do(); err != nil { 75 | t.Error(err) 76 | } 77 | log.Println(list) 78 | 79 | // 一个POST API 80 | var rs mp_api.AccountShortUrlRes 81 | err = a.NewMpReq(mp_api.AccountShortUrl).SendData(mp_api.AccountShortUrlData{ 82 | Action: mp_api.ShortUrlAction, 83 | LongUrl: "https://developers.weixin.qq.com/doc/offiaccount/Account_Management/URL_Shortener.html", 84 | }).Bind(&rs).Do() 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | log.Println(rs) 89 | 90 | // 一个上传媒体文件的API 91 | err = a.NewMpReq(mp_api.MessageCustomServiceKfAccountUploadHeadImg).Query(mp_api.MessageCustomServiceKfAccountUploadHeadImgQuery{ 92 | KfAccount: "1@1", 93 | }).Upload(resp.Body, "png") 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | ``` 98 | 99 | # 商户账号API(V2版) 100 | `App、JSAPI、小程序下单` `分账` `付款至微信零钱` `付款至个人银行卡` `发红包` 101 | - [x] 自动填充基础信息 102 | - [x] 自动签名 103 | - [x] 私有证书HTTP客户端自动缓存 104 | - [x] 支持`MD5`、`HMAC-SHA256`加密 105 | - [x] 支持付款至银行卡时,隐私信息加密 106 | 107 | ## 程序设计 108 | ### 对象设计 109 | 一个基础账号对象`MchAccount`,它有以下适用于V2的行为: 110 | - 创建请求 `NewMchReq(url string)` 111 | - 将订单签名给App `OrderSign4App(or mch_api.PayUnifiedOrderRes)` 112 | - 将订单签名给于H5、小程序 `OrderSign(or mch_api.PayUnifiedOrderRes)` 113 | - 验证支付成功通知 `PayNotify(pn mch_api.PayNotify)` 114 | - 付款至银行卡时,隐私信息项加密 `RsaEncrypt(plain string)` 115 | 116 | ### 数据设计 117 | - 常量:[constant.go](https://github.com/blusewang/wx/blob/master/mch_api/constant.go) 118 | - 数据结构:[structs.go](https://github.com/blusewang/wx/blob/master/mch_api/structs.go) 119 | 120 | ### 说明 121 | 只实现了很有限的数据。若需要使用本库自带的数据结构之外的API。完全可以参考本库的数据结构写法,自行另起书写(建议参考structs.go中的方式书写)。 122 | 能得到一样的兼容体验! 123 | 124 | ## 举例 125 | ```go 126 | mch := wx.MchAccount{} 127 | 128 | var data mch_api.PayProfitSharingRes 129 | var body = mch_api.PayProfitSharingData{ 130 | TransactionId: "4200000531202004307536721907", 131 | OutOrderNo: "TSF_216144_1065_ye7DvHdSed", 132 | } 133 | _ = body.SerReceivers([]mch_api.PayProfitSharingReceiver{ 134 | { 135 | Type: "", 136 | Account: "", 137 | Amount: 10, 138 | Description: "", 139 | }, 140 | }) 141 | 142 | err := mch.NewMchReq(mch_api.PayProfitSharing). 143 | Send(&body). // 注意:发送的数据需传指针,以便自动填充基础信息和签名 144 | UseHMacSign(). // 指定使用HMAC-SHA256 145 | UsePrivateCert(). // 指定使用私有证书通信 146 | Bind(&data).Do() // 传指针 147 | log.Println(err) 148 | log.Println(data) 149 | ``` 150 | 151 | # 商户账号API(V3版)(含服务商) 152 | `App、JSAPI、小程序等支付` `分账` `付款至微信零钱` `付款至个人银行卡` `服务商系列接口` 153 | - [x] 支持付款至银行卡时,隐私信息加密 154 | - [x] 自动签名 155 | - [x] 自动下载微信支付官方证书 156 | 157 | ## 程序设计 158 | ### 对象设计 159 | 一个基础账号对象`MchAccount`,它有以下适用于V3的行为: 160 | - 创建请求 `NewMchReqV3(api mch_api_v3.MchApiV3)` 161 | - 获取微信支付官方证书 `LoadV3Cert()` 162 | - AEAD_AES_256_GCM 解密 `DecryptAES256GCM(nonce, associatedData, ciphertext string)` 163 | - V3版通用签名 `SignBaseV3(message string)` 164 | 165 | ### 数据设计 166 | - 常量:[constant.go](https://github.com/blusewang/wx/blob/master/mch_api_v3/constant.go) 167 | - 基础支付业务数据结构:[base.go](https://github.com/blusewang/wx/blob/master/mch_api_v3/base.go) 168 | - 服务商基础支付业务数据结构:[base.go](https://github.com/blusewang/wx/blob/master/mch_api_v3/partner_base.go) 169 | 170 | ### 说明 171 | 如果程序长时间运行,需要给`DownloadV3Cert()`做一个定时更新的任务。按自己的需求自行实现。 172 | 如果不实现,`DownloadV3Cert()`只会在第一次操作V3接口时获取一次。 173 | 174 | ## 举例 175 | ```go 176 | mch := wx.MchAccount{} 177 | 178 | var res mch_api_v3.JsApiTransactionResp 179 | err = mch.NewMchReqV3(mch_api_v3.JsApiTransaction).Send(&mch_api_v3.JsApiTransactionReq{ 180 | AppId: constant.WxAppMember, 181 | MchId: mch.MchId, 182 | Description: "this is a description", 183 | OutTradeNo: wx.NewRandStr(22), 184 | NotifyUrl: "https://yourdomain.name/notify/url", 185 | Amount: mch_api_v3.Amount{Total: 100}, 186 | Payer: mch_api_v3.Payer{OpenId: "oEG8Ss_yCRB315jjLfdTRK-QicdY"}, 187 | }).Bind(&res).Do(http.MethodPost) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | log.Println(res) 192 | ``` 193 | 194 | # API Hook,同时支持两个类型的所有API 195 | `RegisterHook(hook func(req *http.Request, reqBody []byte, res *http.Response, err error))` 将每一次网络交互过程都开放出来,以便做自定义日志。 196 | 197 | # 为微信业务数据提供的额外工具方法 198 | - `NewRandStr` 生成符合微信要求随机字符 199 | - `LimitString` 限制长度,并将微信不支持的字符替换成'x',能满足公众号App的字符要求 200 | - `SafeString` 安全地限制长度,并将微信不支持的字符替换成'x',能满足商户平台的字符要求 201 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blusewang/wx 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/go-querystring v1.0.0 7 | github.com/youkale/go-querystruct v0.0.0-20190423034802-cb0a446556d0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 2 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 3 | github.com/youkale/go-querystruct v0.0.0-20190423034802-cb0a446556d0 h1:lpFv7OR11GFdg/Q+5jcsHFJBH38Hg0K0Vi8CbyV9gyo= 4 | github.com/youkale/go-querystruct v0.0.0-20190423034802-cb0a446556d0/go.mod h1:xNATC2kLfAGWFTDkrBVOnW+DFjrgXrbqMtcID37lFZY= 5 | -------------------------------------------------------------------------------- /http_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "io/ioutil" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | var _hook func(req *http.Request, reqBody []byte, res *http.Response, startAt time.Time, stopAt time.Time, err error) 17 | 18 | type mt struct { 19 | t http.Transport 20 | } 21 | 22 | func (m *mt) RoundTrip(req *http.Request) (res *http.Response, err error) { 23 | var reqBody []byte 24 | if req.Body != nil { 25 | reqBody, _ = ioutil.ReadAll(req.Body) 26 | req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) 27 | } 28 | t := time.Now() 29 | res, err = m.t.RoundTrip(req) 30 | if _hook != nil { 31 | _hook(req, reqBody, res, t, time.Now(), err) 32 | } 33 | return 34 | } 35 | 36 | var c *http.Client 37 | 38 | func client() *http.Client { 39 | if c == nil { 40 | c = &http.Client{Transport: &mt{}} 41 | } 42 | return c 43 | } 44 | 45 | func RegisterHook(hook func(req *http.Request, reqBody []byte, res *http.Response, startAt time.Time, stopAt time.Time, err error)) { 46 | _hook = hook 47 | } 48 | -------------------------------------------------------------------------------- /http_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "github.com/blusewang/wx/mch_api" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | func TestMt_RoundTrip(t *testing.T) { 21 | log.SetFlags(log.Ltime | log.Lshortfile) 22 | RegisterHook(func(req *http.Request, reqBody []byte, res *http.Response, startAt time.Time, stopAt time.Time, err error) { 23 | var data struct { 24 | Method string `json:"method"` 25 | Url string `json:"url"` 26 | Body string `json:"body"` 27 | ResBody string `json:"res_body"` 28 | } 29 | data.Method = req.Method 30 | data.Url = req.URL.String() 31 | data.Body = string(reqBody) 32 | 33 | if res.Body != nil { 34 | raw, _ := ioutil.ReadAll(res.Body) 35 | data.ResBody = string(raw) 36 | res.Body = ioutil.NopCloser(bytes.NewReader(raw)) 37 | } 38 | 39 | raw, err := json.Marshal(data) 40 | log.Println(string(raw), err) 41 | }) 42 | 43 | var mch = MchAccount{ 44 | MchId: "", 45 | MchKeyV2: "", 46 | MchKeyV3: "", 47 | } 48 | mch.NewMchReq(mch_api.PayOrderQuery) 49 | } 50 | 51 | func TestRegisterHook(t *testing.T) { 52 | log.SetFlags(log.Ltime | log.Lshortfile) 53 | log.Println(client().Get("https://httpbin.org/delay/6")) 54 | } 55 | -------------------------------------------------------------------------------- /mch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "crypto" 11 | "crypto/aes" 12 | "crypto/cipher" 13 | "crypto/hmac" 14 | "crypto/md5" 15 | rand2 "crypto/rand" 16 | "crypto/rsa" 17 | "crypto/sha1" 18 | "crypto/sha256" 19 | "crypto/tls" 20 | "crypto/x509" 21 | "encoding/base64" 22 | "encoding/pem" 23 | "encoding/xml" 24 | "fmt" 25 | "github.com/blusewang/wx/mch_api" 26 | "github.com/blusewang/wx/mch_api_v3" 27 | "log" 28 | "net/http" 29 | "strconv" 30 | "time" 31 | ) 32 | 33 | // MchAccount 商户账号 34 | type MchAccount struct { 35 | MchId string 36 | MchKeyV2 string 37 | MchKeyV3 string 38 | certTls tls.Certificate // 我的证书 tls版 用于传输 39 | certX509 *x509.Certificate // 我的证书 x509版 用于辅助加解密 40 | privateKey *rsa.PrivateKey // 我的Key 41 | publicKeyWxPay *rsa.PublicKey // 加密银行卡信息时用的微信支付的公钥 42 | } 43 | 44 | // NewMchAccount 实例化商户账号 45 | func NewMchAccount(mchid, key2, key3 string, cert, key, pubKey []byte) (ma *MchAccount, err error) { 46 | ma = &MchAccount{ 47 | MchId: mchid, 48 | MchKeyV2: key2, 49 | MchKeyV3: key3, 50 | } 51 | cb, _ := pem.Decode(cert) 52 | if ma.certX509, err = x509.ParseCertificate(cb.Bytes); err != nil { 53 | return 54 | } 55 | ma.certTls, err = tls.X509KeyPair(cert, key) 56 | if err != nil { 57 | return 58 | } 59 | cb, _ = pem.Decode(key) 60 | if cb.Type == "RSA PRIVATE KEY" { 61 | ma.privateKey, err = x509.ParsePKCS1PrivateKey(cb.Bytes) 62 | if err != nil { 63 | return 64 | } 65 | } else if cb.Type == "PRIVATE KEY" { 66 | o, err := x509.ParsePKCS8PrivateKey(cb.Bytes) 67 | if err != nil { 68 | return nil, err 69 | } 70 | ma.privateKey = o.(*rsa.PrivateKey) 71 | } 72 | if pubKey != nil { 73 | cb, _ = pem.Decode(pubKey) 74 | ma.publicKeyWxPay, err = x509.ParsePKCS1PublicKey(cb.Bytes) 75 | } 76 | return 77 | } 78 | 79 | // NewMchReqWithApp 创建请求 80 | func (ma MchAccount) NewMchReqWithApp(api mch_api.MchApi, appId string) (req *mchReq) { 81 | return &mchReq{account: ma, api: api, appId: appId} 82 | } 83 | 84 | // NewMchReq 创建请求 85 | func (ma MchAccount) NewMchReq(api mch_api.MchApi) (req *mchReq) { 86 | return &mchReq{account: ma, api: api} 87 | } 88 | 89 | // OrderSign4App 订单签名给App 90 | func (ma MchAccount) OrderSign4App(or mch_api.PayUnifiedOrderRes) H { 91 | data := make(H) 92 | data["appid"] = or.AppId 93 | data["partnerid"] = or.MchId 94 | data["prepayid"] = or.PrepayId 95 | data["package"] = "Sign=WXPay" 96 | data["noncestr"] = NewRandStr(32) 97 | data["timestamp"] = time.Now().Unix() 98 | data["sign"] = ma.orderSign(data) 99 | delete(data, "appid") 100 | return data 101 | } 102 | 103 | // OrderSign 订单签名,适用于H5、小程序 104 | func (ma MchAccount) OrderSign(or mch_api.PayUnifiedOrderRes) H { 105 | data := make(H) 106 | data["appId"] = or.AppId 107 | data["timeStamp"] = strconv.FormatInt(time.Now().Unix(), 10) 108 | data["nonceStr"] = NewRandStr(32) 109 | data["package"] = fmt.Sprintf("prepay_id=%v", or.PrepayId) 110 | data["signType"] = "MD5" 111 | data["paySign"] = ma.orderSign(data) 112 | delete(data, "appId") 113 | data["timestamp"] = data["timeStamp"] 114 | delete(data, "timeStamp") 115 | return data 116 | } 117 | 118 | // PayNotify 验证支付成功通知 119 | func (ma MchAccount) PayNotify(pn mch_api.PayNotify) bool { 120 | if !pn.IsSuccess() || pn.Sign == "" { 121 | return false 122 | } 123 | sign := pn.Sign 124 | if pn.SignType == mch_api.MchSignTypeMD5 || pn.SignType == "" { 125 | if sign == ma.signMd5(pn) { 126 | return true 127 | } 128 | } else if pn.SignType == mch_api.MchSignTypeHMACSHA256 { 129 | if sign == ma.signHmacSha256(pn) { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | 136 | // DecryptRefundNotify 验证支付成功通知 137 | func (ma MchAccount) DecryptRefundNotify(rn mch_api.RefundNotify) (body mch_api.RefundNotifyBody, err error) { 138 | raw, err := base64.StdEncoding.DecodeString(rn.ReqInfo) 139 | if err != nil { 140 | return 141 | } 142 | block, err := aes.NewCipher([]byte(fmt.Sprintf("%x", md5.Sum([]byte(ma.MchKeyV2))))) 143 | length := len(raw) 144 | size := block.BlockSize() 145 | decrypted := make([]byte, len(raw)) 146 | for bs, be := 0, size; bs < len(raw); bs, be = bs+size, be+size { 147 | block.Decrypt(decrypted[bs:be], raw[bs:be]) 148 | } 149 | up := int(decrypted[length-1]) 150 | decrypted = decrypted[:length-up] 151 | err = xml.Unmarshal(decrypted, &body) 152 | return 153 | } 154 | 155 | // RsaEncrypt 机要信息加密V2 156 | func (ma MchAccount) RsaEncrypt(plain string) (out string) { 157 | raw, err := rsa.EncryptOAEP(sha1.New(), rand2.Reader, ma.publicKeyWxPay, []byte(plain), nil) 158 | if err != nil { 159 | return 160 | } 161 | out = base64.StdEncoding.EncodeToString(raw) 162 | return 163 | } 164 | 165 | // RsaEncryptV3 机要信息加密V3 166 | func (ma MchAccount) RsaEncryptV3(plain string) (out string) { 167 | var pk = wechatPayCerts.GetCert() 168 | raw, err := rsa.EncryptOAEP(sha1.New(), rand2.Reader, pk.PublicKey.(*rsa.PublicKey), []byte(plain), nil) 169 | if err != nil { 170 | return 171 | } 172 | out = base64.StdEncoding.EncodeToString(raw) 173 | return 174 | } 175 | 176 | // RsaDecrypt 机要信息解密 兼容V2/V3 177 | func (ma MchAccount) RsaDecrypt(ciphertext string) (out string, err error) { 178 | raw, _ := base64.StdEncoding.DecodeString(ciphertext) 179 | raw, err = rsa.DecryptOAEP(sha1.New(), rand2.Reader, ma.privateKey, raw, nil) 180 | if err != nil { 181 | return 182 | } 183 | out = string(raw) 184 | return 185 | } 186 | 187 | // DecryptAES256GCM AEAD_AES_256_GCM 解密 188 | func (ma MchAccount) DecryptAES256GCM(nonce, associatedData, ciphertext string) (out []byte, err error) { 189 | decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext) 190 | if err != nil { 191 | return 192 | } 193 | block, err := aes.NewCipher([]byte(ma.MchKeyV3)) 194 | if err != nil { 195 | return 196 | } 197 | gcm, err := cipher.NewGCM(block) 198 | if err != nil { 199 | return 200 | } 201 | out, err = gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData)) 202 | return 203 | } 204 | 205 | func (ma MchAccount) signMd5(obj interface{}) string { 206 | return fmt.Sprintf("%X", md5.Sum([]byte(mapSortByKey(obj2map(obj))+"&key="+ma.MchKeyV2))) 207 | } 208 | 209 | func (ma MchAccount) signHmacSha256(obj interface{}) string { 210 | hm := hmac.New(sha256.New, []byte(ma.MchKeyV2)) 211 | hm.Write([]byte(mapSortByKey(obj2map(obj)) + "&key=" + ma.MchKeyV2)) 212 | return fmt.Sprintf("%X", hm.Sum(nil)) 213 | } 214 | 215 | func (ma MchAccount) orderSign(data map[string]interface{}) string { 216 | return fmt.Sprintf("%X", md5.Sum([]byte(mapSortByKey(data)+"&key="+ma.MchKeyV2))) 217 | } 218 | 219 | func (ma MchAccount) newPrivateClient() (cli *http.Client, err error) { 220 | cli = client() 221 | cli.Transport.(*mt).t.TLSClientConfig = &tls.Config{ 222 | Certificates: []tls.Certificate{ma.certTls}, 223 | } 224 | cli.Transport.(*mt).t.DisableCompression = true 225 | return 226 | } 227 | 228 | // NewMchReqV3 创建请求 229 | func (ma MchAccount) NewMchReqV3(api mch_api_v3.MchApiV3) (req *mchReqV3) { 230 | req = &mchReqV3{account: ma, api: api} 231 | return 232 | } 233 | 234 | // GetCertificate 获取证书 235 | func (ma MchAccount) GetCertificate() (cert *x509.Certificate, err error) { 236 | if wechatPayCerts.IsEmpty() { 237 | if err = ma.DownloadV3Cert(); err != nil { 238 | return 239 | } 240 | } 241 | return wechatPayCerts.GetCert(), nil 242 | } 243 | 244 | // DownloadV3Cert 获取微信支付官方证书 245 | func (ma MchAccount) DownloadV3Cert() (err error) { 246 | if wechatPayCerts.IsEmpty() { 247 | wechatPayCerts.Add(PayCert{ 248 | SerialNo: "", 249 | EffectiveTime: time.Now(), 250 | ExpireTime: time.Now(), 251 | cert: nil, 252 | }) 253 | } 254 | var res mch_api_v3.OtherCertificatesResp 255 | err = ma.NewMchReqV3(mch_api_v3.OtherCertificates).Bind(&res).Do(http.MethodGet) 256 | if err != nil { 257 | return 258 | } 259 | for _, c := range res.Data { 260 | ct, err := ma.DecryptAES256GCM(c.EncryptCertificate.Nonce, c.EncryptCertificate.AssociatedData, c.EncryptCertificate.Ciphertext) 261 | if err != nil { 262 | return err 263 | } 264 | cb, _ := pem.Decode(ct) 265 | cert, err := x509.ParseCertificate(cb.Bytes) 266 | if err != nil { 267 | return err 268 | } 269 | wechatPayCerts.Add(PayCert{ 270 | SerialNo: c.SerialNo, 271 | EffectiveTime: c.EffectiveTime, 272 | ExpireTime: c.ExpireTime, 273 | cert: cert, 274 | }) 275 | } 276 | return 277 | } 278 | 279 | // SignBaseV3 V3版通用签名 280 | func (ma MchAccount) SignBaseV3(message string) (sign string, err error) { 281 | s := sha256.New() 282 | s.Write([]byte(message)) 283 | raw, err := rsa.SignPKCS1v15(rand2.Reader, ma.privateKey, crypto.SHA256, s.Sum(nil)) 284 | if err != nil { 285 | return 286 | } 287 | sign = base64.StdEncoding.EncodeToString(raw) 288 | return 289 | } 290 | 291 | // VerifyV3 验签 292 | func (ma MchAccount) VerifyV3(header http.Header, body []byte) (err error) { 293 | if wechatPayCerts.IsEmpty() { 294 | if err = ma.DownloadV3Cert(); err != nil { 295 | return 296 | } 297 | } 298 | cert := wechatPayCerts.GetCertBySerialNo(header.Get("Wechatpay-Serial")) 299 | if cert == nil { 300 | log.Println("未能在缓存中匹配到对方ID的证书", header.Get("Wechatpay-Serial")) 301 | return nil 302 | } 303 | signRaw, err := base64.StdEncoding.DecodeString(header.Get("Wechatpay-Signature")) 304 | if err != nil { 305 | return 306 | } 307 | s := sha256.New() 308 | s.Write([]byte(fmt.Sprintf("%v\n%s\n%s\n", 309 | header.Get("Wechatpay-Timestamp"), 310 | header.Get("Wechatpay-Nonce"), string(body)))) 311 | return rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), crypto.SHA256, s.Sum(nil), signRaw) 312 | } 313 | 314 | // SignJSAPIV3 JSAPI支付订单签名 315 | func (ma MchAccount) SignJSAPIV3(appId, prepayId string) (out H, err error) { 316 | ts := time.Now().Unix() 317 | nonce := NewRandStr(32) 318 | s, err := ma.SignBaseV3(fmt.Sprintf("%v\n%v\n%v\nprepay_id=%v\n", appId, ts, nonce, prepayId)) 319 | if err != nil { 320 | return 321 | } 322 | out = H{ 323 | "timestamp": fmt.Sprintf("%v", ts), 324 | "nonceStr": nonce, 325 | "package": fmt.Sprintf("prepay_id=%v", prepayId), 326 | "signType": "RSA", 327 | "paySign": s, 328 | } 329 | return 330 | } 331 | 332 | // SignAppV3 App支付订单签名 333 | func (ma MchAccount) SignAppV3(appId, prepayId string) (out H, err error) { 334 | ts := time.Now().Unix() 335 | nonce := NewRandStr(32) 336 | s, err := ma.SignBaseV3(fmt.Sprintf("%v\n%v\n%v\n%v\n", appId, ts, nonce, prepayId)) 337 | if err != nil { 338 | return 339 | } 340 | out = H{ 341 | "partnerid": ma.MchId, 342 | "prepayid": prepayId, 343 | "package": "Sign=WXPay", 344 | "noncestr": nonce, 345 | "timestamp": ts, 346 | "sign": s, 347 | } 348 | return 349 | } 350 | -------------------------------------------------------------------------------- /mch_api/constant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api 8 | 9 | type MchApi string 10 | 11 | const ( 12 | // PayUnifiedOrder 微信下单 13 | PayUnifiedOrder = "pay/unifiedorder" 14 | // PayOrderQuery 支付结果查询 15 | PayOrderQuery = "pay/orderquery" 16 | // PayRefund 退款 17 | PayRefund = "secapi/pay/refund" 18 | // PayProfitSharing 请求单次分账 19 | PayProfitSharing = "secapi/pay/profitsharing" 20 | // PayProfitSharingFinish 结束分账请求 21 | PayProfitSharingFinish = "secapi/pay/profitsharingfinish" 22 | // BankPay 企业付款到银行卡 23 | BankPay = "mmpaysptrans/pay_bank" 24 | // BankQuery 付款到银行卡结果查询 25 | BankQuery = "mmpaysptrans/query_bank" 26 | // RedPackSend 发红包 27 | RedPackSend = "mmpaymkttransfers/sendredpack" 28 | // RedPackInfo 红包状态查询 29 | RedPackInfo = "mmpaymkttransfers/gethbinfo" 30 | // Transfer 企业付款至零钱 31 | Transfer = "mmpaymkttransfers/promotion/transfers" 32 | // PublicKey 获取RSA公钥API获取RSA公钥 33 | PublicKey = "https://fraud.mch.weixin.qq.com/risk/getpublickey" 34 | ) 35 | 36 | type MchSignType string 37 | 38 | const ( 39 | MchSignTypeMD5 = "MD5" 40 | MchSignTypeHMACSHA256 = "HMAC-SHA256" 41 | ) 42 | -------------------------------------------------------------------------------- /mch_api/structs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api 8 | 9 | import ( 10 | "encoding/json" 11 | "encoding/xml" 12 | "errors" 13 | "fmt" 14 | ) 15 | 16 | type MchBase struct { 17 | XMLName xml.Name `xml:"xml"` 18 | MchId string `xml:"mch_id,omitempty"` 19 | AppId string `xml:"appid,omitempty"` 20 | NonceStr string `xml:"nonce_str"` 21 | Sign string `xml:"sign"` 22 | SignType string `xml:"sign_type,omitempty"` 23 | } 24 | 25 | type MchBaseResponse struct { 26 | XMLName xml.Name `xml:"xml"` 27 | ReturnCode string `xml:"return_code"` 28 | ReturnMsg string `xml:"return_msg"` 29 | ResultCode string `xml:"result_code,omitempty"` 30 | ErrCode string `xml:"err_code,omitempty"` 31 | ErrCodeDes string `xml:"err_code_des,omitempty"` 32 | } 33 | 34 | // IsSuccess 是否成功处理 35 | func (m MchBaseResponse) IsSuccess() bool { 36 | return m.ReturnCode == "SUCCESS" && m.ResultCode == "SUCCESS" && (m.ErrCode == "SUCCESS" || m.ErrCode == "") 37 | } 38 | 39 | // IsUnCertain 如果出错,是否是微信程序错误 40 | func (m MchBaseResponse) IsUnCertain() bool { 41 | return m.ErrCode == "SYSTEMERROR" 42 | } 43 | 44 | // ToError 转为Golang错误 45 | func (m MchBaseResponse) ToError() error { 46 | if m.ErrCodeDes != "" { 47 | return errors.New(fmt.Sprintf("%v %v", m.ErrCode, m.ErrCodeDes)) 48 | } else if m.ReturnMsg != "" { 49 | return errors.New(fmt.Sprintf("%v %v", m.ReturnCode, m.ReturnMsg)) 50 | } else { 51 | return nil 52 | } 53 | } 54 | 55 | // PayUnifiedOrderData `ProfitSharing`设置为"Y"为分账定单标记。 56 | // 不设置,或设置为"N",为普通定单 57 | type PayUnifiedOrderData struct { 58 | MchBase 59 | Openid string `xml:"openid,omitempty"` 60 | DeviceInfo string `xml:"device_info"` 61 | Body string `xml:"body"` 62 | OutTradeNo string `xml:"out_trade_no"` 63 | TotalFee int64 `xml:"total_fee"` 64 | SpBillCreateIp string `xml:"spbill_create_ip"` 65 | NotifyUrl string `xml:"notify_url"` 66 | TradeType string `xml:"trade_type"` 67 | Attach string `xml:"attach"` 68 | ProfitSharing string `xml:"profit_sharing,omitempty"` 69 | } 70 | 71 | type PayUnifiedOrderRes struct { 72 | MchBaseResponse 73 | MchBase 74 | PrepayId string `xml:"prepay_id"` 75 | } 76 | 77 | // PayNotify 支付成功通知 78 | type PayNotify struct { 79 | MchBaseResponse 80 | MchBase 81 | DeviceInfo string `xml:"device_info"` 82 | OpenId string `xml:"openid"` 83 | IsSubscribe string `xml:"is_subscribe"` 84 | TradeType string `xml:"trade_type"` 85 | BankType string `xml:"bank_type"` 86 | TotalFee int64 `xml:"total_fee"` 87 | SettlementTotalFee int64 `xml:"settlement_total_fee"` 88 | FeeType string `xml:"fee_type"` 89 | CashFee int64 `xml:"cash_fee"` 90 | CashFeeType string `xml:"cash_fee_type"` 91 | CouponFee int64 `xml:"coupon_fee"` 92 | CouponCount int64 `xml:"coupon_count"` 93 | CouponType0 string `xml:"coupon_type_0"` 94 | CouponId0 string `xml:"coupon_id_0"` 95 | CouponFee0 int64 `xml:"coupon_fee_0"` 96 | CouponType1 string `xml:"coupon_type_1"` 97 | CouponId1 string `xml:"coupon_id_1"` 98 | CouponFee1 int64 `xml:"coupon_fee_1"` 99 | CouponType2 string `xml:"coupon_type_2"` 100 | CouponId2 string `xml:"coupon_id_2"` 101 | CouponFee2 int64 `xml:"coupon_fee_2"` 102 | CouponType3 string `xml:"coupon_type_3"` 103 | CouponId3 string `xml:"coupon_id_3"` 104 | CouponFee3 int64 `xml:"coupon_fee_3"` 105 | TransactionId string `xml:"transaction_id"` 106 | OutTradeNo string `xml:"out_trade_no"` 107 | Attach string `xml:"attach"` 108 | TimeEnd string `xml:"time_end"` 109 | } 110 | 111 | // PayNotifyRes 回复支付成功通知 112 | type PayNotifyRes MchBaseResponse 113 | 114 | type PayOrderQueryData struct { 115 | MchBase 116 | OutTradeNo string `xml:"out_trade_no"` 117 | } 118 | 119 | type PayOrderQueryRes PayNotify 120 | 121 | type PayRefundData struct { 122 | MchBase 123 | TransactionId string `xml:"transaction_id,omitempty"` 124 | OutTradeNo string `xml:"out_trade_no,omitempty"` 125 | OutRefundNo string `xml:"out_refund_no"` 126 | TotalFee int64 `xml:"total_fee"` 127 | RefundFee int64 `xml:"refund_fee"` 128 | RefundDesc string `xml:"refund_desc,omitempty"` 129 | NotifyUrl string `xml:"notify_url,omitempty"` 130 | } 131 | 132 | type PayRefundRes struct { 133 | MchBaseResponse 134 | MchBase 135 | TransactionId string `xml:"transaction_id"` 136 | OutTradeNo string `xml:"out_trade_no"` 137 | OutRefundNo string `xml:"out_refund_no"` 138 | RefundId string `xml:"refund_id"` 139 | RefundFee int64 `xml:"refund_fee"` 140 | TotalFee int64 `xml:"total_fee"` 141 | CashFee int64 `xml:"cash_fee"` 142 | } 143 | 144 | // PayProfitSharingReceiver 分账结果中的接收者 145 | type PayProfitSharingReceiver struct { 146 | Type string `json:"type"` 147 | Account string `json:"account"` 148 | Amount int64 `json:"amount"` 149 | Description string `json:"description"` 150 | } 151 | 152 | type PayProfitSharingData struct { 153 | MchBase 154 | TransactionId string `xml:"transaction_id"` 155 | OutOrderNo string `xml:"out_order_no"` 156 | Receivers string `xml:"receivers"` 157 | } 158 | 159 | func (ppsd *PayProfitSharingData) SerReceivers(list []PayProfitSharingReceiver) (err error) { 160 | raw, err := json.Marshal(list) 161 | if err != nil { 162 | return 163 | } 164 | ppsd.Receivers = string(raw) 165 | return 166 | } 167 | 168 | type PayProfitSharingRes struct { 169 | MchBaseResponse 170 | MchBase 171 | TransactionId string `xml:"transaction_id"` 172 | OutOrderNo string `xml:"out_order_no"` 173 | OrderId string `xml:"order_id"` 174 | } 175 | 176 | type PayProfitSharingFinishData struct { 177 | MchBase 178 | TransactionId string `xml:"transaction_id"` 179 | OutOrderNo string `xml:"out_order_no"` 180 | Description string `xml:"description"` 181 | } 182 | 183 | type PayProfitSharingFinishRes PayProfitSharingRes 184 | 185 | type BankPayData struct { 186 | MchBase 187 | PartnerTradeNo string `xml:"partner_trade_no"` 188 | EncBankNo string `xml:"enc_bank_no"` 189 | EncTrueName string `xml:"enc_true_name"` 190 | BankCode string `xml:"bank_code"` 191 | AmountFen int64 `xml:"amount"` 192 | Desc string `xml:"desc"` 193 | } 194 | 195 | type BankPayRes struct { 196 | MchBaseResponse 197 | PartnerTradeNo string `xml:"partner_trade_no"` 198 | AmountFen int64 `xml:"amount"` 199 | PaymentNo string `xml:"payment_no"` 200 | CMmsAmt int64 `xml:"cmms_amt"` 201 | } 202 | 203 | type BankQueryData struct { 204 | MchBase 205 | PartnerTradeNo string `xml:"partner_trade_no"` 206 | } 207 | 208 | type BankQueryRes struct { 209 | MchBaseResponse 210 | PartnerTradeNo string `xml:"partner_trade_no"` 211 | PaymentNo string `xml:"payment_no"` 212 | AmountFen int64 `xml:"amount"` 213 | Status string `xml:"status"` 214 | CMmsAmtFen int64 `xml:"cmms_amt"` 215 | CreateTime string `xml:"create_time"` 216 | PaySuccessTime string `xml:"pay_succ_time"` 217 | Reason string `xml:"reason"` 218 | } 219 | 220 | type RedPackSendData struct { 221 | MchBase 222 | MchBillNo string `xml:"mch_billno"` 223 | WxAppId string `xml:"wxappid"` 224 | SendName string `xml:"send_name"` 225 | ReOpenId string `xml:"re_openid"` 226 | TotalAmount int `xml:"total_amount"` 227 | TotalNum int `xml:"total_num"` 228 | Wishing string `xml:"wishing"` 229 | ClientIp string `xml:"client_ip"` 230 | ActName string `xml:"act_name"` 231 | Remark string `xml:"remark"` 232 | } 233 | 234 | type RedPackSendRes struct { 235 | MchBaseResponse 236 | MchBillNo string `xml:"mch_billno"` 237 | MchId string `xml:"mch_id"` 238 | WxAppId string `xml:"wxappid"` 239 | ReOpenId string `xml:"re_openid"` 240 | TotalAmount int `xml:"total_amount"` 241 | SendListId string `xml:"send_listid"` 242 | } 243 | 244 | type RedPackInfoData struct { 245 | MchBase 246 | MchBillNo string `xml:"mch_billno"` 247 | BillType string `xml:"bill_type"` 248 | } 249 | 250 | type RedPackInfoRes struct { 251 | MchBaseResponse 252 | MchBillNo string `xml:"mch_billno"` 253 | MchId string `xml:"mch_id"` 254 | Status string `xml:"status"` 255 | SendType string `xml:"send_type"` 256 | HbType string `xml:"hb_type"` 257 | Reason *string `xml:"reason"` 258 | SendTime string `xml:"send_time"` 259 | RefundTime *string `xml:"refund_time"` 260 | RefundAmount *int `xml:"refund_amount"` 261 | Wishing *string `xml:"wishing"` 262 | Remark *string `xml:"remark"` 263 | ActName *string `xml:"act_name"` 264 | HbList []struct { 265 | HbInfo []struct { 266 | OpenId string `xml:"openid"` 267 | Amount int `xml:"amount"` 268 | RcvTime string `xml:"rcv_time"` 269 | } `xml:"hbinfo"` 270 | } `xml:"hblist"` 271 | } 272 | 273 | type TransferData struct { 274 | XMLName xml.Name `xml:"xml"` 275 | NonceStr string `xml:"nonce_str"` 276 | Sign string `xml:"sign"` 277 | SignType string `xml:"sign_type,omitempty"` 278 | MchId string `xml:"mchid"` 279 | MchAppId string `xml:"mch_appid"` 280 | PartnerTradeNo string `xml:"partner_trade_no"` 281 | OpenId string `xml:"openid"` 282 | CheckName string `xml:"check_name"` 283 | ReUserName string `xml:"re_user_name"` 284 | Amount int `xml:"amount"` 285 | Desc string `xml:"desc"` 286 | SpBillCreateIp string `xml:"spbill_create_ip"` 287 | } 288 | 289 | type TransferRes struct { 290 | MchBaseResponse 291 | MchId string `xml:"mchid"` 292 | MchAppId string `xml:"mch_appid"` 293 | NonceStr string `xml:"nonce_str"` 294 | PartnerTradeNo string `xml:"partner_trade_no"` 295 | PaymentNo string `xml:"payment_no"` 296 | PaymentTime string `xml:"payment_time"` 297 | } 298 | 299 | type PublicKeyData struct { 300 | MchBase 301 | } 302 | 303 | type PublicKeyRes struct { 304 | MchBaseResponse 305 | PubKey string `xml:"pub_key"` 306 | } 307 | 308 | // RefundNotify 退款状态通知消息 309 | type RefundNotify struct { 310 | MchBaseResponse 311 | MchBase 312 | ReqInfo string `xml:"req_info"` 313 | } 314 | 315 | // RefundNotifyBody 退款状态通知内容 316 | type RefundNotifyBody struct { 317 | XMLName xml.Name `xml:"root"` 318 | TransactionId string `xml:"transaction_id"` 319 | OutTradeNo string `xml:"out_trade_no"` 320 | RefundId string `xml:"refund_id"` 321 | OutRefundNo string `xml:"out_refund_no"` 322 | TotalFee int64 `xml:"total_fee"` 323 | SettlementTotalFee int64 `xml:"settlement_total_fee,omitempty"` 324 | RefundFee int64 `xml:"refund_fee"` 325 | SettlementRefundFee int64 `xml:"settlement_refund_fee"` 326 | RefundStatus string `xml:"refund_status"` 327 | SuccessTime string `xml:"success_time,omitempty"` 328 | RefundRecvAccount string `xml:"refund_recv_account"` 329 | RefundAccount string `xml:"refund_account"` 330 | RefundRequestSource string `xml:"refund_request_source"` 331 | } 332 | -------------------------------------------------------------------------------- /mch_api_v3/base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | import "time" 10 | 11 | type TransactionAmount struct { 12 | Total int64 `json:"total"` 13 | Currency string `json:"currency,omitempty"` 14 | } 15 | type TransactionPayer struct { 16 | OpenId string `json:"openid,omitempty"` 17 | } 18 | 19 | type TransactionSettleInfo struct { 20 | ProfitSharing bool `json:"profit_sharing"` 21 | } 22 | 23 | type JsApiTransactionReq struct { 24 | AppId string `json:"appid"` 25 | MchId string `json:"mchid"` 26 | Description string `json:"description"` 27 | OutTradeNo string `json:"out_trade_no"` 28 | TimeExpire string `json:"time_expire,omitempty"` 29 | Attach string `json:"attach,omitempty"` 30 | NotifyUrl string `json:"notify_url"` 31 | GoodsTag string `json:"goods_tag,omitempty"` 32 | Amount TransactionAmount `json:"amount"` 33 | Payer TransactionPayer `json:"payer"` 34 | SettleInfo *TransactionSettleInfo `json:"settle_info,omitempty"` 35 | } 36 | 37 | type JsApiTransactionResp struct { 38 | PrepayId string `json:"prepay_id"` 39 | } 40 | 41 | type AppTransactionReq JsApiTransactionReq 42 | 43 | type AppTransactionResp JsApiTransactionResp 44 | 45 | type NotifyPayResult struct { 46 | Id string `json:"id"` 47 | CreateTime string `json:"create_time"` 48 | EventType string `json:"event_type"` 49 | ResourceType string `json:"resource_type"` 50 | Summary string `json:"summary"` 51 | Resource struct { 52 | Algorithm string `json:"algorithm"` 53 | Ciphertext string `json:"ciphertext"` 54 | AssociatedData string `json:"associated_data,omitempty"` 55 | OriginalType string `json:"original_type"` 56 | Nonce string `json:"nonce"` 57 | } `json:"resource"` 58 | } 59 | 60 | type NotifyResource struct { 61 | Appid string `json:"appid,omitempty"` 62 | Mchid string `json:"mchid,omitempty"` 63 | SpAppid string `json:"sp_appid,omitempty"` 64 | SpMchid string `json:"sp_mchid,omitempty"` 65 | SubAppid string `json:"sub_appid,omitempty"` 66 | SubMchid string `json:"sub_mchid,omitempty"` 67 | OutTradeNo string `json:"out_trade_no"` 68 | TransactionId string `json:"transaction_id"` 69 | TradeType string `json:"trade_type"` 70 | TradeState string `json:"trade_state"` 71 | TradeStatDesc string `json:"trade_stat_desc"` 72 | BankType string `json:"bank_type"` 73 | Attach string `json:"attach,omitempty"` 74 | SuccessTime time.Time `json:"success_time"` 75 | Amount struct { 76 | Total int64 `json:"total"` 77 | PayerTotal int64 `json:"payer_total"` 78 | Currency string `json:"currency"` 79 | PayerCurrency string `json:"payer_currency"` 80 | } `json:"amount"` 81 | Payer struct { 82 | Openid string `json:"openid,omitempty"` 83 | SpOpenid string `json:"sp_openid,omitempty"` 84 | SubOpenid string `json:"sub_openid,omitempty"` 85 | } `json:"payer"` 86 | SceneInfo struct { 87 | DeviceId string `json:"device_id,omitempty"` 88 | } `json:"scene_info,omitempty"` 89 | } 90 | 91 | type NotifyResp struct { 92 | Code string `json:"code"` 93 | Message string `json:"message"` 94 | } 95 | -------------------------------------------------------------------------------- /mch_api_v3/constant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | type MchApiV3 string 10 | 11 | const ( 12 | // OtherCertificates 获取平台证书列表 13 | OtherCertificates = "certificates" 14 | OtherUploadImage = "merchant/media/upload" 15 | OtherUploadVideo = "merchant/media/video_upload" 16 | 17 | // PartnerApplyment4Sub 特约商户进件申请单 18 | PartnerApplyment4Sub = "applyment4sub/applyment/" 19 | 20 | // PartnerApplymentQuery 特约商户进件申请状态查询 21 | PartnerApplymentQuery = "applyment4sub/applyment/business_code/" 22 | 23 | // PartnerJsApiTransaction 服务商JSAPI下单 24 | PartnerJsApiTransaction = "pay/partner/transactions/jsapi" 25 | 26 | // PartnerAppTransaction 服务商App下单 27 | PartnerAppTransaction = "pay/partner/transactions/app" 28 | 29 | // JsApiTransaction JSAPI下单 30 | JsApiTransaction = "pay/transactions/jsapi" 31 | 32 | // AppTransaction App下单 33 | AppTransaction = "pay/transactions/app" 34 | 35 | // ProfitSharingOrders 请求分账 36 | ProfitSharingOrders = "profitsharing/orders" 37 | 38 | // ProfitSharingOrdersUnfreeze 解冻剩余资金 39 | ProfitSharingOrdersUnfreeze = "profitsharing/orders/unfreeze" 40 | 41 | // ProfitSharingOrdersAdd 添加分账接收方 42 | ProfitSharingOrdersAdd = "profitsharing/receivers/add" 43 | 44 | // ProfitSharingOrdersDelete 删除分账接收方 45 | ProfitSharingOrdersDelete = "profitsharing/receivers/delete" 46 | 47 | // ProfitSharingMerchantConfigs 查询最大分账比例 48 | ProfitSharingMerchantConfigs = "profitsharing/merchant-configs" 49 | ) 50 | 51 | type ErrorResp struct { 52 | Code string `json:"code"` 53 | Detail struct { 54 | Location string `json:"location"` 55 | Value string `json:"value"` 56 | } `json:"detail"` 57 | Message string `json:"message"` 58 | } 59 | -------------------------------------------------------------------------------- /mch_api_v3/other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | import "time" 10 | 11 | type OtherCertificatesResp struct { 12 | Data []struct { 13 | SerialNo string `json:"serial_no"` 14 | EffectiveTime time.Time `json:"effective_time"` 15 | ExpireTime time.Time `json:"expire_time"` 16 | EncryptCertificate struct { 17 | Algorithm string `json:"algorithm"` 18 | Nonce string `json:"nonce"` 19 | AssociatedData string `json:"associated_data"` 20 | Ciphertext string `json:"ciphertext"` 21 | } `json:"encrypt_certificate"` 22 | } `json:"data"` 23 | } 24 | 25 | type OtherUploadResp struct { 26 | MediaId string `json:"media_id"` 27 | } 28 | -------------------------------------------------------------------------------- /mch_api_v3/partner_applyment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | // PartnerApplymentSubjectType 主体类型 10 | type PartnerApplymentSubjectType string 11 | 12 | const ( 13 | // PartnerApplymentSubjectTypePerson 个体户 14 | PartnerApplymentSubjectTypePerson = "SUBJECT_TYPE_INDIVIDUAL" 15 | // PartnerApplymentSubjectTypeEnterprise 企业 16 | PartnerApplymentSubjectTypeEnterprise = "SUBJECT_TYPE_ENTERPRISE" 17 | ) 18 | 19 | // PartnerApplymentIDType 证件类型 20 | type PartnerApplymentIDType string 21 | 22 | // PartnerApplymentIDCard 银行账户类型 23 | const PartnerApplymentIDCard = "IDENTIFICATION_TYPE_IDCARD" 24 | 25 | // PartnerApplymentBankAccountType 银行账户类型 26 | type PartnerApplymentBankAccountType string 27 | 28 | const ( 29 | // PartnerApplymentBankAccountTypeCorporate 对公银行账户 30 | PartnerApplymentBankAccountTypeCorporate = "BANK_ACCOUNT_TYPE_CORPORATE" 31 | // PartnerApplymentBankAccountTypePersonal 经营者个人银行卡 32 | PartnerApplymentBankAccountTypePersonal = "BANK_ACCOUNT_TYPE_PERSONAL" 33 | ) 34 | 35 | // PartnerApplymentReq 进件申请单 36 | type PartnerApplymentReq struct { 37 | BusinessCode string `json:"business_code"` 38 | ContactInfo struct { 39 | ContactType string `json:"contact_type"` 40 | ContactName string `json:"contact_name"` 41 | ContactIdNumber string `json:"contact_id_number,omitempty"` 42 | OpenId string `json:"open_id,omitempty"` 43 | MobilePhone string `json:"mobile_phone"` 44 | ContactEmail string `json:"contact_email"` 45 | } `json:"contact_info"` 46 | SubjectInfo struct { 47 | SubjectType PartnerApplymentSubjectType `json:"subject_type"` 48 | BusinessLicenseInfo struct { 49 | LicenseCopy string `json:"license_copy"` 50 | LicenseNumber string `json:"license_number"` 51 | MerchantName string `json:"merchant_name"` 52 | LegalPerson string `json:"legal_person"` 53 | PeriodBegin *string `json:"period_begin,omitempty"` 54 | PeriodEnd *string `json:"period_end,omitempty"` 55 | } `json:"business_license_info,omitempty"` 56 | IdentityInfo struct { 57 | IdDocType PartnerApplymentIDType `json:"id_doc_type"` 58 | IdCardInfo struct { 59 | IdCardCopy string `json:"id_card_copy"` 60 | IdCardNational string `json:"id_card_national"` 61 | IdCardName string `json:"id_card_name"` 62 | IdCardNumber string `json:"id_card_number"` 63 | IdCardAddress string `json:"id_card_address"` 64 | CardPeriodBegin string `json:"card_period_begin"` 65 | CardPeriodEnd string `json:"card_period_end"` 66 | } `json:"id_card_info"` 67 | Owner *bool `json:"owner,omitempty"` 68 | } `json:"identity_info"` 69 | } `json:"subject_info"` 70 | BusinessInfo struct { 71 | MerchantShortname string `json:"merchant_shortname"` 72 | ServicePhone string `json:"service_phone"` 73 | SalesInfo struct { 74 | SalesScenesType []string `json:"sales_scenes_type"` 75 | BizStoreInfo struct { 76 | BizStoreName string `json:"biz_store_name"` 77 | BizAddressCode string `json:"biz_address_code"` 78 | BizStoreAddress string `json:"biz_store_address"` 79 | StoreEntrancePic []string `json:"store_entrance_pic"` 80 | IndoorPic []string `json:"indoor_pic"` 81 | BizSubAppid string `json:"biz_sub_appid,omitempty"` 82 | } `json:"biz_store_info"` 83 | } `json:"sales_info"` 84 | } `json:"business_info"` 85 | SettlementInfo struct { 86 | SettlementId string `json:"settlement_id"` 87 | QualificationType string `json:"qualification_type"` 88 | Qualifications []string `json:"qualifications,omitempty"` 89 | ActivitiesId string `json:"activities_id,omitempty"` 90 | ActivitiesRate string `json:"activities_rate,omitempty"` 91 | ActivitiesAdditions []string `json:"activities_additions,omitempty"` 92 | } `json:"settlement_info"` 93 | BankAccountInfo struct { 94 | BankAccountType PartnerApplymentBankAccountType `json:"bank_account_type"` 95 | AccountName string `json:"account_name"` 96 | AccountBank string `json:"account_bank"` 97 | BankAddressCode string `json:"bank_address_code"` 98 | BankBranchId string `json:"bank_branch_id,omitempty"` 99 | BankName string `json:"bank_name,omitempty"` 100 | AccountNumber string `json:"account_number"` 101 | } `json:"bank_account_info"` 102 | AdditionInfo struct { 103 | BusinessAdditionPics []string `json:"business_addition_pics,omitempty"` 104 | } `json:"addition_info,omitempty"` 105 | } 106 | 107 | type PartnerApplymentResp struct { 108 | ApplymentId int64 `json:"applyment_id"` 109 | } 110 | 111 | type PartnerApplymentState string 112 | 113 | const ( 114 | PartnerApplymentStateEditing = "APPLYMENT_STATE_EDITTING" 115 | PartnerApplymentStateAuditing = "APPLYMENT_STATE_AUDITING" 116 | PartnerApplymentStateRejected = "APPLYMENT_STATE_REJECTED" 117 | PartnerApplymentStateBeConfirm = "APPLYMENT_STATE_TO_BE_CONFIRMED" 118 | PartnerApplymentStateBeSigned = "APPLYMENT_STATE_TO_BE_SIGNED" 119 | PartnerApplymentStateSigning = "APPLYMENT_STATE_SIGNING" 120 | PartnerApplymentStateFinished = "APPLYMENT_STATE_FINISHED" 121 | PartnerApplymentStateCanceled = "APPLYMENT_STATE_CANCELED" 122 | ) 123 | 124 | type PartnerApplymentQueryResp struct { 125 | BusinessCode string `json:"business_code"` 126 | ApplymentId int64 `json:"applyment_id"` 127 | SubMchid *string `json:"sub_mchid,omitempty"` 128 | SignUrl *string `json:"sign_url,omitempty"` 129 | ApplymentState PartnerApplymentState `json:"applyment_state"` 130 | ApplymentStateMsg string `json:"applyment_state_msg"` 131 | AuditDetail []struct { 132 | Field string `json:"field"` 133 | FieldName string `json:"field_name"` 134 | RejectReason string `json:"reject_reason"` 135 | } `json:"audit_detail,omitempty"` 136 | } 137 | -------------------------------------------------------------------------------- /mch_api_v3/partner_base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | type PartnerPayer struct { 10 | SpOpenid string `json:"sp_openid,omitempty"` 11 | SubOpenid string `json:"sub_openid,omitempty"` 12 | } 13 | type PartnerJsApiTransactionReq struct { 14 | SpAppId string `json:"sp_appid"` 15 | SpMchId string `json:"sp_mchid"` 16 | SubAppid string `json:"sub_appid,omitempty"` 17 | SubMchId string `json:"sub_mchid"` 18 | Description string `json:"description"` 19 | OutTradeNo string `json:"out_trade_no"` 20 | TimeExpire string `json:"time_expire,omitempty"` 21 | Attach string `json:"attach,omitempty"` 22 | NotifyUrl string `json:"notify_url"` 23 | GoodsTag string `json:"goods_tag,omitempty"` 24 | Amount TransactionAmount `json:"amount"` 25 | Payer PartnerPayer `json:"payer"` 26 | SettleInfo *TransactionSettleInfo `json:"settle_info,omitempty"` 27 | } 28 | 29 | type PartnerJsApiTransactionResp JsApiTransactionResp 30 | -------------------------------------------------------------------------------- /mch_api_v3/profit_sharing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mch_api_v3 8 | 9 | type ProfitSharingOrdersReqReceiver struct { 10 | Type string `json:"type"` 11 | Account string `json:"account"` 12 | Name string `json:"name,omitempty"` 13 | Amount int64 `json:"amount"` 14 | Description string `json:"description"` 15 | } 16 | type ProfitSharingOrdersReq struct { 17 | SubMchid string `json:"sub_mchid,omitempty"` 18 | AppId string `json:"appid"` 19 | SubAppid string `json:"sub_appid,omitempty"` 20 | TransactionId string `json:"transaction_id"` 21 | OutOrderNo string `json:"out_order_no"` 22 | UnfreezeUnsplit bool `json:"unfreeze_unsplit"` 23 | Receivers []ProfitSharingOrdersReqReceiver `json:"receivers"` 24 | } 25 | 26 | type ProfitSharingOrdersRespReceiver struct { 27 | ProfitSharingOrdersReqReceiver 28 | Result string `json:"result"` 29 | FailReason string `json:"fail_reason"` 30 | DetailId string `json:"detail_id"` 31 | CreateTime string `json:"create_time"` 32 | FinishTime string `json:"finish_time"` 33 | } 34 | 35 | type ProfitSharingOrdersResp struct { 36 | TransactionId string `json:"transaction_id"` 37 | OutOrderNo string `json:"out_order_no"` 38 | OrderId string `json:"order_id"` 39 | State string `json:"state"` 40 | Receivers *[]ProfitSharingOrdersRespReceiver `json:"receivers,omitempty"` 41 | } 42 | 43 | type ProfitSharingOrdersQueryResp struct { 44 | TransactionId string `json:"transaction_id"` 45 | OutOrderNo string `json:"out_order_no"` 46 | OrderId string `json:"order_id"` 47 | State string `json:"state"` 48 | Receivers *[]ProfitSharingOrdersRespReceiver `json:"receivers,omitempty"` 49 | } 50 | 51 | type ProfitSharingOrdersUnfreezeReq struct { 52 | SubMchid string `json:"sub_mchid,omitempty"` 53 | TransactionId string `json:"transaction_id"` 54 | OutOrderNo string `json:"out_order_no"` 55 | Description string `json:"description"` 56 | } 57 | 58 | type ProfitSharingOrdersUnfreezeResp ProfitSharingOrdersQueryResp 59 | 60 | type ProfitSharingOrdersAddReq struct { 61 | SubMchid string `json:"sub_mchid,omitempty"` 62 | AppId string `json:"appid"` 63 | SubAppid string `json:"sub_appid,omitempty"` 64 | Type string `json:"type"` 65 | Account string `json:"account"` 66 | Name string `json:"name,omitempty"` 67 | RelationType string `json:"relation_type"` 68 | CustomRelation string `json:"custom_relation,omitempty"` 69 | } 70 | 71 | type ProfitSharingOrdersDeleteReq struct { 72 | SubMchid string `json:"sub_mchid,omitempty"` 73 | AppId string `json:"appid"` 74 | SubAppid string `json:"sub_appid,omitempty"` 75 | Type string `json:"type"` 76 | Account string `json:"account"` 77 | } 78 | 79 | type ProfitSharingMerchantConfigsResp struct { 80 | SubMchid string `json:"sub_mchid"` 81 | MaxRatio int64 `json:"max_ratio"` 82 | } 83 | -------------------------------------------------------------------------------- /mch_req.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "encoding/xml" 12 | "errors" 13 | "fmt" 14 | "github.com/blusewang/wx/mch_api" 15 | "net/http" 16 | "reflect" 17 | "strings" 18 | ) 19 | 20 | // 商户请求 21 | type mchReq struct { 22 | account MchAccount 23 | //privateClient *http.Client // 私有加密传输客户端 24 | api mch_api.MchApi 25 | appId string 26 | isHmacSign bool 27 | isPrivateClient bool 28 | sendData interface{} 29 | res interface{} 30 | err error 31 | } 32 | 33 | // Send 填充POST里的Body数据 34 | func (mr *mchReq) Send(data interface{}) *mchReq { 35 | mr.sendData = data 36 | return mr 37 | } 38 | 39 | // UseHMacSign 使用 HMAC-SHA256 签名 40 | // 默认采用 MD5 签名 41 | func (mr *mchReq) UseHMacSign() *mchReq { 42 | mr.isHmacSign = true 43 | return mr 44 | } 45 | 46 | // UsePrivateCert 使用私有证书通信 47 | func (mr *mchReq) UsePrivateCert() *mchReq { 48 | mr.isPrivateClient = true 49 | return mr 50 | } 51 | 52 | // Bind 绑定请求结果的解码数据体 53 | func (mr *mchReq) Bind(data interface{}) *mchReq { 54 | mr.res = data 55 | return mr 56 | } 57 | 58 | // Do 执行 59 | func (mr *mchReq) Do() (err error) { 60 | if err = mr.sign(); err != nil { 61 | return 62 | } 63 | 64 | var buf = new(bytes.Buffer) 65 | if err = xml.NewEncoder(buf).Encode(mr.sendData); err != nil { 66 | return 67 | } 68 | api := fmt.Sprintf("https://api.mch.weixin.qq.com/%v", mr.api) 69 | if strings.HasPrefix(string(mr.api), "http") { 70 | api = string(mr.api) 71 | } 72 | var cli *http.Client 73 | if mr.isPrivateClient { 74 | cli, err = mr.account.newPrivateClient() 75 | if err != nil { 76 | return err 77 | } 78 | } else { 79 | cli = client() 80 | } 81 | resp, err := cli.Post(api, "application/xml", buf) 82 | defer resp.Body.Close() 83 | if err != nil { 84 | return 85 | } 86 | if resp.StatusCode != http.StatusOK { 87 | return errors.New(resp.Status) 88 | } 89 | if err = xml.NewDecoder(resp.Body).Decode(&mr.res); err != nil { 90 | return 91 | } 92 | return 93 | } 94 | 95 | func (mr *mchReq) sign() (err error) { 96 | if mr.sendData == nil { 97 | return errors.New("the data to be sign is not set") 98 | } 99 | 100 | vf := reflect.ValueOf(mr.sendData) 101 | if vf.Kind() != reflect.Ptr { 102 | return errors.New("the send data must be ptr") 103 | } 104 | 105 | if vf.Elem().FieldByName("MchBase").IsValid() { 106 | var base = vf.Elem().FieldByName("MchBase") 107 | base.FieldByName("MchId").SetString(mr.account.MchId) 108 | base.FieldByName("AppId").SetString(mr.appId) 109 | base.FieldByName("NonceStr").SetString(NewRandStr(32)) 110 | 111 | var sign string 112 | if mr.isHmacSign { 113 | base.FieldByName("SignType").SetString(mch_api.MchSignTypeHMACSHA256) 114 | sign = mr.account.signHmacSha256(mr.sendData) 115 | } else { 116 | sign = mr.account.signMd5(mr.sendData) 117 | } 118 | base.FieldByName("Sign").SetString(sign) 119 | } else if vf.Elem().FieldByName("Sign").IsValid() && vf.Elem().FieldByName("NonceStr").IsValid() { 120 | vf.Elem().FieldByName("NonceStr").SetString(NewRandStr(32)) 121 | var sign string 122 | if mr.isHmacSign { 123 | if vf.Elem().FieldByName("SignType").IsValid() { 124 | vf.Elem().FieldByName("SignType").SetString(mch_api.MchSignTypeHMACSHA256) 125 | sign = mr.account.signHmacSha256(mr.sendData) 126 | } 127 | } else { 128 | sign = mr.account.signMd5(mr.sendData) 129 | } 130 | vf.Elem().FieldByName("Sign").SetString(sign) 131 | } 132 | 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /mch_req_v3.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "crypto/sha256" 12 | "encoding/json" 13 | "errors" 14 | "fmt" 15 | "github.com/blusewang/wx/mch_api_v3" 16 | "io/ioutil" 17 | "log" 18 | "net/http" 19 | "path" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | var ( 25 | // wechatPayCerts 微信支付官方证书缓存 26 | wechatPayCerts = NewPayCerManager() 27 | ) 28 | 29 | // 商户请求 30 | type mchReqV3 struct { 31 | account MchAccount 32 | api mch_api_v3.MchApiV3 33 | sendData interface{} 34 | res interface{} 35 | err error 36 | } 37 | 38 | // Send 填充POST里的Body数据 39 | func (mr *mchReqV3) Send(data interface{}) *mchReqV3 { 40 | mr.sendData = data 41 | return mr 42 | } 43 | 44 | // Bind 绑定请求结果的解码数据体 45 | func (mr *mchReqV3) Bind(data interface{}) *mchReqV3 { 46 | mr.res = data 47 | return mr 48 | } 49 | 50 | // Do 执行 51 | func (mr *mchReqV3) Do(method string) (err error) { 52 | if wechatPayCerts.IsEmpty() { 53 | if err = mr.account.DownloadV3Cert(); err != nil { 54 | return 55 | } 56 | } 57 | var buf = new(bytes.Buffer) 58 | if mr.sendData != nil { 59 | if err = json.NewEncoder(buf).Encode(mr.sendData); err != nil { 60 | return 61 | } 62 | } 63 | api := fmt.Sprintf("https://api.mch.weixin.qq.com/v3/%v", mr.api) 64 | if strings.HasPrefix(string(mr.api), "http") { 65 | api = string(mr.api) 66 | } 67 | var cli = client() 68 | req, err := http.NewRequest(method, api, bytes.NewReader(buf.Bytes())) 69 | if err != nil { 70 | return 71 | } 72 | if err = mr.sign(req, buf.Bytes()); err != nil { 73 | return 74 | } 75 | resp, err := cli.Do(req) 76 | defer resp.Body.Close() 77 | if err != nil { 78 | return 79 | } 80 | raw, err := ioutil.ReadAll(resp.Body) 81 | if err != nil { 82 | return 83 | } 84 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNoContent { 85 | var rs mch_api_v3.ErrorResp 86 | _ = json.Unmarshal(raw, &rs) 87 | return errors.New(rs.Message) 88 | } 89 | if mr.api != mch_api_v3.OtherCertificates { 90 | if err = mr.account.VerifyV3(resp.Header, raw); err != nil { 91 | return 92 | } 93 | } 94 | if resp.StatusCode == http.StatusOK { 95 | if mr.res != nil { 96 | return json.Unmarshal(raw, &mr.res) 97 | } else { 98 | return 99 | } 100 | } else { 101 | return 102 | } 103 | } 104 | 105 | // Upload 上传图片视频 106 | func (mr *mchReqV3) Upload(fileName string, raw []byte) (err error) { 107 | // 准备证书 108 | if wechatPayCerts.IsEmpty() { 109 | if err = mr.account.DownloadV3Cert(); err != nil { 110 | return 111 | } 112 | } 113 | // 准备描述 114 | s := sha256.New() 115 | s.Write(raw) 116 | meta, err := json.Marshal(H{"filename": fileName, "sha256": fmt.Sprintf("%X", s.Sum(nil))}) 117 | if err != nil { 118 | return 119 | } 120 | // 开始构建请求体 121 | // 微信不承认golang 自带的 multipart 规范 122 | var body = new(bytes.Buffer) 123 | var nonce = NewRandStr(23) 124 | var boundary = fmt.Sprintf("--%v\r\n", nonce) 125 | body.WriteString(boundary) 126 | body.WriteString("Content-Disposition: form-data; name=\"meta\";\r\n") 127 | body.WriteString("Content-Type: application/json\r\n\r\n") 128 | body.Write(meta) 129 | body.WriteString("\r\n") 130 | body.WriteString(boundary) 131 | body.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"file\"; filename=\"%v\";\r\n", fileName)) 132 | body.WriteString(fmt.Sprintf("Content-Type: image/%v\r\n\r\n", path.Ext(fileName))) 133 | body.Write(raw) 134 | body.WriteString("\r\n") 135 | body.WriteString(fmt.Sprintf("--%v--\r\n", nonce)) 136 | 137 | // 构建API地址 138 | api := fmt.Sprintf("https://api.mch.weixin.qq.com/v3/%v", mr.api) 139 | if strings.HasPrefix(string(mr.api), "http") { 140 | api = string(mr.api) 141 | } 142 | // 构建请求 143 | req, err := http.NewRequest(http.MethodPost, api, bytes.NewReader(body.Bytes())) 144 | if err != nil { 145 | return 146 | } 147 | // 签名 148 | if err = mr.sign(req, meta); err != nil { 149 | return 150 | } 151 | req.Header.Set("Content-Type", "multipart/form-data") 152 | // 网络操作 153 | resp, err := client().Do(req) 154 | defer resp.Body.Close() 155 | if err != nil { 156 | return 157 | } 158 | 159 | // 处理结果 160 | raw, err = ioutil.ReadAll(resp.Body) 161 | if err != nil { 162 | return 163 | } 164 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNoContent { 165 | var rs mch_api_v3.ErrorResp 166 | _ = json.Unmarshal(raw, &rs) 167 | log.Println(api, rs, resp.Header.Get("Request-ID")) 168 | return errors.New(fmt.Sprintf("%v | Request-ID:%v", rs.Message, resp.Header.Get("Request-ID"))) 169 | } 170 | if err = mr.account.VerifyV3(resp.Header, raw); err != nil { 171 | return 172 | } 173 | if resp.StatusCode == http.StatusOK { 174 | return json.Unmarshal(raw, &mr.res) 175 | } else { 176 | return 177 | } 178 | } 179 | 180 | func (mr *mchReqV3) sign(request *http.Request, body []byte) (err error) { 181 | request.Header.Set("User-Agent", "Gdb/1.0") 182 | request.Header.Set("Content-Type", "application/json") 183 | request.Header.Set("Accept", "application/json") 184 | if !wechatPayCerts.IsEmpty() { 185 | request.Header.Set("Wechatpay-Serial", wechatPayCerts.GetSerialNo()) 186 | } 187 | if body == nil { 188 | body = make([]byte, 0) 189 | } 190 | nonce := NewRandStr(32) 191 | ts := time.Now().Unix() 192 | sign, err := mr.account.SignBaseV3(fmt.Sprintf("%v\n%v\n%v\n%v\n%v\n", request.Method, 193 | request.URL.RequestURI(), ts, nonce, string(body))) 194 | if err != nil { 195 | return 196 | } 197 | request.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%v",nonce_str="%v",signature="%v",timestamp="%v",serial_no="%X"`, 198 | mr.account.MchId, nonce, sign, ts, mr.account.certX509.SerialNumber)) 199 | return 200 | } 201 | -------------------------------------------------------------------------------- /mch_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "github.com/blusewang/wx/mch_api" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "reflect" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | func TestMchAccount_NewMchReq(t *testing.T) { 21 | log.SetFlags(log.Ltime | log.Lshortfile) 22 | RegisterHook(func(req *http.Request, reqBody []byte, res *http.Response, startAt time.Time, stopAt time.Time, err error) { 23 | log.Println(req, res, err) 24 | raw, _ := ioutil.ReadAll(res.Body) 25 | log.Println(string(raw)) 26 | res.Body = ioutil.NopCloser(bytes.NewReader(raw)) 27 | }) 28 | mch, err := NewMchAccount("", "", nil, nil, nil) 29 | var data mch_api.PayProfitSharingRes 30 | var body = mch_api.PayProfitSharingData{ 31 | TransactionId: "4200000531202004307536721907", 32 | OutOrderNo: "TSF_216144_1065_ye7DvHdSed", 33 | } 34 | _ = body.SerReceivers([]mch_api.PayProfitSharingReceiver{ 35 | { 36 | Type: "", 37 | Account: "", 38 | Amount: 10, 39 | Description: "", 40 | }, 41 | }) 42 | err = mch.NewMchReqWithApp(mch_api.PayProfitSharing, "wxbb4d55eb95f282f4"). 43 | Send(&body). 44 | UseHMacSign(). 45 | UsePrivateCert(). 46 | Bind(&data).Do() 47 | log.Println(err) 48 | log.Println(data) 49 | } 50 | 51 | func TestMchAccount_OrderSign(t *testing.T) { 52 | //var mch MchAccount 53 | var data interface{} = &mch_api.PayUnifiedOrderRes{ 54 | MchBaseResponse: mch_api.MchBaseResponse{ 55 | ReturnCode: "ReturnCode", 56 | ReturnMsg: "ReturnMsg", 57 | }, 58 | MchBase: mch_api.MchBase{ 59 | MchId: "MchId", 60 | AppId: "AppId", 61 | }, 62 | PrepayId: "24wer", 63 | } 64 | vs := reflect.ValueOf(data).Elem() 65 | log.Println(vs.Field(0)) 66 | vs.FieldByName("MchBase").FieldByName("MchId").Set(reflect.ValueOf("asdf")) 67 | log.Println(vs.FieldByName("MchBase").FieldByName("MchId")) 68 | } 69 | -------------------------------------------------------------------------------- /mch_v3_cert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "crypto/x509" 11 | "time" 12 | ) 13 | 14 | type PayCert struct { 15 | SerialNo string `json:"serial_no"` 16 | EffectiveTime time.Time `json:"effective_time"` 17 | ExpireTime time.Time `json:"expire_time"` 18 | cert *x509.Certificate 19 | } 20 | 21 | type PayCertManager struct { 22 | certs []PayCert 23 | } 24 | 25 | func NewPayCerManager() PayCertManager { 26 | pcm := PayCertManager{} 27 | return pcm 28 | } 29 | 30 | func (pcm *PayCertManager) GetCert() *x509.Certificate { 31 | for _, cert := range pcm.certs { 32 | if cert.ExpireTime.After(time.Now()) { 33 | return cert.cert 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | func (pcm *PayCertManager) GetCertBySerialNo(no string) *x509.Certificate { 40 | for _, cert := range pcm.certs { 41 | if cert.SerialNo == no { 42 | return cert.cert 43 | } 44 | } 45 | return nil 46 | } 47 | func (pcm *PayCertManager) GetSerialNo() string { 48 | for _, cert := range pcm.certs { 49 | if cert.ExpireTime.After(time.Now()) { 50 | return cert.SerialNo 51 | } 52 | } 53 | return "" 54 | } 55 | 56 | func (pcm *PayCertManager) IsEmpty() bool { 57 | return len(pcm.certs) == 0 58 | } 59 | 60 | func (pcm *PayCertManager) Add(pc PayCert) { 61 | pcm.certs = append(pcm.certs, pc) 62 | } 63 | -------------------------------------------------------------------------------- /mch_v3_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "crypto" 12 | "crypto/aes" 13 | "crypto/cipher" 14 | rand2 "crypto/rand" 15 | "crypto/rsa" 16 | "crypto/sha1" 17 | "crypto/sha256" 18 | "crypto/x509" 19 | "encoding/base64" 20 | "encoding/json" 21 | "encoding/pem" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "mime/multipart" 26 | "net/http" 27 | "testing" 28 | "time" 29 | ) 30 | 31 | func TestNewRandStrs(t *testing.T) { 32 | log.SetFlags(log.Ltime | log.Lshortfile) 33 | buf := new(bytes.Buffer) 34 | f := multipart.NewWriter(buf) 35 | r, err := f.CreateFormField("meta") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if err = json.NewEncoder(r).Encode(H{"filename": "x.jpg", "sha256": "435dfslkdjfa;sldkfja;sdf"}); err != nil { 40 | t.Fatal(err) 41 | } 42 | r, err = f.CreateFormFile("file", "file.jpg") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | _, _ = r.Write([]byte("asdfasdf")) 47 | log.Println(buf.String()) 48 | } 49 | func TestNewRandStr(t *testing.T) { 50 | log.SetFlags(log.Ltime | log.Lshortfile) 51 | mchId := "1276387801" 52 | sslCrt := []byte("-----BEGIN CERTIFICATE-----\nMIID8DCCAtigAwIBAgIUXbQoC1THyO2teqJpuMEqWWl5iwcwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjAxMjMxMDYxNzMwWhcNMjUxMjMwMDYxNzMwWjCBgTETMBEGA1UEAwwK\nMTI3NjM4NzgwMTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL\nDCTljJfkuqzlo7nmraXmk43kvZznp5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYM\nAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKwbbmr7yld9XUd6THxVx47bBYLriEC2/Y89o+ohblvJj4N2DmF5cJWA\naRRs9i1l9zcGgxQvkufxyf88/KOh4wckmEbBsS/ozbJ2v0W8Ft30Kcf6UL0/Kod0\ni3j/pwgDlJcS0X6UTCByIeCDm0m/RKFGQWUSZy6Gt7zE8KdWucLCaPSfO3RHEJfc\n50isGqdMtoU2nJqkiD71KZUNZMFoc55SNzN08cHCYpfysMhMvtaBcmFTtK/u4fru\n4RCOHdOXq5OzUhb4wvuscLzDfwfz1ZxCnq5GepQV0y7JL9o4XGcNqlYSsuT+0tOU\nQ1/eYkD6DLizXfkLo6AfR0eMdR/zVEMCAwEAAaOBgTB/MAkGA1UdEwQCMAAwCwYD\nVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0dHA6Ly9ldmNhLml0cnVzLmNv\nbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1\nNDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsFAAOCAQEAE2gzOwbl5NE7QvRq\nqhXfW6UDA4cTDTZ5HRojNhdM6YyFLwXnIXngVH+aNH4AlpJ3/VczUHIv5T6+GheE\nGROeQO/Iouv21lTX+bS/Y72bBlwLwfwkRGUogmsbGH8szJuPLamkbaOoA2HGaCOu\nQLNdaYTGlpXOk69w7zWV7YMb7Tq2i1ACi5lYCMeaNgM697kQKKoNdcka6OoZeBff\nczwbbLVtxN+a75rgLZWhG1a5suh/Stte5EWe3dZcWjVtyPbMpBjYhAjg5byeAZVk\nntBPwx708DkrDCFNmnk+DV2Z6rKWA0axJb95YxPDdlSb6ofER1KtjTzL9bHzVho2\n2y+Rqw==\n-----END CERTIFICATE-----") 53 | sslKey := []byte("-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsG25q+8pXfV1H\nekx8VceO2wWC64hAtv2PPaPqIW5byY+Ddg5heXCVgGkUbPYtZfc3BoMUL5Ln8cn/\nPPyjoeMHJJhGwbEv6M2ydr9FvBbd9CnH+lC9PyqHdIt4/6cIA5SXEtF+lEwgciHg\ng5tJv0ShRkFlEmcuhre8xPCnVrnCwmj0nzt0RxCX3OdIrBqnTLaFNpyapIg+9SmV\nDWTBaHOeUjczdPHBwmKX8rDITL7WgXJhU7Sv7uH67uEQjh3Tl6uTs1IW+ML7rHC8\nw38H89WcQp6uRnqUFdMuyS/aOFxnDapWErLk/tLTlENf3mJA+gy4s135C6OgH0dH\njHUf81RDAgMBAAECggEAJVMppjAHGORKR4chcVGVHsknL9ZuzUIiSV9f3hXz/hn/\ncs42njMdFH8tys06smvLqnZSFR2gKYdJfH44eDBSsSjhkW7OQ4qkmZChOLlq6CXc\nrc7+lZxOV+QRn2MqUVWdcwoUvvPgcqTt7ef81IiTlLpM0mOkVvXGgTzgyBnJ3Y+6\nvPgALGAWTVxngyDkyK1BbAr4zeMweX5uiC0daCkKBNYIOJUDGtBIeu2JYOo63/s8\ng4RcgYOL1rYmAZM6bfhdI4uAn6IQN8nHtcUlwCkrTRkECQb7g8oh5ysXpSFkA59x\ngzowQQbo9cJoPLsldyn5Us3oVUILrnDHOE/yEWhGcQKBgQDgiAZhSDSxIKVbRdUq\n1Waq4+1V6Odb120CjZvXk5o0/nr0D+xIp05r5CKx33m4jAMj5EV4w8QDLSbmub0z\nA16yf3Q2KL12X7V9P7lRIttJaGYcSARVoIHm+Srvr/3eooAB0LxsWrMQB6ErihW9\nUS5ZK3/GIKoJxGvqueMTk/M+WwKBgQDEOnmr5LRuB3EYtW+kMlUaic9W/1VY/TFM\nt3EpqNE/1VpHN2WB5SGlH6qGoymFO8adVH7cVjgjekwZQIynI9GHqUN7n8vhtmyB\nH1PbDbtPlqZT7bS09X5QcGPuo9xXdOpcqV2iNz265T/XR1vHNRQnUQ5uvVbCUlpx\nqVMZJs32OQKBgQDAAvNZzDLratyd8lk6eSaEa8iyGCuKOe8KKOml8J8GRL4G63sI\nIrOIxp74+ACS1oF09yiF/vwoLzu+QgbPkkkwYpiSHELx8SU2iAFFpoZa/4GbG+dB\nBrMwP9L9CMcU1mibpNMN4n6Q7cVhg4PV04/MR8vMNnDTS3tyTycmvfZdUwKBgGgt\nBzVb2PJlHwTYJioM0qOhMCNmsP/qg4bQCNLuHhD+iswuO8SnSaJpWlXaP4vNPVd/\naU4+s9UZ81agr0t4t5+HHB2Aq3PsLlSqthEgjCXnu+vo0bwUbPf1gwhJlAwWNOn2\nvJAHNc2IMclvx+jNZCKvZLMj7/CAWiXnmAdNU6D5AoGAN4c0EsKkmSdKuHmwJQLI\nziHuMFfTf8ahsLAEPabHwe3OZM0pbGHsxp0Yd+98jN8nyEgraES1r4scKi8Ksg+y\nSX9fqtlwwvQHP6/INNTOcRviCUCuh4+kD7jhz0bENu1Oa/u9/jDUatMA2LAtoPVx\nsY96UkR0maga41wGvMW5UdY=\n-----END PRIVATE KEY-----") 54 | 55 | cb, _ := pem.Decode(sslCrt) 56 | pubKey, err := x509.ParseCertificate(cb.Bytes) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | cb, _ = pem.Decode(sslKey) 61 | priKey, err := x509.ParsePKCS8PrivateKey(cb.Bytes) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | h := sha256.New() 67 | ts := time.Now().Unix() 68 | rs := NewRandStr(32) 69 | 70 | req, err := http.NewRequest(http.MethodGet, "https://api.mch.weixin.qq.com/v3/certificates", nil) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | str := fmt.Sprintf("%v\n%v\n%v\n%v\n%v\n", req.Method, req.URL.Path, ts, rs, "") 76 | h.Write([]byte(str)) 77 | signRaw, err := rsa.SignPKCS1v15(rand2.Reader, priKey.(*rsa.PrivateKey), crypto.SHA256, h.Sum(nil)) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | req.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%v",nonce_str="%v",signature="%v",timestamp="%v",serial_no="%X"`, 83 | mchId, rs, base64.StdEncoding.EncodeToString(signRaw), ts, pubKey.SerialNumber)) 84 | req.Header.Set("User-Agent", "Gdb/1.0") 85 | req.Header.Set("Content-Type", "application/json") 86 | req.Header.Set("Accept", "application/json") 87 | resp, err := client().Do(req) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | raw, _ := ioutil.ReadAll(resp.Body) 92 | log.Println(resp.Status, string(raw)) 93 | 94 | str = fmt.Sprintf("%v\n%v\n%v\n", 95 | resp.Header.Get("Wechatpay-Timestamp"), 96 | resp.Header.Get("Wechatpay-Nonce"), string(raw)) 97 | signRaw, err = base64.StdEncoding.DecodeString(resp.Header.Get("Wechatpay-Signature")) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | log.Println(pubKey.PublicKey.(*rsa.PublicKey)) 103 | log.Printf("pubKey.SerialNumber: %X\n", pubKey.SerialNumber) 104 | h.Reset() 105 | h.Write([]byte(str)) 106 | err = rsa.VerifyPKCS1v15(pubKey.PublicKey.(*rsa.PublicKey), crypto.SHA256, h.Sum(nil), signRaw) 107 | if err != nil { 108 | log.Println(">>>>>>>>>>", err) 109 | } 110 | 111 | for s, strings := range resp.Header { 112 | log.Println("resp.Header ->", s, strings) 113 | } 114 | 115 | var res struct { 116 | Data []struct { 117 | EffectiveTime time.Time `json:"effective_time"` 118 | EncryptCertificate struct { 119 | Algorithm string `json:"algorithm"` 120 | AssociatedData string `json:"associated_data"` 121 | Ciphertext string `json:"ciphertext"` 122 | Nonce string `json:"nonce"` 123 | } `json:"encrypt_certificate"` 124 | ExpireTime time.Time `json:"expire_time"` 125 | SerialNo string `json:"serial_no"` 126 | } `json:"data"` 127 | } 128 | if err = json.Unmarshal(raw, &res); err != nil { 129 | t.Fatal(err) 130 | } 131 | log.Println(res) 132 | // AEAD_AES_256_GCM 133 | cipherRaw, _ := base64.StdEncoding.DecodeString(res.Data[0].EncryptCertificate.Ciphertext) 134 | block, err := aes.NewCipher([]byte("14da084a425129ed376084a49caae96b")) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | gcm, err := cipher.NewGCM(block) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | plain, err := gcm.Open(nil, []byte(res.Data[0].EncryptCertificate.Nonce), cipherRaw, []byte(res.Data[0].EncryptCertificate.AssociatedData)) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | log.Println(string(plain)) 147 | raw, err = rsa.EncryptOAEP(sha1.New(), rand2.Reader, pubKey.PublicKey.(*rsa.PublicKey), []byte("name"), nil) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | log.Println(base64.StdEncoding.EncodeToString(raw)) 152 | } 153 | 154 | func TestLimitString2(t *testing.T) { 155 | log.Printf("%x", sha256.Sum256([]byte("abcd\n"))) 156 | } 157 | 158 | func TestClientMiddleware(t *testing.T) { 159 | log.SetFlags(log.Ltime | log.Lshortfile) 160 | log.Println(_hook == nil) 161 | cli := &http.Client{Transport: &mt{http.Transport{}}} 162 | log.Println(cli.Get("https://cashier.mywsy.cn")) 163 | } 164 | -------------------------------------------------------------------------------- /mp.go: -------------------------------------------------------------------------------- 1 | package wx 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/xml" 6 | "fmt" 7 | "github.com/blusewang/wx/mp_api" 8 | "github.com/youkale/go-querystruct/params" 9 | "net/http" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // MpAccount 应用账号 15 | // ServerHost 默认为:mp_api.ServerHostUniversal 16 | type MpAccount struct { 17 | AppId string `json:"app_id"` 18 | AccessToken string `json:"access_token"` 19 | AppSecret string `json:"app_secret"` 20 | PrivateToken string `json:"private_token"` 21 | EncodingAESKey string `json:"encoding_aes_key"` 22 | JsSdkTicket string `json:"js_sdk_ticket"` 23 | ComponentVerifyTicket *string `json:"component_verify_ticket"` 24 | ServerHost mp_api.ServerHost `json:"server_host"` 25 | } 26 | 27 | // ReadMessage 读取通知消息 28 | func (ma MpAccount) ReadMessage(req *http.Request) (q mp_api.MessageQuery, msg mp_api.MessageData, err error) { 29 | if err = params.Unmarshal(req.URL.Query(), &q); err != nil { 30 | return 31 | } 32 | if q.EchoStr != "" { 33 | return 34 | } 35 | if err = q.Validate(ma.PrivateToken); err != nil { 36 | return 37 | } 38 | if err = xml.NewDecoder(req.Body).Decode(&msg); err != nil { 39 | return 40 | } 41 | if msg.Encrypt != "" { 42 | if err = msg.ShouldDecode(ma.EncodingAESKey); err != nil { 43 | return 44 | } 45 | } 46 | return 47 | } 48 | 49 | // UrlSign 微信网页的网址签名 50 | func (ma MpAccount) UrlSign(u string) (d H) { 51 | data := make(H) 52 | data["noncestr"] = NewRandStr(32) 53 | data["jsapi_ticket"] = ma.JsSdkTicket 54 | data["timestamp"] = time.Now().Unix() 55 | data["url"] = u 56 | d = make(H) 57 | d["appId"] = ma.AppId 58 | d["timestamp"] = data["timestamp"] 59 | d["nonceStr"] = data["noncestr"] 60 | 61 | str := mapSortByKey(data) 62 | d["signature"] = strings.ToUpper(fmt.Sprintf("%x", sha1.Sum([]byte(str)))) 63 | d["jsApiList"] = []string{} 64 | return 65 | } 66 | 67 | // NewMpReq 新建一个请求 68 | func (ma MpAccount) NewMpReq(path mp_api.MpApi) *mpReq { 69 | return &mpReq{account: ma, path: path} 70 | } 71 | -------------------------------------------------------------------------------- /mp_api/account.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mp_api 8 | 9 | type AccountQrScene struct { 10 | SceneId int64 `json:"scene_id,omitempty"` 11 | SceneStr string `json:"scene_str,omitempty"` 12 | } 13 | type AccountQrActionInfo struct { 14 | Scene AccountQrScene `json:"scene"` 15 | } 16 | 17 | type AccountQrCreateData struct { 18 | ExpireSeconds int64 `json:"expire_seconds,omitempty"` 19 | ActionName QrActionType `json:"action_name"` 20 | ActionInfo AccountQrActionInfo `json:"action_info"` 21 | } 22 | 23 | type AccountQrCreateRes struct { 24 | MpBaseResp 25 | Ticket string `json:"ticket"` 26 | ExpireSeconds int64 `json:"expire_seconds,omitempty"` 27 | Url string `json:"url"` 28 | } 29 | 30 | type AccountShortUrlData struct { 31 | Action string `json:"action"` 32 | LongUrl string `json:"long_url"` 33 | } 34 | 35 | type AccountShortUrlRes struct { 36 | MpBaseResp 37 | ShortUrl string `json:"short_url"` 38 | } 39 | -------------------------------------------------------------------------------- /mp_api/basic_information.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type BasicInformationTokenQuery struct { 4 | GrantType string `url:"grant_type"` 5 | AppId string `url:"appid"` 6 | Secret string `url:"secret"` 7 | } 8 | 9 | type BasicInformationTokenRes struct { 10 | MpBaseResp 11 | AccessToken string `json:"access_token"` 12 | ExpiresIn int64 `json:"expires_in"` 13 | } 14 | 15 | type GetApiDomainIpRes struct { 16 | MpBaseResp 17 | IpList []string `json:"ip_list"` 18 | } 19 | 20 | type CallbackCheckData struct { 21 | Action string `json:"action"` 22 | CheckOperator string `json:"check_operator"` 23 | } 24 | 25 | type CallbackCheckRes struct { 26 | MpBaseResp 27 | Dns []struct { 28 | Ip string `json:"ip"` 29 | RealOperator string `json:"real_operator"` 30 | } `json:"dns"` 31 | Ping []struct { 32 | Ip string `json:"ip"` 33 | FromOperator string `json:"from_operator"` 34 | PackageLoss string `json:"package_loss"` 35 | Time string `json:"time"` 36 | } `json:"ping"` 37 | } 38 | -------------------------------------------------------------------------------- /mp_api/constant.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type ServerHost string 4 | 5 | const ( 6 | // 服务器类型 7 | 8 | // ServerHostUniversal 通用域名 9 | ServerHostUniversal = "api.weixin.qq.com" 10 | // ServerHostUniversal2 通用异地容灾域名 11 | ServerHostUniversal2 = "api2.weixin.qq.com" 12 | // ServerHostShangHai 上海域名 13 | ServerHostShangHai = "sh.api.weixin.qq.com" 14 | // ServerHostShenZhen 深圳域名 15 | ServerHostShenZhen = "sz.api.weixin.qq.com" 16 | // ServerHostHK 香港域名 17 | ServerHostHK = "hk.api.weixin.qq.com" 18 | ) 19 | 20 | type MpApi string 21 | 22 | const ( 23 | // 开始开发 24 | 25 | // BasicInformationToken 获取Access token 26 | BasicInformationToken = "cgi-bin/token" 27 | // BasicInformationApiDomainIp 获取微信服务器IP地址 28 | BasicInformationApiDomainIp = "cgi-bin/get_api_domain_ip" 29 | // BasicInformationCallbackCheck 网络检测 30 | BasicInformationCallbackCheck = "cgi-bin/callback/check" 31 | 32 | // 自定义菜单 33 | 34 | // CustomMenuCreate 创建自定义菜单 35 | CustomMenuCreate = "cgi-bin/menu/create" 36 | // CustomMenuCurrentSelfMenuInfo 查询自定义菜单 37 | CustomMenuCurrentSelfMenuInfo = "cgi-bin/get_current_selfmenu_info" 38 | // CustomMenuDelete 删除默认菜单及全部个性化菜单 39 | CustomMenuDelete = "cgi-bin/menu/delete" 40 | 41 | // 消息 42 | 43 | // MessageCustomServiceKfAccountAdd 添加客服 44 | MessageCustomServiceKfAccountAdd = "customservice/kfaccount/add" 45 | // MessageCustomServiceKfAccountUpdate 修改客服 46 | MessageCustomServiceKfAccountUpdate = "customservice/kfaccount/update" 47 | // MessageCustomServiceKfAccountDel 删除客服 48 | MessageCustomServiceKfAccountDel = "customservice/kfaccount/del" 49 | // MessageCustomServiceKfAccountUploadHeadImg 上传客服头像 50 | MessageCustomServiceKfAccountUploadHeadImg = "customservice/kfaccount/uploadheadimg" 51 | // MessageCustomServiceKfList 获取所有客服 52 | MessageCustomServiceKfList = "cgi-bin/customservice/getkflist" 53 | // MessageCustomSend 客服接口-发消息 54 | MessageCustomSend = "cgi-bin/message/custom/send" 55 | // MessageTemplateSend 发送模板消息 56 | MessageTemplateSend = "cgi-bin/message/template/send" 57 | // MessageMassSend 根据OpenID列表群发 58 | MessageMassSend = "cgi-bin/message/mass/send" 59 | 60 | // 媒体文件上传 61 | 62 | // MediaUploadImg 上传图文消息内的图片获取URL 63 | MediaUploadImg = "cgi-bin/media/uploadimg" 64 | // MediaUpload 新增临时素材 65 | MediaUpload = "cgi-bin/media/upload" 66 | 67 | // 微信网页开发 68 | 69 | // OaWebAppsSnsAuth2AccessToken 通过code换取网页授权access_token 70 | OaWebAppsSnsAuth2AccessToken = "sns/oauth2/access_token" 71 | // OaWebAppsSnsUserInfo 拉取用户信息(需scope为 snsapi_userinfo) 72 | OaWebAppsSnsUserInfo = "sns/userinfo" 73 | // OaWebAppsJsSDKTicket 获取JsSDK ticket 74 | OaWebAppsJsSDKTicket = "cgi-bin/ticket/getticket" 75 | 76 | // 用户管理 77 | 78 | // UserTagsCreate 创建标签 79 | UserTagsCreate = "cgi-bin/tags/create" 80 | // UserTagsGet 获取公众号已创建的标签 81 | UserTagsGet = "cgi-bin/tags/get" 82 | // UserTagsUpdate 编辑标签 83 | UserTagsUpdate = "cgi-bin/tags/update" 84 | // UserTagsDelete 删除标签 85 | UserTagsDelete = "cgi-bin/tags/delete" 86 | // UserTagGet 获取标签下粉丝列表 87 | UserTagGet = "cgi-bin/user/tag/get" 88 | // UserTagMembersBatch 批量为用户打标签 89 | UserTagMembersBatch = "cgi-bin/tags/members/batchtagging" 90 | // UserTagMembersBatchUnTag 批量为用户取消标签 91 | UserTagMembersBatchUnTag = "cgi-bin/tags/members/batchuntagging" 92 | // UserTagsGetIdList 获取用户身上的标签列表 93 | UserTagsGetIdList = "cgi-bin/tags/getidlist" 94 | // UserInfoUpdateRemark 用户设置备注名 95 | UserInfoUpdateRemark = "cgi-bin/user/info/updateremark" 96 | // UserInfo 获取用户基本信息(包括UnionID机制) 97 | UserInfo = "cgi-bin/user/info" 98 | // UserInfoBatchGet 批量获取用户基本信息 99 | UserInfoBatchGet = "cgi-bin/user/info/batchget" 100 | // UserGet 获取关注者列表 101 | UserGet = "cgi-bin/user/get" 102 | 103 | // 账号管理 104 | 105 | // AccountQrCreate 二维码 106 | AccountQrCreate = "cgi-bin/qrcode/create" 107 | // AccountShortUrl 长链接转成短链接 108 | AccountShortUrl = "cgi-bin/shorturl" 109 | 110 | // 对话能力 111 | 112 | // GuideAccountAdd 添加顾问 113 | GuideAccountAdd = "cgi-bin/guide/addguideacct" 114 | // GuideAddBuyer 为顾问分配客户 115 | GuideAddBuyer = "cgi-bin/guide/addguidebuyerrelation" 116 | 117 | // 小程序 118 | 119 | // SnsJsCode2Session 登录凭证校验 120 | SnsJsCode2Session = "sns/jscode2session" 121 | // WXACode 获取小程序码 122 | WXACode = "wxa/getwxacode" 123 | // WXACodeUnLimit 获取不限制的小程序码 124 | WXACodeUnLimit = "wxa/getwxacodeunlimit" 125 | // WXAQrCode 获取小程序二维码 126 | WXAQrCode = "cgi-bin/wxaapp/createwxaqrcode" 127 | 128 | // OCR 129 | 130 | // OcrBandCard 银行卡识别 131 | OcrBandCard = "cv/ocr/bankcard" 132 | // OcrBusinessLicense 营业执照识别 133 | OcrBusinessLicense = "cv/ocr/bizlicense" 134 | // OcrDrivingLicense 营业执照识别 135 | OcrDrivingLicense = "cv/ocr/drivinglicense" 136 | // OcrIdCard 身份证识别 137 | OcrIdCard = "cv/ocr/idcard" 138 | // OcrText 普通文字识别 139 | OcrText = "cv/ocr/comm" 140 | ) 141 | 142 | type MessageCustomSendType string 143 | 144 | const ( 145 | MessageCustomSendTypeText = "text" 146 | MessageCustomSendTypeImage = "image" 147 | MessageCustomSendTypeVideo = "video" 148 | MessageCustomSendTypeMusic = "music" 149 | MessageCustomSendTypeNews = "news" 150 | MessageCustomSendTypeMpNews = "mpnews" 151 | MessageCustomSendTypeMsgMenu = "msgmenu" 152 | MessageCustomSendTypeWxCard = "wxcard" 153 | MessageCustomSendTypeMiniProgramPage = "miniprogrampage" 154 | ) 155 | 156 | type MessageMassSendType string 157 | 158 | const ( 159 | MessageMassSendTypeMpNews = "mpnews" 160 | MessageMassSendTypeText = "text" 161 | MessageMassSendTypeVoice = "voice" 162 | MessageMassSendTypeImages = "images" 163 | MessageMassSendTypeMpVideo = "mpvideo" 164 | MessageMassSendTypeWxCard = "wxcard" 165 | ) 166 | 167 | type MediaType string 168 | 169 | const ( 170 | MediaTypeImage = "image" 171 | MediaTypeVoice = "voice" 172 | MediaTypeVideo = "video" 173 | MediaTypeThumb = "thumb" 174 | ) 175 | 176 | type QrActionType string 177 | 178 | const ( 179 | QrActionTypeScene = "QR_SCENE" 180 | QrActionTypeStrScene = "QR_STR_SCENE" 181 | QrActionTypeLimitScene = "QR_LIMIT_SCENE" 182 | QrActionTypeLimitStrScene = "QR_LIMIT_STR_SCENE" 183 | ) 184 | 185 | const ShortUrlAction = "long2short" 186 | 187 | type TokenGrantType string 188 | 189 | const ( 190 | TokenGrantTypeClientCredential = "client_credential" 191 | TokenGrantTypeAuthCode = "authorization_code" 192 | ) 193 | 194 | type JsSDKTicketType string 195 | 196 | const ( 197 | JsSDKTicketTypeJSAPI = "jsapi" 198 | JsSDKTicketTypeWxCard = "wx_card" 199 | ) 200 | -------------------------------------------------------------------------------- /mp_api/custom_menus.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type MenuButton struct { 4 | Type string `json:"type"` 5 | Name string `json:"name"` 6 | Key string `json:"key,omitempty"` 7 | Url string `json:"url,omitempty"` 8 | MediaId string `json:"media_id,omitempty"` 9 | AppId string `json:"appid,omitempty"` 10 | PagePath string `json:"pagepath,omitempty"` 11 | SubButton []MenuButton `json:"sub_button,omitempty"` 12 | } 13 | 14 | type MenuCreateData struct { 15 | Button []MenuButton `json:"button"` 16 | } 17 | 18 | type CustomMenuCurrentSelfMenuInfoRes struct { 19 | MpBaseResp 20 | IsMenuOpen int64 `json:"is_menu_open"` 21 | SelfMenuInfo struct { 22 | Button []MenuButton `json:"button"` 23 | } `json:"selfmenu_info"` 24 | } 25 | -------------------------------------------------------------------------------- /mp_api/guide.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mp_api 8 | 9 | type GuideAccountAddData struct { 10 | GuideAccount string `json:"guide_account,omitempty"` 11 | GuideOpenId string `json:"guide_openid,omitempty"` 12 | GuideHeadImgUrl string `json:"guide_headimgurl,omitempty"` 13 | GuideNickname string `json:"guide_nickname,omitempty"` 14 | } 15 | 16 | type GuideBuyer struct { 17 | OpenId string `json:"openid,omitempty"` 18 | BuyerNickname string `json:"buyer_nickname,omitempty"` 19 | } 20 | 21 | type GuideAddBuyerData struct { 22 | GuideAccount string `json:"guide_account,omitempty"` 23 | GuideOpenid string `json:"guide_openid,omitempty"` 24 | GuideBuyer 25 | BuyerList []GuideBuyer `json:"buyer_list,omitempty"` 26 | } 27 | -------------------------------------------------------------------------------- /mp_api/media.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type MediaUploadImgRes struct { 4 | MpBaseResp 5 | Url string `json:"url"` 6 | } 7 | 8 | type MediaUploadQuery struct { 9 | Type MediaType `url:"type"` 10 | } 11 | type MediaUploadRes struct { 12 | MpBaseResp 13 | Type string `json:"type"` 14 | MediaId string `json:"media_id"` 15 | CreatedAt int64 `json:"created_at"` 16 | } 17 | -------------------------------------------------------------------------------- /mp_api/message.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "encoding/xml" 10 | "errors" 11 | "fmt" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | // MessageQuery 消息通知`query`数据 17 | type MessageQuery struct { 18 | Signature string `param:"signature" binding:"required"` 19 | Timestamp string `param:"timestamp" binding:"required"` 20 | Nonce string `param:"nonce" binding:"required"` 21 | EchoStr string `param:"echostr"` 22 | OpenId string `param:"openid"` 23 | EncryptType string `param:"encrypt_type"` 24 | MsgSignature string `param:"msg_signature"` 25 | } 26 | 27 | // Validate 安全验证 28 | func (mq MessageQuery) Validate(PrivateToken string) (err error) { 29 | arr := []string{PrivateToken, mq.Timestamp, mq.Nonce} 30 | sort.Strings(arr) 31 | 32 | sign := fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(arr, "")))) 33 | 34 | if mq.Signature != sign { 35 | err = errors.New("签名验证失败") 36 | } 37 | return 38 | } 39 | 40 | // MessageData 公众号消息 41 | type MessageData struct { 42 | ToUserName string `xml:"ToUserName" json:"to_user_name,omitempty"` 43 | Encrypt string `xml:"Encrypt" json:"encrypt,omitempty"` 44 | FromUserName string `xml:"FromUserName" json:"from_user_name,omitempty"` 45 | CreateTime int64 `xml:"CreateTime" json:"create_time,omitempty"` 46 | MsgType string `xml:"MsgType" json:"msg_type,omitempty"` 47 | Content string `xml:"Content" json:"content,omitempty"` 48 | MsgId int64 `xml:"MsgId" json:"msg_id,omitempty"` 49 | PicUrl string `xml:"PicUrl" json:"pic_url,omitempty"` 50 | MediaId string `xml:"MediaId" json:"media_id,omitempty"` 51 | Format string `xml:"Format" json:"format,omitempty"` 52 | Recognition string `xml:"Recognition" json:"recognition,omitempty"` 53 | ThumbMediaId string `xml:"ThumbMediaId" json:"thumb_media_id,omitempty"` 54 | LocationX float64 `xml:"Location_X" json:"location_x,omitempty"` 55 | LocationY float64 `xml:"Location_Y" json:"location_y,omitempty"` 56 | Scale int64 `xml:"Scale" json:"scale,omitempty"` 57 | Label string `xml:"Label" json:"label,omitempty"` 58 | Title string `xml:"Title" json:"title,omitempty"` 59 | Description string `xml:"Description" json:"description,omitempty"` 60 | Url string `xml:"Url" json:"url,omitempty"` 61 | Event string `xml:"Event" json:"event,omitempty"` 62 | EventKey string `xml:"EventKey" json:"event_key,omitempty"` 63 | Ticket string `xml:"Ticket" json:"ticket,omitempty"` 64 | Latitude float64 `xml:"Latitude" json:"latitude,omitempty"` 65 | Longitude float64 `xml:"Longitude" json:"longitude,omitempty"` 66 | Precision float64 `xml:"Precision" json:"precision,omitempty"` 67 | SessionFrom string `xml:"SessionFrom" json:"session_from,omitempty"` 68 | Status string `xml:"Status" json:"status,omitempty"` 69 | MsgID int64 `xml:"MsgID" json:"msgID,omitempty"` 70 | SentCount int64 `xml:"SentCount" json:"sent_count,omitempty"` 71 | AppId string `xml:"AppId,omitempty" json:"app_id,omitempty"` 72 | InfoType string `xml:"InfoType,omitempty" json:"info_type,omitempty"` 73 | Msg string `xml:"msg,omitempty" json:"msg,omitempty"` 74 | Info MsgInfo `xml:"info,omitempty" json:"info,omitempty"` 75 | } 76 | 77 | type MsgInfo struct { 78 | ComponentVerifyTicket string `xml:"ComponentVerifyTicket,omitempty" json:"component_verify_ticket,omitempty"` 79 | Status2 int64 `xml:"status,omitempty" json:"status_2,omitempty"` 80 | AuthCode string `xml:"auth_code,omitempty" json:"auth_code,omitempty,omitempty"` 81 | Name string `xml:"name,omitempty" json:"name,omitempty"` 82 | Code string `xml:"code,omitempty" json:"code,omitempty"` 83 | CodeType int64 `xml:"code_type,omitempty" json:"codeType,omitempty"` 84 | LegalPersonaWechat string `xml:"legal_persona_wechat,omitempty" json:"legalPersonaWechat,omitempty"` 85 | LegalPersonaName string `xml:"legal_persona_name,omitempty" json:"legalPersonaName,omitempty"` 86 | ComponentPhone string `xml:"component_phone,omitempty" json:"componentPhone,omitempty"` 87 | } 88 | 89 | // ShouldDecode 公众号消息解密 90 | func (msg *MessageData) ShouldDecode(EncodingAESKey string) (err error) { 91 | if msg.Encrypt == "" { 92 | // 没加密 93 | return 94 | } 95 | if msg.FromUserName != "" { 96 | // 解密过了 97 | return 98 | } 99 | 100 | // 读密钥 101 | raw, err := base64.StdEncoding.DecodeString(EncodingAESKey + "=") 102 | if err != nil { 103 | return 104 | } 105 | 106 | // 生成密钥 107 | block, err := aes.NewCipher(raw) 108 | if err != nil { 109 | return 110 | } 111 | // 读密文 112 | raw, err = base64.StdEncoding.DecodeString(msg.Encrypt) 113 | if err != nil { 114 | return 115 | } 116 | if len(raw) < block.BlockSize() { 117 | return errors.New("无效密文") 118 | } 119 | // 解密 120 | cipher.NewCBCDecrypter(block, raw[:block.BlockSize()]).CryptBlocks(raw, raw) 121 | 122 | // 微信格式解码 AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] 123 | _pad := int(raw[len(raw)-1]) 124 | _length := binary.BigEndian.Uint32(raw[16:20]) 125 | raw = raw[:len(raw)-_pad] 126 | 127 | // 取出格式化数据 128 | if err = xml.Unmarshal(raw[20:_length+20], &msg); err != nil { 129 | return 130 | } 131 | msg.AppId = string(raw[_length+20:]) 132 | msg.Encrypt = "" 133 | return 134 | } 135 | 136 | type KfAccountData struct { 137 | KfId int64 `json:"kf_id,omitempty"` 138 | KfAccount string `json:"kf_account"` 139 | NickName string `json:"nickname"` 140 | Password string `json:"password"` 141 | KfHeadImgUrl string `json:"kf_headimgurl,omitempty"` 142 | } 143 | 144 | type MessageCustomServiceKfAccountUploadHeadImgQuery struct { 145 | KfAccount string `url:"kf_account"` 146 | } 147 | 148 | type MessageCustomServiceKfListRes struct { 149 | MpBaseResp 150 | KfList []KfAccountData `json:"kf_list"` 151 | } 152 | 153 | type MessageCustomSendArticle struct { 154 | Title string `json:"title"` 155 | Description string `json:"description"` 156 | Url string `json:"url"` 157 | PicUrl string `json:"picurl"` 158 | } 159 | type MessageCustomSendMsgMenuItem struct { 160 | Id string `json:"id"` 161 | Content string `json:"content"` 162 | } 163 | type MessageCustomSendMsgText struct { 164 | Content string `json:"content"` 165 | } 166 | type MessageCustomSendMsgImage struct { 167 | MediaId string `json:"media_id"` 168 | } 169 | type MessageCustomSendMsgVoice MessageCustomSendMsgImage 170 | type MessageCustomSendMsgVideo struct { 171 | MediaId string `json:"media_id"` 172 | ThumbMediaId string `json:"thumb_media_id"` 173 | Title string `json:"title"` 174 | Description string `json:"description"` 175 | } 176 | type MessageCustomSendMsgMusic struct { 177 | Title string `json:"title"` 178 | Description string `json:"description"` 179 | MusicUrl string `json:"music_url"` 180 | HqMusicUrl string `json:"hq_music_url"` 181 | ThumbMediaId string `json:"thumb_media_id"` 182 | } 183 | type MessageCustomSendMsgNews struct { 184 | Articles []MessageCustomSendArticle `json:"articles"` 185 | } 186 | type MessageCustomSendMsgMpNews MessageCustomSendMsgImage 187 | type MessageCustomSendMsgMenu struct { 188 | HeadContent string `json:"head_content"` 189 | List []MessageCustomSendMsgMenuItem `json:"list"` 190 | TailContent string `json:"tail_content"` 191 | } 192 | type MessageCustomSendMsgWxCard struct { 193 | CardId string `json:"card_id"` 194 | } 195 | type MessageCustomSendMsgMiniProgramPage struct { 196 | Title string `json:"title"` 197 | AppId string `json:"appid"` 198 | PagePath string `json:"pagepath"` 199 | ThumbMediaId string `json:"thumb_media_id"` 200 | } 201 | type MessageCustomSendMsgCustomService struct { 202 | KfAccount string `json:"kf_account"` 203 | } 204 | type MessageCustomSendData struct { 205 | ToUser string `json:"touser"` 206 | MsgType MessageCustomSendType `json:"msgtype"` 207 | Text MessageCustomSendMsgText `json:"text"` 208 | Image MessageCustomSendMsgImage `json:"image"` 209 | Voice MessageCustomSendMsgVoice `json:"voice"` 210 | Video MessageCustomSendMsgVideo `json:"video"` 211 | Music MessageCustomSendMsgMusic `json:"music"` 212 | News MessageCustomSendMsgNews `json:"news"` 213 | MpNews MessageCustomSendMsgMpNews `json:"mpnews"` 214 | MsgMenu MessageCustomSendMsgMenu `json:"msgmenu"` 215 | WxCard MessageCustomSendMsgWxCard `json:"wxcard"` 216 | MiniProgramPage MessageCustomSendMsgMiniProgramPage `json:"miniprogrampage"` 217 | CustomService MessageCustomSendMsgCustomService `json:"customservice"` 218 | } 219 | 220 | type MessageTemplateSendDataItem struct { 221 | Value string `json:"value"` 222 | Color string `json:"color"` 223 | } 224 | type MessageTemplateMiniProgram struct { 225 | AppId string `json:"appid"` 226 | PagePath string `json:"pagepath"` 227 | } 228 | type MessageTemplateSendData struct { 229 | ToUser string `json:"touser"` 230 | TemplateId string `json:"template_id"` 231 | Url string `json:"url"` 232 | MiniProgram MessageTemplateMiniProgram `json:"miniprogram"` 233 | Data map[string]MessageTemplateSendDataItem `json:"data"` 234 | } 235 | 236 | type MessageTemplateSendRes struct { 237 | MpBaseResp 238 | MsgId int64 `json:"msgid"` 239 | } 240 | 241 | type MessageMassSendMediaId struct { 242 | MediaId string `json:"media_id"` 243 | } 244 | type MessageMassSendText struct { 245 | Content string `json:"content"` 246 | } 247 | type MessageMassSendImages struct { 248 | MediaIds []string `json:"media_ids"` 249 | Recommend string `json:"recommend"` 250 | NeedOpenComment int `json:"need_open_comment"` 251 | OnlyFansCanComment int `json:"only_fans_can_comment"` 252 | } 253 | type MessageMassSendMpVideo struct { 254 | MediaId string `json:"media_id"` 255 | Title string `json:"title"` 256 | Description string `json:"description"` 257 | } 258 | type MessageMassSendWxCard struct { 259 | CardId string `json:"card_id"` 260 | } 261 | type MessageMassSendData struct { 262 | ToUser []string `json:"touser"` 263 | MsgType MessageMassSendType `json:"msgtype"` 264 | MpNews *MessageMassSendMediaId `json:"mpnews,omitempty"` 265 | Text *MessageMassSendText `json:"text,omitempty"` 266 | Voice *MessageMassSendMediaId `json:"voice,omitempty"` 267 | Images *MessageMassSendImages `json:"images,omitempty"` 268 | MpVideo *MessageMassSendMpVideo `json:"mpvideo,omitempty"` 269 | WxCard *MessageMassSendWxCard `json:"wxcard,omitempty"` 270 | SendIgnoreReprint int `json:"send_ignore_reprint"` 271 | } 272 | 273 | type MessageMassSendRes struct { 274 | MpBaseResp 275 | MsgId int64 `json:"msg_id"` 276 | MsgDataId int64 `json:"msg_data_id"` 277 | } 278 | -------------------------------------------------------------------------------- /mp_api/mini_program.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mp_api 8 | 9 | type SnsJsCode2SessionQuery struct { 10 | AppId string `url:"appid"` 11 | Secret string `url:"secret"` 12 | JsCode string `url:"js_code"` 13 | GrantType string `url:"grant_type"` 14 | } 15 | 16 | type SnsJsCode2SessionRes struct { 17 | MpBaseResp 18 | OpenId string `json:"openid"` 19 | SessionKey string `json:"session_key"` 20 | UnionId string `json:"unionid"` 21 | } 22 | 23 | type WXACodeReqColor struct { 24 | R int64 `json:"r"` 25 | G int64 `json:"g"` 26 | B int64 `json:"b"` 27 | } 28 | 29 | type WXACodeReq struct { 30 | Path string `json:"path"` 31 | Width int64 `json:"width,omitempty"` 32 | AutoColor bool `json:"auto_color,omitempty"` 33 | LineColor *WXACodeReqColor `json:"line_color,omitempty"` 34 | IsHyaline bool `json:"is_hyaline"` 35 | } 36 | 37 | type WXACodeUnLimitReq struct { 38 | Scene string `json:"scene"` 39 | Page string `json:"page,omitempty"` 40 | CheckPath bool `json:"check_path"` 41 | EnvVersion string `json:"env_version,omitempty"` 42 | Width int64 `json:"width,omitempty"` 43 | AutoColor bool `json:"auto_color,omitempty"` 44 | LineColor *WXACodeReqColor `json:"line_color,omitempty"` 45 | IsHyaline bool `json:"is_hyaline,omitempty"` 46 | } 47 | -------------------------------------------------------------------------------- /mp_api/oa_web_apps.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type OaWebAppsSnsAuth2AccessTokenQuery struct { 4 | AppId string `url:"appid"` 5 | Secret string `url:"secret"` 6 | Code string `url:"code"` 7 | GrantType string `url:"grant_type"` 8 | } 9 | type OaWebAppsSnsAuth2AccessTokenRes struct { 10 | MpBaseResp 11 | AccessToken string `json:"access_token"` 12 | ExpireIn int64 `json:"expire_in"` 13 | RefreshToken string `json:"refresh_token"` 14 | OpenId string `json:"openid"` 15 | Scope string `json:"scope"` 16 | } 17 | 18 | type OaWebAppsSnsUserInfoQuery struct { 19 | OpenId string `url:"openid"` 20 | Lang string `url:"lang"` 21 | } 22 | 23 | type OaWebAppsSnsUserInfoRes struct { 24 | MpBaseResp 25 | OpenId string `json:"openid"` 26 | NickName string `json:"nickname"` 27 | Sex int64 `json:"sex"` 28 | Province string `json:"province"` 29 | City string `json:"city"` 30 | Country string `json:"country"` 31 | HeadImgUrl string `json:"headimgurl"` 32 | Privilege []string `json:"privilege"` 33 | UnionId string `json:"unionid"` 34 | } 35 | 36 | type OaWebAppsJsSDKTicketQuery struct { 37 | Type JsSDKTicketType `url:"type"` 38 | } 39 | 40 | type OaWebAppsJsSDKTicketRes struct { 41 | MpBaseResp 42 | Ticket string `json:"ticket"` 43 | ExpiresIn int64 `json:"expires_in"` 44 | } 45 | -------------------------------------------------------------------------------- /mp_api/ocr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package mp_api 8 | 9 | type OcrBaseQuery struct { 10 | ImgUrl string `url:"img_url"` 11 | } 12 | 13 | type OcrBandCardResp struct { 14 | MpBaseResp 15 | Number string `json:"number"` 16 | } 17 | 18 | type OcrBusinessLicenseResp struct { 19 | MpBaseResp 20 | RegNum string `json:"reg_num"` 21 | Serial string `json:"serial"` 22 | LegalRepresentative string `json:"legal_representative"` 23 | EnterpriseName string `json:"enterprise_name"` 24 | TypeOfOrganization string `json:"type_of_organization"` 25 | Address string `json:"address"` 26 | TypeOfEnterprise string `json:"type_of_enterprise"` 27 | BusinessScope string `json:"business_scope"` 28 | RegisteredCapital string `json:"registered_capital"` 29 | PaidInCapital string `json:"paid_in_capital"` 30 | ValidPeriod string `json:"valid_period"` 31 | RegisteredDate string `json:"registered_date"` 32 | } 33 | 34 | type OcrIdCardResp struct { 35 | MpBaseResp 36 | Type string `json:"type"` 37 | Name string `json:"name"` 38 | Id string `json:"id"` 39 | Addr string `json:"addr"` 40 | Gender string `json:"gender"` 41 | Nationality string `json:"nationality"` 42 | ValidDate string `json:"valid_date"` 43 | } 44 | -------------------------------------------------------------------------------- /mp_api/structs.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | type MpBaseResp struct { 9 | ErrCode int64 `json:"errcode,omitempty"` 10 | ErrMsg string `json:"errmsg,omitempty"` 11 | } 12 | 13 | func (mbr MpBaseResp) IsError() bool { 14 | return mbr.ErrCode != 0 15 | } 16 | 17 | func (mbr MpBaseResp) ToError() error { 18 | return errors.New(fmt.Sprintf("微信错误: %v", mbr.ErrMsg)) 19 | } 20 | -------------------------------------------------------------------------------- /mp_api/user.go: -------------------------------------------------------------------------------- 1 | package mp_api 2 | 3 | type UserTag struct { 4 | Id int64 `json:"id,omitempty"` 5 | Name string `json:"name,omitempty"` 6 | Count int64 `json:"count,omitempty"` 7 | } 8 | 9 | type UserTagsCreateData struct { 10 | Tag UserTag `json:"tag"` 11 | } 12 | 13 | type UserTagsCreateRes struct { 14 | MpBaseResp 15 | Tag UserTag `json:"tag"` 16 | } 17 | 18 | type UserTagsGetRes struct { 19 | MpBaseResp 20 | Tags []UserTag `json:"tags"` 21 | } 22 | 23 | type UserTagsUpdateData struct { 24 | Tag UserTag `json:"tag"` 25 | } 26 | 27 | type UserTagsDeleteData struct { 28 | Tag UserTag `json:"tag"` 29 | } 30 | 31 | type UserTagGetQuery struct { 32 | TagId int64 `url:"tagid"` 33 | NextOpenId string `url:"next_openid"` 34 | } 35 | 36 | type UserTagGetRes struct { 37 | Count int64 `json:"count"` 38 | Data struct { 39 | OpenId []string `json:"openid"` 40 | } `json:"data"` 41 | NextOpenId string `json:"next_openid"` 42 | } 43 | 44 | type UserTagMembersBatchData struct { 45 | OpenIdList []string `json:"openid_list"` 46 | TagId int64 `json:"tagid"` 47 | } 48 | 49 | type UserTagMembersBatchUnTagData struct { 50 | OpenIdList []string `json:"openid_list"` 51 | TagId int64 `json:"tagid"` 52 | } 53 | 54 | type UserTagsGetIdListData struct { 55 | OpenId string `json:"openid"` 56 | } 57 | 58 | type UserTagsGetIdListRes struct { 59 | MpBaseResp 60 | TagIdList []int64 `json:"tagid_list"` 61 | } 62 | 63 | type UserInfoUpdateRemarkData struct { 64 | OpenId string `json:"openid"` 65 | Remark string `json:"remark"` 66 | } 67 | 68 | type UserInfoQuery struct { 69 | OpenId string `url:"openid"` 70 | Lang string `url:"lang"` 71 | } 72 | type UserInfoRes struct { 73 | MpBaseResp 74 | Subscribe int64 `json:"subscribe"` 75 | OpenId string `json:"openid"` 76 | NickName string `json:"nickname"` 77 | Sex int64 `json:"sex"` 78 | Language string `json:"language"` 79 | City string `json:"city"` 80 | Province string `json:"province"` 81 | Country string `json:"country"` 82 | HeadImgUrl string `json:"headimgurl"` 83 | SubscribeTime int64 `json:"subscribe_time"` 84 | UnionId string `json:"unionid"` 85 | Remark string `json:"remark"` 86 | GroupId int64 `json:"group_id"` 87 | TagIdList []int64 `json:"tagid_list"` 88 | SubscribeScene string `json:"subscribe_scene"` 89 | QrScene int64 `json:"qr_scene"` 90 | QrSceneStr string `json:"qr_scene_str"` 91 | } 92 | 93 | type UserInfoBatchGetDataItem struct { 94 | OpenId string `json:"open_id"` 95 | Lang string `json:"lang"` 96 | } 97 | 98 | type UserInfoBatchGetData struct { 99 | UserList []UserInfoBatchGetDataItem `json:"user_list"` 100 | } 101 | 102 | type UserInfoBatchGetRes struct { 103 | MpBaseResp 104 | UserInfoList []UserInfoRes `json:"user_info_list"` 105 | } 106 | 107 | type UserGetQuery struct { 108 | NextOpenId string `url:"next_openid"` 109 | } 110 | 111 | type UserGetRes struct { 112 | MpBaseResp 113 | Total int64 `json:"total"` 114 | Count int64 `json:"count"` 115 | Data struct { 116 | OpenId []string `json:"openid"` 117 | } `json:"data"` 118 | NextOpenId string `json:"next_openid"` 119 | } 120 | -------------------------------------------------------------------------------- /mp_req.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 YBCZ, Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by a MIT license 4 | // that can be found in the LICENSE file in the root of the source 5 | // tree. 6 | 7 | package wx 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "github.com/blusewang/wx/mp_api" 15 | "github.com/google/go-querystring/query" 16 | "io" 17 | "mime/multipart" 18 | "net/http" 19 | "net/url" 20 | "reflect" 21 | ) 22 | 23 | // Api请求数据体 24 | type mpReq struct { 25 | account MpAccount 26 | path mp_api.MpApi 27 | param interface{} 28 | sendData interface{} 29 | res interface{} 30 | err error 31 | } 32 | 33 | // Query 填充查询信息 34 | // access_token 会自动填充,无需指定 35 | func (mp *mpReq) Query(d interface{}) *mpReq { 36 | mp.param = d 37 | return mp 38 | } 39 | 40 | // SendData 填充POST里的Body数据 41 | func (mp *mpReq) SendData(d interface{}) *mpReq { 42 | mp.sendData = d 43 | return mp 44 | } 45 | 46 | // Bind 绑定请求结果的解码数据体 47 | func (mp *mpReq) Bind(d interface{}) *mpReq { 48 | if reflect.ValueOf(d).Kind() != reflect.Ptr { 49 | mp.err = errors.New("mp.Bind must be Ptr") 50 | } 51 | mp.res = d 52 | return mp 53 | } 54 | 55 | // Download 下载 56 | func (mp *mpReq) Download() (resp *http.Response, err error) { 57 | if mp.err != nil { 58 | err = mp.err 59 | return 60 | } 61 | 62 | var v url.Values 63 | v, err = query.Values(mp.param) 64 | if err != nil { 65 | return 66 | } 67 | 68 | if mp.account.AccessToken != "" { 69 | v.Set("access_token", mp.account.AccessToken) 70 | } 71 | if mp.account.ServerHost == "" { 72 | mp.account.ServerHost = mp_api.ServerHostUniversal 73 | } 74 | var apiUrl = fmt.Sprintf("https://%v/%v?%v", mp.account.ServerHost, mp.path, v.Encode()) 75 | var req *http.Request 76 | if mp.sendData == nil { 77 | req, err = http.NewRequest(http.MethodGet, apiUrl, nil) 78 | } else { 79 | var buf = new(bytes.Buffer) 80 | var coder = json.NewEncoder(buf) 81 | coder.SetEscapeHTML(false) 82 | if err = coder.Encode(mp.sendData); err != nil { 83 | return 84 | } 85 | req, err = http.NewRequest(http.MethodPost, apiUrl, buf) 86 | req.Header.Set("Content-Type", "application/json") 87 | } 88 | if err != nil { 89 | return 90 | } 91 | return client().Do(req) 92 | } 93 | 94 | // Do 执行 95 | func (mp *mpReq) Do() (err error) { 96 | if mp.err != nil { 97 | return mp.err 98 | } 99 | 100 | var v url.Values 101 | v, err = query.Values(mp.param) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if mp.account.AccessToken != "" { 107 | v.Set("access_token", mp.account.AccessToken) 108 | } 109 | if mp.account.ServerHost == "" { 110 | mp.account.ServerHost = mp_api.ServerHostUniversal 111 | } 112 | var apiUrl = fmt.Sprintf("https://%v/%v?%v", mp.account.ServerHost, mp.path, v.Encode()) 113 | var req *http.Request 114 | if mp.sendData == nil { 115 | req, err = http.NewRequest(http.MethodGet, apiUrl, nil) 116 | } else { 117 | var buf = new(bytes.Buffer) 118 | var coder = json.NewEncoder(buf) 119 | coder.SetEscapeHTML(false) 120 | if err = coder.Encode(mp.sendData); err != nil { 121 | return 122 | } 123 | req, err = http.NewRequest(http.MethodPost, apiUrl, buf) 124 | req.Header.Set("Content-Type", "application/json") 125 | } 126 | if err != nil { 127 | return 128 | } 129 | resp, err := client().Do(req) 130 | if resp != nil { 131 | defer resp.Body.Close() 132 | } 133 | if err != nil { 134 | return 135 | } 136 | if mp.res == nil { 137 | mp.res = &mp_api.MpBaseResp{} 138 | } 139 | if err = json.NewDecoder(resp.Body).Decode(mp.res); err != nil { 140 | return 141 | } 142 | rv := reflect.ValueOf(mp.res).Elem() 143 | for i := 0; i < rv.NumField(); i++ { 144 | iv := rv.Field(i) 145 | if iv.Type().String() == "mp_api.MpBaseResp" { 146 | if iv.FieldByName("ErrCode").Int() > 0 { 147 | err = errors.New(fmt.Sprintf("%v %v", iv.FieldByName("ErrCode").Int(), iv.FieldByName("ErrMsg").String())) 148 | return 149 | } 150 | } 151 | } 152 | bs, has := mp.res.(*mp_api.MpBaseResp) 153 | if has { 154 | if bs.ErrCode > 0 { 155 | err = errors.New(fmt.Sprintf("%v %v", bs.ErrCode, bs.ErrMsg)) 156 | } 157 | } 158 | return 159 | } 160 | 161 | // Upload 上传文档。 162 | // reader 一个打开的文件reader。 163 | // fileExtension 该文件的后缀名。 164 | func (mp *mpReq) Upload(reader io.Reader, fileExtension string) (err error) { 165 | if mp.err != nil { 166 | return mp.err 167 | } 168 | 169 | var v url.Values 170 | v, err = query.Values(mp.param) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | if mp.account.AccessToken != "" { 176 | v.Set("access_token", mp.account.AccessToken) 177 | } 178 | if mp.account.ServerHost == "" { 179 | mp.account.ServerHost = mp_api.ServerHostUniversal 180 | } 181 | var apiUrl = fmt.Sprintf("https://%v/%v?%v", mp.account.ServerHost, mp.path, v.Encode()) 182 | body := &bytes.Buffer{} 183 | w := multipart.NewWriter(body) 184 | wf, err := w.CreateFormFile("media", fmt.Sprintf("/tmp/%v.%v", NewRandStr(23), fileExtension)) 185 | if err != nil { 186 | return 187 | } 188 | if _, err = io.Copy(wf, reader); err != nil { 189 | return 190 | } 191 | // 关闭`w`令数据从缓冲区刷写入`body` 192 | if err = w.Close(); err != nil { 193 | return 194 | } 195 | resp, err := client().Post(apiUrl, w.FormDataContentType(), body) 196 | defer resp.Body.Close() 197 | if err != nil { 198 | return 199 | } 200 | if mp.res == nil { 201 | mp.res = &mp_api.MpBaseResp{} 202 | } 203 | if err = json.NewDecoder(resp.Body).Decode(mp.res); err != nil { 204 | return 205 | } 206 | bs, has := mp.res.(*mp_api.MpBaseResp) 207 | if has { 208 | if bs.ErrCode > 0 { 209 | err = errors.New(fmt.Sprintf("%v %v", bs.ErrCode, bs.ErrMsg)) 210 | } 211 | } 212 | return 213 | } 214 | -------------------------------------------------------------------------------- /mp_test.go: -------------------------------------------------------------------------------- 1 | package wx 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/blusewang/wx/mp_api" 6 | "github.com/youkale/go-querystruct/params" 7 | "log" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | func TestLimitString(t *testing.T) { 13 | } 14 | 15 | func TestMpAccount_NewMpReq(t *testing.T) { 16 | var s mp_api.MessageQuery 17 | var v = url.Values{ 18 | "signature": []string{"G0gkxwXEutoJOd6zXGHXPHd7M56SgWEQcjxnuRWuEud98Mh0iaeibcMWG4SaVF0OPYbh0G0qdYlALGbmrp5G36fw"}, 19 | "timestamp": []string{"234234234"}, 20 | } 21 | log.Println(params.Unmarshal(v, &s)) 22 | log.Println(s) 23 | } 24 | 25 | func TestMp_ShortUrl(t *testing.T) { 26 | log.SetFlags(log.Ltime | log.Lshortfile) 27 | var a = MpAccount{ 28 | AppId: "wx20a7b1888ed3de1b", 29 | AccessToken: "38_DXXrtUF80DxFW9ngM49GZypgVQ632G1GDEsK641bMMSafF0dXx9WLipivcAMHCkP7WwmIHmPum4RqXlN4ueDr49Q-OuDE2pUpV8tdGs6st-U50aUjRCI9X0bM-ErCRGruevqaXX8-SIDwlEkKUGdACAWGS", 30 | ServerHost: mp_api.ServerHostShangHai, 31 | } 32 | 33 | var rs mp_api.AccountShortUrlRes 34 | err := a.NewMpReq(mp_api.AccountShortUrl).SendData(&mp_api.AccountShortUrlData{ 35 | Action: mp_api.ShortUrlAction, 36 | LongUrl: "https://developers.weixin.qq.com/doc/offiaccount/Account_Management/URL_Shortener.html", 37 | }).Bind(&rs).Do() 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | log.Println(rs) 42 | } 43 | 44 | func TestMpAccount_ReadMessage(t *testing.T) { 45 | var r mp_api.MessageMassSendData 46 | r.Text = &mp_api.MessageMassSendText{Content: "sdfasdf"} 47 | r.MsgType = mp_api.MessageMassSendTypeText 48 | raw, _ := json.Marshal(r) 49 | log.Println(string(raw)) 50 | } 51 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package wx 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "reflect" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | type H map[string]interface{} 12 | 13 | const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 14 | 15 | // SafeString 安全地限制长度,并将微信不支持的字符替换成'x',能满足商户平台的字符要求 16 | func SafeString(str string, length int) string { 17 | if length <= 3 { 18 | return "" 19 | } 20 | runs := []rune(str) 21 | // 单字符长度高于3的,不是一般的utf8字符,剔除掉 22 | for k, v := range runs { 23 | switch len([]byte(string(v))) { 24 | case 1: 25 | // 全部放行 26 | case 3: 27 | if v < 19968 || v > 40869 { 28 | // 只支持中文 29 | runs[k] = 'x' 30 | } 31 | default: 32 | runs[k] = 'x' 33 | } 34 | } 35 | str = string(runs) 36 | if len(str) > length { 37 | var r2 []rune 38 | for k := range runs { 39 | 40 | if len(string(runs[:k])) <= length-3 { 41 | r2 = runs[:k] 42 | } 43 | } 44 | r2 = append(r2, '…') 45 | str = string(r2) 46 | } 47 | return str 48 | } 49 | 50 | // LimitString 限制长度,并将微信不支持的字符替换成'x',能满足公众号App的字符要求 51 | func LimitString(str string, length int) string { 52 | runs := []rune(str) 53 | // 单字符长度高于3的,不是一般的utf8字符,剔除掉 54 | for k, v := range runs { 55 | switch len([]byte(string(v))) { 56 | case 1: 57 | // 全部放行 58 | case 3: 59 | // 全部放行 60 | default: 61 | runs[k] = 'x' 62 | } 63 | } 64 | str = string(runs) 65 | if len(runs) > length { 66 | var r2 = runs[:length-1] 67 | r2 = append(r2, '…') 68 | str = string(r2) 69 | } 70 | return str 71 | } 72 | 73 | // NewRandStr 生成符合微信要求随机字符 74 | func NewRandStr(n int) string { 75 | b := make([]byte, n) 76 | for i := range b { 77 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] 78 | } 79 | return string(b) 80 | } 81 | 82 | func obj2map(obj interface{}) (p map[string]interface{}) { 83 | vs := reflect.ValueOf(obj) 84 | if vs.Kind() == reflect.Ptr { 85 | vs = vs.Elem() 86 | } 87 | p = make(map[string]interface{}) 88 | obj2mapOnce(vs, &p) 89 | return 90 | } 91 | 92 | func obj2mapOnce(vs reflect.Value, data *map[string]interface{}) { 93 | for i := 0; i < vs.NumField(); i++ { 94 | if vs.Type().Field(i).Anonymous { 95 | obj2mapOnce(vs.Field(i), data) 96 | } else { 97 | k := vs.Type().Field(i).Tag.Get("json") 98 | if k == "" { 99 | k = vs.Type().Field(i).Tag.Get("xml") 100 | if k == "xml" { 101 | continue 102 | } 103 | } 104 | if k == "sign" || k == "-" { 105 | continue 106 | } 107 | k = strings.Split(k, ",")[0] 108 | // 跳过空值 109 | if reflect.Zero(vs.Field(i).Type()).Interface() == vs.Field(i).Interface() { 110 | continue 111 | } 112 | (*data)[k] = vs.Field(i).Interface() 113 | } 114 | } 115 | } 116 | 117 | func mapSortByKey(data map[string]interface{}) string { 118 | var keys []string 119 | nData := "" 120 | for k := range data { 121 | keys = append(keys, k) 122 | } 123 | 124 | sort.Strings(keys) 125 | for _, k := range keys { 126 | nData = fmt.Sprintf("%v&%v=%v", nData, k, data[k]) 127 | } 128 | return nData[1:] 129 | } 130 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package wx 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "unicode/utf8" 7 | ) 8 | 9 | func TestSafeString(t *testing.T) { 10 | str := "均】A 爱尚美  亮哥(^ω^)人心的丑陋^⒈个乆旳天荒地老🚖👮🏾绝对没问题👌🏼唯τā命゛L金剪子￿LK花🌺侑你僦好💝无忧🌹V沙龙杨斌 AA·Enya-ZZJG" 11 | for k, v := range []rune(str) { 12 | log.Println(k, string(v), utf8.ValidRune(v), len([]byte(string(v))), v) 13 | } 14 | log.Println(SafeString(str, 320)) 15 | } 16 | --------------------------------------------------------------------------------