供应商返回 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就可能不够。此时要重新检查模型按能力分开建接口而不是把所有业务塞进一个万能方法。我更愿意用几个具体问题检查这套设计新增供应商时是否要改订单主流程外部新状态会不会被误判同一供应商的不同业务是否会冲突账号配置会不会被并发请求覆盖这些问题比代码里出现了多少个设计模式更能说明边界是否合适。

相关新闻

内容主权保卫战:从依赖流量平台到自建博客的迁移全攻略

内容主权保卫战:从依赖流量平台到自建博客的迁移全攻略

“我不再在那个平台发文章了”——这句话我憋了大概半年,还是说出来了。说出来之后没有想象中如释重负,反而要先面对一连串追问:为什么不发了?辛辛苦苦攒的阅读量怎么办?文章能搬走吗?搬到哪里去&#xff1…

2026/10/11 8:45:38 阅读更多 →
苏格拉底式提问如何让LoRA微调小模型攻克多步算术推理?

苏格拉底式提问如何让LoRA微调小模型攻克多步算术推理?

🌊 专注 AI 大模型与前沿科技深度解析,习惯从工程师视角拆解技术热点,让我们一起在技术浪潮中保持清醒与好奇 🚀苏格拉底式提问如何让LoRA微调小模型攻克多步算术推理? 1. 引言 1.1 小模型在多步算术推理中的困境 在当…

2026/10/11 8:45:38 阅读更多 →
基于角度回归的指针仪表读数:passagegmd表盘读取实践解析

基于角度回归的指针仪表读数:passagegmd表盘读取实践解析

简介:面向水表识别与指针式仪表自动读数场景,内容聚焦物联网与自动化系统中的机器视觉读数需求,服务开发者、工程师及公用事业项目人员,解决人工抄表效率低、易读错的问题,也适合作为远程监控系统的入门参考。压缩包共…

2026/10/11 8:44:38 阅读更多 →

最新新闻

深度学习OCR系统实战:基于PyTorch的CRNN+CTC文字识别

深度学习OCR系统实战:基于PyTorch的CRNN+CTC文字识别

简介:基于深度学习的文字识别系统完整项目包,面向毕业设计、课程设计与期末大作业场景,适合需要快速搭建OCR系统的计算机相关专业学生。项目采用CNN与RNN结合实现文字检测与识别,覆盖图像预处理、模型训练、后端接口与移动端展示全…

2026/10/11 10:25:10 阅读更多 →
使用 claude-howto 的 blog-draft 技能与草稿模板,系统化产出高质量技术博客

使用 claude-howto 的 blog-draft 技能与草稿模板,系统化产出高质量技术博客

教程文档 【免费下载链接】claude-howto A visual, example-driven guide to Claude Code — from basic concepts to advanced agents, with copy-paste templates that bring immediate value. 项目地址: https://gitcode.com/GitHub_Trending/cl/claude-howto 点…

2026/10/11 10:25:10 阅读更多 →
Excel自动评分:用LOOKUP和IF函数实现体育成绩折算自动化

Excel自动评分:用LOOKUP和IF函数实现体育成绩折算自动化

简介:这份资源是一份面向体育教师及学校教务人员的Excel实用教程文档,聚焦体育测试成绩换算这一高频痛点,帮助读者用公式与函数替代人工比对,降低错漏率。文档围绕学生成绩空表搭建、跳远与跳绳评分标准表制作、LOOKUP近似匹配与I…

2026/10/11 10:25:10 阅读更多 →
Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比

Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比

Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比 【免费下载链接】Legendary_OSINT A list of OSINT tools & resources for (fraud-)investigators, CTI-analysts, KYC, AML and more. 项目地址: https://gitcode.com/GitHub_Trending/le/Legendary_OSINT…

2026/10/11 10:25:10 阅读更多 →
ProxCenter热迁移深度解析:VDDK+nbdkit实现虚拟机零停机迁移的原理与调优

ProxCenter热迁移深度解析:VDDK+nbdkit实现虚拟机零停机迁移的原理与调优

【免费下载链接】proxcenter-ui ProxCenter is an alternative to VMware vCenter for Proxmox environments. It provides a modern, intuitive web interface to manage multiple Proxmox VE clusters and Proxmox Backup Server instances from a single pane of glass. 项目…

2026/10/11 10:25:10 阅读更多 →
2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:24:10 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →