两步实现支付宝沙箱

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();
    }
}
相关推荐
Stream_Silver2 小时前
【系统架构设计师】第一章 计算机硬件 1.1 计算机硬件组成
笔记·硬件架构
山川行2 小时前
Git学习笔记:Git进阶操作
笔记·git·vscode·学习·编辑器·visual studio code
CDN3602 小时前
运维笔记|360CDN高防服务器部署教程,抗D+源站防护一站式配置
运维·服务器·笔记
左左右右左右摇晃2 小时前
Java Object 类笔记
java·笔记
橘bird2 小时前
LangChain1.2 学习笔记(自用)(未完结)
笔记·python·学习·langchain
智者知已应修善业2 小时前
【任何一个自然数m的立方均可写成m个连续奇数之和】2024-10-17
c语言·数据结构·c++·经验分享·笔记·算法
吃着火锅x唱着歌2 小时前
PHP7内核剖析 学习笔记 第十章 扩展开发(3)
java·笔记·学习
猹叉叉(学习版)2 小时前
【ASP.NET CORE】 11. SignalR
笔记·后端·c#·asp.net·.netcore