diff --git a/credential/work_js_ticket.go b/credential/work_js_ticket.go index e3906b6..1ce8c11 100644 --- a/credential/work_js_ticket.go +++ b/credential/work_js_ticket.go @@ -10,11 +10,11 @@ import ( "github.com/silenceper/wechat/v2/util" ) -// 获取ticket的url https://developer.work.weixin.qq.com/document/path/90506 +//获取ticket的url https://developer.work.weixin.qq.com/document/path/90506 const getQyWxTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s" const getQyAppTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=%s&type=agent_config" -// WorkJsTicket 默认获取js ticket方法 +//WorkJsTicket 默认获取js ticket方法 type WorkJsTicket struct { appID string agentID string @@ -24,7 +24,7 @@ type WorkJsTicket struct { jsAPITicketLock *sync.Mutex } -// NewWorkJsTicket new +//NewWorkJsTicket new func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle { return &WorkJsTicket{ appID: appID, @@ -35,7 +35,7 @@ func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache } } -// GetTicket 获取企业微信jsapi_ticket +//GetTicket 获取企业微信jsapi_ticket func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err error) { //先从cache中取 jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID) @@ -62,7 +62,7 @@ func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err err return } -// GetQyWxTicketFromServer 从企业微信服务器中获取ticket +//GetQyWxTicketFromServer 从企业微信服务器中获取ticket func GetQyWxTicketFromServer(accessToken string, isApp bool) (ticket ResTicket, err error) { var response []byte url := fmt.Sprintf(getQyWxTicketURL, accessToken) diff --git a/miniprogram/encryptor/encryptor.go b/miniprogram/encryptor/encryptor.go index af3c4c2..a5dffb4 100644 --- a/miniprogram/encryptor/encryptor.go +++ b/miniprogram/encryptor/encryptor.go @@ -7,8 +7,8 @@ import ( "encoding/json" "errors" "fmt" - "github.com/silenceper/wechat/v2/miniprogram/context" + "strings" ) // Encryptor struct @@ -108,13 +108,23 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) { } // Decrypt 解密数据 -func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) { +func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, appid string) (*PlainData, error) { + ivB := make([]byte, 16) + iv := base64.StdEncoding.EncodeToString(ivB) cipherText, err := GetCipherText(sessionKey, encryptedData, iv) if err != nil { return nil, err } + length := string(cipherText[:20]) + + cipherTextData := strings.TrimPrefix(string(cipherText), string(cipherText[:20])) + cipherTextData = strings.TrimSuffix(cipherTextData, appid) + + if len(length) != len(cipherTextData) { + return nil, fmt.Errorf("length not match, %d != %d", length, len(cipherTextData)) + } var plainData PlainData - err = json.Unmarshal(cipherText, &plainData) + err = json.Unmarshal([]byte(cipherTextData), &plainData) if err != nil { return nil, err } diff --git a/util/signature.go b/util/signature.go index 2deb8e2..80ac8f0 100644 --- a/util/signature.go +++ b/util/signature.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "crypto/sha1" "fmt" "io" @@ -16,3 +17,16 @@ func Signature(params ...string) string { } return fmt.Sprintf("%x", h.Sum(nil)) } + +func CalSignature(params ...string) string { + sort.Strings(params) + var buffer bytes.Buffer + for _, value := range params { + buffer.WriteString(value) + } + + sha := sha1.New() + sha.Write(buffer.Bytes()) + signature := fmt.Sprintf("%x", sha.Sum(nil)) + return string(signature) +} diff --git a/work/externalcontact/add_msg_template.go b/work/externalcontact/add_msg_template.go new file mode 100644 index 0000000..0f3b200 --- /dev/null +++ b/work/externalcontact/add_msg_template.go @@ -0,0 +1,90 @@ +package externalcontact + +import ( + "encoding/json" + "fmt" + "github.com/silenceper/wechat/v2/util" +) + +const ( + addMsgTemplateUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template" +) + +type ChatType string + +const ( + ChatTypeSingle ChatType = "single" + ChatTypeGroup ChatType = "group" +) + +// ReqMessage 企业群发参数 +type ReqMessage struct { + ChatType ChatType `json:"chat_type"` //群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群 + ExternalUserid []string `json:"external_userid"` // 客户的外部联系人id列表,仅在chat_type为single时有效,不可与sender同时为空,最多可传入1万个客户 + Sender string `json:"sender"` //发送企业群发消息的成员userid,当类型为发送给客户群时必填 + Text struct { + Content string `json:"content"` + } `json:"text"` + Attachments []struct { + Msgtype string `json:"msgtype"` + Image MsgImage `json:"image"` + Link MsgLink `json:"link"` + Miniprogram MsgMiniprogram `json:"miniprogram"` + Video MsgVideo `json:"video"` + File MsgFile `json:"file"` + } `json:"attachments"` +} +type MsgImage struct { + MediaId string `json:"media_id"` + PicUrl string `json:"pic_url"` +} +type MsgLink struct { + Title string `json:"title"` + Picurl string `json:"picurl"` + Desc string `json:"desc"` + Url string `json:"url"` +} +type MsgMiniprogram struct { + Title string `json:"title"` + PicMediaId string `json:"pic_media_id"` + Appid string `json:"appid"` + Page string `json:"page"` +} +type MsgVideo struct { + MediaId string `json:"media_id"` +} +type MsgFile struct { + MediaId string `json:"media_id"` +} + +type resTemplateSend struct { + util.CommonError + FailList string `json:"fail_list"` + MsgID int64 `json:"msgid"` +} + +// Send 发送应用消息 +func (r *Client) Send(msg *ReqMessage) (msgID int64, err error) { + var accessToken string + accessToken, err = r.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", addMsgTemplateUrl, accessToken) + var response []byte + response, err = util.PostJSON(uri, msg) + if err != nil { + return + } + var result resTemplateSend + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + msgID = result.MsgID + return +} diff --git a/work/externalcontact/user.go b/work/externalcontact/user.go new file mode 100644 index 0000000..60b29f8 --- /dev/null +++ b/work/externalcontact/user.go @@ -0,0 +1,162 @@ +package externalcontact + +import ( + "encoding/json" + "fmt" + "github.com/silenceper/wechat/v2/util" +) + +const ( + listUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list" + getUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get" + getByUserBatchUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user" +) + +type ReqGetByUser struct { + UseridList []string `json:"userid_list"` + Cursor string `json:"cursor"` + Limit int `json:"limit"` +} +type OneUser struct { + util.CommonError + ExternalContact ExternalContact `json:"external_contact"` + FollowUser []FollowInfo `json:"follow_user"` //注意,仅获取单个客户详情的时候这里返回的是跟进人列表 + NextCursor string `json:"next_cursor"` +} +type resUserList struct { + util.CommonError + ExternalContactList []UserInfo `json:"external_contact_list"` + NextCursor string `json:"next_cursor"` +} +type resUserids struct { + util.CommonError + ExternalUserid []string `json:"external_userid"` +} + +type UserInfo struct { + ExternalContact ExternalContact `json:"external_contact"` + FollowInfo FollowInfo `json:"follow_info"` //企业成员客户跟进人信息,可以参考获取客户详情,但标签信息只会返回企业标签和规则组标签的tag_id,个人标签将不再返回 +} + +// GetUseridList 获取我的客户列表 +func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s&userid=%s", listUrl, accessToken, myUserid) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var result resUserids + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + externalUserid = result.ExternalUserid + return +} + +// GetUseridList 获取我的全部客户列表及详情 +func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) { + var userInfoList []UserInfo + var req ReqGetByUser + req.UseridList = qyUserid + req.Limit = 100 + for { + userInfoPage, resCursor, err := tpl.GetUserInfoListByUserIds(req) + if err != nil { + return userInfoList, err + } + userInfoList = append(userInfoList, userInfoPage...) + if resCursor != "" { + req.Cursor = resCursor + } else { + break + } + } + return userInfoList, nil +} + +// GetUserInfoAndAllFollow 获取客户详情以及全部跟进人 +func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) { + var result, res OneUser + var err error + var cursor string + for { + res, err = tpl.GetUserInfo(userid, cursor) + if err != nil { + return result, err + } + result.FollowUser = append(result.FollowUser, res.FollowUser...) + result.ExternalContact = res.ExternalContact + if res.NextCursor != "" { + cursor = res.NextCursor + } else { + break + } + } + return result, nil +} + +// GetUserInfo 获取客户详情 +func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result OneUser, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + var page = "" + if len(cursor) > 0 { + page = cursor[0] + } + uri := fmt.Sprintf("%s?access_token=%s&external_userid=%s&cursor=%s", getUrl, accessToken, externalUserid, page) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + return +} + +// GetUserInfoListByUserId 批量获取客户详情 +func (tpl *Client) GetUserInfoListByUserIds(req ReqGetByUser) (userList []UserInfo, nextCursor string, err error) { + var accessToken string + accessToken, err = tpl.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", getByUserBatchUrl, accessToken) + var response []byte + response, err = util.PostJSON(uri, req) + if err != nil { + return + } + var result resUserList + err = json.Unmarshal(response, &result) + if err != nil { + return + } + if result.ErrCode != 0 { + err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg) + return + } + userList = result.ExternalContactList + nextCursor = result.NextCursor + return +} diff --git a/work/js/js.go b/work/js/js.go index 4d2a23b..dc4a8f1 100644 --- a/work/js/js.go +++ b/work/js/js.go @@ -23,7 +23,7 @@ type Config struct { Signature string `json:"signature"` } -// NewJs init +//NewJs init func NewJs(context *context.Context) *Js { js := new(Js) js.Context = context @@ -32,13 +32,13 @@ func NewJs(context *context.Context) *Js { return js } -// SetJsTicketHandle 自定义js ticket取值方式 +//SetJsTicketHandle 自定义js ticket取值方式 func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) { js.JsTicketHandle = ticketHandle } -// GetConfig 获取jssdk需要的配置参数 -// uri 为当前网页地址 +//GetConfig 获取jssdk需要的配置参数 +//uri 为当前网页地址 func (js *Js) GetConfig(uri string) (config *Config, err error) { config = new(Config) var accessToken string @@ -65,8 +65,8 @@ func (js *Js) GetConfig(uri string) (config *Config, err error) { return } -// GetAgentConfig 获取jssdk需要的配置参数 -// uri 为当前网页地址 +//GetAgentConfig 获取jssdk需要的配置参数 +//uri 为当前网页地址 func (js *Js) GetAgentConfig(uri string) (config *Config, err error) { config = new(Config) var accessToken string diff --git a/work/message/README.md b/work/message/README.md new file mode 100644 index 0000000..e69de29 diff --git a/work/message/group.go b/work/message/group.go new file mode 100644 index 0000000..ede1b09 --- /dev/null +++ b/work/message/group.go @@ -0,0 +1 @@ +package message diff --git a/work/message/image.go b/work/message/image.go new file mode 100644 index 0000000..b877042 --- /dev/null +++ b/work/message/image.go @@ -0,0 +1,16 @@ +package message + +//Image 图片消息 +type Image struct { + CommonToken `json:"-"` + Image struct { + MediaID string `xml:"MediaId" json:"media_id"` + } `xml:"Image" json:"image"` +} + +//NewImage 回复图片消息 +func NewImage(mediaID string) *Image { + image := new(Image) + image.Image.MediaID = mediaID + return image +} diff --git a/work/message/message.go b/work/message/message.go index ef6d24d..db4a8aa 100644 --- a/work/message/message.go +++ b/work/message/message.go @@ -24,7 +24,7 @@ type ( // 消息类型,此时固定为:text MsgType string `json:"msgtype"` // 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 - AgentID string `json:"agentid"` + AgentID int `json:"agentid"` // 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 Safe int `json:"safe"` // 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。 diff --git a/work/message/news.go b/work/message/news.go new file mode 100644 index 0000000..b4cd16e --- /dev/null +++ b/work/message/news.go @@ -0,0 +1,41 @@ +package message + +//News 图文消息 +type News struct { + CommonToken `json:"-"` + ArticleCount int `xml:"ArticleCount" json:"-"` + Articles []*Article `xml:"Articles>item,omitempty" json:"articles"` +} + +//NewNews 初始化图文消息 +func NewNews(articles []*Article) *News { + news := new(News) + news.ArticleCount = len(articles) + news.Articles = articles + return news +} + +//Article 单篇文章 +type Article struct { + Title string `xml:"Title,omitempty" json:"title"` + Description string `xml:"Description,omitempty" json:"description"` + PicURL string `xml:"PicUrl,omitempty" json:"picurl"` + URL string `xml:"Url,omitempty" json:"url"` + Appid string `xml:"-" json:"appid"` //仅在发送应用消息时需要 + Pagepath string `xml:"-" json:"pagepath"` //仅在发送应用消息时需要 +} + +//MpNews 图文消息 +type MpNews struct { + Articles []*MpNewsArticle `xml:"-" json:"articles"` +} + +//MpNewsArticle mpnews类型的图文消息,跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信 +type MpNewsArticle struct { + Title string `json:"title"` + ThumbMediaId string `json:"thumb_media_id"` + Author string `json:"author"` + ContentSourceUrl string `json:"content_source_url"` + Content string `json:"content"` + Digest string `json:"digest"` +} diff --git a/work/message/reply.go b/work/message/reply.go index a6e4dc7..8f53903 100644 --- a/work/message/reply.go +++ b/work/message/reply.go @@ -2,13 +2,13 @@ package message import "errors" -// ErrInvalidReply 无效的回复 +//ErrInvalidReply 无效的回复 var ErrInvalidReply = errors.New("无效的回复消息") -// ErrUnsupportReply 不支持的回复类型 +//ErrUnsupportReply 不支持的回复类型 var ErrUnsupportReply = errors.New("无需回复消息") -// Reply 消息回复 +//Reply 消息回复 type Reply struct { MsgType MsgType MsgData interface{} diff --git a/work/message/text.go b/work/message/text.go new file mode 100644 index 0000000..46d3bf4 --- /dev/null +++ b/work/message/text.go @@ -0,0 +1,14 @@ +package message + +//Text 文本消息 +type Text struct { + CommonToken `json:"-"` + Content CDATA `json:"content" xml:"Content"` +} + +//NewText 初始化文本消息 +func NewText(content string) *Text { + text := new(Text) + text.Content = CDATA(content) + return text +} diff --git a/work/message/video.go b/work/message/video.go new file mode 100644 index 0000000..4a60307 --- /dev/null +++ b/work/message/video.go @@ -0,0 +1,20 @@ +package message + +//Video 视频消息 +type Video struct { + CommonToken `json:"-"` + Video struct { + MediaID string `xml:"MediaId" json:"media_id"` + Title string `xml:"Title,omitempty" json:"title"` + Description string `xml:"Description,omitempty" json:"description"` + } `xml:"Video" json:"video"` +} + +//NewVideo 回复图片消息 +func NewVideo(mediaID, title, description string) *Video { + video := new(Video) + video.Video.MediaID = mediaID + video.Video.Title = title + video.Video.Description = description + return video +} diff --git a/work/message/voice.go b/work/message/voice.go new file mode 100644 index 0000000..4a389c0 --- /dev/null +++ b/work/message/voice.go @@ -0,0 +1,16 @@ +package message + +//Voice 语音消息 +type Voice struct { + CommonToken `json:"-"` + Voice struct { + MediaID string `xml:"MediaId" json:"media_id"` + } `xml:"Voice" json:"voice"` +} + +//NewVoice 回复语音消息 +func NewVoice(mediaID string) *Voice { + voice := new(Voice) + voice.Voice.MediaID = mediaID + return voice +} diff --git a/work/oauth/user.go b/work/oauth/user.go new file mode 100644 index 0000000..581c758 --- /dev/null +++ b/work/oauth/user.go @@ -0,0 +1,64 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "github.com/silenceper/wechat/v2/util" +) + +const ( + code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code" + launchCode = "https://qyapi.weixin.qq.com/cgi-bin/get_launch_code?access_token=%s" +) + +func (ctr *Oauth) Code2Session(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(code2SessionURL, 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 +} + +type RespLaunchCode struct { + util.CommonError + LaunchCode string `json:"launch_code"` +} + +// GetLaunchCode 用于打开个人聊天窗口schema +func (ctr *Oauth) GetLaunchCode(userID, other string) (userInfo *RespLaunchCode, err error) { + var accessToken string + accessToken, err = ctr.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(launchCode, accessToken) + var response []byte + response, err = util.PostJSON(uri, map[string]interface{}{"operator_userid": userID, "single_chat": map[string]string{"userid": other}}) + if err != nil { + return + } + userInfo = new(RespLaunchCode) + err = json.Unmarshal(response, userInfo) + if err != nil { + return + } + if userInfo.ErrCode != 0 { + err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg) + return + } + return +} diff --git a/work/server/error.go b/work/server/error.go index 6d8100f..d6dfa10 100644 --- a/work/server/error.go +++ b/work/server/error.go @@ -24,7 +24,7 @@ const ( SDKUnknownError Error = "未知错误" ) -// Error 输出错误信息 +//Error 输出错误信息 func (r Error) Error() string { return reflect.ValueOf(r).String() } diff --git a/work/server/server.go b/work/server/server.go index 3380011..5f0298a 100644 --- a/work/server/server.go +++ b/work/server/server.go @@ -16,7 +16,7 @@ import ( "github.com/silenceper/wechat/v2/util" ) -// Server struct +//Server struct type Server struct { *context.Context Writer http.ResponseWriter @@ -36,7 +36,7 @@ type Server struct { timestamp int64 } -// NewServer 实例化 +//NewServer init func NewServer(context *context.Context) *Server { srv := new(Server) srv.Context = context @@ -64,7 +64,7 @@ func (srv *Server) SkipValidate(skip bool) { srv.skipValidate = skip } -// Serve 处理企业微信的请求消息 +//Serve 处理企业微信的请求消息 func (srv *Server) Serve() error { response, err := srv.handleRequest() if err != nil { @@ -76,7 +76,7 @@ func (srv *Server) Serve() error { return srv.buildResponse(response) } -// Validate 校验请求是否合法 +//Validate 校验请求是否合法 func (srv *Server) Validate() bool { if srv.skipValidate { return true @@ -88,7 +88,7 @@ func (srv *Server) Validate() bool { return signature == util.Signature(srv.Token, timestamp, nonce) } -// HandleRequest 处理企业微信的请求 +//HandleRequest 处理企业微信的请求 func (srv *Server) handleRequest() (reply *message.Reply, err error) { var msg interface{} @@ -105,7 +105,7 @@ func (srv *Server) handleRequest() (reply *message.Reply, err error) { return } -// getMessage 解析企业微信返回的消息 +//getMessage 解析企业微信返回的消息 func (srv *Server) getMessage() (interface{}, error) { var rawXMLMsgBytes []byte var err error @@ -145,7 +145,7 @@ func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixM return } -// SetMessageHandler 设置用户自定义的回调方法 +//SetMessageHandler 设置用户自定义的回调方法 func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) { srv.messageHandler = handler } @@ -199,7 +199,7 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) { return } -// Send 将自定义的消息发送 +//Send 将自定义的消息发送 func (srv *Server) Send() (err error) { replyMsg := srv.ResponseMsg log.Debugf("response msg =%+v", replyMsg) diff --git a/work/server/util.go b/work/server/util.go index 574d014..6c108fa 100644 --- a/work/server/util.go +++ b/work/server/util.go @@ -15,7 +15,7 @@ func writeContextType(w http.ResponseWriter, value []string) { } } -// Render render from bytes +//Render render from bytes func (srv *Server) Render(bytes []byte) { //debug //fmt.Println("response msg = ", string(bytes)) @@ -26,13 +26,13 @@ func (srv *Server) Render(bytes []byte) { } } -// String render from string +//String render from string func (srv *Server) String(str string) { writeContextType(srv.Writer, plainContentType) srv.Render([]byte(str)) } -// XML render to xml +//XML render to xml func (srv *Server) XML(obj interface{}) { writeContextType(srv.Writer, xmlContentType) bytes, err := xml.Marshal(obj) diff --git a/work/tools/calendar.go b/work/tools/calendar.go new file mode 100644 index 0000000..e69de29 diff --git a/work/user/user.go b/work/user/user.go new file mode 100644 index 0000000..93d9012 --- /dev/null +++ b/work/user/user.go @@ -0,0 +1,194 @@ +package user + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/context" +) + +const ( + userInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s" + updateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=1" + userListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get" +) + +// User 用户管理 +type User struct { + *context.Context +} + +// NewUser 实例化 +func NewUser(context *context.Context) *User { + user := new(User) + user.Context = context + return user +} + +// Info 用户基本信息 +type Info struct { + util.CommonError + Userid string `json:"userid"` + Name string `json:"name"` + Department []int `json:"department"` + Order []int `json:"order"` + Position string `json:"position"` + Mobile string `json:"mobile"` + Gender string `json:"gender"` + Email string `json:"email"` + IsLeaderInDept []int `json:"is_leader_in_dept"` + Avatar string `json:"avatar"` + ThumbAvatar string `json:"thumb_avatar"` + Telephone string `json:"telephone"` + Alias string `json:"alias"` + Address string `json:"address"` + OpenUserid string `json:"open_userid"` + MainDepartment int `json:"main_department"` + Extattr struct { + Attrs []struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + Url string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + } `json:"attrs"` + } `json:"extattr"` + Status int `json:"status"` + QrCode string `json:"qr_code"` + ExternalPosition string `json:"external_position"` + ExternalProfile struct { + ExternalCorpName string `json:"external_corp_name"` + WechatChannels struct { + Nickname string `json:"nickname"` + Status int `json:"status"` + } `json:"wechat_channels"` + ExternalAttr []struct { + Type int `json:"type"` + Name string `json:"name"` + Text struct { + Value string `json:"value"` + } `json:"text,omitempty"` + Web struct { + Url string `json:"url"` + Title string `json:"title"` + } `json:"web,omitempty"` + Miniprogram struct { + Appid string `json:"appid"` + Pagepath string `json:"pagepath"` + Title string `json:"title"` + } `json:"miniprogram,omitempty"` + } `json:"external_attr"` + } `json:"external_profile"` +} + +// OpenidList 用户列表 +type OpenidList struct { + util.CommonError + + Total int `json:"total"` + Count int `json:"count"` + Data struct { + OpenIDs []string `json:"openid"` + } `json:"data"` + NextOpenID string `json:"next_openid"` +} + +// GetUserInfo 获取用户基本信息 +func (user *User) GetUserInfo(userID string) (userInfo *Info, err error) { + var accessToken string + accessToken, err = user.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(userInfoURL, accessToken, userID) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + userInfo = new(Info) + err = json.Unmarshal(response, userInfo) + if err != nil { + return + } + if userInfo.ErrCode != 0 { + err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg) + return + } + return +} + +// Update 更新员工资料 +func (user *User) Update(userID, external_position string) (err error) { + var accessToken string + accessToken, err = user.GetAccessToken() + if err != nil { + return + } + + uri := fmt.Sprintf(updateURL, accessToken, userID) + var response []byte + response, err = util.PostJSON(uri, map[string]string{"userid": userID, "external_position": external_position}) + if err != nil { + return + } + + return util.DecodeWithCommonError(response, "updateURL") +} + +// ListUserOpenIDs 返回用户列表 +func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) { + accessToken, err := user.GetAccessToken() + if err != nil { + return nil, err + } + + uri, _ := url.Parse(userListURL) + q := uri.Query() + q.Set("access_token", accessToken) + if len(nextOpenid) > 0 && nextOpenid[0] != "" { + q.Set("next_openid", nextOpenid[0]) + } + uri.RawQuery = q.Encode() + + response, err := util.HTTPGet(uri.String()) + if err != nil { + return nil, err + } + + userlist := OpenidList{} + + err = util.DecodeWithError(response, &userlist, "ListUserOpenIDs") + if err != nil { + return nil, err + } + + return &userlist, nil +} + +// ListAllUserOpenIDs 返回所有用户OpenID列表 +func (user *User) ListAllUserOpenIDs() ([]string, error) { + nextOpenid := "" + openids := make([]string, 0) + count := 0 + for { + ul, err := user.ListUserOpenIDs(nextOpenid) + if err != nil { + return nil, err + } + openids = append(openids, ul.Data.OpenIDs...) + count += ul.Count + if ul.Total > count { + nextOpenid = ul.NextOpenID + } else { + return openids, nil + } + } +}