Java版微信/支付宝支付

一、微信支付接入与介绍

1、微信支付产品介绍

付款码支付

用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。

JSAPI支付

  • 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支 付。
  • 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
  • PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。

特点:用户在客户端输入支付金额

小程序支付

在微信小程序平台内实现支付的功能。

Native支付

Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。

特点:商家预先指定支付金额

APP支付

商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。

刷脸支付

用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。

2、接入指引

1、获取商户号

微信商户平台:https://pay.weixin.qq.com/ 场景:Native支付

步骤:提交资料 => 签署协议 => 获取商户号

2、获取APPID

微信公众平台:https://mp.weixin.qq.com/

步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

3、获取API秘钥

APIv2版本的接口需要此秘钥

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

4、获取APIv3秘钥

APIv3版本的接口需要此秘钥

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

随机密码生成工具:https://suijimimashengcheng.bmcx.com/

5、申请商户API证书

商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。

APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)

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

商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

6、获取微信平台证书

微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。

可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取

平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

注意:以上所有API秘钥和证书需妥善保管防止泄露

二、支付安全基础(证书/秘钥/签名)

1、安全基础

**明文:**加密前的消息叫“明文”(plain text)

**密文:**加密后的文本叫“密文”(cipher text)

**密钥:**只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)

“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串

**加密:**实现机密性最常用的手段是“加密”(encrypt)

按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。

**解密:**使用密钥还原明文的过程叫“解密”(decrypt)

**加密算法:**加密解密的操作过程就是“加密算法”

所有的加密算法都是公开的,而算法使用的“密钥”则必须保密

2、对称加密和非对称加密

对称加密

  • 特点:只使用一个密钥,密钥必须保密,常用的有AES算法
  • 优点:运算速度快
  • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换

非对称加密

  • 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有RSA
  • 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
  • 缺点:运算速度非常慢

混合加密

实际场景中把对称加密和非对称加密结合起来使用

身份认证

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

3、摘要算法

摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
摘要算法的作用是保证信息的完整性

特性

  • 不可逆:只有算法,没有秘钥,只能加密,不能解密难题友好性:想要破解,只能暴力枚举
  • 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化抗碰撞性:原文不同,计算后的摘要也要不同

常见摘要算法

MD5、SHA1、SHA2(SHA224、SHA256、SHA384)

4、数字签名与证书

4.1 数字签名

数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。但是黑客可以伪造公钥与客户进行通信

在这里插入图片描述

4.2 数字证书

数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。个人不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA(证书颁发机构)颁发

在这里插入图片描述

4.3 https协议中的数字证书

在这里插入图片描述

三、基础支付API V3

1、支付配置准备

1.1 引入支付参数

注:这里使用了尚硅谷的资料

新建 wxpay.properties文件并放置在resources文件夹下

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

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

# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

新建 WxPayConfig.java获取配置文件信息

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

    // APIv2密钥
    private String partnerKey;

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

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

    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier(){

        log.info("获取签名验证器");

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

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

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

        // 使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials,
                apiV3Key.getBytes(StandardCharsets.UTF_8));

        return verifier;
    }


    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier 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;
    }

}

最后为了让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示,让配置文件和Java代码之间的对应参数可以自动定位,方便开发,这里需要配置Annotation Processormaven依赖,同时进入File -> Project Structure -> Modules -> 选择小叶子- > 选择+号- >选中配置文件,即可配置成功

<!-- 生成自定义配置的元数据信息 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

1.2 加载商户私钥

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient

引入微信SDK详情查看官网,搜索如何加载商户私钥,实现了请求签名的生成和应答签名的验证

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.1</version>
</dependency>

1.3 获取签名验证器和HttpClient

流程如图所示,其余可查看Github官网,1.1中已经具体写明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Po2JQaN-1649921278437)(https://pay.weixin.qq.com/wiki/doc/apiv3/assets/img/common/ico-guide/chapter1_5_1.png “”)]

1.4 API字典和相关工具

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml

微信Native支付的API列表

在这里插入图片描述

同时微信支付 APIv3 使用 JSON 作为消息体的数据交换格式,因此需要引入json转换依赖

<!--json处理-->
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
</dependency>

最后将Native支付接口写成枚举类,方便调用,以及HttpUtil工具类

//举例
@AllArgsConstructor
@Getter
public enum PayType {
    /**
     * 微信
     */
    WXPAY("微信"),
    
    /**
     * 支付宝
     */
    ALIPAY("支付宝");

    /**
     * 类型
     */
    private final String type;
}

public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

1.5 设置全局返回类

// 全局返回
@Data
@Accessors(chain = true)
public class R {

    private Integer code; //响应码
    private String message; //响应消息
    private Map<String, Object> data = new HashMap<>();

    public static R ok(){
        R r = new R();
        r.setCode(0);
        r.setMessage("成功");
        return r;
    }

    public static R error(){
        R r = new R();
        r.setCode(-1);
        r.setMessage("失败");
        return r;
    }

    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }
}

2、签名和验签解析

2.1 签名

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml

在这里插入图片描述

2.2 验签

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml

在这里插入图片描述

3、Native支付

3.1 Native支付流程

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

在这里插入图片描述

3.2 Native下单Api

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

商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户 端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付

/**
 * 创建订单,调用Native支付接口
 * @param productId
 * @return code_url 和 订单号
 * @throws Exception
 */
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {

    log.info("生成订单");

    //生成订单
    OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
    String codeUrl = orderInfo.getCodeUrl();
    if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
        log.info("订单已存在,二维码已保存");
        //返回二维码
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());
        return map;
    }


    log.info("调用统一下单API");

    //调用统一下单API
    HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

    // 请求body参数
    Gson gson = new Gson();
    Map 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 amountMap = new HashMap();
    amountMap.put("total", orderInfo.getTotalFee());
    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 {
            log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
            throw new IOException("request failed");
        }

        //响应结果
        Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
        //二维码
        codeUrl = resultMap.get("code_url");

        //保存二维码
        String orderNo = orderInfo.getOrderNo();
        orderInfoService.saveCodeUrl(orderNo, codeUrl);

        //返回二维码
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());

        return map;

    } finally {
        response.close();
    }
}

使用微信SDK自带的验签和签名函数,进行订单创建,同时为了减轻服务器压力,需要缓存二维码,同时定时检查二维吗url是否已经过期,并同步到数据库

3.3 支付通知API

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

微信支付通过支付通知接口将用户支付成功消息通知给商户

  • 内网穿透
    这里使用了ngrok作为内网穿透工具,原因是微信支付回调函数需要通知地址,而我们的程序一般都部署在内网;同时针对微信的回调函数,需要考虑失败、超时等情况,否则微信会持续发送回调通知,直到成功返回
  • 验签工具
    参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest
  • 解密工具
    验签成功的签名串经过对称加密,需要进行api-v3-key秘钥进行解密,证书和回调解密的AesGcm解密参考AesUtil.java
  • 处理订单
    获取微信支付回调函数后,需要更新订单状态以及插入支付记录,同时使用可重入锁来处理并发、重复请求,保证订单记录的唯一性与可靠性

4、微信支付查单API

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

查单接口是为了确定用户已经完成支付但却未回调到系统的情况,需要增加定时任务,定时查找超时订单与处理超时订单等

@Slf4j
@Component
public class WxPayTask {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private WxPayService wxPayService;

    @Resource
    private RefundInfoService refundInfoService;

    /**
     * 秒 分 时 日 月 周
     * 以秒为例
     * *:每秒都执行
     * 1-3:从第1秒开始执行,到第3秒结束执行
     * 0/3:从第0秒开始,每隔3秒执行1次
     * 1,2,3:在指定的第1、2、3秒执行
     * ?:不指定
     * 日和周不能同时制定,指定其中之一,则另一个设置为?
     */
    //@Scheduled(cron = "0/3 * * * * ?")
    public void task1(){
        log.info("task1 被执行......");
    }

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    //@Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() throws Exception {
        log.info("orderConfirm 被执行......");

        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.WXPAY.getType());

        for (OrderInfo orderInfo : orderInfoList) {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);

            //核实订单状态:调用微信支付查单接口
            wxPayService.checkOrderStatus(orderNo);
        }
    }


    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     */
    //@Scheduled(cron = "0/30 * * * * ?")
    public void refundConfirm() throws Exception {
        log.info("refundConfirm 被执行......");

        //找出申请退款超过5分钟并且未成功的退款单
        List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1);

        for (RefundInfo refundInfo : refundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);

            //核实订单状态:调用微信支付查询退款接口
            wxPayService.checkRefundStatus(refundNo);
        }
    }

}

5、微信支付退款API

退款回调和通知回调类似

6、微信支付账单下载

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_4_8.shtml

前端Vue下载函数

//下载账单
downloadBill(type){
  //获取账单内容
  billApi.downloadBill(this.billDate, type).then(response => {
    console.log(response)
    //response.data.result是后端传输的数据流,后端已经从微信支付下载完毕数据,
    //将其变成base64data,前端进行excel下载
    const element = document.createElement('a')
    element.setAttribute('href', 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(response.data.result))
    element.setAttribute('download', this.billDate + '-' + type)
    element.style.display = 'none'
    element.click()
  })
}

7、基础支付APIv2

https://pay.weixin.qq.com/wiki/doc/api/index.html

在这里插入图片描述

四、支付宝支付介绍与环境准备

1、接入介绍

支付宝开发者开发平台:https://open.alipay.com/

1.1 常规接入流程

例如网页&移动应用:https://opendocs.alipay.com/open/200

  • 创建应⽤:选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
  • 绑定应⽤:将开发者账号中的APPID和商家账号PID进⾏绑定
  • 配置秘钥:即创建应⽤中的“配置应⽤环境”步骤
  • 上线应⽤:将应⽤提交审核
  • 签约功能:在商家中⼼上传营业执照、已备案⽹站信息等,提交审核进⾏签约

1.2 使用沙箱

2、支付参数引入

2.1 引入配置文件

创建alipay.properties文件,里面存放支付宝相关参数,这里使用了尚硅谷的真实支付宝参数,沙箱版自行替换参数

# 支付宝支付相关参数
# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021003124617201

# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088102040215494

# 支付宝网关
alipay.gateway-url=https://openapi.alipay.com/gateway.do

# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDmMcbk+ezXjrwG2kTIQugXLpMJnl8b112Bq+TV/yQgL2oMC2alYCXDzyHWyjXLHhpeb9MVrKKqqdifcv3+r0U2rclsmoBVIdmlpk9E3Hi0Ulb7qIoYwgrPUMpSQssCPnyCqoN7Xg+y7PqZhgHQpBOF34lGokc1hAVyHHIt+JybTvDaWr00Em9NwGslw3oV8mDXA1rpoPSEpMyxrGW6kFaOlX08zsYoLJUgL9VkQAJ/WUm5gMpQArSzO9MFL3VwnyEBg2KBNlxuDwj+PJ6D3RB6o4SWID8X7y/UCscNLwTmVr2qLf6zSf3GtX+/jnXSVLTtqqL8bnZNFXKNbfoWNH5ZAgMBAAECggEAVEROkg3npLVMoZmPalwLyEi1bOT73h5Fza1WVPxUhi+1O3mE9u8ug/K0aYOWk6eOcZmwBRQwbBdHBH+8+VnCFZUi0k3wwrlkil5KUGQBD8nAq9lzzEJkYKYrmld3J3gmblLrVOMHDjHwPvkueulFeFFvWFsZhD6zG6XMKoYDFlrq1k4yCfimMrPTSLRxhUtkdbPXt0V8vTgrX9D5z9wFiAebjzRhRRhbxWH4xkdy5WaPI7z+zakIjjrWECdXEVVF7BvahBv1dGtKCGjwRwPw15Bc336yZ2Gqfa9Il06PHY3XCB5YCtE31T1s+Wm620hbtJ1Dk3sDnspoYpV8ZnPHLQKBgQD0QOHErtE6OcFMezWLWjbtak1QWUuxdCYtmVCgWqpPpe/JuFSKUYy6AF6Bd5RBJOa4i+RtrZuMAjM0QzZ/6gCZyWzMo0aZB9KhtC3ftpAKWvlmFdvH08I0GKd/PLY/Wcz5YIgByhcV2VZ410NH2u1nVqKL+jIfpzJOzM3zsLFGdwKBgQDxQ87dsVPxdwqcvAkv4WwFY0Dv1932uscyb5ZjkasWtiQ6TPrfzzkT13Zu81vqXL9eaiTgaP8bYeVUQVVScFiO0r2Ylp6TJzp3nl3qEm8N4DmoA/Vwo5TXoR55HgDUMBv7DMzcBa/PU5J1ErZ1pt6k2uBD0U3sMxIRP+VBpsMFrwKBgQCENM4/GGS1gGdpT1NXHziV3zED6aF35qd3jQHAGfMPc4DMDdLsn2FtmB+PMjtz21Zq04WL/Ckyakpu4maQbAdxNj6GsWXYFQzka9NcwMNMZ5uQrwosKil261VWIHWA6slwvdhAJ7PBJseQVuva69wOUC1hWMZirawkTOS5H42E1wKBgQCuDt6CgDlwXhKQ6vOx0G6vIGEr58/h/fRSBcE4ylHlS7itOvZPW1/xWaO+/eFVHl6NzgQWxokthx39ADl/BUBOoelY2WlD/qwmumFEytHF7/uIpHqBLfLm8f1bIfM1IhQ9tYliPtQMvl1OCxcJoD7GLoZXRvxxqJKjUTaje5z9TwKBgQDul1tsUHKpuWyoXoHQ6j0zqzMPgPohvFGN1fV6MBpHHFDDCLajuCrpj/gQPWcwRpOYArZK7u0cyvn/sWijbdL1v6RtjYt7sASiDRbf2FFraLeRJOu/KKkFZ5UZbXWiR2fs02+sTkLBD2MI16HquJzsS3rK+8Hitgdd/U3PyHDSyQ==

# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAguAlv4mG5/uvo5VjyU2ZAJuZR3VbSbhUN11DKYuNbtaGh9lzysF0N7ZbjWLb8E3TLYsvzSYiDwPUJyVU1uPLld9mXiLQ/k8FOAma1rCG7OFMhWtBWglZq2LxLN68qz8aIsUPbuaqiuIfF+zqg9dQ4y9CrhC18U5cCzpYmqoSbxZPMLGRE28qgs7m9FxmGBjttpa+oHHg2Jf1i79DOtbUCHTrK9Mr6Cfd47dAMQf0OdIuvD+fxYD3fF8tVeJWH+GyibMCojYn66lFUhR1TqKoKZtUxFCcnGbodEhoWr2iTlPTMQzm24EiYODod0xn3GWwigZcJma2tLruW51de6U+dwIDAQAB

# 接口内容加密秘钥,对称秘钥
alipay.content-key=pdF//ropAEqHXxI360iwUg==

# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success

# 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://a863-180-174-204-169.ngrok.io/api/ali-pay/trade/notify

2.2 创建配置文件

在config包创建AlipayClientConfig,这里使用Environment 获取参数,和上面微信支付获取参数方法不同

@Configuration
//加载配置文件
@PropertySource("classpath:alipay.properties")
public class AlipayClientConfig {

    @Resource
    private Environment config;
    
    // 可以通过以下方式获取config.getProperty("alipay.app-id")
}

3、引入服务端SDK

3.1 引入依赖

参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 服务端SDK => Java => 通⽤版 => Maven项⽬依赖https://search.maven.org/artifact/com.alipay.sdk/alipay-sdk-java

<dependency>
  <groupId>com.alipay.sdk</groupId>
  <artifactId>alipay-sdk-java</artifactId>
  <version>4.23.21.ALL</version>
</dependency>

3.2 创建客户端连接对象

参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 技术接⼊指南 => 数据签名
https://opendocs.alipay.com/common/02kf5q

参考⽂档中 公钥方式 完善 AlipayClientConfig 类,添加 alipayClient() ⽅法 初始化 AlipayClient 对象

@Configuration
//加载配置文件
@PropertySource("classpath:alipay.properties")
public class AlipayClientConfig {

    @Resource
    private Environment config;

    @Bean
    public AlipayClient alipayClient() throws AlipayApiException {

        AlipayConfig alipayConfig = new AlipayConfig();

        //设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
        //设置应用Id
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
        //设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
        //设置请求格式,固定值json
        alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
        //设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
        //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
        //设置签名类型
        alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
        //构造client
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);

        return alipayClient;
    }
}

五、支付宝支付功能开发

1、统一收单下单与支付

1.1 支付调用流程

https://opendocs.alipay.com/open/270/105899

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWwD20cH-1653225486947)(https://secure2.wostatic.cn/static/uxiuZngjTEt6V1s3cK7vcG/image.png)]

1.2 接口说明

https://opendocs.alipay.com/apis/028r8t?scene=22

  • 公共请求参数:所有接⼝都需要的参数
  • 请求参数:当前接⼝需要的参数
  • 公共响应参数:所有接⼝的响应中都包含的数据
  • 响应参数:当前接⼝的响应中包含的数据

1.3 发起支付请求

在线调试:https://opendocs.alipay.com/open/02no3m

后端核心

@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private Environment config;

    /**支付宝开放平台接受 request 请求对象后
    * 会为开发者生成一个html 形式的 form表单,包含自动提交的脚本
    * 我们将form表单字符串返回给前端程序,之后前端将会调用自动提交脚本,进行表单的提交
    * 此时,表单会自动提交到action属性所指向的支付宝开放平台中,从而为用户展示一个支付页面
    */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String tradeCreate(Long productId) {

        try {
            //生成订单
            log.info("生成订单");
            // 先查存在就直接返回,不存在就插入
            OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());

            //调用支付宝接口
            AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
            //配置需要的公共请求参数
            //支付完成后,支付宝向谷粒学院发起异步通知的地址
            request.setNotifyUrl(config.getProperty("alipay.notify-url"));
            //支付完成后,我们想让页面跳转回谷粒学院的页面,配置returnUrl
            request.setReturnUrl(config.getProperty("alipay.return-url"));

            //组装当前业务方法的请求参数
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderInfo.getOrderNo());
            BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
            bizContent.put("total_amount", total);
            bizContent.put("subject", orderInfo.getTitle());
            bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");

            request.setBizContent(bizContent.toString());

            //执行请求,调用支付宝接口
            AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

            if(response.isSuccess()){
                log.info("调用成功,返回结果 ===> " + response.getBody());
                return response.getBody();
            } else {
                log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
                throw new RuntimeException("创建支付交易失败");
            }
        } catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("创建支付交易失败");
        }
    }

}

前端核心

//确认支付
    toPay() {
      //禁用按钮,防止重复提交
      this.payBtnDisabled = true

      //微信支付
      if (this.payOrder.payType === 'wxpay') {
       ...
        //支付宝支付
      } else if (this.payOrder.payType === 'alipay') {

        //调用支付宝统一收单下单并支付页面接口
        aliPayApi.tradePagePay(this.payOrder.productId).then((response) => {
          //将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交
          document.write(response.data.formStr)
        })
      }
    },

前端API方法

import request from '@/utils/request'

export default{

  //发起支付请求
  tradePagePay(productId) {
    return request({
      url: '/api/ali-pay/trade/page/pay/' + productId,
      method: 'post'
    })
  }
 }

2、支付结果通知

2.1 环境配置

AliPayServiceImpl tradeCreate ⽅法中设置异步通知地址。同时启动ngrok内网穿透,上面微信支付已经说明,注意要修改成自己的配置

2.2 开发异步通知接口

https://opendocs.alipay.com/open/270/105902#异步通知参数

AliPayController

@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String, String> params){

    log.info("支付通知正在执行");
    log.info("通知参数 ===> {}", params);

    String result = "failure";

    try {
        //异步通知验签
        boolean signVerified = AlipaySignature.rsaCheckV1(
                params,
                config.getProperty("alipay.alipay-public-key"),
                AlipayConstants.CHARSET_UTF8,
                AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名

        if(!signVerified){
            //验签失败则记录异常日志,并在response中返回failure.
            log.error("支付成功异步通知验签失败!");
            return result;
        }

        // 验签成功后
        log.info("支付成功异步通知验签成功!");

        //按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,
        //1 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
        String outTradeNo = params.get("out_trade_no");
        OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
        if(order == null){
            log.error("订单不存在");
            return result;
        }

        //2 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
        String totalAmount = params.get("total_amount");
        int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
        int totalFeeInt = order.getTotalFee().intValue();
        if(totalAmountInt != totalFeeInt){
            log.error("金额校验失败");
            return result;
        }

        //3 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方
        String sellerId = params.get("seller_id");
        String sellerIdProperty = config.getProperty("alipay.seller-id");
        if(!sellerId.equals(sellerIdProperty)){
            log.error("商家pid校验失败");
            return result;
        }

        //4 验证 app_id 是否为该商户本身
        String appId = params.get("app_id");
        String appIdProperty = config.getProperty("alipay.app-id");
        if(!appId.equals(appIdProperty)){
            log.error("appid校验失败");
            return result;
        }

        //在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS时,
        // 支付宝才会认定为买家付款成功。
        String tradeStatus = params.get("trade_status");
        if(!"TRADE_SUCCESS".equals(tradeStatus)){
            log.error("支付未成功");
            return result;
        }

        //处理业务 修改订单状态 记录支付日志
        aliPayService.processOrder(params);

        //校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
        result = "success";
    } catch (AlipayApiException e) {
        e.printStackTrace();
    }
    return result;
}

AliPayService

/**
 * 处理订单
 * @param params
 */
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, String> params) {

    log.info("处理订单");

    //获取订单号
    String orderNo = params.get("out_trade_no");

    /*在对业务数据进行状态检查和处理之前,
    要采用数据锁进行并发控制,
    以避免函数重入造成的数据混乱*/
    //尝试获取锁:
    // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
    if(lock.tryLock()) {
        try {

            //处理重复通知
            //接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
            String orderStatus = orderInfoService.getOrderStatus(orderNo);
            if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
                return;
            }

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

            //记录支付日志
            paymentInfoService.createPaymentInfoForAliPay(params);

        } finally {
            //要主动释放锁
            lock.unlock();
        }
    }

}

3、统一收单交易关闭

https://opendocs.alipay.com/apis/028wob

/**
 * 用户取消订单
 * @param orderNo
 */
@Override
public void cancelOrder(String orderNo) {

    //调用支付宝提供的统一收单交易关闭接口
    this.closeOrder(orderNo);

    //更新用户订单状态
    orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
    
    
/**
 * 关单接口的调用
 * @param orderNo 订单号
 */
private void closeOrder(String orderNo) {

    try {
        log.info("关单接口的调用,订单号 ===> {}", orderNo);

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

        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            //throw new RuntimeException("关单接口的调用失败");
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("关单接口的调用失败");
    }
}

注意:针对⼆维码⽀付,只有经过扫码的订单才在⽀付宝端有交易记录。针对⽀付宝账号⽀付,只有经过登录的订单才在⽀付宝端有交易记录。

4、统一收单交易查询

https://opendocs.alipay.com/apis/028woa

4.1 查单接口调用

商户后台未收到异步⽀付结果通知时,商户应该主动调⽤《统⼀收单线下交易查询接⼝》,同步订单状态。

/**
 * 查询订单
 * @param orderNo
 * @return 返回订单查询结果,如果返回null则表示支付宝端尚未创建订单
 */
@Override
public String queryOrder(String orderNo) {

    try {
        log.info("查单接口调用 ===> {}", orderNo);

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

        AlipayTradeQueryResponse response = alipayClient.execute(request);
        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
            return response.getBody();
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            //throw new RuntimeException("查单接口的调用失败");
            return null;//订单不存在
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("查单接口的调用失败");
    }
}

4.2 定时查单

创建定时任务

@Slf4j
@Component
public class AliPayTask {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private AliPayService aliPayService;

    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm(){

        log.info("orderConfirm 被执行......");

        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());

        for (OrderInfo orderInfo : orderInfoList) {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);

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

4.3 处理查询到的订单

/**
 * 根据订单号调用支付宝查单接口,核实订单状态
 * 如果订单未创建,则更新商户端订单状态
 * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
 * 如果订单已支付,则更新商户端订单状态,并记录支付日志
 * @param orderNo
 */
@Override
public void checkOrderStatus(String orderNo) {

    log.warn("根据订单号核实订单状态 ===> {}", orderNo);

    String result = this.queryOrder(orderNo);

    //订单未创建
    if(result == null){
        log.warn("核实订单未创建 ===> {}", orderNo);
        //更新本地订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
    }

    //解析查单响应结果
    Gson gson = new Gson();
    HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
    LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");

    String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
    if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
        log.warn("核实订单未支付 ===> {}", orderNo);

        //如果订单未支付,则调用关单接口关闭订单
        this.closeOrder(orderNo);

        // 并更新商户端订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
    }

    if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
        log.warn("核实订单已支付 ===> {}", orderNo);

        //如果订单已支付,则更新商户端订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

        //并记录支付日志
        paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
    }

}

5、统一交易退款

https://opendocs.alipay.com/apis/028sm9

/**
 * 退款
 * @param orderNo
 * @param reason
 */
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) {

    try {
        log.info("调用退款API");

        //创建退款单
        RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(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"));
        //BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
        bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
        bizContent.put("refund_reason", reason);//退款原因(可选)

        request.setBizContent(bizContent.toString());

        //执行请求,调用支付宝接口
        AlipayTradeRefundResponse response = alipayClient.execute(request);

        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

            //更新退款单
            refundsInfoService.updateRefundForAliPay(
                    refundInfo.getRefundNo(),
                    response.getBody(),
                    AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功

        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);

            //更新退款单
            refundsInfoService.updateRefundForAliPay(
                    refundInfo.getRefundNo(),
                    response.getBody(),
                    AliPayTradeState.REFUND_ERROR.getType()); //退款失败
        }


    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("创建退款申请失败");
    }
}

6、收单退款冲退完成通知

https://opendocs.alipay.com/apis/029yy3

退款存在退到银⾏卡场景下时,收单会根据银⾏回执消息发送退款完成信息。开发流程类似⽀付结果通知。

7、对账

https://opendocs.alipay.com/apis/028woc

查询对账单下载地址接⼝

/**
 * 申请账单
 * @param billDate
 * @param type
 * @return
 */
@Override
public String queryBill(String billDate, String type) {

    try {

        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 billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
            String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");

            return billDownloadUrl;
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            throw new RuntimeException("申请账单失败");
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("申请账单失败");
    }
}

参考文章

尚硅谷微信/支付宝支付视频教学

微信支付官方文档

支付开发者平台

github资源地址(微信支付)

github资源地址(支付宝支付)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值