微信支付/支付宝支付


微信支付

微信支付-开发者文档 (qq.com)

# 微信支付相关参数
# 商户号
wxpay.mch-id=
# 商户API证书序列号
wxpay.mch-serial-no=

# 商户私钥文件 (放到src同目录下)
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=
# APPID
wxpay.appid=
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=

# APIv2密钥
wxpay.partnerKey: 

下单功能

这一步主要完成,生成订单数据,查询订单数据中是否有code_url,如果没有则需要请求微信服务器的下单接口返回code_url,用于前端展示

为了防止url写错,可以使用menu进行保存url数据

请求url https://api.mch.weixin.qq.com/v3/pay/transactions/native
方式 post

必要参数

参数名 变量名
应用id appid
直连商户号 mchid
商品描述(自己后台生成) description
商户订单号(自己后台生成) out_trade_no
通知地址(公网ip) notify_url
订单金额(map的keyvalue形式) amount 总金额 total 货币类型 currency

返回参数

二维码链接 code_url 两个小时有效期,每次并不是固定值

接下来就是用户扫前端生成的二维码,进行付款,在付款成功之后微信会通过上述发送的notify_url同时是否完成支付

WxPayController.java

@PostMapping("/native/{productId}")
@ApiOperation("调用统一下单Api,返回code_url,前端生成支付二维码")
public R nativePay(@PathVariable Long productId) throws IOException {
    log.info("发送请求"+productId);
    //返回支付二维码和订单号
    Map<String,Object> map= wxPayService.nativePay(productId);
    return R.ok().setData(map);
}

WxPayServiceImpl.java

@Override
public Map<String, Object> nativePay(Long productId) throws IOException {
    log.info("生成订单");
    //        // 创建一个临时的订单对象,放数据
    //        OrderInfo orderInfo=new OrderInfo();
    //        orderInfo.setTitle("Test");
    //        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//生成订单编号
    //        orderInfo.setProductId(productId);
    //        orderInfo.setTotalFee(1); //1分
    //        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

    // 订单数据生成,要保存到数据库中
    OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);

    // 先查询数据库,看二维码是否存在
    // 如果不存在,再调用微信下单api
    // 下边订单超过五分钟会进行关单操作,以保证每次的二维码都是可以使用的
    String codeUrl=orderInfo.getCodeUrl();
    if(codeUrl!=null && !StringUtils.isEmpty(codeUrl)){
        log.info("=====二维码已经存在====");
        Map map=new HashMap();
        map.put("codeUrl",codeUrl);
        map.put("orderNo",orderInfo.getOrderNo());
        return map;
    }
    log.info("开始调用统一下单api");
    HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
    // 开始拼接参数
    Map<Object,Object> paramsMap=new HashMap<>();
    paramsMap.put("appid",wxPayConfig.getAppid());
    paramsMap.put("mchid",wxPayConfig.getMchId());
    paramsMap.put("description",orderInfo.getTitle());// 商品描述,可以自己随意填写
    paramsMap.put("out_trade_no",orderInfo.getOrderNo());// 要唯一
    paramsMap.put("notify_url",
                  wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

    Map<Object,Object> amountMap =new HashMap<>();
    amountMap.put("total",orderInfo.getTotalFee());//订单金额
    amountMap.put("currency","CNY");
    paramsMap.put("amount",amountMap);

    // 参数拼接完成,接下来要进行数据类型转换
    // map--->json
    Gson gson = new Gson();
    String jsonParams = gson.toJson(paramsMap);
    log.info(jsonParams);
    // 把json形式的参数,封装到请求体中
    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){ //处理成功,但是没有返回值
            log.info("sucess");
        }else {
            log.info("失败"+statusCode+"    返回体"+bodyAsString);
            // 微信支付出现问题,可以手动抛出异常
            throw new IOException("请求异常");
        }
        //解析响应结果,得到code_url
        // 把返回结果转换成map类型
        HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
        String code_url= resultMap.get("code_url");

        // 把二维码保存到数据库
        String orderNo=orderInfo.getOrderNo();
        orderInfoService.saveCodeUrl(orderNo,code_url);

        // 把结果给前端,让前端根据code_url生成支付二维码
        Map map=new HashMap();
        map.put("codeUrl",code_url);
        map.put("orderNo",orderInfo.getOrderNo());
        return map;
    }finally {
        response.close();
    }
}

notify_url回调函数

post请求,地址时通过下单功能给微信的notify_url地址,商户要接受并处理该消息,并且给应答

商户端(后台)接收String响应

对响应进行验签操作,如果验签失败返回一个验签失败的响应;验签成功对响应数据进行解密,更改后端数据库中的订单支付状态。对更改状态进行加锁,以防止同时到达两条请求

这里验签时用户WechatPay2ValidatorForRequest,是通过更改微信sdk的WechatPay2ValidatorForResponse

// 验签
// 签名: 就微信的私钥对信息加密
// 加密: 用微信的公钥解密
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest =
    new WechatPay2ValidatorForRequest(verifier, (String) id, body);

取消订单

1、传递本地数据库订单号,访问微信服务器关单接口

post请求

地址: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close

还要添加参数:直连商户号 mchid

2、更改本地订单状态为取消订单

//开始组装数据 使用json和map
Gson gson = new Gson();
Map<String,String> paramsMap=new HashMap<>();
paramsMap.put("mchid",wxPayConfig.getMchId());
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);

查单操作

地址: https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}

方式:get

还需要在拼接 直连商户号mchid

拼接请求url 发送请求,解析响应体

定时任务

/**
 * 开启定时任务,需要在程序主类上添加 @EnableScheduling 开启定时任务
 * 秒  分  时  日  月   周
 * ? 不指定
 * * 每...
 * 日和周不能同时指定,指定其中一个,则另一个设置为?
 * 例如   每三秒执行一次
          每天中午十二点执行一次  0 0 12 * * ?
          2022年  每天上午10:30执行一次  0 30 10 * * ? 2022

 */

代码(这个要注意在主函数上加注释 @EnableScheduling

@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws IOException {
    log.info("定时任务查询超过五分钟没有支付的订单");
    List<OrderInfo> orderInfoList=orderInfoService.getNoPayOrderByDuration(5, PayType.WXPAY.getType());
    for(OrderInfo orderInfo:orderInfoList){
        String orderNo =orderInfo.getOrderNo();
        log.warn("超时订单号======》{}",orderNo);
        // 调用微信查单接口,核实订单状态
        // 这些订单超时了    我们要看时已支付没有接到回调通知,还是确实没有支付
        wxPayService.checkOrderStatus(orderNo);
    }
}

申请退款

地址 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds

方式:post

根据订单号,创建退款单。

调用微信退款api

解析响应体,看看是否发送退款成功;如果发送成功,则更新本地订单状态(退款中),并更新退款单

请求封装参数

//构建参数
Gson gson=new Gson();
HashMap<Object, Object> paramsMap = new HashMap<>();
paramsMap.put("out_trade_no",orderNo);
paramsMap.put("out_refund_no",refundInfo.getRefundNo());
paramsMap.put("reason",reason);
paramsMap.put("notify_url", 					                          wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
HashMap<String,Object> amountMap=new HashMap<>();
amountMap.put("refund",refundInfo.getRefund());
amountMap.put("total",refundInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);

退款结果通知

跟通知支付结果一样,通知退款的notify_url,也是在发送申请退款的时候,也已经指定

获取post请求,解析加密的请求参数

对参数进行验签操作,同时解密,同通知支付结果一样;如果验签失败,返回验签失败

解析解密之后的请求体参数,

对更新订单状态和更新退款单进行加锁操作,以防止同时到达两个请求

if(reentrantLock.tryLock()){
    try {
        String orderStatus = orderInfoService.getOrderStatus(orderNo);
        if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
            return;
        }
        // 更新订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
        refundInfoService.updateRefund(plainText);
    }finally {
        reentrantLock.unlock();
    }
}

查询退款结果

通过刚申请退款时候的退款号进行退款

地址 : https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}

方式 : get

发送get请求,解析响应体

获取账单

这一步获取的url不能直接使用,需要通过下一步下载账单来获取账单数据

调用申请交易账单和申请资金账单的url差不多,所以方式也一样,所以将两者写到一块了

@Override
public String queryBill(String billData, String type) throws IOException {
    log.info("申请账单接口调用===》{}",billData);
    String url="";
    if ("tradebill".equals(type)){
        url=WxApiType.TRADE_BILLS.getType();
    }else if ("fundflowbill".equals(type)){
        url=WxApiType.FUND_FLOW_BILLS.getType();
    }else {
        throw new RuntimeException("不支持的账单类型");
    }
    url=wxPayConfig.getDomain().concat(url).concat("?bill_date=".concat(billData));

    HttpGet httpGet   =new HttpGet(url);
    httpGet.setHeader("Accept","application/json");

    CloseableHttpResponse response = wxPayClient.execute(httpGet);
    try{
        String bodyAsString =EntityUtils.toString(response.getEntity());
        int statusCode=response.getStatusLine().getStatusCode();
        if (statusCode==200){
            log.info("申请账单成功,账单返回值===》{}",bodyAsString);
        }else if (statusCode==204){
            log.info("申请账单成功");
        }else{
            throw  new RuntimeException("申请账单异常"+statusCode+",返回信息===》"+bodyAsString);
        }
        Gson gson=new Gson();
        HashMap<String,String> hashMap = gson.fromJson(bodyAsString, HashMap.class);
        return hashMap.get("download_url");
    }finally {
        response.close();
    }
}

下载账单

通过上边的获取账单得到的url,对url发送get请求

判断响应状态

返回响应体

>@Override
>public String downloadBill(String billDate, String type) throws IOException {
   log.info("下载账单接口调用,{},{}",billDate,type);
   String url=this.queryBill(billDate,type);
   HttpGet httpGet=new HttpGet(url);
   httpGet.setHeader("Accept","application/json");

   CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
   try{
       String bodyAsString =EntityUtils.toString(response.getEntity());
       int statusCode=response.getStatusLine().getStatusCode();
       if (statusCode==200){
           log.info("下载账单成功,账单返回值===》{}",bodyAsString);
       }else if (statusCode==204){
           log.info("下载账单成功");
       }else{
           throw  new RuntimeException("下载账单异常"+statusCode+",返回信息===》"+bodyAsString);
       }
       return bodyAsString;
   }finally {
       response.close();
   }
>}

解密函数(在支付回调函数和申请退款回调函数中使用了,解密微信返回的响应体)

public String decryptFromREsource(Map<String, Object> bodyMap) throws GeneralSecurityException {
    Map<String ,String> resourceMap =(Map<String, String>) bodyMap.get("resource");
    String ciphertext=resourceMap.get("ciphertext");
    String nonce = resourceMap.get("nonce");
    String associatedData = resourceMap.get("associated_data");
    log.info("密文开始解密");
    log .info("密文 ===>{}",ciphertext);
    // 得到自己的密钥
    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);
    log.info("明文=====> {}",plainText);
    return plainText;
}

支付宝支付

统一下单并支付页面接口的调用

根据货品id生成订单号,并存在本地数据中,接下来就是拼接参数,发送请求,解析返回体

这个当时犯了个问题,在网页沙箱环境配置了一个应用网关还有授权回调地址,导致一直没有回调函数,最后查看支付宝开发文档,可以看出 ;通常这两个是不用配置的,一般会在需要机型回调函数的的请求头进行绑定

应用网关:用于接受支付宝的异步通知

image-20230715212958894

授权回调地址:是用于在网页端支付成功之后的本地给他一个成功地址

授权回调地址

//调用支付宝接口
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(config.getProperty("alipay.notify-url"));
request.setReturnUrl(config.getProperty("alipay.return-url"));
//组装当前业务方法的请求参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderInfo.getOrderNo());
BigDecimal totalFee = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
bizContent.put("total_amount", totalFee);
bizContent.put("subject", "测试商品");
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");

request.setBizContent(bizContent.toString());
//执行请求,调用支付接口
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if(response.isSuccess()){
    System.out.println("调用成功");
    log.info("调用成功,返回结果 "+response.getBody());
    return response.getBody();
} else {
    System.out.println("调用失败");
    log.info("调用失败,返回码 "+response.getCode()+" 返回描述 "+response.getMsg());
    throw new RuntimeException("创建支付交易失败");
}

支付通知

对异步通知进行验签操作,如果验签失败,则输出日志,并返回failure

验签成功之后,按照异步通知中的out_trade_no的支付金额和数据库中的支付金额进行对比如果不相同则金额校验失败,返回failure

校验商家seller_id 校验商家app_id

接下来就是校验支付状态,只有TRADE_SUCCESS或者TRADE_FINISHED支付宝才会认定买家支付成功

所有校验通过之后更新订单状态

//异步通知的验签
// Map<String, String> paramsMap = ... //将异步通知中收到的所有参数都存放到map中
boolean signVerified = AlipaySignature.rsaCheckV1(
        params, config.getProperty("alipay.alipay-public-key"),
        AlipayConstants.CHARSET_UTF8,
        AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名
if(!signVerified){
    // TODO 验签失败则记录异常日志,并在response中返回failure.
    log.error("支付成功异步通知验签失败!");
    return result;
}

取消订单

调用支付宝关单接口,成功之后调用本地接口更新订单状态

AlipayTradeCloseRequest request=new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeCloseResponse response=alipayClient.execute(request);

查询订单接口

调用支付宝主动查单接口

AlipayTradeQueryRequest request=new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);

定时任务,每隔三十秒,查询未支付的支付宝订单

@Scheduled(cron = "*/30 * * * * ?")
public void orderConfirm() throws IOException{
  log.info("支付宝,超过五分钟未支付的订单。。。。");
    List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5, PayType.ALIPAY.getType());
    for (OrderInfo orderInfo: orderInfoList){
        String orderNo=orderInfo.getOrderNo();
        log.info("超时未支付的订单=====》{}",orderNo);

        //核实订单状态,调用支付宝查单接口
        aliPayService.checkOrderStatus(orderNo);
    }
}

申请退款

//创建退款单
RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
//调用统一收单交易退款接口
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderNo);
BigDecimal refund=new BigDecimal(refundInfo.getRefund().toString())
        .divide(new BigDecimal("100"));
bizContent.put("refund_amount", refund);
bizContent.put("refund_reason", reason);
request.setBizContent(bizContent.toString());
AlipayTradeRefundResponse response = alipayClient.execute(request);

查询退款

log.info("查询退款接口调用=======》{}",orderNo);
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no",orderNo);
bizContent.put("out_request_no",orderNo);
request.setBizContent(bizContent.toString());
AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);

获取账单

AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("bill_type",type);
bizContent.put("bill_date",billDate);
request.setBizContent(bizContent.toString());
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()){
    log.info("申请账单调用成功,返回结果===》{}",response.getBody());
    //获取账单地址
    Gson gson=new Gson();
    HashMap<String ,LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
    LinkedTreeMap downloadurlQueryResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
    String downloadUrl= (String) downloadurlQueryResponse.get("bill_download_url");
    return downloadUrl;
}else{
    log.info("调用失败,返回码===》"+response.getCode()+",返回描述"+response.getMsg());
    throw new RuntimeException("申请账单失败");
}


文章作者: 毛豆不逗比
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 毛豆不逗比 !
  目录
{% include '_third-party/exturl.swig' %} {% include '_third-party/bookmark.swig' %} {% include '_third-party/copy-code.swig' %} + {% include '_custom/custom.swig' %}