一:说明
*当前文档比较简陋,如果有其他方法不明确可以留言。
项目框架:.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;
}
}
}