简介:面向Java后端开发者与电商项目实践者的H5微信支付接入示例包,聚焦在网页端完成微信支付闭环这一常见需求,适合已具备Java基础、希望打通统一下单、支付回调与订单查询等环节的中级开发者参考。压缩包共11个文件、约16KB,以5个java源码与3个xml配置为主体,辅以properties配置文件、html支付页面和doc部署说明,覆盖从后台接口调用到前端唤起支付控件的基本结构。目前已有1320人学习下载,可作为轻量级接入模板使用。资源围绕统一下单获取预支付标识、生成支付链接、异步回调验签、订单状态查询及异常重试等关键流程展开,并附测试环境调试与安全合规提示,便于读者快速理解Java侧与微信支付接口的对接方式,对照自身项目梳理参数签名、回调处理与部署配置,减少从零摸索的成本。
1. JavaH5微信支付:一份能直接跑通的下单到回调链路资源
做后台的同行大概都遇到过这种场面:前端页面在微信里打开,用户点“立即支付”,后端要在一两秒内拼出预支付参数、拉起收银台,支付完还得接住异步回调把订单状态改对。链路不长,但每一环都有坑——签名算法对不上、回调验签失败、金额单位搞错、重复通知把库存扣两次。这份 JavaH5微信支付 资源包,就是围绕这条链路整理的一套可运行参考实现,覆盖统一下单、H5 拉起、异步通知验签、订单状态机与幂等处理几个核心环节。它适合正在接微信支付、或者接过但被回调折腾过的 Java 后端,也适合想搞清楚“一笔支付到底经过哪些步骤”的新手照着跑一遍。下面按“先看清结构、再动手跑、最后避坑”的顺序拆。
2. 拆开资源包:模块划分与关键类职责
拿到一个支付相关的源码包,我一般不会急着跑,先看目录结构和依赖,判断它是“能跑的工程”还是“贴片代码合集”。这份资源按典型 Spring Boot 分层组织,支付相关逻辑集中在独立模块里,和业务订单模块解耦,这个设计本身值得借鉴——支付是横切关注点,混在订单服务里后期很难维护。
2.1 目录结构与依赖清单
常见做法是分成pay-core(签名、配置、HTTP 客户端)、pay-channel-wechat(微信渠道实现)、pay-demo(可启动的示例应用)三层。核心依赖大致如下表,版本以资源包内pom.xml为准,这里只列用途,不编造具体版本号。
| 依赖 | 用途 | 备注 |
|---|---|---|
| Spring Boot Web | 提供下单接口与回调入口 | 回调接口需公网可达 |
| HTTP 客户端(OkHttp 或 RestTemplate) | 调用微信统一下单 API | 需支持超时与重试配置 |
| JSON 库(Jackson / Fastjson) | 请求体序列化、回调报文解析 | 注意字段顺序影响签名 |
| 签名工具类 | MD5 / HMAC-SHA256 签名 | 资源内自带,勿自行替换 |
| 日志组件 | 记录请求与回调原文 | 排查问题的主要依据 |
依赖里最容易被忽视的是 JSON 库。微信签名对参数顺序敏感,某些库默认会重排字段或过滤空值,导致本地算出的签名和服务端不一致。资源包里签名工具类自己维护了有序 Map,就是为了绕开这个玄学问题。
2.2 核心类职责与调用关系
支付链路的入口通常是WechatPayService,它内部持有配置对象WechatPayConfig(存商户号、密钥、证书路径)、签名器Signer、HTTP 客户端。下单方法大致做四件事:组装参数、签名、发请求、解析预支付信息。回调处理则由NotifyController接收,交给NotifyHandler验签、解析、更新订单。
调用关系上有个细节值得说:资源把“验签”和“业务处理”拆成两步,验签失败直接返回失败响应,业务处理放在事务里。这个拆分很关键,因为微信回调可能重复推送,业务处理必须幂等,而验签是纯计算,不该混进事务。读代码时重点看NotifyHandler里订单状态判断那段,它决定了重复通知会不会把库存扣两次。
3. 跑通下单链路:统一下单参数与 H5 拉起
这一章是动手部分。目标是从零跑通一笔 H5 支付:后端生成预支付参数,前端在微信内拉起收银台。先把配置准备好,再写下单接口,最后处理返回结构。
3.1 配置项与商户参数准备
配置一般放在application.yml,敏感信息走环境变量,别硬编码进仓库——这是血泪经验,密钥泄露的后果比代码写错严重得多。资源包里的配置类大致长这样:
@Component @ConfigurationProperties(prefix = "wechat.pay") public class WechatPayConfig { private String appId; // 公众号/应用 AppID private String mchId; // 商户号 private String apiKey; // API 密钥,用于签名 private String notifyUrl; // 异步回调地址,必须公网可达 private String tradeType = "MWEB"; // H5 支付固定为 MWEB // getter / setter 省略 }参数说明:appId和mchId必须与商户平台绑定关系一致,填错会在下单时直接报“appid 与 mch_id 不匹配”;apiKey是签名密钥,不是证书私钥,两者别混;notifyUrl必须是 https 且公网能访问,本地调试要用内网穿透工具映射出去,否则回调永远收不到。tradeType对 H5 支付固定MWEB,写成JSAPI会拉起失败。
3.2 统一下单请求组装与签名
下单接口的核心是组装参数并签名。资源包里的实现大致如下,我保留了关键注释:
public Map<String, String> createH5Order(String orderNo, long amountFen, String description) { Map<String, String> params = new TreeMap<>(); // TreeMap 保证字典序,签名必需 params.put("appid", config.getAppId()); params.put("mch_id", config.getMchId()); params.put("nonce_str", UUID.randomUUID().toString().replace("-", "")); params.put("body", description); params.put("out_trade_no", orderNo); // 商户订单号,全局唯一 params.put("total_fee", String.valueOf(amountFen)); // 单位:分,不是元 params.put("trade_type", "MWEB"); params.put("notify_url", config.getNotifyUrl()); params.put("scene_info", buildSceneInfo()); // H5 必需,含终端 IP String sign = Signer.sign(params, config.getApiKey()); // MD5 或 HMAC-SHA256 params.put("sign", sign); String xml = XmlUtil.toXml(params); // 微信 v2 接口用 XML String resp = httpClient.post("https://api.mch.weixin.qq.com/pay/unifiedorder", xml); return XmlUtil.parse(resp); // 返回含 mweb_url 或 prepay_id }逻辑说明:TreeMap保证参数按字典序排列,这是签名算法的硬要求,换成HashMap顺序随机,签名必然对不上。total_fee单位是分,前端传元的话后端要乘 100 并取整,浮点数直接转 int 会丢精度,建议用BigDecimal处理。scene_info是 H5 支付特有参数,缺了会报“缺少 scene_info”。签名算法资源包里封装好了,别自己重写,MD5 和 HMAC-SHA256 两种模式按商户平台配置选。
3.3 返回结构与前端拉起方式
下单成功后,微信返回mweb_url,前端拿到后直接跳转即可拉起收银台。返回结构解析要注意:微信 v2 接口返回 XML,成功时return_code和result_code都是SUCCESS,但两者含义不同——return_code表示通信层成功,result_code表示业务层成功,只看一个会误判。
Map<String, String> result = wechatPayService.createH5Order(orderNo, amountFen, "测试商品"); if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) { String mwebUrl = result.get("mweb_url"); // 追加 redirect_url 参数,支付完成后跳回商户页面 String redirect = URLEncoder.encode("https://your-domain.com/pay/result", "UTF-8"); return mwebUrl + "&redirect_url=" + redirect; } else { log.warn("下单失败: {}", result.get("err_code_des")); throw new BizException("下单失败"); }redirect_url需要 URL 编码,且必须是已备案域名,否则支付完成后跳转会被拦截。前端在微信内直接location.href = mwebUrl即可,不需要额外 SDK。这里有个容易翻车的点:mweb_url有效期很短,生成后要立即使用,缓存几分钟再跳转大概率失效。
4. 回调验签与订单状态机:把“支付成功”落对
下单只是开始,真正决定系统正确性的是回调处理。微信异步通知会反复推送直到商户返回成功,处理不当就会出现重复扣库存、订单状态错乱。这一章讲验签、幂等和状态机。
4.1 异步通知验签流程
回调入口收到的是 XML 报文,处理顺序必须是:解析 → 验签 → 判断业务状态 → 处理 → 返回成功。验签用同样的密钥和算法,把报文里的sign字段剔除后重新计算比对。
@PostMapping("/pay/notify") public String handleNotify(@RequestBody String xmlBody) { Map<String, String> notify = XmlUtil.parse(xmlBody); // 1. 验签,失败直接返回失败,不处理业务 if (!Signer.verify(notify, config.getApiKey())) { log.warn("回调验签失败, out_trade_no={}", notify.get("out_trade_no")); return "<xml><return_code>FAIL</return_code></xml>"; } // 2. 判断业务结果 if (!"SUCCESS".equals(notify.get("result_code"))) { return "<xml><return_code>SUCCESS</return_code></xml>"; // 非成功通知也需应答 } // 3. 幂等处理,交给业务层 orderService.handlePaidNotify(notify.get("out_trade_no"), notify.get("transaction_id")); return "<xml><return_code>SUCCESS</return_code></xml>"; }验签失败绝不能继续处理业务,否则伪造回调就能白嫖订单。返回体必须是 XML 且return_code为SUCCESS,否则微信会持续重推,最多推多次后停止,期间日志会被刷屏。注意非成功通知(如支付失败)也要应答成功,避免无意义重推。
4.2 幂等设计与订单状态机
幂等的核心是“同一笔支付只处理一次”。常见做法是用out_trade_no做唯一约束,处理前先查订单状态,已是“已支付”就直接返回。资源包里的状态机大致是:待支付 → 支付中 → 已支付 / 已关闭,回调只允许从“待支付/支付中”迁移到“已支付”。
@Transactional public void handlePaidNotify(String orderNo, String transactionId) { Order order = orderMapper.selectForUpdate(orderNo); // 行锁,防并发 if (order == null) { log.error("订单不存在: {}", orderNo); return; } if (order.getStatus() == OrderStatus.PAID) { log.info("重复通知,已忽略: {}", orderNo); // 幂等出口 return; } if (order.getStatus() != OrderStatus.PENDING && order.getStatus() != OrderStatus.PAYING) { log.warn("状态不允许支付: {} -> {}", order.getStatus(), orderNo); return; } orderMapper.updateStatus(orderNo, OrderStatus.PAID, transactionId); inventoryService.deduct(order.getSkuId(), order.getQuantity()); // 扣库存 }selectForUpdate加行锁是为了防并发回调同时进来,两个线程都读到“待支付”然后各扣一次库存。状态判断放在锁内,顺序不能反。扣库存放在同一事务里,失败则整体回滚,订单状态不会脏。这里有个边界:如果扣库存是远程调用,事务管不到,需要补偿机制,资源包里留了注释提示,实际项目要按业务补。
5. 避坑清单:签名、金额与回调的五条血泪记录
支付这块的坑大多不是逻辑错,而是细节错。下面五条是我在类似项目里真实踩过或见别人踩过的,按“现象 → 原因 → 解决”整理。
5.1 签名始终不匹配
现象:本地算出的签名和微信返回的对比总是不一致,换算法也不行。原因通常是参数里混入了空值字段,或者 JSON 序列化重排了字段顺序,又或者sign字段本身被算进了签名。解决:签名前过滤掉空值和sign字段,用有序 Map 组装,签名串拼接时用&连接且末尾不加&key=之外的字符。资源包的Signer类已经处理了这些,别自己另写一套。
5.2 金额单位错位导致少收钱
现象:测试环境支付 1 元,实际扣了 0.01 元,或者反过来。原因:微信 v2 接口total_fee单位是分,前端传的是元,后端直接Integer.parseInt没乘 100,或者用double乘 100 出现99.999取整成 99。解决:统一用BigDecimal做元转分,amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).longValue(),入库和传参都用分。
5.3 回调收不到或重复处理
现象:用户已付款,订单还是待支付;或者库存被扣了两次。原因:notify_url不可达、返回体格式不对导致微信重推、业务处理没做幂等。解决:先用日志确认回调是否到达,没到达查公网可达性和 https 证书;到达了但重复处理,检查状态判断是否在锁内、是否有唯一约束兜底。返回体务必是合法 XML 且return_code为SUCCESS。
5.4 H5 拉起失败提示“商家参数格式有误”
现象:mweb_url跳转后收银台报错。原因:scene_info缺失或格式不对,或者redirect_url未编码、域名未备案。解决:scene_info按规范填h5_info结构,含type、wap_url、wap_name;redirect_url做 URL 编码且用已备案域名。另外mweb_url有时效,别缓存。
5.5 证书与密钥混用
现象:退款或某些接口报“证书错误”。原因:把 API 密钥当成了证书私钥,或者证书路径在打包后失效。解决:API 密钥用于签名,证书用于退款等敏感接口,两者分开配置;证书用 classpath 或绝对路径加载,打包后验证一次路径可读。资源包里配置类对两者做了区分,照着填即可。
6. 进阶:用对账与日志把支付链路闭环
跑通下单和回调只是及格线,生产环境还得能对账、能追溯。我一般会在资源包基础上补两件事:一是每日对账,二是全链路日志。对账的逻辑是拉取微信侧账单和本地订单比对,差异单人工介入。资源包里没有现成对账模块,但WechatPayService的 HTTP 客户端可以直接复用,调下载账单接口拿到 CSV 后按out_trade_no关联本地订单,重点比对金额和状态。
日志方面,下单请求、回调原文、验签结果、状态迁移各打一条,用同一个traceId串起来。排查“用户说付了但订单没变”这类问题时,有 traceId 能几分钟定位到是回调没到、验签失败还是状态机拒绝。下面这段是我习惯加的日志埋点:
log.info("[pay] traceId={}, action=createOrder, orderNo={}, amount={}", traceId, orderNo, amountFen); log.info("[pay] traceId={}, action=notify, orderNo={}, verify={}, resultCode={}", traceId, notify.get("out_trade_no"), verified, notify.get("result_code"));参数说明:traceId在下单时生成并透传到前端,回调时从订单反查,保证两端能对上;verify记录验签布尔值,一眼看出是不是签名问题。对账脚本建议做成定时任务,每天凌晨跑一次,差异结果落表而不是只发告警,方便回溯。
有个习惯我保持了几年:任何支付相关改动,上线前必走一遍“下单 → 支付 → 回调 → 对账”全链路,且故意制造一次重复回调验证幂等。从那以后我每次接支付都强制走一遍这个流程,省下的排查时间远超这点测试成本。希望帮到你。
本文还有配套的精品资源,点击获取