冬雨财经 发表于 2024-11-28 22:12:07

付出宝沙箱版(什么是付出宝沙箱、配置付出宝沙箱、配置内网穿透、在Spring

0. 媒介

许多项目都需要有付出功能,但是对接第三方付出又需要企业资质,作为个人开发者,达不到这个条件,但付出功能是项目中一个很紧张的的部门,面试官也比较喜好问,如果面试官问与付出功能的相关的问题时,我们说由于没有企业资质,跳过了付出功能,那就错失了一次在面试官面前表现本身的机会
本日为大家介绍的付出宝沙箱正是为相识决这一痛点,通过使用付出宝沙箱,我们就能够在项目中模拟真正的付出,让我们的项目锦上添花(使用付出宝沙箱无需企业资质,只需要一个付出宝账号)
1. 什么是付出宝沙箱

付出宝沙箱(Alipay Sandbox)是付出宝为开发者提供的一个模拟真实付出宝情况的测试平台。开发者可以在沙箱情况中进行应用程序的开发和测试,无需使用真实的付出宝账户进行买卖业务
以下是付出宝沙箱的一些特点和用途:

[*]安全隔离:沙箱情况与生产情况是隔离的,开发者可以在不影响真实用户数据的情况下进行测试
[*]模拟买卖业务:沙箱允许开发者模拟各种买卖业务场景,如付出、退款、转账等,以便验证应用程序的功能
[*]测试账号:付出宝沙箱提供测试用的商户ID和应用ID,以及模拟的买家和卖家账户,用于测试付出流程
[*]API调用:开发者可以使用沙箱情况中的API进行接口调用,测试接口的相应和数据返回是否符合预期
[*]问题排查:在沙箱中测试可以资助开发者发现和解决在集成付出宝付出时可能出现的问题
[*]文档和工具:付出宝沙箱提供详细的文档和测试工具,资助开发者更快地熟悉和掌握付出宝API的使用
付出宝沙箱对于希望集成付出宝付出服务的开发者来说是一个非常有用的工具,它能够资助开发者在不影响实际业务的情况下,高效、安全地完成付出功能的开发和测试工作
2. 配置付出宝沙箱

付出宝沙箱官网:沙箱应用 - 开放平台 (打开官网后需要用真实的付出宝账号进行登录)
2.1 沙箱应用的应用信息(获取app-id和gateway-url)

APPID 和 付出宝网关所在 字段下面会用到
https://i-blog.csdnimg.cn/direct/d009e703c640438a978bff06267cfebb.png
2.2 沙箱账号的商家信息和买家信息

买家账号在付款时会用到
https://i-blog.csdnimg.cn/direct/313c942310b74cd094357d776ec19a23.png
2.3 下载秘钥工具

下载所在:密钥工具下载 - 付出宝文档中央 (alipay.com)
https://i-blog.csdnimg.cn/direct/79e57e0475414b2292e5420e43035fd4.png
   请不要安装在含有空格的目次路径下,否则会导致公私钥乱码的问题
2.4 天生秘钥(获取private-key)

https://i-blog.csdnimg.cn/direct/8f1ee3f3246e452aa3930315f867153e.png
秘钥天生乐成的界面(应用私钥对应 private-key)
https://i-blog.csdnimg.cn/direct/257b0849a6b846e2bf59cd0fba630f88.png
2.5 配置秘钥(获取alipay-public-key)

配置所在:沙箱应用 - 开放平台 (alipay.com)
点击自定义秘钥,再点击公钥模式的设置并检察
https://i-blog.csdnimg.cn/direct/8d32f5d5417d49b786ef451ca50d374a.png
将刚才天生的应用公钥复制过来,接着点击生存按钮
https://i-blog.csdnimg.cn/direct/737fced4c7994159bc25c605b33eec56.png
最后点击确定按钮
https://i-blog.csdnimg.cn/direct/ef1ac88a80fa4cf6b9984f92d4fea9a2.png
将得到的付出宝公钥生存下来(付出宝公钥对应 alipay-public-key)
3. 配置内网穿透

3.1 使用cpolar实现内网穿透

我们使用 cpolar 来实现内网穿透,cpolar 的使用教程可参考我的另一篇博文:使用cpolar实现内网穿透,将Web项目部署到公网上
3.2 创建隧道(获取notify-url)

在欣赏器输入以下网址,创建隧道
http://localhost:9200/#/tunnels/create
https://i-blog.csdnimg.cn/direct/e58dd127c25e47ff91449440cacae502.png
创建隧道后会有一个公网所在,下面会用到
https://i-blog.csdnimg.cn/direct/0671808c98b84321a7c31d60e8f56121.png
   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', '')
      }
    }
}
})
关键代码
https://i-blog.csdnimg.cn/direct/2207b627c2f54406b0fb27ec908682a7.png
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.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>
关键代码如下
https://i-blog.csdnimg.cn/direct/ee1e70125902447184f2d06cbf40d19f.png
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. 付出宝沙箱乐成付出的整个过程

   

[*]可以使用账号暗码付款,也可以扫描付款
[*]发起使用账号暗码付款,使用扫描付出有时候会出现乐成付出后页面没有发生跳转的情况
以下是乐成付出的整个过程的截图
https://i-blog.csdnimg.cn/direct/ddd36ea7a6a84f6e8ad2a2ed32a32ca7.png
付出暗码默认是111111
https://i-blog.csdnimg.cn/direct/a57acc10da11426da0acb11819ff5433.png
https://i-blog.csdnimg.cn/direct/d8d7c8d0150847969e73b1d1cd39557c.png
https://i-blog.csdnimg.cn/direct/8455a55bfce34f5b9d58dcc429ad4e1f.png
https://i-blog.csdnimg.cn/direct/c83d3c5df7a041cb8b0baf2b03cd00b8.png
付出乐成后付出宝会返回一个流水订单号(trade_no),退款时需要用到流水订单号,可以将流水订单号生存到数据库中
7. 付出宝沙箱的退款操作

   

[*]退款可以使用商户订单号(out_trade_no),也可以使用付出宝返回的流水订单号(trade_no)
[*]可以多次执行退款操作,但是账号余额不会发生变化如果是第一次退款,AlipayTradeRefundResponse 的 fundChange 属性取值为 “Y”,如果不是第一次退款,AlipayTradeRefundResponse 的 fundChange 字段的取值为 “N”,可以根据 fundChange 字段美满业务逻辑
在实际开发中,与商品有关的信息是需要从后端查出来的,为了方便测试,由前端传给后端(退款的具体代码参考本文的编写controller(设置notify-url和return-url)部门)
https://i-blog.csdnimg.cn/direct/246b1e4471ee4a3d9b6c456610e7732d.png
8. 可能遇到的问题

8.1 504 Gateway Time-out

   出现这种情况大概率是由于付出宝沙箱处于维护状态(常出现于节假日),可以过一段时间再进行实验
https://i-blog.csdnimg.cn/direct/5573464841a94221945913e66cf3dc48.png
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.00Server:excashier-36.gz00z.sandbox.alipay.netDate:2024/10/05 14:38:26 Powered by Tengine
8.2 系统有点儿忙,一会儿再试试

   出现这种情况大概率是由于付出宝沙箱处于维护状态(常出现于节假日),可以过一段时间再进行实验
https://i-blog.csdnimg.cn/direct/c71a2a613e4e4179a281d25f5a2a6810.png
https://i-blog.csdnimg.cn/direct/4612d02080fb4d078e188d63cbb876a8.png
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==”}
https://i-blog.csdnimg.cn/direct/d92fdaf3933b4981a093822a918a77ab.png
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)
https://i-blog.csdnimg.cn/direct/f615ea5eb99a45c981e748aebfc363b8.png
11. 温馨提示


[*]如果不确定本身的 notify-url 是否能够生效,可以用 PostMan 进行测试;如果确定本身的 notify-url 能够生效,但 notify-url 对应的接口没有收到来自付出宝的异步通知,大概是是由于付出宝沙箱处于维护状态
[*]发起使用账号暗码付款,使用扫描付出有时候会出现乐成付出后页面没有发生跳转的情况
[*]在 return-url 对应的接口中,需要提前与前端沟通好付出乐成后要跳转的页面
[*]发起付出乐成之后将付出宝返回的流水订单号(trade_no)生存到数据库中
[*]构建与付出宝沙箱进行通信的 request 对象时,可以使用 setBizContent 方法,也可以使用 setBizModel 方法
12. 完备的示例代码

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


[*]后端:alipay-sandbox-backend
[*]前端:alipay-sandbox-frontend

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 付出宝沙箱版(什么是付出宝沙箱、配置付出宝沙箱、配置内网穿透、在Spring