kefu/lib/wechat_kf_sdk/kf.go

486 lines
12 KiB
Go
Raw Permalink Normal View History

2024-12-10 02:50:12 +00:00
package wechat_kf_sdk
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"github.com/patrickmn/go-cache"
"github.com/silenceper/wechat/v2/util"
"github.com/tidwall/gjson"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
// 定义获取access_token的响应数据结构体
type AccessTokenResponse struct {
ErrCode int `json:"errcode"` // 错误码
ErrMsg string `json:"errmsg"` // 错误信息
AccessToken string `json:"access_token"` // access_token
ExpiresIn int `json:"expires_in"` // 过期时间
}
// 定义微信客服API的封装结构体
type KefuWework struct {
corpid string // 企业ID
corpsecret string // 企业密钥
Token string // 令牌
EncodingAESKey string // AES加密密钥
mutex sync.Mutex // 互斥锁用于保护access_token的获取
}
// 微信回调消息
type WeixinUserAskMsg struct {
ToUserName string `xml:"ToUserName"`
CreateTime int64 `xml:"CreateTime"`
MsgType string `xml:"MsgType"`
Event string `xml:"Event"`
Token string `xml:"Token"`
OpenKfId string `xml:"OpenKfId"`
}
// 同步消息结果
type SyncMsgRet struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
NextCursor string `json:"next_cursor"`
MsgList []SyncMsg `json:"msg_list"`
}
type SyncMsg struct {
Msgid string `json:"msgid"`
SendTime int64 `json:"send_time"`
Origin int `json:"origin"`
Msgtype string `json:"msgtype"`
Event struct {
EventType string `json:"event_type"`
Scene string `json:"scene"`
OpenKfid string `json:"open_kfid"`
ExternalUserid string `json:"external_userid"`
WelcomeCode string `json:"welcome_code"`
} `json:"event"`
Text struct {
Content string `json:"content"`
} `json:"text"`
Image struct {
MediaId string `json:"media_id"`
} `json:"image"`
Voice struct {
MediaId string `json:"media_id"`
} `json:"voice"`
OpenKfid string `json:"open_kfid"`
ExternalUserid string `json:"external_userid"`
}
// 发送的文本消息
type SendMsgText struct {
Touser string `json:"touser,omitempty"`
OpenKfid string `json:"open_kfid,omitempty"`
Msgid string `json:"msgid,omitempty"`
Msgtype string `json:"msgtype,omitempty"`
Text struct {
Content string `json:"content,omitempty"`
} `json:"text,omitempty"`
Image struct {
MediaId string `json:"media_id,omitempty"`
} `json:"image,omitempty"`
Voice struct {
MediaId string `json:"media_id,omitempty"`
} `json:"voice,omitempty"`
}
var weworkCache = cache.New(5*time.Minute, 10*time.Minute) // 缓存用于存储access_token
// 创建微信客服API的封装结构体实例
func NewKefuWework(corpid string, corpsecret, Token, EncodingAESKey string) *KefuWework {
return &KefuWework{
corpid: corpid,
corpsecret: corpsecret,
Token: Token,
EncodingAESKey: EncodingAESKey,
}
}
// 获取access_token的函数
func (s *KefuWework) GetAccessToken() (string, error) {
// 加锁避免并发调用获取access_token接口
s.mutex.Lock()
defer s.mutex.Unlock()
// 判断access_token是否过期如果未过期则直接返回
cacheKey := "wework_access_" + s.corpid
if accessToken, ok := weworkCache.Get(cacheKey); ok {
return accessToken.(string), nil
}
// 发送GET请求构建请求URL
reqURL := fmt.Sprintf("%s?corpid=%s&corpsecret=%s", "https://qyapi.weixin.qq.com/cgi-bin/gettoken", s.corpid, s.corpsecret)
resp, err := http.Get(reqURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 解析响应数据到AccessTokenResponse结构体
var tokenResp AccessTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", err
}
// 判断获取access_token是否成功
if tokenResp.ErrCode != 0 {
return "", fmt.Errorf("GetAccessToken failed: %s", tokenResp.ErrMsg)
}
weworkCache.Set(cacheKey, tokenResp.AccessToken, time.Duration(tokenResp.ExpiresIn-3600)*time.Second)
log.Printf("GetAccessToken kefuWework:%s\n", tokenResp.AccessToken)
// 返回access_token
return tokenResp.AccessToken, nil
}
// GetAccountList 获取客服帐号列表包括所有的客服帐号的客服ID、名称和头像。
func (s *KefuWework) GetAccountList(offset, limit uint32) (string, error) {
var respData string
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return respData, err
}
// 构建请求数据
reqData := map[string]uint32{
"offset": offset, // 偏移量
"limit": limit, // 限制数量
}
reqBody, err := json.Marshal(reqData)
if err != nil {
return respData, err
}
// 发送POST请求
reqURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/account/list?access_token=%s", accessToken)
resp, err := http.Post(reqURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
return respData, err
}
defer resp.Body.Close()
ioBytes, _ := io.ReadAll(resp.Body)
return string(ioBytes), nil
}
// GetAccountList 获取客服帐号链接
func (s *KefuWework) GetAccountLink(openKfId string) (string, error) {
var respData string
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return respData, err
}
// 构建请求数据
reqData := map[string]string{
"open_kfid": openKfId,
"scene": "123456",
}
reqBody, err := json.Marshal(reqData)
if err != nil {
return respData, err
}
// 发送POST请求
reqURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/add_contact_way?access_token=%s", accessToken)
resp, err := http.Post(reqURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
return respData, err
}
defer resp.Body.Close()
ioBytes, _ := io.ReadAll(resp.Body)
return string(ioBytes), nil
}
// 发送同步客服消息请求
func (s *KefuWework) SyncMsg(reqData map[string]interface{}) (SyncMsgRet, error) {
var msgRet SyncMsgRet
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return msgRet, err
}
reqBody, err := json.Marshal(reqData)
if err != nil {
return msgRet, err
}
// 发送POST请求
reqURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=%s", accessToken)
resp, err := http.Post(reqURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
return msgRet, err
}
defer resp.Body.Close()
ioBytes, _ := ioutil.ReadAll(resp.Body)
json.Unmarshal(ioBytes, &msgRet)
return msgRet, nil
}
// 发送消息
func (s *KefuWework) SendMsg(reply interface{}) error {
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return err
}
url := "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=" + accessToken
reqBody, err := json.Marshal(reply)
if err != nil {
return err
}
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("SendMsg", body, err)
return err
}
log.Println("SendMsg", string(body))
return nil
}
// 发送文本消息
func (s *KefuWework) SendTextMsg(kfId, toUser, content string) error {
reply := SendMsgText{
Touser: toUser,
OpenKfid: kfId,
Msgid: "",
Msgtype: "text",
Text: struct {
Content string `json:"content,omitempty"`
}{
Content: content,
},
}
err := s.SendMsg(reply)
return err
}
// 发送图片消息消息
func (s *KefuWework) SendImagesMsg(kfId, toUser, path string) error {
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s", accessToken, "image")
response, err := util.PostFile("media", path, uri)
mediaId := gjson.Get(string(response), "media_id").String()
reply := SendMsgText{
Touser: toUser,
OpenKfid: kfId,
Msgid: "",
Msgtype: "image",
Image: struct {
MediaId string `json:"media_id,omitempty"`
}{
MediaId: mediaId,
},
}
err = s.SendMsg(reply)
return err
}
// 发送语音消息
func (s *KefuWework) SendVoiceMsg(kfId, toUser, path string) error {
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s", accessToken, "voice")
response, err := util.PostFile("media", path, uri)
mediaId := gjson.Get(string(response), "media_id").String()
reply := SendMsgText{
Touser: toUser,
OpenKfid: kfId,
Msgid: "",
Msgtype: "voice",
Voice: struct {
MediaId string `json:"media_id,omitempty"`
}{
MediaId: mediaId,
},
}
err = s.SendMsg(reply)
return err
}
// 发送客服欢迎语
func (s *KefuWework) SendWelcomeMsg(content, welcomeCode string) error {
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return err
}
// 构建请求数据
reqData := map[string]interface{}{
"code": welcomeCode,
"msgtype": "text",
"text": map[string]string{
"content": content,
},
}
reqBody, err := json.Marshal(reqData)
if err != nil {
log.Println("SendWelcomeMsg", err)
return err
}
// 发送POST请求
reqURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token=%s", accessToken)
resp, err := http.Post(reqURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
log.Println("SendWelcomeMsg", err)
return err
}
defer resp.Body.Close()
resBody, err := io.ReadAll(resp.Body)
log.Println("SendWelcomeMsg", string(resBody), err)
if err != nil {
log.Println("SendWelcomeMsg", err)
return err
}
return nil
}
// 发送WEBHOOK
func (s *KefuWework) SendWebHook(url, content string) error {
// 获取access_token
_, err := s.GetAccessToken()
if err != nil {
return err
}
return nil
}
// 获取客户列表
func (s *KefuWework) BatchGet(reqData map[string]interface{}) (string, error) {
var respData string
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return respData, err
}
reqBody, err := json.Marshal(reqData)
if err != nil {
return respData, err
}
// 发送POST请求
reqURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=%s", accessToken)
resp, err := http.Post(reqURL, "application/json", bytes.NewReader(reqBody))
if err != nil {
return respData, err
}
defer resp.Body.Close()
ioBytes, _ := io.ReadAll(resp.Body)
return string(ioBytes), nil
}
// 验证签名
func (s *KefuWework) CheckSign(signature, timestamp, nonce, echostr string) (string, error) {
wxcpt := NewWXBizMsgCrypt(s.Token, s.EncodingAESKey, s.corpid, XmlType)
echoStr, cryptErr := wxcpt.VerifyURL(signature, timestamp, nonce, echostr)
if cryptErr != nil {
return "", errors.New(cryptErr.ErrMsg)
}
return string(echoStr), nil
}
// 解析数据
func (s *KefuWework) DecryptMsg(signature, timestamp, nonce, data string) (WeixinUserAskMsg, error) {
var weixinUserAskMsg WeixinUserAskMsg
wxcpt := NewWXBizMsgCrypt(s.Token, s.EncodingAESKey, s.corpid, XmlType)
msg, cryptErr := wxcpt.DecryptMsg(signature, timestamp, nonce, []byte(data))
if cryptErr != nil {
return weixinUserAskMsg, errors.New(cryptErr.ErrMsg)
}
err := xml.Unmarshal(msg, &weixinUserAskMsg)
if err != nil {
return weixinUserAskMsg, err
}
return weixinUserAskMsg, nil
}
// 下载临时文件
func (s *KefuWework) DownloadTempFileByMediaID(mediaId, path string) (string, error) {
respData := ""
// 获取access_token
accessToken, err := s.GetAccessToken()
if err != nil {
return respData, err
}
// 发起HTTP GET请求
url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s", accessToken, mediaId)
response, err := http.Get(url)
if err != nil {
return respData, err
}
defer response.Body.Close()
// 创建本地文件用于保存下载的文件
file, err := os.Create(path)
if err != nil {
return respData, err
}
defer file.Close()
// 将HTTP响应的内容复制到本地文件
_, err = io.Copy(file, response.Body)
if err != nil {
return respData, err
}
return path, nil
}
// ConvertXMLToJSON 将 XML 转换为 JSON 字符串
func ConvertXMLToJSON(xmlStr string) (string, error) {
var result interface{}
err := xml.Unmarshal([]byte(xmlStr), &result)
if err != nil {
return "", err
}
jsonStr, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(jsonStr), nil
}
func parseFileName(response *http.Response) string {
headers := response.Header["Content-Disposition"]
if len(headers) > 0 {
// 提取文件名
fileParts := strings.Split(headers[0], "=")
if len(fileParts) > 1 {
return fileParts[1]
}
}
return ""
}