package service
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"html"
"io"
"log"
"math/rand"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"xiawan/wx/api/req"
"xiawan/wx/api/vo"
"xiawan/wx/clientsdk/baseinfo"
"xiawan/wx/clientsdk/baseutils"
"xiawan/wx/db"
"xiawan/wx/srv/wxcore"
"xiawan/wx/srv/wxface"
"xiawan/wx/srv/wxtask"
"github.com/gin-gonic/gin"
"github.com/gogf/gf/container/garray"
"github.com/gorilla/websocket"
)
// AddMessageMgrService 添加要发送的消息进入消息管理器
func AddMessageMgrService(queryKey string, m req.SendMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
// 获取好友消息助手
friendMsgMgr := connect.GetWXFriendMsgMgr()
// 发送文本消息
if len(m.MsgItem) <= 0 {
return vo.NewFail("没有要加入消息管理器的消息!")
}
for _, item := range m.MsgItem {
//1 text 2 Image
if item.MsgType == 1 {
friendMsgMgr.AddNewTextMsg(item.TextContent, item.ToUserName)
} else if item.MsgType == 2 {
sImageBase := strings.Split(item.ImageContent, ",")
if len(sImageBase) > 1 {
item.ImageContent = sImageBase[1]
}
imageBytes, _ := base64.StdEncoding.DecodeString(item.ImageContent)
friendMsgMgr.AddImageMsg(imageBytes, item.ToUserName)
}
}
return vo.NewSuccess(gin.H{}, "已加入消息管理器队列")
})
}
func SendTestService(queryKey string, m req.SendMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
err := reqInvoker.SendAutoAuthRequest()
return vo.NewSuccessObj(err, "")
})
}
func WebSocketHandler(c *gin.Context, key string) {
connectMgr := WXServer.GetWXConnectMgr()
wxconn := connectMgr.GetWXConnectByUserInfoUUID(key)
if wxconn == nil {
c.JSON(http.StatusOK, vo.NewFail("WX 长链接不存在"))
return
}
currentTaskMgr := wxconn.GetWXTaskMgr()
taskMgr, _ := currentTaskMgr.(*wxcore.WXTaskMgr)
wsTask := taskMgr.SocketMsgTask
currentReqInvoker := wxconn.GetWXReqInvoker()
// 防止重复连接,如果已经有WebSocket连接,先清理掉
existingConn := wsTask.GetWebSocket(key)
if existingConn != nil {
fmt.Println("已存在WebSocket连接,正在清理...")
existingConn.Close()
wsTask.DeleteWebSocket(key)
}
// 启用此用户的WebSocket功能
wsTask.SetWebSocketEnabled(true)
conn, err := wsTask.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println("WebSocket升级失败:", err)
// 返回一个JSON错误响应,然后退出
c.JSON(http.StatusInternalServerError, gin.H{"error": "WebSocket升级失败: " + err.Error()})
return
}
// 设置WebSocket超时和心跳
conn.SetReadDeadline(time.Now().Add(300 * time.Second))
conn.SetPongHandler(func(appData string) error {
conn.SetReadDeadline(time.Now().Add(300 * time.Second)) // 收到Pong后刷新超时
fmt.Println("收到Pong消息")
return nil
})
// 更新WebSocket连接
wsTask.UpdateWebSocket(key, conn)
// 创建一个关闭函数,确保资源正确释放
cleanup := func() {
fmt.Println("正在关闭WebSocket连接...")
// 使用PushTypedMessageToClient来发送关闭消息,确保线程安全
_ = wxtask.PushTypedMessageToClient(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), conn)
_ = conn.Close()
wsTask.DeleteWebSocket(key)
}
// 确保在函数退出时资源被释放
defer cleanup()
// WebSocket连接建立时,先重置初始化状态为false,跳过历史数据
wxCache := wxconn.GetWXCache()
wxCache.SetInitNewSyncFinished(false)
fmt.Println("WebSocket连接已建立,正在跳过历史数据...")
// 等待5秒,让所有历史数据被跳过
time.Sleep(5 * time.Second)
// 延迟后标记初始化完成,开始接收实时消息
wxCache.SetInitNewSyncFinished(true)
fmt.Println("WebSocket初始化完成,开始接收实时消息")
// 创建一个心跳检测goroutine
stopPing := make(chan bool)
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 发送Ping消息,使用PushTypedMessageToClient来确保线程安全
if err := wxtask.PushTypedMessageToClient(websocket.PingMessage, nil, conn); err != nil {
fmt.Println("发送Ping失败:", err)
return
}
fmt.Println("已发送Ping消息")
case <-stopPing:
return
}
}
}()
// 处理WebSocket消息
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("WebSocket读取错误:", err)
close(stopPing) // 通知心跳goroutine退出
break // 连接断开,跳出循环,函数将退出
}
fmt.Printf("收到WebSocket消息[%d]: %s\n", messageType, string(message))
if string(message) == "ping" {
// 使用PushMessageToClient来发送pong响应,确保线程安全
err = wxtask.PushMessageToClient("pong", conn)
if err != nil {
fmt.Println("发送pong响应失败:", err)
break
}
} else if string(message) == "sync" {
_ = currentReqInvoker.SendNewSyncRequest(baseinfo.MMSyncSceneTypeNeed)
// 使用PushMessageToClient来发送完成消息,确保线程安全
err = wxtask.PushMessageToClient("sync completed", conn)
if err != nil {
fmt.Println("发送同步完成响应失败:", err)
break
}
}
}
fmt.Println("WebSocket连接断开")
}
// HttpSyncMsg HTTP-轮询 同步消息时 获取 Redis 内的消息
func HttpSyncMsg(queryKey string, count int) vo.DTO {
results, err := db.HttpSyncMsy(queryKey, count)
if err != nil {
return vo.NewFail("发生错误: " + err.Error())
}
return vo.NewSuccessObj(results, "")
}
// SendImageMessageService 发送图片消息
func SendImageMessageService(queryKey string, m req.SendMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
// 发送文本消息
if len(m.MsgItem) <= 0 {
return vo.NewFail("没有要加入消息管理器的消息!")
}
results := garray.New(true)
for _, item := range m.MsgItem {
sImageBase := strings.Split(item.ImageContent, ",")
if len(sImageBase) > 1 {
item.ImageContent = sImageBase[1]
}
imageBytes, _ := base64.StdEncoding.DecodeString(item.ImageContent)
imageId := baseutils.Md5ValueByte(imageBytes, false)
cdnUploadImageResp, err := reqInvoker.SendCdnUploadImageReuqest(imageBytes, item.ToUserName)
if err != nil {
results.Append(gin.H{
"imageId": imageId,
"toUSerName": item.ToUserName,
"isSendSuccess": cdnUploadImageResp,
"errMsg": err.Error(),
})
continue
} else {
results.Append(gin.H{
"imageId": imageId,
"toUSerName": item.ToUserName,
"isSendSuccess": cdnUploadImageResp,
})
}
}
return vo.NewSuccessObj(results, "")
})
}
// SendImageNewMessageService 发送图片New
func SendImageNewMessageService(queryKey string, m req.SendMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
// 发送文本消息
if len(m.MsgItem) <= 0 {
return vo.NewFail("没有要加入消息管理器的消息!")
}
results := garray.New(true)
for _, item := range m.MsgItem {
sImageBase := strings.Split(item.ImageContent, ",")
if len(sImageBase) > 1 {
item.ImageContent = sImageBase[1]
}
imageBytes, _ := base64.StdEncoding.DecodeString(item.ImageContent)
imageId := baseutils.Md5ValueByte(imageBytes, false)
resp, err := reqInvoker.SendUploadImageNewRequest(imageBytes, item.ToUserName)
if err != nil {
results.Append(gin.H{
"imageId": imageId,
"toUSerName": item.ToUserName,
"resp": resp,
"errMsg": err.Error(),
})
continue
} else {
results.Append(gin.H{
"imageId": imageId,
"toUSerName": item.ToUserName,
"resp": resp,
})
}
/*if len(m.MsgItem) > 1 {
v := rand.Intn(3)
if v == 0 || v == 1 {
time.Sleep(time.Second * 1)
} else if v == 2 {
time.Sleep(time.Second * 2)
} else {
time.Sleep(time.Second * 3)
}
}*/
}
return vo.NewSuccessObj(results, "")
})
}
// SendTextMessageService 发送文本消息
func SendTextMessageService(queryKey string, m req.SendMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
// 发送文本消息
if len(m.MsgItem) <= 0 {
return vo.NewFail("没有要加入消息管理器的消息!")
}
results := garray.New(true)
for _, item := range m.MsgItem {
resp, err := reqInvoker.SendTextMsgRequest(item.ToUserName, item.TextContent, item.AtWxIDList, item.MsgType)
if err != nil {
results.Append(gin.H{
"toUSerName": item.ToUserName,
"textContent": item.TextContent,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
results.Append(gin.H{
"textContent": item.TextContent,
"toUSerName": item.ToUserName,
"resp": resp,
"isSendSuccess": true,
})
}
if len(m.MsgItem) > 1 {
v := rand.Intn(3)
if v == 0 || v == 1 {
time.Sleep(time.Second * 1)
} else if v == 2 {
time.Sleep(time.Second * 2)
} else {
time.Sleep(time.Second * 3)
}
}
}
return vo.NewSuccessObj(results, "")
})
}
// 发送不显示好友消息
func SendTextMessageNoShowService(queryKey string, m req.MessageNoShowParam) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
// \n\nlastMessage\n{\"messageSvrId\":\"2454843855128515400\",\"MsgCreateTime\":\"1726013089\"}\n\n
// \n\nlastMessage\n\n\n
content := fmt.Sprintf("\n\nlastMessage\n\n\n")
reqInvoker := connect.GetWXReqInvoker()
// 发送文本消息
resp, err := reqInvoker.SendTextMsgRequest(m.ToUserName, content, []string{}, 10000)
var result gin.H
if err != nil {
result = gin.H{
"toUserName": m.ToUserName,
"textContent": "",
"isSendSuccess": false,
"errMsg": err.Error(),
}
} else {
result = gin.H{
"toUserName": m.ToUserName,
"textContent": "",
"resp": resp,
"isSendSuccess": true,
}
}
return vo.NewSuccessObj(result, "")
})
}
// ShareCardMessageService 分享名片消息
func ShareCardMessageService(queryKey string, m req.ShareCardParam) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
content := fmt.Sprintf("", m.CardWxId, m.CardNickName, m.CardAlias)
reqInvoker := connect.GetWXReqInvoker()
// 发送文本消息
resp, err := reqInvoker.SendTextMsgRequest(m.ToUserName, content, []string{}, 42)
var result gin.H
if err != nil {
result = gin.H{
"toUserName": m.ToUserName,
"textContent": "",
"isSendSuccess": false,
"errMsg": err.Error(),
}
} else {
result = gin.H{
"toUserName": m.ToUserName,
"textContent": "",
"resp": resp,
"isSendSuccess": true,
}
}
return vo.NewSuccessObj(result, "")
})
}
// ForwardImageMessageService 转发图片
func ForwardImageMessageService(queryKey string, m req.ForwardMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
results := garray.New(true)
if len(m.ForwardImageList) <= 0 {
return vo.NewFail("没有要进行转发的数据。")
}
for i, item := range m.ForwardImageList {
var cdnItem baseinfo.ForwardImageItem
StructCopy(&cdnItem, &item)
resp, err := reqInvoker.ForwardCdnImageRequest(cdnItem)
if err != nil {
results.Append(gin.H{
"toUSerName": item.ToUserName,
"cdnMidImgUrl": item.CdnMidImgUrl,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
isSendSuccess := false
if resp.GetBaseResponse().GetRet() == 0 {
isSendSuccess = true
}
results.Append(gin.H{
"cdnMidImgUrl": item.CdnMidImgUrl,
"toUSerName": item.ToUserName,
"isSendSuccess": isSendSuccess,
"resp": resp,
"retCode": resp.GetBaseResponse().GetRet(),
"errMsg": resp.GetBaseResponse().GetErrMsg().GetStr(),
})
}
//每两天延迟1秒
if i != 0 && i%2 == 0 {
time.Sleep(time.Second * 1)
}
}
return vo.NewSuccessObj(results.Interfaces(), "")
})
}
// ForwardVideoMessageService 转发视频
func ForwardVideoMessageService(queryKey string, m req.ForwardMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
results := garray.New(true)
if len(m.ForwardVideoList) <= 0 {
return vo.NewFail("没有要进行转发的数据。")
}
for i, item := range m.ForwardVideoList {
var cdnItem baseinfo.ForwardVideoItem
StructCopy(&cdnItem, &item)
resp, err := reqInvoker.ForwardCdnVideoRequest(cdnItem)
if err != nil {
results.Append(gin.H{
"toUSerName": item.ToUserName,
"CdnVideoUrl": item.CdnVideoUrl,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
isSendSuccess := false
if resp.GetBaseResponse().GetRet() == 0 {
isSendSuccess = true
}
results.Append(gin.H{
"CdnVideoUrl": item.CdnVideoUrl,
"toUSerName": item.ToUserName,
"isSendSuccess": isSendSuccess,
"resp": resp,
"retCode": resp.GetBaseResponse().GetRet(),
"errMsg": resp.GetBaseResponse().GetErrMsg().GetStr(),
})
}
//每两天延迟1秒
if i != 0 && i%2 == 0 {
time.Sleep(time.Second * 1)
}
}
return vo.NewSuccessObj(results.Interfaces(), "")
})
}
// SendAppMessageService 发送app消息
func SendAppMessageService(queryKey string, m req.AppMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
results := garray.New(true)
if len(m.AppList) == 0 {
return vo.NewFail("没有要进行发送的数据。")
}
for _, item := range m.AppList {
resp, err := reqInvoker.SendAppMessage(item.ContentXML, item.ToUserName, item.ContentType)
if err != nil {
results.Append(gin.H{
"contentXML": item.ContentXML,
"toUserName": item.ToUserName,
"contentType": item.ContentType,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
isSendSuccess, retCode := false, resp.GetBaseResponse().GetRet()
if resp.GetBaseResponse().GetRet() == 0 {
isSendSuccess = true
}
results.Append(gin.H{
"contentXML": item.ContentXML,
"toUserName": item.ToUserName,
"contentType": item.ContentType,
"isSendSuccess": isSendSuccess,
"resp": resp,
"retCode": retCode,
})
}
}
return vo.NewSuccessObj(results.Interfaces(), "")
})
}
// SendEmojiMessageService 发送表情
func SendEmojiMessageService(queryKey string, m req.SendEmojiMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
results := garray.New(true)
if len(m.EmojiList) <= 0 {
return vo.NewFail("没有要进行转发的数据。")
}
for i, item := range m.EmojiList {
resp, err := reqInvoker.SendEmojiRequest(item.EmojiMd5, item.ToUserName, item.EmojiSize)
if err != nil {
results.Append(gin.H{
"toUSerName": item.ToUserName,
"EmojiMd5": item.EmojiMd5,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
isSendSuccess := false
if resp.GetBaseResponse().GetRet() == 0 {
isSendSuccess = true
}
results.Append(gin.H{
"EmojiMd5": item.EmojiMd5,
"toUSerName": item.ToUserName,
"isSendSuccess": isSendSuccess,
"retCode": resp.GetBaseResponse().GetRet(),
"errMsg": resp.GetBaseResponse().GetErrMsg().GetStr(),
})
}
//每两天延迟1秒
if i != 0 && i%2 == 0 {
time.Sleep(time.Second * 1)
}
}
return vo.NewSuccessObj(results.Interfaces(), "")
})
}
// ForwardEmojiService 发送表情&包含动图
func ForwardEmojiService(queryKey string, m req.SendEmojiMessageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
results := garray.New(true)
if len(m.EmojiList) <= 0 {
return vo.NewFail("没有要进行转发的数据。")
}
for i, item := range m.EmojiList {
resp, err := reqInvoker.ForwardEmojiRequest(item.EmojiMd5, item.ToUserName, item.EmojiSize)
if err != nil {
results.Append(gin.H{
"toUSerName": item.ToUserName,
"EmojiMd5": item.EmojiMd5,
"isSendSuccess": false,
"errMsg": err.Error(),
})
continue
} else {
isSendSuccess := false
if resp.GetBaseResponse().GetRet() == 0 {
isSendSuccess = true
}
results.Append(gin.H{
"EmojiMd5": item.EmojiMd5,
"toUSerName": item.ToUserName,
"isSendSuccess": isSendSuccess,
"retCode": resp.GetBaseResponse().GetRet(),
"errMsg": resp.GetBaseResponse().GetErrMsg().GetStr(),
})
}
//每两天延迟1秒
if i != 0 && i%2 == 0 {
time.Sleep(time.Second * 1)
}
}
return vo.NewSuccessObj(results.Interfaces(), "")
})
}
// RevokeMsgService 撤销消息
func RevokeMsgService(queryKey string, m req.RevokeMsgModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendRevokeMsgRequest(m.NewMsgId, m.ClientMsgId, m.ToUserName)
if err != nil {
return vo.NewFail("RevokeMsgService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// RevokeMsgNewService 撤回消息New
func RevokeMsgNewService(queryKey string, m req.RevokeMsgModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendRevokeMsgRequestNew(m)
if err != nil {
return vo.NewFail("RevokeMsgNewService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// UploadVoiceRequestService 发送语音
func UploadVoiceRequestService(queryKey string, m req.SendUploadVoiceRequestModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
/*voiceData, err := base64.StdEncoding.DecodeString(m.VoiceData)
if err != nil {
return vo.NewFail("UploadVoiceRequestService base64.StdEncoding.DecodeString err" + err.Error())
}*/
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendUploadVoiceRequest(m.ToUserName, m.VoiceData, m.VoiceSecond, m.VoiceFormat)
if err != nil {
return vo.NewFail("UploadVoiceRequestService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// SendCdnUploadVideoRequestService UploadVoiceRequestService 发送视频
func SendCdnUploadVideoRequestService(queryKey string, m req.CdnUploadVideoRequest) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendCdnUploadVideoRequest(m.ToUserName, m.ThumbData, m.VideoData)
if err != nil {
return vo.NewFail("UploadVoiceRequestService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// SendCdnDownloadService 下载请求
func SendCdnDownloadService(queryKey string, m req.DownMediaModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
item := baseinfo.DownMediaItem{
AesKey: m.AesKey,
FileURL: m.FileURL,
FileType: m.FileType,
}
errorCdn := reqInvoker.SendGetCDNDnsRequest()
if errorCdn != nil {
return vo.NewFail("SendGetCDNDnsRequest err:" + errorCdn.Error())
}
resp, err := reqInvoker.SendCdnDownloadReuqest(&item)
if err != nil {
return vo.NewFail("UploadVoiceRequestService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// GetMsgBigImgService 获取图片请求
func GetMsgBigImgService(queryKey string, m req.DownloadParam) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.GetMsgBigImg(m)
if err != nil {
return vo.NewFail("GetMsgBigImgService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// GetMsgVideoService 获取视频请求
func GetMsgVideoService(queryKey string, m req.DownloadParam) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.GetMsgVideo(m)
if err != nil {
return vo.NewFail("GetMsgVideoService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// GroupMassMsgTextService 群发文字
func GroupMassMsgTextService(queryKey string, m req.GroupMassMsgTextModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendGroupMassMsgTextRequest(m.ToUserName, m.Content)
if err != nil {
return vo.NewFail("GroupMassMsgTextService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// GroupMassMsgImageService 群发图片
func GroupMassMsgImageService(queryKey string, m req.GroupMassMsgImageModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
imageBytes, _ := base64.StdEncoding.DecodeString(m.ImageBase64)
resp, err := reqInvoker.SendGroupMassMsgImageRequest(m.ToUserName, imageBytes)
if err != nil {
return vo.NewFail("GroupMassMsgImageService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// SendPatService 群拍一拍
func SendPatService(queryKey string, m req.SendPatModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendSendPatRequest(m.ChatRoomName, m.ToUserName, m.Scene)
if err != nil {
return vo.NewFail("GroupMassMsgTextService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// GetMsgVoiceService 下载语音
func GetMsgVoiceService(queryKey string, m req.DownloadVoiceModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendGetMsgVoiceRequest(m.ToUserName, m.NewMsgId, m.Bufid, m.Length)
if err != nil {
return vo.NewFail("GetMsgVoiceService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "")
})
}
// NewSyncHistoryMessageService 同步历史消息
func NewSyncHistoryMessageService(queryKey string) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(iwxConnect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := iwxConnect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !iwxConnect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := iwxConnect.GetWXReqInvoker()
resp, err := reqInvoker.SendWXSyncContactRequest()
if err != nil {
return vo.NewFail("NewSyncHistoryMessageService err:" + err.Error())
}
return vo.NewSuccessObj(resp, "成功")
})
}
// EmojiXML 表情XML结构体
type EmojiXML struct {
XMLName xml.Name `xml:"msg"`
Emoji struct {
EncryptURL string `xml:"encrypturl,attr"`
AESKey string `xml:"aeskey,attr"`
MD5 string `xml:"md5,attr"`
} `xml:"emoji"`
}
// EmojiDownloadResult 表情下载结果
type EmojiDownloadResult struct {
MD5 string `json:"md5"`
EncryptURL string `json:"encrypt_url"`
AESKey string `json:"aes_key"`
DecryptData string `json:"decrypt_data"` // base64编码的解密后数据
FileExt string `json:"file_ext"`
}
// sanitizeXMLEntities 修复非法 & 为合法 XML 实体
func sanitizeXMLEntities(xmlStr string) string {
xmlStr = html.UnescapeString(xmlStr)
// 先替换所有 & 为临时标记,然后恢复合法实体
xmlStr = strings.ReplaceAll(xmlStr, "&", "__AMP__")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__amp;", "&")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__lt;", "<")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__gt;", ">")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__quot;", """)
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__apos;", "'")
// 处理数字实体 数字;
re := regexp.MustCompile(`__AMP__(#[0-9]{1,6};)`)
xmlStr = re.ReplaceAllString(xmlStr, "&$1")
// 剩余的 __AMP__ 替换为 &
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__", "&")
return xmlStr
}
// parseEmojiXML 解析表情XML
func parseEmojiXML(xmlStr string) (string, string, string, string, error) {
if strings.TrimSpace(xmlStr) == "" {
return "", "", "", "", fmt.Errorf("XML内容为空")
}
xmlStr = sanitizeXMLEntities(xmlStr)
var emojiXML EmojiXML
err := xml.Unmarshal([]byte(xmlStr), &emojiXML)
if err != nil {
return "", "", "", "", fmt.Errorf("解析XML失败: %v", err)
}
encryptURL := emojiXML.Emoji.EncryptURL
aesKey := emojiXML.Emoji.AESKey
md5 := emojiXML.Emoji.MD5
// 验证必要字段
if encryptURL == "" {
return "", "", "", "", fmt.Errorf("缺少encrypturl字段")
}
if aesKey == "" {
return "", "", "", "", fmt.Errorf("缺少aeskey字段")
}
if md5 == "" {
return "", "", "", "", fmt.Errorf("缺少md5字段")
}
// 从URL推断文件扩展名
ext := ".gif" // 默认为gif
if strings.Contains(encryptURL, ".jpg") || strings.Contains(encryptURL, ".jpeg") {
ext = ".jpg"
} else if strings.Contains(encryptURL, ".png") {
ext = ".png"
}
return encryptURL, aesKey, md5, ext, nil
}
// downloadFile 下载文件
func downloadFile(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("下载文件失败,状态码: %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取文件数据失败: %v", err)
}
return data, nil
}
// decryptFile 解密文件
func decryptFile(encryptedData []byte, aeskeyHex string) ([]byte, error) {
key, err := hex.DecodeString(aeskeyHex)
if err != nil {
return nil, fmt.Errorf("解析AES密钥失败: %v", err)
}
// 支持16字节(AES-128)和32字节(AES-256)密钥
if len(key) != 16 && len(key) != 32 {
return nil, fmt.Errorf("AES密钥长度错误,期望16或32字节,实际%d字节, AESKey: %s", len(key), aeskeyHex)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("创建AES加密器失败: %v", err)
}
if len(encryptedData) < aes.BlockSize {
return nil, errors.New("加密数据长度不足")
}
// 使用密钥的前16字节作为IV(与Python代码保持一致)
var iv []byte
if len(key) >= 16 {
iv = key[:16]
} else {
iv = key
}
mode := cipher.NewCBCDecrypter(block, iv)
// 确保数据长度是块大小的倍数
if len(encryptedData)%aes.BlockSize != 0 {
return nil, errors.New("加密数据长度不是块大小的倍数")
}
decryptedData := make([]byte, len(encryptedData))
mode.CryptBlocks(decryptedData, encryptedData)
// 移除PKCS7填充(与Python代码保持一致)
if len(decryptedData) > 0 {
padLen := int(decryptedData[len(decryptedData)-1])
if padLen > 0 && padLen < 16 && padLen <= len(decryptedData) {
decryptedData = decryptedData[:len(decryptedData)-padLen]
}
}
return decryptedData, nil
}
// processEmojiDownload 处理表情下载
func processEmojiDownload(xmlContent string) (*EmojiDownloadResult, error) {
// 解析XML获取下载信息
encryptURL, aesKey, md5, ext, err := parseEmojiXML(xmlContent)
if err != nil {
log.Printf("[EmojiDownload] 解析XML失败: %v, XML内容: %s", err, xmlContent)
return nil, fmt.Errorf("解析XML失败: %v", err)
}
log.Printf("[EmojiDownload] 开始下载表情: URL=%s, MD5=%s, AESKey=%s", encryptURL, md5, aesKey)
// 下载加密文件
encryptedData, err := downloadFile(encryptURL)
if err != nil {
log.Printf("[EmojiDownload] 下载文件失败: %v, URL: %s", err, encryptURL)
return nil, fmt.Errorf("下载文件失败: %v", err)
}
log.Printf("[EmojiDownload] 文件下载成功,大小: %d bytes", len(encryptedData))
// 解密文件
decryptedData, err := decryptFile(encryptedData, aesKey)
if err != nil {
log.Printf("[EmojiDownload] 解密文件失败: %v, AESKey: %s", err, aesKey)
return nil, fmt.Errorf("解密文件失败: %v", err)
}
log.Printf("[EmojiDownload] 文件解密成功,解密后大小: %d bytes", len(decryptedData))
// 将解密后的数据编码为base64
decryptedBase64 := base64.StdEncoding.EncodeToString(decryptedData)
log.Printf("[EmojiDownload] 表情处理完成,MD5: %s", md5)
result := &EmojiDownloadResult{
MD5: md5,
EncryptURL: encryptURL,
AESKey: aesKey,
DecryptData: decryptedBase64,
FileExt: ext,
}
return result, nil
}
// DownloadEmojiGifService 下载表情gif服务
func DownloadEmojiGifService(queryKey string, m req.DownloadEmojiModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
// 调用表情下载处理函数
result, err := processEmojiDownload(m.XmlContent)
if err != nil {
return vo.NewFail("处理表情下载失败: " + err.Error())
}
return vo.NewSuccessObj(result, "表情下载成功")
})
}
// UploadImageToCDNService 纯CDN图片上传
func UploadImageToCDNService(queryKey string, m req.UploadImageToCDNModel) vo.DTO {
return checkExIdPerformNoCreateConnect(queryKey, func(connect wxface.IWXConnect, newIWXConnect bool) vo.DTO {
wxAccount := connect.GetWXAccount()
loginState := wxAccount.GetLoginState()
//判断在线情况
if loginState == baseinfo.MMLoginStateNoLogin {
return vo.NewFail("该账号需要重新登录!loginState == MMLoginStateNoLogin ")
} else if !connect.CheckOnLineStatus() {
return vo.NewFail("账号离线,自动上线失败!loginState == " + strconv.Itoa(int(wxAccount.GetLoginState())))
}
reqInvoker := connect.GetWXReqInvoker()
if m.ImageContent == "" {
return vo.NewFail("没有要上传的图片!")
}
// 解析 base64 图片
imageContent := m.ImageContent
sImageBase := strings.Split(imageContent, ",")
if len(sImageBase) > 1 {
imageContent = sImageBase[1]
}
imageBytes, err := base64.StdEncoding.DecodeString(imageContent)
if err != nil {
return vo.NewFail("图片解码失败: " + err.Error())
}
imageId := baseutils.Md5ValueByte(imageBytes, false)
// 使用文件传输助手作为占位符
toUser := "filehelper"
// 使用默认 source 值 2
sourceValue := uint32(2)
var cdnResp *baseinfo.CdnImageUploadResponse
var aesKey string
cdnResp, aesKey, err = reqInvoker.SendCdnUploadImageReuqestWithSource(imageBytes, toUser, sourceValue)
if err != nil || cdnResp == nil {
errMsg := "上传失败"
if err != nil {
errMsg = err.Error()
}
return vo.NewFail(errMsg)
}
// 返回完整的CDN响应
result := gin.H{
"imageId": imageId,
"imageMD5": imageId,
"isSendSuccess": true,
"aesKey": aesKey,
"cdnResponse": gin.H{
"ver": cdnResp.Ver,
"seq": cdnResp.Seq,
"retCode": cdnResp.RetCode,
"fileKey": cdnResp.FileKey,
"recvLen": cdnResp.RecvLen,
"sKeyResp": cdnResp.SKeyResp,
"fileID": cdnResp.FileID,
"cdnBigImgUrl": cdnResp.CdnBigImgUrl,
"cdnMidImgUrl": cdnResp.CdnMidImgUrl,
"cdnThumbImgUrl": cdnResp.CdnThumbImgUrl,
"existFlag": cdnResp.ExistFlag,
"hitType": cdnResp.HitType,
"retrySec": cdnResp.RetrySec,
"isRetry": cdnResp.IsRetry,
"isOverLoad": cdnResp.IsOverLoad,
"isGetCDN": cdnResp.IsGetCDN,
"xClientIP": cdnResp.XClientIP,
},
}
return vo.NewSuccessObj(result, "")
})
}