Mud.Wechat.Redis 1.0.0

dotnet add package Mud.Wechat.Redis --version 1.0.0
                    
NuGet\Install-Package Mud.Wechat.Redis -Version 1.0.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="Mud.Wechat.Redis" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mud.Wechat.Redis" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Mud.Wechat.Redis" />
                    
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 Mud.Wechat.Redis --version 1.0.0
                    
#r "nuget: Mud.Wechat.Redis, 1.0.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 Mud.Wechat.Redis@1.0.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=Mud.Wechat.Redis&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Mud.Wechat.Redis&version=1.0.0
                    
Install as a Cake Tool

Mud.Wechat.Redis

跨产品线的 Redis 分布式存储扩展包:为多实例分布式部署提供五个存储端口的 Redis 实现, 适用于多实例共享令牌、跨实例持久化企业授权(永久授权码)、多实例共享 suite_ticket、回调抗重放 与智能机器人长连接主备租约;另按 A-5 分层共存裁决(上游对账清单,2026-10-14)承载 通用 ITokenStore 面(官方 Mud.HttpUtils.Redis 的 RedisTokenStore,随本包一并注册), 并注册 U-3 跨进程刷新租约(官方 RedisRefreshLease,AddWechatRedis 注册即启用—— 各线令牌 / 票据刷新在多实例间去重,未抢到租约的实例限时轮询读穿透、超时 fail-open)。

五个端口中,令牌存储 / 企业授权 / 套件票据 / 长连接租约来自企业微信线(Mud.Wechat.Work.Abstractions), 回调抗重放端口 IWechatCallbackReplayGuard 已下沉到 Mud.Wechat.Abstractions 叶层, 故企微 / 公众号 / 支付三条产品线的回调接收面共用同一个 Redis 实现。

对应 Mud.Feishu.Redis 的同构模块(连接基座 / 键构造护栏 / DI 编排模式一致,存储层按微信契约重设计)。

覆盖的存储端口

端口 Redis 实现 多实例语义
IWechatTokenStore RedisWechatTokenStore 令牌跨实例共享;实现 IWechatTokenStoreBatchRemove(W1/M10 批量删除,N 次往返收敛为 ⌈N/500⌉ 次)与官方 ITokenStoreAppPurge(U-4 按应用清库)
IWechatCorpAuthStore RedisWechatCorpAuthStore 企业授权(含不可重取的永久授权码)跨实例持久化,重启不再丢授权
IWechatSuiteTicketStore RedisWechatSuiteTicketStore suite_ticket 跨实例共享——不再只有收到推送的实例能签发套件令牌
IWechatCallbackReplayGuard RedisWechatCallbackReplayGuard 回调抗重放窗口跨实例生效(进程内实现多实例下重放窗口失效);企微 / 公众号 / 支付三条回调接收面共用此端口
IWechatBotConnectionLease RedisWechatBotConnectionLease 智能机器人长连接(官方 101463)主备租约跨实例互斥——多实例部署必须注册本包,避免多实例同时订阅同一 Bot 的推送帧
ITokenStore(通用面) RedisTokenStore(官方 Mud.HttpUtils.Redis,F-3) 通用令牌持久化 SPI 的官方落地;随 AddWechatRedis 以 TryAddSingleton 注册(宿主预注册者胜)
ITokenRefreshLease(通用面) RedisRefreshLease(官方 Mud.HttpUtils.Redis,U-3) 跨进程令牌刷新去重租约(SET NX PX 获取 + compare-and-del 释放,fail-open);随 AddWechatRedis 以 TryAddSingleton 注册,各线管理器装配自动透传

分层共存(A-5):两个令牌面为何同时存在

  • 本院令牌面(IWechatTokenStore → RedisWechatTokenStore):键 {本院前缀}:token:{storeKey} (三段式,与 InMemoryWechatTokenStore 键空间逐字节一致),值由组件桥接器写官方相对秒形态 {expiresInSeconds}|{token}(U-1 起;读取方兼容存量旧绝对戳与官方 v2 信封);实现 IWechatTokenStoreBatchRemove 批删扩展(W1/M10)与官方 ITokenStoreAppPurge (U-4 按应用清库,见下)。
  • 通用令牌面(ITokenStore → 官方 RedisTokenStore):键 {官方前缀}:{tokenType}:{appKey}:access|refresh (默认前缀 mud:httputils,配置节 WechatRedis:TokenStore),值经官方 ITokenStoreCodec ({expiresInSeconds}|{token} 相对秒;U-2 起可选 v2 版本化信封 v2:{秒}|{token}——读取面无条件兼容、 写入面经 TokenStore.UseVersionedEnvelope 两阶段选开,默认关闭);官方 ITokenStore 维持最小 SPI,不实现微信类型与任意键批删 (A-5 明令)——U-4 起官方另有按应用清库面 ITokenStoreAppPurge(3.0.7 重打包新增;官方 RedisTokenStore 与本包 RedisWechatTokenStore 实现之,InMemoryWechatTokenStore 不再实现—— 9c16ff8 起管理器清库走 GetTokenTypesAsync 匹配 + IWechatTokenStoreBatchRemove 批删,该接口不再是管理器依赖); 企微侧 PurgeAppTokensAsync 因此只依赖 GetTokenTypesAsync + 批删,不再做接口探测。红线:本院管理器绝不对未实现官方接口的 store 调官方 TokenStorePurgeExtensions.PurgeAppTokensAsync 扩展——其回退路径忽略 appKey, 会误清全部应用(任意键批删仍无官方面,B-3 逐键回退语义仅剩此扩展一处)。
  • 键命名空间互不干扰(前缀不同)⇒ 共库无需数据迁移;IWechatTokenStore : ITokenStore 为薄标记 (二进制兼容保留),新增代码按 A-5 直接依赖 ITokenStore。两个面解析为不同实例属分层设计而非缺陷。

公众号线复用同一 IWechatTokenStore 端口承载 access_token 与 jsapi_ticket / wx_card 票据 (键空间分别以 Wechat.Mp.JsApiTicket:{AppKey} / Wechat.Mp.WxCardTicket:{AppKey} 隔离),故装配本包后 公众号令牌与票据同样跨实例共享,冷启动直接命中其它实例写入的票据——本包不引用公众号工程,跨线语义经端口接口达成。

五个本院端口的默认实现均为进程内(TryAddSingleton 注册)——其中令牌 / 企业授权 / 套件票据 / 长连接租约四个端口由企业微信基座注册, IWechatCallbackReplayGuard 现由各回调宿主包(Work.Callback / OfficialAccount.Callback / Pay.Callback)各自注册。 本包先注册即胜出:

// Program.cs —— 完整装配:顺序即契约,Redis 必须最先
builder.Services
    .AddWechatRedis(builder.Configuration)                 // ① 连接基座 + 五个存储端口 + 官方 ITokenStore / ITokenRefreshLease 层(默认注册健康检查)
    .AddWechatApp(builder.Configuration, "WechatApps")     // ② 令牌/授权/票据基座(Redis 实现已就位)
    .AddWechatCallback(o => { /* 回调凭据与处理器 */ });    // ③ 回调(抗重放窗口跨实例生效)

var app = builder.Build();
app.MapHealthChecks("/health");                            // 健康检查端点(唯一判据 PING,端点连通数仅作 data 呈现)

app.Run();

调用顺序 = 契约:颠倒顺序时 TryAdd 语义会让 Redis 实现静默失效(默认进程内实现仍然生效且无任何错误)。 AddWechatRedis 在注册期探测「应用注册已在装配」的信号并 fail-fast:企微侧探测 IWechatAppManager (AddWechatApp / AddWechatWorkServices)、公众号侧按接口全名探测 IMpAppManager(AddMpApp)、 回调侧按全名探测 InMemoryWechatCallbackReplayGuard(AddWechatCallback / AddMpCallback / AddWechatPayCallback)。 后两者取全名字符串而非类型引用,是本包不引用各产品线程序集(保持产品线隔离)的代价——全名漂移即顺序守卫静默失效, 由契约守卫 RD-G5 / RD-G6 以反射锁定。宿主自行预注册的自定义实现按契约优先于本包(TryAdd 语义),不受顺序守卫影响。

配置(配置节 WechatRedis)

{
  "WechatRedis": {
    "Connection": {
      "ServerAddress": "localhost:6379",   // host:port / redis:// / rediss://(后者显式启用 TLS)
      "Password": "",
      "Ssl": false,
      "DefaultDatabase": null,
      "ConnectTimeout": 5000,
      "SyncTimeout": 5000,
      "ConnectRetry": 3,
      "AbortOnConnectFail": true
    },
    "Advanced": {
      "AllowAdmin": false,
      "ClientName": null                    // 兜底 Wechat-Redis-{MachineName}
    },
    "KeyPrefix": "wechat",                  // 本院各类键共享的唯一前缀旋钮(含 U-3 租约派生前缀 {KeyPrefix}:lease);多环境隔离改此值
    "SuiteTicketTtl": "00:00:00",           // 默认不过期(覆盖写保新鲜);正值建议 ≥ 20 分钟
    "TokenStore": {                         // 官方 Mud.HttpUtils.Redis 令牌存储映射(A-5 分层共存)
      "KeyPrefix": "mud:httputils",         //   官方键前缀(与本院 wechat 前缀互不干扰)
      "AppKey": null,                       //   官方命名空间段;空 ⇒ shared(单应用场景)
      "DefaultTtl": null,                   //   兜底 TTL;生产建议显式配置(如 30 天)防无界滞留
      "Database": -1,                       //   官方存储库编号;-1 = 连接默认库
      "UseVersionedEnvelope": false,        //   U-2:写入侧 v2 信封(两阶段:先升级全部读取实例;默认关闭)
      "AcknowledgeVersionedEnvelopeReadersUpgraded": false  // U-2:开启上项时的显式确认,未确认双闸 fail-fast
    }
  }
}

代码配置形态:services.AddWechatRedis(o => o.Connection.ServerAddress = "localhost:6379");

两个重载均可选传 sectionName(配置节名,默认 WechatRedis)与 registerHealthCheck(默认 true; false 时仅注册 RedisHealthCheck 类型,由宿主自行 AddCheck)。

校验:IValidateOptions<WechatRedisOptions> + net6+ ValidateOnStart()(ns2.0 在首次解析 options 时触发)。 连接初始化失败、超时、服务端错误与非法参数统一包装为 WechatRedisException(Kind ∈ WechatRedisFailureKind.{Connection,Timeout,Server,InvalidArgument}),异常消息只携带脱敏后的配置描述。

IConfiguration 重载在 net6+ 标注 RequiresUnreferencedCode / RequiresDynamicCode(反射式配置绑定), 编程式委托重载无此标注——AOT 宿主优先用委托形态。

键空间

存储 Redis 键 TTL
令牌(本院面) {KeyPrefix}:token:{storeKey} 跟随调用方 expiresInSeconds;<= 0 时删键
令牌(官方面) {TokenStore:KeyPrefix}:{tokenType}:{TokenStore:AppKey}:access\|refresh 访问令牌跟随 expiresInSeconds(缺省回落 TokenStore:DefaultTtl);刷新令牌走 DefaultTtl
企业授权 {KeyPrefix}:corpa:{appKey}:{authCorpId} 不过期(轮换/撤销显式删)
套件票据 {KeyPrefix}:ticket:{suiteId} SuiteTicketTtl(默认不过期)
回调重放 {KeyPrefix}:replay:{fingerprint} 调用方传入的窗口(±300s)
刷新租约(U-3) {KeyPrefix}:lease:{leaseKey} 租约 TTL(默认 30s;leaseKey = 管理器 {MetricsKey}:{scopeKey})

键名仅含 appKey / authCorpId / suiteId / SHA1 指纹 / 租约 scopeKey——均非凭据,可安全入日志。 令牌 storeKey 为三段式 {tokenType}:{appKey}:{scopeKey}(与 InMemoryWechatTokenStore 键空间逐字节一致, PurgeAppTokensAsync 三段解析依赖此不变量)。 官方面 tokenType 段允许含 :(官方 RedisTokenStoreKeyBuilder 从右侧剥除固定段,天然单射)。

回调抗重放(fail-closed)

Redis 实现的 TryMarkAsync 在存储故障时分类包装上抛:外层为叶层契约异常 WechatCallbackInfrastructureException(回调中间件的专用 catch 映射 503,触发官方 96238 重推; 严禁与验证类失败的 403 混流——Redis 故障期 403 会静默丢事件),WechatRedisException 作为 InnerException 保留(FailureKind 分类语义与既有宿主捕获面零破坏)。接收器无捕获面, 显式禁止吞异常返回 true(放行重放)或 false(静默丢事件且官方不再重试)。

健康 / 预热 / 生命周期

  • 健康检查:默认注册(registerHealthCheck: false 可关闭,仅注册类型由宿主自行 AddCheck); 唯一判据为 PING,端点连通数仅作 data 呈现。
  • 启动预热:IHostedService 启动期 PING;AbortOnConnectFail=true(默认)时 PING 失败终止启动 (fail-fast),否则告警放行交由自动重连。
  • 连接事件:ConnectionFailed / ConnectionRestored 记录日志;连接初始化失败的异常消息只携带 脱敏后的配置描述(Password 掩码)。

AOT / Trim

  • net8.0+ 支持 Native AOT 发布(AotStrictMode=true 门禁全绿;StackExchange.Redis 3.4.5 已实测)。
  • 企业授权聚合序列化:net8+ 走 AuthenticationJsonContext(JsonTypeInfo,零反射); ns2.0/net6 为反射 STJ(AOT 门禁仅对 net8.0 构建,低 TFM 做 AOT 非受支持场景)。
  • 配置绑定走源生成器形状(Configure<T>(o => section.Bind(o)));IConfiguration 入口标注 RequiresUnreferencedCode / RequiresDynamicCode(与 AddWechatApp 同款)。

集成测试

真实 Redis 的端到端用例由环境变量门控(CI 默认不跑):

WECHAT_REDIS_TESTS_CONNECTION=localhost:6379 dotnet test Tests/Mud.Wechat.Redis.Tests

目标框架与 SE.Redis 分档锚定

netstandard2.0 / net6.0 / net8.0 / net10.0(继承根 Directory.Build.props)。官方 Mud.HttpUtils.Redis 3.0.7 重打包起逐档要求 SE.Redis ≥3.4.5(net8+)/ ≥2.13.17(低 TFM), 本包随之分档锚定(T1,2026-10-15):net8.0/net10.0 → 3.4.5、netstandard2.0/net6.0 → 2.13.17(含原生 net6.0 资产)。net6.0 下 System.IO.Hashing 9.0.12 经 SE.Redis 2.13.17 传递 产生「未测试 TFM」咨询性提示(非功能缺失),故仅该 TFM 设 SuppressTfmSupportBuildWarnings。

SE.Redis 分档已知陷阱

  • 超时异常继承关系(B-8 复核,2026-10-15 反射实测):上游 B-8 称「3.x 中 RedisTimeoutException 不再继承 TimeoutException」,但本仓对全部在盘版本(2.8.24 / 2.13.17 / 3.1.31 / 3.3.0 / 3.4.5)实测 结论一致:RedisTimeoutException 恒继承 System.TimeoutException、恒不派生 RedisException (派生 RedisException 的是 RedisConnectionException / RedisServerException)⇒ 无「翻转」, 2.x 的 catch (TimeoutException) 习惯在 3.4.5 下依然有效。操作口径不变(更稳):宿主 catch 用 RedisTimeoutException 或 RedisException 基类;本包 WechatRedisErrors.ShouldWrap 的 is RedisException or RedisTimeoutException 显式双类型因「超时异常不派生 RedisException」而 必需。分歧待经 F-6 通道回写上游对账清单。官方 Mud.HttpUtils.Redis 3.0.7 重打包起逐档引用 SE.Redis(net8+ ≥3.4.5 / 低 TFM ≥2.13.17)、不再固定 2.8.24。
  • StringSetAsync 有多套重载形态:生产调用一律用命名参数钉住(keepTtl: false / when: …), 否则参数位置漂移会静默改变「是否覆盖 TTL」的语义。

键构造与批量

WechatRedisKeyBuilder(internal)是唯一键名出口:段上限 256、: 转义为 \:、模式串以 :* 结尾、 TryStripTokenKeyPrefix 负责三段式令牌键反解析。按应用清库(U-4)走官方 ITokenStoreAppPurge: EnumerateKeysAsync 返回完整快照(SCAN 全量遍历、Cluster 聚合全部主节点、ScanPageSize = 250 分页, 口径与 GetTokenTypesAsync 一致)+ PurgeAppAsync 复用 RemoveRangeAsync 批删并返回实际删除数 (中途失败分类上抛、不得静默半删),空白 appKey 返回空集 / 0。任意键批删走可选端口 IWechatTokenStoreBatchRemove (ScanPageSize = 250 游标分页 + DeleteBatchSize = 500 每批删除,N 次往返收敛为 ⌈N/500⌉ 次), 未实现该端口的存储回退逐键删除;企业授权读批 ReadBatchSize = 500(MGET 批读,避免 SCAN 后逐键 GET 的 N+1)。

契约守卫

Tests/Mud.Wechat.Redis.Tests/ContractGuards/ 的 RD-G1 ~ RD-G7 锁定:SCAN 模式仅经 WechatRedisKeyBuilder.Pattern 产出(G1)、配置 DTO 禁 required(G2)、重放守卫 fail-closed(G3,存储故障必须上抛)、敏感凭据不得进入日志调用点(G4)、 顺序守卫的两处探测全名与被探测类型 FullName 一致(G5 回调 InMemory 实现 / G7 公众号 IMpAppManager,反射断言——顺序守卫的生命线)、 本包单依赖形态(G6:不得引用 Callback / 各线主包)。功能用例为 T-R1 ~ T-R10(T-R10 = 官方面分层共存: 键布局 / 配置映射 / 两层互不干扰 / 官方面不实现本院任意键批删接口 IWechatTokenStoreBatchRemove—— U-4 官方另实现按应用清库面),T2 起另有 U-4 按应用清库等价用例 (TokenStoreBehaviorEquivalenceTests.PurgeApp_*:目标应用命中集 + 返回真实删除数 + 空白 appKey 空转)、 U-3 注册与派生前缀用例(WechatRedisServiceCollectionExtensionsTests:单例形态 / 预注册者胜 / {KeyPrefix}:lease 派生 / 自定义前缀跟随);三层探测与红线守卫在 Work.Abstractions 侧 (WechatAppManagerTests.PurgeAppTokensAsync_*:官方 purge 优先 → 本院批删 → 逐键,且未命中能力层时 不得调官方扩展)。真实 Redis 端到端由 RedisIntegrationTests 环境变量门控(假后端在 Support/FakeRedisBackend.cs)。 改动端口实现、键空间或注册顺序守卫时须同批更新这些守卫。

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
1.0.0 59 10/29/2026