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
|
||
}
|