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
<PackageReference Include="Mud.Wechat.Redis" Version="1.0.0" />
<PackageVersion Include="Mud.Wechat.Redis" Version="1.0.0" />
<PackageReference Include="Mud.Wechat.Redis" />
paket add Mud.Wechat.Redis --version 1.0.0
#r "nuget: Mud.Wechat.Redis, 1.0.0"
#:package Mud.Wechat.Redis@1.0.0
#addin nuget:?package=Mud.Wechat.Redis&version=1.0.0
#tool nuget:?package=Mud.Wechat.Redis&version=1.0.0
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.Redis3.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.Redis3.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 | Versions 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. |
-
.NETStandard 2.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Mud.HttpUtils.Redis (>= 3.0.9)
- Mud.Wechat.Work (>= 1.0.0)
- StackExchange.Redis (>= 2.13.17)
- System.Text.Json (>= 10.0.12)
-
net10.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Mud.HttpUtils.Redis (>= 3.0.9)
- Mud.Wechat.Work (>= 1.0.0)
- StackExchange.Redis (>= 3.4.5)
-
net6.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Mud.HttpUtils.Redis (>= 3.0.9)
- Mud.Wechat.Work (>= 1.0.0)
- StackExchange.Redis (>= 2.13.17)
-
net8.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Mud.HttpUtils.Redis (>= 3.0.9)
- Mud.Wechat.Work (>= 1.0.0)
- StackExchange.Redis (>= 3.4.5)
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 |