NEW

免费试用已开放

立即开始

API · SDK · 文档与调试

企业微信 API 二次开发的分层设计

更新于 2026-08-168 分钟

企业微信 API 二次开发的第一版通常很干净:一个封装好的请求函数,几个业务方法,跑得挺好。烂掉是从第三个需求开始的 —— 业务代码里开始出现平台侧的标识、分页游标、错误码,改一个接入细节要动七个文件。问题不在代码写得糙,在于中间少了一层:只有「接入」和「业务」,没有属于你自己的领域。这篇按 wecomapi 的接入方式讲这三层怎么切、依赖方向怎么定。

两层结构撑不过第三个需求

绝大多数项目的真实形态是两层:底下一个 client.js 负责拼请求、带凭证、解响应,上面是一堆业务函数直接调它。这个结构在第一个需求里是对的 —— 它简单、直白、没有任何多余抽象。它的问题不在当下,在于它没有为「平台概念」设防线。

两层结构崩塌的方式很固定:平台概念自下而上渗透。第一次是某个业务函数为了拼请求,直接把平台侧的会话标识存进了自己的表;第二次是分页游标从 client 漏出来,业务代码开始自己维护 cursor;第三次是某个业务分支按平台返回的错误码做判断。到这一步,你的业务代码已经绑死在一种接入形态上了。

  • 数据库里的主键是平台侧标识 —— 换接入路线、或者同一个客户在两条路线下标识不同,你要写迁移脚本。
  • 业务代码里出现 token、cursor、平台错误码这类词 —— 说明接入细节已经越界。
  • 写单元测试必须起一个 HTTP mock —— 说明业务逻辑和网络调用长在一起了。

这三个症状出现任意一个,加层就已经比重构便宜。三个都出现,通常意味着下一个需求的排期要翻倍。

三层各装什么,用两个问题判定

分层的名字不重要,边界才重要。接入层负责「怎么和平台说话」:拼请求、带凭证、翻页、把平台返回翻译成结构化结果。领域层负责「你的业务对象是什么、能发生什么」:你自己的客户、会话、任务,以及它们之间的规则。业务层负责「什么时候该发生」:订单发货了要通知客户、工单超时了要提醒负责人。

判定一段代码该放哪层,问两个问题:换一条接入路线,这段代码要不要改?换一个业务需求,这段代码要不要改?只对第一个答「要」的是接入层,只对第二个答「要」的是业务层,两个都答「不要」的多半就是领域层 —— 领域层最稳定,这是它存在的全部理由。两个都答「要」,说明这段代码把两件事揉在一起了,先拆再谈别的。

层的厚度会随接入路线变,层的边界不该变。用 wecomapi 的统一 REST 接口接入时,接入层薄到几乎只剩一个 HTTP 包装加一张错误映射表;走官方开放平台自建应用,接入层还要处理授权与可见范围带来的差异。但无论哪种,领域层和业务层看到的东西应该一模一样 —— 如果不一样,说明接入层没兜住。

领域层要立起三样东西

领域层最常见的失败是做成「又一层转发」:函数名跟着接入层走,参数原样透传,只是多了一个文件。这样的层没有价值,删掉更好。真正的领域层要立起三样自己的东西。

第一样是自己的主键。你的客户表主键必须是你自己生成的 ID,平台侧标识作为一列外部标识存在旁边,并且允许一个客户对应多个外部标识。这一列的意义在项目第一周看起来是零,在换路线、多账号、合并重复客户这三件事上,它是唯一能救你的东西。事后补的成本不是加一列,是全表回填加一轮业务改造。

第二样是自己的动词。领域层暴露的应该是 notifyCustomer、assignOwner、closeConversation 这类业务动作,不是 sendText、listPage 这类传输动作。判据很直接:看函数名能不能让一个不懂接入细节的同事读懂。读不懂,说明词汇还是平台的词汇。

第三样是自己的状态。消息发出去了、还在重试、终态失败,这是三种领域状态,不是三个 HTTP 状态码。把它们建模成显式状态,业务层才可能写出「发送失败超过三次就转人工」这种规则,而不用去理解底层返回。

领域层不应该知道 HTTP 存在。这句话可以当验收标准用:在领域层的文件里搜 http、header、status、token,一个都搜不到,这层才算立住了。

依赖方向只能向内

三层不是三个目录名,是一条单向的依赖链:业务层依赖领域层,领域层依赖它自己声明的端口,接入层实现这些端口。反过来不行 —— 接入层 import 业务层的那一刻,分层就名存实亡了,因为编译器不再帮你挡任何东西。

示意结构:字段名与端点仅表意,精确定义以线上接口文档为准javascript
// —— 接入层:只认平台形状,平台侧标识只在这一层出现 ——
async function deliverText(ref, text) {
  const res = await http.post("https://manager.wecomapi.com/message/sendText", {
    guid:    ref.accountId,
    toId:    ref.peerId,
    content: text,
  });
  return toOutcome(res);   // 平台返回 → 领域结果:sent / retryable / failed
}

// —— 领域层:只认自己的实体与动作,不知道 HTTP 存在 ——
async function notifyCustomer(customerId, draft) {
  const ref     = await identity.resolve(customerId); // 自己的主键 → 外部标识
  const outcome = await ports.deliverText(ref, draft.text);
  return conversation.record(customerId, draft, outcome);
}

// —— 业务层:只认业务语义,换接入路线这行不用动 ——
await notifyCustomer(order.customerId, templates.shipped(order));

这段里 ports.deliverText 是关键:它由领域层声明形状、由接入层提供实现。领域层拿到的是一个函数,不是一个 client 对象。这个区别决定了领域层能不能脱离网络做单测 —— 传一个假实现进去就能跑,不用起 mock server,一条用例几毫秒。测试速度不是附赠品,它直接决定了三个月后还有没有人愿意改这段代码。

identity.resolve 那一步也不能省。业务层从头到尾只拿自己的 customerId,外部标识在接入层边界上才被换出来。把这一步下沉进接入层是常见的偷懒,代价是领域层被迫开始传外部标识,第一样东西就白立了。

幂等、限流、重试必须各有唯一落点

这三件事是横切关注点,最容易的处理方式是「哪里需要就在哪里写」,最贵的后果也是它。同一个系统里出现三处退避实现、两套幂等键规则,排障时你根本说不清一次重复发送是哪一处造成的。规则很简单:每一件都只能有一个落点。

  1. 1限流落在接入层。频率约束是接入形态带来的,不是业务带来的;业务层不该知道有这层约束,它只应该在超限时收到一个明确的领域结果。
  2. 2重试落在接入层,但「重试几次之后算彻底失败」由领域层定。前者是技术判断,后者是业务判断,混在一起就会出现「通知类消息也在死磕重试」这种事。
  3. 3出向幂等落在领域层。幂等键要能对应一个业务意图(哪个客户、哪条通知、哪一次触发),而接入层不知道业务意图,只能拿请求体做哈希,那是错的键。
  4. 4入站幂等落在接入层。事件唯一标识是平台给的,在归一化的同时去重,业务层不该看到重复事件。

重试的判断依据也该收在一处。哪些结果可重试、退避多久,取决于接入侧返回的结构 —— 用 wecomapi 的统一错误模型时这段映射只需要写一遍,同时挂两条接入路线就得写两遍。但无论几遍,都该写在接入层的那张映射表里,不该漏进业务代码。业务层看到的只能是 retryable 还是 failed。

切歪了的四个信号,和不该分层的场景

分层做完之后,用这四条定期自查,比对着架构图讨论有用得多。它们都是可以 grep 出来的。

  1. 1业务层的文件里能搜到 token、header、cursor 或平台错误码 —— 接入细节越界了。
  2. 2领域层的函数签名里带着平台侧标识 —— identity 映射的位置不对。
  3. 3改一个接入参数需要动业务层的文件 —— 依赖方向反了。
  4. 4领域层的单测需要起 HTTP mock —— 端口没有真正倒置,领域层还捏着 client。

反过来,也有不该分三层的场景:一次性的数据导出脚本、只调两三个接口且明确不会长的内部小工具、以及做技术验证的原型。这些东西的生命周期比抽象的回本周期还短,分层是纯成本。但即便是脚本,有一条仍然值得守:别把平台侧标识当自己的主键存进库。这条的代价在写的时候是零,在补的时候是一次迁移。

本文讲的是结构与依赖方向,不涉及具体接口定义。精确字段、错误分类与端点以 wecomapi 线上接口文档为准,示意代码只用于表达层次关系,不要直接上生产。

常见问题

只做一个内部工具,企业微信开发API 也要分三层吗?
看两件事会不会长:接入路线可能变吗?业务规则会持续加吗?两个都是「不会」,两层足够,硬分三层是自找麻烦。两个里有一个是「会」,那么在写第三个业务功能之前把领域层立起来,成本大概是半天,晚一个月做就是一次重构。
领域层和「封装一个 SDK」有什么区别?
看词汇。SDK 封装用的还是平台的词汇 —— sendText、listCustomers,只是参数拼装省事了;领域层用的是你自己的业务动词 —— notifyCustomer、assignOwner,平台词汇被挡在下面。用 wecomapi 的统一 REST 接口接入时这层封装很薄,薄到你会怀疑有没有必要,但它的价值不在代码量,在于它是业务和平台之间那道防线。
三层要拆成三个服务或三个仓库吗?
不要。分层描述的是依赖方向,不是部署单元,同一个进程里用目录和模块边界就能表达。拆服务的理由只有伸缩需求和故障隔离,跟分层无关。按层拆服务的典型后果是每次改一个字段要发三次版,还得处理三段网络调用的失败。

准备好动手了?

精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。

相关文章