init push
This commit is contained in:
347
backend/pkg/wecom/wecom.go
Normal file
347
backend/pkg/wecom/wecom.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/store/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
// AuthURL api doc https://developer.work.weixin.qq.com/document/path/98152
|
||||
AuthWebURL = "https://login.work.weixin.qq.com/wwlogin/sso/login"
|
||||
AuthAPPURL = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
||||
TokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
UserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
|
||||
UserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||
// DepartmentListURL https://developer.work.weixin.qq.com/document/path/90344
|
||||
DepartmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list"
|
||||
// UserListUrl https://developer.work.weixin.qq.com/document/path/90337
|
||||
UserListUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
|
||||
callbackPath = "/share/pro/v1/openapi/wecom/callback"
|
||||
)
|
||||
|
||||
// Client 企业微信客户端
|
||||
type Client struct {
|
||||
context context.Context
|
||||
cache *cache.Cache
|
||||
httpClient *http.Client
|
||||
oauthConfig *oauth2.Config
|
||||
logger *log.Logger
|
||||
corpID string
|
||||
agentID string
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type UserInfoResponse struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
UserID string `json:"userid"`
|
||||
UserTicket string `json:"user_ticket"`
|
||||
OpenID string `json:"openid"`
|
||||
ExternalUserid string `json:"external_userid"`
|
||||
}
|
||||
|
||||
type UserDetailResponse struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Userid string `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
Mobile string `json:"mobile"`
|
||||
Gender string `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
OpenUserid string `json:"open_userid"`
|
||||
}
|
||||
type DepartmentListResponse struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Department []struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
NameEn string `json:"name_en"`
|
||||
DepartmentLeader []string `json:"department_leader"`
|
||||
Parentid int `json:"parentid"`
|
||||
Order int `json:"order"`
|
||||
} `json:"department"`
|
||||
}
|
||||
|
||||
type UserListResponse struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Userlist []struct {
|
||||
Name string `json:"name"`
|
||||
Department []int `json:"department"`
|
||||
Position string `json:"position"`
|
||||
Status int `json:"status"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Enable int `json:"enable"`
|
||||
Isleader int `json:"isleader"`
|
||||
Extattr struct {
|
||||
Attrs []interface{} `json:"attrs"`
|
||||
} `json:"extattr"`
|
||||
HideMobile int `json:"hide_mobile"`
|
||||
Telephone string `json:"telephone"`
|
||||
Order []int `json:"order"`
|
||||
ExternalProfile struct {
|
||||
ExternalAttr []interface{} `json:"external_attr"`
|
||||
ExternalCorpName string `json:"external_corp_name"`
|
||||
} `json:"external_profile"`
|
||||
MainDepartment int `json:"main_department"`
|
||||
Alias string `json:"alias"`
|
||||
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||
Userid string `json:"userid"`
|
||||
DirectLeader []interface{} `json:"direct_leader"`
|
||||
} `json:"userlist"`
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, logger *log.Logger, corpID, corpSecret, agentID, baseUrl string, cache *cache.Cache, isApp bool) (*Client, error) {
|
||||
redirectURI, err := url.JoinPath(baseUrl, callbackPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authUrl := AuthWebURL
|
||||
if isApp {
|
||||
authUrl = AuthAPPURL
|
||||
}
|
||||
|
||||
oauthConfig := &oauth2.Config{
|
||||
ClientID: fmt.Sprintf("%s-%s", corpID, agentID),
|
||||
ClientSecret: corpSecret,
|
||||
RedirectURL: redirectURI,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authUrl,
|
||||
TokenURL: TokenURL,
|
||||
},
|
||||
Scopes: []string{"snsapi_privateinfo"},
|
||||
}
|
||||
|
||||
return &Client{
|
||||
context: ctx,
|
||||
httpClient: &http.Client{},
|
||||
cache: cache,
|
||||
logger: logger.WithModule("wecom.client"),
|
||||
oauthConfig: oauthConfig,
|
||||
corpID: corpID,
|
||||
agentID: agentID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateAuthURL 生成授权 URL
|
||||
func (c *Client) GenerateAuthURL(state string) string {
|
||||
params := url.Values{}
|
||||
params.Set("appid", c.corpID)
|
||||
params.Set("redirect_uri", c.oauthConfig.RedirectURL)
|
||||
params.Set("response_type", "code")
|
||||
params.Set("scope", "snsapi_privateinfo")
|
||||
params.Set("login_type", "CorpApp")
|
||||
params.Set("agentid", c.agentID)
|
||||
params.Set("state", state)
|
||||
|
||||
authUrl := fmt.Sprintf("%s?%s", c.oauthConfig.Endpoint.AuthURL, params.Encode())
|
||||
if c.oauthConfig.Endpoint.AuthURL == AuthAPPURL {
|
||||
authUrl += "#wechat_redirect"
|
||||
}
|
||||
return authUrl
|
||||
}
|
||||
|
||||
// GetAccessToken 获取企业微信访问令牌
|
||||
func (c *Client) GetAccessToken(ctx context.Context) (string, error) {
|
||||
|
||||
cacheKey := fmt.Sprintf("wecom-access-token:%s", c.oauthConfig.ClientID)
|
||||
cachedData, err := c.cache.Get(ctx, cacheKey).Result()
|
||||
if err == nil && cachedData != "" {
|
||||
return cachedData, nil
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("corpid", c.corpID)
|
||||
params.Set("corpsecret", c.oauthConfig.ClientSecret)
|
||||
|
||||
resp, err := c.httpClient.Get(fmt.Sprintf("%s?%s", TokenURL, params.Encode()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get access token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return "", fmt.Errorf("failed to decode token response: %w", err)
|
||||
}
|
||||
|
||||
if tokenResp.ErrCode != 0 {
|
||||
return "", fmt.Errorf("failed to get access token: %s", tokenResp.ErrMsg)
|
||||
}
|
||||
|
||||
if err := c.cache.Set(ctx, cacheKey, tokenResp.AccessToken, time.Duration(tokenResp.ExpiresIn-300)*time.Second).Err(); err != nil {
|
||||
c.logger.Warn("failed to set cache", log.Error(err))
|
||||
}
|
||||
|
||||
return tokenResp.AccessToken, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByCode 通过授权码获取用户信息
|
||||
func (c *Client) GetUserInfoByCode(ctx context.Context, code string) (*UserDetailResponse, error) {
|
||||
|
||||
accessToken, err := c.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %w", err)
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
params.Set("code", code)
|
||||
|
||||
userInfoURL := fmt.Sprintf("%s?%s", UserInfoURL, params.Encode())
|
||||
|
||||
c.logger.Debug("GetUserInfoByCode", log.Any("userInfoURL", userInfoURL))
|
||||
|
||||
resp, err := c.httpClient.Get(userInfoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read body: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
c.logger.Debug("GetUserInfoByCode raw resp:", log.Any("raw", string(rawBody)))
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
||||
|
||||
var userInfoResp UserInfoResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userInfoResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode user info response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("GetUserInfoByCode resp:", log.Any("resp", userInfoResp))
|
||||
|
||||
if userInfoResp.ErrCode != 0 {
|
||||
return nil, fmt.Errorf("failed to get user info: %s", userInfoResp.ErrMsg)
|
||||
}
|
||||
|
||||
detailParams := url.Values{}
|
||||
detailParams.Set("access_token", accessToken)
|
||||
detailParams.Set("userid", userInfoResp.UserID)
|
||||
|
||||
userDetailURL := fmt.Sprintf("%s?%s", UserDetailURL, detailParams.Encode())
|
||||
|
||||
detailResp, err := c.httpClient.Get(userDetailURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user detail: %w", err)
|
||||
}
|
||||
defer detailResp.Body.Close()
|
||||
|
||||
var UserDetailResp UserDetailResponse
|
||||
if err := json.NewDecoder(detailResp.Body).Decode(&UserDetailResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode user detail response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("GetUserInfoByCode detail info", log.Any("resp", UserDetailResp))
|
||||
|
||||
if UserDetailResp.Errcode != 0 {
|
||||
return nil, fmt.Errorf("failed to get user detail: %s", UserDetailResp.Errmsg)
|
||||
}
|
||||
|
||||
return &UserDetailResp, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetDepartmentList(ctx context.Context) (*DepartmentListResponse, error) {
|
||||
|
||||
accessToken, err := c.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %w", err)
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
|
||||
departmentListURL := fmt.Sprintf("%s?%s", DepartmentListURL, params.Encode())
|
||||
|
||||
resp, err := c.httpClient.Get(departmentListURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get department list: %w", err)
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read body: %w", err)
|
||||
}
|
||||
c.logger.Debug("GetDepartmentList raw resp:", log.Any("raw", string(rawBody)))
|
||||
defer resp.Body.Close()
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
||||
|
||||
var departmentListResponse DepartmentListResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&departmentListResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode department list response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("GetDepartmentList resp:", log.Any("resp", departmentListResponse))
|
||||
|
||||
if departmentListResponse.Errcode != 0 {
|
||||
return nil, fmt.Errorf("failed to get user info: %s", departmentListResponse.Errmsg)
|
||||
}
|
||||
|
||||
return &departmentListResponse, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUserList(ctx context.Context, deptID string) (*UserListResponse, error) {
|
||||
|
||||
accessToken, err := c.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %w", err)
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("access_token", accessToken)
|
||||
params.Set("department_id", deptID)
|
||||
|
||||
userListUrl := fmt.Sprintf("%s?%s", UserListUrl, params.Encode())
|
||||
|
||||
resp, err := c.httpClient.Get(userListUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user list: %w", err)
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read body: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("GetUserList raw resp:", log.Any("raw", string(rawBody)))
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
||||
|
||||
var userListResponse UserListResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userListResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode user list response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("GetUserList resp:", log.Any("resp", userListResponse))
|
||||
|
||||
if userListResponse.Errcode != 0 {
|
||||
return nil, fmt.Errorf("failed to get user info: %s", userListResponse.Errmsg)
|
||||
}
|
||||
|
||||
return &userListResponse, nil
|
||||
}
|
||||
Reference in New Issue
Block a user