using System;
|
using CoreCms.Net.WeChat.Service.HttpClients;
|
using Microsoft.AspNetCore.Mvc;
|
using SKIT.FlurlHttpClient.Wechat.Api;
|
using SKIT.FlurlHttpClient.Wechat.Api.Events;
|
using System.Text;
|
using System.Threading.Tasks;
|
using System.Xml.Linq;
|
using CoreCms.Net.Caching.AccressToken;
|
using CoreCms.Net.Configuration;
|
using CoreCms.Net.IServices;
|
using CoreCms.Net.Loging;
|
using CoreCms.Net.Model.Entities;
|
using CoreCms.Net.Utility.Helper;
|
using CoreCms.Net.WeChat.Service.Configuration;
|
using CoreCms.Net.WeChat.Service.Mediator;
|
using CoreCms.Net.WeChat.Service.Models;
|
using CoreCms.Net.WeChat.Service.Options;
|
using CoreCms.Net.WeChat.Service.Utilities;
|
using MediatR;
|
using Microsoft.Extensions.Options;
|
using Newtonsoft.Json;
|
using SKIT.FlurlHttpClient.Wechat.Api.Models;
|
using LogLevel = NLog.LogLevel;
|
using CoreCms.Net.Services;
|
using SqlSugar;
|
|
namespace CoreCms.Net.Web.Controllers.WeChat
|
{
|
|
/// <summary>
|
/// 微信公众号消息推送对接
|
/// </summary>
|
public class WeChatOffiaccountNotifyController : ControllerBase
|
{
|
private readonly IWeChatApiHttpClientFactory _weChatApiHttpClientFactory;
|
private readonly WeChatOptions _weChatOptions;
|
private readonly IMediator _mediator;
|
|
private readonly ICoreCmsUserWeChatInfoServices _weChatUserInfoServices;
|
|
|
/// <summary>
|
/// 原始的加密请求(如果不加密则为null)
|
/// </summary>
|
public XDocument? EcryptRequestDocument { get; set; } = null;
|
|
/// <summary>
|
/// 是否使用加密
|
/// </summary>
|
public bool UsingEncryptMessage = false;
|
|
/// <summary>
|
/// 是否取消执行
|
/// </summary>
|
public bool CancelExecute = false;
|
/// <summary>
|
/// 是否使用兼容模式
|
/// </summary>
|
public bool UsingCompatibilityModelEncryptMessage = false;
|
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
public WeChatOffiaccountNotifyController(IWeChatApiHttpClientFactory weChatApiHttpClientFactory, IOptions<WeChatOptions> weChatOptions, IMediator mediator, ICoreCmsUserWeChatInfoServices weChatUserInfoServices)
|
{
|
_weChatApiHttpClientFactory = weChatApiHttpClientFactory;
|
_mediator = mediator;
|
_weChatUserInfoServices = weChatUserInfoServices;
|
_weChatOptions = weChatOptions.Value;
|
}
|
|
/// <summary>
|
/// GET请求用于处理微信公众号后台的URL验证
|
/// </summary>
|
/// <returns></returns>
|
[HttpGet]
|
[ActionName("Index")]
|
public ActionResult Get([FromQuery(Name = "timestamp")] string timestamp,
|
[FromQuery(Name = "nonce")] string nonce,
|
[FromQuery(Name = "signature")] string signature,
|
[FromQuery(Name = "echostr")] string echoString)
|
{
|
// 验证服务器推送
|
// 文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
|
|
var getModel = new
|
{
|
timestamp,
|
nonce,
|
signature,
|
echoString
|
};
|
|
|
//NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "GET接收到微信推送的数据", JsonConvert.SerializeObject(getModel));
|
|
//var client = _weChatApiHttpClientFactory.CreateWeXinClient();
|
//var valid = client.VerifyEventSignatureForEcho(callbackTimestamp: timestamp, callbackNonce: nonce, callbackSignature: signature);
|
|
//return Content(!valid ? "fail" : echoString);
|
|
if (!CheckSignature.Check(signature, timestamp, nonce, _weChatOptions.WeiXinToken))
|
{
|
NLogUtil.WriteFileLog(LogLevel.Error, LogType.WeChat, "GET接收到微信推送的数据(签名错误)", JsonConvert.SerializeObject(getModel));
|
return Content("fail");
|
}
|
else
|
{
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "GET接收到微信推送的数据(签名成功)", JsonConvert.SerializeObject(getModel));
|
return Content(echoString);
|
}
|
|
}
|
|
/// <summary>
|
/// 接收服务器推送
|
/// </summary>
|
/// <returns></returns>
|
[HttpPost]
|
[ActionName("Index")]
|
public async Task<IActionResult> Post(PostModel postModel)
|
{
|
// 接收服务器推送
|
// 文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
|
|
if (!CheckSignature.Check(postModel.Signature, postModel.Timestamp, postModel.Nonce, _weChatOptions.WeiXinToken))
|
{
|
NLogUtil.WriteFileLog(LogLevel.Error, LogType.WeChat, "Post接收服务器推送(签名错误)", JsonConvert.SerializeObject(postModel));
|
return Content("fail");
|
}
|
else
|
{
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "Post接收服务器推送(签名成功)", JsonConvert.SerializeObject(postModel));
|
}
|
|
postModel.Token = _weChatOptions.WeiXinToken;//根据自己后台的设置保持一致
|
postModel.EncodingAESKey = _weChatOptions.WeiXinEncodingAesKey;//根据自己后台的设置保持一致
|
postModel.AppId = _weChatOptions.WeiXinAppId;//根据自己后台的设置保持一致(必须提供)
|
|
//获取流数据转xml流
|
XDocument postDataDocument = XmlUtility.Convert(Request.GetRequestStream());
|
|
var msgXml = string.Empty;
|
var callbackXml = Init(postDataDocument, postModel, ref msgXml);
|
|
//怕出现误判,所以将最优结果判断
|
if (callbackXml != null && CancelExecute == false && !string.IsNullOrEmpty(msgXml))
|
{
|
/* 如果是 XML 格式的通知内容 */
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(XML格式的通知内容)", JsonConvert.SerializeObject(callbackXml));
|
var callBack = await ExecuteProcess(callbackXml, msgXml);
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(XML通知微信服务器)", callBack.Data);
|
return Content(callBack.Data);
|
}
|
else
|
{
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(解密失败)", JsonConvert.SerializeObject(callbackXml));
|
return Content("fail");
|
}
|
}
|
|
|
|
#region 处理xml内容
|
/// <summary>
|
/// 对解密后的xml数据进行筛选并分发处理结果
|
/// </summary>
|
public async Task<WeChatApiCallBack> ExecuteProcess(XDocument sourceXml, string msgXml)
|
{
|
//被动回复消息
|
//文件:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
|
var requestType = sourceXml.Root?.Element("MsgType")?.Value;
|
|
WeChatApiCallBack callBack = new WeChatApiCallBack();
|
|
if (!string.IsNullOrEmpty(requestType))
|
{
|
var client = _weChatApiHttpClientFactory.CreateWeXinClient();
|
|
switch (requestType)
|
{
|
case RequestMsgType.Text:
|
{
|
var eventModel = client.DeserializeEventFromXml<TextMessageEvent>(msgXml);
|
var replyModel = new TextMessageReply()
|
{
|
ToUserName = eventModel.FromUserName,
|
FromUserName = eventModel.ToUserName,
|
CreateTimestamp = CommonHelper.GetTimeStampByTotalSeconds(),
|
Content = "您发送的消息是" + eventModel.Content
|
};
|
var replyXml = client.SerializeEventToXml(replyModel, false);
|
callBack.Data = replyXml;
|
}
|
break;
|
case RequestMsgType.Location:
|
{
|
var eventModel = client.DeserializeEventFromXml<LocationMessageEvent>(msgXml);
|
|
}
|
break;
|
case RequestMsgType.Image:
|
{
|
var eventModel = client.DeserializeEventFromXml<ImageMessageEvent>(msgXml);
|
|
var replyModel = new ImageMessageReply()
|
{
|
ToUserName = eventModel.FromUserName,
|
FromUserName = eventModel.ToUserName,
|
CreateTimestamp = CommonHelper.GetTimeStampByTotalSeconds(),
|
|
};
|
var replyXml = client.SerializeEventToXml(replyModel, false);
|
callBack.Data = replyXml;
|
|
}
|
break;
|
case RequestMsgType.Voice:
|
{
|
var eventModel = client.DeserializeEventFromXml<VoiceMessageEvent>(msgXml);
|
|
var replyModel = new VoiceMessageReply()
|
{
|
ToUserName = eventModel.FromUserName,
|
FromUserName = eventModel.ToUserName,
|
CreateTimestamp = CommonHelper.GetTimeStampByTotalSeconds(),
|
|
};
|
var replyXml = client.SerializeEventToXml(replyModel, false);
|
callBack.Data = replyXml;
|
|
}
|
break;
|
case RequestMsgType.Video:
|
{
|
var eventModel = client.DeserializeEventFromXml<VideoMessageEvent>(msgXml);
|
|
var replyModel = new VideoMessageReply()
|
{
|
ToUserName = eventModel.FromUserName,
|
FromUserName = eventModel.ToUserName,
|
CreateTimestamp = CommonHelper.GetTimeStampByTotalSeconds(),
|
|
};
|
var replyXml = client.SerializeEventToXml(replyModel, false);
|
callBack.Data = replyXml;
|
}
|
break;
|
case RequestMsgType.ShortVideo:
|
{
|
var eventModel = client.DeserializeEventFromXml<ShortVideoMessageEvent>(msgXml);
|
|
|
}
|
break;
|
case RequestMsgType.Link:
|
{
|
var eventModel = client.DeserializeEventFromXml<VoiceMessageEvent>(msgXml);
|
|
}
|
break;
|
case RequestMsgType.MessageEvent:
|
{
|
var eventType = sourceXml.Root?.Element("Event")?.Value;
|
if (!string.IsNullOrEmpty(eventType))
|
{
|
switch (eventType)
|
{
|
//订阅(关注)事件
|
case EventType.Subscribe:
|
{
|
var eventModel = client.DeserializeEventFromXml<SubscribePushEvent>(msgXml);
|
var accessToken = WeChatCacheAccessTokenHelper.GetWeChatAccessToken();
|
|
if (eventModel.FromUserName != null)
|
{
|
var request = new CgibinUserInfoRequest() { AccessToken = accessToken, OpenId = eventModel.FromUserName };
|
var response = await client.ExecuteCgibinUserInfoAsync(request, cancellationToken: HttpContext.RequestAborted);
|
if (!response.IsSuccessful())
|
{
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "获取用户基本信息失败", @"获取用户基本信息失败(状态码:{response.RawStatus},错误代码:{response.ErrorCode},错误描述:{ response.ErrorMessage})。");
|
}
|
else
|
{
|
var userInfo = await _weChatUserInfoServices.QueryByClauseAsync(p => p.openid == eventModel.FromUserName);
|
if (userInfo == null)
|
{
|
|
userInfo = new Model.Entities.CoreCmsUserWeChatInfo()
|
{
|
isSubscribe = response.IsSubscribed,
|
openid = response.OpenId,
|
language = response.Language,
|
//createTime = response.SubscribeTimestamp,
|
type = (int)GlobalEnumVars.UserAccountTypes.微信公众号,
|
//sessionKey = response.s,
|
gender = 1,
|
createTime = DateTime.Now,
|
unionId = response.UnionId,
|
};
|
var id = await _weChatUserInfoServices.InsertAsync(userInfo);
|
if (id > 0)
|
{
|
await _weChatUserInfoServices.UpdateAsync(
|
p => new Model.Entities.CoreCmsUserWeChatInfo()
|
{
|
userId = id
|
}, p => p.id == id);
|
}
|
}
|
else
|
{
|
userInfo.isSubscribe = response.IsSubscribed;
|
userInfo.unionId = response.UnionId;
|
userInfo.updateTime = DateTime.Now;
|
|
await _weChatUserInfoServices.UpdateAsync(userInfo);
|
}
|
}
|
}
|
}
|
break;
|
// 退订
|
// 实际上用户无法收到非订阅账号的消息,所以这里可以随便写。
|
// unsubscribe事件的意义在于及时删除网站应用中已经记录的OpenID绑定,消除冗余数据。并且关注用户流失的情况。
|
case EventType.Unsubscribe:
|
{
|
|
|
}
|
break;
|
case EventType.Localtion:
|
{
|
|
|
}
|
break;
|
case EventType.Click:
|
{
|
var eventModel = client.DeserializeEventFromXml<ClickPushEvent>(msgXml);
|
|
var replyModel = new TextMessageReply()
|
{
|
ToUserName = eventModel.FromUserName,
|
FromUserName = eventModel.ToUserName,
|
CreateTimestamp = CommonHelper.GetTimeStampByTotalSeconds(),
|
Content = "您刚才发送了ENTER事件请求"
|
};
|
var replyXml = client.SerializeEventToXml(replyModel, false);
|
callBack.Data = replyXml;
|
|
}
|
break;
|
default:
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(处理xml内容/Event无匹配)", JsonConvert.SerializeObject(sourceXml));
|
break;
|
}
|
}
|
}
|
break;
|
default:
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(处理xml内容/MsgType无匹配)", JsonConvert.SerializeObject(sourceXml));
|
break;
|
}
|
}
|
else
|
{
|
NLogUtil.WriteFileLog(LogLevel.Info, LogType.WeChat, "接收服务器推送(处理xml内容/获取MsgType失败)", JsonConvert.SerializeObject(sourceXml));
|
}
|
|
return callBack;
|
|
}
|
#endregion
|
|
#region 初始化获取xml文本数据
|
|
/// <summary>
|
/// 初始化获取xml文本数据
|
/// </summary>
|
/// <param name="postDataDocument"></param>
|
/// <param name="postModel"></param>
|
/// <param name="msgXml"></param>
|
/// <returns></returns>
|
private XDocument? Init(XDocument postDataDocument, PostModel postModel, ref string msgXml)
|
{
|
//进行加密判断并处理
|
var postDataStr = postDataDocument.ToString();
|
XDocument decryptDoc = postDataDocument;
|
if (postDataDocument.Root?.Element("Encrypt") != null && !string.IsNullOrEmpty(postDataDocument.Root.Element("Encrypt")?.Value))
|
{
|
//使用了加密
|
UsingEncryptMessage = true;
|
EcryptRequestDocument = postDataDocument;
|
|
WXBizMsgCrypt msgCrype = new WXBizMsgCrypt(postModel.Token, postModel.EncodingAESKey, postModel.AppId);
|
|
var result = msgCrype.DecryptMsg(postModel.Msg_Signature, postModel.Timestamp, postModel.Nonce, postDataStr, ref msgXml);
|
//判断result类型
|
if (result != 0)
|
{
|
//验证没有通过,取消执行
|
CancelExecute = true;
|
return null;
|
}
|
if (postDataDocument.Root.Element("FromUserName") != null && !string.IsNullOrEmpty(postDataDocument.Root.Element("FromUserName")?.Value))
|
{
|
//TODO:使用了兼容模式,进行验证即可
|
UsingCompatibilityModelEncryptMessage = true;
|
}
|
decryptDoc = XDocument.Parse(msgXml);//完成解密
|
}
|
return decryptDoc;
|
|
}
|
|
|
#endregion
|
|
}
|
}
|