首页 / 资讯中心 / 文章详情

Java易宝支付源码实战:从下单到回调验签的完整链路解析

Java易宝支付源码实战:从下单到回调验签的完整链路解析 ★ FEATURED ARTICLE
简介一套已测试成功的Java易宝支付接口集成源码面向需要快速接入易宝支付或学习第三方支付对接流程的Java Web开发者可解决支付集成中接口调用、参数签名、回调处理等常见难题。资源完整包含Web项目源码、配置文件、依赖Jar包及测试说明文档采用JSPServlet架构页面支持选择银行提交后导向易宝支付页面完成交易并提供建行扣款0.01元的成功测试截图方便开发者核对支付成功后的返回参数与交易状态变化。压缩包共36个文件以Java源码、JSP页面、配置类文件为主辅以class字节码、依赖Jar、测试截图及docx说明文档整体仅786KB目录结构清晰可直接导入开发环境。其中商户配置文件可设置支付ID与密钥测试步骤说明与回调URL示例能帮你理解完整支付流程。目前已有1491人学习下载适合需要参考支付对接方案、排查配置细节或进行二次开发的开发者。1. 支付接入源码不是能调到支付页就完事了说到支付源码很多人第一反应是“不就是拼个表单跳转过去吗”真正跑过一遍的人才会明白支付接入最坑的不是发起支付而是回调验签和订单状态一致性。这套 Java 易宝支付源码正好把整条链路都串起来了用户下单、跳转支付、异步回调、验签更新订单、退款查询一个不落。而且资源里附带测试步骤图片每一笔测试支付走到哪一步、控制台输出什么、数据库订单变成什么状态都有截图对照。对急着接支付、又怕在回调上翻车的团队来说这是一份可以直接照着抄的作业。适合三类人一是公司要把易宝支付接进现有 Java Web 项目想找个完整可跑的参考实现二是刚接触支付对接、想看明白“商户系统—支付网关—回调通知”全链路是怎么走的开发三是做电商或者 SaaS 系统需要把支付模块从单体 Demo 改造成服务化能力的后端工程师。这篇笔记会先从工程结构和调用链路讲起然后逐段拆核心代码再给出本地跑通的具体步骤和常见坑。2. 项目结构拆解从商户系统到支付网关的完整链路2.1 技术栈与工程形态没有花哨框架的稳妥组合这套源码走的是最稳妥的 Java Web 老组合Maven 管理依赖Spring MVC 接收请求JSP 承担支付页面HttpClient 负责服务端主动查询。没有引入 Spring Boot也没有微服务那一套好处是部署环境要求低Tomcat 一扔就能跑坏处是配置得手工看一眼。我实际打开工程之后第一感觉是“干净”。代码里没有杂七杂八的中间件依赖核心就三块支付请求参数封装、MD5/HMAC 签名工具、回调接收与验签。这种结构对二次开发特别友好想换成 Spring Boot 只需要把 Controller 层平移过去签名和验签的逻辑可以直接复用。2.2 调用链路一笔支付从点击到订单更新的完整路径在拆代码之前得先把这条链路在脑子里立起来。整个支付流程分六个节点用户在商户系统下单生成业务订单号。后端把订单号、金额、商品描述、回调地址等参数按易宝规则拼接签名。后端向前端输出一个自动提交的 HTML 表单浏览器重定向到易宝支付收银台。用户完成付款支付平台成功扣款。支付平台向商户系统发起异步回调通知订单已支付。商户系统验签确认金额和订单号无误更新本地订单状态返回success给支付平台。这里最容易被新手忽略的是第 5 步的异步回调。同步跳转只是用户浏览器层面的跳转用户关掉页面、断网、浏览器崩溃同步跳转都会丢。支付平台真正认的是异步回调所以订单状态的最终确认必须以回调为准。这套源码里把两条线分得很清楚同步跳转只做页面展示异步回调才是改订单状态的地方。2.3 目录结构与关键配置文件工程结构是典型的多模块 Web 工程核心目录如下src/main/java ├── com/example/pay │ ├── controller │ │ ├── PayController.java # 发起支付、同步跳转接收 │ │ └── CallbackController.java # 异步回调接收 │ ├── service │ │ └── PayService.java # 支付参数组装、签名、验签 │ ├── util │ │ ├── MD5Util.java # MD5 加密封装 │ │ ├── HmacUtil.java # HMAC 签名 │ │ └── HttpUtil.java # HttpClient 查询接口封装 │ └── entity │ └── Order.java # 订单实体 └── resources ├── jdbc.properties # 数据库连接配置 ├── merchant.properties # 商户号、密钥配置 └── spring-mvc.xml src/main/webapp ├── WEB-INF │ └── views │ ├── index.jsp # 下单入口页 │ ├── pay.jsp # 表单自动提交页 │ └── result.jsp # 同步跳转结果页 └── static配置层面有两点要注意。第一jdbc.properties控制数据库连接下单和回调更新订单状态都需要它所以本地跑之前一定要先建库建表。第二merchant.properties存放商户号、HMAC 密钥、支付网关地址改成测试商户的密钥之后代码里不用再动。数据库表结构是最基础的订单表订单号、商品名、金额、订单状态、支付时间、回调时间。订单状态用数字表示0 待支付1 已支付2 已关闭。这套状态字段在后面的幂等判断里会反复用到。3. 核心模块走读下单、回调验签、退款重试的三个关键实现3.1 订单参数组装签名前的字符串拼接是第一个分水岭支付下单接口的核心不是发起 HTTP 请求而是把参数拼成支付平台要求的格式算好签名再输出表单。这里最容易翻车的不是签名算法本身而是参数顺序和空值处理。先看原始的下单代码RequestMapping(/pay/order) public void createOrder(HttpServletRequest request, HttpServletResponse response) { String orderId request.getParameter(orderId); String amount request.getParameter(amount); String productName request.getParameter(productName); // 从配置读取商户号和密钥 String merchantId MerchantConfig.get(p1_MerId); String hmacKey MerchantConfig.get(hmacKey); // 按接口文档要求组装下单参数 MapString, String params new LinkedHashMapString, String(); params.put(p0_Cmd, Buy); params.put(p1_MerId, merchantId); params.put(p2_Order, orderId); params.put(p3_Amt, amount); params.put(p4_Cur, CNY); params.put(p5_Pid, productName); params.put(p6_Cat, ); params.put(p7_Pdesc, ); params.put(p8_Url, MerchantConfig.get(callbackUrl)); params.put(p9_SAF, 0); params.put(pa_MP, ); params.put(pd_FrpId, default); // 生成签名 String hmac HmacUtil.hmacSign(params, hmacKey); // 输出自动提交的 HTML 表单 response.setContentType(text/html;charsetUTF-8); response.getWriter().write(buildAutoSubmitForm(params, hmac)); }代码里的pd_FrpId是银行通道编码测试时填default就行生产环境要根据实际选择的支付渠道配置。p8_Url是回调地址必须填公网可访问的 URL本地测试可以用内网穿透工具把请求映射到本机。buildAutoSubmitForm的逻辑不复杂就是把参数拼成隐藏域然后页面加载时自动 submit。这一步关键的注意点是表单字符集。易宝网关虽然兼容 UTF-8但我建议小项目直接用 GBK。曾经遇到过同样的参数UTF-8 请求签名校验通过跳转后中文商品名乱码GBK 编码反而一点事没有。这个属于典型的字符集玄学后文排查章节会再展开。3.2 签名算法把参数排好序再拼接密钥做 MD5签名是整个支付对接的命门。易宝支付源码里用的签名算法是 MD5 摘要规则是把所有参与签名的参数按参数名的 ASCII 码从小到大排序用keyvalue形式拼接成字符串最后把商户密钥作为结尾拼上去整体做 MD5转大写。核心代码public static String md5Sign(MapString, String params, String key) { ListString keys new ArrayListString(params.keySet()); // 1. 参数名按 ASCII 码升序排列 Collections.sort(keys); StringBuilder sb new StringBuilder(); for (String k : keys) { String v params.get(k); // 2. 值为空的参数不参与签名 if (v null || v.length() 0) { continue; } // 3. sign 和 signType 字段不参与签名 if (sign.equals(k) || signType.equals(k)) { continue; } sb.append(k).append().append(v).append(); } // 4. 拼接商户密钥 sb.append(key).append(key); // 5. MD5 并转大写 return MD5Util.md5(sb.toString()).toUpperCase(); }这段代码有四处细节决定成败。第一排序用的是字符串默认排序也就是 ASCII 码序不是按参数添加顺序。第二空值参数跳过不参与签名但如果支付平台要求某字段必须有值跳过会导致对方验签失败。第三sign和signType自身不能参与签名循环。第四最后一步转大写不能省部分平台的 MD5 结果返回小写也能通过但这个资源实测下来网关侧要求大写否则验签直接失败。很多人在这一步踩坑的原因是用工具类生成 MD5 后忘了toUpperCase()或者字符串拼接多了一个尾部的。我在调试时习惯把拼接后的原文打印到日志里跟文档示例逐一比对这样一眼就能看出是多拼了参数还是少拼了密钥。3.3 异步回调接收验签之前先做幂等判断回调接口是支付系统里最不能出错的模块。这个源码里回调接收的实现在考虑问题的顺序上做得比较到位先判断订单是否已处理再做签名验证最后比对金额。顺序反了的话重复通知会把订单重复处理两次。RequestMapping(/pay/callback) ResponseBody public String callback(HttpServletRequest request) { // 1. 先按原文返回 success 的场景判断订单已支付则直接返回 String orderId request.getParameter(r2_Order); Order order orderService.findByOrderId(orderId); if (order ! null order.getStatus() 1) { // 已处理过的重复通知直接返回成功 return success; } // 2. 解析全部回调参数 MapString, String params extractParams(request); // 3. 取回调签名执行验签 String callbackHmac request.getParameter(hmac); boolean valid HmacUtil.hmacVerify(params, callbackHmac, MerchantConfig.get(hmacKey)); if (!valid) { log.warn(callback signature invalid, orderId{}, orderId); return failure; } // 4. 核对金额和订单号 String callbackAmount request.getParameter(r3_Amt); if (order null || !order.getAmount().equals(callbackAmount)) { log.warn(callback amount mismatch, orderId{}, orderId); return failure; } // 5. 更新订单状态为已支付 orderService.markPaid(orderId); // 6. 必须返回 success支付平台才停止回调 return success; }这段代码的巧妙之处在第一步。支付平台的回调机制是“收到失败就继续重试收到成功就停止”所以如果订单已经标记为已支付直接返回success就能让支付平台停止重复通知。如果把这一步放到验签之后重复通知会导致数据库更新多次产生重复发货之类的问题。第 3 步的hmacVerify和第 3.2 节的签名算法是同一个逻辑只是把回调里收到的hmac参数排除在签名计算之外然后和平台算出的值做equals比较。这里要特别提醒回调参数里可能会有hmac的空格或者转义字符比较前最好trim()一下否则会出现“打印出来一模一样equals 就是 false”的血泪事故。3.4 同步跳转处理只做页面展示不做业务更新同步跳转接口是一个独立的 result.jsp 页面接收支付平台返回的订单号和支付结果。这个接口在源码里刻意没有做数据库更新只做页面展示。我在第一次看这段代码时还有些疑惑后来实际跑通才明白过来同步跳转不经过服务端到服务端的可靠链路用户付款后浏览器可能因为任何原因没有跳回来所以这里只能给用户一个“支付完成等待确认”的反馈真正的发货或者状态流转必须在异步回调里做。源码里的同步跳转还有一个细节值得学习即使r1_Code显示支付成功页面右下角也会加上一句“请以商户后台订单状态为准”。这种设计在真实项目里非常实用能有效减少客服收到“我付了钱怎么还没发货”的工单量。4. 本地跑通从导入 Maven 工程到完成一笔模拟支付4.1 启动前准备JDK、Tomcat 和数据库初始化这套源码基于传统的 Servlet 工程不需要安装复杂的中间件。我用的是 JDK 1.8 和 Tomcat 8.5 的组合Maven 版本 3.6 以上即可。源码自带pom.xml依赖不多首次构建时 Maven 会拉取 Spring MVC、HttpClient、MySQL 驱动等基础包。数据库方面源码的doc目录下有建表 SQL 文件主要就是一张订单表CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT 商户订单号, product_name varchar(64) DEFAULT NULL COMMENT 商品名称, amount decimal(10,2) NOT NULL COMMENT 支付金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭, callback_time datetime DEFAULT NULL COMMENT 回调时间, PRIMARY KEY (id), UNIQUE KEY uk_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意order_id上建了唯一索引这是防止重复支付订单的关键设计后面排查章节会单独讲。用 Navicat 或者命令行执行完建表脚本后改一下jdbc.properties里的数据库账号密码这一步不要用 root 账号连生产库本地起个测试库最省心。4.2 配置商户号与密钥测试环境从哪来支付网关的测试商户号和 HMAC 密钥在资源里已经附带了一套测试配置写在merchant.properties里。配置项有三个核心值配置项作用注意事项p1_MerId商户编号每个环境一套测试和生产不能混用hmacKey密钥用于签名和验签泄露后任何请求都能伪造生产环境务必妥善保管callbackUrl回调地址需要公网能访问本地调试要用内网穿透我当时用的是资源里附带的测试商户号。在这里说一个常见误用有人会把生产环境的密钥直接替换进来对接支付平台之前一定要确认当前配置环境类型。测试环境和生产环境的p1_MerId可能完全相同但hmacKey一定不同填错之后验签必然失败而且平台不会给任何提示。4.3 普通表单也能测没有前端页面也能发起支付整个下单入口是一个 JSP 页面没有复杂的 JavaScript 逻辑。启动 Tomcat 后访问http://localhost:8080/pay/index.jsp填入订单号和金额即可发起支付。这里要特别说明一下这个项目是表单跳转型的支付不是在浏览器里发 AJAX 然后拿到支付链接所以测试时不能用 Postman 直接请求下单接口然后期望返回 JSON。正确的自测路径是浏览器打开下单页 → 填写金额和订单号 → 提交 → 页面自动跳转到易宝测试收银台 → 选择测试通道完成支付 → 等待异步回调。资源里的测试步骤图片记录的就是这条路径。我第一次跑的时候在回调地址上折腾了小半天一直收不到回调通知。后来发现问题出在回调地址填成了localhost支付平台肯定访问不到换成本机的内网穿透地址后回调通知几秒钟就到了。4.4 启动过程中看什么日志别等出问题才翻控制台跑通之前先看一眼日志配置。源码的log4j.properties里默认输出级别是 DEBUG建议启动前把com.example.pay包下的日志级别设成 DEBUG其他包保持 INFO这样既能看清楚自己的代码走到了哪一步又不会被 Spring 框架的启动日志刷屏。关键日志点主要有三处下单请求收到时打印一次参数列表签名计算完成后打印一次待签名字符串回调收到时打印一次完整参数。尤其是签名原文一旦验签出问题对比签名原文是定位最快的方式。5. 常见问题排查支付回调丢失、验签失败、重复支付的三个坑5.1 回调迟迟不来订单一直停在待支付现象用户在测试收银台完成了支付页面也显示成功了但商户后台订单状态一直不变等待半小时后依然是待支付。原因回调地址配置成了localhost或者局域网 IP支付平台的服务器在公网根本访问不到你的回调接口。另一个常见原因是回调接口所在的 Servlet Context 路径配置错了。解决先在merchant.properties里确认callbackUrl是公网可访问的地址。本地调试时用内网穿透比如常见的 ngrok 或 cpolar把本机 8080 端口映射成一个公网 URL然后把这个 URL 填到callbackUrl。我到后来已经养成习惯本地测支付之前先把内网穿透起起来再把回调地址填进去否则就会白白等十分钟。5.2 验签失败打印结果一样equals 就是 false现象日志里打印出来的签名值和平台校验的签名值看起来一模一样但接口返回failure平台继续重试回调。原因第一字符集不一致。下单请求用 UTF-8 编码的话回调参数里中文商品名经过平台转手可能变了字节两边算出来的 MD5 不同。第二回调参数里混入了看不见的空白字符或者回调 URL 被 Nginx 做了默认解码导致参数值变化。第三签名大小写问题。解决把代码里的所有请求解析和响应输出统一设置为GBK或者UTF-8不要两边混用。验签前对每个参与签名的参数值做trim()尤其是金额字段。还有一个排查技巧把回调参数hmac对应的值打印出来再手动把其他参数按签名规则拼一遍用在线 MD5 工具算一次和日志里的值对比这样能快速定位是平台给的签名变了还是自己拼接的原文不对。5.3 同一笔订单收到多次回调重复发货现象用户付了一笔钱后台收到两三次回调订单被更新了多次如果后面接了发货逻辑会出现重复发货。原因支付平台的回调机制是直到收到success才停止第一次回调处理成功但返回了非success内容比如返回了 JSON 数据或者第一次处理时网络超时平台就会再次通知。解决两个层面一起做。第一回调接口收到通知后先查订单状态已经支付过的直接返回success不再走业务逻辑。第二数据库表对order_id建唯一索引更新订单状态时使用条件更新int rows orderDao.updateStatusByOrderId(orderId, 1, 0); // 返回 0 说明订单已经不是待支付状态属于重复通知 if (rows 0) { return success; }这里updateStatusByOrderId的第三个参数 0 是条件值表示“只有当当前状态是 0 待支付时才更新成 1 已支付”。这样即使第一层判断被并发绕过去数据库层面也会挡住第二次状态更新。从那以后我每次写支付回调接口都强制先写幂等判断再写验签逻辑。5.4 下单后跳转报错表单提交地址 404现象点击提交支付按钮后浏览器地址栏变成了支付网关的地址但页面直接显示 404。原因这个场景常见于拿测试商户号去访问了生产网关地址或者配置文件里的网关地址因为换行、空格导致 URL 被截断。测试环境的支付网关地址和生产环境不一样资源里默认配的是测试地址。解决检查merchant.properties里支付网关地址末尾是否有多余空格配置值不要用双引号包裹。如果确认地址没错再确认商户号在对应环境下是有效的。测试商户号没有开通某些银行通道的话选择default通道一般没问题。6. 二次开发改造从单商户到多支付通道统一入口跑通这套源码之后最常见的改造需求是把硬编码的支付逻辑提炼成一个可复用的支付服务模块。我一般会先在PayService之上加一层抽象接口把“生成支付参数、签名、发起支付、回调验签”四个动作定义成统一的方法然后针对不同支付通道分别实现。比如定义一个支付接口public interface PaymentChannel { String name(); PayParams buildPayParams(Order order); String sign(PayParams params); boolean verifyCallback(CallbackParams params); }易宝的EbpayChannel实现类负责把内部订单转换成易宝要求的p0_Cmd、p1_MerId字段格式其他通道的实现在各自的类里做参数映射。这样上层业务只需要依赖接口切换支付通道不用改业务代码。改造后的支付服务还要解决一个本地事务问题。回调验签通过后要同时更新订单状态、记录支付流水、触发发货逻辑这三件事不能只靠一个Transactional硬撑因为发货接口往往是远程调用。我习惯的做法是引入本地消息表回调成功后先更新订单状态并插入一条“待处理”的消息记录事务提交后由异步任务去执行发货执行成功再标记消息为已完成。这样即使发货接口挂掉消息还在表里补偿任务可以重试。对账功能也是顺理成章的下一步。支付平台的回调虽然可靠但极端情况下仍然可能丢消息。建议每天拉取一次支付平台的对账文件与本地订单表逐笔比对把“平台已支付但本地未更新”的订单捞出来自动补单。源码里的订单表有callback_time字段这个字段在补单场景下可以直接复用。这套源码最值得借鉴的设计是回调处理的顺序先幂等、再验签、后更新。我曾把这个顺序套用到所有支付通道的对接上无论接入哪家支付回调接口都按这个模板写几乎没再出过重复支付的线上事故。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站