├── .gitignore ├── LICENSE ├── README.md ├── alipay.png ├── doc.go ├── donors.md ├── go.mod ├── go.sum ├── internal ├── debug │ ├── README.md │ ├── api │ │ ├── debug.go │ │ ├── release.go │ │ └── retry │ │ │ ├── debug.go │ │ │ └── release.go │ ├── callback │ │ ├── debug.go │ │ └── release.go │ └── mch │ │ ├── api │ │ ├── debug.go │ │ └── release.go │ │ └── callback │ │ ├── debug.go │ │ └── release.go └── util │ ├── aes_crypto.go │ ├── aes_crypto_test.go │ ├── doc.go │ ├── helper.go │ ├── sign.go │ ├── sign_test.go │ ├── to_lower.go │ └── to_lower_test.go ├── mch ├── core │ ├── api_base_url.go │ ├── buffer_pool.go │ ├── client.go │ ├── const.go │ ├── context.go │ ├── doc.go │ ├── error.go │ ├── error_handler.go │ ├── handler.go │ ├── native_url.go │ ├── server.go │ ├── sign.go │ ├── sign_test.go │ ├── ssl_http_client_go1.6.go │ ├── ssl_http_client_go1.7.go │ ├── ssl_http_client_go1.8.go │ └── time.go ├── doc.go ├── mmpaymkttransfers │ ├── gethbinfo.go │ ├── gettransferinfo.go │ ├── promotion │ │ └── transfers.go │ ├── query_coupon_stock.go │ ├── send_coupon.go │ ├── sendgroupredpack.go │ └── sendredpack.go ├── pay │ ├── closeorder.go │ ├── downloadbill.go │ ├── micropay.go │ ├── orderquery.go │ ├── refund.go │ ├── refundquery.go │ ├── reverse.go │ └── unifiedorder.go ├── payutil │ └── report.go ├── promotion │ └── query_coupon.go └── tools │ ├── authcodetoopenid.go │ └── shorturl.go ├── mp ├── README.md ├── account │ ├── README.md │ ├── doc.go │ └── event.go ├── base │ ├── doc.go │ ├── getcallbackip.go │ ├── shorturl.go │ └── uploadimg.go ├── bizwifi │ ├── device │ │ ├── add.go │ │ ├── delete.go │ │ └── list.go │ ├── doc.go │ ├── event.go │ ├── homepage │ │ ├── get.go │ │ └── set.go │ ├── qrcode │ │ └── get.go │ ├── shop │ │ └── list.go │ └── statistics │ │ └── list.go ├── card │ ├── advanced_info_struct.go │ ├── base_info_struct.go │ ├── boardingpass │ │ └── checkin.go │ ├── card.go │ ├── card_struct.go │ ├── code │ │ ├── code.go │ │ ├── consume.go │ │ ├── decrypt.go │ │ ├── get.go │ │ ├── unavailable.go │ │ └── update.go │ ├── color.go │ ├── datacube │ │ └── doc.go │ ├── doc.go │ ├── event.go │ ├── meetingticket │ │ └── updateuser.go │ ├── membercard │ │ ├── activate.go │ │ ├── updateuser.go │ │ └── userinfo │ │ │ └── get.go │ ├── movieticket │ │ └── updateuser.go │ ├── mpnews │ │ └── gethtml.go │ ├── qrcode │ │ └── create.go │ ├── sign.go │ ├── testwhitelist │ │ └── set.go │ └── user │ │ └── getcardlist.go ├── core │ ├── README.md │ ├── access_token_server.go │ ├── buffer_pool.go │ ├── callback20160118.png │ ├── client.go │ ├── client_upload.go │ ├── context.go │ ├── doc.go │ ├── error.go │ ├── error_handler.go │ ├── example_test.go │ ├── handler.go │ ├── mixed_msg.go │ ├── server.go │ ├── server_test.go │ └── started_checker.go ├── datacube │ ├── README.md │ ├── article.go │ ├── card │ │ ├── card.go │ │ ├── getcardbizuininfo.go │ │ ├── getcardcardinfo.go │ │ └── getcardmembercardinfo.go │ ├── doc.go │ ├── interface.go │ ├── reqeust.go │ ├── upstream_msg.go │ └── user.go ├── dkf │ ├── README.md │ ├── account │ │ ├── README.md │ │ ├── account.go │ │ └── head_img.go │ ├── doc.go │ ├── kf_list.go │ ├── record │ │ ├── README.md │ │ ├── iter.go │ │ └── record.go │ ├── resp.go │ └── session │ │ ├── README.md │ │ ├── event.go │ │ └── sesson.go ├── doc.go ├── jssdk │ ├── README.md │ ├── card_ticket_server.go │ ├── doc.go │ ├── sign.go │ ├── sign_test.go │ └── ticket_server.go ├── material │ ├── README.md │ ├── doc.go │ ├── download.go │ ├── material.go │ ├── news.go │ ├── upload.go │ └── video.go ├── media │ ├── README.md │ ├── doc.go │ ├── download.go │ ├── mpvideo.go │ ├── news.go │ └── upload.go ├── menu │ ├── README.md │ ├── api.go │ ├── api_conditional.go │ ├── doc.go │ ├── event.go │ ├── menu.go │ └── menu_info.go ├── message │ ├── callback │ │ ├── doc.go │ │ ├── request │ │ │ ├── doc.go │ │ │ ├── event.go │ │ │ ├── event_test.go │ │ │ ├── msg.go │ │ │ └── msg_test.go │ │ └── response │ │ │ └── msg.go │ ├── custom │ │ ├── custom.go │ │ └── msg.go │ ├── doc.go │ ├── mass │ │ ├── doc.go │ │ ├── event.go │ │ ├── mass.go │ │ ├── mass2all │ │ │ ├── mass2all.go │ │ │ └── msg.go │ │ ├── mass2group │ │ │ ├── mass2group.go │ │ │ └── msg.go │ │ ├── mass2users │ │ │ ├── mass2users.go │ │ │ └── msg.go │ │ └── preview │ │ │ ├── msg.go │ │ │ └── preview.go │ └── template │ │ ├── doc.go │ │ ├── event.go │ │ ├── send.go │ │ └── template.go ├── oauth2 │ ├── README.md │ ├── api_test.go │ ├── component │ │ ├── endpoint.go │ │ └── oauth2.go │ ├── doc.go │ ├── endpoint.go │ ├── oauth2.go │ ├── session.go │ └── userinfo.go ├── poi │ ├── README.md │ ├── add.go │ ├── category.go │ ├── del.go │ ├── doc.go │ ├── event.go │ ├── get.go │ ├── list.go │ └── update.go ├── qrcode │ ├── README.md │ ├── create.go │ ├── doc.go │ ├── download.go │ └── shorturl.go ├── shakearound │ ├── account │ │ └── account.go │ ├── device │ │ ├── applyid.go │ │ ├── applystatus.go │ │ ├── bindlocation.go │ │ ├── bindpage.go │ │ ├── search.go │ │ └── update.go │ ├── doc.go │ ├── event.go │ ├── material │ │ └── add.go │ ├── page │ │ ├── add.go │ │ ├── delete.go │ │ ├── search.go │ │ └── update.go │ ├── relation │ │ └── search.go │ ├── statistics │ │ ├── device.go │ │ ├── devicelist.go │ │ ├── page.go │ │ ├── pagelist.go │ │ └── statistics.go │ └── user │ │ └── getshakeinfo.go └── user │ ├── README.md │ ├── doc.go │ ├── group.go │ ├── group │ ├── README.md │ └── group.go │ ├── list.go │ ├── tag │ └── tag.go │ └── user.go ├── oauth2 ├── README.md ├── api.go ├── client.go ├── doc.go ├── error.go └── oauth2.go ├── open ├── README.md ├── doc.go └── oauth2 │ ├── README.md │ ├── doc.go │ ├── endpoint.go │ ├── oauth2.go │ └── userinfo.go ├── util ├── doc.go ├── helper.go ├── http_client.go ├── http_response_writer.go ├── location.go ├── nonce_str.go ├── wxver.go └── wxver_test.go ├── weixin_pay.png └── weixin_qrcode.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Folder view configuration files 2 | .DS_Store 3 | Desktop.ini 4 | 5 | # Thumbnail cache files 6 | Thumbs.db 7 | 8 | # Files that might appear on external disks 9 | .Spotlight-V100 10 | .Trashes 11 | 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binary, build with `go tests -c` 19 | *.test 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | 24 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 25 | .glide/ 26 | 27 | # IntelliJ IDEA 28 | .idea/ 29 | *.iml 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 chanxuehong(chanxuehong@gmail.com) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat SDK for golang 2 | https://github.com/chanxuehong/wechat 3 | 4 | ## 简介 5 | | 模块 | 描述 | 6 | |-----:|:-------------------------| 7 | | mp | 微信公众平台 SDK | 8 | | mch | 微信商户平台(微信支付) SDK | 9 | 10 | ## 安装 11 | go get -u github.com/chanxuehong/wechat/... 12 | 13 | ## 一点简单的帮助文档, 也许对你有作用 14 | * [微信公众号 SDK 核心 package](/mp/core/README.md) 15 | * [基本的 api 调用](/mp/README.md) 16 | * [微信网页授权](/mp/oauth2/README.md) 17 | 18 | ## 联系方式 19 | QQ群: 297489459 20 | 21 | ## 文档 22 | 代码下载下来后,放入 GOPATH 路径的 src 下面,可以在shell(windows 下面是 cmd) 里运行 23 | ```sh 24 | godoc -http=:8080 25 | ``` 26 | 27 | 然后在浏览器里地址栏输入 28 | ```sh 29 | http://localhost:8080/ 30 | ``` 31 | 即可查看文档 32 | 33 | ## 授权(LICENSE) 34 | [wechat is licensed under the Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 35 | -------------------------------------------------------------------------------- /alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chanxuehong/wechat/36f0325263cdec440d6e36f93d168e4cc39b64b8/alipay.png -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // https://github.com/chanxuehong/wechat 2 | package wechat 3 | -------------------------------------------------------------------------------- /donors.md: -------------------------------------------------------------------------------- 1 | 感谢下列捐助者: 2 | 3 | 2015-01-29 深秋晚来゛Elop(539820986@qq.com) 4 | 2015-01-29 思春的野猫(329839705@qq.com) 5 | 2015-01-29 无风不飞雨(503307696@qq.com) 6 | 2015-01-29 杨海(453926638@qq.com) 7 | 2015-02-26 sole(80629340@qq.com) 8 | 2015-05-06 麦可思哲(771478394@qq.com) 9 | 2015-05-21 无风不飞雨(503307696@qq.com) 10 | 2015-06-15 Chandler(bzshow@gmail.com) 11 | 2015-07-30 Peter(不知道邮箱,麻烦看到告知) 12 | 2015-08-03 无风不飞雨(503307696@qq.com) 13 | 2015-08-03 Chandler(bzshow@gmail.com) 14 | 2015-08-03 悟空(36621534@qq.com) 15 | 2015-08-03 杨海(453926638@qq.com) 16 | 2015-08-03 如风(849613813@qq.com) 17 | 2015-08-28 Albert(不知道邮箱,麻烦看到告知) 18 | 2015-11-13 远非¢贤(364863375@qq.com) 19 | 2015-12-30 sole(80629340@qq.com) 20 | 2016-01-09 楚石(26143397@qq.com) 21 | 2016-03-03 sole(80629340@qq.com) 22 | 2016-03-11 逐梦 23 | 2016-03-21 稻草人 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chanxuehong/wechat 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99 7 | github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99 h1:K62Lb6bsgLOB++z/VAvRvtiEBdNCuMfmQGTGGWMdPpM= 2 | github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99/go.mod h1:9+sJ9zvvkXC5sPjPEZM3Jpb9n2Q2VtcrGZly0UHYF5I= 3 | github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 h1:c1vUDbnwvu5d2ucfzXvMzBWzeu5IxPvtESOFPl3CieA= 4 | github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13/go.mod h1:XEYt99iTxMqkv+gW85JX/DdUINHUe43Sbe5AtqSaDAQ= 5 | -------------------------------------------------------------------------------- /internal/debug/README.md: -------------------------------------------------------------------------------- 1 | ## 提供 debug 和 release 两个版本的实现 2 | -------------------------------------------------------------------------------- /internal/debug/api/debug.go: -------------------------------------------------------------------------------- 1 | //go:build wechat_debug 2 | // +build wechat_debug 3 | 4 | package api 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | ) 13 | 14 | func DebugPrintGetRequest(url string) { 15 | log.Println("[WECHAT_DEBUG] [API] GET", url) 16 | } 17 | 18 | func DebugPrintPostJSONRequest(url string, body []byte) { 19 | const format = "[WECHAT_DEBUG] [API] JSON POST %s\n" + 20 | "http request body:\n%s\n" 21 | 22 | buf := bytes.NewBuffer(make([]byte, 0, len(body)+1024)) 23 | if err := json.Indent(buf, body, "", " "); err == nil { 24 | body = buf.Bytes() 25 | } 26 | log.Printf(format, url, body) 27 | } 28 | 29 | func DebugPrintPostMultipartRequest(url string, body []byte) { 30 | log.Println("[WECHAT_DEBUG] [API] multipart/form-data POST", url) 31 | } 32 | 33 | func DecodeJSONHttpResponse(r io.Reader, v interface{}) error { 34 | body, err := ioutil.ReadAll(r) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | body2 := body 40 | buf := bytes.NewBuffer(make([]byte, 0, len(body2)+1024)) 41 | if err := json.Indent(buf, body2, "", " "); err == nil { 42 | body2 = buf.Bytes() 43 | } 44 | log.Printf("[WECHAT_DEBUG] [API] http response body:\n%s\n", body2) 45 | 46 | return json.Unmarshal(body, v) 47 | } 48 | -------------------------------------------------------------------------------- /internal/debug/api/release.go: -------------------------------------------------------------------------------- 1 | //go:build !wechat_debug 2 | // +build !wechat_debug 3 | 4 | package api 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | ) 10 | 11 | func DebugPrintGetRequest(url string) {} 12 | 13 | func DebugPrintPostJSONRequest(url string, body []byte) {} 14 | 15 | func DebugPrintPostMultipartRequest(url string, body []byte) {} 16 | 17 | func DecodeJSONHttpResponse(r io.Reader, v interface{}) error { 18 | return json.NewDecoder(r).Decode(v) 19 | } 20 | -------------------------------------------------------------------------------- /internal/debug/api/retry/debug.go: -------------------------------------------------------------------------------- 1 | //go:build wechat_debug 2 | // +build wechat_debug 3 | 4 | package retry 5 | 6 | import ( 7 | "log" 8 | ) 9 | 10 | func DebugPrintError(errcode int64, errmsg string, token string) { 11 | const format = "[WECHAT_DEBUG] [API] [RETRY] errcode: %d, errmsg: %s\n" + 12 | "current token: %s\n" 13 | log.Printf(format, errcode, errmsg, token) 14 | } 15 | 16 | func DebugPrintNewToken(token string) { 17 | log.Println("[WECHAT_DEBUG] [API] [RETRY] new token:", token) 18 | } 19 | 20 | func DebugPrintFallthrough(token string) { 21 | log.Println("[WECHAT_DEBUG] [API] [RETRY] fallthrough, current token:", token) 22 | } 23 | -------------------------------------------------------------------------------- /internal/debug/api/retry/release.go: -------------------------------------------------------------------------------- 1 | //go:build !wechat_debug 2 | // +build !wechat_debug 3 | 4 | package retry 5 | 6 | func DebugPrintError(errcode int64, errmsg string, token string) {} 7 | 8 | func DebugPrintNewToken(token string) {} 9 | 10 | func DebugPrintFallthrough(token string) {} 11 | -------------------------------------------------------------------------------- /internal/debug/callback/debug.go: -------------------------------------------------------------------------------- 1 | //go:build wechat_debug 2 | // +build wechat_debug 3 | 4 | package callback 5 | 6 | import ( 7 | "encoding/xml" 8 | "io" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func DebugPrintRequest(r *http.Request) { 14 | log.Println("[WECHAT_DEBUG] [CALLBACK]", r.Method, r.RequestURI) 15 | } 16 | 17 | func DebugPrintPlainRequestMessage(msg []byte) { 18 | log.Printf("[WECHAT_DEBUG] [CALLBACK] plain request message:\n%s\n", msg) 19 | } 20 | 21 | func XmlMarshalResponseMessage(msg interface{}) ([]byte, error) { 22 | bs, err := xml.MarshalIndent(msg, "", " ") 23 | if err != nil { 24 | return nil, err 25 | } 26 | log.Printf("[WECHAT_DEBUG] [CALLBACK] plain response message:\n%s\n", bs) 27 | return bs, nil 28 | } 29 | 30 | func XmlEncodeResponseMessage(w io.Writer, msg interface{}) error { 31 | bs, err := xml.MarshalIndent(msg, "", " ") 32 | if err != nil { 33 | return err 34 | } 35 | log.Printf("[WECHAT_DEBUG] [CALLBACK] plain response message:\n%s\n", bs) 36 | 37 | _, err = w.Write(bs) 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /internal/debug/callback/release.go: -------------------------------------------------------------------------------- 1 | //go:build !wechat_debug 2 | // +build !wechat_debug 3 | 4 | package callback 5 | 6 | import ( 7 | "encoding/xml" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func DebugPrintRequest(r *http.Request) {} 13 | 14 | func DebugPrintPlainRequestMessage(msg []byte) {} 15 | 16 | func XmlMarshalResponseMessage(msg interface{}) ([]byte, error) { 17 | return xml.Marshal(msg) 18 | } 19 | 20 | func XmlEncodeResponseMessage(w io.Writer, msg interface{}) error { 21 | return xml.NewEncoder(w).Encode(msg) 22 | } 23 | -------------------------------------------------------------------------------- /internal/debug/mch/api/debug.go: -------------------------------------------------------------------------------- 1 | //go:build wechat_debug 2 | // +build wechat_debug 3 | 4 | package api 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | 12 | "github.com/chanxuehong/util" 13 | ) 14 | 15 | func DebugPrintGetRequest(url string) { 16 | log.Println("[WECHAT_DEBUG] [MCH] [API] GET", url) 17 | } 18 | 19 | func DebugPrintPostXMLRequest(url string, body []byte) { 20 | const format = "[WECHAT_DEBUG] [MCH] [API] XML POST %s\n" + 21 | "http request body:\n%s\n" 22 | log.Printf(format, url, body) 23 | } 24 | 25 | func DecodeXMLHttpResponse(r io.Reader) (map[string]string, error) { 26 | body, err := ioutil.ReadAll(r) 27 | if err != nil { 28 | return nil, err 29 | } 30 | log.Printf("[WECHAT_DEBUG] [MCH] [API] http response body:\n%s\n", body) 31 | 32 | return util.DecodeXMLToMap(bytes.NewReader(body)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/debug/mch/api/release.go: -------------------------------------------------------------------------------- 1 | //go:build !wechat_debug 2 | // +build !wechat_debug 3 | 4 | package api 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/chanxuehong/util" 10 | ) 11 | 12 | func DebugPrintGetRequest(url string) {} 13 | 14 | func DebugPrintPostXMLRequest(url string, body []byte) {} 15 | 16 | func DecodeXMLHttpResponse(r io.Reader) (map[string]string, error) { 17 | return util.DecodeXMLToMap(r) 18 | } 19 | -------------------------------------------------------------------------------- /internal/debug/mch/callback/debug.go: -------------------------------------------------------------------------------- 1 | //go:build wechat_debug 2 | // +build wechat_debug 3 | 4 | package callback 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "log" 10 | "net/http" 11 | 12 | "github.com/chanxuehong/util" 13 | ) 14 | 15 | func DebugPrintRequest(r *http.Request) { 16 | log.Println("[WECHAT_DEBUG] [MCH] [CALLBACK]", r.Method, r.RequestURI) 17 | } 18 | 19 | func DebugPrintRequestMessage(msg []byte) { 20 | log.Printf("[WECHAT_DEBUG] [MCH] [CALLBACK] http request body:\n%s\n", msg) 21 | } 22 | 23 | func EncodeXMLResponseMessage(w io.Writer, msg map[string]string) (err error) { 24 | var buf bytes.Buffer 25 | if err = util.EncodeXMLFromMap(&buf, msg, "xml"); err != nil { 26 | return 27 | } 28 | log.Printf("[WECHAT_DEBUG] [MCH] [CALLBACK] http response body:\n%s\n", buf.Bytes()) 29 | 30 | _, err = buf.WriteTo(w) 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /internal/debug/mch/callback/release.go: -------------------------------------------------------------------------------- 1 | //go:build !wechat_debug 2 | // +build !wechat_debug 3 | 4 | package callback 5 | 6 | import ( 7 | "io" 8 | "net/http" 9 | 10 | "github.com/chanxuehong/util" 11 | ) 12 | 13 | func DebugPrintRequest(r *http.Request) {} 14 | 15 | func DebugPrintRequestMessage(msg []byte) {} 16 | 17 | func EncodeXMLResponseMessage(w io.Writer, msg map[string]string) (err error) { 18 | return util.EncodeXMLFromMap(w, msg, "xml") 19 | } 20 | -------------------------------------------------------------------------------- /internal/util/doc.go: -------------------------------------------------------------------------------- 1 | // 提供一些实用函数 2 | package util 3 | -------------------------------------------------------------------------------- /internal/util/helper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Bool is a helper routine that allocates a new bool value 4 | // to store v and returns a pointer to it. 5 | func Bool(v bool) *bool { 6 | return &v 7 | } 8 | 9 | // Int is a helper routine that allocates a new int value 10 | // to store v and returns a pointer to it. 11 | func Int(v int) *int { 12 | return &v 13 | } 14 | 15 | // Int32 is a helper routine that allocates a new int32 value 16 | // to store v and returns a pointer to it. 17 | func Int32(v int32) *int32 { 18 | return &v 19 | } 20 | 21 | // Int64 is a helper routine that allocates a new int64 value 22 | // to store v and returns a pointer to it. 23 | func Int64(v int64) *int64 { 24 | return &v 25 | } 26 | 27 | // Float32 is a helper routine that allocates a new float32 value 28 | // to store v and returns a pointer to it. 29 | func Float32(v float32) *float32 { 30 | return &v 31 | } 32 | 33 | // Float64 is a helper routine that allocates a new float64 value 34 | // to store v and returns a pointer to it. 35 | func Float64(v float64) *float64 { 36 | return &v 37 | } 38 | 39 | // Uint32 is a helper routine that allocates a new uint32 value 40 | // to store v and returns a pointer to it. 41 | func Uint32(v uint32) *uint32 { 42 | return &v 43 | } 44 | 45 | // Uint64 is a helper routine that allocates a new uint64 value 46 | // to store v and returns a pointer to it. 47 | func Uint64(v uint64) *uint64 { 48 | return &v 49 | } 50 | 51 | // String is a helper routine that allocates a new string value 52 | // to store v and returns a pointer to it. 53 | func String(v string) *string { 54 | return &v 55 | } 56 | -------------------------------------------------------------------------------- /internal/util/sign.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "sort" 8 | ) 9 | 10 | // Sign 微信公众号 url 签名. 11 | func Sign(token, timestamp, nonce string) (signature string) { 12 | strs := sort.StringSlice{token, timestamp, nonce} 13 | strs.Sort() 14 | 15 | buf := make([]byte, 0, len(token)+len(timestamp)+len(nonce)) 16 | buf = append(buf, strs[0]...) 17 | buf = append(buf, strs[1]...) 18 | buf = append(buf, strs[2]...) 19 | 20 | hashsum := sha1.Sum(buf) 21 | return hex.EncodeToString(hashsum[:]) 22 | } 23 | 24 | // MsgSign 微信公众号/企业号 消息体签名. 25 | func MsgSign(token, timestamp, nonce, encryptedMsg string) (signature string) { 26 | strs := sort.StringSlice{token, timestamp, nonce, encryptedMsg} 27 | strs.Sort() 28 | 29 | h := sha1.New() 30 | 31 | bufw := bufio.NewWriterSize(h, 128) // sha1.BlockSize 的整数倍 32 | bufw.WriteString(strs[0]) 33 | bufw.WriteString(strs[1]) 34 | bufw.WriteString(strs[2]) 35 | bufw.WriteString(strs[3]) 36 | bufw.Flush() 37 | 38 | hashsum := h.Sum(nil) 39 | return hex.EncodeToString(hashsum) 40 | } 41 | -------------------------------------------------------------------------------- /internal/util/to_lower.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // ToLower 返回字符串对应的小写版本. 4 | // 5 | // 如果确定 s 是[a-zA-Z0-9_-] 的组合, 那么可以用这个函数, 否则请用 strings.ToLower! 6 | func ToLower(s string) string { 7 | var b []byte 8 | for i := 0; i < len(s); i++ { 9 | c := s[i] 10 | if c > 'Z' || c < 'A' { 11 | if b != nil { 12 | b[i] = c 13 | } 14 | } else { 15 | c += 'a' - 'A' 16 | if b == nil { 17 | b = make([]byte, len(s)) 18 | copy(b, s[:i]) 19 | } 20 | b[i] = c 21 | } 22 | } 23 | if b != nil { 24 | return string(b) 25 | } 26 | return s 27 | } 28 | -------------------------------------------------------------------------------- /internal/util/to_lower_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestToLower(t *testing.T) { 9 | strs := []string{ 10 | "aaaa_bbbb-cccc", 11 | "aaaA_Bbbb-CCCC", 12 | "AAAA_BBBB-cccc", 13 | } 14 | for _, str := range strs { 15 | dst1 := ToLower(str) 16 | dst2 := strings.ToLower(str) 17 | if dst1 != dst2 { 18 | t.Errorf("TestToLower failed, have: %s, want %s\n", dst1, dst2) 19 | } 20 | } 21 | } 22 | 23 | func BenchmarkToLower(b *testing.B) { 24 | s := "scancode_waitmsg" 25 | b.ReportAllocs() 26 | b.ResetTimer() 27 | for i := 0; i < b.N; i++ { 28 | ToLower(s) 29 | } 30 | } 31 | 32 | func BenchmarkStringsToLower(b *testing.B) { 33 | s := "scancode_waitmsg" 34 | b.ReportAllocs() 35 | b.ResetTimer() 36 | for i := 0; i < b.N; i++ { 37 | strings.ToLower(s) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mch/core/api_base_url.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | func APIBaseURL() string { 4 | // TODO(chanxuehong): 后期做容灾功能 5 | return "https://api.mch.weixin.qq.com" 6 | } 7 | -------------------------------------------------------------------------------- /mch/core/buffer_pool.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var textBufferPool = sync.Pool{ 9 | New: func() interface{} { 10 | return bytes.NewBuffer(make([]byte, 0, 4<<10)) // 4KB 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /mch/core/const.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | ReturnCodeSuccess = "SUCCESS" 5 | ReturnCodeFail = "FAIL" 6 | ) 7 | 8 | const ( 9 | ResultCodeSuccess = "SUCCESS" 10 | ResultCodeFail = "FAIL" 11 | ) 12 | 13 | const ( 14 | SignType_MD5 = "MD5" 15 | SignType_SHA1 = "SHA1" 16 | SignType_HMAC_SHA256 = "HMAC-SHA256" 17 | ) 18 | -------------------------------------------------------------------------------- /mch/core/context.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/chanxuehong/util" 7 | ) 8 | 9 | const ( 10 | initHandlerIndex = -1 11 | abortHandlerIndex = maxHandlerChainSize 12 | ) 13 | 14 | // Context 是 Handler 处理消息(事件)的上下文环境. 非并发安全! 15 | type Context struct { 16 | Server *Server 17 | 18 | ResponseWriter http.ResponseWriter 19 | Request *http.Request 20 | 21 | RequestBody []byte // 回调请求的 http-body, 就是消息体的原始内容, 记录log可能需要这个信息 22 | Msg map[string]string // 请求消息, return_code == "SUCCESS" && result_code == "SUCCESS" 23 | 24 | handlers HandlerChain 25 | handlerIndex int 26 | 27 | kvs map[string]interface{} 28 | } 29 | 30 | // IsAborted 返回 true 如果 Context.Abort() 被调用了, 否则返回 false. 31 | func (ctx *Context) IsAborted() bool { 32 | return ctx.handlerIndex >= abortHandlerIndex 33 | } 34 | 35 | // Abort 阻止系统调用当前 handler 后续的 handlers, 即当前的 handler 处理完毕就返回, 一般在 middleware 中调用. 36 | func (ctx *Context) Abort() { 37 | ctx.handlerIndex = abortHandlerIndex 38 | } 39 | 40 | // Next 中断当前 handler 程序逻辑执行其后续的 handlers, 一般在 middleware 中调用. 41 | func (ctx *Context) Next() { 42 | for { 43 | ctx.handlerIndex++ 44 | if ctx.handlerIndex >= len(ctx.handlers) { 45 | ctx.handlerIndex-- 46 | break 47 | } 48 | handler := ctx.handlers[ctx.handlerIndex] 49 | if handler != nil { 50 | handler.ServeMsg(ctx) 51 | } 52 | } 53 | } 54 | 55 | // SetHandlers 设置 handlers 给 Context.Next() 调用, 务必在 Context.Next() 调用之前设置, 否则会 panic. 56 | // 57 | // NOTE: 此方法一般用不到, 除非你自己实现一个 Handler 给 Server 使用, 参考 HandlerChain. 58 | func (ctx *Context) SetHandlers(handlers HandlerChain) { 59 | if len(handlers) > maxHandlerChainSize { 60 | panic("too many handlers") 61 | } 62 | for _, h := range handlers { 63 | if h == nil { 64 | panic("handler can not be nil") 65 | } 66 | } 67 | if ctx.handlerIndex != initHandlerIndex { 68 | panic("can't set handlers after Context.Next() called") 69 | } 70 | ctx.handlers = handlers 71 | } 72 | 73 | // Response 回复消息给微信服务器 74 | func (ctx *Context) Response(msg map[string]string) (err error) { 75 | return util.EncodeXMLFromMap(ctx.ResponseWriter, msg, "xml") 76 | } 77 | 78 | // Set 存储 key-value pair 到 Context 中. 79 | func (ctx *Context) Set(key string, value interface{}) { 80 | if ctx.kvs == nil { 81 | ctx.kvs = make(map[string]interface{}) 82 | } 83 | ctx.kvs[key] = value 84 | } 85 | 86 | // Get 返回 Context 中 key 对应的 value, 如果 key 存在的返回 (value, true), 否则返回 (nil, false). 87 | func (ctx *Context) Get(key string) (value interface{}, exists bool) { 88 | value, exists = ctx.kvs[key] 89 | return 90 | } 91 | 92 | // MustGet 返回 Context 中 key 对应的 value, 如果 key 不存在则会 panic. 93 | func (ctx *Context) MustGet(key string) interface{} { 94 | if value, exists := ctx.Get(key); exists { 95 | return value 96 | } 97 | panic(`[kvs] key "` + key + `" does not exist`) 98 | } 99 | -------------------------------------------------------------------------------- /mch/core/doc.go: -------------------------------------------------------------------------------- 1 | // 微信商家平台api 2 | package core 3 | -------------------------------------------------------------------------------- /mch/core/error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | var ( 10 | ErrNotFoundReturnCode = errors.New("not found return_code parameter") 11 | ErrNotFoundResultCode = errors.New("not found result_code parameter") 12 | ErrNotFoundSign = errors.New("not found sign parameter") 13 | ) 14 | 15 | var _ error = (*Error)(nil) 16 | 17 | // 协议错误, return_code 不为 SUCCESS 时有返回. 18 | type Error struct { 19 | XMLName struct{} `xml:"xml" json:"-"` 20 | ReturnCode string `xml:"return_code" json:"return_code"` 21 | ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg,omitempty"` 22 | } 23 | 24 | func (e *Error) Error() string { 25 | bs, err := xml.Marshal(e) 26 | if err != nil { 27 | return fmt.Sprintf("return_code: %q, return_msg: %q", e.ReturnCode, e.ReturnMsg) 28 | } 29 | return string(bs) 30 | } 31 | 32 | var _ error = (*BizError)(nil) 33 | 34 | // 业务错误, result_code 不为 SUCCESS 时有返回. 35 | type BizError struct { 36 | XMLName struct{} `xml:"xml" json:"-"` 37 | ResultCode string `xml:"result_code" json:"result_code"` 38 | ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` 39 | ErrCodeDesc string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` 40 | } 41 | 42 | func (e *BizError) Error() string { 43 | bs, err := xml.Marshal(e) 44 | if err != nil { 45 | return fmt.Sprintf("result_code: %q, err_code: %q, err_code_des: %q", e.ResultCode, e.ErrCode, e.ErrCodeDesc) 46 | } 47 | return string(bs) 48 | } 49 | -------------------------------------------------------------------------------- /mch/core/error_handler.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | type ErrorHandler interface { 10 | // ServeError 处理回调的错误, 比如 xml 解码出错, return_code != "SUCCESS", result_code != "SUCCESS", ... 11 | ServeError(http.ResponseWriter, *http.Request, error) 12 | } 13 | 14 | var DefaultErrorHandler ErrorHandler = ErrorHandlerFunc(defaultErrorHandlerFunc) 15 | 16 | type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, error) 17 | 18 | func (fn ErrorHandlerFunc) ServeError(w http.ResponseWriter, r *http.Request, err error) { 19 | fn(w, r, err) 20 | } 21 | 22 | var errorLogger = log.New(os.Stderr, "[WECHAT_ERROR] ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile) 23 | 24 | func defaultErrorHandlerFunc(w http.ResponseWriter, r *http.Request, err error) { 25 | errorLogger.Output(3, err.Error()) 26 | } 27 | -------------------------------------------------------------------------------- /mch/core/handler.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Handler interface { 4 | ServeMsg(*Context) 5 | } 6 | 7 | // HandlerChain -------------------------------------------------------------------------------------------------------- 8 | 9 | const maxHandlerChainSize = 64 10 | 11 | var _ Handler = (HandlerChain)(nil) 12 | 13 | type HandlerChain []Handler 14 | 15 | // ServeMsg 实现 Handler 接口 16 | func (chain HandlerChain) ServeMsg(ctx *Context) { 17 | ctx.handlers = chain 18 | ctx.Next() 19 | } 20 | 21 | func (chain *HandlerChain) AppendHandlerFunc(handlers ...func(*Context)) { 22 | if len(handlers) == 0 { 23 | return 24 | } 25 | for _, h := range handlers { 26 | if h == nil { 27 | panic("handler can not be nil") 28 | } 29 | } 30 | handlers2 := make(HandlerChain, len(handlers)) 31 | for i := 0; i < len(handlers); i++ { 32 | handlers2[i] = HandlerFunc(handlers[i]) 33 | } 34 | chain.AppendHandler(handlers2) 35 | } 36 | 37 | func (chain *HandlerChain) AppendHandler(handlers ...Handler) { 38 | if len(handlers) == 0 { 39 | return 40 | } 41 | for _, h := range handlers { 42 | if h == nil { 43 | panic("handler can not be nil") 44 | } 45 | } 46 | *chain = combineHandlerChain(*chain, handlers) 47 | } 48 | 49 | func combineHandlerChain(middlewares, handlers HandlerChain) HandlerChain { 50 | if len(middlewares)+len(handlers) > maxHandlerChainSize { 51 | panic("too many handlers") 52 | } 53 | return append(middlewares, handlers...) 54 | } 55 | 56 | // HandlerFunc --------------------------------------------------------------------------------------------------------- 57 | 58 | var _ Handler = HandlerFunc(nil) 59 | 60 | type HandlerFunc func(*Context) 61 | 62 | // ServeMsg 实现 Handler 接口 63 | func (fn HandlerFunc) ServeMsg(ctx *Context) { fn(ctx) } 64 | -------------------------------------------------------------------------------- /mch/core/native_url.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // 扫码原生支付模式1的地址 8 | func NativeURL1(appId, mchId, productId, timestamp, nonceStr, apiKey string) string { 9 | m := make(map[string]string, 5) 10 | m["appid"] = appId 11 | m["mch_id"] = mchId 12 | m["product_id"] = productId 13 | m["time_stamp"] = timestamp 14 | m["nonce_str"] = nonceStr 15 | 16 | return "weixin://wxpay/bizpayurl?sign=" + Sign(m, apiKey, nil) + 17 | "&appid=" + url.QueryEscape(appId) + 18 | "&mch_id=" + url.QueryEscape(mchId) + 19 | "&product_id=" + url.QueryEscape(productId) + 20 | "&time_stamp=" + url.QueryEscape(timestamp) + 21 | "&nonce_str=" + url.QueryEscape(nonceStr) 22 | } 23 | -------------------------------------------------------------------------------- /mch/core/sign.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/md5" 7 | "crypto/sha1" 8 | "encoding/hex" 9 | "hash" 10 | "sort" 11 | ) 12 | 13 | // Sign 微信支付签名. 14 | // 15 | // params: 待签名的参数集合 16 | // apiKey: api密钥 17 | // fn: func() hash.Hash, 如果为 nil 则默认用 md5.New 18 | func Sign(params map[string]string, apiKey string, fn func() hash.Hash) string { 19 | if fn == nil { 20 | fn = md5.New 21 | } 22 | return Sign2(params, apiKey, fn()) 23 | } 24 | 25 | // Sign2 微信支付签名. 26 | // 27 | // params: 待签名的参数集合 28 | // apiKey: api密钥 29 | // h: hash.Hash, 如果为 nil 则默认用 md5.New(), 特别注意 h 必须是 initial state. 30 | func Sign2(params map[string]string, apiKey string, h hash.Hash) string { 31 | if h == nil { 32 | h = md5.New() 33 | } 34 | 35 | keys := make([]string, 0, len(params)) 36 | for k := range params { 37 | if k == "sign" { 38 | continue 39 | } 40 | keys = append(keys, k) 41 | } 42 | sort.Strings(keys) 43 | 44 | bufw := bufio.NewWriterSize(h, 128) 45 | for _, k := range keys { 46 | v := params[k] 47 | if v == "" { 48 | continue 49 | } 50 | bufw.WriteString(k) 51 | bufw.WriteByte('=') 52 | bufw.WriteString(v) 53 | bufw.WriteByte('&') 54 | } 55 | bufw.WriteString("key=") 56 | bufw.WriteString(apiKey) 57 | bufw.Flush() 58 | 59 | signature := make([]byte, hex.EncodedLen(h.Size())) 60 | hex.Encode(signature, h.Sum(nil)) 61 | return string(bytes.ToUpper(signature)) 62 | } 63 | 64 | // jssdk 支付签名, signType 只支持 "MD5", "SHA1", 传入其他的值会 panic. 65 | func JsapiSign(appId, timeStamp, nonceStr, packageStr, signType string, apiKey string) string { 66 | var h hash.Hash 67 | switch signType { 68 | case SignType_MD5: 69 | h = md5.New() 70 | case SignType_SHA1: 71 | h = sha1.New() 72 | default: 73 | panic("unsupported signType") 74 | } 75 | bufw := bufio.NewWriterSize(h, 128) 76 | 77 | // appId 78 | // nonceStr 79 | // package 80 | // signType 81 | // timeStamp 82 | bufw.WriteString("appId=") 83 | bufw.WriteString(appId) 84 | bufw.WriteString("&nonceStr=") 85 | bufw.WriteString(nonceStr) 86 | bufw.WriteString("&package=") 87 | bufw.WriteString(packageStr) 88 | bufw.WriteString("&signType=") 89 | bufw.WriteString(signType) 90 | bufw.WriteString("&timeStamp=") 91 | bufw.WriteString(timeStamp) 92 | bufw.WriteString("&key=") 93 | bufw.WriteString(apiKey) 94 | 95 | bufw.Flush() 96 | signature := make([]byte, hex.EncodedLen(h.Size())) 97 | hex.Encode(signature, h.Sum(nil)) 98 | return string(bytes.ToUpper(signature)) 99 | } 100 | 101 | // EditAddressSign 收货地址共享接口签名 102 | func EditAddressSign(appId, url, timestamp, nonceStr, accessToken string) string { 103 | h := sha1.New() 104 | bufw := bufio.NewWriterSize(h, 128) 105 | 106 | // accesstoken 107 | // appid 108 | // noncestr 109 | // timestamp 110 | // url 111 | bufw.WriteString("accesstoken=") 112 | bufw.WriteString(accessToken) 113 | bufw.WriteString("&appid=") 114 | bufw.WriteString(appId) 115 | bufw.WriteString("&noncestr=") 116 | bufw.WriteString(nonceStr) 117 | bufw.WriteString("×tamp=") 118 | bufw.WriteString(timestamp) 119 | bufw.WriteString("&url=") 120 | bufw.WriteString(url) 121 | 122 | bufw.Flush() 123 | return hex.EncodeToString(h.Sum(nil)) 124 | } 125 | -------------------------------------------------------------------------------- /mch/core/ssl_http_client_go1.6.go: -------------------------------------------------------------------------------- 1 | //go:build go1.6 && !go1.7 2 | // +build go1.6,!go1.7 3 | 4 | package core 5 | 6 | import ( 7 | "crypto/tls" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // NewTLSHttpClient 创建支持双向证书认证的 http.Client. 14 | func NewTLSHttpClient(certFile, keyFile string) (httpClient *http.Client, err error) { 15 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 16 | if err != nil { 17 | return nil, err 18 | } 19 | tlsConfig := &tls.Config{ 20 | Certificates: []tls.Certificate{cert}, 21 | } 22 | return newTLSHttpClient(tlsConfig) 23 | } 24 | 25 | // NewTLSHttpClient2 创建支持双向证书认证的 http.Client. 26 | func NewTLSHttpClient2(certPEMBlock, keyPEMBlock []byte) (httpClient *http.Client, err error) { 27 | cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) 28 | if err != nil { 29 | return nil, err 30 | } 31 | tlsConfig := &tls.Config{ 32 | Certificates: []tls.Certificate{cert}, 33 | } 34 | return newTLSHttpClient(tlsConfig) 35 | } 36 | 37 | func newTLSHttpClient(tlsConfig *tls.Config) (*http.Client, error) { 38 | dialTLS := func(network, addr string) (net.Conn, error) { 39 | return tls.DialWithDialer(&net.Dialer{ 40 | Timeout: 5 * time.Second, 41 | KeepAlive: 30 * time.Second, 42 | }, network, addr, tlsConfig) 43 | } 44 | return &http.Client{ 45 | Transport: &http.Transport{ 46 | Proxy: http.ProxyFromEnvironment, 47 | Dial: (&net.Dialer{ 48 | Timeout: 5 * time.Second, 49 | KeepAlive: 30 * time.Second, 50 | }).Dial, 51 | DialTLS: dialTLS, 52 | ExpectContinueTimeout: 1 * time.Second, 53 | }, 54 | }, nil 55 | } 56 | -------------------------------------------------------------------------------- /mch/core/ssl_http_client_go1.7.go: -------------------------------------------------------------------------------- 1 | //go:build go1.7 && !go1.8 2 | // +build go1.7,!go1.8 3 | 4 | package core 5 | 6 | import ( 7 | "crypto/tls" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // NewTLSHttpClient 创建支持双向证书认证的 http.Client. 14 | func NewTLSHttpClient(certFile, keyFile string) (httpClient *http.Client, err error) { 15 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 16 | if err != nil { 17 | return nil, err 18 | } 19 | tlsConfig := &tls.Config{ 20 | Certificates: []tls.Certificate{cert}, 21 | } 22 | return newTLSHttpClient(tlsConfig) 23 | } 24 | 25 | // NewTLSHttpClient2 创建支持双向证书认证的 http.Client. 26 | func NewTLSHttpClient2(certPEMBlock, keyPEMBlock []byte) (httpClient *http.Client, err error) { 27 | cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) 28 | if err != nil { 29 | return nil, err 30 | } 31 | tlsConfig := &tls.Config{ 32 | Certificates: []tls.Certificate{cert}, 33 | } 34 | return newTLSHttpClient(tlsConfig) 35 | } 36 | 37 | func newTLSHttpClient(tlsConfig *tls.Config) (*http.Client, error) { 38 | dialTLS := func(network, addr string) (net.Conn, error) { 39 | return tls.DialWithDialer(&net.Dialer{ 40 | Timeout: 5 * time.Second, 41 | KeepAlive: 30 * time.Second, 42 | }, network, addr, tlsConfig) 43 | } 44 | return &http.Client{ 45 | Transport: &http.Transport{ 46 | Proxy: http.ProxyFromEnvironment, 47 | DialContext: (&net.Dialer{ 48 | Timeout: 5 * time.Second, 49 | KeepAlive: 30 * time.Second, 50 | }).DialContext, 51 | DialTLS: dialTLS, 52 | MaxIdleConns: 100, 53 | IdleConnTimeout: 90 * time.Second, 54 | ExpectContinueTimeout: 1 * time.Second, 55 | }, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /mch/core/ssl_http_client_go1.8.go: -------------------------------------------------------------------------------- 1 | //go:build go1.8 2 | // +build go1.8 3 | 4 | package core 5 | 6 | import ( 7 | "crypto/tls" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // NewTLSHttpClient 创建支持双向证书认证的 http.Client. 14 | func NewTLSHttpClient(certFile, keyFile string) (httpClient *http.Client, err error) { 15 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 16 | if err != nil { 17 | return nil, err 18 | } 19 | tlsConfig := &tls.Config{ 20 | Certificates: []tls.Certificate{cert}, 21 | } 22 | return newTLSHttpClient(tlsConfig) 23 | } 24 | 25 | // NewTLSHttpClient2 创建支持双向证书认证的 http.Client. 26 | func NewTLSHttpClient2(certPEMBlock, keyPEMBlock []byte) (httpClient *http.Client, err error) { 27 | cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) 28 | if err != nil { 29 | return nil, err 30 | } 31 | tlsConfig := &tls.Config{ 32 | Certificates: []tls.Certificate{cert}, 33 | } 34 | return newTLSHttpClient(tlsConfig) 35 | } 36 | 37 | func newTLSHttpClient(tlsConfig *tls.Config) (*http.Client, error) { 38 | dialTLS := func(network, addr string) (net.Conn, error) { 39 | return tls.DialWithDialer(&net.Dialer{ 40 | Timeout: 5 * time.Second, 41 | KeepAlive: 30 * time.Second, 42 | }, network, addr, tlsConfig) 43 | } 44 | return &http.Client{ 45 | Transport: &http.Transport{ 46 | Proxy: http.ProxyFromEnvironment, 47 | DialContext: (&net.Dialer{ 48 | Timeout: 5 * time.Second, 49 | KeepAlive: 30 * time.Second, 50 | DualStack: true, 51 | }).DialContext, 52 | DialTLS: dialTLS, 53 | MaxIdleConns: 100, 54 | IdleConnTimeout: 90 * time.Second, 55 | ExpectContinueTimeout: 1 * time.Second, 56 | }, 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /mch/core/time.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/chanxuehong/wechat/util" 7 | ) 8 | 9 | // FormatTime 将参数 t 格式化成北京时间 yyyyMMddHHmmss. 10 | func FormatTime(t time.Time) string { 11 | return t.In(util.BeijingLocation).Format("20060102150405") 12 | } 13 | 14 | // ParseTime 将北京时间 yyyyMMddHHmmss 字符串解析到 time.Time. 15 | func ParseTime(value string) (time.Time, error) { 16 | return time.ParseInLocation("20060102150405", value, util.BeijingLocation) 17 | } 18 | -------------------------------------------------------------------------------- /mch/doc.go: -------------------------------------------------------------------------------- 1 | // mch 微信支付sdk 2 | package mch 3 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/gethbinfo.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 红包查询接口. 8 | // 9 | // NOTE: 请求需要双向证书 10 | func GetRedPackInfo(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/gethbinfo", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/gettransferinfo.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 查询企业付款. 8 | // 9 | // NOTE: 请求需要双向证书 10 | func GetTransferInfo(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/gettransferinfo", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/promotion/transfers.go: -------------------------------------------------------------------------------- 1 | package promotion 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 企业付款. 8 | // 9 | // NOTE: 请求需要双向证书 10 | func Transfers(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/promotion/transfers", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/query_coupon_stock.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 查询代金券批次信息. 8 | func QueryCouponStock(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 9 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/query_coupon_stock", req) 10 | } 11 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/send_coupon.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 发放代金券. 8 | // 9 | // 请求需要双向证书 10 | func SendCoupon(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/send_coupon", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/sendgroupredpack.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 发放裂变红包. 8 | // 9 | // NOTE: 请求需要双向证书 10 | func SendGroupRedPack(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/sendgroupredpack", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/mmpaymkttransfers/sendredpack.go: -------------------------------------------------------------------------------- 1 | package mmpaymkttransfers 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 红包发放. 8 | // 9 | // NOTE: 请求需要双向证书 10 | func SendRedPack(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 11 | return clt.PostXML(core.APIBaseURL()+"/mmpaymkttransfers/sendredpack", req) 12 | } 13 | -------------------------------------------------------------------------------- /mch/pay/closeorder.go: -------------------------------------------------------------------------------- 1 | package pay 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | "github.com/chanxuehong/wechat/util" 6 | ) 7 | 8 | // CloseOrder 关闭订单. 9 | func CloseOrder(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 10 | return clt.PostXML(core.APIBaseURL()+"/pay/closeorder", req) 11 | } 12 | 13 | type CloseOrderRequest struct { 14 | XMLName struct{} `xml:"xml" json:"-"` 15 | 16 | // 必选参数 17 | OutTradeNo string `xml:"out_trade_no"` // 商户系统内部订单号 18 | 19 | // 可选参数 20 | NonceStr string `xml:"nonce_str"` // 随机字符串,不长于32位。NOTE: 如果为空则系统会自动生成一个随机字符串。 21 | SignType string `xml:"sign_type"` // 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 22 | } 23 | 24 | // CloseOrder2 关闭订单. 25 | func CloseOrder2(clt *core.Client, req *CloseOrderRequest) (err error) { 26 | m1 := make(map[string]string, 8) 27 | m1["out_trade_no"] = req.OutTradeNo 28 | if req.NonceStr != "" { 29 | m1["nonce_str"] = req.NonceStr 30 | } else { 31 | m1["nonce_str"] = util.NonceStr() 32 | } 33 | if req.SignType != "" { 34 | m1["sign_type"] = req.SignType 35 | } 36 | 37 | _, err = CloseOrder(clt, m1) 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /mch/pay/reverse.go: -------------------------------------------------------------------------------- 1 | package pay 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | "github.com/chanxuehong/wechat/util" 6 | ) 7 | 8 | // Reverse 撤销订单. 9 | // 10 | // NOTE: 请求需要双向证书. 11 | func Reverse(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 12 | return clt.PostXML(core.APIBaseURL()+"/secapi/pay/reverse", req) 13 | } 14 | 15 | type ReverseRequest struct { 16 | XMLName struct{} `xml:"xml" json:"-"` 17 | 18 | // 必选参数,二选一 19 | TransactionId string `xml:"transaction_id"` // 微信的订单号,优先使用 20 | OutTradeNo string `xml:"out_trade_no"` // 商户系统内部订单号 21 | 22 | // 可选参数 23 | NonceStr string `xml:"nonce_str"` // 随机字符串,不长于32位。NOTE: 如果为空则系统会自动生成一个随机字符串。 24 | SignType string `xml:"sign_type"` // 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 25 | } 26 | 27 | type ReverseResponse struct { 28 | XMLName struct{} `xml:"xml" json:"-"` 29 | 30 | // 必选返回 31 | Recall bool `xml:"recall"` // 是否需要继续调用撤销 32 | } 33 | 34 | // Reverse2 撤销订单. 35 | // 36 | // NOTE: 请求需要双向证书. 37 | func Reverse2(clt *core.Client, req *ReverseRequest) (resp *ReverseResponse, err error) { 38 | m1 := make(map[string]string, 8) 39 | if req.TransactionId != "" { 40 | m1["transaction_id"] = req.TransactionId 41 | } 42 | if req.OutTradeNo != "" { 43 | m1["out_trade_no"] = req.OutTradeNo 44 | } 45 | if req.NonceStr != "" { 46 | m1["nonce_str"] = req.NonceStr 47 | } else { 48 | m1["nonce_str"] = util.NonceStr() 49 | } 50 | if req.SignType != "" { 51 | m1["sign_type"] = req.SignType 52 | } 53 | 54 | m2, err := Reverse(clt, m1) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | resp = &ReverseResponse{} 60 | if recall := m2["recall"]; recall == "Y" || recall == "y" { 61 | resp.Recall = true 62 | } 63 | return resp, nil 64 | } 65 | -------------------------------------------------------------------------------- /mch/payutil/report.go: -------------------------------------------------------------------------------- 1 | package payutil 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/chanxuehong/wechat/mch/core" 8 | "github.com/chanxuehong/wechat/util" 9 | ) 10 | 11 | // Report 交易保障. 12 | func Report(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 13 | return clt.PostXML(core.APIBaseURL()+"/payitil/report", req) 14 | } 15 | 16 | type ReportRequest struct { 17 | XMLName struct{} `xml:"xml" json:"-"` 18 | DeviceInfo string `xml:"device_info"` // 微信支付分配的终端设备号,商户自定义 19 | NonceStr string `xml:"nonce_str"` // 随机字符串,不长于32位。NOTE: 如果为空则系统会自动生成一个随机字符串。 20 | SignType string `xml:"sign_type"` // 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 21 | InterfaceURL string `xml:"interface_url"` // 刷卡支付终端上报统一填:https://api.mch.weixin.qq.com/pay/batchreport/micropay/total 22 | UserIP string `xml:"user_ip"` // 发起接口调用时的机器IP 23 | Trades string `xml:"trades"` // 上报数据包 24 | ExecuteTime int `xml:"execute_time"` // 接口耗时情况,单位为毫秒 25 | ReturnCode string `xml:"return_code"` // 返回状态码 26 | ReturnMsg string `xml:"return_msg"` // 返回信息 27 | ResultCode string `xml:"result_code"` // 业务结果 28 | ErrCode string `xml:"err_code"` // 错误代码 29 | ErrCodeDesc string `xml:"err_code_des"` // 错误代码描述 30 | OutTradeNo string `xml:"out_trade_no"` // 商户订单号 31 | Time time.Time `xml:"time"` // 商户上报时间 32 | } 33 | 34 | // Report2 交易保障. 35 | func Report2(clt *core.Client, req *ReportRequest) (err error) { 36 | m1 := make(map[string]string, 24) 37 | if req.DeviceInfo != "" { 38 | m1["device_info"] = req.DeviceInfo 39 | } 40 | if req.NonceStr != "" { 41 | m1["nonce_str"] = req.NonceStr 42 | } else { 43 | m1["nonce_str"] = util.NonceStr() 44 | } 45 | if req.SignType != "" { 46 | m1["sign_type"] = req.SignType 47 | } 48 | if req.InterfaceURL != "" { 49 | m1["interface_url"] = req.InterfaceURL 50 | } 51 | if req.UserIP != "" { 52 | m1["user_ip"] = req.UserIP 53 | } 54 | if req.Trades != "" { 55 | m1["trades"] = req.Trades 56 | } 57 | if req.ExecuteTime > 0 { 58 | m1["execute_time"] = strconv.Itoa(req.ExecuteTime) 59 | } 60 | if req.ReturnCode != "" { 61 | m1["return_code"] = req.ReturnCode 62 | } 63 | if req.ReturnMsg != "" { 64 | m1["return_msg"] = req.ReturnMsg 65 | } 66 | if req.ResultCode != "" { 67 | m1["result_code"] = req.ResultCode 68 | } 69 | if req.ErrCode != "" { 70 | m1["err_code"] = req.ErrCode 71 | } 72 | if req.ErrCodeDesc != "" { 73 | m1["err_code_des"] = req.ErrCodeDesc 74 | } 75 | if req.OutTradeNo != "" { 76 | m1["out_trade_no"] = req.OutTradeNo 77 | } 78 | if !req.Time.IsZero() { 79 | m1["time"] = core.FormatTime(req.Time) 80 | } 81 | 82 | _, err = Report(clt, m1) 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /mch/promotion/query_coupon.go: -------------------------------------------------------------------------------- 1 | package promotion 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | ) 6 | 7 | // 查询代金券信息. 8 | func QueryCoupon(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 9 | return clt.PostXML(core.APIBaseURL()+"/promotion/query_coupon", req) 10 | } 11 | -------------------------------------------------------------------------------- /mch/tools/authcodetoopenid.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | "github.com/chanxuehong/wechat/util" 6 | ) 7 | 8 | // AuthCodeToOpenId 授权码查询openid. 9 | func AuthCodeToOpenId(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 10 | return clt.PostXML(core.APIBaseURL()+"/tools/authcodetoopenid", req) 11 | } 12 | 13 | type AuthCodeToOpenIdRequest struct { 14 | XMLName struct{} `xml:"xml" json:"-"` 15 | 16 | // 必选参数 17 | AuthCode string `xml:"auth_code"` // 扫码支付授权码,设备读取用户微信中的条码或者二维码信息 18 | 19 | // 可选参数 20 | NonceStr string `xml:"nonce_str"` // 随机字符串,不长于32位。NOTE: 如果为空则系统会自动生成一个随机字符串。 21 | SignType string `xml:"sign_type"` // 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 22 | } 23 | 24 | type AuthCodeToOpenIdResponse struct { 25 | XMLName struct{} `xml:"xml" json:"-"` 26 | 27 | // 必选返回 28 | OpenId string `xml:"openid"` // 用户在商户appid下的唯一标识 29 | 30 | // 下面字段都是可选返回的(详细见微信支付文档), 为空值表示没有返回, 程序逻辑里需要判断 31 | SubOpenId string `xml:"sub_openid"` // 用户在子商户appid下的唯一标识 32 | } 33 | 34 | // AuthCodeToOpenId2 授权码查询openid. 35 | func AuthCodeToOpenId2(clt *core.Client, req *AuthCodeToOpenIdRequest) (resp *AuthCodeToOpenIdResponse, err error) { 36 | m1 := make(map[string]string, 8) 37 | m1["auth_code"] = req.AuthCode 38 | if req.NonceStr != "" { 39 | m1["nonce_str"] = req.NonceStr 40 | } else { 41 | m1["nonce_str"] = util.NonceStr() 42 | } 43 | if req.SignType != "" { 44 | m1["sign_type"] = req.SignType 45 | } 46 | 47 | m2, err := AuthCodeToOpenId(clt, m1) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | resp = &AuthCodeToOpenIdResponse{ 53 | OpenId: m2["openid"], 54 | SubOpenId: m2["sub_openid"], 55 | } 56 | return resp, nil 57 | } 58 | -------------------------------------------------------------------------------- /mch/tools/shorturl.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mch/core" 5 | "github.com/chanxuehong/wechat/util" 6 | ) 7 | 8 | // ShortURL 转换短链接. 9 | func ShortURL(clt *core.Client, req map[string]string) (resp map[string]string, err error) { 10 | return clt.PostXML(core.APIBaseURL()+"/tools/shorturl", req) 11 | } 12 | 13 | type ShortURLRequest struct { 14 | XMLName struct{} `xml:"xml" json:"-"` 15 | 16 | // 必选参数 17 | LongURL string `xml:"long_url"` // URL链接 18 | 19 | // 可选参数 20 | NonceStr string `xml:"nonce_str"` // 随机字符串,不长于32位。NOTE: 如果为空则系统会自动生成一个随机字符串。 21 | SignType string `xml:"sign_type"` // 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 22 | } 23 | 24 | type ShortURLResponse struct { 25 | XMLName struct{} `xml:"xml" json:"-"` 26 | 27 | // 必选返回 28 | ShortURL string `xml:"short_url"` // 转换后的URL 29 | } 30 | 31 | // ShortURL2 转换短链接. 32 | func ShortURL2(clt *core.Client, req *ShortURLRequest) (resp *ShortURLResponse, err error) { 33 | m1 := make(map[string]string, 8) 34 | m1["long_url"] = req.LongURL 35 | if req.NonceStr != "" { 36 | m1["nonce_str"] = req.NonceStr 37 | } else { 38 | m1["nonce_str"] = util.NonceStr() 39 | } 40 | if req.SignType != "" { 41 | m1["sign_type"] = req.SignType 42 | } 43 | 44 | m2, err := ShortURL(clt, m1) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | resp = &ShortURLResponse{ 50 | ShortURL: m2["short_url"], 51 | } 52 | return resp, nil 53 | } 54 | -------------------------------------------------------------------------------- /mp/README.md: -------------------------------------------------------------------------------- 1 | ### 回调请求的一般处理逻辑(一个回调地址处理一个公众号的消息和事件) 2 | ```Go 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | "github.com/chanxuehong/wechat/mp/core" 10 | "github.com/chanxuehong/wechat/mp/menu" 11 | "github.com/chanxuehong/wechat/mp/message/callback/request" 12 | "github.com/chanxuehong/wechat/mp/message/callback/response" 13 | ) 14 | 15 | const ( 16 | wxAppId = "appid" 17 | wxAppSecret = "appsecret" 18 | 19 | wxOriId = "oriid" 20 | wxToken = "token" 21 | wxEncodedAESKey = "aeskey" 22 | ) 23 | 24 | var ( 25 | // 下面两个变量不一定非要作为全局变量, 根据自己的场景来选择. 26 | msgHandler core.Handler 27 | msgServer *core.Server 28 | ) 29 | 30 | func init() { 31 | mux := core.NewServeMux() 32 | mux.DefaultMsgHandleFunc(defaultMsgHandler) 33 | mux.DefaultEventHandleFunc(defaultEventHandler) 34 | mux.MsgHandleFunc(request.MsgTypeText, textMsgHandler) 35 | mux.EventHandleFunc(menu.EventTypeClick, menuClickEventHandler) 36 | 37 | msgHandler = mux 38 | msgServer = core.NewServer(wxOriId, wxAppId, wxToken, wxEncodedAESKey, msgHandler, nil) 39 | } 40 | 41 | func textMsgHandler(ctx *core.Context) { 42 | log.Printf("收到文本消息:\n%s\n", ctx.MsgPlaintext) 43 | 44 | msg := request.GetText(ctx.MixedMsg) 45 | resp := response.NewText(msg.FromUserName, msg.ToUserName, msg.CreateTime, msg.Content) 46 | //ctx.RawResponse(resp) // 明文回复 47 | ctx.AESResponse(resp, 0, "", nil) // aes密文回复 48 | } 49 | 50 | func defaultMsgHandler(ctx *core.Context) { 51 | log.Printf("收到消息:\n%s\n", ctx.MsgPlaintext) 52 | ctx.NoneResponse() 53 | } 54 | 55 | func menuClickEventHandler(ctx *core.Context) { 56 | log.Printf("收到菜单 click 事件:\n%s\n", ctx.MsgPlaintext) 57 | 58 | event := menu.GetClickEvent(ctx.MixedMsg) 59 | resp := response.NewText(event.FromUserName, event.ToUserName, event.CreateTime, "收到 click 类型的事件") 60 | //ctx.RawResponse(resp) // 明文回复 61 | ctx.AESResponse(resp, 0, "", nil) // aes密文回复 62 | } 63 | 64 | func defaultEventHandler(ctx *core.Context) { 65 | log.Printf("收到事件:\n%s\n", ctx.MsgPlaintext) 66 | ctx.NoneResponse() 67 | } 68 | 69 | func init() { 70 | http.HandleFunc("/wx_callback", wxCallbackHandler) 71 | } 72 | 73 | // wxCallbackHandler 是处理回调请求的 http handler. 74 | // 1. 不同的 web 框架有不同的实现 75 | // 2. 一般一个 handler 处理一个公众号的回调请求(当然也可以处理多个, 这里我只处理一个) 76 | func wxCallbackHandler(w http.ResponseWriter, r *http.Request) { 77 | msgServer.ServeHTTP(w, r, nil) 78 | } 79 | 80 | func main() { 81 | log.Println(http.ListenAndServe(":80", nil)) 82 | } 83 | ``` 84 | 85 | ### 公众号api调用的一般处理逻辑 86 | ```Go 87 | package main 88 | 89 | import ( 90 | "fmt" 91 | 92 | "github.com/chanxuehong/wechat/mp/base" 93 | "github.com/chanxuehong/wechat/mp/core" 94 | ) 95 | 96 | const ( 97 | wxAppId = "appid" 98 | wxAppSecret = "appsecret" 99 | 100 | wxOriId = "oriid" 101 | wxToken = "token" 102 | wxEncodedAESKey = "aeskey" 103 | ) 104 | 105 | var ( 106 | accessTokenServer core.AccessTokenServer = core.NewDefaultAccessTokenServer(wxAppId, wxAppSecret, nil) 107 | wechatClient *core.Client = core.NewClient(accessTokenServer, nil) 108 | ) 109 | 110 | func main() { 111 | fmt.Println(base.GetCallbackIP(wechatClient)) 112 | } 113 | ``` -------------------------------------------------------------------------------- /mp/account/README.md: -------------------------------------------------------------------------------- 1 | ## 账号管理. 2 | 3 | * 二维码管理在 qrcode 模块. -------------------------------------------------------------------------------- /mp/account/doc.go: -------------------------------------------------------------------------------- 1 | // 账号管理. 2 | // 3 | // 二维码管理在 qrcode 模块. 4 | package account 5 | -------------------------------------------------------------------------------- /mp/base/doc.go: -------------------------------------------------------------------------------- 1 | // 基础模块 2 | package base 3 | -------------------------------------------------------------------------------- /mp/base/getcallbackip.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 获取微信服务器IP地址. 8 | // 9 | // 如果公众号基于安全等考虑,需要获知微信服务器的IP地址列表,以便进行相关限制,可以通过该接口获得微信服务器IP地址列表。 10 | func GetCallbackIP(clt *core.Client) (ipList []string, err error) { 11 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=" 12 | 13 | var result struct { 14 | core.Error 15 | List []string `json:"ip_list"` 16 | } 17 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 18 | return 19 | } 20 | if result.ErrCode != core.ErrCodeOK { 21 | err = &result.Error 22 | return 23 | } 24 | ipList = result.List 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/base/shorturl.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // ShortURL 将一条长链接转成短链接. 8 | func ShortURL(clt *core.Client, longURL string) (shortURL string, err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/shorturl?access_token=" 10 | 11 | var request = struct { 12 | Action string `json:"action"` 13 | LongURL string `json:"long_url"` 14 | }{ 15 | Action: "long2short", 16 | LongURL: longURL, 17 | } 18 | var result struct { 19 | core.Error 20 | ShortURL string `json:"short_url"` 21 | } 22 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 23 | return 24 | } 25 | if result.ErrCode != core.ErrCodeOK { 26 | err = &result.Error 27 | return 28 | } 29 | shortURL = result.ShortURL 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /mp/base/uploadimg.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/chanxuehong/wechat/mp/core" 9 | ) 10 | 11 | // UploadImage 上传图片到微信服务器, 返回的图片url给其他场景使用, 比如图文消息, 卡卷, POI. 12 | func UploadImage(clt *core.Client, imgFilePath string) (url string, err error) { 13 | file, err := os.Open(imgFilePath) 14 | if err != nil { 15 | return 16 | } 17 | defer file.Close() 18 | 19 | return UploadImageFromReader(clt, filepath.Base(imgFilePath), file) 20 | } 21 | 22 | // UploadImageFromReader 上传图片到微信服务器, 返回的图片url给其他场景使用, 比如图文消息, 卡卷, POI. 23 | // 24 | // NOTE: 参数 filename 不是文件路径, 是 multipart/form-data 里面 filename 的值. 25 | func UploadImageFromReader(clt *core.Client, filename string, reader io.Reader) (url string, err error) { 26 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=" 27 | 28 | var fields = []core.MultipartFormField{ 29 | { 30 | IsFile: true, 31 | Name: "media", 32 | FileName: filename, 33 | Value: reader, 34 | }, 35 | } 36 | var result struct { 37 | core.Error 38 | URL string `json:"url"` 39 | } 40 | if err = clt.PostMultipartForm(incompleteURL, fields, &result); err != nil { 41 | return 42 | } 43 | if result.ErrCode != core.ErrCodeOK { 44 | err = &result.Error 45 | return 46 | } 47 | url = result.URL 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /mp/bizwifi/device/add.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type AddParameters struct { 8 | ShopId int64 `json:"shop_id"` // 必须, 门店ID 9 | SSID string `json:"ssid"` // 必须, 无线网络设备的ssid。非认证公众号添加的ssid必需是“WX”开头(“WX”为大写字母),认证公众号和第三方平台无此限制;所有ssid均不能包含中文字符 10 | Password string `json:"password"` // 必须, 无线网络设备的密码,大于8个字符,不能包含中文字符 11 | BSSID string `json:"bssid"` // 必须, 无线网络设备无线mac地址,格式冒号分隔,字符长度17个,并且字母小写,例如:00:1f:7a:ad:5c:a8 12 | } 13 | 14 | // 添加设备 15 | func Add(clt *core.Client, para *AddParameters) (err error) { 16 | var result core.Error 17 | 18 | incompleteURL := "https://api.weixin.qq.com/bizwifi/device/add?access_token=" 19 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 20 | return 21 | } 22 | 23 | if result.ErrCode != core.ErrCodeOK { 24 | err = &result 25 | return 26 | } 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /mp/bizwifi/device/delete.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 删除设备 8 | func Delete(clt *core.Client, bssid string) (err error) { 9 | request := struct { 10 | BSSID string `json:"bssid"` 11 | }{ 12 | BSSID: bssid, 13 | } 14 | 15 | var result core.Error 16 | 17 | incompleteURL := "https://api.weixin.qq.com/bizwifi/device/delete?access_token=" 18 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 19 | return 20 | } 21 | 22 | if result.ErrCode != core.ErrCodeOK { 23 | err = &result 24 | return 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /mp/bizwifi/doc.go: -------------------------------------------------------------------------------- 1 | // 微信连Wi-Fi 2 | package bizwifi 3 | -------------------------------------------------------------------------------- /mp/bizwifi/event.go: -------------------------------------------------------------------------------- 1 | package bizwifi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | // 推送到公众号URL上的事件类型 9 | EventTypeWifiConnected core.EventType = "WifiConnected" // Wi-Fi连网成功事件 10 | ) 11 | 12 | type WifiConnectedEvent struct { 13 | XMLName struct{} `xml:"xml" json:"-"` 14 | core.MsgHeader 15 | 16 | EventType core.EventType `xml:"Event" json:"Event"` // 事件类型,WifiConnected (Wi-Fi连网成功) 17 | 18 | ConnectTime int64 `xml:"ConnectTime" json:"ConnectTime"` // 连网时间(整型) 19 | ExpireTime int64 `xml:"ExpireTime" json:"ExpireTime"` // 系统保留字段,固定值 20 | VendorId string `xml:"VendorId" json:"VendorId"` // 系统保留字段,固定值 21 | PlaceId int64 `xml:"PlaceId" json:"PlaceId"` // 连网的门店id 22 | DeviceNo string `xml:"DeviceNo" json:"DeviceNo"` // 连网的设备无线mac地址,对应bssid 23 | } 24 | 25 | func GetWifiConnectedEvent(msg *core.MixedMsg) *WifiConnectedEvent { 26 | return &WifiConnectedEvent{ 27 | MsgHeader: msg.MsgHeader, 28 | EventType: msg.EventType, 29 | ConnectTime: msg.ConnectTime, 30 | ExpireTime: msg.ExpireTime, 31 | VendorId: msg.VendorId, 32 | PlaceId: msg.PlaceId, 33 | DeviceNo: msg.DeviceNo, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mp/bizwifi/homepage/get.go: -------------------------------------------------------------------------------- 1 | package homepage 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Homepage struct { 8 | ShopId int64 `json:"shop_id"` // 门店ID 9 | TemplateId int64 `json:"template_id"` // 模板类型 10 | URL string `json:"url"` // 商家主页链接 11 | } 12 | 13 | func Get(clt *core.Client, shopId int64) (homepage *Homepage, err error) { 14 | request := struct { 15 | ShopId int64 `json:"shop_id"` 16 | }{ 17 | ShopId: shopId, 18 | } 19 | 20 | var result struct { 21 | core.Error 22 | Homepage `json:"data"` 23 | } 24 | 25 | incompleteURL := "https://api.weixin.qq.com/bizwifi/homepage/get?access_token=" 26 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 27 | return 28 | } 29 | 30 | if result.ErrCode != core.ErrCodeOK { 31 | err = &result.Error 32 | return 33 | } 34 | 35 | homepage = &result.Homepage 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /mp/bizwifi/homepage/set.go: -------------------------------------------------------------------------------- 1 | package homepage 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 默认模板 8 | func NewSetParameters1(shopId int64) interface{} { 9 | return &struct { 10 | ShopId int64 `json:"shop_id"` 11 | TemplateId int64 `json:"template_id"` 12 | }{ 13 | ShopId: shopId, 14 | TemplateId: 0, 15 | } 16 | } 17 | 18 | // 自定义url 19 | func NewSetParameters2(shopId int64, url string) interface{} { 20 | para := struct { 21 | ShopId int64 `json:"shop_id"` 22 | TemplateId int64 `json:"template_id"` 23 | Struct struct { 24 | URL string `json:"url"` 25 | } `json:"struct"` 26 | }{ 27 | ShopId: shopId, 28 | TemplateId: 1, 29 | } 30 | 31 | para.Struct.URL = url 32 | return ¶ 33 | } 34 | 35 | // 设置商家主页 36 | // 37 | // 要求 para 经过 encoding/json 后满足指定的格式要求 38 | func Set(clt *core.Client, para interface{}) (err error) { 39 | var result core.Error 40 | 41 | incompleteURL := "https://api.weixin.qq.com/bizwifi/homepage/set?access_token=" 42 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 43 | return 44 | } 45 | 46 | if result.ErrCode != core.ErrCodeOK { 47 | err = &result 48 | return 49 | } 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /mp/bizwifi/qrcode/get.go: -------------------------------------------------------------------------------- 1 | package qrcode 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 获取物料二维码 8 | // 9 | // shopId: 门店ID 10 | // imgId: 物料样式编号: 11 | // 0-二维码,可用于自由设计宣传材料; 12 | // 1-桌贴(二维码),100mm×100mm(宽×高),可直接张贴 13 | func Get(clt *core.Client, shopId int64, imgId int) (qrcodeURL string, err error) { 14 | request := struct { 15 | ShopId int64 `json:"shop_id"` 16 | ImgId int `json:"img_id"` 17 | }{ 18 | ShopId: shopId, 19 | ImgId: imgId, 20 | } 21 | 22 | var result struct { 23 | core.Error 24 | Data struct { 25 | QrcodeURL string `json:"qrcode_url"` 26 | } `json:"data"` 27 | } 28 | 29 | incompleteURL := "https://api.weixin.qq.com/bizwifi/qrcode/get?access_token=" 30 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 31 | return 32 | } 33 | 34 | if result.ErrCode != core.ErrCodeOK { 35 | err = &result.Error 36 | return 37 | } 38 | 39 | qrcodeURL = result.Data.QrcodeURL 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /mp/bizwifi/statistics/list.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Statistics struct { 8 | ShopId int64 `json:"shop_id"` // 门店ID,-1为总统计 9 | StatisTime int64 `json:"statis_time"` // 统计时间,单位为毫秒 10 | TotalUser int `json:"total_user"` // 微信连wifi成功人数 11 | HomepageUV int `json:"homepage_uv"` // 商家主页访问人数 12 | NewFans int `json:"new_fans"` // 新增公众号关注人数 13 | TotalFans int `json:"total_fans"` // 累计公众号关注人数 14 | } 15 | 16 | // 数据统计 17 | // 18 | // shopId 按门店ID搜索,-1为总统计 19 | // beginDate: 起始日期时间,格式yyyy-mm-dd,最长时间跨度为30天 20 | // endDate: 结束日期时间戳,格式yyyy-mm-dd,最长时间跨度为30天 21 | func List(clt *core.Client, shopId int64, beginDate, endDate string) (data []Statistics, err error) { 22 | request := struct { 23 | ShopId int64 `json:"shop_id"` 24 | BeginDate string `json:"begin_date"` 25 | EndDate string `json:"end_date"` 26 | }{ 27 | ShopId: shopId, 28 | BeginDate: beginDate, 29 | EndDate: endDate, 30 | } 31 | 32 | var result struct { 33 | core.Error 34 | Data []Statistics `json:"data"` 35 | } 36 | 37 | incompleteURL := "https://api.weixin.qq.com/bizwifi/statistics/list?access_token=" 38 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 39 | return 40 | } 41 | 42 | if result.ErrCode != core.ErrCodeOK { 43 | err = &result.Error 44 | return 45 | } 46 | 47 | data = result.Data 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /mp/card/advanced_info_struct.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | type AdvancedInfo struct { 4 | UseCondition *UseCondition `json:"use_condition,omitempty"` //使用门槛(条件)字段,若不填写使用条件则在券面拼写 :无最低消费限制,全场通用,不限品类;并在使用说明显示: 可与其他优惠共享 5 | Abstract *Abstract `json:"abstract,omitempty"` //封面摘要结构体名称 6 | TextImageList []TextImageList `json:"text_image_list,omitempty"` //图文列表,显示在详情内页 ,优惠券券开发者须至少传入 一组图文列表 7 | TimeLimit []TimeLimit `json:"time_limit,omitempty"` //使用时段限制,包含以下字段 8 | BusinessService []string `json:"business_service,omitempty"` //商家服务类型: BIZ_SERVICE_DELIVER 外卖服务; BIZ_SERVICE_FREE_PARK 停车位; BIZ_SERVICE_WITH_PET 可带宠物; BIZ_SERVICE_FREE_WIFI 免费wifi, 可多选 9 | } 10 | 11 | type UseCondition struct { 12 | AcceptCategory string `json:"accept_category,omitempty"` //指定可用的商品类目,仅用于代金券类型 ,填入后将在券面拼写适用于xxx 13 | RejectCategory string `json:"reject_category,omitempty"` //指定不可用的商品类目,仅用于代金券类型 ,填入后将在券面拼写不适用于xxxx 14 | LeastCost int `json:"least_cost,omitempty"` //满减门槛字段,可用于兑换券和代金券 ,填入后将在全面拼写消费满xx元可用。 15 | CanUseWithOtherDiscount bool `json:"can_use_with_other_discount,omitempty"` 16 | } 17 | 18 | type Abstract struct { 19 | Abstract string `json:"abstract,omitempty"` //封面摘要简介。 20 | IconURLList []string `json:"icon_url_list,omitempty"` //封面图片列表,仅支持填入一 个封面图片链接, 上传图片接口 上传获取图片获得链接,填写 非CDN链接会报错,并在此填入。 建议图片尺寸像素850*350 21 | } 22 | 23 | type TextImageList struct { 24 | ImageURL string `json:"image_url,omitempty"` //图片链接,必须调用 上传图片接口 上传图片获得链接,并在此填入, 否则报错 25 | Text string `json:"text,omitempty"` //图文描述 26 | } 27 | 28 | type TimeLimit struct { 29 | Type string `json:"type,omitempty"` //限制类型枚举值:支持填入 MONDAY 周一 TUESDAY 周二 WEDNESDAY 周三 THURSDAY 周四 FRIDAY 周五 SATURDAY 周六 SUNDAY 周日 此处只控制显示, 不控制实际使用逻辑,不填默认不显示 30 | BeginHour int `json:"begin_hour,omitempty"` //当前type类型下的起始时间(小时) ,如当前结构体内填写了MONDAY, 此处填写了10,则此处表示周一 10:00可用 31 | EndHour int `json:"end_hour,omitempty"` //当前type类型下的起始时间(分钟) ,如当前结构体内填写了MONDAY, begin_hour填写10,此处填写了59, 则此处表示周一 10:59可用 32 | BeginMinute int `json:"begin_minute,omitempty"` //当前type类型下的结束时间(小时) ,如当前结构体内填写了MONDAY, 此处填写了20, 则此处表示周一 10:00-20:00可用 33 | EndMinute int `json:"end_minute,omitempty"` //当前type类型下的结束时间(分钟) ,如当前结构体内填写了MONDAY, begin_hour填写10,此处填写了59, 则此处表示周一 10:59-00:59可用 34 | } 35 | -------------------------------------------------------------------------------- /mp/card/boardingpass/checkin.go: -------------------------------------------------------------------------------- 1 | package boardingpass 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type CheckinParameters struct { 8 | Code string `json:"code"` // 必须; 卡券Code码。 9 | CardId string `json:"card_id,omitempty"` // 可选; 卡券ID,自定义Code码的卡券必填。 10 | 11 | PassengerName string `json:"passenger_name,omitempty"` // 必须; 乘客姓名, 上限为15 个汉字. 12 | Class string `json:"class,omitempty"` // 必须; 舱等,如头等舱等,上限为5个汉字。 13 | ETKT_NBR string `json:"etkt_bnr,omitempty"` // 必须; 电子客票号,上限为14个数字。 14 | Seat string `json:"seat,omitempty"` // 可选; 乘客座位号。 15 | QrcodeData string `json:"qrcode_data,omitempty"` // 可选; 二维码数据。乘客用于值机的二维码字符串,微信会通过此数据为用户生成值机用的二维码。 16 | IsCancel *bool `json:"is_cancel,omitempty"` // 可选; 是否取消值机。填写true或false。true代表取消,如填写true上述字段(如calss等)均不做判断,机票返回未值机状态,乘客可重新值机。默认填写false。 17 | } 18 | 19 | // 更新飞机票信息接口 20 | func Checkin(clt *core.Client, para *CheckinParameters) (err error) { 21 | var result core.Error 22 | 23 | incompleteURL := "https://api.weixin.qq.com/card/boardingpass/checkin?access_token=" 24 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 25 | return 26 | } 27 | 28 | if result.ErrCode != core.ErrCodeOK { 29 | err = &result 30 | return 31 | } 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /mp/card/code/code.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | // 某一张特定卡券的标识 4 | type CardItemIdentifier struct { 5 | Code string `json:"code"` // 卡券的Code码 6 | CardId string `json:"card_id,omitempty"` // 卡券ID。创建卡券时use_custom_code填写true时必填。非自定义Code不必填写。 7 | } 8 | 9 | // 某一张特定卡券的信息 10 | type CardItem struct { 11 | Code string `json:"code"` // 卡券的Code码 12 | OpenId string `json:"openid"` // 用户openid 13 | CanConsume bool `json:"can_consume"` // 是否可核销 14 | Card struct { 15 | CardId string `json:"card_id"` // 卡券ID 16 | BeginTime int64 `json:"begin_time"` // 起始使用时间 17 | EndTime int64 `json:"end_time"` // 结束时间 18 | } `json:"card"` 19 | } 20 | -------------------------------------------------------------------------------- /mp/card/code/consume.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 核销Code接口. 8 | func Consume(clt *core.Client, id *CardItemIdentifier) (cardId, openId string, err error) { 9 | var result struct { 10 | core.Error 11 | Card struct { 12 | CardId string `json:"card_id"` 13 | } `json:"card"` 14 | OpenId string `json:"openid"` 15 | } 16 | 17 | incompleteURL := "https://api.weixin.qq.com/card/code/consume?access_token=" 18 | if err = clt.PostJSON(incompleteURL, id, &result); err != nil { 19 | return 20 | } 21 | 22 | if result.ErrCode != core.ErrCodeOK { 23 | err = &result.Error 24 | return 25 | } 26 | cardId = result.Card.CardId 27 | openId = result.OpenId 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /mp/card/code/decrypt.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // Code解码接口 8 | func Decrypt(clt *core.Client, encryptCode string) (code string, err error) { 9 | request := struct { 10 | EncryptCode string `json:"encrypt_code"` 11 | }{ 12 | EncryptCode: encryptCode, 13 | } 14 | 15 | var result struct { 16 | core.Error 17 | Code string `json:"code"` 18 | } 19 | 20 | incompleteURL := "https://api.weixin.qq.com/card/code/decrypt?access_token=" 21 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 22 | return 23 | } 24 | 25 | if result.ErrCode != core.ErrCodeOK { 26 | err = &result.Error 27 | return 28 | } 29 | code = result.Code 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /mp/card/code/get.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 查询code. 8 | func Get(clt *core.Client, id *CardItemIdentifier) (info *CardItem, err error) { 9 | var result struct { 10 | core.Error 11 | CardItem 12 | } 13 | 14 | incompleteURL := "https://api.weixin.qq.com/card/code/get?access_token=" 15 | if err = clt.PostJSON(incompleteURL, id, &result); err != nil { 16 | return 17 | } 18 | 19 | if result.ErrCode != core.ErrCodeOK { 20 | err = &result.Error 21 | return 22 | } 23 | result.CardItem.Code = id.Code 24 | info = &result.CardItem 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/card/code/unavailable.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 设置卡券失效接口. 8 | func Unavailable(clt *core.Client, id *CardItemIdentifier) (err error) { 9 | var result core.Error 10 | 11 | incompleteURL := "https://api.weixin.qq.com/card/code/unavailable?access_token=" 12 | if err = clt.PostJSON(incompleteURL, id, &result); err != nil { 13 | return 14 | } 15 | 16 | if result.ErrCode != core.ErrCodeOK { 17 | err = &result 18 | return 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /mp/card/code/update.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 更改Code接口. 8 | func Update(clt *core.Client, id *CardItemIdentifier, newCode string) (err error) { 9 | request := struct { 10 | *CardItemIdentifier 11 | NewCode string `json:"new_code,omitempty"` 12 | }{ 13 | CardItemIdentifier: id, 14 | NewCode: newCode, 15 | } 16 | 17 | var result core.Error 18 | 19 | incompleteURL := "https://api.weixin.qq.com/card/code/update?access_token=" 20 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 21 | return 22 | } 23 | 24 | if result.ErrCode != core.ErrCodeOK { 25 | err = &result 26 | return 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /mp/card/color.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Color struct { 8 | Name string `json:"name"` 9 | Value string `json:"value"` 10 | } 11 | 12 | // 获取卡券最新的颜色列表. 13 | func GetColors(clt *core.Client) (colors []Color, err error) { 14 | var result struct { 15 | core.Error 16 | Colors []Color `json:"colors"` 17 | } 18 | 19 | incompleteURL := "https://api.weixin.qq.com/card/getcolors?access_token=" 20 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 21 | return 22 | } 23 | 24 | if result.ErrCode != core.ErrCodeOK { 25 | err = &result.Error 26 | return 27 | } 28 | colors = result.Colors 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /mp/card/datacube/doc.go: -------------------------------------------------------------------------------- 1 | // see github.com/chanxuehong/wechat/mp/datacube 2 | package datacube 3 | -------------------------------------------------------------------------------- /mp/card/doc.go: -------------------------------------------------------------------------------- 1 | // 微信卡券接口 2 | package card 3 | -------------------------------------------------------------------------------- /mp/card/meetingticket/updateuser.go: -------------------------------------------------------------------------------- 1 | package meetingticket 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type UpdateUserParameters struct { 8 | Code string `json:"code"` // 必须; 用户的门票唯一序列号 9 | CardId string `json:"card_id,omitempty"` // 可选; 要更新门票序列号所述的card_id , 生成券时use_custom_code 填写true 时必填. 10 | 11 | Zone string `json:"zone,omitempty"` // 必须; 区域 12 | Entrance string `json:"entrance,omitempty"` // 必须; 入口 13 | SeatNumber string `json:"seat_number,omitempty"` // 必须; 座位号 14 | BeginTime int64 `json:"begin_time,omitempty"` // 可选; 开场时间,Unix时间戳格式。 15 | EndTime int64 `json:"end_time,omitempty"` // 可选; 结束时间,Unix时间戳格式。 16 | } 17 | 18 | // 更新会议门票 19 | func UpdateUser(clt *core.Client, para *UpdateUserParameters) (err error) { 20 | var result core.Error 21 | 22 | incompleteURL := "https://api.weixin.qq.com/card/meetingticket/updateuser?access_token=" 23 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 24 | return 25 | } 26 | 27 | if result.ErrCode != core.ErrCodeOK { 28 | err = &result 29 | return 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /mp/card/membercard/activate.go: -------------------------------------------------------------------------------- 1 | package membercard 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type ActivateParameters struct { 8 | Code string `json:"code"` // 必填, 创建会员卡时获取的初始code。 9 | CardId string `json:"card_id,omitempty"` // 可选; 卡券ID. 自定义code 的会员卡必填card_id, 非自定义code 的会员卡不必填. 10 | 11 | MembershipNumber string `json:"membership_number,omitempty"` // 必填, 会员卡编号,由开发者填入,作为序列号显示在用户的卡包里。可与Code码保持等值。 12 | 13 | ActivateBeginTime int64 `json:"activate_begin_time,omitempty"` // 可选; 激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。 14 | ActivateEndTime int64 `json:"activate_end_time,omitempty"` // 可选; 激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。 15 | 16 | InitBonus *int `json:"init_bonus,omitempty"` // 可选; 初始积分, 不填为0 17 | InitBalance *int `json:"init_balance,omitempty"` // 可选; 初始余额, 不填为0 18 | 19 | InitCustomFieldValue1 string `json:"init_custom_field_value1,omitempty"` // 可选, 创建时字段custom_field1定义类型的初始值,限制为4个汉字,12字节。 20 | InitCustomFieldValue2 string `json:"init_custom_field_value2,omitempty"` // 可选, 创建时字段custom_field2定义类型的初始值,限制为4个汉字,12字节。 21 | InitCustomFieldValue3 string `json:"init_custom_field_value3,omitempty"` // 可选, 创建时字段custom_field3定义类型的初始值,限制为4个汉字,12字节。 22 | } 23 | 24 | // 激活/绑定会员卡 25 | func Activate(clt *core.Client, para *ActivateParameters) (err error) { 26 | var result core.Error 27 | 28 | incompleteURL := "https://api.weixin.qq.com/card/membercard/activate?access_token=" 29 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 30 | return 31 | } 32 | 33 | if result.ErrCode != core.ErrCodeOK { 34 | err = &result 35 | return 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /mp/card/membercard/updateuser.go: -------------------------------------------------------------------------------- 1 | package membercard 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type UpdateUserParameters struct { 8 | Code string `json:"code"` // 必须; 要消耗的序列号. 9 | CardId string `json:"card_id,omitempty"` // 可选; 要消耗序列号所述的card_id. 自定义code 的会员卡必填 10 | 11 | AddBonus int `json:"add_bonus,omitempty"` // 必须; 需要变更的积分,扣除积分用“-“表示。 12 | RecordBonus string `json:"record_bonus,omitempty"` // 可选; 商家自定义积分消耗记录,不超过14个汉字。 13 | AddBalance int `json:"add_balance,omitempty"` // 可选; 需要变更的余额,扣除金额用“-”表示。单位为分。 14 | RecordBalance string `json:"record_balance,omitempty"` // 可选; 商家自定义金额消耗记录,不超过14个汉字。 15 | 16 | CustomFieldValue1 string `json:"custom_field_value1,omitempty"` // 可选, 创建时字段custom_field1定义类型的初始值,限制为4个汉字,12字节。 17 | CustomFieldValue2 string `json:"custom_field_value2,omitempty"` // 可选, 创建时字段custom_field2定义类型的初始值,限制为4个汉字,12字节。 18 | CustomFieldValue3 string `json:"custom_field_value3,omitempty"` // 可选, 创建时字段custom_field3定义类型的初始值,限制为4个汉字,12字节。 19 | } 20 | 21 | type UpdateUserResult struct { 22 | ResultBonus int `json:"result_bonus"` // 当前用户积分总额。 23 | ResultBalance int `json:"result_balance"` // 当前用户预存总金额。 24 | OpenId string `json:"openid"` // 用户openid。 25 | } 26 | 27 | // 更新会员信息 28 | func UpdateUser(clt *core.Client, para *UpdateUserParameters) (rslt *UpdateUserResult, err error) { 29 | var result struct { 30 | core.Error 31 | UpdateUserResult 32 | } 33 | 34 | incompleteURL := "https://api.weixin.qq.com/card/membercard/updateuser?access_token=" 35 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 36 | return 37 | } 38 | 39 | if result.ErrCode != core.ErrCodeOK { 40 | err = &result.Error 41 | return 42 | } 43 | rslt = &result.UpdateUserResult 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /mp/card/membercard/userinfo/get.go: -------------------------------------------------------------------------------- 1 | package userinfo 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/card/code" 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | type CustomField struct { 9 | Name string `json:"name"` 10 | Value string `json:"value"` 11 | } 12 | 13 | type UserInfo struct { 14 | OpenID string `json:"openid"` 15 | Nickname string `json:"nickname"` 16 | Sex string `json:"sex"` 17 | CustomFieldList []CustomField `json:"custom_field_list"` 18 | } 19 | 20 | // 拉取会员信息(积分查询)接口 21 | func Get(clt *core.Client, id *code.CardItemIdentifier) (info *UserInfo, err error) { 22 | var result struct { 23 | core.Error 24 | UserInfo 25 | } 26 | 27 | incompleteURL := "https://api.weixin.qq.com/card/membercard/userinfo/get?access_token=" 28 | if err = clt.PostJSON(incompleteURL, id, &result); err != nil { 29 | return 30 | } 31 | 32 | if result.ErrCode != core.ErrCodeOK { 33 | err = &result.Error 34 | return 35 | } 36 | info = &result.UserInfo 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /mp/card/movieticket/updateuser.go: -------------------------------------------------------------------------------- 1 | package movieticket 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type UpdateUserParameters struct { 8 | Code string `json:"code"` // 必须; 卡券Code码。 9 | CardId string `json:"card_id,omitempty"` // 可选; 要更新门票序列号所述的card_id,生成券时use_custom_code填写true时必填。 10 | 11 | TicketClass string `json:"ticket_class,omitempty"` // 必须; 电影票的类别,如2D、3D。 12 | ShowTime int64 `json:"show_time,omitempty"` // 必须; 电影的放映时间,Unix时间戳格式。 13 | Duration int `json:"duration,omitempty"` // 必须; 放映时长,填写整数。 14 | ScreeningRoom string `json:"screening_room,omitempty"` // 可选; 该场电影的影厅信息。 15 | SeatNumber string `json:"seat_number,omitempty"` // 可选; 座位号。 16 | } 17 | 18 | // 更新电影票 19 | func UpdateUser(clt *core.Client, para *UpdateUserParameters) (err error) { 20 | var result core.Error 21 | 22 | incompleteURL := "https://api.weixin.qq.com/card/movieticket/updateuser?access_token=" 23 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 24 | return 25 | } 26 | 27 | if result.ErrCode != core.ErrCodeOK { 28 | err = &result 29 | return 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /mp/card/mpnews/gethtml.go: -------------------------------------------------------------------------------- 1 | package mpnews 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 获取卡券嵌入图文消息的标准格式代码. 8 | // 9 | // 将返回代码填入上传图文素材接口中content字段,即可获取嵌入卡券的图文消息素材。 10 | func GetHTML(clt *core.Client, cardId string) (content string, err error) { 11 | request := struct { 12 | CardId string `json:"card_id"` 13 | }{ 14 | CardId: cardId, 15 | } 16 | 17 | var result struct { 18 | core.Error 19 | Content string `json:"content"` 20 | } 21 | 22 | incompleteURL := "https://api.weixin.qq.com/card/mpnews/gethtml?access_token=" 23 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 24 | return 25 | } 26 | 27 | if result.ErrCode != core.ErrCodeOK { 28 | err = &result.Error 29 | return 30 | } 31 | content = result.Content 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /mp/card/qrcode/create.go: -------------------------------------------------------------------------------- 1 | package qrcode 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | func QrcodePicURL(ticket string) string { 10 | return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + url.QueryEscape(ticket) 11 | } 12 | 13 | type CreateParameters struct { 14 | CardId string `json:"card_id"` // 必须; 卡券ID 15 | Code string `json:"code,omitempty"` // 可选; use_custom_code字段为true的卡券必须填写,非自定义code不必填写 16 | OpenId string `json:"openid,omitempty"` // 可选; 指定领取者的openid,只有该用户能领取。bind_openid字段为true的卡券必须填写,非指定openid不必填写。 17 | ExpireSeconds int `json:"expire_seconds,omitempty"` // 可选; 指定二维码的有效时间,范围是60 ~ 1800秒。不填(值为0)默认为永久有效。 18 | IsUniqueCode *bool `json:"is_unique_code,omitempty"` // 可选; 指定下发二维码,生成的二维码随机分配一个code,领取后不可再次扫描。填写true或false。默认false。 19 | OuterId *int64 `json:"outer_id,omitempty"` // 可选; 领取场景值,用于领取渠道的数据统计,默认值为0,字段类型为整型,长度限制为60位数字。用户领取卡券后触发的事件推送中会带上此自定义场景值。 20 | } 21 | 22 | type QrcodeInfo struct { 23 | Ticket string `json:"ticket"` 24 | URL string `json:"url"` 25 | ExpireSeconds int `json:"expire_seconds"` // 0 表示永久二维码 26 | } 27 | 28 | // 卡券投放, 创建二维码接口. 29 | func Create(clt *core.Client, para *CreateParameters) (info *QrcodeInfo, err error) { 30 | request := struct { 31 | ActionName string `json:"action_name"` 32 | ExpireSeconds int `json:"expire_seconds,omitempty"` 33 | ActionInfo struct { 34 | Card *CreateParameters `json:"card,omitempty"` 35 | } `json:"action_info"` 36 | }{ 37 | ActionName: "QR_CARD", 38 | ExpireSeconds: para.ExpireSeconds, 39 | } 40 | request.ActionInfo.Card = para 41 | 42 | var result struct { 43 | core.Error 44 | QrcodeInfo 45 | } 46 | 47 | incompleteURL := "https://api.weixin.qq.com/card/qrcode/create?access_token=" 48 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 49 | return 50 | } 51 | 52 | if result.ErrCode != core.ErrCodeOK { 53 | err = &result.Error 54 | return 55 | } 56 | info = &result.QrcodeInfo 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /mp/card/sign.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "sort" 8 | ) 9 | 10 | // 卡券通用签名方法. 11 | // 12 | // 将 strs 里面的字符串字典排序, 然后拼接成一个字符串后做 sha1 签名. 13 | func Sign(strs []string) (signature string) { 14 | sort.Strings(strs) 15 | 16 | h := sha1.New() 17 | 18 | bufw := bufio.NewWriterSize(h, 128) // sha1.BlockSize 的整数倍 19 | for _, str := range strs { 20 | bufw.WriteString(str) 21 | } 22 | bufw.Flush() 23 | 24 | return hex.EncodeToString(h.Sum(nil)) 25 | } 26 | -------------------------------------------------------------------------------- /mp/card/testwhitelist/set.go: -------------------------------------------------------------------------------- 1 | package testwhitelist 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type SetParameters struct { 8 | OpenIdList []string `json:"openid,omitempty"` // 测试的openid列表 9 | UserNameList []string `json:"username,omitempty"` // 测试的微信号列表 10 | } 11 | 12 | // 设置测试白名单 13 | func Set(clt *core.Client, para *SetParameters) (err error) { 14 | var result core.Error 15 | 16 | incompleteURL := "https://api.weixin.qq.com/card/testwhitelist/set?access_token=" 17 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 18 | return 19 | } 20 | 21 | if result.ErrCode != core.ErrCodeOK { 22 | err = &result 23 | return 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/card/user/getcardlist.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/card/code" 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | // 获取用户已领取卡券接口 9 | // 10 | // openid: 需要查询的用户openid 11 | // cardid: 卡券ID。不填写时默认查询当前appid下的卡券。 12 | func GetCardList(clt *core.Client, openid, cardid string) (list []code.CardItemIdentifier, err error) { 13 | request := struct { 14 | OpenId string `json:"openid"` 15 | CardId string `json:"card_id,omitempty"` 16 | }{ 17 | OpenId: openid, 18 | CardId: cardid, 19 | } 20 | 21 | var result struct { 22 | core.Error 23 | CardList []code.CardItemIdentifier `json:"card_list"` 24 | } 25 | 26 | incompleteURL := "https://api.weixin.qq.com/card/user/getcardlist?access_token=" 27 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 28 | return 29 | } 30 | 31 | if result.ErrCode != core.ErrCodeOK { 32 | err = &result.Error 33 | return 34 | } 35 | list = result.CardList 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /mp/core/README.md: -------------------------------------------------------------------------------- 1 | ## 微信公众号 SDK 核心 package 2 | 微信公众号的处理逻辑都在这个 package 里面, 其他的模块都是在这个 package 基础上的再封装! 3 | 4 | ### 回调请求处理 5 | 一个回调地址(多个公众号可以共用一个回调地址)的 http 请求对应了一个 http handler(http.Handler, gin.HandlerFunc…), 6 | 这个 http handler 里面的主要逻辑是调用对应公众号的 core.Server 的 ServeHTTP 方法来处理回调请求, 7 | core.Server.ServeHTTP 做签名的验证和消息解密, 然后调用 core.Server 的 core.Handler 属性的 ServeMsg 方法来处理消息(事件). 8 | ![回调请求处理逻辑图](https://github.com/chanxuehong/wechat/blob/v2/mp/core/callback20160118.png) 9 | -------------------------------------------------------------------------------- /mp/core/buffer_pool.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var textBufferPool = sync.Pool{ 9 | New: func() interface{} { 10 | return bytes.NewBuffer(make([]byte, 0, 4<<10)) // 4KB 11 | }, 12 | } 13 | 14 | var mediaBufferPool = sync.Pool{ 15 | New: func() interface{} { 16 | return bytes.NewBuffer(make([]byte, 0, 10<<20)) // 10MB 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /mp/core/callback20160118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chanxuehong/wechat/36f0325263cdec440d6e36f93d168e4cc39b64b8/mp/core/callback20160118.png -------------------------------------------------------------------------------- /mp/core/doc.go: -------------------------------------------------------------------------------- 1 | // 微信公众平台(订阅号&服务号) SDK 的核心库 2 | package core 3 | -------------------------------------------------------------------------------- /mp/core/error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | const ( 9 | ErrCodeOK = 0 10 | ErrCodeInvalidCredential = 40001 // access_token 过期错误码 11 | ErrCodeAccessTokenExpired = 42001 // access_token 过期错误码(maybe!!!) 12 | ) 13 | 14 | var ( 15 | errorType = reflect.TypeOf(Error{}) 16 | errorZeroValue = reflect.Zero(errorType) 17 | ) 18 | 19 | const ( 20 | errorErrCodeIndex = 0 21 | errorErrMsgIndex = 1 22 | ) 23 | 24 | type Error struct { 25 | ErrCode int64 `json:"errcode"` 26 | ErrMsg string `json:"errmsg"` 27 | } 28 | 29 | func (err *Error) Error() string { 30 | return fmt.Sprintf("errcode: %d, errmsg: %s", err.ErrCode, err.ErrMsg) 31 | } 32 | -------------------------------------------------------------------------------- /mp/core/error_handler.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | type ErrorHandler interface { 10 | ServeError(http.ResponseWriter, *http.Request, error) 11 | } 12 | 13 | var DefaultErrorHandler ErrorHandler = ErrorHandlerFunc(defaultErrorHandlerFunc) 14 | 15 | type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, error) 16 | 17 | func (fn ErrorHandlerFunc) ServeError(w http.ResponseWriter, r *http.Request, err error) { 18 | fn(w, r, err) 19 | } 20 | 21 | var errorLogger = log.New(os.Stderr, "[WECHAT_ERROR] ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Llongfile) 22 | 23 | func defaultErrorHandlerFunc(w http.ResponseWriter, r *http.Request, err error) { 24 | errorLogger.Output(3, err.Error()) 25 | } 26 | -------------------------------------------------------------------------------- /mp/core/example_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | func ExampleServer_ServeHTTP() { 10 | mux := core.NewServeMux() // 创建 core.Handler, 也可以用自己实现的 core.Handler 11 | 12 | // 注册消息(事件)处理 Handler, 都不是必须的! 13 | { 14 | mux.UseFunc(func(ctx *core.Context) { // 注册中间件, 处理所有的消息(事件) 15 | // TODO: 中间件处理逻辑 16 | }) 17 | mux.UseFuncForMsg(func(ctx *core.Context) { // 注册中间件, 处理所有的消息 18 | // TODO: 中间件处理逻辑 19 | }) 20 | mux.UseFuncForEvent(func(ctx *core.Context) { // 注册中间件, 处理所有的事件 21 | // TODO: 中间件处理逻辑 22 | }) 23 | 24 | mux.DefaultMsgHandleFunc(func(ctx *core.Context) { // 设置默认消息处理 Handler 25 | // TODO: 消息处理逻辑 26 | }) 27 | mux.DefaultEventHandleFunc(func(ctx *core.Context) { // 设置默认事件处理 Handler 28 | // TODO: 事件处理逻辑 29 | }) 30 | 31 | mux.MsgHandleFunc("{MsgType}", func(ctx *core.Context) { // 设置具体类型的消息处理 Handler 32 | // TODO: 消息处理逻辑 33 | }) 34 | mux.EventHandleFunc("{EventType}", func(ctx *core.Context) { // 设置具体类型的事件处理 Handler 35 | // TODO: 事件处理逻辑 36 | }) 37 | } 38 | 39 | // 创建 Server, 设置正确的参数. 40 | // 通常一个 Server 对应一个公众号, 当然一个 Server 也可以对应多个公众号, 这个时候 oriId 和 appId 都应该设置为空值! 41 | srv := core.NewServer("{oriId}", "{appId}", "{token}", "{base64AESKey}", mux, nil) 42 | 43 | // 在回调 URL 的 Handler 里处理消息(事件) 44 | http.HandleFunc("/wechat_callback", func(w http.ResponseWriter, r *http.Request) { 45 | srv.ServeHTTP(w, r, nil) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /mp/core/server_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | ) 7 | 8 | func TestXmlUnmarshal(t *testing.T) { 9 | data := []byte(` 10 | 11 | 12 | `) 13 | 14 | var x cipherRequestHttpBody 15 | if err := xmlUnmarshal(data, &x); err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | if x.ToUserName != "gh_b1eb3f8bd6c6" { 20 | t.Errorf("ToUserName mismatch,\nhave: %s\nwant: %s\n", x.ToUserName, "gh_b1eb3f8bd6c6") 21 | return 22 | } 23 | wantEncrypt := `DlCGq+lWQuyjNNK+vDaO0zUltpdUW3u4V00WCzsdNzmZGEhrU7TPxG52viOKCWYPwTMbCzgbCtakZHyNxr5hjoZJ7ORAUYoIAGQy/LDWtAnYgDO+ppKLp0rDq+67Dv3yt+vatMQTh99NII6x9SEGpY3O2h8RpG99+NYevQiOLVKqiQYzan21sX/jE4Y3wZaeudsb4QVjqzRAPaCJ5nS3T31uIR9fjSRgHTDRDOzjQ1cHchge+t6faUhniN5VQVTE+wIYtmnejc55BmHYPfBnTkYah9+cTYnI3diUPJRRiyVocJyHlb+XOZN22dsx9yzKHBAyagaoDIV8Yyb/PahcUbsqGv5wziOgLJQIa6z93/VY7d2Kq2C2oBS+Qb+FI9jLhgc3RvCi+Yno2X3cWoqbsRwoovYdyg6jme/H7nMZn77PSxOGRt/dYiWx2NuBAF7fNFigmbRiive3DyOumNCMvA==` 24 | if string(x.Base64EncryptedMsg) != wantEncrypt { 25 | t.Errorf("Encrypt mismatch,\nhave: %s\nwant: %s\n", x.Base64EncryptedMsg, wantEncrypt) 26 | return 27 | } 28 | } 29 | 30 | func BenchmarkXmlUnmarshal(b *testing.B) { 31 | b.ReportAllocs() 32 | b.ResetTimer() 33 | 34 | data := []byte(` 35 | 36 | 37 | `) 38 | var x cipherRequestHttpBody 39 | for i := 0; i < b.N; i++ { 40 | xmlUnmarshal(data, &x) 41 | } 42 | } 43 | 44 | func BenchmarkStdXmlUnmarshal(b *testing.B) { 45 | b.ReportAllocs() 46 | b.ResetTimer() 47 | 48 | data := []byte(` 49 | 50 | 51 | `) 52 | var x cipherRequestHttpBody 53 | for i := 0; i < b.N; i++ { 54 | xml.Unmarshal(data, &x) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mp/core/started_checker.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | const ( 8 | startedCheckerInitialValue = uintptr(0) 9 | startedCheckerStartedValue = ^uintptr(0) 10 | ) 11 | 12 | // 正常情况下实例的配置方法和服务方法是不能并行执行的(这两种方法竞争同一份配置数据), 一般我们有两种方案: 13 | // 1. 用互斥锁 14 | // 2. 明确文档告知该实例不是并行安全的 15 | // 对于第2种场景, 很多程序员有可能不小心并行执行了该实例的配置方法和服务方法, 那有没有好的解决方案呢? 16 | // 其实在大部分场景下, 实例可以先配置, 投入服务后就没有必要修改其配置了(如果有必要修改的只能用互斥锁了), 17 | // startedChecker 就是为这种场景设计的, 能在很大程度上保证数据安全(不是绝对, 极小的概率下会出现数据竞争). 18 | type startedChecker uintptr 19 | 20 | func (p *startedChecker) start() { 21 | if uintptr(*p) == startedCheckerInitialValue { 22 | atomic.CompareAndSwapUintptr((*uintptr)(p), startedCheckerInitialValue, startedCheckerStartedValue) 23 | } 24 | } 25 | 26 | func (v startedChecker) check() { 27 | if uintptr(v) != startedCheckerInitialValue { 28 | panic("the service has been started.") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mp/datacube/README.md: -------------------------------------------------------------------------------- 1 | ## 数据统计接口 2 | -------------------------------------------------------------------------------- /mp/datacube/card/card.go: -------------------------------------------------------------------------------- 1 | // 卡券数据统计接口 2 | package card 3 | 4 | // 请求数据结构 5 | type Request struct { 6 | BeginDate string `json:"begin_date"` // 查询数据的起始时间, YYYY-MM-DD 格式; 7 | EndDate string `json:"end_date"` // 查询数据的截至时间, YYYY-MM-DD 格式; 8 | CondSource int `json:"cond_source"` // 卡券来源,0为公众平台创建的卡券数据、1是API创建的卡券数据 9 | CardId string `json:"card_id,omitempty"` // 可选; 卡券ID。填写后,指定拉出该卡券的相关数据。 10 | } 11 | -------------------------------------------------------------------------------- /mp/datacube/card/getcardbizuininfo.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 卡券概况数据 8 | type BizUinData struct { 9 | RefDate string `json:"ref_date"` // 日期信息, YYYY-MM-DD 10 | ViewCount int `json:"view_cnt"` // 浏览次数 11 | ViewUser int `json:"view_user"` // 浏览人数 12 | ReceiveCount int `json:"receive_cnt"` // 领取次数 13 | ReceiveUser int `json:"receive_user"` // 领取人数 14 | VerifyCount int `json:"verify_cnt"` // 使用次数 15 | VerifyUser int `json:"verify_user"` // 使用人数 16 | GivenCount int `json:"given_cnt"` // 转赠次数 17 | GivenUser int `json:"given_user"` // 转赠人数 18 | ExpireCount int `json:"expire_cnt"` // 过期次数 19 | ExpireUser int `json:"expire_user"` // 过期人数 20 | } 21 | 22 | // 拉取卡券概况数据接口 23 | func GetBizUinInfo(clt *core.Client, req *Request) (list []BizUinData, err error) { 24 | var result struct { 25 | core.Error 26 | List []BizUinData `json:"list"` 27 | } 28 | 29 | incompleteURL := "https://api.weixin.qq.com/datacube/getcardbizuininfo?access_token=" 30 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 31 | return 32 | } 33 | 34 | if result.ErrCode != core.ErrCodeOK { 35 | err = &result.Error 36 | return 37 | } 38 | list = result.List 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /mp/datacube/card/getcardcardinfo.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 免费券数据 8 | type CardData struct { 9 | RefDate string `json:"ref_date"` // 日期信息, YYYY-MM-DD 10 | CardId string `json:"card_id"` // 卡券ID 11 | CardType int `json:"card_type"` // cardtype:0:折扣券,1:代金券,2:礼品券,3:优惠券,4:团购券(暂不支持拉取特殊票券类型数据,电影票、飞机票、会议门票、景区门票) 12 | IsPay int `json:"is_pay"` // 是否付费券。0为非付费券,1为付费券 13 | ViewCount int `json:"view_cnt"` // 浏览次数 14 | ViewUser int `json:"view_user"` // 浏览人数 15 | ReceiveCount int `json:"receive_cnt"` // 领取次数 16 | ReceiveUser int `json:"receive_user"` // 领取人数 17 | VerifyCount int `json:"verify_cnt"` // 使用次数 18 | VerifyUser int `json:"verify_user"` // 使用人数 19 | GivenCount int `json:"given_cnt"` // 转赠次数 20 | GivenUser int `json:"given_user"` // 转赠人数 21 | ExpireCount int `json:"expire_cnt"` // 过期次数 22 | ExpireUser int `json:"expire_user"` // 过期人数 23 | } 24 | 25 | // 获取免费券数据接口 26 | func GetCardInfo(clt *core.Client, req *Request) (list []CardData, err error) { 27 | var result struct { 28 | core.Error 29 | List []CardData `json:"list"` 30 | } 31 | 32 | incompleteURL := "https://api.weixin.qq.com/datacube/getcardcardinfo?access_token=" 33 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 34 | return 35 | } 36 | 37 | if result.ErrCode != core.ErrCodeOK { 38 | err = &result.Error 39 | return 40 | } 41 | list = result.List 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /mp/datacube/card/getcardmembercardinfo.go: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 会员卡数据 8 | type MemberCardData struct { 9 | RefDate string `json:"ref_date"` // 日期信息, YYYY-MM-DD 10 | ViewCount int `json:"view_cnt"` // 浏览次数 11 | ViewUser int `json:"view_user"` // 浏览人数 12 | ReceiveCount int `json:"receive_cnt"` // 领取次数 13 | ReceiveUser int `json:"receive_user"` // 领取人数 14 | VerifyCount int `json:"verify_cnt"` // 使用次数 15 | VerifyUser int `json:"verify_user"` // 使用人数 16 | ActiveUser int `json:"active_user"` // 激活人数 17 | TotalUser int `json:"total_user"` // 有效会员总人数 18 | TotalReceiveUser int `json:"total_receive_user"` // 历史领取会员卡总人数 19 | } 20 | 21 | // 拉取会员卡数据接口 22 | func GetMemberCardInfo(clt *core.Client, req *Request) (list []MemberCardData, err error) { 23 | var result struct { 24 | core.Error 25 | List []MemberCardData `json:"list"` 26 | } 27 | 28 | incompleteURL := "https://api.weixin.qq.com/datacube/getcardmembercardinfo?access_token=" 29 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 30 | return 31 | } 32 | 33 | if result.ErrCode != core.ErrCodeOK { 34 | err = &result.Error 35 | return 36 | } 37 | list = result.List 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /mp/datacube/doc.go: -------------------------------------------------------------------------------- 1 | // 数据统计接口. 2 | package datacube 3 | -------------------------------------------------------------------------------- /mp/datacube/interface.go: -------------------------------------------------------------------------------- 1 | package datacube 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | // 接口分析数据 10 | type InterfaceSummaryData struct { 11 | RefDate string `json:"ref_date"` // 数据的日期, YYYY-MM-DD 格式 12 | CallbackCount int `json:"callback_count"` // 通过服务器配置地址获得消息后, 被动回复用户消息的次数 13 | FailCount int `json:"fail_count"` // 上述动作的失败次数 14 | TotalTimeCost int64 `json:"total_time_cost"` // 总耗时, 除以callback_count即为平均耗时 15 | MaxTimeCost int64 `json:"max_time_cost"` // 最大耗时 16 | } 17 | 18 | // 获取接口分析数据. 19 | func GetInterfaceSummary(clt *core.Client, req *Request) (list []InterfaceSummaryData, err error) { 20 | if req == nil { 21 | err = errors.New("nil Request") 22 | return 23 | } 24 | 25 | var result struct { 26 | core.Error 27 | List []InterfaceSummaryData `json:"list"` 28 | } 29 | 30 | incompleteURL := "https://api.weixin.qq.com/datacube/getinterfacesummary?access_token=" 31 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 32 | return 33 | } 34 | 35 | if result.ErrCode != core.ErrCodeOK { 36 | err = &result.Error 37 | return 38 | } 39 | list = result.List 40 | return 41 | } 42 | 43 | type InterfaceSummaryHourData struct { 44 | RefHour int `json:"ref_hour"` // 数据的小时, 包括从000到2300, 分别代表的是[000,100)到[2300,2400), 即每日的第1小时和最后1小时 45 | InterfaceSummaryData 46 | } 47 | 48 | // 获取接口分析分时数据. 49 | func GetInterfaceSummaryHour(clt *core.Client, req *Request) (list []InterfaceSummaryHourData, err error) { 50 | if req == nil { 51 | err = errors.New("nil Request") 52 | return 53 | } 54 | 55 | var result struct { 56 | core.Error 57 | List []InterfaceSummaryHourData `json:"list"` 58 | } 59 | 60 | incompleteURL := "https://api.weixin.qq.com/datacube/getinterfacesummaryhour?access_token=" 61 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 62 | return 63 | } 64 | 65 | if result.ErrCode != core.ErrCodeOK { 66 | err = &result.Error 67 | return 68 | } 69 | list = result.List 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /mp/datacube/reqeust.go: -------------------------------------------------------------------------------- 1 | package datacube 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 获取统计数据通用的请求结构. 8 | type Request struct { 9 | // 获取数据的起始日期, YYYY-MM-DD 格式; 10 | // begin_date 和 end_date 的差值需小于"最大时间跨度"(比如最大时间跨度为1时, 11 | // begin_date 和 end_date 的差值只能为0, 才能小于1), 否则会报错. 12 | BeginDate string `json:"begin_date,omitempty"` 13 | 14 | // 获取数据的结束日期, YYYY-MM-DD 格式; 15 | // end_date 允许设置的最大值为昨日. 16 | EndDate string `json:"end_date,omitempty"` 17 | } 18 | 19 | // NewRequest 创建一个 Request. 20 | // 21 | // 请注意 BeginDate, EndDate 的 Location. 22 | func NewRequest(BeginDate, EndDate time.Time) *Request { 23 | return &Request{ 24 | BeginDate: BeginDate.Format("2006-01-02"), 25 | EndDate: EndDate.Format("2006-01-02"), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mp/datacube/user.go: -------------------------------------------------------------------------------- 1 | package datacube 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | // 用户增减数据 10 | type UserSummaryData struct { 11 | RefDate string `json:"ref_date"` // 数据的日期, YYYY-MM-DD 格式 12 | 13 | // 用户的渠道, 数值代表的含义如下: 14 | // 0 代表其他 15 | // 1 xxx(文档没有说明) 16 | // 2 xxx(文档没有说明) 17 | // 3 代表扫二维码 18 | // 4 xxx(文档没有说明) 19 | // 5 xxx(文档没有说明) 20 | // 17 代表名片分享 21 | // 35 代表搜号码(即微信添加朋友页的搜索) 22 | // 39 代表查询微信公众帐号 23 | // 43 代表图文页右上角菜单 24 | UserSource int `json:"user_source"` 25 | 26 | NewUser int `json:"new_user"` // 新增的用户数量 27 | CancelUser int `json:"cancel_user"` // 取消关注的用户数量, new_user 减去 cancel_user即为净增用户数量 28 | } 29 | 30 | // 获取用户增减数据. 31 | func GetUserSummary(clt *core.Client, req *Request) (list []UserSummaryData, err error) { 32 | if req == nil { 33 | err = errors.New("nil Request") 34 | return 35 | } 36 | 37 | var result struct { 38 | core.Error 39 | List []UserSummaryData `json:"list"` 40 | } 41 | 42 | incompleteURL := "https://api.weixin.qq.com/datacube/getusersummary?access_token=" 43 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 44 | return 45 | } 46 | 47 | if result.ErrCode != core.ErrCodeOK { 48 | err = &result.Error 49 | return 50 | } 51 | list = result.List 52 | return 53 | } 54 | 55 | // 累计用户数据 56 | type UserCumulateData struct { 57 | RefDate string `json:"ref_date"` // 数据的日期, YYYY-MM-DD 格式 58 | UserSource int `json:"user_source"` // 返回的 json 有这个字段, 文档中没有, 都是 0 值, 可能没有实际意义!!! 59 | CumulateUser int `json:"cumulate_user"` // 总用户量 60 | } 61 | 62 | // 获取累计用户数据. 63 | func GetUserCumulate(clt *core.Client, req *Request) (list []UserCumulateData, err error) { 64 | if req == nil { 65 | err = errors.New("nil Request") 66 | return 67 | } 68 | 69 | var result struct { 70 | core.Error 71 | List []UserCumulateData `json:"list"` 72 | } 73 | 74 | incompleteURL := "https://api.weixin.qq.com/datacube/getusercumulate?access_token=" 75 | if err = clt.PostJSON(incompleteURL, req, &result); err != nil { 76 | return 77 | } 78 | 79 | if result.ErrCode != core.ErrCodeOK { 80 | err = &result.Error 81 | return 82 | } 83 | list = result.List 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /mp/dkf/README.md: -------------------------------------------------------------------------------- 1 | ## 多客服接口 2 | -------------------------------------------------------------------------------- /mp/dkf/account/README.md: -------------------------------------------------------------------------------- 1 | ## 客户账号管理 2 | -------------------------------------------------------------------------------- /mp/dkf/account/account.go: -------------------------------------------------------------------------------- 1 | // 客户账号管理 2 | package account 3 | 4 | import ( 5 | "crypto/md5" 6 | "encoding/hex" 7 | "errors" 8 | 9 | "github.com/chanxuehong/wechat/mp/core" 10 | ) 11 | 12 | // Add 添加客服账号. 13 | // 14 | // account: 完整客服账号,格式为:账号前缀@公众号微信号,账号前缀最多10个字符,必须是英文或者数字字符。 15 | // nickname: 客服昵称,最长6个汉字或12个英文字符 16 | // password: 客服账号登录密码 17 | // isPasswordPlain: 标识 password 是否为明文格式, true 表示是明文密码, false 表示是密文密码. 18 | func Add(clt *core.Client, account, nickname, password string, isPasswordPlain bool) (err error) { 19 | const incompleteURL = "https://api.weixin.qq.com/customservice/kfaccount/add?access_token=" 20 | 21 | if password == "" { 22 | return errors.New("empty password") 23 | } 24 | if isPasswordPlain { 25 | md5Sum := md5.Sum([]byte(password)) 26 | password = hex.EncodeToString(md5Sum[:]) 27 | } 28 | 29 | request := struct { 30 | Account string `json:"kf_account"` 31 | Nickname string `json:"nickname"` 32 | Password string `json:"password"` 33 | }{ 34 | Account: account, 35 | Nickname: nickname, 36 | Password: password, 37 | } 38 | var result core.Error 39 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 40 | return 41 | } 42 | if result.ErrCode != core.ErrCodeOK { 43 | err = &result 44 | return 45 | } 46 | return 47 | } 48 | 49 | // Update 设置客服信息(增量更新, 不更新的可以留空). 50 | // 51 | // account: 完整客服账号,格式为:账号前缀@公众号微信号 52 | // nickname: 客服昵称,最长6个汉字或12个英文字符 53 | // password: 客服账号登录密码 54 | // isPasswordPlain: 标识 password 是否为明文格式, true 表示是明文密码, false 表示是密文密码. 55 | func Update(clt *core.Client, account, nickname, password string, isPasswordPlain bool) (err error) { 56 | const incompleteURL = "https://api.weixin.qq.com/customservice/kfaccount/update?access_token=" 57 | 58 | if isPasswordPlain && password != "" { 59 | md5Sum := md5.Sum([]byte(password)) 60 | password = hex.EncodeToString(md5Sum[:]) 61 | } 62 | 63 | request := struct { 64 | Account string `json:"kf_account"` 65 | Nickname string `json:"nickname,omitempty"` 66 | Password string `json:"password,omitempty"` 67 | }{ 68 | Account: account, 69 | Nickname: nickname, 70 | Password: password, 71 | } 72 | var result core.Error 73 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 74 | return 75 | } 76 | if result.ErrCode != core.ErrCodeOK { 77 | err = &result 78 | return 79 | } 80 | return 81 | } 82 | 83 | // Delete 删除客服账号 84 | func Delete(clt *core.Client, kfAccount string) (err error) { 85 | // TODO 86 | // incompleteURL := "https://api.weixin.qq.com/customservice/kfaccount/del?kf_account=" + 87 | // url.QueryEscape(kfAccount) + "&access_token=" 88 | incompleteURL := "https://api.weixin.qq.com/customservice/kfaccount/del?kf_account=" + 89 | kfAccount + "&access_token=" 90 | 91 | var result core.Error 92 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 93 | return 94 | } 95 | if result.ErrCode != core.ErrCodeOK { 96 | err = &result 97 | return 98 | } 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /mp/dkf/account/head_img.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/chanxuehong/wechat/mp/core" 9 | ) 10 | 11 | // UploadHeadImage 上传客服头像. 12 | func UploadHeadImage(clt *core.Client, kfAccount, imageFilePath string) (err error) { 13 | file, err := os.Open(imageFilePath) 14 | if err != nil { 15 | return 16 | } 17 | defer file.Close() 18 | 19 | return UploadHeadImageFromReader(clt, kfAccount, filepath.Base(imageFilePath), file) 20 | } 21 | 22 | // UploadHeadImageFromReader 上传客服头像. 23 | // 24 | // NOTE: 参数 filename 不是文件路径, 是 multipart/form-data 里面 filename 的值. 25 | func UploadHeadImageFromReader(clt *core.Client, kfAccount, filename string, reader io.Reader) (err error) { 26 | // TODO 27 | // incompleteURL := "https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg?kf_account=" + 28 | // url.QueryEscape(kfAccount) + "&access_token=" 29 | incompleteURL := "https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg?kf_account=" + 30 | kfAccount + "&access_token=" 31 | 32 | var fields = []core.MultipartFormField{{ 33 | IsFile: true, 34 | Name: "media", 35 | FileName: filename, 36 | Value: reader, 37 | }} 38 | var result core.Error 39 | if err = clt.PostMultipartForm(incompleteURL, fields, &result); err != nil { 40 | return 41 | } 42 | if result.ErrCode != core.ErrCodeOK { 43 | err = &result 44 | return 45 | } 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /mp/dkf/doc.go: -------------------------------------------------------------------------------- 1 | // 多客服接口. 2 | package dkf 3 | -------------------------------------------------------------------------------- /mp/dkf/kf_list.go: -------------------------------------------------------------------------------- 1 | package dkf 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | // 客服基本信息 10 | type KfInfo struct { 11 | Id json.Number `json:"kf_id"` // 客服工号 12 | Account string `json:"kf_account"` // 完整客服账号,格式为:账号前缀@公众号微信号 13 | Nickname string `json:"kf_nick"` // 客服昵称 14 | HeadImageURL string `json:"kf_headimgurl"` // 客服头像 15 | } 16 | 17 | // KfList 获取客服基本信息. 18 | func KfList(clt *core.Client) (list []KfInfo, err error) { 19 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/customservice/getkflist?access_token=" 20 | 21 | var result struct { 22 | core.Error 23 | KfList []KfInfo `json:"kf_list"` 24 | } 25 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 26 | return 27 | } 28 | if result.ErrCode != core.ErrCodeOK { 29 | err = &result.Error 30 | return 31 | } 32 | list = result.KfList 33 | return 34 | } 35 | 36 | const ( 37 | OnlineKfInfoStatusPC = 1 38 | OnlineKfInfoStatusMobile = 2 39 | OnlineKfInfoStatusPCAndMobile = 3 40 | ) 41 | 42 | // 在线客服接待信息 43 | type OnlineKfInfo struct { 44 | Id json.Number `json:"kf_id"` // 客服工号 45 | Account string `json:"kf_account"` // 完整客服账号,格式为:账号前缀@公众号微信号 46 | Status int `json:"status"` // 客服在线状态 1:pc在线,2:手机在线。若pc和手机同时在线则为 1+2=3 47 | AutoAcceptNumber int `json:"auto_accept"` // 客服设置的最大自动接入数 48 | AcceptingNumber int `json:"accepted_case"` // 客服当前正在接待的会话数 49 | } 50 | 51 | // OnlineKfList 获取在线客服接待信息. 52 | func OnlineKfList(clt *core.Client) (list []OnlineKfInfo, err error) { 53 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist?access_token=" 54 | 55 | var result struct { 56 | core.Error 57 | OnlineKfInfoList []OnlineKfInfo `json:"kf_online_list"` 58 | } 59 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 60 | return 61 | } 62 | if result.ErrCode != core.ErrCodeOK { 63 | err = &result.Error 64 | return 65 | } 66 | list = result.OnlineKfInfoList 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /mp/dkf/record/README.md: -------------------------------------------------------------------------------- 1 | ## 客服聊天记录接口 2 | -------------------------------------------------------------------------------- /mp/dkf/record/iter.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // RecordIterator 8 | // 9 | // iter, err := NewRecordIterator(clt, request) 10 | // if err != nil { 11 | // // TODO: 增加你的代码 12 | // } 13 | // 14 | // for iter.HasNext() { 15 | // records, err := iter.NextPage() 16 | // if err != nil { 17 | // // TODO: 增加你的代码 18 | // } 19 | // // TODO: 增加你的代码 20 | // } 21 | type RecordIterator struct { 22 | clt *core.Client 23 | 24 | nextGetRequest *GetRequest 25 | 26 | lastGetRecords []Record 27 | nextPageCalled bool 28 | } 29 | 30 | func (iter *RecordIterator) HasNext() bool { 31 | if !iter.nextPageCalled { 32 | return len(iter.lastGetRecords) > 0 33 | } 34 | return len(iter.lastGetRecords) >= iter.nextGetRequest.PageSize 35 | } 36 | 37 | func (iter *RecordIterator) NextPage() (records []Record, err error) { 38 | if !iter.nextPageCalled { 39 | iter.nextPageCalled = true 40 | records = iter.lastGetRecords 41 | return 42 | } 43 | 44 | records, err = Get(iter.clt, iter.nextGetRequest) 45 | if err != nil { 46 | return 47 | } 48 | 49 | iter.lastGetRecords = records 50 | iter.nextGetRequest.PageIndex++ 51 | return 52 | } 53 | 54 | func NewRecordIterator(clt *core.Client, request *GetRequest) (iter *RecordIterator, err error) { 55 | // 逻辑上相当于第一次调用 RecordIterator.NextPage, 56 | // 因为第一次调用 RecordIterator.HasNext 需要数据支撑, 所以提前获取了数据 57 | records, err := Get(clt, request) 58 | if err != nil { 59 | return 60 | } 61 | 62 | request.PageIndex++ 63 | 64 | iter = &RecordIterator{ 65 | clt: clt, 66 | nextGetRequest: request, 67 | lastGetRecords: records, 68 | nextPageCalled: false, 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /mp/dkf/record/record.go: -------------------------------------------------------------------------------- 1 | // 客服聊天记录接口 2 | package record 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/chanxuehong/wechat/mp/core" 8 | ) 9 | 10 | type Record struct { 11 | Worker string `json:"worker"` // 客服账号 12 | OpenId string `json:"openid"` // 用户的标识, 对当前公众号唯一 13 | OperCode int `json:"opercode"` // 操作ID(会话状态) 14 | Timestamp int64 `json:"time"` // 操作时间, UNIX时间戳 15 | Text string `json:"text"` // 聊天记录 16 | } 17 | 18 | type GetRequest struct { 19 | StartTime int64 `json:"starttime"` // 查询开始时间, UNIX时间戳 20 | EndTime int64 `json:"endtime"` // 查询结束时间, UNIX时间戳, 每次查询不能跨日查询 21 | PageIndex int `json:"pageindex"` // 查询第几页, 从1开始 22 | PageSize int `json:"pagesize"` // 每页大小, 每页最多拉取50条 23 | OpenId string `json:"openid,omitempty"` // 普通用户的标识, 对当前公众号唯一 24 | } 25 | 26 | // Get 获取客服聊天记录 27 | func Get(clt *core.Client, request *GetRequest) (list []Record, err error) { 28 | const incompleteURL = "https://api.weixin.qq.com/customservice/msgrecord/getrecord?access_token=" 29 | 30 | if request.PageIndex < 1 { 31 | err = fmt.Errorf("Incorrect request.PageIndex: %d", request.PageIndex) 32 | return 33 | } 34 | if request.PageSize <= 0 { 35 | err = fmt.Errorf("Incorrect request.PageSize: %d", request.PageSize) 36 | return 37 | } 38 | 39 | var result struct { 40 | core.Error 41 | RecordList []Record `json:"recordlist"` 42 | } 43 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 44 | return 45 | } 46 | if result.ErrCode != core.ErrCodeOK { 47 | err = &result.Error 48 | return 49 | } 50 | list = result.RecordList 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /mp/dkf/resp.go: -------------------------------------------------------------------------------- 1 | package dkf 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | MsgTypeTransferCustomerService core.MsgType = "transfer_customer_service" // 将消息转发到多客服 9 | ) 10 | 11 | // 将消息转发到多客服消息 12 | type TransferToCustomerService struct { 13 | XMLName struct{} `xml:"xml" json:"-"` 14 | core.MsgHeader 15 | TransInfo *TransInfo `xml:"TransInfo,omitempty" json:"TransInfo,omitempty"` 16 | } 17 | 18 | type TransInfo struct { 19 | KfAccount string `xml:"KfAccount" json:"KfAccount"` 20 | } 21 | 22 | // 如果不指定客服则 kfAccount 留空. 23 | func NewTransferToCustomerService(to, from string, timestamp int64, kfAccount string) (msg *TransferToCustomerService) { 24 | msg = &TransferToCustomerService{ 25 | MsgHeader: core.MsgHeader{ 26 | ToUserName: to, 27 | FromUserName: from, 28 | CreateTime: timestamp, 29 | MsgType: MsgTypeTransferCustomerService, 30 | }, 31 | } 32 | if kfAccount != "" { 33 | msg.TransInfo = &TransInfo{ 34 | KfAccount: kfAccount, 35 | } 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /mp/dkf/session/README.md: -------------------------------------------------------------------------------- 1 | ## 多客服会话控制 2 | -------------------------------------------------------------------------------- /mp/dkf/session/event.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | EventTypeKfCreateSession core.EventType = "kf_create_session" // 接入会话 9 | EventTypeKfCloseSession core.EventType = "kf_close_session" // 关闭会话 10 | EventTypeKfSwitchSession core.EventType = "kf_switch_session" // 转接会话 11 | ) 12 | 13 | type KfCreateSessionEvent struct { 14 | XMLName struct{} `xml:"xml" json:"-"` 15 | core.MsgHeader 16 | EventType core.EventType `xml:"Event" json:"Event"` 17 | KfAccount string `xml:"KfAccount" json:"KfAccount"` 18 | } 19 | 20 | func GetKfCreateSessionEvent(msg *core.MixedMsg) *KfCreateSessionEvent { 21 | return &KfCreateSessionEvent{ 22 | MsgHeader: msg.MsgHeader, 23 | EventType: msg.EventType, 24 | KfAccount: msg.KfAccount, 25 | } 26 | } 27 | 28 | type KfCloseSessionEvent struct { 29 | XMLName struct{} `xml:"xml" json:"-"` 30 | core.MsgHeader 31 | EventType core.EventType `xml:"Event" json:"Event"` 32 | KfAccount string `xml:"KfAccount" json:"KfAccount"` 33 | } 34 | 35 | func GetKfCloseSessionEvent(msg *core.MixedMsg) *KfCloseSessionEvent { 36 | return &KfCloseSessionEvent{ 37 | MsgHeader: msg.MsgHeader, 38 | EventType: msg.EventType, 39 | KfAccount: msg.KfAccount, 40 | } 41 | } 42 | 43 | type KfSwitchSessionEvent struct { 44 | XMLName struct{} `xml:"xml" json:"-"` 45 | core.MsgHeader 46 | EventType core.EventType `xml:"Event" json:"Event"` 47 | FromKfAccount string `xml:"FromKfAccount" json:"FromKfAccount"` 48 | ToKfAccount string `xml:"ToKfAccount" json:"ToKfAccount"` 49 | } 50 | 51 | func GetKfSwitchSessionEvent(msg *core.MixedMsg) *KfSwitchSessionEvent { 52 | return &KfSwitchSessionEvent{ 53 | MsgHeader: msg.MsgHeader, 54 | EventType: msg.EventType, 55 | FromKfAccount: msg.FromKfAccount, 56 | ToKfAccount: msg.ToKfAccount, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /mp/doc.go: -------------------------------------------------------------------------------- 1 | // mp 是微信公众号 sdk 2 | package mp 3 | -------------------------------------------------------------------------------- /mp/jssdk/README.md: -------------------------------------------------------------------------------- 1 | ## 微信JS-SDK -------------------------------------------------------------------------------- /mp/jssdk/doc.go: -------------------------------------------------------------------------------- 1 | // 微信JS-SDK. 2 | package jssdk 3 | -------------------------------------------------------------------------------- /mp/jssdk/sign.go: -------------------------------------------------------------------------------- 1 | package jssdk 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // JS-SDK wx.config 的参数签名. 12 | func WXConfigSign(jsapiTicket, nonceStr, timestamp, url string) (signature string) { 13 | if i := strings.IndexByte(url, '#'); i >= 0 { 14 | url = url[:i] 15 | } 16 | 17 | n := len("jsapi_ticket=") + len(jsapiTicket) + 18 | len("&noncestr=") + len(nonceStr) + 19 | len("×tamp=") + len(timestamp) + 20 | len("&url=") + len(url) 21 | buf := make([]byte, 0, n) 22 | 23 | buf = append(buf, "jsapi_ticket="...) 24 | buf = append(buf, jsapiTicket...) 25 | buf = append(buf, "&noncestr="...) 26 | buf = append(buf, nonceStr...) 27 | buf = append(buf, "×tamp="...) 28 | buf = append(buf, timestamp...) 29 | buf = append(buf, "&url="...) 30 | buf = append(buf, url...) 31 | 32 | hashsum := sha1.Sum(buf) 33 | return hex.EncodeToString(hashsum[:]) 34 | } 35 | 36 | // JS-SDK 卡券 API 参数签名. 37 | func CardSign(strs []string) (signature string) { 38 | sort.Strings(strs) 39 | 40 | h := sha1.New() 41 | 42 | bufw := bufio.NewWriterSize(h, 128) // sha1.BlockSize 的整数倍 43 | for _, str := range strs { 44 | bufw.WriteString(str) 45 | } 46 | bufw.Flush() 47 | 48 | return hex.EncodeToString(h.Sum(nil)) 49 | } 50 | -------------------------------------------------------------------------------- /mp/material/README.md: -------------------------------------------------------------------------------- 1 | ## 永久素材管理 2 | 3 | * 临时素材管理在 media 模块 4 | * 对于图文消息里的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传 -------------------------------------------------------------------------------- /mp/material/doc.go: -------------------------------------------------------------------------------- 1 | // 永久素材管理. 2 | // 3 | // 临时素材管理在 media 模块; 4 | // 对于图文消息里的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传. 5 | package material 6 | -------------------------------------------------------------------------------- /mp/material/video.go: -------------------------------------------------------------------------------- 1 | package material 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Video struct { 8 | Title string `json:"title"` 9 | Description string `json:"description"` 10 | DownloadURL string `json:"down_url"` 11 | } 12 | 13 | // 获取视频消息素材信息. 14 | func GetVideo(clt *core.Client, mediaId string) (info *Video, err error) { 15 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=" 16 | 17 | var request = struct { 18 | MediaId string `json:"media_id"` 19 | }{ 20 | MediaId: mediaId, 21 | } 22 | var result struct { 23 | core.Error 24 | Video 25 | } 26 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 27 | return 28 | } 29 | if result.ErrCode != core.ErrCodeOK { 30 | err = &result.Error 31 | return 32 | } 33 | info = &result.Video 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /mp/media/README.md: -------------------------------------------------------------------------------- 1 | ## 临时素材管理 2 | 3 | * 永久素材管理在 material 模块 4 | * 对于图文消息里的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传 -------------------------------------------------------------------------------- /mp/media/doc.go: -------------------------------------------------------------------------------- 1 | // 临时素材管理. 2 | // 3 | // 永久素材管理在 material 模块; 4 | // 对于图文消息里的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传. 5 | package media 6 | -------------------------------------------------------------------------------- /mp/media/download.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "mime" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | 11 | "github.com/chanxuehong/wechat/internal/debug/api" 12 | "github.com/chanxuehong/wechat/internal/debug/api/retry" 13 | "github.com/chanxuehong/wechat/mp/core" 14 | "github.com/chanxuehong/wechat/util" 15 | ) 16 | 17 | // Download 下载多媒体到文件. 18 | // 19 | // 请注意, 视频文件不支持下载 20 | func Download(clt *core.Client, mediaId, filepath string) (written int64, err error) { 21 | file, err := os.Create(filepath) 22 | if err != nil { 23 | return 24 | } 25 | defer func() { 26 | file.Close() 27 | if err != nil { 28 | os.Remove(filepath) 29 | } 30 | }() 31 | 32 | return DownloadToWriter(clt, mediaId, file) 33 | } 34 | 35 | // DownloadToWriter 下载多媒体到 io.Writer. 36 | // 37 | // 请注意, 视频文件不支持下载 38 | func DownloadToWriter(clt *core.Client, mediaId string, writer io.Writer) (written int64, err error) { 39 | httpClient := clt.HttpClient 40 | if httpClient == nil { 41 | httpClient = util.DefaultMediaHttpClient 42 | } 43 | 44 | var incompleteURL = "https://api.weixin.qq.com/cgi-bin/media/get?media_id=" + url.QueryEscape(mediaId) + "&access_token=" 45 | var errorResult core.Error 46 | 47 | token, err := clt.Token() 48 | if err != nil { 49 | return 50 | } 51 | 52 | hasRetried := false 53 | RETRY: 54 | finalURL := incompleteURL + url.QueryEscape(token) 55 | written, err = httpDownloadToWriter(httpClient, finalURL, writer, &errorResult) 56 | if err != nil { 57 | return 58 | } 59 | if written > 0 { 60 | return 61 | } 62 | 63 | switch errorResult.ErrCode { 64 | case core.ErrCodeOK: 65 | return // 基本不会出现 66 | case core.ErrCodeInvalidCredential, core.ErrCodeAccessTokenExpired: 67 | retry.DebugPrintError(errorResult.ErrCode, errorResult.ErrMsg, token) 68 | if !hasRetried { 69 | hasRetried = true 70 | errorResult = core.Error{} 71 | if token, err = clt.RefreshToken(token); err != nil { 72 | return 73 | } 74 | retry.DebugPrintNewToken(token) 75 | goto RETRY 76 | } 77 | retry.DebugPrintFallthrough(token) 78 | fallthrough 79 | default: 80 | err = &errorResult 81 | return 82 | } 83 | } 84 | 85 | func httpDownloadToWriter(clt *http.Client, url string, writer io.Writer, errorResult *core.Error) (written int64, err error) { 86 | api.DebugPrintGetRequest(url) 87 | httpResp, err := clt.Get(url) 88 | if err != nil { 89 | return 0, err 90 | } 91 | defer httpResp.Body.Close() 92 | 93 | if httpResp.StatusCode != http.StatusOK { 94 | return 0, fmt.Errorf("http.Status: %s", httpResp.Status) 95 | } 96 | 97 | ContentDisposition := httpResp.Header.Get("Content-Disposition") 98 | ContentType, _, _ := mime.ParseMediaType(httpResp.Header.Get("Content-Type")) 99 | if ContentDisposition != "" && ContentType != "text/plain" && ContentType != "application/json" { 100 | // 返回的是媒体流 101 | return io.Copy(writer, httpResp.Body) 102 | } else { 103 | // 返回的是错误信息 104 | return 0, api.DecodeJSONHttpResponse(httpResp.Body, errorResult) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /mp/media/mpvideo.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // UploadVideo2 创建视频素材, 返回的素材一般用于群发消息. 8 | // 9 | // mediaId: 通过 UploadVideo 上传视频文件得到 10 | // title: 标题, 可以为空 11 | // description: 描述, 可以为空 12 | func UploadVideo2(clt *core.Client, mediaId, title, description string) (info *MediaInfo, err error) { 13 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/media/uploadvideo?access_token=" 14 | 15 | var request = struct { 16 | MediaId string `json:"media_id"` 17 | Title string `json:"title,omitempty"` 18 | Description string `json:"description,omitempty"` 19 | }{ 20 | MediaId: mediaId, 21 | Title: title, 22 | Description: description, 23 | } 24 | var result struct { 25 | core.Error 26 | MediaInfo 27 | } 28 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 29 | return 30 | } 31 | if result.ErrCode != core.ErrCodeOK { 32 | err = &result.Error 33 | return 34 | } 35 | info = &result.MediaInfo 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /mp/media/news.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Article struct { 8 | ThumbMediaId string `json:"thumb_media_id"` // 必须; 图文消息缩略图的 media_id, 可以在上传多媒体文件接口中获得 9 | Title string `json:"title"` // 必须; 图文消息的标题 10 | Author string `json:"author,omitempty"` // 可选; 图文消息的作者 11 | Digest string `json:"digest,omitempty"` // 可选; 图文消息的摘要 12 | Content string `json:"content"` // 必须; 图文消息页面的内容, 支持HTML标签 13 | ContentSourceURL string `json:"content_source_url,omitempty"` // 可选; 在图文消息页面点击"阅读原文"后的页面 14 | ShowCoverPic int `json:"show_cover_pic"` // 可选; 是否显示封面, 1为显示, 0为不显示, 默认为不显示 15 | } 16 | 17 | type News struct { 18 | Articles []Article `json:"articles,omitempty"` 19 | } 20 | 21 | // UploadNews 创建图文消息素材, 返回的素材一般用于群发消息. 22 | func UploadNews(clt *core.Client, news *News) (info *MediaInfo, err error) { 23 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=" 24 | 25 | var result struct { 26 | core.Error 27 | MediaInfo 28 | } 29 | if err = clt.PostJSON(incompleteURL, news, &result); err != nil { 30 | return 31 | } 32 | if result.ErrCode != core.ErrCodeOK { 33 | err = &result.Error 34 | return 35 | } 36 | info = &result.MediaInfo 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /mp/menu/README.md: -------------------------------------------------------------------------------- 1 | ## 自定义菜单接口 -------------------------------------------------------------------------------- /mp/menu/api.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 创建自定义菜单. 8 | func Create(clt *core.Client, menu *Menu) (err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" 10 | 11 | var result core.Error 12 | if err = clt.PostJSON(incompleteURL, menu, &result); err != nil { 13 | return 14 | } 15 | if result.ErrCode != core.ErrCodeOK { 16 | err = &result 17 | return 18 | } 19 | return 20 | } 21 | 22 | // 查询自定义菜单. 23 | func Get(clt *core.Client) (menu *Menu, conditionalMenus []Menu, err error) { 24 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=" 25 | 26 | var result struct { 27 | core.Error 28 | Menu Menu `json:"menu"` 29 | ConditionalMenus []Menu `json:"conditionalmenu"` 30 | } 31 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 32 | return 33 | } 34 | if result.ErrCode != core.ErrCodeOK { 35 | err = &result.Error 36 | return 37 | } 38 | menu = &result.Menu 39 | conditionalMenus = result.ConditionalMenus 40 | return 41 | } 42 | 43 | // 删除自定义菜单. 44 | func Delete(clt *core.Client) (err error) { 45 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=" 46 | 47 | var result core.Error 48 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 49 | return 50 | } 51 | if result.ErrCode != core.ErrCodeOK { 52 | err = &result 53 | return 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /mp/menu/api_conditional.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 创建个性化菜单. 8 | func AddConditionalMenu(clt *core.Client, menu *Menu) (menuId int64, err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=" 10 | 11 | var result struct { 12 | core.Error 13 | MenuId int64 `json:"menuId"` 14 | } 15 | if err = clt.PostJSON(incompleteURL, menu, &result); err != nil { 16 | return 17 | } 18 | if result.ErrCode != core.ErrCodeOK { 19 | err = &result.Error 20 | return 21 | } 22 | menuId = result.MenuId 23 | return 24 | } 25 | 26 | // 删除个性化菜单. 27 | func DeleteConditionalMenu(clt *core.Client, menuId int64) (err error) { 28 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=" 29 | 30 | var request = struct { 31 | MenuId int64 `json:"menuid"` 32 | }{ 33 | MenuId: menuId, 34 | } 35 | var result core.Error 36 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 37 | return 38 | } 39 | if result.ErrCode != core.ErrCodeOK { 40 | err = &result 41 | return 42 | } 43 | return 44 | } 45 | 46 | // 测试个性化菜单匹配结果. 47 | // 48 | // userId 可以是粉丝的 OpenID, 也可以是粉丝的微信号 49 | func TryMatch(clt *core.Client, userId string) (menu *Menu, err error) { 50 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/menu/trymatch?access_token=" 51 | 52 | var request = struct { 53 | UserId string `json:"user_id"` 54 | }{ 55 | UserId: userId, 56 | } 57 | var result struct { 58 | core.Error 59 | Menu `json:"menu"` 60 | } 61 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 62 | return 63 | } 64 | if result.ErrCode != core.ErrCodeOK { 65 | err = &result.Error 66 | return 67 | } 68 | menu = &result.Menu 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /mp/menu/doc.go: -------------------------------------------------------------------------------- 1 | // 自定义菜单接口. 2 | package menu 3 | -------------------------------------------------------------------------------- /mp/menu/menu_info.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 获取自定义菜单配置接口. 8 | func GetMenuInfo(clt *core.Client) (info MenuInfo, isMenuOpen bool, err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=" 10 | 11 | var result struct { 12 | core.Error 13 | IsMenuOpen int `json:"is_menu_open"` 14 | MenuInfo MenuInfo `json:"selfmenu_info"` 15 | } 16 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 17 | return 18 | } 19 | if result.ErrCode != core.ErrCodeOK { 20 | err = &result.Error 21 | return 22 | } 23 | info = result.MenuInfo 24 | if result.IsMenuOpen != 0 { 25 | isMenuOpen = true 26 | } 27 | return 28 | } 29 | 30 | type MenuInfo struct { 31 | Buttons []ButtonEx `json:"button,omitempty"` 32 | } 33 | 34 | type ButtonEx struct { 35 | Type string `json:"type,omitempty"` 36 | Name string `json:"name,omitempty"` 37 | Key string `json:"key,omitempty"` 38 | URL string `json:"url,omitempty"` 39 | MediaId string `json:"media_id,omitempty"` 40 | 41 | Value string `json:"value,omitempty"` 42 | NewsInfo struct { 43 | Articles []Article `json:"list,omitempty"` 44 | } `json:"news_info"` 45 | 46 | SubButton struct { 47 | Buttons []ButtonEx `json:"list,omitempty"` 48 | } `json:"sub_button"` 49 | } 50 | 51 | type Article struct { 52 | Title string `json:"title,omitempty"` // 图文消息的标题 53 | Author string `json:"author,omitempty"` // 作者 54 | Digest string `json:"digest,omitempty"` // 摘要 55 | ShowCover int `json:"show_cover"` // 是否显示封面, 0为不显示, 1为显示 56 | CoverURL string `json:"cover_url,omitempty"` // 封面图片的URL 57 | ContentURL string `json:"content_url,omitempty"` // 正文的URL 58 | SourceURL string `json:"source_url,omitempty"` // 原文的URL, 若置空则无查看原文入口 59 | } 60 | -------------------------------------------------------------------------------- /mp/message/callback/doc.go: -------------------------------------------------------------------------------- 1 | // callback 微信回调消息 2 | package callback 3 | -------------------------------------------------------------------------------- /mp/message/callback/request/doc.go: -------------------------------------------------------------------------------- 1 | // 被动接收的基本消息(事件)的数据结构定义, 更多的消息(事件)定义在对应的业务模块内. 2 | package request 3 | -------------------------------------------------------------------------------- /mp/message/custom/custom.go: -------------------------------------------------------------------------------- 1 | // 客服消息. 2 | package custom 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | // Send 发送消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构. 9 | func Send(clt *core.Client, msg interface{}) (err error) { 10 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" 11 | 12 | var result core.Error 13 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 14 | return 15 | } 16 | if result.ErrCode != core.ErrCodeOK { 17 | err = &result 18 | return 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /mp/message/doc.go: -------------------------------------------------------------------------------- 1 | // message 微信公众号消息接口 2 | package message 3 | -------------------------------------------------------------------------------- /mp/message/mass/doc.go: -------------------------------------------------------------------------------- 1 | // 群发消息. 2 | package mass 3 | -------------------------------------------------------------------------------- /mp/message/mass/event.go: -------------------------------------------------------------------------------- 1 | package mass 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | EventTypeMassSendJobFinish core.EventType = "MASSSENDJOBFINISH" 9 | ) 10 | 11 | // 事件推送群发结果 12 | type MassSendJobFinishEvent struct { 13 | XMLName struct{} `xml:"xml" json:"-"` 14 | core.MsgHeader 15 | EventType core.EventType `xml:"Event" json:"Event"` // 事件信息, 此处为 MASSSENDJOBFINISH 16 | MsgId int64 `xml:"MsgId" json:"MsgId"` // 群发的消息ID, 64位整型 17 | 18 | // 群发的结构, 为 "send success" 或 "send fail" 或 "err(num)". 19 | // 但 send success 时, 也有可能因用户拒收公众号的消息, 系统错误等原因造成少量用户接收失败. 20 | // err(num) 是审核失败的具体原因, 可能的情况如下: 21 | // err(10001), //涉嫌广告 22 | // err(20001), //涉嫌政治 23 | // err(20004), //涉嫌社会 24 | // err(20002), //涉嫌色情 25 | // err(20006), //涉嫌违法犯罪 26 | // err(20008), //涉嫌欺诈 27 | // err(20013), //涉嫌版权 28 | // err(22000), //涉嫌互推(互相宣传) 29 | // err(21000), //涉嫌其他 30 | Status string `xml:"Status" json:"Status"` 31 | TotalCount int `xml:"TotalCount" json:"TotalCount"` // group_id 下粉丝数, 或者 openid_list 中的粉丝数 32 | // 过滤(过滤是指特定地区, 性别的过滤, 用户设置拒收的过滤; 用户接收已超4条的过滤)后, 33 | // 准备发送的粉丝数, 原则上, FilterCount = SentCount + ErrorCount 34 | FilterCount int `xml:"FilterCount" json:"FilterCount"` 35 | SentCount int `xml:"SentCount" json:"SentCount"` // 发送成功的粉丝数 36 | ErrorCount int `xml:"ErrorCount" json:"ErrorCount"` // 发送失败的粉丝数 37 | } 38 | 39 | func GetMassSendJobFinishEvent(msg *core.MixedMsg) *MassSendJobFinishEvent { 40 | return &MassSendJobFinishEvent{ 41 | MsgHeader: msg.MsgHeader, 42 | EventType: msg.EventType, 43 | MsgId: msg.MsgID, // NOTE 44 | Status: msg.Status, 45 | TotalCount: msg.TotalCount, 46 | FilterCount: msg.FilterCount, 47 | SentCount: msg.SentCount, 48 | ErrorCount: msg.ErrorCount, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mp/message/mass/mass.go: -------------------------------------------------------------------------------- 1 | package mass 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 群发结果 8 | type Result struct { 9 | MsgId int64 `json:"msg_id"` // 消息发送任务的ID 10 | 11 | // 消息的数据ID,该字段只有在群发图文消息时,才会出现。可以用于在图文分析数据接口中,获取到对应的图文消息的数据, 12 | // 是图文分析数据接口中的msgid字段中的前半部分,详见图文分析数据接口中的msgid字段的介绍。 13 | MsgDataId int64 `json:"msg_data_id"` 14 | } 15 | 16 | // Delete 删除群发. 17 | func Delete(clt *core.Client, msgid int64) (err error) { 18 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/delete?access_token=" 19 | 20 | var request = struct { 21 | MsgId int64 `json:"msg_id"` 22 | }{ 23 | MsgId: msgid, 24 | } 25 | var result core.Error 26 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 27 | return 28 | } 29 | if result.ErrCode != core.ErrCodeOK { 30 | err = &result 31 | return 32 | } 33 | return 34 | } 35 | 36 | type Status struct { 37 | MsgId int64 `json:"msg_id"` 38 | Status string `json:"msg_status"` // 消息发送后的状态, SEND_SUCCESS表示发送成功 39 | } 40 | 41 | // GetStatus 查询群发消息发送状态. 42 | func GetStatus(clt *core.Client, msgid int64) (status *Status, err error) { 43 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/get?access_token=" 44 | 45 | var request = struct { 46 | MsgId int64 `json:"msg_id"` 47 | }{ 48 | MsgId: msgid, 49 | } 50 | var result struct { 51 | core.Error 52 | Status 53 | } 54 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 55 | return 56 | } 57 | if result.ErrCode != core.ErrCodeOK { 58 | err = &result.Error 59 | return 60 | } 61 | status = &result.Status 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /mp/message/mass/mass2all/mass2all.go: -------------------------------------------------------------------------------- 1 | // 群发消息给所有用户. 2 | package mass2all 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | "github.com/chanxuehong/wechat/mp/message/mass" 7 | ) 8 | 9 | // Send 发送消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构. 10 | func Send(clt *core.Client, msg interface{}) (rslt *mass.Result, err error) { 11 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=" 12 | 13 | var result struct { 14 | core.Error 15 | mass.Result 16 | } 17 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 18 | return 19 | } 20 | if result.ErrCode != core.ErrCodeOK { 21 | err = &result.Error 22 | return 23 | } 24 | rslt = &result.Result 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/message/mass/mass2all/msg.go: -------------------------------------------------------------------------------- 1 | package mass2all 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | MsgTypeText core.MsgType = "text" 9 | MsgTypeImage core.MsgType = "image" 10 | MsgTypeVoice core.MsgType = "voice" 11 | MsgTypeVideo core.MsgType = "mpvideo" 12 | MsgTypeNews core.MsgType = "mpnews" 13 | MsgTypeWxCard core.MsgType = "wxcard" 14 | ) 15 | 16 | type MsgHeader struct { 17 | Filter struct { 18 | IsToAll bool `json:"is_to_all"` 19 | } `json:"filter"` 20 | MsgType core.MsgType `json:"msgtype"` 21 | } 22 | 23 | type Text struct { 24 | MsgHeader 25 | Text struct { 26 | Content string `json:"content"` 27 | } `json:"text"` 28 | } 29 | 30 | func NewText(content string) *Text { 31 | var msg Text 32 | msg.MsgType = MsgTypeText 33 | msg.Filter.IsToAll = true 34 | msg.Text.Content = content 35 | return &msg 36 | } 37 | 38 | type Image struct { 39 | MsgHeader 40 | Image struct { 41 | MediaId string `json:"media_id"` 42 | } `json:"image"` 43 | } 44 | 45 | func NewImage(mediaId string) *Image { 46 | var msg Image 47 | msg.MsgType = MsgTypeImage 48 | msg.Filter.IsToAll = true 49 | msg.Image.MediaId = mediaId 50 | return &msg 51 | } 52 | 53 | type Voice struct { 54 | MsgHeader 55 | Voice struct { 56 | MediaId string `json:"media_id"` 57 | } `json:"voice"` 58 | } 59 | 60 | func NewVoice(mediaId string) *Voice { 61 | var msg Voice 62 | msg.MsgType = MsgTypeVoice 63 | msg.Filter.IsToAll = true 64 | msg.Voice.MediaId = mediaId 65 | return &msg 66 | } 67 | 68 | type Video struct { 69 | MsgHeader 70 | Video struct { 71 | MediaId string `json:"media_id"` 72 | } `json:"mpvideo"` 73 | } 74 | 75 | // 新建视频消息 76 | // 77 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadVideo2 得到 78 | func NewVideo(mediaId string) *Video { 79 | var msg Video 80 | msg.MsgType = MsgTypeVideo 81 | msg.Filter.IsToAll = true 82 | msg.Video.MediaId = mediaId 83 | return &msg 84 | } 85 | 86 | // 图文消息 87 | type News struct { 88 | MsgHeader 89 | News struct { 90 | MediaId string `json:"media_id"` 91 | } `json:"mpnews"` 92 | } 93 | 94 | // 新建图文消息 95 | // 96 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadNews 得到 97 | func NewNews(mediaId string) *News { 98 | var msg News 99 | msg.MsgType = MsgTypeNews 100 | msg.Filter.IsToAll = true 101 | msg.News.MediaId = mediaId 102 | return &msg 103 | } 104 | 105 | // 卡券消息 106 | type WxCard struct { 107 | MsgHeader 108 | WxCard struct { 109 | CardId string `json:"card_id"` 110 | CardExt string `json:"card_ext,omitempty"` 111 | } `json:"wxcard"` 112 | } 113 | 114 | // 新建卡券, 特别注意: 目前该接口仅支持填入非自定义code的卡券和预存模式的自定义code卡券. 115 | // 116 | // cardExt 可以为空 117 | func NewWxCard(cardId, cardExt string) *WxCard { 118 | var msg WxCard 119 | msg.MsgType = MsgTypeWxCard 120 | msg.Filter.IsToAll = true 121 | msg.WxCard.CardId = cardId 122 | msg.WxCard.CardExt = cardExt 123 | return &msg 124 | } 125 | -------------------------------------------------------------------------------- /mp/message/mass/mass2group/mass2group.go: -------------------------------------------------------------------------------- 1 | // 群发消息给特定分组用户. 2 | package mass2group 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | "github.com/chanxuehong/wechat/mp/message/mass" 7 | ) 8 | 9 | // Send 发送消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构. 10 | func Send(clt *core.Client, msg interface{}) (rslt *mass.Result, err error) { 11 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=" 12 | 13 | var result struct { 14 | core.Error 15 | mass.Result 16 | } 17 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 18 | return 19 | } 20 | if result.ErrCode != core.ErrCodeOK { 21 | err = &result.Error 22 | return 23 | } 24 | rslt = &result.Result 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/message/mass/mass2group/msg.go: -------------------------------------------------------------------------------- 1 | package mass2group 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | MsgTypeText core.MsgType = "text" 9 | MsgTypeImage core.MsgType = "image" 10 | MsgTypeVoice core.MsgType = "voice" 11 | MsgTypeVideo core.MsgType = "mpvideo" 12 | MsgTypeNews core.MsgType = "mpnews" 13 | MsgTypeWxCard core.MsgType = "wxcard" 14 | ) 15 | 16 | type MsgHeader struct { 17 | Filter struct { 18 | GroupId int64 `json:"group_id"` 19 | } `json:"filter"` 20 | MsgType core.MsgType `json:"msgtype"` 21 | } 22 | 23 | type Text struct { 24 | MsgHeader 25 | Text struct { 26 | Content string `json:"content"` 27 | } `json:"text"` 28 | } 29 | 30 | func NewText(groupId int64, content string) *Text { 31 | var msg Text 32 | msg.MsgType = MsgTypeText 33 | msg.Filter.GroupId = groupId 34 | msg.Text.Content = content 35 | return &msg 36 | } 37 | 38 | type Image struct { 39 | MsgHeader 40 | Image struct { 41 | MediaId string `json:"media_id"` 42 | } `json:"image"` 43 | } 44 | 45 | func NewImage(groupId int64, mediaId string) *Image { 46 | var msg Image 47 | msg.MsgType = MsgTypeImage 48 | msg.Filter.GroupId = groupId 49 | msg.Image.MediaId = mediaId 50 | return &msg 51 | } 52 | 53 | type Voice struct { 54 | MsgHeader 55 | Voice struct { 56 | MediaId string `json:"media_id"` 57 | } `json:"voice"` 58 | } 59 | 60 | func NewVoice(groupId int64, mediaId string) *Voice { 61 | var msg Voice 62 | msg.MsgType = MsgTypeVoice 63 | msg.Filter.GroupId = groupId 64 | msg.Voice.MediaId = mediaId 65 | return &msg 66 | } 67 | 68 | type Video struct { 69 | MsgHeader 70 | Video struct { 71 | MediaId string `json:"media_id"` 72 | } `json:"mpvideo"` 73 | } 74 | 75 | // 新建视频消息 76 | // 77 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadVideo2 得到 78 | func NewVideo(groupId int64, mediaId string) *Video { 79 | var msg Video 80 | msg.MsgType = MsgTypeVideo 81 | msg.Filter.GroupId = groupId 82 | msg.Video.MediaId = mediaId 83 | return &msg 84 | } 85 | 86 | // 图文消息 87 | type News struct { 88 | MsgHeader 89 | News struct { 90 | MediaId string `json:"media_id"` 91 | } `json:"mpnews"` 92 | } 93 | 94 | // 新建图文消息 95 | // 96 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadNews 得到 97 | func NewNews(groupId int64, mediaId string) *News { 98 | var msg News 99 | msg.MsgType = MsgTypeNews 100 | msg.Filter.GroupId = groupId 101 | msg.News.MediaId = mediaId 102 | return &msg 103 | } 104 | 105 | // 卡券消息 106 | type WxCard struct { 107 | MsgHeader 108 | WxCard struct { 109 | CardId string `json:"card_id"` 110 | CardExt string `json:"card_ext,omitempty"` 111 | } `json:"wxcard"` 112 | } 113 | 114 | // 新建卡券, 特别注意: 目前该接口仅支持填入非自定义code的卡券和预存模式的自定义code卡券. 115 | // 116 | // cardExt 可以为空 117 | func NewWxCard(groupId int64, cardId, cardExt string) *WxCard { 118 | var msg WxCard 119 | msg.MsgType = MsgTypeWxCard 120 | msg.Filter.GroupId = groupId 121 | msg.WxCard.CardId = cardId 122 | msg.WxCard.CardExt = cardExt 123 | return &msg 124 | } 125 | -------------------------------------------------------------------------------- /mp/message/mass/mass2users/mass2users.go: -------------------------------------------------------------------------------- 1 | // 根据OpenID列表群发. 2 | package mass2users 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | "github.com/chanxuehong/wechat/mp/message/mass" 7 | ) 8 | 9 | // Send 发送消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构. 10 | func Send(clt *core.Client, msg interface{}) (rslt *mass.Result, err error) { 11 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=" 12 | 13 | var result struct { 14 | core.Error 15 | mass.Result 16 | } 17 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 18 | return 19 | } 20 | if result.ErrCode != core.ErrCodeOK { 21 | err = &result.Error 22 | return 23 | } 24 | rslt = &result.Result 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /mp/message/mass/mass2users/msg.go: -------------------------------------------------------------------------------- 1 | package mass2users 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | MsgTypeText core.MsgType = "text" 9 | MsgTypeImage core.MsgType = "image" 10 | MsgTypeVoice core.MsgType = "voice" 11 | MsgTypeVideo core.MsgType = "mpvideo" 12 | MsgTypeNews core.MsgType = "mpnews" 13 | MsgTypeWxCard core.MsgType = "wxcard" 14 | ) 15 | 16 | type MsgHeader struct { 17 | ToUser []string `json:"touser,omitempty"` 18 | MsgType core.MsgType `json:"msgtype"` 19 | } 20 | 21 | type Text struct { 22 | MsgHeader 23 | Text struct { 24 | Content string `json:"content"` 25 | } `json:"text"` 26 | } 27 | 28 | func NewText(toUser []string, content string) *Text { 29 | var msg Text 30 | msg.MsgType = MsgTypeText 31 | msg.ToUser = toUser 32 | msg.Text.Content = content 33 | return &msg 34 | } 35 | 36 | type Image struct { 37 | MsgHeader 38 | Image struct { 39 | MediaId string `json:"media_id"` 40 | } `json:"image"` 41 | } 42 | 43 | func NewImage(toUser []string, mediaId string) *Image { 44 | var msg Image 45 | msg.MsgType = MsgTypeImage 46 | msg.ToUser = toUser 47 | msg.Image.MediaId = mediaId 48 | return &msg 49 | } 50 | 51 | type Voice struct { 52 | MsgHeader 53 | Voice struct { 54 | MediaId string `json:"media_id"` // mediaId 通过上传多媒体文件得到 55 | } `json:"voice"` 56 | } 57 | 58 | func NewVoice(toUser []string, mediaId string) *Voice { 59 | var msg Voice 60 | msg.MsgType = MsgTypeVoice 61 | msg.ToUser = toUser 62 | msg.Voice.MediaId = mediaId 63 | return &msg 64 | } 65 | 66 | type Video struct { 67 | MsgHeader 68 | Video struct { 69 | MediaId string `json:"media_id"` 70 | } `json:"mpvideo"` 71 | } 72 | 73 | // 新建视频消息. 74 | // 75 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadVideo2 得到 76 | func NewVideo(toUser []string, mediaId string) *Video { 77 | var msg Video 78 | msg.MsgType = MsgTypeVideo 79 | msg.ToUser = toUser 80 | msg.Video.MediaId = mediaId 81 | return &msg 82 | } 83 | 84 | // 图文消息 85 | type News struct { 86 | MsgHeader 87 | News struct { 88 | MediaId string `json:"media_id"` 89 | } `json:"mpnews"` 90 | } 91 | 92 | // 新建图文消息. 93 | // 94 | // NOTE: 对于临时素材, mediaId 应该通过 media.UploadNews 得到 95 | func NewNews(toUser []string, mediaId string) *News { 96 | var msg News 97 | msg.MsgType = MsgTypeNews 98 | msg.ToUser = toUser 99 | msg.News.MediaId = mediaId 100 | return &msg 101 | } 102 | 103 | // 卡券消息 104 | type WxCard struct { 105 | MsgHeader 106 | WxCard struct { 107 | CardId string `json:"card_id"` 108 | CardExt string `json:"card_ext,omitempty"` 109 | } `json:"wxcard"` 110 | } 111 | 112 | // 新建卡券, 特别注意: 目前该接口仅支持填入非自定义code的卡券和预存模式的自定义code卡券. 113 | // 114 | // cardExt 可以为空 115 | func NewWxCard(toUser []string, cardId, cardExt string) *WxCard { 116 | var msg WxCard 117 | msg.MsgType = MsgTypeWxCard 118 | msg.ToUser = toUser 119 | msg.WxCard.CardId = cardId 120 | msg.WxCard.CardExt = cardExt 121 | return &msg 122 | } 123 | -------------------------------------------------------------------------------- /mp/message/mass/preview/preview.go: -------------------------------------------------------------------------------- 1 | // 预览消息. 2 | package preview 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | // Send 发送消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构. 9 | func Send(clt *core.Client, msg interface{}) (err error) { 10 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=" 11 | 12 | var result core.Error 13 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 14 | return 15 | } 16 | if result.ErrCode != core.ErrCodeOK { 17 | err = &result 18 | return 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /mp/message/template/doc.go: -------------------------------------------------------------------------------- 1 | // 模板消息接口. 2 | package template 3 | -------------------------------------------------------------------------------- /mp/message/template/event.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | EventTypeTemplateSendJobFinish core.EventType = "TEMPLATESENDJOBFINISH" 9 | ) 10 | 11 | const ( 12 | TemplateSendStatusSuccess = "success" // 送达成功时 13 | TemplateSendStatusFailedUserBlock = "failed:user block" // 送达由于用户拒收(用户设置拒绝接收公众号消息)而失败 14 | TemplateSendStatusFailedSystemFailed = "failed: system failed" // 送达由于其他原因失败 15 | ) 16 | 17 | type TemplateSendJobFinishEvent struct { 18 | XMLName struct{} `xml:"xml" json:"-"` 19 | core.MsgHeader 20 | EventType core.EventType `xml:"Event" json:"Event"` // 此处为 TEMPLATESENDJOBFINISH 21 | MsgId int64 `xml:"MsgId" json:"MsgId"` // 模板消息ID 22 | Status string `xml:"Status" json:"Status"` // 发送状态 23 | } 24 | 25 | func GetTemplateSendJobFinishEvent(msg *core.MixedMsg) *TemplateSendJobFinishEvent { 26 | return &TemplateSendJobFinishEvent{ 27 | MsgHeader: msg.MsgHeader, 28 | EventType: msg.EventType, 29 | MsgId: msg.MsgID, // NOTE 30 | Status: msg.Status, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mp/message/template/send.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | type TemplateMessage struct { 10 | ToUser string `json:"touser"` // 必须, 接受者OpenID 11 | TemplateId string `json:"template_id"` // 必须, 模版ID 12 | URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 13 | MiniProgram *MiniProgram `json:"miniprogram,omitempty"` // 可选, 跳小程序所需数据,不需跳小程序可不用传该数据 14 | Data json.RawMessage `json:"data"` // 必须, 模板数据, JSON 格式的 []byte, 满足特定的模板需求 15 | } 16 | 17 | type TemplateMessage2 struct { 18 | ToUser string `json:"touser"` // 必须, 接受者OpenID 19 | TemplateId string `json:"template_id"` // 必须, 模版ID 20 | URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 21 | MiniProgram *MiniProgram `json:"miniprogram,omitempty"` // 可选, 跳小程序所需数据,不需跳小程序可不用传该数据 22 | Data interface{} `json:"data"` // 必须, 模板数据, struct 或者 *struct, encoding/json.Marshal 后满足格式要求. 23 | } 24 | 25 | type MiniProgram struct { 26 | AppId string `json:"appid"` // 必选; 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) 27 | PagePath string `json:"pagepath"` // 必选; 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) 28 | } 29 | 30 | // 模版内某个 .DATA 的值 31 | type DataItem struct { 32 | Value string `json:"value"` 33 | Color string `json:"color,omitempty"` 34 | } 35 | 36 | // 发送模板消息, msg 是经过 encoding/json.Marshal 得到的结果符合微信消息格式的任何数据结构, 一般为 *TemplateMessage 类型. 37 | func Send(clt *core.Client, msg interface{}) (msgid int64, err error) { 38 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" 39 | 40 | var result struct { 41 | core.Error 42 | MsgId int64 `json:"msgid"` 43 | } 44 | if err = clt.PostJSON(incompleteURL, msg, &result); err != nil { 45 | return 46 | } 47 | if result.ErrCode != core.ErrCodeOK { 48 | err = &result.Error 49 | return 50 | } 51 | msgid = result.MsgId 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /mp/oauth2/api_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | wxAppId = "" // 填上自己的参数 10 | wxAppSecret = "" // 填上自己的参数 11 | //oauth2RedirectURI = "http://192.168.1.129:8080/page2" // 填上自己的参数 12 | //oauth2Scope = "snsapi_userinfo" // 填上自己的参数 13 | ) 14 | 15 | var ( 16 | oauth2Endpoint *Endpoint = NewEndpoint(wxAppId, wxAppSecret) 17 | ) 18 | 19 | func TestGetSession(t *testing.T) { 20 | 21 | GetSession(oauth2Endpoint, "013Rc7FP0lmgxb2lRIIP0VefFP0Rc7FW") 22 | } 23 | 24 | func TestGetUserInfoBySession(t *testing.T) { 25 | 26 | sessionKey := "" 27 | 28 | iv := "" 29 | 30 | encrypt := "" 31 | 32 | info, err := GetSessionInfo(encrypt, sessionKey, iv) 33 | 34 | fmt.Println(info) 35 | 36 | fmt.Println(err) 37 | } 38 | -------------------------------------------------------------------------------- /mp/oauth2/component/endpoint.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/chanxuehong/wechat/oauth2" 8 | ) 9 | 10 | var _ oauth2.Endpoint = (*Endpoint)(nil) 11 | 12 | // Endpoint 实现了 wechat.v2/oauth2.Endpoint 接口. 13 | type Endpoint struct { 14 | AppId string 15 | ComponentAppId string 16 | ComponentAccessToken string 17 | } 18 | 19 | func NewEndpoint(appId, componentAppId, componentAccessToken string) *Endpoint { 20 | return &Endpoint{ 21 | AppId: appId, 22 | ComponentAppId: componentAppId, 23 | ComponentAccessToken: componentAccessToken, 24 | } 25 | } 26 | 27 | func (p *Endpoint) ExchangeTokenURL(code string) string { 28 | return fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/component/access_token?"+ 29 | "appid=%s&component_appid=%s&component_access_token=%s&code=%s&grant_type=authorization_code", 30 | url.QueryEscape(p.AppId), 31 | url.QueryEscape(p.ComponentAppId), 32 | url.QueryEscape(p.ComponentAccessToken), 33 | url.QueryEscape(code), 34 | ) 35 | } 36 | 37 | func (p *Endpoint) RefreshTokenURL(refreshToken string) string { 38 | return fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/component/refresh_token?"+ 39 | "appid=%s&component_appid=%s&component_access_token=%s&refresh_token=%s&grant_type=refresh_token", 40 | url.QueryEscape(p.AppId), 41 | url.QueryEscape(p.ComponentAppId), 42 | url.QueryEscape(p.ComponentAccessToken), 43 | url.QueryEscape(refreshToken), 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /mp/oauth2/component/oauth2.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // AuthCodeURL 生成网页授权地址. 9 | // 10 | // appId: 公众号的唯一标识 11 | // componentAppId: 服务方的appid,在申请创建公众号服务成功后,可在公众号服务详情页找到 12 | // redirectURI: 授权后重定向的回调链接地址 13 | // scope: 应用授权作用域 14 | // state: 重定向后会带上 state 参数, 开发者可以填写 a-zA-Z0-9 的参数值, 最多128字节 15 | func AuthCodeURL(appId, componentAppId, redirectURI, scope, state string) string { 16 | return fmt.Sprintf("https://open.weixin.qq.com/connect/oauth2/authorize?"+ 17 | "appid=%s&component_appid=%s&redirect_uri=%s&scope=%s&state=%s&response_type=code#wechat_redirect", 18 | url.QueryEscape(appId), 19 | url.QueryEscape(componentAppId), 20 | url.QueryEscape(redirectURI), 21 | url.QueryEscape(scope), 22 | url.QueryEscape(state), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /mp/oauth2/doc.go: -------------------------------------------------------------------------------- 1 | // 微信网页授权. 2 | package oauth2 3 | -------------------------------------------------------------------------------- /mp/oauth2/endpoint.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/chanxuehong/wechat/oauth2" 7 | ) 8 | 9 | var _ oauth2.Endpoint = (*Endpoint)(nil) 10 | 11 | // Endpoint 实现了 github.com/chanxuehong/wechat/oauth2.Endpoint 接口. 12 | type Endpoint struct { 13 | AppId string 14 | AppSecret string 15 | } 16 | 17 | func NewEndpoint(AppId, AppSecret string) *Endpoint { 18 | return &Endpoint{ 19 | AppId: AppId, 20 | AppSecret: AppSecret, 21 | } 22 | } 23 | 24 | func (p *Endpoint) ExchangeTokenURL(code string) string { 25 | return "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + url.QueryEscape(p.AppId) + 26 | "&secret=" + url.QueryEscape(p.AppSecret) + 27 | "&code=" + url.QueryEscape(code) + 28 | "&grant_type=authorization_code" 29 | } 30 | 31 | func (p *Endpoint) RefreshTokenURL(refreshToken string) string { 32 | return "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=" + url.QueryEscape(p.AppId) + 33 | "&grant_type=refresh_token&refresh_token=" + url.QueryEscape(refreshToken) 34 | } 35 | 36 | func (p *Endpoint) SessionCodeUrl(code string) string { 37 | 38 | return "https://api.weixin.qq.com/sns/jscode2session?appid=" + url.QueryEscape(p.AppId) + 39 | "&secret=" + url.QueryEscape(p.AppSecret) + 40 | "&js_code=" + url.QueryEscape(code) + 41 | "&grant_type=authorization_code" 42 | } 43 | -------------------------------------------------------------------------------- /mp/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/chanxuehong/wechat/internal/debug/api" 9 | "github.com/chanxuehong/wechat/oauth2" 10 | "github.com/chanxuehong/wechat/util" 11 | ) 12 | 13 | // AuthCodeURL 生成网页授权地址. 14 | // 15 | // appId: 公众号的唯一标识 16 | // redirectURI: 授权后重定向的回调链接地址 17 | // scope: 应用授权作用域 18 | // state: 重定向后会带上 state 参数, 开发者可以填写 a-zA-Z0-9 的参数值, 最多128字节 19 | func AuthCodeURL(appId, redirectURI, scope, state string) string { 20 | return "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + url.QueryEscape(appId) + 21 | "&redirect_uri=" + url.QueryEscape(redirectURI) + 22 | "&response_type=code&scope=" + url.QueryEscape(scope) + 23 | "&state=" + url.QueryEscape(state) + 24 | "#wechat_redirect" 25 | } 26 | 27 | // Auth 检验授权凭证 access_token 是否有效. 28 | // 29 | // accessToken: 网页授权接口调用凭证 30 | // openId: 用户的唯一标识 31 | // httpClient: 如果不指定则默认为 util.DefaultHttpClient 32 | func Auth(accessToken, openId string, httpClient *http.Client) (valid bool, err error) { 33 | if httpClient == nil { 34 | httpClient = util.DefaultHttpClient 35 | } 36 | 37 | _url := "https://api.weixin.qq.com/sns/auth?access_token=" + url.QueryEscape(accessToken) + 38 | "&openid=" + url.QueryEscape(openId) 39 | api.DebugPrintGetRequest(_url) 40 | httpResp, err := httpClient.Get(_url) 41 | if err != nil { 42 | return 43 | } 44 | defer httpResp.Body.Close() 45 | 46 | if httpResp.StatusCode != http.StatusOK { 47 | err = fmt.Errorf("http.Status: %s", httpResp.Status) 48 | return 49 | } 50 | 51 | var result oauth2.Error 52 | if err = api.DecodeJSONHttpResponse(httpResp.Body, &result); err != nil { 53 | return 54 | } 55 | 56 | switch result.ErrCode { 57 | case oauth2.ErrCodeOK: 58 | valid = true 59 | return 60 | case 42001, 40001, 40014, 40003: 61 | valid = false 62 | return 63 | default: 64 | err = &result 65 | return 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /mp/oauth2/session.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/chanxuehong/wechat/internal/debug/api" 8 | util2 "github.com/chanxuehong/wechat/internal/util" 9 | "github.com/chanxuehong/wechat/oauth2" 10 | "github.com/chanxuehong/wechat/util" 11 | "net/http" 12 | ) 13 | 14 | type Session struct { 15 | OpenId string `json:"openid"` // 用户唯一标识 16 | UnionId string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回 17 | SessionKey string `json:"session_key"` // 会话密钥 18 | } 19 | 20 | type SessionInfo struct { 21 | OpenId string `json:"openId"` // 用户的唯一标识 22 | Nickname string `json:"nickName"` // 用户昵称 23 | Gender int `json:"gender"` // 用户的性别, 值为1时是男性, 值为2时是女性, 值为0时是未知 24 | Language string `json:"language"` // 用户的语言 25 | City string `json:"city"` // 普通用户个人资料填写的城市 26 | Province string `json:"province"` // 用户个人资料填写的省份 27 | Country string `json:"country"` // 国家, 如中国为CN 28 | 29 | // 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像), 30 | // 用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 31 | AvatarUrl string `json:"avatarUrl"` 32 | UnionId string `json:"unionId"` // 只有在将小程序绑定到微信开放平台帐号后,才会出现该字段。 33 | } 34 | 35 | // GetSession 获取小程序会话 36 | func GetSession(Endpoint *Endpoint, code string) (session *Session, err error) { 37 | session = &Session{} 38 | if err = getSession(session, Endpoint.SessionCodeUrl(code), nil); err != nil { 39 | return 40 | } 41 | return 42 | } 43 | 44 | // GetSessionWithClient 获取小程序会话 45 | func GetSessionWithClient(Endpoint *Endpoint, code string, httpClient *http.Client) (session *Session, err error) { 46 | session = &Session{} 47 | if err = getSession(session, Endpoint.SessionCodeUrl(code), httpClient); err != nil { 48 | return 49 | } 50 | return 51 | } 52 | 53 | func getSession(session *Session, url string, httpClient *http.Client) (err error) { 54 | 55 | if httpClient == nil { 56 | httpClient = util.DefaultHttpClient 57 | } 58 | 59 | api.DebugPrintGetRequest(url) 60 | 61 | httpResp, err := httpClient.Get(url) 62 | if err != nil { 63 | return 64 | } 65 | defer httpResp.Body.Close() 66 | 67 | if httpResp.StatusCode != http.StatusOK { 68 | return fmt.Errorf("http.Status: %s", httpResp.Status) 69 | } 70 | 71 | var result struct { 72 | oauth2.Error 73 | Session 74 | } 75 | 76 | if err = api.DecodeJSONHttpResponse(httpResp.Body, &result); err != nil { 77 | return 78 | } 79 | 80 | if result.ErrCode != oauth2.ErrCodeOK { 81 | return &result.Error 82 | } 83 | 84 | *session = result.Session 85 | 86 | return 87 | } 88 | 89 | // GetSessionInfo 解密小程序会话加密信息 90 | func GetSessionInfo(EncryptedData, sessionKey, iv string) (info *SessionInfo, err error) { 91 | 92 | cipherText, err := base64.StdEncoding.DecodeString(EncryptedData) 93 | 94 | aesKey, err := base64.StdEncoding.DecodeString(sessionKey) 95 | aesIv, err := base64.StdEncoding.DecodeString(iv) 96 | 97 | if err != nil { 98 | return 99 | } 100 | 101 | raw, err := util2.AESDecryptData(cipherText, aesKey, aesIv) 102 | 103 | if err != nil { 104 | return 105 | } 106 | 107 | if err = json.Unmarshal(raw, &info); err != nil { 108 | return 109 | } 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /mp/oauth2/userinfo.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/chanxuehong/wechat/internal/debug/api" 9 | "github.com/chanxuehong/wechat/oauth2" 10 | "github.com/chanxuehong/wechat/util" 11 | ) 12 | 13 | const ( 14 | LanguageZhCN = "zh_CN" // 简体中文 15 | LanguageZhTW = "zh_TW" // 繁体中文 16 | LanguageEN = "en" // 英文 17 | ) 18 | 19 | const ( 20 | SexUnknown = 0 // 未知 21 | SexMale = 1 // 男性 22 | SexFemale = 2 // 女性 23 | ) 24 | 25 | type UserInfo struct { 26 | OpenId string `json:"openid"` // 用户的唯一标识 27 | Nickname string `json:"nickname"` // 用户昵称 28 | Sex int `json:"sex"` // 用户的性别, 值为1时是男性, 值为2时是女性, 值为0时是未知 29 | City string `json:"city"` // 普通用户个人资料填写的城市 30 | Province string `json:"province"` // 用户个人资料填写的省份 31 | Country string `json:"country"` // 国家, 如中国为CN 32 | 33 | // 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像), 34 | // 用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 35 | HeadImageURL string `json:"headimgurl,omitempty"` 36 | 37 | Privilege []string `json:"privilege,omitempty"` // 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) 38 | UnionId string `json:"unionid,omitempty"` // 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 39 | } 40 | 41 | // GetUserInfo 获取用户信息. 42 | // 43 | // accessToken: 网页授权接口调用凭证 44 | // openId: 用户的唯一标识 45 | // lang: 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语, 如果留空 "" 则默认为 zh_CN 46 | // httpClient: 如果不指定则默认为 util.DefaultHttpClient 47 | func GetUserInfo(accessToken, openId, lang string, httpClient *http.Client) (info *UserInfo, err error) { 48 | switch lang { 49 | case "": 50 | lang = LanguageZhCN 51 | case LanguageZhCN, LanguageZhTW, LanguageEN: 52 | default: 53 | lang = LanguageZhCN 54 | } 55 | 56 | if httpClient == nil { 57 | httpClient = util.DefaultHttpClient 58 | } 59 | 60 | _url := "https://api.weixin.qq.com/sns/userinfo?access_token=" + url.QueryEscape(accessToken) + 61 | "&openid=" + url.QueryEscape(openId) + 62 | "&lang=" + lang 63 | api.DebugPrintGetRequest(_url) 64 | httpResp, err := httpClient.Get(_url) 65 | if err != nil { 66 | return 67 | } 68 | defer httpResp.Body.Close() 69 | 70 | if httpResp.StatusCode != http.StatusOK { 71 | err = fmt.Errorf("http.Status: %s", httpResp.Status) 72 | return 73 | } 74 | 75 | var result struct { 76 | oauth2.Error 77 | UserInfo 78 | } 79 | if err = api.DecodeJSONHttpResponse(httpResp.Body, &result); err != nil { 80 | return 81 | } 82 | if result.ErrCode != oauth2.ErrCodeOK { 83 | err = &result.Error 84 | return 85 | } 86 | info = &result.UserInfo 87 | return 88 | } 89 | -------------------------------------------------------------------------------- /mp/poi/README.md: -------------------------------------------------------------------------------- 1 | ## 微信门店接口. 2 | 3 | * 对于门店的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传. 4 | -------------------------------------------------------------------------------- /mp/poi/add.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Photo struct { 8 | PhotoURL string `json:"photo_url"` 9 | } 10 | 11 | type AddParameters struct { 12 | BaseInfo struct { 13 | Sid string `json:"sid,omitempty"` // 可选, 商户自己的id,用于后续审核通过收到poi_id 的通知时,做对应关系。请商户自己保证唯一识别性 14 | BusinessName string `json:"business_name,omitempty"` // 必须, 门店名称(仅为商户名,如:国美、麦当劳,不应包含地区、地址、分店名等信息,错误示例:北京国美) 15 | BranchName string `json:"branch_name,omitempty"` // 必须, 分店名称(不应包含地区信息,不应与门店名有重复,错误示例:北京王府井店) 16 | Province string `json:"province,omitempty"` // 必须, 门店所在的省份(直辖市填城市名,如:北京市) 17 | City string `json:"city,omitempty"` // 必须, 门店所在的城市 18 | District string `json:"district,omitempty"` // 必须, 门店所在地区 19 | Address string `json:"address,omitempty"` // 必须, 门店所在的详细街道地址(不要填写省市信息) 20 | Telephone string `json:"telephone,omitempty"` // 必须, 门店的电话(纯数字,区号、分机号均由“-”隔开) 21 | Categories []string `json:"categories,omitempty"` // 必须, 门店的类型(不同级分类用“,”隔开,如:美食,川菜,火锅。详细分类参见附件:微信门店类目表) 22 | OffsetType int `json:"offset_type"` // 必须, 坐标类型,1 为火星坐标(目前只能选1) 23 | Longitude float64 `json:"longitude"` // 必须, 门店所在地理位置的经度 24 | Latitude float64 `json:"latitude"` // 必须, 门店所在地理位置的纬度(经纬度均为火星坐标,最好选用腾讯地图标记的坐标) 25 | PhotoList []Photo `json:"photo_list,omitempty"` // 必须, 图片列表,url 形式,可以有多张图片,尺寸为640*340px。必须为上一接口生成的url。 26 | Recommend string `json:"recommend,omitempty"` // 可选, 推荐品,餐厅可为推荐菜;酒店为推荐套房;景点为推荐游玩景点等,针对自己行业的推荐内容 27 | Special string `json:"special,omitempty"` // 必须, 特色服务,如免费wifi,免费停车,送货上门等商户能提供的特色功能或服务 28 | Introduction string `json:"introduction,omitempty"` // 可选, 商户简介,主要介绍商户信息等 29 | OpenTime string `json:"open_time,omitempty"` // 必须, 营业时间,24 小时制表示,用“-”连接,如 8:00-20:00 30 | AvgPrice int `json:"avg_price,omitempty"` // 必须, 人均价格,大于0 的整数 31 | } `json:"base_info"` 32 | } 33 | 34 | // Add 创建门店. 35 | func Add(clt *core.Client, params *AddParameters) (err error) { 36 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/poi/addpoi?access_token=" 37 | 38 | var request = struct { 39 | *AddParameters `json:"business,omitempty"` 40 | }{ 41 | AddParameters: params, 42 | } 43 | var result core.Error 44 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 45 | return 46 | } 47 | if result.ErrCode != core.ErrCodeOK { 48 | err = &result 49 | return 50 | } 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /mp/poi/category.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // CategoryList 获取门店类目表. 8 | func CategoryList(clt *core.Client) (list []string, err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/api_getwxcategory?access_token=" 10 | 11 | var result struct { 12 | core.Error 13 | CategoryList []string `json:"category_list"` 14 | } 15 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 16 | return 17 | } 18 | if result.ErrCode != core.ErrCodeOK { 19 | err = &result.Error 20 | return 21 | } 22 | list = result.CategoryList 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /mp/poi/del.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // Delete 删除门店. 8 | func Delete(clt *core.Client, poiId int64) (err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/poi/delpoi?access_token=" 10 | 11 | var request = struct { 12 | PoiId int64 `json:"poi_id"` 13 | }{ 14 | PoiId: poiId, 15 | } 16 | var result core.Error 17 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 18 | return 19 | } 20 | if result.ErrCode != core.ErrCodeOK { 21 | err = &result 22 | return 23 | } 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /mp/poi/doc.go: -------------------------------------------------------------------------------- 1 | // 微信门店接口. 2 | // 3 | // 对于门店的图片, 可以调用 base.UploadImage 或者 base.UploadImageFromReader 来上传. 4 | package poi 5 | -------------------------------------------------------------------------------- /mp/poi/event.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | const ( 8 | EventTypePoiCheckNotify core.EventType = "poi_check_notify" // 审核事件推送 9 | ) 10 | 11 | // 创建门店审核事件推送 12 | type PoiCheckNotifyEvent struct { 13 | XMLName struct{} `xml:"xml" json:"-"` 14 | core.MsgHeader 15 | EventType core.EventType `xml:"Event" json:"Event"` // 事件类型,poi_check_notify 16 | UniqId string `xml:"UniqId" json:"UniqId"` // 商户自己内部ID,即字段中的sid 17 | PoiId int64 `xml:"PoiId" json:"PoiId"` // 微信的门店ID,微信内门店唯一标示ID 18 | Result string `xml:"Result" json:"Result"` // 审核结果,成功succ 或失败fail 19 | Msg string `xml:"Msg" json:"Msg"` // 成功的通知信息,或审核失败的驳回理由 20 | } 21 | 22 | func GetPoiCheckNotifyEvent(msg *core.MixedMsg) *PoiCheckNotifyEvent { 23 | return &PoiCheckNotifyEvent{ 24 | MsgHeader: msg.MsgHeader, 25 | EventType: msg.EventType, 26 | UniqId: msg.UniqId, 27 | PoiId: msg.PoiId, 28 | Result: msg.Result, 29 | Msg: msg.Msg, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mp/poi/get.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type Poi struct { 8 | BaseInfo struct { 9 | PoiId int64 `json:"poi_id,omitempty"` // Poi 的id, 只有审核通过后才有 10 | AvailableState int `json:"available_state"` // 门店是否可用状态。1 表示系统错误、2 表示审核中、3 审核通过、4 审核驳回。当该字段为1、2、4 状态时,poi_id 为空 11 | UpdateStatus int `json:"update_status"` // 扩展字段是否正在更新中。1 表示扩展字段正在更新中,尚未生效,不允许再次更新; 0 表示扩展字段没有在更新中或更新已生效,可以再次更新 12 | 13 | Sid string `json:"sid,omitempty"` // 商户自己的id,用于后续审核通过收到poi_id 的通知时,做对应关系。请商户自己保证唯一识别性 14 | BusinessName string `json:"business_name,omitempty"` // 门店名称(仅为商户名,如:国美、麦当劳,不应包含地区、地址、分店名等信息,错误示例:北京国美) 15 | BranchName string `json:"branch_name,omitempty"` // 分店名称(不应包含地区信息,不应与门店名有重复,错误示例:北京王府井店) 16 | Province string `json:"province,omitempty"` // 门店所在的省份(直辖市填城市名,如:北京市) 17 | City string `json:"city,omitempty"` // 门店所在的城市 18 | District string `json:"district,omitempty"` // 门店所在地区 19 | Address string `json:"address,omitempty"` // 门店所在的详细街道地址(不要填写省市信息) 20 | Telephone string `json:"telephone,omitempty"` // 门店的电话(纯数字,区号、分机号均由“-”隔开) 21 | Categories []string `json:"categories,omitempty"` // 门店的类型(不同级分类用“,”隔开,如:美食,川菜,火锅。详细分类参见附件:微信门店类目表) 22 | OffsetType int `json:"offset_type"` // 坐标类型,1 为火星坐标(目前只能选1) 23 | Longitude float64 `json:"longitude"` // 门店所在地理位置的经度 24 | Latitude float64 `json:"latitude"` // 门店所在地理位置的纬度(经纬度均为火星坐标,最好选用腾讯地图标记的坐标) 25 | PhotoList []Photo `json:"photo_list,omitempty"` // 图片列表,url 形式,可以有多张图片,尺寸为640*340px。必须为上一接口生成的url。 26 | Recommend string `json:"recommend,omitempty"` // 推荐品,餐厅可为推荐菜;酒店为推荐套房;景点为推荐游玩景点等,针对自己行业的推荐内容 27 | Special string `json:"special,omitempty"` // 特色服务,如免费wifi,免费停车,送货上门等商户能提供的特色功能或服务 28 | Introduction string `json:"introduction,omitempty"` // 商户简介,主要介绍商户信息等 29 | OpenTime string `json:"open_time,omitempty"` // 营业时间,24 小时制表示,用“-”连接,如 8:00-20:00 30 | AvgPrice int `json:"avg_price,omitempty"` // 人均价格,大于0 的整数 31 | } `json:"base_info"` 32 | } 33 | 34 | // Get 查询门店信息. 35 | func Get(clt *core.Client, poiId int64) (poi *Poi, err error) { 36 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/poi/getpoi?access_token=" 37 | 38 | var request = struct { 39 | PoiId int64 `json:"poi_id"` 40 | }{ 41 | PoiId: poiId, 42 | } 43 | var result struct { 44 | core.Error 45 | Poi `json:"business"` 46 | } 47 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 48 | return 49 | } 50 | if result.ErrCode != core.ErrCodeOK { 51 | err = &result.Error 52 | return 53 | } 54 | result.Poi.BaseInfo.PoiId = poiId 55 | poi = &result.Poi 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /mp/poi/update.go: -------------------------------------------------------------------------------- 1 | package poi 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type UpdateParameters struct { 8 | BaseInfo struct { 9 | PoiId int64 `json:"poi_id"` 10 | 11 | // 下面7个字段,若有填写内容则为覆盖更新,若无内容则视为不修改,维持原有内容。 12 | // photo_list 字段为全列表覆盖,若需要增加图片,需将之前图片同样放入list 中,在其后增加新增图片。 13 | // 如:已有A、B、C 三张图片,又要增加D、E 两张图,则需要调用该接口,photo_list 传入A、B、C、D、E 五张图片的链接。 14 | Telephone string `json:"telephone,omitempty"` 15 | PhotoList []Photo `json:"photo_list,omitempty"` 16 | Recommend string `json:"recommend,omitempty"` 17 | Special string `json:"special,omitempty"` 18 | Introduction string `json:"introduction,omitempty"` 19 | OpenTime string `json:"open_time,omitempty"` 20 | AvgPrice int `json:"avg_price,omitempty"` 21 | } `json:"base_info"` 22 | } 23 | 24 | // Update 修改门店服务信息. 25 | func Update(clt *core.Client, params *UpdateParameters) (err error) { 26 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/poi/updatepoi?access_token=" 27 | 28 | var request = struct { 29 | *UpdateParameters `json:"business,omitempty"` 30 | }{ 31 | UpdateParameters: params, 32 | } 33 | var result core.Error 34 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 35 | return 36 | } 37 | if result.ErrCode != core.ErrCodeOK { 38 | err = &result 39 | return 40 | } 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /mp/qrcode/README.md: -------------------------------------------------------------------------------- 1 | ## 二维码接口 2 | -------------------------------------------------------------------------------- /mp/qrcode/doc.go: -------------------------------------------------------------------------------- 1 | // 二维码接口. 2 | package qrcode 3 | -------------------------------------------------------------------------------- /mp/qrcode/download.go: -------------------------------------------------------------------------------- 1 | package qrcode 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/chanxuehong/wechat/internal/debug/api" 11 | "github.com/chanxuehong/wechat/util" 12 | ) 13 | 14 | // 二维码图片的URL, 可以通过此URL下载二维码 或者 在线显示此二维码. 15 | func QrcodePicURL(ticket string) string { 16 | return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + url.QueryEscape(ticket) 17 | } 18 | 19 | // Download 通过ticket换取二维码, 写入到 filepath 路径的文件. 20 | // 21 | // 如果 clt == nil 则默认用 util.DefaultHttpClient. 22 | func Download(ticket, filepath string, clt *http.Client) (written int64, err error) { 23 | file, err := os.Create(filepath) 24 | if err != nil { 25 | return 26 | } 27 | defer func() { 28 | file.Close() 29 | if err != nil { 30 | os.Remove(filepath) 31 | } 32 | }() 33 | 34 | return DownloadToWriter(ticket, file, clt) 35 | } 36 | 37 | // DownloadToWriter 通过ticket换取二维码, 写入到 writer. 38 | // 39 | // 如果 clt == nil 则默认用 util.DefaultHttpClient. 40 | func DownloadToWriter(ticket string, writer io.Writer, clt *http.Client) (written int64, err error) { 41 | if clt == nil { 42 | clt = util.DefaultHttpClient 43 | } 44 | 45 | url := QrcodePicURL(ticket) 46 | api.DebugPrintGetRequest(url) 47 | httpResp, err := clt.Get(url) 48 | if err != nil { 49 | return 50 | } 51 | defer httpResp.Body.Close() 52 | 53 | if httpResp.StatusCode != http.StatusOK { 54 | err = fmt.Errorf("http.Status: %s", httpResp.Status) 55 | return 56 | } 57 | 58 | return io.Copy(writer, httpResp.Body) 59 | } 60 | -------------------------------------------------------------------------------- /mp/qrcode/shorturl.go: -------------------------------------------------------------------------------- 1 | package qrcode 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/base" 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | // ShortURL 将一条长链接转成短链接. 9 | func ShortURL(clt *core.Client, longURL string) (shortURL string, err error) { 10 | return base.ShortURL(clt, longURL) 11 | } 12 | -------------------------------------------------------------------------------- /mp/shakearound/account/account.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type RegisterParameters struct { 8 | Name string `json:"name"` // 必须, 联系人姓名 9 | PhoneNumber string `json:"phone_number"` // 必须, 联系人电话 10 | Email string `json:"email"` // 必须, 联系人邮箱 11 | IndustryId string `json:"industry_id"` // 必须, 平台定义的行业代号,具体请查看链接行业代号 12 | QualificationCertURLs []string `json:"qualification_cert_urls"` // 必须, 相关资质文件的图片url,图片需先上传至微信侧服务器,用“素材管理-上传图片素材”接口上传图片,返回的图片URL再配置在此处;当不需要资质文件时,数组内可以不填写url 13 | ApplyReason string `json:"apply_reason,omitempty"` // 可选, 申请理由 14 | } 15 | 16 | // 申请开通功能 17 | func Register(clt *core.Client, para *RegisterParameters) (err error) { 18 | var result core.Error 19 | 20 | incompleteURL := "https://api.weixin.qq.com/shakearound/account/register?access_token=" 21 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 22 | return 23 | } 24 | 25 | if result.ErrCode != core.ErrCodeOK { 26 | err = &result 27 | return 28 | } 29 | return 30 | } 31 | 32 | type AuditStatus struct { 33 | ApplyTime int64 `json:"apply_time"` // 提交申请的时间戳 34 | AuditStatus int `json:"audit_status"` // 审核状态。0:审核未通过、1:审核中、2:审核已通过;审核会在三个工作日内完成 35 | AuditComment string `json:"audit_comment"` // 审核备注,包括审核不通过的原因 36 | AuditTime int64 `json:"audit_time"` // 确定审核结果的时间戳;若状态为审核中,则该时间值为0 37 | } 38 | 39 | // 查询审核状态 40 | func GetAuditStatus(clt *core.Client) (status *AuditStatus, err error) { 41 | var result struct { 42 | core.Error 43 | AuditStatus `json:"data"` 44 | } 45 | 46 | incompleteURL := "https://api.weixin.qq.com/shakearound/account/auditstatus?access_token=" 47 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 48 | return 49 | } 50 | 51 | if result.ErrCode != core.ErrCodeOK { 52 | err = &result.Error 53 | return 54 | } 55 | status = &result.AuditStatus 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /mp/shakearound/device/applyid.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type ApplyIdParameters struct { 8 | Quantity int `json:"quantity"` // 必须, 申请的设备ID的数量,单次新增设备超过500个,需走人工审核流程 9 | ApplyReason string `json:"apply_reason"` // 必须, 申请理由,不超过100个字 10 | Comment string `json:"comment,omitempty"` // 可选, 备注,不超过15个汉字或30个英文字母 11 | PoiId *int64 `json:"poi_id,omitempty"` // 可选, 设备关联的门店ID,关联门店后,在门店1KM的范围内有优先摇出信息的机会。 12 | } 13 | 14 | type ApplyIdResult struct { 15 | ApplyId int64 `json:"apply_id"` // 申请的批次ID,可用在“查询设备列表”接口按批次查询本次申请成功的设备ID。 16 | AuditStatus int `json:"audit_status"` // 审核状态。0:审核未通过、1:审核中、2:审核已通过;若单次申请的设备ID数量小于等于500个,系统会进行快速审核;若单次申请的设备ID数量大于500个,会在三个工作日内完成审核 17 | AuditComment string `json:"audit_comment"` // 审核备注,包括审核不通过的原因 18 | } 19 | 20 | // 申请设备ID 21 | func ApplyId(clt *core.Client, para *ApplyIdParameters) (rslt *ApplyIdResult, err error) { 22 | var result struct { 23 | core.Error 24 | ApplyIdResult `json:"data"` 25 | } 26 | 27 | incompleteURL := "https://api.weixin.qq.com/shakearound/device/applyid?access_token=" 28 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 29 | return 30 | } 31 | 32 | if result.ErrCode != core.ErrCodeOK { 33 | err = &result.Error 34 | return 35 | } 36 | 37 | rslt = &result.ApplyIdResult 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /mp/shakearound/device/applystatus.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type ApplyStatus struct { 8 | ApplyTime int64 `json:"apply_time"` // 提交申请的时间戳 9 | AuditStatus int `json:"audit_status"` // 审核状态。0:审核未通过、1:审核中、2:审核已通过;审核会在三个工作日内完成 10 | AuditComment string `json:"audit_comment"` // 审核备注,包括审核不通过的原因 11 | AuditTime int64 `json:"audit_time"` // 确定审核结果的时间戳,若状态为审核中,则该时间值为0 12 | } 13 | 14 | // 查询设备ID申请审核状态 15 | func GetApplyStatus(clt *core.Client, applyId int64) (status *ApplyStatus, err error) { 16 | request := struct { 17 | ApplyId int64 `json:"apply_id"` 18 | }{ 19 | ApplyId: applyId, 20 | } 21 | 22 | var result struct { 23 | core.Error 24 | ApplyStatus `json:"data"` 25 | } 26 | 27 | incompleteURL := "https://api.weixin.qq.com/shakearound/device/applystatus?access_token=" 28 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 29 | return 30 | } 31 | 32 | if result.ErrCode != core.ErrCodeOK { 33 | err = &result.Error 34 | return 35 | } 36 | status = &result.ApplyStatus 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /mp/shakearound/device/bindlocation.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 配置设备与门店的关联关系 8 | func BindLocation(clt *core.Client, deviceIdentifier *DeviceIdentifier, poiId int64) (err error) { 9 | request := struct { 10 | DeviceIdentifier *DeviceIdentifier `json:"device_identifier,omitempty"` 11 | PoiId int64 `json:"poi_id"` 12 | }{ 13 | DeviceIdentifier: deviceIdentifier, 14 | PoiId: poiId, 15 | } 16 | 17 | var result core.Error 18 | 19 | incompleteURL := "https://api.weixin.qq.com/shakearound/device/bindlocation?access_token=" 20 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 21 | return 22 | } 23 | 24 | if result.ErrCode != core.ErrCodeOK { 25 | err = &result 26 | return 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /mp/shakearound/device/bindpage.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type BindPageParameters struct { 8 | DeviceIdentifier *DeviceIdentifier `json:"device_identifier,omitempty"` // 必须, 设备标识 9 | PageIds []int64 `json:"page_ids,omitempty"` // 必须, 待关联的页面列表 10 | Bind int `json:"bind"` // 必须, 关联操作标志位, 0为解除关联关系,1为建立关联关系 11 | Append int `json:"append"` // 必须, 新增操作标志位, 0为覆盖,1为新增 12 | } 13 | 14 | func BindPage(clt *core.Client, para *BindPageParameters) (err error) { 15 | var result core.Error 16 | 17 | incompleteURL := "https://api.weixin.qq.com/shakearound/device/bindpage?access_token=" 18 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 19 | return 20 | } 21 | 22 | if result.ErrCode != core.ErrCodeOK { 23 | err = &result 24 | return 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /mp/shakearound/device/update.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/internal/util" 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | // 设备标识 9 | type DeviceIdentifier struct { 10 | // 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 11 | DeviceId *int64 `json:"device_id,omitempty"` 12 | 13 | // UUID、major、minor,三个信息需填写完整,若填了设备编号,则可不填此信息。 14 | UUID string `json:"uuid,omitempty"` 15 | Major *int `json:"major,omitempty"` 16 | Minor *int `json:"minor,omitempty"` 17 | } 18 | 19 | func NewDeviceIdentifier1(deviceId int64) *DeviceIdentifier { 20 | return &DeviceIdentifier{ 21 | DeviceId: util.Int64(deviceId), 22 | } 23 | } 24 | 25 | func NewDeviceIdentifier2(uuid string, major, minor int) *DeviceIdentifier { 26 | return &DeviceIdentifier{ 27 | UUID: uuid, 28 | Major: util.Int(major), 29 | Minor: util.Int(minor), 30 | } 31 | } 32 | 33 | func NewDeviceIdentifier3(deviceId int64, uuid string, major, minor int) *DeviceIdentifier { 34 | return &DeviceIdentifier{ 35 | DeviceId: util.Int64(deviceId), 36 | UUID: uuid, 37 | Major: util.Int(major), 38 | Minor: util.Int(minor), 39 | } 40 | } 41 | 42 | // 编辑设备信息 43 | func Update(clt *core.Client, deviceIdentifier *DeviceIdentifier, comment string) (err error) { 44 | request := struct { 45 | DeviceIdentifier *DeviceIdentifier `json:"device_identifier,omitempty"` 46 | Comment string `json:"comment"` 47 | }{ 48 | DeviceIdentifier: deviceIdentifier, 49 | Comment: comment, 50 | } 51 | 52 | var result core.Error 53 | 54 | incompleteURL := "https://api.weixin.qq.com/shakearound/device/update?access_token=" 55 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 56 | return 57 | } 58 | 59 | if result.ErrCode != core.ErrCodeOK { 60 | err = &result 61 | return 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /mp/shakearound/doc.go: -------------------------------------------------------------------------------- 1 | // 摇一摇周边 2 | package shakearound 3 | -------------------------------------------------------------------------------- /mp/shakearound/event.go: -------------------------------------------------------------------------------- 1 | package shakearound 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | const ( 10 | // 推送到公众号URL上的事件类型 11 | EventTypeUserShake core.EventType = "ShakearoundUserShake" // 摇一摇事件通知 12 | ) 13 | 14 | type UserShakeEvent struct { 15 | XMLName struct{} `xml:"xml" json:"-"` 16 | core.MsgHeader 17 | 18 | EventType core.EventType `xml:"Event" json:"Event"` // 事件类型,ShakearoundUserShake 19 | 20 | ChosenBeacon *ChosenBeacon `xml:"ChosenBeacon,omitempty" json:"ChosenBeacon,omitempty"` 21 | AroundBeacons []AroundBeacon `xml:"AroundBeacons>AroundBeacon,omitempty" json:"AroundBeacons,omitempty"` 22 | } 23 | 24 | // 和 github.com/chanxuehong/wechat/mp/core.MixedMsg.ChosenBeacon 一样, 同步修改 25 | type ChosenBeacon struct { 26 | UUID string `xml:"Uuid" json:"Uuid"` 27 | Major int `xml:"Major" json:"Major"` 28 | Minor int `xml:"Minor" json:"Minor"` 29 | Distance float64 `xml:"Distance" json:"Distance"` 30 | } 31 | 32 | // 和 github.com/chanxuehong/wechat/mp/core.MixedMsg.AroundBeacon 一样, 同步修改 33 | type AroundBeacon struct { 34 | UUID string `xml:"Uuid" json:"Uuid"` 35 | Major int `xml:"Major" json:"Major"` 36 | Minor int `xml:"Minor" json:"Minor"` 37 | Distance float64 `xml:"Distance" json:"Distance"` 38 | } 39 | 40 | func GetUserShakeEvent(msg *core.MixedMsg) *UserShakeEvent { 41 | return &UserShakeEvent{ 42 | MsgHeader: msg.MsgHeader, 43 | EventType: msg.EventType, 44 | ChosenBeacon: (*ChosenBeacon)(msg.ChosenBeacon), 45 | AroundBeacons: *(*[]AroundBeacon)(unsafe.Pointer(&msg.AroundBeacons)), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mp/shakearound/material/add.go: -------------------------------------------------------------------------------- 1 | package material 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/chanxuehong/wechat/mp/core" 11 | ) 12 | 13 | type ImageInfo struct { 14 | PicURL string `json:"pic_url"` 15 | } 16 | 17 | func Add(clt *core.Client, imagePath, _type string) (info ImageInfo, err error) { 18 | file, err := os.Open(imagePath) 19 | if err != nil { 20 | return 21 | } 22 | defer file.Close() 23 | 24 | return addFromReader(clt, filepath.Base(imagePath), file, _type) 25 | } 26 | 27 | func AddFromReader(clt *core.Client, filename string, reader io.Reader, _type string) (info ImageInfo, err error) { 28 | if filename == "" { 29 | err = errors.New("empty filename") 30 | return 31 | } 32 | if reader == nil { 33 | err = errors.New("nil reader") 34 | return 35 | } 36 | 37 | return addFromReader(clt, filename, reader, _type) 38 | } 39 | 40 | func addFromReader(clt *core.Client, filename string, reader io.Reader, _type string) (info ImageInfo, err error) { 41 | var result struct { 42 | core.Error 43 | ImageInfo `json:"data"` 44 | } 45 | 46 | var incompleteURL string 47 | if _type != "" { 48 | incompleteURL = "https://api.weixin.qq.com/shakearound/material/add?type=" + url.QueryEscape(_type) + 49 | "&access_token=" 50 | } else { 51 | incompleteURL = "https://api.weixin.qq.com/shakearound/material/add?access_token=" 52 | } 53 | fields := []core.MultipartFormField{{ 54 | IsFile: true, 55 | Name: "media", 56 | FileName: filename, 57 | Value: reader, 58 | }} 59 | if err = clt.PostMultipartForm(incompleteURL, fields, &result); err != nil { 60 | return 61 | } 62 | 63 | if result.ErrCode != core.ErrCodeOK { 64 | err = &result.Error 65 | return 66 | } 67 | info = result.ImageInfo 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /mp/shakearound/page/add.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type AddParameters struct { 8 | Title string `json:"title"` // 必须, 在摇一摇页面展示的主标题,不超过6个字 9 | Description string `json:"description"` // 必须, 在摇一摇页面展示的副标题,不超过7个字 10 | PageURL string `json:"page_url"` // 必须, 跳转链接 11 | IconURL string `json:"icon_url"` // 必须, 在摇一摇页面展示的图片。图片需先上传至微信侧服务器,用“素材管理-上传图片素材”接口上传图片,返回的图片URL再配置在此处 12 | Comment string `json:"comment,omitempty"` // 可选, 页面的备注信息,不超过15个字 13 | } 14 | 15 | // 新增页面 16 | func Add(clt *core.Client, para *AddParameters) (pageId int64, err error) { 17 | var result struct { 18 | core.Error 19 | Data struct { 20 | PageId int64 `json:"page_id"` 21 | } `json:"data"` 22 | } 23 | 24 | incompleteURL := "https://api.weixin.qq.com/shakearound/page/add?access_token=" 25 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 26 | return 27 | } 28 | 29 | if result.ErrCode != core.ErrCodeOK { 30 | err = &result.Error 31 | return 32 | } 33 | pageId = result.Data.PageId 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /mp/shakearound/page/delete.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 删除页面 8 | func Delete(clt *core.Client, pageIds []int64) (err error) { 9 | request := struct { 10 | PageIds []int64 `json:"page_ids,omitempty"` 11 | }{ 12 | PageIds: pageIds, 13 | } 14 | 15 | var result core.Error 16 | 17 | incompleteURL := "https://api.weixin.qq.com/shakearound/page/delete?access_token=" 18 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 19 | return 20 | } 21 | 22 | if result.ErrCode != core.ErrCodeOK { 23 | err = &result 24 | return 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /mp/shakearound/page/update.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type UpdateParameters struct { 8 | PageId int64 `json:"page_id"` // 必须, 摇周边页面唯一ID 9 | Title string `json:"title,omitempty"` // 必须, 在摇一摇页面展示的主标题,不超过6个字 10 | Description string `json:"description,omitempty"` // 必须, 在摇一摇页面展示的副标题,不超过7个字 11 | PageURL string `json:"page_url,omitempty"` // 必须, 跳转链接 12 | IconURL string `json:"icon_url,omitempty"` // 必须, 在摇一摇页面展示的图片。图片需先上传至微信侧服务器,用“素材管理-上传图片素材”接口上传图片,返回的图片URL再配置在此处 13 | Comment string `json:"comment,omitempty"` // 可选, 页面的备注信息,不超过15个字 14 | } 15 | 16 | // 编辑页面信息 17 | func Update(clt *core.Client, para *UpdateParameters) (err error) { 18 | var result core.Error 19 | 20 | incompleteURL := "https://api.weixin.qq.com/shakearound/page/update?access_token=" 21 | if err = clt.PostJSON(incompleteURL, para, &result); err != nil { 22 | return 23 | } 24 | 25 | if result.ErrCode != core.ErrCodeOK { 26 | err = &result 27 | return 28 | } 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /mp/shakearound/statistics/device.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | "github.com/chanxuehong/wechat/mp/shakearound/device" 6 | ) 7 | 8 | // 以设备为维度的数据统计接口 9 | func Device(clt *core.Client, deviceIdentifier *device.DeviceIdentifier, beginDate, endDate int64) (data []StatisticsBase, err error) { 10 | request := struct { 11 | DeviceIdentifier *device.DeviceIdentifier `json:"device_identifier,omitempty"` 12 | BeginDate int64 `json:"begin_date"` 13 | EndDate int64 `json:"end_date"` 14 | }{ 15 | DeviceIdentifier: deviceIdentifier, 16 | BeginDate: beginDate, 17 | EndDate: endDate, 18 | } 19 | 20 | var result struct { 21 | core.Error 22 | Data []StatisticsBase `json:"data"` 23 | } 24 | 25 | incompleteURL := "https://api.weixin.qq.com/shakearound/statistics/device?access_token=" 26 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 27 | return 28 | } 29 | 30 | if result.ErrCode != core.ErrCodeOK { 31 | err = &result.Error 32 | return 33 | } 34 | data = result.Data 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /mp/shakearound/statistics/page.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // 以页面为维度的数据统计接口 8 | func Page(clt *core.Client, pageId, beginDate, endDate int64) (data []StatisticsBase, err error) { 9 | request := struct { 10 | PageId int64 `json:"page_id"` 11 | BeginDate int64 `json:"begin_date"` 12 | EndDate int64 `json:"end_date"` 13 | }{ 14 | PageId: pageId, 15 | BeginDate: beginDate, 16 | EndDate: endDate, 17 | } 18 | 19 | var result struct { 20 | core.Error 21 | Data []StatisticsBase `json:"data"` 22 | } 23 | 24 | incompleteURL := "https://api.weixin.qq.com/shakearound/statistics/page?access_token=" 25 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 26 | return 27 | } 28 | 29 | if result.ErrCode != core.ErrCodeOK { 30 | err = &result.Error 31 | return 32 | } 33 | data = result.Data 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /mp/shakearound/statistics/statistics.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/shakearound/device" 5 | ) 6 | 7 | type StatisticsBase struct { 8 | Ftime int64 `json:"ftime"` // 当天0点对应的时间戳 9 | ClickPV int `json:"click_pv"` // 点击摇周边消息的次数 10 | ClickUV int `json:"click_uv"` // 点击摇周边消息的人数 11 | ShakePV int `json:"shake_pv"` // 摇周边的次数 12 | ShakeUV int `json:"shake_uv"` // 摇周边的人数 13 | } 14 | 15 | type DeviceStatistics struct { 16 | device.DeviceBase 17 | StatisticsBase 18 | } 19 | 20 | type PageStatistics struct { 21 | PageId int64 `json:"page_id"` 22 | StatisticsBase 23 | } 24 | -------------------------------------------------------------------------------- /mp/shakearound/user/getshakeinfo.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | type BeaconInfo struct { 8 | Distance float64 `json:"distance"` // Beacon信号与手机的距离,单位为米 9 | UUID string `json:"uuid"` 10 | Major int `json:"major"` 11 | Minor int `json:"minor"` 12 | } 13 | 14 | type Shakeinfo struct { 15 | PageId int64 `json:"page_id"` // 摇周边页面唯一ID 16 | BeaconInfo BeaconInfo `json:"beacon_info"` // 设备信息,包括UUID、major、minor,以及距离 17 | Openid string `json:"openid"` // 商户AppID下用户的唯一标识 18 | PoiId *int64 `json:"poi_id"` // 门店ID,有的话则返回,反之不会在JSON格式内 19 | } 20 | 21 | // 获取摇周边的设备及用户信息 22 | // 23 | // ticket: 摇周边业务的ticket,可在摇到的URL中得到,ticket生效时间为30分钟,每一次摇都会重新生成新的ticket 24 | // needPoi: 是否需要返回门店poi_id 25 | func GetShakeInfo(clt *core.Client, ticket string, needPoi bool) (info *Shakeinfo, err error) { 26 | request := struct { 27 | Ticket string `json:"ticket"` 28 | NeedPoi int `json:"need_poi,omitempty"` 29 | }{ 30 | Ticket: ticket, 31 | } 32 | 33 | if needPoi { 34 | request.NeedPoi = 1 35 | } 36 | 37 | var result struct { 38 | core.Error 39 | Shakeinfo `json:"data"` 40 | } 41 | 42 | incompleteURL := "https://api.weixin.qq.com/shakearound/user/getshakeinfo?access_token=" 43 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 44 | return 45 | } 46 | 47 | if result.ErrCode != core.ErrCodeOK { 48 | err = &result.Error 49 | return 50 | } 51 | info = &result.Shakeinfo 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /mp/user/README.md: -------------------------------------------------------------------------------- 1 | ## 用户管理 2 | -------------------------------------------------------------------------------- /mp/user/doc.go: -------------------------------------------------------------------------------- 1 | // 用户管理. 2 | package user 3 | -------------------------------------------------------------------------------- /mp/user/group.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/chanxuehong/wechat/mp/core" 5 | ) 6 | 7 | // GroupId 查询用户所在分组. 8 | func GroupId(clt *core.Client, openId string) (groupId int64, err error) { 9 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=" 10 | 11 | var request = struct { 12 | OpenId string `json:"openid"` 13 | }{ 14 | OpenId: openId, 15 | } 16 | var result struct { 17 | core.Error 18 | GroupId int64 `json:"groupid"` 19 | } 20 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 21 | return 22 | } 23 | if result.ErrCode != core.ErrCodeOK { 24 | err = &result.Error 25 | return 26 | } 27 | groupId = result.GroupId 28 | return 29 | } 30 | 31 | // MoveToGroup 移动用户分组. 32 | func MoveToGroup(clt *core.Client, openId string, toGroupId int64) (err error) { 33 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=" 34 | 35 | var request = struct { 36 | OpenId string `json:"openid"` 37 | ToGroupId int64 `json:"to_groupid"` 38 | }{ 39 | OpenId: openId, 40 | ToGroupId: toGroupId, 41 | } 42 | var result core.Error 43 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 44 | return 45 | } 46 | if result.ErrCode != core.ErrCodeOK { 47 | err = &result 48 | return 49 | } 50 | return 51 | } 52 | 53 | // BatchMoveToGroup 批量移动用户分组. 54 | func BatchMoveToGroup(clt *core.Client, openIdList []string, toGroupId int64) (err error) { 55 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/members/batchupdate?access_token=" 56 | 57 | if len(openIdList) <= 0 { 58 | return 59 | } 60 | 61 | var request = struct { 62 | OpenIdList []string `json:"openid_list,omitempty"` 63 | ToGroupId int64 `json:"to_groupid"` 64 | }{ 65 | OpenIdList: openIdList, 66 | ToGroupId: toGroupId, 67 | } 68 | var result core.Error 69 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 70 | return 71 | } 72 | if result.ErrCode != core.ErrCodeOK { 73 | err = &result 74 | return 75 | } 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /mp/user/group/README.md: -------------------------------------------------------------------------------- 1 | ## 用户分组管理 2 | -------------------------------------------------------------------------------- /mp/user/group/group.go: -------------------------------------------------------------------------------- 1 | // 用户分组管理. 2 | package group 3 | 4 | import ( 5 | "github.com/chanxuehong/wechat/mp/core" 6 | ) 7 | 8 | type Group struct { 9 | Id int64 `json:"id"` // 分组id, 由微信分配 10 | Name string `json:"name"` // 分组名字, UTF8编码 11 | UserCount int `json:"count"` // 分组内用户数量 12 | } 13 | 14 | // Create 创建分组. 15 | func Create(clt *core.Client, name string) (group *Group, err error) { 16 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/create?access_token=" 17 | 18 | var request struct { 19 | Group struct { 20 | Name string `json:"name"` 21 | } `json:"group"` 22 | } 23 | request.Group.Name = name 24 | 25 | var result struct { 26 | core.Error 27 | Group `json:"group"` 28 | } 29 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 30 | return 31 | } 32 | if result.ErrCode != core.ErrCodeOK { 33 | err = &result.Error 34 | return 35 | } 36 | result.Group.UserCount = 0 37 | group = &result.Group 38 | return 39 | } 40 | 41 | // Delete 删除分组. 42 | func Delete(clt *core.Client, groupId int64) (err error) { 43 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/delete?access_token=" 44 | 45 | var request struct { 46 | Group struct { 47 | Id int64 `json:"id"` 48 | } `json:"group"` 49 | } 50 | request.Group.Id = groupId 51 | 52 | var result core.Error 53 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 54 | return 55 | } 56 | if result.ErrCode != core.ErrCodeOK { 57 | err = &result 58 | return 59 | } 60 | return 61 | } 62 | 63 | // Update 修改分组名. 64 | func Update(clt *core.Client, groupId int64, name string) (err error) { 65 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/update?access_token=" 66 | 67 | var request struct { 68 | Group struct { 69 | Id int64 `json:"id"` 70 | Name string `json:"name"` 71 | } `json:"group"` 72 | } 73 | request.Group.Id = groupId 74 | request.Group.Name = name 75 | 76 | var result core.Error 77 | if err = clt.PostJSON(incompleteURL, &request, &result); err != nil { 78 | return 79 | } 80 | if result.ErrCode != core.ErrCodeOK { 81 | err = &result 82 | return 83 | } 84 | return 85 | } 86 | 87 | // List 查询所有分组. 88 | func List(clt *core.Client) (groups []Group, err error) { 89 | const incompleteURL = "https://api.weixin.qq.com/cgi-bin/groups/get?access_token=" 90 | 91 | var result struct { 92 | core.Error 93 | Groups []Group `json:"groups"` 94 | } 95 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 96 | return 97 | } 98 | if result.ErrCode != core.ErrCodeOK { 99 | err = &result.Error 100 | return 101 | } 102 | groups = result.Groups 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /mp/user/list.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/chanxuehong/wechat/mp/core" 7 | ) 8 | 9 | // 获取用户列表返回的数据结构 10 | type ListResult struct { 11 | TotalCount int `json:"total"` // 关注该公众账号的总用户数 12 | ItemCount int `json:"count"` // 拉取的OPENID个数, 最大值为10000 13 | 14 | Data struct { 15 | OpenIdList []string `json:"openid,omitempty"` 16 | } `json:"data"` // 列表数据, OPENID的列表 17 | 18 | // 拉取列表的最后一个用户的OPENID, 如果 next_openid == "" 则表示没有了用户数据 19 | NextOpenId string `json:"next_openid"` 20 | } 21 | 22 | // List 获取用户列表. 23 | // 24 | // NOTE: 每次最多能获取 10000 个用户, 可以多次指定 nextOpenId 来获取以满足需求, 如果 nextOpenId == "" 则表示从头获取 25 | func List(clt *core.Client, nextOpenId string) (rslt *ListResult, err error) { 26 | var incompleteURL string 27 | if nextOpenId == "" { 28 | incompleteURL = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=" 29 | } else { 30 | incompleteURL = "https://api.weixin.qq.com/cgi-bin/user/get?next_openid=" + url.QueryEscape(nextOpenId) + "&access_token=" 31 | } 32 | 33 | var result struct { 34 | core.Error 35 | ListResult 36 | } 37 | if err = clt.GetJSON(incompleteURL, &result); err != nil { 38 | return 39 | } 40 | if result.ErrCode != core.ErrCodeOK { 41 | err = &result.Error 42 | return 43 | } 44 | rslt = &result.ListResult 45 | return 46 | } 47 | 48 | // ===================================================================================================================== 49 | 50 | // UserIterator 51 | // 52 | // iter, err := NewUserIterator(clt, "NextOpenId") 53 | // if err != nil { 54 | // // TODO: 增加你的代码 55 | // } 56 | // 57 | // for iter.HasNext() { 58 | // openids, err := iter.NextPage() 59 | // if err != nil { 60 | // // TODO: 增加你的代码 61 | // } 62 | // // TODO: 增加你的代码 63 | // } 64 | type UserIterator struct { 65 | clt *core.Client 66 | 67 | lastListResult *ListResult 68 | nextPageCalled bool 69 | } 70 | 71 | func (iter *UserIterator) TotalCount() int { 72 | return iter.lastListResult.TotalCount 73 | } 74 | 75 | func (iter *UserIterator) HasNext() bool { 76 | if !iter.nextPageCalled { 77 | return iter.lastListResult.ItemCount > 0 || iter.lastListResult.NextOpenId != "" 78 | } 79 | return iter.lastListResult.NextOpenId != "" 80 | } 81 | 82 | func (iter *UserIterator) NextPage() (openIdList []string, err error) { 83 | if !iter.nextPageCalled { 84 | iter.nextPageCalled = true 85 | openIdList = iter.lastListResult.Data.OpenIdList 86 | return 87 | } 88 | 89 | rslt, err := List(iter.clt, iter.lastListResult.NextOpenId) 90 | if err != nil { 91 | return 92 | } 93 | 94 | iter.lastListResult = rslt 95 | 96 | openIdList = rslt.Data.OpenIdList 97 | return 98 | } 99 | 100 | // NewUserIterator 获取用户遍历器, 从 nextOpenId 开始遍历, 如果 nextOpenId == "" 则表示从头遍历. 101 | func NewUserIterator(clt *core.Client, nextOpenId string) (iter *UserIterator, err error) { 102 | // 逻辑上相当于第一次调用 UserIterator.NextPage, 103 | // 因为第一次调用 UserIterator.HasNext 需要数据支撑, 所以提前获取了数据 104 | rslt, err := List(clt, nextOpenId) 105 | if err != nil { 106 | return 107 | } 108 | 109 | iter = &UserIterator{ 110 | clt: clt, 111 | lastListResult: rslt, 112 | nextPageCalled: false, 113 | } 114 | return 115 | } 116 | -------------------------------------------------------------------------------- /oauth2/README.md: -------------------------------------------------------------------------------- 1 | ## 微信 oauth2 基础库 -------------------------------------------------------------------------------- /oauth2/client.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/chanxuehong/wechat/util" 8 | ) 9 | 10 | type Client struct { 11 | Endpoint Endpoint 12 | 13 | // TokenStorage 和 Token 两个字段正常情况下只用指定一个, 如果两个同时被指定了, 优先使用 TokenStorage 14 | TokenStorage TokenStorage 15 | Token *Token // Client 自动将最新的 Token 更新到此字段, 不管 Token 字段一开始是否被指定!!! 16 | 17 | HttpClient *http.Client // 如果 HttpClient == nil 则默认用 util.DefaultHttpClient 18 | } 19 | 20 | func (clt *Client) httpClient() *http.Client { 21 | if clt.HttpClient != nil { 22 | return clt.HttpClient 23 | } 24 | return util.DefaultHttpClient 25 | } 26 | 27 | // GetToken 获取 Token, autoRefresh 为 true 时如果 Token 过期则自动刷新. 28 | func (clt *Client) GetToken(autoRefresh bool) (tk *Token, err error) { 29 | if clt.TokenStorage != nil { 30 | if tk, err = clt.TokenStorage.Token(); err != nil { 31 | return 32 | } 33 | if tk == nil { 34 | err = errors.New("incorrect TokenStorage.Token implementation") 35 | return 36 | } 37 | clt.Token = tk // update local 38 | } else { 39 | tk = clt.Token 40 | if tk == nil { 41 | err = errors.New("nil TokenStorage and nil Token") 42 | return 43 | } 44 | } 45 | if autoRefresh && tk.Expired() { 46 | return clt.RefreshToken(tk.RefreshToken) 47 | } 48 | return 49 | } 50 | 51 | func (clt *Client) putToken(tk *Token) (err error) { 52 | if clt.TokenStorage != nil { 53 | if err = clt.TokenStorage.PutToken(tk); err != nil { 54 | return 55 | } 56 | } 57 | clt.Token = tk 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /oauth2/doc.go: -------------------------------------------------------------------------------- 1 | // 微信 oauth2 基础库 2 | package oauth2 3 | -------------------------------------------------------------------------------- /oauth2/error.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | ErrCodeOK = 0 9 | ) 10 | 11 | type Error struct { 12 | ErrCode int64 `json:"errcode"` 13 | ErrMsg string `json:"errmsg"` 14 | } 15 | 16 | func (err *Error) Error() string { 17 | return fmt.Sprintf("errcode: %d, errmsg: %s", err.ErrCode, err.ErrMsg) 18 | } 19 | -------------------------------------------------------------------------------- /oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Endpoint interface { 8 | ExchangeTokenURL(code string) string // 通过code换取access_token的地址 9 | RefreshTokenURL(refreshToken string) string // 刷新access_token的地址 10 | } 11 | 12 | type TokenStorage interface { 13 | Token() (*Token, error) 14 | PutToken(*Token) error 15 | } 16 | 17 | type Token struct { 18 | AccessToken string `json:"access_token"` // 网页授权接口调用凭证 19 | CreatedAt int64 `json:"created_at"` // access_token 创建时间, unixtime, 分布式系统要求时间同步, 建议使用 NTP 20 | ExpiresIn int64 `json:"expires_in"` // access_token 接口调用凭证超时时间, 单位: 秒 21 | RefreshToken string `json:"refresh_token,omitempty"` // 刷新 access_token 的凭证 22 | 23 | OpenId string `json:"openid,omitempty"` 24 | UnionId string `json:"unionid,omitempty"` 25 | Scope string `json:"scope,omitempty"` // 用户授权的作用域, 使用逗号(,)分隔 26 | } 27 | 28 | // Expired 判断 token.AccessToken 是否过期, 过期返回 true, 否则返回 false. 29 | func (token *Token) Expired() bool { 30 | return time.Now().Unix() >= token.CreatedAt+token.ExpiresIn 31 | } 32 | -------------------------------------------------------------------------------- /open/README.md: -------------------------------------------------------------------------------- 1 | ## 微信开放平台相关 SDK 2 | -------------------------------------------------------------------------------- /open/doc.go: -------------------------------------------------------------------------------- 1 | // open 微信開放平臺 2 | package open 3 | -------------------------------------------------------------------------------- /open/oauth2/README.md: -------------------------------------------------------------------------------- 1 | ## 微信开放平台 移动应用、网站应用 微信登录功能 SDK 2 | -------------------------------------------------------------------------------- /open/oauth2/doc.go: -------------------------------------------------------------------------------- 1 | // 微信开放平台 移动应用、网站应用 微信登录功能 SDK. 2 | package oauth2 3 | -------------------------------------------------------------------------------- /open/oauth2/endpoint.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | mpoauth2 "github.com/chanxuehong/wechat/mp/oauth2" 5 | "github.com/chanxuehong/wechat/oauth2" 6 | ) 7 | 8 | var _ oauth2.Endpoint = (*Endpoint)(nil) 9 | 10 | type Endpoint mpoauth2.Endpoint 11 | 12 | func NewEndpoint(AppId, AppSecret string) *Endpoint { 13 | return (*Endpoint)(mpoauth2.NewEndpoint(AppId, AppSecret)) 14 | } 15 | 16 | func (p *Endpoint) ExchangeTokenURL(code string) string { 17 | return ((*mpoauth2.Endpoint)(p)).ExchangeTokenURL(code) 18 | } 19 | 20 | func (p *Endpoint) RefreshTokenURL(refreshToken string) string { 21 | return ((*mpoauth2.Endpoint)(p)).RefreshTokenURL(refreshToken) 22 | } 23 | -------------------------------------------------------------------------------- /open/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | mpoauth2 "github.com/chanxuehong/wechat/mp/oauth2" 8 | ) 9 | 10 | // AuthCodeURL 生成网页授权地址. 11 | // 12 | // appId: 公众号的唯一标识 13 | // redirectURI: 授权后重定向的回调链接地址 14 | // scope: 应用授权作用域 15 | // state: 重定向后会带上 state 参数, 开发者可以填写 a-zA-Z0-9 的参数值, 最多128字节 16 | func AuthCodeURL(appId, redirectURI, scope, state string) string { 17 | return "https://open.weixin.qq.com/connect/qrconnect?appid=" + url.QueryEscape(appId) + 18 | "&redirect_uri=" + url.QueryEscape(redirectURI) + 19 | "&response_type=code&scope=" + url.QueryEscape(scope) + 20 | "&state=" + url.QueryEscape(state) + 21 | "#wechat_redirect" 22 | } 23 | 24 | // Auth 检验授权凭证 access_token 是否有效. 25 | // 26 | // accessToken: 网页授权接口调用凭证 27 | // openId: 用户的唯一标识 28 | // httpClient: 如果不指定则默认为 util.DefaultHttpClient 29 | func Auth(accessToken, openId string, httpClient *http.Client) (valid bool, err error) { 30 | return mpoauth2.Auth(accessToken, openId, httpClient) 31 | } 32 | -------------------------------------------------------------------------------- /open/oauth2/userinfo.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "net/http" 5 | 6 | mpoauth2 "github.com/chanxuehong/wechat/mp/oauth2" 7 | ) 8 | 9 | const ( 10 | LanguageZhCN = mpoauth2.LanguageZhCN 11 | LanguageZhTW = mpoauth2.LanguageZhTW 12 | LanguageEN = mpoauth2.LanguageEN 13 | ) 14 | 15 | const ( 16 | SexUnknown = mpoauth2.SexUnknown 17 | SexMale = mpoauth2.SexMale 18 | SexFemale = mpoauth2.SexFemale 19 | ) 20 | 21 | type UserInfo mpoauth2.UserInfo 22 | 23 | // GetUserInfo 获取用户信息. 24 | // 25 | // accessToken: 网页授权接口调用凭证 26 | // openId: 用户的唯一标识 27 | // lang: 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语, 如果留空 "" 则默认为 zh_CN 28 | // httpClient: 如果不指定则默认为 util.DefaultHttpClient 29 | func GetUserInfo(accessToken, openId, lang string, httpClient *http.Client) (info *UserInfo, err error) { 30 | infox, err := mpoauth2.GetUserInfo(accessToken, openId, lang, httpClient) 31 | if err != nil { 32 | return 33 | } 34 | info = (*UserInfo)(infox) 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /util/doc.go: -------------------------------------------------------------------------------- 1 | // 提供一些实用函数 2 | package util 3 | -------------------------------------------------------------------------------- /util/helper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Bool is a helper routine that allocates a new bool value 4 | // to store v and returns a pointer to it. 5 | func Bool(v bool) *bool { 6 | return &v 7 | } 8 | 9 | // Int is a helper routine that allocates a new int value 10 | // to store v and returns a pointer to it. 11 | func Int(v int) *int { 12 | return &v 13 | } 14 | 15 | // Int32 is a helper routine that allocates a new int32 value 16 | // to store v and returns a pointer to it. 17 | func Int32(v int32) *int32 { 18 | return &v 19 | } 20 | 21 | // Int64 is a helper routine that allocates a new int64 value 22 | // to store v and returns a pointer to it. 23 | func Int64(v int64) *int64 { 24 | return &v 25 | } 26 | 27 | // Float32 is a helper routine that allocates a new float32 value 28 | // to store v and returns a pointer to it. 29 | func Float32(v float32) *float32 { 30 | return &v 31 | } 32 | 33 | // Float64 is a helper routine that allocates a new float64 value 34 | // to store v and returns a pointer to it. 35 | func Float64(v float64) *float64 { 36 | return &v 37 | } 38 | 39 | // Uint32 is a helper routine that allocates a new uint32 value 40 | // to store v and returns a pointer to it. 41 | func Uint32(v uint32) *uint32 { 42 | return &v 43 | } 44 | 45 | // Uint64 is a helper routine that allocates a new uint64 value 46 | // to store v and returns a pointer to it. 47 | func Uint64(v uint64) *uint64 { 48 | return &v 49 | } 50 | 51 | // String is a helper routine that allocates a new string value 52 | // to store v and returns a pointer to it. 53 | func String(v string) *string { 54 | return &v 55 | } 56 | -------------------------------------------------------------------------------- /util/http_client.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | var DefaultHttpClient *http.Client 9 | 10 | func init() { 11 | client := *http.DefaultClient 12 | client.Timeout = time.Second * 5 13 | DefaultHttpClient = &client 14 | } 15 | 16 | var DefaultMediaHttpClient *http.Client 17 | 18 | func init() { 19 | client := *http.DefaultClient 20 | client.Timeout = time.Second * 60 21 | DefaultMediaHttpClient = &client 22 | } 23 | -------------------------------------------------------------------------------- /util/http_response_writer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | type httpResponseWriter struct { 9 | io.Writer 10 | } 11 | 12 | func (httpResponseWriter) Header() http.Header { 13 | return make(map[string][]string) 14 | } 15 | func (httpResponseWriter) WriteHeader(int) {} 16 | 17 | // 将 io.Writer 从语义上实现 http.ResponseWriter. 18 | func HttpResponseWriter(w io.Writer) http.ResponseWriter { 19 | if rw, ok := w.(http.ResponseWriter); ok { 20 | return rw 21 | } 22 | return httpResponseWriter{Writer: w} 23 | } 24 | -------------------------------------------------------------------------------- /util/location.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | var BeijingLocation = time.FixedZone("Asia/Shanghai", 8*60*60) 6 | -------------------------------------------------------------------------------- /util/nonce_str.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "github.com/chanxuehong/rand" 4 | 5 | func NonceStr() string { 6 | return string(rand.NewHex()) 7 | } 8 | -------------------------------------------------------------------------------- /util/wxver.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // 获取微信客户端的版本. 11 | // 12 | // userAgent: 微信内置浏览器的 User-Agent; 13 | // x, y, z, w: 如果微信版本为 5.3.1.2 则有 x==5, y==3, z==1, w==2 14 | // err: 错误信息 15 | func WXVersion(userAgent string) (x, y, z, w int, err error) { 16 | userAgent = versionRegexp.FindString(userAgent) 17 | if userAgent == "" { 18 | err = fmt.Errorf("不是有效的微信浏览器 User-Agent: %s", userAgent) 19 | return 20 | } 21 | userAgent = userAgent[len("MicroMessenger/"):] 22 | 23 | strArr := strings.Split(userAgent, ".") 24 | verArr := make([]int, len(strArr)) 25 | 26 | for i, str := range strArr { 27 | verArr[i], err = strconv.Atoi(str) 28 | if err != nil { 29 | err = fmt.Errorf("不是有效的微信浏览器 User-Agent: %s", userAgent) 30 | return 31 | } 32 | } 33 | 34 | switch len(verArr) { 35 | default: 36 | fallthrough 37 | case 4: 38 | x = verArr[0] 39 | y = verArr[1] 40 | z = verArr[2] 41 | w = verArr[3] 42 | return 43 | case 3: 44 | x = verArr[0] 45 | y = verArr[1] 46 | z = verArr[2] 47 | return 48 | case 2: 49 | x = verArr[0] 50 | y = verArr[1] 51 | return 52 | case 1: 53 | x = verArr[0] 54 | return 55 | } 56 | } 57 | 58 | // Mozilla/5.0 (Linux; Android 4.4.4; Che1-CL10 Build/Che1-CL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043128 Safari/537.36 MicroMessenger/6.5.7.1041 NetType/WIFI Language/zh_CN 59 | var versionRegexp = regexp.MustCompile(`MicroMessenger/\d+(\.\d+)*`) 60 | -------------------------------------------------------------------------------- /weixin_pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chanxuehong/wechat/36f0325263cdec440d6e36f93d168e4cc39b64b8/weixin_pay.png -------------------------------------------------------------------------------- /weixin_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chanxuehong/wechat/36f0325263cdec440d6e36f93d168e4cc39b64b8/weixin_qrcode.png --------------------------------------------------------------------------------