SpringBoot整合支付宝(沙箱)
**电脑网站支付:统一收单交易关闭接口 - 支付宝文档中心 (alipay.com)**
1、依赖
<!--支付宝-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.34.0.ALL</version>
</dependency>
2、工具类
package com.ruoyi.pay.util;
/**
* 支付工具类
*/
public class PayUtil {
//支付宝配置参数
public static class Alipay {
public static final String appId = "";
//支付宝沙盒私钥
public static final String privateKey = "";
//支付宝沙盒公钥
public static final String alipayPublicKey = "";
//支付宝网关地址(沙盒)
public static final String url = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
//支付宝异步回调地址 (必须公网)
public static final String notifyUrl = "http://wechat.**.com/pay/alicallback";
//支付宝同步跳转地址(前端,本地也可以)
public static final String synchronousNotifyUrl = "http://localhost:81/Reservation/reservationRegister";
}
}
3、雪花生成
package com.ruoyi.pay.util;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
4、service
package com.ruoyi.pay.service;
import com.ruoyi.pay.domain.dto.PayMethodDto;
/**
* 支付服务
*/
public interface PayService {
/**
* 拉起支付页面 创建订单
* @param payMethodDto
* @return
*/
String getPayPage(PayMethodDto payMethodDto);
/**
* 统一收单交易查询
* @param orderId
*/
void getByOrderId(String orderId);
}
package com.ruoyi.appointment.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.appointment.domain.PatientReservation;
import com.ruoyi.appointment.domain.vo.PatientReservationDto;
import com.ruoyi.appointment.domain.dto.PatientReservationVo;
import java.util.List;
public interface PatientReservationService extends IService<PatientReservation> {
//拉起支付
String getAlipayPage(PatientReservationVo patientReservationVo);
}
5、impl
package com.ruoyi.pay.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.ruoyi.pay.domain.dto.PayMethodDto;
import com.ruoyi.pay.service.PayService;
import com.ruoyi.pay.util.PayUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 支付宝支付
*/
@Slf4j
public class AliPayServiceImpl implements PayService {
@Override
public String getPayPage(PayMethodDto payMethodDto) {
log.info("支付宝支付");
AlipayClient alipayClient = new DefaultAlipayClient(PayUtil.Alipay.url,
PayUtil.Alipay.appId,
PayUtil.Alipay.privateKey, "json", "utf-8",
PayUtil.Alipay.alipayPublicKey,
"RSA2");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
//异步接收地址,仅支持http/https,公网可访问
request.setNotifyUrl(PayUtil.Alipay.notifyUrl);
//同步跳转地址,仅支持http/https (本地也可以)
//request.setReturnUrl(PayUtil.Alipay.synchronousNotifyUrl);
/******必传参数******/
JSONObject bizContent = new JSONObject();
//商户订单号,商家自定义,保持唯一性
bizContent.put("out_trade_no", payMethodDto.getOrderId());
//支付金额,最小值0.01元
bizContent.put("total_amount", payMethodDto.getPayPrice());
//订单标题,不可使用特殊符号
bizContent.put("subject", payMethodDto.getDepartmentTitle());
//电脑网站支付场景固定传值FAST_INSTANT_TRADE_PAY
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
/******可选参数******/
//bizContent.put("time_expire", "2022-08-01 22:00:00");
商品明细信息,按需传入
//JSONArray goodsDetail = new JSONArray();
//JSONObject goods1 = new JSONObject();
//goods1.put("goods_id", "goodsNo1");
//goods1.put("goods_name", "子商品1");
//goods1.put("quantity", 1);
//goods1.put("price", 0.01);
//goodsDetail.add(goods1);
//bizContent.put("goods_detail", goodsDetail);
扩展信息,按需传入
//JSONObject extendParams = new JSONObject();
//extendParams.put("sys_service_provider_id", "2088511833207846");
//bizContent.put("extend_params", extendParams);
request.setBizContent(bizContent.toString());
AlipayTradePagePayResponse response = null;
try {
// 如果需要返回GET请求,请使用
response = alipayClient.pageExecute(request, "GET");
//response = alipayClient.pageExecute(request, "POST");
} catch (AlipayApiException e) {
throw new RuntimeException(e);
}
String pageRedirectionData = response.getBody();
System.out.println(pageRedirectionData);
if (response.isSuccess()) {
System.out.println("调用成功");
return pageRedirectionData;
} else {
System.out.println("调用失败");
}
return null;
}
@Override
public void getByOrderId(String orderId) {
AlipayClient alipayClient = new DefaultAlipayClient(PayUtil.Alipay.url,
PayUtil.Alipay.appId,
PayUtil.Alipay.privateKey, "json", "utf-8",
PayUtil.Alipay.alipayPublicKey,
"RSA2");
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
// 设置请求参数
JSONObject jsonObject = new JSONObject();
jsonObject.put("out_trade_no",orderId);
request.setBizContent(jsonObject.toJSONString());
AlipayTradeQueryResponse response = null;
try {
response = alipayClient.execute(request);
} catch (AlipayApiException e) {
throw new RuntimeException(e);
}
if(response.isSuccess()){
String body = response.getBody();
JSONObject jsonObject1 = new JSONObject();
JSONObject jsonObject2 = jsonObject1.parseObject(body);
System.err.println(jsonObject2);
JSONObject alipayTradeQueryResponse = jsonObject2.getJSONObject("alipay_trade_query_response");
System.err.println(alipayTradeQueryResponse);
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
}
}
package com.ruoyi.appointment.service.impl;
@Slf4j
@Service
public class PatientReservationServiceImpl extends ServiceImpl<PatientReservationMapper, PatientReservation> implements PatientReservationService {
//拉起支付
@Override
@Transactional
public String getAlipayPage(PatientReservationVo patientReservationVo) {
//雪花算法生成订单唯一id
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
long id = idWorker.nextId();
patientReservationVo.setOrderId(id+"");
//获取预约的科室
DepartmentType dept = departmentTypeService.getById(patientReservationVo.getDepartmentId());
//构建支付对象
PayMethodDto payMethodDto = new PayMethodDto()
.setOrderId(id+"")
.setPayPrice(patientReservationVo.getPayPrice())
.setDepartmentTitle(dept.getDepartmentTypeName());
//创建订单
this.register(patientReservationVo);
//简单工厂模式拉起预支付
String paymented = SimpleFactory.paymentMethod(patientReservationVo.getPayMethod(), payMethodDto);
return paymented;
}
}
6、策略模式
package com.ruoyi.pay.factory;
import com.ruoyi.pay.domain.dto.PayMethodDto;
import com.ruoyi.pay.service.PayService;
/**
* 策略模式 提供客户端使用
*/
public class PayClient {
private PayService payService;
public PayClient(PayService payService) {
this.payService = payService;
}
/**
* 拉起支付页面
* @param payMethodDto
* @return
*/
public String getPayPage(PayMethodDto payMethodDto){
if(payService!=null){
return payService.getPayPage(payMethodDto);
}
throw new RuntimeException("支付服务不可用");
}
/**
* * 统一收单交易查询
* @param orderId
*/
public void getByOrderId(String orderId){
if(payService!=null){
payService.getByOrderId(orderId);
}
throw new RuntimeException("查询失败");
}
}
7、简单工厂模式
package com.ruoyi.pay.factory;
import com.ruoyi.pay.domain.dto.PayMethodDto;
import com.ruoyi.pay.service.impl.AliPayServiceImpl;
import com.ruoyi.pay.service.impl.WechatServiceImpl;
/**
* 简单工厂模式
*/
public class SimpleFactory {
/**
* 判断支付方式
* @param paymentMethod 0:支付宝 1:微信 2:余额
* @return
*/
public static String paymentMethod(Integer paymentMethod, PayMethodDto payMethodDto){
if( 0 == paymentMethod){ //支付宝
AliPayServiceImpl aliPayService = new AliPayServiceImpl();
return new PayClient(aliPayService).getPayPage(payMethodDto);
}else if(1 == paymentMethod){ //微信
WechatServiceImpl wechatService = new WechatServiceImpl();
return new PayClient(wechatService).getPayPage(payMethodDto);
}
throw new RuntimeException("支付方式不支持");
}
/**
* * 统一收单交易查询
* @param orderId
* @param payType
*/
public static void getByOrderId(String orderId, Long payType){
if( 0 == payType){ //支付宝
AliPayServiceImpl aliPayService = new AliPayServiceImpl();
new PayClient(aliPayService).getByOrderId(orderId);
}else if(1 == payType){ //微信
WechatServiceImpl wechatService = new WechatServiceImpl();
new PayClient(wechatService).getByOrderId(orderId);
}
throw new RuntimeException("查询失败");
}
}
8、controller
@Slf4j
@RestController
@RequestMapping("/pay")
public class PayController {
@Autowired
private PatientReservationService patientReservationService;
/**
* 获取预支付跳转页面 (创建订单)
* @param patientReservationVo
* @return
*/
@PostMapping("/getAlipayPage")
public AjaxResult getAlipayPage(@RequestBody PatientReservationVo patientReservationVo){
return AjaxResult.success( patientReservationService.getAlipayPage(patientReservationVo));
}
/**
* 支付宝回调(支付成功,退款成功) 异步接收
* @param request
* @param response
* @return
*/
@PostMapping("/alicallback")
public void aliCallback(HttpServletRequest request, HttpServletResponse response) throws IOException, AlipayApiException, ParseException {
log.info("回调异步通知");
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<>(8);
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> stringEntry : requestParams.entrySet()) {
String[] values = stringEntry.getValue();
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(stringEntry.getKey(), valueStr);
}
//在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数。(官方解释)
//要删除字段不然会验签失败
params.remove("sign_type");
// boolean signVerified = AlipaySignature.rsaCheckV1(params, PayUtil.Alipay.alipayPublicKey, AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV2(params, PayUtil.Alipay.alipayPublicKey, AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名
System.err.println(signVerified);
PrintWriter out = response.getWriter();
if(!signVerified){
// TODO 验签失败则记录异常日志,并在response中返回failure.
out.write("failure");
log.info("回调异步通知验签失败");
return;
}
// TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
log.info("回调异步通知验签成功");
log.info("进入二次校验");
if(PayStatusEnum.TRADE_SUCCESS.getName().equals(params.get("trade_status"))){ // 交易成功
// 商户订单号(商家定义 订单唯一id)
String out_trade_no = params.get("out_trade_no");
// 支付宝交易号
String trade_no = params.get("trade_no");
// 付款金额
String total_amount = params.get("total_amount");
//支付成功时间
Date pay_success_time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(params.get("gmt_payment"));
//根据订单唯一id查询
PatientReservation patientReservation = patientReservationService.getByOrderId(out_trade_no);
if(patientReservation == null){
out.write("failure");
log.info("订单不存在");
// TODO 验签失败则记录异常日志,并在response中返回failure.
return;
}
//更新状态为已支付
patientReservation.setPayStatus(PayMethodEnum.SCUESS.getCode())
.setPayPrice(new BigDecimal(total_amount))
.setPayTime(pay_success_time)
.setTradeNo(trade_no);
boolean b = patientReservationService.updateById(patientReservation);//更新订单
if(!b){
out.write("failure");
log.info("回调更新失败");
// TODO 验签失败则记录异常日志,并在response中返回failure.
return;
}
log.info("回调更新成功");
out.write("success");
return;
}
out.write("failure");
}
/**
* * 统一收单交易查询
* @param orderId
* @param payType
*/
@GetMapping("/getByOrderId")
public void getByOrderId(String orderId,Long payType) {
SimpleFactory.getByOrderId(orderId,payType);
}
9、vue
getAlipayPage(this.reservationinfo).then((res) => {
console.log(res)
window.open(res.msg,'_ blank') //新窗口打开支付宝预支付页面
});
10、浏览器展示如下
支付成功进入回调