支付宝沙箱版(什么是支付宝沙箱、配置支付宝沙箱、配置内网穿透、在SpringBoot项目中对接支付宝沙箱、前端代码、支付宝沙箱的退款操作、可能遇到的问题、完整的示例代码)

0. 前言

很多项目都需要有支付功能,但是对接第三方支付又需要企业资质,作为个人开发者,达不到这个条件,但支付功能是项目中一个很重要的的部分,面试官也比较喜欢问,如果面试官问与支付功能的相关的问题时,我们说因为没有企业资质,跳过了支付功能,那就错失了一次在面试官面前表现自己的机会

今天为大家介绍的支付宝沙箱正是为了解决这一痛点,通过使用支付宝沙箱,我们就能够在项目中模拟真正的支付,让我们的项目锦上添花(使用支付宝沙箱无需企业资质,只需要一个支付宝账号)

1. 什么是支付宝沙箱

支付宝沙箱(Alipay Sandbox)是支付宝为开发者提供的一个模拟真实支付宝环境的测试平台。开发者可以在沙箱环境中进行应用程序的开发和测试,无需使用真实的支付宝账户进行交易


以下是支付宝沙箱的一些特点和用途:

  1. 安全隔离:沙箱环境与生产环境是隔离的,开发者可以在不影响真实用户数据的情况下进行测试
  2. 模拟交易:沙箱允许开发者模拟各种交易场景,如支付、退款、转账等,以便验证应用程序的功能
  3. 测试账号:支付宝沙箱提供测试用的商户ID和应用ID,以及模拟的买家和卖家账户,用于测试支付流程
  4. API调用:开发者可以使用沙箱环境中的API进行接口调用,测试接口的响应和数据返回是否符合预期
  5. 问题排查:在沙箱中测试可以帮助开发者发现和解决在集成支付宝支付时可能出现的问题
  6. 文档和工具:支付宝沙箱提供详细的文档和测试工具,帮助开发者更快地熟悉和掌握支付宝API的使用

支付宝沙箱对于希望集成支付宝支付服务的开发者来说是一个非常有用的工具,它能够帮助开发者在不影响实际业务的情况下,高效、安全地完成支付功能的开发和测试工作

2. 配置支付宝沙箱

支付宝沙箱官网:沙箱应用 - 开放平台 (打开官网后需要用真实的支付宝账号进行登录)

2.1 沙箱应用的应用信息(获取app-id和gateway-url)

APPID支付宝网关地址 字段下面会用到

在这里插入图片描述

2.2 沙箱账号的商家信息和买家信息

买家账号在付款时会用到

在这里插入图片描述

2.3 下载秘钥工具

下载地址:密钥工具下载 - 支付宝文档中心 (alipay.com)

在这里插入图片描述

请不要安装在含有空格的目录路径下,否则会导致公私钥乱码的问题

2.4 生成秘钥(获取private-key)

在这里插入图片描述

秘钥生成成功的界面(应用私钥对应 private-key)

在这里插入图片描述

2.5 配置秘钥(获取alipay-public-key)

配置地址:沙箱应用 - 开放平台 (alipay.com)


点击自定义秘钥,再点击公钥模式的设置并查看

在这里插入图片描述

将刚才生成的应用公钥复制过来,接着点击保存按钮

在这里插入图片描述

最后点击确定按钮

在这里插入图片描述

将得到的支付宝公钥保存下来(支付宝公钥对应 alipay-public-key)

3. 配置内网穿透

3.1 使用cpolar实现内网穿透

我们使用 cpolar 来实现内网穿透,cpolar 的使用教程可参考我的另一篇博文:使用cpolar实现内网穿透,将Web项目部署到公网上

3.2 创建隧道(获取notify-url)

在浏览器输入以下网址,创建隧道

http://localhost:9200/#/tunnels/create

在这里插入图片描述

创建隧道后会有一个公网地址,下面会用到

在这里插入图片描述

cpolar 生成的公网地址在电脑重启之后可能会发生变化

4. 在SpringBoot项目中如何对接支付宝沙箱

本次演示的后端环境为:JDK 17.0.7 + SpringBoot 3.0.2

4.1 引入依赖(支付宝文档中心)

在项目的 pom.xml 文件中引入依赖(本次演示使用的是较新的版本,支付宝文档中心的版本是 4.34.0.ALL)

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.39.218.ALL</version>
</dependency>

完整的官方文档:通用版 - 支付宝文档中心 (alipay.com)


为了方便测试,我们额外引入 Spring Web 和 fastjson2 的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.53</version>
</dependency>

4.2 编写实体类

我们编写一个简单的实体类,用于测试

/**
 * 订单类
 */
public class Order {
    
    

    /**
     * 用于唯一标识一次支付请求,可以是订单号或与其他业务相关的唯一标识
     */
    private Long id;

    /**
     * 支付的总金额
     */
    private String totalAmount;

    /**
     * 支付时显示的商品描述
     */
    private String productDescription;

    /**
     * 支付时显示的商品名称
     */
    private String productName;

    public Long getId() {
    
    
        return id;
    }

    public void setId(Long id) {
    
    
        this.id = id;
    }

    public String getTotalAmount() {
    
    
        return totalAmount;
    }

    public void setTotalAmount(String totalAmount) {
    
    
        this.totalAmount = totalAmount;
    }

    public String getProductDescription() {
    
    
        return productDescription;
    }

    public void setProductDescription(String productDescription) {
    
    
        this.productDescription = productDescription;
    }

    public String getProductName() {
    
    
        return productName;
    }

    public void setProductName(String productName) {
    
    
        this.productName = productName;
    }

    @Override
    public String toString() {
    
    
        return "Order{" +
                "id=" + id +
                ", totalAmount='" + totalAmount + '\'' +
                ", productDescription='" + productDescription + '\'' +
                ", productName='" + productName + '\'' +
                '}';
    }

}

4.3 编写配置属性类

AlipayConfiguration.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfiguration {
    
    

    private String appId;

    private String gatewayUrl;

    private String privateKey;

    private String alipayPublicKey;

    private String notifyUrl;

    private String returnUrl;

    public String getAppId() {
    
    
        return appId;
    }

    public void setAppId(String appId) {
    
    
        this.appId = appId;
    }

    public String getGatewayUrl() {
    
    
        return gatewayUrl;
    }

    public void setGatewayUrl(String gatewayUrl) {
    
    
        this.gatewayUrl = gatewayUrl;
    }

    public String getPrivateKey() {
    
    
        return privateKey;
    }

    public void setPrivateKey(String privateKey) {
    
    
        this.privateKey = privateKey;
    }

    public String getAlipayPublicKey() {
    
    
        return alipayPublicKey;
    }

    public void setAlipayPublicKey(String alipayPublicKey) {
    
    
        this.alipayPublicKey = alipayPublicKey;
    }

    public String getNotifyUrl() {
    
    
        return notifyUrl;
    }

    public void setNotifyUrl(String notifyUrl) {
    
    
        this.notifyUrl = notifyUrl;
    }

    public String getReturnUrl() {
    
    
        return returnUrl;
    }

    public void setReturnUrl(String returnUrl) {
    
    
        this.returnUrl = returnUrl;
    }

    @Override
    public String toString() {
    
    
        return "AlipayConfiguration{" +
                "appId='" + appId + '\'' +
                ", gatewayUrl='" + gatewayUrl + '\'' +
                ", privateKey='" + privateKey + '\'' +
                ", alipayPublicKey='" + alipayPublicKey + '\'' +
                ", notifyUrl='" + notifyUrl + '\'' +
                ", returnUrl='" + returnUrl + '\'' +
                '}';
    }

}

4.4 编写controller(设置notify-url和return-url)

AlipayController.java

import cn.edu.scau.config.AlipayConfiguration;
import cn.edu.scau.pojo.Order;
import com.alibaba.fastjson2.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.diagnosis.DiagnosisUtils;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static com.alipay.api.AlipayConstants.*;

@Controller
@RequestMapping("/alipay")
public class AlipayController {
    
    

    private final AlipayConfiguration alipayConfiguration;

    public AlipayController(AlipayConfiguration alipayConfiguration) {
    
    
        this.alipayConfiguration = alipayConfiguration;
    }

    @GetMapping("/pay")
    public void pay(@RequestParam Long orderId, HttpServletResponse httpResponse) throws IOException {
    
    
        // 创建默认的支付宝客户端实例
        AlipayClient alipayClient = new DefaultAlipayClient(
                alipayConfiguration.getGatewayUrl(),
                alipayConfiguration.getAppId(),
                alipayConfiguration.getPrivateKey(),
                FORMAT_JSON,
                CHARSET_UTF8,
                alipayConfiguration.getAlipayPublicKey(),
                SIGN_TYPE_RSA2
        );

        AlipayTradePagePayRequest alipayTradePagePayRequest = new AlipayTradePagePayRequest();
        // 设置异步通知地址
        alipayTradePagePayRequest.setNotifyUrl(alipayConfiguration.getNotifyUrl());
        // 设置重定向地址
        alipayTradePagePayRequest.setReturnUrl(alipayConfiguration.getReturnUrl());

        Order order = new Order();
        order.setId(orderId); // 商户订单号
        order.setProductName("爱国者键盘"); // 商品名称
        order.setProductDescription("键盘中的战斗机"); // 商品描述
        order.setTotalAmount("0.01"); // 付款金额,必填

        // 构造业务请求参数(如果是电脑网页支付,product_code是必传参数)
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("out_trade_no", order.getId());
        jsonObject.put("subject", order.getProductName());
        jsonObject.put("body", order.getProductDescription());
        jsonObject.put("total_amount", order.getTotalAmount());
        jsonObject.put("product_code", "FAST_INSTANT_TRADE_PAY");

        alipayTradePagePayRequest.setBizContent(jsonObject.toJSONString());

        // 请求支付宝接口
        String alipayForm;
        try {
    
    
            alipayForm = alipayClient.pageExecute(alipayTradePagePayRequest).getBody();
        } catch (AlipayApiException e) {
    
    
            throw new RuntimeException(e);
        }

        // 将表单直接输出到页面,用户点击后会跳转到支付宝支付页面
        httpResponse.setContentType("text/html;charset=" + CHARSET_UTF8);
        httpResponse.getWriter().write(alipayForm);
        httpResponse.getWriter().flush();
        httpResponse.getWriter().close();
    }

    /**
     * 处理支付宝异步通知的接口(注意这里必须是POST接口)
     *
     * @param request HTTP请求对象,包含支付宝返回的通知信息
     * @return 返回给支付宝的确认信息
     */
    @PostMapping("/notify")
    @ResponseBody
    public String payNotify(HttpServletRequest request) {
    
    
        System.err.println("====================payNotify====================");

        // 获取支付宝返回的各个参数
        Map<String, String[]> requestParameterMap = request.getParameterMap();
        requestParameterMap.forEach((key, value) -> System.err.println(key + " = " + Arrays.toString(value)));

        // 检查交易状态是否为成功
        if ("TRADE_SUCCESS".equals(request.getParameter("trade_status"))) {
    
    
            System.err.println("交易成功");

            // 执行更新数据库中的订单状态等操作
        }

        System.err.println("====================payNotify====================");
        System.err.println();

        // 告诉支付宝,已经成功收到异步通知
        return "success";
    }

    @GetMapping("/return")
    public void payReturn(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
        System.out.println("====================payReturn====================");

        // 获取支付宝返回的各个参数
        Map<String, String[]> requestParameterMap = request.getParameterMap();
        System.out.println("支付宝返回的参数:");
        requestParameterMap.forEach((key, value) -> System.out.println(key + " = " + Arrays.toString(value)));

        System.out.println("====================payReturn====================");
        System.out.println();

        // 设置响应的字符编码为UTF-8,防止中文乱码
        response.setCharacterEncoding("UTF-8");

        // 设置响应的内容类型为HTML,并指定字符编码为UTF-8
        response.setContentType("text/html; charset=UTF-8");

        // 重定向到指定的HTML页面(需要提前与前端沟通好支付成功后要跳转的页面)
        response.sendRedirect("http://localhost:5173/paySuccess");
    }

    @GetMapping("/refund")
    @ResponseBody
    public String refund(@RequestParam(required = false) Long id, @RequestParam(required = false) String tradeNo) {
    
    
        System.out.println("====================refund====================");

        // 创建默认的支付宝客户端实例
        AlipayClient alipayClient = new DefaultAlipayClient(
                alipayConfiguration.getGatewayUrl(),
                alipayConfiguration.getAppId(),
                alipayConfiguration.getPrivateKey(),
                FORMAT_JSON,
                CHARSET_UTF8,
                alipayConfiguration.getAlipayPublicKey(),
                SIGN_TYPE_RSA2
        );

        Order order = new Order();
        order.setId(id);
        order.setProductName("爱国者键盘");
        order.setProductDescription("键盘中的战斗机");
        order.setTotalAmount("0.01");

        AlipayTradeRefundRequest alipayTradeRefundRequest = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel alipayTradeRefundModel = new AlipayTradeRefundModel();

        // 设置商户订单号
        if (id != null) {
    
    
            alipayTradeRefundModel.setOutTradeNo(String.valueOf(order.getId()));
        }

        // 设置查询选项
        List<String> queryOptions = new ArrayList<>();
        queryOptions.add("refund_detail_item_list");
        alipayTradeRefundModel.setQueryOptions(queryOptions);
        // 设置退款金额
        alipayTradeRefundModel.setRefundAmount("0.01");
        // 设置退款原因说明
        alipayTradeRefundModel.setRefundReason("正常退款");
        // 设置流水订单号
        if (tradeNo != null && !tradeNo.isEmpty()) {
    
    
            alipayTradeRefundModel.setTradeNo(tradeNo);
        }

        alipayTradeRefundRequest.setBizModel(alipayTradeRefundModel);
        // 执行请求
        AlipayTradeRefundResponse alipayTradeRefundResponse = null;

        int count = 1;
        int limit = 3;
        while (count <= limit) {
    
     // 最多重试3次
            try {
    
    
                alipayTradeRefundResponse = alipayClient.execute(alipayTradeRefundRequest);
                break;
            } catch (AlipayApiException e) {
    
    
                System.out.println("第" + count + "次重试");
                e.printStackTrace();
                count++;
            }
        }

        if (count > limit || alipayTradeRefundResponse == null) {
    
    
            return "退款失败,请联系客服人员";
        }

        System.out.println("alipayTradeRefundResponse.getBody() = " + alipayTradeRefundResponse.getBody());
        System.out.println("====================refund====================");
        System.out.println();

        if (!alipayTradeRefundResponse.isSuccess()) {
    
    
            // SDK版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
            String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(alipayTradeRefundResponse);
            System.out.println("diagnosisUrl = " + diagnosisUrl);

            return "退款失败,请联系客服人员";
        }

        if ("N".equals(alipayTradeRefundResponse.getFundChange())) {
    
    
            return "该订单已经退款,请勿重复操作";
        }

        return "退款成功";
    }

}

需要提前与前端沟通好支付成功后要跳转的页面

4.5 编写配置文件

SpringBoot 应用程序的端口号需要与内网穿透中隧道的端口号保持一致

application.yml

alipay:
  # app-id:支付宝开放平台中的APPID
  app-id: 
  # gateway-url:支付宝开放平台中的支付宝网关地址
  gateway-url: 
  # private-key:在支付宝开放平台秘钥工具中生成的应用私钥
  private-key: 
  # alipay-public-key:支付宝开放平台中的支付宝公钥
  alipay-public-key: 
  # notify-url:支付宝通过该URL通知交易状态变化
  notify-url: 
  # return-url:用户完成支付后,支付宝会重定向到该URL
  return-url: http://localhost:10005/alipay/return

server:
  port: 10005

cpolar 生成的公网地址在电脑重启之后可能会发生变化,需要更改 notify-url 属性

5. 前端代码

本次演示使用的是由 Vue3 搭建的工程

5.1 vite.config.js

import {
    
    fileURLToPath, URL} from 'node:url'

import {
    
    defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
    
    
  plugins: [
    vue()
  ],
  resolve: {
    
    
    alias: {
    
    
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    
    
    proxy: {
    
    
      '/api': {
    
    
        target: 'http://localhost:10005',
        changeOrigin: true,
        rewrite: (path) => path.replace('/api', '')
      }
    }
  }
})

关键代码

在这里插入图片描述

5.2 src/utils/request.js

import axios from 'axios'

const request = axios.create({
    
    
  baseURL: '/api',
  timeout: 20000,
  headers: {
    
    
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

request.interceptors.request.use(

)

request.interceptors.response.use(response => {
    
    
  if (response.data) {
    
    
    return response.data
  }
  return response
}, (error) => {
    
    
  // Do something with response error
  return Promise.reject(error)
})

export default request

5.3 src/router/index.js

import {
    
    createRouter, createWebHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
    
    
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
    
    
      path: '/',
      name: 'home',
      component: HomeView
    }, {
    
    
      path: '/paySuccess',
      name: 'paySuccess',
      component: () => import('../views/PaySuccess.vue')
    }
  ]
})

export default router

5.4 src/views/HomeView.vue

<template>
  <div class="payment-container">
    <input type="number" v-model="orderId" placeholder="订单号" class="input-field">
    <button class="pay-button" @click="pay">支付</button>
    <div ref="alipayForm" class="hidden-form"></div>
  </div>
</template>

<script setup>
import request from '@/utils/request.js'
import {
      
       ref } from 'vue'

const alipayForm = ref()
const orderId = ref('')

const pay = () => {
      
      
  request
      .get('/alipay/pay', {
      
      
        params: {
      
      
          orderId: orderId.value
        }
      })
      .then((response) => {
      
      
        let innerHtml = response.toString()
        innerHtml = innerHtml.substring(0, innerHtml.indexOf('<script>'))
        alipayForm.value.innerHTML = innerHtml
        window.document.forms[0].submit()
      })
      .catch((error) => {
      
      
        console.error('支付请求错误:', error)
      })
}
</script>

<style scoped>
.payment-container {
      
      
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background-color: #f0f8ff; /* 淡蓝色背景 */
  font-family: Arial, sans-serif;
}

.input-field {
      
      
  width: 300px; /* 输入框宽度 */
  margin: 10px 0; /* 上下间距 */
  padding: 15px; /* 内边距 */
  border: 2px solid #c7e0ff; /* 边框颜色 */
  border-radius: 5px; /* 圆角 */
  outline: none; /* 去除默认轮廓 */
  font-size: 16px; /* 字体大小 */
  transition: border-color 0.3s; /* 平滑过渡效果 */
}

.input-field:focus {
      
      
  border-color: #007bff; /* 聚焦时的边框颜色 */
}

.pay-button {
      
      
  width: 333px; /* 按钮宽度比输入框略长 */
  margin: 10px 0; /* 上下间距 */
  padding: 15px; /* 内边距 */
  border: none; /* 无边框 */
  border-radius: 5px; /* 圆角 */
  background-color: #007bff; /* 按钮颜色 */
  color: white; /* 文字颜色 */
  cursor: pointer; /* 鼠标指针样式 */
  font-size: 16px; /* 字体大小 */
  transition: background-color 0.3s; /* 平滑过渡效果 */
}

.pay-button:hover {
      
      
  background-color: #0056b3; /* 悬停时更深的颜色 */
}

.hidden-form {
      
      
  display: none; /* 隐藏表单 */
}
</style>

关键代码如下

在这里插入图片描述

5.5 src/views/paySuccess.vue

<template>
  <div class="success-container">
    <div class="success-icon">
      <svg width="100" height="100" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
            d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"
            fill="#4CAF50"/>
      </svg>
    </div>
    <h1>支付成功</h1>
    <p class="success-message">您已完成支付,感谢您的使用</p>

    <button class="btn" @click="returnHomePage">返回首页</button>
  </div>
</template>

<script setup>
import router from '@/router/index.js'

const returnHomePage = () => {
      
      
  router.push('/')
}
</script>

<style scoped>
.success-container {
      
      
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f4f8fa; /* 柔和的背景色调 */
}

.success-icon {
      
      
  margin-bottom: 20px;
}

.success-icon svg {
      
      
  fill: #4CAF50; /* 柔和的绿色调 */
}

h1 {
      
      
  font-size: 2.5rem;
  color: #333; /* 深色调的文字 */
  margin: 0;
  font-weight: 500;
}

.success-message {
      
      
  font-size: 1.25rem;
  color: #666; /* 稍浅的文字颜色 */
  text-align: center;
  max-width: 300px;
  margin-top: 10px;
}

.btn {
      
      
  display: inline-block;
  padding: 14px 40px; /* 增加按钮的宽度和内边距 */
  margin-top: 20px;
  font-size: 1rem;
  font-weight: 500;
  color: #333; /* 深色调文字,确保在淡色背景下可见 */
  background-image: linear-gradient(to right, #639cbc, #1c67a8); /* 更淡的渐变色 */
  border: none;
  border-radius: 25px;
  box-shadow: 0 2px 4px rgba(50, 50, 93, 0.1), 0 1px 2px rgba(0, 0, 0, 0.08);
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn:hover {
      
      
  transform: translateY(-2px);
  box-shadow: 0 4px 6px rgba(50, 50, 93, 0.1), 0 2px 4px rgba(0, 0, 0, 0.08);
}

.btn:focus {
      
      
  outline: none;
  box-shadow: 0 0 0 3px rgba(200, 200, 200, 0.5);
}
</style>

6. 支付宝沙箱成功支付的整个过程

  • 可以使用账号密码付款,也可以扫描付款
  • 建议使用账号密码付款,使用扫描支付有时候会出现成功支付后页面没有发生跳转的情况

以下是成功支付的整个过程的截图

在这里插入图片描述

支付密码默认是111111

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

支付成功后支付宝会返回一个流水订单号(trade_no),退款时需要用到流水订单号,可以将流水订单号保存到数据库中

7. 支付宝沙箱的退款操作

  • 退款可以使用商户订单号(out_trade_no),也可以使用支付宝返回的流水订单号(trade_no)
  • 可以多次执行退款操作,但是账号余额不会发生变化如果是第一次退款,AlipayTradeRefundResponse 的 fundChange 属性取值为 “Y”,如果不是第一次退款,AlipayTradeRefundResponse 的 fundChange 字段的取值为 “N”,可以根据 fundChange 字段完善业务逻辑

在实际开发中,与商品有关的信息是需要从后端查出来的,为了方便测试,由前端传给后端(退款的具体代码参考本文的编写controller(设置notify-url和return-url)部分)

在这里插入图片描述

8. 可能遇到的问题

8.1 504 Gateway Time-out

出现这种情况大概率是因为支付宝沙箱处于维护状态(常出现于节假日),可以过一段时间再进行尝试

在这里插入图片描述

The gateway did not receive a timely response from the upstream server or application. Sorry for the inconvenience.
Please report this message and include the following information to us.
Thank you very much!

URL: http://excashier-sandbox.dl.alipaydev.com/standard/auth.htm?payOrderId=451a320ca38641729ed983e9ace73143.00
Server: excashier-36.gz00z.sandbox.alipay.net
Date: 2024/10/05 14:38:26

Powered by Tengine

8.2 系统有点儿忙,一会儿再试试

出现这种情况大概率是因为支付宝沙箱处于维护状态(常出现于节假日),可以过一段时间再进行尝试

在这里插入图片描述

在这里插入图片描述

8.3 退款服务不可用(Service Currently Unavailable)

执行退款操作时,如果支付宝沙箱返回以下信息,说明退款服务不可用,大概率是因为支付宝沙箱处于维护状态(常出现于节假日),可以过一段时间再进行尝试


{“alipay_trade_refund_response”:{“msg”:“Service Currently Unavailable”,“code”:“20000”,“sub_msg”:“系统异常”,“refund_fee”:“0.00”,“sub_code”:“aop.ACQ.SYSTEM_ERROR”,“send_back_fee”:“0.00”},“sign”:“c5Ct9FwyzUNhUPPYXjM6FdCtILlIzAWwx3dPSC+do0uL2aVp0QPVjnFlkN2MDApwbeuQCoRRuAnlZMocVyP0P84cUmLKrptj2p+wfkfuJXGor0KVUH7CtttIrdTLMbMHfKaPvfBqUKLy/NwKuCwAyQP2va/KwEfBna3xlIhgTobu2EN3C1s3LRFvEK+9x08XNjomG1t2ponftQLj6dag5M/UpTTDswrZY4GB74bH9YbwN7bS8NiD4gUDn+ncTZ6sjPfnJbUKOvOSmaFAaf9WmYjznj0tjcBFqMYLIezZ8NtniUktxJHOJC6GHj88Y9eLccF2fvwrq+Oj6iBR+9eXqA==”}

在这里插入图片描述

8.4 退款操作遇到Read timed out错误

执行退款操作时,如果程序抛出以下错误,是因为在与支付宝API进行通信时,读取操作超时了(可能是因为客户端与支付宝服务器之间的网络连接不稳定或速度过慢,导致数据传输超时),可以进行有限制次数的重试

com.alipay.api.AlipayApiException: java.net.SocketTimeoutException: Read timed out
at com.alipay.api.AbstractAlipayClient.doPost(AbstractAlipayClient.java:1015)
at com.alipay.api.AbstractAlipayClient._execute(AbstractAlipayClient.java:921)
at com.alipay.api.AbstractAlipayClient.execute(AbstractAlipayClient.java:395)
at com.alipay.api.AbstractAlipayClient.execute(AbstractAlipayClient.java:377)
at com.alipay.api.AbstractAlipayClient.execute(AbstractAlipayClient.java:372)
at com.alipay.api.AbstractAlipayClient.execute(AbstractAlipayClient.java:366)

9. 支付宝沙箱社区

社区地址:支付宝开发者社区-学习提问互动交流开放社区 (alipay.com)

在使用支付宝沙箱时,如果遇到了无法解决的问题,可以在社区中看看有没有解决办法

10. 支付宝沙箱手机端

除了用账号密码进行支付,也可以使用手机扫描支付,目前支付宝沙箱在手机端仅提供 Android 版本

下载地址:沙箱工具 - 开放平台 (alipay.com)

在这里插入图片描述

11. 温馨提示

  1. 如果不确定自己的 notify-url 是否能够生效,可以用 PostMan 进行测试;如果确定自己的 notify-url 能够生效,但 notify-url 对应的接口没有收到来自支付宝的异步通知,大概是是因为支付宝沙箱处于维护状态
  2. 建议使用账号密码付款,使用扫描支付有时候会出现成功支付后页面没有发生跳转的情况
  3. 在 return-url 对应的接口中,需要提前与前端沟通好支付成功后要跳转的页面
  4. 建议支付成功之后将支付宝返回的流水订单号(trade_no)保存到数据库中
  5. 构建与支付宝沙箱进行通信的 request 对象时,可以使用 setBizContent 方法,也可以使用 setBizModel 方法

12. 完整的示例代码

完整的示例代码已经放到了 Gitee 上

猜你喜欢

转载自blog.csdn.net/m0_62128476/article/details/142968104