企业微信 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 业务层的那一刻,分层就名存实亡了,因为编译器不再帮你挡任何东西。
// —— 接入层:只认平台形状,平台侧标识只在这一层出现 ——
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限流落在接入层。频率约束是接入形态带来的,不是业务带来的;业务层不该知道有这层约束,它只应该在超限时收到一个明确的领域结果。
- 2重试落在接入层,但「重试几次之后算彻底失败」由领域层定。前者是技术判断,后者是业务判断,混在一起就会出现「通知类消息也在死磕重试」这种事。
- 3出向幂等落在领域层。幂等键要能对应一个业务意图(哪个客户、哪条通知、哪一次触发),而接入层不知道业务意图,只能拿请求体做哈希,那是错的键。
- 4入站幂等落在接入层。事件唯一标识是平台给的,在归一化的同时去重,业务层不该看到重复事件。
重试的判断依据也该收在一处。哪些结果可重试、退避多久,取决于接入侧返回的结构 —— 用 wecomapi 的统一错误模型时这段映射只需要写一遍,同时挂两条接入路线就得写两遍。但无论几遍,都该写在接入层的那张映射表里,不该漏进业务代码。业务层看到的只能是 retryable 还是 failed。
切歪了的四个信号,和不该分层的场景
分层做完之后,用这四条定期自查,比对着架构图讨论有用得多。它们都是可以 grep 出来的。
- 1业务层的文件里能搜到 token、header、cursor 或平台错误码 —— 接入细节越界了。
- 2领域层的函数签名里带着平台侧标识 —— identity 映射的位置不对。
- 3改一个接入参数需要动业务层的文件 —— 依赖方向反了。
- 4领域层的单测需要起 HTTP mock —— 端口没有真正倒置,领域层还捏着 client。
反过来,也有不该分三层的场景:一次性的数据导出脚本、只调两三个接口且明确不会长的内部小工具、以及做技术验证的原型。这些东西的生命周期比抽象的回本周期还短,分层是纯成本。但即便是脚本,有一条仍然值得守:别把平台侧标识当自己的主键存进库。这条的代价在写的时候是零,在补的时候是一次迁移。
本文讲的是结构与依赖方向,不涉及具体接口定义。精确字段、错误分类与端点以 wecomapi 线上接口文档为准,示意代码只用于表达层次关系,不要直接上生产。
常见问题
- 只做一个内部工具,企业微信开发API 也要分三层吗?
- 看两件事会不会长:接入路线可能变吗?业务规则会持续加吗?两个都是「不会」,两层足够,硬分三层是自找麻烦。两个里有一个是「会」,那么在写第三个业务功能之前把领域层立起来,成本大概是半天,晚一个月做就是一次重构。
- 领域层和「封装一个 SDK」有什么区别?
- 看词汇。SDK 封装用的还是平台的词汇 —— sendText、listCustomers,只是参数拼装省事了;领域层用的是你自己的业务动词 —— notifyCustomer、assignOwner,平台词汇被挡在下面。用 wecomapi 的统一 REST 接口接入时这层封装很薄,薄到你会怀疑有没有必要,但它的价值不在代码量,在于它是业务和平台之间那道防线。
- 三层要拆成三个服务或三个仓库吗?
- 不要。分层描述的是依赖方向,不是部署单元,同一个进程里用目录和模块边界就能表达。拆服务的理由只有伸缩需求和故障隔离,跟分层无关。按层拆服务的典型后果是每次改一个字段要发三次版,还得处理三段网络调用的失败。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
相关文章
- 企业微信 API 开发怎么入门企业微信 API 开发的第一条链路应该怎么选、怎么跑通:从拿凭证、发第一条消息、接上事件回调到形成闭环,以及新手最常绕的三段弯路;字段与端点以 wecomapi 文档为准。阅读
- 企业微信 API 频控与重试怎么设计企业微信api频率限制不止一种:接口调用速率、账号行为节奏、对客户的打扰频次,三者处理方式完全不同。这篇讲清成因、退避重试怎么写、批量任务如何分时打散、出向幂等键怎么选。阅读
- 企业微信 API 凭证与 Token 管理实践企业微信api鉴权在工程上的真正难点:Token 存进程内还是集中缓存、主动刷新与被动兜底怎么组合、多实例并发刷新的惊群与旧值覆盖怎么防,以及凭证泄露后按什么顺序处置。阅读
