diff --git a/.gitignore b/.gitignore index 36eec47..0c874bc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ _testmain.go .DS_Store .vscode/ vendor/*/ +.idea/ diff --git a/.travis.yml b/.travis.yml index 0738b0f..065893a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: go go: + - 1.11.x + - 1.10.x - 1.9.x - 1.8.x - - 1.7.x - - 1.6.x services: - memcached diff --git a/README.md b/README.md index 656a0d4..030926c 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ type Reply struct { } ``` -注意:`retrun nil`表示什么也不做 +注意:`return nil`表示什么也不做 #### 回复文本消息 ```go diff --git a/cache/redis.go b/cache/redis.go index 9d102b0..7440b9e 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -4,7 +4,7 @@ import ( "encoding/json" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" ) //Redis redis cache @@ -45,6 +45,11 @@ func NewRedis(opts *RedisOpts) *Redis { return &Redis{pool} } +//SetConn 设置conn +func (r *Redis) SetConn(conn *redis.Pool) { + r.conn = conn +} + //Get 获取一个值 func (r *Redis) Get(key string) interface{} { conn := r.conn.Get() diff --git a/context/qy_access_token.go b/context/qy_access_token.go new file mode 100644 index 0000000..c76d35a --- /dev/null +++ b/context/qy_access_token.go @@ -0,0 +1,76 @@ +package context + +import ( + "encoding/json" + "fmt" + "log" + "sync" + "time" + + "github.com/silenceper/wechat/util" +) + +const ( + //qyAccessTokenURL 获取access_token的接口 + qyAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" +) + +//ResQyAccessToken struct +type ResQyAccessToken struct { + util.CommonError + + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` +} + +//SetQyAccessTokenLock 设置读写锁(一个appID一个读写锁) +func (ctx *Context) SetQyAccessTokenLock(l *sync.RWMutex) { + ctx.accessTokenLock = l +} + +//GetQyAccessToken 获取access_token +func (ctx *Context) GetQyAccessToken() (accessToken string, err error) { + ctx.accessTokenLock.Lock() + defer ctx.accessTokenLock.Unlock() + + accessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID) + val := ctx.Cache.Get(accessTokenCacheKey) + if val != nil { + accessToken = val.(string) + return + } + + //从微信服务器获取 + var resQyAccessToken ResQyAccessToken + resQyAccessToken, err = ctx.GetQyAccessTokenFromServer() + if err != nil { + return + } + + accessToken = resQyAccessToken.AccessToken + return +} + +//GetQyAccessTokenFromServer 强制从微信服务器获取token +func (ctx *Context) GetQyAccessTokenFromServer() (resQyAccessToken ResQyAccessToken, err error) { + log.Printf("GetQyAccessTokenFromServer") + url := fmt.Sprintf(qyAccessTokenURL, ctx.AppID, ctx.AppSecret) + var body []byte + body, err = util.HTTPGet(url) + if err != nil { + return + } + err = json.Unmarshal(body, &resQyAccessToken) + if err != nil { + return + } + if resQyAccessToken.ErrCode != 0 { + err = fmt.Errorf("get qy_access_token error : errcode=%v , errormsg=%v", resQyAccessToken.ErrCode, resQyAccessToken.ErrMsg) + return + } + + qyAccessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID) + expires := resQyAccessToken.ExpiresIn - 1500 + err = ctx.Cache.Set(qyAccessTokenCacheKey, resQyAccessToken.AccessToken, time.Duration(expires)*time.Second) + return +} diff --git a/material/material.go b/material/material.go index a9c512e..62a5215 100644 --- a/material/material.go +++ b/material/material.go @@ -184,13 +184,6 @@ func (material *Material) DeleteMaterial(mediaID string) error { if err != nil { return err } - var resDeleteMaterial util.CommonError - err = json.Unmarshal(response, &resDeleteMaterial) - if err != nil { - return err - } - if resDeleteMaterial.ErrCode != 0 { - return fmt.Errorf("DeleteMaterial error : errcode=%v , errmsg=%v", resDeleteMaterial.ErrCode, resDeleteMaterial.ErrMsg) - } - return nil + + return util.DecodeWithCommonError(response, "DeleteMaterial") } diff --git a/material/media.go b/material/media.go index 4465605..70ff09e 100644 --- a/material/media.go +++ b/material/media.go @@ -31,10 +31,10 @@ const ( type Media struct { util.CommonError - Type MediaType `json:"type"` - MediaID string `json:"media_id"` - ThumbMediaID string `json:"thumb_media_id"` - CreatedAt int64 `json:"created_at"` + Type MediaType `json:"type"` + MediaID string `json:"media_id"` + ThumbMediaID string `json:"thumb_media_id"` + CreatedAt int64 `json:"created_at"` } //MediaUpload 临时素材上传 diff --git a/menu/button.go b/menu/button.go index f7c293f..ec67b35 100644 --- a/menu/button.go +++ b/menu/button.go @@ -7,6 +7,8 @@ type Button struct { Key string `json:"key,omitempty"` URL string `json:"url,omitempty"` MediaID string `json:"media_id,omitempty"` + AppID string `json:"appid,omitempty"` + PagePath string `json:"pagepath,omitempty"` SubButtons []*Button `json:"sub_button,omitempty"` } @@ -126,3 +128,16 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) { btn.URL = "" btn.SubButtons = nil } + +//SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序) +func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) { + btn.Type = "miniprogram" + btn.Name = name + btn.URL = url + btn.AppID = appID + btn.PagePath = pagePath + + btn.Key = "" + btn.MediaID = "" + btn.SubButtons = nil +} diff --git a/menu/menu.go b/menu/menu.go index f4aeb6d..aff335c 100644 --- a/menu/menu.go +++ b/menu/menu.go @@ -134,15 +134,8 @@ func (menu *Menu) SetMenu(buttons []*Button) error { if err != nil { return err } - var commError util.CommonError - err = json.Unmarshal(response, &commError) - if err != nil { - return err - } - if commError.ErrCode != 0 { - return fmt.Errorf("SetMenu Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg) - } - return nil + + return util.DecodeWithCommonError(response, "SetMenu") } //GetMenu 获取菜单配置 @@ -180,15 +173,8 @@ func (menu *Menu) DeleteMenu() error { if err != nil { return err } - var commError util.CommonError - err = json.Unmarshal(response, &commError) - if err != nil { - return err - } - if commError.ErrCode != 0 { - return fmt.Errorf("GetMenu Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg) - } - return nil + + return util.DecodeWithCommonError(response, "GetMenu") } //AddConditional 添加个性化菜单 @@ -208,15 +194,8 @@ func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error if err != nil { return err } - var commError util.CommonError - err = json.Unmarshal(response, &commError) - if err != nil { - return err - } - if commError.ErrCode != 0 { - return fmt.Errorf("AddConditional Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg) - } - return nil + + return util.DecodeWithCommonError(response, "AddConditional") } //DeleteConditional 删除个性化菜单 @@ -235,15 +214,8 @@ func (menu *Menu) DeleteConditional(menuID int64) error { if err != nil { return err } - var commError util.CommonError - err = json.Unmarshal(response, &commError) - if err != nil { - return err - } - if commError.ErrCode != 0 { - return fmt.Errorf("DeleteConditional Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg) - } - return nil + + return util.DecodeWithCommonError(response, "DeleteConditional") } //MenuTryMatch 菜单匹配 @@ -286,7 +258,6 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err if err != nil { return } - fmt.Println(string(response)) err = json.Unmarshal(response, &resSelfMenuInfo) if err != nil { return diff --git a/message/message.go b/message/message.go index b426a7f..00809be 100644 --- a/message/message.go +++ b/message/message.go @@ -69,6 +69,7 @@ type MixMessage struct { //基本消息 MsgID int64 `xml:"MsgId"` Content string `xml:"Content"` + Recognition string `xml:"Recognition"` PicURL string `xml:"PicUrl"` MediaID string `xml:"MediaId"` Format string `xml:"Format"` @@ -82,15 +83,15 @@ type MixMessage struct { URL string `xml:"Url"` //事件相关 - Event EventType `xml:"Event"` - EventKey string `xml:"EventKey"` - Ticket string `xml:"Ticket"` - Latitude string `xml:"Latitude"` - Longitude string `xml:"Longitude"` - Precision string `xml:"Precision"` - MenuID string `xml:"MenuId"` - Status string `xml:"Status"` - SessionFrom string `xml:"SessionFrom"` + Event EventType `xml:"Event"` + EventKey string `xml:"EventKey"` + Ticket string `xml:"Ticket"` + Latitude string `xml:"Latitude"` + Longitude string `xml:"Longitude"` + Precision string `xml:"Precision"` + MenuID string `xml:"MenuId"` + Status string `xml:"Status"` + SessionFrom string `xml:"SessionFrom"` ScanCodeInfo struct { ScanType string `xml:"ScanType"` diff --git a/oauth/qy_oauth.go b/oauth/qy_oauth.go new file mode 100644 index 0000000..3f6916d --- /dev/null +++ b/oauth/qy_oauth.go @@ -0,0 +1,95 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/silenceper/wechat/util" +) + +var ( + qyRedirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&agentid=%s&state=%s#wechat_redirect" + qyUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" + qyUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail" +) + +//GetQyRedirectURL 获取企业微信跳转的url地址 +func (oauth *Oauth) GetQyRedirectURL(redirectURI, agentid, scope, state string) (string, error) { + //url encode + urlStr := url.QueryEscape(redirectURI) + return fmt.Sprintf(qyRedirectOauthURL, oauth.AppID, urlStr, scope, agentid, state), nil +} + +//QyUserInfo 用户授权获取到用户信息 +type QyUserInfo struct { + util.CommonError + + UserID string `json:"UserId"` + DeviceID string `json:"DeviceId"` + UserTicket string `json:"user_ticket"` + ExpiresIn int64 `json:"expires_in"` +} + +//GetQyUserInfoByCode 根据code获取企业user_info +func (oauth *Oauth) GetQyUserInfoByCode(code string) (result QyUserInfo, err error) { + qyAccessToken, e := oauth.GetQyAccessToken() + if e != nil { + err = e + return + } + urlStr := fmt.Sprintf(qyUserInfoURL, qyAccessToken, code) + 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("GetQyUserInfoByCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +//QyUserDetail 到用户详情 +type QyUserDetail struct { + util.CommonError + + UserID string `json:"UserId"` + Name string `json:"name"` + Mobile string `json:"mobile"` + Gender string `json:"gender"` + Email string `json:"email"` + Avatar string `json:"avatar"` + QrCode string `json:"qr_code"` +} + +//GetQyUserDetailUserTicket 根据user_ticket获取到用户详情 +func (oauth *Oauth) GetQyUserDetailUserTicket(userTicket string) (result QyUserDetail, err error) { + var qyAccessToken string + qyAccessToken, err = oauth.GetQyAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", qyUserDetailURL, qyAccessToken) + var response []byte + response, err = util.PostJSON(uri, map[string]string{ + "user_ticket": userTicket, + }) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("GetQyUserDetailUserTicket Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg) + return + } + return +} diff --git a/server/server.go b/server/server.go index 5a40c25..ad3c8ca 100644 --- a/server/server.go +++ b/server/server.go @@ -18,6 +18,8 @@ import ( type Server struct { *context.Context + debug bool + openID string messageHandler func(message.MixMessage) *message.Reply @@ -40,6 +42,11 @@ func NewServer(context *context.Context) *Server { return srv } +// SetDebug set debug field +func (srv *Server) SetDebug(debug bool) { + srv.debug = debug +} + //Serve 处理微信的请求消息 func (srv *Server) Serve() error { if !srv.Validate() { @@ -65,6 +72,9 @@ func (srv *Server) Serve() error { //Validate 校验请求是否合法 func (srv *Server) Validate() bool { + if srv.debug { + return true + } timestamp := srv.Query("timestamp") nonce := srv.Query("nonce") signature := srv.Query("signature") diff --git a/user/user.go b/user/user.go index 73f127c..a792aae 100644 --- a/user/user.go +++ b/user/user.go @@ -9,7 +9,8 @@ import ( ) const ( - userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info" + 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" ) //User 用户管理 @@ -28,20 +29,20 @@ 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 []string `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"` } //GetUserInfo 获取用户基本信息 @@ -52,7 +53,7 @@ func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) { return } - uri := fmt.Sprintf("%s?access_token=%s&openid=%s&lang=zh_CN", userInfoURL, accessToken, openID) + uri := fmt.Sprintf(userInfoURL, accessToken, openID) var response []byte response, err = util.HTTPGet(uri) if err != nil { @@ -69,3 +70,21 @@ func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) { } return } + +// UpdateRemark 设置用户备注名 +func (user *User) UpdateRemark(openID, remark string) (err error) { + var accessToken string + accessToken, err = user.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(updateRemarkURL, accessToken) + var response []byte + response, err = util.PostJSON(uri, map[string]string{"openid": openID, "remark": remark}) + if err != nil { + return + } + + return util.DecodeWithCommonError(response, "UpdateRemark") +} diff --git a/util/error.go b/util/error.go index 4a9ab64..79ad14d 100644 --- a/util/error.go +++ b/util/error.go @@ -1,7 +1,25 @@ package util +import ( + "encoding/json" + "fmt" +) + // CommonError 微信返回的通用错误json type CommonError struct { ErrCode int64 `json:"errcode"` ErrMsg string `json:"errmsg"` } + +// DecodeWithCommonError 将返回值按照CommonError解析 +func DecodeWithCommonError(response []byte, apiName string) (err error) { + var commError CommonError + err = json.Unmarshal(response, &commError) + if err != nil { + return + } + if commError.ErrCode != 0 { + return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, commError.ErrCode, commError.ErrMsg) + } + return nil +} diff --git a/wechat.go b/wechat.go index 02e3a34..19856b8 100644 --- a/wechat.go +++ b/wechat.go @@ -10,10 +10,10 @@ import ( "github.com/silenceper/wechat/material" "github.com/silenceper/wechat/menu" "github.com/silenceper/wechat/oauth" + "github.com/silenceper/wechat/pay" "github.com/silenceper/wechat/server" "github.com/silenceper/wechat/template" "github.com/silenceper/wechat/user" - "github.com/silenceper/wechat/pay" ) // Wechat struct @@ -27,9 +27,9 @@ type Config struct { AppSecret string Token string EncodingAESKey string - PayMchID string //支付 - 商户 ID - PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址 - PayKey string //支付 - 商户后台设置的支付 key + PayMchID string //支付 - 商户 ID + PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址 + PayKey string //支付 - 商户后台设置的支付 key Cache cache.Cache } @@ -98,4 +98,4 @@ func (wc *Wechat) GetTemplate() *template.Template { // GetPay 返回支付消息的实例 func (wc *Wechat) GetPay() *pay.Pay { return pay.NewPay(wc.Context) -} \ No newline at end of file +}