springboot微信支付

微信支付官网api:https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/introduction.html

微信支付在Java中的应用已经非常流行,今天让我们一起来写一篇实战篇的微信支付;

1.开发参数的准备:https://pay.weixin.qq.com/docs/merchant/development/development-preparation/download-configure-merchant-certificates.html

2.获取到商户证书以后,把压缩包里的apiclient_key 文件放在项目的根目录下,方便使用和读取,如下:

image.png
image.png

3.创建加载商户私钥、加载平台证书、初始化httpClient的通用方法。
这里我自己写了一个配置文件
config配置文件会在项目启动时被加载(使用签名验证器在每次使用微信支付的接口时会自动验证身份)
3.1wxpay.propertie配置文件

#商户号
wxpay.mch-id=替换为你自己的商户号
#API
wxpay.mch-serial-no=替换为你自己的商户号序列号
#商户私钥文件,放在工程目录下
wxpay.private-key-path=替换为你自己的apiclient_key.pem
#APIv3密钥
wxpay.api-v3-key=替换为你自己的APIv3密钥
#APPID
wxpay.appid=替换为你自己的appid
#微信服务器地
wxpay.domain=替换为你自己的微信服务器地
# 接收结果通知地址
wxpay.notify-domain=替换为你自己的接收结果通知地址

3.2config配置文件

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.annotation.Resource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;

@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {

    @Resource
    private WxPayConfig wxPayConfig;

    // 商户号
    private String mchId;

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

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

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

    // 接收结果通知地址
    private String notifyDomain;


    /**
     * 获取商户的私钥文件
     *
     * @param filename
     * @return
     */
    public PrivateKey getPrivateKey(String filename) {

        try {
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器
     *
     * @return
     */
    @Bean
    public verifier  getVerifier() throws HttpCodeException, GeneralSecurityException, IOException, NotFoundException {
        log.info("获取签名验证器");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));

        //若有多个商户号,可继续调用putMerchant添加商户信息
        certificatesManager.putMerchant(wxPayConfig.getMchId(), wechatPay2CredentialsDc, wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));

        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(mchId);
        return verifier ;
    }


    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClientverifier  verifier){
        log.info("获取httpClient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier ));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        return httpClient;
    }

    /**
     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient() {

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //用于构造HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(mchId, mchSerialNo, privateKey)
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        log.info("== getWxPayNoSignClient END ==");

        return httpClient;
    }

}

4.调用支付接口,发起支付(此时需要对数据进行签名,签名工具类在支付方法下;具体业务逻辑和参数自己添加)

    @PostMapping("/pay")
    public HashMap<String, Object> outpatientPlaceOrderNew() throws Exception {

        // 开始支付
        long timestamp = new Date().getTime();
        log.info("订单开始支付,调用统一下单API");
        //调用统一下单API
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", "支付");
        paramsMap.put("out_trade_no", wjOrder.getId().toString());
        paramsMap.put("notify_url", "https://www.wjgzzdyy.mil.cn" + "/api/wxPay/notice");
        Map amountMap = new HashMap();
        amountMap.put("total", wjOrder.getAmount().multiply(new BigDecimal(100)).intValue());
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
        Map payer = new HashMap();
        payer.put("openid", wjOrder.getOpenId());
        paramsMap.put("payer", payer);
        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}" + jsonParams);

        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        //完成签名并执行请求
        CloseableHttpResponse response = dcWxPayClient.execute(httpPost);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            log.info("响应结果 = " + bodyAsString);
            if (statusCode == 200) {
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功,无返回Body
                log.info("成功");
            } else {
            log.info("失败");
            }
            //响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            String prepayId = resultMap.get("prepay_id");
            //组装前端拉起支付需要的参数
            String nonceStr = signUtil.makeString();
            String signType = "RSA";
            String packageId = "prepay_id=" + prepayId;
            String source = appid + "\n";
            source += timestamp + "\n";
            source += nonceStr + "\n";
            source += packageId + "\n";
            //签名
            String paySign = signUtil.signBySHA256WithRSAOrder(source, "UTF-8");
            HashMap<String, Object> res = new HashMap<>();
            res.put("timestamp", timestamp);
            res.put("nonceStr", nonceStr);
            res.put("package", packageId);
            res.put("signType", signType);
            res.put("paySign", paySign);
      
        } finally {
            response.close();
        }
        return res;
    }    

5.签名工具类SignUtil


import com.colorful.hospital.config.WxPayConfig;
import com.colorful.hospital.config.WxPayDcConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.security.PrivateKey;
import java.security.Signature;

@Component
public class SignUtil {
    @Autowired
    WxPayConfig wxPayConfig;

    /**
     * SHA256withRSA签名
     * @author xpl
     * @param content
     * @param charset
     * @return
     */
    public String signBySHA256WithRSA(String content, String charset){
        try {
            PrivateKey privateKey = wxPayConfig.getPrivateKey("apiclient_key.pem");
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(privateKey);
            signature.update(content.getBytes(charset));
            return org.apache.commons.codec.binary.Base64.encodeBase64String(signature.sign());
        } catch (Exception e) {
            //签名失败
            return null;
        }
    }

    public String makeString() {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random1 = new Random();
        //指定字符串长度,拼接字符并toString
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 16; i++) {
            //获取指定长度的字符串中任意一个字符的索引值int number=random1.nextInt(str.length());
            //根据索引值获取对应的字符
            char charAt = str.charAt(i);
            sb.append(charAt);
        }
        return sb.toString();
    }
}

6.微信支付通知(此处没有做签名验证)

     @ApiOperation("支付通知")
    @PostMapping("/notice")
    public Map wxJsApiCallback (@RequestBody Map body, HttpServletRequest request){
        log.info("进入微信支付通知");
        Map<String, Object> result = new HashMap();
        //1:获取微信支付回调的获取签名信息
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String nonce = request.getHeader("Wechatpay-Nonce");
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 2: 开始解析报文体
            String data = objectMapper.writeValueAsString(body);
            String message = timestamp + "\n" + nonce + "\n" + data + "\n";
            //3:获取应答签名
            String sign = request.getHeader("Wechatpay-Signature");
            //4:获取平台对应的证书
            String serialNo = request.getHeader("Wechatpay-Serial");
            Map<String, String> resource = (Map) body.get("resource");
            // 5:回调报文解密
            AesUtil aesUtil = new AesUtil(privateApiV3Key.getBytes());
            //解密后json字符串
            String decryptToString = aesUtil.decryptToString(
                    resource.get("associated_data").getBytes(),
                    resource.get("nonce").getBytes(),
                    resource.get("ciphertext"));
            //6:获取微信支付返回的信息
            com.alibaba.fastjson.JSONObject jsonData = com.alibaba.fastjson.JSONObject.parseObject(decryptToString);
            log.info("wxJsApiCallback responseJson:" + jsonData.toJSONString());
            //7: 支付状态的判断 如果是success就代表支付成功
            if ("SUCCESS".equals(jsonData.get("trade_state"))) {
                // 8:获取支付的交易单号,流水号,和附属参数
                String out_trade_no = jsonData.get("out_trade_no").toString();
                String transaction_id = jsonData.get("transaction_id").toString();
                String success_time = jsonData.get("success_time").toString();
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
                Date parse = df.parse(success_time);
                com.alibaba.fastjson.JSONObject amount = jsonData.getJSONObject("amount");// 订单金额信息
                int payMoney = amount.getIntValue("payer_total"); //实际支付金额
                // 成功处理,加自己的的业务
            
            } else {
                String out_trade_no = jsonData.get("out_trade_no").toString();
                // 失败处理,加自己的的业务
            }
            result.put("code", "SUCCESS");
            result.put("message", "成功");
        } catch (Exception e) {
            result.put("code", "FAIL");
            result.put("message", "系统错误");
            e.printStackTrace();
        }
        return result;
    }

7.退款

 @Resource
    private WxPayConfig wxPayConfig;
    @PostMapping("/refund")
    public AjaxResult refund1(@RequestBody Order Order) throws Exception {
        AjaxResult ajaxResult = new AjaxResult();
        //数据库自行计算金额
        BigDecimal refundAmount = new BigDecimal(0);
        //商户退款单号
            String outRefundNo = "R" + SnowFlake.nextId().toString();
            Long orderId = order.getId();
            // 退款金额
            int refund = refundAmount.multiply(new BigDecimal(100)).intValue();
            log.info("调用退款API");
            //调用统一下单API
            HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds");
            // 请求Body参数
            Gson gson = new Gson();
            Map<String, Object> paramsMap = new HashMap<>();
            paramsMap.put("out_trade_no", orderId.toString());
            paramsMap.put("out_refund_no", outRefundNo);
            Map amountMap = new HashMap<>();
            amountMap.put("total", order.getAmount().multiply(new BigDecimal(100)).intValue());
            amountMap.put("refund", refund);
            amountMap.put("currency", "CNY");
            paramsMap.put("amount", amountMap);
            // 将参数转换成json字符串
            String jsonParams = gson.toJson(paramsMap);
            log.info("请求参数 ===> {} " + jsonParams);

            StringEntity entity = new StringEntity(jsonParams, "utf-8");
            entity.setContentType("application/json");
            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();
                if (statusCode == 200) {
                    log.info("成功 退款返回结果 = " + bodyAsString);
                   
                } else if (statusCode == 204) {
                    // 处理成功,无返回body
                    log.info("成功");
                } else {
                    throw new RuntimeException(" 退款异常,响应码 = " + statusCode + ", 退款返回结果 = " + bodyAsString);
                }
                ajaxResult = AjaxResult.success();

            } finally {
                response.close();
            }
        return ajaxResult;
    }

只介绍了一些基础用法,如有其他更复杂的需求,请参考官网(开头)
这里只是我个人理解的用法,如有理解不当的地方请指出;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容