使用SDK实现微信支付

662 阅读6分钟

前言

最近公司在做一款自用的点餐小程序, 对接微信支付,所以我第二次进行了对接微信支付的业务代码开发。以小程序支付和退款为例,整理了服务器和微信交互的代码,以备将来使用。

依赖配置

pom.xml配置微信支付SDK

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.12</version>
</dependency>

application.yml配置信息

wechat:
  # 商户号
  mch-id: 1666840757
  # 商户API证书序列号
  mch-serial-no: 6396D77164223E3CD722C28CD2DD2DF28485F0180021
  # 商户私钥文件 注意:该文件放在项目根目录下
  private-key-path: apiclient_key.pem
  # APIv3密钥
  api-v3-key: Admin123Admin123Admin123Admin12301
  # APPID
  appid: wx8e5c28426745f9fb7855
  # 支付结果微信回调
  notify-url: https://myweb-url/notify/notifyPayResult
  # 退款结果微信回调
  refund-notify-Url: https://myweb-url/notify/refundNotify

业务步骤

  1. 前端调用“预支付”接口,获取唤醒微信支付页面需要的参数。
  2. 后端系统进行业务处理,并与微信端交互,获取prepayId参数,并组装数据返给前端。
  3. 通过解析微信异步返回的支付结果,对业务进行处理。

业务代码

微信配置信息,用来读取yml文件中的配置。

/**
 * @author xsong.Y
 * @description 微信支付配置信息
 */
@Configuration
@ConfigurationProperties(prefix = "wechat")
@Data
public class WeChatConfig {

    /** 商户号 */
    private String mchId;
    /** 商户API证书序列号 */
    private String mchSerialNo;
    /** 商户私钥文件路径 */
    private String privateKeyPath;
    /** APIv3密钥 */
    private String apiV3Key;
    /** APPID */
    private String appid;
    /** 支付通知回调 */
    private String notifyUrl;
    /** 退款通知回调 */
    private String refundNotifyUrl;
    
}

支付服务代码,主要实现预支付和申请退款

package com.yxsong.wechatpay.service;

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 com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.yxsong.common.tools.DateTimeUtil;
import com.yxsong.wechatpay.config.WeChatConfig;
import com.yxsong.wechatpay.dto.PayOrderDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.FileCopyUtils;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author xsong.Y
 * @description 支付
 */
@Service
@Slf4j
public class PayService {
    
    @Resource
    private WeChatConfig weChatConfig;
    
    
    /**
     * 小程序支付。首先需要进行预支付获取"预支付交易会话标识",前端获取到该数据后,唤起微信支付页面。
     * 预支付
     * @param order 请求参数,可以根据具体业务来设计
     * @return Map
     */
    public Map<String, Object> prepaid(PayOrderDto order) throws Exception {
        
        
        
        // 业务-交易订单信息校验,如重复支付、订单是否存在等。
        //.......
        
        // 预支付业务
        // 时间戳
        long timestamp = DateTimeUtil.getTimestamp();
        // 随机字符串
        String nonceStr = UUID.randomUUID().toString().replaceAll("-", "").substring(0,32);
        String signType = "RSA";
        // 创建SDK中的JsapiService对象
        JsapiService jsapiService = new JsapiService.Builder().config(this.createConfig()).build();
        // 构建业务信息
        PrepayRequest prepayRequest = this.buildPrepayRequest("", 100, "", "");

        // 与微信端交互
        PrepayResponse res = jsapiService.prepay(prepayRequest);
        String prepayId = "";
//        Assert.notNull(res, "预支付交互响应数据为空!");
        Optional<PrepayResponse> prepayResponseOptional = Optional.ofNullable(res);
        if (!prepayResponseOptional.isPresent()) {
            // 失败业务处理
        } else {
            prepayId = res.getPrepayId();
        }
        
        
        // 返回前端数据
        Map<String, Object> result = new HashMap<>();

        result.put("appId", this.weChatConfig.getAppid());
        result.put("timeStamp",timestamp);
        result.put("nonceStr", nonceStr);
        result.put("signType", signType);
        result.put("package", "prepay_id="+prepayId);
        String signatureStr = Stream.of(this.weChatConfig.getAppid(), String.valueOf(timestamp), nonceStr, "prepay_id=" + prepayId)
                .collect(Collectors.joining("\n", "", "\n"));
        // 
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKeyFromString("");
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(merchantPrivateKey);
        signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        String sign = Base64Utils.encodeToString(signature.sign());
        result.put("sign", sign);
        return result;
    }

    /**
     * 退款申请
     * @param order 可以根据具体业务来设计
     * @return
     * @throws Exception
     */
    public Map<String, Object> refundOrder(PayOrderDto order) throws Exception {
        Map<String, Object> result = new HashMap<>();
        // 业务处理 ...
        
        // 退款申请对接微信
        CreateRequest createRequest = this.buildCreateRequest("", "", "", "", 0);
        RefundService refundService = new RefundService.Builder().config(this.createConfig()).build();
        // 数据交互
        Refund refund = refundService.create(createRequest);
        Optional<Refund> refundOptional = Optional.ofNullable(refund);
        if (!refundOptional.isPresent()) {
            // 通信失败 业务处理 .......
        }
        if("SUCCESS".equals(refund.getStatus().name())) {
            // 成功
        }
        return new HashMap<>();
    }
    
    
    
    private Config createConfig() throws Exception {
        // 生成privateKey
        ClassPathResource resource = new ClassPathResource(this.weChatConfig.getPrivateKeyPath());
        byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream());
       String privateKey = new String(byteArray, StandardCharsets.UTF_8);
       // 构建Config
       return new RSAAutoCertificateConfig.Builder()
               .merchantId(this.weChatConfig.getMchId())
               .privateKey(privateKey)
               .merchantSerialNumber(this.weChatConfig.getMchSerialNo())
               .apiV3Key(this.weChatConfig.getApiV3Key())
               .build();
    }

    /**
     * 创建预支付请求对象
     * @param tradeOrderId 商户订单号,唯一
     * @param total 总金额,int类型,单位分
     * @param openId 用户openId
     * @param payerClientIp 用户IP 
     */
    private PrepayRequest buildPrepayRequest(String tradeOrderId, int total, String openId, String payerClientIp) {
        PrepayRequest request = new PrepayRequest();
        // 
        request.setAppid(this.weChatConfig.getAppid());
        request.setMchid(this.weChatConfig.getMchId());
        request.setDescription("预支付获取-预支付交易会话标识");
        // 商户订单号
        request.setOutTradeNo(tradeOrderId);
        // 交易结束时间
        request.setTimeExpire(DateTimeUtil.getZoneTime());
        // 附加数据
        request.setAttach("");
        // 支付结果回调URL
        request.setNotifyUrl(this.weChatConfig.getNotifyUrl());
        // 订单优惠标记
        request.setGoodsTag("");
        // 发票标识,需要商户后台申请开启
        request.setSupportFapiao(true);

        // 订单金额
        Amount amount = new Amount();
        amount.setTotal(total);
        amount.setCurrency("CNY");
        request.setAmount(amount);

        // 支付人信息
        Payer payer = new Payer();
        payer.setOpenid(openId);
        request.setPayer(payer);

        // 优惠功能
        GoodsDetail temp = new GoodsDetail();
        temp.setMerchantGoodsId("1110");    // 商户侧商品编码
        temp.setQuantity(1); // 数量
        temp.setUnitPrice(total); // 单价

        List<GoodsDetail> goodsDetails = new ArrayList<>();
        goodsDetails.add(temp);
        
        Detail detail = new Detail();
        detail.setGoodsDetail(goodsDetails);
        request.setDetail(detail);

        // 场景信息
        SceneInfo sceneInfo = new SceneInfo();
        sceneInfo.setPayerClientIp("payerClientIp");
        request.setSceneInfo(sceneInfo);

        // 结算信息
        SettleInfo settleInfo = new SettleInfo();
        settleInfo.setProfitSharing(false);
        request.setSettleInfo(settleInfo);
        
        return request;
    }

    /**
     * 创建申请退款对象
     * @param transactionId  原支付交易对应的微信订单号
     * @param outTradeNo 原支付交易对应的商户订单号
     * @param outRefundNo  商户系统内部的退款单号
     * @param reason 退款原因
     * @param total 退款金额,单位为分,只能为整数
     */
    private CreateRequest buildCreateRequest(String transactionId, String outTradeNo, String outRefundNo, 
                                             String reason, long total) {
        // 组装CreateRequest 退款申请类
        CreateRequest createRequest = new CreateRequest();
        // 文档中未说明该字段作用
//        createRequest.setSubMchid("");
        // 原支付交易对应的微信订单号
        createRequest.setTransactionId(transactionId);
        // 原支付交易对应的商户订单号
        createRequest.setOutTradeNo(outTradeNo);
        // 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
        createRequest.setOutRefundNo(outRefundNo);
        // 退款原因
        createRequest.setReason(reason);
        // 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
        // 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。
        createRequest.setNotifyUrl(this.weChatConfig.getRefundNotifyUrl());

        AmountReq amountReq = new AmountReq();
        // 退款金额,单位为分,只能为整数
        amountReq.setRefund(total);
        // 原支付交易的订单总金额,单位为分,只能为整数。
        amountReq.setTotal(total);
        // 退款币种,目前只支持人民币:CNY。
        amountReq.setCurrency("CNY");
        createRequest.setAmount(amountReq);
        return createRequest;
    } 
}

结果通知

package com.yxsong.wechatpay.service;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.yxsong.wechatpay.config.WeChatConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;

import javax.annotation.Resource;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
 * @author xsong.Y
 * @description 结果通知
 */
@Service
@Slf4j
public class NotifyService {

    @Resource
    private WeChatConfig weChatConfig;
    
    /**
     * 支付结果通知
     * @param request
     * @return
     * @throws Exception
     */
    public String payNotify(HttpServletRequest request) throws Exception {
        RequestParam requestParam = this.create(request);
        // 构造一个RSAAutoCertificateConfig
        NotificationConfig config = (NotificationConfig) this.createConfig();
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser(config);
        Transaction transaction = parser.parse(requestParam, Transaction.class);
        Optional<Transaction> optionalTransaction = Optional.ofNullable(transaction);
        if (!optionalTransaction.isPresent()) {
            // 解析失败,业务处理 ...
        }
        // 业务处理....
        
        return "";
    }

    public String refundNotify(HttpServletRequest request) throws Exception {
        RequestParam requestParam = this.create(request);
        // 构造一个RSAAutoCertificateConfig
        NotificationConfig config = (NotificationConfig) this.createConfig();
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser(config);
        RefundNotification notification = parser.parse(requestParam, RefundNotification.class);
        Optional<RefundNotification> refundOptional = Optional.ofNullable(notification);
        if (!refundOptional.isPresent()) {
            // 解析失败,业务处理....
        }
        // 业务处理.....
        return "";
    }
    
    


    private Config createConfig() throws Exception {
        // 生成privateKey
        ClassPathResource resource = new ClassPathResource(this.weChatConfig.getPrivateKeyPath());
        byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream());
        String privateKey = new String(byteArray, StandardCharsets.UTF_8);
        // 构建Config
        return new RSAAutoCertificateConfig.Builder()
                .merchantId(this.weChatConfig.getMchId())
                .privateKey(privateKey)
                .merchantSerialNumber(this.weChatConfig.getMchSerialNo())
                .apiV3Key(this.weChatConfig.getApiV3Key())
                .build();
    }

    private RequestParam create(HttpServletRequest request) throws Exception{
        // 请求头信息
        String wechatSignature = request.getHeader("Wechatpay-Signature");
        // 微信支付平台证书的序列号,验签必须使用序列号对应的微信支付平台证书。
        String wechatPaySerial= request.getHeader("Wechatpay-Serial");
        // 签名中的随机数
        String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
        // 签名中的时间戳。
        String wechatTimestamp = request.getHeader("Wechatpay-Timestamp");
        // 签名类型
        String wechatpaySignatureType = request.getHeader("Wechatpay-Signature-Type");

        // 请求体信息
        ServletInputStream inputStream = request.getInputStream();
        StringBuilder stringBuffer = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s;
        //读取回调请求体
        while ((s = bufferedReader.readLine()) != null) {
            stringBuffer.append(s);
        }


        String body = stringBuffer.toString();

        // 构造 RequestParam
        return new RequestParam.Builder()
                .serialNumber(wechatPaySerial)
                .nonce(wechatpayNonce)
                .signature(wechatSignature)
                .timestamp(wechatTimestamp)
                // 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
                .signType(wechatpaySignatureType)
                .body(body)
                .build();
    }
}