简介:面向Java开发者的易宝支付接口对接源码包,包含完整可运行的Eclipse项目工程与实测记录。演示了从提交订单、跳转易宝支付、银行扣款到返回支付结果的全流程,适合需要快速接入易宝支付、完成毕业设计或进行第三方支付二次开发的开发者参考。包内共36个文件,以Java源码、JSP页面和class文件为主,配有properties/xml配置文件、jar依赖、数据库文件、说明文档及测试截图,压缩包仅786KB,目录划分清楚,便于按模块查看。已有1485人学习下载,说明该案例具备不错的实践参考价值,尤其适合正在开发支付模块的中小型项目团队。资源附有详细测试步骤图片,可对照配置文件切换商户ID与密钥,实际验证建行等银行渠道0.01元扣款,帮助读者理解支付结果通知与回调处理机制,避开常见对接陷阱。 先说结论:这套 Java 对接易宝支付的源码,我拿到手之后在本地完整跑通过,从下单、跳转支付、异步回调到订单查询,全链路都验证过有好几次了。整个过程里踩了几个比较典型的坑,尤其是签名规则和回调验签那一块,网上资料讲得比较零散,我这篇直接把能用的代码、配置、测试步骤和注意点全部整理出来,给正在做支付对接的兄弟做个参考。
很多刚接触支付接口的开发者,第一次看易宝支付的文档都会有点懵。它的接口协议不是现在常见的 RESTful JSON 风格,而是基于 HTTP POST 的键值对报文,加密方式也是传统的 AES + RSA + MD5 混合体系,跟微信支付宝那种“下载 SDK 直接调”的体验完全不一样。所以如果你是从零开始接,光理清它的加解密流程就得花不少时间。这篇博文假设你已经有一个基础的 Spring Boot 项目,能跑通一个 Controller 就够,我会把核心代码直接贴出来。
1. 整体设计思路与选型考虑
1.1 为什么选易宝支付,以及适合什么场景
易宝支付算是国内老牌第三方支付平台了,行业解决方案覆盖航空旅游、行政教育、B2C 电商这些领域。它跟微信、支付宝最大的区别在于:易宝更侧重于 PC 端和 B2B 场景,商户后台支持多级商户体系、分账结算、信用卡大额支付这些能力。如果你做的是面向企业用户的系统,比如缴费平台、会员充值、机票酒店预订,易宝的支付成功率跟稳定性其实很能打。
另外一个很实际的理由是资质和费率。易宝对个体户、小微商户的入驻门槛相对宽松,支持的银行数量也多,尤其是信用卡大额通道,微信支付宝在这些场景下限制比较多,易宝反而灵活。我当时接手这个项目,就是因为业务方要求必须支持对公转账和信用卡大额支付,一类户单笔限额都到了几十万,这才选了易宝。
1.2 对接前的三件套准备:参数、证书、回调地址
在写代码之前,先把这些材料备齐,不然开发到一半发现缺东西就很尴尬:
- 商户编号(merchantId):易宝分配给商户的唯一编号,形如 1001 开头的数字串。
- 商户密钥(merchantKey):用于 MD5 签名和 AES 密钥解密,后台可重置。
- AES 密钥:易宝后台生成的 16 位密钥,用于解密回调报文中的敏感信息。
- RSA 公钥:易宝提供的公钥,用于加密商户密钥传输。
- 回调地址(callbackUrl):接收支付结果通知的公网地址,本地测试可以用内网穿透工具临时映射。
注意:测试环境的商户编号跟生产环境是两套,测试环境通常以 100 开头,生产环境是正式审核通过后才分配的。千万别拿测试密钥去请求生产接口,会直接返回「商户不存在」。
1.3 技术架构与工程结构
我用的技术栈是 Spring Boot 2.7 + JDK 8 + Maven,工程结构比较简单,按支付能力做了分包:
com.example.yeepay ├── config │ └── PayConfig.java ├── controller │ └── PayController.java ├── service │ ├── PayService.java │ └── PayServiceImpl.java ├── utils │ ├── YeepayUtils.java │ ├── AESUtil.java │ ├── RSAUtil.java │ └── HttpUtil.java └── dto ├── PayOrderRequest.java ├── PayOrderResponse.java └── CallbackRequest.java为什么这么分?支付这块逻辑最容易乱的就是加解密和 HTTP 通信,如果混在业务代码里,后面排查问题会很痛苦。单独抽出 utils 包,方便复用,也方便单测。这里给个建议:所有第三方交互的出入参,一律用 DTO 封装,不要直接用 Map,后面维护会爽很多。
2. 核心功能拆解与关键技术点解析
2.1 易宝支付的加密体系到底是怎么回事
易宝支付的老版接口用的是三层加密:
- MD5 签名:使用商户密钥对关键参数做 MD5 摘要,保证参数没有被篡改。
- AES 加密:用于传输敏感字段,比如银行卡号、身份证号,AES 密钥是商户在后台自己设置的。
- RSA 加密:用于安全传输 AES 密钥,也就是用易宝公钥加密 AESKey 后随请求发送。
这个设计在当年是很标准的金融级加密方案,但现在看确实有点重。好消息是,如果只是做标准网银支付或者移动支付收单,很多敏感字段用不到,核心流程只需要处理 MD5 签名和 AES 解密回调报文。像是“一键支付”这类需要绑定银行卡的场景,才需要完整走三层。
初学的时候容易搞混的一个点:MD5 签名和对 AES 密钥的 RSA 加密不是一回事。MD5 是对“业务参数拼接串”做摘要,防止参数被篡改;RSA 加密是为了把 AES 密钥安全地传给易宝,防止密钥在传输中泄露。两个动作目的不同,缺一不可。
签名时的参数排序规则也比较讲究。易宝要求把所有参与签名的参数按照字典序升序排列,然后拼接成 key1=value1&key2=value2 的格式,最后在末尾拼接上商户密钥,再做 MD5 摘要。
2.2 支付流程的状态机设计
我的支付服务里维护了一个订单状态字段,整个生命周期是这样的:
待支付 → 支付中 → 已支付(同步) → 已回调确认(异步) → 已退款 ↘ (支付失败)→ 关闭/重试这里有个很关键的实践经验:永远不要只依赖同步跳转返回值来更新订单状态。因为同步返回只是“用户从支付页面被跳转回来”,并不能 100% 保证支付成功,用户可能支付的页面没输完密码就关了,同步返回说的是“已受理”,不是“已成功”。真正的“上帝视角”是异步回调通知,必须在回调里做状态流转的最终确认。
所以在设计数据库时,我给订单表加了三个字段:
pay_status TINYINT COMMENT '支付状态 0待支付 1支付中 2成功 3失败 4退款', callback_status TINYINT DEFAULT 0 COMMENT '回调通知处理状态 0未处理 1已处理', callback_time DATETIME COMMENT '最后回调通知时间'每次回调处理完之后,必须做幂等判断:如果 callback_status 已经是 1,直接返回成功,防止重复通知对订单造成覆盖。
3. 实操过程与核心代码实现
3.1 配置参数加载
配置文件 application.yml 里,我把所有易宝参数都集中管理了:
yeepay: merchant-id: 100157xxx merchant-key: abcdef1234567890 aes-key: 1234567890abcdef rsa-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ... # 易宝公钥,按实际配置 pay-url: https://api.yeepay.com/app-pay callback-url: https://your-server.com/api/pay/callback query-url: https://api.yeepay.com/query然后建对应的配置类:
@Data @Component @ConfigurationProperties(prefix = "yeepay") public class PayConfig { private String merchantId; private String merchantKey; private String aesKey; private String rsaPublicKey; private String payUrl; private String callbackUrl; private String queryUrl; }这里有个细节:配置类的字段名要和 yml 严格对应,或者用 @ConfigurationProperties 的 relax binding 规则写成 merchant-id 也能自动映射到 merchantId。建议统一用驼峰,不然容易埋坑。
3.2 MD5 签名工具类
public class YeepayUtils { public static String md5Sign(Map<String, String> params, String merchantKey) { // 1. 过滤空值,去掉 sign 和 sign_type 本身 Map<String, String> filtered = new TreeMap<>(); for (Map.Entry<String, String> entry : params.entrySet()) { if (entry.getValue() != null && !"".equals(entry.getValue().trim()) && !"sign".equals(entry.getKey()) && !"sign_type".equals(entry.getKey())) { filtered.put(entry.getKey(), entry.getValue()); } } // 2. 按字典序拼接 key=value&key=value StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : filtered.entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } String raw = sb.substring(0, sb.length() - 1); // 去掉最后的 & raw = raw + merchantKey; // 密钥放最末尾 // 3. MD5 摘要,转大写 return DigestUtils.md5Hex(raw.getBytes(StandardCharsets.UTF_8)).toUpperCase(); } }为什么用 TreeMap?因为它默认按 key 的字典序排序,省得手动 sort。拼接格式别搞错,密钥是直接拼接不加 & 的,有些文档写的是 key=value&key=value&key=密钥,注意校验易宝官网给的示例报文,以实际验证结果为准。
提醒:MD5 摘要转大写还是小写,以易宝文档为准。我接的这套要求大写,但有些银行接口要求小写,好多人就是死在这儿,一直报签名错误。
3.3 发起支付下单请求
下单接口的 DTO 长这样:
@Data public class PayOrderRequest { private String p0_Cmd = "Buy"; private String p1_MerId; // 商户编号 private String p2_Order; // 订单号 private String p3_Amt; // 金额,单位元,支持两位小数 private String p4_Cur = "CNY"; private String p5_Pid; // 商品名称 private String p6_Pcat; // 商品类别 private String p7_Pdesc; // 商品描述 private String p8_Url; // 回调地址 private String p9_SAF = "1"; // 应答机制 0立即 1延迟 private String pa_MP; // 商户扩展信息 private String pd_FrpId; // 支付通道编码,如 CMB 招商银行 private String pr_NeedResponse = "1"; // 是否需要应答机制 private String sign; // 签名 }核心逻辑:
public String createPayOrder(PayOrderRequest request, PayConfig config) { // 必填参数校验 if (StringUtils.isBlank(request.getP1_MerId())) { request.setP1_MerId(config.getMerchantId()); } if (StringUtils.isBlank(request.getP8_Url())) { request.setP8_Url(config.getCallbackUrl()); } // 组装签名参数 Map<String, String> params = new HashMap<>(); params.put("p0_Cmd", request.getP0_Cmd()); params.put("p1_MerId", request.getP1_MerId()); params.put("p2_Order", request.getP2_Order()); params.put("p3_Amt", request.getP3_Amt()); params.put("p4_Cur", request.getP4_Cur()); params.put("p5_Pid", request.getP5_Pid()); params.put("p6_Pcat", request.getP6_Pcat()); params.put("p7_Pdesc", request.getP7_Pdesc()); params.put("p8_Url", request.getP8_Url()); params.put("p9_SAF", request.getP9_SAF()); params.put("pa_MP", request.getPa_MP()); params.put("pd_FrpId", request.getPd_FrpId()); params.put("pr_NeedResponse", request.getPr_NeedResponse()); String sign = YeepayUtils.md5Sign(params, config.getMerchantKey()); request.setSign(sign); // 转成表单,直接 POST 跳转到易宝支付页面 return buildPayHtml(request); }注意 p3_Amt 的格式。金额单位是元,但不要用 float/double 去算,不然会出现精度问题,比如 99.99 实际可能是 99.99000000000001。建议用 BigDecimal,或者把元转成分在内部处理,传参时再格式化保留两位小数。
buildPayHtml 这个方法是把请求参数拼成一个自动提交的 HTML 表单,核心思路是让用户的浏览器直接 POST 到易宝网关,完成跳转支付:
private String buildPayHtml(PayOrderRequest req) { StringBuilder html = new StringBuilder(); html.append("<html><body>"); html.append("<form id='payForm' action='").append(payUrl).append("' method='post'>"); // 反射遍历字段拼 hidden input ... html.append("</form>"); html.append("<script>document.getElementById('payForm').submit();</script>"); html.append("</body></html>"); return html; }这样做的原因是易宝老版网银接口不支持后端 JSON 调用,必须通过浏览器表单提交来完成跳转。如果是 App 内嵌入,可以让 WebView 加载这个 HTML。
3.4 接收异步回调与验签解密
回调是支付成功后的“正式通知”,必须认真处理。易宝回调的 Content-Type 是 application/x-www-form-urlencoded,参数跟下单参数基本一致,额外多了 r1_Code(支付结果 1 成功 2 失败)、r6_Order 等字段。
后端接收代码:
@PostMapping("/api/pay/callback") public String payCallback(HttpServletRequest request) { Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (Map.Entry<String, String[]> entry : requestParams.entrySet()) { params.put(entry.getKey(), entry.getValue()[0]); } // 1. 验签 String sign = params.get("sign"); String localSign = YeepayUtils.md5Sign(params, payConfig.getMerchantKey()); if (!sign.equals(localSign)) { return "sign error"; } // 2. 判断业务状态 String resultCode = params.get("r1_Code"); String orderId = params.get("r6_Order"); String amount = params.get("r3_Amt"); if ("1".equals(resultCode)) { // 3. 幂等处理+订单更新 boolean handled = payService.handleSuccessOrder(orderId, amount); if (handled) { return "success"; // 只有返回 success,易宝才停止回调 } } return "fail"; }易宝的异步回调机制是:如果商户没有返回 success,它会隔一段时间重新通知,最长持续数天。所以即使处理逻辑有 bug,也不要直接给易宝返回成功,否则会造成“单边账”。
除了验签,还要比对金额:回调里的 r3_Amt 必须和本地订单的应付金额一致。在实际生产环境,有的攻击者会尝试伪造回调报文,但因为没有密钥,所以签名过不了。但为了稳妥,金额比对这一步必须有。
3.5 订单查询与主动对账
回调有概率因为网络原因延迟或者丢失,为了可靠,还必须提供一个主动查询接口。易宝订单查询接口的参数相对简单:
public QueryResult queryOrder(String orderId) { Map<String, String> params = new HashMap<>(); params.put("p0_Cmd", "Query"); params.put("p1_MerId", payConfig.getMerchantId()); params.put("p2_Order", orderId); String sign = YeepayUtils.md5Sign(params, payConfig.getMerchantKey()); params.put("sign", sign); // 使用 HttpClient 发送 POST 请求,解析返回的 XML/键值对 String response = HttpUtil.post(payConfig.getQueryUrl(), params); return parseQueryResult(response); }返回结果里有一个 r1_Code 字段表示查询结果,但注意:这个 r1_Code 的含义和回调里的 r1_Code 不完全一样,查询接口里 1 表示查询成功,而具体的订单支付状态要看 rb_PayStatus。易宝老接口的字段命名确实比较绕,建议把该接口的返回字段说明打印出来逐个对照。
我的做法是写一个定时任务,每 10 分钟扫描一次处于“待支付”状态且创建时间超过 30 分钟的订单,调查询接口确认状态,如果有支付成功但回调没到的,通过查询接口反查后手动补单。
4. 实测环境搭建与测试步骤实录
4.1 本地测试环境准备
测试用到的工具和账号:
- JDK 8+,Maven 3.6+
- 一个内网穿透映射工具,比如 natapp、花生壳、cpolar,目的是把本地服务暴露成一个公网 HTTPS 地址,让易宝能回调到
- 易宝商户后台的测试号
本地启动 Spring Boot 项目后,用 cpolar 把 8080 端口映射出去,会生成一个公网地址,比如https://abc123.cpolar.cn。把后台 pai_Url 和下单请求的 p8_Url 都指向https://abc123.cpolar.cn/api/pay/callback,这样就能实时收到回调。
注意:内网穿透工具的免费版域名是随机变化的,每次重启可能变,所以要先启动工具再填回调地址,最好用付费版固定域名,测试起来方便很多。
4.2 测试步骤全过程记录
整个测试流程我整理了可以照着走的步骤:
- 启动内网穿透工具,拿到公网地址。
- 在易宝商户后台确认测试参数,核对 merchantId、merchantKey、AES Key。
- 启动 Spring Boot 项目,访问下单接口
http://localhost:8080/api/pay/create?orderId=20250101001&amount=0.01。 - 接口返回 HTML 页面,浏览器自动重定向到易宝支付收银台。
- 选择“网银支付”,在测试环境使用易宝提供的测试银行卡号进行支付。
- 支付成功后,观察浏览器同步跳转;同时检查自己的服务日志,确认异步回调是否到达。
- 在台账表里核对订单状态,确认从“待支付”变为“已支付”。
- 在易宝商户后台的交易记录里核对这笔测试单,确认金额和订单号一致。
关键测试点我截图留档了:下单请求日志、易宝返回的 HTML、收银台页面、支付成功页、回调日志、数据库订单状态变化。这些截图不管是对自己复盘还是后面跟业务方讲解,都非常有说服力。
4.3 测试中遇到的签名错误排查
我测试时第一次下单就报签名错误(Sign Error),排查过程分享下:
- 检查 MD5 拼接串是否和文档一致,重点看密钥位置和是否过滤空值。
- 看看是不是 TreeMap 排序规则有误,易宝要求的是按 ASCII 码升序,Java 默认字符串排序就符合这个规则。
- 把最终生成的待签名字符串打印出来,用在线 MD5 工具算一遍,和服务算的一致,再去和易宝文档给的示例报文比对。
- 最后发现问题是草率的把参数 pr_NeedResponse 拼写错了,易宝文档里是大小写敏感的,改成对的之后签名就通过了。
这里插一句:在线 MD5 工具只推荐在测试环境用来核对签名逻辑,生产密钥千万别拿出去算,防人之心必须有。
5. 常见问题与避坑指南
5.1 回调收不到或者延迟严重怎么办
- 先看内网穿透工具是否还活着,很多免费映射域名响应慢,易宝的回调超时时间很短,映射工具不稳定会直接丢通知。
- 查看项目日志,看有没有因为验签失败而丢弃回调。
- 这种情况一定要有定时任务主动查单兜底,不能只等回调。
- 回调处理逻辑务必轻量,不要做大量 DB 操作或调外部接口,容易超时,易宝那边就报失败然后继续重试。
5.2 金额精度导致支付失败
我见过有同事在测试金额时直接传了0.01的字符串,没问题,但后来改成amount = total * 0.01这类 double 运算后,生成了类似0.01000000000001的无理数,导致易宝校验金额失败。解决方案是统一用 BigDecimal,或者金额以分为单位存储,展示时再格式化。
5.3 内网穿透导致HTTP回调被拒
易宝要求回调地址是公网可以访问的,如果在内网直接用局域网 IP 配回调地址,调试没问题,但易宝那边是公网请求回调,它根本访问不到你电脑的局域网 IP,所以支付成功后的回调肯定收不到。我的做法是,平时开发特意开一个 mapping 服务做本地联调,每次改完代码重新启动之后确认一次映射地址可用。
5.4 异步回调重复通知
易宝为了防止通知丢失,达到“最终成功”,会重复通知直到商户返回“success”。所以接口处理必须天然幂等。我的建议是:在回调处理的方法上加 synchronized 或者用分布式锁,保证同一个订单串行处理;另外给 orders 表加一个 callback_status 字段,处理成功后再置为 1,重复回调直接返回成功不处理。
5.5 RSA 公钥和 AES 密钥容易配置错
老版易宝有两种密钥体系,有的接口要 AES,有的要 RSA,还有的签名要用证书。建议把后台的所有密钥下载保存到一处,标明用途和生效日期。我测试的时候就把 AES 密钥和 RSA 公钥搞反了,解密回调一直失败,花了一个多小时才排查出来。
6. 后续扩展与生产部署建议
支付模块上线前必须做好这几件事:
- 日志打印规范:支付相关的请求、响应、签名、验签结果必须打印完整日志,方便查单和对账。我在日志里专门用
[YEEPAY-REQ]和[YEEPAY-RESP]的前缀做标记,排查问题时 grep 一下就出来了。 - 监控告警:对支付接口的失败率、成功率做监控,这个可以直接用 Spring Boot Actuator + Prometheus + Grafana 来做,一旦支付成功率跌破阈值就告警到群里。
- 数据库字段预留:订单表要预留扩展字段,比如易宝的交易流水号、支付渠道编码、回调原始报文等。我建议把回调的原始报文 JSON 序列化后存到一张 pay_callback_log 表,后面有任何纠纷可以直接翻原始记录。
- 多环境隔离:Dev、Test、Prod 三套易宝商户号必须隔离,不能因为测试方便就在生产配置里写测试密钥。我在启动脚本里通过
--spring.profiles.active=test来切换环境,配置文件分开,避免人工改错。
还有一个小技巧:因为易宝老接口返回的是类 HTML 的键值对格式,不是标准 JSON,建议封装一层 “ResponseParser”,兼容多种 Content-Type 的解析,为后续升级新版的 JSON 接口做准备。
希望这篇基于实际踩坑整理出来的文章能帮到你,对接支付类的活儿,关键是细心和耐心。如果卡在签名、验签这些环节,按上面的排查思路走一遍,基本都能解决。
本文还有配套的精品资源,点击获取