Files
wechat_ipad_pro/api/service/messageService.go
2026-02-17 13:06:23 +08:00

1275 lines
43 KiB
Go
Raw 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 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())))
}
// <msg>\n<op id='14'>\n<name>lastMessage</name>\n<arg>{\"messageSvrId\":\"2454843855128515400\",\"MsgCreateTime\":\"1726013089\"}</arg>\n</op>\n</msg>
// <msg>\n<op id='14'>\n<name>lastMessage</name>\n<arg></arg>\n</op>\n</msg>
content := fmt.Sprintf("<msg>\n<op id='14'>\n<name>lastMessage</name>\n<arg></arg>\n</op>\n</msg>")
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("<msg username=\"%v\" nickname=\"%v\" fullpy=\"\" shortpy=\"\" alias=\"%v\" imagestatus=\"3\" scene=\"17\" province=\"\" city=\"\" sign=\"\" sex=\"1\" certflag=\"0\" certinfo=\"\" brandIconUrl=\"\" brandHomeUrl=\"\" brandSubscriptConfigUrl=\"\" brandFlags=\"0\" regionCode=\"CN\" ></msg>", 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;", "&amp;")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__lt;", "&lt;")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__gt;", "&gt;")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__quot;", "&quot;")
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__apos;", "&apos;")
// 处理数字实体 &#数字;
re := regexp.MustCompile(`__AMP__(#[0-9]{1,6};)`)
xmlStr = re.ReplaceAllString(xmlStr, "&$1")
// 剩余的 __AMP__ 替换为 &amp;
xmlStr = strings.ReplaceAll(xmlStr, "__AMP__", "&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, "")
})
}