SpringBoot集成微信支付V3版本流程(商户平台篇)

一、前言

微信支付账号类型分为商户平台和合作伙伴平台,今天主要是梳理商品平台微信支付流程。

商品平台文档地址,(在接入前建议仔细阅读这份文档,会少走很多弯路!!!)

小程序下单 - 小程序支付 | 微信支付商户文档中心

二、接入流程

以下是我SpringBoot项目接入微信支付V3流程,如果还有更好的集成方式,欢迎留言探讨。

2.1、pom.xml导入微信支付SDK

<!--微信支付SDK-->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.3.0</version>
</dependency>

2.2、application.yaml配置微信支付相关配置

# 微信支付相关参数
wxpay: 
# 商户号
  mch-id: 1888888888
# 商户API证书序列号
  mch-serial-no: 188D8D888888888A476B7DEB5F820082570CA4BA  
# APIv3密钥
  api-v3-key: 88888888
# APPID
  appId: wx88888ff5ee888f88
# 微信服务器地址
  domain: https://api.mch.weixin.qq.com
# 微信支付回调地址 https和外网可以访问的地址
  notify-domain: https://35p3811d81.yicp.fun
# 商户私钥 此处注意换行,回车等不可见字符
  private-key: BIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDV9Hl2qVkpVaAfBiH4SCv6dFbWS/Rem+HhFwQ8mV1XtT7kZUmaSEbrfOE3NSHx5oI+zpnr/nNHSh9vTYUbbH0LcE7Ad+oLtLNVTsZXLOrUPoEgcxSokaNHXd9gfGpKX/i8rkdNnOKTESBxGf8tGrQniBycWwGGJMUAngVeaJQKVN3PC+4WrLFm/x1gWUeGOvXNuKuAyu7VPgN+UKAUC1BuadwM6eokA

2.3、WxPayController.java(回调通知接口一定要从权限拦截器中剔除)

  /**
     * 微信支付API
     */
    @PostMapping("/save")
    @Resubmit
    public ApiResponse<?> saveWxPayOrder(@RequestBody OrderWxPayAppReq orderWxPayAppReq) throws Exception {
        orderWxPayAppReq.setUserId(getUserId());
        return ApiResponse.ok(orderWxPayService.saveWxPayOrder(orderWxPayAppReq));
    }

    /**
     * 微信退款API
     */
    @PostMapping("/refunds/apply")
    @Resubmit
    public ApiResponse<?> refunds(@RequestBody OrderWxPayAppReq orderWxPayAppReq) throws Exception {
        log.info("申请退款");
        orderWxPayAppReq.setUserId(getUserId());
        return ApiResponse.ok(orderWxPayService.wxRefund(orderWxPayAppReq));
    }
    
    /**
     * 微信支付回调通知
     */
    @PostMapping("/callback/pay/notify")
    public ApiResponse<?> payCallBack(HttpServletRequest request) {
        Gson gson = new Gson();
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                //失败应答
                return ApiResponse.error("通知验签失败");
            }
            //处理订单
            orderWxPayService.processOrder(bodyMap);
            //成功应答
            return ApiResponse.ok("成功");
        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            return ApiResponse.error("失败");
        }
    }
    
     /**
     * 微信退款结果通知
     */
    @PostMapping("/callback/refunds/notify")
    public ApiResponse<?> refundsNotify(HttpServletRequest request) {
        log.info("退款通知执行");
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Gson gson = new Gson();
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                return ApiResponse.error("通知验签失败");
            }
            //处理退款单
            orderWxPayService.processRefund(bodyMap);
            return ApiResponse.ok("成功");
        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            return ApiResponse.error("失败");
        }
    }

2.4、WxPayService.java

    @Override
    public Map<String, String> saveWxPayOrder(OrderWxPayAppReq orderWxPayAppReq) throws IOException {
        log.info("调用统一下单API");
        List<Long> orderIdList = orderWxPayAppReq.getOrderIdList();
        if (CollUtil.isEmpty(orderIdList)) {
            throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003);
        }
        //查询订单
        LambdaQueryWrapper<OrderInfo> orderQueryWrapper = new LambdaQueryWrapper<OrderInfo>()
                .in(OrderInfo::getOrderId, orderIdList)
                .eq(OrderInfo::getDeleted, Boolean.FALSE);
        List<OrderInfo> orderInfoList = orderInfoMapper.selectList(orderQueryWrapper);
        if (CollUtil.isEmpty(orderInfoList)) {
            throw new BaseRuntimeException(OrderErrorCodeEnums.ORDER_001);
        }
        AtomicReference<Long> totalPayMoney = new AtomicReference<>(0L);
        String orderIdStr = orderInfoList.stream().map(o -> {
            totalPayMoney.updateAndGet(v -> v + o.getPayMoney());
            return String.valueOf(o.getOrderId());
        }).collect(Collectors.joining(","));

        //调用统一下单API
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType()));
        String paymentKey = OrderNoUtils.createOrderNo(OrderNoUtils.PAYMENT_SERIAL_NUMBER_PREFIX);
        // key值有效期为1小时
        redisUtil.set(RedisKeys.OrderKeys.ORDER_PAYMENT_KEY + paymentKey, orderIdStr, 3600);
        // 请求body参数
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        paramsMap.put("appid", wxPayConfig.getAppId());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderIdStr);
        paramsMap.put("out_trade_no", paymentKey);
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.JSAPI_NOTIFY.getType()));

        Map<String, Object> amountMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        Long totalMoney = totalPayMoney.get();
        Boolean enableVirtualPay = Optional.ofNullable(enabled.getEnableVirtualPay()).orElse(Boolean.TRUE);
        if (enableVirtualPay) {
            totalMoney = 1L;
        }
        amountMap.put("total", totalMoney);
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);

        Map<String, String> payerMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        final UserThirdInfoApiVo userThirdInfoApiVo = userFeignApi.getUserThirdInfoRsp(orderWxPayAppReq.getUserId(), "WX", wxPayConfig.getManagerAppId());
        payerMap.put("openid", userThirdInfoApiVo.getOpenId());
        paramsMap.put("payer", payerMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        //返回二维码
        Map<String, String> map = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        try {
            //响应体
            String bodyAsString = EntityUtils.toString(response.getEntity());
            log.info("成功, 支付接口返回结果:{} ", bodyAsString);
            //响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            //响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //处理成功
            if (statusCode != 200 && statusCode != 204) {
                throw new BaseRuntimeException(OrderErrorCodeEnums.ORDER_002.code, resultMap.get("message"));
            }

            //预支付交易会话标识
            String prePayId = resultMap.get("prepay_id");
            String prePayIdStr = "prepay_id=" + prePayId;
            long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
            String timestamp = String.valueOf(epochSecond);
            String nonceStr = NonceUtil.createNonce(32);

            //关联的公众号的appid
            map.put("appId", wxPayConfig.getAppId());
            //时间戳
            map.put("timeStamp", timestamp);
            //生成随机字符串
            map.put("nonceStr", nonceStr);
            map.put("package", prePayIdStr);
            map.put("signType", "RSA");
            map.put("paySign", wxPayConfig.getPaySign(timestamp, nonceStr, prePayIdStr));
        } finally {
            response.close();
        }
        return map;
    }
    
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
        //将明文转换成map
        HashMap plainTextMap = new Gson().fromJson(decryptFromResource(bodyMap), HashMap.class);
        String paymentKey = String.valueOf(plainTextMap.get("out_trade_no"));
        Object orderIdObj = redisUtil.get(RedisKeys.OrderKeys.ORDER_PAYMENT_KEY + paymentKey);
        if (ObjectUtil.isNull(orderIdObj)) {
            return Boolean.FALSE;
        }
        // 处理支付成功后业务数据
        ... ...
        return Boolean.TRUE;
    }
    
      /**
     * 退款
     *
     * @param orderWxPayAppReq
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean wxRefund(OrderWxPayAppReq orderWxPayAppReq) throws IOException {
        if (ObjectUtil.isEmpty(orderWxPayAppReq.getAfterOrderId())) {
            throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003.code, OrderErrorCodeEnums.BASE_003.desc);
        }
        OrderAfter orderAfter = orderAfterMapper.selectById(orderWxPayAppReq.getAfterOrderId());
        OrderInfo orderInfo = orderInfoMapper.selectById(orderAfter.getOrderId());

        if (ObjectUtil.isNull(orderInfo.getPaySerialNumber())) {
            return Boolean.FALSE;
        }
        // 请求body参数
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        paramsMap.put("out_trade_no", orderInfo.getPaySerialNumber());
        paramsMap.put("out_refund_no", String.valueOf(orderAfter.getAfterOrderId()));
        paramsMap.put("reason", orderWxPayAppReq.getRefundReason());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));

        Map<String, Object> amountMap = new HashMap<>(CommonConstants.DEFAULT_MAP_CAPACITY);
        final Boolean enableVirtualPay = Optional.ofNullable(enabled.getEnableVirtualPay()).orElse(Boolean.TRUE);
        Long refund = orderAfter.getApplyRefundAmount();
        Long total = orderInfo.getPayMoney();
        if (enableVirtualPay) {
            refund = 1L;
            total = 1L;
        }
        amountMap.put("refund", refund);
        amountMap.put("total", total);
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");

        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求,并完成验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            //解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            Map<String, Object> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
            if (statusCode != 200 && statusCode != 204) {
                throw new BaseRuntimeException(OrderErrorCodeEnums.BASE_003.getCode(), String.valueOf(bodyMap.get("message")));
            }
            //保存退款日志
            return saveRefundPaymentBillInfo(bodyMap);
        } finally {
            response.close();
        }
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean processRefund(Map<String, Object> bodyMap) throws GeneralSecurityException, InterruptedException {
        //解密报文
        String plainText = decryptFromResource(bodyMap);
        //将明文转换成map
        HashMap plainTextMap = new Gson().fromJson(plainText, HashMap.class);
        String paySerialNumber = (String) plainTextMap.get("out_trade_no");
        String outRefundNo = (String) plainTextMap.get("out_refund_no");
        // 处理退款后的业务逻辑
        ... ...
        return Boolean.TRUE;
    }

    /**
     * 对称解密
     *
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);
        return plainText;
    }

2.5、WxApiType.java

@AllArgsConstructor
@Getter
public enum WxApiType {

	/**
	 * JSAPI下单
	 * https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
	 */
	JSAPI_PAY("/v3/pay/transactions/jsapi"),
	/**
	 * 申请退款
	 * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
	 */
	DOMESTIC_REFUNDS("/v3/refund/domestic/refunds");

	/**
	 * 类型
	 */
	private final String type;
}

2.6、WxPayConfig.java

/**
* 微信支付配置类
*/
@Configuration
@ConfigurationProperties(prefix = "wxpay")
@Data
@Slf4j
public class WxPayConfig {

    /**
     * 商户号
     */
    private String mchId;
    
    /**
     * 商户API证书序列号
     */
    private String mchSerialNo;

    /**
     * APIv3密钥
     */
    private String apiV3Key;

    /**
     * APPID
     */
    private String appId;

    /**
     * 微信服务器地址
     */
    private String domain;

    /**
     * 微信回调通知地址
     */
    private String notifyDomain;

    /**
     * 商户密钥
     */
    private String privateKey;

    /**
     * 获取商户的私钥
     *
     * @return
     */
    public PrivateKey getPrivateKey(String privateKey) {
        return PemUtil.loadPrivateKey(privateKey);
    }

    /**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {
        log.info("获取签名验证器");
        //获取商户私钥
        PrivateKey privateKeyObj = getPrivateKey(privateKey);
        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKeyObj);
        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
        // 使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));
        return verifier;
    }

    /**
     * 获取http请求对象
     *
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) throws FileNotFoundException {
        log.info("获取httpClient");
        //获取商户私钥
        PrivateKey privateKeyObj = getPrivateKey(privateKey);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKeyObj)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }

    public String getPaySign(String timestamp, String nonceStr, String prePayIdStr) {
        String paySign = "";
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(getPrivateKey(privateKey));
            String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prePayIdStr + "\n";
            sign.update(message.getBytes(StandardCharsets.UTF_8));
            paySign = Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
            e.printStackTrace();
        }
        return paySign;
    }

    public static void main(String[] args) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
        WxPayConfig wxPayConfig = new WxPayConfig();
        long timestamp = 1414561699L;
        String nonceStr = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS";
        String packageVal = "prepay_id=wx201410272009395522657a690389285100";
        String appId = "wx8b888b888e888d8a";
        String sign = wxPayConfig.getPaySign(String.valueOf(timestamp), nonceStr, packageVal);
        System.out.println(sign);
    }
}

2.7、WxNotifyType.java

/**
 * 微信支付,退款回调地址
 * 1.此处注意接口路径地址
 * 2.在权限控制拦截器中放开接口限制
 */

@AllArgsConstructor
@Getter
public enum WxNotifyType {

    /**
     * 支付通知
     */
    JSAPI_NOTIFY("/app/order/wx/pay/callback/pay/notify"),

    /**
     * 退款结果通知
     */
    REFUND_NOTIFY("/app/order/wx/pay/callback/refunds/notify");

    /**
     * 类型
     */
    private final String type;
}

三、参考资料

产品介绍 - JSAPI支付 | 微信支付商户文档中心

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中集成微信支付,你可以按照以下步骤进行操作: 1. 首先,你需要在微信商户平台注册账号并开通支付功能。获取到微信支付商户号(mchId)、API密钥(apiKey)和应用ID(appId)等关键信息。 2. 在你的Spring Boot项目中添加相关依赖。你可以在项目的pom.xml文件中添加以下依赖信息: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>3.0.10</version> </dependency> ``` 3. 创建一个配置类,配置微信支付相关参数。在该配置类中,你需要使用上面获取到的商户号、API密钥等信息进行配置: ```java @Configuration public class WxPayConfig { @Value("${wxpay.appId}") private String appId; @Value("${wxpay.mchId}") private String mchId; @Value("${wxpay.apiKey}") private String apiKey; // 创建WxPayService Bean,并配置相关参数 @Bean public WxPayService wxPayService() { WxPayConfig wxPayConfig = new WxPayConfig(); wxPayConfig.setAppId(appId); wxPayConfig.setMchId(mchId); wxPayConfig.setMchKey(apiKey); wxPayConfig.setNotifyUrl("你的异步通知地址"); return new WxPayServiceImpl(wxPayConfig); } } ``` 4. 创建一个Controller处理支付请求。在该Controller中,你可以使用WxPayService来进行支付相关操作,例如生成支付订单、发起支付等。 ```java @RestController @RequestMapping("/pay") public class WxPayController { @Autowired private WxPayService wxPayService; @PostMapping("/createOrder") public String createOrder() { // 生成支付订单 WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); // 设置订单参数 // ... WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request); // 处理支付结果,返回给前端 // ... return "success"; } } ``` 这只是一个简单的示例,你可以根据实际需求进行更详细的配置和处理。同时,你还需要根据自己的业务逻辑来处理支付结果的异步通知和订单查询等操作。 希望以上信息对你有所帮助!如果你还有其他问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值