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 { /// /// 微信公众号消息推送对接 /// public class WeChatOffiaccountNotifyController : ControllerBase { private readonly IWeChatApiHttpClientFactory _weChatApiHttpClientFactory; private readonly WeChatOptions _weChatOptions; private readonly IMediator _mediator; private readonly ICoreCmsUserWeChatInfoServices _weChatUserInfoServices; /// /// 原始的加密请求(如果不加密则为null) /// public XDocument? EcryptRequestDocument { get; set; } = null; /// /// 是否使用加密 /// public bool UsingEncryptMessage = false; /// /// 是否取消执行 /// public bool CancelExecute = false; /// /// 是否使用兼容模式 /// public bool UsingCompatibilityModelEncryptMessage = false; /// /// 构造函数 /// public WeChatOffiaccountNotifyController(IWeChatApiHttpClientFactory weChatApiHttpClientFactory, IOptions weChatOptions, IMediator mediator, ICoreCmsUserWeChatInfoServices weChatUserInfoServices) { _weChatApiHttpClientFactory = weChatApiHttpClientFactory; _mediator = mediator; _weChatUserInfoServices = weChatUserInfoServices; _weChatOptions = weChatOptions.Value; } /// /// GET请求用于处理微信公众号后台的URL验证 /// /// [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); } } /// /// 接收服务器推送 /// /// [HttpPost] [ActionName("Index")] public async Task 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内容 /// /// 对解密后的xml数据进行筛选并分发处理结果 /// public async Task 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(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(msgXml); } break; case RequestMsgType.Image: { var eventModel = client.DeserializeEventFromXml(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(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(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(msgXml); } break; case RequestMsgType.Link: { var eventModel = client.DeserializeEventFromXml(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(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(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文本数据 /// /// 初始化获取xml文本数据 /// /// /// /// /// 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 } }