diff --git a/credential/default_access_token.go b/credential/default_access_token.go index 1157d2e..5d6a279 100644 --- a/credential/default_access_token.go +++ b/credential/default_access_token.go @@ -12,11 +12,15 @@ import ( const ( //AccessTokenURL 获取access_token的接口 - accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token" + accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" + //AccessTokenURL 企业微信获取access_token的接口 + workAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" //CacheKeyOfficialAccountPrefix 微信公众号cache key前缀 CacheKeyOfficialAccountPrefix = "gowechat_officialaccount_" //CacheKeyMiniProgramPrefix 小程序cache key前缀 CacheKeyMiniProgramPrefix = "gowechat_miniprogram_" + //CacheKeyWorkPrefix 企业微信cache key前缀 + CacheKeyWorkPrefix = "gowechat_work_" ) //DefaultAccessToken 默认AccessToken 获取 @@ -65,7 +69,58 @@ func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) { //cache失效,从微信服务器获取 var resAccessToken ResAccessToken - resAccessToken, err = GetTokenFromServer(ak.appID, ak.appSecret) + resAccessToken, err = GetTokenFromServer(fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret)) + if err != nil { + return + } + + expires := resAccessToken.ExpiresIn - 1500 + err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) + if err != nil { + return + } + accessToken = resAccessToken.AccessToken + return +} + +//WorkAccessToken 企业微信AccessToken 获取 +type WorkAccessToken struct { + CorpID string + CorpSecret string + cacheKeyPrefix string + cache cache.Cache + accessTokenLock *sync.Mutex +} + +//NewWorkAccessToken new WorkAccessToken +func NewWorkAccessToken(corpID, corpSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenHandle { + if cache == nil { + panic("cache the not exist") + } + return &WorkAccessToken{ + CorpID: corpID, + CorpSecret: corpSecret, + cache: cache, + cacheKeyPrefix: cacheKeyPrefix, + accessTokenLock: new(sync.Mutex), + } +} + +//GetAccessToken 企业微信获取access_token,先从cache中获取,没有则从服务端获取 +func (ak *WorkAccessToken) GetAccessToken() (accessToken string, err error) { + //加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token + ak.accessTokenLock.Lock() + defer ak.accessTokenLock.Unlock() + accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID) + val := ak.cache.Get(accessTokenCacheKey) + if val != nil { + accessToken = val.(string) + return + } + + //cache失效,从微信服务器获取 + var resAccessToken ResAccessToken + resAccessToken, err = GetTokenFromServer(fmt.Sprintf(workAccessTokenURL, ak.CorpID, ak.CorpSecret)) if err != nil { return } @@ -80,8 +135,7 @@ func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) { } //GetTokenFromServer 强制从微信服务器获取token -func GetTokenFromServer(appID, appSecret string) (resAccessToken ResAccessToken, err error) { - url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenURL, appID, appSecret) +func GetTokenFromServer(url string) (resAccessToken ResAccessToken, err error) { var body []byte body, err = util.HTTPGet(url) if err != nil { @@ -91,7 +145,7 @@ func GetTokenFromServer(appID, appSecret string) (resAccessToken ResAccessToken, if err != nil { return } - if resAccessToken.ErrMsg != "" { + if resAccessToken.ErrCode != 0 { err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg) return } diff --git a/wechat.go b/wechat.go index a7023ff..cc1204e 100644 --- a/wechat.go +++ b/wechat.go @@ -12,6 +12,8 @@ import ( openConfig "github.com/silenceper/wechat/v2/openplatform/config" "github.com/silenceper/wechat/v2/pay" payConfig "github.com/silenceper/wechat/v2/pay/config" + "github.com/silenceper/wechat/v2/work" + workConfig "github.com/silenceper/wechat/v2/work/config" log "github.com/sirupsen/logrus" ) @@ -67,3 +69,8 @@ func (wc *Wechat) GetPay(cfg *payConfig.Config) *pay.Pay { func (wc *Wechat) GetOpenPlatform(cfg *openConfig.Config) *openplatform.OpenPlatform { return openplatform.NewOpenPlatform(cfg) } + +// GetWork 获取企业微信的实例 +func (wc *Wechat) GetWork(cfg *workConfig.Config) *work.Work { + return work.NewWork(cfg) +} diff --git a/work/config/config.go b/work/config/config.go new file mode 100644 index 0000000..2bdc393 --- /dev/null +++ b/work/config/config.go @@ -0,0 +1,14 @@ +// Package config 企业微信config配置 +package config + +import ( + "github.com/silenceper/wechat/v2/cache" +) + +// Config config for 企业微信 +type Config struct { + CorpID string `json:"corp_id"` // corp_id + CorpSecret string `json:"corp_secret"` // corp_secret + AgentID string `json:"agent_id"` // agent_id + Cache cache.Cache +} diff --git a/work/context/context.go b/work/context/context.go new file mode 100644 index 0000000..2927d85 --- /dev/null +++ b/work/context/context.go @@ -0,0 +1,12 @@ +package context + +import ( + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/work/config" +) + +// Context struct +type Context struct { + *config.Config + credential.AccessTokenHandle +} diff --git a/work/oauth/oauth.go b/work/oauth/oauth.go new file mode 100644 index 0000000..296dbaf --- /dev/null +++ b/work/oauth/oauth.go @@ -0,0 +1,87 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/context" +) + +//Oauth auth +type Oauth struct { + *context.Context +} + +var ( + //oauthTargetURL 企业微信内跳转地址 + oauthTargetURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect" + //oauthUserInfoURL 获取用户信息地址 + oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" + //oauthQrContentTargetURL 构造独立窗口登录二维码 + oauthQrContentTargetURL = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s" +) + +//NewOauth new init oauth +func NewOauth(ctx *context.Context) *Oauth { + return &Oauth{ + ctx, + } +} + +//GetTargetURL 获取授权地址 +func (ctr *Oauth) GetTargetURL(callbackURL string) string { + //url encode + urlStr := url.QueryEscape(callbackURL) + return fmt.Sprintf( + oauthTargetURL, + ctr.CorpID, + urlStr, + ) +} + +//GetQrContentTargetURL 构造独立窗口登录二维码 +func (ctr *Oauth) GetQrContentTargetURL(callbackURL string) string { + //url encode + urlStr := url.QueryEscape(callbackURL) + return fmt.Sprintf( + oauthQrContentTargetURL, + ctr.CorpID, + ctr.AgentID, + urlStr, + util.RandomStr(16), + ) +} + +//ResUserInfo 返回得用户信息 +type ResUserInfo struct { + util.CommonError + //当用户为企业成员时返回 + UserID string `json:"UserId"` + DeviceID string `json:"DeviceId"` + //非企业成员授权时返回 + OpenID string `json:"OpenId"` +} + +//UserFromCode 根据code获取用户信息 +func (ctr *Oauth) UserFromCode(code string) (result ResUserInfo, err error) { + var accessToken string + accessToken, err = ctr.GetAccessToken() + if err != nil { + return + } + var response []byte + response, err = util.HTTPGet( + fmt.Sprintf(oauthUserInfoURL, accessToken, code), + ) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if result.ErrCode != 0 { + err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} diff --git a/work/work.go b/work/work.go new file mode 100644 index 0000000..58f577a --- /dev/null +++ b/work/work.go @@ -0,0 +1,33 @@ +package work + +import ( + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/work/config" + "github.com/silenceper/wechat/v2/work/context" + "github.com/silenceper/wechat/v2/work/oauth" +) + +// Work 企业微信 +type Work struct { + ctx *context.Context +} + +//NewWork init work +func NewWork(cfg *config.Config) *Work { + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + ctx := &context.Context{ + Config: cfg, + AccessTokenHandle: defaultAkHandle, + } + return &Work{ctx: ctx} +} + +//GetContext get Context +func (wk *Work) GetContext() *context.Context { + return wk.ctx +} + +//GetOauth get oauth +func (wk *Work) GetOauth() *oauth.Oauth { + return oauth.NewOauth(wk.ctx) +}