├── .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 | 
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
--------------------------------------------------------------------------------