前期工作

浏览器输入 https://open.alipay.com/develop/sandbox/app 支付宝扫码登录

找到APPID,并启动系统默认秘钥,点击查看,有支付宝公钥和应用私钥,页面先开着,等会要用

前期工作1.png

前期工作2.png

内网穿透,用于接收支付宝回调信息,详情见参考网址

代码实现

导入支付宝依赖

<!-- 支付宝依赖 -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.35.79.ALL</version>
</dependency>

导入配置文件

# 支付宝沙箱支付
alipay:
  # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
  APP_ID: 9021000135650427
  # 商户私钥,您的PKCS8格式RSA2私钥
  APP_PRIVATE_KEY: YOUR_KEY
  # 支付宝支付公钥
  ALIPAY_PUBLIC_KEY: YOUR_KEY
  # 异步回调地址 必须外网能够访问(这里需要配置内网穿透),当支付成功后会调用该API(格式:内网穿透后给的url+回调接口名)
  NOTIFY_URL: http://cb8j3f.natappfree.cc/user/notify
  # 同步回调地址 可以不是公网地址
  RETURN_URL: http://localhost:8080/OK_PAY.html
  # 网关(注意沙箱网关和正式网关的区别,这里填写沙箱环境下的网关)
  GATEWAY_URL: https://openapi-sandbox.dl.alipaydev.com/gateway.do
  # 编码
  CHARSET: UTF-8
  # 返回数据格式
  FORMAT: JSON
  # 日志地址
  log_path: /log
  # RSA2
  SIGN_TYPE: RSA2

创建参数配置类

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class AliPayConfig {
    @Value("${alipay.APP_ID}")
    private String APP_ID;
    @Value("${alipay.APP_PRIVATE_KEY}")
    private String APP_PRIVATE_KEY;
    @Value("${alipay.CHARSET}")
    private String CHARSET;
    @Value("${alipay.ALIPAY_PUBLIC_KEY}")
    private String ALIPAY_PUBLIC_KEY;
    @Value("${alipay.GATEWAY_URL}")
    private String GATEWAY_URL;
    @Value("${alipay.FORMAT}")
    private String FORMAT;
    @Value("${alipay.SIGN_TYPE}")
    private String SIGN_TYPE;
    @Value("${alipay.NOTIFY_URL}")
    private String NOTIFY_URL;
    @Value("${alipay.RETURN_URL}")
    private String RETURN_URL;
}

付款接口

/**
 * 支付
 * @param httpResponse
 * @param alipayDTO
 * @return
 */
@Override
public boolean alipay(HttpServletResponse httpResponse, AlipayDTO alipayDTO) throws IOException {
    // 获取用户信息
    User user = userMapper.getUserById(alipayDTO.getUserId());
    // 获取订单信息
    Cart cart = userMapper.getCartById(alipayDTO.getCartId());
    if (cart == null || user == null){
        return false;
    }
    //实例化客户端,填入所需参数
    AlipayClient alipayClient = new DefaultAlipayClient(aliPayConfig.getGATEWAY_URL(), aliPayConfig.getAPP_ID(), aliPayConfig.getAPP_PRIVATE_KEY(), aliPayConfig.getFORMAT(), aliPayConfig.getCHARSET(), aliPayConfig.getALIPAY_PUBLIC_KEY(), aliPayConfig.getSIGN_TYPE());

    //在公共参数中设置回跳和通知地址
    AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
    request.setReturnUrl(aliPayConfig.getRETURN_URL());
    request.setNotifyUrl(aliPayConfig.getNOTIFY_URL());

    //商户订单号,商户网站订单系统中唯一订单号,必填
    Integer out_trade_no = (int) YitIdHelper.nextId();
    /******必传参数******/
    JSONObject bizContent = new JSONObject();
    //商户订单号,商家自定义,保持唯一性
    bizContent.put("out_trade_no", out_trade_no);
    //支付金额,最小值0.01元
    bizContent.put("total_amount", cart.getTotal());
    //订单标题,不可使用特殊符号
    bizContent.put("subject", cart.getName());
    //电脑网站支付场景固定传值FAST_INSTANT_TRADE_PAY
    bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
    request.setBizContent(bizContent.toString());
    String form = "";
    try {
        form = alipayClient.pageExecute(request).getBody(); // 调用SDK生成表单
        Oders oders = new Oders();
        oders.setGoodsId(Integer.valueOf(out_trade_no));
        oders.setGoodsName(cart.getName());
        oders.setGoodsTotal(cart.getTotal());
        oders.setClientId(user.getId());
        oders.setClientPhone(user.getPhone());
        oders.setStatus("待支付");
        oders.setAddress("河南新乡");
        userMapper.saveOrders(oders);
    } catch (AlipayApiException e) {
        e.printStackTrace();
    }
    httpResponse.setContentType("text/html;charset=" + aliPayConfig.getCHARSET());
    httpResponse.getWriter().write(form);// 直接将完整的表单html输出到页面
    httpResponse.getWriter().flush();
    httpResponse.getWriter().close();
    return true;
}

回调接口(必须是POST接口)

/**
 * 回调
 * @param request
 * @param response
 * @return
 */
@Override
public boolean returnUrl(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, AlipayApiException {
    System.out.println("=================================同步回调=====================================");

    // 获取支付宝GET过来反馈信息
    Map<String, String> params = new HashMap<String, String>();
    Map<String, String[]> requestParams = request.getParameterMap();
    for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
        String name = (String) iter.next();
        String[] values = (String[]) requestParams.get(name);
        String valueStr = "";
        for (int i = 0; i < values.length; i++) {
            valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
        }
        // 乱码解决,这段代码在出现乱码时使用
        valueStr = new String(valueStr.getBytes("utf-8"), "utf-8");
        params.put(name, valueStr);
    }

    System.out.println(params);//查看参数都有哪些
    boolean signVerified = AlipaySignature.rsaCheckV1(params, aliPayConfig.getALIPAY_PUBLIC_KEY(), aliPayConfig.getCHARSET(), aliPayConfig.getSIGN_TYPE()); // 调用SDK验证签名
    //验证签名通过
    if (signVerified) {
        // 商户订单号
        String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
        // 支付宝交易号
        String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
        // 付款金额
        String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");

        System.out.println("商户订单号=" + out_trade_no);
        System.out.println("支付宝交易号=" + trade_no);
        System.out.println("付款金额=" + total_amount);
        // 写入支付宝交易号
        userMapper.insertNo(Integer.valueOf(out_trade_no),trade_no);
        //支付成功,修复支付状态
        userMapper.updateStatusById(Integer.valueOf(out_trade_no),"已支付");
        return true;
    } else {
        return false;
    }
}

退款接口

/**
 * 退款
 * @param oders
 * @return
 */
@Override
public boolean returnPay(Oders oders) {
    // 7天无理由退款
    Oders orders = userMapper.getOrdersByGoodsId(oders.getGoodsId());
    if (orders == null) {
        System.out.println("没有该订单号");
        return false;
    }

    // 1. 创建Client,通用SDK提供的Client,负责调用支付宝的API
    AlipayClient alipayClient = new DefaultAlipayClient
            (aliPayConfig.getGATEWAY_URL(), aliPayConfig.getAPP_ID(), aliPayConfig.getAPP_PRIVATE_KEY(),
                    aliPayConfig.getFORMAT(), aliPayConfig.getCHARSET(), aliPayConfig.getALIPAY_PUBLIC_KEY(),
                    aliPayConfig.getSIGN_TYPE());

    // 2. 创建 Request,设置参数
    AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
    JSONObject bizContent = new JSONObject();
    bizContent.put("trade_no", orders.getTradeNo());  // 支付宝回调的订单流水号
    bizContent.put("refund_amount", orders.getGoodsTotal());  // 订单的总金额
    bizContent.put("out_request_no", orders.getGoodsId());   //  我的订单编号

    request.setBizContent(bizContent.toString());

    // 3. 执行请求
    AlipayTradeRefundResponse response;
    try {
        response = alipayClient.execute(request);
    } catch (AlipayApiException e) {
        throw new RuntimeException(e);
    }
    if (response.isSuccess()) {  // 退款成功,isSuccess 为true
        System.out.println("调用成功");
        // 4. 更新数据库状态
        userMapper.updateStatusById(orders.getGoodsId(), "已退款");
        return true;
    } else {   // 退款失败,isSuccess 为false
        System.out.println(response.getBody());
        System.out.println(response.getCode());
        return false;
    }
}

超时处理

① 在启动类上加

@EnableScheduling // 添加此注解以启用定时任务

② 配置定时器类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Service
@Slf4j
public class TimeOutService {
  @Autowired
  private JdbcTemplate jdbcTemplate;
  private static final String **UPDATE_STATUS_SQL** = "UPDATE orders SET status = '已超时' WHERE status != '已支付' AND created_time <= NOW() - INTERVAL 30 SECOND";

  @Scheduled(fixedRate = 5000) // 每5s检查一次
  public void checkTimeoutOrders() {
    log.info("定时器已启动");
    LocalDateTime thirtySecondsAgo = LocalDateTime.**now**().minusSeconds(30);
    jdbcTemplate.update(**UPDATE_STATUS_SQL**);
  }
}

前端页面

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>支付宝沙箱支付</title>
</head>
<body>

<!-- 假设您的支付按钮 -->
<button id="pay-now-btn">立即支付</button>

<script>
    document.getElementById('pay-now-btn').addEventListener('click', function () {
        const payApiUrl = "user/alipay";
        const data = {
            // 这里我是写死的,真实场景基于前端页面动态传入
            userId: 4,
            cartId: 1
        };

        // 发送POST请求携带JSON数据
        fetch(payApiUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(data)
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.text();
            })
            .then(htmlForm => {
                const paymentWindow = window.open('', '_blank');
                paymentWindow.document.write(htmlForm);
                paymentWindow.document.querySelector('form').submit();
            })
            .catch(error => {
                console.error('请求支付接口时出错:', error);
                alert('无法连接到支付服务,请稍后再试');
                if (window.paymentWindow && !paymentWindow.closed) {
                    paymentWindow.close();
                }
            });
    });
</script>
</body>
</html>

测试

基于前端页面和postman联调测试:

  1. 运行程序后,打开配置的前端测试页面,点击支付按钮进行提交数据并跳转

    此时数据库已经插入订单数据,这时可以不支付,等待订单过期查看定时器修改订单状态情况

  2. 输入买家账号密码后进入支付页面

  3. 输入支付密码后支付

  4. 打开数据库查看订单状态情况

  5. 打开IDEA查看支付宝回调情况

  6. 用postman调用退款接口,只需传入订单号即可退款

    1. 打开数据库查看订单修改情况

参考

Springboot集成支付宝沙箱支付(完整版)_springboot支付宝沙箱支付-CSDN博客

Springboot集成支付宝沙箱支付(退款功能)_支付宝沙箱退款-CSDN博客