Java微信支付案例如何实现

wen java案例 31

Java微信支付案例完整实现指南(含核心源码)

目录导读

  • 微信支付接入前的必备准备
  • 核心API与支付流程解析
  • Java后端实现:统一下单与回调处理
  • 常见问题与排坑问答(FAQ)
  • 安全与性能优化建议

微信支付接入前的必备准备

在开始Java微信支付案例之前,需要完成以下环境与资质准备:

Java微信支付案例如何实现

  1. 商户号与AppID
    登录微信支付商户平台,获取商户号(mch_id),同时需拥有已认证的微信服务号或小程序,获取对应的AppID,两者需在商户平台进行关联绑定。

  2. API密钥与证书

    • APIv3密钥:在商户平台设置APIv3密钥,用于生成请求头中的签名。
    • 商户证书:下载商户证书(p12或pem格式),用于加密敏感信息(如退款接口)或作为回调验证凭证。
    • 平台证书:通过平台证书下载接口获取,用于验证微信回调通知的签名。
  3. 开发环境

    • JDK 8+
    • Maven或Gradle(推荐Maven)
    • 内网穿透工具(如ngrok)或已备案域名,用于回调地址测试

注意:回调域名必须在商户平台配置白名单,否则支付成功后无法收到通知。


核心API与支付流程解析

微信支付的主流场景包括:JSAPI支付(公众号内支付)、小程序支付Native支付(扫码支付)、H5支付,本文以JSAPI支付为例,其流程如下:

用户触发支付 → 后端调用统一下单API → 返回预支付ID → 前端调起微信支付 → 用户密码验证 → 异步回调通知后端 → 后端查询订单状态 → 完成业务

关键接口清单

接口名称 请求方式 功能简述
v3/pay/transactions/jsapi POST 统一下单,返回prepay_id
v3/pay/transactions/id/{transaction_id} GET 通过微信支付订单号查询订单
v3/pay/transactions/out-trade-no/{out_trade_no} GET 通过商户订单号查询订单
v3/pay/transactions/jsapi/... POST 关闭订单(支付超时后使用)

Java后端实现:统一下单与回调处理

关键依赖(pom.xml)

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.11</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

配置类(WechatPayConfig.java)

@Component
@ConfigurationProperties(prefix = "wechat.pay")
public class WechatPayConfig {
    private String appId;
    private String mchId;
    private String apiV3Key;
    // 证书路径
    private String privateKeyPath;
    private String merchantSerialNumber;
    private String notifyUrl;
    // 省略getter/setter
}

统一下单核心代码(PayService.java)

@Service
public class PayService {
    @Autowired
    private WechatPayConfig wechatPayConfig;
    public String createJsapiOrder(String openId, String orderNo, Integer totalFee, String description) {
        try {
            // 1. 构建HTTP客户端
            HttpMethods httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(wechatPayConfig.getMchId(), 
                              wechatPayConfig.getMerchantSerialNumber(), 
                              new File(wechatPayConfig.getPrivateKeyPath()))
                .withValidator(new WechatPay2Validator(VerifierHolder.getVerifier()))
                .build();
            // 2. 构建请求体
            JSONObject params = new JSONObject();
            params.put("appid", wechatPayConfig.getAppId());
            params.put("mchid", wechatPayConfig.getMchId());
            params.put("description", description);
            params.put("out_trade_no", orderNo);
            params.put("notify_url", wechatPayConfig.getNotifyUrl());
            params.put("amount", new JSONObject() {{
                put("total", totalFee);  // 单位:分
                put("currency", "CNY");
            }});
            params.put("payer", new JSONObject() {{
                put("openid", openId);
            }});
            // 3. 发送请求
            HttpUriRequest request = RequestBuilder.create("POST")
                .setUri("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi")
                .setHeader("Content-Type", "application/json")
                .setHeader("Accept", "application/json")
                .setEntity(new StringEntity(params.toJSONString(), "utf-8"))
                .build();
            HttpResponse response = httpClient.execute(request);
            String result = EntityUtils.toString(response.getEntity());
            // 4. 解析预支付ID
            JSONObject respObj = JSONObject.parseObject(result);
            String prepayId = respObj.getString("prepay_id");
            // 5. 生成前端调起支付所需的参数
            return generatePayParams(prepayId);
        } catch (Exception e) {
            log.error("统一下单失败", e);
            throw new BusinessException("支付创建失败");
        }
    }
}

回调通知处理(PayNotifyController.java)

@PostMapping("/pay/notify")
public String handleNotify(HttpServletRequest request) {
    // 1. 读取请求体
    String body = IOUtils.toString(request.getInputStream(), "utf-8");
    // 2. 获取签名头
    String signature = request.getHeader("Wechatpay-Signature");
    String timestamp = request.getHeader("Wechatpay-Timestamp");
    String nonce = request.getHeader("Wechatpay-Nonce");
    // 3. 验证签名(核心安全步骤)
    if (!WechatPayUtil.verifySignature(body, signature, timestamp, nonce)) {
        log.warn("回调签名验证失败");
        return "FAIL";
    }
    // 4. 解析回调数据
    JSONObject notifyObj = JSONObject.parseObject(body);
    String resourceType = notifyObj.getString("resource_type");
    if ("encrypt-resource".equals(resourceType)) {
        // 解密resource中的ciphertext
        JSONObject resource = notifyObj.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String associatedData = resource.getString("associated_data");
        String nonceStr = resource.getString("nonce");
        String plaintext = WechatPayUtil.decrypt(ciphertext, associatedData, nonceStr, apiV3Key);
        JSONObject payResult = JSONObject.parseObject(plaintext);
        String outTradeNo = payResult.getString("out_trade_no");
        String transactionId = payResult.getString("transaction_id");
        String tradeState = payResult.getString("trade_state");
        // 5. 更新订单状态(加锁防重复)
        if ("SUCCESS".equals(tradeState)) {
            orderService.updateOrderStatus(outTradeNo, transactionId);
        }
    }
    return "SUCCESS"; // 返回SUCCESS给微信
}

常见问题与排坑问答(FAQ)

Q1:统一下单时报错“签名错误”怎么办?

A:检查以下三点:

  • privateKeyPath 是否正确指向商户证书的私钥文件(通常是pem格式,需要包含 -----BEGIN PRIVATE KEY-----
  • merchantSerialNumber 是否与微信商户平台显示的证书序列号一致(可在商户平台“API安全”-“API证书”中查看)
  • 请求体中的 out_trade_no 是否每次唯一,且长度≤32位

Q2:用户支付成功后,回调通知一直收不到?

A:按顺序排查:

  1. 回调地址 notify_url 必须是外网可访问的URL,且已配置在商户平台的“开发配置”-“支付回调域名”中。
  2. 检查服务器防火墙是否放行443端口,且无反向代理做IP白名单过滤。
  3. 使用ngrok或公网服务器测试时,确认请求体未被篡改或压缩。

Q3:前端调起支付后弹窗提示“缺少参数”?

A:前端参数需要从后端返回的 generatePayParams 方法中获取,检查以下JSON结构是否正确:

{
  "appId": "wx...",
  "timeStamp": "当前时间戳",
  "nonceStr": "随机字符串",
  "package": "prepay_id=wx...",
  "signType": "RSA",
  "paySign": "通过规则生成的签名"
}

注意 paySign 的签名规则:以 appId + \n + timeStamp + \n + nonceStr + \n + package + \n 拼接字符串,再用商户私钥进行SHA256-RSA签名。

Q4:支付金额是1元,但用户实际支付多了一次手续费?

A:微信支付没有额外手续费,检查你的计算逻辑:totalFee 单位是“分”,即1元需传 100,如果误传了 1,用户支付0.01元,而数据库记录为1元,则会出现金额不符,建议在关键位置增加日志:log.info("支付金额:{}分", totalFee);

Q5:如何防止回调重复处理?

A:回调可能因为网络重试等原因到达多次,建议的做法:

  • 在更新订单状态时使用数据库行锁:select ... for update
  • 或使用Redis分布式锁,key = order:notify:${orderNo},过期时间设10秒。
  • 判断 trade_state 后,若订单状态已为“已支付”,直接返回 SUCCESS 不处理。

安全与性能优化建议

  1. 密钥管理
    私钥文件绝不可上传到代码仓库,建议保存在外部配置中心(如Apollo、Nacos)或服务器环境变量中,生产环境可使用HashiCorp Vault进行密钥轮换。

  2. 签名验证
    所有的微信回调(包括退款结果通知)都必须验证签名,推荐使用 wechatpay-java 官方SDK的 WechatPay2Validator,它内置了平台证书自动更新逻辑。

  3. 超时与重试
    统一下单接口建议设置连接超时5s,读取超时10s,对于退款、查询订单等幂等接口,可以实现指数退避重试策略。

  4. 日志脱敏
    打印日志时要注意脱敏。openid 只保留前4位和后4位(如:o***1234),totalFee 可记录,但 apiV3Key 和私钥内容绝不能输出。

  5. 并发控制
    如果秒杀场景下大量并发下单,建议预扣库存并在回调成功后再确认扣减,注意微信支付对同一商户号下并发请求有限流(默认2000 QPS),超出会返回 SYSTEM_ERROR


本文从准备工作、API流程、Java代码实现到排坑问答,完整覆盖了微信支付(JSAPI)的实战全链路,核心要点在于:正确配置证书、严格验证回调签名、幂等处理通知,如果你需要对接Native支付或小程序支付,只需将接口地址替换为对应接口,并调整前端调起支付方式即可,希望这篇案例能帮你一次跑通微信支付流程,避免常见踩坑。

参考来源:微信支付官方文档V3、开源SDK wechatpay-java 示例、互联网社区实践总结(去伪存真整理)。

抱歉,评论功能暂时关闭!