ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java服务端支付对接实战:微信支付+支付宝下单回调退款全解析

Java服务端支付对接实战:微信支付+支付宝下单回调退款全解析 简介Java 服务器端接入微信、支付宝支付与退款功能的实现方法被整理成一份 PDF 资料面向电商及线上服务后端开发者重点解决支付流程集成中的参数签名、统一下单、返回封装等核心问题。资源包仅含 1 个 PDF 文件压缩包大小约 68KB轻量精炼但覆盖完整。内容通过示例代码梳理了微信支付从统一下单、签名生成、发送请求到接收 prepay_id 的完整链路同时对比支付宝支付接口的差异并说明退款接口的调用与异常处理方式此外还介绍了将支付和退款操作封装为 PayService 模块、兼顾异步处理与事务管理、日志与安全监控的设计思路。已有 396 人学习过这份资料对于需要快速掌握 Java 支付服务端要点并落地项目的开发者来说是一份可直接参考的实践性文档。1. 项目总览服务端支付能力的搭建思路前阵子公司接了个电商项目需求很明确用户在小程序里下单能用微信支付付款电脑端网页能用支付宝扫码付款订单异常时运营后台能一键退款。说白了就是一套标准的 Java 服务端支付模块同时覆盖微信支付和支付宝支付两条链路。这个需求几乎每个做电商、知识付费、SaaS 系统的团队都会遇到跟着做一遍能把支付对接的整个套路摸清楚。我在设计阶段把系统拆成了三个核心链路下单、回调、退款。下单负责拉起收银台并生成支付参数回调负责被动接收支付结果并更新订单状态退款则是运营侧的主动操作把用户的钱原路退回去。选 Java 服务端来做这件事最大的好处是生态成熟微信支付和支付宝都有官方 Java SDK社区资料也厚出了问题搜一圈就能找到解法。这篇文章就把我这一轮完整落地的经验梳理出来包含核心代码实现、参数说明、常见的坑和排查思路给准备接支付的兄弟们做个参考。先说下整体技术选型。项目是 Spring Boot 2.7 MyBatis-Plus Redis MySQL微信支付用的是 V3 接口小程序支付也就是 JSAPI 支付支付宝用的是电脑网站支付alipay.trade.page.pay加手机网站支付alipay.trade.wap.pay。为什么没有选第三方聚合支付虽然聚合支付的接入成本低但手续费高、结算周期长最关键的是资金流向不够透明遇到客诉的时候处理起来很憋屈。既然公司本身有支付宝和微信的商户号就老老实实直连官方。另外一个原因支付这种强资金链路每一步都应该掌握在自己手里出了问题可以快速定位第三方帮忙兜底反而容易扯皮。1.1 核心需求解析这个项目表面上是“接入两个支付渠道”但抽开看其实有四个核心点一是支付参数的生成与签名保证请求合法二是异步通知的安全校验防止伪造回调三是订单状态的准确流转防止超卖、重复发货四是退款资金的正确性保证原路退回且金额准确。这四个点里回调处理是很多新手最容易翻车的地方后面单独拿出来细讲。1.2 为什么必须由服务端完成支付对接可能有刚入行的朋友会问小程序端不是可以直接调 wx.requestPayment 吗为什么还要服务端介入因为支付涉及商户私钥、证书、订单金额计算、库存扣减这些敏感操作放在客户端就是裸奔。支付宝的签名私钥、微信的商户 API 证书如果发到小程序或者网页里等于把保险柜钥匙交给路人。所以支付参数必须由服务端生成客户端只负责调起支付控件。这也是支付安全的基本红线。2. 支付对接前的准备参数、证书与沙箱环境正式写代码之前最磨人的其实是各种账号、密钥、证书的申请和配置。我第一次接微信支付是把文档翻了三遍才理清楚这里帮大家把关键项列出来照着准备就行。2.1 微信支付 V3 需要准备什么微信支付商户平台pay.weixin.qq.com申请商户号后主要拿这几个东西商户号 mchid。AppID小程序或公众号的 AppID需要在商户平台关联绑定。商户 API 证书pem 格式的商户私钥 apiclient_key.pem、商户证书 apiclient_cert.pem。APIv3 密钥在商户平台手动设置的 32 字节对称密钥用于回调数据解密。平台证书/公钥用于验签新版的 SDK 可以开启自动更新平台证书。这里有个常见的误解很多新人以为 APIv3 密钥就是商户 API 证书的私钥密码其实不是。APIv3 密钥是你自己在商户平台设置的一串字符用来解密微信支付回调里的敏感信息比如解密 phone、 decrypt 回调资源。而商户私钥是用来生成请求签名的两者用途完全不同容易搞混。提示微信支付 V3 目前推荐使用 微信支付公钥 取代原来的平台证书。如果代码里配置的是平台证书模式新申请商户号可能遇到“无可用的平台证书请在商户平台-API安全申请使用微信支付公钥”的报错。解决办法是去商户平台“API 安全”里申请微信支付公钥然后在代码里把证书加载逻辑换成公钥模式。2.2 支付宝支付需要准备什么支付宝开放平台创建应用后在“应用详情”里能看到APPID。应用私钥自己用支付宝提供的密钥生成工具生成应用私钥保存在服务端。支付宝公钥在开放平台上传应用公钥后平台给返回的公钥用来验签。接口加签方式选 RSA2。支付宝的沙箱环境对开发调试非常友好。不需要真实商户号就能模拟支付网关地址是 openapi.alipaydev.com需要去“沙箱环境”页面获取沙箱版支付宝 APP用于手机端模拟支付。我强烈建议在沙箱里把流程跑通再切正式环境省下来的全是联调时间。2.3 Maven 依赖与配置文件项目里用到的核心依赖就这几个!-- 微信支付V3 SDK -- dependency groupIdcom.github.wechatpay-apiv3/groupId artifactIdwechatpay-java/artifactId version0.2.11/version /dependency !-- 支付宝SDK -- dependency groupIdcom.alipay.sdk/groupId artifactIdalipay-sdk-java/artifactId version4.38.59.ALL/version /dependency配置文件里不要写死密钥要用环境变量或配置中心管理。我的做法是放在 application-prod.yml但实际值从环境变量里读取避免把私钥提交到 Git 仓库。3. 核心接口实现下单、回调与退款这一节是全文的硬菜。我按照真实的调用时序来写服务端生成支付参数 → 用户支付 → 微信/支付宝异步通知服务端 → 服务端更新订单 → 运营发起退款。每一步都贴了关键代码和注释。3.1 微信支付统一下单JSAPI 支付小程序场景走 JSAPI 支付服务端拿到用户的 openid 后调用“JSAPI 下单”接口拿到 prepay_id再签名生成小程序端 wx.requestPayment 需要的参数。核心代码如下public MapString, String wxJsapiPay(WxPayOrderDTO dto) throws Exception { // 1. 构建 HttpClient使用商户私钥进行请求签名 PrivateKey merchantPrivateKey PemUtil.loadPrivateKey( new FileInputStream(apiclient_key.pem)); // 证书序列号从商户证书中读取 String serialNo CertUtil.getSerialNo(apiclient_cert.pem); // 微信支付公钥/平台证书用于验签可用公钥模式或证书模式 PublicKey wechatPayPublicKey PemUtil.loadPublicKey( new FileInputStream(wechatpay_public_key.pem)); RSAAutoCertificateConfig config new RSAAutoCertificateConfig.Builder() .merchantId(mchid) .privateKey(merchantPrivateKey) .merchantSerialNumber(serialNo) .privateKeyPath(apiclient_key.pem) .build(); // 2. 调起统一下单 API HttpService httpService new ApacheHttpClientBuilder() .config(config) .build(); String url https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi; MapString, Object body new HashMap(); body.put(appid, appId); body.put(mchid, mchid); body.put(description, dto.getSubject()); body.put(out_trade_no, dto.getOrderNo()); body.put(notify_url, wxNotifyUrl); body.put(amount, Map.of(total, dto.getAmount(), currency, CNY)); body.put(payer, Map.of(openid, dto.getOpenId())); // 3. 发送 POST 请求拿到 prepay_id HttpResponse response httpService.post(url, body); JSONObject json JSON.parseObject(response.getBody()); String prepayId json.getString(prepay_id); // 4. 二次签名生成小程序端调起支付所需的参数 String timestamp String.valueOf(System.currentTimeMillis() / 1000); String nonceStr RandomUtil.randomString(16); String message appId \n timestamp \n nonceStr \n prepayId \n; String sign SignatureUtil.sign(message, merchantPrivateKey); MapString, String result new HashMap(); result.put(appId, appId); result.put(timeStamp, timestamp); result.put(nonceStr, nonceStr); result.put(package, prepay_id prepayId); result.put(signType, RSA); result.put(paySign, sign); return result; }这里特别注意金额单位是“分”不是“元”。用户支付 99.99 元传给微信的就是 9999。这也是大量 bug 的来源我见过不止一个项目因为单位换算问题导致订单金额对不上最后退款对账一团糟。3.2 支付宝电脑网站支付与手机网站支付支付宝的接入比微信要省心因为 SDK 封得很好核心是组装请求对象、初始化 AlipayClient、调 execute。public String alipayPagePay(AlipayPayDTO dto) { AlipayClient alipayClient new DefaultAlipayClient( https://openapi.alipay.com/gateway.do, appId, privateKey, json, UTF-8, alipayPublicKey, RSA2); AlipayTradePagePayRequest request new AlipayTradePagePayRequest(); request.setNotifyUrl(alipayNotifyUrl); request.setReturnUrl(alipayReturnUrl); // 同步跳转地址仅做展示 JSONObject bizContent new JSONObject(); bizContent.put(out_trade_no, dto.getOrderNo()); bizContent.put(total_amount, dto.getAmount()); // 支付宝的单位是“元” bizContent.put(subject, dto.getSubject()); bizContent.put(product_code, FAST_INSTANT_TRADE_PAY); request.setBizContent(bizContent.toJSONString()); try { AlipayTradePagePayResponse response alipayClient.pageExecute(request); return response.getBody(); // 返回一段自动提交表单的 HTML } catch (AlipayApiException e) { throw new RuntimeException(支付宝下单失败, e); } }有一个细节容易被忽略微信金额用的是“分”支付宝金额用的是“元”字符串类型。如果你的金额实体类统一存的是分在组装支付宝请求前一定要除以 100 并格式化为两位小数比如 10.00不然支付宝会直接报“订单金额格式错误”。有朋友遇到过“支付宝电脑网站支付如何只返回一个二维码链接”的需求其实很简单不要直接拿 pageExecute 返回的 HTML 返给前端而是让后端生成订单后用 pageExecute 拿到完整跳转 URLresponse.getBody() 里可以提取出 action 地址或者更优雅的方式是使用 AlipayTradePrecreateRequest当面付预下单接口直接返回 qrCode 字段。3.3 支付回调处理验签、解密与幂等回调是整个支付环节最核心、也最容易出问题的一步。微信和支付宝的异步通知有一个共同特点可能重复推送而且顺序不定。所以回调必须做两件事验签确认来源合法幂等防止重复处理。我先说微信支付 V3 的回调相比 V2 它安全了不少通知报文里只有一个 encrypted 密文需要用 APIv3 密钥做 AES-256-GCM 解密才能拿到订单数据。同时需要用微信支付公钥验签。代码拆成两步PostMapping(/notify/wx) public String wxNotify(HttpServletRequest request, RequestBody String body) throws Exception { // 1. 获取请求头Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature String timestamp request.getHeader(Wechatpay-Timestamp); String nonce request.getHeader(Wechatpay-Nonce); String signature request.getHeader(Wechatpay-Signature); String serial request.getHeader(Wechatpay-Serial); // 2. 验签逻辑省略证书加载可复用上面的 config boolean ok verifyWxSign(timestamp, nonce, body, signature, serial); if (!ok) { return FAIL; // 微信要求返回 FAIL超过一定次数会停止推送 } // 3. 解密 resource 里的密文 JSONObject json JSON.parseObject(body); JSONObject resource json.getJSONObject(resource); String ciphertext resource.getString(ciphertext); String associatedData resource.getString(associated_data); String nonceForDecrypt resource.getString(nonce); String plaintext AesUtil.decryptToString( apiV3Key.getBytes(StandardCharsets.UTF_8), associatedData.getBytes(StandardCharsets.UTF_8), nonceForDecrypt.getBytes(StandardCharsets.UTF_8), ciphertext); // 4. 解析解密后的 JSON拿到 out_trade_no、trade_state、amount JSONObject data JSON.parseObject(plaintext); String outTradeNo data.getString(out_trade_no); String tradeState data.getString(trade_state); Integer total data.getInteger(total); // 5. 幂等处理如果订单已经是“已支付”状态直接返回 SUCCESS Order order orderMapper.selectByOrderNo(outTradeNo); if (order null) { return FAIL; // 订单不存在返回 FAIL 让微信重试 } if (OrderStatus.PAID.equals(order.getStatus())) { return SUCCESS; // 已处理过防止重复 } if (!SUCCESS.equals(tradeState)) { return FAIL; } if (total ! order.getAmount()) { log.error(微信回调金额不一致, orderNo{}, total{}, dbAmount{}, outTradeNo, total, order.getAmount()); return FAIL; } // 6. 更新订单状态加锁避免并发重复处理 // 推荐用 Redis 分布式锁 唯一索引双重保障 boolean updated orderMapper.paySuccessByOrderNoAndStatus( outTradeNo, OrderStatus.WAIT_PAY, OrderStatus.PAID); if (!updated) { return SUCCESS; // 说明已经被其他线程处理了也算成功 } return SUCCESS; }支付宝的回调验签用的是支付宝公钥SDK 提供了便捷方法public String alipayNotify(HttpServletRequest request) { MapString, String params new HashMap(); request.getParameterMap().forEach((k, v) - params.put(k, v[0])); try { // 验签核心代码就这一行 boolean signVerified AlipaySignature.rsaCheckV1( params, alipayPublicKey, UTF-8, RSA2); if (!signVerified) { return failure; } String tradeStatus params.get(trade_status); String outTradeNo params.get(out_trade_no); String totalAmount params.get(total_amount); if (TRADE_SUCCESS.equals(tradeStatus)) { // 同样的幂等处理逻辑先查订单状态再用乐观锁更新 return processPaidOrder(outTradeNo, totalAmount); } return failure; } catch (Exception e) { log.error(支付宝回调验签失败, e); return failure; } }这里要特别说一下异步通知的“成功”返回语义。微信要求回调接口最终返回“SUCCESS”支付宝要求返回“success”全小写。如果返回别的字符串或者异常支付平台会认为通知失败按照一定的频率重复发送通知。微信和支付宝的重试策略略有差异但设计上都是指数退避所以回调处理逻辑必须天然支持重复调用绝不能因为重复回调产生两条支付流水。3.4 退款功能实现退款跟支付一样也有两条链路接口调用和结果确认。微信退款接口是 POST /v3/refund/domestic/refunds支付宝对应的是 alipay.trade.refund。微信退款的关键参数是 out_trade_no原支付订单号和 out_refund_no本次退款单号金额同样是“分”public void wxRefund(RefundDTO dto) { String url https://api.mch.weixin.qq.com/v3/refund/domestic/refunds; MapString, Object body new HashMap(); body.put(out_trade_no, dto.getOrderNo()); body.put(out_refund_no, dto.getRefundNo()); body.put(reason, dto.getReason()); body.put(notify_url, wxRefundNotifyUrl); body.put(amount, Map.of( refund, dto.getRefundAmount(), // 单位分 total, dto.getTradeAmount(), // 原订单金额单位分 currency, CNY )); // POST 发送请求同步返回退款是否受理成功 }支付宝退款就简单很多public String alipayRefund(RefundDTO dto) { AlipayTradeRefundRequest request new AlipayTradeRefundRequest(); JSONObject bizContent new JSONObject(); bizContent.put(out_trade_no, dto.getOrderNo()); bizContent.put(refund_amount, dto.getRefundAmount()); // 单位元 bizContent.put(out_request_no, dto.getRefundNo()); request.setBizContent(bizContent.toJSONString()); AlipayTradeRefundResponse response alipayClient.execute(request); return response.getBody(); }关于退款的一点切身感受退款不是一提交就立即成功的。微信和支付宝都是“受理制”接口返回 success 只代表退款申请被接受实际打款是异步清算的。所以退款模块必须做两件事一是记录退款流水表并维护状态申请中/成功/失败二是靠回调或主动查询来确认最终结果。微信退款有单独的 refund notify_url支付宝退款可以直接按原支付回调的 notify_url 来收。提示退款一旦成功微信和支付宝都不支持“撤销退款”。所以在发起退款前系统层面一定要做退款金额上限校验累计退款金额不能超过支付金额否则运营手滑多退一次这笔差价只能公司自己担。4. 常见问题与排查技巧实录支付接入过程中踩坑是难免的我把这一轮实际遇到过的问题整理成一个速查表基本都是文档里翻不着的细节。4.1 微信支付高频问题报错“无可用的平台证书请在商户平台-API安全申请使用微信支付公钥”。这是新商户号最常遇到的问题。原因很直接新商户号默认使用“微信支付公钥”模式而非“平台证书”模式但代码还按老 SDK 的规范去加载平台证书。解决方案就是去商户平台申请微信支付公钥然后替换配置。说白了微信在推动公钥模式替代证书模式代码要同步升级。回调验签失败。微信回调验签失败大概率是证书加载错了。检查三点用没用对微信支付公钥不是商户 API 证书公钥有没有填成 APIv3 密钥验签时用的 serial number 跟请求头里的 Wechatpay-Serial 是否一致。我遇到过把商户证书序列号拿去验微信平台签名的能不失败吗。API 请求返回 401 签名错误。这是最常见的网络请求报错排查思路从这四个维度走私钥是否匹配、证书序列号是否对应、请求体中的字符串与签名原文是否一致、时间戳是否偏差过大。你可以把官方提供的签名工具和本地生成的签名结果做对比一秒能定位问题。4.2 支付宝高频问题沙箱环境能付正式环境一直报“公钥不对”。这是环境串了的典型症状。开发环境用沙箱的支付宝公钥切正式环境忘了换。支付宝沙箱地址是 openapi.alipaydev.com正式环境是 openapi.alipay.com两个环境的密钥完全独立。建议把网关地址、公钥、AppID 做成一套环境配置切换环境时一次换全。同步通知和异步通知搞混。支付宝的回调有两种return_url 是用户支付成功后浏览器跳转只做展示notify_url 才是服务端真正要处理的异步通知。很多萌新在 return_url 里更新订单状态会导致支付成功但服务端不知道订单还是未支付。金额校验不通过。支付宝回调参数 total_amount 是字符串的“99.99”拿 BigDecimal 转没问题但如果你直接 Double.parseDouble再跟数据库里的分做比较很容易踩浮点精度坑。我的做法是统一转成 BigDecimalcompareTo 方法比较绝不直接用 equals。4.3 服务端设计方案层面的坑回调接口一定要设置超时短的熔断策略。支付平台在回调时如果发现你的接口迟迟不响应它会持续重试。如果回调期间正好赶上数据库故障也不要让线程死等支付平台直接快速返回 FAIL等支付平台自己重试。这个设计对运维非常友好。幂等不能只靠数据库状态判断。最稳妥的幂等方案是“数据库唯一索引 Redis 分布式锁 乐观锁状态更新”三层叠加。具体操作支付回调处理前先往支付流水表插入一条记录用 out_trade_no 做唯一键插入失败说明已经处理过直接返回 SUCCESS。这个做法比先查后更新更安全能挡掉并发重试的极端情况。4.4 我踩过的一个典型坑最后分享一个真实的翻车经历。有一回上线后用户反馈说“支付成功了但订单没发货”一查日志发现微信回调进来了两三次但第一次处理时 Redis 锁超时释放了第二次线程进来发现订单还是待支付于是又执行了一遍状态更新。问题出在我只用了 Redis 锁没有给数据库层加唯一约束两个线程刚好在锁失效的间隙同时更新了订单表第二次更新覆盖了第一次的处理结果。从那以后我的回调处理逻辑就改成了“先插流水表唯一索引兜底再改订单状态乐观锁”这两步都成功才算处理完成。后来微信又重复回调了几次流水表直接挡住了再也没出现过重复发货的问题。这个教训值一万块写出来给大家避坑。本文还有配套的精品资源点击获取
返回列表