diff --git a/Samples/All/net8-mvc/Senparc.Weixin.Sample.Net8/appsettings.json b/Samples/All/net8-mvc/Senparc.Weixin.Sample.Net8/appsettings.json index cea4a2605d..c76b6ec281 100644 --- a/Samples/All/net8-mvc/Senparc.Weixin.Sample.Net8/appsettings.json +++ b/Samples/All/net8-mvc/Senparc.Weixin.Sample.Net8/appsettings.json @@ -86,6 +86,8 @@ //如果不设置TenPayV3_WxOpenTenpayNotify,默认在 TenPayV3_TenpayNotify 的值最后加上 "WxOpen" "TenPayV3_WxOpenTenpayNotify": "#{TenPayV3_WxOpenTenpayNotify}#", //http://YourDomainName/TenpayV3/PayNotifyUrlWxOpen + "EncryptionType": "#{EncryptionType}#", // 加密类型:RSA / SM + //开放平台 "Component_Appid": "#{Component_Appid}#", "Component_Secret": "#{Component_Secret}#", diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageContext.cs b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageContext.cs deleted file mode 100644 index 92e23f2107..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -/*---------------------------------------------------------------- - Copyright (C) 2024 Senparc - - 文件名:CustomMessageContext.cs - 文件功能描述:微信消息上下文 - - - 创建标识:Senparc - 20150312 -----------------------------------------------------------------*/ - -//DPBMARK_FILE MP -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Senparc.NeuChar.Context; -using Senparc.NeuChar.Entities; -using Senparc.Weixin.MP.MessageContexts; - -namespace Senparc.Weixin.Sample.MP -{ - /* v16.8.0 后,提供分布式缓存,只需要直接使用 DefaultMpMessageContext,即使没有 CustomMessageContext 也没有关系 */ - public class CustomMessageContext : DefaultMpMessageContext //MessageContext - { - public CustomMessageContext() - { - base.MessageContextRemoved += CustomMessageContext_MessageContextRemoved; - } - - /// - /// 当上下文过期,被移除时触发的时间 - /// - /// - /// - void CustomMessageContext_MessageContextRemoved(object sender, Senparc.NeuChar.Context.WeixinContextRemovedEventArgs e) - { - /* 注意,这个事件不是实时触发的(当然你也可以专门写一个线程监控) - * 为了提高效率,根据WeixinContext中的算法,这里的过期消息会在过期后下一条请求执行之前被清除 - */ - - var messageContext = e.MessageContext as CustomMessageContext; - if (messageContext == null) - { - return;//如果是正常的调用,messageContext不会为null - } - - //TODO:这里根据需要执行消息过期时候的逻辑,下面的代码仅供参考 - - //Log.InfoFormat("{0}的消息上下文已过期",e.OpenId); - //api.SendMessage(e.OpenId, "由于长时间未搭理客服,您的客服状态已退出!"); - } - } -} diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler.cs b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler.cs index 037311af93..8f04a59889 100644 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler.cs +++ b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler.cs @@ -16,12 +16,7 @@ ----------------------------------------------------------------*/ //DPBMARK_FILE MP -using Senparc.CO2NET.Helpers; -using Senparc.CO2NET.Utilities; using Senparc.NeuChar.Entities; -using Senparc.NeuChar.Entities.Request; -using Senparc.NeuChar.Helpers; -using Senparc.Weixin.MP.AdvancedAPIs; using Senparc.Weixin.MP.Entities; using Senparc.Weixin.MP.Entities.Request; using Senparc.Weixin.MP.MessageContexts; @@ -33,18 +28,8 @@ namespace Senparc.Weixin.Sample.MP /// 自定义MessageHandler /// 把MessageHandler作为基类,重写对应请求的处理方法 /// - public partial class CustomMessageHandler : MessageHandler /*如果不需要自定义,可以直接使用:MessageHandler */ + public partial class CustomMessageHandler : MessageHandler { - /* - * 重要提示:v1.5起,MessageHandler提供了一个DefaultResponseMessage的抽象方法, - * DefaultResponseMessage必须在子类中重写,用于返回没有处理过的消息类型(也可以用于默认消息,如帮助信息等); - * 其中所有原OnXX的抽象方法已经都改为虚方法,可以不必每个都重写。若不重写,默认返回DefaultResponseMessage方法中的结果。 - */ - - - private string appId = Config.SenparcWeixinSetting.MpSetting.WeixinAppId; - private string appSecret = Config.SenparcWeixinSetting.MpSetting.WeixinAppSecret; - /// /// 为中间件提供生成当前类的委托 /// @@ -62,312 +47,20 @@ public partial class CustomMessageHandler : MessageHandler.GlobalGlobalMessageContext.ExpireMinutes = 3。 - GlobalMessageContext.ExpireMinutes = 3; - - OnlyAllowEncryptMessage = true;//是否只允许接收加密消息,默认为 false - } - - - /// - /// 处理文字请求 - /// - /// 请求消息 - /// - public override async Task OnTextRequestAsync(RequestMessageText requestMessage) - { - var defaultResponseMessage = base.CreateResponseMessage(); - - // 下方 RequestHandler 为消息关键字提供了便捷的处理方式,当然您也可以使用传统的 if...else... 对 requestMessage.Content 进行判断 - var requestHandler = await requestMessage.StartHandler() - //关键字不区分大小写,按照顺序匹配成功后将不再运行下面的逻辑 - .Keyword("关键字1", () => - { - defaultResponseMessage.Content = @"收到关键字1"; - return defaultResponseMessage; - }). - //匹配任一关键字 - Keywords(new[] { "关键字2", "关键字3" }, () => - { - defaultResponseMessage.Content = @"收到“关键字2”或“关键字3”"; - return defaultResponseMessage; - }) - .Keyword("OPENID", () => - { - var openId = requestMessage.FromUserName;//获取OpenId - var userInfo = Weixin.MP.AdvancedAPIs.UserApi.Info(appId, openId, Language.zh_CN); - - defaultResponseMessage.Content = string.Format( - "您的OpenID为:{0}\r\n昵称:{1}\r\n性别:{2}\r\n地区(国家/省/市):{3}/{4}/{5}\r\n关注时间:{6}\r\n关注状态:{7}\r\n\r\n说明:从2021年12月27日起,公众号无法直接获取用户昵称、性别、地区等信息,如需获取相关信息,需要使用OAuth 2.0 接口。", - requestMessage.FromUserName, userInfo.nickname, (WeixinSex)userInfo.sex, userInfo.country, userInfo.province, userInfo.city, DateTimeHelper.GetDateTimeFromXml(userInfo.subscribe_time), userInfo.subscribe); - return defaultResponseMessage; - }) - .Keyword("MUTE", () => //不回复任何消息 - { - //方案一: - return new SuccessResponseMessage(); - - //方案二: - var muteResponseMessage = base.CreateResponseMessage(); - return muteResponseMessage; - - //方案三: - base.TextResponseMessage = "success"; - return null; - - //方案四: - return null;//在 Action 中结合使用 return new FixWeixinBugWeixinResult(messageHandler); - }) - //选择菜单,关键字:101(微信服务器端最终格式:id="s:101",content="满意")——需要对应配置 - .SelectMenuKeyword("101", () => - { - defaultResponseMessage.Content = $"感谢您的评价({requestMessage.Content})!我们会一如既往为提高企业和开发者生产力而努力!"; - return defaultResponseMessage; - }) - //正则表达式 - .Regex(@"^\d+#\d+$", () => - { - defaultResponseMessage.Content = string.Format("您输入了:{0},符合正则表达式:^\\d+#\\d+$", requestMessage.Content); - return defaultResponseMessage; - }) - //当 Default 使用异步方法时,需要写在最后一个,且 requestMessage.StartHandler() 前需要使用 await 等待异步方法执行; - //当 Default 使用同步方法,不一定要在最后一个,并且不需要使用 await - .Default(async () => - { - defaultResponseMessage.Content = $"您刚才发送了文字信息:{requestMessage.Content}"; - return defaultResponseMessage; - }); - - return requestHandler.GetResponseMessage() as IResponseMessageBase; - } - - /// - /// 处理位置请求 - /// - /// - /// - public override async Task OnLocationRequestAsync(RequestMessageLocation requestMessage) - { - var locationService = new LocationService(); - var responseMessage = locationService.GetResponseMessage(requestMessage as RequestMessageLocation); - return responseMessage; } - public override async Task OnShortVideoRequestAsync(RequestMessageShortVideo requestMessage) + public override async Task OnTextOrEventRequestAsync(RequestMessageText requestMessage) { var responseMessage = this.CreateResponseMessage(); - responseMessage.Content = "您刚才发送的是小视频"; - return responseMessage; - } - - /// - /// 处理图片请求 - /// - /// - /// - public override async Task OnImageRequestAsync(RequestMessageImage requestMessage) - { - //一隔一返回News或Image格式 - if (base.GlobalMessageContext.GetMessageContext(requestMessage).RequestMessages.Count() % 2 == 0) - { - var responseMessage = CreateResponseMessage(); - - responseMessage.Articles.Add(new Article() - { - Title = "您刚才发送了图片信息", - Description = "您发送的图片将会显示在边上", - PicUrl = requestMessage.PicUrl, - Url = "https://sdk.weixin.senparc.com" - }); - responseMessage.Articles.Add(new Article() - { - Title = "第二条", - Description = "第二条带连接的内容", - PicUrl = requestMessage.PicUrl, - Url = "https://sdk.weixin.senparc.com" - }); - - return responseMessage; - } - else - { - var responseMessage = CreateResponseMessage(); - responseMessage.Image.MediaId = requestMessage.MediaId; - return responseMessage; - } - } - - /// - /// 处理语音请求 - /// - /// - /// - public override async Task OnVoiceRequestAsync(RequestMessageVoice requestMessage) - { - var responseMessage = CreateResponseMessage(); - //上传缩略图 - //var accessToken = Containers.AccessTokenContainer.TryGetAccessToken(appId, appSecret); - var uploadResult = Weixin.MP.AdvancedAPIs.MediaApi.UploadTemporaryMedia(appId, UploadMediaFileType.image, - ServerUtility.ContentRootMapPath("~/Images/Logo.jpg")); - - //设置音乐信息 - responseMessage.Music.Title = "天籁之音"; - responseMessage.Music.Description = "播放您上传的语音"; - responseMessage.Music.MusicUrl = "https://sdk.weixin.senparc.com/Media/GetVoice?mediaId=" + requestMessage.MediaId; - responseMessage.Music.HQMusicUrl = "https://sdk.weixin.senparc.com/Media/GetVoice?mediaId=" + requestMessage.MediaId; - responseMessage.Music.ThumbMediaId = uploadResult.media_id; - - //推送一条客服消息 - try - { - CustomApi.SendText(appId, OpenId, "本次上传的音频MediaId:" + requestMessage.MediaId); - } - catch - { - } - - return responseMessage; - } - /// - /// 处理视频请求 - /// - /// - /// - public override async Task OnVideoRequestAsync(RequestMessageVideo requestMessage) - { - var responseMessage = CreateResponseMessage(); - responseMessage.Content = "您发送了一条视频信息,ID:" + requestMessage.MediaId; - - #region 上传素材并推送到客户端 - - _ = Task.Factory.StartNew(async () => - { - //上传素材 - var dir = ServerUtility.ContentRootMapPath("~/App_Data/TempVideo/"); - var file = await MediaApi.GetAsync(appId, requestMessage.MediaId, dir); - var uploadResult = await MediaApi.UploadTemporaryMediaAsync(appId, UploadMediaFileType.video, file, 50000); - await CustomApi.SendVideoAsync(appId, base.OpenId, uploadResult.media_id, "这是您刚才发送的视频", "这是一条视频消息"); - }).ContinueWith(async task => - { - if (task.Exception != null) - { - WeixinTrace.Log("OnVideoRequest()储存Video过程发生错误:", task.Exception.Message); - - var msg = string.Format("上传素材出错:{0}\r\n{1}", - task.Exception.Message, - task.Exception.InnerException != null - ? task.Exception.InnerException.Message - : null); - await CustomApi.SendTextAsync(appId, base.OpenId, msg); - } - }); - - #endregion - - return responseMessage; - } - - - /// - /// 处理链接消息请求 - /// - /// - /// - public override async Task OnLinkRequestAsync(RequestMessageLink requestMessage) - { - var responseMessage = ResponseMessageBase.CreateFromRequestMessage(requestMessage); - responseMessage.Content = string.Format(@"您发送了一条连接信息: -Title:{0} -Description:{1} -Url:{2}", requestMessage.Title, requestMessage.Description, requestMessage.Url); - return responseMessage; - } - - public override async Task OnFileRequestAsync(RequestMessageFile requestMessage) - { - var responseMessage = requestMessage.CreateResponseMessage(); - responseMessage.Content = string.Format(@"您发送了一个文件: -文件名:{0} -说明:{1} -大小:{2} -MD5:{3}", requestMessage.Title, requestMessage.Description, requestMessage.FileTotalLen, requestMessage.FileMd5); + responseMessage.Content = $"收到您发来的文字信息:{requestMessage.Content}"; return responseMessage; } - /// - /// 处理事件请求(这个方法一般不用重写,这里仅作为示例出现。除非需要在判断具体Event类型以外对Event信息进行统一操作 - /// - /// - /// - public override async Task OnEventRequestAsync(IRequestMessageEventBase requestMessage) - { - var eventResponseMessage = await base.OnEventRequestAsync(requestMessage);//对于Event下属分类的重写方法,见:CustomerMessageHandler_Events.cs - //TODO: 对Event信息进行统一操作 - return eventResponseMessage; - } - - public override async Task DefaultResponseMessageAsync(IRequestMessageBase requestMessage) - { - return DefaultResponseMessage(requestMessage); - } - public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage) { - /* 所有没有被处理的消息会默认返回这里的结果, - * 因此,如果想把整个微信请求委托出去(例如需要使用分布式或从其他服务器获取请求), - * 只需要在这里统一发出委托请求,如: - * var responseMessage = MessageAgent.RequestResponseMessage(agentUrl, agentToken, RequestDocument.ToString()); - * return responseMessage; - */ - var responseMessage = this.CreateResponseMessage(); - responseMessage.Content = $"这条消息来自DefaultResponseMessage。\r\n您收到这条消息,表明该公众号没有对【{requestMessage.MsgType}】类型做处理。"; - return responseMessage; - } - - - public override async Task OnUnknownTypeRequestAsync(RequestMessageUnknownType requestMessage) - { - /* - * 此方法用于应急处理SDK没有提供的消息类型, - * 原始XML可以通过requestMessage.RequestDocument(或this.RequestDocument)获取到。 - * 如果不重写此方法,遇到未知的请求类型将会抛出异常(v14.8.3 之前的版本就是这么做的) - */ - var msgType = Senparc.NeuChar.Helpers.MsgTypeHelper.GetRequestMsgTypeString(requestMessage.RequestDocument); - var responseMessage = this.CreateResponseMessage(); - responseMessage.Content = "未知消息类型:" + msgType; - - WeixinTrace.SendCustomLog("未知请求消息类型", requestMessage.RequestDocument.ToString());//记录到日志中 - + responseMessage.Content = $"收到一条您发来的信息,类型为:{requestMessage.MsgType}"; return responseMessage; } - - #region 重写执行过程 - - public override async Task OnExecutingAsync(CancellationToken cancellationToken) - { - //演示:MessageContext.StorageData - - var currentMessageContext = await base.GetUnsafeMessageContext();//为了在分布式缓存下提高读写效率,使用此方法,如果需要获取实时数据,应该使用 base.GetCurrentMessageContext() - if (currentMessageContext.StorageData == null || !(currentMessageContext.StorageData is int)) - { - currentMessageContext.StorageData = (int)0; - //await GlobalMessageContext.UpdateMessageContextAsync(currentMessageContext);//储存到缓存 - } - await base.OnExecutingAsync(cancellationToken); - } - - public override async Task OnExecutedAsync(CancellationToken cancellationToken) - { - //演示:MessageContext.StorageData - - var currentMessageContext = await base.GetUnsafeMessageContext();//为了在分布式缓存下提高读写效率,使用此方法,如果需要获取实时数据,应该使用 base.GetCurrentMessageContext() - currentMessageContext.StorageData = ((int)currentMessageContext.StorageData) + 1; - GlobalMessageContext.UpdateMessageContext(currentMessageContext);//储存到缓存 - await base.OnExecutedAsync(cancellationToken); - } - - #endregion } } diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler_Events.cs b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler_Events.cs deleted file mode 100644 index 8237f4f7d0..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/MessageHandlers/CustomMessageHandler_Events.cs +++ /dev/null @@ -1,310 +0,0 @@ -/*---------------------------------------------------------------- - Copyright (C) 2024 Senparc - - 文件名:CustomMessageHandler_Events.cs - 文件功能描述:自定义MessageHandler - - - 创建标识:Senparc - 20150312 -----------------------------------------------------------------*/ - -//DPBMARK_FILE MP -using Senparc.CO2NET.Extensions; -using Senparc.CO2NET.Utilities; -using Senparc.NeuChar.Agents; -using Senparc.NeuChar.Entities; -using Senparc.Weixin.MP; -using Senparc.Weixin.MP.AdvancedAPIs; -using Senparc.Weixin.MP.Entities; -using System.Diagnostics; - - -namespace Senparc.Weixin.Sample.MP -{ - /// - /// 自定义MessageHandler - /// - public partial class CustomMessageHandler - { - private string GetWelcomeInfo() - { - //获取Senparc.Weixin.MP.dll版本信息 - //var filePath = ServerUtility.ContentRootMapPath("~/bin/Release/netcoreapp2.2/Senparc.Weixin.MP.dll");//本地测试路径 - var filePath = ServerUtility.ContentRootMapPath("~/Senparc.Weixin.MP.dll");//发布路径 - var fileVersionInfo = FileVersionInfo.GetVersionInfo(filePath); - - string version = fileVersionInfo == null - ? "-" - : string.Format("{0}.{1}.{2}", fileVersionInfo.FileMajorPart, fileVersionInfo.FileMinorPart, fileVersionInfo.FileBuildPart); - - return string.Format( -@"欢迎关注【Senparc.Weixin 微信公众平台SDK】,当前运行版本:v{0}。 -您可以发送【文字】【位置】【图片】【语音】【文件】等不同类型的信息,查看不同格式的回复。 - -您也可以直接点击菜单查看各种类型的回复。 -还可以点击菜单体验微信支付。 - -SDK官方地址:https://weixin.senparc.com -SDK Demo:https://sdk.weixin.senparc.com -源代码及Demo下载地址:https://github.com/JeffreySu/WeiXinMPSDK -Nuget地址:https://www.nuget.org/packages/Senparc.Weixin.MP -QQ群:377815480 - -=============== -更多: - -1、JSSDK测试:https://sdk.weixin.senparc.com/WeixinJSSDK - -2、开放平台测试(建议PC上打开):https://sdk.weixin.senparc.com/OpenOAuth/JumpToMpOAuth - -3、回复关键字: - -【open】 进入第三方开放平台(Senparc.Weixin.Open)测试 - -【tm】 测试异步模板消息 - -【openid】 获取OpenId等用户信息 - -【约束】 测试微信浏览器约束 - -【AsyncTest】 异步并发测试 - -【错误】 体验发生错误无法返回正确信息 - -【容错】 体验去重容错 - -【ex】 体验错误日志推送提醒 - -【mute】 不返回任何消息,也无出错信息 - -【jssdk】 测试JSSDK图文转发接口 - -格式:【数字#数字】,如2010#0102,调用正则表达式匹配 - -【订阅】 测试“一次性订阅消息”接口 - -【SP】 测试 ServiceProvider -", - version); - } - - public override async Task OnTextOrEventRequestAsync(RequestMessageText requestMessage) - { - // 预处理文字或事件类型请求。 - // 这个请求是一个比较特殊的请求,通常用于统一处理来自文字或菜单按钮的同一个执行逻辑, - // 会在执行OnTextRequest或OnEventRequest之前触发,具有以下一些特征: - // 1、如果返回null,则继续执行OnTextRequest或OnEventRequest - // 2、如果返回不为null,则终止执行OnTextRequest或OnEventRequest,返回最终ResponseMessage - // 3、如果是事件,则会将RequestMessageEvent自动转为RequestMessageText类型,其中RequestMessageText.Content就是RequestMessageEvent.EventKey - - if (requestMessage.Content == "OneClick") - { - var strongResponseMessage = CreateResponseMessage(); - strongResponseMessage.Content = "您点击了底部按钮。\r\n为了测试微信软件换行bug的应对措施,这里做了一个——\r\n换行"; - return strongResponseMessage; - } - return null;//返回null,则继续执行OnTextRequest或OnEventRequest - } - - /// - /// 点击事件 - /// - /// 请求消息 - /// - public override async Task OnEvent_ClickRequestAsync(RequestMessageEvent_Click requestMessage) - { - var reponseMessage = CreateResponseMessage(); - - if (requestMessage.EventKey == "OneClick") - { - reponseMessage.Content = "您点击了【单击测试】按钮"; - } - else - { - reponseMessage.Content = "您点击了其他事件按钮"; - } - - return reponseMessage; - } - - /// - /// 进入事件 - /// - /// - /// - public override async Task OnEvent_EnterRequestAsync(RequestMessageEvent_Enter requestMessage) - { - var responseMessage = ResponseMessageBase.CreateFromRequestMessage(requestMessage); - responseMessage.Content = "您刚才发送了ENTER事件请求。"; - return responseMessage; - } - - /// - /// 位置事件 - /// - /// - /// - public override async Task OnEvent_LocationRequestAsync(RequestMessageEvent_Location requestMessage) - { - //这里是微信客户端(通过微信服务器)自动发送过来的位置信息 - var responseMessage = CreateResponseMessage(); - responseMessage.Content = "这里写什么都无所谓,比如:上帝爱你!"; - return responseMessage;//这里也可以返回null(需要注意写日志时候null的问题) - } - - /// - /// 通过二维码扫描关注扫描事件 - /// - /// - /// - public override async Task OnEvent_ScanRequestAsync(RequestMessageEvent_Scan requestMessage) - { - //通过扫描关注 - var responseMessage = CreateResponseMessage(); - - responseMessage.Content = responseMessage.Content ?? string.Format("通过扫描二维码进入,场景值:{0}", requestMessage.EventKey); - - return responseMessage; - } - - /// - /// 打开网页事件 - /// - /// - /// - public override async Task OnEvent_ViewRequestAsync(RequestMessageEvent_View requestMessage) - { - //说明:这条消息只作为接收,下面的responseMessage到达不了客户端,类似OnEvent_UnsubscribeRequest - var responseMessage = CreateResponseMessage(); - responseMessage.Content = "您点击了view按钮,将打开网页:" + requestMessage.EventKey; - return responseMessage; - } - - /// - /// 群发完成事件 - /// - /// - /// - public override async Task OnEvent_MassSendJobFinishRequestAsync(RequestMessageEvent_MassSendJobFinish requestMessage) - { - var responseMessage = CreateResponseMessage(); - responseMessage.Content = "接收到了群发完成的信息。"; - return responseMessage; - } - - /// - /// 订阅(关注)事件 - /// - /// - public override async Task OnEvent_SubscribeRequestAsync(RequestMessageEvent_Subscribe requestMessage) - { - var responseMessage = ResponseMessageBase.CreateFromRequestMessage(requestMessage); - responseMessage.Content = GetWelcomeInfo(); - if (!string.IsNullOrEmpty(requestMessage.EventKey)) - { - responseMessage.Content += "\r\n============\r\n场景值:" + requestMessage.EventKey; - } - - return responseMessage; - } - - /// - /// 退订 - /// 实际上用户无法收到非订阅账号的消息,所以这里可以随便写。 - /// unsubscribe事件的意义在于及时删除网站应用中已经记录的OpenID绑定,消除冗余数据。并且关注用户流失的情况。 - /// - /// - public override async Task OnEvent_UnsubscribeRequestAsync(RequestMessageEvent_Unsubscribe requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "有空再来"; - return responseMessage; - } - - /// - /// 事件之扫码推事件(scancode_push) - /// - /// - /// - public override async Task OnEvent_ScancodePushRequestAsync(RequestMessageEvent_Scancode_Push requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之扫码推事件"; - return responseMessage; - } - - /// - /// 事件之扫码推事件且弹出“消息接收中”提示框(scancode_waitmsg) - /// - /// - /// - public override async Task OnEvent_ScancodeWaitmsgRequestAsync(RequestMessageEvent_Scancode_Waitmsg requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之扫码推事件且弹出“消息接收中”提示框"; - return responseMessage; - } - - /// - /// 事件之弹出拍照或者相册发图(pic_photo_or_album) - /// - /// - /// - public override async Task OnEvent_PicPhotoOrAlbumRequestAsync(RequestMessageEvent_Pic_Photo_Or_Album requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之弹出拍照或者相册发图"; - return responseMessage; - } - - /// - /// 事件之弹出系统拍照发图(pic_sysphoto) - /// 实际测试时发现微信并没有推送RequestMessageEvent_Pic_Sysphoto消息,只能接收到用户在微信中发送的图片消息。 - /// - /// - /// - public override async Task OnEvent_PicSysphotoRequestAsync(RequestMessageEvent_Pic_Sysphoto requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之弹出系统拍照发图"; - return responseMessage; - } - - /// - /// 事件之弹出微信相册发图器(pic_weixin) - /// - /// - /// - public override async Task OnEvent_PicWeixinRequestAsync(RequestMessageEvent_Pic_Weixin requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之弹出微信相册发图器"; - return responseMessage; - } - - /// - /// 事件之弹出地理位置选择器(location_select) - /// - /// - /// - public override async Task OnEvent_LocationSelectRequestAsync(RequestMessageEvent_Location_Select requestMessage) - { - var responseMessage = base.CreateResponseMessage(); - responseMessage.Content = "事件之弹出地理位置选择器"; - return responseMessage; - } - - #region 微信认证事件推送 - - public override async Task OnEvent_QualificationVerifySuccessRequestAsync(RequestMessageEvent_QualificationVerifySuccess requestMessage) - { - //以下方法可以强制定义返回的字符串值 - //TextResponseMessage = "your content"; - //return null; - - return new SuccessResponseMessage();//返回"success"字符串 - } - - #endregion - } -} \ No newline at end of file diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Program.cs b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Program.cs index 02334e9b60..1fafabed5e 100644 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Program.cs +++ b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Program.cs @@ -36,7 +36,7 @@ app.UseMessageHandlerForMp("/WeixinAsync", CustomMessageHandler.GenerateMessageHandler, options => { //ȡĬ΢ - var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting; + var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting.Items["OpenVip"]; //[] ΢ options.AccountSettingFunc = context => weixinSetting; @@ -56,7 +56,6 @@ //ʾȡѹעû OpenIdȡĵһ var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting.MpSetting; - var result = new StringBuilder(); var users = await Senparc.Weixin.MP.AdvancedAPIs.UserApi.GetAsync(weixinSetting.WeixinAppId, null); Console.WriteLine($"չʾǰ {users.count} OpenId"); diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Services/LocationService.cs b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Services/LocationService.cs deleted file mode 100644 index a6d4efd321..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Services/LocationService.cs +++ /dev/null @@ -1,95 +0,0 @@ -/*---------------------------------------------------------------- - Copyright (C) 2024 Senparc - - 文件名:LocationService.cs - 文件功能描述:地理位置信息处理 - - - 创建标识:Senparc - 20150312 -----------------------------------------------------------------*/ -//DPBMARK_FILE MP - -using System.Collections.Generic; -using Senparc.Weixin.MP.Entities; -using Senparc.CO2NET.Helpers.BaiduMap; -using Senparc.CO2NET.Helpers.GoogleMap; -using Senparc.Weixin.MP.Helpers; -using Senparc.CO2NET.Helpers; -using Senparc.NeuChar.Entities; - -namespace Senparc.Weixin.Sample.MP -{ - public class LocationService - { - public ResponseMessageNews GetResponseMessage(RequestMessageLocation requestMessage) - { - var responseMessage = ResponseMessageBase.CreateFromRequestMessage(requestMessage); - - #region 百度地图 - - { - var markersList = new List(); - markersList.Add(new BaiduMarkers() - { - Longitude = requestMessage.Location_X, - Latitude = requestMessage.Location_Y, - Color = "red", - Label = "S", - Size = BaiduMarkerSize.m - }); - - var mapUrl = BaiduMapHelper.GetBaiduStaticMap(requestMessage.Location_X, requestMessage.Location_Y, 1, 6, markersList); - responseMessage.Articles.Add(new Article() - { - Description = string.Format("【来自百度地图】您刚才发送了地理位置信息。Location_X:{0},Location_Y:{1},Scale:{2},标签:{3}", - requestMessage.Location_X, requestMessage.Location_Y, - requestMessage.Scale, requestMessage.Label), - PicUrl = mapUrl, - Title = "定位地点周边地图", - Url = mapUrl - }); - } - - #endregion - - #region GoogleMap - - { - var markersList = new List(); - markersList.Add(new GoogleMapMarkers() - { - X = requestMessage.Location_X, - Y = requestMessage.Location_Y, - Color = "red", - Label = "S", - Size = GoogleMapMarkerSize.Default, - }); - var mapSize = "480x600"; - var mapUrl = GoogleMapHelper.GetGoogleStaticMap(19 /*requestMessage.Scale*//*微信和GoogleMap的Scale不一致,这里建议使用固定值*/, - markersList, mapSize); - responseMessage.Articles.Add(new Article() - { - Description = string.Format("【来自GoogleMap】您刚才发送了地理位置信息。Location_X:{0},Location_Y:{1},Scale:{2},标签:{3}", - requestMessage.Location_X, requestMessage.Location_Y, - requestMessage.Scale, requestMessage.Label), - PicUrl = mapUrl, - Title = "定位地点周边地图", - Url = mapUrl - }); - } - - #endregion - - - responseMessage.Articles.Add(new Article() - { - Title = "微信公众平台SDK 官网链接", - Description = "Senparc.Weixin.MK SDK地址", - PicUrl = "https://sdk.weixin.senparc.com/images/logo.jpg", - Url = "https://sdk.weixin.senparc.com" - }); - - return responseMessage; - } - } -} \ No newline at end of file diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/Index.cshtml b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/Index.cshtml deleted file mode 100644 index 887942711a..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/Index.cshtml +++ /dev/null @@ -1,55 +0,0 @@ -@*DPBMARK_FILE MP*@ -@{ - Layout = null; -} - - - - - - - OAuth2.0授权测试 - - - - - - - -

OAuth2.0授权测试

-

注意:此页面仅供测试,测试号随时可能过期。请将此DEMO部署到您自己的服务器上,并使用自己的appid和secret。

-

- 当前returnUrl: - @if (ViewData["returnUrl"] == null || ViewData["returnUrl"] as string == "") - { - - 不带returnUrl。 -
- 使用不带returnUrl的页面会停留在Callback页面,此页面如果刷新(或后退到此页面),会导致code过期的错误,只建议在测试阶段使用。 -
- - 测试带returnUrl@(Html.ActionLink("点击这里", "Index", new { returnUrl = Url.Action("TestReturnUrl") }))。 - - } - else - { - @ViewData["returnUrl"]
- 携带returnUrl后,页面最终会跳转到returnUrl对应页面,避免刷新页面导致code的错误。 - } -

-

点击这里测试snsapi_userinfo

-

- 将要链接到的地址:
- -

-

点击这里测试snsapi_base

-

- 将要链接到的地址:
- -

- - diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/UserInfoCallback.cshtml b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/UserInfoCallback.cshtml deleted file mode 100644 index 21f2ffe470..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/OAuth2/UserInfoCallback.cshtml +++ /dev/null @@ -1,62 +0,0 @@ -@model Senparc.Weixin.MP.AdvancedAPIs.OAuth.OAuthUserInfo -@using Senparc.Weixin.Helpers -@*DPBMARK_FILE MP*@ -@{ - Layout = null; -} - - - - - OAuth2.0授权测试授权成功 - - -

OAuth2.0授权测试授权成功!

- @if (ViewData.ContainsKey("ByBase")) - { -

您看到的这个页面来自于snsapi_base授权,因为您已关注本微信,所以才能查询到详细用户信息,否则只能进行常规的授权。

- } - else - { -

您看到的这个页面来自于snsapi_userinfo授权,可以直接获取到用户详细信息。

- } -

下面是通过授权得到的您的部分个人信息:

-

openid:@Model.openid

-

nickname:@Model.nickname

-

country:@Model.country

-

province:@Model.province

-

city:@Model.city

-

sex:@Model.sex

- @if (Model.unionid != null) - { -

unionid:@Model.unionid

- } -

- 头像:
- -
- 默认(某些情况下直接调用可能看不到,需要抓取) -

- -

-
- 46x46 -

-

-
- 64x64 -

-

-
- 96x96 -

-

-
- 132x132 -

-

-
- 640x640 -

- - diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/WeixinJSSDK/Index.cshtml b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/WeixinJSSDK/Index.cshtml deleted file mode 100644 index 53d5899ba5..0000000000 --- a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/Views/WeixinJSSDK/Index.cshtml +++ /dev/null @@ -1,112 +0,0 @@ -@using Microsoft.AspNetCore.Http; -@model Senparc.Weixin.MP.Helpers.JsSdkUiPackage -@*DPBMARK_FILE MP*@ -@{ - Layout = null; -} -@* - 使用JSSDK,首先要到微信公众平台【公众号设置】下的【功能设置】里面设置“JS接口安全域名” -*@ - - - - - 公众号JSSDK演示 - - - - - - - -

公众号JSSDK演示

-
- 此页面是Senparc.Weixin.MP JSSDK的演示,可以点击右上方按钮,转发到朋友圈或者朋友进行测试。
- 顺利的话,转发的内容可以看到自定义的标题,配有一个自定义图片。 -
- - - diff --git a/Samples/MP/Senparc.Weixin.Sample.MP.Simple/readme.md b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/readme.md new file mode 100644 index 0000000000..372e4accfb --- /dev/null +++ b/Samples/MP/Senparc.Weixin.Sample.MP.Simple/readme.md @@ -0,0 +1,169 @@ +# Senparc.Weixin.Sample.MP.Simple 项目说明 + +当先项目为公众号平台提供了简化的示例,相关功能已经可以满足最基本公众号的开发。 + +您可以直接运行 `Senparc.Weixin.Sample.MP.Simple.csproj` 文件单独运行公众号示例,或在 `/Samples/All/net8-mvc` 下的解决方案中找到此项目。 + +> 由于所有 Senparc.Weixin SDK 遵循统一的底层标准,因此当前公众号的所有演示方法同样可以举一反三运用到小程序、企业微信、微信支付、开放平台等其他平台。 + +## 关键代码 + +### 一站式引用类库 + +您可以单独引用公众号的 Nuget 包:Senparc.Weixin.MP,也可以一次性引用完整包,以使您的程序具备开发所有微信平台的能力:Senparc.Weixin.MP。 + +在 .csproj 文件中添加引用(或使用命令行添加:`dotnet add package Senparc.Weixin.MP`): + +``` xml + +``` + +### 添加注册代码 + +在 Program.cs 中对 SDK 及基础库进行注册: + +``` C# +//Senparc.Weixin 注册(必须) +builder.Services.AddSenparcWeixinServices(builder.Configuration); +``` + +``` C# +//启用微信配置(必须) +var registerService = app.UseSenparcWeixin(app.Environment, + senparcSetting: null /* 不为 null 则覆盖 appsettings 中的 SenpacSetting 配置*/, + senparcWeixinSetting: null /* 不为 null 则覆盖 appsettings 中的 SenpacWeixinSetting 配置*/, + globalRegisterConfigure: register => { /* CO2NET 全局配置 */ }, + weixinRegisterConfigure: (register, weixinSetting) => {/* 注册公众号或其他平台信息(可以执行多次,注册多个公众号)*/}, + autoRegisterAllPlatforms: true /* 自动注册所有平台 */ + ); +``` +> 如果不需要进行额外配置,则无需修改上述任何代码 + +只需两行代码,即可完成所有 Senparc.Weixin SDK 调用 + +### 如何使用高级接口? + +您可以在程序任意位置,调用公众号(或其他平台)的高级接口。 + +例如,当您需要获取当前关注用的的 OpenId 时,这样进行调用: + +``` C# + var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting.MpSetting; + var users = await Senparc.Weixin.MP.AdvancedAPIs.UserApi.GetAsync(weixinSetting.WeixinAppId, null); +``` + +您也可以使用 MiniAPI 快速设置一个接口(请注意接口开放范围和安全性): + +``` C# +app.MapGroup("/").MapGet("/TryApi", async () => +{ + //演示获取已关注用户的 OpenId(分批获取的第一批) + + var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting.MpSetting; + var users = await Senparc.Weixin.MP.AdvancedAPIs.UserApi.GetAsync(weixinSetting.WeixinAppId, null); + return users.data.openid; +}); +``` + +>提醒:使用时请注意接口开放范围和安全性 + +### 如何提供消息自动回复能力? + +您可以使用 `hMessageHandler` 快速设置消息回复能力 + +#### 创建消息处理类 + +创建一个新的类,CustomMessageHander.cs: + +``` C# +namespace Senparc.Weixin.Sample.MP +{ + /// + /// 自定义MessageHandler + /// 把MessageHandler作为基类,重写对应请求的处理方法 + /// + public partial class CustomMessageHandler : MessageHandler + { + public CustomMessageHandler(Stream inputStream, PostModel postModel, int maxRecordCount = 0, bool onlyAllowEncryptMessage = false, IServiceProvider serviceProvider = null) + : base(inputStream, postModel, maxRecordCount, onlyAllowEncryptMessage, serviceProvider: serviceProvider) + { + } + + public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage) + { + var responseMessage = this.CreateResponseMessage(); + responseMessage.Content = $"收到一条您发来的信息,类型为:{requestMessage.MsgType}"; + return responseMessage; + } + } +} +``` + +当我们需要处理特定类型信息的时候,只需要添加对应的重写方法,如当我们需要处理用户发过来的文字信息时,添加一个方法: + +``` C# +public override async Task OnTextOrEventRequestAsync(RequestMessageText requestMessage) +{ + var responseMessage = this.CreateResponseMessage(); + responseMessage.Content = $"收到您发来的文字信息:{requestMessage.Content}"; + return responseMessage; +} +``` + +当我们需要将这个 MessageHandler 暴露给微信服务器时,可以将其放入 Controller 直接暴露 API 接口,或使用中间件的方式,只需在 Program.cs 中添加一句代码: + +``` C# +app.UseMessageHandlerForMp("/WeixinAsync", CustomMessageHandler.GenerateMessageHandler, options => +{ + //获取默认微信配置 + var weixinSetting = Senparc.Weixin.Config.SenparcWeixinSetting; + + //[必须] 设置微信配置 + options.AccountSettingFunc = context => weixinSetting; + + //[可选] 设置最大文本长度回复限制(超长后会调用客服接口分批次回复) + options.TextResponseLimitOptions = new TextResponseLimitOptions(2048, weixinSetting.WeixinAppId); +}); +``` + +此时,我们就可以将 `https://domain/WeixinAsync` 这个路径提供给微信作为消息接口。 + +### 如何提供微信秘钥等信息? + +当我们使用微信消息接口、高级接口的时候,需要提供 Token、AppId、AppSecret 等信息,此时我们只需在 `appsettings.json` 中添加相应的注册代码: + +``` json +//CO2NET 设置 +"SenparcSetting": { + //以下为 CO2NET 的 SenparcSetting 全局配置,请勿修改 key,勿删除任何项 + + "IsDebug": true, + "DefaultCacheNamespace": "DefaultCache", + + //分布式缓存 + "Cache_Redis_Configuration": "#{Cache_Redis_Configuration}#", //Redis配置 + "Cache_Memcached_Configuration": "#{Cache_Memcached_Configuration}#", //Memcached配置 + "SenparcUnionAgentKey": "#{SenparcUnionAgentKey}#" //SenparcUnionAgentKey +}, + +//Senparc.Weixin SDK 设置 +"SenparcWeixinSetting": { + //以下为 Senparc.Weixin 的 SenparcWeixinSetting 微信配置 + //注意:所有的字符串值都可能被用于字典索引,因此请勿留空字符串(但可以根据需要,删除对应的整条设置)! + + //微信全局 + "IsDebug": true, + + //以下不使用的参数可以删除,key 修改后将会失效 + + //公众号 + "Token": "#{Token}#", //说明:字符串内两侧#和{}符号为 Azure DevOps 默认的占位符格式,如果您有明文信息,请删除同占位符,修改整体字符串,不保留#和{},如:{"Token": "MyFullToken"} + "EncodingAESKey": "#{EncodingAESKey}#", + "WeixinAppId": "#{WeixinAppId}#", + "WeixinAppSecret": "#{WeixinAppSecret}#" +} +``` + +上述代码中,`SenparcSetting` 节点是基础的系统注册信息(如缓存等、调试状态等),建议保留。`SenparcWeixinSetting` 节点用于设置微信平台, +上述代码仅提供了公众号所必须的信息,如果您需要同时注册更多平台,可以参考 [/Samples/All/net8-mvc](/Samples/All/net8-mvc/Senparc.Weixin.Sample.Net8/appsettings.json) 下的完整示例,或 `/Samples/` 目录下的其他示例。 + diff --git a/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/Senparc.Weixin.Sample.TenPayV3.net8.csproj b/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/Senparc.Weixin.Sample.TenPayV3.net8.csproj index 3f3d683ad6..568cac9db5 100644 --- a/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/Senparc.Weixin.Sample.TenPayV3.net8.csproj +++ b/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/Senparc.Weixin.Sample.TenPayV3.net8.csproj @@ -28,7 +28,7 @@ - + diff --git a/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/appsettings.json b/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/appsettings.json index 4baa280ba5..c3d959f095 100644 --- a/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/appsettings.json +++ b/Samples/TenPayV3/Senparc.Weixin.Sample.TenPayV3/appsettings.json @@ -53,6 +53,7 @@ */ "TenPayV3_PrivateKey": "#{TenPayV3_PrivateKey}#", //(新)证书私钥 "TenPayV3_SerialNumber": "#{TenPayV3_SerialNumber}#", //(新)证书序列号 - "TenPayV3_ApiV3Key": "#{TenPayV3_APIv3Key}#" //(新)APIv3 密钥 + "TenPayV3_ApiV3Key": "#{TenPayV3_APIv3Key}#", //(新)APIv3 密钥 + "EncryptionType": "#{EncryptionType}#" // 加密类型:RSA / SM } } diff --git a/Samples/deploy-sample-net8.ps1 b/Samples/deploy-sample-net8.ps1 new file mode 100644 index 0000000000..21f18307f4 --- /dev/null +++ b/Samples/deploy-sample-net8.ps1 @@ -0,0 +1,55 @@ +# deploy.ps1 +param ( + [string]$sourcePath, + [string]$destinationPath, + [string]$server, + [string]$username, + [string]$privateKeyPath +) + +# 复制文件到服务器 +function Copy-Files { + param ( + [string]$source, + [string]$destination, + [string]$server, + [string]$username, + [string]$privateKey + ) + + # 使用SCP命令上传文件 + $scpCommand = "scp -i $privateKey -r $source $username@$server:$destination" + Invoke-Expression $scpCommand +} + +# 排除特定文件 +function Exclude-Files { + param ( + [string]$source, + [string]$destination + ) + + $filesToExclude = @("Web.Config", "appsettings.json") + foreach ($file in $filesToExclude) { + $sourceFile = Join-Path -Path $source -ChildPath $file + if (Test-Path -Path $sourceFile) { + # 将文件移动到临时路径,以便稍后恢复 + $tempPath = Join-Path -Path $env:TEMP -ChildPath $file + Move-Item -Path $sourceFile -Destination $tempPath + } + } + + Copy-Files -source $source -destination $destination -server $server -username $username -privateKey $privateKeyPath + + # 恢复被排除的文件 + foreach ($file in $filesToExclude) { + $tempPath = Join-Path -Path $env:TEMP -ChildPath $file + if (Test-Path -Path $tempPath) { + $destinationFile = Join-Path -Path $destination -ChildPath $file + Move-Item -Path $tempPath -Destination $destinationFile + } + } +} + +# 调用函数 +Exclude-Files -source $sourcePath -destination $destinationPath diff --git a/src/Senparc.Weixin.MP/Senparc.Weixin.MP/AdvancedAPIs/NewTmpl/NewTmplApi.cs b/src/Senparc.Weixin.MP/Senparc.Weixin.MP/AdvancedAPIs/NewTmpl/NewTmplApi.cs index 3f1b2a9300..46c9851f93 100644 --- a/src/Senparc.Weixin.MP/Senparc.Weixin.MP/AdvancedAPIs/NewTmpl/NewTmplApi.cs +++ b/src/Senparc.Weixin.MP/Senparc.Weixin.MP/AdvancedAPIs/NewTmpl/NewTmplApi.cs @@ -172,7 +172,7 @@ public static WxJsonResult DelTemplate(string accessToken, string priTmplId, int /// 所需下发的订阅模板id /// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } /// 跳转网页时填写 - /// 跳转小程序时填写 + /// 跳转小程序时填写 /// /// public static WxJsonResult BizSend(string accessTokenOrAppId, string toUser, string templateId, @@ -205,7 +205,7 @@ public static WxJsonResult BizSend(string accessTokenOrAppId, string toUser, str /// 所需下发的订阅模板id /// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } /// 跳转网页时填写 - /// 跳转小程序时填写 + /// 跳转小程序时填写 /// /// public static async Task BizSendAsync(string accessTokenOrAppId, string toUser, string templateId, diff --git a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3.Test/TenPayHttpClient/Signer/SM3WithSM2SignerTests.cs b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3.Test/TenPayHttpClient/Signer/SM3WithSM2SignerTests.cs new file mode 100644 index 0000000000..bc205b78b7 --- /dev/null +++ b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3.Test/TenPayHttpClient/Signer/SM3WithSM2SignerTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Client.TenPayHttpClient.Signer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.TenPayHttpClient.Signer.Tests +{ + [TestClass()] + public class SM3WithSM2SignerTests + { + [TestMethod()] + public void SignTest() + { + SM3WithSM2Signer sM3WithSM2Signer = new(); + var message = ""; + var privateKey = ""; + var sign = sM3WithSM2Signer.Sign(message, privateKey); + var exceptResult = "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"; + Assert.AreEqual(exceptResult, sign); + } + } +} \ No newline at end of file diff --git a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayApiRequest.cs b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayApiRequest.cs index 7e01de31b4..36fe44404d 100644 --- a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayApiRequest.cs +++ b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayApiRequest.cs @@ -86,7 +86,9 @@ public void SetHeader(HttpClient client) // 外部 if (_setHeaderAction != null) + { _setHeaderAction(client); + } } /// diff --git a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayHttpHandler.cs b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayHttpHandler.cs index 9ab9af106b..c227bdb523 100644 --- a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayHttpHandler.cs +++ b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/HttpHandlers/TenPayHttpHandler.cs @@ -30,20 +30,18 @@ and limitations under the License. 修改标识:Senparc - 20210822 修改描述:重构使用ISenparcWeixinSettingForTenpayV3初始化实例 + 修改标识:MartyZane - 20240530 + 修改描述:TenPayV3Setting里面增加AuthrizationType属性,用于设置认证类型,选项有WECHATPAY2-SHA256-RSA2048、WECHATPAY2-SM2-WITH-SM3,默认为WECHATPAY2-SM2-WITH-SM3 ----------------------------------------------------------------*/ -using Senparc.Weixin.Entities; -using Senparc.Weixin.TenPayV3.Helpers; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Senparc.Weixin.Entities; +using Senparc.Weixin.TenPayV3.Helpers; namespace Senparc.Weixin.TenPayV3 { @@ -86,7 +84,13 @@ protected async override Task SendAsync( CancellationToken cancellationToken) { var auth = await BuildAuthAsync(request); - string value = $"WECHATPAY2-SHA256-RSA2048 {auth}"; + + var authorizationValue = + _tenpayV3Setting.EncryptionType == CertType.SM.ToString() + ? "WECHATPAY2-SM2-WITH-SM3" + : "WECHATPAY2-SHA256-RSA2048"; + + string value = $"{authorizationValue} {auth}"; request.Headers.Add("Authorization", value); return await base.SendAsync(request, cancellationToken); diff --git a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/TenPayHttpClient/Signer/SM3WithSM2Signer.cs b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/TenPayHttpClient/Signer/SM3WithSM2Signer.cs index 2f9a330052..d07299d4b5 100644 --- a/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/TenPayHttpClient/Signer/SM3WithSM2Signer.cs +++ b/src/Senparc.Weixin.TenPay/Senparc.Weixin.TenPayV3/TenPayHttpClient/Signer/SM3WithSM2Signer.cs @@ -1,10 +1,50 @@ -using System; -using System.Security.Cryptography; +#region Apache License Version 2.0 +/*---------------------------------------------------------------- + +Copyright 2024 Jeffrey Su & Suzhou Senparc Network Technology Co.,Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the +License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the specific language governing permissions +and limitations under the License. + +Detail: https://github.com/JeffreySu/WeiXinMPSDK/blob/master/license.md + +----------------------------------------------------------------*/ +#endregion Apache License Version 2.0 + +/*---------------------------------------------------------------- + Copyright (C) 2024 Senparc + + 文件名:SM3WithSM2Signer.cs + + + 创建标识:MartyZane - 20240530 + + 修改标识:MartyZane - 20240530 + 修改描述:验证国密算法SM3WithSM2,修改返回标识为SM2-WITH-SM3 + +----------------------------------------------------------------*/ + +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Utilities.Encoders; +using System; +using System.Net.Sockets; +using System.Text; namespace Client.TenPayHttpClient.Signer { public class SM3WithSM2Signer : ISigner { + /// + /// 返回同微信中Http请求头的Authrization的认证类型 + /// + /// public string GetAlgorithm() { return "SM2-WITH-SM3"; @@ -12,25 +52,14 @@ public string GetAlgorithm() public string Sign(string message, string privateKey = null) { - //privateKey ??= Senparc.Weixin.Config.SenparcWeixinSetting.TenPayV3_PrivateKey; - byte[] keyData = Convert.FromBase64String(privateKey); - - #region 以下方法不兼容 Linux - //using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob)) - //using (RSACng rsa = new RSACng(cngKey)) - //{ - // byte[] data = System.Text.Encoding.UTF8.GetBytes(message); - // return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); - //} - #endregion - - using (var rsa = System.Security.Cryptography.RSA.Create()) - { - rsa.ImportPkcs8PrivateKey(keyData, out _); - byte[] data = System.Text.Encoding.UTF8.GetBytes(message); - return Convert.ToBase64String(rsa.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); - } + //加密 + SM3Digest sm3 = new SM3Digest(); + sm3.BlockUpdate(keyData, 0, keyData.Length); + byte[] md = new byte[sm3.GetDigestSize()]; + sm3.DoFinal(md, 0); + string PasswdDigest = new UTF8Encoding().GetString(Hex.Encode(md)); + return PasswdDigest; } } } diff --git a/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.Interfaces.cs b/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.Interfaces.cs index 75be0d59d3..023b7cf1a3 100644 --- a/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.Interfaces.cs +++ b/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.Interfaces.cs @@ -229,6 +229,11 @@ public interface ISenparcWeixinSettingForTenpayV3 : ISenparcWeixinSettingBase /// 小程序微信支付WxOpenTenpayNotify /// string TenPayV3_WxOpenTenpayNotify { get; set; } + + /// + /// 设置HTTP头Authrization的加密认证类型 + /// + string EncryptionType { get; set; } } /// diff --git a/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.cs b/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.cs index 8173c663a0..113b248984 100644 --- a/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.cs +++ b/src/Senparc.Weixin/Senparc.Weixin/Entities/SenparcWeixinSettings/SenparcWeixinSettingItem.cs @@ -329,7 +329,15 @@ public virtual string TenPayV3_PrivateKey /// public virtual string TenPayV3_WxOpenTenpayNotify { get; set; } + /// + /// 设置HTTP头Authrization的加密认证类型 + /// + public virtual string EncryptionType { get; set; } + /// + /// 设置HTTP头Authrization的认证类型 + /// + public virtual string AuthrizationType { get; set; } #endregion #endregion