diff --git a/doc/api/officialaccount.md b/doc/api/officialaccount.md index 8b3bf87..46d0bb6 100644 --- a/doc/api/officialaccount.md +++ b/doc/api/officialaccount.md @@ -35,12 +35,14 @@ | 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | | ---------------- | --------- | -------------------------------------- | ---------- | -------- | -| 获取客服基本信息 | GET | /cgi-bin/customservice/getkflist | NO | | -| 添加客服帐号 | POST | /customservice/kfaccount/add | NO | | -| 邀请绑定客服帐号 | POST | /customservice/kfaccount/inviteworker | NO | | -| 设置客服信息 | POST | /customservice/kfaccount/update | NO | | -| 上传客服头像 | POST/FORM | /customservice/kfaccount/uploadheadimg | NO | | -| 删除客服帐号 | GET | /customservice/kfaccount/del | NO | | +| 获取客服基本信息 | GET | /cgi-bin/customservice/getkflist | YES | (csm *Manager) List | +| 添加客服帐号 | POST | /customservice/kfaccount/add | YES | (csm *Manager) Add | +| 邀请绑定客服帐号 | POST | /customservice/kfaccount/inviteworker | YES | (csm *Manager) InviteBind | +| 设置客服信息 | POST | /customservice/kfaccount/update | YES | (csm *Manager) Update | +| 上传客服头像 | POST/FORM | /customservice/kfaccount/uploadheadimg | YES | (csm *Manager) UploadHeadImg | +| 删除客服帐号 | POST | /customservice/kfaccount/del | YES | (csm *Manager) Delete | +| 获取在线客服 | POST | /cgi-bin/customservice/getonlinekflist| YES | (csm *Manager) OnlineList | +| 下发客服输入状态 | POST | /cgi-bin/message/custom/typing | YES | (csm *Manager) SendTypingStatus | #### 会话控制 diff --git a/officialaccount/customerservice/manager.go b/officialaccount/customerservice/manager.go new file mode 100644 index 0000000..3ea2dd9 --- /dev/null +++ b/officialaccount/customerservice/manager.go @@ -0,0 +1,253 @@ +package customerservice + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +// TypingStatus 输入状态类型 +type TypingStatus string + +const ( + customerServiceListURL = "https://api.weixin.qq.com/cgi-bin/customservice/getkflist" + customerServiceOnlineListURL = "https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist" + customerServiceAddURL = "https://api.weixin.qq.com/customservice/kfaccount/add" + customerServiceUpdateURL = "https://api.weixin.qq.com/customservice/kfaccount/update" + customerServiceDeleteURL = "https://api.weixin.qq.com/customservice/kfaccount/del" + customerServiceInviteURL = "https://api.weixin.qq.com/customservice/kfaccount/inviteworker" + customerServiceUploadHeadImg = "https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg" + customerServiceTypingURL = "https://api.weixin.qq.com/cgi-bin/message/custom/typing" +) + +const ( + // Typing 表示正在输入状态 + Typing TypingStatus = "Typing" + // CancelTyping 表示取消正在输入状态 + CancelTyping TypingStatus = "CancelTyping" +) + +// Manager 客服管理者,可以管理客服 +type Manager struct { + *context.Context +} + +// NewCustomerServiceManager 实例化客服管理 +func NewCustomerServiceManager(ctx *context.Context) *Manager { + csm := new(Manager) + csm.Context = ctx + return csm +} + +// KeFuInfo 客服基本信息 +type KeFuInfo struct { + KfAccount string `json:"kf_account"` // 完整客服帐号,格式为:帐号前缀@公众号微信号 + KfNick string `json:"kf_nick"` // 客服昵称 + KfID int `json:"kf_id"` // 客服编号 + KfHeadImgURL string `json:"kf_headimgurl"` // 客服头像 + KfWX string `json:"kf_wx"` // 如果客服帐号已绑定了客服人员微信号, 则此处显示微信号 + InviteWX string `json:"invite_wx"` // 如果客服帐号尚未绑定微信号,但是已经发起了一个绑定邀请, 则此处显示绑定邀请的微信号 + InviteExpTime int `json:"invite_expire_time"` // 如果客服帐号尚未绑定微信号,但是已经发起过一个绑定邀请, 邀请的过期时间,为unix 时间戳 + InviteStatus string `json:"invite_status"` // 邀请的状态,有等待确认“waiting”,被拒绝“rejected”, 过期“expired” +} + +type resKeFuList struct { + util.CommonError + KfList []*KeFuInfo `json:"kf_list"` +} + +// List 获取所有客服基本信息 +func (csm *Manager) List() (customerServiceList []*KeFuInfo, err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceListURL, accessToken) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var res resKeFuList + err = util.DecodeWithError(response, &res, "ListCustomerService") + if err != nil { + return + } + customerServiceList = res.KfList + return +} + +// KeFuOnlineInfo 客服在线信息 +type KeFuOnlineInfo struct { + KfAccount string `json:"kf_account"` + Status int `json:"status"` + KfID int `json:"kf_id"` + AcceptedCase int `json:"accepted_case"` +} + +type resKeFuOnlineList struct { + util.CommonError + KfOnlineList []*KeFuOnlineInfo `json:"kf_online_list"` +} + +// OnlineList 获取在线客服列表 +func (csm *Manager) OnlineList() (customerServiceOnlineList []*KeFuOnlineInfo, err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceOnlineListURL, accessToken) + var response []byte + response, err = util.HTTPGet(uri) + if err != nil { + return + } + var res resKeFuOnlineList + err = util.DecodeWithError(response, &res, "ListOnlineCustomerService") + if err != nil { + return + } + customerServiceOnlineList = res.KfOnlineList + return +} + +// Add 添加客服账号 +func (csm *Manager) Add(kfAccount, nickName string) (err error) { + // kfAccount:完整客服帐号,格式为:帐号前缀@公众号微信号,帐号前缀最多10个字符,必须是英文、数字字符或者下划线,后缀为公众号微信号,长度不超过30个字符 + // nickName:客服昵称,最长16个字 + // 参数此处均不做校验 + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceAddURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + NickName string `json:"nickname"` + }{ + KfAccount: kfAccount, + NickName: nickName, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "AddCustomerService") + return +} + +// Update 修改客服账号 +func (csm *Manager) Update(kfAccount, nickName string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceUpdateURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + NickName string `json:"nickname"` + }{ + KfAccount: kfAccount, + NickName: nickName, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "UpdateCustomerService") + return +} + +// Delete 删除客服帐号 +func (csm *Manager) Delete(kfAccount string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceDeleteURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + }{ + KfAccount: kfAccount, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "DeleteCustomerService") + return +} + +// InviteBind 邀请绑定客服帐号和微信号 +func (csm *Manager) InviteBind(kfAccount, inviteWX string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceInviteURL, accessToken) + data := struct { + KfAccount string `json:"kf_account"` + InviteWX string `json:"invite_wx"` + }{ + KfAccount: kfAccount, + InviteWX: inviteWX, + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "InviteBindCustomerService") + return +} + +// UploadHeadImg 上传客服头像 +func (csm *Manager) UploadHeadImg(kfAccount, fileName string) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s&kf_account=%s", customerServiceUploadHeadImg, accessToken, kfAccount) + var response []byte + response, err = util.PostFile("media", fileName, uri) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "UploadCustomerServiceHeadImg") + return +} + +//SendTypingStatus 下发客服输入状态给用户 +func (csm *Manager) SendTypingStatus(openid string, cmd TypingStatus) (err error) { + var accessToken string + accessToken, err = csm.GetAccessToken() + if err != nil { + return + } + uri := fmt.Sprintf("%s?access_token=%s", customerServiceTypingURL, accessToken) + data := struct { + ToUser string `json:"touser"` + Command string `json:"command"` + }{ + ToUser: openid, + Command: string(cmd), + } + var response []byte + response, err = util.PostJSON(uri, data) + if err != nil { + return + } + err = util.DecodeWithCommonError(response, "SendTypingStatus") + return +} diff --git a/officialaccount/officialaccount.go b/officialaccount/officialaccount.go index d1b1a1a..ed3dc71 100644 --- a/officialaccount/officialaccount.go +++ b/officialaccount/officialaccount.go @@ -14,6 +14,7 @@ import ( "github.com/silenceper/wechat/v2/officialaccount/broadcast" "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/officialaccount/customerservice" "github.com/silenceper/wechat/v2/officialaccount/device" "github.com/silenceper/wechat/v2/officialaccount/js" "github.com/silenceper/wechat/v2/officialaccount/material" @@ -197,3 +198,8 @@ func (officialAccount *OfficialAccount) GetSubscribe() *message.Subscribe { } return officialAccount.subscribeMsg } + +// GetCustomerServiceManager 客服管理 +func (officialAccount *OfficialAccount) GetCustomerServiceManager() *customerservice.Manager { + return customerservice.NewCustomerServiceManager(officialAccount.ctx) +} diff --git a/util/error.go b/util/error.go index f9c835a..5687ce1 100644 --- a/util/error.go +++ b/util/error.go @@ -17,6 +17,15 @@ func (c *CommonError) Error() string { return fmt.Sprintf("%s Error , errcode=%d , errmsg=%s", c.apiName, c.ErrCode, c.ErrMsg) } +// NewCommonError 新建CommonError错误,对于无errcode和errmsg的返回也可以返回该通用错误 +func NewCommonError(apiName string, code int64, msg string) *CommonError { + return &CommonError{ + apiName: apiName, + ErrCode: code, + ErrMsg: msg, + } +} + // DecodeWithCommonError 将返回值按照CommonError解析 func DecodeWithCommonError(response []byte, apiName string) (err error) { var commError CommonError