kefu/lib/wechat_kf_sdk/kf.go

486 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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