跳到正文
博客园· 玩泥巴的|mudtools.cn·· 2 小时前AI 评分38

微信生态 .NET 服务端 SDK:7 条产品线、18 个 NuGet 包、2200+ 端点落地

可能是最现代化的微信生态 .NET 服务端 SDK - 玩泥巴的|mudtools.cn

AI 导读

一套覆盖企业微信、公众号、小程序、微信支付、开放平台、微信小店·视频号、腾讯广告 7 条产品线的微信生态 .NET 服务端 SDK 已完成收尾,拆分为 18 个 NuGet 包按需引用,六线合计 2200+ 官方端点逐页核验落地。

正文

一、引子

上次写完飞书 SDK,微信这边接着写。到收尾的时候,七条产品线都齐了。这篇记录一下它是怎么搭起来的。

标题里的「现代化」指的是六件具体的事:源生成零反射、Native AOT 原生、DI 原生装配、Options 热更、OpenTelemetry 内建、AI/MCP 就绪。每件都有对应的代码和测试。

规模是这样:

  • 7 条产品线:企业微信、公众号、小程序、微信支付(APIv3 + APIv2)、开放平台、微信小店·视频号、腾讯广告
  • 18 个 NuGet 包,一线一包、按需引用
  • 2200+ 官方端点已逐页核验落地(六线合计),企微线另有 470 个声明式接口、覆盖 42 个业务域
  • 8500+ 源文件,650+ 测试文件,20 个测试工程
  • 全仓 Native AOT 严格模式构建通过,四档 TFM(netstandard2.0 / net6.0 / net8.0 / net10.0)

二、为什么要再写一套

飞书 SDK 那套形态在飞书线跑过一轮:声明式接口、源生成、守卫测试。搬到微信,工程方法不用重新设计,要换的是生态和契约。真正要回答的是:微信这边有什么不一样的要求。

市面上不缺微信 SDK,我自己用现成方案时卡在三件事上:

AOT / 裁剪发布。 Native AOT 正在变成 .NET 后端的常规选择:启动快、内存低、部署物小。但 SDK 内部要是靠反射做序列化,AOT 下要么不可用,要么得额外折腾。这类问题往往到发布阶段才暴露,返工成本高。

按需引用。 我只想用微信支付,不想因此背上整个 SDK 的传递依赖。不少方案的形态是「一个大包」或「一个仓库一个工程」,边界靠文档约定,物理上拦不住。

契约正确性。 微信生态的端点以千计。手抄官方文档逐个实现,抄错路由、抄错字段名、令牌传错位,这些都要到运行期才暴露。官方文档本身又庞大、分多个站、还有自相矛盾的地方(后面细说),「抄对了」需要验证手段。

对应的解法分别是源生成序列化加 AOT 门禁、一线一包加守卫锁定边界、契约守卫测试,第四节展开。

另外有几件事我刻意不做:不追链式 API 语法糖;不承诺「五分钟接入」,微信生态的复杂度不会因为 SDK 藏起来就消失;已下架或判非契约的端点不实现,逐页取证后记零声明,跟「还没做」区分开。

三、微信生态有多难搞

3.1 七条线,各自独立

「微信开发」不是一个东西,是至少七条独立产品线:

flowchart TB subgraph LINES["七条产品线 · 一线一包(各自独立令牌 / 回调 / 文档站)"] direction LR Work["企业微信<br/>470 接口"] OA["公众号<br/>266 路由"] MP["小程序<br/>588 端点"] Pay["微信支付<br/>625 + 71 端点"] OP["开放平台<br/>208 端点"] CH["小店 · 视频号<br/>394 端点"] Ads["腾讯广告<br/>48 端点"] end subgraph CORE["跨线共享基建(Core 叶层)"] direction LR Abstractions["Abstractions<br/>令牌存储端口 · 回调密码学 · SSRF 白名单"] Redis["Redis<br/>分布式存储"] OTel["OpenTelemetry<br/>可观测性装配"] end LINES -->|全部依赖| CORE

各线令牌体系不同(Wechat.AccessToken / Wechat.Mp.AccessToken / Wechat.Channels.AccessToken / 支付商户证书签名……),回调协议不同(XML+AES 信封 / 官方强制 JSON 安全模式 / 平台证书验签),文档站各自独立。支付线内部甚至有两套并存的协议(APIv3 是 JSON + 商户私钥签名,APIv2 是 XML + 双向证书),两者不是替代关系。

3.2 企微一侧的三套令牌链

仅企业微信一条线,就有三种应用模式,各走不同的凭据链:

应用模式 凭据链 说明
自建应用 corpid + corpsecret → access_token 最常见
第三方套件 provider_access_token + suite_access_token + 每授权企业 access_token 服务商模式,三段式
服务商代开发 同上;企业令牌用 permanent_code 换取 模板 id 即 suite id

企业级令牌按授权企业分作用域(一企一份),多套件部署时各套件的授权记录互相独立。这些不是「高级用法」,是做服务商系统时的日常。

3.3 加解密的官方特有陷阱

企微回调的 AES 加密使用 32 字节块手工 PKCS7 补位,而 .NET 内置的 PaddingMode.PKCS7 是 16 字节块。直接用内置实现会误拒官方 pad 值在 17~32 区间的合法报文。这个坑官方文档不会提醒,踩过的人才知道。

3.4 文档现状

官方文档的规模用数字说明:腾讯广告 Marketing API 的官方路由全清单是 367 条;小程序一条线的端点数是 588;支付 APIv3 是 625。文档里还有自相矛盾的地方:同一接口的商户页与服务商页字段表不一致、已下架的页面仍留在目录树里、同一页面在两个文档树中重复声明。所以动手抄之前,得先把文档本身核对一遍。

这套组件把官方文档的矛盾点与勘误逐页核验后,写进接口的 XML 注释里(见下节)。

四、怎么做的

五个工程决策。能贴代码和断言的地方直接贴。

4.1 声明式接口 + 源生成

每个端点是一个接口方法,实现类由源生成器发射,仓内没有手写的 HttpClient 代码。企微线「应用管理」域的真实接口长这样(节选):

[HttpClientApi(TokenManage = nameof(IWechatAppManager), IsAbstract = true)]
[Token(TokenType = WechatTokenTypes.AccessToken,
      InjectionMode = TokenInjectionMode.Query, Name = "access_token")]
public interface IWechatWorkAgentService
{
    /// <summary>
    /// 获取指定的应用详情
    /// <para>官方限制:企业仅可获取当前凭证对应的应用;第三方仅可获取被授权的应用。
    /// <c>customized_publish_status</c> 仅代开发自建应用返回。</para>
    /// </summary>
    /// <remarks>
    /// <para>企业自建应用开发SDK文档:<see href="https://developer.work.weixin.qq.com/document/path/90227"/></para>
    /// <para>第三方应用开发SDK文档:<see href="https://developer.work.weixin.qq.com/document/path/90363"/></para>
    /// <para>服务商代开发SDK文档:<see href="https://developer.work.weixin.qq.com/document/path/96448"/></para>
    /// </remarks>
    [Get("/cgi-bin/agent/get")]
    Task<GetAgentResponse> GetAgentAsync(
        [Query("agentid")] int agentid,
        CancellationToken cancellationToken = default);
}

几个细节:

  • XML 文档不是补写的,跟着契约一起写:三种应用模式的官方文档链接、官方权限口径、返回字段列表、已知风险(Query 传令牌是官方强制)都在方法注释里。IDE 里悬停就能看到,不用切浏览器。
  • [Token] 声明令牌归属:哪个令牌、注入方式(Query/Header)、参数名,都在特性上。令牌传错位(该用企业级令牌却用了应用令牌)在注册表校验时直接抛异常,不静默取错。
  • 序列化走源生成 JsonContext:每个域一个生成的 JsonContext,全仓零反射序列化。AOT 能全绿,根子在这里。

4.2 契约守卫:把「没抄错」做成断言

这套组件里我最想讲的设计。每个业务域有一份「守卫测试」,把该域的契约——端点数、HTTP 方法、路由、令牌绑定、DTO 字段名(含官方原文拼写)——逐项写成断言:

// Tests/Mud.Wechat.Channels.Tests/ContractGuards/ChannelsAftersaleContractGuards.cs(节选)
AftersaleRoutes.Should().HaveCount(27,
    "Aftersale 域恰 27 端点(设计方案 v1 §2.2)");
AftersaleRoutes.Select(r => r.Route).Distinct().Should().HaveCount(27);
AftersaleRoutes.Count(r => r.Route.StartsWith("/channels/ec/aftersale/"))
    .Should().Be(27, "本域全部端点走 /channels/ec/aftersale/*,无 /shop/* 历史前缀并存");

断言失败即测试红。守卫是契约的机器可读描述,改契约面必须同批改守卫,这个纪律由 CI 强制。它防两个方向的问题:

  • 防抄错:新落地的端点路由拼错、字段名与官方原文不一致,守卫红;
  • 防漂移:已落地的端点被误删、被改名、令牌绑定被改,守卫红。

守卫里也锁反直觉的官方事实。举一个例子:微信支付的「同一路由、两套契约」——同一个 URL 在商户页和服务商页是不同的请求字段表,守卫锁定它们不得合并成一个 DTO,因为字段集合并集会让「必填口径」失真。这类细节如果不锁,会在后来的「顺手重构」中悄悄坏掉。

数量级:20 个测试工程、650+ 测试文件,测试与源码结构镜像。

4.3 逐页取证,以及端点是怎么批量落地的

先说清楚一个绕不开的问题:2200 个端点,一个人怎么写完的?纯手写不现实,答案是分成三段:

  1. 取证:脚本抓官方文档页面,把路由、HTTP 方法、请求/响应字段表、令牌面提取成结构化清单,与已实现端点比对,产出缺口清单。
  2. 生成草稿:按结构化清单批量发射接口声明、DTO、守卫测试的草稿。这一层只保证形态正确,不保证内容正确。
  3. 人工核对 + 守卫锁定:逐条对着官方页面核对字段名、必填口径、令牌归属,确认后守卫断言把这些事实钉死。仓里固化下来的 AddHttpJsonSerializable.ps1、GenerateJsonContext.ps1 承担 DTO 标注与 JSON 上下文生成这两步。

所以「逐页取证」不是修辞,是字面意思:每一页官方文档都有人看过。两条硬规矩:

  • 未核验的路由不进契约面。广告线的例子:官方全清单 367 条,当前落地 48 支——不是做不完,是其余的还没逐页核验,核验完成一批才落地一批。宁缺勿错。
  • 已下架/判非契约的端点记零声明。由专门的守卫锁定「零声明」——比如开放平台线两个已下架的旧版端点,守卫断言它们在源代码里零出现。这和「还没做」是两种状态,都要可区分。

4.4 多应用基座与回调接收

令牌与作用域。 多企业、多应用、多套件的令牌统一管理:TokenKey 三段式路由({tokenType}:{appKey}:{scopeKey}),企业级令牌按授权企业分作用域。服务商场景用作用域切换器:

// 第三方/代开发推荐入口:一次性 using,进入时写入「应用 + 企业」两级上下文,释放时逆序还原
using (var scope = switcher.UseCorpScope(appKey, authCorpId, permanentCode))
{
    // 此作用域内的声明式调用,自动消费该授权企业的 access_token
    var list = await agentService.GetAgentListAsync(ct);
}

分布式就绪。 令牌与授权记录的存储是端口抽象(IWechatTokenStore 等),默认进程内实现,多实例部署引入 Mud.Wechat.Redis 包换一行注册代码即可。Redis 实现覆盖全部五个存储端口。

回调接收。 企微回调的 AES 按官方 32 字节块补位;抗重放两道闸(±300s 时间窗 + 一次性 SHA1 指纹),fail-closed——分布式守卫故障时上抛 5xx 触发官方重试,不吞异常、不静默丢事件。回调处理器有 4500ms 软超时,小于官方 5s 重推契约。

编译期回调分析器。 一个随包分发的 Roslyn 分析器,校验回调处理器声明的 SupportedEventType 与载荷契约是否一致,不一致,编译期报错:

error MUDCB002: 事件键 'change_external_contact' 不属于载荷 WechatContactPayload 的契约键集

回调丢事件是最难排查的生产事故之一,表现是「用户说没收到反应,日志里什么都没有」。这个分析器把其中一类成因从运行期挪到了编译期。

4.5 DI 原生装配

装配面全部走 IServiceCollection,没有静态 Helper。企微线 312 个可注入接口全部构造注入:

// Program.cs:多应用底座先行,再按需链式注册模块
builder.Services.AddWechatApp(builder.Configuration, "WechatApps");
builder.Services.AddWechatWorkServices(builder => builder
    .AddContactApi()      // 通讯录
    .AddApprovalApi()     // 审批
    .AddKfApi());         // 微信客服
// 业务代码:构造注入 + 直接调用
public class ContactSyncService(
    IWechatWorkInternalUserService users,
    IWechatWorkDepartmentService departments)
{
    public async Task SyncAsync(CancellationToken ct)
    {
        var list = await users.ListUsersAsync(1, ct);   // 部门 id=1(根部门)
        // ...
    }
}

这几件事是跟着 DI 一起成立的:

  • 按域注册:用到哪个域注册哪个域,未注册的域不实例化。WechatModule 枚举 42 个成员对应 42 个注册方法,也有 AddAllApis() 全量入口。
  • Options 模式 + 请求期热更:IOptionsMonitor 支持回调路由前缀与凭据运行时热更新,改配置不用重启应用。
  • scope 纪律可验证:多应用上下文经 DI scope 切换(UseAppScope / UseCorpScope),测试工程在 ValidateScopes = true 下锁定「Singleton 跨 scope 同实例」断言——Captive Dependency 这类问题在测试期暴露。
  • 可 mock:单测直接 mock 接口,不需要自己包一层:
[Fact]
public async Task SyncAsync_ShouldSkipDisabledUsers_WhenDepartmentReturnsMixed()
{
    var users = new Mock<IWechatWorkInternalUserService>();
    users.Setup(u => u.ListUsersAsync(1, It.IsAny<CancellationToken>()))
         .ReturnsAsync(new GetUserListResponse { /* ... */ });

    var sut = new ContactSyncService(users.Object, departments.Object);
    // 断言 ...
}

五、与现成方案的差别

四个取舍。每条只写我这边做了什么、付出了什么代价:

# 取舍 代价 换来
1 AOT 优先于便利:四档 TFM 全覆盖,AOT 严格模式全绿 低 TFM 下要写兼容层、不能用便利 API 老项目(netstandard2.0 兼容面)与新发布模式(Native AOT、裁剪)都能用
2 包边界优先于大而全:18 包、跨线依赖白名单全仓仅 2 条 包多一些 用支付线不引入企微线任何代码;边界由守卫测试锁定,不靠文档约定
3 契约保真优先于交付速度:逐页取证 + 守卫锁定 前期慢 端点数、字段拼写、令牌归属有机器可验证的正确性承诺
4 DI 原生优先于静态便捷:装配全走 IServiceCollection 与构造注入 教程式「三行代码」场景多写两行注册 按域注册、Options 热更、scope 安全、可 mock——这些能力在静态类形态下做不出来

这几个取舍的共同点:都是前期多花工程成本,换长期的可验证性。

六、做成了什么

6.1 七线总览

产品线 NuGet 包 规模 备注
企业微信 Mud.Wechat.Work(+Callback) 42 个业务域、470 个接口(158 父 + 312 可注入)、122 个回调事件键 三种应用模式全覆盖;智能机器人回调 + 长连接
公众号 Mud.Wechat.OfficialAccount(+Callback) 266 条路由(官方面 196 条全落) 认证/票据基座、卡券全族、非税支付面
小程序 Mud.Wechat.MiniProgram 588 端点、39 个域 直播 38、虚拟支付 37、云开发 44、短剧 31
微信支付 Mud.Wechat.Pay(+Callback) APIv3:625 端点 / 91 接口;APIv2:71 端点 服务商全形态、品牌七域、PAPay 3.0
开放平台 Mud.Wechat.OpenPlatform 208 端点 代小程序管理 12 支子接口、批量云开发 50 端点
小店·视频号 Mud.Wechat.Channels(+Callback) 29 域 394 端点 交易 + 内容运营双面;官方强制 JSON 回调
腾讯广告 Mud.Wechat.Ads 13 域 48 端点(45 声明式 + 3 multipart) 官方全清单 367 条,按逐页核验节奏补齐

补充两个口径说明:

  • 「官方面 196 条全落」(公众号)指官方索引页表格取值并集口径的路由全部实现,且经台账逐条比对;
  • 各线的「域」是官方文档目录树的功能分区,不是自造的分类。

6.2 跨线基建

  • Core 叶层(Mud.Wechat.Abstractions):令牌存储端口、回调密码学内核、SSRF 白名单单一来源——BaseUrl 强制 HTTPS + 主机白名单,自定义主机须显式登记,私有化部署不会静默失去令牌恢复能力。
  • Mud.Wechat.Redis:五个存储端口的分布式实现,含长连接主备租约。
  • Mud.Wechat.OpenTelemetry:单根源 Mud.Wechat + 产品线通配 Meter,接标准可观测性栈不需要自己埋点。敏感凭据零泄漏纪律:URL 脱敏词表、异常构造期剥离 query、密钥永不进日志与遥测。
  • 构建期工具:回调契约登记生成器 + 回调处理器分析器(随 Callback 包以 analyzers 形式分发,不占依赖)。

6.3 AI 能力

企微智能机器人的回调与长连接两条接入面,已经接到 AI Agent 运行时上:

flowchart LR subgraph 接入面 CB["Bot JSON 回调"] WS["长连接 wss"] end subgraph Agent["Agent 编排层(Mud.Wechat.Work.AI)"] GATE["同会话闸门<br/>串行 + 并发削峰"] TURN["轮次编排<br/>空包 + 主动回复"] HITL["HITL 工具审批<br/>暂停 → 通知 → 续跑"] end TOOLS["62 个策展工具(14 域)<br/>49 只读 / 12 受控写 / 1 高危写"] MCP["MCP Server(stdio)"] HOST["宿主注册的 IChatClient<br/>(模型接入完全交给宿主)"] CB --> GATE WS --> GATE GATE --> TURN TURN --> HITL HITL --> TOOLS TOOLS --> MCP TURN --> HOST

这套东西有几个地方值得单独说:

  • 「空包 + 主动回复」:回调分发软超时 4500ms,远小于 LLM 生成耗时,且官方只推一次——应答不走回调同步应答体,生成后经主动回复通道下发。
  • 工具分级:62 个工具按「只读 / 受控写 / 高危写」分级,写操作走 HITL 审批(暂停 → 宿主通知 → 续跑),高危写必须有人点头。
  • 不做模型接入层:只消费 IChatClient 抽象,模型选择与密钥管理完全交给宿主。
  • MCP server:工具面经 stdio 协议暴露,可以给任何支持 MCP 的客户端用。

七、接下来

接下来两件事:

AI 域深化。企微已封装的域远多于当前工具面覆盖的 14 个,按「只读优先、写必审批」的纪律把工具面从 62 个扩到 150 上下;逐域补模型可读的使用指引(随 schema 下发——工具能不能被正确使用,指引和工具本身同样重要);流式 / 卡片回复通道;知识库接入。

各产品线清单补齐。七条线都有还没落地的端点,缺口以台账形式逐条登记,逐页核验后进入契约面。企微侧还排着服务商代开发服务端(15 端点)、会话内容存档数据面(19 端点)这些族;公众号、小程序、支付 v3 也各有几十条在册。「已落地」和「已核验」是两个状态,在册缺口走完逐页核验才进契约面,和第四节的工作流是同一套。

参与方式:MIT 协议,接受 PR。适合作为第一批贡献的事情:某条线你正在用的端点的文档勘误、你所在行业的域守卫增强、示例项目。仓库与 issue 渠道见文末链接。

八、结尾

七条线写到现在,缺的东西还不少:广告线官方 367 条清单才落 48 支,企微的工具面才 62 个,文档勘误这种事更是做不完。这些都排在台账和 issue 里,慢慢补。

先写到这里。仓库在下面,用着有问题提 issue;觉得哪条线的守卫把事实锁错了,也欢迎直接指出来。


源码:GitHub | Gitee(MIT 协议)

欢迎添加我的微信公众号
qrcode_for_gh_acad905b0171_258

来源:博客园 · cnblogs.com