一:說明
*當前文檔比較簡陋,如果有其他方法不明確可以留言。
項目框架:.net freamwork 4.6.1
開發工具:vs2019
本次開發使用了Senparc.Weixin組件,地址:https://github.com/JeffreySu/WeiXinMPSDK
相關版本如下:
二:名次解釋
Token:下圖token
EncodingAESKey:下圖key
AppId:下圖appid
三:剛需接口
1:授權推送及component_verify_ticket接收
[HttpPost]
public HttpResponseMessage Auth([FromUri]Senparc.Weixin.Open.Entities.Request.PostModel postModel)
{
var wcs = new WeChatService();
try
{
if (postModel != null)
{
postModel.Token = "你的token"
postModel.AppId = "你的appid"
postModel.EncodingAESKey = "你的key"
}
var inputStream = Request.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
XDocument encryptDoc = XmlUtility.Convert(inputStream);
//獲取解密後的XDocument
XDocument decryptDoc = WeChatHelper.GetDecryptXDocument(encryptDoc, postModel.AppId, postModel.Token, postModel.EncodingAESKey, postModel.Msg_Signature, postModel.Timestamp, postModel.Nonce);
Senparc.Weixin.Open.IRequestMessageBase requestMessage = Senparc.Weixin.Open.RequestMessageFactory.GetRequestEntity(decryptDoc);
if (requestMessage == null)
{
LogHelper.Warn("接收component_verify_ticket方法,requestMessage:接收信息爲空");
}
else
{
LogHelper.Info("接收到通知:" + requestMessage.JSONSerialize());
switch (requestMessage.InfoType)
{
case RequestInfoType.component_verify_ticket://推送component_verify_ticket
var messageComponentVerifyTicket = requestMessage as RequestMessageComponentVerifyTicket;
string verifyTicket = messageComponentVerifyTicket.ComponentVerifyTicket;
new WeChatService().SaveTicket(verifyTicket, postModel.AppId); // 存儲推送的verifyTicket
break;
//我的授權工作是在url回調時完成的,此處並未真正執行。
case RequestInfoType.authorized://授權成功
var requestMessageAuthorized = requestMessage as RequestMessageAuthorized;
string authorizerAppid = requestMessageAuthorized.AuthorizerAppid;
LogHelper.Info("接收component_verify_ticket" + "方法ReceiveRequestType,信息6:授權成功!requestMessageAuthorized:" + JsonConvert.SerializeObject(requestMessageAuthorized));
if (!String.IsNullOrEmpty(authorizerAppid))
{
//wcs.UpdateOfficialStatus(authorizerAppid, 1);
}
break;
case RequestInfoType.unauthorized://公衆號取消授權
var requestMessageUnauthorized = requestMessage as RequestMessageUnauthorized;
string authorizerAppid01 = requestMessageUnauthorized.AuthorizerAppid;
LogHelper.Info("接收component_verify_ticket" + "方法ReceiveRequestType,信息7:取消授權!requestMessageUnauthorized:" + JsonConvert.SerializeObject(requestMessageUnauthorized));
if (!String.IsNullOrEmpty(authorizerAppid01))
{
wcs.UpdateOfficialStatus(authorizerAppid01, 3);
}
break;
case RequestInfoType.updateauthorized://公衆號更新授權
var requestMessageUpdateAuthorized = requestMessage as RequestMessageUpdateAuthorized;
string authorizerAppid02 = requestMessageUpdateAuthorized.AuthorizerAppid;
LogHelper.Info("接收component_verify_ticket" + "方法ReceiveRequestType,信息8:更新授權!requestMessageUpdateAuthorized:" + JsonConvert.SerializeObject(requestMessageUpdateAuthorized));
if (!String.IsNullOrEmpty(authorizerAppid02))
{
wcs.UpdateOfficialStatus(authorizerAppid02, 2);
}
break;
default:
break;
}
}
}
catch (Exception ex)
{
LogHelper.Info("接收component_verify_ticket"+"方法Auth:異常信息:" + ex.ToString());
}
var resutl = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
resutl.Content = new StringContent("success");
return resutl;
}
2: 獲取ComponentToken
/// <summary>
/// 獲取ComponentToken
/// </summary>
/// <param name="isRefresh"></param>
/// <returns></returns>
public static string GetComponentToken(bool isRefresh =false)
{
//1:先判斷數據庫或者緩存裏面的token是否有效,如果是強制刷新則不從緩存或數據庫取。
//代碼忽略
//2:如果token爲空或者已過期或者強制刷新token
//先取ticket
var ticket = "從緩存或數據庫獲取";
if (ticket.IsNullOrEmptyAndTrim())
{
LogHelper.Error($"獲取Component_Token時,Redis讀取ticket失敗,key:{ticketKey}");
return "";
}
var dict = new Dictionary<string, object>();
dict.Add("component_appid", WeChatUtils.Component_AppId);
dict.Add("component_appsecret", WeChatUtils.Component_AppSecret);
dict.Add("component_verify_ticket", ticket);
var token =HttpClientHelper.DoPostAsync<WCComponentToken>(WeChatUtils.URL_GetComponentToken,dict).GetAwaiter().GetResult();
if (token != null && !token.component_access_token.IsNullOrEmptyAndTrim())
{
//獲取成功。此處應該把token存起來。
}
return token.component_access_token;
}
3:處理微信推送的公衆號消息或者事件。 目前只處理了文本消息和關注、取消關注事件。
[HttpPost]
[HttpOptions]
//微信後臺設置的事件接收路徑是:http://www.xxx.com/wechat/callback/$APPID$
public HttpResponseMessage CallBack([FromUri]Senparc.Weixin.MP.Entities.Request.PostModel postModel)
{
//其實當前方法並未接收$APPID$的參數。因爲解析事件的時候,可以獲取到是哪個公衆號推送的事件。
LogHelper.Info($"進入事件處理:"+Request.RequestUri.ToString());
if (postModel != null)
{
postModel.Token = WeChatUtils.Component_Token;
postModel.AppId = WeChatUtils.Component_AppId;
postModel.EncodingAESKey = WeChatUtils.Component_Key;
}
try
{
MessageHandler<CustomMessageContext> messageHandler = new CustomMessageHandler(Request.Content.ReadAsStreamAsync().GetAwaiter().GetResult(), postModel);
messageHandler.Execute(); //執行
var result = new FixWeixinBugWeixinResult(messageHandler);
LogHelper.Info($"返回結果:"+result.Content);
if (!result.Content.IsNullOrEmptyAndTrim())
{
//如果在CustomMessageHandler中返回了消息,則將消息推送到微信服務器。
//如果在CustomMessageHandler不想返回消息,一定要返回null。詳見CustomMessageHandler
var str = result.Content;
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(str, System.Text.Encoding.UTF8, "application/xml")
};
}
}
catch (Exception ex)
{
LogHelper.Error("執行出現錯誤"+ex.ToString());
}
var res = new HttpResponseMessage(HttpStatusCode.OK);
res.Content = new StringContent("success");
return res;
}
3.1 CustomMessageHandler
public class CustomMessageHandler : MessageHandler<CustomMessageContext>
{
public CustomMessageHandler(Stream inputStream, PostModel postModel)
: base(inputStream, postModel)
{
}
/// <summary>
/// 如果不需要返回值,則返回null。否則返回值會使用xml包裹。
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage)
{
LogHelper.Info($"進入了其他事件處理程序:" + requestMessage.JSONSerialize());
//當前頁面只處理了公衆號文本消息,其他消息如:客戶向公衆號發了一張圖片、發了語音、定位等等;
//其他事件如關注事件、取消關注事件等等都沒有單獨處理,所以沒有單獨override的消息或事件,會進入當前方法。
//返回一個默認的文本消息
//var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
//responseMessage.Content = "你好,你正在操作什麼事情";
//return responseMessage;
return null; //如果用戶的操作你不想返回任何消息,此處請返回null。
}
//重寫文本接收事件
public override IResponseMessageBase OnTextRequest(RequestMessageText requestMessage)
{
//測試邏輯
var response = base.CreateResponseMessage<ResponseMessageText>();
//打印接收的文本
response.Content = "你好,你輸入的文本是"+requestMessage.Content;
return response;
//requestMessage.FromUserName(用戶的openID)
//requestMessage.ToUserName(公衆號的原始 ID;第三方平臺公衆號授權後獲取公衆號信息中的username。此處可以通過username反查公衆號的appId,所以在接受消息推送時,沒有存儲$APPID$)
}
/// <summary>
/// 關注公衆號事件
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_SubscribeRequest(RequestMessageEvent_Subscribe requestMessage)
{
//關注公衆號,我就把關注用的信息進行了存儲。
//用戶信息存儲有2種方法,第一種是我當前這種。通過requestMessage.ToUserName獲取到的公衆號的token和用戶的requestMessage.FromUserName(用戶的openID),去獲取。弊端是用戶沒有關注過當前公衆號,是獲取不到的。訂閱號沒有權限也不可以。
//地址 https://api.weixin.qq.com/cgi-bin/user/info?access_token={token}&openid={openID}&lang=zh_CN
//還有一種方式就是在公衆號裏進行授權,然後通過靜默授權或者用戶點同意授權,獲取到用戶的信息。訂閱號沒有權限也不可以。
new WeChatCommand().SaveWeChatUser(requestMessage.FromUserName, requestMessage.ToUserName);
return null; //執行完了自己的方法,如果不需要返回給用戶數據,記得返回null。
}
/// <summary>
/// 取消關注公衆號
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase OnEvent_UnsubscribeRequest(RequestMessageEvent_Unsubscribe requestMessage)
{
return base.OnEvent_UnsubscribeRequest(requestMessage);
}
}
3.2 CustomMessageContext
public class CustomMessageContext : DefaultMpMessageContext
{
public CustomMessageContext()
{
base.MessageContextRemoved += CustomMessageContext_MessageContextRemoved;
}
/// <summary>
/// 當上下文過期,被移除時觸發的時間
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void CustomMessageContext_MessageContextRemoved(object sender, Senparc.NeuChar.Context.WeixinContextRemovedEventArgs<IRequestMessageBase, IResponseMessageBase> e)
{
/* 注意,這個事件不是實時觸發的(當然你也可以專門寫一個線程監控)
* 爲了提高效率,根據WeixinContext中的算法,這裏的過期消息會在過期後下一條請求執行之前被清除
*/
var messageContext = e.MessageContext as CustomMessageContext;
if (messageContext == null)
{
return;//如果是正常的調用,messageContext不會爲null
}
//TODO:這裏根據需要執行消息過期時候的邏輯,下面的代碼僅供參考
//Log.InfoFormat("{0}的消息上下文已過期",e.OpenId);
//api.SendMessage(e.OpenId, "由於長時間未搭理客服,您的客服狀態已退出!");
}
}
四:全網測試發佈。
全網測試分3塊:
1:測試接口component_verify_ticket是否可以正常接收和返回,此測試不用特別處理。我們上面返回的就是“success”;
2:向用戶發送文本消息:TESTCOMPONENT_MSG_TYPE_TEXT,然後被動回覆TESTCOMPONENT_MSG_TYPE_TEXT_callback。此處在消息處理的地方判斷一下就可以。
3:向用戶發送文本消息:QUERY_AUTH_CODE:$query_auth_code$。($query_auth_code$是授權後的授權碼,可以通過$query_auth_code$獲取授權公衆號的AccessToken,繼而通過AccessToken獲取到公衆號詳情和一些以其他公衆號操作)
後臺接收到文本消息後還是先判斷,消息是否包含"QUERY_AUTH_CODE",如果是,就判斷是在進行測試。$query_auth_code$獲取到公衆號的AccessToken,然後使用AccessToken發送返回消息:$query_auth_code$_from_api。
2、3代碼如下:
public class CustomMessageHandler : MessageHandler<CustomMessageContext>
{
public CustomMessageHandler(Stream inputStream, PostModel postModel)
: base(inputStream, postModel)
{
}
/// <summary>
/// 如果不需要返回值,則返回null。否則返回值會使用xml包裹。
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage)
{
LogHelper.Info($"進入了其他事件處理程序:" + requestMessage.JSONSerialize());
//當前頁面只處理了公衆號文本消息,其他消息如:客戶向公衆號發了一張圖片、發了語音、定位等等;
//其他事件如關注事件、取消關注事件等等都沒有單獨處理,所以沒有單獨override的消息或事件,會進入當前方法。
//返回一個默認的文本消息
//var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
//responseMessage.Content = "你好,你正在操作什麼事情";
//return responseMessage;
return null; //如果用戶的操作你不想返回任何消息,此處請返回null。
}
public override IResponseMessageBase OnTextRequest(RequestMessageText requestMessage)
{
var response = base.CreateResponseMessage<ResponseMessageText>();
response.Content = "";
LogHelper.Info("進入了文字處理程序"+requestMessage.JSONSerialize());
#region 監測全網發佈
if (requestMessage.Content == "TESTCOMPONENT_MSG_TYPE_TEXT")
{
LogHelper.Info($"進入了全網測試第一階段:測試公衆號處理用戶消息");
response.Content = "TESTCOMPONENT_MSG_TYPE_TEXT_callback";
return response;
}
else if (requestMessage.Content.Contains("QUERY_AUTH_CODE:"))
{
LogHelper.Info($"進入了全網測試第二階段:測試公衆號處理用戶消息");
var queryAuthCode = requestMessage.Content.Replace("QUERY_AUTH_CODE:", "");
LogHelper.Info($"進入了全網測試第二階段:queryAuthCode:"+ queryAuthCode);
//通過微信傳過來的公衆號的QUERY_AUTH_CODE,直接拿公衆號的AccessToken,拿到了公衆號的AccessToken,就可以通過客服來發消息。
var token = WeChatHelper.GetOfficial_Token(queryAuthCode);
if (token == null || !token.errcode.IsNullOrEmptyAndTrim())
{
LogHelper.Info($"通過Code獲取Token方式失敗:{token.JSONSerialize()}");
return null;
}
var msg = $"{queryAuthCode}_from_api";
CustomApi.SendText(token.authorization_info.authorizer_access_token, requestMessage.FromUserName, msg);
return null;
}
return null;
#endregion
}
}
public static WCOfficialToken GetOfficial_Token(string authorization_code)
{
if (authorization_code.IsNullOrEmptyAndTrim())
return null;
var token = GetComponentToken();
if (token.IsNullOrEmptyAndTrim())
{
LogHelper.Error($"在獲取Official_Token時,獲取ComponentToken失敗");
return null;
}
var url = $"https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token={token}";
var dict = new Dictionary<string, object>();
dict.Add("component_access_token", token);
dict.Add("component_appid", WeChatUtils.Component_AppId);
dict.Add("authorization_code", authorization_code);
var result =HttpClientHelper.DoPostAsync<WCOfficialToken>(url, dict).GetAwaiter().GetResult();
if (result != null && result.errcode.IsNullOrEmptyAndTrim())
{
return result;
}
else
{
LogHelper.Error($"獲取OfficialInfo失敗!url:{url};dict:{dict};result:{result.JSONSerialize()}");
return null;
}
}
/// <summary>
/// Post請求
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="url"></param>
/// <param name="dictionary"></param>
/// <returns></returns>
public static async Task<T> DoPostAsync<T>(string url, Dictionary<string, object> dictionary)
{
//設置HttpClientHandler的AutomaticDecompression 自動壓縮方式
var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip };
//創建HttpClient
using (var http = new System.Net.Http.HttpClient(handler))
{
HttpContent httpContent = new StringContent(dictionary.JSONSerialize());
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
try
{
var result = await http.PostAsync(url, httpContent).Result.Content.ReadAsStringAsync();
var obj = new
{
Url =url,
Result =result
};
LogHelper.Info("Post請求:"+obj.JSONSerialize());
return JsonConvert.DeserializeObject<T>(result, new TimestampConverter());
}
catch (Exception ex)
{
LogHelper.Error("接口地址:" + url + ", 接口參數:" + dictionary.JSONSerialize());
throw ex;
}
}
}