支付业务学习

基本的支付系统有哪些组成-极客

支付系统结构图:

支付系统结构图

支付具体流程:

image-20230605010221785

聚合支付流程图:

image-20230605010043784

支付系统的组成:

支付系统的组成

微信支付的7种支付流程

微信支付商户中心文档中心

JSAPI支付

2_2_4-1

APP支付

3_9

H5支付

7_2

Native支付

5_0

小程序支付

6_2

合单支付

chapter2_3_6

付款码支付

微信支付签名验签流程

用户发起请求支付的过程中, 会有签名和验签的过程:

签名过程

  1. 构造签名串

                          HTTP请求方法\n
                          URL\n
                          请求时间戳\n
                          请求随机串\n
                          请求报文主体\n
    
  2. 计算签名值

     绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
    
    
     $ echo -n -e \
     "GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \
     | openssl dgst -sha256 -sign apiclient_key.pem \
     | openssl base64 -A
     uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
    
  3. 设置HTTP头

     微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。
    
    下面我们使用命令行演示如何生成签名。
    Authorization: 认证类型 签名信息
    
    具体组成为:
    
    1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
    
    2.签名信息
    
        发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
        商户API证书序列号serial_no,用于声明所使用的证书
        请求随机串nonce_str
        时间戳timestamp
        签名值signature
    
        注:以上五项签名信息,无顺序要求。
    
    Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行)
    
    Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"
    

验签

验签有两个时候, 一种是我们发送请求支付生成预支付单, 对方同步应答的时候, 验证微信的同步应答, 一种是支付结果时候对方的异步请求, 同步应答是建议验签, 而异步应答是必须要验签的, 验签的时候使用微信支付平台的公钥验签, 公钥只能从平台证书中获取, 平台证书是通过API获取的

微信支付Java接入

前提

是为了获取:标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL

  • 1.获取商户号

    微信商户平台:https://pay.weixin.qq.com/ 步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号

  • 2.获取AppID

    微信公众平台:https://mp.weixin.qq.com/ 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

  • 3.申请商户证书

    步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥

  • 4.获取微信的证书

    可以预先下载,也可以通过编程的方式获取。

  • 5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)

    步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

1.引依赖

<!--wechatpay-sdk-->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.5</version>
</dependency>

2.写配置类

@Configuration
public class WechatpayConfig{
    @Value("${wechat-pay.merchant-id}")
    private String merchantId;
    @Value("${wechat-pay.merchant-serial-number}")
    private String merchantSerialNumber;
    @Value("${wechat-pay.private-key}")
    private String privateKey;
    @Value("${wechat-pay.api-v3-key}")
    private String apiV3Key;
    /**
     * 给容器中加入WechatPay的HttpClient,虽然它是WechatPay的,
     * 但可以用它给任何外部发请求,因为它只对发给WechatPay的请求做处理而不对发给别的的请求做处理.
     */
    @Bean
    public HttpClient httpClient(){
        //私钥
        PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
        //微信证书校验器
        Verifier verifier=null;
        try{
            //获取证书管理器实例
            CertificatesManager certificatesManager=CertificatesManager.getInstance();
            //向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
            certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
            //从证书管理器中获取verifier
            verifier=certificatesManager.getVerifier(merchantId);
        }
        catch(Exception e){
            new RuntimeException("微信证书校验器配置失败");
        }
        WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
                                                                     .withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey)
                                                                     .withValidator(new WechatPay2Validator(verifier));
        CloseableHttpClient httpClient=builder.build();
        return httpClient;
    }
    /**
     * 和上面相比只是不需要验证签名了
     * @return
     */
    @Bean
    public HttpClient httpClientWithNoSign(){
        //私钥
        PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
        //微信证书校验器
        Verifier verifier=null;
        try{
            //获取证书管理器实例
            CertificatesManager certificatesManager=CertificatesManager.getInstance();
            //向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
            certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
            //从证书管理器中获取verifier
            verifier=certificatesManager.getVerifier(merchantId);
        }
        catch(Exception e){
            new RuntimeException("微信证书校验器配置失败");
        }
        WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
                                                                     .withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey)
                                                                     .withValidator(response->true);
        CloseableHttpClient httpClient=builder.build();
        return httpClient;
    }
}

3.写配置

wechat-pay:
  #接下来两个用来标识用户
  #商户id
  merchant-id: xxxxxxxxxxx
  #公众号appid(和商户id绑定过)
  appid: xxxxxxxxxxx
  #接下来两个用来确保SSL(内容未作任何加密,只做了签名.)
  #商户证书序列号
  merchant-serial-number: xxxxxxxxxxx
  #商户私钥
  private-key: xxxxxxxxxxx
  #APIv3密钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
  api-v3-key: xxxxxxxxxxx
  #接下来两个是相关地址
  #微信服务器地址
  domain: https://api.mch.weixin.qq.com
  #接收结果通知地址
  notify-domain: xxxxxxxxxxx

4.使用

流程图

img

以下涉及的共有的内容

public class WechatPayConstant{
    public static final String CANCEL_PAY_URL="/v3/pay/transactions/out-trade-no/%s/close";
    public static final String CREATE_PAY_URL="/v3/pay/transactions/native";
    public static final String QUERY_PAY_URL="/v3/pay/transactions/out-trade-no/%s?mchid=%s";
    public static final String CREATE_REFUND_URL="/v3/refund/domestic/refunds";
    public static final String QUERY_REFUND_URL="/v3/refund/domestic/refunds/%s";
    public static final String TRADE_BILL_URL="/v3/bill/tradebill?bill_date=%s&bill_type=%s";
    public static final String FLOW_BILL_URL="/v3/bill/fundflowbill?bill_date=%s";
    public static final String TRADE_STATE_SUCCESS="SUCCESS";
    public static final String REFUND_STATE_SUCCESS="SUCCESS";
}
@Value("${wechat-pay.merchant-id}")
private String merchantId;
@Value("${wechat-pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wechat-pay.api-v3-key}")
private String apiV3Key;
@Value("${wechat-pay.domain}")
private String domain;
@Value("${wechat-pay.appid}")
private String appId;
@Value("${wechat-pay.notify-url}")
private String notifyUrl;
@Autowired
private HttpClient httpClient;
@Autowired
private HttpClient httpClientWithNoSign;

支付

Native支付流程图

img

创建支付

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

private String createPay(OrderInfo orderInfo) throws Exception{
    //请求构造
    HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_PAY_URL);
    //请求体
    //构造数据
    HashMap<String,Object> reqData=new HashMap<>();
    reqData.put("appid",appId);
    reqData.put("mchid",merchantId);
    reqData.put("description",orderInfo.getTitle());
    reqData.put("out_trade_no",orderInfo.getOrderNo());
    reqData.put("notify_url",notifyUrl+"/pay/order/order-signal");
    HashMap<String,Integer> amount=new HashMap<>();
    //单位是分
    amount.put("total",orderInfo.getTotalFee());
    reqData.put("amount",amount);
    String jsonReqData=new Gson().toJson(reqData);
    StringEntity entity=new StringEntity(jsonReqData,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    //请求头
    httpPost.setHeader("Accept","application/json");
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
    Map<String,String> dataMap=null;
    try{
        int statusCode=response.getStatusLine()
                               .getStatusCode();
        //成功
        if(statusCode==200){
            String body=EntityUtils.toString(response.getEntity());
            dataMap=new Gson().fromJson(body,HashMap.class);
        }
        //失败
        else{
            if(statusCode!=204){
                String body=EntityUtils.toString(response.getEntity());
                log.error(body);
                return null;
            }
        }
    }
    finally{
        response.close();
    }
    //返回二维码的地址
    return dataMap.get("code_url");
}
支付通知

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

@PostMapping("/pay-signal")
public HashMap<String,String> paySignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
    log.debug("收到微信回调");
    try{
        //TODO:验签
        //用密文解密出明文
        Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
        String ciphertext=resource.get("ciphertext");
        String associatedData=resource.get("associated_data");
        String nonce=resource.get("nonce");
        String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
        //转换
        HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
        //从数据库中查出对应的订单
        QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("order_no",data.get("out_trade_no"));
        OrderInfo orderInfo=orderInfoService.getOne(queryWrapper);
        synchronized(this){
            if(orderInfo.getOrderStatus()
               .equals(OrderInfo.NOT_PAIED)){
                //将订单设置为已支付状态
                orderInfo.setOrderStatus(OrderInfo.PAIED);
                //更新订单状态
                orderInfoService.updateById(orderInfo);
                //添加支付记录
                PaymentInfo paymentInfo=new PaymentInfo();
                paymentInfo.setOrderNo(orderInfo.getOrderNo());
                paymentInfo.setCreateTime(new Date());
                paymentInfo.setUpdateTime(new Date());
                paymentInfo.setPayerTotal(orderInfo.getTotalFee());
                paymentInfo.setPaymentType(PaymentInfo.WECHAT_PAY);
                //微信支付中的支付编号
                paymentInfo.setTransactionId((String)data.get("transaction_id"));
                //交易类型(扫码 刷脸等等)
                paymentInfo.setTradeType((String)data.get("trade_type"));
                paymentInfo.setTradeState((String)data.get("trade_state"));
                //存放全部数据(json)以备不时之需
                paymentInfo.setContent(plainText);
                paymentInfoService.save(paymentInfo);
                log.info("订单{}的支付记录添加成功,支付记录id为{}.",orderInfo.getOrderNo(),paymentInfo.getId());
            }
            else{
                log.debug("订单{}状态为{},回调处理退出.",orderInfo.getOrderNo(),OrderInfo.PAIED);
            }
        }
        return null;
    }
    catch(Exception e){
        log.error(e.getMessage());
        response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
        HashMap<String,String> map=new HashMap<>();
        map.put("code","FAIL");
        map.put("message","支付失败");
        return map;
    }
}
查询支付

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml

private Map<String,Object> queryPay(String orderNo) throws Exception{
    //请求构造
    HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_PAY_URL,orderNo,merchantId));
    //请求头
    httpGet.setHeader("Accept","application/json");
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
    Map<String,Object> dataMap=null;
    try{
        int statusCode=response.getStatusLine()
            .getStatusCode();
        String body=EntityUtils.toString(response.getEntity());
        //成功
        if(statusCode<400){
            dataMap=new Gson().fromJson(body,HashMap.class);
            log.info("查询订单支付{}成功",orderNo);
        }
        //失败
        else{
            log.error("查询订单支付{}失败,返回数据为{}.",orderNo,body);
            throw new IOException("订单查询失败");
        }
    }
    finally{
        response.close();
    }
    return dataMap;
}
取消支付

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml

private boolean cancelPay(String orderNo){
    //请求构造
    HttpPost httpPost=new HttpPost(String.format(domain+WechatPayConstant.CANCEL_PAY_URL,orderNo));
    //请求体
    //构造数据
    HashMap<String,Object> reqData=new HashMap<>();
    reqData.put("mchid",merchantId);
    String jsonReqData=new Gson().toJson(reqData);
    StringEntity entity=new StringEntity(jsonReqData,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    //请求头
    httpPost.setHeader("Accept","application/json");
    CloseableHttpResponse response=null;
    try{
        //完成签名并执行请求
        response=(CloseableHttpResponse)httpClient.execute(httpPost);
        Map<String,String> dataMap=null;
        int statusCode=response.getStatusLine()
            .getStatusCode();
        //成功
        if(statusCode==204){
            log.info("取消订单{}成功",orderNo);
            return true;
        }
        //失败
        else{
            log.error("取消订单{}失败",orderNo);
            return false;
        }
    }
    catch(Exception e){
        log.error("取消订单{}失败",orderNo);
        return false;
    }
    finally{
        try{
            response.close();
        }
        catch(IOException e){
            log.error("关闭response失败");
        }
    }
}

退款

创建退款

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

private String createRefund(RefundInfo refundInfo) throws Exception{
    //请求构造
    HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_REFUND_URL);
    //请求体
    //构造数据
    HashMap<String,Object> reqData=new HashMap<>();
    reqData.put("out_trade_no",refundInfo.getOrderNo());//订单编号
    reqData.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
    reqData.put("reason",refundInfo.getReason());//退款原因
    reqData.put("notify_url",notifyUrl+"/pay/order/refund-signal");//退款通知地址
    HashMap<String,Object> amount=new HashMap<>();
    amount.put("refund",refundInfo.getRefund());//退款金额
    amount.put("total",refundInfo.getTotalFee());//原订单金额
    amount.put("currency","CNY");//币种
    reqData.put("amount",amount);
    //将参数转换成json字符串
    String jsonData=new Gson().toJson(reqData);
    log.info("请求参数 ===> {}"+jsonData);
    StringEntity entity=new StringEntity(jsonData,"utf-8");
    httpPost.setEntity(entity);//将请求报文放入请求对象
    //请求头
    httpPost.setHeader("content-type","application/json");
    httpPost.setHeader("Accept","application/json");//设置响应报文格式
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
    try{
        //解析响应结果
        String bodyAsString=EntityUtils.toString(response.getEntity());
        int statusCode=response.getStatusLine()
                               .getStatusCode();
        if(statusCode==200){
            log.info("成功, 退款返回结果 = "+bodyAsString);
            return bodyAsString;
        }
        else{
            if(statusCode!=204){
                log.warn("退款异常:"+bodyAsString);
            }
            return null;
        }
    }
    finally{
        response.close();
    }
}
退款通知

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml

@PostMapping("/refund-signal")
public HashMap<String,String> refundSignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
    log.debug("收到微信回调");
    try{
        Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
        String ciphertext=resource.get("ciphertext");
        String associatedData=resource.get("associated_data");
        String nonce=resource.get("nonce");
        //解密出明文
        String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
        //转换
        HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
        //从数据库中查出对应的退款信息
        QueryWrapper<RefundInfo> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("refund_no",data.get("out_refund_no"));
        RefundInfo refundInfo=refundInfoService.getOne(queryWrapper);
        synchronized(this){
            if(RefundInfo.REFUND_PROCESSING.equals(refundInfo.getRefundStatus())){
                //将退款状态设置为退款成功
                refundInfo.setRefundStatus(RefundInfo.REFUND_SUCCESS);
                //存放全部数据(json)以备不时之需
                refundInfo.setContentNotify(plainText);
                //更新退款
                refundInfoService.updateById(refundInfo);
                //更新订单的状态
                String orderNo=refundInfo.getOrderNo();
                QueryWrapper<OrderInfo> orderInfoQueryWrapper=new QueryWrapper<>();
                orderInfoQueryWrapper.eq("order_no",orderNo);
                OrderInfo orderInfoToUpdate=new OrderInfo();
                orderInfoToUpdate.setOrderStatus(OrderInfo.REFUNDED);
                orderInfoService.update(orderInfoToUpdate,orderInfoQueryWrapper);
                log.debug("退款成功,退款单为{},对应的订单为{}.",refundInfo.getRefundNo(),refundInfo.getOrderNo());
            }
            else{
                log.debug("退款单{}状态为{},回调处理退出.",refundInfo.getRefundNo(),refundInfo.getRefundStatus());
            }
        }
        HashMap<String,String> map=new HashMap<>();
        map.put("code","SUCCESS");
        map.put("message","成功");
        return map;
    }
    catch(Exception e){
        log.error(e.getMessage());
        response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
        HashMap<String,String> map=new HashMap<>();
        map.put("code","FAIL");
        map.put("message","失败");
        return map;
    }
}
查询退款

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

private Map<String,Object> queryRefund(String refundNo) throws Exception{
    //请求构造
    HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_REFUND_URL,refundNo));
    //请求头
    httpGet.setHeader("Accept","application/json");
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
    Map<String,Object> dataMap=null;
    try{
        int statusCode=response.getStatusLine()
            .getStatusCode();
        String body=EntityUtils.toString(response.getEntity());
        //成功
        if(statusCode<400){
            dataMap=new Gson().fromJson(body,HashMap.class);
            log.info("查询退款{}成功",refundNo);
        }
        //失败
        else{
            log.warn("查询退款{}失败,返回数据为{}.",refundNo,body);
        }
    }
    finally{
        response.close();
    }
    return dataMap;
}

账单

查询交易账单(注重交易双方)下载URL

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_6.shtml

/**
 * @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
 * @param billType ALL(支付成功且未退款+支付成功且退款) SUCCESS(支付成功且未退款) REFUND(支付成功且退款)
 * @return 账单下载url(30s后则失效)
 */
private String queryTradeBillDownloadUrl(String billDate,String billType)throws Exception{
    //请求构造
    HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.TRADE_BILL_URL,billDate,billType));
    //请求头
    httpGet.setHeader("Accept","application/json");
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
    String downloadUrl=null;
    try{
        int statusCode=response.getStatusLine()
                               .getStatusCode();
        String body=EntityUtils.toString(response.getEntity());
        //成功
        if(statusCode<400){
            downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
                                          .get("download_url");
            return downloadUrl;
        }
        //失败
        else{
            log.warn("查询downloadUrl失败,返回数据为{}.",body);
            return null;
        }
    }
    catch(Exception e){
        log.warn("查询downloadUrl失败");
        return null;
    }
    finally{
        response.close();
    }
}
查询流水账单(只注重商户)下载URL

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml

/**
 * @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
 * @return 账单下载url(30s后则失效)
 */
private String queryFlowBillDownloadUrl(String billDate) throws Exception{
    //请求构造
    HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.FLOW_BILL_URL,billDate));
    //请求头
    httpGet.setHeader("Accept","application/json");
    //完成签名并执行请求
    CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
    String downloadUrl=null;
    try{
        int statusCode=response.getStatusLine()
                               .getStatusCode();
        String body=EntityUtils.toString(response.getEntity());
        //成功
        if(statusCode<400){
            downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
                                          .get("download_url");
            return downloadUrl;
        }
        //失败
        else{
            log.warn("查询downloadUrl失败,返回数据为{}.",body);
            return null;
        }
    }
    catch(Exception e){
        log.warn("查询downloadUrl失败");
        return null;
    }
    finally{
        response.close();
    }
}
获取账单(包括交易/流水)数据

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml

/**
 * 调用微信支付的接口只返回数据,然后我们把数据返回给前端,前端将其转化为excel.
 * @param downloadUrl 交易/流水账单的下载URL(即上面两个方法的返回值)
 */
private String downloadBill(String downloadUrl)throws Exception{
    //请求构造
    HttpGet httpGet=new HttpGet(downloadUrl);
    //请求头
    httpGet.addHeader("Accept","application/json");
    //完成签名并执行请求(注意:这里必须用httpClientWithNoSign,因为这个请求返回的数据是没有签名的.)
    CloseableHttpResponse response=(CloseableHttpResponse)httpClientWithNoSign.execute(httpGet);
    try{
        String bodyAsString=EntityUtils.toString(response.getEntity());
        int statusCode=response.getStatusLine()
                               .getStatusCode();
        if(statusCode<400){
            log.debug("下载账单成功");
            //存放数据
            return bodyAsString;
        }
        else{
            log.warn("下载账单失败,返回结果为:{}",bodyAsString);
            return null;
        }
    }
    catch(Exception e){
        log.warn("下载账单失败");
        return null;
    }
    finally{
        response.close();
    }
}
完整下载账单的操作
@GetMapping("/download-trade-bill")
public AjaxResult downloadTradeBill(String billDate,@RequestParam(defaultValue="ALL") String billType)throws Exception{
    //获取downloadUrl
    String downloadUrl=queryTradeBillDownloadUrl(billDate,billType);
    log.debug("downloadUrl:{}",downloadUrl);
    //访问downloadUrl获取数据
    String returnData=downloadBill(downloadUrl);
    HashMap<String,String> resultMap=new HashMap<>();
    resultMap.put("result",returnData);
    return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}
@GetMapping("/download-flow-bill")
public AjaxResult downloadFlowBill(String billDate) throws Exception{
    //获取downloadUrl
    String downloadUrl=queryFlowBillDownloadUrl(billDate);
    log.debug("downloadUrl:{}",downloadUrl);
    //访问downloadUrl获取数据
    String returnData=downloadBill(downloadUrl);
    HashMap<String,String> resultMap=new HashMap<>();
    resultMap.put("result",returnData);
    return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}

微信支付安全-尚硅谷

对称加密和非对称加密

对称加密AES算法, 运算速度快, 只有一个秘钥

非对称RSA算法, 运算速度慢, 公钥和私钥

先用非对称加密传输秘钥, 然后用对称加密来进行信息传输

身份认证

公钥加密, 私钥解密的作用是加密信息

私钥加密, 公钥解密的作用是身份认证

数字签名

image-20230604174926874

MD5和SHA1的抗碰撞性不太好, 比较常用的SHA2

在发送信件的过程总附着摘要, 收信人就可以观察完整性, 但是不具有机密性, 有中间人攻击的可能性.

所以发件人:

  1. 写信
  2. 得到摘要
  3. 私钥将摘要加密形成数字签名附在原文下面

收件人:

  1. 收到信
  2. 将信件原文用摘要算法得到信件摘要
  3. 用公钥解密签名得到摘要
  4. 对比两个摘要, 一致则说明是发件人发的, 并且没有篡改

==数字签名可以保证传递的信息没有被篡改, 并且得到信息传递者的真实身份==

数字证书

==公钥信任问题, 收件人的公钥是否是真实发件人的私钥对应的公钥, 解决办法就是数字证书==

image-20230604175939752

Bob获得证书的过程:

字证书颁发过程一般为:用户首先产生自己的秘钥对,并将公共密钥及部分个人身份信息传送给认证中心CA。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来。然后,认证中心将发给用户一个数字证书,该证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息。用户就可以使用自己的数字证书进行相关的各种活动。

Bob有了数字证书之后, 和Pat的通信过程(也就是此时获得的public key是真正的):

  1. 用户先用CA的public key(==大招是在系统里面的==)来解密签名获得企业未被篡改的public key
  2. 用认证过的public key和企业来进行通信

image-20230604180407427

image-20230604181254696

微信支付安全

微信支付下载下来的有平台证书和私钥是非对称加密所使用的

API秘钥是对称的那个秘钥,

微信支付注意事项-尚硅谷

处理支付结果重复通知

先检查对应业务数据的状态, 判断该通知是否已经处理, 对业务进行查询和处理之前, 需要使用数据锁来进行并发控制, 单体业务可以用 ReentrantLock或者synchronized来获取锁后进行查询和处理, 分布式的话使用redisson.

前端轮询查询支付结果直至成功

用户主动取消订单

后端定时查询订单来获取异步通知结果

引入spring定时任务, @EnableScheduling

从第0秒开始每隔30秒执行1次,向支付平台查询创建超过5分钟,并且未支付的订单, 如果支付平台已支付, 则修改订单状态为已支付, 如果支付平台未支付, 则关闭订单

支付漏洞

整型溢出

小数点四舍五入

比如1.4个物品和1.5个物品

或者2.0019=2.00, 那么每一次可以多一点点钱

重复购买限购商品

签约漏洞: 给一个二维码然后用多个手机扫描, 然后每个手机扫描二维码后停留, 最终一起付款

无限首充: 新用户首充

支付漏洞-越权替别人支付

平台账号和游戏账号时分开的,

两个账号都有首充优惠, 然后其他账号在充值的时候替换A账号为游戏账号, 然后很多首充

并发漏洞

账户只有一个优惠券, 然后手动点击多次, 然后一起放行同一个请求, 这时候每一个订单都有一个优惠券, 大额优惠券危害很大.

返回值

因为支付结果的签名, 无法修改, 但是在提交的时候, 如果传入的是price的话, 可以修改这个参数, 但一般情况下是只传入商品id, 后端查询价格然后向支付平台提交支付请求.

支付逻辑漏洞45分钟开始

资损常见情况和应对-极客时间

网络异常

  1. 调支付平台的支付接口出现网络异常, 此时支付平台并不一定失败, 如果此时再支付就会重复支付, 应该做的方法是此时支付订单记录记录为处理中, 等待定时查询订单的处理。

查询和通知问题

支付接口一般分为交易接口, 主动查询接口, 异步通知接口.

查询和通知类接口出现的问题:

  1. 查询失败或者查询异常
    查询订单出现状态码为失败,并不代表交易失败, 因为这是查询本身操作失败, 而不是交易失败
  2. 查询频率过快
    为了更快的得到支付结果, 所以重试的频率过快, 但是调起支付的过程中有网络异常, 此时等待15s后继续重试重新调起支付, 此时支付平台由于支付链路长, 无此订单, 如果重新调起支付的话, 就会多次支付订单.
  3. 被查询接口幂等性问题
  4. 通知问题
    上游或者下游重复通知问题, 或者上游或者下游重复通知, 并且前后两次通知不一致, 此时如果第一次通知和第二次通知不一样, 则以第一次为准并且人为预警

接口幂等性问题

只要做到幂等, 方可以进行重复提示, 通常用上游订单流水号做唯一索引做幂等

另外还可以在请求入口处用redis来做

状态同步

  1. 查询订单不存在, 需要单独设置响应码, 做特殊预警, 付款类的交易不可以设置为失败状态
  2. 对于不确定的状态, 就认为它是失败的
  3. redis, mq宕机后的重启的状态同步

直接引起重复提交问题

  1. 并发导致的重复支付
  2. 表单重复提交导致的重复支付(点击过快, 网速)
  3. 定时器重复执行, 定时器浪打浪-控制定时器的执行状态以及状态机有限状态转移
  4. 各种重试机制导致的重复机制, 比如mq, http等各种中间件的重复支付

前端防重:

后端防重: 数据库乐观锁+有限状态机+白名单状态

“我的支付总结-博客园”

基础概念

系统设计

常见问题

订单重复提交问题及解决

支付中常见幂等设计

  1. 订单号添加唯一索引, 操作前先查询是否已经支付过
  2. 用redis, 将订单号没有时放在redis中, 操作成功后删除key, 有时直接拒绝2

支付系统的几种常见技术事故

支付场景下的微服务改造与性能优化

LAST


欢迎在评论区中进行批评指正,转载请注明来源,如涉及侵权,请联系作者删除。

×

喜欢就点赞,疼爱就打赏