1、mvn坐标和yml配置文件
bash
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.38.10.ALL</version>
</dependency>
bash
# 沙箱控制台地址:https://open.alipay.com/develop/sandbox/app
alipay:
#你的沙箱APPID
app-id: xxxxxx
#你的应用私钥(PKCS8)
merchant-private-key: xxxxxx
#支付宝公钥
alipay-public-key: xxxxxx
#付款成功回调地址
notify-url: http://xxxxxx.com/alipay/notify
#支付完成后跳转的同步页面地址
return-url: http://xxxxxx.com/alipay/return
# 避开周六日测试 ,有可能代码正常但结果异常;
gateway-url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
charset: UTF-8
format: json
sign-type: RSA2
2、controller
bash
package org.example.alipaysandbox;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/alipay")
@RequiredArgsConstructor
public class AliPayController {
private final AlipayClient alipayClient;
private final AliPayConfig aliPayConfig;
/**
* 发起支付(PC网页支付,返回 HTML 表单,浏览器自动跳转)
*/
@GetMapping("/pay")
public void pay(HttpServletResponse response,
@RequestParam String orderId,
@RequestParam String amount,
@RequestParam String subject) throws Exception {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(aliPayConfig.getNotifyUrl());
request.setReturnUrl(aliPayConfig.getReturnUrl());
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderId); // 商户订单号(唯一)
bizContent.put("total_amount", amount); // 金额,如 "0.01"
bizContent.put("subject", subject); // 商品标题
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定值
request.setBizContent(bizContent.toString());
String form = alipayClient.pageExecute(request).getBody();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(form);
response.getWriter().flush();
}
/**
* 异步回调(支付宝主动 POST 通知,必须返回 "success")
*/
@PostMapping("/notify")
public String notifyUrl(HttpServletRequest request) throws Exception {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((k, v) -> params.put(k, v[0]));
// 验签
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
aliPayConfig.getAlipayPublicKey(),
aliPayConfig.getCharset(),
aliPayConfig.getSignType()
);
if (!signVerified) {
return "failure";
}
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no"); // 商户订单号
String tradeNo = params.get("trade_no"); // 支付宝流水号
if ("TRADE_SUCCESS".equals(tradeStatus)) {
// TODO: 更新订单状态为已支付,保存 tradeNo
System.out.println("支付成功,订单号:" + outTradeNo + ",流水号:" + tradeNo);
}
return "success"; // 必须返回 success,否则支付宝会反复通知
}
/**
* 同步回调(用户支付完跳转回来,仅做页面展示,不可作为支付凭证)
*/
@GetMapping("/return")
public String returnUrl(@RequestParam Map<String, String> params) {
return "支付完成,订单号:" + params.get("out_trade_no");
}
@GetMapping("/query")
public String queryOrder(@RequestParam String tradeNo) throws Exception {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", tradeNo);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
return "订单状态:" + response.getTradeStatus() // TRADE_SUCCESS
+ ",金额:" + response.getTotalAmount();
} else {
return "查询失败:" + response.getSubCode() + " - " + response.getSubMsg();
}
}
/**
* 退款
*/
@RequestMapping("/refund")
public String refund(@RequestParam String orderId, @RequestParam String tradeNo,
@RequestParam String refundAmount,
@RequestParam String reason) throws Exception {
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", tradeNo);
bizContent.put("refund_amount", refundAmount);
bizContent.put("refund_reason", reason);
bizContent.put("out_request_no", orderId + "_refund_" + System.currentTimeMillis());
request.setBizContent(bizContent.toString());
AlipayTradeRefundResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
return "退款成功,退款金额:" + response.getRefundFee();
} else {
return "退款失败:" + response.getSubMsg();
}
}
}
@Configuration
@ConfigurationProperties(prefix = "alipay")
@Data
class AliPayConfig {
private String appId;
private String merchantPrivateKey;
private String alipayPublicKey;
private String notifyUrl;
private String returnUrl;
private String gatewayUrl;
private String charset;
private String format;
private String signType;
@Bean
public AlipayClient alipayClient() {
return new DefaultAlipayClient(
gatewayUrl, appId, merchantPrivateKey,
format, charset, alipayPublicKey, signType
);
}
}
浏览器地址栏粘贴测试:
支付
http://xxx.com/alipay/pay?orderId=ORDER_001&amount=0.01&subject=测试商品
查询(把流水号换成 notify 回调拿到的)
http://xxx.com/alipay/query?tradeNo=2026031522001499940508764457
退款 (GET 也能触发,因为你用的是 @RequestMapping)
http://xxx.com/alipay/refund?orderId=ORDER_001&tradeNo=2026031522001499940508764457&refundAmount=0.01&reason=测试退款
流程就是:先访问支付链接 → 沙箱账号付款 → 看后端日志拿 trade_no → 换掉上面查询和退款 URL 里的流水号即可。
拓展web测试界面 :
xxxx.com/test 即可访问测试界面

bash
package org.example.alipaysandbox;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/test")
public class AliPayTestController {
@GetMapping
public void testPanel(HttpServletResponse response) throws Exception {
response.setContentType("text/html;charset=UTF-8");
String html = """
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付宝沙箱测试台</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=JetBrains+Mono:wght@400;600&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--blue: #1677ff;
--blue2: #0958d9;
--green: #00b96b;
--orange: #fa8c16;
--red: #ff4d4f;
--bg: #0a0c10;
--bg2: #111318;
--bg3: #1a1d24;
--border: rgba(255,255,255,0.08);
--text: #e8eaf0;
--muted: #6b7280;
--mono: 'JetBrains Mono', monospace;
--sans: 'Noto Sans SC', sans-serif;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
min-height: 100vh;
padding: 40px 24px 80px;
}
/* ── header ── */
.header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 48px;
}
.logo {
width: 42px; height: 42px;
background: var(--blue);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 22px;
}
.header h1 { font-size: 22px; font-weight: 700; letter-spacing: -.3px; }
.header p { font-size: 13px; color: var(--muted); margin-top: 2px; }
.badge {
margin-left: auto;
background: rgba(0,185,107,.12);
border: 1px solid rgba(0,185,107,.3);
color: var(--green);
font-size: 11px; font-weight: 600;
padding: 3px 10px; border-radius: 20px;
letter-spacing: .5px;
}
/* ── grid ── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
max-width: 1100px;
margin: 0 auto;
}
/* ── card ── */
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: border-color .2s, transform .15s;
}
.card:hover { border-color: rgba(255,255,255,.18); transform: translateY(-2px); }
.card-header {
display: flex; align-items: center; gap: 10px;
padding: 18px 20px 14px;
border-bottom: 1px solid var(--border);
}
.card-icon {
width: 34px; height: 34px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px; flex-shrink: 0;
}
.card-icon.pay { background: rgba(22,119,255,.15); }
.card-icon.refund { background: rgba(250,140,22,.15); }
.card-icon.query { background: rgba(0,185,107,.15); }
.card-title { font-size: 15px; font-weight: 600; }
.card-method {
margin-left: auto;
font-family: var(--mono);
font-size: 10px; font-weight: 600;
padding: 2px 8px; border-radius: 4px;
letter-spacing: .3px;
}
.method-get { background: rgba(0,185,107,.12); color: var(--green); border: 1px solid rgba(0,185,107,.25); }
.method-post { background: rgba(22,119,255,.12); color: var(--blue); border: 1px solid rgba(22,119,255,.25); }
.card-body { padding: 16px 20px 20px; }
/* ── form ── */
.field { margin-bottom: 12px; }
.field label {
display: block;
font-size: 11px; font-weight: 600;
color: var(--muted);
text-transform: uppercase; letter-spacing: .8px;
margin-bottom: 5px;
}
.field input {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 12px;
font-size: 13px;
font-family: var(--mono);
color: var(--text);
outline: none;
transition: border-color .15s;
}
.field input:focus { border-color: rgba(22,119,255,.5); }
.field input::placeholder { color: #3d4148; }
/* ── button ── */
.btn {
width: 100%; padding: 11px;
border: none; border-radius: 8px;
font-family: var(--sans);
font-size: 14px; font-weight: 600;
cursor: pointer;
margin-top: 6px;
transition: opacity .15s, transform .1s;
position: relative; overflow: hidden;
}
.btn:hover { opacity: .88; }
.btn:active { transform: scale(.98); }
.btn-pay { background: var(--blue); color: #fff; }
.btn-refund { background: var(--orange); color: #fff; }
.btn-query { background: var(--green); color: #fff; }
/* ── result box ── */
.result {
margin-top: 14px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
font-family: var(--mono);
font-size: 12px;
line-height: 1.7;
color: var(--muted);
min-height: 48px;
word-break: break-all;
white-space: pre-wrap;
display: none;
}
.result.visible { display: block; }
.result.ok { border-color: rgba(0,185,107,.3); color: #6ee7b7; }
.result.err { border-color: rgba(255,77,79,.3); color: #fca5a5; }
.result.info { border-color: rgba(22,119,255,.3); color: #93c5fd; }
/* spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
display: inline-block;
width: 13px; height: 13px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .6s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
/* ── log panel ── */
.log-wrap {
max-width: 1100px; margin: 32px auto 0;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 16px; overflow: hidden;
}
.log-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border);
}
.log-title { font-size: 13px; font-weight: 600; color: var(--muted); letter-spacing: .5px; }
.log-clear {
font-size: 11px; color: var(--muted); cursor: pointer;
background: none; border: none; font-family: var(--sans);
padding: 2px 6px; border-radius: 4px;
}
.log-clear:hover { color: var(--red); }
.log-body {
padding: 14px 20px;
font-family: var(--mono);
font-size: 12px; line-height: 1.9;
min-height: 80px;
max-height: 280px; overflow-y: auto;
color: var(--muted);
}
.log-entry { display: flex; gap: 10px; }
.log-time { color: #3d4148; flex-shrink: 0; }
.log-ok { color: var(--green); }
.log-err { color: var(--red); }
.log-info { color: var(--blue); }
/* ── footer tip ── */
.tip {
max-width: 1100px; margin: 20px auto 0;
font-size: 12px; color: #3d4148;
text-align: center; line-height: 1.8;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">💳</div>
<div>
<h1>支付宝沙箱测试台</h1>
<p>alipay-sandbox · localhost:8080</p>
</div>
<span class="badge">SANDBOX</span>
</div>
<div class="grid">
<!-- ── 支付 ── -->
<div class="card">
<div class="card-header">
<div class="card-icon pay">💰</div>
<span class="card-title">发起支付</span>
<span class="card-method method-get">GET</span>
</div>
<div class="card-body">
<div class="field">
<label>订单号</label>
<input id="pay-orderId" value="ORDER_001" placeholder="ORDER_xxx">
</div>
<div class="field">
<label>金额(元)</label>
<input id="pay-amount" value="0.01" placeholder="0.01">
</div>
<div class="field">
<label>商品名称</label>
<input id="pay-subject" value="测试商品" placeholder="商品名称">
</div>
<button class="btn btn-pay" onclick="doPay()">跳转支付页面</button>
<div id="pay-result" class="result"></div>
</div>
</div>
<!-- ── 退款 ── -->
<div class="card">
<div class="card-header">
<div class="card-icon refund">↩️</div>
<span class="card-title">申请退款</span>
<span class="card-method method-post">POST</span>
</div>
<div class="card-body">
<div class="field">
<label>订单号</label>
<input id="ref-orderId" placeholder="ORDER_xxx">
</div>
<div class="field">
<label>支付宝流水号 trade_no</label>
<input id="ref-tradeNo" placeholder="2026031522001xxxxxxxx">
</div>
<div class="field">
<label>退款金额(元)</label>
<input id="ref-amount" value="0.01" placeholder="0.01">
</div>
<div class="field">
<label>退款原因</label>
<input id="ref-reason" value="测试退款" placeholder="退款原因">
</div>
<button class="btn btn-refund" onclick="doRefund()">申请退款</button>
<div id="ref-result" class="result"></div>
</div>
</div>
<!-- ── 查询 ── -->
<div class="card">
<div class="card-header">
<div class="card-icon query">🔍</div>
<span class="card-title">订单查询</span>
<span class="card-method method-get">GET</span>
</div>
<div class="card-body">
<div class="field">
<label>支付宝流水号 trade_no</label>
<input id="qry-tradeNo" placeholder="2026031522001xxxxxxxx">
</div>
<button class="btn btn-query" onclick="doQuery()">查询订单</button>
<div id="qry-result" class="result"></div>
</div>
</div>
</div>
<!-- ── 操作日志 ── -->
<div class="log-wrap">
<div class="log-header">
<span class="log-title">操作日志</span>
<button class="log-clear" onclick="clearLog()">清空</button>
</div>
<div class="log-body" id="log-body">
<div class="log-entry"><span class="log-time">--:--:--</span><span class="log-info">等待操作...</span></div>
</div>
</div>
<p class="tip">
支付成功后,将 notify 回调日志中的 <code>trade_no</code> 填入退款 / 查询框即可一键测试全流程<br>
沙箱每周日维护,退款如遇 SYSTEM_ERROR 属正常现象,周一后重试
</p>
<script>
const base = '';
function ts() {
return new Date().toTimeString().slice(0,8);
}
function log(msg, type='info') {
const body = document.getElementById('log-body');
const el = document.createElement('div');
el.className = 'log-entry';
el.innerHTML = `<span class="log-time">${ts()}</span><span class="log-${type}">${msg}</span>`;
body.appendChild(el);
body.scrollTop = body.scrollHeight;
}
function clearLog() {
document.getElementById('log-body').innerHTML = '';
}
function setResult(id, text, type) {
const el = document.getElementById(id);
el.textContent = text;
el.className = `result visible ${type}`;
}
function setLoading(btnEl, loading) {
if (loading) {
btnEl._orig = btnEl.innerHTML;
btnEl.innerHTML = '<span class="spinner"></span>请求中...';
btnEl.disabled = true;
} else {
btnEl.innerHTML = btnEl._orig || btnEl.innerHTML;
btnEl.disabled = false;
}
}
function doPay() {
const orderId = document.getElementById('pay-orderId').value.trim();
const amount = document.getElementById('pay-amount').value.trim();
const subject = document.getElementById('pay-subject').value.trim();
if (!orderId || !amount || !subject) {
setResult('pay-result', '请填写全部字段', 'err');
return;
}
log(`发起支付 → orderId=${orderId} amount=${amount}`);
setResult('pay-result', '正在跳转支付页面...', 'info');
const url = `${base}/alipay/pay?orderId=${encodeURIComponent(orderId)}&amount=${encodeURIComponent(amount)}&subject=${encodeURIComponent(subject)}`;
window.open(url, '_blank');
log('支付页面已在新标签页打开,请用沙箱买家账号完成付款', 'ok');
}
async function doRefund() {
const btn = document.querySelector('.btn-refund');
const orderId = document.getElementById('ref-orderId').value.trim();
const tradeNo = document.getElementById('ref-tradeNo').value.trim();
const amount = document.getElementById('ref-amount').value.trim();
const reason = document.getElementById('ref-reason').value.trim();
if (!orderId || !tradeNo || !amount) {
setResult('ref-result', '请填写订单号、流水号和退款金额', 'err');
return;
}
setLoading(btn, true);
log(`申请退款 → tradeNo=${tradeNo} amount=${amount}`);
try {
const url = `${base}/alipay/refund?orderId=${encodeURIComponent(orderId)}&tradeNo=${encodeURIComponent(tradeNo)}&refundAmount=${encodeURIComponent(amount)}&reason=${encodeURIComponent(reason)}`;
const res = await fetch(url, { method: 'POST' });
const text = await res.text();
const ok = text.includes('成功');
setResult('ref-result', text, ok ? 'ok' : 'err');
log(text, ok ? 'ok' : 'err');
} catch(e) {
setResult('ref-result', '请求失败:' + e.message, 'err');
log('退款请求失败:' + e.message, 'err');
}
setLoading(btn, false);
}
async function doQuery() {
const btn = document.querySelector('.btn-query');
const tradeNo = document.getElementById('qry-tradeNo').value.trim();
if (!tradeNo) {
setResult('qry-result', '请填写流水号', 'err');
return;
}
setLoading(btn, true);
log(`查询订单 → tradeNo=${tradeNo}`);
try {
const res = await fetch(`${base}/alipay/query?tradeNo=${encodeURIComponent(tradeNo)}`);
const text = await res.text();
const ok = text.includes('SUCCESS') || text.includes('成功');
setResult('qry-result', text, ok ? 'ok' : 'err');
log(text, ok ? 'ok' : 'info');
// 自动填入退款框
if (ok) {
document.getElementById('ref-tradeNo').value = tradeNo;
log('已自动填入退款流水号', 'info');
}
} catch(e) {
setResult('qry-result', '请求失败:' + e.message, 'err');
log('查询请求失败:' + e.message, 'err');
}
setLoading(btn, false);
}
</script>
</body>
</html>
""";
response.getWriter().write(html);
response.getWriter().flush();
}
}