ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

聚合支付IJPay实战:微信支付v3接入与多渠道差异解析

聚合支付IJPay实战:微信支付v3接入与多渠道差异解析 简介IJPay是一款面向Java开发者的聚合支付工具包集成了微信支付、QQ支付、支付宝、京东支付、银联及PayPal等主流支付渠道并封装了各类常用接口。它不依赖第三方MVC框架可作工具类直接嵌入任意业务系统帮助开发者跳过繁琐的支付对接流程快速完成支付模块开发。资源共579个文件压缩包仅3.23MB其中以271个Java源码文件为主体辅以31个Markdown说明文档、63个HTML页面及配套的properties配置、JS/CSS和XML等涵盖代码示例、配置说明与前端展示资源便于按需查阅与二次开发。目前已有245人学习下载适合具备一定Java基础、希望快速集成多种支付方式的开发人员学习借鉴。1. 聚合支付 IJPay为什么支付模块值得单独封装一个 Java 项目里同时接微信支付和支付宝是件很磨人的事。微信走 API v3 要配证书序列号支付宝走 RSA2 要管理公钥私钥两边报文结构完全不一样等这两个渠道调通一周时间就没了。更尴尬的是微信的 SDK 和支付宝的 SDK 各自带一套 HTTP 工具和签名实现同时引进来还可能出现依赖冲突。IJPay 这个支付开发包解决的就是这个问题。它把微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付这些常用渠道的接口统一封装成工具方法不依赖任何第三方 MVC 框架你现有的 Spring Boot、JFinal、甚至纯 Servlet 项目都能直接嵌进去。适合需要快速接入多个支付渠道的程序开发同学尤其是小程序微信支付 v3 对接、Java 后台接入 PayPal 支付这类场景能省掉大部分重复的报文组装和验签代码。2. IJPay 的模块设计与 Maven 引入先看清支付单据长什么样2.1 按支付渠道拆分 jar而不是一个大而全的包很多支付 SDK 会把所有渠道塞进一个 jar引入后整个依赖树都被撑大。IJPay 反着来每个支付渠道一个独立模块IJPay-WxPay、IJPay-AliPay、IJPay-QQ、IJPay-JdPay、IJPay-UnionPay、IJPay-PayPal。用一个渠道就引一个包互不污染。这个设计有个实际好处如果项目只用微信支付接口依赖树里就不会出现支付宝的 SDK 类避免了两个 SDK 里同名的SignType、HttpUtils之类工具类冲突。我在一个老项目里就踩过这种坑——上游传下来的 pom 同时引了两个支付 SDK启动时出现NoSuchMethodError排查半天才发现是传递依赖把类覆盖了。IJPay 的项目目录里能看到jfinal.bat和mvnw.cmd这类构建脚本jfinal.bat是作者用来启动示例 demo 的示例工程基于 JFinal但 SDK 核心层并没有依赖 JFinal。实际接入时你完全可以只引 jar不需要引入 JFinal 框架。2.2 按需引入 Maven 依赖以微信支付 v3 为例pom 里这样配dependency groupIdcom.github.javen205/groupId artifactIdIJPay-WxPay/artifactId version使用中央仓库最新 release/version /dependency !-- 只用支付宝就换成这个 -- dependency groupIdcom.github.javen205/groupId artifactIdIJPay-AliPay/artifactId version使用中央仓库最新 release/version /dependency版本号建议直接去 Maven 中央仓库查com.github.javen205这个 groupId 下的IJPay-WxPay等 artifactId 就是。注意不同大版本的 API 略有调整2.x 和 1.x 的配置类名不完全一致锁定一个版本后不要混用。2.3 配置类商户参数不写死在代码里支付渠道的配置项包含 appId、商户号、API 密钥、证书路径、回调地址这些在不同环境dev、test、prod下值都不同。常见做法是抽一个配置类用ConfigurationProperties绑定到 yamlijpay: wxpay: app-id: wx**************** mch-id: 16******89 api-v3-key: 你的APIv3密钥 cert-serial-no: 证书序列号 private-key-path: classpath:/cert/apiclient_key.pem alipay: app-id: 2021************ private-key: MIIEvQIBADANB... alipay-public-key: MIIBIjANBgkq... sign-type: RSA2 gateway: https://openapi.alipay.com/gateway.doComponent ConfigurationProperties(prefix ijpay.wxpay) public class WxPayProperties { private String appId; private String mchId; private String apiV3Key; private String certSerialNo; private String privateKeyPath; // getter / setter 省略 }配置文件里放私钥要注意安全性生产环境建议用环境变量或配置中心覆盖私钥明文进 git 仓库属于低级事故。.pem证书文件放在classpath:/cert/下构建时注意不要把私钥打进前端资源包。这里说明一下IJPay-WxPay的 v3 接口在部分版本中通过WxPayV3Service或WxPayApiConfig组装请求你引入的 jar 里如果找不到这两个类搜索ApiConfig和Kit结尾的类即可工具方法的命名风格是统一的。3. 微信支付 v3 接入实战从下单到回调验签3.1 初始化微信支付 Bean微信支付 API v3 使用商户私钥对请求签名服务端用平台证书或公钥验签。IJPay 把证书加载和签名逻辑封装在配置类里Configuration public class WxPayAutoConfiguration { Bean public WxPayV3Service wxPayV3Service(WxPayProperties props) throws IOException { PrivateKey privateKey WxPayKit.getPrivateKey( new ClassPathResource(props.getPrivateKeyPath()).getInputStream()); WxPayV3Service service new WxPayV3Service(); service.setAppId(props.getAppId()); service.setMchId(props.getMchId()); service.setApiV3Key(props.getApiV3Key()); service.setCertSerialNo(props.getCertSerialNo()); service.setPrivateKey(privateKey); return service; } }mchId是商户号apiV3Key是 API v3 密钥在微信商户平台「账户中心-API 安全」里设置。certSerialNo是商户 API 证书的序列号privateKey是证书对应的私钥文件内容转换出来的对象。如果项目用的版本里没有WxPayV3Service按照同样的字段自己组装HttpRequest调WxPayApi.v3()也可以。注意API v3 密钥是一个 32 字节的字符串不是 API v2 的 32 个字符的 Key两者不通用。升级到 v3 后很多报错都出在这里。3.2 Native 下单接口调用以 PC 端扫码支付Native为例下单核心逻辑public String createNativeOrder(String orderNo, Integer amount, String description) { MapString, Object params new HashMap(); params.put(appid, wxPayV3Service.getAppId()); params.put(mchid, wxPayV3Service.getMchId()); params.put(description, description); params.put(out_trade_no, orderNo); params.put(notify_url, https://api.example.com/pay/wx/notify); params.put(amount, new Amount().setTotal(amount).setCurrency(CNY)); String result wxPayV3Service.payV3( builder().build(), WxPayModel.NATIVE, params); // 返回结果里包含 code_url生成二维码用 return JSON.parseObject(result).getJSONObject(data).getString(code_url); }这里的payV3方法名在不同版本可能有差异核心参数是固定的appid是公众号或小程序 appIdmchid是商户号out_trade_no是商户订单号notify_url是异步通知地址amount.total是订单金额。金额单位是「分」这是微信支付最容易踩的坑。订单 100.50 元传给微信的 total 必须是 10050写成 100.50 微信会直接返回参数错误。数据库设计金额时就用整数分存储展示层再除以 100一劳永逸地规避浮点精度问题。3.3 异步回调验签与报文解析下单后微信会把支付结果 POST 到notify_url回调处理是整个支付流程里最容易出安全问题的一环——必须验签通过后才能更新订单状态。PostMapping(/pay/wx/notify) public String wxNotify(HttpServletRequest request) throws IOException { String body IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); // 微信支付 v3 的验签需要请求头的 timestamp、nonce、serial、signature boolean verified wxPayV3Service.verifyNotify( request.getHeader(Wechatpay-Timestamp), request.getHeader(Wechatpay-Nonce), request.getHeader(Wechatpay-Signature), body ); if (!verified) { // 验签失败返回非 2xx 状态码让微信稍后重试 return failure; } // 解析回调报文确认订单状态为 SUCCESS JSONObject data JSON.parseObject(body).getJSONObject(resource); // 对 resource 字段做 AES-GCM 解密 String plaintext decryptResource(data.getString(ciphertext), data.getString(nonce), data.getString(associated_data)); // 更新订单状态记录第三方流水号 return success; }验签的本质是确认这个回调确实来自微信服务器而不是伪造请求。Wechatpay-Signature是微信用平台证书私钥生成的签名用平台证书公钥验证。如果验签失败直接返回 HTTP 200攻击者就能无限次尝试伪造回调。回调报文里的订单数据在resource字段是 AES-256-GCM 加密的解密用的密钥就是 API v3 密钥。解密后能拿到out_trade_no、transaction_id、trade_state等字段。业务侧根据out_trade_no找到本地订单比对金额一致后再置为已支付。3.4 查询、关单与退款支付结果异步通知不是百分百可靠必须有主动查询兜底。查询接口按out_trade_no或transaction_id查即可// 主动查询订单状态 String queryResult wxPayV3Service.queryOrderByOutTradeNo(orderNo); // 关单超时未支付时关闭 String closeResult wxPayV3Service.closeOrder(orderNo); // 退款需要商户私钥和证书refundNo 是商户退款单号 RefundModel refund new RefundModel() .setOutTradeNo(orderNo) .setOutRefundNo(refundNo) .setAmount(new RefundAmount().setRefund(refundAmount).setTotal(orderAmount).setCurrency(CNY)); String refundResult wxPayV3Service.refundV3(refund);退款接口要注意refund金额单位同样是分且total必须是订单原始金额。退款是异步处理的返回PROCESSING不代表成功需要接退款结果回调或主动查退款单状态。另外退款会消耗 API 证书的调用额度生产环境要配合退款原因记录。4. 支付宝、银联与 PayPal一套模式下的差异点4.1 支付宝金额单位变成元验签走 RSA2支付宝接入用AliPayApi配置对齐核心是这样AliPayApiConfig alipayConfig AliPayApiConfigKit.init( appId, privateKey, alipayPublicKey, RSA2, gateway);关键差异在于金额单位微信传分支付宝传元。同样是 100.50 元支付宝传的字符串是100.50精确到小数点后两位。所以做一个统一的支付层时内部统一用「元」还是「分」要定清楚对接微信时乘 100支付宝时直接转字符串。支付宝的异步回调验签方式也和微信不同// request 是支付宝 POST 过来的参数不用读 body参数在 request 的 form 里 boolean signVerified AliPayApi.rsaCheckV1( request.getParameterMap(), alipayConfig.getAlipayPublicKey(), UTF-8, RSA2); if (signVerified) { String tradeStatus request.getParameter(trade_status); // TRADE_SUCCESS / TRADE_FINISHED 才表示支付成功 return success; // 必须原样返回 success否则支付宝会重试通知 } return failure;支付宝的通知是 form 表单格式app_id、out_trade_no、total_amount、trade_status都是平铺参数。验签通过后判断trade_status只有TRADE_SUCCESS和TRADE_FINISHED两种状态才要更新本地订单。回调返回字符串必须是success不带引号返回其他内容支付宝会按八次、两倍间隔重试。4.2 银联证书两层敏感字段单独加密银联支付UnionPay的逻辑跟微信、支付宝的 RESTful 风格不一样它走的是报文 证书的老一代网关体系。IJPay 的 UnionPay 模块默认支持签名证书商户私钥证书用来对请求报文签名加密证书银联公钥证书用来加密cardNo、cvn2等敏感报文域验签证书验证银联返回报文的签名// 银联请求分两种前台页面跳转和后台无页面 SupplierEndRequest req new SupplierEndRequest(); req.setMerId(mchId); req.setOrderId(orderNo); req.setTxnAmt(amount); // 单位分和微信一致 req.setTxnTime(yyyyMMddHHmmss); // 证书路径、密码在 sdk.properties 或自定义配置中设置 String result UnionPayApi.supplierEnd(req);银联的txnTime是交易时间格式固定为yyyyMMddHHmmss同一商户一天内订单号不能重复。它的异步通知报文也不是 JSON而是 key-value 拼装起来的明文 签名串IJPay 封装了UnionPayApi的报文解析和验签方法不需要自己拼验签串。做对账时注意银联的清算文件是 T1 生成的要按txnTime和acqInsCode匹配。4.3 PayPal先拿 Access Token再两级确认PayPal 的逻辑跟国内支付完全不同它是 OAuth 2.0 授权模式先创建订单拿到 PayPal 侧订单号前端跳转 PayPal 支付支付完成后 PayPal 回跳后端再调用 capture 接口把钱真正划过来。// 第一步换取 access token String token PayPalApi.getAccessToken(clientId, clientSecret, mode); // 第二步创建订单 PayPalOrder order PayPalApi.createOrder(token, amount, currency, returnUrl, cancelUrl); // 第三步用户支付完成回跳后捕获订单 PayPalApi.captureOrder(token, orderId);注意 PayPal 的金额是字符串100.50没有整数分概念。捕获订单时要判断status COMPLETED而且创建订单和捕获订单之间的间隔不要太长部分 PayPal 商户配置下订单超时后 capture 会失败。国内服务器访问 PayPal 接口有延迟线上环境要调大connectTimeout和readTimeout同时做好失败重试。PayPal 的 webhook 回调验签用的是事件签名和国内渠道的签名算法不通用。4.4 常用接口清单与差异对比功能点微信支付 v3支付宝银联PayPal金额单位分整数元字符串两位小数分整数元字符串签名算法商户私钥签名 平台证书验签RSA2双证书签名加密OAuth 2.0 Bearer Token主动查询queryOrderByOutTradeNoalipay.trade.queryUnifiedTradeQuerygetOrder退款API v3 退款接口alipay.trade.refund退货接口refundOrder异步通知格式JSON AES 密文Form 表单key-value 串Webhook JSON回调失败重试最多重试多次间隔递增8 次2 倍间隔间隔策略可配置Webhook 队列QQ 支付从技术体系上看和微信支付同源都走财付通网关IJPay 的 QQ 模块在报文格式和签名方式上贴近微信 v2配置QQPayConfig时注意区分 appId 和 mchId。京东支付则是独立的一套网关协议JdPayApi的核心参数是ossMerchantNo和merchantNo跟微信支付宝差异较大但整体流程也是下单-回调验签-查询。5. 收尾技巧回调幂等、金额精度与排错链路支付回调必须幂等这是所有接入方的共识但实现起来容易漏。微信和支付宝的异步通知机制都保证「至少一次」而不是「恰好一次」这意味着同一笔订单可能收到多次成功回调。常见做法是更新订单状态时加条件更新Transactional public void handlePaidOrder(String outTradeNo, String thirdTradeNo, long paidAmount) { Order order orderMapper.selectByOutTradeNo(outTradeNo); // 状态已经支付过直接返回不重复处理 if (order ! null PAID.equals(order.getStatus())) { return; } int updated orderMapper.compareAndSetStatus( outTradeNo, UNPAID, PAID, thirdTradeNo, paidAmount); // 返回 0 说明订单状态已被其他回调改成 PAID幂等完成 if (updated 0) { return; } // 到这一步才允许发放在线时长、增加积分等业务动作 }数据库层用UPDATE ... WHERE status UNPAID做条件更新比先查后更稳妥。如果支付结果要同步给多个下游系统优先用本地消息表 MQ 发送而不是在回调线程里同步调用回调线程超时会导致支付平台重发通知形成连锁重复。金额精度的问题在聚合场景下比单渠道更突出。统一的支付入口建议把所有金额定义为long分对外接微信、银联直接传对接支付宝、PayPal 时用BigDecimal从分转换private String fenToYuanStr(long fen) { return BigDecimal.valueOf(fen).divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).toString(); }排错时先看三件事第一打开 IJPay 的 HTTP 日志请求和响应报文都会打到控制台或 log 文件检查mchid和appid是否匹配第二确认服务器时间与标准时间偏差微信支付 v3 的时间戳校验误差超过五分钟就会验签失败NTP 同步不能省第三把回调接口的验签失败原因往后端日志里打印微信回调失败时先确认平台证书是否已更新——平台证书有效期通常是五年但更换商户证书后旧的平台证书就不能再验签了。最后一个实用技巧回调接口的密码学操作集中到一个PayNotifyService所有渠道的验签、解密、幂等判断都收敛在这一个类里每个渠道只暴露parseParams()和verify(params)两个方法。这样换支付渠道时业务层只依赖统一的通知结果对象不会出现「微信下单、支付宝回调」这种剪不断理还乱的后端代码。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进