微信支付,小程序支付V3

简介:

微信支付的文档就不吐槽了,记录下微信支付,小程序支付的实现

开发前准备

账号申请,公钥私钥啥的去官网开发指引-小程序支付 | 微信支付商户平台文档中心 (qq.com)

核心代码

下单及拉起支付

微信支付的所有接口调用都是差不多的,关键在于如何处理网络请求和签名,直接上代码

网络请求我用的是okhttp,以小程序支付为例

/**
     * 微信小程序支付下单
     * 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
     * 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
     * 请求方式:POST
     *
     * @param dto          微信小程序订单参数
     * @param merchantInfo 商户号信息
     * @return
     */
    public static ResultData createWeChatAppletPayOrder(WeChatAppletPlaceOrderDto dto, MerchantInfo merchantInfo) {
        WeChatAppletPlaceOrder param = new WeChatAppletPlaceOrder();
        BeanUtils.copyBeanProp(param, dto);
        String reqData = JSON.toJSONString(param);
        log.info("创建微信小程序订单参数:{}", reqData);
        try {
            String token = WxApiV3SignUtils.getToken("POST", HttpUrl.parse(dto.getUrl()),
                    reqData, merchantInfo.getMchId(), merchantInfo.getPrivateKey(), merchantInfo.getSerialNo());
            OkHttpClient okHttpClient = new OkHttpClient();
            RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqData);
            Request request = new Request.Builder()
                    .url(dto.getUrl())
                    .addHeader("Accept", "*/*")
                    .addHeader("Authorization", token)
                    .post(requestBodyJson)
                    .build();
            Call call = okHttpClient.newCall(request);
            Response response = call.execute();
            String resStr = response.body().string();
            log.info("创建微信小程序订单返回结果:状态码:{}--{}", response.code() ,resStr);
            //响应成功
            if (response.isSuccessful()) {
                JSONObject json = JSON.parseObject(resStr);
                WeChatAppletPlaceOrderVO vo = new WeChatAppletPlaceOrderVO();
                vo.setPrepayId("prepay_id=" + json.getString("prepay_id"));
                log.info("prepay_id=" + json.getString("prepay_id"));
                /*小程序调起支付签名信息*/
                WeChatAppletRequestPaymentVO paymentVO = WxApiV3SignUtils.getSign(dto.getAppId(),"prepay_id=" + json.getString("prepay_id"), merchantInfo.getPrivateKey());
                vo.setTimeStamp(String.valueOf(paymentVO.getTimestamp()));
                log.info(String.valueOf(paymentVO.getTimestamp()));
                vo.setNonceStr(paymentVO.getNonceStr());
                log.info(paymentVO.getNonceStr());
                vo.setPaySign(paymentVO.getSignature());
                log.info(paymentVO.getSignature());
                vo.setPrepayId("prepay_id=" + json.getString("prepay_id"));
                vo.setSignType("RSA");
                return ResultData.success("success", vo);
            } else {
                return ResultData.error(response.code(), response.body().string());
            }
        } catch (Exception e) {
            log.error("创建微信小程序订单出错:" + e.getMessage());
            return ResultData.error(e.getMessage());
        }
    }

请求参数和返回参数按照接口文档封装就好了,讲一下token和返给前端拉起支付的签名,其实官方也有相关的代码示例,下面的getToken是所有微信支付相关请求获取token通用的方法,sign方法也是,下单和拉起支付的签名方式是一样的,只是签名的参数不同,

微信小程序支付的签名参数是:请求参数是请求方式method,小程序下单接口的url,时间戳timestamp,随机字符串nonceStr,请求参数body。

微信小程序拉起支付的参数是:小程序appId,时间戳timestamp,随机字符串nonceStr,下单成功返回的prepayId(小程序签名要在prepayId前拼接上"prepay_id="

package com.carlos.pay.common.util;

import com.alibaba.fastjson.JSONObject;
import com.carlos.pay.doamin.vo.WeChatAppletRequestPaymentVO;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import sun.misc.BASE64Decoder;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Random;

/**
 * @author carlos
 */
@Slf4j
public class WxApiV3SignUtils {

    private static final String schema = "WECHATPAY2-SHA256-RSA2048";
    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final Random RANDOM = new SecureRandom();

    /**
     * 签名,生成token
     *
     * @param method
     * @param url
     * @param body
     * @param mchId      商户号
     * @param privateKey 商户证书私钥
     * @param serialNo   商户API证书序列号
     * @return
     * @throws Exception
     */
    public static String getToken(String method, HttpUrl url, String body, String mchId, String privateKey, String serialNo) throws Exception {
        String nonceStr = generateNonceStr();
        log.info("nonceStr: " + nonceStr);
        long timestamp = System.currentTimeMillis() / 1000;
        log.info("timestamp: " + timestamp);
        String message = buildMessage(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"), privateKey);
        log.info("signature: " + signature);
        return schema + " " + "mchid=\"" + mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + serialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    /**
     * 拉起支付签名返给前端拉起支付
     *
     * @param appId
     * @param prepayId
     * @param privateKey
     * @return
     * @throws Exception
     */
    public static WeChatAppletRequestPaymentVO getSign(String appId, String prepayId, String privateKey) throws Exception {
        String nonceStr = generateNonceStr();
        log.info("nonceStr: " + nonceStr);
        long timestamp = System.currentTimeMillis() / 1000;
        log.info("timestamp: " + timestamp);
        String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
        String signature = sign(message.getBytes("utf-8"), privateKey);
        log.info("signature: " + signature);
        return new WeChatAppletRequestPaymentVO(nonceStr, timestamp, signature);
    }

    /**
     * @param message
     * @param privateKey 商户私钥
     * @return
     */
    private static String sign(byte[] message, String privateKey) throws Exception {

        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(privateKey));
        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }

    private static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

    /**
     * String转私钥PrivateKey
     *
     * @param key
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String key) throws Exception {
        byte[] keyBytes;
        keyBytes = (new BASE64Decoder()).decodeBuffer(key);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * String转私钥PrivateKey 从pem文件中获取私钥
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    public static String getPrivateKeyByFile(String fileName) throws Exception {
        ClassPathResource cpr = new ClassPathResource(fileName);
        String content = new String(FileCopyUtils.copyToByteArray(cpr.getInputStream()), "utf-8");
        String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
        return privateKey;
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

    /**
     * V3回调解密
     *
     * @param body
     * @param keyV3
     * @return
     * @throws IOException
     * @throws GeneralSecurityException
     */
    public static String decryptBody(String body, String keyV3) throws IOException, GeneralSecurityException {

        AesUtil aesUtil = new AesUtil(keyV3.getBytes("utf-8"));
        JSONObject object = JSONObject.parseObject(body);
        JSONObject resource = object.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String associatedData = resource.getString("associated_data");
        String nonce = resource.getString("nonce");
        log.info("回调解密参数--ciphertext:" + ciphertext);
        log.info("回调解密参数--associatedData:" + associatedData);
        log.info("回调解密参数--nonce:" + nonce);
        return aesUtil.decryptToString(associatedData.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);

    }
}

对了上面的getPrivateKey和getPrivateKeyByFile都是用来获取PrivateKey的,一个是从String转的,一个是读pem文件的,我的pem文件是放在resources下的。

好了,下单拉起支付就到这了。

回调

回调没什么好说的,返回的是加密的报文,用上文的WxApiV3SignUtils.decryptBody方法解密据OK啦

上代码咯

/**
     * 微信小程序支付回调v3
     *
     * @param request
     * @param response
     * @return
     * @throws IOException
     * @throws GeneralSecurityException
     */
    @Override
    public synchronized Map<String, String> wxMiniPayCallBackV3(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
        log.info("微信小程序支付回调开始");
        String line = request.getReader().readLine();
        request.getReader().close();
        log.info("接收到的报文:" + line);
        String result = WxApiV3SignUtils.decryptBody(line);
        log.info("解密后的报文:" + result);
        //将字符串转换成json
        JSONObject json = JSONObject.parseObject(result);
        Map<String, String> map = new HashMap<>();
        try {
            //TODO 你的业务 boolean flag = 业务处理的成功失败
        } catch (Exception e) {
            //响应微信
            map.put("code", "FAIL");
            map.put("message", "失败");
            log.info("回复微信消息:FAIL");
            return map;
        }
        if (flag) {
            //响应微信
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            log.info("回复微信消息:SUCCESS");
            return map;
        }
        //响应微信
        map.put("code", "FAIL");
        map.put("message", "失败");
        log.info("回复微信消息:FAIL");
        return map;
    }

总结

微信支付相关的东西其实并不难,其实核心就是WxApiV3SignUtils里面的东西,签名,加解密。

在调用微信支付的退款接口https://api.mch.weixin.qq.com/secapi/pay/refund时,您需要传递以下参数: 1. appid:公众账号ID或应用ID。 2. mch_id:商户号。 3. nonce_str:随机字符串,不长于32位。 4. sign_type:签名类型,目前支持HMAC-SHA256和MD5,默认为MD5。 5. sign:签名,详见下面的签名生成算法。 6. transaction_id:微信订单号,与商户订单号二选一。 7. out_trade_no:商户订单号,与微信订单号二选一。 8. out_refund_no:商户退款单号。 9. total_fee:订单金额,单位为分。 10. refund_fee:退款金额,单位为分。 11. refund_fee_type:退款货币种类,可选,默认为CNY。 12. refund_desc:退款原因。 13. refund_account:退款资金来源。 签名方法根据sign_type参数来决定: 1. 若sign_type为MD5,则按照以下方法计算签名: - 将所有发送给微信支付API的数据按照参数名ASCII码从小到大排序(字典序); - 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA; - 在stringA最后拼接上"&key=商户密钥"得到stringSignTemp字符串; - 对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,即得到sign值。 2. 若sign_type为HMAC-SHA256,则按照以下方法计算签名: - 将所有发送给微信支付API的数据按照参数名ASCII码从小到大排序(字典序); - 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA; - 在stringA最后拼接上"&key=商户密钥"得到stringSignTemp字符串; - 对stringSignTemp进行HMAC-SHA256运算,再将得到的字符串进行Base64编码,即得到sign值。 请确保传递的参数和签名方法正确,以确保接口调用成功。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值