Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

增加企业微信 企业内部开发模块 #418

Merged
merged 11 commits into from
Jul 16, 2021
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ _testmain.go
.vscode/
vendor
.idea/
example/*
example/*
/test
64 changes: 59 additions & 5 deletions credential/default_access_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 获取
Expand Down Expand Up @@ -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 {
Copy link
Owner

@silenceper silenceper Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

参数Corp小写

if cache == nil {
panic("cache is ineed")
}
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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
61 changes: 61 additions & 0 deletions miniprogram/content/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package content

import (
"fmt"

"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)

const (
checkTextURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s"
checkImageURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s"
)

//Content 内容安全
type Content struct {
*context.Context
}

//NewContent 内容安全接口
func NewContent(ctx *context.Context) *Content {
return &Content{ctx}
}

//CheckText 检测文字
//@text 需要检测的文字
func (content *Content) CheckText(text string) error {
accessToken, err := content.GetAccessToken()
if err != nil {
return err
}
response, err := util.PostJSON(
fmt.Sprintf(checkTextURL, accessToken),
map[string]string{
"content": text,
},
)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "ContentCheckText")
}

//CheckImage 检测图片
//所传参数为要检测的图片文件的绝对路径,图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334,同时文件大小以不超过 300K 为宜,否则可能报错
//@media 图片文件的绝对路径
func (content *Content) CheckImage(media string) error {
accessToken, err := content.GetAccessToken()
if err != nil {
return err
}
response, err := util.PostFile(
"media",
media,
fmt.Sprintf(checkImageURL, accessToken),
)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "ContentCheckImage")
}
6 changes: 6 additions & 0 deletions miniprogram/miniprogram.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/silenceper/wechat/v2/miniprogram/analysis"
"github.com/silenceper/wechat/v2/miniprogram/auth"
"github.com/silenceper/wechat/v2/miniprogram/config"
"github.com/silenceper/wechat/v2/miniprogram/content"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
"github.com/silenceper/wechat/v2/miniprogram/message"
Expand Down Expand Up @@ -78,3 +79,8 @@ func (miniProgram *MiniProgram) GetCustomerMessage() *message.Manager {
func (miniProgram *MiniProgram) GetWeRun() *werun.WeRun {
return werun.NewWeRun(miniProgram.ctx)
}

// GetContentSecurity 内容安全接口
func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
return content.NewContent(miniProgram.ctx)
}
7 changes: 7 additions & 0 deletions wechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
14 changes: 14 additions & 0 deletions work/config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 12 additions & 0 deletions work/context/context.go
Original file line number Diff line number Diff line change
@@ -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
}
87 changes: 87 additions & 0 deletions work/oauth/oauth.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions work/work.go
Original file line number Diff line number Diff line change
@@ -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)
}