diff --git a/work/externalcontact/add_msg_template.go b/work/externalcontact/add_msg_template.go new file mode 100644 index 0000000..cbb0d3f --- /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 (tpl *Client) Send(msg *ReqMessage) (msgID int64, err error) { + var accessToken string + accessToken, err = tpl.ctx.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/client.go b/work/externalcontact/client.go new file mode 100644 index 0000000..8574317 --- /dev/null +++ b/work/externalcontact/client.go @@ -0,0 +1,44 @@ +package externalcontact + +import ( + "errors" + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/credential" + "github.com/silenceper/wechat/v2/work/config" + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 企业微信客户联系实例 +type Client struct { + corpID string // 企业ID:企业开通的每个微信客服,都对应唯一的企业ID,企业可在微信客服管理后台的企业信息处查看 + secret string // Secret是微信客服用于校验开发者身份的访问密钥,企业成功注册微信客服后,可在「微信客服管理后台-开发配置」处获取 + token string // 用于生成签名校验回调请求的合法性 + encodingAESKey string // 回调消息加解密参数是AES密钥的Base64编码,用于解密回调消息内容对应的密文 + cache cache.Cache + ctx *context.Context +} + +// NewClient 初始化企业微信客户联系实例 +func NewClient(cfg *config.Config) (client *Client, err error) { + if cfg.Cache == nil { + return nil, errors.New("SDK初始化失败") + } + + //初始化 AccessToken Handle + defaultAkHandle := credential.NewWorkAccessToken(cfg.CorpID, cfg.CorpSecret, credential.CacheKeyWorkPrefix, cfg.Cache) + ctx := &context.Context{ + Config: cfg, + AccessTokenHandle: defaultAkHandle, + } + + client = &Client{ + corpID: cfg.CorpID, + secret: cfg.CorpSecret, + token: cfg.Token, + encodingAESKey: cfg.EncodingAESKey, + cache: cfg.Cache, + ctx: ctx, + } + + return client, nil +} diff --git a/work/externalcontact/user.go b/work/externalcontact/user.go new file mode 100644 index 0000000..c4fd2c1 --- /dev/null +++ b/work/externalcontact/user.go @@ -0,0 +1,211 @@ +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,个人标签将不再返回 +} + +type ExternalContact struct { + ExternalUserid string `json:"external_userid"` + Name string `json:"name"` + Position string `json:"position"` + Avatar string `json:"avatar"` + CorpName string `json:"corp_name"` + CorpFullName string `json:"corp_full_name"` + Type int `json:"type"` + Gender int `json:"gender"` + Unionid string `json:"unionid"` + ExternalProfile struct { + 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,omitempty"` +} + +type FollowInfo struct { + Userid string `json:"userid"` + Remark string `json:"remark"` + Description string `json:"description"` + Createtime int `json:"createtime"` + TagId []string `json:"tag_id"` //批量获取时才有 + Tags []struct { + GroupName string `json:"group_name"` + TagName string `json:"tag_name"` + TagId string `json:"tag_id,omitempty"` + Type int `json:"type"` + } `json:"tags,omitempty"` //单独获取时才有 + RemarkCorpName string `json:"remark_corp_name,omitempty"` + RemarkMobiles []string `json:"remark_mobiles,omitempty"` + OperUserid string `json:"oper_userid"` + AddWay int `json:"add_way"` + State string `json:"state,omitempty"` +} + +//GetUseridList 获取我的客户列表 +func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) { + var accessToken string + accessToken, err = tpl.ctx.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.ctx.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.ctx.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/work.go b/work/work.go index 451f81a..782d219 100644 --- a/work/work.go +++ b/work/work.go @@ -4,6 +4,7 @@ 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/externalcontact" "github.com/silenceper/wechat/v2/work/kf" "github.com/silenceper/wechat/v2/work/msgaudit" "github.com/silenceper/wechat/v2/work/oauth" @@ -65,3 +66,8 @@ func (wk *Work) GetUser() *user.User { func (wk *Work) GetCalendar() *tools.Calendar { return tools.NewCalendar(wk.ctx) } + +//GetExternalContact 客户联系 +func (wk *Work) GetExternalContact() (*externalcontact.Client, error) { + return externalcontact.NewClient(wk.ctx.Config) +}