Spring Boot 整合支付宝实现在线支付方案(沙箱环境)

发布时间:2024年01月13日

TIP:对于许多个人开发者而言,实现支付宝支付功能在以往往往意味着需要跨越复杂的商业流程。这涉及到拥有自己的网站及其备案,以及提交营业执照等一系列文档。但现在,支付宝开放平台带来了突破性的便利——通过沙箱环境,个人仅需拥有支付宝账号,就能够测试并实现支付功能,大大简化了以往繁琐的步骤。

1.理解沙箱环境

沙箱环境是支付宝开放平台特别为开发者们打造的安全且门槛低的测试环境。在这个环境中,开发者们可以自由地调用接口进行支付功能的测试,而无需担心商业资质等要求。更重要的是,这个测试环境允许开发者无需绑定和开通任何产品即可进行操作,使得支付功能变得触手可及。

利用沙箱环境的优势,开发者可以在不影响正式商业流程的同时,进行研发和测试工作。这种并行的工作模式可显著提升项目的开发效率和交付速度。而且,沙箱环境中的支付操作与真实的生产环境保持高度一致,区别仅在于需要修改一些配置信息。

在接下来的部分,我们将细致探讨如何在沙箱环境中顺利实现支付宝支付,能够轻松地在自己的项目中集成支付功能。

2.沙箱环境接入准备

TIP:在接入支付宝沙箱环境之前,有几项准备工作需要完成。让我们逐步了解如何开始。

2.1 访问开发者控制台

首先,前往支付宝沙箱应用的开发者控制台。这里是我们开始接入过程的地方。使用自己的支付宝账号登录以进入控制台。

访问地址:支付宝开放平台-沙箱环境控制台

首次访问需要进行账号注册:

在这个界面,你将见到如下图所示的页面:

2.2 获取重要信息

接下来,我们需要记录下某些关键信息,这些信息在后续配置项目时将会用到:

  • 支付宝网关地址:支付宝沙箱网关地址,开发者在沙箱环境调用 OpenAPI 发送 http(s) 请求的目标地址,需配置在 AlipayClient 中。
  • APPID:应用基本信息之一。
  • 接口加签方式:沙箱提供的默认密钥,通过公钥/证书机制,使用 RSA2 算法加密商户应用与沙箱的交互信息,保障交互安全。若不涉及资金支出类接口调用,建议使用公钥模式进行加签;若涉及资金支出类接口调用,必须使用证书模式进行加签。

这些信息如下图所示:

2.3 处理秘钥

最后,我们需要处理与秘钥相关的部分。进入秘钥管理页面后,会看到三个关键的秘钥:

  • 应用公钥
  • 应用私钥
  • 支付宝公钥

对于沙箱环境,只需要应用私钥支付宝公钥这两个秘钥。请妥善保存这些秘钥,它们在支付流程的安全校验中扮演着重要角色。

秘钥信息如下图所示:

完成这些步骤后,我们就已经做好了接入沙箱环境进行支付功能测试的准备。接下来,我们将进入配置过程,确保项目能够顺利地与支付宝沙箱环境对接。

3.接入支付宝支付的流程

当我们要在网页端集成支付宝支付功能时,关键步骤是调用支付接口 alipay.trade.page.pay,即统一收单下单并支付页面接口。此接口的调用将启动支付流程,下图是一个时序图,展示了整个支付过程的各个步骤:

调用流程如下:

  1. 商家系统调用 alipay.trade.page.pay(统一收单下单并支付页面接口)向支付宝发起支付请求,支付宝对商家请求参数进行校验,而后重新定向至用户登录页面。
  2. 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
  3. 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
  4. 若由于网络等原因,导致商家系统没有收到异步通知,商家可自行调用 alipay.trade.query(统一收单交易查询接口)查询交易以及支付信息(商家也可以直接调用该查询接口,不需要依赖异步通知)。

注意 ??:

  • 由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
  • 商家系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。详细验签规则可查看 异步通知验签
  • 接收到异步通知并验签通过后,请务必核对通知中的 app_id、out_trade_no、total_amount 等参数值是否与请求中的一致,并根据 trade_status 进行后续业务处理。
  • 在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商家端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。

简单理解上面这个流程,有两个关键点需要关注,用以确认支付是否成功:

  1. 异步通知:支付宝会向商户系统提供的回调地址发送一个异步通知,告知交易的支付结果。
  2. 支付结果查询:我们也可以主动通过调用 alipay.trade.query 接口,也就是统一收单线下交易查询接口,来查询交易的支付结果。

现在,我们将转到 SpringBoot 项目,具体实施集成支付宝支付的功能。

4.实现支付

4.1 添加 SDK 依赖

首先,在项目的pom.xml文件中加入如下依赖:

<!-- 支付宝 Java SDK -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.38.72.ALL</version>
</dependency>

添加完支付宝的 Java SDK 依赖后就能确保我们的应用程序能够使用支付宝提供的 API 接口。

4.2 创建配置类

接下来,创建一个名为 AlipayConfig 的属性配置类,它将负责加载 application.yml 文件中的支付宝配置信息:

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

    /**
     * Alipay 分配给开发者的应用ID
     */
    private String appId;

    /**
     * 支付宝公钥(支付宝生成,用于验签)
     */
    private String alipayPublicKey;

    /**
     * 应用私钥(开发者生成,用于签名)
     */
    private String appPrivateKey;

    /**
     * 支付宝网关
     */
    private String gatewayUrl;

    /**
     * 支付宝同步通知地址
     */
    private String returnUrl;

    /**
     * 支付宝异步通知地址
     */
    private String notifyUrl;

    /**
     * 支付宝返回数据格式
     */
    private String format = FormatType.JSON.getFormat();

    /**
     * 字符编码格式
     */
    private String charset = CharsetType.UTF_8.getCharset();

    /**
     * 签名算法类型
     */
    private String signType = SignType.RSA2.getType();
}

上面涉及到的枚举类如下:

/**
 * 常见参数返回格式枚举
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum FormatType {

    JSON("JSON"),
    XML("XML");

    private final String format;

    public static FormatType of(String format) {
        for (FormatType formatType : FormatType.values()) {
            if (formatType.getFormat().equals(format)) {
                return formatType;
            }
        }
        return null;
    }
}

/**
 * 常见编码枚举
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum CharsetType {

    UTF_8("UTF-8", "八位 UCS 转换格式"),
    ISO_8859_1("ISO-8859-1", "拉丁字母表No.1"),
    GBK("GBK", "国标2312的扩展"),
    GB2312("GB2312", "简体中文汉字编码标准"),
    ASCII("ASCII", "美国标准信息交换码");

    private final String charset;
    private final String description;

    public static CharsetType of(String charset) {
        for (CharsetType charsetType : CharsetType.values()) {
            if (charsetType.getCharset().equals(charset)) {
                return charsetType;
            }
        }
        return null;
    }
}

/**
 * 常见签名算法枚举
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum SignType {

    RSA("RSA", "经典的RSA签名"),
    RSA2("RSA2", "RSA签名的SHA-256版本"),
    MD5("MD5", "消息摘要算法5"),
    HMAC_SHA256("HMAC-SHA256", "带有SHA-256的密钥哈希消息认证码"),
    DSA("DSA", "数字签名算法");

    private final String type;
    private final String description;

    public static SignType of(String type) {
        for (SignType signType : SignType.values()) {
            if (signType.getType().equals(type)) {
                return signType;
            }
        }
        return null;
    }
}

确保在 application.yml 中填写所有必要的支付宝配置信息,这些信息应与你在沙箱环境中设置的信息相匹配:

alipay:
  appId: your-app-id # 支付宝应用ID
  alipayPublicKey: your-public-key # 支付宝公钥
  appPrivateKey: your-private-key # 支付宝私钥
  gatewayUrl: your-gateway-url # 支付宝网关地址
  returnUrl: your-return-url # 支付宝同步通知地址
  notifyUrl: your-notify-url # 支付宝异步通知地址

注意:在实际生产中建议将配置项配置在诸如 Nacos 的配置中心,确保数据安全。如果计划直接明文配置在 application.yml 中,至少要进行加解密处理。

最后添加支付宝请求客户端配置类 AlipayClientConfig,用于向支付宝 API 发送请求:

@Configuration
public class AlipayClientConfig {

    @Bean
    public AlipayClient alipayClient(AlipayConfig alipayConfig) {
        return new DefaultAlipayClient(
                alipayConfig.getGatewayUrl(),
                alipayConfig.getAppId(),
                alipayConfig.getAppPrivateKey(),
                alipayConfig.getFormat(),
                alipayConfig.getCharset(),
                alipayConfig.getAlipayPublicKey(),
                alipayConfig.getSignType());
    }
}

4.3 支付宝订单管理接口实现流程

首先,通过 SQL 语句创建 alipay_order 数据库表,用于存储订单的详细信息。

CREATE TABLE `alipay_order` (
    `id`                                BIGINT(20) NOT NULL AUTO_INCREMENT,
    `order_id`                      VARCHAR(64) NOT NULL COMMENT '订单ID',
    `subject`                        VARCHAR(256) DEFAULT NULL COMMENT '订单标题/商品标题/交易标题',
    `total_amount`      	    DECIMAL(10,2) DEFAULT NULL COMMENT '订单总金额',
    `trade_status`      		 VARCHAR(32) DEFAULT NULL COMMENT '交易状态',
    `trade_no`          	       VARCHAR(64) DEFAULT NULL COMMENT '支付宝交易号',
    `buyer_id`                      VARCHAR(64) DEFAULT NULL COMMENT '买家支付宝账号',
    `gmt_payment`               DATETIME DEFAULT NULL COMMENT '交易付款时间',
    `buyer_pay_amount`  	DECIMAL(10,2) DEFAULT NULL COMMENT '用户在交易中支付的金额',
    `create_time`       		   DATETIME NOT NULL COMMENT '创建时间',
    PRIMARY KEY (`ID`),
    UNIQUE KEY `UNIQ_ORDER_ID` (`order_id`),
    INDEX `IDX_TRADE_STATUS` (`trade_status`),
    INDEX `IDX_GMT_PAYMENT` (`gmt_payment`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='支付宝支付订单表';

随后,通过现有的代码生成器(这里我是用的是 MyBatis Plus 的代码生成器),基于 alipay_order 表结构自动生成相应的单表CRUD(创建、读取、更新、删除)代码。从而简化开发流程,快速构建所需的数据访问层代码。

下面给出完善后的各层代码:

AlipayOrder

/**
 * <p>
 * 支付宝支付订单表
 * </p>
 *
 * @author javgo
 * @since 2024-01-13
 */
@Getter
@Setter
@TableName("alipay_order")
@ApiModel(value = "AlipayOrder对象", description = "支付宝支付订单表")
public class AlipayOrder implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty("订单ID")
    private String orderId;

    @ApiModelProperty("订单标题/商品标题/交易标题")
    private String subject;

    @ApiModelProperty("订单总金额")
    private BigDecimal totalAmount;

    @ApiModelProperty("交易状态")
    private String tradeStatus;

    @ApiModelProperty("支付宝交易号")
    private String tradeNo;

    @ApiModelProperty("买家支付宝账号")
    private String buyerId;

    @ApiModelProperty("交易付款时间")
    private Timestamp gmtPayment;

    @ApiModelProperty("用户在交易中支付的金额")
    private BigDecimal buyerPayAmount;

    @ApiModelProperty("创建时间")
    private Timestamp createTime;
}

AlipayOrderMapper

/**
 * <p>
 * 支付宝支付订单表 Mapper 接口
 * </p>
 *
 * @author javgo
 * @since 2024-01-13
 */
public interface AlipayOrderMapper extends BaseMapper<AlipayOrder> {

}

AlipayOrderService:提供了 createOrder()getOrderInfo() 两个抽象方法。

/**
 * <p>
 * 支付宝支付订单表 服务类
 * </p>
 *
 * @author javgo
 * @since 2024-01-13
 */
public interface AlipayOrderService extends IService<AlipayOrder> {

    /**
     * 创建支付宝订单
     */
    Result<?> createOrder();

    /**
     * 支付宝回调
     *
     * @param orderId 订单的唯一标识符
     */
    Result<?> getOrderInfo(String orderId);
}

AlipayOrderServiceImpl:提供了 createOrder()getOrderInfo() 两个方法的具体实现。createOrder()方法中,我们首先生成一个唯一的订单ID,然后模拟商品数量和计算总金额,最终将订单信息插入到数据库中。getOrderInfo()方法则通过订单ID查询数据库,并返回相应的订单实例。

/**
 * <p>
 * 支付宝支付订单表 服务实现类
 * </p>
 *
 * @author javgo
 * @since 2024-01-13
 */
@Service
public class AlipayOrderServiceImpl extends ServiceImpl<AlipayOrderMapper, AlipayOrder> implements AlipayOrderService {

    @Resource
    private AlipayOrderMapper alipayOrderMapper;

    @Override
    public Result<?> createOrder() {
        // 生成订单号
        String orderId = NoUtil.getOrderNo();
        AlipayOrder alipayOrder = new AlipayOrder();
        alipayOrder.setOrderId(orderId);
        // 设置订单标题
        int quantity = RandomUtil.randomInt(1, 10);
        alipayOrder.setSubject("测试订单" + quantity + "个");
        // 设置总金额
        alipayOrder.setTotalAmount(new BigDecimal(50).multiply(new BigDecimal(quantity)));
        // 设置交易状态
        alipayOrder.setTradeStatus(TradeStatusType.WAIT_BUYER_PAY.getCode());
        alipayOrder.setCreateTime(new Timestamp(new Date().getTime()));

        alipayOrderMapper.insert(alipayOrder);
        AlipayOrderDTO alipayOrderDTO = DataTransferUtil.shallowCopy(alipayOrder, AlipayOrderDTO.class);
        return Result.success(alipayOrderDTO, "创建订单成功");
    }

    @Override
    public Result<?> getOrderInfo(String orderId) {
        QueryWrapper<AlipayOrder> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_id", orderId);
        List<AlipayOrder> alipayOrders = alipayOrderMapper.selectList(queryWrapper);
        if (CollectionUtils.isNotEmpty(alipayOrders)) {
            AlipayOrder alipayOrder = alipayOrders.get(0);
            AlipayOrderDTO alipayOrderDTO = DataTransferUtil.shallowCopy(alipayOrder, AlipayOrderDTO.class);
            return Result.success(alipayOrderDTO, "查询订单成功");
        }
        return Result.failed("查询订单失败");
    }
}

AlipayOrderServiceImpl 涉及到的订单交易状态枚举 TradeStatusType

/**
 * 订单交易状态
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum TradeStatusType {

    WAIT_BUYER_PAY("1", "WAIT_BUYER_PAY", "交易创建,等待买家付款"),
    TRADE_CLOSED("2", "TRADE_CLOSED", "未付款交易超时关闭,或支付完成后全额退款"),
    TRADE_SUCCESS("3", "TRADE_SUCCESS", "交易支付成功"),
    TRADE_FINISHED("4", "TRADE_FINISHED", "交易结束,不可退款"),
    TRADE_FAILED("5", "TRADE_FAILED", "支付失败");

    private final String code;
    private final String status;
    private final String description;

    public static TradeStatusType of(String status) {
        for (TradeStatusType tradeStatus : values()) {
            if (tradeStatus.getStatus().equals(status)) {
                return tradeStatus;
            }
        }
        return null;
    }
}

AlipayOrderServiceImpl 涉及到的单号工具类 NoUtil

/**
 * 单号工具类(规则:时间戳 + 随机数)(注意:一定概率会重复,数据库还会做一层唯一性校验,因此可以忽略)
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class NoUtil {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmssSSS");

    /**
     * 生成 6 位随机数
     */
    public static String getVerCode() {
        return RandomUtil.randomNumbers(6);
    }

    /**
     * 生成订单号
     */
    public static synchronized String getOrderNo() {
        return generateTimestamp() + RandomUtil.randomNumbers(3);
    }

    /**
     * 生成流水号
     */
    public static String getSerialNumber() {
        return generateTimestamp() + RandomUtil.randomNumbers(4);
    }

    /**
     * 生成时间戳
     */
    private static String generateTimestamp() {
        return DATE_FORMAT.format(new Date());
    }
}

AlipayOrderController:作为支付宝订单管理的入口点,向外界提供API接口。该控制器定义了两个主要方法:createOrder() getOrderInfo()createOrder() 方法用于创建新的订单,而 getOrderInfo() 方法用于根据订单ID查询订单详情。

/**
 * <p>
 * 支付宝支付订单表 前端控制器
 * </p>
 *
 * @author javgo
 * @since 2024-01-13
 */
@RestController
@RequestMapping("/alipayOrder")
public class AlipayOrderController {

    @Resource
    private AlipayOrderService alipayOrderService;

    @ApiOperation("创建订单")
    @PostMapping("/createOrder")
    public Result<?> createOrder() {
        return alipayOrderService.createOrder();
    }

    @ApiOperation("获取订单信息")
    @GetMapping("/getOrderInfo")
    public Result<?> getOrderInfo(String orderId) {
        return alipayOrderService.getOrderInfo(orderId);
    }
}

4.4 支付宝支付接口实现流程

接下来我们开始编写支付宝支付接口相关实现,首先准备一个 AlipayService 定义需要用到的抽象方法。

/**
 * 支付宝支付服务类(支付宝支付流程中的主要操作方法)
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
public interface AlipayService {

    /**
     * 发起支付宝电脑网站支付请求
     *
     * @param aliPayReq 支付请求参数
     * @return Result 返回支付结果,包含支付表单或错误信息
     */
    Result<?> initiatePcPayment(AliPayReq aliPayReq);

    /**
     * 发起支付宝手机网站支付请求
     *
     * @param aliPayReq 支付请求参数
     * @return Result 返回支付结果,包含支付表单或错误信息
     */
    Result<?> initiateMobilePayment(AliPayReq aliPayReq);

    /**
     * 处理支付宝支付结果的异步通知
     *
     * @param params 从支付宝回调接收到的参数集合
     * @return Result 返回处理结果,成功或失败
     */
    Result<?> processPaymentNotification(Map<String, String> params);

    /**
     * 查询支付宝交易的支付状态
     *
     * @param outTradeNo 商户订单号
     * @param tradeNo    支付宝交易号
     * @return Result 返回查询结果,包含交易状态或错误信息
     */
    Result<?> queryPaymentStatus(String outTradeNo, String tradeNo);
}

对应实现类如下:

/**
 * 支付宝支付服务实现类
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Slf4j
@Service
public class AlipayServiceImpl implements AlipayService {

    @Resource
    private AlipayConfig alipayConfig;

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayOrderService alipayOrderService;

    /**
     * 电脑网站支付产品编号(固定值)
     */
    private static final String PC_PRODUCT_CODE = "FAST_INSTANT_TRADE_PAY";

    /**
     * 手机网站支付产品编号(固定值)
     */
    private static final String MOBILE_PRODUCT_CODE = "QUICK_WAP_WAY";

    /**
     * 交易结算信息
     */
    private static final String TRADE_SETTLE_INFO = "trade_settle_info";

    @Override
    public Result<?> initiatePcPayment(AliPayReq aliPayReq) {
        return initiatePayment(aliPayReq, PC_PRODUCT_CODE, "支付宝 PC 端支付请求失败", "支付宝 PC 端支付请求成功");
    }

    @Override
    public Result<?> initiateMobilePayment(AliPayReq aliPayReq) {
        return initiatePayment(aliPayReq, MOBILE_PRODUCT_CODE, "支付宝手机端支付请求失败", "支付宝手机端支付请求成功");
    }

    @Override
    public Result<?> processPaymentNotification(Map<String, String> params) {
        String result = "failure";
        boolean signVerified = false;
        try {
            // 1. 验证签名
            signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType());
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return Result.failed("支付宝支付结果通知签名验证失败");
        }
        if (signVerified) {
            // 2. 验证交易状态
            String tradeStatus = params.get("trade_status");
            if ("TRADE_SUCCESS".equals(tradeStatus)) {
                // 3. 更新订单状态
                result = "success";
                AlipayOrder alipayOrder = BeanUtil.mapToBean(params, AlipayOrder.class, true, null);
                alipayOrder.setOrderId(params.get("out_trade_no"));
                alipayOrder.setTradeStatus(TradeStatusType.TRADE_SUCCESS.getCode());
                log.info("支付宝支付结果通知参数:{}", JSON.toJSONString(alipayOrder));
                QueryWrapper<AlipayOrder> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("order_id", alipayOrder.getOrderId());
                alipayOrderService.update(alipayOrder, queryWrapper);
                log.info("支付宝订单交易成功,交易状态:{}", tradeStatus);
            } else {
                log.error("支付宝订单交易失败,交易状态:{}", tradeStatus);
            }
        } else {
            log.error("支付宝支付结果通知签名验证失败");
        }
        return result.equals("success") ? Result.success("支付宝支付结果通知处理成功") : Result.failed("支付宝支付结果通知处理失败");
    }

    @Override
    public Result<?> queryPaymentStatus(String outTradeNo, String tradeNo) {
        // 1. 创建支付宝支付查询请求
        AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
        // 2. 设置支付宝支付请求参数
        JSONObject bizContent = new JSONObject();
        if (StringUtils.isNotEmpty(outTradeNo)) {
            // 商户订单号
            bizContent.put("out_trade_no", outTradeNo);
        }
        if (StringUtils.isNotEmpty(tradeNo)) {
            // 支付宝交易号
            bizContent.put("trade_no", tradeNo);
        }
        // 交易结算信息
        String[] queryOptions = new String[]{TRADE_SETTLE_INFO};
        bizContent.put("query_options", queryOptions);
        alipayRequest.setBizContent(bizContent.toJSONString());
        // 3. 发起支付宝支付查询请求
        AlipayTradeQueryResponse alipayResponse = null;
        try {
            alipayResponse = alipayClient.execute(alipayRequest);
        } catch (AlipayApiException e) {
            log.error("支付宝支付查询请求失败", e);
            return Result.failed("支付宝支付查询请求失败");
        }
        // 4. 处理支付宝支付查询结果(支付状态见 TradeStatusType)
        if (alipayResponse.isSuccess()) {
            log.info("支付宝支付查询请求成功");
            return Result.success(alipayResponse.getTradeStatus(), "支付宝支付查询请求成功");
        } else {
            log.error("支付宝支付查询请求失败");
            return Result.failed(alipayResponse.getTradeStatus(), "支付宝支付查询请求失败");
        }
    }

    /**
     * 发起支付宝支付请求(电脑网站支付和手机网站支付)
     * @param aliPayReq 支付请求参数
     * @param productCode 产品编号
     * @param failMessage 失败提示信息
     * @param successMessage 成功提示信息
     * @return Result 返回支付结果,包含支付表单或错误信息
     */
    private Result<?> initiatePayment(AliPayReq aliPayReq, String productCode, String failMessage, String successMessage) {
        // 1. 创建支付宝支付请求
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        // 2. 设置支付宝支付同步通知页面和异步通知地址
        setNotifyAndReturnUrl(alipayRequest);

        // 3. 设置支付宝支付请求参数
        JSONObject bizContent = constructBizContent(aliPayReq, productCode);
        alipayRequest.setBizContent(bizContent.toJSONString());

        String formHtml = null;
        try {
            formHtml = alipayClient.pageExecute(alipayRequest).getBody();
            return Result.success(formHtml, successMessage);
        } catch (Exception e) {
            log.error(failMessage, e);
            return Result.failed(formHtml, failMessage);
        }
    }

    /**
     * 设置支付宝支付同步通知页面和异步通知地址
     * @param alipayRequest 支付宝支付请求
     */
    private void setNotifyAndReturnUrl(AlipayTradePagePayRequest alipayRequest) {
        // 设置同步通知页面
        if (StringUtils.isNotEmpty(alipayConfig.getReturnUrl())) {
            alipayRequest.setReturnUrl(alipayConfig.getReturnUrl());
        }
        // 设置异步通知地址
        if (StringUtils.isNotEmpty(alipayConfig.getNotifyUrl())) {
            alipayRequest.setNotifyUrl(alipayConfig.getNotifyUrl());
        }
    }

    /**
     * 构造支付宝支付请求参数
     *
     * @param aliPayReq   支付请求参数
     * @param productCode 产品编号
     * @return JSONObject 支付宝支付请求参数
     */
    private JSONObject constructBizContent(AliPayReq aliPayReq, String productCode) {
        JSONObject bizContent = new JSONObject();
        // 订单标题(不可以使用特殊字符)
        bizContent.put("subject", aliPayReq.getSubject());
        // 商户订单号(由商家自定义的唯一订单号)
        bizContent.put("out_trade_no", aliPayReq.getOutTradeNo());
        // 订单总金额(元),最小值为0.01
        bizContent.put("total_amount", aliPayReq.getTotalAmount());
        // 销售产品码,与支付宝签约的产品码名称(固定值)
        bizContent.put("product_code", productCode);
        return bizContent;
    }
}

最后编写支付宝支付控制器:

/**
 * 支付宝支付控制器
 *
 * @author javgo.cn
 * @date 2024/1/13
 */
@Controller
@RequestMapping("/alipay")
public class AlipayController {

    @Resource
    private AlipayConfig alipayConfig;

    @Resource
    private AlipayService alipayService;

    @ApiOperation("支付宝电脑网站支付")
    @GetMapping("/pcPayment")
    public void pcPayment(AliPayReq aliPayReq, HttpServletResponse response) throws IOException {
        response.setContentType(ContentType.TEXT_HTML.getContentType() + ";charset=" + alipayConfig.getCharset());
        Result<?> result = alipayService.initiatePcPayment(aliPayReq);
        response.getWriter().write(result.getData().toString());
        response.getWriter().flush();
        response.getWriter().close();
    }

    @ApiOperation("支付宝手机网站支付")
    @GetMapping("/mobilePayment")
    public void mobilePayment(AliPayReq aliPayReq, HttpServletResponse response) throws IOException {
        response.setContentType(ContentType.TEXT_HTML.getContentType() + ";charset=" + alipayConfig.getCharset());
        Result<?> result = alipayService.initiateMobilePayment(aliPayReq);
        response.getWriter().write(result.getData().toString());
        response.getWriter().flush();
        response.getWriter().close();
    }

    @ApiOperation("支付宝支付通知")
    @PostMapping("/notify")
    @ResponseBody
    public Result<?> processPaymentNotification(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        Map<String, String[]> requestParams = request.getParameterMap();
        requestParams.keySet().forEach(r -> params.put(r, request.getParameter(r)));
        return alipayService.processPaymentNotification(params);
    }

    @ApiOperation("查询支付状态")
    @GetMapping("/queryPaymentStatus")
    @ResponseBody
    public Result<?> queryPaymentStatus(String outTradeNo, String tradeNo) {
        return alipayService.queryPaymentStatus(outTradeNo, tradeNo);
    }
}

5.支付宝支付功能演示

这里为了进行功能演示,我们先快速配置一下项目的 API 文档,这里选择 Knife4j。

首先在 pom.xml 文件引入如下依赖:

<!-- knife4j API 文档 -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

然后编写 Knife4jConfig 配置类即可:

@Configuration
@EnableSwagger2
@EnableKnife4j
public class Knife4jConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false) // 关闭默认的响应信息
                .apiInfo(apiInfo()) // 用于定义 api 文档汇总信息
                .select() // 选择那些路径和 api 会生成 document
                .apis(RequestHandlerSelectors.basePackage("cn.edu.just.hostpital.system.controller")) // 指定扫描的包路径
                .paths(PathSelectors.any()) // 指定路径处理 PathSelectors.any() 表示所有路径
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("支付宝支付 API 文档")
                .description("支付宝支付 API 文档")
                .contact(new Contact("JavGO", "http://www.javgo.cn", "javgocn@gmail.com"))
                .version("1.0")
                .build();
    }
}

注意:生产环境建议关闭 Knife4j,否则会导致安全问题,例如 API 泄露导致的恶意模拟请求。

接下来,启动你的 SpringBoot 项目,确保一切准备就绪。

打开浏览器,输入 http://localhost:8080/doc.html 访问项目的 Knife4j 接口文档页面。Knife4j 提供了一个可视化的接口,让你能够方便地测试 API。

通过创建订单接口,生成新的订单。操作完成后,不要忘记记录下返回结果中的关键参数,特别是 orderId,因为它将作为支付API中的 outTradeNo 参数。

orderId=20240113174650548863
subject=测试订单9个
totalAmount=450

接下来,调用支付宝电脑网站支付接口,并传入先前记下的三个参数。

复制生成的请求地址,并在新的浏览器窗口中打开该地址。这将引导你进入支付宝的支付页面。

在支付宝支付页面,使用沙箱环境的测试账号和密码完成支付。

获取沙箱环境的测试账号和密码,可以在支付宝沙箱应用的开发者控制台找到,确保使用买家账号。

登录后使用支付密码进行支付:

支付成功:

支付成功后,使用支付宝提供的统一收单线下交易查询接口,来确认支付结果。如果返回值是 TRADE_SUCCESS,则表明支付已经成功。

image-20240113180341066

如果你需要在移动端实现支付,可以通过调用支付宝移动端支付接口。同样地,传入必要的三个参数,并复制生成的 RequestURL 即可。

6.总结

上面我们演示了如何在 SpringBoot 项目中实现支付宝支付流程。使用沙箱环境,开发者可以在没有实际交易的情况下测试和完善支付功能。这一过程既简化了开发,也为将来切换到正式环境做好了准备。

通过这个例子,我们看到了如何将支付功能无缝集成到你的应用中。无论是对于初学者,还是有经验的开发者,支付宝沙箱环境都是一个宝贵的资源,能让你安全地探索和实现电子商务解决方案。


参考资料:

  • 支付宝官方文档:https://opendocs.alipay.com/open/065yhr
  • 电脑网站支付快速接入:https://opendocs.alipay.com/open/270/105899
  • 手机网站支付快速接入:https://opendocs.alipay.com/open/203/105285
文章来源:https://blog.csdn.net/ly1347889755/article/details/135573558
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。