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