AI摘要
北海のAI

今天想给我之前的项目添加一个支付功能,这里想到了之前用的支付宝沙箱,索性再做个笔记,把自己踩过的坑,还有一些细微的配置,以及前后端 + 支付宝第三方是如何实现真正的联调的,废话不多说,先上个泳道图,让大家先了解一下这里我的逻辑,注意:暂时不考虑到高并发、性能问题

一、流程分析

总体流程我就总结成这个:下单 -> 预支付 -> 唤起收银台 -> 支付 -> 异步/同步回调 -> 前端反馈

%%{init: {'theme': 'base', 'themeVariables': { 'actorBorder': '#D86613', 'actorBkg': '#EBF3FB', 'signalColor': '#333', 'signalTextColor': '#333'}}}%%
sequenceDiagram
    autonumber
    
    %% 定义角色和图标
    actor U as 🦸 用户
    participant F as 📱 前端(App/Web)
    participant B as 🖥️ 后端服务
    participant A as 🔷 支付宝

    %% 阶段一:下单流程
    rect rgb(230, 242, 255)
    note right of U: 🛒 阶段一:创建订单
    U->>F: 点击“购买VIP”
    F->>B: 🚀 发起创建订单请求
    B->>B: ⚙️ 生成订单号 & 记录(Pending)
    end

    %% 阶段二:支付交互
    rect rgb(255, 248, 225)
    note right of U: 💳 阶段二:调用支付
    B->>A: 请求支付参数/预下单
    A-->>B: 返回支付URL 或 HTML表单
    B-->>F: 透传支付信息
    F->>F: 唤起收银台/打开新窗口
    F->>U: 🖼️ 显示支付宝支付页
    U->>A: 💸 确认支付(密码/指纹)
    end

    %% 阶段三:回调与确认
    rect rgb(235, 250, 235)
    note right of U: 🏁 阶段三:结果确认
    
    par 并发处理:异步通知与同步跳转
        A->>B: 🔔 POST 异步通知支付结果
        B->>B: 💾 校验签名并更新订单状态
    and
        A->>U: 🔄 支付成功,跳转同步回调页
        U->>F: 🔙 返回商户前端页面
    end

    loop ⏳ 轮询查单(防止回调延迟)
        F->>B: 🔍 查询订单最新状态
        B-->>F: ✅ 返回状态: [已支付]
    end

    F->>U: 🎉 展示“购买成功”及VIP权益
    end

当前端用户点击购买时候会将jwt令牌以及我这里VIP信息即月卡名称vip_monthly所携带过去,后端接收到这些请求时会根据这些信息生成一张订单表存在数据库中,然后会调用AliPay的SDK,在自己设定参数比如商品名称、价格等之后拼装成一个<form>表单,然后传给前端,前端会根据该表单请求支付宝的沙箱网关生成一个html给前端,然后用户就可以进行扫码或账户+密码进行支付了,在支付完成之后首先支付宝一端会有两个消息,其一:异步回调,会一直发送给公网IP下的一个接口,这个接口是自定义的我在这里直接对订单表进行了修改,修改了订单状态和支付状态都是已支付;其二:同步回调,即这里return-url可以传递本地接口直接跳转到一个新的页面。在整个流程中其实前端只需要使用websocket间隔多少秒去调用查询订单状态的接口即可。


二、前置准备

  1. 支付宝开放平台,选择沙箱应用开发:https://open.alipay.com/develop/sandbox/app

  2. 选择这里的自定义密钥,在本地生成对应的公钥,可以使用 支付宝密钥工具 进行生成

    支付宝

  3. 使用cpolar或者natapp进行内网穿透,放开后端的端口

    1
    cpolar http 8080

三、前端实现

1、用户点击购买

用户点击购买触发按钮事件,此时会创建订单的API,这里也可也进行防范比如误触、多点等限制在一个时间窗口内只能点击1次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// VIP购买弹窗中的购买按钮点击事件
const handleConfirmPurchase = async () => {
processing.value = true
try {
// 调用创建订单API
const response = await vipApi.createOrder({
vipLevel: 'monthly',
paymentMethod: 'alipay'
})

if (response.data.code === 1) {
const paymentData = response.data.data
await handleAlipayPayment(paymentData)
}
} catch (error) {
// 错误处理
} finally {
processing.value = false
}
}

2、创建订单API请求

在触发按钮事件后前端会向后端发送请求,进行创建订单,这一步是用于接收响应然后会在前端进行渲染一个支付宝的支付台

1
2
3
4
5
6
7
8
POST /api/vip/order/create
Content-Type: application/json
Authorization: Bearer ${jwt_token}

{
"vipLevel": "monthly",
"paymentMethod": "alipay"
}

3、前端支付窗口处理

此时前端之前的老窗口需要处于支付等待状态,可以定期去检查支付状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const handleAlipayPayment = async (paymentData) => {
if (paymentData.paymentUrl) {
// URL支付方式
const paymentWindow = window.open(
paymentData.paymentUrl,
'alipay_payment_' + Date.now(),
'width=1200,height=800,scrollbars=yes,resizable=yes,location=yes,menubar=yes,toolbar=yes,status=yes,directories=no'
)

// 设置支付等待状态
waitingForPayment.value = true
paymentStatusText.value = '正在等待您完成支付...'

// 开始定期检查支付状态
startPeriodicStatusCheck(paymentData.orderNo)

} else if (paymentData.paymentHtml) {
// HTML表单支付方式
handleHtmlPayment(paymentData.paymentHtml, paymentData.orderNo)
}
}

四、后端实现

1、application.yml配置

1
2
3
4
5
6
7
alipay:
app-id: 9021000158620xxx # 沙箱应用ID
gatewayUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
private-key: # 沙箱私钥
public-key: # 支付宝公钥
notify-url: https://5506793a.r39.cpolar.top/vip/payment/alipay/notify # 支付宝异步通知必须要是公网IP
return-url: http://localhost:8081/payment/success

2、订单创建阶段

前端携带的用户身份凭证(JWT)及商品标识,后端接收后进行业务校验,并且在数据库中持久化生成一条状态为 “待支付” 的订单记录,在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@PostMapping("/order/create")
public Result createVipOrder(@RequestBody Map<String, Object> params,
@RequestHeader String token) {

try {
Long userId = jwtUtils.getUserIdFromToken(token);
if (userId == null) {
return Result.error("用户身份验证失败");
}

String productId = params.get("productId").toString();
String paymentMethod = params.get("paymentMethod").toString();

VipOrderDTO order = vipService.createVipOrder(userId, productId, paymentMethod);

Map<String, String> paymentParams = Map.of(
"outTradeNo", order.getOrderNo(),
"totalAmount", order.getActualPrice().toString(),
"subject", order.getProductName(),
"body", "开通VIP会员,享受超清画质特权"
);

String paymentUrl = alipayUtil.createPayment(paymentParams);
return Result.success(Map.of(
"orderId", order.getId(),
"orderNo", order.getOrderNo(),
"amount", order.getActualPrice(),
"paymentUrl", paymentUrl
));
} catch (Exception e) {
log.error("创建VIP订单失败", e);
return Result.error("创建订单失败:");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Override
public VipOrderDTO createVipOrder(Long userId, String productId, String paymentMethod) {

Map<String, Object> product = VIP_PRODUCTS.get(productId);
if (product == null) {
throw new RuntimeException("VIP套餐不存在");
}

QueryWrapper<VipOrderDTO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId)
.eq("order_status", 0)
.eq("payment_method", paymentMethod)
.orderByDesc("created_at")
.last("limit 1");

VipOrderDTO existingOrder = vipOrderMapper.selectOne(queryWrapper);
if (existingOrder != null) {
LocalDateTime expiredTime = existingOrder.getCreatedAt().plusMinutes(30);
if (LocalDateTime.now().isBefore(expiredTime)) {
log.info("用户{}已有待支付订单:{}", userId, existingOrder.getOrderNo());
return convertToDTO(existingOrder);
} else {
existingOrder.setOrderStatus(2);
vipOrderMapper.updateById(existingOrder);
}
}

VipOrderDTO order = VipOrderDTO.builder()
.userId(userId)
.orderNo(alipayUtil.generateOrderNo())
.productId(productId)
.productName(product.get("name").toString())
.originalPrice(new BigDecimal(product.get("originalPrice").toString()))
.discountPrice(new BigDecimal(product.get("discountPrice").toString()))
.actualPrice(new BigDecimal(product.get("discountPrice").toString()))
.paymentMethod(paymentMethod)
.orderStatus(0)
.paymentStatus(0)
.validDays(Integer.parseInt(product.get("validDays").toString()))
.build();

vipOrderMapper.insert(order);

log.info("创建VIP订单成功: {}", order.getOrderNo());


return convertToDTO(order);
}

3、支付参数构建

后端集成 Alipay SDK,封装商品名称、金额、订单号等核心参数,调用支付宝的接口生成HTML Form表单(包含签名信息)并响应给前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package com.yyh.common.utils;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.yyh.common.config.AlipayConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Component
@RequiredArgsConstructor
public class AlipayUtil {

private final AlipayConfig alipayConfig;

/**
* 创建支付宝支付
*/
public String createPayment(Map<String, String> params) {
try {
// 初始化AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(
alipayConfig.getGatewayUrl(),
alipayConfig.getAppId(),
alipayConfig.getPrivateKey(),
alipayConfig.getFormat(),
alipayConfig.getCharset(),
alipayConfig.getPublicKey(),
alipayConfig.getSignType()
);

// 创建请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(alipayConfig.getReturnUrl());
request.setNotifyUrl(alipayConfig.getNotifyUrl());

// 设置业务参数
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(params.get("outTradeNo"));
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(params.get("totalAmount"));
model.setSubject(params.get("subject"));
model.setBody(params.get("body"));
request.setBizModel(model);

// 执行请求
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

if (response.isSuccess()) {
return response.getBody();
} else {
log.error("支付宝支付创建失败: {}", response.getMsg());
throw new RuntimeException("支付宝支付创建失败");
}

} catch (AlipayApiException e) {
log.error("支付宝支付异常", e);
throw new RuntimeException("支付宝支付异常");
}
}

/**
* 验证支付宝回调签名
*/
public boolean verifyCallback(Map<String, String> params) {
try {
return AlipaySignature.rsaCheckV1(
params,
alipayConfig.getPublicKey(),
alipayConfig.getCharset(),
alipayConfig.getSignType()
);
} catch (AlipayApiException e) {
log.error("支付宝回调签名验证失败", e);
return false;
}
}

/**
* 生成订单号
*/
public String generateOrderNo() {

String time = new SimpleDateFormat("yyyyMMddHHmmss").format(System.currentTimeMillis());
String randomID = UUID.randomUUID().toString().replace("-", "");
return time + randomID;
}


}

4、唤起收银台

前端将后端返回的Form表单自动提交至支付宝网关,浏览器跳转至支付宝沙箱收银台,用户通过扫码或账户密码完成支付

5、双向回调处理

  1. 异步通知(Notify URL):这是支付结果的唯一可信数据源,支付宝向服务端公网接口发送POST请求,后端在验签通过后,可以在代码层面进一步操作,比如将订单状态幂等的更新呢为“已支付”
  2. 同步跳转(Returen URL):用户在支付成功后,支付宝将浏览器重定向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@PostMapping("/payment/alipay/notify")
@PermitAll()
public String alipayNotify(@RequestParam Map<String, String> params) {

try {
log.info("收到支付宝异步通知: {}", params);

// 验证签名
boolean verifyResult = alipayUtil.verifyCallback(params);
if (!verifyResult) {
log.error("支付宝回调签名验证失败");
return "fail";
}

// 处理支付结果
String tradeStatus = params.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
vipService.handlePaymentSuccess(params);
return "success";
} else {
log.warn("支付宝支付状态异常: {}", tradeStatus);
return "fail";
}

} catch (Exception e) {
log.error("处理支付宝异步通知失败", e);
return "fail";
}

}

五、踩坑点

  • 注意异步回调需要公网IP
  • 拦截器需要放行这个端口