ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序支付Java实现:从下单到回调的完整闭环

微信小程序支付Java实现:从下单到回调的完整闭环 简介这份 PDF 文档是微信小程序支付后台的 Java 实现实例面向需要在小程序项目中快速接入微信支付的后端开发者也适合刚接触支付对接、希望了解完整调用链路的初中级 Java 工程师。内容以 LeanCloud 云引擎为运行环境围绕支付全流程展开从小程序前端登录授权获取 OpenId 开始讲解唯一订单号生成、TreeMap 参数排序与统一下单接口签名、POST 请求发送到微信返回 XML 数据的解析、预支付会话标识 prepay_id 提取与二次签名再到前端 wx.requestPayment 调起支付完整覆盖了前后端衔接的关键环节。文档同时说明了 appid、mch_id、notify_url、trade_type 等敏感参数通过 System.getenv() 环境变量注入的实践以及 AVException、UnsupportedEncodingException、DocumentException 等异常处理和 notify_url 回调、支付结果查询验证等注意事项。资源包为单个 PDF 文件大小仅 71KB内容紧凑适合快速查阅。该资源已有 2000 余人学习对排查支付签名失败、XML 解析异常等典型问题具有直接参考价值。1. 微信小程序支付后台的Java实现卡点从来不在“下单”做过微信小程序支付后台Java实现的人应该都有同感下单接口十分钟能调通支付回调能磨你一个下午。我接过一个小程序商城的后端客户反馈“支付成功但订单不更新”日志里连回调记录都没有最后发现是回调地址的 HTTPS 证书链不完整微信服务器请求直接被握手阶段拦掉。这类问题看不到堆栈排查全靠对协议的理解。这篇文章要讲清楚的是微信小程序里的 JSAPI 支付后台用 Java 怎么做完整闭环统一下单、小程序端拉起支付、回调验签与解密、订单状态更新、查单退款与对账。适合自己接支付功能的独立开发者也适合团队里第一次写支付模块的 Java 工程师。我不讲概念级的东西直接按能落地的顺序来。2. 支付后台的技术链路JSAPI支付与APIv3接口的边界2.1 谁发起、谁签名、谁回调先把链路摆正微信小程序支付由三个角色协作小程序前端、你自己的 Java 后台、微信支付服务器。前端负责 wx.login 拿 code后台拿 code 换 openid再用 openid 调统一下单接口拿到 prepay_id把支付参数返回给前端前端调用 wx.requestPayment 拉起收银台。用户输密码完成支付后微信服务器异步 POST 一笔通知到你的回调接口后台验签、解密、更新订单状态。很多入门的同学会把“后台下单”和“前端拉起支付”搞混。下单是后台的事拉起支付是前端的事前端拿到的不是金额和商品而是一组签名参数。真正改订单状态的地方不是前端 wx.requestPayment 的 success 回调而是微信服务器打到你后台的 notify 接口。前端成功只代表用户完成了支付动作业务是否成立要以回调为准。所以后台 Java 的职责可以拆成五块登录换 openid、统一下单、接收支付回调、查单兜底、退款与对账。这五块里前三块是必须的查单和退款的实现在我后面章节里会给出代码。搞清楚这个边界再看微信支付接口就不会晕。2.2 为什么选APIv3而不是老版v2接口微信支付老接口 v2 用的是 XML 报文加 MD5/HMAC-SHA256 签名配置项多、证书逻辑绕而且很多 v2 接口已经不支持新商户入驻。新接口 APIv3 是 JSON 格式用 RSA 非对称签名配合平台证书做验签与回调解密官方提供了 Java SDK开发体验比 v2 好很多。新项目直接走 v3别给自己挖坑。APIv3 的签名机制要理解三个材料商户 API 证书含商户私钥和证书序列号、APIv3 密钥32 字节用于回调内容 AES-256-GCM 解密、微信支付平台证书用于验证微信返回的签名。请求微信接口时你要用商户私钥对“请求方法 路径 时间戳 随机串 请求体”做 SHA256 然后 RSA 签名放进 Authorization 头。响应和回调则反过来用平台证书验证微信的签名。推荐用官方 Java SDKwechatpay-java而不是自己拼签名。自己实现签名串拼接看起来不难但平台证书轮换会导致验签突然失败SDK 把这个逻辑封装好了。我在小项目里见过有人把签名逻辑手写到 400 行最后时间戳校验和证书序列号问题不断换成 SDK 后代码量少了一半。2.3 后台要准备的材料清单与配置项接入前在微信商户平台要准备好几样东西商户号 mchid、小程序 appid、商户 API 证书下载下来是 apiclient_cert.pem 和 apiclient_key.pem、APIv3 密钥自己设置的一串 32 位字符串、回调域名必须是已备案域名HTTPS 证书有效。这些材料缺一不可商户私钥文件要放在服务器安全目录环境变量或配置中心管理不要提交到 Git。配置项我在代码里习惯用一组前缀wechat.pay统一管理包括 appid、mchid、apiV3Key、商户私钥路径、商户证书序列号、回调地址。回调地址是后面最容易出问题的点我一般单独拿出来放配置因为测试环境和生产环境的回调地址不一样。3. Spring Boot 接统一下单从 openid 到 prepay_id 的完整代码3.1 配置类与 SDK 初始化第一步是引入依赖和配置。微信支付官方 Java SDK 依赖坐标系是com.wechat.pay:wechatpay-java具体版本去 Maven 中央仓库看最新的这里不写死版本。在application.yml里配置商户信息示例配置如下。wechat: pay: app-id: wx1234567890abcdef merchant-id: 1620000000 api-v3-key: 32位长度的字符串密钥 merchant-private-key-path: /data/certs/apiclient_key.pem merchant-serial-number: 商户API证书序列号 notify-url: https://api.example.com/api/pay/notify配置项里最关键的是api-v3-key它不参与请求签名只用于解密回调内容。如果这里填错下单能成功但回调会一直解密失败。merchant-private-key-path指向 apiclient_key.pem 文件注意证书文件权限要收紧Java 进程需要能读但不能让其他用户读。把配置读入一个 Properties 类然后初始化微信支付 SDK 的配置对象。官方 SDK 推荐用RSAAutoCertificateConfig它会自动下载并更新微信支付平台证书避免平台证书轮换时验签失败。Configuration public class WechatPayConfig { Bean public RSAAutoCertificateConfig wechatPayConfig( WechatPayProperties props) throws Exception { PrivateKey privateKey PemUtil.loadPrivateKey( new File(props.getMerchantPrivateKeyPath())); return new RSAAutoCertificateConfig.Builder() .merchantId(props.getMerchantId()) .privateKey(privateKey) .merchantSerialNumber(props.getMerchantSerialNumber()) .apiV3Key(props.getApiV3Key()) .build(); } }PemUtil.loadPrivateKey是官方 SDK 提供的文件加载工具也可以自己实现 PKCS8 格式私钥读取。RSAAutoCertificateConfig内部会做平台证书的自动更新这是新老手之间差距最大的地方老版本方式是手动上传平台证书轮换时只能半夜起来换证书。自动更新配置建议只在后台进程里初始化一次不要每次请求时 new。3.2 统一下单参数怎么组返回值怎么用统一下单的逻辑集中在 Service 层。创建订单前先确认三件事out_trade_no 必须是商户内部唯一的订单号金额单位是分且是整数openid 必须是当前登录用户在微信侧的标识。创建订单的完整方法如下。Service public class WechatPayService { private final RSAAutoCertificateConfig wechatPayConfig; private final WechatPayProperties props; public PrepayWithRequestPaymentResponse createJsapiOrder( String openid, String outTradeNo, long amountFen) { PrepayRequest request new PrepayRequest(); request.setAppid(props.getAppId()); request.setMchid(props.getMerchantId()); request.setDescription(小程序商品支付); request.setOutTradeNo(outTradeNo); request.setNotifyUrl(props.getNotifyUrl()); Amount amount new Amount(); amount.setTotal(amountFen); // 单位是分 amount.setCurrency(CNY); request.setAmount(amount); Payer payer new Payer(); payer.setOpenid(openid); request.setPayer(payer); JsapiService service new JsapiService.Builder() .config(wechatPayConfig) .build(); return service.createOrderWithRequestPayment(request); } }createOrderWithRequestPayment是 JSAPI 下单的快捷方法返回的响应里已经包含小程序端拉起收银台要用的全部参数prepay_id、timeStamp、nonceStr、package、signType、paySign。其中package这个字段在 Java 里是关键字在 JSON 序列化时会映射为字符串键前端直接用就行。下单接口对外提供时最好把返回参数封装成一个 Map 给前端字段名保持微信支付要求的原样。我一般会额外返回一个 outTradeNo 给前端做幂等关联这样前端重复点击时可以对比订单号。注意下单时金额校验要在业务层做比如订单已超时、商品已下架就不允许发起支付避免用户对着无效订单付款。3.3 前端 wx.requestPayment 怎么配合收尾前端拿到后台返回的支付参数后调用wx.requestPayment。这步出错率不高但要注意参数类型支付宝的经验在微信小程序里不通用金额和订单号不是前端传给微信的前端只是把后台的参数原样透传。wx.requestPayment({ timeStamp: res.timeStamp, nonceStr: res.nonceStr, package: res.package, signType: res.signType, paySign: res.paySign, success: () { // 这里不要直接把订单标记为已支付 }, fail: (err) { console.error(支付失败, err); } });很多新人在success回调里直接调后端把订单改成已支付这是典型的翻车姿势。此时微信只是确认前端发起了支付并不代表支付结果一定成功而且 iOS 和安卓的收银台行为有差异。正确做法是前端回到订单页后轮询后台查单接口由后台根据微信侧结果返回真实支付状态。获取 openid 这一步要在下单前完成。小程序的 wx.login 拿到 code后台用 code 调微信的jscode2session接口返回 openid 和 session_key。获取手机号和获取 openid 是两个不同接口热词里混在一起问的同学不少登录获取 openid 走的是 code 换 session获取手机号必须用户在页面点击授权两者不要相互依赖。openid 要绑定到当前登录态后续下单时直接取不要让前端把它当参数传上来。4. 支付回调验签、解密与订单状态机实现4.1 回调报文结构与签名校验支付成功后微信服务器会向 notify_url 发起 POST 请求Content-Type 是 application/json。回调报文结构里外层是通知 ID、创建时间、事件类型、摘要真正重要的是resource字段它里面的 ciphertext 是加密后的订单数据。微信先用平台私钥签名外层报文再用 APIv3 密钥加密 resource 内的数据后台要做两步验签和解密。官方 SDK 的NotificationParser把两步封装好了。拿到原始请求体构造 Notificationparser.parse 内部完成验签和解密直接返回 Transaction 对象。如果验签通过但解密失败会抛异常需要在 catch 里分清原因。RestController RequestMapping(/api/pay) public class PayNotifyController { PostMapping(/notify) public MapString, Object payNotify(RequestBody String body) { try { NotificationParser parser new NotificationParser( (RSAAutoCertificateConfig) wechatPayConfig); Notification notification parser.parse(body); Transaction transaction (Transaction) notification.getData(); String outTradeNo transaction.getOutTradeNo(); String tradeState transaction.getTradeState().name(); long payerTotal transaction.getAmount().getPayerTotal(); orderService.handlePaidNotification(outTradeNo, tradeState, payerTotal); return successResponse(); } catch (Exception e) { log.error(支付回调处理失败, e); return failResponse(); } } }回调接口返回给微信服务器的内容有固定格式成功时返回 HTTP 200JSON 是{code:SUCCESS,message:成功}。不要返回业务自己的 JSON 结构。如果后台处理过程中抛异常或返回非 200微信会按既定策略重试重试间隔是 15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟、30 分钟、30 分钟、30 分钟、60 分钟共 10 次。很多人会在这里犯一个错回调里直接调微信查单接口去确认订单状态。没必要回调报文的 transaction 里已经有 trade_state而且经过了验签解密。再查一次接口除了增加耗时还引入了新的网络错误点。回调处理要快把耗时操作放到 MQ 或者线程池里异步处理。4.2 用事务保证订单状态更新不丢不重回调处理的核心是更新订单这一步必须幂等。微信的重试机制决定了同一个回调可能被送多次如果不加判断订单状态会被反复重写。实现上我用一条带条件更新的 SQL 保持原子性更新时要求订单当前状态必须是待支付。UPDATE t_order SET pay_status 1, transaction_id #{transactionId}, paid_at #{paidAt}, updated_at now() WHERE out_trade_no #{outTradeNo} AND pay_status 0如果影响行数为 1说明本次更新有效订单从未支付变成了已支付。如果影响行数为 0说明订单已经处理过直接返回成功不再重复处理。这个设计比先 select 再 update 更安全并发场景下也不会出现两个线程同时把订单状态改乱的问题。回调接口要放在事务边界内但事务里尽量不要查外部接口微信支付查单、发短信、扣库存建议放到事务提交后的领域事件里。回调里业务处理完立即返回 SUCCESS微信收到成功后就不会再重试。如果库存扣减失败导致事务回滚回调返回失败微信会重试这比返回成功但业务没落库要安全得多。4.3 查单兜底与订单状态机回调不是唯一获取支付结果的途径。用户付完钱后可能手机断网回调虽然发出去了但客户端没收到微信重试还在进行用户着急催发货。这时候后台要提供主动查单接口前端在支付完成后轮询它。查单接口对应微信支付 APIv3 的查询订单接口用 out_trade_no 查。public Transaction queryOrder(String outTradeNo) { QueryOrderByOutTradeNoRequest request new QueryOrderByOutTradeNoRequest(); request.setMchid(props.getMerchantId()); request.setOutTradeNo(outTradeNo); JsapiService service new JsapiService.Builder() .config(wechatPayConfig) .build(); return service.queryOrderByOutTradeNo(request); }查单返回的 trade_state 有几种SUCCESS、REFUND、NOTPAY、CLOSED、REVOKED、USERPAYING、PAYERROR。订单状态机要覆盖这些状态我习惯定义 6 个状态待支付、支付中、已支付、已退款、已关闭、支付异常。待支付可以走到已支付、已关闭已支付可以走到已退款支付中状态主要应对用户正在输密码、还没来得及回调的窗口期。前端轮询查单时要设置合理的间隔一般 3 秒一次最多 10 次。支付成功的订单要立刻停止轮询并跳转结果页。用户点击取消支付后要调用关闭订单接口把未支付的订单关闭防止超时后还能被拉起支付。5. 微信小程序支付 Java 实现5 个高频坑与排查方法5.1 回调一直收不到问题多半不在代码现象下单成功前端也能拉起支付用户付完钱后台日志里却没有任何回调请求。原因回调是微信服务器主动发起 HTTP 请求你的服务器需要能被公网访问。最常见的是回调域名没有备案或者 HTTPS 证书链不完整。有些开发机在本地用 ngrok 之类的隧道工具做穿透微信支付会校验域名临时域名经常被拒。我遇到过一次nginx 的 SSL 证书只配置了叶子证书没有带中间证书浏览器访问正常但微信服务器的 TLS 客户端库校验更严格握手直接失败。解决确认回调地址满足三个条件域名已备案、HTTPS 证书有效且证书链完整、回调路径不被防火墙拦截。然后看微信商户平台的“开发配置”里回调地址是否和下单参数一致。检查证书链用openssl s_client -connect api.example.com:443 -servername api.example.com看返回的证书链是否包含中间证书。5.2 验签失败平台证书与公钥模式混用现象回调能收到但每次都在parse阶段抛验签异常日志里提示签名不匹配或证书序列号找不到。原因微信支付 APIv3 支持两种验签方式平台证书模式和非证书的公钥模式。SDK 初始化用RSAAutoCertificateConfig会自动拉取平台证书但如果你之前手动下载过平台证书并在代码里写死了某个序列号平台证书更新后就会验签失败。另一种是回调方配置的商户证书序列号填成了商户 API 证书的序列号微信签名时用的是平台证书两者对不上。解决优先用RSAAutoCertificateConfig并删除手写的证书序列号配置。如果坚持公钥模式需要从微信支付平台证书下载接口拿到最新公钥并定时刷新。排查时先在日志里打出notification原始报文和验签异常堆栈对比微信商户平台里显示的证书序列号与代码里用到的序列号是否一致。5.3 金额“分”转“元”的精度陷阱现象测试支付 0.10 元数据库里记录的是 9 分钱或者用户支付 19.9 元后台校验不通过。原因微信支付 API 里金额单位是分整数类型没有小数。很多同学在前端把元转分时直接price * 100浮点乘法会丢精度19.9 乘 100 在二进制浮点里是 1989.9999强转成 int 就变成了 1989 分。数据库如果设计成 decimal存进去也会出现精度不一致。解决金额从元转分用BigDecimal.valueOf(price).movePointRight(2).intValue()从分转元用BigDecimal.valueOf(fen).movePointLeft(2).setScale(2)。不要在业务链路里用 double 传金额前后端、MQ、数据库统一用分或统一用 string 的 decimal。我在订单表里直接存分为 int 类型展示层再转简单不容易错。5.4 重复回调与回调覆盖正在处理的订单现象订单状态被覆盖比如先收到支付成功回调又收到一笔退款回调把已支付状态改回了未支付。原因回调处理没做幂等且状态机没有约束条件。微信重试机制下同一个支付结果会多次投递退款事件也会推送到同一个回调地址如果回调里只按 out_trade_no 更新字段不校验当前状态状态就会被来回改写。解决更新 SQL 里必须带当前状态条件已支付状态不允许被支付成功事件回退到待支付。同时回调处理要记录回调消息 ID用消息 ID 做去重表重复投递直接跳过。我习惯在回调处理逻辑最前面查一次订单当前状态已经终态的订单直接返回 SUCCESS不进入后续逻辑。5.5 openid 被换掉支付人不是下单人现象下单时用的 openid 是 A 用户实际拉起支付时却是 B 用户的微信后台回调里拿到的 openid 对不上。原因后台生成下单参数后没有把它们和当前登录态绑定前端拿到支付参数后可能被其他页面或其他用户使用。有些同学图方便把 openid 作为参数传给前端再由前端传回下单接口中间被替换后微信侧校验不过或者校验规则写得太松散直接漏过去。解决下单时用out_trade_no关联当前登录用户微信回调回来时再拿transaction.getPayer().getOpenid()比对订单用户。如果对不上说明支付账号和下单账号不一致要记日志并标记订单异常。更严格的做法是下单前重新从 session 取 openid而不是信任前端传上来的任何用户标识。6. 用日志与抓包验证整条支付链路查单、退款与对账6.1 结构化打印回调参数5分钟定位问题支付类问题最怕的是信息黑洞。我要求所有回调入口先打印一行结构化日志包含 out_trade_no、transaction_id、trade_state、payer_total、回调收到时间。这样排查问题时不需要翻微信商户平台直接在日志里就能还原当时的现场。日志格式我用 JSON 风格方便日志平台检索。字段固定成pay_notify_received加订单号和状态出问题时按订单号 grep 一次就能看到完整链路下单日志、回调日志、查单日志。有些坑是玄学但大部分支付问题只要现场完整十分钟能定位。验证回调参数是否正确的另一个手段是用 Charles 抓包小程序请求。在开发者工具里配上 HTTPS 抓包证书能看到小程序端调 wx.requestPayment 时透传的 timeStamp、nonceStr、package、paySign把这些参数和后台下单日志对比能快速判断是前端透传错参数还是后台签名生成错误。抓包只能看到端上请求看不到微信服务器到后台的回调但这已经能覆盖大部分联调问题。6.2 小额退款自测与账单对账支付联调通过后退款是另一个必须测的功能。微信支付退款接口同样走 APIv3后台业务里需要记录原订单金额、退款金额、退款原因并且退款也要有自己唯一的退款单号。退款接口通常配置在管理后台人工操作不对外暴露。public Refund createRefund(String outTradeNo, long refundAmountFen, String refundReason) { RefundRequest request new RefundRequest(); request.setOutTradeNo(outTradeNo); request.setOutRefundNo(R outTradeNo System.currentTimeMillis()); request.setReason(refundReason); AmountReq amount new AmountReq(); amount.setRefund(refundAmountFen); amount.setTotal(orderService.getOrderAmountFen(outTradeNo)); amount.setCurrency(CNY); request.setAmount(amount); RefundService service new RefundService.Builder() .config(wechatPayConfig) .build(); return service.create(request); }退款成功后微信同样会推送退款结果回调处理方式和支付回调一致但要更新订单状态到已退款并记录退款单号。这里要特别注意退款不一定立刻成功微信返回的处理中状态要落到订单表等回调确认后再改终态。对账是支付系统上线前不要省的一步。微信支付有专门的账单下载 API可以下载日账单和商户转账账单格式是 CSV。我一般每天凌晨跑一个定时任务下载前一天的账单和本地订单表做对账核对金额、订单号、交易状态。对账不平的订单单独列表人工介入这是支付后台最后的防线。我在这个系统上翻过最狠的一次车是上线第一周对账发现少了一笔订单。查了半天原因是回调更新订单时只判断了影响行数没校验支付金额一笔支付 0.01 元的测试单混进了正式数据把订单状态改成了已支付。后来所有回调处理都加了金额一致性校验订单金额和回调金额不一致一律标记异常。支付系统没有银弹每一层都做校验把异常晾在阳光下比依赖微信的重试更可靠。希望帮到你。本文还有配套的精品资源点击获取
返回列表