| 2025-12-01 | sunpengfei | ![]() |
| 2025-12-01 | sunpengfei | ![]() |
| 2025-12-01 | sunpengfei | ![]() |
| 2025-12-01 | sunpengfei | ![]() |
| 2025-12-01 | sunpengfei | ![]() |
| 2025-12-01 | sunpengfei | ![]() |
ApiTools.Application/WxUtils/Commands/WxmpSubscribMessageCommandHandler.cs
@@ -1,5 +1,9 @@ using Aop.Api.Domain; using ApiTools.Core; using ApiTools.Core.Entities.LogRecords; using Furion; using Furion.DatabaseAccessor; using Furion.HttpRemote; using log4net.Core; using MediatR; using Microsoft.AspNetCore.Http; @@ -13,16 +17,18 @@ namespace ApiTools.Application { public class WxmpSubscribMessageCommandHandler( IRepository<WxmpSubscribMessageLog> repWxmpSubscribMessageLog, ILogger<WxmpSubscribMessageCommandHandler> logger, WxmpUtils utils, IHttpContextAccessor httpContextAccessor IHttpRemoteService httpRemoteService ) : IRequestHandler<SendWxmpSubscribMessageCommand, Guid>, IRequestHandler<WxmpSubscribMessageNotifyCommand, bool> IRequestHandler<WxmpSubscribMessageNotifyCommand, Guid> { private readonly IRepository<WxmpSubscribMessageLog> repWxmpSubscribMessageLog = repWxmpSubscribMessageLog; private readonly ILogger<WxmpSubscribMessageCommandHandler> logger = logger; private readonly WxmpUtils utils = utils; private readonly IHttpContextAccessor httpContextAccessor = httpContextAccessor; private readonly IHttpRemoteService httpRemoteService = httpRemoteService; /// <summary> /// 微信小程序发送订阅消息 @@ -40,7 +46,17 @@ WxmpCode = request.WxmpCode, Touser = request.Touser, }); return Guid.Empty; var log = new WxmpSubscribMessageLog { Code = request.WxmpCode, OpenId = request.Touser, SubscribeStatusString = "send", TemplateId = request.TemplateId, Page = request.Page, Data = request.Data.ToJson() }; await repWxmpSubscribMessageLog.InsertNowAsync(log); return log.Id; } /// <summary> @@ -49,18 +65,33 @@ /// <param name="request"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public async Task<bool> Handle(WxmpSubscribMessageNotifyCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(WxmpSubscribMessageNotifyCommand request, CancellationToken cancellationToken) { var req = httpContextAccessor.HttpContext.Request; logger.LogInformation($"微信小程序订阅消息通知query:{req.QueryString.Value}"); var env = App.GetConfig<string>("Environment"); if (env == "Product") { try { var json = request.ToJson(); await httpRemoteService.PostAsStringAsync("http://118.178.252.28:8780/api/common/wxmp/wxmpSubscribMessageNotify", builder => builder.SetJsonContent(json)); } catch { req.EnableBuffering(); req.Body.Position = 0; using var reader = new StreamReader(req.Body, Encoding.UTF8, leaveOpen: true); var body = await reader.ReadToEndAsync(); logger.LogInformation($"微信小程序订阅消息通知body:{body}"); req.Body.Position = 0; return true; } } var log = new WxmpSubscribMessageLog { Code = request.Code, OpenId = request.OpenId, PopupScene = request.Content.PopupScene, SubscribeStatusString = request.Content.SubscribeStatusString, TemplateId = request.Content.TemplateId }; await repWxmpSubscribMessageLog.InsertNowAsync(log); return log.Id; } } } ApiTools.Core/ApiTools.Core.csproj
@@ -39,4 +39,8 @@ </None> </ItemGroup> <ItemGroup> <Folder Include="Utils\WxmpUtils\Crypto\" /> </ItemGroup> </Project> ApiTools.Core/ApiTools.Core.xml
@@ -249,6 +249,46 @@ 耗时毫秒数 </summary> </member> <member name="T:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog"> <summary> 微信订阅消息日志 </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.Code"> <summary> 小程序代码 </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.OpenId"> <summary> 用户开放Id </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.PopupScene"> <summary> 场景 </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.SubscribeStatusString"> <summary> 状态 </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.TemplateId"> <summary> 模板Id </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.Page"> <summary> 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转 </summary> </member> <member name="P:ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog.Data"> <summary> 模板内容,格式形如{ "phrase3": { "value": "审核通过" }, "name1": { "value": "订阅" }, "date2": { "value": "2019-12-25 09:42" } } </summary> </member> <member name="T:ApiTools.Core.ScheduleJobTriggerTimeline"> <summary> 定时任务-作业触发器运行记录 @@ -3321,6 +3361,81 @@ <member name="T:ApiTools.Core.WxmpSubscribMessageNotifyCommand"> <summary> 微信小程序订阅消息通知 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Code"> <summary> 小程序代码 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.OpenId"> <summary> 用户开放Id </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.ToUserName"> <summary> 接收人 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.FromUserName"> <summary> 发送人 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.CreateTime"> <summary> 创建时间 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.MsgType"> <summary> 消息类型 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Event"> <summary> 事件 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Content"> <summary> 内容 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommandContent.PopupScene"> <summary> 场景 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommandContent.SubscribeStatusString"> <summary> 状态 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommandContent.TemplateId"> <summary> 模板Id </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.signature"> <summary> 签名 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.timestamp"> <summary> 时间戳 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.nonce"> <summary> 随机数 </summary> </member> <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.echostr"> <summary> 随机字符串 </summary> </member> <member name="T:ApiTools.Core.GetWxmpSubscribMessageTemplatesQuery"> @@ -7140,6 +7255,21 @@ 微信小程序配置 </summary> </member> <member name="P:ApiTools.Core.WxmpOptions.SubscribMessage"> <summary> 订阅消息 </summary> </member> <member name="P:ApiTools.Core.WxmpOptionsSubscribMessage.Token"> <summary> 令牌 </summary> </member> <member name="P:ApiTools.Core.WxmpOptionsSubscribMessage.EncodingAESKey"> <summary> 消息加密密钥 </summary> </member> <member name="P:ApiTools.Core.WxmpOptionsItem.Code"> <summary> 编号 @@ -7308,5 +7438,20 @@ <param name="xmlDoc"></param> <returns></returns> </member> <member name="M:Tencent.Cryptography.AES_decrypt(System.String,System.String,System.String@)"> <summary> 解密方法 </summary> <param name="Input">密文</param> <param name="EncodingAESKey"></param> <returns></returns> </member> <member name="M:Tencent.Cryptography.chr(System.Int32)"> 将数字转化成ASCII码对应的字符,用于对明文进行补码 @param a 需要转化的数字 @return 转化得到的字符 </member> </members> </doc> ApiTools.Core/Entities/LogRecords/WxmpSubscribMessageLog.cs
New file @@ -0,0 +1,51 @@ using Furion.DatabaseAccessor; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ApiTools.Core.Entities.LogRecords { /// <summary> /// 微信订阅消息日志 /// </summary> public class WxmpSubscribMessageLog : CommonEntity<MasterDbContextLocator>, IDbAuditLogIgnore { /// <summary> /// 小程序代码 /// </summary> public string Code { get; set; } /// <summary> /// 用户开放Id /// </summary> public string OpenId { get; set; } /// <summary> /// 场景 /// </summary> public string PopupScene { get; set; } /// <summary> /// 状态 /// </summary> public string SubscribeStatusString { get; set; } /// <summary> /// 模板Id /// </summary> public string TemplateId { get; set; } /// <summary> /// 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转 /// </summary> public string Page { get; set; } /// <summary> /// 模板内容,格式形如{ "phrase3": { "value": "审核通过" }, "name1": { "value": "订阅" }, "date2": { "value": "2019-12-25 09:42" } } /// </summary> public string Data { get; set; } } } ApiTools.Core/Models/WxmpUtils/Commands/WxmpSubscribMessageNotifyCommand.cs
@@ -1,4 +1,5 @@ using MediatR; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; @@ -10,13 +11,57 @@ /// <summary> /// 微信小程序订阅消息通知 /// </summary> [Resource([EnumResourceController.CommonServerWxmpUtils], Method = EnumResourceMethod.Post)] public class WxmpSubscribMessageNotifyCommand : IRequest<bool> [Resource([EnumResourceController.CommonServerWxmpUtils], AllowAnonymous = true)] public class WxmpSubscribMessageNotifyCommand : IRequest<Guid> { /// <summary> /// 小程序代码 /// </summary> public string Code { get; set; } /// <summary> /// 用户开放Id /// </summary> public string OpenId { get; set; } /// <summary> /// 接收人 /// </summary> public string ToUserName { get; set; } /// <summary> /// 发送人 /// </summary> public string FromUserName { get; set; } public DateTime CreateTime { get; set; } /// <summary> /// 创建时间 /// </summary> public int CreateTime { get; set; } /// <summary> /// 消息类型 /// </summary> public string MsgType { get; set; } /// <summary> /// 事件 /// </summary> public string Event { get; set; } /// <summary> /// 内容 /// </summary> [JsonProperty("List")] public WxmpSubscribMessageNotifyCommandContent Content { get; set; } } public class WxmpSubscribMessageNotifyCommandContent { /// <summary> /// 场景 /// </summary> public string PopupScene { get; set; } /// <summary> /// 状态 /// </summary> public string SubscribeStatusString { get; set; } /// <summary> /// 模板Id /// </summary> public string TemplateId { get; set; } } } ApiTools.Core/Models/WxmpUtils/Models/WxmpSubscribMessageNotifyRequestQuery.cs
New file @@ -0,0 +1,45 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace ApiTools.Core { public class WxmpSubscribMessageNotifyRequestQuery { /// <summary> /// 签名 /// </summary> public string signature { get; set; } /// <summary> /// 时间戳 /// </summary> public string timestamp { get; set; } /// <summary> /// 随机数 /// </summary> public string nonce { get; set; } /// <summary> /// 随机字符串 /// </summary> public string echostr { get; set; } public string openid { get; set; } public string encrypt_type { get; set; } public string msg_signature { get; set; } } public class WxmpSubscribMessageNotifyRequestBody { public string ToUserName { get; set; } public string Encrypt { get; set; } } } ApiTools.Core/Utils/WxmpUtils/Crypto/Cryptography.cs
New file @@ -0,0 +1,232 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Cryptography; using System.IO; using System.Net; namespace Tencent { class Cryptography { public static UInt32 HostToNetworkOrder(UInt32 inval) { UInt32 outval = 0; for (int i = 0; i < 4; i++) outval = (outval << 8) + ((inval >> (i * 8)) & 255); return outval; } public static Int32 HostToNetworkOrder(Int32 inval) { Int32 outval = 0; for (int i = 0; i < 4; i++) outval = (outval << 8) + ((inval >> (i * 8)) & 255); return outval; } /// <summary> /// 解密方法 /// </summary> /// <param name="Input">密文</param> /// <param name="EncodingAESKey"></param> /// <returns></returns> /// public static string AES_decrypt(String Input, string EncodingAESKey, ref string appid) { byte[] Key; Key = Convert.FromBase64String(EncodingAESKey + "="); byte[] Iv = new byte[16]; Array.Copy(Key, Iv, 16); byte[] btmpMsg = AES_decrypt(Input, Iv, Key); int len = BitConverter.ToInt32(btmpMsg, 16); len = IPAddress.NetworkToHostOrder(len); byte[] bMsg = new byte[len]; byte[] bAppid = new byte[btmpMsg.Length - 20 - len]; Array.Copy(btmpMsg, 20, bMsg, 0, len); Array.Copy(btmpMsg, 20+len , bAppid, 0, btmpMsg.Length - 20 - len); string oriMsg = Encoding.UTF8.GetString(bMsg); appid = Encoding.UTF8.GetString(bAppid); return oriMsg; } public static String AES_encrypt(String Input, string EncodingAESKey, string appid) { byte[] Key; Key = Convert.FromBase64String(EncodingAESKey + "="); byte[] Iv = new byte[16]; Array.Copy(Key, Iv, 16); string Randcode = CreateRandCode(16); byte[] bRand = Encoding.UTF8.GetBytes(Randcode); byte[] bAppid = Encoding.UTF8.GetBytes(appid); byte[] btmpMsg = Encoding.UTF8.GetBytes(Input); byte[] bMsgLen = BitConverter.GetBytes(HostToNetworkOrder(btmpMsg.Length)); byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bAppid.Length + btmpMsg.Length]; Array.Copy(bRand, bMsg, bRand.Length); Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length); Array.Copy(btmpMsg, 0, bMsg, bRand.Length + bMsgLen.Length, btmpMsg.Length); Array.Copy(bAppid, 0, bMsg, bRand.Length + bMsgLen.Length + btmpMsg.Length, bAppid.Length); return AES_encrypt(bMsg, Iv, Key); } private static string CreateRandCode(int codeLen) { string codeSerial = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z"; if (codeLen == 0) { codeLen = 16; } string[] arr = codeSerial.Split(','); string code = ""; int randValue = -1; Random rand = new Random(unchecked((int)DateTime.Now.Ticks)); for (int i = 0; i < codeLen; i++) { randValue = rand.Next(0, arr.Length - 1); code += arr[randValue]; } return code; } private static String AES_encrypt(String Input, byte[] Iv, byte[] Key) { var aes = new RijndaelManaged(); //秘钥的大小,以位为单位 aes.KeySize = 256; //支持的块大小 aes.BlockSize = 128; //填充模式 aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; aes.Key = Key; aes.IV = Iv; var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); byte[] xBuff = null; using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) { byte[] xXml = Encoding.UTF8.GetBytes(Input); cs.Write(xXml, 0, xXml.Length); } xBuff = ms.ToArray(); } String Output = Convert.ToBase64String(xBuff); return Output; } private static String AES_encrypt(byte[] Input, byte[] Iv, byte[] Key) { var aes = new RijndaelManaged(); //秘钥的大小,以位为单位 aes.KeySize = 256; //支持的块大小 aes.BlockSize = 128; //填充模式 //aes.Padding = PaddingMode.PKCS7; aes.Padding = PaddingMode.None; aes.Mode = CipherMode.CBC; aes.Key = Key; aes.IV = Iv; var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); byte[] xBuff = null; #region 自己进行PKCS7补位,用系统自己带的不行 byte[] msg = new byte[Input.Length + 32 - Input.Length % 32]; Array.Copy(Input, msg, Input.Length); byte[] pad = KCS7Encoder(Input.Length); Array.Copy(pad, 0, msg, Input.Length, pad.Length); #endregion #region 注释的也是一种方法,效果一样 //ICryptoTransform transform = aes.CreateEncryptor(); //byte[] xBuff = transform.TransformFinalBlock(msg, 0, msg.Length); #endregion using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) { cs.Write(msg, 0, msg.Length); } xBuff = ms.ToArray(); } String Output = Convert.ToBase64String(xBuff); return Output; } private static byte[] KCS7Encoder(int text_length) { int block_size = 32; // 计算需要填充的位数 int amount_to_pad = block_size - (text_length % block_size); if (amount_to_pad == 0) { amount_to_pad = block_size; } // 获得补位所用的字符 char pad_chr = chr(amount_to_pad); string tmp = ""; for (int index = 0; index < amount_to_pad; index++) { tmp += pad_chr; } return Encoding.UTF8.GetBytes(tmp); } /** * 将数字转化成ASCII码对应的字符,用于对明文进行补码 * * @param a 需要转化的数字 * @return 转化得到的字符 */ static char chr(int a) { byte target = (byte)(a & 0xFF); return (char)target; } private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key) { RijndaelManaged aes = new RijndaelManaged(); aes.KeySize = 256; aes.BlockSize = 128; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; aes.Key = Key; aes.IV = Iv; var decrypt = aes.CreateDecryptor(aes.Key, aes.IV); byte[] xBuff = null; using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write)) { byte[] xXml = Convert.FromBase64String(Input); byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32]; Array.Copy(xXml, msg, xXml.Length); cs.Write(xXml, 0, xXml.Length); } xBuff = decode2(ms.ToArray()); } return xBuff; } private static byte[] decode2(byte[] decrypted) { int pad = (int)decrypted[decrypted.Length - 1]; if (pad < 1 || pad > 32) { pad = 0; } byte[] res = new byte[decrypted.Length - pad]; Array.Copy(decrypted, 0, res, 0, decrypted.Length - pad); return res; } } } ApiTools.Core/Utils/WxmpUtils/Crypto/Readme.txt
New file @@ -0,0 +1,4 @@ 注意事项 1.Cryptography.cs文件封装了AES加解密过程,用户无须关心具体实现。WXBizMsgCrypt.cs文件提供了用户接入企业微信的两个接口,Sample.cs文件提供了如何使用这两个接口的示例。 2.WXBizMsgCrypt.cs封装了DecryptMsg, EncryptMsg两个接口,分别用于收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.cs文件。 3.加解密协议请参考微信公众平台官方文档。 ApiTools.Core/Utils/WxmpUtils/Crypto/Sample.cs
New file @@ -0,0 +1,82 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; namespace MsgCryptTest { class Sample { static void Main(string[] args) { //公众平台上开发者设置的token, appID, EncodingAESKey string sToken = "QDG6eK"; string sAppID = "wx5823bf96d3bd56c7"; string sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"; Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(sToken, sEncodingAESKey, sAppID); /* 1. 对用户回复的数据进行解密。 * 用户回复消息或者点击事件响应时,企业会收到回调消息,假设企业收到的推送消息: * POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1 Host: qy.weixin.qq.com Content-Length: 613 * * <xml> <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName> <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt> </xml> */ string sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6"; string sReqTimeStamp = "1409659813"; string sReqNonce = "1372623149"; string sReqData = "<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt></xml>"; string sMsg = ""; //解析之后的明文 int ret = 0; ret = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, sReqData, ref sMsg); if (ret != 0) { System.Console.WriteLine("ERR: Decrypt fail, ret: " + ret); return; } System.Console.WriteLine(sMsg); /* * 2. 企业回复用户消息也需要加密和拼接xml字符串。 * 假设企业需要回复用户的消息为: * <xml> * <ToUserName><![CDATA[mycreate]]></ToUserName> * <FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName> * <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> * <Content><![CDATA[this is a test]]></Content> * <MsgId>1234567890123456</MsgId> * </xml> * 生成xml格式的加密消息过程为: */ string sRespData = "<xml><ToUserName><![CDATA[mycreate]]></ToUserName><FromUserName><![CDATA[wx582测试一下中文的情况,消息长度是按字节来算的396d3bd56c7]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId></xml>"; string sEncryptMsg = ""; //xml格式的密文 ret = wxcpt.EncryptMsg(sRespData, sReqTimeStamp, sReqNonce, ref sEncryptMsg); System.Console.WriteLine("sEncryptMsg"); System.Console.WriteLine(sEncryptMsg); /*测试: * 将sEncryptMsg解密看看是否是原文 * */ XmlDocument doc = new XmlDocument(); doc.LoadXml(sEncryptMsg); XmlNode root = doc.FirstChild; string sig = root["MsgSignature"].InnerText; string enc = root["Encrypt"].InnerText; string timestamp = root["TimeStamp"].InnerText; string nonce = root["Nonce"].InnerText; string stmp = ""; ret = wxcpt.DecryptMsg(sig, timestamp, nonce, sEncryptMsg, ref stmp); System.Console.WriteLine("stemp"); System.Console.WriteLine(stmp + ret); return; } } } ApiTools.Core/Utils/WxmpUtils/Crypto/WXBizMsgCrypt.cs
New file @@ -0,0 +1,221 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.Collections; //using System.Web; using System.Security.Cryptography; //-40001 : 签名验证错误 //-40002 : xml解析失败 //-40003 : sha加密生成签名失败 //-40004 : AESKey 非法 //-40005 : appid 校验错误 //-40006 : AES 加密失败 //-40007 : AES 解密失败 //-40008 : 解密后得到的buffer非法 //-40009 : base64加密异常 //-40010 : base64解密异常 namespace Tencent { public class WXBizMsgCrypt { string m_sToken; string m_sEncodingAESKey; string m_sAppID; enum WXBizMsgCryptErrorCode { WXBizMsgCrypt_OK = 0, WXBizMsgCrypt_ValidateSignature_Error = -40001, WXBizMsgCrypt_ParseXml_Error = -40002, WXBizMsgCrypt_ComputeSignature_Error = -40003, WXBizMsgCrypt_IllegalAesKey = -40004, WXBizMsgCrypt_ValidateAppid_Error = -40005, WXBizMsgCrypt_EncryptAES_Error = -40006, WXBizMsgCrypt_DecryptAES_Error = -40007, WXBizMsgCrypt_IllegalBuffer = -40008, WXBizMsgCrypt_EncodeBase64_Error = -40009, WXBizMsgCrypt_DecodeBase64_Error = -40010 }; //构造函数 // @param sToken: 公众平台上,开发者设置的Token // @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey // @param sAppID: 公众帐号的appid public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sAppID) { m_sToken = sToken; m_sAppID = sAppID; m_sEncodingAESKey = sEncodingAESKey; } // 检验消息的真实性,并且获取解密后的明文 // @param sMsgSignature: 签名串,对应URL参数的msg_signature // @param sTimeStamp: 时间戳,对应URL参数的timestamp // @param sNonce: 随机串,对应URL参数的nonce // @param sPostData: 密文,对应POST请求的数据 // @param sMsg: 解密后的原文,当return返回0时有效 // @return: 成功0,失败返回对应的错误码 public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg) { if (m_sEncodingAESKey.Length!=43) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; } XmlDocument doc = new XmlDocument(); XmlNode root; string sEncryptMsg; try { doc.LoadXml(sPostData); root = doc.FirstChild; sEncryptMsg = root["Encrypt"].InnerText; } catch (Exception) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error; } //verify signature int ret = 0; ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature); if (ret != 0) return ret; //decrypt string cpid = ""; try { sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid); } catch (FormatException) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error; } catch (Exception) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error; } if (cpid != m_sAppID) return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateAppid_Error; return 0; } //将企业号回复用户的消息加密打包 // @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 // @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp // @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce // @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, // 当return返回0时有效 // return:成功0,失败返回对应的错误码 public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg) { if (m_sEncodingAESKey.Length!=43) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; } string raw = ""; try { raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sAppID); } catch (Exception) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error; } string MsgSigature = ""; int ret = 0; ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature); if (0 != ret) return ret; sEncryptMsg = ""; string EncryptLabelHead = "<Encrypt><![CDATA["; string EncryptLabelTail = "]]></Encrypt>"; string MsgSigLabelHead = "<MsgSignature><![CDATA["; string MsgSigLabelTail = "]]></MsgSignature>"; string TimeStampLabelHead = "<TimeStamp><![CDATA["; string TimeStampLabelTail = "]]></TimeStamp>"; string NonceLabelHead = "<Nonce><![CDATA["; string NonceLabelTail = "]]></Nonce>"; sEncryptMsg = sEncryptMsg + "<xml>" + EncryptLabelHead + raw + EncryptLabelTail; sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail; sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail; sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail; sEncryptMsg += "</xml>"; return 0; } public class DictionarySort : System.Collections.IComparer { public int Compare(object oLeft, object oRight) { string sLeft = oLeft as string; string sRight = oRight as string; int iLeftLength = sLeft.Length; int iRightLength = sRight.Length; int index = 0; while (index < iLeftLength && index < iRightLength) { if (sLeft[index] < sRight[index]) return -1; else if (sLeft[index] > sRight[index]) return 1; else index++; } return iLeftLength - iRightLength; } } //Verify Signature private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture) { string hash = ""; int ret = 0; ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash); if (ret != 0) return ret; //System.Console.WriteLine(hash); if (hash == sSigture) return 0; else { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error; } } public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature) { ArrayList AL = new ArrayList(); AL.Add(sToken); AL.Add(sTimeStamp); AL.Add(sNonce); AL.Add(sMsgEncrypt); AL.Sort(new DictionarySort()); string raw = ""; for (int i = 0; i < AL.Count; ++i) { raw += AL[i]; } SHA1 sha; ASCIIEncoding enc; string hash = ""; try { sha = new SHA1CryptoServiceProvider(); enc = new ASCIIEncoding(); byte[] dataToHash = enc.GetBytes(raw); byte[] dataHashed = sha.ComputeHash(dataToHash); hash = BitConverter.ToString(dataHashed).Replace("-", ""); hash = hash.ToLower(); } catch (Exception) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error; } sMsgSignature = hash; return 0; } } } ApiTools.Core/Utils/WxmpUtils/WxmpOptions.cs
@@ -18,6 +18,24 @@ } public List<WxmpOptionsItem> Items { get; set; } /// <summary> /// 订阅消息 /// </summary> public WxmpOptionsSubscribMessage SubscribMessage { get; set; } } public class WxmpOptionsSubscribMessage { /// <summary> /// 令牌 /// </summary> public string Token { get; set; } /// <summary> /// 消息加密密钥 /// </summary> public string EncodingAESKey { get; set; } } public class WxmpOptionsItem ApiTools.Core/Utils/WxmpUtils/WxmpUtils.cs
@@ -176,5 +176,7 @@ if (callback == null || callback.ErrorCode != 0) throw Oops.Oh(EnumErrorCodeType.s510, $"发送订阅消息失败:{callback.ErrorMessage},请联系管理员"); } } } ApiTools.Database.Migrations/Migrations/20251201101428_CreateWxmpSubscribMessageLog.Designer.cs
New file @@ -0,0 +1,1054 @@ // <auto-generated /> using System; using ApiTools.EntityFramework.Core; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable namespace ApiTools.Database.Migrations.Migrations { [DbContext(typeof(DefaultDbContext))] [Migration("20251201101428_CreateWxmpSubscribMessageLog")] partial class CreateWxmpSubscribMessageLog { /// <inheritdoc /> protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("ApiTools.Core.Channel", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<bool>("IsDisabled") .HasColumnType("bit"); b.Property<string>("Name") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("Channel"); }); modelBuilder.Entity("ApiTools.Core.ChannelWallet", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<int>("Access") .HasColumnType("int"); b.Property<decimal>("Balance") .HasColumnType("decimal(18,2)"); b.Property<string>("Bank") .HasColumnType("nvarchar(max)"); b.Property<string>("BankBranch") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("ChannelId") .HasColumnType("uniqueidentifier"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("ErrorCode") .HasColumnType("nvarchar(max)"); b.Property<string>("FailReason") .HasColumnType("nvarchar(max)"); b.Property<string>("Identity") .HasColumnType("nvarchar(max)"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("Name") .HasColumnType("nvarchar(max)"); b.Property<string>("OutWalletId") .HasColumnType("nvarchar(max)"); b.Property<int>("SignStatus") .HasColumnType("int"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("ChannelId"); b.ToTable("ChannelWallet"); }); modelBuilder.Entity("ApiTools.Core.ChannelWalletTransaction", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<decimal>("AfterBalance") .HasColumnType("decimal(18,2)"); b.Property<decimal>("Amount") .HasColumnType("decimal(18,2)"); b.Property<decimal>("Balance") .HasColumnType("decimal(18,2)"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<string>("ConcurrencyLock") .HasColumnType("nvarchar(450)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Currency") .HasColumnType("nvarchar(max)"); b.Property<string>("EreceiptDownloadOssUrl") .HasColumnType("nvarchar(max)"); b.Property<string>("EreceiptDownloadUrl") .HasColumnType("nvarchar(max)"); b.Property<string>("EreceiptErrorMessage") .HasColumnType("nvarchar(max)"); b.Property<string>("EreceiptFileId") .HasColumnType("nvarchar(max)"); b.Property<int?>("EreceiptStatus") .HasColumnType("int"); b.Property<string>("ErrorCode") .HasColumnType("nvarchar(max)"); b.Property<string>("FailReason") .HasColumnType("nvarchar(max)"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<DateTime?>("OperatorTime") .HasColumnType("datetime2"); b.Property<decimal?>("OrderFee") .HasColumnType("decimal(18,2)"); b.Property<string>("OutCode") .HasColumnType("nvarchar(max)"); b.Property<string>("OutOperatorId") .HasColumnType("nvarchar(max)"); b.Property<string>("OutReceiveId") .HasColumnType("nvarchar(max)"); b.Property<string>("PayerAccount") .HasColumnType("nvarchar(max)"); b.Property<string>("PayerBank") .HasColumnType("nvarchar(max)"); b.Property<string>("PayerBankBranch") .HasColumnType("nvarchar(max)"); b.Property<string>("PayerName") .HasColumnType("nvarchar(max)"); b.Property<string>("Purpose") .HasColumnType("nvarchar(max)"); b.Property<string>("ReceiveAccount") .HasColumnType("nvarchar(max)"); b.Property<string>("ReceiveBank") .HasColumnType("nvarchar(max)"); b.Property<string>("ReceiveBankBranch") .HasColumnType("nvarchar(max)"); b.Property<string>("ReceiveIdentity") .HasColumnType("nvarchar(max)"); b.Property<string>("ReceiveName") .HasColumnType("nvarchar(max)"); b.Property<string>("Remark") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTime?>("TransDate") .HasColumnType("datetime2"); b.Property<int>("TransactionStatus") .HasColumnType("int"); b.Property<int>("Type") .HasColumnType("int"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.Property<Guid>("WalletId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("ConcurrencyLock") .IsUnique() .HasFilter("[ConcurrencyLock] IS NOT NULL"); b.HasIndex("WalletId"); b.ToTable("ChannelWalletTransaction"); }); modelBuilder.Entity("ApiTools.Core.ChannelWalletTransactionPingAnPay", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("AccountDate") .HasColumnType("nvarchar(max)"); b.Property<string>("BackRem") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("CstInnerFlowNo") .HasColumnType("nvarchar(max)"); b.Property<string>("Fee") .HasColumnType("nvarchar(max)"); b.Property<string>("FreezeNo") .HasColumnType("nvarchar(max)"); b.Property<string>("FrontLogNo") .HasColumnType("nvarchar(max)"); b.Property<string>("HostErrorCode") .HasColumnType("nvarchar(max)"); b.Property<string>("HostFlowNo") .HasColumnType("nvarchar(max)"); b.Property<string>("IsBack") .HasColumnType("nvarchar(max)"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("ProxyPayAcc") .HasColumnType("nvarchar(max)"); b.Property<string>("ProxyPayBankName") .HasColumnType("nvarchar(max)"); b.Property<string>("ProxyPayName") .HasColumnType("nvarchar(max)"); b.Property<string>("RemoveStopFailReason") .HasColumnType("nvarchar(max)"); b.Property<string>("RemoveStopStt") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("StopFailReason") .HasColumnType("nvarchar(max)"); b.Property<string>("StopStt") .HasColumnType("nvarchar(max)"); b.Property<string>("SubmitTime") .HasColumnType("nvarchar(max)"); b.Property<string>("SysFlag") .HasColumnType("nvarchar(max)"); b.Property<string>("ThirdVoucher") .HasColumnType("nvarchar(max)"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<string>("TransBsn") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Yhcljg") .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.ToTable("ChannelWalletTransactionPingAnPay"); }); modelBuilder.Entity("ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Data") .HasColumnType("nvarchar(max)"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("OpenId") .HasColumnType("nvarchar(max)"); b.Property<string>("Page") .HasColumnType("nvarchar(max)"); b.Property<string>("PopupScene") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("SubscribeStatusString") .HasColumnType("nvarchar(max)"); b.Property<string>("TemplateId") .HasColumnType("nvarchar(max)"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("WxmpSubscribMessageLog"); }); modelBuilder.Entity("ApiTools.Core.Resource", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("ActionName") .HasColumnType("nvarchar(max)"); b.Property<string>("ActionSummary") .HasColumnType("nvarchar(max)"); b.Property<bool>("AllowAnonymous") .HasColumnType("bit"); b.Property<string>("ApplicationName") .HasColumnType("nvarchar(max)"); b.Property<string>("Code") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property<string>("ControllerName") .HasColumnType("nvarchar(max)"); b.Property<string>("ControllerSummary") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<bool>("CustomResponse") .HasColumnType("bit"); b.Property<string>("DynamicAssemblyName") .HasColumnType("nvarchar(max)"); b.Property<bool>("FileUpload") .HasColumnType("bit"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<bool>("IsExpired") .HasColumnType("bit"); b.Property<bool>("IsFromForm") .HasColumnType("bit"); b.Property<int>("Method") .HasColumnType("int"); b.Property<string>("Name") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property<string>("RequestTypeFullName") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property<string>("RequestTypeName") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property<string>("ResponseTypeFullName") .HasColumnType("nvarchar(max)"); b.Property<string>("ResponseTypeName") .HasColumnType("nvarchar(max)"); b.Property<string>("Route") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property<string>("RouteArea") .HasColumnType("nvarchar(max)"); b.Property<string>("ServiceName") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("Resource"); }); modelBuilder.Entity("ApiTools.Core.ScheduleJobDetail", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("AssemblyName") .HasColumnType("nvarchar(max)"); b.Property<bool>("Concurrent") .HasColumnType("bit"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Description") .HasColumnType("nvarchar(max)"); b.Property<string>("GroupName") .HasColumnType("nvarchar(max)"); b.Property<bool>("IncludeAnnotations") .HasColumnType("bit"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("JobId") .HasColumnType("nvarchar(max)"); b.Property<string>("JobType") .HasColumnType("nvarchar(max)"); b.Property<string>("Properties") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("ScheduleJobDetail"); }); modelBuilder.Entity("ApiTools.Core.ScheduleJobTrigger", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("Args") .HasColumnType("nvarchar(max)"); b.Property<string>("AssemblyName") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Description") .HasColumnType("nvarchar(max)"); b.Property<long>("ElapsedTime") .HasColumnType("bigint"); b.Property<DateTime?>("EndTime") .HasColumnType("datetime2"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("JobId") .HasColumnType("nvarchar(max)"); b.Property<DateTime?>("LastRunTime") .HasColumnType("datetime2"); b.Property<long>("MaxNumberOfErrors") .HasColumnType("bigint"); b.Property<long>("MaxNumberOfRuns") .HasColumnType("bigint"); b.Property<DateTime?>("NextRunTime") .HasColumnType("datetime2"); b.Property<long>("NumRetries") .HasColumnType("bigint"); b.Property<long>("NumberOfErrors") .HasColumnType("bigint"); b.Property<long>("NumberOfRuns") .HasColumnType("bigint"); b.Property<bool>("ResetOnlyOnce") .HasColumnType("bit"); b.Property<string>("Result") .HasColumnType("nvarchar(max)"); b.Property<int>("RetryTimeout") .HasColumnType("int"); b.Property<bool>("RunOnStart") .HasColumnType("bit"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<bool>("StartNow") .HasColumnType("bit"); b.Property<DateTime?>("StartTime") .HasColumnType("datetime2"); b.Property<long>("Status") .HasColumnType("bigint"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<string>("TriggerId") .HasColumnType("nvarchar(max)"); b.Property<string>("TriggerType") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("ScheduleJobTrigger"); }); modelBuilder.Entity("ApiTools.Core.SmsLog", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<int>("Access") .HasColumnType("int"); b.Property<Guid?>("ChannelCreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<Guid?>("ChannelId") .HasColumnType("uniqueidentifier"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<DateTime?>("Expiry") .HasColumnType("datetime2"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<bool>("IsUsed") .HasColumnType("bit"); b.Property<string>("Message") .HasColumnType("nvarchar(max)"); b.Property<string>("PhoneNumber") .IsRequired() .HasMaxLength(11) .HasColumnType("nvarchar(11)"); b.Property<string>("RequestId") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<int>("Status") .HasColumnType("int"); b.Property<string>("TemplateCode") .IsRequired() .HasMaxLength(128) .HasColumnType("nvarchar(128)"); b.Property<string>("TemplateParam") .HasColumnType("nvarchar(max)"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("ChannelId"); b.ToTable("SmsLog"); }); modelBuilder.Entity("ApiTools.Core.SmsSetting", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<Guid?>("ChannelId") .HasColumnType("uniqueidentifier"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<int>("DailyMaxCount") .HasColumnType("int"); b.Property<int>("HourlyMaxCount") .HasColumnType("int"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<bool>("IsDisabled") .HasColumnType("bit"); b.Property<int>("MinutelyMaxCount") .HasColumnType("int"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.Property<bool>("WithoutParams") .HasColumnType("bit"); b.HasKey("Id"); b.HasIndex("ChannelId"); b.ToTable("SmsSetting"); }); modelBuilder.Entity("ApiTools.Core.SmsSettingAccess", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<int>("Access") .HasColumnType("int"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<bool>("IsDisabled") .HasColumnType("bit"); b.Property<Guid>("SettingId") .HasColumnType("uniqueidentifier"); b.Property<string>("SignName") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("SettingId"); b.ToTable("SmsSettingAccess"); }); modelBuilder.Entity("ApiTools.Core.User", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("Avatar") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("ChannelId") .HasColumnType("uniqueidentifier"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<bool>("IsCheckPhoneNumber") .HasColumnType("bit"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<int>("Level") .HasColumnType("int"); b.Property<string>("Name") .HasMaxLength(32) .HasColumnType("nvarchar(32)"); b.Property<string>("Password") .HasColumnType("nvarchar(max)"); b.Property<string>("PhoneNumber") .HasMaxLength(11) .HasColumnType("nvarchar(11)"); b.Property<string>("Remark") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<int>("Status") .HasColumnType("int"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<int>("Type") .HasColumnType("int"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("UserName") .IsRequired() .HasMaxLength(32) .HasColumnType("nvarchar(32)"); b.HasKey("Id"); b.HasIndex("ChannelId"); b.ToTable("User"); b.HasData( new { Id = new Guid("11111111-1111-1111-1111-111111111111"), CreatedTime = new DateTimeOffset(new DateTime(2000, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 8, 0, 0, 0)), IsCheckPhoneNumber = false, IsDeleted = false, Level = 999, Name = "管理员", Password = "iEYggKrMhQ3ASUGLobra1w==:fn/DsMJUbD9FGpvBvR3moMpMPptdxzZlourPVhU479I=", Sort = 0, Status = 10, Type = 100, UserName = "system" }); }); modelBuilder.Entity("ApiTools.Core.ChannelWallet", b => { b.HasOne("ApiTools.Core.Channel", "Channel") .WithMany() .HasForeignKey("ChannelId"); b.Navigation("Channel"); }); modelBuilder.Entity("ApiTools.Core.ChannelWalletTransaction", b => { b.HasOne("ApiTools.Core.ChannelWallet", "Wallet") .WithMany() .HasForeignKey("WalletId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Wallet"); }); modelBuilder.Entity("ApiTools.Core.ChannelWalletTransactionPingAnPay", b => { b.HasOne("ApiTools.Core.ChannelWalletTransaction", "Transaction") .WithOne("PingAnPay") .HasForeignKey("ApiTools.Core.ChannelWalletTransactionPingAnPay", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Transaction"); }); modelBuilder.Entity("ApiTools.Core.SmsLog", b => { b.HasOne("ApiTools.Core.Channel", "Channel") .WithMany() .HasForeignKey("ChannelId"); b.Navigation("Channel"); }); modelBuilder.Entity("ApiTools.Core.SmsSetting", b => { b.HasOne("ApiTools.Core.Channel", "Channel") .WithMany() .HasForeignKey("ChannelId"); b.Navigation("Channel"); }); modelBuilder.Entity("ApiTools.Core.SmsSettingAccess", b => { b.HasOne("ApiTools.Core.SmsSetting", "Setting") .WithMany("Accesses") .HasForeignKey("SettingId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Setting"); }); modelBuilder.Entity("ApiTools.Core.User", b => { b.HasOne("ApiTools.Core.Channel", "Channel") .WithMany() .HasForeignKey("ChannelId"); b.Navigation("Channel"); }); modelBuilder.Entity("ApiTools.Core.ChannelWalletTransaction", b => { b.Navigation("PingAnPay"); }); modelBuilder.Entity("ApiTools.Core.SmsSetting", b => { b.Navigation("Accesses"); }); #pragma warning restore 612, 618 } } } ApiTools.Database.Migrations/Migrations/20251201101428_CreateWxmpSubscribMessageLog.cs
New file @@ -0,0 +1,48 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace ApiTools.Database.Migrations.Migrations { /// <inheritdoc /> public partial class CreateWxmpSubscribMessageLog : Migration { /// <inheritdoc /> protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "WxmpSubscribMessageLog", columns: table => new { Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Code = table.Column<string>(type: "nvarchar(max)", nullable: true), OpenId = table.Column<string>(type: "nvarchar(max)", nullable: true), PopupScene = table.Column<string>(type: "nvarchar(max)", nullable: true), SubscribeStatusString = table.Column<string>(type: "nvarchar(max)", nullable: true), TemplateId = table.Column<string>(type: "nvarchar(max)", nullable: true), Page = table.Column<string>(type: "nvarchar(max)", nullable: true), Data = table.Column<string>(type: "nvarchar(max)", nullable: true), Sort = table.Column<int>(type: "int", nullable: false), TraceId = table.Column<string>(type: "nvarchar(max)", nullable: true), CreatedTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false), CreatedUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), CreatedChannelId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), UpdatedTime = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), UpdatedUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), IsDeleted = table.Column<bool>(type: "bit", nullable: false) }, constraints: table => { table.PrimaryKey("PK_WxmpSubscribMessageLog", x => x.Id); }); } /// <inheritdoc /> protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "WxmpSubscribMessageLog"); } } } ApiTools.Database.Migrations/Migrations/DefaultDbContextModelSnapshot.cs
@@ -377,6 +377,62 @@ b.ToTable("ChannelWalletTransactionPingAnPay"); }); modelBuilder.Entity("ApiTools.Core.Entities.LogRecords.WxmpSubscribMessageLog", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property<string>("Code") .HasColumnType("nvarchar(max)"); b.Property<Guid?>("CreatedChannelId") .HasColumnType("uniqueidentifier"); b.Property<DateTimeOffset>("CreatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("CreatedUserId") .HasColumnType("uniqueidentifier"); b.Property<string>("Data") .HasColumnType("nvarchar(max)"); b.Property<bool>("IsDeleted") .HasColumnType("bit"); b.Property<string>("OpenId") .HasColumnType("nvarchar(max)"); b.Property<string>("Page") .HasColumnType("nvarchar(max)"); b.Property<string>("PopupScene") .HasColumnType("nvarchar(max)"); b.Property<int>("Sort") .HasColumnType("int"); b.Property<string>("SubscribeStatusString") .HasColumnType("nvarchar(max)"); b.Property<string>("TemplateId") .HasColumnType("nvarchar(max)"); b.Property<string>("TraceId") .HasColumnType("nvarchar(max)"); b.Property<DateTimeOffset?>("UpdatedTime") .HasColumnType("datetimeoffset"); b.Property<Guid?>("UpdatedUserId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.ToTable("WxmpSubscribMessageLog"); }); modelBuilder.Entity("ApiTools.Core.Resource", b => { b.Property<Guid>("Id") ApiTools.Database.Migrations/REDEME.MD
@@ -1,7 +1,7 @@ -------------------------------主数据库--------------------------------------- 新增迁移文件 dotnet ef migrations add UpdateChannelWallet1119 -s "../ApiTools.Web.Entry" -c DefaultDbContext dotnet ef migrations add CreateWxmpSubscribMessageLog -s "../ApiTools.Web.Entry" -c DefaultDbContext 删除迁移文件 dotnet ef migrations remove -s "../ApiTools.Web.Entry" -c DefaultDbContext ApiTools.Web.Entry/Controllers/WxmpController.cs
New file @@ -0,0 +1,72 @@ using Aop.Api.Domain; using ApiTools.Core; using Furion.DataEncryption; using Furion.DynamicApiController; using Furion.FriendlyException; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Org.BouncyCastle.Ocsp; using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace ApiTools.Web.Entry.Controllers { [Route("api/common/wxmp")] public class WxmpController( WxmpUtils utils, IOptions<WxmpOptions> options, IMediator mediator ) : ControllerBase { private readonly WxmpUtils utils = utils; private readonly IOptions<WxmpOptions> options = options; private readonly IMediator mediator = mediator; [HttpGet("subscribMessageNotify/{code}")] [AllowAnonymous] [NonUnify] public IActionResult SubscribMessageNotify([FromRoute] string code, [FromQuery] WxmpSubscribMessageNotifyRequestQuery query) { var @params = new[] { options.Value.SubscribMessage.Token, query.timestamp, query.nonce } .OrderBy(p => p) .ToArray(); var text = string.Concat(@params); if (SHA1Encryption.Compare(text, query.signature, true)) { return Content(query.echostr); } else { return Unauthorized("验签失败"); } } [HttpPost("subscribMessageNotify/{code}")] [AllowAnonymous] [NonUnify] public async Task<IActionResult> SubscribMessageNotify([FromRoute] string code, [FromQuery] WxmpSubscribMessageNotifyRequestQuery query, [FromBody] WxmpSubscribMessageNotifyRequestBody body) { var appId = options.Value.Items.FirstOrDefault(it => it.Code == code).AppId; Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(options.Value.SubscribMessage.Token, options.Value.SubscribMessage.EncodingAESKey, appId); var data = $"<xml><ToUserName><![CDATA[{body.ToUserName}]]></ToUserName><Encrypt><![CDATA[{body.Encrypt}]]></Encrypt></xml>"; var content = ""; var error = wxcpt.DecryptMsg(query.msg_signature, query.timestamp, query.nonce, data, ref content); if (error != 0) return Unauthorized("验签失败"); var command = content.JsonTo<WxmpSubscribMessageNotifyCommand>(); command.Code = code; command.OpenId = query.openid; await mediator.Send(command); return Content("success"); } } } ApiTools.Web.Entry/appsettings.json
@@ -53,12 +53,22 @@ "EnvVersion": "trial" }, { "Code": "Supplier", "AppId": "wxc47d6f255e7d0566", "AppSecret": "9e02d66cf005fa2f4aefb2e4dc1fced5", "EnvVersion": "trial" }, { "Code": "Public", "AppId": "wxf940ff1d35a98493", "AppSecret": "9a132eda735bc925200b0e215cffe20a", "EnvVersion": "trial" } ] ], "SubscribMessage": { "Token": "8Uu6CZ9KM2CAr3Q3O0YdWUYPfcXFhgMK", "EncodingAESKey": "tbBkUB7nCgZlfton3aKMlfzHSm7QdWgnpKFibl6sjn7" } }, "Task": { "SettlementTime": "T0"