mirror of https://github.com/mindoc-org/mindoc.git
286 lines
8.5 KiB
Go
286 lines
8.5 KiB
Go
|
package wecom
|
|||
|
|
|||
|
import (
|
|||
|
"bytes"
|
|||
|
"context"
|
|||
|
"encoding/json"
|
|||
|
"errors"
|
|||
|
"fmt"
|
|||
|
"github.com/mindoc-org/mindoc/utils/auth2"
|
|||
|
"net/http"
|
|||
|
"net/url"
|
|||
|
"time"
|
|||
|
)
|
|||
|
|
|||
|
// doc
|
|||
|
// - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313
|
|||
|
|
|||
|
const (
|
|||
|
AppName = "workwx"
|
|||
|
|
|||
|
auth2Url = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
|||
|
ssoUrl = "https://login.work.weixin.qq.com/wwlogin/sso/login"
|
|||
|
callbackState = "mindoc"
|
|||
|
)
|
|||
|
|
|||
|
type BasicResponse struct {
|
|||
|
ErrCode int `json:"errcode"`
|
|||
|
ErrMsg string `json:"errmsg"`
|
|||
|
}
|
|||
|
|
|||
|
func (r *BasicResponse) Error() string {
|
|||
|
return fmt.Sprintf("errcode=%d,errmsg=%s", r.ErrCode, r.ErrMsg)
|
|||
|
}
|
|||
|
|
|||
|
func (r *BasicResponse) AsError() error {
|
|||
|
if r == nil {
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
if r.ErrCode != 0 {
|
|||
|
return r
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
// 获取用户Id-请求响应结构
|
|||
|
type UserIdResponse struct {
|
|||
|
// 接口文档: https://developer.work.weixin.qq.com/document/path/91023
|
|||
|
*BasicResponse
|
|||
|
|
|||
|
UserId string `json:"userid"` // 企业成员UserID
|
|||
|
UserTicket string `json:"user_ticket"` // 用于获取敏感信息
|
|||
|
OpenId string `json:"openid"` // 非企业成员的标识,对当前企业唯一
|
|||
|
ExternalUserId string `json:"external_userid"` // 外部联系人ID
|
|||
|
}
|
|||
|
|
|||
|
// 获取用户信息-请求响应结构
|
|||
|
type UserInfoResponse struct {
|
|||
|
// 接口文档: https://developer.work.weixin.qq.com/document/path/90196
|
|||
|
*BasicResponse
|
|||
|
|
|||
|
UserId string `json:"userid"` // 企业成员UserID
|
|||
|
Name string `json:"name"` // 成员名称
|
|||
|
Department []int `json:"department"` // 成员所属部门id列表
|
|||
|
IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
|
|||
|
IsLeader int `json:"isleader"` // 是否是部门上级(领导)
|
|||
|
Alias string `json:"alias"` // 别名
|
|||
|
Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业
|
|||
|
MainDepartment int `json:"main_department"` // 主部门
|
|||
|
}
|
|||
|
|
|||
|
type UserPrivateInfoResponse struct {
|
|||
|
// 文档地址: https://developer.work.weixin.qq.com/document/path/95833
|
|||
|
*BasicResponse
|
|||
|
|
|||
|
UserId string `json:"userid"` // 企业成员userid
|
|||
|
Gender string `json:"gender"` // 成员性别
|
|||
|
Avatar string `json:"avatar"` // 头像
|
|||
|
QrCode string `json:"qr_code"` // 二维码
|
|||
|
Mobile string `json:"mobile"` // 手机号
|
|||
|
Mail string `json:"mail"` // 邮箱
|
|||
|
BizMail string `json:"biz_mail"` // 企业邮箱
|
|||
|
Address string `json:"address"` // 地址
|
|||
|
}
|
|||
|
|
|||
|
// 访问凭据缓存-结构
|
|||
|
type AccessToken struct {
|
|||
|
*BasicResponse
|
|||
|
|
|||
|
AccessToken string `json:"access_token"`
|
|||
|
ExpiresIn int `json:"expires_in"`
|
|||
|
|
|||
|
createTime time.Time `json:"create_time"`
|
|||
|
}
|
|||
|
|
|||
|
func (a AccessToken) GetToken() string {
|
|||
|
return a.AccessToken
|
|||
|
}
|
|||
|
|
|||
|
func (a AccessToken) GetExpireIn() time.Duration {
|
|||
|
return time.Duration(a.ExpiresIn) * time.Second
|
|||
|
}
|
|||
|
|
|||
|
func (a AccessToken) GetExpireTime() time.Time {
|
|||
|
return a.createTime.Add(a.GetExpireIn())
|
|||
|
}
|
|||
|
|
|||
|
// 企业微信用户敏感信息-结构
|
|||
|
type WorkWeixinUserPrivateInfo struct {
|
|||
|
UserId string `json:"userid"` // 企业成员userid
|
|||
|
Name string `json:"name"` // 姓名
|
|||
|
Gender string `json:"gender"` // 成员性别
|
|||
|
Avatar string `json:"avatar"` // 头像
|
|||
|
QrCode string `json:"qr_code"` // 二维码
|
|||
|
Mobile string `json:"mobile"` // 手机号
|
|||
|
Mail string `json:"mail"` // 邮箱
|
|||
|
BizMail string `json:"biz_mail"` // 企业邮箱
|
|||
|
Address string `json:"address"` // 地址
|
|||
|
}
|
|||
|
|
|||
|
// 企业微信用户信息-结构
|
|||
|
type WorkWeixinUserInfo struct {
|
|||
|
UserId string `json:"UserId"` // 企业成员UserID
|
|||
|
Name string `json:"name"` // 成员名称
|
|||
|
HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码
|
|||
|
Mobile string `json:"mobile"` // 手机号码
|
|||
|
Department []int `json:"department"` // 成员所属部门id列表
|
|||
|
Email string `json:"email"` // 邮箱
|
|||
|
IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
|
|||
|
IsLeader int `json:"isleader"` // 是否是部门上级(领导)
|
|||
|
Avatar string `json:"avatar"` // 头像url
|
|||
|
Alias string `json:"alias"` // 别名
|
|||
|
Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业
|
|||
|
MainDepartment int `json:"main_department"` // 主部门
|
|||
|
}
|
|||
|
|
|||
|
func NewClient(corpId, appId, appSecrete string) auth2.Client {
|
|||
|
return NewWorkWechatClient(corpId, appId, appSecrete)
|
|||
|
}
|
|||
|
func NewWorkWechatClient(corpId, appId, appSecrete string) *WorkWechatClient {
|
|||
|
return &WorkWechatClient{
|
|||
|
CorpId: corpId,
|
|||
|
AppId: appId,
|
|||
|
AppSecret: appSecrete,
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type WorkWechatClient struct {
|
|||
|
CorpId string
|
|||
|
AppId string
|
|||
|
AppSecret string
|
|||
|
|
|||
|
token auth2.IAccessToken
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) GetAccessToken(ctx context.Context) (auth2.IAccessToken, error) {
|
|||
|
if c.token != nil {
|
|||
|
return c.token, nil
|
|||
|
}
|
|||
|
|
|||
|
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", c.CorpId, c.AppSecret)
|
|||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|||
|
|
|||
|
var token AccessToken
|
|||
|
if err := auth2.Request(req, &token); err != nil {
|
|||
|
return token, err
|
|||
|
}
|
|||
|
token.createTime = time.Now()
|
|||
|
return token, nil
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) SetAccessToken(token auth2.IAccessToken) {
|
|||
|
c.token = token
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) BuildURL(callback string, isAppBrowser bool) string {
|
|||
|
var endpoint string
|
|||
|
if isAppBrowser {
|
|||
|
// 企业微信内-网页授权登录
|
|||
|
urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect"
|
|||
|
endpoint = fmt.Sprintf(urlFmt, auth2Url, c.CorpId, c.AppId, url.PathEscape(callback), callbackState)
|
|||
|
} else {
|
|||
|
// 浏览器内-扫码授权登录
|
|||
|
urlFmt := "%s?login_type=CorpApp&appid=%s&agentid=%s&redirect_uri=%s&state=%s"
|
|||
|
endpoint = fmt.Sprintf(urlFmt, ssoUrl, c.CorpId, c.AppId, url.PathEscape(callback), callbackState)
|
|||
|
}
|
|||
|
return endpoint
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) ValidateCallback(state string) error {
|
|||
|
if state != callbackState {
|
|||
|
return errors.New("auth2.state.wrong")
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) getUserId(ctx context.Context, code string) (UserIdResponse, error) {
|
|||
|
var userId UserIdResponse
|
|||
|
|
|||
|
token, err := c.GetAccessToken(ctx)
|
|||
|
if err != nil {
|
|||
|
return userId, err
|
|||
|
}
|
|||
|
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s", token.GetToken(), code)
|
|||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|||
|
|
|||
|
if err := auth2.Request(req, &userId); err != nil {
|
|||
|
return userId, err
|
|||
|
}
|
|||
|
|
|||
|
if userId.UserId == "" {
|
|||
|
return userId, errors.New("auth2.userid.empty")
|
|||
|
}
|
|||
|
|
|||
|
return userId, nil
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) getUserInfo(ctx context.Context, userid string) (UserInfoResponse, error) {
|
|||
|
var userInfo UserInfoResponse
|
|||
|
token, err := c.GetAccessToken(ctx)
|
|||
|
if err != nil {
|
|||
|
return userInfo, err
|
|||
|
}
|
|||
|
|
|||
|
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", token.GetToken(), userid)
|
|||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|||
|
|
|||
|
if err := auth2.Request(req, &userInfo); err != nil {
|
|||
|
return userInfo, err
|
|||
|
}
|
|||
|
return userInfo, nil
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) getUserPrivateInfo(ctx context.Context, ticket string) (UserPrivateInfoResponse, error) {
|
|||
|
var userInfo UserPrivateInfoResponse
|
|||
|
|
|||
|
token, err := c.GetAccessToken(ctx)
|
|||
|
if err != nil {
|
|||
|
return userInfo, err
|
|||
|
}
|
|||
|
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s", token.GetToken())
|
|||
|
|
|||
|
b, _ := json.Marshal(map[string]string{
|
|||
|
"user_ticket": ticket,
|
|||
|
})
|
|||
|
|
|||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(b))
|
|||
|
|
|||
|
if err := auth2.Request(req, &userInfo); err != nil {
|
|||
|
return userInfo, err
|
|||
|
}
|
|||
|
return userInfo, nil
|
|||
|
}
|
|||
|
|
|||
|
func (c *WorkWechatClient) GetUserInfo(ctx context.Context, code string) (auth2.UserInfo, error) {
|
|||
|
var info auth2.UserInfo
|
|||
|
|
|||
|
userid, err := c.getUserId(ctx, code)
|
|||
|
if err != nil {
|
|||
|
return info, err
|
|||
|
}
|
|||
|
|
|||
|
userInfo, err := c.getUserInfo(ctx, userid.UserId)
|
|||
|
if err != nil {
|
|||
|
return info, err
|
|||
|
}
|
|||
|
|
|||
|
info.UserId = userInfo.UserId
|
|||
|
info.Name = userInfo.Name
|
|||
|
|
|||
|
if userid.UserTicket == "" {
|
|||
|
return info, nil
|
|||
|
}
|
|||
|
|
|||
|
private, err := c.getUserPrivateInfo(ctx, userid.UserTicket)
|
|||
|
if err != nil {
|
|||
|
return info, err
|
|||
|
}
|
|||
|
|
|||
|
info.Mail = private.BizMail
|
|||
|
info.Avatar = private.Avatar
|
|||
|
info.Mobile = private.Mobile
|
|||
|
return info, nil
|
|||
|
}
|