支付宝沙箱支付流程
大约 6 分钟
前期工作
浏览器输入 https://open.alipay.com/develop/sandbox/app 支付宝扫码登录
找到APPID,并启动系统默认秘钥,点击查看,有支付宝公钥和应用私钥,页面先开着,等会要用
内网穿透,用于接收支付宝回调信息,详情见参考网址
代码实现
导入支付宝依赖
<!-- 支付宝依赖 -->
<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联调测试:
运行程序后,打开配置的前端测试页面,点击支付按钮进行提交数据并跳转
此时数据库已经插入订单数据,这时可以不支付,等待订单过期查看定时器修改订单状态情况
输入买家账号密码后进入支付页面
输入支付密码后支付
打开数据库查看订单状态情况
打开IDEA查看支付宝回调情况
用postman调用退款接口,只需传入订单号即可退款
- 打开数据库查看订单修改情况