├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── http.go ├── logger.go ├── req_base.go ├── req_wxmch.go ├── req_wxmini.go ├── req_wxmini_test.go ├── req_wxpay.go ├── req_wxpay_test.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | .DS_Store 4 | vendor/ 5 | .vscode/ 6 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 小小落木 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 | # 微信小程序sdk 2 | 3 | 用 `golang` 写的一个用于微信小程序支付以及小程序后端接口调用的`sdk` 4 | 5 | ## 接口 6 | 7 | 接口没有实现完整,目前实现的是一些用到比较多的接口 8 | 9 | ### 无需证书支付接口(`req_wxpay`) 10 | 11 | - [x] 统一下单接口(`ReqUnifiedOrder`) 12 | - [x] 订单查询接口(`ReqQueryOrder`) 13 | - [x] 关闭订单接口(`ReqCloseOrder`) 14 | 15 | ### 需要证书支付接口(`req_wxmch`) 16 | 17 | - [x] 企业付款到零钱接口(`ReqWxToMchPay`) 18 | - [x] 企业付款到零钱查询接口(`ReqMchPayment`) 19 | - [x] 申请退款接口(`ReqPayRefund`) 20 | 21 | ### 小程序接口(`req_wxmini`) 22 | 23 | - [x] 获取`AccessToken`的接口(`ReqAccessToken`) 24 | - [x] `code`换`session`接口(`ReqCode2Session`) 25 | - [x] 发送订阅消息接口(`SendSubscribeMessage`) 26 | - [x] 无限获取小程序码接口(`ReqWxCodeUnlimited`) 27 | - [x] 校验图片是否含有违法违规内容接口(`CheckImage`) 28 | - [x] 检查文本是否含有违法违规内容接口(`CheckMessage`) 29 | 30 | ### 工具方法 31 | 32 | - [x] 生成小程序调用微信支付的预支付数据方法(`GenPrepay`) 33 | - [x] 校验签名的方法(`VerifySign`) 34 | - [x] 小程序即可设置token方法(`SetAccessToken`) 35 | 36 | ## 安装 37 | 38 | ```sh 39 | > go get github.com/lujin123/go-wechat 40 | ``` 41 | 42 | ## 使用 43 | 44 | ### 初始化 45 | 46 | 目前是有三个对象去分别处理微信小程序服务端接口、需要证书支付接口和无需证书支付接口 47 | 48 | 接口中使用的`http`服务是自己封装的一个带`context`的`http`,这样更加的灵活和方便 49 | 50 | 日志组件使用的是`uber`的`zap`库,这个库功能很强大,个人很喜欢,就直接用了。这样子会直接引入一个依赖,也许应该写一个接口去做,方便适配, 51 | 但是如果要将参数像`zap`那样序列化还挺麻烦,暂时就算了 52 | 53 | #### 微信小程序 54 | ```go 55 | package myapp 56 | 57 | import ( 58 | "github.com/lujin123/wechat" 59 | ) 60 | 61 | func init() { 62 | cfg := wechat.MiniConfig{ 63 | AppId: "", 64 | AppSecret: "", 65 | SignType: "", 66 | TradeType: "", 67 | } 68 | miniService := wechat.NewWxMiniService(&cfg, wechat.NewCtxHttp()) 69 | //调用需要的方法 70 | } 71 | ``` 72 | 73 | #### 微信无需证书支付 74 | ```go 75 | package myapp 76 | 77 | import ( 78 | "github.com/lujin123/wechat" 79 | ) 80 | 81 | func init() { 82 | cfg := wechat.PayConfig{ 83 | AppId:"", 84 | MchId:"", 85 | ApiKey:"", 86 | SignType:"", 87 | TradeType:"", 88 | } 89 | payService := wechat.NewWxPayService(&cfg, wechat.NewCtxHttp()) 90 | //调用需要的方法 91 | } 92 | ``` 93 | 94 | #### 微信需证书支付 95 | ```go 96 | package myapp 97 | 98 | import ( 99 | "github.com/lujin123/wechat" 100 | ) 101 | 102 | func init() { 103 | //这个服务初始化的时候需要传入微信商户后天的证书,用于生成一个带证书的客户端 104 | cfg := wechat.MchConfig{ 105 | AppId:"", 106 | MchId:"", 107 | ApiKey:"", 108 | CaCertFile:"", 109 | ApiCertFile:"", 110 | ApiKeyFile:"", 111 | } 112 | mchService := wechat.NewWxMchService(&cfg) 113 | //调用需要的方法 114 | } 115 | ``` 116 | 117 | ### 接口 118 | 119 | 具体接口参考测试用例 120 | 121 | ## 最后 122 | 123 | 这是项目中需要用到,所以归总了下,方便其他的项目调用,现在直接用这个做个服务给外面调用, 124 | 尤其是关于`token`的过期问题,服务需要保证`token`有效,其他的服务调用即可,否则每次都要关心这个接口调用是否因为`token`失效原因失败了, 125 | 再获取`token`再调用,而且还要做好锁的问题,否则多个线程调用会导致老的`token`又失效的情况,灰常麻烦,具体的问题[参考官网](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html) 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lujin123/wechat 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/stretchr/testify v1.4.0 7 | go.uber.org/zap v1.15.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 7 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 14 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 20 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 23 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 24 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 25 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 26 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 27 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 28 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 29 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 30 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 32 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 33 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 34 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 35 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 37 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 43 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 44 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 45 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 46 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 47 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 52 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 53 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 54 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 55 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 56 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | const ( 11 | contentTypeXML = "application/xml" 12 | contentTypeJSON = "application/json" 13 | ) 14 | 15 | type HandlerFunc = func(response *http.Response, err error) error 16 | 17 | type Http interface { 18 | Get(ctx context.Context, url string, f HandlerFunc) error 19 | Post(ctx context.Context, url, contentType string, body io.Reader, f HandlerFunc) error 20 | PostJSON(ctx context.Context, url string, body io.Reader, f HandlerFunc) error 21 | PostXML(ctx context.Context, url string, body io.Reader, f HandlerFunc) error 22 | Do(ctx context.Context, method, url string, headers map[string]string, body io.Reader, f HandlerFunc) error 23 | } 24 | 25 | type ctxHttp struct { 26 | client *http.Client 27 | } 28 | 29 | func NewCtxHttp() *ctxHttp { 30 | return &ctxHttp{ 31 | client: http.DefaultClient, 32 | } 33 | } 34 | 35 | func NewCtxHttpWithClient(client *http.Client) *ctxHttp { 36 | if client == nil { 37 | client = http.DefaultClient 38 | } 39 | return &ctxHttp{ 40 | client: client, 41 | } 42 | } 43 | 44 | func (h *ctxHttp) Get(ctx context.Context, url string, f HandlerFunc) error { 45 | return h.Do(ctx, http.MethodGet, url, nil, nil, f) 46 | } 47 | 48 | func (h *ctxHttp) Post(ctx context.Context, url, contentType string, body io.Reader, f HandlerFunc) error { 49 | header := map[string]string{ 50 | "Content-Type": contentType, 51 | } 52 | return h.Do(ctx, http.MethodPost, url, header, body, f) 53 | } 54 | func (h *ctxHttp) PostJSON(ctx context.Context, url string, body io.Reader, f HandlerFunc) error { 55 | header := map[string]string{ 56 | "Content-Type": contentTypeJSON, 57 | } 58 | return h.Do(ctx, http.MethodPost, url, header, body, f) 59 | } 60 | func (h *ctxHttp) PostXML(ctx context.Context, url string, body io.Reader, f HandlerFunc) error { 61 | header := map[string]string{ 62 | "Content-Type": contentTypeXML, 63 | } 64 | return h.Do(ctx, http.MethodPost, url, header, body, f) 65 | } 66 | 67 | func (h *ctxHttp) Do(ctx context.Context, method, url string, headers map[string]string, body io.Reader, f HandlerFunc) error { 68 | req, err := http.NewRequest(method, url, body) 69 | if err != nil { 70 | return err 71 | } 72 | if headers != nil { 73 | for k, v := range headers { 74 | req.Header.Set(k, v) 75 | } 76 | } 77 | return h.do(ctx, req, f) 78 | } 79 | 80 | func (h *ctxHttp) do(ctx context.Context, req *http.Request, f HandlerFunc) error { 81 | c := make(chan error) 82 | req = req.WithContext(ctx) 83 | 84 | go func() { 85 | defer func() { 86 | if err := recover(); err != nil { 87 | log.Printf("ctx http goroutine panic, error=%v", err) 88 | } 89 | }() 90 | select { 91 | case <-ctx.Done(): 92 | log.Println("ctx http goroutine quit...") 93 | c <- ctx.Err() 94 | return 95 | default: 96 | c <- f(h.client.Do(req)) 97 | } 98 | }() 99 | return <-c 100 | } 101 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import "go.uber.org/zap" 4 | 5 | var zapLogger, _ = zap.NewDevelopment() 6 | -------------------------------------------------------------------------------- /req_base.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "encoding/xml" 8 | "io" 9 | "net/http" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | const ( 15 | SignTypeMD5 = "MD5" 16 | TradeType = "JSAPI" 17 | ) 18 | 19 | type wxService struct { 20 | client Http 21 | key string 22 | logger *zap.Logger 23 | } 24 | 25 | func (w wxService) SetLogger(log *zap.Logger) { 26 | w.logger = log 27 | } 28 | 29 | func (w wxService) RandString(n int) string { 30 | return RandStringBytesMaskImprSrc(n) 31 | } 32 | 33 | func (w wxService) Get(ctx context.Context, url string, f HandlerFunc) error { 34 | return w.DoReq(ctx, http.MethodGet, url, "", nil, f) 35 | } 36 | 37 | func (w wxService) PostJSON(ctx context.Context, url string, req interface{}, f HandlerFunc) (err error) { 38 | return w.DoReq(ctx, http.MethodPost, url, contentTypeJSON, req, f) 39 | } 40 | 41 | func (w wxService) PostXML(ctx context.Context, url string, req interface{}, f HandlerFunc) (err error) { 42 | return w.DoReq(ctx, http.MethodPost, url, contentTypeXML, req, f) 43 | } 44 | 45 | func (w wxService) DoReq(ctx context.Context, method, url string, contentType string, req interface{}, f HandlerFunc) (err error) { 46 | w.logger.Info("[wx] request", zap.String("url", url), zap.String("contentType", contentType), zap.Any("body", req)) 47 | defer func() { 48 | if err != nil { 49 | w.logger.Error("[wx] request", zap.Error(err)) 50 | } 51 | }() 52 | var ( 53 | body io.Reader 54 | headers map[string]string 55 | ) 56 | switch contentType { 57 | case contentTypeXML: 58 | buf, err := xml.Marshal(&req) 59 | if err != nil { 60 | return err 61 | } 62 | body = bytes.NewBuffer(buf) 63 | case contentTypeJSON: 64 | buf, err := json.Marshal(&req) 65 | if err != nil { 66 | return err 67 | } 68 | body = bytes.NewBuffer(buf) 69 | default: 70 | if buf, ok := req.([]byte); ok { 71 | body = bytes.NewBuffer(buf) 72 | } 73 | } 74 | 75 | if contentType != "" { 76 | headers = map[string]string{ 77 | "Content-Type": contentType, 78 | } 79 | } 80 | return w.client.Do(ctx, method, url, headers, body, f) 81 | } 82 | 83 | func (w wxService) sign(ctx context.Context, req interface{}) (string, error) { 84 | buf, err := json.Marshal(req) 85 | if err != nil { 86 | return "", err 87 | } 88 | var params map[string]string 89 | if err := json.Unmarshal(buf, ¶ms); err != nil { 90 | return "", err 91 | } 92 | 93 | paramStr, err := GenParamStr(params) 94 | if err != nil { 95 | return "", err 96 | } 97 | stringSignTemp := paramStr + "&key=" + w.key 98 | return HashMd5(stringSignTemp), nil 99 | } 100 | -------------------------------------------------------------------------------- /req_wxmch.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/xml" 8 | "io/ioutil" 9 | "net/http" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | const ( 15 | mchPayUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers" 16 | mchReqUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo" 17 | mchRefundUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund" 18 | ) 19 | 20 | type MchService interface { 21 | ReqWxToMchPay(ctx context.Context, req *MchPayReq) (*MchPayResp, error) 22 | ReqMchPayment(ctx context.Context, tradeNo string) (*MchPaymentQueryResp, error) 23 | ReqPayRefund(ctx context.Context, req *MchPayRefundReq) (*MchPayRefundResp, error) 24 | } 25 | 26 | type ( 27 | MchConfig struct { 28 | AppId string 29 | MchId string 30 | ApiKey string 31 | CaCertFile string 32 | ApiCertFile string 33 | ApiKeyFile string 34 | } 35 | 36 | MchPayReq struct { 37 | XMLName xml.Name `xml:"xml" json:"-"` 38 | MchAppID string `xml:"mch_appid" json:"mch_appid"` 39 | MchID string `xml:"mchid" json:"mchid"` 40 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 41 | Sign string `xml:"sign" json:"sign"` 42 | PartnerTradeNO string `xml:"partner_trade_no" json:"partner_trade_no"` 43 | OpenID string `xml:"openid" json:"openid"` 44 | CheckName string `xml:"check_name" json:"check_name"` 45 | Amount int64 `xml:"amount" json:"amount,string"` 46 | Desc string `xml:"desc" json:"desc"` 47 | SpbillCreateIP string `xml:"spbill_create_ip" json:"spbill_create_ip"` 48 | } 49 | 50 | MchPayResp struct { 51 | XMLName xml.Name `xml:"xml"` 52 | ReturnCode string `xml:"return_code"` 53 | ReturnMsg string `xml:"return_msg"` 54 | MchAppID string `xml:"mch_appid"` 55 | MchID string `xml:"mchid"` 56 | NonceStr string `xml:"nonce_str"` 57 | ResultCode string `xml:"result_code"` 58 | ErrCode string `xml:"err_code"` 59 | ErrCodeDes string `xml:"err_code_des"` 60 | PartnerTradeNO string `xml:"partner_trade_no"` 61 | PaymentNO string `xml:"payment_no"` 62 | PaymentTime string `xml:"payment_time"` 63 | } 64 | 65 | mchPaymentQueryReq struct { 66 | XMLName xml.Name `xml:"xml" json:"-"` 67 | MchAppID string `xml:"appid" json:"appid"` 68 | MchID string `xml:"mch_id" json:"mch_id"` 69 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 70 | Sign string `xml:"sign" json:"sign"` 71 | PartnerTradeNO string `xml:"partner_trade_no" json:"partner_trade_no"` 72 | } 73 | 74 | MchPaymentQueryResp struct { 75 | XMLName xml.Name `xml:"xml"` 76 | ReturnCode string `xml:"return_code"` 77 | ReturnMsg string `xml:"return_msg"` 78 | MchAppID string `xml:"appid"` 79 | MchID string `xml:"mch_id"` 80 | NonceStr string `xml:"nonce_str"` 81 | ResultCode string `xml:"result_code"` 82 | ErrCode string `xml:"err_code"` 83 | ErrCodeDes string `xml:"err_code_des"` 84 | PartnerTradeNO string `xml:"partner_trade_no"` 85 | DetailID string `xml:"detail_id"` 86 | Status string `xml:"status"` 87 | Reason string `xml:"reason"` 88 | OpenID string `xml:"openid"` 89 | TransferName string `xml:"transferName"` 90 | PaymentAmount int64 `xml:"payment_amount"` 91 | TransferTime string `xml:"transfer_time"` 92 | PaymentTime string `xml:"PaymentTime"` 93 | Desc string `xml:"desc"` 94 | } 95 | 96 | MchPayRefundReq struct { 97 | XMLName xml.Name `xml:"xml" json:"-"` 98 | AppID string `xml:"appid" json:"appid"` 99 | MchID string `xml:"mch_id" json:"mch_id"` 100 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 101 | Sign string `xml:"sign" json:"sign"` 102 | TransactionId string `xml:"transaction_id" json:"transaction_id"` 103 | OutRefundNo string `xml:"out_refund_no" json:"out_refund_no"` 104 | TotalFee int64 `xml:"total_fee" json:"total_fee,string"` 105 | RefundFee int64 `xml:"refund_fee" json:"refund_fee,string"` 106 | RefundDesc string `xml:"refund_desc" json:"refund_desc"` 107 | } 108 | 109 | MchPayRefundResp struct { 110 | XMLName xml.Name `xml:"xml"` 111 | ReturnCode string `xml:"return_code"` 112 | ReturnMsg string `xml:"return_msg"` 113 | MchAppID string `xml:"appid"` 114 | MchID string `xml:"mch_id"` 115 | NonceStr string `xml:"nonce_str"` 116 | ResultCode string `xml:"result_code"` 117 | ErrCode string `xml:"err_code"` 118 | ErrCodeDes string `xml:"err_code_des"` 119 | TransactionId string `xml:"transaction_id"` 120 | OutTradeNo string `xml:"out_trade_no"` 121 | OutRefundNo string `xml:"out_refund_no"` 122 | RefundId string `xml:"refund_id"` 123 | RefundFee int64 `xml:"refund_fee"` 124 | TotalFee int64 `xml:"total_fee"` 125 | CashFee int64 `xml:"cash_fee"` 126 | } 127 | 128 | wxMch struct { 129 | cfg *MchConfig 130 | wxService 131 | } 132 | ) 133 | 134 | func NewWxMchService(cfg *MchConfig) *wxMch { 135 | s := &wxMch{ 136 | cfg, 137 | wxService{ 138 | client: nil, 139 | key: cfg.ApiKey, 140 | logger: zapLogger, 141 | }, 142 | } 143 | s.client = NewCtxHttpWithClient(s.TLSClient()) 144 | zapLogger.Info("init wx mch service success...") 145 | return s 146 | } 147 | 148 | // 企业付款到零钱接口 149 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 150 | func (w wxMch) ReqWxToMchPay(ctx context.Context, req *MchPayReq) (*MchPayResp, error) { 151 | sign, err := w.sign(ctx, &req) 152 | if err != nil { 153 | return nil, err 154 | } 155 | req.Sign = sign 156 | 157 | var resp MchPayResp 158 | if err := w.PostXML(ctx, mchPayUrl, &req, func(response *http.Response, err error) error { 159 | if err != nil { 160 | return err 161 | } 162 | return xml.NewDecoder(response.Body).Decode(&resp) 163 | }); err != nil { 164 | return nil, err 165 | } 166 | w.logger.Info("[wxmch] req wx to mch pay", zap.Any("body", resp)) 167 | return &resp, nil 168 | } 169 | 170 | // 企业付款到零钱查询接口 171 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_3 172 | func (w wxMch) ReqMchPayment(ctx context.Context, tradeNo string) (*MchPaymentQueryResp, error) { 173 | req := mchPaymentQueryReq{ 174 | MchAppID: w.cfg.AppId, 175 | MchID: w.cfg.MchId, 176 | NonceStr: w.RandString(32), 177 | Sign: "", 178 | PartnerTradeNO: tradeNo, 179 | } 180 | sign, err := w.sign(ctx, &req) 181 | if err != nil { 182 | return nil, err 183 | } 184 | req.Sign = sign 185 | 186 | var resp MchPaymentQueryResp 187 | if err := w.PostXML(ctx, mchReqUrl, &req, func(response *http.Response, err error) error { 188 | if err != nil { 189 | return err 190 | } 191 | return xml.NewDecoder(response.Body).Decode(&resp) 192 | }); err != nil { 193 | return nil, err 194 | } 195 | w.logger.Info("[wxmch] req mch payment", zap.Any("body", resp)) 196 | return &resp, nil 197 | } 198 | 199 | // 申请退款接口 200 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4 201 | func (w wxMch) ReqPayRefund(ctx context.Context, req *MchPayRefundReq) (*MchPayRefundResp, error) { 202 | sign, err := w.sign(ctx, &req) 203 | if err != nil { 204 | return nil, err 205 | } 206 | req.Sign = sign 207 | 208 | var resp MchPayRefundResp 209 | if err := w.PostXML(ctx, mchRefundUrl, &req, func(response *http.Response, err error) error { 210 | if err != nil { 211 | return err 212 | } 213 | return xml.NewDecoder(response.Body).Decode(&resp) 214 | }); err != nil { 215 | return nil, err 216 | } 217 | w.logger.Info("[wxmch] req mch pay refund", zap.Any("body", resp)) 218 | return &resp, nil 219 | } 220 | 221 | func (w wxMch) TLSClient() *http.Client { 222 | pool := x509.NewCertPool() 223 | caCrt, err := ioutil.ReadFile(w.cfg.CaCertFile) 224 | if err != nil { 225 | w.logger.Panic("[wx] read CACertFile", zap.Error(err)) 226 | } 227 | pool.AppendCertsFromPEM(caCrt) 228 | 229 | cliCrt, err := tls.LoadX509KeyPair(w.cfg.ApiCertFile, w.cfg.ApiKeyFile) 230 | if err != nil { 231 | w.logger.Panic("[wx] LoadX509KeyPair", zap.Error(err)) 232 | } 233 | 234 | tr := &http.Transport{ 235 | TLSClientConfig: &tls.Config{ 236 | RootCAs: pool, 237 | Certificates: []tls.Certificate{cliCrt}, 238 | }, 239 | } 240 | return &http.Client{ 241 | Transport: tr, 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /req_wxmini.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | ) 13 | 14 | const ( 15 | code2sessionUrl = "https://api.weixin.qq.com/sns/jscode2session" 16 | accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token" 17 | subscribeMessageUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send" 18 | wxCodeUnlimitedUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit" 19 | checkImageUrl = "https://api.weixin.qq.com/wxa/img_sec_check" 20 | checkMsgUrl = "https://api.weixin.qq.com/wxa/msg_sec_check" 21 | ) 22 | 23 | var ( 24 | ErrTokenMissing = errors.New("[gowechat] token missing") 25 | ) 26 | 27 | type MiniService interface { 28 | SetAccessToken(token string) 29 | ReqCode2Session(ctx context.Context, code string) (*SessionResp, error) 30 | ReqAccessToken(ctx context.Context) (*AccessTokenResp, error) 31 | SendSubscribeMessage(ctx context.Context, req *SubscribeMessageReq) (*ErrorResp, error) 32 | ReqWxCodeUnlimited(ctx context.Context, req *WxCodeUnlimitedReq) ([]byte, error) 33 | CheckImage(ctx context.Context, media []byte) (*ErrorResp, error) 34 | CheckMessage(ctx context.Context, msg string) (*ErrorResp, error) 35 | } 36 | 37 | type ( 38 | MiniConfig struct { 39 | AppId string 40 | AppSecret string 41 | SignType string 42 | TradeType string 43 | } 44 | ErrorResp struct { 45 | ErrCode int `json:"errcode"` 46 | ErrMsg string `json:"errmsg"` 47 | } 48 | SessionResp struct { 49 | ErrorResp 50 | OpenId string `json:"openid"` 51 | SessionKey string `json:"session_key"` 52 | UnionId string `json:"unionid"` 53 | } 54 | AccessTokenResp struct { 55 | ErrorResp 56 | AccessToken string `json:"access_token"` //获取到的凭证 57 | ExpiresIn int64 `json:"expires_in"` //凭证有效时间,单位:秒。目前是7200秒之内的值。 58 | } 59 | SubscribeMessageReq struct { 60 | Touser string `json:"touser"` 61 | TemplateId string `json:"template_id"` 62 | Page string `json:"page"` 63 | Data map[string]interface{} `json:"data"` 64 | MiniprogramState string `json:"miniprogram_state"` 65 | Lang string `json:"lang"` 66 | } 67 | 68 | WxCodeUnlimitedReq struct { 69 | Scene string `json:"scene"` 70 | Page string `json:"page"` 71 | Width int `json:"width"` 72 | AutoColor bool `json:"auto_color"` 73 | LineColor struct { 74 | R int `json:"r"` 75 | G int `json:"g"` 76 | B int `json:"b"` 77 | } `json:"line_color"` 78 | IsHyaline bool `json:"is_hyaline"` 79 | } 80 | ) 81 | 82 | type wxMini struct { 83 | cfg *MiniConfig 84 | token string 85 | wxService 86 | } 87 | 88 | func NewWxMiniService(cfg *MiniConfig, client Http) *wxMini { 89 | s := &wxMini{ 90 | cfg: cfg, 91 | wxService: wxService{ 92 | client: client, 93 | logger: zapLogger, 94 | }, 95 | } 96 | zapLogger.Info("init wx mini service success...") 97 | return s 98 | } 99 | 100 | //设置access_token 101 | func (w *wxMini) SetAccessToken(token string) { 102 | w.token = token 103 | } 104 | 105 | // 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程 106 | // 文档地址:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 107 | func (w wxMini) ReqCode2Session(ctx context.Context, jsCode string) (*SessionResp, error) { 108 | url := fmt.Sprintf("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", code2sessionUrl, w.cfg.AppId, w.cfg.AppSecret, jsCode) 109 | var sessionResp SessionResp 110 | if err := w.Get(ctx, url, func(response *http.Response, err error) error { 111 | if err != nil { 112 | return err 113 | } 114 | return json.NewDecoder(response.Body).Decode(&sessionResp) 115 | }); err != nil { 116 | return nil, err 117 | } 118 | 119 | return &sessionResp, nil 120 | } 121 | 122 | // 获取小程序全局唯一后台接口调用凭据(access_token) 123 | // 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html 124 | func (w wxMini) ReqAccessToken(ctx context.Context) (*AccessTokenResp, error) { 125 | url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenUrl, w.cfg.AppId, w.cfg.AppSecret) 126 | var resp AccessTokenResp 127 | if err := w.Get(ctx, url, func(response *http.Response, err error) error { 128 | if err != nil { 129 | return err 130 | } 131 | return json.NewDecoder(response.Body).Decode(&resp) 132 | }); err != nil { 133 | return nil, err 134 | } 135 | 136 | return &resp, nil 137 | } 138 | 139 | // 发送订阅消息 140 | // 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 141 | func (w wxMini) SendSubscribeMessage(ctx context.Context, req *SubscribeMessageReq) (*ErrorResp, error) { 142 | if err := w.checkToken(); err != nil { 143 | return nil, err 144 | } 145 | url := fmt.Sprintf("%s?access_token=%s", subscribeMessageUrl, w.token) 146 | var resp ErrorResp 147 | if err := w.PostJSON(ctx, url, req, func(response *http.Response, err error) error { 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return json.NewDecoder(response.Body).Decode(&resp) 153 | }); err != nil { 154 | return nil, err 155 | } 156 | return &resp, nil 157 | } 158 | 159 | // 获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制 160 | // 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html 161 | func (w wxMini) ReqWxCodeUnlimited(ctx context.Context, req *WxCodeUnlimitedReq) ([]byte, error) { 162 | if err := w.checkToken(); err != nil { 163 | return nil, err 164 | } 165 | 166 | url := fmt.Sprintf("%s?access_token=%s", wxCodeUnlimitedUrl, w.token) 167 | var buff []byte 168 | if err := w.PostJSON(ctx, url, req, func(response *http.Response, err error) error { 169 | if err != nil { 170 | return err 171 | } 172 | buff, err = ioutil.ReadAll(response.Body) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | // 检查是否为异常 178 | var resp ErrorResp 179 | // 如果无法解析,就认为是二维码,能解析出来就是异常结果 180 | if err := json.Unmarshal(buff, &resp); err != nil { 181 | return nil 182 | } 183 | return errors.New(resp.ErrMsg) 184 | }); err != nil { 185 | return nil, err 186 | } 187 | return buff, nil 188 | } 189 | 190 | // 校验一张图片是否含有违法违规内容 191 | // 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html 192 | func (w wxMini) CheckImage(ctx context.Context, media []byte) (*ErrorResp, error) { 193 | if err := w.checkToken(); err != nil { 194 | return nil, err 195 | } 196 | url := fmt.Sprintf("%s?access_token=%s", checkImageUrl, w.token) 197 | 198 | var bodyBuff bytes.Buffer 199 | bodyWriter := multipart.NewWriter(&bodyBuff) 200 | if err := bodyWriter.WriteField("media", string(media)); err != nil { 201 | return nil, err 202 | } 203 | var resp ErrorResp 204 | if err := w.DoReq(ctx, http.MethodPost, url, bodyWriter.FormDataContentType(), bodyBuff.Bytes(), func(response *http.Response, err error) error { 205 | if err != nil { 206 | return err 207 | } 208 | 209 | return json.NewDecoder(response.Body).Decode(&resp) 210 | }); err != nil { 211 | return nil, err 212 | } 213 | 214 | return &resp, nil 215 | } 216 | 217 | // 检查一段文本是否含有违法违规内容 218 | // 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html 219 | func (w wxMini) CheckMessage(ctx context.Context, msg string) (*ErrorResp, error) { 220 | if err := w.checkToken(); err != nil { 221 | return nil, err 222 | } 223 | req := map[string]string{ 224 | "content": msg, 225 | } 226 | url := fmt.Sprintf("%s?access_token=%s", checkMsgUrl, w.token) 227 | var resp ErrorResp 228 | if err := w.PostJSON(ctx, url, req, func(response *http.Response, err error) error { 229 | if err != nil { 230 | return err 231 | } 232 | 233 | return json.NewDecoder(response.Body).Decode(&resp) 234 | }); err != nil { 235 | return nil, err 236 | } 237 | return &resp, nil 238 | } 239 | 240 | //////////////////////////////////////////////////////////////////////////////////////////////// 241 | func (w wxMini) checkToken() error { 242 | if w.token == "" { 243 | return ErrTokenMissing 244 | } 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /req_wxmini_test.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var ( 11 | token = "" 12 | miniService MiniService 13 | ) 14 | 15 | func init() { 16 | cfg := MiniConfig{ 17 | AppId: "", 18 | AppSecret: "", 19 | SignType: "", 20 | TradeType: "", 21 | } 22 | miniService = NewWxMiniService(&cfg, NewCtxHttp()) 23 | } 24 | 25 | func TestWxMini_ReqAccessToken(t *testing.T) { 26 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 27 | defer cancel() 28 | 29 | resp, err := miniService.ReqAccessToken(ctx) 30 | assert.Nil(t, err) 31 | t.Log(resp) 32 | assert.Equal(t, "", resp.ErrMsg) 33 | miniService.SetAccessToken(resp.AccessToken) 34 | } 35 | 36 | func TestWxMini_ReqWxCodeUnlimited(t *testing.T) { 37 | miniService.SetAccessToken(token) 38 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 39 | defer cancel() 40 | codeBuff, err := miniService.ReqWxCodeUnlimited(ctx, &WxCodeUnlimitedReq{ 41 | Scene: "ljabcjkabc", 42 | }) 43 | assert.Nil(t, err) 44 | t.Log(codeBuff) 45 | } 46 | 47 | func TestWxMini_CheckMessage(t *testing.T) { 48 | miniService.SetAccessToken(token) 49 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 50 | defer cancel() 51 | 52 | tests := []struct { 53 | Msg string 54 | HasErr bool 55 | }{ 56 | {"hello", false}, 57 | {"习近平", true}, 58 | {"任志强", false}, 59 | {"郝海东", false}, 60 | } 61 | for _, test := range tests { 62 | resp, err := miniService.CheckMessage(ctx, test.Msg) 63 | assert.Nil(t, err) 64 | if test.HasErr { 65 | assert.Equal(t, resp.ErrCode != 0, true, "error msg = %s", resp.ErrMsg) 66 | } else { 67 | assert.Equal(t, resp.ErrCode, 0) 68 | } 69 | } 70 | } 71 | 72 | func TestWxMini_ReqCode2Session(t *testing.T) { 73 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 74 | defer cancel() 75 | 76 | resp, err := miniService.ReqCode2Session(ctx, "12343") 77 | assert.Nil(t, err) 78 | t.Logf("ReqCode2Session resp: %+v", resp) 79 | } 80 | -------------------------------------------------------------------------------- /req_wxpay.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | const ( 14 | unifiedOrderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder" 15 | closeOrderUrl = "https://api.mch.weixin.qq.com/pay/closeorder" 16 | queryOrderUrl = "https://api.mch.weixin.qq.com/pay/orderquery" 17 | ) 18 | 19 | type PayService interface { 20 | // req function 21 | ReqUnifiedOrder(ctx context.Context, req *UnifiedOrderReq) (*UnifiedOrderResp, error) 22 | ReqQueryOrder(ctx context.Context, tradeNo string) (*QueryOrderResp, error) 23 | ReqCloseOrder(ctx context.Context, tradeNo string) (*CloseOrderResp, error) 24 | 25 | // utils function 26 | GenPrepay(ctx context.Context, prepayId, nonceStr string) (*PrepayReturn, error) 27 | VerifySign(ctx context.Context, req *NotifyReq) bool 28 | } 29 | 30 | type ( 31 | PayConfig struct { 32 | AppId string 33 | MchId string 34 | ApiKey string 35 | SignType string 36 | TradeType string 37 | } 38 | 39 | UnifiedOrderReq struct { 40 | XMLName xml.Name `json:"-" xml:"xml"` 41 | AppId string `json:"appid" xml:"appid"` //小程序ID 42 | MchId string `json:"mch_id" xml:"mch_id"` //商户号 43 | DeviceInfo string `json:"device_info" xml:"device_info"` //设备号 44 | NonceStr string `json:"nonce_str" xml:"nonce_str"` //随机字符串 45 | Sign string `json:"sign" xml:"sign"` //签名 46 | SignType string `json:"sign_type" xml:"sign_type"` //签名类型,默认为MD5,支持HMAC-SHA256和MD5 47 | Body string `json:"body" xml:"body"` //商品描述 48 | Detail string `json:"detail" xml:"detail"` //商品详情 49 | Attach string `json:"attach" xml:"attach"` //附加数据 50 | OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"` //商户订单号 51 | FeeType string `json:"fee_type" xml:"fee_type"` //标价币种 52 | TotalFee int64 `json:"total_fee,string" xml:"total_fee"` //标价金额 53 | SpbillCreateIp string `json:"spbill_create_ip" xml:"spbill_create_ip"` //终端IP 54 | TimeStart string `json:"time_start" xml:"time_start"` //交易起始时间 55 | TimeExpire string `json:"time_expire" xml:"time_expire"` //交易结束时间 56 | GoodsTag string `json:"goods_tag" xml:"goods_tag"` //订单优惠标记 57 | NotifyUrl string `json:"notify_url" xml:"notify_url"` //通知地址 58 | TradeType string `json:"trade_type" xml:"trade_type"` //交易类型 59 | OpenId string `json:"openid" xml:"openid"` //用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识 60 | } 61 | 62 | UnifiedOrderResp struct { 63 | XMLName xml.Name `xml:"xml"` 64 | ReturnCode string `xml:"return_code"` 65 | ReturnMsg string `xml:"return_msg"` 66 | AppID string `xml:"appid"` 67 | MchID string `xml:"mch_id"` 68 | NonceStr string `xml:"nonce_str"` 69 | Sign string `xml:"sign"` 70 | ResultCode string `xml:"result_code"` 71 | ErrCode string `xml:"err_code"` 72 | ErrCodeDes string `xml:"err_code_des"` 73 | TradeType string `xml:"trade_type"` 74 | PrepayId string `xml:"prepay_id"` 75 | CodeUrl string `xml:"code_url"` 76 | } 77 | 78 | CloseOrderReq struct { 79 | XMLName xml.Name `json:"-" xml:"xml"` 80 | AppId string `json:"appid" xml:"appid"` 81 | MchId string `json:"mch_id" xml:"mch_id"` 82 | NonceStr string `json:"nonce_str" xml:"nonce_str"` 83 | OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"` 84 | Sign string `json:"sign" xml:"sign"` 85 | SignType string `json:"sign_type" xml:"sign_type"` 86 | } 87 | 88 | CloseOrderResp struct { 89 | ReturnCode string `xml:"return_code"` 90 | ReturnMsg string `xml:"return_msg"` 91 | AppID string `xml:"appid"` 92 | MchID string `xml:"mch_id"` 93 | NonceStr string `xml:"nonce_str"` 94 | Sign string `xml:"sign"` 95 | ResultCode string `xml:"result_code"` 96 | ResultMsg string `xml:"result_msg"` 97 | ErrCode string `xml:"err_code"` 98 | ErrCodeDes string `xml:"err_code_des"` 99 | } 100 | 101 | PrepayReturn struct { 102 | AppId string `json:"appId"` 103 | TimeStamp string `json:"timeStamp"` 104 | NonceStr string `json:"nonceStr"` 105 | Package string `json:"package"` 106 | SignType string `json:"signType"` 107 | PaySign string `json:"paySign"` 108 | } 109 | 110 | NotifyReq struct { 111 | XMLName xml.Name `xml:"xml" json:"-"` 112 | ReturnCode string `xml:"return_code" json:"return_code"` 113 | ReturnMsg string `xml:"return_msg" json:"return_msg"` 114 | AppID string `xml:"appid" json:"appid"` 115 | MchID string `xml:"mch_id" json:"mch_id"` 116 | DeviceInfo string `xml:"device_info" json:"device_info"` 117 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 118 | Sign string `xml:"sign" json:"sign"` 119 | SignType string `xml:"sign_type" json:"sign_type"` 120 | ResultCode string `xml:"result_code" json:"result_code"` 121 | ErrCode string `xml:"err_code" json:"err_code"` 122 | ErrCodeDes string `xml:"err_code_des" json:"err_code_des"` 123 | OpenId string `xml:"openid" json:"openid"` 124 | IsSubscribe string `xml:"is_subscribe" json:"is_subscribe"` 125 | TradeType string `xml:"trade_type" json:"trade_type"` 126 | BankType string `xml:"bank_type" json:"bank_type"` 127 | TotalFee string `xml:"total_fee" json:"total_fee"` 128 | FeeType string `xml:"fee_type" json:"fee_type"` 129 | CashFee string `xml:"cash_fee" json:"cash_fee"` 130 | CashFeeType string `xml:"cash_fee_type" json:"cash_fee_type"` 131 | TransactionId string `xml:"transaction_id" json:"transaction_id"` 132 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no"` 133 | TimeEnd string `xml:"time_end" json:"time_end"` 134 | } 135 | 136 | NotifyResp struct { 137 | XMLName xml.Name `xml:"xml"` 138 | ReturnCode string `xml:"return_code"` 139 | ReturnMsg string `xml:"return_msg"` 140 | } 141 | 142 | QueryOrderReq struct { 143 | XMLName xml.Name `xml:"xml" json:"-"` 144 | AppID string `xml:"appid" json:"appid"` 145 | MchID string `xml:"mchid" json:"mchid"` 146 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no"` 147 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 148 | Sign string `xml:"sign" json:"sign"` 149 | SignType string `xml:"sign_type" json:"sign_type"` 150 | } 151 | 152 | QueryOrderResp struct { 153 | XMLName xml.Name `xml:"xml" json:"-"` 154 | ReturnCode string `xml:"return_code" json:"return_code"` 155 | ReturnMsg string `xml:"return_msg" json:"return_msg"` 156 | AppID string `xml:"appid" json:"appid"` 157 | MchID string `xml:"mch_id" json:"mch_id"` 158 | NonceStr string `xml:"nonce_str" json:"nonce_str"` 159 | Sign string `xml:"sign" json:"sign"` 160 | ResultCode string `xml:"result_code" json:"result_code"` 161 | ErrCode string `xml:"err_code" json:"err_code"` 162 | ErrCodeDes string `xml:"err_code_des" json:"err_code_des"` 163 | DeviceInfo string `xml:"device_info" json:"device_info"` 164 | OpenId string `xml:"openid" json:"openid"` 165 | IsSubscribe string `xml:"is_subscribe" json:"is_subscribe"` 166 | TradeType string `xml:"trade_type" json:"trade_type"` 167 | //SUCCESS—支付成功 168 | //REFUND—转入退款 169 | //NOTPAY—未支付 170 | //CLOSED—已关闭 171 | //REVOKED—已撤销(刷卡支付) 172 | //USERPAYING--用户支付中 173 | //PAYERROR--支付失败(其他原因,如银行返回失败) 174 | TradeState string `xml:"trade_state" json:"trade_state"` //交易状态 175 | TradeStateDesc string `xml:"trade_state_desc" json:"trade_state_desc"` //交易状态描述 176 | BankType string `xml:"bank_type" json:"bank_type"` 177 | TotalFee int64 `xml:"total_fee" json:"total_fee"` 178 | SettlementTotalFee int64 `xml:"settlement_total_fee" json:"settlement_total_fee"` 179 | FeeType string `xml:"fee_type" json:"fee_type"` 180 | CashFee int64 `xml:"cash_fee" json:"cash_fee"` 181 | CashFeeType string `xml:"cash_fee_type" json:"cash_fee_type"` 182 | TransactionId string `xml:"transaction_id" json:"transaction_id"` 183 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no"` 184 | TimeEnd string `xml:"time_end" json:"time_end"` 185 | } 186 | ) 187 | 188 | type wxPay struct { 189 | cfg *PayConfig 190 | wxService 191 | } 192 | 193 | func NewWxPayService(cfg *PayConfig, client Http) *wxPay { 194 | s := &wxPay{ 195 | cfg, 196 | wxService{ 197 | client: client, 198 | key: cfg.ApiKey, 199 | logger: zapLogger, 200 | }, 201 | } 202 | zapLogger.Info("init wx pay service success...") 203 | return s 204 | } 205 | 206 | // 统一下单接口 207 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 208 | func (w wxPay) ReqUnifiedOrder(ctx context.Context, req *UnifiedOrderReq) (*UnifiedOrderResp, error) { 209 | var resp UnifiedOrderResp 210 | if err := w.PostXML(ctx, unifiedOrderUrl, &req, func(response *http.Response, err error) error { 211 | if err != nil { 212 | return err 213 | } 214 | return xml.NewDecoder(response.Body).Decode(&resp) 215 | }); err != nil { 216 | return nil, err 217 | } 218 | w.logger.Info("[wxpay] unified order", zap.Any("resp", resp)) 219 | return &resp, nil 220 | } 221 | 222 | // 订单查询接口 223 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2 224 | func (w wxPay) ReqQueryOrder(ctx context.Context, tradeNo string) (*QueryOrderResp, error) { 225 | req := QueryOrderReq{ 226 | AppID: w.cfg.AppId, 227 | MchID: w.cfg.MchId, 228 | OutTradeNo: tradeNo, 229 | NonceStr: w.RandString(32), 230 | Sign: "", 231 | SignType: SignTypeMD5, 232 | } 233 | sign, err := w.sign(ctx, &req) 234 | if err != nil { 235 | return nil, err 236 | } 237 | req.Sign = sign 238 | var resp QueryOrderResp 239 | if err := w.PostXML(ctx, queryOrderUrl, &req, func(response *http.Response, err error) error { 240 | if err != nil { 241 | return err 242 | } 243 | return xml.NewDecoder(response.Body).Decode(&resp) 244 | }); err != nil { 245 | return nil, err 246 | } 247 | w.logger.Info("[wxpay] query order", zap.Any("resp", resp)) 248 | return &resp, nil 249 | } 250 | 251 | // 关闭订单 252 | // 以下情况需要调用关单接口:商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。 253 | // 接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3 254 | func (w wxPay) ReqCloseOrder(ctx context.Context, tradeNo string) (*CloseOrderResp, error) { 255 | req := CloseOrderReq{ 256 | AppId: w.cfg.AppId, 257 | MchId: w.cfg.MchId, 258 | NonceStr: w.RandString(32), 259 | OutTradeNo: tradeNo, 260 | Sign: "", 261 | SignType: SignTypeMD5, 262 | } 263 | sign, err := w.sign(ctx, &req) 264 | if err != nil { 265 | return nil, err 266 | } 267 | req.Sign = sign 268 | 269 | var resp CloseOrderResp 270 | if err := w.PostXML(ctx, closeOrderUrl, &req, func(response *http.Response, err error) error { 271 | if err != nil { 272 | return err 273 | } 274 | return xml.NewDecoder(response.Body).Decode(&resp) 275 | }); err != nil { 276 | return nil, err 277 | } 278 | w.logger.Info("[wxpay] close order", zap.Any("resp", resp)) 279 | return &resp, nil 280 | } 281 | 282 | // 生成小程序预支付数据 283 | func (w wxPay) GenPrepay(ctx context.Context, prepayId, nonceStr string) (*PrepayReturn, error) { 284 | if nonceStr == "" { 285 | nonceStr = w.RandString(32) 286 | } 287 | prepay := PrepayReturn{ 288 | AppId: w.cfg.AppId, 289 | TimeStamp: strconv.FormatInt(time.Now().Unix(), 10), 290 | NonceStr: nonceStr, 291 | Package: "prepay_id=" + prepayId, 292 | SignType: SignTypeMD5, 293 | PaySign: "", 294 | } 295 | sign, err := w.sign(ctx, &prepay) 296 | if err != nil { 297 | return nil, err 298 | } 299 | prepay.PaySign = sign 300 | return &prepay, nil 301 | } 302 | 303 | // 校验签名 304 | func (w wxPay) VerifySign(ctx context.Context, req *NotifyReq) bool { 305 | oldSign := req.Sign 306 | req.Sign = "" 307 | sign, err := w.sign(ctx, req) 308 | if err != nil { 309 | w.logger.Error("[wxpay] verify sign", zap.Error(err)) 310 | return false 311 | } 312 | return oldSign == sign 313 | } 314 | -------------------------------------------------------------------------------- /req_wxpay_test.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import "testing" 4 | 5 | var ( 6 | payService PayService 7 | ) 8 | 9 | func init() { 10 | cfg := PayConfig{ 11 | AppId: "wx3dfcc5eb4a8e335f", 12 | MchId: "", 13 | ApiKey: "", 14 | SignType: "", 15 | TradeType: "", 16 | } 17 | payService = NewWxPayService(&cfg, NewCtxHttp()) 18 | } 19 | 20 | func TestWxPay_GenPrepay(t *testing.T) { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "math/rand" 7 | "net/url" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 14 | letterIdxBits = 6 // 6 bits to represent a letter index 15 | letterIdxMask = 1<= 0; { 24 | if remain == 0 { 25 | cache, remain = src.Int63(), letterIdxMax 26 | } 27 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 28 | b[i] = letterBytes[idx] 29 | i-- 30 | } 31 | cache >>= letterIdxBits 32 | remain-- 33 | } 34 | 35 | return string(b) 36 | } 37 | 38 | func GenParamStr(params map[string]string) (string, error) { 39 | v := url.Values{} 40 | for k := range params { 41 | value := params[k] 42 | if value != "" { 43 | v.Set(k, value) 44 | } 45 | } 46 | escapedString := v.Encode() 47 | return url.QueryUnescape(escapedString) 48 | } 49 | 50 | func HashMd5(signStr string) string { 51 | hasher := md5.New() 52 | hasher.Write([]byte(signStr)) 53 | sign := strings.ToUpper(hex.EncodeToString(hasher.Sum(nil))) 54 | return sign 55 | } 56 | --------------------------------------------------------------------------------