From ebf6158b7c16c895132c4f3aeb72013a1e4f1440 Mon Sep 17 00:00:00 2001 From: aris song Date: Sun, 15 Apr 2018 13:06:14 +0800 Subject: [PATCH 01/25] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=BC=80=E6=94=BE?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=BD=91=E9=A1=B5=E5=BA=94=E7=94=A8=E8=8E=B7?= =?UTF-8?q?=E5=8F=96oauth=E5=9B=9E=E8=B0=83=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oauth/oauth.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/oauth/oauth.go b/oauth/oauth.go index 6b2b710..044b66e 100644 --- a/oauth/oauth.go +++ b/oauth/oauth.go @@ -11,11 +11,12 @@ import ( ) const ( - redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect" - accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" - refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s" - userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN" - checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s" + redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect" + webAppRedirectOauthURL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect" + accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" + refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s" + userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN" + checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s" ) //Oauth 保存用户授权信息 @@ -37,6 +38,12 @@ func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, er return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil } +//GetWebAppRedirectURL 获取网页应用跳转的url地址 +func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) { + urlStr := url.QueryEscape(redirectURI) + return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil +} + //Redirect 跳转到网页授权 func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error { location, err := oauth.GetRedirectURL(redirectURI, scope, state) From 2da9755c586921e6a499c0f5bfebe124fd61aff5 Mon Sep 17 00:00:00 2001 From: Chuanjian Wang Date: Tue, 18 Dec 2018 20:01:33 +0800 Subject: [PATCH 02/25] add component funcs --- context/component_access_token.go | 221 ++++++++++++++++++++++++++++++ context/component_test.go | 19 +++ 2 files changed, 240 insertions(+) create mode 100644 context/component_access_token.go create mode 100644 context/component_test.go diff --git a/context/component_access_token.go b/context/component_access_token.go new file mode 100644 index 0000000..1acb0a0 --- /dev/null +++ b/context/component_access_token.go @@ -0,0 +1,221 @@ +package context + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/funxdata/wechat/util" +) + +const ( + componentAccessTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_component_token" + getPreCodeURL = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=%s" + queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s" + refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s" + getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s" + getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s" +) + +// ComponentAccessToken 第三方平台 +type ComponentAccessToken struct { + AccessToken string `json:"component_access_token"` + ExpiresIn int64 `json:"expires_in"` +} + +// GetComponentAccessToken 获取 ComponentAccessToken +func (ctx *Context) GetComponentAccessToken() (string, error) { + accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID) + val := ctx.Cache.Get(accessTokenCacheKey) + if val == nil { + return "", fmt.Errorf("cann't get component access token") + } + return val.(string), nil +} + +// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken +func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAccessToken, error) { + body := map[string]string{ + "component_appid": ctx.AppID, + "component_appsecret": ctx.AppSecret, + "component_verify_ticket": verifyTicket, + } + respBody, err := util.PostJSON(componentAccessTokenURL, body) + if err != nil { + return nil, err + } + + at := &ComponentAccessToken{} + if err := json.Unmarshal(respBody, at); err != nil { + return nil, err + } + + accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID) + expires := at.ExpiresIn - 1500 + ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second) + return at, nil +} + +// GetPreCode 获取预授权码 +func (ctx *Context) GetPreCode() (string, error) { + cat, err := ctx.GetComponentAccessToken() + if err != nil { + return "", err + } + req := map[string]string{ + "component_appid": ctx.AppID, + } + uri := fmt.Sprintf(getPreCodeURL, cat) + body, err := util.PostJSON(uri, req) + if err != nil { + return "", err + } + + var ret struct { + PreCode string `json:"pre_auth_code"` + } + if err := json.Unmarshal(body, &ret); err != nil { + return "", err + } + + return ret.PreCode, nil +} + +// ID 微信返回接口中各种类型字段 +type ID struct { + ID int `json:"id"` +} + +// AuthBaseInfo 授权的基本信息 +type AuthBaseInfo struct { + AuthrAccessToken + FuncInfo []AuthFuncInfo `json:"func_info"` +} + +// AuthFuncInfo 授权的接口内容 +type AuthFuncInfo struct { + FuncscopeCategory ID `json:"funcscope_category"` +} + +// AuthrAccessToken 授权方AccessToken +type AuthrAccessToken struct { + Appid string `json:"authorizer_appid"` + AccessToken string `json:"authorizer_access_token"` + ExpiresIn int64 `json:"expires_in"` + RefreshToken string `json:"authorizer_refresh_token"` +} + +// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息 +func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) { + cat, err := ctx.GetComponentAccessToken() + if err != nil { + return nil, err + } + + req := map[string]string{ + "component_appid": ctx.AppID, + "authorization_code": authCode, + } + uri := fmt.Sprintf(queryAuthURL, cat) + body, err := util.PostJSON(uri, req) + if err != nil { + return nil, err + } + + var ret struct { + Info *AuthBaseInfo `json:"authorization_info"` + } + + if err := json.Unmarshal(body, &ret); err != nil { + return nil, err + } + + return ret.Info, nil +} + +// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌) +func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) { + cat, err := ctx.GetComponentAccessToken() + if err != nil { + return nil, err + } + + req := map[string]string{ + "component_appid": ctx.AppID, + "authorizer_appid": appid, + "authorizer_refresh_token": refreshToken, + } + uri := fmt.Sprintf(refreshTokenURL, cat) + body, err := util.PostJSON(uri, req) + if err != nil { + return nil, err + } + + ret := &AuthrAccessToken{} + if err := json.Unmarshal(body, ret); err != nil { + return nil, err + } + + authrTokenKey := "authorizer_access_token_" + appid + ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80) + + return ret, nil +} + +// GetAuthrAccessToken 获取授权方AccessToken +func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) { + authrTokenKey := "authorizer_access_token_" + appid + val := ctx.Cache.Get(authrTokenKey) + if val == nil { + return "", fmt.Errorf("cannot get authorizer %s access token", appid) + } + return val.(string), nil +} + +// AuthorizerInfo 授权方详细信息 +type AuthorizerInfo struct { + NickName string `json:"nick_name"` + HeadImg string `json:"head_img"` + ServiceTypeInfo ID `json:"service_type_info"` + VerifyTypeInfo ID `json:"verify_type_info"` + UserName string `json:"user_name"` + PrincipalName string `json:"principal_name"` + BusinessInfo struct { + OpenStore string `json:"open_store"` + OpenScan string `json:"open_scan"` + OpenPay string `json:"open_pay"` + OpenCard string `json:"open_card"` + OpenShake string `json:"open_shake"` + } + Alias string `json:"alias"` + QrcodeURL string `json:"qrcode_url"` +} + +// GetAuthrInfo 获取授权方的帐号基本信息 +func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) { + cat, err := ctx.GetComponentAccessToken() + if err != nil { + return nil, nil, err + } + + req := map[string]string{ + "component_appid": ctx.AppID, + "authorizer_appid": appid, + } + + uri := fmt.Sprintf(getComponentInfoURL, cat) + body, err := util.PostJSON(uri, req) + if err != nil { + return nil, nil, err + } + + var ret struct { + AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"` + AuthorizationInfo *AuthBaseInfo `json:"authorization_info"` + } + if err := json.Unmarshal(body, &ret); err != nil { + return nil, nil, err + } + + return ret.AuthorizerInfo, ret.AuthorizationInfo, nil +} diff --git a/context/component_test.go b/context/component_test.go new file mode 100644 index 0000000..5bf087d --- /dev/null +++ b/context/component_test.go @@ -0,0 +1,19 @@ +package context + +import ( + "encoding/json" + "testing" +) + +var testdata = `{"authorizer_info":{"nick_name":"就爱浪","head_img":"http:\/\/wx.qlogo.cn\/mmopen\/xPKCxELaaj6hiaTZGv19oQPBJibb7hBoKmNOjQibCNOUycE8iaBhiaHOA6eC8hadQSAUZTuHUJl4qCIbCQGjSWialicfzWh4mdxuejY\/0","service_type_info":{"id":1},"verify_type_info":{"id":-1},"user_name":"gh_dcdbaa6f1687","alias":"ckeyer","qrcode_url":"http:\/\/mmbiz.qpic.cn\/mmbiz_jpg\/FribWCoIzQbAX7R1PQ8iaxGonqKp0doYD2ibhC0uhx11LrRcblASiazsbQJTJ4icQnMzfH7G0SUPuKbibTA8Cs4uk5WQ\/0","business_info":{"open_pay":0,"open_shake":0,"open_scan":0,"open_card":0,"open_store":0},"idc":1,"principal_name":"个人","signature":"不折腾会死。"},"authorization_info":{"authorizer_appid":"yyyyy","authorizer_refresh_token":"xxxx","func_info":[{"funcscope_category":{"id":1}},{"funcscope_category":{"id":15}},{"funcscope_category":{"id":4}},{"funcscope_category":{"id":7}},{"funcscope_category":{"id":2}},{"funcscope_category":{"id":3}},{"funcscope_category":{"id":11}},{"funcscope_category":{"id":6}},{"funcscope_category":{"id":5}},{"funcscope_category":{"id":8}},{"funcscope_category":{"id":13}},{"funcscope_category":{"id":9}},{"funcscope_category":{"id":12}},{"funcscope_category":{"id":22}},{"funcscope_category":{"id":23}},{"funcscope_category":{"id":24},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":26}},{"funcscope_category":{"id":27},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":33},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":35}}]}}` + +// TestDecode +func TestDecode(t *testing.T) { + var ret struct { + AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"` + AuthorizationInfo *AuthBaseInfo `json:"authorization_info"` + } + json.Unmarshal([]byte(testdata), &ret) + t.Logf("%+v", ret.AuthorizerInfo) + t.Logf("%+v", ret.AuthorizationInfo) +} From 3984f13c76ec7b7f1a707f335c52699da147a4d7 Mon Sep 17 00:00:00 2001 From: Chuanjian Wang Date: Fri, 17 Aug 2018 14:00:56 +0800 Subject: [PATCH 03/25] add memory cache --- cache/memory.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 cache/memory.go diff --git a/cache/memory.go b/cache/memory.go new file mode 100644 index 0000000..5bf115b --- /dev/null +++ b/cache/memory.go @@ -0,0 +1,74 @@ +package cache + +import ( + "sync" + "time" +) + +//Memory struct contains *memcache.Client +type Memory struct { + sync.Mutex + + data map[string]*data +} + +type data struct { + Data interface{} + Expired time.Time +} + +//NewMemory create new memcache +func NewMemory() *Memory { + return &Memory{ + data: map[string]*data{}, + } +} + +//Get return cached value +func (mem *Memory) Get(key string) interface{} { + if ret, ok := mem.data[key]; ok { + if ret.Expired.Before(time.Now()) { + mem.deleteKey(key) + return nil + } + return ret.Data + } + return nil +} + +// IsExist check value exists in memcache. +func (mem *Memory) IsExist(key string) bool { + if ret, ok := mem.data[key]; ok { + if ret.Expired.Before(time.Now()) { + mem.deleteKey(key) + return false + } + return true + } + return false +} + +//Set cached value with key and expire time. +func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) { + mem.Lock() + defer mem.Unlock() + + mem.data[key] = &data{ + Data: val, + Expired: time.Now().Add(timeout), + } + return nil +} + +//Delete delete value in memcache. +func (mem *Memory) Delete(key string) error { + return mem.deleteKey(key) +} + +// deleteKey +func (mem *Memory) deleteKey(key string) error { + mem.Lock() + defer mem.Unlock() + delete(mem.data, key) + return nil +} From fedd5a96caec77d39a9d461a47d78a9a122cf25b Mon Sep 17 00:00:00 2001 From: Chuanjian Wang Date: Wed, 19 Dec 2018 19:53:07 +0800 Subject: [PATCH 04/25] add list user openids --- user/user.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/user/user.go b/user/user.go index a792aae..9cf4851 100644 --- a/user/user.go +++ b/user/user.go @@ -3,6 +3,7 @@ package user import ( "encoding/json" "fmt" + "net/url" "github.com/silenceper/wechat/context" "github.com/silenceper/wechat/util" @@ -11,6 +12,7 @@ import ( const ( userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN" updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s" + userListURL = "https://api.weixin.qq.com/cgi-bin/user/get" ) //User 用户管理 @@ -45,6 +47,16 @@ type Info struct { TagidList []int32 `json:"tagid_list"` } +// OpenidList 用户列表 +type OpenidList struct { + Total int `json:"total"` + Count int `json:"count"` + Data struct { + OpenIDs []string `json:"openid"` + } `json:"data"` + NextOpenID string `json:"next_openid"` +} + //GetUserInfo 获取用户基本信息 func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) { var accessToken string @@ -88,3 +100,52 @@ func (user *User) UpdateRemark(openID, remark string) (err error) { return util.DecodeWithCommonError(response, "UpdateRemark") } + +// ListUserOpenIDs 返回用户列表 +func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) { + accessToken, err := user.GetAccessToken() + if err != nil { + return nil, err + } + + uri, _ := url.Parse(userListURL) + q := uri.Query() + q.Set("access_token", accessToken) + if len(nextOpenid) > 0 && nextOpenid[0] != "" { + q.Set("next_openid", nextOpenid[0]) + } + uri.RawQuery = q.Encode() + + response, err := util.HTTPGet(uri.String()) + if err != nil { + return nil, err + } + + userlist := new(OpenidList) + err = json.Unmarshal(response, userlist) + if err != nil { + return nil, err + } + + return userlist, nil +} + +// ListAllUserOpenIDs 返回所有用户OpenID列表 +func (user *User) ListAllUserOpenIDs() ([]string, error) { + nextOpenid := "" + openids := []string{} + count := 0 + for { + ul, err := user.ListUserOpenIDs(nextOpenid) + if err != nil { + return nil, err + } + openids = append(openids, ul.Data.OpenIDs...) + count += ul.Count + if ul.Total > count { + nextOpenid = ul.NextOpenID + } else { + return openids, nil + } + } +} From 036183e5ff652aecf7ef449c8e47c0f57f774393 Mon Sep 17 00:00:00 2001 From: Chuanjian Wang Date: Sun, 23 Dec 2018 15:55:36 +0800 Subject: [PATCH 05/25] rcode --- qr/qr.go | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ wechat.go | 6 +++ 2 files changed, 128 insertions(+) create mode 100644 qr/qr.go diff --git a/qr/qr.go b/qr/qr.go new file mode 100644 index 0000000..a2b4fdb --- /dev/null +++ b/qr/qr.go @@ -0,0 +1,122 @@ +package qr + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/silenceper/wechat/context" + "github.com/silenceper/wechat/util" +) + +const ( + qrCreateURL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s" + getQRImgURL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s" +) + +const ( + actionID = "QR_SCENE" + actionStr = "QR_STR_SCENE" + + actionLimitID = "QR_LIMIT_SCENE" + actionLimitStr = "QR_LIMIT_STR_SCENE" +) + +// QR 二维码 +type QR struct { + *context.Context +} + +//NewQR 二维码实例 +func NewQR(context *context.Context) *QR { + q := new(QR) + q.Context = context + return q +} + +// Request 临时二维码 +type Request struct { + ExpireSeconds int64 `json:"expire_seconds,omitempty"` + ActionName string `json:"action_name"` + ActionInfo struct { + Scene struct { + SceneStr string `json:"scene_str,omitempty"` + SceneID int `json:"scene_id,omitempty"` + } `json:"scene"` + } `json:"action_info"` +} + +// Ticket 二维码ticket +type Ticket struct { + util.CommonError `json:",inline"` + Ticket string `json:"ticket"` + ExpireSeconds int64 `json:"expire_seconds"` + URL string `json:"url"` +} + +// GetQRTicket 获取二维码 Ticket +func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) { + accessToken, err := q.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(qrCreateURL, accessToken) + response, err := util.PostJSON(uri, tq) + if err != nil { + err = fmt.Errorf("get qr ticket failed, %s", err) + return + } + + t = new(Ticket) + err = json.Unmarshal(response, &t) + if err != nil { + return + } + + return +} + +// ShowQRCode 通过ticket换取二维码 +func ShowQRCode(tk *Ticket) string { + return fmt.Sprintf(getQRImgURL, tk.Ticket) +} + +// NewTmpQrRequest 新建临时二维码请求实例 +func NewTmpQrRequest(exp time.Duration, scene interface{}) *Request { + tq := &Request{ + ExpireSeconds: int64(exp.Seconds()), + } + switch reflect.ValueOf(scene).Kind() { + case reflect.String: + tq.ActionName = actionStr + tq.ActionInfo.Scene.SceneStr = scene.(string) + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64: + tq.ActionName = actionID + tq.ActionInfo.Scene.SceneID = scene.(int) + } + + return tq +} + +// NewLimitQrRequest 新建永久二维码请求实例 +func NewLimitQrRequest(scene interface{}) *Request { + tq := &Request{} + switch reflect.ValueOf(scene).Kind() { + case reflect.String: + tq.ActionName = actionLimitStr + tq.ActionInfo.Scene.SceneStr = scene.(string) + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64: + tq.ActionName = actionLimitID + tq.ActionInfo.Scene.SceneID = scene.(int) + } + + return tq +} diff --git a/wechat.go b/wechat.go index 19856b8..deadc8a 100644 --- a/wechat.go +++ b/wechat.go @@ -11,6 +11,7 @@ import ( "github.com/silenceper/wechat/menu" "github.com/silenceper/wechat/oauth" "github.com/silenceper/wechat/pay" + "github.com/silenceper/wechat/qr" "github.com/silenceper/wechat/server" "github.com/silenceper/wechat/template" "github.com/silenceper/wechat/user" @@ -99,3 +100,8 @@ func (wc *Wechat) GetTemplate() *template.Template { func (wc *Wechat) GetPay() *pay.Pay { return pay.NewPay(wc.Context) } + +// GetQR 返回二维码的实例 +func (wc *Wechat) GetQR() *qr.QR { + return qr.NewQR(wc.Context) +} From 443435343c4b85f4e2add776ae9be8bf1aea5dfb Mon Sep 17 00:00:00 2001 From: Chuanjian Wang Date: Sun, 23 Dec 2018 16:23:48 +0800 Subject: [PATCH 06/25] add unionid --- oauth/oauth.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/oauth/oauth.go b/oauth/oauth.go index 6b2b710..ba7e4b4 100644 --- a/oauth/oauth.go +++ b/oauth/oauth.go @@ -56,6 +56,10 @@ type ResAccessToken struct { RefreshToken string `json:"refresh_token"` OpenID string `json:"openid"` Scope string `json:"scope"` + + // UnionID 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 + // 公众号文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 + UnionID string `json:"unionid"` } // GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token) From eda287070d6e176e9f937b695ad39df62f78d2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jefferwang=28=E7=8E=8B=E4=BF=8A=E9=94=8B=29?= Date: Thu, 14 Feb 2019 16:07:24 +0800 Subject: [PATCH 07/25] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/analysis.go | 305 +++++++++++++++++++++++++++++++++++++ miniprogram/miniprogram.go | 17 +++ miniprogram/qrcode.go | 91 +++++++++++ miniprogram/sns.go | 40 +++++ wechat.go | 6 + 5 files changed, 459 insertions(+) create mode 100644 miniprogram/analysis.go create mode 100644 miniprogram/miniprogram.go create mode 100644 miniprogram/qrcode.go create mode 100644 miniprogram/sns.go diff --git a/miniprogram/analysis.go b/miniprogram/analysis.go new file mode 100644 index 0000000..a87d53a --- /dev/null +++ b/miniprogram/analysis.go @@ -0,0 +1,305 @@ +package miniprogram + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/util" +) + +const ( + // 获取用户访问小程序日留存 + getAnalysisDailyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyretaininfo?access_token=%s" + // 获取用户访问小程序月留存 + getAnalysisMonthlyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyretaininfo?access_token=%s" + // 获取用户访问小程序周留存 + getAnalysisWeeklyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyretaininfo?access_token=%s" + // 获取用户访问小程序数据概况 + getAnalysisDailySummaryURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailysummarytrend?access_token=%s" + // 获取用户访问小程序数据日趋势 + getAnalysisDailyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=%s" + // 获取用户访问小程序数据月趋势 + getAnalysisMonthlyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyvisittrend?access_token=%s" + // 获取用户访问小程序数据周趋势 + getAnalysisWeeklyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyvisittrend?access_token=%s" + // 获取小程序新增或活跃用户的画像分布数据 + getAnalysisUserPortraitURL = "https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=%s" + // 获取用户小程序访问分布数据 + getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s" + // 访问页面 + getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s" +) + +// fetchData 拉取统计数据 +func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) { + var accessToken string + accessToken, err = wxa.GetAccessToken() + if err != nil { + return + } + urlStr = fmt.Sprintf(urlStr, accessToken) + response, err = util.PostJSON(urlStr, body) + return +} + +// AnalysisRetainItem 留存项结构 +type AnalysisRetainItem struct { + Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推 + Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时) +} + +// ResAnalysisRetain 小程序留存数据返回 +type ResAnalysisRetain struct { + util.CommonError + RefDate string `json:"ref_date"` // 日期 + VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存 + VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存 +} + +// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周) +func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(urlStr, body) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("getAnalysisRetain error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// GetAnalysisDailyRetain 获取用户访问小程序日留存 +func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) { + return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate) +} + +// GetAnalysisMonthlyRetain 获取用户访问小程序月留存 +func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) { + return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate) +} + +// GetAnalysisWeeklyRetain 获取用户访问小程序日留存 +func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) { + return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate) +} + +// ResAnalysisDailySummary 小程序访问数据概况 +type ResAnalysisDailySummary struct { + util.CommonError + List []struct { + RefDate string `json:"ref_date"` // 日期 + VisitTotal int `json:"visit_total"` // 累计用户数 + SharePV int `json:"share_pv"` // 转发次数 + ShareUV int `json:"share_uv"` // 转发人数 + } `json:"list"` +} + +// GetAnalysisDailySummary 获取用户访问小程序数据概况 +func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(getAnalysisDailySummaryURL, body) + if err != nil { + return + } + fmt.Println(string(response)) + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("GetAnalysisDailySummary error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// ResAnalysisVisitTrend 小程序访问数据趋势(日、月、周) +type ResAnalysisVisitTrend struct { + util.CommonError + List []struct { + RefDate string `json:"ref_date"` // 日期 + SessionCnt int `json:"session_cnt"` // 打开次数 + VisitPV int `json:"visit_pv"` // 访问次数 + VisitUV int `json:"visit_uv"` // 访问人数 + VisitUVNew int `json:"visit_uv_new"` // 新用户数 + StayTimeUV float64 `json:"stay_time_uv"` // 人均停留时长 + StayTimeSession float64 `json:"stay_time_session"` // 次均停留时常 + VisitDepth float64 `json:"visit_depth"` // 平均访问深度 + } `json:"list"` +} + +// getAnalysisRetain 获取小程序访问数据趋势(日、月、周) +func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(urlStr, body) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("getAnalysisVisitTrend error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势 +func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { + return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate) +} + +// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据日趋势 +func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { + return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate) +} + +// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据日趋势 +func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { + return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate) +} + +// UserPortraitItem 用户画像项目 +type UserPortraitItem struct { + ID int `json:"id"` // 属性值id + Name string `json:"name"` // 属性值名称 + AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景访问uv +} + +// UserPortrait 用户画像 +type UserPortrait struct { + Index int `json:"index"` // 分布类型 + Province []UserPortraitItem `json:"province"` // 省份,如北京、广东等 + City []UserPortraitItem `json:"city"` // 城市,如北京、广州等 + Genders []UserPortraitItem `json:"genders"` // 性别,包括男、女、未知 + Platforms []UserPortraitItem `json:"platforms"` // 终端类型,包括iPhone, android, 其他 + Devices []UserPortraitItem `json:"devices"` // 机型,如苹果iPhone 6, OPPO R9等 + Ages []UserPortraitItem `json:"ages"` // 年龄,包括17岁以下、18-24对等区间 +} + +// ResAnalysisUserPortrait 小程序新增或活跃用户的画像分布数据返回 +type ResAnalysisUserPortrait struct { + util.CommonError + RefDate string `json:"ref_date"` // 日期 + VisitUVNew UserPortrait `json:"visit_uv_new"` // 新用户画像 + VisitUV UserPortrait `json:"visit_uv"` // 活跃用户画像 +} + +// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据 +func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(getAnalysisUserPortraitURL, body) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("GetAnalysisUserPortrait error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// VisitDistributionIndexItem 访问分数数据结构 +type VisitDistributionIndexItem struct { + Key int `json:"key"` // 场景id + Value int `json:"value"` // 该场景id访问pv + AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景id访问uv +} + +// VisitDistributionIndex 访问分布单分布类型数据 +type VisitDistributionIndex struct { + Index string `json:"index"` // 分布类型 + ItemList []VisitDistributionIndexItem `json:"item_list"` // 分布数据列表 +} + +// ResAnalysisVisitDistribution 小程序访问分布数据返回 +type ResAnalysisVisitDistribution struct { + util.CommonError + RefDate string `json:"ref_date"` // 日期 + List []VisitDistributionIndex `json:"list"` // 数据列表 +} + +// GetAnalysisVisitDistribution 获取用户小程序访问分布数据 +func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("GetAnalysisVisitDistribution error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// VisitPageItem 访问单个页面的数据结构 +type VisitPageItem struct { + PagePath string `json:"page_path"` // 页面路径 + PageVisitPV int `json:"page_visit_pv"` // 访问次数 + PageVisitUV int `json:"page_visit_uv"` // 访问人数 + PageStaytimePV float64 `json:"page_staytime_pv"` // 次均停留时常 + EntrypagePV int `json:"entrypage_pv"` // 进入页次数 + ExitpagePV int `json:"exitpage_pv"` // 退出页次数 + PageSharePV int `json:"page_share_pv"` // 转发次数 + PageShareUV int `json:"page_share_uv"` // 转发人数 +} + +// ResAnalysisVisitPage 访问小程序页面访问数据返回 +type ResAnalysisVisitPage struct { + util.CommonError + RefDate string `json:"ref_date"` // 日期 + List []VisitPageItem `json:"list"` // 数据列表 +} + +// GetAnalysisVisitPage 获取小程序页面访问数据 +func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) { + body := map[string]string{ + "begin_date": beginDate, + "end_date": endDate, + } + response, err := wxa.fetchData(getAnalysisVisitPageURL, body) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("GetAnalysisVisitPage error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} diff --git a/miniprogram/miniprogram.go b/miniprogram/miniprogram.go new file mode 100644 index 0000000..7b5535c --- /dev/null +++ b/miniprogram/miniprogram.go @@ -0,0 +1,17 @@ +package miniprogram + +import ( + "github.com/silenceper/wechat/context" +) + +// MiniProgram struct extends context +type MiniProgram struct { + *context.Context +} + +// NewMiniProgram 实例化小程序接口 +func NewMiniProgram(context *context.Context) *MiniProgram { + miniProgram := new(MiniProgram) + miniProgram.Context = context + return miniProgram +} diff --git a/miniprogram/qrcode.go b/miniprogram/qrcode.go new file mode 100644 index 0000000..d73651c --- /dev/null +++ b/miniprogram/qrcode.go @@ -0,0 +1,91 @@ +package miniprogram + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/silenceper/wechat/util" +) + +const ( + createWXAQRCodeURL = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=%s" + getWXACodeURL = "https://api.weixin.qq.com/wxa/getwxacode?access_token=%s" + getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s" +) + +// QRCoder 小程序码参数 +type QRCoder struct { + // page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 + Page string `json:"page,omitempty"` + // path 扫码进入的小程序页面路径 + Path string `json:"path,omitempty"` + // width 图片宽度 + Width int `json:"width,omitempty"` + // scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) + Scene string `json:"scene,omitempty"` + // autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 + AutoColor bool `json:"auto_color,omitempty"` + // lineColor AutoColor 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"},十进制表示 + LineColor Color `json:"line_color,omitempty"` + // isHyaline 是否需要透明底色 + IsHyaline bool `json:"is_hyaline,omitempty"` +} + +// Color QRCode color +type Color struct { + R string `json:"r"` + G string `json:"g"` + B string `json:"b"` +} + +// fetchCode 请求并返回二维码二进制数据 +func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []byte, err error) { + var accessToken string + accessToken, err = wxa.GetAccessToken() + if err != nil { + return + } + + urlStr = fmt.Sprintf(urlStr, accessToken) + var contentType string + response, contentType, err = util.PostJSONWithRespContentType(urlStr, body) + if err != nil { + return + } + if strings.HasPrefix(contentType, "application/json") { + // 返回错误信息 + var result util.CommonError + err = json.Unmarshal(response, &result) + if err == nil && result.ErrCode != 0 { + err = fmt.Errorf("fetchCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return nil, err + } + } else if contentType == "image/jpeg" { + // 返回文件 + return response, nil + } else { + err = fmt.Errorf("fetchCode error : unknown response content type - %v", contentType) + return nil, err + } + + return +} + +// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景 +// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html +func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) { + return wxa.fetchCode(createWXAQRCodeURL, coderParams) +} + +// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景 +// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html +func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) { + return wxa.fetchCode(getWXACodeURL, coderParams) +} + +// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景 +// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html +func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) { + return wxa.fetchCode(getWXACodeUnlimitURL, coderParams) +} diff --git a/miniprogram/sns.go b/miniprogram/sns.go new file mode 100644 index 0000000..03b939b --- /dev/null +++ b/miniprogram/sns.go @@ -0,0 +1,40 @@ +package miniprogram + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/util" +) + +const ( + code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code" +) + +// ResCode2Session 登录凭证校验的返回结果 +type ResCode2Session struct { + util.CommonError + + OpenID string `json:"openid"` // 用户唯一标识 + SessionKey string `json:"session_key"` // 会话密钥 + UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回 +} + +// Code2Session 登录凭证校验 +func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) { + urlStr := fmt.Sprintf(code2SessionURL, wxa.AppID, wxa.AppSecret, jsCode) + var response []byte + response, err = util.HTTPGet(urlStr) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("Code2Session error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} diff --git a/wechat.go b/wechat.go index 19856b8..b487404 100644 --- a/wechat.go +++ b/wechat.go @@ -4,6 +4,7 @@ import ( "net/http" "sync" + "github.com/JefferyWang/wechat/miniprogram" "github.com/silenceper/wechat/cache" "github.com/silenceper/wechat/context" "github.com/silenceper/wechat/js" @@ -99,3 +100,8 @@ func (wc *Wechat) GetTemplate() *template.Template { func (wc *Wechat) GetPay() *pay.Pay { return pay.NewPay(wc.Context) } + +// GetMiniProgram 获取小程序的实例 +func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram { + return miniprogram.NewMiniProgram(wc.Context) +} From 61476d351dee8b3b511f0e1ed5718c8caf276c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jefferwang=28=E7=8E=8B=E4=BF=8A=E9=94=8B=29?= Date: Tue, 19 Feb 2019 15:11:45 +0800 Subject: [PATCH 08/25] add PostJSONWithRespContentType --- miniprogram/qrcode.go | 2 +- util/http.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/miniprogram/qrcode.go b/miniprogram/qrcode.go index d73651c..a7f3aac 100644 --- a/miniprogram/qrcode.go +++ b/miniprogram/qrcode.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/silenceper/wechat/util" + "github.com/JefferyWang/wechat/util" ) const ( diff --git a/util/http.go b/util/http.go index 6881052..006f916 100644 --- a/util/http.go +++ b/util/http.go @@ -50,6 +50,32 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) { return ioutil.ReadAll(response.Body) } +// PostJSONWithRespContentType post json数据请求,且返回数据类型 +func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, error) { + jsonData, err := json.Marshal(obj) + if err != nil { + return nil, "", err + } + + jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1) + jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1) + jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1) + + body := bytes.NewBuffer(jsonData) + response, err := http.Post(uri, "application/json;charset=utf-8", body) + if err != nil { + return nil, "", err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) + } + responseData, err := ioutil.ReadAll(response.Body) + contentType := response.Header.Get("Content-Type") + return responseData, contentType, err +} + //PostFile 上传文件 func PostFile(fieldname, filename, uri string) ([]byte, error) { fields := []MultipartFormField{ From 5677b60759a3922e30ba872ba34fe4572275d5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jefferwang=28=E7=8E=8B=E4=BF=8A=E9=94=8B=29?= Date: Tue, 19 Feb 2019 16:41:15 +0800 Subject: [PATCH 09/25] update readme and some comments --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++ miniprogram/analysis.go | 6 +-- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 030926c..adfc050 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Cache主要用来保存全局access_token以及js-sdk中的ticket: - 检验access_token是否有效 - 获取js-sdk配置 - [素材管理](#素材管理) +- [小程序开发](#小程序开发) ## 消息管理 @@ -529,6 +530,110 @@ type Config struct { [素材管理API](https://godoc.org/github.com/silenceper/wechat/material#Material) +## 小程序开发 + +获取小程序操作对象 + +``` go +memCache=cache.NewMemcache("127.0.0.1:11211") +config := &wechat.Config{ + AppID: "xxx", + AppSecret: "xxx", + Cache: memCache=cache.NewMemcache("127.0.0.1:11211"), +} +wc := wechat.NewWechat(config) + +wxa := wc.GetMiniProgram() +``` + +### 小程序登录凭证校验 + +``` go +func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) +``` + +### 小程序数据统计 + +**获取用户访问小程序日留存** + +``` go +func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) +``` + +**获取用户访问小程序月留存** + +``` go +func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) +``` + +**获取用户访问小程序周留存** + +``` go +func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) +``` + +**获取用户访问小程序数据概况** + +``` go +func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) +``` + +**获取用户访问小程序数据日趋势** + +``` go +func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) +``` + +**获取用户访问小程序数据月趋势** + +``` go +func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) +``` + +**获取用户访问小程序数据周趋势** + +``` go +func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) +``` + +**获取小程序新增或活跃用户的画像分布数据** + +``` go +func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) +``` + +**获取用户小程序访问分布数据** + +``` go +func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) +``` + +**获取小程序页面访问数据** + +``` go +func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) +``` + +### 小程序二维码生成 + +**获取小程序二维码,适用于需要的码数量较少的业务场景** + +``` go +func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) +``` + +**获取小程序码,适用于需要的码数量较少的业务场景** + +``` go +func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) +``` + +**获取小程序码,适用于需要的码数量极多的业务场景** + +``` go +func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) +``` + 更多API使用请参考 godoc : [https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat) diff --git a/miniprogram/analysis.go b/miniprogram/analysis.go index a87d53a..4c56894 100644 --- a/miniprogram/analysis.go +++ b/miniprogram/analysis.go @@ -87,7 +87,7 @@ func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (res return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate) } -// GetAnalysisWeeklyRetain 获取用户访问小程序日留存 +// GetAnalysisWeeklyRetain 获取用户访问小程序周留存 func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) { return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate) } @@ -166,12 +166,12 @@ func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (r return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate) } -// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据日趋势 +// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势 func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate) } -// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据日趋势 +// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势 func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) { return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate) } From 862e54636796f0bfa84187aa7b3346bed8a90499 Mon Sep 17 00:00:00 2001 From: silenceper Date: Mon, 25 Feb 2019 14:07:27 +0800 Subject: [PATCH 10/25] Update component_access_token.go update import path --- context/component_access_token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/component_access_token.go b/context/component_access_token.go index 1acb0a0..5f263f5 100644 --- a/context/component_access_token.go +++ b/context/component_access_token.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/funxdata/wechat/util" + "github.com/silenceper/wechat/util" ) const ( From 4f5945fb0fb1a3ba19b7adbe6644cc30f15c2093 Mon Sep 17 00:00:00 2001 From: sunyaqiu Date: Sat, 6 Apr 2019 14:52:05 +0800 Subject: [PATCH 11/25] add refund --- pay/refund.go | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 pay/refund.go diff --git a/pay/refund.go b/pay/refund.go new file mode 100644 index 0000000..10d313c --- /dev/null +++ b/pay/refund.go @@ -0,0 +1,180 @@ +package pay + +import ( + "bytes" + "crypto/tls" + "encoding/pem" + "encoding/xml" + "fmt" + "github.com/akikistyle/wechat/util" + "golang.org/x/crypto/pkcs12" + "io/ioutil" + "log" + "net/http" +) + +var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund" + +//Refund Parameter +type RefundParams struct { + TransactionId string + OutRefundNo string + TotalFee string + RefundFee string + RefundDesc string + RootCa string //ca证书 +} + +//Refund request +type refundRequest struct { + AppID string `xml:"appid"` + MchID string `xml:"mch_id"` + NonceStr string `xml:"nonce_str"` + Sign string `xml:"sign"` + SignType string `xml:"sign_type,omitempty"` + TransactionId string `xml:"transaction_id"` + OutRefundNo string `xml:"out_refund_no"` + TotalFee string `xml:"total_fee"` + RefundFee string `xml:"refund_fee"` + RefundDesc string `xml:"refund_desc,omitempty"` + //NotifyUrl string `xml:"notify_url,omitempty"` +} + +//Refund Response +type RefundResponse struct { + ReturnCode string `xml:"return_code"` + ReturnMsg string `xml:"return_msg"` + AppID string `xml:"appid,omitempty"` + MchID string `xml:"mch_id,omitempty"` + NonceStr string `xml:"nonce_str,omitempty"` + Sign string `xml:"sign,omitempty"` + ResultCode string `xml:"result_code,omitempty"` + ErrCode string `xml:"err_code,omitempty"` + ErrCodeDes string `xml:"err_code_des,omitempty"` + TransactionId string `xml:"transaction_id,omitempty"` + OutTradeNo string `xml:"out_trade_no,omitempty"` + OutRefundNo string `xml:"out_refund_no,omitempty"` + RefundId string `xml:"refund_id,omitempty"` + RefundFee string `xml:"refund_fee,omitempty"` + SettlementRefundFee string `xml:"settlement_refund_fee,omitempty"` + TotalFee string `xml:"total_fee,omitempty"` + SettlementTotalFee string `xml:"settlement_total_fee,omitempty"` + FeeType string `xml:"fee_type,omitempty"` + CashFee string `xml:"cash_fee,omitempty"` + CashFeeType string `xml:"cash_fee_type,omitempty"` +} + +//退款申请 +func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { + nonceStr := util.RandomStr(32) + param := make(map[string]interface{}) + param["appid"] = pcf.AppID + param["mch_id"] = pcf.PayMchID + param["nonce_str"] = nonceStr + param["out_refund_no"] = p.OutRefundNo + param["refund_desc"] = p.RefundDesc + param["refund_fee"] = p.RefundFee + param["total_fee"] = p.TotalFee + param["sign_type"] = "MD5" + param["transaction_id"] = p.TransactionId + + bizKey := "&key=" + pcf.PayKey + str := orderParam(param, bizKey) + sign := util.MD5Sum(str) + request := refundRequest{ + AppID: pcf.AppID, + MchID: pcf.PayMchID, + NonceStr: nonceStr, + Sign: sign, + SignType: "MD5", + TransactionId: p.TransactionId, + OutRefundNo: p.OutRefundNo, + TotalFee: p.TotalFee, + RefundFee: p.RefundFee, + RefundDesc: p.RefundDesc, + } + rawRet, err := postXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID) + if err != nil { + return + } + err = xml.Unmarshal(rawRet, &rsp) + if err != nil { + return + } + if rsp.ReturnCode == "SUCCESS" { + if rsp.ResultCode == "SUCCESS" { + err = nil + return + } + err = fmt.Errorf("refund error, errcode=%s,errmsg=%s", rsp.ErrCode, rsp.ErrCodeDes) + return + } + err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [params : %s] [sign : %s]", + string(rawRet), str, sign) + return +} + +//http TLS +func httpWithTLS(rootCa, key string) (*http.Client, error) { + var client *http.Client + certData, err := ioutil.ReadFile(rootCa) + if err != nil { + return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err) + } + cert := pkcs12ToPem(certData, key) + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + tr := &http.Transport{ + TLSClientConfig: config, + DisableCompression: true, + } + client = &http.Client{Transport: tr} + return client, nil +} + +//将Pkcs12转成Pem +func pkcs12ToPem(p12 []byte, password string) tls.Certificate { + blocks, err := pkcs12.ToPEM(p12, password) + defer func() { + if x := recover(); x != nil { + log.Print(x) + } + }() + if err != nil { + panic(err) + } + var pemData []byte + for _, b := range blocks { + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + cert, err := tls.X509KeyPair(pemData, pemData) + if err != nil { + panic(err) + } + return cert +} + +//Post XML with TLS +func postXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { + xmlData, err := xml.Marshal(obj) + if err != nil { + return nil, err + } + + body := bytes.NewBuffer(xmlData) + client, err := httpWithTLS(ca, key) + if err != nil { + return nil, err + } + response, err := client.Post(uri, "application/xml;charset=utf-8", body) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode) + } + return ioutil.ReadAll(response.Body) +} From 02b3fcc6487c097aebf25e828ff916802042658a Mon Sep 17 00:00:00 2001 From: sunyaqiu Date: Sat, 6 Apr 2019 14:59:38 +0800 Subject: [PATCH 12/25] add refund --- pay/refund.go | 74 +-------------------------------------------------- util/http.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 73 deletions(-) diff --git a/pay/refund.go b/pay/refund.go index 10d313c..18c71f0 100644 --- a/pay/refund.go +++ b/pay/refund.go @@ -1,16 +1,9 @@ package pay import ( - "bytes" - "crypto/tls" - "encoding/pem" "encoding/xml" "fmt" "github.com/akikistyle/wechat/util" - "golang.org/x/crypto/pkcs12" - "io/ioutil" - "log" - "net/http" ) var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund" @@ -93,7 +86,7 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { RefundFee: p.RefundFee, RefundDesc: p.RefundDesc, } - rawRet, err := postXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID) + rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID) if err != nil { return } @@ -113,68 +106,3 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { string(rawRet), str, sign) return } - -//http TLS -func httpWithTLS(rootCa, key string) (*http.Client, error) { - var client *http.Client - certData, err := ioutil.ReadFile(rootCa) - if err != nil { - return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err) - } - cert := pkcs12ToPem(certData, key) - config := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - tr := &http.Transport{ - TLSClientConfig: config, - DisableCompression: true, - } - client = &http.Client{Transport: tr} - return client, nil -} - -//将Pkcs12转成Pem -func pkcs12ToPem(p12 []byte, password string) tls.Certificate { - blocks, err := pkcs12.ToPEM(p12, password) - defer func() { - if x := recover(); x != nil { - log.Print(x) - } - }() - if err != nil { - panic(err) - } - var pemData []byte - for _, b := range blocks { - pemData = append(pemData, pem.EncodeToMemory(b)...) - } - cert, err := tls.X509KeyPair(pemData, pemData) - if err != nil { - panic(err) - } - return cert -} - -//Post XML with TLS -func postXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { - xmlData, err := xml.Marshal(obj) - if err != nil { - return nil, err - } - - body := bytes.NewBuffer(xmlData) - client, err := httpWithTLS(ca, key) - if err != nil { - return nil, err - } - response, err := client.Post(uri, "application/xml;charset=utf-8", body) - if err != nil { - return nil, err - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode) - } - return ioutil.ReadAll(response.Body) -} diff --git a/util/http.go b/util/http.go index 6881052..bc3ed8d 100644 --- a/util/http.go +++ b/util/http.go @@ -2,11 +2,15 @@ package util import ( "bytes" + "crypto/tls" "encoding/json" + "encoding/pem" "encoding/xml" "fmt" + "golang.org/x/crypto/pkcs12" "io" "io/ioutil" + "log" "mime/multipart" "net/http" "os" @@ -141,3 +145,68 @@ func PostXML(uri string, obj interface{}) ([]byte, error) { } return ioutil.ReadAll(response.Body) } + +//http TLS +func httpWithTLS(rootCa, key string) (*http.Client, error) { + var client *http.Client + certData, err := ioutil.ReadFile(rootCa) + if err != nil { + return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err) + } + cert := pkcs12ToPem(certData, key) + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + tr := &http.Transport{ + TLSClientConfig: config, + DisableCompression: true, + } + client = &http.Client{Transport: tr} + return client, nil +} + +//将Pkcs12转成Pem +func pkcs12ToPem(p12 []byte, password string) tls.Certificate { + blocks, err := pkcs12.ToPEM(p12, password) + defer func() { + if x := recover(); x != nil { + log.Print(x) + } + }() + if err != nil { + panic(err) + } + var pemData []byte + for _, b := range blocks { + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + cert, err := tls.X509KeyPair(pemData, pemData) + if err != nil { + panic(err) + } + return cert +} + +//Post XML with TLS +func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { + xmlData, err := xml.Marshal(obj) + if err != nil { + return nil, err + } + + body := bytes.NewBuffer(xmlData) + client, err := httpWithTLS(ca, key) + if err != nil { + return nil, err + } + response, err := client.Post(uri, "application/xml;charset=utf-8", body) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode) + } + return ioutil.ReadAll(response.Body) +} From f4f1860e678814aa459b65d4bc0673021c6728f2 Mon Sep 17 00:00:00 2001 From: sunyaqiu Date: Sat, 6 Apr 2019 15:24:40 +0800 Subject: [PATCH 13/25] fix some comment and struct field --- pay/refund.go | 20 ++++++++++---------- util/http.go | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pay/refund.go b/pay/refund.go index 18c71f0..b401990 100644 --- a/pay/refund.go +++ b/pay/refund.go @@ -8,9 +8,9 @@ import ( var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund" -//Refund Parameter +//RefundParams 调用参数 type RefundParams struct { - TransactionId string + TransactionID string OutRefundNo string TotalFee string RefundFee string @@ -18,14 +18,14 @@ type RefundParams struct { RootCa string //ca证书 } -//Refund request +//refundRequest 接口请求参数 type refundRequest struct { AppID string `xml:"appid"` MchID string `xml:"mch_id"` NonceStr string `xml:"nonce_str"` Sign string `xml:"sign"` SignType string `xml:"sign_type,omitempty"` - TransactionId string `xml:"transaction_id"` + TransactionID string `xml:"transaction_id"` OutRefundNo string `xml:"out_refund_no"` TotalFee string `xml:"total_fee"` RefundFee string `xml:"refund_fee"` @@ -33,7 +33,7 @@ type refundRequest struct { //NotifyUrl string `xml:"notify_url,omitempty"` } -//Refund Response +//RefundResponse 接口返回 type RefundResponse struct { ReturnCode string `xml:"return_code"` ReturnMsg string `xml:"return_msg"` @@ -44,10 +44,10 @@ type RefundResponse struct { ResultCode string `xml:"result_code,omitempty"` ErrCode string `xml:"err_code,omitempty"` ErrCodeDes string `xml:"err_code_des,omitempty"` - TransactionId string `xml:"transaction_id,omitempty"` + TransactionID string `xml:"transaction_id,omitempty"` OutTradeNo string `xml:"out_trade_no,omitempty"` OutRefundNo string `xml:"out_refund_no,omitempty"` - RefundId string `xml:"refund_id,omitempty"` + RefundID string `xml:"refund_id,omitempty"` RefundFee string `xml:"refund_fee,omitempty"` SettlementRefundFee string `xml:"settlement_refund_fee,omitempty"` TotalFee string `xml:"total_fee,omitempty"` @@ -57,7 +57,7 @@ type RefundResponse struct { CashFeeType string `xml:"cash_fee_type,omitempty"` } -//退款申请 +//Refund 退款申请 func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { nonceStr := util.RandomStr(32) param := make(map[string]interface{}) @@ -69,7 +69,7 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { param["refund_fee"] = p.RefundFee param["total_fee"] = p.TotalFee param["sign_type"] = "MD5" - param["transaction_id"] = p.TransactionId + param["transaction_id"] = p.TransactionID bizKey := "&key=" + pcf.PayKey str := orderParam(param, bizKey) @@ -80,7 +80,7 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) { NonceStr: nonceStr, Sign: sign, SignType: "MD5", - TransactionId: p.TransactionId, + TransactionID: p.TransactionID, OutRefundNo: p.OutRefundNo, TotalFee: p.TotalFee, RefundFee: p.RefundFee, diff --git a/util/http.go b/util/http.go index bc3ed8d..086e65f 100644 --- a/util/http.go +++ b/util/http.go @@ -146,7 +146,7 @@ func PostXML(uri string, obj interface{}) ([]byte, error) { return ioutil.ReadAll(response.Body) } -//http TLS +//httpWithTLS CA证书 func httpWithTLS(rootCa, key string) (*http.Client, error) { var client *http.Client certData, err := ioutil.ReadFile(rootCa) @@ -165,7 +165,7 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) { return client, nil } -//将Pkcs12转成Pem +//pkcs12ToPem 将Pkcs12转成Pem func pkcs12ToPem(p12 []byte, password string) tls.Certificate { blocks, err := pkcs12.ToPEM(p12, password) defer func() { @@ -187,7 +187,7 @@ func pkcs12ToPem(p12 []byte, password string) tls.Certificate { return cert } -//Post XML with TLS +//PostXMLWithTLS:Post XML with TLS func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { xmlData, err := xml.Marshal(obj) if err != nil { From e66652f4b5d81017422dfd4717bd0212577f31d7 Mon Sep 17 00:00:00 2001 From: sunyaqiu Date: Sat, 6 Apr 2019 15:28:31 +0800 Subject: [PATCH 14/25] fix comment --- util/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/http.go b/util/http.go index 086e65f..2376155 100644 --- a/util/http.go +++ b/util/http.go @@ -187,7 +187,7 @@ func pkcs12ToPem(p12 []byte, password string) tls.Certificate { return cert } -//PostXMLWithTLS:Post XML with TLS +//PostXMLWithTLS perform a HTTP/POST request with XML body and TLS func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) { xmlData, err := xml.Marshal(obj) if err != nil { From 823c54fda5b838940e29b021f74a8ab4f18a89c1 Mon Sep 17 00:00:00 2001 From: JefferyWang Date: Wed, 10 Apr 2019 17:26:21 +0800 Subject: [PATCH 15/25] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E8=A7=A3=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/decrypt.go | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 miniprogram/decrypt.go diff --git a/miniprogram/decrypt.go b/miniprogram/decrypt.go new file mode 100644 index 0000000..6897368 --- /dev/null +++ b/miniprogram/decrypt.go @@ -0,0 +1,93 @@ +package miniprogram + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "errors" +) + +var ( + // ErrAppIDNotMatch appid不匹配 + ErrAppIDNotMatch = errors.New("app id not match") + // ErrInvalidBlockSize block size不合法 + ErrInvalidBlockSize = errors.New("invalid block size") + // ErrInvalidPKCS7Data PKCS7数据不合法 + ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data") + // ErrInvalidPKCS7Padding 输入padding失败 + ErrInvalidPKCS7Padding = errors.New("invalid padding on input") +) + +// UserInfo 用户信息 +type UserInfo struct { + OpenID string `json:"openId"` + UnionID string `json:"unionId"` + NickName string `json:"nickName"` + Gender int `json:"gender"` + City string `json:"city"` + Province string `json:"province"` + Country string `json:"country"` + AvatarURL string `json:"avatarUrl"` + Language string `json:"language"` + Watermark struct { + Timestamp int64 `json:"timestamp"` + AppID string `json:"appid"` + } `json:"watermark"` +} + +// pkcs7Unpad returns slice of the original data without padding +func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { + if blockSize <= 0 { + return nil, ErrInvalidBlockSize + } + if len(data)%blockSize != 0 || len(data) == 0 { + return nil, ErrInvalidPKCS7Data + } + c := data[len(data)-1] + n := int(c) + if n == 0 || n > len(data) { + return nil, ErrInvalidPKCS7Padding + } + for i := 0; i < n; i++ { + if data[len(data)-n+i] != c { + return nil, ErrInvalidPKCS7Padding + } + } + return data[:len(data)-n], nil +} + +// Decrypt 解密数据 +func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) { + aesKey, err := base64.StdEncoding.DecodeString(sessionKey) + if err != nil { + return nil, err + } + cipherText, err := base64.StdEncoding.DecodeString(encryptedData) + if err != nil { + return nil, err + } + ivBytes, err := base64.StdEncoding.DecodeString(iv) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + mode := cipher.NewCBCDecrypter(block, ivBytes) + mode.CryptBlocks(cipherText, cipherText) + cipherText, err = pkcs7Unpad(cipherText, block.BlockSize()) + if err != nil { + return nil, err + } + var userInfo UserInfo + err = json.Unmarshal(cipherText, &userInfo) + if err != nil { + return nil, err + } + if userInfo.Watermark.AppID != wxa.AppID { + return nil, ErrAppIDNotMatch + } + return &userInfo, nil +} From 3005852946e8fc72957e6508f456abc2be60593c Mon Sep 17 00:00:00 2001 From: silenceper Date: Tue, 23 Apr 2019 21:19:12 +0800 Subject: [PATCH 16/25] fix import path --- miniprogram/qrcode.go | 2 +- pay/refund.go | 3 ++- wechat.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/miniprogram/qrcode.go b/miniprogram/qrcode.go index a7f3aac..d73651c 100644 --- a/miniprogram/qrcode.go +++ b/miniprogram/qrcode.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/JefferyWang/wechat/util" + "github.com/silenceper/wechat/util" ) const ( diff --git a/pay/refund.go b/pay/refund.go index b401990..5d55771 100644 --- a/pay/refund.go +++ b/pay/refund.go @@ -3,7 +3,8 @@ package pay import ( "encoding/xml" "fmt" - "github.com/akikistyle/wechat/util" + + "github.com/silenceper/wechat/util" ) var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund" diff --git a/wechat.go b/wechat.go index 142d7ac..f49ece9 100644 --- a/wechat.go +++ b/wechat.go @@ -4,12 +4,12 @@ import ( "net/http" "sync" - "github.com/JefferyWang/wechat/miniprogram" "github.com/silenceper/wechat/cache" "github.com/silenceper/wechat/context" "github.com/silenceper/wechat/js" "github.com/silenceper/wechat/material" "github.com/silenceper/wechat/menu" + "github.com/silenceper/wechat/miniprogram" "github.com/silenceper/wechat/oauth" "github.com/silenceper/wechat/pay" "github.com/silenceper/wechat/qr" From 3fc556c425ada2ba9f436a4d6a4ec923dc0bbb93 Mon Sep 17 00:00:00 2001 From: silenceper Date: Tue, 23 Apr 2019 21:21:31 +0800 Subject: [PATCH 17/25] add vendor --- vendor/vendor.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/vendor/vendor.json b/vendor/vendor.json index 3b868d0..5a8e1ad 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -80,6 +80,18 @@ "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", "revisionTime": "2016-11-17T03:31:26Z" }, + { + "checksumSHA1": "w3QCCIYHgZzIXQ+xTl7oLfFrXHs=", + "path": "github.com/gomodule/redigo/internal", + "revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace", + "revisionTime": "2018-06-27T14:45:07Z" + }, + { + "checksumSHA1": "To/N5YA/FD0Rrs6r2OOmHXgxYwI=", + "path": "github.com/gomodule/redigo/redis", + "revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace", + "revisionTime": "2018-06-27T14:45:07Z" + }, { "checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=", "path": "github.com/manucorporat/sse", @@ -92,6 +104,18 @@ "revision": "30a891c33c7cde7b02a981314b4228ec99380cca", "revisionTime": "2016-11-23T14:36:37Z" }, + { + "checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=", + "path": "golang.org/x/crypto/pkcs12", + "revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94", + "revisionTime": "2017-09-21T17:41:56Z" + }, + { + "checksumSHA1": "iVJcif9M9uefvvqHCNR9VQrjc/s=", + "path": "golang.org/x/crypto/pkcs12/internal/rc2", + "revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94", + "revisionTime": "2017-09-21T17:41:56Z" + }, { "checksumSHA1": "pancewZW3HwGvpDwfH5Imrbadc4=", "path": "golang.org/x/net/context", From fdd9768a967bdb123e1de0462431ded3accb03bd Mon Sep 17 00:00:00 2001 From: zdpdpdp Date: Thu, 16 May 2019 15:27:26 +0800 Subject: [PATCH 18/25] add: user.info subscribe info --- user/user.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/user/user.go b/user/user.go index 9cf4851..2196ed6 100644 --- a/user/user.go +++ b/user/user.go @@ -31,20 +31,23 @@ func NewUser(context *context.Context) *User { type Info struct { util.CommonError - Subscribe int32 `json:"subscribe"` - OpenID string `json:"openid"` - Nickname string `json:"nickname"` - Sex int32 `json:"sex"` - City string `json:"city"` - Country string `json:"country"` - Province string `json:"province"` - Language string `json:"language"` - Headimgurl string `json:"headimgurl"` - SubscribeTime int32 `json:"subscribe_time"` - UnionID string `json:"unionid"` - Remark string `json:"remark"` - GroupID int32 `json:"groupid"` - TagidList []int32 `json:"tagid_list"` + Subscribe int32 `json:"subscribe"` + OpenID string `json:"openid"` + Nickname string `json:"nickname"` + Sex int32 `json:"sex"` + City string `json:"city"` + Country string `json:"country"` + Province string `json:"province"` + Language string `json:"language"` + Headimgurl string `json:"headimgurl"` + SubscribeTime int32 `json:"subscribe_time"` + UnionID string `json:"unionid"` + Remark string `json:"remark"` + GroupID int32 `json:"groupid"` + TagidList []int32 `json:"tagid_list"` + SubscribeScene string `json:"subscribe_scene"` + QrScene int `json:"qr_scene"` + QrSceneStr string `json:"qr_scene_str"` } // OpenidList 用户列表 From 81f26cd6dc80eb51df38335b0c46290f94f8a386 Mon Sep 17 00:00:00 2001 From: zdpdpdp Date: Tue, 21 May 2019 12:47:31 +0800 Subject: [PATCH 19/25] =?UTF-8?q?add:=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=20access=20token=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- context/access_token.go | 10 ++++++++++ context/access_token_test.go | 30 ++++++++++++++++++++++++++++++ context/context.go | 3 +++ 3 files changed, 43 insertions(+) create mode 100644 context/access_token_test.go diff --git a/context/access_token.go b/context/access_token.go index a7f7810..22b9639 100644 --- a/context/access_token.go +++ b/context/access_token.go @@ -22,16 +22,26 @@ type ResAccessToken struct { ExpiresIn int64 `json:"expires_in"` } +type CustomGetAccessToken func(ctx *Context) (accessToken string, err error) + //SetAccessTokenLock 设置读写锁(一个appID一个读写锁) func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) { ctx.accessTokenLock = l } +//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存 +func (ctx *Context) SetGetAccessTokenFunc(f CustomGetAccessToken) { + ctx.accessTokenFunc = f +} + //GetAccessToken 获取access_token func (ctx *Context) GetAccessToken() (accessToken string, err error) { ctx.accessTokenLock.Lock() defer ctx.accessTokenLock.Unlock() + if ctx.accessTokenFunc != nil { + return ctx.accessTokenFunc(ctx) + } accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID) val := ctx.Cache.Get(accessTokenCacheKey) if val != nil { diff --git a/context/access_token_test.go b/context/access_token_test.go new file mode 100644 index 0000000..fdae218 --- /dev/null +++ b/context/access_token_test.go @@ -0,0 +1,30 @@ +package context + +import ( + "sync" + "testing" +) + +func TestContext_SetCustomAccessTokenFunc(t *testing.T) { + ctx := Context{ + accessTokenLock: new(sync.RWMutex), + } + f := func(ctx *Context) (accessToken string, err error) { + return "fake token", nil + } + ctx.SetGetAccessTokenFunc(f) + res, err := ctx.GetAccessToken() + if res != "fake token" || err != nil { + t.Error("expect fake token but error") + } +} + +func TestContext_NoSetCustomAccessTokenFunc(t *testing.T) { + ctx := Context{ + accessTokenLock: new(sync.RWMutex), + } + + if ctx.accessTokenFunc != nil { + t.Error("error accessTokenFunc") + } +} diff --git a/context/context.go b/context/context.go index 45bcf50..bd0591c 100644 --- a/context/context.go +++ b/context/context.go @@ -27,6 +27,9 @@ type Context struct { //jsAPITicket 读写锁 同一个AppID一个 jsAPITicketLock *sync.RWMutex + + //自定义获取 access token 的方法 + accessTokenFunc CustomGetAccessToken } // Query returns the keyed url query value if it exists From 9a4d41563ea6804e732674b06c837dbea5fcefe4 Mon Sep 17 00:00:00 2001 From: zdpdpdp Date: Tue, 21 May 2019 13:03:07 +0800 Subject: [PATCH 20/25] =?UTF-8?q?add:=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=20access=20token=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- context/access_token.go | 5 +++-- context/context.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/context/access_token.go b/context/access_token.go index 22b9639..7405771 100644 --- a/context/access_token.go +++ b/context/access_token.go @@ -22,7 +22,8 @@ type ResAccessToken struct { ExpiresIn int64 `json:"expires_in"` } -type CustomGetAccessToken func(ctx *Context) (accessToken string, err error) +//GetAccessTokenFunc 获取 access token 的函数签名 +type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error) //SetAccessTokenLock 设置读写锁(一个appID一个读写锁) func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) { @@ -30,7 +31,7 @@ func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) { } //SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存 -func (ctx *Context) SetGetAccessTokenFunc(f CustomGetAccessToken) { +func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) { ctx.accessTokenFunc = f } diff --git a/context/context.go b/context/context.go index bd0591c..07e42c3 100644 --- a/context/context.go +++ b/context/context.go @@ -28,8 +28,8 @@ type Context struct { //jsAPITicket 读写锁 同一个AppID一个 jsAPITicketLock *sync.RWMutex - //自定义获取 access token 的方法 - accessTokenFunc CustomGetAccessToken + //accessTokenFunc 自定义获取 access token 的方法 + accessTokenFunc GetAccessTokenFunc } // Query returns the keyed url query value if it exists From 529323e6b5786802e309d79a7af4edf885076e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=BB=E5=86=A0=E5=BC=9B?= Date: Wed, 22 May 2019 11:51:24 +0800 Subject: [PATCH 21/25] =?UTF-8?q?fix=20README=F0=9F=98=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index adfc050..18cc85f 100644 --- a/README.md +++ b/README.md @@ -336,14 +336,14 @@ Url :点击图文消息跳转链接 ## 自定义菜单 -通过` wechat.GetMenu(req, writer)`获取menu的实例 +通过` wechat.GetMenu()`获取menu的实例 ### 自定义菜单创建接口 以下是一个创建二级菜单的例子 ```go -mu := wc.GetMenu(c.Request, c.Writer) +mu := wc.GetMenu() buttons := make([]*menu.Button, 1) btn := new(menu.Button) @@ -403,7 +403,7 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) { ### 自定义菜单查询接口 ```go -mu := wc.GetMenu(c.Request, c.Writer) +mu := wc.GetMenu() resMenu,err:=mu.GetMenu() ``` >返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体 @@ -411,7 +411,7 @@ resMenu,err:=mu.GetMenu() ### 自定义菜单删除接口 ```go -mu := wc.GetMenu(c.Request, c.Writer) +mu := wc.GetMenu() err:=mu.DeleteMenu() ``` @@ -459,7 +459,7 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err **1.发起授权** ```go -oauth := wc.GetOauth(c.Request, c.Writer) +oauth := wc.GetOauth() err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123") if err != nil { fmt.Println(err) @@ -506,7 +506,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er ### 获取js-sdk配置 ```go -js := wc.GetJs(c.Request, c.Writer) +js := wc.GetJs() cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址") if err != nil { fmt.Println(err) From 44dae6e9509589d7230fdeb6ca4aa7e9d3e2e7f5 Mon Sep 17 00:00:00 2001 From: silenceper Date: Mon, 15 Jul 2019 18:14:53 +0800 Subject: [PATCH 22/25] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bd97ebb..1c5d540 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: go go: + - 1.12.x - 1.11.x - 1.10.x - - 1.9.x services: - memcached From 79ff0321e39c210371a0d1037530bcc3a1b1e8a6 Mon Sep 17 00:00:00 2001 From: silenceper Date: Mon, 15 Jul 2019 18:18:12 +0800 Subject: [PATCH 23/25] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c5d540..d983b50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ services: before_script: - GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/) - - go get github.com/golang/lint/golint + - go get golang.org/x/lint/golint script: - go test -v -race ./... From dbb43ac7ad535cef4a525be13edcbb206c843a91 Mon Sep 17 00:00:00 2001 From: silenceper Date: Mon, 15 Jul 2019 18:24:02 +0800 Subject: [PATCH 24/25] fix Unmarshal --- material/material.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/material/material.go b/material/material.go index 62a5215..f4f1dfb 100644 --- a/material/material.go +++ b/material/material.go @@ -63,7 +63,7 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken) responseBytes, err := util.PostJSON(uri, req) var res resArticles - err = json.Unmarshal(responseBytes, res) + err = json.Unmarshal(responseBytes, &res) if err != nil { return } From 1a9600b49fbdf95020514849dff06708169505fd Mon Sep 17 00:00:00 2001 From: silenceper Date: Mon, 15 Jul 2019 18:25:01 +0800 Subject: [PATCH 25/25] go fmt --- pay/pay.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pay/pay.go b/pay/pay.go index dd9fafd..1afa094 100644 --- a/pay/pay.go +++ b/pay/pay.go @@ -93,16 +93,16 @@ func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) { param["appid"] = pcf.AppID param["body"] = p.Body param["mch_id"] = pcf.PayMchID - param["nonce_str"] =nonceStr - param["notify_url"] =pcf.PayNotifyURL - param["out_trade_no"] =p.OutTradeNo - param["spbill_create_ip"] =p.CreateIP - param["total_fee"] =p.TotalFee - param["trade_type"] =p.TradeType + param["nonce_str"] = nonceStr + param["notify_url"] = pcf.PayNotifyURL + param["out_trade_no"] = p.OutTradeNo + param["spbill_create_ip"] = p.CreateIP + param["total_fee"] = p.TotalFee + param["trade_type"] = p.TradeType param["openid"] = p.OpenID - bizKey := "&key="+pcf.PayKey - str := orderParam(param,bizKey) + bizKey := "&key=" + pcf.PayKey + str := orderParam(param, bizKey) sign := util.MD5Sum(str) request := payRequest{ AppID: pcf.AppID,