Skip to content

当一次订单创建不再是一个事务:Service、Manager 与 Channel 的职责划分

1. 背景:传统三层结构遇到复杂业务

在很多 Java 后端项目中,最常见的结构是:

Controller

Service

Mapper

这种结构对于简单 CRUD 业务通常已经足够。

Controller 负责接口入口,Service 负责业务逻辑,Mapper 负责数据访问。

但是随着业务开始接入支付、第三方平台、外部服务等外部渠道后,传统三层结构会逐渐暴露问题:

  • 本地数据库和外部渠道不是同一个事务;
  • 外部调用可能超时、失败或返回未知结果;
  • 同一个业务动作可能被重复执行;
  • 订单状态可能来自多个来源;
  • Service 容易同时承担流程编排、数据库操作和外部调用。

本文讨论订单类业务接入外部渠道后的职责划分问题。

对于简单 CRUD 场景,Controller / Service / Mapper 已经足够;本文关注的是引入外部渠道后,本地事务、外部调用和状态确认之间如何划分职责。

2. 一个订单创建流程中的隐藏问题

一个订单创建流程通常包含:

创建订单
保存本地数据
调用外部渠道
更新订单状态

从业务角度看,这是一个完整动作。

但是从系统角度看,它实际上跨越两个不同边界:

  • 本地业务边界;
  • 外部渠道边界。

本地数据库可以使用事务保证一致性,但外部渠道无法参与本地事务。

因此:

一个业务流程,不一定等于一个数据库事务。

例如:

本地订单保存成功



调用外部渠道



请求超时

此时可能出现:

  • 本地订单已经存在;
  • 外部渠道是否成功未知。

真正的问题不是简单的"接口失败",而是:

系统如何在不确定状态下继续推进业务。

3. 从三层结构到职责边界

一个更清晰的结构:

Controller



Service

    ├── Manager
    │       ↓
    │     Mapper

    └── Channel Component

        External Channel
组件职责
Service业务流程编排
Manager本地业务操作单元
Mapper数据访问
Channel外部渠道协议边界

4. Service:负责业务流程编排

Service 关注:

这件业务应该按照什么顺序发生。

例如订单创建:

  • 创建本地订单;
  • 调用外部渠道;
  • 根据结果推进状态;
  • 异常时进入确认流程。

Service 不应该关心:

  • 某张表如何保存;
  • 某个渠道如何签名;
  • 某个接口参数如何转换。

它负责连接不同边界。

5. Manager:表达本地业务操作单元

Manager 解决的问题是:

在自己的系统内部,一个业务动作如何完整完成。

例如创建订单可能涉及:

order

order_action

timeline

这些数据属于同一个业务动作。

Manager 不是 Mapper 的简单包装,而是一个本地事务边界。

示意:

@Transactional
public Order createOrder(CreateOrderCommand command) {

    Order order = saveOrder(command);

    saveAction(order);

    saveTimeline(order);

    return order;
}

如果只是简单查询:

Service → Mapper

即可。

只有涉及多个数据对象、统一事务或完整业务动作时,才需要 Manager。

6. Channel:负责外部渠道协议边界

Channel 负责:

  • 请求和响应格式;
  • HTTP 通信;
  • 签名;
  • 验签;
  • 加密;
  • 错误码转换;
  • 外部协议变化。

例如:

channel-wechatpay

它解决:

如何正确地和外部渠道通信。

Manager 解决:

如何在自己的系统中完成一次完整业务操作。

二者不应该混合。

7. 外部调用后的状态确认

外部调用可能出现超时。

系统不能简单认为失败。

需要根据业务唯一号确认外部事实:

  • 外部订单是否存在;
  • 当前状态是什么;
  • 是否需要更新本地状态。

创建接口负责发起业务,查询接口负责确认事实。

8. 幂等、状态约束与最终一致性

回到前面提到的重复执行问题:外部调用失败后的重试,以及异步通知重复到达,都要求系统具备幂等能力。

业务唯一号用于:

  • 防止重复创建;
  • 支持重试;
  • 支持状态查询。

幂等不仅适用于请求重试,也适用于重复通知。

状态更新也不能简单覆盖。

系统需要限制状态流转:

  • 当前状态是否允许进入目标状态;
  • 是否存在状态倒退;
  • 是否需要版本号或乐观锁避免并发覆盖。

本地数据库和外部渠道通常无法强一致,需要通过查询、通知、补偿等方式实现最终一致。

9. 主动查询与异步通知

外部渠道确认结果通常有两种方式:

主动查询

系统根据业务唯一号主动查询外部状态。

异步通知

外部渠道通过回调通知结果。

回调处理需要:

  • 验证来源真实性;
  • 验签;
  • 校验业务数据;
  • 遵循幂等原则。

这些属于 Channel 边界的一部分。

实际系统中,主动查询和异步通知通常需要同时存在。异步通知可以提高实时性,但由于网络延迟、通知丢失等原因,主动查询通常作为最终确认手段。

10. 数据对比能力

除了单笔查询,系统还需要具备数据对比能力。

例如:

  • 获取外部渠道订单;
  • 获取本地订单;
  • 按业务唯一号比较。

发现:

  • 外部存在,本地不存在;
  • 本地存在,外部不存在;
  • 双方状态不一致。

在支付、清算等领域,这类能力通常进一步发展为更完整的对账体系。

11. 什么情况下需要 Manager

不是所有调用都需要经过 Manager。

简单场景:

Controller

Service

Mapper

复杂场景:

Controller

Service

Manager

Mapper

判断标准:

是否存在一个值得独立表达的本地业务操作单元。

需要 Manager:

  • 创建订单;
  • 保存多个业务对象;
  • 更新多个状态。

不需要 Manager:

  • 单表查询;
  • 简单字段更新。

12. 总结:让不同问题归属于不同边界

最终结构:

Controller



Service

    ├── Manager
    │       ↓
    │     Mapper

    └── Channel Component

        External Channel
组件职责
Controller接口入口
Service业务流程编排
Manager本地业务操作单元
Mapper数据访问
Channel外部渠道协议

本文关注的是职责划分本身。

更进一步的分布式事务方案,例如本地消息表、事务消息、Saga、TCC,以及完整对账系统设计,属于后续演进方向。