之前在做微信服务号的时候会出现消息重复发送的情况,查阅了一些资料实现了消息去重的效果
具体流程是这样的
服务端接收消息,服务响应客户端(将此消息保存到消息队列中),服务端再次接收到消息,判断消息队列中是否存在此消息(通过时间来确定或者通过消息ID来确定,消息是否已经发送过),若存在则表示已经发送过该消息返回null值,若不存在则继续发送消息,对于延迟的消息也放入到队列中。
主要的中心点就是,你确保一个字段或者数据表明消息的唯一性,因为大部分服务对于消息延迟会有重复请求应答的情况,就会出现重发消息。
代码如下:
public string Executes(XmlElement rootElement)
{
if (_queue == null)
{
_queue = new List<BaseMsg>();
}
else if (_queue.Count >= 50)
{
_queue = _queue.Where(q => { return q.CreateTime.AddSeconds(20) > DateTime.Now; }).ToList();//保留20秒内未响应的消息
}
RequstXmlModel WxXmlModel = new RequstXmlModel();
WxXmlModel.ToUserName = rootElement.SelectSingleNode("ToUserName").InnerText;
WxXmlModel.FromUserName = rootElement.SelectSingleNode("FromUserName").InnerText;
WxXmlModel.CreateTime = rootElement.SelectSingleNode("CreateTime").InnerText;
WxXmlModel.MsgType = rootElement.SelectSingleNode("MsgType").InnerText;
if (WxXmlModel.MsgType != "event")
{
if (_queue.FirstOrDefault(m => { return m.MsgFlag == WxXmlModel.MsgID; }) == null)
{
_queue.Add(new BaseMsg
{
CreateTime = DateTime.Now,
FromUser = WxXmlModel.FromUserName,
MsgFlag = WxXmlModel.MsgID
});
}
else
{
return null;
}
}
else
{
if (_queue.FirstOrDefault(m => { return m.MsgFlag == WxXmlModel.CreateTime; }) == null)
{
_queue.Add(new BaseMsg
{
CreateTime = DateTime.Now,
FromUser = WxXmlModel.FromUserName,
MsgFlag = WxXmlModel.CreateTime
});
}
else
{
return null;
}
}
switch (WxXmlModel.MsgType)
{
case "text"://文本
WxXmlModel.Content = rootElement.SelectSingleNode("Content").InnerText;
break;
case "event"://事件
#region 事件处理
WxXmlModel.Event = rootElement.SelectSingleNode("Event").InnerText;
string Code = string.Empty;
switch (WxXmlModel.Event)
{
case "subscribe"://关注类型
LogTextHelper.WriteLog("关注扫码了");
WxXmlModel.EventKey = rootElement.SelectSingleNode("EventKey").InnerText;
Code = WxXmlModel.EventKey.Replace("qrscene_", "");
//LogTextHelper.WriteLog("参数加密:" + Code);
//LogTextHelper.WriteLog("参数解密:" + decHelper.Decrypto(Code));
//if (!IsExist(Code))//用来判断是否已经扫过二维码
//{
//EditWeChatMenu.DeleteMenu();//删除菜单
//EditWeChatMenu.CreateMenu();//创建自定义菜单
if (Code.Contains("加密了:"))
{
Code = Code.Replace("加密了:", "");
LogTextHelper.WriteLog("参数加密:" + Code);
LogTextHelper.WriteLog("参数解密:" + decHelper.Decrypto(Code));
LogTextHelper.WriteLog("序列化完的数据:" + JsonConvert.DeserializeObject(decHelper.Decrypto(Code)));
string getTemplateData = PutTemplateMsg(decHelper.Decrypto(Code));//WXHttpPost.HttpPostData("http://PharmaOne.winning.com.cn/api/Template/PutTemplateMsg", decHelper.Decrypto(Code));
dataObj data = JsonConvert.DeserializeObject<dataObj>(getTemplateData);
ResponeTemplate.SendTemplate(this.templatedSend, WxXmlModel, this.templateId, data.data, data.url);
}
else
{
ResponeTemplate.ResponeTemplateMsgTest(this.templateId, this.templatedSend, WxXmlModel, url2);
}
//}
//else
//{
// WxXmlModel.Event = "二维码已失效";
// WxXmlModel.Content = "二维码已失效";
//}
break;
case "SCAN"://扫码类型
LogTextHelper.WriteLog("扫码了");
WxXmlModel.EventKey = rootElement.SelectSingleNode("EventKey").InnerText;
//取到参数中的GUID判断是否已经被扫过【扫过的提示二维码已过期,没有扫过的就推送内容】
Code = WxXmlModel.EventKey;
if (Code.Contains("加密了:"))
{
Code = Code.Replace("加密了:", "");
LogTextHelper.WriteLog("参数加密:" + Code);
LogTextHelper.WriteLog("参数解密:" + decHelper.Decrypto(Code));
LogTextHelper.WriteLog("序列化完的数据:" + JsonConvert.DeserializeObject(decHelper.Decrypto(Code)));
//string getTemplateData = temPApi.PutTemplateMsg(decHelper.Decrypto(Code));
string getTemplateData = PutTemplateMsg(decHelper.Decrypto(Code));
dataObj data = JsonConvert.DeserializeObject<dataObj>(getTemplateData);
LogTextHelper.WriteLog("模板数据:" + getTemplateData);
if (data.status == "200")
{
LogTextHelper.WriteLog("模板数据data:" + data.data);
LogTextHelper.WriteLog("模板数据url:" + data.url);
ResponeTemplate.SendTemplate(this.templatedSend, WxXmlModel, this.templateId, data.data, data.url);
}
else
{
return "";
}
}
else
{
ResponeTemplate.ResponeTemplateMsgTest(this.templateId, this.templatedSend, WxXmlModel, url2);
}
//if (!IsExist(Code))
//{
//LogTextHelper.WriteLog("序列化完的数据:" + JsonConvert.DeserializeObject(decHelper.Decrypto(Code)));
//
//LogTextHelper.WriteLog("返回的模板数据的数据:" + getTemplateData);
//}
//else
//{
// WxXmlModel.Event = "二维码已失效";
// WxXmlModel.Content = "二维码已失效";
//}
break;
#endregion
case "TEMPLATESENDJOBFINISH"://推送模板消息成功事件
WxXmlModel.MsgID = rootElement.SelectSingleNode("MsgID").InnerText;
WxXmlModel.Status = rootElement.SelectSingleNode("Status").InnerText;
break;
default:
break;
}
break;
}
return ResponseXML(WxXmlModel);//回复消息
}
建个静态列表_queue,用来存储消息列表,列表的类型是List<BaseMsg>.
在处理微信消息体前,首先判断列表是否实例化,如果没有实例化则实例化,否则判断列表的长度是否大于或等于50(这个可以自定义,用处就是微信并发的消息量),如果大于或等于50,则保留20秒内未响应的消息(5秒重试一次,总共重试3次,就是15秒,保险起见这里写20秒)。
获取当前消息体的消息类型,并根据_queue判断当前消息是否已经请求了。如果是事件则保存FromUser和创建时间。如果是普通消息则保存MsgFlag。:
BaseMsg类型定义如下:
public class BaseMsg
{
/// <summary>
/// 发送者标识
/// </summary>
public string FromUser { get; set; }
/// <summary>
/// 消息表示。普通消息时,为msgid,事件消息时,为事件的创建时间
/// </summary>
public string MsgFlag { get; set; }
/// <summary>
/// 添加到队列的时间
/// </summary>
public DateTime CreateTime { get; set; }
}
RequstXmlModels:
public class RequstXmlModel
{
/// <summary>
/// 消息接收方微信号
/// </summary>
public string ToUserName { get; set; }
/// <summary>
/// 消息发送方微信号
/// </summary>
public string FromUserName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public string CreateTime { get; set; }
/// <summary>
/// 信息类型 地理位置:location,文本消息:text,消息类型:image
/// </summary>
public string MsgType { get; set; }
/// <summary>
/// 信息内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 地理位置纬度
/// </summary>
public string Location_X { get; set; }
/// <summary>
/// 地理位置经度
/// </summary>
public string Location_Y { get; set; }
/// <summary>
/// 地图缩放大小
/// </summary>
public string Scale { get; set; }
/// <summary>
/// 地理位置信息
/// </summary>
public string Label { get; set; }
/// <summary>
/// 图片链接,开发者可以用HTTP GET获取
/// </summary>
public string PicUrl { get; set; }
/// <summary>
/// 事件类型,subscribe(订阅/扫描带参数二维码订阅)、unsubscribe(取消订阅)、CLICK(自定义菜单点击事件) 、SCAN(已关注的状态下扫描带参数二维码)
/// </summary>
public string Event { get; set; }
/// <summary>
/// 事件KEY值
/// </summary>
public string EventKey { get; set; }
/// <summary>
/// 二维码的ticket,可以用来换取二维码
/// </summary>
public string Ticket { get; set; }
/// <summary>
/// 模板消息回复ID
/// </summary>
public string MsgID { get; set; }
/// <summary>
/// 回复状态
/// </summary>
public string Status { get; set; }
/// <summary>
/// 商品编码标准
/// </summary>
public string KeyStandard { get; set; }
/// <summary>
/// 商品编码内容
/// </summary>
public string KeyStr { get; set; }
/// <summary>
/// 获取商品二维码接口时传入的extinfo,为标识参数
/// </summary>
public string ExtInfo { get; set; }
/// <summary>
/// 图片点击的链接
/// </summary>
public string Url { get; set; }
/// <summary>
/// 图文信息的标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 图文信息的详情
/// </summary>
public string Description { get; set; }
/// <summary>
/// 指菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了。
/// </summary>
public string MenuID { get; set; }
/// <summary>
/// 扫描类型,一般是qrcode
/// </summary>
public string ScanType { get; set; }
/// <summary>
/// 扫描结果,即二维码对应的字符串信息
/// </summary>
public string ScanResult { get; set; }
}
这样,我在每次请求微信服务的时候,在有时间延迟的情况下,服务端的应答就只能发送一条回复了,在做响应的时候就判断返回的对象是否为null,为null就不做respone操作,这就实现了消息去重。
如果有更好的建议,可以多交流。