Java服务端整合微信支付与支付宝支付:从下单到退款全链路实战
发布时间:2026/9/25 23:28:27来源:尧图网络
简介面向需要对接主流支付平台的后端开发者围绕Java服务器端微信、支付宝支付及退款集成展开。内容梳理统一下单、签名生成与验签、HTTP请求封装、前端调起字段返回、退款接口调用及回调处理等关键环节并给出WXPay与Alipay工具类中的核心代码片段涵盖参数构造、XML/JSON解析、异常处理、事务一致性、日志监控与安全设计思路覆盖微信统一下单、支付宝预支付、退款与回调通知等场景。强调HTTPS通信、敏感信息不落库等要点帮助读者构建稳定、安全且易扩展的支付服务。压缩包为1个PDF文件体积约68KB便于在移动端或电脑端查阅目前已吸引396人学习。整体内容紧凑实用适合已有Java基础、希望快速掌握微信支付宝支付退款实现细节的开发者参考。1. Java服务端接住微信支付和支付宝支付从下单到退款的一条完整链路上周我一同事在电商项目里接支付前后折腾了四天最后卡在退款回调上钱退了订单状态没变用户投诉到客服。他用的就是一套Java服务端微信支付、支付宝支付的实现核心逻辑没问题但边界条件没处理干净。这个资源说白了就是一套能落地的服务端代码微信支付、支付宝支付的下单、异步通知验签、原路退款、退款回调、订单状态联动整条支付闭环都在里面。适合两类人一是Java后端刚接到支付需求想把两个渠道一次打通的新手二是已经接过多家支付、想快速对照参数和踩坑点的老手。我拆了一遍把实现思路、参数来源和容易翻车的位置全都展开讲。2. 支付集成的地基商户参数、密钥与回调地址的配置约定支付集成第一步不是写代码是把渠道参数理清楚。很多人在这一步就埋下隐患微信回调验签失败、支付宝退款金额对不上多半是参数拿错或放错位置。2.1 参数清单这些配置项分别从哪个后台拿微信支付和支付宝的参数体系完全不一样但落地的原则相同能放配置文件的就不硬编码能按环境隔离的就别混着用。我拆这套资源时先列了一张参数清单照着填就不会漏。渠道参数获取位置用途说明微信支付mchid商户号微信商户平台-账户中心下单、退款请求的商户标识微信支付appid应用ID微信开放平台/公众号后台绑定App或小程序的凭证微信支付api v3 keyAPIv3密钥商户平台-API安全解密回调敏感信息和部分签名场景微信支付商户私钥 apiclient_key.pem商户平台-API安全生成请求签名必须保密微信支付平台证书序列号下载平台证书后解析回调验签和响应验签用支付宝appId应用ID支付宝开放平台-应用详情下单、退款请求的应用标识支付宝应用私钥开放平台工具生成请求签名用支付宝支付宝公钥开放平台-应用详情异步通知验签用支付宝网关地址开放平台文档沙箱与生产两个网关必须分开这里最容易被忽略的是微信支付「同一个小程序/App可以绑定多个商户号」的对应关系。资源里一般会预留 multi-merchant 的配置结构也就是把appid mchid作为一组后面下单和退款都基于这一组来取参数。真实项目里一个平台可能同时服务多个子商户不做好分组后续分账和对账会非常痛苦。2.2 证书与密钥的存放约定证书和密钥的存放方式直接影响本地开发和生产部署的差异。我见过直接把apiclient_key.pem塞进src/main/resources就提交进 Git 仓库的那等于把线上密钥公开了。配置目录要按环境分离比如resources/cert/dev/和resources/cert/prod/生产证书不进开发环境微信商户私钥通常命名为apiclient_key.pem支付宝私钥则是 PKCS8 格式的文本串以-----BEGIN PRIVATE KEY-----开头支付宝公钥在配置里要和应用私钥配对放沙箱环境下必须用沙箱公钥不能复用生产的建议启动时增加一个配置检查方法缺失私钥或证书直接抛异常不要让服务在缺参数的状态下假启动这套资源里一般会提供一个WxPayConfig和AlipayConfig的加载类把上面的参数从application.yml中读取并初始化。我习惯在加载完后打印一段脱敏日志只显示商户号和序列号后四位方便排查环境切换问题。2.3 第一笔交易服务端下单接口的完整链路下单接口是支付流程的入口。以微信支付V3的 Native 下单为例服务端要做的事是收到前端请求 → 生成订单号 → 组装支付参数 → 调微信下单接口 → 拿到code_url返回给前端生成二维码。public MapString, Object createNativeOrder(OrderCreateRequest req) { // 1. 订单幂等检查同一笔业务订单不能重复创建支付单 PayOrder payOrder payOrderMapper.selectByBusinessNo(req.getBusinessNo()); if (payOrder ! null PENDING.equals(payOrder.getStatus())) { throw new BizException(订单已存在待支付记录); } // 2. 落库生成内部支付单状态为待支付 String outTradeNo generateOrderNo(); payOrder.setOutTradeNo(outTradeNo); payOrder.setStatus(PENDING); payOrderMapper.insert(payOrder); // 3. 组装微信下单参数 MapString, Object params new HashMap(); params.put(appid, config.getAppid()); params.put(mchid, config.getMchid()); params.put(description, req.getProductName()); params.put(out_trade_no, outTradeNo); params.put(notify_url, config.getNotifyUrl()); params.put(amount, Map.of(total, req.getAmountInFen(), currency, CNY)); // 4. 调用微信V3下单接口返回 code_url String codeUrl wxPayClient.postJson(/v3/pay/transactions/native, params); return Map.of(codeUrl, codeUrl, outTradeNo, outTradeNo); }这段代码里最关键的是幂等检查和金额单位。幂等检查防止用户重复点击下单生成多笔支付记录金额单位则必须保持一致微信支付V3接口里total的单位是分不是元。如果项目里数据库存的是元这里要做一次换算我一般写死为BigDecimal乘法和intValue()转换避免浮点精度问题。支付宝的下单方式和微信不同它返回的是一段会自动跳转的页面代码或表单服务端只需要把收银台地址或表单字符串返回给前端。下单逻辑的骨架是一样的先落库、再调渠道接口但支付宝的out_trade_no可以直接用不需要像微信那样额外拆出out_trade_no和transaction_id两个概念。3. 微信支付V3与支付宝的签名逻辑同一笔交易两套做法下单接口能跑通紧接着就是回调处理。回调是支付业务里最容易出 bug 的地方因为两个渠道的签名机制差异很大不能靠着「差不多」的想法来写。3.1 微信支付V3商户私钥签名请求平台证书验签回调微信支付V3 的签名逻辑是所有请求用商户私钥做 RSA-SHA256 签名请求头带上Authorization收到回调时微信用平台私钥签名商户用平台证书验签。这种双向签名设计比老版 V2 的 MD5 加盐安全得多但代价是参数多了一套。验签的核心不是自己实现 RSA 算法而是正确加载平台证书并调用 SDK 的方法。这套资源里一般会封装一个payNotifyHandler处理逻辑是这样public String handleWxNotify(String body, MapString, String headers) { // 1. 先验签验签失败说明回调可能被篡改或证书不匹配 boolean verify wxPayClient.verifyNotifySign(headers.get(Wechatpay-Signature), body); if (!verify) { return FAIL; // 微信要求返回 FAIL否则会持续重试 } // 2. 验签通过后再解密V3 回调的敏感字段是 AES-256-GCM 加密的 String plaintext wxPayClient.decryptNotify(body); JSONObject data JSONObject.parseObject(plaintext); // 3. 处理业务只有金额一致且订单未处理时才更新状态 PayOrder payOrder payOrderMapper.selectByOutTradeNo(data.getString(out_trade_no)); if (payOrder null) { return FAIL; } String tradeState data.getString(trade_state); if (SUCCESS.equals(tradeState) PENDING.equals(payOrder.getStatus())) { payOrder.setStatus(PAID); payOrder.setTransactionId(data.getString(transaction_id)); payOrderMapper.updateById(payOrder); } return SUCCESS; // 处理完成返回 SUCCESS微信停止重试 }这里要特别说明两点。第一微信V3回调的out_trade_no是在解密之后的 JSON 里不是在请求头里不能直接拿原始 body 里的字段去查订单。第二验签时需要用到请求头里的Wechatpay-Signature如果前面经过了 Nginx 转发必须让 Nginx 把原始头透传过来否则验签一定失败。这两个坑我在实际项目里都踩过。3.2 支付宝应用私钥签名、平台公钥验签与参数排序支付宝的签名机制相对直白一些但参数排序规则很死板所有参数按 key 的 ASCII 码升序排列拼成字符串做 RSA2 签名。异步通知验签时同样按这个规则把参数重排然后用支付宝公钥验签。public boolean verifyAlipayNotify(MapString, String params) { // 1. 剔除掉 sign 和 sign_type 两个字段其余参数参与验签 MapString, String signParams new TreeMap(params); signParams.remove(sign); signParams.remove(sign_type); // 2. 拼接原始字符串并调用 SDK 验签 String content AlipaySignature.getSignContent(signParams); return AlipaySignature.rsaCheckV1(content, config.getAlipayPublicKey(), UTF-8, RSA2); }注意这一步是最容易翻车的有的实现里把sign_type也拿去参与拼接导致验签永远失败。rsaCheckV1默认会把空值参数也拼进去所以TreeMap排序后直接交给getSignContent就行不要在外部手工拼接字符串。既然两个渠道的异步通知都要先验签再处理业务控制器的写法就统一成先验签失败返回特定标识成功处理业务后返回约定内容。微信返回SUCCESS/FAIL支付宝返回success/fail字符串不一致也会导致回调被重复发送。3.3 两个渠道的差异对照表模型建对上线后能少一半问题。下面是实操中最容易踩的差异点我建议整理成团队文档对比项微信支付V3支付宝下单方式JSAPI/Native/H5 分开接口统一接口 返回跳转表单金额单位分整数元保留两位小数异步通知协议平台私钥加密 平台证书验签参数重排 公钥验签通知返回SUCCESS/FAILsuccess/fail接受重复通知会重试最多 24 小时会重试间隔逐渐拉大同步返回结果只返回 code_url无支付结果同步页面跳转不代表支付成功金额单位这一行值得单独强调微信传分、支付宝传元如果直接在同一个订单服务里复用同一个金额字段必须明确保存的基准单位是分。这属于那种「上线前看着没问题上线后第一笔退款就出问题」的坑。4. 退款功能实现原路退回、部分退与状态机联动支付做完退款是第二个大头。退款比支付更容易出问题因为有大量边界能不能部分退、退款单号怎么生成、要不要等渠道结果、退款失败怎么处理。4.1 微信退款原路退回与部分退实现微信支付V3的退款接口支持全额退和多次部分退只要累计退款金额不超过订单金额即可。和下单一样退款也是异步的提交退款请求后微信先受理退款结果通过单独的退款回调通知服务端。public void refundByOutTradeNo(String outTradeNo, int refundAmount, String refundReason) { // 1. 校验订单状态只有 PAID 的订单才能发起退款 PayOrder order payOrderMapper.selectByOutTradeNo(outTradeNo); if (order null || !PAID.equals(order.getStatus())) { throw new BizException(订单状态不允许退款); } // 2. 累计已退金额不能超过订单金额 int refunded payOrderMapper.sumRefundedAmount(outTradeNo); int maxRefund order.getAmountInFen() - refunded; if (refundAmount maxRefund) { throw new BizException(退款金额超出可退金额); } // 3. 生成退款单号这个号在微信侧是幂等标识 String outRefundNo RF System.currentTimeMillis() RandomUtil.randomNumbers(4); MapString, Object params new HashMap(); params.put(out_trade_no, outTradeNo); params.put(out_refund_no, outRefundNo); params.put(reason, refundReason); params.put(amount, Map.of( refund, refundAmount, total, order.getAmountInFen(), currency, CNY )); // 4. 先落退款记录表再调微信接口保证本地有据可查 RefundRecord record new RefundRecord(); record.setOutRefundNo(outRefundNo); record.setStatus(PROCESSING); refundRecordMapper.insert(record); wxPayClient.postJson(/v3/refund/domestic/refunds, params); }退款接口的构造点在于out_refund_no就是幂等键同一个退款单号提交两次第二次会报「退款单号已存在」或返回原结果。因此我一般把退款记录先落库再去调微信接口。如果网络超时导致结果未知可以拿同一个out_refund_no查退款状态或重新提交微信能识别为同一笔请求。微信的退款结果是通过异步通知下发的不是退款接口同步返回。所以服务端必须有一个监听退款通知的入口拿到refund_status是SUCCESS才把退款记录更新为已退款同时把订单状态改为 REFUNDED。这套资源里通常会有refund_notify_url和支付回调共用一个 controller但验签逻辑要区分支付回调解密出的是trade_state退款回调解密出的是refund_status。4.2 支付宝退款退款单号唯一性与重复请求处理支付宝的退款接口和微信差别挺大它支持单笔退款和批量退款退款请求是同步返回结果的结果里带fund_change标记当前请求是否真正发生了资金变化。还有一个关键限制支付宝同一笔退款请求out_request_no只能使用一次不能像微信那样用同一个单号提交两次查状态。public AlipayTradeRefundResponse refund(AlipayRefundRequest req) { // 1. 校验原订单是支付宝渠道且已支付 PayOrder order payOrderMapper.selectByBusinessNo(req.getBusinessNo()); if (!ALIPAY.equals(order.getChannel())) { throw new BizException(订单渠道不是支付宝); } // 2. 组装退款参数注意支付宝金额单位是元不能用分直接传 AlipayTradeRefundRequest request new AlipayTradeRefundRequest(); request.setBizContent({ \out_trade_no\:\ order.getOutTradeNo() \, \refund_amount\:\ order.getAmountInYuan() \, \out_request_no\:\ UUID.randomUUID().toString().replace(-, ) \, }); AlipayTradeRefundResponse response alipayClient.execute(request); if (response.isSuccess()) { order.setStatus(REFUNDED); payOrderMapper.updateById(order); } else { // 注意同步返回失败不一定是退款失败也可能是网络抖动 log.warn(ali refund respond code{}, msg{}, response.getCode(), response.getMsg()); } return response; }支付宝这里有个容易误判的点execute抛出异常或者返回失败码不代表退款一定没成功。正确做法是当结果不明确时用out_trade_no和out_request_no去支付宝查询退款状态而不是直接给用户提示「退款失败」。这个资源里如果封装了查询接口建议保留并复用同一个out_request_no来查保证幂等。4.3 订单状态机支付、关单、退款三态间的边界支付和退款放在一起最怕的就是状态乱跳已退款订单又被支付回调更新为已支付已关单订单又收到退款请求。要守住这个边界核心是每个状态更新前都要做前置校验。当前状态允许的操作触发后状态关联渠道事件PENDING待支付支付成功回调PAID支付异步通知 trade_stateSUCCESSPENDING待支付超时关单CLOSED微信关单接口/本地定时任务PAID已支付发起退款REFUNDING退款接口受理REFUNDING退款中退款成功回调REFUNDED退款异步通知 refund_statusSUCCESSREFUNDING退款中退款失败回调PAID退款异步通知 refund_statusFAILED状态变更的代码不要写散落在各处我建议收敛到一个方法里updateOrderStatus(id, expectedStatus, targetStatus)。更新时带上WHERE status expectedStatus作为乐观锁。这样即使支付回调在极端情况下重复到达第二次更新时因为状态已经是 PAID 而不是 PENDING更新行数为 0也就不会把已退款订单重新刷回已支付。5. 支付集成避坑指南六类线上翻车现象与排查方法支付集成的坑集中爆发在后端联调和上线初期下面这几类是我在拆项目和实际开发中遇到最多的按「现象 → 原因 → 解决」记录。5.1 用户支付成功订单仍是待支付且等很久才恢复现象用户明明付了钱订单状态一两个小时还是待支付催单客服被骂惨。原因支付回调没到常见有两种。一是回调地址配错渠道方的notify_url指向了测试环境二是回调收到后处理失败代码里没有 return 正确的 SUCCESS渠道方会反复重试重试期间不做幂等订单状态被覆盖。解决先查渠道后台的「回调记录」确认是否送达再看服务端日志里回调入口有没有进来。处理逻辑必须幂等回调进来先查订单状态已支付就直接返回成功不再重复更新。通知入口要同时支持支付通知和退款通知的路由拆分。5.2 异步通知验签失败但同步结果正常现象微信支付的同步结果一切正常一旦真正付完钱回调验签就报错日志里全是 verify fail。原因最常见的是拿错了验签公钥。微信要拿平台证书里的公钥不是商户私钥支付宝容易把沙箱公钥和正式公钥混用。还有一个隐蔽原因Nginx 请求头过滤掉了Wechatpay-Signature或Wechatpay-Timestamp导致验签数据不完整。解决在验签失败时把请求头和原始 body 打印到日志先肉眼对比是哪个参数异常。如果是 Nginx 转发导致请求头丢失在proxy_pass前显式带上proxy_set_header Wechatpay-Signature $http_wechatpay_signature;。证书类问题去商户平台重新下载最新平台证书替换后重启服务。5.3 退款金额始终对不上账现象部分退 9.9 元账上退了 990 分另一笔退 0.07 元的订单账账不平。原因99% 是金额单位问题。微信接口要求分支付宝接口要求元项目库内金额可能存的是元或分三处不一致就会出现玄学式差异。解决定性一个内部基准单位全工程统一用分存储。调用微信时直接传整数分调用支付宝时用一个单独的工具方法把分转成字符串元longToYuan(amountInFen)返回BigDecimal.valueOf(amountInFen).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP).toPlainString()。所有渠道边界做单测确保金额换算不丢精度。5.4 下单接口偶发报错但过一会又好了现象运行时下单接口报「无效的证书」或「签名错误」重启后恢复但过段时间又出现。原因微信平台证书是有有效期的到期后本地缓存的旧证书私钥验签失败。支付宝应用公钥配置错误也会周期性出现这个问题。另外微信的 APIv3 密钥如果重置过本地配置没同步。解决给证书和密钥设置监控任务有效期剩余 30 天时发告警更换证书后优先在测试环境验证下单和回调两条链路。配置项建议用配置中心管理换证书不用重新发版。5.5 回调接口公网无法访问本地联调只见 request 不见 response现象本地起服务渠道方回调一直报「无法访问」微信后台看不到通知记录。原因回调地址要求公网可达。本地开发没有公网 IP或者安全组没有放行对应端口还有一种情况是内网网关把回调请求拦截了。解决本地联调用内网穿透把本地端口映射成临时公网地址跑通支付链路后马上关掉。生产环境检查安全组放行 HTTPS 443 和回调入口路径顺便确认域名有备案国内云厂商对未备案域名做拦截比较常见。5.6 退款请求提交成功但用户迟迟没收到钱现象服务端日志显示退款接口返回成功但用户银行账户一两天都没到账。原因支付宝/微信的退款是逐级清算的接口受理成功不代表资金已到账。微信退款回调refund_statusSUCCESS才代表渠道确认退款成功支付宝同步返回的fund_change字段能标识本次请求是否真的发生资金转移。解决不要用退款接口的同步结果当最终状态。服务端只管记录退款受理成功真正的状态迁移必须等退款异步通知。在退款记录表增加一个渠道最终状态字段与订单状态解耦定时任务扫描长时间未终态的退款单做渠道查询。6. 从沙箱到生产三个验证场景与上线前的强制检查6.1 沙箱里必跑的链路支付宝有沙箱环境微信没有公开沙箱常见做法是用一毛钱真实交易来验证全链路。我在验证这套资源时会依次跑通三组脚本微信创建订单 → 扫码支付 0.01 元 → 收支付回调 → 发起全额退款 → 收退款回调微信创建订单 → 支付 → 部分退一半 → 再部分退剩余 → 确认累计退款金额不超过原金额支付宝创建订单 → 沙箱账号支付 → 收异步通知 → 发起退款 → 确认fund_change为 true这三条跑完支付主链路才算基本可信。注意支付宝沙箱的账号余额有限退款测试时不要用大额避免把测试号余额刷没。6.2 用对账脚本验证状态一致性线上跑的订单多了总会有些状态错乱的脏数据。我习惯每天凌晨跑一个对账任务把本地订单表、支付记录表、退款记录表三方比对金额不平就告警-- 找出支付成功但本地状态不是 PAID/REFUNDING/REFUNDED 的订单 SELECT o.business_no, o.amount_in_fen, o.status FROM pay_order o WHERE o.status NOT IN (PENDING, CLOSED) AND o.channel_transaction_id IS NULL; -- 找出退款金额累计超过订单金额的记录 SELECT r.out_trade_no, SUM(r.refund_amount) AS total_refund FROM refund_record r WHERE r.status SUCCESS GROUP BY r.out_trade_no HAVING SUM(r.refund_amount) (SELECT amount_in_fen FROM pay_order WHERE out_trade_no r.out_trade_no);第一条 SQL 能查出「渠道侧已扣款但本地没落支付单」的记录多半是回调丢失后的脏数据第二条能直接揪出退款超额的问题。把这两个查询做成定时任务比人工对账靠谱得多。6.3 上线前的强制检查清单接支付功能上线我每次都会强制自己走一遍下面这份清单少一项都不允许发版私钥和证书不进 Git 仓库生产证书由发布系统注入支付回调入口有验签、有幂等、有状态乐观锁退款单号每次生成且落库渠道未知结果时可幂等重查金额统一以分为单位渠道边界处做显式转换并有单测覆盖沙箱配置和生产配置互相隔离启动时打印当前环境标记这套资源的代码本身覆盖了微信支付、支付宝支付、退款和回调的核心链路我拆完之后最大的收获是把两个渠道的差异点收在一张表里而不是各自维护一套逻辑。从那以后我每次接支付都强制走一遍「下单 → 回调 → 退款 → 重试」的验证链路微信和支付宝各跑一次才算完凡是偷懒跳过的环节后面都在线上用事故补了课。希望这套笔记能帮你少走这几步弯路也希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网