微信直连商户公众号 JSAPI 支付,详细教程+源码

举报
悟空码字 发表于 2026/01/06 14:48:17 2026/01/06
【摘要】 JSAPI 支付用于微信公众号内的网页调起微信收银台,常见于在公众号菜单、文章页或 H5 活动页中完成支付。该方式依赖微信内置浏览器环境,非微信浏览器无法调起。

大家好,我是小悟。

适用场景与入口:JSAPI 支付用于微信公众号内的网页调起微信收银台,常见于在公众号菜单、文章页或 H5 活动页中完成支付。该方式依赖微信内置浏览器环境,非微信浏览器无法调起。

身份标识:下单前必须获取用户在当前公众号下的OpenID,通常通过 OAuth2 授权码 code → access_token → openid 流程获得。

下单与预支付:商户后端调用微信统一下单 API,传入必要参数(如 appid、mchid、out_trade_no、total、body、notify_url、openid、trade_type=JSAPI),微信返回prepay_id。

前端调起:前端使用微信 JSSDK  chooseWXPay 调起支付,传入由后端生成的支付参数与签名(appId、timeStamp、nonceStr、package=prepay_id=…、signType、paySign)。

结果处理:前端仅用于展示结果;商户以微信服务器的异步通知(notify)为准更新订单状态,并做好幂等。

域名与目录:需在公众号后台配置网页授权域名,在商户平台配置支付授权目录,否则会报“当前页面的 URL 未注册”等错误。

安全要点:全程使用 HTTPS,参数签名校验、金额一致性校验、通知去重与状态机控制。

以上要点与流程为微信公众号内网页支付的标准实践,适用于公众号场景的 JSAPI 调起与结果处理。

后端 Spring Boot 实现(Java)

说明:以下示例采用微信支付v3 版 API + SDK,包含配置、统一下单、回调验签与通知回执,便于快速落地。

  1. 1、Maven 依赖(pom.xml)
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 微信支付 v3 Java SDK -->
    <dependency>
        <groupId>com.github.wechatpay-apiv3</groupId>
        <artifactId>wechatpay-java</artifactId>
        <version>0.4.12</version>
    </dependency>
    <!-- JSON 处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

说明:使用官方维护的 wechatpay-java SDK,简化签名、验签与 HTTP 调用,适配 v3 接口与证书自动更新。

2、配置文件(application.yml)

wechat:
  appId: wx8888888888888888
  mchId: 1900000001
  merchantSerialNumber: 444C3F6B5A7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F
  privateKeyPath: classpath:cert/merchant/apiclient_key.pem
  apiV3Key: 7788990011223344556677889900112233445566
  notifyUrl: https://yourdomain.com/api/wxpay/notify

说明:配置包含公众号 appId商户号 mchId商户证书序列号商户私钥路径APIv3 密钥支付回调地址。其中 APIv3 密钥用于回调内容解密与验签。

3、配置类与 SDK 初始化

package com.example.wxpay.config;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.util.PemUtil;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;

@Configuration
@ConfigurationProperties(prefix = "wechat")
public class WechatPayConfig {

    private String appId;
    private String mchId;
    private String merchantSerialNumber;
    private String privateKeyPath;
    private String apiV3Key;
    private String notifyUrl;

    @Bean
    public JsapiService jsapiService() throws Exception {
        // 读取商户私钥(PKCS#8 PEM)
        InputStream keyInputStream = getClass().getClassLoader().getResourceAsStream(privateKeyPath);
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(keyInputStream);

        RSAAutoCertificateConfig config = new RSAAutoCertificateConfig.Builder()
                .merchantId(mchId)
                .privateKeyFromPath(privateKeyPath)
                .merchantSerialNumber(merchantSerialNumber)
                .apiV3Key(apiV3Key.getBytes(StandardCharsets.UTF_8))
                .build();

        return new JsapiService.Builder().config(config).build();
    }

    // getter/setter 省略
}

说明:通过 RSAAutoCertificateConfig 自动管理平台证书并完成请求签名,是微信支付 v3 推荐的接入方式。

4、统一下单与生成前端调起参数

package com.example.wxpay.service;

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.nativepay.model.Amount as NativeAmount;
import com.wechat.pay.java.service.payments.nativepay.model.Transaction;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.UUID;

@Service
public class WxPayService {

    private final JsapiService jsapiService;
    private final String appId;
    private final String notifyUrl;

    public WxPayService(JsapiService jsapiService,
                        @Value("${wechat.appId}") String appId,
                        @Value("${wechat.notifyUrl}") String notifyUrl) {
        this.jsapiService = jsapiService;
        this.appId = appId;
        this.notifyUrl = notifyUrl;
    }

    // 金额单位:分 -> 元(微信统一下单以“分”为单位)
    private long fenToYuan(Long fen) {
        return fen == null ? 0L : fen / 100L;
    }

    public PrepayResponse jsapiPrepay(String openid, Long totalFee, String body, String outTradeNo) throws Exception {
        PrepayRequest request = new PrepayRequest();
        Amount amount = new Amount();
        amount.setTotal(fenToYuan(totalFee)); // 微信金额单位:分
        request.setAmount(amount);
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setOutTradeNo(outTradeNo);
        request.setNonceStr(UUID.randomUUID().toString().replace("-", ""));
        request.setBody(body);
        request.setNotifyUrl(notifyUrl);
        request.setPayer(new com.wechat.pay.java.service.payments.jsapi.model.Payer().setOpenid(openid));
        return jsapiService.prepay(request);
    }
}

说明:调用 JsapiService.prepay 获取 prepay_id,后续用于前端调起支付。注意金额单位为,openid 为必填。

5、回调控制器(验签 + 解密 + 回执)

ackage com.example.wxpay.controller;

import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.NotificationRequest;
import com.wechat.pay.java.core.notification.NotificationResponse;
import com.wechat.pay.java.service.payments.nativepay.model.Transaction;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
@RestController
@RequestMapping("/api/wxpay")
public class WxPayNotifyController {

    private final NotificationParser notificationParser;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public WxPayNotifyController(RSAAutoCertificateConfig config) {
        this.notificationParser = new NotificationParser(config);
    }

    @PostMapping(value = "/notify", consumes = "application/json", produces = "application/json")
    public ResponseEntity<byte[]> handleNotify(HttpServletRequest request) {
        try {
            // 1) 解析并验签,自动解密 resource.data
            NotificationRequest req = notificationParser.parse(request.getInputStream());
            String resourceType = req.getResourceType();
            String plaintext = new String(req.getPlaintext(), StandardCharsets.UTF_8);
            String serialNo = req.getWechatpaySerial();
            String signature = req.getWechatpaySignature();
            String nonce = req.getWechatpayNonce();

            // 2) 根据资源类型处理(此处以 JSAPI 为例,实际可能为 TRANSACTION.TRANSACTION 或 JSAPI.PREPAYMENT_ID 等)
            if ("TRANSACTION.TRANSACTION".equals(resourceType)) {
                Transaction txn = objectMapper.readValue(plaintext, Transaction.class);
                String tradeState = txn.getTradeState();
                String outTradeNo = txn.getOutTradeNo();
                log.info("支付回调 trade_state={}, out_trade_no={}", tradeState, outTradeNo);

                // TODO: 校验商户订单号、金额一致性、幂等处理、更新订单状态

                // 3) 返回成功回执(必须)
                return ResponseEntity.ok().body("{\"code\":\"SUCCESS\",\"message\":\"成功\"}".getBytes(StandardCharsets.UTF_8));
            } else {
                log.warn("暂不支持的回调类型: {}", resourceType);
                return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                        .body("{\"code\":\"FAIL\",\"message\":\"不支持的回调类型\"}".getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            log.error("处理微信回调异常", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("{\"code\":\"FAIL\",\"message\":\"处理失败\"}".getBytes(StandardCharsets.UTF_8));
        }
    }
}

说明:使用 NotificationParser 自动完成验签与解密,业务侧仅需按资源类型解析并处理。回调必须返回200 与成功 JSON,否则微信会重试。

前端 UniApp 实现(Vue 2⁄3 通用思路)

说明:前端在公众号内页面调用 JSSDK 的 chooseWXPay,参数由后端生成并签名。注意使用 HTTPS 微信授权域名,并在支付回调中仅作页面跳转,以服务器异步通知为准更新订单。

1、安装并引入 JSSDK(NPM)

npm install jweixin-module --save

说明:使用官方推荐的 jweixin-module 在小程序/公众号 H5 中调用微信 JSAPI。

2、获取 OpenID(示例:OAuth2 简化流程)

// utils/auth.js
export function getWxCode(callback) {
  const appid = 'wx8888888888888888';
  const redirectUri = encodeURIComponent('https://yourdomain.com/pages/pay/index');
  const scope = 'snsapi_base'; // 静默授权,仅获取 openid
  const state = 'STATE';

  const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
  // 若当前页面无 code,则跳转授权
  if (!getLocationSearch('code')) {
    window.location.href = url;
  } else {
    const code = getLocationSearch('code');
    // 将 code 发送给后端换取 openid(示例)
    // axios.post('/api/wxpay/auth', { code }).then(res => callback(res.data.openid));
    callback('MOCK_OPENID'); // 仅演示,实际请请求后端
  }
}

function getLocationSearch(key) {
  const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)');
  const r = window.location.search.substr(1).match(reg);
  return r ? decodeURIComponent(r[2]) : null;
}

说明:通过 snsapi_base 静默授权获取 openid,用于统一下单必填字段。生产环境请在后端完成换取与校验。

3、调起支付页面(Vue 示例)

<template>
  <view class="pay-page">
    <button type="primary" @click="handlePay">微信支付</button>
  </view>
</template>

<script>
import jweixin from 'jweixin-module';
import { getWxCode } from '@/utils/auth';

export default {
  data() {
    return {
      payParams: null, // { appId, timeStamp, nonceStr, package: 'prepay_id=...', signType, paySign }
    };
  },
  async onLoad() {
    await getWxCode((openid) => {
      this.createOrder(openid);
    });
  },
  methods: {
    async createOrder(openid) {
      try {
        const res = await uni.request({
          url: 'https://yourdomain.com/api/wxpay/jsapi-prepay',
          method: 'POST',
          data: { openid, totalFee: 100, body: '测试商品', outTradeNo: 'ORDER_' + Date.now() },
        });
        this.payParams = res.data.data; // 后端返回的支付参数
        this.initWxConfig();
      } catch (err) {
        uni.showToast({ title: '下单失败', icon: 'none' });
      }
    },
    initWxConfig() {
      jweixin.config({
        debug: false,
        appId: this.payParams.appId,
        timestamp: parseInt(this.payParams.timeStamp, 10),
        nonceStr: this.payParams.nonceStr,
        signature: this.payParams.paySign,
        jsApiList: ['chooseWXPay'],
      });

      jweixin.ready(() => {
        // 可选:检测接口可用性
        jweixin.checkJsApi({
          jsApiList: ['chooseWXPay'],
        });
      });

      jweixin.error((err) => {
        console.error('JSSDK 配置失败', err);
        uni.showToast({ title: '支付初始化失败', icon: 'none' });
      });
    },
    pay() {
      if (!this.payParams) return;

      uni.showLoading({ title: '调起支付中' });

      jweixin.chooseWXPay({
        timestamp: parseInt(this.payParams.timeStamp, 10), // 注意:JSSDK 字段名为小写 timeStamp
        nonceStr: this.payParams.nonceStr,
        package: this.payParams.package,                 // 形如:prepay_id=wx123456...
        signType: this.payParams.signType,               // 通常为 MD5
        paySign: this.payParams.paySign,
        success: (res) => {
          uni.hideLoading();
          uni.showToast({ title: '支付成功', icon: 'success' });
          // 注意:前端成功回调并非最终结果,需以服务器异步通知为准
          uni.redirectTo({ url: '/pages/pay/result?status=success' });
        },
        cancel: () => {
          uni.hideLoading();
          uni.showToast({ title: '支付取消', icon: 'none' });
        },
        fail: (err) => {
          uni.hideLoading();
          console.error('支付失败', err);
          uni.showToast({ title: '支付失败', icon: 'none' });
        },
      });
    },
  },
};
</script>

说明:前端通过 chooseWXPay 调起支付,参数中的 package=prepay_id=…  paySign 由后端提供。注意 timeStamp 在 JSSDK 中为小写,与后端统一下单的 timeStamp(大写)命名不同。支付结果以服务器异步通知为准,前端回调仅用于页面提示与跳转。

部署与安全要点

域名与目录:在公众号后台配置网页授权域名,在商户平台配置支付授权目录,页面 URL 必须与配置完全匹配,否则无法调起支付。

HTTPS 与证书:全链路使用 HTTPS;商户 API 证书妥善保管,SDK 已支持自动更新平台证书,无需重启。

金额与签名:金额统一用;签名使用微信指定算法(v3 为 RSA-SHA256),严格按字典序与规则拼接。

幂等与去重:以微信异步通知为准更新订单,做好幂等(如按商户订单号去重、状态机校验)。

以上为公众号支付的关键注意事项,能有效避免“未注册 URL”“签名错误”“无法调起”等常见问题。

微信直连商户公众号 JSAPI 支付,详细教程+源码.jpg

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。