简介:基于 Java 的易宝支付对接源码已经过真实支付测试,定位清晰:适合需要快速接入易宝网关、学习支付回调与验签逻辑的 Java 开发者,可用于电商、众筹、缴费等业务模块的支付集成。资源内置 Eclipse 工程目录,共 36 个文件,压缩后约 786KB;其中 java 与 jsp 文件负责业务逻辑和支付前端页面,class 为编译产物,properties 与 xml 用于配置商户 ID、密钥及回调参数,jar 为依赖库,另含建行支付成功截图、测试步骤图片与说明文档。测试流程完整覆盖从登录页输入信息、选择银行、提交订单到跳转易宝支付页,并借助建行网银回调地址确认已扣款 0.01 元;开发者可按照配置示例替换商户 ID 和密钥,快速复现真实支付环境,同时对照图片理解 hmac 签名机制。资源目录结构清晰,符合常规 Web 工程布局,关键配置集中在属性文件,只需在配置文件中填入自己的商户信息即可跑通支付流程。已有 1491 人浏览学习,适合作为支付接口二次开发的基础模板,也有助于理解第三方支付网关的完整交互流程。
1. 支付源码示例能做什么:一个已经测通的下单→回调闭环
很多开发第一次对接支付时,都以为最难的是下单接口,等真做起来才发现,异步回调的验签和处理才是那个让人挠头的部分。这套 Java 支付源码示例围绕易宝支付把完整链路串了一遍,从下单组装参数、MD5 签名,到异步回调验签、订单状态更新,再到主动查单和退款,全部都在测试环境跑通过,测试步骤图片也放在包里,照着点一遍就能看到效果。
它适合两类人:一类是手里有个项目要快速接第三方支付,想找一个能跑起来的参考实现,照着改商户号、密钥和回调地址就能用;另一类是刚接触支付对接,想搞清楚“下单参数怎么拼、签名怎么算、回调怎么验”这些底层细节的开发者。这个资源解决的问题很直接——支付对接本来就是一个链路长、参数多、排查成本高的活,有一份跑通的代码在手,比看十篇接口文档都管用。
2. 项目结构与技术选型:Maven + Spring Boot 下的支付模块怎么搭
2.1 交付清单与代码布局
拿到源码包后,先别急着找下单方法,建议按下面这张清单把交付物过一遍,知道每部分放在哪里,改起来才顺手:
| 交付物 | 对应路径 | 作用 |
|---|---|---|
| 主程序源码 | src/main/java | 下单、回调、查询、退款等核心逻辑 |
| 前端页面 | src/main/resources/static | 测试下单页和结果展示页 |
| 配置文件 | src/main/resources/application.yml | 商户号、密钥、回调地址等可修改参数 |
| 构建文件 | pom.xml | Maven 依赖与打包配置 |
| 运行说明 | README.md | 环境要求、启动方式、测试步骤 |
| 测试步骤图片 | doc/images | 每一步操作对应的界面截图 |
目录结构是这样划分的,一个典型的支付 Demo 模块:
src/main/java/com/example/paydemo ├── PayDemoApplication.java // Spring Boot 启动类 ├── config │ └── PayConfig.java // 读取商户号、密钥等配置 ├── controller │ ├── OrderController.java // 下单接口入口 │ └── NotifyController.java // 异步回调接收入口 ├── service │ ├── PayService.java // 下单、查单、退款等支付服务 │ └── PayServiceImpl.java // 核心实现 ├── util │ ├── SignUtil.java // MD5 签名与验签 │ ├── HttpUtil.java // 封装 HTTP 请求 │ └── OrderStatus.java // 订单状态枚举 └── model └── Order.java // 简化版订单实体这个分层参考了实际支付模块的常规写法:Controller 只负责接收请求和返回结果,所有支付逻辑收敛到 PayService 里,签名计算单独抽成工具类。这样做的直接好处是,后面如果要换支付渠道或者加签名算法,改动范围能被控制在一个类内,不至于全局翻找。
2.2 技术选型与交易链路设计
这套源码的基础框架是 Spring Boot + Maven + HttpClient,订单数据用内存 Map 暂存,没有引入数据库依赖。之所以这么做,我理解是为了让下载的人拿到就能跑,省掉建表、连库这些和支付本身无关的步骤。如果你要接到真实项目里,把内存 Map 换成数据库表,再把订单实体映射成数据表字段就行,Controller 和 Service 层的逻辑基本不用动。
支付链路的调用顺序是固定的,理解它比理解代码本身更重要:
客户端请求 → 商户服务端下单接口 → 拼参与签名 → 请求支付平台收银台 → 用户在收银台完成支付 ↑ ↓ └──── 平台异步通知 → 商户回调接口 → 验签 → 更新订单状态这里有个关键点:用户付款完成后,支付平台不是直接跳转回商户页面,而是先向商户的异步回调地址发一条通知,商户收到通知后验签、改订单状态,然后返回“success”告诉平台别再重试了。这套源码的 NotifyController 就是干这个的,也是整个项目里最值得逐行读的部分。
2.3 配置文件里的三个关键参数
打开 application.yml,和支付相关的配置只有三段,改动时需要注意的也就这三个:
pay: merchant-id: 100000000000001 # 商户号,拿测试商户去替换 secret-key: 测试密钥 # 签名密钥,泄露等于资金风险 notify-url: http://localhost:8080/api/pay/notify # 异步回调地址 request-url: https://openapi.yeepay.com/ # 支付平台接口地址前缀大部分跑不起来的问题,根源都出在 merchant-id 和 secret-key 这两个值和自己的测试商户不一致。还有一个高频坑是 notify-url 填成了页面跳转地址,回调地址必须是能直接接收 POST 请求的接口路径,这点在第四章会详细说。
3. 核心模块拆解:一次支付请求从组装签名到验签入账的实现链路
3.1 下单参数组装与 MD5 签名生成
下单接口是整个支付的起点,核心流程是:接收页面提交的金额和订单号 → 组装平台要求的参数列表 → 对所有参数按字典序排序后拼接密钥 → 计算 MD5 签名 → 带着签名去请求支付平台收银台。PayServiceImpl 里的 createOrder 方法完整做了这件事:
public String createOrder(String orderId, long amountFen) { // 1. 组装支付平台要求的下单参数 TreeMap<String, String> params = new TreeMap<>(); params.put("merchant_id", payConfig.getMerchantId()); params.put("order_id", orderId); params.put("amount", String.valueOf(amountFen)); // 金额单位是分 params.put("notify_url", payConfig.getNotifyUrl()); params.put("return_url", "http://localhost:8080/payResult.html"); // 2. 生成签名:key=value&key=value...&key=商户密钥 String sign = SignUtil.md5Sign(params, payConfig.getSecretKey()); params.put("sign", sign); // 3. 发起请求,返回收银台 URL 或表单 HTML return HttpUtil.postForm(payConfig.getRequestUrl() + "/cashier", params); }代码里有几个地方值得细看。TreeMap 有两个作用:一是自动按 key 的字典序排序,这是签名算法硬性要求的,顺序错了签名必错;二是保证同样的参数在任何环境算出来的签名一致。amountFen 这个参数名已经说明了单位是“分”,页面上如果是用元输入,后端一定要先乘以 100 再传进来,这个单位转换是后面避坑章节要重点讲的高频问题。
SignUtil.md5Sign 方法的实现也不复杂,就是把 TreeMap 里的参数拼成 key1=value1&key2=value2 这样的字符串,末尾再拼上 &key=商户密钥,然后做一次 MD5:
public static String md5Sign(TreeMap<String, String> params, String secretKey) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(entry.getKey()).append("=") .append(entry.getValue()).append("&"); } // 最后的 & 不要,改拼商户密钥 sb.deleteCharAt(sb.length() - 1); sb.append("&key=").append(secretKey); return DigestUtils.md5Hex(sb.toString()).toUpperCase(); }签名计算的核心逻辑就这三行字符串拼接。平台方验签时拿到你传过来的所有参数,按同样的规则重新拼一遍、重新算一遍 MD5,然后和你传的 sign 值做对比。所以拼接规则必须和平台文档一字不差,多一个字符、少一个字符,验签就会失败。
3.2 异步回调接收与验签:最容易被坑的一段代码
回调接口是支付平台主动请求商户服务端的一个 POST 接口,所以它不能像普通页面那样依赖登录态,接收参数后第一步是验签,第二步是处理订单。这段代码是整个 Demo 里我建议大家逐行读的:
@PostMapping("/api/pay/notify") public String notify(@RequestBody String requestBody) { // 1. 把回调 JSON 转成 Map,验签前先做 URL 解码 Map<String, String> params = JSON.parseObject(requestBody, Map.class); String sign = params.get("sign"); params.remove("sign"); if (!SignUtil.verifySign(params, sign, payConfig.getSecretKey())) { return "验签失败"; } // 2. 幂等处理:同一个订单重复通知,不能重复入账 String orderId = params.get("order_id"); Order order = orderRepository.findById(orderId); if (order == null || "PAID".equals(order.getStatus())) { return "success"; // 已处理过,直接告诉平台别再发了 } // 3. 落库并记录回调原文,方便后续排查 order.setStatus("PAID"); order.setPlatformNo(params.get("platform_no")); order.setPaidAt(new Date()); orderRepository.save(order); return "success"; }这里有几个容易被带偏的细节。第一,验签之前必须把 sign 从参数集合里摘出去,因为签名计算时并不包含 sign 自己。第二,回调通知平台会重试多次,频率可能是每隔几分钟来一次,持续一两天,所以幂等判断不能省。第三,返回值固定是字符串 “success”,大小写都不能错,平台只有在收到 success 时才停止重试,返回其他内容会一直被打。
URL 编码是回调验签里最隐蔽的坑。平台 POST 过来的某个字段里如果包含 + 或 %20 这类字符,不经解码直接参与验签,算出来的签名和对方对不上。回调里验签前先做一次 URLDecoder.decode,其他地方也建议统一做,这样处理最省心。我在实际对接中就被这个问题绊过一次,当时日志里看参数都一样,算出来签名就是不一样,最后发现是某个字段带了 URL 编码后的特殊字符。
3.3 主动查单与退款:两大兜底接口
异步回调偶尔会有延迟,或者网络原因导致平台通知根本没送到,所以正规的支付模块都会加一个主动查单的兜底接口。这套源码里也包含了,逻辑是拿着订单号去支付平台查支付状态,然后同步本地订单状态:
public OrderStatus queryOrder(String orderId) { TreeMap<String, String> params = new TreeMap<>(); params.put("merchant_id", payConfig.getMerchantId()); params.put("order_id", orderId); String sign = SignUtil.md5Sign(params, payConfig.getSecretKey()); params.put("sign", sign); String response = HttpUtil.postForm(payConfig.getRequestUrl() + "/query", params); Map<String, String> result = JSON.parseObject(response, Map.class); // 平台返回 code=1 表示支付成功 if ("1".equals(result.get("code"))) { Order order = orderRepository.findById(orderId); if (order != null) { order.setStatus("PAID"); orderRepository.save(order); } return OrderStatus.PAID; } return OrderStatus.WAITING_PAY; }查单接口的签名规则和下单完全一致,所以可以直接复用 SignUtil。它适合放在一个定时任务里,每分钟扫描一次那些“下单了但一直没收到回调”的订单做补偿。退款接口的请求方式和查单基本一样,只是接口地址换成了 /refund,参数里多了退款金额和退款单号,这里就不重复贴了。
4. 本地运行与测试:配置商户号、启动服务、观察回调日志
4.1 环境准备与主要配置项修改
运行这套源码之前,先确认本机环境:JDK 8 或以上版本,Maven 3.5 以上,IDE 用哪个都行,IDEA 或 Eclipse 都能直接打开。然后准备一个支付平台的测试商户号和测试密钥。申请测试商户的入口一般在平台开放平台的“沙箱环境”里,找不到的话看 README 里的说明,很详细。
拿到测试商户后改两个位置:application.yml 里的 pay.merchant-id 填测试商户号,pay.secret-key 填测试密钥。这两个值在代码里被 PayConfig 读取,PayServiceImpl 里所有签名计算都依赖它们。改完这一步,源码包的其余配置基本不用动,直接启动即可。
4.2 按测试步骤跑通一笔订单
包里的测试步骤图片记录的是完整操作路径,按顺序执行就能看到效果:
# 1. 启动服务 mvn spring-boot:run # 2. 浏览器打开下单页 http://localhost:8080/ # 3. 输入订单号和金额,点击“去支付”,页面会跳转到收银台 # 4. 在收银台完成支付(测试环境用测试卡即可) # 5. 观察控制台回调日志,看到“回调验签成功,订单已更新”即完成闭环第三步提交后,页面会先跳到支付平台收银台,这一步能正常跳转说明下单参数和签名已经通过了平台校验。如果卡在这一步,控制台一般会打印平台返回的错误码,最常见的两个是“签名错误”和“商户号不存在”,前者排查拼参与密钥,后者排查商户号是否填对。
第四步支付完成后,不需要手动操作,支付平台会自动向配置的 notify-url 发一条异步通知。这时候盯着控制台日志就行,看到“回调验签成功”这行字,就说明整条链路通了。测试步骤图片里每一步对应的界面长什么样、按钮叫什么名字,按着图片对照操作,基本不会走偏。
4.3 测试过程中的两个观察点
跑测试时建议开两个观察窗口:一个看控制台日志,一个看本地数据库或内存里的订单状态。下单成功后订单状态应该是“待支付”,支付完成后状态变成“已支付”。如果支付完成很久状态没变化,先别怀疑代码,检查一下 application.yml 里的 notify-url 是否是外网可以访问到的地址。本地用 localhost 跑测试没问题,但支付平台的通知是发到公网的,这就引出第五个避坑章节里一个非常重要的常见问题。
测试完成后可以把日志级别调一下,把 log 里的 http 请求和响应原文打开,方便复盘。在 application.yml 里加一行 logging.level.com.example.paydemo=DEBUG 即可,这样下单请求的完整参数和平台返回都会打出来,排查问题会直观很多。
5. 避坑手册:验签失败、重复通知、金额精度这些坑一次说清
5.1 回调始终提示“验签失败”
现象:收银台能正常跳转,支付也完成了,但回调接口日志里一直打印“验签失败”,订单状态始终没有更新。
原因:签名拼接规则不一致。最常见的有三种:一是参与签名的参数没有排成字典序,二是有字段在传输过程中被 URL 编码了但验签前没做解码,三是参数集合里还带着 sign 字段本身没有剔除。
解决:把平台回调的原始报文完整打出来,对照平台文档里的签名规则逐项核对。先把回调参数里的 sign 值摘除,确认剩余参数全部参与了拼接,再确认拼接顺序是 key=value 按字典序排,最后给所有字符串字段统一过一遍 URLDecoder.decode。这套源码里的 Notification 接口已经处理了这几点,直接抄它的思路即可。
5.2 支付成功但回调一直调不到本地接口
现象:收银台支付完成,页面跳转也正常,但本地接口没有被调用,订单状态一直停在“待支付”。
原因:回调地址是 localhost 或者内网地址,支付平台通知发不到你本机。很多人第一次测试时会忽略这一点,以为本地启动服务就能收到平台通知。
解决:平台后台一般提供“手动模拟回调”功能,直接按回调成功报文的格式给本地接口发一条测试通知;或者把服务部署到一台有公网地址的测试服务器上,再把 notify-url 改成服务器地址。我一般先用模拟回调验证验签逻辑,逻辑通了再部署到测试环境做联调。
5.3 同一笔订单被重复入账
现象:平台在通知失败时会多次重试,如果第一个通知处理成功后,第二个通知又来了,代码里没有拦截,订单会被重复置为已支付状态。换成有数据库的项目,就是订单金额被累加两次,对账时发现钱对不上。
原因:回调处理逻辑没有做幂等判断。能导致重复通知的情况很多:网络超时、程序返回非 success、程序重启过程中丢了响应等。平台默认会一直重试到你明确返回 success 为止。
解决:在回调处理的第二步加一个状态判断,只要订单已经处于 PAID 状态,直接返回 success,不去重复更新。上面 NotifyController 里的代码就是这么处理的,写真实项目时建议在数据库里加唯一索引做双保险,以订单号加支付流水号作为唯一键,双写双校验。
5.4 金额字段用 double 存,出现了科学计数法
现象:下单时传了 0.01 元,结果收到回调后发现金额变成了 1.0E-4 这种数值,签名也对不上了。
原因:金额在数据库或变量里用了 double 类型,计算过程中精度丢失,转成字符串后变成了科学计数法表示,平台和本地的金额值不一致,签名自然验不过。
解决:金额统一用分存储,以整数类型传递。下单、回调、存库全链路都用 int 或 long 表示“分”,仅在页面展示时转换成元。这套源码里金额参数直接叫 amountFen,就是为这个原因设计的。整个过程我踩过一次改名不改类型的坑,后来所有支付相关字段都强制用分做单位,再没出过这个问题。
5.5 测试环境的回调模拟后,本地状态没更新
现象:用平台后台的“模拟回调”功能发了成功通知,接口也收到了,日志显示验签通过,但页面查询订单状态还是“待支付”。
原因:模拟回调发送的报文里,字段名和平台规范可能不完全一致。常见的是 order_id 和 merchant_id 的大小写被平台后台自动规范化了,而代码里取参数时用的是小写。
解决:打印回调完整报文,核对所有字段名是否和代码里取值的写法一致。不一致就统一规范,要么代码适配平台,要么平台后台按商户文档配置报文字段。这种问题排查起来最费时间,因为日志到处都正常,就是状态不变,定位到字段名不匹配才算找到根因。
6. 二次开发改造:从示例源码变成可接线的订单支付模块
这套支付源码能跑通只是第一步,要接到真实项目里,最快见效的改造是引入订单状态枚举,替代现在散落各处的字符串常量:
public enum OrderStatus { WAITING_PAY(0, "待支付"), PAID(1, "已支付"), REFUNDED(2, "已退款"), CLOSED(3, "已关闭"); private final int code; private final String desc; // 构造方法、getter 略 }改造完状态枚举,下一步把签名算法从 MD5 升级成 HMAC-SHA256。易宝支付这类平台一般同时支持多种签名算法,在 PayConfig 里加一个 sign-type 配置项,SignUtil 里根据配置分发到不同算法实现,比直接替换 MD5 更灵活。真实项目里建议把密钥放到配置中心或环境变量里,不要硬编码在 yml 文件里,防止源码泄露导致密钥泄露。
最后补一段“回调核对日志”,每次处理回调时把订单号、金额、签名结果、最终状态打全,格式像这样:
[callback] order=20250101001 amount=10000 verify=success status=PAID日志带上这些字段,后面线上排查问题时不用翻接口文档,一眼就能看出是哪一步出的问题。从那以后我每次接入新的支付渠道,都会先把“下单参数核对表”和“回调验签核对清单”打一遍,确认单位和签名规则无误再动代码。这份源码本身已经把这些经验沉淀进去了,你拿着改改商户号就能跑通,希望帮到你。
本文还有配套的精品资源,点击获取