diff --git a/.gitignore b/.gitignore index adafc57..4c14e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ *.out .idea/ -*.swp \ No newline at end of file +*.swp + +mch_v3_test.go \ No newline at end of file diff --git a/README.md b/README.md index 9f240b3..b691ce8 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,12 @@ wechat weixin sdk,支持微信应用和商户。 log.Println(data) ``` +# 商户账号API(V3版) +功能初步实现。 + +# 简易中间件 +`SetClientMiddleware(middleware func(req *http.Request,res *http.Response,err error))` 将每一次网络交互过程都开放出来,以便做自定义日志。 + # 为微信业务数据提供的额外工具方法 - `NewRandStr` 生成符合微信要求随机字符 - `LimitString` 限制长度,并将微信不支持的字符替换成'x',能满足公众号App的字符要求 diff --git a/http_client.go b/http_client.go new file mode 100644 index 0000000..e047ac6 --- /dev/null +++ b/http_client.go @@ -0,0 +1,37 @@ +// Copyright 2021 YBCZ, Inc. All rights reserved. +// +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file in the root of the source +// tree. + +package wx + +import ( + "net/http" +) + +var _cli *http.Client +var _middleware func(req *http.Request, res *http.Response, err error) + +type mt struct { + t http.Transport +} + +func (m *mt) RoundTrip(req *http.Request) (res *http.Response, err error) { + res, err = m.t.RoundTrip(req) + if _middleware != nil { + _middleware(req, res, err) + } + return +} + +func client() *http.Client { + if _cli == nil { + _cli = &http.Client{Transport: &mt{http.Transport{}}} + } + return _cli +} + +func SetClientMiddleware(middleware func(req *http.Request, res *http.Response, err error)) { + _middleware = middleware +} diff --git a/mch.go b/mch.go index cf94756..91b047c 100644 --- a/mch.go +++ b/mch.go @@ -27,7 +27,7 @@ import ( var cache = make(map[string]*http.Client) -// 商户账号 +// MchAccount 商户账号 type MchAccount struct { MchId string MchKey string @@ -36,17 +36,17 @@ type MchAccount struct { MchRSAPublicKey []byte // 加密银行卡信息时用的公钥 } -// 创建请求 +// NewMchReqWithApp 创建请求 func (ma MchAccount) NewMchReqWithApp(api mch_api.MchApi, appId string) (req *mchReq) { return &mchReq{account: ma, privateClient: cache[ma.MchId], api: api, appId: appId} } -// 创建请求 +// NewMchReq 创建请求 func (ma MchAccount) NewMchReq(api mch_api.MchApi) (req *mchReq) { return &mchReq{account: ma, privateClient: cache[ma.MchId], api: api} } -// 订单签名给App +// OrderSign4App 订单签名给App func (ma MchAccount) OrderSign4App(or mch_api.PayUnifiedOrderRes) map[string]interface{} { data := make(map[string]interface{}) data["appid"] = or.AppId @@ -60,7 +60,7 @@ func (ma MchAccount) OrderSign4App(or mch_api.PayUnifiedOrderRes) map[string]int return data } -// 订单签名,适用于H5、小程序 +// OrderSign 订单签名,适用于H5、小程序 func (ma MchAccount) OrderSign(or mch_api.PayUnifiedOrderRes) map[string]interface{} { data := make(map[string]interface{}) data["appId"] = or.AppId @@ -75,7 +75,7 @@ func (ma MchAccount) OrderSign(or mch_api.PayUnifiedOrderRes) map[string]interfa return data } -// 验证支付成功通知 +// PayNotify 验证支付成功通知 func (ma MchAccount) PayNotify(pn mch_api.PayNotify) bool { if !pn.IsSuccess() || pn.Sign == "" { return false @@ -93,7 +93,7 @@ func (ma MchAccount) PayNotify(pn mch_api.PayNotify) bool { return false } -// 银行卡机要信息加密 +// RsaEncrypt 银行卡机要信息加密 func (ma MchAccount) RsaEncrypt(plain string) (out string) { block, _ := pem.Decode(ma.MchRSAPublicKey) publicKey, err := x509.ParsePKCS1PublicKey(block.Bytes) @@ -146,13 +146,23 @@ func (ma MchAccount) newPrivateClient() (cli http.Client, err error) { return } cert.PrivateKey = key - cli = http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, - DisableCompression: true, - }, + cli = *client() + cli.Transport.(*mt).t.TLSClientConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, } + cli.Transport.(*mt).t.DisableCompression = true + //cli = http.Client{ + // Transport: &http.Transport{ + // TLSClientConfig: &tls.Config{ + // Certificates: []tls.Certificate{cert}, + // }, + // DisableCompression: true, + // }, + //} return } + +// NewMchReqV3 创建请求 +func (ma MchAccount) NewMchReqV3(api mch_api.MchApi) (req *mchReqV3) { + return &mchReqV3{account: ma, api: api, hashHandler: sha256.New()} +} diff --git a/mch_api/structs.go b/mch_api/structs.go index 5869b98..1a93d0d 100644 --- a/mch_api/structs.go +++ b/mch_api/structs.go @@ -31,17 +31,17 @@ type MchBaseResponse struct { ErrCodeDes string `xml:"err_code_des,omitempty"` } -// 是否成功处理 +// IsSuccess 是否成功处理 func (m MchBaseResponse) IsSuccess() bool { return m.ReturnCode == "SUCCESS" && m.ResultCode == "SUCCESS" && (m.ErrCode == "SUCCESS" || m.ErrCode == "") } -// 如果出错,是否是微信程序错误 +// IsUnCertain 如果出错,是否是微信程序错误 func (m MchBaseResponse) IsUnCertain() bool { return m.ErrCode == "SYSTEMERROR" } -// 转为Golang错误 +// ToError 转为Golang错误 func (m MchBaseResponse) ToError() error { if m.ErrCodeDes != "" { return errors.New(fmt.Sprintf("%v %v", m.ErrCode, m.ErrCodeDes)) @@ -52,7 +52,7 @@ func (m MchBaseResponse) ToError() error { } } -// `ProfitSharing`设置为"Y"为分账定单标记。 +// PayUnifiedOrderData `ProfitSharing`设置为"Y"为分账定单标记。 // 不设置,或设置为"N",为普通定单 type PayUnifiedOrderData struct { MchBase @@ -74,7 +74,7 @@ type PayUnifiedOrderRes struct { PrepayId string `xml:"prepay_id"` } -// 支付成功通知 +// PayNotify 支付成功通知 type PayNotify struct { MchBaseResponse MchBase @@ -108,7 +108,7 @@ type PayNotify struct { TimeEnd string `xml:"time_end"` } -// 回复支付成功通知 +// PayNotifyRes 回复支付成功通知 type PayNotifyRes MchBaseResponse type PayOrderQueryData struct { @@ -141,7 +141,7 @@ type PayRefundRes struct { CashFee int64 `xml:"cash_fee"` } -// 分账结果中的接收者 +// PayProfitSharingReceiver 分账结果中的接收者 type PayProfitSharingReceiver struct { Type string `json:"type"` Account string `json:"account"` diff --git a/mch_req.go b/mch_req.go index 83c23f2..100c6dd 100644 --- a/mch_req.go +++ b/mch_req.go @@ -32,34 +32,34 @@ type mchReq struct { err error } -// 填充POST里的Body数据 +// Send 填充POST里的Body数据 func (mr *mchReq) Send(data interface{}) *mchReq { mr.sendData = data return mr } -// 使用 HMAC-SHA256 签名 +// UseHMacSign 使用 HMAC-SHA256 签名 // 默认采用 MD5 签名 func (mr *mchReq) UseHMacSign() *mchReq { mr.isHmacSign = true return mr } -// 使用私有证书通信 +// UsePrivateCert 使用私有证书通信 func (mr *mchReq) UsePrivateCert() *mchReq { mr.isPrivateClient = true return mr } -// 绑定请求结果的解码数据体 +// Bind 绑定请求结果的解码数据体 func (mr *mchReq) Bind(data interface{}) *mchReq { mr.res = data return mr } -// 执行 +// Do 执行 func (mr *mchReq) Do() (err error) { - var cli = *http.DefaultClient + var cli http.Client if mr.isPrivateClient { if privateClientCache[mr.account.MchId] != nil { cli = *privateClientCache[mr.account.MchId] diff --git a/mch_req_v3.go b/mch_req_v3.go new file mode 100644 index 0000000..7f138ff --- /dev/null +++ b/mch_req_v3.go @@ -0,0 +1,92 @@ +// Copyright 2020 YBCZ, Inc. All rights reserved. +// +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file in the root of the source +// tree. + +package wx + +import ( + "crypto" + rand2 "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/blusewang/wx/mch_api" + "hash" + "net/http" +) + +// 商户请求 +type mchReqV3 struct { + account MchAccount + cert *x509.Certificate + privateKey *rsa.PrivateKey + hashHandler hash.Hash + api mch_api.MchApi + ts int64 + nonceStr string + sendData interface{} + res interface{} + err error +} + +// Send 填充POST里的Body数据 +func (mr *mchReqV3) Send(data interface{}) *mchReqV3 { + mr.sendData = data + return mr +} + +// Bind 绑定请求结果的解码数据体 +func (mr *mchReqV3) Bind(data interface{}) *mchReqV3 { + mr.res = data + return mr +} + +// Do 执行 +func (mr *mchReqV3) Do() (err error) { + + return +} + +func (mr *mchReqV3) prepareCert() (err error) { + cb, _ := pem.Decode(mr.account.MchSSLCert) + mr.cert, err = x509.ParseCertificate(cb.Bytes) + if err != nil { + return + } + cb, _ = pem.Decode(mr.account.MchSSLKey) + key, err := x509.ParsePKCS8PrivateKey(cb.Bytes) + if err != nil { + return + } + mr.privateKey = key.(*rsa.PrivateKey) + mr.hashHandler = sha256.New() + return +} + +func (mr *mchReqV3) sign(request *http.Request, body interface{}) (err error) { + raw, err := json.Marshal(body) + if err != nil { + return + } + mr.nonceStr = NewRandStr(32) + str := fmt.Sprintf("%v\n%v\n%v\n%v\n%v\n", request.Method, request.URL.Path, mr.ts, mr.nonceStr, string(raw)) + mr.hashHandler.Reset() + mr.hashHandler.Write([]byte(str)) + signRaw, err := rsa.SignPKCS1v15(rand2.Reader, mr.privateKey, crypto.SHA256, mr.hashHandler.Sum(nil)) + if err != nil { + return + } + + request.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%v",nonce_str="%v",signature="%v",timestamp="%v",serial_no="%X"`, + mr.account.MchId, mr.nonceStr, base64.StdEncoding.EncodeToString(signRaw), mr.ts, mr.cert.SerialNumber)) + request.Header.Set("User-Agent", "Gdb/1.0") + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + return +} diff --git a/mch_test.go b/mch_test.go index 5becd36..4b51b82 100644 --- a/mch_test.go +++ b/mch_test.go @@ -7,14 +7,23 @@ package wx import ( + "bytes" "github.com/blusewang/wx/mch_api" + "io/ioutil" "log" + "net/http" "reflect" "testing" ) func TestMchAccount_NewMchReq(t *testing.T) { log.SetFlags(log.Ltime | log.Lshortfile) + SetClientMiddleware(func(req *http.Request, res *http.Response, err error) { + log.Println(req, res, err) + raw, _ := ioutil.ReadAll(res.Body) + log.Println(string(raw)) + res.Body = ioutil.NopCloser(bytes.NewReader(raw)) + }) mch := MchAccount{ MchId: "", MchKey: "", @@ -35,7 +44,7 @@ func TestMchAccount_NewMchReq(t *testing.T) { Description: "", }, }) - err := mch.NewMchReqWithApp(mch_api.PayProfitSharing, ""). + err := mch.NewMchReqWithApp(mch_api.PayProfitSharing, "wxbb4d55eb95f282f4"). Send(&body). UseHMacSign(). UsePrivateCert(). diff --git a/mch_v3_test.go b/mch_v3_test.go new file mode 100644 index 0000000..4817bd9 --- /dev/null +++ b/mch_v3_test.go @@ -0,0 +1,143 @@ +// Copyright 2020 YBCZ, Inc. All rights reserved. +// +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file in the root of the source +// tree. + +package wx + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + rand2 "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "net/http" + "testing" + "time" +) + +func TestNewRandStr(t *testing.T) { + log.SetFlags(log.Ltime | log.Lshortfile) + mchId := "" + sslCrt := []byte("") + sslKey := []byte("") + + cb, _ := pem.Decode(sslCrt) + pubKey, err := x509.ParseCertificate(cb.Bytes) + if err != nil { + t.Fatal(err) + } + cb, _ = pem.Decode(sslKey) + priKey, err := x509.ParsePKCS8PrivateKey(cb.Bytes) + if err != nil { + t.Fatal(err) + } + + h := sha256.New() + ts := time.Now().Unix() + rs := NewRandStr(32) + + req, err := http.NewRequest(http.MethodGet, "https://api.mch.weixin.qq.com/v3/certificates", nil) + if err != nil { + t.Fatal(err) + } + + str := fmt.Sprintf("%v\n%v\n%v\n%v\n%v\n", req.Method, req.URL.Path, ts, rs, "") + h.Write([]byte(str)) + signRaw, err := rsa.SignPKCS1v15(rand2.Reader, priKey.(*rsa.PrivateKey), crypto.SHA256, h.Sum(nil)) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%v",nonce_str="%v",signature="%v",timestamp="%v",serial_no="%X"`, + mchId, rs, base64.StdEncoding.EncodeToString(signRaw), ts, pubKey.SerialNumber)) + req.Header.Set("User-Agent", "Gdb/1.0") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + resp, err := client().Do(req) + if err != nil { + t.Fatal(err) + } + raw, _ := ioutil.ReadAll(resp.Body) + log.Println(resp.Status, string(raw)) + + str = fmt.Sprintf("%v\n%v\n%v\n", + resp.Header.Get("Wechatpay-Timestamp"), + resp.Header.Get("Wechatpay-Nonce"), string(raw)) + signRaw, err = base64.StdEncoding.DecodeString(resp.Header.Get("Wechatpay-Signature")) + if err != nil { + log.Fatal(err) + } + + log.Println(pubKey.PublicKey.(*rsa.PublicKey)) + log.Printf("%X\n", pubKey.SerialNumber) + h.Reset() + h.Write([]byte(str)) + err = rsa.VerifyPKCS1v15(pubKey.PublicKey.(*rsa.PublicKey), crypto.SHA256, h.Sum(nil), signRaw) + if err != nil { + log.Println(err) + } + + for s, strings := range resp.Header { + log.Println(s, strings) + } + + var res struct { + Data []struct { + EffectiveTime time.Time `json:"effective_time"` + EncryptCertificate struct { + Algorithm string `json:"algorithm"` + AssociatedData string `json:"associated_data"` + Ciphertext string `json:"ciphertext"` + Nonce string `json:"nonce"` + } `json:"encrypt_certificate"` + ExpireTime time.Time `json:"expire_time"` + SerialNo string `json:"serial_no"` + } `json:"data"` + } + if err = json.Unmarshal(raw, &res); err != nil { + t.Fatal(err) + } + log.Println(res) + // AEAD_AES_256_GCM + cipherRaw, _ := base64.StdEncoding.DecodeString(res.Data[0].EncryptCertificate.Ciphertext) + block, err := aes.NewCipher([]byte("14da084a425129ed376084a49caae96b")) + if err != nil { + t.Fatal(err) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + t.Fatal(err) + } + plain, err := gcm.Open(nil, []byte(res.Data[0].EncryptCertificate.Nonce), cipherRaw, []byte(res.Data[0].EncryptCertificate.AssociatedData)) + if err != nil { + t.Fatal(err) + } + log.Println(string(plain)) + raw, err = rsa.EncryptOAEP(sha1.New(), rand2.Reader, pubKey.PublicKey.(*rsa.PublicKey), []byte("name"), nil) + if err != nil { + t.Fatal(err) + } + log.Println(base64.StdEncoding.EncodeToString(raw)) +} + +func TestLimitString2(t *testing.T) { + log.Printf("%x", sha256.Sum256([]byte("abcd\n"))) +} + +func TestClientMiddleware(t *testing.T) { + log.SetFlags(log.Ltime | log.Lshortfile) + log.Println(_middleware == nil) + cli := &http.Client{Transport: &mt{http.Transport{}}} + log.Println(cli.Get("https://cashier.mywsy.cn")) +} diff --git a/mp.go b/mp.go index d722fd1..52b3a02 100644 --- a/mp.go +++ b/mp.go @@ -11,7 +11,7 @@ import ( "time" ) -// 应用账号 +// MpAccount 应用账号 // ServerHost 默认为:mp_api.ServerHostUniversal type MpAccount struct { AppId string `json:"app_id"` @@ -23,7 +23,7 @@ type MpAccount struct { ServerHost mp_api.ServerHost `json:"server_host"` } -// 读取通知消息 +// ReadMessage 读取通知消息 func (ma MpAccount) ReadMessage(req *http.Request) (q mp_api.MessageQuery, msg mp_api.MessageData, err error) { if err = params.Unmarshal(req.URL.Query(), &q); err != nil { return @@ -45,7 +45,7 @@ func (ma MpAccount) ReadMessage(req *http.Request) (q mp_api.MessageQuery, msg m return } -// 微信网页的网址签名 +// UrlSign 微信网页的网址签名 func (ma MpAccount) UrlSign(u string) (d map[string]interface{}) { data := make(map[string]interface{}) data["noncestr"] = NewRandStr(32) @@ -63,7 +63,7 @@ func (ma MpAccount) UrlSign(u string) (d map[string]interface{}) { return } -// 新建一个请求 +// NewMpReq 新建一个请求 func (ma MpAccount) NewMpReq(path mp_api.MpApi) *mpReq { return &mpReq{account: ma, path: path} } diff --git a/mp_api/constant.go b/mp_api/constant.go index 9eda5ae..0eadc6b 100644 --- a/mp_api/constant.go +++ b/mp_api/constant.go @@ -4,68 +4,117 @@ type ServerHost string const ( // 服务器类型 - ServerHostUniversal = "api.weixin.qq.com" // 通用域名 - ServerHostUniversal2 = "api2.weixin.qq.com" // 通用异地容灾域名 - ServerHostShangHai = "sh.api.weixin.qq.com" // 上海域名 - ServerHostShenZhen = "sz.api.weixin.qq.com" // 深圳域名 - ServerHostHK = "hk.api.weixin.qq.com" // 香港域名 + + // ServerHostUniversal 通用域名 + ServerHostUniversal = "api.weixin.qq.com" + // ServerHostUniversal2 通用异地容灾域名 + ServerHostUniversal2 = "api2.weixin.qq.com" + // ServerHostShangHai 上海域名 + ServerHostShangHai = "sh.api.weixin.qq.com" + // ServerHostShenZhen 深圳域名 + ServerHostShenZhen = "sz.api.weixin.qq.com" + // ServerHostHK 香港域名 + ServerHostHK = "hk.api.weixin.qq.com" ) type MpApi string const ( // 开始开发 - BasicInformationToken = "cgi-bin/token" // 获取Access token - BasicInformationApiDomainIp = "cgi-bin/get_api_domain_ip" // 获取微信服务器IP地址 - BasicInformationCallbackCheck = "cgi-bin/callback/check" // 网络检测 + + // BasicInformationToken 获取Access token + BasicInformationToken = "cgi-bin/token" + // BasicInformationApiDomainIp 获取微信服务器IP地址 + BasicInformationApiDomainIp = "cgi-bin/get_api_domain_ip" + // BasicInformationCallbackCheck 网络检测 + BasicInformationCallbackCheck = "cgi-bin/callback/check" // 自定义菜单 - CustomMenuCreate = "cgi-bin/menu/create" // 创建自定义菜单 - CustomMenuCurrentSelfMenuInfo = "cgi-bin/get_current_selfmenu_info" // 查询自定义菜单 - CustomMenuDelete = "cgi-bin/menu/delete" // 删除默认菜单及全部个性化菜单 + + // CustomMenuCreate 创建自定义菜单 + CustomMenuCreate = "cgi-bin/menu/create" + // CustomMenuCurrentSelfMenuInfo 查询自定义菜单 + CustomMenuCurrentSelfMenuInfo = "cgi-bin/get_current_selfmenu_info" + // CustomMenuDelete 删除默认菜单及全部个性化菜单 + CustomMenuDelete = "cgi-bin/menu/delete" // 消息 - MessageCustomServiceKfAccountAdd = "customservice/kfaccount/add" // 添加客服 - MessageCustomServiceKfAccountUpdate = "customservice/kfaccount/update" // 修改客服 - MessageCustomServiceKfAccountDel = "customservice/kfaccount/del" // 删除客服 - MessageCustomServiceKfAccountUploadHeadImg = "customservice/kfaccount/uploadheadimg" // 上传客服头像 - MessageCustomServiceKfList = "cgi-bin/customservice/getkflist" // 获取所有客服 - MessageCustomSend = "cgi-bin/message/custom/send" // 客服接口-发消息 - MessageTemplateSend = "cgi-bin/message/template/send" // 发送模板消息 - MessageMassSend = "cgi-bin/message/mass/send" // 根据OpenID列表群发 + + // MessageCustomServiceKfAccountAdd 添加客服 + MessageCustomServiceKfAccountAdd = "customservice/kfaccount/add" + // MessageCustomServiceKfAccountUpdate 修改客服 + MessageCustomServiceKfAccountUpdate = "customservice/kfaccount/update" + // MessageCustomServiceKfAccountDel 删除客服 + MessageCustomServiceKfAccountDel = "customservice/kfaccount/del" + // MessageCustomServiceKfAccountUploadHeadImg 上传客服头像 + MessageCustomServiceKfAccountUploadHeadImg = "customservice/kfaccount/uploadheadimg" + // MessageCustomServiceKfList 获取所有客服 + MessageCustomServiceKfList = "cgi-bin/customservice/getkflist" + // MessageCustomSend 客服接口-发消息 + MessageCustomSend = "cgi-bin/message/custom/send" + // MessageTemplateSend 发送模板消息 + MessageTemplateSend = "cgi-bin/message/template/send" + // MessageMassSend 根据OpenID列表群发 + MessageMassSend = "cgi-bin/message/mass/send" // 媒体文件上传 - MediaUploadImg = "cgi-bin/media/uploadimg" // 上传图文消息内的图片获取URL - MediaUpload = "cgi-bin/media/upload" // 新增临时素材 + + // MediaUploadImg 上传图文消息内的图片获取URL + MediaUploadImg = "cgi-bin/media/uploadimg" + // MediaUpload 新增临时素材 + MediaUpload = "cgi-bin/media/upload" // 微信网页开发 - OaWebAppsSnsAuth2AccessToken = "sns/oauth2/access_token" // 通过code换取网页授权access_token - OaWebAppsSnsUserInfo = "sns/userinfo" // 拉取用户信息(需scope为 snsapi_userinfo) - OaWebAppsJsSDKTicket = "cgi-bin/ticket/getticket" // 获取JsSDK ticket + + // OaWebAppsSnsAuth2AccessToken 通过code换取网页授权access_token + OaWebAppsSnsAuth2AccessToken = "sns/oauth2/access_token" + // OaWebAppsSnsUserInfo 拉取用户信息(需scope为 snsapi_userinfo) + OaWebAppsSnsUserInfo = "sns/userinfo" + // OaWebAppsJsSDKTicket 获取JsSDK ticket + OaWebAppsJsSDKTicket = "cgi-bin/ticket/getticket" // 用户管理 - UserTagsCreate = "cgi-bin/tags/create" // 创建标签 - UserTagsGet = "cgi-bin/tags/get" // 获取公众号已创建的标签 - UserTagsUpdate = "cgi-bin/tags/update" // 编辑标签 - UserTagsDelete = "cgi-bin/tags/delete" // 删除标签 - UserTagGet = "cgi-bin/user/tag/get" // 获取标签下粉丝列表 - UserTagMembersBatch = "cgi-bin/tags/members/batchtagging" // 批量为用户打标签 - UserTagMembersBatchUnTag = "cgi-bin/tags/members/batchuntagging" // 批量为用户取消标签 - UserTagsGetIdList = "cgi-bin/tags/getidlist" // 获取用户身上的标签列表 - UserInfoUpdateRemark = "cgi-bin/user/info/updateremark" // 用户设置备注名 - UserInfo = "cgi-bin/user/info" // 获取用户基本信息(包括UnionID机制) - UserInfoBatchGet = "cgi-bin/user/info/batchget" // 批量获取用户基本信息 - UserGet = "cgi-bin/user/get" // 获取关注者列表 + + // UserTagsCreate 创建标签 + UserTagsCreate = "cgi-bin/tags/create" + // UserTagsGet 获取公众号已创建的标签 + UserTagsGet = "cgi-bin/tags/get" + // UserTagsUpdate 编辑标签 + UserTagsUpdate = "cgi-bin/tags/update" + // UserTagsDelete 删除标签 + UserTagsDelete = "cgi-bin/tags/delete" + // UserTagGet 获取标签下粉丝列表 + UserTagGet = "cgi-bin/user/tag/get" + // UserTagMembersBatch 批量为用户打标签 + UserTagMembersBatch = "cgi-bin/tags/members/batchtagging" + // UserTagMembersBatchUnTag 批量为用户取消标签 + UserTagMembersBatchUnTag = "cgi-bin/tags/members/batchuntagging" + // UserTagsGetIdList 获取用户身上的标签列表 + UserTagsGetIdList = "cgi-bin/tags/getidlist" + // UserInfoUpdateRemark 用户设置备注名 + UserInfoUpdateRemark = "cgi-bin/user/info/updateremark" + // UserInfo 获取用户基本信息(包括UnionID机制) + UserInfo = "cgi-bin/user/info" + // UserInfoBatchGet 批量获取用户基本信息 + UserInfoBatchGet = "cgi-bin/user/info/batchget" + // UserGet 获取关注者列表 + UserGet = "cgi-bin/user/get" // 账号管理 - AccountQrCreate = "cgi-bin/qrcode/create" // 二维码 - AccountShortUrl = "cgi-bin/shorturl" // 长链接转成短链接 + + // AccountQrCreate 二维码 + AccountQrCreate = "cgi-bin/qrcode/create" + // AccountShortUrl 长链接转成短链接 + AccountShortUrl = "cgi-bin/shorturl" // 对话能力 - GuideAccountAdd = "cgi-bin/guide/addguideacct" // 添加顾问 - GuideAddBuyer = "cgi-bin/guide/addguidebuyerrelation" // 为顾问分配客户 - // 小程序 + // GuideAccountAdd 添加顾问 + GuideAccountAdd = "cgi-bin/guide/addguideacct" + // GuideAddBuyer 为顾问分配客户 + GuideAddBuyer = "cgi-bin/guide/addguidebuyerrelation" + + // MiniProgramJsCode2Session 小程序 MiniProgramJsCode2Session = "sns/jscode2session" // 登录凭证校验 ) diff --git a/mp_api/message.go b/mp_api/message.go index 185e5cb..6dc2f77 100644 --- a/mp_api/message.go +++ b/mp_api/message.go @@ -71,7 +71,7 @@ type MessageData struct { AppId string `xml:"-" json:"app_id,omitempty"` } -// 公众号消息解密 +// ShouldDecode 公众号消息解密 func (msg *MessageData) ShouldDecode(EncodingAESKey string) (err error) { if msg.Encrypt == "" { // 没加密 diff --git a/mp_req.go b/mp_req.go index 718a34f..38615fd 100644 --- a/mp_req.go +++ b/mp_req.go @@ -30,20 +30,20 @@ type mpReq struct { err error } -// 填充查询信息 +// Query 填充查询信息 // access_token 会自动填充,无需指定 func (mp *mpReq) Query(d interface{}) *mpReq { mp.param = d return mp } -// 填充POST里的Body数据 +// SendData 填充POST里的Body数据 func (mp *mpReq) SendData(d interface{}) *mpReq { mp.sendData = d return mp } -// 绑定请求结果的解码数据体 +// Bind 绑定请求结果的解码数据体 func (mp *mpReq) Bind(d interface{}) *mpReq { if reflect.ValueOf(d).Kind() != reflect.Ptr { mp.err = errors.New("mp.Bind must be Ptr") @@ -52,7 +52,7 @@ func (mp *mpReq) Bind(d interface{}) *mpReq { return mp } -// 执行 +// Do 执行 func (mp *mpReq) Do() (err error) { if mp.err != nil { return mp.err @@ -73,7 +73,7 @@ func (mp *mpReq) Do() (err error) { var apiUrl = fmt.Sprintf("https://%v/%v?%v", mp.account.ServerHost, mp.path, v.Encode()) var resp *http.Response if mp.sendData == nil { - resp, err = http.Get(apiUrl) + resp, err = client().Get(apiUrl) } else { var buf = new(bytes.Buffer) var coder = json.NewEncoder(buf) @@ -81,7 +81,7 @@ func (mp *mpReq) Do() (err error) { if err = coder.Encode(mp.sendData); err != nil { return } - resp, err = http.Post(apiUrl, "application/json", buf) + resp, err = client().Post(apiUrl, "application/json", buf) } if err != nil { return @@ -145,7 +145,7 @@ func (mp *mpReq) Upload(reader io.Reader, fileExtension string) (err error) { if err = w.Close(); err != nil { return } - resp, err := http.Post(apiUrl, w.FormDataContentType(), body) + resp, err := client().Post(apiUrl, w.FormDataContentType(), body) if err != nil { return } diff --git a/utils.go b/utils.go index 969bbb3..ef61411 100644 --- a/utils.go +++ b/utils.go @@ -1,18 +1,8 @@ package wx import ( - rand2 "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" "fmt" - "io" "math/rand" - "net/http" "reflect" "sort" "strings" @@ -21,45 +11,7 @@ import ( type H map[string]interface{} -func postWithCert(cert tls.Certificate, api string, body io.Reader) (resp *http.Response, err error) { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, - DisableCompression: true, - }, - } - req, err := http.NewRequest("POST", api, body) - if err != nil { - return - } - resp, err = client.Do(req) - return -} - -func postStreamWithCert(cert tls.Certificate, api string, data io.Reader) (body io.ReadCloser, err error) { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, - DisableCompression: true, - }, - } - req, err := http.NewRequest("POST", api, data) - if err != nil { - return - } - resp, err := client.Do(req) - if err != nil { - return - } - body = resp.Body - return -} - -// 安全地限制长度,并将微信不支持的字符替换成'x',能满足商户平台的字符要求 +// SafeString 安全地限制长度,并将微信不支持的字符替换成'x',能满足商户平台的字符要求 func SafeString(str string, length int) string { if length <= 3 { return "" @@ -94,7 +46,7 @@ func SafeString(str string, length int) string { return str } -// 限制长度,并将微信不支持的字符替换成'x',能满足公众号App的字符要求 +// LimitString 限制长度,并将微信不支持的字符替换成'x',能满足公众号App的字符要求 func LimitString(str string, length int) string { runs := []rune(str) // 单字符长度高于3的,不是一般的utf8字符,剔除掉 @@ -117,7 +69,7 @@ func LimitString(str string, length int) string { return str } -// 生成符合微信要求随机字符 +// NewRandStr 生成符合微信要求随机字符 func NewRandStr(length int) string { codes := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" codeLen := len(codes) @@ -131,6 +83,7 @@ func NewRandStr(length int) string { return string(data) } + func obj2map(obj interface{}) (p map[string]interface{}) { vs := reflect.ValueOf(obj) if vs.Kind() == reflect.Ptr { @@ -179,53 +132,3 @@ func mapSortByKey(data map[string]interface{}) string { } return nData[1:] } - -var certs = make(map[string]*tls.Certificate) - -func parseCertificate(pemByte, keyByte []byte, password string) (cert *tls.Certificate, err error) { - if certs[password] != nil { - return certs[password], nil - } - - block, restPem := pem.Decode(pemByte) - if block == nil { - err = errors.New("pem解析失败") - return - } - - var c tls.Certificate - c.Certificate = append(c.Certificate, block.Bytes) - certDerBlockChain, _ := pem.Decode(restPem) - if certDerBlockChain != nil { - c.Certificate = append(c.Certificate, certDerBlockChain.Bytes) - } - // 解码pem格式的私钥 - var key interface{} - keyDer, _ := pem.Decode(keyByte) - if keyDer.Type == "RSA PRIVATE KEY" { - key, err = x509.ParsePKCS1PrivateKey(keyDer.Bytes) - } else if keyDer.Type == "PRIVATE KEY" { - key, err = x509.ParsePKCS8PrivateKey(keyDer.Bytes) - } - if err != nil { - return - } - c.PrivateKey = key - cert = &c - certs[password] = cert - return -} - -func rsaEncrypt(rsaPubic []byte, plain string) (cipherText string, err error) { - block, _ := pem.Decode(rsaPubic) - publicKey, err := x509.ParsePKCS1PublicKey(block.Bytes) - if err != nil { - return - } - raw, err := rsa.EncryptOAEP(sha1.New(), rand2.Reader, publicKey, []byte(plain), nil) - if err != nil { - return - } - cipherText = base64.StdEncoding.EncodeToString(raw) - return -}