供应商返回 OK,订单就成功了?Spring Boot 多供应商接入:策略、工厂、适配器各管什么
供应商接口返回了OK你的订单该标成成功吗如果这笔订单是充值答案可能是不该。有的供应商OK只代表请求已受理充值还在路上。把它统一映射成成功接口是接通了业务状态却已经错了。这类问题往往在接第二、第三家供应商时开始冒头金额单位不同订单号字段不同状态码含义也不同。业务层多出一段段if (supplier ...)下单、查单、回调里到处都是。新增一家供应商得先读懂原来的每个分支。在充值供应商接入这类系统里这是一个非常典型的问题。工厂、抽象服务、API 适配、状态归一化常被统称为策略模式其实各自承担着不同的职责。本文用两个虚构的充值供应商把这些职责拆开讲清楚并给出 Spring Boot 的装配方式。读完你会拿到内部统一模型怎么定而不是从供应商字段反推状态归一化怎么做才不会把受理当成完成、把未知当成失败新增一家供应商时改动应该落在哪里。文中代码只依赖 JDK可以直接编译运行。先看全局上游路由确定供应商编码应用服务供应商工厂注册表Alpha 适配器Beta 适配器Alpha 客户端Beta 客户端Alpha 原始结果Beta 原始结果统一 SubmitResult1. 先看看两份接口到底差在哪里假设内部要发起一笔 100 元充值。两个供应商分别约定项目AlphaBeta内部订单号对应字段requestNomerchantOrderId金额字段整数分10000元字符串100.00外部订单号tradeNotransactionId结果OK表示受理DECLINED表示拒绝1表示受理2表示充值完成3表示拒绝这些语义只属于本例不代表任何真实供应商协议。注意 Alpha 的OK它只说明请求被受理不说明充值完成。这一点在第 4 节的映射里会处理。业务层需要的是稳定的内部请求和结果。至于供应商用什么字段、金额单位和状态码应该在接入边界完成转换。2. 策略、工厂、适配器各管什么角色回答的问题本例对应策略同一项能力有哪些可替换实现RechargeSupplier与各供应商实现工厂注册表已知供应商编码去哪里找到实现RechargeSupplierFactory适配器怎样把内部请求、结果与外部协议互相转换AlphaSupplierAdapter、BetaSupplierAdapter路由规则这笔业务应该选择哪个供应商本例假定上游已经决定应用服务怎样组织这次业务调用RechargeApplicationService策略与适配器可以落在同一个类上。AlphaSupplierAdapter对内部来说是一个可替换的充值实现对外部来说它负责适配 Alpha 协议。这里没有必要为了模式名字再包一层只做转发的类。还要把路由和工厂分开。按价格、库存、商品资格选择供应商是业务规则根据alpha编码找到 Alpha 实现是对象定位。工厂不会因为持有多个供应商就自动拥有选路能力。调用关系可以回看文章开头的流程图。3. 先定内部接口别从供应商字段反推业务模型本例只处理 CNY 充值内部金额固定为整数分。javaenum SubmitStatus { PROCESSING, SUCCEEDED, REJECTED, UNKNOWN } interface RechargeSupplier { String supplierCode(); SubmitResult submit(RechargeCommand command); }RechargeCommand保存内部订单号和amountFenSubmitResult保存统一状态、外部订单号和原始状态。保留原始状态有助于解释映射结果。统一后只有一个UNKNOWN但 Alpha 返回了新字符串、Beta 返回了新数字它们是不同的排查线索。真实工程还需要记录请求身份、错误分类和原始证据引用。敏感响应不要直接放进普通日志。4. 适配器负责转换业务层不认识外部状态码两个客户端分别使用自己的协议类型。演示中的客户端由本地函数提供固定结果不会联网javainterface AlphaClient { AlphaResponse create(String requestNo, long amountFen); } interface BetaClient { BetaResponse recharge(String merchantOrderId, String amountYuan); }Alpha 的适配器直接传整数分但要把受理映射成处理中javastatic final class AlphaSupplierAdapter implements RechargeSupplier { private final AlphaClient client; AlphaSupplierAdapter(AlphaClient client) { this.client client; } Override public String supplierCode() { return alpha; } Override public SubmitResult submit(RechargeCommand command) { AlphaResponse response client.create(command.orderNo, command.amountFen); SubmitStatus status OK.equals(response.code) ? SubmitStatus.PROCESSING : DECLINED.equals(response.code) ? SubmitStatus.REJECTED : SubmitStatus.UNKNOWN; return new SubmitResult(status, response.tradeNo, response.code); } }Beta 要完成金额换算和另一套状态映射javastatic final class BetaSupplierAdapter implements RechargeSupplier { private final BetaClient client; BetaSupplierAdapter(BetaClient client) { this.client client; } Override public String supplierCode() { return beta; } Override public SubmitResult submit(RechargeCommand command) { String amountYuan java.math.BigDecimal.valueOf(command.amountFen, 2) .toPlainString(); BetaResponse response client.recharge(command.orderNo, amountYuan); SubmitStatus status; switch (response.state) { case 1: status SubmitStatus.PROCESSING; break; case 2: status SubmitStatus.SUCCEEDED; break; case 3: status SubmitStatus.REJECTED; break; default: status SubmitStatus.UNKNOWN; } return new SubmitResult(status, response.transactionId, Integer.toString(response.state)); } }这里用BigDecimal.valueOf(amountFen, 2)将整数分精确转换为元字符串不经过double。未知码被保留为UNKNOWN不会默认变成拒绝。上游遇到它应进入待确认处理不能凭这个值认定没发生充值可以换一家再下。本例没有实现这个后续流程。只有供应商契约明确保证某个状态是充值完成才能映射为SUCCEEDED。HTTP 200 或调用成功都不能替代业务语义。5. 工厂建立注册表重复编码要尽早报错javastatic final class RechargeSupplierFactory { private final MapString, RechargeSupplier suppliers; RechargeSupplierFactory(ListRechargeSupplier candidates) { MapString, RechargeSupplier registry new HashMap(); for (RechargeSupplier supplier : candidates) { String code supplier.supplierCode(); if (code null || code.trim().isEmpty()) { throw new IllegalArgumentException(供应商编码不能为空); } if (registry.putIfAbsent(code, supplier) ! null) { throw new IllegalArgumentException(重复供应商编码: code); } } this.suppliers Collections.unmodifiableMap(registry); } RechargeSupplier getRequired(String code) { RechargeSupplier supplier suppliers.get(code); if (supplier null) { throw new IllegalArgumentException(未注册供应商: code); } return supplier; } }注册表在创建时完成后续只读。putIfAbsent用于检测重复编码假如两个实现都声称自己是alpha启动装配就应该失败不能靠注册顺序决定覆盖谁。这种类在业务工程里常被命名为 Factory。严格说它更接近已有策略对象的注册与查找没有展示 GoF 工厂方法模式中的创建方法继承体系。类名可以沿用团队习惯解释时要说明它到底做了什么。本例限定充值所以只用供应商编码。扩展到多个业务时可以按业务类型供应商编码建立键避免同一个供应商的充值和电影票实现互相覆盖。账号是另一个维度多租户、多账号配置不宜保存在单例适配器的可变成员里。6. 应用服务通过统一接口调用javastatic final class RechargeApplicationService { private final RechargeSupplierFactory factory; RechargeApplicationService(RechargeSupplierFactory factory) { this.factory factory; } SubmitResult submit(String supplierCode, RechargeCommand command) { // supplierCode 已由上游路由确定本方法不负责价格或库存选路。 return factory.getRequired(supplierCode).submit(command); } }这里的应用服务被有意收窄只展示查找和调用。实际项目中的应用编排还会负责订单检查、记录执行身份、持久化状态以及安排后续动作。转换供应商字段、签名和状态属于适配器父子订单如何聚合、什么时候通知渠道则属于核心业务处理。不要把两类职责一起塞进供应商实现否则每新增一家就会复制一份订单规则。7. Spring Boot 怎么装配这些对象前面的类通过构造器传入依赖既可以手动创建也可以交给 Spring 管理。下面是接线示意类型沿用上文简称放入项目时需拆为相应 Java 类型并提供两个真实或模拟客户端 Bean。javaConfiguration public class SupplierConfiguration { Bean public RechargeSupplier alphaSupplier(AlphaClient client) { return new AlphaSupplierAdapter(client); } Bean public RechargeSupplier betaSupplier(BetaClient client) { return new BetaSupplierAdapter(client); } Bean public RechargeSupplierFactory rechargeSupplierFactory( ListRechargeSupplier suppliers) { return new RechargeSupplierFactory(suppliers); } Bean public RechargeApplicationService rechargeApplicationService( RechargeSupplierFactory factory) { return new RechargeApplicationService(factory); } }Spring 支持注入同一类型的 Bean 集合这里借此构建业务编码注册表。若注入MapString, RechargeSupplier默认键对应 Bean 名称不会自动变成supplierCode()的返回值。这一点可以查阅 Spring 官方依赖注入文档。本次实际编译运行的是框架无关的 Java 示例这段 Spring 配置没有作为独立 Boot 工程启动验证。集合注册是成熟框架能力本例不依赖某个新版本特性。8. 放到真实系统里职责怎么分层上面的示例做了收窄。在一个完整的充值供应商接入系统里这几类职责通常分布在不同层次层次典型职责供应商服务工厂按供应商标识定位充值服务抽象充值服务定义下单、查单等统一能力供应商 API 层含签名实现账号、签名、HTTP 协议适配回调 Service将供应商字段转成统一回调 DTO订单管理Manager订单编排与结果处理标准商品SKU 管理根据标准商品、场景等条件选实际供应商 SKU这里有几个容易踩的点1Manager 容易膨胀。订单编排如果同时承担业务规则、数据库、消息和锁状态处理就会散落在多个方法里改一处牵动一片。更稳妥的做法是让订单状态流转有统一入口把供应商差异挡在适配层。2供应商账号上下文要想清楚。用 ThreadLocal 加切面清理的方式保存账号同步链路取用很方便但异步任务不能假定能拿到原请求的上下文。本文示例选择通过构造器传入客户端避开了这部分复杂度真实系统仍需明确账号解析、上下文传递与清理方式。3重复编码要在装配期暴露。本文示例在构造器里完成注册表初始化并加了重复编码检查让配置冲突在启动阶段就失败而不是在线上随机覆盖。9. 跑一次示例检查的不只有正常返回完整代码是一个只依赖 JDK 的单文件示例MultiSupplierDemo.java使用 Java 8 可用的语法和 API。在文件所在目录执行bashjavac -encoding UTF-8 MultiSupplierDemo.java java MultiSupplierDemo实际输出textalpha - statusPROCESSING, supplierOrderNoA-DEMO-001, rawStatusOK beta - statusSUCCEEDED, supplierOrderNoB-DEMO-001, rawStatus2 PASS: 金额换算、受理/完成/拒绝/未知状态、注册冲突与输入校验主方法中有显式检查结果不符合预期就抛出AssertionError不依赖 JVM 的-ea参数。检查预期同一内部金额 10000 分Alpha 收到 10000Beta 收到100.00Alpha 返回 OK处理中而非完成Beta 返回 2按本例协议映射充值完成两家明确拒绝REJECTED两家返回未识别状态UNKNOWN查找未注册供应商明确报错两个实现使用相同编码装配时报错空订单号、非正金额输入被拒绝本次用 JDK 25 的--release 8编译并运行通过。这说明演示使用 Java 8 的编译目标和 API 范围不代表验证了真实供应商协议、Spring 容器或订单一致性。10. 新接一家供应商改动应该落在哪儿如果新协议能被当前业务语义表达通常需要新增客户端、请求响应类型、适配器和注册配置。应用服务的调用方式可以保持稳定。但新供应商如果引入预下单、确认交易、部分成功等新能力原来的submit就可能不够。此时要重新检查模型按能力分开建接口而不是把所有业务塞进一个万能方法。我更愿意用几个具体问题检查这套设计新增供应商时是否要改订单主流程外部新状态会不会被误判同一供应商的不同业务是否会冲突账号配置会不会被并发请求覆盖这些问题比代码里出现了多少个设计模式更能说明边界是否合适。