Lord.Service 8.2.0

dotnet add package Lord.Service --version 8.2.0
                    
NuGet\Install-Package Lord.Service -Version 8.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lord.Service" Version="8.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lord.Service" Version="8.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Lord.Service" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lord.Service --version 8.2.0
                    
#r "nuget: Lord.Service, 8.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lord.Service@8.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lord.Service&version=8.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Lord.Service&version=8.2.0
                    
Install as a Cake Tool

LordService 完整使用文档

企业级推送与消息队列集成库

支持钉钉/企业微信/飞书消息推送、RabbitMQ/RocketMQ 发布订阅与死信队列、Redis 分布式缓存与锁、AES-GCM 认证加密、多日志框架。

版本:v7.10.9 | 目标框架:net6.0 / net8.0 / net9.0 / net10.0


目录


安装

dotnet add package Lord.Service

完整 appsettings.json 配置参考

以下是所有模块的完整配置示例,实际使用时按需选取:

{
  "RabbitMQ": {
    "HostName": "localhost",
    "VirtualHost": "/",
    "UserName": "guest",
    "Password": "guest",
    "Prefix": "MyApp",
    "ServiceName": "订单服务"
  },

  "RocketMQ": {
    "NameServerAddress": "localhost:9876",
    "Group": "LordService",
    "Prefix": "MyApp",
    "ServiceName": "订单服务",
    "RequestTimeoutSeconds": 30,
    "ConsumeBatchSize": 1,
    "RetryCount": 5
  },

  "Redis": {
    "ConnectionString": "localhost:6379",
    "Prefix": "MyApp",
    "Database": 0,
    "ConnectTimeout": 5,
    "SyncTimeout": 5,
    "AllowAdmin": false,
    "Ssl": false,
    "Password": "",
    "ClientName": "LordService"
  },

  "Encryption": {
    "PublicKeyOrKey": "你的AES密钥Base64字符串",
    "PrivateKeyOrIV": "备用IV的Base64字符串",
    "EncryptType": "Aes"
  },

  "DingTalk": {
    "Alias": "系统通知群",
    "Url": "https://oapi.dingtalk.com",
    "Token": "your_dingtalk_access_token",
    "Secret": "your_dingtalk_secret"
  },

  "WeChatPush": {
    "Alias": "运维群",
    "Url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send",
    "Token": "your_wechat_key"
  },

  "LarkPush": {
    "Alias": "开发群",
    "Url": "https://open.feishu.cn/open-apis/bot/v2/hook",
    "Token": "your_lark_token",
    "Secret": "your_lark_secret"
  },

  "DingApp": {
    "AppKey": "your_ding_app_key",
    "AppSecret": "your_ding_app_secret",
    "AgentId": "your_agent_id"
  },

  "WeChatOfficial": {
    "AppId": "your_app_id",
    "AppSecret": "your_app_secret",
    "Token": "your_token",
    "EncodingAesKey": null
  },

  "WeChatMiniProgram": {
    "AppId": "your_mini_program_appid",
    "AppSecret": "your_mini_program_secret",
    "BaseUrl": "https://api.weixin.qq.com"
  },

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

多群组配置

钉钉/企业微信/飞书支持多群组,使用数组配置:

{
  "DingTalk": [
    {
      "Alias": "生产告警群",
      "Url": "https://oapi.dingtalk.com",
      "Token": "prod_alert_token",
      "Secret": "prod_alert_secret"
    },
    {
      "Alias": "运维群",
      "Url": "https://oapi.dingtalk.com",
      "Token": "ops_token",
      "Secret": "ops_secret"
    }
  ]
}

RabbitMQ 多连接池

不同业务可使用不同的 RabbitMQ 连接池:

{
  "RabbitMQ": {
    "HostName": "localhost",
    "VirtualHost": "/",
    "UserName": "guest",
    "Password": "guest",
    "Prefix": "MyApp"
  },
  "OrderMQ": {
    "HostName": "mq-order.internal",
    "VirtualHost": "/order",
    "UserName": "order_user",
    "Password": "order_pass",
    "Prefix": "OrderApp"
  },
  "OrderQueue": {
    "QueueName": "Order_Process_Queue",
    "ExchangeName": "Order_Process_Topic",
    "RouteKey": "Service.order.process",
    "MQPool": "OrderMQ"
  }
}

快速上手

最简配置(内存缓存 + NLog + AES)

// Program.cs
builder.Services.AddLordService(lord => lord.UseDefaults());

UseDefaults() 等价于:

builder.Services.AddLordService(lord => lord
    .UseHttp()                                          // RestSharp,超时 1 分钟
    .UseLogging(log => log.UseNLog())                  // NLog 日志
    .UseCache(cache => cache.UseMemoryCache())         // 内存缓存
    .UseEncryption(enc => enc.UseAES(a => a.FromConfiguration())) // AES-GCM
);

生产推荐配置

builder.Services.AddLordService(lord => lord
    .UseHttp(h => h.UseNetHttp().WithTimeout(TimeSpan.FromSeconds(30)))
    .UseLogging(log => log.UseNLog())
    .UseCache(cache => cache.UseRedis(r => r.FromConfiguration("Redis")))
    .UseEncryption(enc => enc.UseAES(a => a.FromConfiguration("Encryption")))
    .UseMQ(mq => mq
        .UseDeadLetter()
        .UseRabbitMQ(r => r.FromConfiguration("RabbitMQ")))
    .UsePush(push => push
        .AddDingTalk(d => d.FromConfiguration("DingTalk"))
        .AddWeChat(w => w.FromConfiguration("WeChatPush"))
        .AddLark(l => l.FromConfiguration("LarkPush")))
);

模块详解

1. 缓存(Redis / Memory)

注册方式
// 方式1:从配置加载 Redis
.UseCache(cache => cache.UseRedis(r => r.FromConfiguration("Redis")))

// 方式2:直接传连接字符串
.UseCache(cache => cache.UseRedis("localhost:6379"))

// 方式3:使用内存缓存
.UseCache(cache => cache.UseMemoryCache())

// 方式4:自定义缓存实现
.UseCache(cache => cache.UseCustom<MyRedisCache>(c => c.FromConfiguration("Redis").AsSingleton()))

// 方式5:直接传实例
.UseCache(cache => cache.UseCustom(new MyRedisCache()))
Redis 配置项说明
配置项 类型 默认值 说明
ConnectionString string localhost:6379 Redis 连接字符串
Prefix string "" Key 前缀,用于多系统共用 Redis 隔离
Database int 0 Redis 数据库编号
ConnectTimeout int 5 连接超时(秒)
SyncTimeout int 5 同步操作超时(秒)
AllowAdmin bool false 是否允许管理操作(如 FlushDatabase)
Ssl bool false 是否启用 SSL
Password string? null Redis 密码
ClientName string? LordService 客户端名称
业务使用
public class UserService
{
    private readonly ICache _cache;

    public UserService(ICache cache) => _cache = cache;

    // 基本读写
    public async Task<User?> GetUserAsync(int userId)
    {
        return await _cache.GetItemAsync<User>($"user:{userId}");
    }

    public async Task SetUserAsync(int userId, User user)
    {
        await _cache.SetItemAsync($"user:{userId}", user, TimeSpan.FromMinutes(30));
    }

    // 缓存穿透保护:AddOrGetCacheItem 保证同 key 只回源一次
    public async Task<User> GetOrLoadUserAsync(int userId)
    {
        return await _cache.AddOrGetCacheItemAsync(
            $"user:{userId}",
            async () => await LoadFromDbAsync(userId), // 只在缓存缺失时执行
            TimeSpan.FromMinutes(30),
            isSlidingExpiration: true); // 命中时刷新 TTL(访问即续期)—— 仅 AddOrGetCacheItem* 支持此语义
    }

    // 批量操作(自动分批,每批 500 条)
    public async Task SetUsersBatchAsync(Dictionary<string, User> users)
    {
        await _cache.SetBatchAsync(users, TimeSpan.FromHours(1));
    }

    public async Task<Dictionary<string, User?>> GetUsersBatchAsync(IEnumerable<string> keys)
    {
        return await _cache.GetBatchAsync<User>(keys);
    }

    // 分布式锁
    public async Task DoWithLockAsync(string resourceKey, Func<Task> action)
    {
        await using var handle = await _cache.DistributedLock.AcquireAsync(
            $"lock:{resourceKey}",
            TimeSpan.FromMinutes(1));

        if (handle.IsAcquired)
        {
            await action();
        }
    }

    // 自动续期锁(看门狗模式,适合长时间任务)
    public async Task DoWithAutoRenewalLockAsync(string resourceKey, Func<Task> action)
    {
        await using var handle = await _cache.DistributedLock.AcquireWithRenewalAsync(
            $"lock:{resourceKey}",
            TimeSpan.FromSeconds(30)); // 锁 30 秒,自动续期

        if (handle.IsAcquired)
        {
            await action(); // 即使任务超过 30 秒,锁也会自动续期
        }
    }
}

安全提示:同步 AddOrGetCacheItem 的回源等待有 30 秒超时保护,避免 cachePopulate 卡住时线程池饥饿。生产环境推荐使用异步 AddOrGetCacheItemAsync

⚠️ isSlidingExpiration 的真实语义(两个实现不一致)

Redis 服务端没有"读取时自动滑动过期"的能力,因此该参数只在部分方法上有意义:

方法 RedisCache(生产常用) MemoryCache
SetItem / SetItemAsync 忽略该参数,一律按绝对过期写入 生效:任何读取都续期
AddOrUpdateCacheItem / Async 忽略该参数 生效
AddOrGetCacheItem / Async 生效:命中时刷新 TTL(访问即续期) 生效
GetItem / GetItemAsync 不续期 续期(若写入时设了滑动)

规则:

  1. 需要"访问即续期"的会话/令牌类缓存,在 Redis 上必须走 AddOrGetCacheItem* 读取;写成 SetItem(key, val, ttl, isSlidingExpiration: true) + GetItem 不会续期,到点即过期。
  2. 该参数被忽略时不报错也不告警,属于静默降级——排查"缓存为什么按时失效"时优先核对是否用了 SetItem
  3. MemoryCache(开发/单机)切到 RedisCache(生产)时,滑动语义会退化为绝对过期,需要同步把读取路径改为 AddOrGetCacheItem*

2. 消息队列(RabbitMQ)

注册方式
.UseMQ(mq => mq
    .UseDeadLetter()  // 全局启用死信队列(所有队列自动生成 per-queue DLX)
    .UseRabbitMQ(r => r
        .FromConfiguration("RabbitMQ")           // 从配置加载
        // 或:.WithConnection("host", "/vhost", "user", "pass") // 直接配置
        .WithPrefix("MyApp")                      // 队列名前缀,隔离多系统
        .WithServiceName("订单服务"))              // 连接名称,便于运维识别
)
RabbitMQ 配置项说明
配置项 说明
HostName RabbitMQ 主机地址
VirtualHost 虚拟主机
UserName 用户名
Password 密码
Prefix 队列/交换机/路由键前缀
ServiceName 连接名称(便于 RabbitMQ 管理端识别)
PublisherDeclareQueue 发布方是否声明专属队列(默认 false)。详见下方发布方队列声明
发布方队列声明(ghost queue 迁移说明)

背景:8.0.0 及之前的版本中,IMQPublisher 发布消息时会在 Exchange 侧声明并绑定一个专属队列({topic}_{tag}_Queue)。该队列没有消费者——订阅方消费的是自己 consumerGroup 的队列——因此每条消息会被投递两次,且发布方队列中的消息无限堆积,形成"ghost queue"。

8.1.0 起的新行为(默认)

  • 发布方只声明 Exchange,不再声明/绑定专属队列
  • 配合 publisher confirm + mandatory: true,消息不可路由(如订阅方尚未启动、绑定不存在)时会显式抛出 PublishException,不会静默丢失。

何时需要设为 true(恢复旧行为)

  • 存量部署平滑迁移:旧版本已创建的 {topic}_{tag}_Queue 中尚有未消费的历史消息,设为 true 可保持声明参数一致,避免升级后与存量队列参数冲突(406 PRECONDITION_FAILED),待存量队列消费完毕并清理后再移除该配置;
  • 先发后订:业务上需要发布方队列在订阅方上线前暂存消息(注意:旧行为在消息不可路由时是静默堆积而非报错)。
"RabbitMQ": {
  "HostName": "localhost",
  "PublisherDeclareQueue": true
}

存量队列清理:确认各订阅方均正常消费后,在 RabbitMQ 管理端删除废弃的 {topic}_{tag}_Queue 队列及其绑定即可。

注意:本开关计划在 9.0 版本移除,届时发布方一律不声明专属队列。

IMQHub 简化 API(推荐)
// 定义消息(队列名自动从类型名推导)
public record OrderCreated(string OrderId, decimal Amount);

// 发布
await _hub.PublishAsync(new OrderCreated("ORD-001", 99.9m));

// 订阅
await _hub.SubscribeAsync<OrderCreated>(async (msg, sp) =>
{
    var logger = sp.GetRequiredService<ILogger<Program>>();
    logger.LogInformation("收到订单: {OrderId}, 金额: {Amount}", msg.OrderId, msg.Amount);
    return true; // true=Ack, false=Reject(触发重试或死信)
});

// 死信订阅
await _hub.SubscribeDeadLetterAsync<OrderCreated>(async (msg, sp) =>
{
    var logger = sp.GetRequiredService<ILogger<Program>>();
    logger.LogWarning("订单消息进入死信: {OrderId}", msg.OrderId);
    return true;
});
IMQHub 完整 API
// 发布
await hub.PublishAsync(message);                              // 类型推导
await hub.PublishAsync(message, cfg => cfg.Qos = 20);         // 自定义配置
await hub.PublishAsync("custom_name", message);               // 自定义队列名
await hub.PublishAsync("custom_name", message, cfg => { });   // 自定义名 + 配置
await hub.PublishAsync(rawJsonString);                        // 原始字符串

// 订阅(5 种重载)
await hub.SubscribeAsync<T>(msg => Task.FromResult(true));            // 最简
await hub.SubscribeAsync<T>(msg => true);                             // 同步
await hub.SubscribeAsync<T>(async (msg, sp) => true);                 // 注入 ServiceProvider
await hub.SubscribeAsync<T>(handler, cfg => { });                     // 自定义配置
await hub.SubscribeAsync<T>("name", handler);                        // 自定义队列名

// 死信订阅
await hub.SubscribeDeadLetterAsync<T>(async (msg, sp) => true);
await hub.SubscribeDeadLetterAsync<T>("name", handler, cfg => { });
MQ 拦截器
.UseMQ(mq => mq
    .AddFilter<MyQueueArgsFilter>()      // IMQQueueArgsFilter: 修改队列参数
    .AddFilter<MyDeadLetterFilter>()     // IMQDeadLetterFilter: 修改死信配置
    .AddFilter<MyCryptoFilter>()         // IMQCryptoFilter: 自定义加解密
    .AddFilter<MyJsonFilter>()           // IMQJsonFilter: 自定义序列化
    .AddFilter<MyExceptionFilter>()      // IMQExceptionFilter: 异常处理
    .UseRabbitMQ(r => r.FromConfiguration("RabbitMQ"))
)
消费端看门狗(RabbitMQ)

RabbitMQ 消费端内置看门狗机制:

  • 心跳检测:每 10 秒检查 consumer 存活状态
  • 自动恢复:consumer 关闭或 Channel 断开时自动重建
  • 指数退避:恢复失败时 1s→2s→4s→8s→16s→30s 退避重试
  • 低频持续:连续失败 20 次后进入低频模式(每 60 秒一次),永不永久停止
  • 超时保护:handler 执行超过 3 分钟自动 Reject 并取消后台任务

2.5 消息队列(RocketMQ)

说明:v7.11.0 新增 RocketMQ 基础支持,采用 NewLife.RocketMQ 客户端,适用于信创场景和国产化 MQ 需求。

注册方式
.UseMQ(mq => mq
    .UseRocketMQ(r => r
        .FromConfiguration("RocketMQ")           // 从配置加载
        // 或:.WithNameServer("127.0.0.1:9876")  // 直接配置 NameServer
        .WithGroup("LordService")                 // 默认生产/消费组
        .WithPrefix("MyApp")                      // Topic/Tag/ConsumerGroup 前缀
        .WithServiceName("订单服务"))              // 连接名称,便于运维识别
)
RocketMQ 配置项说明
配置项 说明
NameServerAddress NameServer 地址,如 127.0.0.1:9876
Group 默认生产/消费组名
Prefix Topic/Tag/ConsumerGroup/ProducerGroup 前缀
ServiceName 服务名称(便于识别)
RequestTimeoutSeconds 请求超时时间(秒)
ConsumeBatchSize 消费批量大小
RetryCount 最大重试次数
RocketMQ 与 RabbitMQ 字段映射
通用字段 RocketMQ 映射 说明
ExchangeName Topic 消息主题
RouteKey Tag 消息标签过滤
QueueName ConsumerGroup 消费者组
EnableDeadLetter EnableFailedMessage 失败消息能力
IMQHub 简化 API(推荐)
// 定义消息(Topic/Tag/ConsumerGroup 自动从类型名推导)
public record OrderCreated(string OrderId, decimal Amount);

// 发布(自动生成 Topic: OrderCreated_Topic, Tag: Service.ordercreated)
await _hub.PublishAsync(new OrderCreated("ORD-001", 99.9m));

// 订阅(自动生成 ConsumerGroup: OrderCreated_ConsumerGroup)
await _hub.SubscribeAsync<OrderCreated>(async (msg, sp) =>
{
    var logger = sp.GetRequiredService<ILogger<Program>>();
    logger.LogInformation("收到订单: {OrderId}, 金额: {Amount}", msg.OrderId, msg.Amount);
    return true; // true=消费成功, false=触发 RocketMQ 重试
});

// 失败消息订阅(通用接口,避免 RabbitMQ DLX 术语)
await _hub.SubscribeFailedAsync<OrderCreated>(async (msg, sp) =>
{
    logger.LogWarning("订单处理失败: {OrderId}", msg.OrderId);
    return true; // 确认已处理
});
失败消息处理

RocketMQ 使用 %DLQ%{ConsumerGroup} 约定 Topic 存储失败消息:

// 消费失败时返回 false,RocketMQ 自动重试
await _hub.SubscribeAsync<OrderCreated>(async msg =>
{
    try
    {
        // 业务处理
        return true; // 成功
    }
    catch
    {
        return false; // 失败,进入 RocketMQ 重试机制
    }
});

// 订阅失败消息
await _hub.SubscribeFailedAsync<OrderCreated>(async msg =>
{
    // 处理进入 %DLQ%{ConsumerGroup} 的失败消息
    return true;
});
高级功能

RocketMQ 支持以下高级功能(通过 IRocketMQAdvancedPushIRocketMQAdvancedReceive 接口):

1. 事务消息
// 获取高级发布接口
var push = await _factory.GetPushServiceAsync<OrderCreated>();
if (push is IRocketMQAdvancedPush advancedPush)
{
    // 发送事务消息(半消息)
    var sendResult = await advancedPush.PublishTransactionAsync(new OrderCreated("ORD-001", 99.9m));
    
    try
    {
        // 执行本地事务
        await _dbContext.SaveOrderAsync(new Order { OrderId = "ORD-001" });
        
        // 提交事务
        await advancedPush.EndTransactionAsync(sendResult, commit: true);
    }
    catch
    {
        // 回滚事务
        await advancedPush.EndTransactionAsync(sendResult, commit: false);
    }
}
2. 顺序消息
// 发送顺序消息(相同 orderKey 的消息进入同一队列)
await advancedPush.PublishOrderAsync(new OrderCreated("ORD-001", 99.9m), orderKey: "ORD-001");
3. 延迟消息
// 发送延迟消息(延迟级别 1-18)
await advancedPush.PublishDelayAsync(new OrderCreated("ORD-001", 99.9m), delayLevel: 3);
// 延迟级别对应:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
4. Request-Reply 模式
// 发送请求并等待响应
var response = await advancedPush.RequestAsync<OrderQuery, OrderResult>(
    new OrderQuery("ORD-001"), 
    timeout: 5000);
5. Tag/SQL92 过滤
// 获取高级消费接口
var receive = await _factory.GetReceiveServiceAsync<OrderCreated>();
if (receive is IRocketMQAdvancedReceive advancedReceive)
{
    // Tag 过滤
    await advancedReceive.SubscribeWithFilterAsync<OrderCreated>(
        async msg => { /* 处理消息 */ return true; },
        RocketMQExpressionType.Tag,
        "TagA || TagB");
    
    // SQL92 过滤
    await advancedReceive.SubscribeWithFilterAsync<OrderCreated>(
        async msg => { /* 处理消息 */ return true; },
        RocketMQExpressionType.SQL92,
        "amount > 100 AND status = 'ACTIVE'");
}
6. 多 Topic 订阅
// 一个 Consumer 同时消费多个 Topic
await advancedReceive.SubscribeMultipleTopicsAsync<OrderCreated>(
    async msg => { /* 处理消息 */ return true; },
    topics: "Topic1;Topic2;Topic3");
7. Pop 消费模式(RocketMQ 5.0+)
// 轻量消费模式,无需客户端 Rebalance
await advancedReceive.PopConsumeAsync<OrderCreated>(
    async msg => { /* 处理消息 */ return true; },
    maxNums: 32,
    invisibleTime: 30000);
云厂商支持

RocketMQ 支持主流云厂商的托管服务:

{
  "RocketMQ": {
    "NameServerAddress": "MQ_INST_xxx.aliyuncs.com:80",
    "CloudProvider": "Aliyun",
    "AccessKey": "your_access_key",
    "SecretKey": "your_secret_key",
    "InstanceId": "MQ_INST_xxx"
  }
}
云厂商 CloudProvider 必需配置
阿里云 Aliyun AccessKey, SecretKey, InstanceId
华为云 Huawei AccessKey, SecretKey, InstanceId, EnableSsl
腾讯云 Tencent AccessKey, SecretKey, Namespace
Apache ACL Apache AccessKey, SecretKey
自建 RocketMQ 密码配置(ACL)

v7.10.6 新增:Builder 链式配置,无需手写 appsettings。

自建 RocketMQ 开启 ACL 认证(broker.conf 配置 aclEnable=true + aclAccessKey/aclSecretKey):

.UseMQ(mq => mq.UseRocketMQ(r => r
    .WithNameServer("127.0.0.1:9876")
    .WithAcl("your_access_key", "your_secret_key")))   // ✅ Apache ACL 认证

或者 appsettings.json 配置

{
  "RocketMQ": {
    "NameServerAddress": "127.0.0.1:9876",
    "AccessKey": "your_access_key",
    "SecretKey": "your_secret_key"
  }
}

云厂商托管服务链式配置

.UseMQ(mq => mq.UseRocketMQ(r => r
    .WithNameServer("MQ_INST_xxx.aliyuncs.com:80")
    .WithCloudProvider(RocketMQCloudProvider.Aliyun, "ak", "sk", "MQ_INST_xxx")
    .WithSsl(true)))   // 华为云等需要 SSL

完整 Builder 方法

方法 说明
WithNameServer(addr) NameServer 地址
WithGroup(group) 默认生产/消费组
WithPrefix(prefix) Topic/Tag/Group 前缀
WithAcl(ak, sk) 自建 RocketMQ ACL 认证(等价 CloudProvider=Apache)
WithCloudProvider(type, ak, sk, extra) 云厂商托管(Aliyun/Huawei/Tencent)
WithSsl(enable) SSL/TLS 开关(华为云等)
WithServiceName(name) 服务名称
消费端看门狗(RocketMQ)

RocketMQ 消费端内置看门狗机制(RocketMQSubscriberRocketMQReceive 均支持):

  • 心跳检测:每 10 秒检查消费活动状态
  • 停滞检测:超过 5 分钟无消费活动,判定 consumer 可能已死,触发恢复
  • 自动恢复:Dispose 旧 Consumer + 创建新 Consumer + Start
  • 指数退避:恢复失败时 1s->2s->4s->8s->16s->30s 退避重试
  • 低频持续:连续失败 20 次后进入低频模式(每 60 秒一次),永不永久停止

看门狗默认启用,无需额外配置。详见 MQ 看门狗使用指南

注意事项
  1. 第一版限制:事务消息、顺序消息、延迟消息、ACL 等高级特性通过扩展接口提供,需要手动转换接口类型
  2. Topic 预创建:生产环境建议由运维预先创建 Topic,不依赖自动创建
  3. 多 Provider 并存:同一容器不能同时启用 RabbitMQ 和 RocketMQ
  4. 失败消息:推荐使用 IMQFailedMessageHub 通用接口,避免依赖特定 MQ 术语

2.6 IMQProvider 统一入口(推荐)

v7.10.4 新增IMQProvider 是 MQ 的统一提供者接口,一站式获取所有 MQ 服务。 支持细粒度能力检查,避免"静默降级"问题(如 RabbitMQ 的事务消息实际是普通消息)。

注册方式

v7.10.6 更新UseRocketMQ / UseRabbitMQ 已自动包含 IMQProvider 注册,无需单独调用 UseMQProvider

.UseMQ(mq => mq
    // ✅ 推荐:直接配置引擎,IMQProvider 自动注册
    .UseRocketMQ(r => r
        .WithNameServer("127.0.0.1:9876")
        .WithAcl("ak", "sk")))

UseMQProvider 保留为快捷别名(不关心连接参数、只需切换引擎类型时使用):

.UseMQ(mq => mq.UseMQProvider(MQProviderType.RocketMQ))  // 等价于 UseRocketMQ()
.UseMQ(mq => mq.UseMQProvider(MQProviderType.RabbitMQ))  // 等价于 UseRabbitMQ()
一站式获取所有服务
public class OrderService
{
    private readonly IMQProvider _provider;

    public OrderService(IMQProvider provider) => _provider = provider;

    // 场景 1:IMQHub(日常发布订阅,类型/Topic 驱动)
    public async Task PublishWithHub(OrderEvent order)
    {
        var hub = _provider.GetHub();
        await hub.PublishAsync(order);  // 类型自动推导队列名
    }

    // 场景 2:IMQPublisher(基础发布)
    public async Task PublishWithPublisher(OrderEvent order)
    {
        var publisher = _provider.GetPublisher();
        await publisher.PublishAsync("OrderTopic", order);
    }

    // 场景 3:IMQSubscriber(基础订阅)
    public async Task SubscribeOrders()
    {
        var subscriber = _provider.GetSubscriber();
        await subscriber.SubscribeAsync<OrderEvent>("OrderTopic", "OrderGroup", async order => {
            return true;
        });
    }

    // 场景 4:IMQFactory(按配置节获取)
    public async Task PublishWithFactory(OrderEvent order)
    {
        var factory = _provider.GetFactory();
        var push = await factory.GetPushServiceAsync("OrderMQ");
        await push.PublishAsync(order);
    }
}
细粒度能力检查

IMQProvider 提供 5 个能力标志,业务代码可以提前检查,避免使用不支持的功能:

能力标志 RabbitMQ RocketMQ 说明
SupportsTransactionMessages 事务消息(半消息 + 二次确认)
SupportsDelayedMessages 延迟消息(RocketMQ 18 级)
SupportsRequestReply Request-Reply 模式
SupportsSql92Filtering SQL92 过滤表达式
SupportsOrderedMessages 严格顺序消息
public class PaymentService
{
    private readonly IMQProvider _provider;

    public PaymentService(IMQProvider provider) => _provider = provider;

    // ✅ 事务消息 + 能力检查
    public async Task SendTransactionMessage(PaymentEvent payment)
    {
        if (!_provider.SupportsTransactionMessages)
        {
            throw new NotSupportedException(
                $"当前 MQ 引擎 ({_provider.ProviderType}) 不支持事务消息,请切换到 RocketMQ");
        }

        var publisher = _provider.GetAdvancedPublisher()
            ?? throw new InvalidOperationException("无法获取高级发布者");

        var result = await publisher.PublishTransactionAsync("PaymentTopic", payment);
        try
        {
            await _db.SavePaymentAsync(payment);       // 本地事务
            await publisher.EndTransactionAsync(result, commit: true);
        }
        catch
        {
            await publisher.EndTransactionAsync(result, commit: false);
            throw;
        }
    }

    // ✅ 延迟消息 + 能力检查(RabbitMQ 降级为调度框架)
    public async Task SendDelayMessage(PaymentEvent payment, int delayMinutes)
    {
        if (!_provider.SupportsDelayedMessages)
        {
            // RabbitMQ:使用调度框架替代
            await _scheduler.ScheduleAsync(
                () => _provider.GetPublisher().PublishAsync("PaymentTopic", payment),
                TimeSpan.FromMinutes(delayMinutes));
            return;
        }

        // RocketMQ:原生延迟消息(1-18 级)
        var publisher = _provider.GetAdvancedPublisher()!;
        await publisher.PublishDelayAsync("PaymentTopic", payment, delayLevel: 3);
    }
}
性能说明
  • 能力检查是常量属性,零运行时开销
  • GetHub() / GetFactory() 内部懒加载缓存,只解析一次 DI
  • GetPublisher() / GetSubscriber() 每次返回新实例(Transient 语义),内部连接池(Producer/Consumer)复用,实例创建成本极低
  • Provider 本身是 Singleton,全应用共享一个实例
未来扩展(MQTT 等)

新增 MQ 引擎只需:

  1. MQProviderType 枚举中新增成员(如 MQTT = 2
  2. 实现 IMQProvider(如 MQTTProvider
  3. 实现对应的 Hub/Publisher/Subscriber 适配器
  4. 注册时一行切换,业务代码零改动

2.7 PushMessage 统一消息模型(推荐)

v7.10.6 新增PushMessage渠道无关的统一推送消息模型。 业务代码只依赖 PushMessage 构建消息,推送时由各渠道适配器自动转换为钉钉/飞书/企业微信的 webhook JSON。 切换渠道业务代码零改动(只换注册)。

为什么需要 PushMessage?
❌ 旧方式:业务代码直接使用渠道专属消息类
DingTalkMessage.Markdown("告警", "CPU过高")  → 切飞书要改 LarkMessage.Post(...),方法名/属性全不同

✅ 新方式:PushMessage 统一模型(渠道无关)
PushMessage.Markdown("告警", "CPU过高")      → 钉钉/飞书/企业微信通用
五种消息类型
using Lord.Service.ApiPush.Messages;

// 1. 纯文本
PushMessage.Text("hello world", "13800000000");          // 可选 @用户

// 2. Markdown 富文本
PushMessage.Markdown("告警", "CPU 使用率过高", "138xxxx");

// 3. 链接卡片
PushMessage.Link("标题", "描述", "https://example.com", "picUrl");

// 4. 按钮卡片(整体跳转 / 多个按钮)
PushMessage.ActionCard("确认", "是否继续?", "确认", "https://a.com");
PushMessage.ActionCard("标题", "内容", new[] { ("确认", "https://a.com"), ("取消", "https://b.com") });

// 5. 信息流卡片
PushMessage.FeedCard(new[] { ("标题1", "https://a.com", "pic1.jpg") });
链式构建(IMessageBuilder)

PushMessage / DingTalkMessage / LarkMessage / WeChatMessage 均实现统一的 IMessageBuilder<T> 接口, 支持完全一致的链式构建体验:

var msg = PushMessage.Markdown("告警", "CPU 使用率过高")
    .Add("主机", "192.168.1.1")                              // 单条键值对
    .Add(new Dictionary<string, string> {                     // 批量字典
        ["CPU"] = "95%",
        ["内存"] = "80%"
    })
    .Add(new { 负载 = "2.5", 磁盘 = "70%" })                  // 实体属性自动转键值对
    .At("13800000000");                                       // @用户

// DingTalkMessage / LarkMessage / WeChatMessage 用法完全一致
var ding = new DingTalkMessage("告警").Add("主机", "192.168.1.1").At("138xxxx");
var lark = new LarkMessage("告警").Add("主机", "192.168.1.1");
var wechat = new WeChatMessage("告警").Add("主机", "192.168.1.1");
推送(渠道自动转换)
// 注入推送工厂(钉钉/飞书/企业微信)
public class AlertService(IApiPush push)
{
    public async Task SendAlertAsync(string title, string content)
    {
        var msg = PushMessage.Markdown(title, content).Add("主机", "192.168.1.1");
        await push.PushAsync(msg);   // 自动转换为当前渠道 JSON
    }
}

// 切换渠道 = 只改注册,业务代码零改动!
// 钉钉:    .UsePush(p => p.AddDingTalk(c => c.FromConfiguration("DingPush")))
// 飞书:    .UsePush(p => p.AddLark(c => c.FromConfiguration("LarkPush")))
// 企业微信: .UsePush(p => p.AddWeChat(c => c.FromConfiguration("WeChatPush")))
MQ 场景(生产端发 PushMessage,消费端任意渠道推送)
// 生产端:MQ 里传 PushMessage(渠道无关 JSON)
var msg = PushMessage.Markdown("异常通知", null)
    .Add("异常信息", ex.Message)
    .Add(new { 服务器 = ip, 时间 = DateTime.Now });
await hub.PublishAsync(msg);

// 消费端:订阅 PushMessage,用任意渠道推送
await _hub.SubscribeAsync<PushMessage>(async msg =>
{
    return await push.PushAsync(msg);   // 自动转当前渠道
});

注意:MQ 场景下生产端和消费端的消息类型必须一致(都用 PushMessage 或都用 DingTalkMessage)。

渠道适配器(内部原理)
PushMessage.Markdown("告警", "CPU过高").Add("主机", "192.168.1.1")
    ├─ 钉钉适配器     → {"msgtype":"markdown","markdown":{"title":"告警",...}}
    ├─ 飞书适配器     → {"msg_type":"post","content":{"zh_cn":{"title":"告警",...}}}
    └─ 企业微信适配器 → {"msgtype":"markdown","markdown":{"content":"#### 告警..."}}

各推送实现构造时自动注册自己的适配器,PushAsync(PushMessage) 按实例类型自动转换。

与渠道专属消息类的选择
场景 推荐
需要切换渠道 / MQ 传输 PushMessage(渠道无关)
固定使用钉钉 + 高级定制 DingTalkMessage
固定使用飞书 + 高级定制 LarkMessage
固定使用企业微信 + 高级定制 WeChatMessage

2.8 MQ 消息加密开关

v7.10.6 新增:消息级加密控制。无敏感信息的消息可明文传输(提升性能、避免加解密配置不匹配问题)。

三级控制(优先级从高到低)
IMQSettings(消息级) > UseMQ 全局配置 > appsettings.json(引擎配置)
1. 消息级(IMQSettings.EnableEncryption / EncryptionPrefixes)
// 单个消息关闭加密(无敏感信息)
await hub.PublishAsync(msg, s => { s.EnableEncryption = false; });

// 前缀白名单:只加密指定前缀的队列(其余明文)
await hub.SubscribeAsync<T>(handler, s => { s.EncryptionPrefixes = "secret_;private_"; });
2. UseMQ 全局配置(引擎无关:RabbitMQ/RocketMQ/未来 Kafka)
.UseMQ(mq => mq
    // 全局关闭加密(所有队列明文)
    .EnableEncryption(false)

    // 或:前缀白名单(只加密 "secret_" 和 "private_" 开头的队列)
    .EncryptionPrefixes("secret_", "private_")

    .UseRocketMQ(r => r.FromConfiguration("RocketMQ")))
3. appsettings.json(引擎配置级)
{
  "RocketMQ": {
    "NameServerAddress": "localhost:9876",
    "EnableEncryption": false,
    "EncryptionPrefixes": "secret_;private_"
  }
}
生产端与消费端一致性

生产端和消费端使用相同的队列名时自动保持一致的加密策略(都按队列名前缀判断)。


2.9 MQ 事务功能详解

事务消息是 RocketMQ 的原生能力(半消息 + 二次确认),RabbitMQ 不支持(调用时降级为普通消息并输出警告)。

为什么需要事务消息?

场景:订单系统先落库、再发 MQ 通知下游。两者要么都成功、要么都失败:

❌ 普通消息:先发消息后落库 → 落库失败但消息已发出(下游处理了不存在的订单)
❌ 先落库后发消息 → 发送失败但订单已存在(下游不知道有新订单)

✅ 事务消息:
1. 发送"半消息"(Broker 暂存,不投递)
2. 执行本地事务(落库)
3. 提交 → Broker 投递消息;回滚 → Broker 丢弃消息
4. 若第 2 步崩溃 → Broker 回查事务状态,决定投递还是丢弃
使用方式(RocketMQ)
// 1. 获取高级发布者(IMQProvider 已随 UseRocketMQ 自动注册)
public class OrderService(IMQProvider provider)
{
    public async Task CreateOrderAsync(Order order)
    {
        // 检查引擎能力(RabbitMQ 会返回 false)
        if (!provider.SupportsTransactionMessages)
        {
            throw new NotSupportedException("当前 MQ 引擎不支持事务消息,请使用 RocketMQ");
        }

        var publisher = provider.GetAdvancedPublisher()
            ?? throw new InvalidOperationException("无法获取高级发布者");

        // 2. 发送半消息(Broker 暂存,不投递),返回事务对象
        // ✅ 方式 A:Topic 由类型自动推导(OrderEvent → OrderEvent_Topic,推荐)
        var tx = await publisher.PublishTransactionAsync(order);

        // 方式 B:显式指定 Topic
        // var tx = await publisher.PublishTransactionAsync("OrderTopic", order);

        try
        {
            // 3. 执行本地事务
            await _db.SaveOrderAsync(order);

            // 4. 提交事务 → Broker 投递消息(✅ 事务对象直接提交,直观方便)
            await tx.CommitAsync();
        }
        catch
        {
            // 5. 回滚事务 → Broker 丢弃消息(✅ 事务对象直接回滚)
            await tx.RollbackAsync();
            throw;
        }
    }
}
事务消息的可靠性保障
┌─────────────┐   半消息     ┌──────────────┐   提交     ┌──────────┐
│   业务代码   │ ──────────→ │  RocketMQ    │ ────────→ │  消费者   │
│             │              │  (Broker)    │            │          │
│  1. 发半消息  │              │  暂存不投递   │            │          │
│  2. 本地事务  │              │              │            │          │
│  3. 提交/回滚 │ ←────────── │  回查(可选)  │            │          │
└─────────────┘   回查状态    └──────────────┘            └──────────┘
  • 半消息:Broker 收到但暂不投递,消费者不可见
  • 二次确认:业务提交/回滚后 Broker 才投递或丢弃
  • 事务回查:业务进程崩溃时,Broker 主动回查本地事务状态(需业务实现回查接口,本库提供基础支持)
事务对象 API(推荐)

PublishTransactionAsync 返回的 MQTransactionResult 对象自带提交/回滚方法。

两种重载

// 方式 A:自动推导 Topic(消息类型 → {TypeName}_Topic,泛型安全)
var tx = await publisher.PublishTransactionAsync(order);
//   例如 OrderEvent → OrderEvent_Topic;List<int> → List_Int32_Topic

// 方式 B:显式指定 Topic
var tx = await publisher.PublishTransactionAsync("OrderTopic", order);
var tx = await publisher.PublishTransactionAsync("OrderTopic", order);
await tx.CommitAsync();    // 提交事务(投递消息)
await tx.RollbackAsync();  // 回滚事务(丢弃消息)
  • 事务对象只能结束一次:重复 Commit/Rollback 抛出 InvalidOperationException
  • 引擎未绑定(如 RabbitMQ 降级)时 Commit/Rollback 会明确告警,不再静默失败
  • 旧 API EndTransactionAsync(result, commit) 仍然可用(兼容)
RabbitMQ 的降级行为(警告)
// RabbitMQ 上调用事务 API:
var result = await publisher.PublishTransactionAsync("OrderTopic", order);
// ⚠️ 输出警告:RabbitMQ 不支持事务消息,降级为普通消息
// ⚠️ 消息立即投递,EndTransactionAsync(commit:false) 无法回滚!

// 生产环境必须提前检查能力:
if (!provider.SupportsTransactionMessages)
{
    // 方案 A:改用 RocketMQ
    // 方案 B:本地事务表 + 补偿机制
}
顺序消息(RocketMQ 原生)
// 相同 orderKey 的消息路由到同一队列,保证消费顺序
await publisher.PublishOrderAsync("OrderTopic", order, orderKey: order.OrderId);
延迟消息(RocketMQ 原生 18 级)
// 延迟级别 1-18 对应:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
await publisher.PublishDelayAsync("OrderTopic", order, delayLevel: 3);
Request-Reply(RocketMQ 原生)
// 发送请求并等待响应(同步 RPC 模式)
var response = await publisher.RequestAsync<OrderQuery, OrderResult>(
    new OrderQuery("ORD-001"), timeout: 5000);
能力检查总览
provider.SupportsTransactionMessages   // 事务消息(RabbitMQ: false, RocketMQ: true)
provider.SupportsDelayedMessages       // 延迟消息(RabbitMQ: false, RocketMQ: true)
provider.SupportsRequestReply          // Request-Reply(RabbitMQ: false, RocketMQ: true)
provider.SupportsSql92Filtering        // SQL92 过滤(RabbitMQ: false, RocketMQ: true)
provider.SupportsOrderedMessages       // 顺序消息(RabbitMQ: false, RocketMQ: true)

3. 消息推送(钉钉/企业微信/飞书)

注册方式
.UsePush(push => push
    // 钉钉机器人
    .AddDingTalk(d => d
        .FromConfiguration("DingTalk")           // 从配置加载
        // 或:.AddGroup("告警群", "token", "secret") // 直接添加群组
    )
    // 企业微信机器人
    .AddWeChat(w => w
        .FromConfiguration("WeChatPush")
        // 或:.AddGroup("运维群", "key")
    )
    // 飞书机器人
    .AddLark(l => l
        .FromConfiguration("LarkPush")
        // 或:.AddGroup("开发群", "token", "secret")
    )
    // 钉钉应用推送(工作通知)
    .AddDingApp(a => a
        .FromConfiguration("DingApp")
        // 或:.WithCredentials("appKey", "appSecret", "agentId")
    )
)
业务使用
public class NotificationService
{
    private readonly IDingTalkApiFactory _dingFactory;
    private readonly IWeChatApiFactory _weChatFactory;
    private readonly ILarkApiFactory _larkFactory;

    public NotificationService(
        IDingTalkApiFactory dingFactory,
        IWeChatApiFactory weChatFactory,
        ILarkApiFactory larkFactory)
    {
        _dingFactory = dingFactory;
        _weChatFactory = weChatFactory;
        _larkFactory = larkFactory;
    }

    // 推送到默认群
    public async Task<bool> NotifyDingTalkAsync(string content)
    {
        var push = _dingFactory.GetPushService();
        return await push.PushAsync(content);
    }

    // 推送到指定群(按 Alias)
    public async Task<bool> NotifyDingTalkGroupAsync(string alias, string content)
    {
        var push = _dingFactory.GetPushService(alias);
        return await push.PushAsync(content);
    }

    // 使用消息格式化器
    public async Task<bool> NotifyWithFormatAsync()
    {
        var push = _dingFactory.GetPushService();
        return await push.PushAsync(format => format.Text("服务器 CPU 超过 90%"));
    }

    // 获取所有推送服务
    public async Task BroadcastAsync(string content)
    {
        foreach (var push in _dingFactory.GetAllPushService())
        {
            await push.PushAsync(content);
        }
    }
}

安全提示:推送失败日志已自动脱敏,不会将完整消息内容写入日志。


4. 加密(AES-GCM / RSA)

注册方式
// AES-GCM(推荐,默认)
.UseEncryption(enc => enc.UseAES(a => a
    .FromConfiguration("Encryption")        // 从配置加载
    // 或:.WithKeys("base64_key", "base64_iv")  // 直接配置
))

// RSA
.UseEncryption(enc => enc.UseRSA(r => r
    .FromConfiguration("Encryption")
    // 或:.WithKeys("public_key_xml", "private_key_xml")
))

// DES(已标记 Obsolete,仅用于历史数据解密,不推荐生产新用)
// .UseEncryption(enc => enc.UseDES(d => d.FromConfiguration("Encryption")))
AES 密钥要求
项目 要求
密钥格式 Base64 编码字符串
解码后长度 必须为 16 / 24 / 32 字节
加密算法 AES-GCM(认证加密)
密文格式 [LRD 0x02][nonce(12)][tag(16)][ciphertext]
压缩 加密前先 Deflate 压缩

重要:v7.11.0 起,AES 新加密统一使用 AES-GCM。密钥长度不合法时会抛 InvalidOperationException,不再静默截断/补零。

生成 AES 密钥
using System.Security.Cryptography;

var key = RandomNumberGenerator.GetBytes(32); // 256 位
var iv = RandomNumberGenerator.GetBytes(16);  // 备用 IV(仅 legacy 解密使用)
var keyBase64 = Convert.ToBase64String(key);
var ivBase64 = Convert.ToBase64String(iv);

// 写入 appsettings.json
// "Encryption": { "PublicKeyOrKey": "...", "PrivateKeyOrIV": "...", "EncryptType": "Aes" }
业务使用
public class SecureService
{
    private readonly IEncryptProvider _encryptor;

    public SecureService(IEncryptProvider encryptor) => _encryptor = encryptor;

    public byte[] Encrypt(string plainText)
    {
        var bytes = Encoding.UTF8.GetBytes(plainText);
        return _encryptor.Encryption(bytes);
    }

    public string Decrypt(byte[] cipher)
    {
        var bytes = _encryptor.Decryption(cipher);
        return Encoding.UTF8.GetString(bytes);
    }
}
加密配置示例
{
  "Encryption": {
    "PublicKeyOrKey": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
    "PrivateKeyOrIV": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
    "EncryptType": "Aes"
  }
}

5. HTTP 请求

注册方式
// RestSharp(默认)
.UseHttp(h => h.UseRestSharp().WithTimeout(TimeSpan.FromSeconds(30)))

// HttpClient
.UseHttp(h => h.UseNetHttp().WithTimeout(TimeSpan.FromSeconds(30)))
业务使用
public class ApiService
{
    private readonly IHttpFactory _httpFactory;

    public ApiService(IHttpFactory httpFactory) => _httpFactory = httpFactory;

    public async Task<MyResult?> GetDataAsync()
    {
        var factory = _httpFactory.CreateFactory("https://api.example.com");
        var response = await factory.CreateRequest("/api/v1/data")
            .AddHeader("Authorization", "Bearer token")
            .AddQueryParameter("page", "1")
            .GetAsync<MyResult>();
        return response.Content;
    }

    public async Task PostDataAsync(object payload)
    {
        var factory = _httpFactory.CreateFactory("https://api.example.com");
        await factory.CreateRequest("/api/v1/submit")
            .AddJsonBody(payload)
            .PostAsync();
    }
}
HTTP 重试策略
方法 默认重试 说明
GET / HEAD / OPTIONS / PUT / DELETE 3 次 幂等方法自动重试
POST / PATCH 不重试 防止重复提交
4xx(除 429) 不重试 客户端错误不可恢复
429 / 5xx 重试 服务端临时错误
超时/连接失败 重试 网络问题可恢复

如需对 POST 启用重试:

var request = factory.CreateRequest("/api/submit")
    .AddJsonBody(payload)
    .SetRetryCount(3)
    .AllowRetryNonIdempotent()  // 显式允许 POST 重试
    .PostAsync<MyResult>();

安全提示:HttpClient 缓存 key 已从 url.Host 改为 scheme://host:port,避免同 host 不同端口复用错误客户端。响应消息在读取内容后立即释放,避免连接池耗尽。


6. 日志(NLog / Log4Net)

注册方式
// NLog(默认)
.UseLogging(log => log.UseNLog())

// Log4Net
.UseLogging(log => log.UseLog4Net())

// 自定义日志配置
.UseLogging(log => log.UseNLog(builder =>
{
    builder.SetMinimumLevel(LogLevel.Information);
    builder.AddFilter<Microsoft.Hosting.Lifetime>("Microsoft", LogLevel.Warning);
}))

日志配置文件 nlog.config / log4net.config 优先从应用根目录加载,不存在时使用内置默认配置。


7. 微信公众号

注册方式
.UseWeChatOfficial(wx =>
{
    wx.FromConfiguration("WeChatOfficial");
    // 或:wx.WithSettings("appId", "appSecret", "token", "encodingAesKey?");
    // 可选:wx.UseMessageHandler<MyCustomHandler>();
})
微信公众号配置
{
  "WeChatOfficial": {
    "AppId": "your_app_id",
    "AppSecret": "your_app_secret",
    "Token": "your_token",
    "EncodingAesKey": null
  }
}
Controller 接入(安全 POST 入口)
[ApiController]
[Route("api/wechat")]
public class WeChatController : ControllerBase
{
    private readonly IWeChatSecureMessageHandler _handler;

    public WeChatController(IWeChatSecureMessageHandler handler)
        => _handler = handler;

    // GET:微信 URL 验证
    [HttpGet]
    public string Get(
        [FromQuery] string signature,
        [FromQuery] string timestamp,
        [FromQuery] string nonce,
        [FromQuery] string echostr)
    {
        var service = _handler.MsgOfficialService;
        return service.ValidateSignature(signature, timestamp, nonce, echostr, out var response)
            ? response : "fail";
    }

    // POST:微信消息回调(自动验签)
    [HttpPost]
    public async Task<string> Post(
        [FromQuery] string signature,
        [FromQuery] string timestamp,
        [FromQuery] string nonce)
    {
        using var reader = new StreamReader(Request.Body);
        var xml = await reader.ReadToEndAsync();
        // 先验签,再处理 XML
        return await _handler.HandleMessageAsync(xml, signature, timestamp, nonce);
    }
}

安全提示IWeChatSecureMessageHandler 在处理 XML 前会先校验微信签名。签名校验使用固定时间比较(CryptographicOperations.FixedTimeEquals),防止时序攻击。

事件订阅
public class WeChatEventService
{
    private readonly IWeChatEventHandler _events;

    public WeChatEventService(IWeChatEventHandler events)
    {
        _events = events;

        // 用户关注
        _events.OnUserSubscribed += async args =>
        {
            Console.WriteLine($"用户关注: {args.FromUser}");
            await Task.CompletedTask;
        };

        // 文本消息
        _events.OnTextMessageReceived += async args =>
        {
            Console.WriteLine($"收到文本: {args.Content}");
            await Task.CompletedTask;
        };
    }
}

8. 微信小程序

注册方式
.UseWeChatMiniProgram(wx =>
{
    wx.FromConfiguration("WeChatMiniProgram");
    // 或:wx.WithSettings("appId", "appSecret");
    // 多小程序:wx.AddProgram("alias", "appId", "appSecret");
})
配置
{
  "WeChatMiniProgram": {
    "AppId": "your_appid",
    "AppSecret": "your_secret",
    "BaseUrl": "https://api.weixin.qq.com"
  }
}

9. JSON 序列化

// Newtonsoft.Json(默认)
.UseJson(j => j.UseNewtonsoftJson(settings =>
{
    settings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
    settings.NullValueHandling = NullValueHandling.Ignore;
}))

// System.Text.Json
.UseJson(j => j.UseTextJson(opts =>
{
    opts.Serialize = o => o.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
}))

安全最佳实践

1. 加密

  • 生产环境必须使用 AES-GCM,不要使用 DES
  • AES 密钥长度必须为 16/24/32 字节(推荐 32 字节 = AES-256)
  • 密钥不要硬编码在代码中,使用 appsettings.json 或环境变量
  • 定期轮换密钥

2. 日志脱敏

  • 推送失败日志已自动脱敏,不会记录完整消息内容
  • 微信 XML 日志已自动脱敏 ContentRecognitionFromUserName 等敏感字段
  • 如需自定义脱敏,使用 LogSanitizer 工具类

3. 微信公众号

  • POST 回调必须使用 IWeChatSecureMessageHandler 进行签名校验
  • 签名校验使用固定时间比较,防止时序攻击
  • EncodingAesKey 配置后可支持加密模式

4. HTTP 请求

  • POST/PATCH 默认不重试,防止重复提交
  • 响应消息在读取内容后立即释放,避免连接池耗尽
  • HttpClient 缓存按 scheme://host:port 隔离

5. RabbitMQ

  • 连接创建有 30 秒总超时,不会无限阻塞
  • 消费端 handler 超时 3 分钟后自动取消,避免资源泄漏
  • 看门狗永不永久停止,失败后进入低频持续重试

6. Redis

  • 同步 AddOrGetCacheItem 有 30 秒超时保护
  • 分布式锁自动续期有重入保护,防止续期任务堆积
  • 批量操作自动分批(每批 500 条),避免大命令阻塞 Redis

v7.10.9 变更与迁移

根本性改进:RocketMQ 消费重试交给 broker

重试次数由 broker 按消息维度记录(消息属性 RECONSUME_TIMES),不再使用客户端本地内存计数。

旧实现(7.10.8 及以前):客户端 _retryCounts 字典按 MsgId 记录失败次数,达到 RetryCount 后由客户端 Producer 手动转发%DLQ%{group}。问题:

  • 计数在进程内存中,进程重启 / 消费者 Rebalance / 多实例集群消费时丢失或分散
  • 同一消息被多个实例各自计数、各自转发 → 死信重复入队
  • 本地 Task.Delay 指数退避(1s~30s)阻塞消费线程,重试节奏与 broker 延迟等级叠加混乱

新实现(7.10.9):消费失败时主动 SendMessageBackAsync 请求 broker 转投:

场景 broker 行为
未达 RetryCount 上限 转投 %RETRY%{group},按延迟等级(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)重投,自动等待下游服务恢复
已达上限(RECONSUME_TIMES >= RetryCount broker 原子转投 %DLQ%{group}(死信)
已达上限 + 前缀白名单不匹配 客户端 ack 丢弃,不进入 %DLQ%(行为与 7.10.8 一致)

可靠性保障:

  • 转投请求成功 → ack(offset 推进,broker 转投唯一副本,无重复回调)
  • 转投请求失败 → 不 ack(下个消费周期重新尝试,消息不丢失)
  • Consumer.EnableRetry 必须为 false:NewLife 自动重试在 offset 不推进时会对同一条消息无限重复回调并重复转投副本(真实 broker 实测 90 秒回调 4279 次全部 rt=0),重试动作由库统一接管

建议配置(下游服务可能维护的场景):RetryCount 按维护窗口设定,如下游最长维护 2 小时,配置 RetryCount = 16(broker 延迟等级全部用完 ≈ 5 小时窗口),维护期间消息自动在 %RETRY% 等待,恢复后自动消费成功,几乎不会进死信。

兼容性

  • RetryCount / EnableDeadLetter / DeadLetterPrefixes 配置与 7.10.8 完全一致,无需改代码
  • SubscribeFailedAsync / SubscribeDeadLetterAsync 死信订阅 API 不变
  • 行为差异:重试节奏从"客户端本地退避"变为"broker 延迟等级",重试间隔更合理

v7.10.8 变更与迁移

新功能

功能 说明
死信前缀白名单 DeadLetterPrefixes EncryptionPrefixes 同模式:队列名匹配前缀才创建/转发死信,不匹配的队列消费失败后直接丢弃("不重要的扔了就扔了")。builder(.DeadLetterPrefixes("important_", ...))、配置级、settings 级三级配置,RabbitMQ / RocketMQ 双引擎生效
RocketMQ 死信主题探针 死信订阅启动前自动发送 LORD_SERVICE_DLQ_PROBE 触发 %DLQ%{group} 主题创建,修复"Consumer 提前订阅不存在的 %DLQ% 主题后无法自动恢复"的问题(探针不会泄漏到主订阅,已实测)
RocketMQSubscriber 死信前缀过滤 SubscribeFailedAsync 与失败转发路径对齐 RocketMQReceive:消费组不匹配白名单时禁止订阅死信 / 失败直接丢弃(未配置前缀时行为与旧版完全一致)

重要修复

问题 修复
RocketMQFactory 死信前缀继承失效 括号嵌套错误(死信继承误嵌加密前缀 if 块内),配置 EncryptionPrefixesDeadLetterPrefixes 不再从 config 继承
RocketMQSubscriber 无视前缀白名单 消费失败超限后无条件转发 %DLQ%,现按前缀白名单丢弃
MQDeadLetterContext.Enabled 默认值 7.10.7 回归:默认 false 导致 EnableDeadLetter / UseDeadLetter() 静默不产生死信配置,超限消息被 broker 直接丢弃;恢复默认 true(8c81bee 审计修复)

迁移指南

  1. 死信前缀:默认(不配置)行为与旧版完全一致——全部队列启用死信。需要"不重要队列不建死信"时:

    .UseMQ(mq => mq.UseDeadLetter().DeadLetterPrefixes("important_", "critical_").UseRabbitMQ())
    

    或配置级:

    "RabbitMQ": { "DeadLetterPrefixes": "important_;critical_" }
    

    队列名以任一前缀开头(忽略大小写)才创建死信;不匹配的队列消费失败超限后直接丢弃消息。


v7.10.6 变更与迁移

新功能

功能 说明
PushMessage 统一消息模型 渠道无关推送,切换钉钉/飞书/企业微信零改动
IMessageBuilder 一致性接口 4 个消息类统一 Add 字典/实体/At 用户
消息加密开关 EnableEncryption / EncryptionPrefixes(消息级 + UseMQ 全局 + 配置级)
RocketMQ ACL 链式配置 WithAcl / WithCloudProvider / WithSsl
IMQProvider 自动注册 UseRocketMQ / UseRabbitMQ 已包含,无需单独调用
HTTP 幂等保护 POST 默认不重试 + AllowRetryNonIdempotent()
IHttpStreamResponse 释放 实现 IDisposable

重要修复

问题 修复
钉钉推送 @所有人逻辑写反 未指定 users 时不再误 @全员
飞书富文本消息必然抛异常 style 对象缺属性名 → 省略可选字段
Redis 信号量超时破坏 Wait 返回值检查 + 防 SemaphoreFullException
Redis ClearAsync 误清整库 无前缀无 pattern 时拒绝执行
Redis DateTime 时区偏移 保持 UTC Kind
DeepCopy 循环引用 StackOverflow AsyncLocal 深度计数,抛明确异常
NetHttp SendAsync 无限递归 修复调用自身
探针消息被消费无限重试 移除探针机制
看门狗停滞误判 连接活性检查 + 卡死检测

迁移指南

  1. IMQProvider 注册:原来 UseMQProvider() 单独调用可以删除,UseRocketMQ / UseRabbitMQ 已自动注册。

  2. 消息推送:新项目推荐直接使用 PushMessage(渠道无关);存量 DingTalkMessage 代码不受影响(API 兼容)。

  3. 加密:默认仍为 AES-GCM(安全)。无敏感信息的消息可用 EnableEncryption(false) 关闭加密。

  4. HTTP:POST 默认不重试(防重复提交)。需要重试时显式调用 .AllowRetryNonIdempotent()


v7.11.0 变更与迁移

安全增强

变更项 旧行为 新行为
默认加密 DES AES-GCM
AES 模式 CBC 无认证 GCM 认证加密
AES 密钥 静默截断/补零 严格校验 16/24/32 字节
推送日志 完整记录 content 自动脱敏
微信日志 完整记录 XML 自动脱敏敏感字段
微信 POST 无签名校验入口 新增 IWeChatSecureMessageHandler
签名比较 字符串相等 固定时间比较

稳定性增强

变更项 旧行为 新行为
MQ 连接创建 CancellationToken.None 30 秒内部超时
MQ 看门狗 20 次失败后永久停止 低频持续重试(60 秒一次)
MQ handler 超时 后台任务继续运行 取消后台任务
HTTP 响应 未释放 using var 确保释放
HTTP POST 默认重试 3 次 默认不重试
HTTP 缓存 key url.Host scheme://host:port

性能增强

变更项 旧行为 新行为
Redis RemoveBatch 一次性删除 分批 500 条
Redis SetBatch 一次性写入 分批 500 条
Redis 锁续期 可能重入堆积 重入保护
Redis singleflight 无超时 30 秒超时

架构改进

变更项 旧行为 新行为
Builder 注册 8 处 BuildServiceProvider 全部移除,改用 BindConfiguration 或懒加载

迁移指南

  1. DES → AES:如果旧数据是用 DES 加密的,解密时仍可使用 UseDES。新数据必须使用 UseAES

  2. 推送配置FromConfiguration() 的配置在首次调用 GetPushService(sectionName) 时懒加载,行为与之前一致。

  3. HTTP POST 重试:如果业务依赖 POST 自动重试,调用 .AllowRetryNonIdempotent() 显式开启。

  4. 微信 POST:将 Controller 中的 HandleMessageAsync(xml) 替换为 HandleMessageAsync(xml, signature, timestamp, nonce)

  5. RabbitMQ → RocketMQ 迁移

    无需修改的代码(使用通用接口):

    // ✅ 以下代码从 RabbitMQ 切换到 RocketMQ 无需修改
    await hub.PublishAsync(new OrderCreated("ORD-001", 99.9m));
    await hub.SubscribeAsync<OrderCreated>(async msg => { return true; });
    await hub.SubscribeFailedAsync<OrderCreated>(async msg => { return true; });
    

    需要修改的代码(仅注册部分):

    // ❌ 旧代码(RabbitMQ)
    .UseMQ(mq => mq.UseRabbitMQ(r => r.FromConfiguration("RabbitMQ")))
    
    // ✅ 新代码(RocketMQ)
    .UseMQ(mq => mq.UseRocketMQ(r => r.FromConfiguration("RocketMQ")))
    

    字段映射关系: | RabbitMQ 概念 | RocketMQ 概念 | 自动映射 | |--------------|--------------|---------| | Exchange | Topic | ✅ 自动 | | Queue | ConsumerGroup | ✅ 自动 | | RouteKey | Tag | ✅ 自动 | | DLX/DLQ | %DLQ%{ConsumerGroup} | ✅ 自动 |

    注意事项

    • 同一容器不能同时启用 RabbitMQ 和 RocketMQ
    • RocketMQ 需要在 broker 上预先创建 Topic(或启用自动创建)
    • 高级功能(事务/顺序/延迟消息)需要手动转换接口类型
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.2.0 31 9/8/2026
7.10.18 90 8/26/2026
7.10.8 136 8/11/2026
7.10.7 112 8/6/2026
7.10.6 110 8/5/2026
7.10.5 104 8/4/2026
7.10.1 114 8/1/2026
7.10.0 111 7/30/2026
7.0.8 133 7/29/2026
7.0.5 125 4/24/2026
7.0.3 152 2/10/2026
7.0.2 143 2/6/2026
7.0.1 150 2/4/2026
Loading failed

8.2.0(含破坏性变更,故按 SemVer 升次版本号;8.1.0 未曾对外发布,本版为其完整内容 + 订阅上限配置化):

 【已知问题与边界(发布前未全部修复,如实披露)】
  1. RabbitMQ 消费端集合(_handlers/_consumerTags)的无锁读取与重建写并发时,最坏产生一次多余的
     消费者重建(自愈、不丢消息);超时 Reject 与后台 handler 完成的结算竞态仍有极小窗口
     (有 _timedOutTags 缓解)。
  2. RocketMQ 的 SQL92 表达式过滤模式未做端到端验证(默认走 tag 过滤,已正反双向实测通过)。
  3. RedisCluster 部署下 Clear/GetBatch/RemoveBatch 的多 key 命令可能跨 slot 报 CROSSSLOT;
     单机/主从不受影响。集群用户请改用单 key API(相关 API 的 XML 注释已标注该限制)。
  4. ICache.UseDatabaseAsync 的数据库隔离基于 AsyncLocal,异步链上切换可能泄漏到后续调用
     (同步版 UseDatabase 正确);建议生产使用独立前缀而非切换数据库。
  5. TextJson 与 NewtonsoftJson 的错误契约仍不完全一致(抛异常 vs 部分场景返回 null);
     NewtonsoftJson 已支持注入 onError 回调接入日志观测,切换 IJsonFormat 实现前仍建议先冒烟验证。
  6. 加密模块:DesProvider 保留([Obsolete]);EncryptOption.EncryptType 仅为兼容遗留字段
     (实际按注册类型分派);老版本 CBC 密文无解密路径,升级前请排空队列。
  7. IMQSettings.MaxCacheCount 仅当显式配置正数且队列为新创建时生效
     (存量队列需先删除或启用 WithAutoRebuildOnConflict)。

 【修复】RocketMQ 稳定性收尾(消费停机/实例名/工厂缓存/订阅上限/停滞检测)
  - HandleMessageAsync 捕获 OperationCanceledException 短路返回:停机/重连窗口内的取消
    不再被当普通异常记 Error 并触发无谓重试。
  - Consumer 实例名身份稳定化:consumerGroup + 订阅表达式改用 FNV-1a 稳定哈希
    (原 string.GetHashCode 进程随机化,重启后实例名漂移导致 broker 侧消费状态不连续);
    实例名超长按上限截断。
  - RocketMQFactory 缓存语义修复:Lazy<Task> 内部改用 CancellationToken.None 创建,
    调用方传入的取消 token 不再"毒化"缓存条目(首个调用方取消 → 后续所有调用方永远拿到
    Canceled 任务);构造失败的条目成对移除(含 TryGetValue 快速路径),下次调用可重建重试。
  - 订阅数量上限(默认 256,新增 RocketMQConfig.MaxSubscriptionCount 可配置,≤0 回落默认):
    防异常调用方无限创建 consumer 耗尽连接资源,超限显式抛异常并提示配置项。
  - RocketMQSubscriber 看门狗补齐 per-consumer 停滞检测(与 RocketMQReceive 的 ProcessingTick
    同构):单个 consumer 的 handler 卡死不再影响其它 consumer 的判定与重建。

 【修复】微信模块收尾(区域无关解析/日志脱敏/事件短路/注册隔离)
  - 位置事件/位置消息坐标与 scale 解析改 InvariantCulture + NumberStyles.Any:
    系统区域为逗号小数点时不再抛 FormatException;scale "18.000000" 正确取整。
  - 日志脱敏补齐:Code2Session 的 openid/js_code、事件日志的 FromUser 经 LogSanitizer
    掩码;插值日志改结构化模板(Release 同样输出)。
  - 事件处理器链命中 IsHandled 后短路,后续 handler 不再重复处理同一消息。
  - EventDrivenWeChatMessageHandler 默认处理器注册改为按 invoker 实例幂等
    (ConditionalWeakTable),多容器/多 invoker 场景不再互相污染静态注册。

 【修复】HTTP/JSON 收尾(重试谓词/JSON 错误回调/超时语义/缓存计数)
  - LordResilience.CreateHttpRetryPipeline 增加 shouldRetryOn 谓词,默认只重试瞬时网络异常
    (HttpRequestException/TaskCanceledException)——修复 Polly v8 未配置 ShouldHandle 时
    任意异常(含确定性失败)都被重试、POST 被重放的问题;幂等性判断仍由调用方负责。
  - NewtonsoftJson 反序列化失败回调可注入(Action<Exception, string>):默认
    Trace.TraceWarning(此前 Debug.WriteLine 在 Release 被编译器擦除 → 失败零日志);
    "返回 default 不抛异常"的既有契约保持不变(TypeConvertUtil 等依赖此行为)。
  - IHttpRequest.SetTimeout XML 注释明确语义:仅约束单次尝试,受 HttpFactory 全局超时
    (默认 60s)封顶,总耗时 ≈ timeout × RetryCount;设置值超过工厂上限时告警。
  - HttpClientFactory/HttpFactory 缓存条目计数改 Interlocked 维护的近似值:
    热路径不再每次遍历桶统计 Count(O(n)),容量淘汰触发后闸内仍用真实 Count 复核。

 【修复】缓存收尾(空值哨兵/坏载荷自愈/Cluster 护栏)
  - MemoryCache 空值哨兵防穿透:AddOrGetCacheItem 回源 null 写入 NullSentinel
    (对照 RedisCache 的 NullValueMarker),命中哨兵返回 default、不再反复回源;
    显式 SetItem(null) 保持"清除"语义,MemoryCache 与 RedisCache 行为一致。
  - RedisCache 坏载荷自愈:AddOrGet(同步/异步 × 泛型/object)命中但反序列化失败时
    LogError + 删坏键 + 重新回源——此前坏载荷被判定命中返回 null,且持续整个 TTL 永不回源。
  - GetBatch/GetBatchAsync/RemoveBatch/RemoveBatchAsync/DeleteInBatches 的 XML 注释明示
    Cluster 部署下多 key 原子命令的 CROSSSLOT 限制(行为不变,见已知问题第 3 条)。

 【破坏性变更】RabbitMQ 发布方默认不再声明专属队列(ghost queue 修复)
  - 8.0.0 及之前 IMQPublisher 会在 Exchange 侧声明并绑定 {topic}_{tag}_Queue,该队列没有消费者
    (订阅方消费自己 consumerGroup 的队列),导致每条消息双投、该队列无限堆积,最终耗尽 broker 磁盘。
  - 新增 RabbitMQConfig.PublisherDeclareQueue(默认 false):发布方仅声明交换机。
  - 行为变更:订阅方未上线或路由键无绑定时,消息不可路由会经 mandatory + publisher confirm 抛
    PublishException(原先是静默写入 ghost 队列)。PublishOrderAsync(路由键 {orderKey}.ordered)
    与 PublishDelayAsync(路由键 Delay{n})在订阅方绑定 Default 时必然不可路由,升级后当场抛异常。
  - ⚠️ 置 PublisherDeclareQueue=true 只是回到"发布不报错、消息进一个无消费者的队列",
    并不能实现先发后订——订阅方消费的是自己的 consumerGroup 队列,取不到 ghost 队列里那份,
    消息事实上仍然丢失。需要"订阅方未上线也不丢"请让两端指向同一队列,而非依赖此开关。
    存量 ghost 队列请在管理端手动清理。

 【修复】RocketMQ 消费失败的消息静默永久丢失(重试主题 %RETRY% 从未被订阅)
  - NewLife.RocketMQ 的 Consumer 不会自动订阅重试主题(Java 客户端此处会加 MixAll.getRetryTopic(group))。
    HandleFailureAsync 主动转投的副本(转投本身成功、broker 侧 RECONSUME_TIMES 正常递增)因此永远留在
    %RETRY%{group} 中无人消费,最终随 commitlog TTL(默认 48-72h)过期 —— 重试与死信机制整体失效。
  - CreateConsumer 现将 %RETRY%{consumerGroup} 并入订阅集,RocketMQReceive 与 RocketMQSubscriber 两套实现
    同步修改;死信订阅排除,避免重试队列被死信消费者分走。
  - 真实 broker 端到端验证:失败消息按 10s、30s 延迟重投且 reconsume 递增,达上限后正常转入 %DLQ%。

 【修复】钉钉加签机器人推送必然失败(签名被双重 URL 编码)
  - CreateSign 对 Base64 签名做了 HttpUtility.UrlEncode,调用方再经 AddQueryParameter 传入时
    HTTP 层编码第二次,'=' 变成 %3D,钉钉解一次得到非法 Base64 → errcode 310000 sign not match。
    改为返回裸 Base64,与 LarkAuthentication 一致。

 【修复】微信公众号回调验签在 Token 为空时可被伪造
  - Token 为空时 SHA1(sort(["", timestamp, nonce])) 可由请求自带的 timestamp/nonce 直接算出,
    任何人可伪造合法签名通过服务器配置校验。现改为 Token 为空即拒绝验签并记 LogError(fail-closed)。

 【修复】DateTime? 解析时区错误(InvariantCulture 统一的遗漏分支)
  - ParseSimpleType 有 DateTimeOffset 分支却缺 DateTime,Nullable 解包后落到 Convert.ChangeType
    → DateTime.Parse(..., DateTimeStyles.None) → 转成本地时间。同一实体里 DateTime 字段得 Utc、
    DateTime? 字段得 Local,相差一个时区偏移。

 【修复】加密过滤器返回 Empty 会把消息体清空(RabbitMQ/RocketMQ 发布链路)
  - ApplyEncryption 写成 bytes = filter.OnEncrypt(...),把"该过滤器不处理"的 Empty 覆盖进 bytes
    本身,后续过滤器与兜底 Provider 收到空输入,发布出零字节消息体。改用局部变量,与解密侧对齐。

 【修复】RedisCache.SetItem(key, null) 不再静默保留旧值
  - 原实现 value == null 直接 return,既不写空值哨兵也不删键 → Redis 里的旧值一直命中到 TTL 结束。
    业务把实体删除/下架/清空后写 null 清缓存,读回来仍是删除前的对象(最长一整个 TTL)。
  - 现改为 KeyDelete 清除该键,与 MemoryCache.SetItem(null) 行为一致。
    (不写空值哨兵:哨兵是 AddOrGetCacheItem 防穿透用的内部标记,写进 SetItem 会让 ContainsKey
    返回 true 而 GetItem 返回 null 自相矛盾,且让 O(1) 存在性检查退化为全量读值。)

 【修复】RedisCache.HashSet 对接口/抽象类型读写不对称,读取直接抛异常
  - HashSet 原先传运行时类型 SerializeWithType(value, value.GetType()),NeedsTypeWrapper(具体类) 恒为
    false → 写入裸 JSON;而 HashGet<接口> 按声明类型判定、要求 TypeWrapper → 解析得到空 TypeName
    后回落 Deserialize<接口> → 接口无法实例化直接抛异常(HashGet 无 try/catch,异常直达业务)。
  - 现改传声明类型 typeof(T),与 String 路径 WriteValue(…, typeof(T)) 及 SetBatch 的既有修复对齐;
    同步与异步两条路径同时修复。

 【修复】Redis 分布式锁续期回调读取已释放的 CTS,未处理异常终止进程
  - RenewCoreSafe 在 _cts == null 守卫之后才求值 _cts.Token,与 DisposeAsync 的 _cts.Dispose()
    竞态 → ObjectDisposedException 从 Timer 回调同步抛出(无接收方即未处理异常,.NET 6+ 终止进程)。
    另一路径:带已取消 token 的 Task.Run 返回 Canceled 任务使 finally 不执行,_renewing 永久停在 1,
    此后所有续期被跳过,锁在临界区内静默过期。已移除该 CancellationTokenSource(未承担实际取消工作)。

 【修复】HttpFactory 静态缓存永久缓存 Lazy 构造异常
  - Lazy(ExecutionAndPublication) 会缓存构造异常:证书 pfx 被占用、密码错误、应用池未加载用户配置
    文件等暂时性故障一旦触发,该端点在进程存活期间每次调用都抛同一个缓存异常。补上移除损坏条目的
    守卫,与 HttpClientFactory、RabbitMQFactory 的既有实现一致。

 【修复】DeepCopy 三处:object 属性被替换成空对象、maxDepth 参数无效、索引器类型抛异常
  - object/dynamic 属性:object 有公共无参构造,此前落入"按声明类型递归深拷贝"分支,
    而表达式树对 typeof(object) 取不到任何公共属性 → 生成 new object(),原值被【静默替换成空对象】
    (字符串与 POCO 全丢)。现按运行时类型分流,与接口/抽象类型同一策略。
  - CopySafe 的 maxDepth 此前完全无效:CopySafeCore 只在顶层调用一次、depth 恒为 0,
    真正的递归走编译委托链并使用硬编码的 100 → 150 层无环链表传 maxDepth:500 照样抛异常,
    且文案一律写"可能存在循环引用"误导排查。现经 AsyncLocal 把预算下发到递归链,
    文案区分"过深"与"成环";maxDepth 非正数直接抛 ArgumentOutOfRangeException。
  - 带索引器的属性类型(List<T>/T[]/Dictionary<K,V>)此前在 Expression.Property 处抛
    ArgumentException 并被包成 TargetInvocationException,调用方只看到无意义的包装异常。
    现跳过索引器绑定(索引器不是可拷贝状态)。

 【修复】GetArrayOrSingle 对扁平单对象配置节绑出多个空占位对象
  - 原实现先调 Get<TEntity[]>() 再判 null,但对扁平节("DingTalk":{Alias,Token,Secret})
    它不返回 null,而是把每个叶子键当成一个数组元素各绑一次 → 产出 N 个字段不全的对象,
    于是 if (array != null) 恒真、单对象分支沦为死代码。
    而 LordService.md / MessageFormatGuide.md / PushServiceConfiguration.md 的主示例正是扁平节,
    下游把空 Token/空 Url 注册进去后,推送发往坏目标且不报"未注册"。
  - 现先判形态:任一子节本身仍是容器(有更深的子键)→ 集合;子节全是标量叶子 → 单个对象。
    索引节(0:/1:)与键控节(DevGroup/OpsGroup)两种数组写法均保留。

 【安全】LogSanitizer 脱敏覆盖补齐
  - 新增:Authorization: Bearer <jwt> 整段抹除(此前只抹到 Bearer 一词,完整 JWT 入日志);
    URL 路径中的 /hook/ /webhook/ /notify/ 密钥段(飞书自定义机器人密钥在路径里,
    只处理查询串会完全漏掉);openid / unionid / js_code / session_key / api_key /
    client_secret / private_key 等字段名。
  - 修正:值优先匹配"带引号的完整字符串","password":"my secret phrase" 不再只抹第一段;
    字段名统一加词边界并容忍 JSON 引号,monkey / keywords / zipCode 等业务字段不再被误抹;
    分号纳入裸值终止符,SQL 风格连接串不再连带抹掉后续段。

 【修复】内置 nlog.config 会静默丢弃宿主框架的 Error/Fatal 日志
  - 三条规则只有 minlevel="Info" 而缺 maxlevel,且 final="true" 不写任何 target,
    导致 Microsoft.AspNetCore.* / Microsoft.EntityFrameworkCore.* / System.* 的 Info 及以上
    (含 Warn/Error/Fatal)全部被吞 —— 与规则自身注释"丢弃 Debug/Trace"的意图相反。
    宿主没有自带 nlog.config 时,Kestrel/中间件/EF 的异常一条都不落盘,故障无法定位。
  - 现补 maxlevel="Info":只吞 Trace/Debug/Info,Warn/Error/Fatal 继续走后续规则写入文件。

 【修复】WeChatMiniProgramFactory 跨容器凭据泄漏
  - SettingCache 是 static readonly 且永不清空:同一进程内多个 ServiceProvider(多租户宿主、
    后台 Worker 独立容器、并行测试类)互相看到并使用对方的 AppId/AppSecret ——
    只注册 tenantB 的容器也能取到 tenantA 的服务并用其密钥建连。
  - 现静态字典只作 Build 阶段的待注册队列,工厂构造时认领到实例级缓存并清空静态队列,
    与 ApiPush 侧 WeChatApiFactory / LarkApiFactory / DingTalkApiFactory 的既有修复方式一致。

 【修复】IMQHub 类型驱动路径:configure 改路由字段不再被静默丢弃(RabbitMQ 与 RocketMQ 两套)
  - GetCacheKey<T>(name, MQPool) 只含 类型+name+MQPool,不含 Exchange/Queue/RouteKey,
    而工厂对同一键复用首个实例并忽略后到的 settings。调用方在 configure 里改的路由字段因此被静默丢弃:
    发布端仍按首次的 RouteKey 发出(订阅端收不到),订阅端更严重——仍停留在首次的 QueueName,
    一条消息都收不到且全程无日志无异常。
  - 现记录每个键首次生效的路由指纹,检测到不一致即抛 MQException 并给出正确出路
    (改用带 name 的重载,或改用 Topic 驱动的 IMQPublisher/IMQSubscriber)。
    同键重复传入相同路由配置不受影响。

 【破坏性变更】RabbitMQ 消费端 handler 异常/超时改为退回队列(at-most-once → at-least-once)
  - 原实现 reject(requeue:false),而 EnableDeadLetter 默认 false(队列无 DLX)时 broker 直接删除消息,
    且绕过 RetryAsync 的退避重试。现改为 requeue:true 退回队列等待重投 + 1 秒延迟防瞬时热循环。
  - ⚠️ 投递语义变为 at-least-once:handler 已产生副作用后才抛异常的场景(HTTP 推送成功、后续步骤抛错;
    DB 写成功、日志抛错)会重复执行副作用。非幂等 handler 必须自行做幂等保护。
  - 超过 x-delivery-limit(= RetryCount,默认 5)后由 broker 按既有设计处置:启用 EnableDeadLetter 时
    消息进 DLX 队列长期留存(不消费就一直在那儿,由业务层按需排查/重放);未启用 DLX 时按丢弃处理。
    这是原有语义,非本次引入的缺陷。需要退避阶梯的场景仍应让 handler 返回 false
    (走 RetryAsync 的延迟副本 + x-retry-count),两条路径语义不同属已知分叉。
  - 死信队列(终点队列)与已达重试上限的最终丢弃路径保持 requeue:false 不变。
  - 已用真实 broker 端到端验证:handler 首次抛异常后消息被退回队列并再次投递(attempts=2),
    修复前该处只投递一次即被删除。

 【安全】HTTP 埋点 URL 脱敏,避免凭据被导出到观测后端
  - Activity 标签 http.url 原先打完整 URL:查询串里的 appkey/appsecret、access_token/sign/
    timestamp/js_code,以及飞书自定义机器人写在【路径】中的 /bot/v2/hook/<key> 密钥,
    都会随 OpenTelemetry / Application Insights / SkyWalking 导出并在采集端长期存储。
  - 新增 LordDiagnostics.SanitizeUrl(public):查询串只保留参数名(值一律替换为 ***),
    hook/webhook/notify 路径段掩码;NetHttp 三处、RestHttp 两处统一走它。

 【修复】RocketMQ 消息加密白名单两端判据不一致
  - IMQPublisher 按 Topic 判定是否加密,IMQSubscriber 却按 ConsumerGroup 判定是否解密;
    settings 驱动的两端则按 QueueName(RocketMQ 下映射为 ConsumerGroup,而发布端没有消费组上下文)。
    配置 EncryptionPrefixes 后会出现"生产加密、消费不解密"(业务收到密文乱码)或
    "生产明文、消费调解密"(抛异常进重试)。
  - 现 RocketMQ 一律按 Topic 判定(生产与消费唯一共同已知的量);
    RabbitMQ 保持按 QueueName 判定(其队列名两端生成规则一致,不受影响)。
  - ⚠️ 行为变更:此前按 ConsumerGroup 配置 EncryptionPrefixes 的 RocketMQ 用户,
    升级后需改为按 Topic 前缀配置。白名单为空(默认)的用户不受影响。

 【修复】RabbitMQ 发布端 Channel 无界增长(tag 基数耗尽 channel 上限)
  - IMQPublisher 原按 {topic}|{tag} 缓存 Push 实例,而每个实例独占一个 IChannel 且工厂字典只增不减。
    tag 是业务维度(PublishOrderAsync 直接用订单号当路由键),基数无上限;逼近 RequestedChannelMax(500)
    时 broker 以 connection 级 504 CHANNEL_ERROR 关闭整条连接 —— 该连接上所有发布与消费一起死。
  - 现缓存键退回 topic(一个 Exchange 一个 Channel),路由键改为每次发布显式传入(internal 重载,
    未改动任何 public 签名)。路由键前缀归一化与工厂共用同一份实现(NormalizeRouteKey),
    配 RabbitMQ:Prefix 的部署行为与 8.0.x 一致。
    工厂不是本库 RabbitMQFactory 时(拿不到显式路由键),从第一次调用起就按 tag 区分键,正确性优先。
  - 附带影响:PublishOrderAsync 不再按订单号占用 Channel(原先每个订单号一个 Channel,是泄漏最坏实例)。

 【修复】RabbitMQ 断开的连接被遗弃但仍在自动重连(幽灵连接 / 幽灵消费者)
  - ConnectionShutdown 回调原先只把连接移出缓存、从不 Close/Dispose。本工厂启用了
    AutomaticRecoveryEnabled + TopologyRecoveryEnabled,其恢复循环是无限重试,于是每次心跳超时、
    broker 重启或网络闪断都会留下一条幽灵连接,且它会把名下的 channel 与 consumer 一并恢复回来,
    成为管理端查不到、却与新连接并行抢消息的幽灵消费者;累积至撞破 broker max_connections。
  - 现在移除缓存的同时对旧连接 DisposeAsync,取消其自动恢复循环。
    已在隔离 broker 上真实断连验证:修复后 30 秒恢复窗口内连接数持续为 0;回退为旧行为
    (只移除不释放)则 10 秒后出现 1 条连接并持续存在,证实幽灵连接确由被遗弃对象自行重连产生。
    另有 DispatchProxy 伪造 IConnection 的零副作用单测 3 条覆盖边界。

 【新增】IMQSettings.MaxCacheCount 现真正生效(此前是死配置,写了队列名却从不进 broker)
  - RabbitMQ 生产/消费两端队列声明同步写入 x-max-length(两端必须一致,否则 406)。
    quorum 队列满时以 reject-publish 拒绝新发布,不静默丢队头,发布端经 publisher confirm 感知。
  - ⚠️ 默认值由 1000 改为 0(= 不限制),与历史实际行为保持一致:队列声明参数不可变,
    若默认写入该参数会让所有存量队列在升级后触发 406 PRECONDITION_FAILED 而停止服务。
    显式设为正数时,Broker 上已存在的队列需先删除旧队列或启用 WithAutoRebuildOnConflict(true)。
  - 已用 broker 管理 API 取硬证据:MaxCacheCount=0 的队列 arguments 中 x-max-length 确实缺失;
    =1000 的队列 broker 侧实际记录 x-max-length=1000;两者均为 x-queue-type=quorum
    (满时 reject-publish,不会静默丢队头)。

 【修复】DateTime 序列化端到端不对称:TextJson 宽松读取、用户转换器优先、Unix 长度窗口收紧,
  Builder 兜底改回 ISO 8601。
 【修复】RabbitMQ 消费链路三处:定时器泄漏、死信语义统一、Handler 依赖注入作用域。
 【修复】RabbitMQPush 声明熔断永久卡死:冷却自愈机制 + Channel 交换原子化。
 【修复】RocketMQ 消费停滞检测 per-consumer 隔离 + Producer 双检锁竞态。
 【修复】RocketMQ 伪顺序消息:PublishOrderAsync 改为按 orderKey 确定性哈希选固定队列。
 【修复】缓存与分布式锁四处:连接串优先级、Redis Cluster 键扫描、Dispose 快速失败、锁续期钳制。
 【修复】HttpClientFactory 释放链完整;类型转换统一使用 InvariantCulture。
 【变更】RedisCache 源码中的空值哨兵改用 \u0000 转义书写(编译产物完全不变,已缓存值不受影响)。
  此前该文件因含裸 NUL 字符被 git 与 ripgrep 判定为二进制,diff 与代码搜索会整体跳过它。
 【文档】澄清 isSlidingExpiration 的真实语义(包内 README 兼许可证文件 LordService.md)
  - 原注释"滑动过期:每次读取续期"易被理解为任何读取都会续期。实际仅
    AddOrGetCacheItem / AddOrGetCacheItemAsync 命中时刷新 TTL;
    SetItem / SetItemAsync / AddOrUpdateCacheItem 传入该参数在 RedisCache 下被静默忽略(绝对过期)。
  - 新增方法×实现语义对照表与三条使用规则(含 MemoryCache 切 RedisCache 时滑动退化为绝对过期的提醒)。

 ⚠️ 升级检查清单(8.0.x → 8.1.0,请逐项确认)
  1. 配了 RabbitMQ:Prefix 的部署(多项目共用 broker 的标准做法):发布端路由键现与订阅端同样补
     {prefix}. 前缀,两处共用同一份归一化实现(NormalizeRouteKey),无需改配置;
     建议测试环境验证一次 Topic 路由命中。
  2. 设了 PublisherDeclareQueue=true(仅回到"发布不报错",不提供先发后订能力):该模式自动退回"按 tag 独立实例",
     每个 tag 各占一个 Channel —— 此模式下 Channel 数量仍随 tag 基数增长,注意 RequestedChannelMax 上限。
  3. 显式配过 IMQSettings.MaxCacheCount 为正数:升级后会真的写入 x-max-length,对 Broker 上已存在的
     队列触发 406 PRECONDITION_FAILED,而本库默认对 406 停止服务。需先删除旧队列或启用
     WithAutoRebuildOnConflict(true)(会丢队列内未消费消息)。未配置过者不受影响(默认已改为 0=不限制)。
  4. RabbitMQ handler 可能抛异常的项目:投递语义由 at-most-once 变为 at-least-once,
     已产生副作用后抛异常会重复执行,非幂等 handler 必须自行加幂等保护。
  5. RocketMQ 配过 EncryptionPrefixes:判据由 ConsumerGroup 改为 Topic,需按 Topic 前缀重新配置;
     未配置(默认全部加密)者不受影响。
  6. RocketMQ 存量消费组:已用真实 broker 验证"先建立业务 topic 的 commit offset、再让消息失败"场景下
     %RETRY% 仍能正常重投(间隔 10s/30s、reconsume 递增),升级即恢复重试能力。
     ⚠️ 但此前长期堆积在各 group %RETRY% 中、尚未过 TTL 的历史副本会开始被消费 ——
     上线前请评估是否需要先在控制台清理这些重试队列,避免出现集中重放。
  7. 接了 APM(OpenTelemetry / Application Insights / SkyWalking)的部署:http.url 标签现只保留
     参数名与去密钥后的路径,若看板或告警依赖完整 URL 需相应调整。

 【测试】本发布周期新增/更新回归用例,全量 499 个全部通过(会话开始时基线 451 个、450 通过)。
  其中钉钉签名、RocketMQ 加密判据、RedisCache 缓存语义三处做过变异测试(注入原缺陷确认对应用例转红)。
  覆盖:钉钉签名裸 Base64 与飞书编码约定一致性、钉钉推送端到端
  (本地 TcpListener 抓原始 HTTP 请求,RestHttp/NetHttp 双后端)验证 sign 恰好编码一次
  (按钉钉服务端同法用收到的 timestamp 复算签名比对)且消息正文标题与全部键值对完整送达、
  Base64 特殊字符 +/-//= 经查询串往返不被篡改、微信空/Null Token 拒绝验签且正常 Token 不受影响、
  DateTime 与 DateTime? 解析结果必须一致、RedisCache 哨兵改写后 IL 值不变、
  发布端逐次路由键正确 + 非本库工厂时首次调用即用 tag 级键、路由键前缀归一化(幂等/大小写/
  Order+Delay+Transaction 形态)、RedisCache 接口类型 Hash 读写往返(同步/异步)、
  SetItem(null) 清除旧值且 ContainsKey 与 GetItem 保持一致。
  含连本地 Redis 真实执行的 RedisCache 往返用例。此前长期超时失败的 RocketMQ 死信集成测试
  (FailedMessage_ForwardedToDlx_AndConsumedBySubscribeDeadLetter)在补订阅 %RETRY% 后已转绿。

8.0.0(稳定版):
 本版本为首个大版本稳定版,包含 7.10.x 全部修复,重点:
- 【可靠性】RabbitMQ 发布启用 publisher confirm + tracking(不丢消息)
  - 所有 Channel 创建时开启 PublisherConfirmationsEnabled/PublisherConfirmationTrackingEnabled。
    BasicPublishAsync 仅在收到 broker ack 后才算成功;broker nack 或无法路由(basic.return)
    会以 PublishException 抛出。修复“发布成功仅代表写入本地缓冲”的静默丢失风险。
  - 重试转发改为“转发成功(含 confirm)才 ACK 原消息”,消除“副本未落盘、原件已确认”的丢失窗口。
- 【可靠性】RabbitMQ 延迟重试拓扑与性能优化
  - 回投改走默认交换机("")+ 路由键=原队列名,精确投递回失败队列,避免 fanout/topic 多绑定重复消费。
  - 移除 {queue}.retry.exchange:不再创建多余 exchange,省去 ExchangeDeclare/QueueBind,
    也避免队列过期回收后残留永久 exchange。
  - 按需声明:每次失败只声明当前延迟等级对应的那一个队列(1 次 QueueDeclare),
    不再一次性声明全部等级(原为 1+8+8 次 broker RPC)。
  - 声明缓存按延迟等级分别记录(不同等级是不同队列,原单一时间戳会导致第二个等级被错误跳过)。
  - 声明窗口 = min(60s, x-expires/2),保证窗口内队列必然未被空闲回收。
- 【可靠性】RabbitMQ 延迟队列 x-expires 安全钳制
  - x-expires 官方语义不要求队列为空——等待中的消息也会随队列过期被删除且不走死信。
    因此过期时长被强制取 max(配置值, 延迟×2),杜绝“过期时长小于延迟导致等待消息丢失”。
- 【可靠性】RabbitMQ 重试转发失败不静默丢失
  - 原实现在转发失败时 requeue=false 直接丢弃(日志误称“由死信兜底”,未启用 DLX 时不成立)。
    现改为 requeue=true 让消息回到原队列等待下次投递,并短暂延迟避免故障期热循环。
  - 重试发布改走 mandatory=true,配合 confirm 使队列不存在/无法路由时 await 抛异常,走重入队路径。
  - 重试发布完整复制原消息属性(ReplyTo/Priority/Type/UserId/AppId 等,原实现仅复制部分)。
  - 移除上一版新增的“延迟队列声明 406 切换无 x-expires 兼容”分支(按用户要求,该场景无需处理)。
- 【修复】三平台消息构建器序列化与构建缺陷
  - 补 Newtonsoft [OnSerializing] 生命周期:默认 JSON 引擎是 Newtonsoft,仅实现 STJ 的
    IJsonOnSerializing 会导致链式消息在默认引擎下序列化出缺正文的协议体(推送内容全空)。
  - 修复 _built 缓存失效:Build() 后继续 Add/FromSystem/At 会失效构建缓存并重新构建,
    不再静默忽略新增项;null 值规范为空串,避免 value.Replace 空引用。
- 【协议修复】飞书(Lark)
  - interactive 卡片改为位于顶层 card 字段(原误放 content,不符合自定义机器人协议)。
  - At() 真正生成提及:text 内联 <at user_id="ou_xxx">,post 以 {"tag":"at"} 元素;
    标识语义修正为用户 open_id(原注释误称手机号/邮箱)。AtUsers 不再泄漏为顶层非协议字段。
  - 恢复被删除的 Post(title, content) 重载与 Interactive 可选按钮参数;
    Create(string) 恢复旧语义(内容作为正文 + 默认标题,原误改为“仅标题”)。
  - LarkMessageAdapter:text 的 content 改为对象(原误序列化为 JSON 字符串);
    移除顶层无效 at/mentions 字段,@改为协议内 at 标签/元素。
- 【协议修复】企业微信(WeChat)
  - Image 恢复为 base64+md5(群机器人协议,无 media_id)——修复上一版回归。
  - At() 真正生效:text → text.mentioned_mobile_list(手机号)/mentioned_list(userid);
    markdown 无 @ 字段,userid 以 <@userid> 内嵌正文。AtUsers 不再泄漏为顶层非协议字段。
  - WeChatMessageAdapter:mentioned_* 列表从顶层移入 text 对象(顶层字段无效)。
  - 恢复 News 图片 URL 可空、Create(string) 旧语义(默认标题 Markdown)。
- 【修复】DingTalkMessage:补 Interactive() 兼容别名(委托 ActionCard,文档早已宣称存在)。
- 【公共 API】IMQBuilder 补充 RetryBackoffLevels(params string[]),
  修复 .UseMQ(mq => mq.RetryBackoffLevels("1s","5s")) 因接口缺方法而无法编译的问题。
- 【性能/隔离】HttpFactory / HttpClientFactory 静态缓存加固
  - jsonFormat 隔离改用按实例的稳定身份编号(ConditionalWeakTable),修复“同类型不同配置
    实例共享 RestClient”与“不同命名空间同名类型误判为同一”的问题。
  - 命名/证书工厂缓存键纳入 URL+超时(+证书标识),修复“同名不同配置第一次配置永久胜出”的静默复用。
  - handler 隔离改用稳定实例身份(替代可碰撞的 RuntimeHelpers.GetHashCode)。
  - 容量淘汰加并发闸(Interlocked):同一时刻只允许一个线程执行全量扫描+批量淘汰,
    避免多线程同时触顶造成过度淘汰与缓存抖动。
  - 缓存键不含证书密码(避免敏感信息进入诊断字符串)。
- 【测试】新增/更新约 20 个测试:三平台四种 JSON 往返矩阵、confirm/mandatory 语义、
  按需单等级声明、默认交换机精确回投、完整属性复制、x-expires 钳制、缓存身份隔离等。

7.10.19:
 - 所有 Channel 创建时开启 PublisherConfirmationsEnabled/PublisherConfirmationTrackingEnabled。
   BasicPublishAsync 仅在收到 broker ack 后才算成功;broker nack 或无法路由(basic.return)
   会以 PublishException 抛出。修复“发布成功仅代表写入本地缓冲”的静默丢失风险。
 - 重试转发改为“转发成功(含 confirm)才 ACK 原消息”,消除“副本未落盘、原件已确认”的丢失窗口。
- 【可靠性】RabbitMQ 延迟重试拓扑与性能优化
 - 回投改走默认交换机("")+ 路由键=原队列名,精确投递回失败队列,避免 fanout/topic 多绑定重复消费。
 - 移除 {queue}.retry.exchange:不再创建多余 exchange,省去 ExchangeDeclare/QueueBind,
   也避免队列过期回收后残留永久 exchange。
 - 按需声明:每次失败只声明当前延迟等级对应的那一个队列(1 次 QueueDeclare),
   不再一次性声明全部等级(原为 1+8+8 次 broker RPC)。
 - 声明缓存按延迟等级分别记录(不同等级是不同队列,原单一时间戳会导致第二个等级被错误跳过)。
 - 声明窗口 = min(60s, x-expires/2),保证窗口内队列必然未被空闲回收。
- 【可靠性】RabbitMQ 延迟队列 x-expires 安全钳制
 - x-expires 官方语义不要求队列为空——等待中的消息也会随队列过期被删除且不走死信。
   因此过期时长被强制取 max(配置值, 延迟×2),杜绝“过期时长小于延迟导致等待消息丢失”。
- 【可靠性】RabbitMQ 重试转发失败不静默丢失
 - 原实现在转发失败时 requeue=false 直接丢弃(日志误称“由死信兜底”,未启用 DLX 时不成立)。
   现改为 requeue=true 让消息回到原队列等待下次投递,并短暂延迟避免故障期热循环。
 - 重试发布改走 mandatory=true,配合 confirm 使队列不存在/无法路由时 await 抛异常,走重入队路径。
 - 重试发布完整复制原消息属性(ReplyTo/Priority/Type/UserId/AppId 等,原实现仅复制部分)。
 - 移除上一版新增的“延迟队列声明 406 切换无 x-expires 兼容”分支(按用户要求,该场景无需处理)。
- 【修复】三平台消息构建器序列化与构建缺陷
 - 补 Newtonsoft [OnSerializing] 生命周期:默认 JSON 引擎是 Newtonsoft,仅实现 STJ 的
   IJsonOnSerializing 会导致链式消息在默认引擎下序列化出缺正文的协议体(推送内容全空)。
 - 修复 _built 缓存失效:Build() 后继续 Add/FromSystem/At 会失效构建缓存并重新构建,
   不再静默忽略新增项;null 值规范为空串,避免 value.Replace 空引用。
- 【协议修复】飞书(Lark)
 - interactive 卡片改为位于顶层 card 字段(原误放 content,不符合自定义机器人协议)。
 - At() 真正生成提及:text 内联 <at user_id="ou_xxx">,post 以 {"tag":"at"} 元素;
   标识语义修正为用户 open_id(原注释误称手机号/邮箱)。AtUsers 不再泄漏为顶层非协议字段。
 - 恢复被删除的 Post(title, content) 重载与 Interactive 可选按钮参数;
   Create(string) 恢复旧语义(内容作为正文 + 默认标题,原误改为“仅标题”)。
 - LarkMessageAdapter:text 的 content 改为对象(原误序列化为 JSON 字符串);
   移除顶层无效 at/mentions 字段,@改为协议内 at 标签/元素。
- 【协议修复】企业微信(WeChat)
 - Image 恢复为 base64+md5(群机器人协议,无 media_id)——修复上一版回归。
 - At() 真正生效:text → text.mentioned_mobile_list(手机号)/mentioned_list(userid);
   markdown 无 @ 字段,userid 以 <@userid> 内嵌正文。AtUsers 不再泄漏为顶层非协议字段。
 - WeChatMessageAdapter:mentioned_* 列表从顶层移入 text 对象(顶层字段无效)。
 - 恢复 News 图片 URL 可空、Create(string) 旧语义(默认标题 Markdown)。
- 【修复】DingTalkMessage:补 Interactive() 兼容别名(委托 ActionCard,文档早已宣称存在)。
- 【公共 API】IMQBuilder 补充 RetryBackoffLevels(params string[]),
 修复 .UseMQ(mq => mq.RetryBackoffLevels("1s","5s")) 因接口缺方法而无法编译的问题。
- 【性能/隔离】HttpFactory / HttpClientFactory 静态缓存加固
 - jsonFormat 隔离改用按实例的稳定身份编号(ConditionalWeakTable),修复“同类型不同配置
   实例共享 RestClient”与“不同命名空间同名类型误判为同一”的问题。
 - 命名/证书工厂缓存键纳入 URL+超时(+证书标识),修复“同名不同配置第一次配置永久胜出”的静默复用。
 - handler 隔离改用稳定实例身份(替代可碰撞的 RuntimeHelpers.GetHashCode)。
 - 容量淘汰加并发闸(Interlocked):同一时刻只允许一个线程执行全量扫描+批量淘汰,
   避免多线程同时触顶造成过度淘汰与缓存抖动。
 - 缓存键不含证书密码(避免敏感信息进入诊断字符串)。
- 【测试】新增/更新约 20 个测试:三平台四种 JSON 往返矩阵、confirm/mandatory 语义、
 按需单等级声明、默认交换机精确回投、完整属性复制、x-expires 钳制、缓存身份隔离等。
- 【文档】更正 release notes 与使用文档中不准确表述(406 兼容已移除、Interactive 别名、
 x-expires 语义、转发失败重入队而非“死信兜底”等)。

7.10.18:
- 【重构】消息构建器设计优化——职责分离、MQ 往返安全、API 更清晰
 - 设计原则:构建状态(_title/_items 等)保持私有 + 不序列化,消息体(MarkdownInfo 等)公开 + 可序列化
 - MQ 往返安全:IJsonOnSerializing 在序列化前自动调用 Build(),反序列化后消息体已有值时跳过重建
 - Build() 重构为 HasBuilderState() / HasBodyContent() / ResolveTitle() 三个职责清晰的方法
 - DingTalkMessage:新增交互式卡片工厂方法 Interactive(),清理冗余代码
 - LarkMessage:新增交互式卡片支持,工厂方法更丰富
 - WeChatMessage:新增图文消息 News()、文件消息 File() 工厂方法
 - 三个消息类同步重构,348/348 全绿
 - 彻底消除 MQ 往返丢失内容的风险(不再依赖 Build() 的 MQ 往返保护逻辑)

7.10.15:
- 【紧急修复】钉钉/飞书/企微推送内容为空(MQ 往返丢失构建状态)
- 【升级兼容】MQ 延迟队列声明 406 冲突处理(版本升级场景)
- 【重构】DingTalkMessage/LarkMessage/WeChatMessage 构建状态改为可序列化的公开属性
 - 问题:私有字段 (_title/_items/_content) 不会被 JSON 序列化,MQ 往返后内容丢失,
   Build() 因私有字段为 null 覆盖了反序列化的公开属性,导致推送内容为空
 - 重构:私有字段改为公开属性 (BuilderTitle/BuilderItems/BuilderContent/BuilderSystem),
   MQ 往返后构建状态完整保留,Build() 可正确重建内容
 - 彻底消除 MQ 往返丢失内容的风险,无需依赖 Build() 的 MQ 往返保护逻辑
 - 三个消息类同步重构,348/348 全绿

7.10.14:
- 【紧急修复】钉钉/飞书/企微推送内容为空(MQ 往返丢失构建状态)
- 【升级兼容】MQ 延迟队列声明 406 冲突处理(版本升级场景)

7.10.13:
- 【优化】延迟退避队列懒声明 + x-expires 空闲自动删除(解决管理界面队列堆积)
 - 懒声明:消费端启动不再创建延迟队列,首次消费失败发布前才幂等声明
   (60s 窗口缓存避免高频失败重复声明 RPC)——从未失败的业务队列零残留
 - x-expires:延迟队列空闲(空 + 无消费者 + 未重新声明)超时后 broker 自动删除,
   默认 24 小时,IMQSettings.RetryQueueExpiration 可配(TimeSpan.Zero 禁用)
 - 约束:过期时长必须大于最大延迟等级(默认 24h vs 最长 30m),消息等待期间不会被误删;
   发布前总会声明(重置计时),延迟消息不丢失
 - 新增测试:懒声明不创建队列 / 首次失败声明且带 x-expires / Zero 禁用,333/333 全绿

7.10.12:
- 【紧急修复】HttpFactory 序列化器对原始 JSON 字符串透传(修复推送内容全空)
 - 根因:7.10.10 自定义 RestSharp 序列化器(委托 IJsonFormat)遗漏了内置 SystemTextJsonSerializer
   对"string 值 + JSON ContentType"参数的原样透传逻辑——钉钉/飞书/企微推送走 AddJsonBody(string)
   → AddStringBody(原始JSON),原始 JSON 被二次序列化(加引号+转义),webhook 收到无效内容,推送内容全空
 - 修复:Serialize(Parameter) 对 string 值且 ContentType 含 json 的参数原样返回;非 JSON 仍走序列化
 - 回归测试:3 个(string 透传/对象序列化委托/真实 AddStringBody 参数结构),333/333 全绿

7.10.11:
- 【新功能】RabbitMQ 消费延迟退避重试(DLX + TTL 递增延迟)——统一退避语义,默认启用
 - 消费失败(handler 返回 false)→ 发布到 {queue}.retry.exchange → 按重试轮次路由到
   {queue}.retry.{level} 延迟队列(x-message-ttl + 死信指回原 exchange)→ TTL 到期自动回投原队列重试
 - 重试轮次记录在 x-retry-count 头(消息属性随死信回投延续),达 RetryCount 上限进最终死信/丢弃
 - 默认等级表 1s/5s/10s/30s/1m/5m/10m/30m;配置:UseMQ(mq => mq.RetryBackoffLevels("1s","5s"))
   全局或 IMQSettings.RetryBackoffLevels(消息级)
 - 与其他 MQ 统一:RocketMQ 由 broker %RETRY% 延迟等级天然接管(无需配置),RabbitMQ 由库内实现,
   不再引入引擎专属开关(保持 IMQSettings 跨 MQ 一致性)
 - 队列仍声明 x-delivery-limit(客户端从不 requeue,broker 计数不打断 TTL 回投,仅作极端兜底;
   存量队列升级声明参数不变,不触发 406)
 - 转发成功即 ack 原消息;转发失败进死信/丢弃(不 requeue 空转);属性保留(加密标记/业务头/MessageId 等)
 - 延迟队列为经典队列(TTL+死信延迟是经典能力),业务队列仍为 Quorum
 - 死信订阅语义不变:达上限进 {queue}.dlx.queue 由 DlxReceiveAsync 处理,处理失败直接丢弃不循环
- 【修复】HttpFactory 静态缓存未按 jsonFormat 隔离:不同 DI 容器以不同 UseJson 创建同 URL 工厂时,
 后注册者会复用前者的 RestClient(序列化器绑定了 jsonFormat),请求体序列化静默用错引擎——
 缓存 key 加入 |json:{类型名} 后缀;DisposeFactory 改前缀匹配清理全部变体

7.10.10:
- 【依赖加固】net6.0/net8.0 目标显式声明 System.Text.Json 9.0.0(修复 SystemTextJsonSerializer 加载 FileNotFoundException)
 - 根因:RestSharp 112/113 的 net6/net8 资产引用 STJ 6.0.0.0/8.0.0.0(共享框架自带),但其高版本资产
   (net9 编译,请求 9.0.0.0)若因部署目录残留/混入被加载,net6/net8 运行时无 9.0.0.0 → 必现崩溃
 - 修复:net6.0(netstandard2.0 资产)与 net8.0(net8 资产)显式依赖 STJ 9.0.0,nupkg 传递依赖自动
   输出 System.Text.Json.dll 9.0.0.0,任意 RestSharp 资产混入均可加载;net9/net10 共享框架自带无需
 - 用户侧无需再手动加 System.Text.Json 引用;若部署目录曾残留高版本 RestSharp.dll,请一并清理

7.10.9:
- 【根本性改进】RocketMQ 消费重试次数交给 broker 记录(消息属性 RECONSUME_TIMES),移除客户端本地内存计数
 - 旧实现:客户端 _retryCounts 字典按 MsgId 记次数 + 手动 Producer 转发 %DLQ%
 - 问题:进程重启 / 消费者 Rebalance / 多实例集群消费时内存计数丢失或分散,同一消息被重复转发进 %DLQ%
 - 新实现:失败时主动 SendMessageBackAsync 请求 broker 转投——未达 RetryCount 上限 -> %RETRY%{group}
   按延迟等级(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)重投,等待下游服务恢复;
   已达上限 -> broker 判定 RECONSUME_TIMES >= maxReconsumeTimes 原子转投 %DLQ%{group}
 - 转投成功即 ack(offset 推进,无重复回调);转投失败不 ack(下个消费周期重试,消息不丢失)
 - 死信前缀白名单语义不变:已达上限且前缀不匹配 -> ack 丢弃,不进入 %DLQ%
- 【关键修复】Consumer.EnableRetry 必须为 false(实测验证)
 - NewLife 自动重试在 offset 不推进时对同一条消息无限重复回调并重复转投副本(真实 broker 实测 90 秒回调 4279 次、全部 rt=0)
 - 重试动作由 HandleFailureAsync 统一接管,杜绝重复回调与 %RETRY% 副本堆积
- 【删除】RocketMQMessageHelper.GetRetryDelay(客户端指数退避不再需要,broker 延迟等级接管重试节奏)
- 【测试】死信前缀测试对齐新语义:达上限+前缀不匹配 -> ack 丢弃;未达上限/达上限+前缀匹配 -> SendMessageBack 转投失败不 ack(+6 测试,281/281 全绿)

7.10.7:
- 国密算法支持(SM4 对称 + SM2 非对称),基于 BouncyCastle
- 多套加密并行注册(UseAES+UseSM4+UseSM2),IEncryptProviderFactory 按名称解析
- 修复多套并行时 EncryptOption 单例覆盖(每个 Provider 独立配置实例)
- 注册风格与 MQ 一致:UseEncryption(s => s.UseSM4(m => m.FromConfiguration()))
- SM4: CBC+随机IV+Magic头+DoS防护;SM2: 单次上限检查+双密钥格式
- 新增 11 个国密单元测试(加解密往返/随机IV/大数据/非法密钥/超限)

7.10.6:
- 【新功能】统一消息模型 PushMessage + 渠道适配器(钉钉/飞书/企业微信)
 - 业务代码用 PushMessage 构建消息(渠道无关),推送时自动转换为各渠道 webhook JSON
 - 切换渠道(钉钉→飞书→企业微信)业务代码零改动,只换 AddDingTalk/AddLark/AddWeChat 注册
 - IApiPush 新增 Push(PushMessage)/PushAsync(PushMessage) 统一入口(默认接口实现)
 - 支持 Markdown/Text/Link/ActionCard/FeedCard 五种消息类型
- 【新功能】消息构建器统一接口 IMessageBuilder{T}(一致性)
 - PushMessage/DingTalkMessage/LarkMessage/WeChatMessage 均支持 Add 字典、Add 实体、At 用户
 - Add(IDictionary) 批量键值对、Add(TEntity) 属性自动转键值对
 - 修复泛型重载决议问题(传入 Dictionary 时运行时分流,避免被当实体反射)
- 【根本性改进】移除 RocketMQ 探针消息机制(LORD_SERVICE_DLQ_PROBE)
 - 根因:探针用于创建 %DLQ% 主题,但 RocketMQ Consumer 会自动订阅 %RETRY%/%DLQ% 队列,探针被业务消费 → 解密失败 → 无限重试
 - 验证:Consumer 订阅不存在的 Topic 时客户端会重试等待,失败消息转发时 Producer 发布(autoCreateTopic)自动创建 Topic,探针完全不需要
 - 保留存量探针跳过检查(兼容清理旧版本积压)
- 【看门狗】修复消费停滞检测误判:生产端正常无消息时被误判"停滞"导致每 5 分钟反复重建 consumer
 - 主检查改为 consumer 连接活性(Active/Disposed),停滞检测改为 handler 卡死检测(处理中标记 _processingTick,30 分钟阈值)
- 【看门狗】修复 Dispose 竞态:等待恢复任务结束再释放锁(避免 ObjectDisposedException)
- 【语义修复】Consumer.Tags 恢复 null(RocketMQ 协议缺省=不过滤),移除 Array.Empty 赋值(空数组可能导致过滤语义变化)
- 【新功能】消息级加密开关(IMQSettings.EnableEncryption + EncryptionPrefixes 前缀白名单)
 - 无需敏感信息的消息可关闭加密(提升性能、避免加解密配置不匹配)
 - 支持按队列名前缀白名单只加密部分队列(如 "secret_;private_")
 - 生产端与消费端使用相同队列名时自动保持一致的加密策略
 - RocketMQConfig/RabbitMQConfig 提供全局默认值(Topic 驱动接口场景)
- 【重构】提取 RocketMQMessageHelper(探针识别/重试退避)与 MQEncryptionHelper(加密策略),消除重复代码
- 【新功能】UseMQ 全局加密配置(引擎无关:RabbitMQ/RocketMQ/未来 Kafka)
 - mq.EnableEncryption(false) 全局关闭加密;mq.EncryptionPrefixes("secret_", "private_") 白名单前缀
 - 通过 PostConfigureAll 应用到所有已注册引擎,Factory 创建 settings 时自动继承
 - RabbitMQ 生产/消费链路已接入加密开关(RabbitMQPush.ApplyEncryption / RabbitMQReceive.GetMessage)
 - 优先级:IMQSettings(消息级) > UseMQ 全局配置 > appsettings 配置
- 【新功能】RocketMQ Builder 新增 ACL/云厂商/SSL 链式配置
 - WithAcl(accessKey, secretKey) Apache ACL 认证
 - WithCloudProvider(Aliyun/Huawei/Tencent, ak, sk, extra) 云厂商托管
 - WithSsl(true) SSL/TLS 开关
- 【测试】新增 MQBuilder 全局加密配置测试 + RocketMQ Builder ACL 测试 + 全部 MQ 相关测试通过(42 个)
- 【全面审查修复】Redis/加密/推送格式/工具类 30+ 个 bug
 - 修复 DingTextFormat @所有人逻辑写反(未指定 users 时不再误 @全员)
 - 修复 LarkTextFormat 富文本消息必然抛异常(style 对象缺属性名)
 - 修复 Redis AddOrGetCacheItem 信号量超时破坏(Wait 返回值检查,防 SemaphoreFullException)
 - 修复 Redis ClearAsync 误清整库(无前缀无 pattern 时拒绝清理,与同步版一致)
 - 修复 Redis DateTime 时区偏移(保持 UTC Kind,不再 ToLocalTime)
 - 修复 object 类型缓存读写不对称(读侧先还原原始值再走 JSON)
 - 修复 RedisDistributedLock 前缀双冒号问题(TrimEnd(':'))
 - AES-GCM 解密增加认证前长度上限(防内存 DoS)+ 密钥解码缓存(性能)
 - RSA 加密增加单次上限检查(190 字节明确报错,提示用 AES)
 - DES 启用时输出安全警告(56 位密钥可破解,仅建议兼容旧数据)
 - 修复 DeepCopy 循环引用防护失效(AsyncLocal 深度计数,循环引用抛明确异常而非 StackOverflow)
 - 修复 MemoryCache.Increment 类型判断(int/uint 等数值类型也能正确递增)
 - 修复 TypeConvertUtil 数组/object 分支畸形 JSON 抛异常(统一返回 default)
 - 修复 LarkTextFormat 卡片 config/header 序列化为字符串(改为对象结构)
- 【HTTP 类库审查修复】性能/稳定性/易用性全面优化
 - 修复 NetHttp SendAsync 无限递归 bug(调用自身导致死循环,单元测试捕获)
 - 修复 NetHttp 流式响应生命周期 bug(response 提前 Dispose 导致流关闭)
 - RestHttp 与 NetHttp 重试策略对齐:POST 默认不重试(防重复提交)+ 4xx 不重试(429 限流除外)
 - IHttpRequest 新增 AllowRetryNonIdempotent(显式允许非幂等重试)
 - IHttpStreamResponse 实现 IDisposable(流使用后可释放)
 - AddJsonBody(string) 语义统一:原始 JSON 原样发送(修复二次转义问题)

7.10.5:
- IMQProvider 性能优化(懒加载缓存 Hub/Factory/IJsonFormat,避免重复 DI 解析)
- 修复 RocketMQSettings 默认值点号问题(RocketMQ Topic/Group 不允许点号,改为下划线)
- 文档新增 IMQProvider 使用指南(一站式入口 + 能力检查 + MQTT 扩展指引)

7.10.4:
- IMQProvider 新增细粒度能力检查(SupportsTransactionMessages/SupportsDelayedMessages/SupportsRequestReply/SupportsSql92Filtering/SupportsOrderedMessages)
- IMQProvider 新增便捷方法(GetPublisher/GetSubscriber/GetHub/GetFactory),一站式获取所有 MQ 服务
- 修复 RabbitMQ 事务消息"静默降级"问题(现在可通过 SupportsTransactionMessages 提前检查)
- 修复 RabbitMQ 延迟消息"静默降级"问题(现在可通过 SupportsDelayedMessages 提前检查)
- 新增 RocketMQCloudHelper 共享帮助类,减少 160+ 行重复代码

7.10.3:
- 修复 RocketMQPublisher 缺少 Producer 活性检查问题(添加 IsProducerAlive 双检锁,与 RocketMQPush 对齐)
- 修复 EnsureFailedTopicAsync 中 Producer 资源泄漏(改用 finally 确保释放)
- 抽取 ConfigureCloudProvider 到共享帮助类 RocketMQCloudHelper(减少 160 行重复代码)
- 优化 CleanupExpiredRetryEntries 避免 LINQ 分配(改用 foreach 直接遍历)
- 文档版本同步更新至 v7.10.3

7.10.2:
- 修复 RocketMQ 模块 116 个 nullable reference 警告(提升类库质量)
- 修复 _retryCounts 字典无限增长问题(添加基于时间的自动清理机制)
- 修复 _failedProducer 资源泄漏问题(Dispose 时正确释放)
- 修复 _failedProducer 并发初始化竞态条件(双检锁保护)
- 优化 typeNameCache 缓存淘汰策略(达到上限时清空重建,确保新类型能被缓存)
- 修复 Tags 属性 null 赋值问题(改用 Array.Empty 兼容 C# 10)
- 修复 Type.FullName 可能为 null 的问题(添加回退值)
- 修复 message.Tags/Keys 可能为 null 的问题(添加默认值)

7.10.0:
- AES 加密改用随机 IV(每次加密生成新 IV,向后兼容旧数据)
- 新增 Polly 8 弹性策略(LordResilience),MQ 连接重试改为指数退避+抖动
- HTTP 重试改为指数退避+随机抖动(RestSharp + HttpClient 双实现)
- 移除 RabbitMQFactory/RabbitMQService 终结器(消除 GC 线程死锁风险)
- _timedOutTags 添加基于时间的自动淘汰(心跳中清理超过 5 分钟的泄漏条目)
- 修复 LarkPush.PushAsync 空路径 bug(异步飞书推送发送到错误端点)
- 修复 DingTalkPush/LarkPush 并发竞态条件(GetAuthentication 修改共享对象)
- 修复 WeChatPush/LarkPush 死代码和 null 检查不一致

7.0.8:
- IMQHub 新增显式名称参数重载(PublishAsync/SubscribeAsync/SubscribeDeadLetterAsync)
- 新增 MQNameAttribute,支持为消息类型指定自定义名称前缀
- IMQHub 中文 XML 注释
- RabbitMQ Prefix 前缀隔离(多系统共用 broker 时区分队列名)
- RabbitMQ 消费端看门狗重写(心跳检测+无限重试+指数退避+Channel 重建)
- 生产端 RabbitMQPush 新增 Channel 自动恢复机制
- 死信队列(DLX)纳入看门狗保护
- RSA 改为标准保密模式(加密用公钥,解密用私钥)
- 默认加密算法从 DES 改为 AES
- 修复缓存 AddOrGetCacheItem 默认值误判问题
- 修复服务工厂缓存 token 污染问题
- 修复消费端异常消息无限 requeue 问题
- 移除 FreeRedis 支持(聚焦 StackExchange.Redis)