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