312ebed2d86858e4fb57ec09679244e9b806b57f..6396dac27ca99e84a2e3c772fb079bceddf67ff8
2025-12-01 sunpengfei
feat:开发
6396da 对比 | 目录
2025-12-01 sunpengfei
feat:开发
38998c 对比 | 目录
2025-12-01 sunpengfei
feat:开发
c87d61 对比 | 目录
2025-12-01 sunpengfei
feat:开发
37df52 对比 | 目录
2025-12-01 sunpengfei
feat:开发
2b0556 对比 | 目录
2025-12-01 sunpengfei
feat:开发
32d421 对比 | 目录
9个文件已修改
9个文件已添加
2158 ■■■■■ 已修改文件
ApiTools.Application/WxUtils/Commands/WxmpSubscribMessageCommandHandler.cs 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/ApiTools.Core.csproj 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/ApiTools.Core.xml 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Entities/LogRecords/WxmpSubscribMessageLog.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Models/WxmpUtils/Commands/WxmpSubscribMessageNotifyCommand.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Models/WxmpUtils/Models/WxmpSubscribMessageNotifyRequestQuery.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/Crypto/Cryptography.cs 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/Crypto/Readme.txt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/Crypto/Sample.cs 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/Crypto/WXBizMsgCrypt.cs 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/WxmpOptions.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Core/Utils/WxmpUtils/WxmpUtils.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Database.Migrations/Migrations/20251201101428_CreateWxmpSubscribMessageLog.Designer.cs 1054 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Database.Migrations/Migrations/20251201101428_CreateWxmpSubscribMessageLog.cs 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Database.Migrations/Migrations/DefaultDbContextModelSnapshot.cs 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Database.Migrations/REDEME.MD 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Web.Entry/Controllers/WxmpController.cs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ApiTools.Web.Entry/appsettings.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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&timestamp=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"